@telus-uds/components-web 2.34.2 → 2.34.3
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 +13 -2
- package/lib/Footnote/Footnote.js +32 -15
- package/lib/utils/index.js +7 -0
- package/lib/utils/isElementFocusable.js +15 -0
- package/lib-module/Footnote/Footnote.js +33 -16
- package/lib-module/utils/index.js +2 -1
- package/lib-module/utils/isElementFocusable.js +8 -0
- package/package.json +3 -3
- package/src/Footnote/Footnote.jsx +36 -16
- package/src/utils/index.js +2 -0
- package/src/utils/isElementFocusable.js +13 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
# Change Log - @telus-uds/components-web
|
|
2
2
|
|
|
3
|
-
This log was last generated on
|
|
3
|
+
This log was last generated on Fri, 07 Jun 2024 22:36:16 GMT and should not be manually modified.
|
|
4
4
|
|
|
5
5
|
<!-- Start content -->
|
|
6
6
|
|
|
7
|
+
## 2.34.3
|
|
8
|
+
|
|
9
|
+
Fri, 07 Jun 2024 22:36:16 GMT
|
|
10
|
+
|
|
11
|
+
### Patches
|
|
12
|
+
|
|
13
|
+
- Fixes for Footnote accesibility (Mauricio.BatresMontejo@telus.com)
|
|
14
|
+
- `Footnote`: fix hydration errors (guillermo.peitzner@telus.com)
|
|
15
|
+
- Bump @telus-uds/components-base to v1.86.0
|
|
16
|
+
- Bump @telus-uds/system-theme-tokens to v2.57.0
|
|
17
|
+
|
|
7
18
|
## 2.34.2
|
|
8
19
|
|
|
9
|
-
Tue, 28 May 2024
|
|
20
|
+
Tue, 28 May 2024 11:16:51 GMT
|
|
10
21
|
|
|
11
22
|
### Patches
|
|
12
23
|
|
package/lib/Footnote/Footnote.js
CHANGED
|
@@ -308,7 +308,7 @@ const Footnote = /*#__PURE__*/_react.default.forwardRef((props, ref) => {
|
|
|
308
308
|
const headerRef = _react.default.useRef(null);
|
|
309
309
|
const bodyRef = _react.default.useRef(null);
|
|
310
310
|
const contentRef = _react.default.useRef(null);
|
|
311
|
-
const
|
|
311
|
+
const closeButtonRef = _react.default.useRef(null);
|
|
312
312
|
const [data, setData] = _react.default.useState({
|
|
313
313
|
content: null,
|
|
314
314
|
number: null
|
|
@@ -329,8 +329,14 @@ const Footnote = /*#__PURE__*/_react.default.forwardRef((props, ref) => {
|
|
|
329
329
|
onClose(event, options);
|
|
330
330
|
}, [onClose]);
|
|
331
331
|
|
|
332
|
-
|
|
333
|
-
|
|
332
|
+
/**
|
|
333
|
+
* When listen for ESCAPE, close button clicks, and clicks outside of the Footnote. Call onClose.
|
|
334
|
+
* When the event type is a 'keydonw' and the event key is a 'Tab', using a 'querySelectorAll we obtain all
|
|
335
|
+
* the interactive elements within the footnote, we order and save the first and the last,
|
|
336
|
+
* if the footnote is active the focus will be inside the footnote until it is closed,
|
|
337
|
+
* if there are no interactive elements the focus will remain inside the close button.
|
|
338
|
+
*/
|
|
339
|
+
const manageFootnoteFocusAndClose = _react.default.useCallback(event => {
|
|
334
340
|
var _footnoteRef$current, _footnoteRef$current2;
|
|
335
341
|
if (!isVisible) {
|
|
336
342
|
return;
|
|
@@ -340,6 +346,17 @@ const Footnote = /*#__PURE__*/_react.default.forwardRef((props, ref) => {
|
|
|
340
346
|
closeFootnote(event, {
|
|
341
347
|
returnFocus: true
|
|
342
348
|
});
|
|
349
|
+
} else if (event.key === 'Tab') {
|
|
350
|
+
const focusableElements = Array.from(footnoteRef.current.querySelectorAll('*')).filter(_utils.isElementFocusable);
|
|
351
|
+
const firstElement = focusableElements[0];
|
|
352
|
+
const lastElement = focusableElements[focusableElements.length - 1];
|
|
353
|
+
if (event.shiftKey && document.activeElement === firstElement) {
|
|
354
|
+
event.preventDefault();
|
|
355
|
+
lastElement.focus();
|
|
356
|
+
} else if (!event.shiftKey && document.activeElement === lastElement) {
|
|
357
|
+
event.preventDefault();
|
|
358
|
+
firstElement.focus();
|
|
359
|
+
}
|
|
343
360
|
}
|
|
344
361
|
} else if ((event.type === 'click' || event.type === 'mousedown') && footnoteRef !== null && footnoteRef !== void 0 && footnoteRef.current && event.target && !(footnoteRef !== null && footnoteRef !== void 0 && (_footnoteRef$current = footnoteRef.current) !== null && _footnoteRef$current !== void 0 && _footnoteRef$current.contains(event.target)) && event.target.getAttribute('data-tds-id') !== 'footnote-link') {
|
|
345
362
|
closeFootnote(event, {
|
|
@@ -356,8 +373,8 @@ const Footnote = /*#__PURE__*/_react.default.forwardRef((props, ref) => {
|
|
|
356
373
|
setBodyHeight(oldHeight);
|
|
357
374
|
};
|
|
358
375
|
const focusHeading = () => {
|
|
359
|
-
if (Boolean(content) && isVisible &&
|
|
360
|
-
|
|
376
|
+
if (Boolean(content) && isVisible && closeButtonRef && closeButtonRef.current !== null) {
|
|
377
|
+
closeButtonRef.current.focus();
|
|
361
378
|
}
|
|
362
379
|
};
|
|
363
380
|
const handleStyledFootnoteTransitionEnd = event => {
|
|
@@ -410,24 +427,24 @@ const Footnote = /*#__PURE__*/_react.default.forwardRef((props, ref) => {
|
|
|
410
427
|
_react.default.useEffect(() => {
|
|
411
428
|
if (isOpen) {
|
|
412
429
|
setIsVisible(true);
|
|
413
|
-
document.addEventListener('mousedown',
|
|
414
|
-
window.addEventListener('click',
|
|
415
|
-
window.addEventListener('keydown',
|
|
416
|
-
window.addEventListener('touchstart',
|
|
430
|
+
document.addEventListener('mousedown', manageFootnoteFocusAndClose);
|
|
431
|
+
window.addEventListener('click', manageFootnoteFocusAndClose);
|
|
432
|
+
window.addEventListener('keydown', manageFootnoteFocusAndClose);
|
|
433
|
+
window.addEventListener('touchstart', manageFootnoteFocusAndClose);
|
|
417
434
|
window.addEventListener('touchmove', preventDefault, {
|
|
418
435
|
passive: false
|
|
419
436
|
});
|
|
420
437
|
}
|
|
421
438
|
return () => {
|
|
422
439
|
if (isOpen) {
|
|
423
|
-
document.removeEventListener('mousedown',
|
|
424
|
-
window.removeEventListener('click',
|
|
425
|
-
window.removeEventListener('keydown',
|
|
426
|
-
window.removeEventListener('touchstart',
|
|
440
|
+
document.removeEventListener('mousedown', manageFootnoteFocusAndClose);
|
|
441
|
+
window.removeEventListener('click', manageFootnoteFocusAndClose);
|
|
442
|
+
window.removeEventListener('keydown', manageFootnoteFocusAndClose);
|
|
443
|
+
window.removeEventListener('touchstart', manageFootnoteFocusAndClose);
|
|
427
444
|
window.removeEventListener('touchmove', preventDefault);
|
|
428
445
|
}
|
|
429
446
|
};
|
|
430
|
-
}, [
|
|
447
|
+
}, [manageFootnoteFocusAndClose, isOpen]);
|
|
431
448
|
|
|
432
449
|
// Set data if opening a new footnote
|
|
433
450
|
_react.default.useEffect(() => {
|
|
@@ -517,7 +534,6 @@ const Footnote = /*#__PURE__*/_react.default.forwardRef((props, ref) => {
|
|
|
517
534
|
ref: headerRef,
|
|
518
535
|
viewport: viewport,
|
|
519
536
|
children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(StyledHeader, {
|
|
520
|
-
ref: headingRef,
|
|
521
537
|
footnoteHeaderPaddingLeft: footnoteHeaderPaddingLeft,
|
|
522
538
|
footnoteHeaderPaddingRight: footnoteHeaderPaddingRight,
|
|
523
539
|
footnoteHeaderPaddingTop: footnoteHeaderPaddingTop,
|
|
@@ -533,6 +549,7 @@ const Footnote = /*#__PURE__*/_react.default.forwardRef((props, ref) => {
|
|
|
533
549
|
},
|
|
534
550
|
children: getCopy('heading')
|
|
535
551
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(CloseButton, {
|
|
552
|
+
ref: closeButtonRef,
|
|
536
553
|
closeButtonBorder: `${closeButtonBorderSize} solid ${closeButtonBorderColor}`,
|
|
537
554
|
closeButtonWidth: `${closeButtonWidth}px`,
|
|
538
555
|
closeButtonHeight: `${closeButtonHeight}px`,
|
package/lib/utils/index.js
CHANGED
|
@@ -15,6 +15,12 @@ Object.defineProperty(exports, "htmlAttrs", {
|
|
|
15
15
|
return _componentsBase.htmlAttrs;
|
|
16
16
|
}
|
|
17
17
|
});
|
|
18
|
+
Object.defineProperty(exports, "isElementFocusable", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
get: function () {
|
|
21
|
+
return _isElementFocusable.default;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
18
24
|
Object.defineProperty(exports, "media", {
|
|
19
25
|
enumerable: true,
|
|
20
26
|
get: function () {
|
|
@@ -63,6 +69,7 @@ var _transforms = require("./transforms");
|
|
|
63
69
|
var _useTypographyTheme = _interopRequireDefault(require("./useTypographyTheme"));
|
|
64
70
|
var _media = _interopRequireDefault(require("./media"));
|
|
65
71
|
var _ssr = _interopRequireDefault(require("./ssr"));
|
|
72
|
+
var _isElementFocusable = _interopRequireDefault(require("./isElementFocusable"));
|
|
66
73
|
var _renderStructuredContent = _interopRequireDefault(require("./renderStructuredContent"));
|
|
67
74
|
var _useOverlaidPosition = _interopRequireDefault(require("./useOverlaidPosition"));
|
|
68
75
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Returns focusable elements inside of the Footnote
|
|
9
|
+
*/
|
|
10
|
+
const isElementFocusable = element => {
|
|
11
|
+
const focusableElements = `a[href], button, textarea, input, select, form, label, audio, video, source, track, canvas, rect, polygon, iframe[data-src], [tabindex]:not([tabindex="-1"]), [contenteditable]`;
|
|
12
|
+
return element.matches(focusableElements) && !element.hasAttribute('disabled') && !element.matches('[tabindex="-1"]');
|
|
13
|
+
};
|
|
14
|
+
var _default = isElementFocusable;
|
|
15
|
+
exports.default = _default;
|
|
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|
|
3
3
|
import styled, { createGlobalStyle } from 'styled-components';
|
|
4
4
|
import { Icon, Portal, selectSystemProps, Typography, useCopy, useTheme, useResponsiveProp, useThemeTokens, useViewport, getTokensPropType } from '@telus-uds/components-base';
|
|
5
5
|
import OrderedListBase from '../OrderedList/OrderedListBase';
|
|
6
|
-
import { htmlAttrs, media, renderStructuredContent } from '../utils';
|
|
6
|
+
import { htmlAttrs, media, renderStructuredContent, isElementFocusable } from '../utils';
|
|
7
7
|
import defaultDictionary from './dictionary';
|
|
8
8
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
9
9
|
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
@@ -300,7 +300,7 @@ const Footnote = /*#__PURE__*/React.forwardRef((props, ref) => {
|
|
|
300
300
|
const headerRef = React.useRef(null);
|
|
301
301
|
const bodyRef = React.useRef(null);
|
|
302
302
|
const contentRef = React.useRef(null);
|
|
303
|
-
const
|
|
303
|
+
const closeButtonRef = React.useRef(null);
|
|
304
304
|
const [data, setData] = React.useState({
|
|
305
305
|
content: null,
|
|
306
306
|
number: null
|
|
@@ -321,8 +321,14 @@ const Footnote = /*#__PURE__*/React.forwardRef((props, ref) => {
|
|
|
321
321
|
onClose(event, options);
|
|
322
322
|
}, [onClose]);
|
|
323
323
|
|
|
324
|
-
|
|
325
|
-
|
|
324
|
+
/**
|
|
325
|
+
* When listen for ESCAPE, close button clicks, and clicks outside of the Footnote. Call onClose.
|
|
326
|
+
* When the event type is a 'keydonw' and the event key is a 'Tab', using a 'querySelectorAll we obtain all
|
|
327
|
+
* the interactive elements within the footnote, we order and save the first and the last,
|
|
328
|
+
* if the footnote is active the focus will be inside the footnote until it is closed,
|
|
329
|
+
* if there are no interactive elements the focus will remain inside the close button.
|
|
330
|
+
*/
|
|
331
|
+
const manageFootnoteFocusAndClose = React.useCallback(event => {
|
|
326
332
|
var _footnoteRef$current, _footnoteRef$current2;
|
|
327
333
|
if (!isVisible) {
|
|
328
334
|
return;
|
|
@@ -332,6 +338,17 @@ const Footnote = /*#__PURE__*/React.forwardRef((props, ref) => {
|
|
|
332
338
|
closeFootnote(event, {
|
|
333
339
|
returnFocus: true
|
|
334
340
|
});
|
|
341
|
+
} else if (event.key === 'Tab') {
|
|
342
|
+
const focusableElements = Array.from(footnoteRef.current.querySelectorAll('*')).filter(isElementFocusable);
|
|
343
|
+
const firstElement = focusableElements[0];
|
|
344
|
+
const lastElement = focusableElements[focusableElements.length - 1];
|
|
345
|
+
if (event.shiftKey && document.activeElement === firstElement) {
|
|
346
|
+
event.preventDefault();
|
|
347
|
+
lastElement.focus();
|
|
348
|
+
} else if (!event.shiftKey && document.activeElement === lastElement) {
|
|
349
|
+
event.preventDefault();
|
|
350
|
+
firstElement.focus();
|
|
351
|
+
}
|
|
335
352
|
}
|
|
336
353
|
} else if ((event.type === 'click' || event.type === 'mousedown') && footnoteRef !== null && footnoteRef !== void 0 && footnoteRef.current && event.target && !(footnoteRef !== null && footnoteRef !== void 0 && (_footnoteRef$current = footnoteRef.current) !== null && _footnoteRef$current !== void 0 && _footnoteRef$current.contains(event.target)) && event.target.getAttribute('data-tds-id') !== 'footnote-link') {
|
|
337
354
|
closeFootnote(event, {
|
|
@@ -348,8 +365,8 @@ const Footnote = /*#__PURE__*/React.forwardRef((props, ref) => {
|
|
|
348
365
|
setBodyHeight(oldHeight);
|
|
349
366
|
};
|
|
350
367
|
const focusHeading = () => {
|
|
351
|
-
if (Boolean(content) && isVisible &&
|
|
352
|
-
|
|
368
|
+
if (Boolean(content) && isVisible && closeButtonRef && closeButtonRef.current !== null) {
|
|
369
|
+
closeButtonRef.current.focus();
|
|
353
370
|
}
|
|
354
371
|
};
|
|
355
372
|
const handleStyledFootnoteTransitionEnd = event => {
|
|
@@ -402,24 +419,24 @@ const Footnote = /*#__PURE__*/React.forwardRef((props, ref) => {
|
|
|
402
419
|
React.useEffect(() => {
|
|
403
420
|
if (isOpen) {
|
|
404
421
|
setIsVisible(true);
|
|
405
|
-
document.addEventListener('mousedown',
|
|
406
|
-
window.addEventListener('click',
|
|
407
|
-
window.addEventListener('keydown',
|
|
408
|
-
window.addEventListener('touchstart',
|
|
422
|
+
document.addEventListener('mousedown', manageFootnoteFocusAndClose);
|
|
423
|
+
window.addEventListener('click', manageFootnoteFocusAndClose);
|
|
424
|
+
window.addEventListener('keydown', manageFootnoteFocusAndClose);
|
|
425
|
+
window.addEventListener('touchstart', manageFootnoteFocusAndClose);
|
|
409
426
|
window.addEventListener('touchmove', preventDefault, {
|
|
410
427
|
passive: false
|
|
411
428
|
});
|
|
412
429
|
}
|
|
413
430
|
return () => {
|
|
414
431
|
if (isOpen) {
|
|
415
|
-
document.removeEventListener('mousedown',
|
|
416
|
-
window.removeEventListener('click',
|
|
417
|
-
window.removeEventListener('keydown',
|
|
418
|
-
window.removeEventListener('touchstart',
|
|
432
|
+
document.removeEventListener('mousedown', manageFootnoteFocusAndClose);
|
|
433
|
+
window.removeEventListener('click', manageFootnoteFocusAndClose);
|
|
434
|
+
window.removeEventListener('keydown', manageFootnoteFocusAndClose);
|
|
435
|
+
window.removeEventListener('touchstart', manageFootnoteFocusAndClose);
|
|
419
436
|
window.removeEventListener('touchmove', preventDefault);
|
|
420
437
|
}
|
|
421
438
|
};
|
|
422
|
-
}, [
|
|
439
|
+
}, [manageFootnoteFocusAndClose, isOpen]);
|
|
423
440
|
|
|
424
441
|
// Set data if opening a new footnote
|
|
425
442
|
React.useEffect(() => {
|
|
@@ -509,7 +526,6 @@ const Footnote = /*#__PURE__*/React.forwardRef((props, ref) => {
|
|
|
509
526
|
ref: headerRef,
|
|
510
527
|
viewport: viewport,
|
|
511
528
|
children: /*#__PURE__*/_jsxs(StyledHeader, {
|
|
512
|
-
ref: headingRef,
|
|
513
529
|
footnoteHeaderPaddingLeft: footnoteHeaderPaddingLeft,
|
|
514
530
|
footnoteHeaderPaddingRight: footnoteHeaderPaddingRight,
|
|
515
531
|
footnoteHeaderPaddingTop: footnoteHeaderPaddingTop,
|
|
@@ -525,6 +541,7 @@ const Footnote = /*#__PURE__*/React.forwardRef((props, ref) => {
|
|
|
525
541
|
},
|
|
526
542
|
children: getCopy('heading')
|
|
527
543
|
}), /*#__PURE__*/_jsx(CloseButton, {
|
|
544
|
+
ref: closeButtonRef,
|
|
528
545
|
closeButtonBorder: `${closeButtonBorderSize} solid ${closeButtonBorderColor}`,
|
|
529
546
|
closeButtonWidth: `${closeButtonWidth}px`,
|
|
530
547
|
closeButtonHeight: `${closeButtonHeight}px`,
|
|
@@ -4,6 +4,7 @@ import { transformGradient } from './transforms';
|
|
|
4
4
|
import useTypographyTheme from './useTypographyTheme';
|
|
5
5
|
import media from './media';
|
|
6
6
|
import ssrStyles from './ssr';
|
|
7
|
+
import isElementFocusable from './isElementFocusable';
|
|
7
8
|
import renderStructuredContent from './renderStructuredContent';
|
|
8
9
|
import useOverlaidPosition from './useOverlaidPosition';
|
|
9
|
-
export { deprecate, htmlAttrs, transformGradient, useTypographyTheme, warn, media, renderStructuredContent, ssrStyles, useOverlaidPosition };
|
|
10
|
+
export { deprecate, htmlAttrs, transformGradient, useTypographyTheme, warn, media, renderStructuredContent, ssrStyles, isElementFocusable, useOverlaidPosition };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns focusable elements inside of the Footnote
|
|
3
|
+
*/
|
|
4
|
+
const isElementFocusable = element => {
|
|
5
|
+
const focusableElements = `a[href], button, textarea, input, select, form, label, audio, video, source, track, canvas, rect, polygon, iframe[data-src], [tabindex]:not([tabindex="-1"]), [contenteditable]`;
|
|
6
|
+
return element.matches(focusableElements) && !element.hasAttribute('disabled') && !element.matches('[tabindex="-1"]');
|
|
7
|
+
};
|
|
8
|
+
export default isElementFocusable;
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
],
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@gorhom/portal": "^1.0.14",
|
|
8
|
-
"@telus-uds/components-base": "1.
|
|
8
|
+
"@telus-uds/components-base": "1.86.0",
|
|
9
9
|
"@telus-uds/system-constants": "^1.3.0",
|
|
10
10
|
"fscreen": "^1.2.0",
|
|
11
11
|
"lodash.omit": "^4.5.0",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"react-dates": "^21.8.0",
|
|
14
14
|
"react-helmet-async": "^1.3.0",
|
|
15
15
|
"react-moment-proptypes": "^1.8.1",
|
|
16
|
-
"@telus-uds/system-theme-tokens": "^2.
|
|
16
|
+
"@telus-uds/system-theme-tokens": "^2.57.0",
|
|
17
17
|
"prop-types": "^15.7.2",
|
|
18
18
|
"lodash.throttle": "^4.1.1",
|
|
19
19
|
"react-youtube": "^10.1.0",
|
|
@@ -83,5 +83,5 @@
|
|
|
83
83
|
"skip": true
|
|
84
84
|
},
|
|
85
85
|
"types": "types/index.d.ts",
|
|
86
|
-
"version": "2.34.
|
|
86
|
+
"version": "2.34.3"
|
|
87
87
|
}
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from '@telus-uds/components-base'
|
|
16
16
|
|
|
17
17
|
import OrderedListBase from '../OrderedList/OrderedListBase'
|
|
18
|
-
import { htmlAttrs, media, renderStructuredContent } from '../utils'
|
|
18
|
+
import { htmlAttrs, media, renderStructuredContent, isElementFocusable } from '../utils'
|
|
19
19
|
import defaultDictionary from './dictionary'
|
|
20
20
|
|
|
21
21
|
const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs])
|
|
@@ -263,7 +263,7 @@ const Footnote = React.forwardRef((props, ref) => {
|
|
|
263
263
|
const headerRef = React.useRef(null)
|
|
264
264
|
const bodyRef = React.useRef(null)
|
|
265
265
|
const contentRef = React.useRef(null)
|
|
266
|
-
const
|
|
266
|
+
const closeButtonRef = React.useRef(null)
|
|
267
267
|
const [data, setData] = React.useState({ content: null, number: null })
|
|
268
268
|
const [headerHeight, setHeaderHeight] = React.useState('auto')
|
|
269
269
|
const [bodyHeight, setBodyHeight] = React.useState('auto')
|
|
@@ -283,8 +283,14 @@ const Footnote = React.forwardRef((props, ref) => {
|
|
|
283
283
|
[onClose]
|
|
284
284
|
)
|
|
285
285
|
|
|
286
|
-
|
|
287
|
-
|
|
286
|
+
/**
|
|
287
|
+
* When listen for ESCAPE, close button clicks, and clicks outside of the Footnote. Call onClose.
|
|
288
|
+
* When the event type is a 'keydonw' and the event key is a 'Tab', using a 'querySelectorAll we obtain all
|
|
289
|
+
* the interactive elements within the footnote, we order and save the first and the last,
|
|
290
|
+
* if the footnote is active the focus will be inside the footnote until it is closed,
|
|
291
|
+
* if there are no interactive elements the focus will remain inside the close button.
|
|
292
|
+
*/
|
|
293
|
+
const manageFootnoteFocusAndClose = React.useCallback(
|
|
288
294
|
(event) => {
|
|
289
295
|
if (!isVisible) {
|
|
290
296
|
return
|
|
@@ -293,6 +299,20 @@ const Footnote = React.forwardRef((props, ref) => {
|
|
|
293
299
|
if (event.type === 'keydown') {
|
|
294
300
|
if (event.key === 'Escape' || event.key === 27) {
|
|
295
301
|
closeFootnote(event, { returnFocus: true })
|
|
302
|
+
} else if (event.key === 'Tab') {
|
|
303
|
+
const focusableElements = Array.from(footnoteRef.current.querySelectorAll('*')).filter(
|
|
304
|
+
isElementFocusable
|
|
305
|
+
)
|
|
306
|
+
const firstElement = focusableElements[0]
|
|
307
|
+
const lastElement = focusableElements[focusableElements.length - 1]
|
|
308
|
+
|
|
309
|
+
if (event.shiftKey && document.activeElement === firstElement) {
|
|
310
|
+
event.preventDefault()
|
|
311
|
+
lastElement.focus()
|
|
312
|
+
} else if (!event.shiftKey && document.activeElement === lastElement) {
|
|
313
|
+
event.preventDefault()
|
|
314
|
+
firstElement.focus()
|
|
315
|
+
}
|
|
296
316
|
}
|
|
297
317
|
} else if (
|
|
298
318
|
(event.type === 'click' || event.type === 'mousedown') &&
|
|
@@ -321,8 +341,8 @@ const Footnote = React.forwardRef((props, ref) => {
|
|
|
321
341
|
}
|
|
322
342
|
|
|
323
343
|
const focusHeading = () => {
|
|
324
|
-
if (Boolean(content) && isVisible &&
|
|
325
|
-
|
|
344
|
+
if (Boolean(content) && isVisible && closeButtonRef && closeButtonRef.current !== null) {
|
|
345
|
+
closeButtonRef.current.focus()
|
|
326
346
|
}
|
|
327
347
|
}
|
|
328
348
|
|
|
@@ -376,22 +396,22 @@ const Footnote = React.forwardRef((props, ref) => {
|
|
|
376
396
|
React.useEffect(() => {
|
|
377
397
|
if (isOpen) {
|
|
378
398
|
setIsVisible(true)
|
|
379
|
-
document.addEventListener('mousedown',
|
|
380
|
-
window.addEventListener('click',
|
|
381
|
-
window.addEventListener('keydown',
|
|
382
|
-
window.addEventListener('touchstart',
|
|
399
|
+
document.addEventListener('mousedown', manageFootnoteFocusAndClose)
|
|
400
|
+
window.addEventListener('click', manageFootnoteFocusAndClose)
|
|
401
|
+
window.addEventListener('keydown', manageFootnoteFocusAndClose)
|
|
402
|
+
window.addEventListener('touchstart', manageFootnoteFocusAndClose)
|
|
383
403
|
window.addEventListener('touchmove', preventDefault, { passive: false })
|
|
384
404
|
}
|
|
385
405
|
return () => {
|
|
386
406
|
if (isOpen) {
|
|
387
|
-
document.removeEventListener('mousedown',
|
|
388
|
-
window.removeEventListener('click',
|
|
389
|
-
window.removeEventListener('keydown',
|
|
390
|
-
window.removeEventListener('touchstart',
|
|
407
|
+
document.removeEventListener('mousedown', manageFootnoteFocusAndClose)
|
|
408
|
+
window.removeEventListener('click', manageFootnoteFocusAndClose)
|
|
409
|
+
window.removeEventListener('keydown', manageFootnoteFocusAndClose)
|
|
410
|
+
window.removeEventListener('touchstart', manageFootnoteFocusAndClose)
|
|
391
411
|
window.removeEventListener('touchmove', preventDefault)
|
|
392
412
|
}
|
|
393
413
|
}
|
|
394
|
-
}, [
|
|
414
|
+
}, [manageFootnoteFocusAndClose, isOpen])
|
|
395
415
|
|
|
396
416
|
// Set data if opening a new footnote
|
|
397
417
|
React.useEffect(() => {
|
|
@@ -496,7 +516,6 @@ const Footnote = React.forwardRef((props, ref) => {
|
|
|
496
516
|
<ContentContainer maxWidth={maxWidth}>
|
|
497
517
|
<StyledFootnoteHeader ref={headerRef} viewport={viewport}>
|
|
498
518
|
<StyledHeader
|
|
499
|
-
ref={headingRef}
|
|
500
519
|
footnoteHeaderPaddingLeft={footnoteHeaderPaddingLeft}
|
|
501
520
|
footnoteHeaderPaddingRight={footnoteHeaderPaddingRight}
|
|
502
521
|
footnoteHeaderPaddingTop={footnoteHeaderPaddingTop}
|
|
@@ -513,6 +532,7 @@ const Footnote = React.forwardRef((props, ref) => {
|
|
|
513
532
|
{getCopy('heading')}
|
|
514
533
|
</Typography>
|
|
515
534
|
<CloseButton
|
|
535
|
+
ref={closeButtonRef}
|
|
516
536
|
closeButtonBorder={`${closeButtonBorderSize} solid ${closeButtonBorderColor}`}
|
|
517
537
|
closeButtonWidth={`${closeButtonWidth}px`}
|
|
518
538
|
closeButtonHeight={`${closeButtonHeight}px`}
|
package/src/utils/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { transformGradient } from './transforms'
|
|
|
4
4
|
import useTypographyTheme from './useTypographyTheme'
|
|
5
5
|
import media from './media'
|
|
6
6
|
import ssrStyles from './ssr'
|
|
7
|
+
import isElementFocusable from './isElementFocusable'
|
|
7
8
|
import renderStructuredContent from './renderStructuredContent'
|
|
8
9
|
import useOverlaidPosition from './useOverlaidPosition'
|
|
9
10
|
|
|
@@ -16,5 +17,6 @@ export {
|
|
|
16
17
|
media,
|
|
17
18
|
renderStructuredContent,
|
|
18
19
|
ssrStyles,
|
|
20
|
+
isElementFocusable,
|
|
19
21
|
useOverlaidPosition
|
|
20
22
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns focusable elements inside of the Footnote
|
|
3
|
+
*/
|
|
4
|
+
const isElementFocusable = (element) => {
|
|
5
|
+
const focusableElements = `a[href], button, textarea, input, select, form, label, audio, video, source, track, canvas, rect, polygon, iframe[data-src], [tabindex]:not([tabindex="-1"]), [contenteditable]`
|
|
6
|
+
return (
|
|
7
|
+
element.matches(focusableElements) &&
|
|
8
|
+
!element.hasAttribute('disabled') &&
|
|
9
|
+
!element.matches('[tabindex="-1"]')
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default isElementFocusable
|