@atlaskit/avatar-group 9.3.2 → 9.3.4

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.
Files changed (30) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cjs/components/avatar-group-item.js +18 -10
  3. package/dist/cjs/components/avatar-group.js +3 -2
  4. package/dist/cjs/components/internal/components/focus-manager.js +64 -0
  5. package/dist/cjs/components/internal/hooks/use-register-item-with-focus-manager.js +23 -0
  6. package/dist/cjs/components/internal/utiles/handle-focus.js +82 -0
  7. package/dist/cjs/version.json +1 -1
  8. package/dist/es2019/components/avatar-group-item.js +14 -8
  9. package/dist/es2019/components/avatar-group.js +3 -2
  10. package/dist/es2019/components/internal/components/focus-manager.js +54 -0
  11. package/dist/es2019/components/internal/hooks/use-register-item-with-focus-manager.js +18 -0
  12. package/dist/es2019/components/internal/utiles/handle-focus.js +78 -0
  13. package/dist/es2019/version.json +1 -1
  14. package/dist/esm/components/avatar-group-item.js +15 -10
  15. package/dist/esm/components/avatar-group.js +3 -2
  16. package/dist/esm/components/internal/components/focus-manager.js +53 -0
  17. package/dist/esm/components/internal/hooks/use-register-item-with-focus-manager.js +17 -0
  18. package/dist/esm/components/internal/utiles/handle-focus.js +75 -0
  19. package/dist/esm/version.json +1 -1
  20. package/dist/types/components/avatar-group-item.d.ts +2 -2
  21. package/dist/types/components/internal/components/focus-manager.d.ts +21 -0
  22. package/dist/types/components/internal/hooks/use-register-item-with-focus-manager.d.ts +4 -0
  23. package/dist/types/components/internal/utiles/handle-focus.d.ts +2 -0
  24. package/dist/types/components/types.d.ts +2 -0
  25. package/dist/types-ts4.5/components/avatar-group-item.d.ts +2 -2
  26. package/dist/types-ts4.5/components/internal/components/focus-manager.d.ts +21 -0
  27. package/dist/types-ts4.5/components/internal/hooks/use-register-item-with-focus-manager.d.ts +4 -0
  28. package/dist/types-ts4.5/components/internal/utiles/handle-focus.d.ts +2 -0
  29. package/dist/types-ts4.5/components/types.d.ts +2 -0
  30. package/package.json +7 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @atlaskit/avatar-group
2
2
 
3
+ ## 9.3.4
4
+
5
+ ### Patch Changes
6
+
7
+ - [`cb7033c5b72`](https://bitbucket.org/atlassian/atlassian-frontend/commits/cb7033c5b72) - keyboard arrow (UP and DOWN) support in avatar-group popup component
8
+
9
+ ## 9.3.3
10
+
11
+ ### Patch Changes
12
+
13
+ - [`e7ea6832ad2`](https://bitbucket.org/atlassian/atlassian-frontend/commits/e7ea6832ad2) - Bans the use of React.FC/React.FunctionComponent type in ADS components as part of the React 18 migration work. The change is internal only and should not introduce any changes for the component consumers.
14
+
3
15
  ## 9.3.2
4
16
 
5
17
  ### Patch Changes
@@ -1,28 +1,34 @@
1
1
  "use strict";
2
2
 
3
3
  var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ var _typeof = require("@babel/runtime/helpers/typeof");
4
5
  Object.defineProperty(exports, "__esModule", {
5
6
  value: true
6
7
  });
7
8
  exports.default = void 0;
8
9
  var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
9
10
  var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties"));
10
- var _react = _interopRequireDefault(require("react"));
11
+ var _react = _interopRequireWildcard(require("react"));
11
12
  var _avatar = _interopRequireDefault(require("@atlaskit/avatar"));
13
+ var _mergeRefs = _interopRequireDefault(require("@atlaskit/ds-lib/merge-refs"));
12
14
  var _menu = require("@atlaskit/menu");
15
+ var _useRegisterItemWithFocusManager = _interopRequireDefault(require("./internal/hooks/use-register-item-with-focus-manager"));
13
16
  var _excluded = ["href", "onClick"],
14
17
  _excluded2 = ["children"];
15
- var AvatarGroupItem = function AvatarGroupItem(_ref) {
16
- var avatar = _ref.avatar,
17
- onAvatarClick = _ref.onAvatarClick,
18
- testId = _ref.testId,
19
- index = _ref.index;
18
+ function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
19
+ function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
20
+ var AvatarGroupItem = /*#__PURE__*/(0, _react.forwardRef)(function (props, ref) {
21
+ var avatar = props.avatar,
22
+ onAvatarClick = props.onAvatarClick,
23
+ testId = props.testId,
24
+ index = props.index;
20
25
  var href = avatar.href,
21
26
  onClick = avatar.onClick,
22
27
  rest = (0, _objectWithoutProperties2.default)(avatar, _excluded);
23
- var CustomComponent = function CustomComponent(_ref2) {
24
- var children = _ref2.children,
25
- props = (0, _objectWithoutProperties2.default)(_ref2, _excluded2);
28
+ var itemRef = (0, _useRegisterItemWithFocusManager.default)();
29
+ var CustomComponent = function CustomComponent(_ref) {
30
+ var children = _ref.children,
31
+ props = (0, _objectWithoutProperties2.default)(_ref, _excluded2);
26
32
  // eslint-disable-next-line @repo/internal/react/no-unsafe-spread-props
27
33
  return /*#__PURE__*/_react.default.createElement("span", props, children);
28
34
  };
@@ -37,6 +43,7 @@ var AvatarGroupItem = function AvatarGroupItem(_ref) {
37
43
  var callback = onClick || onAvatarClick;
38
44
  if (href) {
39
45
  return /*#__PURE__*/_react.default.createElement(_menu.LinkItem, {
46
+ ref: (0, _mergeRefs.default)([ref, itemRef]),
40
47
  href: href,
41
48
  target: avatar.target,
42
49
  rel: avatar.target === '_blank' ? 'noopener noreferrer' : undefined,
@@ -49,6 +56,7 @@ var AvatarGroupItem = function AvatarGroupItem(_ref) {
49
56
  }
50
57
  if (typeof callback === 'function') {
51
58
  return /*#__PURE__*/_react.default.createElement(_menu.ButtonItem, {
59
+ ref: (0, _mergeRefs.default)([ref, itemRef]),
52
60
  onClick: function onClick(event) {
53
61
  return callback && callback(event, undefined, index);
54
62
  },
@@ -61,7 +69,7 @@ var AvatarGroupItem = function AvatarGroupItem(_ref) {
61
69
  component: CustomComponent,
62
70
  testId: testId
63
71
  }, avatar.name);
64
- };
72
+ });
65
73
 
66
74
  // eslint-disable-next-line @repo/internal/react/require-jsdoc
67
75
  var _default = AvatarGroupItem;
@@ -17,6 +17,7 @@ var _constants = require("@atlaskit/theme/constants");
17
17
  var _tooltip = _interopRequireDefault(require("@atlaskit/tooltip"));
18
18
  var _avatarGroupItem = _interopRequireDefault(require("./avatar-group-item"));
19
19
  var _grid = _interopRequireDefault(require("./grid"));
20
+ var _focusManager = _interopRequireDefault(require("./internal/components/focus-manager"));
20
21
  var _moreIndicator = _interopRequireDefault(require("./more-indicator"));
21
22
  var _stack = _interopRequireDefault(require("./stack"));
22
23
  var _utils = require("./utils");
@@ -122,7 +123,7 @@ var AvatarGroup = function AvatarGroup(_ref) {
122
123
  shouldFlip: true,
123
124
  zIndex: _constants.layers.modal(),
124
125
  content: function content() {
125
- return (0, _react2.jsx)(_menu.PopupMenuGroup, {
126
+ return (0, _react2.jsx)(_focusManager.default, null, (0, _react2.jsx)(_menu.PopupMenuGroup, {
126
127
  onClick: function onClick(e) {
127
128
  return e.stopPropagation();
128
129
  },
@@ -138,7 +139,7 @@ var AvatarGroup = function AvatarGroup(_ref) {
138
139
  // This index holds the true index,
139
140
  // adding up the index of non-overflowed avatars and overflowed avatars.
140
141
  index + max);
141
- })));
142
+ }))));
142
143
  },
143
144
  trigger: function trigger(triggerProps) {
144
145
  return renderMoreButton(_objectSpread(_objectSpread({}, triggerProps), {}, {
@@ -0,0 +1,64 @@
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.default = exports.FocusManagerContext = void 0;
9
+ var _react = _interopRequireWildcard(require("react"));
10
+ var _bindEventListener = require("bind-event-listener");
11
+ var _noop = _interopRequireDefault(require("@atlaskit/ds-lib/noop"));
12
+ var _handleFocus = _interopRequireDefault(require("../utiles/handle-focus"));
13
+ function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
14
+ function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
15
+ /**
16
+ *
17
+ *
18
+ * Context provider which maintains the list of focusable elements and a method to
19
+ * register new menu items.
20
+ * This list drives the keyboard navgation of the menu.
21
+ *
22
+ */
23
+ var FocusManagerContext = /*#__PURE__*/(0, _react.createContext)({
24
+ menuItemRefs: [],
25
+ registerRef: _noop.default
26
+ });
27
+
28
+ /**
29
+ * Focus manager logic
30
+ */
31
+ exports.FocusManagerContext = FocusManagerContext;
32
+ var FocusManager = function FocusManager(_ref) {
33
+ var children = _ref.children;
34
+ var menuItemRefs = (0, _react.useRef)([]);
35
+ var registerRef = (0, _react.useCallback)(function (ref) {
36
+ if (ref && !menuItemRefs.current.includes(ref)) {
37
+ menuItemRefs.current.push(ref);
38
+ }
39
+ }, []);
40
+
41
+ // set focus and intentionally rebinding listener and clean up listener on each render
42
+ (0, _react.useEffect)(function () {
43
+ (0, _bindEventListener.bind)(window, {
44
+ type: 'keydown',
45
+ listener: (0, _handleFocus.default)(menuItemRefs.current)
46
+ });
47
+ var unbind = function unbind() {
48
+ (0, _bindEventListener.bind)(window, {
49
+ type: 'keydown',
50
+ listener: (0, _handleFocus.default)(menuItemRefs.current)
51
+ });
52
+ };
53
+ return unbind;
54
+ }, []);
55
+ var contextValue = {
56
+ menuItemRefs: menuItemRefs.current,
57
+ registerRef: registerRef
58
+ };
59
+ return /*#__PURE__*/_react.default.createElement(FocusManagerContext.Provider, {
60
+ value: contextValue
61
+ }, children);
62
+ };
63
+ var _default = FocusManager;
64
+ exports.default = _default;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _react = require("react");
8
+ var _focusManager = require("../components/focus-manager");
9
+ // The refs stored in the context are used to programatically
10
+ // control focus on a user navigates using the keyboard.
11
+ function useRegisterItemWithFocusManager() {
12
+ var _useContext = (0, _react.useContext)(_focusManager.FocusManagerContext),
13
+ registerRef = _useContext.registerRef;
14
+ var itemRef = (0, _react.useRef)(null);
15
+ (0, _react.useEffect)(function () {
16
+ if (itemRef.current !== null) {
17
+ registerRef(itemRef.current);
18
+ }
19
+ }, [registerRef]);
20
+ return itemRef;
21
+ }
22
+ var _default = useRegisterItemWithFocusManager;
23
+ exports.default = _default;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.default = handleFocus;
8
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
+ var _keycodes = require("@atlaskit/ds-lib/keycodes");
10
+ var _actionMap;
11
+ var actionMap = (_actionMap = {}, (0, _defineProperty2.default)(_actionMap, _keycodes.KEY_DOWN, 'next'), (0, _defineProperty2.default)(_actionMap, _keycodes.KEY_UP, 'prev'), (0, _defineProperty2.default)(_actionMap, _keycodes.KEY_HOME, 'first'), (0, _defineProperty2.default)(_actionMap, _keycodes.KEY_END, 'last'), _actionMap);
12
+
13
+ /**
14
+ * currentFocusedIdx + 1 will not work if the next focusable element
15
+ * is disabled. So, we need to iterate through the following menu items
16
+ * to find one that isn't disabled. If all following elements are disabled,
17
+ * return undefined.
18
+ */
19
+ var getNextFocusableElement = function getNextFocusableElement(refs, currentFocusedIdx) {
20
+ while (currentFocusedIdx + 1 < refs.length) {
21
+ var isDisabled = refs[currentFocusedIdx + 1].getAttribute('disabled') !== null;
22
+ if (!isDisabled) {
23
+ return refs[currentFocusedIdx + 1];
24
+ }
25
+ currentFocusedIdx++;
26
+ }
27
+ };
28
+
29
+ /**
30
+ * currentFocusedIdx - 1 will not work if the prev focusable element
31
+ * is disabled. So, we need to iterate through the previous menu items
32
+ * to find one that isn't disabled. If all previous elements are disabled,
33
+ * return undefined.
34
+ */
35
+ var getPrevFocusableElement = function getPrevFocusableElement(refs, currentFocusedIdx) {
36
+ while (currentFocusedIdx > 0) {
37
+ var isDisabled = refs[currentFocusedIdx - 1].getAttribute('disabled') !== null;
38
+ if (!isDisabled) {
39
+ return refs[currentFocusedIdx - 1];
40
+ }
41
+ currentFocusedIdx--;
42
+ }
43
+ };
44
+ function handleFocus(refs) {
45
+ return function (e) {
46
+ var currentFocusedIdx = refs.findIndex(function (el) {
47
+ var _document$activeEleme;
48
+ return (_document$activeEleme = document.activeElement) === null || _document$activeEleme === void 0 ? void 0 : _document$activeEleme.isSameNode(el);
49
+ });
50
+ var action = actionMap[e.key];
51
+ switch (action) {
52
+ case 'next':
53
+ if (currentFocusedIdx < refs.length - 1) {
54
+ e.preventDefault();
55
+ var _nextFocusableElement = getNextFocusableElement(refs, currentFocusedIdx);
56
+ _nextFocusableElement && _nextFocusableElement.focus();
57
+ }
58
+ break;
59
+ case 'prev':
60
+ if (currentFocusedIdx > 0) {
61
+ e.preventDefault();
62
+ var _prevFocusableElement = getPrevFocusableElement(refs, currentFocusedIdx);
63
+ _prevFocusableElement && _prevFocusableElement.focus();
64
+ }
65
+ break;
66
+ case 'first':
67
+ e.preventDefault();
68
+ // Search for first non-disabled element if first element is disabled
69
+ var nextFocusableElement = getNextFocusableElement(refs, -1);
70
+ nextFocusableElement && nextFocusableElement.focus();
71
+ break;
72
+ case 'last':
73
+ e.preventDefault();
74
+ // Search for last non-disabled element if last element is disabled
75
+ var prevFocusableElement = getPrevFocusableElement(refs, refs.length);
76
+ prevFocusableElement && prevFocusableElement.focus();
77
+ break;
78
+ default:
79
+ return;
80
+ }
81
+ };
82
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/avatar-group",
3
- "version": "9.3.2",
3
+ "version": "9.3.4",
4
4
  "sideEffects": false
5
5
  }
@@ -1,18 +1,22 @@
1
1
  import _extends from "@babel/runtime/helpers/extends";
2
- import React from 'react';
2
+ import React, { forwardRef } from 'react';
3
3
  import Avatar from '@atlaskit/avatar';
4
+ import mergeRefs from '@atlaskit/ds-lib/merge-refs';
4
5
  import { ButtonItem, CustomItem, LinkItem } from '@atlaskit/menu';
5
- const AvatarGroupItem = ({
6
- avatar,
7
- onAvatarClick,
8
- testId,
9
- index
10
- }) => {
6
+ import useRegisterItemWithFocusManager from './internal/hooks/use-register-item-with-focus-manager';
7
+ const AvatarGroupItem = /*#__PURE__*/forwardRef((props, ref) => {
8
+ const {
9
+ avatar,
10
+ onAvatarClick,
11
+ testId,
12
+ index
13
+ } = props;
11
14
  const {
12
15
  href,
13
16
  onClick,
14
17
  ...rest
15
18
  } = avatar;
19
+ const itemRef = useRegisterItemWithFocusManager();
16
20
  const CustomComponent = ({
17
21
  children,
18
22
  ...props
@@ -31,6 +35,7 @@ const AvatarGroupItem = ({
31
35
  const callback = onClick || onAvatarClick;
32
36
  if (href) {
33
37
  return /*#__PURE__*/React.createElement(LinkItem, {
38
+ ref: mergeRefs([ref, itemRef]),
34
39
  href: href,
35
40
  target: avatar.target,
36
41
  rel: avatar.target === '_blank' ? 'noopener noreferrer' : undefined,
@@ -41,6 +46,7 @@ const AvatarGroupItem = ({
41
46
  }
42
47
  if (typeof callback === 'function') {
43
48
  return /*#__PURE__*/React.createElement(ButtonItem, {
49
+ ref: mergeRefs([ref, itemRef]),
44
50
  onClick: event => callback && callback(event, undefined, index),
45
51
  iconBefore: AvatarIcon,
46
52
  testId: testId
@@ -51,7 +57,7 @@ const AvatarGroupItem = ({
51
57
  component: CustomComponent,
52
58
  testId: testId
53
59
  }, avatar.name);
54
- };
60
+ });
55
61
 
56
62
  // eslint-disable-next-line @repo/internal/react/require-jsdoc
57
63
  export default AvatarGroupItem;
@@ -9,6 +9,7 @@ import { layers } from '@atlaskit/theme/constants';
9
9
  import Tooltip from '@atlaskit/tooltip';
10
10
  import AvatarGroupItem from './avatar-group-item';
11
11
  import Grid from './grid';
12
+ import FocusManager from './internal/components/focus-manager';
12
13
  import MoreIndicator from './more-indicator';
13
14
  import Stack from './stack';
14
15
  import { composeUniqueKey } from './utils';
@@ -97,7 +98,7 @@ const AvatarGroup = ({
97
98
  rootBoundary: rootBoundary,
98
99
  shouldFlip: true,
99
100
  zIndex: layers.modal(),
100
- content: () => jsx(PopupMenuGroup, {
101
+ content: () => jsx(FocusManager, null, jsx(PopupMenuGroup, {
101
102
  onClick: e => e.stopPropagation(),
102
103
  minWidth: 250,
103
104
  maxHeight: 300
@@ -109,7 +110,7 @@ const AvatarGroup = ({
109
110
  },
110
111
  // This index holds the true index,
111
112
  // adding up the index of non-overflowed avatars and overflowed avatars.
112
- index + max)))),
113
+ index + max))))),
113
114
  trigger: triggerProps => renderMoreButton({
114
115
  ...triggerProps,
115
116
  onClick: () => setIsOpen(!isOpen)
@@ -0,0 +1,54 @@
1
+ import React, { createContext, useCallback, useEffect, useRef } from 'react';
2
+ import { bind } from 'bind-event-listener';
3
+ import __noop from '@atlaskit/ds-lib/noop';
4
+ import handleFocus from '../utiles/handle-focus';
5
+
6
+ /**
7
+ *
8
+ *
9
+ * Context provider which maintains the list of focusable elements and a method to
10
+ * register new menu items.
11
+ * This list drives the keyboard navgation of the menu.
12
+ *
13
+ */
14
+ export const FocusManagerContext = /*#__PURE__*/createContext({
15
+ menuItemRefs: [],
16
+ registerRef: __noop
17
+ });
18
+
19
+ /**
20
+ * Focus manager logic
21
+ */
22
+ const FocusManager = ({
23
+ children
24
+ }) => {
25
+ const menuItemRefs = useRef([]);
26
+ const registerRef = useCallback(ref => {
27
+ if (ref && !menuItemRefs.current.includes(ref)) {
28
+ menuItemRefs.current.push(ref);
29
+ }
30
+ }, []);
31
+
32
+ // set focus and intentionally rebinding listener and clean up listener on each render
33
+ useEffect(() => {
34
+ bind(window, {
35
+ type: 'keydown',
36
+ listener: handleFocus(menuItemRefs.current)
37
+ });
38
+ const unbind = () => {
39
+ bind(window, {
40
+ type: 'keydown',
41
+ listener: handleFocus(menuItemRefs.current)
42
+ });
43
+ };
44
+ return unbind;
45
+ }, []);
46
+ const contextValue = {
47
+ menuItemRefs: menuItemRefs.current,
48
+ registerRef
49
+ };
50
+ return /*#__PURE__*/React.createElement(FocusManagerContext.Provider, {
51
+ value: contextValue
52
+ }, children);
53
+ };
54
+ export default FocusManager;
@@ -0,0 +1,18 @@
1
+ import { useContext, useEffect, useRef } from 'react';
2
+ import { FocusManagerContext } from '../components/focus-manager';
3
+
4
+ // The refs stored in the context are used to programatically
5
+ // control focus on a user navigates using the keyboard.
6
+ function useRegisterItemWithFocusManager() {
7
+ const {
8
+ registerRef
9
+ } = useContext(FocusManagerContext);
10
+ const itemRef = useRef(null);
11
+ useEffect(() => {
12
+ if (itemRef.current !== null) {
13
+ registerRef(itemRef.current);
14
+ }
15
+ }, [registerRef]);
16
+ return itemRef;
17
+ }
18
+ export default useRegisterItemWithFocusManager;
@@ -0,0 +1,78 @@
1
+ import { KEY_DOWN, KEY_END, KEY_HOME, KEY_UP } from '@atlaskit/ds-lib/keycodes';
2
+ const actionMap = {
3
+ [KEY_DOWN]: 'next',
4
+ [KEY_UP]: 'prev',
5
+ [KEY_HOME]: 'first',
6
+ [KEY_END]: 'last'
7
+ };
8
+
9
+ /**
10
+ * currentFocusedIdx + 1 will not work if the next focusable element
11
+ * is disabled. So, we need to iterate through the following menu items
12
+ * to find one that isn't disabled. If all following elements are disabled,
13
+ * return undefined.
14
+ */
15
+ const getNextFocusableElement = (refs, currentFocusedIdx) => {
16
+ while (currentFocusedIdx + 1 < refs.length) {
17
+ const isDisabled = refs[currentFocusedIdx + 1].getAttribute('disabled') !== null;
18
+ if (!isDisabled) {
19
+ return refs[currentFocusedIdx + 1];
20
+ }
21
+ currentFocusedIdx++;
22
+ }
23
+ };
24
+
25
+ /**
26
+ * currentFocusedIdx - 1 will not work if the prev focusable element
27
+ * is disabled. So, we need to iterate through the previous menu items
28
+ * to find one that isn't disabled. If all previous elements are disabled,
29
+ * return undefined.
30
+ */
31
+ const getPrevFocusableElement = (refs, currentFocusedIdx) => {
32
+ while (currentFocusedIdx > 0) {
33
+ const isDisabled = refs[currentFocusedIdx - 1].getAttribute('disabled') !== null;
34
+ if (!isDisabled) {
35
+ return refs[currentFocusedIdx - 1];
36
+ }
37
+ currentFocusedIdx--;
38
+ }
39
+ };
40
+ export default function handleFocus(refs) {
41
+ return e => {
42
+ const currentFocusedIdx = refs.findIndex(el => {
43
+ var _document$activeEleme;
44
+ return (_document$activeEleme = document.activeElement) === null || _document$activeEleme === void 0 ? void 0 : _document$activeEleme.isSameNode(el);
45
+ });
46
+ const action = actionMap[e.key];
47
+ switch (action) {
48
+ case 'next':
49
+ if (currentFocusedIdx < refs.length - 1) {
50
+ e.preventDefault();
51
+ const nextFocusableElement = getNextFocusableElement(refs, currentFocusedIdx);
52
+ nextFocusableElement && nextFocusableElement.focus();
53
+ }
54
+ break;
55
+ case 'prev':
56
+ if (currentFocusedIdx > 0) {
57
+ e.preventDefault();
58
+ const prevFocusableElement = getPrevFocusableElement(refs, currentFocusedIdx);
59
+ prevFocusableElement && prevFocusableElement.focus();
60
+ }
61
+ break;
62
+ case 'first':
63
+ e.preventDefault();
64
+ // Search for first non-disabled element if first element is disabled
65
+ const nextFocusableElement = getNextFocusableElement(refs, -1);
66
+ nextFocusableElement && nextFocusableElement.focus();
67
+ break;
68
+ case 'last':
69
+ e.preventDefault();
70
+ // Search for last non-disabled element if last element is disabled
71
+ const prevFocusableElement = getPrevFocusableElement(refs, refs.length);
72
+ prevFocusableElement && prevFocusableElement.focus();
73
+ break;
74
+ default:
75
+ return;
76
+ }
77
+ };
78
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/avatar-group",
3
- "version": "9.3.2",
3
+ "version": "9.3.4",
4
4
  "sideEffects": false
5
5
  }
@@ -2,20 +2,23 @@ import _extends from "@babel/runtime/helpers/extends";
2
2
  import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties";
3
3
  var _excluded = ["href", "onClick"],
4
4
  _excluded2 = ["children"];
5
- import React from 'react';
5
+ import React, { forwardRef } from 'react';
6
6
  import Avatar from '@atlaskit/avatar';
7
+ import mergeRefs from '@atlaskit/ds-lib/merge-refs';
7
8
  import { ButtonItem, CustomItem, LinkItem } from '@atlaskit/menu';
8
- var AvatarGroupItem = function AvatarGroupItem(_ref) {
9
- var avatar = _ref.avatar,
10
- onAvatarClick = _ref.onAvatarClick,
11
- testId = _ref.testId,
12
- index = _ref.index;
9
+ import useRegisterItemWithFocusManager from './internal/hooks/use-register-item-with-focus-manager';
10
+ var AvatarGroupItem = /*#__PURE__*/forwardRef(function (props, ref) {
11
+ var avatar = props.avatar,
12
+ onAvatarClick = props.onAvatarClick,
13
+ testId = props.testId,
14
+ index = props.index;
13
15
  var href = avatar.href,
14
16
  onClick = avatar.onClick,
15
17
  rest = _objectWithoutProperties(avatar, _excluded);
16
- var CustomComponent = function CustomComponent(_ref2) {
17
- var children = _ref2.children,
18
- props = _objectWithoutProperties(_ref2, _excluded2);
18
+ var itemRef = useRegisterItemWithFocusManager();
19
+ var CustomComponent = function CustomComponent(_ref) {
20
+ var children = _ref.children,
21
+ props = _objectWithoutProperties(_ref, _excluded2);
19
22
  // eslint-disable-next-line @repo/internal/react/no-unsafe-spread-props
20
23
  return /*#__PURE__*/React.createElement("span", props, children);
21
24
  };
@@ -30,6 +33,7 @@ var AvatarGroupItem = function AvatarGroupItem(_ref) {
30
33
  var callback = onClick || onAvatarClick;
31
34
  if (href) {
32
35
  return /*#__PURE__*/React.createElement(LinkItem, {
36
+ ref: mergeRefs([ref, itemRef]),
33
37
  href: href,
34
38
  target: avatar.target,
35
39
  rel: avatar.target === '_blank' ? 'noopener noreferrer' : undefined,
@@ -42,6 +46,7 @@ var AvatarGroupItem = function AvatarGroupItem(_ref) {
42
46
  }
43
47
  if (typeof callback === 'function') {
44
48
  return /*#__PURE__*/React.createElement(ButtonItem, {
49
+ ref: mergeRefs([ref, itemRef]),
45
50
  onClick: function onClick(event) {
46
51
  return callback && callback(event, undefined, index);
47
52
  },
@@ -54,7 +59,7 @@ var AvatarGroupItem = function AvatarGroupItem(_ref) {
54
59
  component: CustomComponent,
55
60
  testId: testId
56
61
  }, avatar.name);
57
- };
62
+ });
58
63
 
59
64
  // eslint-disable-next-line @repo/internal/react/require-jsdoc
60
65
  export default AvatarGroupItem;
@@ -13,6 +13,7 @@ import { layers } from '@atlaskit/theme/constants';
13
13
  import Tooltip from '@atlaskit/tooltip';
14
14
  import AvatarGroupItem from './avatar-group-item';
15
15
  import Grid from './grid';
16
+ import FocusManager from './internal/components/focus-manager';
16
17
  import MoreIndicator from './more-indicator';
17
18
  import Stack from './stack';
18
19
  import { composeUniqueKey } from './utils';
@@ -116,7 +117,7 @@ var AvatarGroup = function AvatarGroup(_ref) {
116
117
  shouldFlip: true,
117
118
  zIndex: layers.modal(),
118
119
  content: function content() {
119
- return jsx(PopupMenuGroup, {
120
+ return jsx(FocusManager, null, jsx(PopupMenuGroup, {
120
121
  onClick: function onClick(e) {
121
122
  return e.stopPropagation();
122
123
  },
@@ -132,7 +133,7 @@ var AvatarGroup = function AvatarGroup(_ref) {
132
133
  // This index holds the true index,
133
134
  // adding up the index of non-overflowed avatars and overflowed avatars.
134
135
  index + max);
135
- })));
136
+ }))));
136
137
  },
137
138
  trigger: function trigger(triggerProps) {
138
139
  return renderMoreButton(_objectSpread(_objectSpread({}, triggerProps), {}, {
@@ -0,0 +1,53 @@
1
+ import React, { createContext, useCallback, useEffect, useRef } from 'react';
2
+ import { bind } from 'bind-event-listener';
3
+ import __noop from '@atlaskit/ds-lib/noop';
4
+ import handleFocus from '../utiles/handle-focus';
5
+
6
+ /**
7
+ *
8
+ *
9
+ * Context provider which maintains the list of focusable elements and a method to
10
+ * register new menu items.
11
+ * This list drives the keyboard navgation of the menu.
12
+ *
13
+ */
14
+ export var FocusManagerContext = /*#__PURE__*/createContext({
15
+ menuItemRefs: [],
16
+ registerRef: __noop
17
+ });
18
+
19
+ /**
20
+ * Focus manager logic
21
+ */
22
+ var FocusManager = function FocusManager(_ref) {
23
+ var children = _ref.children;
24
+ var menuItemRefs = useRef([]);
25
+ var registerRef = useCallback(function (ref) {
26
+ if (ref && !menuItemRefs.current.includes(ref)) {
27
+ menuItemRefs.current.push(ref);
28
+ }
29
+ }, []);
30
+
31
+ // set focus and intentionally rebinding listener and clean up listener on each render
32
+ useEffect(function () {
33
+ bind(window, {
34
+ type: 'keydown',
35
+ listener: handleFocus(menuItemRefs.current)
36
+ });
37
+ var unbind = function unbind() {
38
+ bind(window, {
39
+ type: 'keydown',
40
+ listener: handleFocus(menuItemRefs.current)
41
+ });
42
+ };
43
+ return unbind;
44
+ }, []);
45
+ var contextValue = {
46
+ menuItemRefs: menuItemRefs.current,
47
+ registerRef: registerRef
48
+ };
49
+ return /*#__PURE__*/React.createElement(FocusManagerContext.Provider, {
50
+ value: contextValue
51
+ }, children);
52
+ };
53
+ export default FocusManager;
@@ -0,0 +1,17 @@
1
+ import { useContext, useEffect, useRef } from 'react';
2
+ import { FocusManagerContext } from '../components/focus-manager';
3
+
4
+ // The refs stored in the context are used to programatically
5
+ // control focus on a user navigates using the keyboard.
6
+ function useRegisterItemWithFocusManager() {
7
+ var _useContext = useContext(FocusManagerContext),
8
+ registerRef = _useContext.registerRef;
9
+ var itemRef = useRef(null);
10
+ useEffect(function () {
11
+ if (itemRef.current !== null) {
12
+ registerRef(itemRef.current);
13
+ }
14
+ }, [registerRef]);
15
+ return itemRef;
16
+ }
17
+ export default useRegisterItemWithFocusManager;
@@ -0,0 +1,75 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ var _actionMap;
3
+ import { KEY_DOWN, KEY_END, KEY_HOME, KEY_UP } from '@atlaskit/ds-lib/keycodes';
4
+ var actionMap = (_actionMap = {}, _defineProperty(_actionMap, KEY_DOWN, 'next'), _defineProperty(_actionMap, KEY_UP, 'prev'), _defineProperty(_actionMap, KEY_HOME, 'first'), _defineProperty(_actionMap, KEY_END, 'last'), _actionMap);
5
+
6
+ /**
7
+ * currentFocusedIdx + 1 will not work if the next focusable element
8
+ * is disabled. So, we need to iterate through the following menu items
9
+ * to find one that isn't disabled. If all following elements are disabled,
10
+ * return undefined.
11
+ */
12
+ var getNextFocusableElement = function getNextFocusableElement(refs, currentFocusedIdx) {
13
+ while (currentFocusedIdx + 1 < refs.length) {
14
+ var isDisabled = refs[currentFocusedIdx + 1].getAttribute('disabled') !== null;
15
+ if (!isDisabled) {
16
+ return refs[currentFocusedIdx + 1];
17
+ }
18
+ currentFocusedIdx++;
19
+ }
20
+ };
21
+
22
+ /**
23
+ * currentFocusedIdx - 1 will not work if the prev focusable element
24
+ * is disabled. So, we need to iterate through the previous menu items
25
+ * to find one that isn't disabled. If all previous elements are disabled,
26
+ * return undefined.
27
+ */
28
+ var getPrevFocusableElement = function getPrevFocusableElement(refs, currentFocusedIdx) {
29
+ while (currentFocusedIdx > 0) {
30
+ var isDisabled = refs[currentFocusedIdx - 1].getAttribute('disabled') !== null;
31
+ if (!isDisabled) {
32
+ return refs[currentFocusedIdx - 1];
33
+ }
34
+ currentFocusedIdx--;
35
+ }
36
+ };
37
+ export default function handleFocus(refs) {
38
+ return function (e) {
39
+ var currentFocusedIdx = refs.findIndex(function (el) {
40
+ var _document$activeEleme;
41
+ return (_document$activeEleme = document.activeElement) === null || _document$activeEleme === void 0 ? void 0 : _document$activeEleme.isSameNode(el);
42
+ });
43
+ var action = actionMap[e.key];
44
+ switch (action) {
45
+ case 'next':
46
+ if (currentFocusedIdx < refs.length - 1) {
47
+ e.preventDefault();
48
+ var _nextFocusableElement = getNextFocusableElement(refs, currentFocusedIdx);
49
+ _nextFocusableElement && _nextFocusableElement.focus();
50
+ }
51
+ break;
52
+ case 'prev':
53
+ if (currentFocusedIdx > 0) {
54
+ e.preventDefault();
55
+ var _prevFocusableElement = getPrevFocusableElement(refs, currentFocusedIdx);
56
+ _prevFocusableElement && _prevFocusableElement.focus();
57
+ }
58
+ break;
59
+ case 'first':
60
+ e.preventDefault();
61
+ // Search for first non-disabled element if first element is disabled
62
+ var nextFocusableElement = getNextFocusableElement(refs, -1);
63
+ nextFocusableElement && nextFocusableElement.focus();
64
+ break;
65
+ case 'last':
66
+ e.preventDefault();
67
+ // Search for last non-disabled element if last element is disabled
68
+ var prevFocusableElement = getPrevFocusableElement(refs, refs.length);
69
+ prevFocusableElement && prevFocusableElement.focus();
70
+ break;
71
+ default:
72
+ return;
73
+ }
74
+ };
75
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/avatar-group",
3
- "version": "9.3.2",
3
+ "version": "9.3.4",
4
4
  "sideEffects": false
5
5
  }
@@ -1,4 +1,4 @@
1
- import { FC } from 'react';
1
+ import React from 'react';
2
2
  import { AvatarProps, onAvatarClickHandler } from './types';
3
3
  export interface AvatarGroupItemProps {
4
4
  avatar: AvatarProps;
@@ -8,5 +8,5 @@ export interface AvatarGroupItemProps {
8
8
  onAvatarClick?: onAvatarClickHandler;
9
9
  testId?: string;
10
10
  }
11
- declare const AvatarGroupItem: FC<AvatarGroupItemProps>;
11
+ declare const AvatarGroupItem: React.ForwardRefExoticComponent<AvatarGroupItemProps & React.RefAttributes<HTMLElement>>;
12
12
  export default AvatarGroupItem;
@@ -0,0 +1,21 @@
1
+ import React, { FC, ReactNode } from 'react';
2
+ import { FocusableElement } from '../../types';
3
+ /**
4
+ *
5
+ *
6
+ * Context provider which maintains the list of focusable elements and a method to
7
+ * register new menu items.
8
+ * This list drives the keyboard navgation of the menu.
9
+ *
10
+ */
11
+ export declare const FocusManagerContext: React.Context<{
12
+ menuItemRefs: FocusableElement[];
13
+ registerRef: (ref: FocusableElement) => void;
14
+ }>;
15
+ /**
16
+ * Focus manager logic
17
+ */
18
+ declare const FocusManager: FC<{
19
+ children: ReactNode;
20
+ }>;
21
+ export default FocusManager;
@@ -0,0 +1,4 @@
1
+ /// <reference types="react" />
2
+ import { FocusableElement } from '../../types';
3
+ declare function useRegisterItemWithFocusManager(): import("react").RefObject<FocusableElement>;
4
+ export default useRegisterItemWithFocusManager;
@@ -0,0 +1,2 @@
1
+ import { FocusableElement } from '../../types';
2
+ export default function handleFocus(refs: Array<FocusableElement>): (e: KeyboardEvent) => void;
@@ -18,3 +18,5 @@ export interface AvatarGroupOverrides {
18
18
  };
19
19
  }
20
20
  export type onAvatarClickHandler = (event: React.MouseEvent, analyticsEvent: AnalyticsEvent | undefined, index: number) => void;
21
+ export type FocusableElement = HTMLAnchorElement | HTMLButtonElement;
22
+ export type Action = 'next' | 'prev' | 'first' | 'last';
@@ -1,4 +1,4 @@
1
- import { FC } from 'react';
1
+ import React from 'react';
2
2
  import { AvatarProps, onAvatarClickHandler } from './types';
3
3
  export interface AvatarGroupItemProps {
4
4
  avatar: AvatarProps;
@@ -8,5 +8,5 @@ export interface AvatarGroupItemProps {
8
8
  onAvatarClick?: onAvatarClickHandler;
9
9
  testId?: string;
10
10
  }
11
- declare const AvatarGroupItem: FC<AvatarGroupItemProps>;
11
+ declare const AvatarGroupItem: React.ForwardRefExoticComponent<AvatarGroupItemProps & React.RefAttributes<HTMLElement>>;
12
12
  export default AvatarGroupItem;
@@ -0,0 +1,21 @@
1
+ import React, { FC, ReactNode } from 'react';
2
+ import { FocusableElement } from '../../types';
3
+ /**
4
+ *
5
+ *
6
+ * Context provider which maintains the list of focusable elements and a method to
7
+ * register new menu items.
8
+ * This list drives the keyboard navgation of the menu.
9
+ *
10
+ */
11
+ export declare const FocusManagerContext: React.Context<{
12
+ menuItemRefs: FocusableElement[];
13
+ registerRef: (ref: FocusableElement) => void;
14
+ }>;
15
+ /**
16
+ * Focus manager logic
17
+ */
18
+ declare const FocusManager: FC<{
19
+ children: ReactNode;
20
+ }>;
21
+ export default FocusManager;
@@ -0,0 +1,4 @@
1
+ /// <reference types="react" />
2
+ import { FocusableElement } from '../../types';
3
+ declare function useRegisterItemWithFocusManager(): import("react").RefObject<FocusableElement>;
4
+ export default useRegisterItemWithFocusManager;
@@ -0,0 +1,2 @@
1
+ import { FocusableElement } from '../../types';
2
+ export default function handleFocus(refs: Array<FocusableElement>): (e: KeyboardEvent) => void;
@@ -18,3 +18,5 @@ export interface AvatarGroupOverrides {
18
18
  };
19
19
  }
20
20
  export type onAvatarClickHandler = (event: React.MouseEvent, analyticsEvent: AnalyticsEvent | undefined, index: number) => void;
21
+ export type FocusableElement = HTMLAnchorElement | HTMLButtonElement;
22
+ export type Action = 'next' | 'prev' | 'first' | 'last';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/avatar-group",
3
- "version": "9.3.2",
3
+ "version": "9.3.4",
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/"
@@ -35,13 +35,15 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@atlaskit/avatar": "^21.3.0",
38
+ "@atlaskit/ds-lib": "^2.1.0",
38
39
  "@atlaskit/menu": "^1.7.0",
39
40
  "@atlaskit/popup": "^1.6.0",
40
41
  "@atlaskit/theme": "^12.5.0",
41
- "@atlaskit/tokens": "^1.4.0",
42
+ "@atlaskit/tokens": "^1.5.0",
42
43
  "@atlaskit/tooltip": "^17.8.0",
43
44
  "@babel/runtime": "^7.0.0",
44
- "@emotion/react": "^11.7.1"
45
+ "@emotion/react": "^11.7.1",
46
+ "bind-event-listener": "^2.1.1"
45
47
  },
46
48
  "peerDependencies": {
47
49
  "react": "^16.8.0"
@@ -54,7 +56,7 @@
54
56
  "@atlaskit/ds-lib": "^2.2.0",
55
57
  "@atlaskit/form": "^8.11.0",
56
58
  "@atlaskit/icon": "^21.12.0",
57
- "@atlaskit/modal-dialog": "^12.5.0",
59
+ "@atlaskit/modal-dialog": "^12.6.0",
58
60
  "@atlaskit/section-message": "^6.4.0",
59
61
  "@atlaskit/ssr": "*",
60
62
  "@atlaskit/toggle": "^12.6.0",
@@ -63,6 +65,7 @@
63
65
  "@atlassian/atlassian-frontend-prettier-config-1.0.1": "npm:@atlassian/atlassian-frontend-prettier-config@1.0.1",
64
66
  "@emotion/styled": "^11.0.0",
65
67
  "@testing-library/react": "^12.1.5",
68
+ "jest-axe": "^4.0.0",
66
69
  "lodash": "^4.17.21",
67
70
  "react-dom": "^16.8.0",
68
71
  "typescript": "~4.9.5",