@cahyo-dimas/freeday 1.54.0 → 2.0.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/CHANGELOG.md +625 -545
- package/COMPONENTS.md +141 -138
- package/README.id.md +46 -45
- package/README.md +46 -43
- package/USAGE.md +34 -34
- package/adapters/blazor/FdyAppShell.razor +2 -2
- package/adapters/blazor/FdyAppShell.razor.cs +6 -6
- package/adapters/blazor/FdyCfl.razor.cs +1 -1
- package/adapters/blazor/FdyChart.razor +1 -1
- package/adapters/blazor/FdyChart.razor.cs +3 -3
- package/adapters/blazor/FdyChartSeries.cs +1 -1
- package/adapters/blazor/FdyCombo.razor.cs +2 -2
- package/adapters/blazor/FdyModal.razor +1 -1
- package/adapters/blazor/FdyTable.razor.cs +7 -7
- package/adapters/blazor/FdyTableFilter.razor +1 -1
- package/adapters/blazor/FdyTableFooter.razor +1 -1
- package/adapters/blazor/FdyTableFooter.razor.cs +4 -4
- package/adapters/blazor/Freeday.Blazor.csproj +1 -1
- package/adapters/blazor/FreedayComponentBase.cs +1 -1
- package/adapters/blazor/TableModel.cs +3 -3
- package/adapters/blazor/TableTypes.cs +3 -3
- package/adapters/blazor/freeday-blazor.js +5 -5
- package/adapters/core/app-shell.d.ts +1 -1
- package/adapters/core/app-shell.js +6 -6
- package/adapters/core/table-model.d.ts +2 -2
- package/adapters/core/table-model.js +8 -8
- package/adapters/react/components/FdyAppShell.tsx +5 -5
- package/adapters/react/components/FdyAutocomplete.tsx +1 -1
- package/adapters/react/components/FdyCascade.tsx +1 -1
- package/adapters/react/components/FdyCfl.tsx +11 -11
- package/adapters/react/components/FdyDateRange.tsx +2 -2
- package/adapters/react/components/FdyDatepicker.tsx +5 -5
- package/adapters/react/components/FdyDrawer.tsx +1 -1
- package/adapters/react/components/FdyModal.tsx +2 -2
- package/adapters/react/components/FdyTable.tsx +7 -7
- package/adapters/react/components/FdyTableFilter.tsx +1 -1
- package/adapters/react/components/FdyTableFooter.tsx +5 -5
- package/adapters/react/index.d.ts +1 -1
- package/adapters/react/useFreeday.js +3 -3
- package/adapters/vue/components/FdyAppShell.vue +5 -5
- package/adapters/vue/components/FdyAutocomplete.vue +1 -1
- package/adapters/vue/components/FdyCascade.vue +1 -1
- package/adapters/vue/components/FdyCfl.vue +8 -8
- package/adapters/vue/components/FdyChart.vue +2 -2
- package/adapters/vue/components/FdyCombo.vue +1 -1
- package/adapters/vue/components/FdyDateRange.vue +2 -2
- package/adapters/vue/components/FdyDatepicker.vue +5 -5
- package/adapters/vue/components/FdyDrawer.vue +3 -3
- package/adapters/vue/components/FdyModal.vue +3 -3
- package/adapters/vue/components/FdyTable.vue +7 -7
- package/adapters/vue/components/FdyTableFilter.vue +1 -1
- package/adapters/vue/components/FdyTableFooter.vue +5 -5
- package/adapters/vue/index.d.ts +2 -2
- package/adapters/vue/useFreeday.js +3 -3
- package/dist/asset.d.ts +3 -3
- package/dist/freeday-app-shell.js +9 -9
- package/dist/freeday-autocomplete.js +2 -2
- package/dist/freeday-breakpoint.js +1 -1
- package/dist/freeday-carousel.js +5 -5
- package/dist/freeday-cascade.js +7 -7
- package/dist/freeday-cfl.js +9 -9
- package/dist/freeday-chart.js +11 -11
- package/dist/freeday-chip.js +3 -3
- package/dist/freeday-datepicker.js +11 -7
- package/dist/freeday-datetime.js +5 -5
- package/dist/freeday-drawer.js +1 -1
- package/dist/freeday-form.js +14 -14
- package/dist/freeday-mask.js +6 -6
- package/dist/freeday-menu.js +1 -1
- package/dist/freeday-number.js +3 -3
- package/dist/freeday-popover.js +3 -3
- package/dist/freeday-rating.js +2 -2
- package/dist/freeday-select.js +5 -5
- package/dist/freeday-slider.js +2 -2
- package/dist/freeday-stepper.js +7 -7
- package/dist/freeday-table.js +15 -15
- package/dist/freeday-tabs.js +1 -1
- package/dist/freeday-timepicker.js +2 -2
- package/dist/freeday-toast.js +8 -8
- package/dist/freeday-tree.js +3 -3
- package/dist/freeday-upload.js +25 -25
- package/dist/freeday.bundle.css +176 -176
- package/dist/freeday.css +170 -170
- package/dist/freeday.d.ts +2 -2
- package/dist/freeday.js +162 -158
- package/dist/freeday.tokens.css +5 -5
- package/docs/agent-onboarding.md +41 -41
- package/docs/getting-started.md +35 -35
- package/docs/integrations.md +18 -18
- package/docs/reference-screen.html +14 -14
- package/package.json +4 -3
- package/src/base.css +6 -6
- package/src/components/accordion.css +4 -4
- package/src/components/alert.css +1 -1
- package/src/components/app-shell.css +10 -10
- package/src/components/appbar.css +3 -3
- package/src/components/autocomplete.css +1 -1
- package/src/components/avatar.css +3 -3
- package/src/components/badge.css +5 -5
- package/src/components/breadcrumb.css +1 -1
- package/src/components/breakpoints.css +4 -4
- package/src/components/button.css +13 -13
- package/src/components/card.css +8 -8
- package/src/components/carousel.css +2 -2
- package/src/components/cascade.css +2 -2
- package/src/components/cfl.css +4 -4
- package/src/components/chart.css +3 -3
- package/src/components/chip.css +2 -2
- package/src/components/combo.css +3 -3
- package/src/components/composition.css +7 -7
- package/src/components/datepicker.css +6 -6
- package/src/components/datetimepicker.css +1 -1
- package/src/components/description-list.css +1 -1
- package/src/components/divider.css +1 -1
- package/src/components/drawer.css +1 -1
- package/src/components/file-upload.css +5 -5
- package/src/components/filterbar.css +7 -7
- package/src/components/form-grid.css +2 -2
- package/src/components/input-group.css +6 -6
- package/src/components/input.css +6 -6
- package/src/components/kbd.css +1 -1
- package/src/components/list.css +6 -6
- package/src/components/menu.css +3 -3
- package/src/components/modal.css +1 -1
- package/src/components/pagination.css +1 -1
- package/src/components/progress.css +1 -1
- package/src/components/rating.css +2 -2
- package/src/components/selection.css +4 -4
- package/src/components/skeleton.css +1 -1
- package/src/components/slider.css +1 -1
- package/src/components/spinner.css +1 -1
- package/src/components/states.css +1 -1
- package/src/components/stepper.css +2 -2
- package/src/components/table.css +15 -15
- package/src/components/tabs.css +3 -3
- package/src/components/timeline.css +1 -1
- package/src/components/timepicker.css +1 -1
- package/src/components/toast.css +3 -3
- package/src/components/tooltip.css +1 -1
- package/src/components/tree.css +2 -2
- package/tokens/breakpoints.d.ts +2 -2
- package/tokens/breakpoints.mjs +3 -3
- package/tokens/tokens.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// Freeday
|
|
1
|
+
// Freeday app-shell behaviour, framework-agnostic (DOM helpers, zero dependencies).
|
|
2
2
|
//
|
|
3
3
|
// The focus trap, `inert` bookkeeping and focus restore shared by FdyAppShell in the Vue, React and
|
|
4
|
-
// Blazor adapters. Kept here
|
|
4
|
+
// Blazor adapters. Kept here, plain ESM with a .d.ts sidecar, for the same reason as
|
|
5
5
|
// table-model.js: three copies of a focus trap is three chances to write it differently, and the
|
|
6
6
|
// one that gets it wrong strands a keyboard user with no way out of a nav panel.
|
|
7
7
|
//
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { breakpoints } from '../../tokens/breakpoints.mjs';
|
|
16
16
|
|
|
17
|
-
/** The media query the shell switches at
|
|
17
|
+
/** The media query the shell switches at, the same 721px app-shell.css uses. */
|
|
18
18
|
export const NAV_QUERY = `(min-width: ${breakpoints.nav}px)`;
|
|
19
19
|
|
|
20
20
|
const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]),'
|
|
@@ -36,7 +36,7 @@ export function shellParts(root) {
|
|
|
36
36
|
/**
|
|
37
37
|
* Focusable descendants, in tab order, that are actually rendered.
|
|
38
38
|
* getClientRects() rather than offsetParent: an overlay sidebar is position:fixed, and a fixed
|
|
39
|
-
* element reports no offsetParent at all
|
|
39
|
+
* element reports no offsetParent at all, filtering on that calls every nav item invisible.
|
|
40
40
|
* @param {HTMLElement} el
|
|
41
41
|
* @returns {HTMLElement[]}
|
|
42
42
|
*/
|
|
@@ -47,7 +47,7 @@ export function focusablesIn(el) {
|
|
|
47
47
|
/**
|
|
48
48
|
* Reconcile the parts of the shell that are not classes: `inert` and `aria-expanded`.
|
|
49
49
|
*
|
|
50
|
-
* One rule
|
|
50
|
+
* One rule for both modes: the sidebar is inert whenever the nav is not visible. `width:0` (collapsed)
|
|
51
51
|
* and `translateX(-100%)` (off-canvas) hide a panel from the eye and neither hides it from the
|
|
52
52
|
* keyboard, so without this a nav nobody can see still swallows Tab on the way into the page.
|
|
53
53
|
* The content is inert only while the nav is an open overlay.
|
|
@@ -63,7 +63,7 @@ export function applyShellState(root, state) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
|
-
* Move focus into the panel, and hand back whatever had it
|
|
66
|
+
* Move focus into the panel, and hand back whatever had it. The caller keeps that until close.
|
|
67
67
|
* @param {HTMLElement} root
|
|
68
68
|
* @returns {Element|null} the element that was focused before
|
|
69
69
|
*/
|
|
@@ -9,7 +9,7 @@ export interface FdySortState {
|
|
|
9
9
|
dir: FdySortDir;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
/** A column's comparator type
|
|
12
|
+
/** A column's comparator type, how its values are ordered when sorting. */
|
|
13
13
|
export type FdyColumnType = 'text' | 'number' | 'date';
|
|
14
14
|
|
|
15
15
|
/** Cell alignment. Maps to `text-align` on the header and body cells. */
|
|
@@ -25,7 +25,7 @@ export interface FdyTableColumn<T> {
|
|
|
25
25
|
/** Header label. */
|
|
26
26
|
label: string;
|
|
27
27
|
/**
|
|
28
|
-
* Render the label for assistive tech only
|
|
28
|
+
* Render the label for assistive tech only, the header cell looks empty.
|
|
29
29
|
*
|
|
30
30
|
* For a column of row CONTROLS (an edit button, a row menu), where a visible
|
|
31
31
|
* heading is noise above a column of icons but the column still has to be
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// Freeday
|
|
1
|
+
// Freeday, framework-agnostic table model (pure functions, zero dependencies).
|
|
2
2
|
//
|
|
3
3
|
// The sort / filter / paginate logic shared by adapters/vue/components/FdyTable.vue and
|
|
4
|
-
// adapters/react/components/FdyTable.tsx. Kept here
|
|
4
|
+
// adapters/react/components/FdyTable.tsx. Kept here, plain ESM with a .d.ts sidecar, so the
|
|
5
5
|
// two adapters can't drift and so the logic is unit-testable under `node --test`
|
|
6
6
|
// (test/table-model.test.mjs) without a framework runtime. Every function is pure: inputs are
|
|
7
7
|
// never mutated (rows are sliced before sorting), so it is safe to call on each render.
|
|
@@ -34,15 +34,15 @@ function localDay(d) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* @param {unknown} v @returns {string} ISO calendar day (yyyy-mm-dd)
|
|
37
|
+
* @param {unknown} v @returns {string} ISO calendar day (yyyy-mm-dd), lexicographically comparable
|
|
38
38
|
*
|
|
39
39
|
* Two bugs lived here, both of which let a column SORT by date correctly and
|
|
40
|
-
* FILTER by date wrongly
|
|
40
|
+
* FILTER by date wrongly, the worst pairing, because a working sort is what
|
|
41
41
|
* persuades you the header understands dates.
|
|
42
42
|
*
|
|
43
43
|
* 1. It sliced instead of parsing, while `toTime` (which the date SORT uses)
|
|
44
|
-
* parses. A column whose `value` returns a formatted date
|
|
45
|
-
* to render one
|
|
44
|
+
* parses. A column whose `value` returns a formatted date, the normal way
|
|
45
|
+
* to render one, gave `"18 Mar 2024"`, sliced to `"18 Mar 202"`, and
|
|
46
46
|
* compared as text against `"2024-03-18"`: every row failed, silently.
|
|
47
47
|
* 2. A `Date` went through `toISOString`, which is UTC. At UTC+7 a date picked
|
|
48
48
|
* as the 18th is `2024-03-17T17:00Z`, so filtering from the 18th dropped it.
|
|
@@ -155,7 +155,7 @@ export function paginate(rows, pageIndex, pageSize) {
|
|
|
155
155
|
return rows.slice(start, start + pageSize);
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
/** Distinct non-empty cell texts for a column, naturally sorted
|
|
158
|
+
/** Distinct non-empty cell texts for a column, naturally sorted, the source for an enum filter. */
|
|
159
159
|
export function distinctValues(rows, column) {
|
|
160
160
|
const seen = new Set();
|
|
161
161
|
const out = [];
|
|
@@ -173,7 +173,7 @@ export function distinctValues(rows, column) {
|
|
|
173
173
|
* The page to land on when the page SIZE changes: whichever page still holds the first row you were
|
|
174
174
|
* already looking at.
|
|
175
175
|
*
|
|
176
|
-
* The two obvious answers are both wrong. Jumping to page 1 throws away your place on a long list
|
|
176
|
+
* The two obvious answers are both wrong. Jumping to page 1 throws away your place on a long list,
|
|
177
177
|
* you asked to see more rows, not to start over. Keeping the same INDEX can land past the end: page
|
|
178
178
|
* 5 of 5 at twenty rows is page 2 of 2 at fifty, and index 4 is nowhere. Anchoring on the first
|
|
179
179
|
* visible row is the only one that always resolves, and it is what the reader expects: the row they
|
|
@@ -13,11 +13,11 @@ import {
|
|
|
13
13
|
// drift into two different focus traps.
|
|
14
14
|
//
|
|
15
15
|
// ONE model: `navOpen` means "the nav is visible to the reader". Above the nav breakpoint a hidden
|
|
16
|
-
// nav is `--nav-collapsed`, below it a visible nav is `--nav-open
|
|
16
|
+
// nav is `--nav-collapsed`, below it a visible nav is `--nav-open`, the kit owns that mapping so an
|
|
17
17
|
// app never reasons about the viewport to answer a question about its own UI.
|
|
18
18
|
//
|
|
19
19
|
// Omit `navOpen` and the component keeps its own, defaulting BY VIEWPORT: a column on a wide screen,
|
|
20
|
-
// hidden on a narrow one. That default is why the prop is optional
|
|
20
|
+
// hidden on a narrow one. That default is why the prop is optional, a parent cannot express it as a
|
|
21
21
|
// single initial value before it knows the viewport.
|
|
22
22
|
|
|
23
23
|
export interface FdyAppShellProps {
|
|
@@ -52,7 +52,7 @@ export function FdyAppShell(props: FdyAppShellProps): JSX.Element {
|
|
|
52
52
|
|
|
53
53
|
/* The media listener reads these through refs and subscribes ONCE. Depending on navVisible would
|
|
54
54
|
resubscribe on every toggle and re-run the handler, and its "narrowing with the nav visible
|
|
55
|
-
hides it" rule would then fire on an ordinary open
|
|
55
|
+
hides it" rule would then fire on an ordinary open, closing the panel in the same tick the
|
|
56
56
|
reader opened it. */
|
|
57
57
|
const navVisibleRef = useRef<boolean>(navVisible);
|
|
58
58
|
navVisibleRef.current = navVisible;
|
|
@@ -72,7 +72,7 @@ export function FdyAppShell(props: FdyAppShellProps): JSX.Element {
|
|
|
72
72
|
});
|
|
73
73
|
if (nowOverlay && navVisibleRef.current) setVisibleRef.current(false);
|
|
74
74
|
};
|
|
75
|
-
// Mount: adopt the viewport without the hide-on-narrow side effect
|
|
75
|
+
// Mount: adopt the viewport without the hide-on-narrow side effect, nothing is open yet.
|
|
76
76
|
setOverlay(!media.matches);
|
|
77
77
|
if (!controlled) setUncontrolled(media.matches);
|
|
78
78
|
media.addEventListener('change', onChange);
|
|
@@ -120,7 +120,7 @@ export function FdyAppShell(props: FdyAppShellProps): JSX.Element {
|
|
|
120
120
|
<aside
|
|
121
121
|
className="fdy-app__sidebar"
|
|
122
122
|
onClick={(e): void => {
|
|
123
|
-
// Following a link in an overlay nav means "take me there"
|
|
123
|
+
// Following a link in an overlay nav means "take me there", the panel must not stay over
|
|
124
124
|
// the page it was just asked for. On a wide viewport the nav is a column: nothing to close.
|
|
125
125
|
if (!overlay || !navVisible) return;
|
|
126
126
|
const target: HTMLElement | null = e.target as HTMLElement | null;
|
|
@@ -3,7 +3,7 @@ import { useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
|
3
3
|
import { usePopover } from '../usePopover';
|
|
4
4
|
|
|
5
5
|
// A controlled React port of freeday's autocomplete (src/freeday-autocomplete.js +
|
|
6
|
-
// autocomplete.css): a WAI-ARIA APG *editable* combobox
|
|
6
|
+
// autocomplete.css): a WAI-ARIA APG *editable* combobox, a text input filters a listbox of
|
|
7
7
|
// options as you type; picking one fills the input. `value` + `onChange` in place of the
|
|
8
8
|
// enhancer's DOM mutation, so React owns the input. Filtering mirrors the enhancer exactly
|
|
9
9
|
// (case-insensitive substring on the trimmed query); pass server-filtered `options` and it
|
|
@@ -3,7 +3,7 @@ import { useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
|
3
3
|
import { usePopover } from '../usePopover';
|
|
4
4
|
|
|
5
5
|
// A controlled React port of freeday's cascade select (src/freeday-cascade.js + cascade.css):
|
|
6
|
-
// a hierarchical drill-down picker showing one level at a time
|
|
6
|
+
// a hierarchical drill-down picker showing one level at a time, branches drill in, a back
|
|
7
7
|
// control ascends, a leaf selects and the value is the leaf's value (the display is the full
|
|
8
8
|
// path). The enhancer's data model is a hidden nested <ul>; here it is a typed tree, which is
|
|
9
9
|
// what a framework app actually has. `value` + `onChange` in place of the DOM mutation.
|
|
@@ -21,7 +21,7 @@ export interface CflPage<Row> {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export interface FdyCflProps<Row extends Record<string, unknown>> {
|
|
24
|
-
/** Single: `Row | null`. With `multiple`, an array
|
|
24
|
+
/** Single: `Row | null`. With `multiple`, an array, `Row[] | null`, where null and [] both mean
|
|
25
25
|
* nothing picked. The enhancer has had `data-fdy-cfl-multiple` all along; this is the typed
|
|
26
26
|
* wrappers catching up (#019). */
|
|
27
27
|
value: Row | Row[] | null;
|
|
@@ -30,7 +30,7 @@ export interface FdyCflProps<Row extends Record<string, unknown>> {
|
|
|
30
30
|
columns: ReadonlyArray<CflColumn<Row>>;
|
|
31
31
|
display: (row: Row) => string;
|
|
32
32
|
rowKey: (row: Row) => string;
|
|
33
|
-
/** Advisory only
|
|
33
|
+
/** Advisory only, the caller's `fetchPage` owns paging; kept for API documentation. */
|
|
34
34
|
pageSize?: number;
|
|
35
35
|
/** The dialog's heading. Default 'Choose data', matching the Blazor adapter's `Title`. */
|
|
36
36
|
title?: string;
|
|
@@ -81,7 +81,7 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
81
81
|
const rowId = (index: number): string => `${baseId}-row-${index}`;
|
|
82
82
|
|
|
83
83
|
/* Unsetting is not "picking nothing": it must not touch the dialog, and focus must land on a
|
|
84
|
-
control that still exists
|
|
84
|
+
control that still exists, the trigger beside it, since this button disappears with the value. */
|
|
85
85
|
const clearLabelText: string = props.clearLabel ?? 'Clear selection';
|
|
86
86
|
const clearValue = (): void => {
|
|
87
87
|
setPicked([]);
|
|
@@ -106,7 +106,7 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
106
106
|
|
|
107
107
|
// Out-of-order guard: every fetch takes a monotonically increasing token; a resolved or
|
|
108
108
|
// rejected page is applied only if it still owns the latest token. A newer search, a
|
|
109
|
-
// load-more, a dialog close, or an unmount all bump it
|
|
109
|
+
// load-more, a dialog close, or an unmount all bump it, this keeps a slow stale response
|
|
110
110
|
// from overwriting fresh results (or a closed/unmounted component's state).
|
|
111
111
|
const reqIdRef = useRef<number>(0);
|
|
112
112
|
// In-memory cache keyed by `${query}::${page}`, cleared on open and on close.
|
|
@@ -117,7 +117,7 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
117
117
|
|
|
118
118
|
const isDisabled: boolean = props.disabled === true;
|
|
119
119
|
const isReadonly: boolean = props.readonly === true;
|
|
120
|
-
/* `display()` takes one row, so in multi the field states HOW MANY
|
|
120
|
+
/* `display()` takes one row, so in multi the field states HOW MANY, naming one of six would be a
|
|
121
121
|
lie, and naming all six does not fit a control that is 22rem wide. */
|
|
122
122
|
const currentRows: Row[] = Array.isArray(props.value)
|
|
123
123
|
? props.value
|
|
@@ -137,7 +137,7 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
137
137
|
? props.display(props.value as Row)
|
|
138
138
|
: '';
|
|
139
139
|
// The results <table> (owner of `resultsId`) only renders in the rows branch, so gate the
|
|
140
|
-
// search input's aria refs on rows existing
|
|
140
|
+
// search input's aria refs on rows existing, otherwise they'd dangle during loading/empty/error.
|
|
141
141
|
const hasRows: boolean = rows.length > 0;
|
|
142
142
|
const controlsId: string | undefined = hasRows ? resultsId : undefined;
|
|
143
143
|
const activeDescendant: string | undefined = hasRows && activeIndex >= 0 ? rowId(activeIndex) : undefined;
|
|
@@ -156,7 +156,7 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
156
156
|
try {
|
|
157
157
|
const cached: CflPage<Row> | undefined = cacheRef.current.get(key);
|
|
158
158
|
const res: CflPage<Row> = cached ?? (await props.fetchPage(targetQuery, targetPage));
|
|
159
|
-
if (token !== reqIdRef.current) return; // stale
|
|
159
|
+
if (token !== reqIdRef.current) return; // stale, a newer request has started
|
|
160
160
|
if (cached === undefined) cacheRef.current.set(key, res);
|
|
161
161
|
const copy: Row[] = res.rows.slice(); // never mutate the caller's array
|
|
162
162
|
setRows((prev: Row[]): Row[] => (append ? prev.concat(copy) : copy));
|
|
@@ -171,7 +171,7 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
// Mirrors Vue's `retry()`: only `lastPage`/`lastAppend` are frozen from the failed request
|
|
174
|
+
// Mirrors Vue's `retry()`: only `lastPage`/`lastAppend` are frozen from the failed request,
|
|
175
175
|
// the query is re-read live so a retry after editing the search box (within the debounce
|
|
176
176
|
// window) uses what's currently typed, not a stale snapshot.
|
|
177
177
|
function retry(): void {
|
|
@@ -203,7 +203,7 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
203
203
|
setActiveIndex(clamped);
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
// Scroll the active row into view once it (re)renders
|
|
206
|
+
// Scroll the active row into view once it (re)renders. React's analogue of Vue's
|
|
207
207
|
// `nextTick(() => scrollIntoView(...))`; fires for both keyboard nav and row hover, same as
|
|
208
208
|
// the Vue source's `setActive`.
|
|
209
209
|
useEffect((): void => {
|
|
@@ -225,7 +225,7 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
225
225
|
);
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
/* A click means "tick this" in multi and "this is my answer" in single
|
|
228
|
+
/* A click means "tick this" in multi and "this is my answer" in single, the whole difference. */
|
|
229
229
|
function onRowClick(row: Row): void {
|
|
230
230
|
if (isMultiple) togglePick(row);
|
|
231
231
|
else commit(row);
|
|
@@ -321,7 +321,7 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
321
321
|
}
|
|
322
322
|
|
|
323
323
|
// Unmount safety: bump the token so any in-flight fetch's setState is dropped, and cancel a
|
|
324
|
-
// pending debounce
|
|
324
|
+
// pending debounce, mirrors Vue's onBeforeUnmount plus the same token trick onClose uses.
|
|
325
325
|
useEffect((): (() => void) => {
|
|
326
326
|
return (): void => {
|
|
327
327
|
reqIdRef.current++;
|
|
@@ -5,7 +5,7 @@ import { FdyDatepicker } from './FdyDatepicker';
|
|
|
5
5
|
// two linked FdyDatepickers where the end can't precede the start (start.max = end, end.min = start).
|
|
6
6
|
// `value` + `onChange` in place of Vue's `v-model`; this composes the single-date picker rather than
|
|
7
7
|
// re-implementing the calendar. Note: the vanilla enhancer's cross-calendar `.in-range` day shading
|
|
8
|
-
// is not reproduced (each picker is independent)
|
|
8
|
+
// is not reproduced (each picker is independent), the min/max linkage is what keeps the range valid.
|
|
9
9
|
|
|
10
10
|
export interface DateRangeValue {
|
|
11
11
|
start: string | null;
|
|
@@ -19,7 +19,7 @@ export interface FdyDateRangeProps {
|
|
|
19
19
|
max?: string;
|
|
20
20
|
locale?: string;
|
|
21
21
|
disabled?: boolean;
|
|
22
|
-
/** Locked/view mode for both pickers
|
|
22
|
+
/** Locked/view mode for both pickers, focusable, values shown, but can't be opened or changed. */
|
|
23
23
|
readonly?: boolean;
|
|
24
24
|
invalid?: boolean;
|
|
25
25
|
describedby?: string;
|
|
@@ -55,7 +55,7 @@ export interface FdyDatepickerProps {
|
|
|
55
55
|
ariaLabelledby?: string;
|
|
56
56
|
/** Show a clear (×) button in the trigger when a date is set, so an optional date can be unset. Calls onChange('') to reset. Off by default. */
|
|
57
57
|
clearable?: boolean;
|
|
58
|
-
/** aria-label for the previous-month nav button. Default 'Previous month'
|
|
58
|
+
/** aria-label for the previous-month nav button. Default 'Previous month', override for non-English UIs (month/weekday names already follow `locale`). */
|
|
59
59
|
/** aria-label for the title button that drills to the month grid. Default 'Choose month'. */
|
|
60
60
|
chooseMonthLabel?: string;
|
|
61
61
|
/** aria-labels for the year arrows shown in the month grid. Defaults 'Previous year' / 'Next year'. */
|
|
@@ -132,7 +132,7 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
132
132
|
const pendingFocusRef = useRef<string | null>(null);
|
|
133
133
|
|
|
134
134
|
const [open, setOpen] = useState<boolean>(false);
|
|
135
|
-
/* 'days' | 'months'
|
|
135
|
+
/* 'days' | 'months', the calendar drills one level up instead of growing furniture beside the
|
|
136
136
|
title. Before this the only pointer route to another month was one click per month. */
|
|
137
137
|
const [mode, setMode] = useState<'days' | 'months' | 'years'>('days');
|
|
138
138
|
const [focusMonth, setFocusMonth] = useState<number>(0);
|
|
@@ -224,7 +224,7 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
224
224
|
return false;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
/** A month is only unreachable when the WHOLE month falls outside min/max
|
|
227
|
+
/** A month is only unreachable when the WHOLE month falls outside min/max, a range that sits
|
|
228
228
|
* inside one month disables both ends while the middle is perfectly selectable. */
|
|
229
229
|
function isMonthDisabled(year: number, month: number): boolean {
|
|
230
230
|
const first: Date = new Date(year, month, 1);
|
|
@@ -429,7 +429,7 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
429
429
|
closePanel(true);
|
|
430
430
|
}
|
|
431
431
|
|
|
432
|
-
// Clear (reset to empty). Calls onChange('')
|
|
432
|
+
// Clear (reset to empty). Calls onChange(''), parseISO('') is null, so the placeholder shows again.
|
|
433
433
|
function clearValue(): void {
|
|
434
434
|
props.onChange('');
|
|
435
435
|
setOpen(false);
|
|
@@ -506,7 +506,7 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
506
506
|
}
|
|
507
507
|
|
|
508
508
|
// Close when focus leaves the control entirely (e.g. Shift+Tab off the trigger).
|
|
509
|
-
/* A null relatedTarget means focus fell to <body
|
|
509
|
+
/* A null relatedTarget means focus fell to <body>, which is what happens when the element the
|
|
510
510
|
user just pressed is REMOVED by the click it triggered (drilling into the month grid replaces
|
|
511
511
|
the grid, and with it the cell that had focus). That is not focus leaving the control, and
|
|
512
512
|
closing on it made the panel vanish mid-navigation. A pointer that really lands outside is
|
|
@@ -2,7 +2,7 @@ import type { JSX, ReactNode } from 'react';
|
|
|
2
2
|
import { useEffect, useId, useRef } from 'react';
|
|
3
3
|
|
|
4
4
|
// A controlled React wrapper over freeday's `.fdy-drawer` native <dialog> side panel
|
|
5
|
-
// (src/components/drawer.css). React port of adapters/vue/components/FdyDrawer.vue
|
|
5
|
+
// (src/components/drawer.css). React port of adapters/vue/components/FdyDrawer.vue, the same
|
|
6
6
|
// controlled glue as FdyModal (guarded showModal()/close(), onCancel + preventDefault so Esc routes
|
|
7
7
|
// through app state, backdrop-click via `event.target === dialogEl`) applied to a drawer that anchors
|
|
8
8
|
// left (default) or right. Native <dialog> supplies the focus trap, focus restore, top-layer stacking
|
|
@@ -3,7 +3,7 @@ import { useEffect, useId, useRef } from 'react';
|
|
|
3
3
|
|
|
4
4
|
// A controlled React wrapper over freeday's `.fdy-modal` native <dialog> (src/components/modal.css).
|
|
5
5
|
// React port of adapters/vue/components/FdyModal.vue: reconciles the reactive `open` boolean with a
|
|
6
|
-
// DOM element whose open/close is a method call
|
|
6
|
+
// DOM element whose open/close is a method call, showModal()/close() guarded against the
|
|
7
7
|
// already-open/closed cases (showModal() on an open dialog throws), onCancel + preventDefault so Esc
|
|
8
8
|
// routes through app state instead of closing the DOM behind its back, and backdrop-click detection
|
|
9
9
|
// via `event.target === dialogEl`. Native <dialog> already gives the focus trap, focus restore,
|
|
@@ -44,7 +44,7 @@ export function FdyModal(props: FdyModalProps): JSX.Element {
|
|
|
44
44
|
if (dismissible) props.onClose();
|
|
45
45
|
}}
|
|
46
46
|
onClick={(e): void => {
|
|
47
|
-
// The ::backdrop is not a separate element
|
|
47
|
+
// The ::backdrop is not a separate element, target === the dialog box means a backdrop click.
|
|
48
48
|
if (dismissible && e.target === dialogRef.current) props.onClose();
|
|
49
49
|
}}
|
|
50
50
|
>
|
|
@@ -45,14 +45,14 @@ export interface FdyTableProps<Row extends object> {
|
|
|
45
45
|
/** Client-side page size when `page` is absent; 0/undefined = render all rows (no pager). */
|
|
46
46
|
pageSize?: number;
|
|
47
47
|
/**
|
|
48
|
-
* Controlled client-side page index (0-based). Provide it
|
|
48
|
+
* Controlled client-side page index (0-based). Provide it, with `pageSize`, without `page`, to
|
|
49
49
|
* own the page while the table keeps doing filter/sort/paginate. This is what lets an EXTERNAL
|
|
50
50
|
* pager drive the table: a responsive screen that hides `.fdy-datatable` below `md` and renders a
|
|
51
51
|
* card list from `onProcess` can render one pager for both breakpoints and point it here.
|
|
52
52
|
* Omit for the internal index (unchanged default).
|
|
53
53
|
*/
|
|
54
54
|
/** Withhold the table's own footer (pager + range) so the screen can render one. Server mode had
|
|
55
|
-
* no way to do this: the app owns the page there ANYWAY, and was still handed a second control
|
|
55
|
+
* no way to do this: the app owns the page there ANYWAY, and was still handed a second control,
|
|
56
56
|
* a responsive list that shows a table at one breakpoint and cards at another ended up with the
|
|
57
57
|
* kit's pager stacked under its own. Client mode's counterpart is `pageIndex`. Default true. */
|
|
58
58
|
pager?: boolean;
|
|
@@ -61,13 +61,13 @@ export interface FdyTableProps<Row extends object> {
|
|
|
61
61
|
* (unchanged default). Every back office has one, and a table that renders two thirds of its own
|
|
62
62
|
* footer forces the app to rebuild all three to add the last (#008).
|
|
63
63
|
*
|
|
64
|
-
* Server mode reports the pick through `onPageChange
|
|
64
|
+
* Server mode reports the pick through `onPageChange`, same callback as a page click, with a new
|
|
65
65
|
* `size`. Client mode keeps it internally and also calls `onPageSizeChange`, so the control works
|
|
66
66
|
* with nothing wired and can still be persisted.
|
|
67
67
|
*/
|
|
68
68
|
pageSizes?: readonly number[];
|
|
69
69
|
/** Client mode with `pageSizes`: the reader picked a new rows-per-page. The table has already
|
|
70
|
-
* applied it
|
|
70
|
+
* applied it, this is for a caller that wants to persist the choice. */
|
|
71
71
|
onPageSizeChange?: (size: number) => void;
|
|
72
72
|
pageIndex?: number;
|
|
73
73
|
/** Client mode with `pageIndex` provided: the table asks for a new 0-based index (pager click, a
|
|
@@ -86,7 +86,7 @@ export interface FdyTableProps<Row extends object> {
|
|
|
86
86
|
rowClass?: (row: Row) => string | undefined;
|
|
87
87
|
/** A row was activated (click, or Enter/Space while the row itself is focused). */
|
|
88
88
|
onRowActivate?: (row: Row) => void;
|
|
89
|
-
/** Called with the processed page of rows (after filter/sort/paginate) plus the total row count
|
|
89
|
+
/** Called with the processed page of rows (after filter/sort/paginate) plus the total row count,
|
|
90
90
|
* in BOTH modes, whenever they change. Lets a consumer render the SAME processed set elsewhere
|
|
91
91
|
* (a `< md` card list, a "selected" summary, export-to-CSV) without re-deriving the pipeline. */
|
|
92
92
|
onProcess?: (result: { rows: Row[]; total: number }) => void;
|
|
@@ -105,7 +105,7 @@ export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Ele
|
|
|
105
105
|
const [internalFilters, setInternalFilters] = useState<FdyFilterMap>({});
|
|
106
106
|
const [internalPageIndex, setInternalPageIndex] = useState<number>(0);
|
|
107
107
|
/* Client-mode rows-per-page. `pageSize` is a plain prop with no callback, so a footer control that
|
|
108
|
-
* only reported would do nothing in the app that wired nothing
|
|
108
|
+
* only reported would do nothing in the app that wired nothing, the table applies the pick itself
|
|
109
109
|
* and reports it. `prevPageSize` is React's adjust-state-during-render idiom: an explicit change to
|
|
110
110
|
* the prop wins back, because the parent said something newer. */
|
|
111
111
|
const [internalPageSize, setInternalPageSize] = useState<number | null>(null);
|
|
@@ -238,7 +238,7 @@ export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Ele
|
|
|
238
238
|
.join(' ');
|
|
239
239
|
return cls === '' ? undefined : cls;
|
|
240
240
|
}
|
|
241
|
-
// Enter/Space activate only when the row itself is focused
|
|
241
|
+
// Enter/Space activate only when the row itself is focused, a control inside a cell keeps its own
|
|
242
242
|
// event (the `event.target !== event.currentTarget` guard). Click relies on inner controls calling
|
|
243
243
|
// stopPropagation, matching the pattern consumers hand-roll today.
|
|
244
244
|
function onRowKeydown(e: React.KeyboardEvent<HTMLTableRowElement>, row: Row): void {
|
|
@@ -7,7 +7,7 @@ import type { FdyColumnFilter, FdyColumnFilterType } from '../../core/table-mode
|
|
|
7
7
|
// Internal to FdyTable: one column's header funnel button + its type-aware filter popover
|
|
8
8
|
// (text / enum / number / date) over freeday's `.fdy-table__filterbtn` + `.fdy-filter*` classes.
|
|
9
9
|
// React port of adapters/vue/components/FdyTableFilter.vue. Reuses usePopover so the panel escapes
|
|
10
|
-
// the table's `overflow:hidden` via the top layer. Purely controlled
|
|
10
|
+
// the table's `overflow:hidden` via the top layer. Purely controlled, renders the current
|
|
11
11
|
// `filter`, emits the next one (or null to clear); the parent owns where it goes. Not exported.
|
|
12
12
|
|
|
13
13
|
export interface FdyTableFilterProps {
|
|
@@ -8,19 +8,19 @@ import { FdyCombo } from './FdyCombo';
|
|
|
8
8
|
* The band under a table: what you are looking at, how much of it you see, where you are.
|
|
9
9
|
*
|
|
10
10
|
* `FdyTable` renders this itself, so most screens never import it. It is exported for the one shape
|
|
11
|
-
* that cannot use the table's own
|
|
11
|
+
* that cannot use the table's own, a RESPONSIVE list, where a `.fdy-datatable` at `lg` and a
|
|
12
12
|
* `.fdy-list` below it are two renderings of ONE page of rows. A footer inside the table is inside
|
|
13
13
|
* the half that is hidden on a phone, so those screens need to render it once, outside both
|
|
14
14
|
* (improvement notes #005 and #008, from IDU_EMATE_APPL_WEB).
|
|
15
15
|
*
|
|
16
|
-
* It owns nothing. `page` in, `onPageChange` out
|
|
16
|
+
* It owns nothing. `page` in, `onPageChange` out, the same contract as the table's server mode.
|
|
17
17
|
*/
|
|
18
18
|
export interface FdyTableFooterProps {
|
|
19
19
|
/** The page being shown. `size` drives the range AND the rows-per-page control's value. */
|
|
20
20
|
page: FdyPageState;
|
|
21
21
|
onPageChange?: (page: FdyPageState) => void;
|
|
22
22
|
/**
|
|
23
|
-
* Offer a rows-per-page control. Omit for none
|
|
23
|
+
* Offer a rows-per-page control. Omit for none; the footer is then range + pager, exactly as
|
|
24
24
|
* before. Picking a size calls `onPageChange` with the new `size` and the index that still holds
|
|
25
25
|
* the row you were looking at.
|
|
26
26
|
*/
|
|
@@ -29,7 +29,7 @@ export interface FdyTableFooterProps {
|
|
|
29
29
|
|
|
30
30
|
export function FdyTableFooter(props: FdyTableFooterProps): JSX.Element | null {
|
|
31
31
|
const { index, size, total } = props.page;
|
|
32
|
-
/* The combo is labelled by the visible word beside it
|
|
32
|
+
/* The combo is labelled by the visible word beside it, a <label htmlFor> cannot reach inside a
|
|
33
33
|
* component, and an aria-label would leave that word attached to nothing. */
|
|
34
34
|
const sizeLabelId: string = useId();
|
|
35
35
|
|
|
@@ -41,7 +41,7 @@ export function FdyTableFooter(props: FdyTableFooterProps): JSX.Element | null {
|
|
|
41
41
|
|
|
42
42
|
const hasPager: boolean = size > 0 && totalPages > 1;
|
|
43
43
|
const sizes: readonly number[] = props.pageSizes ?? [];
|
|
44
|
-
/* One page and no size control means there is nothing here to say
|
|
44
|
+
/* One page and no size control means there is nothing here to say, the table has always withheld
|
|
45
45
|
* the whole band in that case, and this is where that decision now lives. */
|
|
46
46
|
if (!hasPager && sizes.length === 0) return null;
|
|
47
47
|
|
|
@@ -36,7 +36,7 @@ export { FdyCascade, type FdyCascadeProps, type CascadeNode } from './components
|
|
|
36
36
|
export { FdyCfl, type FdyCflProps, type CflColumn, type CflPage } from './components/FdyCfl';
|
|
37
37
|
export { FdyChart, type FdyChartProps, type FdyChartSeries } from './components/FdyChart';
|
|
38
38
|
export { FdyTable, type FdyTableProps } from './components/FdyTable';
|
|
39
|
-
/** The table's own footer, standalone
|
|
39
|
+
/** The table's own footer, standalone, for a responsive list whose table is hidden at some
|
|
40
40
|
* breakpoints and so cannot host it. */
|
|
41
41
|
export { FdyTableFooter, type FdyTableFooterProps } from './components/FdyTableFooter';
|
|
42
42
|
export { FdyModal, type FdyModalProps } from './components/FdyModal';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Freeday
|
|
1
|
+
/* Freeday. React adapter (thin). The enhancers stay the source of truth; this
|
|
2
2
|
* hook re-runs every registered window.Freeday*.initAll() over a subtree after
|
|
3
3
|
* React commits, so [data-fdy-*] markup rendered by React gets enhanced (and
|
|
4
4
|
* re-enhanced after updates). Enhancers are idempotent, so the repeated calls
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
* useFreeday(root); // scopes init to this component's subtree
|
|
10
10
|
* // <div ref={root}> ...[data-fdy-*] markup... </div>
|
|
11
11
|
*
|
|
12
|
-
* The ref may also sit ON the widget itself
|
|
12
|
+
* The ref may also sit ON the widget itself, <div ref={root} data-fdy-menu>, which is what you
|
|
13
13
|
* write when the component's root element IS the widget. initAll() matches its own root as
|
|
14
14
|
* well as descendants, so both shapes work. (Before 1.23.0 only the wrapping shape did, and
|
|
15
15
|
* the other failed silently: querySelectorAll never matches its root.)
|
|
16
16
|
*
|
|
17
|
-
* Events are native bubbling CustomEvents
|
|
17
|
+
* Events are native bubbling CustomEvents, add listeners on the root (they
|
|
18
18
|
* bubble) and read event.detail. See index.d.ts for the detail types.
|
|
19
19
|
*/
|
|
20
20
|
import { useEffect, useCallback } from 'react';
|
|
@@ -14,13 +14,13 @@ import {
|
|
|
14
14
|
// Blazor bridge cannot drift into three different focus traps.
|
|
15
15
|
//
|
|
16
16
|
// ONE model: `navOpen` means "the nav is visible to the reader". The two state classes are the
|
|
17
|
-
// kit's business
|
|
18
|
-
// nav is `--nav-open
|
|
17
|
+
// kit's business, above the nav breakpoint a hidden nav is `--nav-collapsed`, below it a visible
|
|
18
|
+
// nav is `--nav-open`, so an app never reasons about the viewport to answer a question about its
|
|
19
19
|
// own UI.
|
|
20
20
|
//
|
|
21
21
|
// Leave `navOpen` unbound and the component keeps its own: visible on a wide viewport, hidden on a
|
|
22
22
|
// narrow one, which is the right default and one an app cannot express in a single initial value.
|
|
23
|
-
// Bind it (`v-model:navOpen`) when the app wants to drive it
|
|
23
|
+
// Bind it (`v-model:navOpen`) when the app wants to drive it, a menu item, a persisted preference.
|
|
24
24
|
|
|
25
25
|
const props = withDefaults(defineProps<{
|
|
26
26
|
navOpen?: boolean;
|
|
@@ -78,7 +78,7 @@ function onBackdrop(): void {
|
|
|
78
78
|
if (overlay.value && navVisible.value) setVisible(false);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
/* Following a link in an overlay nav means "take me there"
|
|
81
|
+
/* Following a link in an overlay nav means "take me there", the panel must not stay over the page
|
|
82
82
|
it was just asked for. On a wide viewport the nav is a column and clicking it changes nothing. */
|
|
83
83
|
function onSidebarClick(e: MouseEvent): void {
|
|
84
84
|
if (!overlay.value || !navVisible.value) return;
|
|
@@ -99,7 +99,7 @@ function onKeydown(e: KeyboardEvent): void {
|
|
|
99
99
|
|
|
100
100
|
/* Both directions matter, and they are not symmetrical.
|
|
101
101
|
Narrowing with the nav visible would drop an overlay panel over a page nobody asked to leave, so
|
|
102
|
-
the nav is hidden. Widening is harmless
|
|
102
|
+
the nav is hidden. Widening is harmless, a visible nav simply becomes the column again, and the
|
|
103
103
|
watcher clears the `inert` the overlay had put on the content. */
|
|
104
104
|
function onMediaChange(): void {
|
|
105
105
|
const nowOverlay: boolean = media !== null && !media.matches;
|
|
@@ -3,7 +3,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, watch, type
|
|
|
3
3
|
import { usePopover } from '../usePopover';
|
|
4
4
|
|
|
5
5
|
// A controlled Vue port of freeday's autocomplete (src/freeday-autocomplete.js +
|
|
6
|
-
// autocomplete.css): a WAI-ARIA APG *editable* combobox
|
|
6
|
+
// autocomplete.css): a WAI-ARIA APG *editable* combobox, a text input filters a listbox of
|
|
7
7
|
// options as you type; picking one fills the input. A real `v-model` in place of the enhancer's
|
|
8
8
|
// DOM mutation, so Vue owns the input. Filtering mirrors the enhancer exactly (case-insensitive
|
|
9
9
|
// substring on the trimmed query); pass server-filtered `options` and it stays a no-op re-filter.
|
|
@@ -25,7 +25,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, watch, type
|
|
|
25
25
|
import { usePopover } from '../usePopover';
|
|
26
26
|
|
|
27
27
|
// A controlled Vue port of freeday's cascade select (src/freeday-cascade.js + cascade.css):
|
|
28
|
-
// a hierarchical drill-down picker showing one level at a time
|
|
28
|
+
// a hierarchical drill-down picker showing one level at a time, branches drill in, a back
|
|
29
29
|
// control ascends, a leaf selects and the value is the leaf's value (the display is the full
|
|
30
30
|
// path). The enhancer's data model is a hidden nested <ul>; here it is a typed tree, which is
|
|
31
31
|
// what a framework app actually has. A real `v-model` in place of the DOM mutation.
|