Skip to content
This repository was archived by the owner on Aug 1, 2024. It is now read-only.

Commit c6c579a

Browse files
committed
Accessibility improvements for thumb slider follows instructions given:
https://www.w3.org/TR/wai-aria-practices/#slider https://www.w3.org/TR/wai-aria-practices/#slidertwothumb Sets the ARIA roles, states, properties on the thumbs. RELNOTES: For slider and two thumbs slider: TAB focus is set on the thumbs instead of the highlighted slider handle. For two thumbs slider: Key events (HOME, END, LEFT arrow, RIGHT arrow...) interacts on the focused thumb (the start thumb or the end thumb) other than the highlighted slider handle. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=125745734
1 parent fd05d94 commit c6c579a

File tree

3 files changed

+415
-145
lines changed

3 files changed

+415
-145
lines changed

closure/goog/deps.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

closure/goog/ui/sliderbase.js

+186-34
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,6 @@ goog.ui.SliderBase.prototype.enterDocument = function() {
436436
this.keyHandler_ = new goog.events.KeyHandler(this.getElement());
437437
this.enableEventHandlers_(true);
438438

439-
this.getElement().tabIndex = 0;
440439
this.updateUi_();
441440
};
442441

@@ -587,45 +586,49 @@ goog.ui.SliderBase.prototype.handleThumbDragStartEnd_ = function(e) {
587586
*/
588587
goog.ui.SliderBase.prototype.handleKeyDown_ = function(e) {
589588
var handled = true;
589+
var thumb;
590+
if (e.target == this.valueThumb || e.target == this.extentThumb) {
591+
thumb = goog.asserts.assertElement(e.target);
592+
}
593+
var delta;
590594
switch (e.keyCode) {
591595
case goog.events.KeyCodes.HOME:
592-
this.animatedSetValue(this.getMinimum());
596+
this.animateThumbToStart_(thumb);
593597
break;
594598
case goog.events.KeyCodes.END:
595-
this.animatedSetValue(this.getMaximum());
599+
this.animateThumbToEnd_(thumb);
596600
break;
597601
case goog.events.KeyCodes.PAGE_UP:
598-
this.moveThumbs(this.getBlockIncrement());
602+
delta = this.getBlockIncrement();
599603
break;
600604
case goog.events.KeyCodes.PAGE_DOWN:
601-
this.moveThumbs(-this.getBlockIncrement());
605+
delta = -this.getBlockIncrement();
602606
break;
603607
case goog.events.KeyCodes.LEFT:
604608
var sign = this.flipForRtl_ && this.isRightToLeft() ? 1 : -1;
605-
this.moveThumbs(
606-
e.shiftKey ? sign * this.getBlockIncrement() :
607-
sign * this.getUnitIncrement());
609+
delta = e.shiftKey ? sign * this.getBlockIncrement() :
610+
sign * this.getUnitIncrement();
608611
break;
609612
case goog.events.KeyCodes.DOWN:
610-
this.moveThumbs(
611-
e.shiftKey ? -this.getBlockIncrement() : -this.getUnitIncrement());
613+
delta = e.shiftKey ? -this.getBlockIncrement() : -this.getUnitIncrement();
612614
break;
613615
case goog.events.KeyCodes.RIGHT:
614616
var sign = this.flipForRtl_ && this.isRightToLeft() ? -1 : 1;
615-
this.moveThumbs(
616-
e.shiftKey ? sign * this.getBlockIncrement() :
617-
sign * this.getUnitIncrement());
617+
delta = e.shiftKey ? sign * this.getBlockIncrement() :
618+
sign * this.getUnitIncrement();
618619
break;
619620
case goog.events.KeyCodes.UP:
620-
this.moveThumbs(
621-
e.shiftKey ? this.getBlockIncrement() : this.getUnitIncrement());
621+
delta = e.shiftKey ? this.getBlockIncrement() : this.getUnitIncrement();
622622
break;
623623

624624
default:
625625
handled = false;
626626
}
627627

628628
if (handled) {
629+
if (delta) {
630+
thumb ? this.moveThumb_(delta, thumb) : this.moveThumbs(delta);
631+
}
629632
e.preventDefault();
630633
}
631634
};
@@ -851,6 +854,54 @@ goog.ui.SliderBase.prototype.getThumbPosition_ = function(thumb) {
851854
};
852855

853856

857+
/**
858+
* Animates the given thumb (or the handle if thumb is not specified) towards
859+
* the minimum value position.
860+
* @param {!Element=} opt_thumb
861+
* @private
862+
*/
863+
goog.ui.SliderBase.prototype.animateThumbToStart_ = function(opt_thumb) {
864+
if (opt_thumb) {
865+
if (opt_thumb == this.valueThumb) {
866+
if (this.getValue() != this.getMinimum()) {
867+
this.animatedSetValue(this.getMinimum(), this.valueThumb);
868+
}
869+
} else if (opt_thumb == this.extentThumb) {
870+
var newExtentValue = this.getValue() + this.minExtent_;
871+
if (this.getValue() + this.getExtent() != newExtentValue) {
872+
this.animatedSetValue(newExtentValue, this.extentThumb);
873+
}
874+
}
875+
} else {
876+
this.animatedSetValue(this.getMinimum());
877+
}
878+
};
879+
880+
881+
/**
882+
* Animates the given thumb (or the handle if thumb is not specified) towards
883+
* the maximum value position.
884+
* @param {!Element=} opt_thumb
885+
* @private
886+
*/
887+
goog.ui.SliderBase.prototype.animateThumbToEnd_ = function(opt_thumb) {
888+
if (opt_thumb) {
889+
if (opt_thumb == this.extentThumb) {
890+
if (this.getValue() + this.getExtent() != this.getMaximum()) {
891+
this.animatedSetValue(this.getMaximum(), this.extentThumb);
892+
}
893+
} else if (opt_thumb == this.valueThumb) {
894+
var newValue = this.getValue() + this.getExtent() - this.minExtent_;
895+
if (this.getValue() != newValue) {
896+
this.animatedSetValue(newValue, this.valueThumb);
897+
}
898+
}
899+
} else {
900+
this.animatedSetValue(this.getMaximum());
901+
}
902+
};
903+
904+
854905
/**
855906
* Returns whether a thumb is currently being dragged with the mouse (or via
856907
* touch). Note that changing the value with keyboard, mouswheel, or via
@@ -890,6 +941,44 @@ goog.ui.SliderBase.prototype.moveThumbs = function(delta) {
890941
};
891942

892943

944+
/**
945+
* Moves the given thumb by the the specified delta as follows:
946+
* - The thumb is moved as long as it stays within its [min,max].
947+
* - Once it reaches or exceeds min (or max), it stays at min (or max).
948+
* In case the thumb has reached min (or max), no change event will fire.
949+
* If the specified delta is smaller than the step size, it will be rounded
950+
* to the step size.
951+
* @param {number} delta The delta by which to move the selected range.
952+
* @param {!Element} thumb The thumb to move if given.
953+
* @private
954+
*/
955+
goog.ui.SliderBase.prototype.moveThumb_ = function(delta, thumb) {
956+
// Assume that a small delta is supposed to be at least a step.
957+
if (Math.abs(delta) < this.getStep()) {
958+
delta = goog.math.sign(delta) * this.getStep();
959+
}
960+
var currentMinPos = this.getValue();
961+
var currentMaxPos = this.getValue() + this.getExtent();
962+
if (thumb == this.valueThumb) {
963+
var newMinPos = this.getThumbPosition_(this.valueThumb) + delta;
964+
newMinPos = goog.math.clamp(
965+
newMinPos, this.getMinimum(), this.getMaximum() - this.minExtent_);
966+
// Change the extent value to keep the present extentThumb position while
967+
// move the valueThumb to newMinPos.
968+
var newExtent = currentMaxPos - newMinPos;
969+
this.setValueAndExtent(newMinPos, newExtent >= 0 ? newExtent : 0);
970+
} else if (thumb == this.extentThumb) {
971+
var newMaxPos = this.getThumbPosition_(this.extentThumb) + delta;
972+
newMaxPos = goog.math.clamp(
973+
newMaxPos, this.getMinimum() + this.minExtent_, this.getMaximum());
974+
// Change the extent value to keep the present valueThumb position while
975+
// move the extentThumb to newMaxPos.
976+
var newExtent = newMaxPos - currentMinPos;
977+
this.setValueAndExtent(currentMinPos, newExtent >= 0 ? newExtent : 0);
978+
}
979+
};
980+
981+
893982
/**
894983
* Sets the position of the given thumb. The set is ignored and no CHANGE event
895984
* fires if it violates the constraint minimum <= value (valueThumb position) <=
@@ -1119,8 +1208,9 @@ goog.ui.SliderBase.prototype.getThumbCoordinateForValue = function(val) {
11191208
/**
11201209
* Sets the value and starts animating the handle towards that position.
11211210
* @param {number} v Value to set and animate to.
1211+
* @param {!HTMLDivElement=} opt_thumb The thumb to move if given.
11221212
*/
1123-
goog.ui.SliderBase.prototype.animatedSetValue = function(v) {
1213+
goog.ui.SliderBase.prototype.animatedSetValue = function(v, opt_thumb) {
11241214
// the value might be out of bounds
11251215
v = goog.math.clamp(v, this.getMinimum(), this.getMaximum());
11261216

@@ -1131,7 +1221,7 @@ goog.ui.SliderBase.prototype.animatedSetValue = function(v) {
11311221
var animations = new goog.fx.AnimationParallelQueue();
11321222
var end;
11331223

1134-
var thumb = this.getClosestThumb_(v);
1224+
var thumb = opt_thumb || this.getClosestThumb_(v);
11351225
var previousValue = this.getValue();
11361226
var previousExtent = this.getExtent();
11371227
var previousThumbValue = this.getThumbPosition_(thumb);
@@ -1519,10 +1609,17 @@ goog.ui.SliderBase.prototype.setVisible = function(visible) {
15191609
* @protected
15201610
*/
15211611
goog.ui.SliderBase.prototype.setAriaRoles = function() {
1522-
var el = this.getElement();
1523-
goog.asserts.assert(
1524-
el, 'The DOM element for the slider base cannot be null.');
1525-
goog.a11y.aria.setRole(el, goog.a11y.aria.Role.SLIDER);
1612+
if (!this.valueThumb) {
1613+
return;
1614+
}
1615+
1616+
var valueThumb = goog.asserts.assertElement(this.valueThumb);
1617+
goog.a11y.aria.setRole(valueThumb, goog.a11y.aria.Role.SLIDER);
1618+
valueThumb.tabIndex = 0;
1619+
var extentThumb = goog.asserts.assertElement(this.extentThumb);
1620+
goog.a11y.aria.setRole(extentThumb, goog.a11y.aria.Role.SLIDER);
1621+
extentThumb.tabIndex = 0;
1622+
15261623
this.updateAriaStates();
15271624
};
15281625

@@ -1532,18 +1629,48 @@ goog.ui.SliderBase.prototype.setAriaRoles = function() {
15321629
* @protected
15331630
*/
15341631
goog.ui.SliderBase.prototype.updateAriaStates = function() {
1535-
var element = this.getElement();
1536-
if (element) {
1537-
goog.a11y.aria.setState(
1538-
element, goog.a11y.aria.State.VALUEMIN, this.getMinimum());
1539-
goog.a11y.aria.setState(
1540-
element, goog.a11y.aria.State.VALUEMAX, this.getMaximum());
1541-
goog.a11y.aria.setState(
1542-
element, goog.a11y.aria.State.VALUENOW, this.getValue());
1543-
// Passing an empty value to setState will restore the default.
1544-
goog.a11y.aria.setState(
1545-
element, goog.a11y.aria.State.VALUETEXT, this.getTextValue() || '');
1632+
if (!this.valueThumb) {
1633+
return;
15461634
}
1635+
1636+
var valueThumb = goog.asserts.assertElement(this.valueThumb);
1637+
var extentThumb = goog.asserts.assertElement(this.extentThumb);
1638+
1639+
goog.a11y.aria.setState(
1640+
extentThumb, goog.a11y.aria.State.VALUEMIN,
1641+
this.getValue() + this.minExtent_);
1642+
goog.a11y.aria.setState(
1643+
valueThumb, goog.a11y.aria.State.VALUEMIN, this.getMinimum());
1644+
1645+
goog.a11y.aria.setState(
1646+
extentThumb, goog.a11y.aria.State.VALUENOW,
1647+
this.getValue() + this.getExtent());
1648+
goog.a11y.aria.setState(
1649+
valueThumb, goog.a11y.aria.State.VALUENOW, this.getValue());
1650+
1651+
goog.a11y.aria.setState(
1652+
valueThumb, goog.a11y.aria.State.VALUEMAX,
1653+
this.getValue() + this.getExtent() - this.minExtent_);
1654+
goog.a11y.aria.setState(
1655+
extentThumb, goog.a11y.aria.State.VALUEMAX, this.getMaximum());
1656+
1657+
this.updateAriaValueText_();
1658+
};
1659+
1660+
1661+
/**
1662+
* Updates the 'aria-valuetext' property for the slider thumbs.
1663+
* @private
1664+
*/
1665+
goog.ui.SliderBase.prototype.updateAriaValueText_ = function() {
1666+
// Passing an empty value to setState will restore the default.
1667+
goog.a11y.aria.setState(
1668+
goog.asserts.assertElement(this.extentThumb),
1669+
goog.a11y.aria.State.VALUETEXT,
1670+
this.getTextValue(this.getValue() + this.getExtent()) || '');
1671+
goog.a11y.aria.setState(
1672+
goog.asserts.assertElement(this.valueThumb),
1673+
goog.a11y.aria.State.VALUETEXT, this.getTextValue(this.getValue()) || '');
15471674
};
15481675

15491676

@@ -1605,6 +1732,20 @@ goog.ui.SliderBase.prototype.setEnabled = function(enable) {
16051732
return;
16061733
}
16071734

1735+
if (enable) {
1736+
this.valueThumb.tabIndex = 0;
1737+
goog.a11y.aria.removeState(this.valueThumb, goog.a11y.aria.State.DISABLED);
1738+
this.extentThumb.tabIndex = 0;
1739+
goog.a11y.aria.removeState(this.extentThumb, goog.a11y.aria.State.DISABLED);
1740+
} else {
1741+
this.valueThumb.tabIndex = -1;
1742+
goog.a11y.aria.setState(
1743+
this.valueThumb, goog.a11y.aria.State.DISABLED, 'true');
1744+
this.extentThumb.tabIndex = -1;
1745+
goog.a11y.aria.setState(
1746+
this.extentThumb, goog.a11y.aria.State.DISABLED, 'true');
1747+
}
1748+
16081749
var eventType = enable ? goog.ui.Component.EventType.ENABLE :
16091750
goog.ui.Component.EventType.DISABLE;
16101751
if (this.dispatchEvent(eventType)) {
@@ -1644,14 +1785,25 @@ goog.ui.SliderBase.prototype.getOffsetStart_ = function(element) {
16441785

16451786

16461787
/**
1788+
* @param {number=} opt_value
16471789
* @return {?string} The text value for the slider's current value, or null if
16481790
* unavailable.
16491791
*/
1650-
goog.ui.SliderBase.prototype.getTextValue = function() {
1651-
return this.labelFn_(this.getValue());
1792+
goog.ui.SliderBase.prototype.getTextValue = function(opt_value) {
1793+
var value = opt_value || this.getValue();
1794+
return this.labelFn_(value);
16521795
};
16531796

16541797

1798+
/**
1799+
* Sets the function to map slider values to text description.
1800+
* @param {!function(number): string} labelFn
1801+
*/
1802+
goog.ui.SliderBase.prototype.setLabelFn = function(labelFn) {
1803+
this.labelFn_ = labelFn;
1804+
this.updateAriaValueText_();
1805+
};
1806+
16551807

16561808
/**
16571809
* The factory for creating additional animations to be played when animating to

0 commit comments

Comments
 (0)