@colixsystems/widget-sdk 0.245.0 → 0.247.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 +60 -2
- package/dist/button-tokens.js +154 -0
- package/dist/button-view.js +251 -93
- package/dist/button.js +5 -3
- package/dist/button.native.js +6 -4
- package/dist/chart-logic.js +575 -0
- package/dist/chart-tokens.js +131 -0
- package/dist/chart-view.js +262 -0
- package/dist/chart.js +21 -0
- package/dist/chart.native.js +10 -0
- package/dist/contract.cjs +132 -35
- package/dist/contract.js +132 -35
- package/dist/detail-parts-view.js +31 -2
- package/dist/file-image-logic.js +162 -0
- package/dist/file-image-source.js +71 -0
- package/dist/file-image-view.js +119 -0
- package/dist/file-image.js +8 -0
- package/dist/file-image.native.js +8 -0
- package/dist/form-control-logic.js +23 -0
- package/dist/form-control-tokens.js +25 -10
- package/dist/grid-logic.js +222 -0
- package/dist/grid-view.js +109 -0
- package/dist/grid.js +7 -0
- package/dist/grid.native.js +7 -0
- package/dist/host.d.ts +6 -0
- package/dist/host.js +2 -0
- package/dist/index.d.ts +481 -20
- package/dist/index.js +14 -1
- package/dist/index.native.js +14 -1
- package/dist/menu-view.js +168 -0
- package/dist/overlay-tokens.js +50 -0
- package/dist/overlay-view.js +67 -24
- package/dist/overlay.native.js +3 -1
- package/dist/people-picker-logic.js +203 -0
- package/dist/people-picker-view.js +247 -0
- package/dist/people-picker.js +11 -0
- package/dist/people-picker.native.js +9 -0
- package/dist/popover-tokens.js +145 -0
- package/dist/popover-view.js +252 -0
- package/dist/popover.js +23 -0
- package/dist/popover.native.js +22 -0
- package/dist/radio-group-logic.js +61 -0
- package/dist/radio-group-tokens.js +62 -0
- package/dist/radio-group-view.js +262 -0
- package/dist/radio-group.js +12 -0
- package/dist/radio-group.native.js +7 -0
- package/dist/select-search-logic.js +150 -0
- package/dist/select-view.js +255 -43
- package/dist/select.js +6 -2
- package/dist/select.native.js +5 -3
- package/dist/tabs-tokens.js +59 -0
- package/dist/tabs-view.js +9 -43
- package/dist/tooltip-view.js +137 -0
- package/dist/web-modal.js +95 -23
- package/package.json +1 -1
- package/src/contract.js +132 -35
package/dist/select-view.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// `<Overlay>`, at screen level, so no card or scroll container the widget
|
|
7
7
|
// sits in can clip them; a long list scrolls inside the sheet. With
|
|
8
8
|
// `multiple` the same sheet ticks options into a draft that Done commits.
|
|
9
|
+
// Searchable, the sheet leads with a `<SearchField>`: the list filters where
|
|
10
|
+
// it is held, or each pause in typing asks `onSearch` / async `options`.
|
|
9
11
|
|
|
10
12
|
import React from "react";
|
|
11
13
|
import { useHostTheme } from "./hooks.js";
|
|
@@ -13,10 +15,9 @@ import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
|
|
|
13
15
|
import { Icon } from "./icon.js";
|
|
14
16
|
import { TEXT_FIELD_TOKEN_NAMES } from "./text-field-tokens.js";
|
|
15
17
|
import {
|
|
16
|
-
CONTROL_MIN_TARGET,
|
|
17
18
|
FIELD_OUTLINE_OFF,
|
|
18
19
|
fieldStateStyle,
|
|
19
|
-
|
|
20
|
+
optionRowStyle,
|
|
20
21
|
resolveFormControlTokens,
|
|
21
22
|
selectedOption,
|
|
22
23
|
} from "./form-control-tokens.js";
|
|
@@ -25,10 +26,11 @@ import {
|
|
|
25
26
|
hasNote,
|
|
26
27
|
isShown,
|
|
27
28
|
isText,
|
|
29
|
+
optionKeyDown,
|
|
28
30
|
shareRef,
|
|
29
|
-
spaceActivates,
|
|
30
31
|
spokenLabel,
|
|
31
32
|
} from "./form-control-logic.js";
|
|
33
|
+
import { popoverPanelStyle } from "./popover-tokens.js";
|
|
32
34
|
import { makeFieldFrame } from "./form-control-view.js";
|
|
33
35
|
import { checkboxBox } from "./checkbox-view.js";
|
|
34
36
|
import {
|
|
@@ -41,9 +43,20 @@ import {
|
|
|
41
43
|
triggerSummary,
|
|
42
44
|
valueList,
|
|
43
45
|
} from "./select-logic.js";
|
|
46
|
+
import {
|
|
47
|
+
DEFAULT_SEARCH_DEBOUNCE_MS,
|
|
48
|
+
createDebouncer,
|
|
49
|
+
createRequestGate,
|
|
50
|
+
filterOptions,
|
|
51
|
+
knownOption,
|
|
52
|
+
knownOptions,
|
|
53
|
+
normaliseQuery,
|
|
54
|
+
rememberOptions,
|
|
55
|
+
searchRowOverrides,
|
|
56
|
+
sheetBody,
|
|
57
|
+
} from "./select-search-logic.js";
|
|
44
58
|
|
|
45
59
|
const h = React.createElement;
|
|
46
|
-
const NAVIGATION_KEYS = ["ArrowDown", "ArrowUp", "Home", "End"];
|
|
47
60
|
// Wide enough that no picked chip shrinks while it is measured.
|
|
48
61
|
const MEASURE_SPAN = 10000;
|
|
49
62
|
// A compact chip that fits the field's own line, so the trigger keeps its height.
|
|
@@ -54,10 +67,12 @@ const UNMEASURED = Object.freeze({ available: null, needed: null });
|
|
|
54
67
|
* Bind the shared select to one platform's primitives.
|
|
55
68
|
*
|
|
56
69
|
* @param {{View: any, Text: any, Pressable: any, ScrollView: any, Platform: any,
|
|
57
|
-
* Overlay: any, Button: any, Chip: any}} rn
|
|
70
|
+
* Overlay: any, Button: any, Chip: any, SearchField?: any, Spinner?: any}} rn
|
|
71
|
+
* — `SearchField` and `Spinner` are needed only by a searchable select.
|
|
58
72
|
*/
|
|
59
73
|
export function makeSelect(rn) {
|
|
60
|
-
const { View, Text, Pressable, ScrollView, Platform, Overlay, Button, Chip } =
|
|
74
|
+
const { View, Text, Pressable, ScrollView, Platform, Overlay, Button, Chip, SearchField, Spinner } =
|
|
75
|
+
rn;
|
|
61
76
|
const FieldFrame = makeFieldFrame(rn);
|
|
62
77
|
|
|
63
78
|
// Keyboard focus is a web concern: a device moves its reader's focus into
|
|
@@ -67,14 +82,31 @@ export function makeSelect(rn) {
|
|
|
67
82
|
}
|
|
68
83
|
|
|
69
84
|
function OptionSheet(props) {
|
|
70
|
-
const {
|
|
71
|
-
|
|
85
|
+
const {
|
|
86
|
+
t,
|
|
87
|
+
options,
|
|
88
|
+
isPicked,
|
|
89
|
+
multiple,
|
|
90
|
+
title,
|
|
91
|
+
emptyText,
|
|
92
|
+
listId,
|
|
93
|
+
onPick,
|
|
94
|
+
testID,
|
|
95
|
+
footer,
|
|
96
|
+
search,
|
|
97
|
+
body = options.length === 0 ? "empty" : "list",
|
|
98
|
+
bodyText,
|
|
99
|
+
rowStyle,
|
|
100
|
+
} = props;
|
|
72
101
|
const nodes = React.useRef([]);
|
|
73
102
|
const initial = initialFocusIndex(options, isPicked);
|
|
74
103
|
// Focus lands on the current choice once, when the sheet mounts it; the
|
|
75
|
-
// browser scrolls a focused option into view in a long list.
|
|
76
|
-
|
|
104
|
+
// browser scrolls a focused option into view in a long list. A search
|
|
105
|
+
// field takes that first focus instead.
|
|
106
|
+
const pendingFocus = React.useRef(!search);
|
|
77
107
|
const text = { fontFamily: t.fontFamily, fontSize: t.fontSize, color: t.popover.textColor };
|
|
108
|
+
const note = { ...text, color: t.helperColor, paddingHorizontal: t.paddingHorizontal };
|
|
109
|
+
const focusFirst = () => focusNode(nodes.current[initialFocusIndex(options, () => false)]);
|
|
78
110
|
|
|
79
111
|
return h(
|
|
80
112
|
View,
|
|
@@ -86,11 +118,23 @@ export function makeSelect(rn) {
|
|
|
86
118
|
title,
|
|
87
119
|
)
|
|
88
120
|
: null,
|
|
89
|
-
|
|
121
|
+
search ? search(focusFirst) : null,
|
|
122
|
+
body === "loading"
|
|
123
|
+
? h(
|
|
124
|
+
View,
|
|
125
|
+
{ style: { paddingVertical: t.rowGap }, testID: testID ? `${testID}-loading` : undefined },
|
|
126
|
+
h(Spinner, { size: "sm", label: bodyText }),
|
|
127
|
+
)
|
|
128
|
+
: body !== "list"
|
|
90
129
|
? h(
|
|
91
130
|
Text,
|
|
92
|
-
{
|
|
93
|
-
|
|
131
|
+
{
|
|
132
|
+
style: body === "error" ? { ...note, color: t.errorColor } : note,
|
|
133
|
+
// Results arrive after typing; a status line says what came back.
|
|
134
|
+
...(search ? { role: "status", accessibilityLiveRegion: "polite" } : null),
|
|
135
|
+
testID: search && testID ? `${testID}-${body}` : undefined,
|
|
136
|
+
},
|
|
137
|
+
body === "empty" ? emptyText : bodyText,
|
|
94
138
|
)
|
|
95
139
|
: h(
|
|
96
140
|
ScrollView,
|
|
@@ -123,38 +167,39 @@ export function makeSelect(rn) {
|
|
|
123
167
|
}
|
|
124
168
|
},
|
|
125
169
|
onPress: pick,
|
|
126
|
-
onKeyDown: (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
spaceActivates(pick)(event);
|
|
133
|
-
},
|
|
170
|
+
onKeyDown: optionKeyDown({
|
|
171
|
+
options,
|
|
172
|
+
index,
|
|
173
|
+
focusAt: (next) => focusNode(nodes.current[next]),
|
|
174
|
+
activate: pick,
|
|
175
|
+
}),
|
|
134
176
|
disabled: option.disabled,
|
|
135
177
|
role: "option",
|
|
136
178
|
"aria-selected": selected,
|
|
137
179
|
"aria-disabled": !!option.disabled,
|
|
138
180
|
testID: option.testID,
|
|
139
181
|
style: ({ pressed }) => [
|
|
140
|
-
{
|
|
141
|
-
|
|
142
|
-
|
|
182
|
+
...optionRowStyle(t, {
|
|
183
|
+
pressed,
|
|
184
|
+
disabled: option.disabled,
|
|
143
185
|
justifyContent: multiple ? "flex-start" : "space-between",
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
paddingHorizontal: t.paddingHorizontal,
|
|
147
|
-
paddingVertical: t.paddingVertical,
|
|
148
|
-
borderRadius: t.radius,
|
|
149
|
-
},
|
|
150
|
-
pressed ? { opacity: 0.7 } : null,
|
|
151
|
-
option.disabled ? { opacity: t.disabledOpacity } : null,
|
|
186
|
+
}),
|
|
187
|
+
rowStyle,
|
|
152
188
|
],
|
|
153
189
|
},
|
|
154
190
|
multiple ? checkboxBox(View, t, { checked: selected }) : null,
|
|
191
|
+
option.leading ? h(View, { style: { flexShrink: 0 } }, option.leading) : null,
|
|
155
192
|
h(
|
|
156
193
|
Text,
|
|
157
|
-
{
|
|
194
|
+
{
|
|
195
|
+
style: [
|
|
196
|
+
text,
|
|
197
|
+
{ flexShrink: 1 },
|
|
198
|
+
// Keeps the check mark at the end once a leading node is drawn.
|
|
199
|
+
option.leading ? { flexGrow: 1 } : null,
|
|
200
|
+
selected ? { fontWeight: "600" } : null,
|
|
201
|
+
],
|
|
202
|
+
},
|
|
158
203
|
option.label,
|
|
159
204
|
),
|
|
160
205
|
selected && !multiple ? h(Icon, { name: "check", size: 18, color: t.accent }) : null,
|
|
@@ -241,6 +286,50 @@ export function makeSelect(rn) {
|
|
|
241
286
|
);
|
|
242
287
|
}
|
|
243
288
|
|
|
289
|
+
// The sheet's search field and what it shows instead of the list.
|
|
290
|
+
function searchSheetProps({ theme, search, count, loading, failed, texts, testID }) {
|
|
291
|
+
const body = sheetBody({ loading, error: failed, query: search.text, count });
|
|
292
|
+
return {
|
|
293
|
+
body,
|
|
294
|
+
bodyText:
|
|
295
|
+
body === "loading"
|
|
296
|
+
? texts.loadingText
|
|
297
|
+
: body === "error"
|
|
298
|
+
? texts.loadErrorText
|
|
299
|
+
: texts.noResultsText,
|
|
300
|
+
rowStyle: searchRowOverrides(theme),
|
|
301
|
+
search: (focusFirst) =>
|
|
302
|
+
h(SheetSearchField, {
|
|
303
|
+
value: search.text,
|
|
304
|
+
onChangeText: search.type,
|
|
305
|
+
placeholder: texts.searchPlaceholder,
|
|
306
|
+
onArrowDown: focusFirst,
|
|
307
|
+
testID: testID ? `${testID}-search` : undefined,
|
|
308
|
+
}),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function SheetSearchField({ value, onChangeText, placeholder, onArrowDown, testID }) {
|
|
313
|
+
const node = React.useRef(null);
|
|
314
|
+
React.useEffect(() => {
|
|
315
|
+
focusNode(node.current);
|
|
316
|
+
}, []);
|
|
317
|
+
return h(SearchField, {
|
|
318
|
+
ref: node,
|
|
319
|
+
value,
|
|
320
|
+
onChangeText,
|
|
321
|
+
placeholder,
|
|
322
|
+
// Down from the field reaches the first option, as in a combobox.
|
|
323
|
+
onKeyPress: (event) => {
|
|
324
|
+
const key = event && event.nativeEvent ? event.nativeEvent.key : undefined;
|
|
325
|
+
if (key !== "ArrowDown") return;
|
|
326
|
+
if (typeof event.preventDefault === "function") event.preventDefault();
|
|
327
|
+
onArrowDown();
|
|
328
|
+
},
|
|
329
|
+
testID,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
244
333
|
const Select = React.forwardRef(function Select(props, ref) {
|
|
245
334
|
const {
|
|
246
335
|
value,
|
|
@@ -265,6 +354,16 @@ export function makeSelect(rn) {
|
|
|
265
354
|
testID,
|
|
266
355
|
onFocus,
|
|
267
356
|
onBlur,
|
|
357
|
+
searchable: searchableProp = false,
|
|
358
|
+
onSearch,
|
|
359
|
+
searchDebounceMs = DEFAULT_SEARCH_DEBOUNCE_MS,
|
|
360
|
+
loading: loadingProp = false,
|
|
361
|
+
optionsError,
|
|
362
|
+
selectedOptions,
|
|
363
|
+
searchPlaceholder = "Search",
|
|
364
|
+
noResultsText = "No results",
|
|
365
|
+
loadingText = "Loading…",
|
|
366
|
+
loadErrorText = "Could not load the options",
|
|
268
367
|
...others
|
|
269
368
|
} = props;
|
|
270
369
|
const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
|
|
@@ -283,9 +382,27 @@ export function makeSelect(rn) {
|
|
|
283
382
|
const setRef = React.useMemo(() => shareRef(ref, triggerRef), [ref]);
|
|
284
383
|
const listId = React.useId();
|
|
285
384
|
const noteId = React.useId();
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
385
|
+
const search = useSearchState({
|
|
386
|
+
open,
|
|
387
|
+
options,
|
|
388
|
+
searchable: searchableProp,
|
|
389
|
+
onSearch,
|
|
390
|
+
debounceMs: searchDebounceMs,
|
|
391
|
+
});
|
|
392
|
+
const { searchable } = search;
|
|
393
|
+
const held = search.fetched
|
|
394
|
+
? search.fetched.options
|
|
395
|
+
: Array.isArray(options)
|
|
396
|
+
? options.filter(Boolean)
|
|
397
|
+
: [];
|
|
398
|
+
const list = search.filtersHeld ? filterOptions(held, search.text) : held;
|
|
399
|
+
// Searchable, a picked option keeps its label after a search drops it.
|
|
400
|
+
const known = React.useRef(new Map());
|
|
401
|
+
if (searchable) known.current = rememberOptions(known.current, selectedOptions, held);
|
|
402
|
+
const optionsFor = (own) =>
|
|
403
|
+
searchable ? knownOptions(known.current, own) : pickedOptions(list, own);
|
|
404
|
+
const current = searchable ? knownOption(known.current, value) : selectedOption(list, value);
|
|
405
|
+
const picked = multiple ? optionsFor(values) : [];
|
|
289
406
|
const draft = ownDraft === null ? valueList(values) : ownDraft;
|
|
290
407
|
const invalid = isShown(error);
|
|
291
408
|
const sheetTitle = isShown(title) ? title : isText(label) ? label : undefined;
|
|
@@ -307,7 +424,7 @@ export function makeSelect(rn) {
|
|
|
307
424
|
const next = committedValues(list, draft);
|
|
308
425
|
close();
|
|
309
426
|
if (!sameValues(next, valueList(values)) && typeof onValuesChange === "function") {
|
|
310
|
-
onValuesChange(next,
|
|
427
|
+
onValuesChange(next, optionsFor(next));
|
|
311
428
|
}
|
|
312
429
|
};
|
|
313
430
|
const pick = (option) => {
|
|
@@ -417,12 +534,7 @@ export function makeSelect(rn) {
|
|
|
417
534
|
size: "sm",
|
|
418
535
|
accessibilityLabel: isText(sheetTitle) ? String(sheetTitle) : name,
|
|
419
536
|
...(closeAccessibilityLabel ? { closeAccessibilityLabel } : null),
|
|
420
|
-
style: {
|
|
421
|
-
backgroundColor: t.popover.background,
|
|
422
|
-
borderWidth: 1,
|
|
423
|
-
borderColor: t.popover.borderColor,
|
|
424
|
-
padding: t.rowGap,
|
|
425
|
-
},
|
|
537
|
+
style: { ...popoverPanelStyle(t.popover, "dialog"), padding: t.rowGap },
|
|
426
538
|
},
|
|
427
539
|
h(OptionSheet, {
|
|
428
540
|
t,
|
|
@@ -437,6 +549,23 @@ export function makeSelect(rn) {
|
|
|
437
549
|
testID,
|
|
438
550
|
onPick: pick,
|
|
439
551
|
footer: multiple ? h(DoneFooter, { doneText, onDone: done, testID }) : null,
|
|
552
|
+
...(searchable
|
|
553
|
+
? searchSheetProps({
|
|
554
|
+
theme,
|
|
555
|
+
search,
|
|
556
|
+
count: list.length,
|
|
557
|
+
loading:
|
|
558
|
+
search.pending || (search.fetched ? search.fetched.loading : !!loadingProp),
|
|
559
|
+
failed: search.fetched ? search.fetched.error : isShown(optionsError),
|
|
560
|
+
texts: {
|
|
561
|
+
searchPlaceholder,
|
|
562
|
+
noResultsText,
|
|
563
|
+
loadingText,
|
|
564
|
+
loadErrorText: typeof optionsError === "string" ? optionsError : loadErrorText,
|
|
565
|
+
},
|
|
566
|
+
testID,
|
|
567
|
+
})
|
|
568
|
+
: null),
|
|
440
569
|
}),
|
|
441
570
|
),
|
|
442
571
|
);
|
|
@@ -445,3 +574,86 @@ export function makeSelect(rn) {
|
|
|
445
574
|
Select.displayName = "Select";
|
|
446
575
|
return Select;
|
|
447
576
|
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* A searchable select's query: the text typed into the sheet, sent after a
|
|
580
|
+
* pause to `onSearch` or to async `options`, whose latest answer it keeps.
|
|
581
|
+
* Opening the sheet starts from an empty query; a plain select is inert here.
|
|
582
|
+
*/
|
|
583
|
+
function useSearchState({ open, options, searchable, onSearch, debounceMs }) {
|
|
584
|
+
const asyncOptions = typeof options === "function";
|
|
585
|
+
const remote = typeof onSearch === "function" || asyncOptions;
|
|
586
|
+
const enabled = !!searchable || remote;
|
|
587
|
+
const [text, setText] = React.useState("");
|
|
588
|
+
// The query last handed on; typed text that differs is still pending.
|
|
589
|
+
const [sentQuery, setSentQuery] = React.useState("");
|
|
590
|
+
const [fetched, setFetched] = React.useState({ options: [], loading: false, error: false });
|
|
591
|
+
const latest = React.useRef({ onSearch, options });
|
|
592
|
+
latest.current = { onSearch, options };
|
|
593
|
+
const gate = React.useRef(null);
|
|
594
|
+
if (gate.current === null) gate.current = createRequestGate();
|
|
595
|
+
const mounted = React.useRef(false);
|
|
596
|
+
// Set in the body too: StrictMode runs the cleanup, then the effect again.
|
|
597
|
+
React.useEffect(() => {
|
|
598
|
+
mounted.current = true;
|
|
599
|
+
return () => {
|
|
600
|
+
mounted.current = false;
|
|
601
|
+
};
|
|
602
|
+
}, []);
|
|
603
|
+
|
|
604
|
+
const send = React.useCallback((query) => {
|
|
605
|
+
setSentQuery(query);
|
|
606
|
+
const own = latest.current;
|
|
607
|
+
if (typeof own.onSearch === "function") own.onSearch(query);
|
|
608
|
+
if (typeof own.options !== "function") return;
|
|
609
|
+
const ticket = gate.current.begin();
|
|
610
|
+
setFetched((state) => ({ ...state, loading: true, error: false }));
|
|
611
|
+
Promise.resolve()
|
|
612
|
+
.then(() => own.options(query))
|
|
613
|
+
.then(
|
|
614
|
+
(rows) => {
|
|
615
|
+
if (!mounted.current || !gate.current.isCurrent(ticket)) return;
|
|
616
|
+
const list = Array.isArray(rows) ? rows.filter(Boolean) : [];
|
|
617
|
+
setFetched({ options: list, loading: false, error: false });
|
|
618
|
+
},
|
|
619
|
+
() => {
|
|
620
|
+
if (!mounted.current || !gate.current.isCurrent(ticket)) return;
|
|
621
|
+
setFetched({ options: [], loading: false, error: true });
|
|
622
|
+
},
|
|
623
|
+
);
|
|
624
|
+
}, []);
|
|
625
|
+
|
|
626
|
+
const delay = React.useRef(debounceMs);
|
|
627
|
+
delay.current = debounceMs;
|
|
628
|
+
const debouncer = React.useRef(null);
|
|
629
|
+
if (debouncer.current === null) debouncer.current = createDebouncer(() => delay.current, send);
|
|
630
|
+
React.useEffect(() => () => debouncer.current.cancel(), []);
|
|
631
|
+
|
|
632
|
+
// Each opening starts from an empty query, asked for at once; it is
|
|
633
|
+
// cleared on closing so the next opening never draws the old filter.
|
|
634
|
+
React.useEffect(() => {
|
|
635
|
+
if (!enabled) return;
|
|
636
|
+
if (!open) {
|
|
637
|
+
debouncer.current.cancel();
|
|
638
|
+
setText("");
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (remote) send("");
|
|
642
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
643
|
+
}, [open, enabled, remote]);
|
|
644
|
+
|
|
645
|
+
const type = (next) => {
|
|
646
|
+
const own = typeof next === "string" ? next : "";
|
|
647
|
+
setText(own);
|
|
648
|
+
if (remote) debouncer.current.push(own);
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
return {
|
|
652
|
+
searchable: enabled,
|
|
653
|
+
filtersHeld: enabled && !remote,
|
|
654
|
+
text: enabled ? text : "",
|
|
655
|
+
pending: remote && normaliseQuery(text) !== sentQuery,
|
|
656
|
+
type,
|
|
657
|
+
fetched: asyncOptions ? fetched : null,
|
|
658
|
+
};
|
|
659
|
+
}
|
package/dist/select.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// `<Select>` (web). Binds ./select-view.js to react-native-web and the web
|
|
2
|
-
// `<Overlay>`, `<Button>` and `<
|
|
3
|
-
// bare specifier.
|
|
2
|
+
// `<Overlay>`, `<Button>`, `<Chip>`, `<SearchField>` and `<Spinner>`; see
|
|
3
|
+
// ./primitives.js for why not the bare specifier.
|
|
4
4
|
|
|
5
5
|
import * as ReactNative from "react-native-web";
|
|
6
6
|
import { makeSelect } from "./select-view.js";
|
|
7
7
|
import { Overlay } from "./overlay.js";
|
|
8
8
|
import { Button } from "./button.js";
|
|
9
9
|
import { Chip } from "./chip.js";
|
|
10
|
+
import { SearchField } from "./search-field.js";
|
|
11
|
+
import { Spinner } from "./loading.js";
|
|
10
12
|
|
|
11
13
|
export const Select = makeSelect({
|
|
12
14
|
View: ReactNative.View,
|
|
@@ -17,4 +19,6 @@ export const Select = makeSelect({
|
|
|
17
19
|
Overlay,
|
|
18
20
|
Button,
|
|
19
21
|
Chip,
|
|
22
|
+
SearchField,
|
|
23
|
+
Spinner,
|
|
20
24
|
});
|
package/dist/select.native.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// `<Select>` (native). Binds ./select-view.js to React Native's own
|
|
2
|
-
// primitives and the native `<Overlay>`, `<Button
|
|
3
|
-
// imports differ from ./select.js.
|
|
2
|
+
// primitives and the native `<Overlay>`, `<Button>`, `<Chip>`,
|
|
3
|
+
// `<SearchField>` and `<Spinner>`; only the imports differ from ./select.js.
|
|
4
4
|
|
|
5
5
|
import { View, Text, Pressable, ScrollView, Platform } from "react-native";
|
|
6
6
|
import { makeSelect } from "./select-view.js";
|
|
7
7
|
import { Overlay } from "./overlay.native.js";
|
|
8
8
|
import { Button } from "./button.native.js";
|
|
9
9
|
import { Chip } from "./chip.native.js";
|
|
10
|
+
import { SearchField } from "./search-field.native.js";
|
|
11
|
+
import { Spinner } from "./loading.native.js";
|
|
10
12
|
|
|
11
|
-
export const Select = makeSelect({ View, Text, Pressable, ScrollView, Platform, Overlay, Button, Chip });
|
|
13
|
+
export const Select = makeSelect({ View, Text, Pressable, ScrollView, Platform, Overlay, Button, Chip, SearchField, Spinner });
|
package/dist/tabs-tokens.js
CHANGED
|
@@ -67,3 +67,62 @@ export function resolveTabsTokens(theme, overrides) {
|
|
|
67
67
|
rule: colors.border,
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
|
+
|
|
71
|
+
const SEGMENT_INSET = 4;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The segmented strip's surfaces: the muted bar, its inset and each segment.
|
|
75
|
+
* `<Tabs>` and a segmented `<RadioGroup>` both draw from it, so they match.
|
|
76
|
+
* A concentric outer radius keeps the inset chip's corners parallel, and the
|
|
77
|
+
* active segment is outlined as well as lifted: a shadow alone disappears on
|
|
78
|
+
* a dark theme.
|
|
79
|
+
*
|
|
80
|
+
* @param {object} t — resolved tab tokens.
|
|
81
|
+
* @param {object} theme — the resolved workspace theme.
|
|
82
|
+
*/
|
|
83
|
+
export function segmentedStripStyles(t, theme) {
|
|
84
|
+
const elevation = (theme && theme.elevation) || DEFAULT_THEME_TOKENS.elevation;
|
|
85
|
+
return {
|
|
86
|
+
bar: {
|
|
87
|
+
backgroundColor: t.barBackground,
|
|
88
|
+
borderRadius: t.radius + 2,
|
|
89
|
+
alignSelf: "flex-start",
|
|
90
|
+
maxWidth: "100%",
|
|
91
|
+
},
|
|
92
|
+
content: { padding: SEGMENT_INSET, gap: SEGMENT_INSET },
|
|
93
|
+
item: (active) => ({
|
|
94
|
+
borderRadius: t.radius,
|
|
95
|
+
borderWidth: 1,
|
|
96
|
+
borderColor: active ? t.rule : "transparent",
|
|
97
|
+
...(active ? { backgroundColor: t.activeSurface, ...(elevation.sm || {}) } : null),
|
|
98
|
+
}),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** One tab or segment's box, before its style's own surface. */
|
|
103
|
+
export const TAB_ITEM_LAYOUT = Object.freeze({
|
|
104
|
+
flexDirection: "row",
|
|
105
|
+
alignItems: "center",
|
|
106
|
+
justifyContent: "center",
|
|
107
|
+
gap: 6,
|
|
108
|
+
minHeight: 44,
|
|
109
|
+
paddingHorizontal: 12,
|
|
110
|
+
paddingVertical: 8,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A tab or segment's label.
|
|
115
|
+
*
|
|
116
|
+
* @param {object} t — resolved tab tokens.
|
|
117
|
+
* @param {object} theme — the resolved workspace theme.
|
|
118
|
+
* @param {boolean} active
|
|
119
|
+
*/
|
|
120
|
+
export function tabLabelStyle(t, theme, active) {
|
|
121
|
+
const typography = (theme && theme.typography) || DEFAULT_THEME_TOKENS.typography;
|
|
122
|
+
return {
|
|
123
|
+
fontFamily: typography.fontFamily,
|
|
124
|
+
fontSize: typography.sizes.sm,
|
|
125
|
+
fontWeight: active ? "600" : "500",
|
|
126
|
+
color: active ? t.activeTextColor : t.textColor,
|
|
127
|
+
};
|
|
128
|
+
}
|
package/dist/tabs-view.js
CHANGED
|
@@ -10,13 +10,15 @@ import React from "react";
|
|
|
10
10
|
import { useHostTheme } from "./hooks.js";
|
|
11
11
|
import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
|
|
12
12
|
import { Icon } from "./icon.js";
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
import {
|
|
14
|
+
TAB_ITEM_LAYOUT,
|
|
15
|
+
resolveTabsTokens,
|
|
16
|
+
segmentedStripStyles,
|
|
17
|
+
tabLabelStyle,
|
|
18
|
+
} from "./tabs-tokens.js";
|
|
16
19
|
|
|
17
20
|
function stripStyles(t, theme) {
|
|
18
21
|
const spacing = theme.spacing || DEFAULT_THEME_TOKENS.spacing;
|
|
19
|
-
const elevation = theme.elevation || DEFAULT_THEME_TOKENS.elevation;
|
|
20
22
|
if (t.style === "pill") {
|
|
21
23
|
// An idle chip is outlined: a muted fill vanishes on a white card.
|
|
22
24
|
return {
|
|
@@ -42,26 +44,7 @@ function stripStyles(t, theme) {
|
|
|
42
44
|
}),
|
|
43
45
|
};
|
|
44
46
|
}
|
|
45
|
-
|
|
46
|
-
// bar hugs its segments, and the active one is outlined as well as lifted
|
|
47
|
-
// because a shadow alone disappears on a dark theme.
|
|
48
|
-
return {
|
|
49
|
-
bar: {
|
|
50
|
-
backgroundColor: t.barBackground,
|
|
51
|
-
borderRadius: t.radius + 2,
|
|
52
|
-
alignSelf: "flex-start",
|
|
53
|
-
maxWidth: "100%",
|
|
54
|
-
},
|
|
55
|
-
content: { padding: SEGMENT_INSET, gap: SEGMENT_INSET },
|
|
56
|
-
item: (active) => ({
|
|
57
|
-
borderRadius: t.radius,
|
|
58
|
-
borderWidth: 1,
|
|
59
|
-
borderColor: active ? t.rule : "transparent",
|
|
60
|
-
...(active
|
|
61
|
-
? { backgroundColor: t.activeSurface, ...(elevation.sm || {}) }
|
|
62
|
-
: null),
|
|
63
|
-
}),
|
|
64
|
-
};
|
|
47
|
+
return segmentedStripStyles(t, theme);
|
|
65
48
|
}
|
|
66
49
|
|
|
67
50
|
/**
|
|
@@ -98,7 +81,6 @@ export function makeTabs(rn) {
|
|
|
98
81
|
textColor,
|
|
99
82
|
radius,
|
|
100
83
|
});
|
|
101
|
-
const typography = theme.typography || DEFAULT_THEME_TOKENS.typography;
|
|
102
84
|
const strip = stripStyles(t, theme);
|
|
103
85
|
|
|
104
86
|
return React.createElement(
|
|
@@ -130,15 +112,7 @@ export function makeTabs(rn) {
|
|
|
130
112
|
"aria-disabled": !!item.disabled,
|
|
131
113
|
testID: item.testID,
|
|
132
114
|
style: [
|
|
133
|
-
|
|
134
|
-
flexDirection: "row",
|
|
135
|
-
alignItems: "center",
|
|
136
|
-
justifyContent: "center",
|
|
137
|
-
gap: 6,
|
|
138
|
-
minHeight: 44,
|
|
139
|
-
paddingHorizontal: 12,
|
|
140
|
-
paddingVertical: 8,
|
|
141
|
-
},
|
|
115
|
+
TAB_ITEM_LAYOUT,
|
|
142
116
|
strip.item(active),
|
|
143
117
|
item.disabled ? { opacity: 0.5 } : null,
|
|
144
118
|
],
|
|
@@ -148,15 +122,7 @@ export function makeTabs(rn) {
|
|
|
148
122
|
: null,
|
|
149
123
|
React.createElement(
|
|
150
124
|
Text,
|
|
151
|
-
{
|
|
152
|
-
numberOfLines: 1,
|
|
153
|
-
style: {
|
|
154
|
-
fontFamily: typography.fontFamily,
|
|
155
|
-
fontSize: typography.sizes.sm,
|
|
156
|
-
fontWeight: active ? "600" : "500",
|
|
157
|
-
color: ink,
|
|
158
|
-
},
|
|
159
|
-
},
|
|
125
|
+
{ numberOfLines: 1, style: tabLabelStyle(t, theme, active) },
|
|
160
126
|
item.label,
|
|
161
127
|
),
|
|
162
128
|
);
|