Prefer accessible links

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
environments/review-links-a11y-wuaw5o/deployments/3741
marcin mikołajczak 2023-08-25 23:19:56 +02:00
rodzic eb05a67671
commit 14e2e07305
9 zmienionych plików z 128 dodań i 178 usunięć

Wyświetl plik

@ -1,5 +1,6 @@
import clsx from 'clsx';
import React from 'react';
import { Link } from 'react-router-dom';
import { v4 as uuidv4 } from 'uuid';
import { SelectDropdown } from '../features/forms';
@ -17,13 +18,14 @@ const List: React.FC<IList> = ({ children }) => (
interface IListItem {
label: React.ReactNode
hint?: React.ReactNode
to?: string
onClick?(): void
onSelect?(): void
isSelected?: boolean
children?: React.ReactNode
}
const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelect, isSelected }) => {
const ListItem: React.FC<IListItem> = ({ label, hint, children, to, onClick, onSelect, isSelected }) => {
const id = uuidv4();
const domId = `list-group-${id}`;
@ -33,9 +35,9 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelec
}
};
const Comp = onClick ? 'a' : 'div';
const LabelComp = onClick || onSelect ? 'span' : 'label';
const linkProps = onClick || onSelect ? { onClick: onClick || onSelect, onKeyDown, tabIndex: 0, role: 'link' } : {};
const Comp = to ? Link : (onClick ? 'a' : 'div');
const LabelComp = to || onClick || onSelect ? 'span' : 'label';
const linkProps = to ? { to } : (onClick || onSelect ? { onClick: onClick || onSelect, onKeyDown, tabIndex: 0, role: 'link' } : {});
const renderChildren = React.useCallback(() => {
return React.Children.map(children, (child) => {
@ -58,7 +60,7 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelec
return (
<Comp
className={clsx('flex items-center justify-between overflow-hidden bg-gradient-to-r from-gradient-start/20 to-gradient-end/20 px-4 py-2 first:rounded-t-lg last:rounded-b-lg dark:from-gradient-start/10 dark:to-gradient-end/10', {
'cursor-pointer hover:from-gradient-start/30 hover:to-gradient-end/30 dark:hover:from-gradient-start/5 dark:hover:to-gradient-end/5': typeof onClick !== 'undefined' || typeof onSelect !== 'undefined',
'cursor-pointer hover:from-gradient-start/30 hover:to-gradient-end/30 dark:hover:from-gradient-start/5 dark:hover:to-gradient-end/5': typeof to !== 'undefined' || typeof onClick !== 'undefined' || typeof onSelect !== 'undefined',
})}
{...linkProps}
>
@ -70,7 +72,7 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelec
) : null}
</div>
{onClick ? (
{(to || onClick) ? (
<HStack space={1} alignItems='center' className='overflow-hidden text-gray-700 dark:text-gray-600'>
{children}
@ -105,7 +107,7 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelec
</div>
) : null}
{typeof onClick === 'undefined' && typeof onSelect === 'undefined' ? renderChildren() : null}
{typeof to === 'undefined' && typeof onClick === 'undefined' && typeof onSelect === 'undefined' ? renderChildren() : null}
</Comp>
);
};

Wyświetl plik

@ -1,6 +1,5 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { useHistory } from 'react-router-dom';
import { getSubscribersCsv, getUnsubscribersCsv, getCombinedCsv } from 'soapbox/actions/email-list';
import List, { ListItem } from 'soapbox/components/list';
@ -15,7 +14,6 @@ import RegistrationModePicker from '../components/registration-mode-picker';
const Dashboard: React.FC = () => {
const dispatch = useAppDispatch();
const history = useHistory();
const instance = useInstance();
const features = useFeatures();
const { account } = useOwnAccount();
@ -41,10 +39,6 @@ const Dashboard: React.FC = () => {
e.preventDefault();
};
const navigateToSoapboxConfig = () => history.push('/soapbox/config');
const navigateToModerationLog = () => history.push('/soapbox/admin/log');
const navigateToAnnouncements = () => history.push('/soapbox/admin/announcements');
const v = parseVersion(instance.version);
const userCount = instance.stats.get('user_count');
@ -87,19 +81,19 @@ const Dashboard: React.FC = () => {
<List>
{account.admin && (
<ListItem
onClick={navigateToSoapboxConfig}
to='/soapbox/config'
label={<FormattedMessage id='navigation_bar.soapbox_config' defaultMessage='Soapbox config' />}
/>
)}
<ListItem
onClick={navigateToModerationLog}
to='/soapbox/admin/log'
label={<FormattedMessage id='column.admin.moderation_log' defaultMessage='Moderation Log' />}
/>
{features.announcements && (
<ListItem
onClick={navigateToAnnouncements}
to='/soapbox/admin/announcements'
label={<FormattedMessage id='column.admin.announcements' defaultMessage='Announcements' />}
/>
)}

Wyświetl plik

@ -2,7 +2,7 @@ import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { changeEmail } from 'soapbox/actions/security';
import { Button, Card, CardBody, CardHeader, CardTitle, Column, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui';
import { Button, Column, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui';
import { useAppDispatch } from 'soapbox/hooks';
import toast from 'soapbox/toast';
@ -48,47 +48,33 @@ const EditEmail = () => {
}, [email, password, dispatch, intl]);
return (
<Column
label={intl.formatMessage(messages.header)}
transparent
withHeader={false}
>
<Card variant='rounded'>
<CardHeader backHref='/settings'>
<CardTitle
title={intl.formatMessage(messages.header)}
<Column label={intl.formatMessage(messages.header)} backHref='/settings'>
<Form onSubmit={handleSubmit}>
<FormGroup labelText={intl.formatMessage(messages.emailFieldLabel)}>
<Input
type='text'
placeholder={intl.formatMessage(messages.emailFieldPlaceholder)}
name='email'
autoComplete='off'
onChange={handleInputChange}
value={email}
/>
</CardHeader>
</FormGroup>
<CardBody>
<Form onSubmit={handleSubmit}>
<FormGroup labelText={intl.formatMessage(messages.emailFieldLabel)}>
<Input
type='text'
placeholder={intl.formatMessage(messages.emailFieldPlaceholder)}
name='email'
autoComplete='off'
onChange={handleInputChange}
value={email}
/>
</FormGroup>
<FormGroup labelText={intl.formatMessage(messages.passwordFieldLabel)}>
<Input
type='password'
name='password'
onChange={handleInputChange}
value={password}
/>
</FormGroup>
<FormGroup labelText={intl.formatMessage(messages.passwordFieldLabel)}>
<Input
type='password'
name='password'
onChange={handleInputChange}
value={password}
/>
</FormGroup>
<FormActions>
<Button to='/settings' theme='tertiary'>{intl.formatMessage(messages.cancel)}</Button>
<Button type='submit' theme='primary' disabled={isLoading}>{intl.formatMessage(messages.submit)}</Button>
</FormActions>
</Form>
</CardBody>
</Card>
<FormActions>
<Button to='/settings' theme='tertiary'>{intl.formatMessage(messages.cancel)}</Button>
<Button type='submit' theme='primary' disabled={isLoading}>{intl.formatMessage(messages.submit)}</Button>
</FormActions>
</Form>
</Column>
);
};

Wyświetl plik

@ -2,7 +2,7 @@ import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { changePassword } from 'soapbox/actions/security';
import { Button, Card, CardBody, CardHeader, CardTitle, Column, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui';
import { Button, Column, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui';
import { useAppDispatch, useFeatures } from 'soapbox/hooks';
import toast from 'soapbox/toast';
@ -55,57 +55,49 @@ const EditPassword = () => {
}, [currentPassword, newPassword, newPasswordConfirmation, dispatch, intl]);
return (
<Column label={intl.formatMessage(messages.header)} transparent withHeader={false}>
<Card variant='rounded'>
<CardHeader backHref='/settings'>
<CardTitle title={intl.formatMessage(messages.header)} />
</CardHeader>
<Column label={intl.formatMessage(messages.header)} backHref='/settings'>
<Form onSubmit={handleSubmit}>
<FormGroup labelText={intl.formatMessage(messages.oldPasswordFieldLabel)}>
<Input
type='password'
name='currentPassword'
onChange={handleInputChange}
value={currentPassword}
/>
</FormGroup>
<CardBody>
<Form onSubmit={handleSubmit}>
<FormGroup labelText={intl.formatMessage(messages.oldPasswordFieldLabel)}>
<Input
type='password'
name='currentPassword'
onChange={handleInputChange}
value={currentPassword}
/>
</FormGroup>
<FormGroup labelText={intl.formatMessage(messages.newPasswordFieldLabel)}>
<Input
type='password'
name='newPassword'
onChange={handleInputChange}
value={newPassword}
/>
<FormGroup labelText={intl.formatMessage(messages.newPasswordFieldLabel)}>
<Input
type='password'
name='newPassword'
onChange={handleInputChange}
value={newPassword}
/>
{passwordRequirements && (
<PasswordIndicator password={newPassword} onChange={setHasValidPassword} />
)}
</FormGroup>
{passwordRequirements && (
<PasswordIndicator password={newPassword} onChange={setHasValidPassword} />
)}
</FormGroup>
<FormGroup labelText={intl.formatMessage(messages.confirmationFieldLabel)}>
<Input
type='password'
name='newPasswordConfirmation'
onChange={handleInputChange}
value={newPasswordConfirmation}
/>
</FormGroup>
<FormGroup labelText={intl.formatMessage(messages.confirmationFieldLabel)}>
<Input
type='password'
name='newPasswordConfirmation'
onChange={handleInputChange}
value={newPasswordConfirmation}
/>
</FormGroup>
<FormActions>
<Button to='/settings' theme='tertiary'>
{intl.formatMessage(messages.cancel)}
</Button>
<FormActions>
<Button to='/settings' theme='tertiary'>
{intl.formatMessage(messages.cancel)}
</Button>
<Button type='submit' theme='primary' disabled={isLoading || !hasValidPassword}>
{intl.formatMessage(messages.submit)}
</Button>
</FormActions>
</Form>
</CardBody>
</Card>
<Button type='submit' theme='primary' disabled={isLoading || !hasValidPassword}>
{intl.formatMessage(messages.submit)}
</Button>
</FormActions>
</Form>
</Column>
);
};

Wyświetl plik

@ -76,10 +76,6 @@ const ManageGroup: React.FC<IManageGroup> = ({ params }) => {
},
}));
const navigateToEdit = () => history.push(`/group/${group.slug}/manage/edit`);
const navigateToPending = () => history.push(`/group/${group.slug}/manage/requests`);
const navigateToBlocks = () => history.push(`/group/${group.slug}/manage/blocks`);
return (
<Column label={intl.formatMessage(messages.heading)} backHref={`/group/${group.slug}`}>
<CardBody className='space-y-4'>
@ -90,7 +86,7 @@ const ManageGroup: React.FC<IManageGroup> = ({ params }) => {
</CardHeader>
<List>
<ListItem label={intl.formatMessage(messages.editGroup)} onClick={navigateToEdit}>
<ListItem label={intl.formatMessage(messages.editGroup)} to={`/group/${group.slug}/manage/edit`}>
<span dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
</ListItem>
</List>
@ -103,10 +99,10 @@ const ManageGroup: React.FC<IManageGroup> = ({ params }) => {
<List>
{backend.software !== TRUTHSOCIAL && (
<ListItem label={intl.formatMessage(messages.pendingRequests)} onClick={navigateToPending} />
<ListItem label={intl.formatMessage(messages.pendingRequests)} to={`/group/${group.slug}/manage/requests`} />
)}
<ListItem label={intl.formatMessage(messages.blockedMembers)} onClick={navigateToBlocks} />
<ListItem label={intl.formatMessage(messages.blockedMembers)} to={`/group/${group.slug}/manage/blocks`} />
</List>
{isOwner && (

Wyświetl plik

@ -1,5 +1,4 @@
import React from 'react';
import { useHistory } from 'react-router-dom';
import { Avatar, Button, CardTitle, Stack } from 'soapbox/components/ui';
import { type Card as StatusCard } from 'soapbox/types/entities';
@ -9,13 +8,9 @@ interface IGroupLinkPreview {
}
const GroupLinkPreview: React.FC<IGroupLinkPreview> = ({ card }) => {
const history = useHistory();
const { group } = card;
if (!group) return null;
const navigateToGroup = () => history.push(`/group/${group.slug}`);
return (
<Stack className='cursor-default overflow-hidden rounded-lg border border-gray-300 text-center dark:border-gray-800'>
<div
@ -32,7 +27,7 @@ const GroupLinkPreview: React.FC<IGroupLinkPreview> = ({ card }) => {
<Stack space={4} className='p-4'>
<CardTitle title={<span dangerouslySetInnerHTML={{ __html: group.display_name_html }} />} />
<Button theme='primary' onClick={navigateToGroup} block>
<Button theme='primary' to={`/group/${group.slug}`} block>
View Group
</Button>
</Stack>
@ -40,4 +35,4 @@ const GroupLinkPreview: React.FC<IGroupLinkPreview> = ({ card }) => {
);
};
export { GroupLinkPreview };
export { GroupLinkPreview };

Wyświetl plik

@ -1,6 +1,5 @@
import React, { useEffect } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom';
import { fetchMfa } from 'soapbox/actions/mfa';
import List, { ListItem } from 'soapbox/components/list';
@ -38,27 +37,12 @@ const messages = defineMessages({
/** User settings page. */
const Settings = () => {
const dispatch = useAppDispatch();
const history = useHistory();
const intl = useIntl();
const mfa = useAppSelector((state) => state.security.get('mfa'));
const features = useFeatures();
const { account } = useOwnAccount();
const navigateToChangeEmail = () => history.push('/settings/email');
const navigateToChangePassword = () => history.push('/settings/password');
const navigateToMfa = () => history.push('/settings/mfa');
const navigateToSessions = () => history.push('/settings/tokens');
const navigateToEditProfile = () => history.push('/settings/profile');
const navigateToDeleteAccount = () => history.push('/settings/account');
const navigateToMoveAccount = () => history.push('/settings/migration');
const navigateToAliases = () => history.push('/settings/aliases');
const navigateToBackups = () => history.push('/settings/backups');
const navigateToImportData = () => history.push('/settings/import');
const navigateToExportData = () => history.push('/settings/export');
const navigateToMutes = () => history.push('/mutes');
const navigateToBlocks = () => history.push('/blocks');
const isMfaEnabled = mfa.getIn(['settings', 'totp']);
useEffect(() => {
@ -78,7 +62,7 @@ const Settings = () => {
<CardBody>
<List>
<ListItem label={intl.formatMessage(messages.editProfile)} onClick={navigateToEditProfile}>
<ListItem label={intl.formatMessage(messages.editProfile)} to='/settings/profile'>
<span className='max-w-full truncate'>{displayName}</span>
</ListItem>
</List>
@ -90,8 +74,8 @@ const Settings = () => {
<CardBody>
<List>
<ListItem label={intl.formatMessage(messages.mutes)} onClick={navigateToMutes} />
<ListItem label={intl.formatMessage(messages.blocks)} onClick={navigateToBlocks} />
<ListItem label={intl.formatMessage(messages.mutes)} to='/mutes' />
<ListItem label={intl.formatMessage(messages.blocks)} to='/blocks' />
</List>
</CardBody>
@ -105,9 +89,9 @@ const Settings = () => {
<List>
{features.security && (
<>
<ListItem label={intl.formatMessage(messages.changeEmail)} onClick={navigateToChangeEmail} />
<ListItem label={intl.formatMessage(messages.changePassword)} onClick={navigateToChangePassword} />
<ListItem label={intl.formatMessage(messages.configureMfa)} onClick={navigateToMfa}>
<ListItem label={intl.formatMessage(messages.changeEmail)} to='/settings/email' />
<ListItem label={intl.formatMessage(messages.changePassword)} to='/settings/password' />
<ListItem label={intl.formatMessage(messages.configureMfa)} to='/settings/mfa'>
<span>
{isMfaEnabled ?
intl.formatMessage(messages.mfaEnabled) :
@ -117,7 +101,7 @@ const Settings = () => {
</>
)}
{features.sessions && (
<ListItem label={intl.formatMessage(messages.sessions)} onClick={navigateToSessions} />
<ListItem label={intl.formatMessage(messages.sessions)} to='/settings/tokens' />
)}
</List>
</CardBody>
@ -153,25 +137,25 @@ const Settings = () => {
<CardBody>
<List>
{features.importData && (
<ListItem label={intl.formatMessage(messages.importData)} onClick={navigateToImportData} />
<ListItem label={intl.formatMessage(messages.importData)} to='/settings/import' />
)}
{features.exportData && (
<ListItem label={intl.formatMessage(messages.exportData)} onClick={navigateToExportData} />
<ListItem label={intl.formatMessage(messages.exportData)} to='/settings/export' />
)}
{features.backups && (
<ListItem label={intl.formatMessage(messages.backups)} onClick={navigateToBackups} />
<ListItem label={intl.formatMessage(messages.backups)} to='/settings/backups' />
)}
{features.federating && (features.accountMoving ? (
<ListItem label={intl.formatMessage(messages.accountMigration)} onClick={navigateToMoveAccount} />
<ListItem label={intl.formatMessage(messages.accountMigration)} to='/settings/migrations' />
) : features.accountAliases && (
<ListItem label={intl.formatMessage(messages.accountAliases)} onClick={navigateToAliases} />
<ListItem label={intl.formatMessage(messages.accountAliases)} to='/settings/aliases' />
))}
{features.security && (
<ListItem label={<Text theme='danger'>{intl.formatMessage(messages.deleteAccount)}</Text>} onClick={navigateToDeleteAccount} />
<ListItem label={<Text theme='danger'>{intl.formatMessage(messages.deleteAccount)}</Text>} to='/settings/account' />
)}
</List>
</CardBody>

Wyświetl plik

@ -1,7 +1,6 @@
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import React, { useState, useEffect, useMemo } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { useHistory } from 'react-router-dom';
import { updateSoapboxConfig } from 'soapbox/actions/admin';
import { uploadMedia } from 'soapbox/actions/media';
@ -70,7 +69,6 @@ const templates: Record<string, Template> = {
const SoapboxConfig: React.FC = () => {
const intl = useIntl();
const history = useHistory();
const dispatch = useAppDispatch();
const features = useFeatures();
@ -83,8 +81,6 @@ const SoapboxConfig: React.FC = () => {
const [rawJSON, setRawJSON] = useState<string>(JSON.stringify(initialData, null, 2));
const [jsonValid, setJsonValid] = useState(true);
const navigateToThemeEditor = () => history.push('/soapbox/admin/theme');
const soapbox = useMemo(() => {
return normalizeSoapboxConfig(data);
}, [data]);
@ -211,7 +207,7 @@ const SoapboxConfig: React.FC = () => {
<ListItem
label={<FormattedMessage id='soapbox_config.fields.edit_theme_label' defaultMessage='Edit theme' />}
onClick={navigateToThemeEditor}
to='/soapbox/admin/theme'
/>
</List>

Wyświetl plik

@ -2,7 +2,7 @@ import clsx from 'clsx';
import { List as ImmutableList } from 'immutable';
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { useHistory } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { openModal } from 'soapbox/actions/modals';
import { HStack, Text, Emoji } from 'soapbox/components/ui';
@ -17,8 +17,6 @@ interface IStatusInteractionBar {
}
const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.Element | null => {
const history = useHistory();
const me = useAppSelector(({ me }) => me);
const { allowedEmoji } = useSoapboxConfig();
const dispatch = useAppDispatch();
@ -91,16 +89,10 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
return null;
};
const navigateToQuotes: React.EventHandler<React.MouseEvent> = (e) => {
e.preventDefault();
history.push(`/@${status.getIn(['account', 'acct'])}/posts/${status.id}/quotes`);
};
const getQuotes = () => {
if (status.quotes_count) {
return (
<InteractionCounter count={status.quotes_count} onClick={navigateToQuotes}>
<InteractionCounter count={status.quotes_count} to={`/@${status.getIn(['account', 'acct'])}/posts/${status.id}/quotes`}>
<FormattedMessage
id='status.interactions.quotes'
defaultMessage='{count, plural, one {Quote} other {Quotes}}'
@ -209,34 +201,47 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
interface IInteractionCounter {
count: number
onClick?: React.MouseEventHandler<HTMLButtonElement>
children: React.ReactNode
onClick?: React.MouseEventHandler<HTMLButtonElement>
to?: string
}
const InteractionCounter: React.FC<IInteractionCounter> = ({ count, onClick, children }) => {
const InteractionCounter: React.FC<IInteractionCounter> = ({ count, children, onClick, to }) => {
const features = useFeatures();
const className = clsx({
'text-gray-600 dark:text-gray-700': true,
'hover:underline': features.exposableReactions,
'cursor-default': !features.exposableReactions,
});
const body = (
<HStack space={1} alignItems='center'>
<Text weight='bold'>
{shortNumberFormat(count)}
</Text>
<Text tag='div' theme='muted'>
{children}
</Text>
</HStack>
);
if (to) {
return (
<Link to={to} className={className}>
{body}
</Link>
);
}
return (
<button
type='button'
onClick={onClick}
className={
clsx({
'text-gray-600 dark:text-gray-700': true,
'hover:underline': features.exposableReactions,
'cursor-default': !features.exposableReactions,
})
}
className={className}
>
<HStack space={1} alignItems='center'>
<Text weight='bold'>
{shortNumberFormat(count)}
</Text>
<Text tag='div' theme='muted'>
{children}
</Text>
</HStack>
{body}
</button>
);
};