512 Ability to navigate dropdown menus with keyboard (#11735)

# Ability to navigate dropdown menus with keyboard

The aim of this PR is to improve accessibility by allowing the user to
navigate inside the dropdown menus with the keyboard.
This PR refactors the `SelectableList` and `SelectableListItem`
components to move the Enter event handling responsibility from
`SelectableList` to the individual `SelectableListItem` components.
Closes [512](https://github.com/twentyhq/core-team-issues/issues/512)

## Key Changes:
- All dropdowns are now navigable with arrow keys

## Technical Implementation:
- Each `SelectableListItem` now has direct access to its own `Enter` key
handler, improving component encapsulation
- Removed the central `Enter` key handler logic from `SelectableList`
- Added `SelectableList` and `SelectableListItem` to all `Dropdown`
components inside the app
- Updated all component implementations to adapt to the new pattern:
  - Action menu components (`ActionDropdownItem`, `ActionListItem`)
  - Command menu components
  - Object filter, sort and options dropdowns
  - Record picker components
  - Select components

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Raphaël Bosi
2025-04-25 18:55:39 +02:00
committed by GitHub
parent 0b1b81429e
commit f201091c68
61 changed files with 1196 additions and 762 deletions

View File

@ -35,6 +35,7 @@ export type MenuItemProps = {
text: ReactNode;
contextualText?: ReactNode;
hasSubMenu?: boolean;
focused?: boolean;
};
export const MenuItem = ({
@ -53,6 +54,7 @@ export const MenuItem = ({
contextualText,
hasSubMenu = false,
disabled = false,
focused = false,
}: MenuItemProps) => {
const theme = useTheme();
const showIconButtons = Array.isArray(iconButtons) && iconButtons.length > 0;
@ -75,6 +77,7 @@ export const MenuItem = ({
isIconDisplayedOnHoverOnly={isIconDisplayedOnHoverOnly}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
focused={focused}
>
<StyledMenuItemLeftContent>
<MenuItemLeftContent

View File

@ -7,19 +7,11 @@ import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent
import { StyledMenuItemBase } from '../internals/components/StyledMenuItemBase';
export const StyledMenuItemSelect = styled(StyledMenuItemBase)<{
selected: boolean;
disabled?: boolean;
hovered?: boolean;
focused?: boolean;
}>`
${({ theme, selected, disabled, hovered }) => {
if (selected) {
return css`
background: ${theme.background.transparent.light};
&:hover {
background: ${theme.background.transparent.medium};
}
`;
} else if (disabled === true) {
${({ theme, disabled, focused }) => {
if (disabled === true) {
return css`
background: inherit;
&:hover {
@ -30,7 +22,7 @@ export const StyledMenuItemSelect = styled(StyledMenuItemBase)<{
cursor: default;
`;
} else if (hovered === true) {
} else if (focused === true) {
return css`
background: ${theme.background.transparent.light};
`;
@ -46,7 +38,7 @@ type MenuItemSelectProps = {
className?: string;
onClick?: () => void;
disabled?: boolean;
hovered?: boolean;
focused?: boolean;
hasSubMenu?: boolean;
contextualText?: ReactNode;
};
@ -59,7 +51,7 @@ export const MenuItemSelect = ({
className,
onClick,
disabled,
hovered,
focused,
hasSubMenu = false,
contextualText,
}: MenuItemSelectProps) => {
@ -69,9 +61,8 @@ export const MenuItemSelect = ({
<StyledMenuItemSelect
onClick={onClick}
className={className}
selected={selected}
disabled={disabled}
hovered={hovered}
focused={focused}
role="option"
aria-selected={selected}
aria-disabled={disabled}

View File

@ -17,7 +17,7 @@ type MenuItemSelectAvatarProps = {
className?: string;
onClick?: (event?: React.MouseEvent) => void;
disabled?: boolean;
hovered?: boolean;
focused?: boolean;
testId?: string;
};
@ -28,7 +28,7 @@ export const MenuItemSelectAvatar = ({
className,
onClick,
disabled,
hovered,
focused,
testId,
}: MenuItemSelectAvatarProps) => {
const theme = useTheme();
@ -37,9 +37,8 @@ export const MenuItemSelectAvatar = ({
<StyledMenuItemSelect
onClick={onClick}
className={className}
selected={selected}
disabled={disabled}
hovered={hovered}
focused={focused}
data-testid={testId}
role="option"
aria-selected={selected}

View File

@ -15,7 +15,7 @@ type MenuItemSelectColorProps = {
className?: string;
onClick?: () => void;
disabled?: boolean;
hovered?: boolean;
focused?: boolean;
color: ThemeColor;
variant?: ColorSampleVariant;
};
@ -39,7 +39,7 @@ export const MenuItemSelectColor = ({
className,
onClick,
disabled,
hovered,
focused,
variant = 'default',
}: MenuItemSelectColorProps) => {
const theme = useTheme();
@ -48,9 +48,8 @@ export const MenuItemSelectColor = ({
<StyledMenuItemSelect
onClick={onClick}
className={className}
selected={selected}
disabled={disabled}
hovered={hovered}
focused={focused}
>
<StyledMenuItemLeftContent>
<ColorSample colorName={color} variant={variant} />

View File

@ -11,7 +11,8 @@ import { ThemeColor } from '@ui/theme';
import { StyledMenuItemSelect } from './MenuItemSelect';
type MenuItemSelectTagProps = {
selected: boolean;
selected?: boolean;
focused?: boolean;
isKeySelected?: boolean;
className?: string;
onClick?: () => void;
@ -24,6 +25,7 @@ type MenuItemSelectTagProps = {
export const MenuItemSelectTag = ({
color,
selected,
focused,
isKeySelected,
className,
onClick,
@ -36,7 +38,7 @@ export const MenuItemSelectTag = ({
<StyledMenuItemSelect
onClick={onClick}
className={className}
selected={selected}
focused={focused}
isKeySelected={isKeySelected}
>
<StyledMenuItemLeftContent>

View File

@ -17,6 +17,7 @@ const StyledToggleContainer = styled.label`
`;
type MenuItemToggleProps = {
focused?: boolean;
LeftIcon?: IconComponent;
toggled: boolean;
text: string;
@ -26,6 +27,7 @@ type MenuItemToggleProps = {
};
export const MenuItemToggle = ({
focused,
LeftIcon,
text,
toggled,
@ -35,7 +37,7 @@ export const MenuItemToggle = ({
}: MenuItemToggleProps) => {
const inputId = useId();
return (
<StyledMenuItemBase className={className}>
<StyledMenuItemBase className={className} focused={focused}>
<StyledToggleContainer htmlFor={inputId}>
<MenuItemLeftContent LeftIcon={LeftIcon} text={text} />
<StyledMenuItemRightContent>

View File

@ -13,6 +13,7 @@ export type MenuItemBaseProps = {
isHoverBackgroundDisabled?: boolean;
hovered?: boolean;
disabled?: boolean;
focused?: boolean;
};
export const StyledMenuItemBase = styled.div<MenuItemBaseProps>`
@ -72,6 +73,12 @@ export const StyledMenuItemBase = styled.div<MenuItemBaseProps>`
}
}}
${({ focused, theme }) =>
focused &&
css`
background: ${theme.background.transparent.light};
`};
position: relative;
user-select: none;