@@ -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
+
+