Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Patch Changes

- make dropdown menu and language picker accessible

## 0.17.1

### Patch Changes
Expand All @@ -25,6 +29,7 @@
- a11y: Enable child node loading and fold/unfold via keyboard without requiring an initial mouse click.
- badge: fix the export


## 0.16.1

### Patch Changes
Expand Down
75 changes: 46 additions & 29 deletions src/components/dropdown-menu/DropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type DropdownMenuProps = {
onSelectValue?: (value: string) => void;
isOpen?: boolean;
topMessage?: string;
label?: string;
shouldCloseOnInteractOutside?: (element: Element) => boolean;
};

Expand All @@ -20,19 +21,39 @@ export const DropdownMenu = ({
selectedValues = [],
onSelectValue,
topMessage,
label = "Menu",
shouldCloseOnInteractOutside,
}: PropsWithChildren<DropdownMenuProps>) => {
const id = useId();
const triggerRef = useRef<HTMLDivElement | null>(null);

const onOpenChangeHandler = (isOpen: boolean) => {
onOpenChange?.(isOpen);
};

const triggerRef = useRef(null);
const isOptionChecked = (option: DropdownMenuOption): boolean => {
return Boolean(
option.isChecked ||
(option.value && selectedValues.includes(option.value))
);
};

const handleOptionAction = (option: DropdownMenuOption): void => {
if (option.value) onSelectValue?.(option.value);
option.callback?.();
onOpenChangeHandler(false);
};

return (
<>
{/* Trigger accessible div */}
<div
className="c__dropdown-menu-trigger"
ref={triggerRef}
role="button"
aria-haspopup="menu"
aria-expanded={isOpen}
aria-controls={`${id}-menu`}
id={id}
onClick={(e) => {
e.stopPropagation();
Expand All @@ -44,51 +65,47 @@ export const DropdownMenu = ({

<Popover
triggerRef={triggerRef}
style={{
marginTop: "0px",
}}
isOpen={isOpen}
shouldCloseOnInteractOutside={shouldCloseOnInteractOutside}
onOpenChange={onOpenChangeHandler}
shouldCloseOnInteractOutside={shouldCloseOnInteractOutside}
style={{ marginTop: "0px" }}
>
<Menu className="c__dropdown-menu" aria-labelledby={id}>
<Menu
className="c__dropdown-menu"
aria-labelledby={id}
aria-label={label}
selectionMode="single"
selectedKeys={selectedValues}
autoFocus
>
{topMessage && (
<MenuItem className="c__dropdown-menu-item-top-message">
<MenuItem className="c__dropdown-menu-item-top-message" isDisabled>
{topMessage}
</MenuItem>
)}

{options.map((option) => {
if (option.isHidden) {
return null;
}
if (option.isHidden) return null;

return (
<Fragment key={option.label}>
<Fragment key={option.value ?? option.label}>
<MenuItem
className="c__dropdown-menu-item"
aria-label={option.label}
key={option.label}
onAction={() => {
if (option.value) {
onSelectValue?.(option.value);
}
option.callback?.();
onOpenChangeHandler(false);
}}
id={option.value}
onAction={() => handleOptionAction(option)}
isDisabled={option.isDisabled}
>
{option.icon}
<div
className="c__dropdown-menu-item__label"
aria-label={option.label}
>
{option.label}
<div className="c__dropdown-menu-item__label">
<span lang={option.value}>{option.label}</span>
</div>
{(option.isChecked ||
(option.value &&
selectedValues.includes(option.value))) && (
<span className="material-icons checked">check</span>
{isOptionChecked(option) && (
<span className="material-icons checked" aria-hidden="true">
check
</span>
)}
</MenuItem>

{option.showSeparator && <Separator />}
</Fragment>
);
Expand Down
21 changes: 20 additions & 1 deletion src/components/dropdown-menu/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,26 @@
display: flex;
align-items: center;
justify-content: center;
border: none;
background-color: transparent;
box-shadow: none;

&:focus-visible {
border-color: #fff;
border-radius: var(--c--components--button--border-radius--focus);
box-shadow: 0 0 0 2px var(--c--theme--colors--primary-400);
outline: none;
}
&:hover {
background: var(--c--theme--colors--greyscale-100);
}
}

.c__dropdown-menu {
background-color: var(--c--contextuals--background--surface--primary);
z-index: 1000;
max-width: 320px;
max-height: inherit;
box-sizing: border-box;
overflow: auto;
min-width: 150px;
box-sizing: border-box;
Expand Down Expand Up @@ -75,6 +88,12 @@
}
}

&:focus-visible {
outline: 2px solid var(--c--theme--colors--primary-400);
outline-offset: -2px;
background-color: var(--c--theme--colors--greyscale-050);
}

.material-icons {
display: flex;
align-items: center;
Expand Down
13 changes: 9 additions & 4 deletions src/components/language/language-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const LanguagePicker = ({
return selectedLanguage?.value ?? languages[0].value!;
};
const [selectedLanguage, setSelectedLanguage] =
useState<string>(getInitialLanguage);
useState<string>(getInitialLanguage());

const handleLanguageChange = (value: string) => {
setSelectedLanguage(value);
Expand Down Expand Up @@ -77,15 +77,20 @@ export const LanguagePicker = ({
<Button
onClick={() => setIsOpen(!isOpen)}
className="c__language-picker"
icon={<Icon name={isOpen ? "arrow_drop_up" : "arrow_drop_down"} />}
icon={
<Icon
name={isOpen ? "arrow_drop_up" : "arrow_drop_down"}
aria-hidden="true"
/>
}
iconPosition="right"
size={size}
color={color}
variant={variant}
fullWidth={fullWidth}
>
<Icon name="translate" size={iconSize} />
<span className="c__language-picker__label">
<Icon name="translate" size={iconSize} aria-hidden="true" />
<span className="c__language-picker__label" lang={selectedLanguage}>
{languages.find((lang) => lang.value === selectedLanguage)?.label}
</span>
</Button>
Expand Down