UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

355 lines (347 loc) • 11.9 kB
import { c } from 'react-compiler-runtime'; import React, { forwardRef, useState, useCallback, useRef } from 'react'; import { KebabHorizontalIcon } from '@primer/octicons-react'; import { ActionList } from '../ActionList/index.js'; import useIsomorphicLayoutEffect from '../utils/useIsomorphicLayoutEffect.js'; import { useOnEscapePress } from '../hooks/useOnEscapePress.js'; import { useResizeObserver } from '../hooks/useResizeObserver.js'; import { useOnOutsideClick } from '../hooks/useOnOutsideClick.js'; import { IconButton } from '../Button/IconButton.js'; import { useFocusZone } from '../hooks/useFocusZone.js'; import styles from './ActionBar.module.css.js'; import { clsx } from 'clsx'; import { jsx, jsxs } from 'react/jsx-runtime'; import { FocusKeys } from '@primer/behaviors'; import { ActionMenu } from '../ActionMenu/ActionMenu.js'; const ActionBarContext = /*#__PURE__*/React.createContext({ size: 'medium', setChildrenWidth: () => null }); /* small (28px), medium (32px), large (40px) */ const MORE_BTN_WIDTH = 86; const getValidChildren = children => { return React.Children.toArray(children).filter(child => { return /*#__PURE__*/React.isValidElement(child); }); }; const calculatePossibleItems = (childWidthArray, navWidth, moreMenuWidth = 0) => { const widthToFit = navWidth - moreMenuWidth; let breakpoint = childWidthArray.length; // assume all items will fit let sumsOfChildWidth = 0; for (const [index, childWidth] of childWidthArray.entries()) { sumsOfChildWidth = sumsOfChildWidth + childWidth.width; // + GAP if (sumsOfChildWidth > widthToFit) { breakpoint = index; break; } else { continue; } } return breakpoint; }; const overflowEffect = (navWidth, moreMenuWidth, childArray, childWidthArray, updateListAndMenu, hasActiveMenu) => { if (childWidthArray.length === 0) { updateListAndMenu({ items: childArray, menuItems: [] }); } const numberOfItemsPossible = calculatePossibleItems(childWidthArray, navWidth); const numberOfItemsPossibleWithMoreMenu = calculatePossibleItems(childWidthArray, navWidth, moreMenuWidth || MORE_BTN_WIDTH); const items = []; const menuItems = []; // First, we check if we can fit all the items with their icons if (childArray.length >= numberOfItemsPossible) { /* Below is an accessibility requirement. Never show only one item in the overflow menu. * If there is only one item left to display in the overflow menu according to the calculation, * we need to pull another item from the list into the overflow menu. */ const numberOfItemsInMenu = childArray.length - numberOfItemsPossibleWithMoreMenu; const numberOfListItems = numberOfItemsInMenu === 1 ? numberOfItemsPossibleWithMoreMenu - 1 : numberOfItemsPossibleWithMoreMenu; for (const [index, child] of childArray.entries()) { if (index < numberOfListItems) { items.push(child); //if the last item is a divider } else if (childWidthArray[index].text === 'divider') { if (index === numberOfListItems - 1 || index === numberOfListItems) { continue; } else { const divider = /*#__PURE__*/React.createElement(ActionList.Divider, { key: index }); menuItems.push(divider); } } else { menuItems.push(child); } } updateListAndMenu({ items, menuItems }); } else if (numberOfItemsPossible > childArray.length && hasActiveMenu) { /* If the items fit in the list and there are items in the overflow menu, we need to move them back to the list */ updateListAndMenu({ items: childArray, menuItems: [] }); } }; const ActionBar = props => { const { size = 'medium', children, 'aria-label': ariaLabel, flush = false, className } = props; const [childWidthArray, setChildWidthArray] = useState([]); const setChildrenWidth = useCallback(size_0 => { setChildWidthArray(arr => { const newArr = [...arr, size_0]; return newArr; }); }, []); const navRef = useRef(null); const listRef = useRef(null); const moreMenuRef = useRef(null); const moreMenuBtnRef = useRef(null); const containerRef = React.useRef(null); const validChildren = getValidChildren(children); // Responsive props object manages which items are in the list and which items are in the menu. const [responsiveProps, setResponsiveProps] = useState({ items: validChildren, menuItems: [] }); // Make sure to have the fresh props data for list items when children are changed (keeping aria-current up-to-date) const listItems = responsiveProps.items.map(item => { var _validChildren$find; return (_validChildren$find = validChildren.find(child => child.key === item.key)) !== null && _validChildren$find !== void 0 ? _validChildren$find : item; }); // Make sure to have the fresh props data for menu items when children are changed (keeping aria-current up-to-date) const menuItems = responsiveProps.menuItems.map(menuItem => { var _validChildren$find2; return (_validChildren$find2 = validChildren.find(child_0 => child_0.key === menuItem.key)) !== null && _validChildren$find2 !== void 0 ? _validChildren$find2 : menuItem; }); const updateListAndMenu = useCallback(props_0 => { setResponsiveProps(props_0); }, []); useResizeObserver(resizeObserverEntries => { var _moreMenuRef$current$, _moreMenuRef$current; const navWidth = resizeObserverEntries[0].contentRect.width; const moreMenuWidth = (_moreMenuRef$current$ = (_moreMenuRef$current = moreMenuRef.current) === null || _moreMenuRef$current === void 0 ? void 0 : _moreMenuRef$current.getBoundingClientRect().width) !== null && _moreMenuRef$current$ !== void 0 ? _moreMenuRef$current$ : 0; const hasActiveMenu = menuItems.length > 0; navWidth !== 0 && overflowEffect(navWidth, moreMenuWidth, validChildren, childWidthArray, updateListAndMenu, hasActiveMenu); }, navRef); const [isWidgetOpen, setIsWidgetOpen] = useState(false); const closeOverlay = React.useCallback(() => { setIsWidgetOpen(false); }, [setIsWidgetOpen]); const focusOnMoreMenuBtn = React.useCallback(() => { var _moreMenuBtnRef$curre; (_moreMenuBtnRef$curre = moreMenuBtnRef.current) === null || _moreMenuBtnRef$curre === void 0 ? void 0 : _moreMenuBtnRef$curre.focus(); }, []); useOnEscapePress(event => { if (isWidgetOpen) { event.preventDefault(); closeOverlay(); focusOnMoreMenuBtn(); } }, [isWidgetOpen]); useOnOutsideClick({ onClickOutside: closeOverlay, containerRef, ignoreClickRefs: [moreMenuBtnRef] }); useFocusZone({ containerRef: listRef, bindKeys: FocusKeys.ArrowHorizontal | FocusKeys.HomeAndEnd, focusOutBehavior: 'wrap' }); return /*#__PURE__*/jsx(ActionBarContext.Provider, { value: { size, setChildrenWidth }, children: /*#__PURE__*/jsx("div", { ref: navRef, className: clsx(className, styles.Nav), "data-flush": flush, children: /*#__PURE__*/jsxs("div", { ref: listRef, role: "toolbar", className: styles.List, children: [listItems, menuItems.length > 0 && /*#__PURE__*/jsxs(ActionMenu, { children: [/*#__PURE__*/jsx(ActionMenu.Anchor, { children: /*#__PURE__*/jsx(IconButton, { variant: "invisible", "aria-label": `More ${ariaLabel} items`, icon: KebabHorizontalIcon }) }), /*#__PURE__*/jsx(ActionMenu.Overlay, { children: /*#__PURE__*/jsx(ActionList, { children: menuItems.map((menuItem_0, index) => { if (menuItem_0.type === ActionList.Divider) { return /*#__PURE__*/jsx(ActionList.Divider, {}, index); } else { const { children: menuItemChildren, onClick, icon: Icon, 'aria-label': ariaLabel_0, disabled } = menuItem_0.props; return /*#__PURE__*/jsxs(ActionList.Item, { // eslint-disable-next-line primer-react/prefer-action-list-item-onselect onClick: event_0 => { closeOverlay(); focusOnMoreMenuBtn(); typeof onClick === 'function' && onClick(event_0); }, disabled: disabled, children: [Icon ? /*#__PURE__*/jsx(ActionList.LeadingVisual, { children: /*#__PURE__*/jsx(Icon, {}) }) : null, ariaLabel_0] }, menuItemChildren); } }) }) })] })] }) }) }); }; ActionBar.displayName = "ActionBar"; const ActionBarIconButton = /*#__PURE__*/forwardRef((t0, forwardedRef) => { const $ = c(20); let disabled; let onClick; let props; if ($[0] !== t0) { ({ disabled, onClick, ...props } = t0); $[0] = t0; $[1] = disabled; $[2] = onClick; $[3] = props; } else { disabled = $[1]; onClick = $[2]; props = $[3]; } const backupRef = useRef(null); const ref = forwardedRef !== null && forwardedRef !== void 0 ? forwardedRef : backupRef; const { size, setChildrenWidth } = React.useContext(ActionBarContext); let t1; if ($[4] !== props || $[5] !== ref || $[6] !== setChildrenWidth) { t1 = () => { const text = props["aria-label"] ? props["aria-label"] : ""; const domRect = ref.current.getBoundingClientRect(); setChildrenWidth({ text, width: domRect.width }); }; $[4] = props; $[5] = ref; $[6] = setChildrenWidth; $[7] = t1; } else { t1 = $[7]; } let t2; if ($[8] !== ref || $[9] !== setChildrenWidth) { t2 = [ref, setChildrenWidth]; $[8] = ref; $[9] = setChildrenWidth; $[10] = t2; } else { t2 = $[10]; } useIsomorphicLayoutEffect(t1, t2); let t3; if ($[11] !== disabled || $[12] !== onClick) { t3 = event => { var _onClick; if (disabled) { return; } (_onClick = onClick) === null || _onClick === void 0 ? void 0 : _onClick(event); }; $[11] = disabled; $[12] = onClick; $[13] = t3; } else { t3 = $[13]; } const clickHandler = t3; let t4; if ($[14] !== clickHandler || $[15] !== disabled || $[16] !== props || $[17] !== ref || $[18] !== size) { t4 = /*#__PURE__*/jsx(IconButton, { "aria-disabled": disabled, ref: ref, size: size, onClick: clickHandler, ...props, variant: "invisible" }); $[14] = clickHandler; $[15] = disabled; $[16] = props; $[17] = ref; $[18] = size; $[19] = t4; } else { t4 = $[19]; } return t4; }); const VerticalDivider = () => { const $ = c(4); const ref = useRef(null); const { setChildrenWidth } = React.useContext(ActionBarContext); let t0; let t1; if ($[0] !== setChildrenWidth) { t0 = () => { const domRect = ref.current.getBoundingClientRect(); setChildrenWidth({ text: "divider", width: domRect.width }); }; t1 = [ref, setChildrenWidth]; $[0] = setChildrenWidth; $[1] = t0; $[2] = t1; } else { t0 = $[1]; t1 = $[2]; } useIsomorphicLayoutEffect(t0, t1); let t2; if ($[3] === Symbol.for("react.memo_cache_sentinel")) { t2 = /*#__PURE__*/jsx("div", { ref: ref, "data-component": "ActionBar.VerticalDivider", "aria-hidden": "true", className: styles.Divider }); $[3] = t2; } else { t2 = $[3]; } return t2; }; export { ActionBar, ActionBarIconButton, VerticalDivider };