Bug 1301284 - Update time picker style to match visual spec; r=mconley

MozReview-Commit-ID: LqYl8qRzlBg

--HG--
extra : rebase_source : af865b9253bd2001eb468863cce13b585bb546e3
This commit is contained in:
Scott Wu 2016-10-17 17:24:39 +08:00
parent 5b45c5a120
commit 5a636ae0ec
5 changed files with 199 additions and 68 deletions

View File

@ -15,11 +15,9 @@
<div id="time-picker"></div>
<template id="spinner-template">
<div class="spinner-container">
<button class="up"></button>
<div class="stack">
<div class="spinner"></div>
</div>
<button class="down"></button>
<button class="up"/>
<div class="spinner"></div>
<button class="down"/>
</div>
</template>
<script type="application/javascript">

View File

@ -15,8 +15,8 @@
<field name="dateTimePopupFrame">
this.querySelector("#dateTimePopupFrame");
</field>
<field name="TIME_PICKER_WIDTH" readonly="true">"14em"</field>
<field name="TIME_PICKER_HEIGHT" readonly="true">"14em"</field>
<field name="TIME_PICKER_WIDTH" readonly="true">"12em"</field>
<field name="TIME_PICKER_HEIGHT" readonly="true">"21em"</field>
<method name="loadPicker">
<parameter name="type"/>
<parameter name="detail"/>
@ -24,6 +24,8 @@
this.hidden = false;
this.type = type;
this.pickerState = {};
// TODO: Resize picker according to content zoom level
this.style.fontSize = "10px";
switch (type) {
case "time": {
this.detail = detail;

View File

@ -18,11 +18,12 @@ function Spinner(props, context) {
}
{
const debug = 0 ? console.log.bind(console, '[spinner]') : function() {};
const debug = 0 ? console.log.bind(console, "[spinner]") : function() {};
const ITEM_HEIGHT = 20,
VIEWPORT_SIZE = 5,
VIEWPORT_COUNT = 5;
const ITEM_HEIGHT = 2.5,
VIEWPORT_SIZE = 7,
VIEWPORT_COUNT = 5,
SCROLL_TIMEOUT = 100;
Spinner.prototype = {
/**
@ -35,13 +36,14 @@ function Spinner(props, context) {
* the parent component.
* {Function} getDisplayString: Takes a value, and output it
* as localized strings.
* {Number} itemHeight [optional]: Item height in pixels.
* {Number} viewportSize [optional]: Number of items in a
* viewport.
* {Boolean} hideButtons [optional]: Hide up & down buttons
* {Number} rootFontSize [optional]: Used to support zoom in/out
* }
*/
_init(props) {
const { setValue, getDisplayString, itemHeight = ITEM_HEIGHT } = props;
const { setValue, getDisplayString, hideButtons, rootFontSize = 10 } = props;
const spinnerTemplate = document.getElementById("spinner-template");
const spinnerElement = document.importNode(spinnerTemplate.content, true);
@ -55,19 +57,26 @@ function Spinner(props, context) {
isScrolling: false
};
this.props = {
setValue, getDisplayString, itemHeight, viewportSize,
setValue, getDisplayString, viewportSize, rootFontSize,
// We can assume that the viewportSize is an odd number. Calculate how many
// items we need to insert on top of the spinner so that the selected is at
// the center. Ex: if viewportSize is 5, we need 2 items on top.
viewportTopOffset: (viewportSize - 1) / 2
};
this.elements = {
container: spinnerElement.querySelector(".spinner-container"),
spinner: spinnerElement.querySelector(".spinner"),
up: spinnerElement.querySelector(".up"),
down: spinnerElement.querySelector(".down"),
itemsViewElements: []
};
this.elements.spinner.style.height = (ITEM_HEIGHT * viewportSize) + "rem";
if (hideButtons) {
this.elements.container.classList.add("hide-buttons");
}
this.context.appendChild(spinnerElement);
this._attachEventListeners();
},
@ -119,7 +128,7 @@ function Spinner(props, context) {
*/
_onScroll() {
const { items, itemsView, isInfiniteScroll } = this.state;
const { viewportSize, viewportTopOffset, itemHeight } = this.props;
const { viewportSize, viewportTopOffset } = this.props;
const { spinner, itemsViewElements } = this.elements;
this.state.index = this._getIndexByOffset(spinner.scrollTop);
@ -148,6 +157,16 @@ function Spinner(props, context) {
this._scrollTo(this.state.value, true);
}
}
// Use a timer to detect if a scroll event has not fired within some time
// (defined in SCROLL_TIMEOUT). This is required because we need to hide
// highlight and hover state when user is scrolling.
clearTimeout(this.state.scrollTimer);
this.elements.spinner.classList.add("scrolling");
this.state.scrollTimer = setTimeout(() => {
this.elements.spinner.classList.remove("scrolling");
this.elements.spinner.dispatchEvent(new CustomEvent("ScrollStop"));
}, SCROLL_TIMEOUT);
},
/**
@ -191,13 +210,21 @@ function Spinner(props, context) {
*/
_prepareNodes(length, parent) {
const diff = length - parent.childElementCount;
let count = Math.abs(diff);
if (!diff) {
return;
}
if (diff > 0) {
// Add more elements if length is greater than current
let frag = document.createDocumentFragment();
for (let i = 0; i < count; i++) {
// Remove margin bottom on the last element before appending
if (parent.lastChild) {
parent.lastChild.style.marginBottom = "";
}
for (let i = 0; i < diff; i++) {
let el = document.createElement("div");
frag.appendChild(el);
this.elements.itemsViewElements.push(el);
@ -205,11 +232,14 @@ function Spinner(props, context) {
parent.appendChild(frag);
} else if (diff < 0) {
// Remove elements if length is less than current
for (let i = 0; i < count; i++) {
for (let i = 0; i < Math.abs(diff); i++) {
parent.removeChild(parent.lastChild);
}
this.elements.itemsViewElements.splice(diff);
}
parent.lastChild.style.marginBottom =
(ITEM_HEIGHT * this.props.viewportTopOffset) + "rem";
},
/**
@ -260,20 +290,24 @@ function Spinner(props, context) {
case "mousedown": {
// Use preventDefault to keep focus on input boxes
event.preventDefault();
event.target.setCapture();
this.state.mouseState = {
down: true,
layerX: event.layerX,
layerY: event.layerY
};
if (event.target == up) {
// An "active" class is needed to simulate :active pseudo-class
// because element is not focused.
event.target.classList.add("active");
this._smoothScrollToIndex(index + 1);
}
if (event.target == down) {
event.target.classList.add("active");
this._smoothScrollToIndex(index - 1);
}
if (event.target.parentNode == spinner) {
// Listen to dragging events
event.target.setCapture();
spinner.addEventListener("mousemove", this, { passive: true });
spinner.addEventListener("mouseleave", this, { passive: true });
}
@ -281,6 +315,9 @@ function Spinner(props, context) {
}
case "mouseup": {
this.state.mouseState.down = false;
if (event.target == up || event.target == down) {
event.target.classList.remove("active");
}
if (event.target.parentNode == spinner) {
// Check if user clicks or drags, scroll to the item if clicked,
// otherwise get the current index and smooth scroll there.
@ -326,7 +363,7 @@ function Spinner(props, context) {
* @return {Number} Index number
*/
_getIndexByOffset(offset) {
return Math.round(offset / this.props.itemHeight);
return Math.round(offset / (ITEM_HEIGHT * this.props.rootFontSize));
},
/**
@ -381,7 +418,7 @@ function Spinner(props, context) {
// Do nothing if the value is not found
if (index > -1) {
this.state.index = index;
this.elements.spinner.scrollTop = this.state.index * this.props.itemHeight;
this.elements.spinner.scrollTop = this.state.index * ITEM_HEIGHT * this.props.rootFontSize;
}
},

View File

@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
"use strict";
function TimePicker(context) {
this.context = context;
@ -10,7 +10,7 @@ function TimePicker(context) {
}
{
const debug = 0 ? console.log.bind(console, '[timepicker]') : function() {};
const debug = 0 ? console.log.bind(console, "[timepicker]") : function() {};
const DAY_PERIOD_IN_HOURS = 12,
SECOND_IN_MS = 1000,
@ -61,9 +61,6 @@ function TimePicker(context) {
timeKeeper.setState({ hour: timerHour, minute: timerMinute });
this.state = { timeKeeper };
// TODO: Resize picker based on zoom level
document.documentElement.style.fontSize = "10px";
},
/**
@ -118,6 +115,13 @@ function TimePicker(context) {
}, this.context)
};
this._insertLayoutElement({
tag: "div",
textContent: ":",
className: "colon",
insertBefore: this.components.minute.elements.container
});
// The AM/PM spinner is only available in 12hr mode
// TODO: Replace AM & PM string with localized string
if (format == "12") {
@ -126,11 +130,36 @@ function TimePicker(context) {
timeKeeper.setDayPeriod(value);
this.state.isDayPeriodSet = true;
}),
getDisplayString: dayPeriod => dayPeriod == 0 ? "AM" : "PM"
getDisplayString: dayPeriod => dayPeriod == 0 ? "AM" : "PM",
hideButtons: true
}, this.context);
this._insertLayoutElement({
tag: "div",
className: "spacer",
insertBefore: this.components.dayPeriod.elements.container
});
}
},
/**
* Insert element for layout purposes.
*
* @param {Object}
* {
* {String} tag: The tag to create
* {DOMElement} insertBefore: The DOM node to insert before
* {String} className [optional]: Class name
* {String} textContent [optional]: Text content
* }
*/
_insertLayoutElement({ tag, insertBefore, className, textContent }) {
let el = document.createElement(tag);
el.textContent = textContent;
el.className = className;
this.context.insertBefore(el, insertBefore);
},
/**
* Set component states.
*/
@ -188,7 +217,7 @@ function TimePicker(context) {
}, "*");
},
_attachEventListeners() {
window.addEventListener('message', this);
window.addEventListener("message", this);
},
/**

View File

@ -3,26 +3,45 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
:root {
--font-size: 1.2rem;
--spinner-item-height: 2rem;
--spinner-width: 5rem;
--spinner-height: 10rem;
--scroller-width: 1.5rem;
--disabled-color: #ccc;
--selected-color: #fff;
--selected-bgcolor: #83BFFC;
--hover-bgcolor: #aaa;
--hover-outline: #999;
--font-size-default: 1.1rem;
--spinner-width: 3rem;
--spinner-margin-top-bottom: 0.4rem;
--spinner-item-height: 2.4rem;
--spinner-item-margin-bottom: 0.1rem;
--spinner-button-height: 1.2rem;
--colon-width: 2rem;
--day-period-spacing-width: 1rem;
--border: 0.1rem solid #D6D6D6;
--border-radius: 0.3rem;
--font-color: #191919;
--fill-color: #EBEBEB;
--selected-font-color: #FFFFFF;
--selected-fill-color: #0996F8;
--button-font-color: #858585;
--button-font-color-hover: #4D4D4D;
--button-font-color-active: #191919;
--disabled-opacity: 0.2;
}
html {
font-size: 10px;
}
body {
margin: 0;
font-size: var(--font-size);
color: var(--font-color);
font-size: var(--font-size-default);
}
#time-picker {
display: flex;
flex-direction: row;
justify-content: space-around;
}
.spinner-container {
@ -32,57 +51,103 @@ body {
width: var(--spinner-width);
}
.spinner-container button {
.spinner-container > button {
-moz-appearance: none;
border: none;
background: none;
height: var(--spinner-item-height);
background-color: var(--button-font-color);
height: var(--spinner-button-height);
}
.spinner-container .stack {
.spinner-container > button:hover {
background-color: var(--button-font-color-hover);
}
.spinner-container > button.active {
background-color: var(--button-font-color-active);
}
.spinner-container > button.up {
mask: url("chrome://global/skin/icons/find-arrows.svg#glyph-find-previous") no-repeat 50% 50%;
}
.spinner-container > button.down {
mask: url("chrome://global/skin/icons/find-arrows.svg#glyph-find-next") no-repeat 50% 50%;
}
.spinner-container.hide-buttons > button {
visibility: hidden;
}
.spinner-container > .spinner {
position: relative;
height: var(--spinner-height);
}
.spinner-container .spinner {
position: absolute;
height: var(--spinner-height);
width: 100%;
margin: var(--spinner-margin-top-bottom) 0;
cursor: default;
overflow-y: scroll;
scroll-snap-type: mandatory;
scroll-snap-points-y: repeat(100%);
}
.spinner-container .spinner > div {
.spinner-container > .spinner > div {
box-sizing: border-box;
position: relative;
text-align: center;
padding: calc(var(--spinner-item-height) / 4) 0;
height: calc(var(--spinner-item-height) / 2);
padding: calc((var(--spinner-item-height) - var(--font-size-default)) / 2) 0;
margin-bottom: var(--spinner-item-margin-bottom);
height: var(--spinner-item-height);
-moz-user-select: none;
scroll-snap-coordinate: 0 0;
}
.spinner-container .spinner > div:last-child {
margin-bottom: calc(var(--spinner-item-height) * 2);
}
.spinner-container .spinner > div.selection {
color: var(--selected-color);
}
.spinner-container .spinner > div.selection::before {
.spinner-container > .spinner > div:hover::before {
background: var(--fill-color);
border: var(--border);
border-radius: var(--border-radius);
content: "";
background: var(--selected-bgcolor);
position: absolute;
top: 5%;
bottom: 5%;
left: 10%;
right: 10%;
border-radius: 5%;
top: 0%;
bottom: 0%;
left: 0%;
right: 0%;
z-index: -10;
}
.spinner-container .spinner > div.disabled {
color: var(--disabled-color);
.spinner-container > .spinner:not(.scrolling) > div.selection {
color: var(--selected-font-color);
}
.spinner-container > .spinner > div.selection::before {
background: var(--selected-fill-color);
border: none;
border-radius: var(--border-radius);
content: "";
position: absolute;
top: 0%;
bottom: 0%;
left: 0%;
right: 0%;
z-index: -10;
}
.spinner-container > .spinner > div.disabled::before,
.spinner-container > .spinner.scrolling > div.selection::before,
.spinner-container > .spinner.scrolling > div:hover::before {
display: none;
}
.spinner-container > .spinner > div.disabled {
opacity: var(--disabled-opacity);
}
.colon {
display: flex;
justify-content: center;
align-items: center;
width: var(--colon-width);
margin-bottom: 0.3rem;
}
.spacer {
width: var(--day-period-spacing-width);
}