panelui-native 0.71.0 → 0.73.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/README.md +2 -1
- package/lib/module/components/accordion/index.js +10 -4
- package/lib/module/components/accordion/index.js.map +1 -1
- package/lib/module/components/area-chart/index.js +1 -0
- package/lib/module/components/area-chart/index.js.map +1 -1
- package/lib/module/components/breadcrumb/index.js +2 -1
- package/lib/module/components/breadcrumb/index.js.map +1 -1
- package/lib/module/components/button/index.js +7 -5
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/carousel/index.js +1 -1
- package/lib/module/components/carousel/index.js.map +1 -1
- package/lib/module/components/chip/index.js +2 -2
- package/lib/module/components/chip/index.js.map +1 -1
- package/lib/module/components/code-block/index.js.map +1 -1
- package/lib/module/components/collapsible/index.js +5 -1
- package/lib/module/components/collapsible/index.js.map +1 -1
- package/lib/module/components/color-picker/index.js.map +1 -1
- package/lib/module/components/fab/index.js +7 -6
- package/lib/module/components/fab/index.js.map +1 -1
- package/lib/module/components/flow/index.js +1 -1
- package/lib/module/components/flow/index.js.map +1 -1
- package/lib/module/components/frame/index.js +2 -2
- package/lib/module/components/frame/index.js.map +1 -1
- package/lib/module/components/grid-item/index.js +2 -2
- package/lib/module/components/grid-item/index.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +1 -0
- package/lib/module/components/heatmap-chart/index.js.map +1 -1
- package/lib/module/components/hex-chart/index.js +1 -0
- package/lib/module/components/hex-chart/index.js.map +1 -1
- package/lib/module/components/input-group/index.js +7 -2
- package/lib/module/components/input-group/index.js.map +1 -1
- package/lib/module/components/item/index.js +2 -2
- package/lib/module/components/item/index.js.map +1 -1
- package/lib/module/components/line-chart/index.js +1 -0
- package/lib/module/components/line-chart/index.js.map +1 -1
- package/lib/module/components/marker/index.js +2 -2
- package/lib/module/components/marker/index.js.map +1 -1
- package/lib/module/components/marquee/index.js +9 -12
- package/lib/module/components/marquee/index.js.map +1 -1
- package/lib/module/components/marquee/marquee-math.js +39 -0
- package/lib/module/components/marquee/marquee-math.js.map +1 -0
- package/lib/module/components/menu/index.js +11 -3
- package/lib/module/components/menu/index.js.map +1 -1
- package/lib/module/components/pagination/index.js +27 -11
- package/lib/module/components/pagination/index.js.map +1 -1
- package/lib/module/components/panelside/index.js +7 -2
- package/lib/module/components/panelside/index.js.map +1 -1
- package/lib/module/components/planner/index.js +26 -5
- package/lib/module/components/planner/index.js.map +1 -1
- package/lib/module/components/plot/index.js +105 -42
- package/lib/module/components/plot/index.js.map +1 -1
- package/lib/module/components/post/index.js +4 -4
- package/lib/module/components/post/index.js.map +1 -1
- package/lib/module/components/search-bar/index.js +331 -0
- package/lib/module/components/search-bar/index.js.map +1 -0
- package/lib/module/components/table/index.js +3 -4
- package/lib/module/components/table/index.js.map +1 -1
- package/lib/module/components/timeline/index.js +288 -39
- package/lib/module/components/timeline/index.js.map +1 -1
- package/lib/module/components/toggle-button/index.js +1 -1
- package/lib/module/components/toggle-button/index.js.map +1 -1
- package/lib/module/components/tree/index.js +13 -8
- package/lib/module/components/tree/index.js.map +1 -1
- package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/chart.js +50 -7
- package/lib/module/utils/chart.js.map +1 -1
- package/lib/typescript/src/components/accordion/index.d.ts +2 -2
- package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
- package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/breadcrumb/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/code-block/index.d.ts +3 -3
- package/lib/typescript/src/components/code-block/index.d.ts.map +1 -1
- package/lib/typescript/src/components/collapsible/index.d.ts +2 -2
- package/lib/typescript/src/components/collapsible/index.d.ts.map +1 -1
- package/lib/typescript/src/components/color-picker/index.d.ts.map +1 -1
- package/lib/typescript/src/components/fab/index.d.ts +4 -2
- package/lib/typescript/src/components/fab/index.d.ts.map +1 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/hex-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/input-group/index.d.ts.map +1 -1
- package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/marquee/index.d.ts +1 -1
- package/lib/typescript/src/components/marquee/index.d.ts.map +1 -1
- package/lib/typescript/src/components/marquee/marquee-math.d.ts +21 -0
- package/lib/typescript/src/components/marquee/marquee-math.d.ts.map +1 -0
- package/lib/typescript/src/components/menu/index.d.ts +1 -1
- package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
- package/lib/typescript/src/components/pagination/index.d.ts.map +1 -1
- package/lib/typescript/src/components/panelside/index.d.ts +1 -1
- package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
- package/lib/typescript/src/components/plan/index.d.ts +2 -2
- package/lib/typescript/src/components/planner/index.d.ts.map +1 -1
- package/lib/typescript/src/components/plot/index.d.ts +59 -5
- package/lib/typescript/src/components/plot/index.d.ts.map +1 -1
- package/lib/typescript/src/components/search-bar/index.d.ts +216 -0
- package/lib/typescript/src/components/search-bar/index.d.ts.map +1 -0
- package/lib/typescript/src/components/timeline/index.d.ts +35 -2
- package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
- package/lib/typescript/src/components/tree/index.d.ts +3 -4
- package/lib/typescript/src/components/tree/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts +2 -2
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/chart.d.ts +28 -3
- package/lib/typescript/src/utils/chart.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/accordion/index.tsx +17 -7
- package/src/components/area-chart/index.tsx +8 -0
- package/src/components/breadcrumb/index.tsx +2 -1
- package/src/components/button/index.tsx +3 -2
- package/src/components/carousel/index.tsx +1 -1
- package/src/components/chip/index.tsx +2 -2
- package/src/components/code-block/index.tsx +3 -3
- package/src/components/collapsible/index.tsx +13 -4
- package/src/components/color-picker/index.tsx +3 -2
- package/src/components/fab/index.tsx +21 -11
- package/src/components/flow/index.tsx +1 -1
- package/src/components/frame/index.tsx +2 -2
- package/src/components/grid-item/index.tsx +2 -2
- package/src/components/heatmap-chart/index.tsx +7 -1
- package/src/components/hex-chart/index.tsx +7 -1
- package/src/components/input-group/index.tsx +7 -2
- package/src/components/item/index.tsx +2 -2
- package/src/components/line-chart/index.tsx +9 -0
- package/src/components/marker/index.tsx +2 -2
- package/src/components/marquee/index.tsx +14 -12
- package/src/components/marquee/marquee-math.ts +37 -0
- package/src/components/menu/index.tsx +11 -3
- package/src/components/pagination/index.tsx +26 -13
- package/src/components/panelside/index.tsx +7 -2
- package/src/components/plan/index.tsx +2 -2
- package/src/components/planner/index.tsx +20 -5
- package/src/components/plot/index.tsx +183 -39
- package/src/components/post/index.tsx +4 -4
- package/src/components/search-bar/index.tsx +436 -0
- package/src/components/table/index.tsx +2 -2
- package/src/components/timeline/index.tsx +335 -39
- package/src/components/toggle-button/index.tsx +1 -1
- package/src/components/tree/index.tsx +17 -12
- package/src/hooks/use-keyboard-avoidance.ts +7 -2
- package/src/index.ts +2 -0
- package/src/utils/chart.ts +66 -7
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SearchBar — a text field for querying a list, with the two controls a search
|
|
3
|
+
* needs and an ordinary field does not.
|
|
4
|
+
*
|
|
5
|
+
* ```tsx
|
|
6
|
+
* <SearchBar placeholder="Search orders" onSubmit={run} />
|
|
7
|
+
* <SearchBar variant="filled" shape="pill" cancel="focus" />
|
|
8
|
+
* <SearchBar debounce={250} onDebouncedChange={filter} loading={pending} />
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* ## The clear button, and why it is not the platform's
|
|
12
|
+
*
|
|
13
|
+
* A ✕ appears inside the field as soon as there is something to clear, and
|
|
14
|
+
* takes it back to empty without dismissing the keyboard — clearing a query is
|
|
15
|
+
* the start of the next one, not the end of the search. It is drawn here
|
|
16
|
+
* rather than left to `clearButtonMode`, which exists on iOS only, cannot be
|
|
17
|
+
* labelled for a screen reader and cannot be swapped for a spinner while
|
|
18
|
+
* results are in flight.
|
|
19
|
+
*
|
|
20
|
+
* The glyph is 24 points and its touch box is 48, made up with slop rather
|
|
21
|
+
* than with size. A 48-point circle inside a 40-point field either overflows
|
|
22
|
+
* it or forces every search bar in an app to be as tall as the largest one.
|
|
23
|
+
*
|
|
24
|
+
* ## Cancel is a row, not a decoration
|
|
25
|
+
*
|
|
26
|
+
* `cancel="focus"` puts a Cancel button beside the field and slides it in
|
|
27
|
+
* while the field is being edited, which is the platform's own answer to
|
|
28
|
+
* "how do I get out of this search". It is a sibling of the field rather than
|
|
29
|
+
* something inside it, because it acts on the search as a whole: it empties
|
|
30
|
+
* the query, drops focus and calls `onCancel`, and a control that ends the
|
|
31
|
+
* thing it sits inside reads as part of the query it is about to discard.
|
|
32
|
+
*
|
|
33
|
+
* Its width is measured once and animated on the UI thread. The button is
|
|
34
|
+
* always mounted when `cancel` is not `never`, so the measurement is already
|
|
35
|
+
* there the first time the field is touched and the first slide is as smooth
|
|
36
|
+
* as the tenth.
|
|
37
|
+
*
|
|
38
|
+
* ## Debouncing belongs to the caller's search, not to the field
|
|
39
|
+
*
|
|
40
|
+
* `onChangeText` always fires on every keystroke — a controlled field that
|
|
41
|
+
* lags its own input is unusable. `debounce` is about the *query*: it holds
|
|
42
|
+
* `onDebouncedChange` until typing pauses, so a network search runs once per
|
|
43
|
+
* pause instead of once per letter. Submitting flushes it immediately, since
|
|
44
|
+
* a return key is somebody saying they are done waiting.
|
|
45
|
+
*/
|
|
46
|
+
import {
|
|
47
|
+
forwardRef,
|
|
48
|
+
useCallback,
|
|
49
|
+
useEffect,
|
|
50
|
+
useImperativeHandle,
|
|
51
|
+
useRef,
|
|
52
|
+
useState,
|
|
53
|
+
type ReactNode,
|
|
54
|
+
} from 'react';
|
|
55
|
+
import {
|
|
56
|
+
View,
|
|
57
|
+
type LayoutChangeEvent,
|
|
58
|
+
type NativeSyntheticEvent,
|
|
59
|
+
type TextInput,
|
|
60
|
+
type TextInputSubmitEditingEventData,
|
|
61
|
+
} from 'react-native';
|
|
62
|
+
import Animated, {
|
|
63
|
+
useAnimatedStyle,
|
|
64
|
+
useSharedValue,
|
|
65
|
+
withTiming,
|
|
66
|
+
} from 'react-native-reanimated';
|
|
67
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
68
|
+
import { SearchIcon, XIcon } from '../../icons';
|
|
69
|
+
import { AnimatedPressable } from '../../primitives/animated-pressable';
|
|
70
|
+
import { Text } from '../../primitives/text';
|
|
71
|
+
import { Input, type InputProps } from '../input';
|
|
72
|
+
import { Spinner } from '../spinner';
|
|
73
|
+
|
|
74
|
+
/** Matches the field's own focus crossfade, so the row settles as one thing. */
|
|
75
|
+
const CANCEL_DURATION = 180;
|
|
76
|
+
|
|
77
|
+
/** Space kept between the field and the Cancel button while it is out. */
|
|
78
|
+
const CANCEL_GAP = 8;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Slop around the 24-point clear glyph, taking its touch box to 48 without
|
|
82
|
+
* changing the height of the field it sits in.
|
|
83
|
+
*/
|
|
84
|
+
const CLEAR_HIT_SLOP = 12;
|
|
85
|
+
|
|
86
|
+
const searchBarVariants = tv({
|
|
87
|
+
slots: {
|
|
88
|
+
row: 'w-full flex-row items-center',
|
|
89
|
+
field: '',
|
|
90
|
+
// Clipped, because this is what the Cancel button is revealed out of: the
|
|
91
|
+
// button keeps its measured width and the container's grows past it.
|
|
92
|
+
//
|
|
93
|
+
// `self-stretch` gives it the row's height, which it has no other way of
|
|
94
|
+
// getting — its only child is positioned absolutely, so it has no content
|
|
95
|
+
// to be as tall as.
|
|
96
|
+
cancelClip: 'self-stretch overflow-hidden',
|
|
97
|
+
cancelButton: 'absolute bottom-0 end-0 top-0 items-center justify-center ps-2',
|
|
98
|
+
cancelLabel: 'font-medium text-primary',
|
|
99
|
+
clear: 'items-center justify-center rounded-full',
|
|
100
|
+
},
|
|
101
|
+
variants: {
|
|
102
|
+
size: {
|
|
103
|
+
sm: { cancelLabel: 'text-[14px]', clear: 'h-6 w-6' },
|
|
104
|
+
md: { cancelLabel: 'text-[16px]', clear: 'h-6 w-6' },
|
|
105
|
+
lg: { cancelLabel: 'text-[16px]', clear: 'h-7 w-7' },
|
|
106
|
+
},
|
|
107
|
+
/**
|
|
108
|
+
* The field's corner. `pill` is the shape a search field takes when it is
|
|
109
|
+
* chrome — sitting above a list, in a header — and `rounded` the one it
|
|
110
|
+
* takes inside a form beside other fields.
|
|
111
|
+
*/
|
|
112
|
+
shape: {
|
|
113
|
+
rounded: { field: '' },
|
|
114
|
+
pill: { field: 'rounded-full' },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
defaultVariants: {
|
|
118
|
+
size: 'md',
|
|
119
|
+
shape: 'rounded',
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
type SearchBarVariantProps = VariantProps<typeof searchBarVariants>;
|
|
124
|
+
|
|
125
|
+
/** Glyph sizes per field size — the icon tracks the text, not the box. */
|
|
126
|
+
const ICON_SIZE = { sm: 16, md: 18, lg: 20 } as const;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* What SearchBar takes from Input, minus everything it owns itself. The form
|
|
130
|
+
* furniture is dropped along with it: a label and an error line stack above
|
|
131
|
+
* and below the field, and Cancel sits beside the whole stack rather than
|
|
132
|
+
* beside the field it belongs to. Use `Field` for a search that is one answer
|
|
133
|
+
* in a form.
|
|
134
|
+
*/
|
|
135
|
+
type InheritedInputProps = Omit<
|
|
136
|
+
InputProps,
|
|
137
|
+
| 'defaultValue'
|
|
138
|
+
| 'description'
|
|
139
|
+
| 'endContent'
|
|
140
|
+
| 'errorMessage'
|
|
141
|
+
| 'interactiveContent'
|
|
142
|
+
| 'isRequired'
|
|
143
|
+
| 'label'
|
|
144
|
+
| 'multiline'
|
|
145
|
+
| 'onChangeText'
|
|
146
|
+
| 'size'
|
|
147
|
+
| 'startContent'
|
|
148
|
+
| 'value'
|
|
149
|
+
>;
|
|
150
|
+
|
|
151
|
+
export interface SearchBarProps extends InheritedInputProps, SearchBarVariantProps {
|
|
152
|
+
/**
|
|
153
|
+
* The field's background, from `Input`. `outline` draws its own edge, for a
|
|
154
|
+
* search bar sitting on the page; `filled` drops it, for one inside a card
|
|
155
|
+
* or a header where a second border reads as a seam. Defaults to `outline`.
|
|
156
|
+
*/
|
|
157
|
+
variant?: InputProps['variant'];
|
|
158
|
+
/** The query, when the caller holds it. Leave unset to let the field keep it. */
|
|
159
|
+
value?: string;
|
|
160
|
+
/** Starting query for an uncontrolled field. Ignored once `value` is passed. */
|
|
161
|
+
defaultValue?: string;
|
|
162
|
+
/** Fires on every keystroke. For a search that costs something, see `debounce`. */
|
|
163
|
+
onChangeText?: (value: string) => void;
|
|
164
|
+
/** The return key, which is labelled Search. Flushes `onDebouncedChange` first. */
|
|
165
|
+
onSubmit?: (value: string) => void;
|
|
166
|
+
/**
|
|
167
|
+
* How long typing has to pause before `onDebouncedChange` runs, in
|
|
168
|
+
* milliseconds. `0` runs it on every keystroke, which is only right for a
|
|
169
|
+
* filter over a list already in memory.
|
|
170
|
+
*/
|
|
171
|
+
debounce?: number;
|
|
172
|
+
/** The query, once typing has paused for `debounce` milliseconds. */
|
|
173
|
+
onDebouncedChange?: (value: string) => void;
|
|
174
|
+
/** Fires after the ✕ empties the field. The field keeps focus. */
|
|
175
|
+
onClear?: () => void;
|
|
176
|
+
/** Fires after Cancel empties the field and drops focus. */
|
|
177
|
+
onCancel?: () => void;
|
|
178
|
+
/** Whether the ✕ appears once there is a query. */
|
|
179
|
+
isClearable?: boolean;
|
|
180
|
+
/**
|
|
181
|
+
* When the Cancel button is beside the field. `focus` slides it in while the
|
|
182
|
+
* field is being edited and away again when it is not, which is what a
|
|
183
|
+
* search bar above a list wants. `always` keeps it out, for a screen that is
|
|
184
|
+
* nothing but the search.
|
|
185
|
+
*/
|
|
186
|
+
cancel?: 'never' | 'focus' | 'always';
|
|
187
|
+
/** The Cancel button's word. */
|
|
188
|
+
cancelLabel?: string;
|
|
189
|
+
/** How the ✕ announces itself. */
|
|
190
|
+
clearLabel?: string;
|
|
191
|
+
/**
|
|
192
|
+
* Results are on their way. A spinner takes the ✕'s place, because the two
|
|
193
|
+
* would otherwise sit on top of one another at exactly the moment a query is
|
|
194
|
+
* both non-empty and running.
|
|
195
|
+
*/
|
|
196
|
+
loading?: boolean;
|
|
197
|
+
/** The leading glyph, for a search over something with a symbol of its own. */
|
|
198
|
+
icon?: ReactNode;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export const SearchBar = forwardRef<TextInput, SearchBarProps>(
|
|
202
|
+
(
|
|
203
|
+
{
|
|
204
|
+
value: valueProp,
|
|
205
|
+
defaultValue,
|
|
206
|
+
onChangeText,
|
|
207
|
+
onSubmit,
|
|
208
|
+
debounce = 0,
|
|
209
|
+
onDebouncedChange,
|
|
210
|
+
onClear,
|
|
211
|
+
onCancel,
|
|
212
|
+
isClearable = true,
|
|
213
|
+
cancel = 'never',
|
|
214
|
+
cancelLabel = 'Cancel',
|
|
215
|
+
clearLabel = 'Clear search',
|
|
216
|
+
loading = false,
|
|
217
|
+
icon,
|
|
218
|
+
shape,
|
|
219
|
+
size = 'md',
|
|
220
|
+
className,
|
|
221
|
+
containerClassName,
|
|
222
|
+
disabled,
|
|
223
|
+
onFocus,
|
|
224
|
+
onBlur,
|
|
225
|
+
onSubmitEditing,
|
|
226
|
+
...props
|
|
227
|
+
},
|
|
228
|
+
ref
|
|
229
|
+
) => {
|
|
230
|
+
const controlled = valueProp !== undefined;
|
|
231
|
+
const [internal, setInternal] = useState(defaultValue ?? '');
|
|
232
|
+
const text = controlled ? valueProp : internal;
|
|
233
|
+
|
|
234
|
+
const [focused, setFocused] = useState(false);
|
|
235
|
+
const inputRef = useRef<TextInput | null>(null);
|
|
236
|
+
useImperativeHandle(ref, () => inputRef.current as TextInput, []);
|
|
237
|
+
|
|
238
|
+
const slots = searchBarVariants({ size, shape });
|
|
239
|
+
|
|
240
|
+
const setText = useCallback(
|
|
241
|
+
(next: string) => {
|
|
242
|
+
if (!controlled) setInternal(next);
|
|
243
|
+
onChangeText?.(next);
|
|
244
|
+
},
|
|
245
|
+
[controlled, onChangeText]
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
/*
|
|
249
|
+
* The debounced callback is read through a ref so an inline arrow function
|
|
250
|
+
* — which is what a caller writes — does not restart the timer on every
|
|
251
|
+
* render and push the pause out forever.
|
|
252
|
+
*/
|
|
253
|
+
const debouncedRef = useRef(onDebouncedChange);
|
|
254
|
+
useEffect(() => {
|
|
255
|
+
debouncedRef.current = onDebouncedChange;
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Skipped on mount: nothing was typed, so there is no pause to be at the
|
|
259
|
+
// end of, and firing here would run a search for the initial value.
|
|
260
|
+
const settled = useRef(false);
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
if (!settled.current) {
|
|
263
|
+
settled.current = true;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (debounce <= 0) {
|
|
267
|
+
debouncedRef.current?.(text);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const timer = setTimeout(() => debouncedRef.current?.(text), debounce);
|
|
271
|
+
return () => clearTimeout(timer);
|
|
272
|
+
}, [text, debounce]);
|
|
273
|
+
|
|
274
|
+
const handleFocus = useCallback<NonNullable<InputProps['onFocus']>>(
|
|
275
|
+
(event) => {
|
|
276
|
+
setFocused(true);
|
|
277
|
+
onFocus?.(event);
|
|
278
|
+
},
|
|
279
|
+
[onFocus]
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const handleBlur = useCallback<NonNullable<InputProps['onBlur']>>(
|
|
283
|
+
(event) => {
|
|
284
|
+
setFocused(false);
|
|
285
|
+
onBlur?.(event);
|
|
286
|
+
},
|
|
287
|
+
[onBlur]
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const handleSubmit = useCallback(
|
|
291
|
+
(event: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
|
|
292
|
+
// A return key is somebody saying they are done waiting, so the
|
|
293
|
+
// pending pause is spent rather than waited out.
|
|
294
|
+
debouncedRef.current?.(text);
|
|
295
|
+
onSubmit?.(text);
|
|
296
|
+
onSubmitEditing?.(event);
|
|
297
|
+
},
|
|
298
|
+
[onSubmit, onSubmitEditing, text]
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
const handleClear = useCallback(() => {
|
|
302
|
+
setText('');
|
|
303
|
+
onClear?.();
|
|
304
|
+
// The keyboard stays: clearing a query is the start of the next one.
|
|
305
|
+
// Android takes focus away with the press, so it is asked back.
|
|
306
|
+
inputRef.current?.focus();
|
|
307
|
+
}, [onClear, setText]);
|
|
308
|
+
|
|
309
|
+
const handleCancel = useCallback(() => {
|
|
310
|
+
setText('');
|
|
311
|
+
inputRef.current?.blur();
|
|
312
|
+
onCancel?.();
|
|
313
|
+
}, [onCancel, setText]);
|
|
314
|
+
|
|
315
|
+
/*
|
|
316
|
+
* Cancel's width is measured once and driven from a shared value, so the
|
|
317
|
+
* slide costs no re-render. `cancelWidth` is a shared value rather than
|
|
318
|
+
* state for the same reason — a measurement that arrives as state would
|
|
319
|
+
* re-render the field it is beside.
|
|
320
|
+
*/
|
|
321
|
+
const cancelWidth = useSharedValue(0);
|
|
322
|
+
const cancelOut = cancel === 'always' || (cancel === 'focus' && focused);
|
|
323
|
+
const cancelProgress = useSharedValue(cancel === 'always' ? 1 : 0);
|
|
324
|
+
|
|
325
|
+
useEffect(() => {
|
|
326
|
+
if (cancel === 'never') return;
|
|
327
|
+
cancelProgress.value = withTiming(cancelOut ? 1 : 0, {
|
|
328
|
+
duration: CANCEL_DURATION,
|
|
329
|
+
});
|
|
330
|
+
}, [cancel, cancelOut, cancelProgress]);
|
|
331
|
+
|
|
332
|
+
const cancelStyle = useAnimatedStyle(() => ({
|
|
333
|
+
width: (cancelWidth.value + CANCEL_GAP) * cancelProgress.value,
|
|
334
|
+
opacity: cancelProgress.value,
|
|
335
|
+
}));
|
|
336
|
+
|
|
337
|
+
const handleCancelLayout = useCallback(
|
|
338
|
+
(event: LayoutChangeEvent) => {
|
|
339
|
+
cancelWidth.value = event.nativeEvent.layout.width;
|
|
340
|
+
},
|
|
341
|
+
[cancelWidth]
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
const startContent = (
|
|
345
|
+
/*
|
|
346
|
+
* Decorative, and said so here rather than through Input's
|
|
347
|
+
* `interactiveContent` — that flag covers both ends of the field, and
|
|
348
|
+
* turning it off to quieten the magnifier would take the clear button's
|
|
349
|
+
* touches with it.
|
|
350
|
+
*/
|
|
351
|
+
<View
|
|
352
|
+
pointerEvents="none"
|
|
353
|
+
accessibilityElementsHidden
|
|
354
|
+
importantForAccessibility="no-hide-descendants"
|
|
355
|
+
>
|
|
356
|
+
{icon ?? <SearchIcon size={ICON_SIZE[size]} />}
|
|
357
|
+
</View>
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const endContent = loading ? (
|
|
361
|
+
<Spinner size="sm" />
|
|
362
|
+
) : isClearable && text.length > 0 ? (
|
|
363
|
+
<AnimatedPressable
|
|
364
|
+
onPress={handleClear}
|
|
365
|
+
disabled={disabled}
|
|
366
|
+
hitSlop={CLEAR_HIT_SLOP}
|
|
367
|
+
accessibilityRole="button"
|
|
368
|
+
accessibilityLabel={clearLabel}
|
|
369
|
+
className={slots.clear()}
|
|
370
|
+
>
|
|
371
|
+
<XIcon size={ICON_SIZE[size]} />
|
|
372
|
+
</AnimatedPressable>
|
|
373
|
+
) : null;
|
|
374
|
+
|
|
375
|
+
const field = (
|
|
376
|
+
<Input
|
|
377
|
+
ref={inputRef}
|
|
378
|
+
size={size}
|
|
379
|
+
disabled={disabled}
|
|
380
|
+
className={slots.field({ className })}
|
|
381
|
+
containerClassName={cancel === 'never' ? containerClassName : 'flex-1'}
|
|
382
|
+
startContent={startContent}
|
|
383
|
+
endContent={endContent}
|
|
384
|
+
value={text}
|
|
385
|
+
onChangeText={setText}
|
|
386
|
+
onFocus={handleFocus}
|
|
387
|
+
onBlur={handleBlur}
|
|
388
|
+
onSubmitEditing={handleSubmit}
|
|
389
|
+
accessibilityRole="search"
|
|
390
|
+
returnKeyType="search"
|
|
391
|
+
// The platform's own ✕ would sit on top of this component's, and only
|
|
392
|
+
// on iOS.
|
|
393
|
+
clearButtonMode="never"
|
|
394
|
+
autoCapitalize="none"
|
|
395
|
+
autoCorrect={false}
|
|
396
|
+
spellCheck={false}
|
|
397
|
+
placeholder="Search"
|
|
398
|
+
{...props}
|
|
399
|
+
/>
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
if (cancel === 'never') return field;
|
|
403
|
+
|
|
404
|
+
return (
|
|
405
|
+
<View className={slots.row({ className: containerClassName })}>
|
|
406
|
+
{field}
|
|
407
|
+
<Animated.View
|
|
408
|
+
style={cancelStyle}
|
|
409
|
+
className={slots.cancelClip()}
|
|
410
|
+
pointerEvents={cancelOut ? 'auto' : 'none'}
|
|
411
|
+
accessibilityElementsHidden={!cancelOut}
|
|
412
|
+
importantForAccessibility={cancelOut ? 'auto' : 'no-hide-descendants'}
|
|
413
|
+
>
|
|
414
|
+
{/*
|
|
415
|
+
* Absolute, and pinned to the end edge: it keeps its natural width
|
|
416
|
+
* inside a container whose width is animating, so the clip reveals
|
|
417
|
+
* it from the edge instead of squeezing the word as it arrives. It
|
|
418
|
+
* is also what makes the measurement possible at all — a child laid
|
|
419
|
+
* out against a container of width 0 would otherwise report 0.
|
|
420
|
+
*/}
|
|
421
|
+
<AnimatedPressable
|
|
422
|
+
onLayout={handleCancelLayout}
|
|
423
|
+
onPress={handleCancel}
|
|
424
|
+
hitSlop={8}
|
|
425
|
+
accessibilityRole="button"
|
|
426
|
+
className={slots.cancelButton()}
|
|
427
|
+
>
|
|
428
|
+
<Text className={slots.cancelLabel()}>{cancelLabel}</Text>
|
|
429
|
+
</AnimatedPressable>
|
|
430
|
+
</Animated.View>
|
|
431
|
+
</View>
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
SearchBar.displayName = 'SearchBar';
|
|
@@ -414,10 +414,10 @@ const TableRow = forwardRef<View, TableRowProps>(
|
|
|
414
414
|
return (
|
|
415
415
|
<View
|
|
416
416
|
ref={ref}
|
|
417
|
+
{...(props as ViewProps)}
|
|
417
418
|
role="row"
|
|
418
419
|
accessibilityState={{ disabled: !!disabled, selected: !!selected }}
|
|
419
420
|
className={classes}
|
|
420
|
-
{...(props as ViewProps)}
|
|
421
421
|
>
|
|
422
422
|
{cells}
|
|
423
423
|
</View>
|
|
@@ -427,6 +427,7 @@ const TableRow = forwardRef<View, TableRowProps>(
|
|
|
427
427
|
return (
|
|
428
428
|
<AnimatedPressable
|
|
429
429
|
ref={ref}
|
|
430
|
+
{...props}
|
|
430
431
|
// A row you can act on is a button first: announcing it as a row would
|
|
431
432
|
// hide the fact that it does anything.
|
|
432
433
|
accessibilityRole="button"
|
|
@@ -434,7 +435,6 @@ const TableRow = forwardRef<View, TableRowProps>(
|
|
|
434
435
|
disabled={disabled}
|
|
435
436
|
onPress={onPress}
|
|
436
437
|
className={classes}
|
|
437
|
-
{...props}
|
|
438
438
|
>
|
|
439
439
|
{cells}
|
|
440
440
|
</AnimatedPressable>
|