@douglasneuroinformatics/libui 3.1.5 → 3.2.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.
@@ -150,10 +150,11 @@ function useOnClickOutside(ref, handler, mouseEvent = "mousedown") {
150
150
  });
151
151
  }
152
152
 
153
- // src/hooks/useSessionStorage/useSessionStorage.ts
153
+ // src/hooks/useStorage/useStorage.ts
154
154
  import { useCallback as useCallback2, useEffect as useEffect6, useState as useState3 } from "react";
155
- function useSessionStorage(key, initialValue, options = {}) {
155
+ function useStorage(key, initialValue, storageName, options = {}) {
156
156
  const { initializeWithValue = true } = options;
157
+ const storage = window[storageName];
157
158
  const serializer = useCallback2(
158
159
  (value) => {
159
160
  if (options.serializer) {
@@ -167,8 +168,7 @@ function useSessionStorage(key, initialValue, options = {}) {
167
168
  (value) => {
168
169
  if (options.deserializer) {
169
170
  return options.deserializer(value);
170
- }
171
- if (value === "undefined") {
171
+ } else if (value === "undefined") {
172
172
  return void 0;
173
173
  }
174
174
  const defaultValue = initialValue instanceof Function ? initialValue() : initialValue;
@@ -188,7 +188,7 @@ function useSessionStorage(key, initialValue, options = {}) {
188
188
  if (!isBrowser()) {
189
189
  return initialValueToUse;
190
190
  }
191
- const raw = window.sessionStorage.getItem(key);
191
+ const raw = storage.getItem(key);
192
192
  return raw ? deserializer(raw) : initialValueToUse;
193
193
  }, [initialValue, key, deserializer]);
194
194
  const [storedValue, setStoredValue] = useState3(() => {
@@ -199,15 +199,15 @@ function useSessionStorage(key, initialValue, options = {}) {
199
199
  });
200
200
  const setValue = useEventCallback((value) => {
201
201
  if (!isBrowser()) {
202
- console.warn(`Tried setting sessionStorage key \u201C${key}\u201D even though environment is not a client`);
202
+ console.warn(`Tried setting storage key \u201C${key}\u201D even though environment is not a client`);
203
203
  }
204
204
  try {
205
205
  const newValue = value instanceof Function ? value(readValue()) : value;
206
- window.sessionStorage.setItem(key, serializer(newValue));
206
+ storage.setItem(key, serializer(newValue));
207
207
  setStoredValue(newValue);
208
- window.dispatchEvent(new StorageEvent("session-storage", { key }));
208
+ window.dispatchEvent(new StorageEvent(storageName, { key }));
209
209
  } catch (error) {
210
- console.warn(`Error setting sessionStorage key \u201C${key}\u201D:`, error);
210
+ console.warn(`Error setting storage key \u201C${key}\u201D:`, error);
211
211
  }
212
212
  });
213
213
  useEffect6(() => {
@@ -223,10 +223,20 @@ function useSessionStorage(key, initialValue, options = {}) {
223
223
  [key, readValue]
224
224
  );
225
225
  useEventListener("storage", handleStorageChange);
226
- useEventListener("session-storage", handleStorageChange);
226
+ useEventListener(storageName, handleStorageChange);
227
227
  return [storedValue, setValue];
228
228
  }
229
229
 
230
+ // src/hooks/useStorage/useLocalStorage.ts
231
+ function useLocalStorage(key, initialValue, options = {}) {
232
+ return useStorage(key, initialValue, "localStorage", options);
233
+ }
234
+
235
+ // src/hooks/useStorage/useSessionStorage.ts
236
+ function useSessionStorage(key, initialValue, options = {}) {
237
+ return useStorage(key, initialValue, "sessionStorage", options);
238
+ }
239
+
230
240
  // src/hooks/useTheme/useTheme.ts
231
241
  import { useEffect as useEffect7, useState as useState4 } from "react";
232
242
  var DEFAULT_THEME = "light";
@@ -324,6 +334,8 @@ export {
324
334
  useInterval,
325
335
  useMediaQuery,
326
336
  useOnClickOutside,
337
+ useStorage,
338
+ useLocalStorage,
327
339
  useSessionStorage,
328
340
  DEFAULT_THEME,
329
341
  THEME_ATTRIBUTE,
@@ -333,4 +345,4 @@ export {
333
345
  useTranslation,
334
346
  useWindowSize
335
347
  };
336
- //# sourceMappingURL=chunk-K2YKS4A5.js.map
348
+ //# sourceMappingURL=chunk-AFNJZUOE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hooks/useDownload/useDownload.ts","../src/hooks/useNotificationsStore/useNotificationsStore.ts","../src/hooks/useEventCallback/useEventCallback.ts","../src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.ts","../src/hooks/useEventListener/useEventListener.ts","../src/hooks/useInterval/useInterval.ts","../src/hooks/useMediaQuery/useMediaQuery.ts","../src/hooks/useOnClickOutside/useOnClickOutside.ts","../src/hooks/useStorage/useStorage.ts","../src/hooks/useStorage/useLocalStorage.ts","../src/hooks/useStorage/useSessionStorage.ts","../src/hooks/useTheme/useTheme.ts","../src/hooks/useTranslation/useTranslation.ts","../src/hooks/useWindowSize/useWindowSize.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\n\nimport type { Promisable } from 'type-fest';\n\nimport { useNotificationsStore } from '../useNotificationsStore';\n\ntype DownloadTextOptions = {\n blobType: 'text/csv' | 'text/plain';\n};\n\ntype DownloadBlobOptions = {\n blobType: 'application/zip' | 'image/jpeg' | 'image/png' | 'image/webp';\n};\n\n// eslint-disable-next-line @typescript-eslint/consistent-type-definitions\ninterface DownloadFunction {\n (filename: string, data: Blob, options: DownloadBlobOptions): Promise<void>;\n (filename: string, data: () => Promisable<Blob>, options: DownloadBlobOptions): Promise<void>;\n (filename: string, data: string, options?: DownloadTextOptions): Promise<void>;\n (filename: string, data: () => Promisable<string>, options?: DownloadTextOptions): Promise<void>;\n}\n\n/**\n * Used to trigger downloads of arbitrary data to the client\n * @returns A function to invoke the download\n */\nexport function useDownload(): DownloadFunction {\n const notifications = useNotificationsStore();\n const [state, setState] = useState<{\n blobType: string;\n data: Blob | string;\n filename: string;\n } | null>(null);\n\n useEffect(() => {\n if (state) {\n const { blobType, data, filename } = state;\n const anchor = document.createElement('a');\n document.body.appendChild(anchor);\n\n const blob = new Blob([data], { type: blobType });\n\n const url = URL.createObjectURL(blob);\n anchor.href = url;\n anchor.download = filename;\n anchor.click();\n URL.revokeObjectURL(url);\n anchor.remove();\n setState(null);\n }\n }, [state]);\n\n return async (filename, _data, options) => {\n try {\n const data = typeof _data === 'function' ? await _data() : _data;\n if (typeof data !== 'string' && !options?.blobType) {\n throw new Error(\"argument 'blobType' must be defined when download is called with a Blob object\");\n }\n setState({ blobType: options?.blobType ?? 'text/plain', data, filename });\n } catch (error) {\n const message = error instanceof Error ? error.message : 'An unknown error occurred';\n notifications.addNotification({\n message,\n title: 'Error',\n type: 'error'\n });\n }\n };\n}\n","import { create } from 'zustand';\n\nexport type NotificationInterface = {\n id: number;\n message?: string;\n title?: string;\n type: 'error' | 'info' | 'success' | 'warning';\n variant?: 'critical' | 'standard';\n};\n\nexport type NotificationsStore = {\n addNotification: (notification: Omit<NotificationInterface, 'id'>) => void;\n dismissNotification: (id: number) => void;\n notifications: NotificationInterface[];\n};\n\nexport const useNotificationsStore = create<NotificationsStore>((set) => ({\n addNotification: (notification) => {\n set((state) => ({\n notifications: [...state.notifications, { id: Date.now(), ...notification }]\n }));\n },\n dismissNotification: (id) => {\n set((state) => ({\n notifications: state.notifications.filter((notification) => notification.id !== id)\n }));\n },\n notifications: []\n}));\n","import { useCallback, useRef } from 'react';\n\nimport { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect';\n\nexport function useEventCallback<Args extends unknown[], R>(fn: (...args: Args) => R) {\n const ref = useRef<typeof fn>(() => {\n throw new Error('Cannot call an event handler while rendering.');\n });\n\n useIsomorphicLayoutEffect(() => {\n ref.current = fn;\n }, [fn]);\n\n return useCallback((...args: Args) => ref.current(...args), [ref]);\n}\n","import { useEffect, useLayoutEffect } from 'react';\n\nimport { isBrowser } from '@/utils';\n\nexport const useIsomorphicLayoutEffect = isBrowser() ? useLayoutEffect : useEffect;\n","import { type RefObject, useEffect, useRef } from 'react';\n\nimport { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect';\n\n// MediaQueryList Event based useEventListener interface\nfunction useEventListener<K extends keyof MediaQueryListEventMap>(\n eventName: K,\n handler: (event: MediaQueryListEventMap[K]) => void,\n element: RefObject<MediaQueryList>,\n options?: AddEventListenerOptions | boolean\n): void;\n\n// Window Event based useEventListener interface\nfunction useEventListener<K extends keyof WindowEventMap>(\n eventName: K,\n handler: (event: WindowEventMap[K]) => void,\n element?: undefined,\n options?: AddEventListenerOptions | boolean\n): void;\n\n// Element Event based useEventListener interface\nfunction useEventListener<K extends keyof HTMLElementEventMap, T extends HTMLElement = HTMLDivElement>(\n eventName: K,\n handler: (event: HTMLElementEventMap[K]) => void,\n element: RefObject<T>,\n options?: AddEventListenerOptions | boolean\n): void;\n\n// Document Event based useEventListener interface\nfunction useEventListener<K extends keyof DocumentEventMap>(\n eventName: K,\n handler: (event: DocumentEventMap[K]) => void,\n element: RefObject<Document>,\n options?: AddEventListenerOptions | boolean\n): void;\n\nfunction useEventListener<\n KW extends keyof WindowEventMap,\n KH extends keyof HTMLElementEventMap,\n KM extends keyof MediaQueryListEventMap,\n T extends HTMLElement | MediaQueryList | void = void\n>(\n eventName: KH | KM | KW,\n handler: (event: Event | HTMLElementEventMap[KH] | MediaQueryListEventMap[KM] | WindowEventMap[KW]) => void,\n element?: RefObject<T>,\n options?: AddEventListenerOptions | boolean\n) {\n // Create a ref that stores handler\n const savedHandler = useRef(handler);\n\n useIsomorphicLayoutEffect(() => {\n savedHandler.current = handler;\n }, [handler]);\n\n useEffect(() => {\n // Define the listening target\n const targetElement: T | Window = element?.current ?? window;\n\n if (!(targetElement && targetElement.addEventListener)) return;\n\n // Create event listener that calls handler function stored in ref\n const listener: typeof handler = (event) => savedHandler.current(event);\n\n targetElement.addEventListener(eventName, listener, options);\n\n // Remove event listener on cleanup\n return () => {\n targetElement.removeEventListener(eventName, listener, options);\n };\n }, [eventName, element, options]);\n}\n\nexport { useEventListener };\n","import { useEffect, useRef } from 'react';\n\nimport { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect';\n\nexport function useInterval(callback: () => void, delay: null | number) {\n const savedCallback = useRef(callback);\n\n // Remember the latest callback if it changes.\n useIsomorphicLayoutEffect(() => {\n savedCallback.current = callback;\n }, [callback]);\n\n // Set up the interval.\n useEffect(() => {\n // Don't schedule if no delay is specified.\n // Note: 0 is a valid value for delay.\n if (!delay && delay !== 0) {\n return;\n }\n\n const id = setInterval(() => savedCallback.current(), delay);\n\n return () => clearInterval(id);\n }, [delay]);\n}\n","import { useEffect, useState } from 'react';\n\nimport { isBrowser } from '@/utils';\n\n/**\n * Get the result of an arbitrary CSS media query\n *\n * @param query - the CSS media query\n * @returns a boolean indicating the result of the query\n * @example\n * // true if the viewport is at least 768px wide\n * const matches = useMediaQuery('(min-width: 768px)')\n */\nexport function useMediaQuery(query: string): boolean {\n const getMatches = (query: string): boolean => {\n // Prevents SSR issues\n if (isBrowser()) {\n return window.matchMedia(query).matches;\n }\n return false;\n };\n\n const [matches, setMatches] = useState<boolean>(getMatches(query));\n\n function handleChange() {\n setMatches(getMatches(query));\n }\n\n useEffect(() => {\n const matchMedia = window.matchMedia(query);\n\n // Triggered at the first client-side load and if query changes\n handleChange();\n\n matchMedia.addEventListener('change', handleChange);\n\n return () => {\n matchMedia.removeEventListener('change', handleChange);\n };\n }, [query]);\n\n return matches;\n}\n","import { type RefObject } from 'react';\n\nimport { useEventListener } from '../useEventListener';\n\ntype Handler = (event: MouseEvent) => void;\n\nexport function useOnClickOutside<T extends HTMLElement = HTMLElement>(\n ref: RefObject<T>,\n handler: Handler,\n mouseEvent: 'mousedown' | 'mouseup' = 'mousedown'\n): void {\n useEventListener(mouseEvent, (event) => {\n const el = ref.current;\n\n // Do nothing if clicking ref's element or descendent elements\n if (!el || el.contains(event.target as Node)) {\n return;\n }\n\n handler(event);\n });\n}\n","import { useCallback, useEffect, useState } from 'react';\nimport type { Dispatch, SetStateAction } from 'react';\n\nimport { isBrowser } from '@/utils';\n\nimport { useEventCallback } from '../useEventCallback';\nimport { useEventListener } from '../useEventListener';\n\ntype StorageName = 'localStorage' | 'sessionStorage';\n\ntype StorageEventMap = {\n [K in StorageName]: CustomEvent;\n};\n\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/consistent-type-definitions, @typescript-eslint/no-empty-object-type\n interface WindowEventMap extends StorageEventMap {}\n}\n\n/**\n * Represents the options for customizing the behavior of serialization and deserialization.\n * @template T - The type of the state to be stored in storage.\n */\ntype UseStorageOptions<T> = {\n /** A function to deserialize the stored value. */\n deserializer?: (value: string) => T;\n /**\n * If `true` (default), the hook will initialize reading the storage. In SSR, you should set it to `false`, returning the initial value initially.\n * @default true\n */\n initializeWithValue?: boolean;\n /** A function to serialize the value before storing it. */\n serializer?: (value: T) => string;\n};\n\n/**\n * Custom hook that uses local or session storage to persist state across page reloads.\n * @template T - The type of the state to be stored in storage.\n * @param key - The key under which the value will be stored in storage.\n * @param initialValue - The initial value of the state or a function that returns the initial value.\n * @param options - Options for customizing the behavior of serialization and deserialization (optional).\n * @returns A tuple containing the stored value and a function to set the value.\n * @public\n * @example\n * ```tsx\n * const [count, setCount] = useStorage('count', 0);\n * // Access the `count` value and the `setCount` function to update it.\n * ```\n */\nexport function useStorage<T>(\n key: string,\n initialValue: (() => T) | T,\n storageName: StorageName,\n options: UseStorageOptions<T> = {}\n): [T, Dispatch<SetStateAction<T>>] {\n const { initializeWithValue = true } = options;\n const storage = window[storageName];\n\n const serializer = useCallback<(value: T) => string>(\n (value) => {\n if (options.serializer) {\n return options.serializer(value);\n }\n return JSON.stringify(value);\n },\n [options]\n );\n\n const deserializer = useCallback<(value: string) => T>(\n (value) => {\n if (options.deserializer) {\n return options.deserializer(value);\n } else if (value === 'undefined') {\n return undefined as unknown as T;\n }\n const defaultValue = initialValue instanceof Function ? initialValue() : initialValue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(value);\n } catch (err) {\n console.error(`Error parsing JSON: ${(err as Error).message}`);\n return defaultValue;\n }\n return parsed as T;\n },\n [options, initialValue]\n );\n\n const readValue = useCallback((): T => {\n const initialValueToUse = initialValue instanceof Function ? initialValue() : initialValue;\n if (!isBrowser()) {\n return initialValueToUse;\n }\n const raw = storage.getItem(key);\n return raw ? deserializer(raw) : initialValueToUse;\n }, [initialValue, key, deserializer]);\n\n const [storedValue, setStoredValue] = useState(() => {\n if (initializeWithValue) {\n return readValue();\n }\n return initialValue instanceof Function ? initialValue() : initialValue;\n });\n\n const setValue: Dispatch<SetStateAction<T>> = useEventCallback((value) => {\n if (!isBrowser()) {\n console.warn(`Tried setting storage key “${key}” even though environment is not a client`);\n }\n try {\n const newValue = value instanceof Function ? value(readValue()) : value;\n storage.setItem(key, serializer(newValue));\n setStoredValue(newValue);\n window.dispatchEvent(new StorageEvent(storageName, { key }));\n } catch (error) {\n console.warn(`Error setting storage key “${key}”:`, error);\n }\n });\n\n useEffect(() => {\n setStoredValue(readValue());\n }, [key]);\n\n const handleStorageChange = useCallback(\n (event: CustomEvent | StorageEvent) => {\n if ((event as StorageEvent).key && (event as StorageEvent).key !== key) {\n return;\n }\n setStoredValue(readValue());\n },\n [key, readValue]\n );\n\n // this only works for other documents, not the current one\n useEventListener('storage', handleStorageChange);\n\n useEventListener(storageName, handleStorageChange);\n\n return [storedValue, setValue];\n}\n\nexport type { StorageName, UseStorageOptions };\n","import type { Dispatch, SetStateAction } from 'react';\n\nimport { useStorage, type UseStorageOptions } from './useStorage';\n\n/** Custom hook that uses local storage to persist state across page reloads */\nexport function useLocalStorage<T>(\n key: string,\n initialValue: (() => T) | T,\n options: UseStorageOptions<T> = {}\n): [T, Dispatch<SetStateAction<T>>] {\n return useStorage(key, initialValue, 'localStorage', options);\n}\n","import type { Dispatch, SetStateAction } from 'react';\n\nimport { useStorage, type UseStorageOptions } from './useStorage';\n\n/** Custom hook that uses session storage to persist state across page reloads */\nexport function useSessionStorage<T>(\n key: string,\n initialValue: (() => T) | T,\n options: UseStorageOptions<T> = {}\n): [T, Dispatch<SetStateAction<T>>] {\n return useStorage(key, initialValue, 'sessionStorage', options);\n}\n","import { useEffect, useState } from 'react';\n\n// this is required since our storybook manager plugin cannot use vite aliases\nimport { isBrowser } from '../../utils';\n\ntype Theme = 'dark' | 'light';\n\ntype UpdateTheme = (theme: Theme) => void;\n\n/** @private */\nconst DEFAULT_THEME: Theme = 'light';\n\n/** @private */\nconst THEME_ATTRIBUTE = 'data-mode';\n\n/** @private */\nconst THEME_KEY = 'theme';\n\n/** @private */\nconst SYS_DARK_MEDIA_QUERY = '(prefers-color-scheme: dark)';\n\n/**\n * Returns the current theme and a function to update the current theme\n *\n * The reason the implementation of this hook is rather convoluted is for\n * cases where the theme is updated outside this hook\n */\nfunction useTheme(): readonly [Theme, UpdateTheme] {\n // Initial theme value is based on the value saved in local storage or the system theme\n const [theme, setTheme] = useState<Theme>(() => {\n if (!isBrowser()) {\n return DEFAULT_THEME;\n }\n const savedTheme = window.localStorage.getItem(THEME_KEY);\n let initialTheme: Theme;\n if (savedTheme === 'dark' || savedTheme === 'light') {\n initialTheme = savedTheme;\n } else {\n initialTheme = window.matchMedia(SYS_DARK_MEDIA_QUERY).matches ? 'dark' : 'light';\n }\n document.documentElement.setAttribute(THEME_ATTRIBUTE, initialTheme);\n return initialTheme;\n });\n\n useEffect(() => {\n const observer = new MutationObserver((mutations) => {\n mutations.forEach((mutation) => {\n if (mutation.attributeName === THEME_ATTRIBUTE) {\n const updatedTheme = (mutation.target as HTMLHtmlElement).getAttribute(THEME_ATTRIBUTE);\n if (updatedTheme === 'light' || updatedTheme === 'dark') {\n window.localStorage.setItem(THEME_KEY, updatedTheme);\n setTheme(updatedTheme);\n } else {\n console.error(`Unexpected value for 'data-mode' attribute: ${updatedTheme}`);\n }\n }\n });\n });\n observer.observe(document.documentElement, {\n attributes: true\n });\n return () => observer.disconnect();\n }, []);\n\n // When the user wants to change the theme\n const updateTheme = (theme: Theme) => {\n document.documentElement.setAttribute(THEME_ATTRIBUTE, theme);\n };\n\n return [theme, updateTheme] as const;\n}\n\nexport { DEFAULT_THEME, SYS_DARK_MEDIA_QUERY, type Theme, THEME_ATTRIBUTE, THEME_KEY, useTheme };\n","import { useCallback } from 'react';\n\nimport { useStore } from 'zustand';\n\nimport { translationStore } from '@/i18n';\nimport type { TranslateFunction, TranslationNamespace } from '@/i18n';\nimport { getTranslation } from '@/i18n/internal';\n\nexport function useTranslation<TNamespace extends TranslationNamespace | undefined = undefined>(\n namespace?: TNamespace\n) {\n const changeLanguage = useStore(translationStore, (store) => store.changeLanguage);\n const fallbackLanguage = useStore(translationStore, (store) => store.fallbackLanguage);\n const resolvedLanguage = useStore(translationStore, (store) => store.resolvedLanguage);\n const translations = useStore(translationStore, (store) => {\n if (namespace) {\n return store.translations[namespace];\n }\n return store.translations;\n });\n\n const t: TranslateFunction<TNamespace> = useCallback(\n (target, ...args) => {\n return getTranslation(target, { fallbackLanguage, resolvedLanguage, translations }, ...args);\n },\n [fallbackLanguage, resolvedLanguage, translations]\n );\n\n return { changeLanguage, resolvedLanguage, t };\n}\n","import { useState } from 'react';\n\nimport { useEventListener } from '../useEventListener';\nimport { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect';\n\nexport type WindowSize = {\n height: number;\n width: number;\n};\n\nexport function useWindowSize(): WindowSize {\n const [windowSize, setWindowSize] = useState<WindowSize>({\n height: 0,\n width: 0\n });\n\n const handleSize = () => {\n setWindowSize({\n height: window.innerHeight,\n width: window.innerWidth\n });\n };\n\n useEventListener('resize', handleSize);\n\n // Set size at the first client-side load\n useIsomorphicLayoutEffect(() => {\n handleSize();\n }, []);\n\n return windowSize;\n}\n"],"mappings":"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","names":["useEffect","useEffect","useEffect","useRef","useRef","useEffect","useEffect","useRef","useRef","useEffect","useEffect","useState","query","useState","useEffect","useCallback","useEffect","useState","useCallback","useState","useEffect","useEffect","useState","useState","useEffect","theme","useCallback","useCallback","useState","useState"]}
@@ -2,7 +2,7 @@ import {
2
2
  useNotificationsStore,
3
3
  useTheme,
4
4
  useTranslation
5
- } from "./chunk-K2YKS4A5.js";
5
+ } from "./chunk-AFNJZUOE.js";
6
6
  import "./chunk-53GZFQK3.js";
7
7
  import {
8
8
  cn
package/dist/hooks.d.ts CHANGED
@@ -60,20 +60,23 @@ declare const useNotificationsStore: zustand.UseBoundStore<zustand.StoreApi<Noti
60
60
  type Handler = (event: MouseEvent) => void;
61
61
  declare function useOnClickOutside<T extends HTMLElement = HTMLElement>(ref: RefObject<T>, handler: Handler, mouseEvent?: 'mousedown' | 'mouseup'): void;
62
62
 
63
+ type StorageName = 'localStorage' | 'sessionStorage';
64
+ type StorageEventMap = {
65
+ [K in StorageName]: CustomEvent;
66
+ };
63
67
  declare global {
64
- interface WindowEventMap {
65
- 'session-storage': CustomEvent;
68
+ interface WindowEventMap extends StorageEventMap {
66
69
  }
67
70
  }
68
71
  /**
69
72
  * Represents the options for customizing the behavior of serialization and deserialization.
70
- * @template T - The type of the state to be stored in session storage.
73
+ * @template T - The type of the state to be stored in storage.
71
74
  */
72
- type UseSessionStorageOptions<T> = {
75
+ type UseStorageOptions<T> = {
73
76
  /** A function to deserialize the stored value. */
74
77
  deserializer?: (value: string) => T;
75
78
  /**
76
- * If `true` (default), the hook will initialize reading the session storage. In SSR, you should set it to `false`, returning the initial value initially.
79
+ * If `true` (default), the hook will initialize reading the storage. In SSR, you should set it to `false`, returning the initial value initially.
77
80
  * @default true
78
81
  */
79
82
  initializeWithValue?: boolean;
@@ -81,20 +84,26 @@ type UseSessionStorageOptions<T> = {
81
84
  serializer?: (value: T) => string;
82
85
  };
83
86
  /**
84
- * Custom hook that uses session storage to persist state across page reloads.
85
- * @template T - The type of the state to be stored in session storage.
86
- * @param {string} key - The key under which the value will be stored in session storage.
87
- * @param {T | (() => T)} initialValue - The initial value of the state or a function that returns the initial value.
88
- * @param {?UseSessionStorageOptions<T>} [options] - Options for customizing the behavior of serialization and deserialization (optional).
89
- * @returns {[T, Dispatch<SetStateAction<T>>]} A tuple containing the stored value and a function to set the value.
87
+ * Custom hook that uses local or session storage to persist state across page reloads.
88
+ * @template T - The type of the state to be stored in storage.
89
+ * @param key - The key under which the value will be stored in storage.
90
+ * @param initialValue - The initial value of the state or a function that returns the initial value.
91
+ * @param options - Options for customizing the behavior of serialization and deserialization (optional).
92
+ * @returns A tuple containing the stored value and a function to set the value.
90
93
  * @public
91
94
  * @example
92
95
  * ```tsx
93
- * const [count, setCount] = useSessionStorage('count', 0);
96
+ * const [count, setCount] = useStorage('count', 0);
94
97
  * // Access the `count` value and the `setCount` function to update it.
95
98
  * ```
96
99
  */
97
- declare function useSessionStorage<T>(key: string, initialValue: (() => T) | T, options?: UseSessionStorageOptions<T>): [T, Dispatch<SetStateAction<T>>];
100
+ declare function useStorage<T>(key: string, initialValue: (() => T) | T, storageName: StorageName, options?: UseStorageOptions<T>): [T, Dispatch<SetStateAction<T>>];
101
+
102
+ /** Custom hook that uses local storage to persist state across page reloads */
103
+ declare function useLocalStorage<T>(key: string, initialValue: (() => T) | T, options?: UseStorageOptions<T>): [T, Dispatch<SetStateAction<T>>];
104
+
105
+ /** Custom hook that uses session storage to persist state across page reloads */
106
+ declare function useSessionStorage<T>(key: string, initialValue: (() => T) | T, options?: UseStorageOptions<T>): [T, Dispatch<SetStateAction<T>>];
98
107
 
99
108
  type Theme = 'dark' | 'light';
100
109
  type UpdateTheme = (theme: Theme) => void;
@@ -126,4 +135,4 @@ type WindowSize = {
126
135
  };
127
136
  declare function useWindowSize(): WindowSize;
128
137
 
129
- export { DEFAULT_THEME, type NotificationInterface, type NotificationsStore, SYS_DARK_MEDIA_QUERY, THEME_ATTRIBUTE, THEME_KEY, type Theme, type WindowSize, useDownload, useEventCallback, useEventListener, useInterval, useIsomorphicLayoutEffect, useMediaQuery, useNotificationsStore, useOnClickOutside, useSessionStorage, useTheme, useTranslation, useWindowSize };
138
+ export { DEFAULT_THEME, type NotificationInterface, type NotificationsStore, SYS_DARK_MEDIA_QUERY, type StorageName, THEME_ATTRIBUTE, THEME_KEY, type Theme, type UseStorageOptions, type WindowSize, useDownload, useEventCallback, useEventListener, useInterval, useIsomorphicLayoutEffect, useLocalStorage, useMediaQuery, useNotificationsStore, useOnClickOutside, useSessionStorage, useStorage, useTheme, useTranslation, useWindowSize };
package/dist/hooks.js CHANGED
@@ -8,14 +8,16 @@ import {
8
8
  useEventListener,
9
9
  useInterval,
10
10
  useIsomorphicLayoutEffect,
11
+ useLocalStorage,
11
12
  useMediaQuery,
12
13
  useNotificationsStore,
13
14
  useOnClickOutside,
14
15
  useSessionStorage,
16
+ useStorage,
15
17
  useTheme,
16
18
  useTranslation,
17
19
  useWindowSize
18
- } from "./chunk-K2YKS4A5.js";
20
+ } from "./chunk-AFNJZUOE.js";
19
21
  import "./chunk-53GZFQK3.js";
20
22
  import "./chunk-IX6RFIQL.js";
21
23
  export {
@@ -28,10 +30,12 @@ export {
28
30
  useEventListener,
29
31
  useInterval,
30
32
  useIsomorphicLayoutEffect,
33
+ useLocalStorage,
31
34
  useMediaQuery,
32
35
  useNotificationsStore,
33
36
  useOnClickOutside,
34
37
  useSessionStorage,
38
+ useStorage,
35
39
  useTheme,
36
40
  useTranslation,
37
41
  useWindowSize
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@douglasneuroinformatics/libui",
3
3
  "type": "module",
4
- "version": "3.1.5",
4
+ "version": "3.2.0",
5
5
  "packageManager": "pnpm@9.3.0",
6
6
  "description": "Generic UI components for DNP projects, built using React and Tailwind CSS",
7
7
  "author": "Joshua Unrau",
@@ -6,7 +6,7 @@ export * from './useIsomorphicLayoutEffect';
6
6
  export * from './useMediaQuery';
7
7
  export * from './useNotificationsStore';
8
8
  export * from './useOnClickOutside';
9
- export * from './useSessionStorage';
9
+ export * from './useStorage';
10
10
  export * from './useTheme';
11
11
  export * from './useTranslation';
12
12
  export * from './useWindowSize';
@@ -0,0 +1,3 @@
1
+ export * from './useLocalStorage.ts';
2
+ export * from './useSessionStorage.ts';
3
+ export * from './useStorage.ts';
@@ -0,0 +1,16 @@
1
+ import { renderHook } from '@testing-library/react';
2
+ import { describe, it, vi } from 'vitest';
3
+
4
+ import { mockStorage } from '@/testing/mocks';
5
+
6
+ import { useLocalStorage } from './useLocalStorage';
7
+
8
+ mockStorage('localStorage');
9
+
10
+ vi.mock('@/utils', () => ({ isBrowser: vi.fn(() => true) }));
11
+
12
+ describe('useLocalStorage()', () => {
13
+ it('should render', () => {
14
+ renderHook(() => useLocalStorage('key', 'value'));
15
+ });
16
+ });
@@ -0,0 +1,12 @@
1
+ import type { Dispatch, SetStateAction } from 'react';
2
+
3
+ import { useStorage, type UseStorageOptions } from './useStorage';
4
+
5
+ /** Custom hook that uses local storage to persist state across page reloads */
6
+ export function useLocalStorage<T>(
7
+ key: string,
8
+ initialValue: (() => T) | T,
9
+ options: UseStorageOptions<T> = {}
10
+ ): [T, Dispatch<SetStateAction<T>>] {
11
+ return useStorage(key, initialValue, 'localStorage', options);
12
+ }
@@ -0,0 +1,16 @@
1
+ import { renderHook } from '@testing-library/react';
2
+ import { describe, it, vi } from 'vitest';
3
+
4
+ import { mockStorage } from '@/testing/mocks';
5
+
6
+ import { useSessionStorage } from './useSessionStorage';
7
+
8
+ mockStorage('sessionStorage');
9
+
10
+ vi.mock('@/utils', () => ({ isBrowser: vi.fn(() => true) }));
11
+
12
+ describe('useSessionStorage()', () => {
13
+ it('should render', () => {
14
+ renderHook(() => useSessionStorage('key', 'value'));
15
+ });
16
+ });
@@ -0,0 +1,12 @@
1
+ import type { Dispatch, SetStateAction } from 'react';
2
+
3
+ import { useStorage, type UseStorageOptions } from './useStorage';
4
+
5
+ /** Custom hook that uses session storage to persist state across page reloads */
6
+ export function useSessionStorage<T>(
7
+ key: string,
8
+ initialValue: (() => T) | T,
9
+ options: UseStorageOptions<T> = {}
10
+ ): [T, Dispatch<SetStateAction<T>>] {
11
+ return useStorage(key, initialValue, 'sessionStorage', options);
12
+ }
@@ -0,0 +1,193 @@
1
+ import { act, renderHook } from '@testing-library/react';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { mockStorage } from '@/testing/mocks';
5
+ import { isBrowser } from '@/utils';
6
+
7
+ import { useStorage } from './useStorage';
8
+
9
+ import type { StorageName } from './useStorage';
10
+
11
+ const storages: StorageName[] = ['localStorage', 'sessionStorage'];
12
+ storages.forEach(mockStorage);
13
+
14
+ vi.mock('@/utils', () => ({ isBrowser: vi.fn(() => true) }));
15
+
16
+ describe('useStorage()', () => {
17
+ storages.forEach((storageName) => {
18
+ const storage = window[storageName];
19
+
20
+ beforeEach(() => {
21
+ window[storageName].clear();
22
+ });
23
+
24
+ afterEach(() => {
25
+ vi.restoreAllMocks();
26
+ });
27
+ it('should return the initial state if running on the server', () => {
28
+ vi.mocked(isBrowser).mockReturnValue(false);
29
+ const { result } = renderHook(() => useStorage('key', 'value', storageName));
30
+ expect(result.current[0]).toBe('value');
31
+ });
32
+
33
+ it('should return the initial state if using a string', () => {
34
+ const { result } = renderHook(() => useStorage('key', 'value', storageName));
35
+ expect(result.current[0]).toBe('value');
36
+ });
37
+
38
+ it('return the initial state return if using a callback function', () => {
39
+ const { result } = renderHook(() => useStorage('key', () => 'value', storageName));
40
+ expect(result.current[0]).toBe('value');
41
+ });
42
+
43
+ it('return the initial state if using a string', () => {
44
+ const { result } = renderHook(() => useStorage('digits', [1, 2], storageName));
45
+ expect(result.current[0]).toEqual([1, 2]);
46
+ });
47
+
48
+ it('return the initial state if it is a map', () => {
49
+ const { result } = renderHook(() => useStorage('map', new Map([['a', 1]]), storageName));
50
+ expect(result.current[0]).toEqual(new Map([['a', 1]]));
51
+ });
52
+
53
+ it('return the initial state if it is a set', () => {
54
+ const { result } = renderHook(() => useStorage('set', new Set([1, 2]), storageName));
55
+ expect(result.current[0]).toEqual(new Set([1, 2]));
56
+ });
57
+
58
+ it('return the initial state if it is a date', () => {
59
+ const { result } = renderHook(() => useStorage('date', new Date(2020, 1, 1), storageName));
60
+ expect(result.current[0]).toEqual(new Date(2020, 1, 1));
61
+ });
62
+
63
+ it('should update the state and write to session storage', () => {
64
+ const { result } = renderHook(() => useStorage('key', 'value', storageName));
65
+ act(() => {
66
+ const setState = result.current[1];
67
+ setState('edited');
68
+ });
69
+ expect(result.current[0]).toBe('edited');
70
+ expect(storage.getItem('key')).toBe(JSON.stringify('edited'));
71
+ });
72
+
73
+ it('should update the state with undefined', () => {
74
+ const { result } = renderHook(() => useStorage<string | undefined>('keytest', 'value', storageName));
75
+ act(() => {
76
+ const setState = result.current[1];
77
+ setState(undefined);
78
+ });
79
+ expect(result.current[0]).toBeUndefined();
80
+ });
81
+
82
+ it('should update the state with a callback function', () => {
83
+ const { result } = renderHook(() => useStorage('count', 2, storageName));
84
+ act(() => {
85
+ const setState = result.current[1];
86
+ setState((prev) => prev + 1);
87
+ });
88
+ expect(result.current[0]).toBe(3);
89
+ expect(storage.getItem('count')).toEqual('3');
90
+ });
91
+
92
+ it('should update the state with a callback function multiple times per render', () => {
93
+ const { result } = renderHook(() => useStorage('count', 2, storageName));
94
+ act(() => {
95
+ const setState = result.current[1];
96
+ setState((prev) => prev + 1);
97
+ setState((prev) => prev + 1);
98
+ setState((prev) => prev + 1);
99
+ });
100
+ expect(result.current[0]).toBe(5);
101
+ expect(storage.getItem('count')).toEqual('5');
102
+ });
103
+
104
+ it('should update if another hook updates the same key', () => {
105
+ const initialValues: [string, unknown] = ['key', 'initial'];
106
+ const { result: A } = renderHook(() => useStorage(...initialValues, storageName));
107
+ const { result: B } = renderHook(() => useStorage(...initialValues, storageName));
108
+ const { result: C } = renderHook(() => useStorage('other-key', 'initial', storageName));
109
+
110
+ act(() => {
111
+ const setState = A.current[1];
112
+ setState('edited');
113
+ });
114
+
115
+ expect(B.current[0]).toBe('edited');
116
+ expect(C.current[0]).toBe('initial');
117
+ });
118
+
119
+ it('should not update if another hook updates a different key', () => {
120
+ let renderCount = 0;
121
+ const { result: A } = renderHook(() => {
122
+ renderCount++;
123
+ return useStorage('key1', {}, storageName);
124
+ });
125
+ const { result: B } = renderHook(() => useStorage('key2', 'initial', storageName));
126
+ expect(renderCount).toBe(1);
127
+ act(() => {
128
+ const setStateA = A.current[1];
129
+ setStateA({ a: 1 });
130
+ });
131
+ expect(renderCount).toBe(2);
132
+ act(() => {
133
+ const setStateB = B.current[1];
134
+ setStateB('edited');
135
+ });
136
+ expect(renderCount).toBe(2);
137
+ });
138
+
139
+ it('should return a referentially stable setter', () => {
140
+ const { result } = renderHook(() => useStorage('count', 1, storageName));
141
+ const originalCallback = result.current[1];
142
+ act(() => {
143
+ const setState = result.current[1];
144
+ setState((prev) => prev + 1);
145
+ });
146
+ expect(result.current[1] === originalCallback).toBe(true);
147
+ });
148
+
149
+ it('should use the default JSON.stringify and JSON.parse when serializer/deserializer are not provided', () => {
150
+ const { result } = renderHook(() => useStorage('key', 'initialValue', storageName));
151
+ act(() => {
152
+ result.current[1]('newValue');
153
+ });
154
+ expect(storage.getItem('key')).toBe(JSON.stringify('newValue'));
155
+ });
156
+
157
+ it('should write to stderr and set the store to the initial value if the stored value in not serializable', () => {
158
+ vi.spyOn(console, 'error');
159
+ storage.setItem('key', '{{ x: foo }}');
160
+ renderHook(() => useStorage('key', 'initialValue', storageName));
161
+ expect(console.error).toHaveBeenLastCalledWith(expect.stringContaining('Error parsing JSON'));
162
+ });
163
+
164
+ it('should use a custom serializer and deserializer when provided', () => {
165
+ const serializer = (value: string) => value.toUpperCase();
166
+ const deserializer = (value: string) => value.toLowerCase();
167
+ const { result } = renderHook(() => useStorage('key', 'initialValue', storageName, { deserializer, serializer }));
168
+ act(() => {
169
+ result.current[1]('NewValue');
170
+ });
171
+ expect(storage.getItem('key')).toBe('NEWVALUE');
172
+ });
173
+
174
+ it('should handle undefined values with custom deserializer', () => {
175
+ const serializer = (value: number | undefined) => String(value);
176
+ const deserializer = (value: string) => (value === 'undefined' ? undefined : Number(value));
177
+ const { result } = renderHook(() =>
178
+ useStorage<number | undefined>('key', 0, storageName, {
179
+ deserializer,
180
+ serializer
181
+ })
182
+ );
183
+ act(() => {
184
+ result.current[1](undefined);
185
+ });
186
+ expect(storage.getItem('key')).toBe('undefined');
187
+ act(() => {
188
+ result.current[1](42);
189
+ });
190
+ expect(storage.getItem('key')).toBe('42');
191
+ });
192
+ });
193
+ });
@@ -6,22 +6,26 @@ import { isBrowser } from '@/utils';
6
6
  import { useEventCallback } from '../useEventCallback';
7
7
  import { useEventListener } from '../useEventListener';
8
8
 
9
+ type StorageName = 'localStorage' | 'sessionStorage';
10
+
11
+ type StorageEventMap = {
12
+ [K in StorageName]: CustomEvent;
13
+ };
14
+
9
15
  declare global {
10
- // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
11
- interface WindowEventMap {
12
- 'session-storage': CustomEvent;
13
- }
16
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions, @typescript-eslint/no-empty-object-type
17
+ interface WindowEventMap extends StorageEventMap {}
14
18
  }
15
19
 
16
20
  /**
17
21
  * Represents the options for customizing the behavior of serialization and deserialization.
18
- * @template T - The type of the state to be stored in session storage.
22
+ * @template T - The type of the state to be stored in storage.
19
23
  */
20
- type UseSessionStorageOptions<T> = {
24
+ type UseStorageOptions<T> = {
21
25
  /** A function to deserialize the stored value. */
22
26
  deserializer?: (value: string) => T;
23
27
  /**
24
- * If `true` (default), the hook will initialize reading the session storage. In SSR, you should set it to `false`, returning the initial value initially.
28
+ * If `true` (default), the hook will initialize reading the storage. In SSR, you should set it to `false`, returning the initial value initially.
25
29
  * @default true
26
30
  */
27
31
  initializeWithValue?: boolean;
@@ -30,25 +34,27 @@ type UseSessionStorageOptions<T> = {
30
34
  };
31
35
 
32
36
  /**
33
- * Custom hook that uses session storage to persist state across page reloads.
34
- * @template T - The type of the state to be stored in session storage.
35
- * @param {string} key - The key under which the value will be stored in session storage.
36
- * @param {T | (() => T)} initialValue - The initial value of the state or a function that returns the initial value.
37
- * @param {?UseSessionStorageOptions<T>} [options] - Options for customizing the behavior of serialization and deserialization (optional).
38
- * @returns {[T, Dispatch<SetStateAction<T>>]} A tuple containing the stored value and a function to set the value.
37
+ * Custom hook that uses local or session storage to persist state across page reloads.
38
+ * @template T - The type of the state to be stored in storage.
39
+ * @param key - The key under which the value will be stored in storage.
40
+ * @param initialValue - The initial value of the state or a function that returns the initial value.
41
+ * @param options - Options for customizing the behavior of serialization and deserialization (optional).
42
+ * @returns A tuple containing the stored value and a function to set the value.
39
43
  * @public
40
44
  * @example
41
45
  * ```tsx
42
- * const [count, setCount] = useSessionStorage('count', 0);
46
+ * const [count, setCount] = useStorage('count', 0);
43
47
  * // Access the `count` value and the `setCount` function to update it.
44
48
  * ```
45
49
  */
46
- export function useSessionStorage<T>(
50
+ export function useStorage<T>(
47
51
  key: string,
48
52
  initialValue: (() => T) | T,
49
- options: UseSessionStorageOptions<T> = {}
53
+ storageName: StorageName,
54
+ options: UseStorageOptions<T> = {}
50
55
  ): [T, Dispatch<SetStateAction<T>>] {
51
56
  const { initializeWithValue = true } = options;
57
+ const storage = window[storageName];
52
58
 
53
59
  const serializer = useCallback<(value: T) => string>(
54
60
  (value) => {
@@ -64,14 +70,10 @@ export function useSessionStorage<T>(
64
70
  (value) => {
65
71
  if (options.deserializer) {
66
72
  return options.deserializer(value);
67
- }
68
- // Support 'undefined' as a value
69
- if (value === 'undefined') {
73
+ } else if (value === 'undefined') {
70
74
  return undefined as unknown as T;
71
75
  }
72
-
73
76
  const defaultValue = initialValue instanceof Function ? initialValue() : initialValue;
74
-
75
77
  let parsed: unknown;
76
78
  try {
77
79
  parsed = JSON.parse(value);
@@ -79,22 +81,17 @@ export function useSessionStorage<T>(
79
81
  console.error(`Error parsing JSON: ${(err as Error).message}`);
80
82
  return defaultValue;
81
83
  }
82
-
83
84
  return parsed as T;
84
85
  },
85
86
  [options, initialValue]
86
87
  );
87
88
 
88
- // Get from session storage then
89
- // parse stored json or return initialValue
90
89
  const readValue = useCallback((): T => {
91
90
  const initialValueToUse = initialValue instanceof Function ? initialValue() : initialValue;
92
-
93
- // Prevent build error "window is undefined" but keep keep working
94
91
  if (!isBrowser()) {
95
92
  return initialValueToUse;
96
93
  }
97
- const raw = window.sessionStorage.getItem(key);
94
+ const raw = storage.getItem(key);
98
95
  return raw ? deserializer(raw) : initialValueToUse;
99
96
  }, [initialValue, key, deserializer]);
100
97
 
@@ -102,32 +99,20 @@ export function useSessionStorage<T>(
102
99
  if (initializeWithValue) {
103
100
  return readValue();
104
101
  }
105
-
106
102
  return initialValue instanceof Function ? initialValue() : initialValue;
107
103
  });
108
104
 
109
- // Return a wrapped version of useState's setter function that ...
110
- // ... persists the new value to sessionStorage.
111
105
  const setValue: Dispatch<SetStateAction<T>> = useEventCallback((value) => {
112
- // Prevent build error "window is undefined" but keeps working
113
106
  if (!isBrowser()) {
114
- console.warn(`Tried setting sessionStorage key “${key}” even though environment is not a client`);
107
+ console.warn(`Tried setting storage key “${key}” even though environment is not a client`);
115
108
  }
116
-
117
109
  try {
118
- // Allow value to be a function so we have the same API as useState
119
110
  const newValue = value instanceof Function ? value(readValue()) : value;
120
-
121
- // Save to session storage
122
- window.sessionStorage.setItem(key, serializer(newValue));
123
-
124
- // Save state
111
+ storage.setItem(key, serializer(newValue));
125
112
  setStoredValue(newValue);
126
-
127
- // We dispatch a custom event so every similar useSessionStorage hook is notified
128
- window.dispatchEvent(new StorageEvent('session-storage', { key }));
113
+ window.dispatchEvent(new StorageEvent(storageName, { key }));
129
114
  } catch (error) {
130
- console.warn(`Error setting sessionStorage key “${key}”:`, error);
115
+ console.warn(`Error setting storage key “${key}”:`, error);
131
116
  }
132
117
  });
133
118
 
@@ -148,9 +133,9 @@ export function useSessionStorage<T>(
148
133
  // this only works for other documents, not the current one
149
134
  useEventListener('storage', handleStorageChange);
150
135
 
151
- // this is a custom event, triggered in writeValueToSessionStorage
152
- // See: useSessionStorage()
153
- useEventListener('session-storage', handleStorageChange);
136
+ useEventListener(storageName, handleStorageChange);
154
137
 
155
138
  return [storedValue, setValue];
156
139
  }
140
+
141
+ export type { StorageName, UseStorageOptions };
@@ -1,6 +1,8 @@
1
1
  import { faker } from '@faker-js/faker';
2
2
  import { vi } from 'vitest';
3
3
 
4
+ import type { StorageName } from '@/hooks';
5
+
4
6
  /**
5
7
  * Mocks the matchMedia API
6
8
  * @param {boolean} matches - whether the media query matches
@@ -22,12 +24,9 @@ export const mockMatchMedia = (matches: ((query: string) => boolean) | boolean):
22
24
 
23
25
  /**
24
26
  * Mocks the Storage API
25
- * @param {'localStorage' | 'sessionStorage'} name - The name of the storage to mock
26
- * @example
27
- * mockStorage('localStorage')
28
- * // Then use window.localStorage as usual (it will be mocked)
27
+ * @param name - The name of the storage to mock
29
28
  */
30
- export const mockStorage = (name: 'localStorage' | 'sessionStorage'): void => {
29
+ export const mockStorage = (name: StorageName): void => {
31
30
  class StorageMock implements Omit<Storage, 'key' | 'length'> {
32
31
  store: { [key: string]: string } = {};
33
32
 
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/hooks/useDownload/useDownload.ts","../src/hooks/useNotificationsStore/useNotificationsStore.ts","../src/hooks/useEventCallback/useEventCallback.ts","../src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.ts","../src/hooks/useEventListener/useEventListener.ts","../src/hooks/useInterval/useInterval.ts","../src/hooks/useMediaQuery/useMediaQuery.ts","../src/hooks/useOnClickOutside/useOnClickOutside.ts","../src/hooks/useSessionStorage/useSessionStorage.ts","../src/hooks/useTheme/useTheme.ts","../src/hooks/useTranslation/useTranslation.ts","../src/hooks/useWindowSize/useWindowSize.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\n\nimport type { Promisable } from 'type-fest';\n\nimport { useNotificationsStore } from '../useNotificationsStore';\n\ntype DownloadTextOptions = {\n blobType: 'text/csv' | 'text/plain';\n};\n\ntype DownloadBlobOptions = {\n blobType: 'application/zip' | 'image/jpeg' | 'image/png' | 'image/webp';\n};\n\n// eslint-disable-next-line @typescript-eslint/consistent-type-definitions\ninterface DownloadFunction {\n (filename: string, data: Blob, options: DownloadBlobOptions): Promise<void>;\n (filename: string, data: () => Promisable<Blob>, options: DownloadBlobOptions): Promise<void>;\n (filename: string, data: string, options?: DownloadTextOptions): Promise<void>;\n (filename: string, data: () => Promisable<string>, options?: DownloadTextOptions): Promise<void>;\n}\n\n/**\n * Used to trigger downloads of arbitrary data to the client\n * @returns A function to invoke the download\n */\nexport function useDownload(): DownloadFunction {\n const notifications = useNotificationsStore();\n const [state, setState] = useState<{\n blobType: string;\n data: Blob | string;\n filename: string;\n } | null>(null);\n\n useEffect(() => {\n if (state) {\n const { blobType, data, filename } = state;\n const anchor = document.createElement('a');\n document.body.appendChild(anchor);\n\n const blob = new Blob([data], { type: blobType });\n\n const url = URL.createObjectURL(blob);\n anchor.href = url;\n anchor.download = filename;\n anchor.click();\n URL.revokeObjectURL(url);\n anchor.remove();\n setState(null);\n }\n }, [state]);\n\n return async (filename, _data, options) => {\n try {\n const data = typeof _data === 'function' ? await _data() : _data;\n if (typeof data !== 'string' && !options?.blobType) {\n throw new Error(\"argument 'blobType' must be defined when download is called with a Blob object\");\n }\n setState({ blobType: options?.blobType ?? 'text/plain', data, filename });\n } catch (error) {\n const message = error instanceof Error ? error.message : 'An unknown error occurred';\n notifications.addNotification({\n message,\n title: 'Error',\n type: 'error'\n });\n }\n };\n}\n","import { create } from 'zustand';\n\nexport type NotificationInterface = {\n id: number;\n message?: string;\n title?: string;\n type: 'error' | 'info' | 'success' | 'warning';\n variant?: 'critical' | 'standard';\n};\n\nexport type NotificationsStore = {\n addNotification: (notification: Omit<NotificationInterface, 'id'>) => void;\n dismissNotification: (id: number) => void;\n notifications: NotificationInterface[];\n};\n\nexport const useNotificationsStore = create<NotificationsStore>((set) => ({\n addNotification: (notification) => {\n set((state) => ({\n notifications: [...state.notifications, { id: Date.now(), ...notification }]\n }));\n },\n dismissNotification: (id) => {\n set((state) => ({\n notifications: state.notifications.filter((notification) => notification.id !== id)\n }));\n },\n notifications: []\n}));\n","import { useCallback, useRef } from 'react';\n\nimport { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect';\n\nexport function useEventCallback<Args extends unknown[], R>(fn: (...args: Args) => R) {\n const ref = useRef<typeof fn>(() => {\n throw new Error('Cannot call an event handler while rendering.');\n });\n\n useIsomorphicLayoutEffect(() => {\n ref.current = fn;\n }, [fn]);\n\n return useCallback((...args: Args) => ref.current(...args), [ref]);\n}\n","import { useEffect, useLayoutEffect } from 'react';\n\nimport { isBrowser } from '@/utils';\n\nexport const useIsomorphicLayoutEffect = isBrowser() ? useLayoutEffect : useEffect;\n","import { type RefObject, useEffect, useRef } from 'react';\n\nimport { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect';\n\n// MediaQueryList Event based useEventListener interface\nfunction useEventListener<K extends keyof MediaQueryListEventMap>(\n eventName: K,\n handler: (event: MediaQueryListEventMap[K]) => void,\n element: RefObject<MediaQueryList>,\n options?: AddEventListenerOptions | boolean\n): void;\n\n// Window Event based useEventListener interface\nfunction useEventListener<K extends keyof WindowEventMap>(\n eventName: K,\n handler: (event: WindowEventMap[K]) => void,\n element?: undefined,\n options?: AddEventListenerOptions | boolean\n): void;\n\n// Element Event based useEventListener interface\nfunction useEventListener<K extends keyof HTMLElementEventMap, T extends HTMLElement = HTMLDivElement>(\n eventName: K,\n handler: (event: HTMLElementEventMap[K]) => void,\n element: RefObject<T>,\n options?: AddEventListenerOptions | boolean\n): void;\n\n// Document Event based useEventListener interface\nfunction useEventListener<K extends keyof DocumentEventMap>(\n eventName: K,\n handler: (event: DocumentEventMap[K]) => void,\n element: RefObject<Document>,\n options?: AddEventListenerOptions | boolean\n): void;\n\nfunction useEventListener<\n KW extends keyof WindowEventMap,\n KH extends keyof HTMLElementEventMap,\n KM extends keyof MediaQueryListEventMap,\n T extends HTMLElement | MediaQueryList | void = void\n>(\n eventName: KH | KM | KW,\n handler: (event: Event | HTMLElementEventMap[KH] | MediaQueryListEventMap[KM] | WindowEventMap[KW]) => void,\n element?: RefObject<T>,\n options?: AddEventListenerOptions | boolean\n) {\n // Create a ref that stores handler\n const savedHandler = useRef(handler);\n\n useIsomorphicLayoutEffect(() => {\n savedHandler.current = handler;\n }, [handler]);\n\n useEffect(() => {\n // Define the listening target\n const targetElement: T | Window = element?.current ?? window;\n\n if (!(targetElement && targetElement.addEventListener)) return;\n\n // Create event listener that calls handler function stored in ref\n const listener: typeof handler = (event) => savedHandler.current(event);\n\n targetElement.addEventListener(eventName, listener, options);\n\n // Remove event listener on cleanup\n return () => {\n targetElement.removeEventListener(eventName, listener, options);\n };\n }, [eventName, element, options]);\n}\n\nexport { useEventListener };\n","import { useEffect, useRef } from 'react';\n\nimport { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect';\n\nexport function useInterval(callback: () => void, delay: null | number) {\n const savedCallback = useRef(callback);\n\n // Remember the latest callback if it changes.\n useIsomorphicLayoutEffect(() => {\n savedCallback.current = callback;\n }, [callback]);\n\n // Set up the interval.\n useEffect(() => {\n // Don't schedule if no delay is specified.\n // Note: 0 is a valid value for delay.\n if (!delay && delay !== 0) {\n return;\n }\n\n const id = setInterval(() => savedCallback.current(), delay);\n\n return () => clearInterval(id);\n }, [delay]);\n}\n","import { useEffect, useState } from 'react';\n\nimport { isBrowser } from '@/utils';\n\n/**\n * Get the result of an arbitrary CSS media query\n *\n * @param query - the CSS media query\n * @returns a boolean indicating the result of the query\n * @example\n * // true if the viewport is at least 768px wide\n * const matches = useMediaQuery('(min-width: 768px)')\n */\nexport function useMediaQuery(query: string): boolean {\n const getMatches = (query: string): boolean => {\n // Prevents SSR issues\n if (isBrowser()) {\n return window.matchMedia(query).matches;\n }\n return false;\n };\n\n const [matches, setMatches] = useState<boolean>(getMatches(query));\n\n function handleChange() {\n setMatches(getMatches(query));\n }\n\n useEffect(() => {\n const matchMedia = window.matchMedia(query);\n\n // Triggered at the first client-side load and if query changes\n handleChange();\n\n matchMedia.addEventListener('change', handleChange);\n\n return () => {\n matchMedia.removeEventListener('change', handleChange);\n };\n }, [query]);\n\n return matches;\n}\n","import { type RefObject } from 'react';\n\nimport { useEventListener } from '../useEventListener';\n\ntype Handler = (event: MouseEvent) => void;\n\nexport function useOnClickOutside<T extends HTMLElement = HTMLElement>(\n ref: RefObject<T>,\n handler: Handler,\n mouseEvent: 'mousedown' | 'mouseup' = 'mousedown'\n): void {\n useEventListener(mouseEvent, (event) => {\n const el = ref.current;\n\n // Do nothing if clicking ref's element or descendent elements\n if (!el || el.contains(event.target as Node)) {\n return;\n }\n\n handler(event);\n });\n}\n","import { useCallback, useEffect, useState } from 'react';\nimport type { Dispatch, SetStateAction } from 'react';\n\nimport { isBrowser } from '@/utils';\n\nimport { useEventCallback } from '../useEventCallback';\nimport { useEventListener } from '../useEventListener';\n\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/consistent-type-definitions\n interface WindowEventMap {\n 'session-storage': CustomEvent;\n }\n}\n\n/**\n * Represents the options for customizing the behavior of serialization and deserialization.\n * @template T - The type of the state to be stored in session storage.\n */\ntype UseSessionStorageOptions<T> = {\n /** A function to deserialize the stored value. */\n deserializer?: (value: string) => T;\n /**\n * If `true` (default), the hook will initialize reading the session storage. In SSR, you should set it to `false`, returning the initial value initially.\n * @default true\n */\n initializeWithValue?: boolean;\n /** A function to serialize the value before storing it. */\n serializer?: (value: T) => string;\n};\n\n/**\n * Custom hook that uses session storage to persist state across page reloads.\n * @template T - The type of the state to be stored in session storage.\n * @param {string} key - The key under which the value will be stored in session storage.\n * @param {T | (() => T)} initialValue - The initial value of the state or a function that returns the initial value.\n * @param {?UseSessionStorageOptions<T>} [options] - Options for customizing the behavior of serialization and deserialization (optional).\n * @returns {[T, Dispatch<SetStateAction<T>>]} A tuple containing the stored value and a function to set the value.\n * @public\n * @example\n * ```tsx\n * const [count, setCount] = useSessionStorage('count', 0);\n * // Access the `count` value and the `setCount` function to update it.\n * ```\n */\nexport function useSessionStorage<T>(\n key: string,\n initialValue: (() => T) | T,\n options: UseSessionStorageOptions<T> = {}\n): [T, Dispatch<SetStateAction<T>>] {\n const { initializeWithValue = true } = options;\n\n const serializer = useCallback<(value: T) => string>(\n (value) => {\n if (options.serializer) {\n return options.serializer(value);\n }\n return JSON.stringify(value);\n },\n [options]\n );\n\n const deserializer = useCallback<(value: string) => T>(\n (value) => {\n if (options.deserializer) {\n return options.deserializer(value);\n }\n // Support 'undefined' as a value\n if (value === 'undefined') {\n return undefined as unknown as T;\n }\n\n const defaultValue = initialValue instanceof Function ? initialValue() : initialValue;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(value);\n } catch (err) {\n console.error(`Error parsing JSON: ${(err as Error).message}`);\n return defaultValue;\n }\n\n return parsed as T;\n },\n [options, initialValue]\n );\n\n // Get from session storage then\n // parse stored json or return initialValue\n const readValue = useCallback((): T => {\n const initialValueToUse = initialValue instanceof Function ? initialValue() : initialValue;\n\n // Prevent build error \"window is undefined\" but keep keep working\n if (!isBrowser()) {\n return initialValueToUse;\n }\n const raw = window.sessionStorage.getItem(key);\n return raw ? deserializer(raw) : initialValueToUse;\n }, [initialValue, key, deserializer]);\n\n const [storedValue, setStoredValue] = useState(() => {\n if (initializeWithValue) {\n return readValue();\n }\n\n return initialValue instanceof Function ? initialValue() : initialValue;\n });\n\n // Return a wrapped version of useState's setter function that ...\n // ... persists the new value to sessionStorage.\n const setValue: Dispatch<SetStateAction<T>> = useEventCallback((value) => {\n // Prevent build error \"window is undefined\" but keeps working\n if (!isBrowser()) {\n console.warn(`Tried setting sessionStorage key “${key}” even though environment is not a client`);\n }\n\n try {\n // Allow value to be a function so we have the same API as useState\n const newValue = value instanceof Function ? value(readValue()) : value;\n\n // Save to session storage\n window.sessionStorage.setItem(key, serializer(newValue));\n\n // Save state\n setStoredValue(newValue);\n\n // We dispatch a custom event so every similar useSessionStorage hook is notified\n window.dispatchEvent(new StorageEvent('session-storage', { key }));\n } catch (error) {\n console.warn(`Error setting sessionStorage key “${key}”:`, error);\n }\n });\n\n useEffect(() => {\n setStoredValue(readValue());\n }, [key]);\n\n const handleStorageChange = useCallback(\n (event: CustomEvent | StorageEvent) => {\n if ((event as StorageEvent).key && (event as StorageEvent).key !== key) {\n return;\n }\n setStoredValue(readValue());\n },\n [key, readValue]\n );\n\n // this only works for other documents, not the current one\n useEventListener('storage', handleStorageChange);\n\n // this is a custom event, triggered in writeValueToSessionStorage\n // See: useSessionStorage()\n useEventListener('session-storage', handleStorageChange);\n\n return [storedValue, setValue];\n}\n","import { useEffect, useState } from 'react';\n\n// this is required since our storybook manager plugin cannot use vite aliases\nimport { isBrowser } from '../../utils';\n\ntype Theme = 'dark' | 'light';\n\ntype UpdateTheme = (theme: Theme) => void;\n\n/** @private */\nconst DEFAULT_THEME: Theme = 'light';\n\n/** @private */\nconst THEME_ATTRIBUTE = 'data-mode';\n\n/** @private */\nconst THEME_KEY = 'theme';\n\n/** @private */\nconst SYS_DARK_MEDIA_QUERY = '(prefers-color-scheme: dark)';\n\n/**\n * Returns the current theme and a function to update the current theme\n *\n * The reason the implementation of this hook is rather convoluted is for\n * cases where the theme is updated outside this hook\n */\nfunction useTheme(): readonly [Theme, UpdateTheme] {\n // Initial theme value is based on the value saved in local storage or the system theme\n const [theme, setTheme] = useState<Theme>(() => {\n if (!isBrowser()) {\n return DEFAULT_THEME;\n }\n const savedTheme = window.localStorage.getItem(THEME_KEY);\n let initialTheme: Theme;\n if (savedTheme === 'dark' || savedTheme === 'light') {\n initialTheme = savedTheme;\n } else {\n initialTheme = window.matchMedia(SYS_DARK_MEDIA_QUERY).matches ? 'dark' : 'light';\n }\n document.documentElement.setAttribute(THEME_ATTRIBUTE, initialTheme);\n return initialTheme;\n });\n\n useEffect(() => {\n const observer = new MutationObserver((mutations) => {\n mutations.forEach((mutation) => {\n if (mutation.attributeName === THEME_ATTRIBUTE) {\n const updatedTheme = (mutation.target as HTMLHtmlElement).getAttribute(THEME_ATTRIBUTE);\n if (updatedTheme === 'light' || updatedTheme === 'dark') {\n window.localStorage.setItem(THEME_KEY, updatedTheme);\n setTheme(updatedTheme);\n } else {\n console.error(`Unexpected value for 'data-mode' attribute: ${updatedTheme}`);\n }\n }\n });\n });\n observer.observe(document.documentElement, {\n attributes: true\n });\n return () => observer.disconnect();\n }, []);\n\n // When the user wants to change the theme\n const updateTheme = (theme: Theme) => {\n document.documentElement.setAttribute(THEME_ATTRIBUTE, theme);\n };\n\n return [theme, updateTheme] as const;\n}\n\nexport { DEFAULT_THEME, SYS_DARK_MEDIA_QUERY, type Theme, THEME_ATTRIBUTE, THEME_KEY, useTheme };\n","import { useCallback } from 'react';\n\nimport { useStore } from 'zustand';\n\nimport { translationStore } from '@/i18n';\nimport type { TranslateFunction, TranslationNamespace } from '@/i18n';\nimport { getTranslation } from '@/i18n/internal';\n\nexport function useTranslation<TNamespace extends TranslationNamespace | undefined = undefined>(\n namespace?: TNamespace\n) {\n const changeLanguage = useStore(translationStore, (store) => store.changeLanguage);\n const fallbackLanguage = useStore(translationStore, (store) => store.fallbackLanguage);\n const resolvedLanguage = useStore(translationStore, (store) => store.resolvedLanguage);\n const translations = useStore(translationStore, (store) => {\n if (namespace) {\n return store.translations[namespace];\n }\n return store.translations;\n });\n\n const t: TranslateFunction<TNamespace> = useCallback(\n (target, ...args) => {\n return getTranslation(target, { fallbackLanguage, resolvedLanguage, translations }, ...args);\n },\n [fallbackLanguage, resolvedLanguage, translations]\n );\n\n return { changeLanguage, resolvedLanguage, t };\n}\n","import { useState } from 'react';\n\nimport { useEventListener } from '../useEventListener';\nimport { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect';\n\nexport type WindowSize = {\n height: number;\n width: number;\n};\n\nexport function useWindowSize(): WindowSize {\n const [windowSize, setWindowSize] = useState<WindowSize>({\n height: 0,\n width: 0\n });\n\n const handleSize = () => {\n setWindowSize({\n height: window.innerHeight,\n width: window.innerWidth\n });\n };\n\n useEventListener('resize', handleSize);\n\n // Set size at the first client-side load\n useIsomorphicLayoutEffect(() => {\n handleSize();\n }, []);\n\n return windowSize;\n}\n"],"mappings":"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","names":["useEffect","useEffect","useEffect","useRef","useRef","useEffect","useEffect","useRef","useRef","useEffect","useEffect","useState","query","useState","useEffect","useCallback","useEffect","useState","useCallback","useState","useEffect","useEffect","useState","useState","useEffect","theme","useCallback","useCallback","useState","useState"]}
@@ -1 +0,0 @@
1
- export * from './useSessionStorage';
@@ -1,187 +0,0 @@
1
- import { act, renderHook } from '@testing-library/react';
2
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
-
4
- import { mockStorage } from '@/testing/mocks';
5
- import { isBrowser } from '@/utils';
6
-
7
- import { useSessionStorage } from './useSessionStorage';
8
-
9
- mockStorage('sessionStorage');
10
-
11
- vi.mock('@/utils', () => ({ isBrowser: vi.fn(() => true) }));
12
-
13
- describe('useSessionStorage()', () => {
14
- beforeEach(() => {
15
- window.sessionStorage.clear();
16
- });
17
-
18
- afterEach(() => {
19
- vi.restoreAllMocks();
20
- });
21
-
22
- it('should return the initial state if running on the server', () => {
23
- vi.mocked(isBrowser).mockReturnValue(false);
24
- const { result } = renderHook(() => useSessionStorage('key', 'value'));
25
- expect(result.current[0]).toBe('value');
26
- });
27
-
28
- it('should return the initial state if using a string', () => {
29
- const { result } = renderHook(() => useSessionStorage('key', 'value'));
30
- expect(result.current[0]).toBe('value');
31
- });
32
-
33
- it('return the initial state return if using a callback function', () => {
34
- const { result } = renderHook(() => useSessionStorage('key', () => 'value'));
35
- expect(result.current[0]).toBe('value');
36
- });
37
-
38
- it('return the initial state if using a string', () => {
39
- const { result } = renderHook(() => useSessionStorage('digits', [1, 2]));
40
- expect(result.current[0]).toEqual([1, 2]);
41
- });
42
-
43
- it('return the initial state if it is a map', () => {
44
- const { result } = renderHook(() => useSessionStorage('map', new Map([['a', 1]])));
45
- expect(result.current[0]).toEqual(new Map([['a', 1]]));
46
- });
47
-
48
- it('return the initial state if it is a set', () => {
49
- const { result } = renderHook(() => useSessionStorage('set', new Set([1, 2])));
50
- expect(result.current[0]).toEqual(new Set([1, 2]));
51
- });
52
-
53
- it('return the initial state if it is a date', () => {
54
- const { result } = renderHook(() => useSessionStorage('date', new Date(2020, 1, 1)));
55
- expect(result.current[0]).toEqual(new Date(2020, 1, 1));
56
- });
57
-
58
- it('should update the state and write to session storage', () => {
59
- const { result } = renderHook(() => useSessionStorage('key', 'value'));
60
- act(() => {
61
- const setState = result.current[1];
62
- setState('edited');
63
- });
64
- expect(result.current[0]).toBe('edited');
65
- expect(window.sessionStorage.getItem('key')).toBe(JSON.stringify('edited'));
66
- });
67
-
68
- it('should update the state with undefined', () => {
69
- const { result } = renderHook(() => useSessionStorage<string | undefined>('keytest', 'value'));
70
- act(() => {
71
- const setState = result.current[1];
72
- setState(undefined);
73
- });
74
- expect(result.current[0]).toBeUndefined();
75
- });
76
-
77
- it('should update the state with a callback function', () => {
78
- const { result } = renderHook(() => useSessionStorage('count', 2));
79
- act(() => {
80
- const setState = result.current[1];
81
- setState((prev) => prev + 1);
82
- });
83
- expect(result.current[0]).toBe(3);
84
- expect(window.sessionStorage.getItem('count')).toEqual('3');
85
- });
86
-
87
- it('should update the state with a callback function multiple times per render', () => {
88
- const { result } = renderHook(() => useSessionStorage('count', 2));
89
- act(() => {
90
- const setState = result.current[1];
91
- setState((prev) => prev + 1);
92
- setState((prev) => prev + 1);
93
- setState((prev) => prev + 1);
94
- });
95
- expect(result.current[0]).toBe(5);
96
- expect(window.sessionStorage.getItem('count')).toEqual('5');
97
- });
98
-
99
- it('should update if another hook updates the same key', () => {
100
- const initialValues: [string, unknown] = ['key', 'initial'];
101
- const { result: A } = renderHook(() => useSessionStorage(...initialValues));
102
- const { result: B } = renderHook(() => useSessionStorage(...initialValues));
103
- const { result: C } = renderHook(() => useSessionStorage('other-key', 'initial'));
104
-
105
- act(() => {
106
- const setState = A.current[1];
107
- setState('edited');
108
- });
109
-
110
- expect(B.current[0]).toBe('edited');
111
- expect(C.current[0]).toBe('initial');
112
- });
113
-
114
- it('should not update if another hook updates a different key', () => {
115
- let renderCount = 0;
116
- const { result: A } = renderHook(() => {
117
- renderCount++;
118
- return useSessionStorage('key1', {});
119
- });
120
- const { result: B } = renderHook(() => useSessionStorage('key2', 'initial'));
121
- expect(renderCount).toBe(1);
122
- act(() => {
123
- const setStateA = A.current[1];
124
- setStateA({ a: 1 });
125
- });
126
- expect(renderCount).toBe(2);
127
- act(() => {
128
- const setStateB = B.current[1];
129
- setStateB('edited');
130
- });
131
- expect(renderCount).toBe(2);
132
- });
133
-
134
- it('should return a referentially stable setter', () => {
135
- const { result } = renderHook(() => useSessionStorage('count', 1));
136
- const originalCallback = result.current[1];
137
- act(() => {
138
- const setState = result.current[1];
139
- setState((prev) => prev + 1);
140
- });
141
- expect(result.current[1] === originalCallback).toBe(true);
142
- });
143
-
144
- it('should use the default JSON.stringify and JSON.parse when serializer/deserializer are not provided', () => {
145
- const { result } = renderHook(() => useSessionStorage('key', 'initialValue'));
146
- act(() => {
147
- result.current[1]('newValue');
148
- });
149
- expect(sessionStorage.getItem('key')).toBe(JSON.stringify('newValue'));
150
- });
151
-
152
- it('should write to stderr and set the store to the initial value if the stored value in not serializable', () => {
153
- vi.spyOn(console, 'error');
154
- window.sessionStorage.setItem('key', '{{ x: foo }}');
155
- renderHook(() => useSessionStorage('key', 'initialValue'));
156
- expect(console.error).toHaveBeenLastCalledWith(expect.stringContaining('Error parsing JSON'));
157
- });
158
-
159
- it('should use a custom serializer and deserializer when provided', () => {
160
- const serializer = (value: string) => value.toUpperCase();
161
- const deserializer = (value: string) => value.toLowerCase();
162
- const { result } = renderHook(() => useSessionStorage('key', 'initialValue', { deserializer, serializer }));
163
- act(() => {
164
- result.current[1]('NewValue');
165
- });
166
- expect(sessionStorage.getItem('key')).toBe('NEWVALUE');
167
- });
168
-
169
- it('should handle undefined values with custom deserializer', () => {
170
- const serializer = (value: number | undefined) => String(value);
171
- const deserializer = (value: string) => (value === 'undefined' ? undefined : Number(value));
172
- const { result } = renderHook(() =>
173
- useSessionStorage<number | undefined>('key', 0, {
174
- deserializer,
175
- serializer
176
- })
177
- );
178
- act(() => {
179
- result.current[1](undefined);
180
- });
181
- expect(sessionStorage.getItem('key')).toBe('undefined');
182
- act(() => {
183
- result.current[1](42);
184
- });
185
- expect(sessionStorage.getItem('key')).toBe('42');
186
- });
187
- });