@lotics/ui 17.0.2 → 18.1.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/MIGRATION.md +20 -0
- package/docs/catalog.md +45 -17
- package/docs/composition.md +23 -12
- package/docs/data_entry.md +3 -1
- package/docs/templates.md +3 -2
- package/examples/tpl_record.tsx +2 -2
- package/package.json +1 -1
- package/src/combobox.tsx +24 -14
- package/src/date_calendar.tsx +6 -19
- package/src/date_filter.tsx +95 -102
- package/src/date_picker.tsx +5 -0
- package/src/date_range_selection.test.ts +60 -0
- package/src/date_range_selection.ts +32 -0
- package/src/date_segments.test.ts +48 -0
- package/src/date_segments.ts +34 -0
- package/src/inline_time_picker.tsx +11 -12
- package/src/linked_record_box.tsx +77 -21
- package/src/locale.tsx +2 -2
- package/src/menu_button.tsx +32 -5
- package/src/pressable_highlight.tsx +7 -0
- package/src/section_heading.tsx +2 -1
- package/src/section_stack.tsx +14 -8
- package/src/text_input_field.tsx +46 -7
- package/src/time_picker.tsx +97 -39
- package/src/use_section_nav.test.ts +103 -1
- package/src/use_section_nav.ts +47 -20
package/MIGRATION.md
CHANGED
|
@@ -4,6 +4,26 @@ Breaking changes, newest first — normally per major, plus the rare minor that
|
|
|
4
4
|
anyway (recorded under its exact version). The current contract lives in `AGENTS.md` + `docs/`;
|
|
5
5
|
this file exists only to move an app from one release to the next.
|
|
6
6
|
|
|
7
|
+
## v18 from 17.x
|
|
8
|
+
|
|
9
|
+
**`TimePicker` is a segmented field, not `<input type="time">`.** A native time input takes
|
|
10
|
+
its 12- vs 24-hour form from the BROWSER's UI locale and ignores the `lang` attribute
|
|
11
|
+
entirely, so a Vietnamese screen on an en-US browser rendered "01:45 PM" beside its own
|
|
12
|
+
24-hour text. It now uses the same segment engine as `DateField`, with the format derived
|
|
13
|
+
from a `locale` prop (defaulting to the active `LoticsLocaleProvider` locale).
|
|
14
|
+
|
|
15
|
+
The value is unchanged — canonical 24-hour `"HH:mm"` either way. Two prop changes:
|
|
16
|
+
|
|
17
|
+
- **`onKeyDown` is GONE.** It typed the DOM input's key events, which no longer exist. Use
|
|
18
|
+
`onEscape` for cancel; a completed entry emits through `onValueChange` as before.
|
|
19
|
+
- **`onIncompleteChange` is NEW** and matters for inline editors: segments emit only a
|
|
20
|
+
COMPLETE `"HH:mm"`, so a half-typed entry (an hour, no minute) arrives as `""` and would
|
|
21
|
+
otherwise commit as a silent clear. Block the commit on it — `InlineTimePicker` does, and
|
|
22
|
+
surfaces the new `datePicker.invalidTime` label.
|
|
23
|
+
|
|
24
|
+
`DatePickerLabels` gains **`invalidTime`**; a caller that builds the label bag by hand (rather
|
|
25
|
+
than spreading the locale pack) must add it.
|
|
26
|
+
|
|
7
27
|
## v17 from 16.x
|
|
8
28
|
|
|
9
29
|
**`Checklist` / `ChecklistRow` are DELETED.** They are replaced by the `Task` compound
|
package/docs/catalog.md
CHANGED
|
@@ -171,7 +171,7 @@ component — children are the body), `Subsection` (+ `SubsectionHeading`/
|
|
|
171
171
|
`SubsectionHeadingTitle` — the named group INSIDE a section, `###` lg-semibold title),
|
|
172
172
|
`SectionStack` (the flat page's content column — owns the fixed 56px beat + hairline between
|
|
173
173
|
top-level blocks), `SubsectionStack` (a section's `Subsection` groups — fixed 32px beat,
|
|
174
|
-
|
|
174
|
+
space-only while the groups are SHORT, `divided` once they run long), `SectionCard`, `PageHeader` /
|
|
175
175
|
`PageContent`, `Stack`, `Spacer`, `Divider`, `Accordion`, `Tabs`, `SegmentedControl`,
|
|
176
176
|
`Stepper`, `DangerZone` (the destructive section — delete/archive — set apart at the bottom
|
|
177
177
|
of a record/settings surface).
|
|
@@ -388,9 +388,10 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
388
388
|
`@lotics/ui/section` only when you specifically want this collapsible titled block.
|
|
389
389
|
- **`section_stack`** — `SectionStack` · `SubsectionStack` — stacks that own the
|
|
390
390
|
between-block law, skipping null children: `SectionStack` = a fixed 56px beat + a hairline
|
|
391
|
-
`Divider` between top-level blocks; `SubsectionStack` = a fixed 32px beat, space-only
|
|
392
|
-
|
|
393
|
-
|
|
391
|
+
`Divider` between top-level blocks; `SubsectionStack` = a fixed 32px beat, space-only while
|
|
392
|
+
the groups are SHORT (the titles carry the grouping, hairlines stay at the section level) and
|
|
393
|
+
`divided` once they run LONG — past a screenful, 32px reads as one more row gap and the next
|
|
394
|
+
title arrives unmarked. Length decides, not headings. Stop hand-rolling gap + `<Divider />` pairs.
|
|
394
395
|
- **`section_card`** — `SectionCard`: a titled card with a one-line description and an
|
|
395
396
|
optional hairline-set `footer` (where `TrendFooter` and source notes go).
|
|
396
397
|
- **`stack`** — `Stack`: gap-spaced row/column; `useSeparator` inserts `Separator`s between
|
|
@@ -454,8 +455,13 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
454
455
|
- **`action_menu`** — `ActionMenu`: the ⋯ overflow menu (`ActionMenuItem[]`; danger items
|
|
455
456
|
last).
|
|
456
457
|
- **`menu_button`** — `MenuButton`: the menu/rail row (icon · title · `right` slot;
|
|
457
|
-
`
|
|
458
|
-
|
|
458
|
+
`focused`/`danger`; `role` menuitem|button|option) — popover menus, outline rails, section
|
|
459
|
+
pickers. Its resting highlight has TWO meanings and they are not interchangeable:
|
|
460
|
+
**`selected`** is listbox SELECTION (emitted as `aria-selected`, and only under
|
|
461
|
+
`role="option"` — the attribute is invalid anywhere else), **`current`** is the current
|
|
462
|
+
item of a NAVIGATION set (`aria-current`, global, so any role) — `true`, or the token
|
|
463
|
+
naming the set (`"page"` for a routed pane, `"step"`, `"location"`). An outline rail and a
|
|
464
|
+
section picker take `current`; a picker option takes `selected`.
|
|
459
465
|
- **`menu_list_item`** — `MenuListItem`: the richer listbox/menu row (title + description +
|
|
460
466
|
`right`; `selected` vs roving `focused` via `aria-activedescendant`). No fixed height — it
|
|
461
467
|
GROWS with its content, and `title` takes a node, so it is what `OptionList`/`Combobox` render
|
|
@@ -474,11 +480,18 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
474
480
|
matches the surface it spans. `TableRow` / `LinkedRecordBox` are the in-kit consumers —
|
|
475
481
|
reach for it when designing a row those don't cover.
|
|
476
482
|
- **`linked_record_box`** — `LinkedRecordBox`: a bordered box scoping ANOTHER record's data
|
|
477
|
-
(`icon` · `name` · `subtitle` · vertical `facts`)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
483
|
+
(`icon` · `name` · `subtitle` · vertical `facts`); verbs ride an `actions` slot in a
|
|
484
|
+
hairline-fenced footer at the bottom (destructive LEFT, go-to RIGHT — the divider draws itself
|
|
485
|
+
when `actions` is present, so don't hand-add one). Use it wherever a record points at another (a
|
|
486
|
+
shipment's customer, an invoice's party, a case's sibling). **`onOpen` picks the variant, and
|
|
487
|
+
they are exclusive at the type level**: WITH it (+ the required `doorLabel`) the whole box is a
|
|
488
|
+
keyboard door into the record's detail and `facts` values are TEXT (`""` → "—"); WITHOUT it the
|
|
489
|
+
box is a STATIC card — no door, no tab stop, no pointer, nothing announced as a button — and a
|
|
490
|
+
`facts` value may be a NODE (an inline editor; a node prints as authored, no "—"). Reach for the
|
|
491
|
+
static one when the linked record has NO page of its own: nothing to open, so the box is where
|
|
492
|
+
its values are read and edited, and the only interactive parts are `actions` + the fact nodes.
|
|
493
|
+
Under a door, `actions` is the ONLY place a control may live (a fact editor there would compete
|
|
494
|
+
with the press). The door variant **enforces the a11y contract** copy-pasting got wrong: a
|
|
482
495
|
container with interactive descendants is never `role="button"` (invalid HTML) — an internal
|
|
483
496
|
empty door sibling carries the tab stop / name / focus ring, verbs lift above via `zIndex`.
|
|
484
497
|
The reassign picker / empty state that swaps in for the box is the consumer's.
|
|
@@ -495,7 +508,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
495
508
|
- **`count`** — `Count`: a small circular count bubble (`color` highlight|muted|red).
|
|
496
509
|
- **`shortcut_badge`** — `ShortcutBadge`: the keycap hint pill — a zinc-50 badge rendering a
|
|
497
510
|
shortcut from a raw string or `ShortcutDescriptor` (⌘B on Mac, Ctrl+B elsewhere); null on
|
|
498
|
-
small screens. `TextInputField shortcut` renders it built-in
|
|
511
|
+
small screens. `TextInputField shortcut` renders it built-in, while the field is EMPTY —
|
|
512
|
+
it hints at reaching the field, so a value replaces it (with the clear ✕, when `clearable`).
|
|
499
513
|
- **`keyboard`** — `isMac` · `ShortcutDescriptor` · `formatShortcut` (platform-aware
|
|
500
514
|
shortcut formatting behind `ShortcutBadge`).
|
|
501
515
|
|
|
@@ -555,7 +569,10 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
555
569
|
- **`text_input_field`** — `TextInputField`: the standard text input (multiline grows via
|
|
556
570
|
the auto-grow engine; `shortcut` renders a `ShortcutBadge`). Its surface is WHITE on any
|
|
557
571
|
background — on a tinted panel the input still reads as an open well, never blending into
|
|
558
|
-
the tint; override via `style` only when a surface genuinely wants otherwise.
|
|
572
|
+
the tint; override via `style` only when a surface genuinely wants otherwise. It RESERVES
|
|
573
|
+
its own right gutter whenever a trailing affordance is up (the `clearable` ✕, the `shortcut`
|
|
574
|
+
badge), so a long value is held off it — never hand-roll a `paddingRight` to compensate. A
|
|
575
|
+
field with an empty slot reserves nothing, so its text starts and ends where it always did.
|
|
559
576
|
- **`number_input`** — `NumberInput`: numeric input; `format` for currency/units.
|
|
560
577
|
- **`search_input`** — `SearchInput`: the search box for toolbars/filters.
|
|
561
578
|
- **`form_field`** — `FormField` + `useFormField`: label / description / warning / error /
|
|
@@ -600,11 +617,19 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
600
617
|
localized weekday/month names via BCP-47 `locale`, `firstDayOfWeek` (default Monday),
|
|
601
618
|
`ref.navigateToMonth`. The engine `DatePicker`/`DateFilter` wrap in field chrome — reach
|
|
602
619
|
for it bare only when the calendar lives permanently on the surface, not behind a field.
|
|
603
|
-
- **`date_filter`** — `DateFilter`: the
|
|
620
|
+
- **`date_filter`** — `DateFilter`: the date+time period filter panel — presets
|
|
604
621
|
(`PresetId`), calendar, optional time segments; the body `DateRangeFilterField` opens.
|
|
622
|
+
**One selection rule, no modes**: a click OPENS a range, the next CLOSES it (in either
|
|
623
|
+
order — bounds come back sorted), and a single day is the same day clicked twice
|
|
624
|
+
(`start`/`end` equal — which is how a consumer detects "one day" and how the trigger
|
|
625
|
+
renders it as a single date). Both bounds always show, so a half-picked range names the
|
|
626
|
+
end that is still open.
|
|
605
627
|
- **`date_range_filter_field`** — `DateRangeFilterField`: the register's period filter field
|
|
606
628
|
(presets + footer + time-segment a11y; localized via the `dateRange` locale slice).
|
|
607
|
-
- **`time_picker`** — `TimePicker`: the time-of-day
|
|
629
|
+
- **`time_picker`** — `TimePicker`: the time-of-day field. Segments (hour / minute / AM-PM),
|
|
630
|
+
**12- vs 24-hour derived from `locale`** — never from the browser, which is why this is not
|
|
631
|
+
an `<input type="time">`. The value is always canonical 24-hour `"HH:mm"`; `locale` changes
|
|
632
|
+
only what is displayed and typed.
|
|
608
633
|
|
|
609
634
|
### Inline editing & record surfaces
|
|
610
635
|
|
|
@@ -849,8 +874,11 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
849
874
|
is open. Lives in the primitives — never call it from a screen.
|
|
850
875
|
- **`use_section_nav`** — `useSectionNav`: scroll-spy for a LONG record surface with a left
|
|
851
876
|
outline rail — keys in page order → `{scrollRef, onScroll, register(key)→onLayout,
|
|
852
|
-
jumpTo(key), activeKey}`; rail items are `MenuButton`s (`
|
|
853
|
-
|
|
877
|
+
jumpTo(key), activeKey}`; rail items are `MenuButton`s (`current={activeKey===key}` — a
|
|
878
|
+
rail item is navigation, never a selected option);
|
|
879
|
+
section wrappers must be DIRECT children of the ScrollView content. The highlight and the
|
|
880
|
+
jump both re-measure the live DOM on web, so content loading in above a section never
|
|
881
|
+
strands them. On NARROW containers
|
|
854
882
|
the rail becomes a PINNED bar naming the CURRENT section that opens a full-page
|
|
855
883
|
section-picker `Modal` — never a horizontal tab strip. Worked example:
|
|
856
884
|
[`tpl_record`](../examples/tpl_record.tsx).
|
package/docs/composition.md
CHANGED
|
@@ -63,9 +63,12 @@ restyle a heading level per-page.
|
|
|
63
63
|
`SubsectionHeading` › `SubsectionHeadingTitle` (`###` — lg semibold, with the same `info` ⓘ
|
|
64
64
|
affordance as `SectionHeadingTitle`; siblings — a `Badge`, a
|
|
65
65
|
`SectionHeadingMeta`, an action — ride the heading row's right edge). Sibling subsections stack
|
|
66
|
-
in a **`SubsectionStack`** (a fixed 32px beat
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
in a **`SubsectionStack`** (a fixed 32px beat; no margins, no hand-rolled dividers).
|
|
67
|
+
It is SPACE-ONLY while the groups are SHORT — the titles carry the grouping and hairlines
|
|
68
|
+
belong to the SECTION level, one rule per altitude — and takes `divided` once the groups run
|
|
69
|
+
LONG: past a screenful of rows the 32px beat reads as one more row gap and the next title
|
|
70
|
+
arrives with nothing marking that a new group started. LENGTH is the discriminator, not
|
|
71
|
+
whether the groups are titled. A headingless `Subsection` is the
|
|
69
72
|
section's lead group. Group leads INSIDE a subsection's rows are `md` medium at most. Do NOT
|
|
70
73
|
hand-roll `Text weight="semibold"` group leads inside a section, and never promote a subsection
|
|
71
74
|
to its own section-level heading just to separate it.
|
|
@@ -448,13 +451,19 @@ gate's SCOPE, once** — never prose beside the button, never revealed only on p
|
|
|
448
451
|
the band's invisible bottom half must never read as a hole above the description.
|
|
449
452
|
- **A LINKED record never renders as field rows** — another record's data must not read as
|
|
450
453
|
fields on THIS one. Use **`LinkedRecordBox`** (`tpl_record`'s Customer/sibling are the worked
|
|
451
|
-
examples): a BORDERED
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
set across every box of a group (a party wall's five links all read "Gỡ", not a mix of
|
|
454
|
+
examples): a BORDERED box (glyph anchor · identity · facts stacked vertically) whose verbs
|
|
455
|
+
ride its `actions` slot in a hairline-fenced footer INSIDE the box — destructive LEFT in
|
|
456
|
+
danger, go-to RIGHT. One verb set across every box of a group (a party wall's five links all read "Gỡ", not a mix of
|
|
455
457
|
"Đổi"/"Gỡ"): with a clearable link, changing = remove → the empty-state picker re-prompts, so
|
|
456
|
-
the destructive verb covers both required anchors and optional adds.
|
|
457
|
-
|
|
458
|
+
the destructive verb covers both required anchors and optional adds.
|
|
459
|
+
**`onOpen` decides whether the box is a DOOR, and the kit makes the two exclusive.** Pass it
|
|
460
|
+
(with `doorLabel`) when the linked record HAS a detail surface: the whole box presses open, its
|
|
461
|
+
facts are TEXT, and editing that record happens on ITS page, never here. OMIT it when the record
|
|
462
|
+
has NO page of its own — the box renders static (no tab stop, no pointer, nothing announced as a
|
|
463
|
+
button), the explicit `actions` verbs are its only affordances, and a `facts` value may then be a
|
|
464
|
+
NODE holding the inline editor, since there is no destination a press could steal. Never wire a
|
|
465
|
+
door "so the box presses": it sends the reader somewhere they did not ask to go and duplicates
|
|
466
|
+
whatever change verb the footer already shows.
|
|
458
467
|
**The empty / attach state is a find-or-create `Combobox`, never a bare picker** (`tpl_record`'s
|
|
459
468
|
Customer is the worked example): rows are `renderOptionContent`-rich, NOT a wall of bare names —
|
|
460
469
|
the premium shape is a **glyph tile (28·radius 7·zinc-100) · the name over a muted metadata line**
|
|
@@ -468,13 +477,15 @@ gate's SCOPE, once** — never prose beside the button, never revealed only on p
|
|
|
468
477
|
WITHOUT leaving the record. `customOptionPlacement:"top"` keeps the create row visible while the
|
|
469
478
|
keyboard highlight stays on the first MATCH, so Enter on a partial never makes a duplicate. A
|
|
470
479
|
linked record seeded by a workflow (not user-attached) skips the picker — show the box or an
|
|
471
|
-
`EmptyState`, no create affordance. The component OWNS the press ANATOMY (the
|
|
472
|
-
got wrong): the box is a `PressableRow` (role-less, non-focusable surface — hover wash spans
|
|
480
|
+
`EmptyState`, no create affordance. The component OWNS the door variant's press ANATOMY (the
|
|
481
|
+
error-prone part hand-rolling got wrong): the box is a `PressableRow` (role-less, non-focusable surface — hover wash spans
|
|
473
482
|
the nested verbs) with the keyboard DOOR — an EMPTY absolutely-positioned `role="button"`
|
|
474
483
|
sibling carrying the tab stop, the accessible name and the focus ring — beneath the content,
|
|
475
484
|
the interior verbs lifted above it via `zIndex: 1`. NEVER `role="button"` on a container with
|
|
476
485
|
interactive descendants: a button must not contain a button (invalid HTML, a hydration error,
|
|
477
|
-
and AT double-announcement).
|
|
486
|
+
and AT double-announcement). That fenced, lifted footer is the ONLY place a control may sit
|
|
487
|
+
inside a door box — a control in the facts column would compete with the press for the same
|
|
488
|
+
click, which is why a node fact typechecks only on the doorless variant.
|
|
478
489
|
|
|
479
490
|
## Color discipline — solid / tint / ramp + ONE accent
|
|
480
491
|
|
package/docs/data_entry.md
CHANGED
|
@@ -346,7 +346,9 @@ level up — money never reads as one pot); each closes with a SEQUENTIAL confir
|
|
|
346
346
|
its blockers ("Confirm check-in first."). The header status chip carries the phase.
|
|
347
347
|
|
|
348
348
|
A LONG record surface pairs with a LEFT OUTLINE RAIL — `MenuButton` items + `useSectionNav`
|
|
349
|
-
(scroll-spy: jump to a section, the highlight follows the scroll);
|
|
349
|
+
(scroll-spy: jump to a section, the highlight follows the scroll); the active item is
|
|
350
|
+
`current={activeKey === key}`, the NAVIGATION state, never `selected`, which is listbox
|
|
351
|
+
selection. On narrow containers the rail
|
|
350
352
|
becomes a PINNED bar naming the current section that opens a full-page section-picker `Modal`. A
|
|
351
353
|
per-phase dot on a rail item carries its confirmed state. A direction that is a TYPE (one record
|
|
352
354
|
per event) is instead a discriminator chosen ONCE at creation — a `SegmentedControl` in the
|
package/docs/templates.md
CHANGED
|
@@ -392,8 +392,9 @@ billing, and quick-capture templates. Top → bottom:
|
|
|
392
392
|
section; the fenced red `DangerZone` card is the section's BODY.
|
|
393
393
|
- A LEFT OUTLINE RAIL (`MenuButton` + `useSectionNav`) — **the rail is the page's COMPLETE
|
|
394
394
|
map** (every section including Danger zone; subsections never join it) — headed by the
|
|
395
|
-
bare circular `BackButton`; it scroll-spies the active section
|
|
396
|
-
|
|
395
|
+
bare circular `BackButton`; it scroll-spies the active section (`current={activeKey === key}`
|
|
396
|
+
on each item — navigation, not selection), and on narrow containers becomes the pinned
|
|
397
|
+
current-section bar opening a section-picker `Modal` whose items carry the same `current`.
|
|
397
398
|
- **The page layout is the DOCS layout — the reading column is CENTRED IN THE VIEWPORT, and
|
|
398
399
|
the rail sits in its left gutter.** The `ScrollView` is FULL-WIDTH — **never cap it at
|
|
399
400
|
`CONTENT_MAX`, or the scroll container becomes the column and the gutters go wheel-dead**
|
package/examples/tpl_record.tsx
CHANGED
|
@@ -2552,7 +2552,7 @@ export function TplRecord() {
|
|
|
2552
2552
|
key={sec.key}
|
|
2553
2553
|
icon={sec.icon}
|
|
2554
2554
|
title={sec.label}
|
|
2555
|
-
|
|
2555
|
+
current={nav.activeKey === sec.key}
|
|
2556
2556
|
onPress={() => {
|
|
2557
2557
|
setSectionsOpen(false);
|
|
2558
2558
|
nav.jumpTo(sec.key);
|
|
@@ -3075,7 +3075,7 @@ export function TplRecord() {
|
|
|
3075
3075
|
<BackButton label="Records" onPress={() => {}} />
|
|
3076
3076
|
<View style={{ gap: 2 }}>
|
|
3077
3077
|
{SECTIONS.map((sec) => (
|
|
3078
|
-
<MenuButton key={sec.key} icon={sec.icon} title={sec.label}
|
|
3078
|
+
<MenuButton key={sec.key} icon={sec.icon} title={sec.label} current={nav.activeKey === sec.key} onPress={() => nav.jumpTo(sec.key)} />
|
|
3079
3079
|
))}
|
|
3080
3080
|
</View>
|
|
3081
3081
|
</View>
|
package/package.json
CHANGED
package/src/combobox.tsx
CHANGED
|
@@ -360,12 +360,13 @@ export interface ComboboxInputProps {
|
|
|
360
360
|
export function ComboboxInput(props: ComboboxInputProps) {
|
|
361
361
|
const { icon, placeholder, clearable = false, onClear, clearLabel, accessibilityLabel, testID } = props;
|
|
362
362
|
const ctx = useComboboxContext("ComboboxInput");
|
|
363
|
-
//
|
|
364
|
-
// carries text (a reflected selection or a live query) AND it's clearable
|
|
365
|
-
//
|
|
366
|
-
// The
|
|
367
|
-
//
|
|
368
|
-
//
|
|
363
|
+
// `showClear` MIRRORS `TextInputField`'s own clear-✕ condition — the field
|
|
364
|
+
// carries text (a reflected selection or a live query) AND it's clearable — so
|
|
365
|
+
// the ✕ that renders there and the chevron decided here can never both be up.
|
|
366
|
+
// The chevron is what THIS file owns, and it is select-mode only (no leading
|
|
367
|
+
// icon), so an EMPTY picker still reads as a picker; ✕ replaces it once there's
|
|
368
|
+
// something to clear, and pressing that resets the query + reopens the browse
|
|
369
|
+
// list (via `handleChangeText("")`) so `onClear` lands back on the full set.
|
|
369
370
|
const showClear = clearable && ctx.query.length > 0;
|
|
370
371
|
const showChevron = !icon && !showClear;
|
|
371
372
|
|
|
@@ -391,9 +392,12 @@ export function ComboboxInput(props: ComboboxInputProps) {
|
|
|
391
392
|
autoCorrect={false}
|
|
392
393
|
accessibilityLabel={accessibilityLabel}
|
|
393
394
|
style={[
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
|
|
395
|
+
// Only the CHEVRON's gutter belongs here — it is the one trailing
|
|
396
|
+
// affordance this file owns. `TextInputField` reserves its own slot for
|
|
397
|
+
// the clear ✕, so gating this on "select mode" instead would both
|
|
398
|
+
// over-ride that reserve with the wrong width and leave a SEARCH-mode
|
|
399
|
+
// (`icon`) combobox with no gutter at all.
|
|
400
|
+
showChevron ? styles.withChevron : undefined,
|
|
397
401
|
// Gate on `showList`, not `open`: an empty query with no options has
|
|
398
402
|
// `open=true` but renders NO dropdown, so a ring tied to `open` would
|
|
399
403
|
// show with nothing below it AND linger after blur (no popover fires a
|
|
@@ -408,7 +412,7 @@ export function ComboboxInput(props: ComboboxInputProps) {
|
|
|
408
412
|
/>
|
|
409
413
|
{showChevron ? (
|
|
410
414
|
<View style={styles.chevron}>
|
|
411
|
-
<Icon name="chevrons-up-down" size={
|
|
415
|
+
<Icon name="chevrons-up-down" size={CHEVRON_SIZE} color={colors.zinc["400"]} />
|
|
412
416
|
</View>
|
|
413
417
|
) : null}
|
|
414
418
|
</View>
|
|
@@ -562,17 +566,23 @@ export function ComboboxFooter(props: { children: ReactNode }) {
|
|
|
562
566
|
return <View style={styles.footer}>{props.children}</View>;
|
|
563
567
|
}
|
|
564
568
|
|
|
569
|
+
/** The chevron is absolutely positioned — out of flow — so the text reserves the
|
|
570
|
+
* span it occupies: the offset, the glyph, and a gap (a bare `Icon` carries no
|
|
571
|
+
* interior padding of its own). One source for the offset and the reserve. */
|
|
572
|
+
const CHEVRON_INSET = 10;
|
|
573
|
+
const CHEVRON_SIZE = 16;
|
|
574
|
+
const CHEVRON_GUTTER = CHEVRON_INSET + CHEVRON_SIZE + 6;
|
|
575
|
+
|
|
565
576
|
const styles = StyleSheet.create({
|
|
566
577
|
openRing: {
|
|
567
578
|
boxShadow: FOCUS_RING,
|
|
568
579
|
},
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
paddingRight: 32,
|
|
580
|
+
withChevron: {
|
|
581
|
+
paddingRight: CHEVRON_GUTTER,
|
|
572
582
|
},
|
|
573
583
|
chevron: {
|
|
574
584
|
position: "absolute",
|
|
575
|
-
right:
|
|
585
|
+
right: CHEVRON_INSET,
|
|
576
586
|
top: 0,
|
|
577
587
|
bottom: 0,
|
|
578
588
|
justifyContent: "center",
|
package/src/date_calendar.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import { IconButton } from "./icon_button";
|
|
|
8
8
|
import { FOCUS_RING } from "./control_surface";
|
|
9
9
|
import { useFocusRing } from "./use_focus_ring";
|
|
10
10
|
import { useLoticsLocale, useLocaleTag } from "./locale";
|
|
11
|
+
import { nextRangeSelection } from "./date_range_selection";
|
|
11
12
|
|
|
12
13
|
/** Accessible names for the calendar's month-navigation arrows. Backs the
|
|
13
14
|
* `calendar` locale slice. (The month/weekday NAMES come from `Intl` + the
|
|
@@ -529,28 +530,14 @@ function RangeCalendar(props: RangeCalendarInternalProps) {
|
|
|
529
530
|
const rightYear = leftMonth === 11 ? leftYear + 1 : leftYear;
|
|
530
531
|
const rightMonth = leftMonth === 11 ? 0 : leftMonth + 1;
|
|
531
532
|
|
|
532
|
-
//
|
|
533
|
-
|
|
534
|
-
|
|
533
|
+
// Which half of the range a click lands on is READ from the value (a range is
|
|
534
|
+
// open exactly when it has a start and no end), never tracked alongside it —
|
|
535
|
+
// a separate flag can disagree with the value the parent actually holds.
|
|
535
536
|
const handleDateSelect = useCallback(
|
|
536
537
|
(date: Date) => {
|
|
537
|
-
|
|
538
|
-
// Start new selection
|
|
539
|
-
onValueChange({ start: date, end: null });
|
|
540
|
-
setSelectingEnd(true);
|
|
541
|
-
} else {
|
|
542
|
-
// Complete selection
|
|
543
|
-
const start = value.start;
|
|
544
|
-
// Ensure start is before end
|
|
545
|
-
if (date < start) {
|
|
546
|
-
onValueChange({ start: date, end: start });
|
|
547
|
-
} else {
|
|
548
|
-
onValueChange({ start, end: date });
|
|
549
|
-
}
|
|
550
|
-
setSelectingEnd(false);
|
|
551
|
-
}
|
|
538
|
+
onValueChange(nextRangeSelection(value, date));
|
|
552
539
|
},
|
|
553
|
-
[value, onValueChange
|
|
540
|
+
[value, onValueChange],
|
|
554
541
|
);
|
|
555
542
|
|
|
556
543
|
const handleMonthChange = useCallback((year: number, month: number) => {
|