@vitavision/ui 0.9.0 → 0.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.
@@ -52,8 +52,8 @@ export interface VectorInputProps {
52
52
  * A row of number fields editing one small vector, with the axis labels before the fields
53
53
  * and the unit once after them.
54
54
  *
55
- * Controlled. A `role="group"` named by `aria-label`, each field named by its label and
56
- * described by the unit. While a field has focus it shows what is typed; on blur or Enter it
55
+ * Controlled. A `role="group"` named by `aria-label`. Each field is named by the group and its
56
+ * label ("Translation x"), so two vectors on one screen stay apart, and is described by the unit. While a field has focus it shows what is typed; on blur or Enter it
57
57
  * shows the value at `precision`; Escape restores the value it had on focus. With `readOnly`
58
58
  * it renders a `ReadoutStrip` (`x 0.100 m · y …`) instead. The row carries `data-readonly`
59
59
  * and `data-disabled`.
@@ -106,6 +106,7 @@ export function VectorInput({
106
106
  <label key={index} className="flex min-w-0 flex-1 items-center gap-1">
107
107
  <span className="shrink-0 font-mono text-[11px] text-fg-subtle">{labelOf(index)}</span>
108
108
  <ComponentField
109
+ name={`${ariaLabel} ${labelOf(index)}`}
109
110
  value={component}
110
111
  precision={precision}
111
112
  step={step}
@@ -128,6 +129,7 @@ export function VectorInput({
128
129
 
129
130
  /** One component: formatted at rest, the typed text while focused. */
130
131
  function ComponentField({
132
+ name,
131
133
  value,
132
134
  precision,
133
135
  step,
@@ -137,6 +139,7 @@ function ComponentField({
137
139
  describedBy,
138
140
  onCommit,
139
141
  }: {
142
+ name: string;
140
143
  value: number;
141
144
  precision: number;
142
145
  step: number | undefined;
@@ -167,6 +170,7 @@ function ComponentField({
167
170
  min={min}
168
171
  max={max}
169
172
  disabled={disabled}
173
+ aria-label={name}
170
174
  aria-describedby={describedBy}
171
175
  onFocus={() => setDraft({ text: formatted, initial: value })}
172
176
  onBlur={() => setDraft(null)}
@@ -12,6 +12,16 @@ export { Button, ButtonLink, buttonClasses, type ButtonSize, type ButtonVariant
12
12
  export { DensityProvider, byDensity, useDensity, type Density } from "./Density";
13
13
  export { ConfirmDialog, Dialog, DialogClose } from "./Dialog";
14
14
  export { Disclosure } from "./Disclosure";
15
+ export {
16
+ DropdownMenu,
17
+ MenuCheckboxItem,
18
+ MenuItem,
19
+ MenuLabel,
20
+ MenuSeparator,
21
+ type DropdownMenuProps,
22
+ type MenuCheckboxItemProps,
23
+ type MenuItemProps,
24
+ } from "./DropdownMenu";
15
25
  export {
16
26
  Callout,
17
27
  type CalloutTone,
@@ -31,6 +41,14 @@ export {
31
41
  useControlHeight,
32
42
  type NumberInputProps,
33
43
  } from "./Input";
44
+ export { Kbd } from "./Kbd";
45
+ export {
46
+ Listbox,
47
+ type ListboxOption,
48
+ type ListboxOptionState,
49
+ type ListboxProps,
50
+ } from "./Listbox";
51
+ export { formatNumber, parseNumber } from "./numberText";
34
52
  export {
35
53
  PageHeader,
36
54
  type BackLink,
@@ -39,6 +57,7 @@ export {
39
57
  Section,
40
58
  type ReadoutItem,
41
59
  } from "./Panel";
60
+ export { Popover, PopoverClose, type PopoverProps } from "./Popover";
42
61
  export {
43
62
  PoseInput,
44
63
  type PoseInputProps,
@@ -0,0 +1,63 @@
1
+ /*
2
+ * Keyboard movement in a single-select listbox, as a pure function so it is tested without a
3
+ * DOM. `Listbox` calls it on every key press.
4
+ */
5
+
6
+ /** The keys that move the active option. */
7
+ export type ListboxKey = "ArrowDown" | "ArrowUp" | "Home" | "End" | "PageDown" | "PageUp";
8
+
9
+ /**
10
+ * The option a key moves the active one to, skipping disabled options.
11
+ *
12
+ * Arrows move by one and stop at the ends (no wrap: a long list that wraps loses its place).
13
+ * Home and End go to the first and last enabled option. Page keys move by `page` options.
14
+ * From no active option (`-1`), Down and Page Down go to the first enabled option, and Up and
15
+ * Page Up to the last.
16
+ *
17
+ * @param disabled - Per option, whether it is disabled.
18
+ * @param from - The active index, or -1.
19
+ * @param key - The key pressed.
20
+ * @param page - Options per page, for Page Up/Down. Defaults to 10.
21
+ * @returns The new active index; `from` when there is nowhere to go; -1 when every option is
22
+ * disabled.
23
+ */
24
+ export function moveActive(
25
+ disabled: readonly boolean[],
26
+ from: number,
27
+ key: ListboxKey,
28
+ page = 10,
29
+ ): number {
30
+ const enabled = disabled.flatMap((off, index) => (off ? [] : [index]));
31
+ if (enabled.length === 0) return -1;
32
+ const first = enabled[0]!;
33
+ const last = enabled[enabled.length - 1]!;
34
+ if (key === "Home") return first;
35
+ if (key === "End") return last;
36
+ if (from < 0) return key === "ArrowDown" || key === "PageDown" ? first : last;
37
+
38
+ const forward = key === "ArrowDown" || key === "PageDown";
39
+ const steps = key === "PageDown" || key === "PageUp" ? Math.max(1, page) : 1;
40
+ let target = from;
41
+ let moved = 0;
42
+ for (let index = from + (forward ? 1 : -1); index >= 0 && index < disabled.length; index += forward ? 1 : -1) {
43
+ if (disabled[index]) continue;
44
+ target = index;
45
+ moved += 1;
46
+ if (moved === steps) break;
47
+ }
48
+ return target;
49
+ }
50
+
51
+ /**
52
+ * The first enabled option at or after `index`, or before it when none is after. Used to place
53
+ * the active option when the list opens or changes.
54
+ *
55
+ * @param disabled - Per option, whether it is disabled.
56
+ * @param index - The preferred index.
57
+ * @returns An enabled index, or -1 when every option is disabled.
58
+ */
59
+ export function nearestEnabled(disabled: readonly boolean[], index: number): number {
60
+ for (let i = Math.max(0, index); i < disabled.length; i += 1) if (!disabled[i]) return i;
61
+ for (let i = Math.min(index, disabled.length) - 1; i >= 0; i -= 1) if (!disabled[i]) return i;
62
+ return -1;
63
+ }
@@ -1,6 +1,6 @@
1
1
  /*
2
- * Numbers as a field shows them and as a person types them. Internal to this package (used by
3
- * `VectorInput` and `PoseInput`); not exported.
2
+ * Numbers as a field shows them and as a person types them. Used by `NumberInput`'s
3
+ * number-valued mode, `VectorInput` and `PoseInput`, and exported for an app's own fields.
4
4
  *
5
5
  * A field bound straight to a number re-formats under the cursor: typing `0.` into a field
6
6
  * showing `0.000` becomes `0` before the next digit arrives. So the multi-number fields keep