From 4f501abb72fe0657aff091e3cdb0d043137f1023 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Sun, 26 Mar 2023 23:07:09 -0700 Subject: [PATCH] Focus traps for dialog and dropdown --- package-lock.json | 50 +++++++++++ package.json | 1 + src-web/components/Overlay.tsx | 31 +++---- src-web/components/ResponsePane.tsx | 3 +- src-web/components/Workspace.tsx | 3 +- src-web/components/core/Button.tsx | 15 ++-- src-web/components/core/Dialog.tsx | 2 +- src-web/components/core/Dropdown.tsx | 116 ++++++++++++++++---------- src-web/components/core/Tabs/Tabs.tsx | 2 + src-web/hooks/Confirm.tsx | 10 ++- tailwind.config.cjs | 7 +- 11 files changed, 169 insertions(+), 71 deletions(-) diff --git a/package-lock.json b/package-lock.json index c0606a1f..53fe27b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "classnames": "^2.3.2", "cm6-graphql": "^0.0.4-canary-b30a2325.0", "codemirror": "^6.0.1", + "focus-trap-react": "^10.1.1", "format-graphql": "^1.4.0", "framer-motion": "^9.0.4", "parse-color": "^1.0.0", @@ -4363,6 +4364,28 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/focus-trap": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.4.0.tgz", + "integrity": "sha512-yI7FwUqU4TVb+7t6PaQ3spT/42r/KLEi8mtdGoQo2li/kFzmu9URmalTvw7xCCJtSOyhBxscvEAmvjeN9iHARg==", + "dependencies": { + "tabbable": "^6.1.1" + } + }, + "node_modules/focus-trap-react": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-10.1.1.tgz", + "integrity": "sha512-OtLeSIQPKFzMzbLHkGtfZYwGLMhTRHd3CDhfyd0DDx8tvXzlgpseClDiuiKoiIHZtdjsbXTfTmUuuLKaxrwSyQ==", + "dependencies": { + "focus-trap": "^7.4.0", + "tabbable": "^6.1.1" + }, + "peerDependencies": { + "prop-types": "^15.8.1", + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -7006,6 +7029,11 @@ "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", "dev": true }, + "node_modules/tabbable": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.1.1.tgz", + "integrity": "sha512-4kl5w+nCB44EVRdO0g/UGoOp3vlwgycUVtkk/7DPyeLZUCuNFFKCFG6/t/DgHLrUPHjrZg6s5tNm+56Q2B0xyg==" + }, "node_modules/tailwindcss": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.7.tgz", @@ -10700,6 +10728,23 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "focus-trap": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.4.0.tgz", + "integrity": "sha512-yI7FwUqU4TVb+7t6PaQ3spT/42r/KLEi8mtdGoQo2li/kFzmu9URmalTvw7xCCJtSOyhBxscvEAmvjeN9iHARg==", + "requires": { + "tabbable": "^6.1.1" + } + }, + "focus-trap-react": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-10.1.1.tgz", + "integrity": "sha512-OtLeSIQPKFzMzbLHkGtfZYwGLMhTRHd3CDhfyd0DDx8tvXzlgpseClDiuiKoiIHZtdjsbXTfTmUuuLKaxrwSyQ==", + "requires": { + "focus-trap": "^7.4.0", + "tabbable": "^6.1.1" + } + }, "for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -12582,6 +12627,11 @@ "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", "dev": true }, + "tabbable": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.1.1.tgz", + "integrity": "sha512-4kl5w+nCB44EVRdO0g/UGoOp3vlwgycUVtkk/7DPyeLZUCuNFFKCFG6/t/DgHLrUPHjrZg6s5tNm+56Q2B0xyg==" + }, "tailwindcss": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.7.tgz", diff --git a/package.json b/package.json index de3d7112..d40a1ce3 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "classnames": "^2.3.2", "cm6-graphql": "^0.0.4-canary-b30a2325.0", "codemirror": "^6.0.1", + "focus-trap-react": "^10.1.1", "format-graphql": "^1.4.0", "framer-motion": "^9.0.4", "parse-color": "^1.0.0", diff --git a/src-web/components/Overlay.tsx b/src-web/components/Overlay.tsx index 1e1bfc32..d6a09d9a 100644 --- a/src-web/components/Overlay.tsx +++ b/src-web/components/Overlay.tsx @@ -1,13 +1,14 @@ import classnames from 'classnames'; +import FocusTrap from 'focus-trap-react'; import type { ReactNode } from 'react'; import { motion } from 'framer-motion'; import { Portal } from './Portal'; interface Props { children: ReactNode; - onClick?: () => void; portalName: string; open: boolean; + onClose?: () => void; zIndex?: keyof typeof zIndexes; } @@ -19,22 +20,24 @@ const zIndexes: Record = { 50: 'z-50', }; -export function Overlay({ zIndex = 30, open, children, onClick, portalName }: Props) { +export function Overlay({ zIndex = 30, open, onClose, portalName, children }: Props) { return ( {open && ( - -
- {children} - + + +
+ {children} + + )} ); diff --git a/src-web/components/ResponsePane.tsx b/src-web/components/ResponsePane.tsx index 1eaca4c6..8ab1ba92 100644 --- a/src-web/components/ResponsePane.tsx +++ b/src-web/components/ResponsePane.tsx @@ -51,7 +51,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro className={classnames( className, 'bg-gray-50 max-h-full h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1 ', - 'dark:bg-gray-100 rounded-md overflow-hidden border border-highlight', + 'dark:bg-gray-100 rounded-md border border-highlight', 'shadow shadow-gray-100 dark:shadow-gray-0 relative', )} > @@ -103,6 +103,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro icon="triangleDown" className="ml-auto" size="sm" + iconSize="sm" /> diff --git a/src-web/components/Workspace.tsx b/src-web/components/Workspace.tsx index 0e6a411c..80479ce8 100644 --- a/src-web/components/Workspace.tsx +++ b/src-web/components/Workspace.tsx @@ -108,12 +108,13 @@ export default function Workspace() { {floating ? ( - + diff --git a/src-web/components/core/Button.tsx b/src-web/components/core/Button.tsx index 63378f56..7b639c9f 100644 --- a/src-web/components/core/Button.tsx +++ b/src-web/components/core/Button.tsx @@ -6,12 +6,12 @@ import { Icon } from './Icon'; const colorStyles = { custom: '', - default: 'text-gray-700 enabled:hover:bg-gray-700/10 enabled:hover:text-gray-1000', - gray: 'text-gray-800 bg-highlight enabled:hover:bg-gray-500/20 enabled:hover:text-gray-1000', - primary: 'bg-blue-400 text-white hover:bg-blue-500', - secondary: 'bg-violet-400 text-white hover:bg-violet-500', - warning: 'bg-orange-400 text-white hover:bg-orange-500', - danger: 'bg-red-400 text-white hover:bg-red-500', + default: 'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-1000', + gray: 'text-gray-800 bg-highlight enabled:hocus:bg-gray-500/20 enabled:hocus:text-gray-1000', + primary: 'bg-blue-400 text-white enabled:hocus:bg-blue-500', + secondary: 'bg-violet-400 text-white enabled:hocus:bg-violet-500', + warning: 'bg-orange-400 text-white enabled:hocus:bg-orange-500', + danger: 'bg-red-400 text-white enabled:hocus:bg-red-500', }; export type ButtonProps = HTMLAttributes & { @@ -44,7 +44,8 @@ const _Button = forwardRef(function Button( classnames( className, 'outline-none whitespace-nowrap', - 'border border-transparent focus-visible:border-blue-300', + // 'border border-transparent focus-visible:border-focus', + 'focus-visible:ring ring-blue-300', 'rounded-md flex items-center', colorStyles[color || 'default'], justify === 'start' && 'justify-start', diff --git a/src-web/components/core/Dialog.tsx b/src-web/components/core/Dialog.tsx index b41ee40f..da919c7b 100644 --- a/src-web/components/core/Dialog.tsx +++ b/src-web/components/core/Dialog.tsx @@ -34,7 +34,7 @@ export function Dialog({ ); return ( - +
{ + console.log(document.activeElement); + }); + const containerRef = useRef(null); const [menuStyles, setMenuStyles] = useState({}); @@ -155,53 +160,71 @@ function Menu({ className, items, onClose, triggerRect }: MenuProps) { }, [onClose], ); + + const handleFocus = useCallback( + (i: DropdownItem) => { + const index = items.findIndex((item) => item === i) ?? null; + setSelectedIndex(index); + }, + [items], + ); + const [selectedIndex, setSelectedIndex] = useState(null); return ( - - + ); } diff --git a/tailwind.config.cjs b/tailwind.config.cjs index b68b0367..248d3da2 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -1,3 +1,5 @@ +const plugin = require('tailwindcss/plugin') + /** @type {import("tailwindcss").Config} */ module.exports = { darkMode: ["class", "[data-appearance=\"dark\"]"], @@ -59,7 +61,10 @@ module.exports = { } }, plugins: [ - require("@tailwindcss/container-queries") + require("@tailwindcss/container-queries"), + plugin(function({ addVariant }) { + addVariant('hocus', ['&:hover', '&:focus']) + }) ] };