@cahyo-dimas/freeday 2.1.0 → 2.2.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 +67 -0
- package/COMPONENTS.md +34 -13
- package/README.id.md +1 -1
- package/README.md +1 -1
- package/adapters/blazor/FdyAutocomplete.razor +4 -1
- package/adapters/blazor/FdyAutocomplete.razor.cs +26 -0
- package/adapters/blazor/FdyCascade.razor +6 -1
- package/adapters/blazor/FdyCascade.razor.cs +24 -0
- package/adapters/blazor/FdyCombo.razor +1 -0
- package/adapters/blazor/FdyCombo.razor.cs +13 -0
- package/adapters/blazor/FdyDatepicker.razor +15 -1
- package/adapters/blazor/FdyDatepicker.razor.cs +46 -0
- package/adapters/react/components/FdyDrawer.tsx +3 -1
- package/adapters/react/components/FdyModal.tsx +3 -1
- package/adapters/vue/components/FdyDrawer.vue +4 -1
- package/adapters/vue/components/FdyModal.vue +4 -1
- package/dist/freeday-autocomplete.js +17 -1
- package/dist/freeday-cascade.js +53 -3
- package/dist/freeday-chart.js +33 -3
- package/dist/freeday-datepicker.js +109 -17
- package/dist/freeday-select.js +18 -1
- package/dist/freeday-table.js +4 -2
- package/dist/freeday-timepicker.js +19 -1
- package/dist/freeday.js +253 -28
- package/docs/getting-started.md +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,73 @@ benar. Perubahan seperti itu ditulis di bawah `### Changed: BREAKING (types)`
|
|
|
9
9
|
lama → tipe baru, dan cara menyempitkannya — bukan di bawah `### Added`, betapapun aditifnya dari
|
|
10
10
|
sisi kit.
|
|
11
11
|
|
|
12
|
+
## [2.2.0] - 2026-08-26
|
|
13
|
+
### Added
|
|
14
|
+
- **The raw path can disable, lock and invalidate a picker** (`NEXT-UP.md` #12). `datepicker.css`
|
|
15
|
+
and `cascade.css` have styled `:disabled`, `[aria-readonly="true"]` and `[aria-invalid="true"]`
|
|
16
|
+
since they were written, and the enhancers never set any of them — so the states existed only in
|
|
17
|
+
the stacks that re-implement the control natively (Vue, React), and a **Blazor** app, whose
|
|
18
|
+
picker *is* the enhancer, could not disable a field at all. Now `data-disabled` /
|
|
19
|
+
`data-readonly` / `data-invalid` on the seed, plus `data-id` and `data-describedby` so the
|
|
20
|
+
trigger the enhancer builds can be labelled and described by a form's own markup.
|
|
21
|
+
- **`setState` on four enhancers** — `FreedayDatepicker`, `FreedayCascade`, `FreedayCombo`,
|
|
22
|
+
`FreedayAutocomplete`: `setState(root, { disabled, readonly, invalid })`. Not decoration: every
|
|
23
|
+
Blazor wrapper stops rendering once hydrated (`ShouldRender => !Hydrated`), so a parameter that
|
|
24
|
+
is only *rendered* goes quiet the moment a real form toggles it. The wrappers push through this
|
|
25
|
+
instead.
|
|
26
|
+
- **The Blazor pickers reach parity** with their Vue and React twins. `FdyDatepicker` goes from 6
|
|
27
|
+
parameters to 20 (the three states, `Id`, `Describedby`, and the nine calendar navigation
|
|
28
|
+
labels), `FdyAutocomplete` from 6 to 12, `FdyCascade` from 8 to 13, and `FdyCombo` finally has
|
|
29
|
+
`Describedby`. Four exemptions remain, each about the platform rather than effort, and each
|
|
30
|
+
named in `COMPONENTS.md`: `locale` (the enhancer formats through `Intl` from `<html lang>`),
|
|
31
|
+
`clearable`/`clearLabel` (this path builds no clear button), `ariaLabelledby` (the built trigger
|
|
32
|
+
is named by `Label`).
|
|
33
|
+
- **`closeLabel` on `FdyModal` and `FdyDrawer`** in Vue and React. Blazor has had `CloseLabel`
|
|
34
|
+
since it shipped; the other two hard-coded `aria-label="Close"`, so the gap ran in both
|
|
35
|
+
directions and this is the half nobody had reported yet.
|
|
36
|
+
- **The datepicker's ten labels are overridable**, as `data-fdy-text-prev-month`,
|
|
37
|
+
`-next-month`, `-prev-year`, `-next-year`, `-prev-years`, `-next-years`, `-choose-month`,
|
|
38
|
+
`-choose-year`, `-back-to-months`. Month and weekday NAMES still come from `Intl` via the page's
|
|
39
|
+
`lang`, which is the better hatch; these are the buttons around them.
|
|
40
|
+
### Fixed
|
|
41
|
+
- **A chart series with no label read `Seri 1`.** Indonesian, in a release train whose 2.0.0
|
|
42
|
+
exists precisely to stop the enhancers mixing languages. It is `Series {n}` now, and
|
|
43
|
+
overridable. The donut's centre caption (`Total`) was hard-coded in the same file and is
|
|
44
|
+
overridable too; it is also built as an element rather than assigned through `innerHTML`, since
|
|
45
|
+
an author-supplied string is not markup.
|
|
46
|
+
- **The table's number-range filter offered `Min` and `Maks`.** Same leftover, same fix:
|
|
47
|
+
`filterMin` / `filterMax` in the strings table, English by default.
|
|
48
|
+
- **The cascade's own `Select` / `Select…` defaults and the timepicker's `Choose a time`** were
|
|
49
|
+
literals no host could reach. Both tables now carry them.
|
|
50
|
+
- **A `readonly` autocomplete opened its suggestion list.** The input carries `readonly` natively
|
|
51
|
+
and the enhancer never read it, so the list dropped over a field nobody could edit.
|
|
52
|
+
### Changed
|
|
53
|
+
- **The hard-coded-string guard (#016) is inverted.** It used to find the line that writes to the
|
|
54
|
+
DOM and read the literals on it, which cannot see a string passed to a helper —
|
|
55
|
+
`navButton('‹', 'Previous month', fn)` writes its label inside `navButton`, where the value is a
|
|
56
|
+
variable. Ten datepicker labels, the timepicker's, the cascade's two, the table's two and the
|
|
57
|
+
chart's `Seri 1` all hid there, some of them for the whole life of the file. It now reads every
|
|
58
|
+
literal in every enhancer and asks the opposite question — is this prose, and is it in the
|
|
59
|
+
`TEXT` table — dropping non-prose by shape (selectors, CSS custom properties, kebab keys,
|
|
60
|
+
camelCase identifiers, URLs, locale tags) and one short vocabulary of DOM tokens. Verified by
|
|
61
|
+
reintroducing both blind spots and watching it fail.
|
|
62
|
+
### Added: guards
|
|
63
|
+
- `test/blazor/FdyPickerStateTests.cs`: 7 bUnit tests covering what `dotnet build` cannot see —
|
|
64
|
+
each parameter reaching the markup the enhancer reads, an absent one leaving no empty attribute
|
|
65
|
+
behind, and a state change **after** the first render arriving through interop.
|
|
66
|
+
- `browser/picker-states.mjs`: 7 tests in real Chrome with real mouse gestures — a disabled picker
|
|
67
|
+
that will not open, a readonly one that keeps focus and still refuses, both `aria-invalid` and
|
|
68
|
+
the `--error` class, `setState` unlocking a picker, and a nav label overridden per element.
|
|
69
|
+
- `test/docs.test.mjs`: the Blazor picker parity guard, Vue's prop surface against the four
|
|
70
|
+
wrappers, with the four exemptions written out with their reasons rather than a bare list.
|
|
71
|
+
### Notes on the shape of the fix
|
|
72
|
+
- The regression this work caused, and what caught it: naming the new flag `isDisabled` shadowed
|
|
73
|
+
an **existing** `isDisabled(date)` in the datepicker that decides whether a day falls outside
|
|
74
|
+
`min`/`max`, so the day grid threw on every render — while the panel still opened and only its
|
|
75
|
+
cells went missing. Nothing in the node suite noticed; `browser/vanilla.mjs`, which clicks
|
|
76
|
+
through the calendar in a real browser, failed immediately. The flags are named `state*` now,
|
|
77
|
+
with the collision written above them.
|
|
78
|
+
|
|
12
79
|
## [2.1.0] - 2026-08-25
|
|
13
80
|
### Added
|
|
14
81
|
- **`singleRow()`, exported from `@cahyo-dimas/freeday/vue` and `/react`** (#045). `FdyCfl` emits
|
package/COMPONENTS.md
CHANGED
|
@@ -456,8 +456,9 @@ Native inputs, styled. `.fdy-check` · `.fdy-radio` · `.fdy-switch` on the wrap
|
|
|
456
456
|
| `invalid?` | `boolean` | Sets `aria-invalid`; pair it with `describedby` pointing at the error text. |
|
|
457
457
|
| `describedby?` | `string` | Id of the help or error text (`aria-describedby`). |
|
|
458
458
|
|
|
459
|
-
Blazor: the same names in `PascalCase`, bound with `@bind-Value` (`Value` / `ValueChanged`)
|
|
460
|
-
|
|
459
|
+
Blazor: the same names in `PascalCase`, bound with `@bind-Value` (`Value` / `ValueChanged`).
|
|
460
|
+
`FreedayCombo.setState(root, { disabled, readonly, invalid })` pushes a state change onto a combo
|
|
461
|
+
whose host has stopped re-rendering.
|
|
461
462
|
|
|
462
463
|
Fully styleable dropdown, APG combobox+listbox. Needs `freeday-select.js`.
|
|
463
464
|
|
|
@@ -501,9 +502,10 @@ keeps matching after selection. Do not put a glyph in that span.
|
|
|
501
502
|
| `id?` · `ariaLabel?` · `ariaLabelledby?` · `describedby?` | `string` | Input id; its accessible name as text or as a reference; the help/error text it points at. |
|
|
502
503
|
| `disabled?` · `readonly?` · `invalid?` | `boolean` | `readonly` keeps focus and tab order and shows its value, but the input is not editable and the list will not open. Unlike `disabled` it is not greyed. |
|
|
503
504
|
|
|
504
|
-
Blazor
|
|
505
|
-
`
|
|
506
|
-
|
|
505
|
+
Blazor carries all of these. The states live on the input natively, and
|
|
506
|
+
`FreedayAutocomplete.setState(root, { disabled, readonly, invalid })` changes them after the first
|
|
507
|
+
render. A `readonly` input no longer opens its suggestion list, which it used to do over a field
|
|
508
|
+
nobody could edit.
|
|
507
509
|
|
|
508
510
|
Editable combobox that filters as you type. Needs `freeday-autocomplete.js`.
|
|
509
511
|
|
|
@@ -528,7 +530,10 @@ Editable combobox that filters as you type. Needs `freeday-autocomplete.js`.
|
|
|
528
530
|
| `id?` · `ariaLabelledby?` · `describedby?` | `string` | Trigger id; the element that labels it; the help/error text it points at. |
|
|
529
531
|
| `disabled?` · `readonly?` · `invalid?` | `boolean` | As on `<FdyCombo>`. |
|
|
530
532
|
|
|
531
|
-
|
|
533
|
+
Raw path: `data-disabled` / `data-readonly` / `data-invalid`, `data-id`, `data-describedby`, and
|
|
534
|
+
`FreedayCascade.setState(root, { disabled, readonly, invalid })` for a host that rendered once.
|
|
535
|
+
Blazor calls the tree `Nodes`, adds `SubmenuLabel`, and carries everything else except
|
|
536
|
+
`ariaLabelledby` — the trigger it builds is named by `Label`.
|
|
532
537
|
|
|
533
538
|
Hierarchical drill-down. The data model is a **nested `<ul>`** inside the wrapper: an `<li>` with a
|
|
534
539
|
child `<ul>` is a branch, one without is a leaf. Needs `freeday-cascade.js`.
|
|
@@ -675,9 +680,15 @@ a new class.
|
|
|
675
680
|
The ten label props exist because month and weekday names follow `locale` while the buttons around
|
|
676
681
|
them do not: without these, a Spanish calendar would be navigated by English arrows.
|
|
677
682
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
683
|
+
On the **raw path** these all have hooks now: `data-disabled` / `data-readonly` / `data-invalid`,
|
|
684
|
+
`data-id`, `data-describedby`, and one `data-fdy-text-<key>` per label above
|
|
685
|
+
(`data-fdy-text-prev-month`, …). A host that renders its markup once and cannot re-render it can
|
|
686
|
+
change the three states afterwards with `FreedayDatepicker.setState(root, { disabled, readonly,
|
|
687
|
+
invalid })`.
|
|
688
|
+
|
|
689
|
+
Blazor carries the same surface bar four: `locale` (the enhancer formats through `Intl` from the
|
|
690
|
+
page's `<html lang>`), `clearable` and `clearLabel` (this path builds no clear button), and
|
|
691
|
+
`ariaLabelledby` (the trigger it builds is named by `Label`).
|
|
681
692
|
|
|
682
693
|
### Props — `<FdyDateRange>`
|
|
683
694
|
|
|
@@ -1038,7 +1049,11 @@ pagination. Needs `freeday-table.js`. Wrap the whole thing in `.fdy-datatable` +
|
|
|
1038
1049
|
(`data-fdy-text-required`, `data-fdy-text-type`, …), narrower than the per-field
|
|
1039
1050
|
`data-fdy-msg-<alias>` that still wins. `Freeday.toast()` takes `closeLabel` in its options
|
|
1040
1051
|
object. `npm test` asserts no enhancer string is written outside its `TEXT` table, so a new one
|
|
1041
|
-
arrives overridable or not at all.
|
|
1052
|
+
arrives overridable or not at all — a claim that was only half true until **2.2.0**, when the
|
|
1053
|
+
guard stopped looking for the line that writes to the DOM and started reading every literal in
|
|
1054
|
+
the file. The old shape could not see a string handed to a helper, which is how ten datepicker
|
|
1055
|
+
labels, the timepicker's, the cascade's two defaults, the table's `Min`/`Maks` filter
|
|
1056
|
+
placeholders and the chart's `Seri 1` all sat outside it.
|
|
1042
1057
|
|
|
1043
1058
|
**Testing note:** a column's filter button and the dialog it opens deliberately share one
|
|
1044
1059
|
accessible name (`Filter <column>`), since a dialog named after its trigger is the normal pattern. In a
|
|
@@ -1132,6 +1147,10 @@ Pure SVG/CSS, no dependency, re-colours with the theme. Needs `freeday-chart.js`
|
|
|
1132
1147
|
- Colour: `data-fdy-color="primary"` or `data-fdy-colors="success,warning,danger"` for semantic token
|
|
1133
1148
|
names **or** `chart-1`…`chart-8` slots to pin a category's colour. Multi-series defaults to the
|
|
1134
1149
|
validated categorical palette `--chart-1`…`--chart-8`.
|
|
1150
|
+
- **Its two strings are overridable**, and until 2.2.0 neither was: a series with no `label` fell
|
|
1151
|
+
back to `Seri 1` — Indonesian, three months after 2.0.0 turned the enhancers English — and the
|
|
1152
|
+
donut's centre caption was hard-coded. Both take the usual hook now:
|
|
1153
|
+
`data-fdy-text-series="Series {n}"` and `data-fdy-text-total="Total"`.
|
|
1135
1154
|
- Format: `data-fdy-format="number|percent|currency"`; legend `data-fdy-legend` (`none` to drop);
|
|
1136
1155
|
axes `data-fdy-axes`
|
|
1137
1156
|
- Sizing is already set by the kit. Override **these**, never a `height` on the chart root (a
|
|
@@ -1328,12 +1347,13 @@ so the same look serves routed sub-navigation built from plain links. See the no
|
|
|
1328
1347
|
| `onClose` | `() => void` | React. Vue emits `close`. Fires from Escape, a backdrop click and the ×, all three of which exist only under `dismissible`. |
|
|
1329
1348
|
| `size?` | `'sm' \| 'md' \| 'lg' \| 'wide'` | Dialog width. |
|
|
1330
1349
|
| `dismissible?` | `boolean` | Whether Escape, the backdrop and the × can close it. Off means the reader has to take a footer action, which is the point of a blocking confirm. |
|
|
1350
|
+
| `closeLabel?` | `string` | Accessible name for the × button. Default `Close`. |
|
|
1331
1351
|
| `footer?` | `ReactNode` | React. Vue: the `footer` slot. |
|
|
1332
1352
|
| `children?` | `ReactNode` | React. The body. Vue: the default slot. |
|
|
1333
1353
|
|
|
1334
|
-
Blazor binds `@bind-Open` and takes `TitleContent` / `ChildContent` / `FooterContent`, plus
|
|
1335
|
-
|
|
1336
|
-
|
|
1354
|
+
Blazor binds `@bind-Open` and takes `TitleContent` / `ChildContent` / `FooterContent`, plus
|
|
1355
|
+
`OnClose`. `CloseLabel` used to be Blazor-only, which left the × in Vue and React fixed at `Close`;
|
|
1356
|
+
all three stacks carry it now.
|
|
1337
1357
|
|
|
1338
1358
|
Native `<dialog>`: focus trap, Esc and backdrop come from the browser. Sizes `--sm` `--md` `--lg`
|
|
1339
1359
|
`--wide`; `--cfl` for the choose-from-list dialog.
|
|
@@ -1381,6 +1401,7 @@ The modal's props with `side` in place of `size`; the behaviour notes there appl
|
|
|
1381
1401
|
| `onClose` | `() => void` | React. Vue emits `close`. |
|
|
1382
1402
|
| `side?` | `'left' \| 'right'` | Which edge it slides from. |
|
|
1383
1403
|
| `dismissible?` | `boolean` | Escape, backdrop and × on or off. |
|
|
1404
|
+
| `closeLabel?` | `string` | Accessible name for the × button. Default `Close`. |
|
|
1384
1405
|
| `footer?` | `ReactNode` | React. Vue: the `footer` slot. |
|
|
1385
1406
|
| `children?` | `ReactNode` | React. The body. Vue: the default slot. |
|
|
1386
1407
|
|
package/README.id.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
> **Lebih banyak _free day_ buat dev. UI kit-nya sudah siap pakai.**
|
|
6
6
|
|
|
7
7
|
[](https://cahyo-dimas.github.io/freeday-ui-kit/)
|
|
8
|
-
[](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v2.2.0)
|
|
9
9
|
|
|
10
10
|
UI KIT yang token-driven & framework-agnostic. Satu sumber kebenaran untuk warna, tipografi,
|
|
11
11
|
spasi, dan komponen. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
> **More free days for devs. The UI kit is ready to use.**
|
|
6
6
|
|
|
7
7
|
[](https://cahyo-dimas.github.io/freeday-ui-kit/)
|
|
8
|
-
[](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v2.2.0)
|
|
9
9
|
|
|
10
10
|
A token-driven, framework-agnostic UI kit. One source of truth for color, typography,
|
|
11
11
|
spacing, and components. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
<div @ref="Root" data-fdy-autocomplete class="fdy-autocomplete">
|
|
9
9
|
<input class="fdy-input" role="combobox" aria-expanded="false" aria-autocomplete="list"
|
|
10
10
|
aria-controls="@_listId" autocomplete="off" value="@Value"
|
|
11
|
-
placeholder="@Placeholder" aria-label="@AriaLabel"
|
|
11
|
+
placeholder="@Placeholder" aria-label="@AriaLabel"
|
|
12
|
+
id="@Id" aria-labelledby="@AriaLabelledby" aria-describedby="@Describedby"
|
|
13
|
+
disabled="@Disabled" readonly="@Readonly"
|
|
14
|
+
aria-invalid="@(Invalid ? "true" : null)" />
|
|
12
15
|
<ul class="fdy-autocomplete__listbox" id="@_listId" role="listbox" hidden>
|
|
13
16
|
@foreach (string item in Options)
|
|
14
17
|
{
|
|
@@ -25,8 +25,34 @@ public partial class FdyAutocomplete
|
|
|
25
25
|
protected override async ValueTask OnHydratedAsync()
|
|
26
26
|
=> await SubscribeAsync("fdy-autocomplete-select", nameof(OnSelect));
|
|
27
27
|
|
|
28
|
+
/// <summary>Explicit id for the input.</summary>
|
|
29
|
+
[Parameter] public string? Id { get; set; }
|
|
30
|
+
|
|
31
|
+
/// <summary>Id of the element that labels the input, when a visible label does the naming.</summary>
|
|
32
|
+
[Parameter] public string? AriaLabelledby { get; set; }
|
|
33
|
+
|
|
34
|
+
/// <summary>Id of the help or error text the input describes itself with.</summary>
|
|
35
|
+
[Parameter] public string? Describedby { get; set; }
|
|
36
|
+
|
|
37
|
+
/// <summary>Greyed and out of the tab order.</summary>
|
|
38
|
+
[Parameter] public bool Disabled { get; set; }
|
|
39
|
+
|
|
40
|
+
/// <summary>Locked/view mode: focusable and showing its value, not editable, and the
|
|
41
|
+
/// suggestion list will not open.</summary>
|
|
42
|
+
[Parameter] public bool Readonly { get; set; }
|
|
43
|
+
|
|
44
|
+
/// <summary>Marks the field invalid (<c>aria-invalid</c>).</summary>
|
|
45
|
+
[Parameter] public bool Invalid { get; set; }
|
|
46
|
+
|
|
28
47
|
protected override bool ShouldRender() => !Hydrated;
|
|
29
48
|
|
|
49
|
+
protected override async Task OnParametersSetAsync()
|
|
50
|
+
{
|
|
51
|
+
if (!Hydrated) return;
|
|
52
|
+
await JS.InvokeVoidAsync("FreedayAutocomplete.setState", Root,
|
|
53
|
+
new { disabled = Disabled, @readonly = Readonly, invalid = Invalid });
|
|
54
|
+
}
|
|
55
|
+
|
|
30
56
|
/// <summary>Invoked by the bridge when the user picks a suggestion.</summary>
|
|
31
57
|
[JSInvokable]
|
|
32
58
|
public async Task OnSelect(SelectDetail detail)
|
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
data-placeholder="@Placeholder"
|
|
12
12
|
data-separator="@Separator"
|
|
13
13
|
data-fdy-text-back="@BackLabel"
|
|
14
|
-
data-fdy-text-submenu="@SubmenuLabel"
|
|
14
|
+
data-fdy-text-submenu="@SubmenuLabel"
|
|
15
|
+
data-id="@Id"
|
|
16
|
+
data-describedby="@Describedby"
|
|
17
|
+
data-disabled="@(Disabled ? "true" : null)"
|
|
18
|
+
data-readonly="@(Readonly ? "true" : null)"
|
|
19
|
+
data-invalid="@(Invalid ? "true" : null)">
|
|
15
20
|
<ul>
|
|
16
21
|
@foreach (FdyCascadeNode node in Nodes)
|
|
17
22
|
{
|
|
@@ -31,8 +31,32 @@ public partial class FdyCascade
|
|
|
31
31
|
protected override async ValueTask OnHydratedAsync()
|
|
32
32
|
=> await SubscribeAsync("fdy-cascade-change", nameof(OnChange));
|
|
33
33
|
|
|
34
|
+
/// <summary>Id for the trigger the enhancer builds, so a form's label can point at it.</summary>
|
|
35
|
+
[Parameter] public string? Id { get; set; }
|
|
36
|
+
|
|
37
|
+
/// <summary>Id of the help or error text the trigger describes itself with.</summary>
|
|
38
|
+
[Parameter] public string? Describedby { get; set; }
|
|
39
|
+
|
|
40
|
+
/// <summary>Greyed and out of the tab order.</summary>
|
|
41
|
+
[Parameter] public bool Disabled { get; set; }
|
|
42
|
+
|
|
43
|
+
/// <summary>Locked/view mode: focusable and showing its value, but it will not open.</summary>
|
|
44
|
+
[Parameter] public bool Readonly { get; set; }
|
|
45
|
+
|
|
46
|
+
/// <summary>Marks the field invalid (<c>aria-invalid</c> + the error styling).</summary>
|
|
47
|
+
[Parameter] public bool Invalid { get; set; }
|
|
48
|
+
|
|
34
49
|
protected override bool ShouldRender() => !Hydrated;
|
|
35
50
|
|
|
51
|
+
/* Pushed, not re-rendered: the seed's <ul> is consumed and removed by the enhancer, so this
|
|
52
|
+
component can never render again. */
|
|
53
|
+
protected override async Task OnParametersSetAsync()
|
|
54
|
+
{
|
|
55
|
+
if (!Hydrated) return;
|
|
56
|
+
await JS.InvokeVoidAsync("FreedayCascade.setState", Root,
|
|
57
|
+
new { disabled = Disabled, @readonly = Readonly, invalid = Invalid });
|
|
58
|
+
}
|
|
59
|
+
|
|
36
60
|
/// <summary>Invoked by the bridge when the user selects a leaf.</summary>
|
|
37
61
|
[JSInvokable]
|
|
38
62
|
public async Task OnChange(ChangeDetail detail)
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
<button id="@ButtonId" type="button" class="fdy-combo__button" role="combobox"
|
|
11
11
|
aria-haspopup="listbox" aria-expanded="false"
|
|
12
12
|
aria-labelledby="@AriaLabelledby"
|
|
13
|
+
aria-describedby="@Describedby"
|
|
13
14
|
aria-invalid="@(Invalid ? "true" : null)"
|
|
14
15
|
aria-readonly="@(Readonly ? "true" : null)"
|
|
15
16
|
disabled="@Disabled">
|
|
@@ -29,6 +29,10 @@ public partial class FdyCombo<TValue>
|
|
|
29
29
|
|
|
30
30
|
[Parameter] public bool Invalid { get; set; }
|
|
31
31
|
|
|
32
|
+
/// <summary>Id of the help or error text the combobox describes itself with. Vue and React
|
|
33
|
+
/// have always had this one; Blazor was the odd stack out.</summary>
|
|
34
|
+
[Parameter] public string? Describedby { get; set; }
|
|
35
|
+
|
|
32
36
|
private readonly string _autoId = $"fdy-combo-{Guid.NewGuid():N}";
|
|
33
37
|
private TValue _lastValue = default!;
|
|
34
38
|
private bool _ready;
|
|
@@ -66,6 +70,15 @@ public partial class FdyCombo<TValue>
|
|
|
66
70
|
_lastValue = Value;
|
|
67
71
|
await JS.InvokeVoidAsync("FreedayBlazor.comboSetValue", Root, KeyOf(Value));
|
|
68
72
|
}
|
|
73
|
+
|
|
74
|
+
/* The three states need the same treatment for the same reason the value did: this
|
|
75
|
+
component stops rendering once hydrated, so a page that disables the field afterwards
|
|
76
|
+
was changing a parameter nothing read. */
|
|
77
|
+
if (_ready)
|
|
78
|
+
{
|
|
79
|
+
await JS.InvokeVoidAsync("FreedayCombo.setState", Root,
|
|
80
|
+
new { disabled = Disabled, @readonly = Readonly, invalid = Invalid });
|
|
81
|
+
}
|
|
69
82
|
}
|
|
70
83
|
|
|
71
84
|
/// <summary>Invoked by the bridge when the user picks an option (enhancer's fdy-change).</summary>
|
|
@@ -10,4 +10,18 @@
|
|
|
10
10
|
data-label="@Label"
|
|
11
11
|
data-placeholder="@Placeholder"
|
|
12
12
|
data-min="@Min"
|
|
13
|
-
data-max="@Max"
|
|
13
|
+
data-max="@Max"
|
|
14
|
+
data-id="@Id"
|
|
15
|
+
data-describedby="@Describedby"
|
|
16
|
+
data-disabled="@(Disabled ? "true" : null)"
|
|
17
|
+
data-readonly="@(Readonly ? "true" : null)"
|
|
18
|
+
data-invalid="@(Invalid ? "true" : null)"
|
|
19
|
+
data-fdy-text-prev-month="@PrevMonthLabel"
|
|
20
|
+
data-fdy-text-next-month="@NextMonthLabel"
|
|
21
|
+
data-fdy-text-prev-year="@PrevYearLabel"
|
|
22
|
+
data-fdy-text-next-year="@NextYearLabel"
|
|
23
|
+
data-fdy-text-prev-years="@PrevYearsLabel"
|
|
24
|
+
data-fdy-text-next-years="@NextYearsLabel"
|
|
25
|
+
data-fdy-text-choose-month="@ChooseMonthLabel"
|
|
26
|
+
data-fdy-text-choose-year="@ChooseYearLabel"
|
|
27
|
+
data-fdy-text-back-to-months="@BackToMonthsLabel"></div>
|
|
@@ -18,6 +18,42 @@ public partial class FdyDatepicker
|
|
|
18
18
|
[Parameter] public string? Min { get; set; }
|
|
19
19
|
[Parameter] public string? Max { get; set; }
|
|
20
20
|
|
|
21
|
+
/// <summary>Id for the trigger the enhancer builds, so a form's own label can point at it.</summary>
|
|
22
|
+
[Parameter] public string? Id { get; set; }
|
|
23
|
+
|
|
24
|
+
/// <summary>Id of the help or error text the trigger describes itself with.</summary>
|
|
25
|
+
[Parameter] public string? Describedby { get; set; }
|
|
26
|
+
|
|
27
|
+
/// <summary>Greyed and out of the tab order.</summary>
|
|
28
|
+
[Parameter] public bool Disabled { get; set; }
|
|
29
|
+
|
|
30
|
+
/// <summary>Locked/view mode: focusable and showing its date, but the calendar will not open.
|
|
31
|
+
/// Unlike <see cref="Disabled"/> it keeps tab order and is not greyed.</summary>
|
|
32
|
+
[Parameter] public bool Readonly { get; set; }
|
|
33
|
+
|
|
34
|
+
/// <summary>Marks the field invalid (<c>aria-invalid</c> + the error styling).</summary>
|
|
35
|
+
[Parameter] public bool Invalid { get; set; }
|
|
36
|
+
|
|
37
|
+
/* The calendar's own navigation labels. Month and weekday NAMES follow the page's `lang`
|
|
38
|
+
through Intl and need nothing here; these are the buttons around them, which until 2.1.1
|
|
39
|
+
were literals inside the enhancer that no host could reach. Leave one null and the
|
|
40
|
+
enhancer's English default stands. */
|
|
41
|
+
[Parameter] public string? PrevMonthLabel { get; set; }
|
|
42
|
+
[Parameter] public string? NextMonthLabel { get; set; }
|
|
43
|
+
[Parameter] public string? PrevYearLabel { get; set; }
|
|
44
|
+
[Parameter] public string? NextYearLabel { get; set; }
|
|
45
|
+
[Parameter] public string? PrevYearsLabel { get; set; }
|
|
46
|
+
[Parameter] public string? NextYearsLabel { get; set; }
|
|
47
|
+
|
|
48
|
+
/// <summary>The title button that drills into the month grid. <c>{label}</c> is the period shown.</summary>
|
|
49
|
+
[Parameter] public string? ChooseMonthLabel { get; set; }
|
|
50
|
+
|
|
51
|
+
/// <summary>The title button that drills into the year grid. <c>{label}</c> is the period shown.</summary>
|
|
52
|
+
[Parameter] public string? ChooseYearLabel { get; set; }
|
|
53
|
+
|
|
54
|
+
/// <summary>The year grid's title, back to months. <c>{start}</c> and <c>{end}</c> are the page range.</summary>
|
|
55
|
+
[Parameter] public string? BackToMonthsLabel { get; set; }
|
|
56
|
+
|
|
21
57
|
protected override ValueTask HydrateAsync() => JS.InvokeVoidAsync("FreedayDatepicker.init", Root);
|
|
22
58
|
|
|
23
59
|
protected override async ValueTask OnHydratedAsync()
|
|
@@ -26,6 +62,16 @@ public partial class FdyDatepicker
|
|
|
26
62
|
// The enhancer owns the built trigger + calendar; never let Blazor re-render the seed.
|
|
27
63
|
protected override bool ShouldRender() => !Hydrated;
|
|
28
64
|
|
|
65
|
+
/* Which is exactly why the three states are pushed rather than re-rendered: the seed above is
|
|
66
|
+
written once, so a page that disables this field later would otherwise be ignored in
|
|
67
|
+
silence. */
|
|
68
|
+
protected override async Task OnParametersSetAsync()
|
|
69
|
+
{
|
|
70
|
+
if (!Hydrated) return;
|
|
71
|
+
await JS.InvokeVoidAsync("FreedayDatepicker.setState", Root,
|
|
72
|
+
new { disabled = Disabled, @readonly = Readonly, invalid = Invalid });
|
|
73
|
+
}
|
|
74
|
+
|
|
29
75
|
/// <summary>Invoked by the bridge when the user picks (or clears) a date.</summary>
|
|
30
76
|
[JSInvokable]
|
|
31
77
|
public async Task OnChange(ChangeDetail detail)
|
|
@@ -14,6 +14,8 @@ export interface FdyDrawerProps {
|
|
|
14
14
|
onClose: () => void;
|
|
15
15
|
side?: 'left' | 'right';
|
|
16
16
|
dismissible?: boolean;
|
|
17
|
+
/** aria-label for the × button. Default 'Close'. */
|
|
18
|
+
closeLabel?: string;
|
|
17
19
|
footer?: ReactNode;
|
|
18
20
|
children?: ReactNode;
|
|
19
21
|
}
|
|
@@ -47,7 +49,7 @@ export function FdyDrawer(props: FdyDrawerProps): JSX.Element {
|
|
|
47
49
|
<div className="fdy-drawer__header">
|
|
48
50
|
<h3 id={titleId} className="fdy-drawer__title">{props.title}</h3>
|
|
49
51
|
{dismissible && (
|
|
50
|
-
<button className="fdy-drawer__close" type="button" aria-label=
|
|
52
|
+
<button className="fdy-drawer__close" type="button" aria-label={props.closeLabel ?? 'Close'} onClick={props.onClose}>×</button>
|
|
51
53
|
)}
|
|
52
54
|
</div>
|
|
53
55
|
|
|
@@ -15,6 +15,8 @@ export interface FdyModalProps {
|
|
|
15
15
|
onClose: () => void;
|
|
16
16
|
size?: 'sm' | 'md' | 'lg' | 'wide';
|
|
17
17
|
dismissible?: boolean;
|
|
18
|
+
/** aria-label for the × button. Default 'Close'. */
|
|
19
|
+
closeLabel?: string;
|
|
18
20
|
footer?: ReactNode;
|
|
19
21
|
children?: ReactNode;
|
|
20
22
|
}
|
|
@@ -51,7 +53,7 @@ export function FdyModal(props: FdyModalProps): JSX.Element {
|
|
|
51
53
|
<div className="fdy-modal__header">
|
|
52
54
|
<h3 id={titleId} className="fdy-modal__title">{props.title}</h3>
|
|
53
55
|
{dismissible && (
|
|
54
|
-
<button className="fdy-modal__close" type="button" aria-label=
|
|
56
|
+
<button className="fdy-modal__close" type="button" aria-label={props.closeLabel ?? 'Close'} onClick={props.onClose}>×</button>
|
|
55
57
|
)}
|
|
56
58
|
</div>
|
|
57
59
|
|
|
@@ -16,6 +16,9 @@ const props = withDefaults(defineProps<{
|
|
|
16
16
|
title: string;
|
|
17
17
|
side?: 'left' | 'right';
|
|
18
18
|
dismissible?: boolean;
|
|
19
|
+
/** aria-label for the × button. Default 'Close'. Blazor has had `CloseLabel` since it
|
|
20
|
+
* shipped; these two hard-coded the string, so a non-English app could not rename it. */
|
|
21
|
+
closeLabel?: string;
|
|
19
22
|
}>(), { dismissible: true });
|
|
20
23
|
|
|
21
24
|
const emit = defineEmits<{
|
|
@@ -58,7 +61,7 @@ function onClick(e: MouseEvent): void {
|
|
|
58
61
|
<h3 :id="titleId" class="fdy-drawer__title">
|
|
59
62
|
<slot name="title">{{ title }}</slot>
|
|
60
63
|
</h3>
|
|
61
|
-
<button v-if="dismissible" class="fdy-drawer__close" type="button" aria-label="Close" @click="$emit('close')">×</button>
|
|
64
|
+
<button v-if="dismissible" class="fdy-drawer__close" type="button" :aria-label="closeLabel ?? 'Close'" @click="$emit('close')">×</button>
|
|
62
65
|
</div>
|
|
63
66
|
|
|
64
67
|
<div class="fdy-drawer__body">
|
|
@@ -18,6 +18,9 @@ const props = withDefaults(defineProps<{
|
|
|
18
18
|
title: string;
|
|
19
19
|
size?: 'sm' | 'md' | 'lg' | 'wide';
|
|
20
20
|
dismissible?: boolean;
|
|
21
|
+
/** aria-label for the × button. Default 'Close'. Blazor has had `CloseLabel` since it
|
|
22
|
+
* shipped; these two hard-coded the string, so a non-English app could not rename it. */
|
|
23
|
+
closeLabel?: string;
|
|
21
24
|
}>(), { dismissible: true });
|
|
22
25
|
|
|
23
26
|
const emit = defineEmits<{
|
|
@@ -65,7 +68,7 @@ function onClick(e: MouseEvent): void {
|
|
|
65
68
|
<h3 :id="titleId" class="fdy-modal__title">
|
|
66
69
|
<slot name="title">{{ title }}</slot>
|
|
67
70
|
</h3>
|
|
68
|
-
<button v-if="dismissible" class="fdy-modal__close" type="button" aria-label="Close" @click="$emit('close')">×</button>
|
|
71
|
+
<button v-if="dismissible" class="fdy-modal__close" type="button" :aria-label="closeLabel ?? 'Close'" @click="$emit('close')">×</button>
|
|
69
72
|
</div>
|
|
70
73
|
|
|
71
74
|
<div class="fdy-modal__body">
|
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
var _pop = null;
|
|
41
41
|
function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(listbox, input); return _pop; }
|
|
42
42
|
function open() {
|
|
43
|
+
/* The input carries these natively; a disabled one fires nothing, but a READONLY one still
|
|
44
|
+
takes focus and clicks, and the list used to open over a field nobody can edit. */
|
|
45
|
+
if (input.readOnly || input.disabled) return;
|
|
43
46
|
if (!listbox.hidden) return;
|
|
44
47
|
var p = popCtl(); if (p) p.show(); else listbox.hidden = false;
|
|
45
48
|
input.setAttribute('aria-expanded', 'true');
|
|
@@ -134,5 +137,18 @@
|
|
|
134
137
|
initAll();
|
|
135
138
|
}
|
|
136
139
|
|
|
137
|
-
|
|
140
|
+
/* Same contract as the combo's: the states live on the input natively, and a host that stops
|
|
141
|
+
re-rendering after hydration needs a way to change them afterwards. */
|
|
142
|
+
function setState(root, state) {
|
|
143
|
+
var input = root ? root.querySelector('input') : null;
|
|
144
|
+
if (!input || !state) return;
|
|
145
|
+
if (state.disabled != null) input.disabled = !!state.disabled;
|
|
146
|
+
if (state.readonly != null) input.readOnly = !!state.readonly;
|
|
147
|
+
if (state.invalid != null) {
|
|
148
|
+
if (state.invalid) input.setAttribute('aria-invalid', 'true');
|
|
149
|
+
else input.removeAttribute('aria-invalid');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
window.FreedayAutocomplete = { init: initAutocomplete, initAll: initAll, setState: setState };
|
|
138
154
|
})();
|
package/dist/freeday-cascade.js
CHANGED
|
@@ -60,6 +60,8 @@
|
|
|
60
60
|
* forking this file. Keeping them in ONE table is also what lets a guard prove none is
|
|
61
61
|
* hard-coded further down. */
|
|
62
62
|
var TEXT = {
|
|
63
|
+
label: 'Select',
|
|
64
|
+
placeholder: 'Select…',
|
|
63
65
|
back: 'Back one level',
|
|
64
66
|
submenu: '{label}, submenu'
|
|
65
67
|
};
|
|
@@ -89,8 +91,8 @@
|
|
|
89
91
|
var root = sourceUl ? parse(sourceUl) : [];
|
|
90
92
|
if (sourceUl) sourceUl.remove();
|
|
91
93
|
|
|
92
|
-
var label = wrap.getAttribute('data-label') || '
|
|
93
|
-
var placeholder = wrap.getAttribute('data-placeholder') || '
|
|
94
|
+
var label = wrap.getAttribute('data-label') || textOf(wrap, 'label');
|
|
95
|
+
var placeholder = wrap.getAttribute('data-placeholder') || textOf(wrap, 'placeholder');
|
|
94
96
|
var sep = wrap.getAttribute('data-separator') || ' / ';
|
|
95
97
|
|
|
96
98
|
var trigger = document.createElement('button');
|
|
@@ -205,7 +207,44 @@
|
|
|
205
207
|
|
|
206
208
|
var _pop = null;
|
|
207
209
|
function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(panel, trigger); return _pop; }
|
|
210
|
+
|
|
211
|
+
/* The three field states the CSS has always styled (`:disabled`, `[aria-readonly="true"]`,
|
|
212
|
+
`[aria-invalid="true"]`) and this enhancer never set, so only the stacks that re-implement
|
|
213
|
+
the control natively had them. Read from the seed at init and settable afterwards, because
|
|
214
|
+
a host that renders once — every Blazor wrapper does, `ShouldRender => false` — cannot
|
|
215
|
+
express a later change any other way. */
|
|
216
|
+
function flagOf(name) {
|
|
217
|
+
var v = wrap.getAttribute('data-' + name);
|
|
218
|
+
return v != null && v !== 'false';
|
|
219
|
+
}
|
|
220
|
+
/* Named `state*` to match the datepicker, where `is*` collided with an older function. */
|
|
221
|
+
var stateDisabled = flagOf('disabled');
|
|
222
|
+
var stateReadonly = flagOf('readonly');
|
|
223
|
+
var stateInvalid = flagOf('invalid');
|
|
224
|
+
/* `data-id` and `data-describedby`: the trigger this enhancer BUILDS is the element a form
|
|
225
|
+
has to point its label and its error text at, and the raw path had no way to say so. */
|
|
226
|
+
if (wrap.getAttribute('data-id')) trigger.id = wrap.getAttribute('data-id');
|
|
227
|
+
if (wrap.getAttribute('data-describedby')) trigger.setAttribute('aria-describedby', wrap.getAttribute('data-describedby'));
|
|
228
|
+
|
|
229
|
+
function applyState() {
|
|
230
|
+
trigger.disabled = stateDisabled;
|
|
231
|
+
if (stateReadonly) trigger.setAttribute('aria-readonly', 'true');
|
|
232
|
+
else trigger.removeAttribute('aria-readonly');
|
|
233
|
+
if (stateInvalid) trigger.setAttribute('aria-invalid', 'true');
|
|
234
|
+
else trigger.removeAttribute('aria-invalid');
|
|
235
|
+
wrap.classList.toggle('fdy-cascade--error', stateInvalid);
|
|
236
|
+
if ((stateDisabled || stateReadonly) && !panel.hidden) close(false);
|
|
237
|
+
}
|
|
238
|
+
function setState(next) {
|
|
239
|
+
if (!next) return;
|
|
240
|
+
if (next.disabled != null) stateDisabled = !!next.disabled;
|
|
241
|
+
if (next.readonly != null) stateReadonly = !!next.readonly;
|
|
242
|
+
if (next.invalid != null) stateInvalid = !!next.invalid;
|
|
243
|
+
applyState();
|
|
244
|
+
}
|
|
245
|
+
|
|
208
246
|
function open() {
|
|
247
|
+
if (stateDisabled || stateReadonly) return;
|
|
209
248
|
if (!panel.hidden) return;
|
|
210
249
|
// Re-open at the selected leaf's level for quick re-selection.
|
|
211
250
|
var trail = selectedValue ? pathTo(root, selectedValue, []) : null;
|
|
@@ -260,8 +299,11 @@
|
|
|
260
299
|
valueSpan.classList.add('fdy-cascade__value--placeholder');
|
|
261
300
|
}
|
|
262
301
|
|
|
302
|
+
applyState();
|
|
303
|
+
|
|
263
304
|
var api = {
|
|
264
305
|
wrap: wrap,
|
|
306
|
+
setState: setState,
|
|
265
307
|
getValue: function () { return selectedValue; },
|
|
266
308
|
clear: function () { selectedValue = ''; valueSpan.textContent = placeholder; valueSpan.classList.add('fdy-cascade__value--placeholder'); }
|
|
267
309
|
};
|
|
@@ -282,5 +324,13 @@
|
|
|
282
324
|
initAll();
|
|
283
325
|
}
|
|
284
326
|
|
|
285
|
-
window.FreedayCascade = {
|
|
327
|
+
window.FreedayCascade = {
|
|
328
|
+
init: initCascade,
|
|
329
|
+
initAll: initAll,
|
|
330
|
+
/* Same reason as the datepicker's: a seed rendered once still has to be lockable later. */
|
|
331
|
+
setState: function (root, state) {
|
|
332
|
+
var api = root && root._fdyCascade ? root._fdyCascade : null;
|
|
333
|
+
if (api && api.setState) api.setState(state);
|
|
334
|
+
}
|
|
335
|
+
};
|
|
286
336
|
})();
|