@cahyo-dimas/freeday 3.10.0 → 3.12.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/COMPONENTS.md +72 -3
  3. package/README.id.md +4 -4
  4. package/README.md +3 -3
  5. package/adapters/blazor/FdyCascade.razor +2 -1
  6. package/adapters/blazor/FdyCascade.razor.cs +7 -0
  7. package/adapters/blazor/FdyCombo.razor +2 -1
  8. package/adapters/blazor/FdyCombo.razor.cs +7 -0
  9. package/adapters/blazor/FdyDateRange.razor +20 -2
  10. package/adapters/blazor/FdyDateRange.razor.cs +20 -0
  11. package/adapters/blazor/FdyDatepicker.razor +1 -0
  12. package/adapters/blazor/FdyDatepicker.razor.cs +7 -0
  13. package/adapters/blazor/FdyMonthpicker.razor +1 -0
  14. package/adapters/blazor/FdyMonthpicker.razor.cs +7 -0
  15. package/adapters/blazor/FdyTimepicker.razor +24 -0
  16. package/adapters/blazor/FdyTimepicker.razor.cs +86 -0
  17. package/adapters/react/components/FdyCascade.tsx +10 -1
  18. package/adapters/react/components/FdyCombo.tsx +9 -0
  19. package/adapters/react/components/FdyDateRange.tsx +29 -0
  20. package/adapters/react/components/FdyDatepicker.tsx +10 -1
  21. package/adapters/react/components/FdyMonthpicker.tsx +10 -1
  22. package/adapters/react/components/FdyTimepicker.tsx +221 -0
  23. package/adapters/react/index.d.ts +1 -0
  24. package/adapters/react/index.js +1 -0
  25. package/adapters/vue/components/FdyCascade.vue +10 -1
  26. package/adapters/vue/components/FdyCombo.vue +9 -0
  27. package/adapters/vue/components/FdyDateRange.vue +29 -0
  28. package/adapters/vue/components/FdyDatepicker.vue +10 -1
  29. package/adapters/vue/components/FdyMonthpicker.vue +10 -1
  30. package/adapters/vue/components/FdyTimepicker.vue +285 -0
  31. package/adapters/vue/index.d.ts +1 -0
  32. package/adapters/vue/index.js +1 -0
  33. package/dist/freeday-cascade.js +5 -0
  34. package/dist/freeday-datepicker.js +5 -0
  35. package/dist/freeday-form.js +60 -0
  36. package/dist/freeday-timepicker.js +49 -1
  37. package/dist/freeday.js +119 -1
  38. package/docs/agent-onboarding.md +3 -3
  39. package/docs/getting-started.md +5 -5
  40. package/docs/integrations.md +2 -2
  41. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -9,6 +9,111 @@ 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
+ ## [3.12.0] - 2026-10-27
13
+
14
+ **Sebuah form kini dapat menuntut kontrol kit terisi.** `data-fdy-validate` mewirekan Constraint
15
+ Validation **native**: `isCandidate` menuntut `willValidate` dan menolak `type === 'button'`, dan
16
+ pemicu kontrol kit memang `<button>`. Jadi `form.elements` tak pernah menawarkannya, `required` di
17
+ atasnya tak berarti apa pun, dan form tak bisa menuntut combo, datepicker, monthpicker, timepicker
18
+ atau cascade diisi.
19
+
20
+ Ini yang menahan audit 3.11.0 di 3 dari 7 pemilih. Empat `<input type="time">` di formulir shift
21
+ keamanan-cluster tetap native **hanya** karena menukarnya ke `FdyTimepicker` akan memperbaiki
22
+ penampilannya dan diam-diam membuang penegakan wajib-isinya.
23
+
24
+ Koreksi atas catatan rilis sebelumnya, yang layak dibawa: ia menulis "tiga belas wrapper bertipe
25
+ merender `<button>`". Salah. Hanya **lima** yang berpemicu tombol; `FdyAutocomplete` dan `FdyCfl`
26
+ merender `<input>` asli, dan tujuh sisanya bukan kontrol bernilai. Angka yang dibesarkan membuat
27
+ perbaikan ini terdengar jauh lebih mahal daripada yang ternyata.
28
+
29
+ ### Added
30
+ - **`data-fdy-required` pada akar kontrol** (`#096`), dan prop `required` / `Required` di
31
+ `FdyCombo`, `FdyDatepicker`, `FdyMonthpicker`, `FdyTimepicker` dan `FdyCascade` di ketiga stack.
32
+ Submit ditolak, pesannya dilukis di slot `.fdy-field` yang sudah ada, dan fokusnya ke pemicu.
33
+ Aturannya **tidak menyebut satu pun nama komponen**: nilainya dibaca dari `data-value`, elemen
34
+ yang menerima `aria-invalid` dan fokus ditemukan lewat `aria-haspopup` yang sudah dipasang setiap
35
+ pemicu, dan re-validasinya memakai `MutationObserver` pada atribut itu — supaya enhancer tak
36
+ perlu mengenal lima nama event (`fdy-change`, `fdy-datepicker-change`, `fdy-time-select`,
37
+ `fdy-cascade-change`).
38
+ - **Kontrol yang ditolak diurut menurut urutan DOM** (`#096`). Mengumpulkan kontrol native lebih
39
+ dulu lalu akar kit akan memfokuskan field KEDUA sementara yang pertama juga merah, dan pembaca
40
+ yang dikirim melewati field merah pertamanya belajar hal yang salah.
41
+
42
+ ### Changed
43
+ - **`data-value` di akar kontrol kini cermin hidup, bukan seed.** Di jalur mentah, datepicker,
44
+ timepicker dan cascade hanya MEMBACANYA saat init; `freeday-select.js` sudah menulisnya kembali
45
+ sejak lama, jadi ini membuat keluarganya sepakat alih-alih memperkenalkan atribut kedua. Aditif:
46
+ tak ada yang membaca atribut itu sesudah init, sebab sebelum ini nilainya basi. Cerminnya
47
+ dipasang di `updateDisplay()`, satu titik sempit yang dilalui pick, clear, pilih-bulan dan
48
+ setRange, bukan di tiap tempat event dikirim.
49
+
50
+ ### Fixed
51
+ - **`FdyDatepicker`, `FdyMonthpicker` dan `FdyCascade` tak pernah mencerminkan nilainya ke akar**
52
+ di adapter Vue dan React (`#096`), padahal `FdyCombo` dan `FdyTimepicker` melakukannya. Tanpa itu
53
+ aturan di atas mustahil, dan atributnya juga pegangan yang dapat dibaca uji atau aturan CSS
54
+ keadaan-kosong.
55
+
56
+ ### Catatan
57
+ - `FdyAutocomplete` dan `FdyCfl` **di luar** aturan ini: keduanya merender `<input>` asli, jadi
58
+ `required` native yang cocok. Belum ada prop yang mengeksposnya, dan atribut tak dikenal jatuh ke
59
+ AKAR wrapper bukan ke input di dalamnya, jadi hari ini keduanya tak bisa dijadikan wajib di stack
60
+ mana pun. Dicatat terbuka di `NEXT-UP.md` alih-alih ditutup separuh: mengalirkannya lewat jalur
61
+ kit akan memberi satu field dua sumber kebenaran.
62
+ - Penimpaan pesan per-kontrol (`data-fdy-msg-required` di akar) bekerja di mana atribut mencapai
63
+ akar: jalur mentah, dan Vue, yang meneruskan atribut tak dikenal. React dan Blazor tidak, jadi
64
+ jalur yang terbaca sama di semua stack adalah `data-fdy-text-required` di elemen `<form>`.
65
+
66
+ ## [3.11.0] - 2026-10-26
67
+
68
+ **Audit kontrol native di seluruh layar sebuah aplikasi konsumen, bukan di satu layar yang
69
+ mengeluh.** Pemiliknya melaporkan tampilan HP aslinya tak cocok dengan peramban di lebar yang sama,
70
+ dan dugaan pertama -- termasuk dugaan saya -- adalah CSS. Pengukuran menyangkalnya: dengan
71
+ stylesheet aplikasi yang sebenarnya, pada 375x667, Chromium dan WebKit menata `date`, `time`,
72
+ `number`, `search` dan `text` **identik**, 293x44 keenamnya, selisih satu piksel pada `y` yang
73
+ cuma pembulatan. Jadi yang berbeda bukan tata letaknya, melainkan **widget native-nya**: `date` dan
74
+ `time` digambar sistem operasi, sehingga iOS memberi roda gulir, Android piringan jam dan desktop
75
+ sebuah stepper, masing-masing dengan lebar dan format nilainya sendiri, dan tak ada CSS yang
76
+ menjangkaunya. Itu sebabnya satu HP dan satu peramban di lebar yang sama bisa tidak sepakat soal
77
+ layar yang selebihnya sama.
78
+
79
+ Kit sudah mengatakan ini di tabelnya sendiri ("Use `.fdy-datepicker` / `.fdy-timepicker` /
80
+ `.fdy-datetimepicker` instead") dan sudah mengirim CSS plus enhancer-nya. Yang belum ada adalah
81
+ wrapper bertipe untuk timepicker, jadi aplikasi Vue/React/Blazor harus merakit sendiri
82
+ plumbing custom-event-nya -- dan merakit sendiri adalah yang membuat empat `<input type="time">`
83
+ bertahan di satu formulir shift.
84
+
85
+ Catatan kejujuran: perbedaan iOS itu **tidak** direproduksi di sini. WebKit desktop bukan Mobile
86
+ Safari, dan penampilan kontrol native memang bersifat platform; buktinya adalah tangkapan layar
87
+ perangkat nyata plus fakta terukur bahwa tata letak per lebar sudah identik. Yang dapat diuji
88
+ lokal, dan kini diuji, adalah perilaku penggantinya.
89
+
90
+ ### Added
91
+ - **`FdyTimepicker`** (`#094`), wrapper bertipe di **ketiga** stack, menjadikannya tiga belas.
92
+ Vue dan React mengimplementasikan listbox-nya secara native sebab ia kontrol form -- alasan yang
93
+ sama dengan `FdyCombo`, dan alasan yang `FdyChart` nyatakan dari sisi sebaliknya: enhancer yang
94
+ menguasai DOM berkelahi dengan render framework dan tak menyisakan `v-model`. Blazor
95
+ menjembatani enhancer lewat seed, seperti `FdyDatepicker`. Sorotannya **dijepit, bukan
96
+ berputar**: pada daftar 48 opsi, ArrowUp di `00:00` yang melompat ke `23:30` terbaca sebagai
97
+ kerusakan. Sorotan juga digulirkan ke dalam pandangan, sebab panelnya `max-height:15rem` dan
98
+ sorotan yang tak terlihat bukan sorotan.
99
+ - **`data-id` dan `data-describedby` pada seed timepicker** (`#094`). Keduanya tak terbaca
100
+ sebelumnya, jadi host yang menampilkan teks galat tak punya cara menautkannya. Pemicunya
101
+ `<button>`, yang **bukan** elemen labelable, sehingga `<label for>` memang tak akan pernah
102
+ mengaktifkannya -- itu didokumentasikan sekarang, bukan dibiarkan jadi kejutan.
103
+ - **`FreedayTimepicker.setState(root, { disabled, readonly, invalid })`** (`#094`), bentuknya sama
104
+ dengan `FreedayDatepicker.setState`. Tanpa ini, host yang merender seed-nya sekali lalu tidak
105
+ pernah lagi (Blazor, `ShouldRender => false`) memiliki tiga parameter keadaan yang tak ada yang
106
+ membacanya setelah render pertama.
107
+
108
+ ### Fixed
109
+ - **`FdyDateRange` membuang kedelapan prop label navigasi kalender** (`#095`), di ketiga stack.
110
+ `FdyDatepicker` menerimanya sejak 2.1.1; rentangnya tidak meneruskannya, sehingga rentang tanggal
111
+ adalah **satu-satunya** kontrol tanggal di kit yang navigasinya tak bisa dinamai dalam bahasa
112
+ halamannya sendiri -- aplikasi berbahasa Indonesia mendapat "Previous month" tepat di tempat yang
113
+ tak dapat ia jangkau. Diteruskan apa adanya ke kedua pemilih. Prop datar `xxxLabel`, bukan objek
114
+ `labels`, sebab bentuk itulah yang sudah dipakai setiap komponen lain di kit. Blazor menambah
115
+ `BackToMonthsLabel`, karena seed yang kedua pemilihnya dibangun dari situ punya tombol itu.
116
+
12
117
  ## [3.10.0] - 2026-10-26
13
118
 
14
119
  **Lima rilis dalam satu hari, empat di antaranya menambal satu kelas yang sama, dan pemiliknya
package/COMPONENTS.md CHANGED
@@ -34,8 +34,9 @@ live docs.
34
34
  invalid state does not belong to the control itself.
35
35
  6. **Interactive components need their enhancer script.** Static ones (button, card, badge, table,
36
36
  alert, breadcrumb, timeline, accordion, tree-without-cascade…) are CSS-only.
37
- 7. **On Vue, React or Blazor, twelve components have a typed wrapper. Use it.** `FdyCombo` ·
38
- `FdyDatepicker` · `FdyMonthpicker` · `FdyDateRange` · `FdyAutocomplete` · `FdyCascade` · `FdyCfl` ·
37
+ 7. **On Vue, React or Blazor, thirteen components have a typed wrapper. Use it.** `FdyCombo` ·
38
+ `FdyDatepicker` · `FdyMonthpicker` · `FdyDateRange` · `FdyTimepicker` · `FdyAutocomplete` ·
39
+ `FdyCascade` · `FdyCfl` ·
39
40
  `FdyChart` · `FdyTable` · `FdyModal` · `FdyDrawer` · `FdyAppShell`, from `@cahyo-dimas/freeday/vue`, `/react`, or the
40
41
  `Freeday.Blazor` RCL. Each is flagged at its own section below. Hand-writing their raw markup in
41
42
  those stacks *looks* right at first, because the enhancer initialises once and the first render is
@@ -708,6 +709,7 @@ while each line referenced directly would be read even when hidden. Zero JS; wor
708
709
  | `ariaLabelledby?` | `string` | Id of the element that labels the combobox. |
709
710
  | `placeholder?` | `string` | Shown when no option matches the current value. |
710
711
  | `disabled?` | `boolean` | Greyed and out of the tab order, the native `disabled` semantics. |
712
+ | `required?` | `boolean` | Lets a [`data-fdy-validate`](#form-validation--data-fdy-validate) form demand this control be filled. Renders `data-fdy-required` on the root. Needed because the trigger is a `<button>`, whose `willValidate` is false, so a plain `required` on it has never meant anything. Blazor: `Required`. |
711
713
  | `readonly?` | `boolean` | Locked/view mode: keeps focus and tab order and shows its value, but cannot be opened or changed. Unlike `disabled` it is not greyed. |
712
714
  | `invalid?` | `boolean` | Sets `aria-invalid`; pair it with `describedby` pointing at the error text. |
713
715
  | `describedby?` | `string` | Id of the help or error text (`aria-describedby`). |
@@ -785,6 +787,7 @@ Editable combobox that filters as you type. Needs `freeday-autocomplete.js`.
785
787
  | `placeholder?` | `string` | |
786
788
  | `id?` · `ariaLabelledby?` · `describedby?` | `string` | Trigger id; the element that labels it; the help/error text it points at. |
787
789
  | `disabled?` · `readonly?` · `invalid?` | `boolean` | As on `<FdyCombo>`. |
790
+ | `required?` | `boolean` | Lets a [`data-fdy-validate`](#form-validation--data-fdy-validate) form demand this control be filled. Renders `data-fdy-required` on the root. Needed because the trigger is a `<button>`, whose `willValidate` is false, so a plain `required` on it has never meant anything. Blazor: `Required`. |
788
791
 
789
792
  Raw path: `data-disabled` / `data-readonly` / `data-invalid`, `data-id`, `data-describedby`, and
790
793
  `FreedayCascade.setState(root, { disabled, readonly, invalid })` for a host that rendered once.
@@ -961,6 +964,7 @@ a new class.
961
964
  | `placeholder?` | `string` | |
962
965
  | `id?` · `ariaLabelledby?` · `describedby?` | `string` | Trigger id; the element that labels it; the help/error text it points at. |
963
966
  | `disabled?` · `readonly?` · `invalid?` | `boolean` | `readonly` keeps focus and tab order and shows its date, but it cannot be opened, cleared or changed. |
967
+ | `required?` | `boolean` | Lets a [`data-fdy-validate`](#form-validation--data-fdy-validate) form demand this control be filled. Renders `data-fdy-required` on the root. Needed because the trigger is a `<button>`, whose `willValidate` is false, so a plain `required` on it has never meant anything. Blazor: `Required`. |
964
968
  | `clearable?` | `boolean` | Show a × in the trigger once a date is set, so an optional date can be unset. Sends `''`. Off by default. |
965
969
  | `clearLabel?` | `string` | Accessible name for that ×. Default `Clear date`. |
966
970
  | `prevMonthLabel?` · `nextMonthLabel?` | `string` | The month arrows. Defaults `Previous month` / `Next month`. |
@@ -995,6 +999,7 @@ page's `<html lang>`), `clearable` and `clearLabel` (this path builds no clear b
995
999
  | `placeholder?` | `string` | Shown while nothing is picked. Default `Select month`. |
996
1000
  | `id?` · `ariaLabelledby?` · `describedby?` | `string` | Trigger id; the element that labels it; the help/error text it points at. |
997
1001
  | `disabled?` · `readonly?` · `invalid?` | `boolean` | `readonly` keeps focus and tab order and shows its month, but it cannot be opened, cleared or changed. |
1002
+ | `required?` | `boolean` | Lets a [`data-fdy-validate`](#form-validation--data-fdy-validate) form demand this control be filled. Renders `data-fdy-required` on the root. Needed because the trigger is a `<button>`, whose `willValidate` is false, so a plain `required` on it has never meant anything. Blazor: `Required`. |
998
1003
  | `clearable?` | `boolean` | Show a × in the trigger once a month is set, so an optional month can be unset. Sends `''`. Off by default. |
999
1004
  | `clearLabel?` | `string` | Accessible name for that ×. Default `Clear month`. |
1000
1005
  | `prevYearLabel?` · `nextYearLabel?` | `string` | The arrows beside the month grid, which step a year. Defaults `Previous year` / `Next year`. |
@@ -1020,6 +1025,7 @@ grid's own title.
1020
1025
  | `startPlaceholder?` · `endPlaceholder?` | `string` | One per field. |
1021
1026
  | `ariaLabel?` · `ariaLabelledby?` · `describedby?` | `string` | Names the group (`role="group"`), or points at the text that does. |
1022
1027
  | `disabled?` · `readonly?` · `invalid?` | `boolean` | Applied to both pickers together. |
1028
+ | `prevMonthLabel?` · `nextMonthLabel?` · `prevYearLabel?` · `nextYearLabel?` · `prevYearsLabel?` · `nextYearsLabel?` · `chooseMonthLabel?` · `chooseYearLabel?` | `string` | Forwarded verbatim to **both** pickers. Until 3.11.0 this was the one picker in the kit whose calendar nav could not be named in the page's own language: the single picker took these, the range dropped them. Blazor adds `BackToMonthsLabel`, since the raw seed both pickers are built from has that button. |
1023
1029
 
1024
1030
  Blazor splits the value into two scalars, `From` / `FromChanged` and `To` / `ToChanged`, because a
1025
1031
  C# binding cannot express one object bound in two places, and it adds `Separator`.
@@ -1106,10 +1112,53 @@ it has always been, down to the `YYYY-MM-DD` it reports.
1106
1112
  ```
1107
1113
 
1108
1114
  ## Time picker — `data-fdy-timepicker`
1109
- 24-hour `HH:MM` listbox. `data-step` (minutes, default 30), `data-min`, `data-max`. Needs
1115
+ > **Typed wrapper: `<FdyTimepicker>`**. Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
1116
+
1117
+ 24-hour `HH:MM` listbox. **Author an empty `<div>`** — the enhancer builds everything. Needs
1110
1118
  `freeday-timepicker.js`. Classes: `.fdy-timepicker` (+`--error`) · `__trigger` `__value`
1111
1119
  (+`--placeholder`) `__icon` `__panel` `__opt`.
1112
1120
 
1121
+ Seed attributes: `data-step` (minutes, default 30), `data-min` (default `00:00`), `data-max`
1122
+ (default `23:59`), `data-value`, `data-label`, `data-placeholder` (default `--:--`), `data-id`,
1123
+ `data-describedby`, `data-disabled`, `data-readonly`, `data-invalid`. Emits a bubbling
1124
+ `fdy-time-select` (`detail {value}`). `FreedayTimepicker.setState(root, { disabled, readonly,
1125
+ invalid })` changes the three states afterwards, which is what a host that renders its seed once
1126
+ and never again (Blazor) needs.
1127
+
1128
+ **Use this instead of `<input type="time">`**, and not for theming alone: the native picker is
1129
+ drawn by the *operating system*, so iOS shows a scroll wheel, Android a clock dial and desktop a
1130
+ stepper, each with its own width and value format, and no CSS reaches any of them. A phone and a
1131
+ browser at the same width therefore disagree about a screen that is otherwise identical.
1132
+
1133
+ ```html
1134
+ <div data-fdy-timepicker data-step="15" data-min="06:00" data-max="22:00"
1135
+ data-value="07:00" data-label="Jam mulai"></div>
1136
+ ```
1137
+
1138
+ ### Props — `<FdyTimepicker>`
1139
+
1140
+ | Prop | Type | What it does |
1141
+ |---|---|---|
1142
+ | `modelValue` · `value` | `string \| null` | `HH:MM`, 24-hour. `''`/`null` for empty. A loose `H:MM` is normalised for matching. Blazor: `Value` / `@bind-Value`. |
1143
+ | `onChange` | `(value: string) => void` | React. Vue emits `update:modelValue` and `change`, Blazor `ValueChanged`. |
1144
+ | `step?` | `number` | Minutes between options. Default 30. |
1145
+ | `min?` · `max?` | `string` | `HH:MM` bounds, inclusive. Default `00:00` to `23:59`. |
1146
+ | `placeholder?` | `string` | Shown when there is no value. Default `--:--`. |
1147
+ | `ariaLabel?` · `ariaLabelledby?` · `describedby?` | `string` | Names the trigger, or points at the text that does. Blazor: `Label` / `Describedby`. |
1148
+ | `id?` | `string` | Id for the trigger. It is a `<button>`, which is **not** a labelable element, so a `<label for>` will not activate it — name it with `ariaLabel`/`ariaLabelledby`. |
1149
+ | `disabled?` · `readonly?` · `invalid?` | `boolean` | `readonly` keeps tab order and shows the time but will not open. |
1150
+ | `required?` | `boolean` | Lets a [`data-fdy-validate`](#form-validation--data-fdy-validate) form demand this control be filled. Renders `data-fdy-required` on the root. Needed because the trigger is a `<button>`, whose `willValidate` is false, so a plain `required` on it has never meant anything. Blazor: `Required`. |
1151
+
1152
+ Keyboard: `ArrowDown`/`ArrowUp` move one option, `Home`/`End` jump to the ends, `Enter`/`Space`
1153
+ pick, `Escape` closes. The highlight is **clamped, not cyclic** — `ArrowUp` at `00:00` stays there,
1154
+ because wrapping to `23:30` reads as a glitch in a list this long — and it is scrolled into view,
1155
+ since the panel is `max-height:15rem` and a highlight nobody can see is no highlight.
1156
+
1157
+ **Deliberate difference between the two paths.** The wrappers keep focus on the **trigger** with
1158
+ `aria-activedescendant` on it, which is what `FdyCombo`, `FdyDatepicker` and `FdyAutocomplete` do;
1159
+ the raw enhancer moves focus **into the panel**. Both are valid APG listbox patterns, and the Vue
1160
+ and React adapters follow their own family rather than the enhancer.
1161
+
1113
1162
  ## Datetime picker — `data-fdy-datetimepicker`
1114
1163
  A **composition** of the two above, not a monolith: nest one datepicker and one timepicker inside
1115
1164
  the wrapper. `freeday-datetime.js` joins them and emits one `fdy-datetime-change` with
@@ -1179,6 +1228,26 @@ blur/input. Needs `freeday-form.js`.
1179
1228
  - Per-control messages: `data-fdy-msg-required`, `-type`, `-minlength`, `-min`, `-mismatch`, …
1180
1229
  - Cross-field: `data-fdy-match="#otherFieldId"`
1181
1230
  - Emits `fdy-form-valid` / `fdy-form-invalid`.
1231
+ - **A kit control can be required too** — `data-fdy-required` on its root. Combo, datepicker,
1232
+ monthpicker, timepicker and cascade build a `<button>` trigger, and a button's `willValidate` is
1233
+ false, so `form.elements` never offered one and `required` on it meant nothing. The enhancer now
1234
+ collects these roots alongside the native controls: empty `data-value` refuses the submit, the
1235
+ message lands in the same `.fdy-field` slot, and the focus goes to the trigger. It names no
1236
+ component — the value is read from `data-value` and the trigger is found by `aria-haspopup`. In
1237
+ the typed stacks pass `required` / `Required` instead of writing the attribute.
1238
+ **`FdyAutocomplete` and `FdyCfl` are outside this rule**: they render a real `<input>`, so the
1239
+ native `required` is the mechanism that fits them — but no prop exposes it yet, and an unknown
1240
+ attribute falls through to a wrapper's ROOT rather than to the input inside it, so today an app
1241
+ cannot require either one. Tracked in `NEXT-UP.md`; the rule above deliberately does not cover
1242
+ them, because painting a native input through the kit path would give it two sources of truth.
1243
+ Re-validation watches the attribute with a `MutationObserver`, so no control has to announce
1244
+ itself. The message override is per-control (`data-fdy-msg-required` on the root) wherever
1245
+ attributes reach the root — the raw path, and Vue, where an unknown attribute falls through. In
1246
+ React and Blazor they do not, so use the form-level `data-fdy-text-required`, which reads the
1247
+ same in every stack.
1248
+ - **`data-value` on a control's root is the live value**, not just the seed it used to be on the
1249
+ datepicker, timepicker and cascade (the combo always kept it current). That is what makes the
1250
+ rule above possible, and it is also the handle a test or a CSS empty-state rule can read.
1182
1251
  - **A refused field is revealed before it is focused**, because `focus()` on a control that is not
1183
1252
  rendered does nothing. A closed `<details>` around it (accordion, tree, nav group) is opened, an
1184
1253
  unselected tab is selected by clicking it, and a `.fdy-step-panel` left behind is returned to by
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-v3.10.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.10.0)
8
+ [![Release](https://img.shields.io/badge/release-v3.12.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.12.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`.
@@ -44,9 +44,9 @@ enhancer:
44
44
  | **Blazor (net8.0)** | RCL `Freeday.Blazor` | `@bind-Value` |
45
45
  | HTML statis · Svelte · template server-rendered | *(tanpa wrapper)* markup mentah + enhancer, di bawah | event `fdy-*` |
46
46
 
47
- Kedua belasnya: `FdyCombo` · `FdyDatepicker` · `FdyMonthpicker` · `FdyDateRange` ·
48
- `FdyAutocomplete` · `FdyCascade` · `FdyCfl` · `FdyChart` · `FdyTable` · `FdyModal` · `FdyDrawer` ·
49
- `FdyAppShell`. Menulis markup mentah untuk kedua belas ini di dalam Vue/React/Blazor *kelihatan* baik-baik saja pada mulanya, karena render
47
+ Ketiga belasnya: `FdyCombo` · `FdyDatepicker` · `FdyMonthpicker` · `FdyDateRange` ·
48
+ `FdyTimepicker` · `FdyAutocomplete` · `FdyCascade` · `FdyCfl` · `FdyChart` · `FdyTable` · `FdyModal` · `FdyDrawer` ·
49
+ `FdyAppShell`. Menulis markup mentah untuk ketiga belas ini di dalam Vue/React/Blazor *kelihatan* baik-baik saja pada mulanya, karena render
50
50
  pertamanya memang benar. Setelah itu ia rusak diam-diam: DOM yang di-render framework belakangan tak pernah ter-hydrate, dan state widget
51
51
  berakhir di DOM alih-alih di state framework-mu. Selebihnya (button, card, badge, alert, markup
52
52
  tabel, layout, dan komponen interaktif yang tak punya wrapper) sama persis di semua stack. Untuk
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-v3.10.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.10.0)
8
+ [![Release](https://img.shields.io/badge/release-v3.12.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.12.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`.
@@ -44,8 +44,8 @@ enhancer:
44
44
  | **Blazor (net8.0)** | `Freeday.Blazor` RCL | `@bind-Value` |
45
45
  | Static HTML · Svelte · server-rendered templates | *(no wrapper)* raw markup + enhancers, below | `fdy-*` events |
46
46
 
47
- The twelve: `FdyCombo` · `FdyDatepicker` · `FdyMonthpicker` · `FdyDateRange` · `FdyAutocomplete` ·
48
- `FdyCascade` · `FdyCfl` · `FdyChart` · `FdyTable` · `FdyModal` · `FdyDrawer` · `FdyAppShell`.
47
+ The thirteen: `FdyCombo` · `FdyDatepicker` · `FdyMonthpicker` · `FdyDateRange` · `FdyTimepicker` ·
48
+ `FdyAutocomplete` · `FdyCascade` · `FdyCfl` · `FdyChart` · `FdyTable` · `FdyModal` · `FdyDrawer` · `FdyAppShell`.
49
49
  Hand-writing the raw
50
50
  markup for these inside Vue/React/Blazor *looks* fine at first, because the first render is correct.
51
51
  Then it breaks quietly:
@@ -16,7 +16,8 @@
16
16
  data-describedby="@Describedby"
17
17
  data-disabled="@(Disabled ? "true" : null)"
18
18
  data-readonly="@(Readonly ? "true" : null)"
19
- data-invalid="@(Invalid ? "true" : null)">
19
+ data-invalid="@(Invalid ? "true" : null)"
20
+ data-fdy-required="@(Required ? "" : null)">
20
21
  <ul>
21
22
  @foreach (FdyCascadeNode node in Nodes)
22
23
  {
@@ -46,6 +46,13 @@ public partial class FdyCascade
46
46
  /// <summary>Marks the field invalid (<c>aria-invalid</c> + the error styling).</summary>
47
47
  [Parameter] public bool Invalid { get; set; }
48
48
 
49
+ /// <summary>Let a <c>data-fdy-validate</c> form demand this control be filled. Renders
50
+ /// <c>data-fdy-required</c> on the root, and <c>freeday-form.js</c> refuses the submit, paints
51
+ /// the message in the field and focuses the trigger. Needed because the trigger is a
52
+ /// <c>&lt;button&gt;</c>: <c>willValidate</c> is false, so a plain <c>required</c> on it has
53
+ /// never meant anything. Override the message with <c>data-fdy-msg-required</c> on the root.</summary>
54
+ [Parameter] public bool Required { get; set; }
55
+
49
56
  protected override bool ShouldRender() => !Hydrated;
50
57
 
51
58
  /* Pushed, not re-rendered: the seed's <ul> is consumed and removed by the enhancer, so this
@@ -6,7 +6,8 @@
6
6
  markup the freeday-select.js enhancer expects; the enhancer owns the open/keyboard/focus
7
7
  behaviour and emits fdy-change, which flows back to @bind-Value. An externally-changed Value
8
8
  is pushed onto the enhancer silently (no event echo). *@
9
- <div @ref="Root" class="@RootClass" data-fdy-combo data-value="@SelectedKey">
9
+ <div @ref="Root" class="@RootClass" data-fdy-combo data-value="@SelectedKey"
10
+ data-fdy-required="@(Required ? "" : null)">
10
11
  <button id="@ButtonId" type="button" class="fdy-combo__button" role="combobox"
11
12
  aria-haspopup="listbox" aria-expanded="false"
12
13
  aria-labelledby="@AriaLabelledby"
@@ -29,6 +29,13 @@ public partial class FdyCombo<TValue>
29
29
 
30
30
  [Parameter] public bool Invalid { get; set; }
31
31
 
32
+ /// <summary>Let a <c>data-fdy-validate</c> form demand this control be filled. Renders
33
+ /// <c>data-fdy-required</c> on the root, and <c>freeday-form.js</c> refuses the submit, paints
34
+ /// the message in the field and focuses the trigger. Needed because the trigger is a
35
+ /// <c>&lt;button&gt;</c>: <c>willValidate</c> is false, so a plain <c>required</c> on it has
36
+ /// never meant anything. Override the message with <c>data-fdy-msg-required</c> on the root.</summary>
37
+ [Parameter] public bool Required { get; set; }
38
+
32
39
  /// <summary>Id of the help or error text the combobox describes itself with. Vue and React
33
40
  /// have always had this one; Blazor was the odd stack out.</summary>
34
41
  [Parameter] public string? Describedby { get; set; }
@@ -8,9 +8,27 @@
8
8
  <div @ref="Root">
9
9
  <div data-fdy-daterange role="group" aria-label="@AriaLabel">
10
10
  <div data-fdy-datepicker data-role="from"
11
- data-value="@From" data-placeholder="@FromPlaceholder" data-min="@Min" data-max="@Max"></div>
11
+ data-value="@From" data-placeholder="@FromPlaceholder" data-min="@Min" data-max="@Max"
12
+ data-fdy-text-prev-month="@PrevMonthLabel"
13
+ data-fdy-text-next-month="@NextMonthLabel"
14
+ data-fdy-text-prev-year="@PrevYearLabel"
15
+ data-fdy-text-next-year="@NextYearLabel"
16
+ data-fdy-text-prev-years="@PrevYearsLabel"
17
+ data-fdy-text-next-years="@NextYearsLabel"
18
+ data-fdy-text-choose-month="@ChooseMonthLabel"
19
+ data-fdy-text-choose-year="@ChooseYearLabel"
20
+ data-fdy-text-back-to-months="@BackToMonthsLabel"></div>
12
21
  <span class="fdy-daterange__sep">@Separator</span>
13
22
  <div data-fdy-datepicker data-role="to"
14
- data-value="@To" data-placeholder="@ToPlaceholder" data-min="@Min" data-max="@Max"></div>
23
+ data-value="@To" data-placeholder="@ToPlaceholder" data-min="@Min" data-max="@Max"
24
+ data-fdy-text-prev-month="@PrevMonthLabel"
25
+ data-fdy-text-next-month="@NextMonthLabel"
26
+ data-fdy-text-prev-year="@PrevYearLabel"
27
+ data-fdy-text-next-year="@NextYearLabel"
28
+ data-fdy-text-prev-years="@PrevYearsLabel"
29
+ data-fdy-text-next-years="@NextYearsLabel"
30
+ data-fdy-text-choose-month="@ChooseMonthLabel"
31
+ data-fdy-text-choose-year="@ChooseYearLabel"
32
+ data-fdy-text-back-to-months="@BackToMonthsLabel"></div>
15
33
  </div>
16
34
  </div>
@@ -23,6 +23,26 @@ public partial class FdyDateRange
23
23
  [Parameter] public string? Max { get; set; }
24
24
  [Parameter] public string Separator { get; set; } = "–";
25
25
 
26
+ /* The calendars' own navigation labels, forwarded verbatim to BOTH pickers. Month and weekday
27
+ NAMES follow the page's `lang` through Intl and need nothing here; these are the buttons
28
+ around them. Until now the range was the one picker in the kit whose nav could not be named
29
+ in the page's own language -- FdyDatepicker took these, FdyDateRange dropped them, so an
30
+ Indonesian app got "Previous month" on the only date control it could not reach. Raised
31
+ from keamanan-cluster (#095). */
32
+ [Parameter] public string? PrevMonthLabel { get; set; }
33
+ [Parameter] public string? NextMonthLabel { get; set; }
34
+ [Parameter] public string? PrevYearLabel { get; set; }
35
+ [Parameter] public string? NextYearLabel { get; set; }
36
+ [Parameter] public string? PrevYearsLabel { get; set; }
37
+ [Parameter] public string? NextYearsLabel { get; set; }
38
+ [Parameter] public string? ChooseMonthLabel { get; set; }
39
+ [Parameter] public string? ChooseYearLabel { get; set; }
40
+
41
+ /// <summary>The year grid's title, back to months. Blazor only, because the raw seed both
42
+ /// pickers are built from supports it and <see cref="FdyDatepicker"/> already exposes it;
43
+ /// the Vue and React adapters build no such button.</summary>
44
+ [Parameter] public string? BackToMonthsLabel { get; set; }
45
+
26
46
  private int _token;
27
47
 
28
48
  // initAll(Root) links the daterange (descendant) and inits both child pickers.
@@ -18,6 +18,7 @@
18
18
  data-disabled="@(Disabled ? "true" : null)"
19
19
  data-readonly="@(Readonly ? "true" : null)"
20
20
  data-invalid="@(Invalid ? "true" : null)"
21
+ data-fdy-required="@(Required ? "" : null)"
21
22
  data-fdy-text-prev-month="@PrevMonthLabel"
22
23
  data-fdy-text-next-month="@NextMonthLabel"
23
24
  data-fdy-text-prev-year="@PrevYearLabel"
@@ -54,6 +54,13 @@ public partial class FdyDatepicker
54
54
  /// <summary>Marks the field invalid (<c>aria-invalid</c> + the error styling).</summary>
55
55
  [Parameter] public bool Invalid { get; set; }
56
56
 
57
+ /// <summary>Let a <c>data-fdy-validate</c> form demand this control be filled. Renders
58
+ /// <c>data-fdy-required</c> on the root, and <c>freeday-form.js</c> refuses the submit, paints
59
+ /// the message in the field and focuses the trigger. Needed because the trigger is a
60
+ /// <c>&lt;button&gt;</c>: <c>willValidate</c> is false, so a plain <c>required</c> on it has
61
+ /// never meant anything. Override the message with <c>data-fdy-msg-required</c> on the root.</summary>
62
+ [Parameter] public bool Required { get; set; }
63
+
57
64
  /* The calendar's own navigation labels. Month and weekday NAMES follow the page's `lang`
58
65
  through Intl and need nothing here; these are the buttons around them, which until 2.1.1
59
66
  were literals inside the enhancer that no host could reach. Leave one null and the
@@ -21,6 +21,7 @@
21
21
  data-disabled="@(Disabled ? "true" : null)"
22
22
  data-readonly="@(Readonly ? "true" : null)"
23
23
  data-invalid="@(Invalid ? "true" : null)"
24
+ data-fdy-required="@(Required ? "" : null)"
24
25
  data-fdy-text-prev-year="@PrevYearLabel"
25
26
  data-fdy-text-next-year="@NextYearLabel"
26
27
  data-fdy-text-prev-years="@PrevYearsLabel"
@@ -57,6 +57,13 @@ public partial class FdyMonthpicker
57
57
  /// <summary>Marks the field invalid (<c>aria-invalid</c> + the error styling).</summary>
58
58
  [Parameter] public bool Invalid { get; set; }
59
59
 
60
+ /// <summary>Let a <c>data-fdy-validate</c> form demand this control be filled. Renders
61
+ /// <c>data-fdy-required</c> on the root, and <c>freeday-form.js</c> refuses the submit, paints
62
+ /// the message in the field and focuses the trigger. Needed because the trigger is a
63
+ /// <c>&lt;button&gt;</c>: <c>willValidate</c> is false, so a plain <c>required</c> on it has
64
+ /// never meant anything. Override the message with <c>data-fdy-msg-required</c> on the root.</summary>
65
+ [Parameter] public bool Required { get; set; }
66
+
60
67
  /* The calendar's navigation labels, and only the ones this picker shows. Month NAMES follow the
61
68
  page's `lang` through Intl and need nothing here. The day grid's own arrows are absent along
62
69
  with the day grid, so there is no PrevMonthLabel to pass: the arrows beside a month grid step
@@ -0,0 +1,24 @@
1
+ @namespace Freeday.Blazor
2
+ @inherits FreedayComponentBase<FdyTimepicker>
3
+
4
+ @* Seed-based wrapper over freeday-timepicker.js: the enhancer builds the input-styled trigger
5
+ + time listbox from this empty div and emits fdy-time-select. The enhancer owns that DOM, so
6
+ the seed renders once (ShouldRender => false) and the picked time is read back through
7
+ @bind-Value. Value is "HH:MM" (24-hour) or "".
8
+
9
+ This is what replaces <input type="time">, and not for theming alone: the native picker is
10
+ drawn by the OPERATING SYSTEM, so iOS shows a scroll wheel, Android a clock dial and desktop a
11
+ stepper, each with its own width and value format, and no CSS reaches any of them (#094). *@
12
+ <div @ref="Root" data-fdy-timepicker
13
+ data-value="@Value"
14
+ data-label="@Label"
15
+ data-placeholder="@Placeholder"
16
+ data-step="@StepAttribute"
17
+ data-min="@Min"
18
+ data-max="@Max"
19
+ data-id="@Id"
20
+ data-describedby="@Describedby"
21
+ data-disabled="@(Disabled ? "true" : null)"
22
+ data-readonly="@(Readonly ? "true" : null)"
23
+ data-invalid="@(Invalid ? "true" : null)"
24
+ data-fdy-required="@(Required ? "" : null)"></div>
@@ -0,0 +1,86 @@
1
+ using Microsoft.AspNetCore.Components;
2
+ using Microsoft.JSInterop;
3
+
4
+ namespace Freeday.Blazor;
5
+
6
+ public partial class FdyTimepicker
7
+ {
8
+ /// <summary>Selected time as <c>HH:MM</c> (24-hour), or "" when unset. Two-way bindable
9
+ /// via <c>@bind-Value</c>.</summary>
10
+ [Parameter] public string? Value { get; set; }
11
+ [Parameter] public EventCallback<string?> ValueChanged { get; set; }
12
+
13
+ /// <summary>Accessible label for the trigger.</summary>
14
+ [Parameter] public string? Label { get; set; }
15
+
16
+ /// <summary>Shown when there is no value. The enhancer's own default is <c>--:--</c>.</summary>
17
+ [Parameter] public string? Placeholder { get; set; }
18
+
19
+ /// <summary>Minutes between options. The enhancer's own default is 30.</summary>
20
+ [Parameter] public int? Step { get; set; }
21
+
22
+ /* null, not "30", for the default: an attribute the enhancer does not need is one no reader
23
+ of the seed has to interpret, and the enhancer already falls back to 30. */
24
+ private string? StepAttribute => Step is null ? null : Step.Value.ToString();
25
+
26
+ /// <summary>Earliest selectable time (<c>HH:MM</c>). Default <c>00:00</c>.</summary>
27
+ [Parameter] public string? Min { get; set; }
28
+
29
+ /// <summary>Latest selectable time (<c>HH:MM</c>). Default <c>23:59</c>.</summary>
30
+ [Parameter] public string? Max { get; set; }
31
+
32
+ /// <summary>Id for the trigger the enhancer builds, so <c>aria-describedby</c> targets and
33
+ /// other references can point at it. Note the trigger is a <c>&lt;button&gt;</c>, which is not
34
+ /// a labelable element, so a <c>&lt;label for&gt;</c> will not activate it — use
35
+ /// <see cref="Label"/>.</summary>
36
+ [Parameter] public string? Id { get; set; }
37
+
38
+ /// <summary>Id of the help or error text the trigger describes itself with.</summary>
39
+ [Parameter] public string? Describedby { get; set; }
40
+
41
+ /// <summary>Greyed and out of the tab order.</summary>
42
+ [Parameter] public bool Disabled { get; set; }
43
+
44
+ /// <summary>Locked/view mode: focusable and showing its time, but the list will not open.
45
+ /// Unlike <see cref="Disabled"/> it keeps tab order and is not greyed.</summary>
46
+ [Parameter] public bool Readonly { get; set; }
47
+
48
+ /// <summary>Marks the field invalid (<c>aria-invalid</c> + the error styling).</summary>
49
+ [Parameter] public bool Invalid { get; set; }
50
+
51
+ /// <summary>Let a <c>data-fdy-validate</c> form demand this control be filled. Renders
52
+ /// <c>data-fdy-required</c> on the root, and <c>freeday-form.js</c> refuses the submit, paints
53
+ /// the message in the field and focuses the trigger. Needed because the trigger is a
54
+ /// <c>&lt;button&gt;</c>: <c>willValidate</c> is false, so a plain <c>required</c> on it has
55
+ /// never meant anything. Override the message with <c>data-fdy-msg-required</c> on the root.</summary>
56
+ [Parameter] public bool Required { get; set; }
57
+
58
+ // The enhancer owns this control's DOM; init it directly (initAll would skip Root itself).
59
+ protected override ValueTask HydrateAsync() => JS.InvokeVoidAsync("FreedayTimepicker.init", Root);
60
+
61
+ protected override async ValueTask OnHydratedAsync()
62
+ => await SubscribeAsync("fdy-time-select", nameof(OnSelect));
63
+
64
+ // The enhancer owns the built trigger + listbox; never let Blazor re-render the seed.
65
+ protected override bool ShouldRender() => !Hydrated;
66
+
67
+ /* Which is exactly why the three states are pushed rather than re-rendered: the seed above is
68
+ written once, so a page that disables this field later would otherwise be ignored. */
69
+ protected override async Task OnParametersSetAsync()
70
+ {
71
+ if (!Hydrated) return;
72
+ await JS.InvokeVoidAsync("FreedayTimepicker.setState", Root,
73
+ new { disabled = Disabled, @readonly = Readonly, invalid = Invalid });
74
+ }
75
+
76
+ /// <summary>Invoked by the bridge when the reader picks a time (enhancer's fdy-time-select).</summary>
77
+ [JSInvokable]
78
+ public async Task OnSelect(SelectDetail detail)
79
+ {
80
+ Value = detail.Value;
81
+ await ValueChanged.InvokeAsync(detail.Value);
82
+ }
83
+
84
+ /// <summary>The detail shape of the enhancer's <c>fdy-time-select</c> event.</summary>
85
+ public sealed record SelectDetail(string Value);
86
+ }
@@ -33,6 +33,14 @@ export interface FdyCascadeProps {
33
33
  disabled?: boolean;
34
34
  /** Locked/view mode: stays focusable and shows its value, but can't be opened or changed. Unlike `disabled`, it keeps tab order and isn't greyed. */
35
35
  readonly?: boolean;
36
+ /**
37
+ * Let a `data-fdy-validate` form demand this control be filled. It renders `data-fdy-required`
38
+ * on the root, and `freeday-form.js` refuses the submit, paints the message in the field and
39
+ * focuses the trigger. Needed because the trigger is a `<button>`: `willValidate` is false, so
40
+ * a plain `required` on it has never meant anything (#096). Override the message per control
41
+ * with `data-fdy-msg-required` on the root.
42
+ */
43
+ required?: boolean;
36
44
  invalid?: boolean;
37
45
  describedby?: string;
38
46
  }
@@ -168,7 +176,8 @@ export function FdyCascade(props: FdyCascadeProps): JSX.Element {
168
176
  }, [active, open, stack]);
169
177
 
170
178
  return (
171
- <div ref={rootRef} className={isInvalid ? 'fdy-cascade fdy-cascade--error' : 'fdy-cascade'}>
179
+ <div ref={rootRef} className={isInvalid ? 'fdy-cascade fdy-cascade--error' : 'fdy-cascade'}
180
+ data-value={props.value} data-fdy-required={props.required === true ? '' : undefined}>
172
181
  <button
173
182
  ref={triggerRef}
174
183
  id={triggerId}
@@ -17,6 +17,14 @@ export interface FdyComboProps<T extends string> {
17
17
  disabled?: boolean;
18
18
  /** Locked/view mode: stays focusable and shows its value, but can't be opened or changed. Unlike `disabled`, it keeps tab order and isn't greyed. */
19
19
  readonly?: boolean;
20
+ /**
21
+ * Let a `data-fdy-validate` form demand this control be filled. It renders `data-fdy-required`
22
+ * on the root, and `freeday-form.js` refuses the submit, paints the message in the field and
23
+ * focuses the trigger. Needed because the trigger is a `<button>`: `willValidate` is false, so
24
+ * a plain `required` on it has never meant anything (#096). Override the message per control
25
+ * with `data-fdy-msg-required` on the root.
26
+ */
27
+ required?: boolean;
20
28
  invalid?: boolean;
21
29
  describedby?: string;
22
30
  }
@@ -128,6 +136,7 @@ export function FdyCombo<T extends string>(props: FdyComboProps<T>): JSX.Element
128
136
  ref={rootRef}
129
137
  className={isInvalid ? 'fdy-combo fdy-combo--error' : 'fdy-combo'}
130
138
  data-value={props.value}
139
+ data-fdy-required={props.required === true ? '' : undefined}
131
140
  onKeyDown={onKeyDown}
132
141
  onBlur={onFocusout}
133
142
  >