Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion docs/components/text-field.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ Multi-line text fields behave like
elements.

Textareas can specify the initial number of `rows`. Use CSS `width`, `height`,
and `resize` to control the resize behavior of a textarea.
`resize`, and `field-sizing` to control the resize behavior of a textarea.

<!-- no-catalog-start -->

Expand All @@ -183,6 +183,25 @@ resize](images/textfield/usage-textarea.webp)
</md-filled-text-field>
```

Set `field-sizing: content` on a textarea to automatically grow with its content
in supporting browsers. When `field-sizing: content` is used, `rows` and `cols`
no longer set default dimensions, so use CSS `width`, `min-height`, and
`max-height` to bound the size.

```html
<style>
md-filled-text-field {
field-sizing: content;
width: 300px;
max-height: 200px;
}
</style>
<md-filled-text-field
type="textarea"
label="Auto-growing">
</md-filled-text-field>
```

### Icons

Text fields may display optional icons. Use icons to describe the input method
Expand Down
4 changes: 4 additions & 0 deletions field/internal/_content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ $_enter-delay: $_label-duration - $_visible-duration;
// Use margin for textareas since they will scroll over the label if not.
margin-top: var(--_top-space);
margin-bottom: var(--_bottom-space);
// Allow unbroken strings to wrap within explicit widths when using
// `field-sizing: content` (since `.container` has
// `min-width: min-content`).
overflow-wrap: anywhere;
}

:hover .content {
Expand Down
12 changes: 12 additions & 0 deletions field/internal/_shared.scss
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@
resize: inherit;
}

// Inherit field-sizing down through slotted content so that it can be
// customized on the host.
.field,
.container-overflow,
.container,
.middle,
.content,
.content slot,
.content ::slotted(*) {
field-sizing: inherit;
}

.resizable:not(.disabled) .container {
// `resize` is inherited from the host, but only applies to the container
// when resizable.
Expand Down
90 changes: 90 additions & 0 deletions textfield/filled-text-field_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

// import 'jasmine'; (google3-only)

import {html} from 'lit';

import {Environment} from '../testing/environment.js';
import {createTokenTests} from '../testing/tokens.js';

import {MdFilledTextField} from './filled-text-field.js';
Expand All @@ -15,3 +18,90 @@ describe('<md-filled-field>', () => {
createTokenTests(MdFilledTextField.styles);
});
});

describe('<md-filled-text-field>', () => {
const env = new Environment();

describe('field-sizing', () => {
it('grows height with multiline content', async () => {
if (!CSS.supports('field-sizing', 'content')) {
return;
}
const root = env.render(html`
<md-filled-text-field
type="textarea"
style="field-sizing: content"></md-filled-text-field>
`);
const field = root.querySelector('md-filled-text-field');
if (!field) {
throw new Error('Missing <md-filled-text-field>.');
}
await env.waitForStability();
const initialHeight = field.getBoundingClientRect().height;

field.value = 'Line 1\nLine 2\nLine 3\nLine 4\nLine 5';
await env.waitForStability();

expect(field.getBoundingClientRect().height)
.withContext('expanded textarea height')
.toBeGreaterThan(initialHeight);
});

it('wraps unbroken text within an explicit width', async () => {
if (!CSS.supports('field-sizing', 'content')) {
return;
}
const root = env.render(html`
<md-filled-text-field
type="textarea"
style="field-sizing: content; width: 200px"
value="short"></md-filled-text-field>
`);
const field = root.querySelector('md-filled-text-field');
if (!field) {
throw new Error('Missing <md-filled-text-field>.');
}
await env.waitForStability();
const initialHeight = field.getBoundingClientRect().height;

field.value = 'a'.repeat(200);
await env.waitForStability();

const rect = field.getBoundingClientRect();
expect(rect.width)
.withContext('bounded host width')
.toBeLessThanOrEqual(200);
expect(field.scrollWidth)
.withContext('no horizontal overflow')
.toBeLessThanOrEqual(200);
expect(rect.height)
.withContext('wrapped height')
.toBeGreaterThan(initialHeight);
});

it('caps height and scrollHeight at max-height', async () => {
if (!CSS.supports('field-sizing', 'content')) {
return;
}
const root = env.render(html`
<md-filled-text-field
type="textarea"
style="field-sizing: content; max-height: 120px"
value=${'line\n'.repeat(20)}></md-filled-text-field>
`);
const field = root.querySelector('md-filled-text-field');
if (!field) {
throw new Error('Missing <md-filled-text-field>.');
}

await env.waitForStability();

expect(field.getBoundingClientRect().height)
.withContext('capped host height')
.toBeLessThanOrEqual(120);
expect(field.scrollHeight)
.withContext('no vertical overflow past max-height')
.toBeLessThanOrEqual(120);
});
});
});
1 change: 1 addition & 0 deletions textfield/internal/_shared.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

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

Expand Down
90 changes: 90 additions & 0 deletions textfield/outlined-text-field_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

// import 'jasmine'; (google3-only)

import {html} from 'lit';

import {Environment} from '../testing/environment.js';
import {createTokenTests} from '../testing/tokens.js';

import {MdOutlinedTextField} from './outlined-text-field.js';
Expand All @@ -15,3 +18,90 @@ describe('<md-outlined-field>', () => {
createTokenTests(MdOutlinedTextField.styles);
});
});

describe('<md-outlined-text-field>', () => {
const env = new Environment();

describe('field-sizing', () => {
it('grows height with multiline content', async () => {
if (!CSS.supports('field-sizing', 'content')) {
return;
}
const root = env.render(html`
<md-outlined-text-field
type="textarea"
style="field-sizing: content"></md-outlined-text-field>
`);
const field = root.querySelector('md-outlined-text-field');
if (!field) {
throw new Error('Missing <md-outlined-text-field>.');
}
await env.waitForStability();
const initialHeight = field.getBoundingClientRect().height;

field.value = 'Line 1\nLine 2\nLine 3\nLine 4\nLine 5';
await env.waitForStability();

expect(field.getBoundingClientRect().height)
.withContext('expanded textarea height')
.toBeGreaterThan(initialHeight);
});

it('wraps unbroken text within an explicit width', async () => {
if (!CSS.supports('field-sizing', 'content')) {
return;
}
const root = env.render(html`
<md-outlined-text-field
type="textarea"
style="field-sizing: content; width: 200px"
value="short"></md-outlined-text-field>
`);
const field = root.querySelector('md-outlined-text-field');
if (!field) {
throw new Error('Missing <md-outlined-text-field>.');
}
await env.waitForStability();
const initialHeight = field.getBoundingClientRect().height;

field.value = 'a'.repeat(200);
await env.waitForStability();

const rect = field.getBoundingClientRect();
expect(rect.width)
.withContext('bounded host width')
.toBeLessThanOrEqual(200);
expect(field.scrollWidth)
.withContext('no horizontal overflow')
.toBeLessThanOrEqual(200);
expect(rect.height)
.withContext('wrapped height')
.toBeGreaterThan(initialHeight);
});

it('caps height and scrollHeight at max-height', async () => {
if (!CSS.supports('field-sizing', 'content')) {
return;
}
const root = env.render(html`
<md-outlined-text-field
type="textarea"
style="field-sizing: content; max-height: 120px"
value=${'line\n'.repeat(20)}></md-outlined-text-field>
`);
const field = root.querySelector('md-outlined-text-field');
if (!field) {
throw new Error('Missing <md-outlined-text-field>.');
}

await env.waitForStability();

expect(field.getBoundingClientRect().height)
.withContext('capped host height')
.toBeLessThanOrEqual(120);
expect(field.scrollHeight)
.withContext('no vertical overflow past max-height')
.toBeLessThanOrEqual(120);
});
});
});
Loading