Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/components/DashboardHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ export default function DashboardHeader() {

<div className="flex items-center gap-2 rounded-xl border border-[var(--border)] bg-[var(--card-muted)] px-3 py-2 shadow-sm">

<div>
<KeyboardShortcuts />

</div>

<div className="hover:scale-110 transition-transform duration-200">
<NotificationBell />
Expand Down
23 changes: 20 additions & 3 deletions src/components/KeyboardShortcuts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default function KeyboardShortcuts() {
const [announcement, setAnnouncement] = useState("");
const { theme, toggleTheme } = useTheme();
const keyboardToggleRef = useRef(false);
const shortcutsRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (keyboardToggleRef.current && theme !== undefined) {
Expand Down Expand Up @@ -59,24 +60,40 @@ export default function KeyboardShortcuts() {
};
}, [toggleTheme]);

useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (
shortcutsRef.current &&
!shortcutsRef.current.contains(e.target as Node)
) {
setIsOpen(false);
}
}

document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);

return (
<>
<div className="relative" ref={shortcutsRef}>
<div aria-live="polite" className="sr-only">
{announcement}
</div>

<button
type="button"
onClick={() => setIsOpen(true)}
onClick={() => setIsOpen((prev) => !prev)}
className="inline-flex h-10 items-center gap-1.5 rounded-full border border-[var(--border)] bg-[var(--card)] px-3 text-xs font-medium text-[var(--muted-foreground)] transition-colors hover:bg-[var(--control)] hover:text-[var(--card-foreground)]"
aria-label="Show keyboard shortcuts"
aria-expanded={isOpen}
aria-haspopup="dialog"
>
<kbd className="rounded bg-[var(--control)] px-1.5 py-0.5 text-[10px] font-bold text-[var(--card-foreground)]">
?
</kbd>
<span>Shortcuts</span>
</button>
<ShortcutsModal isOpen={isOpen} onClose={() => setIsOpen(false)} />
</>
</div>
);
}
114 changes: 56 additions & 58 deletions src/components/ShortcutsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,17 @@ const SHORTCUTS: ShortcutItem[] = [
{ key: "?", action: "Show shortcuts" },
];

export default function ShortcutsModal({ isOpen, onClose}: ShortcutsModalProps) {
export default function ShortcutsModal({
isOpen,
onClose,
}: ShortcutsModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
const closeBtnRef = useRef<HTMLButtonElement>(null);

useEffect(() => {
if (!isOpen) return;

if (closeBtnRef.current) {
closeBtnRef.current.focus();
}
closeBtnRef.current?.focus();

const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
Expand All @@ -48,16 +49,12 @@ export default function ShortcutsModal({ isOpen, onClose}: ShortcutsModalProps)
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];

if (e.shiftKey) {
if (document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
if (e.shiftKey && document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
} else if (!e.shiftKey && document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
}
};
Expand All @@ -72,52 +69,53 @@ export default function ShortcutsModal({ isOpen, onClose}: ShortcutsModalProps)

return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm transition-opacity"
onClick={onClose}
ref={modalRef}
role="dialog"
aria-labelledby="shortcuts-title"
className="absolute right-0 top-full z-50 mt-2 w-80 rounded-xl border border-[var(--border)] bg-[var(--card)] shadow-xl"
>
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
className="w-full max-w-md rounded-xl border border-[var(--border)] bg-[var(--card)] p-6 shadow-xl transition-transform"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between border-b border-[var(--border)] pb-4">
<h2 id="modal-title" className="text-lg font-semibold text-[var(--card-foreground)]">
Keyboard Shortcuts
</h2>
<button
ref={closeBtnRef}
type="button"
onClick={onClose}
className="rounded-lg p-1 text-[var(--muted-foreground)] hover:bg-[var(--control)] hover:text-[var(--card-foreground)] transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--accent)]"
aria-label="Close modal"
>
βœ•
</button>
</div>

<div className="mt-4 space-y-3">
{SHORTCUTS.map((item) => (
<div key={item.key} className="flex items-center justify-between py-1.5 border-b border-[var(--border)]/50 last:border-0">
<span className="text-sm text-[var(--muted-foreground)]">{item.action}</span>
<kbd className="min-w-[28px] text-center rounded-md border border-[var(--border)] bg-[var(--control)] px-2 py-1 text-xs font-semibold text-[var(--card-foreground)] shadow-sm">
{item.key}
</kbd>
</div>
))}
</div>

<div className="mt-6 pt-4 border-t border-[var(--border)] flex justify-end">
<button
type="button"
onClick={onClose}
className="rounded-lg bg-[var(--control)] px-4 py-2 text-sm font-medium text-[var(--card-foreground)] hover:bg-[var(--accent)] hover:text-[var(--accent-foreground)] transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--accent)]"
<div className="flex items-center justify-between border-b border-[var(--border)] px-4 py-3">
<h2
id="shortcuts-title"
className="text-sm font-semibold text-[var(--card-foreground)]"
>
Keyboard Shortcuts
</h2>
<button
ref={closeBtnRef}
type="button"
onClick={onClose}
className="rounded-lg p-1 text-[var(--muted-foreground)] transition-colors hover:bg-[var(--control)] hover:text-[var(--card-foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--accent)]"
aria-label="Close shortcuts"
>
x
</button>
</div>

<div className="px-4 py-3">
{SHORTCUTS.map((item) => (
<div
key={item.key}
className="flex items-center justify-between border-b border-[var(--border)]/50 py-2 last:border-0"
>
Got it
</button>
</div>
<span className="text-sm text-[var(--muted-foreground)]">
{item.action}
</span>
<kbd className="min-w-[28px] rounded-md border border-[var(--border)] bg-[var(--control)] px-2 py-1 text-center text-xs font-semibold text-[var(--card-foreground)] shadow-sm">
{item.key}
</kbd>
</div>
))}
</div>

<div className="flex justify-end border-t border-[var(--border)] px-4 py-3">
<button
type="button"
onClick={onClose}
className="rounded-lg bg-[var(--control)] px-4 py-2 text-sm font-medium text-[var(--card-foreground)] transition-colors hover:bg-[var(--accent)] hover:text-[var(--accent-foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--accent)]"
>
Got it
</button>
</div>
</div>
);
Expand Down
Loading