@oxy.so/bloom 4.18.0 → 4.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/adoption-matrix.mdx +5 -5
- package/docs/overlay.mdx +21 -0
- package/docs/select.mdx +10 -0
- 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/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 +19 -4
- package/lib/commonjs/overlay/Overlay.js.map +1 -1
- package/lib/commonjs/overlay/use-hardware-back.js +32 -0
- package/lib/commonjs/overlay/use-hardware-back.js.map +1 -0
- 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/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/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 +19 -4
- package/lib/module/overlay/Overlay.js.map +1 -1
- package/lib/module/overlay/use-hardware-back.js +29 -0
- package/lib/module/overlay/use-hardware-back.js.map +1 -0
- 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/overlay/Overlay.d.ts +1 -1
- package/lib/typescript/commonjs/overlay/Overlay.d.ts.map +1 -1
- package/lib/typescript/commonjs/overlay/types.d.ts +18 -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/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/overlay/Overlay.d.ts +1 -1
- package/lib/typescript/module/overlay/Overlay.d.ts.map +1 -1
- package/lib/typescript/module/overlay/types.d.ts +18 -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/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/chat-list/parts.tsx +1 -1
- package/src/chat-screen/ChatBackground.tsx +6 -6
- package/src/dialog/Dialog.tsx +9 -1
- 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 +32 -14
- package/src/overlay/types.ts +18 -0
- package/src/overlay/use-hardware-back.ts +27 -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
|
@@ -44,9 +44,9 @@ function PatternLayer({
|
|
|
44
44
|
<View
|
|
45
45
|
pointerEvents="none"
|
|
46
46
|
testID="chat-background-pattern"
|
|
47
|
-
style={StyleSheet.
|
|
47
|
+
style={StyleSheet.absoluteFill}
|
|
48
48
|
>
|
|
49
|
-
<Svg style={StyleSheet.
|
|
49
|
+
<Svg style={StyleSheet.absoluteFill} width="100%" height="100%">
|
|
50
50
|
<Defs>
|
|
51
51
|
<Pattern
|
|
52
52
|
id={id}
|
|
@@ -83,9 +83,9 @@ function GradientLayer({ colors }: { colors: string[] }) {
|
|
|
83
83
|
<View
|
|
84
84
|
pointerEvents="none"
|
|
85
85
|
testID="chat-background-gradient"
|
|
86
|
-
style={StyleSheet.
|
|
86
|
+
style={StyleSheet.absoluteFill}
|
|
87
87
|
>
|
|
88
|
-
<Svg style={StyleSheet.
|
|
88
|
+
<Svg style={StyleSheet.absoluteFill} width="100%" height="100%">
|
|
89
89
|
<Defs>
|
|
90
90
|
<LinearGradient id={id} x1="0" y1="0" x2="0.35" y2="1">
|
|
91
91
|
{colors.map((color, i) => (
|
|
@@ -159,13 +159,13 @@ function ChatBackgroundComponent({
|
|
|
159
159
|
accessibilityElementsHidden
|
|
160
160
|
importantForAccessibility="no-hide-descendants"
|
|
161
161
|
testID="chat-background-image"
|
|
162
|
-
style={StyleSheet.
|
|
162
|
+
style={StyleSheet.absoluteFill}
|
|
163
163
|
/>
|
|
164
164
|
<View
|
|
165
165
|
pointerEvents="none"
|
|
166
166
|
testID="chat-background-dim"
|
|
167
167
|
style={[
|
|
168
|
-
StyleSheet.
|
|
168
|
+
StyleSheet.absoluteFill,
|
|
169
169
|
{ backgroundColor: paint.imageDim, opacity: dim },
|
|
170
170
|
]}
|
|
171
171
|
/>
|
package/src/dialog/Dialog.tsx
CHANGED
|
@@ -546,7 +546,15 @@ function SideSheet({
|
|
|
546
546
|
return (
|
|
547
547
|
// `OverlayRoot` takes this drawer's place in the open-order overlay stack.
|
|
548
548
|
// It mounts here, past the `mounted` guard, so the rank tracks OPENING.
|
|
549
|
-
|
|
549
|
+
// `onRequestClose`: the drawer is not an RN `Modal`, so Android back would
|
|
550
|
+
// otherwise pass straight to the screen behind it. A drawer that refuses
|
|
551
|
+
// backdrop dismissal still swallows the press, and stays open.
|
|
552
|
+
<OverlayRoot
|
|
553
|
+
style={[sideStyles.root, containerStyle]}
|
|
554
|
+
className={containerClassName}
|
|
555
|
+
onRequestClose={handleBackdropPress}
|
|
556
|
+
modal
|
|
557
|
+
>
|
|
550
558
|
<Backdrop
|
|
551
559
|
testID={testID ? `${testID}-backdrop` : DIALOG_SHEET_BACKDROP_TESTID}
|
|
552
560
|
accessibilityLabel={label ? `Dismiss ${label}` : 'Dismiss dialog'}
|
|
@@ -81,7 +81,7 @@ const PULSE_SCALE = 0.18;
|
|
|
81
81
|
|
|
82
82
|
/** One full-size layer that centres whatever it holds on the box's centre. */
|
|
83
83
|
const layerStyle: ViewStyle = {
|
|
84
|
-
...StyleSheet.
|
|
84
|
+
...StyleSheet.absoluteFill,
|
|
85
85
|
alignItems: 'center',
|
|
86
86
|
justifyContent: 'center',
|
|
87
87
|
};
|
package/src/mail-list/parts.tsx
CHANGED
|
@@ -77,7 +77,7 @@ export function MailRowLink({
|
|
|
77
77
|
};
|
|
78
78
|
const interactive = Boolean(onPress || href || onLongPress);
|
|
79
79
|
const style: WebCssStyle = {
|
|
80
|
-
...StyleSheet.
|
|
80
|
+
...StyleSheet.absoluteFill,
|
|
81
81
|
borderRadius: MAIL_ROW_RADIUS,
|
|
82
82
|
'--bloom-mail-ring': paint.accent,
|
|
83
83
|
};
|
package/src/media-card/parts.tsx
CHANGED
|
@@ -287,7 +287,7 @@ export function CardLink({ name, onPress, href, onLongPress, selected = false, r
|
|
|
287
287
|
if (!IS_WEB && href) void Linking.openURL(href).catch(() => undefined);
|
|
288
288
|
};
|
|
289
289
|
const style: WebCssStyle = {
|
|
290
|
-
...StyleSheet.
|
|
290
|
+
...StyleSheet.absoluteFill,
|
|
291
291
|
borderRadius: radius,
|
|
292
292
|
'--bloom-media-card-ring': paint.ring,
|
|
293
293
|
};
|
package/src/overlay/Overlay.tsx
CHANGED
|
@@ -57,6 +57,7 @@ import { WEB_POSITION_FIXED } from '../styles/web-view-style';
|
|
|
57
57
|
import { StyledView } from '../styles/styled-primitives';
|
|
58
58
|
import { SurfaceLevelProvider } from '../styles/surface-levels';
|
|
59
59
|
import { layerForRank, type OverlayLayer } from './stack';
|
|
60
|
+
import { useHardwareBack } from './use-hardware-back';
|
|
60
61
|
import { useOverlayLayer } from './use-overlay-layer';
|
|
61
62
|
import type { OverlayRootProps, BackdropProps } from './types';
|
|
62
63
|
|
|
@@ -109,32 +110,43 @@ export const BACKDROP_DIM_OPACITY = 0.28;
|
|
|
109
110
|
* every Bloom surface already puts it. Descendants that need to order
|
|
110
111
|
* themselves within the surface read `useOverlayLayerContext()`.
|
|
111
112
|
*/
|
|
112
|
-
export function OverlayRoot({
|
|
113
|
+
export function OverlayRoot({
|
|
114
|
+
children,
|
|
115
|
+
className,
|
|
116
|
+
style,
|
|
117
|
+
testID,
|
|
118
|
+
zIndex,
|
|
119
|
+
onRequestClose,
|
|
120
|
+
modal = false,
|
|
121
|
+
}: OverlayRootProps) {
|
|
122
|
+
// On the root rather than in each surface: every portaled surface mounts one
|
|
123
|
+
// inside its open guard, so this is the one place that cannot be forgotten.
|
|
124
|
+
// The gallery once forgot it and Android back finished the whole activity.
|
|
125
|
+
useHardwareBack(onRequestClose);
|
|
113
126
|
// Split into two components rather than branching on the hook: a pinned root
|
|
114
127
|
// must not CONSUME a rank either. The toast host is pinned and mounts for the
|
|
115
128
|
// whole life of the app, so holding a rank would keep the live set permanently
|
|
116
129
|
// non-empty — the counter would never reset and depths would climb for the
|
|
117
130
|
// rest of the session.
|
|
118
131
|
return zIndex === undefined ? (
|
|
119
|
-
<StackedOverlayRoot className={className} style={style} testID={testID}>
|
|
132
|
+
<StackedOverlayRoot className={className} style={style} testID={testID} modal={modal}>
|
|
120
133
|
{children}
|
|
121
134
|
</StackedOverlayRoot>
|
|
122
135
|
) : (
|
|
123
|
-
<PinnedOverlayRoot zIndex={zIndex} className={className} style={style} testID={testID}>
|
|
136
|
+
<PinnedOverlayRoot zIndex={zIndex} className={className} style={style} testID={testID} modal={modal}>
|
|
124
137
|
{children}
|
|
125
138
|
</PinnedOverlayRoot>
|
|
126
139
|
);
|
|
127
140
|
}
|
|
128
141
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}: Omit<OverlayRootProps, 'zIndex'>) {
|
|
142
|
+
type OverlayRootViewProps = Pick<OverlayRootProps, 'children' | 'className' | 'style' | 'testID'> & {
|
|
143
|
+
modal: boolean;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
function StackedOverlayRoot({ children, className, style, testID, modal }: OverlayRootViewProps) {
|
|
135
147
|
const layer = useOverlayLayer();
|
|
136
148
|
return (
|
|
137
|
-
<OverlayRootView layer={layer} className={className} style={style} testID={testID}>
|
|
149
|
+
<OverlayRootView layer={layer} className={className} style={style} testID={testID} modal={modal}>
|
|
138
150
|
{children}
|
|
139
151
|
</OverlayRootView>
|
|
140
152
|
);
|
|
@@ -146,7 +158,8 @@ function PinnedOverlayRoot({
|
|
|
146
158
|
style,
|
|
147
159
|
testID,
|
|
148
160
|
zIndex,
|
|
149
|
-
|
|
161
|
+
modal,
|
|
162
|
+
}: OverlayRootViewProps & { zIndex: number }) {
|
|
150
163
|
// Outside the stack, so descendants must not read stack depths from it
|
|
151
164
|
// either — every slot is the pinned depth.
|
|
152
165
|
const layer = useMemo(
|
|
@@ -154,7 +167,7 @@ function PinnedOverlayRoot({
|
|
|
154
167
|
[zIndex],
|
|
155
168
|
);
|
|
156
169
|
return (
|
|
157
|
-
<OverlayRootView layer={layer} className={className} style={style} testID={testID}>
|
|
170
|
+
<OverlayRootView layer={layer} className={className} style={style} testID={testID} modal={modal}>
|
|
158
171
|
{children}
|
|
159
172
|
</OverlayRootView>
|
|
160
173
|
);
|
|
@@ -166,14 +179,19 @@ function OverlayRootView({
|
|
|
166
179
|
style,
|
|
167
180
|
testID,
|
|
168
181
|
layer,
|
|
169
|
-
|
|
182
|
+
modal,
|
|
183
|
+
}: OverlayRootViewProps & { layer: OverlayLayer }) {
|
|
170
184
|
return (
|
|
171
185
|
<OverlayLayerContext.Provider value={layer}>
|
|
172
186
|
{/* `pointerEvents` stays a PROP: react-native-web resolves the RN-only
|
|
173
187
|
`box-none` from the prop path only, and as a style entry it is silently
|
|
174
|
-
dropped — which makes the whole portaled surface click-through.
|
|
188
|
+
dropped — which makes the whole portaled surface click-through.
|
|
189
|
+
`accessibilityViewIsModal` sits HERE, not on the panel: iOS hides only
|
|
190
|
+
the modal view's SIBLINGS, and at the native outlet this root is the
|
|
191
|
+
sibling of the app content, where the panel is not. */}
|
|
175
192
|
<StyledView
|
|
176
193
|
pointerEvents="box-none"
|
|
194
|
+
accessibilityViewIsModal={modal && Platform.OS !== 'web' ? true : undefined}
|
|
177
195
|
className={className}
|
|
178
196
|
style={[styles.root, { zIndex: layer.root }, style]}
|
|
179
197
|
testID={testID}
|
package/src/overlay/types.ts
CHANGED
|
@@ -23,6 +23,24 @@ export interface OverlayRootProps {
|
|
|
23
23
|
* `./stack.ts` exists to remove.
|
|
24
24
|
*/
|
|
25
25
|
zIndex?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Android hardware back while this surface is open. A portaled surface is not
|
|
28
|
+
* an RN `Modal`, so nothing else consumes the press: without this the app's
|
|
29
|
+
* own back handling runs and, at the root of a stack, finishes the activity —
|
|
30
|
+
* the surface "closes" by closing the whole app. Called and consumed while the
|
|
31
|
+
* root is mounted; later-opened surfaces register later, and `BackHandler`
|
|
32
|
+
* runs the newest listener first, so the top surface closes first. A blocking
|
|
33
|
+
* surface still passes a handler (a no-op) so the press is swallowed rather
|
|
34
|
+
* than falling through to the screen underneath.
|
|
35
|
+
*/
|
|
36
|
+
onRequestClose?: () => void;
|
|
37
|
+
/**
|
|
38
|
+
* The surface is modal: a screen reader must not wander into the app behind
|
|
39
|
+
* it. Native only — sets `accessibilityViewIsModal` on this root, which is the
|
|
40
|
+
* app content's sibling at the portal outlet (iOS confines VoiceOver to
|
|
41
|
+
* it). Web surfaces already carry `aria-modal` on their own dialog node.
|
|
42
|
+
*/
|
|
43
|
+
modal?: boolean;
|
|
26
44
|
}
|
|
27
45
|
|
|
28
46
|
export interface BackdropProps {
|
|
@@ -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
|
+
}
|
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,
|