@oxy.so/bloom 4.18.0 → 4.19.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 +5 -5
- package/docs/overlay.mdx +32 -0
- package/docs/portal.mdx +37 -0
- package/docs/rating.mdx +19 -2
- package/docs/select.mdx +10 -0
- package/lib/commonjs/bottom-sheet/BottomSheet.web.js +1 -0
- package/lib/commonjs/bottom-sheet/BottomSheet.web.js.map +1 -1
- package/lib/commonjs/chat-list/parts.js +1 -1
- package/lib/commonjs/chat-list/parts.js.map +1 -1
- package/lib/commonjs/chat-screen/ChatBackground.js +6 -6
- package/lib/commonjs/chat-screen/ChatBackground.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.js +5 -0
- package/lib/commonjs/dialog/Dialog.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.web.js +4 -1
- package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +7 -0
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/location-puck/LocationPuck.js +1 -1
- package/lib/commonjs/location-puck/LocationPuck.js.map +1 -1
- package/lib/commonjs/mail-list/parts.js +1 -1
- package/lib/commonjs/mail-list/parts.js.map +1 -1
- package/lib/commonjs/media-card/parts.js +1 -1
- package/lib/commonjs/media-card/parts.js.map +1 -1
- package/lib/commonjs/overlay/Overlay.js +23 -4
- package/lib/commonjs/overlay/Overlay.js.map +1 -1
- package/lib/commonjs/overlay/OverlayInertBoundary.js +78 -0
- package/lib/commonjs/overlay/OverlayInertBoundary.js.map +1 -0
- package/lib/commonjs/overlay/index.js +14 -0
- package/lib/commonjs/overlay/index.js.map +1 -1
- package/lib/commonjs/overlay/modal-registry.js +104 -0
- package/lib/commonjs/overlay/modal-registry.js.map +1 -0
- package/lib/commonjs/overlay/use-hardware-back.js +32 -0
- package/lib/commonjs/overlay/use-hardware-back.js.map +1 -0
- package/lib/commonjs/overlay/use-modal-overlay-active.js +33 -0
- package/lib/commonjs/overlay/use-modal-overlay-active.js.map +1 -0
- package/lib/commonjs/portal/index.js +7 -0
- package/lib/commonjs/portal/index.js.map +1 -1
- package/lib/commonjs/portal/index.web.js +7 -0
- package/lib/commonjs/portal/index.web.js.map +1 -1
- package/lib/commonjs/rating/Rating.js +33 -2
- package/lib/commonjs/rating/Rating.js.map +1 -1
- package/lib/commonjs/search/Search.js +36 -3
- package/lib/commonjs/search/Search.js.map +1 -1
- package/lib/commonjs/settings-modal/SettingsModal.js +19 -12
- package/lib/commonjs/settings-modal/SettingsModal.js.map +1 -1
- package/lib/commonjs/teleport/PortalHost.js +1 -1
- package/lib/commonjs/zoomable-media-gallery/ZoomableMediaGallery.js +32 -12
- package/lib/commonjs/zoomable-media-gallery/ZoomableMediaGallery.js.map +1 -1
- package/lib/module/bottom-sheet/BottomSheet.web.js +1 -0
- package/lib/module/bottom-sheet/BottomSheet.web.js.map +1 -1
- package/lib/module/chat-list/parts.js +1 -1
- package/lib/module/chat-list/parts.js.map +1 -1
- package/lib/module/chat-screen/ChatBackground.js +6 -6
- package/lib/module/chat-screen/ChatBackground.js.map +1 -1
- package/lib/module/dialog/Dialog.js +5 -0
- package/lib/module/dialog/Dialog.js.map +1 -1
- package/lib/module/dialog/Dialog.web.js +4 -1
- package/lib/module/dialog/Dialog.web.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +1 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/location-puck/LocationPuck.js +1 -1
- package/lib/module/location-puck/LocationPuck.js.map +1 -1
- package/lib/module/mail-list/parts.js +1 -1
- package/lib/module/mail-list/parts.js.map +1 -1
- package/lib/module/media-card/parts.js +1 -1
- package/lib/module/media-card/parts.js.map +1 -1
- package/lib/module/overlay/Overlay.js +23 -4
- package/lib/module/overlay/Overlay.js.map +1 -1
- package/lib/module/overlay/OverlayInertBoundary.js +73 -0
- package/lib/module/overlay/OverlayInertBoundary.js.map +1 -0
- package/lib/module/overlay/index.js +2 -0
- package/lib/module/overlay/index.js.map +1 -1
- package/lib/module/overlay/modal-registry.js +96 -0
- package/lib/module/overlay/modal-registry.js.map +1 -0
- package/lib/module/overlay/use-hardware-back.js +29 -0
- package/lib/module/overlay/use-hardware-back.js.map +1 -0
- package/lib/module/overlay/use-modal-overlay-active.js +28 -0
- package/lib/module/overlay/use-modal-overlay-active.js.map +1 -0
- package/lib/module/portal/index.js +4 -0
- package/lib/module/portal/index.js.map +1 -1
- package/lib/module/portal/index.web.js +4 -0
- package/lib/module/portal/index.web.js.map +1 -1
- package/lib/module/rating/Rating.js +32 -2
- package/lib/module/rating/Rating.js.map +1 -1
- package/lib/module/search/Search.js +37 -4
- package/lib/module/search/Search.js.map +1 -1
- package/lib/module/settings-modal/SettingsModal.js +20 -13
- package/lib/module/settings-modal/SettingsModal.js.map +1 -1
- package/lib/module/teleport/PortalHost.js +1 -1
- package/lib/module/zoomable-media-gallery/ZoomableMediaGallery.js +32 -12
- package/lib/module/zoomable-media-gallery/ZoomableMediaGallery.js.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/BottomSheet.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.web.d.ts +2 -1
- package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +1 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +1 -1
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/overlay/Overlay.d.ts +1 -1
- package/lib/typescript/commonjs/overlay/Overlay.d.ts.map +1 -1
- package/lib/typescript/commonjs/overlay/OverlayInertBoundary.d.ts +6 -0
- package/lib/typescript/commonjs/overlay/OverlayInertBoundary.d.ts.map +1 -0
- package/lib/typescript/commonjs/overlay/index.d.ts +3 -1
- package/lib/typescript/commonjs/overlay/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/overlay/modal-registry.d.ts +53 -0
- package/lib/typescript/commonjs/overlay/modal-registry.d.ts.map +1 -0
- package/lib/typescript/commonjs/overlay/types.d.ts +36 -0
- package/lib/typescript/commonjs/overlay/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/overlay/use-hardware-back.d.ts +11 -0
- package/lib/typescript/commonjs/overlay/use-hardware-back.d.ts.map +1 -0
- package/lib/typescript/commonjs/overlay/use-modal-overlay-active.d.ts +7 -0
- package/lib/typescript/commonjs/overlay/use-modal-overlay-active.d.ts.map +1 -0
- package/lib/typescript/commonjs/portal/index.d.ts +2 -0
- package/lib/typescript/commonjs/portal/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/portal/index.web.d.ts +2 -0
- package/lib/typescript/commonjs/portal/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/rating/Rating.d.ts +9 -1
- package/lib/typescript/commonjs/rating/Rating.d.ts.map +1 -1
- package/lib/typescript/commonjs/rating/types.d.ts +9 -0
- package/lib/typescript/commonjs/rating/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/search/Search.d.ts.map +1 -1
- package/lib/typescript/commonjs/select/types.d.ts +7 -0
- package/lib/typescript/commonjs/select/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/settings-modal/SettingsModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/teleport/PortalHost.d.ts +1 -1
- package/lib/typescript/commonjs/zoomable-media-gallery/ZoomableMediaGallery.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/BottomSheet.web.d.ts.map +1 -1
- package/lib/typescript/module/dialog/Dialog.web.d.ts +2 -1
- package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +1 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +1 -1
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/overlay/Overlay.d.ts +1 -1
- package/lib/typescript/module/overlay/Overlay.d.ts.map +1 -1
- package/lib/typescript/module/overlay/OverlayInertBoundary.d.ts +6 -0
- package/lib/typescript/module/overlay/OverlayInertBoundary.d.ts.map +1 -0
- package/lib/typescript/module/overlay/index.d.ts +3 -1
- package/lib/typescript/module/overlay/index.d.ts.map +1 -1
- package/lib/typescript/module/overlay/modal-registry.d.ts +53 -0
- package/lib/typescript/module/overlay/modal-registry.d.ts.map +1 -0
- package/lib/typescript/module/overlay/types.d.ts +36 -0
- package/lib/typescript/module/overlay/types.d.ts.map +1 -1
- package/lib/typescript/module/overlay/use-hardware-back.d.ts +11 -0
- package/lib/typescript/module/overlay/use-hardware-back.d.ts.map +1 -0
- package/lib/typescript/module/overlay/use-modal-overlay-active.d.ts +7 -0
- package/lib/typescript/module/overlay/use-modal-overlay-active.d.ts.map +1 -0
- package/lib/typescript/module/portal/index.d.ts +2 -0
- package/lib/typescript/module/portal/index.d.ts.map +1 -1
- package/lib/typescript/module/portal/index.web.d.ts +2 -0
- package/lib/typescript/module/portal/index.web.d.ts.map +1 -1
- package/lib/typescript/module/rating/Rating.d.ts +9 -1
- package/lib/typescript/module/rating/Rating.d.ts.map +1 -1
- package/lib/typescript/module/rating/types.d.ts +9 -0
- package/lib/typescript/module/rating/types.d.ts.map +1 -1
- package/lib/typescript/module/search/Search.d.ts.map +1 -1
- package/lib/typescript/module/select/types.d.ts +7 -0
- package/lib/typescript/module/select/types.d.ts.map +1 -1
- package/lib/typescript/module/settings-modal/SettingsModal.d.ts.map +1 -1
- package/lib/typescript/module/teleport/PortalHost.d.ts +1 -1
- package/lib/typescript/module/zoomable-media-gallery/ZoomableMediaGallery.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/bottom-sheet/BottomSheet.web.tsx +6 -2
- package/src/chat-list/parts.tsx +1 -1
- package/src/chat-screen/ChatBackground.tsx +6 -6
- package/src/dialog/Dialog.tsx +9 -1
- package/src/dialog/Dialog.web.tsx +7 -4
- package/src/index.ts +1 -0
- package/src/index.web.ts +1 -0
- package/src/location-puck/LocationPuck.tsx +1 -1
- package/src/mail-list/parts.tsx +1 -1
- package/src/media-card/parts.tsx +1 -1
- package/src/overlay/Overlay.tsx +36 -14
- package/src/overlay/OverlayInertBoundary.tsx +72 -0
- package/src/overlay/index.ts +3 -1
- package/src/overlay/modal-registry.ts +101 -0
- package/src/overlay/types.ts +37 -0
- package/src/overlay/use-hardware-back.ts +27 -0
- package/src/overlay/use-modal-overlay-active.ts +31 -0
- package/src/portal/index.ts +5 -0
- package/src/portal/index.web.ts +5 -0
- package/src/rating/Rating.tsx +37 -2
- package/src/rating/types.ts +9 -0
- package/src/search/Search.tsx +42 -4
- package/src/select/types.ts +7 -0
- package/src/settings-modal/SettingsModal.tsx +20 -15
- package/src/teleport/PortalHost.tsx +1 -1
- package/src/zoomable-media-gallery/ZoomableMediaGallery.tsx +33 -12
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { BackHandler, Platform } from 'react-native';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Consume Android's hardware back for as long as the caller is mounted with a
|
|
6
|
+
* handler, calling the CURRENT `onBack` (read through a ref, so a new closure
|
|
7
|
+
* each render does not re-register and lose its place in the LIFO order).
|
|
8
|
+
*
|
|
9
|
+
* `BackHandler` runs the newest listener first, so surfaces opened later close
|
|
10
|
+
* first. No-op off Android and when `BackHandler` is absent (web bundles, and
|
|
11
|
+
* some test environments' react-native mocks).
|
|
12
|
+
*/
|
|
13
|
+
export function useHardwareBack(onBack: (() => void) | undefined): void {
|
|
14
|
+
const onBackRef = useRef(onBack);
|
|
15
|
+
onBackRef.current = onBack;
|
|
16
|
+
const active = onBack !== undefined;
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!active || Platform.OS !== 'android') return;
|
|
20
|
+
if (typeof BackHandler?.addEventListener !== 'function') return;
|
|
21
|
+
const subscription = BackHandler.addEventListener('hardwareBackPress', () => {
|
|
22
|
+
onBackRef.current?.();
|
|
23
|
+
return true;
|
|
24
|
+
});
|
|
25
|
+
return () => subscription.remove();
|
|
26
|
+
}, [active]);
|
|
27
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `useModalOverlayActive` — whether any modal Bloom overlay is open.
|
|
3
|
+
*
|
|
4
|
+
* Re-renders only when the answer flips. See `./modal-registry.ts` for who
|
|
5
|
+
* registers and why.
|
|
6
|
+
*/
|
|
7
|
+
import { useEffect, useSyncExternalStore } from 'react';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
hasActiveModalOverlays,
|
|
11
|
+
registerModalOverlay,
|
|
12
|
+
subscribeModalOverlays,
|
|
13
|
+
} from './modal-registry';
|
|
14
|
+
|
|
15
|
+
/** `false` on the server: nothing is open before hydration. */
|
|
16
|
+
const serverSnapshot = () => false;
|
|
17
|
+
|
|
18
|
+
export function useModalOverlayActive(): boolean {
|
|
19
|
+
return useSyncExternalStore(subscribeModalOverlays, hasActiveModalOverlays, serverSnapshot);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Register as an open modal overlay while mounted and `modal` is true. Used by
|
|
24
|
+
* `OverlayRoot`; exported only within the family.
|
|
25
|
+
*/
|
|
26
|
+
export function useModalOverlayRegistration(modal: boolean): void {
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!modal) return undefined;
|
|
29
|
+
return registerModalOverlay({});
|
|
30
|
+
}, [modal]);
|
|
31
|
+
}
|
package/src/portal/index.ts
CHANGED
|
@@ -3,3 +3,8 @@ export {
|
|
|
3
3
|
PortalOutlet,
|
|
4
4
|
Portal,
|
|
5
5
|
} from './Portal';
|
|
6
|
+
// The app-content half of a modal overlay: wrap everything EXCEPT the
|
|
7
|
+
// `PortalOutlet` in it. Lives in `overlay/` (it reads the modal registry) and is
|
|
8
|
+
// offered here because this is the subpath the app root already imports.
|
|
9
|
+
export { OverlayInertBoundary } from '../overlay/OverlayInertBoundary';
|
|
10
|
+
export type { OverlayInertBoundaryProps } from '../overlay/types';
|
package/src/portal/index.web.ts
CHANGED
|
@@ -8,3 +8,8 @@ export {
|
|
|
8
8
|
PortalOutlet,
|
|
9
9
|
Portal,
|
|
10
10
|
} from './Portal.web';
|
|
11
|
+
// The app-content half of a modal overlay: wrap everything EXCEPT the
|
|
12
|
+
// `PortalOutlet` in it. Lives in `overlay/` (it reads the modal registry) and is
|
|
13
|
+
// offered here because this is the subpath the app root already imports.
|
|
14
|
+
export { OverlayInertBoundary } from '../overlay/OverlayInertBoundary';
|
|
15
|
+
export type { OverlayInertBoundaryProps } from '../overlay/types';
|
package/src/rating/Rating.tsx
CHANGED
|
@@ -26,7 +26,8 @@ import type { RatingProps, RatingSize } from './types';
|
|
|
26
26
|
* `variant="stars"` draws five stars before the value instead of one, 2px
|
|
27
27
|
* apart, each filled to its share of the value: the unfilled star in
|
|
28
28
|
* `emptyStarColor` (border), and over it the filled star clipped to the
|
|
29
|
-
* fraction. The
|
|
29
|
+
* fraction. The fill comes from `fillValue` when given, else from `value`
|
|
30
|
+
* (parsed by `parseRatingValue` when it is a localised string). The clip is anchored with a LOGICAL inset (`insetInlineStart`) so a
|
|
30
31
|
* right-to-left row fills from the right — the root carries
|
|
31
32
|
* `useDirectionProps()` for react-native-web to resolve it.
|
|
32
33
|
*
|
|
@@ -52,12 +53,45 @@ const IS_WEB = Platform.OS === 'web';
|
|
|
52
53
|
const STAR_COUNT = 5;
|
|
53
54
|
const STAR_GAP = 2;
|
|
54
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Bidi isolates and marks (U+2066–U+2069 LRI/RLI/FSI/PDI, U+200E/U+200F
|
|
58
|
+
* LRM/RLM, U+061C ALM) a localised string is wrapped in. None of them are
|
|
59
|
+
* part of the number.
|
|
60
|
+
*/
|
|
61
|
+
const FORMAT_CHARACTERS = /[\u2066-\u2069\u200E\u200F\u061C]/g;
|
|
62
|
+
/** Arabic-Indic (U+0660) and extended Arabic-Indic (U+06F0) digit zeros. */
|
|
63
|
+
const ARABIC_ZERO = 0x0660;
|
|
64
|
+
const EXTENDED_ARABIC_ZERO = 0x06f0;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The number a drawn rating value stands for, for the `stars` fill: a number
|
|
68
|
+
* is itself; a string is read after dropping bidi/format characters, with a
|
|
69
|
+
* comma or Arabic decimal separator (U+066B) accepted as the decimal point and
|
|
70
|
+
* Arabic-Indic digits accepted — so "4,5" wrapped in FSI…PDI is 4.5. Otherwise it is
|
|
71
|
+
* `Number()`'s reading, so anything unreadable is `NaN` (no fill).
|
|
72
|
+
*/
|
|
73
|
+
export function parseRatingValue(value: number | string): number {
|
|
74
|
+
if (typeof value === 'number') return value;
|
|
75
|
+
const text = value
|
|
76
|
+
.replace(FORMAT_CHARACTERS, '')
|
|
77
|
+
.trim()
|
|
78
|
+
.replace(/[\u0660-\u0669]/g, (d) => String(d.charCodeAt(0) - ARABIC_ZERO))
|
|
79
|
+
.replace(/[\u06F0-\u06F9]/g, (d) => String(d.charCodeAt(0) - EXTENDED_ARABIC_ZERO))
|
|
80
|
+
.replace(/[,\u066B]/, '.');
|
|
81
|
+
return Number(text);
|
|
82
|
+
}
|
|
83
|
+
|
|
55
84
|
/** How much of star `index` (0-based) a rating fills, `0..1`. */
|
|
56
85
|
export function starFill(rating: number, index: number): number {
|
|
57
86
|
if (!Number.isFinite(rating)) return 0;
|
|
58
87
|
return Math.min(1, Math.max(0, rating - index));
|
|
59
88
|
}
|
|
60
89
|
|
|
90
|
+
/** A fill on the five-star scale, clamped to `0..5`; `NaN` stays `NaN` (no fill). */
|
|
91
|
+
function clampRating(rating: number): number {
|
|
92
|
+
return Math.min(STAR_COUNT, Math.max(0, rating));
|
|
93
|
+
}
|
|
94
|
+
|
|
61
95
|
function StarRow({
|
|
62
96
|
rating,
|
|
63
97
|
size,
|
|
@@ -113,6 +147,7 @@ function RatingComponent({
|
|
|
113
147
|
starColor,
|
|
114
148
|
countColor,
|
|
115
149
|
emptyStarColor,
|
|
150
|
+
fillValue,
|
|
116
151
|
accessibilityLabel,
|
|
117
152
|
style,
|
|
118
153
|
testID,
|
|
@@ -148,7 +183,7 @@ function RatingComponent({
|
|
|
148
183
|
>
|
|
149
184
|
{variant === 'stars' && rated ? (
|
|
150
185
|
<StarRow
|
|
151
|
-
rating={
|
|
186
|
+
rating={clampRating(fillValue ?? parseRatingValue(value))}
|
|
152
187
|
size={config.star}
|
|
153
188
|
fill={starPaint}
|
|
154
189
|
empty={emptyStarColor ?? theme.colors.border}
|
package/src/rating/types.ts
CHANGED
|
@@ -44,6 +44,15 @@ export interface RatingProps {
|
|
|
44
44
|
countColor?: string;
|
|
45
45
|
/** `stars` variant: the unfilled part of each star. Default the theme's border colour. */
|
|
46
46
|
emptyStarColor?: string;
|
|
47
|
+
/**
|
|
48
|
+
* `stars` variant: the number the stars are filled to, clamped to `0..5`,
|
|
49
|
+
* independent of the drawn `value`. Pass it with a localised `value` string
|
|
50
|
+
* (`value="4,5"` with `fillValue={4.5}`) so the text follows the locale and
|
|
51
|
+
* the fill does not depend on parsing it. Without it a string `value` is
|
|
52
|
+
* parsed (bidi marks dropped, a comma decimal accepted). Ignored by
|
|
53
|
+
* `compact` and while there is no rating (`value` empty).
|
|
54
|
+
*/
|
|
55
|
+
fillValue?: number;
|
|
47
56
|
/**
|
|
48
57
|
* Overrides the composed accessible name ("Rated 4.92 out of 5, 128
|
|
49
58
|
* reviews"), which is English — pass a translated sentence here.
|
package/src/search/Search.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { forwardRef } from 'react';
|
|
1
|
+
import { forwardRef, useCallback, useMemo, useRef } from 'react';
|
|
2
2
|
import { Platform, type TextInput, View } from 'react-native';
|
|
3
3
|
|
|
4
|
+
import { mergeRefs } from '../hooks/merge-refs';
|
|
5
|
+
|
|
4
6
|
import { useTheme } from '../theme/use-theme';
|
|
5
7
|
import { atoms as a } from '../styles';
|
|
6
8
|
import { borderRadius } from '../styles/tokens';
|
|
@@ -18,22 +20,56 @@ type SearchProps = Omit<TextFieldInputProps, 'label'> & {
|
|
|
18
20
|
};
|
|
19
21
|
|
|
20
22
|
export const Search = forwardRef<TextInput, SearchProps>(
|
|
21
|
-
function Search({ value, label = 'Search', onClearText, ...rest }, ref) {
|
|
23
|
+
function Search({ value, label = 'Search', onClearText, onFocus, onPressIn, ...rest }, ref) {
|
|
22
24
|
const theme = useTheme();
|
|
23
25
|
const showClear = value != null && value.length > 0;
|
|
24
26
|
|
|
27
|
+
// Select-on-focus, done here rather than through RN's `selectTextOnFocus`.
|
|
28
|
+
// On Android that prop does not select on focus: it arms a one-shot flag and
|
|
29
|
+
// calls `selectAll()` on the input's NEXT layout while focused. An
|
|
30
|
+
// autofocused search lays out nothing after focusing until the first
|
|
31
|
+
// keystroke mounts the clear button and pads the input for it — and that
|
|
32
|
+
// layout selects everything, so the SECOND keystroke replaced the query.
|
|
33
|
+
// Typing "mention" in one burst left "on" (OxyHQ/Mention#1126). Now a
|
|
34
|
+
// press-driven focus selects the current query once, immediately, and a
|
|
35
|
+
// programmatic focus (autoFocus, `ref.focus()`) selects nothing. Web never
|
|
36
|
+
// selected on focus and still does not.
|
|
37
|
+
const inputRef = useRef<TextInput | null>(null);
|
|
38
|
+
const refs = useMemo(() => mergeRefs([inputRef, ref]), [ref]);
|
|
39
|
+
const pressedRef = useRef(false);
|
|
40
|
+
const valueRef = useRef(value);
|
|
41
|
+
valueRef.current = value;
|
|
42
|
+
const handlePressIn = useCallback<NonNullable<SearchProps['onPressIn']>>(
|
|
43
|
+
(event) => {
|
|
44
|
+
pressedRef.current = true;
|
|
45
|
+
onPressIn?.(event);
|
|
46
|
+
},
|
|
47
|
+
[onPressIn],
|
|
48
|
+
);
|
|
49
|
+
const handleFocus = useCallback<NonNullable<SearchProps['onFocus']>>(
|
|
50
|
+
(event) => {
|
|
51
|
+
onFocus?.(event);
|
|
52
|
+
const pressed = pressedRef.current;
|
|
53
|
+
pressedRef.current = false;
|
|
54
|
+
const length = valueRef.current?.length ?? 0;
|
|
55
|
+
if (pressed && Platform.OS !== 'web' && length > 0) {
|
|
56
|
+
inputRef.current?.setSelection(0, length);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
[onFocus],
|
|
60
|
+
);
|
|
61
|
+
|
|
25
62
|
return (
|
|
26
63
|
<View style={[a.w_full, a.relative]}>
|
|
27
64
|
<TextField radius={borderRadius.full}>
|
|
28
65
|
<TextFieldIcon icon={MagnifyingGlassIcon} />
|
|
29
66
|
<TextFieldInput
|
|
30
|
-
inputRef={
|
|
67
|
+
inputRef={refs}
|
|
31
68
|
label={label}
|
|
32
69
|
value={value}
|
|
33
70
|
placeholder={label}
|
|
34
71
|
returnKeyType="search"
|
|
35
72
|
keyboardAppearance={theme.mode === 'light' ? 'light' : 'dark'}
|
|
36
|
-
selectTextOnFocus={Platform.OS !== 'web'}
|
|
37
73
|
autoFocus={false}
|
|
38
74
|
accessibilityRole="search"
|
|
39
75
|
autoCorrect={false}
|
|
@@ -41,6 +77,8 @@ export const Search = forwardRef<TextInput, SearchProps>(
|
|
|
41
77
|
autoCapitalize="none"
|
|
42
78
|
style={showClear ? { paddingRight: 24 } : undefined}
|
|
43
79
|
{...rest}
|
|
80
|
+
onPressIn={handlePressIn}
|
|
81
|
+
onFocus={handleFocus}
|
|
44
82
|
/>
|
|
45
83
|
</TextField>
|
|
46
84
|
|
package/src/select/types.ts
CHANGED
|
@@ -37,6 +37,13 @@ export type SelectTriggerProps = {
|
|
|
37
37
|
/**
|
|
38
38
|
* Utility classes APPENDED to the part's own — never substituted for them, so
|
|
39
39
|
* a single layout class cannot strip the chrome.
|
|
40
|
+
*
|
|
41
|
+
* Never pin a HEIGHT here (`h-8`). The value's line box is 20 × the system
|
|
42
|
+
* font scale, and on native Yoga measures the text at most as tall as the
|
|
43
|
+
* space left inside the field: a fixed height cuts the glyphs in half on
|
|
44
|
+
* Android (OxyHQ/Mention#1126 — `h-8` is 28dp there, leaving 15.5dp for a
|
|
45
|
+
* 20dp line). Size a compact trigger with its padding (`fieldStyle`) so the
|
|
46
|
+
* text sets the height.
|
|
40
47
|
*/
|
|
41
48
|
className?: string;
|
|
42
49
|
/**
|
|
@@ -6,7 +6,7 @@ import React, {
|
|
|
6
6
|
useRef,
|
|
7
7
|
useState,
|
|
8
8
|
} from 'react';
|
|
9
|
-
import {
|
|
9
|
+
import { Pressable, StyleSheet, View, useWindowDimensions } from 'react-native';
|
|
10
10
|
import Animated, {
|
|
11
11
|
Easing,
|
|
12
12
|
useAnimatedStyle,
|
|
@@ -263,18 +263,13 @@ export function SettingsModal({
|
|
|
263
263
|
const compactPageOpenRef = useRef(false);
|
|
264
264
|
compactPageOpenRef.current = compactPageOpen && layoutRef.current === 'compact';
|
|
265
265
|
|
|
266
|
-
// Android back closes, like a native modal
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
else requestClose();
|
|
274
|
-
return true;
|
|
275
|
-
});
|
|
276
|
-
return () => sub.remove();
|
|
277
|
-
}, [mounted, requestClose, requestNavigation]);
|
|
266
|
+
// Android back closes, like a native modal — handed to `OverlayRoot`, which
|
|
267
|
+
// owns hardware back for every portaled surface.
|
|
268
|
+
const onHardwareBack = useCallback(() => {
|
|
269
|
+
// On a pushed page (compact), back returns to the section list first.
|
|
270
|
+
if (compactPageOpenRef.current) requestNavigation();
|
|
271
|
+
else requestClose();
|
|
272
|
+
}, [requestClose, requestNavigation]);
|
|
278
273
|
|
|
279
274
|
// ----- saved toast -------------------------------------------------------
|
|
280
275
|
const [savedPhase, setSavedPhase] = useState<SavedPhase>('hidden');
|
|
@@ -351,7 +346,7 @@ export function SettingsModal({
|
|
|
351
346
|
return (
|
|
352
347
|
<ModalPortal>
|
|
353
348
|
<SettingsModalContext.Provider value={context}>
|
|
354
|
-
<OverlayRoot>
|
|
349
|
+
<OverlayRoot onRequestClose={onHardwareBack} modal>
|
|
355
350
|
<Backdrop
|
|
356
351
|
onPress={requestClose}
|
|
357
352
|
progress={progress}
|
|
@@ -813,7 +808,17 @@ function SavedToast({
|
|
|
813
808
|
|
|
814
809
|
const styles = StyleSheet.create({
|
|
815
810
|
center: {
|
|
816
|
-
|
|
811
|
+
// Spelled out, not `StyleSheet.absoluteFillObject`: RN 0.85 removed it, and
|
|
812
|
+
// spreading the resulting `undefined` left this box in flow at ZERO height.
|
|
813
|
+
// The panel still painted (nothing clips it) and still took touches, but
|
|
814
|
+
// Android reports every view under a zero-area ancestor as not visible to
|
|
815
|
+
// the user, so TalkBack and UI Automator saw no modal content at all —
|
|
816
|
+
// only the backdrop's full-screen "Close".
|
|
817
|
+
position: 'absolute',
|
|
818
|
+
top: 0,
|
|
819
|
+
left: 0,
|
|
820
|
+
right: 0,
|
|
821
|
+
bottom: 0,
|
|
817
822
|
alignItems: 'center',
|
|
818
823
|
justifyContent: 'center',
|
|
819
824
|
padding: VIEWPORT_GUTTER,
|
|
@@ -25,7 +25,7 @@ import PortalHost from "./PortalHostView";
|
|
|
25
25
|
* export default function App() {
|
|
26
26
|
* return (
|
|
27
27
|
* <PortalProvider>
|
|
28
|
-
* <PortalHost style={StyleSheet.
|
|
28
|
+
* <PortalHost style={StyleSheet.absoluteFill} name="overlay" />
|
|
29
29
|
* </PortalProvider>
|
|
30
30
|
* );
|
|
31
31
|
* }
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
Gesture,
|
|
26
26
|
GestureDetector,
|
|
27
27
|
} from 'react-native-gesture-handler';
|
|
28
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
28
29
|
import { borderRadius } from '../styles/tokens';
|
|
29
30
|
import { Backdrop, OverlayRoot } from '../overlay';
|
|
30
31
|
import { Portal } from '../portal';
|
|
@@ -109,6 +110,13 @@ function shareUri(item: GalleryMedia): string | undefined {
|
|
|
109
110
|
// image. `Platform.select` keeps them off native. `cursor` is a real
|
|
110
111
|
// `ViewStyle` key on RN 0.83; `userSelect` is declared on `TextStyle` only,
|
|
111
112
|
// which is what `WebCssStyle` covers.
|
|
113
|
+
/** Share button inset from the top-right corner of the safe area. */
|
|
114
|
+
const CHROME_EDGE = 16;
|
|
115
|
+
/** Page indicator (and a lone caption) above the bottom of the safe area. */
|
|
116
|
+
const INDICATOR_BOTTOM = 48;
|
|
117
|
+
/** A caption lifted above the page counter + dots when the indicator shows. */
|
|
118
|
+
const CAPTION_BOTTOM_WITH_INDICATOR = 110;
|
|
119
|
+
|
|
112
120
|
const WEB_POINTER: WebCssStyle = { userSelect: 'none', cursor: 'pointer' };
|
|
113
121
|
const WEB_USER_SELECT_NONE: WebCssStyle = { userSelect: 'none' };
|
|
114
122
|
|
|
@@ -207,6 +215,9 @@ function resolveCornerRadius(cornerRadius: number | 'circle', fit: FittedSize):
|
|
|
207
215
|
*/
|
|
208
216
|
const ZoomableMediaGalleryInner = React.forwardRef<ZoomableMediaGalleryHandle, ZoomableMediaGalleryProps>(({ measureThumb, cornerRadius = DEFAULT_CORNER_RADIUS, indicatorVariant = 'dots', videoControls = false }, ref) => {
|
|
209
217
|
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = useWindowDimensions();
|
|
218
|
+
// The viewer is full-bleed, so its chrome must clear the status bar, the
|
|
219
|
+
// cutout and the home indicator itself — nothing above it applies them.
|
|
220
|
+
const insets = useSafeAreaInsets();
|
|
210
221
|
|
|
211
222
|
const radiusFor = useCallback(
|
|
212
223
|
(fit: FittedSize) => resolveCornerRadius(cornerRadius, fit),
|
|
@@ -928,7 +939,10 @@ const ZoomableMediaGalleryInner = React.forwardRef<ZoomableMediaGalleryHandle, Z
|
|
|
928
939
|
}, [handleDismiss, isOpen, pageTo]);
|
|
929
940
|
|
|
930
941
|
const renderContent = () => (
|
|
931
|
-
|
|
942
|
+
// `onRequestClose`: Android back flies the media home like every other
|
|
943
|
+
// dismiss. The viewer is a Portal, not an RN `Modal`, so without it nothing
|
|
944
|
+
// consumed the press and back finished the whole app.
|
|
945
|
+
<OverlayRoot style={styles.modalContainer} onRequestClose={handleDismiss} modal>
|
|
932
946
|
<GestureHandlerRootView style={StyleSheet.absoluteFill}>
|
|
933
947
|
{/* Shared `Backdrop` (a Pressable) rather than a `Gesture.Tap()`: the
|
|
934
948
|
viewer renders through the web Portal, whose root is
|
|
@@ -1063,7 +1077,10 @@ const ZoomableMediaGalleryInner = React.forwardRef<ZoomableMediaGalleryHandle, Z
|
|
|
1063
1077
|
)}
|
|
1064
1078
|
|
|
1065
1079
|
{pagerReady && items.length > 1 && (
|
|
1066
|
-
<Animated.View
|
|
1080
|
+
<Animated.View
|
|
1081
|
+
style={[styles.indicatorWrap, { bottom: INDICATOR_BOTTOM + insets.bottom }, backdropStyle]}
|
|
1082
|
+
pointerEvents="box-none"
|
|
1083
|
+
>
|
|
1067
1084
|
<View style={styles.counterPill} pointerEvents="none">
|
|
1068
1085
|
<Text style={styles.counterText}>{`${activeIndex + 1} / ${items.length}`}</Text>
|
|
1069
1086
|
</View>
|
|
@@ -1120,7 +1137,11 @@ const ZoomableMediaGalleryInner = React.forwardRef<ZoomableMediaGalleryHandle, Z
|
|
|
1120
1137
|
accessibilityRole="button"
|
|
1121
1138
|
accessibilityLabel="Share media"
|
|
1122
1139
|
hitSlop={8}
|
|
1123
|
-
style={[
|
|
1140
|
+
style={[
|
|
1141
|
+
styles.shareButton,
|
|
1142
|
+
{ top: CHROME_EDGE + insets.top, right: CHROME_EDGE + insets.right },
|
|
1143
|
+
webPointerStyle,
|
|
1144
|
+
]}
|
|
1124
1145
|
>
|
|
1125
1146
|
<RiUpload2Line fill="#fff" size="md" />
|
|
1126
1147
|
</Pressable>
|
|
@@ -1130,7 +1151,11 @@ const ZoomableMediaGalleryInner = React.forwardRef<ZoomableMediaGalleryHandle, Z
|
|
|
1130
1151
|
<Animated.View
|
|
1131
1152
|
style={[
|
|
1132
1153
|
styles.altCaptionWrap,
|
|
1133
|
-
|
|
1154
|
+
{
|
|
1155
|
+
bottom:
|
|
1156
|
+
(items.length > 1 ? CAPTION_BOTTOM_WITH_INDICATOR : INDICATOR_BOTTOM) +
|
|
1157
|
+
insets.bottom,
|
|
1158
|
+
},
|
|
1134
1159
|
backdropStyle,
|
|
1135
1160
|
]}
|
|
1136
1161
|
pointerEvents="none"
|
|
@@ -1209,9 +1234,9 @@ const styles = StyleSheet.create({
|
|
|
1209
1234
|
mediaBox: {
|
|
1210
1235
|
overflow: 'hidden',
|
|
1211
1236
|
},
|
|
1237
|
+
// `bottom` is inline: INDICATOR_BOTTOM above the bottom safe-area inset.
|
|
1212
1238
|
indicatorWrap: {
|
|
1213
1239
|
position: 'absolute',
|
|
1214
|
-
bottom: 48,
|
|
1215
1240
|
left: 0,
|
|
1216
1241
|
right: 0,
|
|
1217
1242
|
alignItems: 'center',
|
|
@@ -1298,10 +1323,10 @@ const styles = StyleSheet.create({
|
|
|
1298
1323
|
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
1299
1324
|
transform: [{ translateY: -22 }, { scale: 1.08 }],
|
|
1300
1325
|
},
|
|
1326
|
+
// `top`/`right` are inline: CHROME_EDGE inside the safe area. A bare 16
|
|
1327
|
+
// put the button under the Android status bar, over the battery icon.
|
|
1301
1328
|
shareButton: {
|
|
1302
1329
|
position: 'absolute',
|
|
1303
|
-
top: 16,
|
|
1304
|
-
right: 16,
|
|
1305
1330
|
width: 40,
|
|
1306
1331
|
height: 40,
|
|
1307
1332
|
borderRadius: borderRadius.full,
|
|
@@ -1309,18 +1334,14 @@ const styles = StyleSheet.create({
|
|
|
1309
1334
|
justifyContent: 'center',
|
|
1310
1335
|
backgroundColor: 'rgba(0,0,0,0.45)',
|
|
1311
1336
|
},
|
|
1337
|
+
// `bottom` is inline, above the indicator when there is one, inside the safe area.
|
|
1312
1338
|
altCaptionWrap: {
|
|
1313
1339
|
position: 'absolute',
|
|
1314
|
-
bottom: 48,
|
|
1315
1340
|
left: 0,
|
|
1316
1341
|
right: 0,
|
|
1317
1342
|
alignItems: 'center',
|
|
1318
1343
|
paddingHorizontal: 24,
|
|
1319
1344
|
},
|
|
1320
|
-
// Lifted above the page counter + dots when the multi-image indicator shows.
|
|
1321
|
-
altCaptionWrapWithIndicator: {
|
|
1322
|
-
bottom: 110,
|
|
1323
|
-
},
|
|
1324
1345
|
altCaptionPill: {
|
|
1325
1346
|
maxWidth: 600,
|
|
1326
1347
|
paddingHorizontal: 14,
|