Skip to content

Commit

Permalink
feat: Select zones input field for screen reader (#3702)
Browse files Browse the repository at this point in the history
* Track route changes to focus on zones on return

* Use navigation param instead to trigger selection

* Use 200 ms timeout for older devices

* Use InteractionManager.runAfterInteractions instead of setTimeout with giveFocus

* eslint fix
  • Loading branch information
marius-at-atb committed Jul 3, 2023
1 parent f77b70a commit 6690aa3
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 114 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
PurchaseOverviewTexts,
useTranslation,
} from '@atb/translations';
import React, {useEffect, useState} from 'react';
import React, {useEffect, useRef, useState} from 'react';
import {ScrollView, View} from 'react-native';
import {ProductSelection} from './components/ProductSelection';
import {PurchaseMessages} from './components/PurchaseMessages';
Expand All @@ -23,6 +23,7 @@ import {useOfferState} from './use-offer-state';
import {FlexTicketDiscountInfo} from './components/FlexTicketDiscountInfo';
import {RootStackScreenProps} from '@atb/stacks-hierarchy';
import {useAnalytics} from '@atb/analytics';
import {giveFocus} from '@atb/utils/use-focus-on-load';

type Props = RootStackScreenProps<'Root_PurchaseOverviewScreen'>;

Expand Down Expand Up @@ -87,6 +88,14 @@ export const Root_PurchaseOverviewScreen: React.FC<Props> = ({

const closeModal = () => navigation.popToTop();

const zonesInputSectionItemRef = useRef(null);

useEffect(() => {
if (params.onFocusElement === 'zone-selection') {
giveFocus(zonesInputSectionItemRef);
}
}, [params.onFocusElement]);

return (
<View style={styles.container}>
<FullScreenHeader
Expand All @@ -96,6 +105,7 @@ export const Root_PurchaseOverviewScreen: React.FC<Props> = ({
onPress: closeModal,
}}
globalMessageContext="app-ticketing"
setFocusOnLoad={!params.onFocusElement}
/>

<ScrollView testID="ticketingScrollView">
Expand Down Expand Up @@ -151,10 +161,12 @@ export const Root_PurchaseOverviewScreen: React.FC<Props> = ({
toTariffZone={toTariffZone}
fareProductTypeConfig={params.fareProductTypeConfig}
preassignedFareProduct={preassignedFareProduct}
onSelect={(t) =>
navigation.push('Root_PurchaseTariffZonesSearchByMapScreen', t)
}
onSelect={(t) => {
navigation.setParams({onFocusElement: undefined});
navigation.push('Root_PurchaseTariffZonesSearchByMapScreen', t);
}}
style={styles.selectionComponent}
ref={zonesInputSectionItemRef}
/>

<StartTimeSelection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ import {
TranslateFunction,
useTranslation,
} from '@atb/translations';
import React from 'react';
import {AccessibilityProps, StyleProp, View, ViewStyle} from 'react-native';
import React, {forwardRef} from 'react';
import {
AccessibilityProps,
StyleProp,
View,
ViewStyle,
TouchableOpacity,
} from 'react-native';
import {TariffZoneWithMetadata} from '@atb/tariff-zones-selector';
import {getReferenceDataName} from '@atb/reference-data/utils';
import {GenericClickableSectionItem, Section} from '@atb/components/sections';
Expand All @@ -31,107 +37,114 @@ type ZonesSelectionProps = {
style?: StyleProp<ViewStyle>;
};

export function ZonesSelection({
fareProductTypeConfig,
fromTariffZone,
toTariffZone,
preassignedFareProduct,
onSelect,
style,
}: ZonesSelectionProps) {
const styles = useStyles();
const {t, language} = useTranslation();
export const ZonesSelection = forwardRef<TouchableOpacity, ZonesSelectionProps>(
(
{
fareProductTypeConfig,
fromTariffZone,
toTariffZone,
preassignedFareProduct,
onSelect,
style,
}: ZonesSelectionProps,
zonesInputSectionItemRef,
) => {
const styles = useStyles();
const {t, language} = useTranslation();

const accessibility: AccessibilityProps = {
accessible: true,
accessibilityRole: 'button',
accessibilityLabel:
a11yLabel(fromTariffZone, toTariffZone, language, t) + screenReaderPause,
accessibilityHint: t(PurchaseOverviewTexts.zones.a11yHint),
};
const accessibility: AccessibilityProps = {
accessible: true,
accessibilityRole: 'button',
accessibilityLabel:
a11yLabel(fromTariffZone, toTariffZone, language, t) +
screenReaderPause,
accessibilityHint: t(PurchaseOverviewTexts.zones.a11yHint),
};

let selectionMode = fareProductTypeConfig.configuration.zoneSelectionMode;
let selectionMode = fareProductTypeConfig.configuration.zoneSelectionMode;

if (selectionMode === 'none') {
return null;
}
if (selectionMode === 'none') {
return null;
}

// Only support multiple/single zone in app for now. Stop place is built into selector.
if (selectionMode == 'multiple-stop' || selectionMode == 'multiple-zone') {
selectionMode = 'multiple';
}
if (
preassignedFareProduct.zoneSelectionMode?.includes('single') ||
selectionMode == 'single-stop' ||
selectionMode == 'single-zone'
) {
selectionMode = 'single';
}
// Only support multiple/single zone in app for now. Stop place is built into selector.
if (selectionMode == 'multiple-stop' || selectionMode == 'multiple-zone') {
selectionMode = 'multiple';
}
if (
preassignedFareProduct.zoneSelectionMode?.includes('single') ||
selectionMode == 'single-stop' ||
selectionMode == 'single-zone'
) {
selectionMode = 'single';
}

const displayAsOneZone =
fromTariffZone.id === toTariffZone.id &&
fromTariffZone.venueName === toTariffZone.venueName;
const displayAsOneZone =
fromTariffZone.id === toTariffZone.id &&
fromTariffZone.venueName === toTariffZone.venueName;

return (
<View style={style}>
<ThemeText
type="body__secondary"
color="secondary"
style={styles.sectionText}
accessibilityLabel={t(
PurchaseOverviewTexts.zones.title[selectionMode].a11yLabel,
)}
>
{t(PurchaseOverviewTexts.zones.title[selectionMode].text)}
</ThemeText>
<Section {...accessibility}>
<GenericClickableSectionItem
onPress={() =>
onSelect({
fromTariffZone,
toTariffZone,
fareProductTypeConfig,
preassignedFareProduct,
})
}
testID="selectZonesButton"
return (
<View style={style}>
<ThemeText
type="body__secondary"
color="secondary"
style={styles.sectionText}
accessibilityLabel={t(
PurchaseOverviewTexts.zones.title[selectionMode].a11yLabel,
)}
>
<View style={styles.sectionContentContainer}>
<View>
{displayAsOneZone ? (
<ZoneLabel tariffZone={fromTariffZone} />
) : (
<>
<View style={styles.fromZone}>
<ThemeText
color="secondary"
type="body__secondary"
style={styles.toFromLabel}
>
{t(PurchaseOverviewTexts.zones.label.from)}
</ThemeText>
<ZoneLabel tariffZone={fromTariffZone} />
</View>
<View style={styles.toZone}>
<ThemeText
color="secondary"
type="body__secondary"
style={styles.toFromLabel}
>
{t(PurchaseOverviewTexts.zones.label.to)}
</ThemeText>
<ZoneLabel tariffZone={toTariffZone} />
</View>
</>
)}
{t(PurchaseOverviewTexts.zones.title[selectionMode].text)}
</ThemeText>
<Section {...accessibility}>
<GenericClickableSectionItem
ref={zonesInputSectionItemRef}
onPress={() =>
onSelect({
fromTariffZone,
toTariffZone,
fareProductTypeConfig,
preassignedFareProduct,
})
}
testID="selectZonesButton"
>
<View style={styles.sectionContentContainer}>
<View>
{displayAsOneZone ? (
<ZoneLabel tariffZone={fromTariffZone} />
) : (
<>
<View style={styles.fromZone}>
<ThemeText
color="secondary"
type="body__secondary"
style={styles.toFromLabel}
>
{t(PurchaseOverviewTexts.zones.label.from)}
</ThemeText>
<ZoneLabel tariffZone={fromTariffZone} />
</View>
<View style={styles.toZone}>
<ThemeText
color="secondary"
type="body__secondary"
style={styles.toFromLabel}
>
{t(PurchaseOverviewTexts.zones.label.to)}
</ThemeText>
<ZoneLabel tariffZone={toTariffZone} />
</View>
</>
)}
</View>
<ThemeIcon svg={Edit} size="normal" />
</View>
<ThemeIcon svg={Edit} size="normal" />
</View>
</GenericClickableSectionItem>
</Section>
</View>
);
}
</GenericClickableSectionItem>
</Section>
</View>
);
},
);

const ZoneLabel = ({tariffZone}: {tariffZone: TariffZoneWithMetadata}) => {
const {t, language} = useTranslation();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export type Root_PurchaseOverviewScreenParams = {
toTariffZone?: TariffZoneWithMetadata;
mode?: 'Ticket' | 'TravelSearch';
travelDate?: string;
onFocusElement?: string;
};
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const Root_PurchaseTariffZonesSearchByMapScreen = ({
mode: 'Ticket',
fareProductTypeConfig,
fromTariffZone: selectedZones.from,
onFocusElement: 'zone-selection',
toTariffZone: isApplicableOnSingleZoneOnly
? selectedZones.from
: selectedZones.to,
Expand Down
31 changes: 16 additions & 15 deletions src/utils/use-focus-on-load.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import React, {useEffect, useRef} from 'react';
import {AccessibilityInfo, findNodeHandle} from 'react-native';
import {
AccessibilityInfo,
InteractionManager,
findNodeHandle,
} from 'react-native';
import {useNavigationSafe} from '@atb/utils/use-navigation-safe';

/**
Expand All @@ -16,32 +20,29 @@ export function useFocusOnLoad(setFocusOnLoad: boolean = true) {
useEffect(() => {
if (!setFocusOnLoad || !focusRef.current) return;

const timeoutId = setTimeout(() => giveFocus(focusRef), 200);
return () => clearTimeout(timeoutId);
giveFocus(focusRef);
}, [focusRef.current, setFocusOnLoad]);

const navigation = useNavigationSafe();
useEffect(() => {
if (!navigation || !focusRef.current || !setFocusOnLoad) return;

let timeoutId: NodeJS.Timeout | undefined = undefined;
const unsubscribe = navigation.addListener('focus', () => {
timeoutId = setTimeout(() => giveFocus(focusRef), 200);
});
return () => {
if (timeoutId) clearTimeout(timeoutId);
unsubscribe();
};
const unsubscribe = navigation.addListener('focus', () =>
giveFocus(focusRef),
);
return () => unsubscribe();
}, [navigation, focusRef.current, setFocusOnLoad]);

return focusRef;
}

export const giveFocus = (focusRef: React.MutableRefObject<any>) => {
if (focusRef.current) {
const reactTag = findNodeHandle(focusRef.current);
if (reactTag) {
AccessibilityInfo.setAccessibilityFocus(reactTag);
}
InteractionManager.runAfterInteractions(() => {
const reactTag = findNodeHandle(focusRef.current);
if (reactTag) {
AccessibilityInfo.setAccessibilityFocus(reactTag);
}
});
}
};

0 comments on commit 6690aa3

Please sign in to comment.