@nepdatejs/react 0.0.0-bootstrap → 0.1.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/LICENSE +21 -0
- package/README.md +73 -1
- package/dist/date-text-BT82cZ7i.cjs +84 -0
- package/dist/date-text-BT82cZ7i.cjs.map +1 -0
- package/dist/date-text-Df1gFksR.js +61 -0
- package/dist/date-text-Df1gFksR.js.map +1 -0
- package/dist/date-text-Dipetpbp.d.cts +37 -0
- package/dist/date-text-Dipetpbp.d.ts +37 -0
- package/dist/headless-6BuUOtOR.d.cts +194 -0
- package/dist/headless-BK0XRHKb.js +513 -0
- package/dist/headless-BK0XRHKb.js.map +1 -0
- package/dist/headless-CFZ06zLn.d.ts +194 -0
- package/dist/headless-HxTkvgbz.cjs +572 -0
- package/dist/headless-HxTkvgbz.cjs.map +1 -0
- package/dist/headless.cjs +11 -0
- package/dist/headless.d.cts +2 -0
- package/dist/headless.d.ts +2 -0
- package/dist/headless.js +2 -0
- package/dist/index.cjs +891 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +245 -0
- package/dist/index.d.ts +245 -0
- package/dist/index.js +871 -0
- package/dist/index.js.map +1 -0
- package/dist/server.cjs +6 -0
- package/dist/server.d.cts +2 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +1 -0
- package/package.json +107 -4
- package/src/bindings.ts +247 -0
- package/src/calendar-body.tsx +133 -0
- package/src/calendar.tsx +34 -0
- package/src/date-picker.tsx +350 -0
- package/src/date-range-picker.tsx +223 -0
- package/src/date-text.tsx +110 -0
- package/src/date-time-picker.tsx +287 -0
- package/src/headless.ts +21 -0
- package/src/hooks.ts +474 -0
- package/src/icons.tsx +35 -0
- package/src/index.ts +21 -0
- package/src/multi-date-picker.tsx +243 -0
- package/src/panel.ts +67 -0
- package/src/presets.tsx +44 -0
- package/src/server.ts +8 -0
- package/src/time-columns.tsx +60 -0
- package/src/use-store.ts +113 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { useRef, type CSSProperties, type ReactElement } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
getLocale,
|
|
4
|
+
type CalendarDate,
|
|
5
|
+
type CalendarSystem,
|
|
6
|
+
type LocaleCode,
|
|
7
|
+
type Weekday,
|
|
8
|
+
} from '@nepdatejs/core';
|
|
9
|
+
import type { HolidayInput, HolidayLookup } from '@nepdatejs/calendar';
|
|
10
|
+
import type { RangeDetail, RangePreset, RangeValue } from '@nepdatejs/datepicker';
|
|
11
|
+
import { restoreFocus } from '@nepdatejs/dom';
|
|
12
|
+
import { CalendarBody } from './calendar-body';
|
|
13
|
+
import { useDateRangePicker } from './hooks';
|
|
14
|
+
import { Icon } from './icons';
|
|
15
|
+
import { usePanel } from './panel';
|
|
16
|
+
import type { PanelLayout } from '@nepdatejs/dom';
|
|
17
|
+
import { PANEL_WIDTH, PANEL_WIDTH_WITH_PRESETS, PanelBody } from './presets';
|
|
18
|
+
|
|
19
|
+
export interface DateRangePickerProps {
|
|
20
|
+
value?: RangeValue | undefined;
|
|
21
|
+
defaultValue?: RangeValue | undefined;
|
|
22
|
+
onChange?: ((value: RangeValue, detail: RangeDetail) => void) | undefined;
|
|
23
|
+
calendar?: CalendarSystem | undefined;
|
|
24
|
+
defaultCalendar?: CalendarSystem | undefined;
|
|
25
|
+
min?: string | undefined;
|
|
26
|
+
max?: string | undefined;
|
|
27
|
+
minDays?: number | undefined;
|
|
28
|
+
maxDays?: number | undefined;
|
|
29
|
+
/** Shortcuts beside the calendar: 'last7Days', 'thisMonth', 'thisFiscalYear', … or { id, label, resolve }. */
|
|
30
|
+
presets?: readonly RangePreset[] | undefined;
|
|
31
|
+
isDateDisabled?: ((date: CalendarDate) => boolean) | undefined;
|
|
32
|
+
holidays?: readonly HolidayInput[] | HolidayLookup | undefined;
|
|
33
|
+
locale?: LocaleCode | undefined;
|
|
34
|
+
weekStartsOn?: Weekday | undefined;
|
|
35
|
+
weekendDays?: readonly Weekday[] | undefined;
|
|
36
|
+
showOtherCalendar?: boolean | undefined;
|
|
37
|
+
today?: CalendarDate | undefined;
|
|
38
|
+
/** Field names for plain form posts. */
|
|
39
|
+
startName?: string | undefined;
|
|
40
|
+
endName?: string | undefined;
|
|
41
|
+
startLabel?: string | undefined;
|
|
42
|
+
endLabel?: string | undefined;
|
|
43
|
+
/** Show a "Fiscal year" button that switches the panel to fiscal-year tiles. */
|
|
44
|
+
fiscalYearView?: boolean | undefined;
|
|
45
|
+
disabled?: boolean | undefined;
|
|
46
|
+
/** Add the dashes of YYYY-MM-DD while the user types digits. Default true. */
|
|
47
|
+
mask?: boolean | undefined;
|
|
48
|
+
/** Show the calendar in the page, always open, instead of in a popover. */
|
|
49
|
+
inline?: boolean | undefined;
|
|
50
|
+
/** 'sheet' opens a bottom sheet; 'popover' sits beside the field; 'auto' (default) uses the sheet below 480px. */
|
|
51
|
+
panelLayout?: PanelLayout | undefined;
|
|
52
|
+
theme?: 'light' | 'dark' | 'auto' | undefined;
|
|
53
|
+
className?: string | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Two BS/AD date fields sharing one calendar panel in range mode. */
|
|
57
|
+
export function DateRangePicker({
|
|
58
|
+
startName,
|
|
59
|
+
endName,
|
|
60
|
+
startLabel,
|
|
61
|
+
endLabel,
|
|
62
|
+
fiscalYearView = false,
|
|
63
|
+
disabled = false,
|
|
64
|
+
panelLayout,
|
|
65
|
+
theme,
|
|
66
|
+
className,
|
|
67
|
+
...options
|
|
68
|
+
}: DateRangePickerProps): ReactElement {
|
|
69
|
+
const picker = useDateRangePicker(options);
|
|
70
|
+
const { state, store, calendar } = picker;
|
|
71
|
+
const labels = getLocale(options.locale);
|
|
72
|
+
const field = useRef<HTMLDivElement | null>(null);
|
|
73
|
+
const row = useRef<HTMLDivElement | null>(null);
|
|
74
|
+
const panel = useRef<HTMLDivElement | null>(null);
|
|
75
|
+
const start = useRef<HTMLInputElement | null>(null);
|
|
76
|
+
|
|
77
|
+
const panelWidth = state.presets.length > 0 ? PANEL_WIDTH_WITH_PRESETS : PANEL_WIDTH;
|
|
78
|
+
usePanel(
|
|
79
|
+
state.open && !state.inline,
|
|
80
|
+
{ anchor: row, panel, field, returnFocus: start },
|
|
81
|
+
() => {
|
|
82
|
+
store.actions.close();
|
|
83
|
+
},
|
|
84
|
+
calendar.focusGrid,
|
|
85
|
+
panelWidth,
|
|
86
|
+
panelLayout,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const invalid = (which: 'start' | 'end'): boolean =>
|
|
90
|
+
state.error !== null &&
|
|
91
|
+
(state.error.field === which || state.error.field === 'range');
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div
|
|
95
|
+
ref={field}
|
|
96
|
+
className={className ? `ndp-field ${className}` : 'ndp-field'}
|
|
97
|
+
data-ndp-theme={theme}
|
|
98
|
+
>
|
|
99
|
+
<div ref={row} className="ndp-range">
|
|
100
|
+
<div
|
|
101
|
+
className="ndp-input"
|
|
102
|
+
data-invalid={invalid('start') || undefined}
|
|
103
|
+
data-disabled={disabled || undefined}
|
|
104
|
+
>
|
|
105
|
+
<input
|
|
106
|
+
ref={start}
|
|
107
|
+
className="ndp-input__control"
|
|
108
|
+
{...picker.getInputProps('start')}
|
|
109
|
+
aria-label={startLabel}
|
|
110
|
+
disabled={disabled}
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
<span className="ndp-range__separator" aria-hidden="true">
|
|
114
|
+
→
|
|
115
|
+
</span>
|
|
116
|
+
<div
|
|
117
|
+
className="ndp-input"
|
|
118
|
+
data-invalid={invalid('end') || undefined}
|
|
119
|
+
data-disabled={disabled || undefined}
|
|
120
|
+
>
|
|
121
|
+
<input
|
|
122
|
+
className="ndp-input__control"
|
|
123
|
+
{...picker.getInputProps('end')}
|
|
124
|
+
aria-label={endLabel}
|
|
125
|
+
disabled={disabled}
|
|
126
|
+
/>
|
|
127
|
+
{!state.inline && (
|
|
128
|
+
<span className="ndp-input__actions">
|
|
129
|
+
<button
|
|
130
|
+
className="ndp-icon-button"
|
|
131
|
+
disabled={disabled}
|
|
132
|
+
{...picker.getToggleProps()}
|
|
133
|
+
>
|
|
134
|
+
<Icon name="calendar" />
|
|
135
|
+
</button>
|
|
136
|
+
</span>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
{state.error && (
|
|
141
|
+
<p id={picker.ids.hint} className="ndp-hint" data-error aria-live="polite">
|
|
142
|
+
{state.error.message}
|
|
143
|
+
</p>
|
|
144
|
+
)}
|
|
145
|
+
{startName !== undefined && (
|
|
146
|
+
<input
|
|
147
|
+
type="hidden"
|
|
148
|
+
name={startName}
|
|
149
|
+
value={state.value.start}
|
|
150
|
+
disabled={disabled}
|
|
151
|
+
/>
|
|
152
|
+
)}
|
|
153
|
+
{endName !== undefined && (
|
|
154
|
+
<input type="hidden" name={endName} value={state.value.end} disabled={disabled} />
|
|
155
|
+
)}
|
|
156
|
+
{state.open && (
|
|
157
|
+
<div
|
|
158
|
+
ref={panel}
|
|
159
|
+
className="ndp-panel"
|
|
160
|
+
popover={state.inline ? undefined : 'manual'}
|
|
161
|
+
data-layout={state.inline ? 'inline' : undefined}
|
|
162
|
+
style={
|
|
163
|
+
state.inline
|
|
164
|
+
? ({ '--ndp-panel-width': `${String(panelWidth)}px` } as CSSProperties)
|
|
165
|
+
: undefined
|
|
166
|
+
}
|
|
167
|
+
data-ndp-theme={theme}
|
|
168
|
+
{...picker.getPanelProps()}
|
|
169
|
+
>
|
|
170
|
+
<PanelBody
|
|
171
|
+
presets={state.presets}
|
|
172
|
+
label={labels.labels.presets}
|
|
173
|
+
onPreset={(id) => {
|
|
174
|
+
if (store.actions.applyPreset(id) && !store.getState().open) {
|
|
175
|
+
restoreFocus(start.current);
|
|
176
|
+
}
|
|
177
|
+
}}
|
|
178
|
+
>
|
|
179
|
+
<CalendarBody bindings={calendar} />
|
|
180
|
+
</PanelBody>
|
|
181
|
+
{(state.canSwitchCalendar || fiscalYearView) && (
|
|
182
|
+
<div className="ndp-panel__footer">
|
|
183
|
+
{state.canSwitchCalendar ? (
|
|
184
|
+
<div
|
|
185
|
+
className="ndp-segment"
|
|
186
|
+
role="group"
|
|
187
|
+
aria-label={labels.labels.calendar}
|
|
188
|
+
>
|
|
189
|
+
{(['BS', 'AD'] as const).map((option) => (
|
|
190
|
+
<button
|
|
191
|
+
key={option}
|
|
192
|
+
className="ndp-segment__item"
|
|
193
|
+
{...picker.getCalendarOptionProps(option)}
|
|
194
|
+
>
|
|
195
|
+
{labels.calendarNames[option]}
|
|
196
|
+
</button>
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
) : (
|
|
200
|
+
<span />
|
|
201
|
+
)}
|
|
202
|
+
{fiscalYearView && (
|
|
203
|
+
<button
|
|
204
|
+
type="button"
|
|
205
|
+
className="ndp-button"
|
|
206
|
+
aria-pressed={state.calendarState.view === 'fiscal'}
|
|
207
|
+
onClick={() => {
|
|
208
|
+
store.calendar.actions.setView(
|
|
209
|
+
state.calendarState.view === 'fiscal' ? 'day' : 'fiscal',
|
|
210
|
+
);
|
|
211
|
+
calendar.focusGrid();
|
|
212
|
+
}}
|
|
213
|
+
>
|
|
214
|
+
{labels.labels.fiscalYear}
|
|
215
|
+
</button>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
</div>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
formatDate,
|
|
4
|
+
fromIsoDate,
|
|
5
|
+
fromIsoTimestamp,
|
|
6
|
+
toCalendar,
|
|
7
|
+
type CalendarSystem,
|
|
8
|
+
type DateFormatPattern,
|
|
9
|
+
type LocaleCode,
|
|
10
|
+
} from '@nepdatejs/core';
|
|
11
|
+
|
|
12
|
+
export interface DateTextProps {
|
|
13
|
+
/** An AD date or timestamp from the server ("2026-09-26" or an ISO timestamp). */
|
|
14
|
+
value: string | null | undefined;
|
|
15
|
+
/** Which calendar to show first. Default 'BS'. */
|
|
16
|
+
calendar?: CalendarSystem | undefined;
|
|
17
|
+
/** Also show the day in the other calendar, muted. Default true. */
|
|
18
|
+
withEquivalent?: boolean | undefined;
|
|
19
|
+
format?: DateFormatPattern | undefined;
|
|
20
|
+
locale?: LocaleCode | undefined;
|
|
21
|
+
fallback?: string | undefined;
|
|
22
|
+
className?: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A server date shown in BS (or AD), with the other calendar alongside:
|
|
27
|
+
* "10 Ashwin 2083 · 26 Sep 2026". No hooks, so it works in server components.
|
|
28
|
+
*/
|
|
29
|
+
export function DateText({
|
|
30
|
+
value,
|
|
31
|
+
calendar = 'BS',
|
|
32
|
+
withEquivalent = true,
|
|
33
|
+
format = 'long',
|
|
34
|
+
locale = 'en',
|
|
35
|
+
fallback = '—',
|
|
36
|
+
className,
|
|
37
|
+
}: DateTextProps): ReactElement {
|
|
38
|
+
const date = fromIsoDate(value);
|
|
39
|
+
if (!date) {
|
|
40
|
+
return <span className={className}>{fallback}</span>;
|
|
41
|
+
}
|
|
42
|
+
const other: CalendarSystem = calendar === 'BS' ? 'AD' : 'BS';
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<time dateTime={formatDate(date, 'Y-m-d')} className={className}>
|
|
46
|
+
{formatDate(toCalendar(date, calendar), format, locale)}
|
|
47
|
+
{withEquivalent && (
|
|
48
|
+
<span className="ndp-muted" style={{ color: 'var(--ndp-text-muted, #64748b)' }}>
|
|
49
|
+
{' · '}
|
|
50
|
+
{formatDate(toCalendar(date, other), other === 'AD' ? 'j M Y' : format, locale)}
|
|
51
|
+
</span>
|
|
52
|
+
)}
|
|
53
|
+
</time>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A server date as short text: "12 Ashwin 2083", keeping the time of day when
|
|
59
|
+
* the value has one. A timestamp with a zone is read in the configured zone
|
|
60
|
+
* (Nepal by default). Anything that is not a date is returned unchanged.
|
|
61
|
+
*/
|
|
62
|
+
export function shortDate(value: string | null | undefined): string | null {
|
|
63
|
+
if (!value) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const instant = fromIsoTimestamp(value);
|
|
67
|
+
const date = instant?.date ?? fromIsoDate(value);
|
|
68
|
+
if (!date) {
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
const time = instant?.time ?? /[T ](\d{2}:\d{2})/.exec(value)?.[1];
|
|
72
|
+
|
|
73
|
+
return `${formatDate(toCalendar(date, 'BS'), 'j F Y')}${time ? `, ${time}` : ''}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A server date as documents print it, in both calendars: "31 Shrawan 2083 (16 Aug 2026)". */
|
|
77
|
+
export function bothDates(value: string | null | undefined): string {
|
|
78
|
+
const date = fromIsoDate(value);
|
|
79
|
+
|
|
80
|
+
return date
|
|
81
|
+
? `${formatDate(toCalendar(date, 'BS'), 'j F Y')} (${formatDate(date, 'j M Y')})`
|
|
82
|
+
: (value ?? '');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A server date for a table cell or list row, with the AD day on hover. */
|
|
86
|
+
export function ShortDate({
|
|
87
|
+
value,
|
|
88
|
+
fallback = '—',
|
|
89
|
+
className,
|
|
90
|
+
}: {
|
|
91
|
+
value: string | null | undefined;
|
|
92
|
+
fallback?: string | undefined;
|
|
93
|
+
className?: string | undefined;
|
|
94
|
+
}): ReactElement {
|
|
95
|
+
const text = shortDate(value);
|
|
96
|
+
const date = fromIsoDate(value);
|
|
97
|
+
if (!text) {
|
|
98
|
+
return <span className={className}>{fallback}</span>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<time
|
|
103
|
+
dateTime={value ?? undefined}
|
|
104
|
+
title={date ? formatDate(date, 'j M Y') : undefined}
|
|
105
|
+
className={className}
|
|
106
|
+
>
|
|
107
|
+
{text}
|
|
108
|
+
</time>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { useRef, type CSSProperties, type ReactElement } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
getLocale,
|
|
4
|
+
type CalendarDate,
|
|
5
|
+
type CalendarSystem,
|
|
6
|
+
type LocaleCode,
|
|
7
|
+
type Weekday,
|
|
8
|
+
} from '@nepdatejs/core';
|
|
9
|
+
import type { HolidayInput, HolidayLookup } from '@nepdatejs/calendar';
|
|
10
|
+
import type { DateTimeDetail, SinglePreset } from '@nepdatejs/datepicker';
|
|
11
|
+
import { CalendarBody } from './calendar-body';
|
|
12
|
+
import { popupProps, useDateTimePicker } from './hooks';
|
|
13
|
+
import { Icon } from './icons';
|
|
14
|
+
import { usePanel } from './panel';
|
|
15
|
+
import type { PanelLayout } from '@nepdatejs/dom';
|
|
16
|
+
import {
|
|
17
|
+
PANEL_WIDTH,
|
|
18
|
+
PANEL_WIDTH_WITH_PRESETS,
|
|
19
|
+
PanelBody,
|
|
20
|
+
TIME_COLUMNS_WIDTH,
|
|
21
|
+
} from './presets';
|
|
22
|
+
import { TimeColumns } from './time-columns';
|
|
23
|
+
|
|
24
|
+
export interface DateTimePickerProps {
|
|
25
|
+
/** ISO timestamp with offset ("2026-10-18T14:30:00+05:45") or "". */
|
|
26
|
+
value?: string | undefined;
|
|
27
|
+
defaultValue?: string | undefined;
|
|
28
|
+
onChange?: ((value: string, detail: DateTimeDetail | null) => void) | undefined;
|
|
29
|
+
/** The zone the picker shows and the value's offset. Default Asia/Kathmandu (core config). */
|
|
30
|
+
timeZone?: string | undefined;
|
|
31
|
+
hour12?: boolean | undefined;
|
|
32
|
+
/** Minutes in the column (1–30). Default 5. */
|
|
33
|
+
minuteStep?: number | undefined;
|
|
34
|
+
/** Time used when a day is picked first. Default "00:00". */
|
|
35
|
+
defaultTime?: string | undefined;
|
|
36
|
+
calendar?: CalendarSystem | undefined;
|
|
37
|
+
defaultCalendar?: CalendarSystem | undefined;
|
|
38
|
+
/** Earliest and latest days, AD "YYYY-MM-DD". */
|
|
39
|
+
min?: string | undefined;
|
|
40
|
+
max?: string | undefined;
|
|
41
|
+
isDateDisabled?: ((date: CalendarDate) => boolean) | undefined;
|
|
42
|
+
holidays?: readonly HolidayInput[] | HolidayLookup | undefined;
|
|
43
|
+
presets?: readonly SinglePreset[] | undefined;
|
|
44
|
+
locale?: LocaleCode | undefined;
|
|
45
|
+
weekStartsOn?: Weekday | undefined;
|
|
46
|
+
today?: CalendarDate | undefined;
|
|
47
|
+
/** Submit the ISO value with a plain form. */
|
|
48
|
+
name?: string | undefined;
|
|
49
|
+
id?: string | undefined;
|
|
50
|
+
required?: boolean | undefined;
|
|
51
|
+
disabled?: boolean | undefined;
|
|
52
|
+
/** Add the dashes of YYYY-MM-DD while the user types digits. Default true. */
|
|
53
|
+
mask?: boolean | undefined;
|
|
54
|
+
/** Show the calendar in the page, always open, instead of in a popover. */
|
|
55
|
+
inline?: boolean | undefined;
|
|
56
|
+
/** 'sheet' opens a bottom sheet; 'popover' sits beside the field; 'auto' (default) uses the sheet below 480px. */
|
|
57
|
+
panelLayout?: PanelLayout | undefined;
|
|
58
|
+
theme?: 'light' | 'dark' | 'auto' | undefined;
|
|
59
|
+
className?: string | undefined;
|
|
60
|
+
/** Accessible names for the two inputs. */
|
|
61
|
+
'aria-label'?: string | undefined;
|
|
62
|
+
timeLabel?: string | undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A BS/AD date and time field. The value is one instant, with the zone's offset. */
|
|
66
|
+
export function DateTimePicker({
|
|
67
|
+
name,
|
|
68
|
+
required,
|
|
69
|
+
disabled = false,
|
|
70
|
+
panelLayout,
|
|
71
|
+
theme,
|
|
72
|
+
className,
|
|
73
|
+
'aria-label': ariaLabel,
|
|
74
|
+
timeLabel,
|
|
75
|
+
...options
|
|
76
|
+
}: DateTimePickerProps): ReactElement {
|
|
77
|
+
const picker = useDateTimePicker(options);
|
|
78
|
+
const { state, store, calendar, ids } = picker;
|
|
79
|
+
const labels = getLocale(options.locale);
|
|
80
|
+
const field = useRef<HTMLDivElement | null>(null);
|
|
81
|
+
const row = useRef<HTMLDivElement | null>(null);
|
|
82
|
+
const panel = useRef<HTMLDivElement | null>(null);
|
|
83
|
+
const dateInput = useRef<HTMLInputElement | null>(null);
|
|
84
|
+
|
|
85
|
+
const panelWidth =
|
|
86
|
+
(state.presets.length > 0 ? PANEL_WIDTH_WITH_PRESETS : PANEL_WIDTH) +
|
|
87
|
+
TIME_COLUMNS_WIDTH;
|
|
88
|
+
usePanel(
|
|
89
|
+
state.open && !state.inline,
|
|
90
|
+
{ anchor: row, panel, field, returnFocus: dateInput },
|
|
91
|
+
() => {
|
|
92
|
+
store.actions.close();
|
|
93
|
+
},
|
|
94
|
+
calendar.focusGrid,
|
|
95
|
+
panelWidth,
|
|
96
|
+
panelLayout,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const hint = state.error?.message ?? state.equivalentText;
|
|
100
|
+
const invalid = (which: 'date' | 'time'): boolean => state.error?.field === which;
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div
|
|
104
|
+
ref={field}
|
|
105
|
+
className={className ? `ndp-field ${className}` : 'ndp-field'}
|
|
106
|
+
data-ndp-theme={theme}
|
|
107
|
+
>
|
|
108
|
+
<div ref={row} className="ndp-datetime">
|
|
109
|
+
<div
|
|
110
|
+
className="ndp-input"
|
|
111
|
+
data-invalid={invalid('date') || undefined}
|
|
112
|
+
data-disabled={disabled || undefined}
|
|
113
|
+
>
|
|
114
|
+
<input
|
|
115
|
+
ref={dateInput}
|
|
116
|
+
id={ids.date}
|
|
117
|
+
className="ndp-input__control"
|
|
118
|
+
{...popupProps(state)}
|
|
119
|
+
type="text"
|
|
120
|
+
inputMode="numeric"
|
|
121
|
+
autoComplete="off"
|
|
122
|
+
value={state.dateText}
|
|
123
|
+
placeholder={state.placeholder}
|
|
124
|
+
aria-label={ariaLabel}
|
|
125
|
+
aria-controls={ids.panel}
|
|
126
|
+
aria-invalid={invalid('date') || undefined}
|
|
127
|
+
aria-describedby={hint ? ids.hint : undefined}
|
|
128
|
+
required={required}
|
|
129
|
+
disabled={disabled}
|
|
130
|
+
onChange={(event) => {
|
|
131
|
+
store.actions.typeDate(event.target.value);
|
|
132
|
+
}}
|
|
133
|
+
onBlur={() => {
|
|
134
|
+
store.actions.blurDate();
|
|
135
|
+
}}
|
|
136
|
+
onKeyDown={(event) => {
|
|
137
|
+
if (event.key === 'ArrowDown' && (event.altKey || state.dateText === '')) {
|
|
138
|
+
event.preventDefault();
|
|
139
|
+
store.actions.open();
|
|
140
|
+
}
|
|
141
|
+
}}
|
|
142
|
+
/>
|
|
143
|
+
{!state.inline && (
|
|
144
|
+
<span className="ndp-input__actions">
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
className="ndp-icon-button"
|
|
148
|
+
disabled={disabled}
|
|
149
|
+
aria-label={labels.labels.openCalendar}
|
|
150
|
+
aria-expanded={state.open}
|
|
151
|
+
aria-controls={ids.panel}
|
|
152
|
+
onClick={() => {
|
|
153
|
+
store.actions.toggle();
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
<Icon name="calendar" />
|
|
157
|
+
</button>
|
|
158
|
+
</span>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
<div
|
|
162
|
+
className="ndp-input ndp-input--time"
|
|
163
|
+
data-invalid={invalid('time') || undefined}
|
|
164
|
+
data-disabled={disabled || undefined}
|
|
165
|
+
>
|
|
166
|
+
<input
|
|
167
|
+
id={ids.time}
|
|
168
|
+
className="ndp-input__control"
|
|
169
|
+
type="text"
|
|
170
|
+
inputMode="text"
|
|
171
|
+
autoComplete="off"
|
|
172
|
+
value={state.timeText}
|
|
173
|
+
placeholder={state.timePlaceholder}
|
|
174
|
+
aria-label={timeLabel ?? labels.labels.time}
|
|
175
|
+
aria-invalid={invalid('time') || undefined}
|
|
176
|
+
aria-describedby={hint ? ids.hint : undefined}
|
|
177
|
+
disabled={disabled}
|
|
178
|
+
onChange={(event) => {
|
|
179
|
+
store.actions.typeTime(event.target.value);
|
|
180
|
+
}}
|
|
181
|
+
onBlur={() => {
|
|
182
|
+
store.actions.blurTime();
|
|
183
|
+
}}
|
|
184
|
+
/>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
{hint !== null && (
|
|
188
|
+
<p
|
|
189
|
+
id={ids.hint}
|
|
190
|
+
className="ndp-hint"
|
|
191
|
+
data-error={state.error ? true : undefined}
|
|
192
|
+
aria-live="polite"
|
|
193
|
+
>
|
|
194
|
+
{hint}
|
|
195
|
+
</p>
|
|
196
|
+
)}
|
|
197
|
+
{name !== undefined && (
|
|
198
|
+
<input type="hidden" name={name} value={state.value} disabled={disabled} />
|
|
199
|
+
)}
|
|
200
|
+
{state.open && (
|
|
201
|
+
<div
|
|
202
|
+
ref={panel}
|
|
203
|
+
id={ids.panel}
|
|
204
|
+
className="ndp-panel"
|
|
205
|
+
popover={state.inline ? undefined : 'manual'}
|
|
206
|
+
data-layout={state.inline ? 'inline' : undefined}
|
|
207
|
+
style={
|
|
208
|
+
state.inline
|
|
209
|
+
? ({ '--ndp-panel-width': `${String(panelWidth)}px` } as CSSProperties)
|
|
210
|
+
: undefined
|
|
211
|
+
}
|
|
212
|
+
role={state.inline ? 'group' : 'dialog'}
|
|
213
|
+
aria-label={labels.labels.chooseDate}
|
|
214
|
+
data-ndp-theme={theme}
|
|
215
|
+
>
|
|
216
|
+
<PanelBody
|
|
217
|
+
presets={state.presets}
|
|
218
|
+
label={labels.labels.presets}
|
|
219
|
+
onPreset={(presetId) => {
|
|
220
|
+
store.actions.applyPreset(presetId);
|
|
221
|
+
}}
|
|
222
|
+
>
|
|
223
|
+
<CalendarBody bindings={calendar} />
|
|
224
|
+
<TimeColumns
|
|
225
|
+
hours={state.hours}
|
|
226
|
+
minutes={state.minutes}
|
|
227
|
+
locale={options.locale}
|
|
228
|
+
onHour={(hour) => {
|
|
229
|
+
store.actions.selectHour(hour);
|
|
230
|
+
}}
|
|
231
|
+
onMinute={(minute) => {
|
|
232
|
+
store.actions.selectMinute(minute);
|
|
233
|
+
}}
|
|
234
|
+
/>
|
|
235
|
+
</PanelBody>
|
|
236
|
+
<div className="ndp-panel__footer">
|
|
237
|
+
{state.canSwitchCalendar ? (
|
|
238
|
+
<div
|
|
239
|
+
className="ndp-segment"
|
|
240
|
+
role="group"
|
|
241
|
+
aria-label={labels.labels.calendar}
|
|
242
|
+
>
|
|
243
|
+
{(['BS', 'AD'] as const).map((option) => (
|
|
244
|
+
<button
|
|
245
|
+
key={option}
|
|
246
|
+
type="button"
|
|
247
|
+
className="ndp-segment__item"
|
|
248
|
+
aria-pressed={state.calendar === option}
|
|
249
|
+
onClick={() => {
|
|
250
|
+
store.actions.switchCalendar(option);
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
{labels.calendarNames[option]}
|
|
254
|
+
</button>
|
|
255
|
+
))}
|
|
256
|
+
</div>
|
|
257
|
+
) : (
|
|
258
|
+
<span />
|
|
259
|
+
)}
|
|
260
|
+
<span>
|
|
261
|
+
<button
|
|
262
|
+
type="button"
|
|
263
|
+
className="ndp-button"
|
|
264
|
+
onClick={() => {
|
|
265
|
+
store.actions.selectNow();
|
|
266
|
+
}}
|
|
267
|
+
>
|
|
268
|
+
{labels.labels.now}
|
|
269
|
+
</button>
|
|
270
|
+
<button
|
|
271
|
+
type="button"
|
|
272
|
+
className="ndp-button"
|
|
273
|
+
data-variant="primary"
|
|
274
|
+
onClick={() => {
|
|
275
|
+
store.actions.close();
|
|
276
|
+
dateInput.current?.focus();
|
|
277
|
+
}}
|
|
278
|
+
>
|
|
279
|
+
{labels.labels.done}
|
|
280
|
+
</button>
|
|
281
|
+
</span>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
)}
|
|
285
|
+
</div>
|
|
286
|
+
);
|
|
287
|
+
}
|
package/src/headless.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** @nepdatejs/react/headless: hooks only; no markup, no CSS. */
|
|
2
|
+
export {
|
|
3
|
+
useCalendar,
|
|
4
|
+
useDatePicker,
|
|
5
|
+
useDateRangePicker,
|
|
6
|
+
useDateTimePicker,
|
|
7
|
+
useMultiDatePicker,
|
|
8
|
+
type DatePickerBindings,
|
|
9
|
+
type DateTimePickerBindings,
|
|
10
|
+
type DateRangePickerBindings,
|
|
11
|
+
type MultiDatePickerBindings,
|
|
12
|
+
type InputProps,
|
|
13
|
+
type PanelProps,
|
|
14
|
+
} from './hooks';
|
|
15
|
+
export {
|
|
16
|
+
useCalendarBindings,
|
|
17
|
+
type ButtonProps,
|
|
18
|
+
type CalendarBindings,
|
|
19
|
+
type GridProps,
|
|
20
|
+
} from './bindings';
|
|
21
|
+
export { deepEqual, useStore } from './use-store';
|