diff --git a/examples/data-exchange/app/[locale]/dashboard/components/main-nav.tsx b/examples/data-exchange/app/[locale]/dashboard/components/main-nav.tsx index 94be5e98..12125c7b 100644 --- a/examples/data-exchange/app/[locale]/dashboard/components/main-nav.tsx +++ b/examples/data-exchange/app/[locale]/dashboard/components/main-nav.tsx @@ -2,7 +2,6 @@ import React from 'react'; import Link from 'next/link'; -import { useMetaKeyPress } from '@/hooks/use-meta-key-press'; import { Session } from 'next-auth'; import { signIn, signOut, useSession } from 'next-auth/react'; import { @@ -22,6 +21,7 @@ import { Spinner, Text, } from 'opub-ui'; +import { useMetaKeyPress } from 'opub-ui/utils'; import { Icons } from '@/components/icons'; diff --git a/packages/opub-ui/package.json b/packages/opub-ui/package.json index 38bf73c3..86653577 100644 --- a/packages/opub-ui/package.json +++ b/packages/opub-ui/package.json @@ -1,7 +1,7 @@ { "name": "opub-ui", "description": "OPub UI Library", - "version": "0.3.16", + "version": "0.3.17", "private": false, "license": "MIT", "author": "CivicDataLab ", diff --git a/packages/opub-ui/src/utils/css.ts b/packages/opub-ui/src/utils/css.ts index 544915d8..f98ab0db 100644 --- a/packages/opub-ui/src/utils/css.ts +++ b/packages/opub-ui/src/utils/css.ts @@ -1,9 +1,28 @@ import { ClassNameValue, twMerge } from 'tailwind-merge'; -export function variationName(name: string, value: string) { +type VariationName = (name: string, value: string) => string; + +/** + * Constructs a variation name by capitalizing the first letter of the value and appending it to the name. + * + * @example + * // returns "colorRed" + * variationName("color", "red"); + */ +export const variationName: VariationName = (name: string, value: string) => { return `${name}${value.charAt(0).toUpperCase()}${value.slice(1)}`; -} +}; +/** + * A utility function that merges multiple class names into a single string. + * + * This function uses the `twMerge` function from the `twin.macro` library to merge the class names. + * It accepts an array of class names, which can be strings, arrays of strings, or objects with class names as keys and boolean values. + * + * @example + * // returns "text-red-500 bg-blue-500" + * cn('text-red-500', 'bg-blue-500'); + */ export function cn(...inputs: ClassNameValue[]) { return twMerge(inputs); } diff --git a/packages/opub-ui/src/utils/helpers.ts b/packages/opub-ui/src/utils/helpers.ts index bd4b16fe..d14b244e 100644 --- a/packages/opub-ui/src/utils/helpers.ts +++ b/packages/opub-ui/src/utils/helpers.ts @@ -30,7 +30,7 @@ export function mergeRefs( * @param {number} max - The upper boundary. * @return {number} The clamped number. */ -export function clamp(number: number, min: number, max: number) { +export function clamp(number: number, min: number, max: number): number { if (number < min) return min; if (number > max) return max; return number; @@ -50,7 +50,7 @@ export function clamp(number: number, min: number, max: number) { * // returns "World" * capitalize("WORLD"); */ -export function capitalize(word = '') { +export function capitalize(word: string = ''): string { const wordLower = word.toLowerCase(); return wordLower.charAt(0).toUpperCase() + wordLower.slice(1); } @@ -65,7 +65,7 @@ export function capitalize(word = '') { * // returns [0, 1, 2, 3, 4] * range(5); */ -export const range = (len: number) => { +export const range = (len: number): number[] => { const arr = []; for (let i = 0; i < len; i++) { arr.push(i); @@ -84,7 +84,7 @@ export const range = (len: number) => { * groupBy([{ 'group': 'group1', 'value': 1 }, { 'group': 'group2', 'value': 2 }], 'group'); * // returns { 'group1': [{ 'group': 'group1', 'value': 1 }], 'group2': [{ 'group': 'group2', 'value': 2 }] } */ -export const groupBy = function (arr: any[], criteria: string) { +export const groupBy = function (arr: any[], criteria: string): object { return arr.reduce(function (obj, item) { var key = item[criteria]; // If the key doesn't exist yet, create it diff --git a/packages/opub-ui/src/utils/hooks/use-forward-ref.ts b/packages/opub-ui/src/utils/hooks/use-forward-ref.ts index 7e292688..c20a5ae1 100644 --- a/packages/opub-ui/src/utils/hooks/use-forward-ref.ts +++ b/packages/opub-ui/src/utils/hooks/use-forward-ref.ts @@ -11,7 +11,7 @@ import React from 'react'; export const useForwardRef = ( ref: React.ForwardedRef, initialValue: any = null -) => { +): React.MutableRefObject => { const targetRef = React.useRef(initialValue); React.useEffect(() => { diff --git a/packages/opub-ui/src/utils/hooks/use-is-mac.ts b/packages/opub-ui/src/utils/hooks/use-is-mac.ts index 8be99b28..fd09906c 100644 --- a/packages/opub-ui/src/utils/hooks/use-is-mac.ts +++ b/packages/opub-ui/src/utils/hooks/use-is-mac.ts @@ -1,6 +1,18 @@ import React from 'react'; -export const useIsMac = () => { +/** + * A React Hook that determines if the current user's operating system is macOS. + * + * This is determined by checking the user agent string for the presence of 'Mac'. + * + * @return {boolean} A state value indicating whether the current user's operating system is macOS. + * + * @example + * // returns true if the user's operating system is macOS + * // returns false otherwise + * const isMacOS = useIsMac(); + */ +export const useIsMac = (): boolean => { const [isMac, setIsMac] = React.useState(false); React.useEffect(() => { setIsMac(window.navigator.userAgent.includes('Mac')); diff --git a/packages/opub-ui/src/utils/hooks/use-isomorphic-layout-effect.ts b/packages/opub-ui/src/utils/hooks/use-isomorphic-layout-effect.ts index 5f57b268..4225299e 100644 --- a/packages/opub-ui/src/utils/hooks/use-isomorphic-layout-effect.ts +++ b/packages/opub-ui/src/utils/hooks/use-isomorphic-layout-effect.ts @@ -1,6 +1,19 @@ 'use client'; -import { isServer } from '../target'; import { useEffect, useLayoutEffect } from 'react'; +import { isServer } from '../target'; + +/** + * A custom hook that uses `useLayoutEffect` on the client and `useEffect` on the server. + * + * This is useful for avoiding warnings about `useLayoutEffect` not working on the server, + * while still getting the benefits of `useLayoutEffect` when running on the client. + * + * @example + * // In a component... + * useIsomorphicLayoutEffect(() => { + * // Your effect here... + * }); + */ export const useIsomorphicLayoutEffect = isServer ? useEffect : useLayoutEffect; diff --git a/packages/opub-ui/src/utils/hooks/use-key-detect.ts b/packages/opub-ui/src/utils/hooks/use-key-detect.ts index 4f0db388..ae29260f 100644 --- a/packages/opub-ui/src/utils/hooks/use-key-detect.ts +++ b/packages/opub-ui/src/utils/hooks/use-key-detect.ts @@ -1,25 +1,30 @@ import { useEffect, useState } from 'react'; -export function useKeyDetect(): { key: string; metaKey?: boolean } { +/** + * A custom React Hook that listens for a key press event and returns the key pressed. + * + * The hook listens for the 'keydown' event on the window object. When a key is pressed, it updates the state with the key pressed. + * The state is cleared after 1 millisecond. + * + * @return {object} An object with one property: `key` (the key that was pressed). + * + * @example + * // In a component... + * const { key } = useKeyDetect(); + * // `key` is the key that was pressed + */ +export function useKeyDetect(): { key: string } { const [keyPressed, setKeyPressed] = useState<{ key: string; - metaKey?: boolean; }>({ key: '', - metaKey: false, }); - function downHandler({ - key, - metaKey, - }: { - key: string; - metaKey?: boolean; - }): void { - setKeyPressed({ key, metaKey }); + function downHandler({ key }: { key: string; metaKey?: boolean }): void { + setKeyPressed({ key }); // clear key after 1 milisecond setTimeout(() => { - setKeyPressed({ key: '', metaKey: false }); + setKeyPressed({ key: '' }); }, 1); } @@ -32,5 +37,5 @@ export function useKeyDetect(): { key: string; metaKey?: boolean } { window.removeEventListener('keydown', downHandler); }; }, []); // Empty array ensures that effect is only run on mount and unmount - return { key: keyPressed.key, metaKey: keyPressed.metaKey }; + return { key: keyPressed.key }; } diff --git a/packages/opub-ui/src/utils/hooks/use-lock-body.ts b/packages/opub-ui/src/utils/hooks/use-lock-body.ts index ddf59442..6c81ad00 100644 --- a/packages/opub-ui/src/utils/hooks/use-lock-body.ts +++ b/packages/opub-ui/src/utils/hooks/use-lock-body.ts @@ -1,7 +1,16 @@ import * as React from 'react'; -// @see https://usehooks.com/useLockBodyScroll. -export function useLockBody() { +/** + * A React Hook that locks the body scroll by setting the overflow style to 'hidden'. + * + * The hook uses `useLayoutEffect` to change the body's overflow style to 'hidden' when the component mounts, effectively locking the body scroll. + * When the component unmounts, the hook restores the original overflow style, unlocking the body scroll. + * + * @example + * // In a component... + * useLockBody(); + * // The body scroll is now locked. It will be unlocked when the component unmounts. + */ export function useLockBody() { React.useLayoutEffect((): (() => void) => { const originalStyle: string = window.getComputedStyle( document.body diff --git a/packages/opub-ui/src/utils/hooks/use-meta-key-press.ts b/packages/opub-ui/src/utils/hooks/use-meta-key-press.ts index 6a453a1f..5a7347e9 100644 --- a/packages/opub-ui/src/utils/hooks/use-meta-key-press.ts +++ b/packages/opub-ui/src/utils/hooks/use-meta-key-press.ts @@ -1,5 +1,17 @@ import React, { useState } from 'react'; +/** + * A custom React Hook that listens for a specific key press and triggers a function when the key is pressed. + * On Mac, it listens for the Meta key (Command ⌘) and on other platforms, it listens for the Control key. + * + * @param {string} targetKey - The specific key to listen for. + * @param {() => void} [fn] - The function to execute when the target key is pressed. Optional. + * @return {boolean} A state value indicating whether the target key is currently pressed. + * + * @example + * // Listen for 's' key press and log a message when it's pressed with the Meta/Control key + * useMetaKeyPress('s', () => console.log('Save command triggered')); + */ export function useMetaKeyPress(targetKey: string, fn?: () => void): boolean { const [keyPressed, setKeyPressed] = useState(false); diff --git a/packages/opub-ui/src/utils/hooks/use-mounted.ts b/packages/opub-ui/src/utils/hooks/use-mounted.ts index 1bc7bde2..86383d59 100644 --- a/packages/opub-ui/src/utils/hooks/use-mounted.ts +++ b/packages/opub-ui/src/utils/hooks/use-mounted.ts @@ -1,6 +1,20 @@ -import * as React from 'react'; +import React from 'react'; -export function useMounted() { +/** + * A React Hook that tracks if the component is currently mounted. + * + * The hook uses `useState` to create a `mounted` state variable, initially set to `false`. + * It then uses `useEffect` to set `mounted` to `true` when the component mounts. + * + * @return {boolean} A state value indicating whether the component is currently mounted. + * + * @example + * // In a component... + * const isMounted = useMounted(); + * // `isMounted` is true if the component is currently mounted, false otherwise. + */ + +export function useMounted(): boolean { const [mounted, setMounted] = React.useState(false); React.useEffect(() => { diff --git a/packages/opub-ui/src/utils/hooks/use-on-click-outside.tsx b/packages/opub-ui/src/utils/hooks/use-on-click-outside.tsx index 79d99fa2..e29a90bd 100644 --- a/packages/opub-ui/src/utils/hooks/use-on-click-outside.tsx +++ b/packages/opub-ui/src/utils/hooks/use-on-click-outside.tsx @@ -1,5 +1,20 @@ import React from 'react'; +/** + * A React Hook that triggers a handler function when a click event occurs outside of the specified element. + * + * The hook uses `useEffect` to add a click event listener to the document when the component mounts. + * The event listener checks if the click event occurred inside the specified element. If it did not, it calls the handler function. + * When the component unmounts, the hook removes the event listener. + * + * @param {React.RefObject} ref - A ref object representing the element to detect clicks outside of. + * @param {(event: MouseEvent | TouchEvent) => void} handler - The function to execute when a click event occurs outside the specified element. + * + * @example + * // In a component... + * const ref = useRef(null); + * useOnClickOutside(ref, () => console.log('Clicked outside')); + */ export function useOnClickOutside( ref: React.RefObject, handler: (event: MouseEvent | TouchEvent) => void diff --git a/packages/opub-ui/src/utils/hooks/use-toggle.ts b/packages/opub-ui/src/utils/hooks/use-toggle.ts index 72e1bca7..e950b99e 100644 --- a/packages/opub-ui/src/utils/hooks/use-toggle.ts +++ b/packages/opub-ui/src/utils/hooks/use-toggle.ts @@ -1,12 +1,31 @@ 'use client'; -import { useState, useCallback } from 'react'; +import { useCallback, useState } from 'react'; /** - * Returns a stateful value, and a set of memoized functions to toggle it, - * set it to true and set it to false + * A React Hook that provides a boolean state value and functions to toggle it, set it to true, and set it to false. + * + * The hook uses `useState` to create a `value` state variable, initially set to the provided `initialState`. + * It also provides three functions: `toggle` (which toggles the `value`), `setTrue` (which sets `value` to true), and `setFalse` (which sets `value` to false). + * These functions are wrapped in `useCallback` to prevent unnecessary re-renders. + * + * @param {boolean} initialState - The initial state value. + * @return {object} An object containing the `value` state value and the `toggle`, `setTrue`, and `setFalse` functions. + * + * @example + * // In a component... + * const { value, toggle, setTrue, setFalse } = useToggle(false); + * // `value` is the current state value + * // `toggle` is a function that toggles the state value + * // `setTrue` is a function that sets the state value to true + * // `setFalse` is a function that sets the state value to false */ -export function useToggle(initialState: boolean) { +export function useToggle(initialState: boolean): { + value: boolean; + toggle: () => void; + setTrue: () => void; + setFalse: () => void; +} { const [value, setState] = useState(initialState); return { diff --git a/packages/opub-ui/src/utils/hooks/use-window-size.ts b/packages/opub-ui/src/utils/hooks/use-window-size.ts index d8c5266d..8fde7902 100644 --- a/packages/opub-ui/src/utils/hooks/use-window-size.ts +++ b/packages/opub-ui/src/utils/hooks/use-window-size.ts @@ -1,4 +1,3 @@ -// https://usehooks.com/useWindowSize/ import { useEffect, useState } from 'react'; // Define general type for useWindowSize hook, which includes width and height @@ -7,7 +6,21 @@ export interface Size { height: number | undefined; } -// Hook +/** + * A custom React Hook that provides the current window size. + * + * The hook uses `useState` to create a `windowSize` state variable, initially set to an object with `width` and `height` both undefined. + * It then uses `useEffect` to add a 'resize' event listener to the window when the component mounts. This event listener updates `windowSize` with the current window size whenever the window is resized. + * The initial window size is set by calling the event handler immediately after adding the event listener. + * The event listener is removed when the component unmounts. + * + * @return {Size} An object containing the current window width and height. + * + * @example + * // In a component... + * const { width, height } = useWindowSize(); + * // `width` and `height` are the current window width and height, respectively. + */ export function useWindowSize(): Size { // Initialize state with undefined width/height so server and client renders match // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/ diff --git a/packages/opub-ui/src/utils/target.ts b/packages/opub-ui/src/utils/target.ts index ba0c19ee..92060373 100644 --- a/packages/opub-ui/src/utils/target.ts +++ b/packages/opub-ui/src/utils/target.ts @@ -11,5 +11,5 @@ * // returns false in a browser environment * const serverCheck = isServer; */ -export const isServer = +export const isServer: boolean = typeof window === 'undefined' || typeof document === 'undefined';