@primer/react
Version:
An implementation of GitHub's Primer Design System using React
276 lines (268 loc) • 11.8 kB
JavaScript
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 };