Skip to content

Commit

Permalink
chore: enhance accessibility features across components
Browse files Browse the repository at this point in the history
  • Loading branch information
JulienTexier committed Nov 27, 2024
1 parent c97970c commit 63731d9
Show file tree
Hide file tree
Showing 34 changed files with 2,839 additions and 2,176 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ module.exports = {
'plugin:react/jsx-runtime',

// Accessibility, Localization, and Utility Libraries
'plugin:react-native-a11y/all',
'plugin:lingui/recommended',
'plugin:react-native-a11y/ios',
'plugin:lodash/recommended',
],
parser: '@typescript-eslint/parser',
Expand Down Expand Up @@ -136,6 +136,7 @@ module.exports = {

// Accessibility rules
// Custom accessibility rules for React Native (provided by react-native-a11y plugin)
'react-native-a11y/has-accessibility-hint': 'warn',
},

ignorePatterns: ['/dist/*', '*.d.ts'],
Expand Down
331 changes: 331 additions & 0 deletions docs/A11Y.md

Large diffs are not rendered by default.

4,001 changes: 1,973 additions & 2,028 deletions package-lock.json

Large diffs are not rendered by default.

38 changes: 31 additions & 7 deletions src/app/(auth)/landing.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Trans, msg } from '@lingui/macro';
import { Link } from 'expo-router';
import { useWindowDimensions } from 'react-native';
import { AccessibilityInfo, useWindowDimensions } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import * as DropdownMenu from 'zeego/dropdown-menu';

Expand All @@ -14,6 +14,7 @@ export default function Landing() {
const { height } = useWindowDimensions();
const insets = useSafeAreaInsets();
const theme = useTheme();
const { _ } = useI18n();

return (
<Wrapper>
Expand Down Expand Up @@ -51,10 +52,13 @@ export default function Landing() {
<Stack axis="y" spacing="regular" align="center">
<WhiteText variant="body" align="center" withLineHeight>
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
{/* prettier-ignore */}<Trans>Start your journey</Trans>
{/* prettier-ignore */} <Trans>Start your journey</Trans>
</WhiteText>
<Link href="/(auth)/login" asChild>
<Button testID="loginButton">
<Button
testID="loginButton"
accessibilityHint={_(msg`Navigates to the sign-in screen`)}
>
<WhiteText variant="bodyBold">
<Trans>Sign in</Trans>
</WhiteText>
Expand All @@ -68,7 +72,10 @@ export default function Landing() {
<Line />

<Link href="/(auth)/signup" asChild>
<Button testID="signInButton">
<Button
testID="signInButton"
accessibilityHint={_(msg`Navigates to the sign-up screen`)}
>
<WhiteText variant="bodyBold">
<Trans>Create an account</Trans>
</WhiteText>
Expand All @@ -87,15 +94,32 @@ function LanguageSelector() {
const { _, setLocale } = useI18n();

return (
// Note: This is not a11y optimized. It would only say "Button" when focused while using a screen reader.
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<IconButton icon="globe" color="neutral" />
<IconButton icon="globe" />
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item key="fi" onSelect={() => setLocale('fi')}>
<DropdownMenu.Item
key="fi"
onSelect={() => {
setLocale('fi');
AccessibilityInfo.announceForAccessibility(
_(msg`Language set to Finnish`)
);
}}
>
<DropdownMenu.ItemTitle>{_(msg`Finnish`)}</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
<DropdownMenu.Item key="en" onSelect={() => setLocale('en')}>
<DropdownMenu.Item
key="en"
onSelect={() => {
setLocale('en');
AccessibilityInfo.announceForAccessibility(
_(msg`Language set to English`)
);
}}
>
<DropdownMenu.ItemTitle>{_(msg`English`)}</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
</DropdownMenu.Content>
Expand Down
18 changes: 14 additions & 4 deletions src/app/(auth)/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,24 @@ import { Button, Stack, Text, TextInput } from '~components/uikit';
import { useAuthStore } from '~services/auth';
import { useI18n } from '~services/i18n';
import { styled } from '~styles';
import { announceForAccessibility } from '~utils/a11y';

type Credentials = {
email: string;
password: string;
};

export default function Login() {
const { _ } = useI18n();
const form = useForm<Credentials>({ mode: 'onBlur' });
const form = useForm<Credentials>({ mode: 'onChange' });
const { status, login } = useAuthStore();

async function handleSubmit() {
try {
await login(form.getValues());
announceForAccessibility({
message: _(msg`Logged in successfully, entering the app`),
});
} catch (error) {
console.log('> Failed to login', error);
showToast({ title: _(msg`Failed to login`), type: 'error' });
Expand All @@ -31,10 +36,14 @@ export default function Login() {
<InnerStack axis="y" spacing="medium" justify="between">
<Stack axis="y" spacing="medium">
<Stack axis="y" spacing="small">
<Text variant="headingL">
<Text variant="headingL" accessibilityRole="header">
<Trans>Enter your credentials</Trans>
</Text>
<Text variant="bodySmall" color="textMuted">
<Text
variant="bodySmall"
color="textMuted"
accessibilityRole="text"
>
<Trans>You can enter any email and password to login.</Trans>
</Text>
</Stack>
Expand Down Expand Up @@ -102,9 +111,10 @@ export default function Login() {
variant="filled"
size="large"
onPress={form.handleSubmit(handleSubmit)}
disabled={status === 'logging-in'}
disabled={status === 'logging-in' || !form.formState.isValid}
loading={status === 'logging-in'}
testID="loginButton"
accessibilityHint={_(msg`Double tap to log in with the provided email and password`)} // prettier-ignore
>
<Trans>Login</Trans>
</Button>
Expand Down
9 changes: 7 additions & 2 deletions src/app/(auth)/signup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Button, Stack, Text, TextInput } from '~components/uikit';
import { useAuthStore } from '~services/auth';
import { useI18n } from '~services/i18n';
import { styled } from '~styles/styled';
import { announceForAccessibility } from '~utils/a11y';

type Credentials = {
email: string;
Expand Down Expand Up @@ -37,7 +38,10 @@ export default function Signup() {
};

await signup(credentials);
} catch (error: any) {
announceForAccessibility({
message: _(msg`Signed up successfully, entering the app`),
});
} catch (error) {
console.log('> Failed to signup', error);
showToast({ title: _(msg`Failed to signup`), type: 'error' });
}
Expand All @@ -48,7 +52,7 @@ export default function Signup() {
<InnerStack axis="y" spacing="small" justify="between">
<Stack axis="y" spacing="small">
<Stack axis="y" spacing="small">
<Text variant="headingL">
<Text variant="headingL" accessibilityRole="header">
<Trans>Create an account</Trans>
</Text>
<Text variant="bodySmall" color="textMuted">
Expand Down Expand Up @@ -229,6 +233,7 @@ export default function Signup() {
disabled={!isValidForm}
loading={status === 'signing-in'}
testID="signupButton"
accessibilityHint={_(msg`Double tap to signup with the provided information`)} // prettier-ignore
>
<Trans>Signup</Trans>
</Button>
Expand Down
1 change: 1 addition & 0 deletions src/app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export default function TabsLayout() {
name={id}
options={{
title,
tabBarAccessibilityLabel: _(msg`${title} tab`),
tabBarItemStyle: {
// On certain devices without insets, the tab bar is too close to the bottom of the screen
paddingBottom: insets.bottom === 0 ? 4 : 0,
Expand Down
11 changes: 10 additions & 1 deletion src/app/(tabs)/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,25 @@ import { Icon, alert } from '~components/uikit';
import { useAuthStore } from '~services/auth';
import { useI18n } from '~services/i18n';
import { styled } from '~styles';
import { announceForAccessibility } from '~utils/a11y';

export default function Settings() {
useHeaderPlaygroundButton();

const { _ } = useI18n();
const logout = useAuthStore((s) => s.logout);

function onLogout() {
logout();
announceForAccessibility({
message: _(msg`Logged out successfully, back to the landing page`),
});
}

function handleLogout() {
alert(_(msg`Are you sure you want to logout?`), '', [
{ text: _(msg`Cancel`), style: 'cancel', onPress: () => {} },
{ text: _(msg`I am sure`), onPress: logout },
{ text: _(msg`I am sure`), onPress: onLogout },
]);
}

Expand Down
8 changes: 7 additions & 1 deletion src/app/playground/bottom-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,12 @@ export default function BottomSheets() {
<Text variant="headingM" align="center">
Awesome Bottom Sheet
</Text>
<Button onPress={handleClosePress}>Close</Button>
<Button
onPress={handleClosePress}
accessibilityHint="Double tap to close the bottom sheet"
>
Close
</Button>
</ContentContainer>
</BottomSheet>
</Wrapper>
Expand All @@ -90,4 +95,5 @@ const Wrapper = styled('View', {

const ContentContainer = styled(Stack, {
padding: '$large',
zIndex: 1,
});
4 changes: 3 additions & 1 deletion src/app/playground/buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ function Section({
return (
<Card>
<Stack axis="y" spacing="small">
<Text variant="headingS">{title}</Text>
<Text variant="headingS" accessibilityRole="header">
{title}
</Text>
{children}
</Stack>
</Card>
Expand Down
56 changes: 48 additions & 8 deletions src/app/playground/design-system.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,36 @@ export default function DesignSystem() {
<Wrapper>
<Stack axis="y" spacing="xl">
<Stack axis="y" spacing="medium">
<Text variant="headingS">Colors</Text>
<Text
variant="headingS"
accessibilityRole="header"
accessibilityLabel="Colors"
>
Colors
</Text>

{Object.entries(colors).map((category) => (
<Stack key={category[0]} axis="y" spacing="small">
<Text variant="bodyBold">{startCase(category[0])}</Text>
<Text
variant="bodyBold"
accessibilityLabel={startCase(category[0])}
accessibilityRole="header"
>
{startCase(category[0])}
</Text>

<Grid spacing="regular" columns={3}>
{Object.entries(category[1]).map((color) => {
const colorName = color[0] as colors.ColorsToken;
return (
<Stack key={colorName} axis="y" spacing="xs" align="center">
<Stack
key={colorName}
axis="y"
spacing="xs"
align="center"
accessible
accessibilityLabel={`Color token: ${colorName}, color value: ${color[1]}`}
>
<ColorBlock bg={colorName} />
<Text variant="bodySmall" color="neutral2">
{startCase(colorName)}
Expand All @@ -47,9 +66,12 @@ export default function DesignSystem() {
</Stack>

<Stack axis="y" spacing="medium">
<Text variant="headingS">Typography</Text>
<Text variant="headingS" accessibilityRole="header">
Typography
</Text>

<Stack axis="y" spacing="xs">
{/* Accessibility note: Unless we have a description attached to the typography variant coming from Figma, we cannot make this very accessible */}
{typographyNames.map((name) => (
<TypographyBlock key={name}>
<Text variant={name as any}>{startCase(name)}</Text>
Expand All @@ -64,11 +86,20 @@ export default function DesignSystem() {
</Stack>

<Stack axis="y" spacing="regular">
<Text variant="headingS">Radii</Text>
<Text variant="headingS" accessibilityRole="header">
Radii
</Text>

<Grid spacing="regular" justify="center">
{radiiEntries.map(([name, value]) => (
<Stack key={name} axis="y" spacing="xs" align="center">
<Stack
key={name}
axis="y"
spacing="xs"
align="center"
accessible
accessibilityLabel={`Radii token: ${name}, radii value: ${value} pixels`}
>
<RadiiBlock style={{ borderRadius: value }}>
<Text variant="body" color="textMuted">
{value}px
Expand All @@ -81,11 +112,20 @@ export default function DesignSystem() {
</Stack>

<Stack axis="y" spacing="medium">
<Text variant="headingS">Spacing</Text>
<Text variant="headingS" accessibilityRole="header">
Spacing
</Text>

<Stack axis="y" spacing="xxs">
{spacingEntries.map(([name, value]) => (
<Stack key={name} axis="x" spacing="xs" align="center">
<Stack
key={name}
axis="x"
spacing="xs"
align="center"
accessible
accessibilityLabel={`Spacing token: ${name}, spacing value: ${value} pixels`}
>
<Text variant="bodySmall" style={{ minWidth: 56 }}>
{startCase(name)}
</Text>
Expand Down
12 changes: 7 additions & 5 deletions src/app/playground/icons.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Trans, msg } from '@lingui/macro';
import { msg } from '@lingui/macro';
import { setStringAsync } from 'expo-clipboard';
import { Pressable } from 'react-native';

Expand All @@ -16,9 +16,7 @@ export default function Icons() {
<Wrapper>
<Stack axis="y" spacing="medium">
<Note>
<Trans>
You can long press on an icon to copy its name to the clipboard.
</Trans>
You can long press on an icon to copy its name to the clipboard.
</Note>
<Grid spacing="small" justify="between" align="center">
{Object.keys(icons).map((name) => (
Expand All @@ -40,7 +38,11 @@ export default function Icons() {
align="center"
justify="center"
>
<Icon name={name as IconName} size={24} />
<Icon
name={name as IconName}
size={24}
accessibilityLabel={'Icon'}
/>
<Text
variant="bodyExtraSmall"
color="textMuted"
Expand Down
Loading

0 comments on commit 63731d9

Please sign in to comment.