Skip to content

Commit

Permalink
feat(core): handle accessibility in focusable views (#79)
Browse files Browse the repository at this point in the history
  • Loading branch information
JulienIzz authored Feb 28, 2024
1 parent 3d02c5e commit 1d4bdcf
Show file tree
Hide file tree
Showing 7 changed files with 59 additions and 44 deletions.
13 changes: 8 additions & 5 deletions docs/accessibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ Here's a video of what we could achieve.

![talkback](./talkback.gif)

Since we bypass the native focus, and the screen readers rely on the native elements, it's
a difficult topic.
Since we bypass the native focus, and the screen readers rely on the native elements, it's a difficult topic.

We export a hook that returns you props that you can provide to your focusable elements.
The main caveat is that your elements will still be focusable, but the user will need to press
enter to grab focus on an element, which is not standard at all.
The `SpatialNavigatioNFocusableView` that you can use integrate basic accessibility props relevant to make the library work with TalkBack (Android only).

The two main caveats are :

- Your elements will still be focusable, but the user will need to press
enter to grab focus on an element, which is not standard at all.
- You might need to add the props `accessible` on `SpatialNavigationFocusableView` children. For example, focusable images won't work without it. You can check it out in the example, in the `Program.tsx` component.

We could not find a way to properly intercept the accessibility focus event, even with a React Native patch.

Expand Down
14 changes: 2 additions & 12 deletions packages/example/src/components/Menu/MenuButton.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import styled from '@emotion/native';
import { forwardRef } from 'react';
import { Animated, View } from 'react-native';
import {
SpatialNavigationFocusableView,
useSpatialNavigatorFocusableAccessibilityProps,
} from 'react-tv-space-navigation';
import { SpatialNavigationFocusableView } from 'react-tv-space-navigation';
import { scaledPixels } from '../../design-system/helpers/scaledPixels';
import { useFocusAnimation } from '../../design-system/helpers/useFocusAnimation';
import { theme } from '../../design-system/theme/theme';
Expand All @@ -21,15 +18,8 @@ const ButtonContent = forwardRef<View, { icon: IconName; isFocused: boolean; isM
(props, ref) => {
const { isFocused, icon, isMenuOpen } = props;
const anim = useFocusAnimation(isFocused && isMenuOpen);
const accessibilityProps = useSpatialNavigatorFocusableAccessibilityProps();
return (
<Container
style={anim}
isFocused={isFocused}
isMenuOpen={isMenuOpen}
ref={ref}
{...accessibilityProps}
>
<Container style={anim} isFocused={isFocused} isMenuOpen={isMenuOpen} ref={ref}>
<Icon
icon={icon}
size={theme.sizes.menu.icon}
Expand Down
8 changes: 2 additions & 6 deletions packages/example/src/design-system/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { forwardRef } from 'react';
import { Animated, View } from 'react-native';
import {
SpatialNavigationFocusableView,
useSpatialNavigatorFocusableAccessibilityProps,
} from 'react-tv-space-navigation';
import { SpatialNavigationFocusableView } from 'react-tv-space-navigation';
import { Typography } from './Typography';
import styled from '@emotion/native';
import { useFocusAnimation } from '../helpers/useFocusAnimation';
Expand All @@ -17,9 +14,8 @@ type ButtonProps = {
const ButtonContent = forwardRef<View, { label: string; isFocused: boolean }>((props, ref) => {
const { isFocused, label } = props;
const anim = useFocusAnimation(isFocused);
const accessibilityProps = useSpatialNavigatorFocusableAccessibilityProps();
return (
<Container style={anim} isFocused={isFocused} ref={ref} {...accessibilityProps}>
<Container style={anim} isFocused={isFocused} ref={ref}>
<ColoredTypography isFocused={isFocused}>{label}</ColoredTypography>
</Container>
);
Expand Down
6 changes: 1 addition & 5 deletions packages/example/src/modules/program/view/LongProgram.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import React from 'react';
import { Animated, Image, View } from 'react-native';
import { ProgramInfo } from '../domain/programInfo';
import { useFocusAnimation } from '../../../design-system/helpers/useFocusAnimation';
import { useSpatialNavigatorFocusableAccessibilityProps } from 'react-tv-space-navigation';

type LongProgramProps = {
isFocused?: boolean;
Expand All @@ -16,16 +15,13 @@ export const LongProgram = React.forwardRef<View, LongProgramProps>(

const scaleAnimation = useFocusAnimation(isFocused);

const accessibilityProps = useSpatialNavigatorFocusableAccessibilityProps();

return (
<LongProgramContainer
style={scaleAnimation} // Apply the animated scale transform
ref={ref}
isFocused={isFocused}
{...accessibilityProps}
>
<LongProgramImage source={imageSource} />
<LongProgramImage source={imageSource} accessible />
</LongProgramContainer>
);
},
Expand Down
6 changes: 1 addition & 5 deletions packages/example/src/modules/program/view/Program.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import React from 'react';
import { Animated, Image, View } from 'react-native';
import { ProgramInfo } from '../domain/programInfo';
import { useFocusAnimation } from '../../../design-system/helpers/useFocusAnimation';
import { useSpatialNavigatorFocusableAccessibilityProps } from 'react-tv-space-navigation';

type ProgramProps = {
isFocused?: boolean;
Expand All @@ -16,16 +15,13 @@ export const Program = React.forwardRef<View, ProgramProps>(

const scaleAnimation = useFocusAnimation(isFocused);

const accessibilityProps = useSpatialNavigatorFocusableAccessibilityProps();

return (
<ProgramContainer
style={scaleAnimation} // Apply the animated scale transform
ref={ref}
isFocused={isFocused}
{...accessibilityProps}
>
<ProgramImage source={imageSource} />
<ProgramImage source={imageSource} accessible />
</ProgramContainer>
);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import React from 'react';
import { Animated, Image, View } from 'react-native';
import { ProgramInfo } from '../domain/programInfo';
import { useFocusAnimation } from '../../../design-system/helpers/useFocusAnimation';
import { useSpatialNavigatorFocusableAccessibilityProps } from 'react-tv-space-navigation';

type ProgramProps = {
isFocused?: boolean;
Expand All @@ -16,16 +15,13 @@ export const ProgramLandscape = React.forwardRef<View, ProgramProps>(

const scaleAnimation = useFocusAnimation(isFocused);

const accessibilityProps = useSpatialNavigatorFocusableAccessibilityProps();

return (
<ProgramContainer
style={scaleAnimation} // Apply the animated scale transform
ref={ref}
isFocused={isFocused}
{...accessibilityProps}
>
<ProgramImage source={imageSource} />
<ProgramImage source={imageSource} accessible />
</ProgramContainer>
);
},
Expand Down
50 changes: 44 additions & 6 deletions packages/lib/src/spatial-navigation/components/FocusableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@ import { Platform, View, ViewStyle, ViewProps } from 'react-native';
import { forwardRef, useImperativeHandle, useRef } from 'react';
import { SpatialNavigationNodeRef } from '../types/SpatialNavigationNodeRef';
import { useSpatialNavigationDeviceType } from '../context/DeviceContext';
import { useSpatialNavigatorFocusableAccessibilityProps } from '../hooks/useSpatialNavigatorFocusableAccessibilityProps';

type Props = SpatialNavigationNodeDefaultProps & {
type FocusableViewProps = {
style?: ViewStyle;
children:
| React.ReactElement
| ((props: { isFocused: boolean; isActive: boolean }) => React.ReactElement);
viewProps?: ViewProps & { onMouseEnter?: () => void };
viewProps?: ViewProps & {
onMouseEnter?: () => void;
};
};

type Props = SpatialNavigationNodeDefaultProps & FocusableViewProps;

export const SpatialNavigationFocusableView = forwardRef<SpatialNavigationNodeRef, Props>(
({ children, style, viewProps, ...props }: Props, ref) => {
({ children, style, viewProps, ...props }, ref) => {
const { deviceType } = useSpatialNavigationDeviceType();
const nodeRef = useRef<SpatialNavigationNodeRef>(null);

Expand Down Expand Up @@ -45,12 +50,45 @@ export const SpatialNavigationFocusableView = forwardRef<SpatialNavigationNodeRe
return (
<SpatialNavigationNode isFocusable {...props} ref={nodeRef}>
{({ isFocused, isActive }) => (
<View style={style} {...viewProps} {...webProps}>
{typeof children === 'function' ? children({ isFocused, isActive }) : children}
</View>
<InnerFocusableView
viewProps={viewProps}
webProps={webProps}
style={style}
isActive={isActive}
isFocused={isFocused}
>
{children}
</InnerFocusableView>
)}
</SpatialNavigationNode>
);
},
);
SpatialNavigationFocusableView.displayName = 'SpatialNavigationFocusableView';

type InnerFocusableViewProps = FocusableViewProps & {
webProps:
| {
onMouseEnter: () => void;
onClick: () => void;
}
| {
onMouseEnter?: undefined;
onClick?: undefined;
};
isActive: boolean;
isFocused: boolean;
};

const InnerFocusableView = forwardRef<View, InnerFocusableViewProps>(
({ viewProps, webProps, children, isActive, isFocused, style }, ref) => {
const accessibilityProps = useSpatialNavigatorFocusableAccessibilityProps();

return (
<View ref={ref} style={style} {...accessibilityProps} {...viewProps} {...webProps}>
{typeof children === 'function' ? children({ isFocused, isActive }) : children}
</View>
);
},
);
InnerFocusableView.displayName = 'InnerFocusableView';

0 comments on commit 1d4bdcf

Please sign in to comment.