Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.

Run prettier formatting

+1 -1
package.json
···
"@rollup/plugin-commonjs": "^19.0.1",
"@rollup/plugin-node-resolve": "^13.0.2",
"@types/react": "^17.0.14",
-
"husky": "^7.0.1",
"lint-staged": "^11.0.1",
"npm-run-all": "^4.1.5",
"prettier": "^2.1.2",
···
"@rollup/plugin-commonjs": "^19.0.1",
"@rollup/plugin-node-resolve": "^13.0.2",
"@types/react": "^17.0.14",
+
"husky-v4": "^4.3.8",
"lint-staged": "^11.0.1",
"npm-run-all": "^4.1.5",
"prettier": "^2.1.2",
+17 -4
src/useDialogDismiss.ts
···
const usePriority = makePriorityHook();
-
export function useDialogDismiss<T extends HTMLElement>(ref: Ref<T>, onDismiss: () => void) {
const hasPriority = usePriority(ref);
useLayoutEffect(() => {
if (!hasPriority) return;
function onKey(event: KeyboardEvent) {
-
if (event.isComposing || event.defaultPrevented || event.code !== 'Escape') return;
event.preventDefault();
onDismiss();
-
};
function onClick(event: MouseEvent | TouchEvent) {
-
if (!ref.current || contains(ref.current, event.target) || event.defaultPrevented) return;
event.preventDefault();
onDismiss();
}
···
const usePriority = makePriorityHook();
+
export function useDialogDismiss<T extends HTMLElement>(
+
ref: Ref<T>,
+
onDismiss: () => void
+
) {
const hasPriority = usePriority(ref);
useLayoutEffect(() => {
if (!hasPriority) return;
function onKey(event: KeyboardEvent) {
+
if (
+
event.isComposing ||
+
event.defaultPrevented ||
+
event.code !== 'Escape'
+
)
+
return;
event.preventDefault();
onDismiss();
+
}
function onClick(event: MouseEvent | TouchEvent) {
+
if (
+
!ref.current ||
+
contains(ref.current, event.target) ||
+
event.defaultPrevented
+
)
+
return;
event.preventDefault();
onDismiss();
}
+25 -8
src/useDialogFocus.ts
···
import { snapshotSelection, restoreSelection } from './utils/selection';
-
import { getFirstFocusTarget, getFocusTargets, getNextFocusTarget } from './utils/focus';
import { useLayoutEffect } from './utils/react';
import { contains, isInputElement } from './utils/element';
import { makePriorityHook } from './usePriority';
···
if (!ref.current || event.defaultPrevented) return;
const active = document.activeElement as HTMLElement;
-
const owner = (ownerRef && ownerRef.current) || selection && selection.element;
if (willReceiveFocus || (owner && event.target === owner)) {
-
if (!contains(ref.current, active)) selection = snapshotSelection(owner);
willReceiveFocus = false;
return;
}
const { relatedTarget, target } = event;
// Check whether focus is about to move into the container and prevent it
-
if (contains(ref.current, target) && !contains(ref.current, relatedTarget)) {
// Get the next focus target of the container
const focusTarget = getNextFocusTarget(ref.current, !focusMovesForward);
if (focusTarget) {
···
}
const active = document.activeElement as HTMLElement;
-
const owner = (ownerRef && ownerRef.current) || selection && selection.element;
const focusTargets = getFocusTargets(ref.current);
if (
···
// Implement forward movement in focus targets
event.preventDefault();
const focusIndex = focusTargets.indexOf(active);
-
const nextIndex = focusIndex < focusTargets.length - 1 ? focusIndex + 1 : 0;
willReceiveFocus = true;
focusTargets[nextIndex].focus();
} else if (
···
// Implement backward movement in focus targets
event.preventDefault();
const focusIndex = focusTargets.indexOf(active);
-
const nextIndex = focusIndex > 0 ? focusIndex - 1 : focusTargets.length - 1;
willReceiveFocus = true;
focusTargets[nextIndex].focus();
} else if (selection && event.code === 'Escape') {
···
event.preventDefault();
willReceiveFocus = false;
restoreSelection(selection);
-
} else if (owner && isInputElement(owner) && contains(owner, active) && event.code === 'Enter') {
// Move focus to first target when enter is pressed
event.preventDefault();
const newTarget = getFirstFocusTarget(ref.current);
···
import { snapshotSelection, restoreSelection } from './utils/selection';
+
import {
+
getFirstFocusTarget,
+
getFocusTargets,
+
getNextFocusTarget,
+
} from './utils/focus';
import { useLayoutEffect } from './utils/react';
import { contains, isInputElement } from './utils/element';
import { makePriorityHook } from './usePriority';
···
if (!ref.current || event.defaultPrevented) return;
const active = document.activeElement as HTMLElement;
+
const owner =
+
(ownerRef && ownerRef.current) || (selection && selection.element);
if (willReceiveFocus || (owner && event.target === owner)) {
+
if (!contains(ref.current, active))
+
selection = snapshotSelection(owner);
willReceiveFocus = false;
return;
}
const { relatedTarget, target } = event;
// Check whether focus is about to move into the container and prevent it
+
if (
+
contains(ref.current, target) &&
+
!contains(ref.current, relatedTarget)
+
) {
// Get the next focus target of the container
const focusTarget = getNextFocusTarget(ref.current, !focusMovesForward);
if (focusTarget) {
···
}
const active = document.activeElement as HTMLElement;
+
const owner =
+
(ownerRef && ownerRef.current) || (selection && selection.element);
const focusTargets = getFocusTargets(ref.current);
if (
···
// Implement forward movement in focus targets
event.preventDefault();
const focusIndex = focusTargets.indexOf(active);
+
const nextIndex =
+
focusIndex < focusTargets.length - 1 ? focusIndex + 1 : 0;
willReceiveFocus = true;
focusTargets[nextIndex].focus();
} else if (
···
// Implement backward movement in focus targets
event.preventDefault();
const focusIndex = focusTargets.indexOf(active);
+
const nextIndex =
+
focusIndex > 0 ? focusIndex - 1 : focusTargets.length - 1;
willReceiveFocus = true;
focusTargets[nextIndex].focus();
} else if (selection && event.code === 'Escape') {
···
event.preventDefault();
willReceiveFocus = false;
restoreSelection(selection);
+
} else if (
+
owner &&
+
isInputElement(owner) &&
+
contains(owner, active) &&
+
event.code === 'Enter'
+
) {
// Move focus to first target when enter is pressed
event.preventDefault();
const newTarget = getFirstFocusTarget(ref.current);
+36 -10
src/useMenuFocus.ts
···
-
import { RestoreSelection, snapshotSelection, restoreSelection } from './utils/selection';
import { getFirstFocusTarget, getFocusTargets } from './utils/focus';
import { useLayoutEffect } from './utils/react';
import { contains, isInputElement } from './utils/element';
···
ownerRef?: Ref<HTMLElement>;
}
-
export function useMenuFocus<T extends HTMLElement>(ref: Ref<T>, options?: MenuFocusOptions) {
const ownerRef = options && options.ownerRef;
const disabled = !!(options && options.disabled);
···
function onFocus(event: FocusEvent) {
if (!ref.current || event.defaultPrevented) return;
-
const owner = (ownerRef && ownerRef.current) || selection && selection.element;
const { relatedTarget, target } = event;
if (relatedTarget === owner) {
// When owner is explicitly passed we can make a snapshot early
selection = snapshotSelection(owner);
-
} else if (contains(ref.current, target) && !contains(ref.current, relatedTarget)) {
// Check whether focus is about to move into the container and snapshot last focus
selection = snapshotSelection(owner);
-
} else if (contains(ref.current, relatedTarget) && !contains(ref.current, target)) {
// Reset focus if it's lost and has left the menu
selection = null;
}
···
function onKey(event: KeyboardEvent) {
if (!ref.current || event.defaultPrevented || event.isComposing) return;
-
const owner = (ownerRef && ownerRef.current) || selection && selection.element;
const active = document.activeElement as HTMLElement;
const focusTargets = getFocusTargets(ref.current);
-
if (!focusTargets.length || !contains(ref.current, active) || !contains(owner, active)) {
// Do nothing if container doesn't contain focus or not targets are available
return;
}
···
// Implement forward movement in focus targets
event.preventDefault();
const focusIndex = focusTargets.indexOf(active);
-
const nextIndex = focusIndex < focusTargets.length - 1 ? focusIndex + 1 : 0;
focusTargets[nextIndex].focus();
} else if (
(!isInputElement(active) && event.code === 'ArrowLeft') ||
···
// Implement backward movement in focus targets
event.preventDefault();
const focusIndex = focusTargets.indexOf(active);
-
const nextIndex = focusIndex > 0 ? focusIndex - 1 : focusTargets.length - 1;
focusTargets[nextIndex].focus();
} else if (event.code === 'Home') {
// Implement Home => first item
···
// Implement End => last item
event.preventDefault();
focusTargets[focusTargets.length - 1].focus();
-
} else if (owner && isInputElement(owner) && contains(owner, active) && event.code === 'Enter') {
// Move focus to first target when enter is pressed
event.preventDefault();
const newTarget = getFirstFocusTarget(ref.current);
···
+
import {
+
RestoreSelection,
+
snapshotSelection,
+
restoreSelection,
+
} from './utils/selection';
import { getFirstFocusTarget, getFocusTargets } from './utils/focus';
import { useLayoutEffect } from './utils/react';
import { contains, isInputElement } from './utils/element';
···
ownerRef?: Ref<HTMLElement>;
}
+
export function useMenuFocus<T extends HTMLElement>(
+
ref: Ref<T>,
+
options?: MenuFocusOptions
+
) {
const ownerRef = options && options.ownerRef;
const disabled = !!(options && options.disabled);
···
function onFocus(event: FocusEvent) {
if (!ref.current || event.defaultPrevented) return;
+
const owner =
+
(ownerRef && ownerRef.current) || (selection && selection.element);
const { relatedTarget, target } = event;
if (relatedTarget === owner) {
// When owner is explicitly passed we can make a snapshot early
selection = snapshotSelection(owner);
+
} else if (
+
contains(ref.current, target) &&
+
!contains(ref.current, relatedTarget)
+
) {
// Check whether focus is about to move into the container and snapshot last focus
selection = snapshotSelection(owner);
+
} else if (
+
contains(ref.current, relatedTarget) &&
+
!contains(ref.current, target)
+
) {
// Reset focus if it's lost and has left the menu
selection = null;
}
···
function onKey(event: KeyboardEvent) {
if (!ref.current || event.defaultPrevented || event.isComposing) return;
+
const owner =
+
(ownerRef && ownerRef.current) || (selection && selection.element);
const active = document.activeElement as HTMLElement;
const focusTargets = getFocusTargets(ref.current);
+
if (
+
!focusTargets.length ||
+
!contains(ref.current, active) ||
+
!contains(owner, active)
+
) {
// Do nothing if container doesn't contain focus or not targets are available
return;
}
···
// Implement forward movement in focus targets
event.preventDefault();
const focusIndex = focusTargets.indexOf(active);
+
const nextIndex =
+
focusIndex < focusTargets.length - 1 ? focusIndex + 1 : 0;
focusTargets[nextIndex].focus();
} else if (
(!isInputElement(active) && event.code === 'ArrowLeft') ||
···
// Implement backward movement in focus targets
event.preventDefault();
const focusIndex = focusTargets.indexOf(active);
+
const nextIndex =
+
focusIndex > 0 ? focusIndex - 1 : focusTargets.length - 1;
focusTargets[nextIndex].focus();
} else if (event.code === 'Home') {
// Implement Home => first item
···
// Implement End => last item
event.preventDefault();
focusTargets[focusTargets.length - 1].focus();
+
} else if (
+
owner &&
+
isInputElement(owner) &&
+
contains(owner, active) &&
+
event.code === 'Enter'
+
) {
// Move focus to first target when enter is pressed
event.preventDefault();
const newTarget = getFirstFocusTarget(ref.current);
+17 -5
src/useModalFocus.ts
···
-
import { RestoreSelection, snapshotSelection, restoreSelection } from './utils/selection';
import { getFirstFocusTarget, getFocusTargets } from './utils/focus';
import { useLayoutEffect } from './utils/react';
import { contains } from './utils/element';
···
disabled?: boolean;
}
-
export function useModalFocus<T extends HTMLElement>(ref: Ref<T>, options?: ModalFocusOptions) {
const disabled = !!(options && options.disabled);
const hasPriority = usePriority(ref, disabled);
···
if (!ref.current || !hasPriority || disabled) return;
let selection: RestoreSelection | null = null;
-
if (!document.activeElement || !ref.current.contains(document.activeElement)) {
const newTarget = getFirstFocusTarget(ref.current);
if (newTarget) {
selection = snapshotSelection(ref.current);
···
const parent = ref.current;
if (!parent || event.defaultPrevented) return;
-
if (contains(parent, event.target) && !contains(parent, event.relatedTarget)) {
const target = getFirstFocusTarget(parent);
if (target) target.focus();
}
···
event.preventDefault();
targets[0].focus();
}
-
}
}
···
+
import {
+
RestoreSelection,
+
snapshotSelection,
+
restoreSelection,
+
} from './utils/selection';
import { getFirstFocusTarget, getFocusTargets } from './utils/focus';
import { useLayoutEffect } from './utils/react';
import { contains } from './utils/element';
···
disabled?: boolean;
}
+
export function useModalFocus<T extends HTMLElement>(
+
ref: Ref<T>,
+
options?: ModalFocusOptions
+
) {
const disabled = !!(options && options.disabled);
const hasPriority = usePriority(ref, disabled);
···
if (!ref.current || !hasPriority || disabled) return;
let selection: RestoreSelection | null = null;
+
if (
+
!document.activeElement ||
+
!ref.current.contains(document.activeElement)
+
) {
const newTarget = getFirstFocusTarget(ref.current);
if (newTarget) {
selection = snapshotSelection(ref.current);
···
const parent = ref.current;
if (!parent || event.defaultPrevented) return;
+
if (
+
contains(parent, event.target) &&
+
!contains(parent, event.relatedTarget)
+
) {
const target = getFirstFocusTarget(parent);
if (target) target.focus();
}
···
event.preventDefault();
targets[0].focus();
}
}
}
+8 -4
src/usePriority.ts
···
(x & 16 /* a contains b */ && -1) ||
(x & 8 /* b contains a */ && 1) ||
(x & 2 /* b follows a */ && -1) ||
-
(x & 4 /* a follows b */ && 1)
-
) || 0;
};
/** Indicates whether a given element on a stack of active priority hooks is the deepest element. */
-
return function usePriority<T extends HTMLElement>(ref: Ref<T>, disabled?: boolean): boolean {
function computeHasPriority(): boolean {
if (!ref.current) return false;
const tempStack = priorityStack.concat(ref.current).sort(sortByHierarchy);
···
}, [ref, isDisabled]);
return hasPriority;
-
}
};
···
(x & 16 /* a contains b */ && -1) ||
(x & 8 /* b contains a */ && 1) ||
(x & 2 /* b follows a */ && -1) ||
+
(x & 4 /* a follows b */ && 1) ||
+
0
+
);
};
/** Indicates whether a given element on a stack of active priority hooks is the deepest element. */
+
return function usePriority<T extends HTMLElement>(
+
ref: Ref<T>,
+
disabled?: boolean
+
): boolean {
function computeHasPriority(): boolean {
if (!ref.current) return false;
const tempStack = priorityStack.concat(ref.current).sort(sortByHierarchy);
···
}, [ref, isDisabled]);
return hasPriority;
+
};
};
+19 -17
src/utils/element.ts
···
export const getTabIndex = (node: Element): number => {
const index = parseInt(node.getAttribute('tabindex')!, 10);
return (
-
index === index &&
-
!(node as HTMLElement).isContentEditable &&
-
index
-
) || 0;
};
/** Returns whether an element is visible in the context of focusability. */
-
export const isVisible = (node: Element): boolean => !!(
-
(node as HTMLElement).offsetWidth &&
-
(node as HTMLElement).offsetHeight &&
-
node.getClientRects().length &&
-
getComputedStyle(node).visibility !== 'hidden'
-
);
/** Returns whether an element accepts text input. */
-
export const isInputElement = (node: Element): boolean => !!(
-
node.tagName === 'INPUT'
-
|| node.tagName === 'TEXTAREA'
-
|| (node as HTMLElement).isContentEditable
-
);
-
export const contains = (owner: Element | null, node: Element | EventTarget | null) =>
-
!!(node && owner && (owner === node || owner.contains(node as Element)));
···
export const getTabIndex = (node: Element): number => {
const index = parseInt(node.getAttribute('tabindex')!, 10);
return (
+
(index === index && !(node as HTMLElement).isContentEditable && index) || 0
+
);
};
/** Returns whether an element is visible in the context of focusability. */
+
export const isVisible = (node: Element): boolean =>
+
!!(
+
(node as HTMLElement).offsetWidth &&
+
(node as HTMLElement).offsetHeight &&
+
node.getClientRects().length &&
+
getComputedStyle(node).visibility !== 'hidden'
+
);
/** Returns whether an element accepts text input. */
+
export const isInputElement = (node: Element): boolean =>
+
!!(
+
node.tagName === 'INPUT' ||
+
node.tagName === 'TEXTAREA' ||
+
(node as HTMLElement).isContentEditable
+
);
+
export const contains = (
+
owner: Element | null,
+
node: Element | EventTarget | null
+
) => !!(node && owner && (owner === node || owner.contains(node as Element)));
+23 -11
src/utils/focus.ts
···
import { getTabIndex, isVisible } from './element';
-
const excludeSelector = ':not([tabindex^="-"]):not([aria-modal]):not([role="dialog"])';
const focusableSelectors = [
'input:not([type="hidden"]):not([disabled])' + excludeSelector,
···
].join(',');
/** Generic sorting function for tupel containing elements with indices and tab indices. */
-
const sortByTabindex = <T extends HTMLElement>(a: [number, number, T], b: [number, number, T]) => {
-
return a[1] === a[1]
-
? a[0] - b[0]
-
: a[1] - a[1];
};
/** Returns whether this node may contain focusable elements. */
export const hasFocusTargets = (node: Element): boolean =>
-
!node.matches(excludeSelector) && isVisible(node) && !!node.querySelector(focusableSelectors);
/** Returns a sorted list of focus targets inside the given element. */
export const getFocusTargets = (node: Element): HTMLElement[] => {
const elements = node.querySelectorAll(focusableSelectors);
const targets: HTMLElement[] = [];
-
const tabIndexTargets: [index: number, tabIndex: number, element: HTMLElement][] = [];
for (let i = 0, l = elements.length; i < l; i++) {
const element = elements[i] as HTMLElement;
if (isVisible(element)) {
···
};
/** Returns the next (optionally in reverse) focus target given a target node. */
-
export const getNextFocusTarget = (node: HTMLElement, reverse?: boolean): HTMLElement | null => {
let current: Element | null = node;
while (current) {
let next: Element | null = current;
-
while (next = reverse ? next.previousElementSibling : next.nextElementSibling) {
if (isVisible(next) && !!node.matches(focusableSelectors)) {
return next as HTMLElement;
} else if (hasFocusTargets(next)) {
const targets = getFocusTargets(next);
-
if (targets.length)
-
return targets[reverse ? targets.length - 1 : 0];
}
}
···
import { getTabIndex, isVisible } from './element';
+
const excludeSelector =
+
':not([tabindex^="-"]):not([aria-modal]):not([role="dialog"])';
const focusableSelectors = [
'input:not([type="hidden"]):not([disabled])' + excludeSelector,
···
].join(',');
/** Generic sorting function for tupel containing elements with indices and tab indices. */
+
const sortByTabindex = <T extends HTMLElement>(
+
a: [number, number, T],
+
b: [number, number, T]
+
) => {
+
return a[1] === a[1] ? a[0] - b[0] : a[1] - a[1];
};
/** Returns whether this node may contain focusable elements. */
export const hasFocusTargets = (node: Element): boolean =>
+
!node.matches(excludeSelector) &&
+
isVisible(node) &&
+
!!node.querySelector(focusableSelectors);
/** Returns a sorted list of focus targets inside the given element. */
export const getFocusTargets = (node: Element): HTMLElement[] => {
const elements = node.querySelectorAll(focusableSelectors);
const targets: HTMLElement[] = [];
+
const tabIndexTargets: [
+
index: number,
+
tabIndex: number,
+
element: HTMLElement
+
][] = [];
for (let i = 0, l = elements.length; i < l; i++) {
const element = elements[i] as HTMLElement;
if (isVisible(element)) {
···
};
/** Returns the next (optionally in reverse) focus target given a target node. */
+
export const getNextFocusTarget = (
+
node: HTMLElement,
+
reverse?: boolean
+
): HTMLElement | null => {
let current: Element | null = node;
while (current) {
let next: Element | null = current;
+
while (
+
(next = reverse ? next.previousElementSibling : next.nextElementSibling)
+
) {
if (isVisible(next) && !!node.matches(focusableSelectors)) {
return next as HTMLElement;
} else if (hasFocusTargets(next)) {
const targets = getFocusTargets(next);
+
if (targets.length) return targets[reverse ? targets.length - 1 : 0];
}
}
+2 -3
src/utils/react.ts
···
import { useEffect, useLayoutEffect } from 'react';
-
const useIsomorphicEffect = typeof window !== 'undefined'
-
? useLayoutEffect
-
: useEffect;
export { useIsomorphicEffect as useLayoutEffect };
···
import { useEffect, useLayoutEffect } from 'react';
+
const useIsomorphicEffect =
+
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
export { useIsomorphicEffect as useLayoutEffect };
+22 -14
src/utils/selection.ts
···
interface RestoreInputSelection {
-
element: HTMLElement,
-
method: 'setSelectionRange',
-
arguments: [number, number, 'forward' | 'backward' | 'none' | undefined],
}
interface RestoreActiveNode {
-
element: HTMLElement,
-
method: 'focus',
}
interface RestoreSelectionRange {
-
element: HTMLElement,
-
method: 'range',
-
range: Range
}
-
export type RestoreSelection = RestoreInputSelection | RestoreActiveNode | RestoreSelectionRange;
-
const isInputElement = (node: HTMLElement): node is HTMLInputElement => (
(node.nodeName === 'input' || node.nodeName === 'textarea') &&
typeof (node as HTMLInputElement).selectionStart === 'number' &&
-
typeof (node as HTMLInputElement).selectionEnd === 'number'
-
);
/** Snapshots the current focus or selection target, optinally using a ref if it's passed. */
-
export const snapshotSelection = (node?: HTMLElement | null): RestoreSelection | null => {
const target = document.activeElement as HTMLElement | null;
const element = node && target && node !== target ? node : target;
if (!element || !target) {
···
return {
element,
method: 'setSelectionRange',
-
arguments: [target.selectionStart!, target.selectionEnd!, target.selectionDirection || undefined],
};
}
···
interface RestoreInputSelection {
+
element: HTMLElement;
+
method: 'setSelectionRange';
+
arguments: [number, number, 'forward' | 'backward' | 'none' | undefined];
}
interface RestoreActiveNode {
+
element: HTMLElement;
+
method: 'focus';
}
interface RestoreSelectionRange {
+
element: HTMLElement;
+
method: 'range';
+
range: Range;
}
+
export type RestoreSelection =
+
| RestoreInputSelection
+
| RestoreActiveNode
+
| RestoreSelectionRange;
+
const isInputElement = (node: HTMLElement): node is HTMLInputElement =>
(node.nodeName === 'input' || node.nodeName === 'textarea') &&
typeof (node as HTMLInputElement).selectionStart === 'number' &&
+
typeof (node as HTMLInputElement).selectionEnd === 'number';
/** Snapshots the current focus or selection target, optinally using a ref if it's passed. */
+
export const snapshotSelection = (
+
node?: HTMLElement | null
+
): RestoreSelection | null => {
const target = document.activeElement as HTMLElement | null;
const element = node && target && node !== target ? node : target;
if (!element || !target) {
···
return {
element,
method: 'setSelectionRange',
+
arguments: [
+
target.selectionStart!,
+
target.selectionEnd!,
+
target.selectionDirection || undefined,
+
],
};
}
+94 -5
yarn.lock
···
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
-
chalk@^4.1.0, chalk@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad"
integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
clean-stack@^2.0.0:
version "2.2.0"
···
version "1.0.1"
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
concat-map@0.0.1:
version "0.0.1"
···
locate-path "^5.0.0"
path-exists "^4.0.0"
fs-extra@8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
···
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
-
husky@^7.0.1:
-
version "7.0.1"
-
resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.1.tgz#579f4180b5da4520263e8713cc832942b48e1f1c"
-
integrity sha512-gceRaITVZ+cJH9sNHqx5tFwbzlLCVxtVZcusME8JYQ8Edy5mpGDOqD8QBCdMhpyo9a+JXddnujQ4rpY2Ff9SJA==
import-fresh@^3.2.1:
version "3.3.0"
···
dependencies:
p-locate "^4.1.0"
log-symbols@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
···
dependencies:
mimic-fn "^2.1.0"
p-limit@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
···
dependencies:
p-try "^2.0.0"
p-locate@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
dependencies:
p-limit "^2.2.0"
p-map@^4.0.0:
version "4.0.0"
···
dependencies:
find-up "^4.0.0"
please-upgrade-node@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942"
···
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w=
"semver@2 || 3 || 4 || 5", semver@^5.5.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
···
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
slice-ansi@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787"
···
is-string "^1.0.5"
is-symbol "^1.0.3"
which@^1.2.9:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
···
version "1.10.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
···
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
+
chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad"
integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
+
+
ci-info@^2.0.0:
+
version "2.0.0"
+
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
+
integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
clean-stack@^2.0.0:
version "2.2.0"
···
version "1.0.1"
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
+
+
compare-versions@^3.6.0:
+
version "3.6.0"
+
resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62"
+
integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==
concat-map@0.0.1:
version "0.0.1"
···
locate-path "^5.0.0"
path-exists "^4.0.0"
+
find-up@^5.0.0:
+
version "5.0.0"
+
resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
+
integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
+
dependencies:
+
locate-path "^6.0.0"
+
path-exists "^4.0.0"
+
+
find-versions@^4.0.0:
+
version "4.0.0"
+
resolved "https://registry.yarnpkg.com/find-versions/-/find-versions-4.0.0.tgz#3c57e573bf97769b8cb8df16934b627915da4965"
+
integrity sha512-wgpWy002tA+wgmO27buH/9KzyEOQnKsG/R0yrcjPT9BOFm0zRBVQbZ95nRGXWMywS8YR5knRbpohio0bcJABxQ==
+
dependencies:
+
semver-regex "^3.1.2"
+
fs-extra@8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
···
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
+
husky-v4@^4.3.8:
+
version "4.3.8"
+
resolved "https://registry.yarnpkg.com/husky-v4/-/husky-v4-4.3.8.tgz#af3be56a8b62b941371b5190e265f76dd1af2e57"
+
integrity sha512-M7A9u/t6BnT/qbDzKb7SdXhr8qLTGTkqZL6YLDDM20jfCdmpIMEuO384LvYXSBcgv50oIgNWI/IaO3g4A4ShjA==
+
dependencies:
+
chalk "^4.0.0"
+
ci-info "^2.0.0"
+
compare-versions "^3.6.0"
+
cosmiconfig "^7.0.0"
+
find-versions "^4.0.0"
+
opencollective-postinstall "^2.0.2"
+
pkg-dir "^5.0.0"
+
please-upgrade-node "^3.2.0"
+
slash "^3.0.0"
+
which-pm-runs "^1.0.0"
import-fresh@^3.2.1:
version "3.3.0"
···
dependencies:
p-locate "^4.1.0"
+
locate-path@^6.0.0:
+
version "6.0.0"
+
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
+
integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
+
dependencies:
+
p-locate "^5.0.0"
+
log-symbols@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
···
dependencies:
mimic-fn "^2.1.0"
+
opencollective-postinstall@^2.0.2:
+
version "2.0.3"
+
resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259"
+
integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==
+
p-limit@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
···
dependencies:
p-try "^2.0.0"
+
p-limit@^3.0.2:
+
version "3.1.0"
+
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
+
integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
+
dependencies:
+
yocto-queue "^0.1.0"
+
p-locate@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
dependencies:
p-limit "^2.2.0"
+
+
p-locate@^5.0.0:
+
version "5.0.0"
+
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
+
integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
+
dependencies:
+
p-limit "^3.0.2"
p-map@^4.0.0:
version "4.0.0"
···
dependencies:
find-up "^4.0.0"
+
pkg-dir@^5.0.0:
+
version "5.0.0"
+
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-5.0.0.tgz#a02d6aebe6ba133a928f74aec20bafdfe6b8e760"
+
integrity sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==
+
dependencies:
+
find-up "^5.0.0"
+
please-upgrade-node@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942"
···
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w=
+
semver-regex@^3.1.2:
+
version "3.1.2"
+
resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-3.1.2.tgz#34b4c0d361eef262e07199dbef316d0f2ab11807"
+
integrity sha512-bXWyL6EAKOJa81XG1OZ/Yyuq+oT0b2YLlxx7c+mrdYPaPbnj6WgVULXhinMIeZGufuUBu/eVRqXEhiv4imfwxA==
+
"semver@2 || 3 || 4 || 5", semver@^5.5.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
···
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
+
slash@^3.0.0:
+
version "3.0.0"
+
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
+
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
+
slice-ansi@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787"
···
is-string "^1.0.5"
is-symbol "^1.0.3"
+
which-pm-runs@^1.0.0:
+
version "1.0.0"
+
resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb"
+
integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=
+
which@^1.2.9:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
···
version "1.10.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
+
+
yocto-queue@^0.1.0:
+
version "0.1.0"
+
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
+
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==