Implement sub-iterators for list navigation (#2155)

We recently introduced shortcut-based list navigation that ran into problems when the list contained items that were loaded asynchronously but had stand at the beginning of the list. As the system is based on render order, we had to introduce a new layer in-between the main iterator and the asynchronously loaded items. This new layer is called a "sub-iterator", which functions literally like the main iterator with the exception that it can also be used as an item of a parent-iterator. This allows for more complicated use-cases and the support of keyboard iterators around extension points. This pull request also fixes an unreleased regression where usages of the deprecated confirmAlert function caused a nullpointer exception.
This commit is contained in:
Konstantin Schaper
2022-11-16 11:01:27 +01:00
committed by GitHub
parent 3e74985203
commit 4a556dda8b
8 changed files with 546 additions and 82 deletions

View File

@@ -26,4 +26,15 @@ import React from "react";
export type ModalStateContextType = { value: number; increment: () => void; decrement: () => void };
export default React.createContext<ModalStateContextType>({} as ModalStateContextType);
export default React.createContext<ModalStateContextType>({
value: 0,
increment: () => {
// eslint-disable-next-line no-console
console.warn(
"Modals should be declared inside a ModalStateContext. Did you use the deprecated 'confirmAlert' function over the 'ConfirmAlert' component ?"
);
},
decrement: () => {
/* Do nothing */
},
});

View File

@@ -30,7 +30,8 @@
"@scm-manager/eslint-config": "^2.16.0",
"@scm-manager/tsconfig": "^2.13.0",
"@testing-library/react-hooks": "8.0.1",
"@testing-library/react": "12.1.5"
"@testing-library/react": "12.1.5",
"jest-extended": "3.1.0"
},
"babel": {
"presets": [
@@ -43,5 +44,8 @@
},
"publishConfig": {
"access": "public"
},
"jest": {
"setupFilesAfterEnv": ["jest-extended/all"]
}
}

View File

@@ -25,4 +25,4 @@
export { default as useShortcut } from "./useShortcut";
export { default as useShortcutDocs, ShortcutDocsContextProvider } from "./useShortcutDocs";
export { default as usePauseShortcuts } from "./usePauseShortcuts";
export { useKeyboardIteratorTarget, KeyboardIterator } from "./iterator/keyboardIterator";
export { useKeyboardIteratorTarget, KeyboardIterator, KeyboardSubIterator } from "./iterator/keyboardIterator";

View File

@@ -0,0 +1,220 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { MutableRefObject, useMemo, useRef } from "react";
const INACTIVE_INDEX = -1;
export type Callback = () => void;
type Direction = "forward" | "backward";
/**
* Restricts the api surface exposed by {@link CallbackIterator} so that we do not have to implement
* the whole class when providing a default context.
*/
export type CallbackRegistry = {
/**
* Registers the given item and returns its index to use in {@link deregister}.
*/
register: (item: Callback | CallbackIterator) => number;
/**
* Use the index returned from {@link register} to de-register.
*/
deregister: (index: number) => void;
};
const isSubiterator = (item?: Callback | CallbackIterator): item is CallbackIterator =>
item instanceof CallbackIterator;
const offset = (direction: Direction) => (direction === "forward" ? 1 : -1);
/**
* ## Definition
* - A list of callback functions and/or recursively nested iterators
* - The iterator can move in either direction
* - New items can be added/removed on-the-fly
*
* ## Terminology
* - Item: Either a callback or a nested iterator
* - Available: Item is a non-empty iterator OR a regular callback
* - Inactive: Current index is -1
* - Activate: Move iterator while in inactive state OR call regular callback
*
* ## Moving
* When an iterator is moved in either direction, there are 4 cases:
*
* 1. The iterator is inactive => activate item at first available index from given direction
* 1. The current item is a sub-iterator with more items in the given direction => move the sub-iterator
* 1. The current item is a sub-iterator that has reached its bounds in the given direction => reset sub-iterator & activate item at next available index
* 1. The current item is not a sub-iterator => activate item at next available index
*/
export class CallbackIterator implements CallbackRegistry {
private parent?: CallbackIterator;
constructor(
private readonly activeIndexRef: MutableRefObject<number>,
private readonly itemsRef: MutableRefObject<Array<Callback | CallbackIterator>>
) {}
private get activeIndex() {
return this.activeIndexRef.current;
}
private set activeIndex(newValue: number) {
this.activeIndexRef.current = newValue;
}
private get items() {
return this.itemsRef.current;
}
private get currentItem(): Callback | CallbackIterator | undefined {
return this.items[this.activeIndex];
}
private get isInactive() {
return this.activeIndex === INACTIVE_INDEX;
}
private get lastIndex() {
return this.items.length - 1;
}
private firstIndex = (direction: "forward" | "backward") => {
return direction === "forward" ? 0 : this.lastIndex;
};
private firstAvailableIndex = (direction: Direction, fromIndex = this.firstIndex(direction)) => {
for (; direction === "forward" ? fromIndex < this.items.length : fromIndex >= 0; fromIndex += offset(direction)) {
const callback = this.items[fromIndex];
if (callback) {
if (!isSubiterator(callback) || callback.hasNext(direction)) {
return fromIndex;
}
}
}
return null;
};
private hasAvailableIndex = (direction: Direction, fromIndex?: number) => {
return this.firstAvailableIndex(direction, fromIndex) !== null;
};
private activateCurrentItem = (direction: Direction) => {
if (isSubiterator(this.currentItem)) {
this.currentItem.move(direction);
} else if (this.currentItem) {
this.currentItem();
}
};
private setIndexAndActivateCurrentItem = (index: number | null, direction: Direction) => {
if (index !== null && index !== INACTIVE_INDEX) {
this.activeIndex = index;
this.activateCurrentItem(direction);
}
};
private move = (direction: Direction) => {
if (isSubiterator(this.currentItem) && this.currentItem.hasNext(direction)) {
this.currentItem.move(direction);
} else {
if (isSubiterator(this.currentItem)) {
this.currentItem.reset();
}
let nextIndex: number | null;
if (this.isInactive) {
nextIndex = this.firstAvailableIndex(direction);
} else {
nextIndex = this.firstAvailableIndex(direction, this.activeIndex + offset(direction));
}
this.setIndexAndActivateCurrentItem(nextIndex, direction);
}
};
private hasNext = (inDirection: Direction): boolean => {
if (this.isInactive) {
return this.hasAvailableIndex(inDirection);
}
if (isSubiterator(this.currentItem) && this.currentItem.hasNext(inDirection)) {
return true;
}
return this.hasAvailableIndex(inDirection, this.activeIndex + offset(inDirection));
};
public next = () => {
if (this.hasNext("forward")) {
return this.move("forward");
}
};
public previous = () => {
if (this.hasNext("backward")) {
return this.move("backward");
}
};
public reset = () => {
this.activeIndex = INACTIVE_INDEX;
for (const cb of this.items) {
if (isSubiterator(cb)) {
cb.reset();
}
}
};
public register = (item: Callback | CallbackIterator) => {
if (isSubiterator(item)) {
item.parent = this;
}
return this.items.push(item) - 1;
};
public deregister = (index: number) => {
this.items.splice(index, 1);
if (this.activeIndex === index || this.activeIndex >= this.items.length) {
if (this.hasAvailableIndex("backward", index)) {
this.setIndexAndActivateCurrentItem(this.firstAvailableIndex("backward", index), "backward");
} else if (this.hasAvailableIndex("forward", index)) {
this.setIndexAndActivateCurrentItem(this.firstAvailableIndex("forward", index), "backward");
} else if (this.parent) {
if (this.parent.hasNext("forward")) {
this.parent.move("forward");
} else if (this.parent.hasNext("backward")) {
this.parent.move("backward");
}
} else {
this.reset();
}
}
};
}
export const useCallbackIterator = (initialIndex = INACTIVE_INDEX) => {
const items = useRef<Array<Callback | CallbackIterator>>([]);
const activeIndex = useRef<number>(initialIndex);
return useMemo(() => new CallbackIterator(activeIndex, items), [activeIndex, items]);
};

View File

@@ -24,10 +24,16 @@
import { renderHook } from "@testing-library/react-hooks";
import React, { FC } from "react";
import { KeyboardIteratorContextProvider, useKeyboardIteratorCallback } from "./keyboardIterator";
import {
KeyboardIteratorContextProvider,
KeyboardSubIterator,
KeyboardSubIteratorContextProvider,
useKeyboardIteratorItem,
} from "./keyboardIterator";
import { render } from "@testing-library/react";
import { ShortcutDocsContextProvider } from "../useShortcutDocs";
import Mousetrap from "mousetrap";
import "jest-extended";
jest.mock("react-i18next", () => ({
useTranslation: () => [jest.fn()],
@@ -49,7 +55,7 @@ const createWrapper =
<Wrapper initialIndex={initialIndex}>{children}</Wrapper>;
const Item: FC<{ callback: () => void }> = ({ callback }) => {
useKeyboardIteratorCallback(callback);
useKeyboardIteratorItem(callback);
return <li>example</li>;
};
@@ -69,7 +75,7 @@ describe("shortcutIterator", () => {
it("should not call callback upon registration", () => {
const callback = jest.fn();
renderHook(() => useKeyboardIteratorCallback(callback), {
renderHook(() => useKeyboardIteratorItem(callback), {
wrapper: Wrapper,
});
@@ -79,7 +85,7 @@ describe("shortcutIterator", () => {
it("should not throw if not inside keyboard iterator context", () => {
const callback = jest.fn();
const { result, unmount } = renderHook(() => useKeyboardIteratorCallback(callback), {
const { result, unmount } = renderHook(() => useKeyboardIteratorItem(callback), {
wrapper: DocsWrapper,
});
@@ -203,6 +209,26 @@ describe("shortcutIterator", () => {
expect(callback3).not.toHaveBeenCalled();
});
it("should move to existing index when active index in the middle is deregistered", async () => {
const callback = jest.fn();
const callback2 = jest.fn();
const callback3 = jest.fn();
const { rerender } = render(<List callbacks={[callback, callback2, callback3]} />, {
wrapper: createWrapper(1),
});
expect(callback).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
rerender(<List callbacks={[callback, callback3]} />);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback2).not.toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
});
it("should not move on deregistration if iterator is not active", async () => {
const callback = jest.fn();
const callback2 = jest.fn();
@@ -236,4 +262,170 @@ describe("shortcutIterator", () => {
expect(callback).not.toHaveBeenCalled();
});
describe("With Subiterator", () => {
it("should call in correct order", () => {
const callback = jest.fn();
const callback2 = jest.fn();
const callback3 = jest.fn();
render(
<Wrapper>
<KeyboardSubIterator>
<List callbacks={[callback, callback2]} />
</KeyboardSubIterator>
<List callbacks={[callback3]} />
</Wrapper>
);
Mousetrap.trigger("j");
Mousetrap.trigger("j");
Mousetrap.trigger("j");
expect(callback).toHaveBeenCalledTimes(1);
expect(callback2).toHaveBeenCalledTimes(1);
expect(callback3).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledBefore(callback2);
expect(callback2).toHaveBeenCalledBefore(callback3);
});
it("should call first target that is not an empty subiterator", () => {
const callback = jest.fn();
const callback2 = jest.fn();
const callback3 = jest.fn();
render(
<Wrapper>
<KeyboardSubIterator>
<List callbacks={[]} />
</KeyboardSubIterator>
<KeyboardSubIterator>
<List callbacks={[]} />
</KeyboardSubIterator>
<List callbacks={[callback, callback2, callback3]} />
</Wrapper>
);
Mousetrap.trigger("j");
expect(callback).toHaveBeenCalledTimes(1);
expect(callback2).not.toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
});
it("should skip empty sub-iterators during navigation", () => {
const callback = jest.fn();
const callback2 = jest.fn();
const callback3 = jest.fn();
render(
<Wrapper>
<List callbacks={[callback]} />
<KeyboardSubIterator>
<List callbacks={[]} />
</KeyboardSubIterator>
<KeyboardSubIterator>
<List callbacks={[]} />
</KeyboardSubIterator>
<List callbacks={[callback2, callback3]} />
</Wrapper>
);
Mousetrap.trigger("j");
Mousetrap.trigger("j");
Mousetrap.trigger("j");
expect(callback).toHaveBeenCalledTimes(1);
expect(callback2).toHaveBeenCalledTimes(1);
expect(callback3).toHaveBeenCalledTimes(1);
});
it("should not enter subiterator if its empty", () => {
const callback = jest.fn();
const callback2 = jest.fn();
const callback3 = jest.fn();
render(
<Wrapper initialIndex={1}>
<KeyboardSubIterator>
<List callbacks={[]} />
</KeyboardSubIterator>
<List callbacks={[callback, callback2, callback3]} />
</Wrapper>
);
Mousetrap.trigger("k");
expect(callback).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
});
it("should not loop", () => {
const callback = jest.fn();
const callback2 = jest.fn();
const callback3 = jest.fn();
render(
<Wrapper initialIndex={1}>
<KeyboardSubIterator>
<List callbacks={[callback, callback2]} />
</KeyboardSubIterator>
<List callbacks={[callback3]} />
</Wrapper>
);
Mousetrap.trigger("k");
Mousetrap.trigger("k");
Mousetrap.trigger("k");
Mousetrap.trigger("k");
Mousetrap.trigger("k");
Mousetrap.trigger("k");
expect(callback3).not.toHaveBeenCalled();
expect(callback2).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback2).toHaveBeenCalledBefore(callback);
});
it("should move subiterator if its active callback is de-registered", () => {
const callback = jest.fn();
const callback2 = jest.fn();
const callback3 = jest.fn();
const callback4 = jest.fn();
const { rerender } = render(
<>
<KeyboardSubIteratorContextProvider initialIndex={1}>
<List callbacks={[callback, callback2, callback3]} />
</KeyboardSubIteratorContextProvider>
<List callbacks={[callback4]} />
</>,
{
wrapper: createWrapper(0),
}
);
expect(callback).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
expect(callback4).not.toHaveBeenCalled();
rerender(
<>
<KeyboardSubIteratorContextProvider initialIndex={1}>
<List callbacks={[callback, callback3]} />
</KeyboardSubIteratorContextProvider>
<List callbacks={[callback4]} />
</>
);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback2).not.toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
expect(callback4).not.toHaveBeenCalled();
});
});
});

View File

@@ -22,18 +22,12 @@
* SOFTWARE.
*/
import React, { FC, useCallback, useContext, useEffect, useMemo, useRef } from "react";
import React, { FC, useCallback, useContext, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { useShortcut } from "../index";
import { Callback, CallbackIterator, CallbackRegistry, useCallbackIterator } from "./callbackIterator";
type Callback = () => void;
type KeyboardIteratorContextType = {
register: (callback: Callback) => number;
deregister: (index: number) => void;
};
const KeyboardIteratorContext = React.createContext<KeyboardIteratorContextType>({
const KeyboardIteratorContext = React.createContext<CallbackRegistry>({
register: () => {
if (process.env.NODE_ENV === "development") {
// eslint-disable-next-line no-console
@@ -49,77 +43,45 @@ const KeyboardIteratorContext = React.createContext<KeyboardIteratorContextType>
},
});
export const KeyboardIteratorContextProvider: FC<{ initialIndex?: number }> = ({ children, initialIndex = -1 }) => {
export const useKeyboardIteratorItem = (item: Callback | CallbackIterator) => {
const { register, deregister } = useContext(KeyboardIteratorContext);
useEffect(() => {
const index = register(item);
return () => deregister(index);
}, [item, register, deregister]);
};
export const KeyboardSubIteratorContextProvider: FC<{ initialIndex?: number }> = ({ children, initialIndex }) => {
const callbackIterator = useCallbackIterator(initialIndex);
useKeyboardIteratorItem(callbackIterator);
return <KeyboardIteratorContext.Provider value={callbackIterator}>{children}</KeyboardIteratorContext.Provider>;
};
export const KeyboardIteratorContextProvider: FC<{ initialIndex?: number }> = ({ children, initialIndex }) => {
const [t] = useTranslation("commons");
const callbacks = useRef<Array<Callback>>([]);
const activeIndex = useRef<number>(initialIndex);
const executeCallback = useCallback((index: number) => callbacks.current[index](), []);
const callbackIterator = useCallbackIterator(initialIndex);
const navigateBackward = useCallback(() => {
if (activeIndex.current === -1) {
activeIndex.current = callbacks.current.length - 1;
executeCallback(activeIndex.current);
} else if (activeIndex.current > 0) {
activeIndex.current -= 1;
executeCallback(activeIndex.current);
}
}, [executeCallback]);
const navigateForward = useCallback(() => {
if (activeIndex.current === -1) {
activeIndex.current = 0;
executeCallback(activeIndex.current);
} else if (activeIndex.current < callbacks.current.length - 1) {
activeIndex.current += 1;
executeCallback(activeIndex.current);
}
}, [executeCallback]);
const value = useMemo(
() => ({
register: (callback: () => void) => callbacks.current.push(callback) - 1,
deregister: (index: number) => {
callbacks.current.splice(index, 1);
if (callbacks.current.length === 0) {
activeIndex.current = -1;
} else if (activeIndex.current === index || activeIndex.current >= callbacks.current.length) {
if (activeIndex.current > 0) {
activeIndex.current -= 1;
}
executeCallback(activeIndex.current);
}
},
}),
[executeCallback]
);
useShortcut("k", navigateBackward, {
useShortcut("k", callbackIterator.previous.bind(callbackIterator), {
description: t("shortcuts.iterator.previous"),
});
useShortcut("j", navigateForward, {
useShortcut("j", callbackIterator.next.bind(callbackIterator), {
description: t("shortcuts.iterator.next"),
});
useShortcut("tab", () => {
activeIndex.current = -1;
callbackIterator.reset();
return true;
});
return <KeyboardIteratorContext.Provider value={value}>{children}</KeyboardIteratorContext.Provider>;
};
export const useKeyboardIteratorCallback = (callback: Callback) => {
const { register, deregister } = useContext(KeyboardIteratorContext);
useEffect(() => {
const index = register(callback);
return () => deregister(index);
}, [callback, register, deregister]);
return <KeyboardIteratorContext.Provider value={callbackIterator}>{children}</KeyboardIteratorContext.Provider>;
};
/**
* Use the {@link React.RefObject} returned from this hook to register a target to the nearest enclosing {@link KeyboardIterator}.
* Use the {@link React.RefObject} returned from this hook to register a target to the nearest enclosing {@link KeyboardIterator} or {@link KeyboardSubIterator}.
*
* @example
* const ref = useKeyboardIteratorTarget();
@@ -133,7 +95,7 @@ export function useKeyboardIteratorTarget(): React.RefCallback<HTMLElement> {
ref.current = el;
}
}, []);
useKeyboardIteratorCallback(callback);
useKeyboardIteratorItem(callback);
return refCallback;
}
@@ -144,7 +106,36 @@ export function useKeyboardIteratorTarget(): React.RefCallback<HTMLElement> {
*
* Press `k` to navigate backwards and `j` to navigate forward.
* Pressing `tab` will reset the iterator to its initial state.
*
* Use the {@link KeyboardSubIterator} to wrap asynchronously loaded targets.
*/
export const KeyboardIterator: FC = ({ children }) => (
<KeyboardIteratorContextProvider>{children}</KeyboardIteratorContextProvider>
);
/**
* Allows deferred {@link useKeyboardIteratorTarget} invocations enclosed in this sub-iterator to be registered in the correct order within a {@link KeyboardIterator}.
*
* This is especially useful for extension points which might contain further iterable elements that are loaded asynchronously.
*
* @example
* <KeyboardIterator>
* <KeyboardSubIterator>
* <ExtensionPoint<extensionPoints.RepositoryOverviewTop>
* name="repository.overview.top"
* renderAll={true}
* props={{
* page,
* search,
* namespace,
* }}
* />
* </KeyboardSubIterator>
* {groups.map((group) => {
* return <RepositoryGroupEntry group={group} key={group.name} />;
* })}
* </KeyboardIterator>
*/
export const KeyboardSubIterator: FC = ({ children }) => (
<KeyboardSubIteratorContextProvider>{children}</KeyboardSubIteratorContextProvider>
);

View File

@@ -28,7 +28,7 @@ import { NamespaceCollection, Repository } from "@scm-manager/ui-types";
import groupByNamespace from "./groupByNamespace";
import RepositoryGroupEntry from "./RepositoryGroupEntry";
import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
import { KeyboardIterator } from "@scm-manager/ui-shortcuts";
import { KeyboardIterator, KeyboardSubIterator } from "@scm-manager/ui-shortcuts";
type Props = {
repositories: Repository[];
@@ -45,6 +45,8 @@ class RepositoryList extends React.Component<Props> {
const groups = groupByNamespace(repositories, namespaces);
return (
<div className="content">
<KeyboardIterator>
<KeyboardSubIterator>
<ExtensionPoint<extensionPoints.RepositoryOverviewTop>
name="repository.overview.top"
renderAll={true}
@@ -54,7 +56,7 @@ class RepositoryList extends React.Component<Props> {
namespace,
}}
/>
<KeyboardIterator>
</KeyboardSubIterator>
{groups.map((group) => {
return <RepositoryGroupEntry group={group} key={group.name} />;
})}

View File

@@ -1806,6 +1806,13 @@
dependencies:
"@sinclair/typebox" "^0.24.1"
"@jest/schemas@^29.0.0":
version "29.0.0"
resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.0.0.tgz#5f47f5994dd4ef067fb7b4188ceac45f77fe952a"
integrity sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==
dependencies:
"@sinclair/typebox" "^0.24.1"
"@jest/source-map@^24.3.0", "@jest/source-map@^24.9.0":
version "24.9.0"
resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-24.9.0.tgz#0e263a94430be4b41da683ccc1e6bffe2a191714"
@@ -7735,6 +7742,11 @@ diff-sequences@^28.1.1:
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-28.1.1.tgz#9989dc731266dc2903457a70e996f3a041913ac6"
integrity sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==
diff-sequences@^29.3.1:
version "29.3.1"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.3.1.tgz#104b5b95fe725932421a9c6e5b4bef84c3f2249e"
integrity sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ==
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
@@ -11399,6 +11411,16 @@ jest-diff@^28.1.3:
jest-get-type "^28.0.2"
pretty-format "^28.1.3"
jest-diff@^29.0.0:
version "29.3.1"
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.3.1.tgz#d8215b72fed8f1e647aed2cae6c752a89e757527"
integrity sha512-vU8vyiO7568tmin2lA3r2DP8oRvzhvRcD4DjpXc6uGveQodyk7CKLhQlCSiwgx3g0pFaE88/KLZ0yaTWMc4Uiw==
dependencies:
chalk "^4.0.0"
diff-sequences "^29.3.1"
jest-get-type "^29.2.0"
pretty-format "^29.3.1"
jest-docblock@^24.3.0:
version "24.9.0"
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.9.0.tgz#7970201802ba560e1c4092cc25cbedf5af5a8ce2"
@@ -11483,6 +11505,14 @@ jest-environment-node@^26.6.2:
jest-mock "^26.6.2"
jest-util "^26.6.2"
jest-extended@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/jest-extended/-/jest-extended-3.1.0.tgz#7998699751f3b5d9207d212b39c1837f59c7fecc"
integrity sha512-BbuAVUb2dchgwm7euayVt/7hYlkKaknQItKyzie7Li8fmXCglgf21XJeRIdOITZ/cMOTTj5Oh5IjQOxQOe/hfQ==
dependencies:
jest-diff "^29.0.0"
jest-get-type "^29.0.0"
jest-get-type@^24.9.0:
version "24.9.0"
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e"
@@ -11498,6 +11528,11 @@ jest-get-type@^28.0.2:
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-28.0.2.tgz#34622e628e4fdcd793d46db8a242227901fcf203"
integrity sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==
jest-get-type@^29.0.0, jest-get-type@^29.2.0:
version "29.2.0"
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.2.0.tgz#726646f927ef61d583a3b3adb1ab13f3a5036408"
integrity sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA==
jest-haste-map@^24.9.0:
version "24.9.0"
resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.9.0.tgz#b38a5d64274934e21fa417ae9a9fbeb77ceaac7d"
@@ -15184,6 +15219,15 @@ pretty-format@^28.0.0, pretty-format@^28.1.3:
ansi-styles "^5.0.0"
react-is "^18.0.0"
pretty-format@^29.3.1:
version "29.3.1"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.3.1.tgz#1841cac822b02b4da8971dacb03e8a871b4722da"
integrity sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg==
dependencies:
"@jest/schemas" "^29.0.0"
ansi-styles "^5.0.0"
react-is "^18.0.0"
pretty-format@^3.8.0:
version "3.8.0"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385"