Skip to content

Commit

Permalink
Replace mwc-tab-bar with custom tab bar (google#159)
Browse files Browse the repository at this point in the history
* Run latest prettier format
* Remove unused imports
* Rename lib/ to internal/
* Add playground-internal-tab and bar
* Remove mwc-tab dependency
  • Loading branch information
aomarks authored Jun 9, 2021
1 parent f659973 commit dab68e5
Show file tree
Hide file tree
Showing 18 changed files with 563 additions and 573 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/packages/*/node_modules
/node_modules
/lib/
/internal/
/service-worker/
/shared/
/configurator/*.js
Expand Down
459 changes: 87 additions & 372 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"playground-service-worker.js",
"playground-typescript-worker.js",
"playground-styles.{css,js,d.ts,d.ts.map}",
"lib/**/*.{js,d.ts,d.ts.map}",
"internal/**/*.{js,d.ts,d.ts.map}",
"shared/**/*.{js,d.ts,d.ts.map}",
"src/**/*.{ts,js}",
"!src/test/**",
Expand Down Expand Up @@ -79,8 +79,6 @@
"@material/mwc-linear-progress": "^0.21.0",
"@material/mwc-list": "^0.21.0",
"@material/mwc-menu": "^0.21.0",
"@material/mwc-tab": "^0.21.0",
"@material/mwc-tab-bar": "^0.21.0",
"@material/mwc-textfield": "^0.21.0",
"@types/codemirror": "^5.60.0",
"comlink": "=4.3.1",
Expand Down
4 changes: 1 addition & 3 deletions src/configurator/playground-configurator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,7 @@ export class PlaygroundConfigurator extends LitElement {
}

private async setValue<T extends KnobId>(id: T, value: KnobValueType<T>) {
await this.setValues(
new Map<KnobId, unknown>([[id, value]])
);
await this.setValues(new Map<KnobId, unknown>([[id, value]]));
}

private async setValues(values: Map<KnobId, unknown>) {
Expand Down
6 changes: 3 additions & 3 deletions src/configurator/playground-theme-detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
} from 'lit-element';
import '../playground-code-editor.js';
import {PlaygroundCodeEditor} from '../playground-code-editor.js';
import '@material/mwc-tab-bar';
import {tokens} from './highlight-tokens.js';

@customElement('playground-theme-detector')
Expand Down Expand Up @@ -309,8 +308,9 @@ export class PlaygroundThemeDetector extends LitElement {
}
} else if (!foundBackground && node.nodeType === Node.ELEMENT_NODE) {
// Use the first non-transparent background (depth first).
const background = window.getComputedStyle(node as Element)
.backgroundColor;
const background = window.getComputedStyle(
node as Element
).backgroundColor;
if (background !== 'rgba(0, 0, 0, 0)') {
foundBackground = true;
this._propertyValues.set('--playground-code-background', background);
Expand Down
8 changes: 5 additions & 3 deletions src/lib/codemirror.ts → src/internal/codemirror.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import type CoreMirrorFolding from 'codemirror/addon/fold/foldcode.js';
* This function is defined as window.CodeMirror, but @types/codemirror doesn't
* declare that.
*/
export const CodeMirror = (window as {
CodeMirror: typeof CodeMirrorCore & typeof CoreMirrorFolding;
}).CodeMirror;
export const CodeMirror = (
window as {
CodeMirror: typeof CodeMirrorCore & typeof CoreMirrorFolding;
}
).CodeMirror;
223 changes: 223 additions & 0 deletions src/internal/tab-bar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

import {html, css, LitElement, customElement, property} from 'lit-element';
import {ifDefined} from 'lit-html/directives/if-defined.js';

import type {PlaygroundInternalTab} from './tab.js';

/**
* A horizontal bar of tabs.
*
* Slots:
* - default: The <playground-internal-tab> tabs.
*/
@customElement('playground-internal-tab-bar')
export class PlaygroundInternalTabBar extends LitElement {
static styles = css`
:host {
display: flex;
overflow-x: auto;
}
:host::-webkit-scrollbar {
display: none;
}
div {
display: flex;
}
`;

/**
* Aria label of the tab list.
*/
@property()
label?: string;

/**
* Get or set the active tab.
*/
get active(): PlaygroundInternalTab | undefined {
return this._active;
}

set active(tab: PlaygroundInternalTab | undefined) {
/**
* Note the active tab can be set either by setting the bar's `active`
* property to the tab, or by setting the tab's `active` property to
* true. The two become synchronized according to the following flow:
*
* bar click/keydown
* |
* v
* bar.active = tab ---> changed? ---> tab.active = true
* ^ |
* | v
* bar tabchange listener changed from false to true?
* ^ |
* | |
* +--- tab dispatches tabchange <---+
*/
const oldActive = this._active;
if (tab === oldActive) {
return;
}
this._active = tab;
if (oldActive !== undefined) {
oldActive.active = false;
}
if (tab !== undefined) {
tab.active = true;
} else {
// Usually the tab itself emits the tabchange event, but we need to handle
// the "no active tab" case here.
this.dispatchEvent(
new CustomEvent<{tab?: PlaygroundInternalTab}>('tabchange', {
detail: {tab: undefined},
bubbles: true,
})
);
}
}

private _tabs: PlaygroundInternalTab[] = [];
private _active: PlaygroundInternalTab | undefined = undefined;

render() {
return html`
<div role="tablist" aria-label=${ifDefined(this.label)}>
<slot
@slotchange=${this._onSlotchange}
@click=${this._activateTab}
@keydown=${this._onKeydown}
@tabchange=${this._activateTab}
></slot>
</div>
`;
}

private _onSlotchange(event: Event) {
this._tabs = (
event.target as HTMLSlotElement
).assignedElements() as PlaygroundInternalTab[];
let newActive;
// Manage the idx and active properties on all tabs. The first tab that
// asserts it is active wins.
for (let i = 0; i < this._tabs.length; i++) {
const tab = this._tabs[i];
tab.index = i;
if (newActive !== undefined) {
tab.active = false;
} else if (tab.active || tab.hasAttribute('active')) {
// Check both the active property and the active attribute, because the
// user could have set the initial active state either way, and it might
// not have reflected to the other yet.
newActive = tab;
}
}
this.active = newActive;
}

private _activateTab(event: Event) {
const tab = this._findEventTab(event);
if (tab === undefined) {
return;
}
this.active = tab;
this._scrollTabIntoViewIfNeeded(tab);
}

/**
* If the given tab is not visible, or if not enough of its adjacent tabs are
* visible, scroll so that the tab is centered.
*/
private _scrollTabIntoViewIfNeeded(tab: PlaygroundInternalTab) {
// Note we don't want to use tab.scrollIntoView() because that would also
// scroll the viewport to show the tab bar.
const barRect = this.getBoundingClientRect();
const tabRect = tab.getBoundingClientRect();
// Add a margin so that we'll also scroll if not enough of an adjacent tab
// is visible, so that it's clickable. 48px is the recommended minimum touch
// target size from the Material Accessibility guidelines
// (https://material.io/design/usability/accessibility.html#layout-and-typography)
const margin = 48;
if (
tabRect.left - margin < barRect.left ||
tabRect.right + margin > barRect.right
) {
const centered =
tabRect.left -
barRect.left +
this.scrollLeft -
barRect.width / 2 +
tabRect.width / 2;
this.scroll({left: centered, behavior: 'smooth'});
}
}

private async _onKeydown(event: KeyboardEvent) {
const oldIdx = this.active?.index ?? 0;
const endIdx = this._tabs.length - 1;
let newIdx = oldIdx;
switch (event.key) {
case 'ArrowLeft': {
if (oldIdx === 0) {
newIdx = endIdx; // Wrap around.
} else {
newIdx--;
}
break;
}
case 'ArrowRight': {
if (oldIdx === endIdx) {
newIdx = 0; // Wrap around.
} else {
newIdx++;
}
break;
}
case 'Home': {
newIdx = 0;
break;
}
case 'End': {
newIdx = endIdx;
break;
}
}
if (newIdx !== oldIdx) {
// Prevent default scrolling behavior.
event.preventDefault();
const tab = this._tabs[newIdx];
this.active = tab;
// Wait for tabindex to update so we can call focus.
await tab.updateComplete;
tab.focus();
}
}

private _findEventTab(event: Event): PlaygroundInternalTab | undefined {
const target = event.target as HTMLElement | undefined;
if (target?.localName === 'playground-internal-tab') {
return event.target as PlaygroundInternalTab;
}
for (const el of event.composedPath()) {
if (
(el as HTMLElement | undefined)?.localName === 'playground-internal-tab'
) {
return el as PlaygroundInternalTab;
}
}
return undefined;
}
}

declare global {
interface HTMLElementTagNameMap {
'playground-internal-tab-bar': PlaygroundInternalTabBar;
}
}
Loading

0 comments on commit dab68e5

Please sign in to comment.