fix(docs): a11y projectconfigvariables (#50002)

## What kind of change does this PR introduce?

bug fix for accessibility, fixes
[docs-1280](https://linear.app/supabase/issue/DOCS-1280/projectconfigvariables-label-the-readonly-inputs-and-name)

## What is the current behavior?

the project url and api key fields in `ProjectConfigVariables` have no
associated label, so a screen reader announces an edit field with no
indication of which value it holds

## What is the new behavior?

- associates a `<label>` with each readonly input, so the fields
announce as "project url" and "publishable key"
- names each copy button after the value it copies
- drops `role="combobox"` from the trigger, keeping the `aria-haspopup`,
`aria-expanded` and `aria-controls` radix already supplies
- names the trigger from its content instead of `aria-label`, so it
announces the current selection
- names the shared `CommandInput` reset button and hides its icons

## test

- `pnpm dev:docs`
- `/docs/guides/getting-started/quickstarts/nextjs` (`url` +
`publishable`)
- `/docs/guides/auth/server-side/creating-a-client`, branch selector,
needs a branching-enabled project
- `/docs/guides/observability/log-drains`
- `api_settings` in any getting-started quickstart

## Additional context

reverses part of #49952 as that pr added `aria-label` to satisfy
`button-name`, but did replace the accessible name rather than adding to
it _ the sr-only prefix added here keeps the rule passing and announces
the selection

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Accessibility**
* Improved screen reader support for variable configuration controls,
including clearer labels and copy-status announcements.
* Enhanced combobox and search interactions with accessible labeling,
empty-result announcements, and clearer reset-button names.
* Decorative icons and visual-only messages are now hidden from
assistive technologies.
* **Tests**
* Added accessibility coverage for search input icons and the
clear-search control.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Anthony Lio authored and GitHub committed 2026-09-10 13:22:00 +03:00
1 parent 1966209483
commit 5d78b1da1a
3 files changed
+36 -15

No files matched your search

@@ -83,10 +83,8 @@ export function ComboBox<Opt extends ComboBoxOption>({
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
disabled={disabled}
aria-expanded={open}
aria-label={`Select your ${name}`}
className={cn(
'overflow-hidden',
'h-auto min-h-10',
@@ -96,17 +94,18 @@ export function ComboBox<Opt extends ComboBoxOption>({
className
)}
>
<span className="sr-only">{name}: </span>
{selectedDisplayName ??
(isLoading && options.length > 0
? 'Loading...'
: options.length === 0
? `No ${name} found`
: `Select a ${name}...`)}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" aria-hidden />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" side="bottom" align="start">
<Command shouldFilter={useCommandSearch}>
<Command shouldFilter={useCommandSearch} label={`Search ${name}`}>
<CommandInput
placeholder={`Search ${name}...`}
className="border-none ring-0"
@@ -115,7 +114,10 @@ export function ComboBox<Opt extends ComboBoxOption>({
onValueChange={setSearch}
handleReset={() => setSearch('')}
/>
<CommandList>
<span className="sr-only" role="status">
{!isLoading && search.length > 0 && options.length === 0 ? `No ${name} found` : ''}
</span>
<CommandList label={`${name} options`}>
<CommandGroup>
{isLoading ? (
<div className="px-2 py-1 flex flex-col gap-2">
@@ -125,7 +127,7 @@ export function ComboBox<Opt extends ComboBoxOption>({
) : (
<>
{search.length > 0 && options.length === 0 && (
<p className="text-xs text-center text-foreground-lighter py-3">
<p className="text-xs text-center text-foreground-lighter py-3" aria-hidden>
No {name}s found based on your search
</p>
)}
@@ -146,6 +148,7 @@ export function ComboBox<Opt extends ComboBoxOption>({
'mr-2 h-4 w-4',
selectedOption === option.value ? 'opacity-100' : 'opacity-0'
)}
aria-hidden
/>
{option.displayName}
</CommandItem>
@@ -34,7 +34,7 @@ import { useOnLogout } from '~/lib/userAuth'
import { LOCAL_STORAGE_KEYS, useIsLoggedIn, useIsUserLoading } from 'common'
import { Check, Copy } from 'lucide-react'
import Link from 'next/link'
import { useEffect, useMemo, useState } from 'react'
import { useEffect, useId, useMemo, useState } from 'react'
import CopyToClipboard from 'react-copy-to-clipboard'
import { withErrorBoundary } from 'react-error-boundary'
import { Button_Shadcn_ as Button, cn, Input } from 'ui'
@@ -303,7 +303,15 @@ function BranchSelector() {
) : null
}
function VariableView({ variable, className }: { variable: Variable; className?: string }) {
function VariableView({
variable,
inputId,
className,
}: {
variable: Variable
inputId: string
className?: string
}) {
const isUserLoading = useIsUserLoading()
const isLoggedIn = useIsLoggedIn()
@@ -397,6 +405,7 @@ function VariableView({ variable, className }: { variable: Variable; className?:
<div className={cn('flex items-center gap-2', className)}>
<Input
readOnly
id={inputId}
type="text"
className="font-mono"
value={
@@ -422,11 +431,14 @@ function VariableView({ variable, className }: { variable: Variable; className?:
properties: { variable },
})
}}
aria-label="Copy"
aria-label={`Copy ${prettyFormatVariable[variable]}`}
>
{copied ? <Check size="18" /> : <Copy size="18" />}
{copied ? <Check size="18" aria-hidden /> : <Copy size="18" aria-hidden />}
</Button>
</CopyToClipboard>
<span className="sr-only" role="status">
{copied ? `${prettyFormatVariable[variable]} copied` : ''}
</span>
</div>
{stateSummary === 'loggedIn.selectedProject.dataError' && (
<p className="text-foreground-muted text-sm mt-2 mb-0 ml-1">
@@ -472,16 +484,21 @@ function ProjectConfigVariablesInternal({ variable }: { variable: Variable }) {
const { clear: clearSharedStoreData } = useSnapshot(projectsStore)
useOnLogout(clearSharedStoreData)
const inputId = useId()
return (
<div className="max-w-[min(100%, 500px)] my-6">
<span className={cn('block mt-0 mb-1 font-heading font-semibold', 'text-foreground')}>
<label
htmlFor={inputId}
className={cn('block mt-0 mb-1 font-heading font-semibold', 'text-foreground')}
>
{prettyFormatVariable[variable]}
</span>
</label>
<div className="flex flex-wrap gap-x-6">
<OrgProjectSelector />
<BranchSelector />
</div>
<VariableView variable={variable} className="mt-1" />
<VariableView variable={variable} inputId={inputId} className="mt-1" />
<LoginHint variable={variable} />
</div>
)
@@ -58,7 +58,7 @@ const CommandInput = React.forwardRef<
ref
) => (
<div className={cn('flex items-center border-b px-4', wrapperClassName)} cmdk-input-wrapper="">
{showSearchIcon && <Search className="h-4 w-4 shrink-0 opacity-50" />}
{showSearchIcon && <Search className="h-4 w-4 shrink-0 opacity-50" aria-hidden />}
<CommandPrimitive.Input
ref={ref}
className={cn(
@@ -73,12 +73,13 @@ const CommandInput = React.forwardRef<
tabIndex={props.disabled || !props.value?.length ? -1 : 0}
disabled={props.disabled || !props.value?.length}
onClick={handleReset}
aria-label="Clear search"
className={cn(
'text-foreground-lighter hover:text-foreground-light hover:cursor-pointer transition-all opacity-0 duration-100',
!!props.value?.length && 'opacity-100'
)}
>
<RemoveIcon size={14} />
<RemoveIcon size={14} aria-hidden />
</button>
)}
</div>