diff --git a/examples/article.html b/examples/article.html
index 0c4901e..9288be3 100644
--- a/examples/article.html
+++ b/examples/article.html
@@ -43,8 +43,8 @@
-
-
+
+
diff --git a/src/ui/d-slider.js b/src/ui/d-slider.js
index 65362bd..5f798f1 100644
--- a/src/ui/d-slider.js
+++ b/src/ui/d-slider.js
@@ -79,6 +79,7 @@ const T = Template('d-slider', `
position: relative;
margin-top: 4px;
height: 4px;
+ z-index: -1;
}
.ticks .tick {
@@ -100,21 +101,7 @@ const T = Template('d-slider', `
`);
-// Events
-// change, only on commit
-// input, onslide
-
-// Properties
-// min
-// max
-// step [float | "any"]
-//
-
// ARIA
-// The element serving as the focusable slider control has role slider.
-// The slider element has the aria-valuenow property set to a decimal value representing the current value of the slider.
-// The slider element has the aria-valuemin property set to a decimal value representing the minimum allowed value of the slider.
-// The slider element has the aria-valuemax property set to a decimal value representing the maximum al
// If the slider has a visible label, it is referenced by aria-labelledby on the slider element. Otherwise, the slider element has a label provided by aria-label.
// If the slider is vertically oriented, it has aria-orientation set to vertical. The default value of aria-orientation for a slider is horizontal.
@@ -130,25 +117,39 @@ const keyCodes = {
};
export class Slider extends T(HTMLElement) {
+
+
connectedCallback() {
- if (!this.hasAttribute("tabindex")) {
- this.setAttribute("tabindex", 0);
- }
+ this.setAttribute("role", "slider");
+ // Makes the element tab-able.
+ if (!this.hasAttribute("tabindex")) { this.setAttribute("tabindex", 0); }
+
+ // Keeps track of keyboard vs. mouse interactions for focus rings
this.mouseEvent = false;
+
+ // Handles to shadow DOM elements
this.knob = this.root.querySelector(".knob-container");
this.background = this.root.querySelector(".background");
this.trackFill = this.root.querySelector(".track-fill");
this.track = this.root.querySelector(".track");
- this.min = this.hasAttribute("min") ? +this.getAttribute("min") : 0;
- this.max = this.hasAttribute("max") ? +this.getAttribute("max") : 100;
- this.value = this.hasAttribute("value") ? +this.getAttribute("value") : 0;
- this.step = this.hasAttribute("step") ? +this.getAttribute("step") : 1;
+
+ // Default values for attributes
+ this.min = this.min ? this.min : 0;
+ this.max = this.max ? this.max : 100;
this.scale = scaleLinear().domain([this.min, this.max]).range([0, 1]).clamp(true);
+
+ this.step = this.step ? this.step : 1;
+ this.update(this.value ? this.value : 0);
+
+ this.ticks = this.ticks ? this.ticks : false;
+ this.renderTicks();
+
this.drag = drag()
.container(this.track)
.on("start", () => {
this.mouseEvent = true;
this.background.classList.add("mousedown");
+ this.changeValue = this.value;
this.dragUpdate();
})
.on("drag", () => {
@@ -157,9 +158,12 @@ export class Slider extends T(HTMLElement) {
.on("end", () => {
this.mouseEvent = false;
this.background.classList.remove("mousedown");
+ this.dragUpdate();
+ if (this.changeValue !== this.value) this.dispatchChange();
+ this.changeValue = this.value;
});
this.drag(select(this.background));
- this.renderTicks();
+
this.addEventListener("focusin", (e) => {
if(!this.mouseEvent) {
this.background.classList.add("focus");
@@ -168,51 +172,86 @@ export class Slider extends T(HTMLElement) {
this.addEventListener("focusout", (e) => {
this.background.classList.remove("focus");
});
- this.addEventListener("keydown", (e) => {
- console.log("keydown", e);
- let stopPropagation = false;
- switch (event.keyCode) {
- case keyCodes.left:
- case keyCodes.down:
- this.update(this.value - this.step)
- stopPropagation = true;
- break;
- case keyCodes.right:
- case keyCodes.up:
- this.update(this.value + this.step)
- stopPropagation = true;
- break;
- case keyCodes.pageUp:
- this.update(this.value + this.step * 10)
- stopPropagation = true;
- break;
-
- case keyCodes.pageDown:
- this.update(this.value + this.step * 10)
- stopPropagation = true;
- break;
- case keyCodes.home:
- this.update(this.min);
- stopPropagation = true;
- break;
- case keyCodes.end:
- this.update(this.max);
- stopPropagation = true;
- break;
- default:
- break;
- }
- if (stopPropagation) {
- e.preventDefault();
- e.stopPropagation();
- }
- });
+ this.addEventListener("keydown", this.onKeyDown);
this.addEventListener("focus", () => {
console.log("focus");
});
this.addEventListener("blur", () => {
console.log("blur");
- })
+ });
+
+ }
+
+ static get observedAttributes() {return ["min", "max", "value", "step", "ticks"]; }
+
+ attributeChangedCallback(attr, oldValue, newValue) {
+ if (attr == "min") {
+ this.min = +newValue;
+ this.setAttribute("aria-valuemin", this.min);
+ }
+ if (attr == "max") {
+ this.max = +newValue;
+ this.setAttribute("aria-valuemax", this.max);
+ }
+ if (attr == "value") this.value = +newValue;
+ if (attr == "step") {
+ if (newValue > 0) {
+ this.step = +newValue;
+ }
+ }
+ if (attr == "ticks") {
+ this.ticks = (newValue === "" ? true : newValue);
+ }
+ }
+
+ onKeyDown(e) {
+ this.changeValue = this.value;
+ let stopPropagation = false;
+ switch (event.keyCode) {
+ case keyCodes.left:
+ case keyCodes.down:
+ this.update(this.value - this.step)
+ stopPropagation = true;
+ break;
+ case keyCodes.right:
+ case keyCodes.up:
+ this.update(this.value + this.step)
+ stopPropagation = true;
+ break;
+ case keyCodes.pageUp:
+ this.update(this.value + this.step * 10)
+ stopPropagation = true;
+ break;
+
+ case keyCodes.pageDown:
+ this.update(this.value + this.step * 10)
+ stopPropagation = true;
+ break;
+ case keyCodes.home:
+ this.update(this.min);
+ stopPropagation = true;
+ break;
+ case keyCodes.end:
+ this.update(this.max);
+ stopPropagation = true;
+ break;
+ default:
+ break;
+ }
+ if (stopPropagation) {
+ this.background.classList.add("focus");
+ e.preventDefault();
+ e.stopPropagation();
+ if (this.changeValue !== this.value) this.dispatchChange();
+ }
+ }
+
+ validateValueRange(min, max, value) {
+ return Math.max(Math.min(max, value), min)
+ }
+
+ quantizeValue(value, step) {
+ return Math.round(value / step) * step;
}
dragUpdate() {
@@ -226,24 +265,25 @@ export class Slider extends T(HTMLElement) {
update(value) {
let v = value;
if (this.step !== "any") {
- v = Math.round(value / this.step) * this.step;
+ v = this.quantizeValue(value, this.step);
}
- v = Math.max(Math.min(this.max, v), this.min);
- this.knob.style.left = this.scale(v) * 100 + "%";
- this.trackFill.style.width = this.scale(v) * 100 + "%";
+ v = this.validateValueRange(this.min, this.max, v);
if (this.value !== v) {
+ this.knob.style.left = this.scale(v) * 100 + "%";
+ this.trackFill.style.width = this.scale(v) * 100 + "%";
this.value = v;
+ this.setAttribute("aria-valuenow", this.value);
this.dispatchInput();
- // TODO change should only update on commit.
- this.dispatchChange();
}
}
+ // Dispatches only on a committed change (basically only on mouseup).
dispatchChange() {
const e = new Event("change");
this.dispatchEvent(e, {});
}
+ // Dispatches on each value change.
dispatchInput() {
const e = new Event("input");
this.dispatchEvent(e, {});
@@ -251,17 +291,21 @@ export class Slider extends T(HTMLElement) {
renderTicks() {
const ticksContainer = this.root.querySelector(".ticks");
- let tickData = [];
- if (this.step === "any") {
- tickData = this.scale.ticks();
+ if (this.ticks !== false) {
+ let tickData = [];
+ if (this.step === "any") {
+ tickData = this.scale.ticks();
+ } else {
+ tickData = range(this.min, this.max + 1e-6, this.step);
+ }
+ tickData.forEach(d => {
+ const tick = document.createElement("div");
+ tick.classList.add("tick");
+ tick.style.left = this.scale(d) * 100 + "%";
+ ticksContainer.appendChild(tick)
+ });
} else {
- tickData = range(this.min, this.max + 1e-6, this.step);
+ ticksContainer.style.display = "none";
}
- tickData.forEach(d => {
- const tick = document.createElement("div");
- tick.classList.add("tick");
- tick.style.left = this.scale(d) * 100 + "%";
- ticksContainer.appendChild(tick)
- });
}
}
\ No newline at end of file