diff --git a/docs/components/text-field.md b/docs/components/text-field.md
index 462ee8731f..a324e65aeb 100644
--- a/docs/components/text-field.md
+++ b/docs/components/text-field.md
@@ -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.
@@ -183,6 +183,25 @@ resize](images/textfield/usage-textarea.webp)
```
+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
+
+
+
+```
+
### Icons
Text fields may display optional icons. Use icons to describe the input method
diff --git a/field/internal/_content.scss b/field/internal/_content.scss
index c3fe0ac3ce..f93646b03d 100644
--- a/field/internal/_content.scss
+++ b/field/internal/_content.scss
@@ -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 {
diff --git a/field/internal/_shared.scss b/field/internal/_shared.scss
index ee1e555e79..c40ff494f6 100644
--- a/field/internal/_shared.scss
+++ b/field/internal/_shared.scss
@@ -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.
diff --git a/textfield/filled-text-field_test.ts b/textfield/filled-text-field_test.ts
index 80dd7030b4..f040e5fecc 100644
--- a/textfield/filled-text-field_test.ts
+++ b/textfield/filled-text-field_test.ts
@@ -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';
@@ -15,3 +18,90 @@ describe('', () => {
createTokenTests(MdFilledTextField.styles);
});
});
+
+describe('', () => {
+ 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`
+
+ `);
+ const field = root.querySelector('md-filled-text-field');
+ if (!field) {
+ throw new Error('Missing .');
+ }
+ 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`
+
+ `);
+ const field = root.querySelector('md-filled-text-field');
+ if (!field) {
+ throw new Error('Missing .');
+ }
+ 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`
+
+ `);
+ const field = root.querySelector('md-filled-text-field');
+ if (!field) {
+ throw new Error('Missing .');
+ }
+
+ 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);
+ });
+ });
+});
diff --git a/textfield/internal/_shared.scss b/textfield/internal/_shared.scss
index a9c036e3d2..db068e3044 100644
--- a/textfield/internal/_shared.scss
+++ b/textfield/internal/_shared.scss
@@ -23,6 +23,7 @@
.text-field,
.field {
+ field-sizing: inherit;
width: 100%;
}
diff --git a/textfield/outlined-text-field_test.ts b/textfield/outlined-text-field_test.ts
index 30c40d21f7..6c89c471ea 100644
--- a/textfield/outlined-text-field_test.ts
+++ b/textfield/outlined-text-field_test.ts
@@ -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';
@@ -15,3 +18,90 @@ describe('', () => {
createTokenTests(MdOutlinedTextField.styles);
});
});
+
+describe('', () => {
+ 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`
+
+ `);
+ const field = root.querySelector('md-outlined-text-field');
+ if (!field) {
+ throw new Error('Missing .');
+ }
+ 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`
+
+ `);
+ const field = root.querySelector('md-outlined-text-field');
+ if (!field) {
+ throw new Error('Missing .');
+ }
+ 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`
+
+ `);
+ const field = root.querySelector('md-outlined-text-field');
+ if (!field) {
+ throw new Error('Missing .');
+ }
+
+ 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);
+ });
+ });
+});