UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

153 lines (146 loc) 5.58 kB
import { clsx } from 'clsx'; import React, { useState, useRef, useEffect } from 'react'; import { DEFAULT_AVATAR_SIZE } from '../Avatar/Avatar.js'; import { isResponsiveValue } from '../hooks/useResponsiveValue.js'; import classes from './AvatarStack.module.css.js'; import { hasInteractiveNodes } from '../internal/utils/hasInteractiveNodes.js'; import { BoxWithFallback } from '../internal/components/BoxWithFallback.js'; import { jsx, jsxs } from 'react/jsx-runtime'; const transformChildren = (children, shape) => { return React.Children.map(children, child => { if (! /*#__PURE__*/React.isValidElement(child)) return child; return /*#__PURE__*/React.cloneElement(child, { ...child.props, square: shape === 'square' ? true : undefined, className: clsx(child.props.className, 'pc-AvatarItem', classes.AvatarItem) }); }); }; const AvatarStackBody = ({ disableExpand, hasInteractiveChildren, stackContainer, children }) => { return /*#__PURE__*/jsx("div", { "data-disable-expand": disableExpand ? '' : undefined, className: clsx({ 'pc-AvatarStack--disableExpand': disableExpand }, 'pc-AvatarStackBody', classes.AvatarStackBody), tabIndex: !hasInteractiveChildren && !disableExpand ? 0 : undefined, ref: stackContainer, children: children }); }; AvatarStackBody.displayName = "AvatarStackBody"; const AvatarStack = ({ children, variant = 'cascade', shape = 'circle', alignRight, disableExpand, size, className, style, sx: sxProp }) => { const [hasInteractiveChildren, setHasInteractiveChildren] = useState(false); const stackContainer = useRef(null); const count = React.Children.count(children); const getAvatarChildSizes = () => { const avatarSizeMap = { narrow: [], regular: [], wide: [] }; return React.Children.toArray(children).reduce((acc, child) => { // if child is not an Avatar, return the default avatar sizes from the accumulator if (! /*#__PURE__*/React.isValidElement(child)) return acc; for (const responsiveKey of Object.keys(avatarSizeMap)) { // if the child has responsive `size` prop values, push the value to the appropriate viewport property in the avatarSizeMap if (isResponsiveValue(child.props.size)) { avatarSizeMap[responsiveKey].push(child.props.size[responsiveKey] || DEFAULT_AVATAR_SIZE); } // otherwise, the size is a number (or undefined), so push the value to all viewport properties in the avatarSizeMap else { avatarSizeMap[responsiveKey].push(child.props.size || DEFAULT_AVATAR_SIZE); } // set the smallest size in each viewport property as the value for that viewport property in the accumulator acc[responsiveKey] = Math.min(...avatarSizeMap[responsiveKey]); } return acc; }, { narrow: DEFAULT_AVATAR_SIZE, regular: DEFAULT_AVATAR_SIZE, wide: DEFAULT_AVATAR_SIZE }); }; const childSizes = getAvatarChildSizes(); useEffect(() => { if (stackContainer.current) { const interactiveChildren = () => { setHasInteractiveChildren(hasInteractiveNodes(stackContainer.current)); }; const observer = new MutationObserver(interactiveChildren); observer.observe(stackContainer.current, { childList: true }); // Call on initial render, then call it again only if there's a mutation interactiveChildren(); return () => { observer.disconnect(); }; } }, []); const getResponsiveAvatarSizeStyles = () => { // if there is no size set on the AvatarStack, use the `size` props of the Avatar children to set the `--avatar-stack-size` CSS variable if (!size) { return { '--stackSize-narrow': `${childSizes.narrow}px`, '--stackSize-regular': `${childSizes.regular}px`, '--stackSize-wide': `${childSizes.wide}px` }; } // if the `size` prop is set and responsive, set the `--avatar-stack-size` CSS variable for each viewport if (isResponsiveValue(size)) { return { '--stackSize-narrow': `${size.narrow || DEFAULT_AVATAR_SIZE}px`, '--stackSize-regular': `${size.regular || DEFAULT_AVATAR_SIZE}px`, '--stackSize-wide': `${size.wide || DEFAULT_AVATAR_SIZE}px` }; } // if the `size` prop is set and not responsive, it is a number, so we can just set the `--avatar-stack-size` CSS variable to that number return { '--avatar-stack-size': `${size}px` }; }; return /*#__PURE__*/jsx(BoxWithFallback, { as: "span", "data-variant": variant, "data-shape": shape, "data-avatar-count": count > 3 ? '3+' : count, "data-align-right": alignRight ? '' : undefined, "data-responsive": !size || isResponsiveValue(size) ? '' : undefined, className: clsx({ 'pc-AvatarStack--variant': variant, 'pc-AvatarStack--shape': shape, 'pc-AvatarStack--two': count === 2, 'pc-AvatarStack--three': count === 3, 'pc-AvatarStack--three-plus': count > 3, 'pc-AvatarStack--right': alignRight }, className, classes.AvatarStack), style: { ...getResponsiveAvatarSizeStyles(), ...style }, sx: sxProp, children: /*#__PURE__*/jsxs(AvatarStackBody, { disableExpand: disableExpand, hasInteractiveChildren: hasInteractiveChildren, stackContainer: stackContainer, children: [' ', transformChildren(children, shape)] }) }); }; AvatarStack.displayName = "AvatarStack"; export { AvatarStack as default };