@ostack.tech/ui 0.3.3 → 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/dist/ostack-ui.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { forwardRef, createContext, useContext, useCallback, useRef, useEffect, useId, useMemo, useState, memo, isValidElement, cloneElement, Fragment, Children, useImperativeHandle, startTransition, useLayoutEffect as useLayoutEffect$1, createElement, useDeferredValue, useSyncExternalStore } from "react";
2
2
  import { tinykeys } from "tinykeys";
3
- import { shallow } from "zustand/shallow";
4
3
  import fromExponential from "from-exponential";
5
4
  import { isValid, isDate, addMonths, isAfter, isBefore, startOfMonth, setMonth, getYear, setYear, startOfYear, format, isSameYear, getMonth, max, min, lastDayOfMonth, parseISO, parse, isWithinInterval, isEqual } from "date-fns";
6
5
  import { createStore, useStore, create } from "zustand";
6
+ import { shallow } from "zustand/shallow";
7
7
  import { removeNumericFormat, numericFormatter, NumericFormat } from "react-number-format";
8
8
  import { jsx, jsxs, Fragment as Fragment$1 } from "react/jsx-runtime";
9
9
  import { faClose, faTriangleExclamation, faCircleExclamation, faCircleCheck, faCircleInfo, faChevronUp, faChevronDown, faRedo, faCircleQuestion, faSortDown, faSortUp, faAsterisk, faMinus, faCheck, faFilter, faArrowLeft, faArrowRight, faSearch, faChevronLeft, faChevronRight, faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
@@ -410,7 +410,7 @@ function useCreateDataTableContext({
410
410
  onSelectedRowsChange,
411
411
  disabledSelections
412
412
  }) {
413
- const listeners2 = useLatestValues({
413
+ const listeners = useLatestValues({
414
414
  onOffsetChange,
415
415
  onLimitChange,
416
416
  onSort,
@@ -499,11 +499,11 @@ function useCreateDataTableContext({
499
499
  },
500
500
  setOffset: (offset2) => {
501
501
  set({ updateCounter: get().updateCounter + 1, _offset: offset2 });
502
- listeners2.onOffsetChange?.(offset2);
502
+ listeners.onOffsetChange?.(offset2);
503
503
  },
504
504
  setLimit: (limit2) => {
505
505
  set({ updateCounter: get().updateCounter + 1, limit: limit2 });
506
- listeners2.onLimitChange?.(limit2);
506
+ listeners.onLimitChange?.(limit2);
507
507
  },
508
508
  setFilter: (filter2) => {
509
509
  if (get().offset() > 0) {
@@ -511,7 +511,7 @@ function useCreateDataTableContext({
511
511
  }
512
512
  set({ filter: filter2 });
513
513
  get().actions.refresh(null);
514
- listeners2.onFilterChange?.(filter2);
514
+ listeners.onFilterChange?.(filter2);
515
515
  },
516
516
  setSort: (sortBy2, sortDirection2) => {
517
517
  if (get().offset() > 0) {
@@ -519,7 +519,7 @@ function useCreateDataTableContext({
519
519
  }
520
520
  set({ sortBy: sortBy2, sortDirection: sortDirection2 });
521
521
  get().actions.refresh();
522
- listeners2.onSort?.(sortBy2, sortDirection2);
522
+ listeners.onSort?.(sortBy2, sortDirection2);
523
523
  },
524
524
  selectRows: (keys) => {
525
525
  const {
@@ -535,7 +535,7 @@ function useCreateDataTableContext({
535
535
  }
536
536
  if (selected) {
537
537
  set({ selectedRows: [selectedRows2] });
538
- listeners2.onSelectedRowsChange?.(Array.from(selectedRows2).sort());
538
+ listeners.onSelectedRowsChange?.(Array.from(selectedRows2).sort());
539
539
  }
540
540
  },
541
541
  unselectRows: (keys) => {
@@ -551,7 +551,7 @@ function useCreateDataTableContext({
551
551
  }
552
552
  if (unselected) {
553
553
  set({ selectedRows: [selectedRows2] });
554
- listeners2.onSelectedRowsChange?.(Array.from(selectedRows2).sort());
554
+ listeners.onSelectedRowsChange?.(Array.from(selectedRows2).sort());
555
555
  }
556
556
  },
557
557
  updateWindow: (offset2, limit2) => {
@@ -1180,14 +1180,31 @@ function unsetRef(ref, cleanupsRef) {
1180
1180
  const LocalizationContext = createContext(null);
1181
1181
  function useControllableState(initialValue, controlledValue) {
1182
1182
  const [uncontrolledValue, setUncontrolledValue] = useState(initialValue);
1183
- const isControlled = controlledValue !== void 0;
1183
+ const latest = useLatestValues({ controlledValue });
1184
1184
  const setValue = useCallback(
1185
- (nextValue) => isControlled ? typeof nextValue === "function" ? setUncontrolledValue(
1186
- nextValue(controlledValue)
1187
- ) : setUncontrolledValue(controlledValue) : setUncontrolledValue(nextValue),
1188
- [controlledValue, isControlled]
1185
+ (nextValue) => {
1186
+ const { controlledValue: controlledValue2 } = latest;
1187
+ if (controlledValue2 === void 0) {
1188
+ setUncontrolledValue(nextValue);
1189
+ } else if (typeof nextValue === "function") {
1190
+ setUncontrolledValue(
1191
+ nextValue(controlledValue2)
1192
+ );
1193
+ } else {
1194
+ setUncontrolledValue(controlledValue2);
1195
+ }
1196
+ },
1197
+ [latest]
1198
+ );
1199
+ return useMemo(
1200
+ () => [
1201
+ // See: https://github.com/typescript-eslint/typescript-eslint/issues/7842
1202
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
1203
+ controlledValue === void 0 ? uncontrolledValue : controlledValue,
1204
+ setValue
1205
+ ],
1206
+ [controlledValue, setValue, uncontrolledValue]
1189
1207
  );
1190
- return [isControlled ? controlledValue : uncontrolledValue, setValue];
1191
1208
  }
1192
1209
  function LocalizationProvider({
1193
1210
  defaultLocale,
@@ -10622,128 +10639,144 @@ function useKeyboardShortcut(keybinds, action, {
10622
10639
  function ignoreFormControlsKeyboardShortcut(event) {
10623
10640
  return event.target !== null && event.target instanceof HTMLElement && FORM_CONTROLS.includes(event.target.tagName);
10624
10641
  }
10625
- function useLocation() {
10626
- const prevLocation = useRef(currentLocation());
10627
- const location2 = useSyncExternalStore(subscribeToHistoryEvents, () => {
10628
- const newLocation = currentLocation();
10629
- return shallow(prevLocation.current, newLocation) ? prevLocation.current : prevLocation.current = newLocation;
10630
- });
10631
- return useMemo(() => [location2, navigate], [location2]);
10642
+ function navigate(url, options) {
10643
+ history[options?.replace ? "replaceState" : "pushState"](
10644
+ options?.state,
10645
+ "",
10646
+ url
10647
+ );
10632
10648
  }
10633
- function useSearchParams() {
10634
- const [location2, navigate2] = useLocation();
10635
- const latestValues = useLatestValues({ location: location2 });
10636
- const setSearchParams = useCallback(
10637
- (nextSearchParams, { clearHash, ...options } = {}) => {
10638
- const { pathname, search, hash } = latestValues.location;
10639
- let newSearchParams = typeof nextSearchParams === "function" ? nextSearchParams(new URLSearchParams(search)) : nextSearchParams;
10640
- if (!(newSearchParams instanceof URLSearchParams)) {
10641
- newSearchParams = new URLSearchParams(newSearchParams);
10642
- }
10643
- navigate2(
10644
- `${pathname}?${newSearchParams}${clearHash ? "" : hash}`,
10649
+ function useIntersectionObserver(element, onIntersectionEntryChange, options) {
10650
+ useEffect(() => {
10651
+ if (element !== null) {
10652
+ const observer = new IntersectionObserver(
10653
+ ([e]) => onIntersectionEntryChange(e),
10645
10654
  options
10646
10655
  );
10647
- },
10648
- [latestValues, navigate2]
10649
- );
10650
- return useMemo(
10651
- () => [new URLSearchParams(location2.search), setSearchParams],
10652
- [location2.search, setSearchParams]
10653
- );
10654
- }
10655
- function useSearchParam(searchParam, defaultValue) {
10656
- const [searchParams, setSearchParams] = useSearchParams();
10657
- const searchParamValue = useMemo(
10658
- () => searchParams.get(searchParam) ?? defaultValue,
10659
- [defaultValue, searchParam, searchParams]
10660
- );
10661
- const setSearchParamValue = useCallback(
10662
- (nextSearchParamValue, { clearDefaultValue, ...options } = {}) => {
10663
- setSearchParams((prevSearchParams) => {
10664
- const prevValue = prevSearchParams.get(searchParam) ?? defaultValue;
10665
- const newValue = (typeof nextSearchParamValue === "function" ? nextSearchParamValue(prevValue) : nextSearchParamValue)?.toString();
10666
- if (newValue == null || clearDefaultValue && newValue === defaultValue) {
10667
- prevSearchParams.delete(searchParam);
10668
- } else {
10669
- prevSearchParams.set(searchParam, newValue);
10670
- }
10671
- return prevSearchParams;
10672
- }, options);
10673
- },
10674
- [defaultValue, searchParam, setSearchParams]
10675
- );
10676
- return useMemo(
10677
- () => [searchParamValue, setSearchParamValue],
10678
- [searchParamValue, setSearchParamValue]
10679
- );
10656
+ observer.observe(element);
10657
+ return () => observer.unobserve(element);
10658
+ }
10659
+ }, [element, onIntersectionEntryChange, options]);
10680
10660
  }
10681
10661
  const MONKEY_PATCHED_HISTORY = Symbol.for("ostack-ui.monkeyPatchedHistory");
10682
10662
  const HISTORY_EVENT_TYPES = [
10683
10663
  "popstate",
10684
10664
  "hashchange",
10685
10665
  // Events emitted by monkey-patched `pushState`/`replaceState` respectively
10686
- "ostack-ui.pushstate",
10687
- "ostack-ui.replacestate"
10666
+ "ostack-ui.pushState",
10667
+ "ostack-ui.replaceState"
10688
10668
  ];
10689
- const listeners = /* @__PURE__ */ new Set();
10690
- function handleHistoryEvent(evt) {
10691
- for (const listener of listeners) {
10692
- listener(evt);
10693
- }
10669
+ let locationStore;
10670
+ function currentLocation() {
10671
+ const loc = window.location;
10672
+ return { pathname: loc.pathname, search: loc.search, hash: loc.hash };
10694
10673
  }
10695
- function subscribeToHistoryEvents(listener) {
10696
- if (!globalThis[MONKEY_PATCHED_HISTORY]) {
10697
- if (typeof history !== "undefined") {
10698
- for (const fnName of ["pushState", "replaceState"]) {
10699
- const evtName = `ostack-ui.${fnName.toLowerCase()}`;
10700
- const originalFn = history[fnName];
10701
- history[fnName] = function() {
10702
- const result = originalFn.apply(this, arguments);
10703
- dispatchEvent(new Event(evtName));
10704
- return result;
10674
+ function createLocationStore() {
10675
+ return createStore((set, get) => ({
10676
+ location: currentLocation(),
10677
+ subscribers: 0,
10678
+ actions: {
10679
+ setCurrentLocation: () => {
10680
+ const newLocation = currentLocation();
10681
+ if (!shallow(get().location, newLocation)) {
10682
+ set({ location: newLocation });
10683
+ }
10684
+ },
10685
+ subscribe: () => {
10686
+ if (!globalThis[MONKEY_PATCHED_HISTORY]) {
10687
+ if (typeof history !== "undefined") {
10688
+ for (const fnName of ["pushState", "replaceState"]) {
10689
+ const evtName = `ostack-ui.${fnName}`;
10690
+ const originalFn = history[fnName];
10691
+ history[fnName] = function() {
10692
+ const result = originalFn.apply(this, arguments);
10693
+ dispatchEvent(new Event(evtName));
10694
+ return result;
10695
+ };
10696
+ }
10697
+ }
10698
+ globalThis[MONKEY_PATCHED_HISTORY] = true;
10699
+ }
10700
+ if (get().subscribers === 0) {
10701
+ get().actions.setCurrentLocation();
10702
+ for (const evtType of HISTORY_EVENT_TYPES) {
10703
+ addEventListener(evtType, get().actions.setCurrentLocation);
10704
+ }
10705
+ }
10706
+ set(({ subscribers }) => ({ subscribers: subscribers + 1 }));
10707
+ return () => {
10708
+ set(({ subscribers }) => ({ subscribers: subscribers - 1 }));
10709
+ if (get().subscribers === 0) {
10710
+ for (const evtType of HISTORY_EVENT_TYPES) {
10711
+ removeEventListener(evtType, get().actions.setCurrentLocation);
10712
+ }
10713
+ }
10705
10714
  };
10706
10715
  }
10707
10716
  }
10708
- globalThis[MONKEY_PATCHED_HISTORY] = true;
10709
- }
10710
- if (listeners.size === 0) {
10711
- for (const evtType of HISTORY_EVENT_TYPES) {
10712
- addEventListener(evtType, handleHistoryEvent);
10713
- }
10714
- }
10715
- listeners.add(listener);
10716
- return () => {
10717
- listeners.delete(listener);
10718
- if (listeners.size === 0) {
10719
- for (const evtType of HISTORY_EVENT_TYPES) {
10720
- removeEventListener(evtType, handleHistoryEvent);
10721
- }
10722
- }
10723
- };
10717
+ }));
10724
10718
  }
10725
- function currentLocation() {
10726
- const loc = window.location;
10727
- return { pathname: loc.pathname, search: loc.search, hash: loc.hash };
10719
+ function useLocation(selector) {
10720
+ locationStore ??= createLocationStore();
10721
+ useEffect(() => locationStore.getState().actions.subscribe(), []);
10722
+ return useStore(
10723
+ locationStore,
10724
+ ({ location: location2 }) => selector ? selector(location2) : location2
10725
+ );
10728
10726
  }
10729
- function navigate(url, options) {
10730
- history[options?.replace ? "replaceState" : "pushState"](
10731
- options?.state,
10732
- "",
10733
- url
10727
+ function useSearchParam(searchParam, defaultValue) {
10728
+ const searchParamValue = useLocation(
10729
+ (location2) => new URLSearchParams(location2.search).get(searchParam) ?? defaultValue
10730
+ );
10731
+ const latest = useLatestValues({ searchParam, defaultValue });
10732
+ const setSearchParamValue = useCallback(
10733
+ (nextSearchParamValue, { clearHash, clearDefaultValue, ...options } = {}) => {
10734
+ const { pathname, search, hash } = window.location;
10735
+ const { searchParam: searchParam2, defaultValue: defaultValue2 } = latest;
10736
+ const searchParams = new URLSearchParams(search);
10737
+ const prevValue = searchParams.get(searchParam2) ?? defaultValue2;
10738
+ const value = (typeof nextSearchParamValue === "function" ? nextSearchParamValue(prevValue) : nextSearchParamValue)?.toString();
10739
+ if (value == null || clearDefaultValue && value === defaultValue2) {
10740
+ searchParams.delete(searchParam2);
10741
+ } else {
10742
+ searchParams.set(searchParam2, value);
10743
+ }
10744
+ let searchParamsStr = searchParams.toString();
10745
+ searchParamsStr &&= `?${searchParamsStr}`;
10746
+ navigate(
10747
+ `${pathname}${searchParamsStr}${clearHash ? "" : hash}`,
10748
+ options
10749
+ );
10750
+ },
10751
+ [latest]
10752
+ );
10753
+ return useMemo(
10754
+ () => [searchParamValue, setSearchParamValue],
10755
+ [searchParamValue, setSearchParamValue]
10734
10756
  );
10735
10757
  }
10736
- function useIntersectionObserver(element, onIntersectionEntryChange, options) {
10737
- useEffect(() => {
10738
- if (element !== null) {
10739
- const observer = new IntersectionObserver(
10740
- ([e]) => onIntersectionEntryChange(e),
10758
+ function useSearchParams() {
10759
+ const locationSearch = useLocation((location2) => location2.search);
10760
+ const setSearchParams = useCallback(
10761
+ (nextSearchParams, { clearHash, ...options } = {}) => {
10762
+ const { pathname, search, hash } = window.location;
10763
+ let searchParams = typeof nextSearchParams === "function" ? nextSearchParams(new URLSearchParams(search)) : nextSearchParams;
10764
+ if (!(searchParams instanceof URLSearchParams)) {
10765
+ searchParams = new URLSearchParams(searchParams);
10766
+ }
10767
+ let searchParamsStr = searchParams.toString();
10768
+ searchParamsStr &&= `?${searchParamsStr}`;
10769
+ navigate(
10770
+ `${pathname}${searchParamsStr}${clearHash ? "" : hash}`,
10741
10771
  options
10742
10772
  );
10743
- observer.observe(element);
10744
- return () => observer.unobserve(element);
10745
- }
10746
- }, [element, onIntersectionEntryChange, options]);
10773
+ },
10774
+ []
10775
+ );
10776
+ return useMemo(
10777
+ () => [new URLSearchParams(locationSearch), setSearchParams],
10778
+ [locationSearch, setSearchParams]
10779
+ );
10747
10780
  }
10748
10781
  export {
10749
10782
  Alert,
@@ -10923,6 +10956,7 @@ export {
10923
10956
  isNumericStringNegative,
10924
10957
  matchAgainstFilter,
10925
10958
  mergeAriaIds,
10959
+ navigate,
10926
10960
  normalizeNumericString,
10927
10961
  numericStringFloatToInt,
10928
10962
  numericStringIntToFloat,