@diwauhris/ui 1.7.23 → 1.7.25

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/lib/assets/ui.css CHANGED
@@ -378,6 +378,10 @@
378
378
  right: 0.5rem;
379
379
  }
380
380
 
381
+ .right-2\.5 {
382
+ right: 0.625rem;
383
+ }
384
+
381
385
  .right-3 {
382
386
  right: 0.75rem;
383
387
  }
package/lib/index.cjs CHANGED
@@ -1398,7 +1398,8 @@ function getScrollParent(el) {
1398
1398
  if (["auto", "scroll"].includes(overflowY) || ["auto", "scroll"].includes(overflow)) return el;
1399
1399
  return getScrollParent(el.parentElement);
1400
1400
  }
1401
- function computePopupStyle(rect) {
1401
+ function computePopupStyle(rect, placement = "auto", measuredHeight) {
1402
+ const height = measuredHeight ?? CALENDAR_HEIGHT;
1402
1403
  const spaceBelow = window.innerHeight - rect.bottom;
1403
1404
  const spaceAbove = rect.top;
1404
1405
  const isNarrow = window.innerWidth < 368;
@@ -1407,17 +1408,29 @@ function computePopupStyle(rect) {
1407
1408
  left: isNarrow ? 8 : Math.max(8, Math.min(rect.left, window.innerWidth - POPOVER_WIDTH - 8)),
1408
1409
  width: isNarrow ? `calc(100vw - 16px)` : void 0
1409
1410
  };
1410
- if (spaceAbove > spaceBelow && spaceBelow < CALENDAR_HEIGHT / 3) style.top = Math.max(8, rect.top - CALENDAR_HEIGHT - 6);
1411
- else style.top = rect.bottom + 6;
1411
+ if (placement === "below" ? true : placement === "above" ? false : spaceBelow >= height || spaceBelow >= spaceAbove) style.top = Math.min(rect.bottom + 6, window.innerHeight - height - 8);
1412
+ else style.top = Math.max(rect.top - height - 6, 8);
1412
1413
  return style;
1413
1414
  }
1414
- function DatePicker({ value, onChange, placeholder, className, disabled, error, minDate, maxDate, id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, required }) {
1415
- const [isOpen, setIsOpen] = (0, react.useState)(false);
1415
+ function DatePicker({ value, onChange, placeholder, className, disabled, error, minDate, maxDate, id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, required, dateFormat = "MMM d, yyyy", clearable = false, onOpenChange, placement = "auto", locale, name, isDateDisabled, readOnly = false, open, defaultOpen = false }) {
1416
+ const isControlled = open !== void 0;
1417
+ const [uncontrolledOpen, setUncontrolledOpen] = (0, react.useState)(defaultOpen);
1418
+ const isOpen = isControlled ? open : uncontrolledOpen;
1416
1419
  const [popupStyle, setPopupStyle] = (0, react.useState)({});
1417
1420
  const wrapperRef = (0, react.useRef)(null);
1418
1421
  const popoverRef = (0, react.useRef)(null);
1419
1422
  const inputRef = (0, react.useRef)(null);
1420
1423
  const internalId = (0, react.useId)();
1424
+ const setIsOpen = (0, react.useCallback)((next) => {
1425
+ if (isControlled) {
1426
+ onOpenChange?.(next);
1427
+ return;
1428
+ }
1429
+ setUncontrolledOpen((prev) => {
1430
+ if (prev !== next) onOpenChange?.(next);
1431
+ return next;
1432
+ });
1433
+ }, [isControlled, onOpenChange]);
1421
1434
  const ctx = useFieldContext();
1422
1435
  const inputId = id ?? ctx?.inputId ?? internalId;
1423
1436
  const resolvedRequired = required ?? ctx?.required ?? false;
@@ -1429,10 +1442,10 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1429
1442
  const d2 = new Date(value);
1430
1443
  return isNaN(d2.getTime()) ? void 0 : d2;
1431
1444
  })() : void 0;
1432
- const displayValue = selectedDate ? (0, date_fns.format)(selectedDate, "MMM d, yyyy") : "";
1445
+ const displayValue = selectedDate ? (0, date_fns.format)(selectedDate, dateFormat) : "";
1433
1446
  const skipNextOpen = (0, react.useRef)(false);
1434
1447
  const openPopup = (0, react.useCallback)(() => {
1435
- if (disabled) return;
1448
+ if (disabled || readOnly) return;
1436
1449
  if (skipNextOpen.current) {
1437
1450
  skipNextOpen.current = false;
1438
1451
  return;
@@ -1441,12 +1454,23 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1441
1454
  if (!wrapperRef.current) return;
1442
1455
  requestAnimationFrame(() => {
1443
1456
  if (!wrapperRef.current) return;
1444
- const style = computePopupStyle(wrapperRef.current.getBoundingClientRect());
1457
+ const style = computePopupStyle(wrapperRef.current.getBoundingClientRect(), placement);
1445
1458
  setPopupStyle(style);
1446
1459
  setIsOpen(true);
1447
1460
  });
1448
1461
  }, 50);
1449
- }, [disabled]);
1462
+ }, [
1463
+ disabled,
1464
+ readOnly,
1465
+ placement,
1466
+ setIsOpen
1467
+ ]);
1468
+ (0, react.useEffect)(() => {
1469
+ if (!isOpen || !popoverRef.current || !wrapperRef.current) return;
1470
+ const measuredHeight = popoverRef.current.offsetHeight;
1471
+ const rect = wrapperRef.current.getBoundingClientRect();
1472
+ setPopupStyle(computePopupStyle(rect, placement, measuredHeight));
1473
+ }, [isOpen, placement]);
1450
1474
  (0, react.useEffect)(() => {
1451
1475
  function handleClickOutside(e) {
1452
1476
  const target = e.target;
@@ -1504,6 +1528,7 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1504
1528
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1505
1529
  ref: inputRef,
1506
1530
  id: inputId,
1531
+ name,
1507
1532
  type: "text",
1508
1533
  readOnly: true,
1509
1534
  value: displayValue,
@@ -1517,16 +1542,31 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1517
1542
  "aria-required": resolvedRequired ? "true" : void 0,
1518
1543
  "aria-invalid": resolvedInvalid ? "true" : void 0,
1519
1544
  "aria-describedby": describedBy,
1520
- "aria-haspopup": "dialog",
1521
- "aria-expanded": isOpen,
1545
+ "aria-readonly": readOnly ? "true" : void 0,
1546
+ "aria-haspopup": readOnly ? void 0 : "dialog",
1547
+ "aria-expanded": readOnly ? void 0 : isOpen,
1522
1548
  role: "combobox",
1523
1549
  className: inputClasses
1524
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.CalendarDays, {
1550
+ }), clearable && value && !disabled && !readOnly ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1551
+ type: "button",
1552
+ "aria-label": "Clear date",
1553
+ onMouseDown: (e) => {
1554
+ e.preventDefault();
1555
+ e.stopPropagation();
1556
+ },
1557
+ onClick: () => {
1558
+ onChange("");
1559
+ setIsOpen(false);
1560
+ inputRef.current?.focus();
1561
+ },
1562
+ className: "absolute right-2.5 top-1/2 -translate-y-1/2 flex h-5 w-5 items-center justify-center rounded-full text-slate-300 transition hover:bg-slate-100 hover:text-slate-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30",
1563
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, { size: 14 })
1564
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.CalendarDays, {
1525
1565
  size: 16,
1526
1566
  className: `absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none ${isOpen ? "text-brand-blue" : "text-slate-300"}`,
1527
1567
  "aria-hidden": "true"
1528
1568
  })]
1529
- }), isOpen && !disabled && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1569
+ }), isOpen && !disabled && !readOnly && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1530
1570
  ref: popoverRef,
1531
1571
  className: "pis-datepicker-popover",
1532
1572
  style: popupStyle,
@@ -1546,6 +1586,8 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1546
1586
  defaultActiveStartDate: selectedDate ?? minDate,
1547
1587
  minDate,
1548
1588
  maxDate,
1589
+ tileDisabled: isDateDisabled ? ({ date, view }) => view === "month" && isDateDisabled(date) : void 0,
1590
+ locale,
1549
1591
  defaultView: "month",
1550
1592
  showNeighboringDecade: true,
1551
1593
  showNeighboringCentury: true,
package/lib/index.mjs CHANGED
@@ -1374,7 +1374,8 @@ function getScrollParent(el) {
1374
1374
  if (["auto", "scroll"].includes(overflowY) || ["auto", "scroll"].includes(overflow)) return el;
1375
1375
  return getScrollParent(el.parentElement);
1376
1376
  }
1377
- function computePopupStyle(rect) {
1377
+ function computePopupStyle(rect, placement = "auto", measuredHeight) {
1378
+ const height = measuredHeight ?? CALENDAR_HEIGHT;
1378
1379
  const spaceBelow = window.innerHeight - rect.bottom;
1379
1380
  const spaceAbove = rect.top;
1380
1381
  const isNarrow = window.innerWidth < 368;
@@ -1383,17 +1384,29 @@ function computePopupStyle(rect) {
1383
1384
  left: isNarrow ? 8 : Math.max(8, Math.min(rect.left, window.innerWidth - POPOVER_WIDTH - 8)),
1384
1385
  width: isNarrow ? `calc(100vw - 16px)` : void 0
1385
1386
  };
1386
- if (spaceAbove > spaceBelow && spaceBelow < CALENDAR_HEIGHT / 3) style.top = Math.max(8, rect.top - CALENDAR_HEIGHT - 6);
1387
- else style.top = rect.bottom + 6;
1387
+ if (placement === "below" ? true : placement === "above" ? false : spaceBelow >= height || spaceBelow >= spaceAbove) style.top = Math.min(rect.bottom + 6, window.innerHeight - height - 8);
1388
+ else style.top = Math.max(rect.top - height - 6, 8);
1388
1389
  return style;
1389
1390
  }
1390
- function DatePicker({ value, onChange, placeholder, className, disabled, error, minDate, maxDate, id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, required }) {
1391
- const [isOpen, setIsOpen] = useState(false);
1391
+ function DatePicker({ value, onChange, placeholder, className, disabled, error, minDate, maxDate, id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, required, dateFormat = "MMM d, yyyy", clearable = false, onOpenChange, placement = "auto", locale, name, isDateDisabled, readOnly = false, open, defaultOpen = false }) {
1392
+ const isControlled = open !== void 0;
1393
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
1394
+ const isOpen = isControlled ? open : uncontrolledOpen;
1392
1395
  const [popupStyle, setPopupStyle] = useState({});
1393
1396
  const wrapperRef = useRef(null);
1394
1397
  const popoverRef = useRef(null);
1395
1398
  const inputRef = useRef(null);
1396
1399
  const internalId = useId();
1400
+ const setIsOpen = useCallback((next) => {
1401
+ if (isControlled) {
1402
+ onOpenChange?.(next);
1403
+ return;
1404
+ }
1405
+ setUncontrolledOpen((prev) => {
1406
+ if (prev !== next) onOpenChange?.(next);
1407
+ return next;
1408
+ });
1409
+ }, [isControlled, onOpenChange]);
1397
1410
  const ctx = useFieldContext();
1398
1411
  const inputId = id ?? ctx?.inputId ?? internalId;
1399
1412
  const resolvedRequired = required ?? ctx?.required ?? false;
@@ -1405,10 +1418,10 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1405
1418
  const d2 = new Date(value);
1406
1419
  return isNaN(d2.getTime()) ? void 0 : d2;
1407
1420
  })() : void 0;
1408
- const displayValue = selectedDate ? format(selectedDate, "MMM d, yyyy") : "";
1421
+ const displayValue = selectedDate ? format(selectedDate, dateFormat) : "";
1409
1422
  const skipNextOpen = useRef(false);
1410
1423
  const openPopup = useCallback(() => {
1411
- if (disabled) return;
1424
+ if (disabled || readOnly) return;
1412
1425
  if (skipNextOpen.current) {
1413
1426
  skipNextOpen.current = false;
1414
1427
  return;
@@ -1417,12 +1430,23 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1417
1430
  if (!wrapperRef.current) return;
1418
1431
  requestAnimationFrame(() => {
1419
1432
  if (!wrapperRef.current) return;
1420
- const style = computePopupStyle(wrapperRef.current.getBoundingClientRect());
1433
+ const style = computePopupStyle(wrapperRef.current.getBoundingClientRect(), placement);
1421
1434
  setPopupStyle(style);
1422
1435
  setIsOpen(true);
1423
1436
  });
1424
1437
  }, 50);
1425
- }, [disabled]);
1438
+ }, [
1439
+ disabled,
1440
+ readOnly,
1441
+ placement,
1442
+ setIsOpen
1443
+ ]);
1444
+ useEffect(() => {
1445
+ if (!isOpen || !popoverRef.current || !wrapperRef.current) return;
1446
+ const measuredHeight = popoverRef.current.offsetHeight;
1447
+ const rect = wrapperRef.current.getBoundingClientRect();
1448
+ setPopupStyle(computePopupStyle(rect, placement, measuredHeight));
1449
+ }, [isOpen, placement]);
1426
1450
  useEffect(() => {
1427
1451
  function handleClickOutside(e) {
1428
1452
  const target = e.target;
@@ -1480,6 +1504,7 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1480
1504
  children: [/* @__PURE__ */ jsx("input", {
1481
1505
  ref: inputRef,
1482
1506
  id: inputId,
1507
+ name,
1483
1508
  type: "text",
1484
1509
  readOnly: true,
1485
1510
  value: displayValue,
@@ -1493,16 +1518,31 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1493
1518
  "aria-required": resolvedRequired ? "true" : void 0,
1494
1519
  "aria-invalid": resolvedInvalid ? "true" : void 0,
1495
1520
  "aria-describedby": describedBy,
1496
- "aria-haspopup": "dialog",
1497
- "aria-expanded": isOpen,
1521
+ "aria-readonly": readOnly ? "true" : void 0,
1522
+ "aria-haspopup": readOnly ? void 0 : "dialog",
1523
+ "aria-expanded": readOnly ? void 0 : isOpen,
1498
1524
  role: "combobox",
1499
1525
  className: inputClasses
1500
- }), /* @__PURE__ */ jsx(CalendarDays, {
1526
+ }), clearable && value && !disabled && !readOnly ? /* @__PURE__ */ jsx("button", {
1527
+ type: "button",
1528
+ "aria-label": "Clear date",
1529
+ onMouseDown: (e) => {
1530
+ e.preventDefault();
1531
+ e.stopPropagation();
1532
+ },
1533
+ onClick: () => {
1534
+ onChange("");
1535
+ setIsOpen(false);
1536
+ inputRef.current?.focus();
1537
+ },
1538
+ className: "absolute right-2.5 top-1/2 -translate-y-1/2 flex h-5 w-5 items-center justify-center rounded-full text-slate-300 transition hover:bg-slate-100 hover:text-slate-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30",
1539
+ children: /* @__PURE__ */ jsx(X, { size: 14 })
1540
+ }) : /* @__PURE__ */ jsx(CalendarDays, {
1501
1541
  size: 16,
1502
1542
  className: `absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none ${isOpen ? "text-brand-blue" : "text-slate-300"}`,
1503
1543
  "aria-hidden": "true"
1504
1544
  })]
1505
- }), isOpen && !disabled && createPortal(/* @__PURE__ */ jsx("div", {
1545
+ }), isOpen && !disabled && !readOnly && createPortal(/* @__PURE__ */ jsx("div", {
1506
1546
  ref: popoverRef,
1507
1547
  className: "pis-datepicker-popover",
1508
1548
  style: popupStyle,
@@ -1522,6 +1562,8 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1522
1562
  defaultActiveStartDate: selectedDate ?? minDate,
1523
1563
  minDate,
1524
1564
  maxDate,
1565
+ tileDisabled: isDateDisabled ? ({ date, view }) => view === "month" && isDateDisabled(date) : void 0,
1566
+ locale,
1525
1567
  defaultView: "month",
1526
1568
  showNeighboringDecade: true,
1527
1569
  showNeighboringCentury: true,
@@ -12,6 +12,47 @@ interface DatePickerProps {
12
12
  'aria-label'?: string;
13
13
  'aria-labelledby'?: string;
14
14
  required?: boolean;
15
+ /**
16
+ * date-fns format string for the text shown in the trigger.
17
+ * The committed `onChange` value is always ISO `yyyy-MM-dd` regardless of this.
18
+ * Default: 'MMM d, yyyy'.
19
+ */
20
+ dateFormat?: string;
21
+ /** Show a clear (×) button that resets the value to '' when a date is set. */
22
+ clearable?: boolean;
23
+ /** Notified whenever the calendar popover opens or closes. */
24
+ onOpenChange?: (open: boolean) => void;
25
+ /**
26
+ * Popover placement relative to the trigger.
27
+ * 'auto' (default) flips above/below based on viewport space;
28
+ * 'below'/'above' force a side (still clamped to stay on-screen).
29
+ */
30
+ placement?: 'auto' | 'below' | 'above';
31
+ /** BCP-47 locale forwarded to the calendar (e.g. 'en-US', 'fr-FR'). */
32
+ locale?: string;
33
+ /** Name forwarded to the trigger input for form association. */
34
+ name?: string;
35
+ /**
36
+ * Per-day predicate to disable individual dates that min/maxDate can't express
37
+ * (weekends, holidays, already-booked days). Return true to disable the date.
38
+ * Only consulted on the day (month) view.
39
+ */
40
+ isDateDisabled?: (date: Date) => boolean;
41
+ /**
42
+ * Read-only: the value is shown but the calendar can't be opened and no date
43
+ * can be picked. Unlike `disabled`, it keeps normal (non-greyed) styling for
44
+ * view/detail screens.
45
+ */
46
+ readOnly?: boolean;
47
+ /**
48
+ * Controlled open state. When provided, the component no longer manages its
49
+ * own open/close — the parent owns it via `onOpenChange`. Leave undefined for
50
+ * the default uncontrolled behavior.
51
+ */
52
+ open?: boolean;
53
+ /** Initial open state when uncontrolled. Ignored when `open` is provided. */
54
+ defaultOpen?: boolean;
15
55
  }
16
- export declare function DatePicker({ value, onChange, placeholder, className, disabled, error, minDate, maxDate, id, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, required, }: DatePickerProps): import("react").JSX.Element;
56
+ export declare function computePopupStyle(rect: DOMRect, placement?: 'auto' | 'below' | 'above', measuredHeight?: number): React.CSSProperties;
57
+ export declare function DatePicker({ value, onChange, placeholder, className, disabled, error, minDate, maxDate, id, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, required, dateFormat, clearable, onOpenChange, placement, locale, name, isDateDisabled, readOnly, open, defaultOpen, }: DatePickerProps): import("react").JSX.Element;
17
58
  export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@diwauhris/ui",
3
3
  "private": false,
4
- "version": "1.7.23",
4
+ "version": "1.7.25",
5
5
  "type": "module",
6
6
  "description": "DIWA UHRIS UI component library — React + Tailwind CSS",
7
7
  "keywords": [