Focus trap and Modal Functionality (#145)

* Focus trap and Modal Functionality

* Changes

* Changes

* Update components/ui/Modal/Modal.tsx

Co-authored-by: Luis Alvarez D. <luis@vercel.com>

* changes

Co-authored-by: Luis Alvarez D. <luis@vercel.com>
This commit is contained in:
B 2021-01-09 11:03:03 -03:00 committed by GitHub
parent 86c396edd5
commit 66b8bdb6bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 538 additions and 735 deletions

View File

@ -94,7 +94,7 @@ const CartSidebarView: FC = () => {
My Cart
</h2>
<ul className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-accents-3 border-t border-accents-3">
{items.map((item) => (
{items.map((item: any) => (
<CartItem
key={item.id}
item={item}

View File

@ -9,9 +9,11 @@ import { useAcceptCookies } from '@lib/hooks/useAcceptCookies'
import { Sidebar, Button, Modal, LoadingDots } from '@components/ui'
import { CartSidebarView } from '@components/cart'
import LoginView from '@components/auth/LoginView'
import { CommerceProvider } from '@framework'
import type { Page } from '@framework/api/operations/get-all-pages'
const Loading = () => (
<div className="w-80 h-80 flex items-center text-center justify-center p-3">
<LoadingDots />
@ -22,10 +24,6 @@ const dynamicProps = {
loading: () => <Loading />,
}
const LoginView = dynamic(
() => import('@components/auth/LoginView'),
dynamicProps
)
const SignUpView = dynamic(
() => import('@components/auth/SignUpView'),
dynamicProps

View File

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

View File

@ -1,4 +1,4 @@
import { FC, useRef, useEffect } from 'react'
import { FC, useRef, useEffect, useCallback } from 'react'
import Portal from '@reach/portal'
import s from './Modal.module.css'
import { Cross } from '@components/icons'
@ -7,45 +7,53 @@ import {
enableBodyScroll,
clearAllBodyScrollLocks,
} from 'body-scroll-lock'
import FocusTrap from '@lib/focus-trap'
interface Props {
className?: string
children?: any
open?: boolean
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 handleKey = (e: KeyboardEvent) =>
useCallback(() => {
if (e.key === 'Escape') {
return onClose()
}
}, [onClose])
useEffect(() => {
if (ref.current) {
if (open) {
disableBodyScroll(ref.current)
window.addEventListener('keydown', handleKey)
} else {
enableBodyScroll(ref.current)
}
}
return () => {
window.removeEventListener('keydown', handleKey)
clearAllBodyScrollLocks()
}
}, [open])
}, [open, handleKey])
return (
<Portal>
{open ? (
<div className={s.root} ref={ref}>
<div className={s.modal}>
<div className="h-7 flex items-center justify-end w-full">
<button
onClick={() => onClose()}
aria-label="Close panel"
className="hover:text-gray-500 transition ease-in-out duration-150 focus:outline-none"
>
<Cross className="h-6 w-6" />
</button>
</div>
{children}
<div className={s.root}>
<div className={s.modal} role="dialog" ref={ref}>
<button
onClick={() => onClose()}
aria-label="Close panel"
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" />
</button>
<FocusTrap focusFirst>{children}</FocusTrap>
</div>
</div>
) : 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,
})
}

View File

@ -65,7 +65,8 @@
"react-dom": "^16.14.0",
"react-merge-refs": "^1.1.0",
"react-ticker": "^1.2.2",
"swr": "^0.3.11",
"swr": "^0.4.0",
"tabbable": "^5.1.5",
"tailwindcss": "^1.9"
},
"devDependencies": {

View File

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

1151
yarn.lock

File diff suppressed because it is too large Load Diff