Files
supabase/apps/www/components/PrevNextFeatureNav.tsx
Miranda LimonczenkoandClaude Opus 5 21fccb0ecd fix(www): name the /features page button controls (#49343)
Closes FE-4097


https://github.com/user-attachments/assets/bc7cad1a-763e-469f-8a3b-e4d23bed94d9

_See bottom left of screen for screen reader captions._

## Problem

Two controls in the shared `/features/[slug]` template have no
accessible name. Both live in the template, so both fire on all 79
feature pages.

* The feature list dropdown trigger contains only a `List` icon.
`button-name`, critical.
* The breadcrumb back chevron wraps only a `ChevronLeft`. `link-name`,
serious.

## Solution

* Name both with `sr-only` text, matching the sibling prev and next
controls in the same component and the theme switcher in the site
header.
* Label the product pill with its destination. It announced only
"vector", with no indication it filters the catalog. Not an axe finding,
since the product name already supplies a name. The label keeps the
visible word so it satisfies WCAG 2.5.3 Label in Name.
* Fix a stray `className="` inside the `iconClassName` string literal,
which dropped the icons' width class.
* Add `cursor-pointer` to `buttonClassName`. Tailwind 4 no longer sets a
pointer cursor on buttons, so the middle control behaved differently
from its two anchor siblings. This line belongs to FE-4227 and sits here
only to keep two open PRs off adjacent lines of the same file.

## Manual testing

1. Open
[/features/ai-integrations](https://zone-www-dot-com-git-www-features-chrome-access-1aef01-supabase.vercel.app/features/ai-integrations)
using a Screenreader.
2. Tab through the three round controls at top right. They announce
"Previous feature", "Browse all features", "Next feature". **Note:** The
order of the elements is strange; captured in a separate ticket.
3. Tab to the round back control at top left. It announces "Back to all
features".
4. Tab to the product pill beside it. It announces "All vector
features", and the visible word "vector" is unchanged.
5. Hover each of the three round controls. All show a pointer cursor.
6. Run axe on the page. `button-name` and `link-name` report zero
elements.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 08:48:04 -07:00

133 lines
4.3 KiB
TypeScript

import { features, type FeatureType } from '~/data/features'
import { ArrowLeft, ArrowRight, List } from 'lucide-react'
import Link, { LinkProps } from 'next/link'
import { useRouter } from 'next/router'
import { PropsWithChildren, useEffect, useState } from 'react'
import {
cn,
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from 'ui'
interface Props {
className?: string
wrapperClassName?: string
currentFeature: FeatureType
prevLink: string
nextLink: string
}
const buttonClassName =
'relative z-10 flex items-center gap-1 px-2 cursor-pointer pointer-events-auto overflow-hidden h-[30px]! min-w-[30px]! max-w-[30px]! py-1 justify-center rounded-full border bg-default hover:bg-surface-100 hover:text-foreground hover:border-foreground-lighter transition-all'
const iconClassName = 'w-4 h-4 shrink-0'
const PrevNextFeatureNav: React.FC<Props> = ({
className,
wrapperClassName,
currentFeature,
prevLink,
nextLink,
...props
}) => {
const [open, setOpen] = useState(false)
const router = useRouter()
useEffect(() => {
function handleRouteChange() {
setOpen(false)
}
router.events.on('routeChangeComplete', handleRouteChange)
return () => {
router.events.off('routeChangeComplete', handleRouteChange)
}
}, [router.events])
return (
<div
className={cn(
'h-full w-full max-w-2xl absolute mx-auto inset-0 pointer-events-none',
wrapperClassName
)}
{...props}
>
<div
className={cn(
'absolute top-9 w-fit pointer-events-auto flex items-center text-sm gap-1 text-foreground-light right-8 md:right-0',
className
)}
>
<ButtonLink href={prevLink} className="text-right pl-2">
<ArrowLeft className={iconClassName} />
<span className="sr-only">Previous feature</span>
</ButtonLink>
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger className={cn(buttonClassName, 'p-0')}>
<List className={iconClassName} />
<span className="sr-only">Browse all features</span>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" alignOffset={-38} className="pb-0">
<DropdownMenuItem asChild className="text-foreground-lighter p-0">
<Link
href="/features"
as="/features"
className="group/link flex items-center gap-2 px-2 py-1.5 w-full hover:text-foreground"
>
<span className="truncate grow">All Features</span>
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator className="mb-0" />
<DropdownMenuGroup className="max-h-[400px] overflow-y-scroll py-1">
{features
.sort((a, b) => {
if (a['title'] < b['title']) return -1
if (a['title'] > b['title']) return 1
return 0
})
.map((feature) => (
<DropdownMenuItem asChild key={feature.slug} className="p-0">
<Link
href={`/features/${feature.slug}`}
as={`/features/${feature.slug}`}
className="group/link flex items-center gap-2 px-2 py-1.5 w-full hover:text-foreground"
>
<feature.icon className="w-3 h-3 text-foreground-lighter group-hover:text-foreground transition-colors" />
<span className="line-clamp-1 grow">{feature.title}</span>
</Link>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<ButtonLink href={nextLink}>
<span className="sr-only">Next feature</span>
<ArrowRight className={iconClassName} />
</ButtonLink>
</div>
</div>
)
}
interface ButtonLinkProps extends LinkProps {
className?: string
}
const ButtonLink: React.FC<PropsWithChildren<ButtonLinkProps>> = ({
href,
className,
children,
}) => {
return (
<Link href={href} className={cn(buttonClassName, className)}>
{children}
</Link>
)
}
export default PrevNextFeatureNav