@elliemae/ds-app-picker 3.70.0-next.61 → 3.70.0-next.63
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/dist/cjs/config/useAppPicker.js +2 -2
- package/dist/cjs/config/useAppPicker.js.map +2 -2
- package/dist/cjs/config/useFocusTracker.js +7 -7
- package/dist/cjs/config/useFocusTracker.js.map +2 -2
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/index.js.map +2 -2
- package/dist/cjs/parts/AppPanel.js +5 -14
- package/dist/cjs/parts/AppPanel.js.map +2 -2
- package/dist/cjs/parts/AppPickerFloatingContext/AppPickerContent.js +12 -3
- package/dist/cjs/parts/AppPickerFloatingContext/AppPickerContent.js.map +2 -2
- package/dist/cjs/parts/AppPickerFloatingContext/useAppPickerFloatingContext.js +46 -22
- package/dist/cjs/parts/AppPickerFloatingContext/useAppPickerFloatingContext.js.map +2 -2
- package/dist/cjs/parts/Trigger.js +3 -1
- package/dist/cjs/parts/Trigger.js.map +2 -2
- package/dist/cjs/react-desc-prop-types.js +5 -1
- package/dist/cjs/react-desc-prop-types.js.map +2 -2
- package/dist/cjs/typescript-testing/typescript-app-picker-valid.js +2 -1
- package/dist/cjs/typescript-testing/typescript-app-picker-valid.js.map +2 -2
- package/dist/cjs/util/getDSAppPickerContractProps.js +38 -0
- package/dist/cjs/util/getDSAppPickerContractProps.js.map +7 -0
- package/dist/esm/config/useAppPicker.js +2 -2
- package/dist/esm/config/useAppPicker.js.map +2 -2
- package/dist/esm/config/useFocusTracker.js +7 -7
- package/dist/esm/config/useFocusTracker.js.map +2 -2
- package/dist/esm/index.js +3 -1
- package/dist/esm/index.js.map +2 -2
- package/dist/esm/parts/AppPanel.js +5 -14
- package/dist/esm/parts/AppPanel.js.map +2 -2
- package/dist/esm/parts/AppPickerFloatingContext/AppPickerContent.js +12 -3
- package/dist/esm/parts/AppPickerFloatingContext/AppPickerContent.js.map +2 -2
- package/dist/esm/parts/AppPickerFloatingContext/useAppPickerFloatingContext.js +46 -22
- package/dist/esm/parts/AppPickerFloatingContext/useAppPickerFloatingContext.js.map +2 -2
- package/dist/esm/parts/Trigger.js +3 -1
- package/dist/esm/parts/Trigger.js.map +2 -2
- package/dist/esm/react-desc-prop-types.js +5 -1
- package/dist/esm/react-desc-prop-types.js.map +2 -2
- package/dist/esm/typescript-testing/typescript-app-picker-valid.js +2 -1
- package/dist/esm/typescript-testing/typescript-app-picker-valid.js.map +2 -2
- package/dist/esm/util/getDSAppPickerContractProps.js +8 -0
- package/dist/esm/util/getDSAppPickerContractProps.js.map +7 -0
- package/dist/types/config/useAppPicker.d.ts +2 -2
- package/dist/types/config/useFocusTracker.d.ts +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/parts/AppPickerFloatingContext/useAppPickerFloatingContext.d.ts +3 -2
- package/dist/types/parts/Trigger.d.ts +1 -0
- package/dist/types/react-desc-prop-types.d.ts +32 -0
- package/dist/types/util/getDSAppPickerContractProps.d.ts +37 -0
- package/package.json +16 -15
|
@@ -2,11 +2,20 @@ import * as React from "react";
|
|
|
2
2
|
import { useFloatingContext } from "@elliemae/ds-floating-context";
|
|
3
3
|
import { useCallback, useMemo, useRef } from "react";
|
|
4
4
|
const useAppPickerFloatingContext = (appPickerConfig) => {
|
|
5
|
-
const { propsWithDefault, focusTrackers,
|
|
6
|
-
const {
|
|
7
|
-
|
|
5
|
+
const { propsWithDefault, focusTrackers, openIntentRef, resolvedIsOpen, setInternalIsOpen } = appPickerConfig;
|
|
6
|
+
const {
|
|
7
|
+
onOpen,
|
|
8
|
+
onClose,
|
|
9
|
+
actionRef,
|
|
10
|
+
onClick,
|
|
11
|
+
onClickOutside,
|
|
12
|
+
onKeyDown,
|
|
13
|
+
triggerRef,
|
|
14
|
+
isOpen,
|
|
15
|
+
openAndFocusOnArrowKeys
|
|
16
|
+
} = propsWithDefault;
|
|
8
17
|
const isControlled = typeof isOpen === "boolean";
|
|
9
|
-
const { trackFocusSelectedItem, trackFocusItemAtIndex, trackFocusPanel
|
|
18
|
+
const { trackFocusSelectedItem, trackFocusSelectedOrLastItem, trackFocusItemAtIndex, trackFocusPanel } = focusTrackers;
|
|
10
19
|
const defaultTriggerRef = useRef(null);
|
|
11
20
|
const handleClickOutside = useCallback(
|
|
12
21
|
(e) => {
|
|
@@ -31,20 +40,12 @@ const useAppPickerFloatingContext = (appPickerConfig) => {
|
|
|
31
40
|
);
|
|
32
41
|
const handleFloatingOpen = useCallback(
|
|
33
42
|
(info) => {
|
|
34
|
-
if (
|
|
35
|
-
else if (
|
|
36
|
-
else
|
|
43
|
+
if (openIntentRef.current === "keyboard-first") trackFocusSelectedItem();
|
|
44
|
+
else if (openIntentRef.current === "keyboard-last") trackFocusSelectedOrLastItem();
|
|
45
|
+
else trackFocusPanel();
|
|
37
46
|
if (info.reason === "controlled-flip" && isControlled) onOpen?.({ reason: "controlled" });
|
|
38
47
|
},
|
|
39
|
-
[
|
|
40
|
-
wasOpenedByKeyboardRef,
|
|
41
|
-
triggerIsInternal,
|
|
42
|
-
trackFocusSelectedItem,
|
|
43
|
-
trackFocusPanel,
|
|
44
|
-
trackFocusFirstItem,
|
|
45
|
-
isControlled,
|
|
46
|
-
onOpen
|
|
47
|
-
]
|
|
48
|
+
[openIntentRef, trackFocusSelectedItem, trackFocusSelectedOrLastItem, trackFocusPanel, isControlled, onOpen]
|
|
48
49
|
);
|
|
49
50
|
const floatingContextConfig = useMemo(
|
|
50
51
|
() => ({
|
|
@@ -55,10 +56,19 @@ const useAppPickerFloatingContext = (appPickerConfig) => {
|
|
|
55
56
|
externallyControlledIsOpen: resolvedIsOpen,
|
|
56
57
|
withoutAnimation: true,
|
|
57
58
|
onClickOutside: handleClickOutside,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
/*
|
|
60
|
+
* Two conditions, and the second is a safety gate rather than a preference.
|
|
61
|
+
*
|
|
62
|
+
* `!onKeyDown` — a consumer who supplies onKeyDown owns the key entirely, so useFloatingContext
|
|
63
|
+
* neither routes the Escape to onEscape (see handleScopedEscape) nor returns focus to the trigger.
|
|
64
|
+
*
|
|
65
|
+
* `!isControlled` — `returnFocusToReference` is true below, and the hook applies it on a scoped
|
|
66
|
+
* Escape regardless of who owns the open state. For a controlled picker `handleScopedEscape`'s
|
|
67
|
+
* `setInternalIsOpen(false)` is inert, so leaving this on would move focus to the trigger while
|
|
68
|
+
* the consumer's panel is still open: focus outside an open dialog. A controlled consumer owns
|
|
69
|
+
* the close, so they own the Escape and the refocus that goes with it.
|
|
70
|
+
*/
|
|
71
|
+
closeOnEscape: !onKeyDown && !isControlled,
|
|
62
72
|
onClose: handleFloatingClose,
|
|
63
73
|
onEscape: handleScopedEscape,
|
|
64
74
|
onOpen: handleFloatingOpen,
|
|
@@ -70,6 +80,7 @@ const useAppPickerFloatingContext = (appPickerConfig) => {
|
|
|
70
80
|
handleScopedEscape,
|
|
71
81
|
handleFloatingClose,
|
|
72
82
|
handleFloatingOpen,
|
|
83
|
+
isControlled,
|
|
73
84
|
onKeyDown,
|
|
74
85
|
resolvedIsOpen,
|
|
75
86
|
triggerRef
|
|
@@ -116,7 +127,7 @@ const useAppPickerFloatingContext = (appPickerConfig) => {
|
|
|
116
127
|
);
|
|
117
128
|
const handleTriggerClick = useCallback(
|
|
118
129
|
(e) => {
|
|
119
|
-
|
|
130
|
+
openIntentRef.current = e.detail === 0 ? "keyboard-first" : "pointer";
|
|
120
131
|
onClick?.(e);
|
|
121
132
|
if (!isControlled) {
|
|
122
133
|
if (resolvedIsOpen) onClose?.({ reason: "trigger-toggle", event: e });
|
|
@@ -124,7 +135,19 @@ const useAppPickerFloatingContext = (appPickerConfig) => {
|
|
|
124
135
|
}
|
|
125
136
|
setInternalIsOpen((prev) => !prev);
|
|
126
137
|
},
|
|
127
|
-
[onClick,
|
|
138
|
+
[onClick, openIntentRef, setInternalIsOpen, isControlled, resolvedIsOpen, onClose, onOpen]
|
|
139
|
+
);
|
|
140
|
+
const handleTriggerKeyDown = useCallback(
|
|
141
|
+
(e) => {
|
|
142
|
+
if (!openAndFocusOnArrowKeys) return;
|
|
143
|
+
if (e.code !== "ArrowDown" && e.code !== "ArrowUp") return;
|
|
144
|
+
if (resolvedIsOpen) return;
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
openIntentRef.current = e.code === "ArrowDown" ? "keyboard-first" : "keyboard-last";
|
|
147
|
+
if (!isControlled) onOpen?.({ reason: "trigger", event: e });
|
|
148
|
+
setInternalIsOpen(true);
|
|
149
|
+
},
|
|
150
|
+
[openAndFocusOnArrowKeys, resolvedIsOpen, openIntentRef, isControlled, onOpen, setInternalIsOpen]
|
|
128
151
|
);
|
|
129
152
|
return {
|
|
130
153
|
floatingStyles,
|
|
@@ -132,6 +155,7 @@ const useAppPickerFloatingContext = (appPickerConfig) => {
|
|
|
132
155
|
floatingContext,
|
|
133
156
|
handleTriggerRef,
|
|
134
157
|
handleTriggerClick,
|
|
158
|
+
handleTriggerKeyDown,
|
|
135
159
|
floatingInnerRef
|
|
136
160
|
};
|
|
137
161
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../scripts/build/transpile/react-shim.js", "../../../../src/parts/AppPickerFloatingContext/useAppPickerFloatingContext.ts"],
|
|
4
|
-
"sourcesContent": ["import * as React from 'react';\nexport { React };\n", "import { useFloatingContext, type DSHookFloatingContextT } from '@elliemae/ds-floating-context';\nimport type React from 'react';\nimport { useCallback, useMemo, useRef } from 'react';\nimport type { useAppPicker } from '../../config/useAppPicker.js';\nimport type { DSAppPickerT } from '../../react-desc-prop-types.js';\n\n/**\n * useAppPickerFloatingContext \u2014 the single point that wires DSAppPicker to\n * `@elliemae/ds-floating-context`'s `useFloatingContext`.\n *\n * WHAT IT DOES (as a black box):\n * AppPicker can be used in two very different ways:\n * - Controlled: the consumer supplies `isOpen` and drives the open/close transition.\n * \u2514\u2192 AppPicker can't be event-based as the flip might originate from any cause and we don't own the business logic\n * \u2514\u2192 we need to ensure WCAG embedded behaviours (focus, Escape, click-outside) are wired to the consumer's `isOpen` flip.\n * - Uncontrolled: the consumer does not supply `isOpen` and AppPicker owns the full flows, we only notify the consumer of the transition and its cause.\n *\n * The AppPicker and this file in particular serves as the one place that coordinates the two different ways to use the component and wires with the useFloatingContext hook.\n * This files owns:\n * - Focus management relative to open/close transitions (initial focus on open, return focus to trigger on close)\n * - Escape key handling (scoped to the panel and trigger)\n * - Click-outside handling (panel and trigger)\n * - Imperative focus API via actionRef\n * - Controlled/Uncontrolled forks and differences in event notification (eager vs controlled-flip)\n * - OnOpen and OnClose notification to the consumer with the correct reason and event (if any)\n *\n * IMPORTANT:\n * Parts of the flow/logic required in this file is offloaded to useFloatingContext internal assumptions and logics.\n * Changes to useFloatingContext may break the flows in this file, or alter them in ways that are not obvious, the two are closely coupled.\n *\n * DATA / EVENT FLOW\n *\n * Two concerns run in parallel and must not be conflated: (A) what makes the panel show/hide and take\n * focus, and (B) how the consumer is told it opened/closed. They have different origins.\n *\n * (A) PANEL VISIBILITY + FOCUS \u2014 useFloatingContext internal co-ownership of the flow.\n *\n * a user action \u2502 \u2502 isOpen prop flip\n * (trigger click / scoped Escape / click outside) \u2502 \u2502 (not a traceable user action, but the consumer's own flip)\n * uncontrolled - flips the internal flag \u2502 \u2502 controlled - AppPicker's own flip is inert, consumer drives it\n * \u2502\n * \u25BC\n * resolvedIsOpen changes\n * \u2502\n * \u25BC\n * useFloatingContext (externallyControlledIsOpen) detects the transition\n * \u2502\n * \u25BC\n * on open \u2502 \u2502 on close\n * handleFloatingOpen \u2502 \u2502 focus is only restored after a SCOPED ESCAPE:\n * AppPicker business logic seeds initial focus \u2502 \u2502 useFloatingContext returns focus to the trigger\n * \u2502 \u2502 (returnFocusToReference). Click-outside / toggle\n * \u2502 \u2502 / controlled closes do NOT move focus, and\n * \u2502 \u2502 handleFloatingClose only NOTIFIES (see (B)).\n * \u2502\n * \u25BC\n * \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n * \u2502 visibility + focus are delivered to final user as expected \u2502\n * \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n *\n * (B) CONSUMER NOTIFICATION\n * Each flow below is the UNCONTROLLED path, where AppPicker owns the transition and can name the true cause.\n * The controlled equivalent for every trigger is stated once at the end.\n * \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n * \u2502 OPENING TRIGGERS FLOWS \u2502\n * \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n * TRIGGER CLICK (uncontrolled)\n * trigger click\n * user clicks the internal trigger, panel closed\n * \u2502\n * \u25BC\n * handleTriggerClick\n * ( flips the flag and wires the visual and focus Flow (A) above )\n * \u2502\n * \u25BC\n * \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n * \u2502 onOpen { reason: 'trigger', event } \u2502 fired EAGERLY, when the event is triggered\n * \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n * ------------------------------------------------------------------------------------------------------------------------\n * \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n * \u2502 CLOSING TRIGGERS FLOWS \u2502\n * \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n * KEYBOARD ESCAPE (uncontrolled)\n * Escape within the panel DOM tree or the trigger\n * scoped Escape (routed only while closeOnEscape, i.e. no consumer onKeyDown)\n * \u2502\n * \u25BC\n * onEscape \u2192 handleScopedEscape\n * ( flips the flag and wires the visual and focus Flow (A) above )\n * \u2502\n * \u25BC\n * \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n * \u2502 onClose { reason: 'escape', event } \u2502 fired EAGERLY, when the event is triggered\n * \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n *\n * CLICK OUTSIDE (uncontrolled)\n * Click outside the panel DOM tree and the trigger\n * useFloatingContext detects a pointer press outside the panel + trigger\n * \u2502\n * \u25BC\n * handleClickOutside\n * ( flips the flag and wires the visual and focus Flow (A) above )\n * \u2502\n * \u25BC\n * \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n * \u2502 onClose { reason: 'click-outside', event } \u2502 fired EAGERLY, when the event is triggered\n * \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n *\n * CLICK ON TRIGGER (uncontrolled, while internal state is open)\n * trigger click while open\n * user clicks the internal trigger, panel open\n * \u2502\n * \u25BC\n * handleTriggerClick\n * ( flips the flag and wires the visual and focus Flow (A) above )\n * \u2502\n * \u25BC\n * \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n * \u2502 onClose { reason: 'trigger-toggle', event } \u2502 fired EAGERLY, when the event is triggered\n * \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n * ------------------------------------------------------------------------------------------------------------------------\n * \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n * \u2502 CONTROLLED OPEN & CLOSE FLOWS \u2502\n * \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n * AppPicker's own flip is inert;\n * We can't fire EAGERLY and we can't track an originating event, there may be NONE\n * The consumer's `isOpen` flip is reacted to by useFloatingContext, which fires onOpen / onClose with reason 'controlled-flip'\n * We wire the visual and focus Flow (A) above, and then notify the consumer with reason 'controlled' (no event)\n * \u2502\n * \u25BC\n * \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n * \u2502 onOpen { reason: 'controlled' } \u2502 no event\n * \u2502 onClose { reason: 'controlled' } \u2502 a prop flip is not a traceable user action\n * \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n * ------------------------------------------------------------------------------------------------------------------------\n *\n * Eager (uncontrolled) and controlled-flip paths are mutually exclusive on `isControlled`.\n * the consumer is notified exactly once per transition \u2014 never twice.\n * On Escape specifically\n * a controlled consumer supplies onKeyDown, so closeOnEscape is false in useFloatingContext\n * \u2514\u2192 AppPicker's onEscape never runs \u2014\n * \u2514\u2192 the close arrives purely by listening to the flag transition\n * ------------------------------------------------------------------------------------------------------------------------\n * A nuance for an edge case:\n *\n * Declarative mount-open (rendered already-open).\n * useFloatingContext fires no onOpen for it (it did not transition to open, it started open),\n * \u2514\u2192 neither the focus-seed (A) nor the notification (B) runs\n * AppPanel compensates this locally via useMountedOpenAutofocusNuance, it needs to be seen if this is even desired or should be removed\n * ------------------------------------------------------------------------------------------------------------------------\n */\n\ntype ConfiguredAppPickerCtx = ReturnType<typeof useAppPicker>;\n\ntype AppPickerFloatingContextConfig = {\n propsWithDefault: DSAppPickerT.InternalProps;\n focusTrackers: ConfiguredAppPickerCtx['focusTrackers'];\n wasOpenedByKeyboardRef: ConfiguredAppPickerCtx['wasOpenedByKeyboardRef'];\n // Open state is owned by useAppPicker (lifted so useFocusTracker can seed focus in the same fiber).\n // This hook reads the resolved value for positioning and flips it through the setter to close/toggle.\n resolvedIsOpen: ConfiguredAppPickerCtx['resolvedIsOpen'];\n setInternalIsOpen: ConfiguredAppPickerCtx['setInternalIsOpen'];\n};\n\nexport const useAppPickerFloatingContext = (appPickerConfig: AppPickerFloatingContextConfig) => {\n const { propsWithDefault, focusTrackers, wasOpenedByKeyboardRef, resolvedIsOpen, setInternalIsOpen } =\n appPickerConfig;\n const { onOpen, onClose, actionRef, onClick, onClickOutside, onKeyDown, triggerRef, renderTrigger, isOpen } =\n propsWithDefault;\n const triggerIsInternal = !renderTrigger;\n // Controlled when the consumer supplies `isOpen`. This decides who owns a transition: uncontrolled,\n // AppPicker's own handlers own it and report the specific event-carrying reason eagerly; controlled,\n // AppPicker's own flips are inert and the consumer drives open/close via the prop (reason 'controlled').\n const isControlled = typeof isOpen === 'boolean';\n\n const { trackFocusSelectedItem, trackFocusItemAtIndex, trackFocusPanel, trackFocusFirstItem } = focusTrackers;\n\n const defaultTriggerRef = useRef<HTMLButtonElement>(null);\n\n // Uncontrolled close \u2014 see file header (B) \"CLICK OUTSIDE\". Also forwards the raw event to onClickOutside.\n const handleClickOutside = useCallback(\n (e: MouseEvent | TouchEvent) => {\n if (!isControlled) onClose?.({ reason: 'click-outside', event: e });\n setInternalIsOpen(false);\n onClickOutside?.(e as MouseEvent);\n },\n [isControlled, onClose, setInternalIsOpen, onClickOutside],\n );\n\n // onEscape target \u2014 see file header (B) \"KEYBOARD ESCAPE\". Scoped Escape only; gated by closeOnEscape\n // (`!onKeyDown`, see the config below).\n const handleScopedEscape = useCallback(\n (event: KeyboardEvent) => {\n if (!isControlled) onClose?.({ reason: 'escape', event });\n setInternalIsOpen(false);\n },\n [isControlled, onClose, setInternalIsOpen],\n );\n\n // Controlled close notify \u2014 see file header \"CONTROLLED OPEN & CLOSE FLOWS\". The guard also filters\n // out a non-scoped Escape, which reaches here as reason 'interaction' (not 'controlled-flip').\n const handleFloatingClose = useCallback(\n (info: DSHookFloatingContextT.OpenChange) => {\n if (info.reason === 'controlled-flip' && isControlled) onClose?.({ reason: 'controlled' });\n },\n [isControlled, onClose],\n );\n\n // Open transition \u2014 see file header (A) (focus) + \"CONTROLLED OPEN & CLOSE FLOWS\" (notify). Seeds\n // initial focus (targets per the CONSUMER CONFIGURATION table) and, when controlled, notifies the\n // consumer. Mount-open is not a transition, so this never fires for it (AppPanel's\n // useMountedOpenAutofocusNuance handles that one case).\n const handleFloatingOpen = useCallback(\n (info: DSHookFloatingContextT.OpenChange) => {\n if (wasOpenedByKeyboardRef.current && triggerIsInternal) trackFocusSelectedItem();\n else if (triggerIsInternal) trackFocusPanel();\n else trackFocusFirstItem();\n if (info.reason === 'controlled-flip' && isControlled) onOpen?.({ reason: 'controlled' });\n },\n [\n wasOpenedByKeyboardRef,\n triggerIsInternal,\n trackFocusSelectedItem,\n trackFocusPanel,\n trackFocusFirstItem,\n isControlled,\n onOpen,\n ],\n );\n\n const floatingContextConfig = useMemo(\n () =>\n ({\n placement: 'bottom',\n // Arrow tip is ~6.1px above the panel top (arrowPlacementBase16 \u2248 17.5px, tip at 11.4px from box top).\n // 4px spec gap \u2192 4 + 6.1 \u2248 10px offset.\n customOffset: [0, 10],\n externallyControlledIsOpen: resolvedIsOpen,\n withoutAnimation: true,\n onClickOutside: handleClickOutside,\n // closeOnEscape = !onKeyDown: with a consumer onKeyDown, useFloatingContext neither routes\n // the Escape to onEscape (see handleScopedEscape) nor returns focus to the trigger \u2014 the\n // consumer's onKeyDown owns the key entirely.\n closeOnEscape: !onKeyDown,\n onClose: handleFloatingClose,\n onEscape: handleScopedEscape,\n onOpen: handleFloatingOpen,\n returnFocusToReference: true,\n externalReferenceElement: triggerRef?.current || defaultTriggerRef.current,\n }) as DSHookFloatingContextT.Props,\n [\n handleClickOutside,\n handleScopedEscape,\n handleFloatingClose,\n handleFloatingOpen,\n onKeyDown,\n resolvedIsOpen,\n triggerRef,\n ],\n );\n\n const {\n refs: floatingRefs,\n floatingStyles,\n arrowStyles,\n context: floatingContext,\n } = useFloatingContext(floatingContextConfig);\n\n const handleTriggerRef = useCallback<React.RefCallback<HTMLButtonElement>>(\n (node) => {\n (defaultTriggerRef as React.MutableRefObject<HTMLButtonElement | null>).current = node;\n floatingRefs.setReference(node);\n if (triggerRef) {\n (triggerRef as React.MutableRefObject<HTMLButtonElement | null>).current = node;\n }\n },\n [floatingRefs, triggerRef],\n );\n\n // Populates the consumer's actionRef with imperative focus methods. Fires on mount with the panel\n // node. Every method routes through the focus tracker \u2014 no DOM queries.\n const handleActionRef = useCallback<React.RefCallback<HTMLDivElement>>(\n (node) => {\n if (node && actionRef?.current) {\n actionRef.current.focusToIndex = (index: number) => {\n trackFocusItemAtIndex(index);\n };\n actionRef.current.focusWrapper = () => {\n trackFocusPanel();\n };\n actionRef.current.focusSelectedOrFirstAvailable = () => {\n trackFocusSelectedItem();\n };\n }\n },\n [actionRef, trackFocusItemAtIndex, trackFocusPanel, trackFocusSelectedItem],\n );\n\n const floatingInnerRef = useCallback<React.RefCallback<HTMLDivElement>>(\n (node) => {\n (floatingRefs.setFloating as React.RefCallback<HTMLDivElement>)(node);\n handleActionRef(node);\n },\n [floatingRefs.setFloating, handleActionRef],\n );\n\n const handleTriggerClick = useCallback(\n (e: React.MouseEvent | React.KeyboardEvent) => {\n // detail === 0 \u21D2 keyboard activation (Enter/Space), not a pointer. handleFloatingOpen reads\n // this on the next open transition to pick keyboard-vs-pointer initial focus.\n wasOpenedByKeyboardRef.current = (e as React.MouseEvent).detail === 0;\n onClick?.(e);\n // Uncontrolled eager notify \u2014 see file header (B) \"TRIGGER CLICK\" / \"CLICK ON TRIGGER\".\n // `resolvedIsOpen` is the pre-flip value: open \u21D2 this click closes (trigger-toggle), else opens.\n if (!isControlled) {\n if (resolvedIsOpen) onClose?.({ reason: 'trigger-toggle', event: e });\n else onOpen?.({ reason: 'trigger', event: e });\n }\n setInternalIsOpen((prev) => !prev);\n },\n [onClick, wasOpenedByKeyboardRef, setInternalIsOpen, isControlled, resolvedIsOpen, onClose, onOpen],\n );\n\n return {\n floatingStyles,\n arrowStyles,\n floatingContext,\n handleTriggerRef,\n handleTriggerClick,\n floatingInnerRef,\n };\n};\n"],
|
|
5
|
-
"mappings": "AAAA,YAAY,WAAW;ACAvB,SAAS,0BAAuD;AAEhE,SAAS,aAAa,SAAS,cAAc;AAkKtC,MAAM,8BAA8B,CAAC,oBAAoD;AAC9F,QAAM,EAAE,kBAAkB,eAAe,
|
|
4
|
+
"sourcesContent": ["import * as React from 'react';\nexport { React };\n", "import { useFloatingContext, type DSHookFloatingContextT } from '@elliemae/ds-floating-context';\nimport type React from 'react';\nimport { useCallback, useMemo, useRef } from 'react';\nimport type { useAppPicker } from '../../config/useAppPicker.js';\nimport type { DSAppPickerT } from '../../react-desc-prop-types.js';\n\n/**\n * useAppPickerFloatingContext \u2014 the single point that wires DSAppPicker to\n * `@elliemae/ds-floating-context`'s `useFloatingContext`.\n *\n * WHAT IT DOES (as a black box):\n * AppPicker can be used in two very different ways:\n * - Controlled: the consumer supplies `isOpen` and drives the open/close transition.\n * \u2514\u2192 AppPicker can't be event-based as the flip might originate from any cause and we don't own the business logic\n * \u2514\u2192 we need to ensure WCAG embedded behaviours (focus, Escape, click-outside) are wired to the consumer's `isOpen` flip.\n * - Uncontrolled: the consumer does not supply `isOpen` and AppPicker owns the full flows, we only notify the consumer of the transition and its cause.\n *\n * The AppPicker and this file in particular serves as the one place that coordinates the two different ways to use the component and wires with the useFloatingContext hook.\n * This files owns:\n * - Focus management relative to open/close transitions (initial focus on open, return focus to trigger on close)\n * - Escape key handling (scoped to the panel and trigger)\n * - Click-outside handling (panel and trigger)\n * - Imperative focus API via actionRef\n * - Controlled/Uncontrolled forks and differences in event notification (eager vs controlled-flip)\n * - OnOpen and OnClose notification to the consumer with the correct reason and event (if any)\n *\n * IMPORTANT:\n * Parts of the flow/logic required in this file is offloaded to useFloatingContext internal assumptions and logics.\n * Changes to useFloatingContext may break the flows in this file, or alter them in ways that are not obvious, the two are closely coupled.\n *\n * DATA / EVENT FLOW\n *\n * Two concerns run in parallel and must not be conflated: (A) what makes the panel show/hide and take\n * focus, and (B) how the consumer is told it opened/closed. They have different origins.\n *\n * (A) PANEL VISIBILITY + FOCUS \u2014 useFloatingContext internal co-ownership of the flow.\n *\n * a user action \u2502 \u2502 isOpen prop flip\n * (trigger click / scoped Escape / click outside) \u2502 \u2502 (not a traceable user action, but the consumer's own flip)\n * uncontrolled - flips the internal flag \u2502 \u2502 controlled - AppPicker's own flip is inert, consumer drives it\n * \u2502\n * \u25BC\n * resolvedIsOpen changes\n * \u2502\n * \u25BC\n * useFloatingContext (externallyControlledIsOpen) detects the transition\n * \u2502\n * \u25BC\n * on open \u2502 \u2502 on close\n * handleFloatingOpen \u2502 \u2502 focus is only restored after a SCOPED ESCAPE:\n * AppPicker business logic seeds initial focus \u2502 \u2502 useFloatingContext returns focus to the trigger\n * \u2502 \u2502 (returnFocusToReference). Click-outside / toggle\n * \u2502 \u2502 / controlled closes do NOT move focus, and\n * \u2502 \u2502 handleFloatingClose only NOTIFIES (see (B)).\n * \u2502\n * \u25BC\n * \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n * \u2502 visibility + focus are delivered to final user as expected \u2502\n * \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n *\n * (B) CONSUMER NOTIFICATION\n * Each flow below is the UNCONTROLLED path, where AppPicker owns the transition and can name the true cause.\n * The controlled equivalent for every trigger is stated once at the end.\n * \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n * \u2502 OPENING TRIGGERS FLOWS \u2502\n * \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n * TRIGGER CLICK (uncontrolled)\n * trigger click\n * user clicks the internal trigger, panel closed\n * \u2502\n * \u25BC\n * handleTriggerClick\n * ( flips the flag and wires the visual and focus Flow (A) above )\n * \u2502\n * \u25BC\n * \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n * \u2502 onOpen { reason: 'trigger', event } \u2502 fired EAGERLY, when the event is triggered\n * \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n * ------------------------------------------------------------------------------------------------------------------------\n * \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n * \u2502 CLOSING TRIGGERS FLOWS \u2502\n * \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n * KEYBOARD ESCAPE (uncontrolled)\n * Escape within the panel DOM tree or the trigger\n * scoped Escape (routed only while closeOnEscape, i.e. no consumer onKeyDown)\n * \u2502\n * \u25BC\n * onEscape \u2192 handleScopedEscape\n * ( flips the flag and wires the visual and focus Flow (A) above )\n * \u2502\n * \u25BC\n * \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n * \u2502 onClose { reason: 'escape', event } \u2502 fired EAGERLY, when the event is triggered\n * \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n *\n * CLICK OUTSIDE (uncontrolled)\n * Click outside the panel DOM tree and the trigger\n * useFloatingContext detects a pointer press outside the panel + trigger\n * \u2502\n * \u25BC\n * handleClickOutside\n * ( flips the flag and wires the visual and focus Flow (A) above )\n * \u2502\n * \u25BC\n * \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n * \u2502 onClose { reason: 'click-outside', event } \u2502 fired EAGERLY, when the event is triggered\n * \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n *\n * CLICK ON TRIGGER (uncontrolled, while internal state is open)\n * trigger click while open\n * user clicks the internal trigger, panel open\n * \u2502\n * \u25BC\n * handleTriggerClick\n * ( flips the flag and wires the visual and focus Flow (A) above )\n * \u2502\n * \u25BC\n * \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n * \u2502 onClose { reason: 'trigger-toggle', event } \u2502 fired EAGERLY, when the event is triggered\n * \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n * ------------------------------------------------------------------------------------------------------------------------\n * \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n * \u2502 CONTROLLED OPEN & CLOSE FLOWS \u2502\n * \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n * AppPicker's own flip is inert;\n * We can't fire EAGERLY and we can't track an originating event, there may be NONE\n * The consumer's `isOpen` flip is reacted to by useFloatingContext, which fires onOpen / onClose with reason 'controlled-flip'\n * We wire the visual and focus Flow (A) above, and then notify the consumer with reason 'controlled' (no event)\n * \u2502\n * \u25BC\n * \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n * \u2502 onOpen { reason: 'controlled' } \u2502 no event\n * \u2502 onClose { reason: 'controlled' } \u2502 a prop flip is not a traceable user action\n * \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n * ------------------------------------------------------------------------------------------------------------------------\n *\n * Eager (uncontrolled) and controlled-flip paths are mutually exclusive on `isControlled`.\n * the consumer is notified exactly once per transition \u2014 never twice.\n * On Escape specifically\n * closeOnEscape is false whenever the picker is controlled (and whenever a consumer supplies onKeyDown)\n * \u2514\u2192 AppPicker's onEscape never runs \u2014\n * \u2514\u2192 the close arrives purely by listening to the flag transition\n * ------------------------------------------------------------------------------------------------------------------------\n * A nuance for an edge case:\n *\n * Declarative mount-open (rendered already-open).\n * useFloatingContext fires no onOpen for it (it did not transition to open, it started open),\n * \u2514\u2192 neither the focus-seed (A) nor the notification (B) runs\n * AppPanel compensates this locally via useMountedOpenAutofocusNuance, it needs to be seen if this is even desired or should be removed\n * ------------------------------------------------------------------------------------------------------------------------\n */\n\ntype ConfiguredAppPickerCtx = ReturnType<typeof useAppPicker>;\n\ntype AppPickerFloatingContextConfig = {\n propsWithDefault: DSAppPickerT.InternalProps;\n focusTrackers: ConfiguredAppPickerCtx['focusTrackers'];\n openIntentRef: ConfiguredAppPickerCtx['openIntentRef'];\n // Open state is owned by useAppPicker (lifted so useFocusTracker can seed focus in the same fiber).\n // This hook reads the resolved value for positioning and flips it through the setter to close/toggle.\n resolvedIsOpen: ConfiguredAppPickerCtx['resolvedIsOpen'];\n setInternalIsOpen: ConfiguredAppPickerCtx['setInternalIsOpen'];\n};\n\nexport const useAppPickerFloatingContext = (appPickerConfig: AppPickerFloatingContextConfig) => {\n const { propsWithDefault, focusTrackers, openIntentRef, resolvedIsOpen, setInternalIsOpen } = appPickerConfig;\n const {\n onOpen,\n onClose,\n actionRef,\n onClick,\n onClickOutside,\n onKeyDown,\n triggerRef,\n isOpen,\n openAndFocusOnArrowKeys,\n } = propsWithDefault;\n // Controlled when the consumer supplies `isOpen`. This decides who owns a transition: uncontrolled,\n // AppPicker's own handlers own it and report the specific event-carrying reason eagerly; controlled,\n // AppPicker's own flips are inert and the consumer drives open/close via the prop (reason 'controlled').\n const isControlled = typeof isOpen === 'boolean';\n\n const { trackFocusSelectedItem, trackFocusSelectedOrLastItem, trackFocusItemAtIndex, trackFocusPanel } =\n focusTrackers;\n\n const defaultTriggerRef = useRef<HTMLButtonElement>(null);\n\n // Uncontrolled close \u2014 see file header (B) \"CLICK OUTSIDE\". Also forwards the raw event to onClickOutside.\n const handleClickOutside = useCallback(\n (e: MouseEvent | TouchEvent) => {\n if (!isControlled) onClose?.({ reason: 'click-outside', event: e });\n setInternalIsOpen(false);\n onClickOutside?.(e as MouseEvent);\n },\n [isControlled, onClose, setInternalIsOpen, onClickOutside],\n );\n\n // onEscape target \u2014 see file header (B) \"KEYBOARD ESCAPE\". Scoped Escape only; gated by closeOnEscape\n // (`!onKeyDown`, see the config below).\n const handleScopedEscape = useCallback(\n (event: KeyboardEvent) => {\n if (!isControlled) onClose?.({ reason: 'escape', event });\n setInternalIsOpen(false);\n },\n [isControlled, onClose, setInternalIsOpen],\n );\n\n // Controlled close notify \u2014 see file header \"CONTROLLED OPEN & CLOSE FLOWS\". The guard also filters\n // out a non-scoped Escape, which reaches here as reason 'interaction' (not 'controlled-flip').\n const handleFloatingClose = useCallback(\n (info: DSHookFloatingContextT.OpenChange) => {\n if (info.reason === 'controlled-flip' && isControlled) onClose?.({ reason: 'controlled' });\n },\n [isControlled, onClose],\n );\n\n // Open transition \u2014 see file header (A) (focus) + \"CONTROLLED OPEN & CLOSE FLOWS\" (notify). Seeds\n // initial focus (targets stated in the block comment inside, which is the authority) and, when\n // controlled, notifies the consumer. Mount-open is not a transition, so this never fires for it (AppPanel's\n // useMountedOpenAutofocusNuance handles that one case).\n const handleFloatingOpen = useCallback(\n (info: DSHookFloatingContextT.OpenChange) => {\n /*\n * Initial focus on open, decided by HOW the picker was opened and by nothing else:\n *\n * keyboard (Enter/Space) \u2192 the selected app, falling back to the first available one\n * pointer (real click/tap) \u2192 the panel wrapper\n *\n * Neither branch depends on who rendered the trigger, because in both cases the landing spot is the\n * picker's own element:\n *\n * - \"A keyboard open lands on the current selection\" is stated by `trackFocusSelectedItem`, by the\n * public `actionRef.focusSelectedOrFirstAvailable`, and by the `aria-current` marking of the\n * selected chip.\n * - \"A pointer open lands on the panel\" exists as a explicit design preference that has never been marked explicitly as A11y violating,\n * and as such, we have no strong reason to oppose the preference yet. This is subject to review the moment any A11y concern is raised.\n *\n * `openIntentRef` is written by the two handlers that own an activation \u2014 `handleTriggerClick` from\n * the click's `detail === 0`, and `handleTriggerKeyDown` from the arrow key. That makes this a genuine\n * pointer-vs-keyboard split rather than the generic click semantic: Enter/Space on a <button> also\n * fires a click, and it counts as keyboard here.\n *\n * Reading a single value is the point. Every landing is decided here, so no other code path can write\n * a competing focus intent and have the two race.\n */\n if (openIntentRef.current === 'keyboard-first') trackFocusSelectedItem();\n else if (openIntentRef.current === 'keyboard-last') trackFocusSelectedOrLastItem();\n else trackFocusPanel();\n if (info.reason === 'controlled-flip' && isControlled) onOpen?.({ reason: 'controlled' });\n },\n [openIntentRef, trackFocusSelectedItem, trackFocusSelectedOrLastItem, trackFocusPanel, isControlled, onOpen],\n );\n\n const floatingContextConfig = useMemo(\n () =>\n ({\n placement: 'bottom',\n // Arrow tip is ~6.1px above the panel top (arrowPlacementBase16 \u2248 17.5px, tip at 11.4px from box top).\n // 4px spec gap \u2192 4 + 6.1 \u2248 10px offset.\n customOffset: [0, 10],\n externallyControlledIsOpen: resolvedIsOpen,\n withoutAnimation: true,\n onClickOutside: handleClickOutside,\n /*\n * Two conditions, and the second is a safety gate rather than a preference.\n *\n * `!onKeyDown` \u2014 a consumer who supplies onKeyDown owns the key entirely, so useFloatingContext\n * neither routes the Escape to onEscape (see handleScopedEscape) nor returns focus to the trigger.\n *\n * `!isControlled` \u2014 `returnFocusToReference` is true below, and the hook applies it on a scoped\n * Escape regardless of who owns the open state. For a controlled picker `handleScopedEscape`'s\n * `setInternalIsOpen(false)` is inert, so leaving this on would move focus to the trigger while\n * the consumer's panel is still open: focus outside an open dialog. A controlled consumer owns\n * the close, so they own the Escape and the refocus that goes with it.\n */\n closeOnEscape: !onKeyDown && !isControlled,\n onClose: handleFloatingClose,\n onEscape: handleScopedEscape,\n onOpen: handleFloatingOpen,\n returnFocusToReference: true,\n externalReferenceElement: triggerRef?.current || defaultTriggerRef.current,\n }) as DSHookFloatingContextT.Props,\n [\n handleClickOutside,\n handleScopedEscape,\n handleFloatingClose,\n handleFloatingOpen,\n isControlled,\n onKeyDown,\n resolvedIsOpen,\n triggerRef,\n ],\n );\n\n const {\n refs: floatingRefs,\n floatingStyles,\n arrowStyles,\n context: floatingContext,\n } = useFloatingContext(floatingContextConfig);\n\n const handleTriggerRef = useCallback<React.RefCallback<HTMLButtonElement>>(\n (node) => {\n (defaultTriggerRef as React.MutableRefObject<HTMLButtonElement | null>).current = node;\n floatingRefs.setReference(node);\n if (triggerRef) {\n (triggerRef as React.MutableRefObject<HTMLButtonElement | null>).current = node;\n }\n },\n [floatingRefs, triggerRef],\n );\n\n // Populates the consumer's actionRef with imperative focus methods. Fires on mount with the panel\n // node. Every method routes through the focus tracker \u2014 no DOM queries.\n const handleActionRef = useCallback<React.RefCallback<HTMLDivElement>>(\n (node) => {\n if (node && actionRef?.current) {\n actionRef.current.focusToIndex = (index: number) => {\n trackFocusItemAtIndex(index);\n };\n actionRef.current.focusWrapper = () => {\n trackFocusPanel();\n };\n actionRef.current.focusSelectedOrFirstAvailable = () => {\n trackFocusSelectedItem();\n };\n }\n },\n [actionRef, trackFocusItemAtIndex, trackFocusPanel, trackFocusSelectedItem],\n );\n\n const floatingInnerRef = useCallback<React.RefCallback<HTMLDivElement>>(\n (node) => {\n (floatingRefs.setFloating as React.RefCallback<HTMLDivElement>)(node);\n handleActionRef(node);\n },\n [floatingRefs.setFloating, handleActionRef],\n );\n\n const handleTriggerClick = useCallback(\n (e: React.MouseEvent | React.KeyboardEvent) => {\n // detail === 0 \u21D2 keyboard activation (Enter/Space), not a pointer. handleFloatingOpen reads\n // this on the next open transition to pick keyboard-vs-pointer initial focus.\n openIntentRef.current = (e as React.MouseEvent).detail === 0 ? 'keyboard-first' : 'pointer';\n onClick?.(e);\n // Uncontrolled eager notify \u2014 see file header (B) \"TRIGGER CLICK\" / \"CLICK ON TRIGGER\".\n // `resolvedIsOpen` is the pre-flip value: open \u21D2 this click closes (trigger-toggle), else opens.\n if (!isControlled) {\n if (resolvedIsOpen) onClose?.({ reason: 'trigger-toggle', event: e });\n else onOpen?.({ reason: 'trigger', event: e });\n }\n setInternalIsOpen((prev) => !prev);\n },\n [onClick, openIntentRef, setInternalIsOpen, isControlled, resolvedIsOpen, onClose, onOpen],\n );\n\n /*\n * Trigger keydown \u2014 the arrow-key open, gated on `openAndFocusOnArrowKeys`.\n *\n * This exists as its own handler rather than being folded into handleTriggerClick because a key press is\n * not a click: handleTriggerClick derives keyboard-vs-pointer from the click's `detail`, which is a\n * property of clicks. Routing a keydown through it would work by accident and read as a lie at the call\n * site. Both handlers write the same single `openIntentRef`, so there is still one place that decides the\n * landing \u2014 see handleFloatingOpen.\n *\n * ArrowDown \u2192 the selection, else the FIRST available item.\n * ArrowUp \u2192 the selection, else the LAST available item.\n *\n * Only opens; never closes or toggles. An arrow key on the trigger while the panel is already open is\n * ignored, because focus is inside the panel by then and re-seeding would yank it back.\n *\n * Enter and Space are deliberately absent: the built-in trigger is a native <button>, so the browser\n * turns those into a click and they arrive through handleTriggerClick. A consumer's TriggerComponent that\n * is also a real button gets the same for free.\n *\n * `preventDefault` stops the page from scrolling on a key that is being used to open a widget.\n *\n * Returns nothing, like any event handler. A trigger that keeps the picker CONTROLLED makes\n * `setInternalIsOpen` inert and has to run its own open for ArrowDown/ArrowUp \u2014 see\n * ds-global-header's TriggerDefaultComponent. Changing which keys are handled here means changing it\n * there too.\n */\n const handleTriggerKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (!openAndFocusOnArrowKeys) return;\n if (e.code !== 'ArrowDown' && e.code !== 'ArrowUp') return;\n if (resolvedIsOpen) return;\n\n e.preventDefault();\n openIntentRef.current = e.code === 'ArrowDown' ? 'keyboard-first' : 'keyboard-last';\n if (!isControlled) onOpen?.({ reason: 'trigger', event: e });\n setInternalIsOpen(true);\n },\n [openAndFocusOnArrowKeys, resolvedIsOpen, openIntentRef, isControlled, onOpen, setInternalIsOpen],\n );\n\n return {\n floatingStyles,\n arrowStyles,\n floatingContext,\n handleTriggerRef,\n handleTriggerClick,\n handleTriggerKeyDown,\n floatingInnerRef,\n };\n};\n"],
|
|
5
|
+
"mappings": "AAAA,YAAY,WAAW;ACAvB,SAAS,0BAAuD;AAEhE,SAAS,aAAa,SAAS,cAAc;AAkKtC,MAAM,8BAA8B,CAAC,oBAAoD;AAC9F,QAAM,EAAE,kBAAkB,eAAe,eAAe,gBAAgB,kBAAkB,IAAI;AAC9F,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAIJ,QAAM,eAAe,OAAO,WAAW;AAEvC,QAAM,EAAE,wBAAwB,8BAA8B,uBAAuB,gBAAgB,IACnG;AAEF,QAAM,oBAAoB,OAA0B,IAAI;AAGxD,QAAM,qBAAqB;AAAA,IACzB,CAAC,MAA+B;AAC9B,UAAI,CAAC,aAAc,WAAU,EAAE,QAAQ,iBAAiB,OAAO,EAAE,CAAC;AAClE,wBAAkB,KAAK;AACvB,uBAAiB,CAAe;AAAA,IAClC;AAAA,IACA,CAAC,cAAc,SAAS,mBAAmB,cAAc;AAAA,EAC3D;AAIA,QAAM,qBAAqB;AAAA,IACzB,CAAC,UAAyB;AACxB,UAAI,CAAC,aAAc,WAAU,EAAE,QAAQ,UAAU,MAAM,CAAC;AACxD,wBAAkB,KAAK;AAAA,IACzB;AAAA,IACA,CAAC,cAAc,SAAS,iBAAiB;AAAA,EAC3C;AAIA,QAAM,sBAAsB;AAAA,IAC1B,CAAC,SAA4C;AAC3C,UAAI,KAAK,WAAW,qBAAqB,aAAc,WAAU,EAAE,QAAQ,aAAa,CAAC;AAAA,IAC3F;AAAA,IACA,CAAC,cAAc,OAAO;AAAA,EACxB;AAMA,QAAM,qBAAqB;AAAA,IACzB,CAAC,SAA4C;AAwB3C,UAAI,cAAc,YAAY,iBAAkB,wBAAuB;AAAA,eAC9D,cAAc,YAAY,gBAAiB,8BAA6B;AAAA,UAC5E,iBAAgB;AACrB,UAAI,KAAK,WAAW,qBAAqB,aAAc,UAAS,EAAE,QAAQ,aAAa,CAAC;AAAA,IAC1F;AAAA,IACA,CAAC,eAAe,wBAAwB,8BAA8B,iBAAiB,cAAc,MAAM;AAAA,EAC7G;AAEA,QAAM,wBAAwB;AAAA,IAC5B,OACG;AAAA,MACC,WAAW;AAAA;AAAA;AAAA,MAGX,cAAc,CAAC,GAAG,EAAE;AAAA,MACpB,4BAA4B;AAAA,MAC5B,kBAAkB;AAAA,MAClB,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAahB,eAAe,CAAC,aAAa,CAAC;AAAA,MAC9B,SAAS;AAAA,MACT,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,wBAAwB;AAAA,MACxB,0BAA0B,YAAY,WAAW,kBAAkB;AAAA,IACrE;AAAA,IACF;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,QAAM;AAAA,IACJ,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA,SAAS;AAAA,EACX,IAAI,mBAAmB,qBAAqB;AAE5C,QAAM,mBAAmB;AAAA,IACvB,CAAC,SAAS;AACR,MAAC,kBAAuE,UAAU;AAClF,mBAAa,aAAa,IAAI;AAC9B,UAAI,YAAY;AACd,QAAC,WAAgE,UAAU;AAAA,MAC7E;AAAA,IACF;AAAA,IACA,CAAC,cAAc,UAAU;AAAA,EAC3B;AAIA,QAAM,kBAAkB;AAAA,IACtB,CAAC,SAAS;AACR,UAAI,QAAQ,WAAW,SAAS;AAC9B,kBAAU,QAAQ,eAAe,CAAC,UAAkB;AAClD,gCAAsB,KAAK;AAAA,QAC7B;AACA,kBAAU,QAAQ,eAAe,MAAM;AACrC,0BAAgB;AAAA,QAClB;AACA,kBAAU,QAAQ,gCAAgC,MAAM;AACtD,iCAAuB;AAAA,QACzB;AAAA,MACF;AAAA,IACF;AAAA,IACA,CAAC,WAAW,uBAAuB,iBAAiB,sBAAsB;AAAA,EAC5E;AAEA,QAAM,mBAAmB;AAAA,IACvB,CAAC,SAAS;AACR,MAAC,aAAa,YAAkD,IAAI;AACpE,sBAAgB,IAAI;AAAA,IACtB;AAAA,IACA,CAAC,aAAa,aAAa,eAAe;AAAA,EAC5C;AAEA,QAAM,qBAAqB;AAAA,IACzB,CAAC,MAA8C;AAG7C,oBAAc,UAAW,EAAuB,WAAW,IAAI,mBAAmB;AAClF,gBAAU,CAAC;AAGX,UAAI,CAAC,cAAc;AACjB,YAAI,eAAgB,WAAU,EAAE,QAAQ,kBAAkB,OAAO,EAAE,CAAC;AAAA,YAC/D,UAAS,EAAE,QAAQ,WAAW,OAAO,EAAE,CAAC;AAAA,MAC/C;AACA,wBAAkB,CAAC,SAAS,CAAC,IAAI;AAAA,IACnC;AAAA,IACA,CAAC,SAAS,eAAe,mBAAmB,cAAc,gBAAgB,SAAS,MAAM;AAAA,EAC3F;AA4BA,QAAM,uBAAuB;AAAA,IAC3B,CAAC,MAA2B;AAC1B,UAAI,CAAC,wBAAyB;AAC9B,UAAI,EAAE,SAAS,eAAe,EAAE,SAAS,UAAW;AACpD,UAAI,eAAgB;AAEpB,QAAE,eAAe;AACjB,oBAAc,UAAU,EAAE,SAAS,cAAc,mBAAmB;AACpE,UAAI,CAAC,aAAc,UAAS,EAAE,QAAQ,WAAW,OAAO,EAAE,CAAC;AAC3D,wBAAkB,IAAI;AAAA,IACxB;AAAA,IACA,CAAC,yBAAyB,gBAAgB,eAAe,cAAc,QAAQ,iBAAiB;AAAA,EAClG;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -9,7 +9,7 @@ const StyledButton = styled(DSButtonV3, { name: DSAppPickerName, slot: DSAppPick
|
|
|
9
9
|
height: fit-content;
|
|
10
10
|
`;
|
|
11
11
|
const Trigger = (PropsFromFloatingContext) => {
|
|
12
|
-
const { handleTriggerRef, handleTriggerClick, dialogLabelId } = PropsFromFloatingContext;
|
|
12
|
+
const { handleTriggerRef, handleTriggerClick, handleTriggerKeyDown, dialogLabelId } = PropsFromFloatingContext;
|
|
13
13
|
const {
|
|
14
14
|
propsWithDefault: { icon: Icon, renderTrigger, TriggerComponent },
|
|
15
15
|
ownerPropsConfig
|
|
@@ -21,6 +21,7 @@ const Trigger = (PropsFromFloatingContext) => {
|
|
|
21
21
|
innerRef: handleTriggerRef,
|
|
22
22
|
id: dialogLabelId,
|
|
23
23
|
onClick: handleTriggerClick,
|
|
24
|
+
onKeyDown: handleTriggerKeyDown,
|
|
24
25
|
"aria-haspopup": "dialog"
|
|
25
26
|
}
|
|
26
27
|
);
|
|
@@ -38,6 +39,7 @@ const Trigger = (PropsFromFloatingContext) => {
|
|
|
38
39
|
"aria-label": "Application picker",
|
|
39
40
|
innerRef: handleTriggerRef,
|
|
40
41
|
onClick: handleTriggerClick,
|
|
42
|
+
onKeyDown: handleTriggerKeyDown,
|
|
41
43
|
getOwnerProps: ownerPropsConfig.getOwnerProps,
|
|
42
44
|
getOwnerPropsArguments: ownerPropsConfig.getOwnerPropsArguments,
|
|
43
45
|
children: /* @__PURE__ */ jsx(Icon, {})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../scripts/build/transpile/react-shim.js", "../../../src/parts/Trigger.tsx"],
|
|
4
|
-
"sourcesContent": ["import * as React from 'react';\nexport { React };\n", "import { DSButtonV3 } from '@elliemae/ds-button-v2';\nimport { styled } from '@elliemae/ds-system';\nimport React, { useContext } from 'react';\nimport { DSAppPickerDataTestIds, DSAppPickerName, DSAppPickerSlots } from '../constants/index.js';\nimport { DSAppPickerContext } from '../DSAppPickerCTX.js';\nimport type { useAppPickerFloatingContext } from './AppPickerFloatingContext/useAppPickerFloatingContext.js';\n\n// DSButtonV3 sizes itself with `min-height` (height stays `auto`) so multi-line labels can grow.\n// As a side effect, when the trigger is a direct grid/flex child of a consumer layout with the\n// default `align-items: stretch`, it stretches to fill the container height (PUI-18354 regression:\n// the legacy DSButtonV2 trigger used a fixed `height` and did not stretch). `height: fit-content`\n// pins the trigger to its intrinsic size \u2014 mirroring the button's own `width: fit-content` \u2014 so it\n// stays ~28px regardless of layout context.\nconst StyledButton = styled(DSButtonV3, { name: DSAppPickerName, slot: DSAppPickerSlots.BUTTON })`\n height: fit-content;\n`;\n\ntype UseAppPickerFloatingContextT = ReturnType<typeof useAppPickerFloatingContext>;\ntype FloatingContextDictatedProps = {\n handleTriggerRef: UseAppPickerFloatingContextT['handleTriggerRef'];\n handleTriggerClick: UseAppPickerFloatingContextT['handleTriggerClick'];\n dialogLabelId: string;\n};\n\nexport const Trigger = (PropsFromFloatingContext: FloatingContextDictatedProps) => {\n const { handleTriggerRef, handleTriggerClick, dialogLabelId } = PropsFromFloatingContext;\n const {\n propsWithDefault: { icon: Icon, renderTrigger, TriggerComponent },\n ownerPropsConfig,\n } = useContext(DSAppPickerContext);\n\n // Precedence: TriggerComponent > renderTrigger > the built-in button. TriggerComponent wins when\n // both custom APIs are supplied \u2014 it is the one that receives `dialogLabelId`, so preferring\n // renderTrigger here would drop the dialog's accessible name.\n if (TriggerComponent) {\n return (\n <TriggerComponent\n innerRef={handleTriggerRef}\n id={dialogLabelId}\n onClick={handleTriggerClick}\n aria-haspopup=\"dialog\"\n />\n );\n }\n\n if (renderTrigger) {\n // DEPRECATED (v4.x) \u2014 see DEV-002. Plain function call (not a React element) preserves the\n // legacy DSPopover render-prop contract: consumers receive `ref` as a regular prop, no\n // forwardRef required. The contract has no channel for `dialogLabelId`, so on this path naming\n // the dialog is the consumer's: put an id on the trigger element and inject the matching\n // aria-labelledby through the FLOATING_WRAPPER slot (or pass `id` to DSAppPicker, which makes\n // the generated id predictable). Covered by DSAppPicker.renderTrigger-ioc-aria-labelledby.test.js.\n return renderTrigger({ ref: handleTriggerRef });\n }\n return (\n <StyledButton\n data-testid={DSAppPickerDataTestIds.BUTTON}\n id={dialogLabelId}\n buttonType=\"icon\"\n aria-haspopup=\"dialog\"\n aria-label=\"Application picker\"\n innerRef={handleTriggerRef}\n onClick={handleTriggerClick}\n getOwnerProps={ownerPropsConfig.getOwnerProps}\n getOwnerPropsArguments={ownerPropsConfig.getOwnerPropsArguments}\n >\n <Icon />\n </StyledButton>\n );\n};\n\nTrigger.displayName = 'DSAppPicker.Trigger';\n"],
|
|
5
|
-
"mappings": "AAAA,YAAY,WAAW;
|
|
4
|
+
"sourcesContent": ["import * as React from 'react';\nexport { React };\n", "import { DSButtonV3 } from '@elliemae/ds-button-v2';\nimport { styled } from '@elliemae/ds-system';\nimport React, { useContext } from 'react';\nimport { DSAppPickerDataTestIds, DSAppPickerName, DSAppPickerSlots } from '../constants/index.js';\nimport { DSAppPickerContext } from '../DSAppPickerCTX.js';\nimport type { useAppPickerFloatingContext } from './AppPickerFloatingContext/useAppPickerFloatingContext.js';\n\n// DSButtonV3 sizes itself with `min-height` (height stays `auto`) so multi-line labels can grow.\n// As a side effect, when the trigger is a direct grid/flex child of a consumer layout with the\n// default `align-items: stretch`, it stretches to fill the container height (PUI-18354 regression:\n// the legacy DSButtonV2 trigger used a fixed `height` and did not stretch). `height: fit-content`\n// pins the trigger to its intrinsic size \u2014 mirroring the button's own `width: fit-content` \u2014 so it\n// stays ~28px regardless of layout context.\nconst StyledButton = styled(DSButtonV3, { name: DSAppPickerName, slot: DSAppPickerSlots.BUTTON })`\n height: fit-content;\n`;\n\ntype UseAppPickerFloatingContextT = ReturnType<typeof useAppPickerFloatingContext>;\ntype FloatingContextDictatedProps = {\n handleTriggerRef: UseAppPickerFloatingContextT['handleTriggerRef'];\n handleTriggerClick: UseAppPickerFloatingContextT['handleTriggerClick'];\n handleTriggerKeyDown: UseAppPickerFloatingContextT['handleTriggerKeyDown'];\n dialogLabelId: string;\n};\n\nexport const Trigger = (PropsFromFloatingContext: FloatingContextDictatedProps) => {\n const { handleTriggerRef, handleTriggerClick, handleTriggerKeyDown, dialogLabelId } = PropsFromFloatingContext;\n const {\n propsWithDefault: { icon: Icon, renderTrigger, TriggerComponent },\n ownerPropsConfig,\n } = useContext(DSAppPickerContext);\n\n // Precedence: TriggerComponent > renderTrigger > the built-in button. TriggerComponent wins when\n // both custom APIs are supplied \u2014 it is the one that receives `dialogLabelId`, so preferring\n // renderTrigger here would drop the dialog's accessible name.\n if (TriggerComponent) {\n return (\n <TriggerComponent\n innerRef={handleTriggerRef}\n id={dialogLabelId}\n onClick={handleTriggerClick}\n onKeyDown={handleTriggerKeyDown}\n aria-haspopup=\"dialog\"\n />\n );\n }\n\n if (renderTrigger) {\n // DEPRECATED (v4.x) \u2014 see DEV-002. Plain function call (not a React element) preserves the\n // legacy DSPopover render-prop contract: consumers receive `ref` as a regular prop, no\n // forwardRef required. The contract has no channel for `dialogLabelId`, so on this path naming\n // the dialog is the consumer's: put an id on the trigger element and inject the matching\n // aria-labelledby through the FLOATING_WRAPPER slot (or pass `id` to DSAppPicker, which makes\n // the generated id predictable). Covered by DSAppPicker.renderTrigger-ioc-aria-labelledby.test.js.\n return renderTrigger({ ref: handleTriggerRef });\n }\n return (\n <StyledButton\n data-testid={DSAppPickerDataTestIds.BUTTON}\n id={dialogLabelId}\n buttonType=\"icon\"\n aria-haspopup=\"dialog\"\n aria-label=\"Application picker\"\n innerRef={handleTriggerRef}\n onClick={handleTriggerClick}\n onKeyDown={handleTriggerKeyDown}\n getOwnerProps={ownerPropsConfig.getOwnerProps}\n getOwnerPropsArguments={ownerPropsConfig.getOwnerPropsArguments}\n >\n <Icon />\n </StyledButton>\n );\n};\n\nTrigger.displayName = 'DSAppPicker.Trigger';\n"],
|
|
5
|
+
"mappings": "AAAA,YAAY,WAAW;ACqCjB;AArCN,SAAS,kBAAkB;AAC3B,SAAS,cAAc;AACvB,SAAgB,kBAAkB;AAClC,SAAS,wBAAwB,iBAAiB,wBAAwB;AAC1E,SAAS,0BAA0B;AASnC,MAAM,eAAe,OAAO,YAAY,EAAE,MAAM,iBAAiB,MAAM,iBAAiB,OAAO,CAAC;AAAA;AAAA;AAYzF,MAAM,UAAU,CAAC,6BAA2D;AACjF,QAAM,EAAE,kBAAkB,oBAAoB,sBAAsB,cAAc,IAAI;AACtF,QAAM;AAAA,IACJ,kBAAkB,EAAE,MAAM,MAAM,eAAe,iBAAiB;AAAA,IAChE;AAAA,EACF,IAAI,WAAW,kBAAkB;AAKjC,MAAI,kBAAkB;AACpB,WACE;AAAA,MAAC;AAAA;AAAA,QACC,UAAU;AAAA,QACV,IAAI;AAAA,QACJ,SAAS;AAAA,QACT,WAAW;AAAA,QACX,iBAAc;AAAA;AAAA,IAChB;AAAA,EAEJ;AAEA,MAAI,eAAe;AAOjB,WAAO,cAAc,EAAE,KAAK,iBAAiB,CAAC;AAAA,EAChD;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,eAAa,uBAAuB;AAAA,MACpC,IAAI;AAAA,MACJ,YAAW;AAAA,MACX,iBAAc;AAAA,MACd,cAAW;AAAA,MACX,UAAU;AAAA,MACV,SAAS;AAAA,MACT,WAAW;AAAA,MACX,eAAe,iBAAiB;AAAA,MAChC,wBAAwB,iBAAiB;AAAA,MAEzC,8BAAC,QAAK;AAAA;AAAA,EACR;AAEJ;AAEA,QAAQ,cAAc;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -13,7 +13,8 @@ const defaultProps = {
|
|
|
13
13
|
customApps: [],
|
|
14
14
|
sectionTitle: "APPLICATIONS",
|
|
15
15
|
customSectionTitle: "CUSTOM APPLICATIONS",
|
|
16
|
-
icon: () => /* @__PURE__ */ jsx(MenuPicker, { color: ["brand-primary", "700"], size: "m" })
|
|
16
|
+
icon: () => /* @__PURE__ */ jsx(MenuPicker, { color: ["brand-primary", "700"], size: "m" }),
|
|
17
|
+
openAndFocusOnArrowKeys: false
|
|
17
18
|
};
|
|
18
19
|
const DSAppPickerPropTypes = {
|
|
19
20
|
...getPropsPerSlotPropTypes(DSAppPickerName, DSAppPickerSlots),
|
|
@@ -28,6 +29,9 @@ const DSAppPickerPropTypes = {
|
|
|
28
29
|
sectionTitle: PropTypes.string.description("main section title").defaultValue("APPLICATIONS"),
|
|
29
30
|
customSectionTitle: PropTypes.string.description("custom section title").defaultValue("CUSTOM APPLICATIONS"),
|
|
30
31
|
icon: PropTypes.func.description("trigger button s icon").defaultValue(MenuPicker),
|
|
32
|
+
openAndFocusOnArrowKeys: PropTypes.bool.description(
|
|
33
|
+
"Opt in to arrow-key opening on the trigger. ArrowDown opens and focuses the selected application, falling back to the first available one; ArrowUp opens and focuses the selected application, falling back to the last available one. The picker owns the whole flow \u2014 no key handler, no isOpen wiring and no actionRef needed. Independent of actionRef, which remains the imperative handle for moving focus from your own logic."
|
|
34
|
+
).defaultValue(false),
|
|
31
35
|
renderTrigger: PropTypes.func.description(
|
|
32
36
|
"Custom trigger, called as a plain function with { ref }. Superseded by TriggerComponent: this render-prop cannot receive the id that the panel's aria-labelledby points at, so the dialog is left without an accessible name. TriggerComponent takes precedence when both are supplied."
|
|
33
37
|
).deprecated({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../scripts/build/transpile/react-shim.js", "../../src/react-desc-prop-types.tsx"],
|
|
4
|
-
"sourcesContent": ["import * as React from 'react';\nexport { React };\n", "/* eslint-disable @typescript-eslint/no-empty-interface */\nimport React from 'react';\nimport { MenuPicker } from '@elliemae/ds-icons';\nimport type { SvgIconT } from '@elliemae/ds-icons';\nimport type { GlobalAttributesT, XstyledProps, DSPropTypesSchema, ValidationMap } from '@elliemae/ds-props-helpers';\nimport {\n PropTypes,\n getPropsPerSlotPropTypes,\n globalAttributesPropTypes,\n xstyledPropTypes,\n} from '@elliemae/ds-props-helpers';\nimport type { TypescriptHelpersT } from '@elliemae/ds-typescript-helpers';\nimport { DSAppPickerName, DSAppPickerSlots } from './constants/index.js';\n\nexport declare namespace DSAppPickerT {\n export interface AppItem {\n label: string;\n icon: React.ComponentType<{ className: string; size: string }>;\n onClick?: (e: React.MouseEvent, item: AppItem) => void | null;\n disabled?: boolean;\n applyAriaDisabled?: boolean;\n id?: string;\n selected?: boolean;\n wrapText?: boolean;\n }\n\n export type ActionRef = React.MutableRefObject<{\n focusToIndex?: (index: number) => void;\n focusSelectedOrFirstAvailable?: () => void;\n focusWrapper: () => void;\n }>;\n\n /**\n * Why the panel opened. `trigger` carries the activating event and fires synchronously in the\n * trigger's click handler (AppPicker owns the open). `controlled` fires when the consumer flips\n * the `isOpen` prop themselves \u2014 no event, delivered when that flip takes effect.\n */\n export type OpenChange =\n { reason: 'trigger'; event: React.MouseEvent | React.KeyboardEvent } | { reason: 'controlled' };\n\n /**\n * Why the panel closed. The event-carrying reasons fire synchronously from the handler that owns\n * the close (AppPicker in uncontrolled mode): `escape` (a scoped Escape), `click-outside`, and\n * `trigger-toggle` (the trigger clicked while open). `controlled` fires when the consumer flips the\n * `isOpen` prop themselves \u2014 no event, delivered when that flip takes effect.\n */\n export type CloseChange =\n | { reason: 'escape'; event: KeyboardEvent }\n | { reason: 'click-outside'; event: MouseEvent | TouchEvent }\n | { reason: 'trigger-toggle'; event: React.MouseEvent | React.KeyboardEvent }\n | { reason: 'controlled' };\n\n export interface RequiredProps {}\n\n export type SlotFunctionArguments = {\n dsApppickerRoot: () => object;\n dsApppickerGroup: () => object;\n dsApppickerItem: () => object;\n dsApppickerTitle: () => object;\n dsApppickerSeparator: () => object;\n dsApppickerRow: () => object;\n dsApppickerChip: () => object;\n dsApppickerButton: () => object;\n dsApppickerFloatingWrapper: () => object;\n };\n\n export interface DefaultProps {\n apps: AppItem[];\n customApps: AppItem[];\n sectionTitle: string;\n customSectionTitle: string;\n icon: React.ComponentType<SvgIconT.Props>;\n }\n\n interface RenderTriggerProp {\n ref: React.RefCallback<HTMLButtonElement>;\n [key: string]: unknown;\n }\n\n /**\n * Props handed to a `TriggerComponent`. This is the only trigger API that participates in the\n * dialog's accessible-name wiring: `id` is the element the panel's `aria-labelledby` points at,\n * so a TriggerComponent that drops it leaves the dialog unnamed.\n *\n * `innerRef`, not `ref`, deliberately: under React 18 a `ref` passed through JSX is consumed by\n * the fiber and never reaches a plain function component's props unless it is wrapped in\n * `forwardRef`. `innerRef` is the established Dimsum convention (see `ds-system`'s styled\n * components) and keeps `forwardRef` optional for the consumer \u2014 which is the same ergonomic\n * concern that produced DEV-002, solved here without the plain-function call.\n */\n export interface TriggerComponentProps {\n innerRef: React.RefCallback<HTMLButtonElement>;\n id: string;\n onClick: (e: React.MouseEvent | React.KeyboardEvent) => void;\n 'aria-haspopup': 'dialog';\n }\n\n export interface OptionalProps extends TypescriptHelpersT.PropsForGlobalOnSlots<\n typeof DSAppPickerName,\n typeof DSAppPickerSlots\n > {\n onOpen?: (info: OpenChange) => void;\n onClose?: (info: CloseChange) => void;\n onClick?: (e: React.MouseEvent | React.KeyboardEvent) => void;\n onClickOutside?: (e: MouseEvent | React.MouseEvent) => void;\n onKeyDown?: (e: React.KeyboardEvent) => void;\n actionRef?: ActionRef;\n /**\n * @deprecated v4.x \u2014 use `TriggerComponent`. `renderTrigger` receives only `ref` and cannot be\n * given the id the dialog's `aria-labelledby` points at, so the panel is left without an\n * accessible name. See DEV-002 in KNOWN_INTENTIONAL_DEVIATIONS.md.\n */\n renderTrigger?: (props: RenderTriggerProp) => React.ReactElement | null;\n /**\n * Custom trigger, rendered as a real React element so it owns its own fiber. Takes precedence\n * over `renderTrigger` when both are supplied.\n */\n TriggerComponent?: React.ComponentType<TriggerComponentProps>;\n isOpen?: boolean;\n triggerRef?: React.RefObject<HTMLButtonElement>;\n }\n\n export interface Props\n extends\n Partial<DefaultProps>,\n OptionalProps,\n Omit<GlobalAttributesT<HTMLDivElement>, keyof DefaultProps | keyof OptionalProps | keyof XstyledProps>,\n XstyledProps,\n RequiredProps {}\n\n export interface InternalProps\n extends\n DefaultProps,\n OptionalProps,\n Omit<GlobalAttributesT<HTMLDivElement>, keyof DefaultProps | keyof OptionalProps | keyof XstyledProps>,\n XstyledProps,\n RequiredProps {}\n}\n\nexport const defaultProps: DSAppPickerT.DefaultProps = {\n apps: [],\n customApps: [],\n sectionTitle: 'APPLICATIONS',\n customSectionTitle: 'CUSTOM APPLICATIONS',\n icon: () => <MenuPicker color={['brand-primary', '700']} size=\"m\" />,\n};\n\n// =============================================================================\n// PropTypes\n// =============================================================================\n\nexport const DSAppPickerPropTypes: DSPropTypesSchema<DSAppPickerT.Props> = {\n ...getPropsPerSlotPropTypes(DSAppPickerName, DSAppPickerSlots),\n ...globalAttributesPropTypes,\n ...xstyledPropTypes,\n apps: PropTypes.array\n .description(\n 'Main items. Format: [{ label:string, icon:component, onClick:func, disabled:bool, selected:bool }]. ' +\n 'Conditionally required: at least one of \"apps\" or \"customApps\" must contain items \u2014 a picker with ' +\n 'nothing to pick has no purpose, so providing both empty throws.',\n )\n .defaultValue([]),\n customApps: PropTypes.array\n .description(\n 'Custom items. Format: [{ label:string, icon:component, onClick:func, disabled:bool, selected:bool }]. ' +\n 'Conditionally required: at least one of \"apps\" or \"customApps\" must contain items \u2014 a picker with ' +\n 'nothing to pick has no purpose, so providing both empty throws.',\n )\n .defaultValue([]),\n sectionTitle: PropTypes.string.description('main section title').defaultValue('APPLICATIONS'),\n customSectionTitle: PropTypes.string.description('custom section title').defaultValue('CUSTOM APPLICATIONS'),\n icon: PropTypes.func.description('trigger button s icon').defaultValue(MenuPicker),\n renderTrigger: PropTypes.func\n .description(\n 'Custom trigger, called as a plain function with { ref }. Superseded by TriggerComponent: this ' +\n \"render-prop cannot receive the id that the panel's aria-labelledby points at, so the dialog \" +\n 'is left without an accessible name. TriggerComponent takes precedence when both are supplied.',\n )\n .deprecated({\n version: '4.x',\n message: 'Use \"TriggerComponent\" instead \u2014 it receives the id required for the dialog accessible name.',\n }),\n TriggerComponent: PropTypes.func.description(\n 'Custom trigger rendered as a React element. Receives { innerRef, id, onClick, aria-haspopup }; ' +\n 'the id must be applied to the focusable trigger element for the panel to have an accessible name. ' +\n 'Takes precedence over the deprecated renderTrigger when both are supplied.',\n ),\n actionRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: PropTypes.any })]).description(\n 'Ref containing a focusToIndex method. This method allows you to focus any App inside the AppPicker.',\n ),\n isOpen: PropTypes.bool.description('Wether the AppPicker should be open or not.'),\n onOpen: PropTypes.func.description(\n 'Callback when the AppPicker opens; receives { reason: \"trigger\" | \"controlled\", event? }.',\n ),\n onClose: PropTypes.func.description(\n 'Callback when the AppPicker closes; receives { reason: \"escape\" | \"click-outside\" | \"trigger-toggle\" | \"controlled\", event? }.',\n ),\n onKeyDown: PropTypes.func.description('OnKeyDown handler callback.'),\n onClick: PropTypes.func.description('Custom onClick for Trigger component.'),\n onClickOutside: PropTypes.func.description('Callback event when the user clicks outside the App Picker.'),\n triggerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: PropTypes.any })]).description(\n 'Ref to the trigger button.',\n ),\n};\n\nexport const DSAppPickerPropTypesSchema = DSAppPickerPropTypes as unknown as ValidationMap<DSAppPickerT.Props>;\n"],
|
|
5
|
-
"mappings": "AAAA,YAAY,WAAW;
|
|
4
|
+
"sourcesContent": ["import * as React from 'react';\nexport { React };\n", "/* eslint-disable @typescript-eslint/no-empty-interface */\nimport React from 'react';\nimport { MenuPicker } from '@elliemae/ds-icons';\nimport type { SvgIconT } from '@elliemae/ds-icons';\nimport type { GlobalAttributesT, XstyledProps, DSPropTypesSchema, ValidationMap } from '@elliemae/ds-props-helpers';\nimport {\n PropTypes,\n getPropsPerSlotPropTypes,\n globalAttributesPropTypes,\n xstyledPropTypes,\n} from '@elliemae/ds-props-helpers';\nimport type { TypescriptHelpersT } from '@elliemae/ds-typescript-helpers';\nimport { DSAppPickerName, DSAppPickerSlots } from './constants/index.js';\n\nexport declare namespace DSAppPickerT {\n export interface AppItem {\n label: string;\n icon: React.ComponentType<{ className: string; size: string }>;\n onClick?: (e: React.MouseEvent, item: AppItem) => void | null;\n disabled?: boolean;\n applyAriaDisabled?: boolean;\n id?: string;\n selected?: boolean;\n wrapText?: boolean;\n }\n\n export type ActionRef = React.MutableRefObject<{\n focusToIndex?: (index: number) => void;\n focusSelectedOrFirstAvailable?: () => void;\n focusWrapper: () => void;\n }>;\n\n /**\n * Why the panel opened. `trigger` carries the activating event and fires synchronously in the\n * trigger's click handler (AppPicker owns the open). `controlled` fires when the consumer flips\n * the `isOpen` prop themselves \u2014 no event, delivered when that flip takes effect.\n */\n export type OpenChange =\n { reason: 'trigger'; event: React.MouseEvent | React.KeyboardEvent } | { reason: 'controlled' };\n\n /**\n * Why the panel closed. The event-carrying reasons fire synchronously from the handler that owns\n * the close (AppPicker in uncontrolled mode): `escape` (a scoped Escape), `click-outside`, and\n * `trigger-toggle` (the trigger clicked while open). `controlled` fires when the consumer flips the\n * `isOpen` prop themselves \u2014 no event, delivered when that flip takes effect.\n */\n export type CloseChange =\n | { reason: 'escape'; event: KeyboardEvent }\n | { reason: 'click-outside'; event: MouseEvent | TouchEvent }\n | { reason: 'trigger-toggle'; event: React.MouseEvent | React.KeyboardEvent }\n | { reason: 'controlled' };\n\n export interface RequiredProps {}\n\n export type SlotFunctionArguments = {\n dsApppickerRoot: () => object;\n dsApppickerGroup: () => object;\n dsApppickerItem: () => object;\n dsApppickerTitle: () => object;\n dsApppickerSeparator: () => object;\n dsApppickerRow: () => object;\n dsApppickerChip: () => object;\n dsApppickerButton: () => object;\n dsApppickerFloatingWrapper: () => object;\n };\n\n export interface DefaultProps {\n apps: AppItem[];\n customApps: AppItem[];\n sectionTitle: string;\n customSectionTitle: string;\n icon: React.ComponentType<SvgIconT.Props>;\n /**\n * Opt in to arrow-key opening on the trigger: ArrowDown opens and focuses the selected application\n * (the first available one when nothing is selected), ArrowUp opens and focuses the selected\n * application (the last available one when nothing is selected).\n *\n * The picker owns this flow end to end \u2014 a consumer does not wire a key handler, drive `isOpen`, or\n * reach through `actionRef` to place the focus. Defaults to `false`, so it is purely additive: a\n * picker that does not opt in behaves exactly as before, with the arrow keys doing nothing on the\n * trigger.\n *\n * Independent of `actionRef`, which stays the imperative handle for a consumer to move focus from\n * their own business logic at any time.\n */\n openAndFocusOnArrowKeys: boolean;\n }\n\n interface RenderTriggerProp {\n ref: React.RefCallback<HTMLButtonElement>;\n [key: string]: unknown;\n }\n\n /**\n * Props handed to a `TriggerComponent`. This is the only trigger API that participates in the\n * dialog's accessible-name wiring: `id` is the element the panel's `aria-labelledby` points at,\n * so a TriggerComponent that drops it leaves the dialog unnamed.\n *\n * `innerRef`, not `ref`, deliberately: under React 18 a `ref` passed through JSX is consumed by\n * the fiber and never reaches a plain function component's props unless it is wrapped in\n * `forwardRef`. `innerRef` is the established Dimsum convention (see `ds-system`'s styled\n * components) and keeps `forwardRef` optional for the consumer \u2014 which is the same ergonomic\n * concern that produced DEV-002, solved here without the plain-function call.\n */\n export interface TriggerComponentProps {\n innerRef: React.RefCallback<HTMLButtonElement>;\n id: string;\n onClick: (e: React.MouseEvent | React.KeyboardEvent) => void;\n /**\n * Attach to the trigger element alongside `onClick`. It is what implements\n * `openAndFocusOnArrowKeys`, and it is a keydown handler in its own right \u2014 never route a keydown\n * through `onClick` to get the same effect: `onClick` infers keyboard activation from the click's\n * `detail`, which is a property of clicks, not of key presses.\n *\n * Harmless to attach when `openAndFocusOnArrowKeys` is false \u2014 it ignores every key in that case.\n *\n * The keys it acts on are ArrowDown and ArrowUp. A trigger that drives its own `isOpen` \u2014 which makes\n * the picker's internal flip inert \u2014 has to open on those same two keys itself.\n */\n onKeyDown: (e: React.KeyboardEvent) => void;\n 'aria-haspopup': 'dialog';\n }\n\n /**\n * How the pending open was triggered, which is the only input to where initial focus lands:\n * `pointer` \u2192 the panel \u00B7 `keyboard-first` \u2192 the selection, else the first available item \u00B7\n * `keyboard-last` \u2192 the selection, else the last available item.\n */\n export type OpenIntent = 'pointer' | 'keyboard-first' | 'keyboard-last';\n\n export interface OptionalProps extends TypescriptHelpersT.PropsForGlobalOnSlots<\n typeof DSAppPickerName,\n typeof DSAppPickerSlots\n > {\n onOpen?: (info: OpenChange) => void;\n onClose?: (info: CloseChange) => void;\n onClick?: (e: React.MouseEvent | React.KeyboardEvent) => void;\n onClickOutside?: (e: MouseEvent | React.MouseEvent) => void;\n onKeyDown?: (e: React.KeyboardEvent) => void;\n actionRef?: ActionRef;\n /**\n * @deprecated v4.x \u2014 use `TriggerComponent`. `renderTrigger` receives only `ref` and cannot be\n * given the id the dialog's `aria-labelledby` points at, so the panel is left without an\n * accessible name. See DEV-002 in KNOWN_INTENTIONAL_DEVIATIONS.md.\n */\n renderTrigger?: (props: RenderTriggerProp) => React.ReactElement | null;\n /**\n * Custom trigger, rendered as a real React element so it owns its own fiber. Takes precedence\n * over `renderTrigger` when both are supplied.\n */\n TriggerComponent?: React.ComponentType<TriggerComponentProps>;\n isOpen?: boolean;\n triggerRef?: React.RefObject<HTMLButtonElement>;\n }\n\n export interface Props\n extends\n Partial<DefaultProps>,\n OptionalProps,\n Omit<GlobalAttributesT<HTMLDivElement>, keyof DefaultProps | keyof OptionalProps | keyof XstyledProps>,\n XstyledProps,\n RequiredProps {}\n\n export interface InternalProps\n extends\n DefaultProps,\n OptionalProps,\n Omit<GlobalAttributesT<HTMLDivElement>, keyof DefaultProps | keyof OptionalProps | keyof XstyledProps>,\n XstyledProps,\n RequiredProps {}\n}\n\nexport const defaultProps: DSAppPickerT.DefaultProps = {\n apps: [],\n customApps: [],\n sectionTitle: 'APPLICATIONS',\n customSectionTitle: 'CUSTOM APPLICATIONS',\n icon: () => <MenuPicker color={['brand-primary', '700']} size=\"m\" />,\n openAndFocusOnArrowKeys: false,\n};\n\n// =============================================================================\n// PropTypes\n// =============================================================================\n\nexport const DSAppPickerPropTypes: DSPropTypesSchema<DSAppPickerT.Props> = {\n ...getPropsPerSlotPropTypes(DSAppPickerName, DSAppPickerSlots),\n ...globalAttributesPropTypes,\n ...xstyledPropTypes,\n apps: PropTypes.array\n .description(\n 'Main items. Format: [{ label:string, icon:component, onClick:func, disabled:bool, selected:bool }]. ' +\n 'Conditionally required: at least one of \"apps\" or \"customApps\" must contain items \u2014 a picker with ' +\n 'nothing to pick has no purpose, so providing both empty throws.',\n )\n .defaultValue([]),\n customApps: PropTypes.array\n .description(\n 'Custom items. Format: [{ label:string, icon:component, onClick:func, disabled:bool, selected:bool }]. ' +\n 'Conditionally required: at least one of \"apps\" or \"customApps\" must contain items \u2014 a picker with ' +\n 'nothing to pick has no purpose, so providing both empty throws.',\n )\n .defaultValue([]),\n sectionTitle: PropTypes.string.description('main section title').defaultValue('APPLICATIONS'),\n customSectionTitle: PropTypes.string.description('custom section title').defaultValue('CUSTOM APPLICATIONS'),\n icon: PropTypes.func.description('trigger button s icon').defaultValue(MenuPicker),\n openAndFocusOnArrowKeys: PropTypes.bool\n .description(\n 'Opt in to arrow-key opening on the trigger. ArrowDown opens and focuses the selected application, ' +\n 'falling back to the first available one; ArrowUp opens and focuses the selected application, falling ' +\n 'back to the last available one. The picker owns the whole flow \u2014 no key handler, no isOpen wiring and ' +\n 'no actionRef needed. Independent of actionRef, which remains the imperative handle for moving focus ' +\n 'from your own logic.',\n )\n .defaultValue(false),\n renderTrigger: PropTypes.func\n .description(\n 'Custom trigger, called as a plain function with { ref }. Superseded by TriggerComponent: this ' +\n \"render-prop cannot receive the id that the panel's aria-labelledby points at, so the dialog \" +\n 'is left without an accessible name. TriggerComponent takes precedence when both are supplied.',\n )\n .deprecated({\n version: '4.x',\n message: 'Use \"TriggerComponent\" instead \u2014 it receives the id required for the dialog accessible name.',\n }),\n TriggerComponent: PropTypes.func.description(\n 'Custom trigger rendered as a React element. Receives { innerRef, id, onClick, aria-haspopup }; ' +\n 'the id must be applied to the focusable trigger element for the panel to have an accessible name. ' +\n 'Takes precedence over the deprecated renderTrigger when both are supplied.',\n ),\n actionRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: PropTypes.any })]).description(\n 'Ref containing a focusToIndex method. This method allows you to focus any App inside the AppPicker.',\n ),\n isOpen: PropTypes.bool.description('Wether the AppPicker should be open or not.'),\n onOpen: PropTypes.func.description(\n 'Callback when the AppPicker opens; receives { reason: \"trigger\" | \"controlled\", event? }.',\n ),\n onClose: PropTypes.func.description(\n 'Callback when the AppPicker closes; receives { reason: \"escape\" | \"click-outside\" | \"trigger-toggle\" | \"controlled\", event? }.',\n ),\n onKeyDown: PropTypes.func.description('OnKeyDown handler callback.'),\n onClick: PropTypes.func.description('Custom onClick for Trigger component.'),\n onClickOutside: PropTypes.func.description('Callback event when the user clicks outside the App Picker.'),\n triggerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: PropTypes.any })]).description(\n 'Ref to the trigger button.',\n ),\n};\n\nexport const DSAppPickerPropTypesSchema = DSAppPickerPropTypes as unknown as ValidationMap<DSAppPickerT.Props>;\n"],
|
|
5
|
+
"mappings": "AAAA,YAAY,WAAW;ACiLT;AA/Kd,SAAS,kBAAkB;AAG3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,iBAAiB,wBAAwB;AAgK3C,MAAM,eAA0C;AAAA,EACrD,MAAM,CAAC;AAAA,EACP,YAAY,CAAC;AAAA,EACb,cAAc;AAAA,EACd,oBAAoB;AAAA,EACpB,MAAM,MAAM,oBAAC,cAAW,OAAO,CAAC,iBAAiB,KAAK,GAAG,MAAK,KAAI;AAAA,EAClE,yBAAyB;AAC3B;AAMO,MAAM,uBAA8D;AAAA,EACzE,GAAG,yBAAyB,iBAAiB,gBAAgB;AAAA,EAC7D,GAAG;AAAA,EACH,GAAG;AAAA,EACH,MAAM,UAAU,MACb;AAAA,IACC;AAAA,EAGF,EACC,aAAa,CAAC,CAAC;AAAA,EAClB,YAAY,UAAU,MACnB;AAAA,IACC;AAAA,EAGF,EACC,aAAa,CAAC,CAAC;AAAA,EAClB,cAAc,UAAU,OAAO,YAAY,oBAAoB,EAAE,aAAa,cAAc;AAAA,EAC5F,oBAAoB,UAAU,OAAO,YAAY,sBAAsB,EAAE,aAAa,qBAAqB;AAAA,EAC3G,MAAM,UAAU,KAAK,YAAY,uBAAuB,EAAE,aAAa,UAAU;AAAA,EACjF,yBAAyB,UAAU,KAChC;AAAA,IACC;AAAA,EAKF,EACC,aAAa,KAAK;AAAA,EACrB,eAAe,UAAU,KACtB;AAAA,IACC;AAAA,EAGF,EACC,WAAW;AAAA,IACV,SAAS;AAAA,IACT,SAAS;AAAA,EACX,CAAC;AAAA,EACH,kBAAkB,UAAU,KAAK;AAAA,IAC/B;AAAA,EAGF;AAAA,EACA,WAAW,UAAU,UAAU,CAAC,UAAU,MAAM,UAAU,MAAM,EAAE,SAAS,UAAU,IAAI,CAAC,CAAC,CAAC,EAAE;AAAA,IAC5F;AAAA,EACF;AAAA,EACA,QAAQ,UAAU,KAAK,YAAY,6CAA6C;AAAA,EAChF,QAAQ,UAAU,KAAK;AAAA,IACrB;AAAA,EACF;AAAA,EACA,SAAS,UAAU,KAAK;AAAA,IACtB;AAAA,EACF;AAAA,EACA,WAAW,UAAU,KAAK,YAAY,6BAA6B;AAAA,EACnE,SAAS,UAAU,KAAK,YAAY,uCAAuC;AAAA,EAC3E,gBAAgB,UAAU,KAAK,YAAY,6DAA6D;AAAA,EACxG,YAAY,UAAU,UAAU,CAAC,UAAU,MAAM,UAAU,MAAM,EAAE,SAAS,UAAU,IAAI,CAAC,CAAC,CAAC,EAAE;AAAA,IAC7F;AAAA,EACF;AACF;AAEO,MAAM,6BAA6B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../scripts/build/transpile/react-shim.js", "../../../src/typescript-testing/typescript-app-picker-valid.tsx"],
|
|
4
|
-
"sourcesContent": ["import * as React from 'react';\nexport { React };\n", "/* eslint-disable @typescript-eslint/no-unused-vars, no-unused-vars */\nimport { DSAppPicker } from '../index.js';\nimport type { DSAppPickerT } from '../index.js';\n\n// test we expose the namespace and the namespace follows our deliverable conventions\ntype ComponentPropsForApp = DSAppPickerT.Props;\ntype ComponentPropsInternals = DSAppPickerT.InternalProps;\ntype ComponentPropsDefaultProps = DSAppPickerT.DefaultProps;\ntype ComponentPropsOptionalProps = DSAppPickerT.OptionalProps;\ntype ComponentPropsRequiredProps = DSAppPickerT.RequiredProps;\n\nconst testOptionalProps: ComponentPropsOptionalProps = {};\nconst testRequiredProps: ComponentPropsRequiredProps = {};\n\n// difference Props and InternalProps is that InternalProps has all the default props filled in\n// Props allows for partial defaults\nconst testPartialDefaults: Partial<ComponentPropsDefaultProps> = {\n apps: [],\n customApps: [],\n sectionTitle: '',\n customSectionTitle: '',\n icon: () => null,\n};\n\nconst testProps: ComponentPropsForApp = {\n ...testOptionalProps,\n ...testRequiredProps,\n ...testPartialDefaults,\n};\nconst testPropsAsSyntax = {\n ...testOptionalProps,\n ...testRequiredProps,\n ...testPartialDefaults,\n} as ComponentPropsForApp;\n// InternalProps requires all defaults to be filled in\nconst testCompleteDefaults: Required<ComponentPropsDefaultProps> = {\n apps: [],\n customApps: [],\n sectionTitle: '',\n customSectionTitle: '',\n icon: () => null,\n};\n\nconst testInternalProps: ComponentPropsInternals = {\n ...testOptionalProps,\n ...testRequiredProps,\n ...testCompleteDefaults,\n};\nconst testInternalPropsAsSyntax = {\n ...testOptionalProps,\n ...testRequiredProps,\n ...testCompleteDefaults,\n} as ComponentPropsInternals;\n\n// using the explicit type definition, if there is an error, it will be marked on the key that is wrong\nconst testExplicitDefinition: ComponentPropsForApp = {\n apps: [],\n customApps: [],\n sectionTitle: '',\n customSectionTitle: '',\n icon: () => null,\n};\n\n// using the \"as\" syntax, if there is an error, it will be marking the whole object as wrong because it is not compatible with the type\nconst testInferedTypeCompatibility = {\n apps: [],\n customApps: [],\n sectionTitle: '',\n customSectionTitle: '',\n icon: () => null,\n} as ComponentPropsForApp;\n\n// TriggerComponent is typed as a real component, so its props arrive through the fiber. The exported\n// TriggerComponentProps type is what a consumer annotates their own trigger with \u2014 if that type stops\n// being reachable from the namespace, or its members change shape, this stops compiling.\nconst CustomTrigger: React.ComponentType<DSAppPickerT.TriggerComponentProps> = ({ innerRef, id, onClick, ...rest }) => (\n <button ref={innerRef} id={id} onClick={onClick} type=\"button\" {...rest}>\n Open apps\n </button>\n);\n\n// A consumer may also declare the trigger inline without naming the props type; both must compile.\nconst testTriggerComponentProp: ComponentPropsOptionalProps = {\n TriggerComponent: CustomTrigger,\n};\n\nconst ExampleUsageComponent = () => (\n <>\n {/* works with explicitly casted props, all syntaxes */}\n <DSAppPicker {...testExplicitDefinition} />\n <DSAppPicker {...testInferedTypeCompatibility} />\n {/* works with inline values */}\n <DSAppPicker apps={[]} customApps={[]} sectionTitle=\"\" customSectionTitle=\"\" icon={() => null} />\n {/* multiple auto inference works */}\n <DSAppPicker apps={[]} customApps={[]} sectionTitle=\"\" customSectionTitle=\"\" icon={() => null} />\n {/* works with partial defaults */}\n {/* TriggerComponent \u2014 the supported custom trigger API */}\n <DSAppPicker apps={[]} TriggerComponent={CustomTrigger} />\n <DSAppPicker {...testTriggerComponentProp} apps={[]} />\n {/* renderTrigger \u2014 deprecated (v4.x) but must still type-check until removal */}\n <DSAppPicker apps={[]} renderTrigger={({ ref }) => <button ref={ref} type=\"button\" aria-label=\"banana\" />} />\n </>\n);\n"],
|
|
5
|
-
"mappings": "AAAA,YAAY,WAAW;
|
|
4
|
+
"sourcesContent": ["import * as React from 'react';\nexport { React };\n", "/* eslint-disable @typescript-eslint/no-unused-vars, no-unused-vars */\nimport { DSAppPicker } from '../index.js';\nimport type { DSAppPickerT } from '../index.js';\n\n// test we expose the namespace and the namespace follows our deliverable conventions\ntype ComponentPropsForApp = DSAppPickerT.Props;\ntype ComponentPropsInternals = DSAppPickerT.InternalProps;\ntype ComponentPropsDefaultProps = DSAppPickerT.DefaultProps;\ntype ComponentPropsOptionalProps = DSAppPickerT.OptionalProps;\ntype ComponentPropsRequiredProps = DSAppPickerT.RequiredProps;\n\nconst testOptionalProps: ComponentPropsOptionalProps = {};\nconst testRequiredProps: ComponentPropsRequiredProps = {};\n\n// difference Props and InternalProps is that InternalProps has all the default props filled in\n// Props allows for partial defaults\nconst testPartialDefaults: Partial<ComponentPropsDefaultProps> = {\n apps: [],\n customApps: [],\n sectionTitle: '',\n customSectionTitle: '',\n icon: () => null,\n};\n\nconst testProps: ComponentPropsForApp = {\n ...testOptionalProps,\n ...testRequiredProps,\n ...testPartialDefaults,\n};\nconst testPropsAsSyntax = {\n ...testOptionalProps,\n ...testRequiredProps,\n ...testPartialDefaults,\n} as ComponentPropsForApp;\n// InternalProps requires all defaults to be filled in\nconst testCompleteDefaults: Required<ComponentPropsDefaultProps> = {\n apps: [],\n customApps: [],\n sectionTitle: '',\n customSectionTitle: '',\n icon: () => null,\n openAndFocusOnArrowKeys: false,\n};\n\nconst testInternalProps: ComponentPropsInternals = {\n ...testOptionalProps,\n ...testRequiredProps,\n ...testCompleteDefaults,\n};\nconst testInternalPropsAsSyntax = {\n ...testOptionalProps,\n ...testRequiredProps,\n ...testCompleteDefaults,\n} as ComponentPropsInternals;\n\n// using the explicit type definition, if there is an error, it will be marked on the key that is wrong\nconst testExplicitDefinition: ComponentPropsForApp = {\n apps: [],\n customApps: [],\n sectionTitle: '',\n customSectionTitle: '',\n icon: () => null,\n};\n\n// using the \"as\" syntax, if there is an error, it will be marking the whole object as wrong because it is not compatible with the type\nconst testInferedTypeCompatibility = {\n apps: [],\n customApps: [],\n sectionTitle: '',\n customSectionTitle: '',\n icon: () => null,\n} as ComponentPropsForApp;\n\n// TriggerComponent is typed as a real component, so its props arrive through the fiber. The exported\n// TriggerComponentProps type is what a consumer annotates their own trigger with \u2014 if that type stops\n// being reachable from the namespace, or its members change shape, this stops compiling.\nconst CustomTrigger: React.ComponentType<DSAppPickerT.TriggerComponentProps> = ({ innerRef, id, onClick, ...rest }) => (\n <button ref={innerRef} id={id} onClick={onClick} type=\"button\" {...rest}>\n Open apps\n </button>\n);\n\n// A consumer may also declare the trigger inline without naming the props type; both must compile.\nconst testTriggerComponentProp: ComponentPropsOptionalProps = {\n TriggerComponent: CustomTrigger,\n};\n\nconst ExampleUsageComponent = () => (\n <>\n {/* works with explicitly casted props, all syntaxes */}\n <DSAppPicker {...testExplicitDefinition} />\n <DSAppPicker {...testInferedTypeCompatibility} />\n {/* works with inline values */}\n <DSAppPicker apps={[]} customApps={[]} sectionTitle=\"\" customSectionTitle=\"\" icon={() => null} />\n {/* multiple auto inference works */}\n <DSAppPicker apps={[]} customApps={[]} sectionTitle=\"\" customSectionTitle=\"\" icon={() => null} />\n {/* works with partial defaults */}\n {/* TriggerComponent \u2014 the supported custom trigger API */}\n <DSAppPicker apps={[]} TriggerComponent={CustomTrigger} />\n <DSAppPicker {...testTriggerComponentProp} apps={[]} />\n {/* renderTrigger \u2014 deprecated (v4.x) but must still type-check until removal */}\n <DSAppPicker apps={[]} renderTrigger={({ ref }) => <button ref={ref} type=\"button\" aria-label=\"banana\" />} />\n </>\n);\n"],
|
|
5
|
+
"mappings": "AAAA,YAAY,WAAW;AC6ErB,SAWA,UAXA,KAWA,YAXA;AA5EF,SAAS,mBAAmB;AAU5B,MAAM,oBAAiD,CAAC;AACxD,MAAM,oBAAiD,CAAC;AAIxD,MAAM,sBAA2D;AAAA,EAC/D,MAAM,CAAC;AAAA,EACP,YAAY,CAAC;AAAA,EACb,cAAc;AAAA,EACd,oBAAoB;AAAA,EACpB,MAAM,MAAM;AACd;AAEA,MAAM,YAAkC;AAAA,EACtC,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AACL;AACA,MAAM,oBAAoB;AAAA,EACxB,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AACL;AAEA,MAAM,uBAA6D;AAAA,EACjE,MAAM,CAAC;AAAA,EACP,YAAY,CAAC;AAAA,EACb,cAAc;AAAA,EACd,oBAAoB;AAAA,EACpB,MAAM,MAAM;AAAA,EACZ,yBAAyB;AAC3B;AAEA,MAAM,oBAA6C;AAAA,EACjD,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AACL;AACA,MAAM,4BAA4B;AAAA,EAChC,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AACL;AAGA,MAAM,yBAA+C;AAAA,EACnD,MAAM,CAAC;AAAA,EACP,YAAY,CAAC;AAAA,EACb,cAAc;AAAA,EACd,oBAAoB;AAAA,EACpB,MAAM,MAAM;AACd;AAGA,MAAM,+BAA+B;AAAA,EACnC,MAAM,CAAC;AAAA,EACP,YAAY,CAAC;AAAA,EACb,cAAc;AAAA,EACd,oBAAoB;AAAA,EACpB,MAAM,MAAM;AACd;AAKA,MAAM,gBAAyE,CAAC,EAAE,UAAU,IAAI,SAAS,GAAG,KAAK,MAC/G,oBAAC,YAAO,KAAK,UAAU,IAAQ,SAAkB,MAAK,UAAU,GAAG,MAAM,uBAEzE;AAIF,MAAM,2BAAwD;AAAA,EAC5D,kBAAkB;AACpB;AAEA,MAAM,wBAAwB,MAC5B,iCAEE;AAAA,sBAAC,eAAa,GAAG,wBAAwB;AAAA,EACzC,oBAAC,eAAa,GAAG,8BAA8B;AAAA,EAE/C,oBAAC,eAAY,MAAM,CAAC,GAAG,YAAY,CAAC,GAAG,cAAa,IAAG,oBAAmB,IAAG,MAAM,MAAM,MAAM;AAAA,EAE/F,oBAAC,eAAY,MAAM,CAAC,GAAG,YAAY,CAAC,GAAG,cAAa,IAAG,oBAAmB,IAAG,MAAM,MAAM,MAAM;AAAA,EAG/F,oBAAC,eAAY,MAAM,CAAC,GAAG,kBAAkB,eAAe;AAAA,EACxD,oBAAC,eAAa,GAAG,0BAA0B,MAAM,CAAC,GAAG;AAAA,EAErD,oBAAC,eAAY,MAAM,CAAC,GAAG,eAAe,CAAC,EAAE,IAAI,MAAM,oBAAC,YAAO,KAAU,MAAK,UAAS,cAAW,UAAS,GAAI;AAAA,GAC7G;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { getContractProps } from "@elliemae/ds-props-helpers";
|
|
3
|
+
import { DSAppPickerPropTypes } from "../react-desc-prop-types.js";
|
|
4
|
+
const getDSAppPickerContractProps = (props) => getContractProps(props, DSAppPickerPropTypes);
|
|
5
|
+
export {
|
|
6
|
+
getDSAppPickerContractProps
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=getDSAppPickerContractProps.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../scripts/build/transpile/react-shim.js", "../../../src/util/getDSAppPickerContractProps.ts"],
|
|
4
|
+
"sourcesContent": ["import * as React from 'react';\nexport { React };\n", "import { getContractProps } from '@elliemae/ds-props-helpers';\nimport { DSAppPickerPropTypes, type DSAppPickerT } from '../react-desc-prop-types.js';\n\n/**\n * Filters an arbitrary object down to the props DSAppPicker actually accepts.\n *\n * **Who this is for.** Components that wrap DSAppPicker and receive its props indirectly \u2014 as a\n * configuration object from their own consumer rather than as JSX props.\n *\n * The canonical case is `ds-global-header`, whose `toolbar` config carries a `componentProps` object for\n * the `ds-app-picker` item type. Such a wrapper cannot spread that object blindly: its config type is\n * DSAppPicker's props *plus* the wrapper's own \u2014 `triggerOnClick` there \u2014 and those must not leak into\n * DSAppPicker.\n *\n * Do not swap this for a hand-listed set of forwardable keys. A hand-list has no way to fail when the\n * contract grows: the new prop type-checks at the call site, is dropped silently at runtime, and nothing\n * reports it. Deriving the set from the contract is what makes that class of bug impossible.\n *\n * **Why a utility instead of exporting the schema.** `DSAppPickerPropTypes` is internal. It exists to\n * drive prop validation and the Storybook props table; that it also happens to describe the exact set\n * of forwardable props is a convenience today, not a promise. Exposing the schema would weld those\n * concerns together and make every future change to it a public API change. Exposing this utility\n * instead means the public commitment is only \"you get the props DSAppPicker accepts\" \u2014 if the two\n * ever need to diverge, only this function changes.\n *\n * Anything the contract does not declare is discarded. Arbitrary `data-*` attributes are kept (a\n * supported consumer surface no schema can enumerate).\n *\n * `undefined` vs `null` \u2014 see `getContractProps` for the implemented rule and the React precedent it\n * follows. In short: `undefined` is dropped (\"I am not defining this, you decide\" \u2014 the wrapper's\n * default stands), `null` is forwarded (\"I am explicitly asking for nothing\") and is then judged by\n * DSAppPicker's own strictly-typed contract, which rejects it for props whose declared type does not\n * admit null. To disable a handler, pass an explicit no-op (`() => {}`) \u2014 it honours the type and\n * states the intent unambiguously.\n *\n * @param props - the object to filter, typically a wrapper's `componentProps` configuration\n * @returns a new object containing only props DSAppPicker declares\n */\nexport const getDSAppPickerContractProps = (props: object): Partial<DSAppPickerT.Props> =>\n getContractProps<DSAppPickerT.Props>(props, DSAppPickerPropTypes);\n"],
|
|
5
|
+
"mappings": "AAAA,YAAY,WAAW;ACAvB,SAAS,wBAAwB;AACjC,SAAS,4BAA+C;AAqCjD,MAAM,8BAA8B,CAAC,UAC1C,iBAAqC,OAAO,oBAAoB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -12,7 +12,7 @@ export declare const useAppPicker: (props: DSAppPickerT.Props) => {
|
|
|
12
12
|
trackFocusTrigger: () => void;
|
|
13
13
|
trackFocusPanel: () => void;
|
|
14
14
|
trackFocusSelectedItem: () => void;
|
|
15
|
-
|
|
15
|
+
trackFocusSelectedOrLastItem: () => void;
|
|
16
16
|
trackFocusItemAtIndex: (index: number) => void;
|
|
17
17
|
};
|
|
18
18
|
propsWithDefault: DSAppPickerT.InternalProps;
|
|
@@ -387,7 +387,7 @@ export declare const useAppPicker: (props: DSAppPickerT.Props) => {
|
|
|
387
387
|
wrap?: string | undefined | undefined;
|
|
388
388
|
}, never>>;
|
|
389
389
|
xstyledProps: import("@elliemae/ds-props-helpers").XstyledProps;
|
|
390
|
-
|
|
390
|
+
openIntentRef: React.MutableRefObject<DSAppPickerT.OpenIntent>;
|
|
391
391
|
mountedOpenAutofocusPendingRef: React.MutableRefObject<boolean>;
|
|
392
392
|
resolvedIsOpen: boolean;
|
|
393
393
|
setInternalIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
@@ -13,7 +13,7 @@ export declare const useFocusTracker: (config: FocusTrackerConfigT) => {
|
|
|
13
13
|
trackFocusTrigger: () => void;
|
|
14
14
|
trackFocusPanel: () => void;
|
|
15
15
|
trackFocusSelectedItem: () => void;
|
|
16
|
-
|
|
16
|
+
trackFocusSelectedOrLastItem: () => void;
|
|
17
17
|
trackFocusItemAtIndex: (index: number) => void;
|
|
18
18
|
};
|
|
19
19
|
export {};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { DSAppPicker, AppPickerWithSchema } from './DSAppPicker.js';
|
|
2
2
|
export { DSAppPickerName, DSAppPickerSlots, DSAppPickerDataTestIds } from './constants/index.js';
|
|
3
|
+
export { getDSAppPickerContractProps } from './util/getDSAppPickerContractProps.js';
|
|
3
4
|
export type { DSAppPickerT } from './react-desc-prop-types.js';
|
|
@@ -134,7 +134,7 @@ import type { DSAppPickerT } from '../../react-desc-prop-types.js';
|
|
|
134
134
|
* Eager (uncontrolled) and controlled-flip paths are mutually exclusive on `isControlled`.
|
|
135
135
|
* the consumer is notified exactly once per transition — never twice.
|
|
136
136
|
* On Escape specifically
|
|
137
|
-
*
|
|
137
|
+
* closeOnEscape is false whenever the picker is controlled (and whenever a consumer supplies onKeyDown)
|
|
138
138
|
* └→ AppPicker's onEscape never runs —
|
|
139
139
|
* └→ the close arrives purely by listening to the flag transition
|
|
140
140
|
* ------------------------------------------------------------------------------------------------------------------------
|
|
@@ -150,7 +150,7 @@ type ConfiguredAppPickerCtx = ReturnType<typeof useAppPicker>;
|
|
|
150
150
|
type AppPickerFloatingContextConfig = {
|
|
151
151
|
propsWithDefault: DSAppPickerT.InternalProps;
|
|
152
152
|
focusTrackers: ConfiguredAppPickerCtx['focusTrackers'];
|
|
153
|
-
|
|
153
|
+
openIntentRef: ConfiguredAppPickerCtx['openIntentRef'];
|
|
154
154
|
resolvedIsOpen: ConfiguredAppPickerCtx['resolvedIsOpen'];
|
|
155
155
|
setInternalIsOpen: ConfiguredAppPickerCtx['setInternalIsOpen'];
|
|
156
156
|
};
|
|
@@ -165,6 +165,7 @@ export declare const useAppPickerFloatingContext: (appPickerConfig: AppPickerFlo
|
|
|
165
165
|
};
|
|
166
166
|
handleTriggerRef: (instance: HTMLButtonElement | null) => void | React.DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES[keyof React.DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES];
|
|
167
167
|
handleTriggerClick: (e: React.MouseEvent | React.KeyboardEvent) => void;
|
|
168
|
+
handleTriggerKeyDown: (e: React.KeyboardEvent) => void;
|
|
168
169
|
floatingInnerRef: (instance: HTMLDivElement | null) => void | React.DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES[keyof React.DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES];
|
|
169
170
|
};
|
|
170
171
|
export {};
|
|
@@ -4,6 +4,7 @@ type UseAppPickerFloatingContextT = ReturnType<typeof useAppPickerFloatingContex
|
|
|
4
4
|
type FloatingContextDictatedProps = {
|
|
5
5
|
handleTriggerRef: UseAppPickerFloatingContextT['handleTriggerRef'];
|
|
6
6
|
handleTriggerClick: UseAppPickerFloatingContextT['handleTriggerClick'];
|
|
7
|
+
handleTriggerKeyDown: UseAppPickerFloatingContextT['handleTriggerKeyDown'];
|
|
7
8
|
dialogLabelId: string;
|
|
8
9
|
};
|
|
9
10
|
export declare const Trigger: {
|
|
@@ -70,6 +70,20 @@ export declare namespace DSAppPickerT {
|
|
|
70
70
|
sectionTitle: string;
|
|
71
71
|
customSectionTitle: string;
|
|
72
72
|
icon: React.ComponentType<SvgIconT.Props>;
|
|
73
|
+
/**
|
|
74
|
+
* Opt in to arrow-key opening on the trigger: ArrowDown opens and focuses the selected application
|
|
75
|
+
* (the first available one when nothing is selected), ArrowUp opens and focuses the selected
|
|
76
|
+
* application (the last available one when nothing is selected).
|
|
77
|
+
*
|
|
78
|
+
* The picker owns this flow end to end — a consumer does not wire a key handler, drive `isOpen`, or
|
|
79
|
+
* reach through `actionRef` to place the focus. Defaults to `false`, so it is purely additive: a
|
|
80
|
+
* picker that does not opt in behaves exactly as before, with the arrow keys doing nothing on the
|
|
81
|
+
* trigger.
|
|
82
|
+
*
|
|
83
|
+
* Independent of `actionRef`, which stays the imperative handle for a consumer to move focus from
|
|
84
|
+
* their own business logic at any time.
|
|
85
|
+
*/
|
|
86
|
+
openAndFocusOnArrowKeys: boolean;
|
|
73
87
|
}
|
|
74
88
|
interface RenderTriggerProp {
|
|
75
89
|
ref: React.RefCallback<HTMLButtonElement>;
|
|
@@ -90,8 +104,26 @@ export declare namespace DSAppPickerT {
|
|
|
90
104
|
innerRef: React.RefCallback<HTMLButtonElement>;
|
|
91
105
|
id: string;
|
|
92
106
|
onClick: (e: React.MouseEvent | React.KeyboardEvent) => void;
|
|
107
|
+
/**
|
|
108
|
+
* Attach to the trigger element alongside `onClick`. It is what implements
|
|
109
|
+
* `openAndFocusOnArrowKeys`, and it is a keydown handler in its own right — never route a keydown
|
|
110
|
+
* through `onClick` to get the same effect: `onClick` infers keyboard activation from the click's
|
|
111
|
+
* `detail`, which is a property of clicks, not of key presses.
|
|
112
|
+
*
|
|
113
|
+
* Harmless to attach when `openAndFocusOnArrowKeys` is false — it ignores every key in that case.
|
|
114
|
+
*
|
|
115
|
+
* The keys it acts on are ArrowDown and ArrowUp. A trigger that drives its own `isOpen` — which makes
|
|
116
|
+
* the picker's internal flip inert — has to open on those same two keys itself.
|
|
117
|
+
*/
|
|
118
|
+
onKeyDown: (e: React.KeyboardEvent) => void;
|
|
93
119
|
'aria-haspopup': 'dialog';
|
|
94
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* How the pending open was triggered, which is the only input to where initial focus lands:
|
|
123
|
+
* `pointer` → the panel · `keyboard-first` → the selection, else the first available item ·
|
|
124
|
+
* `keyboard-last` → the selection, else the last available item.
|
|
125
|
+
*/
|
|
126
|
+
type OpenIntent = 'pointer' | 'keyboard-first' | 'keyboard-last';
|
|
95
127
|
interface OptionalProps extends TypescriptHelpersT.PropsForGlobalOnSlots<typeof DSAppPickerName, typeof DSAppPickerSlots> {
|
|
96
128
|
onOpen?: (info: OpenChange) => void;
|
|
97
129
|
onClose?: (info: CloseChange) => void;
|