UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

386 lines (376 loc) • 16.7 kB
'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'); var _VisuallyHidden = require('../_VisuallyHidden.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 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, ...props }, forwardedRef) => { var _slots$trailingVisual; const [slots, childrenWithoutSlots] = useSlots.useSlots(props.children, { leadingVisual: Visuals.LeadingVisual, trailingVisual: Visuals.TrailingVisual, trailingAction: TrailingAction.TrailingAction, blockDescription: [Description.Description, props => props.variant === 'block'], inlineDescription: [Description.Description, props => props.variant !== 'block'] }); const { container, afterSelect, selectionAttribute, defaultTrailingVisual } = React__default.default.useContext(ActionListContainerContext.ActionListContainerContext); const buttonSemanticsFeatureFlag = useFeatureFlag.useFeatureFlag('primer_react_action_list_item_as_button'); // 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 !== void 0 ? _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) : 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 listSemantics = listRole === 'listbox' || listRole === 'menu' || inactive || container === 'NavList'; const buttonSemantics = !listSemantics && !_PrivateItemWrapper && buttonSemanticsFeatureFlag; 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: 'accent.fg', borderRadius: 2 } }; const hoverStyles = { '@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 === void 0 ? void 0 : theme.colors.actionListItem.default.activeBorder}` }, '&:focus-visible, > a.focus-visible, &:focus.focus-visible': { outline: 'none', border: `2 solid`, boxShadow: `0 0 0 2px ${theme === null || theme === void 0 ? void 0 : theme.colors.accent.emphasis}` }, ':active:not([aria-disabled]):not([data-inactive])': { backgroundColor: `actionListItem.${variant}.activeBg`, color: shared.getVariantStyles(variant, disabled, inactive).hoverColor } } }; const listItemStyles = { display: 'flex', // show between 2 items ':not(:first-of-type)': { '--divider-color': theme === null || theme === void 0 ? void 0 : theme.colors.actionListItem.inlineDivider }, width: buttonSemantics && listVariant !== 'full' ? 'calc(100% - 16px)' : '100%', marginX: buttonSemantics && listVariant !== 'full' ? '2' : '0', borderRadius: 2, ...(buttonSemantics ? hoverStyles : {}) }; const styles = { position: 'relative', display: 'flex', paddingX: 2, fontSize: 1, paddingY: '6px', // custom value off the scale lineHeight: shared.TEXT_ROW_HEIGHT, minHeight: 5, marginX: listVariant === 'inset' && !buttonSemantics ? 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(--color-input-disabled-bg, rgba(175, 184, 193, 0.2))', borderColor: selected ? 'fg.muted' : 'var(--color-input-disabled-bg, rgba(175, 184, 193, 0.2))' } }, // Button reset styles (to support as="button") appearance: 'none', background: 'unset', border: 'unset', width: listVariant === 'inset' && !buttonSemantics ? 'calc(100% - 16px)' : '100%', fontFamily: 'unset', textAlign: 'unset', marginY: 'unset', '@media (forced-colors: active)': { ':focus, &:focus-visible, > a.focus-visible': { // Support for Windows high contrast https://sarahmhigley.com/writing/whcm-quick-tips outline: 'solid 1px transparent !important' } }, /** 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 === void 0 ? void 0 : 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 ? activeStyles : {}), ...(!buttonSemantics ? hoverStyles : {}) }; 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 inactiveWarningId = inactive && !showInactiveIndicator ? `${itemId}--warning-message` : undefined; const ButtonItemWrapper = /*#__PURE__*/React__default.default.forwardRef(({ as: Component = 'button', children, ...props }, forwardedRef) => { return /*#__PURE__*/React__default.default.createElement(Box, _extends({ as: Component, sx: merge__default.default(styles, sxProp), ref: forwardedRef }, props), children); }); let DefaultItemWrapper = React__default.default.Fragment; if (buttonSemanticsFeatureFlag) { DefaultItemWrapper = listSemantics ? React__default.default.Fragment : ButtonItemWrapper; } 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); 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: disabled || showInactiveIndicator ? undefined : 0, 'aria-labelledby': `${labelId} ${slots.inlineDescription ? inlineDescriptionId : ''}`, 'aria-describedby': slots.blockDescription ? [blockDescriptionId, inactiveWarningId].join(' ') : inactiveWarningId, ...(includeSelectionAttribute && { [itemSelectionAttribute]: selected }), role: itemRole, id: itemId }; let containerProps; let wrapperProps; if (buttonSemanticsFeatureFlag) { containerProps = _PrivateItemWrapper ? { role: itemRole ? 'none' : undefined, ...props } : // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition listSemantics && { ...menuItemProps, ...props, ref: forwardedRef } || {}; wrapperProps = _PrivateItemWrapper ? menuItemProps : !listSemantics && { ...menuItemProps, ...props, styles: merge__default.default(styles, sxProp), ref: forwardedRef }; } else { containerProps = _PrivateItemWrapper ? { role: itemRole ? 'none' : undefined } : { ...menuItemProps, ...props }; wrapperProps = _PrivateItemWrapper ? menuItemProps : {}; } return /*#__PURE__*/React__default.default.createElement(shared.ItemContext.Provider, { value: { variant, disabled, inactive: Boolean(inactiveText), inlineDescriptionId, blockDescriptionId } }, /*#__PURE__*/React__default.default.createElement(LiBox, _extends({ ref: !buttonSemanticsFeatureFlag || listSemantics ? forwardedRef : null, sx: buttonSemanticsFeatureFlag ? merge__default.default(listSemantics || _PrivateItemWrapper ? styles : listItemStyles, listSemantics || _PrivateItemWrapper ? sxProp : {}) : 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: 'break-word' } }, 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;