Skip to content

Commit

Permalink
feat(textfield): add textarea type
Browse files Browse the repository at this point in the history
Fixes #4171, #1305, #2926

Changes:
- Added "textarea" type (matches native <textarea>, which has a "type" property like <input>)
- Moved pointer-events to the label wrapper so it wouldn't overlap content
PiperOrigin-RevId: 547614040
  • Loading branch information
asyncLiz authored and copybara-github committed Jul 13, 2023
1 parent 15df1d5 commit 70c76aa
Show file tree
Hide file tree
Showing 11 changed files with 233 additions and 107 deletions.
11 changes: 10 additions & 1 deletion field/lib/_content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,18 @@ $_enter-delay: $_label-duration - $_visible-duration;
// below.
color: currentColor;
font: var(--_content-type);
width: 100%;
}

.content ::slotted(:not(textarea)) {
padding-top: var(--_top-space);
padding-bottom: var(--_bottom-space);
width: 100%;
}

.content ::slotted(textarea) {
// Use margin for textareas since they will scroll over the label if not.
margin-top: var(--_top-space);
margin-bottom: var(--_bottom-space);
}

:hover .content {
Expand Down
14 changes: 11 additions & 3 deletions field/lib/_filled-field.scss
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,11 @@ $_md-sys-motion: tokens.md-sys-motion-values();
top: var(--_with-label-top-space);
}

.field:not(.with-start) .label-space {
.field:not(.with-start) .label-wrapper {
margin-inline-start: var(--_leading-space);
}

.field:not(.with-end) .label-space {
.field:not(.with-end) .label-wrapper {
margin-inline-end: var(--_trailing-space);
}

Expand Down Expand Up @@ -141,13 +141,21 @@ $_md-sys-motion: tokens.md-sys-motion-values();
padding-inline-end: var(--_trailing-space);
}

.field:not(.no-label) .content ::slotted(*) {
.field:not(.no-label) .content ::slotted(:not(textarea)) {
padding-bottom: var(--_with-label-bottom-space);
padding-top: calc(
var(--_with-label-top-space) + var(--_label-text-populated-line-height)
);
}

.field:not(.no-label) .content ::slotted(textarea) {
// Use margin for textareas since they will scroll over the label if not.
margin-bottom: var(--_with-label-bottom-space);
margin-top: calc(
var(--_with-label-top-space) + var(--_label-text-populated-line-height)
);
}

:hover .active-indicator::before {
border-bottom-color: var(--_hover-active-indicator-color);
border-bottom-width: var(--_hover-active-indicator-height);
Expand Down
21 changes: 13 additions & 8 deletions field/lib/_label.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,6 @@
color: var(--_label-text-color);
overflow: hidden;
max-width: 100%;
// The resting label at 100% height can block pointer events to the content
// if it's very long and spans the full width of the field. Additionally,
// selecting the label's text doesn't present a good UX, since the user
// selection should be re-focused to another element (such as the input)
// upon focusing. Finally, since the actual label elements are swapped, it
// is not easy to maintain the user's label text selection.
pointer-events: none;
// TODO: Check with design, should there be any transition from resting to
// floating when there is a mismatch between ellipsis, such as opacity
// transition?
Expand All @@ -26,6 +19,18 @@
width: min-content;
}

.label-wrapper {
inset: 0;
// The resting label at 100% height can block pointer events to the content
// if it's very long and spans the full width of the field. Additionally,
// selecting the label's text doesn't present a good UX, since the user
// selection should be re-focused to another element (such as the input)
// upon focusing. Finally, since the actual label elements are swapped, it
// is not easy to maintain the user's label text selection.
pointer-events: none;
position: absolute;
}

.label.resting {
position: absolute;
top: var(--_top-space);
Expand All @@ -48,7 +53,7 @@
// Labels need start/end padding when there isn't start/end content so they
// don't sit on the edge of the field. We use a wrapper element around the
// labels so as not to affect the dimensions used in the label keyframes.
.label-space {
.label-wrapper {
inset: 0;
position: absolute;
// Don't let setting text-align on the field change the label's alignment.
Expand Down
4 changes: 2 additions & 2 deletions field/lib/_outlined-field.scss
Original file line number Diff line number Diff line change
Expand Up @@ -237,15 +237,15 @@ $_md-sys-motion: tokens.md-sys-motion-values();
padding-inline-start: $start-space;
}

.field:not(.with-start) .label-space {
.field:not(.with-start) .label-wrapper {
margin-inline-start: $start-space;
}

.field:not(.with-end) .content ::slotted(*) {
padding-inline-end: $end-space;
}

.field:not(.with-end) .label-space {
.field:not(.with-end) .label-wrapper {
margin-inline-end: $end-space;
}

Expand Down
2 changes: 2 additions & 0 deletions field/lib/_shared.scss
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
border-radius: inherit;
display: flex;
flex: 1;
min-height: 100%;
min-width: min-content;
overflow: hidden;
position: relative;
Expand All @@ -52,6 +53,7 @@
.field,
.container-overflow,
.container {
height: 100%;
resize: inherit;
}

Expand Down
2 changes: 1 addition & 1 deletion field/lib/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export class Field extends LitElement implements SurfacePositionTarget {
<slot name="start"></slot>
</div>
<div class="middle">
<div class="label-space">
<div class="label-wrapper">
${restingLabel}
${outline ? nothing : floatingLabel}
</div>
Expand Down
1 change: 1 addition & 0 deletions textfield/demo/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {stories, StoryKnobs} from './stories.js';
const collection =
new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>('Textfield', [
new Knob('label', {ui: textInput(), defaultValue: 'Label'}),
new Knob('textarea', {ui: boolInput(), defaultValue: false}),
new Knob('disabled', {ui: boolInput(), defaultValue: false}),
new Knob('required', {ui: boolInput(), defaultValue: false}),
new Knob('prefixText', {ui: textInput(), defaultValue: ''}),
Expand Down
62 changes: 39 additions & 23 deletions textfield/demo/stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import '@material/web/textfield/outlined-text-field.js';

import {MaterialStoryInit} from './material-collection.js';
import {MdFilledTextField} from '@material/web/textfield/filled-text-field.js';
import {html, nothing} from 'lit';
import {css, html, nothing} from 'lit';

/** Knob types for Textfield stories. */
export interface StoryKnobs {
label: string;
textarea: boolean;
disabled: boolean;
required: boolean;
prefixText: string;
Expand All @@ -30,23 +31,36 @@ export interface StoryKnobs {
'trailing icon': boolean;
}

// Set min-height for resizable textareas
const styles = css`
[type=textarea] {
min-height: 56px;
}
[type=textarea][supporting-text] {
min-height: 76px;
}
`;

const filled: MaterialStoryInit<StoryKnobs> = {
name: '<md-filled-text-field>',
styles,
render(knobs) {
return html`
<md-filled-text-field
.label=${knobs.label}
label=${knobs.label || nothing}
?disabled=${knobs.disabled}
.prefixText=${knobs.prefixText}
.max=${knobs.max}
.maxLength=${knobs.maxLength}
.min=${knobs.min}
.minLength=${knobs.minLength}
.pattern=${knobs.pattern}
.required=${knobs.required}
.step=${knobs.step}
.suffixText=${knobs.suffixText}
.supportingText=${knobs.supportingText}
prefix-text=${knobs.prefixText || nothing}
max=${knobs.max || nothing}
max-length=${knobs.maxLength || nothing}
min=${knobs.min || nothing}
min-length=${knobs.minLength || nothing}
pattern=${knobs.pattern || nothing}
?required=${knobs.required}
step=${knobs.step || nothing}
suffix-text=${knobs.suffixText || nothing}
supporting-text=${knobs.supportingText || nothing}
type=${knobs.textarea ? 'textarea' : 'text'}
@change=${reportValidity}
>
${knobs['leading icon'] ? LEADING_ICON : nothing}
Expand All @@ -58,21 +72,23 @@ const filled: MaterialStoryInit<StoryKnobs> = {

const outlined: MaterialStoryInit<StoryKnobs> = {
name: '<md-outlined-text-field>',
styles,
render(knobs) {
return html`
<md-outlined-text-field
.label=${knobs.label}
label=${knobs.label || nothing}
?disabled=${knobs.disabled}
.prefixText=${knobs.prefixText}
.max=${knobs.max}
.maxLength=${knobs.maxLength}
.min=${knobs.min}
.minLength=${knobs.minLength}
.pattern=${knobs.pattern}
.required=${knobs.required}
.step=${knobs.step}
.suffixText=${knobs.suffixText}
.supportingText=${knobs.supportingText}
prefix-text=${knobs.prefixText || nothing}
max=${knobs.max || nothing}
max-length=${knobs.maxLength || nothing}
min=${knobs.min || nothing}
min-length=${knobs.minLength || nothing}
pattern=${knobs.pattern || nothing}
?required=${knobs.required}
step=${knobs.step || nothing}
suffix-text=${knobs.suffixText || nothing}
supporting-text=${knobs.supportingText || nothing}
type=${knobs.textarea ? 'textarea' : 'text'}
@change=${reportValidity}
>
${knobs['leading icon'] ? LEADING_ICON : nothing}
Expand Down
19 changes: 11 additions & 8 deletions textfield/lib/_input.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@
//

@mixin styles() {
.content {
.input-wrapper {
display: flex;
}

input,
.prefix,
.suffix {
.input-wrapper > * {
// Inherit field CSS set on the input wrapper, like font, but not margin or
// padding. This wrapper is needed since text fields may have prefix and
// suffix text next to an <input>
all: inherit;
padding: 0;
}

input {
.input {
caret-color: var(--_caret-color);
// remove extra height added by horizontal scrollbars
overflow-x: hidden;
text-align: inherit;

&::placeholder {
Expand All @@ -34,11 +37,11 @@
}
}

:focus-within input {
:focus-within .input {
caret-color: var(--_focus-caret-color);
}

.error:focus-within input {
.error:focus-within .input {
caret-color: var(--_error-focus-caret-color);
}

Expand All @@ -50,7 +53,7 @@
color: var(--_input-text-suffix-color);
}

.text-field:not(.disabled) input::placeholder {
.text-field:not(.disabled) .input::placeholder {
color: var(--_input-text-placeholder-color);
}

Expand Down
14 changes: 12 additions & 2 deletions textfield/lib/_shared.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,33 @@
:host {
display: inline-flex;
outline: none;
resize: both;
-webkit-tap-highlight-color: transparent;
}

.text-field,
.field {
width: 100%;
}

.text-field {
display: inline-flex;
flex: 1;
}

.field {
cursor: text;
flex: 1;
}

.disabled .field {
cursor: default;
}

.text-field,
.textarea .field {
// Note: only inherit default `resize: both` to the field when textarea.
resize: inherit;
}

@include icon.styles;
@include input.styles;
}
Loading

0 comments on commit 70c76aa

Please sign in to comment.