From 7f54d9620e32231021aad607b7af7eeee5c24f54 Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Mon, 18 Dec 2023 12:03:13 -0600 Subject: [PATCH] Rating Slider Example: Redesign as an input for 10-value satisfaction scale (pull #2831) Resolves issue #2501 by modifying the rating slider example as follows: * Changes it to a 10-point scale from a 5-star scale that had half star ratings. * Changes the visual design to better represent common 10-point satisfaction sliders. * Improves the high contrast support. * Adds documentation that explains why the slider pattern is appropriate for this use case. --------- Co-authored-by: Andrea N. Cardona Co-authored-by: Matt King --- .../patterns/radio/examples/radio-rating.html | 2 +- .../slider/examples/css/slider-rating.css | 103 ++--- .../slider/examples/js/slider-rating.js | 371 +++++++++++++----- .../slider/examples/slider-color-viewer.html | 3 +- .../slider/examples/slider-rating.html | 131 ++++--- .../patterns/slider/examples/slider-seek.html | 3 +- .../slider/examples/slider-temperature.html | 3 +- content/patterns/slider/slider-pattern.html | 2 +- cspell.json | 2 + test/tests/slider_slider-rating.js | 64 +-- 10 files changed, 440 insertions(+), 244 deletions(-) diff --git a/content/patterns/radio/examples/radio-rating.html b/content/patterns/radio/examples/radio-rating.html index 6be2fc6161..05d94eced9 100644 --- a/content/patterns/radio/examples/radio-rating.html +++ b/content/patterns/radio/examples/radio-rating.html @@ -36,7 +36,7 @@

About This Example

Similar examples include:

diff --git a/content/patterns/slider/examples/css/slider-rating.css b/content/patterns/slider/examples/css/slider-rating.css index a5474d5bdd..0dc3c3fc00 100644 --- a/content/patterns/slider/examples/css/slider-rating.css +++ b/content/patterns/slider/examples/css/slider-rating.css @@ -6,6 +6,7 @@ .rating-slider { color: #005a9c; + user-select: none; } .rating-slider svg { @@ -14,84 +15,88 @@ } .rating-slider svg .focus-ring { - fill: #eee; + fill: currentcolor; stroke-width: 0; fill-opacity: 0; } -.rating-slider svg .star { +.rating-slider svg .target { stroke-width: 2px; stroke: currentcolor; fill-opacity: 0; } -.rating-slider svg .fill-left, -.rating-slider svg .fill-right { - stroke-width: 0; - fill-opacity: 0; -} - -.rating-slider[aria-valuenow="5"] svg .star { +.rating-slider svg .label { + font-size: 90%; + font-family: sans-serif; fill: currentcolor; - fill-opacity: 1; } -.rating-slider[aria-valuenow="0.5"] svg .star-1 .fill-left { - fill: currentcolor; - fill-opacity: 1; +.rating-slider svg .description { + font-size: 90%; + fill: canvastext; } -.rating-slider[aria-valuenow="1"] svg .star-1 .star { +.rating-slider svg .current .target { fill: currentcolor; fill-opacity: 1; } -.rating-slider[aria-valuenow="1.5"] svg .star-1 .star, -.rating-slider[aria-valuenow="1.5"] svg .star-2 .fill-left { - fill: currentcolor; - fill-opacity: 1; +.rating-slider svg .current .label { + fill: white; + font-weight: bold; } -.rating-slider[aria-valuenow="2"] svg .star-2 .star { - fill: currentcolor; - fill-opacity: 1; -} +/* focus styling */ -.rating-slider[aria-valuenow="2.5"] svg .star-2 .star, -.rating-slider[aria-valuenow="2.5"] svg .star-3 .fill-left { - fill: currentcolor; - fill-opacity: 1; +.rating-slider:focus, +.rating-slider:focus-visible { + outline: none !important; } -.rating-slider[aria-valuenow="3"] svg .star-3 .star { - fill: currentcolor; - fill-opacity: 1; +.rating-slider svg .focus { + stroke-width: 0; + stroke: currentcolor; + fill-opacity: 0; } -.rating-slider[aria-valuenow="3.5"] svg .star-3 .star, -.rating-slider[aria-valuenow="3.5"] svg .star-4 .fill-left { - fill: currentcolor; - fill-opacity: 1; +.rating-slider:focus svg .focus-ring { + stroke-width: 2px; + stroke: currentcolor; } -.rating-slider[aria-valuenow="4"] svg .star-4 .star { - fill: currentcolor; - fill-opacity: 1; -} +@media (forced-colors: active) { + .rating-slider svg .focus-ring { + fill: linktext; + } -.rating-slider[aria-valuenow="4.5"] svg .star-4 .star, -.rating-slider[aria-valuenow="4.5"] svg .star-5 .fill-left { - fill: currentcolor; - fill-opacity: 1; -} + .rating-slider svg .target { + stroke: linktext; + } -/* focus styling */ + .rating-slider svg .label { + fill: linktext; + } -.rating-slider:focus { - outline: none; -} + .rating-slider svg .description { + fill: linktext; + } -.rating-slider:focus svg .focus-ring { - stroke-width: 2px; - stroke: currentcolor; + .rating-slider svg .current .target { + fill: linktext; + } + + .rating-slider svg .current .label { + fill: canvas; + } + + /* focus styling */ + + .rating-slider svg .focus { + stroke: linktext; + } + + .rating-slider:focus svg .focus-ring { + stroke: linktext; + } } diff --git a/content/patterns/slider/examples/js/slider-rating.js b/content/patterns/slider/examples/js/slider-rating.js index 5896160310..4eea74a725 100644 --- a/content/patterns/slider/examples/js/slider-rating.js +++ b/content/patterns/slider/examples/js/slider-rating.js @@ -8,6 +8,11 @@ * Desc: RatingSlider widget that implements ARIA Authoring Practices */ +const SELECTED_SIZE = 6; +const RAIL_LEFT = 13; +const RAIL_TOP = 35; +const RAIL_HEIGHT = 24; + class RatingSlider { constructor(domNode) { this.sliderNode = domNode; @@ -15,47 +20,49 @@ class RatingSlider { this.isMoving = false; this.svgNode = domNode.querySelector('svg'); + this.focusRect = domNode.querySelector('.focus-ring'); - // Inherit system text colors - // var color = getComputedStyle(this.sliderNode).color; - // this.svgNode.setAttribute('color', color); + this.targetRects = Array.from( + domNode.querySelectorAll('g.rating rect.target') + ); - this.starsWidth = 198; - this.starsX = 0; + this.labelTexts = Array.from( + domNode.querySelectorAll('g.rating text.label') + ); - this.svgPoint = this.svgNode.createSVGPoint(); + [this.targetInfo, this.railWidth] = this.calcRatingRects(); + this.infoDefaultFocusRect = this.calcDefaultFocusRect(); - // define possible slider positions + this.valueMin = this.getValueMin(); + this.valueMax = this.getValueMax(); this.sliderNode.addEventListener( 'keydown', this.onSliderKeydown.bind(this) ); - this.svgNode.addEventListener('click', this.onRailClick.bind(this)); - this.svgNode.addEventListener( - 'pointerdown', - this.onSliderPointerDown.bind(this) - ); + this.labelTexts.forEach((lt) => { + lt.addEventListener('click', this.onTargetClick.bind(this)); + }); - // bind a pointermove event handler to move pointer - this.svgNode.addEventListener('pointermove', this.onPointerMove.bind(this)); + this.targetRects.forEach((tr) => { + tr.addEventListener('click', this.onTargetClick.bind(this)); + tr.addEventListener('pointerdown', this.onSliderPointerDown.bind(this)); + tr.addEventListener('pointermove', this.onPointerMove.bind(this)); + }); // bind a pointerup event handler to stop tracking pointer movements document.addEventListener('pointerup', this.onPointerUp.bind(this)); - this.addTotalStarsToRatingLabel(); + this.addTotalRectsToRatingLabel(); this.sliderNode.addEventListener( 'blur', - this.addTotalStarsToRatingLabel.bind(this) + this.addTotalRectsToRatingLabel.bind(this) ); - } - // Get point in global SVG space - getSVGPoint(event) { - this.svgPoint.x = event.clientX; - this.svgPoint.y = event.clientY; - return this.svgPoint.matrixTransform(this.svgNode.getScreenCTM().inverse()); + window.addEventListener('resize', this.onResize.bind(this)); + + this.setFocusRing(0); } getValue() { @@ -70,46 +77,40 @@ class RatingSlider { return parseFloat(this.sliderNode.getAttribute('aria-valuemax')); } - isInRange(value) { - let valueMin = this.getValueMin(); - let valueMax = this.getValueMax(); - return value <= valueMax && value >= valueMin; - } - getValueText(value) { switch (value) { case 0: - return 'zero stars'; + return 'Choose a rating from one to ten where 10 is extremely satisfied'; - case 0.5: - return 'one half star'; + case 1: + return 'one, extremely dissatisfied'; - case 1.0: - return 'one star'; + case 2: + return 'two'; - case 1.5: - return 'one and a half stars'; + case 3: + return 'three'; - case 2.0: - return 'two stars'; + case 4: + return 'four'; - case 2.5: - return 'two and a half stars'; + case 5: + return 'five'; - case 3.0: - return 'three stars'; + case 6: + return 'six'; - case 3.5: - return 'three and a half stars'; + case 7: + return 'seven'; - case 4.0: - return 'four stars'; + case 8: + return 'eight'; - case 4.5: - return 'four and a half stars'; + case 9: + return 'nine'; - case 5.0: - return 'five stars'; + case 10: + return 'ten, extremely satisfied'; default: break; @@ -121,37 +122,37 @@ class RatingSlider { getValueTextWithMax(value) { switch (value) { case 0: - return 'zero of five stars'; + return 'Choose a rating from one to ten where 10 is extremely satisfied'; - case 0.5: - return 'one half of five stars'; + case 1: + return 'one out of 10, extremely dissatisfied'; - case 1.0: - return 'one of five stars'; + case 2: + return 'two out of ten where ten is extremely satisfied'; - case 1.5: - return 'one and a half of five stars'; + case 3: + return 'three out of ten where ten is extremely satisfied'; - case 2.0: - return 'two of five stars'; + case 4: + return 'four out of ten where ten is extremely satisfied'; - case 2.5: - return 'two and a half of five stars'; + case 5: + return 'five out of ten where ten is extremely satisfied'; - case 3.0: - return 'three of five stars'; + case 6: + return 'six out of ten where ten is extremely satisfied'; - case 3.5: - return 'three and a half of five stars'; + case 7: + return 'seven out of ten where ten is extremely satisfied'; - case 4.0: - return 'four of five stars'; + case 8: + return 'eight out of ten where ten is extremely satisfied'; - case 4.5: - return 'four and a half of five stars'; + case 9: + return 'nine out of ten where ten is extremely satisfied'; - case 5.0: - return 'five of five stars'; + case 10: + return 'ten out of ten, extremely satisfied'; default: break; @@ -160,55 +161,209 @@ class RatingSlider { return 'Unexpected value: ' + value; } - moveSliderTo(value) { - let valueMax, valueMin; + calcRatingRects() { + let infoRatingRects = []; - valueMin = this.getValueMin(); - valueMax = this.getValueMax(); + const railWidth = Math.min( + Math.max(260, this.sliderNode.getBoundingClientRect().width), + 600 + ); + const rectWidth = Math.round((railWidth - RAIL_LEFT) / 10); - value = Math.min(Math.max(value, valueMin), valueMax); + let left = RAIL_LEFT; - this.sliderNode.setAttribute('aria-valuenow', value); + for (let i = 0; i < this.targetRects.length; i += 1) { + const targetNode = this.targetRects[i]; + const labelNode = this.labelTexts[i]; + + targetNode.setAttribute('x', left); + targetNode.setAttribute('y', RAIL_TOP); + targetNode.setAttribute('width', rectWidth); + targetNode.setAttribute('height', RAIL_HEIGHT); + targetNode.removeAttribute('rx'); + + this.setLabelPosition(labelNode, left, rectWidth); + + const targetInfo = { + x: left, + y: RAIL_TOP, + width: rectWidth, + height: RAIL_HEIGHT, + rx: 0, + }; + + infoRatingRects[i] = targetInfo; + + targetNode.parentNode.classList.remove('current'); + + left += rectWidth; + } + + // adjust extremely satisfied label position + const descNodes = this.sliderNode.querySelectorAll('g.rating .description'); + let descX = RAIL_LEFT; + descNodes[0].setAttribute('x', descX); + descX = Math.round(railWidth - descNodes[1].getBBox().width + 2); + descNodes[1].setAttribute('x', descX); + + return [infoRatingRects, railWidth]; + } + + calcDefaultFocusRect() { + return { + x: 2, + y: 2, + width: this.railWidth + SELECTED_SIZE, + height: RAIL_TOP + RAIL_HEIGHT + SELECTED_SIZE, + rx: SELECTED_SIZE, + }; + } + + resetRects() { + for (let i = 0; i < this.targetRects.length; i += 1) { + const targetNode = this.targetRects[i]; + const targetInfo = this.targetInfo[i]; + const labelNode = this.labelTexts[i]; + + targetNode.setAttribute('x', targetInfo.x); + targetNode.setAttribute('y', targetInfo.y); + targetNode.setAttribute('width', targetInfo.width); + targetNode.setAttribute('height', targetInfo.height); + targetNode.removeAttribute('rx'); + + this.setLabelPosition(labelNode, targetInfo.x, targetInfo.width); + + targetNode.parentNode.classList.remove('current'); + } + } + + setSelectedRatingRect(value) { + let labelNode, targetNode, targetInfo; + + const leftValue = value - 1; + const rightValue = value + 1; + + if (value > 0) { + targetNode = this.targetRects[value - 1]; + targetInfo = this.targetInfo[value - 1]; + labelNode = this.labelTexts[value - 1]; + + targetNode.parentNode.classList.add('current'); + + const rectWidth = targetInfo.width + 2 * SELECTED_SIZE; + const x = targetInfo.x - SELECTED_SIZE; + + targetNode.setAttribute('x', x); + targetNode.setAttribute('y', targetInfo.y - SELECTED_SIZE); + targetNode.setAttribute('width', rectWidth); + targetNode.setAttribute('height', targetInfo.height + 2 * SELECTED_SIZE); + targetNode.setAttribute('rx', SELECTED_SIZE); + + this.setLabelPosition(labelNode, x, rectWidth, '120%'); + } + + if (leftValue > 0) { + targetNode = this.targetRects[leftValue - 1]; + targetInfo = this.targetInfo[leftValue - 1]; + + targetNode.setAttribute('width', targetInfo.width - SELECTED_SIZE); + } + + if (rightValue <= this.valueMax && value > 0) { + targetNode = this.targetRects[rightValue - 1]; + targetInfo = this.targetInfo[rightValue - 1]; + + targetNode.setAttribute('x', targetInfo.x + SELECTED_SIZE); + targetNode.setAttribute('width', targetInfo.width - SELECTED_SIZE); + } + } + + setLabelPosition(labelNode, x, rectWidth, fontSize = '95%') { + labelNode.setAttribute('style', `font-size: ${fontSize}`); + + const labelWidth = Math.round(labelNode.getBBox().width); + const labelHeight = Math.round(labelNode.getBBox().height); + + labelNode.setAttribute( + 'x', + 2 + x + Math.round((rectWidth - labelWidth) / 2) + ); + labelNode.setAttribute( + 'y', + -1 + + RAIL_TOP + + RAIL_HEIGHT - + Math.round((RAIL_HEIGHT - labelHeight + 4) / 2) + ); + } + + setFocusRing(value) { + const size = 2 * SELECTED_SIZE; + + if (value > 0 && value <= this.valueMax) { + const targetInfo = this.targetInfo[value - 1]; + + this.focusRect.setAttribute('x', targetInfo.x - size); + this.focusRect.setAttribute('y', targetInfo.y - size); + this.focusRect.setAttribute('width', targetInfo.width + 2 * size); + this.focusRect.setAttribute('height', targetInfo.height + 2 * size); + this.focusRect.setAttribute('rx', size); + } else { + // Set ring around entire control + + this.focusRect.setAttribute('x', this.infoDefaultFocusRect.x); + this.focusRect.setAttribute('y', this.infoDefaultFocusRect.y); + this.focusRect.setAttribute('width', this.infoDefaultFocusRect.width); + this.focusRect.setAttribute('height', this.infoDefaultFocusRect.height); + this.focusRect.setAttribute('rx', SELECTED_SIZE); + } + } + + moveSliderTo(value) { + value = Math.min(Math.max(value, this.valueMin + 1), this.valueMax); + this.sliderNode.setAttribute('aria-valuenow', value); this.sliderNode.setAttribute('aria-valuetext', this.getValueText(value)); + + this.resetRects(); + this.setSelectedRatingRect(value); + this.setFocusRing(value); } onSliderKeydown(event) { - var flag = false; - var value = this.getValue(); - var valueMin = this.getValueMin(); - var valueMax = this.getValueMax(); + let flag = false; + let value = this.getValue(); switch (event.key) { case 'ArrowLeft': case 'ArrowDown': - this.moveSliderTo(value - 0.5); + this.moveSliderTo(value - 1); flag = true; break; case 'ArrowRight': case 'ArrowUp': - this.moveSliderTo(value + 0.5); + this.moveSliderTo(value + 1); flag = true; break; case 'PageDown': - this.moveSliderTo(value - 1); + this.moveSliderTo(value - 2); flag = true; break; case 'PageUp': - this.moveSliderTo(value + 1); + this.moveSliderTo(value + 2); flag = true; break; case 'Home': - this.moveSliderTo(valueMin); + this.moveSliderTo(this.valueMin + 1); flag = true; break; case 'End': - this.moveSliderTo(valueMax); + this.moveSliderTo(this.valueMax); flag = true; break; @@ -222,18 +377,15 @@ class RatingSlider { } } - addTotalStarsToRatingLabel() { + addTotalRectsToRatingLabel() { let valuetext = this.getValueTextWithMax(this.getValue()); this.sliderNode.setAttribute('aria-valuetext', valuetext); } - onRailClick(event) { - var x = this.getSVGPoint(event).x; - var min = this.getValueMin(); - var max = this.getValueMax(); - var diffX = x - this.starsX; - var value = Math.round((2 * (diffX * (max - min))) / this.starsWidth) / 2; - this.moveSliderTo(value); + onTargetClick(event) { + this.moveSliderTo( + event.currentTarget.parentNode.getAttribute('data-value') + ); event.preventDefault(); event.stopPropagation(); @@ -243,31 +395,36 @@ class RatingSlider { } onSliderPointerDown(event) { - this.isMoving = true; - + if (!this.isMoving) { + this.isMoving = true; + } event.preventDefault(); event.stopPropagation(); - - // Set focus to the clicked handle - this.sliderNode.focus(); } onPointerMove(event) { if (this.isMoving) { - var x = this.getSVGPoint(event).x; - var min = this.getValueMin(); - var max = this.getValueMax(); - var diffX = x - this.starsX; - var value = Math.round((2 * (diffX * (max - min))) / this.starsWidth) / 2; - this.moveSliderTo(value); - + this.moveSliderTo( + event.currentTarget.parentNode.getAttribute('data-value') + ); event.preventDefault(); event.stopPropagation(); } } onPointerUp() { - this.isMoving = false; + if (this.isMoving) { + this.isMoving = false; + // Set focus to the clicked handle + this.sliderNode.focus(); + } + } + + onResize() { + [this.targetInfo, this.railWidth] = this.calcRatingRects(); + this.infoDefaultFocusRect = this.calcDefaultFocusRect(); + this.setSelectedRatingRect(this.getValue()); + this.setFocusRing(this.getValue()); } } diff --git a/content/patterns/slider/examples/slider-color-viewer.html b/content/patterns/slider/examples/slider-color-viewer.html index 867dd0c16d..6fe6559ed7 100644 --- a/content/patterns/slider/examples/slider-color-viewer.html +++ b/content/patterns/slider/examples/slider-color-viewer.html @@ -46,9 +46,8 @@

Warning!

Similar examples include:

    -
  • Rating Radio Group Example: Radio group that provides input for a five-star rating scale.
  • -
  • Color Viewer Slider Example: Basic horizontal sliders that illustrate setting numeric values for a color picker.
  • Vertical Temperature Slider Example: Demonstrates using aria-orientation to specify vertical orientation and aria-valuetext to communicate unit of measure for a temperature input.
  • +
  • Rating Slider Example: Horizontal slider that demonstrates using aria-valuetext to make it easy for assistive technology users to understand the meaning of the current value chosen on a ten-point satisfaction scale.
  • Media Seek Slider Example: Horizontal slider that demonstrates using aria-valuetext to communicate current and maximum values of time in media to make the values easy to understand for assistive technology users by converting the total number of seconds to minutes and seconds.
  • Horizontal Multi-Thumb Slider Example: Demonstrates using sliders with two thumbs to provide inputs for numeric ranges, such as for searching in a price range.
diff --git a/content/patterns/slider/examples/slider-rating.html b/content/patterns/slider/examples/slider-rating.html index e6eeb74a66..bb619bb937 100644 --- a/content/patterns/slider/examples/slider-rating.html +++ b/content/patterns/slider/examples/slider-rating.html @@ -41,13 +41,19 @@

Warning!

Following is an example of a rating input that demonstrates the Slider Pattern. - This rating widget employs a slider because the slider pattern supports step values of any size. - This particular input enables half-star steps. - A typical five-star rating widget that allows only five possible values could instead be implemented as a radio group. + This rating widget employs a slider because it enables users to choose from ten rating values, which is a relatively large number of choices for users to navigate. + For inputs with seven or fewer choices, another pattern that could be used is radio group as demonstrated by the + Rating Radio Group Example. + However, when there are more than seven choices, other patterns provide additional keyboard commands that significantly increase efficiency for users who rely on keyboard navigation to perceive options and make a selection. + These include + slider, + spin button, + combobox, + and listbox.

Similar examples include:

    -
  • Rating Radio Group Example: Radio group that provides input for a five-star rating scale.
  • +
  • Rating Radio Group Example: Radio group that provides input for a five star rating scale.
  • Color Viewer Slider Example: Basic horizontal sliders that illustrate setting numeric values for a color picker.
  • Vertical Temperature Slider Example: Demonstrates using aria-orientation to specify vertical orientation and aria-valuetext to communicate unit of measure for a temperature input.
  • Media Seek Slider Example: Horizontal slider that demonstrates using aria-valuetext to communicate current and maximum values of time in media to make the values easy to understand for assistive technology users by converting the total number of seconds to minutes and seconds.
  • @@ -61,41 +67,63 @@

    Example

    -
    Rating
    +
    Rate your satisfaction with the service you received
    -
    +
    -
    @@ -106,17 +134,19 @@

    Example

    Accessibility Features

    • - To ensure assistive technology users correctly perceive the maximum slider value, this example uses the aria-valuetext property to communicate both the current and maximum values. + To ensure assistive technology users correctly understand the meaning of the current value, this example uses the + aria-valuetext + property to communicate the current value, maximum value, and the meaning of the maximum value (Extremely Satisfied). However, since repeating the maximum value every time the slider value changes is potentially distracting, the maximum value is included in aria-valuetext only when the slider is initialized and when the thumb loses keyboard focus.
    • -
    • To highlight the interactive nature of the rating stars, a focus ring appears around the group of stars when the thumb has focus.
    • +
    • To highlight the interactive nature of the satisfaction rating, a focus ring appears around the group of rating options when the slider has focus and a rating value has not yet been set.
    • - To ensure the borders of the stars and focus ring have sufficient contrast with the background when high contrast settings invert colors, the color of the borders are synchronized with the color of the text content. - For example, the color of the star borders is set to match the foreground color of high contrast mode text by specifying the CSS currentcolor value for the stroke property of each inline SVG polygon element. - To enable the high contrast background color to be the used as the contrasting color when a star is not fully or partially filled, the fill-opacity attribute of the polygon is set to zero. + To ensure the borders of the rating values and focus ring have sufficient contrast with the background when high contrast settings invert colors, the color of the borders is synchronized with the color of the text content using a CSS media query selector (e.g. @media (forced-colors: active)) . + For example, the color of the rating value borders is set to match the link foreground color of high contrast mode text by specifying the CSS canvas and linkText values for the stroke and fill properties of each inline SVG rect and text element. If specific colors were used to specify the stroke and fill properties, the color of these elements would remain the same in high contrast mode, which could lead to insufficient contrast between them and their background or even make them invisible if their color were to match the high contrast mode background.
      Note: The SVG element needs to have the CSS forced-color-adjust property set to the value auto for the currentcolor value to be updated in high contrast modes. - Some browsers do not use auto for the default value.
    • + Some browsers do not use auto for the default value. +
    @@ -132,41 +162,41 @@

    Keyboard Support

    Right Arrow - Increases slider one half star. + Increases slider by one rating step. Up Arrow - Increases slider one half star. + Increases slider by one rating step. Left Arrow - Decreases slider one half star. + Decreases slider by one rating step. Down Arrow - Decreases slider one half star. + Decreases slider by one rating step. Page Up Increases slider value multiple steps. - In this slider, jumps one star. + In this slider, by two rating steps. Page Down Decreases slider value multiple steps. - In this slider, jumps one star. + In this slider, by two rating steps. Home - Sets slider to its minimum value, no stars. + Sets slider to its minimum value, extremely dissatisfied. End - Sets slider to its maximum value, five stars. + Sets slider to its maximum value, extremely satisfied. @@ -212,7 +242,7 @@

    Role, Property, State, and Tabindex Attributes

    - aria-valuemax="5" + aria-valuemax="10" div @@ -227,7 +257,7 @@

    Role, Property, State, and Tabindex Attributes

    div - Specifies the minimum value of the slider. + Specifies the minimum value of the slider.
    NOTE: 0 indicates a rating value has not yet been set. @@ -249,15 +279,16 @@

    Role, Property, State, and Tabindex Attributes

      -
    • A string value that provides a user-friendly name for the current value of the slider -- the number of stars and half stars.
    • -
    • When initialized, and when the slider loses focus, the string also includes the maximum value of five stars, e.g., 3 of 5 stars.
    • +
    • A string that provides a user-friendly name for the current value.
    • +
    • When the value is 0, 1, or 10, provides the name of the value.
    • +
    • When initialized and when the slider loses focus, the string also includes the number of rating values and the meaning of the maximum value, e.g., seven out of 10 where 10 is extremely satisfied.
    - aria-labelledby="IDREF" + aria-labelledby="ID_REFERENCE" div diff --git a/content/patterns/slider/examples/slider-seek.html b/content/patterns/slider/examples/slider-seek.html index 91f61c5082..5b589cd67f 100644 --- a/content/patterns/slider/examples/slider-seek.html +++ b/content/patterns/slider/examples/slider-seek.html @@ -49,10 +49,9 @@

    Warning!

    Similar examples include:

      -
    • Rating Radio Group Example: Radio group that provides input for a five-star rating scale.
    • Color Viewer Slider Example: Basic horizontal sliders that illustrate setting numeric values for a color picker.
    • Vertical Temperature Slider Example: Demonstrates using aria-orientation to specify vertical orientation and aria-valuetext to communicate unit of measure for a temperature input.
    • -
    • Media Seek Slider Example: Horizontal slider that demonstrates using aria-valuetext to communicate current and maximum values of time in media to make the values easy to understand for assistive technology users by converting the total number of seconds to minutes and seconds.
    • +
    • Rating Slider Example: Horizontal slider that demonstrates using aria-valuetext to make it easy for assistive technology users to understand the meaning of the current value chosen on a ten-point satisfaction scale.
    • Horizontal Multi-Thumb Slider Example: Demonstrates using sliders with two thumbs to provide inputs for numeric ranges, such as for searching in a price range.
    diff --git a/content/patterns/slider/examples/slider-temperature.html b/content/patterns/slider/examples/slider-temperature.html index 2d8c79ad15..54c9c7fbbe 100644 --- a/content/patterns/slider/examples/slider-temperature.html +++ b/content/patterns/slider/examples/slider-temperature.html @@ -45,9 +45,8 @@

    Warning!

    Similar examples include:

      -
    • Rating Radio Group Example: Radio group that provides input for a five-star rating scale.
    • Color Viewer Slider Example: Basic horizontal sliders that illustrate setting numeric values for a color picker.
    • -
    • Vertical Temperature Slider Example: Demonstrates using aria-orientation to specify vertical orientation and aria-valuetext to communicate unit of measure for a temperature input.
    • +
    • Rating Slider Example: Horizontal slider that demonstrates using aria-valuetext to make it easy for assistive technology users to understand the meaning of the current value chosen on a ten-point satisfaction scale.
    • Media Seek Slider Example: Horizontal slider that demonstrates using aria-valuetext to communicate current and maximum values of time in media to make the values easy to understand for assistive technology users by converting the total number of seconds to minutes and seconds.
    • Horizontal Multi-Thumb Slider Example: Demonstrates using sliders with two thumbs to provide inputs for numeric ranges, such as for searching in a price range.
    diff --git a/content/patterns/slider/slider-pattern.html b/content/patterns/slider/slider-pattern.html index c8cc0d5ddd..35d308de8a 100644 --- a/content/patterns/slider/slider-pattern.html +++ b/content/patterns/slider/slider-pattern.html @@ -39,7 +39,7 @@

    Examples

    • Color Viewer Slider Example: Basic horizontal sliders that illustrate setting numeric values for a color picker.
    • Vertical Temperature Slider Example: Demonstrates using aria-orientation to specify vertical orientation and aria-valuetext to communicate unit of measure for a temperature input.
    • -
    • Rating Slider Example: Horizontal slider that demonstrates using aria-valuetext to communicate current and maximum value of a rating input for a five star rating scale.
    • +
    • Rating Slider Example: Horizontal slider that demonstrates using aria-valuetext to make it easy for assistive technology users to understand the meaning of the current value chosen on a ten-point satisfaction scale.
    • Media Seek Slider Example: Horizontal slider that demonstrates using aria-valuetext to communicate current and maximum values of time in media to make the values easy to understand for assistive technology users by converting the total number of seconds to minutes and seconds.
    diff --git a/cspell.json b/cspell.json index 4d1b974495..26cd4f08d9 100644 --- a/cspell.json +++ b/cspell.json @@ -26,6 +26,7 @@ "Brinza", "Bucketwheat", "camelcase", + "canvastext", "Capitan", "Carron", "checkmark", @@ -123,6 +124,7 @@ "Leventhal", "Lewandowski", "Lilley", + "linktext", "listbox's", "Listboxes", "listitems", diff --git a/test/tests/slider_slider-rating.js b/test/tests/slider_slider-rating.js index 4dd7360c16..c802d3af3e 100644 --- a/test/tests/slider_slider-rating.js +++ b/test/tests/slider_slider-rating.js @@ -10,12 +10,13 @@ const ex = { sliderSelector: '#ex1 [role="slider"]', ratingSelector: '#id-rating', svgSelector: '#ex1 svg', - ratingMax: '5', + ratingMax: '10', ratingMin: '0', ratingDefault: '0', - ratingDefaultValue: 'zero of five stars', - ratingInc: 0.5, - ratingPageInc: 1, + ratingDefaultValue: + 'Choose a rating from one to ten where 10 is extremely satisfied', + ratingInc: 1, + ratingPageInc: 2, }; const sendAllSlidersToEnd = async function (t) { @@ -28,53 +29,57 @@ const sendAllSlidersToEnd = async function (t) { const getRatingValueAndText = function (v, change) { let value = parseFloat(v) + parseFloat(change); - value = Math.min(Math.max(value, ex.ratingMin), ex.ratingMax); + + let min = parseFloat(change) <= 0 ? 1 : parseFloat(ex.ratingMin); + + value = Math.min(Math.max(value, min), ex.ratingMax); let valuetext = 'Unexpected value: ' + value; switch (value) { case 0: - valuetext = 'zero stars'; + valuetext = + 'Choose a rating from one to ten where 10 is extremely satisfied'; break; - case 0.5: - valuetext = 'one half star'; + case 1: + valuetext = 'one, extremely dissatisfied'; break; - case 1.0: - valuetext = 'one star'; + case 2: + valuetext = 'two'; break; - case 1.5: - valuetext = 'one and a half stars'; + case 3: + valuetext = 'three'; break; - case 2.0: - valuetext = 'two stars'; + case 4: + valuetext = 'four'; break; - case 2.5: - valuetext = 'two and a half stars'; + case 5: + valuetext = 'five'; break; - case 3.0: - valuetext = 'three stars'; + case 6: + valuetext = 'six'; break; - case 3.5: - valuetext = 'three and a half stars'; + case 7: + valuetext = 'seven'; break; - case 4.0: - valuetext = 'four stars'; + case 8: + valuetext = 'eight'; break; - case 4.5: - valuetext = 'four and a half stars'; + case 9: + valuetext = 'nine'; break; - case 5.0: - valuetext = 'five stars'; + case 10: + valuetext = 'ten, extremely satisfied'; break; default: @@ -204,8 +209,7 @@ ariaTest( t.deepEqual( await getValueAndText(t, ex.ratingSelector), [value, text], - 'After sending 1 arrow right key to the rating slider, aria-valuenow should be " + value + " and aria-value-text should be: ' + - text + `After sending 1 arrow right key to the rating slider, aria-valuenow should be "${value}"" and aria-value-text should be: ${text}` ); // Send more than 5 keys to rating slider @@ -325,7 +329,7 @@ ariaTest( t.deepEqual( await getValueAndText(t, ex.ratingSelector), [value, text], - 'After sending key end to the heat slider, aria-valuenow should be "' + + 'After sending key end to the rating slider, aria-valuenow should be "' + value + '" and aria-value-text should be: ' + text @@ -480,7 +484,7 @@ ariaTest( t.deepEqual( await getValueAndText(t, ex.ratingSelector), [value, text], - 'After sending key home to the heat slider, aria-valuenow should be "' + + 'After sending key home to the rating slider, aria-valuenow should be "' + ex.ratingMin + '" and aria-value-text should be: ' + text