@workday/canvas-kit-react 7.3.14 → 7.3.16
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/common/lib/utils/elements.ts +4 -4
- package/dist/commonjs/common/lib/utils/elements.d.ts +1 -1
- package/dist/commonjs/common/lib/utils/elements.d.ts.map +1 -1
- package/dist/commonjs/common/lib/utils/elements.js +3 -3
- package/dist/commonjs/popup/lib/hooks/useReturnFocus.d.ts.map +1 -1
- package/dist/commonjs/popup/lib/hooks/useReturnFocus.js +72 -1
- package/dist/commonjs/tabs/lib/TabsItem.d.ts +1 -1
- package/dist/es6/common/lib/utils/elements.d.ts +1 -1
- package/dist/es6/common/lib/utils/elements.d.ts.map +1 -1
- package/dist/es6/common/lib/utils/elements.js +3 -3
- package/dist/es6/popup/lib/hooks/useReturnFocus.d.ts.map +1 -1
- package/dist/es6/popup/lib/hooks/useReturnFocus.js +73 -2
- package/package.json +3 -3
- package/popup/lib/hooks/useReturnFocus.tsx +82 -3
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Is an element focusable? This function performs various tests to see if the element in question
|
|
3
3
|
* can receive focus. Should skip disabled elements as they are not focusable.
|
|
4
4
|
*/
|
|
5
|
-
export const isFocusable = (element:
|
|
5
|
+
export const isFocusable = (element: Element): boolean => {
|
|
6
6
|
if (element.hasAttribute('disabled')) {
|
|
7
|
-
return
|
|
7
|
+
return false;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const nodeName = element.nodeName.toLowerCase();
|
|
@@ -35,7 +35,7 @@ export const getFirstFocusableElement = (container: HTMLElement): HTMLElement |
|
|
|
35
35
|
|
|
36
36
|
for (let i = 0; i < elements.length; i++) {
|
|
37
37
|
const element = elements.item(i);
|
|
38
|
-
if (element && isFocusable(element
|
|
38
|
+
if (element && isFocusable(element) && element.getAttribute('tabindex') !== '-1') {
|
|
39
39
|
return element as HTMLElement;
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -51,7 +51,7 @@ export const getLastFocusableElement = (container: HTMLElement): HTMLElement | n
|
|
|
51
51
|
|
|
52
52
|
for (let i = elements.length - 1; i >= 0; i--) {
|
|
53
53
|
const element = elements.item(i);
|
|
54
|
-
if (element && isFocusable(element
|
|
54
|
+
if (element && isFocusable(element) && element.getAttribute('tabindex') !== '-1') {
|
|
55
55
|
return element as HTMLElement;
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Is an element focusable? This function performs various tests to see if the element in question
|
|
3
3
|
* can receive focus. Should skip disabled elements as they are not focusable.
|
|
4
4
|
*/
|
|
5
|
-
export declare const isFocusable: (element:
|
|
5
|
+
export declare const isFocusable: (element: Element) => boolean;
|
|
6
6
|
/**
|
|
7
7
|
* Get the first focusable element in a container.
|
|
8
8
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"elements.d.ts","sourceRoot":"","sources":["../../../../../common/lib/utils/elements.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,WAAW,YAAa,
|
|
1
|
+
{"version":3,"file":"elements.d.ts","sourceRoot":"","sources":["../../../../../common/lib/utils/elements.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,WAAW,YAAa,OAAO,KAAG,OAuB9C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,wBAAwB,cAAe,WAAW,KAAG,WAAW,GAAG,IAW/E,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,uBAAuB,cAAe,WAAW,KAAG,WAAW,GAAG,IAW9E,CAAC"}
|
|
@@ -7,7 +7,7 @@ exports.getLastFocusableElement = exports.getFirstFocusableElement = exports.isF
|
|
|
7
7
|
*/
|
|
8
8
|
var isFocusable = function (element) {
|
|
9
9
|
if (element.hasAttribute('disabled')) {
|
|
10
|
-
return
|
|
10
|
+
return false;
|
|
11
11
|
}
|
|
12
12
|
var nodeName = element.nodeName.toLowerCase();
|
|
13
13
|
var validInput = nodeName === 'input' && element.getAttribute('type') !== 'hidden';
|
|
@@ -32,7 +32,7 @@ var getFirstFocusableElement = function (container) {
|
|
|
32
32
|
var elements = container.querySelectorAll('*');
|
|
33
33
|
for (var i = 0; i < elements.length; i++) {
|
|
34
34
|
var element = elements.item(i);
|
|
35
|
-
if (element && exports.isFocusable(element)) {
|
|
35
|
+
if (element && exports.isFocusable(element) && element.getAttribute('tabindex') !== '-1') {
|
|
36
36
|
return element;
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -46,7 +46,7 @@ var getLastFocusableElement = function (container) {
|
|
|
46
46
|
var elements = container.querySelectorAll('*');
|
|
47
47
|
for (var i = elements.length - 1; i >= 0; i--) {
|
|
48
48
|
var element = elements.item(i);
|
|
49
|
-
if (element && exports.isFocusable(element)) {
|
|
49
|
+
if (element && exports.isFocusable(element) && element.getAttribute('tabindex') !== '-1') {
|
|
50
50
|
return element;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useReturnFocus.d.ts","sourceRoot":"","sources":["../../../../../popup/lib/hooks/useReturnFocus.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"useReturnFocus.d.ts","sourceRoot":"","sources":["../../../../../popup/lib/hooks/useReturnFocus.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAgC1B;;;;;;GAMG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;OAsGzB,CAAC"}
|
|
@@ -7,6 +7,28 @@ exports.useReturnFocus = void 0;
|
|
|
7
7
|
var react_1 = __importDefault(require("react"));
|
|
8
8
|
var common_1 = require("@workday/canvas-kit-react/common");
|
|
9
9
|
var usePopupModel_1 = require("./usePopupModel");
|
|
10
|
+
function getScrollParent(element) {
|
|
11
|
+
if (element === document.body || !element.parentElement) {
|
|
12
|
+
return element;
|
|
13
|
+
}
|
|
14
|
+
var styles = getComputedStyle(element);
|
|
15
|
+
if (styles.overflowY === 'scroll' || styles.overflowY === 'auto') {
|
|
16
|
+
return element;
|
|
17
|
+
}
|
|
18
|
+
return getScrollParent(element.parentElement);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Get a focusable element in an element's tree
|
|
22
|
+
*/
|
|
23
|
+
function getFocusableElement(element) {
|
|
24
|
+
if (element === null || element === document.body || !element.parentElement) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
else if (common_1.isFocusable(element)) {
|
|
28
|
+
return element;
|
|
29
|
+
}
|
|
30
|
+
return getFocusableElement(element.parentElement);
|
|
31
|
+
}
|
|
10
32
|
/**
|
|
11
33
|
* Returns focus to the target element when the popup is hidden. This works well with
|
|
12
34
|
* `useInitialFocus`. This should be used with `useFocusRedirect` or `useFocusTrap` for a complete
|
|
@@ -16,6 +38,7 @@ var usePopupModel_1 = require("./usePopupModel");
|
|
|
16
38
|
*/
|
|
17
39
|
exports.useReturnFocus = common_1.createElemPropsHook(usePopupModel_1.usePopupModel)(function (model) {
|
|
18
40
|
var visible = model.state.visibility !== 'hidden';
|
|
41
|
+
var elementRef = react_1.default.useRef(null);
|
|
19
42
|
// This boolean tracks keyboard-driven focus changes. This is required for `useFocusRedirect` as
|
|
20
43
|
// focus redirection needs synchronous focus management and everything else needs asynchronous
|
|
21
44
|
// focus management.
|
|
@@ -29,17 +52,56 @@ exports.useReturnFocus = common_1.createElemPropsHook(usePopupModel_1.usePopupMo
|
|
|
29
52
|
var onKeyUp = react_1.default.useCallback(function (event) {
|
|
30
53
|
requiresFocusChangeRef.current = false;
|
|
31
54
|
}, []);
|
|
55
|
+
// track mousedown events to determine if the mouse target is a focusable element. If it is, we
|
|
56
|
+
// should not return focus
|
|
57
|
+
var onMouseDown = react_1.default.useCallback(function (event) {
|
|
58
|
+
elementRef.current = event.target;
|
|
59
|
+
}, []);
|
|
60
|
+
var onMouseUp = react_1.default.useCallback(function () {
|
|
61
|
+
elementRef.current = null;
|
|
62
|
+
}, []);
|
|
63
|
+
// We use `useLayoutEffect` because the callback will be called _before_ the browser changes
|
|
64
|
+
// focus. This allows the browser to change focus as normal. For example, if the popup closes
|
|
65
|
+
// because the user hits the tab key when focused on the last focusable element,
|
|
66
|
+
// `useFocusRedirect` will cause the popup to close before the browser is finished processing the
|
|
67
|
+
// `keydown`. The browser's default action of a keydown of a tab key is to advance focus to the
|
|
68
|
+
// next focusable element. This means that the `useFocusRedirect` will change focus back to the
|
|
69
|
+
// target, then the tab key is processed by the browser to focus on the next item after the target
|
|
70
|
+
// without us having to use a focus manager to guess what the next focusable element is.
|
|
71
|
+
//
|
|
72
|
+
// But because we're using `useLayoutEffect` instead of `useEffect`, we cannot use
|
|
73
|
+
// `document.activeElement` (because the focus hasn't changed yet because the browser hasn't
|
|
74
|
+
// triggered a default action yet) to determine if a user clicked on something that can accept
|
|
75
|
+
// focus itself. If we don't short-circuit return focus, the user will intend to click on an
|
|
76
|
+
// `input` element only to get focus changed back. This is annoying to users and unintuitive. Instead,
|
|
77
|
+
// we'll use `mousedown` and `mouseup` to determine if we're closing because of a mouse click.
|
|
32
78
|
react_1.default.useLayoutEffect(function () {
|
|
33
79
|
if (!visible) {
|
|
34
80
|
return;
|
|
35
81
|
}
|
|
36
82
|
// capture the element here. The refs will be null by the time the cleanup function is called
|
|
37
83
|
var element = (model.state.returnFocusRef || model.state.targetRef).current;
|
|
84
|
+
document.addEventListener('mousedown', onMouseDown, true);
|
|
85
|
+
document.addEventListener('mouseup', onMouseUp, true);
|
|
38
86
|
document.addEventListener('keydown', onKeyDown, true);
|
|
39
87
|
document.addEventListener('keyup', onKeyUp, true);
|
|
40
88
|
return function () {
|
|
89
|
+
document.removeEventListener('mousedown', onMouseDown, true);
|
|
90
|
+
document.removeEventListener('mouseup', onMouseUp, true);
|
|
41
91
|
document.removeEventListener('keydown', onKeyDown, true);
|
|
42
92
|
document.removeEventListener('keyup', onKeyUp, true);
|
|
93
|
+
var scrollParent = getScrollParent(element);
|
|
94
|
+
var scrollParentRect = scrollParent.getBoundingClientRect();
|
|
95
|
+
var elementRect = element.getBoundingClientRect();
|
|
96
|
+
// Don't change focus if user focused on a different element like a `input` or the target
|
|
97
|
+
// element isn't on at least halfway rendered on the screen.
|
|
98
|
+
if ((elementRef.current && getFocusableElement(elementRef.current)) ||
|
|
99
|
+
elementRect.top + elementRect.height / 2 < scrollParentRect.top ||
|
|
100
|
+
elementRect.bottom - elementRect.height / 2 > scrollParentRect.bottom ||
|
|
101
|
+
elementRect.left + elementRect.width / 2 < scrollParentRect.left ||
|
|
102
|
+
elementRect.right - elementRect.width / 2 > scrollParentRect.right) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
43
105
|
if (requiresFocusChangeRef.current) {
|
|
44
106
|
// We need to change focus _before_ the browser process the default action of picking a new
|
|
45
107
|
// focus target. Doing this immediately prevents the `focus` event from firing on `element`,
|
|
@@ -53,7 +115,16 @@ exports.useReturnFocus = common_1.createElemPropsHook(usePopupModel_1.usePopupMo
|
|
|
53
115
|
common_1.changeFocus(element);
|
|
54
116
|
});
|
|
55
117
|
}
|
|
118
|
+
elementRef.current = null;
|
|
56
119
|
};
|
|
57
|
-
}, [
|
|
120
|
+
}, [
|
|
121
|
+
model.state.returnFocusRef,
|
|
122
|
+
model.state.targetRef,
|
|
123
|
+
visible,
|
|
124
|
+
onMouseDown,
|
|
125
|
+
onMouseUp,
|
|
126
|
+
onKeyDown,
|
|
127
|
+
onKeyUp,
|
|
128
|
+
]);
|
|
58
129
|
return {};
|
|
59
130
|
});
|
|
@@ -53,7 +53,7 @@ export interface TabsItemProps extends ExtractProps<typeof Box, never>, Partial<
|
|
|
53
53
|
*/
|
|
54
54
|
tabIndex?: number;
|
|
55
55
|
}
|
|
56
|
-
export declare const StyledTabItem: import("@emotion/styled").StyledComponent<import("../../layout").BorderShorthandStyleProps & import("../../layout").BorderColorStyleProps & import("../../layout").BorderRadiusStyleProps & import("../../layout").BorderLineStyleProps & import("../../layout").BorderWidthStyleProps & import("../../layout").BorderLogicalStyleProps & import("../../layout").ColorStyleProps & import("../../layout").DepthStyleProps & import("../../layout").FlexItemStyleProps & import("../../layout").LayoutStyleProps & import("../../layout/lib/utils/other").OtherStyleProps & import("../../layout").PositionStandardProps & import("../../layout").PositionLogicalProps & import("../../layout/lib/utils/space").SpaceStandardProps & import("../../layout/lib/utils/space").SpaceLogicalProps & Pick<React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "name" | "style" | "ref" | "form" | "slot" | "title" | "dir" | "value" | "children" | "hidden" | "
|
|
56
|
+
export declare const StyledTabItem: import("@emotion/styled").StyledComponent<import("../../layout").BorderShorthandStyleProps & import("../../layout").BorderColorStyleProps & import("../../layout").BorderRadiusStyleProps & import("../../layout").BorderLineStyleProps & import("../../layout").BorderWidthStyleProps & import("../../layout").BorderLogicalStyleProps & import("../../layout").ColorStyleProps & import("../../layout").DepthStyleProps & import("../../layout").FlexItemStyleProps & import("../../layout").LayoutStyleProps & import("../../layout/lib/utils/other").OtherStyleProps & import("../../layout").PositionStandardProps & import("../../layout").PositionLogicalProps & import("../../layout/lib/utils/space").SpaceStandardProps & import("../../layout/lib/utils/space").SpaceLogicalProps & Pick<React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "name" | "style" | "ref" | "form" | "slot" | "title" | "dir" | "value" | "children" | "hidden" | "disabled" | "type" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "accessKey" | "className" | "contentEditable" | "contextMenu" | "draggable" | "id" | "lang" | "placeholder" | "spellCheck" | "tabIndex" | "translate" | "radioGroup" | "role" | "about" | "datatype" | "inlist" | "prefix" | "property" | "resource" | "typeof" | "vocab" | "autoCapitalize" | "autoCorrect" | "autoSave" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "results" | "security" | "unselectable" | "inputMode" | "is" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-hidden" | "aria-invalid" | "aria-keyshortcuts" | "aria-label" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "dangerouslySetInnerHTML" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocus" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChange" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmit" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDown" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUp" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClick" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDrag" | "onDragCapture" | "onDragEnd" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStart" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerEnterCapture" | "onPointerLeave" | "onPointerLeaveCapture" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onWheel" | "onWheelCapture" | "onAnimationStart" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onTransitionEnd" | "onTransitionEndCapture" | "key" | "autoFocus" | "formAction" | "formEncType" | "formMethod" | "formNoValidate" | "formTarget"> & {
|
|
57
57
|
ref?: ((instance: HTMLButtonElement | null) => void) | React.RefObject<HTMLButtonElement> | null | undefined;
|
|
58
58
|
} & {
|
|
59
59
|
theme?: import("@emotion/react").Theme | undefined;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Is an element focusable? This function performs various tests to see if the element in question
|
|
3
3
|
* can receive focus. Should skip disabled elements as they are not focusable.
|
|
4
4
|
*/
|
|
5
|
-
export declare const isFocusable: (element:
|
|
5
|
+
export declare const isFocusable: (element: Element) => boolean;
|
|
6
6
|
/**
|
|
7
7
|
* Get the first focusable element in a container.
|
|
8
8
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"elements.d.ts","sourceRoot":"","sources":["../../../../../common/lib/utils/elements.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,WAAW,YAAa,
|
|
1
|
+
{"version":3,"file":"elements.d.ts","sourceRoot":"","sources":["../../../../../common/lib/utils/elements.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,WAAW,YAAa,OAAO,KAAG,OAuB9C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,wBAAwB,cAAe,WAAW,KAAG,WAAW,GAAG,IAW/E,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,uBAAuB,cAAe,WAAW,KAAG,WAAW,GAAG,IAW9E,CAAC"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export var isFocusable = function (element) {
|
|
6
6
|
if (element.hasAttribute('disabled')) {
|
|
7
|
-
return
|
|
7
|
+
return false;
|
|
8
8
|
}
|
|
9
9
|
var nodeName = element.nodeName.toLowerCase();
|
|
10
10
|
var validInput = nodeName === 'input' && element.getAttribute('type') !== 'hidden';
|
|
@@ -28,7 +28,7 @@ export var getFirstFocusableElement = function (container) {
|
|
|
28
28
|
var elements = container.querySelectorAll('*');
|
|
29
29
|
for (var i = 0; i < elements.length; i++) {
|
|
30
30
|
var element = elements.item(i);
|
|
31
|
-
if (element && isFocusable(element)) {
|
|
31
|
+
if (element && isFocusable(element) && element.getAttribute('tabindex') !== '-1') {
|
|
32
32
|
return element;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
@@ -41,7 +41,7 @@ export var getLastFocusableElement = function (container) {
|
|
|
41
41
|
var elements = container.querySelectorAll('*');
|
|
42
42
|
for (var i = elements.length - 1; i >= 0; i--) {
|
|
43
43
|
var element = elements.item(i);
|
|
44
|
-
if (element && isFocusable(element)) {
|
|
44
|
+
if (element && isFocusable(element) && element.getAttribute('tabindex') !== '-1') {
|
|
45
45
|
return element;
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useReturnFocus.d.ts","sourceRoot":"","sources":["../../../../../popup/lib/hooks/useReturnFocus.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"useReturnFocus.d.ts","sourceRoot":"","sources":["../../../../../popup/lib/hooks/useReturnFocus.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAgC1B;;;;;;GAMG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;OAsGzB,CAAC"}
|
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { changeFocus, createElemPropsHook } from '@workday/canvas-kit-react/common';
|
|
2
|
+
import { changeFocus, createElemPropsHook, isFocusable } from '@workday/canvas-kit-react/common';
|
|
3
3
|
import { usePopupModel } from './usePopupModel';
|
|
4
|
+
function getScrollParent(element) {
|
|
5
|
+
if (element === document.body || !element.parentElement) {
|
|
6
|
+
return element;
|
|
7
|
+
}
|
|
8
|
+
var styles = getComputedStyle(element);
|
|
9
|
+
if (styles.overflowY === 'scroll' || styles.overflowY === 'auto') {
|
|
10
|
+
return element;
|
|
11
|
+
}
|
|
12
|
+
return getScrollParent(element.parentElement);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Get a focusable element in an element's tree
|
|
16
|
+
*/
|
|
17
|
+
function getFocusableElement(element) {
|
|
18
|
+
if (element === null || element === document.body || !element.parentElement) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
else if (isFocusable(element)) {
|
|
22
|
+
return element;
|
|
23
|
+
}
|
|
24
|
+
return getFocusableElement(element.parentElement);
|
|
25
|
+
}
|
|
4
26
|
/**
|
|
5
27
|
* Returns focus to the target element when the popup is hidden. This works well with
|
|
6
28
|
* `useInitialFocus`. This should be used with `useFocusRedirect` or `useFocusTrap` for a complete
|
|
@@ -10,6 +32,7 @@ import { usePopupModel } from './usePopupModel';
|
|
|
10
32
|
*/
|
|
11
33
|
export var useReturnFocus = createElemPropsHook(usePopupModel)(function (model) {
|
|
12
34
|
var visible = model.state.visibility !== 'hidden';
|
|
35
|
+
var elementRef = React.useRef(null);
|
|
13
36
|
// This boolean tracks keyboard-driven focus changes. This is required for `useFocusRedirect` as
|
|
14
37
|
// focus redirection needs synchronous focus management and everything else needs asynchronous
|
|
15
38
|
// focus management.
|
|
@@ -23,17 +46,56 @@ export var useReturnFocus = createElemPropsHook(usePopupModel)(function (model)
|
|
|
23
46
|
var onKeyUp = React.useCallback(function (event) {
|
|
24
47
|
requiresFocusChangeRef.current = false;
|
|
25
48
|
}, []);
|
|
49
|
+
// track mousedown events to determine if the mouse target is a focusable element. If it is, we
|
|
50
|
+
// should not return focus
|
|
51
|
+
var onMouseDown = React.useCallback(function (event) {
|
|
52
|
+
elementRef.current = event.target;
|
|
53
|
+
}, []);
|
|
54
|
+
var onMouseUp = React.useCallback(function () {
|
|
55
|
+
elementRef.current = null;
|
|
56
|
+
}, []);
|
|
57
|
+
// We use `useLayoutEffect` because the callback will be called _before_ the browser changes
|
|
58
|
+
// focus. This allows the browser to change focus as normal. For example, if the popup closes
|
|
59
|
+
// because the user hits the tab key when focused on the last focusable element,
|
|
60
|
+
// `useFocusRedirect` will cause the popup to close before the browser is finished processing the
|
|
61
|
+
// `keydown`. The browser's default action of a keydown of a tab key is to advance focus to the
|
|
62
|
+
// next focusable element. This means that the `useFocusRedirect` will change focus back to the
|
|
63
|
+
// target, then the tab key is processed by the browser to focus on the next item after the target
|
|
64
|
+
// without us having to use a focus manager to guess what the next focusable element is.
|
|
65
|
+
//
|
|
66
|
+
// But because we're using `useLayoutEffect` instead of `useEffect`, we cannot use
|
|
67
|
+
// `document.activeElement` (because the focus hasn't changed yet because the browser hasn't
|
|
68
|
+
// triggered a default action yet) to determine if a user clicked on something that can accept
|
|
69
|
+
// focus itself. If we don't short-circuit return focus, the user will intend to click on an
|
|
70
|
+
// `input` element only to get focus changed back. This is annoying to users and unintuitive. Instead,
|
|
71
|
+
// we'll use `mousedown` and `mouseup` to determine if we're closing because of a mouse click.
|
|
26
72
|
React.useLayoutEffect(function () {
|
|
27
73
|
if (!visible) {
|
|
28
74
|
return;
|
|
29
75
|
}
|
|
30
76
|
// capture the element here. The refs will be null by the time the cleanup function is called
|
|
31
77
|
var element = (model.state.returnFocusRef || model.state.targetRef).current;
|
|
78
|
+
document.addEventListener('mousedown', onMouseDown, true);
|
|
79
|
+
document.addEventListener('mouseup', onMouseUp, true);
|
|
32
80
|
document.addEventListener('keydown', onKeyDown, true);
|
|
33
81
|
document.addEventListener('keyup', onKeyUp, true);
|
|
34
82
|
return function () {
|
|
83
|
+
document.removeEventListener('mousedown', onMouseDown, true);
|
|
84
|
+
document.removeEventListener('mouseup', onMouseUp, true);
|
|
35
85
|
document.removeEventListener('keydown', onKeyDown, true);
|
|
36
86
|
document.removeEventListener('keyup', onKeyUp, true);
|
|
87
|
+
var scrollParent = getScrollParent(element);
|
|
88
|
+
var scrollParentRect = scrollParent.getBoundingClientRect();
|
|
89
|
+
var elementRect = element.getBoundingClientRect();
|
|
90
|
+
// Don't change focus if user focused on a different element like a `input` or the target
|
|
91
|
+
// element isn't on at least halfway rendered on the screen.
|
|
92
|
+
if ((elementRef.current && getFocusableElement(elementRef.current)) ||
|
|
93
|
+
elementRect.top + elementRect.height / 2 < scrollParentRect.top ||
|
|
94
|
+
elementRect.bottom - elementRect.height / 2 > scrollParentRect.bottom ||
|
|
95
|
+
elementRect.left + elementRect.width / 2 < scrollParentRect.left ||
|
|
96
|
+
elementRect.right - elementRect.width / 2 > scrollParentRect.right) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
37
99
|
if (requiresFocusChangeRef.current) {
|
|
38
100
|
// We need to change focus _before_ the browser process the default action of picking a new
|
|
39
101
|
// focus target. Doing this immediately prevents the `focus` event from firing on `element`,
|
|
@@ -47,7 +109,16 @@ export var useReturnFocus = createElemPropsHook(usePopupModel)(function (model)
|
|
|
47
109
|
changeFocus(element);
|
|
48
110
|
});
|
|
49
111
|
}
|
|
112
|
+
elementRef.current = null;
|
|
50
113
|
};
|
|
51
|
-
}, [
|
|
114
|
+
}, [
|
|
115
|
+
model.state.returnFocusRef,
|
|
116
|
+
model.state.targetRef,
|
|
117
|
+
visible,
|
|
118
|
+
onMouseDown,
|
|
119
|
+
onMouseUp,
|
|
120
|
+
onKeyDown,
|
|
121
|
+
onKeyUp,
|
|
122
|
+
]);
|
|
52
123
|
return {};
|
|
53
124
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workday/canvas-kit-react",
|
|
3
|
-
"version": "7.3.
|
|
3
|
+
"version": "7.3.16",
|
|
4
4
|
"description": "The parent module that contains all Workday Canvas Kit React components",
|
|
5
5
|
"author": "Workday, Inc. (https://www.workday.com)",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@emotion/styled": "^11.6.0",
|
|
50
50
|
"@popperjs/core": "^2.5.4",
|
|
51
51
|
"@workday/canvas-colors-web": "^2.0.0",
|
|
52
|
-
"@workday/canvas-kit-popup-stack": "^7.3.
|
|
52
|
+
"@workday/canvas-kit-popup-stack": "^7.3.16",
|
|
53
53
|
"@workday/canvas-system-icons-web": "^3.0.0",
|
|
54
54
|
"@workday/design-assets-types": "^0.2.8",
|
|
55
55
|
"chroma-js": "^2.1.0",
|
|
@@ -68,5 +68,5 @@
|
|
|
68
68
|
"@workday/canvas-accent-icons-web": "^3.0.0",
|
|
69
69
|
"@workday/canvas-applet-icons-web": "^2.0.0"
|
|
70
70
|
},
|
|
71
|
-
"gitHead": "
|
|
71
|
+
"gitHead": "a5be6896cbc5baaeb6a6b0ba2ea40a7ab3b2315b"
|
|
72
72
|
}
|
|
@@ -1,9 +1,35 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
-
import {changeFocus, createElemPropsHook} from '@workday/canvas-kit-react/common';
|
|
3
|
+
import {changeFocus, createElemPropsHook, isFocusable} from '@workday/canvas-kit-react/common';
|
|
4
4
|
|
|
5
5
|
import {usePopupModel} from './usePopupModel';
|
|
6
6
|
|
|
7
|
+
function getScrollParent(element: HTMLElement): HTMLElement {
|
|
8
|
+
if (element === document.body || !element.parentElement) {
|
|
9
|
+
return element;
|
|
10
|
+
}
|
|
11
|
+
const styles = getComputedStyle(element);
|
|
12
|
+
|
|
13
|
+
if (styles.overflowY === 'scroll' || styles.overflowY === 'auto') {
|
|
14
|
+
return element;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return getScrollParent(element.parentElement);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get a focusable element in an element's tree
|
|
22
|
+
*/
|
|
23
|
+
function getFocusableElement(element: Element | null): Element | null {
|
|
24
|
+
if (element === null || element === document.body || !element.parentElement) {
|
|
25
|
+
return null;
|
|
26
|
+
} else if (isFocusable(element)) {
|
|
27
|
+
return element;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return getFocusableElement(element.parentElement);
|
|
31
|
+
}
|
|
32
|
+
|
|
7
33
|
/**
|
|
8
34
|
* Returns focus to the target element when the popup is hidden. This works well with
|
|
9
35
|
* `useInitialFocus`. This should be used with `useFocusRedirect` or `useFocusTrap` for a complete
|
|
@@ -13,6 +39,7 @@ import {usePopupModel} from './usePopupModel';
|
|
|
13
39
|
*/
|
|
14
40
|
export const useReturnFocus = createElemPropsHook(usePopupModel)(model => {
|
|
15
41
|
const visible = model.state.visibility !== 'hidden';
|
|
42
|
+
const elementRef = React.useRef<Element | null>(null);
|
|
16
43
|
|
|
17
44
|
// This boolean tracks keyboard-driven focus changes. This is required for `useFocusRedirect` as
|
|
18
45
|
// focus redirection needs synchronous focus management and everything else needs asynchronous
|
|
@@ -30,19 +57,62 @@ export const useReturnFocus = createElemPropsHook(usePopupModel)(model => {
|
|
|
30
57
|
requiresFocusChangeRef.current = false;
|
|
31
58
|
}, []);
|
|
32
59
|
|
|
60
|
+
// track mousedown events to determine if the mouse target is a focusable element. If it is, we
|
|
61
|
+
// should not return focus
|
|
62
|
+
const onMouseDown = React.useCallback((event: MouseEvent) => {
|
|
63
|
+
elementRef.current = event.target as Element;
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
const onMouseUp = React.useCallback(() => {
|
|
67
|
+
elementRef.current = null;
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
// We use `useLayoutEffect` because the callback will be called _before_ the browser changes
|
|
71
|
+
// focus. This allows the browser to change focus as normal. For example, if the popup closes
|
|
72
|
+
// because the user hits the tab key when focused on the last focusable element,
|
|
73
|
+
// `useFocusRedirect` will cause the popup to close before the browser is finished processing the
|
|
74
|
+
// `keydown`. The browser's default action of a keydown of a tab key is to advance focus to the
|
|
75
|
+
// next focusable element. This means that the `useFocusRedirect` will change focus back to the
|
|
76
|
+
// target, then the tab key is processed by the browser to focus on the next item after the target
|
|
77
|
+
// without us having to use a focus manager to guess what the next focusable element is.
|
|
78
|
+
//
|
|
79
|
+
// But because we're using `useLayoutEffect` instead of `useEffect`, we cannot use
|
|
80
|
+
// `document.activeElement` (because the focus hasn't changed yet because the browser hasn't
|
|
81
|
+
// triggered a default action yet) to determine if a user clicked on something that can accept
|
|
82
|
+
// focus itself. If we don't short-circuit return focus, the user will intend to click on an
|
|
83
|
+
// `input` element only to get focus changed back. This is annoying to users and unintuitive. Instead,
|
|
84
|
+
// we'll use `mousedown` and `mouseup` to determine if we're closing because of a mouse click.
|
|
33
85
|
React.useLayoutEffect(() => {
|
|
34
86
|
if (!visible) {
|
|
35
87
|
return;
|
|
36
88
|
}
|
|
37
89
|
// capture the element here. The refs will be null by the time the cleanup function is called
|
|
38
90
|
const element = (model.state.returnFocusRef || model.state.targetRef).current as HTMLElement;
|
|
39
|
-
|
|
91
|
+
document.addEventListener('mousedown', onMouseDown, true);
|
|
92
|
+
document.addEventListener('mouseup', onMouseUp, true);
|
|
40
93
|
document.addEventListener('keydown', onKeyDown, true);
|
|
41
94
|
document.addEventListener('keyup', onKeyUp, true);
|
|
42
95
|
|
|
43
96
|
return () => {
|
|
97
|
+
document.removeEventListener('mousedown', onMouseDown, true);
|
|
98
|
+
document.removeEventListener('mouseup', onMouseUp, true);
|
|
44
99
|
document.removeEventListener('keydown', onKeyDown, true);
|
|
45
100
|
document.removeEventListener('keyup', onKeyUp, true);
|
|
101
|
+
const scrollParent = getScrollParent(element);
|
|
102
|
+
const scrollParentRect = scrollParent.getBoundingClientRect();
|
|
103
|
+
const elementRect = element.getBoundingClientRect();
|
|
104
|
+
|
|
105
|
+
// Don't change focus if user focused on a different element like a `input` or the target
|
|
106
|
+
// element isn't on at least halfway rendered on the screen.
|
|
107
|
+
if (
|
|
108
|
+
(elementRef.current && getFocusableElement(elementRef.current)) ||
|
|
109
|
+
elementRect.top + elementRect.height / 2 < scrollParentRect.top ||
|
|
110
|
+
elementRect.bottom - elementRect.height / 2 > scrollParentRect.bottom ||
|
|
111
|
+
elementRect.left + elementRect.width / 2 < scrollParentRect.left ||
|
|
112
|
+
elementRect.right - elementRect.width / 2 > scrollParentRect.right
|
|
113
|
+
) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
46
116
|
|
|
47
117
|
if (requiresFocusChangeRef.current) {
|
|
48
118
|
// We need to change focus _before_ the browser process the default action of picking a new
|
|
@@ -56,8 +126,17 @@ export const useReturnFocus = createElemPropsHook(usePopupModel)(model => {
|
|
|
56
126
|
changeFocus(element);
|
|
57
127
|
});
|
|
58
128
|
}
|
|
129
|
+
elementRef.current = null;
|
|
59
130
|
};
|
|
60
|
-
}, [
|
|
131
|
+
}, [
|
|
132
|
+
model.state.returnFocusRef,
|
|
133
|
+
model.state.targetRef,
|
|
134
|
+
visible,
|
|
135
|
+
onMouseDown,
|
|
136
|
+
onMouseUp,
|
|
137
|
+
onKeyDown,
|
|
138
|
+
onKeyUp,
|
|
139
|
+
]);
|
|
61
140
|
|
|
62
141
|
return {};
|
|
63
142
|
});
|