/** * Shared ARIA attribute helpers for consistent accessibility across all components * * These utilities ensure consistent ARIA implementation following WCAG 2.2 Level AA standards * and reduce code duplication across components. */ export type AriaAttributes = { 'aria-label'?: string 'aria-labelledby'?: string 'aria-describedby'?: string 'aria-expanded'?: boolean 'aria-selected'?: boolean 'aria-disabled'?: boolean 'aria-busy'?: boolean 'aria-invalid'?: boolean 'aria-required'?: boolean 'aria-controls'?: string 'aria-live'?: 'polite' | 'assertive' | 'off' 'aria-atomic'?: boolean 'aria-haspopup'?: | 'true' | 'false' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' 'aria-modal'?: boolean 'aria-orientation'?: 'horizontal' | 'vertical' role?: string } /** * Generate ARIA attributes for button components * * @example * ```tsx * const ariaAttrs = getButtonAriaAttributes({ * disabled: true, * loading: false, * ariaLabel: 'Close dialog' * }) * * ``` */ export function getButtonAriaAttributes(props: { disabled?: boolean loading?: boolean skeleton?: boolean ariaLabel?: string ariaLabelledBy?: string }): AriaAttributes { const attrs: AriaAttributes = {} if (props.disabled) { attrs['aria-disabled'] = true } if (props.loading || props.skeleton) { attrs['aria-busy'] = true } if (props.ariaLabel) { attrs['aria-label'] = props.ariaLabel } if (props.ariaLabelledBy) { attrs['aria-labelledby'] = props.ariaLabelledBy } return attrs } /** * Generate ARIA attributes for input components * * @example * ```tsx * const ariaAttrs = getInputAriaAttributes({ * id: 'email-input', * labelId: 'email-label', * errorId: 'email-error', * hintId: 'email-hint', * required: true, * invalid: true * }) * * ``` */ export function getInputAriaAttributes(props: { id: string labelId?: string errorId?: string hintId?: string required?: boolean invalid?: boolean describedBy?: string[] }): AriaAttributes { const attrs: AriaAttributes = {} const describedBy: string[] = [] if (props.labelId) { attrs['aria-labelledby'] = props.labelId } if (props.errorId) { describedBy.push(props.errorId) attrs['aria-invalid'] = props.invalid ?? true } if (props.hintId) { describedBy.push(props.hintId) } if (props.describedBy) { describedBy.push(...props.describedBy) } if (describedBy.length > 0) { attrs['aria-describedby'] = describedBy.join(' ') } if (props.required) { attrs['aria-required'] = true } return attrs } /** * Generate ARIA attributes for modal/dialog components * * @example * ```tsx * const ariaAttrs = getModalAriaAttributes({ * id: 'dialog-1', * titleId: 'dialog-title', * descriptionId: 'dialog-description', * isAlert: false * }) *
...
* ``` */ export function getModalAriaAttributes(props: { id: string titleId: string descriptionId?: string isAlert?: boolean }): AriaAttributes { return { role: props.isAlert ? 'alertdialog' : 'dialog', 'aria-modal': true, 'aria-labelledby': props.titleId, ...(props.descriptionId && { 'aria-describedby': props.descriptionId, }), } } /** * Generate ARIA attributes for dropdown/select components * * @example * ```tsx * const ariaAttrs = getDropdownAriaAttributes({ * id: 'select-1', * isOpen: true, * triggerId: 'select-trigger', * menuId: 'select-menu' * }) * * ``` */ export function getDropdownAriaAttributes(props: { id: string isOpen: boolean triggerId: string menuId: string hasPopup?: 'listbox' | 'menu' }): AriaAttributes { return { 'aria-expanded': props.isOpen, 'aria-controls': props.menuId, 'aria-haspopup': props.hasPopup || 'listbox', } } /** * Generate ARIA attributes for tab components * * @example * ```tsx * const ariaAttrs = getTabAriaAttributes({ * id: 'tab-1', * panelId: 'panel-1', * selected: true * }) * * ``` */ export function getTabAriaAttributes(props: { id: string panelId: string selected: boolean controls?: string }): AriaAttributes { return { role: 'tab', 'aria-selected': props.selected, 'aria-controls': props.panelId, ...(props.controls && { 'aria-controls': props.controls }), } } /** * Generate ARIA attributes for tab panel components * * @example * ```tsx * const ariaAttrs = getTabPanelAriaAttributes({ * id: 'panel-1', * tabId: 'tab-1', * labelledBy: 'tab-1-label' * }) *
Panel content
* ``` */ export function getTabPanelAriaAttributes(props: { id: string tabId: string labelledBy?: string }): AriaAttributes { return { role: 'tabpanel', 'aria-labelledby': props.labelledBy || props.tabId, } } /** * Generate ARIA attributes for accordion components * * @example * ```tsx * const ariaAttrs = getAccordionAriaAttributes({ * headerId: 'accordion-header-1', * contentId: 'accordion-content-1', * expanded: false * }) * * ``` */ export function getAccordionAriaAttributes(props: { headerId: string contentId: string expanded: boolean }): AriaAttributes { return { role: 'button', 'aria-expanded': props.expanded, 'aria-controls': props.contentId, } } /** * Generate ARIA live region attributes for dynamic content * * @example * ```tsx * const ariaAttrs = getLiveRegionAriaAttributes({ * live: 'assertive', * atomic: true * }) *
Error message
* ``` */ export function getLiveRegionAriaAttributes(props: { live?: 'polite' | 'assertive' | 'off' atomic?: boolean }): AriaAttributes { return { 'aria-live': props.live || 'polite', 'aria-atomic': props.atomic ?? true, } } /** * Generate unique IDs for ARIA relationships * * @example * ```tsx * const labelId = generateAriaId('label') * const inputId = generateAriaId('input') * ``` */ export function generateAriaId(prefix: string): string { return `${prefix}-${Math.random().toString(36).substring(2, 9)}` } /** * Combine multiple ARIA attribute objects * Later objects override earlier ones for conflicting keys * * @example * ```tsx * const baseAttrs = getButtonAriaAttributes({ disabled: true }) * const additionalAttrs = { 'aria-label': 'Custom label' } * const merged = mergeAriaAttributes(baseAttrs, additionalAttrs) * ``` */ export function mergeAriaAttributes( ...attrs: Array ): AriaAttributes { return attrs.reduce((acc, curr) => { if (!curr) return acc return { ...acc, ...curr } }, {}) }