@cahyo-dimas/freeday 3.11.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.
- package/CHANGELOG.md +54 -0
- package/COMPONENTS.md +25 -0
- package/README.id.md +1 -1
- package/README.md +1 -1
- package/adapters/blazor/FdyCascade.razor +2 -1
- package/adapters/blazor/FdyCascade.razor.cs +7 -0
- package/adapters/blazor/FdyCombo.razor +2 -1
- package/adapters/blazor/FdyCombo.razor.cs +7 -0
- package/adapters/blazor/FdyDatepicker.razor +1 -0
- package/adapters/blazor/FdyDatepicker.razor.cs +7 -0
- package/adapters/blazor/FdyMonthpicker.razor +1 -0
- package/adapters/blazor/FdyMonthpicker.razor.cs +7 -0
- package/adapters/blazor/FdyTimepicker.razor +2 -1
- package/adapters/blazor/FdyTimepicker.razor.cs +7 -0
- package/adapters/react/components/FdyCascade.tsx +10 -1
- package/adapters/react/components/FdyCombo.tsx +9 -0
- package/adapters/react/components/FdyDatepicker.tsx +10 -1
- package/adapters/react/components/FdyMonthpicker.tsx +10 -1
- package/adapters/react/components/FdyTimepicker.tsx +9 -0
- package/adapters/vue/components/FdyCascade.vue +10 -1
- package/adapters/vue/components/FdyCombo.vue +9 -0
- package/adapters/vue/components/FdyDatepicker.vue +10 -1
- package/adapters/vue/components/FdyMonthpicker.vue +10 -1
- package/adapters/vue/components/FdyTimepicker.vue +9 -0
- package/dist/freeday-cascade.js +5 -0
- package/dist/freeday-datepicker.js +5 -0
- package/dist/freeday-form.js +60 -0
- package/dist/freeday-timepicker.js +5 -0
- package/dist/freeday.js +75 -0
- package/docs/getting-started.md +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,60 @@ 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
|
+
|
|
12
66
|
## [3.11.0] - 2026-10-26
|
|
13
67
|
|
|
14
68
|
**Audit kontrol native di seluruh layar sebuah aplikasi konsumen, bukan di satu layar yang
|
package/COMPONENTS.md
CHANGED
|
@@ -709,6 +709,7 @@ while each line referenced directly would be read even when hidden. Zero JS; wor
|
|
|
709
709
|
| `ariaLabelledby?` | `string` | Id of the element that labels the combobox. |
|
|
710
710
|
| `placeholder?` | `string` | Shown when no option matches the current value. |
|
|
711
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`. |
|
|
712
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. |
|
|
713
714
|
| `invalid?` | `boolean` | Sets `aria-invalid`; pair it with `describedby` pointing at the error text. |
|
|
714
715
|
| `describedby?` | `string` | Id of the help or error text (`aria-describedby`). |
|
|
@@ -786,6 +787,7 @@ Editable combobox that filters as you type. Needs `freeday-autocomplete.js`.
|
|
|
786
787
|
| `placeholder?` | `string` | |
|
|
787
788
|
| `id?` · `ariaLabelledby?` · `describedby?` | `string` | Trigger id; the element that labels it; the help/error text it points at. |
|
|
788
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`. |
|
|
789
791
|
|
|
790
792
|
Raw path: `data-disabled` / `data-readonly` / `data-invalid`, `data-id`, `data-describedby`, and
|
|
791
793
|
`FreedayCascade.setState(root, { disabled, readonly, invalid })` for a host that rendered once.
|
|
@@ -962,6 +964,7 @@ a new class.
|
|
|
962
964
|
| `placeholder?` | `string` | |
|
|
963
965
|
| `id?` · `ariaLabelledby?` · `describedby?` | `string` | Trigger id; the element that labels it; the help/error text it points at. |
|
|
964
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`. |
|
|
965
968
|
| `clearable?` | `boolean` | Show a × in the trigger once a date is set, so an optional date can be unset. Sends `''`. Off by default. |
|
|
966
969
|
| `clearLabel?` | `string` | Accessible name for that ×. Default `Clear date`. |
|
|
967
970
|
| `prevMonthLabel?` · `nextMonthLabel?` | `string` | The month arrows. Defaults `Previous month` / `Next month`. |
|
|
@@ -996,6 +999,7 @@ page's `<html lang>`), `clearable` and `clearLabel` (this path builds no clear b
|
|
|
996
999
|
| `placeholder?` | `string` | Shown while nothing is picked. Default `Select month`. |
|
|
997
1000
|
| `id?` · `ariaLabelledby?` · `describedby?` | `string` | Trigger id; the element that labels it; the help/error text it points at. |
|
|
998
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`. |
|
|
999
1003
|
| `clearable?` | `boolean` | Show a × in the trigger once a month is set, so an optional month can be unset. Sends `''`. Off by default. |
|
|
1000
1004
|
| `clearLabel?` | `string` | Accessible name for that ×. Default `Clear month`. |
|
|
1001
1005
|
| `prevYearLabel?` · `nextYearLabel?` | `string` | The arrows beside the month grid, which step a year. Defaults `Previous year` / `Next year`. |
|
|
@@ -1143,6 +1147,7 @@ browser at the same width therefore disagree about a screen that is otherwise id
|
|
|
1143
1147
|
| `ariaLabel?` · `ariaLabelledby?` · `describedby?` | `string` | Names the trigger, or points at the text that does. Blazor: `Label` / `Describedby`. |
|
|
1144
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`. |
|
|
1145
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`. |
|
|
1146
1151
|
|
|
1147
1152
|
Keyboard: `ArrowDown`/`ArrowUp` move one option, `Home`/`End` jump to the ends, `Enter`/`Space`
|
|
1148
1153
|
pick, `Escape` closes. The highlight is **clamped, not cyclic** — `ArrowUp` at `00:00` stays there,
|
|
@@ -1223,6 +1228,26 @@ blur/input. Needs `freeday-form.js`.
|
|
|
1223
1228
|
- Per-control messages: `data-fdy-msg-required`, `-type`, `-minlength`, `-min`, `-mismatch`, …
|
|
1224
1229
|
- Cross-field: `data-fdy-match="#otherFieldId"`
|
|
1225
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.
|
|
1226
1251
|
- **A refused field is revealed before it is focused**, because `focus()` on a control that is not
|
|
1227
1252
|
rendered does nothing. A closed `<details>` around it (accordion, tree, nav group) is opened, an
|
|
1228
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
|
[](https://cahyo-dimas.github.io/freeday-ui-kit/)
|
|
8
|
-
[](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`.
|
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/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`.
|
|
@@ -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><button></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><button></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; }
|
|
@@ -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><button></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><button></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
|
|
@@ -20,4 +20,5 @@
|
|
|
20
20
|
data-describedby="@Describedby"
|
|
21
21
|
data-disabled="@(Disabled ? "true" : null)"
|
|
22
22
|
data-readonly="@(Readonly ? "true" : null)"
|
|
23
|
-
data-invalid="@(Invalid ? "true" : null)"
|
|
23
|
+
data-invalid="@(Invalid ? "true" : null)"
|
|
24
|
+
data-fdy-required="@(Required ? "" : null)"></div>
|
|
@@ -48,6 +48,13 @@ public partial class FdyTimepicker
|
|
|
48
48
|
/// <summary>Marks the field invalid (<c>aria-invalid</c> + the error styling).</summary>
|
|
49
49
|
[Parameter] public bool Invalid { get; set; }
|
|
50
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><button></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
|
+
|
|
51
58
|
// The enhancer owns this control's DOM; init it directly (initAll would skip Root itself).
|
|
52
59
|
protected override ValueTask HydrateAsync() => JS.InvokeVoidAsync("FreedayTimepicker.init", Root);
|
|
53
60
|
|
|
@@ -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
|
>
|
|
@@ -73,6 +73,14 @@ export interface FdyDatepickerProps {
|
|
|
73
73
|
disabled?: boolean;
|
|
74
74
|
/** Locked/view mode: stays focusable and shows its date, but can't be opened, cleared, or changed. Unlike `disabled`, it keeps tab order and isn't greyed. */
|
|
75
75
|
readonly?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Let a `data-fdy-validate` form demand this control be filled. It renders `data-fdy-required`
|
|
78
|
+
* on the root, and `freeday-form.js` refuses the submit, paints the message in the field and
|
|
79
|
+
* focuses the trigger. Needed because the trigger is a `<button>`: `willValidate` is false, so
|
|
80
|
+
* a plain `required` on it has never meant anything (#096). Override the message per control
|
|
81
|
+
* with `data-fdy-msg-required` on the root.
|
|
82
|
+
*/
|
|
83
|
+
required?: boolean;
|
|
76
84
|
invalid?: boolean;
|
|
77
85
|
describedby?: string;
|
|
78
86
|
id?: string;
|
|
@@ -527,7 +535,8 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
527
535
|
}, [open]);
|
|
528
536
|
|
|
529
537
|
return (
|
|
530
|
-
<div ref={rootRef} className={isInvalid ? 'fdy-datepicker fdy-datepicker--error' : 'fdy-datepicker'}
|
|
538
|
+
<div ref={rootRef} className={isInvalid ? 'fdy-datepicker fdy-datepicker--error' : 'fdy-datepicker'}
|
|
539
|
+
data-value={props.value ?? ''} data-fdy-required={props.required === true ? '' : undefined} onBlur={onFocusout}>
|
|
531
540
|
<button
|
|
532
541
|
id={triggerId}
|
|
533
542
|
ref={triggerRef}
|
|
@@ -50,6 +50,14 @@ export interface FdyMonthpickerProps {
|
|
|
50
50
|
disabled?: boolean;
|
|
51
51
|
/** Locked/view mode: stays focusable and shows its month, but can't be opened, cleared, or changed. Unlike `disabled`, it keeps tab order and isn't greyed. */
|
|
52
52
|
readonly?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Let a `data-fdy-validate` form demand this control be filled. It renders `data-fdy-required`
|
|
55
|
+
* on the root, and `freeday-form.js` refuses the submit, paints the message in the field and
|
|
56
|
+
* focuses the trigger. Needed because the trigger is a `<button>`: `willValidate` is false, so
|
|
57
|
+
* a plain `required` on it has never meant anything (#096). Override the message per control
|
|
58
|
+
* with `data-fdy-msg-required` on the root.
|
|
59
|
+
*/
|
|
60
|
+
required?: boolean;
|
|
53
61
|
invalid?: boolean;
|
|
54
62
|
describedby?: string;
|
|
55
63
|
id?: string;
|
|
@@ -277,7 +285,8 @@ export function FdyMonthpicker(props: FdyMonthpickerProps): JSX.Element {
|
|
|
277
285
|
}, [open]);
|
|
278
286
|
|
|
279
287
|
return (
|
|
280
|
-
<div ref={rootRef} className={isInvalid ? 'fdy-datepicker fdy-datepicker--error' : 'fdy-datepicker'}
|
|
288
|
+
<div ref={rootRef} className={isInvalid ? 'fdy-datepicker fdy-datepicker--error' : 'fdy-datepicker'}
|
|
289
|
+
data-value={props.value ?? ''} data-fdy-required={props.required === true ? '' : undefined} onBlur={onFocusout}>
|
|
281
290
|
<button
|
|
282
291
|
id={triggerId}
|
|
283
292
|
ref={triggerRef}
|
|
@@ -26,6 +26,14 @@ export interface FdyTimepickerProps {
|
|
|
26
26
|
disabled?: boolean;
|
|
27
27
|
/** Locked/view mode: stays focusable and shows its time, but can't be opened or changed. Unlike `disabled`, it keeps tab order and isn't greyed. */
|
|
28
28
|
readonly?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Let a `data-fdy-validate` form demand this control be filled. It renders `data-fdy-required`
|
|
31
|
+
* on the root, and `freeday-form.js` refuses the submit, paints the message in the field and
|
|
32
|
+
* focuses the trigger. Needed because the trigger is a `<button>`: `willValidate` is false, so
|
|
33
|
+
* a plain `required` on it has never meant anything (#096). Override the message per control
|
|
34
|
+
* with `data-fdy-msg-required` on the root.
|
|
35
|
+
*/
|
|
36
|
+
required?: boolean;
|
|
29
37
|
invalid?: boolean;
|
|
30
38
|
describedby?: string;
|
|
31
39
|
}
|
|
@@ -160,6 +168,7 @@ export function FdyTimepicker(props: FdyTimepickerProps): JSX.Element {
|
|
|
160
168
|
ref={rootRef}
|
|
161
169
|
className={isInvalid ? 'fdy-timepicker fdy-timepicker--error' : 'fdy-timepicker'}
|
|
162
170
|
data-value={value}
|
|
171
|
+
data-fdy-required={props.required === true ? '' : undefined}
|
|
163
172
|
onKeyDown={onKeyDown}
|
|
164
173
|
onBlur={onFocusout}
|
|
165
174
|
>
|
|
@@ -46,6 +46,14 @@ const props = defineProps<{
|
|
|
46
46
|
disabled?: boolean;
|
|
47
47
|
/** 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. */
|
|
48
48
|
readonly?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Let a `data-fdy-validate` form demand this control be filled. It renders `data-fdy-required`
|
|
51
|
+
* on the root, and `freeday-form.js` refuses the submit, paints the message in the field and
|
|
52
|
+
* focuses the trigger. Needed because the trigger is a `<button>`: `willValidate` is false, so
|
|
53
|
+
* a plain `required` on it has never meant anything (#096). Override the message per control
|
|
54
|
+
* with `data-fdy-msg-required`, which falls through onto the root.
|
|
55
|
+
*/
|
|
56
|
+
required?: boolean;
|
|
49
57
|
invalid?: boolean;
|
|
50
58
|
describedby?: string;
|
|
51
59
|
}>();
|
|
@@ -184,7 +192,8 @@ onBeforeUnmount((): void => {
|
|
|
184
192
|
</script>
|
|
185
193
|
|
|
186
194
|
<template>
|
|
187
|
-
<div ref="rootEl" :class="isInvalid ? 'fdy-cascade fdy-cascade--error' : 'fdy-cascade'"
|
|
195
|
+
<div ref="rootEl" :class="isInvalid ? 'fdy-cascade fdy-cascade--error' : 'fdy-cascade'"
|
|
196
|
+
:data-value="modelValue" :data-fdy-required="required === true ? '' : undefined">
|
|
188
197
|
<button
|
|
189
198
|
:id="triggerId"
|
|
190
199
|
ref="triggerEl"
|
|
@@ -24,6 +24,14 @@ const props = defineProps<{
|
|
|
24
24
|
disabled?: boolean;
|
|
25
25
|
/** 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. */
|
|
26
26
|
readonly?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Let a `data-fdy-validate` form demand this control be filled. It renders `data-fdy-required`
|
|
29
|
+
* on the root, and `freeday-form.js` refuses the submit, paints the message in the field and
|
|
30
|
+
* focuses the trigger. Needed because the trigger is a `<button>`: `willValidate` is false, so
|
|
31
|
+
* a plain `required` on it has never meant anything (#096). Override the message per control
|
|
32
|
+
* with `data-fdy-msg-required`, which falls through onto the root.
|
|
33
|
+
*/
|
|
34
|
+
required?: boolean;
|
|
27
35
|
invalid?: boolean;
|
|
28
36
|
describedby?: string;
|
|
29
37
|
}>();
|
|
@@ -194,6 +202,7 @@ onBeforeUnmount((): void => {
|
|
|
194
202
|
class="fdy-combo"
|
|
195
203
|
:class="{ 'fdy-combo--error': isInvalid }"
|
|
196
204
|
:data-value="modelValue"
|
|
205
|
+
:data-fdy-required="required === true ? '' : undefined"
|
|
197
206
|
@keydown="onKeydown"
|
|
198
207
|
@focusout="onFocusout"
|
|
199
208
|
>
|
|
@@ -75,6 +75,14 @@ const props = defineProps<{
|
|
|
75
75
|
disabled?: boolean;
|
|
76
76
|
/** Locked/view mode: stays focusable and shows its date, but can't be opened, cleared, or changed. Unlike `disabled`, it keeps tab order and isn't greyed. */
|
|
77
77
|
readonly?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Let a `data-fdy-validate` form demand this control be filled. It renders `data-fdy-required`
|
|
80
|
+
* on the root, and `freeday-form.js` refuses the submit, paints the message in the field and
|
|
81
|
+
* focuses the trigger. Needed because the trigger is a `<button>`: `willValidate` is false, so
|
|
82
|
+
* a plain `required` on it has never meant anything (#096). Override the message per control
|
|
83
|
+
* with `data-fdy-msg-required`, which falls through onto the root.
|
|
84
|
+
*/
|
|
85
|
+
required?: boolean;
|
|
78
86
|
invalid?: boolean;
|
|
79
87
|
describedby?: string;
|
|
80
88
|
id?: string;
|
|
@@ -516,7 +524,8 @@ onBeforeUnmount((): void => {
|
|
|
516
524
|
</script>
|
|
517
525
|
|
|
518
526
|
<template>
|
|
519
|
-
<div ref="rootEl" class="fdy-datepicker" :class="{ 'fdy-datepicker--error': isInvalid }"
|
|
527
|
+
<div ref="rootEl" class="fdy-datepicker" :class="{ 'fdy-datepicker--error': isInvalid }"
|
|
528
|
+
:data-value="modelValue ?? ''" :data-fdy-required="required === true ? '' : undefined" @focusout="onFocusout">
|
|
520
529
|
<button
|
|
521
530
|
:id="triggerId"
|
|
522
531
|
ref="triggerEl"
|
|
@@ -57,6 +57,14 @@ const props = defineProps<{
|
|
|
57
57
|
disabled?: boolean;
|
|
58
58
|
/** Locked/view mode: stays focusable and shows its month, but can't be opened, cleared, or changed. Unlike `disabled`, it keeps tab order and isn't greyed. */
|
|
59
59
|
readonly?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Let a `data-fdy-validate` form demand this control be filled. It renders `data-fdy-required`
|
|
62
|
+
* on the root, and `freeday-form.js` refuses the submit, paints the message in the field and
|
|
63
|
+
* focuses the trigger. Needed because the trigger is a `<button>`: `willValidate` is false, so
|
|
64
|
+
* a plain `required` on it has never meant anything (#096). Override the message per control
|
|
65
|
+
* with `data-fdy-msg-required`, which falls through onto the root.
|
|
66
|
+
*/
|
|
67
|
+
required?: boolean;
|
|
60
68
|
invalid?: boolean;
|
|
61
69
|
describedby?: string;
|
|
62
70
|
id?: string;
|
|
@@ -290,7 +298,8 @@ onBeforeUnmount((): void => {
|
|
|
290
298
|
</script>
|
|
291
299
|
|
|
292
300
|
<template>
|
|
293
|
-
<div ref="rootEl" class="fdy-datepicker" :class="{ 'fdy-datepicker--error': isInvalid }"
|
|
301
|
+
<div ref="rootEl" class="fdy-datepicker" :class="{ 'fdy-datepicker--error': isInvalid }"
|
|
302
|
+
:data-value="modelValue ?? ''" :data-fdy-required="required === true ? '' : undefined" @focusout="onFocusout">
|
|
294
303
|
<button
|
|
295
304
|
:id="triggerId"
|
|
296
305
|
ref="triggerEl"
|
|
@@ -34,6 +34,14 @@ const props = defineProps<{
|
|
|
34
34
|
disabled?: boolean;
|
|
35
35
|
/** Locked/view mode: stays focusable and shows its time, but can't be opened or changed. Unlike `disabled`, it keeps tab order and isn't greyed. */
|
|
36
36
|
readonly?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Let a `data-fdy-validate` form demand this control be filled. It renders `data-fdy-required`
|
|
39
|
+
* on the root, and `freeday-form.js` refuses the submit, paints the message in the field and
|
|
40
|
+
* focuses the trigger. Needed because the trigger is a `<button>`: `willValidate` is false, so
|
|
41
|
+
* a plain `required` on it has never meant anything (#096). Override the message per control
|
|
42
|
+
* with `data-fdy-msg-required`, which falls through onto the root.
|
|
43
|
+
*/
|
|
44
|
+
required?: boolean;
|
|
37
45
|
invalid?: boolean;
|
|
38
46
|
describedby?: string;
|
|
39
47
|
}>();
|
|
@@ -224,6 +232,7 @@ onBeforeUnmount((): void => {
|
|
|
224
232
|
class="fdy-timepicker"
|
|
225
233
|
:class="{ 'fdy-timepicker--error': isInvalid }"
|
|
226
234
|
:data-value="value"
|
|
235
|
+
:data-fdy-required="required === true ? '' : undefined"
|
|
227
236
|
@keydown="onKeydown"
|
|
228
237
|
@focusout="onFocusout"
|
|
229
238
|
>
|
package/dist/freeday-cascade.js
CHANGED
|
@@ -198,6 +198,11 @@
|
|
|
198
198
|
if (!node) return;
|
|
199
199
|
if (node.children) { drill(i); return; }
|
|
200
200
|
selectedValue = node.value;
|
|
201
|
+
/* Mirror the value onto the ROOT, which until now only seeded it. `freeday-select.js` has
|
|
202
|
+
always done this, so this makes the family agree rather than inventing a second idea, and
|
|
203
|
+
it is what lets `freeday-form.js` require a kit control without knowing which control it
|
|
204
|
+
is: one attribute instead of five event names (#096). */
|
|
205
|
+
wrap.setAttribute('data-value', node.value);
|
|
201
206
|
var labels = stack.map(function (n) { return n.label; }).concat([node.label]);
|
|
202
207
|
valueSpan.textContent = labels.join(sep);
|
|
203
208
|
valueSpan.classList.remove('fdy-cascade__value--placeholder');
|
|
@@ -175,6 +175,11 @@
|
|
|
175
175
|
wrap.appendChild(panel);
|
|
176
176
|
|
|
177
177
|
function updateDisplay() {
|
|
178
|
+
/* Mirror the value onto the ROOT, which until now only seeded it. `freeday-select.js` has
|
|
179
|
+
always done this, so this makes the family agree rather than inventing a second idea, and
|
|
180
|
+
it is what lets `freeday-form.js` require a kit control without knowing which control it
|
|
181
|
+
is: one attribute instead of five event names (#096). */
|
|
182
|
+
wrap.setAttribute('data-value', selected ? (monthOnly ? toISOMonth(selected) : toISO(selected)) : '');
|
|
178
183
|
if (selected) {
|
|
179
184
|
/* Through the same `Intl` formatter the month CELLS and the day grid's title already use,
|
|
180
185
|
which is the whole reason this mode exists: <input type="month"> names the month in the
|
package/dist/freeday-form.js
CHANGED
|
@@ -74,6 +74,36 @@
|
|
|
74
74
|
|
|
75
75
|
function fieldOf(el) { return el.closest('.fdy-field') || el.parentElement; }
|
|
76
76
|
|
|
77
|
+
/* A kit control (combo, datepicker, monthpicker, timepicker, cascade) is NOT a form control.
|
|
78
|
+
* Its trigger is a <button>, so `willValidate` is false and `form.elements` never offers it --
|
|
79
|
+
* `required` on one meant nothing, and a form could not demand that it be filled (#096).
|
|
80
|
+
*
|
|
81
|
+
* Nothing here names a component. Two attributes carry it:
|
|
82
|
+
* data-fdy-required the app asking for it, on the control's ROOT
|
|
83
|
+
* data-value the live value, which every value-bearing kit control now mirrors
|
|
84
|
+
* and the element that takes aria-invalid and focus is found by `aria-haspopup`, which every
|
|
85
|
+
* popup-style kit trigger already sets. `FdyAutocomplete` and `FdyCfl` are absent on purpose:
|
|
86
|
+
* they render a real <input>, so the NATIVE `required` is what fits them, and routing them
|
|
87
|
+
* through here would give one field two sources of truth. No prop exposes that yet, which is a
|
|
88
|
+
* separate gap and is written down in NEXT-UP.md rather than half-closed here. */
|
|
89
|
+
function kitRoots(form) {
|
|
90
|
+
return Array.prototype.slice.call(form.querySelectorAll('[data-fdy-required]'));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* The trigger, or the root itself if a control ever ships without one, so a missing trigger
|
|
94
|
+
degrades to a visible error rather than a thrown TypeError. */
|
|
95
|
+
function kitAria(root) {
|
|
96
|
+
return root.querySelector('[aria-haspopup]') || root;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function kitMessage(root) {
|
|
100
|
+
if ((root.getAttribute('data-value') || '') !== '') return '';
|
|
101
|
+
var form = root.closest('form');
|
|
102
|
+
return root.getAttribute('data-fdy-msg-required')
|
|
103
|
+
|| root.getAttribute('data-fdy-msg')
|
|
104
|
+
|| textOf(form, 'required');
|
|
105
|
+
}
|
|
106
|
+
|
|
77
107
|
function ensureErrorEl(el) {
|
|
78
108
|
var field = fieldOf(el);
|
|
79
109
|
var errEl = field ? field.querySelector('[data-fdy-error]') : null;
|
|
@@ -164,6 +194,21 @@
|
|
|
164
194
|
function controls() {
|
|
165
195
|
return Array.prototype.filter.call(form.elements, isCandidate);
|
|
166
196
|
}
|
|
197
|
+
function validateKit(root) {
|
|
198
|
+
var msg = kitMessage(root);
|
|
199
|
+
paint(kitAria(root), msg);
|
|
200
|
+
return !msg;
|
|
201
|
+
}
|
|
202
|
+
/* The value lives in an attribute, and each control changes it through its own event name
|
|
203
|
+
(fdy-change, fdy-datepicker-change, fdy-time-select, fdy-cascade-change). Observing the
|
|
204
|
+
attribute is what keeps this enhancer from having to know any of them. */
|
|
205
|
+
function kitLiveBind(root) {
|
|
206
|
+
if (root.dataset.fdyKitBound === '1') return;
|
|
207
|
+
root.dataset.fdyKitBound = '1';
|
|
208
|
+
new MutationObserver(function () {
|
|
209
|
+
if (submitted || eager) validateKit(root);
|
|
210
|
+
}).observe(root, { attributes: true, attributeFilter: ['data-value'] });
|
|
211
|
+
}
|
|
167
212
|
function validateOne(el) {
|
|
168
213
|
var msg = messageFor(el);
|
|
169
214
|
paint(el, msg);
|
|
@@ -178,6 +223,7 @@
|
|
|
178
223
|
}
|
|
179
224
|
|
|
180
225
|
Array.prototype.forEach.call(controls(), liveBind);
|
|
226
|
+
kitRoots(form).forEach(kitLiveBind);
|
|
181
227
|
|
|
182
228
|
form.addEventListener('submit', function (e) {
|
|
183
229
|
submitted = true;
|
|
@@ -186,6 +232,18 @@
|
|
|
186
232
|
liveBind(el);
|
|
187
233
|
if (!validateOne(el)) invalid.push(el);
|
|
188
234
|
});
|
|
235
|
+
kitRoots(form).forEach(function (root) {
|
|
236
|
+
kitLiveBind(root);
|
|
237
|
+
if (!validateKit(root)) invalid.push(kitAria(root));
|
|
238
|
+
});
|
|
239
|
+
/* DOM order, not native-then-kit: `invalid[0]` is what gets revealed and focused, and a
|
|
240
|
+
reader sent to the second field while the first is also red learns the wrong thing. */
|
|
241
|
+
invalid.sort(function (a, b) {
|
|
242
|
+
var rel = a.compareDocumentPosition(b);
|
|
243
|
+
if (rel & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
|
|
244
|
+
if (rel & Node.DOCUMENT_POSITION_PRECEDING) return 1;
|
|
245
|
+
return 0;
|
|
246
|
+
});
|
|
189
247
|
if (invalid.length) {
|
|
190
248
|
e.preventDefault();
|
|
191
249
|
reveal(invalid[0]);
|
|
@@ -202,11 +260,13 @@
|
|
|
202
260
|
submitted = true;
|
|
203
261
|
var invalid = [];
|
|
204
262
|
Array.prototype.forEach.call(controls(), function (el) { if (!validateOne(el)) invalid.push(el); });
|
|
263
|
+
kitRoots(form).forEach(function (root) { if (!validateKit(root)) invalid.push(kitAria(root)); });
|
|
205
264
|
return invalid;
|
|
206
265
|
},
|
|
207
266
|
reset: function () {
|
|
208
267
|
submitted = false;
|
|
209
268
|
Array.prototype.forEach.call(controls(), function (el) { paint(el, ''); });
|
|
269
|
+
kitRoots(form).forEach(function (root) { paint(kitAria(root), ''); });
|
|
210
270
|
}
|
|
211
271
|
};
|
|
212
272
|
form._fdyForm = api;
|
|
@@ -130,6 +130,11 @@
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
function updateDisplay() {
|
|
133
|
+
/* Mirror the value onto the ROOT, which until now only seeded it. `freeday-select.js` has
|
|
134
|
+
always done this, so this makes the family agree rather than inventing a second idea, and
|
|
135
|
+
it is what lets `freeday-form.js` require a kit control without knowing which control it
|
|
136
|
+
is: one attribute instead of five event names (#096). */
|
|
137
|
+
wrap.setAttribute('data-value', value);
|
|
133
138
|
if (value) {
|
|
134
139
|
valueSpan.textContent = value;
|
|
135
140
|
valueSpan.classList.remove('fdy-timepicker__value--placeholder');
|
package/dist/freeday.js
CHANGED
|
@@ -944,6 +944,11 @@
|
|
|
944
944
|
if (!node) return;
|
|
945
945
|
if (node.children) { drill(i); return; }
|
|
946
946
|
selectedValue = node.value;
|
|
947
|
+
/* Mirror the value onto the ROOT, which until now only seeded it. `freeday-select.js` has
|
|
948
|
+
always done this, so this makes the family agree rather than inventing a second idea, and
|
|
949
|
+
it is what lets `freeday-form.js` require a kit control without knowing which control it
|
|
950
|
+
is: one attribute instead of five event names (#096). */
|
|
951
|
+
wrap.setAttribute('data-value', node.value);
|
|
947
952
|
var labels = stack.map(function (n) { return n.label; }).concat([node.label]);
|
|
948
953
|
valueSpan.textContent = labels.join(sep);
|
|
949
954
|
valueSpan.classList.remove('fdy-cascade__value--placeholder');
|
|
@@ -2262,6 +2267,11 @@
|
|
|
2262
2267
|
wrap.appendChild(panel);
|
|
2263
2268
|
|
|
2264
2269
|
function updateDisplay() {
|
|
2270
|
+
/* Mirror the value onto the ROOT, which until now only seeded it. `freeday-select.js` has
|
|
2271
|
+
always done this, so this makes the family agree rather than inventing a second idea, and
|
|
2272
|
+
it is what lets `freeday-form.js` require a kit control without knowing which control it
|
|
2273
|
+
is: one attribute instead of five event names (#096). */
|
|
2274
|
+
wrap.setAttribute('data-value', selected ? (monthOnly ? toISOMonth(selected) : toISO(selected)) : '');
|
|
2265
2275
|
if (selected) {
|
|
2266
2276
|
/* Through the same `Intl` formatter the month CELLS and the day grid's title already use,
|
|
2267
2277
|
which is the whole reason this mode exists: <input type="month"> names the month in the
|
|
@@ -3011,6 +3021,36 @@
|
|
|
3011
3021
|
|
|
3012
3022
|
function fieldOf(el) { return el.closest('.fdy-field') || el.parentElement; }
|
|
3013
3023
|
|
|
3024
|
+
/* A kit control (combo, datepicker, monthpicker, timepicker, cascade) is NOT a form control.
|
|
3025
|
+
* Its trigger is a <button>, so `willValidate` is false and `form.elements` never offers it --
|
|
3026
|
+
* `required` on one meant nothing, and a form could not demand that it be filled (#096).
|
|
3027
|
+
*
|
|
3028
|
+
* Nothing here names a component. Two attributes carry it:
|
|
3029
|
+
* data-fdy-required the app asking for it, on the control's ROOT
|
|
3030
|
+
* data-value the live value, which every value-bearing kit control now mirrors
|
|
3031
|
+
* and the element that takes aria-invalid and focus is found by `aria-haspopup`, which every
|
|
3032
|
+
* popup-style kit trigger already sets. `FdyAutocomplete` and `FdyCfl` are absent on purpose:
|
|
3033
|
+
* they render a real <input>, so the NATIVE `required` is what fits them, and routing them
|
|
3034
|
+
* through here would give one field two sources of truth. No prop exposes that yet, which is a
|
|
3035
|
+
* separate gap and is written down in NEXT-UP.md rather than half-closed here. */
|
|
3036
|
+
function kitRoots(form) {
|
|
3037
|
+
return Array.prototype.slice.call(form.querySelectorAll('[data-fdy-required]'));
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
/* The trigger, or the root itself if a control ever ships without one, so a missing trigger
|
|
3041
|
+
degrades to a visible error rather than a thrown TypeError. */
|
|
3042
|
+
function kitAria(root) {
|
|
3043
|
+
return root.querySelector('[aria-haspopup]') || root;
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
function kitMessage(root) {
|
|
3047
|
+
if ((root.getAttribute('data-value') || '') !== '') return '';
|
|
3048
|
+
var form = root.closest('form');
|
|
3049
|
+
return root.getAttribute('data-fdy-msg-required')
|
|
3050
|
+
|| root.getAttribute('data-fdy-msg')
|
|
3051
|
+
|| textOf(form, 'required');
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3014
3054
|
function ensureErrorEl(el) {
|
|
3015
3055
|
var field = fieldOf(el);
|
|
3016
3056
|
var errEl = field ? field.querySelector('[data-fdy-error]') : null;
|
|
@@ -3101,6 +3141,21 @@
|
|
|
3101
3141
|
function controls() {
|
|
3102
3142
|
return Array.prototype.filter.call(form.elements, isCandidate);
|
|
3103
3143
|
}
|
|
3144
|
+
function validateKit(root) {
|
|
3145
|
+
var msg = kitMessage(root);
|
|
3146
|
+
paint(kitAria(root), msg);
|
|
3147
|
+
return !msg;
|
|
3148
|
+
}
|
|
3149
|
+
/* The value lives in an attribute, and each control changes it through its own event name
|
|
3150
|
+
(fdy-change, fdy-datepicker-change, fdy-time-select, fdy-cascade-change). Observing the
|
|
3151
|
+
attribute is what keeps this enhancer from having to know any of them. */
|
|
3152
|
+
function kitLiveBind(root) {
|
|
3153
|
+
if (root.dataset.fdyKitBound === '1') return;
|
|
3154
|
+
root.dataset.fdyKitBound = '1';
|
|
3155
|
+
new MutationObserver(function () {
|
|
3156
|
+
if (submitted || eager) validateKit(root);
|
|
3157
|
+
}).observe(root, { attributes: true, attributeFilter: ['data-value'] });
|
|
3158
|
+
}
|
|
3104
3159
|
function validateOne(el) {
|
|
3105
3160
|
var msg = messageFor(el);
|
|
3106
3161
|
paint(el, msg);
|
|
@@ -3115,6 +3170,7 @@
|
|
|
3115
3170
|
}
|
|
3116
3171
|
|
|
3117
3172
|
Array.prototype.forEach.call(controls(), liveBind);
|
|
3173
|
+
kitRoots(form).forEach(kitLiveBind);
|
|
3118
3174
|
|
|
3119
3175
|
form.addEventListener('submit', function (e) {
|
|
3120
3176
|
submitted = true;
|
|
@@ -3123,6 +3179,18 @@
|
|
|
3123
3179
|
liveBind(el);
|
|
3124
3180
|
if (!validateOne(el)) invalid.push(el);
|
|
3125
3181
|
});
|
|
3182
|
+
kitRoots(form).forEach(function (root) {
|
|
3183
|
+
kitLiveBind(root);
|
|
3184
|
+
if (!validateKit(root)) invalid.push(kitAria(root));
|
|
3185
|
+
});
|
|
3186
|
+
/* DOM order, not native-then-kit: `invalid[0]` is what gets revealed and focused, and a
|
|
3187
|
+
reader sent to the second field while the first is also red learns the wrong thing. */
|
|
3188
|
+
invalid.sort(function (a, b) {
|
|
3189
|
+
var rel = a.compareDocumentPosition(b);
|
|
3190
|
+
if (rel & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
|
|
3191
|
+
if (rel & Node.DOCUMENT_POSITION_PRECEDING) return 1;
|
|
3192
|
+
return 0;
|
|
3193
|
+
});
|
|
3126
3194
|
if (invalid.length) {
|
|
3127
3195
|
e.preventDefault();
|
|
3128
3196
|
reveal(invalid[0]);
|
|
@@ -3139,11 +3207,13 @@
|
|
|
3139
3207
|
submitted = true;
|
|
3140
3208
|
var invalid = [];
|
|
3141
3209
|
Array.prototype.forEach.call(controls(), function (el) { if (!validateOne(el)) invalid.push(el); });
|
|
3210
|
+
kitRoots(form).forEach(function (root) { if (!validateKit(root)) invalid.push(kitAria(root)); });
|
|
3142
3211
|
return invalid;
|
|
3143
3212
|
},
|
|
3144
3213
|
reset: function () {
|
|
3145
3214
|
submitted = false;
|
|
3146
3215
|
Array.prototype.forEach.call(controls(), function (el) { paint(el, ''); });
|
|
3216
|
+
kitRoots(form).forEach(function (root) { paint(kitAria(root), ''); });
|
|
3147
3217
|
}
|
|
3148
3218
|
};
|
|
3149
3219
|
form._fdyForm = api;
|
|
@@ -5016,6 +5086,11 @@
|
|
|
5016
5086
|
}
|
|
5017
5087
|
|
|
5018
5088
|
function updateDisplay() {
|
|
5089
|
+
/* Mirror the value onto the ROOT, which until now only seeded it. `freeday-select.js` has
|
|
5090
|
+
always done this, so this makes the family agree rather than inventing a second idea, and
|
|
5091
|
+
it is what lets `freeday-form.js` require a kit control without knowing which control it
|
|
5092
|
+
is: one attribute instead of five event names (#096). */
|
|
5093
|
+
wrap.setAttribute('data-value', value);
|
|
5019
5094
|
if (value) {
|
|
5020
5095
|
valueSpan.textContent = value;
|
|
5021
5096
|
valueSpan.classList.remove('fdy-timepicker__value--placeholder');
|
package/docs/getting-started.md
CHANGED
|
@@ -198,7 +198,7 @@ live docs also have a copy button per component.
|
|
|
198
198
|
```bash
|
|
199
199
|
npm i @cahyo-dimas/freeday
|
|
200
200
|
```
|
|
201
|
-
Lands in `package.json` as `"@cahyo-dimas/freeday": "^3.
|
|
201
|
+
Lands in `package.json` as `"@cahyo-dimas/freeday": "^3.12.0"` (public npm package). `dist/` is
|
|
202
202
|
committed and published → no build step; `npm ci` runs without auth.
|
|
203
203
|
|
|
204
204
|
### 2. Import the CSS + enhancers **once** in your entry (`src/main.ts`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cahyo-dimas/freeday",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.12.0",
|
|
4
4
|
"description": "Freeday: token-driven, framework-agnostic UI KIT (design source-of-truth).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"scripts": {
|
|
97
97
|
"build": "node tokens/build.mjs",
|
|
98
98
|
"test": "node --test",
|
|
99
|
-
"test:browser": "node --test --test-concurrency=3 browser/color-mix.mjs browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs browser/root-init.mjs browser/upload-states.mjs browser/number.mjs browser/card-stretch.mjs browser/text-override.mjs browser/control-heights.mjs browser/field-width.mjs browser/cfl-multi.mjs browser/cfl-dialog-only.mjs browser/crowding.mjs browser/over-dialog.mjs browser/chart-scale.mjs browser/overlay-stack.mjs browser/chart-a11y.mjs browser/app-shell.mjs browser/bottom-nav.mjs browser/picker-states.mjs browser/tabs-strip.mjs browser/tabs-vertical.mjs browser/menu-align.mjs browser/cfl-narrow.mjs browser/form-reveal.mjs browser/confirm-search-copy.mjs browser/daterange-settled.mjs browser/display-parts.mjs browser/table-empty.mjs browser/table-footer-text.mjs browser/month-mode.mjs browser/monthpicker.mjs browser/page-columns.mjs browser/table-hover.mjs browser/typed-popover.mjs browser/filter-parity.mjs browser/filter-wrap.mjs browser/filter-stack.mjs browser/timepicker.mjs browser/harness-contract.mjs",
|
|
99
|
+
"test:browser": "node --test --test-concurrency=3 browser/color-mix.mjs browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs browser/root-init.mjs browser/upload-states.mjs browser/number.mjs browser/card-stretch.mjs browser/text-override.mjs browser/control-heights.mjs browser/field-width.mjs browser/cfl-multi.mjs browser/cfl-dialog-only.mjs browser/crowding.mjs browser/over-dialog.mjs browser/chart-scale.mjs browser/overlay-stack.mjs browser/chart-a11y.mjs browser/app-shell.mjs browser/bottom-nav.mjs browser/picker-states.mjs browser/tabs-strip.mjs browser/tabs-vertical.mjs browser/menu-align.mjs browser/cfl-narrow.mjs browser/form-reveal.mjs browser/confirm-search-copy.mjs browser/daterange-settled.mjs browser/display-parts.mjs browser/table-empty.mjs browser/table-footer-text.mjs browser/month-mode.mjs browser/monthpicker.mjs browser/page-columns.mjs browser/table-hover.mjs browser/typed-popover.mjs browser/filter-parity.mjs browser/filter-wrap.mjs browser/filter-stack.mjs browser/timepicker.mjs browser/form-kit-required.mjs browser/harness-contract.mjs",
|
|
100
100
|
"prepack": "node tokens/build.mjs",
|
|
101
101
|
"version": "node tokens/build.mjs && git add dist",
|
|
102
102
|
"typecheck:react": "tsc -p adapters/react/tsconfig.json --noEmit",
|