@base-ui-components/react
Version:
Base UI is a library of headless ('unstyled') React components and low-level hooks. You gain complete control over your app's CSS and accessibility features.
114 lines (112 loc) • 3.69 kB
JavaScript
import * as React from 'react';
import { useId } from '@base-ui-components/utils/useId';
import { getFloatingFocusElement } from "../utils.js";
import { useFloatingParentNodeId } from "../components/FloatingTree.js";
import { EMPTY_OBJECT } from "../../utils/constants.js";
const componentRoleToAriaRoleMap = new Map([['select', 'listbox'], ['combobox', 'listbox'], ['label', false]]);
/**
* Adds base screen reader props to the reference and floating elements for a
* given floating element `role`.
* @see https://floating-ui.com/docs/useRole
*/
export function useRole(context, props = {}) {
const store = 'rootStore' in context ? context.rootStore : context;
const open = store.useState('open');
const defaultFloatingId = store.useState('floatingId');
const domReference = store.useState('domReferenceElement');
const floatingElement = store.useState('floatingElement');
const {
enabled = true,
role = 'dialog'
} = props;
const defaultReferenceId = useId();
const referenceId = domReference?.id || defaultReferenceId;
const floatingId = React.useMemo(() => getFloatingFocusElement(floatingElement)?.id || defaultFloatingId, [floatingElement, defaultFloatingId]);
const ariaRole = componentRoleToAriaRoleMap.get(role) ?? role;
const parentId = useFloatingParentNodeId();
const isNested = parentId != null;
const trigger = React.useMemo(() => {
if (ariaRole === 'tooltip' || role === 'label') {
return EMPTY_OBJECT;
}
return {
'aria-haspopup': ariaRole === 'alertdialog' ? 'dialog' : ariaRole,
'aria-expanded': 'false',
...(ariaRole === 'listbox' && {
role: 'combobox'
}),
...(ariaRole === 'menu' && isNested && {
role: 'menuitem'
}),
...(role === 'select' && {
'aria-autocomplete': 'none'
}),
...(role === 'combobox' && {
'aria-autocomplete': 'list'
})
};
}, [ariaRole, isNested, role]);
const reference = React.useMemo(() => {
if (ariaRole === 'tooltip' || role === 'label') {
return {
[`aria-${role === 'label' ? 'labelledby' : 'describedby'}`]: open ? floatingId : undefined
};
}
const triggerProps = trigger;
return {
...triggerProps,
'aria-expanded': open ? 'true' : 'false',
'aria-controls': open ? floatingId : undefined,
...(ariaRole === 'menu' && {
id: referenceId
})
};
}, [ariaRole, floatingId, open, referenceId, role, trigger]);
const floating = React.useMemo(() => {
const floatingProps = {
id: floatingId,
...(ariaRole && {
role: ariaRole
})
};
if (ariaRole === 'tooltip' || role === 'label') {
return floatingProps;
}
return {
...floatingProps,
...(ariaRole === 'menu' && {
'aria-labelledby': referenceId
})
};
}, [ariaRole, floatingId, referenceId, role]);
const item = React.useCallback(({
active,
selected
}) => {
const commonProps = {
role: 'option',
...(active && {
id: `${floatingId}-fui-option`
})
};
// For `menu`, we are unable to tell if the item is a `menuitemradio`
// or `menuitemcheckbox`. For backwards-compatibility reasons, also
// avoid defaulting to `menuitem` as it may overwrite custom role props.
switch (role) {
case 'select':
case 'combobox':
return {
...commonProps,
'aria-selected': selected
};
default:
}
return {};
}, [floatingId, role]);
return React.useMemo(() => enabled ? {
reference,
floating,
item,
trigger
} : {}, [enabled, reference, floating, trigger, item]);
}