@koslibs/components 1.15.0 → 1.17.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 (46) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/components/Chips/chips.d.ts +2 -0
  3. package/dist/components/Chips/chips.js +48 -0
  4. package/dist/components/Chips/hooks/use-chips-state.d.ts +7 -0
  5. package/dist/components/Chips/hooks/use-chips-state.js +56 -0
  6. package/dist/components/Chips/index.d.ts +2 -0
  7. package/dist/components/Chips/index.js +1 -0
  8. package/dist/components/Chips/index.module.js +9 -0
  9. package/dist/components/Chips/index_module.css +99 -0
  10. package/dist/components/Chips/types.d.ts +86 -0
  11. package/dist/components/Chips/types.js +0 -0
  12. package/dist/components/Chips/utils/normalize-value.d.ts +2 -0
  13. package/dist/components/Chips/utils/normalize-value.js +9 -0
  14. package/dist/components/Chips/vars.css +19 -0
  15. package/dist/components/Datepicker/components/calendar-month.d.ts +8 -0
  16. package/dist/components/Datepicker/components/calendar-month.js +68 -0
  17. package/dist/components/Datepicker/components/range-calendar.d.ts +5 -0
  18. package/dist/components/Datepicker/components/range-calendar.js +78 -0
  19. package/dist/components/Datepicker/datepicker-panel.d.ts +2 -0
  20. package/dist/components/Datepicker/datepicker-panel.js +187 -0
  21. package/dist/components/Datepicker/datepicker.d.ts +2 -0
  22. package/dist/components/Datepicker/datepicker.js +194 -0
  23. package/dist/components/Datepicker/hooks/use-datepicker-draft.d.ts +22 -0
  24. package/dist/components/Datepicker/hooks/use-datepicker-draft.js +105 -0
  25. package/dist/components/Datepicker/hooks/use-datepicker-state.d.ts +23 -0
  26. package/dist/components/Datepicker/hooks/use-datepicker-state.js +106 -0
  27. package/dist/components/Datepicker/hooks/use-datepicker-value.d.ts +8 -0
  28. package/dist/components/Datepicker/hooks/use-datepicker-value.js +31 -0
  29. package/dist/components/Datepicker/hooks/use-range-scroll.d.ts +15 -0
  30. package/dist/components/Datepicker/hooks/use-range-scroll.js +219 -0
  31. package/dist/components/Datepicker/index.d.ts +3 -0
  32. package/dist/components/Datepicker/index.js +2 -0
  33. package/dist/components/Datepicker/index.module.js +37 -0
  34. package/dist/components/Datepicker/index_module.css +251 -0
  35. package/dist/components/Datepicker/panel.module.js +39 -0
  36. package/dist/components/Datepicker/panel_module.css +375 -0
  37. package/dist/components/Datepicker/types.d.ts +133 -0
  38. package/dist/components/Datepicker/types.js +0 -0
  39. package/dist/components/Datepicker/utils/date-utils.d.ts +17 -0
  40. package/dist/components/Datepicker/utils/date-utils.js +80 -0
  41. package/dist/components/Datepicker/utils/range-scroll-geometry.d.ts +15 -0
  42. package/dist/components/Datepicker/utils/range-scroll-geometry.js +29 -0
  43. package/dist/components/Datepicker/vars.css +26 -0
  44. package/dist/index.d.ts +4 -0
  45. package/dist/index.js +2 -0
  46. package/package.json +2 -2
@@ -0,0 +1,187 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { CalendarIcon } from "@koslibs/icons/CalendarIcon";
3
+ import { ChevronLeftIcon } from "@koslibs/icons/ChevronLeftIcon";
4
+ import { ChevronRightIcon } from "@koslibs/icons/ChevronRightIcon";
5
+ import classnames from "classnames";
6
+ import { useId } from "react";
7
+ import { useDataTestId } from "../../hooks/use-data-test-id.js";
8
+ import { Button } from "../Button/index.js";
9
+ import { Input } from "../Input/index.js";
10
+ import { TypographyText } from "../Typography/text.js";
11
+ import { CalendarMonth, Weekdays } from "./components/calendar-month.js";
12
+ import { RangeCalendar } from "./components/range-calendar.js";
13
+ import { useDatepickerDraft } from "./hooks/use-datepicker-draft.js";
14
+ import { useDatepickerValue } from "./hooks/use-datepicker-value.js";
15
+ import { LAST_MONTH, monthLabel } from "./utils/date-utils.js";
16
+ import panel_module from "./panel.module.js";
17
+ function DatepickerPanel(props) {
18
+ const { mode = 'single', className, style, dataTestId, onCancel, id: panelId } = props;
19
+ const selection = useDatepickerValue(props);
20
+ const state = useDatepickerDraft(mode, selection.value);
21
+ const id = useDataTestId(dataTestId);
22
+ const titleId = useId();
23
+ const range = 'range' === mode;
24
+ let countLabel = range ? 'Choose dates' : 'Choose date';
25
+ if (range && state.draft.from) countLabel = 'Choose an end date';
26
+ if (state.count) countLabel = `${state.count} ${1 === state.count ? 'day' : 'days'}`;
27
+ function cancel() {
28
+ state.reset();
29
+ onCancel?.();
30
+ }
31
+ function apply(event) {
32
+ selection.commit(event, state.value);
33
+ }
34
+ const handleKeyDown = (event)=>{
35
+ if ('Escape' === event.key && !event.defaultPrevented) {
36
+ event.preventDefault();
37
+ event.stopPropagation();
38
+ cancel();
39
+ }
40
+ };
41
+ const handlePreviousMonth = ()=>state.navigate(state.month - 1);
42
+ const handleNextMonth = ()=>state.navigate(state.month + 1);
43
+ return /*#__PURE__*/ jsxs("div", {
44
+ id: panelId,
45
+ className: classnames(panel_module.panel, range && panel_module.range, className),
46
+ style: style,
47
+ role: "dialog",
48
+ "data-test-id": dataTestId,
49
+ onKeyDown: handleKeyDown,
50
+ children: [
51
+ /*#__PURE__*/ jsx(TypographyText, {
52
+ tag: "div",
53
+ size: 16,
54
+ weight: "semibold",
55
+ id: titleId,
56
+ className: panel_module.title,
57
+ children: range ? 'Choose dates' : 'Choose date'
58
+ }),
59
+ /*#__PURE__*/ jsx("div", {
60
+ className: panel_module.fields,
61
+ children: (range ? [
62
+ 'from',
63
+ 'to'
64
+ ] : [
65
+ 'from'
66
+ ]).map((field)=>{
67
+ const inputId = `${titleId}-${field}`;
68
+ const error = state.errors[field];
69
+ const errorMessage = error ? /*#__PURE__*/ jsx("span", {
70
+ id: `${inputId}-error`,
71
+ children: error
72
+ }) : void 0;
73
+ const inputLabel = range ? ({
74
+ from: 'From',
75
+ to: 'To'
76
+ })[field] : 'Date';
77
+ const handleChange = (_, { value })=>state.input(field, value);
78
+ return /*#__PURE__*/ jsx(Input, {
79
+ id: inputId,
80
+ block: true,
81
+ view: "secondary",
82
+ size: 40,
83
+ label: inputLabel,
84
+ placeholder: "Choose date",
85
+ value: state.draft[field],
86
+ onChange: handleChange,
87
+ error: errorMessage,
88
+ hint: errorMessage,
89
+ messageClassName: panel_module.inputError,
90
+ labelClassName: panel_module.inputLabel,
91
+ className: panel_module.input,
92
+ fieldClassName: panel_module.inputField,
93
+ rightAddon: /*#__PURE__*/ jsx(CalendarIcon, {
94
+ width: 16,
95
+ height: 16
96
+ }),
97
+ dataTestId: id(range ? field : 'date'),
98
+ autoComplete: "off",
99
+ maxLength: 10
100
+ }, field);
101
+ })
102
+ }),
103
+ /*#__PURE__*/ jsx("div", {
104
+ className: panel_module.body,
105
+ children: range ? /*#__PURE__*/ jsx(RangeCalendar, {
106
+ state: state,
107
+ dataTestId: dataTestId
108
+ }) : /*#__PURE__*/ jsxs("div", {
109
+ className: panel_module.singleCalendar,
110
+ "data-test-id": id('calendar'),
111
+ children: [
112
+ /*#__PURE__*/ jsxs("div", {
113
+ className: panel_module.navigation,
114
+ children: [
115
+ /*#__PURE__*/ jsx("button", {
116
+ type: "button",
117
+ className: panel_module.arrow,
118
+ title: "Previous month",
119
+ disabled: 0 === state.month,
120
+ onClick: handlePreviousMonth,
121
+ "data-test-id": id('previous'),
122
+ children: /*#__PURE__*/ jsx(ChevronLeftIcon, {
123
+ width: 16,
124
+ height: 16
125
+ })
126
+ }),
127
+ /*#__PURE__*/ jsx(TypographyText, {
128
+ size: 14,
129
+ weight: "semibold",
130
+ children: monthLabel(state.month)
131
+ }),
132
+ /*#__PURE__*/ jsx("button", {
133
+ type: "button",
134
+ className: panel_module.arrow,
135
+ title: "Next month",
136
+ disabled: state.month === LAST_MONTH,
137
+ onClick: handleNextMonth,
138
+ "data-test-id": id('next'),
139
+ children: /*#__PURE__*/ jsx(ChevronRightIcon, {
140
+ width: 16,
141
+ height: 16
142
+ })
143
+ })
144
+ ]
145
+ }),
146
+ /*#__PURE__*/ jsx(Weekdays, {}),
147
+ /*#__PURE__*/ jsx(CalendarMonth, {
148
+ month: state.month,
149
+ state: state,
150
+ range: false,
151
+ dataTestId: dataTestId
152
+ })
153
+ ]
154
+ })
155
+ }),
156
+ /*#__PURE__*/ jsxs("div", {
157
+ className: panel_module.footer,
158
+ children: [
159
+ /*#__PURE__*/ jsx(TypographyText, {
160
+ size: 12,
161
+ color: "secondary",
162
+ className: panel_module.count,
163
+ dataTestId: id('count'),
164
+ children: countLabel
165
+ }),
166
+ /*#__PURE__*/ jsx(Button, {
167
+ view: "text",
168
+ size: 40,
169
+ className: panel_module.cancel,
170
+ onClick: cancel,
171
+ dataTestId: id('cancel'),
172
+ children: "Cancel"
173
+ }),
174
+ /*#__PURE__*/ jsx(Button, {
175
+ view: "secondary",
176
+ size: 40,
177
+ disabled: !state.value,
178
+ onClick: apply,
179
+ dataTestId: id('apply'),
180
+ children: "Apply"
181
+ })
182
+ ]
183
+ })
184
+ ]
185
+ });
186
+ }
187
+ export { DatepickerPanel };
@@ -0,0 +1,2 @@
1
+ import type { DatepickerProps } from './types.js';
2
+ export declare const Datepicker: import("react").ForwardRefExoticComponent<DatepickerProps & import("react").RefAttributes<HTMLButtonElement>>;
@@ -0,0 +1,194 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { CalendarIcon } from "@koslibs/icons/CalendarIcon";
3
+ import { CrossIcon } from "@koslibs/icons/CrossIcon";
4
+ import classnames from "classnames";
5
+ import { forwardRef, useId, useImperativeHandle } from "react";
6
+ import { useDataTestId } from "../../hooks/use-data-test-id.js";
7
+ import { Button } from "../Button/index.js";
8
+ import { Popover } from "../Popover/index.js";
9
+ import { TypographyText } from "../Typography/text.js";
10
+ import { DatepickerPanel } from "./datepicker-panel.js";
11
+ import { useDatepickerState } from "./hooks/use-datepicker-state.js";
12
+ import { formatDate } from "./utils/date-utils.js";
13
+ import index_module from "./index.module.js";
14
+ const textSizes = {
15
+ 40: 13,
16
+ 48: 13,
17
+ 56: 14,
18
+ 64: 14,
19
+ 72: 16
20
+ };
21
+ const offset = [
22
+ 0,
23
+ 8
24
+ ];
25
+ const Datepicker = /*#__PURE__*/ forwardRef((props, ref)=>{
26
+ const { mode, value: _value, defaultValue: _defaultValue, onChange: _onChange, view = 'primary', size = 40, label, labelView = 'outer', hint, error, block = false, hasOutline = false, clear = false, disabled = false, readOnly = false, placeholder = 'range' === mode ? 'Choose dates' : 'Choose date', open: _open, defaultOpen: _defaultOpen, onOpenChange: _onOpenChange, onClear: _onClear, id, name, form, className, style, labelClassName, messageClassName, panelClassName, getPortalContainer, dataTestId, ...buttonProps } = props;
27
+ const generatedId = useId();
28
+ const triggerId = id ?? generatedId;
29
+ const labelId = `${triggerId}-label`;
30
+ const messageId = `${triggerId}-message`;
31
+ const panelId = `${triggerId}-panel`;
32
+ const getId = useDataTestId(dataTestId);
33
+ const state = useDatepickerState(props);
34
+ useImperativeHandle(ref, ()=>state.buttonRef.current, []);
35
+ const hasLabel = null != label && false !== label && '' !== label;
36
+ const innerLabel = hasLabel && 'inner' === labelView;
37
+ const message = (hint && 'boolean' != typeof error ? error : void 0) || hint;
38
+ let display = placeholder;
39
+ if ('string' == typeof state.value) display = formatDate(state.value);
40
+ else if (state.value) display = `${formatDate(state.value.from)} – ${formatDate(state.value.to)}`;
41
+ const panelProps = 'range' === props.mode ? {
42
+ mode: 'range',
43
+ value: 'object' == typeof state.value ? state.value : null
44
+ } : {
45
+ mode: 'single',
46
+ value: 'string' == typeof state.value ? state.value : null
47
+ };
48
+ const handleClearMouseDown = (event)=>event.preventDefault();
49
+ const handleClose = ()=>state.close();
50
+ const handleCancel = ()=>state.close(true);
51
+ const handleChange = (event, { value: nextValue })=>state.apply(event, nextValue);
52
+ return /*#__PURE__*/ jsxs("div", {
53
+ className: classnames(index_module.component, index_module[`size-${size}`], {
54
+ [index_module.secondary]: 'secondary' === view,
55
+ [index_module.block]: block,
56
+ [index_module.disabled]: disabled,
57
+ [index_module.readOnly]: readOnly,
58
+ [index_module.error]: Boolean(error),
59
+ [index_module.hasOutline]: hasOutline,
60
+ [index_module.open]: state.open,
61
+ [index_module.hasInnerLabel]: innerLabel,
62
+ [index_module.labelRaised]: state.raisedLabel
63
+ }, className),
64
+ style: style,
65
+ "data-test-id": getId('form-control'),
66
+ children: [
67
+ hasLabel && !innerLabel && /*#__PURE__*/ jsx("label", {
68
+ id: labelId,
69
+ htmlFor: triggerId,
70
+ className: classnames(index_module.label, labelClassName),
71
+ "data-test-id": getId('label'),
72
+ children: /*#__PURE__*/ jsx(TypographyText, {
73
+ size: 13,
74
+ weight: "medium",
75
+ children: label
76
+ })
77
+ }),
78
+ /*#__PURE__*/ jsxs("div", {
79
+ ref: state.setAnchor,
80
+ className: index_module.field,
81
+ "data-test-id": getId('field'),
82
+ onKeyDown: state.handleKeyDown,
83
+ children: [
84
+ /*#__PURE__*/ jsx("button", {
85
+ ...buttonProps,
86
+ ...state.triggerProps,
87
+ ref: state.buttonRef,
88
+ id: triggerId,
89
+ type: "button",
90
+ form: form,
91
+ disabled: disabled,
92
+ className: index_module.trigger,
93
+ "data-test-id": dataTestId,
94
+ children: /*#__PURE__*/ jsxs("span", {
95
+ className: index_module.valueColumn,
96
+ children: [
97
+ innerLabel && /*#__PURE__*/ jsx(TypographyText, {
98
+ id: labelId,
99
+ size: state.raisedLabel ? 11 : textSizes[size],
100
+ weight: "medium",
101
+ rowLimit: 1,
102
+ className: classnames(index_module.innerLabel, labelClassName),
103
+ dataTestId: getId('label'),
104
+ children: label
105
+ }),
106
+ /*#__PURE__*/ jsx(TypographyText, {
107
+ size: textSizes[size],
108
+ rowLimit: 1,
109
+ className: classnames(index_module.value, !state.value && index_module.placeholder),
110
+ dataTestId: getId('value'),
111
+ children: display
112
+ })
113
+ ]
114
+ })
115
+ }),
116
+ clear && state.value && !state.blocked && /*#__PURE__*/ jsx(Button, {
117
+ view: "text",
118
+ size: 32,
119
+ className: index_module.clear,
120
+ title: "Clear date",
121
+ onMouseDown: handleClearMouseDown,
122
+ onClick: state.clear,
123
+ dataTestId: getId('clear'),
124
+ children: /*#__PURE__*/ jsx(CrossIcon, {
125
+ width: 14,
126
+ height: 14
127
+ })
128
+ }),
129
+ /*#__PURE__*/ jsx(CalendarIcon, {
130
+ className: index_module.icon,
131
+ width: 16,
132
+ height: 16
133
+ })
134
+ ]
135
+ }),
136
+ message && /*#__PURE__*/ jsx(TypographyText, {
137
+ tag: "div",
138
+ size: 12,
139
+ id: messageId,
140
+ className: classnames(index_module.message, messageClassName),
141
+ dataTestId: getId('boolean' != typeof error && error ? 'error' : 'hint'),
142
+ children: message
143
+ }),
144
+ name && ('range' === mode ? /*#__PURE__*/ jsxs(Fragment, {
145
+ children: [
146
+ /*#__PURE__*/ jsx("input", {
147
+ type: "hidden",
148
+ name: `${name}.from`,
149
+ value: 'object' == typeof state.value ? state.value?.from ?? '' : '',
150
+ disabled: disabled,
151
+ form: form
152
+ }),
153
+ /*#__PURE__*/ jsx("input", {
154
+ type: "hidden",
155
+ name: `${name}.to`,
156
+ value: 'object' == typeof state.value ? state.value?.to ?? '' : '',
157
+ disabled: disabled,
158
+ form: form
159
+ })
160
+ ]
161
+ }) : /*#__PURE__*/ jsx("input", {
162
+ type: "hidden",
163
+ name: name,
164
+ value: 'string' == typeof state.value ? state.value : '',
165
+ disabled: disabled,
166
+ form: form
167
+ })),
168
+ /*#__PURE__*/ jsx(Popover, {
169
+ ref: state.popupRef,
170
+ open: state.open,
171
+ anchorElement: state.anchor,
172
+ offset: offset,
173
+ onClose: handleClose,
174
+ escapeKeyDown: false,
175
+ getPortalContainer: getPortalContainer,
176
+ className: classnames(index_module.popup, 'range' === state.mode && index_module.rangePopup),
177
+ dataTestId: getId('popup'),
178
+ children: /*#__PURE__*/ jsx("div", {
179
+ className: index_module.panelContainer,
180
+ children: /*#__PURE__*/ jsx(DatepickerPanel, {
181
+ id: panelId,
182
+ ...panelProps,
183
+ onChange: handleChange,
184
+ onCancel: handleCancel,
185
+ className: panelClassName,
186
+ dataTestId: getId('panel')
187
+ }, `${state.mode}-${state.session}`)
188
+ })
189
+ })
190
+ ]
191
+ });
192
+ });
193
+ Datepicker.displayName = 'Datepicker';
194
+ export { Datepicker };
@@ -0,0 +1,22 @@
1
+ import type { DatepickerRange } from '../types.js';
2
+ export declare function useDatepickerDraft(mode: 'single' | 'range', committed: string | DatepickerRange | null): {
3
+ draft: {
4
+ from: string;
5
+ to: string;
6
+ };
7
+ value: string | DatepickerRange | null;
8
+ errors: {
9
+ from: string | undefined;
10
+ to: string | undefined;
11
+ };
12
+ month: number;
13
+ navigationVersion: number;
14
+ preview: string | null;
15
+ navigate: (index: number) => void;
16
+ reset: () => void;
17
+ input: (field: "from" | "to", text: string) => void;
18
+ select: (date: string) => void;
19
+ count: number;
20
+ hover(date: string | null): void;
21
+ };
22
+ export type DatepickerDraft = ReturnType<typeof useDatepickerDraft>;
@@ -0,0 +1,105 @@
1
+ import { useEffect, useState } from "react";
2
+ import { clampMonth, countDays, getMonthIndex, normalizeValue, parseDate, today } from "../utils/date-utils.js";
3
+ function useDatepickerDraft(mode, committed) {
4
+ const from = 'string' == typeof committed ? committed : committed?.from ?? '';
5
+ const to = 'object' == typeof committed ? committed?.to ?? '' : '';
6
+ const [draft, setDraft] = useState({
7
+ from,
8
+ to
9
+ });
10
+ const [preview, setPreview] = useState(null);
11
+ const [month, setMonth] = useState(()=>clampMonth(getMonthIndex(from || today()), 'range' === mode));
12
+ const [touched, setTouched] = useState({
13
+ from: false,
14
+ to: false
15
+ });
16
+ const [navigationVersion, setNavigationVersion] = useState(0);
17
+ useEffect(()=>{
18
+ setDraft({
19
+ from,
20
+ to
21
+ });
22
+ setTouched({
23
+ from: false,
24
+ to: false
25
+ });
26
+ setPreview(null);
27
+ setMonth(clampMonth(getMonthIndex(from || today()), 'range' === mode));
28
+ setNavigationVersion((current)=>current + 1);
29
+ }, [
30
+ from,
31
+ to,
32
+ mode
33
+ ]);
34
+ const value = normalizeValue(mode, 'single' === mode ? draft.from : draft);
35
+ let toError;
36
+ if (!touched.to || draft.to && parseDate(draft.to)) {
37
+ if (parseDate(draft.from) && parseDate(draft.to) && draft.to < draft.from) toError = 'End date must not precede start date';
38
+ } else toError = 'Enter a valid date (YYYY-MM-DD)';
39
+ const errors = {
40
+ from: !touched.from || draft.from && parseDate(draft.from) ? void 0 : 'Enter a valid date (YYYY-MM-DD)',
41
+ to: toError
42
+ };
43
+ function navigate(index) {
44
+ setMonth(clampMonth(index, 'range' === mode));
45
+ setNavigationVersion((current)=>current + 1);
46
+ }
47
+ function reset() {
48
+ setDraft({
49
+ from,
50
+ to
51
+ });
52
+ setPreview(null);
53
+ setTouched({
54
+ from: false,
55
+ to: false
56
+ });
57
+ navigate(getMonthIndex(from || today()));
58
+ }
59
+ function input(field, text) {
60
+ setDraft((current)=>({
61
+ ...current,
62
+ [field]: text
63
+ }));
64
+ setTouched((current)=>({
65
+ ...current,
66
+ [field]: true
67
+ }));
68
+ setPreview(null);
69
+ if (parseDate(text)) navigate(getMonthIndex(text));
70
+ }
71
+ function select(date) {
72
+ 'single' === mode || !parseDate(draft.from) || value || date < draft.from ? setDraft({
73
+ from: date,
74
+ to: ''
75
+ }) : setDraft({
76
+ from: draft.from,
77
+ to: date
78
+ });
79
+ setTouched({
80
+ from: false,
81
+ to: false
82
+ });
83
+ setPreview(null);
84
+ }
85
+ let count = 0;
86
+ if ('string' == typeof value) count = 1;
87
+ else if (value) count = countDays(value.from, value.to);
88
+ return {
89
+ draft,
90
+ value,
91
+ errors,
92
+ month,
93
+ navigationVersion,
94
+ preview,
95
+ navigate,
96
+ reset,
97
+ input,
98
+ select,
99
+ count,
100
+ hover (date) {
101
+ setPreview('range' === mode && parseDate(draft.from) && !value && date && date >= draft.from ? date : null);
102
+ }
103
+ };
104
+ }
105
+ export { useDatepickerDraft };
@@ -0,0 +1,23 @@
1
+ import { type FocusEvent, type KeyboardEvent as ReactKeyboardEvent, type MouseEvent } from 'react';
2
+ import type { DatepickerProps, DatepickerRange } from '../types.js';
3
+ export declare function useDatepickerState(props: DatepickerProps): {
4
+ mode: "range" | "single";
5
+ value: string | DatepickerRange | null;
6
+ open: boolean;
7
+ blocked: boolean;
8
+ buttonRef: import("react").RefObject<HTMLButtonElement>;
9
+ popupRef: import("react").RefObject<HTMLDivElement>;
10
+ anchor: HTMLDivElement | null;
11
+ setAnchor: import("react").Dispatch<import("react").SetStateAction<HTMLDivElement | null>>;
12
+ session: number;
13
+ close: (restoreFocus?: boolean) => void;
14
+ apply: (event: MouseEvent<HTMLButtonElement>, next: string | DatepickerRange | null) => void;
15
+ raisedLabel: boolean;
16
+ handleKeyDown(event: ReactKeyboardEvent<HTMLElement>): void;
17
+ triggerProps: {
18
+ onClick(event: MouseEvent<HTMLButtonElement>): void;
19
+ onFocus(event: FocusEvent<HTMLButtonElement>): void;
20
+ onBlur(event: FocusEvent<HTMLButtonElement>): void;
21
+ };
22
+ clear(event: MouseEvent<HTMLButtonElement>): void;
23
+ };
@@ -0,0 +1,106 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { useDatepickerValue } from "./use-datepicker-value.js";
3
+ function useDatepickerState(props) {
4
+ const mode = props.mode ?? 'single';
5
+ const selection = useDatepickerValue(props);
6
+ const [localOpen, setLocalOpen] = useState(props.defaultOpen ?? false);
7
+ const [focused, setFocused] = useState(false);
8
+ const [session, setSession] = useState(0);
9
+ const [anchor, setAnchor] = useState(null);
10
+ const buttonRef = useRef(null);
11
+ const popupRef = useRef(null);
12
+ const value = selection.value;
13
+ const blocked = Boolean(props.disabled || props.readOnly);
14
+ const open = !blocked && (props.open ?? localOpen);
15
+ function setOpen(next) {
16
+ if (next && blocked || next === open) return;
17
+ if (void 0 === props.open) setLocalOpen(next);
18
+ props.onOpenChange?.(next);
19
+ }
20
+ function close(restoreFocus = false) {
21
+ setOpen(false);
22
+ setSession((current)=>current + 1);
23
+ if (restoreFocus) buttonRef.current?.focus();
24
+ }
25
+ function apply(event, next) {
26
+ if (blocked || !selection.commit(event, next)) return;
27
+ close(true);
28
+ }
29
+ useEffect(()=>{
30
+ if (!open) return;
31
+ const ownerDocument = buttonRef.current?.ownerDocument;
32
+ const escape = (event)=>{
33
+ if ('Escape' !== event.key || event.defaultPrevented) return;
34
+ event.preventDefault();
35
+ event.stopPropagation();
36
+ close(true);
37
+ };
38
+ ownerDocument?.addEventListener('keydown', escape);
39
+ return ()=>ownerDocument?.removeEventListener('keydown', escape);
40
+ });
41
+ const resetRef = useRef();
42
+ resetRef.current = ()=>{
43
+ selection.reset();
44
+ close();
45
+ };
46
+ useEffect(()=>{
47
+ const form = buttonRef.current?.form;
48
+ let active = true;
49
+ const reset = (event)=>{
50
+ queueMicrotask(()=>{
51
+ if (active && !event.defaultPrevented) resetRef.current?.();
52
+ });
53
+ };
54
+ form?.addEventListener('reset', reset);
55
+ return ()=>{
56
+ active = false;
57
+ form?.removeEventListener('reset', reset);
58
+ };
59
+ }, [
60
+ props.form
61
+ ]);
62
+ return {
63
+ mode,
64
+ value,
65
+ open,
66
+ blocked,
67
+ buttonRef,
68
+ popupRef,
69
+ anchor,
70
+ setAnchor,
71
+ session,
72
+ close,
73
+ apply,
74
+ raisedLabel: Boolean(value || open || focused && !blocked),
75
+ handleKeyDown (event) {
76
+ if ('Escape' !== event.key || event.defaultPrevented || !open) return;
77
+ event.preventDefault();
78
+ event.stopPropagation();
79
+ close(true);
80
+ },
81
+ triggerProps: {
82
+ onClick (event) {
83
+ props.onClick?.(event);
84
+ if (event.defaultPrevented || blocked) return;
85
+ if (open) close();
86
+ else setOpen(true);
87
+ },
88
+ onFocus (event) {
89
+ setFocused(true);
90
+ props.onFocus?.(event);
91
+ },
92
+ onBlur (event) {
93
+ setFocused(false);
94
+ props.onBlur?.(event);
95
+ }
96
+ },
97
+ clear (event) {
98
+ if (blocked) return;
99
+ selection.clear();
100
+ close();
101
+ props.onClear?.(event);
102
+ buttonRef.current?.focus();
103
+ }
104
+ };
105
+ }
106
+ export { useDatepickerState };
@@ -0,0 +1,8 @@
1
+ import { type MouseEvent } from 'react';
2
+ import type { DatepickerRange, DatepickerValueProps } from '../types.js';
3
+ export declare function useDatepickerValue(props: DatepickerValueProps): {
4
+ value: string | DatepickerRange | null;
5
+ commit(event: MouseEvent<HTMLButtonElement>, next: string | DatepickerRange | null): boolean;
6
+ clear(): void;
7
+ reset(): void;
8
+ };