Commit 3c9a4cd0 authored by Esmael El-Moslimany's avatar Esmael El-Moslimany Committed by Commit Bot

WebUI: create cr-tabs, a paper-tabs replacement

Bug: 960564, 900188
Change-Id: Ie4650b0aa9d57a9932a6b2cace8ca46a01fa4130
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1601687Reviewed-by: default avatarDemetrios Papadopoulos <dpapad@chromium.org>
Commit-Queue: Esmael El-Moslimany <aee@chromium.org>
Cr-Commit-Position: refs/heads/master@{#659257}
parent 466ad2d6
......@@ -9,16 +9,15 @@ general use and is not localized.
<link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="chrome://resources/cr_elements/hidden_style_css.html">
<link rel="import" href="chrome://resources/cr_elements/paper_tabs_style_css.html">
<link rel="import" href="chrome://resources/cr_elements/cr_tabs/cr_tabs.html">
<link rel="import" href="chrome://resources/polymer/v1_0/iron-pages/iron-pages.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-tabs/paper-tabs.html">
<link rel="import" href="database_tab.html">
<link rel="import" href="discards_tab.html">
<link rel="import" href="graph_tab.html">
<dom-module id="discards-main">
<template>
<style include="paper-tabs-style cr-hidden-style"></style>
<style include="cr-hidden-style"></style>
<style>
:host {
display: flex;
......@@ -42,11 +41,7 @@ general use and is not localized.
overflow: hidden;
}
</style>
<paper-tabs noink selected="{{selected}}">
<paper-tab>Discards</paper-tab>
<paper-tab>Database</paper-tab>
<paper-tab>Graph</paper-tab>
</paper-tabs>
<cr-tabs selected="{{selected}}" tab-names="[[tabs]]"></cr-tabs>
<iron-pages selected="[[selected]]">
<discards-tab></discards-tab>
......
......@@ -6,6 +6,14 @@ Polymer({
is: 'discards-main',
properties: {
selected: {type: Number, value: 0},
selected: {
type: Number,
value: 0,
},
tabs: {
type: Array,
value: () => ['Discards', 'Database', 'Graph'],
},
},
});
......@@ -379,6 +379,29 @@ TEST_F('CrElementsSliderTest', 'All', function() {
mocha.run();
});
/**
* @constructor
* @extends {CrElementsBrowserTest}
*/
function CrElementsTabsTest() {}
CrElementsTabsTest.prototype = {
__proto__: CrElementsBrowserTest.prototype,
/** @override */
browsePreload: 'chrome://resources/cr_elements/cr_tabs/cr_tabs.html',
/** @override */
extraLibraries: CrElementsBrowserTest.prototype.extraLibraries.concat([
'../settings/test_util.js',
'cr_tabs_test.js',
]),
};
TEST_F('CrElementsTabsTest', 'All', function() {
mocha.run();
});
/**
* @constructor
* @extends {CrElementsBrowserTest}
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
suite('cr_tabs_test', function() {
/** @type {?CrTabsElement} */
let tabs = null;
setup(() => {
PolymerTest.clearBody();
document.body.innerHTML = `<cr-tabs></cr-tabs>`;
tabs = document.querySelector('cr-tabs');
tabs.tabNames = ['tab1', 'tab2', 'tab3'];
return PolymerTest.flushTasks();
});
/**
* @param {number} index
* @return {HTMLElement}
*/
function getTabElement(index) {
return tabs.$$(`.tab:nth-of-type(${index + 1})`);
}
/**
* @param {Function} uiChange
* @param {number} initialSelection
* @param {number} expectedSelection
*/
async function checkUiChange(uiChange, initialSelection, expectedSelection) {
tabs.selected = initialSelection;
const wait = test_util.eventToPromise('selected-changed', tabs);
uiChange();
await wait;
assertEquals(expectedSelection, tabs.selected);
const tabElement = getTabElement(expectedSelection);
assertTrue(!!tabElement);
assertTrue(tabElement.classList.contains('selected'));
}
/**
* @param {string} string
* @param {number} initialSelection
* @param {number} expectedSelection
*/
async function checkKey(key, initialSelection, expectedSelection) {
await checkUiChange(
() => MockInteractions.keyDownOn(tabs, null, [], key), initialSelection,
expectedSelection);
}
/**
* @param {number} initialSelection
* @param {number} expectedSelection
*/
async function checkClickTab(initialSelection, expectedSelection) {
await checkUiChange(
() => getTabElement(expectedSelection).click(), initialSelection,
expectedSelection);
}
test('check CSS classes aria-selected for a tab', () => {
const tab = getTabElement(0);
assertEquals(1, tab.classList.length);
assertEquals('false', tab.getAttribute('aria-selected'));
tabs.selected = 0;
assertEquals(2, tab.classList.length);
assertTrue(tab.classList.contains('selected'));
assertEquals('true', tab.getAttribute('aria-selected'));
tabs.selected = 1;
assertEquals(1, tab.classList.length);
assertEquals('false', tab.getAttribute('aria-selected'));
});
test('right/left pressed, selection changes and event fires', async () => {
await checkKey('ArrowRight', 0, 1);
await checkKey('ArrowRight', 1, 2);
// Check that the selection wraps.
await checkKey('ArrowRight', 2, 0);
await checkKey('ArrowLeft', 2, 1);
await checkKey('ArrowLeft', 1, 0);
// Check that the selection wraps.
await checkKey('ArrowLeft', 0, 2);
});
test('clicking on tabs, selection changes and event fires', async () => {
await checkClickTab(0, 1);
await checkClickTab(1, 2);
await checkClickTab(2, 0);
await checkClickTab(2, 1);
await checkClickTab(1, 0);
await checkClickTab(0, 2);
});
});
......@@ -22,6 +22,7 @@ group("closure_compile") {
"cr_radio_group:closure_compile",
"cr_searchable_drop_down:closure_compile",
"cr_slider:closure_compile",
"cr_tabs:closure_compile",
"cr_toast:closure_compile",
"cr_toggle:closure_compile",
"cr_view_manager:closure_compile",
......
# Copyright 2019 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import("//third_party/closure_compiler/compile_js.gni")
js_type_check("closure_compile") {
deps = [
":cr_tabs",
]
}
js_library("cr_tabs") {
}
<link rel="import" href="../../html/polymer.html">
<link rel="import" href="../hidden_style_css.html">
<link rel="import" href="../shared_vars_css.html">
<dom-module id="cr-tabs">
<template>
<style include="cr-hidden-style">
:host {
--cr-tabs-height: 48px;
cursor: pointer;
display: flex;
flex-direction: row;
font-size: var(--cr-tabs-font-size, 14px);
font-weight: 500;
height: var(--cr-tabs-height);
outline: none;
position: relative;
user-select: none;
}
.tab {
align-items: center;
color: var(--cr-secondary-text-color);
display: flex;
flex: auto;
height: 100%;
justify-content: center;
opacity: .8;
}
.selected {
color: var(--google-blue-600);
font-weight: 700;
opacity: 1;
}
:host-context([dark]) .selected {
color: var(--google-blue-refresh-300);
}
#selectionBar {
--cr-tabs-selection-bar-width: 2px;
border-bottom-color: var(--google-blue-600);
border-bottom-style: solid;
border-bottom-width: var(--cr-tabs-selection-bar-width);
height: 0;
left: 0;
position: absolute;
right: 0;
top: calc(var(--cr-tabs-height) - var(--cr-tabs-selection-bar-width));
transform: scale(0);
transform-origin: left center;
transition: transform;
}
:host-context([dark]) #selectionBar {
border-bottom-color: var(--google-blue-refresh-300);
}
#selectionBar.expand {
transition-duration: 150ms;
transition-timing-function: cubic-bezier(.4, 0, 1, 1);
}
#selectionBar.contract {
transition-duration: 180ms;
transition-timing-function: cubic-bezier(0, 0, .2, 1);
}
</style>
<template is="dom-repeat" items="[[tabNames]]"
on-dom-change="updateSelectionBar_">
<div class$="tab [[getTabCssClass_(index, selected)]]"
role="tab" aria-selected$="[[getTabAriaSelected_(index, selected)]]"
on-click="onTabClick_">
[[item]]
</div>
</template>
<div id="selectionBar"></div>
</template>
<script src="cr_tabs.js"></script>
</dom-module>
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview 'cr-tabs' is a control used for selecting different sections or
* tabs. cr-tabs was created to replace paper-tabs and paper-tab. cr-tabs
* displays the name of each tab provided by |tabs|. A 'selected-changed' event
* is fired any time |selected| is changed.
*
* cr-tabs takes its #selectionBar animation from paper-tabs.
*
* Keyboard behavior
* - left/right changes the tab selection
* - space/enter selects the currently focused tab
*
* Known limitations
* - no "disabled" state for the cr-tabs as a whole or individual tabs
* - cr-tabs does not accept any <slot> (not necessary as of this writing)
* - no horizontal scrolling, it is assumed that tabs always fit in the
* available space
*/
Polymer({
is: 'cr-tabs',
properties: {
/**
* Tab names displayed in each tab.
* @type {!Array<string>}
*/
tabNames: {
type: Array,
value: () => [],
},
/** Index of the selected tab. */
selected: {
type: Number,
notify: true,
observer: 'updateSelectionBar_',
},
},
hostAttributes: {
role: 'tablist',
tabindex: 0,
},
listeners: {
keydown: 'onKeyDown_',
},
/** @private {boolean} */
isRtl_: false,
/** @private {?number} */
lastSelected_: null,
/** @override */
attached: function() {
this.isRtl_ = this.matches(':host-context([dir=rtl]) cr-tabs');
},
/**
* @param {number} index
* @return {string}
* @private
*/
getTabAriaSelected_: function(index) {
return this.selected == index ? 'true' : 'false';
},
/**
* @param {number} index
* @return {string}
* @private
*/
getTabCssClass_: function(index) {
return this.selected == index ? 'selected' : '';
},
/**
* @param {!KeyboardEvent} e
* @private
*/
onKeyDown_: function(e) {
if (e.key != 'ArrowLeft' && e.key != 'ArrowRight') {
return;
}
e.preventDefault();
e.stopPropagation();
const delta =
e.key == 'ArrowLeft' ? (this.isRtl_ ? 1 : -1) : (this.isRtl_ ? -1 : 1);
const count = this.tabNames.length;
this.selected = (count + this.selected + delta) % count;
},
/**
* @param {!{model: !{index: number}}} _
* @private
*/
onTabClick_: function({model: {index}}) {
this.selected = index;
},
/**
* @param {number} left
* @param {number} width
* @private
*/
transformSelectionBar_: function(left, width) {
const containerWidth = this.offsetWidth;
const leftPercent = 100 * left / containerWidth;
const widthRatio = width / containerWidth;
this.$.selectionBar.style.transform =
`translateX(${leftPercent}%) scaleX(${widthRatio})`;
},
/** @private */
updateSelectionBar_: function() {
const selectedTab = this.$$('.selected');
if (!selectedTab) {
return;
}
this.$.selectionBar.classList.remove('expand', 'contract');
const {offsetLeft: selectedLeft, offsetWidth: selectedWidth} = selectedTab;
const oldValue = this.lastSelected_;
this.lastSelected_ = this.selected;
// If there is no previously selected tab or the tab has not changed,
// underline the selected tab instantly.
if (oldValue == null || oldValue == this.selected) {
this.transformSelectionBar_(selectedLeft, selectedWidth);
return;
}
// Expand bar to underline the last selected tab, the newly selected tab and
// everything in between. After expansion is complete, contract bar to
// underline the selected tab.
this.$.selectionBar.classList.add('expand');
this.$.selectionBar.addEventListener('transitionend', () => {
this.$.selectionBar.classList.replace('expand', 'contract');
this.transformSelectionBar_(selectedLeft, selectedWidth);
}, {once: true});
const {offsetLeft: lastLeft, offsetWidth: lastWidth} =
this.$$(`.tab:nth-of-type(${oldValue + 1})`);
const left = Math.min(selectedLeft, lastLeft);
const right = Math.max(selectedLeft + selectedWidth, lastLeft + lastWidth);
this.transformSelectionBar_(left, right - left);
},
});
......@@ -388,6 +388,14 @@
file="cr_elements/shared_vars_css.html"
type="chrome_html"
compress="gzip" />
<structure name="IDR_CR_ELEMENTS_CR_TABS_HTML"
file="cr_elements/cr_tabs/cr_tabs.html"
type="chrome_html"
compress="gzip" />
<structure name="IDR_CR_ELEMENTS_CR_TABS_JS"
file="cr_elements/cr_tabs/cr_tabs.js"
type="chrome_html"
compress="gzip" />
<structure name="IDR_CR_ELEMENTS_CR_TOOLBAR_HTML"
file="cr_elements/cr_toolbar/cr_toolbar.html"
type="chrome_html"
......
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