mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-23 17:28:29 +02:00
Focus traps for dialog and dropdown
This commit is contained in:
50
package-lock.json
generated
50
package-lock.json
generated
@@ -30,6 +30,7 @@
|
|||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"cm6-graphql": "^0.0.4-canary-b30a2325.0",
|
"cm6-graphql": "^0.0.4-canary-b30a2325.0",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
|
"focus-trap-react": "^10.1.1",
|
||||||
"format-graphql": "^1.4.0",
|
"format-graphql": "^1.4.0",
|
||||||
"framer-motion": "^9.0.4",
|
"framer-motion": "^9.0.4",
|
||||||
"parse-color": "^1.0.0",
|
"parse-color": "^1.0.0",
|
||||||
@@ -4363,6 +4364,28 @@
|
|||||||
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
|
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/for-each": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
||||||
@@ -7006,6 +7029,11 @@
|
|||||||
"integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==",
|
"integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "3.2.7",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.7.tgz",
|
||||||
@@ -10700,6 +10728,23 @@
|
|||||||
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
|
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
|
||||||
"dev": true
|
"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": {
|
"for-each": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
||||||
@@ -12582,6 +12627,11 @@
|
|||||||
"integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==",
|
"integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==",
|
||||||
"dev": true
|
"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": {
|
"tailwindcss": {
|
||||||
"version": "3.2.7",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.7.tgz",
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"cm6-graphql": "^0.0.4-canary-b30a2325.0",
|
"cm6-graphql": "^0.0.4-canary-b30a2325.0",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
|
"focus-trap-react": "^10.1.1",
|
||||||
"format-graphql": "^1.4.0",
|
"format-graphql": "^1.4.0",
|
||||||
"framer-motion": "^9.0.4",
|
"framer-motion": "^9.0.4",
|
||||||
"parse-color": "^1.0.0",
|
"parse-color": "^1.0.0",
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import FocusTrap from 'focus-trap-react';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Portal } from './Portal';
|
import { Portal } from './Portal';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
onClick?: () => void;
|
|
||||||
portalName: string;
|
portalName: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
onClose?: () => void;
|
||||||
zIndex?: keyof typeof zIndexes;
|
zIndex?: keyof typeof zIndexes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,22 +20,24 @@ const zIndexes: Record<number, string> = {
|
|||||||
50: 'z-50',
|
50: 'z-50',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Overlay({ zIndex = 30, open, children, onClick, portalName }: Props) {
|
export function Overlay({ zIndex = 30, open, onClose, portalName, children }: Props) {
|
||||||
return (
|
return (
|
||||||
<Portal name={portalName}>
|
<Portal name={portalName}>
|
||||||
{open && (
|
{open && (
|
||||||
<motion.div
|
<FocusTrap>
|
||||||
className={classnames('fixed inset-0', zIndexes[zIndex])}
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
className={classnames('fixed inset-0', zIndexes[zIndex])}
|
||||||
animate={{ opacity: 1 }}
|
initial={{ opacity: 0 }}
|
||||||
>
|
animate={{ opacity: 1 }}
|
||||||
<div
|
>
|
||||||
aria-hidden
|
<div
|
||||||
onClick={onClick}
|
aria-hidden
|
||||||
className="absolute inset-0 bg-gray-600/60 dark:bg-black/50"
|
onClick={onClose}
|
||||||
/>
|
className="absolute inset-0 bg-gray-600/60 dark:bg-black/50"
|
||||||
{children}
|
/>
|
||||||
</motion.div>
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
</FocusTrap>
|
||||||
)}
|
)}
|
||||||
</Portal>
|
</Portal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
|||||||
className={classnames(
|
className={classnames(
|
||||||
className,
|
className,
|
||||||
'bg-gray-50 max-h-full h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1 ',
|
'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',
|
'shadow shadow-gray-100 dark:shadow-gray-0 relative',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -103,6 +103,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
|||||||
icon="triangleDown"
|
icon="triangleDown"
|
||||||
className="ml-auto"
|
className="ml-auto"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
iconSize="sm"
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|||||||
@@ -108,12 +108,13 @@ export default function Workspace() {
|
|||||||
<WorkspaceHeader className="pointer-events-none" />
|
<WorkspaceHeader className="pointer-events-none" />
|
||||||
</HeaderSize>
|
</HeaderSize>
|
||||||
{floating ? (
|
{floating ? (
|
||||||
<Overlay open={!sidebar.hidden} portalName="sidebar" onClick={sidebar.hide}>
|
<Overlay open={!sidebar.hidden} portalName="sidebar" onClose={sidebar.hide}>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, x: -10 }}
|
initial={{ opacity: 0, x: -10 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
className={classnames(
|
className={classnames(
|
||||||
'absolute top-0 left-0 bottom-0 bg-gray-100 border-r border-highlight w-[14rem]',
|
'absolute top-0 left-0 bottom-0 bg-gray-100 border-r border-highlight w-[14rem]',
|
||||||
|
'grid grid-rows-[auto_1fr]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<HeaderSize className="border-transparent">
|
<HeaderSize className="border-transparent">
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import { Icon } from './Icon';
|
|||||||
|
|
||||||
const colorStyles = {
|
const colorStyles = {
|
||||||
custom: '',
|
custom: '',
|
||||||
default: 'text-gray-700 enabled:hover:bg-gray-700/10 enabled:hover:text-gray-1000',
|
default: 'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-1000',
|
||||||
gray: 'text-gray-800 bg-highlight enabled:hover:bg-gray-500/20 enabled:hover: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 hover:bg-blue-500',
|
primary: 'bg-blue-400 text-white enabled:hocus:bg-blue-500',
|
||||||
secondary: 'bg-violet-400 text-white hover:bg-violet-500',
|
secondary: 'bg-violet-400 text-white enabled:hocus:bg-violet-500',
|
||||||
warning: 'bg-orange-400 text-white hover:bg-orange-500',
|
warning: 'bg-orange-400 text-white enabled:hocus:bg-orange-500',
|
||||||
danger: 'bg-red-400 text-white hover:bg-red-500',
|
danger: 'bg-red-400 text-white enabled:hocus:bg-red-500',
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ButtonProps = HTMLAttributes<HTMLElement> & {
|
export type ButtonProps = HTMLAttributes<HTMLElement> & {
|
||||||
@@ -44,7 +44,8 @@ const _Button = forwardRef<any, ButtonProps>(function Button(
|
|||||||
classnames(
|
classnames(
|
||||||
className,
|
className,
|
||||||
'outline-none whitespace-nowrap',
|
'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',
|
'rounded-md flex items-center',
|
||||||
colorStyles[color || 'default'],
|
colorStyles[color || 'default'],
|
||||||
justify === 'start' && 'justify-start',
|
justify === 'start' && 'justify-start',
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function Dialog({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay open={open} onClick={onClose} portalName="dialog">
|
<Overlay open={open} onClose={onClose} portalName="dialog">
|
||||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||||
<div
|
<div
|
||||||
role="dialog"
|
role="dialog"
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import FocusTrap from 'focus-trap-react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import type { CSSProperties, HTMLAttributes, MouseEvent, ReactElement, ReactNode } from 'react';
|
import type { CSSProperties, HTMLAttributes, MouseEvent, ReactElement, ReactNode } from 'react';
|
||||||
import { Children, cloneElement, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { Children, cloneElement, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useKeyPressEvent } from 'react-use';
|
import { useKeyPressEvent, useMount } from 'react-use';
|
||||||
import { Portal } from '../Portal';
|
import { Portal } from '../Portal';
|
||||||
import { Separator } from './Separator';
|
import { Separator } from './Separator';
|
||||||
import { VStack } from './Stacks';
|
import { VStack } from './Stacks';
|
||||||
@@ -82,6 +83,10 @@ interface MenuProps {
|
|||||||
function Menu({ className, items, onClose, triggerRect }: MenuProps) {
|
function Menu({ className, items, onClose, triggerRect }: MenuProps) {
|
||||||
if (triggerRect === undefined) return null;
|
if (triggerRect === undefined) return null;
|
||||||
|
|
||||||
|
useMount(() => {
|
||||||
|
console.log(document.activeElement);
|
||||||
|
});
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [menuStyles, setMenuStyles] = useState<CSSProperties>({});
|
const [menuStyles, setMenuStyles] = useState<CSSProperties>({});
|
||||||
|
|
||||||
@@ -155,53 +160,71 @@ function Menu({ className, items, onClose, triggerRect }: MenuProps) {
|
|||||||
},
|
},
|
||||||
[onClose],
|
[onClose],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleFocus = useCallback(
|
||||||
|
(i: DropdownItem) => {
|
||||||
|
const index = items.findIndex((item) => item === i) ?? null;
|
||||||
|
setSelectedIndex(index);
|
||||||
|
},
|
||||||
|
[items],
|
||||||
|
);
|
||||||
|
|
||||||
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal name="dropdown">
|
<Portal name="dropdown">
|
||||||
<button aria-hidden title="close" className="fixed inset-0" onClick={onClose} />
|
<FocusTrap>
|
||||||
<motion.div
|
<div>
|
||||||
initial={{ opacity: 0, y: -5, scale: 0.98 }}
|
<div tabIndex={-1} aria-hidden className="fixed inset-0" onClick={onClose} />
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
<motion.div
|
||||||
role="menu"
|
tabIndex={0}
|
||||||
aria-orientation="vertical"
|
initial={{ opacity: 0, y: -5, scale: 0.98 }}
|
||||||
dir="ltr"
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
ref={containerRef}
|
role="menu"
|
||||||
style={containerStyles}
|
aria-orientation="vertical"
|
||||||
className={classnames(className, 'mt-1 pointer-events-auto fixed z-50')}
|
dir="ltr"
|
||||||
>
|
ref={containerRef}
|
||||||
<span
|
style={containerStyles}
|
||||||
style={triangleStyles}
|
className={classnames(className, 'outline-none mt-1 pointer-events-auto fixed z-50')}
|
||||||
aria-hidden
|
|
||||||
className="bg-gray-50 absolute rotate-45 border-gray-200 border-t border-l"
|
|
||||||
/>
|
|
||||||
{containerStyles && (
|
|
||||||
<VStack
|
|
||||||
space={0.5}
|
|
||||||
ref={initMenu}
|
|
||||||
style={menuStyles}
|
|
||||||
className={classnames(
|
|
||||||
className,
|
|
||||||
'h-auto bg-gray-50 rounded-md shadow-lg dark:shadow-gray-0 py-1.5 border',
|
|
||||||
'border-gray-200 overflow-auto mb-1 mx-0.5',
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{items.map((item, i) => {
|
<span
|
||||||
if (item.type === 'separator')
|
aria-hidden
|
||||||
return <Separator key={i} className="my-1.5" label={item.label} />;
|
style={triangleStyles}
|
||||||
if (item.hidden) return null;
|
className="bg-gray-50 absolute rotate-45 border-gray-200 border-t border-l"
|
||||||
return (
|
/>
|
||||||
<MenuItem
|
{containerStyles && (
|
||||||
focused={i === selectedIndex}
|
<VStack
|
||||||
onSelect={handleSelect}
|
space={0.5}
|
||||||
key={i + item.label}
|
ref={initMenu}
|
||||||
item={item}
|
style={menuStyles}
|
||||||
/>
|
className={classnames(
|
||||||
);
|
className,
|
||||||
})}
|
'h-auto bg-gray-50 rounded-md shadow-lg dark:shadow-gray-0 py-1.5 border',
|
||||||
</VStack>
|
'border-gray-200 overflow-auto mb-1 mx-0.5',
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
>
|
||||||
|
{items.map((item, i) => {
|
||||||
|
if (item.type === 'separator') {
|
||||||
|
return <Separator key={i} className="my-1.5" label={item.label} />;
|
||||||
|
}
|
||||||
|
if (item.hidden) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
focused={i === selectedIndex}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
key={i + item.label}
|
||||||
|
item={item}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</VStack>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</FocusTrap>
|
||||||
</Portal>
|
</Portal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -210,11 +233,13 @@ interface MenuItemProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
item: DropdownItem;
|
item: DropdownItem;
|
||||||
onSelect: (item: DropdownItem) => void;
|
onSelect: (item: DropdownItem) => void;
|
||||||
|
onFocus: (item: DropdownItem) => void;
|
||||||
focused: boolean;
|
focused: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenuItem({ className, focused, item, onSelect, ...props }: MenuItemProps) {
|
function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: MenuItemProps) {
|
||||||
const handleClick = useCallback(() => onSelect?.(item), [item, onSelect]);
|
const handleClick = useCallback(() => onSelect?.(item), [item, onSelect]);
|
||||||
|
const handleFocus = useCallback(() => onFocus?.(item), [item, onFocus]);
|
||||||
|
|
||||||
const initRef = useCallback(
|
const initRef = useCallback(
|
||||||
(el: HTMLButtonElement | null) => {
|
(el: HTMLButtonElement | null) => {
|
||||||
@@ -231,9 +256,10 @@ function MenuItem({ className, focused, item, onSelect, ...props }: MenuItemProp
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
ref={initRef}
|
ref={initRef}
|
||||||
tabIndex={focused ? 0 : -1}
|
tabIndex={-1}
|
||||||
onMouseEnter={(e) => e.currentTarget.focus()}
|
onMouseEnter={(e) => e.currentTarget.focus()}
|
||||||
onMouseLeave={(e) => e.currentTarget.blur()}
|
onMouseLeave={(e) => e.currentTarget.blur()}
|
||||||
|
onFocus={handleFocus}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className={classnames(
|
className={classnames(
|
||||||
className,
|
className,
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ export function Tabs<T>({
|
|||||||
className={classnames(
|
className={classnames(
|
||||||
tabListClassName,
|
tabListClassName,
|
||||||
'h-md flex items-center overflow-x-auto pb-0.5 hide-scrollbars',
|
'h-md flex items-center overflow-x-auto pb-0.5 hide-scrollbars',
|
||||||
|
// Give space for button focus states within overflow boundary
|
||||||
|
'px-2 -mx-2',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<HStack space={1} className="flex-shrink-0">
|
<HStack space={1} className="flex-shrink-0">
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { useRef } from 'react';
|
||||||
|
import { useMount } from 'react-use';
|
||||||
import { Button } from '../components/core/Button';
|
import { Button } from '../components/core/Button';
|
||||||
import { HStack } from '../components/core/Stacks';
|
import { HStack } from '../components/core/Stacks';
|
||||||
|
|
||||||
@@ -5,12 +7,18 @@ interface Props {
|
|||||||
hide: () => void;
|
hide: () => void;
|
||||||
}
|
}
|
||||||
export function Confirm({ hide }: Props) {
|
export function Confirm({ hide }: Props) {
|
||||||
|
const focusRef = (el: HTMLButtonElement | null) => {
|
||||||
|
el?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack space={2} justifyContent="end">
|
<HStack space={2} justifyContent="end">
|
||||||
<Button color="gray" onClick={hide}>
|
<Button color="gray" onClick={hide}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="primary">Confirm</Button>
|
<Button ref={focusRef} color="primary">
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
</HStack>
|
</HStack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
const plugin = require('tailwindcss/plugin')
|
||||||
|
|
||||||
/** @type {import("tailwindcss").Config} */
|
/** @type {import("tailwindcss").Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
darkMode: ["class", "[data-appearance=\"dark\"]"],
|
darkMode: ["class", "[data-appearance=\"dark\"]"],
|
||||||
@@ -59,7 +61,10 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
require("@tailwindcss/container-queries")
|
require("@tailwindcss/container-queries"),
|
||||||
|
plugin(function({ addVariant }) {
|
||||||
|
addVariant('hocus', ['&:hover', '&:focus'])
|
||||||
|
})
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user