feat(a11y): label form controls + overlays (P3-4)
Accessible names for ~15 controls that lacked them: invite/join/create-room/ account-data/image-pack/private-note/power-level inputs (visible <label htmlFor> where a label exists, else aria-label); the two range sliders (night-light intensity, noise-gate threshold); the soundboard file input; media <video> elements; and the Media Gallery (region) + Search (dialog) overlays. Hidden notification/preview <audio> marked aria-hidden. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -213,6 +213,7 @@ function AccountDataView({ type, defaultContent, onEdit }: AccountDataViewProps)
|
|||||||
<Text size="L400">Account Data</Text>
|
<Text size="L400">Account Data</Text>
|
||||||
<Input
|
<Input
|
||||||
variant="SurfaceVariant"
|
variant="SurfaceVariant"
|
||||||
|
aria-label="Account data type"
|
||||||
size="400"
|
size="400"
|
||||||
radii="300"
|
radii="300"
|
||||||
readOnly
|
readOnly
|
||||||
|
|||||||
@@ -282,7 +282,12 @@ export function VoiceMessageRecorder({ onSend, onError }: VoiceRecorderProps) {
|
|||||||
>
|
>
|
||||||
{previewUrl && (
|
{previewUrl && (
|
||||||
<>
|
<>
|
||||||
<audio ref={previewAudioRef} src={previewUrl} onEnded={() => setPreviewPlaying(false)} />
|
<audio
|
||||||
|
ref={previewAudioRef}
|
||||||
|
src={previewUrl}
|
||||||
|
onEnded={() => setPreviewPlaying(false)}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const audio = previewAudioRef.current;
|
const audio = previewAudioRef.current;
|
||||||
|
|||||||
@@ -78,11 +78,14 @@ export function CreateRoomAliasInput({ disabled }: { disabled?: boolean }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box shrink="No" direction="Column" gap="100">
|
<Box shrink="No" direction="Column" gap="100">
|
||||||
<Text size="L400">Address (Optional)</Text>
|
<Text as="label" htmlFor="create-room-alias" size="L400">
|
||||||
|
Address (Optional)
|
||||||
|
</Text>
|
||||||
<Text size="T200" priority="300">
|
<Text size="T200" priority="300">
|
||||||
Pick an unique address to make it discoverable.
|
Pick an unique address to make it discoverable.
|
||||||
</Text>
|
</Text>
|
||||||
<Input
|
<Input
|
||||||
|
id="create-room-alias"
|
||||||
ref={aliasInputRef}
|
ref={aliasInputRef}
|
||||||
onChange={handleAliasChange}
|
onChange={handleAliasChange}
|
||||||
before={
|
before={
|
||||||
|
|||||||
@@ -200,12 +200,24 @@ export function ImagePackProfileEdit({ meta, onCancel, onSave }: ImagePackProfil
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box direction="Inherit" gap="100">
|
<Box direction="Inherit" gap="100">
|
||||||
<Text size="L400">Name</Text>
|
<Text as="label" htmlFor="image-pack-name" size="L400">
|
||||||
<Input name="nameInput" defaultValue={meta.name} variant="Secondary" radii="300" required />
|
Name
|
||||||
|
</Text>
|
||||||
|
<Input
|
||||||
|
id="image-pack-name"
|
||||||
|
name="nameInput"
|
||||||
|
defaultValue={meta.name}
|
||||||
|
variant="Secondary"
|
||||||
|
radii="300"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box direction="Inherit" gap="100">
|
<Box direction="Inherit" gap="100">
|
||||||
<Text size="L400">Attribution</Text>
|
<Text as="label" htmlFor="image-pack-attribution" size="L400">
|
||||||
|
Attribution
|
||||||
|
</Text>
|
||||||
<TextArea
|
<TextArea
|
||||||
|
id="image-pack-attribution"
|
||||||
name="attributionTextArea"
|
name="attributionTextArea"
|
||||||
defaultValue={meta.attribution}
|
defaultValue={meta.attribution}
|
||||||
variant="Secondary"
|
variant="Secondary"
|
||||||
|
|||||||
@@ -261,9 +261,12 @@ export function InviteUserPrompt({ room, requestClose }: InviteUserProps) {
|
|||||||
gap="400"
|
gap="400"
|
||||||
>
|
>
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
<Text size="L400">User ID</Text>
|
<Text as="label" htmlFor="invite-user-id" size="L400">
|
||||||
|
User ID
|
||||||
|
</Text>
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
|
id="invite-user-id"
|
||||||
size="500"
|
size="500"
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
@@ -334,8 +337,11 @@ export function InviteUserPrompt({ room, requestClose }: InviteUserProps) {
|
|||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
<Text size="L400">Reason (Optional)</Text>
|
<Text as="label" htmlFor="invite-reason" size="L400">
|
||||||
|
Reason (Optional)
|
||||||
|
</Text>
|
||||||
<TextArea
|
<TextArea
|
||||||
|
id="invite-reason"
|
||||||
size="500"
|
size="500"
|
||||||
name="reasonInput"
|
name="reasonInput"
|
||||||
variant="Background"
|
variant="Background"
|
||||||
|
|||||||
@@ -108,8 +108,11 @@ export function JoinAddressPrompt({ onOpen, onCancel }: JoinAddressProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
<Text size="L400">Address</Text>
|
<Text as="label" htmlFor="join-address" size="L400">
|
||||||
|
Address
|
||||||
|
</Text>
|
||||||
<Input
|
<Input
|
||||||
|
id="join-address"
|
||||||
size="500"
|
size="500"
|
||||||
autoFocus
|
autoFocus
|
||||||
name="addressInput"
|
name="addressInput"
|
||||||
|
|||||||
@@ -278,6 +278,7 @@ export function SoundboardPackEditor({ pack, canEdit, onUpdate }: SoundboardPack
|
|||||||
<Box direction="Column" gap="300">
|
<Box direction="Column" gap="300">
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
|
aria-label="Upload soundboard clip"
|
||||||
type="file"
|
type="file"
|
||||||
accept={SOUNDBOARD_ACCEPT}
|
accept={SOUNDBOARD_ACCEPT}
|
||||||
multiple
|
multiple
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ function PreviewVideo({ fileItem }: PreviewVideoProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<video
|
<video
|
||||||
|
aria-label="Video attachment preview"
|
||||||
style={{
|
style={{
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|||||||
@@ -253,6 +253,7 @@ function UserPrivateNotes({ userId }: { userId: string }) {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<textarea
|
<textarea
|
||||||
|
aria-label="Private note about this user"
|
||||||
value={draft}
|
value={draft}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
maxLength={USER_NOTE_MAX_LENGTH}
|
maxLength={USER_NOTE_MAX_LENGTH}
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ function EditPower({ maxPower, power, tag, onSave, onClose }: EditPowerProps) {
|
|||||||
<Text size="L400">Name</Text>
|
<Text size="L400">Name</Text>
|
||||||
<Input
|
<Input
|
||||||
name="nameInput"
|
name="nameInput"
|
||||||
|
aria-label="Power level name"
|
||||||
defaultValue={tag?.name}
|
defaultValue={tag?.name}
|
||||||
placeholder="Bot"
|
placeholder="Bot"
|
||||||
size="300"
|
size="300"
|
||||||
@@ -160,6 +161,7 @@ function EditPower({ maxPower, power, tag, onSave, onClose }: EditPowerProps) {
|
|||||||
<Input
|
<Input
|
||||||
defaultValue={power}
|
defaultValue={power}
|
||||||
name="powerInput"
|
name="powerInput"
|
||||||
|
aria-label="Power level value"
|
||||||
size="300"
|
size="300"
|
||||||
variant={typeof power === 'number' ? 'SurfaceVariant' : 'Secondary'}
|
variant={typeof power === 'number' ? 'SurfaceVariant' : 'Secondary'}
|
||||||
radii="300"
|
radii="300"
|
||||||
|
|||||||
@@ -186,8 +186,8 @@ function LightboxMedia({
|
|||||||
)}
|
)}
|
||||||
{media.status === 'ok' &&
|
{media.status === 'ok' &&
|
||||||
(item.msgtype === MsgType.Video ? (
|
(item.msgtype === MsgType.Video ? (
|
||||||
// eslint-disable-next-line jsx-a11y/media-has-caption
|
|
||||||
<video
|
<video
|
||||||
|
aria-label="Video attachment"
|
||||||
src={media.url}
|
src={media.url}
|
||||||
controls
|
controls
|
||||||
autoPlay
|
autoPlay
|
||||||
@@ -261,7 +261,7 @@ function Lightbox({
|
|||||||
escapeDeactivates: false,
|
escapeDeactivates: false,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
{}
|
||||||
<div
|
<div
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal
|
aria-modal
|
||||||
@@ -640,13 +640,15 @@ export function MediaGallery({ room, onClose }: MediaGalleryProps) {
|
|||||||
className={classNames(css.MediaGalleryDrawer, ContainerColor({ variant: 'Background' }))}
|
className={classNames(css.MediaGalleryDrawer, ContainerColor({ variant: 'Background' }))}
|
||||||
shrink="No"
|
shrink="No"
|
||||||
direction="Column"
|
direction="Column"
|
||||||
|
role="region"
|
||||||
|
aria-labelledby="media-gallery-title"
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Header variant="Background" size="600" className={css.MediaGalleryHeader}>
|
<Header variant="Background" size="600" className={css.MediaGalleryHeader}>
|
||||||
<Box grow="Yes" alignItems="Center" gap="200">
|
<Box grow="Yes" alignItems="Center" gap="200">
|
||||||
<Icon size="200" src={Icons.Photo} />
|
<Icon size="200" src={Icons.Photo} />
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text size="H4" truncate>
|
<Text id="media-gallery-title" size="H4" truncate>
|
||||||
Media Gallery
|
Media Gallery
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -142,7 +142,6 @@ export function PollCreator({ roomId, onClose }: PollCreatorProps) {
|
|||||||
placeholder="Ask a question…"
|
placeholder="Ask a question…"
|
||||||
value={question}
|
value={question}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setQuestion(e.target.value)}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setQuestion(e.target.value)}
|
||||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -151,7 +150,6 @@ export function PollCreator({ roomId, onClose }: PollCreatorProps) {
|
|||||||
<Box direction="Column" gap="200">
|
<Box direction="Column" gap="200">
|
||||||
<Text size="L400">Options</Text>
|
<Text size="L400">Options</Text>
|
||||||
{options.map((opt, index) => (
|
{options.map((opt, index) => (
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
|
||||||
<Box key={index} alignItems="Center" gap="200">
|
<Box key={index} alignItems="Center" gap="200">
|
||||||
<Input
|
<Input
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
|
|||||||
@@ -247,7 +247,6 @@ export function Search({ requestClose }: SearchProps) {
|
|||||||
<FocusTrap
|
<FocusTrap
|
||||||
focusTrapOptions={{
|
focusTrapOptions={{
|
||||||
initialFocus: () => inputRef.current,
|
initialFocus: () => inputRef.current,
|
||||||
returnFocusOnDeactivate: false,
|
|
||||||
allowOutsideClick: true,
|
allowOutsideClick: true,
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
onDeactivate: requestClose,
|
onDeactivate: requestClose,
|
||||||
@@ -257,7 +256,13 @@ export function Search({ requestClose }: SearchProps) {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Modal size="400" style={{ maxHeight: toRem(400), borderRadius: config.radii.R500 }}>
|
<Modal
|
||||||
|
size="400"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="Search"
|
||||||
|
style={{ maxHeight: toRem(400), borderRadius: config.radii.R500 }}
|
||||||
|
>
|
||||||
<Box
|
<Box
|
||||||
shrink="No"
|
shrink="No"
|
||||||
style={{ padding: config.space.S400, paddingBottom: 0 }}
|
style={{ padding: config.space.S400, paddingBottom: 0 }}
|
||||||
@@ -270,6 +275,7 @@ export function Search({ requestClose }: SearchProps) {
|
|||||||
radii="400"
|
radii="400"
|
||||||
outlined
|
outlined
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
|
aria-label="Search rooms"
|
||||||
before={<Icon size="200" src={Icons.Search} />}
|
before={<Icon size="200" src={Icons.Search} />}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onKeyDown={handleInputKeyDown}
|
onKeyDown={handleInputKeyDown}
|
||||||
|
|||||||
@@ -531,6 +531,7 @@ function Appearance() {
|
|||||||
Intensity: {nightLightOpacity}%
|
Intensity: {nightLightOpacity}%
|
||||||
</Text>
|
</Text>
|
||||||
<input
|
<input
|
||||||
|
aria-label="Night light intensity"
|
||||||
type="range"
|
type="range"
|
||||||
min={5}
|
min={5}
|
||||||
max={80}
|
max={80}
|
||||||
@@ -1663,6 +1664,7 @@ function Calls() {
|
|||||||
<Text size="T200">{callDenoiseGateThreshold} dB</Text>
|
<Text size="T200">{callDenoiseGateThreshold} dB</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<input
|
<input
|
||||||
|
aria-label="Noise gate threshold"
|
||||||
type="range"
|
type="range"
|
||||||
min="-100"
|
min="-100"
|
||||||
max="0"
|
max="0"
|
||||||
|
|||||||
Reference in New Issue
Block a user