pl-fe: seemingly accessibility improvements

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2026-01-06 04:45:27 +01:00
parent 4e91d696c7
commit f84cfd3c5f
19 changed files with 38 additions and 37 deletions

View File

@ -247,8 +247,6 @@ const AutosuggestInput: React.FC<IAutosuggestInput> = ({
return [
<div key='input' className='relative w-full'>
<label className='sr-only'>{props.placeholder}</label>
<Input
type='text'
className={props.className}
@ -269,6 +267,7 @@ const AutosuggestInput: React.FC<IAutosuggestInput> = ({
data-testid='autosuggest-input'
theme={props.theme}
lang={props.lang}
aria-label={props.placeholder}
/>
</div>,
<Portal key='portal'>

View File

@ -1,7 +1,7 @@
import { decode } from 'blurhash';
import React, { useRef, useEffect } from 'react';
interface IBlurhash {
interface IBlurhash extends Pick<React.CanvasHTMLAttributes<HTMLCanvasElement>, 'className' | 'aria-label' | 'aria-hidden'> {
/** Hash to render */
hash: string | null | undefined;
/** Width of the blurred region in pixels. Defaults to 32. */
@ -13,8 +13,6 @@ interface IBlurhash {
* and canvas left untouched.
*/
dummy?: boolean;
/** className of the canvas element. */
className?: string;
}
/**

View File

@ -283,6 +283,8 @@ const Item: React.FC<IItem> = ({
<Blurhash
hash={attachment.blurhash}
className='⁂-media-gallery__preview'
aria-label={!visible ? attachment.description : undefined}
aria-hidden={visible}
/>
{(visible || !attachment.blurhash) && thumbnail}
</div>

View File

@ -1,7 +1,7 @@
import { useNavigate } from '@tanstack/react-router';
import clsx from 'clsx';
import React, { useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { defineMessages, useIntl } from 'react-intl';
import AutosuggestAccountInput from 'pl-fe/components/autosuggest-account-input';
import SvgIcon from 'pl-fe/components/ui/svg-icon';
@ -63,12 +63,11 @@ const SearchInput = React.memo(() => {
return (
<div className='w-full'>
<label htmlFor='search' className='sr-only'><FormattedMessage id='search.placeholder' defaultMessage='Search' /></label>
<div className='relative'>
<AutosuggestAccountInput
id='search'
placeholder={intl.formatMessage(messages.placeholder)}
aria-label={intl.formatMessage(messages.placeholder)}
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
@ -88,11 +87,13 @@ const SearchInput = React.memo(() => {
<SvgIcon
src={require('@phosphor-icons/core/regular/magnifying-glass.svg')}
className={clsx('size-4 text-gray-600', { hidden: hasValue })}
aria-hidden
/>
<SvgIcon
src={require('@phosphor-icons/core/regular/x.svg')}
className={clsx('size-4 text-gray-600', { hidden: !hasValue })}
aria-hidden
/>
</button>
</div>

View File

@ -27,7 +27,7 @@ const SidebarNavigationLink = React.memo(React.forwardRef((props: ISidebarNaviga
const matchRoute = useMatchRoute();
const { demetricator } = useSettings();
const LinkComponent = (to === undefined ? 'div' : Link) as typeof Link;
const LinkComponent = (to === undefined ? 'button' : Link) as typeof Link;
const isActive = matchRoute({ to }) !== false;

View File

@ -50,6 +50,7 @@ const StatusActionButton = React.forwardRef<HTMLButtonElement, IStatusActionButt
<Icon
src={active && filledIcon || icon}
className={iconClassName}
aria-hidden
/>
);
};

View File

@ -66,12 +66,13 @@ const Accordion: React.FC<IAccordion> = ({ headline, children, menu, expanded =
)}
{action && actionIcon && (
<button className='⁂-accordion__header__action' onClick={handleAction} title={actionLabel}>
<Icon src={actionIcon} />
<Icon src={actionIcon} aria-hidden />
</button>
)}
<Icon
src={require('@phosphor-icons/core/regular/caret-down.svg')}
className='⁂-accordion__header__chevron'
aria-hidden
/>
</div>
</button>

View File

@ -68,9 +68,8 @@ const CardHeader: React.FC<ICardHeader> = ({ className, children, backHref, onBa
const backAttributes = backHref ? { to: backHref } : { onClick: onBackClick };
return (
<Comp {...backAttributes} className='⁂-card-header__button' aria-label={intl.formatMessage(messages.back)}>
<SvgIcon src={require('@phosphor-icons/core/regular/arrow-left.svg')} />
<span className='⁂-card-header__button__label' data-testid='back-button'>{intl.formatMessage(messages.back)}</span>
<Comp {...backAttributes} className='⁂-card-header__button' aria-label={intl.formatMessage(messages.back)} title={intl.formatMessage(messages.back)}>
<SvgIcon src={require('@phosphor-icons/core/regular/arrow-left.svg')} aria-hidden />
</Comp>
);
};

View File

@ -2,7 +2,7 @@ import { Link, type LinkOptions } from '@tanstack/react-router';
import clsx from 'clsx';
import React from 'react';
import toast, { Toast as RHToast } from 'react-hot-toast';
import { FormattedMessage } from 'react-intl';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { ToastText, ToastType } from 'pl-fe/toast';
@ -11,6 +11,10 @@ import Icon from './icon';
import Stack from './stack';
import Text from './text';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
});
const renderText = (text: ToastText) => {
if (typeof text === 'string') {
return text;
@ -35,6 +39,8 @@ interface IToast {
const Toast = (props: IToast) => {
const { t, message, type, action, actionLinkOptions, actionLabel, summary } = props;
const intl = useIntl();
const dismissToast = () => toast.dismiss(t.id);
const renderIcon = () => {
@ -141,8 +147,8 @@ const Toast = (props: IToast) => {
className='inline-flex rounded-md text-gray-600 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:text-gray-600 dark:hover:text-gray-500'
onClick={dismissToast}
data-testid='toast-dismiss'
title={intl.formatMessage(messages.close)}
>
<span className='sr-only'><FormattedMessage id='lightbox.close' defaultMessage='Close' /></span>
<Icon src={require('@phosphor-icons/core/regular/x.svg')} className='size-5' />
</button>
</div>

View File

@ -87,9 +87,9 @@ const ContentTypeButton: React.FC<IContentTypeButton> = ({ composeId, compact })
}))}
>
<button className='⁂-content-type-button' title={compact ? option?.text : intl.formatMessage(messages.change_content_type)}>
{option?.icon && <Icon src={option.icon} />}
{option?.icon && <Icon src={option.icon} aria-hidden />}
{compact ? undefined : option?.text}
<Icon src={require('@phosphor-icons/core/regular/caret-down.svg')} />
<Icon src={require('@phosphor-icons/core/regular/caret-down.svg')} aria-hidden />
</button>
</DropdownMenu>
);

View File

@ -165,6 +165,7 @@ const getLanguageDropdown = (composeId: string): React.FC<ILanguageDropdown> =>
>
<Icon
src={isSearching ? require('@phosphor-icons/core/regular/backspace.svg') : require('@phosphor-icons/core/regular/magnifying-glass.svg')}
aria-hidden
/>
</button>
</label>
@ -249,9 +250,9 @@ const LanguageDropdownButton: React.FC<ILanguageDropdownButton> = ({ composeId,
className='⁂-language-dropdown'
>
<button title={intl.formatMessage(messages.languagePrompt)}>
<Icon src={require('@phosphor-icons/core/regular/translate.svg')} />
<Icon src={require('@phosphor-icons/core/regular/translate.svg')} aria-hidden />
{buttonLabel}
<Icon src={require('@phosphor-icons/core/regular/caret-down.svg')} />
<Icon src={require('@phosphor-icons/core/regular/caret-down.svg')} aria-hidden />
</button>
</DropdownMenu>
);

View File

@ -182,9 +182,9 @@ const PrivacyDropdown: React.FC<IPrivacyDropdown> = ({
return (
<DropdownMenu items={items} width='16rem'>
<button title={compact ? text : intl.formatMessage(messages.change_privacy)}>
{valueOption?.icon && <Icon src={valueOption.icon} />}
{valueOption?.icon && <Icon src={valueOption.icon} aria-hidden />}
{compact ? undefined : text}
<Icon src={require('@phosphor-icons/core/regular/caret-down.svg')} />
<Icon src={require('@phosphor-icons/core/regular/caret-down.svg')} aria-hidden />
</button>
</DropdownMenu>
);

View File

@ -36,9 +36,7 @@ const Search: React.FC<ISearch> = ({ value, onSubmit }) => {
return (
<Form onSubmit={handleSubmit}>
<HStack space={2}>
<label className='relative grow'>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.search)}</span>
<label className='relative grow' title={intl.formatMessage(messages.search)}>
<Input
type='text'
value={searchValue}
@ -54,7 +52,7 @@ const Search: React.FC<ISearch> = ({ value, onSubmit }) => {
onSubmit('');
}}
>
<Icon src={require('@phosphor-icons/core/regular/backspace.svg')} aria-label={intl.formatMessage(messages.search)} className={clsx('size-5 text-gray-600', { hidden: !hasValue })} />
<Icon src={require('@phosphor-icons/core/regular/backspace.svg')} className={clsx('size-5 text-gray-600', { hidden: !hasValue })} aria-hidden />
</div>
</label>

View File

@ -134,6 +134,8 @@ const MediaItem: React.FC<IMediaItem> = ({ attachment, onOpenMedia, isLast }) =>
'hidden': visible,
'rounded-br-md': isLast,
})}
aria-label={!visible ? attachment.description : undefined}
aria-hidden={visible}
/>
{visible && thumbnail}
{!visible && icon}

View File

@ -77,13 +77,12 @@ const SearchInput: React.FC<ISearchInput> = ({ className, placeholder, query })
<div
className={clsx('z-10 w-full bg-white/90 backdrop-blur backdrop-saturate-200 black:bg-black/75 dark:bg-primary-900/90', className)}
>
<label htmlFor='search' className='sr-only'>{placeholder || intl.formatMessage(messages.placeholder)}</label>
<div className='relative'>
<Input
type='text'
id='search'
placeholder={placeholder || intl.formatMessage(messages.placeholder)}
aria-label={placeholder || intl.formatMessage(messages.placeholder)}
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}

View File

@ -102,9 +102,7 @@ const Search: React.FC<IAliasesSearch> = ({ onSubmit }) => {
return (
<div className='flex items-center gap-1'>
<label className='relative grow'>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.search)}</span>
<label className='relative grow' title={intl.formatMessage(messages.search)}>
<input
className='block w-full rounded-full focus:border-primary-500 focus:ring-primary-500 dark:bg-gray-800 dark:text-white dark:placeholder:text-gray-500 sm:text-sm'
type='text'
@ -120,7 +118,7 @@ const Search: React.FC<IAliasesSearch> = ({ onSubmit }) => {
onClick={handleClear}
title={intl.formatMessage(messages.clear)}
>
<Icon src={require('@phosphor-icons/core/regular/backspace.svg')} className={clsx('size-5 text-gray-600', { 'hidden': !hasValue })} />
<Icon src={require('@phosphor-icons/core/regular/backspace.svg')} className={clsx('size-5 text-gray-600', { 'hidden': !hasValue })} aria-hidden />
</button>
</label>
<Button onClick={handleSubmit}>{intl.formatMessage(messages.searchTitle)}</Button>

View File

@ -254,10 +254,6 @@ a.⁂-list-item,
width: 1.5rem;
@apply rtl:rotate-180;
}
&__label {
@apply sr-only;
}
}
}

View File

@ -193,7 +193,7 @@ body {
}
.-sidebar-navigation-link {
@apply flex items-center py-1 text-sm font-semibold space-x-4 rtl:space-x-reverse transition-colors duration-200;
@apply flex items-center w-full py-1 text-sm font-semibold space-x-4 rtl:space-x-reverse transition-colors duration-200;
@media (height >= 960px) {
padding: 0.5rem 0;

View File

@ -144,7 +144,7 @@
} @else if $theme == tertiary {
@apply bg-white dark:bg-primary-900 border-gray-400 dark:border-gray-800 hover:border-primary-300 dark:hover:border-primary-700 focus:border-primary-500 text-gray-900 dark:text-gray-100 focus:ring-primary-500;
} @else if $theme == accent {
@apply border-transparent bg-secondary-500 hover:bg-secondary-400 focus:bg-secondary-500 text-gray-100 focus:ring-secondary-300;
@apply border-transparent bg-secondary-500 hover:bg-secondary-400 focus:bg-secondary-500 text-white focus:ring-secondary-300;
} @else if $theme == danger {
@apply border-transparent bg-danger-100 dark:bg-danger-900 text-danger-600 dark:text-danger-200 hover:bg-danger-600 hover:text-gray-100 dark:hover:text-gray-100 dark:hover:bg-danger-500 focus:ring-danger-500;
} @else if $theme == transparent {