Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat(Dropdown): Enhance accessibility by exposing combobox role directly to the span label (Fixes #7584) #7585

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

iamkyrylo
Copy link
Contributor

@iamkyrylo iamkyrylo commented Jan 8, 2025

Fix #7584. Please take a look for more details.

Here's a summary of the changes included in this PR:

  • Assigned role="combobox" directly to the visible span label or an editable input element.
  • Removed the redundant hidden select element.
  • Improved ARIA attributes for better labeling with the list and form label, and describing states, like disabled, invalid, etc.
  • Added a new doc to describe of how to build relationship with label.

Demo:

Screen.Recording.2025-01-08.at.13.mp4

Copy link

vercel bot commented Jan 8, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

2 Skipped Deployments
Name Status Preview Updated (UTC)
primereact ⬜️ Ignored (Inspect) Visit Preview Jan 8, 2025 8:37pm
primereact-v9 ⬜️ Ignored (Inspect) Visit Preview Jan 8, 2025 8:37pm

Comment on lines 29 to +30
const inputRef = React.useRef(props.inputRef);
const labelRef = React.useRef(props.labelRef);
Copy link
Contributor Author

@iamkyrylo iamkyrylo Jan 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Differentiating label and input makes sense for me as they represent different elements. In the current implementation, inputRef assigned to the span element.

Comment on lines -151 to -152
setTimeout(() => {
const currentValue = inputRef.current ? inputRef.current.value : undefined;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested the previous issue with onBlur event and couldn't reproduce it. It's working correctly without timeout and calling with the value.

Screen.Recording.2025-01-08.at.13.26.21.mov

Comment on lines +160 to +161
const optionValue = getOptionValue(selectedOption);
const inputValue = getInputValue(selectedOption);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The entire option should be passed as the event value, but the target's value should provide a readable value since exposing an object isn't suitable for forms.

Comment on lines +785 to +794
const focus = React.useCallback(
(autoFocus) => {
if (props.editable) {
DomHandler.focus(inputRef.current, autoFocus);
} else {
DomHandler.focus(focusInputRef.current, autoFocus);
}
},
[props.editable]
);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Centralizing the focus logic for better maintainability.

Comment on lines -810 to +831
DomHandler.alignOverlay(overlayRef.current, inputRef.current.parentElement, props.appendTo || (context && context.appendTo) || PrimeReact.appendTo);
DomHandler.alignOverlay(overlayRef.current, elementRef.current, props.appendTo || (context && context.appendTo) || PrimeReact.appendTo);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to reference the input's parent since we already have a wrapper element.

Comment on lines +958 to +975
React.useEffect(() => {
const labelElement = ariaLabelledBy ? document.getElementById(ariaLabelledBy) : null;

const handleClick = () => {
focus();

if (!overlayVisibleState) {
show();
}
};

labelElement?.addEventListener('click', handleClick);

return () => {
labelElement?.removeEventListener('click', handleClick);
};
// eslint-disable-next-line react-hooks/exhaustive-deps -- show and focus are not required to be in the dependency array
}, [ariaLabelledBy, overlayVisibleState]);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workaround is necessary because the combobox is a span element now.

Comment on lines 1016 to 1018
if (props.editable) {
value = value || props.value || '';
return null;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to manage an additional input when the dropdown is editable.

Comment on lines +164 to +165
ariaLabel: undefined,
ariaLabelledBy: undefined,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using undefined as the default value keeps a cleaner DOM by preventing unnecessary attributes from being added to the element when they are not provided.

@melloware melloware added the Status: Pending Review Issue or pull request is being reviewed by Core Team label Jan 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Status: Pending Review Issue or pull request is being reviewed by Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Dropdown: Enhance accessibility by exposing combobox role
2 participants