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
- Made resizable field respect min-height
- Remove resize handle when field is disabled
PiperOrigin-RevId: 549104722
  • Loading branch information
asyncLiz authored and copybara-github committed Jul 18, 2023
1 parent 10f60d2 commit ff2e089
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 105 deletions.
19 changes: 12 additions & 7 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 Down
7 changes: 6 additions & 1 deletion field/lib/_shared.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
border-end-end-radius: var(--_container-shape-end-end);
border-end-start-radius: var(--_container-shape-end-start);
display: flex;
height: 100%;
position: relative;
}

Expand All @@ -44,14 +45,18 @@
border-radius: inherit;
display: flex;
flex: 1;
max-height: 100%;
min-height: 100%;
min-width: min-content;
overflow: hidden;
position: relative;
}

.field,
.container-overflow,
.container {
.field:not(.disabled) .container {
// Inherit `resize` set on host, but only inherit it for the actual
// container if the field is not disabled.
resize: inherit;
}

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
12 changes: 7 additions & 5 deletions textfield/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export class TextFieldHarness extends Harness<TextField> {
}

protected simulateInput(
element: HTMLInputElement, charactersToAppend: string,
element: HTMLInputElement|HTMLTextAreaElement, charactersToAppend: string,
init?: InputEventInit) {
element.value += charactersToAppend;
if (!init) {
Expand All @@ -106,8 +106,8 @@ export class TextFieldHarness extends Harness<TextField> {
}

protected simulateDeletion(
element: HTMLInputElement, beginIndex?: number, endIndex?: number,
init?: InputEventInit) {
element: HTMLInputElement|HTMLTextAreaElement, beginIndex?: number,
endIndex?: number, init?: InputEventInit) {
const deletedCharacters = element.value.slice(beginIndex, endIndex);
element.value = element.value.substring(0, beginIndex ?? 0) +
element.value.substring(endIndex ?? element.value.length);
Expand All @@ -122,7 +122,8 @@ export class TextFieldHarness extends Harness<TextField> {
element.dispatchEvent(new InputEvent('input', init));
}

protected simulateChangeIfNeeded(element: HTMLInputElement) {
protected simulateChangeIfNeeded(element: HTMLInputElement|
HTMLTextAreaElement) {
if (this.valueBeforeChange === element.value) {
return;
}
Expand All @@ -133,6 +134,7 @@ export class TextFieldHarness extends Harness<TextField> {

protected override async getInteractiveElement() {
await this.element.updateComplete;
return this.element.renderRoot.querySelector('input')!;
return this.element.renderRoot.querySelector('.input') as HTMLInputElement |
HTMLTextAreaElement;
}
}
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 ff2e089

Please sign in to comment.