panelui-native 0.88.1 → 0.89.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/lib/module/components/animated-badge/index.js +10 -2
- package/lib/module/components/animated-badge/index.js.map +1 -1
- package/lib/module/components/avatar/index.js +10 -1
- package/lib/module/components/avatar/index.js.map +1 -1
- package/lib/module/components/feedback/index.js +31 -4
- package/lib/module/components/feedback/index.js.map +1 -1
- package/lib/module/components/search-bar/index.js +35 -7
- package/lib/module/components/search-bar/index.js.map +1 -1
- package/lib/module/components/selection-mode/index.js +33 -3
- package/lib/module/components/selection-mode/index.js.map +1 -1
- package/lib/module/hooks/use-keyboard-avoidance.js +33 -8
- package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
- package/lib/typescript/src/components/animated-badge/index.d.ts +19 -0
- package/lib/typescript/src/components/animated-badge/index.d.ts.map +1 -1
- package/lib/typescript/src/components/avatar/index.d.ts.map +1 -1
- package/lib/typescript/src/components/feedback/index.d.ts +3 -0
- package/lib/typescript/src/components/feedback/index.d.ts.map +1 -1
- package/lib/typescript/src/components/search-bar/index.d.ts +4 -1
- package/lib/typescript/src/components/search-bar/index.d.ts.map +1 -1
- package/lib/typescript/src/components/selection-mode/index.d.ts +63 -0
- package/lib/typescript/src/components/selection-mode/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/animated-badge/index.tsx +24 -2
- package/src/components/avatar/index.tsx +10 -1
- package/src/components/feedback/index.tsx +26 -1
- package/src/components/search-bar/index.tsx +33 -8
- package/src/components/selection-mode/index.tsx +30 -3
- package/src/hooks/use-keyboard-avoidance.ts +33 -8
|
@@ -127,11 +127,11 @@ import {
|
|
|
127
127
|
import {
|
|
128
128
|
ScrollView,
|
|
129
129
|
StyleSheet,
|
|
130
|
+
TextInput,
|
|
130
131
|
View,
|
|
131
132
|
useWindowDimensions,
|
|
132
133
|
type LayoutChangeEvent,
|
|
133
134
|
type NativeSyntheticEvent,
|
|
134
|
-
type TextInput,
|
|
135
135
|
type TextInputKeyPressEventData,
|
|
136
136
|
type TextInputSubmitEditingEventData,
|
|
137
137
|
type ViewProps,
|
|
@@ -287,6 +287,15 @@ const searchBarVariants = tv({
|
|
|
287
287
|
panelList: 'p-1.5',
|
|
288
288
|
/** The hairline between the results and the field. */
|
|
289
289
|
panelDivider: 'w-full bg-border',
|
|
290
|
+
/*
|
|
291
|
+
* A gap between the rows, because a selected one is drawn as a filled
|
|
292
|
+
* shape. Stacked flush, its fill runs into the row underneath and the two
|
|
293
|
+
* read as one block with a coloured top half — a row's rounded corners are
|
|
294
|
+
* only visible when there is background either side of them to round
|
|
295
|
+
* against. Two points separates the fills and still reads as a list rather
|
|
296
|
+
* than a set of cards.
|
|
297
|
+
*/
|
|
298
|
+
section: 'gap-0.5',
|
|
290
299
|
sectionLabel: 'px-3 pb-1 pt-2 text-sm text-muted-foreground',
|
|
291
300
|
item: 'flex-row items-center gap-3 rounded-lg px-3 py-2.5',
|
|
292
301
|
itemLabel: 'flex-1 text-base text-foreground',
|
|
@@ -689,9 +698,24 @@ const SearchBarRoot = forwardRef<TextInput, SearchBarProps>(
|
|
|
689
698
|
clearCloseTimer();
|
|
690
699
|
closeTimer.current = setTimeout(() => {
|
|
691
700
|
closeTimer.current = null;
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
701
|
+
/*
|
|
702
|
+
* A keyboard that never went down is not on its own proof that the
|
|
703
|
+
* focus is still inside this search.
|
|
704
|
+
*
|
|
705
|
+
* Put two search bars on one screen and they hand the keyboard
|
|
706
|
+
* between them: the first blurs, the keyboard stays up because the
|
|
707
|
+
* second now has it, and the first takes it straight back — then the
|
|
708
|
+
* second does the same, and both draw themselves as the field being
|
|
709
|
+
* typed into. A screen showing sizes or variants is four of them in a
|
|
710
|
+
* column, so this is the arrangement, not a corner case.
|
|
711
|
+
*
|
|
712
|
+
* So ask who actually holds it. Another field means somebody moved
|
|
713
|
+
* on deliberately; a focus that has landed nowhere is one of this
|
|
714
|
+
* search's own buttons taking it, which is what the recovery is for.
|
|
715
|
+
*/
|
|
716
|
+
const holder = TextInput.State.currentlyFocusedInput();
|
|
717
|
+
const elsewhere = holder != null && holder !== inputRef.current;
|
|
718
|
+
if (!ending.current && keyboardUp.current && !elsewhere) {
|
|
695
719
|
inputRef.current?.focus();
|
|
696
720
|
return;
|
|
697
721
|
}
|
|
@@ -1121,9 +1145,9 @@ export interface SearchBarSectionProps extends ViewProps {
|
|
|
1121
1145
|
|
|
1122
1146
|
/** A labelled run of rows inside the panel. */
|
|
1123
1147
|
function SearchBarSection({ className, label, children, ...props }: SearchBarSectionProps) {
|
|
1124
|
-
const { sectionLabel } = searchBarVariants();
|
|
1148
|
+
const { section, sectionLabel } = searchBarVariants();
|
|
1125
1149
|
return (
|
|
1126
|
-
<View {...props} className={className}>
|
|
1150
|
+
<View {...props} className={section({ className })}>
|
|
1127
1151
|
{label ? (
|
|
1128
1152
|
<Text accessibilityRole="header" className={sectionLabel()}>
|
|
1129
1153
|
{label}
|
|
@@ -1183,8 +1207,9 @@ function SearchBarItem({
|
|
|
1183
1207
|
// a card rather than a line in a list. The dim is the whole feedback.
|
|
1184
1208
|
pressScale={1}
|
|
1185
1209
|
pressOpacity={0.6}
|
|
1186
|
-
// The
|
|
1187
|
-
//
|
|
1210
|
+
// The two points of gap the section leaves between rows belong to
|
|
1211
|
+
// neither of them otherwise, and a dead line across a list of targets is
|
|
1212
|
+
// a tap that does nothing.
|
|
1188
1213
|
hitSlop={{ top: 2, bottom: 2 }}
|
|
1189
1214
|
{...props}
|
|
1190
1215
|
className={item({ className })}
|
|
@@ -134,7 +134,7 @@ const selectionVariants = tv({
|
|
|
134
134
|
header: 'h-14 flex-row items-center gap-3 border-b border-border px-4',
|
|
135
135
|
title: 'flex-1 text-center text-base font-semibold text-foreground',
|
|
136
136
|
close: 'h-10 w-10 items-center justify-center rounded-full bg-muted',
|
|
137
|
-
group: 'overflow-hidden rounded-2xl
|
|
137
|
+
group: 'overflow-hidden rounded-2xl',
|
|
138
138
|
ring: 'rounded-full border-2 border-transparent p-0.5',
|
|
139
139
|
bar: 'flex-row items-stretch',
|
|
140
140
|
action: 'flex-1 items-center justify-center gap-1.5 px-2 py-3',
|
|
@@ -162,6 +162,23 @@ const selectionVariants = tv({
|
|
|
162
162
|
disabled: {
|
|
163
163
|
true: { action: 'opacity-[0.44]' },
|
|
164
164
|
},
|
|
165
|
+
/**
|
|
166
|
+
* The group's surface, which depends on what is behind it.
|
|
167
|
+
*
|
|
168
|
+
* `card` is a step *down* from the background on a screen, which is what a
|
|
169
|
+
* grouped list wants there. A sheet is already `popover`, and popover sits
|
|
170
|
+
* above card in every dark theme — so the same card inside a sheet is
|
|
171
|
+
* darker than the sheet around it and reads as a hole rather than a raised
|
|
172
|
+
* set of rows.
|
|
173
|
+
*
|
|
174
|
+
* `muted` is an alpha tint, so it steps up from whatever it is drawn on and
|
|
175
|
+
* is correct against either. It is only used in the sheet because the
|
|
176
|
+
* screen's card is right where it is.
|
|
177
|
+
*/
|
|
178
|
+
surface: {
|
|
179
|
+
screen: { group: 'bg-card' },
|
|
180
|
+
sheet: { group: 'bg-muted' },
|
|
181
|
+
},
|
|
165
182
|
/**
|
|
166
183
|
* Flush to the bottom edge, or lifted off it.
|
|
167
184
|
*
|
|
@@ -177,6 +194,7 @@ const selectionVariants = tv({
|
|
|
177
194
|
},
|
|
178
195
|
defaultVariants: {
|
|
179
196
|
placement: 'bar',
|
|
197
|
+
surface: 'screen',
|
|
180
198
|
},
|
|
181
199
|
});
|
|
182
200
|
|
|
@@ -665,7 +683,10 @@ function SelectionModeGroup({
|
|
|
665
683
|
...props
|
|
666
684
|
}: SelectionModeGroupProps) {
|
|
667
685
|
const items = Children.toArray(children).filter(Boolean);
|
|
668
|
-
|
|
686
|
+
// A sheet's own surface already sits above a card's, so which fill the group
|
|
687
|
+
// takes depends on where it is. Every other part reads the context too.
|
|
688
|
+
const { sheet } = useSelectionMode();
|
|
689
|
+
const slots = selectionVariants({ surface: sheet ? 'sheet' : 'screen' });
|
|
669
690
|
|
|
670
691
|
/**
|
|
671
692
|
* The caption, and the wrapper that carries it. A group with no `label` is
|
|
@@ -1133,7 +1154,13 @@ function SelectionModeSheet({
|
|
|
1133
1154
|
* screen but in the wrong branch of the tree.
|
|
1134
1155
|
*/}
|
|
1135
1156
|
<SelectionModeContext.Provider value={context}>
|
|
1136
|
-
|
|
1157
|
+
{/*
|
|
1158
|
+
* `pe-0` gives the trailing edge back. `BottomSheet.Header` reserves
|
|
1159
|
+
* room there for the close button, and this sheet draws none — left
|
|
1160
|
+
* alone, the reserved 48 points push the centred title off the middle
|
|
1161
|
+
* of the sheet and hold "All" that far in from the corner.
|
|
1162
|
+
*/}
|
|
1163
|
+
<BottomSheet.Header className="pe-0">
|
|
1137
1164
|
{/* The sheet already draws the rule and the padding. */}
|
|
1138
1165
|
<SelectionModeHeader
|
|
1139
1166
|
title={title}
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
* </Animated.View>
|
|
45
45
|
* ```
|
|
46
46
|
*/
|
|
47
|
-
import { useCallback, useEffect, useState } from 'react';
|
|
47
|
+
import { useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
|
48
48
|
import {
|
|
49
49
|
TurboModuleRegistry,
|
|
50
50
|
useWindowDimensions,
|
|
@@ -201,7 +201,8 @@ export function useKeyboardAvoidance({
|
|
|
201
201
|
const translation = useSharedValue(0);
|
|
202
202
|
|
|
203
203
|
/*
|
|
204
|
-
* Whether the element
|
|
204
|
+
* Whether the element can be measured: laid out at least once, and still
|
|
205
|
+
* mounted.
|
|
205
206
|
*
|
|
206
207
|
* `measure` warns when it is handed a view the layout engine has no metrics
|
|
207
208
|
* for — every frame between a mount and its first layout, and every frame
|
|
@@ -210,11 +211,26 @@ export function useKeyboardAvoidance({
|
|
|
210
211
|
* the warning is printed before that null comes back. Running once a frame,
|
|
211
212
|
* one of these fills the log on its own.
|
|
212
213
|
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
214
|
+
* The layout pass arms it. Two things disarm it, because one is not enough:
|
|
215
|
+
*
|
|
216
|
+
* - the teardown, in a *layout* effect. A plain effect's cleanup is written
|
|
217
|
+
* from the JS thread after the commit, which is one or more frames after
|
|
218
|
+
* the view it is protecting has gone — long enough for the loop to measure
|
|
219
|
+
* a detached view and log. This is the case an overlay hits every time:
|
|
220
|
+
* a dialog whose content unmounts on close takes the element with it while
|
|
221
|
+
* the keyboard is still on its way down.
|
|
222
|
+
* - the worklet itself, whenever a measurement comes back null. That write
|
|
223
|
+
* belongs to the UI thread, so it lands whatever order the teardown
|
|
224
|
+
* arrives in, and a view that has gone costs one warning rather than one
|
|
225
|
+
* per frame.
|
|
226
|
+
*
|
|
227
|
+
* A disarm that was only a race is put back by the next layout pass, or by
|
|
228
|
+
* the keyboard opening again — `everLaidOut` is what makes the second one
|
|
229
|
+
* safe, since arming before the first layout is the other half of the bug.
|
|
215
230
|
*/
|
|
216
231
|
const laidOut = useSharedValue(false);
|
|
217
|
-
|
|
232
|
+
const everLaidOut = useSharedValue(false);
|
|
233
|
+
useLayoutEffect(
|
|
218
234
|
() => () => {
|
|
219
235
|
laidOut.value = false;
|
|
220
236
|
},
|
|
@@ -250,7 +266,11 @@ export function useKeyboardAvoidance({
|
|
|
250
266
|
if (!laidOut.value) return;
|
|
251
267
|
|
|
252
268
|
const frame = measure(ref);
|
|
253
|
-
if (!frame
|
|
269
|
+
if (!frame) {
|
|
270
|
+
laidOut.value = false;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (frame.height <= 0) return;
|
|
254
274
|
|
|
255
275
|
const restingBottom = frame.pageY + frame.height - translation.value;
|
|
256
276
|
const keyboardTop = screenHeight - keyboardHeight;
|
|
@@ -272,6 +292,10 @@ export function useKeyboardAvoidance({
|
|
|
272
292
|
if (shouldTrack === wasTracking) return;
|
|
273
293
|
runOnJS(setTracking)(shouldTrack);
|
|
274
294
|
|
|
295
|
+
// A keyboard opening is the other place the guard is put back, for the
|
|
296
|
+
// element that was disarmed by a measurement that raced a re-layout.
|
|
297
|
+
if (shouldTrack && everLaidOut.value) laidOut.value = true;
|
|
298
|
+
|
|
275
299
|
// Moving straight from one field to another never closes the keyboard,
|
|
276
300
|
// so the field being left has nothing to follow back down — it is sent
|
|
277
301
|
// home explicitly, or it stays hanging where the keyboard left it.
|
|
@@ -282,7 +306,7 @@ export function useKeyboardAvoidance({
|
|
|
282
306
|
);
|
|
283
307
|
|
|
284
308
|
const { setActive } = track;
|
|
285
|
-
|
|
309
|
+
useLayoutEffect(() => {
|
|
286
310
|
setActive(tracking && mode === 'lift');
|
|
287
311
|
return () => setActive(false);
|
|
288
312
|
}, [tracking, mode, setActive]);
|
|
@@ -290,12 +314,13 @@ export function useKeyboardAvoidance({
|
|
|
290
314
|
const onLayout = useCallback(
|
|
291
315
|
(_event: LayoutChangeEvent) => {
|
|
292
316
|
laidOut.value = true;
|
|
317
|
+
everLaidOut.value = true;
|
|
293
318
|
// A layout pass while the element is at rest means its slot moved for
|
|
294
319
|
// some reason other than the keyboard. Anything left over from the last
|
|
295
320
|
// lift belongs to the old slot.
|
|
296
321
|
if (!tracking && translation.value !== 0) translation.value = 0;
|
|
297
322
|
},
|
|
298
|
-
[laidOut, tracking, translation]
|
|
323
|
+
[everLaidOut, laidOut, tracking, translation]
|
|
299
324
|
);
|
|
300
325
|
|
|
301
326
|
const animatedStyle = useAnimatedStyle(() => {
|