@lotics/ui 5.8.0 → 5.10.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/AGENTS.md +184 -56
- package/examples/tpl_approvals.tsx +3 -3
- package/examples/tpl_convert.tsx +11 -23
- package/examples/tpl_dossier.tsx +5 -4
- package/examples/tpl_order.tsx +1 -1
- package/examples/tpl_pipeline.tsx +400 -0
- package/examples/tpl_ratedesk.tsx +1 -1
- package/examples/tpl_record.tsx +6 -5
- package/examples/tpl_shifts.tsx +7 -5
- package/examples/tpl_task_board.tsx +473 -0
- package/examples/tpl_tasks.tsx +440 -0
- package/package.json +19 -6
- 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 +227 -139
- package/src/file_grid.tsx +184 -0
- package/src/file_preview.web.tsx +349 -12
- package/src/file_preview_types.ts +49 -0
- package/src/file_row.tsx +118 -0
- package/src/file_rows.tsx +109 -0
- package/src/file_thumbnail.tsx +132 -81
- package/src/file_thumbnail_grid.tsx +135 -55
- 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 +14 -3
- package/src/image_gallery.tsx +8 -14
- 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/pdfjs_worker.d.ts +5 -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/uploading_thumbnail.tsx +212 -0
- 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/hover_action.tsx +0 -54
- package/src/picker_menu.tsx +0 -355
- package/src/tag_input.tsx +0 -203
- package/src/time_field.tsx +0 -297
package/src/button.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
ActivityIndicator,
|
|
3
3
|
NativeSyntheticEvent,
|
|
4
4
|
Pressable,
|
|
5
|
+
PressableStateCallbackType,
|
|
5
6
|
StyleProp,
|
|
6
7
|
StyleSheet,
|
|
7
8
|
View,
|
|
@@ -9,11 +10,12 @@ import {
|
|
|
9
10
|
} from "react-native";
|
|
10
11
|
import { Text } from "./text";
|
|
11
12
|
import { colors } from "./colors";
|
|
12
|
-
import { CONTROL_RADIUS } from "./control_surface";
|
|
13
|
+
import { CONTROL_RADIUS, FOCUS_RING } from "./control_surface";
|
|
13
14
|
import { Icon, IconName } from "./icon";
|
|
14
15
|
import { Ref, useCallback } from "react";
|
|
15
16
|
import { TextColor } from "./text_utils";
|
|
16
17
|
import { useTooltip, UseTooltipOptions } from "./tooltip";
|
|
18
|
+
import { useFocusRing, composeHandler } from "./use_focus_ring";
|
|
17
19
|
import { getButtonIconColor } from "./button_colors";
|
|
18
20
|
export type ButtonColor = "primary" | "secondary" | "danger" | "muted" | "danger-secondary";
|
|
19
21
|
export type ButtonIconPosition = "left" | "right";
|
|
@@ -60,6 +62,7 @@ export function Button(props: ButtonProps) {
|
|
|
60
62
|
|
|
61
63
|
const disabledOrLoading = disabled || loading;
|
|
62
64
|
const tooltipProps = useTooltip(tooltip);
|
|
65
|
+
const { focusVisible, focusProps } = useFocusRing();
|
|
63
66
|
|
|
64
67
|
const handlePress = useCallback(
|
|
65
68
|
(event: NativeSyntheticEvent<any>) => {
|
|
@@ -106,8 +109,7 @@ export function Button(props: ButtonProps) {
|
|
|
106
109
|
}
|
|
107
110
|
aria-disabled={disabledOrLoading || undefined} aria-busy={loading || undefined}
|
|
108
111
|
disabled={disabledOrLoading}
|
|
109
|
-
|
|
110
|
-
style={({ pressed, hovered }) => {
|
|
112
|
+
style={({ pressed, hovered }: PressableStateCallbackType & { hovered?: boolean }) => {
|
|
111
113
|
const isPrimary = color === "primary" && !disabled;
|
|
112
114
|
const restPrimary = isPrimary && !pressed && !hovered;
|
|
113
115
|
return [
|
|
@@ -130,10 +132,17 @@ export function Button(props: ButtonProps) {
|
|
|
130
132
|
styles.button,
|
|
131
133
|
style,
|
|
132
134
|
alignSelf && { alignSelf },
|
|
135
|
+
// Keyboard focus: paint the ring last so it wins over the primary's depth
|
|
136
|
+
// shadow (one boxShadow key — separate entries can't stack). The ring sits
|
|
137
|
+
// 2px OUTSIDE the box on the page, so it stays visible even on the dark
|
|
138
|
+
// primary.
|
|
139
|
+
focusVisible && { boxShadow: FOCUS_RING },
|
|
133
140
|
] as StyleProp<ViewStyle>;
|
|
134
141
|
}}
|
|
135
142
|
onPress={handlePress}
|
|
136
143
|
{...tooltipProps}
|
|
144
|
+
onFocus={composeHandler(tooltipProps.onFocus, focusProps.onFocus)}
|
|
145
|
+
onBlur={composeHandler(tooltipProps.onBlur, focusProps.onBlur)}
|
|
137
146
|
>
|
|
138
147
|
{children}
|
|
139
148
|
</Pressable>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
|
-
import { View,
|
|
2
|
+
import { View, StyleSheet } from "react-native";
|
|
3
3
|
import { Text } from "../text";
|
|
4
4
|
import { colors } from "../colors";
|
|
5
|
+
import { FocusRingPressable } from "../focus_ring_pressable";
|
|
5
6
|
import { MonthView } from "./month_view";
|
|
6
7
|
import { TimeGridView } from "./time_grid_view";
|
|
7
8
|
import { addDays, addMonths, viewTitle } from "./dates";
|
|
@@ -50,15 +51,15 @@ export function CalendarView<T = unknown>(props: CalendarViewProps<T>) {
|
|
|
50
51
|
<View style={styles.root}>
|
|
51
52
|
<View style={styles.toolbar}>
|
|
52
53
|
<View style={styles.navGroup}>
|
|
53
|
-
<
|
|
54
|
+
<FocusRingPressable accessibilityRole="button" onPress={() => step(-1)} accessibilityLabel={L.previous} style={styles.iconBtn}>
|
|
54
55
|
<Text size="lg" color="muted">‹</Text>
|
|
55
|
-
</
|
|
56
|
-
<
|
|
56
|
+
</FocusRingPressable>
|
|
57
|
+
<FocusRingPressable accessibilityRole="button" onPress={() => setDate(new Date())} style={styles.todayBtn}>
|
|
57
58
|
<Text size="sm" weight="medium">{L.today}</Text>
|
|
58
|
-
</
|
|
59
|
-
<
|
|
59
|
+
</FocusRingPressable>
|
|
60
|
+
<FocusRingPressable accessibilityRole="button" onPress={() => step(1)} accessibilityLabel={L.next} style={styles.iconBtn}>
|
|
60
61
|
<Text size="lg" color="muted">›</Text>
|
|
61
|
-
</
|
|
62
|
+
</FocusRingPressable>
|
|
62
63
|
</View>
|
|
63
64
|
|
|
64
65
|
<Text size="lg" weight="semibold" style={styles.title} numberOfLines={1}>
|
|
@@ -67,16 +68,15 @@ export function CalendarView<T = unknown>(props: CalendarViewProps<T>) {
|
|
|
67
68
|
|
|
68
69
|
<View style={styles.viewSwitch}>
|
|
69
70
|
{VIEW_ORDER.map((v) => (
|
|
70
|
-
<
|
|
71
|
+
<FocusRingPressable accessibilityRole="button"
|
|
71
72
|
key={v}
|
|
72
73
|
onPress={() => setView(v)}
|
|
73
|
-
accessibilityRole="button"
|
|
74
74
|
style={[styles.viewBtn, view === v && styles.viewBtnActive]}
|
|
75
75
|
>
|
|
76
76
|
<Text size="sm" weight={view === v ? "medium" : "regular"} color={view === v ? "default" : "muted"}>
|
|
77
77
|
{L[v]}
|
|
78
78
|
</Text>
|
|
79
|
-
</
|
|
79
|
+
</FocusRingPressable>
|
|
80
80
|
))}
|
|
81
81
|
</View>
|
|
82
82
|
</View>
|
|
@@ -1,7 +1,28 @@
|
|
|
1
|
-
import { useMemo, useRef } from "react";
|
|
2
|
-
import { View,
|
|
1
|
+
import { useMemo, useRef, type ReactNode, type Ref } from "react";
|
|
2
|
+
import { View, StyleSheet, type StyleProp, type ViewStyle } from "react-native";
|
|
3
3
|
import { Text } from "../text";
|
|
4
4
|
import { colors } from "../colors";
|
|
5
|
+
import { FocusRingPressable } from "../focus_ring_pressable";
|
|
6
|
+
|
|
7
|
+
function MonthPressable(props: {
|
|
8
|
+
onPress?: () => void;
|
|
9
|
+
accessibilityLabel?: string;
|
|
10
|
+
style: StyleProp<ViewStyle>;
|
|
11
|
+
pressableRef?: Ref<View>;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<FocusRingPressable
|
|
16
|
+
ref={props.pressableRef}
|
|
17
|
+
onPress={props.onPress}
|
|
18
|
+
accessibilityRole={props.onPress ? "button" : undefined}
|
|
19
|
+
accessibilityLabel={props.accessibilityLabel}
|
|
20
|
+
style={props.style}
|
|
21
|
+
>
|
|
22
|
+
{props.children}
|
|
23
|
+
</FocusRingPressable>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
5
26
|
import { usePointerDrag } from "../use_pointer_drag";
|
|
6
27
|
import { packEventLanes } from "./layout";
|
|
7
28
|
import { addDays, dayDiff, daysInView, isSameMonth, isToday, startOfDay, weekdayShort } from "./dates";
|
|
@@ -89,10 +110,9 @@ export function MonthView<T = unknown>(props: MonthViewProps<T>) {
|
|
|
89
110
|
const inMonth = isSameMonth(day, date);
|
|
90
111
|
const today = isToday(day, now);
|
|
91
112
|
return (
|
|
92
|
-
<
|
|
113
|
+
<MonthPressable
|
|
93
114
|
key={day.toISOString()}
|
|
94
115
|
onPress={onDayPress ? () => onDayPress(day) : undefined}
|
|
95
|
-
accessibilityRole={onDayPress ? "button" : undefined}
|
|
96
116
|
style={styles.dateCell}
|
|
97
117
|
>
|
|
98
118
|
<View style={[styles.dateBadge, today && { backgroundColor: colors.teal[600] }]}>
|
|
@@ -104,7 +124,7 @@ export function MonthView<T = unknown>(props: MonthViewProps<T>) {
|
|
|
104
124
|
{day.getDate()}
|
|
105
125
|
</Text>
|
|
106
126
|
</View>
|
|
107
|
-
</
|
|
127
|
+
</MonthPressable>
|
|
108
128
|
);
|
|
109
129
|
})}
|
|
110
130
|
</View>
|
|
@@ -124,11 +144,10 @@ export function MonthView<T = unknown>(props: MonthViewProps<T>) {
|
|
|
124
144
|
const banner = bar.span > 1 || !!bar.event.allDay;
|
|
125
145
|
const drag = live && live.id === bar.event.id ? live : null;
|
|
126
146
|
return (
|
|
127
|
-
<
|
|
147
|
+
<MonthPressable
|
|
128
148
|
key={bar.event.id}
|
|
129
|
-
|
|
149
|
+
pressableRef={onEventDrop ? bind(bar.event.id, "grab") : undefined}
|
|
130
150
|
onPress={onEventPress ? () => onEventPress(bar.event) : undefined}
|
|
131
|
-
accessibilityRole={onEventPress ? "button" : undefined}
|
|
132
151
|
accessibilityLabel={bar.event.title}
|
|
133
152
|
style={{
|
|
134
153
|
position: "absolute",
|
|
@@ -161,18 +180,18 @@ export function MonthView<T = unknown>(props: MonthViewProps<T>) {
|
|
|
161
180
|
{bar.event.title}
|
|
162
181
|
</Text>
|
|
163
182
|
</View>
|
|
164
|
-
</
|
|
183
|
+
</MonthPressable>
|
|
165
184
|
);
|
|
166
185
|
})}
|
|
167
186
|
{weekDays.map((day, col) =>
|
|
168
187
|
overflow[col] > 0 ? (
|
|
169
|
-
<
|
|
188
|
+
<MonthPressable
|
|
170
189
|
key={`o${col}`}
|
|
171
190
|
onPress={onDayPress ? () => onDayPress(day) : undefined}
|
|
172
191
|
style={{ position: "absolute", top: MAX_LANES * (LANE_H + LANE_GAP), left: `${(col / 7) * 100}%`, width: `${100 / 7}%`, paddingHorizontal: 6 }}
|
|
173
192
|
>
|
|
174
193
|
<Text size="xs" color="muted">{moreLabel(overflow[col])}</Text>
|
|
175
|
-
</
|
|
194
|
+
</MonthPressable>
|
|
176
195
|
) : null,
|
|
177
196
|
)}
|
|
178
197
|
</View>
|
|
@@ -1,7 +1,26 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { View, ScrollView,
|
|
1
|
+
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
2
|
+
import { View, ScrollView, StyleSheet, Text as RNText, type StyleProp, type ViewStyle } from "react-native";
|
|
3
3
|
import { Text } from "../text";
|
|
4
4
|
import { colors } from "../colors";
|
|
5
|
+
import { FocusRingPressable } from "../focus_ring_pressable";
|
|
6
|
+
|
|
7
|
+
function TimeGridPressable(props: {
|
|
8
|
+
onPress?: () => void;
|
|
9
|
+
accessibilityLabel?: string;
|
|
10
|
+
style: StyleProp<ViewStyle>;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
}) {
|
|
13
|
+
return (
|
|
14
|
+
<FocusRingPressable
|
|
15
|
+
onPress={props.onPress}
|
|
16
|
+
accessibilityRole={props.onPress ? "button" : undefined}
|
|
17
|
+
accessibilityLabel={props.accessibilityLabel}
|
|
18
|
+
style={props.style}
|
|
19
|
+
>
|
|
20
|
+
{props.children}
|
|
21
|
+
</FocusRingPressable>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
5
24
|
import { layoutDayColumns, packEventLanes } from "./layout";
|
|
6
25
|
import {
|
|
7
26
|
daysInView,
|
|
@@ -56,7 +75,7 @@ function EventBlock<T>({
|
|
|
56
75
|
const accent = event.color || colors.teal[600];
|
|
57
76
|
const compact = height < 34;
|
|
58
77
|
return (
|
|
59
|
-
<
|
|
78
|
+
<FocusRingPressable
|
|
60
79
|
onPress={onPress ? () => onPress(event) : undefined}
|
|
61
80
|
accessibilityRole={onPress ? "button" : undefined}
|
|
62
81
|
accessibilityLabel={event.title}
|
|
@@ -90,7 +109,7 @@ function EventBlock<T>({
|
|
|
90
109
|
</Text>
|
|
91
110
|
) : null}
|
|
92
111
|
</View>
|
|
93
|
-
</
|
|
112
|
+
</FocusRingPressable>
|
|
94
113
|
);
|
|
95
114
|
}
|
|
96
115
|
|
|
@@ -170,10 +189,9 @@ export function TimeGridView<T = unknown>(props: TimeGridViewProps<T>) {
|
|
|
170
189
|
{allDayPacked.bars.map((bar) => {
|
|
171
190
|
const accent = bar.event.color || colors.teal[600];
|
|
172
191
|
return (
|
|
173
|
-
<
|
|
192
|
+
<TimeGridPressable
|
|
174
193
|
key={bar.event.id}
|
|
175
194
|
onPress={onEventPress ? () => onEventPress(bar.event) : undefined}
|
|
176
|
-
accessibilityRole={onEventPress ? "button" : undefined}
|
|
177
195
|
accessibilityLabel={bar.event.title}
|
|
178
196
|
style={{
|
|
179
197
|
position: "absolute",
|
|
@@ -189,7 +207,7 @@ export function TimeGridView<T = unknown>(props: TimeGridViewProps<T>) {
|
|
|
189
207
|
{bar.event.title}
|
|
190
208
|
</Text>
|
|
191
209
|
</View>
|
|
192
|
-
</
|
|
210
|
+
</TimeGridPressable>
|
|
193
211
|
);
|
|
194
212
|
})}
|
|
195
213
|
</View>
|
package/src/card_select_item.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { StyleProp, StyleSheet, ViewStyle } from "react-native";
|
|
2
2
|
import { colors } from "./colors";
|
|
3
|
-
import { CONTROL_RADIUS } from "./control_surface";
|
|
4
|
-
import { PressableHighlight } from "./pressable_highlight";
|
|
3
|
+
import { CONTROL_RADIUS, FOCUS_RING } from "./control_surface";
|
|
4
|
+
import { PressableHighlight, PressableHighlightState } from "./pressable_highlight";
|
|
5
5
|
|
|
6
6
|
interface CardSelectItemProps {
|
|
7
7
|
children: React.ReactNode;
|
|
@@ -18,8 +18,8 @@ interface CardSelectItemProps {
|
|
|
18
18
|
/**
|
|
19
19
|
* A bordered, card-shaped button — the selectable item used on auth screens
|
|
20
20
|
* (organization picker, login choices) and in entity switchers. Always
|
|
21
|
-
* interactive: a real focusable `button` that shows
|
|
22
|
-
*
|
|
21
|
+
* interactive: a real focusable `button` that shows the kit FOCUS_RING on
|
|
22
|
+
* hover/press/keyboard focus. Pass `selected` to keep that ring on
|
|
23
23
|
* for the current item in a single-selection list. For a static surface use Card.
|
|
24
24
|
*/
|
|
25
25
|
export function CardSelectItem(props: CardSelectItemProps) {
|
|
@@ -32,9 +32,8 @@ export function CardSelectItem(props: CardSelectItemProps) {
|
|
|
32
32
|
accessibilityLabel={accessibilityLabel}
|
|
33
33
|
aria-pressed={selected}
|
|
34
34
|
onPress={onPress}
|
|
35
|
-
style={(state:
|
|
36
|
-
const
|
|
37
|
-
const active = selected || hovered || state.pressed;
|
|
35
|
+
style={(state: PressableHighlightState) => {
|
|
36
|
+
const active = selected || state.hovered || state.pressed || state.focusVisible;
|
|
38
37
|
return [styles.container, active && styles.ring, style];
|
|
39
38
|
}}
|
|
40
39
|
>
|
|
@@ -51,11 +50,10 @@ const styles = StyleSheet.create({
|
|
|
51
50
|
borderColor: colors.border,
|
|
52
51
|
borderWidth: 1,
|
|
53
52
|
},
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
// border-darken. boxShadow keeps it layout-neutral (no 1px→2px reflow).
|
|
53
|
+
// The kit FOCUS_RING flush against the border (spread 2, no offset/blur), so
|
|
54
|
+
// hover, press, and keyboard focus all read as one consistent ring rather than a
|
|
55
|
+
// thin border-darken. boxShadow keeps it layout-neutral (no 1px→2px reflow).
|
|
58
56
|
ring: {
|
|
59
|
-
...({ boxShadow:
|
|
57
|
+
...({ boxShadow: FOCUS_RING } as ViewStyle),
|
|
60
58
|
},
|
|
61
59
|
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { Animated, StyleSheet, View, type GestureResponderEvent } from "react-native";
|
|
3
|
+
import { colors } from "./colors";
|
|
4
|
+
import { FocusRingPressable } from "./focus_ring_pressable";
|
|
5
|
+
import { Icon } from "./icon";
|
|
6
|
+
|
|
7
|
+
export interface CheckCircleProps {
|
|
8
|
+
checked: boolean;
|
|
9
|
+
/** Toggle handler. Omit for a read-only ring (shows state, not pressable). */
|
|
10
|
+
onChange?: (checked: boolean) => void;
|
|
11
|
+
/** Fill colour when checked. Default zinc-900 (pass a list/project colour to tint it). */
|
|
12
|
+
color?: string;
|
|
13
|
+
/** Diameter. Default 20. */
|
|
14
|
+
size?: number;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
accessibilityLabel: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A circular completion toggle — an empty ring that fills with a spring + a white
|
|
21
|
+
* check when checked (the iOS-Reminders / Linear "done" gesture), distinct from
|
|
22
|
+
* the square form `Checkbox`. The signature control for a task / to-do / checklist
|
|
23
|
+
* item; pair it with a title that strikes + mutes on the same `checked`. It stops
|
|
24
|
+
* the press, so it works inside a pressable row. Read-only (no `onChange`) shows
|
|
25
|
+
* the state without a tap.
|
|
26
|
+
*/
|
|
27
|
+
export function CheckCircle(props: CheckCircleProps) {
|
|
28
|
+
const { checked, onChange, color = colors.zinc[900], size = 20, disabled, accessibilityLabel } = props;
|
|
29
|
+
const v = useRef(new Animated.Value(checked ? 1 : 0)).current;
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
Animated.spring(v, { toValue: checked ? 1 : 0, useNativeDriver: false, friction: 6, tension: 160 }).start();
|
|
32
|
+
}, [checked, v]);
|
|
33
|
+
|
|
34
|
+
const face = (borderColor: string) => (
|
|
35
|
+
<View style={[styles.ring, { width: size, height: size, borderRadius: size / 2, borderColor }]}>
|
|
36
|
+
<Animated.View style={{ position: "absolute", width: size, height: size, borderRadius: size / 2, backgroundColor: color, transform: [{ scale: v }], opacity: v }} />
|
|
37
|
+
<Animated.View style={{ opacity: v, transform: [{ scale: v }] }}>
|
|
38
|
+
<Icon name="check" size={Math.round(size * 0.62)} color={colors.white} />
|
|
39
|
+
</Animated.View>
|
|
40
|
+
</View>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (!onChange) {
|
|
44
|
+
return (
|
|
45
|
+
<View accessibilityRole="image" accessibilityLabel={accessibilityLabel} style={[styles.tap, { width: size, height: size }, disabled ? styles.disabled : null]}>
|
|
46
|
+
{face(checked ? color : colors.zinc[300])}
|
|
47
|
+
</View>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const press = (e: GestureResponderEvent) => {
|
|
52
|
+
e?.stopPropagation?.();
|
|
53
|
+
if (!disabled) onChange(!checked);
|
|
54
|
+
};
|
|
55
|
+
return (
|
|
56
|
+
<FocusRingPressable
|
|
57
|
+
accessibilityRole="checkbox"
|
|
58
|
+
accessibilityState={{ checked, disabled }}
|
|
59
|
+
accessibilityLabel={accessibilityLabel}
|
|
60
|
+
disabled={disabled}
|
|
61
|
+
onPress={press}
|
|
62
|
+
hitSlop={10}
|
|
63
|
+
// The ring follows the element's border-radius — round it to the circle so
|
|
64
|
+
// the keyboard ring hugs the toggle instead of boxing it.
|
|
65
|
+
style={[styles.tap, { width: size, height: size, borderRadius: size / 2 }, disabled ? styles.disabled : null]}
|
|
66
|
+
>
|
|
67
|
+
{({ hovered }: { hovered?: boolean }) => face(checked ? color : hovered ? colors.zinc[500] : colors.zinc[300])}
|
|
68
|
+
</FocusRingPressable>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const styles = StyleSheet.create({
|
|
73
|
+
tap: {
|
|
74
|
+
alignItems: "center",
|
|
75
|
+
justifyContent: "center",
|
|
76
|
+
// @ts-ignore rn-web
|
|
77
|
+
cursor: "pointer",
|
|
78
|
+
},
|
|
79
|
+
disabled: { opacity: 0.4 },
|
|
80
|
+
ring: {
|
|
81
|
+
alignItems: "center",
|
|
82
|
+
justifyContent: "center",
|
|
83
|
+
borderWidth: 1.5,
|
|
84
|
+
// @ts-ignore rn-web
|
|
85
|
+
transitionProperty: "border-color",
|
|
86
|
+
transitionDuration: "0.12s",
|
|
87
|
+
},
|
|
88
|
+
});
|
package/src/checkbox_input.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
2
|
import { Pressable } from "react-native";
|
|
3
3
|
import { Checkbox } from "./checkbox";
|
|
4
|
+
import { FOCUS_RING } from "./control_surface";
|
|
5
|
+
import { useFocusRing } from "./use_focus_ring";
|
|
4
6
|
interface CheckboxInputProps {
|
|
5
7
|
/**
|
|
6
8
|
* Accessible name. Required because an unlabelled checkbox only announces
|
|
@@ -16,6 +18,7 @@ interface CheckboxInputProps {
|
|
|
16
18
|
|
|
17
19
|
export function CheckboxInput(props: CheckboxInputProps) {
|
|
18
20
|
const { accessibilityLabel, indeterminate, checked, onChange, disabled, testID } = props;
|
|
21
|
+
const { focusVisible, focusProps } = useFocusRing();
|
|
19
22
|
|
|
20
23
|
const handlePress = useCallback(() => {
|
|
21
24
|
if (!disabled) {
|
|
@@ -28,7 +31,8 @@ export function CheckboxInput(props: CheckboxInputProps) {
|
|
|
28
31
|
testID={testID}
|
|
29
32
|
onPress={handlePress}
|
|
30
33
|
disabled={disabled}
|
|
31
|
-
|
|
34
|
+
{...focusProps}
|
|
35
|
+
style={[{ opacity: disabled ? 0.5 : 1, borderRadius: 6 }, focusVisible && { boxShadow: FOCUS_RING }]}
|
|
32
36
|
// Use the W3C ARIA props directly: RN-web 0.21 no longer maps
|
|
33
37
|
// `accessibilityState={{ checked }}` to `aria-checked`, which dropped the
|
|
34
38
|
// checked state from the accessibility tree (a role="checkbox" with no
|
package/src/chip.tsx
CHANGED
|
@@ -20,7 +20,7 @@ export function Chip(props: ChipProps) {
|
|
|
20
20
|
<View ref={ref}>
|
|
21
21
|
{onPress ? (
|
|
22
22
|
<PressableHighlight
|
|
23
|
-
testID={testID}
|
|
23
|
+
focusRing testID={testID}
|
|
24
24
|
onPress={onPress}
|
|
25
25
|
style={(state) => [chipSurfaceStyle(state), styles.pillLayout, onDismiss && styles.pillWithDismiss]}
|
|
26
26
|
>
|
package/src/chip_group.tsx
CHANGED
|
@@ -46,7 +46,7 @@ export function ChipGroup<T extends string = string>(props: ChipGroupProps<T>) {
|
|
|
46
46
|
const active = option.value === value;
|
|
47
47
|
return (
|
|
48
48
|
<PressableHighlight
|
|
49
|
-
key={option.value}
|
|
49
|
+
focusRing key={option.value}
|
|
50
50
|
testID={option.testID}
|
|
51
51
|
onPress={() => onValueChange(option.value)}
|
|
52
52
|
accessibilityRole="button"
|
package/src/column_filter.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import { View, StyleSheet } from "react-native";
|
|
|
2
2
|
import { Text } from "./text";
|
|
3
3
|
import { TextInputField } from "./text_input_field";
|
|
4
4
|
import { NumberInput } from "./number_input";
|
|
5
|
-
import {
|
|
5
|
+
import { OptionList } from "./option_list";
|
|
6
6
|
import { FilterChip } from "./filter_chip";
|
|
7
7
|
import type { PickerOption } from "./picker";
|
|
8
8
|
|
|
@@ -143,7 +143,8 @@ export function ColumnFilter(props: ColumnFilterProps) {
|
|
|
143
143
|
/>
|
|
144
144
|
</View>
|
|
145
145
|
) : (
|
|
146
|
-
<
|
|
146
|
+
<OptionList
|
|
147
|
+
search={{ mode: "none" }}
|
|
147
148
|
multi
|
|
148
149
|
options={column.options ?? []}
|
|
149
150
|
value={value?.kind === "select" ? value.selected : []}
|