@lotics/ui 11.7.0 → 11.7.2
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 +4 -1
- package/docs/composition.md +2 -1
- package/docs/data_entry.md +5 -0
- package/package.json +1 -1
- package/src/combobox.tsx +25 -29
- package/src/floating_action_bar.tsx +4 -1
- package/src/locale.tsx +5 -0
- package/src/menu_list_item.tsx +20 -5
- package/src/option_list.tsx +33 -41
package/docs/catalog.md
CHANGED
|
@@ -408,7 +408,10 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
408
408
|
`selected`/`focused`/`danger`; `role` menuitem|button|option) — popover menus, outline
|
|
409
409
|
rails, section pickers.
|
|
410
410
|
- **`menu_list_item`** — `MenuListItem`: the richer listbox/menu row (title + description +
|
|
411
|
-
`right`; `selected` vs roving `focused` via `aria-activedescendant`).
|
|
411
|
+
`right`; `selected` vs roving `focused` via `aria-activedescendant`). No fixed height — it
|
|
412
|
+
GROWS with its content, and `title` takes a node, so it is what `OptionList`/`Combobox` render
|
|
413
|
+
for any rich option (custom `renderOptionContent`, or a label over its description); plain
|
|
414
|
+
label-only rows stay on `MenuButton`.
|
|
412
415
|
- **`floating_action_bar`** — `FloatingActionBar`: the floating bulk-select action bar.
|
|
413
416
|
- **`pressable_row`** — `PressableRow`: THE register row — full-width hover/open wash,
|
|
414
417
|
`selected` (the open record) vs `marked` (ticked in bulk-select), forwards `ref` for
|
package/docs/composition.md
CHANGED
|
@@ -254,7 +254,8 @@ an empty overlay with no content to derive a name from.
|
|
|
254
254
|
|
|
255
255
|
Make a register SELECTABLE with the `Table` `leading` gutter + `selectAll` slot — a
|
|
256
256
|
`CheckboxInput` per `TableRow` (its `leading` slot) + a select-all in the header band, the ticked
|
|
257
|
-
rows `marked`, paired with a `FloatingActionBar
|
|
257
|
+
rows `marked`, paired with a `FloatingActionBar` (its "Clear" escape is locale-resolved — pass
|
|
258
|
+
`clearLabel` only to override it). The selection state is the **`useSelection()`**
|
|
258
259
|
hook (`@lotics/ui/use_selection`: `selected` / `has` / `count` / `toggle` / `setAll` /
|
|
259
260
|
`allSelected` / `indeterminate` / `clear`) — gating which rows CAN be ticked stays with you: pass
|
|
260
261
|
only the selectable ids to `setAll`/`allSelected`/`indeterminate` (a non-selectable row gets a
|
package/docs/data_entry.md
CHANGED
|
@@ -40,6 +40,11 @@ One per type:
|
|
|
40
40
|
- **`InlineSelect`** — plain options OR `renderOptionContent`; floats an `OptionList` in a popover
|
|
41
41
|
so the row never grows; the RESTING value renders like its option (`renderOptionContent` by
|
|
42
42
|
default; `renderSelected` overrides) — a colored `OptionBadge`, not just a label.
|
|
43
|
+
An option ROW may be taller than one line — a rich row (custom `renderOptionContent`, or a
|
|
44
|
+
label over a `getOptionDescription` line) renders as `MenuListItem`, the listbox row that
|
|
45
|
+
GROWS with its content; only a plain label row is the fixed-height `MenuButton`. So a short
|
|
46
|
+
code + the full official name underneath is a supported option shape, not a hack. Reach for it
|
|
47
|
+
whenever the real name is too long to READ in a list but must stay exact somewhere.
|
|
43
48
|
- **`InlineMemberSelect`** — a `MemberChip` at rest → member picker; the inline twin of
|
|
44
49
|
`MemberSelect`; worked example: `tpl_record`'s "Sales owner" fact.
|
|
45
50
|
- **`InlineDatePicker`** — `format="datetime"` for always-on time; `optionalTime` to let the user
|
package/package.json
CHANGED
package/src/combobox.tsx
CHANGED
|
@@ -24,6 +24,7 @@ import { Text } from "./text";
|
|
|
24
24
|
import { Icon, type IconName } from "./icon";
|
|
25
25
|
import { TextInputField } from "./text_input_field";
|
|
26
26
|
import { MenuButton } from "./menu_button";
|
|
27
|
+
import { MenuListItem } from "./menu_list_item";
|
|
27
28
|
import { ActivityIndicator } from "./activity_indicator";
|
|
28
29
|
import { Popover, PopoverContent } from "./popover";
|
|
29
30
|
import type { PickerOption } from "./picker";
|
|
@@ -508,37 +509,32 @@ export function ComboboxContent(props: ComboboxContentProps) {
|
|
|
508
509
|
const opt = row.option;
|
|
509
510
|
const desc = !isCustom ? getOptionDescription?.(opt) : undefined;
|
|
510
511
|
const label = opt.label ?? opt.value;
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
512
|
+
const content = !isCustom ? renderOptionContent?.(opt) : undefined;
|
|
513
|
+
const shared = {
|
|
514
|
+
key: `${row.kind}-${opt.value}`,
|
|
515
|
+
nativeID: row.nativeID,
|
|
516
|
+
testID: isCustom ? "combobox-custom-option" : `combobox-option-${opt.value}`,
|
|
517
|
+
role: "option" as const,
|
|
518
|
+
accessibilityLabel: label,
|
|
519
|
+
icon: isCustom ? <Icon name="plus" size={16} color={colors.zinc["600"]} /> : undefined,
|
|
520
|
+
focused: row.index === list.activeIndex,
|
|
521
|
+
selected: row.selected,
|
|
522
|
+
disabled: opt.disabled,
|
|
523
|
+
onPress: () => list.pickRow(row.index),
|
|
524
|
+
onHoverIn: () => list.setActiveIndex(row.index, false),
|
|
525
|
+
};
|
|
526
|
+
// Rich row (custom content, or a label over its description) → the growing
|
|
527
|
+
// `MenuListItem`; a plain option stays the fixed-height `MenuButton`.
|
|
528
|
+
return content || desc ? (
|
|
529
|
+
<MenuListItem {...shared} title={content ?? label} description={content ? undefined : desc} />
|
|
530
|
+
) : (
|
|
531
|
+
<MenuButton
|
|
532
|
+
{...shared}
|
|
533
|
+
title={
|
|
534
|
+
<Text userSelect="none" numberOfLines={1} weight={isCustom ? "medium" : undefined}>
|
|
517
535
|
{label}
|
|
518
536
|
</Text>
|
|
519
|
-
|
|
520
|
-
{desc}
|
|
521
|
-
</Text>
|
|
522
|
-
</View>
|
|
523
|
-
) : (
|
|
524
|
-
<Text userSelect="none" numberOfLines={1} weight={isCustom ? "medium" : undefined}>
|
|
525
|
-
{label}
|
|
526
|
-
</Text>
|
|
527
|
-
);
|
|
528
|
-
return (
|
|
529
|
-
<MenuButton
|
|
530
|
-
key={`${row.kind}-${opt.value}`}
|
|
531
|
-
nativeID={row.nativeID}
|
|
532
|
-
testID={isCustom ? "combobox-custom-option" : `combobox-option-${opt.value}`}
|
|
533
|
-
role="option"
|
|
534
|
-
accessibilityLabel={label}
|
|
535
|
-
icon={isCustom ? <Icon name="plus" size={16} color={colors.zinc["600"]} /> : undefined}
|
|
536
|
-
title={title}
|
|
537
|
-
focused={row.index === list.activeIndex}
|
|
538
|
-
selected={row.selected}
|
|
539
|
-
disabled={opt.disabled}
|
|
540
|
-
onPress={() => list.pickRow(row.index)}
|
|
541
|
-
onHoverIn={() => list.setActiveIndex(row.index, false)}
|
|
537
|
+
}
|
|
542
538
|
/>
|
|
543
539
|
);
|
|
544
540
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ReactNode } from "react";
|
|
2
2
|
import { StyleSheet, View } from "react-native";
|
|
3
3
|
import { Button } from "./button";
|
|
4
|
+
import { useLoticsLocale } from "./locale";
|
|
4
5
|
import { Card } from "./card";
|
|
5
6
|
import { Text } from "./text";
|
|
6
7
|
|
|
@@ -11,6 +12,7 @@ export interface FloatingActionBarProps {
|
|
|
11
12
|
label: string;
|
|
12
13
|
/** Clears the selection — the always-present escape. */
|
|
13
14
|
onClear: () => void;
|
|
15
|
+
/** Overrides the localized default ("Clear" / "Bỏ chọn"). */
|
|
14
16
|
clearLabel?: string;
|
|
15
17
|
/** The bulk action(s) — a primary `Button` or an "Assign to…" `Popover`. */
|
|
16
18
|
children: ReactNode;
|
|
@@ -25,7 +27,8 @@ export interface FloatingActionBarProps {
|
|
|
25
27
|
* `box-none` wrapper lets clicks fall through everywhere except the bar.
|
|
26
28
|
*/
|
|
27
29
|
export function FloatingActionBar(props: FloatingActionBarProps) {
|
|
28
|
-
const
|
|
30
|
+
const loc = useLoticsLocale().floatingActionBar;
|
|
31
|
+
const { count, label, onClear, clearLabel = loc.clear, children } = props;
|
|
29
32
|
if (count <= 0) return null;
|
|
30
33
|
return (
|
|
31
34
|
<View pointerEvents="box-none" style={styles.wrap}>
|
package/src/locale.tsx
CHANGED
|
@@ -36,6 +36,9 @@ export interface LoticsLocale {
|
|
|
36
36
|
/** `FilterChip` (and `ColumnFilter`): the generic clear affordance, used when
|
|
37
37
|
* a call site doesn't pass a dimension-specific `clearLabel`. */
|
|
38
38
|
filterChip: { clear: string };
|
|
39
|
+
/** `FloatingActionBar`: the escape that drops the selection. The COUNT's noun is
|
|
40
|
+
* the call site's `label` (it names the rows), so only the escape lives here. */
|
|
41
|
+
floatingActionBar: { clear: string };
|
|
39
42
|
/** `FormField`: the "Optional" marker shown next to the label when `optional`. */
|
|
40
43
|
formField: { optional: string };
|
|
41
44
|
/** `Drawer`: the record prev/next + close controls (screen-reader names). */
|
|
@@ -107,6 +110,7 @@ export const en: LoticsLocale = {
|
|
|
107
110
|
datePicker: { today: "Today", now: "Now", clear: "Clear", done: "Done", openCalendar: "Open calendar", time: "Time", startTime: "Start time", endTime: "End time", addTime: "Add time", removeTime: "Remove time", year: "Year", month: "Month", day: "Day", hour: "Hour", minute: "Minute", dayPeriod: "AM/PM" },
|
|
108
111
|
calendar: { previousMonth: "Previous month", nextMonth: "Next month" },
|
|
109
112
|
filterChip: { clear: "Clear" },
|
|
113
|
+
floatingActionBar: { clear: "Clear" },
|
|
110
114
|
formField: { optional: "Optional" },
|
|
111
115
|
drawer: { previous: "Previous record", next: "Next record", close: "Close" },
|
|
112
116
|
inline: { saveError: "Couldn't save. Try again.", save: "Save", cancel: "Cancel" },
|
|
@@ -186,6 +190,7 @@ export const vi: LoticsLocale = {
|
|
|
186
190
|
datePicker: { today: "Hôm nay", now: "Bây giờ", clear: "Xóa", done: "Xong", openCalendar: "Mở lịch", time: "Giờ", startTime: "Giờ bắt đầu", endTime: "Giờ 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" },
|
|
187
191
|
calendar: { previousMonth: "Tháng trước", nextMonth: "Tháng sau" },
|
|
188
192
|
filterChip: { clear: "Xóa" },
|
|
193
|
+
floatingActionBar: { clear: "Bỏ chọn" },
|
|
189
194
|
formField: { optional: "Tùy chọn" },
|
|
190
195
|
drawer: { previous: "Bản ghi trước", next: "Bản ghi sau", close: "Đóng" },
|
|
191
196
|
inline: { saveError: "Không lưu được. Thử lại.", save: "Lưu", cancel: "Hủy" },
|
package/src/menu_list_item.tsx
CHANGED
|
@@ -10,7 +10,10 @@ import type { TextColor } from "./text_utils";
|
|
|
10
10
|
export interface MenuListItemProps {
|
|
11
11
|
ref?: Ref<View>;
|
|
12
12
|
icon?: IconName | React.ReactNode;
|
|
13
|
-
title
|
|
13
|
+
/** A plain string gets the row's own title type; a node renders as-is — an option
|
|
14
|
+
* whose label is richer than one line of text (a code + its full official name).
|
|
15
|
+
* Pass `accessibilityLabel` when the title is a node. */
|
|
16
|
+
title: React.ReactNode;
|
|
14
17
|
/** Title color. Default ink — set it to mark the row as an action (e.g. a
|
|
15
18
|
* leading "Browse all" tinted to match its icon). */
|
|
16
19
|
titleColor?: TextColor;
|
|
@@ -28,6 +31,8 @@ export interface MenuListItemProps {
|
|
|
28
31
|
testID?: string;
|
|
29
32
|
/** DOM id, referenced by a combobox input's `aria-activedescendant`. */
|
|
30
33
|
nativeID?: string;
|
|
34
|
+
/** Required when `title` is a node — the row's accessible name. */
|
|
35
|
+
accessibilityLabel?: string;
|
|
31
36
|
/** ARIA role. Defaults to `button`; pass `option` for a listbox row. */
|
|
32
37
|
role?: "menuitem" | "button" | "option";
|
|
33
38
|
}
|
|
@@ -48,9 +53,15 @@ export function MenuListItem(props: MenuListItemProps) {
|
|
|
48
53
|
style,
|
|
49
54
|
testID,
|
|
50
55
|
nativeID,
|
|
56
|
+
accessibilityLabel,
|
|
51
57
|
role = "button",
|
|
52
58
|
} = props;
|
|
53
59
|
|
|
60
|
+
const titleText = typeof title === "string" ? title : undefined;
|
|
61
|
+
const resolvedLabel =
|
|
62
|
+
accessibilityLabel ??
|
|
63
|
+
(titleText && description ? `${titleText}, ${description}` : titleText);
|
|
64
|
+
|
|
54
65
|
const resolvedIcon =
|
|
55
66
|
typeof icon === "string" ? <Icon size={20} name={icon as IconName} /> : icon;
|
|
56
67
|
|
|
@@ -58,9 +69,13 @@ export function MenuListItem(props: MenuListItemProps) {
|
|
|
58
69
|
<>
|
|
59
70
|
{resolvedIcon}
|
|
60
71
|
<View style={styles.textContainer}>
|
|
61
|
-
|
|
62
|
-
{
|
|
63
|
-
|
|
72
|
+
{titleText !== undefined ? (
|
|
73
|
+
<Text weight="medium" color={titleColor} numberOfLines={1} userSelect="none">
|
|
74
|
+
{titleText}
|
|
75
|
+
</Text>
|
|
76
|
+
) : (
|
|
77
|
+
title
|
|
78
|
+
)}
|
|
64
79
|
{!!description && (
|
|
65
80
|
<Text size="xs" color="zinc-500" numberOfLines={1} userSelect="none">
|
|
66
81
|
{description}
|
|
@@ -89,7 +104,7 @@ export function MenuListItem(props: MenuListItemProps) {
|
|
|
89
104
|
disabled={disabled}
|
|
90
105
|
style={containerStyle}
|
|
91
106
|
role={role}
|
|
92
|
-
accessibilityLabel={
|
|
107
|
+
accessibilityLabel={resolvedLabel}
|
|
93
108
|
aria-selected={selected || undefined}
|
|
94
109
|
aria-disabled={disabled || undefined}
|
|
95
110
|
>
|
package/src/option_list.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { Text } from "./text";
|
|
|
5
5
|
import { Icon } from "./icon";
|
|
6
6
|
import { Checkbox } from "./checkbox";
|
|
7
7
|
import { MenuButton } from "./menu_button";
|
|
8
|
+
import { MenuListItem } from "./menu_list_item";
|
|
8
9
|
import { Button } from "./button";
|
|
9
10
|
import { TextLink } from "./text_link";
|
|
10
11
|
import { ActivityIndicator } from "./activity_indicator";
|
|
@@ -119,50 +120,41 @@ export function OptionList<T extends string, MULTI extends boolean = false, D =
|
|
|
119
120
|
const opt = row.option;
|
|
120
121
|
const desc = row.kind === "option" ? getOptionDescription?.(opt) : undefined;
|
|
121
122
|
const label = opt.label ?? opt.value;
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
123
|
+
const content = row.kind === "option" ? renderOptionContent?.(opt) : undefined;
|
|
124
|
+
const shared = {
|
|
125
|
+
key: `${row.kind}-${opt.value}`,
|
|
126
|
+
nativeID: row.nativeID,
|
|
127
|
+
testID: isCustom ? "option-list-custom" : opt.testID || `picker-option-${opt.value}`,
|
|
128
|
+
role: "option" as const,
|
|
129
|
+
accessibilityLabel: isCustom ? label : (opt.label ?? String(opt.value)),
|
|
130
|
+
icon: isCustom ? (
|
|
131
|
+
<Icon name="plus" size={16} color={colors.zinc["600"]} />
|
|
132
|
+
) : props.multi && row.kind === "option" ? (
|
|
133
|
+
<Checkbox checked={row.selected} />
|
|
134
|
+
) : undefined,
|
|
135
|
+
right:
|
|
136
|
+
!props.multi && row.selected ? (
|
|
137
|
+
<Icon name="check" size={18} color={colors.zinc["950"]} />
|
|
138
|
+
) : undefined,
|
|
139
|
+
focused: row.index === list.activeIndex,
|
|
140
|
+
disabled: opt.disabled,
|
|
141
|
+
onPress: () => list.pickRow(row.index),
|
|
142
|
+
onHoverIn: () => list.setActiveIndex(row.index, false),
|
|
143
|
+
style: styles.option,
|
|
144
|
+
};
|
|
145
|
+
// A row that carries more than a label — custom content, or a label over its
|
|
146
|
+
// description — is `MenuListItem`: the listbox row that GROWS. `MenuButton` is
|
|
147
|
+
// the fixed-height single-line menu row, and stays that for plain options.
|
|
148
|
+
return content || desc ? (
|
|
149
|
+
<MenuListItem {...shared} title={content ?? label} description={content ? undefined : desc} />
|
|
150
|
+
) : (
|
|
151
|
+
<MenuButton
|
|
152
|
+
{...shared}
|
|
153
|
+
title={
|
|
154
|
+
<Text userSelect="none" numberOfLines={1} weight={isCustom ? "medium" : undefined}>
|
|
128
155
|
{label}
|
|
129
156
|
</Text>
|
|
130
|
-
<Text size="xs" color="zinc-500" numberOfLines={1}>
|
|
131
|
-
{desc}
|
|
132
|
-
</Text>
|
|
133
|
-
</View>
|
|
134
|
-
) : (
|
|
135
|
-
<Text userSelect="none" numberOfLines={1} weight={isCustom ? "medium" : undefined}>
|
|
136
|
-
{label}
|
|
137
|
-
</Text>
|
|
138
|
-
);
|
|
139
|
-
return (
|
|
140
|
-
<MenuButton
|
|
141
|
-
key={`${row.kind}-${opt.value}`}
|
|
142
|
-
nativeID={row.nativeID}
|
|
143
|
-
testID={
|
|
144
|
-
isCustom ? "option-list-custom" : opt.testID || `picker-option-${opt.value}`
|
|
145
|
-
}
|
|
146
|
-
role="option"
|
|
147
|
-
accessibilityLabel={isCustom ? label : (opt.label ?? String(opt.value))}
|
|
148
|
-
icon={
|
|
149
|
-
isCustom ? (
|
|
150
|
-
<Icon name="plus" size={16} color={colors.zinc["600"]} />
|
|
151
|
-
) : props.multi && row.kind === "option" ? (
|
|
152
|
-
<Checkbox checked={row.selected} />
|
|
153
|
-
) : undefined
|
|
154
|
-
}
|
|
155
|
-
title={title}
|
|
156
|
-
right={
|
|
157
|
-
!props.multi && row.selected ? (
|
|
158
|
-
<Icon name="check" size={18} color={colors.zinc["950"]} />
|
|
159
|
-
) : undefined
|
|
160
157
|
}
|
|
161
|
-
focused={row.index === list.activeIndex}
|
|
162
|
-
disabled={opt.disabled}
|
|
163
|
-
onPress={() => list.pickRow(row.index)}
|
|
164
|
-
onHoverIn={() => list.setActiveIndex(row.index, false)}
|
|
165
|
-
style={styles.option}
|
|
166
158
|
/>
|
|
167
159
|
);
|
|
168
160
|
})
|