UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

368 lines (352 loc) • 14.9 kB
'use strict'; var reactCompilerRuntime = require('react-compiler-runtime'); var React = require('react'); var styled = require('styled-components'); var octiconsReact = require('@primer/octicons-react'); var utils = require('@primer/behaviors/utils'); var constants = require('../constants.js'); var _VisuallyHidden = require('../_VisuallyHidden.js'); var IconButton = require('../Button/IconButton.js'); var Button = require('../Button/Button.js'); var ThemeProvider = require('../ThemeProvider.js'); var sx = require('../sx.js'); var jsxRuntime = require('react/jsx-runtime'); var AnchoredOverlay = require('../AnchoredOverlay/AnchoredOverlay.js'); var Box = require('../Box/Box.js'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var React__default = /*#__PURE__*/_interopDefault(React); var styled__default = /*#__PURE__*/_interopDefault(styled); const StyledLabelGroupContainer = styled__default.default.div.withConfig({ displayName: "LabelGroup__StyledLabelGroupContainer", componentId: "sc-6tqg8q-0" })(["display:flex;flex-wrap:nowrap;gap:", ";line-height:1;max-width:100%;overflow:hidden;&[data-overflow='inline']{flex-wrap:wrap;}&[data-list]{padding-inline-start:0;margin-block-start:0;margin-block-end:0;list-style-type:none;}", ";"], constants.get('space.1'), sx.default); const ItemWrapper = styled__default.default.div.withConfig({ displayName: "LabelGroup__ItemWrapper", componentId: "sc-6tqg8q-1" })(["display:flex;align-items:center;min-height:28px;&.ItemWrapper--hidden{order:9999;pointer-events:none;visibility:hidden;}"]); // Calculates the width of the overlay to cover the labels/tokens and the expand button. const getOverlayWidth = (buttonClientRect, containerRef, overlayPaddingPx) => { var _containerRef$current; return overlayPaddingPx + buttonClientRect.right - (((_containerRef$current = containerRef.current) === null || _containerRef$current === void 0 ? void 0 : _containerRef$current.getBoundingClientRect().left) || 0); }; const InlineToggle = t0 => { const $ = reactCompilerRuntime.c(7); const { collapseButtonRef, collapseInlineExpandedChildren, expandButtonRef, hiddenItemIds, isOverflowShown, showAllTokensInline } = t0; let t1; if ($[0] !== collapseButtonRef || $[1] !== collapseInlineExpandedChildren || $[2] !== expandButtonRef || $[3] !== hiddenItemIds || $[4] !== isOverflowShown || $[5] !== showAllTokensInline) { t1 = isOverflowShown ? /*#__PURE__*/jsxRuntime.jsx(Button.ButtonComponent, { ref: collapseButtonRef, onClick: collapseInlineExpandedChildren, size: "small", variant: "invisible", children: "Show less" }) : hiddenItemIds.length ? /*#__PURE__*/jsxRuntime.jsxs(Button.ButtonComponent, { ref: expandButtonRef, variant: "invisible", size: "small", onClick: showAllTokensInline, children: [/*#__PURE__*/jsxRuntime.jsxs(_VisuallyHidden, { children: ["Show +", hiddenItemIds.length, " more"] }), /*#__PURE__*/jsxRuntime.jsxs("span", { "aria-hidden": "true", children: ["+", hiddenItemIds.length] })] }) : null; $[0] = collapseButtonRef; $[1] = collapseInlineExpandedChildren; $[2] = expandButtonRef; $[3] = hiddenItemIds; $[4] = isOverflowShown; $[5] = showAllTokensInline; $[6] = t1; } else { t1 = $[6]; } return t1; }; const OverlayToggle = t0 => { const $ = reactCompilerRuntime.c(9); const { children, closeOverflowOverlay, expandButtonRef, hiddenItemIds, isOverflowShown, openOverflowOverlay, overlayPaddingPx, overlayWidth } = t0; let t1; if ($[0] !== children || $[1] !== closeOverflowOverlay || $[2] !== expandButtonRef || $[3] !== hiddenItemIds.length || $[4] !== isOverflowShown || $[5] !== openOverflowOverlay || $[6] !== overlayPaddingPx || $[7] !== overlayWidth) { t1 = hiddenItemIds.length ? /*#__PURE__*/jsxRuntime.jsx(AnchoredOverlay.AnchoredOverlay, { open: isOverflowShown, onOpen: openOverflowOverlay, onClose: closeOverflowOverlay, width: "auto", height: "auto", align: "start", side: "inside-right", anchorRef: expandButtonRef, anchorOffset: overlayPaddingPx * -1, alignmentOffset: overlayPaddingPx * -1, renderAnchor: props => /*#__PURE__*/jsxRuntime.jsxs(Button.ButtonComponent, { variant: "invisible", size: "small", ...props, ref: expandButtonRef, children: [/*#__PURE__*/jsxRuntime.jsxs(_VisuallyHidden, { children: ["Show +", hiddenItemIds.length, " more"] }), /*#__PURE__*/jsxRuntime.jsxs("span", { "aria-hidden": "true", children: ["+", hiddenItemIds.length] })] }), focusZoneSettings: { disabled: true }, children: /*#__PURE__*/jsxRuntime.jsxs(Box, { alignItems: "flex-start", display: "flex", width: overlayWidth, padding: `${overlayPaddingPx}px`, children: [/*#__PURE__*/jsxRuntime.jsx(Box, { display: "flex", flexWrap: "wrap", sx: { gap: 1 }, children: children }), /*#__PURE__*/jsxRuntime.jsx(IconButton.IconButton, { onClick: closeOverflowOverlay, icon: octiconsReact.XIcon, "aria-label": "Close", variant: "invisible", sx: { flexShrink: 0 } })] }) }) : null; $[0] = children; $[1] = closeOverflowOverlay; $[2] = expandButtonRef; $[3] = hiddenItemIds.length; $[4] = isOverflowShown; $[5] = openOverflowOverlay; $[6] = overlayPaddingPx; $[7] = overlayWidth; $[8] = t1; } else { t1 = $[8]; } return t1; }; // TODO: reduce re-renders const LabelGroup = ({ children, visibleChildCount, overflowStyle = 'overlay', sx: sxProp, as = 'ul', className }) => { const containerRef = React__default.default.useRef(null); const collapseButtonRef = React__default.default.useRef(null); const firstHiddenIndexRef = React__default.default.useRef(undefined); const [visibilityMap, setVisibilityMap] = React__default.default.useState({}); const [isOverflowShown, setIsOverflowShown] = React__default.default.useState(false); const [buttonClientRect, setButtonClientRect] = React__default.default.useState({ width: 0, right: 0, height: 0, x: 0, y: 0, top: 0, left: 0, bottom: 0, toJSON: () => undefined }); const { theme } = ThemeProvider.useTheme(); const overlayPaddingPx = parseInt(constants.get('space.2')(theme), 10); const hiddenItemIds = Object.keys(visibilityMap).filter(key => !visibilityMap[key]); // `overlayWidth` is only needed when we render an overlay // if we don't use an overlay, we can skip the width calculation // and save on reflows caused by measuring DOM nodes. const overlayWidth = hiddenItemIds.length && overflowStyle === 'overlay' ? getOverlayWidth(buttonClientRect, containerRef, overlayPaddingPx) : undefined; const expandButtonRef = React__default.default.useCallback(node => { if (node !== null) { const nodeClientRect = node.getBoundingClientRect(); if (nodeClientRect.width !== buttonClientRect.width || nodeClientRect.right !== buttonClientRect.right) { setButtonClientRect(nodeClientRect); } // @ts-ignore you can set `.current` on ref objects or ref callbacks in React expandButtonRef.current = node; } }, [buttonClientRect]); // Sets the visibility map to hide children after the given index. const hideChildrenAfterIndex = React__default.default.useCallback(truncateAfter => { var _containerRef$current2; const containerChildren = ((_containerRef$current2 = containerRef.current) === null || _containerRef$current2 === void 0 ? void 0 : _containerRef$current2.children) || []; const updatedEntries = {}; for (const child of containerChildren) { const targetId = child.getAttribute('data-index'); if (targetId) { updatedEntries[targetId] = parseInt(targetId, 10) < truncateAfter; } } setVisibilityMap(updatedEntries); }, []); const openOverflowOverlay = React__default.default.useCallback(() => setIsOverflowShown(true), [setIsOverflowShown]); const closeOverflowOverlay = React__default.default.useCallback(() => { setIsOverflowShown(false); }, [setIsOverflowShown]); const collapseInlineExpandedChildren = React__default.default.useCallback(() => { setIsOverflowShown(false); if (visibleChildCount && typeof visibleChildCount === 'number') { hideChildrenAfterIndex(visibleChildCount); } // We need to manually re-focus the collapse button if we're not showing the full // list in an overlay. // TODO: get rid of this hack setTimeout(() => { var _expandButtonRef$curr; // @ts-ignore you can set `.current` on ref objects or ref callbacks in React (_expandButtonRef$curr = expandButtonRef.current) === null || _expandButtonRef$curr === void 0 ? void 0 : _expandButtonRef$curr.focus(); }, 10); }, [expandButtonRef, hideChildrenAfterIndex, visibleChildCount]); const showAllTokensInline = React__default.default.useCallback(() => { setVisibilityMap({}); setIsOverflowShown(true); }, [setVisibilityMap, setIsOverflowShown]); React__default.default.useEffect(() => { // If we're not truncating, we don't need to run this useEffect. if (!visibleChildCount || isOverflowShown) { return; } if (visibleChildCount === 'auto') { // Instantiates the IntersectionObserver to track when children fit in the container. const observer = new IntersectionObserver(entries => { const updatedEntries_0 = {}; for (const entry of entries) { // Checks which children are intersecting the root container const targetId_0 = entry.target.getAttribute('data-index'); if (targetId_0) { updatedEntries_0[targetId_0] = entry.isIntersecting; } } // Updates the visibility map based on the intersection results. setVisibilityMap(prev => ({ ...prev, ...updatedEntries_0 })); }, { root: containerRef.current, rootMargin: `0px -${buttonClientRect.width}px 0px 0px`, threshold: 1 }); for (const item of ((_containerRef$current3 = containerRef.current) === null || _containerRef$current3 === void 0 ? void 0 : _containerRef$current3.children) || []) { var _containerRef$current3; if (item.getAttribute('data-index')) { observer.observe(item); } } return () => observer.disconnect(); } // We're not auto truncating, so we need to hide children after the given `visibleChildCount`. else { hideChildrenAfterIndex(visibleChildCount); } }, [buttonClientRect, visibleChildCount, hideChildrenAfterIndex, isOverflowShown]); // Updates the index of the first hidden child. // We need to keep track of this so we can focus the first hidden child when the overflow is shown inline. React__default.default.useEffect(() => { // If we're using an overlay, we don't need to keep track of the first hidden index. if (overflowStyle === 'overlay') { return; } if (hiddenItemIds.length) { firstHiddenIndexRef.current = parseInt(hiddenItemIds[0], 10); } }, [hiddenItemIds, overflowStyle, isOverflowShown]); // Updates the index of the first hidden child. // We need to keep track of this so we can focus the first hidden child when the overflow is shown inline. React__default.default.useEffect(() => { // If we're using an overlay, we don't need to focus the first child that was previously hidden. if (overflowStyle === 'overlay') { return; } const firstHiddenChildDOM = document.querySelector(`[data-index="${firstHiddenIndexRef.current}"]`); const focusableChild = firstHiddenChildDOM ? utils.getFocusableChild(firstHiddenChildDOM) : null; if (isOverflowShown) { // If the first hidden child is focusable, focus it. // Otherwise, focus the collapse button. if (focusableChild) { focusableChild.focus(); } else { var _collapseButtonRef$cu; (_collapseButtonRef$cu = collapseButtonRef.current) === null || _collapseButtonRef$cu === void 0 ? void 0 : _collapseButtonRef$cu.focus(); } } }, [overflowStyle, isOverflowShown]); const isList = as === 'ul' || as === 'ol'; const ToggleWrapper = isList ? 'li' : React__default.default.Fragment; // If truncation is enabled, we need to render based on truncation logic. return visibleChildCount ? /*#__PURE__*/jsxRuntime.jsxs(StyledLabelGroupContainer, { ref: containerRef, "data-overflow": overflowStyle === 'inline' && isOverflowShown ? 'inline' : undefined, "data-list": isList || undefined, sx: sxProp, className: className, as: as, children: [React__default.default.Children.map(children, (child_0, index) => /*#__PURE__*/jsxRuntime.jsx(ItemWrapper // data-index is used as an identifier we can use in the IntersectionObserver , { "data-index": index, className: hiddenItemIds.includes(index.toString()) ? 'ItemWrapper--hidden' : undefined, as: isList ? 'li' : 'span', children: child_0 }, index)), /*#__PURE__*/jsxRuntime.jsx(ToggleWrapper, { children: overflowStyle === 'inline' ? /*#__PURE__*/jsxRuntime.jsx(InlineToggle, { collapseButtonRef: collapseButtonRef, collapseInlineExpandedChildren: collapseInlineExpandedChildren, expandButtonRef: expandButtonRef, hiddenItemIds: hiddenItemIds, isOverflowShown: isOverflowShown, showAllTokensInline: showAllTokensInline, totalLength: React__default.default.Children.toArray(children).length }) : /*#__PURE__*/jsxRuntime.jsx(OverlayToggle, { closeOverflowOverlay: closeOverflowOverlay, expandButtonRef: expandButtonRef, hiddenItemIds: hiddenItemIds, isOverflowShown: isOverflowShown, openOverflowOverlay: openOverflowOverlay, overlayPaddingPx: overlayPaddingPx, overlayWidth: overlayWidth, totalLength: React__default.default.Children.toArray(children).length, children: children }) })] }) : /*#__PURE__*/jsxRuntime.jsx(StyledLabelGroupContainer, { "data-overflow": "inline", "data-list": isList || undefined, sx: sxProp, as: as, className: className, children: isList ? React__default.default.Children.map(children, (child_1, index_0) => { return /*#__PURE__*/jsxRuntime.jsx("li", { children: child_1 }, index_0); }) : children }); }; LabelGroup.displayName = 'LabelGroup'; module.exports = LabelGroup;