mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
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>
|
||||
|
||||
Reference in new issue
Block a user