Commit 72b55d78 authored by Esmael El-Moslimany's avatar Esmael El-Moslimany Committed by Commit Bot

Settings: night-light-slider, active/inactive markers and improved focus styling

Also fixing an issue with the position of a knob when tracking.

Bug: 881290
Change-Id: Ib3520d4c89d7a9f5ac4575b47976e4ceb14131f3
Reviewed-on: https://chromium-review.googlesource.com/1228980
Commit-Queue: Esmael El-Moslimany <aee@chromium.org>
Reviewed-by: default avatarAhmed Fakhry <afakhry@chromium.org>
Reviewed-by: default avatarSteven Bennetts <stevenjb@chromium.org>
Reviewed-by: default avatarScott Chen <scottchen@chromium.org>
Cr-Commit-Position: refs/heads/master@{#595270}
parent 8d2d8d91
......@@ -123,7 +123,6 @@ js_library("layout_behavior") {
js_library("night_light_slider") {
deps = [
"../prefs:prefs_behavior",
"//third_party/polymer/v1_0/components-chromium/iron-a11y-keys-behavior:iron-a11y-keys-behavior-extracted",
"//third_party/polymer/v1_0/components-chromium/iron-resizable-behavior:iron-resizable-behavior-extracted",
"//third_party/polymer/v1_0/components-chromium/paper-behaviors:paper-inky-focus-behavior-extracted",
]
......
<link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="chrome://resources/cr_elements/shared_vars_css.html">
<link rel="import" href="chrome://resources/polymer/v1_0/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
<link rel="import" href="chrome://resources/polymer/v1_0/iron-resizable-behavior/iron-resizable-behavior.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-behaviors/paper-inky-focus-behavior.html">
<link rel="import" href="../prefs/prefs_behavior.html">
......@@ -105,7 +104,8 @@
width: 100%;
}
.markers {
.active-marker,
.inactive-marker {
background-color: rgba(255, 255, 255, 0.54);
border-radius: 50%;
display: block;
......@@ -117,6 +117,10 @@
z-index: 2;
}
.inactive-marker {
background-color: rgba(26, 115, 232, 0.54);
}
#legendContainer {
height: 10px;
position: relative;
......@@ -159,13 +163,11 @@
<div id="markersContainer">
</div>
<div id="startKnob" class="knob" tabindex="1" on-down="startDrag_"
on-up="endDrag_" on-track="continueDrag_" on-focus="onFocus_"
on-blur="onBlur_">
on-up="endDrag_" on-track="continueDrag_">
<div class="knob-inner" tabindex="-1"></div>
</div>
<div id="endKnob" class="knob" tabindex="2" on-down="startDrag_"
on-up="endDrag_" on-track="continueDrag_" on-focus="onFocus_"
on-blur="onBlur_">
on-up="endDrag_" on-track="continueDrag_">
<div class="knob-inner" tabindex="-1"></div>
</div>
</div>
......
......@@ -16,12 +16,32 @@ const MIN_KNOBS_DISTANCE_MINUTES = 60;
const OFFSET_MINUTES_6PM = 18 * 60;
const TOTAL_MINUTES_PER_DAY = 24 * 60;
/**
* % is the javascript remainder operator that satisfies the following for the
* resultant z given the operands x and y as in (z = x % y):
* 1. x = k * y + z
* 2. k is an integer.
* 3. |z| < |y|
* 4. z has the same sign as x.
*
* It is more convenient to have z be the same sign as y. In most cases y
* is a positive integer, and it is more intuitive to have z also be a positive
* integer (0 <= z < y).
*
* For example (-1 % 24) equals -1 whereas modulo(-1, 24) equals 23.
* @param {number} x
* @param {number} y
* @return {number}
*/
function modulo(x, y) {
return ((x % y) + y) % y;
}
Polymer({
is: 'night-light-slider',
behaviors: [
PrefsBehavior,
Polymer.IronA11yKeysBehavior,
Polymer.IronResizableBehavior,
Polymer.PaperInkyFocusBehavior,
],
......@@ -49,38 +69,28 @@ Polymer({
listeners: {
'iron-resize': 'onResize_',
focus: 'onFocus_',
blur: 'onBlur_',
keydown: 'onKeyDown_',
},
observers: [
'updateKnobs_(prefs.ash.night_light.custom_start_time.*, ' +
'prefs.ash.night_light.custom_end_time.*, isRTL_, isReady_)',
'hourFormatChanged_(prefs.settings.clock.use_24hour_clock.*)',
'updateMarkers_(prefs.ash.night_light.custom_start_time.*, ' +
'prefs.ash.night_light.custom_end_time.*, isRTL_, isReady_)',
],
keyBindings: {
'left': 'onLeftKey_',
'right': 'onRightKey_',
},
/**
* The object currently being dragged. Either the start or end knobs.
* @type {?Object}
* @type {Element}
* @private
*/
dragObject_: null,
/** @override */
attached: function() {
// Build the legend markers.
const markersContainer = this.$.markersContainer;
const width = markersContainer.offsetWidth;
for (let i = 0; i <= HOURS_PER_DAY; ++i) {
const marker = document.createElement('div');
marker.className = 'markers';
markersContainer.appendChild(marker);
marker.style.left = (i * 100 / HOURS_PER_DAY) + '%';
}
this.isRTL_ = window.getComputedStyle(this).direction == 'rtl';
this.$.sliderContainer.addEventListener('contextmenu', function(e) {
......@@ -99,6 +109,51 @@ Polymer({
});
},
/**
* @return {boolean}
* @private
*/
prefsAvailable: function() {
return ['custom_start_time', 'custom_end_time']
.map(key => `prefs.ash.night_light.${key}.value`)
.every(path => this.get(path) != undefined);
},
/** @private */
updateMarkers_: function() {
if (!this.isReady_ || !this.prefsAvailable())
return;
const startHour =
/** @type {number} */ (
this.getPref('ash.night_light.custom_start_time').value) /
60.0;
const endHour = /** @type {number} */ (
this.getPref('ash.night_light.custom_end_time').value) /
60.0;
const markersContainer = this.$.markersContainer;
markersContainer.innerHTML = '';
for (let i = 0; i <= HOURS_PER_DAY; ++i) {
const marker = document.createElement('div');
const hourIndex = this.isRTL_ ? 24 - i : i;
// Rotate around clock by 18 hours for the 6pm start.
const hour = (hourIndex + 18) % 24;
if (startHour < endHour) {
marker.className = hour > startHour && hour < endHour ?
'active-marker' :
'inactive-marker';
} else {
marker.className = hour > endHour && hour < startHour ?
'inactive-marker' :
'active-marker';
}
markersContainer.appendChild(marker);
marker.style.left = (i * 100 / HOURS_PER_DAY) + '%';
}
},
/**
* Invoked when the element is resized and the knobs positions need to be
* updated.
......@@ -130,24 +185,6 @@ Polymer({
return 'left: ' + percent + '%';
},
/**
* Expands or un-expands the knob being dragged along with its corresponding
* label bubble.
* @param {boolean} expand True to expand, and false to un-expand.
* @private
*/
setExpanded_: function(expand) {
let knob = this.$.startKnob;
let label = this.$.startLabel;
if (this.dragObject_ == this.$.endKnob) {
knob = this.$.endKnob;
label = this.$.endLabel;
}
knob.classList.toggle('expanded-knob', expand);
label.classList.toggle('expanded-knob', expand);
},
/**
* If one of the two knobs is focused, this function blurs it.
* @private
......@@ -160,6 +197,7 @@ Polymer({
/**
* Start dragging the target knob.
* @param {!Event} event
* @private
*/
startDrag_: function(event) {
......@@ -177,19 +215,14 @@ Polymer({
return;
}
this.setExpanded_(true);
// Focus is only given to the knobs by means of keyboard tab navigations.
// When we start dragging, we don't want to see any focus halos around any
// knob.
this.blurAnyFocusedKnob_();
this.handleKnobEvent_(event, this.dragObject_);
// However, our night-light-slider element must get the focus.
this.focus();
this.valueAtDragStart_ = this.getPrefValue_(this.dragObject_);
},
/**
* Continues dragging the selected knob if any.
* @param {!Event} event
* @private
*/
continueDrag_: function(event) {
......@@ -210,41 +243,48 @@ Polymer({
}
},
/**
* Converts horizontal pixels into number of minutes.
* @param {number} deltaX
* @return {number}
* @private
*/
getDeltaMinutes_: function(deltaX) {
return (this.isRTL_ ? -1 : 1) *
Math.floor(
TOTAL_MINUTES_PER_DAY * deltaX / this.$.sliderBar.offsetWidth);
},
/**
* Updates the knob's corresponding pref value in response to dragging, which
* will in turn update the location of the knob and its corresponding label
* bubble and its text contents.
* @param {!Event} event
* @private
*/
doKnobTracking_: function(event) {
const deltaRatio =
Math.abs(event.detail.ddx) / this.$.sliderBar.offsetWidth;
const deltaMinutes = Math.floor(deltaRatio * TOTAL_MINUTES_PER_DAY);
if (deltaMinutes <= 0)
const lastDeltaMinutes = this.getDeltaMinutes_(event.detail.ddx);
if (Math.abs(lastDeltaMinutes) < 1)
return;
const knobPref = this.dragObject_ == this.$.startKnob ?
'ash.night_light.custom_start_time' :
'ash.night_light.custom_end_time';
const ddx = this.isRTL_ ? event.detail.ddx * -1 : event.detail.ddx;
if (ddx > 0) {
// Increment the knob's pref by the amount of deltaMinutes.
this.incrementPref_(knobPref, deltaMinutes);
} else {
// Decrement the knob's pref by the amount of deltaMinutes.
this.decrementPref_(knobPref, deltaMinutes);
}
// Using |ddx| to compute the delta minutes and adding that to the current
// value will result in a rounding error for every update. The cursor will
// drift away from the knob. Storing the original value and calculating the
// delta minutes from |dx| will provide a stable update that will not lose
// pixel movement due to rounding.
this.updatePref_(
this.valueAtDragStart_ + this.getDeltaMinutes_(event.detail.dx), true);
},
/**
* Ends the dragging.
* @param {!Event} event
* @private
*/
endDrag_: function(event) {
event.preventDefault();
this.setExpanded_(false);
this.dragObject_ = null;
this.removeRipple_();
},
/**
......@@ -300,6 +340,8 @@ Polymer({
* @private
*/
updateKnobs_: function() {
if (!this.isReady_ || !this.prefsAvailable())
return;
const startOffsetMinutes = /** @type {number} */ (
this.getPref('ash.night_light.custom_start_time').value);
this.updateKnobLeft_(this.$.startKnob, startOffsetMinutes);
......@@ -414,118 +456,81 @@ Polymer({
},
/**
* Increments the value of the pref whose path is given by |prefPath| by the
* amount given in |increment|.
* @param {string} prefPath
* @param {number} increment
* Updates the value of the pref and wraps around if necessary.
*
* When the |updatedValue| would put the start and end times closer than the
* minimum distance, the |updatedValue| is changed to maintain the minimum
* distance.
*
* When |fromUserGesture| is true the update source is from a pointer such as
* a mouse, touch or pen. When the knobs are close, the dragging knob will
* stay on the same side with respect to the other knob. For example, when the
* minimum distance is 1 hour, the start knob is at 8:30 am, and the end knob
* is at 7:00, let's examine what happens if the start knob is dragged past
* the end knob. At first the start knob values will change past 8:20 and
* 8:10, all the way up to 8:00. Further movements in the same direction will
* not change the start knob value until the pointer crosses past the end knob
* (modulo the bar width). At that point, the start knob value will be updated
* to 6:00 and remain at 6:00 until the pointer passes the 6:00 location.
*
* When |fromUserGesture| is false, the input is coming from a key event. As
* soon as the |updatedValue| is closer than the minimum distance, the knob
* is moved to the other side of the other knob. For example, with a minimum
* distance of 1 hour, the start knob is at 8:00 am, and the end knob is at
* 7:00, if the start knob value is decreased, then the start knob will be
* updated to 6:00.
* @param {number} updatedValue
* @param {boolean} fromUserGesture
* @private
*/
incrementPref_: function(prefPath, increment) {
let value = this.getPref(prefPath).value + increment;
updatePref_: function(updatedValue, fromUserGesture) {
const prefPath = assert(this.getFocusedKnobPrefPathIfAny_());
const otherValue = this.getOtherKnobPrefValue_(prefPath);
if (otherValue > value &&
((otherValue - value) < MIN_KNOBS_DISTANCE_MINUTES)) {
// We are incrementing the minutes offset moving towards the other knob.
// We have a minimum 60 minutes overlap threshold. Move this knob to the
// other side of the other knob.
//
// Was:
// ------ (+) --- 59 MIN --- + ------->>
//
// Now:
// ------ + --- 60 MIN --- (+) ------->>
//
// (+) ==> Knob being moved.
value = otherValue + MIN_KNOBS_DISTANCE_MINUTES;
}
// The knobs are allowed to wrap around.
this.setPrefValue(prefPath, (value % TOTAL_MINUTES_PER_DAY));
},
/**
* Decrements the value of the pref whose path is given by |prefPath| by the
* amount given in |decrement|.
* @param {string} prefPath
* @param {number} decrement
* @private
*/
decrementPref_: function(prefPath, decrement) {
let value =
/** @type {number} */ (this.getPref(prefPath).value) - decrement;
const otherValue = this.getOtherKnobPrefValue_(prefPath);
if (value > otherValue &&
((value - otherValue) < MIN_KNOBS_DISTANCE_MINUTES)) {
// We are decrementing the minutes offset moving towards the other knob.
// We have a minimum 60 minutes overlap threshold. Move this knob to the
// other side of the other knob.
//
// Was:
// <<------ + --- 59 MIN --- (+) -------
//
// Now:
// <<------ (+) --- 60 MIN --- + ------
//
// (+) ==> Knob being moved.
value = otherValue - MIN_KNOBS_DISTANCE_MINUTES;
}
const totalMinutes = TOTAL_MINUTES_PER_DAY;
const minDistance = MIN_KNOBS_DISTANCE_MINUTES;
if (modulo(otherValue - updatedValue, totalMinutes) < minDistance)
updatedValue = otherValue + (fromUserGesture ? -1 : 1) * minDistance;
else if (modulo(updatedValue - otherValue, totalMinutes) < minDistance)
updatedValue = otherValue + (fromUserGesture ? 1 : -1) * minDistance;
// The knobs are allowed to wrap around.
if (value < 0)
value += TOTAL_MINUTES_PER_DAY;
this.setPrefValue(prefPath, Math.abs(value) % TOTAL_MINUTES_PER_DAY);
this.setPrefValue(prefPath, modulo(updatedValue, TOTAL_MINUTES_PER_DAY));
},
/**
* Gets the pref path of the currently focused knob. Returns null if no knob
* is currently focused.
* @return {?string}
* @param {Element} knob
* @returns {?string}
* @private
*/
getFocusedKnobPrefPathIfAny_: function() {
const focusedElement = this.shadowRoot.activeElement;
if (focusedElement == this.$.startKnob)
getPrefPath_: function(knob) {
if (knob == this.$.startKnob)
return 'ash.night_light.custom_start_time';
if (focusedElement == this.$.endKnob)
if (knob == this.$.endKnob)
return 'ash.night_light.custom_end_time';
return null;
},
/**
* Handles the 'left' key event.
* @param {Element} knob
* @returns {?number}
* @private
*/
onLeftKey_: function(e) {
e.preventDefault();
const knobPref = this.getFocusedKnobPrefPathIfAny_();
if (!knobPref)
return;
if (this.isRTL_)
this.incrementPref_(knobPref, 1);
else
this.decrementPref_(knobPref, 1);
getPrefValue_: function(knob) {
const path = this.getPrefPath_(knob);
return path ? /** @type {number} */ (this.getPref(path).value) : null;
},
/**
* Handles the 'right' key event.
* Gets the pref path of the currently focused knob. Returns null if no knob
* is currently focused.
* @return {?string}
* @private
*/
onRightKey_: function(e) {
e.preventDefault();
const knobPref = this.getFocusedKnobPrefPathIfAny_();
if (!knobPref)
return;
if (this.isRTL_)
this.decrementPref_(knobPref, 1);
else
this.incrementPref_(knobPref, 1);
getFocusedKnobPrefPathIfAny_: function() {
return this.getPrefPath_(this.shadowRoot.activeElement);
},
/**
......@@ -558,10 +563,34 @@ Polymer({
},
/**
* Handles focus events on the start and end knobs.
* @param {!Event} event
* @private
*/
onFocus_: function() {
onFocus_: function(event) {
this.handleKnobEvent_(event);
},
/**
* Handles focus, drag and key events on the start and end knobs.
* If |overrideElement| is provided, it will be the knob that gains focus and
* and the ripple. Otherwise, the knob is determined from the |event|.
* @param {!Event} event
* @param {Element=} overrideElement
* @private
*/
handleKnobEvent_: function(event, overrideElement) {
const knob = overrideElement ||
event.path.find(el => el.classList && el.classList.contains('knob'));
if (!knob) {
event.preventDefault();
return;
}
if (this._rippleContainer != knob) {
this.removeRipple_();
knob.focus();
}
this.ensureRipple();
if (this.hasRipple()) {
......@@ -575,12 +604,65 @@ Polymer({
* @private
*/
onBlur_: function() {
this.removeRipple_();
},
/**
* Removes ripple if one exists.
* @private
*/
removeRipple_: function() {
if (this.hasRipple()) {
this._ripple.remove();
this._ripple = null;
}
},
/**
* @param {!Event} event
* @private
*/
onKeyDown_: function(event) {
const activeElement = this.shadowRoot.activeElement;
if (event.key == 'Tab') {
if (event.shiftKey && this.$.endKnob == activeElement) {
event.preventDefault();
this.handleKnobEvent_(event, this.$.startKnob);
return;
}
if (!event.shiftKey && this.$.startKnob == activeElement) {
event.preventDefault();
this.handleKnobEvent_(event, this.$.endKnob);
}
return;
}
if (event.metaKey || event.shiftKey || event.altKey || event.ctrlKey)
return;
const deltaKeyMap = {
ArrowDown: -1,
ArrowLeft: this.isRTL_ ? 1 : -1,
ArrowRight: this.isRTL_ ? -1 : 1,
ArrowUp: 1,
PageDown: -15,
PageUp: 15,
};
if (event.key in deltaKeyMap) {
this.handleKnobEvent_(event);
event.preventDefault();
const value = this.getPrefValue_(activeElement);
if (value == null)
return;
const delta = deltaKeyMap[event.key];
this.updatePref_(value + delta, false);
}
},
/** @private */
_focusedChanged: function(receivedFocusFromKeyboard) {
// Overrides the _focusedChanged() from the PaperInkyFocusBehavior so that
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment