@workday/canvas-kit-react 7.3.14 → 7.3.15

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.
@@ -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: HTMLElement) => {
5
+ export const isFocusable = (element: Element): boolean => {
6
6
  if (element.hasAttribute('disabled')) {
7
- return null;
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 as HTMLElement)) {
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 as HTMLElement)) {
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: HTMLElement) => boolean | null;
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,WAAW,mBAuB/C,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"}
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 null;
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;AAM1B;;;;;;GAMG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;OAiDzB,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
- }, [model.state.returnFocusRef, model.state.targetRef, visible, onKeyDown, onKeyUp]);
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" | "type" | "disabled" | "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"> & {
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: HTMLElement) => boolean | null;
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,WAAW,mBAuB/C,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"}
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 null;
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;AAM1B;;;;;;GAMG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;OAiDzB,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
- }, [model.state.returnFocusRef, model.state.targetRef, visible, onKeyDown, onKeyUp]);
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.14",
3
+ "version": "7.3.15",
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.14",
52
+ "@workday/canvas-kit-popup-stack": "^7.3.15",
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": "1e0bf2d1e55b84039c81e0ff505ff2c02d930b79"
71
+ "gitHead": "fd5ab8a8e931a200a15f7fa2640f917805b044e9"
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
- }, [model.state.returnFocusRef, model.state.targetRef, visible, onKeyDown, onKeyUp]);
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
  });