@diwauhris/ui 1.7.24 → 1.7.26

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/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, placement = "auto") {
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,23 +1408,30 @@ function computePopupStyle(rect, placement = "auto") {
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 (placement === "below" ? true : placement === "above" ? false : spaceBelow >= CALENDAR_HEIGHT || spaceBelow >= spaceAbove) style.top = Math.min(rect.bottom + 6, window.innerHeight - CALENDAR_HEIGHT - 8);
1411
- else style.top = Math.max(rect.top - CALENDAR_HEIGHT - 6, 8);
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, dateFormat = "MMM d, yyyy", clearable = false, onOpenChange, placement = "auto", locale, name }) {
1415
- const [isOpen, setIsOpenState] = (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)({});
1420
+ const [positioned, setPositioned] = (0, react.useState)(false);
1417
1421
  const wrapperRef = (0, react.useRef)(null);
1418
1422
  const popoverRef = (0, react.useRef)(null);
1419
1423
  const inputRef = (0, react.useRef)(null);
1420
1424
  const internalId = (0, react.useId)();
1421
1425
  const setIsOpen = (0, react.useCallback)((next) => {
1422
- setIsOpenState((prev) => {
1426
+ if (isControlled) {
1427
+ onOpenChange?.(next);
1428
+ return;
1429
+ }
1430
+ setUncontrolledOpen((prev) => {
1423
1431
  if (prev !== next) onOpenChange?.(next);
1424
1432
  return next;
1425
1433
  });
1426
- }, [onOpenChange]);
1434
+ }, [isControlled, onOpenChange]);
1427
1435
  const ctx = useFieldContext();
1428
1436
  const inputId = id ?? ctx?.inputId ?? internalId;
1429
1437
  const resolvedRequired = required ?? ctx?.required ?? false;
@@ -1438,7 +1446,7 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1438
1446
  const displayValue = selectedDate ? (0, date_fns.format)(selectedDate, dateFormat) : "";
1439
1447
  const skipNextOpen = (0, react.useRef)(false);
1440
1448
  const openPopup = (0, react.useCallback)(() => {
1441
- if (disabled) return;
1449
+ if (disabled || readOnly) return;
1442
1450
  if (skipNextOpen.current) {
1443
1451
  skipNextOpen.current = false;
1444
1452
  return;
@@ -1449,14 +1457,27 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1449
1457
  if (!wrapperRef.current) return;
1450
1458
  const style = computePopupStyle(wrapperRef.current.getBoundingClientRect(), placement);
1451
1459
  setPopupStyle(style);
1460
+ setPositioned(false);
1452
1461
  setIsOpen(true);
1453
1462
  });
1454
1463
  }, 50);
1455
1464
  }, [
1456
1465
  disabled,
1466
+ readOnly,
1457
1467
  placement,
1458
1468
  setIsOpen
1459
1469
  ]);
1470
+ (0, react.useLayoutEffect)(() => {
1471
+ if (!isOpen) {
1472
+ setPositioned(false);
1473
+ return;
1474
+ }
1475
+ if (!popoverRef.current || !wrapperRef.current) return;
1476
+ const measuredHeight = popoverRef.current.offsetHeight;
1477
+ const rect = wrapperRef.current.getBoundingClientRect();
1478
+ setPopupStyle(computePopupStyle(rect, placement, measuredHeight));
1479
+ setPositioned(true);
1480
+ }, [isOpen, placement]);
1460
1481
  (0, react.useEffect)(() => {
1461
1482
  function handleClickOutside(e) {
1462
1483
  const target = e.target;
@@ -1528,11 +1549,12 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1528
1549
  "aria-required": resolvedRequired ? "true" : void 0,
1529
1550
  "aria-invalid": resolvedInvalid ? "true" : void 0,
1530
1551
  "aria-describedby": describedBy,
1531
- "aria-haspopup": "dialog",
1532
- "aria-expanded": isOpen,
1552
+ "aria-readonly": readOnly ? "true" : void 0,
1553
+ "aria-haspopup": readOnly ? void 0 : "dialog",
1554
+ "aria-expanded": readOnly ? void 0 : isOpen,
1533
1555
  role: "combobox",
1534
1556
  className: inputClasses
1535
- }), clearable && value && !disabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1557
+ }), clearable && value && !disabled && !readOnly ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1536
1558
  type: "button",
1537
1559
  "aria-label": "Clear date",
1538
1560
  onMouseDown: (e) => {
@@ -1551,10 +1573,13 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1551
1573
  className: `absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none ${isOpen ? "text-brand-blue" : "text-slate-300"}`,
1552
1574
  "aria-hidden": "true"
1553
1575
  })]
1554
- }), isOpen && !disabled && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1576
+ }), isOpen && !disabled && !readOnly && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1555
1577
  ref: popoverRef,
1556
1578
  className: "pis-datepicker-popover",
1557
- style: popupStyle,
1579
+ style: {
1580
+ ...popupStyle,
1581
+ visibility: positioned ? void 0 : "hidden"
1582
+ },
1558
1583
  role: "dialog",
1559
1584
  "aria-label": ariaLabel ? `${ariaLabel} calendar` : "Date picker",
1560
1585
  "aria-modal": "false",
@@ -1571,6 +1596,7 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1571
1596
  defaultActiveStartDate: selectedDate ?? minDate,
1572
1597
  minDate,
1573
1598
  maxDate,
1599
+ tileDisabled: isDateDisabled ? ({ date, view }) => view === "month" && isDateDisabled(date) : void 0,
1574
1600
  locale,
1575
1601
  defaultView: "month",
1576
1602
  showNeighboringDecade: true,
package/lib/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { clsx } from "clsx";
2
2
  import { twMerge } from "tailwind-merge";
3
3
  import * as React$1 from "react";
4
- import React, { cloneElement, createContext, createElement, isValidElement, use, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
4
+ import React, { cloneElement, createContext, createElement, isValidElement, use, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
6
  import { createPortal } from "react-dom";
7
7
  import { AlertCircle, AlertTriangle, Archive, ArchiveRestore, ArrowDown, Ban, CalendarDays, Check, CheckCircle2, ChevronDown, ChevronLeft, ChevronRight, Clock, Command, File, Home, Info, Minus, RotateCcw, Search as Search$1, ShieldCheck, TrendingDown, TrendingUp, UploadCloud, X, ZoomIn, ZoomOut } from "lucide-react";
@@ -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, placement = "auto") {
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,23 +1384,30 @@ function computePopupStyle(rect, placement = "auto") {
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 (placement === "below" ? true : placement === "above" ? false : spaceBelow >= CALENDAR_HEIGHT || spaceBelow >= spaceAbove) style.top = Math.min(rect.bottom + 6, window.innerHeight - CALENDAR_HEIGHT - 8);
1387
- else style.top = Math.max(rect.top - CALENDAR_HEIGHT - 6, 8);
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, dateFormat = "MMM d, yyyy", clearable = false, onOpenChange, placement = "auto", locale, name }) {
1391
- const [isOpen, setIsOpenState] = 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({});
1396
+ const [positioned, setPositioned] = useState(false);
1393
1397
  const wrapperRef = useRef(null);
1394
1398
  const popoverRef = useRef(null);
1395
1399
  const inputRef = useRef(null);
1396
1400
  const internalId = useId();
1397
1401
  const setIsOpen = useCallback((next) => {
1398
- setIsOpenState((prev) => {
1402
+ if (isControlled) {
1403
+ onOpenChange?.(next);
1404
+ return;
1405
+ }
1406
+ setUncontrolledOpen((prev) => {
1399
1407
  if (prev !== next) onOpenChange?.(next);
1400
1408
  return next;
1401
1409
  });
1402
- }, [onOpenChange]);
1410
+ }, [isControlled, onOpenChange]);
1403
1411
  const ctx = useFieldContext();
1404
1412
  const inputId = id ?? ctx?.inputId ?? internalId;
1405
1413
  const resolvedRequired = required ?? ctx?.required ?? false;
@@ -1414,7 +1422,7 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1414
1422
  const displayValue = selectedDate ? format(selectedDate, dateFormat) : "";
1415
1423
  const skipNextOpen = useRef(false);
1416
1424
  const openPopup = useCallback(() => {
1417
- if (disabled) return;
1425
+ if (disabled || readOnly) return;
1418
1426
  if (skipNextOpen.current) {
1419
1427
  skipNextOpen.current = false;
1420
1428
  return;
@@ -1425,14 +1433,27 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1425
1433
  if (!wrapperRef.current) return;
1426
1434
  const style = computePopupStyle(wrapperRef.current.getBoundingClientRect(), placement);
1427
1435
  setPopupStyle(style);
1436
+ setPositioned(false);
1428
1437
  setIsOpen(true);
1429
1438
  });
1430
1439
  }, 50);
1431
1440
  }, [
1432
1441
  disabled,
1442
+ readOnly,
1433
1443
  placement,
1434
1444
  setIsOpen
1435
1445
  ]);
1446
+ useLayoutEffect(() => {
1447
+ if (!isOpen) {
1448
+ setPositioned(false);
1449
+ return;
1450
+ }
1451
+ if (!popoverRef.current || !wrapperRef.current) return;
1452
+ const measuredHeight = popoverRef.current.offsetHeight;
1453
+ const rect = wrapperRef.current.getBoundingClientRect();
1454
+ setPopupStyle(computePopupStyle(rect, placement, measuredHeight));
1455
+ setPositioned(true);
1456
+ }, [isOpen, placement]);
1436
1457
  useEffect(() => {
1437
1458
  function handleClickOutside(e) {
1438
1459
  const target = e.target;
@@ -1504,11 +1525,12 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1504
1525
  "aria-required": resolvedRequired ? "true" : void 0,
1505
1526
  "aria-invalid": resolvedInvalid ? "true" : void 0,
1506
1527
  "aria-describedby": describedBy,
1507
- "aria-haspopup": "dialog",
1508
- "aria-expanded": isOpen,
1528
+ "aria-readonly": readOnly ? "true" : void 0,
1529
+ "aria-haspopup": readOnly ? void 0 : "dialog",
1530
+ "aria-expanded": readOnly ? void 0 : isOpen,
1509
1531
  role: "combobox",
1510
1532
  className: inputClasses
1511
- }), clearable && value && !disabled ? /* @__PURE__ */ jsx("button", {
1533
+ }), clearable && value && !disabled && !readOnly ? /* @__PURE__ */ jsx("button", {
1512
1534
  type: "button",
1513
1535
  "aria-label": "Clear date",
1514
1536
  onMouseDown: (e) => {
@@ -1527,10 +1549,13 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1527
1549
  className: `absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none ${isOpen ? "text-brand-blue" : "text-slate-300"}`,
1528
1550
  "aria-hidden": "true"
1529
1551
  })]
1530
- }), isOpen && !disabled && createPortal(/* @__PURE__ */ jsx("div", {
1552
+ }), isOpen && !disabled && !readOnly && createPortal(/* @__PURE__ */ jsx("div", {
1531
1553
  ref: popoverRef,
1532
1554
  className: "pis-datepicker-popover",
1533
- style: popupStyle,
1555
+ style: {
1556
+ ...popupStyle,
1557
+ visibility: positioned ? void 0 : "hidden"
1558
+ },
1534
1559
  role: "dialog",
1535
1560
  "aria-label": ariaLabel ? `${ariaLabel} calendar` : "Date picker",
1536
1561
  "aria-modal": "false",
@@ -1547,6 +1572,7 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1547
1572
  defaultActiveStartDate: selectedDate ?? minDate,
1548
1573
  minDate,
1549
1574
  maxDate,
1575
+ tileDisabled: isDateDisabled ? ({ date, view }) => view === "month" && isDateDisabled(date) : void 0,
1550
1576
  locale,
1551
1577
  defaultView: "month",
1552
1578
  showNeighboringDecade: true,
@@ -32,7 +32,27 @@ interface DatePickerProps {
32
32
  locale?: string;
33
33
  /** Name forwarded to the trigger input for form association. */
34
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;
35
55
  }
36
- export declare function computePopupStyle(rect: DOMRect, placement?: 'auto' | 'below' | 'above'): React.CSSProperties;
37
- 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, }: 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;
38
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.24",
4
+ "version": "1.7.26",
5
5
  "type": "module",
6
6
  "description": "DIWA UHRIS UI component library — React + Tailwind CSS",
7
7
  "keywords": [