Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 126 additions & 74 deletions README.md

Large diffs are not rendered by default.

193 changes: 164 additions & 29 deletions src/components/DurationScroll/DurationScroll.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import React, {
useMemo,
} from "react";

import { View, Text, FlatList as RNFlatList } from "react-native";
import {
View,
Text,
FlatList as RNFlatList,
AccessibilityInfo,
} from "react-native";
import type {
ViewabilityConfigCallbackPairs,
FlatListProps,
Expand Down Expand Up @@ -36,6 +41,8 @@ const keyExtractor = (item: any, index: number) => index.toString();
const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>(
(props, ref) => {
const {
accessibilityHint,
accessibilityLabel,
aggressivelyGetLatestDuration,
allowFontScaling = false,
amLabel,
Expand All @@ -44,11 +51,13 @@ const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>(
decelerationRate = 0.88,
disableInfiniteScroll = false,
FlatList = RNFlatList,
formatValue,
Haptics,
initialValue = 0,
interval,
is12HourPicker,
isDisabled,
isScreenReaderEnabled = false,
label,
limit,
LinearGradient,
Expand Down Expand Up @@ -469,6 +478,81 @@ const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>(
[styles.pickerItemContainer.height]
);

const handleAccessibilityAction = useCallback(
(event: { nativeEvent: { actionName: string } }) => {
const { actionName } = event.nativeEvent;

if (actionName === "increment") {
let newValue = latestDuration.current + interval;

// Wrap around to minimum if exceeding maximum
if (newValue > adjustedLimited.max) {
newValue = adjustedLimited.min;
}

flatListRef.current?.scrollToIndex({
animated: true,
index: getInitialScrollIndex({
disableInfiniteScroll,
interval,
numberOfItems,
padWithNItems,
repeatNumbersNTimes: safeRepeatNumbersNTimes,
value: newValue,
}),
});
latestDuration.current = newValue;

// Announce the new value to screen readers
const announcement = formatValue
? formatValue(newValue)
: String(newValue);
AccessibilityInfo.announceForAccessibilityWithOptions(
announcement,
{
queue: false,
}
);
} else if (actionName === "decrement") {
let newValue = latestDuration.current - interval;

// Wrap around to maximum if going below minimum
if (newValue < adjustedLimited.min) {
newValue = adjustedLimited.max;
}

flatListRef.current?.scrollToIndex({
animated: true,
index: getInitialScrollIndex({
disableInfiniteScroll,
interval,
numberOfItems,
padWithNItems,
repeatNumbersNTimes: safeRepeatNumbersNTimes,
value: newValue,
}),
});
latestDuration.current = newValue;

// Announce the new value to screen readers
const announcement = formatValue
? formatValue(newValue)
: String(newValue);
AccessibilityInfo.announceForAccessibility(announcement);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for not using announceForAccessibilityWithOptions with queue: false for decrementing (as for incrementing?)

}
},
[
adjustedLimited.max,
adjustedLimited.min,
disableInfiniteScroll,
formatValue,
interval,
numberOfItems,
padWithNItems,
safeRepeatNumbersNTimes,
]
);

useImperativeHandle(ref, () => ({
reset: (options) => {
flatListRef.current?.scrollToIndex({
Expand All @@ -495,37 +579,83 @@ const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>(
const renderContent = useMemo(() => {
return (
<>
<FlatList
key={flatListRenderKey}
ref={flatListRef}
contentContainerStyle={
styles.durationScrollFlatListContentContainer
<View
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this extra View is potentially problematic. It could well interfere with people's layout — it has no style prop, so it'll use default flex behaviour and users can't target it with styling. There's already a wrapper View in this component in the return, I think we can just move these accessibility props there.

accessibilityActions={
isScreenReaderEnabled
? [{ name: "increment" }, { name: "decrement" }]
: undefined
}
accessibilityHint={
isScreenReaderEnabled
? accessibilityHint
: undefined
}
accessibilityLabel={
isScreenReaderEnabled
? accessibilityLabel
: undefined
}
data={numbersForFlatList}
decelerationRate={decelerationRate}
getItemLayout={getItemLayout}
initialScrollIndex={initialScrollIndex}
keyExtractor={keyExtractor}
nestedScrollEnabled
onMomentumScrollEnd={onMomentumScrollEnd}
onScroll={onScroll}
renderItem={renderItem}
scrollEnabled={!isDisabled}
scrollEventThrottle={16}
showsVerticalScrollIndicator={false}
snapToAlignment="start"
// used in place of snapToInterval due to bug on Android
snapToOffsets={[
...Array(numbersForFlatList.length),
].map((_, i) => i * styles.pickerItemContainer.height)}
style={styles.durationScrollFlatList}
testID="duration-scroll-flatlist"
viewabilityConfigCallbackPairs={
viewabilityConfigCallbackPairs
accessibilityRole={
isScreenReaderEnabled ? "adjustable" : undefined
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we all need all of these ternaries? I think a lot of these are harmless even if there's no screen reader so would be cleaner to just set them. Not certain but looks as though the only two that actually matter are: importantForAccessibility="no-hide-descendants" and accessibilityElementsHidden

}
windowSize={numberOfItemsToShow}
/>
accessibilityValue={
isScreenReaderEnabled
? {
text: formatValue
? formatValue(latestDuration.current)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is memoized so this won't receive the latest value of this ref - you'll end up with a stale value here I think

: String(latestDuration.current),
}
: undefined
}
accessible={isScreenReaderEnabled ? true : false}
onAccessibilityAction={handleAccessibilityAction}>
<FlatList
key={flatListRenderKey}
ref={flatListRef}
accessible={
isScreenReaderEnabled ? false : undefined
}
contentContainerStyle={
styles.durationScrollFlatListContentContainer
}
data={numbersForFlatList}
decelerationRate={decelerationRate}
getItemLayout={getItemLayout}
importantForAccessibility={
isScreenReaderEnabled
? "no-hide-descendants"
: undefined
}
initialScrollIndex={initialScrollIndex}
keyExtractor={keyExtractor}
nestedScrollEnabled
onMomentumScrollEnd={onMomentumScrollEnd}
onScroll={onScroll}
renderItem={renderItem}
scrollEnabled={!isDisabled}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think we probably need: accessibilityState={{ disabled: isDisabled }}

scrollEventThrottle={16}
showsVerticalScrollIndicator={false}
snapToAlignment="start"
// used in place of snapToInterval due to bug on Android
snapToOffsets={[
...Array(numbersForFlatList.length),
].map(
(_, i) => i * styles.pickerItemContainer.height
)}
style={styles.durationScrollFlatList}
testID="duration-scroll-flatlist"
viewabilityConfigCallbackPairs={
viewabilityConfigCallbackPairs
}
windowSize={numberOfItemsToShow}
/>
</View>
<View
accessibilityElementsHidden={isScreenReaderEnabled}
accessible={false}
importantForAccessibility={
isScreenReaderEnabled ? "no-hide-descendants" : "no"
}
pointerEvents="none"
style={styles.pickerLabelContainer}>
{typeof label === "string" ? (
Expand All @@ -542,12 +672,17 @@ const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>(
);
}, [
FlatList,
accessibilityHint,
accessibilityLabel,
allowFontScaling,
decelerationRate,
flatListRenderKey,
formatValue,
getItemLayout,
handleAccessibilityAction,
initialScrollIndex,
isDisabled,
isScreenReaderEnabled,
label,
numberOfItemsToShow,
numbersForFlatList,
Expand Down
4 changes: 4 additions & 0 deletions src/components/DurationScroll/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,20 @@ export interface DurationScrollProps {
Haptics?: any;
LinearGradient?: any;
MaskedView?: any;
accessibilityHint?: string;
accessibilityLabel?: string;
aggressivelyGetLatestDuration: boolean;
allowFontScaling?: boolean;
amLabel?: string;
clickSoundAsset?: SoundAsset;
decelerationRate?: number | "normal" | "fast";
disableInfiniteScroll?: boolean;
formatValue?: (value: number) => string;
initialValue?: number;
interval: number;
is12HourPicker?: boolean;
isDisabled?: boolean;
isScreenReaderEnabled?: boolean;
label?: string | React.ReactElement;
limit?: Limit;
maximumValue: number;
Expand Down
Loading