diff --git a/packages/store-ui/package.json b/packages/store-ui/package.json
index b07e754fe2..10d5d1dfc8 100644
--- a/packages/store-ui/package.json
+++ b/packages/store-ui/package.json
@@ -52,7 +52,8 @@
],
"dependencies": {
"@reach/popover": "^0.16.0",
- "react-swipeable": "^6.1.2"
+ "react-swipeable": "^6.1.2",
+ "tabbable": "^5.2.1"
},
"peerDependencies": {
"react": "^17.0.2",
@@ -70,9 +71,12 @@
"@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^13.1.8",
+ "@types/jest-axe": "^3.5.3",
+ "@types/tabbable": "^3.1.1",
"@types/testing-library__jest-dom": "^5.9.5",
"@vtex/theme-b2c-tailwind": "^1.1.11",
"@vtex/tsconfig": "^0.5.0",
+ "jest-axe": "^5.0.1",
"react": "^17.0.2",
"react-docgen-typescript-loader": "^3.7.2",
"react-dom": "^17.0.2",
diff --git a/packages/store-ui/src/index.ts b/packages/store-ui/src/index.ts
index 6a75f5138f..73a0f0d613 100644
--- a/packages/store-ui/src/index.ts
+++ b/packages/store-ui/src/index.ts
@@ -57,6 +57,9 @@ export type { CarouselProps } from './molecules/Carousel'
export { default as IconButton } from './molecules/IconButton'
export type { IconButtonProps } from './molecules/IconButton'
+export { default as Modal } from './molecules/Modal'
+export type { ModalProps } from './molecules/Modal'
+
// Hooks
export { default as useSlider } from './hooks/useSlider'
export type {
diff --git a/packages/store-ui/src/molecules/Modal/Modal.test.tsx b/packages/store-ui/src/molecules/Modal/Modal.test.tsx
new file mode 100644
index 0000000000..907ed87b96
--- /dev/null
+++ b/packages/store-ui/src/molecules/Modal/Modal.test.tsx
@@ -0,0 +1,233 @@
+import type { ReactNode } from 'react'
+import React, { useState } from 'react'
+import { fireEvent, render } from '@testing-library/react'
+import { axe } from 'jest-axe'
+
+import Modal from './Modal'
+import Button from '../../atoms/Button'
+import Input from '../../atoms/Input'
+
+const modalTestId = 'store-modal'
+
+const TestModal = ({
+ children,
+ onDismiss: mockOnDismiss,
+}: {
+ children?: ReactNode
+ onDismiss?: () => void
+}) => {
+ const [isOpen, setIsOpen] = useState(false)
+ const handleOpen = () => {
+ setIsOpen(true)
+ }
+
+ const onDismiss = () => {
+ setIsOpen(false)
+ mockOnDismiss?.()
+ }
+
+ return (
+ <>
+
+
+
+
+ {children}
+
+ >
+ )
+}
+
+describe('Modal', () => {
+ it('The attribute data-store-modal-content should be present', () => {
+ const { getByTestId } = render(
+
+ Foo
+
+ )
+
+ expect(getByTestId('store-modal')).toHaveAttribute(
+ 'data-store-modal-content'
+ )
+ })
+
+ it('Modal should only be rendered if isOpen is true', () => {
+ // Check that modal won't be rendered
+ const { getByTestId } = render()
+
+ expect(document.querySelector(`[data-testid="${modalTestId}"]`)).toBeNull()
+
+ fireEvent.click(getByTestId('trigger'))
+
+ expect(getByTestId('store-modal')).toBeInTheDocument()
+ })
+})
+
+describe('Modal WAI-ARIA Specifications', () => {
+ // WAI-ARIA tests
+ // https://www.w3.org/TR/wai-aria-practices-1.1/#dialog_modal
+ it('AXE Test', async () => {
+ render(
+
+ Foo
+
+ )
+
+ expect(await axe(document.body)).toHaveNoViolations()
+ })
+
+ it('Focus first element', () => {
+ const { getByTestId } = render()
+
+ // Open the modal
+ fireEvent.click(getByTestId('trigger'))
+
+ // Check if the first tabbable is focused
+ expect(getByTestId('first-input')).toHaveFocus()
+ })
+
+ it('Loop focus', () => {
+ const { getByTestId } = render()
+
+ // Open the modal
+ fireEvent.click(getByTestId('trigger'))
+
+ expect(getByTestId('first-input')).toHaveFocus()
+
+ // Simulate loop back: from first to last element
+ fireEvent.keyDown(document.activeElement!, {
+ key: 'Tab',
+ shiftKey: true,
+ })
+
+ fireEvent.focus(getByTestId('beforeElement'))
+ expect(getByTestId('first-button')).toHaveFocus()
+
+ // Simulate loop back: from last to first element
+ fireEvent.keyDown(document.activeElement!, {
+ key: 'Tab',
+ })
+
+ fireEvent.focus(getByTestId('afterElement'))
+ expect(getByTestId('first-input')).toHaveFocus()
+ })
+
+ it('Loop focus inside the child modal', () => {
+ const { getByTestId, getAllByTestId } = render(
+
+
+
+ )
+
+ // Open the first modal
+ fireEvent.click(getByTestId('trigger'))
+
+ const [, secondTrigger] = getAllByTestId('trigger')
+
+ // Open the internal modal
+ fireEvent.click(secondTrigger)
+
+ // Check if the first input of the internal modal is focused
+ expect(secondTrigger).not.toHaveFocus()
+ expect(getAllByTestId('first-input')[1]).toHaveFocus()
+
+ // Simulate loop back: from first to last element of the internal modal
+ fireEvent.keyDown(document.activeElement!, {
+ key: 'Tab',
+ shiftKey: true,
+ })
+
+ fireEvent.focus(getAllByTestId('beforeElement')[1])
+ const [firstButton, secondButton] = getAllByTestId('first-button')
+
+ expect(secondButton).toHaveFocus()
+ expect(firstButton).not.toHaveFocus()
+ })
+
+ it('Focus last element before the modal was opened', () => {
+ const { getByTestId } = render()
+ const triggerModalButton = getByTestId('trigger')
+
+ // Focus the trigger button that's outside the modal
+ triggerModalButton.focus()
+ expect(triggerModalButton).toHaveFocus()
+
+ fireEvent.click(triggerModalButton)
+
+ // Modal focused something inside, so make sure that's not focused
+ expect(triggerModalButton).not.toHaveFocus()
+
+ // Close the modal
+ fireEvent.click(getByTestId('store-overlay'))
+
+ // Make sure that modal focused back the trigger button after close.
+ expect(triggerModalButton).toHaveFocus()
+ })
+
+ it('Call onDismiss when press escape without tabbable children', () => {
+ const mockDismiss = jest.fn()
+ const { getByTestId } = render(
+
+ Not focable content
+
+ )
+
+ fireEvent.keyDown(getByTestId('store-modal'), { key: 'Escape' })
+ expect(mockDismiss).toHaveBeenCalled()
+ })
+
+ it('Call onDismiss when press escape with tabbable children', () => {
+ const mockDismiss = jest.fn()
+ const { getByTestId } = render()
+
+ fireEvent.click(getByTestId('trigger'))
+
+ // Pressing any key other than 'Escape' won't close the modal
+ fireEvent.keyPress(getByTestId('store-modal'), { key: 'j' })
+ expect(mockDismiss).not.toHaveBeenCalled()
+
+ // Press Escape
+ fireEvent.keyDown(getByTestId('store-modal'), {
+ key: 'Escape',
+ })
+
+ expect(mockDismiss).toHaveBeenCalled()
+ })
+
+ it('Call only the onDismiss from internal modal when press escape', () => {
+ const mockExternalDismiss = jest.fn()
+ const mockInternalDismiss = jest.fn()
+ const { getByTestId, getAllByTestId } = render(
+
+
+
+ )
+
+ fireEvent.click(getByTestId('trigger'))
+ fireEvent.click(getAllByTestId('trigger')[1])
+
+ expect(getAllByTestId('store-modal')[1]).toBeInTheDocument()
+
+ // Press Escape on internal modal. Only the internal modal should close
+ fireEvent.keyDown(getAllByTestId('store-modal')[1], {
+ key: 'Escape',
+ })
+
+ expect(mockExternalDismiss).not.toHaveBeenCalled()
+ expect(mockInternalDismiss).toHaveBeenCalled()
+ })
+
+ it('Call onDismiss when click outside the modal', () => {
+ const mockDismiss = jest.fn()
+ const { getByTestId } = render()
+
+ fireEvent.click(getByTestId('trigger'))
+
+ // Close the modal
+ fireEvent.click(getByTestId('store-overlay'))
+
+ expect(mockDismiss).toHaveBeenCalled()
+ })
+})
diff --git a/packages/store-ui/src/molecules/Modal/Modal.tsx b/packages/store-ui/src/molecules/Modal/Modal.tsx
new file mode 100644
index 0000000000..e51241fd10
--- /dev/null
+++ b/packages/store-ui/src/molecules/Modal/Modal.tsx
@@ -0,0 +1,83 @@
+import type {
+ AriaAttributes,
+ KeyboardEvent,
+ MouseEvent,
+ PropsWithChildren,
+} from 'react'
+import React from 'react'
+import { createPortal } from 'react-dom'
+
+import Overlay from '../../atoms/Overlay'
+import ModalContent from './ModalContent'
+import type { ModalContentProps } from './ModalContent'
+
+export interface ModalProps extends ModalContentProps {
+ /**
+ * ID to find this component in testing tools (e.g.: cypress, testing library, and jest).
+ */
+ testId?: string
+ /**
+ * Identifies the element (or elements) that labels the current element.
+ * @see aria-labelledby https://www.w3.org/TR/wai-aria-1.1/#aria-labelledby
+ */
+ 'aria-labelledby'?: AriaAttributes['aria-label']
+
+ /**
+ * This function is called whenever the user hits "Escape" or clicks outside
+ * the dialog.
+ */
+ onDismiss?: (event: MouseEvent | KeyboardEvent) => void
+ /**
+ * Controls whether or not the dialog is open.
+ */
+ isOpen: boolean
+}
+
+/*
+ * This component is based on @reach/dialog.
+ * https://github.com/reach/reach-ui/blob/main/packages/dialog/src/index.tsx
+ * https://reach.tech/dialog
+ */
+
+const Modal = ({
+ isOpen,
+ children,
+ onDismiss,
+ testId = 'store-modal',
+ ...props
+}: PropsWithChildren) => {
+ const handleBackdropClick = (event: MouseEvent) => {
+ if (event.defaultPrevented) {
+ return
+ }
+
+ event.stopPropagation()
+ onDismiss?.(event)
+ }
+
+ const handleBackdropKeyDown = (event: KeyboardEvent) => {
+ if (event.key !== 'Escape' || event.defaultPrevented) {
+ return
+ }
+
+ event.stopPropagation()
+ onDismiss?.(event)
+ }
+
+ return isOpen
+ ? createPortal(
+
+
+ {children}
+
+ ,
+ document.body
+ )
+ : null
+}
+
+export default Modal
diff --git a/packages/store-ui/src/molecules/Modal/ModalContent.tsx b/packages/store-ui/src/molecules/Modal/ModalContent.tsx
new file mode 100644
index 0000000000..df0d029528
--- /dev/null
+++ b/packages/store-ui/src/molecules/Modal/ModalContent.tsx
@@ -0,0 +1,91 @@
+/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
+import type {
+ DetailedHTMLProps,
+ HTMLAttributes,
+ MouseEvent,
+ RefObject,
+} from 'react'
+import React, { useRef } from 'react'
+
+import useTrapFocus from './useTrapFocus'
+
+interface ModalContentPureProps
+ extends Omit<
+ DetailedHTMLProps, HTMLDivElement>,
+ 'ref'
+ > {
+ beforeElementRef: RefObject
+ trapFocusRef: RefObject
+ afterElementRef: RefObject
+ testId?: string
+}
+
+const ModalContentPure = ({
+ beforeElementRef,
+ trapFocusRef,
+ afterElementRef,
+ testId = 'store-modal-content',
+ children,
+ ...props
+}: ModalContentPureProps) => {
+ return (
+ <>
+
+
+ {children}
+
+
+ >
+ )
+}
+
+export type ModalContentProps = Omit<
+ ModalContentPureProps,
+ 'trapFocusRef' | 'onClick' | 'beforeElementRef' | 'afterElementRef'
+>
+
+const ModalContent = ({ children, ...props }: ModalContentProps) => {
+ const trapFocusRef = useRef(null)
+ const beforeElementRef = useRef(null)
+ const afterElementRef = useRef(null)
+
+ useTrapFocus({
+ beforeElementRef,
+ trapFocusRef,
+ afterElementRef,
+ })
+
+ return (
+ {
+ event.stopPropagation()
+ }}
+ >
+ {children}
+
+ )
+}
+
+export default ModalContent
diff --git a/packages/store-ui/src/molecules/Modal/index.tsx b/packages/store-ui/src/molecules/Modal/index.tsx
new file mode 100644
index 0000000000..65fde09f0c
--- /dev/null
+++ b/packages/store-ui/src/molecules/Modal/index.tsx
@@ -0,0 +1,2 @@
+export { default } from './Modal'
+export type { ModalProps } from './Modal'
diff --git a/packages/store-ui/src/molecules/Modal/stories/Modal.mdx b/packages/store-ui/src/molecules/Modal/stories/Modal.mdx
new file mode 100644
index 0000000000..c616454bc6
--- /dev/null
+++ b/packages/store-ui/src/molecules/Modal/stories/Modal.mdx
@@ -0,0 +1,20 @@
+import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'
+import Modal from '../Modal'
+
+# Modal
+
+
+
+## Props
+
+
+
+## CSS Selectors
+
+```css
+[data-store-modal-content] {}
+
+[data-modal-overlay] {}
+```
diff --git a/packages/store-ui/src/molecules/Modal/stories/Modal.stories.tsx b/packages/store-ui/src/molecules/Modal/stories/Modal.stories.tsx
new file mode 100644
index 0000000000..9f67875c03
--- /dev/null
+++ b/packages/store-ui/src/molecules/Modal/stories/Modal.stories.tsx
@@ -0,0 +1,45 @@
+import React, { useState } from 'react'
+import type { PropsWithChildren } from 'react'
+import type { Meta, Story } from '@storybook/react'
+
+import type { ModalProps } from '..'
+import Button from '../../../atoms/Button'
+import Component from '../Modal'
+import mdx from './Modal.mdx'
+
+const ModalTemplate: Story> = ({
+ children,
+ ...props
+}) => {
+ const [isOpen, setIsOpen] = useState(false)
+ const handleClose = () => setIsOpen(false)
+
+ return (
+ <>
+
+
+ My Modal Content
+
+
+
+
+
+ >
+ )
+}
+
+export const Modal = ModalTemplate.bind({})
+
+export default {
+ title: 'Molecules/Modal',
+ parameters: {
+ docs: {
+ page: mdx,
+ },
+ },
+} as Meta
diff --git a/packages/store-ui/src/molecules/Modal/useTrapFocus.ts b/packages/store-ui/src/molecules/Modal/useTrapFocus.ts
new file mode 100644
index 0000000000..f9759f384d
--- /dev/null
+++ b/packages/store-ui/src/molecules/Modal/useTrapFocus.ts
@@ -0,0 +1,110 @@
+import { useEffect, useRef } from 'react'
+import type { RefObject } from 'react'
+import type { FocusableElement } from 'tabbable'
+import { tabbable } from 'tabbable'
+
+interface TrapFocusParams {
+ beforeElementRef: RefObject
+ trapFocusRef: RefObject
+ afterElementRef: RefObject
+}
+
+/*
+ * Element that will maintain the focus inside trapFocusRef, focus the first element,
+ * and focus back on the element that was in focus when useTrapFocus was triggered.
+ *
+ * Inspired by Reakit useTrapFocus https://github.com/reakit/reakit/blob/a211d94da9f3b683182568a56479b91afb1b85ae/packages/reakit/src/Dialog/__utils/useFocusTrap.ts
+ */
+const useTrapFocus = ({
+ trapFocusRef,
+ beforeElementRef,
+ afterElementRef,
+}: TrapFocusParams) => {
+ const tabbableNodesRef = useRef()
+ const nodeToRestoreRef = useRef(
+ document.hasFocus() ? (document.activeElement as HTMLElement) : null
+ )
+
+ // Focus back on the element that was focused when useTrapFocus is triggered.
+ useEffect(() => {
+ const nodeToRestore = nodeToRestoreRef.current
+
+ return () => {
+ nodeToRestore?.focus()
+ }
+ }, [nodeToRestoreRef])
+
+ // Set focus on first tabbable element
+ useEffect(() => {
+ if (!trapFocusRef.current) {
+ return
+ }
+
+ if (!tabbableNodesRef.current) {
+ tabbableNodesRef.current = tabbable(trapFocusRef.current)
+ }
+
+ const [firstTabbable] = tabbableNodesRef.current
+
+ if (!firstTabbable) {
+ trapFocusRef.current.focus()
+
+ return
+ }
+
+ firstTabbable.focus()
+ }, [trapFocusRef])
+
+ // Handle loop focus. Set keydown and focusin event listeners
+ useEffect(() => {
+ if (
+ !trapFocusRef.current ||
+ !beforeElementRef.current ||
+ !afterElementRef.current
+ ) {
+ return
+ }
+
+ const beforeElement = beforeElementRef.current
+ const afterElement = afterElementRef.current
+ const trapFocus = trapFocusRef.current
+
+ const handleLoopFocus = (nativeEvent: FocusEvent) => {
+ if (!document.hasFocus()) {
+ return
+ }
+
+ tabbableNodesRef.current = tabbable(trapFocusRef.current!)
+
+ if (!tabbableNodesRef.current.length) {
+ trapFocus.focus()
+ }
+
+ /*
+ * Handle loop focus from beforeElementRef. This node can only be focused if the user press shift tab.
+ * It will focus the last element of the trapFocusRef.
+ */
+ if (nativeEvent.target === beforeElement) {
+ tabbableNodesRef.current[tabbableNodesRef.current.length - 1]?.focus()
+ }
+
+ /*
+ * Handle loop focus from afterElementRef. This node can only be focused if the user press tab.
+ * It will focus the first element of the trapFocusRef.
+ */
+ if (nativeEvent.target === afterElement) {
+ tabbableNodesRef.current[0]?.focus()
+ }
+ }
+
+ beforeElement?.addEventListener('focusin', handleLoopFocus)
+ afterElement?.addEventListener('focusin', handleLoopFocus)
+
+ return () => {
+ beforeElement?.removeEventListener('focusin', handleLoopFocus)
+ afterElement?.removeEventListener('focusin', handleLoopFocus)
+ }
+ }, [tabbableNodesRef, afterElementRef, beforeElementRef, trapFocusRef])
+}
+
+export default useTrapFocus
diff --git a/packages/store-ui/src/setupTests.ts b/packages/store-ui/src/setupTests.ts
index 264828a905..eceeb9a036 100644
--- a/packages/store-ui/src/setupTests.ts
+++ b/packages/store-ui/src/setupTests.ts
@@ -1 +1,4 @@
import '@testing-library/jest-dom/extend-expect'
+import { toHaveNoViolations } from 'jest-axe'
+
+expect.extend(toHaveNoViolations)
diff --git a/themes/theme-b2c-tailwind/src/atoms/overlay.css b/themes/theme-b2c-tailwind/src/atoms/overlay.css
index 34e5bb963e..749f4ada88 100644
--- a/themes/theme-b2c-tailwind/src/atoms/overlay.css
+++ b/themes/theme-b2c-tailwind/src/atoms/overlay.css
@@ -11,6 +11,6 @@
@apply bg-green-500 bg-opacity-50;
}
-[data-store-overlay][data-black] {
+[data-store-overlay][data-black], [data-modal-overlay] {
@apply bg-gray-500 bg-opacity-50;
}
diff --git a/themes/theme-b2c-tailwind/src/molecules/index.css b/themes/theme-b2c-tailwind/src/molecules/index.css
index 200fb0f606..4a0cf3dd5d 100644
--- a/themes/theme-b2c-tailwind/src/molecules/index.css
+++ b/themes/theme-b2c-tailwind/src/molecules/index.css
@@ -3,3 +3,4 @@
@import "./icon-button.css";
@import './price-range.css';
@import "./carousel.css";
+@import "./modal.css";
diff --git a/themes/theme-b2c-tailwind/src/molecules/modal.css b/themes/theme-b2c-tailwind/src/molecules/modal.css
new file mode 100644
index 0000000000..08ad79015d
--- /dev/null
+++ b/themes/theme-b2c-tailwind/src/molecules/modal.css
@@ -0,0 +1,9 @@
+[data-store-modal-content] {
+ @apply bg-white w-96 font-sans;
+ @apply p-5 rounded;
+}
+
+[data-store-modal-content] > [data-action-container] {
+ @apply flex justify-end;
+}
+
diff --git a/yarn.lock b/yarn.lock
index 60e7b041f4..43be6018eb 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6062,6 +6062,14 @@
dependencies:
"@types/istanbul-lib-report" "*"
+"@types/jest-axe@^3.5.3":
+ version "3.5.3"
+ resolved "https://registry.yarnpkg.com/@types/jest-axe/-/jest-axe-3.5.3.tgz#5af918553388aa0a448af75603b44093985778c6"
+ integrity sha512-ad9qI9f+00N8IlOuGh6dnZ6o0BDdV9VhGfTUr1zCejsPvOfZd6eohffe4JYxUoUuRYEftyMcaJ6Ux4+MsOpGHg==
+ dependencies:
+ "@types/jest" "*"
+ axe-core "^3.5.5"
+
"@types/jest@*":
version "26.0.23"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.23.tgz#a1b7eab3c503b80451d019efb588ec63522ee4e7"
@@ -6333,6 +6341,11 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
+"@types/tabbable@^3.1.1":
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/@types/tabbable/-/tabbable-3.1.1.tgz#af752e3ca8f4062fd4eccb2a8aa1394ceb5d62dc"
+ integrity sha512-W7XRO21VJ3Mh8YQWsjqfp6ooBNn5v+Njs1IahnElCv2g8zufYDQMxIS2seXbpmK/2HCdBQItW+xIimcPe1q+5g==
+
"@types/tapable@^1", "@types/tapable@^1.0.5":
version "1.0.7"
resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.7.tgz#545158342f949e8fd3bfd813224971ecddc3fac4"
@@ -7187,6 +7200,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
dependencies:
color-convert "^2.0.1"
+ansi-styles@^5.0.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
+ integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
+
ansi-to-html@^0.6.11:
version "0.6.14"
resolved "https://registry.yarnpkg.com/ansi-to-html/-/ansi-to-html-0.6.14.tgz#65fe6d08bba5dd9db33f44a20aec331e0010dad8"
@@ -7587,6 +7605,16 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
+axe-core@4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.2.1.tgz#2e50bcf10ee5b819014f6e342e41e45096239e34"
+ integrity sha512-evY7DN8qSIbsW2H/TWQ1bX3sXN1d4MNb5Vb4n7BzPuCwRHdkZ1H2eNLuSh73EoQqkGKUtju2G2HCcjCfhvZIAA==
+
+axe-core@^3.5.5:
+ version "3.5.6"
+ resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.5.6.tgz#e762a90d7f6dbd244ceacb4e72760ff8aad521b5"
+ integrity sha512-LEUDjgmdJoA3LqklSTwKYqkjcZ4HKc4ddIYGSAiSkr46NTjzg2L9RNB+lekO9P7Dlpa87+hBtzc2Fzn/+GUWMQ==
+
axe-core@^4.0.2:
version "4.1.3"
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.1.3.tgz#64a4c85509e0991f5168340edc4bedd1ceea6966"
@@ -8770,6 +8798,14 @@ chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.1, chalk@^2.3.
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
+chalk@4.1.0, chalk@^4.0, chalk@^4.0.0, chalk@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
+ integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==
+ dependencies:
+ ansi-styles "^4.1.0"
+ supports-color "^7.1.0"
+
chalk@^1.0.0, chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
@@ -8789,14 +8825,6 @@ chalk@^3.0.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
-chalk@^4.0, chalk@^4.0.0, chalk@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
- integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==
- dependencies:
- ansi-styles "^4.1.0"
- supports-color "^7.1.0"
-
chalk@^4.1.1, chalk@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
@@ -10704,6 +10732,11 @@ diff-sequences@^26.6.2:
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1"
integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==
+diff-sequences@^27.0.6:
+ version "27.0.6"
+ resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.0.6.tgz#3305cb2e55a033924054695cc66019fd7f8e5723"
+ integrity sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ==
+
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
@@ -12570,7 +12603,12 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
-follow-redirects@^1.0.0, follow-redirects@^1.14.0:
+follow-redirects@^1.0.0:
+ version "1.13.3"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267"
+ integrity sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==
+
+follow-redirects@^1.14.0:
version "1.14.3"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.3.tgz#6ada78118d8d24caee595595accdc0ac6abd022e"
integrity sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw==
@@ -15896,6 +15934,16 @@ iterate-value@^1.0.2:
es-get-iterator "^1.0.2"
iterate-iterator "^1.0.1"
+jest-axe@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/jest-axe/-/jest-axe-5.0.1.tgz#26c43643b2e5f2bd4900c1ab36f8283635957a6e"
+ integrity sha512-MMOWA6gT4pcZGbTLS8ZEqABH08Lnj5bInfLPpn9ADWX2wFF++odbbh8csmSfkwKjHaioVPzlCtIypAtxFDx/rw==
+ dependencies:
+ axe-core "4.2.1"
+ chalk "4.1.0"
+ jest-matcher-utils "27.0.2"
+ lodash.merge "4.6.2"
+
jest-changed-files@^25.5.0:
version "25.5.0"
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-25.5.0.tgz#141cc23567ceb3f534526f8614ba39421383634c"
@@ -15970,6 +16018,16 @@ jest-diff@^26.0.0:
jest-get-type "^26.3.0"
pretty-format "^26.6.2"
+jest-diff@^27.0.2:
+ version "27.2.0"
+ resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.2.0.tgz#bda761c360f751bab1e7a2fe2fc2b0a35ce8518c"
+ integrity sha512-QSO9WC6btFYWtRJ3Hac0sRrkspf7B01mGrrQEiCW6TobtViJ9RWL0EmOs/WnBsZDsI/Y2IoSHZA2x6offu0sYw==
+ dependencies:
+ chalk "^4.0.0"
+ diff-sequences "^27.0.6"
+ jest-get-type "^27.0.6"
+ pretty-format "^27.2.0"
+
jest-docblock@^25.3.0:
version "25.3.0"
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-25.3.0.tgz#8b777a27e3477cd77a168c05290c471a575623ef"
@@ -16022,6 +16080,11 @@ jest-get-type@^26.3.0:
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0"
integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==
+jest-get-type@^27.0.1, jest-get-type@^27.0.6:
+ version "27.0.6"
+ resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.0.6.tgz#0eb5c7f755854279ce9b68a9f1a4122f69047cfe"
+ integrity sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg==
+
jest-haste-map@^25.5.1:
version "25.5.1"
resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-25.5.1.tgz#1df10f716c1d94e60a1ebf7798c9fb3da2620943"
@@ -16114,6 +16177,16 @@ jest-leak-detector@^25.5.0:
jest-get-type "^25.2.6"
pretty-format "^25.5.0"
+jest-matcher-utils@27.0.2:
+ version "27.0.2"
+ resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.0.2.tgz#f14c060605a95a466cdc759acc546c6f4cbfc4f0"
+ integrity sha512-Qczi5xnTNjkhcIB0Yy75Txt+Ez51xdhOxsukN7awzq2auZQGPHcQrJ623PZj0ECDEMOk2soxWx05EXdXGd1CbA==
+ dependencies:
+ chalk "^4.0.0"
+ jest-diff "^27.0.2"
+ jest-get-type "^27.0.1"
+ pretty-format "^27.0.2"
+
jest-matcher-utils@^25.5.0:
version "25.5.0"
resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-25.5.0.tgz#fbc98a12d730e5d2453d7f1ed4a4d948e34b7867"
@@ -17143,7 +17216,7 @@ lodash.memoize@4.x, lodash.memoize@^4.1.2:
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
-lodash.merge@^4.6.2:
+lodash.merge@4.6.2, lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
@@ -20547,6 +20620,16 @@ pretty-format@^26.0.0, pretty-format@^26.6.2:
ansi-styles "^4.0.0"
react-is "^17.0.1"
+pretty-format@^27.0.2, pretty-format@^27.2.0:
+ version "27.2.0"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.2.0.tgz#ee37a94ce2a79765791a8649ae374d468c18ef19"
+ integrity sha512-KyJdmgBkMscLqo8A7K77omgLx5PWPiXJswtTtFV7XgVZv2+qPk6UivpXXO+5k6ZEbWIbLoKdx1pZ6ldINzbwTA==
+ dependencies:
+ "@jest/types" "^27.1.1"
+ ansi-regex "^5.0.0"
+ ansi-styles "^5.0.0"
+ react-is "^17.0.1"
+
pretty-hrtime@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
@@ -23457,6 +23540,11 @@ tabbable@^4.0.0:
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-4.0.0.tgz#5bff1d1135df1482cf0f0206434f15eadbeb9261"
integrity sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ==
+tabbable@^5.2.1:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.2.1.tgz#e3fda7367ddbb172dcda9f871c0fdb36d1c4cd9c"
+ integrity sha512-40pEZ2mhjaZzK0BnI+QGNjJO8UYx9pP5v7BGe17SORTO0OEuuaAwQTkAp8whcZvqon44wKFOikD+Al11K3JICQ==
+
table@^5.2.3:
version "5.4.6"
resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"