@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 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
- with one absence there is no `Describedby`, so a Blazor combo cannot point at its own error text.
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 takes six of these `Value` / `ValueChanged`, `Options`, `Placeholder`, `AriaLabel`,
505
- `EmptyText` — and no state flags or ids. Its wrapper renders a fixed seed element and does not
506
- splat unmatched attributes, so those are not reachable from a Blazor page at all.
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
- Blazor calls the tree `Nodes`, adds `SubmenuLabel`, and takes neither the state flags nor the ids.
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
- Blazor's picker is much thinner `Value` / `ValueChanged`, `Label`, `Placeholder`, `Min`, `Max`
679
- and its seed element takes no unmatched attributes, so state flags, ids, `clearable` and every
680
- label above are unreachable there.
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 two
1335
- things Vue and React do not have: `OnClose` and **`CloseLabel`**. The × in Vue and React is labelled
1336
- `Close` and cannot be renamed.
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
  [![Live docs](https://img.shields.io/badge/docs-live-2050d8?style=flat-square)](https://cahyo-dimas.github.io/freeday-ui-kit/)
8
- [![Release](https://img.shields.io/badge/release-v2.1.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v2.1.0)
8
+ [![Release](https://img.shields.io/badge/release-v2.2.0-0078d4?style=flat-square)](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
  [![Live docs](https://img.shields.io/badge/docs-live-2050d8?style=flat-square)](https://cahyo-dimas.github.io/freeday-ui-kit/)
8
- [![Release](https://img.shields.io/badge/release-v2.1.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v2.1.0)
8
+ [![Release](https://img.shields.io/badge/release-v2.2.0-0078d4?style=flat-square)](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"></div>
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="Close" onClick={props.onClose}>&times;</button>
52
+ <button className="fdy-drawer__close" type="button" aria-label={props.closeLabel ?? 'Close'} onClick={props.onClose}>&times;</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="Close" onClick={props.onClose}>&times;</button>
56
+ <button className="fdy-modal__close" type="button" aria-label={props.closeLabel ?? 'Close'} onClick={props.onClose}>&times;</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')">&times;</button>
64
+ <button v-if="dismissible" class="fdy-drawer__close" type="button" :aria-label="closeLabel ?? 'Close'" @click="$emit('close')">&times;</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')">&times;</button>
71
+ <button v-if="dismissible" class="fdy-modal__close" type="button" :aria-label="closeLabel ?? 'Close'" @click="$emit('close')">&times;</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
- window.FreedayAutocomplete = { init: initAutocomplete, initAll: initAll };
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
  })();
@@ -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') || 'Select';
93
- var placeholder = wrap.getAttribute('data-placeholder') || 'Select…';
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 = { init: initCascade, initAll: initAll };
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
  })();