Replace Focusable navigation wrappers with tabIndex-based focus handling

This commit is contained in:
Mike Cao committed 2026-08-02 19:42:53 -07:00
1 parent d336eba27d
commit 5d8cf10bf1
11 files changed
+133 -382

No files matched your search

+15 -17
View File
@@ -2,7 +2,6 @@ import {
Button,
type ButtonProps,
Column,
Focusable,
Icon,
Row,
Text,
@@ -111,22 +110,21 @@ export function SideNav(props: any) {
{links.map(({ id, path, label, icon }) => {
const isSelected = pathname.startsWith(renderUrl(path, false));
const content = (
<Focusable>
<Row
alignItems="center"
hover={{ backgroundColor: 'surface-sunken' }}
backgroundColor={isSelected ? 'surface-sunken' : undefined}
borderRadius
minHeight="40px"
>
<IconLabel
icon={icon}
label={isCollapsed ? '' : label}
weight={isSelected ? 'bold' : undefined}
padding
/>
</Row>
</Focusable>
<Row
tabIndex={0}
alignItems="center"
hover={{ backgroundColor: 'surface-sunken' }}
backgroundColor={isSelected ? 'surface-sunken' : undefined}
borderRadius
minHeight="40px"
>
<IconLabel
icon={icon}
label={isCollapsed ? '' : label}
weight={isSelected ? 'bold' : undefined}
padding
/>
</Row>
);
return (
<Link key={id} href={renderUrl(path, false)} role="button">
+10 -11
View File
@@ -1,4 +1,4 @@
import { Column, Focusable, Row, Tooltip, TooltipTrigger } from '@umami/react-zen';
import { Column, Row, Tooltip, TooltipTrigger } from '@umami/react-zen';
import { IconLabel } from '@/components/common/IconLabel';
import Link from '@/components/common/Link';
import { NavMenu } from '@/components/common/NavMenu';
@@ -49,16 +49,15 @@ export function AdminNav({ onItemClick }: { onItemClick?: () => void }) {
<Column gap="2">
<Link href={renderUrl('/websites', false)} role="button" onClick={onItemClick}>
<TooltipTrigger delay={0}>
<Focusable>
<Row
alignItems="center"
hover={{ backgroundColor: 'surface-sunken' }}
borderRadius
minHeight="40px"
>
<IconLabel icon={<ArrowLeft />} label={t(labels.back)} padding />
</Row>
</Focusable>
<Row
tabIndex={0}
alignItems="center"
hover={{ backgroundColor: 'surface-sunken' }}
borderRadius
minHeight="40px"
>
<IconLabel icon={<ArrowLeft />} label={t(labels.back)} padding />
</Row>
<Tooltip placement="right">{t(labels.back)}</Tooltip>
</TooltipTrigger>
</Link>
@@ -1,13 +1,4 @@
import {
Button,
Column,
Focusable,
ListItem,
Row,
Select,
Text,
TextField,
} from '@umami/react-zen';
import { Button, Column, ListItem, Row, Select, Text, TextField } from '@umami/react-zen';
import { useEffect, useMemo, useState } from 'react';
import { useApi, useMessages } from '@/components/hooks';
import { LinkSelect } from '@/components/input/LinkSelect';
@@ -465,34 +456,34 @@ export function BoardComponentSelect({
const Icon = def.icon;
return (
<Focusable key={def.type}>
<Row
gap="3"
alignItems="flex-start"
paddingX="3"
paddingY="2"
borderRadius
backgroundColor={
selectedDef?.type === def.type ? 'surface-sunken' : undefined
}
hover={{ backgroundColor: 'surface-sunken' }}
style={{ cursor: 'pointer' }}
onClick={() => handleSelectComponent(def)}
>
<Icon size={16} />
<Column gap="1">
<Text
size="sm"
weight={selectedDef?.type === def.type ? 'bold' : undefined}
>
{def.name}
</Text>
<Text size="xs" color="muted">
{def.description}
</Text>
</Column>
</Row>
</Focusable>
<Row
key={def.type}
tabIndex={0}
gap="3"
alignItems="flex-start"
paddingX="3"
paddingY="2"
borderRadius
backgroundColor={
selectedDef?.type === def.type ? 'surface-sunken' : undefined
}
hover={{ backgroundColor: 'surface-sunken' }}
style={{ cursor: 'pointer' }}
onClick={() => handleSelectComponent(def)}
>
<Icon size={16} />
<Column gap="1">
<Text
size="sm"
weight={selectedDef?.type === def.type ? 'bold' : undefined}
>
{def.name}
</Text>
<Text size="xs" color="muted">
{def.description}
</Text>
</Column>
</Row>
);
})}
</Column>
+25 -27
View File
@@ -1,4 +1,4 @@
import { Column, Focusable, Row, Text, Tooltip, TooltipTrigger } from '@umami/react-zen';
import { Column, Row, Text, Tooltip, TooltipTrigger } from '@umami/react-zen';
import { IconLabel } from '@/components/common/IconLabel';
import Link from '@/components/common/Link';
import { useMessages, useNavigation } from '@/components/hooks';
@@ -60,16 +60,15 @@ export function SettingsNav({
<Link href={renderUrl('/websites', false)} role="button" onClick={onItemClick}>
{(() => {
const content = (
<Focusable>
<Row
alignItems="center"
hover={{ backgroundColor: 'surface-sunken' }}
borderRadius
minHeight="40px"
>
<IconLabel icon={<ArrowLeft />} label={isCollapsed ? '' : t(labels.back)} padding />
</Row>
</Focusable>
<Row
tabIndex={0}
alignItems="center"
hover={{ backgroundColor: 'surface-sunken' }}
borderRadius
minHeight="40px"
>
<IconLabel icon={<ArrowLeft />} label={isCollapsed ? '' : t(labels.back)} padding />
</Row>
);
return isCollapsed ? (
@@ -92,22 +91,21 @@ export function SettingsNav({
{sectionItems.map(({ id, path, label, icon }) => {
const isSelected = selectedKey === id;
const content = (
<Focusable>
<Row
alignItems="center"
hover={{ backgroundColor: 'surface-sunken' }}
backgroundColor={isSelected ? 'surface-sunken' : undefined}
borderRadius
minHeight="40px"
>
<IconLabel
icon={icon}
label={isCollapsed ? '' : label}
weight={isSelected ? 'bold' : undefined}
padding
/>
</Row>
</Focusable>
<Row
tabIndex={0}
alignItems="center"
hover={{ backgroundColor: 'surface-sunken' }}
backgroundColor={isSelected ? 'surface-sunken' : undefined}
borderRadius
minHeight="40px"
>
<IconLabel
icon={icon}
label={isCollapsed ? '' : label}
weight={isSelected ? 'bold' : undefined}
padding
/>
</Row>
);
return (
<Link key={id} href={path} role="button" onClick={onItemClick}>
@@ -1,4 +1,4 @@
import { Column, Focusable, Icon, Row, Text, Tooltip, TooltipTrigger } from '@umami/react-zen';
import { Column, Icon, Row, Text, Tooltip, TooltipTrigger } from '@umami/react-zen';
import classNames from 'classnames';
import { useMemo, useState } from 'react';
import { firstBy } from 'thenby';
@@ -232,9 +232,7 @@ export function Journey({ websiteId, steps, startStep, endStep, view }: JourneyP
<div>{formatLongNumber(nodeCount)}</div>
) : (
<TooltipTrigger delay={0}>
<Focusable>
<div>{formatLongNumber(nodeCount)}</div>
</Focusable>
<div tabIndex={0}>{formatLongNumber(nodeCount)}</div>
<Tooltip placement="top" sideOffset={20} showArrow>
<Text transform="lowercase" color="red">
{`${dropped}% ${t(labels.dropoff)}`}
@@ -1,4 +1,4 @@
import { Column, Focusable, Row, Text, Tooltip, TooltipTrigger } from '@umami/react-zen';
import { Column, Row, Text, Tooltip, TooltipTrigger } from '@umami/react-zen';
import { IconLabel } from '@/components/common/IconLabel';
import Link from '@/components/common/Link';
import { useMessages, useNavigation, useWebsiteNavItems } from '@/components/hooks';
@@ -22,16 +22,15 @@ export function WebsiteNav({
<Link href={renderUrl('/websites', false)} role="button" onClick={onItemClick}>
{isCollapsed ? (
<TooltipTrigger delay={0}>
<Focusable>
<Row
alignItems="center"
hover={{ backgroundColor: 'surface-sunken' }}
borderRadius
minHeight="40px"
>
<IconLabel icon={<ArrowLeft />} label="" padding />
</Row>
</Focusable>
<Row
tabIndex={0}
alignItems="center"
hover={{ backgroundColor: 'surface-sunken' }}
borderRadius
minHeight="40px"
>
<IconLabel icon={<ArrowLeft />} label="" padding />
</Row>
<Tooltip placement="right">{t(labels.back)}</Tooltip>
</TooltipTrigger>
) : (
@@ -58,22 +57,21 @@ export function WebsiteNav({
<Link key={id} href={path} role="button" onClick={onItemClick}>
{isCollapsed ? (
<TooltipTrigger delay={0}>
<Focusable>
<Row
alignItems="center"
hover={{ backgroundColor: 'surface-sunken' }}
backgroundColor={isSelected ? 'surface-sunken' : undefined}
borderRadius
minHeight="40px"
>
<IconLabel
icon={icon}
label=""
weight={isSelected ? 'bold' : undefined}
padding
/>
</Row>
</Focusable>
<Row
tabIndex={0}
alignItems="center"
hover={{ backgroundColor: 'surface-sunken' }}
backgroundColor={isSelected ? 'surface-sunken' : undefined}
borderRadius
minHeight="40px"
>
<IconLabel
icon={icon}
label=""
weight={isSelected ? 'bold' : undefined}
padding
/>
</Row>
<Tooltip placement="right">{label}</Tooltip>
</TooltipTrigger>
) : (
+11 -17
View File
@@ -1,7 +1,6 @@
import {
Button,
Column,
Focusable,
Icon,
Row,
Text,
@@ -179,22 +178,17 @@ export function ShareNav({
<Link key={id} href={path} role="button" onClick={onItemClick}>
{collapsed ? (
<TooltipTrigger delay={0}>
<Focusable>
<Row
alignItems="center"
justifyContent="center"
hover={{ backgroundColor: 'surface-sunken' }}
backgroundColor={isSelected ? 'surface-sunken' : undefined}
borderRadius
minHeight="40px"
>
<IconLabel
icon={icon}
label=""
weight={isSelected ? 'bold' : undefined}
/>
</Row>
</Focusable>
<Row
tabIndex={0}
alignItems="center"
justifyContent="center"
hover={{ backgroundColor: 'surface-sunken' }}
backgroundColor={isSelected ? 'surface-sunken' : undefined}
borderRadius
minHeight="40px"
>
<IconLabel icon={icon} label="" weight={isSelected ? 'bold' : undefined} />
</Row>
<Tooltip placement="right">{label}</Tooltip>
</TooltipTrigger>
) : (
-138
View File
@@ -1,138 +0,0 @@
import {
Column,
Icon,
Menu,
MenuItem,
MenuSection,
MenuSeparator,
MenuTrigger,
Popover,
Pressable,
Row,
SubmenuTrigger,
Text,
} from '@umami/react-zen';
import { ArrowRight } from 'lucide-react';
import type { Key } from 'react';
import { IconLabel } from '@/components/common/IconLabel';
import { useLoginQuery, useMessages, useMobile, useNavigation } from '@/components/hooks';
import { ChevronRight, User, Users } from '@/components/icons';
import { Switch } from '@/components/svg';
import { LAST_TEAM_CONFIG } from '@/lib/constants';
import { removeItem } from '@/lib/storage';
export interface TeamsButtonProps {
showText?: boolean;
onAction?: (id: any) => void;
}
export function NavButton({ showText = true }: TeamsButtonProps) {
const { user } = useLoginQuery();
const { t, labels } = useMessages();
const { teamId, router } = useNavigation();
const { isMobile } = useMobile();
const team = user?.teams?.find(({ id }) => id === teamId);
const selectedKeys = new Set([teamId || 'user']);
const label = teamId ? team?.name : user.username;
const cloudMode = !!process.env.cloudMode;
const getUrl = (url: string) => {
return cloudMode ? `${process.env.cloudUrl}${url}` : url;
};
const handleAction = async (key: Key) => {
if (key === 'user') {
removeItem(LAST_TEAM_CONFIG);
if (cloudMode) {
window.location.href = '/';
} else {
router.push('/');
}
}
};
const handleNavigate = (url: string) => {
if (cloudMode) {
window.location.href = url;
} else {
router.push(url);
}
};
return (
<MenuTrigger>
<Pressable>
<Row
alignItems="center"
justifyContent="space-between"
flexGrow={1}
padding
border
borderRadius
shadow="sm"
minHeight="40px"
role="button"
style={{ cursor: 'pointer', textWrap: 'nowrap', overflow: 'hidden', outline: 'none' }}
>
<Row alignItems="center" position="relative" gap maxHeight="40px">
<Icon>{teamId ? <Users /> : <User />}</Icon>
{showText && <Text>{label}</Text>}
</Row>
{showText && (
<Icon rotate={90} size="sm">
<ChevronRight />
</Icon>
)}
</Row>
</Pressable>
<Popover side="bottom" align="start">
<Column minWidth="300px">
<Menu>
<SubmenuTrigger>
<MenuItem id="teams" showChecked={false} showSubMenuIcon>
<IconLabel icon={<Switch />} label={t(labels.switchAccount)} />
</MenuItem>
<Popover side={isMobile ? 'bottom' : 'right'} align="start">
<Column minWidth="300px">
<Menu selectionMode="single" selectedKeys={selectedKeys}>
<MenuSection title={t(labels.myAccount)}>
<MenuItem id="user" onAction={handleAction}>
<IconLabel icon={<User />} label={user.username} />
</MenuItem>
</MenuSection>
<MenuSeparator />
<MenuSection title={t(labels.teams)}>
{user?.teams?.map(({ id, name }) => (
<MenuItem
key={id}
id={id}
onAction={() => handleNavigate(getUrl(`/teams/${id}`))}
>
<IconLabel icon={<Users />}>
<Text wrap="nowrap">{name}</Text>
</IconLabel>
</MenuItem>
))}
{user?.teams?.length === 0 && (
<MenuItem id="manage-teams">
<a href="/settings/teams" style={{ width: '100%' }}>
<Row alignItems="center" justifyContent="space-between" gap>
<Text align="center">Manage teams</Text>
<Icon>
<ArrowRight />
</Icon>
</Row>
</a>
</MenuItem>
)}
</MenuSection>
</Menu>
</Column>
</Popover>
</SubmenuTrigger>
</Menu>
</Column>
</Popover>
</MenuTrigger>
);
}
+16 -17
View File
@@ -1,4 +1,4 @@
import { Focusable, Grid, Row, Text, Tooltip, TooltipTrigger } from '@umami/react-zen';
import { Grid, Row, Text, Tooltip, TooltipTrigger } from '@umami/react-zen';
import { addHours, format, startOfDay } from 'date-fns';
import { Fragment } from 'react';
import { LoadingPanel } from '@/components/common/LoadingPanel';
@@ -77,26 +77,25 @@ export function WeeklyTraffic({ websiteId }: { websiteId: string }) {
{day?.map((count: number, j) => {
const pct = max ? count / max : 0;
const cell = (
<Focusable>
<Row
tabIndex={0}
alignItems="center"
justifyContent="center"
backgroundColor="surface-raised"
width="16px"
height="16px"
borderRadius="full"
style={{ margin: '0 auto' }}
role="button"
>
<Row
alignItems="center"
justifyContent="center"
backgroundColor="surface-raised"
backgroundColor="primary"
width="16px"
height="16px"
borderRadius="full"
style={{ margin: '0 auto' }}
role="button"
>
<Row
backgroundColor="primary"
width="16px"
height="16px"
borderRadius="full"
style={{ opacity: pct, transform: `scale(${pct})` }}
/>
</Row>
</Focusable>
style={{ opacity: pct, transform: `scale(${pct})` }}
/>
</Row>
);
if (count <= 0) {