mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-02-09 08:48:07 +00:00
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:
parent
5b45c5a120
commit
5a636ae0ec
@ -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">
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user