@cahyo-dimas/freeday 1.41.0 → 1.46.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 +131 -0
- package/COMPONENTS.md +15 -1
- package/adapters/blazor/FdyCfl.razor +24 -1
- package/adapters/blazor/FdyCfl.razor.cs +65 -0
- package/adapters/react/components/FdyCfl.tsx +82 -10
- package/adapters/vue/components/FdyCfl.vue +73 -11
- package/dist/freeday-carousel.js +11 -1
- package/dist/freeday-cascade.js +11 -1
- package/dist/freeday-cfl.js +11 -1
- package/dist/freeday-form.js +6 -1
- package/dist/freeday-mask.js +11 -1
- package/dist/freeday-stepper.js +11 -1
- package/dist/freeday-table.js +11 -1
- package/dist/freeday-toast.js +11 -1
- package/dist/freeday-upload.js +11 -1
- package/dist/freeday.bundle.css +44 -1
- package/dist/freeday.css +44 -1
- package/dist/freeday.js +94 -9
- package/package.json +2 -2
- package/src/components/badge.css +19 -0
- package/src/components/composition.css +12 -0
- package/src/components/file-upload.css +6 -1
- package/src/components/table.css +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,137 @@
|
|
|
3
3
|
Semua perubahan penting dicatat di sini. Format longgar mengikuti
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/); tiap versi = git tag.
|
|
5
5
|
|
|
6
|
+
## [1.46.0] — 2026-08-19
|
|
7
|
+
The docs site catches up with the components, and writing it out as a consumer found a defect in
|
|
8
|
+
#016's own contract.
|
|
9
|
+
### Fixed
|
|
10
|
+
- **`data-fdy-text-*` could not be written the way HTML allows** (#023). The keys in each enhancer's
|
|
11
|
+
`TEXT` table are camelCase, and `textOf` looked up `'data-fdy-text-' + key` — but **HTML lowercases
|
|
12
|
+
attribute names**, so `data-fdy-text-filterText` becomes `...filtertext` while
|
|
13
|
+
`data-fdy-text-filter-text`, the spelling anybody would actually reach for, is a DIFFERENT
|
|
14
|
+
attribute the enhancer never read. It failed silently, which is the only way an override can fail.
|
|
15
|
+
The key is kebab-cased for the lookup now; the run-together spelling still resolves, so markup
|
|
16
|
+
written against 1.39.0 keeps working. Nine enhancers.
|
|
17
|
+
It survived #016's own guard because all seven overrides that guard asserts use **single-word**
|
|
18
|
+
keys, which have no case to lose — the guard was not wrong, it was unrepresentative. The spec now
|
|
19
|
+
overrides `filterText`, reached by clicking a column filter open. Mutation-tested.
|
|
20
|
+
### Docs
|
|
21
|
+
- **`docs/index.html` was nine releases behind** — it stated v1.34.0 and demoed none of what landed
|
|
22
|
+
after it. Brought level with the components, as live demos rather than a feature list, because
|
|
23
|
+
that is what the rest of the page is:
|
|
24
|
+
- **New section — Tabel beku.** `.fdy-table--sticky` / `--sticky-head`, `.fdy-table__freeze` +
|
|
25
|
+
`--fdy-freeze-left`, `.fdy-table-scroll--frozen` + `--fdy-table-frozen-h` (1.35.0, 1.37.0), with
|
|
26
|
+
a rate matrix you can actually scroll on both axes, and the reason the two axes are separate
|
|
27
|
+
modifiers.
|
|
28
|
+
- **New section — Teks enhancer (i18n).** The `data-fdy-text-*` contract (1.39.0), demoed as the
|
|
29
|
+
same data table rendered entirely in English — the demo that found #023.
|
|
30
|
+
- **Badge tone scale** `--tone-1`…`--tone-8` (1.44.0), with the distinction the note drew: semantic
|
|
31
|
+
variants state a judgement, tones distinguish without implying one.
|
|
32
|
+
- **Date picker** now states the days → months → years drill (1.36.0); **Choose-from-list** states
|
|
33
|
+
`multiple` for the typed wrappers, not only the enhancer's `data-fdy-cfl-multiple` (1.42.0).
|
|
34
|
+
- Verified in a browser rather than by reading the markup: the i18n demo renders `Showing 1–3 of 5`
|
|
35
|
+
and a `Previous` pager, and the frozen table computes `border-collapse:separate` with
|
|
36
|
+
`position:sticky` cells.
|
|
37
|
+
|
|
38
|
+
## [1.45.0] — 2026-08-19
|
|
39
|
+
Two defects in 1.42.0's multi-select CFL, found by the app that asked for it (IDU_EMATE_APPL_WEB, #022).
|
|
40
|
+
### Fixed
|
|
41
|
+
- **The row tick rendered as the browser's checkbox, not the kit's** (#022), in all three adapters.
|
|
42
|
+
They shipped `class="fdy-check"` on a bare `<input>`; that class styles a wrapping `<label>` and
|
|
43
|
+
its `input` DESCENDANT, so on the input itself it applied `inline-flex` and a cursor and nothing
|
|
44
|
+
else — a raw UA control in the middle of a styled dialog. `COMPONENTS.md` already said which class
|
|
45
|
+
a bare input in a table takes: `.fdy-checkbox`. Every behavioural assertion in `browser/cfl-multi`
|
|
46
|
+
passed while it was wrong, because none of them looked at the rendering.
|
|
47
|
+
- **`aria-selected` broke `vue-tsc` in consuming apps** (#022). `String(isPicked(row))` widens to
|
|
48
|
+
`string`, and Vue types the attribute as `Booleanish` — so a strict app type-checking the kit's own
|
|
49
|
+
`.vue` source failed on a file it does not own. Now `'true' : 'false'`.
|
|
50
|
+
### Added — guards
|
|
51
|
+
- `browser/cfl-multi.mjs` asserts the tick's computed `appearance` is `none`, which is the tell that
|
|
52
|
+
the kit painted the box rather than the UA. Mutation-tested.
|
|
53
|
+
|
|
54
|
+
## [1.44.0] — 2026-08-19
|
|
55
|
+
One note from the back-office app (IDU_EMATE_APPL_WEB, #021).
|
|
56
|
+
### Added
|
|
57
|
+
- **`--tone-1`…`--tone-8` on `.fdy-badge`** — a status vocabulary larger than the semantic palette.
|
|
58
|
+
`.fdy-avatar--tone-*` and `.fdy-chip--tone-*` already carried the categorical scale; the badge,
|
|
59
|
+
which is the component that actually renders status, did not. The reporting app's document lists
|
|
60
|
+
carry **ten distinct statuses in one column** — Draft, Submitted, Approved, Completed, Settled,
|
|
61
|
+
Closed, Transferred, InDeclaration, Open, Rejected — against five modifiers, so Approved, Closed
|
|
62
|
+
and Completed came out the same green and Draft, InDeclaration and Transferred the same grey.
|
|
63
|
+
Semantics still come first: a state that IS good, bad or waiting takes `--success` / `--danger` /
|
|
64
|
+
`--warning`, and these are for the rest. Same `--tone-*` tokens and the same 18%/50% mix as the
|
|
65
|
+
other two, so the existing contrast test already measures them.
|
|
66
|
+
### Added — guards
|
|
67
|
+
- `test/css.test.mjs` asserts all eight bind their own token (a gap silently reuses a colour) and
|
|
68
|
+
that the mix stays identical to the avatar's and the chip's — one contrast test covers the three,
|
|
69
|
+
so a badge that drifted to its own ratio would be gated by a test measuring something else.
|
|
70
|
+
`test/contrast.test.mjs` renamed from "avatar tones" to "categorical tones" to say so.
|
|
71
|
+
|
|
72
|
+
## [1.43.0] — 2026-08-19
|
|
73
|
+
Three crowding reports from one settlement screen (IDU_EMATE_APPL_WEB, #020). All three measured in
|
|
74
|
+
Chromium rather than read off the CSS — each is a layout outcome, invisible in the stylesheet.
|
|
75
|
+
### Fixed
|
|
76
|
+
- **A control in a table column collapsed** (#020). `.fdy-input` is `width:100%`, which in an
|
|
77
|
+
auto-layout table contributes **no intrinsic width**: the column shrank to its header text and took
|
|
78
|
+
the control with it. A NOTE box measured **36px** — too narrow to read what you typed into it.
|
|
79
|
+
Controls that are direct children of a `td` now keep a `7rem` floor (112px measured). Per-control
|
|
80
|
+
rather than per-column, because the table cannot know which columns hold controls. `width:100%` was
|
|
81
|
+
never wrong; it is only in a table that 100% of nothing is nothing.
|
|
82
|
+
- **A stat value wrapped between the currency and the number** (#020). `.fdy-stat__value` is
|
|
83
|
+
`--text-3xl`; `IDR 300,000.00` needs **224px** at that size and `.fdy-stats` tracks are
|
|
84
|
+
`minmax(11rem,1fr)`, so a three-across row at 660px gave it ~205px and it broke onto two lines.
|
|
85
|
+
It now shrinks **only when its own column cannot hold it** — a 205px track renders 22.7px on one
|
|
86
|
+
line, a 420px track still renders exactly 31px. Shrinking the token instead would have taken 31px
|
|
87
|
+
from every consumer to fix a column that was 19px short. Scoped to a stat inside `.fdy-stats`,
|
|
88
|
+
where the width comes from the grid track: `container-type:inline-size` on a standalone
|
|
89
|
+
`.fdy-stat` would stop it sizing to its own content, and the unconditional rule stays as the
|
|
90
|
+
fallback where `@container` is missing.
|
|
91
|
+
- **`.fdy-filelist` claimed space above itself and none below** (#020), so the "Add files" button
|
|
92
|
+
every upload UI puts after it sat flush against the last row — a measured **0px**. Symmetric now,
|
|
93
|
+
with `:last-child` dropping the bottom margin so nothing pays for room it does not need.
|
|
94
|
+
### Added — guards
|
|
95
|
+
- `browser/crowding.mjs` measures all three. The stat case asserts **both** directions — one line
|
|
96
|
+
when narrow, still exactly `31px` when wide — because a fix that quietly shrank every dashboard
|
|
97
|
+
would pass a "does it wrap?" test.
|
|
98
|
+
|
|
99
|
+
## [1.42.0] — 2026-08-19
|
|
100
|
+
One note from the back-office app (IDU_EMATE_APPL_WEB, #019), and a string the #009 guard could not see.
|
|
101
|
+
### Added
|
|
102
|
+
- **`multiple` on the typed `FdyCfl`** (Vue · React · Blazor). The enhancer has offered
|
|
103
|
+
`data-fdy-cfl-multiple` since it shipped and `COMPONENTS.md` documented it; all three wrappers were
|
|
104
|
+
single-valued **at the type level**, so there was nothing to widen from the outside. An app that
|
|
105
|
+
started on the enhancer and later adopted the wrapper silently lost a capability — which is the
|
|
106
|
+
actual defect, more than the missing feature. The settlement screen that reported it gathers six or
|
|
107
|
+
eight approved claims onto one document and was doing six open→search→click cycles.
|
|
108
|
+
Rows tick instead of committing, the footer counts them, and **Confirm** commits the set. Vue and
|
|
109
|
+
React widen their model to `Row[] | null`; Blazor takes `Values` / `ValuesChanged` instead, because
|
|
110
|
+
a nullable union is not a C# shape. New strings: `selectedText` (`{n} selected`), `confirmText`,
|
|
111
|
+
`hintText` — the last one making the single-mode footer overridable too, which it never was.
|
|
112
|
+
### Notes on the shape of the fix
|
|
113
|
+
- **The ticks are component state, not the model.** Closing without Confirm has to leave the caller's
|
|
114
|
+
value exactly as it was, so the dialog seeds its ticks from the bound value on every open. Binding
|
|
115
|
+
them straight to the model would make Cancel a lie.
|
|
116
|
+
- **The field states `{n} selected`.** `display()` takes ONE row: naming one of six would be wrong and
|
|
117
|
+
naming all six does not fit a 22rem control.
|
|
118
|
+
- **Blazor gained a modal footer**, which it never had — Vue and React both commit from one.
|
|
119
|
+
- The tick checkbox is `aria-hidden` and inert. `.fdy-cfl__check input` is already
|
|
120
|
+
`pointer-events:none` in the kit's CSS, so the row owns the click and a handler on the box would
|
|
121
|
+
have been unreachable code; `aria-selected` on the row is what carries the state.
|
|
122
|
+
### Fixed
|
|
123
|
+
- **`FdyCfl`'s second retry button said `Coba lagi`** (Vue · React). The error state inside the
|
|
124
|
+
results branch had a hard-coded Indonesian literal while the button two states above it correctly
|
|
125
|
+
read `retryText ?? 'Try again'` — so the component both had the prop and ignored it. #009's guard
|
|
126
|
+
missed it because **neither `coba` nor `lagi` is in its word list**, which is #015's lesson landing
|
|
127
|
+
a third time: a word list is a floor, not a ceiling. Both now read `retryText`.
|
|
128
|
+
### Added — guards
|
|
129
|
+
- **The multi flow is driven by real clicks, in both adapters** (#019). `browser/cfl-multi.mjs`
|
|
130
|
+
asserts that a click ticks rather than commits, that the dialog stays open, that `aria-selected`
|
|
131
|
+
and the footer count follow, that a second click unticks, that Confirm hands back an **array** in
|
|
132
|
+
order, and that closing without Confirm discards the ticks *and* does not remember them on re-open.
|
|
133
|
+
Vue and React run the same script, so a divergence between the two fails here instead of in an app.
|
|
134
|
+
A typecheck cannot tell you whether a mode ever renders. Mutation-tested — committing
|
|
135
|
+
`picked[0]` instead of the array fails it by name.
|
|
136
|
+
|
|
6
137
|
## [1.41.0] — 2026-08-19
|
|
7
138
|
One note from the account app (IDU_EMATE_ACCT_WEB, #018) — the first raised from that repo.
|
|
8
139
|
### Fixed
|
package/COMPONENTS.md
CHANGED
|
@@ -463,6 +463,12 @@ the field. The value is always **chosen, never typed**. Needs `freeday-cfl.js`.
|
|
|
463
463
|
- Hooks: `data-fdy-cfl-search` (the search input), `data-fdy-cfl-empty`, `data-fdy-cfl-count`,
|
|
464
464
|
`data-fdy-cfl-confirm` (multi only), `data-fdy-cfl-trigger`, `data-fdy-cfl-open`
|
|
465
465
|
- Rows are server state — in a real app drive them from a controlled fetch, not a global store.
|
|
466
|
+
- **Multi-select in the typed wrappers** (Vue · React · Blazor): `multiple` ticks rows and commits
|
|
467
|
+
them together on **Confirm** instead of committing the row that was clicked. Vue and React widen
|
|
468
|
+
their model to an array (`Row[] | null`); Blazor takes a second pair, `Values` /
|
|
469
|
+
`ValuesChanged`, because a nullable union is not a C# shape. Closing without Confirm leaves the
|
|
470
|
+
bound value untouched, and re-opening seeds the ticks from it. The field states `{n} selected`
|
|
471
|
+
rather than one row's `display()`. Strings: `selectedText`, `confirmText`, `hintText`.
|
|
466
472
|
|
|
467
473
|
```html
|
|
468
474
|
<div class="fdy-field">
|
|
@@ -767,7 +773,9 @@ pagination. Needs `freeday-table.js`. Wrap the whole thing in `.fdy-datatable` +
|
|
|
767
773
|
wrapper**.
|
|
768
774
|
- **Overriding an enhancer's strings.** Each enhancer keeps its strings in one `TEXT` table and
|
|
769
775
|
reads them through `textOf()`, so any of them can be replaced per element with
|
|
770
|
-
**`data-fdy-text-<key
|
|
776
|
+
**`data-fdy-text-<key>`**, the key kebab-cased — no forking, and no rendering the nodes yourself.
|
|
777
|
+
(HTML lowercases attribute names, so a camelCase key like `filterText` is written
|
|
778
|
+
`data-fdy-text-filter-text`; the run-together spelling still resolves.):
|
|
771
779
|
|
|
772
780
|
```html
|
|
773
781
|
<div data-fdy-table
|
|
@@ -1100,6 +1108,12 @@ while it stays a real `<button>`/`<a>` with real keyboard semantics.
|
|
|
1100
1108
|
Inline status pill: `--success` `--warning` `--danger` `--info` `--outline`. Never colour-only —
|
|
1101
1109
|
the text carries the meaning.
|
|
1102
1110
|
|
|
1111
|
+
`--tone-1`…`--tone-8` for a status vocabulary larger than the semantic palette. Semantics first —
|
|
1112
|
+
a state that IS good, bad or waiting takes `--success` / `--danger` / `--warning`; these are for
|
|
1113
|
+
the rest, so two states a workflow distinguishes never share a look. Map each status to a FIXED
|
|
1114
|
+
tone, never to a hash of the string: a status is a closed set, and a hash reassigns colours the
|
|
1115
|
+
moment the set grows.
|
|
1116
|
+
|
|
1103
1117
|
**Notification badge is the overlay variant**, not a separate component: wrap anything in
|
|
1104
1118
|
`.fdy-badge-wrap` and add `.fdy-badge-ov` (`--primary` `--danger` `--accent` `--info` `--success`
|
|
1105
1119
|
`--warning`, `--dot` for a bare dot, `--top-left` / `--bottom-right` / `--bottom-left` to move it).
|
|
@@ -63,6 +63,10 @@
|
|
|
63
63
|
<table class="fdy-table" aria-label="@Title">
|
|
64
64
|
<thead>
|
|
65
65
|
<tr>
|
|
66
|
+
@if (Multiple)
|
|
67
|
+
{
|
|
68
|
+
<th scope="col"><span class="fdy-visually-hidden">@SelectedLabel</span></th>
|
|
69
|
+
}
|
|
66
70
|
@foreach (CflColumn<TRow> col in Columns)
|
|
67
71
|
{
|
|
68
72
|
<th scope="col">@col.Header</th>
|
|
@@ -72,7 +76,15 @@
|
|
|
72
76
|
<tbody>
|
|
73
77
|
@foreach (TRow row in _rows)
|
|
74
78
|
{
|
|
75
|
-
<tr class="fdy-cfl__row" tabindex="0" @key="RowKey(row)"
|
|
79
|
+
<tr class="fdy-cfl__row" tabindex="0" @key="RowKey(row)"
|
|
80
|
+
aria-selected="@(Multiple ? IsPicked(row).ToString().ToLowerInvariant() : null)"
|
|
81
|
+
@onclick="() => RowClickAsync(row)">
|
|
82
|
+
@if (Multiple)
|
|
83
|
+
{
|
|
84
|
+
<td class="fdy-cfl__check">
|
|
85
|
+
<input class="fdy-checkbox" type="checkbox" tabindex="-1" checked="@IsPicked(row)" aria-hidden="true" />
|
|
86
|
+
</td>
|
|
87
|
+
}
|
|
76
88
|
@foreach (CflColumn<TRow> col in Columns)
|
|
77
89
|
{
|
|
78
90
|
<td>@col.Cell(row)</td>
|
|
@@ -99,4 +111,15 @@
|
|
|
99
111
|
}
|
|
100
112
|
</div>
|
|
101
113
|
</div>
|
|
114
|
+
|
|
115
|
+
<div class="fdy-modal__footer">
|
|
116
|
+
<span class="fdy-cfl__count" aria-live="polite">@(Multiple ? SelectedLabel : HintText)</span>
|
|
117
|
+
<div class="fdy-cfl__actions">
|
|
118
|
+
<button class="fdy-btn fdy-btn--ghost" type="button" @onclick="CloseAsync">@CloseLabel</button>
|
|
119
|
+
@if (Multiple)
|
|
120
|
+
{
|
|
121
|
+
<button class="fdy-btn" type="button" @onclick="ConfirmAsync">@ConfirmText</button>
|
|
122
|
+
}
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
102
125
|
</dialog>
|
|
@@ -35,6 +35,26 @@ public partial class FdyCfl<TRow>
|
|
|
35
35
|
/// <summary>aria-label for the button that opens the picker.</summary>
|
|
36
36
|
[Parameter] public string OpenLabel { get; set; } = "Open search";
|
|
37
37
|
|
|
38
|
+
/// <summary>Tick rows and commit them together, instead of committing the row that was clicked.
|
|
39
|
+
/// The kit's own enhancer offers this (<c>data-fdy-cfl-multiple</c>); a screen that gathers six
|
|
40
|
+
/// expense claims onto one document wants one dialog, not six. Binds <see cref="Values"/>.</summary>
|
|
41
|
+
[Parameter] public bool Multiple { get; set; }
|
|
42
|
+
|
|
43
|
+
/// <summary>The committed rows when <see cref="Multiple"/> is set. Vue and React widen their
|
|
44
|
+
/// single model to an array; C# takes a second pair instead, so neither is nullable-of-union.</summary>
|
|
45
|
+
[Parameter] public IReadOnlyList<TRow>? Values { get; set; }
|
|
46
|
+
|
|
47
|
+
[Parameter] public EventCallback<IReadOnlyList<TRow>?> ValuesChanged { get; set; }
|
|
48
|
+
|
|
49
|
+
/// <summary>Footer label while picking; <c>{n}</c> is replaced by the tick count.</summary>
|
|
50
|
+
[Parameter] public string SelectedText { get; set; } = "{n} selected";
|
|
51
|
+
|
|
52
|
+
/// <summary>The multi-select commit button.</summary>
|
|
53
|
+
[Parameter] public string ConfirmText { get; set; } = "Confirm";
|
|
54
|
+
|
|
55
|
+
/// <summary>Footer hint in single mode.</summary>
|
|
56
|
+
[Parameter] public string HintText { get; set; } = "Click a row to choose it";
|
|
57
|
+
|
|
38
58
|
[Parameter] public bool Disabled { get; set; }
|
|
39
59
|
|
|
40
60
|
/// <summary>Locked/view mode: shows the value but the search dialog can't be opened.</summary>
|
|
@@ -78,6 +98,9 @@ public partial class FdyCfl<TRow>
|
|
|
78
98
|
if (Disabled || Readonly || _open) return;
|
|
79
99
|
_open = true;
|
|
80
100
|
_query = string.Empty;
|
|
101
|
+
/* Re-seeded per open, so Cancel really is a cancel: the ticks start as whatever the caller holds. */
|
|
102
|
+
_picked.Clear();
|
|
103
|
+
if (Multiple && Values is not null) _picked.AddRange(Values);
|
|
81
104
|
await JS.InvokeVoidAsync("FreedayBlazor.dialogShow", Root);
|
|
82
105
|
await LoadAsync(reset: true);
|
|
83
106
|
}
|
|
@@ -141,10 +164,52 @@ public partial class FdyCfl<TRow>
|
|
|
141
164
|
/// opens nor closes it.</summary>
|
|
142
165
|
private async Task ClearAsync()
|
|
143
166
|
{
|
|
167
|
+
_picked.Clear();
|
|
168
|
+
if (Multiple)
|
|
169
|
+
{
|
|
170
|
+
Values = null;
|
|
171
|
+
await ValuesChanged.InvokeAsync(null);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
144
174
|
Value = default;
|
|
145
175
|
await ValueChanged.InvokeAsync(default);
|
|
146
176
|
}
|
|
147
177
|
|
|
178
|
+
/* The ticks live here, not in Values, because a multi dialog is only committed at Confirm:
|
|
179
|
+
closing it must leave the caller's value exactly as it was. Seeded from Values on open. */
|
|
180
|
+
private readonly List<TRow> _picked = new();
|
|
181
|
+
|
|
182
|
+
private string SelectedLabel => SelectedText.Replace("{n}", _picked.Count.ToString());
|
|
183
|
+
|
|
184
|
+
private bool IsPicked(TRow row)
|
|
185
|
+
{
|
|
186
|
+
string key = RowKey(row);
|
|
187
|
+
return _picked.Exists(r => RowKey(r) == key);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private void TogglePick(TRow row)
|
|
191
|
+
{
|
|
192
|
+
string key = RowKey(row);
|
|
193
|
+
int at = _picked.FindIndex(r => RowKey(r) == key);
|
|
194
|
+
if (at == -1) _picked.Add(row);
|
|
195
|
+
else _picked.RemoveAt(at);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* A click means "tick this" in multi and "this is my answer" in single — the whole difference. */
|
|
199
|
+
private async Task RowClickAsync(TRow row)
|
|
200
|
+
{
|
|
201
|
+
if (Multiple) TogglePick(row);
|
|
202
|
+
else await ChooseAsync(row);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private async Task ConfirmAsync()
|
|
206
|
+
{
|
|
207
|
+
IReadOnlyList<TRow> picks = _picked.ToArray();
|
|
208
|
+
Values = picks;
|
|
209
|
+
await ValuesChanged.InvokeAsync(picks);
|
|
210
|
+
await CloseAsync();
|
|
211
|
+
}
|
|
212
|
+
|
|
148
213
|
private async Task ChooseAsync(TRow row)
|
|
149
214
|
{
|
|
150
215
|
Value = row;
|
|
@@ -21,8 +21,11 @@ export interface CflPage<Row> {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export interface FdyCflProps<Row extends Record<string, unknown>> {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
/** Single: `Row | null`. With `multiple`, an array — `Row[] | null`, where null and [] both mean
|
|
25
|
+
* nothing picked. The enhancer has had `data-fdy-cfl-multiple` all along; this is the typed
|
|
26
|
+
* wrappers catching up (#019). */
|
|
27
|
+
value: Row | Row[] | null;
|
|
28
|
+
onChange: (value: Row | Row[] | null) => void;
|
|
26
29
|
fetchPage: (query: string, page: number) => Promise<CflPage<Row>>;
|
|
27
30
|
columns: ReadonlyArray<CflColumn<Row>>;
|
|
28
31
|
display: (row: Row) => string;
|
|
@@ -45,6 +48,16 @@ export interface FdyCflProps<Row extends Record<string, unknown>> {
|
|
|
45
48
|
closeLabel?: string;
|
|
46
49
|
/** aria-label for the button that opens the picker. Default 'Open search'. */
|
|
47
50
|
openLabel?: string;
|
|
51
|
+
/** Tick rows and commit them together, instead of committing the row that was clicked. The kit's
|
|
52
|
+
* own enhancer offers this (`data-fdy-cfl-multiple`); a screen that gathers six expense claims
|
|
53
|
+
* onto one document wants one dialog, not six. */
|
|
54
|
+
multiple?: boolean;
|
|
55
|
+
/** Footer label while picking, `{n}` replaced by the tick count. Default '{n} selected'. */
|
|
56
|
+
selectedText?: string;
|
|
57
|
+
/** The multi-select commit button. Default 'Confirm'. */
|
|
58
|
+
confirmText?: string;
|
|
59
|
+
/** Footer hint in single mode. Default 'Click a row to choose it'. */
|
|
60
|
+
hintText?: string;
|
|
48
61
|
placeholder?: string;
|
|
49
62
|
disabled?: boolean;
|
|
50
63
|
/** Locked/view mode: shows the picked value (focusable, copyable), but the search dialog can't be opened. Unlike `disabled`, it keeps tab order and isn't greyed. */
|
|
@@ -71,6 +84,7 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
71
84
|
control that still exists — the trigger beside it, since this button disappears with the value. */
|
|
72
85
|
const clearLabelText: string = props.clearLabel ?? 'Clear selection';
|
|
73
86
|
const clearValue = (): void => {
|
|
87
|
+
setPicked([]);
|
|
74
88
|
props.onChange(null);
|
|
75
89
|
triggerRef.current?.focus();
|
|
76
90
|
};
|
|
@@ -86,6 +100,9 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
86
100
|
const [loading, setLoading] = useState<boolean>(false);
|
|
87
101
|
const [error, setError] = useState<Error | null>(null);
|
|
88
102
|
const [activeIndex, setActiveIndex] = useState<number>(-1);
|
|
103
|
+
/* The ticks live here, not in `value`, because a multi dialog is only committed at Confirm:
|
|
104
|
+
closing it must leave the caller's value exactly as it was. Seeded from `value` on open. */
|
|
105
|
+
const [picked, setPicked] = useState<Row[]>([]);
|
|
89
106
|
|
|
90
107
|
// Out-of-order guard: every fetch takes a monotonically increasing token; a resolved or
|
|
91
108
|
// rejected page is applied only if it still owns the latest token. A newer search, a
|
|
@@ -100,10 +117,25 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
100
117
|
|
|
101
118
|
const isDisabled: boolean = props.disabled === true;
|
|
102
119
|
const isReadonly: boolean = props.readonly === true;
|
|
120
|
+
/* `display()` takes one row, so in multi the field states HOW MANY — naming one of six would be a
|
|
121
|
+
lie, and naming all six does not fit a control that is 22rem wide. */
|
|
122
|
+
const currentRows: Row[] = Array.isArray(props.value)
|
|
123
|
+
? props.value
|
|
124
|
+
: props.value !== null
|
|
125
|
+
? [props.value as Row]
|
|
126
|
+
: [];
|
|
127
|
+
const selectedTextFor = (n: number): string => (props.selectedText ?? '{n} selected').replace('{n}', String(n));
|
|
128
|
+
const isMultiple: boolean = props.multiple === true;
|
|
103
129
|
const showClear: boolean =
|
|
104
|
-
props.clearable === true &&
|
|
130
|
+
props.clearable === true && currentRows.length > 0 && isDisabled === false && isReadonly === false;
|
|
105
131
|
const isInvalid: boolean = props.invalid === true;
|
|
106
|
-
const displayValue: string =
|
|
132
|
+
const displayValue: string = isMultiple
|
|
133
|
+
? currentRows.length === 0
|
|
134
|
+
? ''
|
|
135
|
+
: selectedTextFor(currentRows.length)
|
|
136
|
+
: props.value !== null
|
|
137
|
+
? props.display(props.value as Row)
|
|
138
|
+
: '';
|
|
107
139
|
// The results <table> (owner of `resultsId`) only renders in the rows branch, so gate the
|
|
108
140
|
// search input's aria refs on rows existing — otherwise they'd dangle during loading/empty/error.
|
|
109
141
|
const hasRows: boolean = rows.length > 0;
|
|
@@ -179,6 +211,31 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
179
211
|
document.getElementById(rowId(activeIndex))?.scrollIntoView({ block: 'nearest' });
|
|
180
212
|
}, [activeIndex]);
|
|
181
213
|
|
|
214
|
+
function isPicked(row: Row): boolean {
|
|
215
|
+
const key: string = props.rowKey(row);
|
|
216
|
+
return picked.some((r: Row): boolean => props.rowKey(r) === key);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function togglePick(row: Row): void {
|
|
220
|
+
const key: string = props.rowKey(row);
|
|
221
|
+
setPicked((prev: Row[]): Row[] =>
|
|
222
|
+
prev.some((r: Row): boolean => props.rowKey(r) === key)
|
|
223
|
+
? prev.filter((r: Row): boolean => props.rowKey(r) !== key)
|
|
224
|
+
: [...prev, row],
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* A click means "tick this" in multi and "this is my answer" in single — the whole difference. */
|
|
229
|
+
function onRowClick(row: Row): void {
|
|
230
|
+
if (isMultiple) togglePick(row);
|
|
231
|
+
else commit(row);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function confirmPicks(): void {
|
|
235
|
+
props.onChange(picked);
|
|
236
|
+
closeDialog();
|
|
237
|
+
}
|
|
238
|
+
|
|
182
239
|
function commit(row: Row): void {
|
|
183
240
|
props.onChange(row);
|
|
184
241
|
closeDialog();
|
|
@@ -210,7 +267,7 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
210
267
|
const row: Row | undefined = activeIndex >= 0 ? rows[activeIndex] : undefined;
|
|
211
268
|
if (row !== undefined) {
|
|
212
269
|
e.preventDefault();
|
|
213
|
-
|
|
270
|
+
onRowClick(row);
|
|
214
271
|
}
|
|
215
272
|
break;
|
|
216
273
|
}
|
|
@@ -233,6 +290,8 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
233
290
|
setHasMore(false);
|
|
234
291
|
setError(null);
|
|
235
292
|
setActiveIndex(-1);
|
|
293
|
+
/* Re-seeded per open, so Cancel really is a cancel: the ticks start as whatever the caller holds. */
|
|
294
|
+
setPicked(isMultiple ? [...currentRows] : []);
|
|
236
295
|
dialogRef.current?.showModal();
|
|
237
296
|
searchRef.current?.focus();
|
|
238
297
|
void load('', 0, false);
|
|
@@ -356,6 +415,9 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
356
415
|
<table id={resultsId} className="fdy-table" aria-label="Search results">
|
|
357
416
|
<thead>
|
|
358
417
|
<tr>
|
|
418
|
+
{isMultiple ? (
|
|
419
|
+
<th scope="col"><span className="fdy-visually-hidden">{selectedTextFor(picked.length)}</span></th>
|
|
420
|
+
) : null}
|
|
359
421
|
{props.columns.map((col: CflColumn<Row>): JSX.Element => (
|
|
360
422
|
<th key={col.key} scope="col">{col.label}</th>
|
|
361
423
|
))}
|
|
@@ -366,11 +428,16 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
366
428
|
<tr
|
|
367
429
|
id={rowId(i)}
|
|
368
430
|
key={props.rowKey(row)}
|
|
369
|
-
className=
|
|
370
|
-
aria-selected={i === activeIndex ?
|
|
371
|
-
onClick={(): void =>
|
|
431
|
+
className={i === activeIndex ? 'fdy-cfl__row is-active' : 'fdy-cfl__row'}
|
|
432
|
+
aria-selected={isMultiple ? isPicked(row) : i === activeIndex ? true : undefined}
|
|
433
|
+
onClick={(): void => onRowClick(row)}
|
|
372
434
|
onMouseMove={(): void => setActive(i)}
|
|
373
435
|
>
|
|
436
|
+
{isMultiple ? (
|
|
437
|
+
<td className="fdy-cfl__check">
|
|
438
|
+
<input className="fdy-checkbox" type="checkbox" tabIndex={-1} checked={isPicked(row)} readOnly aria-hidden="true" />
|
|
439
|
+
</td>
|
|
440
|
+
) : null}
|
|
374
441
|
{props.columns.map((col: CflColumn<Row>): JSX.Element => (
|
|
375
442
|
<td key={col.key}>{cellText(row, col.key)}</td>
|
|
376
443
|
))}
|
|
@@ -382,7 +449,7 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
382
449
|
{error !== null ? (
|
|
383
450
|
<div className="fdy-cfl__empty" role="alert" style={{ padding: 'var(--space-4) var(--space-5)' }}>
|
|
384
451
|
<p style={{ margin: '0 0 var(--space-3)' }}>{error.message}</p>
|
|
385
|
-
<button className="fdy-btn fdy-btn--sm" type="button" onClick={retry}>
|
|
452
|
+
<button className="fdy-btn fdy-btn--sm" type="button" onClick={retry}>{props.retryText ?? 'Try again'}</button>
|
|
386
453
|
</div>
|
|
387
454
|
) : hasMore ? (
|
|
388
455
|
<div style={{ padding: 'var(--space-3) var(--space-4)', textAlign: 'center' }}>
|
|
@@ -397,9 +464,14 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
397
464
|
</div>
|
|
398
465
|
|
|
399
466
|
<div className="fdy-modal__footer">
|
|
400
|
-
<span className="fdy-cfl__count"
|
|
467
|
+
<span className="fdy-cfl__count" aria-live="polite">
|
|
468
|
+
{isMultiple ? selectedTextFor(picked.length) : (props.hintText ?? 'Click a row to choose it')}
|
|
469
|
+
</span>
|
|
401
470
|
<div className="fdy-cfl__actions">
|
|
402
471
|
<button className="fdy-btn fdy-btn--ghost" type="button" onClick={closeDialog}>{props.closeLabel ?? 'Close'}</button>
|
|
472
|
+
{isMultiple ? (
|
|
473
|
+
<button className="fdy-btn" type="button" onClick={confirmPicks}>{props.confirmText ?? 'Confirm'}</button>
|
|
474
|
+
) : null}
|
|
403
475
|
</div>
|
|
404
476
|
</div>
|
|
405
477
|
</dialog>
|