diff --git a/demos/select.html b/demos/select.html index c4d43b30a92..2608f0e5865 100644 --- a/demos/select.html +++ b/demos/select.html @@ -131,6 +131,7 @@

Fully-Featured JS Component

+

Select box

@@ -178,6 +179,67 @@

Select box

+ +
+

Outlined Select

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

Currently selected: (none)

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

Pre-selected option via HTML

@@ -307,6 +369,49 @@

MDC Select with optgroups

updateSelectedTextContent(); }); }); + + demoReady(function() { + + var root = document.getElementById('outline-js-select'); + var currentlySelected = document.getElementById('currently-selected-outline'); + var select = new mdc.select.MDCSelect(root); + var demoWrapper = root.parentElement; + var rtlCb = document.getElementById('rtl-outline'); + var alternateColorsCb = document.getElementById('alternate-colors-outline'); + var disabledCb = document.getElementById('disabled-outline'); + var setSelectedButton = document.getElementById('set-selected-index-zero-button-outline'); + var setValueMeatButton = document.getElementById('set-value-meat-button-outline'); + function updateSelectedTextContent() { + var value = select.value; + var index = select.selectedIndex; + currentlySelected.textContent = value ? value + ' at index ' + index : '(none)'; + } + root.addEventListener('change', function() { + updateSelectedTextContent(); + }); + rtlCb.addEventListener('change', function() { + if (rtlCb.checked) { + demoWrapper.setAttribute('dir', 'rtl'); + } else { + demoWrapper.removeAttribute('dir'); + } + select.layout(); + }); + alternateColorsCb.addEventListener('change', function() { + root.classList[alternateColorsCb.checked ? 'add' : 'remove']('demo-select-custom-colors'); + }); + disabledCb.addEventListener('change', function() { + select.disabled = disabledCb.checked; + }); + setSelectedButton.addEventListener('click', function() { + select.selectedIndex = 0; + updateSelectedTextContent(); + }); + setValueMeatButton.addEventListener('click', function() { + select.value = 'meat'; + updateSelectedTextContent(); + }); + }); diff --git a/demos/select.scss b/demos/select.scss index bc00303e9a5..c1ee05f4df2 100644 --- a/demos/select.scss +++ b/demos/select.scss @@ -36,6 +36,12 @@ @include mdc-select-container-fill-color(rgba(blue, .1)); } +.demo-select-custom-colors.mdc-select--outlined { + @include mdc-select-outline-color(rgba(blue, .6)); + @include mdc-select-hover-outline-color(rgba(blue, .87)); + @include mdc-select-focused-outline-color(green); +} + .button-container { margin: 8px 0; } diff --git a/packages/mdc-select/README.md b/packages/mdc-select/README.md index cf211e1c852..d19930ee1d3 100644 --- a/packages/mdc-select/README.md +++ b/packages/mdc-select/README.md @@ -98,6 +98,26 @@ modifier class on the root element. ``` +### Outlined Select + +The Select Outlined variant uses the `mdc-notched-outline` in place of the `mdc-line-ripple` element and adds the +`mdc-select--outlined` modifier class on the root element. + +```html +
+ + +
+ + + +
+
+
+``` + ### Additional Information #### Select with pre-selected option @@ -195,6 +215,7 @@ Mixin | Description `mdc-select-bottom-line-color($color)` | Customizes the color of the default bottom line of the select. `mdc-select-focused-bottom-line-color($color)` | Customizes the color of the bottom line of the select when focused. `mdc-select-hover-bottom-line-color($color)` | Customizes the color of the bottom line when select is hovered. +`mdc-select-outline-corner-radius($color)` | Customizes the color of the notched outline when select is focused. > NOTE: To further customize the floating label, please see the [floating label documentation](./../mdc-floating-label/README.md). @@ -237,6 +258,7 @@ If you are using a JavaScript framework, such as React or Angular, you can creat | Method Signature | Description | | --- | --- | +| `notchOutline(openNotch: boolean) => void` | Opens/closes the notched outline. | | `setValue(value: string) => void` | Sets the value of the component. | | `setDisabled(disabled: boolean) => void` | Adds/removes disabled class, and sets disabled attribute on the component. | | `setSelectedIndex(selectedIndex: number) => void` | Sets the selected index of the component. | diff --git a/packages/mdc-select/_mixins.scss b/packages/mdc-select/_mixins.scss index b789c6f1c37..29798d60335 100644 --- a/packages/mdc-select/_mixins.scss +++ b/packages/mdc-select/_mixins.scss @@ -17,6 +17,7 @@ @import "@material/floating-label/mixins"; @import "@material/theme/mixins"; @import "@material/line-ripple/mixins"; +@import "@material/notched-outline/mixins"; // Public @@ -64,6 +65,24 @@ } } +@mixin mdc-select-outline-color($color) { + &:not(.mdc-select--disabled) { + @include mdc-select-outline-color_($color); + } +} + +@mixin mdc-select-hover-outline-color($color) { + &:not(.mdc-select--disabled) { + @include mdc-select-hover-outline-color_($color); + } +} + +@mixin mdc-select-focused-outline-color($color) { + &:not(.mdc-select--disabled) { + @include mdc-select-focused-outline-color_($color); + } +} + // Private @mixin mdc-select-focused-line-ripple_ { .mdc-select__native-control:focus ~ .mdc-line-ripple { @@ -71,6 +90,13 @@ } } +@mixin mdc-select-focused-outline_ { + .mdc-select__native-control:focus ~ .mdc-notched-outline { + @include mdc-notched-outline-stroke-width(2px); + @content; + } +} + @mixin mdc-select-ink-color_($color) { .mdc-select__native-control { @include mdc-theme-prop(color, $color); @@ -100,3 +126,43 @@ @mixin mdc-select-dd-arrow-svg-bg_($fill-hex-number: 000000, $opacity: .54) { background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2210px%22%20height%3D%225px%22%20viewBox%3D%227%2010%2010%205%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%0A%20%20%20%20%3Cpolygon%20id%3D%22Shape%22%20stroke%3D%22none%22%20fill%3D%22%23#{$fill-hex-number}%22%20fill-rule%3D%22evenodd%22%20opacity%3D%22#{$opacity}%22%20points%3D%227%2010%2012%2015%2017%2010%22%3E%3C%2Fpolygon%3E%0A%3C%2Fsvg%3E"); } + +@mixin mdc-select-outline-corner-radius($radius) { + // NOTE: idle and notched state border radius mixins + // are broken into 2 different mixins, otherwise + // we would be overly specific (big no, no). The cause of + // this is because .mdc-notched-outline and .mdc-notched-outline__idle + // are siblings. .mdc-notched-outline__idle needs to be a child of + // .mdc-notched-outline in order to remedy this issue. + .mdc-notched-outline { + @include mdc-notched-outline-corner-radius($radius); + } + + @include mdc-notched-outline-idle-corner-radius($radius); +} + +@mixin mdc-select-outline-color_($color) { + // NOTE: outlined version of select wants the "idle" and + // "notched" outline to have the same color. This covers two cases: + // 1) text field renders with NO value in the "idle" state + // 2) text field renders with a value in the "notched" state + @include mdc-notched-outline-idle-color($color); + @include mdc-notched-outline-color($color); +} + +@mixin mdc-select-hover-outline-color_($color) { + &:not(.mdc-select__native-control:focus) .mdc-select__native-control:hover ~ { + @include mdc-notched-outline-idle-color($color); + + // stylelint-disable-next-line selector-max-specificity + .mdc-notched-outline { + @include mdc-notched-outline-color($color); + } + } +} + +@mixin mdc-select-focused-outline-color_($color) { + @include mdc-select-focused-outline_ { + @include mdc-notched-outline-color($color); + } +} diff --git a/packages/mdc-select/_variables.scss b/packages/mdc-select/_variables.scss index 1101a7ae346..d2bda4f4ea5 100644 --- a/packages/mdc-select/_variables.scss +++ b/packages/mdc-select/_variables.scss @@ -18,6 +18,7 @@ $mdc-select-arrow-padding: 26px; $mdc-select-label-padding: 16px; +$mdc-select-border-radius: 4px; $mdc-select-ink-color: rgba(mdc-theme-prop-value(on-surface), .87); $mdc-select-disabled-ink-color: rgba(mdc-theme-prop-value(on-surface), .37); @@ -32,3 +33,12 @@ $mdc-select-bottom-line-hover-color: rgba(mdc-theme-prop-value(on-surface), .87) $mdc-select-box-fill-color: mix(mdc-theme-prop-value(on-surface), mdc-theme-prop-value(surface), 4%); $mdc-select-box-disabled-fill-color: mix(mdc-theme-prop-value(on-surface), mdc-theme-prop-value(surface), 2%); + +$mdc-select-outlined-idle-border: rgba(mdc-theme-prop-value(on-surface), .24); + +// should be .06 after mdc-select opacity is applied +$mdc-select-outlined-disabled-border: rgba(mdc-theme-prop-value(on-surface), .16); +$mdc-select-outlined-hover-border: rgba(mdc-theme-prop-value(on-surface), .87); + +$mdc-select-outlined-label-position-y: 130%; +$mdc-select-outlined-dense-label-position-y: 110%; diff --git a/packages/mdc-select/constants.js b/packages/mdc-select/constants.js index 8a4fd6e0d9a..36d42b64a17 100644 --- a/packages/mdc-select/constants.js +++ b/packages/mdc-select/constants.js @@ -13,15 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export const cssClasses = { +const cssClasses = { BOX: 'mdc-select--box', DISABLED: 'mdc-select--disabled', ROOT: 'mdc-select', + OUTLINED: 'mdc-select--outlined', }; -export const strings = { +const strings = { CHANGE_EVENT: 'MDCSelect:change', LINE_RIPPLE_SELECTOR: '.mdc-line-ripple', LABEL_SELECTOR: '.mdc-floating-label', NATIVE_CONTROL_SELECTOR: '.mdc-select__native-control', + OUTLINE_SELECTOR: '.mdc-notched-outline', }; + +/** @enum {number} */ +const numbers = { + LABEL_SCALE: 0.75, +}; + +export {cssClasses, strings, numbers}; diff --git a/packages/mdc-select/foundation.js b/packages/mdc-select/foundation.js index 1400e6a50dd..3b8f2a2209a 100644 --- a/packages/mdc-select/foundation.js +++ b/packages/mdc-select/foundation.js @@ -15,13 +15,17 @@ */ import {MDCFoundation} from '@material/base/index'; -import {cssClasses, strings} from './constants'; +import {cssClasses, strings, numbers} from './constants'; export default class MDCSelectFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } + static get numbers() { + return numbers; + } + static get strings() { return strings; } @@ -30,6 +34,7 @@ export default class MDCSelectFoundation extends MDCFoundation { return { addClass: (/* className: string */) => {}, removeClass: (/* className: string */) => {}, + hasClass: (/* className: string */) => false, floatLabel: (/* value: boolean */) => {}, activateBottomLine: () => {}, deactivateBottomLine: () => {}, @@ -40,6 +45,12 @@ export default class MDCSelectFoundation extends MDCFoundation { setDisabled: (/* disabled: boolean */) => {}, getValue: () => /* string */ '', setValue: (/* value: string */) => {}, + isRtl: () => false, + hasLabel: () => {}, + getLabelWidth: () => {}, + hasOutline: () => {}, + notchOutline: () => {}, + closeOutline: () => {}, }; } @@ -86,10 +97,12 @@ export default class MDCSelectFoundation extends MDCFoundation { floatLabelWithValue_() { const optionHasValue = this.adapter_.getValue().length > 0; this.adapter_.floatLabel(optionHasValue); + this.notchOutline(optionHasValue); } handleFocus_() { this.adapter_.floatLabel(true); + this.notchOutline(true); this.adapter_.activateBottomLine(); } @@ -101,4 +114,23 @@ export default class MDCSelectFoundation extends MDCFoundation { handleSelect_() { this.setSelectedIndex(this.adapter_.getSelectedIndex()); } + + /** + * Opens/closes the notched outline. + * @param {boolean} openNotch + */ + notchOutline(openNotch) { + if (!this.adapter_.hasOutline() || !this.adapter_.hasLabel()) { + return; + } + + if (openNotch) { + const labelScale = numbers.LABEL_SCALE; + const labelWidth = this.adapter_.getLabelWidth() * labelScale; + const isRtl = this.adapter_.isRtl(); + this.adapter_.notchOutline(labelWidth, isRtl); + } else { + this.adapter_.closeOutline(); + } + } } diff --git a/packages/mdc-select/index.js b/packages/mdc-select/index.js index d730b4af03a..92191db0c17 100644 --- a/packages/mdc-select/index.js +++ b/packages/mdc-select/index.js @@ -18,6 +18,7 @@ import {MDCComponent} from '@material/base/index'; import {MDCFloatingLabel} from '@material/floating-label/index'; import {MDCLineRipple} from '@material/line-ripple/index'; import {MDCRipple, MDCRippleFoundation} from '@material/ripple/index'; +import {MDCNotchedOutline} from '@material/notched-outline/index'; import MDCSelectFoundation from './foundation'; import {cssClasses, strings} from './constants'; @@ -53,9 +54,18 @@ export class MDCSelect extends MDCComponent { this.foundation_.setDisabled(disabled); } + /** + * Recomputes the outline SVG path for the outline element. + */ + layout() { + const openNotch = this.nativeControl_.value.length > 0; + this.foundation_.notchOutline(openNotch); + } + initialize( labelFactory = (el) => new MDCFloatingLabel(el), - lineRippleFactory = (el) => new MDCLineRipple(el)) { + lineRippleFactory = (el) => new MDCLineRipple(el), + outlineFactory = (el) => new MDCNotchedOutline(el)) { this.nativeControl_ = this.root_.querySelector(strings.NATIVE_CONTROL_SELECTOR); const labelElement = this.root_.querySelector(strings.LABEL_SELECTOR); if (labelElement) { @@ -65,6 +75,10 @@ export class MDCSelect extends MDCComponent { if (lineRippleElement) { this.lineRipple_ = lineRippleFactory(lineRippleElement); } + const outlineElement = this.root_.querySelector(strings.OUTLINE_SELECTOR); + if (outlineElement) { + this.outline_ = outlineFactory(outlineElement); + } if (this.root_.classList.contains(cssClasses.BOX)) { this.ripple = this.initRipple_(); @@ -81,14 +95,10 @@ export class MDCSelect extends MDCComponent { } getDefaultFoundation() { - return new MDCSelectFoundation({ + return new MDCSelectFoundation((Object.assign({ addClass: (className) => this.root_.classList.add(className), removeClass: (className) => this.root_.classList.remove(className), - floatLabel: (value) => { - if (this.label_) { - this.label_.float(value); - } - }, + hasClass: (className) => this.root_.classList.contains(className), activateBottomLine: () => { if (this.lineRipple_) { this.lineRipple_.activate(); @@ -106,7 +116,11 @@ export class MDCSelect extends MDCComponent { setSelectedIndex: (index) => this.nativeControl_.selectedIndex = index, getValue: () => this.nativeControl_.value, setValue: (value) => this.nativeControl_.value = value, - }); + isRtl: () => window.getComputedStyle(this.root_).getPropertyValue('direction') === 'rtl', + }, + this.getOutlineAdapterMethods_(), + this.getLabelAdapterMethods_())) + ); } initialSyncWithDOM() { @@ -122,6 +136,54 @@ export class MDCSelect extends MDCComponent { if (this.ripple) { this.ripple.destroy(); } + if (this.outline_) { + this.outline_.destroy(); + } super.destroy(); } + + /** + * @return {!{ + * notchOutline: function(number, boolean): undefined, + * hasOutline: function(): boolean, + * }} + */ + getOutlineAdapterMethods_() { + return { + notchOutline: (labelWidth, isRtl) => { + if (this.outline_) { + this.outline_.notch(labelWidth, isRtl); + } + }, + closeOutline: () => { + if (this.outline_) { + this.outline_.closeNotch(); + } + }, + hasOutline: () => !!this.outline_, + }; + } + + /** + * @return {!{ + * floatLabel: function(boolean): undefined, + * hasLabel: function(): boolean, + * getLabelWidth: function(): number, + * }} + */ + getLabelAdapterMethods_() { + return { + floatLabel: (shouldFloat) => { + if (this.label_) { + this.label_.float(shouldFloat); + } + }, + hasLabel: () => !!this.label_, + getLabelWidth: () => { + if (this.label_) { + return this.label_.getWidth(); + } + }, + }; + } } diff --git a/packages/mdc-select/mdc-select.scss b/packages/mdc-select/mdc-select.scss index 12983cc6b18..1a0d5bac325 100644 --- a/packages/mdc-select/mdc-select.scss +++ b/packages/mdc-select/mdc-select.scss @@ -19,6 +19,7 @@ @import "./variables"; @import "@material/animation/variables"; @import "@material/line-ripple/mdc-line-ripple"; +@import "@material/notched-outline/mdc-notched-outline"; @import "@material/floating-label/mdc-floating-label"; @import "@material/typography/mixins"; @import "@material/ripple/common"; @@ -46,7 +47,6 @@ height: 52px; background-repeat: no-repeat; background-position: right 8px bottom 12px; - overflow: hidden; @include mdc-rtl { background-position: left 8px bottom 12px; @@ -106,6 +106,7 @@ height: 56px; border-radius: 4px 4px 0 0; background-position: right 10px center; + overflow: hidden; @include mdc-rtl { background-position: left 10px center; @@ -132,12 +133,44 @@ // stylelint-enable plugin/selector-bem-pattern } -.mdc-select--disabled { - @include mdc-select-dd-arrow-svg-bg_($mdc-select-disabled-arrow-color); +.mdc-select--outlined { + @include mdc-select-outline-color($mdc-select-outlined-idle-border); + @include mdc-select-hover-outline-color($mdc-select-outlined-hover-border); + @include mdc-select-focused-outline-color(primary); + @include mdc-floating-label-float-position($mdc-select-outlined-label-position-y); + @include mdc-floating-label-shake-animation(text-field-outlined); + @include mdc-select-outline-corner-radius($mdc-select-border-radius); - &.mdc-select--box { - @include mdc-select-container-fill-color_($mdc-select-box-disabled-fill-color); + height: 56px; + border: none; + background-position: right 10px center; + + @include mdc-rtl { + background-position: left 10px center; + } + + .mdc-select__native-control { + @include mdc-rtl-reflexive-property(padding, $mdc-select-label-padding, $mdc-select-arrow-padding); + + display: flex; + padding-top: 12px; + padding-bottom: 12px; + border: none; + background-color: transparent; + z-index: 1; + } + + // stylelint-disable-next-line plugin/selector-bem-pattern + .mdc-floating-label { + @include mdc-rtl-reflexive-position(left, 16px); + + position: absolute; + bottom: 20px; } +} + +.mdc-select--disabled { + @include mdc-select-dd-arrow-svg-bg_($mdc-select-disabled-arrow-color); .mdc-floating-label { @include mdc-floating-label-ink-color($mdc-select-disabled-label-color); @@ -147,11 +180,24 @@ .mdc-line-ripple { display: none; } - // stylelint-enable plugin/selector-bem-pattern .mdc-select__native-control { border-bottom-style: dotted; } + // stylelint-enable plugin/selector-bem-pattern + + &.mdc-select--box { + @include mdc-select-container-fill-color_($mdc-select-box-disabled-fill-color); + } + + &.mdc-select--outlined { + // stylelint-disable-next-line plugin/selector-bem-pattern + .mdc-select__native-control { + border-bottom-style: none; + } + + @include mdc-select-outline-color_($mdc-select-outlined-disabled-border); + } opacity: .38; cursor: default; diff --git a/packages/mdc-select/package.json b/packages/mdc-select/package.json index f8631bff087..020572898a0 100644 --- a/packages/mdc-select/package.json +++ b/packages/mdc-select/package.json @@ -19,6 +19,7 @@ "@material/base": "^0.35.0", "@material/floating-label": "^0.36.0", "@material/line-ripple": "^0.35.0", + "@material/notched-outline": "^0.35.0", "@material/ripple": "^0.36.0", "@material/rtl": "^0.36.0", "@material/theme": "^0.35.0", diff --git a/test/unit/mdc-select/foundation.test.js b/test/unit/mdc-select/foundation.test.js index 0c52d674154..06a1189878a 100644 --- a/test/unit/mdc-select/foundation.test.js +++ b/test/unit/mdc-select/foundation.test.js @@ -21,7 +21,7 @@ import {setupFoundationTest} from '../helpers/setup'; import {verifyDefaultAdapter} from '../helpers/foundation'; import MDCSelectFoundation from '../../../packages/mdc-select/foundation'; -import {cssClasses, strings} from '../../../packages/mdc-select/constants'; +import {cssClasses, strings, numbers} from '../../../packages/mdc-select/constants'; suite('MDCSelectFoundation'); @@ -29,16 +29,21 @@ test('exports cssClasses', () => { assert.deepEqual(MDCSelectFoundation.cssClasses, cssClasses); }); +test('exports numbers', () => { + assert.deepEqual(MDCSelectFoundation.numbers, numbers); +}); + test('exports strings', () => { assert.deepEqual(MDCSelectFoundation.strings, strings); }); test('default adapter returns a complete adapter implementation', () => { verifyDefaultAdapter(MDCSelectFoundation, [ - 'addClass', 'removeClass', 'floatLabel', 'activateBottomLine', - 'deactivateBottomLine', 'setDisabled', - 'registerInteractionHandler', 'deregisterInteractionHandler', - 'getValue', 'setValue', 'getSelectedIndex', 'setSelectedIndex', + 'addClass', 'removeClass', 'hasClass', 'floatLabel', 'activateBottomLine', + 'deactivateBottomLine', 'setDisabled', 'registerInteractionHandler', + 'deregisterInteractionHandler', 'getValue', 'setValue', 'getSelectedIndex', + 'setSelectedIndex', 'isRtl', 'hasLabel', 'getLabelWidth', 'hasOutline', + 'notchOutline', 'closeOutline', ]); }); @@ -113,3 +118,42 @@ test('#setValue calls setSelectedIndex, which calls floatLabel true', () => { foundation.setValue('value'); td.verify(mockAdapter.floatLabel(true)); }); + +test('#notchOutline updates the SVG path of the outline element', () => { + const {foundation, mockAdapter} = setupTest(); + td.when(mockAdapter.getLabelWidth()).thenReturn(30); + td.when(mockAdapter.hasLabel()).thenReturn(true); + td.when(mockAdapter.hasOutline()).thenReturn(true); + td.when(mockAdapter.isRtl()).thenReturn(false); + + foundation.notchOutline(true); + td.verify(mockAdapter.notchOutline(30 * numbers.LABEL_SCALE, false)); +}); + +test('#notchOutline does nothing if no outline is present', () => { + const {foundation, mockAdapter} = setupTest(); + td.when(mockAdapter.hasOutline()).thenReturn(false); + td.when(mockAdapter.hasLabel()).thenReturn(true); + + foundation.notchOutline(true); + td.verify(mockAdapter.notchOutline(td.matchers.anything()), {times: 0}); +}); + +test('#notchOutline does nothing if no label is present', () => { + const {foundation, mockAdapter} = setupTest(); + td.when(mockAdapter.hasOutline()).thenReturn(true); + td.when(mockAdapter.hasLabel()).thenReturn(false); + + foundation.notchOutline(true); + td.verify(mockAdapter.notchOutline(td.matchers.anything()), {times: 0}); +}); + +test('#notchOutline calls updates notched outline to return to idle state when ' + + 'openNotch is false', () => { + const {foundation, mockAdapter} = setupTest(); + td.when(mockAdapter.hasLabel()).thenReturn(true); + td.when(mockAdapter.hasOutline()).thenReturn(true); + + foundation.notchOutline(false); + td.verify(mockAdapter.closeOutline()); +}); diff --git a/test/unit/mdc-select/mdc-select.test.js b/test/unit/mdc-select/mdc-select.test.js index e1ae7e54595..69d95c53a36 100644 --- a/test/unit/mdc-select/mdc-select.test.js +++ b/test/unit/mdc-select/mdc-select.test.js @@ -24,6 +24,7 @@ import {supportsCssVariables} from '../../../packages/mdc-ripple/util'; import {MDCRipple, MDCRippleFoundation} from '../../../packages/mdc-ripple'; import {MDCSelect} from '../../../packages/mdc-select'; import {cssClasses} from '../../../packages/mdc-select/constants'; +import {MDCNotchedOutline} from '../../../packages/mdc-notched-outline'; class FakeLabel { constructor() { @@ -38,6 +39,12 @@ class FakeBottomLine { } } +class FakeOutline { + constructor() { + this.destroy = td.func('.destroy'); + } +} + function getFixture() { return bel`
@@ -86,9 +93,10 @@ function setupTest() { const nativeControl = fixture.querySelector('.mdc-select__native-control'); const labelEl = fixture.querySelector('.mdc-floating-label'); const bottomLineEl = fixture.querySelector('.mdc-line-ripple'); - const component = new MDCSelect(fixture, /* foundation */ undefined, () => label, () => bottomLine); + const outline = new FakeOutline(); + const component = new MDCSelect(fixture, /* foundation */ undefined, () => label, () => bottomLine, () => outline); - return {fixture, nativeControl, label, labelEl, bottomLine, bottomLineEl, component}; + return {fixture, nativeControl, label, labelEl, bottomLine, bottomLineEl, component, outline}; } test('#get/setSelectedIndex', () => { @@ -171,6 +179,12 @@ test('adapter#removeClass removes a class from the root element', () => { assert.isFalse(fixture.classList.contains('foo')); }); +test('adapter#hasClass returns true if a class exists on the root element', () => { + const {component, fixture} = setupTest(); + fixture.classList.add('foo'); + assert.isTrue(component.getDefaultFoundation().adapter_.hasClass('foo')); +}); + test('adapter_.floatLabel does not throw error if label does not exist', () => { const fixture = bel`
@@ -212,6 +226,20 @@ test('adapter.activateBottomLine and adapter.deactivateBottomLine ' + () => component.getDefaultFoundation().adapter_.deactivateBottomLine()); }); + +test('#adapter.isRtl returns true when the root element is in an RTL context' + + 'and false otherwise', () => { + const wrapper = bel`
`; + const {fixture, component} = setupTest(); + assert.isFalse(component.getDefaultFoundation().adapter_.isRtl()); + + wrapper.appendChild(fixture); + document.body.appendChild(wrapper); + assert.isTrue(component.getDefaultFoundation().adapter_.isRtl()); + + document.body.removeChild(wrapper); +}); + test(`instantiates ripple when ${cssClasses.BOX} class is present`, function() { if (!supportsCssVariables(window, true)) { this.skip(); // eslint-disable-line no-invalid-this @@ -229,6 +257,13 @@ test(`instantiates ripple when ${cssClasses.BOX} class is present`, function() { raf.restore(); }); +test(`#constructor instantiates an outline on the ${cssClasses.OUTLINE_SELECTOR} element if present`, () => { + const root = getFixture(); + root.appendChild(bel`
`); + const component = new MDCSelect(root); + assert.instanceOf(component.outline_, MDCNotchedOutline); +}); + test(`handles ripple focus properly when ${cssClasses.BOX} class is present`, function() { if (!supportsCssVariables(window, true)) { this.skip(); // eslint-disable-line no-invalid-this @@ -270,6 +305,13 @@ test('#destroy removes the ripple', function() { raf.restore(); }); +test('#destroy cleans up the outline if present', () => { + const {component, outline} = setupTest(); + component.outline_ = outline; + component.destroy(); + td.verify(outline.destroy()); +}); + test(`does not instantiate ripple when ${cssClasses.BOX} class is not present`, () => { const {component} = setupTest(); assert.isUndefined(component.ripple);