@elliemae/ds-hooks-focus-stack 3.70.0-next.9 → 3.70.0-rc.1

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.
@@ -40,12 +40,37 @@ var import_react_desc_prop_types2 = require("./react-desc-prop-types.js");
40
40
  const useFocusStack = (props) => {
41
41
  const { fallBackRef } = props;
42
42
  const [stack, setStack] = (0, import_react.useState)([]);
43
+ const lastInteractionRef = (0, import_react.useRef)("keyboard");
44
+ (0, import_react.useEffect)(() => {
45
+ const onPointerDown = () => {
46
+ lastInteractionRef.current = "pointer";
47
+ };
48
+ const onKeyDown = (e) => {
49
+ if (e.key === "Tab") lastInteractionRef.current = "keyboard";
50
+ };
51
+ document.addEventListener("pointerdown", onPointerDown, true);
52
+ document.addEventListener("keydown", onKeyDown, true);
53
+ return () => {
54
+ document.removeEventListener("pointerdown", onPointerDown, true);
55
+ document.removeEventListener("keydown", onKeyDown, true);
56
+ };
57
+ }, []);
58
+ const isRestoringRef = (0, import_react.useRef)(false);
43
59
  const handleOnFocus = (0, import_react.useCallback)(
44
60
  (e) => {
45
- if (e.target !== stack[0] && e.relatedTarget) setStack((prev) => [e.relatedTarget, ...prev]);
61
+ if (isRestoringRef.current) return;
62
+ const previous = e.relatedTarget;
63
+ if (previous && previous === fallBackRef.current) return;
64
+ if (lastInteractionRef.current === "pointer") {
65
+ if (previous && previous !== e.target && e.currentTarget.contains(previous)) {
66
+ setStack((prev) => [previous, ...prev]);
67
+ }
68
+ return;
69
+ }
70
+ if (e.target !== stack[0] && previous) setStack((prev) => [previous, ...prev]);
46
71
  else setStack((prev) => prev.length > 0 ? prev.slice(1) : prev);
47
72
  },
48
- [stack]
73
+ [stack, fallBackRef]
49
74
  );
50
75
  const config = (0, import_react.useMemo)(
51
76
  () => ({
@@ -55,10 +80,19 @@ const useFocusStack = (props) => {
55
80
  );
56
81
  const handleOnBlur = (0, import_ds_hooks_on_blur_out.useOnBlurOut)(config);
57
82
  const focusLastElement = (0, import_react.useCallback)(() => {
58
- if (stack.length > 0 && stack[0]?.checkVisibility()) {
59
- stack[0]?.focus();
83
+ const beingRemoved = typeof document === "undefined" ? null : document.activeElement;
84
+ const remaining = stack.filter((el) => el !== beingRemoved && el?.checkVisibility());
85
+ if (remaining.length > 0) {
86
+ const [target, ...rest] = remaining;
87
+ setStack(rest);
88
+ isRestoringRef.current = true;
89
+ target?.focus();
90
+ isRestoringRef.current = false;
60
91
  } else {
92
+ setStack([]);
93
+ isRestoringRef.current = true;
61
94
  fallBackRef.current?.focus();
95
+ isRestoringRef.current = false;
62
96
  }
63
97
  }, [fallBackRef, stack]);
64
98
  return (0, import_react.useMemo)(
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/DSHooksFocusStack.tsx", "../../../../../scripts/build/transpile/react-shim.js"],
4
- "sourcesContent": ["import { useCallback, useMemo, useState } from 'react';\nimport { useOnBlurOut } from '@elliemae/ds-hooks-on-blur-out';\nimport { describe, PropTypes } from '@elliemae/ds-props-helpers';\nimport { type DSHooksFocusStackT } from './react-desc-prop-types.js';\nimport { propTypes } from './react-desc-prop-types.js';\nconst useFocusStack = (props: DSHooksFocusStackT.Props) => {\n const { fallBackRef } = props;\n const [stack, setStack] = useState<HTMLElement[]>([]);\n\n const handleOnFocus: React.FocusEventHandler<HTMLElement> = useCallback(\n (e) => {\n // check if the new focused element is not the first element in the stack\n // if it is, remove it from the stack\n if (e.target !== stack[0] && e.relatedTarget) setStack((prev) => [e.relatedTarget as HTMLElement, ...prev]);\n else setStack((prev) => (prev.length > 0 ? prev.slice(1) : prev));\n },\n [stack],\n );\n\n const config = useMemo(\n () => ({\n onBlur: () => setStack([]),\n }),\n [],\n );\n const handleOnBlur = useOnBlurOut(config);\n\n const focusLastElement = useCallback(() => {\n if (stack.length > 0 && stack[0]?.checkVisibility()) {\n stack[0]?.focus();\n } else {\n fallBackRef.current?.focus();\n }\n }, [fallBackRef, stack]);\n\n return useMemo(\n () => ({ trackFocus: handleOnFocus, cleanStack: handleOnBlur, stack, focusLastElement }),\n [handleOnFocus, handleOnBlur, stack, focusLastElement],\n );\n};\n\nconst UseFocusStackWithSchema = describe(useFocusStack);\nUseFocusStackWithSchema.propTypes = propTypes;\nUseFocusStackWithSchema.returnType = {\n stack: PropTypes.object.description('The stack of elements that have been focused').isRequired,\n trackFocus: PropTypes.func\n .description('The function to be invoked when focus enters the wrapping container')\n .signature('((e: React.FocusEvent<HTMLElement>) => void)').isRequired,\n cleanStack: PropTypes.func\n .description('The function to be invoked when focus exits the wrapping container')\n .signature('((e: React.FocusEvent<HTMLElement>) => void)').isRequired,\n focusLastElement: PropTypes.func.description('Focuses the last element in the stack').signature('() => void')\n .isRequired,\n};\nexport { useFocusStack, UseFocusStackWithSchema };\n", "import * as React from 'react';\nexport { React };\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ACAA,YAAuB;ADAvB,mBAA+C;AAC/C,kCAA6B;AAC7B,8BAAoC;AAEpC,IAAAA,gCAA0B;AAC1B,MAAM,gBAAgB,CAAC,UAAoC;AACzD,QAAM,EAAE,YAAY,IAAI;AACxB,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAAwB,CAAC,CAAC;AAEpD,QAAM,oBAAsD;AAAA,IAC1D,CAAC,MAAM;AAGL,UAAI,EAAE,WAAW,MAAM,CAAC,KAAK,EAAE,cAAe,UAAS,CAAC,SAAS,CAAC,EAAE,eAA8B,GAAG,IAAI,CAAC;AAAA,UACrG,UAAS,CAAC,SAAU,KAAK,SAAS,IAAI,KAAK,MAAM,CAAC,IAAI,IAAK;AAAA,IAClE;AAAA,IACA,CAAC,KAAK;AAAA,EACR;AAEA,QAAM,aAAS;AAAA,IACb,OAAO;AAAA,MACL,QAAQ,MAAM,SAAS,CAAC,CAAC;AAAA,IAC3B;AAAA,IACA,CAAC;AAAA,EACH;AACA,QAAM,mBAAe,0CAAa,MAAM;AAExC,QAAM,uBAAmB,0BAAY,MAAM;AACzC,QAAI,MAAM,SAAS,KAAK,MAAM,CAAC,GAAG,gBAAgB,GAAG;AACnD,YAAM,CAAC,GAAG,MAAM;AAAA,IAClB,OAAO;AACL,kBAAY,SAAS,MAAM;AAAA,IAC7B;AAAA,EACF,GAAG,CAAC,aAAa,KAAK,CAAC;AAEvB,aAAO;AAAA,IACL,OAAO,EAAE,YAAY,eAAe,YAAY,cAAc,OAAO,iBAAiB;AAAA,IACtF,CAAC,eAAe,cAAc,OAAO,gBAAgB;AAAA,EACvD;AACF;AAEA,MAAM,8BAA0B,kCAAS,aAAa;AACtD,wBAAwB,YAAY;AACpC,wBAAwB,aAAa;AAAA,EACnC,OAAO,kCAAU,OAAO,YAAY,8CAA8C,EAAE;AAAA,EACpF,YAAY,kCAAU,KACnB,YAAY,qEAAqE,EACjF,UAAU,8CAA8C,EAAE;AAAA,EAC7D,YAAY,kCAAU,KACnB,YAAY,oEAAoE,EAChF,UAAU,8CAA8C,EAAE;AAAA,EAC7D,kBAAkB,kCAAU,KAAK,YAAY,uCAAuC,EAAE,UAAU,YAAY,EACzG;AACL;",
4
+ "sourcesContent": ["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useOnBlurOut } from '@elliemae/ds-hooks-on-blur-out';\nimport { describe, PropTypes } from '@elliemae/ds-props-helpers';\nimport { type DSHooksFocusStackT } from './react-desc-prop-types.js';\nimport { propTypes } from './react-desc-prop-types.js';\nconst useFocusStack = (props: DSHooksFocusStackT.Props) => {\n const { fallBackRef } = props;\n const [stack, setStack] = useState<HTMLElement[]>([]);\n\n // A keyboard shift+Tab back onto the previous element and a pointer click that lands on that\n // same element emit an IDENTICAL focus event (same target, same relatedTarget, same stack\n // state). Treating the click as a backward navigation \u2014 and popping \u2014 was the PUI-13301\n // defect. The last input modality is the only reliable way to tell them apart, so we track it\n // via document-level listeners (a legitimate external-subscription useEffect).\n const lastInteractionRef = useRef<'keyboard' | 'pointer'>('keyboard');\n useEffect(() => {\n const onPointerDown = () => {\n lastInteractionRef.current = 'pointer';\n };\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Tab') lastInteractionRef.current = 'keyboard';\n };\n document.addEventListener('pointerdown', onPointerDown, true);\n document.addEventListener('keydown', onKeyDown, true);\n return () => {\n document.removeEventListener('pointerdown', onPointerDown, true);\n document.removeEventListener('keydown', onKeyDown, true);\n };\n }, []);\n\n // Guards the programmatic focus() in focusLastElement so restoring focus does not re-enter\n // handleOnFocus and record the element we are returning to as a fresh navigation.\n const isRestoringRef = useRef(false);\n\n const handleOnFocus: React.FocusEventHandler<HTMLElement> = useCallback(\n (e) => {\n if (isRestoringRef.current) return;\n const previous = e.relatedTarget as HTMLElement | null;\n\n // The fallback is a safety net, not tracked history: never record it as a came-from (e.g.\n // Shift+Tab off the Reset button), so it can't surface in the exposed stack. PUI-13301.\n if (previous && previous === fallBackRef.current) return;\n\n if (lastInteractionRef.current === 'pointer') {\n // Pointer landed on an element (the click-to-remove pattern): record where focus came\n // from so removing the clicked element returns focus there. Scoped to the container so\n // entering from outside is not tracked.\n if (previous && previous !== e.target && e.currentTarget.contains(previous)) {\n setStack((prev) => [previous, ...prev]);\n }\n return;\n }\n\n // Keyboard navigation: the original stack behavior, unchanged \u2014 push the element focus\n // left when moving forward, pop when returning to the element on top (shift+Tab).\n if (e.target !== stack[0] && previous) setStack((prev) => [previous, ...prev]);\n else setStack((prev) => (prev.length > 0 ? prev.slice(1) : prev));\n },\n [stack, fallBackRef],\n );\n\n const config = useMemo(\n () => ({\n onBlur: () => setStack([]),\n }),\n [],\n );\n const handleOnBlur = useOnBlurOut(config);\n\n const focusLastElement = useCallback(() => {\n // Return focus to the most-recent surviving element in the history. Exclude the element\n // being removed \u2014 it still holds focus at this point (document.activeElement), and its\n // unmount has not committed yet, so checkVisibility() alone would not filter it \u2014 and any\n // element that is no longer present.\n const beingRemoved = typeof document === 'undefined' ? null : document.activeElement;\n const remaining = stack.filter((el) => el !== beingRemoved && el?.checkVisibility());\n if (remaining.length > 0) {\n const [target, ...rest] = remaining;\n // Consume the element we are returning to, so the next removal walks one step further\n // back along the focus history.\n setStack(rest);\n isRestoringRef.current = true;\n target?.focus();\n isRestoringRef.current = false;\n } else {\n setStack([]);\n // The fallback focus is a programmatic restore too, so guard it exactly like the branch\n // above: focusing fallBackRef re-fires the container onFocus with the just-removed element\n // still as relatedTarget, and without this guard the keyboard branch would re-record it as a\n // fresh navigation \u2014 leaving a phantom entry in the history (PUI-13301 QA re-open).\n isRestoringRef.current = true;\n fallBackRef.current?.focus();\n isRestoringRef.current = false;\n }\n }, [fallBackRef, stack]);\n\n return useMemo(\n () => ({ trackFocus: handleOnFocus, cleanStack: handleOnBlur, stack, focusLastElement }),\n [handleOnFocus, handleOnBlur, stack, focusLastElement],\n );\n};\n\nconst UseFocusStackWithSchema = describe(useFocusStack);\nUseFocusStackWithSchema.propTypes = propTypes;\nUseFocusStackWithSchema.returnType = {\n stack: PropTypes.object.description('The stack of elements that have been focused').isRequired,\n trackFocus: PropTypes.func\n .description('The function to be invoked when focus enters the wrapping container')\n .signature('((e: React.FocusEvent<HTMLElement>) => void)').isRequired,\n cleanStack: PropTypes.func\n .description('The function to be invoked when focus exits the wrapping container')\n .signature('((e: React.FocusEvent<HTMLElement>) => void)').isRequired,\n focusLastElement: PropTypes.func.description('Focuses the last element in the stack').signature('() => void')\n .isRequired,\n};\nexport { useFocusStack, UseFocusStackWithSchema };\n", "import * as React from 'react';\nexport { React };\n"],
5
+ "mappings": "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",
6
6
  "names": ["import_react_desc_prop_types"]
7
7
  }
@@ -3,8 +3,5 @@
3
3
  "sideEffects": [
4
4
  "*.css",
5
5
  "*.scss"
6
- ],
7
- "publishConfig": {
8
- "access": "public"
9
- }
6
+ ]
10
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/react-desc-prop-types.ts", "../../../../../scripts/build/transpile/react-shim.js"],
4
- "sourcesContent": ["/* eslint-disable @typescript-eslint/no-empty-interface */\n\nimport type React from 'react';\nimport { PropTypes } from '@elliemae/ds-props-helpers';\nexport declare namespace DSHooksFocusStackT {\n interface Props {\n fallBackRef: React.RefObject<HTMLElement>;\n }\n}\n\nexport const propTypes = {\n fallBackRef: PropTypes.object.description('The fallback ref to focus when the stack is empty').isRequired,\n} as React.WeakValidationMap<unknown>;\n", "import * as React from 'react';\nexport { React };\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;ACAA,YAAuB;ADGvB,8BAA0B;AAOnB,MAAM,YAAY;AAAA,EACvB,aAAa,kCAAU,OAAO,YAAY,mDAAmD,EAAE;AACjG;",
4
+ "sourcesContent": ["import type React from 'react';\nimport { PropTypes } from '@elliemae/ds-props-helpers';\nexport declare namespace DSHooksFocusStackT {\n interface Props {\n fallBackRef: React.RefObject<HTMLElement>;\n }\n}\n\nexport const propTypes = {\n fallBackRef: PropTypes.object.description('The fallback ref to focus when the stack is empty').isRequired,\n} as React.WeakValidationMap<unknown>;\n", "import * as React from 'react';\nexport { React };\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;ACAA,YAAuB;ADCvB,8BAA0B;AAOnB,MAAM,YAAY;AAAA,EACvB,aAAa,kCAAU,OAAO,YAAY,mDAAmD,EAAE;AACjG;",
6
6
  "names": []
7
7
  }
@@ -1,17 +1,42 @@
1
1
  import * as React from "react";
2
- import { useCallback, useMemo, useState } from "react";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { useOnBlurOut } from "@elliemae/ds-hooks-on-blur-out";
4
4
  import { describe, PropTypes } from "@elliemae/ds-props-helpers";
5
5
  import { propTypes } from "./react-desc-prop-types.js";
6
6
  const useFocusStack = (props) => {
7
7
  const { fallBackRef } = props;
8
8
  const [stack, setStack] = useState([]);
9
+ const lastInteractionRef = useRef("keyboard");
10
+ useEffect(() => {
11
+ const onPointerDown = () => {
12
+ lastInteractionRef.current = "pointer";
13
+ };
14
+ const onKeyDown = (e) => {
15
+ if (e.key === "Tab") lastInteractionRef.current = "keyboard";
16
+ };
17
+ document.addEventListener("pointerdown", onPointerDown, true);
18
+ document.addEventListener("keydown", onKeyDown, true);
19
+ return () => {
20
+ document.removeEventListener("pointerdown", onPointerDown, true);
21
+ document.removeEventListener("keydown", onKeyDown, true);
22
+ };
23
+ }, []);
24
+ const isRestoringRef = useRef(false);
9
25
  const handleOnFocus = useCallback(
10
26
  (e) => {
11
- if (e.target !== stack[0] && e.relatedTarget) setStack((prev) => [e.relatedTarget, ...prev]);
27
+ if (isRestoringRef.current) return;
28
+ const previous = e.relatedTarget;
29
+ if (previous && previous === fallBackRef.current) return;
30
+ if (lastInteractionRef.current === "pointer") {
31
+ if (previous && previous !== e.target && e.currentTarget.contains(previous)) {
32
+ setStack((prev) => [previous, ...prev]);
33
+ }
34
+ return;
35
+ }
36
+ if (e.target !== stack[0] && previous) setStack((prev) => [previous, ...prev]);
12
37
  else setStack((prev) => prev.length > 0 ? prev.slice(1) : prev);
13
38
  },
14
- [stack]
39
+ [stack, fallBackRef]
15
40
  );
16
41
  const config = useMemo(
17
42
  () => ({
@@ -21,10 +46,19 @@ const useFocusStack = (props) => {
21
46
  );
22
47
  const handleOnBlur = useOnBlurOut(config);
23
48
  const focusLastElement = useCallback(() => {
24
- if (stack.length > 0 && stack[0]?.checkVisibility()) {
25
- stack[0]?.focus();
49
+ const beingRemoved = typeof document === "undefined" ? null : document.activeElement;
50
+ const remaining = stack.filter((el) => el !== beingRemoved && el?.checkVisibility());
51
+ if (remaining.length > 0) {
52
+ const [target, ...rest] = remaining;
53
+ setStack(rest);
54
+ isRestoringRef.current = true;
55
+ target?.focus();
56
+ isRestoringRef.current = false;
26
57
  } else {
58
+ setStack([]);
59
+ isRestoringRef.current = true;
27
60
  fallBackRef.current?.focus();
61
+ isRestoringRef.current = false;
28
62
  }
29
63
  }, [fallBackRef, stack]);
30
64
  return useMemo(
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../scripts/build/transpile/react-shim.js", "../../src/DSHooksFocusStack.tsx"],
4
- "sourcesContent": ["import * as React from 'react';\nexport { React };\n", "import { useCallback, useMemo, useState } from 'react';\nimport { useOnBlurOut } from '@elliemae/ds-hooks-on-blur-out';\nimport { describe, PropTypes } from '@elliemae/ds-props-helpers';\nimport { type DSHooksFocusStackT } from './react-desc-prop-types.js';\nimport { propTypes } from './react-desc-prop-types.js';\nconst useFocusStack = (props: DSHooksFocusStackT.Props) => {\n const { fallBackRef } = props;\n const [stack, setStack] = useState<HTMLElement[]>([]);\n\n const handleOnFocus: React.FocusEventHandler<HTMLElement> = useCallback(\n (e) => {\n // check if the new focused element is not the first element in the stack\n // if it is, remove it from the stack\n if (e.target !== stack[0] && e.relatedTarget) setStack((prev) => [e.relatedTarget as HTMLElement, ...prev]);\n else setStack((prev) => (prev.length > 0 ? prev.slice(1) : prev));\n },\n [stack],\n );\n\n const config = useMemo(\n () => ({\n onBlur: () => setStack([]),\n }),\n [],\n );\n const handleOnBlur = useOnBlurOut(config);\n\n const focusLastElement = useCallback(() => {\n if (stack.length > 0 && stack[0]?.checkVisibility()) {\n stack[0]?.focus();\n } else {\n fallBackRef.current?.focus();\n }\n }, [fallBackRef, stack]);\n\n return useMemo(\n () => ({ trackFocus: handleOnFocus, cleanStack: handleOnBlur, stack, focusLastElement }),\n [handleOnFocus, handleOnBlur, stack, focusLastElement],\n );\n};\n\nconst UseFocusStackWithSchema = describe(useFocusStack);\nUseFocusStackWithSchema.propTypes = propTypes;\nUseFocusStackWithSchema.returnType = {\n stack: PropTypes.object.description('The stack of elements that have been focused').isRequired,\n trackFocus: PropTypes.func\n .description('The function to be invoked when focus enters the wrapping container')\n .signature('((e: React.FocusEvent<HTMLElement>) => void)').isRequired,\n cleanStack: PropTypes.func\n .description('The function to be invoked when focus exits the wrapping container')\n .signature('((e: React.FocusEvent<HTMLElement>) => void)').isRequired,\n focusLastElement: PropTypes.func.description('Focuses the last element in the stack').signature('() => void')\n .isRequired,\n};\nexport { useFocusStack, UseFocusStackWithSchema };\n"],
5
- "mappings": "AAAA,YAAY,WAAW;ACAvB,SAAS,aAAa,SAAS,gBAAgB;AAC/C,SAAS,oBAAoB;AAC7B,SAAS,UAAU,iBAAiB;AAEpC,SAAS,iBAAiB;AAC1B,MAAM,gBAAgB,CAAC,UAAoC;AACzD,QAAM,EAAE,YAAY,IAAI;AACxB,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,CAAC,CAAC;AAEpD,QAAM,gBAAsD;AAAA,IAC1D,CAAC,MAAM;AAGL,UAAI,EAAE,WAAW,MAAM,CAAC,KAAK,EAAE,cAAe,UAAS,CAAC,SAAS,CAAC,EAAE,eAA8B,GAAG,IAAI,CAAC;AAAA,UACrG,UAAS,CAAC,SAAU,KAAK,SAAS,IAAI,KAAK,MAAM,CAAC,IAAI,IAAK;AAAA,IAClE;AAAA,IACA,CAAC,KAAK;AAAA,EACR;AAEA,QAAM,SAAS;AAAA,IACb,OAAO;AAAA,MACL,QAAQ,MAAM,SAAS,CAAC,CAAC;AAAA,IAC3B;AAAA,IACA,CAAC;AAAA,EACH;AACA,QAAM,eAAe,aAAa,MAAM;AAExC,QAAM,mBAAmB,YAAY,MAAM;AACzC,QAAI,MAAM,SAAS,KAAK,MAAM,CAAC,GAAG,gBAAgB,GAAG;AACnD,YAAM,CAAC,GAAG,MAAM;AAAA,IAClB,OAAO;AACL,kBAAY,SAAS,MAAM;AAAA,IAC7B;AAAA,EACF,GAAG,CAAC,aAAa,KAAK,CAAC;AAEvB,SAAO;AAAA,IACL,OAAO,EAAE,YAAY,eAAe,YAAY,cAAc,OAAO,iBAAiB;AAAA,IACtF,CAAC,eAAe,cAAc,OAAO,gBAAgB;AAAA,EACvD;AACF;AAEA,MAAM,0BAA0B,SAAS,aAAa;AACtD,wBAAwB,YAAY;AACpC,wBAAwB,aAAa;AAAA,EACnC,OAAO,UAAU,OAAO,YAAY,8CAA8C,EAAE;AAAA,EACpF,YAAY,UAAU,KACnB,YAAY,qEAAqE,EACjF,UAAU,8CAA8C,EAAE;AAAA,EAC7D,YAAY,UAAU,KACnB,YAAY,oEAAoE,EAChF,UAAU,8CAA8C,EAAE;AAAA,EAC7D,kBAAkB,UAAU,KAAK,YAAY,uCAAuC,EAAE,UAAU,YAAY,EACzG;AACL;",
4
+ "sourcesContent": ["import * as React from 'react';\nexport { React };\n", "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useOnBlurOut } from '@elliemae/ds-hooks-on-blur-out';\nimport { describe, PropTypes } from '@elliemae/ds-props-helpers';\nimport { type DSHooksFocusStackT } from './react-desc-prop-types.js';\nimport { propTypes } from './react-desc-prop-types.js';\nconst useFocusStack = (props: DSHooksFocusStackT.Props) => {\n const { fallBackRef } = props;\n const [stack, setStack] = useState<HTMLElement[]>([]);\n\n // A keyboard shift+Tab back onto the previous element and a pointer click that lands on that\n // same element emit an IDENTICAL focus event (same target, same relatedTarget, same stack\n // state). Treating the click as a backward navigation \u2014 and popping \u2014 was the PUI-13301\n // defect. The last input modality is the only reliable way to tell them apart, so we track it\n // via document-level listeners (a legitimate external-subscription useEffect).\n const lastInteractionRef = useRef<'keyboard' | 'pointer'>('keyboard');\n useEffect(() => {\n const onPointerDown = () => {\n lastInteractionRef.current = 'pointer';\n };\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Tab') lastInteractionRef.current = 'keyboard';\n };\n document.addEventListener('pointerdown', onPointerDown, true);\n document.addEventListener('keydown', onKeyDown, true);\n return () => {\n document.removeEventListener('pointerdown', onPointerDown, true);\n document.removeEventListener('keydown', onKeyDown, true);\n };\n }, []);\n\n // Guards the programmatic focus() in focusLastElement so restoring focus does not re-enter\n // handleOnFocus and record the element we are returning to as a fresh navigation.\n const isRestoringRef = useRef(false);\n\n const handleOnFocus: React.FocusEventHandler<HTMLElement> = useCallback(\n (e) => {\n if (isRestoringRef.current) return;\n const previous = e.relatedTarget as HTMLElement | null;\n\n // The fallback is a safety net, not tracked history: never record it as a came-from (e.g.\n // Shift+Tab off the Reset button), so it can't surface in the exposed stack. PUI-13301.\n if (previous && previous === fallBackRef.current) return;\n\n if (lastInteractionRef.current === 'pointer') {\n // Pointer landed on an element (the click-to-remove pattern): record where focus came\n // from so removing the clicked element returns focus there. Scoped to the container so\n // entering from outside is not tracked.\n if (previous && previous !== e.target && e.currentTarget.contains(previous)) {\n setStack((prev) => [previous, ...prev]);\n }\n return;\n }\n\n // Keyboard navigation: the original stack behavior, unchanged \u2014 push the element focus\n // left when moving forward, pop when returning to the element on top (shift+Tab).\n if (e.target !== stack[0] && previous) setStack((prev) => [previous, ...prev]);\n else setStack((prev) => (prev.length > 0 ? prev.slice(1) : prev));\n },\n [stack, fallBackRef],\n );\n\n const config = useMemo(\n () => ({\n onBlur: () => setStack([]),\n }),\n [],\n );\n const handleOnBlur = useOnBlurOut(config);\n\n const focusLastElement = useCallback(() => {\n // Return focus to the most-recent surviving element in the history. Exclude the element\n // being removed \u2014 it still holds focus at this point (document.activeElement), and its\n // unmount has not committed yet, so checkVisibility() alone would not filter it \u2014 and any\n // element that is no longer present.\n const beingRemoved = typeof document === 'undefined' ? null : document.activeElement;\n const remaining = stack.filter((el) => el !== beingRemoved && el?.checkVisibility());\n if (remaining.length > 0) {\n const [target, ...rest] = remaining;\n // Consume the element we are returning to, so the next removal walks one step further\n // back along the focus history.\n setStack(rest);\n isRestoringRef.current = true;\n target?.focus();\n isRestoringRef.current = false;\n } else {\n setStack([]);\n // The fallback focus is a programmatic restore too, so guard it exactly like the branch\n // above: focusing fallBackRef re-fires the container onFocus with the just-removed element\n // still as relatedTarget, and without this guard the keyboard branch would re-record it as a\n // fresh navigation \u2014 leaving a phantom entry in the history (PUI-13301 QA re-open).\n isRestoringRef.current = true;\n fallBackRef.current?.focus();\n isRestoringRef.current = false;\n }\n }, [fallBackRef, stack]);\n\n return useMemo(\n () => ({ trackFocus: handleOnFocus, cleanStack: handleOnBlur, stack, focusLastElement }),\n [handleOnFocus, handleOnBlur, stack, focusLastElement],\n );\n};\n\nconst UseFocusStackWithSchema = describe(useFocusStack);\nUseFocusStackWithSchema.propTypes = propTypes;\nUseFocusStackWithSchema.returnType = {\n stack: PropTypes.object.description('The stack of elements that have been focused').isRequired,\n trackFocus: PropTypes.func\n .description('The function to be invoked when focus enters the wrapping container')\n .signature('((e: React.FocusEvent<HTMLElement>) => void)').isRequired,\n cleanStack: PropTypes.func\n .description('The function to be invoked when focus exits the wrapping container')\n .signature('((e: React.FocusEvent<HTMLElement>) => void)').isRequired,\n focusLastElement: PropTypes.func.description('Focuses the last element in the stack').signature('() => void')\n .isRequired,\n};\nexport { useFocusStack, UseFocusStackWithSchema };\n"],
5
+ "mappings": "AAAA,YAAY,WAAW;ACAvB,SAAS,aAAa,WAAW,SAAS,QAAQ,gBAAgB;AAClE,SAAS,oBAAoB;AAC7B,SAAS,UAAU,iBAAiB;AAEpC,SAAS,iBAAiB;AAC1B,MAAM,gBAAgB,CAAC,UAAoC;AACzD,QAAM,EAAE,YAAY,IAAI;AACxB,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,CAAC,CAAC;AAOpD,QAAM,qBAAqB,OAA+B,UAAU;AACpE,YAAU,MAAM;AACd,UAAM,gBAAgB,MAAM;AAC1B,yBAAmB,UAAU;AAAA,IAC/B;AACA,UAAM,YAAY,CAAC,MAAqB;AACtC,UAAI,EAAE,QAAQ,MAAO,oBAAmB,UAAU;AAAA,IACpD;AACA,aAAS,iBAAiB,eAAe,eAAe,IAAI;AAC5D,aAAS,iBAAiB,WAAW,WAAW,IAAI;AACpD,WAAO,MAAM;AACX,eAAS,oBAAoB,eAAe,eAAe,IAAI;AAC/D,eAAS,oBAAoB,WAAW,WAAW,IAAI;AAAA,IACzD;AAAA,EACF,GAAG,CAAC,CAAC;AAIL,QAAM,iBAAiB,OAAO,KAAK;AAEnC,QAAM,gBAAsD;AAAA,IAC1D,CAAC,MAAM;AACL,UAAI,eAAe,QAAS;AAC5B,YAAM,WAAW,EAAE;AAInB,UAAI,YAAY,aAAa,YAAY,QAAS;AAElD,UAAI,mBAAmB,YAAY,WAAW;AAI5C,YAAI,YAAY,aAAa,EAAE,UAAU,EAAE,cAAc,SAAS,QAAQ,GAAG;AAC3E,mBAAS,CAAC,SAAS,CAAC,UAAU,GAAG,IAAI,CAAC;AAAA,QACxC;AACA;AAAA,MACF;AAIA,UAAI,EAAE,WAAW,MAAM,CAAC,KAAK,SAAU,UAAS,CAAC,SAAS,CAAC,UAAU,GAAG,IAAI,CAAC;AAAA,UACxE,UAAS,CAAC,SAAU,KAAK,SAAS,IAAI,KAAK,MAAM,CAAC,IAAI,IAAK;AAAA,IAClE;AAAA,IACA,CAAC,OAAO,WAAW;AAAA,EACrB;AAEA,QAAM,SAAS;AAAA,IACb,OAAO;AAAA,MACL,QAAQ,MAAM,SAAS,CAAC,CAAC;AAAA,IAC3B;AAAA,IACA,CAAC;AAAA,EACH;AACA,QAAM,eAAe,aAAa,MAAM;AAExC,QAAM,mBAAmB,YAAY,MAAM;AAKzC,UAAM,eAAe,OAAO,aAAa,cAAc,OAAO,SAAS;AACvE,UAAM,YAAY,MAAM,OAAO,CAAC,OAAO,OAAO,gBAAgB,IAAI,gBAAgB,CAAC;AACnF,QAAI,UAAU,SAAS,GAAG;AACxB,YAAM,CAAC,QAAQ,GAAG,IAAI,IAAI;AAG1B,eAAS,IAAI;AACb,qBAAe,UAAU;AACzB,cAAQ,MAAM;AACd,qBAAe,UAAU;AAAA,IAC3B,OAAO;AACL,eAAS,CAAC,CAAC;AAKX,qBAAe,UAAU;AACzB,kBAAY,SAAS,MAAM;AAC3B,qBAAe,UAAU;AAAA,IAC3B;AAAA,EACF,GAAG,CAAC,aAAa,KAAK,CAAC;AAEvB,SAAO;AAAA,IACL,OAAO,EAAE,YAAY,eAAe,YAAY,cAAc,OAAO,iBAAiB;AAAA,IACtF,CAAC,eAAe,cAAc,OAAO,gBAAgB;AAAA,EACvD;AACF;AAEA,MAAM,0BAA0B,SAAS,aAAa;AACtD,wBAAwB,YAAY;AACpC,wBAAwB,aAAa;AAAA,EACnC,OAAO,UAAU,OAAO,YAAY,8CAA8C,EAAE;AAAA,EACpF,YAAY,UAAU,KACnB,YAAY,qEAAqE,EACjF,UAAU,8CAA8C,EAAE;AAAA,EAC7D,YAAY,UAAU,KACnB,YAAY,oEAAoE,EAChF,UAAU,8CAA8C,EAAE;AAAA,EAC7D,kBAAkB,UAAU,KAAK,YAAY,uCAAuC,EAAE,UAAU,YAAY,EACzG;AACL;",
6
6
  "names": []
7
7
  }
@@ -3,8 +3,5 @@
3
3
  "sideEffects": [
4
4
  "*.css",
5
5
  "*.scss"
6
- ],
7
- "publishConfig": {
8
- "access": "public"
9
- }
6
+ ]
10
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../scripts/build/transpile/react-shim.js", "../../src/react-desc-prop-types.ts"],
4
- "sourcesContent": ["import * as React from 'react';\nexport { React };\n", "/* eslint-disable @typescript-eslint/no-empty-interface */\n\nimport type React from 'react';\nimport { PropTypes } from '@elliemae/ds-props-helpers';\nexport declare namespace DSHooksFocusStackT {\n interface Props {\n fallBackRef: React.RefObject<HTMLElement>;\n }\n}\n\nexport const propTypes = {\n fallBackRef: PropTypes.object.description('The fallback ref to focus when the stack is empty').isRequired,\n} as React.WeakValidationMap<unknown>;\n"],
5
- "mappings": "AAAA,YAAY,WAAW;ACGvB,SAAS,iBAAiB;AAOnB,MAAM,YAAY;AAAA,EACvB,aAAa,UAAU,OAAO,YAAY,mDAAmD,EAAE;AACjG;",
4
+ "sourcesContent": ["import * as React from 'react';\nexport { React };\n", "import type React from 'react';\nimport { PropTypes } from '@elliemae/ds-props-helpers';\nexport declare namespace DSHooksFocusStackT {\n interface Props {\n fallBackRef: React.RefObject<HTMLElement>;\n }\n}\n\nexport const propTypes = {\n fallBackRef: PropTypes.object.description('The fallback ref to focus when the stack is empty').isRequired,\n} as React.WeakValidationMap<unknown>;\n"],
5
+ "mappings": "AAAA,YAAY,WAAW;ACCvB,SAAS,iBAAiB;AAOnB,MAAM,YAAY;AAAA,EACvB,aAAa,UAAU,OAAO,YAAY,mDAAmD,EAAE;AACjG;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elliemae/ds-hooks-focus-stack",
3
- "version": "3.70.0-next.9",
3
+ "version": "3.70.0-rc.1",
4
4
  "license": "MIT",
5
5
  "description": "ICE MT - Dimsum - Hooks Focus Stack",
6
6
  "files": [
@@ -22,7 +22,7 @@
22
22
  ],
23
23
  "repository": {
24
24
  "type": "git",
25
- "url": "https://git.elliemae.io/platform-ui/dimsum.git"
25
+ "url": "https://github.com/intcx/PLATFORM-UI.dimsum.git"
26
26
  },
27
27
  "engines": {
28
28
  "pnpm": ">=9",
@@ -36,19 +36,20 @@
36
36
  "indent": 4
37
37
  },
38
38
  "dependencies": {
39
- "@elliemae/ds-hooks-on-blur-out": "3.70.0-next.9",
40
- "@elliemae/ds-props-helpers": "3.70.0-next.9"
39
+ "@elliemae/ds-props-helpers": "3.70.0-rc.1",
40
+ "@elliemae/ds-hooks-on-blur-out": "3.70.0-rc.1"
41
41
  },
42
42
  "devDependencies": {
43
- "styled-components": "~5.3.9",
43
+ "@playwright/experimental-ct-react": "1.61.1",
44
+ "styled-components": "~5.3.11",
44
45
  "styled-system": "^5.1.5",
45
- "@elliemae/ds-monorepo-devops": "3.70.0-next.9"
46
+ "@elliemae/ds-monorepo-devops": "3.70.0-rc.1"
46
47
  },
47
48
  "peerDependencies": {
48
- "lodash-es": "^4.17.21",
49
+ "lodash-es": "^4.18.1",
49
50
  "react": "^18.3.1",
50
51
  "react-dom": "^18.3.1",
51
- "styled-components": "~5.3.9",
52
+ "styled-components": "~5.3.11",
52
53
  "styled-system": "^5.1.5"
53
54
  },
54
55
  "publishConfig": {
@@ -57,7 +58,7 @@
57
58
  },
58
59
  "scripts": {
59
60
  "dev": "cross-env NODE_ENV=development node ../../../scripts/build/build.mjs --watch",
60
- "test": "ds-monorepo-devops test --passWithNoTests --coverage=\"false\"",
61
+ "test": "playwright test -c ./playwright.config.mjs && ds-monorepo-devops test --passWithNoTests --coverage=\"false\"",
61
62
  "lint": "node ../../../scripts/lint.mjs --fix",
62
63
  "lint:strict": "node ../../../scripts/lint-strict.mjs",
63
64
  "dts": "node ../../../scripts/dts.mjs",
@@ -66,4 +67,4 @@
66
67
  "dev:install": "pnpm --filter {.}... i --no-lockfile && pnpm run dev:build",
67
68
  "checkDeps": "npx -yes ../../util/ds-codemods check-missing-packages --projectFolderPath=\"./\" --ignorePackagesGlobPattern=\"\" --ignoreFilesGlobPattern=\"**/test-ables/*,**/tests/*\""
68
69
  }
69
- }
70
+ }