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

Tab Strip WebUI: Add sliding in and out animations

Bug: 989131
Change-Id: I152d509958df396b667bd54da5a75eb056f10589
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1778597
Commit-Queue: John Lee <johntlee@chromium.org>
Reviewed-by: default avatarEsmael Elmoslimany <aee@chromium.org>
Cr-Commit-Position: refs/heads/master@{#693405}
parent 93d392f9
...@@ -5,8 +5,9 @@ ...@@ -5,8 +5,9 @@
cursor: pointer; cursor: pointer;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 230px;
overflow: hidden; overflow: hidden;
transition: box-shadow 125ms; width: 280px;
} }
:host([active]) { :host([active]) {
...@@ -72,6 +73,11 @@ ...@@ -72,6 +73,11 @@
} }
/* Pinned tab styles */ /* Pinned tab styles */
:host([pinned]) {
height: 50px;
width: 50px;
}
:host([pinned]) #title { :host([pinned]) #title {
border-block-end: 0; border-block-end: 0;
height: 100%; height: 100%;
......
...@@ -7,6 +7,8 @@ import {getFavicon, getFaviconForPageURL} from 'chrome://resources/js/icon.m.js' ...@@ -7,6 +7,8 @@ import {getFavicon, getFaviconForPageURL} from 'chrome://resources/js/icon.m.js'
import {CustomElement} from './custom_element.js'; import {CustomElement} from './custom_element.js';
import {TabsApiProxy} from './tabs_api_proxy.js'; import {TabsApiProxy} from './tabs_api_proxy.js';
export const DEFAULT_ANIMATION_DURATION = 125;
export class TabElement extends CustomElement { export class TabElement extends CustomElement {
static get template() { static get template() {
return `{__html_template__}`; return `{__html_template__}`;
...@@ -86,6 +88,45 @@ export class TabElement extends CustomElement { ...@@ -86,6 +88,45 @@ export class TabElement extends CustomElement {
event.stopPropagation(); event.stopPropagation();
this.tabsApi_.closeTab(this.tab_.id); this.tabsApi_.closeTab(this.tab_.id);
} }
/**
* @return {!Promise}
*/
slideIn() {
return new Promise(resolve => {
const animation = this.animate(
[
{maxWidth: 0, opacity: 0},
{maxWidth: '280px', opacity: 1},
],
{
duration: DEFAULT_ANIMATION_DURATION,
fill: 'forwards',
});
animation.onfinish = resolve;
});
}
/**
* @return {!Promise}
*/
slideOut() {
return new Promise(resolve => {
const animation = this.animate(
[
{maxWidth: '280px', opacity: 1},
{maxWidth: 0, opacity: 0},
],
{
duration: DEFAULT_ANIMATION_DURATION,
fill: 'forwards',
});
animation.onfinish = () => {
this.remove();
resolve();
};
});
}
} }
customElements.define('tabstrip-tab', TabElement); customElements.define('tabstrip-tab', TabElement);
...@@ -35,11 +35,11 @@ ...@@ -35,11 +35,11 @@
} }
#tabsContainer { #tabsContainer {
display: grid; display: flex;
grid-auto-columns: 280px; }
grid-auto-flow: column;
grid-gap: 16px; #tabsContainer tabstrip-tab:not(:last-child) {
grid-template-rows: 230px; margin-inline-end: 16px;
} }
</style> </style>
......
...@@ -18,6 +18,15 @@ class TabListElement extends CustomElement { ...@@ -18,6 +18,15 @@ class TabListElement extends CustomElement {
constructor() { constructor() {
super(); super();
/**
* A chain of promises that the tab list needs to keep track of. The chain
* is useful in cases when the list needs to wait for all animations to
* finish in order to get accurate pixels (such as getting the position of a
* tab) or accurate element counts.
* @type {!Promise}
*/
this.animationPromises = Promise.resolve();
/** @private {!Element} */ /** @private {!Element} */
this.pinnedTabsContainerElement_ = this.pinnedTabsContainerElement_ =
/** @type {!Element} */ ( /** @type {!Element} */ (
...@@ -38,6 +47,14 @@ class TabListElement extends CustomElement { ...@@ -38,6 +47,14 @@ class TabListElement extends CustomElement {
this.windowId_; this.windowId_;
} }
/**
* @param {!Promise} promise
* @private
*/
addAnimationPromise_(promise) {
this.animationPromises = this.animationPromises.then(() => promise);
}
connectedCallback() { connectedCallback() {
this.tabsApi_.getCurrentWindow().then((currentWindow) => { this.tabsApi_.getCurrentWindow().then((currentWindow) => {
this.windowId_ = currentWindow.id; this.windowId_ = currentWindow.id;
...@@ -140,7 +157,9 @@ class TabListElement extends CustomElement { ...@@ -140,7 +157,9 @@ class TabListElement extends CustomElement {
return; return;
} }
this.insertTabOrMoveTo_(this.createTabElement_(tab), tab.index); const tabElement = this.createTabElement_(tab);
this.insertTabOrMoveTo_(tabElement, tab.index);
this.addAnimationPromise_(tabElement.slideIn());
} }
/** /**
...@@ -171,8 +190,11 @@ class TabListElement extends CustomElement { ...@@ -171,8 +190,11 @@ class TabListElement extends CustomElement {
const tabElement = this.findTabElement_(tabId); const tabElement = this.findTabElement_(tabId);
if (tabElement) { if (tabElement) {
tabElement.remove(); this.addAnimationPromise_(new Promise(async resolve => {
this.updatePinnedTabsState_(); await tabElement.slideOut();
this.updatePinnedTabsState_();
resolve();
}));
} }
} }
......
...@@ -65,6 +65,7 @@ suite('TabList', () => { ...@@ -65,6 +65,7 @@ suite('TabList', () => {
tabList = document.createElement('tabstrip-tab-list'); tabList = document.createElement('tabstrip-tab-list');
document.body.appendChild(tabList); document.body.appendChild(tabList);
return testTabsApiProxy.whenCalled('getCurrentWindow'); return testTabsApiProxy.whenCalled('getCurrentWindow');
}); });
...@@ -114,13 +115,13 @@ suite('TabList', () => { ...@@ -114,13 +115,13 @@ suite('TabList', () => {
assertEquals(currentWindow.tabs.length, tabElements.length); assertEquals(currentWindow.tabs.length, tabElements.length);
}); });
test('removes a tab when tab is removed from current window', () => { test('removes a tab when tab is removed from current window', async () => {
const tabToRemove = currentWindow.tabs[0]; const tabToRemove = currentWindow.tabs[0];
callbackRouter.onRemoved.dispatchEvent(tabToRemove.id, { callbackRouter.onRemoved.dispatchEvent(tabToRemove.id, {
windowId: currentWindow.id, windowId: currentWindow.id,
}); });
const tabElements = getUnpinnedTabs(); await tabList.animationPromises;
assertEquals(currentWindow.tabs.length - 1, tabElements.length); assertEquals(currentWindow.tabs.length - 1, getUnpinnedTabs().length);
}); });
test('updates a tab with new tab data when a tab is updated', () => { test('updates a tab with new tab data when a tab is updated', () => {
...@@ -181,7 +182,7 @@ suite('TabList', () => { ...@@ -181,7 +182,7 @@ suite('TabList', () => {
assertEquals(unpinnedTabElements[0].tab.id, tabToPin.id); assertEquals(unpinnedTabElements[0].tab.id, tabToPin.id);
}); });
test('updates [empty] attribute on container for pinned tabs', () => { test('updates [empty] attribute on container for pinned tabs', async () => {
assertTrue(tabList.shadowRoot.querySelector('#pinnedTabsContainer') assertTrue(tabList.shadowRoot.querySelector('#pinnedTabsContainer')
.hasAttribute('empty')); .hasAttribute('empty'));
const tabToPin = currentWindow.tabs[1]; const tabToPin = currentWindow.tabs[1];
...@@ -194,6 +195,7 @@ suite('TabList', () => { ...@@ -194,6 +195,7 @@ suite('TabList', () => {
// Remove the pinned tab // Remove the pinned tab
callbackRouter.onRemoved.dispatchEvent( callbackRouter.onRemoved.dispatchEvent(
tabToPin.id, {windowId: currentWindow.id}); tabToPin.id, {windowId: currentWindow.id});
await tabList.animationPromises;
assertTrue(tabList.shadowRoot.querySelector('#pinnedTabsContainer') assertTrue(tabList.shadowRoot.querySelector('#pinnedTabsContainer')
.hasAttribute('empty')); .hasAttribute('empty'));
}); });
......
...@@ -6,7 +6,6 @@ import 'chrome://tab-strip/tab.js'; ...@@ -6,7 +6,6 @@ import 'chrome://tab-strip/tab.js';
import {getFavicon, getFaviconForPageURL} from 'chrome://resources/js/icon.m.js'; import {getFavicon, getFaviconForPageURL} from 'chrome://resources/js/icon.m.js';
import {TabsApiProxy} from 'chrome://tab-strip/tabs_api_proxy.js'; import {TabsApiProxy} from 'chrome://tab-strip/tabs_api_proxy.js';
import {TestTabsApiProxy} from './test_tabs_api_proxy.js'; import {TestTabsApiProxy} from './test_tabs_api_proxy.js';
suite('Tab', function() { suite('Tab', function() {
...@@ -29,6 +28,27 @@ suite('Tab', function() { ...@@ -29,6 +28,27 @@ suite('Tab', function() {
document.body.appendChild(tabElement); document.body.appendChild(tabElement);
}); });
test('slideIn animates in the element', async () => {
const animationPromise = tabElement.slideIn();
// Before animation completes
assertEquals('0', window.getComputedStyle(tabElement).opacity);
assertEquals('0px', window.getComputedStyle(tabElement).maxWidth);
await animationPromise;
// After animation completes
assertEquals('1', window.getComputedStyle(tabElement).opacity);
assertEquals('280px', window.getComputedStyle(tabElement).maxWidth);
});
test('slideOut animates out the element', async () => {
const animationPromise = tabElement.slideOut();
// Before animation completes
assertEquals('1', window.getComputedStyle(tabElement).opacity);
assertEquals('280px', window.getComputedStyle(tabElement).maxWidth);
await animationPromise;
// After animation completes
assertFalse(document.body.contains(tabElement));
});
test('toggles an [active] attribute when active', () => { test('toggles an [active] attribute when active', () => {
tabElement.tab = Object.assign({}, tab, {active: true}); tabElement.tab = Object.assign({}, tab, {active: true});
assertTrue(tabElement.hasAttribute('active')); assertTrue(tabElement.hasAttribute('active'));
......
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