@primer/react
Version:
An implementation of GitHub's Primer Design System using React
390 lines (366 loc) • 13.2 kB
JavaScript
import { ChevronDownIcon } from '@primer/octicons-react';
import React, { isValidElement } from 'react';
import styled from 'styled-components';
import { ActionList } from '../ActionList/index.js';
import { SubItem } from '../ActionList/Item.js';
import { ActionListContainerContext } from '../ActionList/ActionListContainerContext.js';
import Box from '../Box/Box.js';
import Octicon from '../Octicon/Octicon.js';
import sx from '../sx.js';
import { defaultSxProp } from '../utils/defaultSxProp.js';
import { useId } from '../hooks/useId.js';
import useIsomorphicLayoutEffect from '../utils/useIsomorphicLayoutEffect.js';
import '../FeatureFlags/FeatureFlags.js';
import { useFeatureFlag } from '../FeatureFlags/useFeatureFlag.js';
import '../FeatureFlags/DefaultFeatureFlags.js';
import classes from '../ActionList/ActionList.module.css.js';
import { toggleStyledComponent } from '../internal/utils/toggleStyledComponent.js';
import merge from 'deepmerge';
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 getSubnavStyles = depth => {
return {
paddingLeft: depth > 0 ? depth + 2 : null,
// Indent sub-items
fontSize: depth > 0 ? 0 : 1,
// Reduce font size of sub-items
fontWeight: depth > 0 ? 'normal' : null // Sub-items don't get bolded
};
};
// ----------------------------------------------------------------------------
// NavList
const NavBox = toggleStyledComponent('primer_react_css_modules_staff', 'nav', styled.nav.withConfig({
displayName: "NavList__NavBox",
componentId: "sc-1c8ygf7-0"
})(sx));
const Root = /*#__PURE__*/React.forwardRef(({
children,
...props
}, ref) => {
return /*#__PURE__*/React.createElement(NavBox, _extends({}, props, {
ref: ref
}), /*#__PURE__*/React.createElement(ActionListContainerContext.Provider, {
value: {
container: 'NavList'
}
}, /*#__PURE__*/React.createElement(ActionList, null, children)));
});
Root.displayName = 'NavList';
// ----------------------------------------------------------------------------
// NavList.Item
const Item = /*#__PURE__*/React.forwardRef(({
'aria-current': ariaCurrent,
children,
defaultOpen,
sx: sxProp = defaultSxProp,
...props
}, ref) => {
const enabled = useFeatureFlag('primer_react_css_modules_staff');
const {
depth
} = React.useContext(SubNavContext);
// Get SubNav from children
const subNav = React.Children.toArray(children).find(child => /*#__PURE__*/isValidElement(child) && child.type === SubNav);
// Get children without SubNav or TrailingAction
const childrenWithoutSubNavOrTrailingAction = React.Children.toArray(children).filter(child => /*#__PURE__*/isValidElement(child) ? child.type !== SubNav && child.type !== TrailingAction : true);
if (! /*#__PURE__*/isValidElement(subNav) && defaultOpen)
// eslint-disable-next-line no-console
console.error('NavList.Item must have a NavList.SubNav to use defaultOpen.');
// Render ItemWithSubNav if SubNav is present
if (subNav && /*#__PURE__*/isValidElement(subNav)) {
return /*#__PURE__*/React.createElement(ItemWithSubNav, {
subNav: subNav,
depth: depth,
defaultOpen: defaultOpen,
sx: sxProp,
style: {
'--subitem-depth': depth
}
}, childrenWithoutSubNavOrTrailingAction);
}
return /*#__PURE__*/React.createElement(ActionList.LinkItem, _extends({
ref: ref,
"aria-current": ariaCurrent,
active: Boolean(ariaCurrent) && ariaCurrent !== 'false',
sx: enabled ? undefined : merge(getSubnavStyles(depth), sxProp),
style: {
'--subitem-depth': depth
}
}, props), children);
});
Item.displayName = 'NavList.Item';
// ----------------------------------------------------------------------------
// ItemWithSubNav (internal)
const ItemWithSubNavContext = /*#__PURE__*/React.createContext({
buttonId: '',
subNavId: '',
isOpen: false
});
function ItemWithSubNav({
children,
subNav,
depth,
defaultOpen,
style = {},
sx: sxProp = defaultSxProp
}) {
var _ref;
const buttonId = useId();
const subNavId = useId();
const [isOpen, setIsOpen] = React.useState((_ref = defaultOpen || null) !== null && _ref !== undefined ? _ref : false);
const subNavRef = React.useRef(null);
const [containsCurrentItem, setContainsCurrentItem] = React.useState(false);
useIsomorphicLayoutEffect(() => {
if (subNavRef.current) {
// Check if SubNav contains current item
// valid values: page, step, location, date, time, true and false
const currentItem = subNavRef.current.querySelector('[aria-current]:not([aria-current=false])');
if (currentItem) {
setContainsCurrentItem(true);
setIsOpen(true);
}
}
}, [subNav, buttonId]);
const enabled = useFeatureFlag('primer_react_css_modules_staff');
if (enabled) {
if (sxProp !== defaultSxProp) {
return /*#__PURE__*/React.createElement(ItemWithSubNavContext.Provider, {
value: {
buttonId,
subNavId,
isOpen
}
}, /*#__PURE__*/React.createElement(ActionList.Item, {
id: buttonId,
"aria-expanded": isOpen,
"aria-controls": subNavId,
active: !isOpen && containsCurrentItem,
onClick: () => setIsOpen(open => !open),
style: style,
sx: sxProp
}, children, /*#__PURE__*/React.createElement(ActionList.TrailingVisual, null, /*#__PURE__*/React.createElement(ChevronDownIcon, {
className: classes.ExpandIcon
})), /*#__PURE__*/React.createElement(SubItem, null, /*#__PURE__*/React.cloneElement(subNav, {
ref: subNavRef
}))));
}
return /*#__PURE__*/React.createElement(ItemWithSubNavContext.Provider, {
value: {
buttonId,
subNavId,
isOpen
}
}, /*#__PURE__*/React.createElement(ActionList.Item, {
id: buttonId,
"aria-expanded": isOpen,
"aria-controls": subNavId,
active: !isOpen && containsCurrentItem,
onClick: () => setIsOpen(open => !open),
style: style
}, children, /*#__PURE__*/React.createElement(ActionList.TrailingVisual, null, /*#__PURE__*/React.createElement(ChevronDownIcon, {
className: classes.ExpandIcon
})), /*#__PURE__*/React.createElement(SubItem, null, /*#__PURE__*/React.cloneElement(subNav, {
ref: subNavRef
}))));
}
return /*#__PURE__*/React.createElement(ItemWithSubNavContext.Provider, {
value: {
buttonId,
subNavId,
isOpen
}
}, /*#__PURE__*/React.createElement(Box, {
as: "li",
"aria-labelledby": buttonId,
sx: {
listStyle: 'none'
}
}, /*#__PURE__*/React.createElement(ActionList.Item, {
as: "button",
id: buttonId,
"aria-expanded": isOpen,
"aria-controls": subNavId
// When the subNav is closed, how should we indicated that the subNav contains the current item?
,
active: !isOpen && containsCurrentItem,
onClick: () => setIsOpen(open => !open),
sx: merge({
...getSubnavStyles(depth),
fontWeight: containsCurrentItem ? 'bold' : null // Parent item is bold if any of it's sub-items are current
}, sxProp)
}, children, /*#__PURE__*/React.createElement(ActionList.TrailingVisual, null, /*#__PURE__*/React.createElement(Octicon, {
icon: ChevronDownIcon,
sx: {
transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)'
}
}))), /*#__PURE__*/React.createElement("div", {
ref: subNavRef
}, subNav)));
}
ItemWithSubNav.displayName = "ItemWithSubNav"; // ----------------------------------------------------------------------------
// NavList.SubNav
const SubNavContext = /*#__PURE__*/React.createContext({
depth: 0
});
// NOTE: SubNav must be a direct child of an Item
const SubNav = /*#__PURE__*/React.forwardRef(({
children,
sx: sxProp = defaultSxProp
}, forwardedRef) => {
const {
buttonId,
subNavId,
isOpen
} = React.useContext(ItemWithSubNavContext);
const {
depth
} = React.useContext(SubNavContext);
const enabled = useFeatureFlag('primer_react_css_modules_staff');
if (!buttonId || !subNavId) {
// eslint-disable-next-line no-console
console.error('NavList.SubNav must be a child of a NavList.Item');
}
if (depth > 3) {
// TODO: write a more informative error message that directs people to rethink their IA
// eslint-disable-next-line no-console
console.error('NavList.SubNav only supports four levels of nesting');
return null;
}
if (enabled) {
if (sxProp !== defaultSxProp) {
return /*#__PURE__*/React.createElement(SubNavContext.Provider, {
value: {
depth: depth + 1
}
}, /*#__PURE__*/React.createElement(Box, {
as: "ul",
id: subNavId,
"aria-labelledby": buttonId,
className: classes.SubGroup,
ref: forwardedRef,
sx: sxProp
}, children));
}
return /*#__PURE__*/React.createElement(SubNavContext.Provider, {
value: {
depth: depth + 1
}
}, /*#__PURE__*/React.createElement("ul", {
className: classes.SubGroup,
id: subNavId,
"aria-labelledby": buttonId,
ref: forwardedRef
}, children));
}
return /*#__PURE__*/React.createElement(SubNavContext.Provider, {
value: {
depth: depth + 1
}
}, /*#__PURE__*/React.createElement(Box, {
as: "ul",
id: subNavId,
"aria-labelledby": buttonId,
sx: merge({
padding: 0,
margin: 0,
display: isOpen ? 'block' : 'none'
}, sxProp)
}, children));
});
SubNav.displayName = 'NavList.SubNav';
// ----------------------------------------------------------------------------
// NavList.LeadingVisual
const LeadingVisual = ActionList.LeadingVisual;
LeadingVisual.displayName = 'NavList.LeadingVisual';
// ----------------------------------------------------------------------------
// NavList.TrailingVisual
const TrailingVisual = ActionList.TrailingVisual;
TrailingVisual.displayName = 'NavList.TrailingVisual';
// ----------------------------------------------------------------------------
// NavList.Divider
const Divider = ActionList.Divider;
Divider.displayName = 'NavList.Divider';
// NavList.TrailingAction
const TrailingAction = ActionList.TrailingAction;
TrailingAction.displayName = 'NavList.TrailingAction';
// ----------------------------------------------------------------------------
// NavList.Group
const defaultSx = {};
const Group = ({
title,
children,
sx: sxProp = defaultSx,
...props
}) => {
const enabled = useFeatureFlag('primer_react_css_modules_staff');
if (enabled) {
if (sxProp !== defaultSx) {
return /*#__PURE__*/React.createElement(Box, {
sx: sxProp,
as: "li",
"data-component": "ActionList.Group"
}, title ? /*#__PURE__*/React.createElement(ActionList.GroupHeading, null, title) : null, children);
}
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(ActionList.Divider, null), /*#__PURE__*/React.createElement(ActionList.Group, props, title ? /*#__PURE__*/React.createElement(ActionList.GroupHeading, {
as: "h3",
"data-component": "ActionList.GroupHeading"
}, title) : null, children));
}
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(ActionList.Divider, {
sx: {
'&:first-child': {
display: 'none'
}
}
}), /*#__PURE__*/React.createElement(ActionList.Group, _extends({}, props, {
// If somebody tries to pass the `title` prop AND a `NavList.GroupHeading` as a child, hide the `ActionList.GroupHeading`
sx: merge(sxProp, {
':has([data-component="GroupHeadingWrap"] + ul > [data-component="GroupHeadingWrap"])': {
'& > [data-component="GroupHeadingWrap"]': {
display: 'none'
}
}
})
}), title ? /*#__PURE__*/React.createElement(ActionList.GroupHeading, {
as: "h3",
"data-component": "ActionList.GroupHeading"
}, title) : null, children));
};
Group.displayName = 'NavList.Group';
/**
* This is an alternative to the `title` prop on `NavList.Group`.
* It was primarily added to allow links in group headings.
*/
const GroupHeading = ({
as = 'h3',
sx: sxProp = defaultSxProp,
...rest
}) => {
return /*#__PURE__*/React.createElement(ActionList.GroupHeading, _extends({
as: as,
sx: merge({
'> a {': {
color: 'var(--fgColor-default)',
textDecoration: 'inherit',
':hover': {
textDecoration: 'underline'
}
}
}, sxProp),
"data-component": "NavList.GroupHeading",
headingWrapElement: "li"
}, rest));
};
GroupHeading.displayName = "GroupHeading";
// ----------------------------------------------------------------------------
// Export
const NavList = Object.assign(Root, {
Item,
SubNav,
LeadingVisual,
TrailingVisual,
TrailingAction,
Divider,
Group,
GroupHeading
});
export { NavList };