From 7b7664a6bd5cc525ada2d2d279340ca2b1562bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Fri, 3 Jan 2025 17:04:35 +0100 Subject: [PATCH 01/13] wip: init pin field v2 --- src/pin-field/pin-field-v2.stories.tsx | 33 ++++++++ src/pin-field/pin-field-v2.tsx | 100 +++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 src/pin-field/pin-field-v2.stories.tsx create mode 100644 src/pin-field/pin-field-v2.tsx diff --git a/src/pin-field/pin-field-v2.stories.tsx b/src/pin-field/pin-field-v2.stories.tsx new file mode 100644 index 0000000..7198650 --- /dev/null +++ b/src/pin-field/pin-field-v2.stories.tsx @@ -0,0 +1,33 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { fn } from "@storybook/test"; + +import { PinFieldV2, defaultProps, Props } from "./pin-field-v2"; + +const defaultArgs = { + length: defaultProps.length, + onResolveKey: fn(), + onRejectKey: fn(), + onChange: fn(), + onComplete: fn(), +} satisfies Props; + +/** + * The `` component is a simple wrapper around a list of HTML inputs. + * + * The component exposes 4 event handlers, see stories below to learn more about the other props. + */ +const meta: Meta = { + title: "PinFieldV2", + component: PinFieldV2, + tags: ["autodocs"], + parameters: { + layout: "centered", + }, +}; + +export const Default: StoryObj = { + render: props => , + args: defaultArgs, +}; + +export default meta; diff --git a/src/pin-field/pin-field-v2.tsx b/src/pin-field/pin-field-v2.tsx new file mode 100644 index 0000000..bd29719 --- /dev/null +++ b/src/pin-field/pin-field-v2.tsx @@ -0,0 +1,100 @@ +import { FC, useEffect, useReducer, useRef } from "react"; + +import { noop, range } from "../utils"; + +export type DefaultProps = { + length: number; + validate: string | string[] | RegExp | ((key: string) => boolean); + format: (char: string) => string; + formatAriaLabel: (idx: number, codeLength: number) => string; + onResolveKey: (key: string, ref?: HTMLInputElement) => any; + onRejectKey: (key: string, ref?: HTMLInputElement) => any; + onChange: (code: string) => void; + onComplete: (code: string) => void; +}; + +export const defaultProps: DefaultProps = { + length: 5, + validate: /^[a-zA-Z0-9]$/, + format: key => key, + formatAriaLabel: (i: number, n: number) => `PIN field ${i} of ${n}`, + onResolveKey: noop, + onRejectKey: noop, + onChange: noop, + onComplete: noop, +}; + +export type Props = Partial; + +export type State = { + cursor: number; + values: string[]; + length: number; +}; + +export function defaultState(length: number): State { + return { + cursor: 0, + values: [], + length, + }; +} + +export type Action = { type: "handle-change"; key: number; value: string }; + +export function reducer(state: State, action: Action): State { + switch (action.type) { + case "handle-change": { + console.log("action", action); + state.values[action.key] = action.value; + + if (action.value === "") { + state.cursor = action.key - 1; + } else { + state.cursor = action.key + 1; + } + } + } + + return { ...state }; +} + +export const PinFieldV2: FC = props => { + const length = props.length || defaultProps.length; + const refs = useRef([]); + const [state, dispatch] = useReducer(reducer, defaultState(length)); + + console.log("state", state); + + function setRefAtIndex(idx: number) { + return function (ref: HTMLInputElement) { + if (ref) { + refs.current[idx] = ref; + } + }; + } + + useEffect(() => { + if (!refs.current) return; + refs.current[state.cursor].focus(); + }, [refs, state.cursor]); + + return ( + <> + {range(0, length).map(key => ( + dispatch({ type: "handle-change", key, value: evt.target.value })} + /> + ))} + + ); +}; + +export default PinFieldV2; From c8aa6be9442a459ec6f093ab3d4537c1afdba5d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sat, 4 Jan 2025 13:50:08 +0100 Subject: [PATCH 02/13] support strict mode and controlled mode --- src/pin-field/pin-field-v2.stories.tsx | 32 ++- src/pin-field/pin-field-v2.tsx | 257 ++++++++++++++++++++++--- 2 files changed, 261 insertions(+), 28 deletions(-) diff --git a/src/pin-field/pin-field-v2.stories.tsx b/src/pin-field/pin-field-v2.stories.tsx index 7198650..11fd362 100644 --- a/src/pin-field/pin-field-v2.stories.tsx +++ b/src/pin-field/pin-field-v2.stories.tsx @@ -1,7 +1,8 @@ +import { StrictMode as ReactStrictMode } from "react"; import type { Meta, StoryObj } from "@storybook/react"; import { fn } from "@storybook/test"; -import { PinFieldV2, defaultProps, Props } from "./pin-field-v2"; +import { PinFieldV2, defaultProps, Props, usePinField } from "./pin-field-v2"; const defaultArgs = { length: defaultProps.length, @@ -30,4 +31,33 @@ export const Default: StoryObj = { args: defaultArgs, }; +export const StrictMode: StoryObj = { + render: props => ( + + + + ), + args: defaultArgs, +}; + +export const Controlled: StoryObj = { + render: props => { + const handler = usePinField(props.length); + + return ( + <> + + + handler.setValue(event.target.value)} + /> + + ); + }, + args: defaultArgs, +}; + export default meta; diff --git a/src/pin-field/pin-field-v2.tsx b/src/pin-field/pin-field-v2.tsx index bd29719..fb62e58 100644 --- a/src/pin-field/pin-field-v2.tsx +++ b/src/pin-field/pin-field-v2.tsx @@ -1,7 +1,25 @@ -import { FC, useEffect, useReducer, useRef } from "react"; +import { + FC, + useEffect, + useReducer, + useRef, + KeyboardEventHandler, + KeyboardEvent, + ChangeEventHandler, + useCallback, + CompositionEventHandler, + forwardRef, + useImperativeHandle, + RefObject, + ActionDispatch, + useMemo, +} from "react"; import { noop, range } from "../utils"; +const BACKSPACE = 8; +const DELETE = 46; + export type DefaultProps = { length: number; validate: string | string[] | RegExp | ((key: string) => boolean); @@ -24,77 +42,262 @@ export const defaultProps: DefaultProps = { onComplete: noop, }; -export type Props = Partial; +export type Props = Partial & { + handler?: Handler; +}; export type State = { cursor: number; values: string[]; length: number; + backspace: boolean; + composition: boolean; }; export function defaultState(length: number): State { return { cursor: 0, - values: [], + values: Array(length), length, + backspace: false, + composition: false, }; } -export type Action = { type: "handle-change"; key: number; value: string }; +export type Action = + | { type: "handle-change"; index: number; value: string | null; reset?: boolean } + | { type: "handle-key-down"; index: number; event: KeyboardEvent } + | { type: "start-composition"; index: number } + | { type: "end-composition"; index: number; value: string }; export function reducer(state: State, action: Action): State { + console.log("action", action); + switch (action.type) { + case "start-composition": { + return { ...state, composition: true }; + } + + case "end-composition": { + state.composition = false; + + if (action.value) { + state.values[action.index] = action.value; + } else { + delete state.values[action.index]; + } + + const dir = state.values[action.index] ? 1 : 0; + state.cursor = Math.min(action.index + dir, state.length - 1); + + return { ...state }; + } + case "handle-change": { - console.log("action", action); - state.values[action.key] = action.value; + if (state.composition) { + break; + } + + if (action.reset) { + state.values = Array(state.length); + } - if (action.value === "") { - state.cursor = action.key - 1; + if (action.value) { + const values = action.value.split(""); + const length = Math.min(state.length - action.index, values.length); + state.values.splice(action.index, length, ...values.slice(0, length)); + state.cursor = Math.min(action.index + length, state.length - 1); } else { - state.cursor = action.key + 1; + delete state.values[action.index]; + const dir = state.backspace ? 0 : 1; + state.cursor = Math.max(0, action.index - dir); } + + return { ...state, backspace: false }; + } + + case "handle-key-down": { + const key = action.event.key === "Backspace" || action.event.key === "Delete"; + const which = action.event.which === BACKSPACE || action.event.which === DELETE; + const keyCode = action.event.keyCode === BACKSPACE || action.event.keyCode === DELETE; + const deletion = key || which || keyCode; + + // Deletion is a bit tricky and requires special attention. + // + // When the current field has a value, deletion works as + // expected AS LONG AS THE STATE IS NOT UPDATED and keeps its + // reference: the value deletes by itself and the `onchange` + // event is triggered, which updates the state. + // + // But when the current field is empty, deletion does not + // trigger the `onchange` event. Therefore the state needs to be + // updated here. Moving the cursor backwards is enough for + // deletion to happen on the previous field, which triggers the + // `onchange` event and re-update the state. + if (deletion) { + // if empty value, move the cursor backwards and update the + // state + if (!state.values[action.index]) { + state.cursor = Math.max(0, action.index - 1); + + // let know the handle-change action that we already moved + // backwards and that we don't need to touch the cursor + // anymore + state.backspace = true; + + return { ...state }; + } + + // otherwise just return the same state and let the onchange + // event do the job + } + + break; } } - return { ...state }; + return state; } -export const PinFieldV2: FC = props => { - const length = props.length || defaultProps.length; +type Handler = { + refs: RefObject; + state: State; + dispatch: ActionDispatch<[Action]>; + value: string; + setValue: (value: string) => void; +}; + +export function usePinField(length?: number): Handler { + return useInternalHandler(length); +} + +export function useInternalHandler(length: number = defaultProps.length, handler?: Handler): Handler { + if (handler) return handler; + const refs = useRef([]); const [state, dispatch] = useReducer(reducer, defaultState(length)); - console.log("state", state); + const value = useMemo(() => { + let value = ""; + for (let index = 0; index < state.values.length; index++) { + value += index in state.values ? state.values[index] : ""; + } + return value; + }, [state]); - function setRefAtIndex(idx: number) { - return function (ref: HTMLInputElement) { - if (ref) { - refs.current[idx] = ref; - } - }; + const setValue = useCallback( + (value: string) => { + dispatch({ type: "handle-change", index: 0, value, reset: true }); + }, + [dispatch, state.cursor], + ); + + return { refs, state, dispatch, value, setValue }; +} + +export const PinFieldV2: FC = forwardRef((props, fwdRef) => { + const handler = useInternalHandler(props.length, props.handler); + + useImperativeHandle(fwdRef, () => handler.refs.current, [handler.refs]); + + console.log("state", handler.state); + + function setRefAt(index: number): (ref: HTMLInputElement) => void { + return useCallback( + ref => { + if (ref) { + handler.refs.current[index] = ref; + } + }, + [index], + ); + } + + function handleKeyDownAt(index: number): KeyboardEventHandler { + return useCallback( + event => { + handler.dispatch({ type: "handle-key-down", index, event }); + }, + [index, handler.dispatch], + ); + } + + function handleChangeAt(index: number): ChangeEventHandler { + return useCallback( + event => { + // should not happen, mostly for typescript to infer properly + if (!(event.nativeEvent instanceof InputEvent)) return; + handler.dispatch({ type: "handle-change", index, value: event.nativeEvent.data }); + }, + [index, handler.dispatch], + ); + } + + function startCompositionAt(index: number): CompositionEventHandler { + return useCallback(() => { + handler.dispatch({ type: "start-composition", index }); + }, [index, handler.dispatch]); + } + + function endCompositionAt(index: number): CompositionEventHandler { + return useCallback( + event => { + handler.dispatch({ type: "end-composition", index, value: event.data }); + }, + [index, handler.dispatch], + ); } useEffect(() => { - if (!refs.current) return; - refs.current[state.cursor].focus(); - }, [refs, state.cursor]); + if (props.onChange === undefined) return; + props.onChange(handler.value); + }, [props.onChange, handler.value]); + + useEffect(() => { + if (!handler.refs.current) return; + console.log("state changed"); + + let innerFocus = false; + + for (let index = 0; index < handler.state.values.length; index++) { + const value = index in handler.state.values ? handler.state.values[index] : ""; + handler.refs.current[index].value = value; + innerFocus = innerFocus || hasFocus(handler.refs.current[index]); + } + + if (innerFocus) { + handler.refs.current[handler.state.cursor].focus(); + } + }, [handler.refs, handler.state]); return ( <> - {range(0, length).map(key => ( + {range(0, handler.state.length).map(index => ( dispatch({ type: "handle-change", key, value: evt.target.value })} + key={index} + ref={setRefAt(index)} + onKeyDown={handleKeyDownAt(index)} + onChange={handleChangeAt(index)} + onCompositionStart={startCompositionAt(index)} + onCompositionEnd={endCompositionAt(index)} /> ))} ); -}; +}); + +export function hasFocus(el: HTMLElement): boolean { + try { + const matches = el.webkitMatchesSelector || el.matches; + return matches.call(el, ":focus"); + } catch (err: any) { + return false; + } +} export default PinFieldV2; From 0927254053c6cd2cd201ffc41674de4e6f306d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sat, 4 Jan 2025 15:05:13 +0100 Subject: [PATCH 03/13] improve usePinField hook --- src/pin-field/pin-field-v2.stories.tsx | 41 +++++-- src/pin-field/pin-field-v2.tsx | 153 +++++++++++++++---------- 2 files changed, 124 insertions(+), 70 deletions(-) diff --git a/src/pin-field/pin-field-v2.stories.tsx b/src/pin-field/pin-field-v2.stories.tsx index 11fd362..f6038a2 100644 --- a/src/pin-field/pin-field-v2.stories.tsx +++ b/src/pin-field/pin-field-v2.stories.tsx @@ -1,4 +1,4 @@ -import { StrictMode as ReactStrictMode } from "react"; +import { FC, StrictMode as ReactStrictMode } from "react"; import type { Meta, StoryObj } from "@storybook/react"; import { fn } from "@storybook/test"; @@ -6,10 +6,10 @@ import { PinFieldV2, defaultProps, Props, usePinField } from "./pin-field-v2"; const defaultArgs = { length: defaultProps.length, - onResolveKey: fn(), - onRejectKey: fn(), - onChange: fn(), - onComplete: fn(), + // onResolveKey: fn(), + // onRejectKey: fn(), + // onChange: fn(), + // onComplete: fn(), } satisfies Props; /** @@ -42,11 +42,13 @@ export const StrictMode: StoryObj = { export const Controlled: StoryObj = { render: props => { - const handler = usePinField(props.length); + const handler = usePinField(); return ( <> - +
+ +
= { args: defaultArgs, }; +/** + * Characters can be formatted with a formatter `(char: string) => string`. + */ +export const Format: StoryObj> = { + render: ({ formatEval, ...props }) => { + try { + let format = eval(formatEval); + format("a"); + return ; + } catch (err: any) { + return
Invalid format function: {err.toString()}
; + } + }, + argTypes: { + formatEval: { + control: "text", + name: "format (fn eval)", + }, + }, + args: { + formatEval: "char => char.toUpperCase()", + ...defaultArgs, + }, +}; + export default meta; diff --git a/src/pin-field/pin-field-v2.tsx b/src/pin-field/pin-field-v2.tsx index fb62e58..b1c604e 100644 --- a/src/pin-field/pin-field-v2.tsx +++ b/src/pin-field/pin-field-v2.tsx @@ -20,51 +20,51 @@ import { noop, range } from "../utils"; const BACKSPACE = 8; const DELETE = 46; -export type DefaultProps = { +export type InnerProps = { length: number; validate: string | string[] | RegExp | ((key: string) => boolean); format: (char: string) => string; formatAriaLabel: (idx: number, codeLength: number) => string; onResolveKey: (key: string, ref?: HTMLInputElement) => any; onRejectKey: (key: string, ref?: HTMLInputElement) => any; - onChange: (code: string) => void; onComplete: (code: string) => void; }; -export const defaultProps: DefaultProps = { +export const defaultProps: InnerProps = { length: 5, validate: /^[a-zA-Z0-9]$/, format: key => key, formatAriaLabel: (i: number, n: number) => `PIN field ${i} of ${n}`, onResolveKey: noop, onRejectKey: noop, - onChange: noop, onComplete: noop, }; -export type Props = Partial & { +export type Props = Partial & { handler?: Handler; }; export type State = { + props: InnerProps; cursor: number; values: string[]; - length: number; backspace: boolean; composition: boolean; + ready: boolean; }; -export function defaultState(length: number): State { - return { - cursor: 0, - values: Array(length), - length, - backspace: false, - composition: false, - }; -} +export const defaultState: State = { + props: defaultProps, + cursor: 0, + values: Array(defaultProps.length), + backspace: false, + composition: false, + ready: false, +}; export type Action = + | { type: "init"; props: Props } + | { type: "update-length"; length: number } | { type: "handle-change"; index: number; value: string | null; reset?: boolean } | { type: "handle-key-down"; index: number; event: KeyboardEvent } | { type: "start-composition"; index: number } @@ -74,6 +74,21 @@ export function reducer(state: State, action: Action): State { console.log("action", action); switch (action.type) { + case "init": { + state.props = { ...defaultProps, ...action.props }; + state.values.splice(state.cursor, state.props.length); + state.ready = true; + state.cursor = Math.min(state.cursor, state.props.length - 1); + return { ...state }; + } + + case "update-length": { + state.values.splice(state.cursor, action.length); + state.props.length = action.length; + state.cursor = Math.min(state.cursor, action.length - 1); + return { ...state }; + } + case "start-composition": { return { ...state, composition: true }; } @@ -88,7 +103,7 @@ export function reducer(state: State, action: Action): State { } const dir = state.values[action.index] ? 1 : 0; - state.cursor = Math.min(action.index + dir, state.length - 1); + state.cursor = Math.min(action.index + dir, state.props.length - 1); return { ...state }; } @@ -99,14 +114,14 @@ export function reducer(state: State, action: Action): State { } if (action.reset) { - state.values = Array(state.length); + state.values = Array(state.props.length); } if (action.value) { - const values = action.value.split(""); - const length = Math.min(state.length - action.index, values.length); + const values = action.value.split("").map(state.props.format); + const length = Math.min(state.props.length - action.index, values.length); state.values.splice(action.index, length, ...values.slice(0, length)); - state.cursor = Math.min(action.index + length, state.length - 1); + state.cursor = Math.min(action.index + length, state.props.length - 1); } else { delete state.values[action.index]; const dir = state.backspace ? 0 : 1; @@ -160,6 +175,7 @@ export function reducer(state: State, action: Action): State { } type Handler = { + init: (props: Props) => void; refs: RefObject; state: State; dispatch: ActionDispatch<[Action]>; @@ -167,15 +183,13 @@ type Handler = { setValue: (value: string) => void; }; -export function usePinField(length?: number): Handler { - return useInternalHandler(length); +export function usePinField(): Handler { + return useInternalHandler(); } -export function useInternalHandler(length: number = defaultProps.length, handler?: Handler): Handler { - if (handler) return handler; - +export function useInternalHandler(): Handler { const refs = useRef([]); - const [state, dispatch] = useReducer(reducer, defaultState(length)); + const [state, dispatch] = useReducer(reducer, defaultState); const value = useMemo(() => { let value = ""; @@ -192,68 +206,77 @@ export function useInternalHandler(length: number = defaultProps.length, handler [dispatch, state.cursor], ); - return { refs, state, dispatch, value, setValue }; + const init = useCallback( + (props: Props) => { + dispatch({ type: "init", props }); + }, + [dispatch], + ); + + return useMemo( + () => ({ refs, state, dispatch, value, setValue, init }), + [refs, state, dispatch, value, setValue, init], + ); } -export const PinFieldV2: FC = forwardRef((props, fwdRef) => { - const handler = useInternalHandler(props.length, props.handler); +export const PinFieldV2: FC = forwardRef(({ handler: customHandler, ...props }, fwdRef) => { + const internalHandler = useInternalHandler(); + const handler = customHandler || internalHandler; useImperativeHandle(fwdRef, () => handler.refs.current, [handler.refs]); console.log("state", handler.state); function setRefAt(index: number): (ref: HTMLInputElement) => void { - return useCallback( - ref => { - if (ref) { - handler.refs.current[index] = ref; - } - }, - [index], - ); + return ref => { + if (ref) { + handler.refs.current[index] = ref; + } + }; } function handleKeyDownAt(index: number): KeyboardEventHandler { - return useCallback( - event => { - handler.dispatch({ type: "handle-key-down", index, event }); - }, - [index, handler.dispatch], - ); + return event => { + handler.dispatch({ type: "handle-key-down", index, event }); + }; } function handleChangeAt(index: number): ChangeEventHandler { - return useCallback( - event => { - // should not happen, mostly for typescript to infer properly - if (!(event.nativeEvent instanceof InputEvent)) return; - handler.dispatch({ type: "handle-change", index, value: event.nativeEvent.data }); - }, - [index, handler.dispatch], - ); + return event => { + // should not happen, mostly for typescript to infer properly + if (!(event.nativeEvent instanceof InputEvent)) return; + handler.dispatch({ type: "handle-change", index, value: event.nativeEvent.data }); + }; } function startCompositionAt(index: number): CompositionEventHandler { - return useCallback(() => { + return () => { handler.dispatch({ type: "start-composition", index }); - }, [index, handler.dispatch]); + }; } function endCompositionAt(index: number): CompositionEventHandler { - return useCallback( - event => { - handler.dispatch({ type: "end-composition", index, value: event.data }); - }, - [index, handler.dispatch], - ); + return event => { + handler.dispatch({ type: "end-composition", index, value: event.data }); + }; } useEffect(() => { - if (props.onChange === undefined) return; - props.onChange(handler.value); - }, [props.onChange, handler.value]); + if (handler.state.ready) return; + handler.init(props); + }, [props, handler.state.ready, handler.init]); useEffect(() => { + handler.init({ length: props.length }); + }, [handler.init, props.length !== handler.state.props.length]); + + // useEffect(() => { + // if (props.length === undefined) return; + // handler.dispatch({ type: "update-length", length: props.length }); + // }, [props.length !== handler.state.props.length, handler.dispatch]); + + useEffect(() => { + if (!handler.state.ready) return; if (!handler.refs.current) return; console.log("state changed"); @@ -270,9 +293,13 @@ export const PinFieldV2: FC = forwardRef((props, fwdRef) => { } }, [handler.refs, handler.state]); + if (!handler.state.ready) { + return null; + } + return ( <> - {range(0, handler.state.length).map(index => ( + {range(0, handler.state.props.length).map(index => ( Date: Sun, 5 Jan 2025 10:24:12 +0100 Subject: [PATCH 04/13] add back HTML input attributes, fix dir --- src/pin-field/pin-field-v2.stories.tsx | 90 +++++- src/pin-field/pin-field-v2.tsx | 384 ++++++++++++++----------- src/utils/utils.ts | 4 +- 3 files changed, 293 insertions(+), 185 deletions(-) diff --git a/src/pin-field/pin-field-v2.stories.tsx b/src/pin-field/pin-field-v2.stories.tsx index f6038a2..ec7d1e2 100644 --- a/src/pin-field/pin-field-v2.stories.tsx +++ b/src/pin-field/pin-field-v2.stories.tsx @@ -2,15 +2,15 @@ import { FC, StrictMode as ReactStrictMode } from "react"; import type { Meta, StoryObj } from "@storybook/react"; import { fn } from "@storybook/test"; -import { PinFieldV2, defaultProps, Props, usePinField } from "./pin-field-v2"; +import { PinFieldV2, defaultProps, Props, usePinField, InnerProps } from "./pin-field-v2"; const defaultArgs = { length: defaultProps.length, - // onResolveKey: fn(), - // onRejectKey: fn(), - // onChange: fn(), - // onComplete: fn(), -} satisfies Props; + format: defaultProps.format, + formatAriaLabel: defaultProps.formatAriaLabel, + onChange: fn(), + onComplete: fn(), +} satisfies InnerProps; /** * The `` component is a simple wrapper around a list of HTML inputs. @@ -31,6 +31,9 @@ export const Default: StoryObj = { args: defaultArgs, }; +/** + * Story to detect inconsistent behaviours in React Strict Mode. + */ export const StrictMode: StoryObj = { render: props => ( @@ -40,14 +43,30 @@ export const StrictMode: StoryObj = { args: defaultArgs, }; -export const Controlled: StoryObj = { - render: props => { +/** + * The `usePinField()` hook exposes a handler to control the PIN field: + * + * - `refs`: the list of HTML input elements that composes the PIN field + * - `value`: the current value of the PIN field + * - `setValue`: change the current value of the PIN field + * + * It also exposes the internal `state` and `dispatch` for advance usage. + * + * The handler returned by `usePinField()` needs to be passed down to the composant for the control to work: + * + * ```tsx + * const handler = usePinField(); + * return + * ``` + */ +export const Controlled: StoryObj> = { + render: ({ controlled }) => { const handler = usePinField(); return ( <>
- +
= { ); }, - args: defaultArgs, + args: { + controlled: true, + }, }; /** @@ -87,4 +108,53 @@ export const Format: StoryObj> = { }, }; +/** + * Characters can be validated using the HTML input attribute `pattern`: + */ +export const HTMLInputAttributes: StoryObj> = { + render: ({ formatAriaLabelEval, ...props }) => { + try { + let formatAriaLabel = eval(formatAriaLabelEval); + formatAriaLabel(0, 0); + return ( +
+
+ +
+ +
+ ); + } catch (err: any) { + return
Invalid format aria label function: {err.toString()}
; + } + }, + argTypes: { + formatAriaLabelEval: { + control: "text", + name: "formatAriaLabel (fn eval)", + }, + type: { + control: "select", + options: ["text", "number", "password"], + }, + dir: { + control: "select", + options: ["ltr", "rtl"], + }, + }, + args: { + type: "password", + className: "pin-field", + pattern: "[0-9]+", + required: false, + autoFocus: true, + disabled: false, + autoCorrect: "off", + autoComplete: "off", + dir: "ltr", + formatAriaLabelEval: "(i, n) => `field ${i}/${n}`", + ...defaultArgs, + }, +}; + export default meta; diff --git a/src/pin-field/pin-field-v2.tsx b/src/pin-field/pin-field-v2.tsx index b1c604e..7c2f0b2 100644 --- a/src/pin-field/pin-field-v2.tsx +++ b/src/pin-field/pin-field-v2.tsx @@ -1,5 +1,6 @@ import { FC, + InputHTMLAttributes, useEffect, useReducer, useRef, @@ -22,80 +23,84 @@ const DELETE = 46; export type InnerProps = { length: number; - validate: string | string[] | RegExp | ((key: string) => boolean); format: (char: string) => string; - formatAriaLabel: (idx: number, codeLength: number) => string; - onResolveKey: (key: string, ref?: HTMLInputElement) => any; - onRejectKey: (key: string, ref?: HTMLInputElement) => any; - onComplete: (code: string) => void; + formatAriaLabel: (index: number, total: number) => string; + onChange: (value: string) => void; + onComplete: (value: string) => void; }; export const defaultProps: InnerProps = { length: 5, - validate: /^[a-zA-Z0-9]$/, - format: key => key, - formatAriaLabel: (i: number, n: number) => `PIN field ${i} of ${n}`, - onResolveKey: noop, - onRejectKey: noop, + format: char => char, + formatAriaLabel: (index: number, total: number) => `PIN field ${index} of ${total}`, + onChange: noop, onComplete: noop, }; -export type Props = Partial & { - handler?: Handler; +export type NativeProps = Omit< + InputHTMLAttributes, + "onChange" | "onKeyDown" | "onCompositionStart" | "onCompositionEnd" +>; + +export const defaultNativeProps: NativeProps = { + type: "text", + inputMode: "text", + autoCapitalize: "off", + autoCorrect: "off", + autoComplete: "off", }; -export type State = { - props: InnerProps; +export type Props = NativeProps & + Partial & { + handler?: Handler; + }; + +export type StateProps = Pick & Pick; + +export type State = StateProps & { cursor: number; values: string[]; backspace: boolean; composition: boolean; ready: boolean; + dirty: boolean; }; export const defaultState: State = { - props: defaultProps, + length: defaultProps.length, + format: defaultProps.format, + dir: "ltr", cursor: 0, values: Array(defaultProps.length), backspace: false, composition: false, ready: false, + dirty: false, }; export type Action = - | { type: "init"; props: Props } - | { type: "update-length"; length: number } + | { type: "update-props"; props: Partial } | { type: "handle-change"; index: number; value: string | null; reset?: boolean } | { type: "handle-key-down"; index: number; event: KeyboardEvent } | { type: "start-composition"; index: number } | { type: "end-composition"; index: number; value: string }; export function reducer(state: State, action: Action): State { - console.log("action", action); - switch (action.type) { - case "init": { - state.props = { ...defaultProps, ...action.props }; - state.values.splice(state.cursor, state.props.length); - state.ready = true; - state.cursor = Math.min(state.cursor, state.props.length - 1); - return { ...state }; - } + case "update-props": { + state = { ...state, ...action.props, ready: true }; + // cannot use Array.splice as it does not keep empty array length + state.values = state.values.slice(state.cursor, state.length); + state.cursor = Math.min(state.cursor, state.length - 1); - case "update-length": { - state.values.splice(state.cursor, action.length); - state.props.length = action.length; - state.cursor = Math.min(state.cursor, action.length - 1); - return { ...state }; + return state; } case "start-composition": { - return { ...state, composition: true }; + return { ...state, dirty: true, composition: true }; } case "end-composition": { - state.composition = false; - if (action.value) { state.values[action.index] = action.value; } else { @@ -103,9 +108,9 @@ export function reducer(state: State, action: Action): State { } const dir = state.values[action.index] ? 1 : 0; - state.cursor = Math.min(action.index + dir, state.props.length - 1); + state.cursor = Math.min(action.index + dir, state.length - 1); - return { ...state }; + return { ...state, dirty: true, composition: false }; } case "handle-change": { @@ -114,68 +119,72 @@ export function reducer(state: State, action: Action): State { } if (action.reset) { - state.values = Array(state.props.length); + state.values = Array(state.length); } if (action.value) { - const values = action.value.split("").map(state.props.format); - const length = Math.min(state.props.length - action.index, values.length); + const values = action.value.split("").map(state.format); + const length = Math.min(state.length - action.index, values.length); state.values.splice(action.index, length, ...values.slice(0, length)); - state.cursor = Math.min(action.index + length, state.props.length - 1); + state.cursor = Math.min(action.index + length, state.length - 1); } else { delete state.values[action.index]; const dir = state.backspace ? 0 : 1; state.cursor = Math.max(0, action.index - dir); } - return { ...state, backspace: false }; + return { ...state, dirty: true, backspace: false }; } case "handle-key-down": { - const key = action.event.key === "Backspace" || action.event.key === "Delete"; - const which = action.event.which === BACKSPACE || action.event.which === DELETE; - const keyCode = action.event.keyCode === BACKSPACE || action.event.keyCode === DELETE; - const deletion = key || which || keyCode; + // determine if a deletion key is pressed + const fromKey = action.event.key === "Backspace" || action.event.key === "Delete"; + const fromCode = action.event.code === "Backspace" || action.event.code === "Delete"; + const fromKeyCode = action.event.keyCode === BACKSPACE || action.event.keyCode === DELETE; + const fromWhich = action.event.which === BACKSPACE || action.event.which === DELETE; + const deletion = fromKey || fromCode || fromKeyCode || fromWhich; + + // return the same state reference if no deletion detected + if (!deletion) { + break; + } // Deletion is a bit tricky and requires special attention. // - // When the current field has a value, deletion works as - // expected AS LONG AS THE STATE IS NOT UPDATED and keeps its - // reference: the value deletes by itself and the `onchange` - // event is triggered, which updates the state. - // - // But when the current field is empty, deletion does not - // trigger the `onchange` event. Therefore the state needs to be - // updated here. Moving the cursor backwards is enough for - // deletion to happen on the previous field, which triggers the - // `onchange` event and re-update the state. - if (deletion) { - // if empty value, move the cursor backwards and update the - // state - if (!state.values[action.index]) { - state.cursor = Math.max(0, action.index - 1); - - // let know the handle-change action that we already moved - // backwards and that we don't need to touch the cursor - // anymore - state.backspace = true; - - return { ...state }; - } - - // otherwise just return the same state and let the onchange - // event do the job + // When the field under cusor has a value and a deletion key is + // pressed, we want to let the browser to do the deletion for + // us, like a regular deletion in a normal input via the + // `onchange` event. For this to happen, we need to return the + // same state reference in order not to trigger any change. The + // state will be automatically updated by the handle-change + // action, when the deleted value will trigger the `onchange` + // event. + if (state.values[action.index]) { + break; } - break; + // But when the field under cursor is empty, deletion cannot + // happen by itself. The trick is to manually move the cursor + // backwards: the browser will then delete the value under this + // new cursor and perform the changes via the triggered + // `onchange` event. + else { + state.cursor = Math.max(0, action.index - 1); + + // let know the handle-change action that we already moved + // backwards and that we don't need to touch the cursor + // anymore + state.backspace = true; + + return { ...state, dirty: true }; + } } } return state; } -type Handler = { - init: (props: Props) => void; +export type Handler = { refs: RefObject; state: State; dispatch: ActionDispatch<[Action]>; @@ -184,10 +193,6 @@ type Handler = { }; export function usePinField(): Handler { - return useInternalHandler(); -} - -export function useInternalHandler(): Handler { const refs = useRef([]); const [state, dispatch] = useReducer(reducer, defaultState); @@ -206,117 +211,152 @@ export function useInternalHandler(): Handler { [dispatch, state.cursor], ); - const init = useCallback( - (props: Props) => { - dispatch({ type: "init", props }); - }, - [dispatch], - ); - - return useMemo( - () => ({ refs, state, dispatch, value, setValue, init }), - [refs, state, dispatch, value, setValue, init], - ); + return useMemo(() => ({ refs, state, dispatch, value, setValue }), [refs, state, dispatch, value, setValue]); } -export const PinFieldV2: FC = forwardRef(({ handler: customHandler, ...props }, fwdRef) => { - const internalHandler = useInternalHandler(); - const handler = customHandler || internalHandler; - - useImperativeHandle(fwdRef, () => handler.refs.current, [handler.refs]); - - console.log("state", handler.state); +export const PinFieldV2: FC = forwardRef( + ( + { + length = defaultProps.length, + format = defaultProps.format, + formatAriaLabel = defaultProps.formatAriaLabel, + onChange: handleChange = defaultProps.onChange, + onComplete: handleComplete = defaultProps.onComplete, + handler: customHandler, + autoFocus, + ...nativeProps + }, + fwdRef, + ) => { + const internalHandler = usePinField(); + const { refs, state, dispatch } = customHandler || internalHandler; - function setRefAt(index: number): (ref: HTMLInputElement) => void { - return ref => { - if (ref) { - handler.refs.current[index] = ref; - } - }; - } + useImperativeHandle(fwdRef, () => refs.current, [refs]); - function handleKeyDownAt(index: number): KeyboardEventHandler { - return event => { - handler.dispatch({ type: "handle-key-down", index, event }); - }; - } + function setRefAt(index: number): (ref: HTMLInputElement) => void { + return ref => { + if (ref) { + refs.current[index] = ref; + } + }; + } - function handleChangeAt(index: number): ChangeEventHandler { - return event => { - // should not happen, mostly for typescript to infer properly - if (!(event.nativeEvent instanceof InputEvent)) return; - handler.dispatch({ type: "handle-change", index, value: event.nativeEvent.data }); - }; - } + function handleKeyDownAt(index: number): KeyboardEventHandler { + return event => { + dispatch({ type: "handle-key-down", index, event }); + }; + } - function startCompositionAt(index: number): CompositionEventHandler { - return () => { - handler.dispatch({ type: "start-composition", index }); - }; - } + function handleChangeAt(index: number): ChangeEventHandler { + return event => { + // should never happen, mostly for typescript to infer properly + if (!(event.nativeEvent instanceof InputEvent)) return; + dispatch({ type: "handle-change", index, value: event.nativeEvent.data }); + }; + } - function endCompositionAt(index: number): CompositionEventHandler { - return event => { - handler.dispatch({ type: "end-composition", index, value: event.data }); - }; - } + function startCompositionAt(index: number): CompositionEventHandler { + return () => { + dispatch({ type: "start-composition", index }); + }; + } - useEffect(() => { - if (handler.state.ready) return; - handler.init(props); - }, [props, handler.state.ready, handler.init]); + function endCompositionAt(index: number): CompositionEventHandler { + return event => { + dispatch({ type: "end-composition", index, value: event.data }); + }; + } - useEffect(() => { - handler.init({ length: props.length }); - }, [handler.init, props.length !== handler.state.props.length]); + // initial props to state update + useEffect(() => { + if (state.ready) return; + const dir = nativeProps.dir?.toLowerCase() || document.documentElement.getAttribute("dir")?.toLowerCase(); + dispatch({ type: "update-props", props: { length, format, dir } }); + }, [state.ready, dispatch, length, format]); + + // props.length to state update + useEffect(() => { + if (!state.ready) return; + if (length === state.length) return; + dispatch({ type: "update-props", props: { length } }); + }, [state.ready, length, state.length, dispatch]); + + // props.format to state update + useEffect(() => { + if (!state.ready) return; + if (format === state.format) return; + dispatch({ type: "update-props", props: { format } }); + }, [state.ready, format, state.format, dispatch]); + + // nativeProps.dir to state update + useEffect(() => { + if (!state.ready) return; + const dir = nativeProps.dir?.toLowerCase() || document.documentElement.getAttribute("dir")?.toLowerCase(); + if (dir === state.dir) return; + dispatch({ type: "update-props", props: { dir } }); + }, [state.ready, nativeProps.dir, state.dir, dispatch]); + + // state to view update + useEffect(() => { + if (!refs.current) return; + if (!state.ready) return; + if (!state.dirty) return; + + let innerFocus = false; + let completed = state.values.length == state.length; + let value = ""; + + for (let index = 0; index < state.values.length; index++) { + const char = index in state.values ? state.values[index] : ""; + refs.current[index].value = char; + innerFocus = innerFocus || hasFocus(refs.current[index]); + completed = completed && index in state.values && refs.current[index].checkValidity(); + value += char; + } - // useEffect(() => { - // if (props.length === undefined) return; - // handler.dispatch({ type: "update-length", length: props.length }); - // }, [props.length !== handler.state.props.length, handler.dispatch]); + if (innerFocus) { + refs.current[state.cursor].focus(); + } - useEffect(() => { - if (!handler.state.ready) return; - if (!handler.refs.current) return; - console.log("state changed"); + if (handleChange) { + handleChange(value); + } - let innerFocus = false; + if (handleComplete && completed) { + handleComplete(value); + } + }, [refs, state, handleChange, handleComplete]); - for (let index = 0; index < handler.state.values.length; index++) { - const value = index in handler.state.values ? handler.state.values[index] : ""; - handler.refs.current[index].value = value; - innerFocus = innerFocus || hasFocus(handler.refs.current[index]); + // wait for props to be accessible in the state + if (!state.ready) { + return null; } - if (innerFocus) { - handler.refs.current[handler.state.cursor].focus(); + const inputs = range(0, state.length).map(index => ( + + )); + + if (state.dir === "rtl") { + inputs.reverse(); } - }, [handler.refs, handler.state]); - if (!handler.state.ready) { - return null; - } - - return ( - <> - {range(0, handler.state.props.length).map(index => ( - - ))} - - ); -}); + return inputs; + }, +); export function hasFocus(el: HTMLElement): boolean { try { diff --git a/src/utils/utils.ts b/src/utils/utils.ts index ca6d9e3..991f41d 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,6 +1,4 @@ -export function noop(): void { - // -} +export function noop(): void {} export function range(start: number, length: number): number[] { return Array.from({ length }, (_, i) => i + start); From 7fe4d511c2a7e5a5b794c797602ee67720341b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sun, 5 Jan 2025 12:39:23 +0100 Subject: [PATCH 05/13] init v2 unit tests --- babel.config.cjs | 7 + babel.config.cts | 5 - jest.config.ts => jest.config.mjs | 8 +- ...stories.scss => pin-field-v2.stories.scss} | 0 src/pin-field/pin-field-v2.stories.tsx | 2 + src/pin-field/pin-field-v2.test.ts | 519 ++++++++++++++++++ src/pin-field/pin-field-v2.tsx | 5 +- src/pin-field/pin-field.spec.tsx | 28 +- src/pin-field/pin-field.stories.tsx | 192 ------- src/pin-field/pin-field.test.ts | 110 ++-- src/pin-field/use-bireducer.spec.tsx | 2 +- src/polyfills/keyboard-evt.test.ts | 4 +- 12 files changed, 607 insertions(+), 275 deletions(-) create mode 100644 babel.config.cjs delete mode 100644 babel.config.cts rename jest.config.ts => jest.config.mjs (57%) rename src/pin-field/{pin-field.stories.scss => pin-field-v2.stories.scss} (100%) create mode 100644 src/pin-field/pin-field-v2.test.ts delete mode 100644 src/pin-field/pin-field.stories.tsx diff --git a/babel.config.cjs b/babel.config.cjs new file mode 100644 index 0000000..7aa143c --- /dev/null +++ b/babel.config.cjs @@ -0,0 +1,7 @@ +/** + * @see https://babeljs.io/docs/options + * @type {import("@babel/core").TransformOptions} + */ +module.exports = { + presets: ["@babel/preset-env", "@babel/preset-typescript", "@babel/preset-react"], +}; diff --git a/babel.config.cts b/babel.config.cts deleted file mode 100644 index cc1e6c8..0000000 --- a/babel.config.cts +++ /dev/null @@ -1,5 +0,0 @@ -import { TransformOptions } from "@babel/core"; - -export default { - presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-typescript", "@babel/preset-react"], -} satisfies TransformOptions; diff --git a/jest.config.ts b/jest.config.mjs similarity index 57% rename from jest.config.ts rename to jest.config.mjs index ed2e5b1..b26108a 100644 --- a/jest.config.ts +++ b/jest.config.mjs @@ -1,7 +1,9 @@ -import type { Config } from "jest"; - +/** + * @see https://jestjs.io/docs/configuration + * @type {import("jest").Config} + */ export default { collectCoverage: true, testEnvironment: "jest-environment-jsdom", testRegex: ".(test|spec).tsx?$", -} satisfies Config; +}; diff --git a/src/pin-field/pin-field.stories.scss b/src/pin-field/pin-field-v2.stories.scss similarity index 100% rename from src/pin-field/pin-field.stories.scss rename to src/pin-field/pin-field-v2.stories.scss diff --git a/src/pin-field/pin-field-v2.stories.tsx b/src/pin-field/pin-field-v2.stories.tsx index ec7d1e2..cc8c7d7 100644 --- a/src/pin-field/pin-field-v2.stories.tsx +++ b/src/pin-field/pin-field-v2.stories.tsx @@ -4,6 +4,8 @@ import { fn } from "@storybook/test"; import { PinFieldV2, defaultProps, Props, usePinField, InnerProps } from "./pin-field-v2"; +import "./pin-field-v2.stories.scss"; + const defaultArgs = { length: defaultProps.length, format: defaultProps.format, diff --git a/src/pin-field/pin-field-v2.test.ts b/src/pin-field/pin-field-v2.test.ts new file mode 100644 index 0000000..f2b6668 --- /dev/null +++ b/src/pin-field/pin-field-v2.test.ts @@ -0,0 +1,519 @@ +import React from "react"; + +import { noop } from "../utils"; +import { BACKSPACE, DELETE, defaultProps, defaultState } from "./pin-field-v2"; + +jest.mock("react", () => ({ + useCallback: (f: any) => f, + forwardRef: (f: any) => f, +})); + +function mockInput(value: string) { + const setValMock = jest.fn(); + const ref = { + focus: jest.fn(), + setCustomValidity: jest.fn(), + set value(val: string) { + setValMock(val); + }, + get value() { + return value; + }, + }; + + return { ref, setValMock }; +} + +test("constants", () => { + expect(BACKSPACE).toEqual(8); + expect(DELETE).toEqual(46); +}); + +test("default props", () => { + expect(defaultProps).toHaveProperty("length", 5); + expect(defaultProps).toHaveProperty("format", expect.any(Function)); + expect(defaultProps.format("a")).toStrictEqual("a"); + expect(defaultProps).toHaveProperty("formatAriaLabel", expect.any(Function)); + expect(defaultProps.formatAriaLabel(1, 2)).toStrictEqual("PIN field 1 of 2"); + expect(defaultProps).toHaveProperty("onChange"); + expect(defaultProps.onChange("a")).toStrictEqual(undefined); + expect(defaultProps).toHaveProperty("onComplete"); + expect(defaultProps.onComplete("a")).toStrictEqual(undefined); +}); + +test("default state", () => { + expect(defaultState).toHaveProperty("length", 5); + expect(defaultState).toHaveProperty("format", expect.any(Function)); + expect(defaultState).toHaveProperty("dir", "ltr"); + expect(defaultState).toHaveProperty("cursor", 0); + expect(defaultState).toHaveProperty("values", Array(5)); + expect(defaultState).toHaveProperty("backspace", false); + expect(defaultState).toHaveProperty("composition", false); + expect(defaultState).toHaveProperty("ready", false); + expect(defaultState).toHaveProperty("dirty", false); +}); + +// describe("state reducer", () => { +// const { NO_EFFECTS, stateReducer, defaultState, defaultProps } = pinField; +// const currState = defaultState(defaultProps); + +// test("default action", () => { +// // @ts-expect-error bad action +// const [state, eff] = stateReducer(currState, { type: "bad-action" }); + +// expect(state).toMatchObject(state); +// expect(eff).toEqual(NO_EFFECTS); +// }); + +// describe("handle-key-down", () => { +// test("unidentified", () => { +// const [state, eff] = stateReducer(currState, { +// type: "handle-key-down", +// key: "Unidentified", +// idx: 0, +// val: "", +// }); + +// expect(state).toMatchObject(state); +// expect(eff).toEqual([]); +// }); + +// test("dead", () => { +// const [state, eff] = stateReducer(currState, { +// type: "handle-key-down", +// key: "Dead", +// idx: 0, +// val: "", +// }); + +// expect(state).toMatchObject(state); +// expect(eff).toEqual([ +// { type: "set-input-val", idx: 0, val: "" }, +// { type: "reject-key", idx: 0, key: "Dead" }, +// { type: "handle-code-change" }, +// ]); +// }); + +// describe("left arrow", () => { +// test("from the first input", () => { +// const [state, eff] = stateReducer(currState, { +// type: "handle-key-down", +// key: "ArrowLeft", +// idx: 0, +// val: "", +// }); + +// expect(state).toMatchObject({ ...state, focusIdx: 0 }); +// expect(eff).toEqual([{ type: "focus-input", idx: 0 }]); +// }); + +// test("from the last input", () => { +// const [state, eff] = stateReducer( +// { ...currState, focusIdx: 4 }, +// { type: "handle-key-down", key: "ArrowLeft", idx: 0, val: "" }, +// ); + +// expect(state).toMatchObject({ ...state, focusIdx: 3 }); +// expect(eff).toEqual([{ type: "focus-input", idx: 3 }]); +// }); +// }); + +// describe("right arrow", () => { +// test("from the first input", () => { +// const [state, eff] = stateReducer(currState, { +// type: "handle-key-down", +// key: "ArrowRight", +// idx: 0, +// val: "", +// }); + +// expect(state).toMatchObject({ ...state, focusIdx: 1 }); +// expect(eff).toEqual([{ type: "focus-input", idx: 1 }]); +// }); + +// test("from the last input", () => { +// const [state, eff] = stateReducer( +// { ...currState, focusIdx: 4 }, +// { type: "handle-key-down", key: "ArrowRight", idx: 0, val: "" }, +// ); + +// expect(state).toMatchObject({ ...state, focusIdx: 4 }); +// expect(eff).toEqual([{ type: "focus-input", idx: 4 }]); +// }); +// }); + +// test("backspace", () => { +// const [state, eff] = stateReducer(currState, { +// type: "handle-key-down", +// key: "Backspace", +// idx: 0, +// val: "", +// }); + +// expect(state).toMatchObject({ ...state, focusIdx: 0 }); +// expect(eff).toEqual([{ type: "handle-delete", idx: 0 }, { type: "handle-code-change" }]); +// }); + +// test("delete", () => { +// const [state, eff] = stateReducer(currState, { +// type: "handle-key-down", +// key: "Delete", +// idx: 0, +// val: "", +// }); + +// expect(state).toMatchObject({ ...state, focusIdx: 0 }); +// expect(eff).toEqual([{ type: "handle-delete", idx: 0 }, { type: "handle-code-change" }]); +// }); + +// describe("default", () => { +// test("resolve", () => { +// const [state, eff] = stateReducer(currState, { +// type: "handle-key-down", +// key: "a", +// idx: 0, +// val: "", +// }); + +// expect(state).toMatchObject({ ...state, focusIdx: 1 }); +// expect(eff).toEqual([ +// { type: "set-input-val", idx: 0, val: "a" }, +// { type: "resolve-key", idx: 0, key: "a" }, +// { type: "focus-input", idx: 1 }, +// { type: "handle-code-change" }, +// ]); +// }); + +// test("reject", () => { +// const [state, eff] = stateReducer(currState, { +// type: "handle-key-down", +// key: "@", +// idx: 0, +// val: "", +// }); + +// expect(state).toMatchObject(state); +// expect(eff).toEqual([{ type: "reject-key", idx: 0, key: "@" }]); +// }); +// }); +// }); + +// describe("handle-key-up", () => { +// test("no fallback", () => { +// const [state, eff] = stateReducer(currState, { +// type: "handle-key-up", +// idx: 0, +// val: "", +// }); + +// expect(state).toMatchObject(state); +// expect(eff).toEqual([]); +// }); + +// test("empty prevVal, empty val", () => { +// const [state, eff] = stateReducer( +// { ...currState, fallback: { idx: 0, val: "" } }, +// { type: "handle-key-up", idx: 0, val: "" }, +// ); + +// expect(state).toMatchObject({ fallback: null }); +// expect(eff).toEqual([{ type: "handle-delete", idx: 0 }, { type: "handle-code-change" }]); +// }); + +// test("empty prevVal, not empty allowed val", () => { +// const [state, eff] = stateReducer( +// { ...currState, fallback: { idx: 0, val: "" } }, +// { type: "handle-key-up", idx: 0, val: "a" }, +// ); + +// expect(state).toMatchObject({ fallback: null }); +// expect(eff).toEqual([ +// { type: "set-input-val", idx: 0, val: "a" }, +// { type: "resolve-key", idx: 0, key: "a" }, +// { type: "focus-input", idx: 1 }, +// { type: "handle-code-change" }, +// ]); +// }); + +// test("empty prevVal, not empty denied val", () => { +// const [state, eff] = stateReducer( +// { ...currState, fallback: { idx: 0, val: "" } }, +// { type: "handle-key-up", idx: 0, val: "@" }, +// ); + +// expect(state).toMatchObject({ fallback: null }); +// expect(eff).toEqual([ +// { type: "set-input-val", idx: 0, val: "" }, +// { type: "reject-key", idx: 0, key: "@" }, +// { type: "handle-code-change" }, +// ]); +// }); + +// test("not empty prevVal", () => { +// const [state, eff] = stateReducer( +// { ...currState, fallback: { idx: 0, val: "a" } }, +// { type: "handle-key-up", idx: 0, val: "a" }, +// ); + +// expect(state).toMatchObject({ fallback: null }); +// expect(eff).toEqual([ +// { type: "set-input-val", idx: 0, val: "a" }, +// { type: "resolve-key", idx: 0, key: "a" }, +// { type: "focus-input", idx: 1 }, +// { type: "handle-code-change" }, +// ]); +// }); +// }); + +// describe("handle-paste", () => { +// test("paste smaller text than code length", () => { +// const [state, eff] = stateReducer(currState, { +// type: "handle-paste", +// idx: 0, +// val: "abc", +// }); + +// expect(state).toMatchObject({ ...state, focusIdx: 3 }); +// expect(eff).toEqual([ +// { type: "set-input-val", idx: 0, val: "a" }, +// { type: "resolve-key", idx: 0, key: "a" }, +// { type: "set-input-val", idx: 1, val: "b" }, +// { type: "resolve-key", idx: 1, key: "b" }, +// { type: "set-input-val", idx: 2, val: "c" }, +// { type: "resolve-key", idx: 2, key: "c" }, +// { type: "focus-input", idx: 3 }, +// { type: "handle-code-change" }, +// ]); +// }); + +// test("paste bigger text than code length", () => { +// const [state, eff] = stateReducer(currState, { +// type: "handle-paste", +// idx: 0, +// val: "abcdefgh", +// }); + +// expect(state).toMatchObject({ ...state, focusIdx: 4 }); +// expect(eff).toEqual([ +// { type: "set-input-val", idx: 0, val: "a" }, +// { type: "resolve-key", idx: 0, key: "a" }, +// { type: "set-input-val", idx: 1, val: "b" }, +// { type: "resolve-key", idx: 1, key: "b" }, +// { type: "set-input-val", idx: 2, val: "c" }, +// { type: "resolve-key", idx: 2, key: "c" }, +// { type: "set-input-val", idx: 3, val: "d" }, +// { type: "resolve-key", idx: 3, key: "d" }, +// { type: "set-input-val", idx: 4, val: "e" }, +// { type: "resolve-key", idx: 4, key: "e" }, +// { type: "focus-input", idx: 4 }, +// { type: "handle-code-change" }, +// ]); +// }); + +// test("paste on last input", () => { +// const [state, eff] = stateReducer({ ...currState, focusIdx: 4 }, { type: "handle-paste", idx: 0, val: "abc" }); + +// expect(state).toMatchObject({ ...state, focusIdx: 4 }); +// expect(eff).toEqual([ +// { type: "set-input-val", idx: 4, val: "a" }, +// { type: "resolve-key", idx: 4, key: "a" }, +// { type: "handle-code-change" }, +// ]); +// }); + +// test("paste with denied key", () => { +// const [state, eff] = stateReducer(currState, { +// type: "handle-paste", +// idx: 1, +// val: "ab@", +// }); + +// expect(state).toMatchObject(state); +// expect(eff).toEqual([ +// { type: "set-input-val", idx: 0, val: "" }, +// { type: "reject-key", idx: 1, key: "ab@" }, +// { type: "handle-code-change" }, +// ]); +// }); +// }); + +// test("focus-input", () => { +// const [state, eff] = stateReducer(currState, { type: "focus-input", idx: 2 }); + +// expect(state).toMatchObject({ ...state, focusIdx: 2 }); +// expect(eff).toEqual([{ type: "focus-input", idx: 2 }]); +// }); +// }); + +// describe("effect reducer", () => { +// const { defaultProps, useEffectReducer } = pinField; +// const inputA = mockInput("a"); +// const inputB = mockInput("b"); +// const inputC = mockInput(""); +// const propsFormatMock = jest.fn(); +// const propsMock = { +// ...defaultProps, +// length: 3, +// format: (char: string) => { +// propsFormatMock.apply(char); +// return char; +// }, +// onResolveKey: jest.fn(), +// onRejectKey: jest.fn(), +// onChange: jest.fn(), +// onComplete: jest.fn(), +// }; + +// const refs: React.RefObject = { +// current: [inputA.ref, inputB.ref, inputC.ref], +// }; +// const effectReducer = useEffectReducer({ ...propsMock, refs }); + +// beforeEach(() => { +// jest.resetAllMocks(); +// }); + +// test("default action", () => { +// // @ts-expect-error bad action +// effectReducer({ type: "bad-action" }); +// }); + +// test("focus input", () => { +// effectReducer({ type: "focus-input", idx: 0 }, noop); +// expect(inputA.ref.focus).toHaveBeenCalledTimes(1); +// }); + +// describe("set input val", () => { +// test("empty char", () => { +// effectReducer({ type: "set-input-val", idx: 0, val: "" }, noop); + +// expect(propsFormatMock).toHaveBeenCalledTimes(1); +// expect(inputA.setValMock).toHaveBeenCalledTimes(1); +// expect(inputA.setValMock).toHaveBeenCalledWith(""); +// }); + +// test("non empty char", () => { +// effectReducer({ type: "set-input-val", idx: 0, val: "a" }, noop); + +// expect(propsFormatMock).toHaveBeenCalledTimes(1); +// expect(inputA.setValMock).toHaveBeenCalledTimes(1); +// expect(inputA.setValMock).toHaveBeenCalledWith("a"); +// }); +// }); + +// test("resolve key", () => { +// effectReducer({ type: "resolve-key", idx: 0, key: "a" }, noop); + +// expect(inputA.ref.setCustomValidity).toHaveBeenCalledTimes(1); +// expect(inputA.ref.setCustomValidity).toHaveBeenCalledWith(""); +// expect(propsMock.onResolveKey).toHaveBeenCalledTimes(1); +// expect(propsMock.onResolveKey).toHaveBeenCalledWith("a", inputA.ref); +// }); + +// test("reject key", () => { +// effectReducer({ type: "reject-key", idx: 0, key: "a" }, noop); + +// expect(inputA.ref.setCustomValidity).toHaveBeenCalledTimes(1); +// expect(inputA.ref.setCustomValidity).toHaveBeenCalledWith("Invalid key"); +// expect(propsMock.onRejectKey).toHaveBeenCalledTimes(1); +// expect(propsMock.onRejectKey).toHaveBeenCalledWith("a", inputA.ref); +// }); + +// describe("handle backspace", () => { +// test("from input A, not empty val", () => { +// effectReducer({ type: "handle-delete", idx: 0 }, noop); + +// expect(inputA.ref.setCustomValidity).toHaveBeenCalledTimes(1); +// expect(inputA.ref.setCustomValidity).toHaveBeenCalledWith(""); +// expect(inputA.setValMock).toHaveBeenCalledTimes(1); +// expect(inputA.setValMock).toHaveBeenCalledWith(""); +// }); + +// test("from input B, not empty val", () => { +// effectReducer({ type: "handle-delete", idx: 1 }, noop); + +// expect(inputB.ref.setCustomValidity).toHaveBeenCalledTimes(1); +// expect(inputB.ref.setCustomValidity).toHaveBeenCalledWith(""); +// expect(inputB.setValMock).toHaveBeenCalledTimes(1); +// expect(inputB.setValMock).toHaveBeenCalledWith(""); +// }); + +// test("from input C, empty val", () => { +// effectReducer({ type: "handle-delete", idx: 2 }, noop); + +// expect(inputC.ref.setCustomValidity).toHaveBeenCalledTimes(1); +// expect(inputC.ref.setCustomValidity).toHaveBeenCalledWith(""); +// expect(inputC.setValMock).toHaveBeenCalledTimes(1); +// expect(inputC.setValMock).toHaveBeenCalledWith(""); +// expect(inputB.ref.focus).toHaveBeenCalledTimes(1); +// expect(inputB.ref.setCustomValidity).toHaveBeenCalledWith(""); +// expect(inputB.setValMock).toHaveBeenCalledTimes(1); +// expect(inputB.setValMock).toHaveBeenCalledWith(""); +// }); +// }); + +// describe("handle-code-change", () => { +// test("code not complete", () => { +// effectReducer({ type: "handle-code-change" }, noop); + +// expect(propsMock.onChange).toHaveBeenCalledTimes(1); +// expect(propsMock.onChange).toHaveBeenCalledWith("ab"); +// }); + +// test("code complete", () => { +// const inputA = mockInput("a"); +// const inputB = mockInput("b"); +// const inputC = mockInput("c"); +// const refs: React.RefObject = { +// current: [inputA.ref, inputB.ref, inputC.ref], +// }; +// const notify = useEffectReducer({ ...propsMock, refs }); + +// notify({ type: "handle-code-change" }, noop); + +// expect(propsMock.onChange).toHaveBeenCalledTimes(1); +// expect(propsMock.onChange).toHaveBeenCalledWith("abc"); +// expect(propsMock.onComplete).toHaveBeenCalledTimes(1); +// expect(propsMock.onComplete).toHaveBeenCalledWith("abc"); +// }); + +// test("rtl", () => { +// jest.spyOn(document.documentElement, "getAttribute").mockImplementation(() => "rtl"); + +// const inputA = mockInput("a"); +// const inputB = mockInput("b"); +// const inputC = mockInput("c"); +// const refs: React.RefObject = { +// current: [inputA.ref, inputB.ref, inputC.ref], +// }; +// const notify = useEffectReducer({ ...propsMock, refs }); + +// notify({ type: "handle-code-change" }, noop); + +// expect(propsMock.onChange).toHaveBeenCalledTimes(1); +// expect(propsMock.onChange).toHaveBeenCalledWith("cba"); +// expect(propsMock.onComplete).toHaveBeenCalledTimes(1); +// expect(propsMock.onComplete).toHaveBeenCalledWith("cba"); +// }); + +// test("rtl with override in props", () => { +// jest.spyOn(document.documentElement, "getAttribute").mockImplementation(() => "rtl"); + +// const inputA = mockInput("a"); +// const inputB = mockInput("b"); +// const inputC = mockInput("c"); +// const refs: React.RefObject = { +// current: [inputA.ref, inputB.ref, inputC.ref], +// }; +// const propsWithDir = { ...propsMock, dir: "ltr" }; +// const notify = useEffectReducer({ ...propsWithDir, refs }); + +// notify({ type: "handle-code-change" }, noop); + +// expect(propsMock.onChange).toHaveBeenCalledTimes(1); +// expect(propsMock.onChange).toHaveBeenCalledWith("abc"); +// expect(propsMock.onComplete).toHaveBeenCalledTimes(1); +// expect(propsMock.onComplete).toHaveBeenCalledWith("abc"); +// }); +// }); +// }); diff --git a/src/pin-field/pin-field-v2.tsx b/src/pin-field/pin-field-v2.tsx index 7c2f0b2..bf4636a 100644 --- a/src/pin-field/pin-field-v2.tsx +++ b/src/pin-field/pin-field-v2.tsx @@ -18,8 +18,8 @@ import { import { noop, range } from "../utils"; -const BACKSPACE = 8; -const DELETE = 46; +export const BACKSPACE = 8; +export const DELETE = 46; export type InnerProps = { length: number; @@ -327,7 +327,6 @@ export const PinFieldV2: FC = forwardRef( } }, [refs, state, handleChange, handleComplete]); - // wait for props to be accessible in the state if (!state.ready) { return null; } diff --git a/src/pin-field/pin-field.spec.tsx b/src/pin-field/pin-field.spec.tsx index 6f9ccd0..f6e544c 100644 --- a/src/pin-field/pin-field.spec.tsx +++ b/src/pin-field/pin-field.spec.tsx @@ -7,7 +7,7 @@ import PinField from "./pin-field"; const TEST_ID = "test"; -test("structure", async () => { +test.skip("structure", async () => { render(); const inputs = await screen.findAllByTestId(TEST_ID); @@ -21,7 +21,7 @@ test("structure", async () => { }); }); -test("ref as object", () => { +test.skip("ref as object", () => { const ref: { current: HTMLInputElement[] | null } = { current: [] }; render(); @@ -29,7 +29,7 @@ test("ref as object", () => { expect(ref.current).toHaveLength(5); }); -test("ref as func", () => { +test.skip("ref as func", () => { const ref: { current: HTMLInputElement[] | null } = { current: [] }; render( { expect(ref.current).toHaveLength(5); }); -test("autoFocus", async () => { +test.skip("autoFocus", async () => { render(); const inputs = await screen.findAllByTestId(TEST_ID); @@ -56,7 +56,7 @@ test("autoFocus", async () => { }); }); -test("className", async () => { +test.skip("className", async () => { render(); const inputs = await screen.findAllByTestId(TEST_ID); @@ -65,7 +65,7 @@ test("className", async () => { }); }); -test("style", async () => { +test.skip("style", async () => { render(); const inputs = await screen.findAllByTestId(TEST_ID); @@ -74,7 +74,7 @@ test("style", async () => { }); }); -test("events", async () => { +test.skip("events", async () => { const handleChangeMock = jest.fn(); const handleCompleteMock = jest.fn(); render(); @@ -91,7 +91,7 @@ test("events", async () => { expect(handleCompleteMock).toHaveBeenCalledWith("abcd"); }); -test("fallback events", async () => { +test.skip("fallback events", async () => { const handleChangeMock = jest.fn(); const handleCompleteMock = jest.fn(); render(); @@ -105,8 +105,8 @@ test("fallback events", async () => { expect(handleChangeMock).toHaveBeenCalledWith("a"); }); -describe("a11y", () => { - test("should have aria-label per input field", () => { +describe.skip("a11y", () => { + test.skip("should have aria-label per input field", () => { render(); expect(screen.getByRole("textbox", { name: /pin code 1 of 3/i })).toBeVisible(); @@ -114,7 +114,7 @@ describe("a11y", () => { expect(screen.getByRole("textbox", { name: /pin code 3 of 3/i })).toBeVisible(); }); - test("should support custom aria-label format", () => { + test.skip("should support custom aria-label format", () => { render( `${i}/${c}`} />); expect(screen.getByRole("textbox", { name: "1/3" })).toBeVisible(); @@ -122,7 +122,7 @@ describe("a11y", () => { expect(screen.getByRole("textbox", { name: "3/3" })).toBeVisible(); }); - test("every input has aria-required", () => { + test.skip("every input has aria-required", () => { render(); expect(screen.getByRole("textbox", { name: /pin code 1 of 3/i })).toHaveAttribute("aria-required", "true"); @@ -130,7 +130,7 @@ describe("a11y", () => { expect(screen.getByRole("textbox", { name: /pin code 3 of 3/i })).toHaveAttribute("aria-required", "true"); }); - test("every input should have aria-disabled when PinField is disabled", () => { + test.skip("every input should have aria-disabled when PinField is disabled", () => { render(); expect(screen.getByRole("textbox", { name: /pin code 1 of 3/i })).toHaveAttribute("aria-disabled", "true"); @@ -138,7 +138,7 @@ describe("a11y", () => { expect(screen.getByRole("textbox", { name: /pin code 3 of 3/i })).toHaveAttribute("aria-disabled", "true"); }); - test("every input should have aria-readonly when PinField is readOnly", () => { + test.skip("every input should have aria-readonly when PinField is readOnly", () => { render(); expect(screen.getByRole("textbox", { name: /pin code 1 of 3/i })).toHaveAttribute("aria-readonly", "true"); diff --git a/src/pin-field/pin-field.stories.tsx b/src/pin-field/pin-field.stories.tsx deleted file mode 100644 index 4263bf0..0000000 --- a/src/pin-field/pin-field.stories.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import { FC, useRef, useState, StrictMode as ReactStrictMode } from "react"; -import type { Meta, StoryObj } from "@storybook/react"; -import { fn } from "@storybook/test"; -import cn from "classnames"; - -import type { PinFieldProps } from "./pin-field.types"; -import { PinField, defaultProps } from "./pin-field"; - -import "./pin-field.stories.scss"; - -const defaultArgs = { - length: defaultProps.length, - onResolveKey: fn(), - onRejectKey: fn(), - onChange: fn(), - onComplete: fn(), -} satisfies PinFieldProps; - -/** - * The `` component is a simple wrapper around a list of HTML inputs. - * - * The component exposes 4 event handlers, see stories below to learn more about the other props. - */ -const meta: Meta = { - title: "PinField", - component: PinField, - tags: ["autodocs"], - parameters: { - layout: "centered", - }, -}; - -export const Default: StoryObj = { - render: props => , - args: defaultArgs, -}; - -export const StrictMode: StoryObj = { - render: props => ( - - - - ), - args: defaultArgs, -}; - -/** - * Every HTML input can be controlled thanks to a React reference. - */ -export const Reference: StoryObj = { - render: props => { - const ref = useRef([]); - - return ( - <> -
- -
-
- - -
- - ); - }, - args: defaultArgs, -}; - -/** - * Characters can be validated with a validator. A validator can take the form of: - * - * - a String of allowed characters: `abcABC123` - * - an Array of allowed characters: `["a", "b", "c", "1", "2", "3"]` - * - a RegExp: `/^[a-zA-Z0-9]$/` - * - a predicate: `(char: string) => boolean` - */ -export const Validate: StoryObj> = { - render: ({ validateRegExp, ...props }) => { - try { - const validate = new RegExp(validateRegExp); - return ; - } catch (err: any) { - return
Invalid RegExp: {err.toString()}
; - } - }, - argTypes: { - validateRegExp: { - name: "validate (RegExp)", - }, - }, - args: { - validateRegExp: "[0-9]", - ...defaultArgs, - }, -}; - -/** - * Characters can be formatted with a formatter `(char: string) => string`. - */ -export const Format: StoryObj> = { - render: ({ formatEval, ...props }) => { - try { - let format = eval(formatEval); - format("a"); - return ; - } catch (err: any) { - return
Invalid format function: {err.toString()}
; - } - }, - argTypes: { - formatEval: { - control: "text", - name: "format (fn eval)", - }, - }, - args: { - formatEval: "char => char.toUpperCase()", - ...defaultArgs, - }, -}; - -/** - * Props inherit from `InputHTMLAttributes`. - */ -export const HTMLInputAttributes: StoryObj> = { - render: ({ formatAriaLabelEval, ...props }) => { - try { - let formatAriaLabel = eval(formatAriaLabelEval); - formatAriaLabel(0, 0); - return ; - } catch (err: any) { - return
Invalid format aria label function: {err.toString()}
; - } - }, - argTypes: { - formatAriaLabelEval: { - control: "text", - name: "formatAriaLabel (fn eval)", - }, - type: { - control: "select", - options: ["text", "number", "password"], - }, - }, - args: { - type: "password", - autoFocus: true, - disabled: false, - autoCorrect: "off", - autoComplete: "off", - formatAriaLabelEval: "(i, n) => `field ${i}/${n}`", - ...defaultArgs, - }, -}; - -/** - * Finally, the pin field can be styled either with `style` or `className`. - * - * This last one allows you to use pseudo-classes like `:nth-of-type`,`:focus`, `:hover`,`:valid`,`:invalid`… - */ -export const Styled: StoryObj = { - render: props => { - const [done, setDone] = useState(false); - const className = cn(props.className, { complete: done }); - const format = (val: string) => val.toUpperCase(); - const handleComplete = (code: string) => { - setDone(true); - if (props.onComplete) props.onComplete(code); - }; - - return ( - - ); - }, - args: { - className: "pin-field", - style: {}, - ...defaultArgs, - }, -}; - -export default meta; diff --git a/src/pin-field/pin-field.test.ts b/src/pin-field/pin-field.test.ts index 9d3f796..e6c4062 100644 --- a/src/pin-field/pin-field.test.ts +++ b/src/pin-field/pin-field.test.ts @@ -24,7 +24,7 @@ function mockInput(value: string) { return { ref, setValMock }; } -test("constants", () => { +test.skip("constants", () => { const { NO_EFFECTS, PROP_KEYS, HANDLER_KEYS, IGNORED_META_KEYS } = pinField; expect(NO_EFFECTS).toEqual([]); @@ -33,7 +33,7 @@ test("constants", () => { expect(IGNORED_META_KEYS).toEqual(["Alt", "Control", "Enter", "Meta", "Shift", "Tab"]); }); -test("default props", () => { +test.skip("default props", () => { const { defaultProps } = pinField; expect(defaultProps).toHaveProperty("length", 5); @@ -50,7 +50,7 @@ test("default props", () => { expect(defaultProps.onComplete("a")).toStrictEqual(undefined); }); -test("default state", () => { +test.skip("default state", () => { const { defaultState, defaultProps } = pinField; const state = defaultState(defaultProps); @@ -63,7 +63,7 @@ test("default state", () => { expect(state).toHaveProperty("fallback", null); }); -test("get previous focus index", () => { +test.skip("get previous focus index", () => { const { getPrevFocusIdx } = pinField; expect(getPrevFocusIdx(5)).toStrictEqual(4); @@ -72,7 +72,7 @@ test("get previous focus index", () => { expect(getPrevFocusIdx(-1)).toStrictEqual(0); }); -test("get next focus index", () => { +test.skip("get next focus index", () => { const { getNextFocusIdx } = pinField; expect(getNextFocusIdx(0, 0)).toStrictEqual(0); @@ -83,10 +83,10 @@ test("get next focus index", () => { expect(getNextFocusIdx(5, 3)).toStrictEqual(2); }); -describe("is key allowed", () => { +describe.skip("is key allowed", () => { const { isKeyAllowed } = pinField; - test("string", () => { + test.skip("string", () => { const str = isKeyAllowed("a"); expect(str("")).toStrictEqual(false); @@ -95,7 +95,7 @@ describe("is key allowed", () => { expect(str("ab")).toStrictEqual(false); }); - test("array", () => { + test.skip("array", () => { const arr = isKeyAllowed(["a", "b"]); expect(arr("a")).toStrictEqual(true); @@ -104,7 +104,7 @@ describe("is key allowed", () => { expect(arr("ab")).toStrictEqual(false); }); - test("regex", () => { + test.skip("regex", () => { const exp = isKeyAllowed(/^[ab]$/); expect(exp("a")).toStrictEqual(true); @@ -112,7 +112,7 @@ describe("is key allowed", () => { expect(exp("ab")).toStrictEqual(false); }); - test("function", () => { + test.skip("function", () => { const func = isKeyAllowed(k => k === "a" || k === "b"); expect(func("a")).toStrictEqual(true); @@ -121,11 +121,11 @@ describe("is key allowed", () => { }); }); -describe("state reducer", () => { +describe.skip("state reducer", () => { const { NO_EFFECTS, stateReducer, defaultState, defaultProps } = pinField; const currState = defaultState(defaultProps); - test("default action", () => { + test.skip("default action", () => { // @ts-expect-error bad action const [state, eff] = stateReducer(currState, { type: "bad-action" }); @@ -133,8 +133,8 @@ describe("state reducer", () => { expect(eff).toEqual(NO_EFFECTS); }); - describe("handle-key-down", () => { - test("unidentified", () => { + describe.skip("handle-key-down", () => { + test.skip("unidentified", () => { const [state, eff] = stateReducer(currState, { type: "handle-key-down", key: "Unidentified", @@ -146,7 +146,7 @@ describe("state reducer", () => { expect(eff).toEqual([]); }); - test("dead", () => { + test.skip("dead", () => { const [state, eff] = stateReducer(currState, { type: "handle-key-down", key: "Dead", @@ -162,8 +162,8 @@ describe("state reducer", () => { ]); }); - describe("left arrow", () => { - test("from the first input", () => { + describe.skip("left arrow", () => { + test.skip("from the first input", () => { const [state, eff] = stateReducer(currState, { type: "handle-key-down", key: "ArrowLeft", @@ -175,7 +175,7 @@ describe("state reducer", () => { expect(eff).toEqual([{ type: "focus-input", idx: 0 }]); }); - test("from the last input", () => { + test.skip("from the last input", () => { const [state, eff] = stateReducer( { ...currState, focusIdx: 4 }, { type: "handle-key-down", key: "ArrowLeft", idx: 0, val: "" }, @@ -186,8 +186,8 @@ describe("state reducer", () => { }); }); - describe("right arrow", () => { - test("from the first input", () => { + describe.skip("right arrow", () => { + test.skip("from the first input", () => { const [state, eff] = stateReducer(currState, { type: "handle-key-down", key: "ArrowRight", @@ -199,7 +199,7 @@ describe("state reducer", () => { expect(eff).toEqual([{ type: "focus-input", idx: 1 }]); }); - test("from the last input", () => { + test.skip("from the last input", () => { const [state, eff] = stateReducer( { ...currState, focusIdx: 4 }, { type: "handle-key-down", key: "ArrowRight", idx: 0, val: "" }, @@ -210,7 +210,7 @@ describe("state reducer", () => { }); }); - test("backspace", () => { + test.skip("backspace", () => { const [state, eff] = stateReducer(currState, { type: "handle-key-down", key: "Backspace", @@ -222,7 +222,7 @@ describe("state reducer", () => { expect(eff).toEqual([{ type: "handle-delete", idx: 0 }, { type: "handle-code-change" }]); }); - test("delete", () => { + test.skip("delete", () => { const [state, eff] = stateReducer(currState, { type: "handle-key-down", key: "Delete", @@ -234,8 +234,8 @@ describe("state reducer", () => { expect(eff).toEqual([{ type: "handle-delete", idx: 0 }, { type: "handle-code-change" }]); }); - describe("default", () => { - test("resolve", () => { + describe.skip("default", () => { + test.skip("resolve", () => { const [state, eff] = stateReducer(currState, { type: "handle-key-down", key: "a", @@ -252,7 +252,7 @@ describe("state reducer", () => { ]); }); - test("reject", () => { + test.skip("reject", () => { const [state, eff] = stateReducer(currState, { type: "handle-key-down", key: "@", @@ -266,8 +266,8 @@ describe("state reducer", () => { }); }); - describe("handle-key-up", () => { - test("no fallback", () => { + describe.skip("handle-key-up", () => { + test.skip("no fallback", () => { const [state, eff] = stateReducer(currState, { type: "handle-key-up", idx: 0, @@ -278,7 +278,7 @@ describe("state reducer", () => { expect(eff).toEqual([]); }); - test("empty prevVal, empty val", () => { + test.skip("empty prevVal, empty val", () => { const [state, eff] = stateReducer( { ...currState, fallback: { idx: 0, val: "" } }, { type: "handle-key-up", idx: 0, val: "" }, @@ -288,7 +288,7 @@ describe("state reducer", () => { expect(eff).toEqual([{ type: "handle-delete", idx: 0 }, { type: "handle-code-change" }]); }); - test("empty prevVal, not empty allowed val", () => { + test.skip("empty prevVal, not empty allowed val", () => { const [state, eff] = stateReducer( { ...currState, fallback: { idx: 0, val: "" } }, { type: "handle-key-up", idx: 0, val: "a" }, @@ -303,7 +303,7 @@ describe("state reducer", () => { ]); }); - test("empty prevVal, not empty denied val", () => { + test.skip("empty prevVal, not empty denied val", () => { const [state, eff] = stateReducer( { ...currState, fallback: { idx: 0, val: "" } }, { type: "handle-key-up", idx: 0, val: "@" }, @@ -317,7 +317,7 @@ describe("state reducer", () => { ]); }); - test("not empty prevVal", () => { + test.skip("not empty prevVal", () => { const [state, eff] = stateReducer( { ...currState, fallback: { idx: 0, val: "a" } }, { type: "handle-key-up", idx: 0, val: "a" }, @@ -333,8 +333,8 @@ describe("state reducer", () => { }); }); - describe("handle-paste", () => { - test("paste smaller text than code length", () => { + describe.skip("handle-paste", () => { + test.skip("paste smaller text than code length", () => { const [state, eff] = stateReducer(currState, { type: "handle-paste", idx: 0, @@ -354,7 +354,7 @@ describe("state reducer", () => { ]); }); - test("paste bigger text than code length", () => { + test.skip("paste bigger text than code length", () => { const [state, eff] = stateReducer(currState, { type: "handle-paste", idx: 0, @@ -378,7 +378,7 @@ describe("state reducer", () => { ]); }); - test("paste on last input", () => { + test.skip("paste on last input", () => { const [state, eff] = stateReducer({ ...currState, focusIdx: 4 }, { type: "handle-paste", idx: 0, val: "abc" }); expect(state).toMatchObject({ ...state, focusIdx: 4 }); @@ -389,7 +389,7 @@ describe("state reducer", () => { ]); }); - test("paste with denied key", () => { + test.skip("paste with denied key", () => { const [state, eff] = stateReducer(currState, { type: "handle-paste", idx: 1, @@ -405,7 +405,7 @@ describe("state reducer", () => { }); }); - test("focus-input", () => { + test.skip("focus-input", () => { const [state, eff] = stateReducer(currState, { type: "focus-input", idx: 2 }); expect(state).toMatchObject({ ...state, focusIdx: 2 }); @@ -413,7 +413,7 @@ describe("state reducer", () => { }); }); -describe("effect reducer", () => { +describe.skip("effect reducer", () => { const { defaultProps, useEffectReducer } = pinField; const inputA = mockInput("a"); const inputB = mockInput("b"); @@ -441,18 +441,18 @@ describe("effect reducer", () => { jest.resetAllMocks(); }); - test("default action", () => { + test.skip("default action", () => { // @ts-expect-error bad action effectReducer({ type: "bad-action" }); }); - test("focus input", () => { + test.skip("focus input", () => { effectReducer({ type: "focus-input", idx: 0 }, noop); expect(inputA.ref.focus).toHaveBeenCalledTimes(1); }); - describe("set input val", () => { - test("empty char", () => { + describe.skip("set input val", () => { + test.skip("empty char", () => { effectReducer({ type: "set-input-val", idx: 0, val: "" }, noop); expect(propsFormatMock).toHaveBeenCalledTimes(1); @@ -460,7 +460,7 @@ describe("effect reducer", () => { expect(inputA.setValMock).toHaveBeenCalledWith(""); }); - test("non empty char", () => { + test.skip("non empty char", () => { effectReducer({ type: "set-input-val", idx: 0, val: "a" }, noop); expect(propsFormatMock).toHaveBeenCalledTimes(1); @@ -469,7 +469,7 @@ describe("effect reducer", () => { }); }); - test("resolve key", () => { + test.skip("resolve key", () => { effectReducer({ type: "resolve-key", idx: 0, key: "a" }, noop); expect(inputA.ref.setCustomValidity).toHaveBeenCalledTimes(1); @@ -478,7 +478,7 @@ describe("effect reducer", () => { expect(propsMock.onResolveKey).toHaveBeenCalledWith("a", inputA.ref); }); - test("reject key", () => { + test.skip("reject key", () => { effectReducer({ type: "reject-key", idx: 0, key: "a" }, noop); expect(inputA.ref.setCustomValidity).toHaveBeenCalledTimes(1); @@ -487,8 +487,8 @@ describe("effect reducer", () => { expect(propsMock.onRejectKey).toHaveBeenCalledWith("a", inputA.ref); }); - describe("handle backspace", () => { - test("from input A, not empty val", () => { + describe.skip("handle backspace", () => { + test.skip("from input A, not empty val", () => { effectReducer({ type: "handle-delete", idx: 0 }, noop); expect(inputA.ref.setCustomValidity).toHaveBeenCalledTimes(1); @@ -497,7 +497,7 @@ describe("effect reducer", () => { expect(inputA.setValMock).toHaveBeenCalledWith(""); }); - test("from input B, not empty val", () => { + test.skip("from input B, not empty val", () => { effectReducer({ type: "handle-delete", idx: 1 }, noop); expect(inputB.ref.setCustomValidity).toHaveBeenCalledTimes(1); @@ -506,7 +506,7 @@ describe("effect reducer", () => { expect(inputB.setValMock).toHaveBeenCalledWith(""); }); - test("from input C, empty val", () => { + test.skip("from input C, empty val", () => { effectReducer({ type: "handle-delete", idx: 2 }, noop); expect(inputC.ref.setCustomValidity).toHaveBeenCalledTimes(1); @@ -520,15 +520,15 @@ describe("effect reducer", () => { }); }); - describe("handle-code-change", () => { - test("code not complete", () => { + describe.skip("handle-code-change", () => { + test.skip("code not complete", () => { effectReducer({ type: "handle-code-change" }, noop); expect(propsMock.onChange).toHaveBeenCalledTimes(1); expect(propsMock.onChange).toHaveBeenCalledWith("ab"); }); - test("code complete", () => { + test.skip("code complete", () => { const inputA = mockInput("a"); const inputB = mockInput("b"); const inputC = mockInput("c"); @@ -545,7 +545,7 @@ describe("effect reducer", () => { expect(propsMock.onComplete).toHaveBeenCalledWith("abc"); }); - test("rtl", () => { + test.skip("rtl", () => { jest.spyOn(document.documentElement, "getAttribute").mockImplementation(() => "rtl"); const inputA = mockInput("a"); @@ -564,7 +564,7 @@ describe("effect reducer", () => { expect(propsMock.onComplete).toHaveBeenCalledWith("cba"); }); - test("rtl with override in props", () => { + test.skip("rtl with override in props", () => { jest.spyOn(document.documentElement, "getAttribute").mockImplementation(() => "rtl"); const inputA = mockInput("a"); diff --git a/src/pin-field/use-bireducer.spec.tsx b/src/pin-field/use-bireducer.spec.tsx index 8013f51..6555263 100644 --- a/src/pin-field/use-bireducer.spec.tsx +++ b/src/pin-field/use-bireducer.spec.tsx @@ -46,7 +46,7 @@ const effectReducer: EffectReducer = (effect, dispatch) => { } }; -describe("useBireducer", () => { +describe.skip("useBireducer", () => { beforeAll(() => { global.Storage.prototype.setItem = jest.fn(); global.Storage.prototype.clear = jest.fn(); diff --git a/src/polyfills/keyboard-evt.test.ts b/src/polyfills/keyboard-evt.test.ts index b99ac16..ee1aeae 100644 --- a/src/polyfills/keyboard-evt.test.ts +++ b/src/polyfills/keyboard-evt.test.ts @@ -1,7 +1,7 @@ import { getKeyFromKeyboardEvent } from "."; -describe("keyboard-evt", () => { - test("getKey", () => { +describe.skip("keyboard-evt", () => { + test.skip("getKey", () => { const cases: Array<[KeyboardEventInit, string]> = [ [{}, "Unidentified"], [{ key: "a" }, "a"], From 0eb9ecc1949a9f76f6211c14d7e24a9fc7089b54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sun, 5 Jan 2025 16:18:31 +0100 Subject: [PATCH 06/13] continue v2 unit tests --- src/pin-field/pin-field-v2.test.ts | 784 ++++++++++++++++++----------- src/pin-field/pin-field-v2.tsx | 50 +- 2 files changed, 529 insertions(+), 305 deletions(-) diff --git a/src/pin-field/pin-field-v2.test.ts b/src/pin-field/pin-field-v2.test.ts index f2b6668..b131f4a 100644 --- a/src/pin-field/pin-field-v2.test.ts +++ b/src/pin-field/pin-field-v2.test.ts @@ -1,7 +1,7 @@ import React from "react"; import { noop } from "../utils"; -import { BACKSPACE, DELETE, defaultProps, defaultState } from "./pin-field-v2"; +import { reducer, BACKSPACE, DELETE, defaultProps, defaultState, State } from "./pin-field-v2"; jest.mock("react", () => ({ useCallback: (f: any) => f, @@ -44,6 +44,7 @@ test("default props", () => { test("default state", () => { expect(defaultState).toHaveProperty("length", 5); expect(defaultState).toHaveProperty("format", expect.any(Function)); + expect(defaultState.format("a")).toStrictEqual("a"); expect(defaultState).toHaveProperty("dir", "ltr"); expect(defaultState).toHaveProperty("cursor", 0); expect(defaultState).toHaveProperty("values", Array(5)); @@ -53,297 +54,496 @@ test("default state", () => { expect(defaultState).toHaveProperty("dirty", false); }); -// describe("state reducer", () => { -// const { NO_EFFECTS, stateReducer, defaultState, defaultProps } = pinField; -// const currState = defaultState(defaultProps); - -// test("default action", () => { -// // @ts-expect-error bad action -// const [state, eff] = stateReducer(currState, { type: "bad-action" }); - -// expect(state).toMatchObject(state); -// expect(eff).toEqual(NO_EFFECTS); -// }); - -// describe("handle-key-down", () => { -// test("unidentified", () => { -// const [state, eff] = stateReducer(currState, { -// type: "handle-key-down", -// key: "Unidentified", -// idx: 0, -// val: "", -// }); - -// expect(state).toMatchObject(state); -// expect(eff).toEqual([]); -// }); - -// test("dead", () => { -// const [state, eff] = stateReducer(currState, { -// type: "handle-key-down", -// key: "Dead", -// idx: 0, -// val: "", -// }); - -// expect(state).toMatchObject(state); -// expect(eff).toEqual([ -// { type: "set-input-val", idx: 0, val: "" }, -// { type: "reject-key", idx: 0, key: "Dead" }, -// { type: "handle-code-change" }, -// ]); -// }); - -// describe("left arrow", () => { -// test("from the first input", () => { -// const [state, eff] = stateReducer(currState, { -// type: "handle-key-down", -// key: "ArrowLeft", -// idx: 0, -// val: "", -// }); - -// expect(state).toMatchObject({ ...state, focusIdx: 0 }); -// expect(eff).toEqual([{ type: "focus-input", idx: 0 }]); -// }); - -// test("from the last input", () => { -// const [state, eff] = stateReducer( -// { ...currState, focusIdx: 4 }, -// { type: "handle-key-down", key: "ArrowLeft", idx: 0, val: "" }, -// ); - -// expect(state).toMatchObject({ ...state, focusIdx: 3 }); -// expect(eff).toEqual([{ type: "focus-input", idx: 3 }]); -// }); -// }); - -// describe("right arrow", () => { -// test("from the first input", () => { -// const [state, eff] = stateReducer(currState, { -// type: "handle-key-down", -// key: "ArrowRight", -// idx: 0, -// val: "", -// }); - -// expect(state).toMatchObject({ ...state, focusIdx: 1 }); -// expect(eff).toEqual([{ type: "focus-input", idx: 1 }]); -// }); - -// test("from the last input", () => { -// const [state, eff] = stateReducer( -// { ...currState, focusIdx: 4 }, -// { type: "handle-key-down", key: "ArrowRight", idx: 0, val: "" }, -// ); - -// expect(state).toMatchObject({ ...state, focusIdx: 4 }); -// expect(eff).toEqual([{ type: "focus-input", idx: 4 }]); -// }); -// }); - -// test("backspace", () => { -// const [state, eff] = stateReducer(currState, { -// type: "handle-key-down", -// key: "Backspace", -// idx: 0, -// val: "", -// }); - -// expect(state).toMatchObject({ ...state, focusIdx: 0 }); -// expect(eff).toEqual([{ type: "handle-delete", idx: 0 }, { type: "handle-code-change" }]); -// }); - -// test("delete", () => { -// const [state, eff] = stateReducer(currState, { -// type: "handle-key-down", -// key: "Delete", -// idx: 0, -// val: "", -// }); - -// expect(state).toMatchObject({ ...state, focusIdx: 0 }); -// expect(eff).toEqual([{ type: "handle-delete", idx: 0 }, { type: "handle-code-change" }]); -// }); - -// describe("default", () => { -// test("resolve", () => { -// const [state, eff] = stateReducer(currState, { -// type: "handle-key-down", -// key: "a", -// idx: 0, -// val: "", -// }); - -// expect(state).toMatchObject({ ...state, focusIdx: 1 }); -// expect(eff).toEqual([ -// { type: "set-input-val", idx: 0, val: "a" }, -// { type: "resolve-key", idx: 0, key: "a" }, -// { type: "focus-input", idx: 1 }, -// { type: "handle-code-change" }, -// ]); -// }); - -// test("reject", () => { -// const [state, eff] = stateReducer(currState, { -// type: "handle-key-down", -// key: "@", -// idx: 0, -// val: "", -// }); - -// expect(state).toMatchObject(state); -// expect(eff).toEqual([{ type: "reject-key", idx: 0, key: "@" }]); -// }); -// }); -// }); - -// describe("handle-key-up", () => { -// test("no fallback", () => { -// const [state, eff] = stateReducer(currState, { -// type: "handle-key-up", -// idx: 0, -// val: "", -// }); - -// expect(state).toMatchObject(state); -// expect(eff).toEqual([]); -// }); - -// test("empty prevVal, empty val", () => { -// const [state, eff] = stateReducer( -// { ...currState, fallback: { idx: 0, val: "" } }, -// { type: "handle-key-up", idx: 0, val: "" }, -// ); - -// expect(state).toMatchObject({ fallback: null }); -// expect(eff).toEqual([{ type: "handle-delete", idx: 0 }, { type: "handle-code-change" }]); -// }); - -// test("empty prevVal, not empty allowed val", () => { -// const [state, eff] = stateReducer( -// { ...currState, fallback: { idx: 0, val: "" } }, -// { type: "handle-key-up", idx: 0, val: "a" }, -// ); - -// expect(state).toMatchObject({ fallback: null }); -// expect(eff).toEqual([ -// { type: "set-input-val", idx: 0, val: "a" }, -// { type: "resolve-key", idx: 0, key: "a" }, -// { type: "focus-input", idx: 1 }, -// { type: "handle-code-change" }, -// ]); -// }); - -// test("empty prevVal, not empty denied val", () => { -// const [state, eff] = stateReducer( -// { ...currState, fallback: { idx: 0, val: "" } }, -// { type: "handle-key-up", idx: 0, val: "@" }, -// ); - -// expect(state).toMatchObject({ fallback: null }); -// expect(eff).toEqual([ -// { type: "set-input-val", idx: 0, val: "" }, -// { type: "reject-key", idx: 0, key: "@" }, -// { type: "handle-code-change" }, -// ]); -// }); - -// test("not empty prevVal", () => { -// const [state, eff] = stateReducer( -// { ...currState, fallback: { idx: 0, val: "a" } }, -// { type: "handle-key-up", idx: 0, val: "a" }, -// ); - -// expect(state).toMatchObject({ fallback: null }); -// expect(eff).toEqual([ -// { type: "set-input-val", idx: 0, val: "a" }, -// { type: "resolve-key", idx: 0, key: "a" }, -// { type: "focus-input", idx: 1 }, -// { type: "handle-code-change" }, -// ]); -// }); -// }); - -// describe("handle-paste", () => { -// test("paste smaller text than code length", () => { -// const [state, eff] = stateReducer(currState, { -// type: "handle-paste", -// idx: 0, -// val: "abc", -// }); - -// expect(state).toMatchObject({ ...state, focusIdx: 3 }); -// expect(eff).toEqual([ -// { type: "set-input-val", idx: 0, val: "a" }, -// { type: "resolve-key", idx: 0, key: "a" }, -// { type: "set-input-val", idx: 1, val: "b" }, -// { type: "resolve-key", idx: 1, key: "b" }, -// { type: "set-input-val", idx: 2, val: "c" }, -// { type: "resolve-key", idx: 2, key: "c" }, -// { type: "focus-input", idx: 3 }, -// { type: "handle-code-change" }, -// ]); -// }); - -// test("paste bigger text than code length", () => { -// const [state, eff] = stateReducer(currState, { -// type: "handle-paste", -// idx: 0, -// val: "abcdefgh", -// }); - -// expect(state).toMatchObject({ ...state, focusIdx: 4 }); -// expect(eff).toEqual([ -// { type: "set-input-val", idx: 0, val: "a" }, -// { type: "resolve-key", idx: 0, key: "a" }, -// { type: "set-input-val", idx: 1, val: "b" }, -// { type: "resolve-key", idx: 1, key: "b" }, -// { type: "set-input-val", idx: 2, val: "c" }, -// { type: "resolve-key", idx: 2, key: "c" }, -// { type: "set-input-val", idx: 3, val: "d" }, -// { type: "resolve-key", idx: 3, key: "d" }, -// { type: "set-input-val", idx: 4, val: "e" }, -// { type: "resolve-key", idx: 4, key: "e" }, -// { type: "focus-input", idx: 4 }, -// { type: "handle-code-change" }, -// ]); -// }); - -// test("paste on last input", () => { -// const [state, eff] = stateReducer({ ...currState, focusIdx: 4 }, { type: "handle-paste", idx: 0, val: "abc" }); - -// expect(state).toMatchObject({ ...state, focusIdx: 4 }); -// expect(eff).toEqual([ -// { type: "set-input-val", idx: 4, val: "a" }, -// { type: "resolve-key", idx: 4, key: "a" }, -// { type: "handle-code-change" }, -// ]); -// }); - -// test("paste with denied key", () => { -// const [state, eff] = stateReducer(currState, { -// type: "handle-paste", -// idx: 1, -// val: "ab@", -// }); - -// expect(state).toMatchObject(state); -// expect(eff).toEqual([ -// { type: "set-input-val", idx: 0, val: "" }, -// { type: "reject-key", idx: 1, key: "ab@" }, -// { type: "handle-code-change" }, -// ]); -// }); -// }); - -// test("focus-input", () => { -// const [state, eff] = stateReducer(currState, { type: "focus-input", idx: 2 }); - -// expect(state).toMatchObject({ ...state, focusIdx: 2 }); -// expect(eff).toEqual([{ type: "focus-input", idx: 2 }]); -// }); -// }); +describe("state reducer", () => { + test("noop", () => { + const prevState = { ...defaultState }; + // @ts-expect-error bad action + const state = reducer(prevState, { type: "noop" }); + expect(state).toStrictEqual(prevState); + }); + + describe("update-props", () => { + describe("new length < prev length", () => { + test("cursor < length", () => { + const prevState: State = { ...defaultState }; + const state = reducer(prevState, { type: "update-props", props: { length: 3 } }); + expect(state).toMatchObject({ + length: 3, + values: [], + cursor: 0, + ready: true, + dirty: false, + }); + }); + + test("cursor = length", () => { + const prevState = { ...defaultState, values: ["a", "b"], cursor: 2, ready: true, dirty: true }; + const state = reducer(prevState, { type: "update-props", props: { length: 3 } }); + expect(state).toMatchObject({ + length: 3, + values: ["a", "b"], + cursor: 2, + ready: true, + dirty: true, + }); + }); + + test("cursor > length", () => { + const prevState = { ...defaultState, values: ["a", "b", "c", "d"], cursor: 3, ready: true, dirty: true }; + const state = reducer(prevState, { type: "update-props", props: { length: 3 } }); + expect(state).toMatchObject({ + length: 3, + values: ["a", "b", "c"], + cursor: 2, + ready: true, + dirty: true, + }); + }); + }); + + test("new length = prev length", () => { + const prevState = { ...defaultState, values: ["a", "b"], cursor: 2, ready: true, dirty: true }; + const state = reducer(prevState, { type: "update-props", props: { length: 5 } }); + expect(state).toMatchObject({ + length: 5, + values: ["a", "b"], + cursor: 2, + ready: true, + dirty: true, + }); + }); + + test("new length > prev length", () => { + const prevState = { ...defaultState, values: ["a", "b"], cursor: 2, ready: true, dirty: true }; + const state = reducer(prevState, { type: "update-props", props: { length: 7 } }); + expect(state).toMatchObject({ + length: 7, + values: ["a", "b"], + cursor: 2, + ready: true, + dirty: true, + }); + }); + }); + + test("start-composition", () => { + const prevState = { ...defaultState }; + + const state = reducer(prevState, { type: "start-composition", index: 0 }); + expect(state).toMatchObject({ + composition: true, + dirty: true, + }); + }); + + describe("end-composition", () => { + const prevState = { ...defaultState }; + + test("with empty value at cursor 0", () => { + const state = reducer(prevState, { type: "end-composition", index: 0, value: "" }); + expect(state).toMatchObject({ + values: [], + cursor: 0, + composition: false, + dirty: true, + }); + }); + + test("with empty value at cursor n + 1", () => { + const prevState: State = { ...defaultState, values: ["a", "b"], cursor: 2 }; + const state = reducer(prevState, { type: "end-composition", index: 2, value: "" }); + expect(state).toMatchObject({ + values: ["a", "b"], + cursor: 2, + composition: false, + dirty: true, + }); + }); + + test("with empty value at cursor length", () => { + const prevState: State = { ...defaultState, values: ["a", "b", "c", "d", "e"], cursor: 4 }; + const state = reducer(prevState, { type: "end-composition", index: 4, value: "" }); + expect(state).toMatchObject({ + values: ["a", "b", "c", "d", undefined], + cursor: 4, + composition: false, + dirty: true, + }); + }); + + test("with non-empty value at cursor 0", () => { + const state = reducer(prevState, { type: "end-composition", index: 0, value: "a" }); + expect(state).toMatchObject({ + values: ["a"], + cursor: 1, + composition: false, + dirty: true, + }); + }); + + test("with non-empty value at cursor n + 1", () => { + const prevState: State = { ...defaultState, values: ["a", "b"], cursor: 2 }; + const state = reducer(prevState, { type: "end-composition", index: 2, value: "c" }); + expect(state).toMatchObject({ + values: ["a", "b", "c"], + cursor: 3, + composition: false, + dirty: true, + }); + }); + + test("with non-empty value at cursor length", () => { + const prevState: State = { ...defaultState, values: ["a", "b", "c", "d", "e"], cursor: 4 }; + const state = reducer(prevState, { type: "end-composition", index: 4, value: "f" }); + expect(state).toMatchObject({ + values: ["a", "b", "c", "d", "f"], + cursor: 4, + composition: false, + dirty: true, + }); + }); + }); + + describe("handle-change", () => { + test("composition", () => { + const prevState = { ...defaultState, composition: true }; + const state = reducer(prevState, { type: "handle-change", index: 0, value: "", reset: false }); + expect(state).toStrictEqual(prevState); + }); + + test("reset", () => { + const prevState = { ...defaultState, values: ["a", "b"] }; + const state = reducer(prevState, { type: "handle-change", index: 0, value: "", reset: true }); + expect(state).toMatchObject({ + values: [], + cursor: 0, + }); + }); + + describe("empty value", () => { + describe("with backspace", () => { + test("without values", () => { + const prevState = { ...defaultState, backspace: true }; + const state = reducer(prevState, { type: "handle-change", index: 0, value: "", reset: false }); + expect(state).toMatchObject({ + values: [], + cursor: 0, + }); + }); + + test("with values, cursor < values length", () => { + const prevState = { ...defaultState, values: ["a", "b", "c"], backspace: true }; + const state = reducer(prevState, { type: "handle-change", index: 1, value: "", reset: false }); + expect(state).toMatchObject({ + values: ["a", undefined, "c"], + cursor: 1, + }); + }); + + test("with values, cursor >= values length", () => { + const prevState = { ...defaultState, values: ["a", "b", "c"], backspace: true }; + const state = reducer(prevState, { type: "handle-change", index: 3, value: "", reset: false }); + expect(state).toMatchObject({ + values: ["a", "b", "c"], + cursor: 3, + }); + }); + }); + + describe("without backspace", () => { + test("without values", () => { + const prevState = { ...defaultState }; + const state = reducer(prevState, { type: "handle-change", index: 0, value: "", reset: false }); + expect(state).toMatchObject({ + values: [], + cursor: 0, + }); + }); + + test("with values, cursor < values length", () => { + const prevState = { ...defaultState, values: ["a", "b", "c"] }; + const state = reducer(prevState, { type: "handle-change", index: 1, value: "", reset: false }); + expect(state).toMatchObject({ + values: ["a", undefined, "c"], + cursor: 0, + }); + }); + + test("with values, cursor >= values length", () => { + const prevState = { ...defaultState, values: ["a", "b", "c"] }; + const state = reducer(prevState, { type: "handle-change", index: 3, value: "", reset: false }); + expect(state).toMatchObject({ + values: ["a", "b", "c"], + cursor: 2, + }); + }); + }); + }); + + describe("single value", () => { + test("empty field", () => { + const prevState = { ...defaultState }; + const state = reducer(prevState, { type: "handle-change", index: 0, value: "a", reset: false }); + expect(state).toMatchObject({ + values: ["a"], + cursor: 1, + }); + }); + + test("non-empty field", () => { + // TODO + }); + + test("completed field", () => { + // TODO + }); + }); + }); + + describe("handle-key-down", () => { + // TODO + }); + + // describe("handle-key-down", () => { + // test("unidentified", () => { + // const [state, eff] = stateReducer(currState, { + // type: "handle-key-down", + // key: "Unidentified", + // idx: 0, + // val: "", + // }); + // expect(state).toMatchObject(state); + // expect(eff).toEqual([]); + // }); + // test("dead", () => { + // const [state, eff] = stateReducer(currState, { + // type: "handle-key-down", + // key: "Dead", + // idx: 0, + // val: "", + // }); + // expect(state).toMatchObject(state); + // expect(eff).toEqual([ + // { type: "set-input-val", idx: 0, val: "" }, + // { type: "reject-key", idx: 0, key: "Dead" }, + // { type: "handle-code-change" }, + // ]); + // }); + // describe("left arrow", () => { + // test("from the first input", () => { + // const [state, eff] = stateReducer(currState, { + // type: "handle-key-down", + // key: "ArrowLeft", + // idx: 0, + // val: "", + // }); + // expect(state).toMatchObject({ ...state, focusIdx: 0 }); + // expect(eff).toEqual([{ type: "focus-input", idx: 0 }]); + // }); + // test("from the last input", () => { + // const [state, eff] = stateReducer( + // { ...currState, focusIdx: 4 }, + // { type: "handle-key-down", key: "ArrowLeft", idx: 0, val: "" }, + // ); + // expect(state).toMatchObject({ ...state, focusIdx: 3 }); + // expect(eff).toEqual([{ type: "focus-input", idx: 3 }]); + // }); + // }); + // describe("right arrow", () => { + // test("from the first input", () => { + // const [state, eff] = stateReducer(currState, { + // type: "handle-key-down", + // key: "ArrowRight", + // idx: 0, + // val: "", + // }); + // expect(state).toMatchObject({ ...state, focusIdx: 1 }); + // expect(eff).toEqual([{ type: "focus-input", idx: 1 }]); + // }); + // test("from the last input", () => { + // const [state, eff] = stateReducer( + // { ...currState, focusIdx: 4 }, + // { type: "handle-key-down", key: "ArrowRight", idx: 0, val: "" }, + // ); + // expect(state).toMatchObject({ ...state, focusIdx: 4 }); + // expect(eff).toEqual([{ type: "focus-input", idx: 4 }]); + // }); + // }); + // test("backspace", () => { + // const [state, eff] = stateReducer(currState, { + // type: "handle-key-down", + // key: "Backspace", + // idx: 0, + // val: "", + // }); + // expect(state).toMatchObject({ ...state, focusIdx: 0 }); + // expect(eff).toEqual([{ type: "handle-delete", idx: 0 }, { type: "handle-code-change" }]); + // }); + // test("delete", () => { + // const [state, eff] = stateReducer(currState, { + // type: "handle-key-down", + // key: "Delete", + // idx: 0, + // val: "", + // }); + // expect(state).toMatchObject({ ...state, focusIdx: 0 }); + // expect(eff).toEqual([{ type: "handle-delete", idx: 0 }, { type: "handle-code-change" }]); + // }); + // describe("default", () => { + // test("resolve", () => { + // const [state, eff] = stateReducer(currState, { + // type: "handle-key-down", + // key: "a", + // idx: 0, + // val: "", + // }); + // expect(state).toMatchObject({ ...state, focusIdx: 1 }); + // expect(eff).toEqual([ + // { type: "set-input-val", idx: 0, val: "a" }, + // { type: "resolve-key", idx: 0, key: "a" }, + // { type: "focus-input", idx: 1 }, + // { type: "handle-code-change" }, + // ]); + // }); + // test("reject", () => { + // const [state, eff] = stateReducer(currState, { + // type: "handle-key-down", + // key: "@", + // idx: 0, + // val: "", + // }); + // expect(state).toMatchObject(state); + // expect(eff).toEqual([{ type: "reject-key", idx: 0, key: "@" }]); + // }); + // }); + // }); + // describe("handle-key-up", () => { + // test("no fallback", () => { + // const [state, eff] = stateReducer(currState, { + // type: "handle-key-up", + // idx: 0, + // val: "", + // }); + // expect(state).toMatchObject(state); + // expect(eff).toEqual([]); + // }); + // test("empty prevVal, empty val", () => { + // const [state, eff] = stateReducer( + // { ...currState, fallback: { idx: 0, val: "" } }, + // { type: "handle-key-up", idx: 0, val: "" }, + // ); + // expect(state).toMatchObject({ fallback: null }); + // expect(eff).toEqual([{ type: "handle-delete", idx: 0 }, { type: "handle-code-change" }]); + // }); + // test("empty prevVal, not empty allowed val", () => { + // const [state, eff] = stateReducer( + // { ...currState, fallback: { idx: 0, val: "" } }, + // { type: "handle-key-up", idx: 0, val: "a" }, + // ); + // expect(state).toMatchObject({ fallback: null }); + // expect(eff).toEqual([ + // { type: "set-input-val", idx: 0, val: "a" }, + // { type: "resolve-key", idx: 0, key: "a" }, + // { type: "focus-input", idx: 1 }, + // { type: "handle-code-change" }, + // ]); + // }); + // test("empty prevVal, not empty denied val", () => { + // const [state, eff] = stateReducer( + // { ...currState, fallback: { idx: 0, val: "" } }, + // { type: "handle-key-up", idx: 0, val: "@" }, + // ); + // expect(state).toMatchObject({ fallback: null }); + // expect(eff).toEqual([ + // { type: "set-input-val", idx: 0, val: "" }, + // { type: "reject-key", idx: 0, key: "@" }, + // { type: "handle-code-change" }, + // ]); + // }); + // test("not empty prevVal", () => { + // const [state, eff] = stateReducer( + // { ...currState, fallback: { idx: 0, val: "a" } }, + // { type: "handle-key-up", idx: 0, val: "a" }, + // ); + // expect(state).toMatchObject({ fallback: null }); + // expect(eff).toEqual([ + // { type: "set-input-val", idx: 0, val: "a" }, + // { type: "resolve-key", idx: 0, key: "a" }, + // { type: "focus-input", idx: 1 }, + // { type: "handle-code-change" }, + // ]); + // }); + // }); + // describe("handle-paste", () => { + // test("paste smaller text than code length", () => { + // const [state, eff] = stateReducer(currState, { + // type: "handle-paste", + // idx: 0, + // val: "abc", + // }); + // expect(state).toMatchObject({ ...state, focusIdx: 3 }); + // expect(eff).toEqual([ + // { type: "set-input-val", idx: 0, val: "a" }, + // { type: "resolve-key", idx: 0, key: "a" }, + // { type: "set-input-val", idx: 1, val: "b" }, + // { type: "resolve-key", idx: 1, key: "b" }, + // { type: "set-input-val", idx: 2, val: "c" }, + // { type: "resolve-key", idx: 2, key: "c" }, + // { type: "focus-input", idx: 3 }, + // { type: "handle-code-change" }, + // ]); + // }); + // test("paste bigger text than code length", () => { + // const [state, eff] = stateReducer(currState, { + // type: "handle-paste", + // idx: 0, + // val: "abcdefgh", + // }); + // expect(state).toMatchObject({ ...state, focusIdx: 4 }); + // expect(eff).toEqual([ + // { type: "set-input-val", idx: 0, val: "a" }, + // { type: "resolve-key", idx: 0, key: "a" }, + // { type: "set-input-val", idx: 1, val: "b" }, + // { type: "resolve-key", idx: 1, key: "b" }, + // { type: "set-input-val", idx: 2, val: "c" }, + // { type: "resolve-key", idx: 2, key: "c" }, + // { type: "set-input-val", idx: 3, val: "d" }, + // { type: "resolve-key", idx: 3, key: "d" }, + // { type: "set-input-val", idx: 4, val: "e" }, + // { type: "resolve-key", idx: 4, key: "e" }, + // { type: "focus-input", idx: 4 }, + // { type: "handle-code-change" }, + // ]); + // }); + // test("paste on last input", () => { + // const [state, eff] = stateReducer({ ...currState, focusIdx: 4 }, { type: "handle-paste", idx: 0, val: "abc" }); + // expect(state).toMatchObject({ ...state, focusIdx: 4 }); + // expect(eff).toEqual([ + // { type: "set-input-val", idx: 4, val: "a" }, + // { type: "resolve-key", idx: 4, key: "a" }, + // { type: "handle-code-change" }, + // ]); + // }); + // test("paste with denied key", () => { + // const [state, eff] = stateReducer(currState, { + // type: "handle-paste", + // idx: 1, + // val: "ab@", + // }); + // expect(state).toMatchObject(state); + // expect(eff).toEqual([ + // { type: "set-input-val", idx: 0, val: "" }, + // { type: "reject-key", idx: 1, key: "ab@" }, + // { type: "handle-code-change" }, + // ]); + // }); + // }); + // test("focus-input", () => { + // const [state, eff] = stateReducer(currState, { type: "focus-input", idx: 2 }); + // expect(state).toMatchObject({ ...state, focusIdx: 2 }); + // expect(eff).toEqual([{ type: "focus-input", idx: 2 }]); + // }); +}); // describe("effect reducer", () => { // const { defaultProps, useEffectReducer } = pinField; diff --git a/src/pin-field/pin-field-v2.tsx b/src/pin-field/pin-field-v2.tsx index bf4636a..d400c55 100644 --- a/src/pin-field/pin-field-v2.tsx +++ b/src/pin-field/pin-field-v2.tsx @@ -85,22 +85,34 @@ export type Action = | { type: "start-composition"; index: number } | { type: "end-composition"; index: number; value: string }; -export function reducer(state: State, action: Action): State { +export function reducer(prevState: State, action: Action): State { switch (action.type) { case "update-props": { - state = { ...state, ...action.props, ready: true }; - // cannot use Array.splice as it does not keep empty array length - state.values = state.values.slice(state.cursor, state.length); + // merge previous state with action's props + const state = { ...prevState, ...action.props }; + + // adjust cursor in case the new length exceed the previous one state.cursor = Math.min(state.cursor, state.length - 1); + // slice values according to the new length + // + // NOTE: use slice because splice does not keep empty items and + // therefore messes up with values length + state.values = state.values.slice(0, state.cursor + 1); + + // state is now ready + state.ready = true; + return state; } case "start-composition": { - return { ...state, dirty: true, composition: true }; + return { ...prevState, dirty: true, composition: true }; } case "end-composition": { + const state: State = { ...prevState }; + if (action.value) { state.values[action.index] = action.value; } else { @@ -110,14 +122,19 @@ export function reducer(state: State, action: Action): State { const dir = state.values[action.index] ? 1 : 0; state.cursor = Math.min(action.index + dir, state.length - 1); - return { ...state, dirty: true, composition: false }; + state.composition = false; + state.dirty = true; + + return state; } case "handle-change": { - if (state.composition) { + if (prevState.composition) { break; } + const state: State = { ...prevState }; + if (action.reset) { state.values = Array(state.length); } @@ -133,7 +150,10 @@ export function reducer(state: State, action: Action): State { state.cursor = Math.max(0, action.index - dir); } - return { ...state, dirty: true, backspace: false }; + state.backspace = false; + state.dirty = true; + + return state; } case "handle-key-down": { @@ -159,7 +179,7 @@ export function reducer(state: State, action: Action): State { // state will be automatically updated by the handle-change // action, when the deleted value will trigger the `onchange` // event. - if (state.values[action.index]) { + if (prevState.values[action.index]) { break; } @@ -169,6 +189,8 @@ export function reducer(state: State, action: Action): State { // new cursor and perform the changes via the triggered // `onchange` event. else { + const state: State = { ...prevState }; + state.cursor = Math.max(0, action.index - 1); // let know the handle-change action that we already moved @@ -176,12 +198,14 @@ export function reducer(state: State, action: Action): State { // anymore state.backspace = true; - return { ...state, dirty: true }; + state.dirty = true; + + return state; } } } - return state; + return prevState; } export type Handler = { @@ -198,7 +222,7 @@ export function usePinField(): Handler { const value = useMemo(() => { let value = ""; - for (let index = 0; index < state.values.length; index++) { + for (let index = 0; index < state.length; index++) { value += index in state.values ? state.values[index] : ""; } return value; @@ -306,7 +330,7 @@ export const PinFieldV2: FC = forwardRef( let completed = state.values.length == state.length; let value = ""; - for (let index = 0; index < state.values.length; index++) { + for (let index = 0; index < state.length; index++) { const char = index in state.values ? state.values[index] : ""; refs.current[index].value = char; innerFocus = innerFocus || hasFocus(refs.current[index]); From 85f9aaf42e9b72bca02df0c967b91fa95930e954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Mon, 6 Jan 2025 16:28:12 +0100 Subject: [PATCH 07/13] add missing tests --- .prettierrc.json | 8 + babel.config.cjs | 7 - babel.config.json | 7 + cypress.config.ts | 3 +- jest.config.json | 5 + jest.config.mjs | 9 - package.json | 1 - prettier.config.js | 12 - shell.nix | 25 +- ...{pin-field.e2e.ts => pin-field-v2.e2e.tsx} | 38 +- src/pin-field/pin-field-v2.spec.tsx | 178 ++ src/pin-field/pin-field-v2.stories.tsx | 1 - src/pin-field/pin-field-v2.test.ts | 953 +++++---- src/pin-field/pin-field-v2.tsx | 87 +- yarn.lock | 1755 ++++------------- 15 files changed, 1151 insertions(+), 1938 deletions(-) create mode 100644 .prettierrc.json delete mode 100644 babel.config.cjs create mode 100644 babel.config.json create mode 100644 jest.config.json delete mode 100644 jest.config.mjs delete mode 100644 prettier.config.js rename src/pin-field/{pin-field.e2e.ts => pin-field-v2.e2e.tsx} (55%) create mode 100644 src/pin-field/pin-field-v2.spec.tsx diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..280dd65 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "arrowParens": "avoid", + "bracketSpacing": true, + "printWidth": 100, + "semi": true, + "singleQuote": false, + "trailingComma": "all" +} diff --git a/babel.config.cjs b/babel.config.cjs deleted file mode 100644 index 7aa143c..0000000 --- a/babel.config.cjs +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @see https://babeljs.io/docs/options - * @type {import("@babel/core").TransformOptions} - */ -module.exports = { - presets: ["@babel/preset-env", "@babel/preset-typescript", "@babel/preset-react"], -}; diff --git a/babel.config.json b/babel.config.json new file mode 100644 index 0000000..604332b --- /dev/null +++ b/babel.config.json @@ -0,0 +1,7 @@ +{ + "presets": [ + ["@babel/preset-env", { "targets": { "node": "current" } }], + "@babel/preset-typescript", + ["@babel/preset-react", { "runtime": "automatic" }] + ] +} diff --git a/cypress.config.ts b/cypress.config.ts index cbe2123..d6c8c11 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -1,6 +1,7 @@ import "cypress"; export default { + experimentalWebKitSupport: true, fixturesFolder: false, video: false, e2e: { @@ -13,7 +14,7 @@ export default { }); }, baseUrl: "http://localhost:3000", - specPattern: "src/**/*.e2e.ts", + specPattern: "src/**/*.e2e.tsx", supportFile: false, }, } satisfies Cypress.ConfigOptions; diff --git a/jest.config.json b/jest.config.json new file mode 100644 index 0000000..7ea1e39 --- /dev/null +++ b/jest.config.json @@ -0,0 +1,5 @@ +{ + "collectCoverage": true, + "testEnvironment": "jest-environment-jsdom", + "testRegex": ".(test|spec).tsx?$" +} diff --git a/jest.config.mjs b/jest.config.mjs deleted file mode 100644 index b26108a..0000000 --- a/jest.config.mjs +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @see https://jestjs.io/docs/configuration - * @type {import("jest").Config} - */ -export default { - collectCoverage: true, - testEnvironment: "jest-environment-jsdom", - testRegex: ".(test|spec).tsx?$", -}; diff --git a/package.json b/package.json index dd20435..aea1d35 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,6 @@ "sass": "^1.83.0", "size-limit": "^11.1.6", "storybook": "^8.4.7", - "ts-node": "^10.9.2", "typescript": "^5.7.2", "vite": "^6.0.7", "vite-plugin-dts": "^4.4.0", diff --git a/prettier.config.js b/prettier.config.js deleted file mode 100644 index 7aa4964..0000000 --- a/prettier.config.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @see https://prettier.io/docs/en/configuration.html - * @type {import("prettier").Config} - */ -export default { - arrowParens: "avoid", - bracketSpacing: true, - printWidth: 120, - semi: true, - singleQuote: false, - trailingComma: "all", -}; diff --git a/shell.nix b/shell.nix index bdb8f68..9f52fe5 100644 --- a/shell.nix +++ b/shell.nix @@ -1,26 +1,25 @@ -{ nixpkgs ? -, system ? builtins.currentSystem -, pkgs ? import nixpkgs { inherit system; } -, extraBuildInputs ? "" -}: +{ nixpkgs ? , system ? builtins.currentSystem +, pkgs ? import nixpkgs { inherit system; }, extraBuildInputs ? "" }: let - inherit (pkgs) cypress lib mkShell nodejs; - inherit (lib) attrVals getExe' optionals splitString; + inherit (pkgs) cypress lib mkShell nodejs playwright playwright-driver; + inherit (lib) attrVals getExe optionals splitString; yarn = pkgs.yarn.override { inherit nodejs; }; - extraBuildInputs' = optionals - (extraBuildInputs != "") + extraBuildInputs' = optionals (extraBuildInputs != "") (attrVals (splitString "," extraBuildInputs) pkgs); -in -mkShell { - buildInputs = [ cypress nodejs yarn ] ++ extraBuildInputs'; +in mkShell { + buildInputs = [ nodejs yarn cypress playwright playwright-driver.browsers ] + ++ extraBuildInputs'; shellHook = '' # configure cypress - export CYPRESS_RUN_BINARY="${getExe' cypress "Cypress"}" + export CYPRESS_RUN_BINARY="${getExe cypress}" # add node_modules/.bin to path export PATH="$PWD/node_modules/.bin/:$PATH" + + export PLAYWRIGHT_BROWSERS_PATH=${playwright-driver.browsers} + export PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS=true ''; } diff --git a/src/pin-field/pin-field.e2e.ts b/src/pin-field/pin-field-v2.e2e.tsx similarity index 55% rename from src/pin-field/pin-field.e2e.ts rename to src/pin-field/pin-field-v2.e2e.tsx index f5a42a3..2fade56 100644 --- a/src/pin-field/pin-field.e2e.ts +++ b/src/pin-field/pin-field-v2.e2e.tsx @@ -5,43 +5,35 @@ describe("PIN Field", () => { beforeEach(() => { cy.visit("/iframe.html", { qs: { - id: "pinfield--default", + id: "pinfieldv2--default", viewMode: "story", }, }); }); - it("should override chars", () => { - cy.get(nthInput(1)).type("a{leftArrow}b{leftArrow}c", { delay: 200 }).should("have.value", "c"); - }); - - it("should focus next input on allowed entry", () => { - cy.get(nthInput(1)).type("abcde"); - + it("should adjust focus according to state's cursor", () => { + cy.get(nthInput(1)).type("abc"); cy.get(nthInput(1)).should("not.be.focused").should("have.value", "a"); cy.get(nthInput(2)).should("not.be.focused").should("have.value", "b"); cy.get(nthInput(3)).should("not.be.focused").should("have.value", "c"); - cy.get(nthInput(4)).should("not.be.focused").should("have.value", "d"); - cy.get(nthInput(5)).should("be.focused").should("have.value", "e"); - }); - - it("should not focus next input on denied entry", () => { - cy.get(nthInput(1)).type("ab-_*c=d{leftArrow}$|"); + cy.get(nthInput(4)).should("be.focused").should("have.value", ""); + cy.get("body").type("def"); cy.get(nthInput(1)).should("not.be.focused").should("have.value", "a"); cy.get(nthInput(2)).should("not.be.focused").should("have.value", "b"); cy.get(nthInput(3)).should("not.be.focused").should("have.value", "c"); - cy.get(nthInput(4)).should("be.focused").should("have.value", "d"); - cy.get(nthInput(5)).should("not.be.focused").should("have.value", ""); + cy.get(nthInput(4)).should("not.be.focused").should("have.value", "d"); + cy.get(nthInput(5)).should("be.focused").should("have.value", "f"); }); - it("should remove chars on backspace or delete", () => { - cy.get(nthInput(1)).type("abc{backspace}{del}defg{backspace}"); - + it.only("should remove values on backspace or delete", () => { + cy.get(nthInput(1)).focus(); + cy.focused().type("abc{backspace}"); + // A second backspace is needed due to event.isTrusted = false. + // From a user interaction, only 1 backspace is needed + cy.focused().type("{backspace}"); cy.get(nthInput(1)).should("not.be.focused").should("have.value", "a"); - cy.get(nthInput(2)).should("not.be.focused").should("have.value", "d"); - cy.get(nthInput(3)).should("not.be.focused").should("have.value", "e"); - cy.get(nthInput(4)).should("not.be.focused").should("have.value", "f"); - cy.get(nthInput(5)).should("be.focused").should("have.value", ""); + cy.get(nthInput(2)).should("not.be.focused").should("have.value", "b"); + cy.get(nthInput(3)).should("be.focused").should("have.value", ""); }); }); diff --git a/src/pin-field/pin-field-v2.spec.tsx b/src/pin-field/pin-field-v2.spec.tsx new file mode 100644 index 0000000..a90dc42 --- /dev/null +++ b/src/pin-field/pin-field-v2.spec.tsx @@ -0,0 +1,178 @@ +import "@testing-library/jest-dom"; + +import { RefObject } from "react"; +import { createEvent, fireEvent, render, screen } from "@testing-library/react"; + +import PinField from "./pin-field-v2"; + +test("default", async () => { + render(); + + const inputs = await screen.findAllByRole("textbox"); + expect(inputs).toHaveLength(5); + + inputs.forEach(input => { + expect(input.getAttribute("type")).toBe("text"); + expect(input.getAttribute("inputmode")).toBe("text"); + expect(input.getAttribute("autocapitalize")).toBe("off"); + expect(input.getAttribute("autocorrect")).toBe("off"); + expect(input.getAttribute("autocomplete")).toBe("off"); + }); +}); + +test("forward ref as object", () => { + const ref: RefObject = { current: [] }; + render(); + + expect(Array.isArray(ref.current)).toBe(true); + expect(ref.current).toHaveLength(5); +}); + +test("forward ref as func", () => { + const ref: RefObject = { current: [] }; + render( + { + if (el) { + ref.current = el; + } + }} + />, + ); + + expect(Array.isArray(ref.current)).toBe(true); + expect(ref.current).toHaveLength(5); +}); + +test("className", async () => { + render(); + const inputs = await screen.findAllByRole("textbox"); + + inputs.forEach(input => { + expect(input.className).toBe("custom-class-name"); + }); +}); + +test("style", async () => { + render(); + const inputs = await screen.findAllByRole("textbox"); + + inputs.forEach(input => { + expect(input.style.position).toBe("absolute"); + }); +}); + +test("autoFocus", async () => { + render(); + const inputs = await screen.findAllByRole("textbox"); + + inputs.forEach((input, index) => { + if (index === 0) { + expect(input).toHaveFocus(); + } else { + expect(input).not.toHaveFocus(); + } + }); +}); + +test("autoFocus rtl", async () => { + render(); + const inputs = await screen.findAllByRole("textbox"); + + inputs.forEach((input, index) => { + if (index === 4) { + expect(input).toHaveFocus(); + } else { + expect(input).not.toHaveFocus(); + } + }); +}); + +describe("events", () => { + test("change single input", async () => { + const handleChangeMock = jest.fn(); + const handleCompleteMock = jest.fn(); + render(); + + const inputs = (await screen.findAllByRole("textbox")) as HTMLInputElement[]; + + fireEvent.change(inputs[0], { target: { value: "a" } }); + expect(handleChangeMock).toHaveBeenCalledWith("a"); + expect(inputs[0].value).toBe("a"); + + fireEvent.change(inputs[1], { target: { value: "b" } }); + expect(handleChangeMock).toHaveBeenCalledWith("ab"); + expect(inputs[0].value).toBe("a"); + expect(inputs[1].value).toBe("b"); + + fireEvent.change(inputs[2], { target: { value: "c" } }); + expect(handleChangeMock).toHaveBeenCalledWith("abc"); + expect(inputs[0].value).toBe("a"); + expect(inputs[1].value).toBe("b"); + expect(inputs[2].value).toBe("c"); + + fireEvent.change(inputs[3], { target: { value: "d" } }); + expect(handleChangeMock).toHaveBeenCalledWith("abcd"); + expect(handleCompleteMock).toHaveBeenCalledWith("abcd"); + expect(inputs[0].value).toBe("a"); + expect(inputs[1].value).toBe("b"); + expect(inputs[2].value).toBe("c"); + expect(inputs[3].value).toBe("d"); + }); + + test("change multi input", async () => { + const handleChangeMock = jest.fn(); + const handleCompleteMock = jest.fn(); + render(); + + const inputs = (await screen.findAllByRole("textbox")) as HTMLInputElement[]; + + fireEvent.change(inputs[0], { target: { value: "abc" } }); + expect(handleChangeMock).toHaveBeenLastCalledWith("abc"); + expect(inputs[0].value).toBe("a"); + expect(inputs[1].value).toBe("b"); + expect(inputs[2].value).toBe("c"); + + fireEvent.change(inputs[1], { target: { value: "def" } }); + expect(handleChangeMock).toHaveBeenLastCalledWith("adef"); + expect(handleCompleteMock).toHaveBeenCalledWith("adef"); + expect(inputs[0].value).toBe("a"); + expect(inputs[1].value).toBe("d"); + expect(inputs[2].value).toBe("e"); + expect(inputs[3].value).toBe("f"); + }); +}); + +describe("a11y", () => { + test("default aria-label", () => { + render(); + + expect(screen.getByRole("textbox", { name: "PIN field 1 of 3" })).toBeVisible(); + expect(screen.getByRole("textbox", { name: "PIN field 2 of 3" })).toBeVisible(); + expect(screen.getByRole("textbox", { name: "PIN field 3 of 3" })).toBeVisible(); + }); + + test("aria-required", () => { + render( `${i}`} required />); + + expect(screen.getByRole("textbox", { name: "1" })).toHaveAttribute("aria-required", "true"); + expect(screen.getByRole("textbox", { name: "2" })).toHaveAttribute("aria-required", "true"); + expect(screen.getByRole("textbox", { name: "3" })).toHaveAttribute("aria-required", "true"); + }); + + test("aria-disabled", () => { + render( `${i}`} disabled />); + + expect(screen.getByRole("textbox", { name: "1" })).toHaveAttribute("aria-disabled", "true"); + expect(screen.getByRole("textbox", { name: "2" })).toHaveAttribute("aria-disabled", "true"); + expect(screen.getByRole("textbox", { name: "3" })).toHaveAttribute("aria-disabled", "true"); + }); + + test("aria-readonly", () => { + render( `${i}`} readOnly />); + + expect(screen.getByRole("textbox", { name: "1" })).toHaveAttribute("aria-readonly", "true"); + expect(screen.getByRole("textbox", { name: "2" })).toHaveAttribute("aria-readonly", "true"); + expect(screen.getByRole("textbox", { name: "3" })).toHaveAttribute("aria-readonly", "true"); + }); +}); diff --git a/src/pin-field/pin-field-v2.stories.tsx b/src/pin-field/pin-field-v2.stories.tsx index cc8c7d7..ef45694 100644 --- a/src/pin-field/pin-field-v2.stories.tsx +++ b/src/pin-field/pin-field-v2.stories.tsx @@ -1,7 +1,6 @@ import { FC, StrictMode as ReactStrictMode } from "react"; import type { Meta, StoryObj } from "@storybook/react"; import { fn } from "@storybook/test"; - import { PinFieldV2, defaultProps, Props, usePinField, InnerProps } from "./pin-field-v2"; import "./pin-field-v2.stories.scss"; diff --git a/src/pin-field/pin-field-v2.test.ts b/src/pin-field/pin-field-v2.test.ts index b131f4a..ea4a38a 100644 --- a/src/pin-field/pin-field-v2.test.ts +++ b/src/pin-field/pin-field-v2.test.ts @@ -1,28 +1,13 @@ -import React from "react"; - -import { noop } from "../utils"; -import { reducer, BACKSPACE, DELETE, defaultProps, defaultState, State } from "./pin-field-v2"; - -jest.mock("react", () => ({ - useCallback: (f: any) => f, - forwardRef: (f: any) => f, -})); - -function mockInput(value: string) { - const setValMock = jest.fn(); - const ref = { - focus: jest.fn(), - setCustomValidity: jest.fn(), - set value(val: string) { - setValMock(val); - }, - get value() { - return value; - }, - }; - - return { ref, setValMock }; -} +import { + reducer, + BACKSPACE, + DELETE, + defaultProps, + defaultState, + State, + HandleKeyDownAction, + defaultNativeProps, +} from "./pin-field-v2"; test("constants", () => { expect(BACKSPACE).toEqual(8); @@ -30,43 +15,60 @@ test("constants", () => { }); test("default props", () => { - expect(defaultProps).toHaveProperty("length", 5); - expect(defaultProps).toHaveProperty("format", expect.any(Function)); + expect(defaultProps).toEqual({ + length: 5, + format: expect.any(Function), + formatAriaLabel: expect.any(Function), + onChange: expect.any(Function), + onComplete: expect.any(Function), + }); + expect(defaultProps.format("a")).toStrictEqual("a"); - expect(defaultProps).toHaveProperty("formatAriaLabel", expect.any(Function)); expect(defaultProps.formatAriaLabel(1, 2)).toStrictEqual("PIN field 1 of 2"); - expect(defaultProps).toHaveProperty("onChange"); expect(defaultProps.onChange("a")).toStrictEqual(undefined); - expect(defaultProps).toHaveProperty("onComplete"); expect(defaultProps.onComplete("a")).toStrictEqual(undefined); }); +test("default native props", () => { + expect(defaultNativeProps).toEqual({ + type: "text", + inputMode: "text", + autoCapitalize: "off", + autoCorrect: "off", + autoComplete: "off", + }); +}); + test("default state", () => { - expect(defaultState).toHaveProperty("length", 5); - expect(defaultState).toHaveProperty("format", expect.any(Function)); + expect(defaultState).toEqual({ + length: 5, + format: expect.any(Function), + dir: "ltr", + cursor: 0, + values: Array(5), + backspace: false, + composition: false, + ready: false, + dirty: false, + }); + expect(defaultState.format("a")).toStrictEqual("a"); - expect(defaultState).toHaveProperty("dir", "ltr"); - expect(defaultState).toHaveProperty("cursor", 0); - expect(defaultState).toHaveProperty("values", Array(5)); - expect(defaultState).toHaveProperty("backspace", false); - expect(defaultState).toHaveProperty("composition", false); - expect(defaultState).toHaveProperty("ready", false); - expect(defaultState).toHaveProperty("dirty", false); }); describe("state reducer", () => { test("noop", () => { - const prevState = { ...defaultState }; - // @ts-expect-error bad action - const state = reducer(prevState, { type: "noop" }); - expect(state).toStrictEqual(prevState); + const state = reducer(defaultState, { type: "noop" }); + expect(state).toStrictEqual(defaultState); }); describe("update-props", () => { describe("new length < prev length", () => { test("cursor < length", () => { const prevState: State = { ...defaultState }; - const state = reducer(prevState, { type: "update-props", props: { length: 3 } }); + const state = reducer(prevState, { + type: "update-props", + props: { length: 3 }, + }); expect(state).toMatchObject({ length: 3, values: [], @@ -77,8 +79,17 @@ describe("state reducer", () => { }); test("cursor = length", () => { - const prevState = { ...defaultState, values: ["a", "b"], cursor: 2, ready: true, dirty: true }; - const state = reducer(prevState, { type: "update-props", props: { length: 3 } }); + const prevState = { + ...defaultState, + values: ["a", "b"], + cursor: 2, + ready: true, + dirty: true, + }; + const state = reducer(prevState, { + type: "update-props", + props: { length: 3 }, + }); expect(state).toMatchObject({ length: 3, values: ["a", "b"], @@ -89,8 +100,17 @@ describe("state reducer", () => { }); test("cursor > length", () => { - const prevState = { ...defaultState, values: ["a", "b", "c", "d"], cursor: 3, ready: true, dirty: true }; - const state = reducer(prevState, { type: "update-props", props: { length: 3 } }); + const prevState = { + ...defaultState, + values: ["a", "b", "c", "d"], + cursor: 3, + ready: true, + dirty: true, + }; + const state = reducer(prevState, { + type: "update-props", + props: { length: 3 }, + }); expect(state).toMatchObject({ length: 3, values: ["a", "b", "c"], @@ -102,8 +122,17 @@ describe("state reducer", () => { }); test("new length = prev length", () => { - const prevState = { ...defaultState, values: ["a", "b"], cursor: 2, ready: true, dirty: true }; - const state = reducer(prevState, { type: "update-props", props: { length: 5 } }); + const prevState = { + ...defaultState, + values: ["a", "b"], + cursor: 2, + ready: true, + dirty: true, + }; + const state = reducer(prevState, { + type: "update-props", + props: { length: 5 }, + }); expect(state).toMatchObject({ length: 5, values: ["a", "b"], @@ -114,8 +143,17 @@ describe("state reducer", () => { }); test("new length > prev length", () => { - const prevState = { ...defaultState, values: ["a", "b"], cursor: 2, ready: true, dirty: true }; - const state = reducer(prevState, { type: "update-props", props: { length: 7 } }); + const prevState = { + ...defaultState, + values: ["a", "b"], + cursor: 2, + ready: true, + dirty: true, + }; + const state = reducer(prevState, { + type: "update-props", + props: { length: 7 }, + }); expect(state).toMatchObject({ length: 7, values: ["a", "b"], @@ -140,7 +178,11 @@ describe("state reducer", () => { const prevState = { ...defaultState }; test("with empty value at cursor 0", () => { - const state = reducer(prevState, { type: "end-composition", index: 0, value: "" }); + const state = reducer(prevState, { + type: "end-composition", + index: 0, + value: "", + }); expect(state).toMatchObject({ values: [], cursor: 0, @@ -150,8 +192,16 @@ describe("state reducer", () => { }); test("with empty value at cursor n + 1", () => { - const prevState: State = { ...defaultState, values: ["a", "b"], cursor: 2 }; - const state = reducer(prevState, { type: "end-composition", index: 2, value: "" }); + const prevState: State = { + ...defaultState, + values: ["a", "b"], + cursor: 2, + }; + const state = reducer(prevState, { + type: "end-composition", + index: 2, + value: "", + }); expect(state).toMatchObject({ values: ["a", "b"], cursor: 2, @@ -161,8 +211,16 @@ describe("state reducer", () => { }); test("with empty value at cursor length", () => { - const prevState: State = { ...defaultState, values: ["a", "b", "c", "d", "e"], cursor: 4 }; - const state = reducer(prevState, { type: "end-composition", index: 4, value: "" }); + const prevState: State = { + ...defaultState, + values: ["a", "b", "c", "d", "e"], + cursor: 4, + }; + const state = reducer(prevState, { + type: "end-composition", + index: 4, + value: "", + }); expect(state).toMatchObject({ values: ["a", "b", "c", "d", undefined], cursor: 4, @@ -172,7 +230,11 @@ describe("state reducer", () => { }); test("with non-empty value at cursor 0", () => { - const state = reducer(prevState, { type: "end-composition", index: 0, value: "a" }); + const state = reducer(prevState, { + type: "end-composition", + index: 0, + value: "a", + }); expect(state).toMatchObject({ values: ["a"], cursor: 1, @@ -182,8 +244,16 @@ describe("state reducer", () => { }); test("with non-empty value at cursor n + 1", () => { - const prevState: State = { ...defaultState, values: ["a", "b"], cursor: 2 }; - const state = reducer(prevState, { type: "end-composition", index: 2, value: "c" }); + const prevState: State = { + ...defaultState, + values: ["a", "b"], + cursor: 2, + }; + const state = reducer(prevState, { + type: "end-composition", + index: 2, + value: "c", + }); expect(state).toMatchObject({ values: ["a", "b", "c"], cursor: 3, @@ -193,8 +263,16 @@ describe("state reducer", () => { }); test("with non-empty value at cursor length", () => { - const prevState: State = { ...defaultState, values: ["a", "b", "c", "d", "e"], cursor: 4 }; - const state = reducer(prevState, { type: "end-composition", index: 4, value: "f" }); + const prevState: State = { + ...defaultState, + values: ["a", "b", "c", "d", "e"], + cursor: 4, + }; + const state = reducer(prevState, { + type: "end-composition", + index: 4, + value: "f", + }); expect(state).toMatchObject({ values: ["a", "b", "c", "d", "f"], cursor: 4, @@ -207,13 +285,23 @@ describe("state reducer", () => { describe("handle-change", () => { test("composition", () => { const prevState = { ...defaultState, composition: true }; - const state = reducer(prevState, { type: "handle-change", index: 0, value: "", reset: false }); + const state = reducer(prevState, { + type: "handle-change", + index: 0, + value: "", + reset: false, + }); expect(state).toStrictEqual(prevState); }); test("reset", () => { const prevState = { ...defaultState, values: ["a", "b"] }; - const state = reducer(prevState, { type: "handle-change", index: 0, value: "", reset: true }); + const state = reducer(prevState, { + type: "handle-change", + index: 0, + value: "", + reset: true, + }); expect(state).toMatchObject({ values: [], cursor: 0, @@ -224,7 +312,12 @@ describe("state reducer", () => { describe("with backspace", () => { test("without values", () => { const prevState = { ...defaultState, backspace: true }; - const state = reducer(prevState, { type: "handle-change", index: 0, value: "", reset: false }); + const state = reducer(prevState, { + type: "handle-change", + index: 0, + value: "", + reset: false, + }); expect(state).toMatchObject({ values: [], cursor: 0, @@ -232,8 +325,17 @@ describe("state reducer", () => { }); test("with values, cursor < values length", () => { - const prevState = { ...defaultState, values: ["a", "b", "c"], backspace: true }; - const state = reducer(prevState, { type: "handle-change", index: 1, value: "", reset: false }); + const prevState = { + ...defaultState, + values: ["a", "b", "c"], + backspace: true, + }; + const state = reducer(prevState, { + type: "handle-change", + index: 1, + value: "", + reset: false, + }); expect(state).toMatchObject({ values: ["a", undefined, "c"], cursor: 1, @@ -241,8 +343,17 @@ describe("state reducer", () => { }); test("with values, cursor >= values length", () => { - const prevState = { ...defaultState, values: ["a", "b", "c"], backspace: true }; - const state = reducer(prevState, { type: "handle-change", index: 3, value: "", reset: false }); + const prevState = { + ...defaultState, + values: ["a", "b", "c"], + backspace: true, + }; + const state = reducer(prevState, { + type: "handle-change", + index: 3, + value: "", + reset: false, + }); expect(state).toMatchObject({ values: ["a", "b", "c"], cursor: 3, @@ -253,7 +364,12 @@ describe("state reducer", () => { describe("without backspace", () => { test("without values", () => { const prevState = { ...defaultState }; - const state = reducer(prevState, { type: "handle-change", index: 0, value: "", reset: false }); + const state = reducer(prevState, { + type: "handle-change", + index: 0, + value: "", + reset: false, + }); expect(state).toMatchObject({ values: [], cursor: 0, @@ -262,7 +378,12 @@ describe("state reducer", () => { test("with values, cursor < values length", () => { const prevState = { ...defaultState, values: ["a", "b", "c"] }; - const state = reducer(prevState, { type: "handle-change", index: 1, value: "", reset: false }); + const state = reducer(prevState, { + type: "handle-change", + index: 1, + value: "", + reset: false, + }); expect(state).toMatchObject({ values: ["a", undefined, "c"], cursor: 0, @@ -271,7 +392,12 @@ describe("state reducer", () => { test("with values, cursor >= values length", () => { const prevState = { ...defaultState, values: ["a", "b", "c"] }; - const state = reducer(prevState, { type: "handle-change", index: 3, value: "", reset: false }); + const state = reducer(prevState, { + type: "handle-change", + index: 3, + value: "", + reset: false, + }); expect(state).toMatchObject({ values: ["a", "b", "c"], cursor: 2, @@ -283,437 +409,282 @@ describe("state reducer", () => { describe("single value", () => { test("empty field", () => { const prevState = { ...defaultState }; - const state = reducer(prevState, { type: "handle-change", index: 0, value: "a", reset: false }); + const state = reducer(prevState, { + type: "handle-change", + index: 0, + value: "a", + reset: false, + }); expect(state).toMatchObject({ values: ["a"], cursor: 1, }); }); - test("non-empty field", () => { - // TODO + test("in progress field, head insertion", () => { + const state = reducer( + { ...defaultState, values: ["a", "b", "c"] }, + { type: "handle-change", index: 1, value: "d", reset: false }, + ); + + expect(state).toMatchObject({ + values: ["a", "d", "c"], + cursor: 2, + }); + }); + + test("in progress field, tail insertion", () => { + const state = reducer( + { ...defaultState, values: ["a", "b", "c"] }, + { type: "handle-change", index: 3, value: "d", reset: false }, + ); + + expect(state).toMatchObject({ + values: ["a", "b", "c", "d"], + cursor: 4, + }); + }); + + test("complete field, head insertion", () => { + const state = reducer( + { ...defaultState, values: ["a", "b", "c", "d", "e"] }, + { type: "handle-change", index: 1, value: "f", reset: false }, + ); + + expect(state).toMatchObject({ + values: ["a", "f", "c", "d", "e"], + cursor: 2, + }); + }); + + test("complete field, tail insertion", () => { + const prevState = { + ...defaultState, + values: ["a", "b", "c", "d", "e"], + }; + const state = reducer(prevState, { + type: "handle-change", + index: 4, + value: "f", + reset: false, + }); + expect(state).toMatchObject({ + values: ["a", "b", "c", "d", "f"], + cursor: 4, + }); + }); + }); + + describe("multi value", () => { + test("empty field, value < length", () => { + const state = reducer(defaultState, { + type: "handle-change", + index: 0, + value: "abc", + reset: false, + }); + + expect(state).toMatchObject({ + values: ["a", "b", "c"], + cursor: 3, + }); + }); + + test("empty field, value = length", () => { + const state = reducer(defaultState, { + type: "handle-change", + index: 0, + value: "abcde", + reset: false, + }); + + expect(state).toMatchObject({ + values: ["a", "b", "c", "d", "e"], + cursor: 4, + }); + }); + + test("empty field, value > length", () => { + const state = reducer(defaultState, { + type: "handle-change", + index: 0, + value: "abcdefg", + reset: false, + }); + + expect(state).toMatchObject({ + values: ["a", "b", "c", "d", "e"], + cursor: 4, + }); + }); + + test("in progress field, head insertion, value < length", () => { + const state = reducer( + { ...defaultState, values: ["a", "b", "c"] }, + { type: "handle-change", index: 1, value: "def", reset: false }, + ); + + expect(state).toMatchObject({ + values: ["a", "d", "e", "f"], + cursor: 4, + }); + }); + + test("in progress field, head insertion, value = length", () => { + const state = reducer( + { ...defaultState, values: ["a", "b", "c"] }, + { type: "handle-change", index: 1, value: "defg", reset: false }, + ); + + expect(state).toMatchObject({ + values: ["a", "d", "e", "f", "g"], + cursor: 4, + }); + }); + + test("in progress field, head insertion, value > length", () => { + const state = reducer( + { ...defaultState, values: ["a", "b", "c"] }, + { type: "handle-change", index: 1, value: "defghi", reset: false }, + ); + + expect(state).toMatchObject({ + values: ["a", "d", "e", "f", "g"], + cursor: 4, + }); + }); + + test("in progress field, tail insertion, value < length", () => { + const state = reducer( + { ...defaultState, values: ["a", "b"] }, + { type: "handle-change", index: 2, value: "cd", reset: false }, + ); + + expect(state).toMatchObject({ + values: ["a", "b", "c", "d"], + cursor: 4, + }); + }); + + test("in progress field, tail insertion, value = length", () => { + const state = reducer( + { ...defaultState, values: ["a", "b"] }, + { type: "handle-change", index: 2, value: "cde", reset: false }, + ); + + expect(state).toMatchObject({ + values: ["a", "b", "c", "d", "e"], + cursor: 4, + }); + }); + + test("in progress field, tail insertion, value > length", () => { + const state = reducer( + { ...defaultState, values: ["a", "b"] }, + { type: "handle-change", index: 2, value: "cdefg", reset: false }, + ); + + expect(state).toMatchObject({ + values: ["a", "b", "c", "d", "e"], + cursor: 4, + }); + }); + + test("complete field, head insertion, value < length", () => { + const state = reducer( + { ...defaultState, values: ["a", "b", "c", "d", "e"] }, + { type: "handle-change", index: 1, value: "fg", reset: false }, + ); + + expect(state).toMatchObject({ + values: ["a", "f", "g", "d", "e"], + cursor: 3, + }); + }); + + test("complete field, head insertion, value = length", () => { + const state = reducer( + { ...defaultState, values: ["a", "b", "c", "d", "e"] }, + { type: "handle-change", index: 1, value: "fghi", reset: false }, + ); + + expect(state).toMatchObject({ + values: ["a", "f", "g", "h", "i"], + cursor: 4, + }); }); - test("completed field", () => { - // TODO + test("complete field, head insertion, value > length", () => { + const state = reducer( + { ...defaultState, values: ["a", "b", "c", "d", "e"] }, + { type: "handle-change", index: 1, value: "fghijkl", reset: false }, + ); + + expect(state).toMatchObject({ + values: ["a", "f", "g", "h", "i"], + cursor: 4, + }); }); }); }); describe("handle-key-down", () => { - // TODO - }); + test("no deletion", () => { + const state = reducer(defaultState, { + type: "handle-key-down", + index: 0, + key: "", + code: "", + keyCode: 0, + which: 0, + }); - // describe("handle-key-down", () => { - // test("unidentified", () => { - // const [state, eff] = stateReducer(currState, { - // type: "handle-key-down", - // key: "Unidentified", - // idx: 0, - // val: "", - // }); - // expect(state).toMatchObject(state); - // expect(eff).toEqual([]); - // }); - // test("dead", () => { - // const [state, eff] = stateReducer(currState, { - // type: "handle-key-down", - // key: "Dead", - // idx: 0, - // val: "", - // }); - // expect(state).toMatchObject(state); - // expect(eff).toEqual([ - // { type: "set-input-val", idx: 0, val: "" }, - // { type: "reject-key", idx: 0, key: "Dead" }, - // { type: "handle-code-change" }, - // ]); - // }); - // describe("left arrow", () => { - // test("from the first input", () => { - // const [state, eff] = stateReducer(currState, { - // type: "handle-key-down", - // key: "ArrowLeft", - // idx: 0, - // val: "", - // }); - // expect(state).toMatchObject({ ...state, focusIdx: 0 }); - // expect(eff).toEqual([{ type: "focus-input", idx: 0 }]); - // }); - // test("from the last input", () => { - // const [state, eff] = stateReducer( - // { ...currState, focusIdx: 4 }, - // { type: "handle-key-down", key: "ArrowLeft", idx: 0, val: "" }, - // ); - // expect(state).toMatchObject({ ...state, focusIdx: 3 }); - // expect(eff).toEqual([{ type: "focus-input", idx: 3 }]); - // }); - // }); - // describe("right arrow", () => { - // test("from the first input", () => { - // const [state, eff] = stateReducer(currState, { - // type: "handle-key-down", - // key: "ArrowRight", - // idx: 0, - // val: "", - // }); - // expect(state).toMatchObject({ ...state, focusIdx: 1 }); - // expect(eff).toEqual([{ type: "focus-input", idx: 1 }]); - // }); - // test("from the last input", () => { - // const [state, eff] = stateReducer( - // { ...currState, focusIdx: 4 }, - // { type: "handle-key-down", key: "ArrowRight", idx: 0, val: "" }, - // ); - // expect(state).toMatchObject({ ...state, focusIdx: 4 }); - // expect(eff).toEqual([{ type: "focus-input", idx: 4 }]); - // }); - // }); - // test("backspace", () => { - // const [state, eff] = stateReducer(currState, { - // type: "handle-key-down", - // key: "Backspace", - // idx: 0, - // val: "", - // }); - // expect(state).toMatchObject({ ...state, focusIdx: 0 }); - // expect(eff).toEqual([{ type: "handle-delete", idx: 0 }, { type: "handle-code-change" }]); - // }); - // test("delete", () => { - // const [state, eff] = stateReducer(currState, { - // type: "handle-key-down", - // key: "Delete", - // idx: 0, - // val: "", - // }); - // expect(state).toMatchObject({ ...state, focusIdx: 0 }); - // expect(eff).toEqual([{ type: "handle-delete", idx: 0 }, { type: "handle-code-change" }]); - // }); - // describe("default", () => { - // test("resolve", () => { - // const [state, eff] = stateReducer(currState, { - // type: "handle-key-down", - // key: "a", - // idx: 0, - // val: "", - // }); - // expect(state).toMatchObject({ ...state, focusIdx: 1 }); - // expect(eff).toEqual([ - // { type: "set-input-val", idx: 0, val: "a" }, - // { type: "resolve-key", idx: 0, key: "a" }, - // { type: "focus-input", idx: 1 }, - // { type: "handle-code-change" }, - // ]); - // }); - // test("reject", () => { - // const [state, eff] = stateReducer(currState, { - // type: "handle-key-down", - // key: "@", - // idx: 0, - // val: "", - // }); - // expect(state).toMatchObject(state); - // expect(eff).toEqual([{ type: "reject-key", idx: 0, key: "@" }]); - // }); - // }); - // }); - // describe("handle-key-up", () => { - // test("no fallback", () => { - // const [state, eff] = stateReducer(currState, { - // type: "handle-key-up", - // idx: 0, - // val: "", - // }); - // expect(state).toMatchObject(state); - // expect(eff).toEqual([]); - // }); - // test("empty prevVal, empty val", () => { - // const [state, eff] = stateReducer( - // { ...currState, fallback: { idx: 0, val: "" } }, - // { type: "handle-key-up", idx: 0, val: "" }, - // ); - // expect(state).toMatchObject({ fallback: null }); - // expect(eff).toEqual([{ type: "handle-delete", idx: 0 }, { type: "handle-code-change" }]); - // }); - // test("empty prevVal, not empty allowed val", () => { - // const [state, eff] = stateReducer( - // { ...currState, fallback: { idx: 0, val: "" } }, - // { type: "handle-key-up", idx: 0, val: "a" }, - // ); - // expect(state).toMatchObject({ fallback: null }); - // expect(eff).toEqual([ - // { type: "set-input-val", idx: 0, val: "a" }, - // { type: "resolve-key", idx: 0, key: "a" }, - // { type: "focus-input", idx: 1 }, - // { type: "handle-code-change" }, - // ]); - // }); - // test("empty prevVal, not empty denied val", () => { - // const [state, eff] = stateReducer( - // { ...currState, fallback: { idx: 0, val: "" } }, - // { type: "handle-key-up", idx: 0, val: "@" }, - // ); - // expect(state).toMatchObject({ fallback: null }); - // expect(eff).toEqual([ - // { type: "set-input-val", idx: 0, val: "" }, - // { type: "reject-key", idx: 0, key: "@" }, - // { type: "handle-code-change" }, - // ]); - // }); - // test("not empty prevVal", () => { - // const [state, eff] = stateReducer( - // { ...currState, fallback: { idx: 0, val: "a" } }, - // { type: "handle-key-up", idx: 0, val: "a" }, - // ); - // expect(state).toMatchObject({ fallback: null }); - // expect(eff).toEqual([ - // { type: "set-input-val", idx: 0, val: "a" }, - // { type: "resolve-key", idx: 0, key: "a" }, - // { type: "focus-input", idx: 1 }, - // { type: "handle-code-change" }, - // ]); - // }); - // }); - // describe("handle-paste", () => { - // test("paste smaller text than code length", () => { - // const [state, eff] = stateReducer(currState, { - // type: "handle-paste", - // idx: 0, - // val: "abc", - // }); - // expect(state).toMatchObject({ ...state, focusIdx: 3 }); - // expect(eff).toEqual([ - // { type: "set-input-val", idx: 0, val: "a" }, - // { type: "resolve-key", idx: 0, key: "a" }, - // { type: "set-input-val", idx: 1, val: "b" }, - // { type: "resolve-key", idx: 1, key: "b" }, - // { type: "set-input-val", idx: 2, val: "c" }, - // { type: "resolve-key", idx: 2, key: "c" }, - // { type: "focus-input", idx: 3 }, - // { type: "handle-code-change" }, - // ]); - // }); - // test("paste bigger text than code length", () => { - // const [state, eff] = stateReducer(currState, { - // type: "handle-paste", - // idx: 0, - // val: "abcdefgh", - // }); - // expect(state).toMatchObject({ ...state, focusIdx: 4 }); - // expect(eff).toEqual([ - // { type: "set-input-val", idx: 0, val: "a" }, - // { type: "resolve-key", idx: 0, key: "a" }, - // { type: "set-input-val", idx: 1, val: "b" }, - // { type: "resolve-key", idx: 1, key: "b" }, - // { type: "set-input-val", idx: 2, val: "c" }, - // { type: "resolve-key", idx: 2, key: "c" }, - // { type: "set-input-val", idx: 3, val: "d" }, - // { type: "resolve-key", idx: 3, key: "d" }, - // { type: "set-input-val", idx: 4, val: "e" }, - // { type: "resolve-key", idx: 4, key: "e" }, - // { type: "focus-input", idx: 4 }, - // { type: "handle-code-change" }, - // ]); - // }); - // test("paste on last input", () => { - // const [state, eff] = stateReducer({ ...currState, focusIdx: 4 }, { type: "handle-paste", idx: 0, val: "abc" }); - // expect(state).toMatchObject({ ...state, focusIdx: 4 }); - // expect(eff).toEqual([ - // { type: "set-input-val", idx: 4, val: "a" }, - // { type: "resolve-key", idx: 4, key: "a" }, - // { type: "handle-code-change" }, - // ]); - // }); - // test("paste with denied key", () => { - // const [state, eff] = stateReducer(currState, { - // type: "handle-paste", - // idx: 1, - // val: "ab@", - // }); - // expect(state).toMatchObject(state); - // expect(eff).toEqual([ - // { type: "set-input-val", idx: 0, val: "" }, - // { type: "reject-key", idx: 1, key: "ab@" }, - // { type: "handle-code-change" }, - // ]); - // }); - // }); - // test("focus-input", () => { - // const [state, eff] = stateReducer(currState, { type: "focus-input", idx: 2 }); - // expect(state).toMatchObject({ ...state, focusIdx: 2 }); - // expect(eff).toEqual([{ type: "focus-input", idx: 2 }]); - // }); -}); + expect(state).toStrictEqual(defaultState); + }); -// describe("effect reducer", () => { -// const { defaultProps, useEffectReducer } = pinField; -// const inputA = mockInput("a"); -// const inputB = mockInput("b"); -// const inputC = mockInput(""); -// const propsFormatMock = jest.fn(); -// const propsMock = { -// ...defaultProps, -// length: 3, -// format: (char: string) => { -// propsFormatMock.apply(char); -// return char; -// }, -// onResolveKey: jest.fn(), -// onRejectKey: jest.fn(), -// onChange: jest.fn(), -// onComplete: jest.fn(), -// }; - -// const refs: React.RefObject = { -// current: [inputA.ref, inputB.ref, inputC.ref], -// }; -// const effectReducer = useEffectReducer({ ...propsMock, refs }); - -// beforeEach(() => { -// jest.resetAllMocks(); -// }); - -// test("default action", () => { -// // @ts-expect-error bad action -// effectReducer({ type: "bad-action" }); -// }); - -// test("focus input", () => { -// effectReducer({ type: "focus-input", idx: 0 }, noop); -// expect(inputA.ref.focus).toHaveBeenCalledTimes(1); -// }); - -// describe("set input val", () => { -// test("empty char", () => { -// effectReducer({ type: "set-input-val", idx: 0, val: "" }, noop); - -// expect(propsFormatMock).toHaveBeenCalledTimes(1); -// expect(inputA.setValMock).toHaveBeenCalledTimes(1); -// expect(inputA.setValMock).toHaveBeenCalledWith(""); -// }); - -// test("non empty char", () => { -// effectReducer({ type: "set-input-val", idx: 0, val: "a" }, noop); - -// expect(propsFormatMock).toHaveBeenCalledTimes(1); -// expect(inputA.setValMock).toHaveBeenCalledTimes(1); -// expect(inputA.setValMock).toHaveBeenCalledWith("a"); -// }); -// }); - -// test("resolve key", () => { -// effectReducer({ type: "resolve-key", idx: 0, key: "a" }, noop); - -// expect(inputA.ref.setCustomValidity).toHaveBeenCalledTimes(1); -// expect(inputA.ref.setCustomValidity).toHaveBeenCalledWith(""); -// expect(propsMock.onResolveKey).toHaveBeenCalledTimes(1); -// expect(propsMock.onResolveKey).toHaveBeenCalledWith("a", inputA.ref); -// }); - -// test("reject key", () => { -// effectReducer({ type: "reject-key", idx: 0, key: "a" }, noop); - -// expect(inputA.ref.setCustomValidity).toHaveBeenCalledTimes(1); -// expect(inputA.ref.setCustomValidity).toHaveBeenCalledWith("Invalid key"); -// expect(propsMock.onRejectKey).toHaveBeenCalledTimes(1); -// expect(propsMock.onRejectKey).toHaveBeenCalledWith("a", inputA.ref); -// }); - -// describe("handle backspace", () => { -// test("from input A, not empty val", () => { -// effectReducer({ type: "handle-delete", idx: 0 }, noop); - -// expect(inputA.ref.setCustomValidity).toHaveBeenCalledTimes(1); -// expect(inputA.ref.setCustomValidity).toHaveBeenCalledWith(""); -// expect(inputA.setValMock).toHaveBeenCalledTimes(1); -// expect(inputA.setValMock).toHaveBeenCalledWith(""); -// }); - -// test("from input B, not empty val", () => { -// effectReducer({ type: "handle-delete", idx: 1 }, noop); - -// expect(inputB.ref.setCustomValidity).toHaveBeenCalledTimes(1); -// expect(inputB.ref.setCustomValidity).toHaveBeenCalledWith(""); -// expect(inputB.setValMock).toHaveBeenCalledTimes(1); -// expect(inputB.setValMock).toHaveBeenCalledWith(""); -// }); - -// test("from input C, empty val", () => { -// effectReducer({ type: "handle-delete", idx: 2 }, noop); - -// expect(inputC.ref.setCustomValidity).toHaveBeenCalledTimes(1); -// expect(inputC.ref.setCustomValidity).toHaveBeenCalledWith(""); -// expect(inputC.setValMock).toHaveBeenCalledTimes(1); -// expect(inputC.setValMock).toHaveBeenCalledWith(""); -// expect(inputB.ref.focus).toHaveBeenCalledTimes(1); -// expect(inputB.ref.setCustomValidity).toHaveBeenCalledWith(""); -// expect(inputB.setValMock).toHaveBeenCalledTimes(1); -// expect(inputB.setValMock).toHaveBeenCalledWith(""); -// }); -// }); - -// describe("handle-code-change", () => { -// test("code not complete", () => { -// effectReducer({ type: "handle-code-change" }, noop); - -// expect(propsMock.onChange).toHaveBeenCalledTimes(1); -// expect(propsMock.onChange).toHaveBeenCalledWith("ab"); -// }); - -// test("code complete", () => { -// const inputA = mockInput("a"); -// const inputB = mockInput("b"); -// const inputC = mockInput("c"); -// const refs: React.RefObject = { -// current: [inputA.ref, inputB.ref, inputC.ref], -// }; -// const notify = useEffectReducer({ ...propsMock, refs }); - -// notify({ type: "handle-code-change" }, noop); - -// expect(propsMock.onChange).toHaveBeenCalledTimes(1); -// expect(propsMock.onChange).toHaveBeenCalledWith("abc"); -// expect(propsMock.onComplete).toHaveBeenCalledTimes(1); -// expect(propsMock.onComplete).toHaveBeenCalledWith("abc"); -// }); - -// test("rtl", () => { -// jest.spyOn(document.documentElement, "getAttribute").mockImplementation(() => "rtl"); - -// const inputA = mockInput("a"); -// const inputB = mockInput("b"); -// const inputC = mockInput("c"); -// const refs: React.RefObject = { -// current: [inputA.ref, inputB.ref, inputC.ref], -// }; -// const notify = useEffectReducer({ ...propsMock, refs }); - -// notify({ type: "handle-code-change" }, noop); - -// expect(propsMock.onChange).toHaveBeenCalledTimes(1); -// expect(propsMock.onChange).toHaveBeenCalledWith("cba"); -// expect(propsMock.onComplete).toHaveBeenCalledTimes(1); -// expect(propsMock.onComplete).toHaveBeenCalledWith("cba"); -// }); - -// test("rtl with override in props", () => { -// jest.spyOn(document.documentElement, "getAttribute").mockImplementation(() => "rtl"); - -// const inputA = mockInput("a"); -// const inputB = mockInput("b"); -// const inputC = mockInput("c"); -// const refs: React.RefObject = { -// current: [inputA.ref, inputB.ref, inputC.ref], -// }; -// const propsWithDir = { ...propsMock, dir: "ltr" }; -// const notify = useEffectReducer({ ...propsWithDir, refs }); - -// notify({ type: "handle-code-change" }, noop); - -// expect(propsMock.onChange).toHaveBeenCalledTimes(1); -// expect(propsMock.onChange).toHaveBeenCalledWith("abc"); -// expect(propsMock.onComplete).toHaveBeenCalledTimes(1); -// expect(propsMock.onComplete).toHaveBeenCalledWith("abc"); -// }); -// }); -// }); + test.each>([ + { key: "Backspace" }, + { key: "Delete" }, + { code: "Backspace" }, + { code: "Delete" }, + { keyCode: BACKSPACE }, + { keyCode: DELETE }, + { which: BACKSPACE }, + { which: DELETE }, + ])("delete with %o when value exists at index", (action) => { + const prevState: State = { ...defaultState, values: ["a", "b"] }; + const state = reducer(prevState, { + type: "handle-key-down", + index: 1, + ...action, + }); + expect(state).toStrictEqual(prevState); + }); + + test("delete when empty value at index, cursor > 0", () => { + const state = reducer( + { ...defaultState, values: ["a", "b"] }, + { type: "handle-key-down", index: 2, key: "Backspace" }, + ); + + expect(state).toMatchObject({ + cursor: 1, + backspace: true, + dirty: true, + }); + }); + + test("delete when empty value at index, cursor = 0", () => { + const state = reducer( + { ...defaultState, values: ["", "b"] }, + { type: "handle-key-down", index: 0, key: "Backspace" }, + ); + + expect(state).toMatchObject({ + cursor: 0, + backspace: true, + dirty: true, + }); + }); + }); +}); diff --git a/src/pin-field/pin-field-v2.tsx b/src/pin-field/pin-field-v2.tsx index d400c55..65d1468 100644 --- a/src/pin-field/pin-field-v2.tsx +++ b/src/pin-field/pin-field-v2.tsx @@ -1,5 +1,4 @@ import { - FC, InputHTMLAttributes, useEffect, useReducer, @@ -78,12 +77,45 @@ export const defaultState: State = { dirty: false, }; +export type NoOpAction = { + type: "noop"; +}; + +export type UpdatePropsAction = { + type: "update-props"; + props: Partial; +}; + +export type HandleCompositionStartAction = { + type: "start-composition"; + index: number; +}; + +export type HandleCompositionEndAction = { + type: "end-composition"; + index: number; + value: string; +}; + +export type HandleKeyChangeAction = { + type: "handle-change"; + index: number; + value: string | null; + reset?: boolean; +}; + +export type HandleKeyDownAction = { + type: "handle-key-down"; + index: number; +} & Partial, "key" | "code" | "keyCode" | "which">>; + export type Action = - | { type: "update-props"; props: Partial } - | { type: "handle-change"; index: number; value: string | null; reset?: boolean } - | { type: "handle-key-down"; index: number; event: KeyboardEvent } - | { type: "start-composition"; index: number } - | { type: "end-composition"; index: number; value: string }; + | NoOpAction + | UpdatePropsAction + | HandleCompositionStartAction + | HandleCompositionEndAction + | HandleKeyChangeAction + | HandleKeyDownAction; export function reducer(prevState: State, action: Action): State { switch (action.type) { @@ -136,7 +168,7 @@ export function reducer(prevState: State, action: Action): State { const state: State = { ...prevState }; if (action.reset) { - state.values = Array(state.length); + state.values.splice(action.index, state.length); } if (action.value) { @@ -158,10 +190,10 @@ export function reducer(prevState: State, action: Action): State { case "handle-key-down": { // determine if a deletion key is pressed - const fromKey = action.event.key === "Backspace" || action.event.key === "Delete"; - const fromCode = action.event.code === "Backspace" || action.event.code === "Delete"; - const fromKeyCode = action.event.keyCode === BACKSPACE || action.event.keyCode === DELETE; - const fromWhich = action.event.which === BACKSPACE || action.event.which === DELETE; + const fromKey = action.key === "Backspace" || action.key === "Delete"; + const fromCode = action.code === "Backspace" || action.code === "Delete"; + const fromKeyCode = action.keyCode === BACKSPACE || action.keyCode === DELETE; + const fromWhich = action.which === BACKSPACE || action.which === DELETE; const deletion = fromKey || fromCode || fromKeyCode || fromWhich; // return the same state reference if no deletion detected @@ -203,6 +235,10 @@ export function reducer(prevState: State, action: Action): State { return state; } } + + case "noop": + default: + break; } return prevState; @@ -235,10 +271,13 @@ export function usePinField(): Handler { [dispatch, state.cursor], ); - return useMemo(() => ({ refs, state, dispatch, value, setValue }), [refs, state, dispatch, value, setValue]); + return useMemo( + () => ({ refs, state, dispatch, value, setValue }), + [refs, state, dispatch, value, setValue], + ); } -export const PinFieldV2: FC = forwardRef( +export const PinFieldV2 = forwardRef( ( { length = defaultProps.length, @@ -267,15 +306,21 @@ export const PinFieldV2: FC = forwardRef( function handleKeyDownAt(index: number): KeyboardEventHandler { return event => { - dispatch({ type: "handle-key-down", index, event }); + console.log("keyDown", index, event); + const { key, code, keyCode, which } = event; + dispatch({ type: "handle-key-down", index, key, code, keyCode, which }); }; } function handleChangeAt(index: number): ChangeEventHandler { return event => { - // should never happen, mostly for typescript to infer properly - if (!(event.nativeEvent instanceof InputEvent)) return; - dispatch({ type: "handle-change", index, value: event.nativeEvent.data }); + if (event.nativeEvent instanceof InputEvent) { + const value = event.nativeEvent.data; + dispatch({ type: "handle-change", index, value }); + } else { + const { value } = event.target; + dispatch({ type: "handle-change", index, value, reset: true }); + } }; } @@ -294,7 +339,9 @@ export const PinFieldV2: FC = forwardRef( // initial props to state update useEffect(() => { if (state.ready) return; - const dir = nativeProps.dir?.toLowerCase() || document.documentElement.getAttribute("dir")?.toLowerCase(); + const dir = + nativeProps.dir?.toLowerCase() || + document.documentElement.getAttribute("dir")?.toLowerCase(); dispatch({ type: "update-props", props: { length, format, dir } }); }, [state.ready, dispatch, length, format]); @@ -315,7 +362,9 @@ export const PinFieldV2: FC = forwardRef( // nativeProps.dir to state update useEffect(() => { if (!state.ready) return; - const dir = nativeProps.dir?.toLowerCase() || document.documentElement.getAttribute("dir")?.toLowerCase(); + const dir = + nativeProps.dir?.toLowerCase() || + document.documentElement.getAttribute("dir")?.toLowerCase(); if (dir === state.dir) return; dispatch({ type: "update-props", props: { dir } }); }, [state.ready, nativeProps.dir, state.dir, dispatch]); diff --git a/yarn.lock b/yarn.lock index 3a002af..1125ba7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,14 +7,6 @@ resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.1.tgz#2447a230bfe072c1659e6815129c03cf170710e3" integrity sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ== -"@ampproject/remapping@^2.1.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" - integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== - dependencies: - "@jridgewell/gen-mapping" "^0.1.0" - "@jridgewell/trace-mapping" "^0.3.9" - "@ampproject/remapping@^2.2.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" @@ -23,22 +15,7 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" - integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== - dependencies: - "@babel/highlight" "^7.18.6" - -"@babel/code-frame@^7.22.13": - version "7.22.13" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" - integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== - dependencies: - "@babel/highlight" "^7.22.13" - chalk "^2.4.2" - -"@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0", "@babel/code-frame@^7.26.2": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0", "@babel/code-frame@^7.26.2": version "7.26.2" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== @@ -47,38 +24,12 @@ js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/compat-data@^7.20.0": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.5.tgz#86f172690b093373a933223b4745deeb6049e733" - integrity sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g== - "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.25.9", "@babel/compat-data@^7.26.0": version "7.26.3" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.3.tgz#99488264a56b2aded63983abd6a417f03b92ed02" integrity sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g== -"@babel/core@^7.11.6", "@babel/core@^7.12.3": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.5.tgz#45e2114dc6cd4ab167f81daf7820e8fa1250d113" - integrity sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ== - dependencies: - "@ampproject/remapping" "^2.1.0" - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.5" - "@babel/helper-compilation-targets" "^7.20.0" - "@babel/helper-module-transforms" "^7.20.2" - "@babel/helpers" "^7.20.5" - "@babel/parser" "^7.20.5" - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.5" - "@babel/types" "^7.20.5" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.1" - semver "^6.3.0" - -"@babel/core@^7.18.9", "@babel/core@^7.26.0": +"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.18.9", "@babel/core@^7.23.9", "@babel/core@^7.26.0": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.0.tgz#d78b6023cc8f3114ccf049eb219613f74a747b40" integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg== @@ -99,26 +50,7 @@ json5 "^2.2.3" semver "^6.3.1" -"@babel/generator@^7.20.5", "@babel/generator@^7.7.2": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.5.tgz#cb25abee3178adf58d6814b68517c62bdbfdda95" - integrity sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA== - dependencies: - "@babel/types" "^7.20.5" - "@jridgewell/gen-mapping" "^0.3.2" - jsesc "^2.5.1" - -"@babel/generator@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" - integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== - dependencies: - "@babel/types" "^7.23.0" - "@jridgewell/gen-mapping" "^0.3.2" - "@jridgewell/trace-mapping" "^0.3.17" - jsesc "^2.5.1" - -"@babel/generator@^7.26.0", "@babel/generator@^7.26.3": +"@babel/generator@^7.26.0", "@babel/generator@^7.26.3", "@babel/generator@^7.7.2": version "7.26.3" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.3.tgz#ab8d4360544a425c90c248df7059881f4b2ce019" integrity sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ== @@ -136,16 +68,6 @@ dependencies: "@babel/types" "^7.25.9" -"@babel/helper-compilation-targets@^7.20.0": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz#6bf5374d424e1b3922822f1d9bdaa43b1a139d0a" - integrity sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ== - dependencies: - "@babel/compat-data" "^7.20.0" - "@babel/helper-validator-option" "^7.18.6" - browserslist "^4.21.3" - semver "^6.3.0" - "@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz#55af025ce365be3cdc0c1c1e56c6af617ce88875" @@ -190,31 +112,6 @@ lodash.debounce "^4.0.8" resolve "^1.14.2" -"@babel/helper-environment-visitor@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" - integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== - -"@babel/helper-environment-visitor@^7.22.20": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" - integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== - -"@babel/helper-function-name@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" - integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== - dependencies: - "@babel/template" "^7.22.15" - "@babel/types" "^7.23.0" - -"@babel/helper-hoist-variables@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" - integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== - dependencies: - "@babel/types" "^7.22.5" - "@babel/helper-member-expression-to-functions@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz#9dfffe46f727005a5ea29051ac835fb735e4c1a3" @@ -223,13 +120,6 @@ "@babel/traverse" "^7.25.9" "@babel/types" "^7.25.9" -"@babel/helper-module-imports@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" - integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== - dependencies: - "@babel/types" "^7.18.6" - "@babel/helper-module-imports@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715" @@ -238,20 +128,6 @@ "@babel/traverse" "^7.25.9" "@babel/types" "^7.25.9" -"@babel/helper-module-transforms@^7.20.2": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz#ac53da669501edd37e658602a21ba14c08748712" - integrity sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-simple-access" "^7.20.2" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/helper-validator-identifier" "^7.19.1" - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.1" - "@babel/types" "^7.20.2" - "@babel/helper-module-transforms@^7.25.9", "@babel/helper-module-transforms@^7.26.0": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae" @@ -268,12 +144,7 @@ dependencies: "@babel/types" "^7.25.9" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.8.0": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" - integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== - -"@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.25.9": +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.25.9", "@babel/helper-plugin-utils@^7.8.0": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz#9cbdd63a9443a2c92a725cca7ebca12cc8dd9f46" integrity sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw== @@ -296,13 +167,6 @@ "@babel/helper-optimise-call-expression" "^7.25.9" "@babel/traverse" "^7.25.9" -"@babel/helper-simple-access@^7.20.2": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9" - integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA== - dependencies: - "@babel/types" "^7.20.2" - "@babel/helper-skip-transparent-expression-wrappers@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz#0b2e1b62d560d6b1954893fd2b705dc17c91f0c9" @@ -311,55 +175,16 @@ "@babel/traverse" "^7.25.9" "@babel/types" "^7.25.9" -"@babel/helper-split-export-declaration@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" - integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-split-export-declaration@^7.22.6": - version "7.22.6" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" - integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-string-parser@^7.19.4": - version "7.19.4" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" - integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== - -"@babel/helper-string-parser@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" - integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== - "@babel/helper-string-parser@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== -"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": - version "7.19.1" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" - integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== - -"@babel/helper-validator-identifier@^7.22.20": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" - integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== - "@babel/helper-validator-identifier@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== -"@babel/helper-validator-option@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" - integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== - "@babel/helper-validator-option@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72" @@ -374,15 +199,6 @@ "@babel/traverse" "^7.25.9" "@babel/types" "^7.25.9" -"@babel/helpers@^7.20.5": - version "7.20.6" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.6.tgz#e64778046b70e04779dfbdf924e7ebb45992c763" - integrity sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w== - dependencies: - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.5" - "@babel/types" "^7.20.5" - "@babel/helpers@^7.26.0": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.0.tgz#30e621f1eba5aa45fe6f4868d2e9154d884119a4" @@ -391,41 +207,13 @@ "@babel/template" "^7.25.9" "@babel/types" "^7.26.0" -"@babel/highlight@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" - integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== - dependencies: - "@babel/helper-validator-identifier" "^7.18.6" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/highlight@^7.22.13": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" - integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== - dependencies: - "@babel/helper-validator-identifier" "^7.22.20" - chalk "^2.4.2" - js-tokens "^4.0.0" - -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.20.5": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.5.tgz#7f3c7335fe417665d929f34ae5dceae4c04015e8" - integrity sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA== - -"@babel/parser@^7.20.7", "@babel/parser@^7.25.3", "@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.3": +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.25.3", "@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.3": version "7.26.3" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.3.tgz#8c51c5db6ddf08134af1ddbacf16aaab48bac234" integrity sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA== dependencies: "@babel/types" "^7.26.3" -"@babel/parser@^7.22.15", "@babel/parser@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" - integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== - "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz#cc2e53ebf0a0340777fff5ed521943e253b4d8fe" @@ -484,13 +272,20 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-class-properties@^7.8.3": +"@babel/plugin-syntax-class-properties@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== dependencies: "@babel/helper-plugin-utils" "^7.12.13" +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-import-assertions@^7.26.0": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz#620412405058efa56e4a564903b79355020f445f" @@ -498,14 +293,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-syntax-import-attributes@^7.26.0": +"@babel/plugin-syntax-import-attributes@^7.24.7", "@babel/plugin-syntax-import-attributes@^7.26.0": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz#3b1412847699eea739b4f2602c74ce36f6b0b0f7" integrity sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A== dependencies: "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-syntax-import-meta@^7.8.3": +"@babel/plugin-syntax-import-meta@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== @@ -519,21 +314,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-jsx@^7.25.9": +"@babel/plugin-syntax-jsx@^7.25.9", "@babel/plugin-syntax-jsx@^7.7.2": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz#a34313a178ea56f1951599b929c1ceacee719290" integrity sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA== dependencies: "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-syntax-jsx@^7.7.2": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0" - integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-syntax-logical-assignment-operators@^7.8.3": +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== @@ -547,7 +335,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-numeric-separator@^7.8.3": +"@babel/plugin-syntax-numeric-separator@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== @@ -575,27 +363,27 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-top-level-await@^7.8.3": +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-syntax-typescript@^7.25.9": +"@babel/plugin-syntax-typescript@^7.25.9", "@babel/plugin-syntax-typescript@^7.7.2": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz#67dda2b74da43727cf21d46cf9afef23f4365399" integrity sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ== dependencies: "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-syntax-typescript@^7.7.2": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz#4e9a0cfc769c85689b77a2e642d24e9f697fc8c7" - integrity sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.19.0" - "@babel/plugin-syntax-unicode-sets-regex@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" @@ -1156,39 +944,14 @@ "@babel/plugin-transform-modules-commonjs" "^7.25.9" "@babel/plugin-transform-typescript" "^7.25.9" -"@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6": - version "7.20.6" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.6.tgz#facf4879bfed9b5326326273a64220f099b0fce3" - integrity sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA== - dependencies: - regenerator-runtime "^0.13.11" - -"@babel/runtime@^7.17.8", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.17.8", "@babel/runtime@^7.8.4": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== dependencies: regenerator-runtime "^0.14.0" -"@babel/template@^7.18.10", "@babel/template@^7.3.3": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" - integrity sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA== - dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/parser" "^7.18.10" - "@babel/types" "^7.18.10" - -"@babel/template@^7.22.15": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" - integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== - dependencies: - "@babel/code-frame" "^7.22.13" - "@babel/parser" "^7.22.15" - "@babel/types" "^7.22.15" - -"@babel/template@^7.25.9": +"@babel/template@^7.25.9", "@babel/template@^7.3.3": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016" integrity sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg== @@ -1210,32 +973,7 @@ debug "^4.3.1" globals "^11.1.0" -"@babel/traverse@^7.20.1", "@babel/traverse@^7.20.5": - version "7.23.2" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" - integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== - dependencies: - "@babel/code-frame" "^7.22.13" - "@babel/generator" "^7.23.0" - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-function-name" "^7.23.0" - "@babel/helper-hoist-variables" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.23.0" - "@babel/types" "^7.23.0" - debug "^4.1.0" - globals "^11.1.0" - -"@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.5.tgz#e206ae370b5393d94dfd1d04cd687cace53efa84" - integrity sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg== - dependencies: - "@babel/helper-string-parser" "^7.19.4" - "@babel/helper-validator-identifier" "^7.19.1" - to-fast-properties "^2.0.0" - -"@babel/types@^7.20.7", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.3", "@babel/types@^7.4.4": +"@babel/types@^7.0.0", "@babel/types@^7.18.9", "@babel/types@^7.20.7", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.3", "@babel/types@^7.3.3", "@babel/types@^7.4.4": version "7.26.3" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.3.tgz#37e79830f04c2b5687acc77db97fbc75fb81f3c0" integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA== @@ -1243,15 +981,6 @@ "@babel/helper-string-parser" "^7.25.9" "@babel/helper-validator-identifier" "^7.25.9" -"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" - integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== - dependencies: - "@babel/helper-string-parser" "^7.22.5" - "@babel/helper-validator-identifier" "^7.22.20" - to-fast-properties "^2.0.0" - "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -1262,13 +991,6 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== -"@cspotcode/source-map-support@^0.8.0": - version "0.8.1" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" - integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== - dependencies: - "@jridgewell/trace-mapping" "0.3.9" - "@cypress/request@^3.0.1": version "3.0.7" resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.7.tgz#6a74a4da98d9e5ae9121d6e2d9c14780c9b5cf1a" @@ -1446,7 +1168,7 @@ js-yaml "^3.13.1" resolve-from "^5.0.0" -"@istanbuljs/schema@^0.1.2": +"@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3": version "0.1.3" resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== @@ -1651,23 +1373,6 @@ magic-string "^0.27.0" react-docgen-typescript "^2.2.2" -"@jridgewell/gen-mapping@^0.1.0": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" - integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== - dependencies: - "@jridgewell/set-array" "^1.0.0" - "@jridgewell/sourcemap-codec" "^1.4.10" - -"@jridgewell/gen-mapping@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" - integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== - dependencies: - "@jridgewell/set-array" "^1.0.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.9" - "@jridgewell/gen-mapping@^0.3.5": version "0.3.8" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142" @@ -1677,26 +1382,11 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.24" -"@jridgewell/resolve-uri@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" - integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== - -"@jridgewell/resolve-uri@^3.0.3": +"@jridgewell/resolve-uri@^3.1.0": version "3.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== -"@jridgewell/resolve-uri@^3.1.0": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" - integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== - -"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" - integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== - "@jridgewell/set-array@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" @@ -1710,46 +1400,12 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.25" -"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": - version "1.4.14" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" - integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== - -"@jridgewell/sourcemap-codec@^1.4.13", "@jridgewell/sourcemap-codec@^1.5.0": +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.13", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== -"@jridgewell/sourcemap-codec@^1.4.14": - version "1.4.15" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" - integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== - -"@jridgewell/trace-mapping@0.3.9": - version "0.3.9" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" - integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.17" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" - integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== - dependencies: - "@jridgewell/resolve-uri" "3.1.0" - "@jridgewell/sourcemap-codec" "1.4.14" - -"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.18": - version "0.3.19" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811" - integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw== - dependencies: - "@jridgewell/resolve-uri" "^3.1.0" - "@jridgewell/sourcemap-codec" "^1.4.14" - -"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": version "0.3.25" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== @@ -1905,100 +1561,100 @@ estree-walker "^2.0.2" picomatch "^4.0.2" -"@rollup/rollup-android-arm-eabi@4.29.1": - version "4.29.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.29.1.tgz#9bd38df6a29afb7f0336d988bc8112af0c8816c0" - integrity sha512-ssKhA8RNltTZLpG6/QNkCSge+7mBQGUqJRisZ2MDQcEGaK93QESEgWK2iOpIDZ7k9zPVkG5AS3ksvD5ZWxmItw== - -"@rollup/rollup-android-arm64@4.29.1": - version "4.29.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.29.1.tgz#bd1a98390e15b76eeef907175a37c5f0f9e4d214" - integrity sha512-CaRfrV0cd+NIIcVVN/jx+hVLN+VRqnuzLRmfmlzpOzB87ajixsN/+9L5xNmkaUUvEbI5BmIKS+XTwXsHEb65Ew== - -"@rollup/rollup-darwin-arm64@4.29.1": - version "4.29.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.29.1.tgz#bc6fa8a2cc77b5f367424e5e994e3537524e6879" - integrity sha512-2ORr7T31Y0Mnk6qNuwtyNmy14MunTAMx06VAPI6/Ju52W10zk1i7i5U3vlDRWjhOI5quBcrvhkCHyF76bI7kEw== - -"@rollup/rollup-darwin-x64@4.29.1": - version "4.29.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.29.1.tgz#76059c91f06b17406347b127df10f065283b2e61" - integrity sha512-j/Ej1oanzPjmN0tirRd5K2/nncAhS9W6ICzgxV+9Y5ZsP0hiGhHJXZ2JQ53iSSjj8m6cRY6oB1GMzNn2EUt6Ng== - -"@rollup/rollup-freebsd-arm64@4.29.1": - version "4.29.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.29.1.tgz#83178315c0be4b4c8c1fd835e1952d2dc1eb4e6e" - integrity sha512-91C//G6Dm/cv724tpt7nTyP+JdN12iqeXGFM1SqnljCmi5yTXriH7B1r8AD9dAZByHpKAumqP1Qy2vVNIdLZqw== - -"@rollup/rollup-freebsd-x64@4.29.1": - version "4.29.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.29.1.tgz#1ef24fa0576bf7899a0a0a649156606dbd7a0d46" - integrity sha512-hEioiEQ9Dec2nIRoeHUP6hr1PSkXzQaCUyqBDQ9I9ik4gCXQZjJMIVzoNLBRGet+hIUb3CISMh9KXuCcWVW/8w== - -"@rollup/rollup-linux-arm-gnueabihf@4.29.1": - version "4.29.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.29.1.tgz#443a6f5681bf4611caae42988994a6d8ee676216" - integrity sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A== - -"@rollup/rollup-linux-arm-musleabihf@4.29.1": - version "4.29.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.29.1.tgz#9738b27184102228637a683e5f35b22ea352394f" - integrity sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ== - -"@rollup/rollup-linux-arm64-gnu@4.29.1": - version "4.29.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.29.1.tgz#b5e9d5e30ff36a19bedd29c715ba18a1889ff269" - integrity sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA== - -"@rollup/rollup-linux-arm64-musl@4.29.1": - version "4.29.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.29.1.tgz#1d8f68f0829b57f746ec03432ad046f1af014a98" - integrity sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA== - -"@rollup/rollup-linux-loongarch64-gnu@4.29.1": - version "4.29.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.29.1.tgz#07027feb883408e74a3002c8e50caaedd288ae38" - integrity sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw== - -"@rollup/rollup-linux-powerpc64le-gnu@4.29.1": - version "4.29.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.29.1.tgz#544ce1b0847a9c1240425e86f33daceac7ec4e12" - integrity sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w== - -"@rollup/rollup-linux-riscv64-gnu@4.29.1": - version "4.29.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.29.1.tgz#64be13d51852ec1e2dfbd25d997ed5f42f35ea6d" - integrity sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ== - -"@rollup/rollup-linux-s390x-gnu@4.29.1": - version "4.29.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.29.1.tgz#31f51e1e05c6264552d03875d9e2e673f0fd86e3" - integrity sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g== - -"@rollup/rollup-linux-x64-gnu@4.29.1": - version "4.29.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.29.1.tgz#f4c95b26f4ad69ebdb64b42f0ae4da2a0f617958" - integrity sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ== - -"@rollup/rollup-linux-x64-musl@4.29.1": - version "4.29.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.29.1.tgz#ab7be89192f72beb9ea6e2386186fefde4f69d82" - integrity sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA== - -"@rollup/rollup-win32-arm64-msvc@4.29.1": - version "4.29.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.29.1.tgz#7f12efb8240b238346951559998802722944421e" - integrity sha512-F2OiJ42m77lSkizZQLuC+jiZ2cgueWQL5YC9tjo3AgaEw+KJmVxHGSyQfDUoYR9cci0lAywv2Clmckzulcq6ig== - -"@rollup/rollup-win32-ia32-msvc@4.29.1": - version "4.29.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.29.1.tgz#353d14d6eee943004d129796e4feddd3aa260921" - integrity sha512-rYRe5S0FcjlOBZQHgbTKNrqxCBUmgDJem/VQTCcTnA2KCabYSWQDrytOzX7avb79cAAweNmMUb/Zw18RNd4mng== - -"@rollup/rollup-win32-x64-msvc@4.29.1": - version "4.29.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.29.1.tgz#c82f04a09ba481e13857d6f2516e072aaa51b7f4" - integrity sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg== +"@rollup/rollup-android-arm-eabi@4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.0.tgz#f2552f6984cfae52784b2fbf0e47633f38955d66" + integrity sha512-qFcFto9figFLz2g25DxJ1WWL9+c91fTxnGuwhToCl8BaqDsDYMl/kOnBXAyAqkkzAWimYMSWNPWEjt+ADAHuoQ== + +"@rollup/rollup-android-arm64@4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.30.0.tgz#7e5764268d3049b7341c60f1c650f1d71760a5b2" + integrity sha512-vqrQdusvVl7dthqNjWCL043qelBK+gv9v3ZiqdxgaJvmZyIAAXMjeGVSqZynKq69T7062T5VrVTuikKSAAVP6A== + +"@rollup/rollup-darwin-arm64@4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.30.0.tgz#c9245577f673802f0f6de0d46ee776691d77552e" + integrity sha512-617pd92LhdA9+wpixnzsyhVft3szYiN16aNUMzVkf2N+yAk8UXY226Bfp36LvxYTUt7MO/ycqGFjQgJ0wlMaWQ== + +"@rollup/rollup-darwin-x64@4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.30.0.tgz#e492705339542f8b54fa66f630c9d820bc708693" + integrity sha512-Y3b4oDoaEhCypg8ajPqigKDcpi5ZZovemQl9Edpem0uNv6UUjXv7iySBpGIUTSs2ovWOzYpfw9EbFJXF/fJHWw== + +"@rollup/rollup-freebsd-arm64@4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.30.0.tgz#3e13b5d4d44ea87598d5d4db97181db1174fb3c8" + integrity sha512-3REQJ4f90sFIBfa0BUokiCdrV/E4uIjhkWe1bMgCkhFXbf4D8YN6C4zwJL881GM818qVYE9BO3dGwjKhpo2ABA== + +"@rollup/rollup-freebsd-x64@4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.30.0.tgz#138daa08d1b345d605f57b4dedd18a50420488e7" + integrity sha512-ZtY3Y8icbe3Cc+uQicsXG5L+CRGUfLZjW6j2gn5ikpltt3Whqjfo5mkyZ86UiuHF9Q3ZsaQeW7YswlHnN+lAcg== + +"@rollup/rollup-linux-arm-gnueabihf@4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.30.0.tgz#bdaece34f93c3dfd521e9ab8f5c740121862468e" + integrity sha512-bsPGGzfiHXMhQGuFGpmo2PyTwcrh2otL6ycSZAFTESviUoBOuxF7iBbAL5IJXc/69peXl5rAtbewBFeASZ9O0g== + +"@rollup/rollup-linux-arm-musleabihf@4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.30.0.tgz#1804c6ec49be21521eac612513e0666cdde2188c" + integrity sha512-kvyIECEhs2DrrdfQf++maCWJIQ974EI4txlz1nNSBaCdtf7i5Xf1AQCEJWOC5rEBisdaMFFnOWNLYt7KpFqy5A== + +"@rollup/rollup-linux-arm64-gnu@4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.30.0.tgz#2c4bd90f77fcf769502743ec38f184c00a087e08" + integrity sha512-CFE7zDNrokaotXu+shwIrmWrFxllg79vciH4E/zeK7NitVuWEaXRzS0mFfFvyhZfn8WfVOG/1E9u8/DFEgK7WQ== + +"@rollup/rollup-linux-arm64-musl@4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.30.0.tgz#63eadee20f220d28e85cbd10aba671ada8e89c84" + integrity sha512-MctNTBlvMcIBP0t8lV/NXiUwFg9oK5F79CxLU+a3xgrdJjfBLVIEHSAjQ9+ipofN2GKaMLnFFXLltg1HEEPaGQ== + +"@rollup/rollup-linux-loongarch64-gnu@4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.30.0.tgz#1c2c2bb30f61cbbc0fcf4e6c359777fcdb7108cc" + integrity sha512-fBpoYwLEPivL3q368+gwn4qnYnr7GVwM6NnMo8rJ4wb0p/Y5lg88vQRRP077gf+tc25akuqd+1Sxbn9meODhwA== + +"@rollup/rollup-linux-powerpc64le-gnu@4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.30.0.tgz#cea71e0359f086a01c57cf312bef9ec9cc3ba010" + integrity sha512-1hiHPV6dUaqIMXrIjN+vgJqtfkLpqHS1Xsg0oUfUVD98xGp1wX89PIXgDF2DWra1nxAd8dfE0Dk59MyeKaBVAw== + +"@rollup/rollup-linux-riscv64-gnu@4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.30.0.tgz#25ab4a6dbcbd27f4a68382f7963363f886a237aa" + integrity sha512-U0xcC80SMpEbvvLw92emHrNjlS3OXjAM0aVzlWfar6PR0ODWCTQtKeeB+tlAPGfZQXicv1SpWwRz9Hyzq3Jx3g== + +"@rollup/rollup-linux-s390x-gnu@4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.30.0.tgz#7054b237152d9e36c51194532a6b70ca1a62a487" + integrity sha512-VU/P/IODrNPasgZDLIFJmMiLGez+BN11DQWfTVlViJVabyF3JaeaJkP6teI8760f18BMGCQOW9gOmuzFaI1pUw== + +"@rollup/rollup-linux-x64-gnu@4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.30.0.tgz#3656a8341a6048f2111f423301aaad8e84a5fe90" + integrity sha512-laQVRvdbKmjXuFA3ZiZj7+U24FcmoPlXEi2OyLfbpY2MW1oxLt9Au8q9eHd0x6Pw/Kw4oe9gwVXWwIf2PVqblg== + +"@rollup/rollup-linux-x64-musl@4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.30.0.tgz#cf8ae018ea6ff65eb36722a28beb93a20a6047f0" + integrity sha512-3wzKzduS7jzxqcOvy/ocU/gMR3/QrHEFLge5CD7Si9fyHuoXcidyYZ6jyx8OPYmCcGm3uKTUl+9jUSAY74Ln5A== + +"@rollup/rollup-win32-arm64-msvc@4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.30.0.tgz#6b968f5b068469db16eac743811ee6c040671042" + integrity sha512-jROwnI1+wPyuv696rAFHp5+6RFhXGGwgmgSfzE8e4xfit6oLRg7GyMArVUoM3ChS045OwWr9aTnU+2c1UdBMyw== + +"@rollup/rollup-win32-ia32-msvc@4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.30.0.tgz#0321de1a0540dd402e8e523d90cbd9d16f1b9e96" + integrity sha512-duzweyup5WELhcXx5H1jokpr13i3BV9b48FMiikYAwk/MT1LrMYYk2TzenBd0jj4ivQIt58JWSxc19y4SvLP4g== + +"@rollup/rollup-win32-x64-msvc@4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.30.0.tgz#7384b359bb45c0c3c76ba2c7aaec1d047305efcb" + integrity sha512-DYvxS0M07PvgvavMIybCOBYheyrqlui6ZQBHJs6GqduVzHSZ06TPPvlfvnYstjODHQ8UUXFwt5YE+h0jFI8kwg== "@rushstack/node-core-library@5.10.1": version "5.10.1" @@ -2045,19 +1701,19 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== -"@sinonjs/commons@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3" - integrity sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg== +"@sinonjs/commons@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" + integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== dependencies: type-detect "4.0.8" "@sinonjs/fake-timers@^10.0.2": - version "10.0.2" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz#d10549ed1f423d80639c528b6c7f5a1017747d0c" - integrity sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw== + version "10.3.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66" + integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== dependencies: - "@sinonjs/commons" "^2.0.0" + "@sinonjs/commons" "^3.0.0" "@size-limit/esbuild@11.1.6": version "11.1.6" @@ -2383,48 +2039,17 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== -"@tsconfig/node10@^1.0.7": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" - integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== - -"@tsconfig/node12@^1.0.7": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" - integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== - -"@tsconfig/node14@^1.0.0": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" - integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== - -"@tsconfig/node16@^1.0.2": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" - integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== - "@types/argparse@1.0.38": version "1.0.38" resolved "https://registry.yarnpkg.com/@types/argparse/-/argparse-1.0.38.tgz#a81fd8606d481f873a3800c6ebae4f1d768a56a9" integrity sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA== "@types/aria-query@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.1.tgz#3286741fb8f1e1580ac28784add4c7a1d49bdfbc" - integrity sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q== - -"@types/babel__core@^7.1.14": - version "7.1.20" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.20.tgz#e168cdd612c92a2d335029ed62ac94c95b362359" - integrity sha512-PVb6Bg2QuscZ30FvOU7z4guG6c926D9YRvOxEaelzndpMsvP+YM74Q/dAFASpg2l6+XLalxSGxcq/lrgYWZtyQ== - dependencies: - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" - "@types/babel__generator" "*" - "@types/babel__template" "*" - "@types/babel__traverse" "*" + version "5.0.4" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" + integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== -"@types/babel__core@^7.18.0", "@types/babel__core@^7.20.5": +"@types/babel__core@^7.1.14", "@types/babel__core@^7.18.0", "@types/babel__core@^7.20.5": version "7.20.5" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== @@ -2436,28 +2061,21 @@ "@types/babel__traverse" "*" "@types/babel__generator@*": - version "7.6.4" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7" - integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== + version "7.6.8" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.8.tgz#f836c61f48b1346e7d2b0d93c6dacc5b9535d3ab" + integrity sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw== dependencies: "@babel/types" "^7.0.0" "@types/babel__template@*": - version "7.4.1" - resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969" - integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g== + version "7.4.4" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" + integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== dependencies: "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" -"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": - version "7.18.3" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.3.tgz#dfc508a85781e5698d5b33443416b6268c4b3e8d" - integrity sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w== - dependencies: - "@babel/types" "^7.3.0" - -"@types/babel__traverse@^7.18.0": +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6", "@types/babel__traverse@^7.18.0": version "7.20.6" resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.6.tgz#8dc9f0ae0f202c08d8d4dab648912c8d6038e3f7" integrity sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg== @@ -2491,28 +2109,28 @@ integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== "@types/graceful-fs@^4.1.3": - version "4.1.5" - resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" - integrity sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw== + version "4.1.9" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" + integrity sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ== dependencies: "@types/node" "*" "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" - integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" + integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== "@types/istanbul-lib-report@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" - integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz#53047614ae72e19fc0401d872de3ae2b4ce350bf" + integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA== dependencies: "@types/istanbul-lib-coverage" "*" "@types/istanbul-reports@^3.0.0": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" - integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54" + integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== dependencies: "@types/istanbul-lib-report" "*" @@ -2539,21 +2157,16 @@ integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== "@types/lodash@^4.17.13": - version "4.17.13" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.13.tgz#786e2d67cfd95e32862143abe7463a7f90c300eb" - integrity sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg== + version "4.17.14" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.14.tgz#bafc053533f4cdc5fcc9635af46a963c1f3deaff" + integrity sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A== "@types/mdx@^2.0.0": version "2.0.13" resolved "https://registry.yarnpkg.com/@types/mdx/-/mdx-2.0.13.tgz#68f6877043d377092890ff5b298152b0a21671bd" integrity sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw== -"@types/node@*": - version "18.11.15" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.15.tgz#de0e1fbd2b22b962d45971431e2ae696643d3f5d" - integrity sha512-VkhBbVo2+2oozlkdHXLrb3zjsRkpdnaU2bXmX8Wgle3PUi569eLRaHGlgETQHR7lLL1w7GiG3h9SnePhxNDecw== - -"@types/node@^22.10.5": +"@types/node@*", "@types/node@^22.10.5": version "22.10.5" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.5.tgz#95af89a3fb74a2bb41ef9927f206e6472026e48b" integrity sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ== @@ -2566,9 +2179,9 @@ integrity sha512-c1s+7TKFaDRRxr1TxccIX2u7sfCnc3RxkVyBIUA2lCpyqCF+QoAwQ/CBg7bsMdVwP120HEH143VQezKtef5nCg== "@types/react@^19.0.2": - version "19.0.2" - resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.2.tgz#9363e6b3ef898c471cb182dd269decc4afc1b4f6" - integrity sha512-USU8ZI/xyKJwFTpjSVIrSeHBVAGagkHQKPNbxeWwql/vDmnTIBgx+TJnhFnj1NXgz8XfprU0egV2dROLGpsBEg== + version "19.0.3" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.3.tgz#7867240defc1a3686f151644ac886a7e8e0868f4" + integrity sha512-UavfHguIjnnuq9O67uXfgy/h3SRJbidAYvNjLceB+2RIKVRBzVsh0QO+Pw6BCSQqFS9xwzKfwstXx0m6AbAREA== dependencies: csstype "^3.0.2" @@ -2583,19 +2196,19 @@ integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g== "@types/sizzle@^2.3.2": - version "2.3.3" - resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef" - integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ== + version "2.3.9" + resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.9.tgz#d4597dbd4618264c414d7429363e3f50acb66ea2" + integrity sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w== "@types/stack-utils@^2.0.0": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" - integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" + integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== "@types/tough-cookie@*": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397" - integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw== + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" + integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== "@types/uuid@^9.0.1": version "9.0.8" @@ -2603,21 +2216,21 @@ integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== "@types/yargs-parser@*": - version "21.0.0" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" - integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== + version "21.0.3" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" + integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== "@types/yargs@^17.0.8": - version "17.0.17" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.17.tgz#5672e5621f8e0fca13f433a8017aae4b7a2a03e7" - integrity sha512-72bWxFKTK6uwWJAVT+3rF6Jo6RTojiJ27FQo8Rf60AL+VZbzoVPnMFhKsUnbjR8A3BTCYQ7Mv3hnl8T0A+CX9g== + version "17.0.33" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.33.tgz#8c32303da83eec050a84b3c7ae7b9f922d13e32d" + integrity sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA== dependencies: "@types/yargs-parser" "*" "@types/yauzl@^2.9.1": - version "2.10.0" - resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599" - integrity sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw== + version "2.10.3" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999" + integrity sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q== dependencies: "@types/node" "*" @@ -2894,23 +2507,13 @@ acorn-globals@^7.0.0: acorn-walk "^8.0.2" acorn-walk@^8.0.2: - version "8.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" - integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== - -acorn-walk@^8.1.1: version "8.3.4" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== dependencies: acorn "^8.11.0" -acorn@^8.1.0, acorn@^8.8.1: - version "8.8.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" - integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== - -acorn@^8.11.0, acorn@^8.14.0, acorn@^8.4.1, acorn@^8.8.2: +acorn@^8.1.0, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.8.1, acorn@^8.8.2: version "8.14.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== @@ -3033,13 +2636,6 @@ ansi-styles@^2.2.1: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" integrity sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA== -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" @@ -3065,11 +2661,6 @@ arch@^2.2.0: resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== -arg@^4.1.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" - integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== - argparse@^1.0.7, argparse@~1.0.9: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -3085,11 +2676,9 @@ aria-query@5.3.0: dequal "^2.0.3" aria-query@^5.0.0: - version "5.1.3" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" - integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== - dependencies: - deep-equal "^2.0.5" + version "5.3.2" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" + integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: version "1.0.2" @@ -3142,9 +2731,9 @@ astral-regex@^2.0.0: integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== async@^3.2.0: - version "3.2.4" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" - integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + version "3.2.6" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== asynckit@^0.4.0: version "0.4.0" @@ -3156,11 +2745,6 @@ at-least-node@^1.0.0: resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== -available-typed-arrays@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" - integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== - available-typed-arrays@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" @@ -3174,9 +2758,9 @@ aws-sign2@~0.7.0: integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== aws4@^1.8.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" - integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== + version "1.13.2" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.13.2.tgz#0aa167216965ac9474ccfa83892cfb6b3e1e52ef" + integrity sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw== babel-jest@^29.7.0: version "29.7.0" @@ -3245,22 +2829,25 @@ babel-plugin-polyfill-regenerator@^0.6.1: "@babel/helper-define-polyfill-provider" "^0.6.3" babel-preset-current-node-syntax@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" - integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== + version "1.1.0" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz#9a929eafece419612ef4ae4f60b1862ebad8ef30" + integrity sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw== dependencies: "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-syntax-bigint" "^7.8.3" - "@babel/plugin-syntax-class-properties" "^7.8.3" - "@babel/plugin-syntax-import-meta" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-import-attributes" "^7.24.7" + "@babel/plugin-syntax-import-meta" "^7.10.4" "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" "@babel/plugin-syntax-object-rest-spread" "^7.8.3" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-top-level-await" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" babel-preset-jest@^29.6.3: version "29.6.3" @@ -3324,13 +2911,6 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - braces@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" @@ -3343,16 +2923,6 @@ browser-assert@^1.2.1: resolved "https://registry.yarnpkg.com/browser-assert/-/browser-assert-1.2.1.tgz#9aaa5a2a8c74685c2ae05bfe46efd606f068c200" integrity sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ== -browserslist@^4.21.3: - version "4.21.4" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987" - integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw== - dependencies: - caniuse-lite "^1.0.30001400" - electron-to-chromium "^1.4.251" - node-releases "^2.0.6" - update-browserslist-db "^1.0.9" - browserslist@^4.24.0, browserslist@^4.24.2: version "4.24.3" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.3.tgz#5fc2725ca8fb3c1432e13dac278c7cc103e026d2" @@ -3394,9 +2964,9 @@ bytes-iec@^3.1.1: integrity sha512-fey6+4jDK7TFtFg/klGSvNKJctyU7n2aQdnM+CO0ruLPbqqMOM8Tio0Pc+deqUeVKX1tL5DQep1zQ7+37aTAsA== cachedir@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" - integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw== + version "2.4.0" + resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.4.0.tgz#7fef9cf7367233d7c88068fe6e34ed0d355a610d" + integrity sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ== call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1: version "1.0.1" @@ -3406,14 +2976,6 @@ call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1: es-errors "^1.3.0" function-bind "^1.1.2" -call-bind@^1.0.0, call-bind@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== - dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" - call-bind@^1.0.7, call-bind@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" @@ -3447,11 +3009,6 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001400: - version "1.0.30001439" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001439.tgz#ab7371faeb4adff4b74dad1718a6fd122e45d9cb" - integrity sha512-1MgUzEkoMO6gKfXflStpYgZDlFM7M/ck/bgfVCACO5vnAf0fXoNVHdWtqGU+MYca+4bL9Z5bpOVmR33cWW9G2A== - caniuse-lite@^1.0.30001688: version "1.0.30001690" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz#f2d15e3aaf8e18f76b2b8c1481abde063b8104c8" @@ -3484,15 +3041,6 @@ chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - chalk@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" @@ -3537,14 +3085,14 @@ chrome-trace-event@^1.0.2: integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== ci-info@^3.2.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.0.tgz#6d01b3696c59915b6ce057e4aa4adfc2fa25f5ef" - integrity sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog== + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== cjs-module-lexer@^1.0.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" - integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== + version "1.4.1" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz#707413784dbb3a72aa11c2f2b042a0bef4004170" + integrity sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA== classnames@^2.5.1: version "2.5.1" @@ -3564,9 +3112,9 @@ cli-cursor@^3.1.0: restore-cursor "^3.1.0" cli-table3@~0.6.1: - version "0.6.3" - resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2" - integrity sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg== + version "0.6.5" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.5.tgz#013b91351762739c16a9567c21a04632e449bf2f" + integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ== dependencies: string-width "^4.2.0" optionalDependencies: @@ -3595,16 +3143,9 @@ co@^4.6.0: integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== collect-v8-coverage@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" - integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" + version "1.0.2" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz#c0b29bcd33bcd0779a1344c2136051e6afd3d9e9" + integrity sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q== color-convert@^2.0.1: version "2.0.1" @@ -3618,20 +3159,15 @@ color-loggers@^0.3.1: resolved "https://registry.yarnpkg.com/color-loggers/-/color-loggers-0.3.2.tgz#04b12224f4ef3f78c1bdfb238f2cee50f72d7e51" integrity sha512-asfXyY1/9N+Cxt30jb0PFy5tccybuMnWVc9J8EJuYoJVlcsUshn+pt2QuyUB3BWKMXVvEH8jgLrCFs11Am8QZA== -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== colorette@^2.0.16: - version "2.0.19" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" - integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" @@ -3675,11 +3211,6 @@ confbox@^0.1.8: resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06" integrity sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w== -convert-source-map@^1.6.0, convert-source-map@^1.7.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" - integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== - convert-source-map@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" @@ -3710,11 +3241,6 @@ create-jest@^29.7.0: jest-util "^29.7.0" prompts "^2.0.1" -create-require@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" - integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== - cross-spawn@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" @@ -3724,9 +3250,9 @@ cross-spawn@^4.0.0: which "^1.2.9" cross-spawn@^7.0.0, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" @@ -3755,9 +3281,9 @@ cssstyle@^2.3.0: cssom "~0.3.6" csstype@^3.0.2: - version "3.1.1" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" - integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== cypress@=13.13.2: version "13.13.2" @@ -3851,21 +3377,21 @@ data-view-byte-offset@^1.0.1: is-data-view "^1.0.1" dayjs@^1.10.4: - version "1.11.7" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2" - integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ== + version "1.11.13" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c" + integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== de-indent@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg== -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4, debug@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== dependencies: - ms "2.1.2" + ms "^2.1.3" debug@^3.1.0: version "3.2.7" @@ -3874,58 +3400,25 @@ debug@^3.1.0: dependencies: ms "^2.1.1" -debug@^4.3.1, debug@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" - integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== - dependencies: - ms "^2.1.3" - decimal.js@^10.4.2: version "10.4.3" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== dedent@^1.0.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.1.tgz#4f3fc94c8b711e9bb2800d185cd6ad20f2a90aff" - integrity sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg== + version "1.5.3" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a" + integrity sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ== deep-eql@^5.0.1: version "5.0.2" resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341" integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q== -deep-equal@^2.0.5: - version "2.1.0" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.1.0.tgz#5ba60402cf44ab92c2c07f3f3312c3d857a0e1dd" - integrity sha512-2pxgvWu3Alv1PoWEyVg7HS8YhGlUFUV7N5oOvfL6d+7xAmLSemMwv/c8Zv/i9KFzxV5Kt5CAvQc70fLwVuf4UA== - dependencies: - call-bind "^1.0.2" - es-get-iterator "^1.1.2" - get-intrinsic "^1.1.3" - is-arguments "^1.1.1" - is-date-object "^1.0.5" - is-regex "^1.1.4" - isarray "^2.0.5" - object-is "^1.1.5" - object-keys "^1.1.1" - object.assign "^4.1.4" - regexp.prototype.flags "^1.4.3" - side-channel "^1.0.4" - which-boxed-primitive "^1.0.2" - which-collection "^1.0.1" - which-typed-array "^1.1.8" - -deep-is@~0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" - integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== - deepmerge@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" - integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== define-data-property@^1.0.1, define-data-property@^1.1.4: version "1.1.4" @@ -3941,14 +3434,6 @@ define-lazy-prop@^2.0.0: resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== -define-properties@^1.1.3, define-properties@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" - integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== - dependencies: - has-property-descriptors "^1.0.0" - object-keys "^1.1.1" - define-properties@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" @@ -3983,11 +3468,6 @@ diff-sequences@^29.6.3: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - doctrine@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" @@ -3996,9 +3476,9 @@ doctrine@^3.0.0: esutils "^2.0.2" dom-accessibility-api@^0.5.9: - version "0.5.14" - resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.14.tgz#56082f71b1dc7aac69d83c4285eef39c15d93f56" - integrity sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg== + version "0.5.16" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" + integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== dom-accessibility-api@^0.6.3: version "0.6.3" @@ -4034,11 +3514,6 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -electron-to-chromium@^1.4.251: - version "1.4.284" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592" - integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA== - electron-to-chromium@^1.5.73: version "1.5.76" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.76.tgz#db20295c5061b68f07c8ea4dfcbd701485d94a3d" @@ -4070,16 +3545,12 @@ enhanced-resolve@^5.17.1: tapable "^2.2.0" enquirer@^2.3.6: - version "2.3.6" - resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" - integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + version "2.4.1" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.4.1.tgz#93334b3fbd74fc7097b224ab4a8fb7e40bf4ae56" + integrity sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ== dependencies: ansi-colors "^4.1.1" - -entities@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174" - integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA== + strip-ansi "^6.0.1" entities@^4.5.0: version "4.5.0" @@ -4160,20 +3631,6 @@ es-errors@^1.3.0: resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== -es-get-iterator@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.2.tgz#9234c54aba713486d7ebde0220864af5e2b283f7" - integrity sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.0" - has-symbols "^1.0.1" - is-arguments "^1.1.0" - is-map "^2.0.2" - is-set "^2.0.2" - is-string "^1.0.5" - isarray "^2.0.5" - es-module-lexer@^1.2.1: version "1.6.0" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.6.0.tgz#da49f587fd9e68ee2404fe4e256c0c7d3a81be21" @@ -4243,12 +3700,7 @@ esbuild-register@^3.5.0: "@esbuild/win32-ia32" "0.24.2" "@esbuild/win32-x64" "0.24.2" -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== - -escalade@^3.2.0: +escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== @@ -4264,14 +3716,13 @@ escape-string-regexp@^2.0.0: integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== escodegen@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" - integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw== + version "2.1.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17" + integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w== dependencies: esprima "^4.0.1" estraverse "^5.2.0" esutils "^2.0.2" - optionator "^0.8.1" optionalDependencies: source-map "~0.6.1" @@ -4434,15 +3885,10 @@ fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-levenshtein@~2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== - fast-uri@^3.0.1: - version "3.0.3" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.3.tgz#892a1c91802d5d7860de728f18608a0573142241" - integrity sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw== + version "3.0.5" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.5.tgz#19f5f9691d0dab9b85861a7bb5d98fca961da9cd" + integrity sha512-5JnBCWpFlMo0a3ciDy/JckMzzv1U9coZrIhedq+HXxxUfDTAiS0LA8OKVao4G9BxmCVck/jtA5r3KAtRWEyD8Q== fb-watchman@^2.0.0: version "2.0.2" @@ -4470,13 +3916,6 @@ figures@^3.2.0: dependencies: escape-string-regexp "^1.0.5" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - fill-range@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" @@ -4536,16 +3975,7 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== -form-data@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" - integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - -form-data@~4.0.0: +form-data@^4.0.0, form-data@~4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48" integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw== @@ -4583,21 +4013,11 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - -fsevents@~2.3.2, fsevents@~2.3.3: +fsevents@^2.3.2, fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - function-bind@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" @@ -4615,7 +4035,7 @@ function.prototype.name@^1.1.6, function.prototype.name@^1.1.8: hasown "^2.0.2" is-callable "^1.2.7" -functions-have-names@^1.2.2, functions-have-names@^1.2.3: +functions-have-names@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== @@ -4630,15 +4050,6 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385" - integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.3" - get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.7.tgz#dcfcb33d3272e15f445d15124bc0a216189b9044" @@ -4740,24 +4151,12 @@ globalthis@^1.0.4: define-properties "^1.2.1" gopd "^1.0.1" -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" - -gopd@^1.2.0: +gopd@^1.0.1, gopd@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.9: - version "4.2.10" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== - -graceful-fs@^4.2.11, graceful-fs@^4.2.4: +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -4769,34 +4168,17 @@ has-ansi@^2.0.0: dependencies: ansi-regex "^2.0.0" -has-bigints@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" - integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== - has-bigints@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.1.0.tgz#28607e965ac967e03cd2a2c70a2636a1edad49fe" integrity sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg== -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== - has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-property-descriptors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" - integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== - dependencies: - get-intrinsic "^1.1.1" - -has-property-descriptors@^1.0.2: +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== @@ -4810,23 +4192,11 @@ has-proto@^1.2.0: dependencies: dunder-proto "^1.0.0" -has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -has-symbols@^1.1.0: +has-symbols@^1.0.3, has-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== -has-tostringtag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" - integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== - dependencies: - has-symbols "^1.0.2" - has-tostringtag@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" @@ -4834,13 +4204,6 @@ has-tostringtag@^1.0.2: dependencies: has-symbols "^1.0.3" -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" @@ -4934,9 +4297,9 @@ import-lazy@~4.0.0: integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw== import-local@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" - integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + version "3.2.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" + integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== dependencies: pkg-dir "^4.2.0" resolve-cwd "^3.0.0" @@ -4986,14 +4349,6 @@ is-arguments@^1.0.4: call-bound "^1.0.2" has-tostringtag "^1.0.2" -is-arguments@^1.1.0, is-arguments@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" - integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - is-array-buffer@^3.0.4, is-array-buffer@^3.0.5: version "3.0.5" resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280" @@ -5018,13 +4373,6 @@ is-async-function@^2.0.0: has-tostringtag "^1.0.2" safe-regex-test "^1.1.0" -is-bigint@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" - integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== - dependencies: - has-bigints "^1.0.1" - is-bigint@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.1.0.tgz#dda7a3445df57a42583db4228682eba7c4170672" @@ -5032,14 +4380,6 @@ is-bigint@^1.1.0: dependencies: has-bigints "^1.0.2" -is-boolean-object@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" - integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - is-boolean-object@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.1.tgz#c20d0c654be05da4fbc23c562635c019e93daf89" @@ -5067,13 +4407,6 @@ is-core-module@^2.16.0: dependencies: hasown "^2.0.2" -is-core-module@^2.9.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" - integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== - dependencies: - has "^1.0.3" - is-data-view@^1.0.1, is-data-view@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.2.tgz#bae0a41b9688986c2188dda6657e56b8f9e63b8e" @@ -5083,14 +4416,7 @@ is-data-view@^1.0.1, is-data-view@^1.0.2: get-intrinsic "^1.2.6" is-typed-array "^1.1.13" -is-date-object@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" - integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== - dependencies: - has-tostringtag "^1.0.0" - -is-date-object@^1.1.0: +is-date-object@^1.0.5, is-date-object@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.1.0.tgz#ad85541996fc7aa8b2729701d27b7319f95d82f7" integrity sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg== @@ -5150,23 +4476,11 @@ is-installed-globally@~0.4.0: global-dirs "^3.0.0" is-path-inside "^3.0.2" -is-map@^2.0.1, is-map@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" - integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== - is-map@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== -is-number-object@^1.0.4: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" - integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== - dependencies: - has-tostringtag "^1.0.0" - is-number-object@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.1.tgz#144b21e95a1bc148205dcc2814a9134ec41b2541" @@ -5190,14 +4504,6 @@ is-potential-custom-element-name@^1.0.1: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== -is-regex@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" - integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - is-regex@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" @@ -5208,11 +4514,6 @@ is-regex@^1.2.1: has-tostringtag "^1.0.2" hasown "^2.0.2" -is-set@^2.0.1, is-set@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" - integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== - is-set@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" @@ -5230,13 +4531,6 @@ is-stream@^2.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== -is-string@^1.0.5: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" - integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== - dependencies: - has-tostringtag "^1.0.0" - is-string@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.1.tgz#92ea3f3d5c5b6e039ca8677e5ac8d07ea773cbb9" @@ -5245,13 +4539,6 @@ is-string@^1.1.1: call-bound "^1.0.3" has-tostringtag "^1.0.2" -is-symbol@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" - integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== - dependencies: - has-symbols "^1.0.2" - is-symbol@^1.0.4, is-symbol@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.1.1.tgz#f47761279f532e2b05a7024a7506dbbedacd0634" @@ -5261,17 +4548,6 @@ is-symbol@^1.0.4, is-symbol@^1.1.1: has-symbols "^1.1.0" safe-regex-test "^1.1.0" -is-typed-array@^1.1.10: - version "1.1.10" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f" - integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A== - dependencies: - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" - for-each "^0.3.3" - gopd "^1.0.1" - has-tostringtag "^1.0.0" - is-typed-array@^1.1.13, is-typed-array@^1.1.14, is-typed-array@^1.1.15, is-typed-array@^1.1.3: version "1.1.15" resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" @@ -5294,11 +4570,6 @@ is-utf8@^0.2.0: resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" integrity sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q== -is-weakmap@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" - integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== - is-weakmap@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" @@ -5311,14 +4582,6 @@ is-weakref@^1.0.2, is-weakref@^1.1.0: dependencies: call-bound "^1.0.2" -is-weakset@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.2.tgz#4569d67a747a1ce5a994dfd4ef6dcea76e7c0a1d" - integrity sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.1" - is-weakset@^2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.4.tgz#c9f5deb0bc1906c6d6f1027f284ddf459249daca" @@ -5350,9 +4613,9 @@ isstream@~0.1.2: integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" - integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== + version "3.2.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" + integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== istanbul-lib-instrument@^5.0.4: version "5.2.1" @@ -5366,23 +4629,23 @@ istanbul-lib-instrument@^5.0.4: semver "^6.3.0" istanbul-lib-instrument@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.0.tgz#7a8af094cbfff1d5bb280f62ce043695ae8dd5b8" - integrity sha512-x58orMzEVfzPUKqlbLd1hXCnySCxKdDKa6Rjg97CwuLLRI4g3FHTdnExu1OqffVFay6zeMW+T6/DowFLndWnIw== + version "6.0.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz#fa15401df6c15874bcb2105f773325d78c666765" + integrity sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q== dependencies: - "@babel/core" "^7.12.3" - "@babel/parser" "^7.14.7" - "@istanbuljs/schema" "^0.1.2" + "@babel/core" "^7.23.9" + "@babel/parser" "^7.23.9" + "@istanbuljs/schema" "^0.1.3" istanbul-lib-coverage "^3.2.0" semver "^7.5.4" istanbul-lib-report@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" - integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== + version "3.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" + integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== dependencies: istanbul-lib-coverage "^3.0.0" - make-dir "^3.0.0" + make-dir "^4.0.0" supports-color "^7.1.0" istanbul-lib-source-maps@^4.0.0: @@ -5395,9 +4658,9 @@ istanbul-lib-source-maps@^4.0.0: source-map "^0.6.1" istanbul-reports@^3.1.3: - version "3.1.5" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.5.tgz#cc9a6ab25cb25659810e4785ed9d9fb742578bae" - integrity sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w== + version "3.1.7" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b" + integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g== dependencies: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" @@ -5848,11 +5111,6 @@ jsdom@^20.0.0: ws "^8.11.0" xml-name-validator "^4.0.0" -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== - jsesc@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" @@ -5888,7 +5146,7 @@ json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== -json5@^2.2.1, json5@^2.2.2, json5@^2.2.3: +json5@^2.2.2, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -5939,14 +5197,6 @@ leven@^3.1.0: resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== -levn@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== - dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" - lilconfig@^3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.3.tgz#a1bcfd6257f9585bf5ae14ceeebb7b559025e4c4" @@ -6102,17 +5352,12 @@ magic-string@^0.30.0, magic-string@^0.30.17: dependencies: "@jridgewell/sourcemap-codec" "^1.5.0" -make-dir@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== +make-dir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" + integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== dependencies: - semver "^6.0.0" - -make-error@^1.1.1: - version "1.3.6" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" - integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + semver "^7.5.3" makeerror@1.0.12: version "1.0.12" @@ -6148,15 +5393,7 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -micromatch@^4.0.4: - version "4.0.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== - dependencies: - braces "^3.0.2" - picomatch "^2.3.1" - -micromatch@^4.0.5: +micromatch@^4.0.4, micromatch@^4.0.5: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== @@ -6207,12 +5444,7 @@ minimatch@~3.0.3: dependencies: brace-expansion "^1.1.7" -minimist@^1.2.6: - version "1.2.7" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" - integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== - -minimist@^1.2.8: +minimist@^1.2.6, minimist@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -6227,11 +5459,6 @@ mlly@^1.7.3: pkg-types "^1.2.1" ufo "^1.5.4" -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" @@ -6253,11 +5480,11 @@ nanoid@^5.0.7: integrity sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q== nanospinner@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/nanospinner/-/nanospinner-1.1.0.tgz#d17ff621cb1784b0a206b400da88a0ef6db39b97" - integrity sha512-yFvNYMig4AthKYfHFl1sLj7B2nkHL4lzdig4osvl9/LdGbXwrdFRoqBS98gsEsOakr0yH+r5NZ/1Y9gdVB8trA== + version "1.2.2" + resolved "https://registry.yarnpkg.com/nanospinner/-/nanospinner-1.2.2.tgz#5a38f4410b5bf7a41585964bee74d32eab3e040b" + integrity sha512-Zt/AmG6qRU3e+WnzGGLuMCEAO/dAu45stNbHY223tUxldaDAeE+FxSPsd9Q+j+paejmm0ZbrNVs5Sraqy3dRxA== dependencies: - picocolors "^1.0.0" + picocolors "^1.1.1" natural-compare@^1.4.0: version "1.4.0" @@ -6284,11 +5511,6 @@ node-releases@^2.0.19: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== -node-releases@^2.0.6: - version "2.0.7" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.7.tgz#593edbc7c22860ee4d32d3933cfebdfab0c0e0e5" - integrity sha512-EJ3rzxL9pTWPjk5arA0s0dgXpnyiAbJDE6wHT62g7VsgrgQgmmZ+Ru++M1BFofncWja+Pnn3rEr3fieRySAdKQ== - normalize-package-data@^2.3.2: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -6312,9 +5534,9 @@ npm-run-path@^4.0.0, npm-run-path@^4.0.1: path-key "^3.0.0" nwsapi@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0" - integrity sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw== + version "2.2.16" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.16.tgz#177760bba02c351df1d2644e220c31dfec8cdb43" + integrity sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ== object-assign@^4.0.1: version "4.1.1" @@ -6326,34 +5548,11 @@ object-inspect@^1.13.3: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.3.tgz#f14c183de51130243d6d18ae149375ff50ea488a" integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA== -object-inspect@^1.9.0: - version "1.12.2" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" - integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== - -object-is@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" - integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object.assign@^4.1.4: - version "4.1.4" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" - integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - has-symbols "^1.0.3" - object-keys "^1.1.1" - object.assign@^4.1.7: version "4.1.7" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d" @@ -6389,18 +5588,6 @@ open@^8.0.4: is-docker "^2.1.1" is-wsl "^2.2.0" -optionator@^0.8.1: - version "0.8.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" - integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.6" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - word-wrap "~1.2.3" - ospath@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" @@ -6487,11 +5674,11 @@ parse-json@^5.2.0: lines-and-columns "^1.1.6" parse5@^7.0.0, parse5@^7.1.1: - version "7.1.2" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" - integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== + version "7.2.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.2.1.tgz#8928f55915e6125f430cc44309765bf17556a33a" + integrity sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ== dependencies: - entities "^4.4.0" + entities "^4.5.0" path-browserify@^1.0.1: version "1.0.1" @@ -6566,12 +5753,7 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== - -picocolors@^1.1.0, picocolors@^1.1.1: +picocolors@^1.0.0, picocolors@^1.1.0, picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== @@ -6604,9 +5786,9 @@ pinkie@^2.0.0: integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== pirates@^4.0.4: - version "4.0.5" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" - integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== + version "4.0.6" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" + integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== pkg-dir@^4.2.0: version "4.2.0" @@ -6652,11 +5834,6 @@ postcss@^8.4.49: picocolors "^1.1.1" source-map-js "^1.2.1" -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== - prettier@^3.4.2: version "3.4.2" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.4.2.tgz#a5ce1fb522a588bf2b78ca44c6e6fe5aa5a2b13f" @@ -6716,27 +5893,29 @@ pseudomap@^1.0.2: integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ== psl@^1.1.33: - version "1.9.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" - integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== + version "1.15.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.15.0.tgz#bdace31896f1d97cec6a79e8224898ce93d974c6" + integrity sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w== + dependencies: + punycode "^2.3.1" pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + version "3.0.2" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.2.tgz#836f3edd6bc2ee599256c924ffe0d88573ddcbf8" + integrity sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw== dependencies: end-of-stream "^1.1.0" once "^1.3.1" -punycode@^2.1.0, punycode@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== pure-rand@^6.0.0: - version "6.0.3" - resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.3.tgz#3c9e6b53c09e52ac3cedffc85ab7c1c7094b38cb" - integrity sha512-KddyFewCsO0j3+np81IQ+SweXLDnDQTs5s67BOnrYmYe/yNmUhttQyGsYzy8yUnoljGAQ9sl38YB4vH8ur7Y+w== + version "6.1.0" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" + integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== qs@6.13.1: version "6.13.1" @@ -6799,9 +5978,9 @@ react-is@^17.0.1: integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== react-is@^18.0.0: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" - integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" + integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== react-refresh@^0.14.2: version "0.14.2" @@ -6887,11 +6066,6 @@ regenerate@^1.4.2: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regenerator-runtime@^0.13.11: - version "0.13.11" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" - integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== - regenerator-runtime@^0.14.0: version "0.14.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" @@ -6904,15 +6078,6 @@ regenerator-transform@^0.15.2: dependencies: "@babel/runtime" "^7.8.4" -regexp.prototype.flags@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" - integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - functions-have-names "^1.2.2" - regexp.prototype.flags@^1.5.3: version "1.5.4" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz#1ad6c62d44a259007e55b3970e00f746efbcaa19" @@ -6984,11 +6149,11 @@ resolve-from@^5.0.0: integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== resolve.exports@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.0.tgz#c1a0028c2d166ec2fbf7d0644584927e76e7400e" - integrity sha512-6K/gDlqgQscOlg9fSRpWstA8sYe8rbELsSTNpx+3kTrsVCzvSl0zIvRErM7fdl9ERWDsKnrLnwB+Ne89918XOg== + version "2.0.3" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.3.tgz#41955e6f1b4013b7586f873749a635dea07ebe3f" + integrity sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A== -resolve@^1.10.0, resolve@^1.14.2, resolve@^1.22.1, resolve@^1.22.8, resolve@~1.22.1, resolve@~1.22.2: +resolve@^1.10.0, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.1, resolve@^1.22.8, resolve@~1.22.1, resolve@~1.22.2: version "1.22.10" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39" integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== @@ -6997,15 +6162,6 @@ resolve@^1.10.0, resolve@^1.14.2, resolve@^1.22.1, resolve@^1.22.8, resolve@~1.2 path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^1.20.0: - version "1.22.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" - integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== - dependencies: - is-core-module "^2.9.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - restore-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" @@ -7015,42 +6171,42 @@ restore-cursor@^3.1.0: signal-exit "^3.0.2" rfdc@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" - integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== + version "1.4.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" + integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== rollup@^4.23.0: - version "4.29.1" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.29.1.tgz#a9aaaece817e5f778489e5bf82e379cc8a5c05bc" - integrity sha512-RaJ45M/kmJUzSWDs1Nnd5DdV4eerC98idtUOVr6FfKcgxqvjwHmxc5upLF9qZU9EpsVzzhleFahrT3shLuJzIw== + version "4.30.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.30.0.tgz#44ae4260029a8362113ef2a0cee7e02f3f740274" + integrity sha512-sDnr1pcjTgUT69qBksNF1N1anwfbyYG6TBQ22b03bII8EdiUQ7J0TlozVaTMjT/eEJAO49e1ndV7t+UZfL1+vA== dependencies: "@types/estree" "1.0.6" optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.29.1" - "@rollup/rollup-android-arm64" "4.29.1" - "@rollup/rollup-darwin-arm64" "4.29.1" - "@rollup/rollup-darwin-x64" "4.29.1" - "@rollup/rollup-freebsd-arm64" "4.29.1" - "@rollup/rollup-freebsd-x64" "4.29.1" - "@rollup/rollup-linux-arm-gnueabihf" "4.29.1" - "@rollup/rollup-linux-arm-musleabihf" "4.29.1" - "@rollup/rollup-linux-arm64-gnu" "4.29.1" - "@rollup/rollup-linux-arm64-musl" "4.29.1" - "@rollup/rollup-linux-loongarch64-gnu" "4.29.1" - "@rollup/rollup-linux-powerpc64le-gnu" "4.29.1" - "@rollup/rollup-linux-riscv64-gnu" "4.29.1" - "@rollup/rollup-linux-s390x-gnu" "4.29.1" - "@rollup/rollup-linux-x64-gnu" "4.29.1" - "@rollup/rollup-linux-x64-musl" "4.29.1" - "@rollup/rollup-win32-arm64-msvc" "4.29.1" - "@rollup/rollup-win32-ia32-msvc" "4.29.1" - "@rollup/rollup-win32-x64-msvc" "4.29.1" + "@rollup/rollup-android-arm-eabi" "4.30.0" + "@rollup/rollup-android-arm64" "4.30.0" + "@rollup/rollup-darwin-arm64" "4.30.0" + "@rollup/rollup-darwin-x64" "4.30.0" + "@rollup/rollup-freebsd-arm64" "4.30.0" + "@rollup/rollup-freebsd-x64" "4.30.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.30.0" + "@rollup/rollup-linux-arm-musleabihf" "4.30.0" + "@rollup/rollup-linux-arm64-gnu" "4.30.0" + "@rollup/rollup-linux-arm64-musl" "4.30.0" + "@rollup/rollup-linux-loongarch64-gnu" "4.30.0" + "@rollup/rollup-linux-powerpc64le-gnu" "4.30.0" + "@rollup/rollup-linux-riscv64-gnu" "4.30.0" + "@rollup/rollup-linux-s390x-gnu" "4.30.0" + "@rollup/rollup-linux-x64-gnu" "4.30.0" + "@rollup/rollup-linux-x64-musl" "4.30.0" + "@rollup/rollup-win32-arm64-msvc" "4.30.0" + "@rollup/rollup-win32-ia32-msvc" "4.30.0" + "@rollup/rollup-win32-x64-msvc" "4.30.0" fsevents "~2.3.2" rxjs@^7.5.1: - version "7.6.0" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.6.0.tgz#361da5362b6ddaa691a2de0b4f2d32028f1eb5a2" - integrity sha512-DDa7d8TFNUalGC9VqXvQ1euWNN7sc63TrUCuM9J998+ViviahMIjKSOU7rfcgFOF+FCD71BhDRv4hrFz+ImDLQ== + version "7.8.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== dependencies: tslib "^2.1.0" @@ -7093,9 +6249,9 @@ safe-regex-test@^1.1.0: integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== sass@^1.83.0: - version "1.83.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.83.0.tgz#e36842c0b88a94ed336fd16249b878a0541d536f" - integrity sha512-qsSxlayzoOjdvXMVLkzF84DJFc2HZEL/rFyGIKbbilYtAvlCxyuzUeff9LawTn4btVnLKg75Z8MMr1lxU1lfGw== + version "1.83.1" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.83.1.tgz#dee1ab94b47a6f9993d3195d36f556bcbda64846" + integrity sha512-EVJbDaEs4Rr3F0glJzFSOvtg2/oy2V/YrGFPqPY24UqcLDWcI9ZY5sN+qyO3c/QCZwzgfirvhXvINiJCE/OLcA== dependencies: chokidar "^4.0.0" immutable "^5.0.2" @@ -7146,28 +6302,23 @@ schema-utils@^4.0.0, schema-utils@^4.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== -semver@^6.0.0, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -semver@^6.3.1: +semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.5.3, semver@^7.5.4, semver@~7.5.4: +semver@^7.5.3, semver@^7.5.4, semver@^7.6.2: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +semver@~7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" -semver@^7.6.2: - version "7.6.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" - integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== - serialize-javascript@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" @@ -7252,15 +6403,6 @@ side-channel-weakmap@^1.0.2: object-inspect "^1.13.3" side-channel-map "^1.0.1" -side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== - dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" - side-channel@^1.0.6, side-channel@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" @@ -7318,12 +6460,7 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" -"source-map-js@>=0.6.2 <2.0.0": - version "1.0.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" - integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== - -source-map-js@^1.2.0, source-map-js@^1.2.1: +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.0, source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== @@ -7547,13 +6684,6 @@ supports-color@^2.0.0: resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" integrity sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g== -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" @@ -7614,9 +6744,9 @@ test-exclude@^6.0.0: minimatch "^3.0.4" throttleit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" - integrity sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g== + version "1.0.1" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.1.tgz#304ec51631c3b770c65c6c6f76938b384000f4d5" + integrity sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ== through@2, through@^2.3.8, through@~2.3, through@~2.3.1: version "2.3.8" @@ -7646,17 +6776,17 @@ tinyspy@^3.0.0: resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a" integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q== -tldts-core@^6.1.70: - version "6.1.70" - resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-6.1.70.tgz#a954e93237ece2e1705b438600793c86a25f8c00" - integrity sha512-RNnIXDB1FD4T9cpQRErEqw6ZpjLlGdMOitdV+0xtbsnwr4YFka1zpc7D4KD+aAn8oSG5JyFrdasZTE04qDE9Yg== +tldts-core@^6.1.71: + version "6.1.71" + resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-6.1.71.tgz#04069cbdcf75b7fcb68fb4c1e00591cd3a2d4a5c" + integrity sha512-LRbChn2YRpic1KxY+ldL1pGXN/oVvKfCVufwfVzEQdFYNo39uF7AJa/WXdo+gYO7PTvdfkCPCed6Hkvz/kR7jg== tldts@^6.1.32: - version "6.1.70" - resolved "https://registry.yarnpkg.com/tldts/-/tldts-6.1.70.tgz#b571e5645ab9dc6f289453115d52602b8a384cfe" - integrity sha512-/W1YVgYVJd9ZDjey5NXadNh0mJXkiUMUue9Zebd0vpdo1sU+H4zFFTaJ1RKD4N6KFoHfcXy6l+Vu7bh+bdWCzA== + version "6.1.71" + resolved "https://registry.yarnpkg.com/tldts/-/tldts-6.1.71.tgz#e0db0853dd533628729d6a97a211450205fa21e4" + integrity sha512-LQIHmHnuzfZgZWAf2HzL83TIIrD8NhhI0DVxqo9/FdOd4ilec+NTNZOlDZf7EwrTNoutccbsHjvWHYXLAtvxjw== dependencies: - tldts-core "^6.1.70" + tldts-core "^6.1.71" tmp@~0.2.3: version "0.2.3" @@ -7668,11 +6798,6 @@ tmpl@1.0.5: resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== - to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -7681,9 +6806,9 @@ to-regex-range@^5.0.1: is-number "^7.0.0" tough-cookie@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874" - integrity sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ== + version "4.1.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" + integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag== dependencies: psl "^1.1.33" punycode "^2.1.1" @@ -7709,25 +6834,6 @@ ts-dedent@^2.0.0, ts-dedent@^2.2.0: resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5" integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ== -ts-node@^10.9.2: - version "10.9.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" - integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== - dependencies: - "@cspotcode/source-map-support" "^0.8.0" - "@tsconfig/node10" "^1.0.7" - "@tsconfig/node12" "^1.0.7" - "@tsconfig/node14" "^1.0.0" - "@tsconfig/node16" "^1.0.2" - acorn "^8.4.1" - acorn-walk "^8.1.1" - arg "^4.1.0" - create-require "^1.1.0" - diff "^4.0.1" - make-error "^1.1.1" - v8-compile-cache-lib "^3.0.1" - yn "3.1.1" - tsconfig-paths@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" @@ -7737,16 +6843,11 @@ tsconfig-paths@^4.2.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^2.0.1: +tslib@^2.0.1, tslib@^2.1.0: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== -tslib@^2.1.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" - integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== - tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" @@ -7759,13 +6860,6 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== - dependencies: - prelude-ls "~1.1.2" - type-detect@4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" @@ -7890,9 +6984,9 @@ universalify@^0.2.0: integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== unplugin@^1.3.1: version "1.16.0" @@ -7907,14 +7001,6 @@ untildify@^4.0.0: resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== -update-browserslist-db@^1.0.9: - version "1.0.10" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" - integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== - dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" - update-browserslist-db@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5" @@ -7959,19 +7045,14 @@ uuid@^9.0.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== -v8-compile-cache-lib@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" - integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== - v8-to-istanbul@^9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz#b6f994b0b5d4ef255e17a0d17dc444a9f5132fa4" - integrity sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w== + version "9.3.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz#b9572abfa62bd556c16d75fdebc1a411d5ff3175" + integrity sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA== dependencies: "@jridgewell/trace-mapping" "^0.3.12" "@types/istanbul-lib-coverage" "^2.0.1" - convert-source-map "^1.6.0" + convert-source-map "^2.0.0" validate-npm-package-license@^3.0.1: version "3.0.4" @@ -8107,17 +7188,6 @@ whatwg-url@^11.0.0: tr46 "^3.0.0" webidl-conversions "^7.0.0" -which-boxed-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" - integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== - dependencies: - is-bigint "^1.0.1" - is-boolean-object "^1.1.0" - is-number-object "^1.0.4" - is-string "^1.0.5" - is-symbol "^1.0.3" - which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e" @@ -8148,16 +7218,6 @@ which-builtin-type@^1.2.1: which-collection "^1.0.2" which-typed-array "^1.1.16" -which-collection@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" - integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== - dependencies: - is-map "^2.0.1" - is-set "^2.0.1" - is-weakmap "^2.0.1" - is-weakset "^2.0.1" - which-collection@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" @@ -8180,18 +7240,6 @@ which-typed-array@^1.1.16, which-typed-array@^1.1.18, which-typed-array@^1.1.2: gopd "^1.2.0" has-tostringtag "^1.0.2" -which-typed-array@^1.1.8: - version "1.1.9" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" - integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== - dependencies: - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" - for-each "^0.3.3" - gopd "^1.0.1" - has-tostringtag "^1.0.0" - is-typed-array "^1.1.10" - which@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" @@ -8206,11 +7254,6 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -word-wrap@~1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== - wrap-ansi@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" @@ -8242,12 +7285,7 @@ write-file-atomic@^4.0.2: imurmurhash "^0.1.4" signal-exit "^3.0.7" -ws@^8.11.0: - version "8.11.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" - integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== - -ws@^8.2.3: +ws@^8.11.0, ws@^8.2.3: version "8.18.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== @@ -8288,9 +7326,9 @@ yargs-parser@^21.1.1: integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== yargs@^17.3.1: - version "17.6.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.2.tgz#2e23f2944e976339a1ee00f18c77fedee8332541" - integrity sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw== + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== dependencies: cliui "^8.0.1" escalade "^3.1.1" @@ -8331,11 +7369,6 @@ yauzl@^2.10.0: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" -yn@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" - integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== - yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" From b78a0a8774b79396f3346339959f6f938c416e7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Tue, 7 Jan 2025 08:53:57 +0100 Subject: [PATCH 08/13] update readme --- CONTRIBUTING.md | 74 +++++++++++ README.md | 190 ++++++++++++----------------- demo.gif | Bin 0 -> 60218 bytes package.json | 7 -- shell.nix | 9 +- src/pin-field/pin-field-v2.e2e.tsx | 2 +- yarn.lock | 5 - 7 files changed, 155 insertions(+), 132 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 demo.gif diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..693d02a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,74 @@ +# Contributing guide + +Thank you for investing your time in contributing to React PIN Field! + +## Development + +The development environment is managed by [Nix](https://nixos.org/download.html). +Running `nix-shell` will spawn a shell with everything you need to get started with the lib. + +If you do not want to use Nix, you can just install manually the following dependencies: + +- [Node.js](https://nodejs.org/en) (`v20.18`) +- [Yarn](https://yarnpkg.com/) (`v1.22`) +- [Cypress](https://www.cypress.io/) (`v13.13.2`) + +## Installation + +``` +yarn +``` + +## Usage + +To run the demo locally, on a random port: + +``` +yarn start +``` + +To run the demo locally, on a custom port: + +``` +yarn start -p 3000 +``` + +To build the demo: + +``` +yarn storybook:build +``` + +To build the lib: + +``` +yarn build +``` + +## Unit tests + +Unit tests are handled by [Jest](https://jestjs.io/) (`.test` files). + +``` +yarn test:unit +``` + +## End-to-end tests + +End-to-end tests are handled by [Cypress](https://www.cypress.io) (`.e2e` files). + +You need first to start a Storybook locally, on the port `3000`: + +``` +yarn start -p 3000 +``` + +Then in another terminal: + +``` +yarn test:e2e +``` + +## Commit style + +Starting from the `v4.0.0`, React PIN Field tries to adopt the [conventional commits specification](https://github.com/conventional-commits/conventionalcommits.org). diff --git a/README.md b/README.md index 984008e..4719780 100644 --- a/README.md +++ b/README.md @@ -1,167 +1,131 @@ -# 📟 React PIN Field [![tests](https://img.shields.io/github/actions/workflow/status/soywod/react-pin-field/tests.yml?branch=master&label=tests&logo=github&style=flat-square)](https://github.com/soywod/react-pin-field/actions/workflows/test.yml) [![codecov](https://img.shields.io/codecov/c/github/soywod/react-pin-field?logo=codecov&style=flat-square)](https://app.codecov.io/gh/soywod/react-pin-field) [![npm](https://img.shields.io/npm/v/react-pin-field?logo=npm&label=npm&color=success&style=flat-square)](https://www.npmjs.com/package/react-pin-field) +# 📟 React PIN Field [![tests](https://img.shields.io/github/actions/workflow/status/soywod/react-pin-field/tests.yml?branch=master&label=tests&logo=github)](https://github.com/soywod/react-pin-field/actions/workflows/test.yml) [![codecov](https://img.shields.io/codecov/c/github/soywod/react-pin-field?logo=codecov)](https://app.codecov.io/gh/soywod/react-pin-field) [![npm](https://img.shields.io/npm/v/react-pin-field?logo=npm&label=npm&color=success)](https://www.npmjs.com/package/react-pin-field) -React component for entering PIN codes. +React component for entering PIN codes -![gif](https://user-images.githubusercontent.com/10437171/70847884-f9d35f00-1e69-11ea-8152-1c70eda12137.gif) +![demo](demo.gif) -_Live demo at https://soywod.github.io/react-pin-field/._ +*Live demo available at .* + +## Features + +- Written in TypeScript, tested with Jest and Cypress +- Relies on `onchange` native event to improve browsers compatibility +- Supports HTML [`dir`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/dir) left-to-right and right-to-left +- Supports HTML [`autofocus`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autofocus) by focusing either the first or the last input, depending on [`dir`] +- Supports [ARIA](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA) attributes +- Handles [key composition](https://developer.mozilla.org/en-US/docs/Web/API/Element/compositionstart_event) ## Installation -```bash -yarn add react-pin-field -# or +### Using npm + +``` npm install react-pin-field ``` +### Using yarn + +``` +yarn add react-pin-field +``` + ## Usage ```typescript import PinField from "react-pin-field"; ``` -## Props +### Props ```typescript -type PinFieldProps = { - ref?: React.Ref; - className?: string; +// React PIN Field inherits native props from HTMLInputElement, +// except few event handlers that are overriden: +type NativeProps = Omit< + InputHTMLAttributes, + "onChange" | "onKeyDown" | "onCompositionStart" | "onCompositionEnd" +>; + +type Props = NativeProps & { length?: number; - validate?: string | string[] | RegExp | ((key: string) => boolean); format?: (char: string) => string; - onResolveKey?: (key: string, ref?: HTMLInputElement) => any; - onRejectKey?: (key: string, ref?: HTMLInputElement) => any; - onChange?: (code: string) => void; - onComplete?: (code: string) => void; - style?: React.CSSProperties; -} & React.InputHTMLAttributes; - -const defaultProps = { - ref: {current: []}, - className: "", - length: 5, - validate: /^[a-zA-Z0-9]$/, - format: key => key, - formatAriaLabel: (idx, length) => `pin code ${idx} of ${length}`, - onResolveKey: () => {}, - onRejectKey: () => {}, - onChange: () => {}, - onComplete: () => {}, - style: {}, + formatAriaLabel?: (index: number, total: number) => string; + onChange?: (value: string) => void; + onComplete?: (value: string) => void; }; ``` -### Reference +#### Props.length -Every input can be controlled thanks to the React reference: - -```typescript -; +The length of the PIN field, which represents the number of inputs. -// reset all inputs -ref.current.forEach(input => (input.value = "")); +*Defaults to `5`* -// focus the third input -ref.current[2].focus(); -``` +#### Props.format -### Style +Characters can be formatted with any function of type `(char: string) => string`. -The pin field can be styled either with `style` or `className`. This -last one allows you to use pseudo-classes like `:nth-of-type`, -`:focus`, `:hover`, `:valid`, `:invalid`… +*Defaults to identity function `char => char`* -### Length +#### Props.formatAriaLabel -Length of the code (number of characters). +This function is used to generate accessible labels for each input within the ``. By default it renders the string `PIN field 1 of 6`, `PIN field 2 of 6`, etc., depending on the actual index of the input field and the total length of the pin field. -### Validate +You can customize the `aria-label` string by passing your own function. This can be useful for: i) site internationalisation (i18n); ii) simply describing each input with different semantics than the ones provided by `react-pin-field`. -Characters can be validated with a validator. A validator can take the -form of: +*Defaults to `(n, total) => "PIN field ${n} of ${total}"`* -- a String of allowed characters: `abcABC123` -- an Array of allowed characters: `["a", "b", "c", "1", "2", "3"]` -- a RegExp: `/^[a-zA-Z0-9]$/` -- a predicate: `(char: string) => boolean` +#### Props.onChange -### Format +This function is called everytime the PIN field changes its value. -Characters can be formatted with a formatter `(char: string) => string`. +#### Props.onComplete -### Format Aria Label(s) +This function is called everytime the PIN field is completed. A PIN field is considered complete when: -This function is used to generate accessible labels for each input within the -``. By default it renders the string `pin code 1 of 6`, -`pin code 2 of 6`, etc., depending on the actual index of the input field -and the total length of the pin field. +- Every input has a defined value +- Every input passed the standard HTML validation (`required`, `pattern` etc) -You can customize the aria-label string by passing your own function. This can -be useful for: i) site internationalisation (i18n); ii) simply describing -each input with different semantics than the ones provided by `react-pin-field`. +#### Reference -### Events +React PIN Field exposes a special reference which is an array of `HTMLInputElement`: -- `onResolveKey`: when a key passes the validator -- `onRejectKey`: when a key is rejected by the validator -- `onChange`: when the code changes -- `onComplete`: when the code has been fully filled +```tsx +const ref = useRef(); -## Examples - -See the [live demo](https://soywod.github.io/react-pin-field/). - -## Development - -```bash -git clone https://github.com/soywod/react-pin-field.git -cd react-pin-field -yarn install -``` - -To start the development server: - -```bash -yarn start -``` - -To build the lib: +; -```bash -yarn build +// focus the third input +ref.current[2].focus(); ``` -To build the demo: +#### Style -```bash -yarn build:demo -``` +React PIN Field can be styled either with `style` or `className`. This last one allows you to use pseudo-classes like `:nth-of-type`, `:focus`, `:hover`, `:valid`, `:invalid`… -## Tests +### Hook -### Unit tests +By default, React PIN Field is an [uncontrolled](https://react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components) component, which means that its internal state cannot be changed. You can only listen to changes via `onChange` and `onComplete`. -Unit tests are handled by [Jest](https://jestjs.io/) (`.test` files) -and [Enzyme](https://airbnb.io/enzyme/) (`.spec` files). +To control the React PIN Field state, you can use the `usePinField()` custom hook: -```bash -yarn test:unit -``` - -### End-to-end tests +```tsx +const handler = usePinField(); -End-to-end tests are handled by [Cypress](https://www.cypress.io) -(`.e2e` files). +// The handler exposes a value and setValue to control the PIN code, +// as well as a state and dispatch for advanced usage. -```bash -yarn start -yarn test:e2e +// Let know the PIN field that you want to use this custom state +// instead of its internal one. +return ``` ## Sponsoring -[![github](https://img.shields.io/badge/-GitHub%20Sponsors-fafbfc?logo=GitHub%20Sponsors&style=flat-square)](https://github.com/sponsors/soywod) -[![paypal](https://img.shields.io/badge/-PayPal-0079c1?logo=PayPal&logoColor=ffffff&style=flat-square)](https://www.paypal.com/paypalme/soywod) -[![ko-fi](https://img.shields.io/badge/-Ko--fi-ff5e5a?logo=Ko-fi&logoColor=ffffff&style=flat-square)](https://ko-fi.com/soywod) -[![buy-me-a-coffee](https://img.shields.io/badge/-Buy%20Me%20a%20Coffee-ffdd00?logo=Buy%20Me%20A%20Coffee&logoColor=000000&style=flat-square)](https://www.buymeacoffee.com/soywod) -[![liberapay](https://img.shields.io/badge/-Liberapay-f6c915?logo=Liberapay&logoColor=222222&style=flat-square)](https://liberapay.com/soywod) +If you appreciate the project, feel free to donate using one of the following providers: + +[![GitHub](https://img.shields.io/badge/-GitHub%20Sponsors-fafbfc?logo=GitHub%20Sponsors)](https://github.com/sponsors/soywod) +[![Ko-fi](https://img.shields.io/badge/-Ko--fi-ff5e5a?logo=Ko-fi&logoColor=ffffff)](https://ko-fi.com/soywod) +[![Buy Me a Coffee](https://img.shields.io/badge/-Buy%20Me%20a%20Coffee-ffdd00?logo=Buy%20Me%20A%20Coffee&logoColor=000000)](https://www.buymeacoffee.com/soywod) +[![Liberapay](https://img.shields.io/badge/-Liberapay-f6c915?logo=Liberapay&logoColor=222222)](https://liberapay.com/soywod) +[![thanks.dev](https://img.shields.io/badge/-thanks.dev-000000?logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQuMDk3IiBoZWlnaHQ9IjE3LjU5NyIgY2xhc3M9InctMzYgbWwtMiBsZzpteC0wIHByaW50Om14LTAgcHJpbnQ6aW52ZXJ0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik05Ljc4MyAxNy41OTdINy4zOThjLTEuMTY4IDAtMi4wOTItLjI5Ny0yLjc3My0uODktLjY4LS41OTMtMS4wMi0xLjQ2Mi0xLjAyLTIuNjA2di0xLjM0NmMwLTEuMDE4LS4yMjctMS43NS0uNjc4LTIuMTk1LS40NTItLjQ0Ni0xLjIzMi0uNjY5LTIuMzQtLjY2OUgwVjcuNzA1aC41ODdjMS4xMDggMCAxLjg4OC0uMjIyIDIuMzQtLjY2OC40NTEtLjQ0Ni42NzctMS4xNzcuNjc3LTIuMTk1VjMuNDk2YzAtMS4xNDQuMzQtMi4wMTMgMS4wMjEtMi42MDZDNS4zMDUuMjk3IDYuMjMgMCA3LjM5OCAwaDIuMzg1djEuOTg3aC0uOTg1Yy0uMzYxIDAtLjY4OC4wMjctLjk4LjA4MmExLjcxOSAxLjcxOSAwIDAgMC0uNzM2LjMwN2MtLjIwNS4xNTYtLjM1OC4zODQtLjQ2LjY4Mi0uMTAzLjI5OC0uMTU0LjY4Mi0uMTU0IDEuMTUxVjUuMjNjMCAuODY3LS4yNDkgMS41ODYtLjc0NSAyLjE1NS0uNDk3LjU2OS0xLjE1OCAxLjAwNC0xLjk4MyAxLjMwNXYuMjE3Yy44MjUuMyAxLjQ4Ni43MzYgMS45ODMgMS4zMDUuNDk2LjU3Ljc0NSAxLjI4Ny43NDUgMi4xNTR2MS4wMjFjMCAuNDcuMDUxLjg1NC4xNTMgMS4xNTIuMTAzLjI5OC4yNTYuNTI1LjQ2MS42ODIuMTkzLjE1Ny40MzcuMjYuNzMyLjMxMi4yOTUuMDUuNjIzLjA3Ni45ODQuMDc2aC45ODVabTE0LjMxNC03LjcwNmgtLjU4OGMtMS4xMDggMC0xLjg4OC4yMjMtMi4zNC42NjktLjQ1LjQ0NS0uNjc3IDEuMTc3LS42NzcgMi4xOTVWMTQuMWMwIDEuMTQ0LS4zNCAyLjAxMy0xLjAyIDIuNjA2LS42OC41OTMtMS42MDUuODktMi43NzQuODloLTIuMzg0di0xLjk4OGguOTg0Yy4zNjIgMCAuNjg4LS4wMjcuOTgtLjA4LjI5Mi0uMDU1LjUzOC0uMTU3LjczNy0uMzA4LjIwNC0uMTU3LjM1OC0uMzg0LjQ2LS42ODIuMTAzLS4yOTguMTU0LS42ODIuMTU0LTEuMTUydi0xLjAyYzAtLjg2OC4yNDgtMS41ODYuNzQ1LTIuMTU1LjQ5Ny0uNTcgMS4xNTgtMS4wMDQgMS45ODMtMS4zMDV2LS4yMTdjLS44MjUtLjMwMS0xLjQ4Ni0uNzM2LTEuOTgzLTEuMzA1LS40OTctLjU3LS43NDUtMS4yODgtLjc0NS0yLjE1NXYtMS4wMmMwLS40Ny0uMDUxLS44NTQtLjE1NC0xLjE1Mi0uMTAyLS4yOTgtLjI1Ni0uNTI2LS40Ni0uNjgyYTEuNzE5IDEuNzE5IDAgMCAwLS43MzctLjMwNyA1LjM5NSA1LjM5NSAwIDAgMC0uOTgtLjA4MmgtLjk4NFYwaDIuMzg0YzEuMTY5IDAgMi4wOTMuMjk3IDIuNzc0Ljg5LjY4LjU5MyAxLjAyIDEuNDYyIDEuMDIgMi42MDZ2MS4zNDZjMCAxLjAxOC4yMjYgMS43NS42NzggMi4xOTUuNDUxLjQ0NiAxLjIzMS42NjggMi4zNC42NjhoLjU4N3oiIGZpbGw9IiNmZmYiLz48L3N2Zz4=)](https://thanks.dev/soywod) +[![PayPal](https://img.shields.io/badge/-PayPal-0079c1?logo=PayPal&logoColor=ffffff)](https://www.paypal.com/paypalme/soywod) diff --git a/demo.gif b/demo.gif new file mode 100644 index 0000000000000000000000000000000000000000..c72f917c4d34579a87fd4c46d1a65d73149cd7ff GIT binary patch literal 60218 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 literal 0 HcmV?d00001 diff --git a/package.json b/package.json index aea1d35..ddf301b 100644 --- a/package.json +++ b/package.json @@ -34,12 +34,6 @@ "require": "./dist/react-pin-field.umd.cjs" } }, - "husky": { - "hooks": { - "pre-commit": "yarn lint", - "pre-push": "run-p lint test:unit" - } - }, "devDependencies": { "@babel/core": "^7.26.0", "@babel/preset-env": "^7.26.0", @@ -67,7 +61,6 @@ "babel-loader": "^9.2.1", "classnames": "^2.5.1", "cypress": "=13.13.2", - "husky": "^9.1.7", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "lodash": "^4.17.21", diff --git a/shell.nix b/shell.nix index 9f52fe5..dc4cd30 100644 --- a/shell.nix +++ b/shell.nix @@ -2,7 +2,7 @@ , pkgs ? import nixpkgs { inherit system; }, extraBuildInputs ? "" }: let - inherit (pkgs) cypress lib mkShell nodejs playwright playwright-driver; + inherit (pkgs) cypress lib mkShell nodejs; inherit (lib) attrVals getExe optionals splitString; yarn = pkgs.yarn.override { inherit nodejs; }; @@ -10,16 +10,13 @@ let (attrVals (splitString "," extraBuildInputs) pkgs); in mkShell { - buildInputs = [ nodejs yarn cypress playwright playwright-driver.browsers ] - ++ extraBuildInputs'; + buildInputs = [ nodejs yarn cypress ] ++ extraBuildInputs'; + shellHook = '' # configure cypress export CYPRESS_RUN_BINARY="${getExe cypress}" # add node_modules/.bin to path export PATH="$PWD/node_modules/.bin/:$PATH" - - export PLAYWRIGHT_BROWSERS_PATH=${playwright-driver.browsers} - export PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS=true ''; } diff --git a/src/pin-field/pin-field-v2.e2e.tsx b/src/pin-field/pin-field-v2.e2e.tsx index 2fade56..639718f 100644 --- a/src/pin-field/pin-field-v2.e2e.tsx +++ b/src/pin-field/pin-field-v2.e2e.tsx @@ -26,7 +26,7 @@ describe("PIN Field", () => { cy.get(nthInput(5)).should("be.focused").should("have.value", "f"); }); - it.only("should remove values on backspace or delete", () => { + it.only("should remove values on backspace", () => { cy.get(nthInput(1)).focus(); cy.focused().type("abc{backspace}"); // A second backspace is needed due to event.isTrusted = false. diff --git a/yarn.lock b/yarn.lock index 1125ba7..bb38952 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4269,11 +4269,6 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== -husky@^9.1.7: - version "9.1.7" - resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.7.tgz#d46a38035d101b46a70456a850ff4201344c0b2d" - integrity sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA== - iconv-lite@0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" From e121298c588446790509556feb670dc21f4eb1fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Tue, 7 Jan 2025 09:29:37 +0100 Subject: [PATCH 09/13] replace v2 component --- .github/FUNDING.yml | 6 + cypress.config.ts => cypress.config.js | 8 +- eslint.config.js | 51 -- jest.config.json | 2 +- src/pin-field/index.ts | 1 - src/pin-field/pin-field-v2.spec.tsx | 178 ----- src/pin-field/pin-field-v2.tsx | 442 ------------ ...{pin-field-v2.e2e.tsx => pin-field.e2e.ts} | 2 +- src/pin-field/pin-field.spec.tsx | 148 ---- ...v2.stories.scss => pin-field.stories.scss} | 0 ...d-v2.stories.tsx => pin-field.stories.tsx} | 25 +- src/pin-field/pin-field.test.ts | 587 ---------------- ...in-field-v2.test.ts => pin-field.test.tsx} | 183 ++++- src/pin-field/pin-field.tsx | 656 ++++++++++-------- src/pin-field/pin-field.types.ts | 45 -- src/pin-field/use-bireducer.spec.tsx | 109 --- src/pin-field/use-bireducer.ts | 47 -- src/polyfills/index.ts | 1 - src/polyfills/keyboard-evt.test.ts | 17 - src/polyfills/keyboard-evt.ts | 122 ---- src/utils/utils.test.ts | 9 +- src/utils/utils.ts | 12 - vite.config.ts => vite.config.js | 6 +- 23 files changed, 590 insertions(+), 2067 deletions(-) rename cypress.config.ts => cypress.config.js (73%) delete mode 100644 eslint.config.js delete mode 100644 src/pin-field/pin-field-v2.spec.tsx delete mode 100644 src/pin-field/pin-field-v2.tsx rename src/pin-field/{pin-field-v2.e2e.tsx => pin-field.e2e.ts} (97%) delete mode 100644 src/pin-field/pin-field.spec.tsx rename src/pin-field/{pin-field-v2.stories.scss => pin-field.stories.scss} (100%) rename src/pin-field/{pin-field-v2.stories.tsx => pin-field.stories.tsx} (85%) delete mode 100644 src/pin-field/pin-field.test.ts rename src/pin-field/{pin-field-v2.test.ts => pin-field.test.tsx} (74%) delete mode 100644 src/pin-field/pin-field.types.ts delete mode 100644 src/pin-field/use-bireducer.spec.tsx delete mode 100644 src/pin-field/use-bireducer.ts delete mode 100644 src/polyfills/index.ts delete mode 100644 src/polyfills/keyboard-evt.test.ts delete mode 100644 src/polyfills/keyboard-evt.ts rename vite.config.ts => vite.config.js (86%) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index e8184f2..f1b25f3 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,7 @@ github: soywod +ko_fi: soywod +buy_me_a_coffee: soywod +liberapay: soywod +thanks_dev: soywod +custom: https://www.paypal.com/paypalme/soywod + diff --git a/cypress.config.ts b/cypress.config.js similarity index 73% rename from cypress.config.ts rename to cypress.config.js index d6c8c11..f79e5cc 100644 --- a/cypress.config.ts +++ b/cypress.config.js @@ -1,6 +1,6 @@ -import "cypress"; +import { defineConfig } from "cypress"; -export default { +export default defineConfig({ experimentalWebKitSupport: true, fixturesFolder: false, video: false, @@ -14,7 +14,7 @@ export default { }); }, baseUrl: "http://localhost:3000", - specPattern: "src/**/*.e2e.tsx", + specPattern: "src/**/*.e2e.ts", supportFile: false, }, -} satisfies Cypress.ConfigOptions; +}); diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index ae3a0b9..0000000 --- a/eslint.config.js +++ /dev/null @@ -1,51 +0,0 @@ -module.exports = { - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "prettier", - "prettier/@typescript-eslint", - ], - plugins: ["@typescript-eslint"], - parser: "@typescript-eslint/parser", - parserOptions: { - ecmaVersion: 8, - }, - env: { - es6: true, - browser: true, - jest: true, - node: true, - }, - settings: { - react: { - version: "detect", - }, - "import/resolver": { - node: { - extensions: [".js", ".jsx", ".ts", ".tsx"], - }, - }, - }, - rules: { - "prefer-const": 0, - "no-console": 0, - "@typescript-eslint/array-type": "off", - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/no-object-literal-type-assertion": "off", - "@typescript-eslint/no-use-before-define": "off", - "@typescript-eslint/no-unused-vars": [ - "error", - { - vars: "all", - args: "after-used", - ignoreRestSiblings: true, - varsIgnorePattern: "^_", - }, - ], - "@typescript-eslint/prefer-interface": "off", - }, -}; diff --git a/jest.config.json b/jest.config.json index 7ea1e39..b6219d4 100644 --- a/jest.config.json +++ b/jest.config.json @@ -1,5 +1,5 @@ { "collectCoverage": true, "testEnvironment": "jest-environment-jsdom", - "testRegex": ".(test|spec).tsx?$" + "testRegex": ".test.tsx?$" } diff --git a/src/pin-field/index.ts b/src/pin-field/index.ts index e47494d..2e2cb47 100644 --- a/src/pin-field/index.ts +++ b/src/pin-field/index.ts @@ -1,2 +1 @@ export * from "./pin-field"; -export * from "./pin-field.types"; diff --git a/src/pin-field/pin-field-v2.spec.tsx b/src/pin-field/pin-field-v2.spec.tsx deleted file mode 100644 index a90dc42..0000000 --- a/src/pin-field/pin-field-v2.spec.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import "@testing-library/jest-dom"; - -import { RefObject } from "react"; -import { createEvent, fireEvent, render, screen } from "@testing-library/react"; - -import PinField from "./pin-field-v2"; - -test("default", async () => { - render(); - - const inputs = await screen.findAllByRole("textbox"); - expect(inputs).toHaveLength(5); - - inputs.forEach(input => { - expect(input.getAttribute("type")).toBe("text"); - expect(input.getAttribute("inputmode")).toBe("text"); - expect(input.getAttribute("autocapitalize")).toBe("off"); - expect(input.getAttribute("autocorrect")).toBe("off"); - expect(input.getAttribute("autocomplete")).toBe("off"); - }); -}); - -test("forward ref as object", () => { - const ref: RefObject = { current: [] }; - render(); - - expect(Array.isArray(ref.current)).toBe(true); - expect(ref.current).toHaveLength(5); -}); - -test("forward ref as func", () => { - const ref: RefObject = { current: [] }; - render( - { - if (el) { - ref.current = el; - } - }} - />, - ); - - expect(Array.isArray(ref.current)).toBe(true); - expect(ref.current).toHaveLength(5); -}); - -test("className", async () => { - render(); - const inputs = await screen.findAllByRole("textbox"); - - inputs.forEach(input => { - expect(input.className).toBe("custom-class-name"); - }); -}); - -test("style", async () => { - render(); - const inputs = await screen.findAllByRole("textbox"); - - inputs.forEach(input => { - expect(input.style.position).toBe("absolute"); - }); -}); - -test("autoFocus", async () => { - render(); - const inputs = await screen.findAllByRole("textbox"); - - inputs.forEach((input, index) => { - if (index === 0) { - expect(input).toHaveFocus(); - } else { - expect(input).not.toHaveFocus(); - } - }); -}); - -test("autoFocus rtl", async () => { - render(); - const inputs = await screen.findAllByRole("textbox"); - - inputs.forEach((input, index) => { - if (index === 4) { - expect(input).toHaveFocus(); - } else { - expect(input).not.toHaveFocus(); - } - }); -}); - -describe("events", () => { - test("change single input", async () => { - const handleChangeMock = jest.fn(); - const handleCompleteMock = jest.fn(); - render(); - - const inputs = (await screen.findAllByRole("textbox")) as HTMLInputElement[]; - - fireEvent.change(inputs[0], { target: { value: "a" } }); - expect(handleChangeMock).toHaveBeenCalledWith("a"); - expect(inputs[0].value).toBe("a"); - - fireEvent.change(inputs[1], { target: { value: "b" } }); - expect(handleChangeMock).toHaveBeenCalledWith("ab"); - expect(inputs[0].value).toBe("a"); - expect(inputs[1].value).toBe("b"); - - fireEvent.change(inputs[2], { target: { value: "c" } }); - expect(handleChangeMock).toHaveBeenCalledWith("abc"); - expect(inputs[0].value).toBe("a"); - expect(inputs[1].value).toBe("b"); - expect(inputs[2].value).toBe("c"); - - fireEvent.change(inputs[3], { target: { value: "d" } }); - expect(handleChangeMock).toHaveBeenCalledWith("abcd"); - expect(handleCompleteMock).toHaveBeenCalledWith("abcd"); - expect(inputs[0].value).toBe("a"); - expect(inputs[1].value).toBe("b"); - expect(inputs[2].value).toBe("c"); - expect(inputs[3].value).toBe("d"); - }); - - test("change multi input", async () => { - const handleChangeMock = jest.fn(); - const handleCompleteMock = jest.fn(); - render(); - - const inputs = (await screen.findAllByRole("textbox")) as HTMLInputElement[]; - - fireEvent.change(inputs[0], { target: { value: "abc" } }); - expect(handleChangeMock).toHaveBeenLastCalledWith("abc"); - expect(inputs[0].value).toBe("a"); - expect(inputs[1].value).toBe("b"); - expect(inputs[2].value).toBe("c"); - - fireEvent.change(inputs[1], { target: { value: "def" } }); - expect(handleChangeMock).toHaveBeenLastCalledWith("adef"); - expect(handleCompleteMock).toHaveBeenCalledWith("adef"); - expect(inputs[0].value).toBe("a"); - expect(inputs[1].value).toBe("d"); - expect(inputs[2].value).toBe("e"); - expect(inputs[3].value).toBe("f"); - }); -}); - -describe("a11y", () => { - test("default aria-label", () => { - render(); - - expect(screen.getByRole("textbox", { name: "PIN field 1 of 3" })).toBeVisible(); - expect(screen.getByRole("textbox", { name: "PIN field 2 of 3" })).toBeVisible(); - expect(screen.getByRole("textbox", { name: "PIN field 3 of 3" })).toBeVisible(); - }); - - test("aria-required", () => { - render( `${i}`} required />); - - expect(screen.getByRole("textbox", { name: "1" })).toHaveAttribute("aria-required", "true"); - expect(screen.getByRole("textbox", { name: "2" })).toHaveAttribute("aria-required", "true"); - expect(screen.getByRole("textbox", { name: "3" })).toHaveAttribute("aria-required", "true"); - }); - - test("aria-disabled", () => { - render( `${i}`} disabled />); - - expect(screen.getByRole("textbox", { name: "1" })).toHaveAttribute("aria-disabled", "true"); - expect(screen.getByRole("textbox", { name: "2" })).toHaveAttribute("aria-disabled", "true"); - expect(screen.getByRole("textbox", { name: "3" })).toHaveAttribute("aria-disabled", "true"); - }); - - test("aria-readonly", () => { - render( `${i}`} readOnly />); - - expect(screen.getByRole("textbox", { name: "1" })).toHaveAttribute("aria-readonly", "true"); - expect(screen.getByRole("textbox", { name: "2" })).toHaveAttribute("aria-readonly", "true"); - expect(screen.getByRole("textbox", { name: "3" })).toHaveAttribute("aria-readonly", "true"); - }); -}); diff --git a/src/pin-field/pin-field-v2.tsx b/src/pin-field/pin-field-v2.tsx deleted file mode 100644 index 65d1468..0000000 --- a/src/pin-field/pin-field-v2.tsx +++ /dev/null @@ -1,442 +0,0 @@ -import { - InputHTMLAttributes, - useEffect, - useReducer, - useRef, - KeyboardEventHandler, - KeyboardEvent, - ChangeEventHandler, - useCallback, - CompositionEventHandler, - forwardRef, - useImperativeHandle, - RefObject, - ActionDispatch, - useMemo, -} from "react"; - -import { noop, range } from "../utils"; - -export const BACKSPACE = 8; -export const DELETE = 46; - -export type InnerProps = { - length: number; - format: (char: string) => string; - formatAriaLabel: (index: number, total: number) => string; - onChange: (value: string) => void; - onComplete: (value: string) => void; -}; - -export const defaultProps: InnerProps = { - length: 5, - format: char => char, - formatAriaLabel: (index: number, total: number) => `PIN field ${index} of ${total}`, - onChange: noop, - onComplete: noop, -}; - -export type NativeProps = Omit< - InputHTMLAttributes, - "onChange" | "onKeyDown" | "onCompositionStart" | "onCompositionEnd" ->; - -export const defaultNativeProps: NativeProps = { - type: "text", - inputMode: "text", - autoCapitalize: "off", - autoCorrect: "off", - autoComplete: "off", -}; - -export type Props = NativeProps & - Partial & { - handler?: Handler; - }; - -export type StateProps = Pick & Pick; - -export type State = StateProps & { - cursor: number; - values: string[]; - backspace: boolean; - composition: boolean; - ready: boolean; - dirty: boolean; -}; - -export const defaultState: State = { - length: defaultProps.length, - format: defaultProps.format, - dir: "ltr", - cursor: 0, - values: Array(defaultProps.length), - backspace: false, - composition: false, - ready: false, - dirty: false, -}; - -export type NoOpAction = { - type: "noop"; -}; - -export type UpdatePropsAction = { - type: "update-props"; - props: Partial; -}; - -export type HandleCompositionStartAction = { - type: "start-composition"; - index: number; -}; - -export type HandleCompositionEndAction = { - type: "end-composition"; - index: number; - value: string; -}; - -export type HandleKeyChangeAction = { - type: "handle-change"; - index: number; - value: string | null; - reset?: boolean; -}; - -export type HandleKeyDownAction = { - type: "handle-key-down"; - index: number; -} & Partial, "key" | "code" | "keyCode" | "which">>; - -export type Action = - | NoOpAction - | UpdatePropsAction - | HandleCompositionStartAction - | HandleCompositionEndAction - | HandleKeyChangeAction - | HandleKeyDownAction; - -export function reducer(prevState: State, action: Action): State { - switch (action.type) { - case "update-props": { - // merge previous state with action's props - const state = { ...prevState, ...action.props }; - - // adjust cursor in case the new length exceed the previous one - state.cursor = Math.min(state.cursor, state.length - 1); - - // slice values according to the new length - // - // NOTE: use slice because splice does not keep empty items and - // therefore messes up with values length - state.values = state.values.slice(0, state.cursor + 1); - - // state is now ready - state.ready = true; - - return state; - } - - case "start-composition": { - return { ...prevState, dirty: true, composition: true }; - } - - case "end-composition": { - const state: State = { ...prevState }; - - if (action.value) { - state.values[action.index] = action.value; - } else { - delete state.values[action.index]; - } - - const dir = state.values[action.index] ? 1 : 0; - state.cursor = Math.min(action.index + dir, state.length - 1); - - state.composition = false; - state.dirty = true; - - return state; - } - - case "handle-change": { - if (prevState.composition) { - break; - } - - const state: State = { ...prevState }; - - if (action.reset) { - state.values.splice(action.index, state.length); - } - - if (action.value) { - const values = action.value.split("").map(state.format); - const length = Math.min(state.length - action.index, values.length); - state.values.splice(action.index, length, ...values.slice(0, length)); - state.cursor = Math.min(action.index + length, state.length - 1); - } else { - delete state.values[action.index]; - const dir = state.backspace ? 0 : 1; - state.cursor = Math.max(0, action.index - dir); - } - - state.backspace = false; - state.dirty = true; - - return state; - } - - case "handle-key-down": { - // determine if a deletion key is pressed - const fromKey = action.key === "Backspace" || action.key === "Delete"; - const fromCode = action.code === "Backspace" || action.code === "Delete"; - const fromKeyCode = action.keyCode === BACKSPACE || action.keyCode === DELETE; - const fromWhich = action.which === BACKSPACE || action.which === DELETE; - const deletion = fromKey || fromCode || fromKeyCode || fromWhich; - - // return the same state reference if no deletion detected - if (!deletion) { - break; - } - - // Deletion is a bit tricky and requires special attention. - // - // When the field under cusor has a value and a deletion key is - // pressed, we want to let the browser to do the deletion for - // us, like a regular deletion in a normal input via the - // `onchange` event. For this to happen, we need to return the - // same state reference in order not to trigger any change. The - // state will be automatically updated by the handle-change - // action, when the deleted value will trigger the `onchange` - // event. - if (prevState.values[action.index]) { - break; - } - - // But when the field under cursor is empty, deletion cannot - // happen by itself. The trick is to manually move the cursor - // backwards: the browser will then delete the value under this - // new cursor and perform the changes via the triggered - // `onchange` event. - else { - const state: State = { ...prevState }; - - state.cursor = Math.max(0, action.index - 1); - - // let know the handle-change action that we already moved - // backwards and that we don't need to touch the cursor - // anymore - state.backspace = true; - - state.dirty = true; - - return state; - } - } - - case "noop": - default: - break; - } - - return prevState; -} - -export type Handler = { - refs: RefObject; - state: State; - dispatch: ActionDispatch<[Action]>; - value: string; - setValue: (value: string) => void; -}; - -export function usePinField(): Handler { - const refs = useRef([]); - const [state, dispatch] = useReducer(reducer, defaultState); - - const value = useMemo(() => { - let value = ""; - for (let index = 0; index < state.length; index++) { - value += index in state.values ? state.values[index] : ""; - } - return value; - }, [state]); - - const setValue = useCallback( - (value: string) => { - dispatch({ type: "handle-change", index: 0, value, reset: true }); - }, - [dispatch, state.cursor], - ); - - return useMemo( - () => ({ refs, state, dispatch, value, setValue }), - [refs, state, dispatch, value, setValue], - ); -} - -export const PinFieldV2 = forwardRef( - ( - { - length = defaultProps.length, - format = defaultProps.format, - formatAriaLabel = defaultProps.formatAriaLabel, - onChange: handleChange = defaultProps.onChange, - onComplete: handleComplete = defaultProps.onComplete, - handler: customHandler, - autoFocus, - ...nativeProps - }, - fwdRef, - ) => { - const internalHandler = usePinField(); - const { refs, state, dispatch } = customHandler || internalHandler; - - useImperativeHandle(fwdRef, () => refs.current, [refs]); - - function setRefAt(index: number): (ref: HTMLInputElement) => void { - return ref => { - if (ref) { - refs.current[index] = ref; - } - }; - } - - function handleKeyDownAt(index: number): KeyboardEventHandler { - return event => { - console.log("keyDown", index, event); - const { key, code, keyCode, which } = event; - dispatch({ type: "handle-key-down", index, key, code, keyCode, which }); - }; - } - - function handleChangeAt(index: number): ChangeEventHandler { - return event => { - if (event.nativeEvent instanceof InputEvent) { - const value = event.nativeEvent.data; - dispatch({ type: "handle-change", index, value }); - } else { - const { value } = event.target; - dispatch({ type: "handle-change", index, value, reset: true }); - } - }; - } - - function startCompositionAt(index: number): CompositionEventHandler { - return () => { - dispatch({ type: "start-composition", index }); - }; - } - - function endCompositionAt(index: number): CompositionEventHandler { - return event => { - dispatch({ type: "end-composition", index, value: event.data }); - }; - } - - // initial props to state update - useEffect(() => { - if (state.ready) return; - const dir = - nativeProps.dir?.toLowerCase() || - document.documentElement.getAttribute("dir")?.toLowerCase(); - dispatch({ type: "update-props", props: { length, format, dir } }); - }, [state.ready, dispatch, length, format]); - - // props.length to state update - useEffect(() => { - if (!state.ready) return; - if (length === state.length) return; - dispatch({ type: "update-props", props: { length } }); - }, [state.ready, length, state.length, dispatch]); - - // props.format to state update - useEffect(() => { - if (!state.ready) return; - if (format === state.format) return; - dispatch({ type: "update-props", props: { format } }); - }, [state.ready, format, state.format, dispatch]); - - // nativeProps.dir to state update - useEffect(() => { - if (!state.ready) return; - const dir = - nativeProps.dir?.toLowerCase() || - document.documentElement.getAttribute("dir")?.toLowerCase(); - if (dir === state.dir) return; - dispatch({ type: "update-props", props: { dir } }); - }, [state.ready, nativeProps.dir, state.dir, dispatch]); - - // state to view update - useEffect(() => { - if (!refs.current) return; - if (!state.ready) return; - if (!state.dirty) return; - - let innerFocus = false; - let completed = state.values.length == state.length; - let value = ""; - - for (let index = 0; index < state.length; index++) { - const char = index in state.values ? state.values[index] : ""; - refs.current[index].value = char; - innerFocus = innerFocus || hasFocus(refs.current[index]); - completed = completed && index in state.values && refs.current[index].checkValidity(); - value += char; - } - - if (innerFocus) { - refs.current[state.cursor].focus(); - } - - if (handleChange) { - handleChange(value); - } - - if (handleComplete && completed) { - handleComplete(value); - } - }, [refs, state, handleChange, handleComplete]); - - if (!state.ready) { - return null; - } - - const inputs = range(0, state.length).map(index => ( - - )); - - if (state.dir === "rtl") { - inputs.reverse(); - } - - return inputs; - }, -); - -export function hasFocus(el: HTMLElement): boolean { - try { - const matches = el.webkitMatchesSelector || el.matches; - return matches.call(el, ":focus"); - } catch (err: any) { - return false; - } -} - -export default PinFieldV2; diff --git a/src/pin-field/pin-field-v2.e2e.tsx b/src/pin-field/pin-field.e2e.ts similarity index 97% rename from src/pin-field/pin-field-v2.e2e.tsx rename to src/pin-field/pin-field.e2e.ts index 639718f..90a03dd 100644 --- a/src/pin-field/pin-field-v2.e2e.tsx +++ b/src/pin-field/pin-field.e2e.ts @@ -5,7 +5,7 @@ describe("PIN Field", () => { beforeEach(() => { cy.visit("/iframe.html", { qs: { - id: "pinfieldv2--default", + id: "pinfield--default", viewMode: "story", }, }); diff --git a/src/pin-field/pin-field.spec.tsx b/src/pin-field/pin-field.spec.tsx deleted file mode 100644 index f6e544c..0000000 --- a/src/pin-field/pin-field.spec.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import "@testing-library/jest-dom"; - -import React from "react"; -import { render, screen, fireEvent } from "@testing-library/react"; - -import PinField from "./pin-field"; - -const TEST_ID = "test"; - -test.skip("structure", async () => { - render(); - const inputs = await screen.findAllByTestId(TEST_ID); - - expect(inputs).toHaveLength(5); - inputs.forEach(input => { - expect(input.getAttribute("type")).toBe("text"); - expect(input.getAttribute("inputmode")).toBe("text"); - expect(input.getAttribute("autocapitalize")).toBe("off"); - expect(input.getAttribute("autocorrect")).toBe("off"); - expect(input.getAttribute("autocomplete")).toBe("off"); - }); -}); - -test.skip("ref as object", () => { - const ref: { current: HTMLInputElement[] | null } = { current: [] }; - render(); - - expect(Array.isArray(ref.current)).toBe(true); - expect(ref.current).toHaveLength(5); -}); - -test.skip("ref as func", () => { - const ref: { current: HTMLInputElement[] | null } = { current: [] }; - render( - { - ref.current = el; - }} - />, - ); - - expect(Array.isArray(ref.current)).toBe(true); - expect(ref.current).toHaveLength(5); -}); - -test.skip("autoFocus", async () => { - render(); - const inputs = await screen.findAllByTestId(TEST_ID); - - inputs.forEach((input, idx) => { - if (idx === 0) { - expect(input).toHaveFocus(); - } else { - expect(input).not.toHaveFocus(); - } - }); -}); - -test.skip("className", async () => { - render(); - const inputs = await screen.findAllByTestId(TEST_ID); - - inputs.forEach(input => { - expect(input.className).toBe("custom-class-name"); - }); -}); - -test.skip("style", async () => { - render(); - const inputs = await screen.findAllByTestId(TEST_ID); - - inputs.forEach(input => { - expect(input.style.position).toBe("absolute"); - }); -}); - -test.skip("events", async () => { - const handleChangeMock = jest.fn(); - const handleCompleteMock = jest.fn(); - render(); - const inputs = await screen.findAllByTestId(TEST_ID); - - fireEvent.focus(inputs[0]); - fireEvent.keyDown(inputs[0], { key: "Alt", target: inputs[0] }); - fireEvent.keyDown(inputs[0], { key: "a", target: inputs[0] }); - fireEvent.keyDown(inputs[1], { which: 66, target: inputs[1] }); - fireEvent.paste(inputs[1], { clipboardData: { getData: () => "cde" } }); - - expect(handleChangeMock).toHaveBeenCalledTimes(3); - expect(handleCompleteMock).toHaveBeenCalledTimes(1); - expect(handleCompleteMock).toHaveBeenCalledWith("abcd"); -}); - -test.skip("fallback events", async () => { - const handleChangeMock = jest.fn(); - const handleCompleteMock = jest.fn(); - render(); - const inputs = await screen.findAllByTestId(TEST_ID); - - fireEvent.focus(inputs[0]); - fireEvent.keyDown(inputs[0], { key: "Unidentified", target: { value: "" } }); - fireEvent.keyUp(inputs[0], { target: { value: "a" } }); - - expect(handleChangeMock).toHaveBeenCalledTimes(1); - expect(handleChangeMock).toHaveBeenCalledWith("a"); -}); - -describe.skip("a11y", () => { - test.skip("should have aria-label per input field", () => { - render(); - - expect(screen.getByRole("textbox", { name: /pin code 1 of 3/i })).toBeVisible(); - expect(screen.getByRole("textbox", { name: /pin code 2 of 3/i })).toBeVisible(); - expect(screen.getByRole("textbox", { name: /pin code 3 of 3/i })).toBeVisible(); - }); - - test.skip("should support custom aria-label format", () => { - render( `${i}/${c}`} />); - - expect(screen.getByRole("textbox", { name: "1/3" })).toBeVisible(); - expect(screen.getByRole("textbox", { name: "2/3" })).toBeVisible(); - expect(screen.getByRole("textbox", { name: "3/3" })).toBeVisible(); - }); - - test.skip("every input has aria-required", () => { - render(); - - expect(screen.getByRole("textbox", { name: /pin code 1 of 3/i })).toHaveAttribute("aria-required", "true"); - expect(screen.getByRole("textbox", { name: /pin code 2 of 3/i })).toHaveAttribute("aria-required", "true"); - expect(screen.getByRole("textbox", { name: /pin code 3 of 3/i })).toHaveAttribute("aria-required", "true"); - }); - - test.skip("every input should have aria-disabled when PinField is disabled", () => { - render(); - - expect(screen.getByRole("textbox", { name: /pin code 1 of 3/i })).toHaveAttribute("aria-disabled", "true"); - expect(screen.getByRole("textbox", { name: /pin code 2 of 3/i })).toHaveAttribute("aria-disabled", "true"); - expect(screen.getByRole("textbox", { name: /pin code 3 of 3/i })).toHaveAttribute("aria-disabled", "true"); - }); - - test.skip("every input should have aria-readonly when PinField is readOnly", () => { - render(); - - expect(screen.getByRole("textbox", { name: /pin code 1 of 3/i })).toHaveAttribute("aria-readonly", "true"); - expect(screen.getByRole("textbox", { name: /pin code 2 of 3/i })).toHaveAttribute("aria-readonly", "true"); - expect(screen.getByRole("textbox", { name: /pin code 3 of 3/i })).toHaveAttribute("aria-readonly", "true"); - }); -}); diff --git a/src/pin-field/pin-field-v2.stories.scss b/src/pin-field/pin-field.stories.scss similarity index 100% rename from src/pin-field/pin-field-v2.stories.scss rename to src/pin-field/pin-field.stories.scss diff --git a/src/pin-field/pin-field-v2.stories.tsx b/src/pin-field/pin-field.stories.tsx similarity index 85% rename from src/pin-field/pin-field-v2.stories.tsx rename to src/pin-field/pin-field.stories.tsx index ef45694..3395c37 100644 --- a/src/pin-field/pin-field-v2.stories.tsx +++ b/src/pin-field/pin-field.stories.tsx @@ -1,9 +1,10 @@ import { FC, StrictMode as ReactStrictMode } from "react"; import type { Meta, StoryObj } from "@storybook/react"; import { fn } from "@storybook/test"; -import { PinFieldV2, defaultProps, Props, usePinField, InnerProps } from "./pin-field-v2"; -import "./pin-field-v2.stories.scss"; +import PinField, { defaultProps, Props, usePinField, InnerProps } from "./pin-field"; + +import "./pin-field.stories.scss"; const defaultArgs = { length: defaultProps.length, @@ -18,27 +19,27 @@ const defaultArgs = { * * The component exposes 4 event handlers, see stories below to learn more about the other props. */ -const meta: Meta = { - title: "PinFieldV2", - component: PinFieldV2, +const meta: Meta = { + title: "PinField", + component: PinField, tags: ["autodocs"], parameters: { layout: "centered", }, }; -export const Default: StoryObj = { - render: props => , +export const Default: StoryObj = { + render: props => , args: defaultArgs, }; /** * Story to detect inconsistent behaviours in React Strict Mode. */ -export const StrictMode: StoryObj = { +export const StrictMode: StoryObj = { render: props => ( - + ), args: defaultArgs, @@ -67,7 +68,7 @@ export const Controlled: StoryObj> = { return ( <>
- +
> = { try { let format = eval(formatEval); format("a"); - return ; + return ; } catch (err: any) { return
Invalid format function: {err.toString()}
; } @@ -120,7 +121,7 @@ export const HTMLInputAttributes: StoryObj
- +
diff --git a/src/pin-field/pin-field.test.ts b/src/pin-field/pin-field.test.ts deleted file mode 100644 index e6c4062..0000000 --- a/src/pin-field/pin-field.test.ts +++ /dev/null @@ -1,587 +0,0 @@ -import React from "react"; - -import * as pinField from "./pin-field"; -import { noop } from "../utils"; - -jest.mock("react", () => ({ - useCallback: (f: any) => f, - forwardRef: (f: any) => f, -})); - -function mockInput(value: string) { - const setValMock = jest.fn(); - const ref = { - focus: jest.fn(), - setCustomValidity: jest.fn(), - set value(val: string) { - setValMock(val); - }, - get value() { - return value; - }, - }; - - return { ref, setValMock }; -} - -test.skip("constants", () => { - const { NO_EFFECTS, PROP_KEYS, HANDLER_KEYS, IGNORED_META_KEYS } = pinField; - - expect(NO_EFFECTS).toEqual([]); - expect(PROP_KEYS).toEqual(["autoFocus", "length", "validate", "format", "formatAriaLabel", "debug"]); - expect(HANDLER_KEYS).toEqual(["onResolveKey", "onRejectKey", "onChange", "onComplete"]); - expect(IGNORED_META_KEYS).toEqual(["Alt", "Control", "Enter", "Meta", "Shift", "Tab"]); -}); - -test.skip("default props", () => { - const { defaultProps } = pinField; - - expect(defaultProps).toHaveProperty("length", 5); - expect(defaultProps).toHaveProperty("validate", /^[a-zA-Z0-9]$/); - expect(defaultProps).toHaveProperty("format"); - expect(defaultProps).toHaveProperty("formatAriaLabel", expect.any(Function)); - expect(defaultProps.format("abcABC123@-_[]")).toStrictEqual("abcABC123@-_[]"); - expect(defaultProps.onResolveKey("a")).toStrictEqual(undefined); - expect(defaultProps).toHaveProperty("onRejectKey"); - expect(defaultProps.onRejectKey("a")).toStrictEqual(undefined); - expect(defaultProps).toHaveProperty("onChange"); - expect(defaultProps.onChange("a")).toStrictEqual(undefined); - expect(defaultProps).toHaveProperty("onComplete"); - expect(defaultProps.onComplete("a")).toStrictEqual(undefined); -}); - -test.skip("default state", () => { - const { defaultState, defaultProps } = pinField; - const state = defaultState(defaultProps); - - expect(state).toHaveProperty("focusIdx", 0); - expect(state).toHaveProperty("codeLength", 5); - expect(state).toHaveProperty("isKeyAllowed"); - expect(typeof state.isKeyAllowed).toStrictEqual("function"); - expect(state.isKeyAllowed("a")).toStrictEqual(true); - expect(state.isKeyAllowed("@")).toStrictEqual(false); - expect(state).toHaveProperty("fallback", null); -}); - -test.skip("get previous focus index", () => { - const { getPrevFocusIdx } = pinField; - - expect(getPrevFocusIdx(5)).toStrictEqual(4); - expect(getPrevFocusIdx(1)).toStrictEqual(0); - expect(getPrevFocusIdx(0)).toStrictEqual(0); - expect(getPrevFocusIdx(-1)).toStrictEqual(0); -}); - -test.skip("get next focus index", () => { - const { getNextFocusIdx } = pinField; - - expect(getNextFocusIdx(0, 0)).toStrictEqual(0); - expect(getNextFocusIdx(0, 1)).toStrictEqual(0); - expect(getNextFocusIdx(1, 1)).toStrictEqual(0); - expect(getNextFocusIdx(0, 2)).toStrictEqual(1); - expect(getNextFocusIdx(3, 5)).toStrictEqual(4); - expect(getNextFocusIdx(5, 3)).toStrictEqual(2); -}); - -describe.skip("is key allowed", () => { - const { isKeyAllowed } = pinField; - - test.skip("string", () => { - const str = isKeyAllowed("a"); - - expect(str("")).toStrictEqual(false); - expect(str("a")).toStrictEqual(true); - expect(str("b")).toStrictEqual(false); - expect(str("ab")).toStrictEqual(false); - }); - - test.skip("array", () => { - const arr = isKeyAllowed(["a", "b"]); - - expect(arr("a")).toStrictEqual(true); - expect(arr("a")).toStrictEqual(true); - expect(arr("b")).toStrictEqual(true); - expect(arr("ab")).toStrictEqual(false); - }); - - test.skip("regex", () => { - const exp = isKeyAllowed(/^[ab]$/); - - expect(exp("a")).toStrictEqual(true); - expect(exp("b")).toStrictEqual(true); - expect(exp("ab")).toStrictEqual(false); - }); - - test.skip("function", () => { - const func = isKeyAllowed(k => k === "a" || k === "b"); - - expect(func("a")).toStrictEqual(true); - expect(func("b")).toStrictEqual(true); - expect(func("ab")).toStrictEqual(false); - }); -}); - -describe.skip("state reducer", () => { - const { NO_EFFECTS, stateReducer, defaultState, defaultProps } = pinField; - const currState = defaultState(defaultProps); - - test.skip("default action", () => { - // @ts-expect-error bad action - const [state, eff] = stateReducer(currState, { type: "bad-action" }); - - expect(state).toMatchObject(state); - expect(eff).toEqual(NO_EFFECTS); - }); - - describe.skip("handle-key-down", () => { - test.skip("unidentified", () => { - const [state, eff] = stateReducer(currState, { - type: "handle-key-down", - key: "Unidentified", - idx: 0, - val: "", - }); - - expect(state).toMatchObject(state); - expect(eff).toEqual([]); - }); - - test.skip("dead", () => { - const [state, eff] = stateReducer(currState, { - type: "handle-key-down", - key: "Dead", - idx: 0, - val: "", - }); - - expect(state).toMatchObject(state); - expect(eff).toEqual([ - { type: "set-input-val", idx: 0, val: "" }, - { type: "reject-key", idx: 0, key: "Dead" }, - { type: "handle-code-change" }, - ]); - }); - - describe.skip("left arrow", () => { - test.skip("from the first input", () => { - const [state, eff] = stateReducer(currState, { - type: "handle-key-down", - key: "ArrowLeft", - idx: 0, - val: "", - }); - - expect(state).toMatchObject({ ...state, focusIdx: 0 }); - expect(eff).toEqual([{ type: "focus-input", idx: 0 }]); - }); - - test.skip("from the last input", () => { - const [state, eff] = stateReducer( - { ...currState, focusIdx: 4 }, - { type: "handle-key-down", key: "ArrowLeft", idx: 0, val: "" }, - ); - - expect(state).toMatchObject({ ...state, focusIdx: 3 }); - expect(eff).toEqual([{ type: "focus-input", idx: 3 }]); - }); - }); - - describe.skip("right arrow", () => { - test.skip("from the first input", () => { - const [state, eff] = stateReducer(currState, { - type: "handle-key-down", - key: "ArrowRight", - idx: 0, - val: "", - }); - - expect(state).toMatchObject({ ...state, focusIdx: 1 }); - expect(eff).toEqual([{ type: "focus-input", idx: 1 }]); - }); - - test.skip("from the last input", () => { - const [state, eff] = stateReducer( - { ...currState, focusIdx: 4 }, - { type: "handle-key-down", key: "ArrowRight", idx: 0, val: "" }, - ); - - expect(state).toMatchObject({ ...state, focusIdx: 4 }); - expect(eff).toEqual([{ type: "focus-input", idx: 4 }]); - }); - }); - - test.skip("backspace", () => { - const [state, eff] = stateReducer(currState, { - type: "handle-key-down", - key: "Backspace", - idx: 0, - val: "", - }); - - expect(state).toMatchObject({ ...state, focusIdx: 0 }); - expect(eff).toEqual([{ type: "handle-delete", idx: 0 }, { type: "handle-code-change" }]); - }); - - test.skip("delete", () => { - const [state, eff] = stateReducer(currState, { - type: "handle-key-down", - key: "Delete", - idx: 0, - val: "", - }); - - expect(state).toMatchObject({ ...state, focusIdx: 0 }); - expect(eff).toEqual([{ type: "handle-delete", idx: 0 }, { type: "handle-code-change" }]); - }); - - describe.skip("default", () => { - test.skip("resolve", () => { - const [state, eff] = stateReducer(currState, { - type: "handle-key-down", - key: "a", - idx: 0, - val: "", - }); - - expect(state).toMatchObject({ ...state, focusIdx: 1 }); - expect(eff).toEqual([ - { type: "set-input-val", idx: 0, val: "a" }, - { type: "resolve-key", idx: 0, key: "a" }, - { type: "focus-input", idx: 1 }, - { type: "handle-code-change" }, - ]); - }); - - test.skip("reject", () => { - const [state, eff] = stateReducer(currState, { - type: "handle-key-down", - key: "@", - idx: 0, - val: "", - }); - - expect(state).toMatchObject(state); - expect(eff).toEqual([{ type: "reject-key", idx: 0, key: "@" }]); - }); - }); - }); - - describe.skip("handle-key-up", () => { - test.skip("no fallback", () => { - const [state, eff] = stateReducer(currState, { - type: "handle-key-up", - idx: 0, - val: "", - }); - - expect(state).toMatchObject(state); - expect(eff).toEqual([]); - }); - - test.skip("empty prevVal, empty val", () => { - const [state, eff] = stateReducer( - { ...currState, fallback: { idx: 0, val: "" } }, - { type: "handle-key-up", idx: 0, val: "" }, - ); - - expect(state).toMatchObject({ fallback: null }); - expect(eff).toEqual([{ type: "handle-delete", idx: 0 }, { type: "handle-code-change" }]); - }); - - test.skip("empty prevVal, not empty allowed val", () => { - const [state, eff] = stateReducer( - { ...currState, fallback: { idx: 0, val: "" } }, - { type: "handle-key-up", idx: 0, val: "a" }, - ); - - expect(state).toMatchObject({ fallback: null }); - expect(eff).toEqual([ - { type: "set-input-val", idx: 0, val: "a" }, - { type: "resolve-key", idx: 0, key: "a" }, - { type: "focus-input", idx: 1 }, - { type: "handle-code-change" }, - ]); - }); - - test.skip("empty prevVal, not empty denied val", () => { - const [state, eff] = stateReducer( - { ...currState, fallback: { idx: 0, val: "" } }, - { type: "handle-key-up", idx: 0, val: "@" }, - ); - - expect(state).toMatchObject({ fallback: null }); - expect(eff).toEqual([ - { type: "set-input-val", idx: 0, val: "" }, - { type: "reject-key", idx: 0, key: "@" }, - { type: "handle-code-change" }, - ]); - }); - - test.skip("not empty prevVal", () => { - const [state, eff] = stateReducer( - { ...currState, fallback: { idx: 0, val: "a" } }, - { type: "handle-key-up", idx: 0, val: "a" }, - ); - - expect(state).toMatchObject({ fallback: null }); - expect(eff).toEqual([ - { type: "set-input-val", idx: 0, val: "a" }, - { type: "resolve-key", idx: 0, key: "a" }, - { type: "focus-input", idx: 1 }, - { type: "handle-code-change" }, - ]); - }); - }); - - describe.skip("handle-paste", () => { - test.skip("paste smaller text than code length", () => { - const [state, eff] = stateReducer(currState, { - type: "handle-paste", - idx: 0, - val: "abc", - }); - - expect(state).toMatchObject({ ...state, focusIdx: 3 }); - expect(eff).toEqual([ - { type: "set-input-val", idx: 0, val: "a" }, - { type: "resolve-key", idx: 0, key: "a" }, - { type: "set-input-val", idx: 1, val: "b" }, - { type: "resolve-key", idx: 1, key: "b" }, - { type: "set-input-val", idx: 2, val: "c" }, - { type: "resolve-key", idx: 2, key: "c" }, - { type: "focus-input", idx: 3 }, - { type: "handle-code-change" }, - ]); - }); - - test.skip("paste bigger text than code length", () => { - const [state, eff] = stateReducer(currState, { - type: "handle-paste", - idx: 0, - val: "abcdefgh", - }); - - expect(state).toMatchObject({ ...state, focusIdx: 4 }); - expect(eff).toEqual([ - { type: "set-input-val", idx: 0, val: "a" }, - { type: "resolve-key", idx: 0, key: "a" }, - { type: "set-input-val", idx: 1, val: "b" }, - { type: "resolve-key", idx: 1, key: "b" }, - { type: "set-input-val", idx: 2, val: "c" }, - { type: "resolve-key", idx: 2, key: "c" }, - { type: "set-input-val", idx: 3, val: "d" }, - { type: "resolve-key", idx: 3, key: "d" }, - { type: "set-input-val", idx: 4, val: "e" }, - { type: "resolve-key", idx: 4, key: "e" }, - { type: "focus-input", idx: 4 }, - { type: "handle-code-change" }, - ]); - }); - - test.skip("paste on last input", () => { - const [state, eff] = stateReducer({ ...currState, focusIdx: 4 }, { type: "handle-paste", idx: 0, val: "abc" }); - - expect(state).toMatchObject({ ...state, focusIdx: 4 }); - expect(eff).toEqual([ - { type: "set-input-val", idx: 4, val: "a" }, - { type: "resolve-key", idx: 4, key: "a" }, - { type: "handle-code-change" }, - ]); - }); - - test.skip("paste with denied key", () => { - const [state, eff] = stateReducer(currState, { - type: "handle-paste", - idx: 1, - val: "ab@", - }); - - expect(state).toMatchObject(state); - expect(eff).toEqual([ - { type: "set-input-val", idx: 0, val: "" }, - { type: "reject-key", idx: 1, key: "ab@" }, - { type: "handle-code-change" }, - ]); - }); - }); - - test.skip("focus-input", () => { - const [state, eff] = stateReducer(currState, { type: "focus-input", idx: 2 }); - - expect(state).toMatchObject({ ...state, focusIdx: 2 }); - expect(eff).toEqual([{ type: "focus-input", idx: 2 }]); - }); -}); - -describe.skip("effect reducer", () => { - const { defaultProps, useEffectReducer } = pinField; - const inputA = mockInput("a"); - const inputB = mockInput("b"); - const inputC = mockInput(""); - const propsFormatMock = jest.fn(); - const propsMock = { - ...defaultProps, - length: 3, - format: (char: string) => { - propsFormatMock.apply(char); - return char; - }, - onResolveKey: jest.fn(), - onRejectKey: jest.fn(), - onChange: jest.fn(), - onComplete: jest.fn(), - }; - - const refs: React.RefObject = { - current: [inputA.ref, inputB.ref, inputC.ref], - }; - const effectReducer = useEffectReducer({ ...propsMock, refs }); - - beforeEach(() => { - jest.resetAllMocks(); - }); - - test.skip("default action", () => { - // @ts-expect-error bad action - effectReducer({ type: "bad-action" }); - }); - - test.skip("focus input", () => { - effectReducer({ type: "focus-input", idx: 0 }, noop); - expect(inputA.ref.focus).toHaveBeenCalledTimes(1); - }); - - describe.skip("set input val", () => { - test.skip("empty char", () => { - effectReducer({ type: "set-input-val", idx: 0, val: "" }, noop); - - expect(propsFormatMock).toHaveBeenCalledTimes(1); - expect(inputA.setValMock).toHaveBeenCalledTimes(1); - expect(inputA.setValMock).toHaveBeenCalledWith(""); - }); - - test.skip("non empty char", () => { - effectReducer({ type: "set-input-val", idx: 0, val: "a" }, noop); - - expect(propsFormatMock).toHaveBeenCalledTimes(1); - expect(inputA.setValMock).toHaveBeenCalledTimes(1); - expect(inputA.setValMock).toHaveBeenCalledWith("a"); - }); - }); - - test.skip("resolve key", () => { - effectReducer({ type: "resolve-key", idx: 0, key: "a" }, noop); - - expect(inputA.ref.setCustomValidity).toHaveBeenCalledTimes(1); - expect(inputA.ref.setCustomValidity).toHaveBeenCalledWith(""); - expect(propsMock.onResolveKey).toHaveBeenCalledTimes(1); - expect(propsMock.onResolveKey).toHaveBeenCalledWith("a", inputA.ref); - }); - - test.skip("reject key", () => { - effectReducer({ type: "reject-key", idx: 0, key: "a" }, noop); - - expect(inputA.ref.setCustomValidity).toHaveBeenCalledTimes(1); - expect(inputA.ref.setCustomValidity).toHaveBeenCalledWith("Invalid key"); - expect(propsMock.onRejectKey).toHaveBeenCalledTimes(1); - expect(propsMock.onRejectKey).toHaveBeenCalledWith("a", inputA.ref); - }); - - describe.skip("handle backspace", () => { - test.skip("from input A, not empty val", () => { - effectReducer({ type: "handle-delete", idx: 0 }, noop); - - expect(inputA.ref.setCustomValidity).toHaveBeenCalledTimes(1); - expect(inputA.ref.setCustomValidity).toHaveBeenCalledWith(""); - expect(inputA.setValMock).toHaveBeenCalledTimes(1); - expect(inputA.setValMock).toHaveBeenCalledWith(""); - }); - - test.skip("from input B, not empty val", () => { - effectReducer({ type: "handle-delete", idx: 1 }, noop); - - expect(inputB.ref.setCustomValidity).toHaveBeenCalledTimes(1); - expect(inputB.ref.setCustomValidity).toHaveBeenCalledWith(""); - expect(inputB.setValMock).toHaveBeenCalledTimes(1); - expect(inputB.setValMock).toHaveBeenCalledWith(""); - }); - - test.skip("from input C, empty val", () => { - effectReducer({ type: "handle-delete", idx: 2 }, noop); - - expect(inputC.ref.setCustomValidity).toHaveBeenCalledTimes(1); - expect(inputC.ref.setCustomValidity).toHaveBeenCalledWith(""); - expect(inputC.setValMock).toHaveBeenCalledTimes(1); - expect(inputC.setValMock).toHaveBeenCalledWith(""); - expect(inputB.ref.focus).toHaveBeenCalledTimes(1); - expect(inputB.ref.setCustomValidity).toHaveBeenCalledWith(""); - expect(inputB.setValMock).toHaveBeenCalledTimes(1); - expect(inputB.setValMock).toHaveBeenCalledWith(""); - }); - }); - - describe.skip("handle-code-change", () => { - test.skip("code not complete", () => { - effectReducer({ type: "handle-code-change" }, noop); - - expect(propsMock.onChange).toHaveBeenCalledTimes(1); - expect(propsMock.onChange).toHaveBeenCalledWith("ab"); - }); - - test.skip("code complete", () => { - const inputA = mockInput("a"); - const inputB = mockInput("b"); - const inputC = mockInput("c"); - const refs: React.RefObject = { - current: [inputA.ref, inputB.ref, inputC.ref], - }; - const notify = useEffectReducer({ ...propsMock, refs }); - - notify({ type: "handle-code-change" }, noop); - - expect(propsMock.onChange).toHaveBeenCalledTimes(1); - expect(propsMock.onChange).toHaveBeenCalledWith("abc"); - expect(propsMock.onComplete).toHaveBeenCalledTimes(1); - expect(propsMock.onComplete).toHaveBeenCalledWith("abc"); - }); - - test.skip("rtl", () => { - jest.spyOn(document.documentElement, "getAttribute").mockImplementation(() => "rtl"); - - const inputA = mockInput("a"); - const inputB = mockInput("b"); - const inputC = mockInput("c"); - const refs: React.RefObject = { - current: [inputA.ref, inputB.ref, inputC.ref], - }; - const notify = useEffectReducer({ ...propsMock, refs }); - - notify({ type: "handle-code-change" }, noop); - - expect(propsMock.onChange).toHaveBeenCalledTimes(1); - expect(propsMock.onChange).toHaveBeenCalledWith("cba"); - expect(propsMock.onComplete).toHaveBeenCalledTimes(1); - expect(propsMock.onComplete).toHaveBeenCalledWith("cba"); - }); - - test.skip("rtl with override in props", () => { - jest.spyOn(document.documentElement, "getAttribute").mockImplementation(() => "rtl"); - - const inputA = mockInput("a"); - const inputB = mockInput("b"); - const inputC = mockInput("c"); - const refs: React.RefObject = { - current: [inputA.ref, inputB.ref, inputC.ref], - }; - const propsWithDir = { ...propsMock, dir: "ltr" }; - const notify = useEffectReducer({ ...propsWithDir, refs }); - - notify({ type: "handle-code-change" }, noop); - - expect(propsMock.onChange).toHaveBeenCalledTimes(1); - expect(propsMock.onChange).toHaveBeenCalledWith("abc"); - expect(propsMock.onComplete).toHaveBeenCalledTimes(1); - expect(propsMock.onComplete).toHaveBeenCalledWith("abc"); - }); - }); -}); diff --git a/src/pin-field/pin-field-v2.test.ts b/src/pin-field/pin-field.test.tsx similarity index 74% rename from src/pin-field/pin-field-v2.test.ts rename to src/pin-field/pin-field.test.tsx index ea4a38a..c30a019 100644 --- a/src/pin-field/pin-field-v2.test.ts +++ b/src/pin-field/pin-field.test.tsx @@ -1,4 +1,9 @@ -import { +import "@testing-library/jest-dom"; + +import { RefObject } from "react"; +import { fireEvent, render, screen } from "@testing-library/react"; + +import PinField, { reducer, BACKSPACE, DELETE, @@ -7,7 +12,7 @@ import { State, HandleKeyDownAction, defaultNativeProps, -} from "./pin-field-v2"; +} from "./pin-field"; test("constants", () => { expect(BACKSPACE).toEqual(8); @@ -651,7 +656,7 @@ describe("state reducer", () => { { keyCode: DELETE }, { which: BACKSPACE }, { which: DELETE }, - ])("delete with %o when value exists at index", (action) => { + ])("delete with %o when value exists at index", action => { const prevState: State = { ...defaultState, values: ["a", "b"] }; const state = reducer(prevState, { type: "handle-key-down", @@ -688,3 +693,175 @@ describe("state reducer", () => { }); }); }); + +test("default", async () => { + render(); + + const inputs = await screen.findAllByRole("textbox"); + expect(inputs).toHaveLength(5); + + inputs.forEach(input => { + expect(input.getAttribute("type")).toBe("text"); + expect(input.getAttribute("inputmode")).toBe("text"); + expect(input.getAttribute("autocapitalize")).toBe("off"); + expect(input.getAttribute("autocorrect")).toBe("off"); + expect(input.getAttribute("autocomplete")).toBe("off"); + }); +}); + +test("forward ref as object", () => { + const ref: RefObject = { current: [] }; + render(); + + expect(Array.isArray(ref.current)).toBe(true); + expect(ref.current).toHaveLength(5); +}); + +test("forward ref as func", () => { + const ref: RefObject = { current: [] }; + render( + { + if (el) { + ref.current = el; + } + }} + />, + ); + + expect(Array.isArray(ref.current)).toBe(true); + expect(ref.current).toHaveLength(5); +}); + +test("className", async () => { + render(); + const inputs = await screen.findAllByRole("textbox"); + + inputs.forEach(input => { + expect(input.className).toBe("custom-class-name"); + }); +}); + +test("style", async () => { + render(); + const inputs = await screen.findAllByRole("textbox"); + + inputs.forEach(input => { + expect(input.style.position).toBe("absolute"); + }); +}); + +test("autoFocus", async () => { + render(); + const inputs = await screen.findAllByRole("textbox"); + + inputs.forEach((input, index) => { + if (index === 0) { + expect(input).toHaveFocus(); + } else { + expect(input).not.toHaveFocus(); + } + }); +}); + +test("autoFocus rtl", async () => { + render(); + const inputs = await screen.findAllByRole("textbox"); + + inputs.forEach((input, index) => { + if (index === 4) { + expect(input).toHaveFocus(); + } else { + expect(input).not.toHaveFocus(); + } + }); +}); + +describe("events", () => { + test("change single input", async () => { + const handleChangeMock = jest.fn(); + const handleCompleteMock = jest.fn(); + render(); + + const inputs = (await screen.findAllByRole("textbox")) as HTMLInputElement[]; + + fireEvent.change(inputs[0], { target: { value: "a" } }); + expect(handleChangeMock).toHaveBeenCalledWith("a"); + expect(inputs[0].value).toBe("a"); + + fireEvent.change(inputs[1], { target: { value: "b" } }); + expect(handleChangeMock).toHaveBeenCalledWith("ab"); + expect(inputs[0].value).toBe("a"); + expect(inputs[1].value).toBe("b"); + + fireEvent.change(inputs[2], { target: { value: "c" } }); + expect(handleChangeMock).toHaveBeenCalledWith("abc"); + expect(inputs[0].value).toBe("a"); + expect(inputs[1].value).toBe("b"); + expect(inputs[2].value).toBe("c"); + + fireEvent.change(inputs[3], { target: { value: "d" } }); + expect(handleChangeMock).toHaveBeenCalledWith("abcd"); + expect(handleCompleteMock).toHaveBeenCalledWith("abcd"); + expect(inputs[0].value).toBe("a"); + expect(inputs[1].value).toBe("b"); + expect(inputs[2].value).toBe("c"); + expect(inputs[3].value).toBe("d"); + }); + + test("change multi input", async () => { + const handleChangeMock = jest.fn(); + const handleCompleteMock = jest.fn(); + render(); + + const inputs = (await screen.findAllByRole("textbox")) as HTMLInputElement[]; + + fireEvent.change(inputs[0], { target: { value: "abc" } }); + expect(handleChangeMock).toHaveBeenLastCalledWith("abc"); + expect(inputs[0].value).toBe("a"); + expect(inputs[1].value).toBe("b"); + expect(inputs[2].value).toBe("c"); + + fireEvent.change(inputs[1], { target: { value: "def" } }); + expect(handleChangeMock).toHaveBeenLastCalledWith("adef"); + expect(handleCompleteMock).toHaveBeenCalledWith("adef"); + expect(inputs[0].value).toBe("a"); + expect(inputs[1].value).toBe("d"); + expect(inputs[2].value).toBe("e"); + expect(inputs[3].value).toBe("f"); + }); +}); + +describe("a11y", () => { + test("default aria-label", () => { + render(); + + expect(screen.getByRole("textbox", { name: "PIN field 1 of 3" })).toBeVisible(); + expect(screen.getByRole("textbox", { name: "PIN field 2 of 3" })).toBeVisible(); + expect(screen.getByRole("textbox", { name: "PIN field 3 of 3" })).toBeVisible(); + }); + + test("aria-required", () => { + render( `${i}`} required />); + + expect(screen.getByRole("textbox", { name: "1" })).toHaveAttribute("aria-required", "true"); + expect(screen.getByRole("textbox", { name: "2" })).toHaveAttribute("aria-required", "true"); + expect(screen.getByRole("textbox", { name: "3" })).toHaveAttribute("aria-required", "true"); + }); + + test("aria-disabled", () => { + render( `${i}`} disabled />); + + expect(screen.getByRole("textbox", { name: "1" })).toHaveAttribute("aria-disabled", "true"); + expect(screen.getByRole("textbox", { name: "2" })).toHaveAttribute("aria-disabled", "true"); + expect(screen.getByRole("textbox", { name: "3" })).toHaveAttribute("aria-disabled", "true"); + }); + + test("aria-readonly", () => { + render( `${i}`} readOnly />); + + expect(screen.getByRole("textbox", { name: "1" })).toHaveAttribute("aria-readonly", "true"); + expect(screen.getByRole("textbox", { name: "2" })).toHaveAttribute("aria-readonly", "true"); + expect(screen.getByRole("textbox", { name: "3" })).toHaveAttribute("aria-readonly", "true"); + }); +}); diff --git a/src/pin-field/pin-field.tsx b/src/pin-field/pin-field.tsx index 5d6992f..ea925d3 100644 --- a/src/pin-field/pin-field.tsx +++ b/src/pin-field/pin-field.tsx @@ -1,336 +1,442 @@ -import React, { FC, forwardRef, useCallback, useImperativeHandle, useRef } from "react"; - -import keyboardEventPolyfill from "../polyfills/keyboard-evt"; -import { noop, range, omit } from "../utils"; -import { EffectReducer, StateReducer, useBireducer } from "./use-bireducer"; - import { - PinFieldDefaultProps as DefaultProps, - PinFieldInputProps as InputProps, - PinFieldProps as Props, - PinFieldNotifierProps as NotifierProps, - PinFieldState as State, - PinFieldAction as Action, - PinFieldEffect as Effect, -} from "./pin-field.types"; - -export const NO_EFFECTS: Effect[] = []; -export const PROP_KEYS = ["autoFocus", "length", "validate", "format", "formatAriaLabel", "debug"]; -export const HANDLER_KEYS = ["onResolveKey", "onRejectKey", "onChange", "onComplete"]; -export const IGNORED_META_KEYS = ["Alt", "Control", "Enter", "Meta", "Shift", "Tab"]; - -export const defaultProps: DefaultProps = { - ref: { current: [] }, + InputHTMLAttributes, + useEffect, + useReducer, + useRef, + KeyboardEventHandler, + KeyboardEvent, + ChangeEventHandler, + useCallback, + CompositionEventHandler, + forwardRef, + useImperativeHandle, + RefObject, + ActionDispatch, + useMemo, +} from "react"; + +import { noop, range } from "../utils"; + +export const BACKSPACE = 8; +export const DELETE = 46; + +export type InnerProps = { + length: number; + format: (char: string) => string; + formatAriaLabel: (index: number, total: number) => string; + onChange: (value: string) => void; + onComplete: (value: string) => void; +}; + +export const defaultProps: InnerProps = { length: 5, - validate: /^[a-zA-Z0-9]$/, - format: key => key, - formatAriaLabel: (idx: number, codeLength: number) => `pin code ${idx} of ${codeLength}`, - onResolveKey: noop, - onRejectKey: noop, + format: char => char, + formatAriaLabel: (index: number, total: number) => `PIN field ${index} of ${total}`, onChange: noop, onComplete: noop, - debug: false, }; -export function defaultState(props: Pick): State { - return { - focusIdx: 0, - codeLength: props.length, - isKeyAllowed: isKeyAllowed(props.validate), - fallback: null, +export type NativeProps = Omit< + InputHTMLAttributes, + "onChange" | "onKeyDown" | "onCompositionStart" | "onCompositionEnd" +>; + +export const defaultNativeProps: NativeProps = { + type: "text", + inputMode: "text", + autoCapitalize: "off", + autoCorrect: "off", + autoComplete: "off", +}; + +export type Props = NativeProps & + Partial & { + handler?: Handler; }; -} -export function getPrevFocusIdx(currFocusIdx: number) { - return Math.max(0, currFocusIdx - 1); -} +export type StateProps = Pick & Pick; -export function getNextFocusIdx(currFocusIdx: number, lastFocusIdx: number) { - if (lastFocusIdx === 0) return 0; - return Math.min(currFocusIdx + 1, lastFocusIdx - 1); -} +export type State = StateProps & { + cursor: number; + values: string[]; + backspace: boolean; + composition: boolean; + ready: boolean; + dirty: boolean; +}; -export function isKeyAllowed(predicate: DefaultProps["validate"]) { - return (key: string) => { - if (!key) return false; - if (key.length > 1) return false; - if (typeof predicate === "string") return predicate.split("").includes(key); - if (predicate instanceof Array) return predicate.includes(key); - if (predicate instanceof RegExp) return predicate.test(key); - return predicate(key); - }; -} +export const defaultState: State = { + length: defaultProps.length, + format: defaultProps.format, + dir: "ltr", + cursor: 0, + values: Array(defaultProps.length), + backspace: false, + composition: false, + ready: false, + dirty: false, +}; -export function pasteReducer(state: State, idx: number, val: string): [State, Effect[]] { - const areAllKeysAllowed = val.split("").slice(0, state.codeLength).every(state.isKeyAllowed); - - if (!areAllKeysAllowed) { - return [ - state, - [ - { type: "set-input-val", idx: state.focusIdx, val: "" }, - { type: "reject-key", idx, key: val }, - { type: "handle-code-change" }, - ], - ]; - } +export type NoOpAction = { + type: "noop"; +}; - const pasteLen = Math.min(val.length, state.codeLength - state.focusIdx); - const nextFocusIdx = getNextFocusIdx(pasteLen + state.focusIdx - 1, state.codeLength); - const effects: Effect[] = range(0, pasteLen).flatMap(idx => [ - { - type: "set-input-val", - idx: idx + state.focusIdx, - val: val[idx], - }, - { - type: "resolve-key", - idx: idx + state.focusIdx, - key: val[idx], - }, - ]); +export type UpdatePropsAction = { + type: "update-props"; + props: Partial; +}; - if (state.focusIdx !== nextFocusIdx) { - effects.push({ type: "focus-input", idx: nextFocusIdx }); - } +export type HandleCompositionStartAction = { + type: "start-composition"; + index: number; +}; - effects.push({ type: "handle-code-change" }); +export type HandleCompositionEndAction = { + type: "end-composition"; + index: number; + value: string; +}; - return [{ ...state, focusIdx: nextFocusIdx }, effects]; -} +export type HandleKeyChangeAction = { + type: "handle-change"; + index: number; + value: string | null; + reset?: boolean; +}; + +export type HandleKeyDownAction = { + type: "handle-key-down"; + index: number; +} & Partial, "key" | "code" | "keyCode" | "which">>; + +export type Action = + | NoOpAction + | UpdatePropsAction + | HandleCompositionStartAction + | HandleCompositionEndAction + | HandleKeyChangeAction + | HandleKeyDownAction; -export const stateReducer: StateReducer = (state, action) => { +export function reducer(prevState: State, action: Action): State { switch (action.type) { - case "handle-key-down": { - switch (action.key) { - case "Unidentified": - case "Process": { - return [{ ...state, fallback: { idx: state.focusIdx, val: action.val } }, NO_EFFECTS]; - } + case "update-props": { + // merge previous state with action's props + const state = { ...prevState, ...action.props }; - case "Dead": { - return [ - state, - [ - { type: "set-input-val", idx: state.focusIdx, val: "" }, - { type: "reject-key", idx: state.focusIdx, key: action.key }, - { type: "handle-code-change" }, - ], - ]; - } + // adjust cursor in case the new length exceed the previous one + state.cursor = Math.min(state.cursor, state.length - 1); - case "ArrowLeft": { - const prevFocusIdx = getPrevFocusIdx(state.focusIdx); - return [{ ...state, focusIdx: prevFocusIdx }, [{ type: "focus-input", idx: prevFocusIdx }]]; - } + // slice values according to the new length + // + // NOTE: use slice because splice does not keep empty items and + // therefore messes up with values length + state.values = state.values.slice(0, state.cursor + 1); - case "ArrowRight": { - const nextFocusIdx = getNextFocusIdx(state.focusIdx, state.codeLength); - return [{ ...state, focusIdx: nextFocusIdx }, [{ type: "focus-input", idx: nextFocusIdx }]]; - } + // state is now ready + state.ready = true; - case "Delete": - case "Backspace": { - return [state, [{ type: "handle-delete", idx: state.focusIdx }, { type: "handle-code-change" }]]; - } + return state; + } - default: { - if (!state.isKeyAllowed(action.key)) { - return [state, [{ type: "reject-key", idx: state.focusIdx, key: action.key }]]; - } - - const nextFocusIdx = getNextFocusIdx(state.focusIdx, state.codeLength); - return [ - { ...state, focusIdx: nextFocusIdx }, - [ - { type: "set-input-val", idx: state.focusIdx, val: action.key }, - { type: "resolve-key", idx: state.focusIdx, key: action.key }, - { type: "focus-input", idx: nextFocusIdx }, - { type: "handle-code-change" }, - ], - ]; - } + case "start-composition": { + return { ...prevState, dirty: true, composition: true }; + } + + case "end-composition": { + const state: State = { ...prevState }; + + if (action.value) { + state.values[action.index] = action.value; + } else { + delete state.values[action.index]; } + + const dir = state.values[action.index] ? 1 : 0; + state.cursor = Math.min(action.index + dir, state.length - 1); + + state.composition = false; + state.dirty = true; + + return state; } - case "handle-key-up": { - if (!state.fallback) { - return [state, NO_EFFECTS]; + case "handle-change": { + if (prevState.composition) { + break; } - const nextState: State = { ...state, fallback: null }; - const effects: Effect[] = []; - const { idx, val: prevVal } = state.fallback; - const val = action.val; + const state: State = { ...prevState }; - if (prevVal === "" && val === "") { - effects.push({ type: "handle-delete", idx }, { type: "handle-code-change" }); - } else if (val !== "") { - return pasteReducer(nextState, idx, val); + if (action.reset) { + state.values.splice(action.index, state.length); } - return [nextState, effects]; - } + if (action.value) { + const values = action.value.split("").map(state.format); + const length = Math.min(state.length - action.index, values.length); + state.values.splice(action.index, length, ...values.slice(0, length)); + state.cursor = Math.min(action.index + length, state.length - 1); + } else { + delete state.values[action.index]; + const dir = state.backspace ? 0 : 1; + state.cursor = Math.max(0, action.index - dir); + } - case "handle-paste": { - return pasteReducer(state, action.idx, action.val); - } + state.backspace = false; + state.dirty = true; - case "focus-input": { - return [{ ...state, focusIdx: action.idx }, [{ type: "focus-input", idx: action.idx }]]; + return state; } - default: { - return [state, NO_EFFECTS]; + case "handle-key-down": { + // determine if a deletion key is pressed + const fromKey = action.key === "Backspace" || action.key === "Delete"; + const fromCode = action.code === "Backspace" || action.code === "Delete"; + const fromKeyCode = action.keyCode === BACKSPACE || action.keyCode === DELETE; + const fromWhich = action.which === BACKSPACE || action.which === DELETE; + const deletion = fromKey || fromCode || fromKeyCode || fromWhich; + + // return the same state reference if no deletion detected + if (!deletion) { + break; + } + + // Deletion is a bit tricky and requires special attention. + // + // When the field under cusor has a value and a deletion key is + // pressed, we want to let the browser to do the deletion for + // us, like a regular deletion in a normal input via the + // `onchange` event. For this to happen, we need to return the + // same state reference in order not to trigger any change. The + // state will be automatically updated by the handle-change + // action, when the deleted value will trigger the `onchange` + // event. + if (prevState.values[action.index]) { + break; + } + + // But when the field under cursor is empty, deletion cannot + // happen by itself. The trick is to manually move the cursor + // backwards: the browser will then delete the value under this + // new cursor and perform the changes via the triggered + // `onchange` event. + else { + const state: State = { ...prevState }; + + state.cursor = Math.max(0, action.index - 1); + + // let know the handle-change action that we already moved + // backwards and that we don't need to touch the cursor + // anymore + state.backspace = true; + + state.dirty = true; + + return state; + } } + + case "noop": + default: + break; } + + return prevState; +} + +export type Handler = { + refs: RefObject; + state: State; + dispatch: ActionDispatch<[Action]>; + value: string; + setValue: (value: string) => void; }; -export function useEffectReducer({ refs, ...props }: NotifierProps): EffectReducer { - return useCallback( - effect => { - switch (effect.type) { - case "focus-input": { - refs.current[effect.idx].focus(); - break; - } +export function usePinField(): Handler { + const refs = useRef([]); + const [state, dispatch] = useReducer(reducer, defaultState); - case "set-input-val": { - const val = props.format(effect.val); - refs.current[effect.idx].value = val; - break; - } + const value = useMemo(() => { + let value = ""; + for (let index = 0; index < state.length; index++) { + value += index in state.values ? state.values[index] : ""; + } + return value; + }, [state]); - case "resolve-key": { - refs.current[effect.idx].setCustomValidity(""); - props.onResolveKey(effect.key, refs.current[effect.idx]); - break; - } + const setValue = useCallback( + (value: string) => { + dispatch({ type: "handle-change", index: 0, value, reset: true }); + }, + [dispatch, state.cursor], + ); - case "reject-key": { - refs.current[effect.idx].setCustomValidity("Invalid key"); - props.onRejectKey(effect.key, refs.current[effect.idx]); - break; - } + return useMemo( + () => ({ refs, state, dispatch, value, setValue }), + [refs, state, dispatch, value, setValue], + ); +} - case "handle-delete": { - const prevVal = refs.current[effect.idx].value; - refs.current[effect.idx].setCustomValidity(""); - refs.current[effect.idx].value = ""; +export const PinField = forwardRef( + ( + { + length = defaultProps.length, + format = defaultProps.format, + formatAriaLabel = defaultProps.formatAriaLabel, + onChange: handleChange = defaultProps.onChange, + onComplete: handleComplete = defaultProps.onComplete, + handler: customHandler, + autoFocus, + ...nativeProps + }, + fwdRef, + ) => { + const internalHandler = usePinField(); + const { refs, state, dispatch } = customHandler || internalHandler; - if (!prevVal) { - const prevIdx = getPrevFocusIdx(effect.idx); - refs.current[prevIdx].focus(); - refs.current[prevIdx].setCustomValidity(""); - refs.current[prevIdx].value = ""; - } + useImperativeHandle(fwdRef, () => refs.current, [refs]); - break; + function setRefAt(index: number): (ref: HTMLInputElement) => void { + return ref => { + if (ref) { + refs.current[index] = ref; } + }; + } - case "handle-code-change": { - const dir = (props.dir || document.documentElement.getAttribute("dir") || "ltr").toLowerCase(); - const codeArr = refs.current.map(r => r.value.trim()); - const code = (dir === "rtl" ? codeArr.reverse() : codeArr).join(""); - props.onChange(code); - code.length === props.length && props.onComplete(code); - break; - } + function handleKeyDownAt(index: number): KeyboardEventHandler { + return event => { + console.log("keyDown", index, event); + const { key, code, keyCode, which } = event; + dispatch({ type: "handle-key-down", index, key, code, keyCode, which }); + }; + } - default: { - break; + function handleChangeAt(index: number): ChangeEventHandler { + return event => { + if (event.nativeEvent instanceof InputEvent) { + const value = event.nativeEvent.data; + dispatch({ type: "handle-change", index, value }); + } else { + const { value } = event.target; + dispatch({ type: "handle-change", index, value, reset: true }); } - } - }, - [props, refs], - ); -} - -export const PinField: FC = forwardRef((customProps, fwdRef) => { - const props: DefaultProps & InputProps = { ...defaultProps, ...customProps }; - const { autoFocus, formatAriaLabel, length: codeLength } = props; - const inputProps: InputProps = omit([...PROP_KEYS, ...HANDLER_KEYS], props); - const refs = useRef([]); - const effectReducer = useEffectReducer({ refs, ...props }); - const dispatch = useBireducer(stateReducer, effectReducer, defaultState(props))[1]; + }; + } - useImperativeHandle(fwdRef, () => refs.current, [refs]); + function startCompositionAt(index: number): CompositionEventHandler { + return () => { + dispatch({ type: "start-composition", index }); + }; + } - function handleFocus(idx: number) { - return function () { - dispatch({ type: "focus-input", idx }); - }; - } + function endCompositionAt(index: number): CompositionEventHandler { + return event => { + dispatch({ type: "end-composition", index, value: event.data }); + }; + } - function handleKeyDown(idx: number) { - return function (evt: React.KeyboardEvent) { - const key = keyboardEventPolyfill.getKey(evt.nativeEvent); - if ( - !IGNORED_META_KEYS.includes(key) && - !evt.ctrlKey && - !evt.altKey && - !evt.metaKey && - evt.nativeEvent.target instanceof HTMLInputElement - ) { - evt.preventDefault(); - dispatch({ type: "handle-key-down", idx, key, val: evt.nativeEvent.target.value }); + // initial props to state update + useEffect(() => { + if (state.ready) return; + const dir = + nativeProps.dir?.toLowerCase() || + document.documentElement.getAttribute("dir")?.toLowerCase(); + dispatch({ type: "update-props", props: { length, format, dir } }); + }, [state.ready, dispatch, length, format]); + + // props.length to state update + useEffect(() => { + if (!state.ready) return; + if (length === state.length) return; + dispatch({ type: "update-props", props: { length } }); + }, [state.ready, length, state.length, dispatch]); + + // props.format to state update + useEffect(() => { + if (!state.ready) return; + if (format === state.format) return; + dispatch({ type: "update-props", props: { format } }); + }, [state.ready, format, state.format, dispatch]); + + // nativeProps.dir to state update + useEffect(() => { + if (!state.ready) return; + const dir = + nativeProps.dir?.toLowerCase() || + document.documentElement.getAttribute("dir")?.toLowerCase(); + if (dir === state.dir) return; + dispatch({ type: "update-props", props: { dir } }); + }, [state.ready, nativeProps.dir, state.dir, dispatch]); + + // state to view update + useEffect(() => { + if (!refs.current) return; + if (!state.ready) return; + if (!state.dirty) return; + + let innerFocus = false; + let completed = state.values.length == state.length; + let value = ""; + + for (let index = 0; index < state.length; index++) { + const char = index in state.values ? state.values[index] : ""; + refs.current[index].value = char; + innerFocus = innerFocus || hasFocus(refs.current[index]); + completed = completed && index in state.values && refs.current[index].checkValidity(); + value += char; } - }; - } - function handleKeyUp(idx: number) { - return function (evt: React.KeyboardEvent) { - if (evt.nativeEvent.target instanceof HTMLInputElement) { - dispatch({ type: "handle-key-up", idx, val: evt.nativeEvent.target.value }); + if (innerFocus) { + refs.current[state.cursor].focus(); } - }; - } - function handlePaste(idx: number) { - return function (evt: React.ClipboardEvent) { - evt.preventDefault(); - const val = evt.clipboardData.getData("Text"); - dispatch({ type: "handle-paste", idx, val }); - }; - } + if (handleChange) { + handleChange(value); + } - function setRefAtIndex(idx: number) { - return function (ref: HTMLInputElement) { - if (ref) { - refs.current[idx] = ref; + if (handleComplete && completed) { + handleComplete(value); } - }; - } + }, [refs, state, handleChange, handleComplete]); - function hasAutoFocus(idx: number) { - return Boolean(idx === 0 && autoFocus); - } + if (!state.ready) { + return null; + } - return ( - <> - {range(0, codeLength).map(idx => ( - - ))} - - ); -}); + const inputs = range(0, state.length).map(index => ( + + )); + + if (state.dir === "rtl") { + inputs.reverse(); + } + + return inputs; + }, +); + +export function hasFocus(el: HTMLElement): boolean { + try { + const matches = el.webkitMatchesSelector || el.matches; + return matches.call(el, ":focus"); + } catch (err: any) { + return false; + } +} export default PinField; diff --git a/src/pin-field/pin-field.types.ts b/src/pin-field/pin-field.types.ts deleted file mode 100644 index 41129f7..0000000 --- a/src/pin-field/pin-field.types.ts +++ /dev/null @@ -1,45 +0,0 @@ -export type PinFieldInputProps = Omit, "onChange" | "value">; - -export type PinFieldDefaultProps = { - ref: React.Ref; - length: number; - validate: string | string[] | RegExp | ((key: string) => boolean); - format: (char: string) => string; - formatAriaLabel: (idx: number, codeLength: number) => string; - onResolveKey: (key: string, ref?: HTMLInputElement) => any; - onRejectKey: (key: string, ref?: HTMLInputElement) => any; - onChange: (code: string) => void; - onComplete: (code: string) => void; - /** - * @deprecated The debug mode no longer exists. - */ - debug: boolean; -}; - -export type PinFieldProps = Partial & PinFieldInputProps; - -export type PinFieldNotifierProps = { - refs: React.RefObject; -} & PinFieldDefaultProps & - PinFieldInputProps; - -export type PinFieldState = { - focusIdx: number; - codeLength: PinFieldDefaultProps["length"]; - isKeyAllowed: (key: string) => boolean; - fallback: { idx: number; val: string } | null; -}; - -export type PinFieldAction = - | { type: "handle-key-down"; key: string; idx: number; val: string } - | { type: "handle-key-up"; idx: number; val: string } - | { type: "handle-paste"; idx: number; val: string } - | { type: "focus-input"; idx: number }; - -export type PinFieldEffect = - | { type: "focus-input"; idx: number } - | { type: "set-input-val"; idx: number; val: string } - | { type: "resolve-key"; idx: number; key: string } - | { type: "reject-key"; idx: number; key: string } - | { type: "handle-delete"; idx: number } - | { type: "handle-code-change" }; diff --git a/src/pin-field/use-bireducer.spec.tsx b/src/pin-field/use-bireducer.spec.tsx deleted file mode 100644 index 6555263..0000000 --- a/src/pin-field/use-bireducer.spec.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import "@testing-library/jest-dom"; - -import React from "react"; -import { fireEvent, render, screen } from "@testing-library/react"; - -import { EffectReducer, StateReducer, useBireducer } from "./use-bireducer"; - -type State = { - count: number; -}; - -type Action = { type: "update"; value: number } | { type: "reset" }; - -type Effect = { type: "log"; value: string } | { type: "backup"; count: number }; - -const stateReducer: StateReducer = (state, action) => { - switch (action.type) { - case "update": { - return [{ count: action.value }, [{ type: "log", value: `set counter ${action.value}` }]]; - } - case "reset": { - return [ - state, - [ - { type: "log", value: "reset counter" }, - { type: "backup", count: state.count }, - ], - ]; - } - } -}; - -const effectReducer: EffectReducer = (effect, dispatch) => { - switch (effect.type) { - case "log": { - console.log(effect.value); - return; - } - case "backup": { - localStorage.setItem("backup", String(effect.count)); - dispatch({ type: "update", value: 0 }); - return () => { - localStorage.clear(); - }; - } - } -}; - -describe.skip("useBireducer", () => { - beforeAll(() => { - global.Storage.prototype.setItem = jest.fn(); - global.Storage.prototype.clear = jest.fn(); - global.console.log = jest.fn(); - }); - - beforeEach(() => { - jest.restoreAllMocks(); - }); - - afterAll(() => { - jest.resetAllMocks(); - }); - - it("should work", () => { - function TestComponent() { - const [state, dispatch] = useBireducer(stateReducer, effectReducer, { - count: 0, - }); - - return ( - <> - {state.count} - - - - - ); - } - - const { unmount } = render(); - expect(screen.getByTestId("counter")).toHaveTextContent("0"); - - fireEvent.click(screen.getByTestId("increment")); - fireEvent.click(screen.getByTestId("increment")); - expect(screen.getByTestId("counter")).toHaveTextContent("2"); - expect(console.log).toHaveBeenNthCalledWith(1, "set counter 1"); - expect(console.log).toHaveBeenNthCalledWith(2, "set counter 2"); - - fireEvent.click(screen.getByTestId("decrement")); - expect(screen.getByTestId("counter")).toHaveTextContent("1"); - expect(console.log).toHaveBeenNthCalledWith(3, "set counter 1"); - - fireEvent.click(screen.getByTestId("reset")); - expect(screen.getByTestId("counter")).toHaveTextContent("0"); - expect(console.log).toHaveBeenNthCalledWith(4, "reset counter"); - expect(console.log).toHaveBeenNthCalledWith(5, "set counter 0"); - expect(localStorage.setItem).toHaveBeenNthCalledWith(1, "backup", "1"); - expect(localStorage.clear).not.toHaveBeenCalled(); - - unmount(); - expect(localStorage.clear).toHaveBeenCalledTimes(1); - }); -}); diff --git a/src/pin-field/use-bireducer.ts b/src/pin-field/use-bireducer.ts deleted file mode 100644 index 45e5904..0000000 --- a/src/pin-field/use-bireducer.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Dispatch, useCallback, useEffect, useReducer, useRef, useState } from "react"; - -export type StateReducer = (state: S, action: A) => [S, E[]]; - -export type EffectReducer = (effect: E, dispatch: Dispatch) => EffectCleanup | void; -export type EffectCleanup = () => void; - -export function useBireducer( - stateReducer: StateReducer, - effectReducer: EffectReducer, - defaultState: S, -): [S, Dispatch] { - const [effects, setEffects] = useState([]); - const cleanups = useRef([]); - - const reducer = useCallback( - (state: S, action: A): S => { - const [nextState, nextEffects] = stateReducer(state, action); - setEffects(effects => [...nextEffects.reverse(), ...effects]); - return nextState; - }, - [stateReducer], - ); - - const [state, dispatch] = useReducer(reducer, defaultState); - - useEffect(() => { - const effect = effects.pop(); - if (effect) { - const cleanup = effectReducer(effect, dispatch); - if (cleanup) cleanups.current.push(cleanup); - setEffects([...effects]); - } - }, [effects, effectReducer]); - - useEffect(() => { - return () => { - cleanups.current.forEach(cleanup => { - cleanup(); - }); - }; - }, []); - - return [state, dispatch]; -} - -export default { useBireducer }; diff --git a/src/polyfills/index.ts b/src/polyfills/index.ts deleted file mode 100644 index fead5ee..0000000 --- a/src/polyfills/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./keyboard-evt"; diff --git a/src/polyfills/keyboard-evt.test.ts b/src/polyfills/keyboard-evt.test.ts deleted file mode 100644 index ee1aeae..0000000 --- a/src/polyfills/keyboard-evt.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { getKeyFromKeyboardEvent } from "."; - -describe.skip("keyboard-evt", () => { - test.skip("getKey", () => { - const cases: Array<[KeyboardEventInit, string]> = [ - [{}, "Unidentified"], - [{ key: "a" }, "a"], - [{ which: 65, shiftKey: false }, "a"], - [{ keyCode: 65, shiftKey: true }, "A"], - ]; - - cases.forEach(([opts, expected]) => { - const evt = new KeyboardEvent("keydown", opts); - expect(getKeyFromKeyboardEvent(evt)).toEqual(expected); - }); - }); -}); diff --git a/src/polyfills/keyboard-evt.ts b/src/polyfills/keyboard-evt.ts deleted file mode 100644 index f2c1a0d..0000000 --- a/src/polyfills/keyboard-evt.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * KeyboardEvent.key polyfill. - * - * @see https://github.com/soywod/keyboardevent-key-polyfill/blob/master/index.js - */ - -const keyMap: { [key: number]: string | [string, string] } = { - 3: "Cancel", - 6: "Help", - 8: "Backspace", - 9: "Tab", - 12: "Clear", - 13: "Enter", - 16: "Shift", - 17: "Control", - 18: "Alt", - 19: "Pause", - 20: "CapsLock", - 27: "Escape", - 28: "Convert", - 29: "NonConvert", - 30: "Accept", - 31: "ModeChange", - 32: " ", - 33: "PageUp", - 34: "PageDown", - 35: "End", - 36: "Home", - 37: "ArrowLeft", - 38: "ArrowUp", - 39: "ArrowRight", - 40: "ArrowDown", - 41: "Select", - 42: "Print", - 43: "Execute", - 44: "PrintScreen", - 45: "Insert", - 46: "Delete", - 48: ["0", ")"], - 49: ["1", "!"], - 50: ["2", "@"], - 51: ["3", "#"], - 52: ["4", "$"], - 53: ["5", "%"], - 54: ["6", "^"], - 55: ["7", "&"], - 56: ["8", "*"], - 57: ["9", "("], - 91: "OS", - 93: "ContextMenu", - 106: "*", - 107: "+", - 109: "-", - 110: ".", - 111: "/", - 144: "NumLock", - 145: "ScrollLock", - 181: "VolumeMute", - 182: "VolumeDown", - 183: "VolumeUp", - 186: [";", ":"], - 187: ["=", "+"], - 188: [",", "<"], - 189: ["-", "_"], - 190: [".", ">"], - 191: ["/", "?"], - 192: ["`", "~"], - 219: ["[", "{"], - 220: ["\\", "|"], - 221: ["]", "}"], - 222: ["'", '"'], - 224: "Meta", - 225: "AltGraph", - 246: "Attn", - 247: "CrSel", - 248: "ExSel", - 249: "EraseEof", - 250: "Play", - 251: "ZoomOut", -}; - -// Function keys (F1-24). - -let i: number; -for (i = 1; i < 25; i += 1) { - keyMap[111 + i] = "F" + i; -} - -// Printable ASCII characters. - -for (i = 65; i < 91; i += 1) { - const letter = String.fromCharCode(i); - keyMap[i] = [letter.toLowerCase(), letter.toUpperCase()]; -} - -// Numbers on numeric keyboard. - -for (i = 96; i < 106; i += 1) { - keyMap[i] = String.fromCharCode(i - 48); -} - -/** - * Find the key associated to a keyboard event. - * Default to "Unidentified". - */ -export function getKeyFromKeyboardEvent(evt: KeyboardEvent): string { - if (evt.key && evt.key !== "Unidentified") { - return evt.key; - } - - const key = keyMap[evt.which || evt.keyCode] || "Unidentified"; - - if (Array.isArray(key)) { - return key[+(evt.shiftKey || 0)]; - } - - return key; -} - -export default { - getKey: getKeyFromKeyboardEvent, -}; diff --git a/src/utils/utils.test.ts b/src/utils/utils.test.ts index d9128a1..4b44de0 100644 --- a/src/utils/utils.test.ts +++ b/src/utils/utils.test.ts @@ -1,4 +1,4 @@ -import { noop, range, omit } from "./utils"; +import { noop, range } from "./utils"; test("noop", () => { expect(noop()).toEqual(undefined); @@ -10,10 +10,3 @@ test("range", () => { expect(range(3, 0)).toEqual([]); expect(range(3, 3)).toEqual([3, 4, 5]); }); - -test("omit", () => { - expect(omit([], { a: 0, b: 1 })).toEqual({ a: 0, b: 1 }); - expect(omit(["a"], { a: 0, b: 1 })).toEqual({ b: 1 }); - expect(omit(["b"], { a: 0, b: 1 })).toEqual({ a: 0 }); - expect(omit(["a", "b"], { a: 0, b: 1 })).toEqual({}); -}); diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 991f41d..afef0c7 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -3,15 +3,3 @@ export function noop(): void {} export function range(start: number, length: number): number[] { return Array.from({ length }, (_, i) => i + start); } - -export function omit>(keys: string[], input: T): T { - let output: T = Object.create({}); - - for (let key in input) { - if (!keys.includes(key)) { - Object.assign(output, { [key]: input[key] }); - } - } - - return output; -} diff --git a/vite.config.ts b/vite.config.js similarity index 86% rename from vite.config.ts rename to vite.config.js index 391dce1..f4ac9cc 100644 --- a/vite.config.ts +++ b/vite.config.js @@ -1,9 +1,9 @@ import { resolve } from "path"; -import type { UserConfig } from "vite"; +import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import dts from "vite-plugin-dts"; -export default { +export default defineConfig({ plugins: [react(), dts()], build: { lib: { @@ -24,4 +24,4 @@ export default { }, }, }, -} satisfies UserConfig; +}); From c36c2970d7ad0a4c8a68513d86b7dcbd598b9ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Tue, 7 Jan 2025 10:12:16 +0100 Subject: [PATCH 10/13] update changelog --- .prettierrc.json | 8 -------- CHANGELOG.md | 27 ++++++++++++++++++++++++++- CONTRIBUTING.md | 6 +++--- LICENSE | 2 +- README.md | 16 ++++++++++------ prettier.config.js | 12 ++++++++++++ 6 files changed, 52 insertions(+), 19 deletions(-) delete mode 100644 .prettierrc.json create mode 100644 prettier.config.js diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index 280dd65..0000000 --- a/.prettierrc.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "arrowParens": "avoid", - "bracketSpacing": true, - "printWidth": 100, - "semi": true, - "singleQuote": false, - "trailingComma": "all" -} diff --git a/CHANGELOG.md b/CHANGELOG.md index c38e48d..0205c5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,27 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + +- Added React 19 support [#93]. +- Added support for [key composnition](https://developer.mozilla.org/en-US/docs/Web/API/Element/compositionstart_event). Only the final, composed key is now added to the field. +- Added `usePinField()` custom hook to use the controlled version of the PIN field. See the controlled section of the live [demo](https://soywod.github.io/react-pin-field/?path=/docs/pinfield--docs#controlled). + +### Changed + +- Changed the way validation works. It is more compliant with HTML standards: invalid characters can be entered but validation will fail (and `onComplete` will not trigger). +- Replaced the hand-made demo with [Storybook](https://storybook.js.org/). +- Bump all dependencies to the latest. + +### Fixed + +- Fixed wrong behaviour in React [``](https://react.dev/reference/react/StrictMode) [#91]. + +### Removed + +- Removed property `validate` in favour of the standard HTML [`pattern`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/pattern) attribute. +- Removed `onResolveKey` and `onRejectKey` properties in favour of standard HTML validation, including `onInvalid`. + ## [3.1.5] - 2024-01-05 ### Changed @@ -169,7 +190,9 @@ React PIN Field is now a React wrapper for [PIN Field](https://github.com/soywod - Fixed unnecessary re-renders (useMVU). - Fixed paste on MacOS [#13]. -[unreleased]: https://github.com/soywod/react-pin-field/compare/v3.1.3...HEAD +[unreleased]: https://github.com/soywod/react-pin-field/compare/v3.1.5...HEAD +[3.1.5]: https://github.com/soywod/react-pin-field/compare/v3.1.4...v3.1.5 +[3.1.4]: https://github.com/soywod/react-pin-field/compare/v3.1.3...v3.1.4 [3.1.3]: https://github.com/soywod/react-pin-field/compare/v3.1.2...v3.1.3 [3.1.2]: https://github.com/soywod/react-pin-field/compare/v3.1.1...v3.1.2 [3.1.1]: https://github.com/soywod/react-pin-field/compare/v3.1.0...v3.1.1 @@ -207,3 +230,5 @@ React PIN Field is now a React wrapper for [PIN Field](https://github.com/soywod [#63]: https://github.com/soywod/react-pin-field/issues/63 [#71]: https://github.com/soywod/react-pin-field/issues/71 [#84]: https://github.com/soywod/react-pin-field/pull/84 +[#91]: https://github.com/soywod/react-pin-field/issues/91 +[#93]: https://github.com/soywod/react-pin-field/issues/93 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 693d02a..cdae26c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,9 +9,9 @@ Running `nix-shell` will spawn a shell with everything you need to get started w If you do not want to use Nix, you can just install manually the following dependencies: -- [Node.js](https://nodejs.org/en) (`v20.18`) -- [Yarn](https://yarnpkg.com/) (`v1.22`) -- [Cypress](https://www.cypress.io/) (`v13.13.2`) +- [Node.js](https://nodejs.org/en): `v20.18` +- [Yarn](https://yarnpkg.com/): `v1.22` +- [Cypress](https://www.cypress.io/): `v13.13.2` ## Installation diff --git a/LICENSE b/LICENSE index 862a1f2..9ae9997 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019-2024 soywod +Copyright (c) 2019-2025 soywod Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 4719780..36ab6b4 100644 --- a/README.md +++ b/README.md @@ -11,19 +11,17 @@ React component for entering PIN codes - Written in TypeScript, tested with Jest and Cypress - Relies on `onchange` native event to improve browsers compatibility - Supports HTML [`dir`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/dir) left-to-right and right-to-left -- Supports HTML [`autofocus`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autofocus) by focusing either the first or the last input, depending on [`dir`] -- Supports [ARIA](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA) attributes -- Handles [key composition](https://developer.mozilla.org/en-US/docs/Web/API/Element/compositionstart_event) +- Supports HTML [`autofocus`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autofocus) by focusing either the first or the last input, depending on `dir` +- Supports [ARIA](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA) attributes like `aria-required`, `aria-label`… +- Works well with [key composition](https://developer.mozilla.org/en-US/docs/Web/API/Element/compositionstart_event) ## Installation -### Using npm - ``` npm install react-pin-field ``` -### Using yarn +Alternatively, using [Yarn](https://yarnpkg.com/): ``` yarn add react-pin-field @@ -119,6 +117,12 @@ const handler = usePinField(); return ``` +*See the controlled section of the live [demo](https://soywod.github.io/react-pin-field/?path=/docs/pinfield--docs#controlled).* + +## Contributing + +*See the [Contributing guide](./CONTRIBUTING.md).* + ## Sponsoring If you appreciate the project, feel free to donate using one of the following providers: diff --git a/prettier.config.js b/prettier.config.js new file mode 100644 index 0000000..259b764 --- /dev/null +++ b/prettier.config.js @@ -0,0 +1,12 @@ +/** + * @see https://prettier.io/docs/en/configuration.html + * @type {import("prettier").Config} + */ +export default { + arrowParens: "avoid", + bracketSpacing: true, + printWidth: 100, + semi: true, + singleQuote: false, + trailingComma: "all", +}; From 484dda0cf75983fbb99cf98d499967c15e338dd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Tue, 7 Jan 2025 10:33:38 +0100 Subject: [PATCH 11/13] add demo gh action to publish storybook --- .github/workflows/demo.yml | 28 ++++++++++++++++++++++++++++ .gitignore | 1 - package.json | 2 +- 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/demo.yml diff --git a/.github/workflows/demo.yml b/.github/workflows/demo.yml new file mode 100644 index 0000000..d80dbc7 --- /dev/null +++ b/.github/workflows/demo.yml @@ -0,0 +1,28 @@ +name: demo + +on: + push: + branches: + - state-refactor + +permissions: + contents: write + +jobs: + demo: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + with: + path: node_modules + key: yarn-${{ hashFiles('yarn.lock') }} + - uses: cachix/install-nix-action@v27 + with: + nix_path: nixpkgs=channel:nixos-24.11 + enable_kvm: true + - run: nix-shell --run 'yarn install --frozen-lockfile --immutable' + - run: nix-shell --run 'yarn storybook:build' + - uses: jamesives/github-pages-deploy-action@v4 + with: + folder: dist diff --git a/.gitignore b/.gitignore index fcf54c1..4027782 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,3 @@ yarn-debug.log* yarn-error.log* *storybook.log -storybook-static/ \ No newline at end of file diff --git a/package.json b/package.json index ddf301b..ee9510e 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,6 @@ "test:e2e": "cypress run", "test": "run-p test:unit test:e2e", "build": "vite build", - "storybook:build": "storybook build" + "storybook:build": "storybook build -o dist" } } From cd89c6620fbfa706d427711f329bc7162d02375d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Tue, 7 Jan 2025 10:39:31 +0100 Subject: [PATCH 12/13] remove autofocus on storybook to prevent auto scroll down --- src/pin-field/pin-field.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pin-field/pin-field.stories.tsx b/src/pin-field/pin-field.stories.tsx index 3395c37..d9d0942 100644 --- a/src/pin-field/pin-field.stories.tsx +++ b/src/pin-field/pin-field.stories.tsx @@ -149,7 +149,7 @@ export const HTMLInputAttributes: StoryObj Date: Tue, 7 Jan 2025 11:02:00 +0100 Subject: [PATCH 13/13] fix publish gh workflow --- .github/workflows/demo.yml | 2 +- .github/workflows/publish.yml | 81 ++++++----------------------------- 2 files changed, 13 insertions(+), 70 deletions(-) diff --git a/.github/workflows/demo.yml b/.github/workflows/demo.yml index d80dbc7..438793f 100644 --- a/.github/workflows/demo.yml +++ b/.github/workflows/demo.yml @@ -3,7 +3,7 @@ name: demo on: push: branches: - - state-refactor + - master permissions: contents: write diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b684180..ae6f64c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,82 +1,25 @@ name: publish -permissions: - contents: write - on: push: tags: - v* jobs: - install: - name: Installs and configures nix + publish: runs-on: ubuntu-latest steps: - - name: Checkouts code - uses: actions/checkout@v2 - - - name: Sets up Node for registry only - uses: actions/setup-node@v3 - with: - node-version: '18.17' - registry-url: 'https://registry.npmjs.org' - - - name: Caches Nix store - uses: actions/cache@v3 - id: nix-cache - with: - path: /tmp/nix-cache - key: nix-${{ hashFiles('**/flake.*') }} - - - name: Caches Node modules - uses: actions/cache@v3 + - uses: actions/checkout@v4 + - uses: actions/cache@v4 with: - path: '**/node_modules' - key: yarn-${{ hashFiles('**/yarn.lock') }} - - - name: Installs Nix - uses: cachix/install-nix-action@v22 + path: node_modules + key: yarn-${{ hashFiles('yarn.lock') }} + - uses: cachix/install-nix-action@v27 with: - nix_path: nixpkgs=channel:nixos-23.05 - extra_nix_config: | - experimental-features = nix-command flakes - access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} - - - name: Imports Nix store cache - if: ${{ steps.nix-cache.outputs.cache-hit == 'true' }} - run: nix-store --import < /tmp/nix-cache - - - name: Installs deps - run: nix develop -c yarn install --frozen-lockfile --immutable - - - name: Exports Nix store cache - if: ${{ steps.nix-cache.outputs.cache-hit != 'true' }} - run: nix-store --export $(find /nix/store -maxdepth 1 -name '*-*') > /tmp/nix-cache - - - name: Builds lib - working-directory: lib - run: nix develop -c yarn build - - - name: Copies README to the lib folder - run: cp README.md LICENSE lib/ - - - name: Publishes the lib - working-directory: lib - run: | - nix develop --impure - yarn publish --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - - name: Builds the demo - working-directory: demo - run: nix develop -c yarn build + nix_path: nixpkgs=channel:nixos-24.11 + enable_kvm: true + - run: nix-shell --run 'yarn install --frozen-lockfile --immutable' + - run: nix-shell --run 'yarn build' + - run: nix-shell --run 'yarn publish --access public' env: - PUBLIC_URL: /react-pin-field - - - name: Deploys the demo - uses: jamesives/github-pages-deploy-action@v4.4.1 - with: - branch: gh-pages - folder: demo/build + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}