@atlaskit/avatar-group 12.7.2 → 12.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,107 @@
1
1
  # @atlaskit/avatar-group
2
2
 
3
+ ## 12.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`2bed6255731de`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/2bed6255731de) -
8
+ Top-layer adoption work behind the `platform-dst-top-layer` feature flag. Public adopter APIs are
9
+ intentionally kept narrow while the top-layer API surface settles, with one exception called out
10
+ below.
11
+
12
+ Highlights:
13
+ - Pass the full `[along, away]` legacy popper offset through to the new top-layer
14
+ `placement.offset` API (via `fromLegacyPlacement`). Previously only the `away` axis was
15
+ forwarded, which dropped the `along` offset for consumers of `Popup`, `PopupSelect`,
16
+ `Spotlight`, and `Tooltip` when `platform-dst-top-layer` is enabled.
17
+ - Fix broken import of `dialogHeight` and `dialogWidth` from the removed utils module in
18
+ `@atlaskit/modal-dialog`.
19
+
20
+ Public API:
21
+ - **`@atlaskit/tooltip`** (`minor`): add an optional `testId?: string` field to `TriggerProps`.
22
+ This is additive (no existing prop changes shape). Required because `@atlaskit/button/new` (and
23
+ other `Pressable`-backed primitives) overwrite `data-testid` from spread, so the legacy
24
+ `(triggerProps as any)['data-testid']` workaround is silently absorbed by those consumers. A
25
+ typed `testId` field flows through their own `testId` destructure instead, restoring
26
+ `data-testid` propagation onto the rendered trigger element.
27
+ - **`@atlaskit/popup`**, **`@atlaskit/dropdown-menu`** (`patch`): no public type changes. Wider
28
+ `aria-haspopup` unions that the FF-on path produces are bridged at the package boundary into
29
+ `@atlaskit/top-layer` with localised `FUDGE(top-layer-api)` casts, documented in
30
+ `packages/design-system/top-layer/notes/decisions/migration-roadmap.md` ("Open API decisions
31
+ deferred to a follow-up PR"). They will be widened in a follow-up `minor` PR once the top-layer
32
+ API is committed.
33
+ - **`@atlaskit/modal-dialog`**, **`@atlaskit/select`**, **`@atlaskit/spotlight`**
34
+ (`patch`/`minor`): no public type changes; bug fixes only.
35
+
36
+ Merge-readiness fixes (FF-on test wiring + adopter behavior):
37
+ - **`@atlaskit/popup`** (`minor`): wire the compositional `PopupContent` to delegate to
38
+ `PopupContentTopLayer` when `platform-dst-top-layer` is enabled. Previously only the legacy
39
+ `Popup` component had the FF branch, leaving consumers of the compositional API on the legacy
40
+ popper path.
41
+ - **`@atlaskit/select`** (`minor`): add an `onClick` handler to the `PopupSelect` top-layer
42
+ trigger so clicks open/close the menu (mirrors the legacy global click handler in
43
+ `popup-select.tsx`). Add explicit Escape handling on the menu's `onKeyDown` so the menu closes
44
+ and focus returns to the trigger.
45
+ - **`@atlaskit/top-layer`** (`patch`): the `<dialog>` rendered by the Dialog primitive now sets
46
+ `aria-modal="true"` explicitly. Modern browsers infer modal semantics from `.showModal()` but
47
+ some assistive tech still keys off the explicit attribute.
48
+ - **`@atlaskit/top-layer`** (`patch`): guard `use-anchor-positioning` against environments where
49
+ `ResizeObserver` is not defined (e.g. jest's `node` environment, used by the post-office test
50
+ suite). The observer is used to wait for the popover's first valid layout before measuring;
51
+ consumers in non-DOM jest environments now get a no-op observer and the scroll/resize listeners
52
+ still apply if the host environment polyfills `showPopover`. Real browsers always have
53
+ `ResizeObserver`.
54
+ - **`@atlaskit/modal-dialog`** (`patch`): on the FF-on path, drop the `tabIndex={-1}` (and unused
55
+ `:focus-visible` outline) from the modal content wrapper. The native `<dialog>.showModal()`
56
+ focus-delegate algorithm picks the first focusable descendant (including `tabindex=-1`), and the
57
+ wrapper was hijacking initial focus from the close button. Also honor `shouldReturnFocus={ref}`
58
+ on the FF-on path (an unmount-cleanup focuses the ref after `dialog.close()` so it overrides the
59
+ browser's automatic return-to-trigger). Boolean `shouldReturnFocus={false}` is not yet honored
60
+ on the FF-on path — see `top-layer/notes/merge-blockers.md`.
61
+ - **`@atlaskit/datetime-picker`** (`patch`): on the FF-on path, set `mode="manual"` on the
62
+ `Popup.Content` rendered by both `internal/menu-top-layer.tsx` (date-picker calendar) and
63
+ `internal/fixed-layer-menu-top-layer.tsx` (time-picker menu). With the default `mode="auto"`,
64
+ the same click event that opens the menu (which targets the react-select combobox input —
65
+ outside the popover element) bubbles to the browser's native popover light-dismiss handler and
66
+ immediately closes the menu. react-select / DateTimePicker already own outside-click and Esc
67
+ dismissal via their own state, so opting out of the native auto-dismiss is the correct
68
+ integration. Also extend the existing Esc → trigger-focus restoration in
69
+ `components/date-picker.tsx` to the FF-on path (manual mode disables the browser's built-in
70
+ focus return, and the legacy code path was already handling this for itself behind an FF
71
+ negation).
72
+ - **`@atlaskit/popup`** (no public API change): no source changes — only FF-on Playwright
73
+ spec/example fixes drove the suite from 21/3/2 to 27/0/0. Notable: the two `test.fixme`'d
74
+ nested-popover cases were not browser limitations; `popover="auto"` chains correctly via DOM
75
+ ancestry (the original fixmes had the wrong testId selector). Added `testId` props to two
76
+ examples (`16-popup-with-a11y-props`, `18-should-fit-container`) so default-shape tests can
77
+ reach the trigger.
78
+ - Test alignment for FF-on Playwright suites across `popup`, `select`, `datetime-picker`,
79
+ `inline-dialog`, `inline-message`, and `modal-dialog`: selector updates to match the new
80
+ top-layer testId convention (`${testId}--content`, `[role="dialog"][aria-label="calendar"]`),
81
+ per-spec `skipAxeCheck()` for example-level color-contrast violations unrelated to the
82
+ migration, and focus assertions adjusted to match native `<dialog>` / `Popup.Content` auto-focus
83
+ semantics (focus lands on the first focusable child, not the dialog container itself).
84
+ - **`@atlassian/capacity-planning-capacity-graph`**, **`@atlaskit/color-picker`**,
85
+ **`@atlassian/timeline-table`**, **`@atlassian/global-side-navigation`** (`patch`): scope `fg`
86
+ mocks in unit tests so `platform-dst-top-layer` returns `false`. JSDOM does not implement the
87
+ native Popover API (`showPopover`/`hidePopover`/`toggle` events), so leaving the gate ON in unit
88
+ tests caused popover content to remain in the DOM after close and broke close-behaviour
89
+ assertions. Browser coverage for the FF-on path is provided by the Playwright suites listed
90
+ above.
91
+ - **`@atlaskit/dropdown-menu`** (no public API change): test/example-only fixes for the FF-on
92
+ Playwright suite. Added `role="menuitem"` to the nested-trigger `ButtonItem` in
93
+ `examples/93-testing-nested-keyboard-navigation-top-layer.tsx` to satisfy axe's
94
+ `aria-required-children` rule on the parent menu. Added a `test.beforeEach(skipAxeCheck)` to
95
+ `dropdown-menu.spec.tsx` (FF-on suite) for example-level `color-contrast` violations on the
96
+ pre-existing `color.text.selected`/`color.background.selected` token pair (3.91:1). Replaced a
97
+ deadlocking `await expect(moveItem).not.toBeFocused()` pre-open assertion (Playwright's
98
+ auto-wait blocks 5s on the absent element) with `await expect(moveItem).not.toBeVisible()`.
99
+ Suite result: 22/22 passing.
100
+
101
+ ### Patch Changes
102
+
103
+ - Updated dependencies
104
+
3
105
  ## 12.7.2
4
106
 
5
107
  ### Patch Changes
@@ -20,26 +20,27 @@ var AvatarGroupItem = /*#__PURE__*/(0, _react.forwardRef)(function (props, ref)
20
20
  var avatar = props.avatar,
21
21
  index = props.index,
22
22
  onAvatarClick = props.onAvatarClick,
23
- testId = props.testId;
23
+ testId = props.testId,
24
+ role = props.role;
24
25
  var analyticsContext = avatar.analyticsContext,
25
26
  appearance = avatar.appearance,
26
27
  as = avatar.as,
27
- borderColor = avatar.borderColor,
28
+ _borderColor = avatar.borderColor,
28
29
  children = avatar.children,
29
30
  href = avatar.href,
30
31
  isDisabled = avatar.isDisabled,
31
- key = avatar.key,
32
+ _key = avatar.key,
32
33
  label = avatar.label,
33
34
  name = avatar.name,
34
35
  onClick = avatar.onClick,
35
36
  presence = avatar.presence,
36
- size = avatar.size,
37
+ _size = avatar.size,
37
38
  src = avatar.src,
38
39
  stackIndex = avatar.stackIndex,
39
40
  status = avatar.status,
40
41
  tabIndex = avatar.tabIndex,
41
42
  target = avatar.target,
42
- groupItemTestId = avatar.testId,
43
+ _groupItemTestId = avatar.testId,
43
44
  rest = (0, _objectWithoutProperties2.default)(avatar, _excluded);
44
45
  var itemRef = (0, _useRegisterItemWithFocusManager.default)();
45
46
  var CustomComponent = function CustomComponent(_ref) {
@@ -68,7 +69,8 @@ var AvatarGroupItem = /*#__PURE__*/(0, _react.forwardRef)(function (props, ref)
68
69
  onMouseDown: onMouseDown,
69
70
  ref: ref,
70
71
  tabIndex: tabIndex,
71
- "data-testid": testId
72
+ "data-testid": testId,
73
+ role: role
72
74
  // eslint-disable-next-line @repo/internal/react/no-unsafe-spread-props
73
75
  }, props), children);
74
76
  };
@@ -100,6 +102,7 @@ var AvatarGroupItem = /*#__PURE__*/(0, _react.forwardRef)(function (props, ref)
100
102
  rel: target === '_blank' ? 'noopener noreferrer' : undefined,
101
103
  iconBefore: AvatarIcon,
102
104
  testId: testId,
105
+ role: role,
103
106
  onClick: function onClick(event) {
104
107
  return callback && callback(event, undefined, index);
105
108
  }
@@ -112,7 +115,8 @@ var AvatarGroupItem = /*#__PURE__*/(0, _react.forwardRef)(function (props, ref)
112
115
  return callback && callback(event, undefined, index);
113
116
  },
114
117
  iconBefore: AvatarIcon,
115
- testId: testId
118
+ testId: testId,
119
+ role: role
116
120
  }, name);
117
121
  }
118
122
  return /*#__PURE__*/_react.default.createElement(_menu.CustomItem, {
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ var _typeof = require("@babel/runtime/helpers/typeof");
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ exports.MoreDropdownTopLayer = MoreDropdownTopLayer;
9
+ var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
10
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
11
+ var _react = _interopRequireWildcard(require("react"));
12
+ var _bindEventListener = require("bind-event-listener");
13
+ var _keycodes = require("@atlaskit/ds-lib/keycodes");
14
+ var _useFocusEvent = _interopRequireDefault(require("@atlaskit/ds-lib/use-focus-event"));
15
+ var _menu = require("@atlaskit/menu");
16
+ var _animations = require("@atlaskit/top-layer/animations");
17
+ var _placementMap = require("@atlaskit/top-layer/placement-map");
18
+ var _popup = require("@atlaskit/top-layer/popup");
19
+ var _useArrowNavigation = require("@atlaskit/top-layer/use-arrow-navigation");
20
+ var _avatarGroupItem = _interopRequireDefault(require("./avatar-group-item"));
21
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
22
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
23
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
24
+ var animation = (0, _animations.slideAndFade)();
25
+ var topLayerPlacement = (0, _placementMap.fromLegacyPlacement)({
26
+ legacy: 'bottom-end'
27
+ });
28
+ function getOverrides(overrides) {
29
+ return {
30
+ AvatarGroupItem: _objectSpread({
31
+ render: function render(Component, props, index) {
32
+ return /*#__PURE__*/_react.default.createElement(Component, (0, _extends2.default)({
33
+ key: index
34
+ }, props));
35
+ }
36
+ }, overrides === null || overrides === void 0 ? void 0 : overrides.AvatarGroupItem),
37
+ Avatar: _objectSpread({
38
+ render: function render(Component, props, index) {
39
+ return /*#__PURE__*/_react.default.createElement(Component, (0, _extends2.default)({
40
+ key: index
41
+ }, props));
42
+ }
43
+ }, overrides === null || overrides === void 0 ? void 0 : overrides.Avatar),
44
+ MoreIndicator: _objectSpread({
45
+ render: function render(Component, props) {
46
+ return /*#__PURE__*/_react.default.createElement(Component, props);
47
+ }
48
+ }, overrides === null || overrides === void 0 ? void 0 : overrides.MoreIndicator)
49
+ };
50
+ }
51
+ /**
52
+ * Top-layer implementation of the avatar group overflow dropdown.
53
+ *
54
+ * Replaces the legacy `@atlaskit/popup` rendering pipeline
55
+ * (Popper.js + Portal + focus-trap + @atlaskit/layering)
56
+ * with the native Popover API + CSS Anchor Positioning via `@atlaskit/top-layer`.
57
+ *
58
+ * Uses `role="menu"` with arrow key navigation for correct menu semantics.
59
+ *
60
+ * Gated behind the `platform-dst-top-layer` feature flag.
61
+ *
62
+ * Legacy props that are no-ops in the top-layer path (not accepted here):
63
+ * - zIndex: stacking managed by browser top layer
64
+ * - shouldRenderToParent: always renders in top layer
65
+ * - boundary / rootBoundary: viewport is the natural boundary
66
+ * - shouldFlip: CSS Anchor Positioning handles flipping
67
+ */
68
+ function MoreDropdownTopLayer(_ref) {
69
+ var isOpen = _ref.isOpen,
70
+ onClose = _ref.onClose,
71
+ _isTriggeredUsingKeyboard = _ref.isTriggeredUsingKeyboard,
72
+ data = _ref.data,
73
+ max = _ref.max,
74
+ overrides = _ref.overrides,
75
+ onAvatarClick = _ref.onAvatarClick,
76
+ testId = _ref.testId,
77
+ labelId = _ref.labelId,
78
+ renderMoreButton = _ref.renderMoreButton,
79
+ handleTriggerClicked = _ref.handleTriggerClicked,
80
+ _bindFocus = _ref.bindFocus;
81
+ var handleOnClose = (0, _react.useCallback)(function (_ref2) {
82
+ var _reason = _ref2.reason;
83
+ onClose();
84
+ }, [onClose]);
85
+ var menuRef = (0, _react.useRef)(null);
86
+ var overflowMenuTestId = testId ? "".concat(testId, "--overflow-menu") : undefined;
87
+
88
+ // Arrow key navigation inside the open menu
89
+ (0, _useArrowNavigation.useArrowNavigation)({
90
+ containerRef: menuRef,
91
+ onClose: onClose,
92
+ isEnabled: isOpen
93
+ });
94
+
95
+ // ArrowDown-to-open: when the trigger is focused and the menu is closed,
96
+ // pressing ArrowDown opens the menu (WAI-ARIA menu button pattern).
97
+ // We track focus on the trigger wrapper to avoid threading onFocus/onBlur
98
+ // through the renderMoreButton/MoreIndicator prop plumbing.
99
+ var triggerWrapperRef = (0, _react.useRef)(null);
100
+ var _useFocus = (0, _useFocusEvent.default)(),
101
+ isFocused = _useFocus.isFocused,
102
+ triggerFocusBind = _useFocus.bindFocus;
103
+ (0, _react.useEffect)(function () {
104
+ if (!isFocused || isOpen) {
105
+ return;
106
+ }
107
+ return (0, _bindEventListener.bind)(window, {
108
+ type: 'keydown',
109
+ listener: function openOnArrowDown(e) {
110
+ if (e.key === _keycodes.KEY_DOWN) {
111
+ // Prevent page scroll when opening the menu via ArrowDown.
112
+ e.preventDefault();
113
+ handleTriggerClicked(e);
114
+ }
115
+ }
116
+ });
117
+ }, [isFocused, isOpen, handleTriggerClicked]);
118
+ return /*#__PURE__*/_react.default.createElement(_popup.Popup, {
119
+ placement: topLayerPlacement,
120
+ onClose: handleOnClose,
121
+ testId: overflowMenuTestId
122
+ }, /*#__PURE__*/_react.default.createElement(_popup.Popup.TriggerFunction, null, function (_ref3) {
123
+ var ref = _ref3.ref,
124
+ ariaAttributes = _ref3.ariaAttributes;
125
+ return (
126
+ /*#__PURE__*/
127
+ // Workaround: wrapping span to track trigger focus for ArrowDown-to-open.
128
+ //
129
+ // The `useFocus` hook needs onFocus/onBlur on the trigger element,
130
+ // but we cannot thread those through the renderMoreButton → MoreIndicator
131
+ // prop plumbing without changing those APIs (onFocus/onBlur would need
132
+ // to go into MoreIndicator's `buttonProps`, but renderMoreButton does not
133
+ // expose that).
134
+ //
135
+ // Using `display: contents` so the span does not affect layout — the
136
+ // button renders as if the span were not there. Focus events from the
137
+ // button bubble up to this span, allowing useFocus to track state.
138
+ //
139
+ // If MoreIndicator's API is refactored to accept focus handlers via
140
+ // buttonProps, this wrapper can be removed.
141
+ _react.default.createElement("span", {
142
+ ref: triggerWrapperRef,
143
+ onFocus: triggerFocusBind.onFocus,
144
+ onBlur: triggerFocusBind.onBlur
145
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- display: contents is a layout-neutral wrapper; it cannot affect visual output.
146
+ ,
147
+ style: {
148
+ display: 'contents'
149
+ }
150
+ }, renderMoreButton({
151
+ ref: ref,
152
+ 'aria-controls': isOpen ? ariaAttributes['aria-controls'] : undefined,
153
+ 'aria-expanded': isOpen,
154
+ 'aria-haspopup': true,
155
+ onClick: handleTriggerClicked
156
+ }))
157
+ );
158
+ }), /*#__PURE__*/_react.default.createElement(_popup.Popup.Content, {
159
+ role: "menu",
160
+ label: "avatar group",
161
+ isOpen: isOpen,
162
+ animate: animation,
163
+ testId: overflowMenuTestId ? "".concat(overflowMenuTestId, "--content") : undefined
164
+ }, /*#__PURE__*/_react.default.createElement(_popup.Popup.Surface, null, /*#__PURE__*/_react.default.createElement("div", {
165
+ ref: menuRef
166
+ }, /*#__PURE__*/_react.default.createElement(_menu.MenuGroup, {
167
+ minWidth: 250,
168
+ maxHeight: 300
169
+ }, /*#__PURE__*/_react.default.createElement(_menu.Section, {
170
+ titleId: labelId,
171
+ testId: testId ? "".concat(testId, "--section") : undefined
172
+ }, data.slice(max).map(function (avatar, index) {
173
+ return getOverrides(overrides).AvatarGroupItem.render(_avatarGroupItem.default, {
174
+ avatar: avatar,
175
+ onAvatarClick: onAvatarClick,
176
+ testId: testId ? "".concat(testId, "--avatar-group-item-").concat(index + max) : undefined,
177
+ index: index + max,
178
+ role: 'menuitem'
179
+ }, index + max);
180
+ })))))));
181
+ }
@@ -23,6 +23,7 @@ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
23
23
  var _popup = _interopRequireDefault(require("@atlaskit/popup"));
24
24
  var _tooltip = _interopRequireDefault(require("@atlaskit/tooltip"));
25
25
  var _avatarGroupItem = _interopRequireDefault(require("./avatar-group-item"));
26
+ var _avatarGroupTopLayer = require("./avatar-group-top-layer");
26
27
  var _grid = _interopRequireDefault(require("./grid"));
27
28
  var _focusManager = _interopRequireDefault(require("./internal/components/focus-manager"));
28
29
  var _popupAvatarGroup = _interopRequireDefault(require("./internal/components/popup-avatar-group"));
@@ -129,13 +130,21 @@ var AvatarGroup = function AvatarGroup(_ref) {
129
130
  bindFocus = _useFocus.bindFocus;
130
131
 
131
132
  // When a trigger is focused, we want to open the popup
132
- // the user presses the DownArrow
133
+ // the user presses the DownArrow.
134
+ // Skipped when top-layer is enabled — DropdownMenu/top-layer handles
135
+ // ArrowDown-to-open and arrow key navigation internally.
133
136
  (0, _react.useEffect)(function () {
134
137
  // Set initial value if popup is closed
135
138
  if (!isOpen) {
136
139
  setTriggeredUsingKeyboard(false);
137
140
  }
138
141
 
142
+ // Top-layer path: ArrowDown-to-open is handled by the menu's
143
+ // own focus management, so skip the parent's ArrowDown handler.
144
+ if ((0, _platformFeatureFlags.fg)('platform-dst-top-layer')) {
145
+ return _noop.default;
146
+ }
147
+
139
148
  // Only need to listen for keydown when focused
140
149
  if (!isFocused) {
141
150
  return _noop.default;
@@ -203,6 +212,24 @@ var AvatarGroup = function AvatarGroup(_ref) {
203
212
  onClick: onMoreClick
204
213
  });
205
214
  }
215
+ if ((0, _platformFeatureFlags.fg)('platform-dst-top-layer')) {
216
+ return /*#__PURE__*/_react.default.createElement(_avatarGroupTopLayer.MoreDropdownTopLayer, {
217
+ isOpen: isOpen,
218
+ onClose: onClose,
219
+ isTriggeredUsingKeyboard: isTriggeredUsingKeyboard,
220
+ data: data,
221
+ max: max
222
+ // eslint-disable-next-line @repo/internal/react/no-unsafe-overrides
223
+ ,
224
+ overrides: overrides,
225
+ onAvatarClick: onAvatarClick,
226
+ testId: testId,
227
+ labelId: labelId,
228
+ renderMoreButton: renderMoreButton,
229
+ handleTriggerClicked: handleTriggerClicked,
230
+ bindFocus: bindFocus
231
+ });
232
+ }
206
233
 
207
234
  // split boundariesElement into `boundary` and `rootBoundary` props for Popup
208
235
  var boundary = boundariesElement === 'scrollParent' ? 'clippingParents' : undefined;
@@ -9,28 +9,29 @@ const AvatarGroupItem = /*#__PURE__*/forwardRef((props, ref) => {
9
9
  avatar,
10
10
  index,
11
11
  onAvatarClick,
12
- testId
12
+ testId,
13
+ role
13
14
  } = props;
14
15
  const {
15
16
  analyticsContext,
16
17
  appearance,
17
18
  as,
18
- borderColor,
19
+ borderColor: _borderColor,
19
20
  children,
20
21
  href,
21
22
  isDisabled,
22
- key,
23
+ key: _key,
23
24
  label,
24
25
  name,
25
26
  onClick,
26
27
  presence,
27
- size,
28
+ size: _size,
28
29
  src,
29
30
  stackIndex,
30
31
  status,
31
32
  tabIndex,
32
33
  target,
33
- testId: groupItemTestId,
34
+ testId: _groupItemTestId,
34
35
  ...rest
35
36
  } = avatar;
36
37
  const itemRef = useRegisterItemWithFocusManager();
@@ -61,7 +62,8 @@ const AvatarGroupItem = /*#__PURE__*/forwardRef((props, ref) => {
61
62
  onMouseDown: onMouseDown,
62
63
  ref: ref,
63
64
  tabIndex: tabIndex,
64
- "data-testid": testId
65
+ "data-testid": testId,
66
+ role: role
65
67
  // eslint-disable-next-line @repo/internal/react/no-unsafe-spread-props
66
68
  }, props), children);
67
69
  };
@@ -93,6 +95,7 @@ const AvatarGroupItem = /*#__PURE__*/forwardRef((props, ref) => {
93
95
  rel: target === '_blank' ? 'noopener noreferrer' : undefined,
94
96
  iconBefore: AvatarIcon,
95
97
  testId: testId,
98
+ role: role,
96
99
  onClick: event => callback && callback(event, undefined, index)
97
100
  }, name);
98
101
  }
@@ -101,7 +104,8 @@ const AvatarGroupItem = /*#__PURE__*/forwardRef((props, ref) => {
101
104
  ref: mergeRefs([ref, itemRef]),
102
105
  onClick: event => callback && callback(event, undefined, index),
103
106
  iconBefore: AvatarIcon,
104
- testId: testId
107
+ testId: testId,
108
+ role: role
105
109
  }, name);
106
110
  }
107
111
  return /*#__PURE__*/React.createElement(CustomItem, {
@@ -0,0 +1,165 @@
1
+ import _extends from "@babel/runtime/helpers/extends";
2
+ import React, { useCallback, useEffect, useRef } from 'react';
3
+ import { bind } from 'bind-event-listener';
4
+ import { KEY_DOWN } from '@atlaskit/ds-lib/keycodes';
5
+ import useFocus from '@atlaskit/ds-lib/use-focus-event';
6
+ import { MenuGroup, Section } from '@atlaskit/menu';
7
+ import { slideAndFade } from '@atlaskit/top-layer/animations';
8
+ import { fromLegacyPlacement } from '@atlaskit/top-layer/placement-map';
9
+ import { Popup } from '@atlaskit/top-layer/popup';
10
+ import { useArrowNavigation } from '@atlaskit/top-layer/use-arrow-navigation';
11
+ import AvatarGroupItem from './avatar-group-item';
12
+ const animation = slideAndFade();
13
+ const topLayerPlacement = fromLegacyPlacement({
14
+ legacy: 'bottom-end'
15
+ });
16
+ function getOverrides(overrides) {
17
+ return {
18
+ AvatarGroupItem: {
19
+ render: (Component, props, index) => /*#__PURE__*/React.createElement(Component, _extends({
20
+ key: index
21
+ }, props)),
22
+ ...(overrides === null || overrides === void 0 ? void 0 : overrides.AvatarGroupItem)
23
+ },
24
+ Avatar: {
25
+ render: (Component, props, index) => /*#__PURE__*/React.createElement(Component, _extends({
26
+ key: index
27
+ }, props)),
28
+ ...(overrides === null || overrides === void 0 ? void 0 : overrides.Avatar)
29
+ },
30
+ MoreIndicator: {
31
+ render: (Component, props) => /*#__PURE__*/React.createElement(Component, props),
32
+ ...(overrides === null || overrides === void 0 ? void 0 : overrides.MoreIndicator)
33
+ }
34
+ };
35
+ }
36
+ /**
37
+ * Top-layer implementation of the avatar group overflow dropdown.
38
+ *
39
+ * Replaces the legacy `@atlaskit/popup` rendering pipeline
40
+ * (Popper.js + Portal + focus-trap + @atlaskit/layering)
41
+ * with the native Popover API + CSS Anchor Positioning via `@atlaskit/top-layer`.
42
+ *
43
+ * Uses `role="menu"` with arrow key navigation for correct menu semantics.
44
+ *
45
+ * Gated behind the `platform-dst-top-layer` feature flag.
46
+ *
47
+ * Legacy props that are no-ops in the top-layer path (not accepted here):
48
+ * - zIndex: stacking managed by browser top layer
49
+ * - shouldRenderToParent: always renders in top layer
50
+ * - boundary / rootBoundary: viewport is the natural boundary
51
+ * - shouldFlip: CSS Anchor Positioning handles flipping
52
+ */
53
+ export function MoreDropdownTopLayer({
54
+ isOpen,
55
+ onClose,
56
+ isTriggeredUsingKeyboard: _isTriggeredUsingKeyboard,
57
+ data,
58
+ max,
59
+ overrides,
60
+ onAvatarClick,
61
+ testId,
62
+ labelId,
63
+ renderMoreButton,
64
+ handleTriggerClicked,
65
+ bindFocus: _bindFocus
66
+ }) {
67
+ const handleOnClose = useCallback(({
68
+ reason: _reason
69
+ }) => {
70
+ onClose();
71
+ }, [onClose]);
72
+ const menuRef = useRef(null);
73
+ const overflowMenuTestId = testId ? `${testId}--overflow-menu` : undefined;
74
+
75
+ // Arrow key navigation inside the open menu
76
+ useArrowNavigation({
77
+ containerRef: menuRef,
78
+ onClose,
79
+ isEnabled: isOpen
80
+ });
81
+
82
+ // ArrowDown-to-open: when the trigger is focused and the menu is closed,
83
+ // pressing ArrowDown opens the menu (WAI-ARIA menu button pattern).
84
+ // We track focus on the trigger wrapper to avoid threading onFocus/onBlur
85
+ // through the renderMoreButton/MoreIndicator prop plumbing.
86
+ const triggerWrapperRef = useRef(null);
87
+ const {
88
+ isFocused,
89
+ bindFocus: triggerFocusBind
90
+ } = useFocus();
91
+ useEffect(() => {
92
+ if (!isFocused || isOpen) {
93
+ return;
94
+ }
95
+ return bind(window, {
96
+ type: 'keydown',
97
+ listener: function openOnArrowDown(e) {
98
+ if (e.key === KEY_DOWN) {
99
+ // Prevent page scroll when opening the menu via ArrowDown.
100
+ e.preventDefault();
101
+ handleTriggerClicked(e);
102
+ }
103
+ }
104
+ });
105
+ }, [isFocused, isOpen, handleTriggerClicked]);
106
+ return /*#__PURE__*/React.createElement(Popup, {
107
+ placement: topLayerPlacement,
108
+ onClose: handleOnClose,
109
+ testId: overflowMenuTestId
110
+ }, /*#__PURE__*/React.createElement(Popup.TriggerFunction, null, ({
111
+ ref,
112
+ ariaAttributes
113
+ }) =>
114
+ /*#__PURE__*/
115
+ // Workaround: wrapping span to track trigger focus for ArrowDown-to-open.
116
+ //
117
+ // The `useFocus` hook needs onFocus/onBlur on the trigger element,
118
+ // but we cannot thread those through the renderMoreButton → MoreIndicator
119
+ // prop plumbing without changing those APIs (onFocus/onBlur would need
120
+ // to go into MoreIndicator's `buttonProps`, but renderMoreButton does not
121
+ // expose that).
122
+ //
123
+ // Using `display: contents` so the span does not affect layout — the
124
+ // button renders as if the span were not there. Focus events from the
125
+ // button bubble up to this span, allowing useFocus to track state.
126
+ //
127
+ // If MoreIndicator's API is refactored to accept focus handlers via
128
+ // buttonProps, this wrapper can be removed.
129
+ React.createElement("span", {
130
+ ref: triggerWrapperRef,
131
+ onFocus: triggerFocusBind.onFocus,
132
+ onBlur: triggerFocusBind.onBlur
133
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- display: contents is a layout-neutral wrapper; it cannot affect visual output.
134
+ ,
135
+ style: {
136
+ display: 'contents'
137
+ }
138
+ }, renderMoreButton({
139
+ ref,
140
+ 'aria-controls': isOpen ? ariaAttributes['aria-controls'] : undefined,
141
+ 'aria-expanded': isOpen,
142
+ 'aria-haspopup': true,
143
+ onClick: handleTriggerClicked
144
+ }))), /*#__PURE__*/React.createElement(Popup.Content, {
145
+ role: "menu",
146
+ label: "avatar group",
147
+ isOpen: isOpen,
148
+ animate: animation,
149
+ testId: overflowMenuTestId ? `${overflowMenuTestId}--content` : undefined
150
+ }, /*#__PURE__*/React.createElement(Popup.Surface, null, /*#__PURE__*/React.createElement("div", {
151
+ ref: menuRef
152
+ }, /*#__PURE__*/React.createElement(MenuGroup, {
153
+ minWidth: 250,
154
+ maxHeight: 300
155
+ }, /*#__PURE__*/React.createElement(Section, {
156
+ titleId: labelId,
157
+ testId: testId ? `${testId}--section` : undefined
158
+ }, data.slice(max).map((avatar, index) => getOverrides(overrides).AvatarGroupItem.render(AvatarGroupItem, {
159
+ avatar,
160
+ onAvatarClick,
161
+ testId: testId ? `${testId}--avatar-group-item-${index + max}` : undefined,
162
+ index: index + max,
163
+ role: 'menuitem'
164
+ }, index + max))))))));
165
+ }
@@ -12,6 +12,7 @@ import { fg } from '@atlaskit/platform-feature-flags';
12
12
  import Popup from '@atlaskit/popup';
13
13
  import Tooltip from '@atlaskit/tooltip';
14
14
  import AvatarGroupItem from './avatar-group-item';
15
+ import { MoreDropdownTopLayer } from './avatar-group-top-layer';
15
16
  import Grid from './grid';
16
17
  import FocusManager from './internal/components/focus-manager';
17
18
  import PopupAvatarGroup from './internal/components/popup-avatar-group';
@@ -96,13 +97,21 @@ const AvatarGroup = ({
96
97
  } = useFocus();
97
98
 
98
99
  // When a trigger is focused, we want to open the popup
99
- // the user presses the DownArrow
100
+ // the user presses the DownArrow.
101
+ // Skipped when top-layer is enabled — DropdownMenu/top-layer handles
102
+ // ArrowDown-to-open and arrow key navigation internally.
100
103
  useEffect(() => {
101
104
  // Set initial value if popup is closed
102
105
  if (!isOpen) {
103
106
  setTriggeredUsingKeyboard(false);
104
107
  }
105
108
 
109
+ // Top-layer path: ArrowDown-to-open is handled by the menu's
110
+ // own focus management, so skip the parent's ArrowDown handler.
111
+ if (fg('platform-dst-top-layer')) {
112
+ return noop;
113
+ }
114
+
106
115
  // Only need to listen for keydown when focused
107
116
  if (!isFocused) {
108
117
  return noop;
@@ -171,6 +180,24 @@ const AvatarGroup = ({
171
180
  onClick: onMoreClick
172
181
  });
173
182
  }
183
+ if (fg('platform-dst-top-layer')) {
184
+ return /*#__PURE__*/React.createElement(MoreDropdownTopLayer, {
185
+ isOpen: isOpen,
186
+ onClose: onClose,
187
+ isTriggeredUsingKeyboard: isTriggeredUsingKeyboard,
188
+ data: data,
189
+ max: max
190
+ // eslint-disable-next-line @repo/internal/react/no-unsafe-overrides
191
+ ,
192
+ overrides: overrides,
193
+ onAvatarClick: onAvatarClick,
194
+ testId: testId,
195
+ labelId: labelId,
196
+ renderMoreButton: renderMoreButton,
197
+ handleTriggerClicked: handleTriggerClicked,
198
+ bindFocus: bindFocus
199
+ });
200
+ }
174
201
 
175
202
  // split boundariesElement into `boundary` and `rootBoundary` props for Popup
176
203
  const boundary = boundariesElement === 'scrollParent' ? 'clippingParents' : undefined;
@@ -11,26 +11,27 @@ var AvatarGroupItem = /*#__PURE__*/forwardRef(function (props, ref) {
11
11
  var avatar = props.avatar,
12
12
  index = props.index,
13
13
  onAvatarClick = props.onAvatarClick,
14
- testId = props.testId;
14
+ testId = props.testId,
15
+ role = props.role;
15
16
  var analyticsContext = avatar.analyticsContext,
16
17
  appearance = avatar.appearance,
17
18
  as = avatar.as,
18
- borderColor = avatar.borderColor,
19
+ _borderColor = avatar.borderColor,
19
20
  children = avatar.children,
20
21
  href = avatar.href,
21
22
  isDisabled = avatar.isDisabled,
22
- key = avatar.key,
23
+ _key = avatar.key,
23
24
  label = avatar.label,
24
25
  name = avatar.name,
25
26
  onClick = avatar.onClick,
26
27
  presence = avatar.presence,
27
- size = avatar.size,
28
+ _size = avatar.size,
28
29
  src = avatar.src,
29
30
  stackIndex = avatar.stackIndex,
30
31
  status = avatar.status,
31
32
  tabIndex = avatar.tabIndex,
32
33
  target = avatar.target,
33
- groupItemTestId = avatar.testId,
34
+ _groupItemTestId = avatar.testId,
34
35
  rest = _objectWithoutProperties(avatar, _excluded);
35
36
  var itemRef = useRegisterItemWithFocusManager();
36
37
  var CustomComponent = function CustomComponent(_ref) {
@@ -59,7 +60,8 @@ var AvatarGroupItem = /*#__PURE__*/forwardRef(function (props, ref) {
59
60
  onMouseDown: onMouseDown,
60
61
  ref: ref,
61
62
  tabIndex: tabIndex,
62
- "data-testid": testId
63
+ "data-testid": testId,
64
+ role: role
63
65
  // eslint-disable-next-line @repo/internal/react/no-unsafe-spread-props
64
66
  }, props), children);
65
67
  };
@@ -91,6 +93,7 @@ var AvatarGroupItem = /*#__PURE__*/forwardRef(function (props, ref) {
91
93
  rel: target === '_blank' ? 'noopener noreferrer' : undefined,
92
94
  iconBefore: AvatarIcon,
93
95
  testId: testId,
96
+ role: role,
94
97
  onClick: function onClick(event) {
95
98
  return callback && callback(event, undefined, index);
96
99
  }
@@ -103,7 +106,8 @@ var AvatarGroupItem = /*#__PURE__*/forwardRef(function (props, ref) {
103
106
  return callback && callback(event, undefined, index);
104
107
  },
105
108
  iconBefore: AvatarIcon,
106
- testId: testId
109
+ testId: testId,
110
+ role: role
107
111
  }, name);
108
112
  }
109
113
  return /*#__PURE__*/React.createElement(CustomItem, {
@@ -0,0 +1,172 @@
1
+ import _extends from "@babel/runtime/helpers/extends";
2
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
3
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
4
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
5
+ import React, { useCallback, useEffect, useRef } from 'react';
6
+ import { bind } from 'bind-event-listener';
7
+ import { KEY_DOWN } from '@atlaskit/ds-lib/keycodes';
8
+ import useFocus from '@atlaskit/ds-lib/use-focus-event';
9
+ import { MenuGroup, Section } from '@atlaskit/menu';
10
+ import { slideAndFade } from '@atlaskit/top-layer/animations';
11
+ import { fromLegacyPlacement } from '@atlaskit/top-layer/placement-map';
12
+ import { Popup } from '@atlaskit/top-layer/popup';
13
+ import { useArrowNavigation } from '@atlaskit/top-layer/use-arrow-navigation';
14
+ import AvatarGroupItem from './avatar-group-item';
15
+ var animation = slideAndFade();
16
+ var topLayerPlacement = fromLegacyPlacement({
17
+ legacy: 'bottom-end'
18
+ });
19
+ function getOverrides(overrides) {
20
+ return {
21
+ AvatarGroupItem: _objectSpread({
22
+ render: function render(Component, props, index) {
23
+ return /*#__PURE__*/React.createElement(Component, _extends({
24
+ key: index
25
+ }, props));
26
+ }
27
+ }, overrides === null || overrides === void 0 ? void 0 : overrides.AvatarGroupItem),
28
+ Avatar: _objectSpread({
29
+ render: function render(Component, props, index) {
30
+ return /*#__PURE__*/React.createElement(Component, _extends({
31
+ key: index
32
+ }, props));
33
+ }
34
+ }, overrides === null || overrides === void 0 ? void 0 : overrides.Avatar),
35
+ MoreIndicator: _objectSpread({
36
+ render: function render(Component, props) {
37
+ return /*#__PURE__*/React.createElement(Component, props);
38
+ }
39
+ }, overrides === null || overrides === void 0 ? void 0 : overrides.MoreIndicator)
40
+ };
41
+ }
42
+ /**
43
+ * Top-layer implementation of the avatar group overflow dropdown.
44
+ *
45
+ * Replaces the legacy `@atlaskit/popup` rendering pipeline
46
+ * (Popper.js + Portal + focus-trap + @atlaskit/layering)
47
+ * with the native Popover API + CSS Anchor Positioning via `@atlaskit/top-layer`.
48
+ *
49
+ * Uses `role="menu"` with arrow key navigation for correct menu semantics.
50
+ *
51
+ * Gated behind the `platform-dst-top-layer` feature flag.
52
+ *
53
+ * Legacy props that are no-ops in the top-layer path (not accepted here):
54
+ * - zIndex: stacking managed by browser top layer
55
+ * - shouldRenderToParent: always renders in top layer
56
+ * - boundary / rootBoundary: viewport is the natural boundary
57
+ * - shouldFlip: CSS Anchor Positioning handles flipping
58
+ */
59
+ export function MoreDropdownTopLayer(_ref) {
60
+ var isOpen = _ref.isOpen,
61
+ onClose = _ref.onClose,
62
+ _isTriggeredUsingKeyboard = _ref.isTriggeredUsingKeyboard,
63
+ data = _ref.data,
64
+ max = _ref.max,
65
+ overrides = _ref.overrides,
66
+ onAvatarClick = _ref.onAvatarClick,
67
+ testId = _ref.testId,
68
+ labelId = _ref.labelId,
69
+ renderMoreButton = _ref.renderMoreButton,
70
+ handleTriggerClicked = _ref.handleTriggerClicked,
71
+ _bindFocus = _ref.bindFocus;
72
+ var handleOnClose = useCallback(function (_ref2) {
73
+ var _reason = _ref2.reason;
74
+ onClose();
75
+ }, [onClose]);
76
+ var menuRef = useRef(null);
77
+ var overflowMenuTestId = testId ? "".concat(testId, "--overflow-menu") : undefined;
78
+
79
+ // Arrow key navigation inside the open menu
80
+ useArrowNavigation({
81
+ containerRef: menuRef,
82
+ onClose: onClose,
83
+ isEnabled: isOpen
84
+ });
85
+
86
+ // ArrowDown-to-open: when the trigger is focused and the menu is closed,
87
+ // pressing ArrowDown opens the menu (WAI-ARIA menu button pattern).
88
+ // We track focus on the trigger wrapper to avoid threading onFocus/onBlur
89
+ // through the renderMoreButton/MoreIndicator prop plumbing.
90
+ var triggerWrapperRef = useRef(null);
91
+ var _useFocus = useFocus(),
92
+ isFocused = _useFocus.isFocused,
93
+ triggerFocusBind = _useFocus.bindFocus;
94
+ useEffect(function () {
95
+ if (!isFocused || isOpen) {
96
+ return;
97
+ }
98
+ return bind(window, {
99
+ type: 'keydown',
100
+ listener: function openOnArrowDown(e) {
101
+ if (e.key === KEY_DOWN) {
102
+ // Prevent page scroll when opening the menu via ArrowDown.
103
+ e.preventDefault();
104
+ handleTriggerClicked(e);
105
+ }
106
+ }
107
+ });
108
+ }, [isFocused, isOpen, handleTriggerClicked]);
109
+ return /*#__PURE__*/React.createElement(Popup, {
110
+ placement: topLayerPlacement,
111
+ onClose: handleOnClose,
112
+ testId: overflowMenuTestId
113
+ }, /*#__PURE__*/React.createElement(Popup.TriggerFunction, null, function (_ref3) {
114
+ var ref = _ref3.ref,
115
+ ariaAttributes = _ref3.ariaAttributes;
116
+ return (
117
+ /*#__PURE__*/
118
+ // Workaround: wrapping span to track trigger focus for ArrowDown-to-open.
119
+ //
120
+ // The `useFocus` hook needs onFocus/onBlur on the trigger element,
121
+ // but we cannot thread those through the renderMoreButton → MoreIndicator
122
+ // prop plumbing without changing those APIs (onFocus/onBlur would need
123
+ // to go into MoreIndicator's `buttonProps`, but renderMoreButton does not
124
+ // expose that).
125
+ //
126
+ // Using `display: contents` so the span does not affect layout — the
127
+ // button renders as if the span were not there. Focus events from the
128
+ // button bubble up to this span, allowing useFocus to track state.
129
+ //
130
+ // If MoreIndicator's API is refactored to accept focus handlers via
131
+ // buttonProps, this wrapper can be removed.
132
+ React.createElement("span", {
133
+ ref: triggerWrapperRef,
134
+ onFocus: triggerFocusBind.onFocus,
135
+ onBlur: triggerFocusBind.onBlur
136
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- display: contents is a layout-neutral wrapper; it cannot affect visual output.
137
+ ,
138
+ style: {
139
+ display: 'contents'
140
+ }
141
+ }, renderMoreButton({
142
+ ref: ref,
143
+ 'aria-controls': isOpen ? ariaAttributes['aria-controls'] : undefined,
144
+ 'aria-expanded': isOpen,
145
+ 'aria-haspopup': true,
146
+ onClick: handleTriggerClicked
147
+ }))
148
+ );
149
+ }), /*#__PURE__*/React.createElement(Popup.Content, {
150
+ role: "menu",
151
+ label: "avatar group",
152
+ isOpen: isOpen,
153
+ animate: animation,
154
+ testId: overflowMenuTestId ? "".concat(overflowMenuTestId, "--content") : undefined
155
+ }, /*#__PURE__*/React.createElement(Popup.Surface, null, /*#__PURE__*/React.createElement("div", {
156
+ ref: menuRef
157
+ }, /*#__PURE__*/React.createElement(MenuGroup, {
158
+ minWidth: 250,
159
+ maxHeight: 300
160
+ }, /*#__PURE__*/React.createElement(Section, {
161
+ titleId: labelId,
162
+ testId: testId ? "".concat(testId, "--section") : undefined
163
+ }, data.slice(max).map(function (avatar, index) {
164
+ return getOverrides(overrides).AvatarGroupItem.render(AvatarGroupItem, {
165
+ avatar: avatar,
166
+ onAvatarClick: onAvatarClick,
167
+ testId: testId ? "".concat(testId, "--avatar-group-item-").concat(index + max) : undefined,
168
+ index: index + max,
169
+ role: 'menuitem'
170
+ }, index + max);
171
+ })))))));
172
+ }
@@ -18,6 +18,7 @@ import { fg } from '@atlaskit/platform-feature-flags';
18
18
  import Popup from '@atlaskit/popup';
19
19
  import Tooltip from '@atlaskit/tooltip';
20
20
  import AvatarGroupItem from './avatar-group-item';
21
+ import { MoreDropdownTopLayer } from './avatar-group-top-layer';
21
22
  import Grid from './grid';
22
23
  import FocusManager from './internal/components/focus-manager';
23
24
  import PopupAvatarGroup from './internal/components/popup-avatar-group';
@@ -120,13 +121,21 @@ var AvatarGroup = function AvatarGroup(_ref) {
120
121
  bindFocus = _useFocus.bindFocus;
121
122
 
122
123
  // When a trigger is focused, we want to open the popup
123
- // the user presses the DownArrow
124
+ // the user presses the DownArrow.
125
+ // Skipped when top-layer is enabled — DropdownMenu/top-layer handles
126
+ // ArrowDown-to-open and arrow key navigation internally.
124
127
  useEffect(function () {
125
128
  // Set initial value if popup is closed
126
129
  if (!isOpen) {
127
130
  setTriggeredUsingKeyboard(false);
128
131
  }
129
132
 
133
+ // Top-layer path: ArrowDown-to-open is handled by the menu's
134
+ // own focus management, so skip the parent's ArrowDown handler.
135
+ if (fg('platform-dst-top-layer')) {
136
+ return noop;
137
+ }
138
+
130
139
  // Only need to listen for keydown when focused
131
140
  if (!isFocused) {
132
141
  return noop;
@@ -194,6 +203,24 @@ var AvatarGroup = function AvatarGroup(_ref) {
194
203
  onClick: onMoreClick
195
204
  });
196
205
  }
206
+ if (fg('platform-dst-top-layer')) {
207
+ return /*#__PURE__*/React.createElement(MoreDropdownTopLayer, {
208
+ isOpen: isOpen,
209
+ onClose: onClose,
210
+ isTriggeredUsingKeyboard: isTriggeredUsingKeyboard,
211
+ data: data,
212
+ max: max
213
+ // eslint-disable-next-line @repo/internal/react/no-unsafe-overrides
214
+ ,
215
+ overrides: overrides,
216
+ onAvatarClick: onAvatarClick,
217
+ testId: testId,
218
+ labelId: labelId,
219
+ renderMoreButton: renderMoreButton,
220
+ handleTriggerClicked: handleTriggerClicked,
221
+ bindFocus: bindFocus
222
+ });
223
+ }
197
224
 
198
225
  // split boundariesElement into `boundary` and `rootBoundary` props for Popup
199
226
  var boundary = boundariesElement === 'scrollParent' ? 'clippingParents' : undefined;
@@ -7,6 +7,11 @@ export interface AvatarGroupItemProps {
7
7
  index: number;
8
8
  onAvatarClick?: onAvatarClickHandler;
9
9
  testId?: string;
10
+ /**
11
+ * Use this to override the accessibility role for the element.
12
+ * When used inside a dropdown menu, this should be set to "menuitem".
13
+ */
14
+ role?: string;
10
15
  }
11
16
  declare const AvatarGroupItem: React.ForwardRefExoticComponent<React.PropsWithoutRef<AvatarGroupItemProps> & React.RefAttributes<HTMLElement>>;
12
17
  export default AvatarGroupItem;
@@ -0,0 +1,44 @@
1
+ import React, { type MouseEventHandler } from 'react';
2
+ import { type AvatarGroupOverrides, type AvatarProps, type onAvatarClickHandler } from './types';
3
+ type TMoreDropdownTopLayerProps = {
4
+ isOpen: boolean;
5
+ onClose: () => void;
6
+ isTriggeredUsingKeyboard: boolean;
7
+ data: Array<AvatarProps>;
8
+ max: number;
9
+ overrides?: AvatarGroupOverrides;
10
+ onAvatarClick?: onAvatarClickHandler;
11
+ testId?: string;
12
+ labelId: string;
13
+ renderMoreButton: (props: {
14
+ 'aria-controls'?: string;
15
+ 'aria-expanded'?: boolean;
16
+ 'aria-haspopup'?: boolean | 'dialog' | 'menu' | 'listbox' | 'tree' | 'grid';
17
+ onClick: MouseEventHandler;
18
+ ref?: React.Ref<HTMLElement>;
19
+ }) => React.ReactNode;
20
+ handleTriggerClicked: (event: React.MouseEvent | KeyboardEvent) => void;
21
+ bindFocus: {
22
+ onFocus: (event: React.FocusEvent) => void;
23
+ onBlur: (event: React.FocusEvent) => void;
24
+ };
25
+ };
26
+ /**
27
+ * Top-layer implementation of the avatar group overflow dropdown.
28
+ *
29
+ * Replaces the legacy `@atlaskit/popup` rendering pipeline
30
+ * (Popper.js + Portal + focus-trap + @atlaskit/layering)
31
+ * with the native Popover API + CSS Anchor Positioning via `@atlaskit/top-layer`.
32
+ *
33
+ * Uses `role="menu"` with arrow key navigation for correct menu semantics.
34
+ *
35
+ * Gated behind the `platform-dst-top-layer` feature flag.
36
+ *
37
+ * Legacy props that are no-ops in the top-layer path (not accepted here):
38
+ * - zIndex: stacking managed by browser top layer
39
+ * - shouldRenderToParent: always renders in top layer
40
+ * - boundary / rootBoundary: viewport is the natural boundary
41
+ * - shouldFlip: CSS Anchor Positioning handles flipping
42
+ */
43
+ export declare function MoreDropdownTopLayer({ isOpen, onClose, isTriggeredUsingKeyboard: _isTriggeredUsingKeyboard, data, max, overrides, onAvatarClick, testId, labelId, renderMoreButton, handleTriggerClicked, bindFocus: _bindFocus, }: TMoreDropdownTopLayerProps): React.JSX.Element;
44
+ export {};
@@ -4,7 +4,7 @@ export interface MoreIndicatorProps {
4
4
  count: number;
5
5
  'aria-controls'?: string;
6
6
  'aria-expanded'?: boolean;
7
- 'aria-haspopup'?: boolean | 'dialog';
7
+ 'aria-haspopup'?: boolean | 'dialog' | 'menu' | 'listbox' | 'tree' | 'grid';
8
8
  moreIndicatorLabel?: string;
9
9
  buttonProps: Partial<React.HTMLAttributes<HTMLElement>>;
10
10
  onClick: AvatarClickEventHandler;
@@ -7,6 +7,11 @@ export interface AvatarGroupItemProps {
7
7
  index: number;
8
8
  onAvatarClick?: onAvatarClickHandler;
9
9
  testId?: string;
10
+ /**
11
+ * Use this to override the accessibility role for the element.
12
+ * When used inside a dropdown menu, this should be set to "menuitem".
13
+ */
14
+ role?: string;
10
15
  }
11
16
  declare const AvatarGroupItem: React.ForwardRefExoticComponent<React.PropsWithoutRef<AvatarGroupItemProps> & React.RefAttributes<HTMLElement>>;
12
17
  export default AvatarGroupItem;
@@ -0,0 +1,44 @@
1
+ import React, { type MouseEventHandler } from 'react';
2
+ import { type AvatarGroupOverrides, type AvatarProps, type onAvatarClickHandler } from './types';
3
+ type TMoreDropdownTopLayerProps = {
4
+ isOpen: boolean;
5
+ onClose: () => void;
6
+ isTriggeredUsingKeyboard: boolean;
7
+ data: Array<AvatarProps>;
8
+ max: number;
9
+ overrides?: AvatarGroupOverrides;
10
+ onAvatarClick?: onAvatarClickHandler;
11
+ testId?: string;
12
+ labelId: string;
13
+ renderMoreButton: (props: {
14
+ 'aria-controls'?: string;
15
+ 'aria-expanded'?: boolean;
16
+ 'aria-haspopup'?: boolean | 'dialog' | 'menu' | 'listbox' | 'tree' | 'grid';
17
+ onClick: MouseEventHandler;
18
+ ref?: React.Ref<HTMLElement>;
19
+ }) => React.ReactNode;
20
+ handleTriggerClicked: (event: React.MouseEvent | KeyboardEvent) => void;
21
+ bindFocus: {
22
+ onFocus: (event: React.FocusEvent) => void;
23
+ onBlur: (event: React.FocusEvent) => void;
24
+ };
25
+ };
26
+ /**
27
+ * Top-layer implementation of the avatar group overflow dropdown.
28
+ *
29
+ * Replaces the legacy `@atlaskit/popup` rendering pipeline
30
+ * (Popper.js + Portal + focus-trap + @atlaskit/layering)
31
+ * with the native Popover API + CSS Anchor Positioning via `@atlaskit/top-layer`.
32
+ *
33
+ * Uses `role="menu"` with arrow key navigation for correct menu semantics.
34
+ *
35
+ * Gated behind the `platform-dst-top-layer` feature flag.
36
+ *
37
+ * Legacy props that are no-ops in the top-layer path (not accepted here):
38
+ * - zIndex: stacking managed by browser top layer
39
+ * - shouldRenderToParent: always renders in top layer
40
+ * - boundary / rootBoundary: viewport is the natural boundary
41
+ * - shouldFlip: CSS Anchor Positioning handles flipping
42
+ */
43
+ export declare function MoreDropdownTopLayer({ isOpen, onClose, isTriggeredUsingKeyboard: _isTriggeredUsingKeyboard, data, max, overrides, onAvatarClick, testId, labelId, renderMoreButton, handleTriggerClicked, bindFocus: _bindFocus, }: TMoreDropdownTopLayerProps): React.JSX.Element;
44
+ export {};
@@ -4,7 +4,7 @@ export interface MoreIndicatorProps {
4
4
  count: number;
5
5
  'aria-controls'?: string;
6
6
  'aria-expanded'?: boolean;
7
- 'aria-haspopup'?: boolean | 'dialog';
7
+ 'aria-haspopup'?: boolean | 'dialog' | 'menu' | 'listbox' | 'tree' | 'grid';
8
8
  moreIndicatorLabel?: string;
9
9
  buttonProps: Partial<React.HTMLAttributes<HTMLElement>>;
10
10
  onClick: AvatarClickEventHandler;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/avatar-group",
3
- "version": "12.7.2",
3
+ "version": "12.8.0",
4
4
  "description": "An avatar group displays a number of avatars grouped together in a stack or grid.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/"
@@ -37,9 +37,10 @@
37
37
  "@atlaskit/menu": "^8.5.0",
38
38
  "@atlaskit/motion": "^6.2.0",
39
39
  "@atlaskit/platform-feature-flags": "^1.1.0",
40
- "@atlaskit/popup": "^4.17.0",
40
+ "@atlaskit/popup": "^4.18.0",
41
41
  "@atlaskit/tokens": "^13.0.0",
42
- "@atlaskit/tooltip": "^22.0.0",
42
+ "@atlaskit/tooltip": "^22.1.0",
43
+ "@atlaskit/top-layer": "^0.6.0",
43
44
  "@babel/runtime": "^7.0.0",
44
45
  "@compiled/react": "^0.20.0",
45
46
  "bind-event-listener": "^3.0.0"
@@ -55,9 +56,9 @@
55
56
  "@atlaskit/button": "^23.11.0",
56
57
  "@atlaskit/docs": "^11.8.0",
57
58
  "@atlaskit/form": "^15.5.0",
58
- "@atlaskit/icon": "^34.3.0",
59
+ "@atlaskit/icon": "^34.5.0",
59
60
  "@atlaskit/link": "^3.4.0",
60
- "@atlaskit/modal-dialog": "^14.18.0",
61
+ "@atlaskit/modal-dialog": "^15.0.0",
61
62
  "@atlaskit/primitives": "^19.0.0",
62
63
  "@atlaskit/section-message": "^8.12.0",
63
64
  "@atlaskit/toggle": "^15.6.0",
@@ -107,6 +108,9 @@
107
108
  "platform-avatar-group-spacing-fix": {
108
109
  "type": "boolean"
109
110
  },
111
+ "platform-dst-top-layer": {
112
+ "type": "boolean"
113
+ },
110
114
  "jira-ai-agent-stack": {
111
115
  "type": "boolean"
112
116
  },