Skip to content

Commit

Permalink
feat(tabs): adds tabs and tab element
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 530452330
  • Loading branch information
material-web-copybara authored and copybara-github committed May 9, 2023
1 parent 7dbf2a0 commit cbb24df
Show file tree
Hide file tree
Showing 16 changed files with 1,366 additions and 103 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Ripple | ✅ | ✅ | 🟡
Select | ✅ | ✅ | ❌
Slider | ✅ | ✅ | ❌
Switch | ✅ | ✅ | ❌
Tabs | 🟡 | | ❌
Tabs | | 🟡 | ❌
Text field | ✅ | ✅ | 🟡

### 1.1+ Components
Expand Down
6 changes: 6 additions & 0 deletions tabs/_tab.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//
// Copyright 2023 Google LLC
// SPDX-License-Identifier: Apache-2.0
//

@forward './lib/tab' show theme;
41 changes: 41 additions & 0 deletions tabs/harness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {ElementWithHarness, Harness} from '../testing/harness.js';

import {Tab} from './lib/tab.js';
import {Tabs} from './lib/tabs.js';

/**
* Test harness for Tab.
*/
export class TabHarness extends Harness<Tab> {
override async getInteractiveElement() {
await this.element.updateComplete;
return this.element.querySelector<HTMLButtonElement|HTMLLinkElement>(
'.button')!;
}

async isIndicatorShowing() {
await this.element.updateComplete;
const opacity = getComputedStyle(this.element.indicator)['opacity'];
return opacity === '1';
}
}

/**
* Test harness for Tabs.
*/
export class TabsHarness extends Harness<Tabs> {
get harnessedItems() {
// Test access to protected property
// tslint:disable-next-line:no-dict-access-on-struct-type
return (this.element['items'] as Array<ElementWithHarness<Tab>>)
.map(item => {
return (item.harness ?? new TabHarness(item)) as TabHarness;
});
}
}
289 changes: 289 additions & 0 deletions tabs/lib/_tab.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
//
// Copyright 2023 Google LLC
// SPDX-License-Identifier: Apache-2.0
//

// go/keep-sorted start
@use 'sass:map';
// go/keep-sorted end
// go/keep-sorted start
@use '../../elevation/elevation';
@use '../../focus/focus-ring';
@use '../../ripple/ripple';
@use '../../sass/string-ext';
@use '../../sass/theme';
@use '../../tokens';
// go/keep-sorted end

@mixin theme($tokens) {
$reference: tokens.md-comp-tab-values();
$tokens: theme.validate-theme($reference, $tokens);
$tokens: theme.create-theme-vars($tokens, '');
@include theme.emit-theme-vars($tokens);
}

@mixin styles() {
// contains tokens for all variants and applied where needed
$tokens: theme.create-theme-vars(tokens.md-comp-tab-values(), '');

:host {
// apply primary-tokens by default
$primary-prefix: 'primary-tab-';
@each $token, $value in $tokens {
@if string-ext.has-prefix($token, $primary-prefix) {
$token: string-ext.trim-prefix(#{$token}, $primary-prefix);
--_#{$token}: #{$value};
}
}

display: inline-flex;
outline: none;
-webkit-tap-highlight-color: transparent;
vertical-align: middle;

@include ripple.theme(
(
focus-color: var(--_focus-state-layer-color),
focus-opacity: var(--_focus-state-layer-opacity),
hover-color: var(--_hover-state-layer-color),
hover-opacity: var(--_hover-state-layer-opacity),
pressed-color: var(--_pressed-state-layer-color),
pressed-opacity: var(--_pressed-state-layer-opacity),
)
);

@include focus-ring.theme(
(
shape: 8px,
offset: -7px,
)
);
}

.button {
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
border: none;
outline: none;
user-select: none;
-webkit-appearance: none;
vertical-align: middle;
background: transparent;
text-decoration: none;
width: 100%;
position: relative;
padding: 0;
margin: 0;
z-index: 0; // Ensure this is a stacking context so the indicator displays
font: var(--_label-text-type);
background-color: var(--_container-color);
border-bottom: var(--_divider-thickness) solid var(--_divider-color);
color: var(--_label-text-color);

&::-moz-focus-inner {
padding: 0;
border: 0;
}
}

.button,
md-ripple {
border-radius: var(--_container-shape);
}

.touch {
position: absolute;
top: 50%;
height: 48px;
left: 0;
right: 0;
transform: translateY(-50%);
}

.content {
position: relative;
box-sizing: border-box;
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;

// TODO (b/261201556) replace with spacing token
$_content-padding: 8px;
// tabs are naturally sized up to their max height.
max-height: calc(var(--_container-height) + 2 * $_content-padding);
padding: $_content-padding;
gap: 4px;
}

.content.inline-icon {
flex-direction: row;
}

.indicator {
position: absolute;
box-sizing: border-box;
z-index: -1;
transform-origin: bottom left;
background: var(--_active-indicator-color);
border-radius: var(--_active-indicator-shape);
height: var(--_active-indicator-height);
inset: auto 0 0 0;
// hidden unless the tab is selected
opacity: 0;
}

// unselected states
.button ::slotted([slot='icon']) {
display: inline-flex;
position: relative;
writing-mode: horizontal-tb;
fill: currentColor;
color: var(--_icon-color);
font-size: var(--_icon-size);
width: var(--_icon-size);
height: var(--_icon-size);
}

.button:hover {
color: var(--_hover-label-text-color);
cursor: pointer;
}

.button:hover ::slotted([slot='icon']) {
color: var(--_hover-icon-color);
}

.button:focus {
color: var(--_focus-label-text-color);
}

.button:focus ::slotted([slot='icon']) {
color: var(--_focus-icon-color);
}

.button:active {
color: var(--_pressed-label-text-color);
outline: none;
}

.button:active ::slotted([slot='icon']) {
color: var(--_pressed-icon-color);
}

// selected styling
:host([selected]) .indicator {
opacity: 1;
}
:host([selected]) .button {
color: var(--_active-label-text-color);
@include elevation.theme(
(
level: var(--_container-elevation),
)
);

@include ripple.theme(
(
focus-color: var(--_active-focus-state-layer-color),
focus-opacity: var(--_active-focus-state-layer-opacity),
hover-color: var(--_active-hover-state-layer-color),
hover-opacity: var(--_active-hover-state-layer-opacity),
pressed-color: var(--_active-pressed-state-layer-color),
pressed-opacity: var(--_active-pressed-state-layer-opacity),
)
);
}

:host([selected]) .button ::slotted([slot='icon']) {
color: var(--_active-icon-color);
}

// selected states
:host([selected]) .button:hover {
color: var(--_active-hover-label-text-color);
}

:host([selected]) .button:hover ::slotted([slot='icon']) {
color: var(--_active-hover-icon-color);
}

:host([selected]) .button:focus {
color: var(--_active-focus-label-text-color);
}

:host([selected]) .button:focus ::slotted([slot='icon']) {
color: var(--_active-focus-icon-color);
}

:host([selected]) .button:active {
color: var(--_active-pressed-label-text-color);
}

:host([selected]) .button:active ::slotted([slot='icon']) {
color: var(--_active-pressed-icon-color);
}

// TODO (b/261201556) implement disabled and high contrast mode
// styling in beta version.
// disabled state
:host([disabled]) {
cursor: default;
pointer-events: none;
// TODO (b/261201556) implement disabled styling in beta version.
opacity: 0.38;
}

// secondary
:host([variant~='secondary']) {
// apply secondary-tab tokens
$secondary-prefix: 'secondary-tab-';
@each $token, $value in $tokens {
@if string-ext.has-prefix($token, $secondary-prefix) {
$token: string-ext.trim-prefix(#{$token}, $secondary-prefix);
--_#{$token}: #{$value};
}
}
}

:host([variant~='secondary']) .content {
width: 100%;
}

:host([variant~='secondary']) .indicator {
min-width: 100%;
}

// vertical (no tokens for vertical as yet)
:host([variant~='vertical']) {
flex: 0;
}

:host([variant~='vertical']) .button {
width: 100%;
flex-direction: row;
border-bottom: none;
border-right: var(--_divider-thickness) solid var(--_divider-color);
}

:host([variant~='vertical']) .content {
width: 100%;
}

:host([variant~='vertical']) .indicator {
height: 100%;
min-width: var(--_active-indicator-height);
inset: 0 0 0 auto;
}

:host([variant~='vertical'][variant~='primary']) {
--_active-indicator-shape: 9999px 0 0 9999px;
}

:host,
::slotted(*) {
white-space: nowrap;
}
}
44 changes: 44 additions & 0 deletions tabs/lib/_tabs.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// Copyright 2023 Google LLC
// SPDX-License-Identifier: Apache-2.0
//

// Note, there are currently no tokens for tabs. Instead, tabs are entirely
// themed via primary/secondary tab.
@mixin styles() {
:host {
box-sizing: border-box;
display: flex;
justify-content: space-between;
align-items: center;
overflow: auto;
scroll-behavior: smooth;
scrollbar-width: none;
position: relative;
}

:host([hidden]) {
display: none;
}

:host([variant~='vertical']:not([hidden])) {
display: inline-flex;
flex-direction: column;
align-items: stretch;
gap: 0px;
}

:host::-webkit-scrollbar {
display: none;
}

::slotted(*) {
flex: 1;
}

// draw selected on top so its indicator can be transitioned from the
// previously selected tab, on top of it
::slotted([selected]) {
z-index: 1;
}
}
Loading

0 comments on commit cbb24df

Please sign in to comment.