@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.
- package/CHANGELOG.md +105 -0
- package/COMPONENTS.md +72 -3
- package/README.id.md +4 -4
- package/README.md +3 -3
- 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/FdyDateRange.razor +20 -2
- package/adapters/blazor/FdyDateRange.razor.cs +20 -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 +24 -0
- package/adapters/blazor/FdyTimepicker.razor.cs +86 -0
- package/adapters/react/components/FdyCascade.tsx +10 -1
- package/adapters/react/components/FdyCombo.tsx +9 -0
- package/adapters/react/components/FdyDateRange.tsx +29 -0
- package/adapters/react/components/FdyDatepicker.tsx +10 -1
- package/adapters/react/components/FdyMonthpicker.tsx +10 -1
- package/adapters/react/components/FdyTimepicker.tsx +221 -0
- package/adapters/react/index.d.ts +1 -0
- package/adapters/react/index.js +1 -0
- package/adapters/vue/components/FdyCascade.vue +10 -1
- package/adapters/vue/components/FdyCombo.vue +9 -0
- package/adapters/vue/components/FdyDateRange.vue +29 -0
- package/adapters/vue/components/FdyDatepicker.vue +10 -1
- package/adapters/vue/components/FdyMonthpicker.vue +10 -1
- package/adapters/vue/components/FdyTimepicker.vue +285 -0
- package/adapters/vue/index.d.ts +1 -0
- package/adapters/vue/index.js +1 -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 +49 -1
- package/dist/freeday.js +119 -1
- package/docs/agent-onboarding.md +3 -3
- package/docs/getting-started.md +5 -5
- package/docs/integrations.md +2 -2
- 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'}
|
|
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}
|
|
@@ -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';
|
package/adapters/react/index.js
CHANGED
|
@@ -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 }"
|
|
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"
|