@primer/react
Version:
An implementation of GitHub's Primer Design System using React
526 lines (513 loc) • 23.7 kB
JavaScript
'use strict';
var React = require('react');
var styled = require('styled-components');
var Box = require('../Box/Box.js');
var useId = require('../hooks/useId.js');
var useSlots = require('../hooks/useSlots.js');
var sx = require('../sx.js');
var ThemeProvider = require('../ThemeProvider.js');
var defaultSxProp = require('../utils/defaultSxProp.js');
var ActionListContainerContext = require('./ActionListContainerContext.js');
var Description = require('./Description.js');
var Group = require('./Group.js');
var Selection = require('./Selection.js');
var Visuals = require('./Visuals.js');
var shared = require('./shared.js');
var TrailingAction = require('./TrailingAction.js');
var ConditionalWrapper = require('../internal/components/ConditionalWrapper.js');
var invariant = require('../utils/invariant.js');
require('../FeatureFlags/FeatureFlags.js');
var useFeatureFlag = require('../FeatureFlags/useFeatureFlag.js');
require('../FeatureFlags/DefaultFeatureFlags.js');
var _VisuallyHidden = require('../_VisuallyHidden.js');
var ActionList_module = require('./ActionList.module.css.js');
var clsx = require('clsx');
var featureflag = require('./featureflag.js');
var merge = require('deepmerge');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var React__default = /*#__PURE__*/_interopDefault(React);
var styled__default = /*#__PURE__*/_interopDefault(styled);
var merge__default = /*#__PURE__*/_interopDefault(merge);
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
const LiBox = styled__default.default.li.withConfig({
displayName: "Item__LiBox",
componentId: "sc-yeql7o-0"
})(sx.default);
const SubItem = ({
children
}) => {
return /*#__PURE__*/React__default.default.createElement(React__default.default.Fragment, null, children);
};
SubItem.displayName = 'ActionList.SubItem';
const ButtonItemContainerNoBox = /*#__PURE__*/React__default.default.forwardRef(({
children,
style,
...props
}, forwardedRef) => {
return /*#__PURE__*/React__default.default.createElement("button", _extends({
type: "button",
ref: forwardedRef,
style: style
}, props), children);
});
const DivItemContainerNoBox = /*#__PURE__*/React__default.default.forwardRef(({
children,
...props
}, forwardedRef) => {
return /*#__PURE__*/React__default.default.createElement("div", _extends({
ref: forwardedRef
}, props), children);
});
const Item = /*#__PURE__*/React__default.default.forwardRef(({
variant = 'default',
disabled = false,
inactiveText,
selected = undefined,
active = false,
onSelect: onSelectUser,
sx: sxProp = defaultSxProp.defaultSxProp,
id,
role,
loading,
_PrivateItemWrapper,
className,
...props
}, forwardedRef) => {
var _slots$trailingVisual, _slots$description$pr, _slots$description;
const enabled = useFeatureFlag.useFeatureFlag(featureflag.actionListCssModulesFlag);
const baseSlots = {
leadingVisual: Visuals.LeadingVisual,
trailingVisual: Visuals.TrailingVisual,
trailingAction: TrailingAction.TrailingAction,
subItem: SubItem
};
const [partialSlots, childrenWithoutSlots] = useSlots.useSlots(props.children, enabled ? {
...baseSlots,
description: Description.Description
} : {
...baseSlots,
blockDescription: [Description.Description, props => props.variant === 'block'],
inlineDescription: [Description.Description, props => props.variant !== 'block']
});
const slots = {
blockDescription: undefined,
inlineDescription: undefined,
description: undefined,
...partialSlots
};
const {
container,
afterSelect,
selectionAttribute,
defaultTrailingVisual
} = React__default.default.useContext(ActionListContainerContext.ActionListContainerContext);
// Be sure to avoid rendering the container unless there is a default
const wrappedDefaultTrailingVisual = defaultTrailingVisual ? /*#__PURE__*/React__default.default.createElement(Visuals.TrailingVisual, null, defaultTrailingVisual) : null;
const trailingVisual = (_slots$trailingVisual = slots.trailingVisual) !== null && _slots$trailingVisual !== undefined ? _slots$trailingVisual : wrappedDefaultTrailingVisual;
const {
variant: listVariant,
role: listRole,
showDividers,
selectionVariant: listSelectionVariant
} = React__default.default.useContext(shared.ListContext);
const {
selectionVariant: groupSelectionVariant
} = React__default.default.useContext(Group.GroupContext);
const inactive = Boolean(inactiveText);
const showInactiveIndicator = inactive && container === undefined;
const onSelect = React__default.default.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 (container === 'SelectPanel' && listRole === 'listbox') {
if (selectionVariant !== undefined) inferredItemRole = 'option';
}
const itemRole = role || inferredItemRole;
const menuContext = container === 'ActionMenu' || container === 'SelectPanel';
if (slots.trailingAction) {
!!menuContext ? process.env.NODE_ENV !== "production" ? invariant.invariant(false, `ActionList.TrailingAction can not be used within a ${container}.`) : invariant.invariant(false) : undefined;
}
/** 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 || !enabled;
const buttonSemantics = !listSemantics && !_PrivateItemWrapper;
const {
theme
} = ThemeProvider.useTheme();
const activeStyles = {
fontWeight: 'bold',
bg: 'actionListItem.default.selectedBg',
'&::after': {
position: 'absolute',
top: 'calc(50% - 12px)',
left: -2,
width: '4px',
height: '24px',
content: '""',
bg: 'var(--borderColor-accent-emphasis)',
borderRadius: 2
}
};
const styles = {
position: 'relative',
display: 'flex',
paddingX: 2,
fontSize: 1,
paddingY: '6px',
// custom value off the scale
lineHeight: '16px',
minHeight: 5,
marginX: listVariant === 'inset' ? 2 : 0,
borderRadius: 2,
transition: 'background 33.333ms linear',
color: shared.getVariantStyles(variant, disabled, inactive || loading).color,
cursor: 'pointer',
'&[data-loading]': {
cursor: 'default'
},
'&[aria-disabled], &[data-inactive]': {
cursor: 'not-allowed',
'[data-component="ActionList.Checkbox"]': {
cursor: 'not-allowed',
bg: selected ? 'fg.muted' : 'var(--control-bgColor-disabled, rgba(175, 184, 193, 0.2))',
borderColor: selected ? 'fg.muted' : 'var(--color-input-disabled-bg, rgba(175, 184, 193, 0.2))'
},
'[data-component="ActionList.Selection"]': {
color: 'primer.fg.disabled'
}
},
// Button reset styles (to support as="button")
appearance: 'none',
background: 'unset',
border: 'unset',
width: listVariant === 'inset' ? 'calc(100% - 16px)' : '100%',
fontFamily: 'unset',
textAlign: 'unset',
marginY: 'unset',
'@media (forced-colors: active)': {
':focus, &:focus-visible, > a.focus-visible, &[data-is-active-descendant]': {
// Support for Windows high contrast https://sarahmhigley.com/writing/whcm-quick-tips
outline: 'solid 1px transparent !important'
}
},
'@media (hover: hover) and (pointer: fine)': {
'&:hover:not([aria-disabled]):not([data-inactive])': {
backgroundColor: `actionListItem.${variant}.hoverBg`,
color: shared.getVariantStyles(variant, disabled, inactive).hoverColor,
boxShadow: `inset 0 0 0 max(1px, 0.0625rem) ${theme === null || theme === undefined ? undefined : theme.colors.actionListItem.default.activeBorder}`
},
'&:focus-visible, > a.focus-visible, &:focus.focus-visible': {
outline: 'none',
border: `2 solid`,
boxShadow: `0 0 0 2px var(--focus-outlineColor)`
},
'&:active:not([aria-disabled]):not([data-inactive])': {
backgroundColor: `actionListItem.${variant}.activeBg`,
color: shared.getVariantStyles(variant, disabled, inactive).hoverColor
}
},
/** Divider styles */
'[data-component="ActionList.Item--DividerContainer"]': {
position: 'relative'
},
'[data-component="ActionList.Item--DividerContainer"]::before': {
content: '" "',
display: 'block',
position: 'absolute',
width: '100%',
top: '-7px',
border: '0 solid',
borderTopWidth: showDividers ? `1px` : '0',
borderColor: 'var(--divider-color, transparent)'
},
// show between 2 items
':not(:first-of-type)': {
'--divider-color': theme === null || theme === undefined ? undefined : theme.colors.actionListItem.inlineDivider
},
// hide divider after dividers & group header, with higher importance!
'[data-component="ActionList.Divider"] + &': {
'--divider-color': 'transparent !important'
},
// hide border on current and previous item
'&:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]), &[data-focus-visible-added]:not([aria-disabled]):not([data-inactive])': {
'--divider-color': 'transparent'
},
'&:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]) + &, &[data-focus-visible-added] + li': {
'--divider-color': 'transparent'
},
/** Active styles */
...(active ? activeStyles : {}),
// NavList
'&[data-is-active-descendant]': {
...activeStyles,
fontWeight: 'normal'
} // SelectPanel
};
const clickHandler = React__default.default.useCallback(event => {
if (disabled || inactive || loading) return;
onSelect(event, afterSelect);
}, [onSelect, disabled, inactive, afterSelect, loading]);
const keyPressHandler = React__default.default.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 it's 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.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;
let DefaultItemWrapper = React__default.default.Fragment;
if (enabled) {
DefaultItemWrapper = listSemantics ? DivItemContainerNoBox : ButtonItemContainerNoBox;
} else {
DefaultItemWrapper = React__default.default.Fragment;
}
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 item is disabled and is of type (menuitem*, option) it should remain focusable, if inactive, apply the same rules
if (disabled && !inferredItemRole || showInactiveIndicator) {
focusable = true;
}
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.inlineDescription ? inlineDescriptionId : ''}`,
'aria-describedby': [slots.blockDescription ? blockDescriptionId : undefined, inactiveWarningId !== null && inactiveWarningId !== undefined ? 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
};
// Extract the variant prop value from the description slot component
const descriptionVariant = (_slots$description$pr = (_slots$description = slots.description) === null || _slots$description === undefined ? undefined : _slots$description.props.variant) !== null && _slots$description$pr !== undefined ? _slots$description$pr : 'inline';
if (enabled) {
if (sxProp !== defaultSxProp.defaultSxProp) {
return /*#__PURE__*/React__default.default.createElement(shared.ItemContext.Provider, {
value: {
variant,
disabled,
inactive: Boolean(inactiveText),
inlineDescriptionId,
blockDescriptionId,
trailingVisualId
}
}, /*#__PURE__*/React__default.default.createElement(LiBox, _extends({}, containerProps, {
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,
className: clsx.clsx(ActionList_module.ActionListItem, className)
}), /*#__PURE__*/React__default.default.createElement(ItemWrapper, _extends({}, wrapperProps, {
className: ActionList_module.ActionListContent
}), /*#__PURE__*/React__default.default.createElement("span", {
className: ActionList_module.Spacer
}), /*#__PURE__*/React__default.default.createElement(Selection.Selection, {
selected: selected,
className: ActionList_module.LeadingAction
}), /*#__PURE__*/React__default.default.createElement(Visuals.VisualOrIndicator, {
inactiveText: showInactiveIndicator ? inactiveText : undefined,
itemHasLeadingVisual: Boolean(slots.leadingVisual),
labelId: labelId,
loading: loading,
position: "leading"
}, slots.leadingVisual), /*#__PURE__*/React__default.default.createElement("span", {
className: ActionList_module.ActionListSubContent
}, /*#__PURE__*/React__default.default.createElement(ConditionalWrapper.ConditionalWrapper, {
if: !!slots.description,
className: ActionList_module.ItemDescriptionWrap,
"data-description-variant": descriptionVariant
}, /*#__PURE__*/React__default.default.createElement("span", {
id: labelId,
className: ActionList_module.ItemLabel
}, childrenWithoutSlots, loading === true && /*#__PURE__*/React__default.default.createElement(_VisuallyHidden, null, "Loading")), slots.description), /*#__PURE__*/React__default.default.createElement(Visuals.VisualOrIndicator, {
inactiveText: showInactiveIndicator ? inactiveText : undefined,
itemHasLeadingVisual: Boolean(slots.leadingVisual),
labelId: labelId,
loading: loading,
position: "trailing"
}, 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.
inactive && container ? /*#__PURE__*/React__default.default.createElement("span", {
className: ActionList_module.InactiveWarning,
id: inactiveWarningId
}, inactiveText) : null)), !inactive && !loading && !menuContext && Boolean(slots.trailingAction) && slots.trailingAction, slots.subItem));
}
return /*#__PURE__*/React__default.default.createElement(shared.ItemContext.Provider, {
value: {
variant,
disabled,
inactive: Boolean(inactiveText),
inlineDescriptionId,
blockDescriptionId,
trailingVisualId
}
}, /*#__PURE__*/React__default.default.createElement("li", _extends({}, containerProps, {
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,
className: clsx.clsx(ActionList_module.ActionListItem, className)
}), /*#__PURE__*/React__default.default.createElement(ItemWrapper, _extends({}, wrapperProps, {
className: ActionList_module.ActionListContent
}), /*#__PURE__*/React__default.default.createElement("span", {
className: ActionList_module.Spacer
}), /*#__PURE__*/React__default.default.createElement(Selection.Selection, {
selected: selected,
className: ActionList_module.LeadingAction
}), /*#__PURE__*/React__default.default.createElement(Visuals.VisualOrIndicator, {
inactiveText: showInactiveIndicator ? inactiveText : undefined,
itemHasLeadingVisual: Boolean(slots.leadingVisual),
labelId: labelId,
loading: loading,
position: "leading"
}, slots.leadingVisual), /*#__PURE__*/React__default.default.createElement("span", {
className: ActionList_module.ActionListSubContent
}, /*#__PURE__*/React__default.default.createElement(ConditionalWrapper.ConditionalWrapper, {
if: !!slots.description,
className: ActionList_module.ItemDescriptionWrap,
"data-description-variant": descriptionVariant
}, /*#__PURE__*/React__default.default.createElement("span", {
id: labelId,
className: ActionList_module.ItemLabel
}, childrenWithoutSlots, loading === true && /*#__PURE__*/React__default.default.createElement(_VisuallyHidden, null, "Loading")), slots.description), /*#__PURE__*/React__default.default.createElement(Visuals.VisualOrIndicator, {
inactiveText: showInactiveIndicator ? inactiveText : undefined,
itemHasLeadingVisual: Boolean(slots.leadingVisual),
labelId: labelId,
loading: loading,
position: "trailing"
}, 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.
inactive && container ? /*#__PURE__*/React__default.default.createElement("span", {
className: ActionList_module.InactiveWarning,
id: inactiveWarningId
}, inactiveText) : null)), !inactive && !loading && !menuContext && Boolean(slots.trailingAction) && slots.trailingAction, slots.subItem));
}
return /*#__PURE__*/React__default.default.createElement(shared.ItemContext.Provider, {
value: {
variant,
disabled,
inactive: Boolean(inactiveText),
inlineDescriptionId,
blockDescriptionId,
trailingVisualId
}
}, /*#__PURE__*/React__default.default.createElement(LiBox, _extends({
ref: listSemantics ? forwardedRef : null,
className: className,
sx: merge__default.default(styles, sxProp),
"data-variant": variant === 'danger' ? variant : undefined
}, containerProps), /*#__PURE__*/React__default.default.createElement(ItemWrapper, wrapperProps, /*#__PURE__*/React__default.default.createElement(Selection.Selection, {
selected: selected
}), /*#__PURE__*/React__default.default.createElement(Visuals.VisualOrIndicator, {
inactiveText: showInactiveIndicator ? inactiveText : undefined,
itemHasLeadingVisual: Boolean(slots.leadingVisual),
labelId: labelId,
loading: loading,
position: "leading"
}, slots.leadingVisual), /*#__PURE__*/React__default.default.createElement(Box, {
"data-component": "ActionList.Item--DividerContainer",
sx: {
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
minWidth: 0
}
}, /*#__PURE__*/React__default.default.createElement(ConditionalWrapper.ConditionalWrapper
// we need a flex container if:
// - there is a trailing visual
// - OR there is a loading or inactive indicator
// - AND no leading visual to replace with an indicator
, {
if: Boolean(trailingVisual || (showInactiveIndicator || loading) && !slots.leadingVisual),
sx: {
display: 'flex',
flexGrow: 1
}
}, /*#__PURE__*/React__default.default.createElement(ConditionalWrapper.ConditionalWrapper, {
if: !!slots.inlineDescription,
sx: {
display: 'flex',
flexGrow: 1,
alignItems: 'baseline',
minWidth: 0
}
}, /*#__PURE__*/React__default.default.createElement(Box, {
as: "span",
id: labelId,
sx: {
flexGrow: slots.inlineDescription ? 0 : 1,
fontWeight: slots.inlineDescription || slots.blockDescription || active ? 'bold' : 'normal',
marginBlockEnd: slots.blockDescription ? '4px' : undefined,
wordBreak: slots.inlineDescription ? 'normal' : 'break-word',
lineHeight: '20px'
}
}, childrenWithoutSlots, loading === true && /*#__PURE__*/React__default.default.createElement(_VisuallyHidden, null, "Loading")), slots.inlineDescription), /*#__PURE__*/React__default.default.createElement(Visuals.VisualOrIndicator, {
inactiveText: showInactiveIndicator ? inactiveText : undefined,
itemHasLeadingVisual: Boolean(slots.leadingVisual),
labelId: labelId,
loading: loading,
position: "trailing"
}, 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.
inactive && container ? /*#__PURE__*/React__default.default.createElement(Box, {
as: "span",
sx: {
fontSize: 0,
lineHeight: '16px',
color: 'attention.fg'
},
id: inactiveWarningId
}, inactiveText) : null, slots.blockDescription)), !inactive && !loading && !menuContext && Boolean(slots.trailingAction) && slots.trailingAction));
});
Item.displayName = 'ActionList.Item';
exports.Item = Item;
exports.SubItem = SubItem;