@kenos-ui/react-datepicker 0.3.1 → 0.3.3

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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # @kenos-ui/react-datepicker
2
2
 
3
+ ## 0.3.3
4
+
5
+ ### Patch Changes
6
+
7
+ - docs: major refresh of README.md and root documentation
8
+
9
+ - Update package README to accurately reflect current API (unified `DatePicker.Root` with `mode`, first-class `<DatePicker.Input>`, full composition)
10
+ - Add strong examples for segmented input, range with dual inputs + hover preview, multiple selection, and custom rendering
11
+ - Align with brand voice and recent announcement content
12
+ - Update root README with correct features and examples
13
+
14
+ ## 0.3.2
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies [aaa8a57]
19
+ - @kenos-ui/utils@0.0.1
20
+
3
21
  ## 0.3.1
4
22
 
5
23
  ### Patch Changes
package/README.md CHANGED
@@ -1,16 +1,14 @@
1
1
  # @kenos-ui/react-datepicker
2
2
 
3
- Kenos UI — headless date & scheduling primitives for React. Built on top of the native `Intl` API, timescape and Floating UI — with full keyboard navigation and WAI-ARIA compliance.
3
+ Headless date & scheduling primitives for React.
4
4
 
5
- ## Features
5
+ Built on native `Intl`, [timescape](https://github.com/dan-lee/timescape) (segmented inputs), and Floating UI. Full keyboard navigation, WAI-ARIA patterns, and a focus on real-world composition.
6
6
 
7
- - **Calendar** WAI-ARIA grid pattern with roving focus and full keyboard navigation
8
- - **Date Picker** Input + popover calendar, semantic HTML only
9
- - **Date Range Picker** Dual-endpoint selection with live hover preview and optional presets
10
- - **Date Field** Segmented spinbutton input (day/month/year), no calendar required
11
- - Locale-aware: respects week start day, date format order, and RTL scripts
12
- - Unstyled — bring your own CSS
13
- - React 19+, TypeScript-first
7
+ - Single, range, and multiple selection with one API (`mode`)
8
+ - Outstanding segmented date input (`<DatePicker.Input />`)
9
+ - Fully unstyledbring your own CSS or Tailwind
10
+ - Excellent keyboard + screen reader support
11
+ - React 19+ and TypeScript-first
14
12
 
15
13
  ## Installation
16
14
 
@@ -18,15 +16,21 @@ Kenos UI — headless date & scheduling primitives for React. Built on top of th
18
16
  npm install @kenos-ui/react-datepicker
19
17
  ```
20
18
 
21
- ## Quick Start
19
+ ## Quick start (with segmented input)
22
20
 
23
21
  ```tsx
24
22
  import { DatePicker } from "@kenos-ui/react-datepicker";
25
23
 
26
24
  function App() {
25
+ const [date, setDate] = useState<Date | null>(null);
26
+
27
27
  return (
28
- <DatePicker.Root>
29
- <DatePicker.Trigger>Pick a date</DatePicker.Trigger>
28
+ <DatePicker.Root value={date} onValueChange={setDate}>
29
+ <DatePicker.Label>Pick a date</DatePicker.Label>
30
+ <div>
31
+ <DatePicker.Input />
32
+ <DatePicker.Trigger>📅</DatePicker.Trigger>
33
+ </div>
30
34
  <DatePicker.Content>
31
35
  <DatePicker.Calendar />
32
36
  </DatePicker.Content>
@@ -35,125 +39,148 @@ function App() {
35
39
  }
36
40
  ```
37
41
 
38
- ## Components
42
+ The `<DatePicker.Input />` gives users a native-feeling segmented editor (month / day / year) that respects the locale's order and separators.
39
43
 
40
- ### Calendar
44
+ ## Date range
41
45
 
42
- Standalone month grid (composed via DatePicker.Root + calendar parts for full control, or the shorthand).
46
+ One of the strongest use cases:
43
47
 
44
48
  ```tsx
45
- import { DatePicker } from "@kenos-ui/react-datepicker";
49
+ import { DatePicker, type DateRange } from "@kenos-ui/react-datepicker";
46
50
 
47
- <DatePicker.Root locale="en-US" onValueChange={(date) => console.log(date)}>
48
- <DatePicker.ViewControl>
49
- <DatePicker.PrevTrigger />
50
- <DatePicker.ViewTrigger />
51
- <DatePicker.NextTrigger />
52
- </DatePicker.ViewControl>
53
- <DatePicker.View view="day">
54
- <DatePicker.Grid header={<DatePicker.WeekDays />}>
55
- {({ weeks }) => weeks.map((week, i) => (
56
- <tr key={i}>
57
- {week.map((d, j) => <DatePicker.Day key={j} date={d} />)}
58
- </tr>
59
- ))}
60
- </DatePicker.Grid>
61
- </DatePicker.View>
62
- </DatePicker.Root>
51
+ function TripDates() {
52
+ const [range, setRange] = useState<DateRange>({ start: null, end: null });
63
53
 
64
- // Or shorthand inside a Root:
65
- <DatePicker.Root>
66
- <DatePicker.Calendar />
67
- </DatePicker.Root>
54
+ return (
55
+ <DatePicker.Root mode="range" value={range} onValueChange={setRange}>
56
+ <DatePicker.Label>Trip dates</DatePicker.Label>
57
+ <div>
58
+ <DatePicker.Input index={0} />
59
+ <span>to</span>
60
+ <DatePicker.Input index={1} />
61
+ <DatePicker.Trigger>📅</DatePicker.Trigger>
62
+ </div>
63
+ <DatePicker.Content>
64
+ <DatePicker.Calendar />
65
+ </DatePicker.Content>
66
+ </DatePicker.Root>
67
+ );
68
+ }
68
69
  ```
69
70
 
70
- **Props on Root:** `value`, `defaultValue`, `onValueChange`, `locale`, `weekStartsOn`, `minDate`, `maxDate`, `disabled`, `mode`
71
-
72
- **Data attributes (on Day):** `[data-selected]`, `[data-today]`, `[data-outside-month]`, `[data-disabled]`, `[data-in-range]`, `[data-range-start]`, `[data-range-end]`
71
+ Range mode includes live hover preview between start and end.
73
72
 
74
- For the dedicated "Date Field" experience use a minimal Root + Input only.
75
-
76
- ---
77
-
78
- ### Date Picker
79
-
80
- Text input paired with a calendar popover.
73
+ ## Multiple dates
81
74
 
82
75
  ```tsx
83
- import { DatePicker } from "@kenos-ui/react-datepicker";
84
-
85
- <DatePicker.Root locale="en-US" onValueChange={(date) => console.log(date)}>
76
+ <DatePicker.Root mode="multiple" onValueChange={setDates}>
77
+ <DatePicker.Input />
86
78
  <DatePicker.Trigger />
87
79
  <DatePicker.Content>
88
80
  <DatePicker.Calendar />
89
81
  </DatePicker.Content>
90
- </DatePicker.Root>;
82
+ </DatePicker.Root>
91
83
  ```
92
84
 
93
- **Props:** `value`, `defaultValue`, `onValueChange`, `open`, `onOpenChange`, `locale`, `format`, `closeOnSelect`, `placement`
85
+ ## Calendar composition
94
86
 
95
- **Keyboard:** `Enter`/`Space` opens popover, `Escape` closes, arrow keys navigate calendar.
96
-
97
- ---
87
+ Use the convenient shorthand:
98
88
 
99
- ### Date Range Picker
89
+ ```tsx
90
+ <DatePicker.Root>
91
+ <DatePicker.Calendar />
92
+ </DatePicker.Root>
93
+ ```
100
94
 
101
- Selects a start and end date with optional night presets.
95
+ Or take full control:
102
96
 
103
97
  ```tsx
104
- import { DateRangePicker } from "@kenos-ui/react-datepicker";
105
-
106
- <DateRangePicker.Root locale="en-US" onValueChange={({ start, end }) => console.log(start, end)}>
107
- <DateRangePicker.Trigger />
108
- <DateRangePicker.Content>
109
- <DateRangePicker.Calendar />
110
- </DateRangePicker.Content>
111
- </DateRangePicker.Root>;
98
+ <DatePicker.Root>
99
+ <DatePicker.ViewControl>
100
+ <DatePicker.PrevTrigger />
101
+ <DatePicker.ViewTrigger />
102
+ <DatePicker.NextTrigger />
103
+ </DatePicker.ViewControl>
104
+
105
+ <DatePicker.View view="day">
106
+ <DatePicker.Grid header={<DatePicker.WeekDays />}>
107
+ {({ weeks }) =>
108
+ weeks.map((week, i) => (
109
+ <tr key={i}>
110
+ {week.map((d, j) => (
111
+ <DatePicker.Day key={j} date={d}>
112
+ {({ isSelected, isToday }) => (
113
+ // custom rendering
114
+ <span className={isSelected ? "selected" : ""}>
115
+ {d.getDate()}
116
+ </span>
117
+ )}
118
+ </DatePicker.Day>
119
+ ))}
120
+ </tr>
121
+ ))
122
+ }
123
+ </DatePicker.Grid>
124
+ </DatePicker.View>
125
+ </DatePicker.Root>
112
126
  ```
113
127
 
114
- **Props:** `value`, `defaultValue`, `onValueChange`, `minNights`, `maxNights`, `presets`, `locale`
128
+ You can also render month/year grids with `MonthGrid` / `YearGrid`.
115
129
 
116
- ---
130
+ ## Positioning & advanced Content
117
131
 
118
- ### Date Field
132
+ `<DatePicker.Content>` is powered by Floating UI and supports:
119
133
 
120
- Segmented spinbutton input no popover, locale-aware segment order.
134
+ - `side`, `align`, `sideOffset`, `alignOffset`
135
+ - `portal`
136
+ - `forceMount` (for enter/exit animations via `[data-state="open"]`)
137
+ - `avoidCollisions`
121
138
 
122
139
  ```tsx
123
- import { DateField } from "@kenos-ui/react-datepicker";
124
-
125
- <DateField.Root locale="en-US" onValueChange={(date) => console.log(date)}>
126
- <DateField.Segment segment="month" />
127
- <DateField.Literal>/</DateField.Literal>
128
- <DateField.Segment segment="day" />
129
- <DateField.Literal>/</DateField.Literal>
130
- <DateField.Segment segment="year" />
131
- </DateField.Root>;
140
+ <DatePicker.Content portal side="top" align="end" forceMount>
141
+ <DatePicker.Calendar />
142
+ </DatePicker.Content>
132
143
  ```
133
144
 
134
- **Props:** `value`, `defaultValue`, `onValueChange`, `locale`, `granularity`, `min`, `max`
145
+ ## Key props on Root
146
+
147
+ - `mode`: `"single" | "range" | "multiple"`
148
+ - `locale`, `weekStartsOn`
149
+ - `minDate`, `maxDate`, `disabled`
150
+ - `readOnly`
151
+ - `modal` — opt-in focus trap + `aria-modal` (default `false` — follows popup policy)
152
+ - `defaultOpen`, `closeOnSelect`
135
153
 
136
- **Keyboard:** `↑`/`↓` increments/decrements the focused segment, `Tab` advances to next segment.
154
+ See the full prop tables and more examples in the [project README](https://github.com/allysontsoares/kenos-ui/blob/main/README.md).
137
155
 
138
156
  ## Localization
139
157
 
140
- All components accept a `locale` prop compatible with `Intl.Locale`. Week start day, month/day/year order, and calendar display adapt automatically.
158
+ Everything is driven by `Intl.Locale`:
141
159
 
142
160
  ```tsx
143
- // Arabic RTL
144
- <DatePicker.Root locale="ar" />
145
-
146
- // Brazilian Portuguese
147
- <DatePicker.Root locale="pt-BR" />
148
-
149
- // Japanese
161
+ <DatePicker.Root locale="pt-BR" /> {/* dd/mm/yyyy, week starts Monday */}
162
+ <DatePicker.Root locale="ar" /> {/* RTL + Saturday start */}
150
163
  <DatePicker.Root locale="ja-JP" />
151
164
  ```
152
165
 
153
166
  ## Requirements
154
167
 
155
- - React `>=19.0.0`
156
- - Node.js `>=22`
168
+ - React 19
169
+ - Node 22
170
+
171
+ ## Exports
172
+
173
+ ```ts
174
+ import {
175
+ DatePicker,
176
+ useDatePickerContext,
177
+ // types
178
+ type DatePickerRootProps,
179
+ type DateRange,
180
+ type DayCellMeta,
181
+ // ...
182
+ } from "@kenos-ui/react-datepicker";
183
+ ```
157
184
 
158
185
  ## License
159
186
 
@@ -161,4 +188,4 @@ MIT
161
188
 
162
189
  ---
163
190
 
164
- *Kenos UI*
191
+ *Kenos UI* — https://github.com/allysontsoares/kenos-ui/tree/main/packages/datepicker
package/dist/index.cjs CHANGED
@@ -4,6 +4,7 @@ var React = require('react');
4
4
  var jsxRuntime = require('react/jsx-runtime');
5
5
  var react = require('timescape/react');
6
6
  var reactDom = require('react-dom');
7
+ var utils = require('@kenos-ui/utils');
7
8
  var reactDom$1 = require('@floating-ui/react-dom');
8
9
 
9
10
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
@@ -418,6 +419,7 @@ function resolveConfig(props) {
418
419
  mode: props.mode ?? "single",
419
420
  locale: props.locale ?? (typeof navigator !== "undefined" ? navigator.language : "en-US"),
420
421
  readOnly: props.readOnly ?? false,
422
+ modal: props.modal ?? false,
421
423
  closeOnSelect: props.closeOnSelect ?? (props.mode !== "range" && props.mode !== "multiple"),
422
424
  ...props.weekStartsOn !== void 0 && { weekStartsOn: props.weekStartsOn },
423
425
  ...props.minDate !== void 0 && { minDate: props.minDate },
@@ -443,7 +445,7 @@ function resolveInitialValue(props) {
443
445
  }
444
446
  function useDatePicker(props) {
445
447
  const uid = React.useId();
446
- const { mode, locale, weekStartsOn, minDate, maxDate, disabled, readOnly, closeOnSelect } = props;
448
+ const { mode, locale, weekStartsOn, minDate, maxDate, disabled, readOnly, closeOnSelect, modal } = props;
447
449
  const config = React.useMemo(
448
450
  () => resolveConfig({
449
451
  mode,
@@ -453,9 +455,10 @@ function useDatePicker(props) {
453
455
  maxDate,
454
456
  disabled,
455
457
  readOnly,
456
- closeOnSelect
458
+ closeOnSelect,
459
+ modal
457
460
  }),
458
- [mode, locale, weekStartsOn, minDate, maxDate, disabled, readOnly, closeOnSelect]
461
+ [mode, locale, weekStartsOn, minDate, maxDate, disabled, readOnly, closeOnSelect, modal]
459
462
  );
460
463
  const initialValue = resolveInitialValue(props);
461
464
  const [state, dispatch] = React.useReducer(
@@ -657,6 +660,7 @@ function Segments({
657
660
  }
658
661
  if (e.key === "Escape") {
659
662
  e.preventDefault();
663
+ e.stopPropagation();
660
664
  if (state.open) dispatch({ type: "CLOSE" });
661
665
  }
662
666
  }
@@ -807,76 +811,6 @@ function Trigger({ children, onClick, disabled, ...props }) {
807
811
  }
808
812
  );
809
813
  }
810
- function useClickOutside(refs, handler, enabled = true) {
811
- const refsRef = React.useRef(refs);
812
- refsRef.current = refs;
813
- React.useEffect(() => {
814
- if (!enabled) return;
815
- function onPointerDown(e) {
816
- const target = e.target;
817
- if (refsRef.current.every((r) => !r.current?.contains(target))) {
818
- handler();
819
- }
820
- }
821
- document.addEventListener("pointerdown", onPointerDown, true);
822
- return () => document.removeEventListener("pointerdown", onPointerDown, true);
823
- }, [enabled, handler]);
824
- }
825
-
826
- // src/utils/aria.ts
827
- function getFocusableElements(container) {
828
- const selector = [
829
- "a[href]",
830
- "button:not([disabled])",
831
- "input:not([disabled])",
832
- "select:not([disabled])",
833
- "textarea:not([disabled])",
834
- '[tabindex]:not([tabindex="-1"])'
835
- ].join(", ");
836
- return Array.from(container.querySelectorAll(selector)).filter(
837
- (el) => !el.closest("[hidden]") && el.offsetParent !== null
838
- );
839
- }
840
-
841
- // src/date-picker/use-focus-trap.ts
842
- function useFocusTrap(containerRef, enabled = true) {
843
- React.useEffect(() => {
844
- if (!enabled || !containerRef.current) return;
845
- const container = containerRef.current;
846
- function onKeyDown(e) {
847
- if (e.key !== "Tab") return;
848
- const focusable = getFocusableElements(container);
849
- if (!focusable.length) return;
850
- const first = focusable[0];
851
- const last = focusable[focusable.length - 1];
852
- if (e.shiftKey) {
853
- if (document.activeElement === first) {
854
- e.preventDefault();
855
- last.focus();
856
- }
857
- } else {
858
- if (document.activeElement === last) {
859
- e.preventDefault();
860
- first.focus();
861
- }
862
- }
863
- }
864
- const observer = new MutationObserver(() => {
865
- const active = document.activeElement;
866
- if (active?.getAttribute("role") === "spinbutton") return;
867
- if (!container.contains(active)) {
868
- const firstFocusable = getFocusableElements(container)[0];
869
- firstFocusable?.focus();
870
- }
871
- });
872
- observer.observe(container, { childList: true, subtree: true });
873
- container.addEventListener("keydown", onKeyDown);
874
- return () => {
875
- container.removeEventListener("keydown", onKeyDown);
876
- observer.disconnect();
877
- };
878
- }, [enabled, containerRef]);
879
- }
880
814
  function toPlacement(side, align) {
881
815
  return align === "center" ? side : `${side}-${align}`;
882
816
  }
@@ -975,12 +909,26 @@ function Content({
975
909
  },
976
910
  [setFloating]
977
911
  );
978
- useClickOutside(
912
+ const close = React.useCallback(() => {
913
+ const source = state.openSource;
914
+ dispatch({ type: "CLOSE" });
915
+ utils.restoreFocus({
916
+ openSource: source === "input" ? "input" : source === "trigger" ? "trigger" : "unknown",
917
+ trigger: document.getElementById(ids.trigger),
918
+ input: document.getElementById(ids.input) ?? document.getElementById(`${ids.input}-0`)
919
+ });
920
+ }, [dispatch, ids.input, ids.trigger, state.openSource]);
921
+ utils.useClickOutside(
979
922
  [contentRef, triggerRef, inputRef, input0Ref, input1Ref],
980
- () => dispatch({ type: "CLOSE" }),
923
+ close,
981
924
  isOpen
982
925
  );
983
- useFocusTrap(contentRef, isOpen);
926
+ utils.useEscapeKey({
927
+ enabled: isOpen,
928
+ stopPropagation: true,
929
+ onEscape: close
930
+ });
931
+ utils.useFocusTrap(contentRef, isOpen && config.modal);
984
932
  const [transitionsReady, setTransitionsReady] = React.useState(false);
985
933
  React.useEffect(() => {
986
934
  if (!isOpen || !isPositioned) {
@@ -1009,7 +957,7 @@ function Content({
1009
957
  ref: mergedRef,
1010
958
  id: ids.content,
1011
959
  role: "dialog",
1012
- "aria-modal": "true",
960
+ "aria-modal": config.modal ? "true" : void 0,
1013
961
  "aria-labelledby": ids.label,
1014
962
  "data-state": isOpen ? "open" : "closed",
1015
963
  style: {
@@ -1023,14 +971,7 @@ function Content({
1023
971
  ...isOpen && !transitionsReady ? { transition: "none" } : void 0,
1024
972
  ...style
1025
973
  },
1026
- onKeyDown: (e) => {
1027
- if (e.key === "Escape") {
1028
- e.preventDefault();
1029
- dispatch({ type: "CLOSE" });
1030
- document.getElementById(ids.trigger)?.focus();
1031
- }
1032
- onKeyDown?.(e);
1033
- },
974
+ onKeyDown,
1034
975
  ...props,
1035
976
  children: [
1036
977
  /* @__PURE__ */ jsxRuntime.jsx(