diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 537ea807015..26db7c688de 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ We strive to make developing Material Components Web as frictionless as possible ### Setting up your development environment -You'll need a recent version of [nodejs](https://nodejs.org/en/) to work on MDC Web. We [test our builds](https://travis-ci.org/material-components/material-components-web) using both the latest and LTS node versions, so use of one of those is recommended. You can use [nvm](https://github.com/creationix/nvm) to easily install and manage different versions of node on your system. +You'll need a recent version of [nodejs](https://nodejs.org/en/) to work on MDC Web. We [test our builds](https://travis-ci.com/material-components/material-components-web) using both the latest and LTS node versions, so use of one of those is recommended. You can use [nvm](https://github.com/creationix/nvm) to easily install and manage different versions of node on your system. > **NOTE**: If you expect to commit updated or new dependencies, please ensure you are using npm 5, which will > also update `package-lock.json` correctly when you install or upgrade packages. @@ -106,7 +106,7 @@ npm run test:closure # Runs closure build tests against all closurized files #### Running Tests across browsers -If you're making big changes or developing new components, we encourage you to be a good citizen and test your changes across browsers! A super simple way to do this is to use [sauce labs](https://saucelabs.com/), which is how we tests our collaborator PRs on TravisCI: +If you're making big changes or developing new components, we encourage you to be a good citizen and test your changes across browsers! A super simple way to do this is to use [sauce labs](https://saucelabs.com/), which is how we test our collaborator PRs on TravisCI: 1. [Sign up](https://saucelabs.com/beta/signup) for a sauce labs account (choose "Open Sauce" as your selected plan; [it's free](https://saucelabs.com/opensauce/)!) 2. [Download sauce connect](https://wiki.saucelabs.com/display/DOCS/Sauce+Connect+Proxy) for your OS and make sure that the `bin` folder in the downloaded zip is somewhere on your `$PATH`. diff --git a/README.md b/README.md index 4fab9b966c7..98d60d2f9e2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://img.shields.io/travis/material-components/material-components-web/master.svg)](https://travis-ci.org/material-components/material-components-web/) +[![Build Status](https://travis-ci.com/material-components/material-components-web.svg?branch=master)](https://travis-ci.com/material-components/material-components-web/) [![codecov](https://codecov.io/gh/material-components/material-components-web/branch/master/graph/badge.svg)](https://codecov.io/gh/material-components/material-components-web) [![Chat](https://img.shields.io/discord/259087343246508035.svg)](https://discord.gg/material-components) diff --git a/demos/switch.html b/demos/switch.html index 9f8d8978dfd..360affd0842 100644 --- a/demos/switch.html +++ b/demos/switch.html @@ -18,6 +18,7 @@ Switch - Material Components Catalog + @@ -63,9 +64,11 @@
- -
-
+
+
+
+ +
@@ -74,21 +77,25 @@

Enabled

- -
-
+
+
+
+ +
-
- -
-
+
+
+
+
+ +
@@ -97,14 +104,16 @@

Enabled

Disabled

-
- -
-
+
+
+
+
+ +
@@ -134,10 +143,16 @@

Disabled

diff --git a/demos/switch.scss b/demos/switch.scss index 22df5f0b2b0..5ca4316327c 100644 --- a/demos/switch.scss +++ b/demos/switch.scss @@ -28,9 +28,7 @@ .demo-switch--custom { $color: $material-color-red-500; - @include mdc-switch-track-color($color); - @include mdc-switch-knob-color($color); - @include mdc-switch-focus-indicator-color($color); + @include mdc-switch-toggled-on-color($color); } .rtl-toggle { diff --git a/demos/tab-bar.html b/demos/tab-bar.html new file mode 100644 index 00000000000..691c4a563c8 --- /dev/null +++ b/demos/tab-bar.html @@ -0,0 +1,320 @@ + + + + + + Tab Bar - Material Components Catalog + + + + + + + + + + +
+
+
+ + + + Tab +
+
+
+ +
+
+
+
+
+
+ + + + + + + + +
+
+
+
+
+ +
+

RTL

+
+ + +
+
+ +
+

Tab Bar

+ +

Start Alignment

+
+
+
+
+
+ + + +
+
+
+
+
+ +

Center Alignment

+
+
+
+
+
+ + + +
+
+
+
+
+ +

End Alignment

+
+
+
+
+
+ + + +
+
+
+
+
+
+ +
+

Customization

+ +
+
+
+
+
+ + + +
+
+
+
+
+
+
+ + + + + + diff --git a/demos/tab-bar.scss b/demos/tab-bar.scss new file mode 100644 index 00000000000..e86e7f64608 --- /dev/null +++ b/demos/tab-bar.scss @@ -0,0 +1,68 @@ +// +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +@import "./common"; +@import "../packages/mdc-tab/mdc-tab"; +@import "../packages/mdc-tab/mixins"; +@import "../packages/mdc-tab-indicator/mdc-tab-indicator"; +@import "../packages/mdc-tab-indicator/mixins"; +@import "../packages/mdc-tab-scroller/mdc-tab-scroller"; +@import "../packages/mdc-tab-bar/mdc-tab-bar"; +@import "../packages/mdc-tab-bar/mixins"; +@import "../packages/mdc-elevation/mixins"; +@import "../packages/mdc-ripple/mixins"; +@import "../packages/mdc-theme/color-palette"; + +.demo { + display: flex; + align-items: center; +} + +.hero-demo-tab-bar { + @include mdc-tab-bar-width(420px); +} + +.custom-demo-tab-bar { + .mdc-tab { + @include mdc-typography(subtitle1); + @include mdc-tab-fixed-width(120px); + @include mdc-tab-text-label-color($material-color-blue-600); + @include mdc-tab-icon-color($material-color-blue-600); + } + + .mdc-tab__ripple { + @include mdc-states($material-color-yellow-700); + } + + .mdc-tab--active { + @include mdc-tab-text-label-color($material-color-blue-900); + @include mdc-tab-icon-color($material-color-blue-900); + } + + .mdc-tab-indicator { + @include mdc-tab-indicator-underline-height(5px); + @include mdc-tab-indicator-underline-color($material-color-yellow-700); + } +} + +.demo-controls { + padding: 0 16px; +} + +.rtl-container { + margin: 24px; + padding: 24px; +} diff --git a/demos/tab-indicator.html b/demos/tab-indicator.html new file mode 100644 index 00000000000..0ad65cf9d15 --- /dev/null +++ b/demos/tab-indicator.html @@ -0,0 +1,269 @@ + + + + + + Tab Indicator - Material Components Catalog + + + + + + + + + + +
+
+
+ + + + Tab Indicator +
+
+
+ +
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+ +
+

Sliding Underline Indicator

+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+ +

Custom Color

+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+ +

Custom Height

+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+ +

Custom Border Radius

+
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+ +
+

Fading Icon Indicator

+
+
+ + favorite + +
+
+ + star + +
+
+ + lens + +
+
+ +
+ +

Custom Color

+
+
+ + favorite + +
+
+ + star + +
+
+ + lens + +
+
+ +
+ +

Custom Height

+
+
+ + favorite + +
+
+ + star + +
+
+ + lens + +
+
+
+ +
+

Mix and Match

+

This demo shows how to use a sliding indicator with custom SVG icon content.

+
+
+ + + + + + +
+
+ + + + + + +
+
+ + + + + + +
+
+
+
+ + + + + diff --git a/demos/tab-indicator.scss b/demos/tab-indicator.scss new file mode 100644 index 00000000000..ac4c2344d57 --- /dev/null +++ b/demos/tab-indicator.scss @@ -0,0 +1,85 @@ +@import "./common"; +@import "../packages/mdc-tab-indicator/mixins"; +@import "../packages/mdc-tab-indicator/mdc-tab-indicator"; +@import "../packages/mdc-elevation/mixins"; +@import "../packages/mdc-theme/color-palette"; + +.hero { + flex-direction: column; +} + +.demo { + @include mdc-elevation(1); + + display: flex; + width: 400px; + height: 50px; + margin: 2rem 0; +} + +.demo-item { + @include mdc-tab-indicator-surface; + + flex: 2 0 auto; + height: inherit; + background-color: $material-color-grey-50; + cursor: pointer; +} + +.demo-item--narrow { + flex: 1 0 auto; +} + +.demo-item--wide { + flex: 3 0 auto; +} + +.demo-item:hover { + background-color: white; +} + +.demo > .demo-item:nth-child(n + 2) { + border-left: 1px solid $material-color-grey-300; +} + +.demo-label { + position: relative; + z-index: 2; +} + +.custom-color-underline { + @include mdc-tab-indicator-underline-color($material-color-orange-a400); +} + +.custom-color-icon { + @include mdc-tab-indicator-icon-color($material-color-orange-a400); +} + +.custom-height-underline { + @include mdc-tab-indicator-underline-height(8px); +} + +.custom-height-icon { + @include mdc-tab-indicator-icon-height(44px); +} + +.custom-border-radius { + @include mdc-tab-indicator-underline-height(5px); + @include mdc-tab-indicator-underline-top-corner-radius(5px); +} + +.custom-fading-underline { + @include mdc-tab-indicator-underline-height(4px); + @include mdc-tab-indicator-underline-color($material-color-green-a400); + @include mdc-tab-indicator-underline-top-corner-radius(4px); +} + +.custom-sliding-icon { + @include mdc-tab-indicator-icon-height(30px); + @include mdc-tab-indicator-icon-color($material-color-pink-a400); +} + +.custom-icon { + height: inherit; + fill: currentColor; +} diff --git a/demos/tab-scroller.html b/demos/tab-scroller.html new file mode 100644 index 00000000000..4a5e6ee8964 --- /dev/null +++ b/demos/tab-scroller.html @@ -0,0 +1,142 @@ + + + + + + Tab Scroller - Material Components Catalog + + + + + + + + + + +
+
+
+ + + + Tab Scroller +
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ + +
+
+
+
+
+
+

RTL

+
+ + +
+
+
+
+ + + + + diff --git a/demos/tab-scroller.scss b/demos/tab-scroller.scss new file mode 100644 index 00000000000..d46fff87e48 --- /dev/null +++ b/demos/tab-scroller.scss @@ -0,0 +1,28 @@ +@import "./common"; +@import "../packages/mdc-theme/color-palette"; +@import "../packages/mdc-tab-scroller/mdc-tab-scroller"; + +.demo-scroller { + width: 360px; + outline: 1px solid $material-color-grey-400; +} + +.demo-cube { + width: 43px; + height: 43px; + margin: 10px; +} + +@for $i from 1 through 34 { + .demo-cube:nth-child(#{$i}) { + background-color: rgb(10 + random(230), 10 + random(230), 10 + random(230)); + } +} + +.demo-controller { + padding: 10px 0; +} + +.demo-controller-row { + padding: 5px 0; +} diff --git a/demos/tab.html b/demos/tab.html new file mode 100644 index 00000000000..91b2a27d1df --- /dev/null +++ b/demos/tab.html @@ -0,0 +1,358 @@ + + + + + + Tab - Material Components Catalog + + + + + + + + + + +
+
+
+ + + + Tab +
+
+
+ +
+
+ + + +
+ +
+

RTL

+
+ + +
+
+ +
+

Tabs

+ +

Text Label

+
+ + + +
+ +
+

Icon

+
+ + + +
+ +
+

Text Label and Icon

+
+ + + +
+ +
+

Stacked Text Label and Icon

+
+ + + +
+ +
+

Text Label Width-Matching Indicator

+
+ + + +
+ +
+

Text Label with Icon Indicator

+
+ + + +
+
+ +
+

Customization

+
+ + + +
+
+
+ + + + + + diff --git a/demos/tab.scss b/demos/tab.scss new file mode 100644 index 00000000000..0be33f01451 --- /dev/null +++ b/demos/tab.scss @@ -0,0 +1,62 @@ +// +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +@import "./common"; +@import "../packages/mdc-tab/mixins"; +@import "../packages/mdc-tab/mdc-tab"; +@import "../packages/mdc-tab-indicator/mdc-tab-indicator"; +@import "../packages/mdc-tab-indicator/mixins"; +@import "../packages/mdc-elevation/mixins"; +@import "../packages/mdc-ripple/mixins"; +@import "../packages/mdc-theme/color-palette"; + +.demo { + display: flex; + align-items: center; +} + +.demo-tab { + flex: 0 1 auto; +} + +.demo-controls { + padding: 0 16px; +} + +.rtl-container { + margin: 24px; + padding: 24px; +} + +.custom-tab { + @include mdc-tab-text-label-color($material-color-blue-300); + @include mdc-tab-icon-color($material-color-orange-300); + + .custom-tab-indicator { + @include mdc-tab-indicator-underline-color($material-color-pink-a700); + @include mdc-tab-indicator-underline-height(5px); + @include mdc-tab-indicator-underline-top-corner-radius(5px); + } + + .mdc-tab__ripple { + @include mdc-states($material-color-pink-100); + } + + &.mdc-tab--active { + @include mdc-tab-text-label-color($material-color-blue-900); + @include mdc-tab-icon-color($material-color-orange-900); + } +} diff --git a/demos/theme/index.html b/demos/theme/index.html index 0ef009d2603..d5081f2f3c2 100644 --- a/demos/theme/index.html +++ b/demos/theme/index.html @@ -677,19 +677,23 @@

-
- -
-
+
+
+
+
+ +
-
- -
-
+
+
+
+
+ +
@@ -933,6 +937,14 @@

mdc.slider.MDCSlider.attachTo(slider); }); + /* + * Switch + */ + + [].forEach.call(document.querySelectorAll('.mdc-switch'), function(switchEl) { + mdc.switchComponent.MDCSwitch.attachTo(switchEl); + }); + /* * Tab Bar */ diff --git a/docs/docsite-tabs.md b/docs/docsite-tabs.md new file mode 100644 index 00000000000..1c20ce1cb51 --- /dev/null +++ b/docs/docsite-tabs.md @@ -0,0 +1,13 @@ +--- +# This file exists describes the directory housing the tab components. If the +# URL is visited, it will immediately redirect to the Tab component. +title: "Tabs" +layout: detail +section: components +excerpt: "Components for tabbed navigation." +iconId: tabs +path: /catalog/tabs/ +redirect_path: /catalog/tabs/tab/ +--- + +{% include redirect-page.html %} diff --git a/package.json b/package.json index 5e0ff4d943c..d9117647189 100644 --- a/package.json +++ b/package.json @@ -192,6 +192,9 @@ "switch", "tabs", "tab", + "tab-bar", + "tab-indicator", + "tab-scroller", "text-field", "theme", "toolbar", @@ -226,7 +229,11 @@ "mdc-select", "mdc-selection-control", "mdc-slider", + "mdc-switch", "mdc-tab", + "mdc-tab-indicator", + "mdc-tab-scroller", + "mdc-tab-bar", "mdc-textfield", "mdc-top-app-bar" ] diff --git a/packages/material-components-web/README.md b/packages/material-components-web/README.md index f5201a24560..42a59d16c2d 100644 --- a/packages/material-components-web/README.md +++ b/packages/material-components-web/README.md @@ -27,6 +27,8 @@ import { checkbox } from 'material-components-web'; const checkbox = new checkbox.MDCCheckbox(document.querySelector('.mdc-checkbox')); ``` +> NOTE: Since switch is a reserved word in JS, switch is instead named `switchControl`. + > NOTE: Built CSS files as well as UMD JS bundles will be available as part of the package > post-alpha. diff --git a/packages/material-components-web/index.js b/packages/material-components-web/index.js index f0d7354af01..3074d891668 100644 --- a/packages/material-components-web/index.js +++ b/packages/material-components-web/index.js @@ -36,7 +36,11 @@ import * as select from '@material/select/index'; import * as selectionControl from '@material/selection-control/index'; import * as slider from '@material/slider/index'; import * as snackbar from '@material/snackbar/index'; -import * as tabs from '@material/tabs/index'; +import * as switchControl from '@material/switch/index'; +import * as tab from '@material/tab/index'; +import * as tabBar from '@material/tab-bar/index'; +import * as tabIndicator from '@material/tab-indicator/index'; +import * as tabScroller from '@material/tab-scroller/index'; import * as textField from '@material/textfield/index'; import * as toolbar from '@material/toolbar/index'; import * as topAppBar from '@material/top-app-bar/index'; @@ -60,12 +64,12 @@ autoInit.register('MDCList', list.MDCList); autoInit.register('MDCNotchedOutline', notchedOutline.MDCNotchedOutline); autoInit.register('MDCRadio', radio.MDCRadio); autoInit.register('MDCSnackbar', snackbar.MDCSnackbar); -autoInit.register('MDCTab', tabs.MDCTab); -autoInit.register('MDCTabBar', tabs.MDCTabBar); +autoInit.register('MDCTabBar', tabBar.MDCTabBar); autoInit.register('MDCTextField', textField.MDCTextField); autoInit.register('MDCMenu', menu.MDCMenu); autoInit.register('MDCSelect', select.MDCSelect); autoInit.register('MDCSlider', slider.MDCSlider); +autoInit.register('MDCSwitch', switchControl.MDCSwitch); autoInit.register('MDCToolbar', toolbar.MDCToolbar); autoInit.register('MDCTopAppBar', topAppBar.MDCTopAppBar); @@ -92,8 +96,12 @@ export { select, selectionControl, slider, + switchControl, snackbar, - tabs, + tab, + tabBar, + tabIndicator, + tabScroller, textField, toolbar, topAppBar, diff --git a/packages/material-components-web/material-components-web.scss b/packages/material-components-web/material-components-web.scss index 7cdfe3e104a..629a889a12e 100644 --- a/packages/material-components-web/material-components-web.scss +++ b/packages/material-components-web/material-components-web.scss @@ -41,7 +41,10 @@ @import "@material/slider/mdc-slider"; @import "@material/snackbar/mdc-snackbar"; @import "@material/switch/mdc-switch"; -@import "@material/tabs/mdc-tabs"; +@import "@material/tab/mdc-tab"; +@import "@material/tab-bar/mdc-tab-bar"; +@import "@material/tab-indicator/mdc-tab-indicator"; +@import "@material/tab-scroller/mdc-tab-scroller"; @import "@material/textfield/mdc-text-field"; @import "@material/theme/mdc-theme"; @import "@material/toolbar/mdc-toolbar"; diff --git a/packages/material-components-web/package.json b/packages/material-components-web/package.json index 6834031aa92..fbe50ddb2d4 100644 --- a/packages/material-components-web/package.json +++ b/packages/material-components-web/package.json @@ -45,7 +45,10 @@ "@material/slider": "^0.36.0", "@material/snackbar": "^0.37.1", "@material/switch": "^0.36.1", - "@material/tabs": "^0.37.1", + "@material/tab": "^0.37.0", + "@material/tab-bar": "^0.0.0", + "@material/tab-indicator": "^0.0.0", + "@material/tab-scroller": "^0.0.0", "@material/textfield": "^0.37.1", "@material/theme": "^0.35.0", "@material/toolbar": "^0.37.1", diff --git a/packages/mdc-switch/README.md b/packages/mdc-switch/README.md index d27d47b5137..be6a006bc67 100644 --- a/packages/mdc-switch/README.md +++ b/packages/mdc-switch/README.md @@ -15,7 +15,7 @@ path: /catalog/input-controls/switches/

--> -Switches toggle the state of a single settings option on or off, and are mobile preferred. +Switches toggle the state of a single setting on or off. They are the preferred way to adjust settings on mobile. ## Design & API Documentation @@ -40,24 +40,47 @@ npm install @material/switch ```html
- -
-
+
+
+
+ +
``` + +### Styles + +```scss +@import "@material/switch/mdc-switch"; +``` + +### JavaScript Instantiation + +The Switch requires JavaScript to function, so it is necessary to instantiate MDCSwitch with the HTML. + +```js +import {MDCSwitch} from '@material/switch'; + +const switchControl = new MDCSwitch(document.querySelector('.mdc-switch')); +``` + +> See [Importing the JS component](../../docs/importing-js.md) for more information on how to import JavaScript. + ## Variant ### Disabled Switch -Users can add the `disabled` attribute directly to the `` element or a parent `
` element to disable a switch. +Users can add the class 'mdc-switch--disabled' to the 'mdc-switch' element to disable the switch. ```html -
- -
-
+
+
+
+
+ +
@@ -70,17 +93,63 @@ Users can add the `disabled` attribute directly to the `` element or a pa CSS Class | Description --- | --- `mdc-switch` | Mandatory, for the parent element. -`mdc-switch__native-control` | Mandatory, for the input checkbox. -`mdc-switch__background` | Mandatory, for the background element. -`mdc-switch__knob` | Mandatory, for the knob element. +`mdc-switch__track` | Mandatory, for the track element. +`mdc-switch__thumb-underlay` | Mandatory, for the ripple effect. +`mdc-switch__thumb` | Mandatory, for the thumb element. +`mdc-switch__native-control` | Mandatory, for the hidden input checkbox. ### Sass Mixins -The following mixins apply only to _enabled_ switches in the _on_ (checked) state. -It is not currently possible to customize the color of a _disabled_ or _off_ (unchecked) switch. +MDC Switch uses [MDC Theme](../mdc-theme)'s `secondary` color by default for the checked (toggled on) state. +Use the following mixins to customize _enabled_ switches. It is not currently possible to customize the color of a + _disabled_ switch. Disabled switches use the same colors as enabled switches, but with a different opacity value. Mixin | Description --- | --- -`mdc-switch-track-color($color)` | Sets the track color. -`mdc-switch-knob-color($color)` | Sets the knob color. -`mdc-switch-focus-indicator-color($color)` | Sets the focus indicator color. +`mdc-switch-toggled-on-color($color)` | Sets the base color of the track, thumb, and ripple when the switch is toggled on. +`mdc-switch-toggled-off-color($color)` | Sets the base color of the track, thumb, and ripple when the switch is toggled off. +`mdc-switch-toggled-on-track-color($color)` | Sets color of the track when the switch is toggled on. +`mdc-switch-toggled-off-track-color($color)` | Sets color of the track when the switch is toggled off. +`mdc-switch-toggled-on-thumb-color($color)` | Sets color of the thumb when the switch is toggled on. +`mdc-switch-toggled-off-thumb-color($color)` | Sets color of the thumb when the switch is toggled off. +`mdc-switch-toggled-on-ripple-color($color)` | Sets the color of the ripple surrounding the thumb when the switch is toggled on. +`mdc-switch-toggled-off-ripple-color($color)` | Sets the color of the ripple surrounding the thumb when the switch is toggled off. + +## `MDCSwitch` Properties and Methods + +Property | Value Type | Description +--- | --- | --- +`checked` | Boolean | Setter/getter for the switch's checked state +`disabled` | Boolean | Setter/getter for the switch's disabled state + +## Usage within Web Frameworks + +If you are using a JavaScript framework, such as React or Angular, you can create a Switch for your framework. Depending on your needs, you can use the _Simple Approach: Wrapping MDC Web Vanilla Components_, or the _Advanced Approach: Using Foundations and Adapters_. Please follow the instructions [here](../../docs/integrating-into-frameworks.md). + +### `MDCSwitchAdapter` + +| Method Signature | Description | +| --- | --- | +| `addClass(className: string) => void` | Adds a class to the root element. | +| `removeClass(className: string) => void` | Removes a class from the root element. | +| `setNativeControlChecked(checked: boolean)` | Sets the checked state of the native control. | +| `isNativeControlChecked() => boolean` | Returns the checked state of the native control. | +| `setNativeControlDisabled(disabled: boolean)` | Sets the disabled state of the native control. | +| `isNativeControlDisabled() => boolean` | Returns the disabled state of the native control. | + +### `MDCSwitchFoundation` + +| Method Signature | Description | +| --- | --- | +| `isChecked() => boolean` | Returns whether the native control is checked. | +| `setChecked(checked: boolean) => void` | Sets the checked value of the native control and updates styling to reflect the checked state. | +| `isDisabled() => boolean` | Returns whether the native control is disabled. | +| `setDisabled(disabled: boolean) => void` | Sets the disabled value of the native control and updates styling to reflect the disabled state. | +| `handleChange() => void` | Handles a change event from the native control. | + +### `MDCSwitchFoundation` Event Handlers +If wrapping the switch component it is necessary to add an event handler for native control change events that calls the `handleChange` foundation method. For an example of this, see the [MDCSwitch](index.js) component `initialSyncWithDOM` method. + +| Event | Element Selector | Foundation Handler | +| --- | --- | --- | +| `change` | `.mdc-switch__native-control` | `handleChange()` | \ No newline at end of file diff --git a/packages/mdc-switch/_functions.scss b/packages/mdc-switch/_functions.scss index 2ff997103a5..06e64497389 100644 --- a/packages/mdc-switch/_functions.scss +++ b/packages/mdc-switch/_functions.scss @@ -1,5 +1,5 @@ // -// Copyright 2016 Google Inc. All Rights Reserved. +// Copyright 2018 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/packages/mdc-switch/_mixins.scss b/packages/mdc-switch/_mixins.scss index 93ac3eedea9..117faeb70b4 100644 --- a/packages/mdc-switch/_mixins.scss +++ b/packages/mdc-switch/_mixins.scss @@ -1,5 +1,5 @@ // -// Copyright 2016 Google Inc. All Rights Reserved. +// Copyright 2018 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,72 +15,58 @@ // @import "@material/theme/mixins"; +@import "@material/ripple/mixins"; +@import "@material/rtl/mixins"; @import "./variables"; -// -// Public -// +@mixin mdc-switch-toggled-on-color($color) { + @include mdc-switch-toggled-on-track-color($color); + @include mdc-switch-toggled-on-thumb-color($color); + @include mdc-switch-toggled-on-ripple-color($color); +} -@mixin mdc-switch-track-color($color) { - .mdc-switch__native-control:enabled:checked ~ .mdc-switch__background::before { +@mixin mdc-switch-toggled-off-color($color) { + @include mdc-switch-toggled-off-track-color($color); + @include mdc-switch-toggled-off-thumb-color($color); + @include mdc-switch-toggled-off-ripple-color($color); +} + +@mixin mdc-switch-toggled-on-track-color($color) { + &.mdc-switch--checked .mdc-switch__track { @include mdc-theme-prop(background-color, $color); @include mdc-theme-prop(border-color, $color); } } -@mixin mdc-switch-knob-color($color) { - // stylelint-disable-next-line selector-max-specificity - .mdc-switch__native-control:enabled:checked ~ .mdc-switch__background .mdc-switch__knob { +@mixin mdc-switch-toggled-on-thumb-color($color) { + &.mdc-switch--checked .mdc-switch__thumb { @include mdc-theme-prop(background-color, $color); @include mdc-theme-prop(border-color, $color); } } -@mixin mdc-switch-focus-indicator-color($color) { - // stylelint-disable-next-line selector-max-specificity - .mdc-switch__native-control:enabled:checked ~ .mdc-switch__background .mdc-switch__knob::before { - @include mdc-theme-prop(background-color, $color); +@mixin mdc-switch-toggled-on-ripple-color($color) { + &.mdc-switch--checked .mdc-switch__thumb-underlay { + @include mdc-states($color); } } -// -// Private -// - -@mixin mdc-switch-unchecked-track-color_($color) { - .mdc-switch__native-control:enabled:not(:checked) ~ .mdc-switch__background::before { +@mixin mdc-switch-toggled-off-track-color($color) { + &:not(.mdc-switch--checked) .mdc-switch__track { @include mdc-theme-prop(background-color, $color); @include mdc-theme-prop(border-color, $color); } } -@mixin mdc-switch-unchecked-knob-color_($color) { - // stylelint-disable-next-line selector-max-specificity - .mdc-switch__native-control:enabled:not(:checked) ~ .mdc-switch__background .mdc-switch__knob { +@mixin mdc-switch-toggled-off-thumb-color($color) { + &:not(.mdc-switch--checked) .mdc-switch__thumb { @include mdc-theme-prop(background-color, $color); @include mdc-theme-prop(border-color, $color); } } -@mixin mdc-switch-unchecked-focus-indicator-color_($color) { - // stylelint-disable-next-line selector-max-specificity - .mdc-switch__native-control:enabled:not(:checked) ~ .mdc-switch__background .mdc-switch__knob::before { - @include mdc-theme-prop(background-color, $color); +@mixin mdc-switch-toggled-off-ripple-color($color) { + &:not(.mdc-switch--checked) .mdc-switch__thumb-underlay { + @include mdc-states($color); } } - -@mixin mdc-switch-native-control_ { - position: absolute; - top: -14px; - left: -14px; - width: $mdc-switch-focus-ring-diameter; - height: $mdc-switch-focus-ring-diameter; -} - -@mixin mdc-switch-tap-target_ { - position: absolute; - top: -24px; - left: -24px; - width: $mdc-switch-focus-ring-diameter; - height: $mdc-switch-focus-ring-diameter; -} diff --git a/packages/mdc-switch/_variables.scss b/packages/mdc-switch/_variables.scss index c6d52982ec7..bf506f9be2f 100644 --- a/packages/mdc-switch/_variables.scss +++ b/packages/mdc-switch/_variables.scss @@ -1,5 +1,5 @@ // -// Copyright 2016 Google Inc. All Rights Reserved. +// Copyright 2018 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,17 +14,33 @@ // limitations under the License. // -$mdc-switch-track-width: 34px; +@import "@material/theme/variables"; + +$mdc-switch-track-width: 32px; $mdc-switch-track-height: 14px; -$mdc-switch-knob-diameter: 20px; -$mdc-switch-focus-ring-diameter: 48px; -$mdc-switch-knob-active-margin: $mdc-switch-track-width - $mdc-switch-knob-diameter; +$mdc-switch-thumb-diameter: 20px; +$mdc-switch-tap-target-size: 48px; -$mdc-switch-unchecked-track-color: #000; -$mdc-switch-unchecked-knob-color: #fafafa; -$mdc-switch-unchecked-focus-ring-color: #9e9e9e; -$mdc-switch-disabled-knob-color: #bdbdbd; +// Amount the edge of the thumb should be offset from the edge of the track. +$mdc-switch-thumb-offset: 4px; -$mdc-switch-baseline-theme-color: secondary; +// Position for the tap target that contains the thumb to align the thumb correctly offset from the track. +$mdc-switch-tap-target-initial-position: + -$mdc-switch-tap-target-size / 2 + $mdc-switch-thumb-diameter / 2 - + $mdc-switch-thumb-offset; + +// Value to cover the whole switch area (including the ripple) with the native control. +$mdc-switch-native-control-width: + $mdc-switch-track-width + + ($mdc-switch-tap-target-size - $mdc-switch-thumb-diameter) + + $mdc-switch-thumb-offset * 2; -$mdc-switch-knob-vertical-offset_: -3px; +$mdc-switch-thumb-active-margin: $mdc-switch-track-width - $mdc-switch-thumb-diameter + $mdc-switch-thumb-offset * 2; + +$mdc-switch-toggled-off-thumb-color: mdc-theme-prop-value(surface); +$mdc-switch-toggled-off-track-color: mdc-theme-prop-value(on-surface); +$mdc-switch-toggled-off-ripple-color: #9e9e9e; +$mdc-switch-disabled-thumb-color: mdc-theme-prop-value(surface); +$mdc-switch-disabled-track-color: mdc-theme-prop-value(on-surface); + +$mdc-switch-baseline-theme-color: secondary; diff --git a/packages/mdc-switch/adapter.js b/packages/mdc-switch/adapter.js new file mode 100644 index 00000000000..69056aa689d --- /dev/null +++ b/packages/mdc-switch/adapter.js @@ -0,0 +1,55 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint no-unused-vars: [2, {"args": "none"}] */ + +/** + * Adapter for MDC Switch. Provides an interface for managing + * - classes + * - dom + * + * Additionally, provides type information for the adapter to the Closure + * compiler. + * + * Implement this adapter for your framework of choice to delegate updates to + * the component in your framework of choice. See architecture documentation + * for more details. + * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md + * + * @record + */ +class MDCSwitchAdapter { + /** @param {string} className */ + addClass(className) {} + + /** @param {string} className */ + removeClass(className) {} + + /** @param {boolean} checked */ + setNativeControlChecked(checked) {} + + /** @return {boolean} checked */ + isNativeControlChecked() {} + + /** @param {boolean} disabled */ + setNativeControlDisabled(disabled) {} + + /** @return {boolean} disabled */ + isNativeControlDisabled() {} +} + +export default MDCSwitchAdapter; diff --git a/packages/mdc-switch/constants.js b/packages/mdc-switch/constants.js new file mode 100644 index 00000000000..13805ce8bdc --- /dev/null +++ b/packages/mdc-switch/constants.js @@ -0,0 +1,31 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** @enum {string} */ +const cssClasses = { + CHECKED: 'mdc-switch--checked', + DISABLED: 'mdc-switch--disabled', +}; + +/** @enum {string} */ +const strings = { + NATIVE_CONTROL_SELECTOR: '.mdc-switch__native-control', + RIPPLE_SURFACE_SELECTOR: '.mdc-switch__thumb-underlay', +}; + + +export {cssClasses, strings}; diff --git a/packages/mdc-switch/foundation.js b/packages/mdc-switch/foundation.js new file mode 100644 index 00000000000..27a70d9534e --- /dev/null +++ b/packages/mdc-switch/foundation.js @@ -0,0 +1,106 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import MDCFoundation from '@material/base/foundation'; +import MDCSwitchAdapter from './adapter'; +/* eslint-enable no-unused-vars */ +import {cssClasses, strings} from './constants'; + +/** + * @extends {MDCFoundation} + */ +class MDCSwitchFoundation extends MDCFoundation { + /** @return enum {string} */ + static get strings() { + return strings; + } + + /** @return enum {string} */ + static get cssClasses() { + return cssClasses; + } + + /** @return {!MDCSwitchAdapter} */ + static get defaultAdapter() { + return /** @type {!MDCSwitchAdapter} */ ({ + addClass: (/* className: string */) => {}, + removeClass: (/* className: string */) => {}, + setNativeControlChecked: (/* checked: boolean */) => {}, + isNativeControlChecked: () => /* boolean */ {}, + setNativeControlDisabled: (/* disabled: boolean */) => {}, + isNativeControlDisabled: () => /* boolean */ {}, + }); + } + + constructor(adapter) { + super(Object.assign(MDCSwitchFoundation.defaultAdapter, adapter)); + } + + /** @override */ + init() { + // Do an initial state update based on the state of the native control. + this.handleChange(); + } + + /** @return {boolean} */ + isChecked() { + return this.adapter_.isNativeControlChecked(); + } + + /** @param {boolean} checked */ + setChecked(checked) { + this.adapter_.setNativeControlChecked(checked); + this.updateCheckedStyling_(checked); + } + + /** @return {boolean} */ + isDisabled() { + return this.adapter_.isNativeControlDisabled(); + } + + /** @param {boolean} disabled */ + setDisabled(disabled) { + this.adapter_.setNativeControlDisabled(disabled); + if (disabled) { + this.adapter_.addClass(cssClasses.DISABLED); + } else { + this.adapter_.removeClass(cssClasses.DISABLED); + } + } + + /** + * Handles the change event for the switch native control. + */ + handleChange() { + this.updateCheckedStyling_(this.isChecked()); + } + + /** + * Updates the styling of the switch based on its checked state. + * @param {boolean} checked + * @private + */ + updateCheckedStyling_(checked) { + if (checked) { + this.adapter_.addClass(cssClasses.CHECKED); + } else { + this.adapter_.removeClass(cssClasses.CHECKED); + } + } +} + +export default MDCSwitchFoundation; diff --git a/packages/mdc-switch/index.js b/packages/mdc-switch/index.js new file mode 100644 index 00000000000..5ea3b4eae5a --- /dev/null +++ b/packages/mdc-switch/index.js @@ -0,0 +1,129 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import MDCComponent from '@material/base/component'; +/* eslint-disable no-unused-vars */ +import {MDCSelectionControlState, MDCSelectionControl} from '@material/selection-control/index'; +/* eslint-enable no-unused-vars */ +import MDCSwitchFoundation from './foundation'; +import {MDCRipple, MDCRippleFoundation} from '@material/ripple/index'; +import {getMatchesProperty} from '@material/ripple/util'; + +/** + * @extends MDCComponent + * @implements {MDCSelectionControl} + */ +class MDCSwitch extends MDCComponent { + static attachTo(root) { + return new MDCSwitch(root); + } + + constructor(...args) { + super(...args); + + /** @private {!MDCRipple} */ + this.ripple_ = this.initRipple_(); + + /** @private {!Function} */ + this.changeHandler_; + } + + destroy() { + super.destroy(); + this.ripple_.destroy(); + this.nativeControl_.removeEventListener('change', this.changeHandler_); + } + + initialSyncWithDOM() { + this.changeHandler_ = this.foundation_.handleChange.bind(this.foundation_); + this.nativeControl_.addEventListener('change', this.changeHandler_); + } + + /** + * Returns the state of the native control element, or null if the native control element is not present. + * @return {?MDCSelectionControlState} + * @private + */ + get nativeControl_() { + const {NATIVE_CONTROL_SELECTOR} = MDCSwitchFoundation.strings; + const el = /** @type {?MDCSelectionControlState} */ ( + this.root_.querySelector(NATIVE_CONTROL_SELECTOR)); + return el; + } + + /** + * @return {!MDCRipple} + * @private + */ + initRipple_() { + const {RIPPLE_SURFACE_SELECTOR} = MDCSwitchFoundation.strings; + const rippleSurface = /** @type {!Element} */ (this.root_.querySelector(RIPPLE_SURFACE_SELECTOR)); + + const MATCHES = getMatchesProperty(HTMLElement.prototype); + const adapter = Object.assign(MDCRipple.createAdapter(this), { + isUnbounded: () => true, + isSurfaceActive: () => this.nativeControl_[MATCHES](':active'), + addClass: (className) => rippleSurface.classList.add(className), + removeClass: (className) => rippleSurface.classList.remove(className), + registerInteractionHandler: (type, handler) => this.nativeControl_.addEventListener(type, handler), + deregisterInteractionHandler: (type, handler) => this.nativeControl_.removeEventListener(type, handler), + updateCssVariable: (varName, value) => rippleSurface.style.setProperty(varName, value), + computeBoundingRect: () => rippleSurface.getBoundingClientRect(), + }); + const foundation = new MDCRippleFoundation(adapter); + return new MDCRipple(this.root_, foundation); + } + + /** @return {!MDCSwitchFoundation} */ + getDefaultFoundation() { + return new MDCSwitchFoundation({ + addClass: (className) => this.root_.classList.add(className), + removeClass: (className) => this.root_.classList.remove(className), + setNativeControlChecked: (checked) => this.nativeControl_.checked = checked, + isNativeControlChecked: () => this.nativeControl_.checked, + setNativeControlDisabled: (disabled) => this.nativeControl_.disabled = disabled, + isNativeControlDisabled: () => this.nativeControl_.disabled, + }); + } + + /** @return {!MDCRipple} */ + get ripple() { + return this.ripple_; + } + + /** @return {boolean} */ + get checked() { + return this.foundation_.isChecked(); + } + + /** @param {boolean} checked */ + set checked(checked) { + this.foundation_.setChecked(checked); + } + + /** @return {boolean} */ + get disabled() { + return this.foundation_.isDisabled(); + } + + /** @param {boolean} disabled */ + set disabled(disabled) { + this.foundation_.setDisabled(disabled); + } +} + +export {MDCSwitchFoundation, MDCSwitch}; diff --git a/packages/mdc-switch/mdc-switch.scss b/packages/mdc-switch/mdc-switch.scss index 9e85409f282..b58c3d1d010 100644 --- a/packages/mdc-switch/mdc-switch.scss +++ b/packages/mdc-switch/mdc-switch.scss @@ -1,5 +1,5 @@ // -// Copyright 2016 Google Inc. All Rights Reserved. +// Copyright 2018 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -16,147 +16,118 @@ @import "@material/elevation/mixins"; @import "@material/rtl/mixins"; +@import "@material/ripple/common"; +@import "@material/ripple/mixins"; @import "./functions"; @import "./mixins"; @import "./variables"; -// postcss-bem-linter: define switch .mdc-switch { + @include mdc-switch-toggled-on-track-color($mdc-switch-baseline-theme-color); + @include mdc-switch-toggled-on-thumb-color($mdc-switch-baseline-theme-color); + @include mdc-switch-toggled-off-track-color($mdc-switch-toggled-off-track-color); + @include mdc-switch-toggled-off-thumb-color($mdc-switch-toggled-off-thumb-color); + @include mdc-switch-toggled-off-ripple-color($mdc-switch-toggled-off-ripple-color); + display: inline-block; position: relative; - - &__native-control { - @include mdc-switch-native-control_; - - display: inline-block; - margin-top: $mdc-switch-knob-vertical-offset_; - margin-left: 0; - transition: mdc-switch-transition(transform); - opacity: 0; - cursor: pointer; - z-index: 2; - - &:checked { - transform: translateX($mdc-switch-knob-active-margin); - - @include mdc-rtl { - transform: translateX(-($mdc-switch-knob-active-margin)); - } - } - } + outline: none; + user-select: none; } -@at-root { - @include mdc-switch-unchecked-track-color_($mdc-switch-unchecked-track-color); - @include mdc-switch-unchecked-knob-color_($mdc-switch-unchecked-knob-color); - @include mdc-switch-unchecked-focus-indicator-color_($mdc-switch-unchecked-focus-ring-color); - @include mdc-switch-track-color($mdc-switch-baseline-theme-color); - @include mdc-switch-knob-color($mdc-switch-baseline-theme-color); - @include mdc-switch-focus-indicator-color($mdc-switch-baseline-theme-color); +.mdc-switch__native-control { + @include mdc-rtl-reflexive-position(left, 0); + + position: absolute; + top: 0; + width: $mdc-switch-native-control-width; + height: $mdc-switch-tap-target-size; + margin: 0; + opacity: 0; + cursor: pointer; + pointer-events: auto; } -.mdc-switch__background { - display: block; - position: relative; +.mdc-switch__track { + box-sizing: border-box; width: $mdc-switch-track-width; height: $mdc-switch-track-height; - border-radius: 50%; - outline: none; - user-select: none; - - // Track - &::before { - display: block; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - transition: - mdc-switch-transition(opacity), - mdc-switch-transition(background-color), - mdc-switch-transition(border-color); - border: 1px solid; - border-radius: 7px; - opacity: .38; - content: ""; - } + transition: + mdc-switch-transition(opacity), + mdc-switch-transition(background-color), + mdc-switch-transition(border-color); + border: 1px solid; + border-radius: $mdc-switch-track-height / 2; + opacity: .38; } -.mdc-switch__knob { - @include mdc-elevation(2); - @include mdc-rtl-reflexive-position(left, 0); +.mdc-switch__thumb-underlay { + @include mdc-rtl-reflexive-position(left, $mdc-switch-tap-target-initial-position); + @include mdc-ripple-surface(); + @include mdc-ripple-radius-unbounded; + @include mdc-states($mdc-switch-baseline-theme-color); - display: block; + display: flex; position: absolute; - top: $mdc-switch-knob-vertical-offset_; - box-sizing: border-box; - width: $mdc-switch-knob-diameter; - height: $mdc-switch-knob-diameter; + // Ensures the knob is centered on the track. + top: -(($mdc-switch-tap-target-size - $mdc-switch-track-height) / 2); + align-items: center; + justify-content: center; + width: $mdc-switch-tap-target-size; + height: $mdc-switch-tap-target-size; transform: translateX(0); transition: mdc-switch-transition(transform), mdc-switch-transition(background-color), mdc-switch-transition(border-color); - border: $mdc-switch-knob-diameter / 2 solid; - border-radius: 50%; - z-index: 1; - - // Focus indicator - &::before { - @include mdc-switch-tap-target_; - - transform: scale(0); - transition: - mdc-switch-transition(transform), - mdc-switch-transition(background-color); - border-radius: 50%; - opacity: .2; - content: ""; - } } -// Focus indicator -.mdc-switch__native-control:focus ~ .mdc-switch__background .mdc-switch__knob::before { - transform: scale(1); +.mdc-switch__thumb { + @include mdc-elevation(2); + + box-sizing: border-box; + width: $mdc-switch-thumb-diameter; + height: $mdc-switch-thumb-diameter; + border: $mdc-switch-thumb-diameter / 2 solid; + border-radius: 50%; + // Allow events to go through to the native control, necessary for IE and Edge. + pointer-events: none; + z-index: 1; } -.mdc-switch__native-control:checked ~ .mdc-switch__background { - // Track - &::before { - opacity: .5; +.mdc-switch--checked { + .mdc-switch__track { + opacity: .54; } - .mdc-switch__knob { - transform: translateX($mdc-switch-knob-active-margin); + .mdc-switch__thumb-underlay { + transform: translateX($mdc-switch-thumb-active-margin); @include mdc-rtl { - transform: translateX(-($mdc-switch-knob-active-margin)); + transform: translateX(-($mdc-switch-thumb-active-margin)); } + } - // Focus indicator - &::before { - opacity: .15; + // Translate the native control the opposite direction so that the tap target stays the same. + .mdc-switch__native-control { + transform: translateX(-($mdc-switch-thumb-active-margin)); + + @include mdc-rtl { + transform: translateX($mdc-switch-thumb-active-margin); } } } -// postcss-bem-linter: end - -.mdc-switch__native-control:disabled { - cursor: initial; -} +.mdc-switch--disabled { + opacity: .38; + pointer-events: none; -.mdc-switch__native-control:disabled ~ .mdc-switch__background { - // Track - &::before { - background-color: $mdc-switch-unchecked-track-color; - opacity: .12; + .mdc-switch__thumb { + border-width: 1px; // In high contrast mode, only show outline of knob. } - .mdc-switch__knob { - border-width: 1px; // In high contrast mode, only show outline of the knob. - border-color: $mdc-switch-disabled-knob-color; - background-color: $mdc-switch-disabled-knob-color; + .mdc-switch__native-control { + cursor: default; + pointer-events: none; } } diff --git a/packages/mdc-switch/package.json b/packages/mdc-switch/package.json index 40873c54721..966b32a604b 100644 --- a/packages/mdc-switch/package.json +++ b/packages/mdc-switch/package.json @@ -8,17 +8,22 @@ "material design", "switch" ], + "main": "index.js", "repository": { "type": "git", "url": "https://github.com/material-components/material-components-web.git" }, "dependencies": { "@material/animation": "^0.34.0", + "@material/base": "^0.35.0", "@material/elevation": "^0.36.1", + "@material/ripple": "^0.37.1", "@material/rtl": "^0.36.0", + "@material/selection-control": "^0.37.1", "@material/theme": "^0.35.0" }, "publishConfig": { "access": "public" } } + diff --git a/packages/mdc-tab-bar/README.md b/packages/mdc-tab-bar/README.md new file mode 100644 index 00000000000..21e002b9bc2 --- /dev/null +++ b/packages/mdc-tab-bar/README.md @@ -0,0 +1,133 @@ + + +# Tab Bar + +Tabs organize and allow navigation between groups of content that are related and at the same level of hierarchy. +The Tab Bar contains the Tab Scroller and Tab components. + +## Design & API Documentation + + + +## Installation + +``` +npm install @material/tab-bar +``` + +## Basic Usage + +### HTML Structure + +```html +
+
+
+
+
+
+ + + +
+ +
+
+
+ +``` + +### Styles + +```scss +@import "@material/tab-bar/mdc-tab-bar"; +@import "@material/tab-scroller/mdc-tab-scroller"; +@import "@material/tab-indicator/mdc-tab-indicator"; +@import "@material/tab/mdc-tab"; +``` + +### JavaScript Instantiation + +```js +import {MDCTabBar} from '@material/tab-bar'; + +const tabBar = new MDCTabBar(document.querySelector('.mdc-tab-bar')); +``` + +> See [Importing the JS component](../../docs/importing-js.md) for more information on how to import JavaScript. + +## Style Customization + +### CSS Classes + +CSS Class | Description +--- | --- +`mdc-tab-bar` | Mandatory. + +### Sass Mixins + +To customize the width of the tab bar, use the following mixin. + +Mixin | Description +--- | --- +`mdc-tab-bar-width($width)` | Customizes the width of the tab bar. + +## `MDCTabBar` Properties and Methods + +Method Signature | Description +--- | --- +`activateTab(index: number) => void` | Activates the tab at the given index. +`scrollIntoView(index: number) => void` | Scrolls the tab at the given index into view. + +Event Name | Event Data Structure | Description +--- | --- | --- +`MDCTabBar:activated` | `{"detail": {"index": number}}` | Emitted when a Tab is activated with the index of the activated Tab. Listen for this to update content when a Tab becomes active. + +## Usage within Web Frameworks + +If you are using a JavaScript framework, such as React or Angular, you can create a Tab Bar for your framework. Depending on your needs, you can use the _Simple Approach: Wrapping MDC Web Vanilla Components_, or the _Advanced Approach: Using Foundations and Adapters_. Please follow the instructions [here](../../docs/integrating-into-frameworks.md). + +### `MDCTabBarAdapter` + +Method Signature | Description +--- | --- +`scrollTo(scrollX: number) => void` | Scrolls the Tab Scroller to the given position. +`incrementScroll(scrollXIncrement: number) => void` | Increments the Tab Scroller by the given value. +`getScrollPosition() => number` | Returns the scroll position of the Tab Scroller. +`getScrollContentWidth() => number` | Returns the width of the Tab Scroller's scroll content element. +`getOffsetWidth() => number` | Returns the offsetWidth of the root element. +`isRTL() => boolean` | Returns if the text direction is RTL. +`activateTabAtIndex(index: number, clientRect: ClientRect) => void` | Activates the Tab at the given index with the given clientRect. +`deactivateTabAtIndex(index) => void` | Deactivates the Tab at the given index. +`getTabIndicatorClientRectAtIndex(index: number) => ClientRect` | Returns the client rect of the Tab at the given index. +`getTabDimensionsAtIndex(index) => MDCTabDimensions` | Returns the dimensions of the Tab at the given index. +`getTabListLength() => number` | Returns the number of child Tab components. +`getActiveTabIndex() => number` | Returns the index of the active Tab. +`getIndexOfTab(tab: MDCTab) => number` | Returns the index of the given Tab instance. +`notifyTabActivated(index: number) => void` | Emits the `MDCTabBar:activated` event. + +### `MDCTabBarFoundation` + +Method Signature | Description +--- | --- +`activateTab(index: number) => void` | Activates the Tab at the given index. +`handleKeyDown(evt: Event) => void` | Handles the logic for the `"keydown"` event. +`handleTabInteraction(evt: Event) => void` | Handles the logic for the `"MDCTab:interacted"` event. +`scrollIntoView(index: number) => void` | Scrolls the Tab at the given index into view. diff --git a/packages/mdc-tab-bar/_mixins.scss b/packages/mdc-tab-bar/_mixins.scss new file mode 100644 index 00000000000..0ea61ac3562 --- /dev/null +++ b/packages/mdc-tab-bar/_mixins.scss @@ -0,0 +1,20 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +@mixin mdc-tab-bar-width($width) { + width: $width; +} diff --git a/packages/mdc-tab-bar/adapter.js b/packages/mdc-tab-bar/adapter.js new file mode 100644 index 00000000000..ed9afcd0594 --- /dev/null +++ b/packages/mdc-tab-bar/adapter.js @@ -0,0 +1,125 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +/* eslint no-unused-vars: [2, {"args": "none"}] */ + +/* eslint-disable no-unused-vars */ +import {MDCTabDimensions} from '@material/tab/adapter'; +import {MDCTab} from '@material/tab/index'; +/* eslint-enable no-unused-vars */ + +/** + * Adapter for MDC Tab Bar. + * + * Defines the shape of the adapter expected by the foundation. Implement this + * adapter to integrate the Tab Bar into your framework. See + * https://github.com/material-components/material-components-web/blob/master/docs/authoring-components.md + * for more information. + * + * @record + */ +class MDCTabBarAdapter { + /** + * Scrolls to the given position + * @param {number} scrollX The position to scroll to + */ + scrollTo(scrollX) {} + + /** + * Increments the current scroll position by the given amount + * @param {number} scrollXIncrement The amount to increment scroll + */ + incrementScroll(scrollXIncrement) {} + + /** + * Returns the current scroll position + * @return {number} + */ + getScrollPosition() {} + + /** + * Returns the width of the scroll content + * @return {number} + */ + getScrollContentWidth() {} + + /** + * Returns the root element's offsetWidth + * @return {number} + */ + getOffsetWidth() {} + + /** + * Returns if the Tab Bar language direction is RTL + * @return {boolean} + */ + isRTL() {} + + /** + * Activates the tab at the given index with the given client rect + * @param {number} index The index of the tab to activate + * @param {!ClientRect} clientRect The client rect of the previously active Tab Indicator + */ + activateTabAtIndex(index, clientRect) {} + + /** + * Deactivates the tab at the given index + * @param {number} index The index of the tab to activate + */ + deactivateTabAtIndex(index) {} + + /** + * Returns the client rect of the tab's indicator + * @param {number} index The index of the tab + * @return {!ClientRect} + */ + getTabIndicatorClientRectAtIndex(index) {} + + /** + * Returns the tab dimensions of the tab at the given index + * @param {number} index The index of the tab + * @return {!MDCTabDimensions} + */ + getTabDimensionsAtIndex(index) {} + + /** + * Returns the length of the tab list + * @return {number} + */ + getTabListLength() {} + + /** + * Returns the index of the active tab + * @return {number} + */ + getActiveTabIndex() {} + + /** + * Returns the index of the given tab + * @param {!MDCTab} tab The tab whose index to determin + * @return {number} + */ + getIndexOfTab(tab) {} + + /** + * Emits the MDCTabBar:activated event + * @param {number} index The index of the activated tab + */ + notifyTabActivated(index) {} +} + +export default MDCTabBarAdapter; diff --git a/packages/mdc-tab-bar/constants.js b/packages/mdc-tab-bar/constants.js new file mode 100644 index 00000000000..26f7d4899a6 --- /dev/null +++ b/packages/mdc-tab-bar/constants.js @@ -0,0 +1,41 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** @enum {string} */ +const strings = { + TAB_ACTIVATED_EVENT: 'MDCTabBar:activated', + TAB_SCROLLER_SELECTOR: '.mdc-tab-scroller', + TAB_SELECTOR: '.mdc-tab', + END_KEY: 'End', + HOME_KEY: 'Home', + ARROW_LEFT_KEY: 'ArrowLeft', + ARROW_RIGHT_KEY: 'ArrowRight', +}; + +/** @enum {number} */ +const numbers = { + EXTRA_SCROLL_AMOUNT: 20, + END_KEYCODE: 35, + HOME_KEYCODE: 36, + ARROW_LEFT_KEYCODE: 37, + ARROW_RIGHT_KEYCODE: 39, +}; + +export { + numbers, + strings, +}; diff --git a/packages/mdc-tab-bar/foundation.js b/packages/mdc-tab-bar/foundation.js new file mode 100644 index 00000000000..ccc30691db5 --- /dev/null +++ b/packages/mdc-tab-bar/foundation.js @@ -0,0 +1,402 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +import MDCFoundation from '@material/base/foundation'; + +import {strings, numbers} from './constants'; +import MDCTabBarAdapter from './adapter'; + +/* eslint-disable no-unused-vars */ +import MDCTabFoundation from '@material/tab/foundation'; +import {MDCTabDimensions} from '@material/tab/adapter'; +/* eslint-enable no-unused-vars */ + +/** + * @type {Set} + */ +const ACCEPTABLE_KEYS = new Set(); +// IE11 has no support for new Set with iterable so we need to initialize this by hand +ACCEPTABLE_KEYS.add(strings.ARROW_LEFT_KEY); +ACCEPTABLE_KEYS.add(strings.ARROW_RIGHT_KEY); +ACCEPTABLE_KEYS.add(strings.END_KEY); +ACCEPTABLE_KEYS.add(strings.HOME_KEY); + +/** + * @type {Map} + */ +const KEYCODE_MAP = new Map(); +// IE11 has no support for new Map with iterable so we need to initialize this by hand +KEYCODE_MAP.set(numbers.HOME_KEYCODE, strings.HOME_KEY); +KEYCODE_MAP.set(numbers.END_KEYCODE, strings.END_KEY); +KEYCODE_MAP.set(numbers.ARROW_LEFT_KEYCODE, strings.ARROW_LEFT_KEY); +KEYCODE_MAP.set(numbers.ARROW_RIGHT_KEYCODE, strings.ARROW_RIGHT_KEY); + +/** + * @extends {MDCFoundation} + * @final + */ +class MDCTabBarFoundation extends MDCFoundation { + /** @return enum {string} */ + static get strings() { + return strings; + } + + /** @return enum {number} */ + static get numbers() { + return numbers; + } + + /** + * @see MDCTabBarAdapter for typing information + * @return {!MDCTabBarAdapter} + */ + static get defaultAdapter() { + return /** @type {!MDCTabBarAdapter} */ ({ + scrollTo: () => {}, + incrementScroll: () => {}, + getScrollPosition: () => {}, + getScrollContentWidth: () => {}, + getOffsetWidth: () => {}, + isRTL: () => {}, + activateTabAtIndex: () => {}, + deactivateTabAtIndex: () => {}, + getTabIndicatorClientRectAtIndex: () => {}, + getTabDimensionsAtIndex: () => {}, + getActiveTabIndex: () => {}, + getIndexOfTab: () => {}, + getTabListLength: () => {}, + notifyTabActivated: () => {}, + }); + } + + /** + * @param {!MDCTabBarAdapter} adapter + * */ + constructor(adapter) { + super(Object.assign(MDCTabBarFoundation.defaultAdapter, adapter)); + } + + init() { + const activeIndex = this.adapter_.getActiveTabIndex(); + this.scrollIntoView(activeIndex); + } + + /** + * Activates the tab at the given index + * @param {number} index + */ + activateTab(index) { + const previousActiveIndex = this.adapter_.getActiveTabIndex(); + if (!this.indexIsInRange_(index)) { + return; + } + + this.adapter_.deactivateTabAtIndex(previousActiveIndex); + this.adapter_.activateTabAtIndex(index, this.adapter_.getTabIndicatorClientRectAtIndex(previousActiveIndex)); + this.scrollIntoView(index); + + // Only notify the tab activation if the index is different than the previously active index + if (index !== previousActiveIndex) { + this.adapter_.notifyTabActivated(index); + } + } + + /** + * Handles the keydown event + * @param {!Event} evt + */ + handleKeyDown(evt) { + // Get the key from the event + const key = this.getKeyFromEvent_(evt); + + // Early exit if the event key isn't one of the keyboard navigation keys + if (key === undefined) { + return; + } + + evt.preventDefault(); + this.activateTabFromKey_(key); + } + + /** + * Handles the MDCTab:interacted event + * @param {!Event} evt + */ + handleTabInteraction(evt) { + this.activateTab(this.adapter_.getIndexOfTab(evt.detail.tab)); + } + + /** + * Scrolls the tab at the given index into view + * @param {number} index The tab index to make visible + */ + scrollIntoView(index) { + // Early exit if the index is out of range + if (!this.indexIsInRange_(index)) { + return; + } + + // Always scroll to 0 if scrolling to the 0th index + if (index === 0) { + return this.adapter_.scrollTo(0); + } + + // Always scroll to the max value if scrolling to the Nth index + // MDCTabScroller.scrollTo() will never scroll past the max possible value + if (index === this.adapter_.getTabListLength() - 1) { + return this.adapter_.scrollTo(this.adapter_.getScrollContentWidth()); + } + + if (this.isRTL_()) { + return this.scrollIntoViewRTL_(index); + } + + this.scrollIntoView_(index); + } + + /** + * Private method for activating a tab from a key + * @param {string} key The name of the key + * @private + */ + activateTabFromKey_(key) { + const isRTL = this.isRTL_(); + const maxTabIndex = this.adapter_.getTabListLength() - 1; + const shouldGoToEnd = key === strings.END_KEY; + const shouldDecrement = key === strings.ARROW_LEFT_KEY && !isRTL || key === strings.ARROW_RIGHT_KEY && isRTL; + const shouldIncrement = key === strings.ARROW_RIGHT_KEY && !isRTL || key === strings.ARROW_LEFT_KEY && isRTL; + let tabIndex = this.adapter_.getActiveTabIndex(); + + if (shouldGoToEnd) { + tabIndex = maxTabIndex; + } else if (shouldDecrement) { + tabIndex -= 1; + } else if (shouldIncrement) { + tabIndex += 1; + } else { + tabIndex = 0; + } + + if (tabIndex < 0) { + tabIndex = maxTabIndex; + } else if (tabIndex > maxTabIndex) { + tabIndex = 0; + } + + this.activateTab(tabIndex); + } + + /** + * Calculates the scroll increment that will make the tab at the given index visible + * @param {number} index The index of the tab + * @param {number} nextIndex The index of the next tab + * @param {number} scrollPosition The current scroll position + * @param {number} barWidth The width of the Tab Bar + * @return {number} + * @private + */ + calculateScrollIncrement_(index, nextIndex, scrollPosition, barWidth) { + const nextTabDimensions = this.adapter_.getTabDimensionsAtIndex(nextIndex); + const relativeContentLeft = nextTabDimensions.contentLeft - scrollPosition - barWidth; + const relativeContentRight = nextTabDimensions.contentRight - scrollPosition; + const leftIncrement = relativeContentRight - numbers.EXTRA_SCROLL_AMOUNT; + const rightIncrement = relativeContentLeft + numbers.EXTRA_SCROLL_AMOUNT; + + if (nextIndex < index) { + return Math.min(leftIncrement, 0); + } + + return Math.max(rightIncrement, 0); + } + + /** + * Calculates the scroll increment that will make the tab at the given index visible in RTL + * @param {number} index The index of the tab + * @param {number} nextIndex The index of the next tab + * @param {number} scrollPosition The current scroll position + * @param {number} barWidth The width of the Tab Bar + * @param {number} scrollContentWidth The width of the scroll content + * @return {number} + * @private + */ + calculateScrollIncrementRTL_(index, nextIndex, scrollPosition, barWidth, scrollContentWidth, ) { + const nextTabDimensions = this.adapter_.getTabDimensionsAtIndex(nextIndex); + const relativeContentLeft = scrollContentWidth - nextTabDimensions.contentLeft - scrollPosition; + const relativeContentRight = scrollContentWidth - nextTabDimensions.contentRight - scrollPosition - barWidth; + const leftIncrement = relativeContentRight + numbers.EXTRA_SCROLL_AMOUNT; + const rightIncrement = relativeContentLeft - numbers.EXTRA_SCROLL_AMOUNT; + + if (nextIndex > index) { + return Math.max(leftIncrement, 0); + } + + return Math.min(rightIncrement, 0); + } + + /** + * Determines the index of the adjacent tab closest to either edge of the Tab Bar + * @param {number} index The index of the tab + * @param {!MDCTabDimensions} tabDimensions The dimensions of the tab + * @param {number} scrollPosition The current scroll position + * @param {number} barWidth The width of the tab bar + * @return {number} + * @private + */ + findAdjacentTabIndexClosestToEdge_(index, tabDimensions, scrollPosition, barWidth) { + /** + * Tabs are laid out in the Tab Scroller like this: + * + * Scroll Position + * +---+ + * | | Bar Width + * | +-----------------------------------+ + * | | | + * | V V + * | +-----------------------------------+ + * V | Tab Scroller | + * +------------+--------------+-------------------+ + * | Tab | Tab | Tab | + * +------------+--------------+-------------------+ + * | | + * +-----------------------------------+ + * + * To determine the next adjacent index, we look at the Tab root left and + * Tab root right, both relative to the scroll position. If the Tab root + * left is less than 0, then we know it's out of view to the left. If the + * Tab root right minus the bar width is greater than 0, we know the Tab is + * out of view to the right. From there, we either increment or decrement + * the index. + */ + const relativeRootLeft = tabDimensions.rootLeft - scrollPosition; + const relativeRootRight = tabDimensions.rootRight - scrollPosition - barWidth; + const relativeRootDelta = relativeRootLeft + relativeRootRight; + const leftEdgeIsCloser = relativeRootLeft < 0 || relativeRootDelta < 0; + const rightEdgeIsCloser = relativeRootRight > 0 || relativeRootDelta > 0; + + if (leftEdgeIsCloser) { + return index - 1; + } + + if (rightEdgeIsCloser) { + return index + 1; + } + + return -1; + } + + /** + * Determines the index of the adjacent tab closest to either edge of the Tab Bar in RTL + * @param {number} index The index of the tab + * @param {!MDCTabDimensions} tabDimensions The dimensions of the tab + * @param {number} scrollPosition The current scroll position + * @param {number} barWidth The width of the tab bar + * @param {number} scrollContentWidth The width of the scroller content + * @return {number} + * @private + */ + findAdjacentTabIndexClosestToEdgeRTL_(index, tabDimensions, scrollPosition, barWidth, scrollContentWidth) { + const rootLeft = scrollContentWidth - tabDimensions.rootLeft - barWidth - scrollPosition; + const rootRight = scrollContentWidth - tabDimensions.rootRight - scrollPosition; + const rootDelta = rootLeft + rootRight; + const leftEdgeIsCloser = rootLeft > 0 || rootDelta > 0; + const rightEdgeIsCloser = rootRight < 0 || rootDelta < 0; + + if (leftEdgeIsCloser) { + return index + 1; + } + + if (rightEdgeIsCloser) { + return index - 1; + } + + return -1; + } + + /** + * Returns the key associated with a keydown event + * @param {!Event} evt The keydown event + * @return {string} + * @private + */ + getKeyFromEvent_(evt) { + if (ACCEPTABLE_KEYS.has(evt.key)) { + return evt.key; + } + + return KEYCODE_MAP.get(evt.keyCode); + } + + /** + * Returns whether a given index is inclusively between the ends + * @param {number} index The index to test + * @private + */ + indexIsInRange_(index) { + return index >= 0 && index < this.adapter_.getTabListLength(); + } + + /** + * Returns the view's RTL property + * @return {boolean} + * @private + */ + isRTL_() { + return this.adapter_.isRTL(); + } + + /** + * Scrolls the tab at the given index into view for left-to-right useragents + * @param {number} index The index of the tab to scroll into view + * @private + */ + scrollIntoView_(index) { + const scrollPosition = this.adapter_.getScrollPosition(); + const barWidth = this.adapter_.getOffsetWidth(); + const tabDimensions = this.adapter_.getTabDimensionsAtIndex(index); + const nextIndex = this.findAdjacentTabIndexClosestToEdge_(index, tabDimensions, scrollPosition, barWidth); + + if (!this.indexIsInRange_(nextIndex)) { + return; + } + + const scrollIncrement = this.calculateScrollIncrement_(index, nextIndex, scrollPosition, barWidth); + this.adapter_.incrementScroll(scrollIncrement); + } + + /** + * Scrolls the tab at the given index into view in RTL + * @param {number} index The tab index to make visible + * @private + */ + scrollIntoViewRTL_(index) { + const scrollPosition = this.adapter_.getScrollPosition(); + const barWidth = this.adapter_.getOffsetWidth(); + const tabDimensions = this.adapter_.getTabDimensionsAtIndex(index); + const scrollWidth = this.adapter_.getScrollContentWidth(); + const nextIndex = this.findAdjacentTabIndexClosestToEdgeRTL_( + index, tabDimensions, scrollPosition, barWidth, scrollWidth); + + if (!this.indexIsInRange_(nextIndex)) { + return; + } + + const scrollIncrement = this.calculateScrollIncrementRTL_(index, nextIndex, scrollPosition, barWidth, scrollWidth); + this.adapter_.incrementScroll(scrollIncrement); + } +} + +export default MDCTabBarFoundation; diff --git a/packages/mdc-tab-bar/index.js b/packages/mdc-tab-bar/index.js new file mode 100644 index 00000000000..1ec6ea937d6 --- /dev/null +++ b/packages/mdc-tab-bar/index.js @@ -0,0 +1,148 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import MDCComponent from '@material/base/component'; + +import {MDCTab, MDCTabFoundation} from '@material/tab/index'; +import {MDCTabScroller} from '@material/tab-scroller/index'; + +import MDCTabBarAdapter from './adapter'; +import MDCTabBarFoundation from './foundation'; + +/** + * @extends {MDCComponent} + * @final + */ +class MDCTabBar extends MDCComponent { + /** + * @param {...?} args + */ + constructor(...args) { + super(...args); + + /** @private {!Array} */ + this.tabList_; + + /** @type {(function(!Element): !MDCTab)} */ + this.tabFactory_; + + /** @private {?MDCTabScroller} */ + this.tabScroller_; + + /** @type {(function(!Element): !MDCTabScroller)} */ + this.tabScrollerFactory_; + + /** @private {?function(?Event): undefined} */ + this.handleTabInteraction_; + + /** @private {?function(?Event): undefined} */ + this.handleKeyDown_; + } + + /** + * @param {!Element} root + * @return {!MDCTabBar} + */ + static attachTo(root) { + return new MDCTabBar(root); + } + + /** + * @param {(function(!Element): !MDCTab)=} tabFactory A function which creates a new MDCTab + * @param {(function(!Element): !MDCTabScroller)=} tabScrollerFactory A function which creates a new MDCTabScroller + */ + initialize( + tabFactory = (el) => new MDCTab(el), + tabScrollerFactory = (el) => new MDCTabScroller(el), + ) { + this.tabFactory_ = tabFactory; + this.tabScrollerFactory_ = tabScrollerFactory; + + const tabElements = [].slice.call(this.root_.querySelectorAll(MDCTabBarFoundation.strings.TAB_SELECTOR)); + this.tabList_ = tabElements.map((el) => this.tabFactory_(el)); + + const tabScrollerElement = this.root_.querySelector(MDCTabBarFoundation.strings.TAB_SCROLLER_SELECTOR); + if (tabScrollerElement) { + this.tabScroller_ = this.tabScrollerFactory_(tabScrollerElement); + } + } + + initialSyncWithDOM() { + this.handleTabInteraction_ = (evt) => this.foundation_.handleTabInteraction(evt); + this.handleKeyDown_ = (evt) => this.foundation_.handleKeyDown(evt); + + this.root_.addEventListener(MDCTabFoundation.strings.INTERACTED_EVENT, this.handleTabInteraction_); + this.root_.addEventListener('keydown', this.handleKeyDown_); + } + + destroy() { + super.destroy(); + this.root_.removeEventListener(MDCTabFoundation.strings.INTERACTED_EVENT, this.handleTabInteraction_); + this.root_.removeEventListener('keydown', this.handleKeyDown_); + this.tabList_.forEach((tab) => tab.destroy()); + this.tabScroller_.destroy(); + } + + /** + * @return {!MDCTabBarFoundation} + */ + getDefaultFoundation() { + return new MDCTabBarFoundation( + /** @type {!MDCTabBarAdapter} */ ({ + scrollTo: (scrollX) => this.tabScroller_.scrollTo(scrollX), + incrementScroll: (scrollXIncrement) => this.tabScroller_.incrementScroll(scrollXIncrement), + getScrollPosition: () => this.tabScroller_.getScrollPosition(), + getScrollContentWidth: () => this.tabScroller_.getScrollContentWidth(), + getOffsetWidth: () => this.root_.offsetWidth, + isRTL: () => window.getComputedStyle(this.root_).getPropertyValue('direction') === 'rtl', + activateTabAtIndex: (index, clientRect) => this.tabList_[index].activate(clientRect), + deactivateTabAtIndex: (index) => this.tabList_[index].deactivate(), + getTabIndicatorClientRectAtIndex: (index) => this.tabList_[index].computeIndicatorClientRect(), + getTabDimensionsAtIndex: (index) => this.tabList_[index].computeDimensions(), + getActiveTabIndex: () => { + for (let i = 0; i < this.tabList_.length; i++) { + if (this.tabList_[i].active) { + return i; + } + } + return -1; + }, + getIndexOfTab: (tabToFind) => this.tabList_.indexOf(tabToFind), + getTabListLength: () => this.tabList_.length, + notifyTabActivated: (index) => this.emit(MDCTabBarFoundation.strings.TAB_ACTIVATED_EVENT, {index}, true), + }) + ); + } + + /** + * Activates the tab at the given index + * @param {number} index The index of the tab + */ + activateTab(index) { + this.foundation_.activateTab(index); + } + + /** + * Scrolls the tab at the given index into view + * @param {number} index THe index of the tab + */ + scrollIntoView(index) { + this.foundation_.scrollIntoView(index); + } +} + +export {MDCTabBar, MDCTabBarFoundation}; diff --git a/packages/mdc-tab-bar/mdc-tab-bar.scss b/packages/mdc-tab-bar/mdc-tab-bar.scss new file mode 100644 index 00000000000..df351f43383 --- /dev/null +++ b/packages/mdc-tab-bar/mdc-tab-bar.scss @@ -0,0 +1,22 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import "./mixins"; + +.mdc-tab-bar { + @include mdc-tab-bar-width(100%); +} diff --git a/packages/mdc-tab-bar/package.json b/packages/mdc-tab-bar/package.json new file mode 100644 index 00000000000..0ac5e8a37ec --- /dev/null +++ b/packages/mdc-tab-bar/package.json @@ -0,0 +1,26 @@ +{ + "name": "@material/tab-bar", + "description": "The Material Components for the web tab bar component", + "version": "0.0.0", + "license": "Apache-2.0", + "keywords": [ + "material components", + "material design", + "tab", + "bar" + ], + "main": "index.js", + "repository": { + "type": "git", + "url": "https://github.com/material-components/material-components-web.git" + }, + "dependencies": { + "@material/base": "^0.35.0", + "@material/tab": "^0.37.1", + "@material/tab-scroller": "^0.0.0", + "@material/elevation": "^0.36.1" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/mdc-tab-indicator/README.md b/packages/mdc-tab-indicator/README.md new file mode 100644 index 00000000000..2e8860a3720 --- /dev/null +++ b/packages/mdc-tab-indicator/README.md @@ -0,0 +1,161 @@ + + +# Tab Indicator + +A Tab Indicator is a visual guide that shows which Tab is active. + +## Design & API Documentation + + + +## Installation + +``` +npm install @material/tab-indicator +``` + +## Basic Usage + +### HTML Structure + +```html + + + +``` + +### Styles + +```scss +@import "@material/tab/mdc-tab-indicator"; +``` + +### JavaScript Instantiation + +```js +import {MDCTabIndicator} from '@material/tab-indicator'; + +const tabIndicator = new MDCTabIndicator(document.querySelector('.mdc-tab-indicator')); +``` + +> See [Importing the JS component](../../docs/importing-js.md) for more information on how to import JavaScript. + +## Variants + +### Active Indicator + +Add the `mdc-tab-indicator--active` class to the `mdc-tab-indicator` element to make the Tab Indicator active. + +### Indicator Types and Transitions + +The Tab Indicator may be represented in one of two ways: + +* Underline, indicated by the `mdc-tab-indicator__content--underline` class +* Icon, indicated by the `mdc-tab-indicator__content--icon` class + +> *NOTE*: One of these classes _must_ be applied to the Tab Indicator's content element. + +The Tab Indicator may transition in one of two ways: + +* Slide, the default behavior +* Fade, indicated by the `mdc-tab-indicator--fade` class + +#### Sliding Underline Indicator + +```html + + + +``` + +#### Fading Icon Indicator + +You can use [Material Icons](https://material.io/icons/) from Google Fonts within your Fading Icon Indicator, or you can use your own icons. + +```html + + star + +``` + +#### Sliding Icon Indicator + +```html + + star + +``` + +## Style Customization + +### CSS Classes + +CSS Class | Description +--- | --- +`mdc-tab-indicator` | Mandatory. Contains the tab indicator content. +`mdc-tab-indicator__content` | Mandatory. Denotes the tab indicator content. +`mdc-tab-indicator--active` | Optional. Visually activates the indicator. +`mdc-tab-indicator--fade` | Optional. Sets up the tab indicator to fade in on activation and fade out on deactivation. +`mdc-tab-indicator__content--underline` | Optional. Denotes an underline tab indicator. +`mdc-tab-indicator__content--icon` | Optional. Denotes an icon tab indicator. + +> *NOTE*: Exactly one of the `--underline` or `--icon` content modifier classes should be present. + +### Sass Mixins + +To customize the tab indicator, use the following mixins. + +Mixin | Description +--- | --- +`mdc-tab-indicator-surface` | Mandatory. Must be applied to the parent element of the `mdc-tab-indicator`. +`mdc-tab-indicator-underline-color($color)` | Customizes the color of the underline. +`mdc-tab-indicator-icon-color($color)` | Customizes the color of the icon subelement. +`mdc-tab-indicator-underline-height($height)` | Customizes the height of the underline. +`mdc-tab-indicator-icon-height($height)` | Customizes the height of the icon subelement. +`mdc-tab-indicator-underline-top-corner-radius($radius)` | Customizes the top left and top right border radius of the underline child element. + +## `MDCTabIndicator` Methods + +Method Signature | Description +--- | --- +`activate(previousIndicatorClientRect: ClientRect) => void` | Activates the tab indicator. +`deactivate() => void` | Deactivates the tab indicator. +`computeContentClientRect() => ClientRect` | Returns the content element bounding client rect. + +## Usage within Web Frameworks + +If you are using a JavaScript framework, such as React or Angular, you can create a Tab Indicator for your framework. Depending on your needs, you can use the _Simple Approach: Wrapping MDC Web Vanilla Components_, or the _Advanced Approach: Using Foundations and Adapters_. Please follow the instructions [here](../../docs/integrating-into-frameworks.md). + +### `MDCTabIndicatorAdapter` + +Method Signature | Description +--- | --- +`addClass(className: string) => void` | Adds a class to the root element. +`removeClass(className: string) => void` | Removes a class from the root element. +`registerEventHandler(evtType: string, handler: EventListener) => void` | Registers an event listener on the root element. +`deregisterEventHandler(evtType: string, handler: EventListener) => void` | Deregisters an event listener on the root element. +`setContentStyleProp(property: string, value: string) => void` | Sets the style property of the content element. +`computeContentClientRect() => ClientRect` | Returns the content element's bounding client rect. + +### `MDCTabIndicatorFoundation` + +Method Signature | Description +--- | --- +`handleTransitionEnd(evt: Event) => void` | Handles the logic for the `"transitionend"` event on the root element. +`activate(previousIndicatorClientRect: ClientRect) => void` | Activates the tab indicator. +`deactivate() => void` | Deactivates the tab indicator. +`computeContentClientRect() => ClientRect` | Returns the content element's bounding client rect. diff --git a/packages/mdc-tab-indicator/_mixins.scss b/packages/mdc-tab-indicator/_mixins.scss new file mode 100644 index 00000000000..47919ff0837 --- /dev/null +++ b/packages/mdc-tab-indicator/_mixins.scss @@ -0,0 +1,54 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +@import "@material/theme/mixins"; + +@mixin mdc-tab-indicator-surface { + position: relative; +} + +@mixin mdc-tab-indicator-underline-color($color) { + > .mdc-tab-indicator__content--underline { + @include mdc-theme-prop(background-color, $color); + } +} + +@mixin mdc-tab-indicator-underline-height($height) { + > .mdc-tab-indicator__content--underline { + height: $height; + } +} + +@mixin mdc-tab-indicator-underline-top-corner-radius($radius) { + > .mdc-tab-indicator__content--underline { + border-top-left-radius: $radius; + border-top-right-radius: $radius; + } +} + +@mixin mdc-tab-indicator-icon-color($color) { + > .mdc-tab-indicator__content--icon { + @include mdc-theme-prop(color, $color); + } +} + +@mixin mdc-tab-indicator-icon-height($height) { + > .mdc-tab-indicator__content--icon { + height: $height; + font-size: $height; + } +} diff --git a/packages/mdc-tab-indicator/adapter.js b/packages/mdc-tab-indicator/adapter.js new file mode 100644 index 00000000000..b3f3edf474b --- /dev/null +++ b/packages/mdc-tab-indicator/adapter.js @@ -0,0 +1,71 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +/* eslint no-unused-vars: [2, {"args": "none"}] */ + +/** + * Adapter for MDC Tab Indicator. + * + * Defines the shape of the adapter expected by the foundation. Implement this + * adapter to integrate the Tab Indicator into your framework. See + * https://github.com/material-components/material-components-web/blob/master/docs/authoring-components.md + * for more information. + * + * @record + */ +class MDCTabIndicatorAdapter { + /** + * Registers an event listener on the root element for a given event. + * @param {string} evtType + * @param {function(!Event): undefined} handler + */ + registerEventHandler(evtType, handler) {} + + /** + * Deregisters an event listener on the root element for a given event. + * @param {string} evtType + * @param {function(!Event): undefined} handler + */ + deregisterEventHandler(evtType, handler) {} + + /** + * Adds the given className to the root element. + * @param {string} className The className to add + */ + addClass(className) {} + + /** + * Removes the given className from the root element. + * @param {string} className The className to remove + */ + removeClass(className) {} + + /** + * Returns the client rect of the content element. + * @return {!ClientRect} + */ + computeContentClientRect() {} + + /** + * Sets a style property of the content element to the passed value + * @param {string} propName The style property name to set + * @param {string} value The style property value + */ + setContentStyleProperty(propName, value) {} +} + +export default MDCTabIndicatorAdapter; diff --git a/packages/mdc-tab-indicator/constants.js b/packages/mdc-tab-indicator/constants.js new file mode 100644 index 00000000000..0fcc30e4543 --- /dev/null +++ b/packages/mdc-tab-indicator/constants.js @@ -0,0 +1,35 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +/** @enum {string} */ +const cssClasses = { + ACTIVE: 'mdc-tab-indicator--active', + FADE: 'mdc-tab-indicator--fade', + FADING_ACTIVATE: 'mdc-tab-indicator--fading-activate', + FADING_DEACTIVATE: 'mdc-tab-indicator--fading-deactivate', + SLIDING_ACTIVATE: 'mdc-tab-indicator--sliding-activate', +}; + +/** @enum {string} */ +const strings = { + CONTENT_SELECTOR: '.mdc-tab-indicator__content', +}; + +export { + cssClasses, + strings, +}; diff --git a/packages/mdc-tab-indicator/fading-foundation.js b/packages/mdc-tab-indicator/fading-foundation.js new file mode 100644 index 00000000000..b86a96306ad --- /dev/null +++ b/packages/mdc-tab-indicator/fading-foundation.js @@ -0,0 +1,53 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +import MDCTabIndicatorFoundation from './foundation'; + +/** + * @extends {MDCTabIndicatorFoundation} + * @final + */ +class MDCFadingTabIndicatorFoundation extends MDCTabIndicatorFoundation { + /** @param {...?} args */ + constructor(...args) { + super(...args); + + /** @private {function(?Event): undefined} */ + this.handleTransitionEnd_ = () => this.handleTransitionEnd(); + } + + /** Handles the transitionend event */ + handleTransitionEnd() { + this.adapter_.deregisterEventHandler('transitionend', this.handleTransitionEnd_); + this.adapter_.removeClass(MDCTabIndicatorFoundation.cssClasses.FADING_ACTIVATE); + this.adapter_.removeClass(MDCTabIndicatorFoundation.cssClasses.FADING_DEACTIVATE); + } + + activate() { + this.adapter_.registerEventHandler('transitionend', this.handleTransitionEnd_); + this.adapter_.addClass(MDCTabIndicatorFoundation.cssClasses.FADING_ACTIVATE); + this.adapter_.addClass(MDCTabIndicatorFoundation.cssClasses.ACTIVE); + } + + deactivate() { + this.adapter_.registerEventHandler('transitionend', this.handleTransitionEnd_); + this.adapter_.addClass(MDCTabIndicatorFoundation.cssClasses.FADING_DEACTIVATE); + this.adapter_.removeClass(MDCTabIndicatorFoundation.cssClasses.ACTIVE); + } +} + +export default MDCFadingTabIndicatorFoundation; diff --git a/packages/mdc-tab-indicator/foundation.js b/packages/mdc-tab-indicator/foundation.js new file mode 100644 index 00000000000..fd160f38386 --- /dev/null +++ b/packages/mdc-tab-indicator/foundation.js @@ -0,0 +1,76 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +import MDCFoundation from '@material/base/foundation'; +import MDCTabIndicatorAdapter from './adapter'; +import { + cssClasses, + strings, +} from './constants'; + +/** + * @extends {MDCFoundation} + * @abstract + */ +class MDCTabIndicatorFoundation extends MDCFoundation { + /** @return enum {string} */ + static get cssClasses() { + return cssClasses; + } + + /** @return enum {string} */ + static get strings() { + return strings; + } + + /** + * @see MDCTabIndicatorAdapter for typing information + * @return {!MDCTabIndicatorAdapter} + */ + static get defaultAdapter() { + return /** @type {!MDCTabIndicatorAdapter} */ ({ + registerEventHandler: () => {}, + deregisterEventHandler: () => {}, + addClass: () => {}, + removeClass: () => {}, + computeContentClientRect: () => {}, + setContentStyleProperty: () => {}, + }); + } + + /** @param {!MDCTabIndicatorAdapter} adapter */ + constructor(adapter) { + super(Object.assign(MDCTabIndicatorFoundation.defaultAdapter, adapter)); + } + + /** @return {!ClientRect} */ + computeContentClientRect() { + return this.adapter_.computeContentClientRect(); + } + + /** + * Activates the indicator + * @param {!ClientRect=} previousIndicatorClientRect + * @abstract + */ + activate(previousIndicatorClientRect) {} // eslint-disable-line no-unused-vars + + /** @abstract */ + deactivate() {} +} + +export default MDCTabIndicatorFoundation; diff --git a/packages/mdc-tab-indicator/index.js b/packages/mdc-tab-indicator/index.js new file mode 100644 index 00000000000..47f120abeaa --- /dev/null +++ b/packages/mdc-tab-indicator/index.js @@ -0,0 +1,92 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +import MDCComponent from '@material/base/component'; + +import MDCTabIndicatorAdapter from './adapter'; +import MDCTabIndicatorFoundation from './foundation'; + +import MDCSlidingTabIndicatorFoundation from './sliding-foundation'; +import MDCFadingTabIndicatorFoundation from './fading-foundation'; + +/** + * @extends {MDCComponent} + * @final + */ +class MDCTabIndicator extends MDCComponent { + /** + * @param {!Element} root + * @return {!MDCTabIndicator} + */ + static attachTo(root) { + return new MDCTabIndicator(root); + } + + /** + * @param {...?} args + */ + constructor(...args) { + super(...args); + /** @type {?Element} */ + this.content_; + } + + initialize() { + this.content_ = this.root_.querySelector(MDCTabIndicatorFoundation.strings.CONTENT_SELECTOR); + } + + /** + * @return {!ClientRect} + */ + computeContentClientRect() { + return this.foundation_.computeContentClientRect(); + } + + /** + * @return {!MDCTabIndicatorFoundation} + */ + getDefaultFoundation() { + const adapter = /** @type {!MDCTabIndicatorAdapter} */ (Object.assign({ + registerEventHandler: (evtType, handler) => this.root_.addEventListener(evtType, handler), + deregisterEventHandler: (evtType, handler) => this.root_.removeEventListener(evtType, handler), + addClass: (className) => this.root_.classList.add(className), + removeClass: (className) => this.root_.classList.remove(className), + computeContentClientRect: () => this.content_.getBoundingClientRect(), + setContentStyleProperty: (prop, value) => this.content_.style.setProperty(prop, value), + })); + + if (this.root_.classList.contains(MDCTabIndicatorFoundation.cssClasses.FADE)) { + return new MDCFadingTabIndicatorFoundation(adapter); + } + + // Default to the sliding indicator + return new MDCSlidingTabIndicatorFoundation(adapter); + } + + /** + * @param {!ClientRect=} previousIndicatorClientRect + */ + activate(previousIndicatorClientRect) { + this.foundation_.activate(previousIndicatorClientRect); + } + + deactivate() { + this.foundation_.deactivate(); + } +} + +export {MDCTabIndicator, MDCTabIndicatorFoundation, MDCSlidingTabIndicatorFoundation, MDCFadingTabIndicatorFoundation}; diff --git a/packages/mdc-tab-indicator/mdc-tab-indicator.scss b/packages/mdc-tab-indicator/mdc-tab-indicator.scss new file mode 100644 index 00000000000..445403901d5 --- /dev/null +++ b/packages/mdc-tab-indicator/mdc-tab-indicator.scss @@ -0,0 +1,68 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +@import "@material/animation/variables"; +@import "./mixins"; + +.mdc-tab-indicator { + @include mdc-tab-indicator-underline-color(primary); + @include mdc-tab-indicator-underline-height(2px); + @include mdc-tab-indicator-icon-color(secondary); + @include mdc-tab-indicator-icon-height(34px); + + display: flex; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 1; +} + +.mdc-tab-indicator__content { + transform-origin: left; + opacity: 0; +} + +.mdc-tab-indicator__content--underline { + align-self: flex-end; + width: 100%; +} + +.mdc-tab-indicator__content--icon { + align-self: center; + margin: 0 auto; +} + +.mdc-tab-indicator--active > .mdc-tab-indicator__content { + opacity: 1; +} + +.mdc-tab-indicator--sliding-activate > .mdc-tab-indicator__content { + transition: 250ms transform $mdc-animation-standard-curve-timing-function; +} + +.mdc-tab-indicator--fading-activate > .mdc-tab-indicator__content, +.mdc-tab-indicator--fading-deactivate > .mdc-tab-indicator__content { + transition: 150ms opacity linear; +} + +.mdc-tab-indicator--fading-activate > .mdc-tab-indicator__content { + transition-delay: 100ms; +} + diff --git a/packages/mdc-tab-indicator/package.json b/packages/mdc-tab-indicator/package.json new file mode 100644 index 00000000000..e954f317593 --- /dev/null +++ b/packages/mdc-tab-indicator/package.json @@ -0,0 +1,25 @@ +{ + "name": "@material/tab-indicator", + "description": "The Material Components for the web tab indicator component", + "version": "0.0.0", + "license": "Apache-2.0", + "keywords": [ + "material components", + "material design", + "tab", + "indicator" + ], + "main": "index.js", + "repository": { + "type": "git", + "url": "https://github.com/material-components/material-components-web.git" + }, + "dependencies": { + "@material/animation": "^0.34.0", + "@material/base": "^0.35.0", + "@material/theme": "^0.35.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/mdc-tab-indicator/sliding-foundation.js b/packages/mdc-tab-indicator/sliding-foundation.js new file mode 100644 index 00000000000..b601f7e8879 --- /dev/null +++ b/packages/mdc-tab-indicator/sliding-foundation.js @@ -0,0 +1,79 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +import MDCTabIndicatorFoundation from './foundation'; + +/** + * @extends {MDCTabIndicatorFoundation} + * @final + */ +class MDCSlidingTabIndicatorFoundation extends MDCTabIndicatorFoundation { + /** @param {...?} args */ + constructor(...args) { + super(...args); + + /** @private {function(?Event): undefined} */ + this.handleTransitionEnd_ = () => this.handleTransitionEnd(); + } + + /** Handles the transitionend event */ + handleTransitionEnd() { + this.adapter_.deregisterEventHandler('transitionend', this.handleTransitionEnd_); + this.adapter_.removeClass(MDCTabIndicatorFoundation.cssClasses.SLIDING_ACTIVATE); + } + + /** @param {!ClientRect=} previousIndicatorClientRect */ + activate(previousIndicatorClientRect) { + this.adapter_.addClass(MDCTabIndicatorFoundation.cssClasses.ACTIVE); + + // Early exit if no indicator is present to handle cases where an indicator + // may be activated without a prior indicator state + if (!previousIndicatorClientRect) { + return; + } + + // This animation uses the FLIP approach. You can read more about it at the link below: + // https://aerotwist.com/blog/flip-your-animations/ + + // Calculate the dimensions based on the dimensions of the previous indicator + const currentClientRect = this.computeContentClientRect(); + const widthDelta = previousIndicatorClientRect.width / currentClientRect.width; + const xPosition = previousIndicatorClientRect.left - currentClientRect.left; + this.adapter_.setContentStyleProperty('transform', `translateX(${xPosition}px) scaleX(${widthDelta})`); + + // Force repaint + this.computeContentClientRect(); + + // Add animating class and remove transformation in a new frame + requestAnimationFrame(() => { + this.adapter_.addClass(MDCTabIndicatorFoundation.cssClasses.SLIDING_ACTIVATE); + this.adapter_.setContentStyleProperty('transform', ''); + }); + + this.adapter_.registerEventHandler('transitionend', this.handleTransitionEnd_); + } + + deactivate() { + this.adapter_.removeClass(MDCTabIndicatorFoundation.cssClasses.ACTIVE); + // We remove the animating class in deactivate in case the Tab is deactivated before the animation completes and + // the "transitionend" handler isn't called. + this.adapter_.removeClass(MDCTabIndicatorFoundation.cssClasses.SLIDING_ACTIVATE); + this.adapter_.deregisterEventHandler('transitionend', this.handleTransitionEnd_); + } +} + +export default MDCSlidingTabIndicatorFoundation; diff --git a/packages/mdc-tab-scroller/README.md b/packages/mdc-tab-scroller/README.md new file mode 100644 index 00000000000..01cc2267f85 --- /dev/null +++ b/packages/mdc-tab-scroller/README.md @@ -0,0 +1,117 @@ + + +# Tab Scroller + +A Tab Scroller allows for smooth native and animated scrolling of tabs. + +## Design & API Documentation + + + +## Installation + +``` +npm install @material/tab-scroller +``` + +## Basic Usage + +### HTML Structure + +```html +
+
+
+
+
+``` + +### Styles + +```scss +@import "@material/tab/mdc-tab-scroller"; +``` + +### JavaScript Instantiation + +```js +import {MDCTabScroller} from '@material/tab-scroller'; + +const tabScroller = new MDCTabScroller(document.querySelector('.mdc-tab-scroller')); +``` + +> See [Importing the JS component](../../docs/importing-js.md) for more information on how to import JavaScript. + +## Style Customization + +### CSS Classes + +CSS Class | Description +--- | --- +`mdc-tab-scroller` | Mandatory. Contains the tab scroller content. +`mdc-tab-scroller__scroll-area` | Mandatory. Denotes the scrolling area. +`mdc-tab-scroller__scroll-content` | Mandatory. Denotes the scrolling content. +`mdc-tab-scroller--align-start` | Optional. Sets the elements inside the scroll content element to be aligned to the start of the scroll content element. +`mdc-tab-scroller--align-end` | Optional. Sets the elements inside the scroll content element to be aligned to the end of the scroll content element. +`mdc-tab-scroller--align-center` | Optional. Sets the elements inside the scroll content element to be aligned to the center of the scroll content element. + +## `MDCTabScroller` Methods + +Method Signature | Description +--- | --- +`scrollTo(scrollX: number) => void` | Scrolls to the scrollX value. +`incrementScroll(scrollX: number) => void` | Increments the current scroll value by the scrollX value. +`getScrollPosition() => number` | Returns the current visual scroll position. +`getScrollContentWidth() => number` | Returns the width of the scroll content element. + +## Usage within Web Frameworks + +If you are using a JavaScript framework, such as React or Angular, you can create a Tab Scroller for your framework. Depending on your needs, you can use the _Simple Approach: Wrapping MDC Web Vanilla Components_, or the _Advanced Approach: Using Foundations and Adapters_. Please follow the instructions [here](../../docs/integrating-into-frameworks.md). + +### `MDCTabScrollerAdapter` + +Method Signature | Description +--- | --- +`eventTargetMatchesSelector(eventTarget: EventTarget, selector: string) => boolean` | Returns `true` if the given event target satisfies the given CSS selector. +`addClass(className: string) => void` | Adds a class to the root element. +`addScrollAreaClass(className: string) => void` | Adds a class to the scroll area element. +`removeClass(className: string) => void` | Removes a class from the root element. +`setScrollAreaStyleProperty(property: string, value: string) => void` | Sets the given style property on the scroll area element. +`setScrollContentStyleProperty(property: string, value: string) => void` | Sets the given style property on the scroll content element. +`getScrollContentStyleValue(property: string) => string` | Returns the given style property value on the scroll content element. +`setScrollAreaScrollLeft(scrollLeft: number) => void` | Sets the scroll area element's `scrollLeft`. +`getScrollAreaScrollLeft() => number` | Returns the scroll area element's `scrollLeft`. +`getScrollContentOffsetWidth() => number` | Returns the scroll content element's `offsetWidth`. +`getScrollAreaOffsetWidth() => number` | Returns the scroll area element's `offsetWidth`. +`computeHorizontalScrollbarHeight() => number` | Returns the height of the browser's horizontal scrollbars (in px). + +#### `util` Functions + +MDC Tab Scroller provides a `util` module with functions to help implement adapter methods. + +Function Signature | Description +--- | --- +`computeHorizontalScrollbarHeight(document: Document) => number` | Returns the height of the browser's horizontal scrollbars (in px). +`getMatchesProperty(HTMLElementPrototype: Object) => string` | Returns the appropriate property name for the `matches` API in the current browser environment. + +### `MDCTabScrollerFoundation` + +Method Signature | Description +--- | --- +`scrollTo(scrollX: number) => void` | Scrolls to the `scrollX` value. +`incrementScroll(scrollX: number) => void` | Increments the current scroll value by the `scrollX` value. +`getScrollPosition() => number` | Returns the current visual scroll position. diff --git a/packages/mdc-tab-scroller/adapter.js b/packages/mdc-tab-scroller/adapter.js new file mode 100644 index 00000000000..78bfd46fe8a --- /dev/null +++ b/packages/mdc-tab-scroller/adapter.js @@ -0,0 +1,145 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint no-unused-vars: [2, {"args": "none"}] */ + +/** + * MDCTabScrollerAnimation contains the values required for animating from the + * current scroll position to the new scroll position. The "finalScrollPosition" + * value represents the new scroll position while the "scrollDelta" value is the + * corresponding transformation that is applied to the scroll content. Together, + * they create the animation by first updating the scroll value then applying + * the transformation and animating the transition. Both pieces are necessary + * for the scroll animation to work. The values are used as-is by the tab + * scroller animation method, ensuring that all logic for determining scroll + * position or transformation is abstracted away from the animation method. + * @typedef {{finalScrollPosition: number, scrollDelta: number}} + */ +let MDCTabScrollerAnimation; + +/** + * MDCTabScrollerHorizontalEdges represents the left and right edges of the + * scroll content. These values vary depending on how scrolling in RTL is + * implemented by the browser. One value is always 0 and one value is always + * the max scrollable value as either a positive or negative integer. + * @typedef {{left: number, right: number}} + */ +let MDCTabScrollerHorizontalEdges; + +/** + * Adapter for MDC Tab Scroller. + * + * Defines the shape of the adapter expected by the foundation. Implement this + * adapter to integrate the Tab into your framework. See + * https://github.com/material-components/material-components-web/blob/master/docs/authoring-components.md + * for more information. + * + * @record + */ +class MDCTabScrollerAdapter { + /** + * Adds the given className to the root element. + * @param {string} className The className to add + */ + addClass(className) {} + + /** + * Removes the given className from the root element. + * @param {string} className The className to remove + */ + removeClass(className) {} + + /** + * Adds the given className to the scroll area element. + * @param {string} className The className to add + */ + addScrollAreaClass(className) {} + + /** + * Returns whether the event target matches given className. + * @param {EventTarget} evtTarget The event target + * @param {string} selector The selector to check + * @return {boolean} + */ + eventTargetMatchesSelector(evtTarget, selector) {} + + /** + * Sets a style property of the area element to the passed value. + * @param {string} propName The style property name to set + * @param {string} value The style property value + */ + setScrollAreaStyleProperty(propName, value) {} + + /** + * Sets a style property of the content element to the passed value. + * @param {string} propName The style property name to set + * @param {string} value The style property value + */ + setScrollContentStyleProperty(propName, value) {} + + /** + * Returns the scroll content element's computed style value of the given css property `propertyName`. + * We achieve this via `getComputedStyle(...).getPropertyValue(propertyName)`. + * @param {string} propertyName + * @return {string} + */ + getScrollContentStyleValue(propertyName) {} + + /** + * Sets the scrollLeft value of the scroll area element to the passed value. + * @param {number} scrollLeft The new scrollLeft value + */ + setScrollAreaScrollLeft(scrollLeft) {} + + /** + * Returns the scrollLeft value of the scroll area element. + * @return {number} + */ + getScrollAreaScrollLeft() {} + + /** + * Returns the offsetWidth of the scroll content element. + * @return {number} + */ + getScrollContentOffsetWidth() {} + + /** + * Returns the offsetWitdth of the scroll area element. + * @return {number} + */ + getScrollAreaOffsetWidth() {} + + /** + * Returns the bounding client rect of the scroll area element. + * @return {!ClientRect} + */ + computeScrollAreaClientRect() {} + + /** + * Returns the bounding client rect of the scroll content element. + * @return {!ClientRect} + */ + computeScrollContentClientRect() {} + + /** + * Returns the height of the browser's horizontal scrollbars (in px). + * @return {number} + */ + computeHorizontalScrollbarHeight() {} +} + +export {MDCTabScrollerAnimation, MDCTabScrollerHorizontalEdges, MDCTabScrollerAdapter}; diff --git a/packages/mdc-tab-scroller/constants.js b/packages/mdc-tab-scroller/constants.js new file mode 100644 index 00000000000..3ac76abb13f --- /dev/null +++ b/packages/mdc-tab-scroller/constants.js @@ -0,0 +1,34 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +/** @enum {string} */ +const cssClasses = { + ANIMATING: 'mdc-tab-scroller--animating', + SCROLL_TEST: 'mdc-tab-scroller__test', + SCROLL_AREA_SCROLL: 'mdc-tab-scroller__scroll-area--scroll', +}; + +/** @enum {string} */ +const strings = { + AREA_SELECTOR: '.mdc-tab-scroller__scroll-area', + CONTENT_SELECTOR: '.mdc-tab-scroller__scroll-content', +}; + +export { + cssClasses, + strings, +}; diff --git a/packages/mdc-tab-scroller/foundation.js b/packages/mdc-tab-scroller/foundation.js new file mode 100644 index 00000000000..8bf9a5a24e4 --- /dev/null +++ b/packages/mdc-tab-scroller/foundation.js @@ -0,0 +1,396 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +import MDCFoundation from '@material/base/foundation'; +import {cssClasses, strings} from './constants'; +/* eslint-disable no-unused-vars */ +import {MDCTabScrollerAnimation, MDCTabScrollerHorizontalEdges, MDCTabScrollerAdapter} from './adapter'; +import MDCTabScrollerRTL from './rtl-scroller'; +/* eslint-enable no-unused-vars */ +import MDCTabScrollerRTLDefault from './rtl-default-scroller'; +import MDCTabScrollerRTLNegative from './rtl-negative-scroller'; +import MDCTabScrollerRTLReverse from './rtl-reverse-scroller'; + +/** + * @extends {MDCFoundation} + * @final + */ +class MDCTabScrollerFoundation extends MDCFoundation { + /** @return enum {string} */ + static get cssClasses() { + return cssClasses; + } + + /** @return enum {string} */ + static get strings() { + return strings; + } + + /** + * @see MDCTabScrollerAdapter for typing information + * @return {!MDCTabScrollerAdapter} + */ + static get defaultAdapter() { + return /** @type {!MDCTabScrollerAdapter} */ ({ + eventTargetMatchesSelector: () => {}, + addClass: () => {}, + removeClass: () => {}, + addScrollAreaClass: () => {}, + setScrollAreaStyleProperty: () => {}, + setScrollContentStyleProperty: () => {}, + getScrollContentStyleValue: () => {}, + setScrollAreaScrollLeft: () => {}, + getScrollAreaScrollLeft: () => {}, + getScrollContentOffsetWidth: () => {}, + getScrollAreaOffsetWidth: () => {}, + computeScrollAreaClientRect: () => {}, + computeScrollContentClientRect: () => {}, + computeHorizontalScrollbarHeight: () => {}, + }); + } + + /** @param {!MDCTabScrollerAdapter} adapter */ + constructor(adapter) { + super(Object.assign(MDCTabScrollerFoundation.defaultAdapter, adapter)); + + /** + * This boolean controls whether we should handle the transitionend and interaction events during the animation. + * @private {boolean} + */ + this.isAnimating_ = false; + + /** + * The MDCTabScrollerRTL instance varies per browser and allows us to encapsulate the peculiar browser behavior + * of RTL scrolling in it's own class. + * @private {?MDCTabScrollerRTL} + */ + this.rtlScrollerInstance_; + } + + init() { + // Compute horizontal scrollbar height on scroller with overflow initially hidden, then update overflow to scroll + // and immediately adjust bottom margin to avoid the scrollbar initially appearing before JS runs. + const horizontalScrollbarHeight = this.adapter_.computeHorizontalScrollbarHeight(); + this.adapter_.setScrollAreaStyleProperty('margin-bottom', -horizontalScrollbarHeight + 'px'); + this.adapter_.addScrollAreaClass(MDCTabScrollerFoundation.cssClasses.SCROLL_AREA_SCROLL); + } + + /** + * Computes the current visual scroll position + * @return {number} + */ + getScrollPosition() { + if (this.isRTL_()) { + return this.computeCurrentScrollPositionRTL_(); + } + + const currentTranslateX = this.calculateCurrentTranslateX_(); + const scrollLeft = this.adapter_.getScrollAreaScrollLeft(); + return scrollLeft - currentTranslateX; + } + + /** + * Handles interaction events that occur during transition + */ + handleInteraction() { + // Early exit if we aren't animating + if (!this.isAnimating_) { + return; + } + + // Prevent other event listeners from handling this event + this.stopScrollAnimation_(); + } + + /** + * Handles the transitionend event + * @param {!Event} evt + */ + handleTransitionEnd(evt) { + // Early exit if we aren't animating or the event was triggered by a different element. + if (!this.isAnimating_ + || !this.adapter_.eventTargetMatchesSelector(evt.target, MDCTabScrollerFoundation.strings.CONTENT_SELECTOR)) { + return; + } + + this.isAnimating_ = false; + this.adapter_.removeClass(MDCTabScrollerFoundation.cssClasses.ANIMATING); + } + + /** + * Increment the scroll value by the scrollXIncrement + * @param {number} scrollXIncrement The value by which to increment the scroll position + */ + incrementScroll(scrollXIncrement) { + // Early exit for non-operational increment values + if (scrollXIncrement === 0) { + return; + } + + if (this.isRTL_()) { + return this.incrementScrollRTL_(scrollXIncrement); + } + + this.incrementScroll_(scrollXIncrement); + } + + /** + * Scrolls to the given scrollX value + * @param {number} scrollX + */ + scrollTo(scrollX) { + if (this.isRTL_()) { + return this.scrollToRTL_(scrollX); + } + + this.scrollTo_(scrollX); + } + + /** + * Returns the appropriate version of the MDCTabScrollerRTL + * @return {!MDCTabScrollerRTL} + */ + getRTLScroller() { + if (!this.rtlScrollerInstance_) { + this.rtlScrollerInstance_ = this.rtlScrollerFactory_(); + } + + return this.rtlScrollerInstance_; + } + + /** + * Returns the translateX value from a CSS matrix transform function string + * @return {number} + * @private + */ + calculateCurrentTranslateX_() { + const transformValue = this.adapter_.getScrollContentStyleValue('transform'); + // Early exit if no transform is present + if (transformValue === 'none') { + return 0; + } + + // The transform value comes back as a matrix transformation in the form + // of `matrix(a, b, c, d, tx, ty)`. We only care about tx (translateX) so + // we're going to grab all the parenthesized values, strip out tx, and + // parse it. + const results = /\((.+)\)/.exec(transformValue)[1]; + const parts = results.split(','); + return parseFloat(parts[4]); + } + + /** + * Calculates a safe scroll value that is > 0 and < the max scroll value + * @param {number} scrollX The distance to scroll + * @return {number} + * @private + */ + clampScrollValue_(scrollX) { + const edges = this.calculateScrollEdges_(); + return Math.min(Math.max(edges.left, scrollX), edges.right); + } + + /** + * @return {number} + * @private + */ + computeCurrentScrollPositionRTL_() { + const translateX = this.calculateCurrentTranslateX_(); + return this.getRTLScroller().getScrollPositionRTL(translateX); + } + + /** + * @return {!MDCTabScrollerHorizontalEdges} + * @private + */ + calculateScrollEdges_() { + const contentWidth = this.adapter_.getScrollContentOffsetWidth(); + const rootWidth = this.adapter_.getScrollAreaOffsetWidth(); + return /** @type {!MDCTabScrollerHorizontalEdges} */ ({ + left: 0, + right: contentWidth - rootWidth, + }); + } + + /** + * Internal scroll method + * @param {number} scrollX The new scroll position + * @private + */ + scrollTo_(scrollX) { + const currentScrollX = this.getScrollPosition(); + const safeScrollX = this.clampScrollValue_(scrollX); + const scrollDelta = safeScrollX - currentScrollX; + this.animate_(/** @type {!MDCTabScrollerAnimation} */ ({ + finalScrollPosition: safeScrollX, + scrollDelta: scrollDelta, + })); + } + + /** + * Internal RTL scroll method + * @param {number} scrollX The new scroll position + * @private + */ + scrollToRTL_(scrollX) { + const animation = this.getRTLScroller().scrollToRTL(scrollX); + this.animate_(animation); + } + + /** + * Internal increment scroll method + * @param {number} scrollX The new scroll position increment + * @private + */ + incrementScroll_(scrollX) { + const currentScrollX = this.getScrollPosition(); + const targetScrollX = scrollX + currentScrollX; + const safeScrollX = this.clampScrollValue_(targetScrollX); + const scrollDelta = safeScrollX - currentScrollX; + this.animate_(/** @type {!MDCTabScrollerAnimation} */ ({ + finalScrollPosition: safeScrollX, + scrollDelta: scrollDelta, + })); + } + + /** + * Internal incremenet scroll RTL method + * @param {number} scrollX The new scroll position RTL increment + * @private + */ + incrementScrollRTL_(scrollX) { + const animation = this.getRTLScroller().incrementScrollRTL(scrollX); + this.animate_(animation); + } + + /** + * Animates the tab scrolling + * @param {!MDCTabScrollerAnimation} animation The animation to apply + * @private + */ + animate_(animation) { + // Early exit if translateX is 0, which means there's no animation to perform + if (animation.scrollDelta === 0) { + return; + } + + this.stopScrollAnimation_(); + // This animation uses the FLIP approach. + // Read more here: https://aerotwist.com/blog/flip-your-animations/ + this.adapter_.setScrollAreaScrollLeft(animation.finalScrollPosition); + this.adapter_.setScrollContentStyleProperty('transform', `translateX(${animation.scrollDelta}px)`); + // Force repaint + this.adapter_.computeScrollAreaClientRect(); + + requestAnimationFrame(() => { + this.adapter_.addClass(MDCTabScrollerFoundation.cssClasses.ANIMATING); + this.adapter_.setScrollContentStyleProperty('transform', 'none'); + }); + + this.isAnimating_ = true; + } + + /** + * Stops scroll animation + * @private + */ + stopScrollAnimation_() { + this.isAnimating_ = false; + const currentScrollPosition = this.getAnimatingScrollPosition_(); + this.adapter_.removeClass(MDCTabScrollerFoundation.cssClasses.ANIMATING); + this.adapter_.setScrollContentStyleProperty('transform', 'translateX(0px)'); + this.adapter_.setScrollAreaScrollLeft(currentScrollPosition); + } + + /** + * Gets the current scroll position during animation + * @return {number} + * @private + */ + getAnimatingScrollPosition_() { + const currentTranslateX = this.calculateCurrentTranslateX_(); + const scrollLeft = this.adapter_.getScrollAreaScrollLeft(); + if (this.isRTL_()) { + return this.getRTLScroller().getAnimatingScrollPosition(scrollLeft, currentTranslateX); + } + + return scrollLeft - currentTranslateX; + } + + /** + * Determines the RTL Scroller to use + * @return {!MDCTabScrollerRTL} + * @private + */ + rtlScrollerFactory_() { + // Browsers have three different implementations of scrollLeft in RTL mode, + // dependent on the browser. The behavior is based off the max LTR + // scrollleft value and 0. + // + // * Default scrolling in RTL * + // - Left-most value: 0 + // - Right-most value: Max LTR scrollLeft value + // + // * Negative scrolling in RTL * + // - Left-most value: Negated max LTR scrollLeft value + // - Right-most value: 0 + // + // * Reverse scrolling in RTL * + // - Left-most value: Max LTR scrollLeft value + // - Right-most value: 0 + // + // We use those principles below to determine which RTL scrollLeft + // behavior is implemented in the current browser. + const initialScrollLeft = this.adapter_.getScrollAreaScrollLeft(); + this.adapter_.setScrollAreaScrollLeft(initialScrollLeft - 1); + const newScrollLeft = this.adapter_.getScrollAreaScrollLeft(); + + // If the newScrollLeft value is negative,then we know that the browser has + // implemented negative RTL scrolling, since all other implementations have + // only positive values. + if (newScrollLeft < 0) { + // Undo the scrollLeft test check + this.adapter_.setScrollAreaScrollLeft(initialScrollLeft); + return new MDCTabScrollerRTLNegative(this.adapter_); + } + + const rootClientRect = this.adapter_.computeScrollAreaClientRect(); + const contentClientRect = this.adapter_.computeScrollContentClientRect(); + const rightEdgeDelta = Math.round(contentClientRect.right - rootClientRect.right); + // Undo the scrollLeft test check + this.adapter_.setScrollAreaScrollLeft(initialScrollLeft); + + // By calculating the clientRect of the root element and the clientRect of + // the content element, we can determine how much the scroll value changed + // when we performed the scrollLeft subtraction above. + if (rightEdgeDelta === newScrollLeft) { + return new MDCTabScrollerRTLReverse(this.adapter_); + } + + return new MDCTabScrollerRTLDefault(this.adapter_); + } + + /** + * @return {boolean} + * @private + */ + isRTL_() { + return this.adapter_.getScrollContentStyleValue('direction') === 'rtl'; + } +} + +export default MDCTabScrollerFoundation; diff --git a/packages/mdc-tab-scroller/index.js b/packages/mdc-tab-scroller/index.js new file mode 100644 index 00000000000..cfe94a2cd82 --- /dev/null +++ b/packages/mdc-tab-scroller/index.js @@ -0,0 +1,141 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +import MDCComponent from '@material/base/component'; + +import {MDCTabScrollerAdapter} from './adapter'; +import MDCTabScrollerFoundation from './foundation'; +import * as util from './util'; + +/** + * @extends {MDCComponent} + * @final + */ +class MDCTabScroller extends MDCComponent { + /** + * @param {!Element} root + * @return {!MDCTabScroller} + */ + static attachTo(root) { + return new MDCTabScroller(root); + } + + constructor(...args) { + super(...args); + + /** @private {?Element} */ + this.content_; + + /** @private {?Element} */ + this.area_; + + /** @private {?function(?Event): undefined} */ + this.handleInteraction_; + + /** @private {?function(!Event): undefined} */ + this.handleTransitionEnd_; + } + + initialize() { + this.area_ = this.root_.querySelector(MDCTabScrollerFoundation.strings.AREA_SELECTOR); + this.content_ = this.root_.querySelector(MDCTabScrollerFoundation.strings.CONTENT_SELECTOR); + } + + initialSyncWithDOM() { + this.handleInteraction_ = () => this.foundation_.handleInteraction(); + this.handleTransitionEnd_ = (evt) => this.foundation_.handleTransitionEnd(evt); + + this.area_.addEventListener('wheel', this.handleInteraction_); + this.area_.addEventListener('touchstart', this.handleInteraction_); + this.area_.addEventListener('pointerdown', this.handleInteraction_); + this.area_.addEventListener('mousedown', this.handleInteraction_); + this.area_.addEventListener('keydown', this.handleInteraction_); + this.content_.addEventListener('transitionend', this.handleTransitionEnd_); + } + + destroy() { + super.destroy(); + + this.area_.removeEventListener('wheel', this.handleInteraction_); + this.area_.removeEventListener('touchstart', this.handleInteraction_); + this.area_.removeEventListener('pointerdown', this.handleInteraction_); + this.area_.removeEventListener('mousedown', this.handleInteraction_); + this.area_.removeEventListener('keydown', this.handleInteraction_); + this.content_.removeEventListener('transitionend', this.handleTransitionEnd_); + } + + /** + * @return {!MDCTabScrollerFoundation} + */ + getDefaultFoundation() { + const adapter = /** @type {!MDCTabScrollerAdapter} */ ({ + eventTargetMatchesSelector: (evtTarget, selector) => { + const MATCHES = util.getMatchesProperty(HTMLElement.prototype); + return evtTarget[MATCHES](selector); + }, + addClass: (className) => this.root_.classList.add(className), + removeClass: (className) => this.root_.classList.remove(className), + addScrollAreaClass: (className) => this.area_.classList.add(className), + setScrollAreaStyleProperty: (prop, value) => this.area_.style.setProperty(prop, value), + setScrollContentStyleProperty: (prop, value) => this.content_.style.setProperty(prop, value), + getScrollContentStyleValue: (propName) => window.getComputedStyle(this.content_).getPropertyValue(propName), + setScrollAreaScrollLeft: (scrollX) => this.area_.scrollLeft = scrollX, + getScrollAreaScrollLeft: () => this.area_.scrollLeft, + getScrollContentOffsetWidth: () => this.content_.offsetWidth, + getScrollAreaOffsetWidth: () => this.area_.offsetWidth, + computeScrollAreaClientRect: () => this.area_.getBoundingClientRect(), + computeScrollContentClientRect: () => this.content_.getBoundingClientRect(), + computeHorizontalScrollbarHeight: () => util.computeHorizontalScrollbarHeight(document), + }); + + return new MDCTabScrollerFoundation(adapter); + } + + /** + * Returns the current visual scroll position + * @return {number} + */ + getScrollPosition() { + return this.foundation_.getScrollPosition(); + } + + /** + * Returns the width of the scroll content + * @return {number} + */ + getScrollContentWidth() { + return this.content_.offsetWidth; + } + + /** + * Increments the scroll value by the given amount + * @param {number} scrollXIncrement The pixel value by which to increment the scroll value + */ + incrementScroll(scrollXIncrement) { + this.foundation_.incrementScroll(scrollXIncrement); + } + + /** + * Scrolls to the given pixel position + * @param {number} scrollX The pixel value to scroll to + */ + scrollTo(scrollX) { + this.foundation_.scrollTo(scrollX); + } +} + +export {MDCTabScroller, MDCTabScrollerFoundation, util}; diff --git a/packages/mdc-tab-scroller/mdc-tab-scroller.scss b/packages/mdc-tab-scroller/mdc-tab-scroller.scss new file mode 100644 index 00000000000..7565edd1fa4 --- /dev/null +++ b/packages/mdc-tab-scroller/mdc-tab-scroller.scss @@ -0,0 +1,82 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +@import "@material/animation/variables"; +@import "@material/tab/mixins"; + +// postcss-bem-linter: define tab-scroller +.mdc-tab-scroller { + overflow-y: hidden; +} + +// Selector for test element used to feature-detect horizontal scrollbar height +.mdc-tab-scroller__test { + position: absolute; + top: -9999px; + width: 100px; + height: 100px; + overflow-x: scroll; +} + +.mdc-tab-scroller__scroll-area { + -webkit-overflow-scrolling: touch; + display: flex; + overflow-x: hidden; +} + +.mdc-tab-scroller__scroll-area, +.mdc-tab-scroller__test { + &::-webkit-scrollbar { + display: none; + } +} + +// This modifier class will be added in JS after computing the OS scrollbar size in order to hide the scrollbar. +.mdc-tab-scroller__scroll-area--scroll { + overflow-x: scroll; +} + +.mdc-tab-scroller__scroll-content { + @include mdc-tab-parent-positioning; + + display: flex; + flex: 1 0 auto; + transform: none; + will-change: transform; +} + +.mdc-tab-scroller--align-start .mdc-tab-scroller__scroll-content { + justify-content: flex-start; +} + +.mdc-tab-scroller--align-end .mdc-tab-scroller__scroll-content { + justify-content: flex-end; +} + +.mdc-tab-scroller--align-center .mdc-tab-scroller__scroll-content { + justify-content: center; +} + +.mdc-tab-scroller--animating .mdc-tab-scroller__scroll-area { + -webkit-overflow-scrolling: auto; +} + +.mdc-tab-scroller--animating .mdc-tab-scroller__scroll-content { + transition: 250ms transform $mdc-animation-standard-curve-timing-function; +} + +// postcss-bem-linter: end diff --git a/packages/mdc-tab-scroller/package.json b/packages/mdc-tab-scroller/package.json new file mode 100644 index 00000000000..ed6084bd89f --- /dev/null +++ b/packages/mdc-tab-scroller/package.json @@ -0,0 +1,25 @@ +{ + "name": "@material/tab-scroller", + "description": "The Material Components for the web tab scroller component", + "version": "0.0.0", + "license": "Apache-2.0", + "keywords": [ + "material components", + "material design", + "tab", + "scroller" + ], + "main": "index.js", + "repository": { + "type": "git", + "url": "https://github.com/material-components/material-components-web.git" + }, + "dependencies": { + "@material/animation": "^0.34.0", + "@material/base": "^0.35.0", + "@material/tab": "^0.37.1" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/mdc-tab-scroller/rtl-default-scroller.js b/packages/mdc-tab-scroller/rtl-default-scroller.js new file mode 100644 index 00000000000..c32ced03107 --- /dev/null +++ b/packages/mdc-tab-scroller/rtl-default-scroller.js @@ -0,0 +1,98 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +import MDCTabScrollerRTL from './rtl-scroller'; + +/* eslint-disable no-unused-vars */ +import {MDCTabScrollerAnimation, MDCTabScrollerHorizontalEdges} from './adapter'; +/* eslint-enable no-unused-vars */ + +/** + * @extends {MDCTabScrollerRTL} + * @final + */ +class MDCTabScrollerRTLDefault extends MDCTabScrollerRTL { + /** + * @return {number} + */ + getScrollPositionRTL() { + const currentScrollLeft = this.adapter_.getScrollAreaScrollLeft(); + const {right} = this.calculateScrollEdges_(); + // Scroll values on most browsers are ints instead of floats so we round + return Math.round(right - currentScrollLeft); + } + + /** + * @param {number} scrollX + * @return {!MDCTabScrollerAnimation} + */ + scrollToRTL(scrollX) { + const edges = this.calculateScrollEdges_(); + const currentScrollLeft = this.adapter_.getScrollAreaScrollLeft(); + const clampedScrollLeft = this.clampScrollValue_(edges.right - scrollX); + return /** @type {!MDCTabScrollerAnimation} */ ({ + finalScrollPosition: clampedScrollLeft, + scrollDelta: clampedScrollLeft - currentScrollLeft, + }); + } + + /** + * @param {number} scrollX + * @return {!MDCTabScrollerAnimation} + */ + incrementScrollRTL(scrollX) { + const currentScrollLeft = this.adapter_.getScrollAreaScrollLeft(); + const clampedScrollLeft = this.clampScrollValue_(currentScrollLeft - scrollX); + return /** @type {!MDCTabScrollerAnimation} */ ({ + finalScrollPosition: clampedScrollLeft, + scrollDelta: clampedScrollLeft - currentScrollLeft, + }); + } + + /** + * @param {number} scrollX + * @return {number} + */ + getAnimatingScrollPosition(scrollX) { + return scrollX; + } + + /** + * @return {!MDCTabScrollerHorizontalEdges} + * @private + */ + calculateScrollEdges_() { + const contentWidth = this.adapter_.getScrollContentOffsetWidth(); + const rootWidth = this.adapter_.getScrollAreaOffsetWidth(); + return /** @type {!MDCTabScrollerHorizontalEdges} */ ({ + left: 0, + right: contentWidth - rootWidth, + }); + } + + /** + * @param {number} scrollX + * @return {number} + * @private + */ + clampScrollValue_(scrollX) { + const edges = this.calculateScrollEdges_(); + return Math.min(Math.max(edges.left, scrollX), edges.right); + } +} + +export default MDCTabScrollerRTLDefault; diff --git a/packages/mdc-tab-scroller/rtl-negative-scroller.js b/packages/mdc-tab-scroller/rtl-negative-scroller.js new file mode 100644 index 00000000000..77695c16b71 --- /dev/null +++ b/packages/mdc-tab-scroller/rtl-negative-scroller.js @@ -0,0 +1,97 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +import MDCTabScrollerRTL from './rtl-scroller'; + +/* eslint-disable no-unused-vars */ +import {MDCTabScrollerAnimation, MDCTabScrollerHorizontalEdges} from './adapter'; +/* eslint-enable no-unused-vars */ + +/** + * @extends {MDCTabScrollerRTL} + * @final + */ +class MDCTabScrollerRTLNegative extends MDCTabScrollerRTL { + /** + * @param {number} translateX The current translateX position + * @return {number} + */ + getScrollPositionRTL(translateX) { + const currentScrollLeft = this.adapter_.getScrollAreaScrollLeft(); + return Math.round(translateX - currentScrollLeft); + } + + /** + * @param {number} scrollX + * @return {!MDCTabScrollerAnimation} + */ + scrollToRTL(scrollX) { + const currentScrollLeft = this.adapter_.getScrollAreaScrollLeft(); + const clampedScrollLeft = this.clampScrollValue_(-scrollX); + return /** @type {!MDCTabScrollerAnimation} */ ({ + finalScrollPosition: clampedScrollLeft, + scrollDelta: clampedScrollLeft - currentScrollLeft, + }); + } + + /** + * @param {number} scrollX + * @return {!MDCTabScrollerAnimation} + */ + incrementScrollRTL(scrollX) { + const currentScrollLeft = this.adapter_.getScrollAreaScrollLeft(); + const clampedScrollLeft = this.clampScrollValue_(currentScrollLeft - scrollX); + return /** @type {!MDCTabScrollerAnimation} */ ({ + finalScrollPosition: clampedScrollLeft, + scrollDelta: clampedScrollLeft - currentScrollLeft, + }); + } + + /** + * @param {number} scrollX + * @param {number} translateX + * @return {number} + */ + getAnimatingScrollPosition(scrollX, translateX) { + return scrollX - translateX; + } + + /** + * @return {!MDCTabScrollerHorizontalEdges} + * @private + */ + calculateScrollEdges_() { + const contentWidth = this.adapter_.getScrollContentOffsetWidth(); + const rootWidth = this.adapter_.getScrollAreaOffsetWidth(); + return /** @type {!MDCTabScrollerHorizontalEdges} */ ({ + left: rootWidth - contentWidth, + right: 0, + }); + } + + /** + * @param {number} scrollX + * @return {number} + * @private + */ + clampScrollValue_(scrollX) { + const edges = this.calculateScrollEdges_(); + return Math.max(Math.min(edges.right, scrollX), edges.left); + } +} + +export default MDCTabScrollerRTLNegative; diff --git a/packages/mdc-tab-scroller/rtl-reverse-scroller.js b/packages/mdc-tab-scroller/rtl-reverse-scroller.js new file mode 100644 index 00000000000..a6b45e26a36 --- /dev/null +++ b/packages/mdc-tab-scroller/rtl-reverse-scroller.js @@ -0,0 +1,97 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +import MDCTabScrollerRTL from './rtl-scroller'; + +/* eslint-disable no-unused-vars */ +import {MDCTabScrollerAnimation, MDCTabScrollerHorizontalEdges} from './adapter'; +/* eslint-enable no-unused-vars */ + +/** + * @extends {MDCTabScrollerRTL} + * @final + */ +class MDCTabScrollerRTLReverse extends MDCTabScrollerRTL { + /** + * @param {number} translateX + * @return {number} + */ + getScrollPositionRTL(translateX) { + const currentScrollLeft = this.adapter_.getScrollAreaScrollLeft(); + // Scroll values on most browsers are ints instead of floats so we round + return Math.round(currentScrollLeft - translateX); + } + + /** + * @param {number} scrollX + * @return {!MDCTabScrollerAnimation} + */ + scrollToRTL(scrollX) { + const currentScrollLeft = this.adapter_.getScrollAreaScrollLeft(); + const clampedScrollLeft = this.clampScrollValue_(scrollX); + return /** @type {!MDCTabScrollerAnimation} */ ({ + finalScrollPosition: clampedScrollLeft, + scrollDelta: currentScrollLeft - clampedScrollLeft, + }); + } + + /** + * @param {number} scrollX + * @return {!MDCTabScrollerAnimation} + */ + incrementScrollRTL(scrollX) { + const currentScrollLeft = this.adapter_.getScrollAreaScrollLeft(); + const clampedScrollLeft = this.clampScrollValue_(currentScrollLeft + scrollX); + return /** @type {!MDCTabScrollerAnimation} */ ({ + finalScrollPosition: clampedScrollLeft, + scrollDelta: currentScrollLeft - clampedScrollLeft, + }); + } + + /** + * @param {number} scrollX + * @return {number} + */ + getAnimatingScrollPosition(scrollX, translateX) { + return scrollX + translateX; + } + + /** + * @return {!MDCTabScrollerHorizontalEdges} + * @private + */ + calculateScrollEdges_() { + const contentWidth = this.adapter_.getScrollContentOffsetWidth(); + const rootWidth = this.adapter_.getScrollAreaOffsetWidth(); + return /** @type {!MDCTabScrollerHorizontalEdges} */ ({ + left: contentWidth - rootWidth, + right: 0, + }); + } + + /** + * @param {number} scrollX + * @return {number} + * @private + */ + clampScrollValue_(scrollX) { + const edges = this.calculateScrollEdges_(); + return Math.min(Math.max(edges.right, scrollX), edges.left); + } +} + +export default MDCTabScrollerRTLReverse; diff --git a/packages/mdc-tab-scroller/rtl-scroller.js b/packages/mdc-tab-scroller/rtl-scroller.js new file mode 100644 index 00000000000..73454c4dbfa --- /dev/null +++ b/packages/mdc-tab-scroller/rtl-scroller.js @@ -0,0 +1,64 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +/* eslint no-unused-vars: [2, {"args": "none"}] */ + +/* eslint-disable no-unused-vars */ +import {MDCTabScrollerAdapter, MDCTabScrollerAnimation} from './adapter'; +/* eslint-enable no-unused-vars */ + +/** + * @abstract + */ +class MDCTabScrollerRTL { + /** @param {!MDCTabScrollerAdapter} adapter */ + constructor(adapter) { + /** @private */ + this.adapter_ = adapter; + } + + /** + * @param {number} translateX The current translateX position + * @return {number} + * @abstract + */ + getScrollPositionRTL(translateX) {} + + /** + * @param {number} scrollX + * @return {!MDCTabScrollerAnimation} + * @abstract + */ + scrollToRTL(scrollX) {} + + /** + * @param {number} scrollX + * @return {!MDCTabScrollerAnimation} + * @abstract + */ + incrementScrollRTL(scrollX) {} + + /** + * @param {number} scrollX The current scrollX position + * @param {number} translateX The current translateX position + * @return {number} + * @abstract + */ + getAnimatingScrollPosition(scrollX, translateX) {} +} + +export default MDCTabScrollerRTL; diff --git a/packages/mdc-tab-scroller/util.js b/packages/mdc-tab-scroller/util.js new file mode 100644 index 00000000000..81b507f222f --- /dev/null +++ b/packages/mdc-tab-scroller/util.js @@ -0,0 +1,61 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {cssClasses} from './constants'; + +/** + * Stores result from computeHorizontalScrollbarHeight to avoid redundant processing. + * @private {number|undefined} + */ +let horizontalScrollbarHeight_; + +/** + * Computes the height of browser-rendered horizontal scrollbars using a self-created test element. + * May return 0 (e.g. on OS X browsers under default configuration). + * @param {!Document} documentObj + * @param {boolean=} shouldCacheResult + * @return {number} + */ +function computeHorizontalScrollbarHeight(documentObj, shouldCacheResult = true) { + if (shouldCacheResult && typeof horizontalScrollbarHeight_ !== 'undefined') { + return horizontalScrollbarHeight_; + } + + const el = documentObj.createElement('div'); + el.classList.add(cssClasses.SCROLL_TEST); + documentObj.body.appendChild(el); + + const horizontalScrollbarHeight = el.offsetHeight - el.clientHeight; + documentObj.body.removeChild(el); + + if (shouldCacheResult) { + horizontalScrollbarHeight_ = horizontalScrollbarHeight; + } + return horizontalScrollbarHeight; +} + +/** + * @param {!Object} HTMLElementPrototype + * @return {!Array} + */ +function getMatchesProperty(HTMLElementPrototype) { + return [ + 'msMatchesSelector', 'matches', + ].filter((p) => p in HTMLElementPrototype).pop(); +} + +export {computeHorizontalScrollbarHeight, getMatchesProperty}; diff --git a/packages/mdc-tab/README.md b/packages/mdc-tab/README.md index bbca3d0d54f..b24e77ce29b 100644 --- a/packages/mdc-tab/README.md +++ b/packages/mdc-tab/README.md @@ -1,13 +1,16 @@ -# Tab + - +# Tab -Tab is a selectable element with an active state +Tabs organize and allow navigation between groups of content that are related and at the same level of hierarchy. +Each Tab governs the visibility of one group of content. ## Design & API Documentation @@ -21,32 +24,77 @@ Tab is a selectable element with an active state ## Installation + ``` -npm install --save @material/tab +npm install @material/tab ``` -## Usage +## Basic Usage ### HTML Structure ```html -
+
+ + + +
+ +``` + +### Styles + +```scss +@import "@material/tab/mdc-tab"; +``` + +### JavaScript Instantiation + +```js +import {MDCTab} from '@material/tab'; + +const tab = new MDCTab(document.querySelector('.mdc-tab')); +``` + +> See [Importing the JS component](../../docs/importing-js.md) for more information on how to import JavaScript. + +## Variants + +### Active Tab + +> *NOTE*: Don't forget to add the `mdc-tab-indicator--active` class to the `mdc-tab-indicator` subcomponent. + +```html +
+ + + +
``` +## Style Customization + ### CSS Classes CSS Class | Description --- | --- `mdc-tab` | Mandatory. +`mdc-tab__content` | Mandatory. Indicates the text label of the tab. +`mdc-tab__ripple` | Mandatory. Denotes the ripple surface for the tab. `mdc-tab--active` | Optional. Indicates that the tab is active. -`mdc-tab__content` | Mandatory. Indicates the text label of the tab -`mdc-tab__text-label` | Optional. Indicates an icon in the tab -`mdc-tab__icon` | Optional. Indicates a leading icon in the tab +`mdc-tab--stacked` | Optional. Indicates that the tab icon and label should flow vertically instead of horizontally. +`mdc-tab--min-width` | Optional. Indicates that the tab should shrink in size to be as narrow as possible without causing text to wrap. +`mdc-tab__text-label` | Optional. Indicates an icon in the tab. +`mdc-tab__icon` | Optional. Indicates a leading icon in the tab. ### Sass Mixins @@ -54,32 +102,60 @@ To customize the colors of any part of the tab, use the following mixins. Mixin | Description --- | --- -`mdc-tab-text-label-color($color)` | Customizes the color of the tab text label -`mdc-tab-icon-color($color)` | Customizes the color of the tab icon +`mdc-tab-text-label-color($color)` | Customizes the color of the tab text label. +`mdc-tab-icon-color($color)` | Customizes the color of the tab icon. +`mdc-tab-parent-positioning` | Sets the positioning of the MDCTab's parent element so that `MDCTab.computeDimensions()` reports the same values in all browsers. +`mdc-tab-fixed-width($width)` | Sets the fixed width of the tab. The tab will never be smaller than the given width. -### `MDCTab` +## `MDCTab` Properties and Methods Property | Value Type | Description --- | --- | --- -`active` | `boolean` | Allows getting/setting the active state of the tab -`ripple` | `MDCRipple` | The `MDCRipple` instance for the root element that `MDCChip` initializes +`active` | `boolean` | Allows getting the active state of the tab. + +Method Signature | Description +--- | --- +`activate(previousIndicatorClientRect: ClientRect=) => void` | Activates the indicator. `previousIndicatorClientRect` is an optional argument. +`deactivate() => void` | Deactivates the indicator. +`computeIndicatorClientRect() => ClientRect` | Returns the bounding client rect of the indicator. +`computeDimensions() => MDCTabDimensions` | Returns the dimensions of the Tab. + +Event Name | Event Data Structure | Description +--- | --- | --- +`MDCTab:interacted` | `{"detail": {"tab": MDCTab}}` | Emitted when the Tab is interacted with, regardless of its active state. Used by parent components to know which Tab to activate. + +## Usage within Web Frameworks + +If you are using a JavaScript framework, such as React or Angular, you can create a Tab for your framework. Depending on your needs, you can use the _Simple Approach: Wrapping MDC Web Vanilla Components_, or the _Advanced Approach: Using Foundations and Adapters_. Please follow the instructions [here](../../docs/integrating-into-frameworks.md). ### `MDCTabAdapter` Method Signature | Description --- | --- -`addClass(className: string) => void` | Adds a class to the root element -`removeClass(className: string) => void` | Removes a class from the root element -`hasClass(className: string) => boolean` | Returns true if the root element contains the given class -`registerEventHandler(evtType: string, handler: EventListener) => void` | Registers an event listener on the root element -`deregisterEventHandler(evtType: string, handler: EventListener) => void` | Deregisters an event listener on the root element -`setAttr(attr: string, value: string) => void` | Sets the given attribute on the root element to the given value +`addClass(className: string) => void` | Adds a class to the root element. +`removeClass(className: string) => void` | Removes a class from the root element. +`hasClass(className: string) => boolean` | Returns true if the root element contains the given class. +`registerEventHandler(evtType: string, handler: EventListener) => void` | Registers an event listener on the root element. +`deregisterEventHandler(evtType: string, handler: EventListener) => void` | Deregisters an event listener on the root element. +`setAttr(attr: string, value: string) => void` | Sets the given attribute on the root element to the given value. +`activateIndicator(previousIndicatorClientRect: ClientRect=) => void` | Activates the tab indicator subcomponent. `previousIndicatorClientRect` is an optional argument. +`deactivateIndicator() => void` | Deactivates the tab indicator subcomponent. +`computeIndicatorClientRect() => ClientRect` | Returns the tab indicator subcomponent's content bounding client rect. +`getOffsetLeft() => number` | Returns the `offsetLeft` value of the root element. +`getOffsetWidth() => number` | Returns the `offsetWidth` value of the root element. +`getContentOffsetLeft() => number` | Returns the `offsetLeft` value of the content element. +`getContentOffsetWidth() => number` | Returns the `offsetWidth` value of the content element. +`notifyInteracted() => void` | Emits the `MDCTab:interacted` event. +`focus() => void` | Applies focus to the root element. ### `MDCTabFoundation` Method Signature | Description --- | --- -`handleTransitionEnd(evt: Event) => void` | Handles the logic for the `"transitionend"` event -`isActive() => boolean` | Returns whether the tab is active -`activate() => void` | Activates the tab -`deactivate() => void` | Deactivates the tab +`handleTransitionEnd(evt: Event) => void` | Handles the logic for the `"transitionend"` event. +`handleClick() => void` | Handles the logic for the `"click"` event. +`isActive() => boolean` | Returns whether the tab is active. +`activate(previousIndicatorClientRect: ClientRect=) => void` | Activates the tab. `previousIndicatorClientRect` is an optional argument. +`deactivate() => void` | Deactivates the tab. +`computeIndicatorClientRect() => ClientRect` | Returns the tab indicator subcomponent's content bounding client rect. +`computeDimensions() => MDCTabDimensions` | Returns the dimensions of the tab. diff --git a/packages/mdc-tab/_mixins.scss b/packages/mdc-tab/_mixins.scss index b68278ca66a..40691116e8b 100644 --- a/packages/mdc-tab/_mixins.scss +++ b/packages/mdc-tab/_mixins.scss @@ -29,3 +29,11 @@ @include mdc-theme-prop(color, $color); } } + +@mixin mdc-tab-parent-positioning { + position: relative; +} + +@mixin mdc-tab-fixed-width($width) { + flex: 0 1 $width; +} diff --git a/packages/mdc-tab/_variables.scss b/packages/mdc-tab/_variables.scss index 8a377c8cc45..9463b129dbe 100644 --- a/packages/mdc-tab/_variables.scss +++ b/packages/mdc-tab/_variables.scss @@ -14,4 +14,8 @@ // limitations under the License. // -$mdc-tab-baseline-color: rgba(25, 25, 25, .6); +$mdc-tab-icon-size: 24px; +$mdc-tab-height: 48px; +$mdc-tab-stacked-height: 72px; +$mdc-tab-text-label-opacity: .6; +$mdc-tab-icon-opacity: .54; diff --git a/packages/mdc-tab/adapter.js b/packages/mdc-tab/adapter.js index 8b88ffd1826..3ee5b6caa6f 100644 --- a/packages/mdc-tab/adapter.js +++ b/packages/mdc-tab/adapter.js @@ -17,6 +17,14 @@ /* eslint no-unused-vars: [2, {"args": "none"}] */ +/** + * MDCTabDimensions provides details about the left and right edges of the Tab + * root element and the Tab content element. These values are used to determine + * the visual position of the Tab with respect it's parent container. + * @typedef {{rootLeft: number, rootRight: number, contentLeft: number, contentRight: number}} + */ +let MDCTabDimensions; + /** * Adapter for MDC Tab. * @@ -67,6 +75,55 @@ class MDCTabAdapter { * @param {string} value The value so give the attribute */ setAttr(attr, value) {} + + /** + * Activates the indicator element. + * @param {!ClientRect=} previousIndicatorClientRect The client rect of the previously activated indicator + */ + activateIndicator(previousIndicatorClientRect) {} + + /** Deactivates the indicator. */ + deactivateIndicator() {} + + /** + * Returns the client rect of the indicator. + * @return {!ClientRect} + */ + computeIndicatorClientRect() {} + + /** + * Emits the MDCTab:interacted event for use by parent components + */ + notifyInteracted() {} + + /** + * Returns the offsetLeft value of the root element. + * @return {number} + */ + getOffsetLeft() {} + + /** + * Returns the offsetWidth value of the root element. + * @return {number} + */ + getOffsetWidth() {} + + /** + * Returns the offsetLeft of the content element. + * @return {number} + */ + getContentOffsetLeft() {} + + /** + * Returns the offsetWidth of the content element. + * @return {number} + */ + getContentOffsetWidth() {} + + /** + * Applies focus to the root element + */ + focus() {} } -export default MDCTabAdapter; +export {MDCTabDimensions, MDCTabAdapter}; diff --git a/packages/mdc-tab/constants.js b/packages/mdc-tab/constants.js index 9eefed94d17..ebfaf14a204 100644 --- a/packages/mdc-tab/constants.js +++ b/packages/mdc-tab/constants.js @@ -25,6 +25,11 @@ const cssClasses = { /** @enum {string} */ const strings = { ARIA_SELECTED: 'aria-selected', + RIPPLE_SELECTOR: '.mdc-tab__ripple', + CONTENT_SELECTOR: '.mdc-tab__content', + TAB_INDICATOR_SELECTOR: '.mdc-tab-indicator', + TABINDEX: 'tabIndex', + INTERACTED_EVENT: 'MDCTab:interacted', }; export { diff --git a/packages/mdc-tab/foundation.js b/packages/mdc-tab/foundation.js index 9dea0ce7a81..8fd7829535a 100644 --- a/packages/mdc-tab/foundation.js +++ b/packages/mdc-tab/foundation.js @@ -16,7 +16,11 @@ */ import MDCFoundation from '@material/base/foundation'; -import MDCTabAdapter from './adapter'; + +/* eslint-disable no-unused-vars */ +import {MDCTabAdapter, MDCTabDimensions} from './adapter'; +/* eslint-enable no-unused-vars */ + import { cssClasses, strings, @@ -49,6 +53,15 @@ class MDCTabFoundation extends MDCFoundation { removeClass: () => {}, hasClass: () => {}, setAttr: () => {}, + activateIndicator: () => {}, + deactivateIndicator: () => {}, + computeIndicatorClientRect: () => {}, + notifyInteracted: () => {}, + getOffsetLeft: () => {}, + getOffsetWidth: () => {}, + getContentOffsetLeft: () => {}, + getContentOffsetWidth: () => {}, + focus: () => {}, }); } @@ -58,6 +71,13 @@ class MDCTabFoundation extends MDCFoundation { /** @private {function(!Event): undefined} */ this.handleTransitionEnd_ = (evt) => this.handleTransitionEnd(evt); + + /** @private {function(?Event): undefined} */ + this.handleClick_ = () => this.handleClick(); + } + + init() { + this.adapter_.registerEventHandler('click', this.handleClick_); } /** @@ -74,6 +94,15 @@ class MDCTabFoundation extends MDCFoundation { this.adapter_.removeClass(cssClasses.ANIMATING_DEACTIVATE); } + /** + * Handles the "click" event + */ + handleClick() { + // It's up to the parent component to keep track of the active Tab and + // ensure we don't activate a Tab that's already active. + this.adapter_.notifyInteracted(); + } + /** * Returns the Tab's active state * @return {boolean} @@ -84,16 +113,21 @@ class MDCTabFoundation extends MDCFoundation { /** * Activates the Tab + * @param {!ClientRect=} previousIndicatorClientRect */ - activate() { + activate(previousIndicatorClientRect) { // Early exit if (this.isActive()) { return; } + this.adapter_.registerEventHandler('transitionend', this.handleTransitionEnd_); this.adapter_.addClass(cssClasses.ANIMATING_ACTIVATE); this.adapter_.addClass(cssClasses.ACTIVE); this.adapter_.setAttr(strings.ARIA_SELECTED, 'true'); + this.adapter_.setAttr(strings.TABINDEX, '0'); + this.adapter_.activateIndicator(previousIndicatorClientRect); + this.adapter_.focus(); } /** @@ -104,10 +138,39 @@ class MDCTabFoundation extends MDCFoundation { if (!this.isActive()) { return; } + this.adapter_.registerEventHandler('transitionend', this.handleTransitionEnd_); this.adapter_.addClass(cssClasses.ANIMATING_DEACTIVATE); this.adapter_.removeClass(cssClasses.ACTIVE); this.adapter_.setAttr(strings.ARIA_SELECTED, 'false'); + this.adapter_.setAttr(strings.TABINDEX, '-1'); + this.adapter_.deactivateIndicator(); + } + + /** + * Returns the indicator's client rect + * @return {!ClientRect} + */ + computeIndicatorClientRect() { + return this.adapter_.computeIndicatorClientRect(); + } + + /** + * Returns the dimensions of the Tab + * @return {!MDCTabDimensions} + */ + computeDimensions() { + const rootWidth = this.adapter_.getOffsetWidth(); + const rootLeft = this.adapter_.getOffsetLeft(); + const contentWidth = this.adapter_.getContentOffsetWidth(); + const contentLeft = this.adapter_.getContentOffsetLeft(); + + return { + rootLeft, + rootRight: rootLeft + rootWidth, + contentLeft: rootLeft + contentLeft, + contentRight: rootLeft + contentLeft + contentWidth, + }; } } diff --git a/packages/mdc-tab/index.js b/packages/mdc-tab/index.js index 342de633a0e..cf4dacb4dff 100644 --- a/packages/mdc-tab/index.js +++ b/packages/mdc-tab/index.js @@ -16,9 +16,13 @@ */ import MDCComponent from '@material/base/component'; -import {MDCRipple} from '@material/ripple/index'; -import MDCTabAdapter from './adapter'; +/* eslint-disable no-unused-vars */ +import {MDCRipple, MDCRippleFoundation, RippleCapableSurface} from '@material/ripple/index'; +import {MDCTabIndicator, MDCTabIndicatorFoundation} from '@material/tab-indicator/index'; +import {MDCTabAdapter, MDCTabDimensions} from './adapter'; +/* eslint-enable no-unused-vars */ + import MDCTabFoundation from './foundation'; /** @@ -31,9 +35,12 @@ class MDCTab extends MDCComponent { */ constructor(...args) { super(...args); - - /** @private {!MDCRipple} */ - this.ripple_ = new MDCRipple(this.root_); + /** @private {?MDCRipple} */ + this.ripple_; + /** @private {?MDCTabIndicator} */ + this.tabIndicator_; + /** @private {?Element} */ + this.content_; } /** @@ -44,6 +51,24 @@ class MDCTab extends MDCComponent { return new MDCTab(root); } + initialize( + rippleFactory = (el, foundation) => new MDCRipple(el, foundation), + tabIndicatorFactory = (el) => new MDCTabIndicator(el)) { + const rippleSurface = this.root_.querySelector(MDCTabFoundation.strings.RIPPLE_SELECTOR); + const rippleAdapter = Object.assign(MDCRipple.createAdapter(/** @type {!RippleCapableSurface} */ (this)), { + addClass: (className) => rippleSurface.classList.add(className), + removeClass: (className) => rippleSurface.classList.remove(className), + updateCssVariable: (varName, value) => rippleSurface.style.setProperty(varName, value), + }); + const rippleFoundation = new MDCRippleFoundation(rippleAdapter); + this.ripple_ = rippleFactory(this.root_, rippleFoundation); + + const tabIndicatorElement = this.root_.querySelector(MDCTabFoundation.strings.TAB_INDICATOR_SELECTOR); + this.tabIndicator_ = tabIndicatorFactory(tabIndicatorElement); + + this.content_ = this.root_.querySelector(MDCTabFoundation.strings.CONTENT_SELECTOR); + } + destroy() { this.ripple_.destroy(); super.destroy(); @@ -53,17 +78,28 @@ class MDCTab extends MDCComponent { * @return {!MDCTabFoundation} */ getDefaultFoundation() { - return new MDCTabFoundation(/** @type {!MDCTabAdapter} */ (Object.assign({ - setAttr: (attr, value) => this.root_.setAttribute(attr, value), - registerEventHandler: (evtType, handler) => this.root_.addEventListener(evtType, handler), - deregisterEventHandler: (evtType, handler) => this.root_.removeEventListener(evtType, handler), - addClass: (className) => this.root_.classList.add(className), - removeClass: (className) => this.root_.classList.remove(className), - hasClass: (className) => this.root_.classList.contains(className), - }))); + return new MDCTabFoundation( + /** @type {!MDCTabAdapter} */ ({ + setAttr: (attr, value) => this.root_.setAttribute(attr, value), + registerEventHandler: (evtType, handler) => this.root_.addEventListener(evtType, handler), + deregisterEventHandler: (evtType, handler) => this.root_.removeEventListener(evtType, handler), + addClass: (className) => this.root_.classList.add(className), + removeClass: (className) => this.root_.classList.remove(className), + hasClass: (className) => this.root_.classList.contains(className), + activateIndicator: (previousIndicatorClientRect) => this.tabIndicator_.activate(previousIndicatorClientRect), + deactivateIndicator: () => this.tabIndicator_.deactivate(), + computeIndicatorClientRect: () => this.tabIndicator_.computeContentClientRect(), + notifyInteracted: () => this.emit(MDCTabFoundation.strings.INTERACTED_EVENT, {tab: this}, true /* bubble */), + getOffsetLeft: () => this.root_.offsetLeft, + getOffsetWidth: () => this.root_.offsetWidth, + getContentOffsetLeft: () => this.content_.offsetLeft, + getContentOffsetWidth: () => this.content_.offsetWidth, + focus: () => this.root_.focus(), + })); } /** + * Getter for the active state of the tab * @return {boolean} */ get active() { @@ -71,14 +107,33 @@ class MDCTab extends MDCComponent { } /** - * @param {boolean} isActive + * Activates the tab + * @param {!ClientRect=} computeIndicatorClientRect + */ + activate(computeIndicatorClientRect) { + this.foundation_.activate(computeIndicatorClientRect); + } + + /** + * Deactivates the tab + */ + deactivate() { + this.foundation_.deactivate(); + } + + /** + * Returns the indicator's client rect + * @return {!ClientRect} + */ + computeIndicatorClientRect() { + return this.foundation_.computeIndicatorClientRect(); + } + + /** + * @return {!MDCTabDimensions} */ - set active(isActive) { - if (isActive) { - this.foundation_.activate(); - } else { - this.foundation_.deactivate(); - } + computeDimensions() { + return this.foundation_.computeDimensions(); } } diff --git a/packages/mdc-tab/mdc-tab.scss b/packages/mdc-tab/mdc-tab.scss index 80533f43ba1..73272e39547 100644 --- a/packages/mdc-tab/mdc-tab.scss +++ b/packages/mdc-tab/mdc-tab.scss @@ -18,22 +18,21 @@ @import "@material/ripple/mixins"; @import "@material/rtl/mixins"; @import "@material/typography/mixins"; +@import "@material/tab-indicator/mixins"; @import "./mixins"; @import "./variables"; .mdc-tab { - @include mdc-ripple-surface; - @include mdc-ripple-radius-bounded; - @include mdc-states(primary); - @include mdc-tab-text-label-color($mdc-tab-baseline-color); - @include mdc-tab-icon-color($mdc-tab-baseline-color); + @include mdc-tab-text-label-color(on-surface); + @include mdc-tab-icon-color(on-surface); + @include mdc-tab-indicator-surface; + @include mdc-typography(button); display: flex; - position: relative; flex: 1 0 auto; justify-content: center; box-sizing: border-box; - height: 48px; + height: $mdc-tab-height; padding: 0 24px; border: none; outline: none; @@ -44,35 +43,73 @@ white-space: nowrap; cursor: pointer; -webkit-appearance: none; - overflow: hidden; z-index: 1; } +.mdc-tab--min-width { + flex: 0 1 auto; +} + +.mdc-tab__ripple { + @include mdc-ripple-surface; + @include mdc-ripple-radius-bounded; + @include mdc-states(primary); + + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; +} + .mdc-tab__content { + @include mdc-tab-indicator-surface; + display: flex; - position: relative; + align-items: center; justify-content: center; height: inherit; - z-index: 1; + pointer-events: none; } -.mdc-tab__text-label { - @include mdc-typography-base; +.mdc-tab__text-label, +.mdc-tab__icon { + z-index: 2; +} +.mdc-tab__text-label { display: inline-block; - padding: 16px 0 20px; - font-size: 14px; - font-weight: 500; + opacity: $mdc-tab-text-label-opacity; + // Setting line-height here overrides the line-height from the typography + // mixin above. The line-height needs to be overridden so that the spacing + // between the text label and the icon as well as the text label and the + // bottom of the tab remain the same. + line-height: 1; } .mdc-tab__icon { - width: 24px; - height: 24px; - padding: 12px 0; + width: $mdc-tab-icon-size; + height: $mdc-tab-icon-size; + opacity: $mdc-tab-icon-opacity; } -.mdc-tab__icon + .mdc-tab__text-label { - @include mdc-rtl-reflexive-box(padding, left, 8px); +.mdc-tab--stacked { + height: $mdc-tab-stacked-height; +} + +.mdc-tab--stacked .mdc-tab__content { + flex-direction: column; + align-items: center; + justify-content: space-between; +} + +.mdc-tab--stacked .mdc-tab__icon { + padding-top: 12px; +} + +.mdc-tab--stacked .mdc-tab__text-label { + padding-bottom: 16px; } // The [de]activation animation affects color for text label and icon @@ -80,7 +117,7 @@ .mdc-tab--animating-activate .mdc-tab__icon, .mdc-tab--animating-deactivate .mdc-tab__text-label, .mdc-tab--animating-deactivate .mdc-tab__icon { - transition: 150ms color linear; + transition: 150ms color linear, 150ms opacity linear; } // The activation animation has a delay of 100ms @@ -92,4 +129,13 @@ .mdc-tab--active { @include mdc-tab-text-label-color(primary); @include mdc-tab-icon-color(primary); + + .mdc-tab__text-label, + .mdc-tab__icon { + opacity: 1; + } +} + +.mdc-tab:not(.mdc-tab--stacked) .mdc-tab__icon + .mdc-tab__text-label { + @include mdc-rtl-reflexive-box(padding, left, 8px); } diff --git a/packages/mdc-tab/package-lock.json b/packages/mdc-tab/package-lock.json deleted file mode 100644 index c99f45b885e..00000000000 --- a/packages/mdc-tab/package-lock.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "@material/tab", - "version": "0.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@material/rtl": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-0.30.0.tgz", - "integrity": "sha512-zxEiXFESaQvNNm+tHO0QVKxkGERkL6rX4ghusl7HhosueOcjaVLNYCxH5F0x6Et04TL0wh1ycBmFUIEqVhEkWw==" - }, - "@material/theme": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/@material/theme/-/theme-0.30.0.tgz", - "integrity": "sha512-jd1iC5qyq5YX4w8sZj9+lXExeK4adXbAjw/lOQuDeT6WSGJf7HovxQlHjlyWJ4p/x0xTGrlaDHnrCxeJQCq4Og==" - } - } -} diff --git a/packages/mdc-tab/package.json b/packages/mdc-tab/package.json index 1caf3f5594d..99e8c219bc9 100644 --- a/packages/mdc-tab/package.json +++ b/packages/mdc-tab/package.json @@ -3,7 +3,6 @@ "description": "The Material Components for the web tab component", "version": "0.37.1", "license": "Apache-2.0", - "private": true, "keywords": [ "material components", "material design", @@ -17,9 +16,10 @@ "dependencies": { "@material/base": "^0.35.0", "@material/ripple": "^0.37.1", - "@material/rtl": "^0.30.0", - "@material/theme": "^0.30.0", - "@material/typography": "^0.37.1" + "@material/rtl": "^0.36.0", + "@material/theme": "^0.35.0", + "@material/typography": "^0.37.1", + "@material/tab-indicator": "^0.0.0" }, "publishConfig": { "access": "public" diff --git a/packages/mdc-tabs/README.md b/packages/mdc-tabs/README.md index b1247e545c9..fc238de10dd 100644 --- a/packages/mdc-tabs/README.md +++ b/packages/mdc-tabs/README.md @@ -1,12 +1,20 @@ +## Important - Deprecation Notice + +The `mdc-tabs` package is deprecated and no longer maintained, and is no longer included in the all-in-one +`material-components-web` package. Improved functionality is available across the `mdc-tab-bar`, `mdc-tab-scroller`, +`mdc-tab-indicator`, and `mdc-tab` packages, which are now included in the `material-components-web` package. +Bugs and feature requests will no longer be accepted for this package. It is recommended that you migrate to the new +packages to continue to receive new features and updates. + # MDC Tabs The MDC Tabs component contains components which are used to create spec-aligned tabbed navigation components adhering to the diff --git a/scripts/check-pkg-for-release.js b/scripts/check-pkg-for-release.js index 139a9c6be60..be7241d712b 100644 --- a/scripts/check-pkg-for-release.js +++ b/scripts/check-pkg-for-release.js @@ -45,6 +45,21 @@ const MASTER_PKG = require(path.join(process.env.PWD, MASTER_PKG_PATH)); // are necessary since other MDC packages depend on them. const CSS_WHITELIST = ['base', 'animation', 'auto-init', 'rtl', 'selection-control']; +// List of packages that are intentionally not included in the MCW package's dependencies +const NOT_MCW_DEP = [ + 'tabs', // Deprecated; CSS classes conflict with tab and tab-bar +]; + +const NOT_AUTOINIT = [ + 'auto-init', + 'base', + 'selection-control', + 'tab', // Only makes sense in context of tab-bar + 'tab-indicator', // Only makes sense in context of tab-bar + 'tab-scroller', // Only makes sense in context of tab-bar + 'tabs', // Deprecated +]; + main(); function main() { @@ -127,15 +142,17 @@ function checkDependencyAddedInMDCPackage() { } function checkPkgDependencyAddedInMDCPackage() { - assert.notEqual(typeof MASTER_PKG.dependencies[pkg.name], 'undefined', - 'FAILURE: Component ' + pkg.name + ' is not a denpendency for MDC Web. ' + - 'Please add ' + pkg.name +' to ' + MASTER_PKG_PATH + '\' dependencies before commit.'); + if (NOT_MCW_DEP.indexOf(getPkgName()) === -1) { + assert.notEqual(typeof MASTER_PKG.dependencies[pkg.name], 'undefined', + 'FAILURE: Component ' + pkg.name + ' is not a denpendency for MDC Web. ' + + 'Please add ' + pkg.name +' to ' + MASTER_PKG_PATH + '\' dependencies before commit.'); + } } function checkCSSDependencyAddedInMDCPackage() { const name = getPkgName(); const nameMDC = `mdc-${name}`; - if (CSS_WHITELIST.indexOf(name) === -1) { + if (CSS_WHITELIST.indexOf(name) === -1 && NOT_MCW_DEP.indexOf(name) === -1) { const src = fs.readFileSync(path.join(process.env.PWD, MASTER_CSS_PATH), 'utf8'); const cssRules = cssom.parse(src).cssRules; const cssRule = path.join(pkg.name, nameMDC); @@ -150,9 +167,8 @@ function checkCSSDependencyAddedInMDCPackage() { function checkJSDependencyAddedInMDCPackage() { const NOT_IMPORTED = ['animation']; - const NOT_AUTOINIT = ['auto-init', 'base', 'selection-control']; const name = getPkgName(); - if (typeof(pkg.main) !== 'undefined' && NOT_IMPORTED.indexOf(name) === -1) { + if (typeof(pkg.main) !== 'undefined' && NOT_IMPORTED.indexOf(name) === -1 && NOT_MCW_DEP.indexOf(name) === -1) { const nameCamel = camelCase(pkg.name.replace('@material/', '')); const src = fs.readFileSync(path.join(process.env.PWD, MASTER_JS_PATH), 'utf8'); const ast = recast.parse(src, { @@ -193,6 +209,8 @@ function checkAutoInitAddedInMDCPackage(ast) { let nameCamel = camelCase(pkg.name.replace('@material/', '')); if (nameCamel === 'textfield') { nameCamel = 'textField'; + } else if (nameCamel === 'switch') { + nameCamel = 'switchControl'; } let autoInitedCount = 0; traverse(ast, { @@ -216,6 +234,8 @@ function checkComponentExportedAddedInMDCPackage(ast) { let nameCamel = camelCase(pkg.name.replace('@material/', '')); if (nameCamel === 'textfield') { nameCamel = 'textField'; + } else if (nameCamel === 'switch') { + nameCamel = 'switchControl'; } let isExported = false; traverse(ast, { diff --git a/scripts/webpack/css-bundle-factory.js b/scripts/webpack/css-bundle-factory.js index 5ecef62e111..a84bac23e4c 100644 --- a/scripts/webpack/css-bundle-factory.js +++ b/scripts/webpack/css-bundle-factory.js @@ -162,6 +162,10 @@ class CssBundleFactory { 'mdc.slider': getAbsolutePath('/packages/mdc-slider/mdc-slider.scss'), 'mdc.snackbar': getAbsolutePath('/packages/mdc-snackbar/mdc-snackbar.scss'), 'mdc.switch': getAbsolutePath('/packages/mdc-switch/mdc-switch.scss'), + 'mdc.tab': getAbsolutePath('/packages/mdc-tab/mdc-tab.scss'), + 'mdc.tab-bar': getAbsolutePath('/packages/mdc-tab-bar/mdc-tab-bar.scss'), + 'mdc.tab-indicator': getAbsolutePath('/packages/mdc-tab-indicator/mdc-tab-indicator.scss'), + 'mdc.tab-scroller': getAbsolutePath('/packages/mdc-tab-scroller/mdc-tab-scroller.scss'), 'mdc.tabs': getAbsolutePath('/packages/mdc-tabs/mdc-tabs.scss'), 'mdc.textfield': getAbsolutePath('/packages/mdc-textfield/mdc-text-field.scss'), 'mdc.theme': getAbsolutePath('/packages/mdc-theme/mdc-theme.scss'), diff --git a/scripts/webpack/js-bundle-factory.js b/scripts/webpack/js-bundle-factory.js index 5c698a59590..38a0da5c373 100644 --- a/scripts/webpack/js-bundle-factory.js +++ b/scripts/webpack/js-bundle-factory.js @@ -147,6 +147,11 @@ class JsBundleFactory { selectionControl: getAbsolutePath('/packages/mdc-selection-control/index.js'), slider: getAbsolutePath('/packages/mdc-slider/index.js'), snackbar: getAbsolutePath('/packages/mdc-snackbar/index.js'), + switch: getAbsolutePath('/packages/mdc-switch/index.js'), + tab: getAbsolutePath('/packages/mdc-tab/index.js'), + tabBar: getAbsolutePath('/packages/mdc-tab-bar/index.js'), + tabIndicator: getAbsolutePath('/packages/mdc-tab-indicator/index.js'), + tabScroller: getAbsolutePath('/packages/mdc-tab-scroller/index.js'), tabs: getAbsolutePath('/packages/mdc-tabs/index.js'), textfield: getAbsolutePath('/packages/mdc-textfield/index.js'), toolbar: getAbsolutePath('/packages/mdc-toolbar/index.js'), diff --git a/test/screenshot/README.md b/test/screenshot/README.md index be4eefa961d..43cf43dc7d2 100644 --- a/test/screenshot/README.md +++ b/test/screenshot/README.md @@ -152,20 +152,20 @@ There are two types of components: 1. **Large** fullpage components (dialog, drawer, top app bar, etc.) 2. **Small** widget components (button, checkbox, linear progress, etc.) -Test pages for **small** components must have a `test-main--mobile-viewport` class on the `
` element: +Test pages for **small** components must have a `test-viewport--mobile` class on the `
` element: ```html -
+
``` This class ensures that all components on the page fit inside an "average" mobile viewport without scrolling. This is necessary because most browsers' WebDriver implementations do not support taking screenshots of the entire `document`. -Test pages for **large** components, however, must _not_ use the `--mobile-viewport` class: +Test pages for **large** components, however, must _not_ use the `test-viewport--mobile` class: ```html -
+
``` For **small** components, you also need to specify the dimensions of the `test-cell--FOO` class in your component's @@ -185,8 +185,8 @@ This prevents noisy diffs in the event that your component's `height` or `margin CSS Class | Description --- | --- -`test-main` | Mandatory. Wraps all page content. -`test-main--mobile-viewport` | Mandatory (**small** components only). Ensures that all page content fits in a mobile viewport. +`test-viewport` | Mandatory. Wraps all page content. +`test-viewport--mobile` | Mandatory (**small** components only). Ensures that all page content fits in a mobile viewport. `test-cell--` | Mandatory (**small** components only). Sets the dimensions of cells in the grid. `custom---` | Mandatory (mixin test pages only). Calls a single Sass theme mixin. diff --git a/test/screenshot/golden.json b/test/screenshot/golden.json index d57ae5718be..cd7e871a792 100644 --- a/test/screenshot/golden.json +++ b/test/screenshot/golden.json @@ -278,6 +278,33 @@ "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-icon-button/mixins/ink-color.html.windows_ie_11.png" } }, + "spec/mdc-switch/classes/baseline.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/21/00_22_42_705/spec/mdc-switch/classes/baseline.html", + "screenshots": { + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/23/18_31_44_533/spec/mdc-switch/classes/baseline.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/23/18_31_44_533/spec/mdc-switch/classes/baseline.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/23/18_31_44_533/spec/mdc-switch/classes/baseline.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/23/18_31_44_533/spec/mdc-switch/classes/baseline.html.windows_ie_11.png" + } + }, + "spec/mdc-switch/mixins/thumb-color.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/21/00_22_42_705/spec/mdc-switch/mixins/thumb-color.html", + "screenshots": { + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/23/18_31_44_533/spec/mdc-switch/mixins/thumb-color.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/23/18_31_44_533/spec/mdc-switch/mixins/thumb-color.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/23/18_31_44_533/spec/mdc-switch/mixins/thumb-color.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/23/18_31_44_533/spec/mdc-switch/mixins/thumb-color.html.windows_ie_11.png" + } + }, + "spec/mdc-switch/mixins/track-color.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/21/00_22_42_705/spec/mdc-switch/mixins/track-color.html", + "screenshots": { + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/23/18_31_44_533/spec/mdc-switch/mixins/track-color.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/23/18_31_44_533/spec/mdc-switch/mixins/track-color.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/23/18_31_44_533/spec/mdc-switch/mixins/track-color.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/23/18_31_44_533/spec/mdc-switch/mixins/track-color.html.windows_ie_11.png" + } + }, "spec/mdc-textfield/classes/baseline-textfield.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/kfranqueiro/2018/07/18/20_19_24_759/spec/mdc-textfield/classes/baseline-textfield.html", "screenshots": { diff --git a/test/screenshot/infra/lib/github-api.js b/test/screenshot/infra/lib/github-api.js index fba2ae9f6f1..e4ab874c0a7 100644 --- a/test/screenshot/infra/lib/github-api.js +++ b/test/screenshot/infra/lib/github-api.js @@ -103,7 +103,7 @@ class GitHubApi { this.createStatusThrottled_({ state, - targetUrl: `https://travis-ci.org/material-components/material-components-web/jobs/${process.env.TRAVIS_JOB_ID}`, + targetUrl: `https://travis-ci.com/material-components/material-components-web/jobs/${process.env.TRAVIS_JOB_ID}`, description, }); } @@ -145,7 +145,7 @@ class GitHubApi { const numTotal = runnableScreenshots.length; state = GitHubApi.PullRequestState.PENDING; - targetUrl = `https://travis-ci.org/material-components/material-components-web/jobs/${process.env.TRAVIS_JOB_ID}`; + targetUrl = `https://travis-ci.com/material-components/material-components-web/jobs/${process.env.TRAVIS_JOB_ID}`; description = `Running ${numTotal.toLocaleString()} screenshots...`; } @@ -159,7 +159,7 @@ class GitHubApi { return await this.createStatusUnthrottled_({ state: GitHubApi.PullRequestState.ERROR, - targetUrl: `https://travis-ci.org/material-components/material-components-web/jobs/${process.env.TRAVIS_JOB_ID}`, + targetUrl: `https://travis-ci.com/material-components/material-components-web/jobs/${process.env.TRAVIS_JOB_ID}`, description: 'Error running screenshot tests', }); } diff --git a/test/screenshot/infra/lib/image-cropper.js b/test/screenshot/infra/lib/image-cropper.js index 473b6bf69f9..2c42ae2b8ab 100644 --- a/test/screenshot/infra/lib/image-cropper.js +++ b/test/screenshot/infra/lib/image-cropper.js @@ -16,7 +16,7 @@ const Jimp = require('jimp'); -const TRIM_COLOR_CSS_VALUE = '#abc123'; // Value must match `$test-trim-color` in `fixture.scss` +const TRIM_COLOR_CSS_VALUE = '#abc123'; // Value must match `$test-viewport-trim-color` in `fixture.scss` /** * Fractional value (0 to 1 inclusive) indicating the minimum percentage of pixels in a row or column that must diff --git a/test/screenshot/spec/fixture.js b/test/screenshot/spec/fixture.js index 15e1f2ae7d0..8e5cb07cba8 100644 --- a/test/screenshot/spec/fixture.js +++ b/test/screenshot/spec/fixture.js @@ -45,8 +45,8 @@ window.mdc.testFixture = { }, measureMobileViewport_() { - const mainEl = document.querySelector('.test-main'); - if (!mainEl || !mainEl.classList.contains('test-main--mobile-viewport')) { + const mainEl = document.querySelector('.test-viewport'); + if (!mainEl || !mainEl.classList.contains('test-viewport--mobile')) { return; } @@ -57,12 +57,12 @@ window.mdc.testFixture = { mainEl.style.height = ''; if (autoHeight > setHeight) { - mainEl.classList.add('test-main--overflowing'); + mainEl.classList.add('test-viewport--overflowing'); console.error(` Page content overflows a mobile viewport! Consider splitting this page into two separate pages. If you are trying to create a test page for a fullscreen component like drawer or top-app-bar, -remove the 'test-main--mobile-viewport' class from the '
' element. +remove the 'test-viewport--mobile' class from the '
' element. `.trim()); } }); diff --git a/test/screenshot/spec/fixture.scss b/test/screenshot/spec/fixture.scss index b2b6ffd6515..65428596488 100644 --- a/test/screenshot/spec/fixture.scss +++ b/test/screenshot/spec/fixture.scss @@ -17,30 +17,30 @@ @import url("https://fonts.googleapis.com/css?family=Roboto:300,400,500,600,700"); @import url("https://fonts.googleapis.com/icon?family=Material+Icons"); -$test-trim-color: #abc123; // Value must match `TRIM_COLOR_CSS_VALUE` in `image-cropper.js` -$test-grid-color: #dddddd; +$test-viewport-trim-color: #abc123; // Value must match `TRIM_COLOR_CSS_VALUE` in `image-cropper.js` +$test-layout-cell-grid-color: #dddddd; -.test-body { +.test-container { display: flex; box-sizing: border-box; margin: 0; padding: 0; } -.test-main { +.test-viewport { position: relative; box-sizing: border-box; } -.test-main--mobile-viewport { +.test-viewport--mobile { width: 350px; // fits 2 columns of buttons within a Galaxy S7 viewport height: 590px; // fits 8 rows of buttons within a Galaxy S7 viewport margin: 5px 0 5px 5px; // Extra padding ensures that CBT's "chromeless" screenshots don't get cut off - border: 1px solid $test-trim-color; + border: 1px solid $test-viewport-trim-color; overflow: hidden; } -.test-main--overflowing::after { +.test-viewport--overflowing::after { display: block; position: absolute; right: 0; @@ -56,7 +56,7 @@ $test-grid-color: #dddddd; content: "ERROR: Content overflows mobile viewport!"; } -.test-grid { +.test-layout { display: flex; flex-wrap: wrap; box-sizing: border-box; @@ -72,8 +72,8 @@ $test-grid-color: #dddddd; // Ruler grid pattern // https://stackoverflow.com/a/32861765/467582 background-image: - linear-gradient(to right, #{$test-grid-color} 1px, transparent 1.01px), // fraction for IE 11 - linear-gradient(to bottom, #{$test-grid-color} 1px, transparent 1.01px); // fraction for IE 11 + linear-gradient(to right, #{$test-layout-cell-grid-color} 1px, transparent 1.01px), // fraction for IE 11 + linear-gradient(to bottom, #{$test-layout-cell-grid-color} 1px, transparent 1.01px); // fraction for IE 11 background-size: 10px 10px; } diff --git a/test/screenshot/spec/mdc-button/classes/baseline-button-with-icons.html b/test/screenshot/spec/mdc-button/classes/baseline-button-with-icons.html index 2f8e45ec535..34682641822 100644 --- a/test/screenshot/spec/mdc-button/classes/baseline-button-with-icons.html +++ b/test/screenshot/spec/mdc-button/classes/baseline-button-with-icons.html @@ -24,9 +24,9 @@ - -
-
+ +
+
diff --git a/test/screenshot/spec/mdc-button/classes/baseline-link-with-icons.html b/test/screenshot/spec/mdc-button/classes/baseline-link-with-icons.html index 589fdb8faf7..cd6db47e573 100644 --- a/test/screenshot/spec/mdc-button/classes/baseline-link-with-icons.html +++ b/test/screenshot/spec/mdc-button/classes/baseline-link-with-icons.html @@ -24,9 +24,9 @@ - -
-
+ +
+
diff --git a/test/screenshot/spec/mdc-button/classes/baseline-link-without-icons.html b/test/screenshot/spec/mdc-button/classes/baseline-link-without-icons.html index a6fc7a758c3..a0df2f4e52b 100644 --- a/test/screenshot/spec/mdc-button/classes/baseline-link-without-icons.html +++ b/test/screenshot/spec/mdc-button/classes/baseline-link-without-icons.html @@ -24,9 +24,9 @@ - -
-
+ +
+
diff --git a/test/screenshot/spec/mdc-button/classes/dense-button-with-icons.html b/test/screenshot/spec/mdc-button/classes/dense-button-with-icons.html index e2bf0599a7c..9a285d2f6a9 100644 --- a/test/screenshot/spec/mdc-button/classes/dense-button-with-icons.html +++ b/test/screenshot/spec/mdc-button/classes/dense-button-with-icons.html @@ -24,9 +24,9 @@ - -
-
+ +
+
diff --git a/test/screenshot/spec/mdc-button/classes/dense-link-with-icons.html b/test/screenshot/spec/mdc-button/classes/dense-link-with-icons.html index 907a60fe354..ca6ff03388b 100644 --- a/test/screenshot/spec/mdc-button/classes/dense-link-with-icons.html +++ b/test/screenshot/spec/mdc-button/classes/dense-link-with-icons.html @@ -24,9 +24,9 @@ - -
-
+ +
+
diff --git a/test/screenshot/spec/mdc-button/classes/dense-link-without-icons.html b/test/screenshot/spec/mdc-button/classes/dense-link-without-icons.html index 992785d7537..33d0af6113a 100644 --- a/test/screenshot/spec/mdc-button/classes/dense-link-without-icons.html +++ b/test/screenshot/spec/mdc-button/classes/dense-link-without-icons.html @@ -24,9 +24,9 @@ - -
-
+ +
+
diff --git a/test/screenshot/spec/mdc-button/mixins/container-fill-color.html b/test/screenshot/spec/mdc-button/mixins/container-fill-color.html index b429d2f8b2f..d0ebb320284 100644 --- a/test/screenshot/spec/mdc-button/mixins/container-fill-color.html +++ b/test/screenshot/spec/mdc-button/mixins/container-fill-color.html @@ -24,9 +24,9 @@ - -
-
+ +
+
diff --git a/test/screenshot/spec/mdc-button/mixins/corner-radius.html b/test/screenshot/spec/mdc-button/mixins/corner-radius.html index 3773867ecde..7d081e41f89 100644 --- a/test/screenshot/spec/mdc-button/mixins/corner-radius.html +++ b/test/screenshot/spec/mdc-button/mixins/corner-radius.html @@ -24,9 +24,9 @@ - -
-
+ +
+
diff --git a/test/screenshot/spec/mdc-button/mixins/filled-accessible.html b/test/screenshot/spec/mdc-button/mixins/filled-accessible.html index fd953216b22..36a565b0f37 100644 --- a/test/screenshot/spec/mdc-button/mixins/filled-accessible.html +++ b/test/screenshot/spec/mdc-button/mixins/filled-accessible.html @@ -24,9 +24,9 @@ - -
-
+ +
+
diff --git a/test/screenshot/spec/mdc-button/mixins/horizontal-padding-baseline.html b/test/screenshot/spec/mdc-button/mixins/horizontal-padding-baseline.html index 3ddf5e79bf4..5a015c37d2f 100644 --- a/test/screenshot/spec/mdc-button/mixins/horizontal-padding-baseline.html +++ b/test/screenshot/spec/mdc-button/mixins/horizontal-padding-baseline.html @@ -24,9 +24,9 @@ - -
-
+ +
+
diff --git a/test/screenshot/spec/mdc-button/mixins/horizontal-padding-dense.html b/test/screenshot/spec/mdc-button/mixins/horizontal-padding-dense.html index 7625528a577..61e4ce3cd1a 100644 --- a/test/screenshot/spec/mdc-button/mixins/horizontal-padding-dense.html +++ b/test/screenshot/spec/mdc-button/mixins/horizontal-padding-dense.html @@ -24,9 +24,9 @@ - -
-
+ +
+
diff --git a/test/screenshot/spec/mdc-button/mixins/icon-color.html b/test/screenshot/spec/mdc-button/mixins/icon-color.html index 11bca0fccc2..da21ecb894f 100644 --- a/test/screenshot/spec/mdc-button/mixins/icon-color.html +++ b/test/screenshot/spec/mdc-button/mixins/icon-color.html @@ -24,9 +24,9 @@ - -
-
+ +
+
diff --git a/test/screenshot/spec/mdc-button/mixins/stroke-color.html b/test/screenshot/spec/mdc-button/mixins/stroke-color.html index b82c581a1d8..35498faf686 100644 --- a/test/screenshot/spec/mdc-button/mixins/stroke-color.html +++ b/test/screenshot/spec/mdc-button/mixins/stroke-color.html @@ -24,9 +24,9 @@ - -
-
+ +
+
diff --git a/test/screenshot/spec/mdc-button/mixins/stroke-width.html b/test/screenshot/spec/mdc-button/mixins/stroke-width.html index 5fed1c11dd7..6799a2ec76b 100644 --- a/test/screenshot/spec/mdc-button/mixins/stroke-width.html +++ b/test/screenshot/spec/mdc-button/mixins/stroke-width.html @@ -24,9 +24,9 @@ - -
-
+ +
+
diff --git a/test/screenshot/spec/mdc-checkbox/classes/baseline.html b/test/screenshot/spec/mdc-checkbox/classes/baseline.html index 786ffc361e5..20ee3722270 100644 --- a/test/screenshot/spec/mdc-checkbox/classes/baseline.html +++ b/test/screenshot/spec/mdc-checkbox/classes/baseline.html @@ -24,9 +24,9 @@ - -
-
+ +
+
- -
+ +