From 031a832df87c4f8f877df3936b56f53d042474ed Mon Sep 17 00:00:00 2001 From: Yury Kalenkou Date: Sun, 18 Oct 2026 00:25:12 +0200 Subject: [PATCH 1/6] feat: Vega OS (Fire TV) support: a JS slider with D-pad focus --- .../RNCSliderNativeComponent.kepler.test.tsx | 242 ++++++++++++++ .../src/RNCSliderNativeComponent.kepler.tsx | 310 ++++++++++++++++++ 2 files changed, 552 insertions(+) create mode 100644 package/__test__/RNCSliderNativeComponent.kepler.test.tsx create mode 100644 package/src/RNCSliderNativeComponent.kepler.tsx diff --git a/package/__test__/RNCSliderNativeComponent.kepler.test.tsx b/package/__test__/RNCSliderNativeComponent.kepler.test.tsx new file mode 100644 index 00000000..11d3fd0a --- /dev/null +++ b/package/__test__/RNCSliderNativeComponent.kepler.test.tsx @@ -0,0 +1,242 @@ +import React from 'react'; +import {act, fireEvent, render} from '@testing-library/react-native'; + +// Vega OS sends the remote's keys through useTVEventHandler. React Native core has no such +// hook, so the tests keep the handler the component registers and press keys through it. +let mockKeyHandler: + | ((event: {eventType: string; eventKeyAction?: number}) => void) + | null = null; +jest.mock('react-native', () => { + const ReactNative = jest.requireActual('react-native'); + return Object.setPrototypeOf( + { + useTVEventHandler: (handler: typeof mockKeyHandler) => { + mockKeyHandler = handler; + }, + }, + ReactNative, + ); +}); + +import RNCSliderKepler from '../src/RNCSliderNativeComponent.kepler'; + +const press = (eventType: string) => + act(() => { + mockKeyHandler?.({eventType, eventKeyAction: 0}); + }); +const release = (eventType: string) => + act(() => { + mockKeyHandler?.({eventType, eventKeyAction: 1}); + }); + +function setup( + props: Partial> = {}, +) { + const events: Array<[string, number]> = []; + const screen = render( + + events.push(['start', e.nativeEvent.value]) + } + onRNCSliderValueChange={(e) => + events.push(['change', e.nativeEvent.value]) + } + onRNCSliderSlidingComplete={(e) => + events.push(['complete', e.nativeEvent.value]) + } + {...props} + />, + ); + const slider = screen.getByTestId('slider'); + const focus = () => act(() => fireEvent(slider, 'focus')); + const now = () => slider.props.accessibilityValue.now; + return {screen, slider, events, focus, now}; +} + +beforeEach(() => { + mockKeyHandler = null; +}); + +test('is an adjustable control with its range and value for VoiceView', () => { + const {slider} = setup(); + expect(slider.props.accessibilityRole).toBe('adjustable'); + expect(slider.props.accessibilityValue).toEqual({min: 0, max: 10, now: 5}); +}); + +test('right and left change the value by one step while the slider has focus', () => { + const {events, focus, now} = setup(); + focus(); + press('right'); + press('right'); + release('right'); + press('left'); + release('left'); + expect(now()).toBe(6); + expect(events).toEqual([ + ['start', 5], + ['change', 6], + ['change', 7], + ['complete', 7], + ['start', 7], + ['change', 6], + ['complete', 6], + ]); +}); + +test("the app's own onFocus and onBlur still run", () => { + const onFocus = jest.fn(); + const onBlur = jest.fn(); + const {slider, focus} = setup({onFocus, onBlur} as never); + focus(); + act(() => fireEvent(slider, 'blur')); + expect(onFocus).toHaveBeenCalledTimes(1); + expect(onBlur).toHaveBeenCalledTimes(1); +}); + +test('keys do nothing while another control has focus', () => { + const {events, now} = setup(); + press('right'); + release('right'); + expect(now()).toBe(5); + expect(events).toEqual([]); +}); + +test('other keys are left to the app', () => { + const {events, focus} = setup(); + focus(); + press('up'); + press('select'); + expect(events).toEqual([]); +}); + +test('the value stays inside the range and inside lowerLimit / upperLimit', () => { + const {events, focus, now} = setup({ + value: 9, + lowerLimit: 2, + upperLimit: 9.5, + }); + focus(); + press('right'); + release('right'); + expect(now()).toBe(9); + expect(events).toEqual([ + ['start', 9], + ['complete', 9], + ]); +}); + +test('the value stays on the step grid at a lowerLimit between two steps', () => { + const {focus, now} = setup({value: 3, lowerLimit: 2.5}); + focus(); + press('left'); + expect(now()).toBe(3); +}); + +test('inverted: right moves towards the minimum', () => { + const {focus, now} = setup({inverted: true}); + focus(); + press('right'); + expect(now()).toBe(4); +}); + +test('without a step one key press moves 1 % of the range', () => { + const {focus, now} = setup({ + step: 0, + value: 0.5, + minimumValue: 0, + maximumValue: 1, + }); + focus(); + press('right'); + press('right'); + expect(now()).toBe(0.52); +}); + +test('a disabled slider takes no focus and ignores keys', () => { + const {slider, focus, events, now} = setup({disabled: true}); + expect(slider.props.focusable).toBe(false); + focus(); + press('right'); + expect(now()).toBe(5); + expect(events).toEqual([]); +}); + +test('losing focus in the middle of a key press completes the slide', () => { + const {slider, events, focus} = setup(); + focus(); + press('right'); + act(() => fireEvent(slider, 'blur')); + expect(events).toEqual([ + ['start', 5], + ['change', 6], + ['complete', 6], + ]); +}); + +test('VoiceView increment and decrement move one step', () => { + const {slider, events, now} = setup(); + act(() => + fireEvent(slider, 'accessibilityAction', { + nativeEvent: {actionName: 'increment'}, + }), + ); + expect(now()).toBe(6); + act(() => + fireEvent(slider, 'accessibilityAction', { + nativeEvent: {actionName: 'decrement'}, + }), + ); + expect(now()).toBe(5); + expect(events).toEqual([ + ['start', 5], + ['change', 6], + ['complete', 6], + ['start', 6], + ['change', 5], + ['complete', 5], + ]); +}); + +test('a new value from the app moves the slider without change events', () => { + const {screen, events, now} = setup(); + screen.rerender( + , + ); + expect(now()).toBe(8); + expect(events).toEqual([]); +}); + +test('touch: the value follows the finger on the track', () => { + const {screen, events, now} = setup(); + const track = screen.getByTestId('slider-track'); + // 220 px track with a 20 px thumb: the thumb centre runs from x = 10 to x = 210. + act(() => + fireEvent(track, 'layout', { + nativeEvent: {layout: {width: 220, height: 40}}, + }), + ); + act(() => + fireEvent(track, 'responderGrant', {nativeEvent: {locationX: 110}}), + ); + act(() => fireEvent(track, 'responderMove', {nativeEvent: {locationX: 190}})); + act(() => + fireEvent(track, 'responderRelease', {nativeEvent: {locationX: 190}}), + ); + expect(now()).toBe(9); + expect(events).toEqual([ + ['start', 5], + ['change', 9], + ['complete', 9], + ]); +}); diff --git a/package/src/RNCSliderNativeComponent.kepler.tsx b/package/src/RNCSliderNativeComponent.kepler.tsx new file mode 100644 index 00000000..f2a10a7a --- /dev/null +++ b/package/src/RNCSliderNativeComponent.kepler.tsx @@ -0,0 +1,310 @@ +import React, {useCallback, useEffect, useRef, useState} from 'react'; +import { + View, + ColorValue, + ViewStyle, + StyleProp, + GestureResponderEvent, + LayoutChangeEvent, + AccessibilityActionEvent, + Image, + ImageSourcePropType, +} from 'react-native'; +//@ts-ignore +import type {ImageSource} from 'react-native/Libraries/Image/ImageSource'; +import {constants} from './utils/constants'; + +// React Native for Vega exports useTVEventHandler; the React Native typings do not. +const {useTVEventHandler} = require('react-native') as { + useTVEventHandler: (handler: (event: RemoteEvent) => void) => void; +}; + +interface RemoteEvent { + eventType: string; + /** 0 when the key goes down, 1 when it comes up. */ + eventKeyAction?: number; +} + +type Event = Readonly<{ + nativeEvent: { + value: number; + }; +}>; + +export interface Props { + value?: number; + minimumValue?: number; + maximumValue?: number; + lowerLimit?: number; + upperLimit?: number; + step?: number; + minimumTrackTintColor?: ColorValue; + maximumTrackTintColor?: ColorValue; + thumbTintColor?: ColorValue; + thumbImage?: ImageSource; + style?: StyleProp; + inverted?: boolean; + disabled?: boolean; + testID?: string; + onRNCSliderSlidingStart?: ((event: Event) => void) | null; + onRNCSliderSlidingComplete?: ((event: Event) => void) | null; + onRNCSliderValueChange?: ((event: Event) => void) | null; + onFocus?: (event: unknown) => void; + onBlur?: (event: unknown) => void; +} + +const valueToEvent = (value: number): Event => ({nativeEvent: {value}}); + +const TRACK_HEIGHT = 4; +const THUMB_SIZE = constants.THUMB_SIZE; +/** Without a step, one key press moves this share of the range. */ +const KEY_FRACTION = 0.01; + +/** Drops floating point noise such as 0.1 + 0.2 = 0.30000000000000004. */ +const tidy = (value: number) => Number.parseFloat(value.toFixed(10)); + +/** + * The slider on Vega OS (Fire TV). Vega has no native slider view, so this one is drawn in + * React Native: a track and a thumb that take D-pad focus. While it has focus, Left and Right + * move the value by one step (1 % of the range without a step), and VoiceView reads it as an + * adjustable control. Touch works on devices that have it. + */ +const RCTSliderKeplerComponent = React.forwardRef( + ( + { + value: propValue = 0, + minimumValue = 0, + maximumValue = 1, + lowerLimit = constants.LIMIT_MIN_VALUE, + upperLimit = constants.LIMIT_MAX_VALUE, + step = 0, + minimumTrackTintColor = '#009688', + maximumTrackTintColor = '#939393', + thumbTintColor = '#009688', + thumbImage, + style, + inverted = false, + disabled = false, + testID, + onRNCSliderSlidingStart, + onRNCSliderSlidingComplete, + onRNCSliderValueChange, + onFocus, + onBlur, + ...others + }, + forwardedRef, + ) => { + const lowest = Math.max(minimumValue, lowerLimit); + const highest = Math.min(maximumValue, upperLimit); + const clamp = useCallback( + (next: number) => tidy(Math.min(highest, Math.max(lowest, next))), + [lowest, highest], + ); + + const [value, setValue] = useState(() => clamp(propValue)); + const [focused, setFocused] = useState(false); + const valueRef = useRef(value); + const focusedRef = useRef(false); + const sliding = useRef(false); + const trackWidth = useRef(0); + + // A new value from the app moves the slider without change events. + useEffect(() => { + const next = clamp(propValue); + valueRef.current = next; + setValue(next); + }, [propValue, clamp]); + + const start = () => { + if (!sliding.current) { + sliding.current = true; + onRNCSliderSlidingStart?.(valueToEvent(valueRef.current)); + } + }; + /** The nearest value on the step grid (minimumValue + k * step) inside the limits. */ + const onGrid = (next: number) => { + if (!(step > 0)) { + return clamp(next); + } + let snapped = + minimumValue + Math.round((next - minimumValue) / step) * step; + if (snapped > highest) { + snapped -= step; + } + if (snapped < lowest) { + snapped += step; + } + return clamp(snapped); + }; + const moveTo = (next: number) => { + const clamped = onGrid(next); + if (clamped !== valueRef.current) { + valueRef.current = clamped; + setValue(clamped); + onRNCSliderValueChange?.(valueToEvent(clamped)); + } + }; + const complete = () => { + if (sliding.current) { + sliding.current = false; + onRNCSliderSlidingComplete?.(valueToEvent(valueRef.current)); + } + }; + + const keyStep = + step > 0 ? step : (maximumValue - minimumValue) * KEY_FRACTION; + const stepBy = (direction: 1 | -1) => + moveTo(valueRef.current + direction * keyStep); + + useTVEventHandler((event: RemoteEvent) => { + if (disabled || !focusedRef.current) { + return; + } + if (event.eventType !== 'left' && event.eventType !== 'right') { + return; + } + if (event.eventKeyAction === 1) { + complete(); + return; + } + if (event.eventKeyAction !== undefined && event.eventKeyAction !== 0) { + return; + } + const towardsMax = (event.eventType === 'right') !== inverted; + start(); + stepBy(towardsMax ? 1 : -1); + }); + + const onAccessibilityAction = (event: AccessibilityActionEvent) => { + const direction = + event.nativeEvent.actionName === 'increment' + ? 1 + : event.nativeEvent.actionName === 'decrement' + ? -1 + : 0; + if (direction === 0 || disabled) { + return; + } + start(); + stepBy(direction); + complete(); + }; + + // Touch: the thumb centre runs from THUMB_SIZE / 2 to width - THUMB_SIZE / 2. + const valueAt = (x: number) => { + const usable = Math.max(1, trackWidth.current - THUMB_SIZE); + const share = Math.min(1, Math.max(0, (x - THUMB_SIZE / 2) / usable)); + const raw = + minimumValue + + (inverted ? 1 - share : share) * (maximumValue - minimumValue); + return step > 0 ? Math.round(raw / step) * step : raw; + }; + const onTouch = ({nativeEvent}: GestureResponderEvent) => + moveTo(valueAt(nativeEvent.locationX)); + + const range = maximumValue - minimumValue || 1; + const share = Math.min(1, Math.max(0, (value - minimumValue) / range)); + const filled = inverted ? 1 - share : share; + const thumbSize = focused ? THUMB_SIZE * 1.4 : THUMB_SIZE; + + return ( + { + focusedRef.current = true; + setFocused(true); + onFocus?.(event); + }} + onBlur={(event) => { + focusedRef.current = false; + setFocused(false); + complete(); + onBlur?.(event); + }}> + { + trackWidth.current = layout.width; + }} + onStartShouldSetResponder={() => !disabled} + onMoveShouldSetResponder={() => !disabled} + onResponderTerminationRequest={() => false} + onResponderGrant={(event) => { + start(); + onTouch(event); + }} + onResponderMove={onTouch} + onResponderRelease={(event) => { + onTouch(event); + complete(); + }}> + + + {thumbImage !== undefined ? ( + + ) : null} + + + + + ); + }, +); + +RCTSliderKeplerComponent.displayName = 'RCTSliderKeplerComponent'; + +export default RCTSliderKeplerComponent; From 1125f11ceac21cac9a5f2547a5074bdfa04958fe Mon Sep 17 00:00:00 2001 From: Yury Kalenkou Date: Sun, 18 Oct 2026 00:25:12 +0200 Subject: [PATCH 2/6] docs: Vega OS (Fire TV) section --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 4d42aa5d..e49a6b74 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,10 @@ To use this library you need to ensure you are using the correct version of Reac | [`1.x.x`](https://github.com/react-native-community/react-native-slider/tree/937f0942f1fffc6ed88b5cf7c88d73b7878f00f0) | `<= 0.59` | +### Vega OS (Fire TV) + +On Vega OS (Amazon Fire TV, React Native for Vega) the slider is drawn in React Native, as on the web: Vega has no native slider view. It takes D-pad focus, and while it has focus Left and Right move the value by one `step` (1 % of the range without a `step`), with `onSlidingStart`, `onValueChange` and `onSlidingComplete` as with a finger. A white ring on the thumb shows the focus. VoiceView reads it as an adjustable control, and its increment and decrement actions move one step. `minimumValue`, `maximumValue`, `step`, `lowerLimit`, `upperLimit`, `inverted`, `disabled`, the tint colors and `thumbImage` work; `vertical`, `tapToSeek`, the track images and `StepMarker` do not yet. Metro picks the Vega file by itself (platform `kepler`), so no setup is needed. + ## Properties | Property | Description | Type | Platform | From 39aade071895e522bb88b31ac3e7edf98fc12422 Mon Sep 17 00:00:00 2001 From: Yury Kalenkou Date: Sun, 18 Oct 2026 00:27:01 +0200 Subject: [PATCH 3/6] refactor: Vega slider styles in a StyleSheet (no inline-style warnings) --- .../src/RNCSliderNativeComponent.kepler.tsx | 76 +++++++++++-------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/package/src/RNCSliderNativeComponent.kepler.tsx b/package/src/RNCSliderNativeComponent.kepler.tsx index f2a10a7a..952d8efe 100644 --- a/package/src/RNCSliderNativeComponent.kepler.tsx +++ b/package/src/RNCSliderNativeComponent.kepler.tsx @@ -1,5 +1,6 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; import { + StyleSheet, View, ColorValue, ViewStyle, @@ -213,7 +214,7 @@ const RCTSliderKeplerComponent = React.forwardRef( ref={forwardedRef} {...others} testID={testID} - style={[{justifyContent: 'center', minHeight: THUMB_SIZE * 2}, style]} + style={[styles.root, style]} focusable={!disabled} accessible={true} accessibilityRole="adjustable" @@ -237,11 +238,7 @@ const RCTSliderKeplerComponent = React.forwardRef( }}> { trackWidth.current = layout.width; }} @@ -259,45 +256,48 @@ const RCTSliderKeplerComponent = React.forwardRef( }}> + focused && styles.focusRing, + disabled && styles.disabled, + ]}> {thumbImage !== undefined ? ( ) : null} @@ -305,6 +305,16 @@ const RCTSliderKeplerComponent = React.forwardRef( }, ); +const styles = StyleSheet.create({ + root: {justifyContent: 'center', minHeight: THUMB_SIZE * 2}, + track: {flexDirection: 'row', alignItems: 'center', height: THUMB_SIZE * 2}, + line: {height: TRACK_HEIGHT, borderRadius: TRACK_HEIGHT / 2}, + thumb: {overflow: 'hidden'}, + focusRing: {borderWidth: 3, borderColor: '#ffffff'}, + disabled: {opacity: 0.5}, + thumbImage: {width: '100%', height: '100%'}, +}); + RCTSliderKeplerComponent.displayName = 'RCTSliderKeplerComponent'; export default RCTSliderKeplerComponent; From 3bee697b0eeea29dd9f9be27229b30e680633071 Mon Sep 17 00:00:00 2001 From: Yury Kalenkou Date: Sun, 18 Oct 2026 00:56:22 +0200 Subject: [PATCH 4/6] fix: Vega touch snaps to the step grid counted from minimumValue --- .../RNCSliderNativeComponent.kepler.test.tsx | 21 +++++++++++++++++++ .../src/RNCSliderNativeComponent.kepler.tsx | 6 +++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/package/__test__/RNCSliderNativeComponent.kepler.test.tsx b/package/__test__/RNCSliderNativeComponent.kepler.test.tsx index 11d3fd0a..bfc41cf9 100644 --- a/package/__test__/RNCSliderNativeComponent.kepler.test.tsx +++ b/package/__test__/RNCSliderNativeComponent.kepler.test.tsx @@ -240,3 +240,24 @@ test('touch: the value follows the finger on the track', () => { ['complete', 9], ]); }); + +test('touch: the value snaps to the nearest step counted from the minimum', () => { + // Steps 1 and 3 (minimumValue 1, step 2). x = 70 is 1.9: nearer to 1 than to 3. + const {screen, now} = setup({ + value: 3, + minimumValue: 1, + maximumValue: 4, + step: 2, + }); + const track = screen.getByTestId('slider-track'); + act(() => + fireEvent(track, 'layout', { + nativeEvent: {layout: {width: 220, height: 40}}, + }), + ); + act(() => fireEvent(track, 'responderGrant', {nativeEvent: {locationX: 70}})); + act(() => + fireEvent(track, 'responderRelease', {nativeEvent: {locationX: 70}}), + ); + expect(now()).toBe(1); +}); diff --git a/package/src/RNCSliderNativeComponent.kepler.tsx b/package/src/RNCSliderNativeComponent.kepler.tsx index 952d8efe..6c7a1227 100644 --- a/package/src/RNCSliderNativeComponent.kepler.tsx +++ b/package/src/RNCSliderNativeComponent.kepler.tsx @@ -196,10 +196,10 @@ const RCTSliderKeplerComponent = React.forwardRef( const valueAt = (x: number) => { const usable = Math.max(1, trackWidth.current - THUMB_SIZE); const share = Math.min(1, Math.max(0, (x - THUMB_SIZE / 2) / usable)); - const raw = + return ( minimumValue + - (inverted ? 1 - share : share) * (maximumValue - minimumValue); - return step > 0 ? Math.round(raw / step) * step : raw; + (inverted ? 1 - share : share) * (maximumValue - minimumValue) + ); }; const onTouch = ({nativeEvent}: GestureResponderEvent) => moveTo(valueAt(nativeEvent.locationX)); From 35d3cafa7ba24a28d6cfb22975c74241fa40a872 Mon Sep 17 00:00:00 2001 From: Yury Kalenkou Date: Sun, 18 Oct 2026 18:46:50 +0200 Subject: [PATCH 5/6] chore: shorter comments in the Vega slider --- .../__test__/RNCSliderNativeComponent.kepler.test.tsx | 4 ++-- package/src/RNCSliderNativeComponent.kepler.tsx | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/package/__test__/RNCSliderNativeComponent.kepler.test.tsx b/package/__test__/RNCSliderNativeComponent.kepler.test.tsx index bfc41cf9..e3de6efc 100644 --- a/package/__test__/RNCSliderNativeComponent.kepler.test.tsx +++ b/package/__test__/RNCSliderNativeComponent.kepler.test.tsx @@ -1,8 +1,8 @@ import React from 'react'; import {act, fireEvent, render} from '@testing-library/react-native'; -// Vega OS sends the remote's keys through useTVEventHandler. React Native core has no such -// hook, so the tests keep the handler the component registers and press keys through it. +// React Native core has no useTVEventHandler: the mock keeps the component's handler, +// and the tests press remote keys through it. let mockKeyHandler: | ((event: {eventType: string; eventKeyAction?: number}) => void) | null = null; diff --git a/package/src/RNCSliderNativeComponent.kepler.tsx b/package/src/RNCSliderNativeComponent.kepler.tsx index 6c7a1227..147f075f 100644 --- a/package/src/RNCSliderNativeComponent.kepler.tsx +++ b/package/src/RNCSliderNativeComponent.kepler.tsx @@ -15,7 +15,7 @@ import { import type {ImageSource} from 'react-native/Libraries/Image/ImageSource'; import {constants} from './utils/constants'; -// React Native for Vega exports useTVEventHandler; the React Native typings do not. +// Vega has useTVEventHandler, but the React Native typings do not. const {useTVEventHandler} = require('react-native') as { useTVEventHandler: (handler: (event: RemoteEvent) => void) => void; }; @@ -65,10 +65,8 @@ const KEY_FRACTION = 0.01; const tidy = (value: number) => Number.parseFloat(value.toFixed(10)); /** - * The slider on Vega OS (Fire TV). Vega has no native slider view, so this one is drawn in - * React Native: a track and a thumb that take D-pad focus. While it has focus, Left and Right - * move the value by one step (1 % of the range without a step), and VoiceView reads it as an - * adjustable control. Touch works on devices that have it. + * Slider for Vega OS (Fire TV), drawn in JS because Vega has no native slider view. + * With D-pad focus, Left/Right move one step (1 % of the range without a step). */ const RCTSliderKeplerComponent = React.forwardRef( ( @@ -123,7 +121,7 @@ const RCTSliderKeplerComponent = React.forwardRef( onRNCSliderSlidingStart?.(valueToEvent(valueRef.current)); } }; - /** The nearest value on the step grid (minimumValue + k * step) inside the limits. */ + /** Nearest step value (minimumValue + k * step) within the limits. */ const onGrid = (next: number) => { if (!(step > 0)) { return clamp(next); From 7dc7340c389c0d8b5da64a8dc286a4093b4be2e4 Mon Sep 17 00:00:00 2001 From: Yury Kalenkou Date: Mon, 19 Oct 2026 22:02:44 +0200 Subject: [PATCH 6/6] fix: VoiceView reads the Vega slider value as 53, not 53.0 --- .../RNCSliderNativeComponent.kepler.test.tsx | 12 +++++++++++- package/src/RNCSliderNativeComponent.kepler.tsx | 8 +++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/package/__test__/RNCSliderNativeComponent.kepler.test.tsx b/package/__test__/RNCSliderNativeComponent.kepler.test.tsx index e3de6efc..153d9535 100644 --- a/package/__test__/RNCSliderNativeComponent.kepler.test.tsx +++ b/package/__test__/RNCSliderNativeComponent.kepler.test.tsx @@ -65,7 +65,17 @@ beforeEach(() => { test('is an adjustable control with its range and value for VoiceView', () => { const {slider} = setup(); expect(slider.props.accessibilityRole).toBe('adjustable'); - expect(slider.props.accessibilityValue).toEqual({min: 0, max: 10, now: 5}); + expect(slider.props.accessibilityValue).toEqual({ + min: 0, + max: 10, + now: 5, + text: '5', + }); +}); + +test('gives VoiceView the value as text, so 53 is not read as "53.0"', () => { + const {slider} = setup({value: 0.1 + 0.2, step: 0.1, maximumValue: 1}); + expect(slider.props.accessibilityValue.text).toBe('0.3'); }); test('right and left change the value by one step while the slider has focus', () => { diff --git a/package/src/RNCSliderNativeComponent.kepler.tsx b/package/src/RNCSliderNativeComponent.kepler.tsx index 147f075f..2090e745 100644 --- a/package/src/RNCSliderNativeComponent.kepler.tsx +++ b/package/src/RNCSliderNativeComponent.kepler.tsx @@ -217,7 +217,13 @@ const RCTSliderKeplerComponent = React.forwardRef( accessible={true} accessibilityRole="adjustable" accessibilityState={{disabled}} - accessibilityValue={{min: minimumValue, max: maximumValue, now: value}} + accessibilityValue={{ + min: minimumValue, + max: maximumValue, + now: value, + // VoiceView reads `now` as "53.0". The text makes it "53". + text: String(value), + }} accessibilityActions={[ {name: 'increment', label: 'increment'}, {name: 'decrement', label: 'decrement'},