@carbon/ibm-products 2.43.2-canary.174 → 2.43.2-canary.177
Sign up to get free protection for your applications and to get access to all the features.
- package/es/components/NotificationsPanel/NotificationsPanel.d.ts +2 -0
- package/es/components/NotificationsPanel/NotificationsPanel.js +75 -5
- package/es/global/js/hooks/useFocus.d.ts +1 -1
- package/es/global/js/hooks/useFocus.js +40 -24
- package/es/global/js/utils/keyboardNavigation.d.ts +27 -0
- package/es/global/js/utils/keyboardNavigation.js +37 -0
- package/es/global/js/utils/wrapFocus.d.ts +25 -0
- package/es/global/js/utils/wrapFocus.js +68 -0
- package/lib/components/NotificationsPanel/NotificationsPanel.d.ts +2 -0
- package/lib/components/NotificationsPanel/NotificationsPanel.js +74 -4
- package/lib/global/js/hooks/useFocus.d.ts +1 -1
- package/lib/global/js/hooks/useFocus.js +40 -24
- package/lib/global/js/utils/keyboardNavigation.d.ts +27 -0
- package/lib/global/js/utils/keyboardNavigation.js +43 -0
- package/lib/global/js/utils/wrapFocus.d.ts +25 -0
- package/lib/global/js/utils/wrapFocus.js +73 -0
- package/package.json +3 -3
@@ -158,6 +158,8 @@ export interface NotificationsPanelProps {
|
|
158
158
|
* Sets the yesterday label text
|
159
159
|
*/
|
160
160
|
yesterdayLabel?: string;
|
161
|
+
/** Specify the CSS selectors that match the floating menus. */
|
162
|
+
selectorsFloatingMenus?: string[];
|
161
163
|
}
|
162
164
|
export declare let NotificationsPanel: React.ForwardRefExoticComponent<NotificationsPanelProps & React.RefAttributes<unknown>>;
|
163
165
|
export {};
|
@@ -6,7 +6,7 @@
|
|
6
6
|
*/
|
7
7
|
|
8
8
|
import { objectWithoutProperties as _objectWithoutProperties, slicedToArray as _slicedToArray, defineProperty as _defineProperty, extends as _extends } from '../../_virtual/_rollupPluginBabelHelpers.js';
|
9
|
-
import { Button, Toggle, Link, IconButton } from '@carbon/react';
|
9
|
+
import { usePrefix, Button, Toggle, Link, IconButton } from '@carbon/react';
|
10
10
|
import { Settings, ErrorFilled, CheckmarkFilled, WarningAltFilled, InformationSquareFilled, Close, ChevronDown } from '@carbon/react/icons';
|
11
11
|
import React__default, { useRef, useState, useEffect } from 'react';
|
12
12
|
import PropTypes from '../../node_modules/prop-types/index.js';
|
@@ -16,12 +16,13 @@ import { pkg } from '../../settings.js';
|
|
16
16
|
import { prepareProps } from '../../global/js/utils/props-helper.js';
|
17
17
|
import { timeAgo } from './utils.js';
|
18
18
|
import usePrefersReducedMotion from '../../global/js/hooks/usePrefersReducedMotion.js';
|
19
|
+
import wrapFocus from '../../global/js/utils/wrapFocus.js';
|
19
20
|
import { usePreviousValue } from '../../global/js/hooks/usePreviousValue.js';
|
20
21
|
import { useClickOutside } from '../../global/js/hooks/useClickOutside.js';
|
21
22
|
import { NotificationsEmptyState } from '../EmptyStates/NotificationsEmptyState/NotificationsEmptyState.js';
|
22
23
|
|
23
24
|
var _Close;
|
24
|
-
var _excluded = ["className", "data", "daysAgoText", "dismissAllLabel", "dismissSingleNotificationIconDescription", "doNotDisturbDefaultToggled", "doNotDisturbLabel", "emptyStateLabel", "hourAgoText", "hoursAgoText", "minuteAgoText", "minutesAgoText", "monthAgoText", "monthsAgoText", "nowText", "onClickOutside", "onDismissAllNotifications", "onDismissSingleNotification", "onDoNotDisturbChange", "onSettingsClick", "onViewAllClick", "open", "previousLabel", "readLessLabel", "readMoreLabel", "secondsAgoText", "settingsIconDescription", "title", "todayLabel", "viewAllLabel", "yearAgoText", "yearsAgoText", "yesterdayAtText", "yesterdayLabel"];
|
25
|
+
var _excluded = ["className", "data", "daysAgoText", "dismissAllLabel", "dismissSingleNotificationIconDescription", "doNotDisturbDefaultToggled", "doNotDisturbLabel", "emptyStateLabel", "hourAgoText", "hoursAgoText", "minuteAgoText", "minutesAgoText", "monthAgoText", "monthsAgoText", "nowText", "onClickOutside", "onDismissAllNotifications", "onDismissSingleNotification", "onDoNotDisturbChange", "onSettingsClick", "onViewAllClick", "open", "previousLabel", "readLessLabel", "readMoreLabel", "secondsAgoText", "settingsIconDescription", "title", "todayLabel", "viewAllLabel", "yearAgoText", "yearsAgoText", "yesterdayAtText", "yesterdayLabel", "selectorsFloatingMenus"];
|
25
26
|
|
26
27
|
// The block part of our conventional BEM class names (blockClass__E--M).
|
27
28
|
var componentName = 'NotificationsPanel';
|
@@ -141,8 +142,12 @@ var NotificationsPanel = /*#__PURE__*/React__default.forwardRef(function (_ref,
|
|
141
142
|
yesterdayAtText = _ref$yesterdayAtText === void 0 ? defaults.yesterdayAtText : _ref$yesterdayAtText,
|
142
143
|
_ref$yesterdayLabel = _ref.yesterdayLabel,
|
143
144
|
yesterdayLabel = _ref$yesterdayLabel === void 0 ? defaults.yesterdayLabel : _ref$yesterdayLabel,
|
145
|
+
selectorsFloatingMenus = _ref.selectorsFloatingMenus,
|
144
146
|
rest = _objectWithoutProperties(_ref, _excluded);
|
145
|
-
var notificationPanelRef = useRef();
|
147
|
+
var notificationPanelRef = useRef(null);
|
148
|
+
var notificationPanelInnerRef = useRef(null);
|
149
|
+
var startSentinel = useRef(null);
|
150
|
+
var endSentinel = useRef(null);
|
146
151
|
var _useState = useState(open),
|
147
152
|
_useState2 = _slicedToArray(_useState, 2),
|
148
153
|
shouldRender = _useState2[0],
|
@@ -155,6 +160,7 @@ var NotificationsPanel = /*#__PURE__*/React__default.forwardRef(function (_ref,
|
|
155
160
|
open: open
|
156
161
|
});
|
157
162
|
var reducedMotion = usePrefersReducedMotion();
|
163
|
+
var carbonPrefix = usePrefix();
|
158
164
|
useEffect(function () {
|
159
165
|
// Set the notifications passed to the state within this component
|
160
166
|
setAllNotifications(data);
|
@@ -166,12 +172,56 @@ var NotificationsPanel = /*#__PURE__*/React__default.forwardRef(function (_ref,
|
|
166
172
|
// initialize the notification panel to open
|
167
173
|
if (open) {
|
168
174
|
setRender(true);
|
175
|
+
var observer = new MutationObserver(function () {
|
176
|
+
if (notificationPanelRef.current) {
|
177
|
+
var _querySelector;
|
178
|
+
var parentElement = notificationPanelRef.current;
|
179
|
+
parentElement === null || parentElement === void 0 || (_querySelector = parentElement.querySelector(".".concat(blockClass, "__dismiss-button"))) === null || _querySelector === void 0 || _querySelector.focus();
|
180
|
+
observer.disconnect();
|
181
|
+
}
|
182
|
+
});
|
183
|
+
if (notificationPanelRef.current) {
|
184
|
+
var parentElement = notificationPanelRef.current;
|
185
|
+
var button = parentElement === null || parentElement === void 0 ? void 0 : parentElement.querySelector(".".concat(blockClass, "__dismiss-button"));
|
186
|
+
button === null || button === void 0 || button.focus();
|
187
|
+
} else {
|
188
|
+
observer.observe(document.body, {
|
189
|
+
childList: true,
|
190
|
+
subtree: true
|
191
|
+
});
|
192
|
+
}
|
193
|
+
return function () {
|
194
|
+
return observer.disconnect();
|
195
|
+
};
|
169
196
|
}
|
170
197
|
}, [open]);
|
171
198
|
var onAnimationEnd = function onAnimationEnd() {
|
172
199
|
// initialize the notification panel to close
|
173
200
|
!open && setRender(false);
|
174
201
|
};
|
202
|
+
var handleBlur = function handleBlur(_ref2) {
|
203
|
+
var oldActiveNode = _ref2.target,
|
204
|
+
currentActiveNode = _ref2.relatedTarget;
|
205
|
+
if (open && currentActiveNode && oldActiveNode && notificationPanelInnerRef.current) {
|
206
|
+
var bodyNode = notificationPanelInnerRef.current;
|
207
|
+
var startSentinelNode = startSentinel.current;
|
208
|
+
var endSentinelNode = endSentinel.current;
|
209
|
+
wrapFocus({
|
210
|
+
bodyNode: bodyNode,
|
211
|
+
startTrapNode: startSentinelNode,
|
212
|
+
endTrapNode: endSentinelNode,
|
213
|
+
currentActiveNode: currentActiveNode,
|
214
|
+
oldActiveNode: oldActiveNode,
|
215
|
+
selectorsFloatingMenus: selectorsFloatingMenus === null || selectorsFloatingMenus === void 0 ? void 0 : selectorsFloatingMenus.filter(Boolean)
|
216
|
+
});
|
217
|
+
}
|
218
|
+
};
|
219
|
+
var handleKeydown = function handleKeydown(event) {
|
220
|
+
event.stopPropagation();
|
221
|
+
if (event.key === 'Escape') {
|
222
|
+
onClickOutside();
|
223
|
+
}
|
224
|
+
};
|
175
225
|
useEffect(function () {
|
176
226
|
if (!open && previousState !== null && previousState !== void 0 && previousState.open && reducedMotion) {
|
177
227
|
setRender(false);
|
@@ -310,7 +360,17 @@ var NotificationsPanel = /*#__PURE__*/React__default.forwardRef(function (_ref,
|
|
310
360
|
onDismissSingleNotification(notification);
|
311
361
|
};
|
312
362
|
var mainSectionClassName = cx(["".concat(blockClass, "__main-section"), _defineProperty({}, "".concat(blockClass, "__main-section-empty"), allNotifications && !allNotifications.length)]);
|
313
|
-
return shouldRender ? /*#__PURE__*/React__default.createElement(
|
363
|
+
return shouldRender ? /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("button", {
|
364
|
+
type: "button",
|
365
|
+
className: "".concat(carbonPrefix, "--visually-hidden"),
|
366
|
+
ref: startSentinel
|
367
|
+
}, "Focus sentinel start"), /*#__PURE__*/React__default.createElement("div", _extends({
|
368
|
+
role: "dialog",
|
369
|
+
"aria-label": "Notification Panel",
|
370
|
+
onBlur: handleBlur,
|
371
|
+
tabIndex: 0,
|
372
|
+
onKeyDown: handleKeydown
|
373
|
+
}, rest, {
|
314
374
|
id: blockClass,
|
315
375
|
className: cx(blockClass, className, "".concat(blockClass, "__container")),
|
316
376
|
style: {
|
@@ -319,6 +379,8 @@ var NotificationsPanel = /*#__PURE__*/React__default.forwardRef(function (_ref,
|
|
319
379
|
onAnimationEnd: onAnimationEnd,
|
320
380
|
ref: ref || notificationPanelRef
|
321
381
|
}, getDevtoolsProps(componentName)), /*#__PURE__*/React__default.createElement("div", {
|
382
|
+
ref: notificationPanelInnerRef
|
383
|
+
}, /*#__PURE__*/React__default.createElement("div", {
|
322
384
|
className: "".concat(blockClass, "__header-container")
|
323
385
|
}, /*#__PURE__*/React__default.createElement("div", {
|
324
386
|
className: "".concat(blockClass, "__header-flex")
|
@@ -382,7 +444,11 @@ var NotificationsPanel = /*#__PURE__*/React__default.forwardRef(function (_ref,
|
|
382
444
|
onClick: function onClick() {
|
383
445
|
return onSettingsClick();
|
384
446
|
}
|
385
|
-
})) : null)
|
447
|
+
})) : null)), /*#__PURE__*/React__default.createElement("button", {
|
448
|
+
type: "button",
|
449
|
+
className: "".concat(carbonPrefix, "--visually-hidden"),
|
450
|
+
ref: endSentinel
|
451
|
+
}, "Focus sentinel end")) : null;
|
386
452
|
});
|
387
453
|
|
388
454
|
// Return a placeholder if not released and not enabled by feature flag
|
@@ -513,6 +579,10 @@ NotificationsPanel.propTypes = {
|
|
513
579
|
* Sets the `seconds ago` label text
|
514
580
|
*/
|
515
581
|
secondsAgoText: PropTypes.func,
|
582
|
+
/**
|
583
|
+
* Specify the CSS selectors that match the floating menus
|
584
|
+
*/
|
585
|
+
selectorsFloatingMenus: PropTypes.arrayOf(PropTypes.string.isRequired),
|
516
586
|
/**
|
517
587
|
* Sets the settings icon description text
|
518
588
|
*/
|
@@ -4,7 +4,7 @@ export function useFocus(modalRef: any, selectorPrimaryFocus: any): {
|
|
4
4
|
lastElement: any;
|
5
5
|
allElements: any;
|
6
6
|
specifiedElement: any;
|
7
|
-
keyDownListener: (event: any) => void
|
7
|
+
keyDownListener: (event: any) => Promise<void>;
|
8
8
|
getFocusable: () => {
|
9
9
|
first: any;
|
10
10
|
last: any;
|
@@ -5,9 +5,11 @@
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
6
6
|
*/
|
7
7
|
|
8
|
+
import { asyncToGenerator as _asyncToGenerator, regeneratorRuntime as _regeneratorRuntime } from '../../../_virtual/_rollupPluginBabelHelpers.js';
|
8
9
|
import { usePrefix } from '@carbon/react';
|
9
10
|
import { pkg } from '../../../settings.js';
|
10
11
|
import { useCallback, useEffect } from 'react';
|
12
|
+
import wait from '../utils/wait.js';
|
11
13
|
|
12
14
|
var getSpecificElement = function getSpecificElement(parentEl, elementId) {
|
13
15
|
var element = parentEl === null || parentEl === void 0 ? void 0 : parentEl.querySelector(elementId);
|
@@ -16,6 +18,7 @@ var getSpecificElement = function getSpecificElement(parentEl, elementId) {
|
|
16
18
|
var useFocus = function useFocus(modalRef, selectorPrimaryFocus) {
|
17
19
|
var carbonPrefix = usePrefix();
|
18
20
|
var tearsheetBaseClass = "".concat(pkg.prefix, "--tearsheet");
|
21
|
+
var sidePanelBaseClass = "".concat(pkg.prefix, "--side-panel");
|
19
22
|
// Querying focusable element in the modal
|
20
23
|
// Query to exclude hidden elements in the modal from querySelectorAll() method
|
21
24
|
// feel free to include more if needed :)
|
@@ -27,8 +30,9 @@ var useFocus = function useFocus(modalRef, selectorPrimaryFocus) {
|
|
27
30
|
var queryTextarea = "textarea".concat(notQuery);
|
28
31
|
var queryLink = "[href]".concat(notQuery);
|
29
32
|
var queryTabIndex = "[tabindex=\"0\"]".concat(notQuery);
|
33
|
+
var querySidePanelScroll = ".".concat(sidePanelBaseClass, "--scrolls");
|
30
34
|
// Final query
|
31
|
-
var query = "".concat(queryButton, ",").concat(queryLink, ",").concat(queryInput, ",").concat(querySelect, ",").concat(queryTextarea, ",
|
35
|
+
var query = "".concat(queryButton, ",").concat(queryLink, ",").concat(queryInput, ",").concat(querySelect, ",").concat(queryTextarea, ",").concat(queryTabIndex, ",").concat(querySidePanelScroll);
|
32
36
|
var modalEl = modalRef === null || modalRef === void 0 ? void 0 : modalRef.current;
|
33
37
|
var getFocusable = useCallback(function () {
|
34
38
|
var _focusableElements, _focusableElements2, _focusableElements3, _focusableElements4;
|
@@ -54,30 +58,42 @@ var useFocus = function useFocus(modalRef, selectorPrimaryFocus) {
|
|
54
58
|
useEffect(function () {
|
55
59
|
getFocusable();
|
56
60
|
}, [getFocusable]);
|
57
|
-
var handleKeyDown = function
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
61
|
+
var handleKeyDown = /*#__PURE__*/function () {
|
62
|
+
var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee(event) {
|
63
|
+
var _document, _document2, _getFocusable, first, last, all;
|
64
|
+
return _regeneratorRuntime().wrap(function _callee$(_context) {
|
65
|
+
while (1) switch (_context.prev = _context.next) {
|
66
|
+
case 0:
|
67
|
+
if (!(event.key === 'Tab')) {
|
68
|
+
_context.next = 5;
|
69
|
+
break;
|
70
|
+
}
|
71
|
+
// updating the focusable elements list
|
72
|
+
_getFocusable = getFocusable(), first = _getFocusable.first, last = _getFocusable.last, all = _getFocusable.all;
|
73
|
+
_context.next = 4;
|
74
|
+
return wait(1);
|
75
|
+
case 4:
|
76
|
+
if (event.shiftKey && !Array.prototype.includes.call(all, (_document = document) === null || _document === void 0 ? void 0 : _document.activeElement)) {
|
77
|
+
// Prevents the default "Tab" behavior
|
78
|
+
event.preventDefault();
|
79
|
+
// if the user press shift+tab and the current element not in focusable items
|
80
|
+
last === null || last === void 0 || last.focus();
|
81
|
+
} else if (!Array.prototype.includes.call(all, (_document2 = document) === null || _document2 === void 0 ? void 0 : _document2.activeElement)) {
|
82
|
+
event.preventDefault();
|
83
|
+
// user pressing tab key only then
|
84
|
+
// focusing the first element if the current element is not in focusable items
|
85
|
+
first === null || first === void 0 || first.focus();
|
86
|
+
}
|
87
|
+
case 5:
|
88
|
+
case "end":
|
89
|
+
return _context.stop();
|
77
90
|
}
|
78
|
-
},
|
79
|
-
}
|
80
|
-
|
91
|
+
}, _callee);
|
92
|
+
}));
|
93
|
+
return function handleKeyDown(_x) {
|
94
|
+
return _ref.apply(this, arguments);
|
95
|
+
};
|
96
|
+
}();
|
81
97
|
return {
|
82
98
|
firstElement: getFocusable().first,
|
83
99
|
lastElement: getFocusable().last,
|
@@ -0,0 +1,27 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright IBM Corp. 2016, 2018
|
3
|
+
*
|
4
|
+
* This source code is licensed under the Apache-2.0 license found in the
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
6
|
+
*/
|
7
|
+
/**
|
8
|
+
* Various utilities to help with a11y work
|
9
|
+
*/
|
10
|
+
/**
|
11
|
+
* A flag `node.compareDocumentPosition(target)` returns,
|
12
|
+
* that indicates `target` is located earlier than `node` in the document or `target` contains `node`.
|
13
|
+
*/
|
14
|
+
export const DOCUMENT_POSITION_BROAD_PRECEDING: number | false;
|
15
|
+
/**
|
16
|
+
* A flag `node.compareDocumentPosition(target)` returns,
|
17
|
+
* that indicates `target` is located later than `node` in the document or `node` contains `target`.
|
18
|
+
*/
|
19
|
+
export const DOCUMENT_POSITION_BROAD_FOLLOWING: number | false;
|
20
|
+
/**
|
21
|
+
* CSS selector that selects major nodes that are sequential-focusable.
|
22
|
+
*/
|
23
|
+
export const selectorTabbable: "\n a[href], area[href], input:not([disabled]):not([tabindex='-1']),\n button:not([disabled]):not([tabindex='-1']),select:not([disabled]):not([tabindex='-1']),\n textarea:not([disabled]):not([tabindex='-1']),\n iframe, object, embed, *[tabindex]:not([tabindex='-1']):not([disabled]), *[contenteditable=true]\n";
|
24
|
+
/**
|
25
|
+
* CSS selector that selects major nodes that are click focusable
|
26
|
+
*/
|
27
|
+
export const selectorFocusable: "\n a[href], area[href], input:not([disabled]),\n button:not([disabled]),select:not([disabled]),\n textarea:not([disabled]),\n iframe, object, embed, *[tabindex]:not([disabled]), *[contenteditable=true]\n";
|
@@ -0,0 +1,37 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright IBM Corp. 2020, 2024
|
3
|
+
*
|
4
|
+
* This source code is licensed under the Apache-2.0 license found in the
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
6
|
+
*/
|
7
|
+
|
8
|
+
/**
|
9
|
+
* Various utilities to help with a11y work
|
10
|
+
*/
|
11
|
+
|
12
|
+
/**
|
13
|
+
* A flag `node.compareDocumentPosition(target)` returns,
|
14
|
+
* that indicates `target` is located earlier than `node` in the document or `target` contains `node`.
|
15
|
+
*/
|
16
|
+
var DOCUMENT_POSITION_BROAD_PRECEDING =
|
17
|
+
// Checks `typeof Node` for `react-docgen`
|
18
|
+
typeof Node !== 'undefined' &&
|
19
|
+
// eslint-disable-next-line ssr-friendly/no-dom-globals-in-module-scope
|
20
|
+
Node.DOCUMENT_POSITION_PRECEDING | Node.DOCUMENT_POSITION_CONTAINS;
|
21
|
+
|
22
|
+
/**
|
23
|
+
* A flag `node.compareDocumentPosition(target)` returns,
|
24
|
+
* that indicates `target` is located later than `node` in the document or `node` contains `target`.
|
25
|
+
*/
|
26
|
+
var DOCUMENT_POSITION_BROAD_FOLLOWING =
|
27
|
+
// Checks `typeof Node` for `react-docgen`
|
28
|
+
typeof Node !== 'undefined' &&
|
29
|
+
// eslint-disable-next-line ssr-friendly/no-dom-globals-in-module-scope
|
30
|
+
Node.DOCUMENT_POSITION_FOLLOWING | Node.DOCUMENT_POSITION_CONTAINED_BY;
|
31
|
+
|
32
|
+
/**
|
33
|
+
* CSS selector that selects major nodes that are sequential-focusable.
|
34
|
+
*/
|
35
|
+
var selectorTabbable = "\n a[href], area[href], input:not([disabled]):not([tabindex='-1']),\n button:not([disabled]):not([tabindex='-1']),select:not([disabled]):not([tabindex='-1']),\n textarea:not([disabled]):not([tabindex='-1']),\n iframe, object, embed, *[tabindex]:not([tabindex='-1']):not([disabled]), *[contenteditable=true]\n";
|
36
|
+
|
37
|
+
export { DOCUMENT_POSITION_BROAD_FOLLOWING, DOCUMENT_POSITION_BROAD_PRECEDING, selectorTabbable };
|
@@ -0,0 +1,25 @@
|
|
1
|
+
export default wrapFocus;
|
2
|
+
/**
|
3
|
+
* @param {Node} node A DOM node.
|
4
|
+
* @param {string[]} selectorsFloatingMenus The CSS selectors that matches floating menus.
|
5
|
+
* @returns {boolean} `true` of the given `node` is in a floating menu.
|
6
|
+
*/
|
7
|
+
export function elementOrParentIsFloatingMenu(node: Node, selectorsFloatingMenus?: string[]): boolean;
|
8
|
+
/**
|
9
|
+
* Ensures the focus is kept in the given `modalNode`, implementing "focus-wrap" behavior.
|
10
|
+
* @param {object} options The options.
|
11
|
+
* @param {Node|null} options.bodyNode The DOM node of the inner modal.
|
12
|
+
* @param {Node|null} options.startTrapNode The DOM node of the focus sentinel the is placed earlier next to `modalNode`.
|
13
|
+
* @param {Node|null} options.endTrapNode The DOM node of the focus sentinel the is placed next to `modalNode`.
|
14
|
+
* @param {Node} options.currentActiveNode The DOM node that has focus.
|
15
|
+
* @param {Node} options.oldActiveNode The DOM node that previously had focus.
|
16
|
+
* @param {string[]} [options.selectorsFloatingMenus] The CSS selectors that matches floating menus
|
17
|
+
*/
|
18
|
+
declare function wrapFocus({ bodyNode, startTrapNode, endTrapNode, currentActiveNode, oldActiveNode, selectorsFloatingMenus, }: {
|
19
|
+
bodyNode: Node | null;
|
20
|
+
startTrapNode: Node | null;
|
21
|
+
endTrapNode: Node | null;
|
22
|
+
currentActiveNode: Node;
|
23
|
+
oldActiveNode: Node;
|
24
|
+
selectorsFloatingMenus?: string[] | undefined;
|
25
|
+
}): void;
|
@@ -0,0 +1,68 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright IBM Corp. 2020, 2024
|
3
|
+
*
|
4
|
+
* This source code is licensed under the Apache-2.0 license found in the
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
6
|
+
*/
|
7
|
+
|
8
|
+
import { DOCUMENT_POSITION_BROAD_PRECEDING, selectorTabbable, DOCUMENT_POSITION_BROAD_FOLLOWING } from './keyboardNavigation.js';
|
9
|
+
import { carbon } from '../../../settings.js';
|
10
|
+
|
11
|
+
/**
|
12
|
+
* @param {Node} node A DOM node.
|
13
|
+
* @param {string[]} selectorsFloatingMenus The CSS selectors that matches floating menus.
|
14
|
+
* @returns {boolean} `true` of the given `node` is in a floating menu.
|
15
|
+
*/
|
16
|
+
function elementOrParentIsFloatingMenu(node) {
|
17
|
+
var selectorsFloatingMenus = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [".".concat(carbon.prefix, "--overflow-menu-options"), ".".concat(carbon.prefix, "--tooltip"), '.flatpickr-calendar'];
|
18
|
+
if (node && typeof node.closest === 'function') {
|
19
|
+
return selectorsFloatingMenus.some(function (selector) {
|
20
|
+
return node.closest(selector);
|
21
|
+
});
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
/**
|
26
|
+
* Ensures the focus is kept in the given `modalNode`, implementing "focus-wrap" behavior.
|
27
|
+
* @param {object} options The options.
|
28
|
+
* @param {Node|null} options.bodyNode The DOM node of the inner modal.
|
29
|
+
* @param {Node|null} options.startTrapNode The DOM node of the focus sentinel the is placed earlier next to `modalNode`.
|
30
|
+
* @param {Node|null} options.endTrapNode The DOM node of the focus sentinel the is placed next to `modalNode`.
|
31
|
+
* @param {Node} options.currentActiveNode The DOM node that has focus.
|
32
|
+
* @param {Node} options.oldActiveNode The DOM node that previously had focus.
|
33
|
+
* @param {string[]} [options.selectorsFloatingMenus] The CSS selectors that matches floating menus
|
34
|
+
*/
|
35
|
+
function wrapFocus(_ref) {
|
36
|
+
var bodyNode = _ref.bodyNode,
|
37
|
+
startTrapNode = _ref.startTrapNode,
|
38
|
+
endTrapNode = _ref.endTrapNode,
|
39
|
+
currentActiveNode = _ref.currentActiveNode,
|
40
|
+
oldActiveNode = _ref.oldActiveNode,
|
41
|
+
selectorsFloatingMenus = _ref.selectorsFloatingMenus;
|
42
|
+
if (bodyNode && currentActiveNode && oldActiveNode && !bodyNode.contains(currentActiveNode) && !elementOrParentIsFloatingMenu(currentActiveNode, selectorsFloatingMenus)) {
|
43
|
+
var comparisonResult = oldActiveNode.compareDocumentPosition(currentActiveNode);
|
44
|
+
if (currentActiveNode === startTrapNode || comparisonResult & DOCUMENT_POSITION_BROAD_PRECEDING) {
|
45
|
+
var arrayNodes = Array.from(bodyNode.querySelectorAll(selectorTabbable));
|
46
|
+
arrayNodes.reverse();
|
47
|
+
var tabbable = arrayNodes.find(function (elem) {
|
48
|
+
return Boolean(elem.offsetParent);
|
49
|
+
});
|
50
|
+
if (tabbable) {
|
51
|
+
tabbable.focus();
|
52
|
+
} else if (bodyNode !== oldActiveNode) {
|
53
|
+
bodyNode.focus();
|
54
|
+
}
|
55
|
+
} else if (currentActiveNode === endTrapNode || comparisonResult & DOCUMENT_POSITION_BROAD_FOLLOWING) {
|
56
|
+
var _tabbable = Array.prototype.find.call(bodyNode.querySelectorAll(selectorTabbable), function (elem) {
|
57
|
+
return Boolean(elem.offsetParent);
|
58
|
+
});
|
59
|
+
if (_tabbable) {
|
60
|
+
_tabbable.focus();
|
61
|
+
} else if (bodyNode !== oldActiveNode) {
|
62
|
+
bodyNode.focus();
|
63
|
+
}
|
64
|
+
}
|
65
|
+
}
|
66
|
+
}
|
67
|
+
|
68
|
+
export { wrapFocus as default, elementOrParentIsFloatingMenu };
|
@@ -158,6 +158,8 @@ export interface NotificationsPanelProps {
|
|
158
158
|
* Sets the yesterday label text
|
159
159
|
*/
|
160
160
|
yesterdayLabel?: string;
|
161
|
+
/** Specify the CSS selectors that match the floating menus. */
|
162
|
+
selectorsFloatingMenus?: string[];
|
161
163
|
}
|
162
164
|
export declare let NotificationsPanel: React.ForwardRefExoticComponent<NotificationsPanelProps & React.RefAttributes<unknown>>;
|
163
165
|
export {};
|
@@ -20,6 +20,7 @@ var settings = require('../../settings.js');
|
|
20
20
|
var propsHelper = require('../../global/js/utils/props-helper.js');
|
21
21
|
var utils = require('./utils.js');
|
22
22
|
var usePrefersReducedMotion = require('../../global/js/hooks/usePrefersReducedMotion.js');
|
23
|
+
var wrapFocus = require('../../global/js/utils/wrapFocus.js');
|
23
24
|
var usePreviousValue = require('../../global/js/hooks/usePreviousValue.js');
|
24
25
|
var useClickOutside = require('../../global/js/hooks/useClickOutside.js');
|
25
26
|
var NotificationsEmptyState = require('../EmptyStates/NotificationsEmptyState/NotificationsEmptyState.js');
|
@@ -30,7 +31,7 @@ var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
|
|
30
31
|
var cx__default = /*#__PURE__*/_interopDefaultLegacy(cx);
|
31
32
|
|
32
33
|
var _Close;
|
33
|
-
var _excluded = ["className", "data", "daysAgoText", "dismissAllLabel", "dismissSingleNotificationIconDescription", "doNotDisturbDefaultToggled", "doNotDisturbLabel", "emptyStateLabel", "hourAgoText", "hoursAgoText", "minuteAgoText", "minutesAgoText", "monthAgoText", "monthsAgoText", "nowText", "onClickOutside", "onDismissAllNotifications", "onDismissSingleNotification", "onDoNotDisturbChange", "onSettingsClick", "onViewAllClick", "open", "previousLabel", "readLessLabel", "readMoreLabel", "secondsAgoText", "settingsIconDescription", "title", "todayLabel", "viewAllLabel", "yearAgoText", "yearsAgoText", "yesterdayAtText", "yesterdayLabel"];
|
34
|
+
var _excluded = ["className", "data", "daysAgoText", "dismissAllLabel", "dismissSingleNotificationIconDescription", "doNotDisturbDefaultToggled", "doNotDisturbLabel", "emptyStateLabel", "hourAgoText", "hoursAgoText", "minuteAgoText", "minutesAgoText", "monthAgoText", "monthsAgoText", "nowText", "onClickOutside", "onDismissAllNotifications", "onDismissSingleNotification", "onDoNotDisturbChange", "onSettingsClick", "onViewAllClick", "open", "previousLabel", "readLessLabel", "readMoreLabel", "secondsAgoText", "settingsIconDescription", "title", "todayLabel", "viewAllLabel", "yearAgoText", "yearsAgoText", "yesterdayAtText", "yesterdayLabel", "selectorsFloatingMenus"];
|
34
35
|
|
35
36
|
// The block part of our conventional BEM class names (blockClass__E--M).
|
36
37
|
var componentName = 'NotificationsPanel';
|
@@ -150,8 +151,12 @@ exports.NotificationsPanel = /*#__PURE__*/React__default["default"].forwardRef(f
|
|
150
151
|
yesterdayAtText = _ref$yesterdayAtText === void 0 ? defaults.yesterdayAtText : _ref$yesterdayAtText,
|
151
152
|
_ref$yesterdayLabel = _ref.yesterdayLabel,
|
152
153
|
yesterdayLabel = _ref$yesterdayLabel === void 0 ? defaults.yesterdayLabel : _ref$yesterdayLabel,
|
154
|
+
selectorsFloatingMenus = _ref.selectorsFloatingMenus,
|
153
155
|
rest = _rollupPluginBabelHelpers.objectWithoutProperties(_ref, _excluded);
|
154
|
-
var notificationPanelRef = React.useRef();
|
156
|
+
var notificationPanelRef = React.useRef(null);
|
157
|
+
var notificationPanelInnerRef = React.useRef(null);
|
158
|
+
var startSentinel = React.useRef(null);
|
159
|
+
var endSentinel = React.useRef(null);
|
155
160
|
var _useState = React.useState(open),
|
156
161
|
_useState2 = _rollupPluginBabelHelpers.slicedToArray(_useState, 2),
|
157
162
|
shouldRender = _useState2[0],
|
@@ -164,6 +169,7 @@ exports.NotificationsPanel = /*#__PURE__*/React__default["default"].forwardRef(f
|
|
164
169
|
open: open
|
165
170
|
});
|
166
171
|
var reducedMotion = usePrefersReducedMotion["default"]();
|
172
|
+
var carbonPrefix = react.usePrefix();
|
167
173
|
React.useEffect(function () {
|
168
174
|
// Set the notifications passed to the state within this component
|
169
175
|
setAllNotifications(data);
|
@@ -175,12 +181,56 @@ exports.NotificationsPanel = /*#__PURE__*/React__default["default"].forwardRef(f
|
|
175
181
|
// initialize the notification panel to open
|
176
182
|
if (open) {
|
177
183
|
setRender(true);
|
184
|
+
var observer = new MutationObserver(function () {
|
185
|
+
if (notificationPanelRef.current) {
|
186
|
+
var _querySelector;
|
187
|
+
var parentElement = notificationPanelRef.current;
|
188
|
+
parentElement === null || parentElement === void 0 || (_querySelector = parentElement.querySelector(".".concat(blockClass, "__dismiss-button"))) === null || _querySelector === void 0 || _querySelector.focus();
|
189
|
+
observer.disconnect();
|
190
|
+
}
|
191
|
+
});
|
192
|
+
if (notificationPanelRef.current) {
|
193
|
+
var parentElement = notificationPanelRef.current;
|
194
|
+
var button = parentElement === null || parentElement === void 0 ? void 0 : parentElement.querySelector(".".concat(blockClass, "__dismiss-button"));
|
195
|
+
button === null || button === void 0 || button.focus();
|
196
|
+
} else {
|
197
|
+
observer.observe(document.body, {
|
198
|
+
childList: true,
|
199
|
+
subtree: true
|
200
|
+
});
|
201
|
+
}
|
202
|
+
return function () {
|
203
|
+
return observer.disconnect();
|
204
|
+
};
|
178
205
|
}
|
179
206
|
}, [open]);
|
180
207
|
var onAnimationEnd = function onAnimationEnd() {
|
181
208
|
// initialize the notification panel to close
|
182
209
|
!open && setRender(false);
|
183
210
|
};
|
211
|
+
var handleBlur = function handleBlur(_ref2) {
|
212
|
+
var oldActiveNode = _ref2.target,
|
213
|
+
currentActiveNode = _ref2.relatedTarget;
|
214
|
+
if (open && currentActiveNode && oldActiveNode && notificationPanelInnerRef.current) {
|
215
|
+
var bodyNode = notificationPanelInnerRef.current;
|
216
|
+
var startSentinelNode = startSentinel.current;
|
217
|
+
var endSentinelNode = endSentinel.current;
|
218
|
+
wrapFocus["default"]({
|
219
|
+
bodyNode: bodyNode,
|
220
|
+
startTrapNode: startSentinelNode,
|
221
|
+
endTrapNode: endSentinelNode,
|
222
|
+
currentActiveNode: currentActiveNode,
|
223
|
+
oldActiveNode: oldActiveNode,
|
224
|
+
selectorsFloatingMenus: selectorsFloatingMenus === null || selectorsFloatingMenus === void 0 ? void 0 : selectorsFloatingMenus.filter(Boolean)
|
225
|
+
});
|
226
|
+
}
|
227
|
+
};
|
228
|
+
var handleKeydown = function handleKeydown(event) {
|
229
|
+
event.stopPropagation();
|
230
|
+
if (event.key === 'Escape') {
|
231
|
+
onClickOutside();
|
232
|
+
}
|
233
|
+
};
|
184
234
|
React.useEffect(function () {
|
185
235
|
if (!open && previousState !== null && previousState !== void 0 && previousState.open && reducedMotion) {
|
186
236
|
setRender(false);
|
@@ -319,7 +369,17 @@ exports.NotificationsPanel = /*#__PURE__*/React__default["default"].forwardRef(f
|
|
319
369
|
onDismissSingleNotification(notification);
|
320
370
|
};
|
321
371
|
var mainSectionClassName = cx__default["default"](["".concat(blockClass, "__main-section"), _rollupPluginBabelHelpers.defineProperty({}, "".concat(blockClass, "__main-section-empty"), allNotifications && !allNotifications.length)]);
|
322
|
-
return shouldRender ? /*#__PURE__*/React__default["default"].createElement("
|
372
|
+
return shouldRender ? /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, null, /*#__PURE__*/React__default["default"].createElement("button", {
|
373
|
+
type: "button",
|
374
|
+
className: "".concat(carbonPrefix, "--visually-hidden"),
|
375
|
+
ref: startSentinel
|
376
|
+
}, "Focus sentinel start"), /*#__PURE__*/React__default["default"].createElement("div", _rollupPluginBabelHelpers["extends"]({
|
377
|
+
role: "dialog",
|
378
|
+
"aria-label": "Notification Panel",
|
379
|
+
onBlur: handleBlur,
|
380
|
+
tabIndex: 0,
|
381
|
+
onKeyDown: handleKeydown
|
382
|
+
}, rest, {
|
323
383
|
id: blockClass,
|
324
384
|
className: cx__default["default"](blockClass, className, "".concat(blockClass, "__container")),
|
325
385
|
style: {
|
@@ -328,6 +388,8 @@ exports.NotificationsPanel = /*#__PURE__*/React__default["default"].forwardRef(f
|
|
328
388
|
onAnimationEnd: onAnimationEnd,
|
329
389
|
ref: ref || notificationPanelRef
|
330
390
|
}, devtools.getDevtoolsProps(componentName)), /*#__PURE__*/React__default["default"].createElement("div", {
|
391
|
+
ref: notificationPanelInnerRef
|
392
|
+
}, /*#__PURE__*/React__default["default"].createElement("div", {
|
331
393
|
className: "".concat(blockClass, "__header-container")
|
332
394
|
}, /*#__PURE__*/React__default["default"].createElement("div", {
|
333
395
|
className: "".concat(blockClass, "__header-flex")
|
@@ -391,7 +453,11 @@ exports.NotificationsPanel = /*#__PURE__*/React__default["default"].forwardRef(f
|
|
391
453
|
onClick: function onClick() {
|
392
454
|
return onSettingsClick();
|
393
455
|
}
|
394
|
-
})) : null)
|
456
|
+
})) : null)), /*#__PURE__*/React__default["default"].createElement("button", {
|
457
|
+
type: "button",
|
458
|
+
className: "".concat(carbonPrefix, "--visually-hidden"),
|
459
|
+
ref: endSentinel
|
460
|
+
}, "Focus sentinel end")) : null;
|
395
461
|
});
|
396
462
|
|
397
463
|
// Return a placeholder if not released and not enabled by feature flag
|
@@ -522,6 +588,10 @@ exports.NotificationsPanel.propTypes = {
|
|
522
588
|
* Sets the `seconds ago` label text
|
523
589
|
*/
|
524
590
|
secondsAgoText: index["default"].func,
|
591
|
+
/**
|
592
|
+
* Specify the CSS selectors that match the floating menus
|
593
|
+
*/
|
594
|
+
selectorsFloatingMenus: index["default"].arrayOf(index["default"].string.isRequired),
|
525
595
|
/**
|
526
596
|
* Sets the settings icon description text
|
527
597
|
*/
|
@@ -4,7 +4,7 @@ export function useFocus(modalRef: any, selectorPrimaryFocus: any): {
|
|
4
4
|
lastElement: any;
|
5
5
|
allElements: any;
|
6
6
|
specifiedElement: any;
|
7
|
-
keyDownListener: (event: any) => void
|
7
|
+
keyDownListener: (event: any) => Promise<void>;
|
8
8
|
getFocusable: () => {
|
9
9
|
first: any;
|
10
10
|
last: any;
|
@@ -9,9 +9,11 @@
|
|
9
9
|
|
10
10
|
Object.defineProperty(exports, '__esModule', { value: true });
|
11
11
|
|
12
|
+
var _rollupPluginBabelHelpers = require('../../../_virtual/_rollupPluginBabelHelpers.js');
|
12
13
|
var react = require('@carbon/react');
|
13
14
|
var settings = require('../../../settings.js');
|
14
15
|
var React = require('react');
|
16
|
+
var wait = require('../utils/wait.js');
|
15
17
|
|
16
18
|
var getSpecificElement = function getSpecificElement(parentEl, elementId) {
|
17
19
|
var element = parentEl === null || parentEl === void 0 ? void 0 : parentEl.querySelector(elementId);
|
@@ -20,6 +22,7 @@ var getSpecificElement = function getSpecificElement(parentEl, elementId) {
|
|
20
22
|
var useFocus = function useFocus(modalRef, selectorPrimaryFocus) {
|
21
23
|
var carbonPrefix = react.usePrefix();
|
22
24
|
var tearsheetBaseClass = "".concat(settings.pkg.prefix, "--tearsheet");
|
25
|
+
var sidePanelBaseClass = "".concat(settings.pkg.prefix, "--side-panel");
|
23
26
|
// Querying focusable element in the modal
|
24
27
|
// Query to exclude hidden elements in the modal from querySelectorAll() method
|
25
28
|
// feel free to include more if needed :)
|
@@ -31,8 +34,9 @@ var useFocus = function useFocus(modalRef, selectorPrimaryFocus) {
|
|
31
34
|
var queryTextarea = "textarea".concat(notQuery);
|
32
35
|
var queryLink = "[href]".concat(notQuery);
|
33
36
|
var queryTabIndex = "[tabindex=\"0\"]".concat(notQuery);
|
37
|
+
var querySidePanelScroll = ".".concat(sidePanelBaseClass, "--scrolls");
|
34
38
|
// Final query
|
35
|
-
var query = "".concat(queryButton, ",").concat(queryLink, ",").concat(queryInput, ",").concat(querySelect, ",").concat(queryTextarea, ",
|
39
|
+
var query = "".concat(queryButton, ",").concat(queryLink, ",").concat(queryInput, ",").concat(querySelect, ",").concat(queryTextarea, ",").concat(queryTabIndex, ",").concat(querySidePanelScroll);
|
36
40
|
var modalEl = modalRef === null || modalRef === void 0 ? void 0 : modalRef.current;
|
37
41
|
var getFocusable = React.useCallback(function () {
|
38
42
|
var _focusableElements, _focusableElements2, _focusableElements3, _focusableElements4;
|
@@ -58,30 +62,42 @@ var useFocus = function useFocus(modalRef, selectorPrimaryFocus) {
|
|
58
62
|
React.useEffect(function () {
|
59
63
|
getFocusable();
|
60
64
|
}, [getFocusable]);
|
61
|
-
var handleKeyDown = function
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
65
|
+
var handleKeyDown = /*#__PURE__*/function () {
|
66
|
+
var _ref = _rollupPluginBabelHelpers.asyncToGenerator( /*#__PURE__*/_rollupPluginBabelHelpers.regeneratorRuntime().mark(function _callee(event) {
|
67
|
+
var _document, _document2, _getFocusable, first, last, all;
|
68
|
+
return _rollupPluginBabelHelpers.regeneratorRuntime().wrap(function _callee$(_context) {
|
69
|
+
while (1) switch (_context.prev = _context.next) {
|
70
|
+
case 0:
|
71
|
+
if (!(event.key === 'Tab')) {
|
72
|
+
_context.next = 5;
|
73
|
+
break;
|
74
|
+
}
|
75
|
+
// updating the focusable elements list
|
76
|
+
_getFocusable = getFocusable(), first = _getFocusable.first, last = _getFocusable.last, all = _getFocusable.all;
|
77
|
+
_context.next = 4;
|
78
|
+
return wait["default"](1);
|
79
|
+
case 4:
|
80
|
+
if (event.shiftKey && !Array.prototype.includes.call(all, (_document = document) === null || _document === void 0 ? void 0 : _document.activeElement)) {
|
81
|
+
// Prevents the default "Tab" behavior
|
82
|
+
event.preventDefault();
|
83
|
+
// if the user press shift+tab and the current element not in focusable items
|
84
|
+
last === null || last === void 0 || last.focus();
|
85
|
+
} else if (!Array.prototype.includes.call(all, (_document2 = document) === null || _document2 === void 0 ? void 0 : _document2.activeElement)) {
|
86
|
+
event.preventDefault();
|
87
|
+
// user pressing tab key only then
|
88
|
+
// focusing the first element if the current element is not in focusable items
|
89
|
+
first === null || first === void 0 || first.focus();
|
90
|
+
}
|
91
|
+
case 5:
|
92
|
+
case "end":
|
93
|
+
return _context.stop();
|
81
94
|
}
|
82
|
-
},
|
83
|
-
}
|
84
|
-
|
95
|
+
}, _callee);
|
96
|
+
}));
|
97
|
+
return function handleKeyDown(_x) {
|
98
|
+
return _ref.apply(this, arguments);
|
99
|
+
};
|
100
|
+
}();
|
85
101
|
return {
|
86
102
|
firstElement: getFocusable().first,
|
87
103
|
lastElement: getFocusable().last,
|
@@ -0,0 +1,27 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright IBM Corp. 2016, 2018
|
3
|
+
*
|
4
|
+
* This source code is licensed under the Apache-2.0 license found in the
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
6
|
+
*/
|
7
|
+
/**
|
8
|
+
* Various utilities to help with a11y work
|
9
|
+
*/
|
10
|
+
/**
|
11
|
+
* A flag `node.compareDocumentPosition(target)` returns,
|
12
|
+
* that indicates `target` is located earlier than `node` in the document or `target` contains `node`.
|
13
|
+
*/
|
14
|
+
export const DOCUMENT_POSITION_BROAD_PRECEDING: number | false;
|
15
|
+
/**
|
16
|
+
* A flag `node.compareDocumentPosition(target)` returns,
|
17
|
+
* that indicates `target` is located later than `node` in the document or `node` contains `target`.
|
18
|
+
*/
|
19
|
+
export const DOCUMENT_POSITION_BROAD_FOLLOWING: number | false;
|
20
|
+
/**
|
21
|
+
* CSS selector that selects major nodes that are sequential-focusable.
|
22
|
+
*/
|
23
|
+
export const selectorTabbable: "\n a[href], area[href], input:not([disabled]):not([tabindex='-1']),\n button:not([disabled]):not([tabindex='-1']),select:not([disabled]):not([tabindex='-1']),\n textarea:not([disabled]):not([tabindex='-1']),\n iframe, object, embed, *[tabindex]:not([tabindex='-1']):not([disabled]), *[contenteditable=true]\n";
|
24
|
+
/**
|
25
|
+
* CSS selector that selects major nodes that are click focusable
|
26
|
+
*/
|
27
|
+
export const selectorFocusable: "\n a[href], area[href], input:not([disabled]),\n button:not([disabled]),select:not([disabled]),\n textarea:not([disabled]),\n iframe, object, embed, *[tabindex]:not([disabled]), *[contenteditable=true]\n";
|
@@ -0,0 +1,43 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright IBM Corp. 2020, 2024
|
3
|
+
*
|
4
|
+
* This source code is licensed under the Apache-2.0 license found in the
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
6
|
+
*/
|
7
|
+
|
8
|
+
'use strict';
|
9
|
+
|
10
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
11
|
+
|
12
|
+
/**
|
13
|
+
* Various utilities to help with a11y work
|
14
|
+
*/
|
15
|
+
|
16
|
+
/**
|
17
|
+
* A flag `node.compareDocumentPosition(target)` returns,
|
18
|
+
* that indicates `target` is located earlier than `node` in the document or `target` contains `node`.
|
19
|
+
*/
|
20
|
+
var DOCUMENT_POSITION_BROAD_PRECEDING =
|
21
|
+
// Checks `typeof Node` for `react-docgen`
|
22
|
+
typeof Node !== 'undefined' &&
|
23
|
+
// eslint-disable-next-line ssr-friendly/no-dom-globals-in-module-scope
|
24
|
+
Node.DOCUMENT_POSITION_PRECEDING | Node.DOCUMENT_POSITION_CONTAINS;
|
25
|
+
|
26
|
+
/**
|
27
|
+
* A flag `node.compareDocumentPosition(target)` returns,
|
28
|
+
* that indicates `target` is located later than `node` in the document or `node` contains `target`.
|
29
|
+
*/
|
30
|
+
var DOCUMENT_POSITION_BROAD_FOLLOWING =
|
31
|
+
// Checks `typeof Node` for `react-docgen`
|
32
|
+
typeof Node !== 'undefined' &&
|
33
|
+
// eslint-disable-next-line ssr-friendly/no-dom-globals-in-module-scope
|
34
|
+
Node.DOCUMENT_POSITION_FOLLOWING | Node.DOCUMENT_POSITION_CONTAINED_BY;
|
35
|
+
|
36
|
+
/**
|
37
|
+
* CSS selector that selects major nodes that are sequential-focusable.
|
38
|
+
*/
|
39
|
+
var selectorTabbable = "\n a[href], area[href], input:not([disabled]):not([tabindex='-1']),\n button:not([disabled]):not([tabindex='-1']),select:not([disabled]):not([tabindex='-1']),\n textarea:not([disabled]):not([tabindex='-1']),\n iframe, object, embed, *[tabindex]:not([tabindex='-1']):not([disabled]), *[contenteditable=true]\n";
|
40
|
+
|
41
|
+
exports.DOCUMENT_POSITION_BROAD_FOLLOWING = DOCUMENT_POSITION_BROAD_FOLLOWING;
|
42
|
+
exports.DOCUMENT_POSITION_BROAD_PRECEDING = DOCUMENT_POSITION_BROAD_PRECEDING;
|
43
|
+
exports.selectorTabbable = selectorTabbable;
|
@@ -0,0 +1,25 @@
|
|
1
|
+
export default wrapFocus;
|
2
|
+
/**
|
3
|
+
* @param {Node} node A DOM node.
|
4
|
+
* @param {string[]} selectorsFloatingMenus The CSS selectors that matches floating menus.
|
5
|
+
* @returns {boolean} `true` of the given `node` is in a floating menu.
|
6
|
+
*/
|
7
|
+
export function elementOrParentIsFloatingMenu(node: Node, selectorsFloatingMenus?: string[]): boolean;
|
8
|
+
/**
|
9
|
+
* Ensures the focus is kept in the given `modalNode`, implementing "focus-wrap" behavior.
|
10
|
+
* @param {object} options The options.
|
11
|
+
* @param {Node|null} options.bodyNode The DOM node of the inner modal.
|
12
|
+
* @param {Node|null} options.startTrapNode The DOM node of the focus sentinel the is placed earlier next to `modalNode`.
|
13
|
+
* @param {Node|null} options.endTrapNode The DOM node of the focus sentinel the is placed next to `modalNode`.
|
14
|
+
* @param {Node} options.currentActiveNode The DOM node that has focus.
|
15
|
+
* @param {Node} options.oldActiveNode The DOM node that previously had focus.
|
16
|
+
* @param {string[]} [options.selectorsFloatingMenus] The CSS selectors that matches floating menus
|
17
|
+
*/
|
18
|
+
declare function wrapFocus({ bodyNode, startTrapNode, endTrapNode, currentActiveNode, oldActiveNode, selectorsFloatingMenus, }: {
|
19
|
+
bodyNode: Node | null;
|
20
|
+
startTrapNode: Node | null;
|
21
|
+
endTrapNode: Node | null;
|
22
|
+
currentActiveNode: Node;
|
23
|
+
oldActiveNode: Node;
|
24
|
+
selectorsFloatingMenus?: string[] | undefined;
|
25
|
+
}): void;
|
@@ -0,0 +1,73 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright IBM Corp. 2020, 2024
|
3
|
+
*
|
4
|
+
* This source code is licensed under the Apache-2.0 license found in the
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
6
|
+
*/
|
7
|
+
|
8
|
+
'use strict';
|
9
|
+
|
10
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
11
|
+
|
12
|
+
var keyboardNavigation = require('./keyboardNavigation.js');
|
13
|
+
var settings = require('../../../settings.js');
|
14
|
+
|
15
|
+
/**
|
16
|
+
* @param {Node} node A DOM node.
|
17
|
+
* @param {string[]} selectorsFloatingMenus The CSS selectors that matches floating menus.
|
18
|
+
* @returns {boolean} `true` of the given `node` is in a floating menu.
|
19
|
+
*/
|
20
|
+
function elementOrParentIsFloatingMenu(node) {
|
21
|
+
var selectorsFloatingMenus = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [".".concat(settings.carbon.prefix, "--overflow-menu-options"), ".".concat(settings.carbon.prefix, "--tooltip"), '.flatpickr-calendar'];
|
22
|
+
if (node && typeof node.closest === 'function') {
|
23
|
+
return selectorsFloatingMenus.some(function (selector) {
|
24
|
+
return node.closest(selector);
|
25
|
+
});
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
/**
|
30
|
+
* Ensures the focus is kept in the given `modalNode`, implementing "focus-wrap" behavior.
|
31
|
+
* @param {object} options The options.
|
32
|
+
* @param {Node|null} options.bodyNode The DOM node of the inner modal.
|
33
|
+
* @param {Node|null} options.startTrapNode The DOM node of the focus sentinel the is placed earlier next to `modalNode`.
|
34
|
+
* @param {Node|null} options.endTrapNode The DOM node of the focus sentinel the is placed next to `modalNode`.
|
35
|
+
* @param {Node} options.currentActiveNode The DOM node that has focus.
|
36
|
+
* @param {Node} options.oldActiveNode The DOM node that previously had focus.
|
37
|
+
* @param {string[]} [options.selectorsFloatingMenus] The CSS selectors that matches floating menus
|
38
|
+
*/
|
39
|
+
function wrapFocus(_ref) {
|
40
|
+
var bodyNode = _ref.bodyNode,
|
41
|
+
startTrapNode = _ref.startTrapNode,
|
42
|
+
endTrapNode = _ref.endTrapNode,
|
43
|
+
currentActiveNode = _ref.currentActiveNode,
|
44
|
+
oldActiveNode = _ref.oldActiveNode,
|
45
|
+
selectorsFloatingMenus = _ref.selectorsFloatingMenus;
|
46
|
+
if (bodyNode && currentActiveNode && oldActiveNode && !bodyNode.contains(currentActiveNode) && !elementOrParentIsFloatingMenu(currentActiveNode, selectorsFloatingMenus)) {
|
47
|
+
var comparisonResult = oldActiveNode.compareDocumentPosition(currentActiveNode);
|
48
|
+
if (currentActiveNode === startTrapNode || comparisonResult & keyboardNavigation.DOCUMENT_POSITION_BROAD_PRECEDING) {
|
49
|
+
var arrayNodes = Array.from(bodyNode.querySelectorAll(keyboardNavigation.selectorTabbable));
|
50
|
+
arrayNodes.reverse();
|
51
|
+
var tabbable = arrayNodes.find(function (elem) {
|
52
|
+
return Boolean(elem.offsetParent);
|
53
|
+
});
|
54
|
+
if (tabbable) {
|
55
|
+
tabbable.focus();
|
56
|
+
} else if (bodyNode !== oldActiveNode) {
|
57
|
+
bodyNode.focus();
|
58
|
+
}
|
59
|
+
} else if (currentActiveNode === endTrapNode || comparisonResult & keyboardNavigation.DOCUMENT_POSITION_BROAD_FOLLOWING) {
|
60
|
+
var _tabbable = Array.prototype.find.call(bodyNode.querySelectorAll(keyboardNavigation.selectorTabbable), function (elem) {
|
61
|
+
return Boolean(elem.offsetParent);
|
62
|
+
});
|
63
|
+
if (_tabbable) {
|
64
|
+
_tabbable.focus();
|
65
|
+
} else if (bodyNode !== oldActiveNode) {
|
66
|
+
bodyNode.focus();
|
67
|
+
}
|
68
|
+
}
|
69
|
+
}
|
70
|
+
}
|
71
|
+
|
72
|
+
exports["default"] = wrapFocus;
|
73
|
+
exports.elementOrParentIsFloatingMenu = elementOrParentIsFloatingMenu;
|
package/package.json
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
{
|
2
2
|
"name": "@carbon/ibm-products",
|
3
3
|
"description": "Carbon for IBM Products",
|
4
|
-
"version": "2.43.2-canary.
|
4
|
+
"version": "2.43.2-canary.177+bf1741045",
|
5
5
|
"license": "Apache-2.0",
|
6
6
|
"main": "lib/index.js",
|
7
7
|
"module": "es/index.js",
|
@@ -96,7 +96,7 @@
|
|
96
96
|
"dependencies": {
|
97
97
|
"@babel/runtime": "^7.23.9",
|
98
98
|
"@carbon/feature-flags": "^0.20.0",
|
99
|
-
"@carbon/ibm-products-styles": "^2.
|
99
|
+
"@carbon/ibm-products-styles": "^2.44.0-rc.0",
|
100
100
|
"@carbon/telemetry": "^0.1.0",
|
101
101
|
"@dnd-kit/core": "^6.0.8",
|
102
102
|
"@dnd-kit/modifiers": "^7.0.0",
|
@@ -120,5 +120,5 @@
|
|
120
120
|
"react": "^16.8.6 || ^17.0.1 || ^18.2.0",
|
121
121
|
"react-dom": "^16.8.6 || ^17.0.1 || ^18.2.0"
|
122
122
|
},
|
123
|
-
"gitHead": "
|
123
|
+
"gitHead": "bf1741045997b784c98068c618260dfbc7a79dc6"
|
124
124
|
}
|