Commit a9925a46 authored by John Lee's avatar John Lee Committed by Commit Bot

Tab Strip WebUI: Add placeholders in pinned tabs section

Pinned tabs are presented as a grid, with a maximum of 4 tabs stacked
vertically. This CL adds placeholder "ghost" tabs to fill in each
column where there are not enough actually pinned tabs to fill the 4
spaces.

This CL aims to create the placeholders using only HTML and CSS, with
minimum JS to avoid keeping track of a dynamic number of placeholders
and constantly re-creating and removing placeholders.

https://i.imgur.com/0NK8ikk.png

Bug: 989131
Change-Id: I511a7f82fbf4480b53a437dae4b6d9445249514a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1772227Reviewed-by: default avatarHector Carmona <hcarmona@chromium.org>
Commit-Queue: John Lee <johntlee@chromium.org>
Cr-Commit-Position: refs/heads/master@{#690954}
parent d0d3ea7b
<style> <style>
:host { :host {
border-radius: 8px; border-radius: var(--tabstrip-card-border-radius);
box-shadow: var(--tabstrip-elevation-box-shadow); box-shadow: var(--tabstrip-elevation-box-shadow);
cursor: pointer; cursor: pointer;
display: flex; display: flex;
......
...@@ -11,6 +11,27 @@ ...@@ -11,6 +11,27 @@
grid-auto-flow: column; grid-auto-flow: column;
grid-gap: 10px; grid-gap: 10px;
grid-template-rows: repeat(4, 50px); grid-template-rows: repeat(4, 50px);
margin-inline-end: 16px;
}
#pinnedTabsContainer[empty] {
display: none;
}
.ghost-pinned-tab {
background: var(--tabstrip-card-background-color);
border-radius: var(--tabstrip-card-border-radius);
box-shadow: var(--tabstrip-elevation-box-shadow);
opacity: 0.5;
}
/* The #pinnedTabsContainer can only fit a maximum of 4 pinned tabs. The
* ghost-pinned-tab elements are meant to add as placeholders if there
* are not enough actual pinned tabs to fill an entire column. Therefore,
* all ghost-pinned-tabs after the 4 * nth element should be hidden. */
.ghost-pinned-tab:nth-child(4n + 1),
.ghost-pinned-tab:nth-child(4n + 1) ~ .ghost-pinned-tab {
display: none;
} }
#tabsContainer { #tabsContainer {
...@@ -20,11 +41,11 @@ ...@@ -20,11 +41,11 @@
grid-gap: 16px; grid-gap: 16px;
grid-template-rows: 230px; grid-template-rows: 230px;
} }
#pinnedTabsContainer:not(:empty) + #tabsContainer {
margin-inline-start: 16px;
}
</style> </style>
<div id="pinnedTabsContainer"></div> <div id="pinnedTabsContainer" empty>
<div class="ghost-pinned-tab"></div>
<div class="ghost-pinned-tab"></div>
<div class="ghost-pinned-tab"></div>
</div>
<div id="tabsContainer"></div> <div id="tabsContainer"></div>
...@@ -8,6 +8,8 @@ import {CustomElement} from './custom_element.js'; ...@@ -8,6 +8,8 @@ import {CustomElement} from './custom_element.js';
import {TabElement} from './tab.js'; import {TabElement} from './tab.js';
import {TabsApiProxy} from './tabs_api_proxy.js'; import {TabsApiProxy} from './tabs_api_proxy.js';
const GHOST_PINNED_TAB_COUNT = 3;
class TabListElement extends CustomElement { class TabListElement extends CustomElement {
static get template() { static get template() {
return `{__html_template__}`; return `{__html_template__}`;
...@@ -94,15 +96,17 @@ class TabListElement extends CustomElement { ...@@ -94,15 +96,17 @@ class TabListElement extends CustomElement {
if (tabElement.tab && tabElement.tab.pinned) { if (tabElement.tab && tabElement.tab.pinned) {
this.pinnedTabsContainerElement_.insertBefore( this.pinnedTabsContainerElement_.insertBefore(
tabElement, this.pinnedTabsContainerElement_.childNodes[index]); tabElement, this.pinnedTabsContainerElement_.childNodes[index]);
return; } else {
// Pinned tabs are in their own container, so the index of non-pinned
// tabs need to be offset by the number of pinned tabs
const offsetIndex = index -
(this.pinnedTabsContainerElement_.childElementCount -
GHOST_PINNED_TAB_COUNT);
this.tabsContainerElement_.insertBefore(
tabElement, this.tabsContainerElement_.childNodes[offsetIndex]);
} }
// Pinned tabs are in their own container, so the index of non-pinned this.updatePinnedTabsState_();
// tabs need to be offset by the number of pinned tabs
const offsetIndex =
index - this.pinnedTabsContainerElement_.childElementCount;
this.tabsContainerElement_.insertBefore(
tabElement, this.tabsContainerElement_.childNodes[offsetIndex]);
} }
/** /**
...@@ -151,6 +155,7 @@ class TabListElement extends CustomElement { ...@@ -151,6 +155,7 @@ class TabListElement extends CustomElement {
const tabElement = this.findTabElement_(tabId); const tabElement = this.findTabElement_(tabId);
if (tabElement) { if (tabElement) {
tabElement.remove(); tabElement.remove();
this.updatePinnedTabsState_();
} }
} }
...@@ -176,6 +181,14 @@ class TabListElement extends CustomElement { ...@@ -176,6 +181,14 @@ class TabListElement extends CustomElement {
} }
} }
} }
/** @private */
updatePinnedTabsState_() {
this.pinnedTabsContainerElement_.toggleAttribute(
'empty',
this.pinnedTabsContainerElement_.childElementCount ===
GHOST_PINNED_TAB_COUNT);
}
} }
customElements.define('tabstrip-tab-list', TabListElement); customElements.define('tabstrip-tab-list', TabListElement);
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
--tabstrip-background-color: rgb(var(--google-grey-50-rgb)); --tabstrip-background-color: rgb(var(--google-grey-50-rgb));
--tabstrip-card-background-color: white; --tabstrip-card-background-color: white;
--tabstrip-card-border-radius: 8px;
--tabstrip-elevation-box-shadow: --tabstrip-elevation-box-shadow:
0 0 0 1px rgb(var(--google-grey-300-rgb)); 0 0 0 1px rgb(var(--google-grey-300-rgb));
--tabstrip-focus-color: rgb(var(--google-blue-500-rgb)); --tabstrip-focus-color: rgb(var(--google-blue-500-rgb));
......
...@@ -39,6 +39,12 @@ suite('TabList', () => { ...@@ -39,6 +39,12 @@ suite('TabList', () => {
], ],
}; };
function pinTabAt(tab, index) {
const changeInfo = {index: index, pinned: true};
const updatedTab = Object.assign({}, tab, changeInfo);
callbackRouter.onUpdated.dispatchEvent(tab.id, changeInfo, updatedTab);
}
function getUnpinnedTabs() { function getUnpinnedTabs() {
return tabList.shadowRoot.querySelectorAll('#tabsContainer tabstrip-tab'); return tabList.shadowRoot.querySelectorAll('#tabsContainer tabstrip-tab');
} }
...@@ -174,4 +180,61 @@ suite('TabList', () => { ...@@ -174,4 +180,61 @@ suite('TabList', () => {
assertEquals(unpinnedTabElements.length, 3); assertEquals(unpinnedTabElements.length, 3);
assertEquals(unpinnedTabElements[0].tab.id, tabToPin.id); assertEquals(unpinnedTabElements[0].tab.id, tabToPin.id);
}); });
test('updates [empty] attribute on container for pinned tabs', () => {
assertTrue(tabList.shadowRoot.querySelector('#pinnedTabsContainer')
.hasAttribute('empty'));
const tabToPin = currentWindow.tabs[1];
const changeInfo = {index: 0, pinned: true};
const updatedTab = Object.assign({}, tabToPin, changeInfo);
callbackRouter.onUpdated.dispatchEvent(tabToPin.id, changeInfo, updatedTab);
assertFalse(tabList.shadowRoot.querySelector('#pinnedTabsContainer')
.hasAttribute('empty'));
// Remove the pinned tab
callbackRouter.onRemoved.dispatchEvent(
tabToPin.id, {windowId: currentWindow.id});
assertTrue(tabList.shadowRoot.querySelector('#pinnedTabsContainer')
.hasAttribute('empty'));
});
test('shows and hides the ghost pinned tabs based on pinned tabs', () => {
const ghostPinnedTabs =
tabList.shadowRoot.querySelectorAll('.ghost-pinned-tab');
assertEquals(3, ghostPinnedTabs.length);
// all are hidden by default when there are no pinned tabs
assertEquals('none', window.getComputedStyle(ghostPinnedTabs[0]).display);
assertEquals('none', window.getComputedStyle(ghostPinnedTabs[1]).display);
assertEquals('none', window.getComputedStyle(ghostPinnedTabs[2]).display);
// all are visible because 1 pinned tabs leaves room for 3 placeholders
pinTabAt(currentWindow.tabs[0], 0);
assertEquals('block', window.getComputedStyle(ghostPinnedTabs[0]).display);
assertEquals('block', window.getComputedStyle(ghostPinnedTabs[1]).display);
assertEquals('block', window.getComputedStyle(ghostPinnedTabs[2]).display);
// only 2 are visible because 2 pinned tabs leaves room for 2 placeholders
pinTabAt(currentWindow.tabs[1], 1);
assertEquals('block', window.getComputedStyle(ghostPinnedTabs[0]).display);
assertEquals('block', window.getComputedStyle(ghostPinnedTabs[1]).display);
assertEquals('none', window.getComputedStyle(ghostPinnedTabs[2]).display);
// only 2 are visible because 3 pinned tabs leaves room for 1 placeholders
pinTabAt(currentWindow.tabs[2], 1);
assertEquals('block', window.getComputedStyle(ghostPinnedTabs[0]).display);
assertEquals('none', window.getComputedStyle(ghostPinnedTabs[1]).display);
assertEquals('none', window.getComputedStyle(ghostPinnedTabs[2]).display);
// all are hidden because 4 pinned tabs means no room for placeholders
callbackRouter.onCreated.dispatchEvent({
index: 3,
pinned: true,
title: 'New pinned tab',
windowId: currentWindow.id,
});
assertEquals('none', window.getComputedStyle(ghostPinnedTabs[0]).display);
assertEquals('none', window.getComputedStyle(ghostPinnedTabs[1]).display);
assertEquals('none', window.getComputedStyle(ghostPinnedTabs[2]).display);
});
}); });
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