@atlaskit/avatar-group
Version:
An avatar group displays a number of avatars grouped together in a stack or grid.
153 lines (151 loc) • 5.42 kB
JavaScript
import _extends from "@babel/runtime/helpers/extends";
import React, { useCallback, useEffect, useRef } from 'react';
import { bind } from 'bind-event-listener';
import { KEY_DOWN } from '@atlaskit/ds-lib/keycodes';
import MenuGroup from '@atlaskit/menu/menu-group';
import Section from '@atlaskit/menu/section';
import { getAriaForTrigger } from '@atlaskit/top-layer/get-aria-for-trigger';
import { fromLegacyPlacement } from '@atlaskit/top-layer/placement-map/index';
import { PopoverSurface } from '@atlaskit/top-layer/popover-surface';
import { Popover } from '@atlaskit/top-layer/popover/popover';
import { useAnchoredPopover } from '@atlaskit/top-layer/use-anchored-popover';
import { useArrowNavigation } from '@atlaskit/top-layer/use-arrow-navigation/use-arrow-navigation';
import { usePopoverId } from '@atlaskit/top-layer/use-popover-id';
import AvatarGroupItem from './avatar-group-item';
const topLayerPlacement = fromLegacyPlacement({
legacy: 'bottom-end'
});
function getOverrides(overrides) {
return {
AvatarGroupItem: {
render: (Component, props, index) => /*#__PURE__*/React.createElement(Component, _extends({
key: index
}, props)),
...(overrides === null || overrides === void 0 ? void 0 : overrides.AvatarGroupItem)
},
Avatar: {
render: (Component, props, index) => /*#__PURE__*/React.createElement(Component, _extends({
key: index
}, props)),
...(overrides === null || overrides === void 0 ? void 0 : overrides.Avatar)
},
MoreIndicator: {
render: (Component, props) => /*#__PURE__*/React.createElement(Component, props),
...(overrides === null || overrides === void 0 ? void 0 : overrides.MoreIndicator)
}
};
}
/**
* Top-layer implementation of the avatar group overflow dropdown.
*
* Replaces the legacy `@atlaskit/popup` rendering pipeline
* (Popper.js + Portal + focus-trap + @atlaskit/layering)
* with the native Popover API + CSS Anchor Positioning via `@atlaskit/top-layer`.
*
* Uses `role="menu"` with arrow key navigation for correct menu semantics.
*
* Gated behind the `platform-dst-top-layer` feature flag.
*
* Legacy props that are no-ops in the top-layer path (not accepted here):
* - zIndex: stacking managed by browser top layer
* - shouldRenderToParent: always renders in top layer
* - boundary / rootBoundary: viewport is the natural boundary
* - shouldFlip: CSS Anchor Positioning handles flipping
*/
export function MoreDropdownTopLayer({
isOpen,
onClose,
isTriggeredUsingKeyboard: _isTriggeredUsingKeyboard,
data,
max,
overrides,
onAvatarClick,
testId,
labelId,
renderMoreButton,
handleTriggerClicked,
bindFocus: _bindFocus
}) {
const handleOnClose = useCallback(({
reason: _reason
}) => {
onClose();
}, [onClose]);
const triggerRef = useRef(null);
const popoverRef = useRef(null);
const menuRef = useRef(null);
const overflowMenuTestId = testId ? `${testId}--overflow-menu` : undefined;
const popoverId = usePopoverId();
useAnchoredPopover({
anchorRef: triggerRef,
popoverRef,
placement: topLayerPlacement,
isOpen
});
// Arrow key navigation inside the open menu
useArrowNavigation({
containerRef: menuRef,
onClose,
isEnabled: isOpen
});
// ArrowDown-to-open: when the trigger is focused and the menu is closed,
// pressing ArrowDown opens the menu (WAI-ARIA menu button pattern).
//
// Bind the keydown listener directly to the trigger element via its ref so
// that opening the menu does not trigger a focus-driven re-render of the
// host component, which would unmount and remount the menu items and drop
// menuitem focus.
useEffect(() => {
const trigger = triggerRef.current;
if (!trigger || isOpen) {
return;
}
return bind(trigger, {
type: 'keydown',
listener: function openOnArrowDown(event) {
if (event.key === KEY_DOWN) {
// Prevent page scroll when opening the menu via ArrowDown.
event.preventDefault();
handleTriggerClicked(event);
}
}
});
}, [isOpen, handleTriggerClicked]);
const triggerAria = getAriaForTrigger({
role: 'menu',
isOpen,
popoverId
});
const setTriggerRef = useCallback(node => {
triggerRef.current = node;
}, []);
return /*#__PURE__*/React.createElement(React.Fragment, null, renderMoreButton({
ref: setTriggerRef,
...triggerAria,
onClick: handleTriggerClicked
}), /*#__PURE__*/React.createElement(Popover, {
ref: popoverRef,
id: popoverId,
role: "menu",
label: "avatar group",
isOpen: isOpen,
onClose: handleOnClose,
shouldAnimate: true,
placement: topLayerPlacement,
testId: overflowMenuTestId ? `${overflowMenuTestId}--content` : undefined
}, /*#__PURE__*/React.createElement(PopoverSurface, null, /*#__PURE__*/React.createElement("div", {
ref: menuRef
}, /*#__PURE__*/React.createElement(MenuGroup, {
minWidth: 250,
maxHeight: 300
}, /*#__PURE__*/React.createElement(Section, {
titleId: labelId,
testId: testId ? `${testId}--section` : undefined
}, data.slice(max).map((avatar, index) => getOverrides(overrides).AvatarGroupItem.render(AvatarGroupItem, {
avatar,
onAvatarClick,
testId: testId ? `${testId}--avatar-group-item-${index + max}` : undefined,
index: index + max,
role: 'menuitem'
}, index + max))))))));
}