@lotics/ui 44.12.0 → 44.13.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/docs/catalog.md +13 -2
- package/package.json +1 -1
- package/src/filter_chip.tsx +6 -2
- package/src/locale.tsx +3 -3
- package/src/option_list.tsx +25 -3
- package/src/sort_header.tsx +14 -2
package/docs/catalog.md
CHANGED
|
@@ -959,6 +959,11 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
959
959
|
It opens SCROLLED to its seat (the selected row in `search.mode:"none"`, else the first real
|
|
960
960
|
option), which only shows on a list longer than its own box and is the difference between a
|
|
961
961
|
96-row list opening on your value and opening at the top.
|
|
962
|
+
`indeterminateValues` (multi) draws a MIXED tick on values only SOME of the things being
|
|
963
|
+
edited carry — the shape for labelling a selection, where an empty box would say "none of
|
|
964
|
+
these" and be wrong. Presentational and one-directional: a mixed row is not in `value`, so
|
|
965
|
+
pressing it selects (the caller applies to all), and it announces itself in the row label
|
|
966
|
+
because `role="option"` has no ARIA state for a third value.
|
|
962
967
|
- **`combobox`** — COMPOUND single-select editable search: `Combobox` root +
|
|
963
968
|
`ComboboxInput` + `ComboboxContent`, optional `ComboboxEmpty`/`ComboboxFooter`,
|
|
964
969
|
`useCombobox()`; over the shared option-list engine; browses on focus; no `multi` —
|
|
@@ -1485,7 +1490,11 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1485
1490
|
- **`table` › `TableGroup`** — a titled BAND of rows inside a `Table` (`label`, `count?`, `color?` for a single valence dot). Turns a register that presents records into one that triages them: group by what implies a different ACTION, order the bands by what needs the reader now, and let air rather than a rule separate them. → [composition.md](./composition.md) §"A register that TRIAGES".
|
|
1486
1491
|
- **`sort_header`** — `SortHeader` + `SortState`/`SortDir` + `cycleSort` + `sortBy` +
|
|
1487
1492
|
`SortHeaderLabels`: the sortable column header and the sort-state helpers `Table`/
|
|
1488
|
-
`DataGrid` consumers drive.
|
|
1493
|
+
`DataGrid` consumers drive. `sortBy` copies rather than mutates, and compares strings with
|
|
1494
|
+
`localeCompare` (case- and accent-folded, `numeric`) rather than `<`/`>` — code-point order
|
|
1495
|
+
exiles every accented letter past the whole unaccented alphabet, so a Vietnamese register
|
|
1496
|
+
reads correctly until its first Đ and looks fine to a reviewer forever. `getValue` returns a
|
|
1497
|
+
NUMBER for anything ordered numerically (a timestamp, a count).
|
|
1489
1498
|
- **`data_grid`** — `DataGrid` + `gridRowStyle`: the inline-managed grouped table (see
|
|
1490
1499
|
[Tabular data](#tabular-data--pick-by-scale--intent)); `labels` localizes the sort-header
|
|
1491
1500
|
a11y via `SortHeaderLabels`.
|
|
@@ -1501,7 +1510,9 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1501
1510
|
on the pill is the clear, and the editor inside brings whatever actions it has, so a
|
|
1502
1511
|
multi-select's "select all / deselect all" is the only rule in the panel. `footer` is for an
|
|
1503
1512
|
editor that must COMMIT (Cancel / Save); reach for it only then, since it adds a second band
|
|
1504
|
-
under an editor that already has one.
|
|
1513
|
+
under an editor that already has one. `testID` names the pill: a register's bulk verbs and
|
|
1514
|
+
its facets routinely share a word ("Tags" is a filter AND a selection action), so a driver
|
|
1515
|
+
reaching by visible name gets whichever one it finds.
|
|
1505
1516
|
- **`summary_line`** — `SummaryLine`: the light inline summary of a register/list's FILTERED
|
|
1506
1517
|
view, sits below the toolbar; NOT the boxed dashboard `kpi_strip` band. Every item is an
|
|
1507
1518
|
AGGREGATE over the rows in view (a count, a sum, a fill), and the strip goes with the set it
|
package/package.json
CHANGED
package/src/filter_chip.tsx
CHANGED
|
@@ -42,6 +42,10 @@ export interface FilterChipProps {
|
|
|
42
42
|
* editor that already carries bottom actions of its own gets a second
|
|
43
43
|
* bordered band from this. */
|
|
44
44
|
footer?: ReactNode;
|
|
45
|
+
/** Names the TRIGGER, so a driver reaches this facet rather than whichever
|
|
46
|
+
* other control on the band happens to share its word — a register's bulk
|
|
47
|
+
* verbs and its filters routinely do ("Tags" is both). */
|
|
48
|
+
testID?: string;
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
/**
|
|
@@ -76,7 +80,7 @@ export function selectSummary(
|
|
|
76
80
|
* toolbar's `ColumnFilter` is this pill plus its query-condition mapping.
|
|
77
81
|
*/
|
|
78
82
|
export function FilterChip(props: FilterChipProps) {
|
|
79
|
-
const { label, summary, onClear, children, side = "bottom", align = "start", open, onOpenChange, footer } = props;
|
|
83
|
+
const { label, summary, onClear, children, side = "bottom", align = "start", open, onOpenChange, footer, testID } = props;
|
|
80
84
|
const clearLabel = props.clearLabel ?? useLoticsLocale().filterChip.clear;
|
|
81
85
|
const active = summary != null && (typeof summary !== "string" || summary.length > 0);
|
|
82
86
|
// THE × IS THE CLEAR, and it is the only one. A valued, non-clearable pill
|
|
@@ -110,7 +114,7 @@ export function FilterChip(props: FilterChipProps) {
|
|
|
110
114
|
return (
|
|
111
115
|
<Popover side={side} align={align} open={isOpen} onOpenChange={setOpen}>
|
|
112
116
|
<PopoverTrigger>
|
|
113
|
-
<Chip onDismiss={showClear ? onClear : undefined} dismissTooltip={clearLabel}>
|
|
117
|
+
<Chip testID={testID} onDismiss={showClear ? onClear : undefined} dismissTooltip={clearLabel}>
|
|
114
118
|
{active && typeof summary !== "string" ? (
|
|
115
119
|
<View style={styles.summaryRow}>
|
|
116
120
|
{/* This branch only renders when ACTIVE, so it takes the accent
|
package/src/locale.tsx
CHANGED
|
@@ -47,7 +47,7 @@ export interface LoticsLocale {
|
|
|
47
47
|
/** `OptionList` (and everything built on it — `Select`, `Combobox`, the
|
|
48
48
|
* in-cell editors): the select-all/deselect-all links, the empty state, the
|
|
49
49
|
* internal search-field placeholder, and the `Combobox` recents header. */
|
|
50
|
-
optionList: { selectAll: string; deselectAll: string; clear: string; noResults: string; recent: string; searchPlaceholder: string };
|
|
50
|
+
optionList: { selectAll: string; deselectAll: string; clear: string; noResults: string; recent: string; searchPlaceholder: string; someSelected: string };
|
|
51
51
|
/** `Picker`'s empty option, when the caller declares empty a CHOICE
|
|
52
52
|
* (`includeEmptyOption`). A value, not an action — hence "None" rather than
|
|
53
53
|
* the option list's "Clear": in a native `<select>` the reader picks it the
|
|
@@ -298,7 +298,7 @@ export const en: LoticsLocale = {
|
|
|
298
298
|
descending: ", descending",
|
|
299
299
|
},
|
|
300
300
|
referenceField: { open: "Open", change: "Change", clear: "Clear", edit: "Edit", save: "Save", saving: "Saving…", cancel: "Cancel" },
|
|
301
|
-
optionList: { selectAll: "Select all", deselectAll: "Deselect all", clear: "Clear", noResults: "No results", recent: "Recent", searchPlaceholder: "Search…" },
|
|
301
|
+
optionList: { selectAll: "Select all", deselectAll: "Deselect all", clear: "Clear", noResults: "No results", recent: "Recent", searchPlaceholder: "Search…", someSelected: "some selected" },
|
|
302
302
|
picker: { emptyOption: "None" },
|
|
303
303
|
datePicker: { today: "Today", now: "Now", clear: "Clear", done: "Done", openCalendar: "Open calendar", chooseTime: "Choose a time", time: "Time", startTime: "Start time", endTime: "End time", startDate: "Start date", endDate: "End date", addTime: "Add time", removeTime: "Remove time", year: "Year", month: "Month", day: "Day", hour: "Hour", minute: "Minute", dayPeriod: "AM/PM", invalidDate: "Enter a complete date", invalidTime: "Enter a complete time" },
|
|
304
304
|
calendar: { previousMonth: "Previous month", nextMonth: "Next month" },
|
|
@@ -485,7 +485,7 @@ export const vi: LoticsLocale = {
|
|
|
485
485
|
descending: " (giảm dần)",
|
|
486
486
|
},
|
|
487
487
|
referenceField: { open: "Mở", change: "Đổi", clear: "Bỏ chọn", edit: "Sửa", save: "Lưu", saving: "Đang lưu…", cancel: "Huỷ" },
|
|
488
|
-
optionList: { selectAll: "Chọn tất cả", deselectAll: "Bỏ chọn tất cả", clear: "Xóa", noResults: "Không có kết quả", recent: "Gần đây", searchPlaceholder: "Tìm…" },
|
|
488
|
+
optionList: { selectAll: "Chọn tất cả", deselectAll: "Bỏ chọn tất cả", clear: "Xóa", noResults: "Không có kết quả", recent: "Gần đây", searchPlaceholder: "Tìm…", someSelected: "một số đã chọn" },
|
|
489
489
|
picker: { emptyOption: "Không có" },
|
|
490
490
|
datePicker: { today: "Hôm nay", now: "Bây giờ", clear: "Xóa", done: "Xong", openCalendar: "Mở lịch", chooseTime: "Chọn giờ", time: "Giờ", startTime: "Giờ bắt đầu", endTime: "Giờ kết thúc", startDate: "Ngày bắt đầu", endDate: "Ngày kết thúc", addTime: "Thêm giờ", removeTime: "Bỏ giờ", year: "Năm", month: "Tháng", day: "Ngày", hour: "Giờ", minute: "Phút", dayPeriod: "SA/CH", invalidDate: "Nhập ngày đầy đủ", invalidTime: "Nhập giờ đầy đủ" },
|
|
491
491
|
calendar: { previousMonth: "Tháng trước", nextMonth: "Tháng sau" },
|
package/src/option_list.tsx
CHANGED
|
@@ -23,6 +23,17 @@ export interface OptionListProps<T extends string = string, MULTI extends boolea
|
|
|
23
23
|
getOptionDescription?: (option: PickerOption<T, D>) => string | undefined;
|
|
24
24
|
loading?: boolean;
|
|
25
25
|
emptyText?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Multi only: values that SOME of the things being edited carry, drawn as a
|
|
28
|
+
* mixed tick rather than an empty one.
|
|
29
|
+
*
|
|
30
|
+
* What it is for is editing a SET at once — five documents, three of which
|
|
31
|
+
* carry a label. An empty box there says "none of them", which is false, and
|
|
32
|
+
* the reader who ticks it to fix that cannot tell afterwards what they
|
|
33
|
+
* changed. Purely presentational: a mixed row is not in `value`, so pressing
|
|
34
|
+
* it selects (adds to all) and the caller decides what that means.
|
|
35
|
+
*/
|
|
36
|
+
indeterminateValues?: readonly T[];
|
|
26
37
|
accessibilityLabel?: string;
|
|
27
38
|
selectAllLabel?: string;
|
|
28
39
|
deselectAllLabel?: string;
|
|
@@ -45,7 +56,7 @@ export interface OptionListProps<T extends string = string, MULTI extends boolea
|
|
|
45
56
|
export function OptionList<T extends string, MULTI extends boolean = false, D = unknown>(
|
|
46
57
|
props: OptionListProps<T, MULTI, D>,
|
|
47
58
|
) {
|
|
48
|
-
const { testID, renderOptionContent, getOptionDescription, loading = false, accessibilityLabel, search, onClear } = props;
|
|
59
|
+
const { testID, renderOptionContent, getOptionDescription, loading = false, accessibilityLabel, search, onClear, indeterminateValues } = props;
|
|
49
60
|
const loc = useLoticsLocale().optionList;
|
|
50
61
|
const emptyText = props.emptyText ?? loc.noResults;
|
|
51
62
|
const selectAllLabel = props.selectAllLabel ?? loc.selectAll;
|
|
@@ -122,6 +133,11 @@ export function OptionList<T extends string, MULTI extends boolean = false, D =
|
|
|
122
133
|
list.rows.map((row) => {
|
|
123
134
|
const isCustom = row.kind === "custom";
|
|
124
135
|
const opt = row.option;
|
|
136
|
+
// A mixed row is PAINTED mixed, and `role="option"` has no ARIA state
|
|
137
|
+
// for it — `aria-checked="mixed"` belongs to checkbox roles, and this
|
|
138
|
+
// row carries `aria-selected`. So the third state is said in the
|
|
139
|
+
// label, or it exists for sighted readers only.
|
|
140
|
+
const mixed = props.multi && !row.selected && indeterminateValues?.includes(opt.value) === true;
|
|
125
141
|
const desc = row.kind === "option" ? getOptionDescription?.(opt) : undefined;
|
|
126
142
|
const label = opt.label ?? opt.value;
|
|
127
143
|
const content = row.kind === "option" ? renderOptionContent?.(opt) : undefined;
|
|
@@ -129,11 +145,17 @@ export function OptionList<T extends string, MULTI extends boolean = false, D =
|
|
|
129
145
|
nativeID: row.nativeID,
|
|
130
146
|
testID: isCustom ? "option-list-custom" : opt.testID || `picker-option-${opt.value}`,
|
|
131
147
|
role: "option" as const,
|
|
132
|
-
accessibilityLabel: isCustom
|
|
148
|
+
accessibilityLabel: isCustom
|
|
149
|
+
? label
|
|
150
|
+
: mixed
|
|
151
|
+
? `${opt.label ?? String(opt.value)}, ${loc.someSelected}`
|
|
152
|
+
: (opt.label ?? String(opt.value)),
|
|
133
153
|
icon: isCustom ? (
|
|
134
154
|
<Icon name="plus" size={16} color={colors.zinc["600"]} />
|
|
135
155
|
) : props.multi && row.kind === "option" ? (
|
|
136
|
-
|
|
156
|
+
// A row already in `value` is fully selected — "some carry it"
|
|
157
|
+
// cannot also be "all carry it", so selection wins.
|
|
158
|
+
<Checkbox checked={row.selected} indeterminate={mixed} />
|
|
137
159
|
) : undefined,
|
|
138
160
|
right:
|
|
139
161
|
!props.multi && row.selected ? (
|
package/src/sort_header.tsx
CHANGED
|
@@ -23,8 +23,13 @@ export function cycleSort(current: SortState | null, key: string): SortState | n
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Order a COPY of `items` by the active column. `getValue` maps (item, key) to a
|
|
26
|
-
* comparable
|
|
27
|
-
*
|
|
26
|
+
* comparable. Returns `items` unchanged when nothing is sorted.
|
|
27
|
+
*
|
|
28
|
+
* Strings are compared with `localeCompare`, NOT `<`/`>`. Those operators order
|
|
29
|
+
* by code point, which puts every accented letter after the whole unaccented
|
|
30
|
+
* alphabet — so a Vietnamese register sorted by name reads correctly until the
|
|
31
|
+
* first Đ, and then does not. Numbers keep numeric order; a mixed pair falls
|
|
32
|
+
* back to the operators, which is the only defined thing left to do.
|
|
28
33
|
*/
|
|
29
34
|
export function sortBy<T>(
|
|
30
35
|
items: T[],
|
|
@@ -36,6 +41,13 @@ export function sortBy<T>(
|
|
|
36
41
|
return [...items].sort((a, b) => {
|
|
37
42
|
const va = getValue(a, sort.key);
|
|
38
43
|
const vb = getValue(b, sort.key);
|
|
44
|
+
if (typeof va === "string" && typeof vb === "string") {
|
|
45
|
+
// Undefined locale = the reader's own, which is the right collation for a
|
|
46
|
+
// register they are reading. `sensitivity: "base"` folds case and accent
|
|
47
|
+
// for ORDERING only, so "Đơn" and "don" land beside each other instead of
|
|
48
|
+
// in different halves of the list.
|
|
49
|
+
return va.localeCompare(vb, undefined, { sensitivity: "base", numeric: true }) * dir;
|
|
50
|
+
}
|
|
39
51
|
return va < vb ? -dir : va > vb ? dir : 0;
|
|
40
52
|
});
|
|
41
53
|
}
|