@oxy.so/bloom 4.22.2 → 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/composer-panel.mdx +10 -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/chat-composer/ChatComposer.js +10 -12
- package/lib/commonjs/chat-composer/ChatComposer.js.map +1 -1
- package/lib/commonjs/composer-panel/ComposerPanelBase.js +9 -11
- package/lib/commonjs/composer-panel/ComposerPanelBase.js.map +1 -1
- package/lib/commonjs/composer-panel/ComposerPillBase.js +24 -2
- package/lib/commonjs/composer-panel/ComposerPillBase.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/hooks/use-textarea-autosize.js +103 -0
- package/lib/commonjs/hooks/use-textarea-autosize.js.map +1 -0
- 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/chat-composer/ChatComposer.js +11 -13
- package/lib/module/chat-composer/ChatComposer.js.map +1 -1
- package/lib/module/composer-panel/ComposerPanelBase.js +10 -12
- package/lib/module/composer-panel/ComposerPanelBase.js.map +1 -1
- package/lib/module/composer-panel/ComposerPillBase.js +24 -2
- package/lib/module/composer-panel/ComposerPillBase.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/hooks/use-textarea-autosize.js +98 -0
- package/lib/module/hooks/use-textarea-autosize.js.map +1 -0
- 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/chat-composer/ChatComposer.d.ts.map +1 -1
- package/lib/typescript/commonjs/composer-panel/ComposerPanelBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/composer-panel/ComposerPillBase.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/hooks/use-textarea-autosize.d.ts +61 -0
- package/lib/typescript/commonjs/hooks/use-textarea-autosize.d.ts.map +1 -0
- 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/chat-composer/ChatComposer.d.ts.map +1 -1
- package/lib/typescript/module/composer-panel/ComposerPanelBase.d.ts.map +1 -1
- package/lib/typescript/module/composer-panel/ComposerPillBase.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/hooks/use-textarea-autosize.d.ts +61 -0
- package/lib/typescript/module/hooks/use-textarea-autosize.d.ts.map +1 -0
- 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/chat-composer/ChatComposer.tsx +11 -13
- package/src/composer-panel/ComposerPanelBase.tsx +10 -12
- package/src/composer-panel/ComposerPillBase.tsx +28 -2
- package/src/dialog/Dialog.web.tsx +22 -18
- package/src/dialog/DialogBottomSheet.tsx +9 -4
- package/src/hooks/use-textarea-autosize.ts +106 -0
- 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
|
@@ -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,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* app owns drafts per conversation and a composer that kept its own would lose
|
|
9
9
|
* them on every navigation.
|
|
10
10
|
*/
|
|
11
|
-
import React, { useCallback,
|
|
11
|
+
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|
12
12
|
import {
|
|
13
13
|
TextInput,
|
|
14
14
|
View,
|
|
@@ -31,6 +31,7 @@ import { RiForbidLine } from '../icons/remix/RiForbidLine';
|
|
|
31
31
|
import { RiMic2Line } from '../icons/remix/RiMic2Line';
|
|
32
32
|
import { RiSendPlaneLine } from '../icons/remix/RiSendPlaneLine';
|
|
33
33
|
import { useControllableState } from '../hooks/use-controllable-state';
|
|
34
|
+
import { useTextareaAutosize } from '../hooks/use-textarea-autosize';
|
|
34
35
|
import type { WebCssStyle } from '../styles/web-view-style';
|
|
35
36
|
import { useTheme } from '../theme/use-theme';
|
|
36
37
|
import { Text } from '../typography';
|
|
@@ -175,19 +176,16 @@ export function ChatComposer({
|
|
|
175
176
|
*
|
|
176
177
|
* They are not interchangeable: react-native-web reports `scrollHeight`,
|
|
177
178
|
* which never falls below the height already applied, so a field grown to six
|
|
178
|
-
* lines and then emptied would stay six lines tall.
|
|
179
|
-
*
|
|
179
|
+
* lines and then emptied would stay six lines tall. `useTextareaAutosize`
|
|
180
|
+
* collapses the node to measure it only when the draft may have got SHORTER —
|
|
181
|
+
* collapsing on every keystroke re-laid-out the whole page three times a key.
|
|
180
182
|
*/
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const measured = node.scrollHeight;
|
|
188
|
-
node.style.height = previous;
|
|
189
|
-
if (measured > 0) setContentHeight(measured);
|
|
190
|
-
}, [text, minHeight]);
|
|
183
|
+
useTextareaAutosize(localRef, text, {
|
|
184
|
+
enabled: IS_WEB,
|
|
185
|
+
height,
|
|
186
|
+
minHeight,
|
|
187
|
+
onMeasure: setContentHeight,
|
|
188
|
+
});
|
|
191
189
|
|
|
192
190
|
const onContentSizeChange = useCallback(
|
|
193
191
|
(event: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback, useEffect,
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
TextInput,
|
|
4
4
|
View,
|
|
@@ -17,6 +17,7 @@ import Animated, {
|
|
|
17
17
|
} from 'react-native-reanimated';
|
|
18
18
|
|
|
19
19
|
import { useControllableState } from '../hooks/use-controllable-state';
|
|
20
|
+
import { useTextareaAutosize } from '../hooks/use-textarea-autosize';
|
|
20
21
|
import type { WebCssStyle } from '../styles/web-view-style';
|
|
21
22
|
import { useTheme } from '../theme/use-theme';
|
|
22
23
|
import { TYPE_SCALE } from '../typography';
|
|
@@ -175,18 +176,15 @@ export function ComposerPanelBase({
|
|
|
175
176
|
);
|
|
176
177
|
|
|
177
178
|
// The prompt grows with its text, one 20px line at a time, up to 200 before it
|
|
178
|
-
// scrolls. Web measures the textarea
|
|
179
|
-
// native reads the content size.
|
|
179
|
+
// scrolls. Web measures the textarea, collapsing it only when the draft may
|
|
180
|
+
// have got shorter (`useTextareaAutosize`); native reads the content size.
|
|
180
181
|
const [height, setHeight] = useState(PROMPT_LINE);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
node.style.height = `${next}px`;
|
|
188
|
-
setHeight(next);
|
|
189
|
-
}, [text]);
|
|
182
|
+
useTextareaAutosize(fieldRef, text, {
|
|
183
|
+
enabled: IS_WEB,
|
|
184
|
+
height,
|
|
185
|
+
minHeight: PROMPT_LINE,
|
|
186
|
+
onMeasure: (content) => setHeight(Math.min(PROMPT_MAX_HEIGHT, Math.max(PROMPT_LINE, content))),
|
|
187
|
+
});
|
|
190
188
|
const onContentSizeChange = useCallback(
|
|
191
189
|
(event: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => {
|
|
192
190
|
if (IS_WEB) return;
|
|
@@ -16,6 +16,7 @@ import Animated, {
|
|
|
16
16
|
} from 'react-native-reanimated';
|
|
17
17
|
|
|
18
18
|
import { useControllableState } from '../hooks/use-controllable-state';
|
|
19
|
+
import { useTextareaAutosize } from '../hooks/use-textarea-autosize';
|
|
19
20
|
import { RiArrowDownSLine } from '../icons/remix/RiArrowDownSLine';
|
|
20
21
|
import { RadioIndicator } from '../radio-indicator';
|
|
21
22
|
import type { WebCssStyle } from '../styles/web-view-style';
|
|
@@ -459,6 +460,15 @@ export function ComposerPillBase({
|
|
|
459
460
|
const [contentHeight, setContentHeight] = useState(LINE_HEIGHT);
|
|
460
461
|
const [modelWidth, setModelWidth] = useState(0);
|
|
461
462
|
|
|
463
|
+
const fieldRef = useRef<TextInput | null>(null);
|
|
464
|
+
const setFieldRef = useCallback(
|
|
465
|
+
(node: TextInput | null) => {
|
|
466
|
+
fieldRef.current = node;
|
|
467
|
+
if (inputRef) (inputRef as React.MutableRefObject<TextInput | null>).current = node;
|
|
468
|
+
},
|
|
469
|
+
[inputRef],
|
|
470
|
+
);
|
|
471
|
+
|
|
462
472
|
// The add control shares the canonical inset and hover surfaces.
|
|
463
473
|
const addPalette: ComposerPalette = useMemo(() => {
|
|
464
474
|
if (glass) return { ...palette, add: 'transparent', addHover: 'transparent' };
|
|
@@ -547,6 +557,22 @@ export function ComposerPillBase({
|
|
|
547
557
|
const pillHeight = PILL_HEIGHT + (lines - 1) * LINE_HEIGHT;
|
|
548
558
|
const multiLine = lines > 1;
|
|
549
559
|
|
|
560
|
+
/*
|
|
561
|
+
* On web the field is measured, not reported. react-native-web's
|
|
562
|
+
* `onContentSizeChange` reads `scrollHeight` on input only, and that never
|
|
563
|
+
* falls below the height already applied: a pill grown to four lines stayed
|
|
564
|
+
* four lines tall after the draft was deleted, and after send — a
|
|
565
|
+
* programmatic clear fires no input at all. `useTextareaAutosize` measures on
|
|
566
|
+
* every change of the draft and collapses the field only when it may have
|
|
567
|
+
* got shorter.
|
|
568
|
+
*/
|
|
569
|
+
useTextareaAutosize(fieldRef, text, {
|
|
570
|
+
enabled: IS_WEB,
|
|
571
|
+
height: fieldHeight,
|
|
572
|
+
minHeight: LINE_HEIGHT,
|
|
573
|
+
onMeasure: setContentHeight,
|
|
574
|
+
});
|
|
575
|
+
|
|
550
576
|
const pillStyle: WebCssStyle = {
|
|
551
577
|
width: '100%',
|
|
552
578
|
height: pillHeight,
|
|
@@ -593,7 +619,7 @@ export function ComposerPillBase({
|
|
|
593
619
|
|
|
594
620
|
<View style={fieldStyle}>
|
|
595
621
|
<TextInput
|
|
596
|
-
ref={
|
|
622
|
+
ref={setFieldRef}
|
|
597
623
|
{...dataHook('bloomComposerInput')}
|
|
598
624
|
testID={testID ? `${testID}-input` : undefined}
|
|
599
625
|
accessibilityLabel={labels.message}
|
|
@@ -609,7 +635,7 @@ export function ComposerPillBase({
|
|
|
609
635
|
cursorColor={palette.accent500}
|
|
610
636
|
returnKeyType="send"
|
|
611
637
|
multiline
|
|
612
|
-
onContentSizeChange={(event) => setContentHeight(event.nativeEvent.contentSize.height)}
|
|
638
|
+
onContentSizeChange={IS_WEB ? undefined : (event) => setContentHeight(event.nativeEvent.contentSize.height)}
|
|
613
639
|
style={{
|
|
614
640
|
width: '100%',
|
|
615
641
|
height: fieldHeight,
|
|
@@ -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
|