@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
@@ -29,6 +29,19 @@ export interface FdyDateRangeProps {
29
29
  endPlaceholder?: string;
30
30
  ariaLabel?: string;
31
31
  ariaLabelledby?: string;
32
+ /* Forwarded verbatim to BOTH pickers. Without these the range was the one picker in the kit
33
+ whose calendar nav could not be named in the page's own language: FdyDatepicker takes them,
34
+ FdyDateRange dropped them, so an Indonesian app got "Previous month" on the only date control
35
+ it could not reach. Flat `xxxLabel` props, not a `labels` object, because that is the shape
36
+ every other component in the kit already uses. Raised from keamanan-cluster (#095). */
37
+ prevMonthLabel?: string;
38
+ nextMonthLabel?: string;
39
+ prevYearLabel?: string;
40
+ nextYearLabel?: string;
41
+ prevYearsLabel?: string;
42
+ nextYearsLabel?: string;
43
+ chooseMonthLabel?: string;
44
+ chooseYearLabel?: string;
32
45
  }
33
46
 
34
47
  export function FdyDateRange(props: FdyDateRangeProps): JSX.Element {
@@ -57,6 +70,14 @@ export function FdyDateRange(props: FdyDateRangeProps): JSX.Element {
57
70
  invalid={props.invalid}
58
71
  describedby={props.describedby}
59
72
  placeholder={props.startPlaceholder ?? 'Dari'}
73
+ prevMonthLabel={props.prevMonthLabel}
74
+ nextMonthLabel={props.nextMonthLabel}
75
+ prevYearLabel={props.prevYearLabel}
76
+ nextYearLabel={props.nextYearLabel}
77
+ prevYearsLabel={props.prevYearsLabel}
78
+ nextYearsLabel={props.nextYearsLabel}
79
+ chooseMonthLabel={props.chooseMonthLabel}
80
+ chooseYearLabel={props.chooseYearLabel}
60
81
  />
61
82
  <span className="fdy-daterange__sep" aria-hidden="true">–</span>
62
83
  <FdyDatepicker
@@ -70,6 +91,14 @@ export function FdyDateRange(props: FdyDateRangeProps): JSX.Element {
70
91
  invalid={props.invalid}
71
92
  describedby={props.describedby}
72
93
  placeholder={props.endPlaceholder ?? 'Sampai'}
94
+ prevMonthLabel={props.prevMonthLabel}
95
+ nextMonthLabel={props.nextMonthLabel}
96
+ prevYearLabel={props.prevYearLabel}
97
+ nextYearLabel={props.nextYearLabel}
98
+ prevYearsLabel={props.prevYearsLabel}
99
+ nextYearsLabel={props.nextYearsLabel}
100
+ chooseMonthLabel={props.chooseMonthLabel}
101
+ chooseYearLabel={props.chooseYearLabel}
73
102
  />
74
103
  </div>
75
104
  );
@@ -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'} onBlur={onFocusout}>
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'} onBlur={onFocusout}>
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}
@@ -0,0 +1,221 @@
1
+ import type { JSX } from 'react';
2
+ import { useEffect, useId, useMemo, useRef, useState } from 'react';
3
+ import { usePopover } from '../usePopover';
4
+
5
+ // React twin of adapters/vue/components/FdyTimepicker.vue over freeday's `.fdy-timepicker`
6
+ // classes (src/components/timepicker.css). See that file's header for why a form control is
7
+ // re-implemented natively instead of wrapping freeday-timepicker.js, and why this replaces
8
+ // `<input type="time">`: the native picker is drawn by the OS, so iOS shows a scroll wheel,
9
+ // Android a clock dial and desktop a stepper, each with its own width and format, and no CSS
10
+ // reaches any of them (#094).
11
+
12
+ export interface FdyTimepickerProps {
13
+ /** `HH:MM`, 24-hour. `''`/`null` for empty. A loose `H:MM` is normalised for matching. */
14
+ value: string | null;
15
+ onChange: (value: string) => void;
16
+ /** Minutes between options. Default 30. */
17
+ step?: number;
18
+ /** `HH:MM` bounds, inclusive. Default `00:00` to `23:59`. */
19
+ min?: string;
20
+ max?: string;
21
+ id?: string;
22
+ ariaLabel?: string;
23
+ ariaLabelledby?: string;
24
+ /** Shown when there is no value. Default `--:--`, the same as the raw path. */
25
+ placeholder?: string;
26
+ disabled?: boolean;
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
+ 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;
37
+ invalid?: boolean;
38
+ describedby?: string;
39
+ }
40
+
41
+ const SHAPE: RegExp = /^\d{1,2}:\d{2}$/;
42
+
43
+ function toMinutes(time: string): number {
44
+ const [h, m] = time.split(':');
45
+ return Number(h) * 60 + Number(m);
46
+ }
47
+ function pad(n: number): string {
48
+ return n < 10 ? `0${n}` : String(n);
49
+ }
50
+ function fromMinutes(total: number): string {
51
+ return `${pad(Math.floor(total / 60))}:${pad(total % 60)}`;
52
+ }
53
+ /** `7:0` is not a time; `7:00` is the same time as `07:00`. Anything else answers `''`. */
54
+ function normalise(time: string | null): string {
55
+ if (time === null || !SHAPE.test(time)) return '';
56
+ const total: number = toMinutes(time);
57
+ return total >= 0 && total <= 23 * 60 + 59 ? fromMinutes(total) : '';
58
+ }
59
+ function bound(time: string | undefined, fallback: number): number {
60
+ return time !== undefined && SHAPE.test(time) ? toMinutes(time) : fallback;
61
+ }
62
+
63
+ export function FdyTimepicker(props: FdyTimepickerProps): JSX.Element {
64
+ const baseId: string = useId();
65
+ const triggerId: string = props.id ?? `${baseId}-trigger`;
66
+ const panelId: string = `${baseId}-panel`;
67
+ const optionId = (index: number): string => `${baseId}-opt-${index}`;
68
+
69
+ const rootRef = useRef<HTMLDivElement>(null);
70
+ const triggerRef = useRef<HTMLButtonElement>(null);
71
+ const panelRef = useRef<HTMLUListElement>(null);
72
+ const [open, setOpen] = useState<boolean>(false);
73
+ const [highlighted, setHighlighted] = useState<number>(-1);
74
+
75
+ // Lift the panel into the top layer so it escapes an ancestor overflow clip. A shift form is a
76
+ // modal, and a time list long enough to scroll is exactly the panel a card would cut off.
77
+ usePopover(panelRef, triggerRef, open);
78
+
79
+ // Popover attr for React 18 (JSX may not type it): set once on mount.
80
+ useEffect((): void => {
81
+ panelRef.current?.setAttribute('popover', 'manual');
82
+ }, []);
83
+
84
+ const isDisabled: boolean = props.disabled === true;
85
+ const isReadonly: boolean = props.readonly === true;
86
+ const isInvalid: boolean = props.invalid === true;
87
+
88
+ const times: ReadonlyArray<string> = useMemo((): ReadonlyArray<string> => {
89
+ const stride: number = Math.max(1, Math.trunc(props.step ?? 30));
90
+ const from: number = bound(props.min, 0);
91
+ const to: number = bound(props.max, 23 * 60 + 59);
92
+ const out: string[] = [];
93
+ for (let m = from; m <= to; m += stride) out.push(fromMinutes(m));
94
+ return out;
95
+ }, [props.step, props.min, props.max]);
96
+
97
+ const value: string = normalise(props.value);
98
+ const selectedIndex: number = times.indexOf(value);
99
+ const shown: string = value !== '' ? value : (props.placeholder ?? '--:--');
100
+ const isPlaceholder: boolean = value === '';
101
+ const activeDescendant: string | undefined = open && highlighted >= 0 ? optionId(highlighted) : undefined;
102
+
103
+ /* The panel is `max-height:15rem` and scrolls: a highlight the reader cannot see is no highlight,
104
+ and with 48 half-hour options most of the list is out of view. */
105
+ useEffect((): void => {
106
+ const list: HTMLUListElement | null = panelRef.current;
107
+ if (list === null || highlighted < 0) return;
108
+ const opt: Element | undefined = list.children[highlighted];
109
+ if (opt instanceof HTMLElement) opt.scrollIntoView({ block: 'nearest' });
110
+ }, [highlighted, open]);
111
+
112
+ /* Clamped, not cyclic: FdyCombo wraps because its lists are short. Here ArrowUp at 00:00 landing
113
+ on 23:30 reads as a glitch. Matches the raw path. */
114
+ const setHighlight = (index: number): void => {
115
+ const last: number = times.length - 1;
116
+ setHighlighted(last < 0 ? -1 : Math.min(last, Math.max(0, index)));
117
+ };
118
+ const openList = (): void => {
119
+ if (isDisabled || isReadonly || open) return;
120
+ setOpen(true);
121
+ setHighlight(selectedIndex >= 0 ? selectedIndex : 0);
122
+ };
123
+ const closeList = (focusTrigger: boolean): void => {
124
+ setOpen(false);
125
+ setHighlighted(-1);
126
+ if (focusTrigger) triggerRef.current?.focus();
127
+ };
128
+ const toggle = (): void => { if (open) closeList(false); else openList(); };
129
+ const choose = (index: number): void => {
130
+ const next: string | undefined = times[index];
131
+ if (next === undefined) return;
132
+ if (next !== value) props.onChange(next);
133
+ closeList(true);
134
+ };
135
+
136
+ const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>): void => {
137
+ if (isDisabled || isReadonly) return;
138
+ switch (e.key) {
139
+ case 'ArrowDown': e.preventDefault(); if (open) setHighlight(highlighted + 1); else openList(); break;
140
+ case 'ArrowUp': e.preventDefault(); if (open) setHighlight(highlighted - 1); else openList(); break;
141
+ case 'Home': if (open) { e.preventDefault(); setHighlight(0); } break;
142
+ case 'End': if (open) { e.preventDefault(); setHighlight(times.length - 1); } break;
143
+ case 'Enter':
144
+ case ' ': e.preventDefault(); if (open && highlighted >= 0) choose(highlighted); else openList(); break;
145
+ case 'Escape': if (open) { e.preventDefault(); closeList(true); } break;
146
+ case 'Tab': if (open) closeList(false); break;
147
+ default: break;
148
+ }
149
+ };
150
+
151
+ const onFocusout = (e: React.FocusEvent<HTMLDivElement>): void => {
152
+ const next: EventTarget | null = e.relatedTarget;
153
+ if (rootRef.current !== null && !(next instanceof Node && rootRef.current.contains(next))) closeList(false);
154
+ };
155
+
156
+ useEffect((): void | (() => void) => {
157
+ if (!open) return;
158
+ const onDocPointerDown = (e: MouseEvent): void => {
159
+ const t: EventTarget | null = e.target;
160
+ if (rootRef.current !== null && t instanceof Node && !rootRef.current.contains(t)) closeList(false);
161
+ };
162
+ document.addEventListener('mousedown', onDocPointerDown);
163
+ return (): void => document.removeEventListener('mousedown', onDocPointerDown);
164
+ }, [open]);
165
+
166
+ return (
167
+ <div
168
+ ref={rootRef}
169
+ className={isInvalid ? 'fdy-timepicker fdy-timepicker--error' : 'fdy-timepicker'}
170
+ data-value={value}
171
+ data-fdy-required={props.required === true ? '' : undefined}
172
+ onKeyDown={onKeyDown}
173
+ onBlur={onFocusout}
174
+ >
175
+ <button
176
+ id={triggerId}
177
+ ref={triggerRef}
178
+ type="button"
179
+ className={open ? 'fdy-timepicker__trigger is-open' : 'fdy-timepicker__trigger'}
180
+ aria-haspopup="listbox"
181
+ aria-controls={panelId}
182
+ aria-expanded={open}
183
+ aria-activedescendant={activeDescendant}
184
+ aria-label={props.ariaLabel}
185
+ aria-labelledby={props.ariaLabelledby}
186
+ aria-invalid={isInvalid ? 'true' : undefined}
187
+ aria-readonly={isReadonly ? 'true' : undefined}
188
+ aria-describedby={props.describedby}
189
+ disabled={isDisabled}
190
+ onClick={toggle}
191
+ >
192
+ <span className={isPlaceholder ? 'fdy-timepicker__value fdy-timepicker__value--placeholder' : 'fdy-timepicker__value'}>{shown}</span>
193
+ <span className="fdy-timepicker__icon">
194
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"
195
+ strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
196
+ <circle cx="12" cy="12" r="9" />
197
+ <path d="M12 7v5l3 2" />
198
+ </svg>
199
+ </span>
200
+ </button>
201
+ <ul id={panelId} ref={panelRef} className="fdy-timepicker__panel" role="listbox"
202
+ aria-label={props.ariaLabel} hidden={!open}>
203
+ {times.map((t: string, i: number): JSX.Element => (
204
+ // onMouseDown preventDefault keeps focus on the trigger: an <li> isn't focusable, so a
205
+ // plain mousedown moves focus out of the control, fires onBlur and closes the panel
206
+ // before the click lands. Same pattern as FdyCombo.
207
+ <li
208
+ id={optionId(i)}
209
+ key={t}
210
+ className={i === highlighted ? 'fdy-timepicker__opt is-active' : 'fdy-timepicker__opt'}
211
+ role="option"
212
+ aria-selected={t === value}
213
+ onMouseDown={(e: React.MouseEvent): void => e.preventDefault()}
214
+ onClick={(): void => choose(i)}
215
+ onMouseMove={(): void => setHighlight(i)}
216
+ >{t}</li>
217
+ ))}
218
+ </ul>
219
+ </div>
220
+ );
221
+ }
@@ -35,6 +35,7 @@ export { FdyDatepicker, type FdyDatepickerProps, type FdyDatepickerMode } from '
35
35
  export { FdyMonthpicker, type FdyMonthpickerProps } from './components/FdyMonthpicker';
36
36
  export type { FdyMonth } from '../core/month-grid';
37
37
  export { FdyDateRange, type FdyDateRangeProps, type DateRangeValue } from './components/FdyDateRange';
38
+ export { FdyTimepicker, type FdyTimepickerProps } from './components/FdyTimepicker';
38
39
  export { FdyAutocomplete, type FdyAutocompleteProps } from './components/FdyAutocomplete';
39
40
  export { FdyCascade, type FdyCascadeProps, type CascadeNode } from './components/FdyCascade';
40
41
  export { FdyCfl, type FdyCflProps, type FdyCflHandle, type CflColumn, type CflPage } from './components/FdyCfl';
@@ -4,6 +4,7 @@ export { FdyCombo } from './components/FdyCombo.tsx';
4
4
  export { FdyDatepicker } from './components/FdyDatepicker.tsx';
5
5
  export { FdyMonthpicker } from './components/FdyMonthpicker.tsx';
6
6
  export { FdyDateRange } from './components/FdyDateRange.tsx';
7
+ export { FdyTimepicker } from './components/FdyTimepicker.tsx';
7
8
  export { FdyAutocomplete } from './components/FdyAutocomplete.tsx';
8
9
  export { FdyCascade } from './components/FdyCascade.tsx';
9
10
  export { FdyCfl } from './components/FdyCfl.tsx';
@@ -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
  >
@@ -29,6 +29,19 @@ const props = defineProps<{
29
29
  endPlaceholder?: string;
30
30
  ariaLabel?: string;
31
31
  ariaLabelledby?: string;
32
+ /* Forwarded verbatim to BOTH pickers. Without these the range was the one picker in the kit
33
+ whose calendar nav could not be named in the page's own language: FdyDatepicker takes them,
34
+ FdyDateRange dropped them, so an Indonesian app got "Previous month" on the only date control
35
+ it could not reach. Flat `xxxLabel` props, not a `labels` object, because that is the shape
36
+ every other component in the kit already uses. Raised from keamanan-cluster (#095). */
37
+ prevMonthLabel?: string;
38
+ nextMonthLabel?: string;
39
+ prevYearLabel?: string;
40
+ nextYearLabel?: string;
41
+ prevYearsLabel?: string;
42
+ nextYearsLabel?: string;
43
+ chooseMonthLabel?: string;
44
+ chooseYearLabel?: string;
32
45
  }>();
33
46
 
34
47
  const emit = defineEmits<{
@@ -69,6 +82,14 @@ function setEnd(end: string): void {
69
82
  :invalid="invalid"
70
83
  :describedby="describedby"
71
84
  :placeholder="startPlaceholder ?? 'Dari'"
85
+ :prev-month-label="prevMonthLabel"
86
+ :next-month-label="nextMonthLabel"
87
+ :prev-year-label="prevYearLabel"
88
+ :next-year-label="nextYearLabel"
89
+ :prev-years-label="prevYearsLabel"
90
+ :next-years-label="nextYearsLabel"
91
+ :choose-month-label="chooseMonthLabel"
92
+ :choose-year-label="chooseYearLabel"
72
93
  @update:model-value="setStart"
73
94
  />
74
95
  <span class="fdy-daterange__sep" aria-hidden="true">–</span>
@@ -82,6 +103,14 @@ function setEnd(end: string): void {
82
103
  :invalid="invalid"
83
104
  :describedby="describedby"
84
105
  :placeholder="endPlaceholder ?? 'Sampai'"
106
+ :prev-month-label="prevMonthLabel"
107
+ :next-month-label="nextMonthLabel"
108
+ :prev-year-label="prevYearLabel"
109
+ :next-year-label="nextYearLabel"
110
+ :prev-years-label="prevYearsLabel"
111
+ :next-years-label="nextYearsLabel"
112
+ :choose-month-label="chooseMonthLabel"
113
+ :choose-year-label="chooseYearLabel"
85
114
  @update:model-value="setEnd"
86
115
  />
87
116
  </div>
@@ -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 }" @focusout="onFocusout">
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 }" @focusout="onFocusout">
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"