Skip to content

Commit ce807c3

Browse files
author
Elliott Marquez
authored
Fix outlined textfield notch when initially rendered hidden (#488)
fixes #444 by providing the mwc-textfied.layout() method. In this PR: added layout function notch performs layout when label is changed fixed hidden bug where foundation was being recreated too often caused bug where changing maxLength caused foundation to unset invalid had to pull logic out of foundation and into registerValidationAttributeHandler The issue: when the label is floating and the element is outlined, but the label has no width, e.g. mwc-textfield[outlined][label="something"][value="something"][hidden] the mdc dataflow will calculate the size of the notch as zero. When hidden is removed, the notch will not update in size. Here we give them the layout method. It is not possible to know when the textfield is changed from display: none to visible without setting a resize observer on the label. I wanted to avoid that because of possible render trashing and the overall heaviness of resizeObserver and a polyfill. Instead, it is up to the user to call layout when they change the visibility of the element.
1 parent af9c047 commit ce807c3

File tree

11 files changed

+550
-53
lines changed

11 files changed

+550
-53
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
99
### Added
1010

1111
- Implemented `mwc-dialog`
12+
- `mwc-textfield.layout` method.
1213

1314
### Changed
1415

packages/floating-label/src/mwc-floating-label-directive.ts

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,28 @@ const createAdapter = (labelElement: HTMLElement): MDCFloatingLabelAdapter => {
3636
};
3737
};
3838

39+
interface LabelAndLabelFoundation {
40+
label: string;
41+
foundation: MDCFloatingLabelFoundation;
42+
}
43+
3944
const partToFoundationMap =
40-
new WeakMap<PropertyPart, MDCFloatingLabelFoundation>();
41-
42-
export const floatingLabel = directive(() => (part: PropertyPart) => {
43-
const lastFoundation = partToFoundationMap.get(part);
44-
if (!lastFoundation) {
45-
const labelElement = part.committer.element as FloatingLabel;
46-
labelElement.classList.add('mdc-floating-label');
47-
const adapter = createAdapter(labelElement);
48-
const foundation = new MDCFloatingLabelFoundation(adapter);
49-
foundation.init();
50-
part.setValue(foundation);
51-
partToFoundationMap.set(part, foundation);
52-
}
53-
});
45+
new WeakMap<PropertyPart, LabelAndLabelFoundation>();
46+
47+
export const floatingLabel =
48+
directive((label: string) => (part: PropertyPart) => {
49+
const lastFoundation = partToFoundationMap.get(part);
50+
if (!lastFoundation) {
51+
const labelElement = part.committer.element as FloatingLabel;
52+
labelElement.classList.add('mdc-floating-label');
53+
const adapter = createAdapter(labelElement);
54+
const foundation = new MDCFloatingLabelFoundation(adapter);
55+
foundation.init();
56+
part.setValue(foundation);
57+
partToFoundationMap.set(part, {label, foundation});
58+
} else if (lastFoundation.label !== label) {
59+
const labelElement = part.committer.element as FloatingLabel;
60+
const labelChangeEvent = new Event('labelchange');
61+
labelElement.dispatchEvent(labelChangeEvent);
62+
}
63+
});

packages/notched-outline/src/mwc-notched-outline-base.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,7 @@ export class NotchedOutlineBase extends BaseElement {
5757
}
5858

5959
render() {
60-
if (this.open !== this.lastOpen) {
61-
// workaround for possible bug in foundation causing recalculation
62-
this.lastOpen = this.open;
63-
this.openOrClose(this.open, this.width);
64-
}
60+
this.openOrClose(this.open, this.width);
6561

6662
return html`
6763
<div class="mdc-notched-outline">

packages/textarea/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ type ValidityTransform = (value: string, nativeValidity: ValidityState) => Parti
166166
| `checkValidity() => boolean` | Returns `true` if the textarea passes validity checks. Returns `false` and fires an [`invalid`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/invalid_event) event on the textarea otherwise.
167167
| `reportValidity() => boolean` | Runs `checkValidity()` method, and if it returns false, then it reports to the user that the input is invalid.
168168
| `setCustomValidity(message:string) => void` | Sets a custom validity message (also overwrites `validationMessage`). If this message is not the empty string, then the element is suffering from a custom validity error and does not validate.
169+
| `layout() => Promise<void>` | Re-calculate layout. If a textarea is styled with `display:none` before it is first rendered, and it has a label that is floating, then you must call `layout()` the first time you remove `display:none`, or else the notch surrounding the label will not render correctly.
169170
170171
### CSS Custom Properties
171172

packages/textarea/src/mwc-textarea-base.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ See the License for the specific language governing permissions and
1515
limitations under the License.
1616
*/
1717

18-
import {characterCounter} from '@material/mwc-textfield/character-counter/mwc-character-counter-directive.js';
1918
import {TextFieldBase} from '@material/mwc-textfield/mwc-textfield-base.js';
2019
import {html, property, query} from 'lit-element';
2120
import {classMap} from 'lit-html/directives/class-map';
@@ -30,23 +29,25 @@ export abstract class TextAreaBase extends TextFieldBase {
3029

3130
@property({type: Number}) cols = 20;
3231

32+
protected get shouldRenderHelperText(): boolean {
33+
return !!this.helper || !!this.validationMessage;
34+
}
35+
3336
render() {
3437
const classes = {
3538
'mdc-text-field--disabled': this.disabled,
3639
'mdc-text-field--no-label': !this.label,
3740
'mdc-text-field--outlined': this.outlined,
3841
'mdc-text-field--fullwidth': this.fullWidth,
3942
};
43+
4044
return html`
4145
<div class="mdc-text-field mdc-text-field--textarea ${classMap(classes)}">
42-
${
43-
this.charCounter ? html`
44-
<div .charCounterFoundation=${characterCounter()}></div>` :
45-
''}
46+
${this.renderCharCounter()}
4647
${this.renderInput()}
4748
${this.outlined ? this.renderOutlined() : this.renderLabelText()}
4849
</div>
49-
${this.helper ? this.renderHelperText() : ''}
50+
${this.renderHelperText()}
5051
`;
5152
}
5253

packages/textarea/src/mwc-textarea.scss

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,12 @@ limitations under the License.
5656
@include mdc-theme-prop(border-bottom-color, $mdc-text-field-disabled-border);
5757
}
5858

59-
.mdc-text-field__input {
59+
.mdc-text-field__input, .mdc-text-field-character-counter.hidden + .mdc-text-field__input {
6060
padding: 0 16px 0 16px;
6161
margin: 20px 0 1px 0;
6262
}
6363

64-
.mdc-text-field-character-counter + .mdc-text-field__input {
64+
.mdc-text-field-character-counter:not(.hidden) + .mdc-text-field__input {
6565
margin-bottom: 28px;
6666
}
6767

@@ -88,7 +88,7 @@ limitations under the License.
8888
.mdc-text-field-character-counter {
8989
bottom: 14px;
9090

91-
&+.mdc-text-field__input {
91+
&:not(.hidden)+.mdc-text-field__input {
9292
margin-bottom: 41px;
9393
}
9494
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/**
2+
* @license
3+
* Copyright 2019 Google Inc. All Rights Reserved.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import {TextArea} from '@material/mwc-textarea';
19+
import {html} from 'lit-html';
20+
21+
import {fixture, TestFixture} from '../../../../test/src/util/helpers';
22+
23+
24+
const basic = html`
25+
<mwc-textarea></mwc-textarea>
26+
`;
27+
28+
suite('mwc-textarea:', () => {
29+
let fixt: TestFixture;
30+
31+
suite('basic', () => {
32+
let element: TextArea;
33+
setup(async () => {
34+
fixt = await fixture(basic);
35+
36+
element = fixt.root.querySelector('mwc-textarea')!;
37+
});
38+
39+
test('initializes as an mwc-textarea', () => {
40+
assert.instanceOf(element, TextArea);
41+
});
42+
43+
test('setting value sets on textarea', async () => {
44+
element.value = 'my test value';
45+
46+
const inputElement = element.shadowRoot!.querySelector('textarea');
47+
assert(inputElement, 'my test value');
48+
});
49+
50+
teardown(() => {
51+
if (fixt) {
52+
fixt.remove();
53+
}
54+
});
55+
});
56+
57+
suite('helper and char counter rendering', () => {
58+
let fixt: TestFixture;
59+
60+
setup(async () => {
61+
fixt = await fixture(basic);
62+
});
63+
64+
test(
65+
'createFoundation called an appropriate amount of times & render interactions',
66+
async () => {
67+
const element = fixt.root.querySelector('mwc-textarea')!;
68+
element.helperPersistent = true;
69+
70+
const oldCreateFoundation =
71+
(element as any).createFoundation.bind(element) as () => void;
72+
let numTimesCreateFoundationCalled = 0;
73+
74+
((element as any).createFoundation as () => void) = () => {
75+
numTimesCreateFoundationCalled = numTimesCreateFoundationCalled + 1;
76+
oldCreateFoundation();
77+
};
78+
79+
const charCounters = element.shadowRoot!.querySelectorAll(
80+
'.mdc-text-field-character-counter');
81+
82+
assert.strictEqual(charCounters.length, 1, 'only one char counter');
83+
84+
const charCounter = charCounters[0] as HTMLElement;
85+
const helperText = element.shadowRoot!.querySelector(
86+
'.mdc-text-field-helper-text') as HTMLElement;
87+
88+
89+
assert.strictEqual(
90+
charCounter.offsetWidth, 0, 'char counter initially hidden');
91+
assert.strictEqual(
92+
helperText.offsetWidth, 0, 'helper line initially hidden');
93+
94+
element.helper = 'my helper';
95+
await element.requestUpdate();
96+
97+
assert.strictEqual(
98+
numTimesCreateFoundationCalled,
99+
0,
100+
'foundation not recreated due to helper change');
101+
assert.strictEqual(
102+
charCounter.offsetWidth,
103+
0,
104+
'char counter hidden when only helper defined');
105+
assert.isTrue(
106+
helperText.offsetWidth > 0, 'helper text shown when defined');
107+
108+
element.helper = '';
109+
await element.requestUpdate();
110+
111+
assert.strictEqual(
112+
numTimesCreateFoundationCalled,
113+
0,
114+
'foundation not recreated due to helper change');
115+
assert.strictEqual(
116+
charCounter.offsetWidth,
117+
0,
118+
'char counter does not render on helper change');
119+
assert.strictEqual(
120+
helperText.offsetWidth,
121+
0,
122+
'helper line hides when reset to empty');
123+
124+
element.maxLength = 10;
125+
await element.requestUpdate();
126+
127+
assert.strictEqual(
128+
numTimesCreateFoundationCalled,
129+
1,
130+
'foundation created when maxlength changed from -1');
131+
assert.strictEqual(
132+
charCounter.offsetWidth,
133+
0,
134+
'char counter does not render without charCounter set');
135+
assert.strictEqual(
136+
helperText.offsetWidth,
137+
0,
138+
'helper line does not render on maxLength change');
139+
140+
numTimesCreateFoundationCalled = 0;
141+
element.maxLength = -1;
142+
await element.requestUpdate();
143+
144+
assert.strictEqual(
145+
numTimesCreateFoundationCalled,
146+
1,
147+
'foundation created when maxlength changed to -1');
148+
149+
numTimesCreateFoundationCalled = 0;
150+
element.charCounter = true;
151+
await element.requestUpdate();
152+
153+
assert.strictEqual(
154+
numTimesCreateFoundationCalled,
155+
0,
156+
'foundation not updated when charCounter changed');
157+
assert.strictEqual(
158+
charCounter.offsetWidth,
159+
0,
160+
'char counter does not render without maxLength set');
161+
assert.strictEqual(
162+
helperText.offsetWidth,
163+
0,
164+
'helper line does not render on charCounter change');
165+
166+
element.maxLength = 20;
167+
await element.requestUpdate();
168+
169+
assert.strictEqual(
170+
numTimesCreateFoundationCalled,
171+
1,
172+
'foundation created when maxlength changed from -1');
173+
assert.isTrue(
174+
charCounter.offsetWidth > 0,
175+
'char counter renders when both charCounter and maxLength set');
176+
177+
numTimesCreateFoundationCalled = 0;
178+
element.maxLength = 15;
179+
await element.requestUpdate();
180+
181+
assert.strictEqual(
182+
numTimesCreateFoundationCalled,
183+
0,
184+
'foundation not recreated when maxLength not changed to or from -1');
185+
assert.isTrue(
186+
charCounter.offsetWidth > 0,
187+
'char counter still visible on maxLength change');
188+
});
189+
190+
teardown(() => {
191+
if (fixt) {
192+
fixt.remove();
193+
}
194+
});
195+
});
196+
});

packages/textfield/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ type ValidityTransform = (value: string, nativeValidity: ValidityState) => Parti
195195
| `checkValidity() => boolean` | Returns `true` if the textfield passes validity checks. Returns `false` and fires an [`invalid`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/invalid_event) event on the textfield otherwise.
196196
| `reportValidity() => boolean` | Runs `checkValidity()` method, and if it returns false, then ir reports to the user that the input is invalid.
197197
| `setCustomValidity(message:string) => void` | Sets a custom validity message (also overwrites `validationMessage`). If this message is not the empty string, then the element is suffering froma custom validity error and does not validate.
198+
| `layout() => Promise<void>` | Re-calculate layout. If a textfield is styled with `display:none` before it is first rendered, and it has a label that is floating, then you must call `layout()` the first time you remove `display:none`, or else the notch surrounding the label will not render correctly.
198199
199200
### CSS Custom Properties
200201

0 commit comments

Comments
 (0)