panelui-native 0.68.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.
Files changed (206) hide show
  1. package/README.md +13 -2
  2. package/lib/module/components/accordion/index.js +38 -13
  3. package/lib/module/components/accordion/index.js.map +1 -1
  4. package/lib/module/components/area-chart/index.js +1 -0
  5. package/lib/module/components/area-chart/index.js.map +1 -1
  6. package/lib/module/components/breadcrumb/index.js +2 -1
  7. package/lib/module/components/breadcrumb/index.js.map +1 -1
  8. package/lib/module/components/button/index.js +21 -12
  9. package/lib/module/components/button/index.js.map +1 -1
  10. package/lib/module/components/carousel/index.js +1 -1
  11. package/lib/module/components/carousel/index.js.map +1 -1
  12. package/lib/module/components/chip/index.js +2 -2
  13. package/lib/module/components/chip/index.js.map +1 -1
  14. package/lib/module/components/code-block/index.js.map +1 -1
  15. package/lib/module/components/collapsible/index.js +284 -0
  16. package/lib/module/components/collapsible/index.js.map +1 -0
  17. package/lib/module/components/color-picker/index.js.map +1 -1
  18. package/lib/module/components/fab/index.js +7 -6
  19. package/lib/module/components/fab/index.js.map +1 -1
  20. package/lib/module/components/flow/index.js +67 -4
  21. package/lib/module/components/flow/index.js.map +1 -1
  22. package/lib/module/components/frame/index.js +2 -2
  23. package/lib/module/components/frame/index.js.map +1 -1
  24. package/lib/module/components/grid-item/index.js +2 -2
  25. package/lib/module/components/grid-item/index.js.map +1 -1
  26. package/lib/module/components/heatmap-chart/index.js +1 -0
  27. package/lib/module/components/heatmap-chart/index.js.map +1 -1
  28. package/lib/module/components/hex-chart/index.js +1 -0
  29. package/lib/module/components/hex-chart/index.js.map +1 -1
  30. package/lib/module/components/input-group/index.js +7 -2
  31. package/lib/module/components/input-group/index.js.map +1 -1
  32. package/lib/module/components/item/index.js +2 -2
  33. package/lib/module/components/item/index.js.map +1 -1
  34. package/lib/module/components/line-chart/index.js +1 -0
  35. package/lib/module/components/line-chart/index.js.map +1 -1
  36. package/lib/module/components/live-line-chart/index.js +103 -9
  37. package/lib/module/components/live-line-chart/index.js.map +1 -1
  38. package/lib/module/components/live-line-chart/live-line-accessibility.js +47 -0
  39. package/lib/module/components/live-line-chart/live-line-accessibility.js.map +1 -0
  40. package/lib/module/components/live-line-chart/live-line-lifecycle.js +33 -0
  41. package/lib/module/components/live-line-chart/live-line-lifecycle.js.map +1 -0
  42. package/lib/module/components/marker/index.js +2 -2
  43. package/lib/module/components/marker/index.js.map +1 -1
  44. package/lib/module/components/marquee/index.js +191 -0
  45. package/lib/module/components/marquee/index.js.map +1 -0
  46. package/lib/module/components/marquee/marquee-math.js +39 -0
  47. package/lib/module/components/marquee/marquee-math.js.map +1 -0
  48. package/lib/module/components/menu/index.js +11 -3
  49. package/lib/module/components/menu/index.js.map +1 -1
  50. package/lib/module/components/meter/index.js +38 -20
  51. package/lib/module/components/meter/index.js.map +1 -1
  52. package/lib/module/components/meter/meter-scale.js +62 -8
  53. package/lib/module/components/meter/meter-scale.js.map +1 -1
  54. package/lib/module/components/pagination/index.js +27 -11
  55. package/lib/module/components/pagination/index.js.map +1 -1
  56. package/lib/module/components/panelside/index.js +7 -2
  57. package/lib/module/components/panelside/index.js.map +1 -1
  58. package/lib/module/components/planner/index.js +159 -64
  59. package/lib/module/components/planner/index.js.map +1 -1
  60. package/lib/module/components/planner/planner-announcement.js +30 -0
  61. package/lib/module/components/planner/planner-announcement.js.map +1 -0
  62. package/lib/module/components/planner/planner-entries.js +15 -7
  63. package/lib/module/components/planner/planner-entries.js.map +1 -1
  64. package/lib/module/components/planner/planner-grid-navigation.js +33 -0
  65. package/lib/module/components/planner/planner-grid-navigation.js.map +1 -0
  66. package/lib/module/components/planner/planner-lifecycle.js +45 -0
  67. package/lib/module/components/planner/planner-lifecycle.js.map +1 -0
  68. package/lib/module/components/plot/index.js +105 -42
  69. package/lib/module/components/plot/index.js.map +1 -1
  70. package/lib/module/components/popover/index.js +2 -2
  71. package/lib/module/components/popover/index.js.map +1 -1
  72. package/lib/module/components/post/index.js +4 -4
  73. package/lib/module/components/post/index.js.map +1 -1
  74. package/lib/module/components/search-bar/index.js +331 -0
  75. package/lib/module/components/search-bar/index.js.map +1 -0
  76. package/lib/module/components/table/index.js +3 -4
  77. package/lib/module/components/table/index.js.map +1 -1
  78. package/lib/module/components/timeline/index.js +288 -39
  79. package/lib/module/components/timeline/index.js.map +1 -1
  80. package/lib/module/components/toggle-button/index.js +1 -1
  81. package/lib/module/components/toggle-button/index.js.map +1 -1
  82. package/lib/module/components/tree/index.js +13 -8
  83. package/lib/module/components/tree/index.js.map +1 -1
  84. package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
  85. package/lib/module/index.js +3 -0
  86. package/lib/module/index.js.map +1 -1
  87. package/lib/module/primitives/focus-restoration-store.js +52 -0
  88. package/lib/module/primitives/focus-restoration-store.js.map +1 -0
  89. package/lib/module/primitives/portal.js +36 -2
  90. package/lib/module/primitives/portal.js.map +1 -1
  91. package/lib/module/utils/chart.js +50 -7
  92. package/lib/module/utils/chart.js.map +1 -1
  93. package/lib/typescript/src/components/accordion/index.d.ts +6 -2
  94. package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
  95. package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
  96. package/lib/typescript/src/components/breadcrumb/index.d.ts.map +1 -1
  97. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  98. package/lib/typescript/src/components/code-block/index.d.ts +3 -3
  99. package/lib/typescript/src/components/code-block/index.d.ts.map +1 -1
  100. package/lib/typescript/src/components/collapsible/index.d.ts +76 -0
  101. package/lib/typescript/src/components/collapsible/index.d.ts.map +1 -0
  102. package/lib/typescript/src/components/color-picker/index.d.ts.map +1 -1
  103. package/lib/typescript/src/components/fab/index.d.ts +4 -2
  104. package/lib/typescript/src/components/fab/index.d.ts.map +1 -1
  105. package/lib/typescript/src/components/flow/index.d.ts +7 -1
  106. package/lib/typescript/src/components/flow/index.d.ts.map +1 -1
  107. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
  108. package/lib/typescript/src/components/hex-chart/index.d.ts.map +1 -1
  109. package/lib/typescript/src/components/input-group/index.d.ts.map +1 -1
  110. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  111. package/lib/typescript/src/components/live-line-chart/index.d.ts +21 -7
  112. package/lib/typescript/src/components/live-line-chart/index.d.ts.map +1 -1
  113. package/lib/typescript/src/components/live-line-chart/live-line-accessibility.d.ts +22 -0
  114. package/lib/typescript/src/components/live-line-chart/live-line-accessibility.d.ts.map +1 -0
  115. package/lib/typescript/src/components/live-line-chart/live-line-lifecycle.d.ts +23 -0
  116. package/lib/typescript/src/components/live-line-chart/live-line-lifecycle.d.ts.map +1 -0
  117. package/lib/typescript/src/components/marquee/index.d.ts +73 -0
  118. package/lib/typescript/src/components/marquee/index.d.ts.map +1 -0
  119. package/lib/typescript/src/components/marquee/marquee-math.d.ts +21 -0
  120. package/lib/typescript/src/components/marquee/marquee-math.d.ts.map +1 -0
  121. package/lib/typescript/src/components/menu/index.d.ts +1 -1
  122. package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
  123. package/lib/typescript/src/components/meter/index.d.ts +7 -4
  124. package/lib/typescript/src/components/meter/index.d.ts.map +1 -1
  125. package/lib/typescript/src/components/meter/meter-scale.d.ts +39 -0
  126. package/lib/typescript/src/components/meter/meter-scale.d.ts.map +1 -1
  127. package/lib/typescript/src/components/pagination/index.d.ts.map +1 -1
  128. package/lib/typescript/src/components/panelside/index.d.ts +1 -1
  129. package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
  130. package/lib/typescript/src/components/plan/index.d.ts +2 -2
  131. package/lib/typescript/src/components/planner/index.d.ts.map +1 -1
  132. package/lib/typescript/src/components/planner/planner-announcement.d.ts +15 -0
  133. package/lib/typescript/src/components/planner/planner-announcement.d.ts.map +1 -0
  134. package/lib/typescript/src/components/planner/planner-entries.d.ts +1 -1
  135. package/lib/typescript/src/components/planner/planner-entries.d.ts.map +1 -1
  136. package/lib/typescript/src/components/planner/planner-grid-navigation.d.ts +4 -0
  137. package/lib/typescript/src/components/planner/planner-grid-navigation.d.ts.map +1 -0
  138. package/lib/typescript/src/components/planner/planner-lifecycle.d.ts +20 -0
  139. package/lib/typescript/src/components/planner/planner-lifecycle.d.ts.map +1 -0
  140. package/lib/typescript/src/components/plot/index.d.ts +59 -5
  141. package/lib/typescript/src/components/plot/index.d.ts.map +1 -1
  142. package/lib/typescript/src/components/search-bar/index.d.ts +216 -0
  143. package/lib/typescript/src/components/search-bar/index.d.ts.map +1 -0
  144. package/lib/typescript/src/components/timeline/index.d.ts +35 -2
  145. package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
  146. package/lib/typescript/src/components/tree/index.d.ts +3 -4
  147. package/lib/typescript/src/components/tree/index.d.ts.map +1 -1
  148. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts +2 -2
  149. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
  150. package/lib/typescript/src/index.d.ts +4 -1
  151. package/lib/typescript/src/index.d.ts.map +1 -1
  152. package/lib/typescript/src/primitives/focus-restoration-store.d.ts +24 -0
  153. package/lib/typescript/src/primitives/focus-restoration-store.d.ts.map +1 -0
  154. package/lib/typescript/src/primitives/portal.d.ts +4 -0
  155. package/lib/typescript/src/primitives/portal.d.ts.map +1 -1
  156. package/lib/typescript/src/utils/chart.d.ts +28 -3
  157. package/lib/typescript/src/utils/chart.d.ts.map +1 -1
  158. package/package.json +36 -3
  159. package/src/components/accordion/index.tsx +48 -15
  160. package/src/components/area-chart/index.tsx +8 -0
  161. package/src/components/breadcrumb/index.tsx +2 -1
  162. package/src/components/button/index.tsx +14 -6
  163. package/src/components/carousel/index.tsx +1 -1
  164. package/src/components/chip/index.tsx +2 -2
  165. package/src/components/code-block/index.tsx +3 -3
  166. package/src/components/collapsible/index.tsx +287 -0
  167. package/src/components/color-picker/index.tsx +3 -2
  168. package/src/components/fab/index.tsx +21 -11
  169. package/src/components/flow/index.tsx +94 -15
  170. package/src/components/frame/index.tsx +2 -2
  171. package/src/components/grid-item/index.tsx +2 -2
  172. package/src/components/heatmap-chart/index.tsx +7 -1
  173. package/src/components/hex-chart/index.tsx +7 -1
  174. package/src/components/input-group/index.tsx +7 -2
  175. package/src/components/item/index.tsx +2 -2
  176. package/src/components/line-chart/index.tsx +9 -0
  177. package/src/components/live-line-chart/index.tsx +130 -13
  178. package/src/components/live-line-chart/live-line-accessibility.ts +61 -0
  179. package/src/components/live-line-chart/live-line-lifecycle.ts +63 -0
  180. package/src/components/marker/index.tsx +2 -2
  181. package/src/components/marquee/index.tsx +234 -0
  182. package/src/components/marquee/marquee-math.ts +37 -0
  183. package/src/components/menu/index.tsx +11 -3
  184. package/src/components/meter/index.tsx +49 -26
  185. package/src/components/meter/meter-scale.ts +84 -8
  186. package/src/components/pagination/index.tsx +26 -13
  187. package/src/components/panelside/index.tsx +7 -2
  188. package/src/components/plan/index.tsx +2 -2
  189. package/src/components/planner/index.tsx +202 -76
  190. package/src/components/planner/planner-announcement.ts +37 -0
  191. package/src/components/planner/planner-entries.ts +16 -7
  192. package/src/components/planner/planner-grid-navigation.ts +45 -0
  193. package/src/components/planner/planner-lifecycle.ts +76 -0
  194. package/src/components/plot/index.tsx +183 -39
  195. package/src/components/popover/index.tsx +3 -3
  196. package/src/components/post/index.tsx +4 -4
  197. package/src/components/search-bar/index.tsx +436 -0
  198. package/src/components/table/index.tsx +2 -2
  199. package/src/components/timeline/index.tsx +335 -39
  200. package/src/components/toggle-button/index.tsx +1 -1
  201. package/src/components/tree/index.tsx +17 -12
  202. package/src/hooks/use-keyboard-avoidance.ts +7 -2
  203. package/src/index.ts +15 -0
  204. package/src/primitives/focus-restoration-store.ts +70 -0
  205. package/src/primitives/portal.tsx +34 -2
  206. 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>