UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

276 lines (268 loc) • 11.8 kB
import React from 'react'; import { useId } from '../hooks/useId.js'; import { useSlots } from '../hooks/useSlots.js'; import { ActionListContainerContext } from './ActionListContainerContext.js'; import { Description } from './Description.js'; import { GroupContext } from './Group.js'; import { Selection } from './Selection.js'; import { TrailingVisual, LeadingVisual, VisualOrIndicator } from './Visuals.js'; import { ListContext, ItemContext } from './shared.js'; import { TrailingAction } from './TrailingAction.js'; import { ConditionalWrapper } from '../internal/components/ConditionalWrapper.js'; import { invariant } from '../utils/invariant.js'; import VisuallyHidden from '../_VisuallyHidden.js'; import classes from './ActionList.module.css.js'; import { clsx } from 'clsx'; import { BoxWithFallback } from '../internal/components/BoxWithFallback.js'; import { jsx, jsxs, Fragment } from 'react/jsx-runtime'; const SubItem = ({ children }) => { return /*#__PURE__*/jsx(Fragment, { children: children }); }; SubItem.displayName = 'ActionList.SubItem'; const ButtonItemContainerNoBox = /*#__PURE__*/React.forwardRef(({ children, style, ...props }, forwardedRef) => { return /*#__PURE__*/jsx("button", { type: "button", ref: forwardedRef, style: style, ...props, children: children }); }); const DivItemContainerNoBox = /*#__PURE__*/React.forwardRef(({ children, ...props }, forwardedRef) => { return /*#__PURE__*/jsx("div", { ref: forwardedRef, ...props, children: children }); }); const Item = /*#__PURE__*/React.forwardRef(({ variant = 'default', size = 'medium', disabled = false, inactiveText, selected = undefined, active = false, onSelect: onSelectUser, sx: sxProp, id, role, loading, _PrivateItemWrapper, className, groupId: _groupId, renderItem: _renderItem, handleAddItem: _handleAddItem, ...props }, forwardedRef) => { var _slots$trailingVisual, _slots$description$pr, _slots$description; const baseSlots = { leadingVisual: LeadingVisual, trailingVisual: TrailingVisual, trailingAction: TrailingAction, subItem: SubItem }; const [partialSlots, childrenWithoutSlots] = useSlots(props.children, { ...baseSlots, description: Description }); const slots = { description: undefined, ...partialSlots }; const { container, afterSelect, selectionAttribute, defaultTrailingVisual } = React.useContext(ActionListContainerContext); // Be sure to avoid rendering the container unless there is a default const wrappedDefaultTrailingVisual = defaultTrailingVisual ? /*#__PURE__*/jsx(TrailingVisual, { children: defaultTrailingVisual }) : null; const trailingVisual = (_slots$trailingVisual = slots.trailingVisual) !== null && _slots$trailingVisual !== void 0 ? _slots$trailingVisual : wrappedDefaultTrailingVisual; const { role: listRole, selectionVariant: listSelectionVariant } = React.useContext(ListContext); const { selectionVariant: groupSelectionVariant } = React.useContext(GroupContext); const inactive = Boolean(inactiveText); // TODO change `menuContext` check to ```listRole !== undefined && ['menu', 'listbox'].includes(listRole)``` // once we have a better way to handle existing usage in dotcom that incorrectly use ActionList.TrailingAction const menuContext = container === 'ActionMenu' || container === 'SelectPanel' || container === 'FilteredActionList'; // TODO: when we change `menuContext` to check `listRole` instead of `container` const showInactiveIndicator = inactive && !(listRole !== undefined && ['menu', 'listbox'].includes(listRole)); const onSelect = React.useCallback((event, afterSelect) => { if (typeof onSelectUser === 'function') onSelectUser(event); if (event.defaultPrevented) return; if (typeof afterSelect === 'function') afterSelect(event); }, [onSelectUser]); const selectionVariant = groupSelectionVariant ? groupSelectionVariant : listSelectionVariant; /** Infer item role based on the container */ let inferredItemRole; if (container === 'ActionMenu') { if (selectionVariant === 'single') inferredItemRole = 'menuitemradio';else if (selectionVariant === 'multiple') inferredItemRole = 'menuitemcheckbox';else inferredItemRole = 'menuitem'; } else if (listRole === 'listbox') { if (selectionVariant !== undefined && !role) inferredItemRole = 'option'; } const itemRole = role || inferredItemRole; if (slots.trailingAction) { !!menuContext ? process.env.NODE_ENV !== "production" ? invariant(false, `ActionList.TrailingAction can not be used within a list with an ARIA role of "menu" or "listbox".`) : invariant(false) : void 0; } /** Infer the proper selection attribute based on the item's role */ let inferredSelectionAttribute; if (itemRole === 'menuitemradio' || itemRole === 'menuitemcheckbox') inferredSelectionAttribute = 'aria-checked';else if (itemRole === 'option') inferredSelectionAttribute = 'aria-selected'; const itemSelectionAttribute = selectionAttribute || inferredSelectionAttribute; // Ensures ActionList.Item retains list item semantics if a valid ARIA role is applied, or if item is inactive const listItemSemantics = role === 'option' || role === 'menuitem' || role === 'menuitemradio' || role === 'menuitemcheckbox'; const listRoleTypes = ['listbox', 'menu', 'list']; const listSemantics = listRole && listRoleTypes.includes(listRole) || inactive || listItemSemantics; const buttonSemantics = !listSemantics && !_PrivateItemWrapper; const clickHandler = React.useCallback(event => { if (disabled || inactive || loading) return; onSelect(event, afterSelect); }, [onSelect, disabled, inactive, afterSelect, loading]); const keyPressHandler = React.useCallback(event => { if (disabled || inactive || loading) return; if ([' ', 'Enter'].includes(event.key)) { if (event.key === ' ') { event.preventDefault(); // prevent scrolling on Space // immediately reset defaultPrevented once its job is done // so as to not disturb the functions that use that event after this event.defaultPrevented = false; } onSelect(event, afterSelect); } }, [onSelect, disabled, loading, inactive, afterSelect]); const itemId = useId(id); const labelId = `${itemId}--label`; const inlineDescriptionId = `${itemId}--inline-description`; const blockDescriptionId = `${itemId}--block-description`; const trailingVisualId = `${itemId}--trailing-visual`; const inactiveWarningId = inactive && !showInactiveIndicator ? `${itemId}--warning-message` : undefined; const DefaultItemWrapper = listSemantics ? DivItemContainerNoBox : ButtonItemContainerNoBox; const ItemWrapper = _PrivateItemWrapper || DefaultItemWrapper; // only apply aria-selected and aria-checked to selectable items const selectableRoles = ['menuitemradio', 'menuitemcheckbox', 'option']; const includeSelectionAttribute = itemSelectionAttribute && itemRole && selectableRoles.includes(itemRole); let focusable; if (showInactiveIndicator) { focusable = true; } // Extract the variant prop value from the description slot component const descriptionVariant = (_slots$description$pr = (_slots$description = slots.description) === null || _slots$description === void 0 ? void 0 : _slots$description.props.variant) !== null && _slots$description$pr !== void 0 ? _slots$description$pr : 'inline'; const menuItemProps = { onClick: clickHandler, onKeyPress: !buttonSemantics ? keyPressHandler : undefined, 'aria-disabled': disabled ? true : undefined, 'data-inactive': inactive ? true : undefined, 'data-loading': loading && !inactive ? true : undefined, tabIndex: focusable ? undefined : 0, 'aria-labelledby': `${labelId} ${slots.trailingVisual ? trailingVisualId : ''} ${slots.description && descriptionVariant === 'inline' ? inlineDescriptionId : ''}`, 'aria-describedby': [slots.description && descriptionVariant === 'block' ? blockDescriptionId : undefined, inactiveWarningId !== null && inactiveWarningId !== void 0 ? inactiveWarningId : undefined].filter(String).join(' ').trim() || undefined, ...(includeSelectionAttribute && { [itemSelectionAttribute]: selected }), role: itemRole, id: itemId }; const containerProps = _PrivateItemWrapper ? { role: itemRole ? 'none' : undefined, ...props } : // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition listSemantics && { ...menuItemProps, ...props, ref: forwardedRef } || {}; const wrapperProps = _PrivateItemWrapper ? menuItemProps : !listSemantics && { ...menuItemProps, ...props, ref: forwardedRef }; return /*#__PURE__*/jsx(ItemContext.Provider, { value: { variant, size, disabled, inactive: Boolean(inactiveText), inlineDescriptionId, blockDescriptionId, trailingVisualId }, children: /*#__PURE__*/jsxs(BoxWithFallback, { ...containerProps, as: "li", sx: sxProp, ref: listSemantics ? forwardedRef : null, "data-variant": variant === 'danger' ? variant : undefined, "data-active": active ? true : undefined, "data-inactive": inactiveText ? true : undefined, "data-has-subitem": slots.subItem ? true : undefined, "data-has-description": slots.description ? true : false, className: clsx(classes.ActionListItem, className), children: [/*#__PURE__*/jsxs(ItemWrapper, { ...wrapperProps, className: classes.ActionListContent, "data-size": size, children: [/*#__PURE__*/jsx("span", { className: classes.Spacer }), /*#__PURE__*/jsx(Selection, { selected: selected, className: classes.LeadingAction }), /*#__PURE__*/jsx(VisualOrIndicator, { inactiveText: showInactiveIndicator ? inactiveText : undefined, itemHasLeadingVisual: Boolean(slots.leadingVisual), labelId: labelId, loading: loading, position: "leading", children: slots.leadingVisual }), /*#__PURE__*/jsxs("span", { className: classes.ActionListSubContent, "data-component": "ActionList.Item--DividerContainer", children: [/*#__PURE__*/jsxs(ConditionalWrapper, { if: !!slots.description, className: classes.ItemDescriptionWrap, "data-description-variant": descriptionVariant, children: [/*#__PURE__*/jsxs("span", { id: labelId, className: classes.ItemLabel, children: [childrenWithoutSlots, loading === true && !inactive && /*#__PURE__*/jsx(VisuallyHidden, { children: "Loading" })] }), slots.description] }), /*#__PURE__*/jsx(VisualOrIndicator, { inactiveText: showInactiveIndicator ? inactiveText : undefined, itemHasLeadingVisual: Boolean(slots.leadingVisual), labelId: labelId, loading: loading, position: "trailing", children: trailingVisual }), // If the item is inactive, but it's not in an overlay (e.g. ActionMenu, SelectPanel), // render the inactive warning message directly in the item. !showInactiveIndicator && inactiveText ? /*#__PURE__*/jsx("span", { className: classes.InactiveWarning, id: inactiveWarningId, children: inactiveText }) : null] })] }), !inactive && !loading && !menuContext && Boolean(slots.trailingAction) && slots.trailingAction, slots.subItem] }) }); }); Item.displayName = 'ActionList.Item'; export { Item, SubItem };