@kenos-ui/react-datepicker 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # @kenos-ui/react-datepicker
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - docs(readme): major update to README
8
+ - Document the unified `DatePicker.Root` + `mode` API correctly
9
+ - Highlight the segmented `<DatePicker.Input />` as the main feature
10
+ - Add clear examples for range (dual inputs + hover preview), multiple, and full composition
11
+ - Improve props, localization and advanced Content sections
12
+
13
+ ## 0.3.3
14
+
15
+ ### Patch Changes
16
+
17
+ - docs: major refresh of README.md and root documentation
18
+ - Update package README to accurately reflect current API (unified `DatePicker.Root` with `mode`, first-class `<DatePicker.Input>`, full composition)
19
+ - Add strong examples for segmented input, range with dual inputs + hover preview, multiple selection, and custom rendering
20
+ - Align with brand voice and recent announcement content
21
+ - Update root README with correct features and examples
22
+
23
+ ## 0.3.2
24
+
25
+ ### Patch Changes
26
+
27
+ - Updated dependencies [aaa8a57]
28
+ - @kenos-ui/utils@0.0.1
29
+
3
30
  ## 0.3.1
4
31
 
5
32
  ### Patch Changes
@@ -11,7 +38,6 @@
11
38
  ### Minor Changes
12
39
 
13
40
  - Axis lift-and-shift: publish DatePicker under `@at5/axis-datepicker`.
14
-
15
41
  - Add `@at5/axis-datepicker` — same DatePicker API and behavior as `@at5/kairo` (migrated from `packages/kairo` to `packages/datepicker`)
16
42
  - Add `@at5/axis` — aggregator re-exporting `DatePicker`
17
43
  - `@at5/kairo` — deprecated; thin re-export of `@at5/axis-datepicker` for transition
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,146 @@ 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" : ""}>{d.getDate()}</span>
115
+ )}
116
+ </DatePicker.Day>
117
+ ))}
118
+ </tr>
119
+ ))
120
+ }
121
+ </DatePicker.Grid>
122
+ </DatePicker.View>
123
+ </DatePicker.Root>
112
124
  ```
113
125
 
114
- **Props:** `value`, `defaultValue`, `onValueChange`, `minNights`, `maxNights`, `presets`, `locale`
126
+ You can also render month/year grids with `MonthGrid` / `YearGrid`.
115
127
 
116
- ---
128
+ ## Positioning & advanced Content
117
129
 
118
- ### Date Field
130
+ `<DatePicker.Content>` is powered by Floating UI and supports:
119
131
 
120
- Segmented spinbutton input no popover, locale-aware segment order.
132
+ - `side`, `align`, `sideOffset`, `alignOffset`
133
+ - `portal`
134
+ - `forceMount` (for enter/exit animations via `[data-state="open"]`)
135
+ - `avoidCollisions`
121
136
 
122
137
  ```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>;
138
+ <DatePicker.Content portal side="top" align="end" forceMount>
139
+ <DatePicker.Calendar />
140
+ </DatePicker.Content>
132
141
  ```
133
142
 
134
- **Props:** `value`, `defaultValue`, `onValueChange`, `locale`, `granularity`, `min`, `max`
143
+ ## Key props on Root
144
+
145
+ - `mode`: `"single" | "range" | "multiple"`
146
+ - `locale`, `weekStartsOn`
147
+ - `minDate`, `maxDate`, `disabled`
148
+ - `readOnly`
149
+ - `modal` — opt-in focus trap + `aria-modal` (default `false` — follows popup policy)
150
+ - `defaultOpen`, `closeOnSelect`
135
151
 
136
- **Keyboard:** `↑`/`↓` increments/decrements the focused segment, `Tab` advances to next segment.
152
+ See the full prop tables and more examples in the [project README](https://github.com/allysontsoares/kenos-ui/blob/main/README.md).
137
153
 
138
154
  ## Localization
139
155
 
140
- All components accept a `locale` prop compatible with `Intl.Locale`. Week start day, month/day/year order, and calendar display adapt automatically.
156
+ Everything is driven by `Intl.Locale`:
141
157
 
142
158
  ```tsx
143
- // Arabic RTL
144
- <DatePicker.Root locale="ar" />
145
-
146
- // Brazilian Portuguese
147
- <DatePicker.Root locale="pt-BR" />
148
-
149
- // Japanese
159
+ <DatePicker.Root locale="pt-BR" /> {/* dd/mm/yyyy, week starts Monday */}
160
+ <DatePicker.Root locale="ar" /> {/* RTL + Saturday start */}
150
161
  <DatePicker.Root locale="ja-JP" />
151
162
  ```
152
163
 
153
164
  ## Requirements
154
165
 
155
- - React `>=19.0.0`
156
- - Node.js `>=22`
166
+ - React 19
167
+ - Node 22
168
+
169
+ ## Exports
170
+
171
+ ```ts
172
+ import {
173
+ DatePicker,
174
+ useDatePickerContext,
175
+ // types
176
+ type DatePickerRootProps,
177
+ type DateRange,
178
+ type DayCellMeta,
179
+ // ...
180
+ } from "@kenos-ui/react-datepicker";
181
+ ```
157
182
 
158
183
  ## License
159
184
 
@@ -161,4 +186,4 @@ MIT
161
186
 
162
187
  ---
163
188
 
164
- *Kenos UI*
189
+ _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,22 @@ function Content({
975
909
  },
976
910
  [setFloating]
977
911
  );
978
- useClickOutside(
979
- [contentRef, triggerRef, inputRef, input0Ref, input1Ref],
980
- () => dispatch({ type: "CLOSE" }),
981
- isOpen
982
- );
983
- useFocusTrap(contentRef, isOpen);
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([contentRef, triggerRef, inputRef, input0Ref, input1Ref], close, isOpen);
922
+ utils.useEscapeKey({
923
+ enabled: isOpen,
924
+ stopPropagation: true,
925
+ onEscape: close
926
+ });
927
+ utils.useFocusTrap(contentRef, isOpen && config.modal);
984
928
  const [transitionsReady, setTransitionsReady] = React.useState(false);
985
929
  React.useEffect(() => {
986
930
  if (!isOpen || !isPositioned) {
@@ -1009,7 +953,7 @@ function Content({
1009
953
  ref: mergedRef,
1010
954
  id: ids.content,
1011
955
  role: "dialog",
1012
- "aria-modal": "true",
956
+ "aria-modal": config.modal ? "true" : void 0,
1013
957
  "aria-labelledby": ids.label,
1014
958
  "data-state": isOpen ? "open" : "closed",
1015
959
  style: {
@@ -1023,14 +967,7 @@ function Content({
1023
967
  ...isOpen && !transitionsReady ? { transition: "none" } : void 0,
1024
968
  ...style
1025
969
  },
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
- },
970
+ onKeyDown,
1034
971
  ...props,
1035
972
  children: [
1036
973
  /* @__PURE__ */ jsxRuntime.jsx(