@atlaskit/avatar-group
Version:
An avatar group displays a number of avatars grouped together in a stack or grid.
233 lines (227 loc) • 7.78 kB
JavaScript
import _extends from "@babel/runtime/helpers/extends";
import React, { useCallback, useEffect, useState } from 'react';
import { bind } from 'bind-event-listener';
import Avatar from '@atlaskit/avatar';
import { KEY_DOWN } from '@atlaskit/ds-lib/keycodes';
import noop from '@atlaskit/ds-lib/noop';
import useFocus from '@atlaskit/ds-lib/use-focus-event';
import { Section } from '@atlaskit/menu';
import Popup from '@atlaskit/popup';
import Tooltip from '@atlaskit/tooltip';
import AvatarGroupItem from './avatar-group-item';
import Grid from './grid';
import FocusManager from './internal/components/focus-manager';
import PopupAvatarGroup from './internal/components/popup-avatar-group';
import MoreIndicator from './more-indicator';
import Stack from './stack';
import { composeUniqueKey } from './utils';
const MAX_COUNT = {
grid: 11,
stack: 5
};
function getOverrides(overrides) {
return {
AvatarGroupItem: {
render: (Component, props, index) => /*#__PURE__*/React.createElement(Component, _extends({}, props, {
key: composeUniqueKey(props.avatar, index)
})),
...(overrides && overrides.AvatarGroupItem)
},
Avatar: {
render: (Component, props, index) => /*#__PURE__*/React.createElement(Component, _extends({}, props, {
key: composeUniqueKey(props, index)
})),
...(overrides && overrides.Avatar)
},
MoreIndicator: {
render: (Component, props) => /*#__PURE__*/React.createElement(Component, props),
...(overrides && overrides.MoreIndicator)
}
};
}
/**
* __Avatar group__
*
* An avatar group displays a number of avatars grouped together in a stack or grid.
*
* - [Examples](https://atlassian.design/components/avatar-group/examples)
* - [Code](https://atlassian.design/components/avatar-group/code)
* - [Usage](https://atlassian.design/components/avatar-group/usage)
*/
const AvatarGroup = ({
appearance = 'stack',
avatar = Avatar,
borderColor,
boundariesElement,
data,
isTooltipDisabled,
maxCount,
onAvatarClick,
onMoreClick,
overrides,
showMoreButtonProps = {},
size = 'medium',
testId,
label = 'avatar group',
moreIndicatorLabel,
tooltipPosition = 'bottom',
shouldPopupRenderToParent = false
}) => {
const [isTriggeredUsingKeyboard, setTriggeredUsingKeyboard] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const onClose = useCallback(() => setIsOpen(false), []);
const handleTriggerClicked = useCallback(event => {
const {
clientX,
clientY,
type
} = event;
// Hitting enter/space is registered as a click with both clientX and clientY === 0
if (type === 'keydown' || clientX === 0 || clientY === 0) {
setTriggeredUsingKeyboard(true);
}
setIsOpen(isOpen => !isOpen);
}, []);
const {
isFocused,
bindFocus
} = useFocus();
// When a trigger is focused, we want to open the popup
// the user presses the DownArrow
useEffect(() => {
// Set initial value if popup is closed
if (!isOpen) {
setTriggeredUsingKeyboard(false);
}
// Only need to listen for keydown when focused
if (!isFocused) {
return noop;
}
// Being safe: we don't want to open the popup if it is already open
// Note: This shouldn't happen as the trigger should not be able to get focus
if (isOpen) {
return noop;
}
bind(window, {
type: 'keydown',
listener: function openOnKeyDown(e) {
if (e.key === KEY_DOWN) {
// prevent page scroll
e.preventDefault();
handleTriggerClicked(e);
}
}
});
const unbind = () => {
bind(window, {
type: 'keydown',
listener: function openOnKeyDown(e) {
if (e.key === KEY_DOWN) {
// prevent page scroll
e.preventDefault();
handleTriggerClicked(e);
}
}
});
};
return unbind;
}, [isFocused, isOpen, handleTriggerClicked]);
function renderMoreDropdown(max, total) {
if (total <= max) {
return null;
}
const renderMoreButton = ({
'aria-controls': ariaControls,
'aria-expanded': ariaExpanded,
'aria-haspopup': ariaHasPopup,
onClick,
...props
}) => getOverrides(overrides).MoreIndicator.render(MoreIndicator, {
buttonProps: showMoreButtonProps,
borderColor: borderColor,
count: total - max,
size: size,
testId: testId && `${testId}--overflow-menu--trigger`,
isActive: isOpen,
moreIndicatorLabel: moreIndicatorLabel,
'aria-controls': ariaControls,
'aria-expanded': ariaExpanded,
'aria-haspopup': ariaHasPopup,
onClick,
...props
});
// bail if the consumer wants to handle onClick
if (typeof onMoreClick === 'function') {
return renderMoreButton({
onClick: onMoreClick
});
}
// split boundariesElement into `boundary` and `rootBoundary` props for Popup
const boundary = boundariesElement === 'scrollParent' ? 'clippingParents' : undefined;
const rootBoundary = (() => {
if (boundariesElement === 'scrollParent') {
return undefined;
}
return boundariesElement === 'window' ? 'document' : 'viewport';
})();
return /*#__PURE__*/React.createElement(Popup, {
isOpen: isOpen,
onClose: onClose,
placement: "bottom-end",
boundary: boundary,
rootBoundary: rootBoundary,
shouldFlip: true,
zIndex: 510,
shouldRenderToParent: shouldPopupRenderToParent,
content: ({
setInitialFocusRef
}) => /*#__PURE__*/React.createElement(FocusManager, null, /*#__PURE__*/React.createElement(PopupAvatarGroup, {
onClick: e => e.stopPropagation(),
minWidth: 250,
maxHeight: 300,
setInitialFocusRef: isTriggeredUsingKeyboard ? setInitialFocusRef : undefined
}, /*#__PURE__*/React.createElement(Section, null, data.slice(max).map((avatar, index) => getOverrides(overrides).AvatarGroupItem.render(AvatarGroupItem, {
avatar,
onAvatarClick,
testId: testId && `${testId}--avatar-group-item-${index + max}`,
index: index + max
},
// This index holds the true index,
// adding up the index of non-overflowed avatars and overflowed avatars.
index + max))))),
trigger: triggerProps => renderMoreButton({
...triggerProps,
...bindFocus,
onClick: handleTriggerClicked
}),
testId: testId && `${testId}--overflow-menu`
});
}
const max = maxCount === undefined || maxCount === 0 ? MAX_COUNT[appearance] : maxCount;
const total = data.length;
const maxAvatar = total > max ? max - 1 : max;
const Group = appearance === 'stack' ? Stack : Grid;
return /*#__PURE__*/React.createElement(Group, {
testId: testId && `${testId}--avatar-group`,
"aria-label": label
}, data.slice(0, maxAvatar).map((avatarData, idx) => {
const callback = avatarData.onClick || onAvatarClick;
const finalAvatar = getOverrides(overrides).Avatar.render(avatar, {
...avatarData,
size,
borderColor: borderColor || avatarData.borderColor,
testId: testId && `${testId}--avatar-${idx}`,
onClick: callback ? (event, analyticsEvent) => {
callback(event, analyticsEvent, idx);
} : undefined,
stackIndex: max - idx
}, idx);
return !isTooltipDisabled && !avatarData.isDisabled ? /*#__PURE__*/React.createElement(Tooltip, {
key: composeUniqueKey(avatarData, idx),
content: avatarData.name,
testId: testId && `${testId}--tooltip-${idx}`,
position: tooltipPosition
}, finalAvatar) : finalAvatar;
}), renderMoreDropdown(+maxAvatar, total));
};
export default AvatarGroup;