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