@lotics/ui 11.7.0 → 11.7.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/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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "11.7.0",
3
+ "version": "11.7.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./tokens": "./src/tokens.ts",
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 title =
512
- !isCustom && renderOptionContent ? (
513
- renderOptionContent(opt)
514
- ) : desc ? (
515
- <View>
516
- <Text userSelect="none" numberOfLines={1}>
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
- <Text size="xs" color="zinc-500" numberOfLines={1}>
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
  })
@@ -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: string;
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
- <Text weight="medium" color={titleColor} numberOfLines={1} userSelect="none">
62
- {title}
63
- </Text>
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={description ? `${title}, ${description}` : title}
107
+ accessibilityLabel={resolvedLabel}
93
108
  aria-selected={selected || undefined}
94
109
  aria-disabled={disabled || undefined}
95
110
  >
@@ -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 title =
123
- row.kind === "option" && renderOptionContent ? (
124
- renderOptionContent(opt)
125
- ) : desc ? (
126
- <View>
127
- <Text userSelect="none" numberOfLines={1}>
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
  })