Skip to content

Commit 3d8d23d

Browse files
Material Design Teamdsn5ft
authored andcommitted
[Slider][A11y] Use arrow keys for value adjustment and TAB for navigation
Refactors Slider/RangeSlider keyboard navigation for a more intuitive and accessible experience, removing the need to press ENTER to toggle between navigation and value adjustment modes. With this change: - Arrow keys (+/-/=/DPAD) always adjust the value of the focused thumb. - TAB and Shift+TAB always navigate focus between thumbs. This simplifies event handling by ensuring `activeThumbIdx` stays synchronized with `focusedThumbIdx` during keyboard input. Also fixes regressions in `SliderTouchTest` by preventing `onFocusChanged` logic (now conditioned on `activeThumbIdx == -1`) from incorrectly overriding thumb selection during touch events. PiperOrigin-RevId: 816168251
1 parent 5c0e8dd commit 3d8d23d

File tree

4 files changed

+21
-218
lines changed

4 files changed

+21
-218
lines changed

lib/java/com/google/android/material/slider/BaseSlider.java

Lines changed: 17 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3788,16 +3788,7 @@ public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
37883788
return super.onKeyDown(keyCode, event);
37893789
}
37903790

3791-
// If there's only one thumb, we can select it right away.
3792-
if (values.size() == 1) {
3793-
activeThumbIdx = 0;
3794-
}
3795-
3796-
// If there is no active thumb, key events will be used to pick the thumb to change.
3797-
if (activeThumbIdx == -1) {
3798-
Boolean handled = onKeyDownNoActiveThumb(keyCode, event);
3799-
return handled != null ? handled : super.onKeyDown(keyCode, event);
3800-
}
3791+
activeThumbIdx = focusedThumbIdx;
38013792

38023793
isLongPress |= event.isLongPress();
38033794
Float increment = calculateIncrementForKey(keyCode);
@@ -3808,64 +3799,19 @@ public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
38083799
}
38093800
return true;
38103801
}
3811-
switch (keyCode) {
3812-
case KeyEvent.KEYCODE_TAB:
3813-
if (event.hasNoModifiers()) {
3814-
return moveFocus(1);
3815-
}
3816-
3817-
if (event.isShiftPressed()) {
3818-
return moveFocus(-1);
3819-
}
3820-
return false;
3821-
case KeyEvent.KEYCODE_DPAD_CENTER:
3822-
case KeyEvent.KEYCODE_ENTER:
3823-
activeThumbIdx = -1;
3824-
postInvalidate();
3825-
return true;
3826-
default:
3827-
// Nothing to do in this case.
3828-
}
3829-
3830-
return super.onKeyDown(keyCode, event);
3831-
}
38323802

3833-
@Nullable
3834-
private Boolean onKeyDownNoActiveThumb(int keyCode, @NonNull KeyEvent event) {
3835-
switch (keyCode) {
3836-
case KeyEvent.KEYCODE_TAB:
3837-
if (event.hasNoModifiers()) {
3838-
return moveFocus(1);
3839-
}
3803+
if (keyCode == KeyEvent.KEYCODE_TAB) {
3804+
if (event.hasNoModifiers()) {
3805+
return moveFocus(1);
3806+
}
38403807

3841-
if (event.isShiftPressed()) {
3842-
return moveFocus(-1);
3843-
}
3844-
return false;
3845-
case KeyEvent.KEYCODE_DPAD_LEFT:
3846-
moveFocusInAbsoluteDirection(-1);
3847-
return true;
3848-
case KeyEvent.KEYCODE_MINUS:
3849-
moveFocus(-1);
3850-
return true;
3851-
case KeyEvent.KEYCODE_DPAD_RIGHT:
3852-
moveFocusInAbsoluteDirection(1);
3853-
return true;
3854-
case KeyEvent.KEYCODE_EQUALS:
3855-
// Numpad Plus == Shift + Equals, at least in AVD, so fall through.
3856-
case KeyEvent.KEYCODE_PLUS:
3857-
moveFocus(1);
3858-
return true;
3859-
case KeyEvent.KEYCODE_DPAD_CENTER:
3860-
case KeyEvent.KEYCODE_ENTER:
3861-
activeThumbIdx = focusedThumbIdx;
3862-
postInvalidate();
3863-
return true;
3864-
default:
3865-
// Nothing to do in this case.
3808+
if (event.isShiftPressed()) {
3809+
return moveFocus(-1);
3810+
}
3811+
return false;
38663812
}
38673813

3868-
return null;
3814+
return super.onKeyDown(keyCode, event);
38693815
}
38703816

38713817
@Override
@@ -3903,9 +3849,7 @@ private boolean moveFocus(int direction) {
39033849
// Move focus to next or previous view.
39043850
return false;
39053851
}
3906-
if (activeThumbIdx != -1) {
3907-
activeThumbIdx = focusedThumbIdx;
3908-
}
3852+
activeThumbIdx = focusedThumbIdx;
39093853
updateHaloHotspot();
39103854
postInvalidate();
39113855
return true;
@@ -3978,7 +3922,12 @@ protected void onFocusChanged(
39783922
activeThumbIdx = -1;
39793923
accessibilityHelper.clearKeyboardFocusForVirtualView(focusedThumbIdx);
39803924
} else {
3981-
focusThumbOnFocusGained(direction);
3925+
// If activeThumbIdx != -1, a touch gesture is in progress and has already
3926+
// picked the thumb to focus. Don't interfere.
3927+
if (activeThumbIdx == -1) {
3928+
focusThumbOnFocusGained(direction);
3929+
activeThumbIdx = focusedThumbIdx;
3930+
}
39823931
accessibilityHelper.requestKeyboardFocusForVirtualView(focusedThumbIdx);
39833932
}
39843933
}

lib/javatests/com/google/android/material/slider/SliderKeyTestCommon.java

Lines changed: 0 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import com.google.android.material.test.R;
2020

21-
import static com.google.android.material.slider.SliderHelper.clickDpadCenter;
2221
import static com.google.common.truth.Truth.assertThat;
2322
import static com.google.common.truth.Truth.assertWithMessage;
2423

@@ -127,61 +126,6 @@ public void testMoveThumbFocus_tab_correctThumbHasFocus() {
127126
sendKeyEventThereAndBack(tab, shiftTab);
128127
}
129128

130-
@Test
131-
public void testMoveThumbFocus_plusMinus_correctThumbHasFocus() {
132-
slider.requestFocus();
133-
134-
KeyEventBuilder plus = new KeyEventBuilder(KeyEvent.KEYCODE_PLUS);
135-
KeyEventBuilder minus = new KeyEventBuilder(KeyEvent.KEYCODE_MINUS);
136-
137-
sendKeyEventThereAndBack(plus, minus);
138-
}
139-
140-
@Test
141-
public void testMoveThumbFocus_equalsMinus_correctThumbHasFocus() {
142-
slider.requestFocus();
143-
144-
// Numpad Plus == Shift + Equals, at least in AVD.
145-
KeyEventBuilder equals = new KeyEventBuilder(KeyEvent.KEYCODE_EQUALS);
146-
KeyEventBuilder minus = new KeyEventBuilder(KeyEvent.KEYCODE_MINUS);
147-
148-
sendKeyEventThereAndBack(equals, minus);
149-
}
150-
151-
@Test
152-
public void testFocusThirdThumb_clickCenterDPad_activatesThirdThumb() {
153-
slider.requestFocus();
154-
155-
slider.setFocusedThumbIndex(2);
156-
157-
clickDpadCenter(slider);
158-
159-
assertThat(slider.getActiveThumbIndex()).isEqualTo(2);
160-
}
161-
162-
protected void activateFocusedThumb() {
163-
int focusedThumbIndex = slider.getFocusedThumbIndex();
164-
if (focusedThumbIndex != -1) {
165-
// Clicking D-Pad in Slider isn't idempotent. Only do it here if we're changing focused thumb.
166-
if (focusedThumbIndex != slider.getActiveThumbIndex()) {
167-
clickDpadCenter(slider);
168-
}
169-
}
170-
}
171-
172-
@Test
173-
public void testActivateThirdThumb_clickCenterDPad_deactivatesThirdThumb() {
174-
slider.requestFocus();
175-
176-
slider.setFocusedThumbIndex(2);
177-
178-
activateFocusedThumb();
179-
180-
clickDpadCenter(slider);
181-
182-
assertThat(slider.getActiveThumbIndex()).isEqualTo(-1);
183-
}
184-
185129
@Test
186130
public void testFocusDefaultThumb_clickUpDPad_unhandled() {
187131
slider.requestFocus();
@@ -194,20 +138,6 @@ public void testFocusDefaultThumb_clickUpDPad_unhandled() {
194138
assertThat(handledUp).isFalse();
195139
}
196140

197-
@Test
198-
public void testActivateDefaultThumb_clickUpDPad_unhandled() {
199-
slider.requestFocus();
200-
201-
activateFocusedThumb();
202-
203-
KeyEventBuilder up = new KeyEventBuilder(KeyEvent.KEYCODE_DPAD_UP);
204-
boolean handledDown = slider.dispatchKeyEvent(up.buildDown());
205-
boolean handledUp = slider.dispatchKeyEvent(up.buildUp());
206-
207-
assertThat(handledDown).isFalse();
208-
assertThat(handledUp).isFalse();
209-
}
210-
211141
@Test
212142
public void testFocusDefaultThumb_clickDownDPad_unhandled() {
213143
slider.requestFocus();
@@ -220,20 +150,6 @@ public void testFocusDefaultThumb_clickDownDPad_unhandled() {
220150
assertThat(handledUp).isFalse();
221151
}
222152

223-
@Test
224-
public void testActivateDefaultThumb_clickDownDPad_unhandled() {
225-
slider.requestFocus();
226-
227-
activateFocusedThumb();
228-
229-
KeyEventBuilder down = new KeyEventBuilder(KeyEvent.KEYCODE_DPAD_DOWN);
230-
boolean handledDown = slider.dispatchKeyEvent(down.buildDown());
231-
boolean handledUp = slider.dispatchKeyEvent(down.buildUp());
232-
233-
assertThat(handledDown).isFalse();
234-
assertThat(handledUp).isFalse();
235-
}
236-
237153
@Test
238154
public void testFocusFirstThumb_shiftTab_unhandled() {
239155
slider.requestFocus();
@@ -249,23 +165,6 @@ public void testFocusFirstThumb_shiftTab_unhandled() {
249165
assertThat(handledUp).isFalse();
250166
}
251167

252-
@Test
253-
public void testActivateFirstThumb_shiftTab_unhandled() {
254-
slider.requestFocus();
255-
256-
slider.setFocusedThumbIndex(0);
257-
258-
activateFocusedThumb();
259-
260-
KeyEventBuilder tab = new KeyEventBuilder(KeyEvent.KEYCODE_TAB);
261-
tab.meta = KeyEvent.META_SHIFT_ON;
262-
boolean handledDown = slider.dispatchKeyEvent(tab.buildDown());
263-
boolean handledUp = slider.dispatchKeyEvent(tab.buildUp());
264-
265-
assertThat(handledDown).isFalse();
266-
assertThat(handledUp).isFalse();
267-
}
268-
269168
@Test
270169
public void testFocusLastThumb_tab_unhandled() {
271170
slider.requestFocus();
@@ -280,22 +179,6 @@ public void testFocusLastThumb_tab_unhandled() {
280179
assertThat(handledUp).isFalse();
281180
}
282181

283-
@Test
284-
public void testActivateLastThumb_tab_unhandled() {
285-
slider.requestFocus();
286-
287-
slider.setFocusedThumbIndex(countTestValues() - 1);
288-
289-
activateFocusedThumb();
290-
291-
KeyEventBuilder tab = new KeyEventBuilder(KeyEvent.KEYCODE_TAB);
292-
boolean handledDown = slider.dispatchKeyEvent(tab.buildDown());
293-
boolean handledUp = slider.dispatchKeyEvent(tab.buildUp());
294-
295-
assertThat(handledDown).isFalse();
296-
assertThat(handledUp).isFalse();
297-
}
298-
299182
protected void sendKeyEventThereAndBack(KeyEventBuilder there, KeyEventBuilder back) {
300183
for (int i = 1; i < countTestValues(); i++) {
301184
there.dispatchEvent(slider);

lib/javatests/com/google/android/material/slider/SliderKeyTestLtr.java

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,11 @@ public void testThumbFocus_focusRight_focusesFirstThumb() {
4646
}
4747

4848
@Test
49-
public void testMoveThumbFocus_dPad_correctThumbHasFocus() {
50-
slider.requestFocus();
51-
52-
KeyEventBuilder right = new KeyEventBuilder(KeyEvent.KEYCODE_DPAD_RIGHT);
53-
KeyEventBuilder left = new KeyEventBuilder(KeyEvent.KEYCODE_DPAD_LEFT);
54-
55-
sendKeyEventThereAndBack(right, left);
56-
}
57-
58-
@Test
59-
public void testActivateThirdThumb_moveRight_movesThirdThumbRight() {
49+
public void testFocusThirdThumb_moveRight_movesThirdThumbRight() {
6050
slider.requestFocus();
6151

6252
slider.setFocusedThumbIndex(2);
6353

64-
activateFocusedThumb();
65-
6654
KeyEventBuilder right = new KeyEventBuilder(KeyEvent.KEYCODE_DPAD_RIGHT);
6755
for (int i = 0; i < 20; i++) {
6856
right.dispatchEvent(slider);
@@ -72,13 +60,11 @@ public void testActivateThirdThumb_moveRight_movesThirdThumbRight() {
7260
}
7361

7462
@Test
75-
public void testActivateThirdThumb_moveLeft_movesThirdThumbLeft() {
63+
public void testFocusThirdThumb_moveLeft_movesThirdThumbLeft() {
7664
slider.requestFocus();
7765

7866
slider.setFocusedThumbIndex(2);
7967

80-
activateFocusedThumb();
81-
8268
KeyEventBuilder left = new KeyEventBuilder(KeyEvent.KEYCODE_DPAD_LEFT);
8369
for (int i = 0; i < 3; i++) {
8470
left.dispatchEvent(slider);

lib/javatests/com/google/android/material/slider/SliderKeyTestRtl.java

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -64,24 +64,11 @@ public void testThumbFocus_focusRight_focusesLastThumb() {
6464
}
6565

6666
@Test
67-
public void testMoveThumbFocus_dPad_correctThumbHasFocus() {
68-
slider.requestFocus();
69-
70-
KeyEventBuilder left = new KeyEventBuilder(KeyEvent.KEYCODE_DPAD_LEFT);
71-
KeyEventBuilder right = new KeyEventBuilder(KeyEvent.KEYCODE_DPAD_RIGHT);
72-
73-
// We start at far right in RTL so go left first, then go back right.
74-
sendKeyEventThereAndBack(left, right);
75-
}
76-
77-
@Test
78-
public void testActivateThirdThumb_moveRight_movesThirdThumbRight() {
67+
public void testFocusThirdThumb_moveRight_movesThirdThumbRight() {
7968
slider.requestFocus();
8069

8170
slider.setFocusedThumbIndex(2);
8271

83-
activateFocusedThumb();
84-
8572
KeyEventBuilder right = new KeyEventBuilder(KeyEvent.KEYCODE_DPAD_RIGHT);
8673
for (int i = 0; i < 3; i++) {
8774
right.dispatchEvent(slider);
@@ -92,12 +79,10 @@ public void testActivateThirdThumb_moveRight_movesThirdThumbRight() {
9279
}
9380

9481
@Test
95-
public void testActivateThirdThumb_moveLeft_movesThirdThumbLeft() {
82+
public void testFocusThirdThumb_moveLeft_movesThirdThumbLeft() {
9683
slider.requestFocus();
9784
slider.setFocusedThumbIndex(2);
9885

99-
activateFocusedThumb();
100-
10186
KeyEventBuilder left = new KeyEventBuilder(KeyEvent.KEYCODE_DPAD_LEFT);
10287
for (int i = 0; i < 20; i++) {
10388
left.dispatchEvent(slider);

0 commit comments

Comments
 (0)