Commit d3fb2e99 authored by Tibor Goldschwendt's avatar Tibor Goldschwendt Committed by Commit Bot

[webui-ntp] Move grid of themes into custom element to be reused later

This prepares for reusing the grid for the backgrounds settings. Also,
this CL adds support for non-full (i.e. the last row has empty spaces)
grids.

Bug: 1032328
Change-Id: Iab76c9bce52b832833d09301c560963b9ab2618c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2003294
Commit-Queue: Tibor Goldschwendt <tiborg@chromium.org>
Reviewed-by: default avatarEsmael Elmoslimany <aee@chromium.org>
Cr-Commit-Position: refs/heads/master@{#732535}
parent 556c657d
......@@ -11,6 +11,7 @@ js_type_check("closure_compile") {
":app",
":browser_proxy",
":customize_dialog",
":grid",
":theme_icon",
":utils",
]
......@@ -59,6 +60,7 @@ js_library("customize_dialog") {
js_library("customize_themes") {
deps = [
":grid",
"//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
]
}
......@@ -84,6 +86,12 @@ js_library("theme_icon") {
]
}
js_library("grid") {
deps = [
"//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
]
}
js_library("utils") {
}
......@@ -129,12 +137,19 @@ polymer_modulizer("theme_icon") {
html_type = "v3-ready"
}
polymer_modulizer("grid") {
js_file = "grid.js"
html_file = "grid.html"
html_type = "v3-ready"
}
group("polymer3_elements") {
deps = [
":app_module",
":customize_dialog_module",
":customize_shortcuts_module",
":customize_themes_module",
":grid_module",
":mini_page_module",
":most_visited_module",
":theme_icon_module",
......
......@@ -47,22 +47,11 @@
}
#themesContainer {
display: inline-grid;
grid-column-gap: 20px;
grid-row-gap: 20px;
grid-template-columns: repeat(var(--num-theme-columns), auto);
outline-width: 0;
--ntp-grid-gap: 20px;
padding: 3px;
width: fit-content;
}
:host-context(.focus-outline-visible) #themesContainer:focus {
box-shadow: inset 0 0 0 2px rgba(var(--google-blue-600-rgb), .4);
}
#themesContainer > * {
align-self: center;
justify-self: center;
outline-width: 0;
}
......@@ -121,8 +110,7 @@
<input id="colorPicker" type="color" on-change="onCustomFrameColorChange_"
hidden>
</input>
<div id="themesContainer" on-keydown="onThemesKeyDown_"
style="--num-theme-columns: [[numThemeColumns_]];">
<ntp-grid id="themesContainer" columns="6">
<div id="autogeneratedThemeContainer" tabindex="0"
on-click="onAutogeneratedThemeClick_">
<ntp-theme-icon id="autogeneratedTheme" title="$i18n{colorPickerLabel}"
......@@ -146,4 +134,4 @@
</ntp-theme-icon>
</template>
</dom-repeat>
</div>
</ntp-grid>
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
import './grid.js';
import './theme_icon.js';
import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
......@@ -30,13 +31,6 @@ class CustomizeThemesElement extends PolymerElement {
/** @private {!Array<!newTabPage.mojom.ChromeTheme>} */
chromeThemes_: Array,
/** @private {number} */
numThemeColumns_: {
type: Number,
readOnly: true,
value: 6,
},
};
}
......@@ -152,46 +146,6 @@ class CustomizeThemesElement extends PolymerElement {
skColorToRgb_(skColor) {
return skColorToRgb(skColor);
}
/**
* @param {!Event} e
* @private
*/
onThemesKeyDown_(e) {
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
e.preventDefault();
const themeIcons = Array.from(this.shadowRoot.querySelectorAll(
'#themesContainer > :-webkit-any(div, ntp-theme-icon)'));
const currentIndex = themeIcons.indexOf(e.target);
const isRtl = window.getComputedStyle(this)['direction'] === 'rtl';
let delta = 0;
switch (e.key) {
case 'ArrowLeft':
delta = isRtl ? 1 : -1;
break;
case 'ArrowRight':
delta = isRtl ? -1 : 1;
break;
case 'ArrowUp':
delta = -this.numThemeColumns_;
break;
case 'ArrowDown':
delta = this.numThemeColumns_;
break;
}
const mod = function(m, n) {
return ((m % n) + n) % n;
};
const newIndex = mod(currentIndex + delta, themeIcons.length);
themeIcons[newIndex].focus();
}
if (['Enter', ' '].includes(e.key)) {
e.preventDefault();
e.stopPropagation();
e.target.click();
}
}
}
customElements.define(CustomizeThemesElement.is, CustomizeThemesElement);
<style>
:host {
--ntp-grid-gap: 0px;
display: block;
}
#grid {
display: grid;
grid-column-gap: var(--ntp-grid-gap);
grid-row-gap: var(--ntp-grid-gap);
grid-template-columns: repeat(var(--columns), auto);
width: fit-content;
}
::slotted(*) {
align-self: center;
justify-self: center;
}
</style>
<div id="grid" on-keydown="onKeyDown_" style="--columns: [[columns]];">
<slot id="items"></slot>
</div>
// Copyright 2020 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 {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
// Displays children in a two-dimensional grid and supports focusing children
// with arrow keys.
class GridElement extends PolymerElement {
static get is() {
return 'ntp-grid';
}
static get template() {
return html`{__html_template__}`;
}
static get properties() {
return {
/** @type {number} */
columns: {
type: Number,
value: 1,
},
};
}
/**
* @param {!Event} e
* @private
*/
onKeyDown_(e) {
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
e.preventDefault();
const items = this.$.items.assignedElements().filter(
(el) =>
(!!(el.offsetWidth || el.offsetHeight ||
el.getClientRects().length)));
const currentIndex = items.indexOf(e.target);
const isRtl = window.getComputedStyle(this)['direction'] === 'rtl';
const bottomRowColumns = items.length % this.columns;
const direction = ['ArrowRight', 'ArrowDown'].includes(e.key) ? 1 : -1;
const inEdgeRow = direction === 1 ?
currentIndex >= items.length - bottomRowColumns :
currentIndex < this.columns;
let delta = 0;
switch (e.key) {
case 'ArrowLeft':
case 'ArrowRight':
delta = direction * (isRtl ? -1 : 1);
break;
case 'ArrowUp':
case 'ArrowDown':
delta = direction * (inEdgeRow ? bottomRowColumns : this.columns);
break;
}
// Handle cases where we move to an empty space in a non-full bottom row
// and have to jump to the next row.
if (e.key === 'ArrowUp' && inEdgeRow &&
currentIndex >= bottomRowColumns) {
delta -= this.columns;
} else if (
e.key === 'ArrowDown' && !inEdgeRow &&
currentIndex + delta >= items.length) {
delta += bottomRowColumns;
}
const mod = function(m, n) {
return ((m % n) + n) % n;
};
const newIndex = mod(currentIndex + delta, items.length);
items[newIndex].focus();
}
if (['Enter', ' '].includes(e.key)) {
e.preventDefault();
e.stopPropagation();
e.target.click();
}
}
}
customElements.define(GridElement.is, GridElement);
......@@ -36,6 +36,9 @@
<include name="IDR_NEW_TAB_PAGE_THEME_ICON_JS"
file="${root_gen_dir}/chrome/browser/resources/new_tab_page/theme_icon.js"
use_base_dir="false" type="BINDATA" compress="gzip" />
<include name="IDR_NEW_TAB_PAGE_GRID_JS"
file="${root_gen_dir}/chrome/browser/resources/new_tab_page/grid.js"
use_base_dir="false" type="BINDATA" compress="gzip" />
<include name="IDR_NEW_TAB_PAGE_ACCOUNT_CIRCLE_SVG"
file="icons/account_circle.svg" type="BINDATA" compress="gzip" />
<include name="IDR_NEW_TAB_PAGE_BRUSH_ICON_SVG"
......
// Copyright 2020 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 'chrome://new-tab-page/customize_themes.js';
import {BrowserProxy} from 'chrome://new-tab-page/browser_proxy.js';
import {assertFocus, keydown, TestProxy} from 'chrome://test/new_tab_page/test_support.js';
import {eventToPromise, flushTasks} from 'chrome://test/test_util.m.js';
suite('NewTabPageCustomizeThemesFocusTest', () => {
/** @type {!customizeThemesElement} */
let customizeThemes;
/** @type {TestProxy} */
let testProxy;
function queryThemeIcons() {
return Array.from(customizeThemes.shadowRoot.querySelectorAll(
'#themesContainer > :-webkit-any(div, ntp-theme-icon)'));
}
setup(async () => {
PolymerTest.clearBody();
testProxy = new TestProxy();
BrowserProxy.instance_ = testProxy;
const colors = {frame: {value: 0xff000000}, activeTab: {value: 0xff0000ff}};
const themes = [];
for (let i = 0; i < 10; ++i) {
themes.push({id: i, label: `theme_${i}`, colors});
}
testProxy.handler.setResultFor('getChromeThemes', Promise.resolve({
chromeThemes: themes,
}));
customizeThemes = document.createElement('ntp-customize-themes');
document.body.appendChild(customizeThemes);
await flushTasks();
});
test('right focuses right theme icon', async () => {
// Act.
const themeIcons = queryThemeIcons();
keydown(themeIcons[0], 'ArrowRight');
// Assert.
assertFocus(themeIcons[1]);
});
test('right wrap around focuses first theme icon', async () => {
// Act.
const themeIcons = queryThemeIcons();
keydown(themeIcons[themeIcons.length - 1], 'ArrowRight');
// Assert.
assertFocus(themeIcons[0]);
});
test('left focuses left theme icon', async () => {
// Act.
const themeIcons = queryThemeIcons();
keydown(themeIcons[1], 'ArrowLeft');
// Assert.
assertFocus(themeIcons[0]);
});
test('left wrap around focuses last theme icon', async () => {
// Act.
const themeIcons = queryThemeIcons();
keydown(themeIcons[0], 'ArrowLeft');
// Assert.
assertFocus(themeIcons[themeIcons.length - 1]);
});
test('right focuses left theme icon in RTL', async () => {
// Arrange.
customizeThemes.dir = 'rtl';
// Act.
const themeIcons = queryThemeIcons();
keydown(themeIcons[1], 'ArrowRight');
// Assert.
assertFocus(themeIcons[0]);
});
test('right wrap around focuses last theme icon in RTL', async () => {
// Arrange.
customizeThemes.dir = 'rtl';
// Act.
const themeIcons = queryThemeIcons();
keydown(themeIcons[0], 'ArrowRight');
// Assert.
assertFocus(themeIcons[themeIcons.length - 1]);
});
test('left focuses right theme icon in RTL', async () => {
// Arrange.
customizeThemes.dir = 'rtl';
// Act.
const themeIcons = queryThemeIcons();
keydown(themeIcons[0], 'ArrowLeft');
// Assert.
assertFocus(themeIcons[1]);
});
test('left wrap around focuses first theme icon in RTL', async () => {
// Arrange.
customizeThemes.dir = 'rtl';
// Act.
const themeIcons = queryThemeIcons();
keydown(themeIcons[themeIcons.length - 1], 'ArrowLeft');
// Assert.
assertFocus(themeIcons[0]);
});
test('down focuses below theme icon', async () => {
// Act.
const themeIcons = queryThemeIcons();
keydown(themeIcons[0], 'ArrowDown');
// Assert.
assertFocus(themeIcons[6]);
});
test('up focuses above theme icon', async () => {
// Act.
const themeIcons = queryThemeIcons();
keydown(themeIcons[6], 'ArrowUp');
// Assert.
assertFocus(themeIcons[0]);
});
test('down wrap around focuses top theme icon', async () => {
// Act.
const themeIcons = queryThemeIcons();
keydown(themeIcons[6], 'ArrowDown');
// Assert.
assertFocus(themeIcons[0]);
});
test('up wrap around focuses bottom theme icon', async () => {
// Act.
const themeIcons = queryThemeIcons();
keydown(themeIcons[0], 'ArrowDown');
// Assert.
assertFocus(themeIcons[6]);
});
test('enter clicks focused theme icon', async () => {
// Arrange.
const themeIcon = queryThemeIcons()[0];
themeIcon.focus();
const themeIconClicked = eventToPromise('click', themeIcon);
// Act.
keydown(themeIcon, 'Enter');
// Assert.
await themeIconClicked;
});
test('space clicks focused theme icon', async () => {
// Arrange.
const themeIcon = queryThemeIcons()[0];
themeIcon.focus();
const themeIconClicked = eventToPromise('click', themeIcon);
// Act.
keydown(themeIcon, ' ');
// Assert.
await themeIconClicked;
});
});
// Copyright 2020 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 'chrome://new-tab-page/grid.js';
import {assertFocus, keydown} from 'chrome://test/new_tab_page/test_support.js';
import {eventToPromise} from 'chrome://test/test_util.m.js';
suite('NewTabPageGridFocusTest', () => {
/**
* @param {number} size
* @returns {!GridElement}
*/
function createGrid(size) {
const grid = document.createElement('ntp-grid');
for (let i = 0; i < size; i++) {
const div = document.createElement('div');
div.tabIndex = '0';
div.id = i;
grid.appendChild(div);
}
grid.columns = 6;
document.body.appendChild(grid);
return grid;
}
setup(() => {
PolymerTest.clearBody();
});
test('right focuses right item', () => {
// Arrange.
const grid = createGrid(12);
// Act.
keydown(grid.children[0], 'ArrowRight');
// Assert.
assertFocus(grid.children[1]);
});
test('right wrap around focuses first item', () => {
// Arrange.
const grid = createGrid(12);
// Act.
keydown(grid.children[grid.children.length - 1], 'ArrowRight');
// Assert.
assertFocus(grid.children[0]);
});
test('left focuses left item', () => {
// Arrange.
const grid = createGrid(12);
// Act.
keydown(grid.children[1], 'ArrowLeft');
// Assert.
assertFocus(grid.children[0]);
});
test('left wrap around focuses last item', () => {
// Arrange.
const grid = createGrid(12);
// Act.
keydown(grid.children[0], 'ArrowLeft');
// Assert.
assertFocus(grid.children[grid.children.length - 1]);
});
test('right focuses left item in RTL', () => {
// Arrange.
const grid = createGrid(12);
grid.dir = 'rtl';
// Act.
keydown(grid.children[1], 'ArrowRight');
// Assert.
assertFocus(grid.children[0]);
});
test('right wrap around focuses last item in RTL', () => {
// Arrange.
const grid = createGrid(12);
grid.dir = 'rtl';
// Act.
keydown(grid.children[0], 'ArrowRight');
// Assert.
assertFocus(grid.children[grid.children.length - 1]);
});
test('left focuses right item in RTL', () => {
// Arrange.
const grid = createGrid(12);
grid.dir = 'rtl';
// Act.
keydown(grid.children[0], 'ArrowLeft');
// Assert.
assertFocus(grid.children[1]);
});
test('left wrap around focuses first item in RTL', () => {
// Arrange.
const grid = createGrid(12);
grid.dir = 'rtl';
// Act.
keydown(grid.children[grid.children.length - 1], 'ArrowLeft');
// Assert.
assertFocus(grid.children[0]);
});
const test_params = [
{
type: 'full',
size: 10,
columns: 5,
},
{
type: 'non-full',
size: 8,
columns: 5,
},
];
test_params.forEach(function(param) {
test(`down focuses below item for ${param.type} grid`, () => {
// Arrange.
const grid = createGrid(param.size);
grid.columns = param.columns;
// Act.
keydown(grid.children[0], 'ArrowDown');
// Assert.
assertFocus(grid.children[param.columns]);
});
test(`last column down focuses below item for ${param.type} grid`, () => {
// Arrange.
const grid = createGrid(param.size);
grid.columns = param.columns;
// Act.
keydown(grid.children[param.columns - 1], 'ArrowDown');
// Assert.
const focusedIndex =
(param.size % param.columns == 0 ? param.size : param.columns) - 1;
assertFocus(grid.children[focusedIndex]);
});
test(`up focuses above item for ${param.type} grid`, () => {
// Arrange.
const grid = createGrid(param.size);
grid.columns = param.columns;
// Act.
keydown(grid.children[param.columns], 'ArrowUp');
// Assert.
assertFocus(grid.children[0]);
});
test(`last column up focuses above item for ${param.type} grid`, () => {
// Arrange.
const grid = createGrid(param.size);
grid.columns = param.columns;
// Act.
keydown(grid.children[param.columns - 1], 'ArrowUp');
// Assert.
const focusedIndex =
(param.size % param.columns == 0 ? param.size : param.columns) - 1;
assertFocus(grid.children[focusedIndex]);
});
test(`down wrap around focuses top item for ${param.type} grid`, () => {
// Arrange.
const grid = createGrid(param.size);
grid.columns = param.columns;
// Act.
keydown(grid.children[param.columns], 'ArrowDown');
// Assert.
assertFocus(grid.children[0]);
});
test(`up wrap around focuses bottom item for ${param.type} grid`, () => {
// Arrange.
const grid = createGrid(param.size);
grid.columns = param.columns;
// Act.
keydown(grid.children[0], 'ArrowDown');
// Assert.
assertFocus(grid.children[param.columns]);
});
});
test('enter clicks focused item', async () => {
// Arrange.
const grid = createGrid(1);
grid.children[0].focus();
const itemClicked = eventToPromise('click', grid.children[0]);
// Act.
keydown(grid.children[0], 'Enter');
// Assert.
await itemClicked;
});
test('space clicks focused item', async () => {
// Arrange.
const grid = createGrid(1);
grid.children[0].focus();
const itemClicked = eventToPromise('click', grid.children[0]);
// Act.
keydown(grid.children[0], ' ');
// Assert.
await itemClicked;
});
});
......@@ -53,14 +53,13 @@ TEST_F('NewTabPageCustomizeDialogFocusTest', 'All', function() {
});
// eslint-disable-next-line no-var
var NewTabPageCustomizeThemesFocusTest =
class extends NewTabPageInteractiveTest {
var NewTabPageGridFocusTest = class extends NewTabPageInteractiveTest {
/** @override */
get browsePreload() {
return 'chrome://new-tab-page/test_loader.html?module=new_tab_page/customize_themes_focus_test.js';
return 'chrome://new-tab-page/test_loader.html?module=new_tab_page/grid_focus_test.js';
}
};
TEST_F('NewTabPageCustomizeThemesFocusTest', 'All', function() {
TEST_F('NewTabPageGridFocusTest', 'All', function() {
mocha.run();
});
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