I am currently tasked with making a current dynamic search accessible. Currently the search field all seems to work and announce. However when triggered the list propagates but does not seem to interact with the screen reader. I did not create this component just tasked with making it a11y compatible.
For example if I type the letter "d" in the search field the list shows all elements that match that. But on arrowDown the screen reader does not announce the value to select. I am curious if there is something I am missing here to allow that to happen, or perhaps my screen reader is just not as advanced as others.
The code is in React and as follows:
<div id={id} className={classNames.join(" ")} onClick={focusInput}>
<a
className="search-icon"
onClick={(ev) => {
onClickLookupIcon();
ev.stopPropagation();
}}
/>
<div className={"styled-scrollbar " + itemsClassNames.join(" ")}>{itemEls}</div>
{results.length > 0 && (
<ul className="auto-complete" style={{ top: allowMultiple ? "42px" : "22px" }}>
{results.map((it, i) => (
<li
key={i}
className={i === selectedSearchIndex ? "selected" : ""}
//use mousedown so that this triggers before the "blur" event on the input which would trigger selecting the `selectedSearchIndex` item instead of the clicked item
onMouseDown={(ev) => {
addOrReplaceItem({ type: "valid", value: it });
resetLookup();
ev.stopPropagation();
}}
>
<SearchDelegate {...it} />
</li>
))}
</ul>
)}
</div>
Expanding some code to see more re {itemEls}
const itemEls = value.map((it) => {
const itemId = it.type === "valid" ? getRecordId(it.value) : it.newItemId;
return currEditingRecord === itemId ? (
inputEl
) : (
<div
key={itemId}
className="hg-lookup-item-wrapper"
onClick={(ev) => {
ev.stopPropagation();
//edit the clicked-on item unless we're already editing it
if (currEditingRecord === itemId) return;
setSearchString(it.type === "valid" ? getItemSearchText(it.value) : it.searchString);
setCurrEditingRecord(itemId);
//select all text when clicking on existing item
setTimeout(() => inputRef.current && inputRef.current.select());
}}
>
{it.type === "valid" ? (
<>
<Delegate item={it.value} />
<a className="hg-remove-icon" onClick={doRemove(itemId)} />
</>
) : (
<PendingResult<T>
key={it.newItemId}
loadMatches={loadMatches}
searchString={it.searchString}
onValueSelected={(val) => {
onChange(
value.map((v) =>
v.type === "valid" || v.newItemId !== it.newItemId || valueExists(val)
? v
: { type: "valid", value: val },
),
);
}}
/>
)}
</div>
);
});
and lastly the inputEl
const inputEl = (
<div key="INPUT" className="hg-lookup-item-wrapper">
<AutosizeInput
id={`${id}-input`}
type="text"
ref={inputRef}
aria-autocomplete="list"
aria-controls={`${id}-listbox`}
value={searchString || ""}
onBlur={() => {
takeSelectedResultOrSetupPendingResult(false);
resetLookup();
}}
autoComplete={"off"}
{...{ disabled }}
onChange={(ev) => {
if (!allowMultiple && isAddingNewRecord() && value.length === 1) return;
setSearchString(ev.currentTarget.value);
}}
onKeyDown={onKeyDown}
/>
</div>
);
on mouseDown the screen reader does not announce the value to select
So you're using a mouse in addition to a screen reader? While there are some users that have that combination, the majority of screen reader users use only the keyboard and not a mouse. Does your component work with just a keyboard? Can you tab to it and the label for the component is read (WCAG 4.1.2). If you type d and a list appears below it, can you arrow down to an item and press enter to select it (WCAG 2.1.1)?
I would initially focus on making sure everything works from a keyboard first, and then augment that work to ensure things are announced by a screen reader. For example, typing d shows a list of options. Are the number of items in the list announced?
While your object might not be a combobox, you should look at the combobox pattern, especially the "autocomplete" options.
https://www.w3.org/TR/wai-aria-practices/#combobox
And check out example 1 on:
https://www.w3.org/TR/wai-aria-practices/examples/combobox/aria1.1pattern/listbox-combo.html#ex1_label
Related
I want to have an edit mode to each field in a div that is mapped out from an array that I fetch from firbase. I succeeded doing that by conditioning the rendered field to the value of a boolean (editField) which I then manipulate using useState, like so:
in the functions seen up there I can manipulate the value of editTitle, so as to switch between the two functions by double clicking or clicking a button, and also update the field value in Firebase. as such:
this all works fine. HOWEVER,
if there are more that one divs rendered from the tasks[], then thay are obviously all effected to the flipping of editTitle's value from false to true, and by double clicking one field, all fields of same name in all divs swithc to edit mode. as such:
what can I do to target only the field in the task I want to edit? I've tried using the elemnt.id and index in some way bat can't seem to come up with the correct method...
const ifEditTitleIsTrue = (element, index) => {
return (
<div>
<input
type="text"
defaultValue={element.Title}
onChange={(e) => setUpdatedTitle(e.target.value)}
/>
<button className="exit__editmode-btn btn" onClick={exitEditMode}>
X
</button>
<button
className="update__edit-btn btn"
id="updateTitle"
onClick={(e) => updateField(e, element.id)}
>
ok
</button>
</div>
);
};
// if editTitle = false (default):
const ifEditTitleIsFalse = (element, index) => {
return (
<h3
id={index}
className="task-title"
onDoubleClick={() => setEditTitle(true)}
>
{element.Title}
</h3>
);
};
// edit mode for inCharge field
const ifEditInChargeIsTrue = (element, index) => {
return (
<div>
{
<GetCollaboratorsForEditMode
catchValueInCharge={catchValueInCharge}
/>
}
<button className="exit__editmode-btn btn" onClick={exitEditMode}>
X
</button>
<button
className="update__edit-btn btn"
id="updateInCharge"
onClick={(e) => updateField(e, element.id)}
>
ok
</button>
</div>
);
};
{tasks[0] &&
tasks.map((element, index) => (
<div id={element.id} className="task" key={element.id}>
{editTitle
? ifEditTitleIsTrue(element, index)
: ifEditTitleIsFalse(element, index)}
You need to keep track of what element is in edit mode. You can do it by storing the element id in your editTitle state, instead of just a boolean
const ifEditTitleIsFalse = (element, index) => {
...
onDoubleClick={() => setEditTitle(element.id)}
...
};
The condition to render an element in edit mode or view mode would change to:
{editTitle === element.id
? ifEditTitleIsTrue(element, index)
: ifEditTitleIsFalse(element, index)}
I've solved it!!!
insted of EditTitle being a boolean, it's just an empty string.
then the condition is editTitle === index ? some function : some othe function;
and the doubleclick is (()=> setEditTitle(index)).
First a heads up - This code is handed over to me and I have tried to simplify the problem to present here as much as I could..
Background
There is a 2d table that is getting column header from data array and row header from samples array.
samples.sampletests are cross functional objects and if sample.sampletests[n].testId matches to the testId in table header (data) then it will be checked.
Now I have added Select All function in the header - introduced an object array state variable testSelectAll having testid and isselectall properties which is working fine I am able to check/uncheck the checkboxes per column as I go
To do this I introduced the checked property of checkbox which was not being used before, only onChange and defaultChecked were being used..
Problem
but now the individual cell checkboxes have stopped taking input(check/uncheck) from user - I understand there is a need to implement onchange handler here but the existing code seems confusing (addItems array is adding objects with isselected property) - I am almost there but just need to clear my head around addItems reducer variable
Here is a sneak peek
My complete running code is available in codesandbox selectAllCheckbox_Example
{data.map((test, index) => (
<StyledTableCell>
{test?.sampleIds !== null &&
test?.sampleIds.includes(linkData.id) === true ? (
<GreenCheckbox
icon={<CircleUnchecked />}
checkedIcon={<CircleCheckedFilled />}
key={index}
defaultChecked={setCheckedData(linkData, test)}
onChange={(e) => {
linkDataHandler(linkData, test, e);
}}
inputProps={{ "aria-label": "uncontrolled-checkbox" }}
checked={
(testSelectAll.filter(
(t) => t.testid === test.testListId
).length > 0
? testSelectAll.filter(
(t) => t.testid === test.testListId
)[0].isSelectAllChecked
: false) ||
samples
.map((elm) => elm.sampleTests)
.flat()
.some(
(elm) =>
linkData.id === elm.sampleId &&
test.testListId === elm.testList.id
)/* ||
addItems.filter(
(a) =>
linkData.id === a.sampleId &&
test.testListId === a.TestId
).isSelected*/
}
/>
) : (
<div style={{ padding: "9px" }}>
<Tooltip
title={<Typography>Test not applicable</Typography>}
>
<RemoveCircleTwoToneIcon />
</Tooltip>
</div>
)}
</StyledTableCell>
))}
As soon as I uncomment the following code, the default and selectall options would stop working
addItems.filter(
(a) =>
linkData.id === a.sampleId &&
test.testListId === a.TestId
).isSelected
I am passing the index as a unique key, yet I am still receiving this warning:
Warning: Each child in a list should have a unique "key" prop.
Check the render method of SectionPackages
Can anyone guide me on what could be the problem?
const SectionPackages = props => {
const listingPackages =
publicData && publicData.packages && publicData.packages.length
? [defaultPackage, ...publicData.packages]
: [];
useEffect(() => {
listingPackages.map((l, index) => {
hidden[index] = true;
setHidden([...hidden]);
});
}, []);
return (
<div className={listingPackages.length ? rootClassName : css.noPackages}>
{listingPackages.length > 0 && (
<h2 className={css.packagesMainTitle}>
<FormattedMessage id="SectionPackages.title" />
</h2>
)}
{listingPackages.map((listingPackage, index) => {
const moneyPrice = new Money(
listingPackage.price.amount * 100,
listingPackage.price.currency
);
return (
<a onClick={e => handleBtnClick(e, index)} key={index}>
<div
key={index}
className={
selectedPackage === index && selectedPackage !== null
? classNames(css.packagesContainer, css.packagesSelectedContainer)
: css.packagesContainer
}
>
<div className={css.packagesDescriptionHolder}>
{listingPackage.description != null && (
<p
className={
hidden[index] === true
? classNames(css.packagesDescriptionHidden, 'packagesDescription')
: classNames(css.packagesDescriptionShown, 'packagesDescription')
}
>
{listingPackage.description}
</p>
)}
{elHeight[index] && elHeight[index].index === index && elHeight[index].height > 40 && (
<p
className={css.packagesDescriptionMore}
onClick={e => showHideDescription(e, index)}
>
{hidden[index] === true ? (
<FormattedMessage id="SectionPackages.showMore" />
) : (
<FormattedMessage id="SectionPackages.showLess" />
)}
</p>
)}
</div>
</div>
</a>
);
})}
</div>
);
};
When you use map or any other looping function you need to assign a unique key to each element you generate in react.
In your case you are assigning the index to the "a" tag and the "div" within the same loop.
You can make them unique by adding some kind of prefix maybe something like:
<a onClick={e => handleBtnClick(e, index)} key={`a_${index}`}>
<div
key={`div_${index}`}
that way the key for the a is different than the key for the div.
As an alternate solution you could remove the "key" from the div and leave it on the "a" tag.
NOTES
I want to clarify because there seem to be some confusion about how this works.
In this case the same key is being used both for the "a" and the "div" which are both within the same loop.
You need to assign the key to the root element being returned by the map function in this case "a".
React will use the key to compare previous virtual dom with current virtual dom and perform merges. This has nothing to do with "identifying" elements within elements.
Also it is not recommeded to use "index" as key because the index might end up on a different item on the next pass.
This is a quote from react's documentation:
We donβt recommend using indexes for keys if the order of items may change. This can negatively impact performance and may cause issues with component state.
https://reactjs.org/docs/lists-and-keys.html
Also check Index As Key Anti-Pattern
I have an object with a property of questions. questions is an array of 13 objects.Each object has a choices array that have between 2-4 objects and each object has an isCorrect property that is either true or null.
All my questions are being rendered. I have a button that I want to add a classname to each correct answer that the user selected. Do you guys have any suggestions of how I can achieve this?
{quiz.questions.map((question, index) => (
<ContentWrapper key={question._key}>
<h3>
{`${index + 1}. `}
{question.title}
</h3>
{question.choices.map(choice => (
<ChoiceStyle id={"wrapper" + choice._key} key={choice._key}>
<input
type="radio"
id={choice._key}
name={question._key}
onChange={updateScore}
value={choice.isCorrect === null ? "0" : "1"}
/>
<label htmlFor={choice._key}>{choice.title}</label>
</ChoiceStyle>
))}
<ButtonComponent
type="button"
buttonType="second"
title="Sjekk svar"
onClick={() => {
calculateScore();
}}
/>
That button just calculates the score but I might need to add another function to that button? I am also using styled components
You have multiple approaches to do this, the easiest and less invasive way is to add a ternary condition to show/hide the class name based on an isCorrect property.
I'm assuming that quiz is a state that is changed in each onChange to refresh all the real-time changes on the quiz. At this point:
{quiz.questions.map((question, index) => (
<ContentWrapper key={question._key}>
<h3>
{`${index + 1}. `}
{question.title}
</h3>
{question.choices.map(choice => (
<ChoiceStyle id={"wrapper" + choice._key} key={choice._key}>
<input
type="radio"
id={choice._key}
name={question._key}
onChange={updateScore}
className={`${choice.isCorrect ? 'is-valid':''}`}
value={choice.isCorrect === null ? "0" : "1"}
/>
<label htmlFor={choice._key}>{choice.title}</label>
</ChoiceStyle>
))}
<ButtonComponent
type="button"
buttonType="second"
title="Sjekk svar"
onClick={() => {
calculateScore();
}}
/>
The trick is the following ternary condition:
className={`${choice.isCorrect ? 'is-valid':''}`}
The problem that occurs is that the class gets applied right away. I
don't want the correct answer to show up unless the user has clicked
the button component
Then you will need to add a new property to your choice object to check and play with it. Let's assume something like:
const choice={
key: "some key"
isCorrect: true
/// other fields
}
// more code
{quiz.questions.map((question, index) => (
<ContentWrapper key={question._key}>
<h3>
{`${index + 1}. `}
{question.title}
</h3>
{question.choices.map(choice => (
<ChoiceStyle id={"wrapper" + choice._key} key={choice._key}>
<input
type="radio"
id={choice._key}
name={question._key}
onChange={updateScore}
className={`${choice.isValid ? 'is-valid':'' }`}
value={choice.isSelected === null ? "0" : "1"}
/>
<label htmlFor={choice._key}>{choice.title}</label>
</ChoiceStyle>
))}
<ButtonComponent
type="button"
buttonType="second"
title="Sjekk svar"
onClick={() => {
calculateScore();
}}
/>
In each updateScore, you'll need to check if the choice is correct and matches your business logic and add the new property (isValid) to trigger it.
const updateScore =()=>{
// some unknown logic
// some validations and then:
choice.isValid = true,
}
Since (I've assumed) that you are setting the quiz in React's state, the new property will force a refresh of the state, applying the class name in the user clicks.
I may have a bad title for this question, but here's my situation.
I use a chunk of json to render a list. The list item can be expanded and showed the sub list if it has children property. The json structure includes two arrays and each array contains more sub-arrays. I use tabs to switch arrays.
I use useState to manage the value isExpanded of each individual sub-array component. but it seems like the state isExpaned is shared for all tabs.
The state isExpanded remains same even if I switch to another tab. In other words, why the sub-list keep expanded when I switch to another tab?
In addition, why the expanded sub-list of each tab overlaps each other. They should keep 'close' when I switch to another tab because I set the initial state to false already. (const [isExpand, setIsExpand] = useState(false))
const ListItem = ({name, children}) => {
const [subList, setSubList] = useState(null)
const [isExpand, setIsExpand] = useState(false)
const handleItemClick = () => {
children && setIsExpand(!isExpand)
console.log(isExpand)
}
useEffect(() => {
isExpand && children && setSubList(children)
}, [isExpand, children])
return (
<div className='list-wrapper'>
<div className='list-item'>
{name}
{
children &&
<span
className='expand'
onClick={() => handleItemClick()}>
{isExpand ? '-' : '+'}
</span>
}
</div>
<div className='list-children'>
{
isExpand && subList && subList.map((item, index) =>
<ListItem key={index} name={item} />
)
}
</div>
</div>
)
}
Here's the codesanbox, anyone helps?
It seems like React is confused due to index being used as ListeItem key.
(React will try to "share" isExpanded state as they look the same according to the key you specified)
You could change the key from key={index}
<div className="contents">
{contents &&
contents.children &&
contents.children.map((item, index) => (
<ListItem
...... π ....
key={index}
name={item.name}
children={item.children}
/>
))}
</div>
to use more distinct key, item.name
<div className="contents">
{contents &&
contents.children &&
contents.children.map(item => (
<ListItem
...... π ....
key={item.name}
name={item.name}
children={item.children}
/>
))}
</div>
Check out the forked sandbox.
https://codesandbox.io/s/soanswer57212032-9ggzj