Skip to content

fix(ios): restore VoiceOver increment/decrement on Fabric - #825

Open
kosmydel wants to merge 1 commit into
callstack:mainfrom
discord:kosmydel/ios-fabric-voiceover
Open

fix(ios): restore VoiceOver increment/decrement on Fabric#825
kosmydel wants to merge 1 commit into
callstack:mainfrom
discord:kosmydel/ios-fabric-voiceover

Conversation

@kosmydel

@kosmydel kosmydel commented Aug 26, 2026

Copy link
Copy Markdown

Summary:

On Fabric, RNCSliderComponentView hosts UISlider as contentView. RCTViewComponentView treats that as the accessibility leaf, so VoiceOver focuses the wrapper instead of the slider. The wrapper is a plain UIView and does not implement increment/decrement, which drops UISlider's Adjustable trait.

This matches React Native's own text-input pattern: the host is not an accessibility element, and accessibilityElement points at the inner control.

Test Plan:

  1. New Architecture iOS app with a Slider that has an accessibilityLabel (no custom accessibilityValue).
  2. Enable VoiceOver. Swipe to the slider.
  3. Before: focus is a generic view; swipe up/down does not change the value.
  4. After: VoiceOver reports Adjustable; swipe up increments and swipe down decrements; the spoken value updates.
Expo example App.tsx
import {useState} from 'react';
import {StatusBar} from 'expo-status-bar';
import {StyleSheet, Text, View} from 'react-native';
import Slider from '@react-native-community/slider';

const MIN = 0;
const MAX = 10;
const STEP = 1;

// VoiceOver uses index = slider value. Length must be maximumValue + 1.
const ACCESSIBILITY_INCREMENTS = Array.from({length: MAX - MIN + 1}, (_, i) =>
  String((MIN + i) * 10),
);

export default function App() {
  const [value, setValue] = useState(5);

  return (
    <View style={styles.container}>
      <Text style={styles.title} accessibilityRole="header">
        Slider draft
      </Text>
      <Text
        style={styles.value}
        importantForAccessibility="no"
        accessibilityElementsHidden>
        {value * 10}%
      </Text>
      <Slider
        style={styles.slider}
        minimumValue={MIN}
        maximumValue={MAX}
        step={STEP}
        value={value}
        onValueChange={setValue}
        tapToSeek
        minimumTrackTintColor="#1411AB"
        maximumTrackTintColor="#ABABAB"
        thumbTintColor="#1411AB"
        thumbSize={32}
        accessibilityLabel="Volume"
        accessibilityHint="Swipe up or down with VoiceOver to adjust"
        accessibilityRole="adjustable"
        accessibilityUnits="percents"
        accessibilityIncrements={ACCESSIBILITY_INCREMENTS}
      />
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
    paddingHorizontal: 24,
  },
  title: {
    fontSize: 24,
    marginBottom: 12,
  },
  value: {
    fontSize: 18,
    marginBottom: 24,
  },
  slider: {
    width: '100%',
    height: 40,
  },
});

RCTViewComponentView treats contentView as the a11y leaf, so the Fabric
wrapper stole UISlider's Adjustable trait. Point VoiceOver at the inner
slider instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
@kosmydel
kosmydel marked this pull request as ready for review August 26, 2026 13:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant