@lotics/ui 5.9.0 → 5.10.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/AGENTS.md +131 -44
- package/examples/tpl_approvals.tsx +3 -3
- package/examples/tpl_dossier.tsx +5 -4
- package/examples/tpl_pipeline.tsx +400 -0
- package/examples/tpl_record.tsx +5 -4
- package/examples/tpl_shifts.tsx +7 -5
- package/examples/tpl_task_board.tsx +473 -0
- package/examples/tpl_tasks.tsx +456 -0
- package/package.json +10 -5
- package/src/accordion.tsx +1 -1
- package/src/agent_progress.tsx +1 -1
- package/src/agent_run.tsx +1 -1
- package/src/breakdown.tsx +2 -2
- package/src/button.tsx +12 -3
- package/src/calendar/calendar_view.tsx +10 -10
- package/src/calendar/month_view.tsx +30 -11
- package/src/calendar/time_grid_view.tsx +25 -7
- package/src/card_select_item.tsx +11 -13
- package/src/check_circle.tsx +88 -0
- package/src/checkbox_input.tsx +5 -1
- package/src/chip.tsx +1 -1
- package/src/chip_group.tsx +1 -1
- package/src/column_filter.tsx +3 -2
- package/src/combobox.tsx +84 -219
- package/src/confidence.tsx +14 -3
- package/src/control_surface.ts +22 -7
- package/src/data_grid.tsx +159 -0
- package/src/date_calendar.tsx +46 -23
- package/src/date_field.tsx +10 -2
- package/src/date_filter.tsx +5 -9
- package/src/date_picker.tsx +78 -26
- package/src/date_picker_value.ts +11 -0
- package/src/date_range_filter_field.tsx +6 -4
- package/src/drawer.tsx +1 -1
- package/src/file_dropzone.tsx +5 -0
- package/src/file_gallery_modal.tsx +43 -42
- package/src/file_preview.web.tsx +25 -11
- package/src/file_preview_types.ts +9 -0
- package/src/file_row.tsx +5 -1
- package/src/file_rows.tsx +6 -2
- package/src/file_thumbnail.tsx +13 -4
- package/src/file_thumbnail_grid.tsx +5 -0
- package/src/files_editor.tsx +232 -0
- package/src/filter_chip.tsx +25 -12
- package/src/focus_ring_pressable.tsx +34 -0
- package/src/form_picker.tsx +3 -3
- package/src/gantt/gantt_view.tsx +25 -7
- package/src/heatmap.tsx +5 -1
- package/src/icon_button.tsx +2 -2
- package/src/image_gallery.tsx +8 -10
- package/src/index.css +8 -12
- package/src/inline_date_picker.tsx +11 -11
- package/src/inline_edit.tsx +22 -6
- package/src/inline_member_select.tsx +1 -1
- package/src/inline_select.tsx +9 -8
- package/src/inline_text_input.tsx +4 -1
- package/src/link.tsx +11 -1
- package/src/link_button.tsx +1 -1
- package/src/list_item.tsx +2 -2
- package/src/member_select.tsx +9 -4
- package/src/menu_button.tsx +2 -2
- package/src/menu_list_item.tsx +2 -2
- package/src/number_input.tsx +11 -4
- package/src/option_list.tsx +211 -0
- package/src/peek.tsx +1 -1
- package/src/picker.tsx +24 -213
- package/src/pressable_highlight.tsx +42 -23
- package/src/radio_picker.tsx +2 -2
- package/src/range_slider.tsx +35 -7
- package/src/react_native.d.ts +3 -0
- package/src/scroll_to_bottom.tsx +1 -1
- package/src/section.tsx +7 -2
- package/src/segmented_control.tsx +2 -2
- package/src/select.tsx +299 -0
- package/src/sort_header.tsx +1 -1
- package/src/sources.tsx +1 -1
- package/src/spreadsheet_view.tsx +43 -17
- package/src/status_grid.tsx +4 -3
- package/src/stepper.tsx +1 -1
- package/src/switch.tsx +5 -1
- package/src/switch_button.tsx +1 -1
- package/src/switcher.tsx +1 -1
- package/src/table.tsx +5 -1
- package/src/tabs.tsx +2 -2
- package/src/text_input_field.tsx +15 -4
- package/src/time_picker.tsx +11 -4
- package/src/timeline.tsx +1 -1
- package/src/use_focus_ring.ts +80 -0
- package/src/use_hover.ts +26 -0
- package/src/use_list_keyboard_nav.test.ts +71 -0
- package/src/use_list_keyboard_nav.ts +52 -5
- package/src/use_option_list.test.ts +193 -0
- package/src/use_option_list.ts +354 -0
- package/src/command_menu.tsx +0 -205
- package/src/picker_menu.tsx +0 -355
- package/src/tag_input.tsx +0 -203
- package/src/time_field.tsx +0 -297
package/src/picker.tsx
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { Picker as RNPicker } from "@react-native-picker/picker";
|
|
2
|
-
import { StyleSheet, View,
|
|
3
|
-
import {
|
|
2
|
+
import { StyleSheet, View, StyleProp, ViewStyle, TextStyle } from "react-native";
|
|
3
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
4
4
|
import { colors } from "./colors";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { FOCUS_RING, CONTROL_RADIUS, HOVER_BORDER, CONTROL_TRANSITION } from "./control_surface";
|
|
6
|
+
import { useFocusRing } from "./use_focus_ring";
|
|
7
|
+
import { useHover } from "./use_hover";
|
|
7
8
|
import { fontFamilyRegular, getInputTextStyle } from "./text_utils";
|
|
8
9
|
import { Icon } from "./icon";
|
|
9
|
-
import { Popover, PopoverTrigger, PopoverContent } from "./popover";
|
|
10
|
-
import { PickerMenu } from "./picker_menu";
|
|
11
10
|
|
|
12
11
|
export interface PickerOption<T extends string = string, D = unknown> {
|
|
13
12
|
label?: string;
|
|
@@ -31,43 +30,30 @@ export type PickerOnClose<T extends string, MULTI extends boolean> = MULTI exten
|
|
|
31
30
|
? (value: T[]) => void
|
|
32
31
|
: (value: T) => void;
|
|
33
32
|
|
|
34
|
-
export interface PickerProps<T extends string = string
|
|
33
|
+
export interface PickerProps<T extends string = string> {
|
|
35
34
|
options?: (PickerOption<T> | undefined | false)[];
|
|
36
35
|
placeholder?: string;
|
|
37
36
|
/** Accessible name for the control. Required in spirit whenever the picker
|
|
38
37
|
* has no visible label next to it (e.g. an unlabeled toolbar filter) — the
|
|
39
38
|
* selected option's text describes the value, not the control. */
|
|
40
39
|
accessibilityLabel?: string;
|
|
41
|
-
renderOptionContent?: (option: PickerOption<T>) => React.ReactNode;
|
|
42
40
|
style?: StyleProp<ViewStyle>;
|
|
43
41
|
testID?: string;
|
|
44
42
|
disabled?: boolean;
|
|
45
43
|
autoFocus?: boolean;
|
|
46
|
-
enableSelectAll?: boolean;
|
|
47
|
-
multi?: MULTI;
|
|
48
44
|
includeEmptyOption?: boolean;
|
|
49
|
-
value?:
|
|
50
|
-
onValueChange?:
|
|
51
|
-
onClose?: PickerOnClose<T, MULTI>;
|
|
52
|
-
/** Label for the "select all" link in multi-select mode. Default: "Select all" */
|
|
53
|
-
selectAllLabel?: string;
|
|
54
|
-
/** Label for the "deselect all" link in multi-select mode. Default: "Deselect all" */
|
|
55
|
-
deselectAllLabel?: string;
|
|
45
|
+
value?: T | null;
|
|
46
|
+
onValueChange?: (value: T) => void;
|
|
56
47
|
}
|
|
57
48
|
|
|
58
49
|
/**
|
|
59
|
-
*
|
|
50
|
+
* A plain, label-only single select rendered as the native `<select>` (the OS
|
|
51
|
+
* dropdown, with native typeahead). The lightest "pick one from a known list"
|
|
52
|
+
* control. For RICH options (badges, member chips), multi-select, select-all, or
|
|
53
|
+
* a chip box (`renderSelected` → `Chip`), use `Select`; to SEARCH a large/remote/free
|
|
54
|
+
* set with the input AS the control use `Combobox`.
|
|
60
55
|
*/
|
|
61
|
-
export function Picker<T extends string
|
|
62
|
-
props: PickerProps<T, MULTI>,
|
|
63
|
-
) {
|
|
64
|
-
if (props.multi || props.renderOptionContent) {
|
|
65
|
-
return <CustomPicker {...props} />;
|
|
66
|
-
}
|
|
67
|
-
return <StandardPicker {...(props as PickerProps<T, false>)} />;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function StandardPicker<T extends string>(props: PickerProps<T, false>) {
|
|
56
|
+
export function Picker<T extends string>(props: PickerProps<T>) {
|
|
71
57
|
const {
|
|
72
58
|
testID,
|
|
73
59
|
options = [],
|
|
@@ -82,6 +68,9 @@ function StandardPicker<T extends string>(props: PickerProps<T, false>) {
|
|
|
82
68
|
} = props;
|
|
83
69
|
|
|
84
70
|
const pickerRef = useRef<RNPicker<string>>(null);
|
|
71
|
+
// The native <select> is a text-like control — ring on any focus (like the inputs).
|
|
72
|
+
const { focusVisible, focusProps } = useFocusRing({ always: true });
|
|
73
|
+
const { hovered, hoverProps } = useHover();
|
|
85
74
|
|
|
86
75
|
useEffect(() => {
|
|
87
76
|
if (autoFocus && !disabled) {
|
|
@@ -89,15 +78,15 @@ function StandardPicker<T extends string>(props: PickerProps<T, false>) {
|
|
|
89
78
|
}
|
|
90
79
|
}, [autoFocus, disabled]);
|
|
91
80
|
|
|
92
|
-
const
|
|
81
|
+
const handleChange = useCallback(
|
|
93
82
|
(newValue: string) => {
|
|
94
|
-
|
|
83
|
+
onValueChange?.(newValue as T);
|
|
95
84
|
},
|
|
96
85
|
[onValueChange],
|
|
97
86
|
);
|
|
98
87
|
|
|
99
88
|
return (
|
|
100
|
-
<View>
|
|
89
|
+
<View {...(hoverProps as object)}>
|
|
101
90
|
<View
|
|
102
91
|
style={{
|
|
103
92
|
position: "absolute",
|
|
@@ -112,12 +101,14 @@ function StandardPicker<T extends string>(props: PickerProps<T, false>) {
|
|
|
112
101
|
ref={pickerRef}
|
|
113
102
|
testID={testID}
|
|
114
103
|
accessibilityLabel={accessibilityLabel}
|
|
104
|
+
onFocus={focusProps.onFocus}
|
|
105
|
+
onBlur={focusProps.onBlur}
|
|
115
106
|
// Empty selection maps to "" (the placeholder option's value), never
|
|
116
107
|
// undefined — otherwise the native <select> goes uncontrolled and a
|
|
117
108
|
// programmatic reset to null leaves the prior DOM selection in place.
|
|
118
109
|
selectedValue={value?.toString() ?? ""}
|
|
119
|
-
onValueChange={
|
|
120
|
-
style={[styles.picker, getInputTextStyle(), style as StyleProp<TextStyle
|
|
110
|
+
onValueChange={handleChange}
|
|
111
|
+
style={[styles.picker, CONTROL_TRANSITION, getInputTextStyle(), style as StyleProp<TextStyle>, hovered && !disabled ? { borderColor: HOVER_BORDER } : null, focusVisible ? { boxShadow: FOCUS_RING } : null]}
|
|
121
112
|
placeholder={placeholder}
|
|
122
113
|
enabled={!disabled}
|
|
123
114
|
>
|
|
@@ -143,168 +134,6 @@ function StandardPicker<T extends string>(props: PickerProps<T, false>) {
|
|
|
143
134
|
);
|
|
144
135
|
}
|
|
145
136
|
|
|
146
|
-
function CustomPicker<T extends string, MULTI extends boolean = false>(
|
|
147
|
-
props: PickerProps<T, MULTI>,
|
|
148
|
-
) {
|
|
149
|
-
const {
|
|
150
|
-
testID,
|
|
151
|
-
options = [],
|
|
152
|
-
value,
|
|
153
|
-
placeholder,
|
|
154
|
-
accessibilityLabel,
|
|
155
|
-
multi = false as MULTI,
|
|
156
|
-
renderOptionContent,
|
|
157
|
-
includeEmptyOption,
|
|
158
|
-
style,
|
|
159
|
-
disabled = false,
|
|
160
|
-
autoFocus = false,
|
|
161
|
-
enableSelectAll = false,
|
|
162
|
-
onValueChange,
|
|
163
|
-
onClose,
|
|
164
|
-
selectAllLabel,
|
|
165
|
-
deselectAllLabel,
|
|
166
|
-
} = props;
|
|
167
|
-
|
|
168
|
-
const [open, setOpen] = useState(autoFocus);
|
|
169
|
-
|
|
170
|
-
const handleToggle = useCallback(() => {
|
|
171
|
-
if (!disabled) {
|
|
172
|
-
setOpen((prev) => !prev);
|
|
173
|
-
}
|
|
174
|
-
}, [disabled]);
|
|
175
|
-
|
|
176
|
-
const selectedItems = useSelectedItems(multi, value, options);
|
|
177
|
-
|
|
178
|
-
const handleClose = useCallback(() => {
|
|
179
|
-
setOpen(false);
|
|
180
|
-
}, []);
|
|
181
|
-
|
|
182
|
-
return (
|
|
183
|
-
<Popover
|
|
184
|
-
open={open && !disabled}
|
|
185
|
-
onOpenChange={setOpen}
|
|
186
|
-
side="bottom"
|
|
187
|
-
align="start"
|
|
188
|
-
inheritTriggerWidth={true}
|
|
189
|
-
>
|
|
190
|
-
<PopoverTrigger>
|
|
191
|
-
<PickerTrigger
|
|
192
|
-
testID={testID}
|
|
193
|
-
open={open}
|
|
194
|
-
style={style}
|
|
195
|
-
onPress={handleToggle}
|
|
196
|
-
renderOptionContent={renderOptionContent}
|
|
197
|
-
selectedItems={selectedItems}
|
|
198
|
-
placeholder={placeholder}
|
|
199
|
-
accessibilityLabel={accessibilityLabel}
|
|
200
|
-
disabled={disabled}
|
|
201
|
-
/>
|
|
202
|
-
</PopoverTrigger>
|
|
203
|
-
<PopoverContent>
|
|
204
|
-
<PickerMenu
|
|
205
|
-
options={options}
|
|
206
|
-
multi={multi}
|
|
207
|
-
value={value}
|
|
208
|
-
onValueChange={onValueChange}
|
|
209
|
-
onClose={onClose}
|
|
210
|
-
onRequestClose={handleClose}
|
|
211
|
-
renderOptionContent={renderOptionContent}
|
|
212
|
-
enableSelectAll={enableSelectAll}
|
|
213
|
-
includeEmptyOption={includeEmptyOption}
|
|
214
|
-
selectAllLabel={selectAllLabel}
|
|
215
|
-
deselectAllLabel={deselectAllLabel}
|
|
216
|
-
/>
|
|
217
|
-
</PopoverContent>
|
|
218
|
-
</Popover>
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function useSelectedItems<T extends string>(
|
|
223
|
-
multi: boolean,
|
|
224
|
-
value: T | T[] | undefined | null,
|
|
225
|
-
options: (PickerOption<T> | undefined | false)[],
|
|
226
|
-
): PickerOption<T>[] {
|
|
227
|
-
return useMemo(() => {
|
|
228
|
-
if (multi) {
|
|
229
|
-
const multiValue = Array.isArray(value) ? value : [];
|
|
230
|
-
const optionsMap = new Map(
|
|
231
|
-
options.filter((opt): opt is PickerOption<T> => !!opt).map((opt) => [opt.value, opt]),
|
|
232
|
-
);
|
|
233
|
-
return multiValue
|
|
234
|
-
.map((val) => optionsMap.get(val))
|
|
235
|
-
.filter((opt): opt is PickerOption<T> => !!opt);
|
|
236
|
-
} else {
|
|
237
|
-
const singleValue = Array.isArray(value) ? value[0] : value;
|
|
238
|
-
if (singleValue !== undefined && singleValue !== null) {
|
|
239
|
-
const selected = options.find((option) => option && option.value === singleValue);
|
|
240
|
-
return selected ? [selected] : [];
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
return [];
|
|
244
|
-
}, [multi, value, options]);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function PickerTrigger<T extends string>({
|
|
248
|
-
ref,
|
|
249
|
-
testID,
|
|
250
|
-
open,
|
|
251
|
-
style,
|
|
252
|
-
onPress,
|
|
253
|
-
renderOptionContent,
|
|
254
|
-
selectedItems,
|
|
255
|
-
placeholder,
|
|
256
|
-
accessibilityLabel,
|
|
257
|
-
disabled = false,
|
|
258
|
-
}: {
|
|
259
|
-
ref?: React.Ref<View>;
|
|
260
|
-
testID?: string;
|
|
261
|
-
open: boolean;
|
|
262
|
-
style?: StyleProp<ViewStyle>;
|
|
263
|
-
onPress: () => void;
|
|
264
|
-
renderOptionContent?: (option: PickerOption<T>) => React.ReactNode;
|
|
265
|
-
selectedItems: PickerOption<T>[];
|
|
266
|
-
placeholder?: string;
|
|
267
|
-
accessibilityLabel?: string;
|
|
268
|
-
disabled?: boolean;
|
|
269
|
-
}) {
|
|
270
|
-
const hasSelection = selectedItems.length > 0;
|
|
271
|
-
|
|
272
|
-
return (
|
|
273
|
-
<Pressable
|
|
274
|
-
ref={ref}
|
|
275
|
-
testID={testID}
|
|
276
|
-
// Without a role this Pressable renders as an unfocusable <div> on web —
|
|
277
|
-
// the trigger drops out of the tab order and Enter/Space can't open it.
|
|
278
|
-
// `button` makes it tab-focusable and maps keyboard activation to onPress;
|
|
279
|
-
// `expanded` announces open/closed to assistive tech. The accessible name
|
|
280
|
-
// comes from `accessibilityLabel` when given (an unlabeled control), else
|
|
281
|
-
// the visible selection/placeholder text below.
|
|
282
|
-
accessibilityRole="button"
|
|
283
|
-
accessibilityLabel={accessibilityLabel}
|
|
284
|
-
accessibilityState={{ expanded: open, disabled }}
|
|
285
|
-
style={[styles.pressable, open && styles.opened, disabled && styles.disabled, style]}
|
|
286
|
-
onPress={!disabled ? onPress : undefined}
|
|
287
|
-
disabled={disabled}
|
|
288
|
-
>
|
|
289
|
-
{hasSelection ? (
|
|
290
|
-
renderOptionContent ? (
|
|
291
|
-
<View style={{ flexDirection: "row", gap: 4, flexWrap: "wrap" }}>
|
|
292
|
-
{selectedItems.map((item) => (
|
|
293
|
-
<View key={item.value}>{renderOptionContent(item)}</View>
|
|
294
|
-
))}
|
|
295
|
-
</View>
|
|
296
|
-
) : (
|
|
297
|
-
<Text userSelect="none">{selectedItems.map((s) => s.label).join(", ")}</Text>
|
|
298
|
-
)
|
|
299
|
-
) : (
|
|
300
|
-
<Text color="zinc-500" userSelect="none">
|
|
301
|
-
{placeholder}
|
|
302
|
-
</Text>
|
|
303
|
-
)}
|
|
304
|
-
</Pressable>
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
137
|
const styles = StyleSheet.create({
|
|
309
138
|
picker: {
|
|
310
139
|
height: 40,
|
|
@@ -318,22 +147,4 @@ const styles = StyleSheet.create({
|
|
|
318
147
|
backgroundColor: "transparent",
|
|
319
148
|
appearance: "none",
|
|
320
149
|
},
|
|
321
|
-
pressable: {
|
|
322
|
-
flexDirection: "row",
|
|
323
|
-
alignItems: "center",
|
|
324
|
-
paddingHorizontal: 6,
|
|
325
|
-
borderWidth: 1,
|
|
326
|
-
borderColor: colors.border,
|
|
327
|
-
borderRadius: CONTROL_RADIUS,
|
|
328
|
-
cursor: "auto",
|
|
329
|
-
height: 40,
|
|
330
|
-
},
|
|
331
|
-
opened: {
|
|
332
|
-
// Mouse-opened, so no `:focus-visible` — wear the same 2px zinc ring a
|
|
333
|
-
// focused input gets, over the unchanged 1px border (not a lone 1px edge).
|
|
334
|
-
boxShadow: ACTIVE_RING,
|
|
335
|
-
},
|
|
336
|
-
disabled: {
|
|
337
|
-
opacity: 0.5,
|
|
338
|
-
},
|
|
339
150
|
});
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
GestureResponderEvent,
|
|
3
3
|
Pressable,
|
|
4
4
|
PressableProps,
|
|
5
|
+
PressableStateCallbackType,
|
|
5
6
|
View,
|
|
6
7
|
StyleProp,
|
|
7
8
|
ViewStyle,
|
|
@@ -9,23 +10,40 @@ import {
|
|
|
9
10
|
import { Ref, useCallback } from "react";
|
|
10
11
|
import { TooltipSide, useTooltip } from "./tooltip";
|
|
11
12
|
import { colors } from "./colors";
|
|
13
|
+
import { FOCUS_RING } from "./control_surface";
|
|
14
|
+
import { composeHandler, useFocusRing } from "./use_focus_ring";
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
a(event);
|
|
21
|
-
b(event);
|
|
22
|
-
};
|
|
23
|
-
}
|
|
16
|
+
/** The pressable state, plus the web-only `hovered` flag react-native-web adds and
|
|
17
|
+
* the `focusVisible` flag PressableHighlight injects so a consumer's style-fn /
|
|
18
|
+
* children can paint their own focus treatment. */
|
|
19
|
+
export type PressableHighlightState = PressableStateCallbackType & {
|
|
20
|
+
hovered?: boolean;
|
|
21
|
+
focusVisible?: boolean;
|
|
22
|
+
};
|
|
24
23
|
|
|
25
24
|
export interface PressableHighlightProps extends PressableProps {
|
|
26
25
|
ref?: Ref<View>;
|
|
27
26
|
tooltip?: string;
|
|
28
27
|
tooltipSide?: TooltipSide;
|
|
28
|
+
/**
|
|
29
|
+
* Opt in to the kit `FOCUS_RING` on keyboard focus. Off by default: the focus
|
|
30
|
+
* treatment is the component's to OWN, not applied blindly — a compact control
|
|
31
|
+
* sets `focusRing`, while a control with its own ring (e.g. `CardSelectItem`, which
|
|
32
|
+
* rings on hover/press/focus) instead reads `state.focusVisible` in its `style` fn.
|
|
33
|
+
* EVERY interactive PressableHighlight must do one or the other; a focusable control
|
|
34
|
+
* with no focus treatment is a bug (the per-Pressable focus audit is the coverage
|
|
35
|
+
* gate). A surface that shouldn't ring is a non-control — give it `tabIndex={-1}`
|
|
36
|
+
* (like `PressableRow`), not a missing `focusRing`. The ring is a later `boxShadow`
|
|
37
|
+
* entry, so on a focused control it supersedes any resting box-shadow for the focus
|
|
38
|
+
* duration — read `state.focusVisible` and compose if both must show at once.
|
|
39
|
+
*/
|
|
40
|
+
focusRing?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Narrows the Pressable state-function `style` to `PressableHighlightState`, so a
|
|
43
|
+
* consumer's `style` fn sees `focusVisible` (and `hovered`) and can paint its own
|
|
44
|
+
* focus/hover treatment — the `state.focusVisible` contract referenced above.
|
|
45
|
+
*/
|
|
46
|
+
style?: StyleProp<ViewStyle> | ((state: PressableHighlightState) => StyleProp<ViewStyle>);
|
|
29
47
|
/**
|
|
30
48
|
* Web-only keyboard handler. `react-native-web`'s `Pressable` forwards this
|
|
31
49
|
* to the rendered DOM element; native platforms ignore it. Exposed
|
|
@@ -56,12 +74,14 @@ export function PressableHighlight(props: PressableHighlightProps) {
|
|
|
56
74
|
tooltipSide = "top",
|
|
57
75
|
onPress,
|
|
58
76
|
userSelect,
|
|
77
|
+
focusRing,
|
|
59
78
|
...restPressableProps
|
|
60
79
|
} = props;
|
|
61
80
|
const tooltipProps = useTooltip({
|
|
62
81
|
side: tooltipSide,
|
|
63
82
|
text: tooltip,
|
|
64
83
|
});
|
|
84
|
+
const { focusVisible, focusProps } = useFocusRing();
|
|
65
85
|
|
|
66
86
|
const handlePress = useCallback(
|
|
67
87
|
(event: GestureResponderEvent) => {
|
|
@@ -74,10 +94,8 @@ export function PressableHighlight(props: PressableHighlightProps) {
|
|
|
74
94
|
<Pressable
|
|
75
95
|
ref={ref}
|
|
76
96
|
disabled={disabled}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const { pressed } = state;
|
|
80
|
-
const hovered = (state as { hovered?: boolean }).hovered;
|
|
97
|
+
style={(state: PressableHighlightState) => {
|
|
98
|
+
const { pressed, hovered } = state;
|
|
81
99
|
return [
|
|
82
100
|
{
|
|
83
101
|
...({ touchAction: "manipulation", cursor: disabled ? "auto" : "pointer", transitionDuration: "0.1s", transitionProperty: "background-color", userSelect } as ViewStyle),
|
|
@@ -85,22 +103,23 @@ export function PressableHighlight(props: PressableHighlightProps) {
|
|
|
85
103
|
{
|
|
86
104
|
backgroundColor: pressed ? colors.zinc["200"] : hovered ? colors.zinc["100"] : null,
|
|
87
105
|
},
|
|
88
|
-
typeof style === "function" ? style(state) : style,
|
|
106
|
+
typeof style === "function" ? style({ ...state, focusVisible }) : style,
|
|
107
|
+
focusRing && focusVisible ? { boxShadow: FOCUS_RING } : null,
|
|
89
108
|
] as StyleProp<ViewStyle>;
|
|
90
109
|
}}
|
|
91
110
|
onPress={handlePress}
|
|
92
111
|
{...restPressableProps}
|
|
93
112
|
// `onFocus` / `onBlur` are part of `PressableProps`, so a trailing
|
|
94
113
|
// `{...tooltipProps}` spread would silently overwrite caller-provided
|
|
95
|
-
// handlers. Compose instead
|
|
96
|
-
// passed through on web) come along
|
|
114
|
+
// handlers. Compose instead — caller + tooltip + the focus-ring tracker.
|
|
115
|
+
// Mouse handlers (not in the RN types but passed through on web) come along
|
|
116
|
+
// with the tooltipProps spread.
|
|
97
117
|
{...tooltipProps}
|
|
98
|
-
onFocus={composeHandler(restPressableProps.onFocus ?? undefined, tooltipProps.onFocus)}
|
|
99
|
-
onBlur={composeHandler(restPressableProps.onBlur ?? undefined, tooltipProps.onBlur)}
|
|
118
|
+
onFocus={composeHandler(composeHandler(restPressableProps.onFocus ?? undefined, tooltipProps.onFocus), focusProps.onFocus)}
|
|
119
|
+
onBlur={composeHandler(composeHandler(restPressableProps.onBlur ?? undefined, tooltipProps.onBlur), focusProps.onBlur)}
|
|
100
120
|
>
|
|
101
|
-
{(state) => {
|
|
102
|
-
|
|
103
|
-
return <>{typeof children === "function" ? children({ ...state, hovered } as typeof state) : children}</>;
|
|
121
|
+
{(state: PressableHighlightState) => {
|
|
122
|
+
return <>{typeof children === "function" ? children({ ...state, focusVisible } as PressableStateCallbackType) : children}</>;
|
|
104
123
|
}}
|
|
105
124
|
</Pressable>
|
|
106
125
|
);
|
package/src/radio_picker.tsx
CHANGED
package/src/range_slider.tsx
CHANGED
|
@@ -2,6 +2,8 @@ import { useRef, useState } from "react";
|
|
|
2
2
|
import { StyleSheet, View, type GestureResponderEvent, type LayoutChangeEvent, type ViewStyle } from "react-native";
|
|
3
3
|
import { Text } from "./text";
|
|
4
4
|
import { colors } from "./colors";
|
|
5
|
+
import { FOCUS_RING } from "./control_surface";
|
|
6
|
+
import { useFocusRing } from "./use_focus_ring";
|
|
5
7
|
|
|
6
8
|
const THUMB = 20;
|
|
7
9
|
const R = THUMB / 2;
|
|
@@ -23,6 +25,30 @@ export interface RangeSliderProps {
|
|
|
23
25
|
|
|
24
26
|
type Thumb = "low" | "high";
|
|
25
27
|
|
|
28
|
+
function SliderThumb(props: {
|
|
29
|
+
label: string;
|
|
30
|
+
min: number;
|
|
31
|
+
max: number;
|
|
32
|
+
now: number;
|
|
33
|
+
left: number;
|
|
34
|
+
color: string;
|
|
35
|
+
active: boolean;
|
|
36
|
+
keyProps: object;
|
|
37
|
+
}) {
|
|
38
|
+
const { focusVisible, focusProps } = useFocusRing();
|
|
39
|
+
return (
|
|
40
|
+
<View
|
|
41
|
+
accessibilityRole="adjustable"
|
|
42
|
+
accessibilityLabel={props.label}
|
|
43
|
+
accessibilityValue={{ min: props.min, max: props.max, now: props.now }}
|
|
44
|
+
focusable
|
|
45
|
+
{...focusProps}
|
|
46
|
+
style={[styles.thumb, { left: props.left, borderColor: props.color }, props.active && styles.thumbActive, focusVisible && { boxShadow: FOCUS_RING }]}
|
|
47
|
+
{...props.keyProps}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
26
52
|
/**
|
|
27
53
|
* A short human summary of a range selection for a `FilterChip` preview —
|
|
28
54
|
* "5tr–10tr", or "≥ 5tr" / "≤ 10tr" when one end sits at its bound, and
|
|
@@ -129,14 +155,16 @@ export function RangeSlider(props: RangeSliderProps) {
|
|
|
129
155
|
{(["low", "high"] as const).map((thumb) => {
|
|
130
156
|
const v = thumb === "low" ? low : high;
|
|
131
157
|
return (
|
|
132
|
-
<
|
|
158
|
+
<SliderThumb
|
|
133
159
|
key={thumb}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
{
|
|
160
|
+
label={`${accessibilityLabel} ${thumb === "low" ? "minimum" : "maximum"}`}
|
|
161
|
+
min={min}
|
|
162
|
+
max={max}
|
|
163
|
+
now={v}
|
|
164
|
+
left={x(v)}
|
|
165
|
+
color={color}
|
|
166
|
+
active={dragging === thumb}
|
|
167
|
+
keyProps={keyProps(thumb)}
|
|
140
168
|
/>
|
|
141
169
|
);
|
|
142
170
|
})}
|
package/src/react_native.d.ts
CHANGED
|
@@ -32,11 +32,14 @@ declare module "react-native" {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
interface TextStyle {
|
|
35
|
+
boxShadow?: string;
|
|
35
36
|
outline?: string;
|
|
36
37
|
outlineColor?: string;
|
|
37
38
|
outlineStyle?: string;
|
|
38
39
|
outlineWidth?: number;
|
|
39
40
|
outlineOffset?: number;
|
|
41
|
+
transitionDuration?: string;
|
|
42
|
+
transitionProperty?: string;
|
|
40
43
|
appearance?: string;
|
|
41
44
|
}
|
|
42
45
|
}
|
package/src/scroll_to_bottom.tsx
CHANGED
package/src/section.tsx
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { Pressable,
|
|
1
|
+
import { Pressable, View } from "react-native";
|
|
2
2
|
import { Text } from "./text";
|
|
3
3
|
import { useState, useImperativeHandle } from "react";
|
|
4
4
|
import { Icon, IconName } from "./icon";
|
|
5
5
|
import { Spacer } from "./spacer";
|
|
6
|
+
import { FOCUS_RING } from "./control_surface";
|
|
7
|
+
import { useFocusRing } from "./use_focus_ring";
|
|
6
8
|
|
|
7
9
|
export interface SectionHandlers {
|
|
8
10
|
expand: () => void;
|
|
@@ -46,9 +48,12 @@ export function Section(props: SectionProps) {
|
|
|
46
48
|
},
|
|
47
49
|
}));
|
|
48
50
|
|
|
51
|
+
const { focusVisible, focusProps } = useFocusRing();
|
|
52
|
+
|
|
49
53
|
const titleComponent = collapsible ? (
|
|
50
54
|
<Pressable
|
|
51
|
-
|
|
55
|
+
{...focusProps}
|
|
56
|
+
style={[{ flexDirection: "row", alignItems: "center", gap: 8, flex: 1 }, focusVisible && { boxShadow: FOCUS_RING, borderRadius: 4 }]}
|
|
52
57
|
onPress={() => setCollapsed(!collapsed)}
|
|
53
58
|
>
|
|
54
59
|
<Icon name={collapsed ? "chevron-right" : "chevron-down"} size={20} />
|
|
@@ -126,8 +126,8 @@ function Segment<T extends string>(props: SegmentProps<T>) {
|
|
|
126
126
|
|
|
127
127
|
return (
|
|
128
128
|
<PressableHighlight
|
|
129
|
-
|
|
130
|
-
// Own the per-state background: the track is zinc-100, so an unselected
|
|
129
|
+
focusRing
|
|
130
|
+
ref={ref} // Own the per-state background: the track is zinc-100, so an unselected
|
|
131
131
|
// segment hovering must lift TOWARD the selected white (a half-white wash),
|
|
132
132
|
// not to PressableHighlight's default zinc-100 (invisible on the track).
|
|
133
133
|
style={(state) => {
|