mirror of
https://github.com/vercel/commerce.git
synced 2025-06-18 13:11:23 +00:00
Focus trap and Modal Functionality
This commit is contained in:
parent
7401f4e417
commit
26ac4fd863
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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 absolute right-0 top-0 m-6"
|
||||||
className="hover:text-gray-500 transition ease-in-out duration-150 focus:outline-none"
|
>
|
||||||
>
|
<Cross className="h-6 w-6" />
|
||||||
<Cross className="h-6 w-6" />
|
</button>
|
||||||
</button>
|
<FocusTrap focusFirst>{children}</FocusTrap>
|
||||||
</div>
|
|
||||||
{children}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
64
lib/focus-trap.tsx
Normal file
64
lib/focus-trap.tsx
Normal 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
7965
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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": {
|
||||||
|
@ -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: {
|
||||||
|
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user