Skip to content

Commit

Permalink
Merge pull request #5472 from uswds/cm-range-slider-sr-unit
Browse files Browse the repository at this point in the history
USWDS - Range: Enhance SR callouts
  • Loading branch information
thisisdano authored Nov 6, 2023
2 parents 8f92ea7 + 3657969 commit 4a1f9fb
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 3 deletions.
65 changes: 65 additions & 0 deletions packages/usa-range/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const selectOrMatches = require("../../uswds-core/src/js/utils/select-or-matches");
const behavior = require("../../uswds-core/src/js/utils/behavior");

const { prefix: PREFIX } = require("../../uswds-core/src/js/config");

const RANGE_CLASSNAME = `${PREFIX}-range`;
const RANGE = `.${RANGE_CLASSNAME}`;

/**
* Update range callout for screen readers using the optional data attributes.
*
* Get optional data attributes, construct and appends aria-valuetext attribute.
*
* @example
*
* <input id="usa-range" class="usa-range" type="range" min="0" max="100" step="10" value="20" data-text-unit="degrees">
*
* Callout returns "20 degrees of 100."
*
* <input id="usa-range" class="usa-range" type="range" min="0" max="100" step="10" value="20" data-text-preposition="de">
*
* Callout returns "20 de 100."
*
* @param {HTMLInputElement} targetRange - The range slider input element
*/
const updateCallout = (targetRange) => {
const rangeSlider = targetRange;
const defaultPrep = "of";
const optionalPrep = rangeSlider.dataset.textPreposition;
const prep = optionalPrep || defaultPrep;
const unit = rangeSlider.dataset.textUnit;
const val = rangeSlider.value;
// Note: 100 is the max attribute's native default value on range inputs
// Reference: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/range#validation
const max = rangeSlider.getAttribute("max") || 100;

let callout;

if (unit) {
callout = `${val} ${unit} ${prep} ${max}`;
} else {
callout = `${val} ${prep} ${max}`;
}

rangeSlider.setAttribute("aria-valuetext", callout);
};

const rangeEvents = {
change: {
[RANGE]() {
updateCallout(this);
},
},
};

const range = behavior(rangeEvents, {
init(root) {
selectOrMatches(RANGE, root).forEach((rangeSlider) => {
updateCallout(rangeSlider);
});
},
updateCallout,
});

module.exports = range;
12 changes: 12 additions & 0 deletions packages/usa-range/src/test/sr-callout-with-preposition.spec.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<form class="usa-form">
<input
id="usa-range"
class="usa-range"
type="range"
min="0"
max="100"
step="10"
value="20"
data-text-preposition="de"
>
</form>
71 changes: 71 additions & 0 deletions packages/usa-range/src/test/sr-callout-with-preposition.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
const fs = require("fs");
const path = require("path");
const assert = require("assert");
const range = require("../index");

const TEMPLATE = fs.readFileSync(
path.join(__dirname, "./sr-callout-with-preposition.spec.html")
);

const EVENTS = {};

/**
* send an change event
* @param {HTMLElement} el the element to sent the event to
*/
EVENTS.change = (el) => {
el.dispatchEvent(new KeyboardEvent("change", { bubbles: true }));
};

const rangeSliderSelector = () => document.querySelector(".usa-range");

const tests = [
{ name: "document.body", selector: () => document.body },
{ name: "range slider", selector: rangeSliderSelector },
];

tests.forEach(({ name, selector: containerSelector }) => {
describe(`Range slider with updated preposition initialized at ${name}`, () => {
describe("range slider component", () => {
const { body } = document;

let slider;
let valueText;

beforeEach(() => {
body.innerHTML = TEMPLATE;
range.on(containerSelector());

slider = rangeSliderSelector();
valueText = slider.getAttribute("aria-valuetext");
});

afterEach(() => {
body.textContent = "";
});

it("adds aria-valuetext attribute with updated preposition", () => {
assert.ok(valueText, "aria-valuetext attribute is added");
assert.strictEqual(
valueText,
"20 de 100",
"initial value is incorrect"
);
});

it("updates aria-valuetext with updated preposition to match new slider value on change", () => {
slider.value = "30";
EVENTS.change(slider);

assert.strictEqual(slider.value, "30");

valueText = slider.getAttribute("aria-valuetext");
assert.strictEqual(
valueText,
"30 de 100",
"Screen reader value does not match range value"
);
});
});
});
});
12 changes: 12 additions & 0 deletions packages/usa-range/src/test/sr-callout.spec.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<form class="usa-form">
<input
id="usa-range"
class="usa-range"
type="range"
min="0"
max="100"
step="10"
value="20"
data-text-unit="percent"
>
</form>
71 changes: 71 additions & 0 deletions packages/usa-range/src/test/sr-callout.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
const fs = require("fs");
const path = require("path");
const assert = require("assert");
const range = require("../index");

const TEMPLATE = fs.readFileSync(
path.join(__dirname, "./sr-callout.spec.html")
);

const EVENTS = {};

/**
* send an change event
* @param {HTMLElement} el the element to sent the event to
*/
EVENTS.change = (el) => {
el.dispatchEvent(new KeyboardEvent("change", { bubbles: true }));
};

const rangeSliderSelector = () => document.querySelector(".usa-range");

const tests = [
{ name: "document.body", selector: () => document.body },
{ name: "range slider", selector: rangeSliderSelector },
];

tests.forEach(({ name, selector: containerSelector }) => {
describe(`Range slider with aria-valuetext initialized at ${name}`, () => {
describe("range slider component", () => {
const { body } = document;

let slider;
let valueText;

beforeEach(() => {
body.innerHTML = TEMPLATE;
range.on(containerSelector());

slider = rangeSliderSelector();
valueText = slider.getAttribute("aria-valuetext");
});

afterEach(() => {
body.textContent = "";
});

it("adds aria-valuetext attribute", () => {
assert.ok(valueText, "aria-valuetext attribute is added");
assert.strictEqual(
valueText,
"20 percent of 100",
"initial value is incorrect"
);
});

it("updates aria-valuetext to match new slider value on change", () => {
slider.value = "30";
EVENTS.change(slider);

assert.strictEqual(slider.value, "30");

valueText = slider.getAttribute("aria-valuetext");
assert.strictEqual(
valueText,
"30 percent of 100",
"Screen reader value does not match range value"
);
});
});
});
});
27 changes: 27 additions & 0 deletions packages/usa-range/src/usa-range.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,39 @@ export default {
control: { type: "radio" },
options: ["none", "disabled", "aria-disabled"],
},
text_unit: {
name: "Unit",
control: { type: "text" },
},
text_preposition: {
name: "Preposition (of, for, in, de, etc.)",
control: { type: "text" },
},
min: {
name: "Min",
control: { type: "number" },
defaultValue: 0,
},
max: {
name: "Max",
control: { type: "number" },
defaultValue: 100,
},
step: {
name: "Step",
control: { type: "number" },
defaultValue: 10,
},
},
};

const Template = (args) => Component(args);

export const Range = Template.bind({});
Range.args = {
text_unit: "",
text_preposition: "",
};

export const Disabled = Template.bind({});
Disabled.args = {
Expand Down
8 changes: 5 additions & 3 deletions packages/usa-range/src/usa-range.twig
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
id="usa-range"
class="usa-range"
type="range"
min="0"
max="100"
step="10"
min="{{ min }}"
max="{{ max }}"
step="{{ step }}"
value="20"
{% if text_unit %}data-text-unit="{{ text_unit }}"{%- endif %}
{% if text_preposition %}data-text-preposition="{{ text_preposition }}"{%- endif %}
{% if disabled_state == 'disabled' %} disabled {%- endif %}
{% if disabled_state == 'aria-disabled' %} aria-disabled="true" {%- endif %}
>
Expand Down
2 changes: 2 additions & 0 deletions packages/uswds-core/src/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const languageSelector = require("../../../usa-language-selector/src/index");
const modal = require("../../../usa-modal/src/index");
const navigation = require("../../../usa-header/src/index");
const password = require("../../../_usa-password/src/index");
const range = require("../../../usa-range/src/index");
const search = require("../../../usa-search/src/index");
const skipnav = require("../../../usa-skipnav/src/index");
const table = require("../../../usa-table/src/index");
Expand All @@ -36,6 +37,7 @@ module.exports = {
modal,
navigation,
password,
range,
search,
skipnav,
table,
Expand Down

0 comments on commit 4a1f9fb

Please sign in to comment.