From 4ce3582b05fc0102213d20de369e4e546a1507fa Mon Sep 17 00:00:00 2001 From: Bonnie Zhou Date: Tue, 19 Dec 2017 09:00:34 -0800 Subject: [PATCH] feat(text-field): Add outline subelement and demo for outlined text field (#1749) BREAKING CHANGE: Public method `layout()` and adapter methods `getIdleOutlineStyleValue()` and `isRtl()` were added to MDCTextField. Added a new subcomponent MDCTextFieldOutline, and adapter method `getWidth()` to MDCTextFieldLabel. --- demos/text-field.html | 61 ++++++++++-- packages/mdc-textfield/README.md | 32 +++++- packages/mdc-textfield/_mixins.scss | 6 +- packages/mdc-textfield/_variables.scss | 4 + packages/mdc-textfield/adapter.js | 14 +++ packages/mdc-textfield/constants.js | 2 + packages/mdc-textfield/foundation.js | 24 +++++ packages/mdc-textfield/index.js | 49 ++++++++-- packages/mdc-textfield/label/README.md | 5 + packages/mdc-textfield/label/adapter.js | 6 ++ packages/mdc-textfield/label/foundation.js | 10 ++ packages/mdc-textfield/label/index.js | 1 + packages/mdc-textfield/mdc-text-field.scss | 98 ++++++++++++++++++- packages/mdc-textfield/outline/README.md | 42 ++++++++ packages/mdc-textfield/outline/adapter.js | 50 ++++++++++ packages/mdc-textfield/outline/constants.js | 23 +++++ packages/mdc-textfield/outline/foundation.js | 88 +++++++++++++++++ packages/mdc-textfield/outline/index.js | 59 +++++++++++ .../outline/mdc-text-field-outline.scss | 61 ++++++++++++ test/unit/mdc-textfield/foundation.test.js | 47 ++++++++- .../mdc-text-field-label-foundation.test.js | 15 ++- .../mdc-text-field-label.test.js | 5 + .../mdc-text-field-outline.foundation.test.js | 45 +++++++++ .../mdc-text-field-outline.test.js | 59 +++++++++++ .../unit/mdc-textfield/mdc-text-field.test.js | 83 ++++++++++++++-- 25 files changed, 862 insertions(+), 27 deletions(-) create mode 100644 packages/mdc-textfield/outline/README.md create mode 100644 packages/mdc-textfield/outline/adapter.js create mode 100644 packages/mdc-textfield/outline/constants.js create mode 100644 packages/mdc-textfield/outline/foundation.js create mode 100644 packages/mdc-textfield/outline/index.js create mode 100644 packages/mdc-textfield/outline/mdc-text-field-outline.scss create mode 100644 test/unit/mdc-textfield/mdc-text-field-outline.foundation.test.js create mode 100644 test/unit/mdc-textfield/mdc-text-field-outline.test.js diff --git a/demos/text-field.html b/demos/text-field.html index 6d38b5a4574..eb403ae0253 100644 --- a/demos/text-field.html +++ b/demos/text-field.html @@ -134,6 +134,7 @@

Full Functionality JS Component (Floating Label, Validation)

+

Password field with validation

@@ -148,6 +149,54 @@

Password field with validation

Must be at least 8 characters long

+ +
+

Outlined Text Field

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

+ Must be at least 8 characters +

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

Text Field box

@@ -194,7 +243,7 @@

Text Field box

} else { wrapper.removeAttribute('dir'); } - tf.ripple.layout(); + tf.layout(); }); document.getElementById('box-dark-theme').addEventListener('change', function(evt) { @@ -203,7 +252,7 @@

Text Field box

document.getElementById('box-dense').addEventListener('change', function(evt) { tfEl.classList[evt.target.checked ? 'add' : 'remove']('mdc-text-field--dense'); - tf.ripple.layout(); + tf.layout(); }); }, 0); @@ -397,8 +446,8 @@

Full-Width Text Field and Textarea

wrapperLeading.removeAttribute('dir'); wrapperTrailing.removeAttribute('dir'); } - tfLeading.ripple.layout(); - tfTrailing.ripple.layout(); + tfLeading.layout(); + tfTrailing.layout(); }); document.getElementById('box-dark-theme-leading-trailing').addEventListener('change', function(evt) { @@ -408,9 +457,9 @@

Full-Width Text Field and Textarea

document.getElementById('box-dense-leading-trailing').addEventListener('change', function(evt) { tfLeadingEl.classList[evt.target.checked ? 'add' : 'remove']('mdc-text-field--dense'); - tfLeading.ripple.layout(); + tfLeading.layout(); tfTrailingEl.classList[evt.target.checked ? 'add' : 'remove']('mdc-text-field--dense'); - tfTrailing.ripple.layout(); + tfTrailing.layout(); }); document.getElementById('box-unclickable-leading-trailing').addEventListener('change', function(evt) { diff --git a/packages/mdc-textfield/README.md b/packages/mdc-textfield/README.md index 2be1b7612e0..606d1337d58 100644 --- a/packages/mdc-textfield/README.md +++ b/packages/mdc-textfield/README.md @@ -169,6 +169,23 @@ Note that **full-width text fields do not support floating labels**. Labels shou included as part of the DOM structure for full-width text fields. Full-width textareas behave normally. +### Outlined Text Fields + +```html +
+ + +
+ + + +
+
+
+``` + +See [here](outline/) for more information on using the outline sub-component. + ### Text Field Boxes ```html @@ -179,8 +196,7 @@ behave normally.
``` -Note that Text field boxes support all of the same features as normal text-fields, including helper -text, validation, and dense UI. +Note that both Text Field Boxes and Outlined Text Fields support all of the same features as normal Text Fields, including helper text, validation, and dense UI. #### CSS-only text field boxes @@ -296,6 +312,10 @@ String setter. Proxies to the foundation's `setHelperTextContent` method when se `MDCRipple` instance. Set to the `MDCRipple` instance for the root element that `MDCTextField` initializes when given an `mdc-text-field--box` root element. Otherwise, the field is set to `null`. +##### MDCTextField.layout() + +Recomputes the outline SVG path for the outline element, and recomputes all dimensions and positions for the ripple element. + ### Using the foundation class Because MDC Text Field is a feature-rich and relatively complex component, its adapter is a bit more @@ -312,8 +332,10 @@ complicated. | registerBottomLineEventHandler(evtType: string, handler: EventListener) => void | Registers an event listener on the bottom line element for a given event | | deregisterBottomLineEventHandler(evtType: string, handler: EventListener) => void | Deregisters an event listener on the bottom line element for a given event | | getNativeInput() => {value: string, disabled: boolean, badInput: boolean, checkValidity: () => boolean}? | Returns an object representing the native text input element, with a similar API shape. The object returned should include the `value`, `disabled` and `badInput` properties, as well as the `checkValidity()` function. We _never_ alter the value within our code, however we _do_ update the disabled property, so if you choose to duck-type the return value for this method in your implementation it's important to keep this in mind. Also note that this method can return null, which the foundation will handle gracefully. | +| getIdleOutlineStyleValue(propertyName: string) => string | Returns the idle outline element's computed style value of the given css property `propertyName`. We achieve this via `getComputedStyle(...).getPropertyValue(propertyName)`.| +| isRtl() => boolean | Returns whether the direction of the root element is set to RTL. | -MDC Text Field has multiple optional sub-elements: bottom line and helper text. The foundations of these sub-elements must be passed in as constructor arguments for the `MDCTextField` foundation. Since the `MDCTextField` component takes care of creating its foundation, we need to pass sub-element foundations through the `MDCTextField` component. This is typically done in the component's implementation of `getDefaultFoundation()`. +MDC Text Field has multiple optional sub-elements: bottom line, helper text, and outline. The foundations of these sub-elements must be passed in as constructor arguments for the `MDCTextField` foundation. Since the `MDCTextField` component takes care of creating its foundation, we need to pass sub-element foundations through the `MDCTextField` component. This is typically done in the component's implementation of `getDefaultFoundation()`. #### The full foundation API @@ -350,6 +372,10 @@ finish. Expects a transition-end event. Sets the content of the helper text, if it exists. +##### MDCTextFieldFoundation.updateOutline() + +Updates the focus outline for outlined text fields. + ### Theming MDC Text Field components use the configured theme's primary color for its underline and label text diff --git a/packages/mdc-textfield/_mixins.scss b/packages/mdc-textfield/_mixins.scss index beae6fb73ca..7c399f054a7 100644 --- a/packages/mdc-textfield/_mixins.scss +++ b/packages/mdc-textfield/_mixins.scss @@ -14,6 +14,10 @@ // limitations under the License. // +@mixin mdc-text-field-outlined-corner-radius($radius) { + border-radius: $radius; +} + @mixin mdc-text-field-box-corner-radius($radius) { border-radius: $radius $radius 0 0; } @@ -45,7 +49,7 @@ 33% { animation-timing-function: cubic-bezier(.5, 0, .701732, .495819); - transform: translateX(10px) translateY(-#{$positionY}) scale(.75, .75); + transform: translateX(5px) translateY(-#{$positionY}) scale(.75, .75); } 66% { diff --git a/packages/mdc-textfield/_variables.scss b/packages/mdc-textfield/_variables.scss index ca1d0db2101..08396d25be2 100644 --- a/packages/mdc-textfield/_variables.scss +++ b/packages/mdc-textfield/_variables.scss @@ -43,6 +43,10 @@ $mdc-text-field-box-disabled-background: rgba(black, .02); $mdc-text-field-box-disabled-background-dark: rgba(white, .05); $mdc-text-field-box-secondary-text: rgba(black, .6); +$mdc-text-field-outlined-idle-border: rgba(black, .12); +$mdc-text-field-outlined-disabled-border: rgba(black, .06); +$mdc-text-field-outlined-hover-border: rgba(black, .87); + $mdc-textarea-border-on-light: rgba(black, .73); $mdc-textarea-border-on-dark: rgba(white, 1); $mdc-textarea-light-background: rgba(white, 1); diff --git a/packages/mdc-textfield/adapter.js b/packages/mdc-textfield/adapter.js index 306d8902ef3..42d70914eae 100644 --- a/packages/mdc-textfield/adapter.js +++ b/packages/mdc-textfield/adapter.js @@ -119,6 +119,20 @@ class MDCTextFieldAdapter { * @return {?Element|?NativeInputType} */ getNativeInput() {} + + /** + * Returns the idle outline element's computed style value of the given css property `propertyName`. + * We achieve this via `getComputedStyle(...).getPropertyValue(propertyName)`. + * @param {string} propertyName + * @return {string} + */ + getIdleOutlineStyleValue(propertyName) {} + + /** + * Returns true if the direction of the root element is set to RTL. + * @return {boolean} + */ + isRtl() {} } export {MDCTextFieldAdapter, NativeInputType, FoundationMapType}; diff --git a/packages/mdc-textfield/constants.js b/packages/mdc-textfield/constants.js index 001770187a0..26ac5ac0548 100644 --- a/packages/mdc-textfield/constants.js +++ b/packages/mdc-textfield/constants.js @@ -21,6 +21,8 @@ const strings = { INPUT_SELECTOR: '.mdc-text-field__input', LABEL_SELECTOR: '.mdc-text-field__label', ICON_SELECTOR: '.mdc-text-field__icon', + IDLE_OUTLINE_SELECTOR: '.mdc-text-field__idle-outline', + OUTLINE_SELECTOR: '.mdc-text-field__outline', BOTTOM_LINE_SELECTOR: '.mdc-text-field__bottom-line', }; diff --git a/packages/mdc-textfield/foundation.js b/packages/mdc-textfield/foundation.js index 435b7576b3a..965bf3bf48d 100644 --- a/packages/mdc-textfield/foundation.js +++ b/packages/mdc-textfield/foundation.js @@ -22,6 +22,7 @@ import MDCTextFieldBottomLineFoundation from './bottom-line/foundation'; import MDCTextFieldHelperTextFoundation from './helper-text/foundation'; import MDCTextFieldIconFoundation from './icon/foundation'; import MDCTextFieldLabelFoundation from './label/foundation'; +import MDCTextFieldOutlineFoundation from './outline/foundation'; /* eslint-enable no-unused-vars */ import {cssClasses, strings} from './constants'; @@ -57,6 +58,8 @@ class MDCTextFieldFoundation extends MDCFoundation { registerBottomLineEventHandler: () => {}, deregisterBottomLineEventHandler: () => {}, getNativeInput: () => {}, + getIdleOutlineStyleValue: () => {}, + isRtl: () => {}, }); } @@ -76,6 +79,8 @@ class MDCTextFieldFoundation extends MDCFoundation { this.icon_ = foundationMap.icon; /** @type {!MDCTextFieldLabelFoundation|undefined} */ this.label_ = foundationMap.label; + /** @type {!MDCTextFieldOutlineFoundation|undefined} */ + this.outline_ = foundationMap.outline; /** @private {boolean} */ this.isFocused_ = false; @@ -142,6 +147,22 @@ class MDCTextFieldFoundation extends MDCFoundation { this.receivedUserInput_ = true; } + /** + * Updates the focus outline for outlined text fields. + */ + updateOutline() { + if (!this.outline_ || !this.label_) { + return; + } + const labelWidth = this.label_.getFloatingWidth(); + // Fall back to reading a specific corner's style because Firefox doesn't report the style on border-radius. + const radiusStyleValue = this.adapter_.getIdleOutlineStyleValue('border-radius') || + this.adapter_.getIdleOutlineStyleValue('border-top-left-radius'); + const radius = parseFloat(radiusStyleValue); + const isRtl = this.adapter_.isRtl(); + this.outline_.updateSvgPath(labelWidth, radius, isRtl); + } + /** * Activates the text field focus state. */ @@ -151,6 +172,9 @@ class MDCTextFieldFoundation extends MDCFoundation { if (this.bottomLine_) { this.bottomLine_.activate(); } + if (this.outline_) { + this.updateOutline(); + } if (this.label_) { this.label_.floatAbove(); } diff --git a/packages/mdc-textfield/index.js b/packages/mdc-textfield/index.js index 6f8e847379f..ad7ca0a2c19 100644 --- a/packages/mdc-textfield/index.js +++ b/packages/mdc-textfield/index.js @@ -28,6 +28,7 @@ import {MDCTextFieldBottomLine, MDCTextFieldBottomLineFoundation} from './bottom import {MDCTextFieldHelperText, MDCTextFieldHelperTextFoundation} from './helper-text'; import {MDCTextFieldIcon, MDCTextFieldIconFoundation} from './icon'; import {MDCTextFieldLabel, MDCTextFieldLabelFoundation} from './label'; +import {MDCTextFieldOutline, MDCTextFieldOutlineFoundation} from './outline'; /* eslint-enable no-unused-vars */ /** @@ -42,8 +43,6 @@ class MDCTextField extends MDCComponent { super(...args); /** @private {?Element} */ this.input_; - /** @private {?MDCTextFieldLabel} */ - this.label_; /** @type {?MDCRipple} */ this.ripple; /** @private {?MDCTextFieldBottomLine} */ @@ -52,6 +51,10 @@ class MDCTextField extends MDCComponent { this.helperText_; /** @private {?MDCTextFieldIcon} */ this.icon_; + /** @private {?MDCTextFieldLabel} */ + this.label_; + /** @private {?MDCTextFieldOutline} */ + this.outline_; } /** @@ -73,13 +76,16 @@ class MDCTextField extends MDCComponent { * creates a new MDCTextFieldIcon. * @param {(function(!Element): !MDCTextFieldLabel)=} labelFactory A function which * creates a new MDCTextFieldLabel. + * @param {(function(!Element): !MDCTextFieldOutline)=} outlineFactory A function which + * creates a new MDCTextFieldOutline. */ initialize( rippleFactory = (el, foundation) => new MDCRipple(el, foundation), bottomLineFactory = (el) => new MDCTextFieldBottomLine(el), helperTextFactory = (el) => new MDCTextFieldHelperText(el), iconFactory = (el) => new MDCTextFieldIcon(el), - labelFactory = (el) => new MDCTextFieldLabel(el)) { + labelFactory = (el) => new MDCTextFieldLabel(el), + outlineFactory = (el) => new MDCTextFieldOutline(el)) { this.input_ = this.root_.querySelector(strings.INPUT_SELECTOR); const labelElement = this.root_.querySelector(strings.LABEL_SELECTOR); if (labelElement) { @@ -95,11 +101,15 @@ class MDCTextField extends MDCComponent { }); const foundation = new MDCRippleFoundation(adapter); this.ripple = rippleFactory(this.root_, foundation); - }; + } const bottomLineElement = this.root_.querySelector(strings.BOTTOM_LINE_SELECTOR); if (bottomLineElement) { this.bottomLine_ = bottomLineFactory(bottomLineElement); } + const outlineElement = this.root_.querySelector(strings.OUTLINE_SELECTOR); + if (outlineElement) { + this.outline_ = outlineFactory(outlineElement); + } if (this.input_.hasAttribute(strings.ARIA_CONTROLS)) { const helperTextElement = document.getElementById(this.input_.getAttribute(strings.ARIA_CONTROLS)); if (helperTextElement) { @@ -122,11 +132,14 @@ class MDCTextField extends MDCComponent { if (this.helperText_) { this.helperText_.destroy(); } + if (this.icon_) { + this.icon_.destroy(); + } if (this.label_) { this.label_.destroy(); } - if (this.icon_) { - this.icon_.destroy(); + if (this.outline_) { + this.outline_.destroy(); } super.destroy(); } @@ -168,6 +181,19 @@ class MDCTextField extends MDCComponent { this.foundation_.setHelperTextContent(content); } + /** + * Recomputes the outline SVG path for the outline element, and recomputes + * all dimensions and positions for the ripple element. + */ + layout() { + if (this.outline_) { + this.foundation_.updateOutline(); + } + if (this.ripple) { + this.ripple.layout(); + } + } + /** * @return {!MDCTextFieldFoundation} */ @@ -188,6 +214,13 @@ class MDCTextField extends MDCComponent { this.bottomLine_.unlisten(evtType, handler); } }, + getIdleOutlineStyleValue: (propertyName) => { + const idleOutlineElement = this.root_.querySelector(strings.IDLE_OUTLINE_SELECTOR); + if (idleOutlineElement) { + return window.getComputedStyle(idleOutlineElement).getPropertyValue(propertyName); + } + }, + isRtl: () => window.getComputedStyle(this.root_).getPropertyValue('direction') === 'rtl', }, this.getInputAdapterMethods_())), this.getFoundationMap_()); @@ -218,6 +251,7 @@ class MDCTextField extends MDCComponent { helperText: this.helperText_ ? this.helperText_.foundation : undefined, icon: this.icon_ ? this.icon_.foundation : undefined, label: this.label_ ? this.label_.foundation : undefined, + outline: this.outline_ ? this.outline_.foundation : undefined, }; } } @@ -226,4 +260,5 @@ export {MDCTextField, MDCTextFieldFoundation, MDCTextFieldBottomLine, MDCTextFieldBottomLineFoundation, MDCTextFieldHelperText, MDCTextFieldHelperTextFoundation, MDCTextFieldIcon, MDCTextFieldIconFoundation, - MDCTextFieldLabel, MDCTextFieldLabelFoundation}; + MDCTextFieldLabel, MDCTextFieldLabelFoundation, + MDCTextFieldOutline, MDCTextFieldOutlineFoundation}; diff --git a/packages/mdc-textfield/label/README.md b/packages/mdc-textfield/label/README.md index ba8db157b7d..641e7c82e34 100644 --- a/packages/mdc-textfield/label/README.md +++ b/packages/mdc-textfield/label/README.md @@ -34,9 +34,14 @@ Method Signature | Description --- | --- addClass(className: string) => void | Adds a class to the label element removeClass(className: string) => void | Removes a class from the label element +getWidth() => number | Returns the width of the label element #### The full foundation API +##### MDCTextFieldLabelFoundation.getFloatingWidth() + +Returns the width of the label element when it floats above the text field. + ##### MDCTextFieldLabelFoundation.floatAbove() Makes the label float above the text field. diff --git a/packages/mdc-textfield/label/adapter.js b/packages/mdc-textfield/label/adapter.js index ea01a7fbfc9..d735245cdb0 100644 --- a/packages/mdc-textfield/label/adapter.js +++ b/packages/mdc-textfield/label/adapter.js @@ -39,6 +39,12 @@ class MDCTextFieldLabelAdapter { * @param {string} className */ removeClass(className) {} + + /** + * Returns the width of the label element. + * @return {number} + */ + getWidth() {} } export default MDCTextFieldLabelAdapter; diff --git a/packages/mdc-textfield/label/foundation.js b/packages/mdc-textfield/label/foundation.js index aae2942a361..be8c26d9aa4 100644 --- a/packages/mdc-textfield/label/foundation.js +++ b/packages/mdc-textfield/label/foundation.js @@ -39,6 +39,7 @@ class MDCTextFieldLabelFoundation extends MDCFoundation { return /** @type {!MDCTextFieldLabelAdapter} */ ({ addClass: () => {}, removeClass: () => {}, + getWidth: () => {}, }); } @@ -49,6 +50,15 @@ class MDCTextFieldLabelFoundation extends MDCFoundation { super(Object.assign(MDCTextFieldLabelFoundation.defaultAdapter, adapter)); } + /** + * Returns the width of the label element when it floats above the text field. + * @return {number} + */ + getFloatingWidth() { + // The label is scaled 75% when it floats above the text field. + return this.adapter_.getWidth() * 0.75; + } + /** Makes the label float above the text field. */ floatAbove() { this.adapter_.addClass(cssClasses.LABEL_FLOAT_ABOVE); diff --git a/packages/mdc-textfield/label/index.js b/packages/mdc-textfield/label/index.js index 69ed8590b27..5eab868c811 100644 --- a/packages/mdc-textfield/label/index.js +++ b/packages/mdc-textfield/label/index.js @@ -47,6 +47,7 @@ class MDCTextFieldLabel extends MDCComponent { return new MDCTextFieldLabelFoundation(/** @type {!MDCTextFieldLabelAdapter} */ (Object.assign({ addClass: (className) => this.root_.classList.add(className), removeClass: (className) => this.root_.classList.remove(className), + getWidth: () => this.root_.offsetWidth, }))); } } diff --git a/packages/mdc-textfield/mdc-text-field.scss b/packages/mdc-textfield/mdc-text-field.scss index e75e92ab9f4..9396e2846e3 100644 --- a/packages/mdc-textfield/mdc-text-field.scss +++ b/packages/mdc-textfield/mdc-text-field.scss @@ -29,8 +29,10 @@ @import "./helper-text/mdc-text-field-helper-text"; @import "./icon/mdc-text-field-icon"; @import "./label/mdc-text-field-label"; +@import "./outline/mdc-text-field-outline"; @include mdc-text-field-invalid-label-shake-keyframes_(standard, 100%); @include mdc-text-field-invalid-label-shake-keyframes_(box, 50%); +@include mdc-text-field-invalid-label-shake_keyframes_(outlined, 130%); // postcss-bem-linter: define text-field @@ -116,6 +118,87 @@ // stylelint-enable plugin/selector-bem-pattern } +.mdc-text-field--outlined { + height: 56px; + border: none; + + // stylelint-disable plugin/selector-bem-pattern + + .mdc-text-field__input { + display: flex; + height: 30px; + padding: 12px; + border: none; + background-color: transparent; + z-index: 3; + + &:hover ~ .mdc-text-field__idle-outline { + border: 1px solid $mdc-text-field-outlined-hover-border; + } + } + + .mdc-text-field__label { + @include mdc-rtl-reflexive-position(left, 16px); + + position: absolute; + bottom: 20px; + left: 16px; + transition: transform 260ms ease; + z-index: 2; + + &--float-above { + @include mdc-theme-prop(color, primary); + + transform: scale(.75) translateY(-170%); + + &.mdc-text-field__label--shake { + animation: invalid-shake-float-above-outlined 250ms 1; + } + } + } + + &.mdc-text-field--focused .mdc-text-field__idle-outline, + .mdc-text-field__label--float-above ~ .mdc-text-field__idle-outline { + opacity: 0; + } + + &.mdc-text-field--focused .mdc-text-field__outline, + .mdc-text-field__label--float-above ~ .mdc-text-field__outline { + opacity: 1; + } + + &:not(.mdc-text-field--focused) .mdc-text-field__outline-path { + stroke-width: 1px; + } + + &.mdc-text-field--disabled { + @include mdc-theme-prop(color, $mdc-text-field-light-placeholder); + + .mdc-text-field__input { + border-bottom: none; + } + + .mdc-text-field__outline-path { + stroke: $mdc-text-field-outlined-disabled-border; + stroke-width: 1px; + } + + .mdc-text-field__idle-outline { + border-color: $mdc-text-field-outlined-disabled-border; + } + + .mdc-text-field__label { + bottom: 20px; + } + + .mdc-text-field__icon { + @include mdc-theme-prop(color, $mdc-text-field-disabled-icon-on-light); + } + } + + // stylelint-enable plugin/selector-bem-pattern +} + .mdc-text-field--box { @include mdc-ripple-surface; @include mdc-states(text-primary-on-light, $has-nested-focusable-element: true); @@ -285,7 +368,8 @@ box-sizing: border-box; margin-top: 16px; - &:not(.mdc-text-field--textarea) { + // stylelint-disable-next-line selector-max-specificity + &:not(.mdc-text-field--textarea):not(.mdc-text-field--outlined) { height: 48px; } @@ -306,6 +390,18 @@ .mdc-text-field__bottom-line { background-color: $mdc-text-field-error-on-light; } + + .mdc-text-field__idle-outline { + border-color: $mdc-text-field-error-on-light; + } + + .mdc-text-field__input:hover ~ .mdc-text-field__idle-outline { + border-color: $mdc-text-field-error-on-light; + } + + .mdc-text-field__outline .mdc-text-field__outline-path { + stroke: $mdc-text-field-error-on-light; + } } // stylelint-disable plugin/selector-bem-pattern diff --git a/packages/mdc-textfield/outline/README.md b/packages/mdc-textfield/outline/README.md new file mode 100644 index 00000000000..63437a274eb --- /dev/null +++ b/packages/mdc-textfield/outline/README.md @@ -0,0 +1,42 @@ + + +# Text Field Outline + +The outline is a border around all sides of the text field. This is used for the Outlined variation of Text Fields. + +## Design & API Documentation + + + +## Usage + +#### MDCTextFieldOutline API + +##### MDCTextFieldOutline.foundation + +MDCTextFieldOutlineFoundation. This allows the parent MDCTextField component to access the public methods on the MDCTextFieldOutlineFoundation class. + +### Using the foundation class + +Method Signature | Description +--- | --- +getWidth() => number | Returns the width of the outline element +getHeight() => number | Returns the height of the outline element +setOutlinePathAttr(value: string) => void | Sets the "d" attribute of the outline element's SVG path + +#### The full foundation API + +##### MDCTextFieldOutlineFoundation.updateSvgPath(width: number, height: number, labelWidth: number, radius: number, isRtl: boolean) + +Updates the SVG path of the focus outline element based on the given width and height of the text field element, the width of the label element, the corner radius, and the RTL context. diff --git a/packages/mdc-textfield/outline/adapter.js b/packages/mdc-textfield/outline/adapter.js new file mode 100644 index 00000000000..b4a7e37a652 --- /dev/null +++ b/packages/mdc-textfield/outline/adapter.js @@ -0,0 +1,50 @@ +/** + * @license + * Copyright 2017 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 Text Field Outline. + * + * Defines the shape of the adapter expected by the foundation. Implement this + * adapter to integrate the Text Field outline into your framework. See + * https://github.com/material-components/material-components-web/blob/master/docs/authoring-components.md + * for more information. + * + * @record + */ +class MDCTextFieldOutlineAdapter { + /** + * Returns the width of the root element. + * @return {number} + */ + getWidth() {} + + /** + * Returns the height of the root element. + * @return {number} + */ + getHeight() {} + + /** + * Sets the "d" attribute of the outline element's SVG path. + * @param {string} value + */ + setOutlinePathAttr(value) {} +} + +export default MDCTextFieldOutlineAdapter; diff --git a/packages/mdc-textfield/outline/constants.js b/packages/mdc-textfield/outline/constants.js new file mode 100644 index 00000000000..fc5ceda157d --- /dev/null +++ b/packages/mdc-textfield/outline/constants.js @@ -0,0 +1,23 @@ +/** + * @license + * Copyright 2016 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 = { + PATH_SELECTOR: '.mdc-text-field__outline-path', +}; + +export {strings}; diff --git a/packages/mdc-textfield/outline/foundation.js b/packages/mdc-textfield/outline/foundation.js new file mode 100644 index 00000000000..e1d019788fa --- /dev/null +++ b/packages/mdc-textfield/outline/foundation.js @@ -0,0 +1,88 @@ +/** + * @license + * Copyright 2017 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 MDCTextFieldOutlineAdapter from './adapter'; +import {strings} from './constants'; + +/** + * @extends {MDCFoundation} + * @final + */ +class MDCTextFieldOutlineFoundation extends MDCFoundation { + /** @return enum {string} */ + static get strings() { + return strings; + } + + /** + * {@see MDCTextFieldOutlineAdapter} for typing information on parameters and return + * types. + * @return {!MDCTextFieldOutlineAdapter} + */ + static get defaultAdapter() { + return /** @type {!MDCTextFieldOutlineAdapter} */ ({ + getWidth: () => {}, + getHeight: () => {}, + setOutlinePathAttr: () => {}, + }); + } + + /** + * @param {!MDCTextFieldOutlineAdapter=} adapter + */ + constructor(adapter = /** @type {!MDCTextFieldOutlineAdapter} */ ({})) { + super(Object.assign(MDCTextFieldOutlineFoundation.defaultAdapter, adapter)); + } + + /** + * Updates the SVG path of the focus outline element based on the given width of the + * label element, the corner radius, and the RTL context. + * @param {number} labelWidth + * @param {number} radius + * @param {boolean=} isRtl + */ + updateSvgPath(labelWidth, radius, isRtl = false) { + const width = this.adapter_.getWidth() + 2; + const height = this.adapter_.getHeight() + 2; + // The right, bottom, and left sides of the outline follow the same SVG path. + const pathMiddle = 'a' + radius + ',' + radius + ' 0 0 1 ' + radius + ',' + radius + + 'v' + (height - 2 * (radius + 2.1)) + + 'a' + radius + ',' + radius + ' 0 0 1 ' + -radius + ',' + radius + + 'h' + (-width + 2 * (radius + 1.7)) + + 'a' + radius + ',' + radius + ' 0 0 1 ' + -radius + ',' + -radius + + 'v' + (-height + 2 * (radius + 2.1)) + + 'a' + radius + ',' + radius + ' 0 0 1 ' + radius + ',' + -radius; + + let path; + if (!isRtl) { + path = 'M' + (radius + 2.1 + Math.abs(10 - radius) + labelWidth + 8) + ',' + 1 + + 'h' + (width - (2 * (radius + 2.1)) - labelWidth - 8.5 - Math.abs(10 - radius)) + + pathMiddle + + 'h' + Math.abs(10 - radius); + } else { + path = 'M' + (width - radius - 2.1 - Math.abs(10 - radius)) + ',' + 1 + + 'h' + Math.abs(10 - radius) + + pathMiddle + + 'h' + (width - (2 * (radius + 2.1)) - labelWidth - 8.5 - Math.abs(10 - radius)); + } + + this.adapter_.setOutlinePathAttr(path); + } +} + +export default MDCTextFieldOutlineFoundation; diff --git a/packages/mdc-textfield/outline/index.js b/packages/mdc-textfield/outline/index.js new file mode 100644 index 00000000000..0a02bce7129 --- /dev/null +++ b/packages/mdc-textfield/outline/index.js @@ -0,0 +1,59 @@ +/** + * @license + * Copyright 2017 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 {strings} from './constants'; +import MDCTextFieldOutlineAdapter from './adapter'; +import MDCTextFieldOutlineFoundation from './foundation'; + +/** + * @extends {MDCComponent} + * @final + */ +class MDCTextFieldOutline extends MDCComponent { + /** + * @param {!Element} root + * @return {!MDCTextFieldOutline} + */ + static attachTo(root) { + return new MDCTextFieldOutline(root); + } + + /** + * @return {!MDCTextFieldOutlineFoundation} + */ + get foundation() { + return this.foundation_; + } + + /** + * @return {!MDCTextFieldOutlineFoundation} + */ + getDefaultFoundation() { + return new MDCTextFieldOutlineFoundation(/** @type {!MDCTextFieldOutlineAdapter} */ (Object.assign({ + getWidth: () => this.root_.offsetWidth, + getHeight: () => this.root_.offsetHeight, + setOutlinePathAttr: (value) => { + const path = this.root_.querySelector(strings.PATH_SELECTOR); + path.setAttribute('d', value); + }, + }))); + } +} + +export {MDCTextFieldOutline, MDCTextFieldOutlineFoundation}; diff --git a/packages/mdc-textfield/outline/mdc-text-field-outline.scss b/packages/mdc-textfield/outline/mdc-text-field-outline.scss new file mode 100644 index 00000000000..257d859d8b0 --- /dev/null +++ b/packages/mdc-textfield/outline/mdc-text-field-outline.scss @@ -0,0 +1,61 @@ +// +// Copyright 2017 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"; +@import "../variables"; +@import "@material/theme/mixins"; + +.mdc-text-field__idle-outline { + @include mdc-text-field-outlined-corner-radius($mdc-text-field-border-radius); + + position: absolute; + top: 0; + left: 0; + width: calc(100% - 4px); + height: calc(100% - 4px); + transition: opacity 100ms ease; + border: 1px solid $mdc-text-field-outlined-idle-border; + opacity: 1; + z-index: 2; +} + +.mdc-text-field__outline { + @include mdc-theme-prop(color, primary); + @include mdc-text-field-outlined-corner-radius($mdc-text-field-border-radius); + + position: absolute; + top: 0; + left: 0; + width: calc(100% - 1px); + height: calc(100% - 2px); + transition: mdc-text-field-transition(opacity); + opacity: 0; + z-index: 2; + + svg { + position: absolute; + width: 100%; + height: 100%; + + .mdc-text-field__outline-path { + @include mdc-theme-prop(stroke, primary); + + stroke-width: 2px; + transition: mdc-text-field-transition(stroke-width), mdc-text-field-transition(opacity); + fill: transparent; + } + } +} diff --git a/test/unit/mdc-textfield/foundation.test.js b/test/unit/mdc-textfield/foundation.test.js index 74bc17cff3b..a30b517b181 100644 --- a/test/unit/mdc-textfield/foundation.test.js +++ b/test/unit/mdc-textfield/foundation.test.js @@ -39,7 +39,7 @@ test('defaultAdapter returns a complete adapter implementation', () => { 'registerTextFieldInteractionHandler', 'deregisterTextFieldInteractionHandler', 'registerInputInteractionHandler', 'deregisterInputInteractionHandler', 'registerBottomLineEventHandler', 'deregisterBottomLineEventHandler', - 'getNativeInput', + 'getNativeInput', 'getIdleOutlineStyleValue', 'isRtl', ]); }); @@ -63,18 +63,23 @@ const setupTest = () => { handleInteraction: () => {}, }); const label = td.object({ + getFloatingWidth: () => {}, floatAbove: () => {}, deactivateFocus: () => {}, setValidity: () => {}, }); + const outline = td.object({ + updateSvgPath: () => {}, + }); const foundationMap = { bottomLine: bottomLine, helperText: helperText, icon: icon, label: label, + outline: outline, }; const foundation = new MDCTextFieldFoundation(mockAdapter, foundationMap); - return {foundation, mockAdapter, bottomLine, helperText, icon, label}; + return {foundation, mockAdapter, bottomLine, helperText, icon, label, outline}; }; test('#constructor sets disabled to false', () => { @@ -210,6 +215,16 @@ test('#setHelperTextContent sets the content of the helper text element', () => td.verify(helperText.setContent('foo')); }); +test('#updateOutline updates the SVG path of the outline element', () => { + const {foundation, mockAdapter, label, outline} = setupTest(); + td.when(label.getFloatingWidth()).thenReturn(30); + td.when(mockAdapter.getIdleOutlineStyleValue('border-radius')).thenReturn('8px'); + td.when(mockAdapter.isRtl()).thenReturn(false); + + foundation.updateOutline(); + td.verify(outline.updateSvgPath(30, 8, false)); +}); + test('on input floats label if input event occurs without any other events', () => { const {foundation, mockAdapter, label} = setupTest(); let input; @@ -262,6 +277,18 @@ test('on focus adds mdc-text-field--focused class', () => { td.verify(mockAdapter.addClass(cssClasses.FOCUSED)); }); +test('on focus activates bottom line', () => { + const {foundation, mockAdapter, bottomLine} = setupTest(); + let focus; + td.when(mockAdapter.registerInputInteractionHandler('focus', td.matchers.isA(Function))) + .thenDo((evtType, handler) => { + focus = handler; + }); + foundation.init(); + focus(); + td.verify(bottomLine.activate()); +}); + test('on focus floats label', () => { const {foundation, mockAdapter, label} = setupTest(); let focus; @@ -367,6 +394,22 @@ test('on blur handles getNativeInput() not returning anything gracefully', () => assert.doesNotThrow(blur); }); +test('on keydown sets receivedUserInput to true when input is enabled', () => { + const {foundation, mockAdapter} = setupTest(); + let keydown; + td.when(mockAdapter.registerTextFieldInteractionHandler('keydown', td.matchers.isA(Function))) + .thenDo((evtType, handler) => { + keydown = handler; + }); + td.when(mockAdapter.getNativeInput()).thenReturn({ + disabled: false, + }); + foundation.init(); + assert.equal(foundation.receivedUserInput_, false); + keydown(); + assert.equal(foundation.receivedUserInput_, true); +}); + test('on transition end deactivates the bottom line if this.isFocused_ is false', () => { const {foundation, mockAdapter, bottomLine} = setupTest(); const mockEvt = { diff --git a/test/unit/mdc-textfield/mdc-text-field-label-foundation.test.js b/test/unit/mdc-textfield/mdc-text-field-label-foundation.test.js index 62233565d75..9bba6cd6c0d 100644 --- a/test/unit/mdc-textfield/mdc-text-field-label-foundation.test.js +++ b/test/unit/mdc-textfield/mdc-text-field-label-foundation.test.js @@ -31,12 +31,19 @@ test('exports cssClasses', () => { test('defaultAdapter returns a complete adapter implementation', () => { verifyDefaultAdapter(MDCTextFieldLabelFoundation, [ - 'addClass', 'removeClass', + 'addClass', 'removeClass', 'getWidth', ]); }); const setupTest = () => setupFoundationTest(MDCTextFieldLabelFoundation); +test('#getFloatingWidth returns the width of the label element scaled by 75%', () => { + const {foundation, mockAdapter} = setupTest(); + const width = 100; + td.when(mockAdapter.getWidth()).thenReturn(width); + assert.equal(foundation.getFloatingWidth(), width * 0.75); +}); + test('#floatAbove adds mdc-text-field__label--float-above class', () => { const {foundation, mockAdapter} = setupTest(); foundation.floatAbove(); @@ -61,3 +68,9 @@ test('#setValidity adds mdc-text-field__label--shake class if isValid is false', foundation.setValidity(false); td.verify(mockAdapter.addClass(cssClasses.LABEL_SHAKE)); }); + +test('#setValidity does nothing if isValid is true', () => { + const {foundation, mockAdapter} = setupTest(); + foundation.setValidity(true); + td.verify(mockAdapter.addClass(cssClasses.LABEL_SHAKE), {times: 0}); +}); diff --git a/test/unit/mdc-textfield/mdc-text-field-label.test.js b/test/unit/mdc-textfield/mdc-text-field-label.test.js index aee9b0ea93c..6ea3624e6a4 100644 --- a/test/unit/mdc-textfield/mdc-text-field-label.test.js +++ b/test/unit/mdc-textfield/mdc-text-field-label.test.js @@ -47,3 +47,8 @@ test('#adapter.removeClass removes a class from the element', () => { component.getDefaultFoundation().adapter_.removeClass('foo'); assert.isFalse(root.classList.contains('foo')); }); + +test('#adapter.getWidth returns the width of the label element', () => { + const {root, component} = setupTest(); + assert.equal(component.getDefaultFoundation().adapter_.getWidth(), root.offsetWidth); +}); diff --git a/test/unit/mdc-textfield/mdc-text-field-outline.foundation.test.js b/test/unit/mdc-textfield/mdc-text-field-outline.foundation.test.js new file mode 100644 index 00000000000..fbc78b9faf3 --- /dev/null +++ b/test/unit/mdc-textfield/mdc-text-field-outline.foundation.test.js @@ -0,0 +1,45 @@ +/** + * Copyright 2017 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 {assert} from 'chai'; +import td from 'testdouble'; + +import {verifyDefaultAdapter} from '../helpers/foundation'; +import {setupFoundationTest} from '../helpers/setup'; +import MDCTextFieldOutlineFoundation from '../../../packages/mdc-textfield/outline/foundation'; + +suite('MDCTextFieldOutlineFoundation'); + +test('exports strings', () => { + assert.isOk('strings' in MDCTextFieldOutlineFoundation); +}); + +test('defaultAdapter returns a complete adapter implementation', () => { + verifyDefaultAdapter(MDCTextFieldOutlineFoundation, [ + 'getWidth', 'getHeight', 'setOutlinePathAttr', + ]); +}); + +const setupTest = () => setupFoundationTest(MDCTextFieldOutlineFoundation); + +test('#updateSvgPath sets the path of the outline element', () => { + const {foundation, mockAdapter} = setupTest(); + const labelWidth = 30; + const radius = 8; + const isRtl = true; + foundation.updateSvgPath(labelWidth, radius, isRtl); + td.verify(mockAdapter.setOutlinePathAttr(td.matchers.anything())); +}); diff --git a/test/unit/mdc-textfield/mdc-text-field-outline.test.js b/test/unit/mdc-textfield/mdc-text-field-outline.test.js new file mode 100644 index 00000000000..c2bc18ce905 --- /dev/null +++ b/test/unit/mdc-textfield/mdc-text-field-outline.test.js @@ -0,0 +1,59 @@ +/** + * Copyright 2017 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 bel from 'bel'; +import {assert} from 'chai'; + +import {MDCTextFieldOutline} from '../../../packages/mdc-textfield/outline'; + +const getFixture = () => bel` +
+ + + +
+`; + +suite('MDCTextFieldOutline'); + +test('attachTo returns an MDCTextFieldOutline instance', () => { + assert.isOk(MDCTextFieldOutline.attachTo(getFixture()) instanceof MDCTextFieldOutline); +}); + +function setupTest() { + const root = getFixture(); + const component = new MDCTextFieldOutline(root); + return {root, component}; +} + +test('#adapter.getWidth returns the width of the element', () => { + const {root, component} = setupTest(); + const width = component.getDefaultFoundation().adapter_.getWidth(); + assert.equal(width, root.offsetWidth); +}); + +test('#adapter.getHeight returns the height of the element', () => { + const {root, component} = setupTest(); + const height = component.getDefaultFoundation().adapter_.getWidth(); + assert.equal(height, root.offsetHeight); +}); + +test('#adapter.setOutlinePathAttr sets the SVG path of the element', () => { + const {root, component} = setupTest(); + component.getDefaultFoundation().adapter_.setOutlinePathAttr('M 0 1'); + const path = root.querySelector('.mdc-text-field__outline-path'); + assert.equal(path.getAttribute('d'), 'M 0 1'); +}); diff --git a/test/unit/mdc-textfield/mdc-text-field.test.js b/test/unit/mdc-textfield/mdc-text-field.test.js index 48b44094d04..68b8b1172cb 100644 --- a/test/unit/mdc-textfield/mdc-text-field.test.js +++ b/test/unit/mdc-textfield/mdc-text-field.test.js @@ -20,8 +20,8 @@ import td from 'testdouble'; import {assert} from 'chai'; import {MDCRipple} from '../../../packages/mdc-ripple'; -import {MDCTextField, MDCTextFieldFoundation, MDCTextFieldBottomLine, - MDCTextFieldHelperText, MDCTextFieldIcon, MDCTextFieldLabel} from '../../../packages/mdc-textfield'; +import {MDCTextField, MDCTextFieldFoundation, MDCTextFieldBottomLine, MDCTextFieldHelperText, + MDCTextFieldIcon, MDCTextFieldLabel, MDCTextFieldOutline} from '../../../packages/mdc-textfield'; const {cssClasses} = MDCTextFieldFoundation; @@ -74,6 +74,12 @@ class FakeLabel { } } +class FakeOutline { + constructor() { + this.destroy = td.func('.destroy'); + } +} + test('#constructor when given a `mdc-text-field--box` element instantiates a ripple on the root element', () => { const root = getFixture(); root.classList.add(cssClasses.BOX); @@ -125,11 +131,28 @@ test('#constructor instantiates a label on the `.mdc-text-field__label` element assert.instanceOf(component.label_, MDCTextFieldLabel); }); +test('#constructor instantiates an outline on the `.mdc-text-field__outline` element if present', () => { + const root = getFixture(); + root.appendChild(bel`
`); + const component = new MDCTextField(root); + assert.instanceOf(component.outline_, MDCTextFieldOutline); +}); + +test('#constructor handles undefined optional sub-elements gracefully', () => { + const root = bel` +
+ +
+ `; + assert.doesNotThrow(() => new MDCTextField(root)); +}); + function setupTest(root = getFixture()) { const bottomLine = new FakeBottomLine(); const helperText = new FakeHelperText(); const icon = new FakeIcon(); const label = new FakeLabel(); + const outline = new FakeOutline(); const component = new MDCTextField( root, undefined, @@ -137,9 +160,10 @@ function setupTest(root = getFixture()) { () => bottomLine, () => helperText, () => icon, - () => label + () => label, + () => outline ); - return {root, component, bottomLine, helperText, icon, label}; + return {root, component, bottomLine, helperText, icon, label, outline}; } test('#destroy cleans up the ripple if present', () => { @@ -179,8 +203,21 @@ test('#destroy cleans up the label if present', () => { td.verify(label.destroy()); }); -test('#destroy accounts for ripple nullability', () => { - const component = new MDCTextField(getFixture()); +test('#destroy cleans up the outline if present', () => { + const root = getFixture(); + root.appendChild(bel`
`); + const {component, outline} = setupTest(root); + component.destroy(); + td.verify(outline.destroy()); +}); + +test('#destroy handles undefined optional sub-elements gracefully', () => { + const root = bel` +
+ +
+ `; + const component = new MDCTextField(root); assert.doesNotThrow(() => component.destroy()); }); @@ -222,6 +259,14 @@ test('set helperTextContent has no effect when no helper text element is present }); }); +test('#layout recomputes all dimensions and positions for the ripple element', () => { + const root = getFixture(); + root.classList.add(cssClasses.BOX); + const component = new MDCTextField(root, undefined, (el) => new FakeRipple(el)); + component.layout(); + td.verify(component.ripple.layout()); +}); + test('#adapter.registerBottomLineEventHandler adds event listener to bottom line', () => { const {component, bottomLine} = setupTest(); const handler = () => {}; @@ -293,3 +338,29 @@ test('#adapter.getNativeInput returns the component input element', () => { root.querySelector('.mdc-text-field__input') ); }); + +test('#adapter.getIdleOutlineStyleValue returns the value of the given property on the idle outline element', () => { + const root = getFixture(); + root.appendChild(bel`
`); + const idleOutline = root.querySelector('.mdc-text-field__idle-outline'); + idleOutline.style.width = '500px'; + + const component = new MDCTextField(root); + assert.equal( + component.getDefaultFoundation().adapter_.getIdleOutlineStyleValue('width'), + getComputedStyle(idleOutline).getPropertyValue('width') + ); +}); + +test('#adapter.isRtl returns true when the root element is in an RTL context' + + 'and false otherwise', () => { + const wrapper = bel`
`; + const {root, component} = setupTest(); + assert.isFalse(component.getDefaultFoundation().adapter_.isRtl()); + + wrapper.appendChild(root); + document.body.appendChild(wrapper); + assert.isTrue(component.getDefaultFoundation().adapter_.isRtl()); + + document.body.removeChild(wrapper); +});