@ahrowe/ui 0.38.0 → 0.39.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/README.md +1 -1
- package/dist/cjs-types/common/datePicker/components/datePickerMonth/datePickerMonth.types.d.ts +6 -6
- package/dist/cjs-types/common/datePicker/dateFunctions.d.ts +10 -19
- package/dist/cjs-types/common/datePicker/datePicker.types.d.ts +20 -14
- package/dist/cjs-types/common/datePicker/index.d.ts +1 -0
- package/dist/cjs-types/common/popover/popover.types.d.ts +5 -0
- package/dist/cjs-types/common/utils/day.d.ts +16 -0
- package/dist/cjs-types/index.d.ts +1 -1
- package/dist/esm/common/datePicker/components/datePickerMonth/datePickerMonth.mjs +1 -1
- package/dist/esm/common/datePicker/components/datePickerMonth/datePickerMonth.mjs.map +1 -1
- package/dist/esm/common/datePicker/dateFunctions.mjs +1 -1
- package/dist/esm/common/datePicker/dateFunctions.mjs.map +1 -1
- package/dist/esm/common/datePicker/datePicker.mjs +1 -1
- package/dist/esm/common/datePicker/datePicker.mjs.map +1 -1
- package/dist/esm/common/hooks/useFocusBoundary.mjs +1 -1
- package/dist/esm/common/hooks/useFocusBoundary.mjs.map +1 -1
- package/dist/esm/common/popover/popover.mjs +1 -1
- package/dist/esm/common/popover/popover.mjs.map +1 -1
- package/dist/esm/common/popover/popover.types.mjs.map +1 -1
- package/dist/esm/common/utils/day.mjs +2 -0
- package/dist/esm/common/utils/day.mjs.map +1 -0
- package/dist/esm/index.mjs +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/types/common/datePicker/components/datePickerMonth/datePickerMonth.types.d.ts +6 -6
- package/dist/types/common/datePicker/dateFunctions.d.ts +10 -19
- package/dist/types/common/datePicker/datePicker.types.d.ts +20 -14
- package/dist/types/common/datePicker/index.d.ts +1 -0
- package/dist/types/common/popover/popover.types.d.ts +5 -0
- package/dist/types/common/utils/day.d.ts +16 -0
- package/dist/types/index.d.ts +1 -1
- package/docs/CLAUDE.md +5 -1
- package/docs/DatePicker.md +72 -45
- package/docs/FormValidator.md +3 -3
- package/docs/Popover.md +15 -0
- package/package.json +2 -2
|
@@ -8,16 +8,16 @@ export interface DatePickerMonthProps {
|
|
|
8
8
|
month?: number;
|
|
9
9
|
/** Year to display. */
|
|
10
10
|
year?: number;
|
|
11
|
-
/** Called with the day that was picked
|
|
12
|
-
onSelect?: (
|
|
11
|
+
/** Called with the day that was picked, `'YYYY-MM-DD'`. */
|
|
12
|
+
onSelect?: (day: string) => void;
|
|
13
13
|
/** First day of the highlighted range, and the day that starts with the tab stop. */
|
|
14
|
-
selectionStart?:
|
|
14
|
+
selectionStart?: string | null;
|
|
15
15
|
/** Last day of the highlighted range. */
|
|
16
|
-
selectionEnd?:
|
|
16
|
+
selectionEnd?: string | null;
|
|
17
17
|
/** Days before this are not selectable. */
|
|
18
|
-
minDate?:
|
|
18
|
+
minDate?: string | null;
|
|
19
19
|
/** Days after this are not selectable. */
|
|
20
|
-
maxDate?:
|
|
20
|
+
maxDate?: string | null;
|
|
21
21
|
/** Called to shift the displayed month by ±1 when arrow navigation runs past its edge. */
|
|
22
22
|
onNavigateMonth?: (delta: 1 | -1) => void;
|
|
23
23
|
}
|
|
@@ -1,26 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function getDateFromDayMonthAndYear(day: number, month: number, year: number): Date;
|
|
1
|
+
import { DayParts } from '../utils/day.js';
|
|
3
2
|
export declare function getMonthName(locale: string, year: number, month: number, day: number, short?: boolean): string;
|
|
4
3
|
export declare function getMonthNames(locale?: string): string[];
|
|
5
4
|
export declare function getWeekDays(locale: string, short?: boolean): string[];
|
|
5
|
+
/** 1 (Monday) to 7 (Sunday). */
|
|
6
6
|
export declare function getStartWeekDayOfMonth(month: number, year: number): number;
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
month: number;
|
|
16
|
-
year: number;
|
|
17
|
-
};
|
|
18
|
-
export declare function getDayMonthAndYearOfDate(date: Date): {
|
|
19
|
-
day: number;
|
|
20
|
-
month: number;
|
|
21
|
-
year: number;
|
|
22
|
-
};
|
|
23
|
-
export declare function getDateString(date: Date): string;
|
|
7
|
+
/** `delta` days from a day, rolling over into the months and years around it. */
|
|
8
|
+
export declare function addDays(day: number, month: number, year: number, delta: number): DayParts;
|
|
9
|
+
/** Today in the user's time zone. */
|
|
10
|
+
export declare function getToday(): DayParts;
|
|
11
|
+
/** A `'YYYY-MM-DD'` day as the field shows it, `dd.mm.yyyy`, or `''` for anything else. */
|
|
12
|
+
export declare function getDateString(value: string | null | undefined): string;
|
|
13
|
+
/** What the field holds, `dd.mm.yyyy`, as `'YYYY-MM-DD'`; `null` while incomplete, or for a day that does not exist. */
|
|
14
|
+
export declare function parseDateString(text: string): string | null;
|
|
24
15
|
export interface DateMaskSelection {
|
|
25
16
|
start: number;
|
|
26
17
|
end: number;
|
|
@@ -3,8 +3,10 @@ import { FormValidator } from '../../services/formValidation/index.js';
|
|
|
3
3
|
import { HtmlProps, SlotClassNames, SlotStyles } from '../types/slots.types.js';
|
|
4
4
|
export type DatePickerSlots = 'root' | 'input' | 'header' | 'monthDropdown' | 'yearDropdown' | 'calendar' | 'clearButton';
|
|
5
5
|
/**
|
|
6
|
-
* `onChange` and `onSelect` report a
|
|
6
|
+
* `onChange` and `onSelect` report a day rather than a DOM event, so the native
|
|
7
7
|
* handlers of those names are dropped instead of being widened to accept both.
|
|
8
|
+
*
|
|
9
|
+
* Every day is a `'YYYY-MM-DD'` string, never a `Date`: see docs/DatePicker.md.
|
|
8
10
|
*/
|
|
9
11
|
export interface DatePickerProps extends Omit<HtmlProps<HTMLDivElement>, 'onChange' | 'onSelect'> {
|
|
10
12
|
/** Class on the root element. */
|
|
@@ -12,33 +14,37 @@ export interface DatePickerProps extends Omit<HtmlProps<HTMLDivElement>, 'onChan
|
|
|
12
14
|
style?: CSSProperties;
|
|
13
15
|
classNames?: SlotClassNames<DatePickerSlots>;
|
|
14
16
|
styles?: SlotStyles<DatePickerSlots>;
|
|
15
|
-
/** Selected
|
|
16
|
-
selected?:
|
|
17
|
+
/** Selected day, `'YYYY-MM-DD'`, controlled. Ignored while a `formValidator` owns the value. */
|
|
18
|
+
selected?: string | null;
|
|
17
19
|
/** Floating label on the input. */
|
|
18
20
|
label?: string;
|
|
19
21
|
/**
|
|
20
22
|
* Called when a day is picked in the calendar, after `onChange`. Picking also
|
|
21
23
|
* closes the picker.
|
|
22
24
|
*/
|
|
23
|
-
onSelect?: (
|
|
25
|
+
onSelect?: (day: string | null) => void;
|
|
24
26
|
/**
|
|
25
27
|
* Called whenever the value changes, whether picked in the calendar or typed
|
|
26
|
-
* into the input. `null` when the field was cleared or holds no valid
|
|
28
|
+
* into the input. `null` when the field was cleared or holds no valid day.
|
|
27
29
|
*/
|
|
28
|
-
onChange?: (
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
onChange?: (day: string | null) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Hands the value to a validator, which then owns it. See docs/FormValidator.md. The three
|
|
33
|
+
* forms are what TypeScript infers for `new FormValidator<string | null>(null)`,
|
|
34
|
+
* `new FormValidator(null)` and `new FormValidator('')`; a `Date` validator fits none of them.
|
|
35
|
+
*/
|
|
36
|
+
formValidator?: FormValidator<string | null> | FormValidator<string> | FormValidator<null> | null;
|
|
37
|
+
/** Earliest selectable day, `'YYYY-MM-DD'`. Also bounds the year dropdown. */
|
|
38
|
+
minDate?: string | null;
|
|
39
|
+
/** Latest selectable day, `'YYYY-MM-DD'`. Also bounds the year dropdown. */
|
|
40
|
+
maxDate?: string | null;
|
|
35
41
|
/**
|
|
36
42
|
* Start of a highlighted range. Defaults to the selected date, so two pickers
|
|
37
43
|
* sharing a start and an end highlight the span between them.
|
|
38
44
|
*/
|
|
39
|
-
startDate?:
|
|
45
|
+
startDate?: string | null;
|
|
40
46
|
/** End of the highlighted range. */
|
|
41
|
-
endDate?:
|
|
47
|
+
endDate?: string | null;
|
|
42
48
|
/** Render the calendar in place instead of in a popover. */
|
|
43
49
|
isInline?: boolean;
|
|
44
50
|
/** Open the calendar in a modal, with the input read-only. */
|
|
@@ -24,6 +24,11 @@ export type PopoverSlots = 'root' | 'trigger' | 'floating' | 'panel' | 'arrow' |
|
|
|
24
24
|
export interface PopoverProps extends Omit<HtmlProps, 'content'> {
|
|
25
25
|
/** The trigger element. */
|
|
26
26
|
children?: ReactNode;
|
|
27
|
+
/**
|
|
28
|
+
* Place the panel against this element instead of `children`, for one Popover does not wrap.
|
|
29
|
+
* Open it with `isOpen`; `null` falls back to `children`.
|
|
30
|
+
*/
|
|
31
|
+
anchor?: HTMLElement | null;
|
|
27
32
|
/** The popover body. */
|
|
28
33
|
content?: ReactNode;
|
|
29
34
|
/** Preferred side (default `Bottom`). */
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface DayParts {
|
|
2
|
+
year: number;
|
|
3
|
+
/** 1 to 12. */
|
|
4
|
+
month: number;
|
|
5
|
+
day: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function daysInMonth({ year, month }: Pick<DayParts, 'year' | 'month'>): number;
|
|
8
|
+
/** The parts of a `'YYYY-MM-DD'` string, or `null` for anything else, 31 February included. */
|
|
9
|
+
export declare function parseDay(value: unknown): DayParts | null;
|
|
10
|
+
/** `value` itself when it is a `'YYYY-MM-DD'` day that exists, otherwise `null`. */
|
|
11
|
+
export declare function asDay(value: unknown): string | null;
|
|
12
|
+
export declare function formatDay({ year, month, day }: DayParts): string;
|
|
13
|
+
/** The day `date` falls on in the browser's time zone, as `'YYYY-MM-DD'`; `''` for an Invalid Date. */
|
|
14
|
+
export declare function dateToDay(date: Date): string;
|
|
15
|
+
/** The first moment of a `'YYYY-MM-DD'` day in the browser's time zone. An Invalid Date for anything else. */
|
|
16
|
+
export declare function dayToDate(day: string): Date;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -158,4 +158,4 @@ export * from './common/hooks/index.js';
|
|
|
158
158
|
export { FormValidator, FormValidatorGroup, Validators, ValidatableComponent, useFormValidator, useFormValidatorGroup, } from './services/formValidation/index.js';
|
|
159
159
|
export type { ValidationError, ValidatorFunction, ErrorParser, FormValidatorGroupOptions, InferGroupValues, MergedValues, } from './services/formValidation/index.js';
|
|
160
160
|
export { mapLocale, setLocale, getLocale } from './services/localization.js';
|
|
161
|
-
export { isNumber, isDate, isISODate, isEmail, isBic, isURL, hasEightPasswordChars, hasOnePasswordLetter, hasOnePasswordDigit, isPassword, } from '@ahrowe/form-validation';
|
|
161
|
+
export { isNumber, isDate, isDay, isISODate, isEmail, isBic, isURL, hasEightPasswordChars, hasOnePasswordLetter, hasOnePasswordDigit, isPassword, } from '@ahrowe/form-validation';
|
package/docs/CLAUDE.md
CHANGED
|
@@ -20,6 +20,10 @@ enums and slot names. Do not infer props from a component's name.
|
|
|
20
20
|
RadioGroup, DatePicker, TimeInput, OtpInput, IconPicker) take `formValidator`, which then owns
|
|
21
21
|
the value: `new FormValidator('', [Validators.required()])`. Whole forms use
|
|
22
22
|
`useFormValidatorGroup`. See FormValidator.md and FormValidatorGroup.md.
|
|
23
|
+
- **Days and times are strings**, never a `Date`: DatePicker holds `'YYYY-MM-DD'`, TimeInput
|
|
24
|
+
`'HH:MM'`. `dateToDay` and `dayToDate` convert in the browser's time zone; never use
|
|
25
|
+
`toISOString().slice(0, 10)`, which gives the UTC day. DatePicker.md shows how to turn a day
|
|
26
|
+
into a moment in a given time zone.
|
|
23
27
|
- **Styling.** `className` and `style` hit the root element; `classNames.<slot>` and `styles.<slot>`
|
|
24
28
|
hit named inner elements. Slot names are on each doc's **Slots** line. Every native attribute of
|
|
25
29
|
the root element passes through (`id`, `aria-*`, `data-*`, `on*`).
|
|
@@ -63,7 +67,7 @@ enums and slot names. Do not infer props from a component's name.
|
|
|
63
67
|
| ConfigProvider.md | ConfigProvider | Set global **default props** for library components once, near the root of your app, instead of repeating the same prop on every instance |
|
|
64
68
|
| ConfirmModal.md | ConfirmModal | Confirmation dialog before destructive or irreversible actions — delete, archive, send, overwrite |
|
|
65
69
|
| CopyButton.md | CopyButton | Icon-only button that copies a value to the clipboard: an invoice number, an ID in a table row, an API key or share link inside an `Input` |
|
|
66
|
-
| DatePicker.md | DatePicker | Date selection input — booking forms, birth date fields, date range pickers, deadline selectors |
|
|
70
|
+
| DatePicker.md | DatePicker, dateToDay, dayToDate | Date selection input — booking forms, birth date fields, date range pickers, deadline selectors |
|
|
67
71
|
| Divider.md | Divider, DividerTextPosition, DividerOrientation | A rule that separates sections of content — horizontal by default, filling the width of its parent |
|
|
68
72
|
| Drawer.md | Drawer | Inline horizontal panel that collapses and expands by animating its width — side panels, navigation rails, detail/filter panes that slide open beside the main content |
|
|
69
73
|
| DropZone.md | DropZone, DropZoneReturnType, DropZoneStatus | Drag-and-drop file upload area, optionally click-to-open the native picker |
|
package/docs/DatePicker.md
CHANGED
|
@@ -2,72 +2,58 @@
|
|
|
2
2
|
|
|
3
3
|
**When to use:** Date selection input — booking forms, birth date fields, date range pickers, deadline selectors. Renders as a text input with a floating label by default; can also be inline or modal.
|
|
4
4
|
|
|
5
|
-
**Keywords:** calendar picker
|
|
5
|
+
**Keywords:** calendar picker, timezone, ISO date, date string, day picker
|
|
6
6
|
|
|
7
7
|
**Keyboard:** the day grid uses a roving tab stop (the selected day, or today, or the 1st — one Tab reaches it, not every day individually); arrow keys move by day (`←`/`→`) or by week (`↑`/`↓`), crossing into the adjacent month automatically when they run past the edge, and Enter/Space selects the focused day.
|
|
8
8
|
|
|
9
|
-
**Import:** `import { DatePicker } from '@ahrowe/ui'`
|
|
9
|
+
**Import:** `import { DatePicker, dateToDay, dayToDate } from '@ahrowe/ui'`
|
|
10
10
|
**Types:** `import type { DatePickerProps } from '@ahrowe/ui'`
|
|
11
11
|
|
|
12
12
|
```tsx
|
|
13
|
-
import { DatePicker, FormValidator } from '@ahrowe/ui';
|
|
13
|
+
import { DatePicker, FormValidator, dateToDay } from '@ahrowe/ui';
|
|
14
14
|
|
|
15
|
-
// Controlled input with floating label
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
selected={date}
|
|
19
|
-
onSelect={(date) => setDate(date)}
|
|
20
|
-
/>
|
|
15
|
+
// Controlled input with floating label. The value is a day: '2026-09-15'
|
|
16
|
+
const [day, setDay] = useState<string | null>(null);
|
|
17
|
+
<DatePicker label="Date of birth" selected={day} onSelect={setDay} />
|
|
21
18
|
|
|
22
19
|
// With FormValidator
|
|
23
|
-
const
|
|
24
|
-
<DatePicker label="Start date" formValidator={
|
|
20
|
+
const dayValidator = new FormValidator<string | null>(null);
|
|
21
|
+
<DatePicker label="Start date" formValidator={dayValidator} />
|
|
25
22
|
|
|
26
23
|
// Inline calendar (no input field)
|
|
27
|
-
<DatePicker
|
|
28
|
-
selected={date}
|
|
29
|
-
onSelect={setDate}
|
|
30
|
-
isInline
|
|
31
|
-
noInput
|
|
32
|
-
/>
|
|
24
|
+
<DatePicker selected={day} onSelect={setDay} isInline noInput />
|
|
33
25
|
|
|
34
26
|
// Inside a modal (calendar opens in a Modal overlay)
|
|
35
|
-
<DatePicker
|
|
36
|
-
label="Schedule date"
|
|
37
|
-
selected={date}
|
|
38
|
-
onSelect={setDate}
|
|
39
|
-
isModal
|
|
40
|
-
/>
|
|
27
|
+
<DatePicker label="Schedule date" selected={day} onSelect={setDay} isModal />
|
|
41
28
|
|
|
42
|
-
//
|
|
29
|
+
// From today to the end of the year, with a clear button
|
|
43
30
|
<DatePicker
|
|
44
31
|
label="Appointment"
|
|
45
|
-
selected={
|
|
46
|
-
onSelect={
|
|
47
|
-
minDate={new Date()}
|
|
48
|
-
maxDate=
|
|
32
|
+
selected={day}
|
|
33
|
+
onSelect={setDay}
|
|
34
|
+
minDate={dateToDay(new Date())}
|
|
35
|
+
maxDate="2026-12-31"
|
|
49
36
|
showClearButton
|
|
50
37
|
/>
|
|
51
38
|
|
|
52
|
-
//
|
|
53
|
-
<DatePicker
|
|
54
|
-
|
|
55
|
-
selected={startDate}
|
|
56
|
-
onSelect={setStartDate}
|
|
57
|
-
startDate={startDate}
|
|
58
|
-
endDate={endDate}
|
|
59
|
-
/>
|
|
39
|
+
// Range: two pickers sharing a highlighted start and end
|
|
40
|
+
<DatePicker label="From" selected={from} onSelect={setFrom} startDate={from} endDate={to} />
|
|
41
|
+
<DatePicker label="To" selected={to} onSelect={setTo} startDate={from} endDate={to} minDate={from} />
|
|
60
42
|
```
|
|
61
43
|
|
|
44
|
+
**A day, not a moment:** every day DatePicker takes or hands out is a `'YYYY-MM-DD'` string, the value an `<input type="date">` has. It has no time and no time zone, so it is the same day for everyone who sees it. The strings sort as the days do, so `from < to` compares two of them. `null`, `''` and anything that is not a real day, such as `'2026-02-30'`, mean no value; anything but the first two also logs a warning naming the prop, once.
|
|
45
|
+
|
|
46
|
+
`dateToDay(date)` and `dayToDate(day)` convert to and from a `Date` in the browser's time zone: `dateToDay(new Date())` is today, `dayToDate(day)` the first moment of that day. Do not use `date.toISOString().slice(0, 10)`: that is the day in UTC, which in Vienna shortly after midnight is still yesterday.
|
|
47
|
+
|
|
62
48
|
**Key props:**
|
|
63
49
|
|
|
64
50
|
| Prop | Type | Description |
|
|
65
51
|
|------|------|-------------|
|
|
66
52
|
| `label` | `string` | Floating label on the input |
|
|
67
|
-
| `selected` | `
|
|
68
|
-
| `onSelect` | `(
|
|
69
|
-
| `onChange` | `(
|
|
70
|
-
| `formValidator` | `FormValidator
|
|
53
|
+
| `selected` | `string \| null` | Selected day, `'YYYY-MM-DD'` |
|
|
54
|
+
| `onSelect` | `(day: string \| null) => void` | Called when a day is picked in the calendar |
|
|
55
|
+
| `onChange` | `(day: string \| null) => void` | Called on every change, picked or typed |
|
|
56
|
+
| `formValidator` | `FormValidator<string \| null>` | Connects to validation, and then owns the value. `new FormValidator(null)` and `new FormValidator('')` fit too |
|
|
71
57
|
| `isRequired` | `boolean` | |
|
|
72
58
|
| `isValid` | `boolean` | Manual valid state |
|
|
73
59
|
| `errorMessage` | `string` | Manual error message |
|
|
@@ -75,12 +61,53 @@ const dateValidator = new FormValidator<Date | null>(null);
|
|
|
75
61
|
| `noHeader` | `boolean` | Hide the month/year navigation header |
|
|
76
62
|
| `isModal` | `boolean` | Open calendar in a Modal overlay |
|
|
77
63
|
| `isInline` | `boolean` | Render calendar always-visible inline |
|
|
78
|
-
| `minDate` | `
|
|
79
|
-
| `maxDate` | `
|
|
64
|
+
| `minDate` | `string \| null` | Earliest selectable day |
|
|
65
|
+
| `maxDate` | `string \| null` | Latest selectable day |
|
|
80
66
|
| `showClearButton` | `boolean` | Show a clear/reset button |
|
|
81
|
-
| `startDate` | `
|
|
82
|
-
| `endDate` | `
|
|
67
|
+
| `startDate` | `string \| null` | Range highlight start |
|
|
68
|
+
| `endDate` | `string \| null` | Range highlight end |
|
|
83
69
|
|
|
84
70
|
**Slots:** `root` `input` `header` `monthDropdown` `yearDropdown` `calendar` `clearButton`
|
|
85
71
|
|
|
86
|
-
The root also takes the full native attribute set (`id`, `data-*`, `aria-*`, `title`, event handlers). `onChange` and `onSelect` report a
|
|
72
|
+
The root also takes the full native attribute set (`id`, `data-*`, `aria-*`, `title`, event handlers). `onChange` and `onSelect` report a day, so the native handlers of those names are not available.
|
|
73
|
+
|
|
74
|
+
## When a day stands for a moment
|
|
75
|
+
|
|
76
|
+
A poll that closes on a day, a booking that starts on one, a deadline: here the day has to become a moment, and only your app knows in which time zone and at which point of the day. Store the moment itself (a UTC timestamp), and show it in the time zone of whoever reads it.
|
|
77
|
+
|
|
78
|
+
In the user's own time zone the platform is enough:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const start = dayToDate(day); // the first moment of the day, where the user is
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
In a given zone, a company's, a household's, a venue's, use a library that knows time zones, such as Luxon. Name the zone (`'Europe/Vienna'`), not its offset (`+02:00`): the offset changes with daylight saving.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { DateTime } from 'luxon';
|
|
88
|
+
|
|
89
|
+
// Closes at the end of the day in Vienna, wherever the reader is.
|
|
90
|
+
const closesAt = DateTime.fromISO(day, { zone: 'Europe/Vienna' }).endOf('day').toJSDate();
|
|
91
|
+
|
|
92
|
+
// And back, for the picker: the day that moment falls on in Vienna.
|
|
93
|
+
const shown = DateTime.fromJSDate(closesAt, { zone: 'Europe/Vienna' }).toISODate();
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
With a [TimeInput](TimeInput.md) next to it, which holds `'HH:MM'`, the two make a moment the same way: ``DateTime.fromISO(`${day}T${time}`, { zone })``, or, in the user's own zone, `dayToDate(day)` followed by `setHours(hours, minutes)`.
|
|
97
|
+
|
|
98
|
+
## Migration from Date values
|
|
99
|
+
|
|
100
|
+
Before 0.39 every day was a `Date` at midnight UTC. Now it is a `'YYYY-MM-DD'` string, and TypeScript points at each place to change, because a `Date` no longer fits any of the props. Two things still get through: a `Date` in plain JavaScript or through a validator typed as `FormValidator<any>`, and a stored value read back as its JSON, `'2026-09-15T00:00:00.000Z'`, which is a string to TypeScript. DatePicker shows either as an empty field, logs a warning naming the prop, and leaves the value alone until the user actually edits the field.
|
|
101
|
+
|
|
102
|
+
| Before | After |
|
|
103
|
+
|--------|-------|
|
|
104
|
+
| `useState<Date \| null>(null)` | `useState<string \| null>(null)` |
|
|
105
|
+
| `new FormValidator<Date \| null>(null)` | `new FormValidator<string \| null>(null)` |
|
|
106
|
+
| `selected={new Date()}`, `minDate={new Date()}` | `selected={dateToDay(new Date())}` |
|
|
107
|
+
| `a.getTime() < b.getTime()` | `a < b` |
|
|
108
|
+
| `date.toLocaleDateString(locale)` | `dayToDate(day).toLocaleDateString(locale)` |
|
|
109
|
+
| `JSON.stringify(date)`, `date.toISOString()` | the string itself |
|
|
110
|
+
|
|
111
|
+
Data you already stored from DatePicker was midnight UTC of the picked day. `stored.toISOString().slice(0, 10)` turns exactly that back into the day, since the value was built in UTC; do this only for values DatePicker produced.
|
|
112
|
+
|
|
113
|
+
Where the value was really a moment (the poll and deadline cases above), decide the time zone once, in your own code, as shown in the section before this one, rather than relying on what DatePicker happened to hand out.
|
package/docs/FormValidator.md
CHANGED
|
@@ -32,7 +32,7 @@ nameValidator.dirty // value has changed since creation
|
|
|
32
32
|
> | Field | Default value |
|
|
33
33
|
> |-------|---------------|
|
|
34
34
|
> | string (text, email, …) | `''` |
|
|
35
|
-
> | `
|
|
35
|
+
> | day (`DatePicker`, `'YYYY-MM-DD'`) | `null` |
|
|
36
36
|
> | number | `undefined` (or `0`) |
|
|
37
37
|
> | checkbox / boolean | `false` |
|
|
38
38
|
> | single-select id | `null` |
|
|
@@ -69,7 +69,7 @@ function NameField() {
|
|
|
69
69
|
|
|
70
70
|
```tsx
|
|
71
71
|
const ageValidator = new FormValidator<number | undefined>(undefined, [Validators.required()]);
|
|
72
|
-
const
|
|
72
|
+
const dayValidator = new FormValidator<string | null>(null); // DatePicker: '2026-09-15'
|
|
73
73
|
const agreedValidator = new FormValidator<boolean>(false, [Validators.required()]);
|
|
74
74
|
```
|
|
75
75
|
|
|
@@ -108,7 +108,7 @@ Validators.multiEmail(errorMsg?) // comma/semicolon-separated list of
|
|
|
108
108
|
Validators.mustBeNumber(errorMsg?) // parses as a number
|
|
109
109
|
Validators.oneOf(allowedValues, errorMsg?) // value is in the allowed array
|
|
110
110
|
Validators.typeOf(allowedTypes, errorMsg?) // typeof value matches (string or string[])
|
|
111
|
-
Validators.date(errorMsg?) //
|
|
111
|
+
Validators.date(errorMsg?) // a real date: a 'YYYY-MM-DD' day (DatePicker), DD.MM.YYYY, a toISOString() timestamp, or a valid Date
|
|
112
112
|
Validators.dateObject(errorMsg?) // an object with an `isValid` flag (e.g. a date wrapper)
|
|
113
113
|
Validators.website(errorMsg?) // valid URL
|
|
114
114
|
Validators.bic(errorMsg?) // valid BIC/SWIFT code
|
package/docs/Popover.md
CHANGED
|
@@ -28,6 +28,18 @@ import { Popover, PopoverPlacement, PopoverAlign, PopoverTrigger } from '@ahrowe
|
|
|
28
28
|
<Popover isOpen={open} onOpenChange={setOpen} content={<Form onDone={() => setOpen(false)} />}>
|
|
29
29
|
<button>Edit</button>
|
|
30
30
|
</Popover>
|
|
31
|
+
|
|
32
|
+
// One popover for many elements it does not wrap: a calendar's events, a table's cells
|
|
33
|
+
const [anchor, setAnchor] = useState<HTMLElement | null>(null);
|
|
34
|
+
{events.map((event) => (
|
|
35
|
+
<button key={event.id} onClick={(e) => setAnchor(e.currentTarget)}>{event.title}</button>
|
|
36
|
+
))}
|
|
37
|
+
<Popover
|
|
38
|
+
anchor={anchor}
|
|
39
|
+
isOpen={anchor !== null}
|
|
40
|
+
onOpenChange={(open) => !open && setAnchor(null)}
|
|
41
|
+
content={<EventDetails />}
|
|
42
|
+
/>
|
|
31
43
|
```
|
|
32
44
|
|
|
33
45
|
**PopoverPlacement enum:** `PopoverPlacement.Top` | `Bottom` | `Left` | `Right` — the preferred side; auto-flips to the opposite side when there's no room.
|
|
@@ -44,6 +56,7 @@ import { Popover, PopoverPlacement, PopoverAlign, PopoverTrigger } from '@ahrowe
|
|
|
44
56
|
|------|------|---------|-------------|
|
|
45
57
|
| `content` | `ReactNode` | — | Panel body |
|
|
46
58
|
| `children` | `ReactNode` | — | Trigger element |
|
|
59
|
+
| `anchor` | `HTMLElement \| null` | | Place the panel against this element instead of `children`. Open it with `isOpen`; `children` can be left out. A click on it is not an outside click, Escape returns the focus to it, and changing it while open moves the panel. `null` falls back to `children` |
|
|
47
60
|
| `placement` | `PopoverPlacement` | `Bottom` | Preferred side |
|
|
48
61
|
| `align` | `PopoverAlign` | `Center` | Cross-axis alignment |
|
|
49
62
|
| `trigger` | `PopoverTrigger` | `Click` | What opens the popover |
|
|
@@ -80,3 +93,5 @@ Requires a `<div id="bodyEnd"></div>` at the app root — the panel is portaled
|
|
|
80
93
|
**Scrolling:** where the browser supports CSS anchor positioning, including the trigger's transforms, the panel is anchored to the trigger in CSS, so it moves with a scrolling trigger in the same frame instead of catching up a frame later. Firefox resolves anchors without transforms, so it keeps the pixel positioning. While it is open the trigger carries an inline `anchor-name`, added to any inline one it already has; an `anchor-name` your stylesheet gives the trigger is overridden for that time.
|
|
81
94
|
|
|
82
95
|
**Keyboard & focus (click trigger):** the panel is portaled to the end of the DOM, so Tab can't reach it naturally. When the panel has focusable content and the keyboard opened it (Enter or Space on the trigger, or ArrowDown/ArrowUp), focus moves to the first focusable element and the panel gets `role="dialog"`; a pointer click leaves the focus where it already is. `focusOnOpen` forces either. Tabbing past the last element closes the popover and moves focus to the element after the trigger; Shift+Tab past the first element, and Escape, close it and return focus to the trigger. This is non-modal: focus is not trapped, and hover/focus triggers don't steal focus. Escape closes only the innermost open layer, so a popover inside a `Modal` closes on the first press and leaves the modal open. Popovers with only static content don't manage focus or claim the dialog role.
|
|
96
|
+
|
|
97
|
+
**Anchored (`anchor`):** your code opens the panel, not a key event Popover sees, so the default `focusOnOpen` looks at the click that opened it: Enter or Space on the anchor or on something inside it (a click with `detail === 0`) moves the focus into the panel, a pointer click leaves it where it is, and an open with no click at all, such as suggestions appearing while the user types into an anchored field, never takes it. Escape, Tab and Shift+Tab out of the panel continue from the anchor as they would from a trigger: from the anchor itself when it takes focus, otherwise from the first focusable element inside it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ahrowe/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
4
4
|
"description": "A React UI component library with theming, CSS Modules, and TypeScript support",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"prepare": "husky"
|
|
89
89
|
},
|
|
90
90
|
"dependencies": {
|
|
91
|
-
"@ahrowe/form-validation": "^0.
|
|
91
|
+
"@ahrowe/form-validation": "^0.3.0",
|
|
92
92
|
"@dnd-kit/core": "^6.3.1",
|
|
93
93
|
"@dnd-kit/sortable": "^10.0.0",
|
|
94
94
|
"@dnd-kit/utilities": "^3.2.2",
|