@oxy.so/bloom 4.23.0 → 4.24.0
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/docs/adoption-matrix.mdx +10 -10
- package/docs/ai-chat.mdx +7 -3
- package/docs/bottom-sheet.mdx +2 -0
- package/docs/dialog.mdx +28 -0
- package/docs/sidebar.mdx +2 -0
- package/lib/commonjs/ai-chat/AiChatContainer.js +22 -11
- package/lib/commonjs/ai-chat/AiChatContainer.js.map +1 -1
- package/lib/commonjs/ai-chat/AiChatShell.js +8 -2
- package/lib/commonjs/ai-chat/AiChatShell.js.map +1 -1
- package/lib/commonjs/ai-chat/use-panel-interaction.js +20 -33
- package/lib/commonjs/ai-chat/use-panel-interaction.js.map +1 -1
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js +10 -3
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.web.js +28 -22
- package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
- package/lib/commonjs/dialog/DialogBottomSheet.js +9 -4
- package/lib/commonjs/dialog/DialogBottomSheet.js.map +1 -1
- package/lib/commonjs/overlay/ModalKeyboard.js +88 -0
- package/lib/commonjs/overlay/ModalKeyboard.js.map +1 -0
- package/lib/commonjs/overlay/modal-keyboard.js +131 -0
- package/lib/commonjs/overlay/modal-keyboard.js.map +1 -0
- package/lib/commonjs/overlay/stack.js +16 -0
- package/lib/commonjs/overlay/stack.js.map +1 -1
- package/lib/commonjs/sidebar/Sidebar.js +12 -4
- package/lib/commonjs/sidebar/Sidebar.js.map +1 -1
- package/lib/module/ai-chat/AiChatContainer.js +22 -11
- package/lib/module/ai-chat/AiChatContainer.js.map +1 -1
- package/lib/module/ai-chat/AiChatShell.js +8 -2
- package/lib/module/ai-chat/AiChatShell.js.map +1 -1
- package/lib/module/ai-chat/use-panel-interaction.js +20 -32
- package/lib/module/ai-chat/use-panel-interaction.js.map +1 -1
- package/lib/module/bottom-sheet/BottomSheetBase.js +10 -3
- package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/module/dialog/Dialog.web.js +28 -22
- package/lib/module/dialog/Dialog.web.js.map +1 -1
- package/lib/module/dialog/DialogBottomSheet.js +9 -4
- package/lib/module/dialog/DialogBottomSheet.js.map +1 -1
- package/lib/module/overlay/ModalKeyboard.js +84 -0
- package/lib/module/overlay/ModalKeyboard.js.map +1 -0
- package/lib/module/overlay/modal-keyboard.js +124 -0
- package/lib/module/overlay/modal-keyboard.js.map +1 -0
- package/lib/module/overlay/stack.js +15 -0
- package/lib/module/overlay/stack.js.map +1 -1
- package/lib/module/sidebar/Sidebar.js +12 -4
- package/lib/module/sidebar/Sidebar.js.map +1 -1
- package/lib/typescript/commonjs/ai-chat/AiChatContainer.d.ts +5 -4
- package/lib/typescript/commonjs/ai-chat/AiChatContainer.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/AiChatShell.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/use-panel-interaction.d.ts +10 -1
- package/lib/typescript/commonjs/ai-chat/use-panel-interaction.d.ts.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts.map +1 -1
- package/lib/typescript/commonjs/overlay/ModalKeyboard.d.ts +32 -0
- package/lib/typescript/commonjs/overlay/ModalKeyboard.d.ts.map +1 -0
- package/lib/typescript/commonjs/overlay/modal-keyboard.d.ts +17 -0
- package/lib/typescript/commonjs/overlay/modal-keyboard.d.ts.map +1 -0
- package/lib/typescript/commonjs/overlay/stack.d.ts +6 -0
- package/lib/typescript/commonjs/overlay/stack.d.ts.map +1 -1
- package/lib/typescript/commonjs/sidebar/Sidebar.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/AiChatContainer.d.ts +5 -4
- package/lib/typescript/module/ai-chat/AiChatContainer.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/AiChatShell.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/use-panel-interaction.d.ts +10 -1
- package/lib/typescript/module/ai-chat/use-panel-interaction.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/module/dialog/DialogBottomSheet.d.ts.map +1 -1
- package/lib/typescript/module/overlay/ModalKeyboard.d.ts +32 -0
- package/lib/typescript/module/overlay/ModalKeyboard.d.ts.map +1 -0
- package/lib/typescript/module/overlay/modal-keyboard.d.ts +17 -0
- package/lib/typescript/module/overlay/modal-keyboard.d.ts.map +1 -0
- package/lib/typescript/module/overlay/stack.d.ts +6 -0
- package/lib/typescript/module/overlay/stack.d.ts.map +1 -1
- package/lib/typescript/module/sidebar/Sidebar.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ai-chat/AiChatContainer.tsx +18 -8
- package/src/ai-chat/AiChatShell.tsx +12 -2
- package/src/ai-chat/use-panel-interaction.ts +20 -34
- package/src/bottom-sheet/BottomSheetBase.tsx +8 -0
- package/src/dialog/Dialog.web.tsx +22 -18
- package/src/dialog/DialogBottomSheet.tsx +9 -4
- package/src/overlay/ModalKeyboard.tsx +88 -0
- package/src/overlay/modal-keyboard.ts +133 -0
- package/src/overlay/stack.ts +13 -0
- package/src/sidebar/Sidebar.tsx +7 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-panel-interaction.d.ts","sourceRoot":"","sources":["../../../../src/ai-chat/use-panel-interaction.ts"],"names":[],"mappings":"AACA,OAAO,EAAyB,KAAK,IAAI,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"use-panel-interaction.d.ts","sourceRoot":"","sources":["../../../../src/ai-chat/use-panel-interaction.ts"],"names":[],"mappings":"AACA,OAAO,EAAyB,KAAK,IAAI,EAAE,MAAM,cAAc,CAAC;AAKhE;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,IAAI,0CAwCnF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BottomSheetBase.d.ts","sourceRoot":"","sources":["../../../../src/bottom-sheet/BottomSheetBase.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"BottomSheetBase.d.ts","sourceRoot":"","sources":["../../../../src/bottom-sheet/BottomSheetBase.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAoC/B,OAAO,KAAK,EAAE,cAAc,EAA2C,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAsB7G,eAAO,MAAM,eAAe,6FA8rB1B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Dialog.web.d.ts","sourceRoot":"","sources":["../../../../src/dialog/Dialog.web.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Dialog.web.d.ts","sourceRoot":"","sources":["../../../../src/dialog/Dialog.web.tsx"],"names":[],"mappings":"AA4EA,OAAO,KAAK,EAIV,WAAW,EACZ,MAAM,SAAS,CAAC;AAmDjB;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,MAAM,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,EAAE,WAAW,2CAazD;AAiuBD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,gBAAgB,8PAG5B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DialogBottomSheet.d.ts","sourceRoot":"","sources":["../../../../src/dialog/DialogBottomSheet.tsx"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAsB,WAAW,EAAE,MAAM,SAAS,CAAC;AAE/D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,IAAI,EAAE,cAAc,EACpB,SAAS,EACT,OAAO,EACP,MAAM,EACN,KAAK,EACL,WAAW,EACX,OAAO,EACP,MAAM,EACN,cAAyC,EACzC,UAAiB,EACjB,iBAAwB,EACxB,cAA+C,EAC/C,UAAU,EACV,KAAK,EACL,KAAK,EACL,UAAU,EACV,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,KAAK,EACL,QAAQ,GACT,EAAE,sBAAsB,
|
|
1
|
+
{"version":3,"file":"DialogBottomSheet.d.ts","sourceRoot":"","sources":["../../../../src/dialog/DialogBottomSheet.tsx"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAsB,WAAW,EAAE,MAAM,SAAS,CAAC;AAE/D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,IAAI,EAAE,cAAc,EACpB,SAAS,EACT,OAAO,EACP,MAAM,EACN,KAAK,EACL,WAAW,EACX,OAAO,EACP,MAAM,EACN,cAAyC,EACzC,UAAiB,EACjB,iBAAwB,EACxB,cAA+C,EAC/C,UAAU,EACV,KAAK,EACL,KAAK,EACL,UAAU,EACV,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,KAAK,EACL,QAAQ,GACT,EAAE,sBAAsB,2CAkRxB;AAQD;;;;GAIG;AACH,MAAM,MAAM,sBAAsB,GAAG,IAAI,CACvC,WAAW,EACT,SAAS,GACT,MAAM,GACN,WAAW,GACX,SAAS,GACT,QAAQ,GACR,OAAO,GACP,aAAa,GACb,SAAS,GACT,QAAQ,GACR,gBAAgB,GAChB,YAAY,GACZ,mBAAmB,GACnB,gBAAgB,GAChB,YAAY,GACZ,OAAO,GACP,OAAO,GACP,YAAY,GACZ,gBAAgB,GAChB,gBAAgB,GAChB,oBAAoB,GACpB,OAAO,GACP,UAAU,CACb,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { View } from 'react-native';
|
|
3
|
+
export interface ModalKeyboardProps {
|
|
4
|
+
/** The surface's panel: where focus goes, and what Tab stays inside. */
|
|
5
|
+
panelRef: React.RefObject<View | null>;
|
|
6
|
+
/** The surface has started its exit: focus goes back now, Escape is spent. */
|
|
7
|
+
closing?: boolean;
|
|
8
|
+
/** Whether Escape may dismiss it (a dialog's `dismissOnBackdrop`). */
|
|
9
|
+
dismissible: boolean;
|
|
10
|
+
dismiss: () => void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* An open modal surface's keyboard and focus, WEB. Renders nothing; it must
|
|
14
|
+
* sit INSIDE the surface's `OverlayRoot`, which is how it knows the surface's
|
|
15
|
+
* place in the overlay stack. Used by the web `Dialog` (card and side drawers)
|
|
16
|
+
* and the web `BottomSheet`.
|
|
17
|
+
*
|
|
18
|
+
* - Focus moves INTO the panel when it opens — to whatever the content
|
|
19
|
+
* focused itself (an `autoFocus` field), else the first tabbable, else the
|
|
20
|
+
* panel — and goes BACK to the element that had it once the surface starts
|
|
21
|
+
* closing (or unmounts), unless the host has already put it somewhere else.
|
|
22
|
+
* - Tab and Shift+Tab wrap at the panel's edges.
|
|
23
|
+
* - Escape dismisses (when `dismissible`), from anywhere — a text field of the
|
|
24
|
+
* surface included, which react-native-web keeps every keydown from
|
|
25
|
+
* bubbling out of (`./modal-keyboard.ts`).
|
|
26
|
+
*
|
|
27
|
+
* All three belong to the TOP-MOST surface only: a menu or a second dialog
|
|
28
|
+
* opened from this one takes them until it closes, so Escape closes one layer
|
|
29
|
+
* at a time. Inert without a DOM, so native never runs any of it.
|
|
30
|
+
*/
|
|
31
|
+
export declare function ModalKeyboard({ panelRef, closing, dismissible, dismiss }: ModalKeyboardProps): null;
|
|
32
|
+
//# sourceMappingURL=ModalKeyboard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModalKeyboard.d.ts","sourceRoot":"","sources":["../../../../src/overlay/ModalKeyboard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAyC,MAAM,OAAO,CAAC;AAC9D,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAMzC,MAAM,WAAW,kBAAkB;IACjC,wEAAwE;IACxE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IACvC,8EAA8E;IAC9E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,sEAAsE;IACtE,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,OAAe,EAAE,WAAW,EAAE,OAAO,EAAE,EAAE,kBAAkB,QAmDpG"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** The elements inside `node` that Tab can land on, in document order. */
|
|
2
|
+
export declare function tabbablesWithin(node: HTMLElement | null): HTMLElement[];
|
|
3
|
+
/**
|
|
4
|
+
* Keep Tab inside `node`: from the last tabbable (or from outside) forward to
|
|
5
|
+
* the first, from the first (or from outside) backward to the last. Returns
|
|
6
|
+
* whether it moved focus.
|
|
7
|
+
*/
|
|
8
|
+
export declare function wrapTab(event: KeyboardEvent, node: HTMLElement | null): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Dismiss on Escape — see the file comment for the three steps. `claim` runs
|
|
11
|
+
* in the capture phase and says whether this surface is the one the key is
|
|
12
|
+
* for (the top-most, nothing anchored open above it). An open anchored
|
|
13
|
+
* surface (a menu, a select's list) always keeps the key: it is the top layer
|
|
14
|
+
* whatever the caller's own bookkeeping says.
|
|
15
|
+
*/
|
|
16
|
+
export declare function listenForEscape(claim: () => boolean, dismiss: () => void): () => void;
|
|
17
|
+
//# sourceMappingURL=modal-keyboard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"modal-keyboard.d.ts","sourceRoot":"","sources":["../../../../src/overlay/modal-keyboard.ts"],"names":[],"mappings":"AAiDA,0EAA0E;AAC1E,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,WAAW,EAAE,CASvE;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO,CAa/E;AAYD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAkCrF"}
|
|
@@ -120,5 +120,11 @@ export declare function releaseOverlayRank(rank: number): void;
|
|
|
120
120
|
export declare function resetOverlayStack(): void;
|
|
121
121
|
/** In-flow shell drawers yield interaction to any portaled surface above them. */
|
|
122
122
|
export declare function hasActiveOverlays(): boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Whether the surface painting at `layer` is the top-most one open — the one
|
|
125
|
+
* Escape and Tab belong to. A menu opened from inside a dialog takes a higher
|
|
126
|
+
* rank than the dialog, so the dialog stands aside until it closes.
|
|
127
|
+
*/
|
|
128
|
+
export declare function isTopmostOverlayLayer(layer: OverlayLayer): boolean;
|
|
123
129
|
export {};
|
|
124
130
|
//# sourceMappingURL=stack.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stack.d.ts","sourceRoot":"","sources":["../../../../src/overlay/stack.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB,OAAO,CAAC;AAEvC;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C;;;GAGG;AACH,eAAO,MAAM,aAAa,QAC8C,CAAC;AAEzE,0EAA0E;AAC1E,MAAM,WAAW,YAAY;IAC3B,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,yBAAyB;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,yCAAyC;IACzC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,UAAU,QAAQ;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACnB;AAED,OAAO,CAAC,MAAM,CAAC;IAEb,IAAI,8BAA8B,EAAE,QAAQ,GAAG,SAAS,CAAC;CAC1D;AAgBD,kFAAkF;AAClF,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAIvD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAI3C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAItD;AAED,+DAA+D;AAC/D,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAIrD;AAED,4CAA4C;AAC5C,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED,kFAAkF;AAClF,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C"}
|
|
1
|
+
{"version":3,"file":"stack.d.ts","sourceRoot":"","sources":["../../../../src/overlay/stack.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB,OAAO,CAAC;AAEvC;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C;;;GAGG;AACH,eAAO,MAAM,aAAa,QAC8C,CAAC;AAEzE,0EAA0E;AAC1E,MAAM,WAAW,YAAY;IAC3B,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,yBAAyB;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,yCAAyC;IACzC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,UAAU,QAAQ;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACnB;AAED,OAAO,CAAC,MAAM,CAAC;IAEb,IAAI,8BAA8B,EAAE,QAAQ,GAAG,SAAS,CAAC;CAC1D;AAgBD,kFAAkF;AAClF,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAIvD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAI3C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAItD;AAED,+DAA+D;AAC/D,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAIrD;AAED,4CAA4C;AAC5C,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED,kFAAkF;AAClF,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAMlE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Sidebar.d.ts","sourceRoot":"","sources":["../../../../src/sidebar/Sidebar.tsx"],"names":[],"mappings":"AACA,OAAO,KAAkE,MAAM,OAAO,CAAC;AA4CvF,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"Sidebar.d.ts","sourceRoot":"","sources":["../../../../src/sidebar/Sidebar.tsx"],"names":[],"mappings":"AACA,OAAO,KAAkE,MAAM,OAAO,CAAC;AA4CvF,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,SAAS,CAAC;AAiuB5D,eAAO,MAAM,OAAO,0CAAyB,CAAC"}
|
package/package.json
CHANGED
|
@@ -92,10 +92,11 @@ const HIDDEN_FROM_A11Y = {
|
|
|
92
92
|
* `floatingChrome` changes the geometry, not the parts:
|
|
93
93
|
*
|
|
94
94
|
* thread the `children` fill the whole card, under everything below
|
|
95
|
-
* header the `header` slot + breadcrumb row, absolute at the top, no band
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
95
|
+
* header the `header` slot + breadcrumb row, absolute at the top, no band
|
|
96
|
+
* at rest; once the transcript scrolls under it, the surface fades
|
|
97
|
+
* in behind it over the first 16px — solid across the whole block,
|
|
98
|
+
* then a ramp to nothing `SCRIM_TAIL_RATIO` below it — so no turn
|
|
99
|
+
* ever shows through the title or the crumbs
|
|
99
100
|
* footer absolute at the bottom, with no band of its own: the transcript
|
|
100
101
|
* passes behind the composer and stays in sight around it
|
|
101
102
|
*
|
|
@@ -340,6 +341,7 @@ function FloatingChrome({
|
|
|
340
341
|
const alpha = parsed?.a ?? 1;
|
|
341
342
|
const topStyle = useAnimatedStyle(() => ({ opacity: topFade.value * alpha }), [topFade, alpha]);
|
|
342
343
|
const prefix = testID ?? 'ai-chat';
|
|
344
|
+
const scrimTail = Math.round(headerHeight * SCRIM_TAIL_RATIO);
|
|
343
345
|
|
|
344
346
|
// Absolute over a card of fixed height; sticky in the flow of a card that
|
|
345
347
|
// grows with the document, pinned to the screen's gutter.
|
|
@@ -363,9 +365,7 @@ function FloatingChrome({
|
|
|
363
365
|
top: 0,
|
|
364
366
|
left: 0,
|
|
365
367
|
right: 0,
|
|
366
|
-
|
|
367
|
-
// does is a line across the transcript again.
|
|
368
|
-
height: Math.round(headerHeight * (1 + SCRIM_TAIL_RATIO)),
|
|
368
|
+
height: headerHeight + scrimTail,
|
|
369
369
|
// The card's own corners, since nothing clips a document card.
|
|
370
370
|
...(documentScroll
|
|
371
371
|
? { borderTopLeftRadius: DOCUMENT_CARD_RADIUS, borderTopRightRadius: DOCUMENT_CARD_RADIUS, overflow: 'hidden' as const }
|
|
@@ -373,7 +373,17 @@ function FloatingChrome({
|
|
|
373
373
|
},
|
|
374
374
|
topStyle,
|
|
375
375
|
]}>
|
|
376
|
-
|
|
376
|
+
{/* SOLID behind the header's own block. A ramp that starts fading
|
|
377
|
+
inside the block leaves the breadcrumb row over half-covered
|
|
378
|
+
turns — measured in a browser, a code block's lines read straight
|
|
379
|
+
through the chat's title and crumbs at every width. The ramp
|
|
380
|
+
only starts where the block ends. */}
|
|
381
|
+
<View testID={`${prefix}-fade-top-surface`} style={{ height: headerHeight, backgroundColor: solid }} />
|
|
382
|
+
{/* Past the block's own bottom: a ramp that ends where the layout
|
|
383
|
+
does is a line across the transcript again. */}
|
|
384
|
+
<View style={{ height: scrimTail }}>
|
|
385
|
+
<EdgeScrim color={solid} />
|
|
386
|
+
</View>
|
|
377
387
|
</Animated.View>
|
|
378
388
|
) : null}
|
|
379
389
|
{header}
|
|
@@ -368,6 +368,10 @@ export function AiChatShell({
|
|
|
368
368
|
const panelOpen = panelOpenState && !wide && !!panel;
|
|
369
369
|
const dismissPanel = useCallback(() => setPanelOpen(false), [setPanelOpen]);
|
|
370
370
|
const panelRef = usePanelInteraction(panelOpenState && !!panel, panelOpen, dismissPanel);
|
|
371
|
+
const dismissNav = useCallback(() => setNavOpen(false), [setNavOpen]);
|
|
372
|
+
// The nav drawer covers the workspace whenever it is open: Escape closes it,
|
|
373
|
+
// focus moves in and is kept there, and returns to the opener on close.
|
|
374
|
+
const navRef = usePanelInteraction(navOpen, navOpen, dismissNav);
|
|
371
375
|
// The swipe's handlers are built once and read the drawer's state from here.
|
|
372
376
|
const navOpenRef = useRef(navOpen);
|
|
373
377
|
navOpenRef.current = navOpen;
|
|
@@ -608,6 +612,7 @@ export function AiChatShell({
|
|
|
608
612
|
) : null}
|
|
609
613
|
{!navInFlow && mobileSidebar ? (
|
|
610
614
|
<View
|
|
615
|
+
ref={navRef}
|
|
611
616
|
{...(navOpen ? null : CLOSED_LAYER)}
|
|
612
617
|
pointerEvents={navOpen ? 'box-none' : 'none'}
|
|
613
618
|
style={{ position: overlayPosition, top: 0, bottom: 0, left: 0, zIndex: 10, width: 272, paddingTop: 12, paddingBottom: 12, paddingLeft: 6 }}>
|
|
@@ -652,12 +657,17 @@ export function AiChatShell({
|
|
|
652
657
|
]}>
|
|
653
658
|
{!navInFlow ? (
|
|
654
659
|
<Animated.View
|
|
660
|
+
{...(navOpen ? null : CLOSED_LAYER)}
|
|
655
661
|
pointerEvents={navOpen ? 'auto' : 'none'}
|
|
656
662
|
style={[{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, zIndex: 50 }, veilStyle]}>
|
|
663
|
+
{/* Never a Tab stop: open, focus is kept in the drawer and Escape
|
|
664
|
+
closes it; closed, the veil is not there at all. `tabIndex`,
|
|
665
|
+
not `focusable` — react-native-web's Pressable writes its own
|
|
666
|
+
tabIndex 0 over `focusable`. */}
|
|
657
667
|
<Pressable
|
|
658
668
|
accessibilityRole="button"
|
|
659
669
|
accessibilityLabel={l.closeNavigation}
|
|
660
|
-
|
|
670
|
+
tabIndex={-1}
|
|
661
671
|
onPress={() => setNavOpen(false)}
|
|
662
672
|
style={{ flex: 1, backgroundColor: veil }}
|
|
663
673
|
/>
|
|
@@ -701,7 +711,7 @@ export function AiChatShell({
|
|
|
701
711
|
<Pressable
|
|
702
712
|
accessibilityRole="button"
|
|
703
713
|
accessibilityLabel={l.closePanel(panelLabel)}
|
|
704
|
-
|
|
714
|
+
tabIndex={panelOpen ? 0 : -1}
|
|
705
715
|
onPress={() => setPanelOpen(false)}
|
|
706
716
|
style={{ flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.4)' }}
|
|
707
717
|
/>
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react';
|
|
2
2
|
import { BackHandler, Platform, type View } from 'react-native';
|
|
3
3
|
import { useAccessibilityFocus } from '../hooks/use-accessibility-focus';
|
|
4
|
+
import { listenForEscape, tabbablesWithin, wrapTab } from '../overlay/modal-keyboard';
|
|
4
5
|
import { hasActiveOverlays } from '../overlay/stack';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
/** Interaction only: the shell continues to own the drawer's layout and motion. */
|
|
7
|
+
/**
|
|
8
|
+
* Interaction only: the shell continues to own the drawer's layout and motion.
|
|
9
|
+
* Used by both shell drawers — the panel and the navigation.
|
|
10
|
+
*
|
|
11
|
+
* open Escape (web) and back (Android) close it
|
|
12
|
+
* modal it covers the workspace: focus moves in on open, Tab is kept
|
|
13
|
+
* inside, and focus goes back to the opener on close
|
|
14
|
+
*
|
|
15
|
+
* Any portaled overlay above (a dialog, a menu) takes all of it first.
|
|
16
|
+
*/
|
|
17
17
|
export function usePanelInteraction(open: boolean, modal: boolean, close: () => void) {
|
|
18
18
|
const ref = useAccessibilityFocus<View>(modal);
|
|
19
19
|
const closeRef = useRef(close);
|
|
@@ -33,36 +33,22 @@ export function usePanelInteraction(open: boolean, modal: boolean, close: () =>
|
|
|
33
33
|
// This ref is a native View on native, a DOM host on web.
|
|
34
34
|
const node = ref.current as unknown as HTMLElement | null;
|
|
35
35
|
const opener = document.activeElement as HTMLElement | null;
|
|
36
|
-
const
|
|
37
|
-
.filter(element => element.tabIndex >= 0 && !element.matches(':disabled, [aria-disabled="true"]') &&
|
|
38
|
-
!element.closest('[hidden], [aria-hidden="true"]') && visible(element));
|
|
39
|
-
const focusFirst = () => candidates()[0]?.focus();
|
|
36
|
+
const focusFirst = () => tabbablesWithin(node)[0]?.focus();
|
|
40
37
|
if (modal) focusFirst();
|
|
41
38
|
const onFocus = (event: FocusEvent) => {
|
|
42
39
|
if (modal && !hasActiveOverlays() && node && !node.contains(event.target as Node)) focusFirst();
|
|
43
40
|
};
|
|
44
|
-
const
|
|
45
|
-
if (event.defaultPrevented
|
|
46
|
-
if (event.key === 'Escape') {
|
|
47
|
-
event.preventDefault();
|
|
48
|
-
event.stopPropagation();
|
|
49
|
-
closeRef.current();
|
|
50
|
-
} else if (modal && event.key === 'Tab') {
|
|
51
|
-
const items = candidates();
|
|
52
|
-
const first = items[0];
|
|
53
|
-
const last = items[items.length - 1];
|
|
54
|
-
if (event.shiftKey && (document.activeElement === first || !node?.contains(document.activeElement))) {
|
|
55
|
-
event.preventDefault(); last?.focus();
|
|
56
|
-
} else if (!event.shiftKey && (document.activeElement === last || !node?.contains(document.activeElement))) {
|
|
57
|
-
event.preventDefault(); first?.focus();
|
|
58
|
-
}
|
|
59
|
-
}
|
|
41
|
+
const onTab = (event: KeyboardEvent) => {
|
|
42
|
+
if (modal && !event.defaultPrevented && !hasActiveOverlays()) wrapTab(event, node);
|
|
60
43
|
};
|
|
61
|
-
|
|
62
|
-
|
|
44
|
+
const stopEscape = listenForEscape(() => !hasActiveOverlays(), () => closeRef.current());
|
|
45
|
+
// Capture: a text field inside the drawer stops its own keydowns (see
|
|
46
|
+
// `overlay/modal-keyboard.ts`), and Tab must wrap from there too.
|
|
47
|
+
window.addEventListener('keydown', onTab, true);
|
|
63
48
|
document.addEventListener('focusin', onFocus);
|
|
64
49
|
return () => {
|
|
65
|
-
|
|
50
|
+
stopEscape();
|
|
51
|
+
window.removeEventListener('keydown', onTab, true);
|
|
66
52
|
document.removeEventListener('focusin', onFocus);
|
|
67
53
|
if (opener?.isConnected) opener.focus();
|
|
68
54
|
};
|
|
@@ -32,6 +32,7 @@ import Animated, {
|
|
|
32
32
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
33
33
|
import { EDGE_GAP, windowEdgeGap } from '../layout/edge';
|
|
34
34
|
import { Backdrop, BACKDROP_DIM_OPACITY } from '../overlay';
|
|
35
|
+
import { ModalKeyboard } from '../overlay/ModalKeyboard';
|
|
35
36
|
import { useTheme } from '../theme/use-theme';
|
|
36
37
|
import type { BottomSheetRef, BottomSheetProps, BottomSheetShellProps, BottomSheetBaseProps } from './types';
|
|
37
38
|
|
|
@@ -92,6 +93,7 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
|
|
|
92
93
|
const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
93
94
|
const hasClosedRef = useRef(false);
|
|
94
95
|
const scrollViewRef = useRef<Animated.ScrollView>(null);
|
|
96
|
+
const sheetRef = useRef<View>(null);
|
|
95
97
|
/**
|
|
96
98
|
* Monotonically increasing counter that identifies "the current close
|
|
97
99
|
* attempt". Bumped every time the sheet re-opens (`present()`), so any
|
|
@@ -696,6 +698,11 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
|
|
|
696
698
|
|
|
697
699
|
return (
|
|
698
700
|
<Shell visible={rendered} onRequestClose={dismiss} keyboardHeight={keyboardHeight}>
|
|
701
|
+
{/* Web keyboard and focus: in on open, back on close, Tab kept in
|
|
702
|
+
the sheet, Escape a USER dismissal — through `onDismissAttempt`,
|
|
703
|
+
exactly like the backdrop. The sheet had no keyboard path at
|
|
704
|
+
all. Inside the shell's `OverlayRoot`; inert on native. */}
|
|
705
|
+
<ModalKeyboard panelRef={sheetRef} dismissible dismiss={handleBackdropPress} />
|
|
699
706
|
<View style={StyleSheet.absoluteFill}>
|
|
700
707
|
{backdropComponent ? (
|
|
701
708
|
<Animated.View style={[StyleSheet.absoluteFill, customBackdropStyle]}>
|
|
@@ -723,6 +730,7 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
|
|
|
723
730
|
|
|
724
731
|
<GestureDetector gesture={panGesture}>
|
|
725
732
|
<Animated.View
|
|
733
|
+
ref={sheetRef}
|
|
726
734
|
onLayout={onLayout}
|
|
727
735
|
style={[
|
|
728
736
|
dynamicStyles.sheet,
|
|
@@ -37,6 +37,7 @@ import Animated, {
|
|
|
37
37
|
import { RemoveScrollBar } from 'react-remove-scroll-bar';
|
|
38
38
|
|
|
39
39
|
import { Backdrop, OverlayRoot } from '../overlay';
|
|
40
|
+
import { ModalKeyboard } from '../overlay/ModalKeyboard';
|
|
40
41
|
import { StyledView } from '../styles/styled-primitives';
|
|
41
42
|
import { Portal } from '../portal/index.web';
|
|
42
43
|
import { adoptStyleSheet } from '../styles/adopt-style-sheet';
|
|
@@ -219,6 +220,7 @@ function CenterOrSideDialog({
|
|
|
219
220
|
// the imperative `control.close()` still drives the exit + post-exit onClose.
|
|
220
221
|
const [isOpen, setIsOpen] = useState(() => controlledOpen ?? startOpen ?? false);
|
|
221
222
|
const [isClosing, setIsClosing] = useState(false);
|
|
223
|
+
const panelRef = useRef<View>(null);
|
|
222
224
|
const closeCallbacksRef = useRef<(() => void)[]>([]);
|
|
223
225
|
// Read the latest controlled flag inside stable callbacks without re-binding.
|
|
224
226
|
const isControlledRef = useRef(isControlled);
|
|
@@ -314,24 +316,6 @@ function CenterOrSideDialog({
|
|
|
314
316
|
: withTiming(1, { duration: BACKDROP_FADE_IN_DURATION, easing: Easing.out(Easing.ease) });
|
|
315
317
|
}, [backdropFade, isOpen, isClosing]);
|
|
316
318
|
|
|
317
|
-
// Escape-to-close while open. The listener is intentionally scoped to the
|
|
318
|
-
// open lifetime so stacked dialogs don't fight for the keydown — the
|
|
319
|
-
// top-most one wins via document-level event order. Escape honors
|
|
320
|
-
// `dismissOnBackdrop`: a blocking dialog (e.g. an unanswered confirm) is not
|
|
321
|
-
// dismissible by Escape, matching the backdrop's behavior. The default
|
|
322
|
-
// (`dismissOnBackdrop` true) is unchanged — Escape still closes.
|
|
323
|
-
useEffect(() => {
|
|
324
|
-
if (!isOpen || !dismissOnBackdrop || typeof document === 'undefined') return;
|
|
325
|
-
const handler = (e: KeyboardEvent) => {
|
|
326
|
-
if (e.key === 'Escape') {
|
|
327
|
-
e.stopPropagation();
|
|
328
|
-
close();
|
|
329
|
-
}
|
|
330
|
-
};
|
|
331
|
-
document.addEventListener('keydown', handler);
|
|
332
|
-
return () => document.removeEventListener('keydown', handler);
|
|
333
|
-
}, [close, dismissOnBackdrop, isOpen]);
|
|
334
|
-
|
|
335
319
|
useImperativeHandle(
|
|
336
320
|
control?.ref,
|
|
337
321
|
() => ({ open, close }),
|
|
@@ -359,6 +343,12 @@ function CenterOrSideDialog({
|
|
|
359
343
|
`OverlayInertBoundary` take the page out of the tab order and
|
|
360
344
|
the accessibility tree while it is open. */}
|
|
361
345
|
<OverlayRoot modal>
|
|
346
|
+
<ModalKeyboard
|
|
347
|
+
panelRef={panelRef}
|
|
348
|
+
closing={isClosing}
|
|
349
|
+
dismissible={dismissOnBackdrop}
|
|
350
|
+
dismiss={close}
|
|
351
|
+
/>
|
|
362
352
|
{/* The press target IS the full-viewport box, so it uses
|
|
363
353
|
`Backdrop` (which opts back in from the Portal root's
|
|
364
354
|
`pointer-events: none` via the `pointerEvents` PROP — the style
|
|
@@ -380,6 +370,7 @@ function CenterOrSideDialog({
|
|
|
380
370
|
}}
|
|
381
371
|
>
|
|
382
372
|
<DialogPanel
|
|
373
|
+
panelRef={panelRef}
|
|
383
374
|
testID={testID}
|
|
384
375
|
label={label}
|
|
385
376
|
title={title}
|
|
@@ -410,6 +401,7 @@ function CenterOrSideDialog({
|
|
|
410
401
|
<ClosingContext.Provider value={isClosing}>
|
|
411
402
|
<RemoveScrollBar />
|
|
412
403
|
<SheetSurface
|
|
404
|
+
panelRef={panelRef}
|
|
413
405
|
testID={testID}
|
|
414
406
|
label={label}
|
|
415
407
|
title={title}
|
|
@@ -439,6 +431,7 @@ function CenterOrSideDialog({
|
|
|
439
431
|
}
|
|
440
432
|
|
|
441
433
|
function DialogPanel({
|
|
434
|
+
panelRef,
|
|
442
435
|
testID,
|
|
443
436
|
label,
|
|
444
437
|
title,
|
|
@@ -454,6 +447,7 @@ function DialogPanel({
|
|
|
454
447
|
isClosing,
|
|
455
448
|
children,
|
|
456
449
|
}: {
|
|
450
|
+
panelRef: React.RefObject<View | null>;
|
|
457
451
|
testID?: string;
|
|
458
452
|
label?: string;
|
|
459
453
|
title?: string;
|
|
@@ -508,7 +502,11 @@ function DialogPanel({
|
|
|
508
502
|
|
|
509
503
|
return (
|
|
510
504
|
<Animated.View
|
|
505
|
+
ref={panelRef}
|
|
511
506
|
role="dialog"
|
|
507
|
+
aria-modal
|
|
508
|
+
// Focus lands on the panel itself when it holds nothing tabbable.
|
|
509
|
+
tabIndex={-1}
|
|
512
510
|
aria-label={label}
|
|
513
511
|
aria-labelledby={title ? titleId : undefined}
|
|
514
512
|
aria-describedby={description ? descriptionId : undefined}
|
|
@@ -625,6 +623,7 @@ function DialogPanel({
|
|
|
625
623
|
* implementation with native and supports drag-to-dismiss on web.
|
|
626
624
|
*/
|
|
627
625
|
function SheetSurface({
|
|
626
|
+
panelRef,
|
|
628
627
|
testID,
|
|
629
628
|
label,
|
|
630
629
|
title,
|
|
@@ -646,6 +645,7 @@ function SheetSurface({
|
|
|
646
645
|
style,
|
|
647
646
|
children,
|
|
648
647
|
}: {
|
|
648
|
+
panelRef: React.RefObject<View | null>;
|
|
649
649
|
testID?: string;
|
|
650
650
|
label?: string;
|
|
651
651
|
title?: string;
|
|
@@ -750,6 +750,7 @@ function SheetSurface({
|
|
|
750
750
|
|
|
751
751
|
return (
|
|
752
752
|
<OverlayRoot style={[sheetStyles.root, containerStyle]} className={containerClassName} modal>
|
|
753
|
+
<ModalKeyboard panelRef={panelRef} closing={!shown} dismissible={dismissOnBackdrop} dismiss={onDismiss} />
|
|
753
754
|
<Backdrop
|
|
754
755
|
testID={testID ? `${testID}-backdrop` : DIALOG_SHEET_BACKDROP_TESTID}
|
|
755
756
|
accessibilityLabel={label ? `Dismiss ${label}` : 'Dismiss dialog'}
|
|
@@ -763,7 +764,10 @@ function SheetSurface({
|
|
|
763
764
|
/>
|
|
764
765
|
|
|
765
766
|
<StyledView
|
|
767
|
+
ref={panelRef}
|
|
766
768
|
role="dialog"
|
|
769
|
+
aria-modal
|
|
770
|
+
tabIndex={-1}
|
|
767
771
|
aria-label={label}
|
|
768
772
|
aria-labelledby={title ? titleId : undefined}
|
|
769
773
|
aria-describedby={description ? descriptionId : undefined}
|
|
@@ -100,6 +100,8 @@ export function DialogBottomSheet({
|
|
|
100
100
|
isControlledRef.current = isControlled;
|
|
101
101
|
const onCloseRef = useRef(onClose);
|
|
102
102
|
onCloseRef.current = onClose;
|
|
103
|
+
const controlledOpenRef = useRef(controlledOpen);
|
|
104
|
+
controlledOpenRef.current = controlledOpen;
|
|
103
105
|
|
|
104
106
|
// Drain queued close callbacks atomically — capturing the list and resetting
|
|
105
107
|
// it before invocation ensures a callback that synchronously re-opens the
|
|
@@ -139,12 +141,15 @@ export function DialogBottomSheet({
|
|
|
139
141
|
}, []);
|
|
140
142
|
|
|
141
143
|
// Fired when the sheet has finished closing. Drains queued callbacks, then
|
|
142
|
-
// fires `onClose`
|
|
143
|
-
//
|
|
144
|
-
// again
|
|
144
|
+
// fires `onClose` in imperative mode. In controlled mode a close the HOST
|
|
145
|
+
// made (it flipped `open` to false, usually after `close()` asked it to) was
|
|
146
|
+
// already requested through `onClose`, so firing it again would double-call
|
|
147
|
+
// it — but a close the SHEET made on its own (the backdrop, a drag, Escape)
|
|
148
|
+
// arrives with `open` still true, and the host has to hear about it or its
|
|
149
|
+
// state says open over a closed sheet.
|
|
145
150
|
const handleDismiss = useCallback(() => {
|
|
146
151
|
callQueuedCallbacks();
|
|
147
|
-
if (!isControlledRef.current) onCloseRef.current?.();
|
|
152
|
+
if (!isControlledRef.current || controlledOpenRef.current) onCloseRef.current?.();
|
|
148
153
|
}, [callQueuedCallbacks]);
|
|
149
154
|
|
|
150
155
|
// In controlled mode, mirror the `open` prop onto the underlying sheet so both
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import type { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { listenForEscape, tabbablesWithin, wrapTab } from './modal-keyboard';
|
|
5
|
+
import { useOverlayLayerContext } from './Overlay';
|
|
6
|
+
import { isTopmostOverlayLayer } from './stack';
|
|
7
|
+
|
|
8
|
+
export interface ModalKeyboardProps {
|
|
9
|
+
/** The surface's panel: where focus goes, and what Tab stays inside. */
|
|
10
|
+
panelRef: React.RefObject<View | null>;
|
|
11
|
+
/** The surface has started its exit: focus goes back now, Escape is spent. */
|
|
12
|
+
closing?: boolean;
|
|
13
|
+
/** Whether Escape may dismiss it (a dialog's `dismissOnBackdrop`). */
|
|
14
|
+
dismissible: boolean;
|
|
15
|
+
dismiss: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* An open modal surface's keyboard and focus, WEB. Renders nothing; it must
|
|
20
|
+
* sit INSIDE the surface's `OverlayRoot`, which is how it knows the surface's
|
|
21
|
+
* place in the overlay stack. Used by the web `Dialog` (card and side drawers)
|
|
22
|
+
* and the web `BottomSheet`.
|
|
23
|
+
*
|
|
24
|
+
* - Focus moves INTO the panel when it opens — to whatever the content
|
|
25
|
+
* focused itself (an `autoFocus` field), else the first tabbable, else the
|
|
26
|
+
* panel — and goes BACK to the element that had it once the surface starts
|
|
27
|
+
* closing (or unmounts), unless the host has already put it somewhere else.
|
|
28
|
+
* - Tab and Shift+Tab wrap at the panel's edges.
|
|
29
|
+
* - Escape dismisses (when `dismissible`), from anywhere — a text field of the
|
|
30
|
+
* surface included, which react-native-web keeps every keydown from
|
|
31
|
+
* bubbling out of (`./modal-keyboard.ts`).
|
|
32
|
+
*
|
|
33
|
+
* All three belong to the TOP-MOST surface only: a menu or a second dialog
|
|
34
|
+
* opened from this one takes them until it closes, so Escape closes one layer
|
|
35
|
+
* at a time. Inert without a DOM, so native never runs any of it.
|
|
36
|
+
*/
|
|
37
|
+
export function ModalKeyboard({ panelRef, closing = false, dismissible, dismiss }: ModalKeyboardProps) {
|
|
38
|
+
const layer = useOverlayLayerContext();
|
|
39
|
+
const state = useRef({ closing, dismissible, dismiss });
|
|
40
|
+
state.current = { closing, dismissible, dismiss };
|
|
41
|
+
const opener = useRef<HTMLElement | null>(null);
|
|
42
|
+
|
|
43
|
+
const restoreFocus = useCallback(() => {
|
|
44
|
+
const target = opener.current;
|
|
45
|
+
opener.current = null;
|
|
46
|
+
if (!target?.isConnected || typeof document === 'undefined') return;
|
|
47
|
+
const panel = panelRef.current as unknown as HTMLElement | null;
|
|
48
|
+
const active = document.activeElement;
|
|
49
|
+
// Only focus the surface still holds (or lost with its nodes): a host that
|
|
50
|
+
// moved focus on purpose while closing keeps its choice.
|
|
51
|
+
if (!active || active === document.body || panel?.contains(active)) target.focus();
|
|
52
|
+
}, [panelRef]);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
|
56
|
+
const active = document.activeElement;
|
|
57
|
+
opener.current = active instanceof HTMLElement && active !== document.body ? active : null;
|
|
58
|
+
const panel = () => panelRef.current as unknown as HTMLElement | null;
|
|
59
|
+
// A frame later, so content that focuses itself on mount has done so.
|
|
60
|
+
const frame = requestAnimationFrame(() => {
|
|
61
|
+
const node = panel();
|
|
62
|
+
if (!node || node.contains(document.activeElement) || !isTopmostOverlayLayer(layer)) return;
|
|
63
|
+
(tabbablesWithin(node)[0] ?? node).focus();
|
|
64
|
+
});
|
|
65
|
+
const onTab = (event: KeyboardEvent) => {
|
|
66
|
+
if (event.defaultPrevented || !isTopmostOverlayLayer(layer)) return;
|
|
67
|
+
wrapTab(event, panel());
|
|
68
|
+
};
|
|
69
|
+
// Capture: a text field in the panel stops its own keydowns.
|
|
70
|
+
window.addEventListener('keydown', onTab, true);
|
|
71
|
+
const stopEscape = listenForEscape(
|
|
72
|
+
() => state.current.dismissible && !state.current.closing && isTopmostOverlayLayer(layer),
|
|
73
|
+
() => state.current.dismiss(),
|
|
74
|
+
);
|
|
75
|
+
return () => {
|
|
76
|
+
cancelAnimationFrame(frame);
|
|
77
|
+
window.removeEventListener('keydown', onTab, true);
|
|
78
|
+
stopEscape();
|
|
79
|
+
restoreFocus();
|
|
80
|
+
};
|
|
81
|
+
}, [layer, panelRef, restoreFocus]);
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (closing) restoreFocus();
|
|
85
|
+
}, [closing, restoreFocus]);
|
|
86
|
+
|
|
87
|
+
return null;
|
|
88
|
+
}
|