-
-
Save MaybeThisIsRu/98ae4883467081f582ba15a13c22524f to your computer and use it in GitHub Desktop.
<!-- | |
Problem statement: | |
I'm writing an interactive category field. | |
I have three major things on play here: | |
1. A text input that is described by a paragraph above it | |
2. A datalist of categories received from the server | |
3. A list of checked checkboxes for each new category | |
Why #3? Well, I don't want to have to manage my form on the frontend before sending it to the backend. A checkbox with the same name attribute automatically takes care of sending multiple categories. | |
1. My main concern is indicating to users that the list of entered categories is *here* -- how do I connect the two? | |
2. A secondary concern is also making sense of what this "x" button *does* and for which particular value. | |
--> | |
<script> | |
import { onMount } from "svelte"; | |
let categories = []; | |
$: userEnteredCategories = []; | |
let currentCategoryName = ""; | |
onMount(async () => { | |
const response = await fetch( | |
"/api/v1/micropub/category/" | |
).catch((error) => console.error(error)); | |
const data = await response | |
.json() | |
.catch((error) => console.error(error)); | |
if (data && data.error) console.error(data.error_description); | |
else categories = data; | |
}); | |
const handleKeyUp = (event) => { | |
if (event.code === "Comma") { | |
userEnteredCategories = [ | |
...userEnteredCategories, | |
currentCategoryName.slice(0, -1), | |
]; | |
currentCategoryName = ""; | |
} | |
}; | |
const deleteUserCategory = (event) => { | |
userEnteredCategories = userEnteredCategories.filter((cat) => | |
event.target.dataset.value === cat ? false : true | |
); | |
}; | |
</script> | |
<input | |
type="text" | |
id="field-group__category" | |
aria-describedby="field-group__description--category" | |
list="category--suggestions" | |
on:keyup|preventDefault={handleKeyUp} | |
bind:value="{currentCategoryName}" | |
/> | |
<datalist class="category" id="category--suggestions"> | |
{#each categories as category} | |
<option value="{category}">{category}</option> | |
{/each} | |
</datalist> | |
<!-- To avoid handling the form manually just for categories, we build a list of checkboxes and have HTML do the heavy lifting for us --> | |
<!-- ! Concerns around accessibility --> | |
<div class="category" id="category--user"> | |
{#each userEnteredCategories as userEnteredCategory, i} | |
<div class="user-category"> | |
<button | |
class="user-category__remove" | |
on:click|preventDefault={deleteUserCategory} | |
data-value="{userEnteredCategory}" | |
> | |
x | |
</button> | |
<label class="user-category__name" for="{userEnteredCategory}_{i}" | |
>{userEnteredCategory}</label | |
> | |
<input | |
type="checkbox" | |
name="category" | |
id="{userEnteredCategory}_{i}" | |
value="{userEnteredCategory}" | |
class="user-category__value hidden" | |
checked | |
/> | |
</div> | |
{/each} | |
</div> |
Very well done, @meduzen, thank you!
Just FYI, there really are two sources of data here -- a completely new category that does not exist in the datalist
, and a category that does exist in the datalist
because the user has previously entered it while creating a new post and the server returned it to us the next time the user opens a post form.
I skimmed through ARIA live regions and it may be useful here in some capacity, but I'll need to figure out how exactly. I would love to be able to clarify to a screen-reader that a new category has been "added" once they hit Enter
. So it does seem worth a go, thank you!
Presenting a list of checkboxes is something I'd rather not do at this stage, but maybe in the future? Some people on the IndieWeb have admitted to having 3000-4000 categories (and even more...). There's also plenty of ongoing development around the query API so I'd rather just rely on a datalist
until this matures further -- along with Celestial so I have more time to devote to niche decisions like these. :)
A fieldset and a label do exist -- they're just not a part of the Svelte component because they do not change based on user input. The Svelte component only has the dynamic HTML portion. However, I think I did use fieldset incorrectly here, so I'll correct that. 😉 Also good tip on the type="button"
attribute, didn't realize that. 😅 Agreed on using lists instead.
For the "x" button, this is what gov.uk has done:
<div class="facet-tag">
<p class="facet-tag__text">Academy for Social Justice</p>
<button type="button" class="facet-tag__remove" aria-label="Remove filter Academy for Social Justice" data-module="remove-filter-link" data-track-label="Academy for Social Justice" data-facet="organisations" data-value="academy-for-social-justice" data-name="">✕</button> </div>
It appears they concluded an aria-label
is sufficient for the button. I do want to use this because the space is so limited (at least for now) in this area of the page. What do you think? An aria-label
won't be read out to the average user though...
P.s. I'm also no good with ARIA, unfortunately.
Oh, I skipped the part where it has to create a new category, indeed. That makes it more complex. 😅
I’m not fan of a x
as content for the button
, but thinking twice, their solution is okay-ish because :
- they properly ARIA-ed it, I think;
- it’s not a the x letter, it’s a
✕
(multiplication sign) but I guess most humans won’t read it “multiplication sign”. 😄
So, to sum up: adding aria-label
will more likely be the right way to do it. 👍
What I usually do in that situation is this (but I’m gonna reconsider):
<button>
<span class="visually-hidden>real button label</span <!-- this is read by screen readers -->
<svg></svg> <!-- fancy “x” icon -->
</button>
I have this CSS utility class in all projects, which is the ultimate way to hide content visually, but not for screen readers.
.visually-hidden:not(:focus):not(:active) {
position: absolute;
size: 1px 1px;
margin: -1px;
padding: 0;
clip: rect(0 0 0 0);
clip-path: inset(100%);
clip-path: polygon(0 0, 0 0, 0 0, 0 0);
overflow: hidden;
border: 0;
white-space: nowrap;
}
This way there’s no doubt about the semantic vs a x or whatever else label we could have, for both readers and maintainers. I often use it to hide headings that are not shown but should structurally exist in the DOM for understanding, like:
<section>
<h2 class="visually-hidden">title that is not visible but still read by screen readers</h2>
<p>some content</p>
</section>
However, Scott O’Hara explains when we should use ARIA and when we should use something else like this class, and I’m probably not using it correctly in some (most?) cases. 😅
(Okay, first I’m bad with ARIA, but rather good with regular HTML, but let’s go!)
Looks like a really complex challenge. So worth trying! 👍
UK gov has a fantastic read about their journey to fill a similar use case… and they have implemented the multi-selection on their search page, but they also explain to have backpedaled on some others.
They seem to use a ARIA live regions for the output (the list of selected tags), in which they use a list of
<button>
that delete the selection (“Remove filter xxxx”, probably partly answering your 2nd question: “Remove category xxx”). But you can also still use the checkboxes in the left panel in order to select/unselect a service.I don’t know if the use of
aria-live
answers your first question, but the main difference is in the left panel: instead of relying on whatdatalist
enabled for text completion, which creates the difficulty you encounter (connecting thedatalist
autocompletion to a generatedbutton
+ checkbox), they use a regular textinput
(with nodatalist
) to filter the list of checkboxes that already exists in the HTML (= better progressive enhancement).It’s the main difference: no
datalist
! Instead of having to connect thedatalist
native stuff (which has no HTML, so nothing you can connect) to a list of checkboxes, they have to connect a list of checkboxes to the deletebutton
in the live region, which I think is more reasonable.Putting aside those considerations for which I’m no expert at all, here are some issues I see in the HTML, and a proposal improvement after.
input
text needs alabel
. Right now, it’s a field without clear context: the sentence before explains where we are (which is great) but I fear it isn’t enough.preventDefault
on thedelete
button, thetype="button"
attribute should do the trick. By default, a button type issubmit
, that’s probably why you callpreventDefault
on it. But withtype="button"
, using the button won’t submit theform
.Proposal (without the GOV UK way of doing it):
I hope this can help you put thing in perspective. I learned a bit of ARIA while searching and writing this. 😅