Focus trap and Modal Functionality

This commit is contained in:
Belen Curcio 2021-01-06 12:54:53 -03:00
parent 7401f4e417
commit 26ac4fd863
8 changed files with 8063 additions and 20 deletions

View File

@ -8,7 +8,7 @@ import { Navbar, Footer } from '@components/common'
import { useAcceptCookies } from '@lib/hooks/useAcceptCookies' import { useAcceptCookies } from '@lib/hooks/useAcceptCookies'
import { Sidebar, Button, Modal, LoadingDots } from '@components/ui' import { Sidebar, Button, Modal, LoadingDots } from '@components/ui'
import { CartSidebarView } from '@components/cart' import { CartSidebarView } from '@components/cart'
import LoginView from '@components/auth/LoginView'
import { CommerceProvider } from '@bigcommerce/storefront-data-hooks' import { CommerceProvider } from '@bigcommerce/storefront-data-hooks'
import type { Page } from '@bigcommerce/storefront-data-hooks/api/operations/get-all-pages' import type { Page } from '@bigcommerce/storefront-data-hooks/api/operations/get-all-pages'
@ -22,10 +22,6 @@ const dynamicProps = {
loading: () => <Loading />, loading: () => <Loading />,
} }
const LoginView = dynamic(
() => import('@components/auth/LoginView'),
dynamicProps
)
const SignUpView = dynamic( const SignUpView = dynamic(
() => import('@components/auth/SignUpView'), () => import('@components/auth/SignUpView'),
dynamicProps dynamicProps

View File

@ -4,7 +4,7 @@
} }
.modal { .modal {
@apply bg-primary p-12 border border-accents-2; @apply bg-primary p-12 border border-accents-2 relative;
} }
.modal:focus { .modal:focus {

View File

@ -7,26 +7,35 @@ import {
enableBodyScroll, enableBodyScroll,
clearAllBodyScrollLocks, clearAllBodyScrollLocks,
} from 'body-scroll-lock' } from 'body-scroll-lock'
import FocusTrap from '@lib/focus-trap'
interface Props { interface Props {
className?: string className?: string
children?: any children?: any
open?: boolean open?: boolean
onClose: () => void onClose: () => void
onEnter?: () => void | null
} }
const Modal: FC<Props> = ({ children, open, onClose }) => { const Modal: FC<Props> = ({ children, open, onClose, onEnter = null }) => {
const ref = useRef() as React.MutableRefObject<HTMLDivElement> const ref = useRef() as React.MutableRefObject<HTMLDivElement>
const handleKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
return onClose()
}
}
useEffect(() => { useEffect(() => {
if (ref.current) { if (ref.current) {
if (open) { if (open) {
disableBodyScroll(ref.current) disableBodyScroll(ref.current)
window.addEventListener('keydown', handleKey)
} else { } else {
enableBodyScroll(ref.current) enableBodyScroll(ref.current)
} }
} }
return () => { return () => {
window.removeEventListener('keydown', handleKey)
clearAllBodyScrollLocks() clearAllBodyScrollLocks()
} }
}, [open]) }, [open])
@ -34,18 +43,16 @@ const Modal: FC<Props> = ({ children, open, onClose }) => {
return ( return (
<Portal> <Portal>
{open ? ( {open ? (
<div className={s.root} ref={ref}> <div className={s.root}>
<div className={s.modal}> <div className={s.modal} role="dialog" ref={ref}>
<div className="h-7 flex items-center justify-end w-full">
<button <button
onClick={() => onClose()} onClick={() => onClose()}
aria-label="Close panel" aria-label="Close panel"
className="hover:text-gray-500 transition ease-in-out duration-150 focus:outline-none" className="hover:text-gray-500 transition ease-in-out duration-150 focus:outline-none absolute right-0 top-0 m-6"
> >
<Cross className="h-6 w-6" /> <Cross className="h-6 w-6" />
</button> </button>
</div> <FocusTrap focusFirst>{children}</FocusTrap>
{children}
</div> </div>
</div> </div>
) : null} ) : null}

64
lib/focus-trap.tsx Normal file
View File

@ -0,0 +1,64 @@
import React, { useEffect, RefObject } from 'react'
import { tabbable } from 'tabbable'
interface Props {
children: React.ReactNode | any
focusFirst?: boolean
}
export default function FocusTrap({ children, focusFirst = false }: Props) {
const root: RefObject<any> = React.useRef()
const anchor: RefObject<any> = React.useRef(document.activeElement)
const returnFocus = () => {
// Returns focus to the last focused element prior to trap.
if (anchor) {
anchor.current.focus()
}
}
const trapFocus = () => {
// Focus the container element
if (root.current) {
root.current.focus()
if (focusFirst) {
selectFirstFocusableEl()
}
}
}
const selectFirstFocusableEl = () => {
// Try to find focusable elements, if match then focus
// Up to 6 seconds of load time threshold
let match = false
let end = 60 // Try to find match at least n times
let i = 0
const timer = setInterval(() => {
if (!match !== i > end) {
match = !!tabbable(root.current).length
if (match) {
// Attempt to focus the first el
tabbable(root.current)[0].focus()
}
i = i + 1
} else {
// Clear interval after n attempts
clearInterval(timer)
}
}, 100)
}
useEffect(() => {
setTimeout(trapFocus, 20)
return () => {
returnFocus()
}
}, [root, children])
return React.createElement('div', {
ref: root,
children,
className: 'outline-none focus-trap',
tabIndex: -1,
})
}

7965
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -65,6 +65,7 @@
"react-intersection-observer": "^8.30.1", "react-intersection-observer": "^8.30.1",
"react-merge-refs": "^1.1.0", "react-merge-refs": "^1.1.0",
"react-ticker": "^1.2.2", "react-ticker": "^1.2.2",
"tabbable": "^5.1.5",
"tailwindcss": "^1.9" "tailwindcss": "^1.9"
}, },
"devDependencies": { "devDependencies": {

View File

@ -8,6 +8,11 @@ module.exports = {
'./pages/**/*.{js,ts,jsx,tsx}', './pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}',
], ],
options: {
safelist: {
standard: ['outline-none'],
},
},
}, },
theme: { theme: {
extend: { extend: {

View File

@ -4463,6 +4463,11 @@ swr@0.3.6:
dependencies: dependencies:
dequal "2.0.2" dequal "2.0.2"
tabbable@^5.1.5:
version "5.1.5"
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.1.5.tgz#efec48ede268d511c261e3b81facbb4782a35147"
integrity sha512-oVAPrWgLLqrbvQE8XqcU7CVBq6SQbaIbHkhOca3u7/jzuQvyZycrUKPCGr04qpEIUslmUlULbSeN+m3QrKEykA==
tailwindcss@^1.9: tailwindcss@^1.9:
version "1.9.6" version "1.9.6"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-1.9.6.tgz#0c5089911d24e1e98e592a31bfdb3d8f34ecf1a0" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-1.9.6.tgz#0c5089911d24e1e98e592a31bfdb3d8f34ecf1a0"