Skip to content
This repository was archived by the owner on Jan 13, 2025. It is now read-only.

Commit 5db7c8e

Browse files
fix(textfield): Update label position on value changed via JS
1 parent b77895b commit 5db7c8e

File tree

3 files changed

+163
-0
lines changed

3 files changed

+163
-0
lines changed

packages/mdc-textfield/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const strings = {
2323
ICON_SELECTOR: '.mdc-textfield__icon',
2424
ICON_EVENT: 'MDCTextfield:icon',
2525
BOTTOM_LINE_SELECTOR: '.mdc-textfield__bottom-line',
26+
INPUT_PROTO_PROP: 'value',
2627
};
2728

2829
/** @enum {string} */

packages/mdc-textfield/foundation.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ class MDCTextfieldFoundation extends MDCFoundation {
109109
this.adapter_.registerTextFieldInteractionHandler(evtType, this.textFieldInteractionHandler_);
110110
});
111111
this.adapter_.registerTransitionEndHandler(this.transitionEndHandler_);
112+
this.installPropertyChangeHooks_();
112113
}
113114

114115
destroy() {
@@ -123,6 +124,7 @@ class MDCTextfieldFoundation extends MDCFoundation {
123124
this.adapter_.deregisterTextFieldInteractionHandler(evtType, this.textFieldInteractionHandler_);
124125
});
125126
this.adapter_.deregisterTransitionEndHandler(this.transitionEndHandler_);
127+
this.uninstallPropertyChangeHooks_();
126128
}
127129

128130
/**
@@ -188,6 +190,17 @@ class MDCTextfieldFoundation extends MDCFoundation {
188190
}
189191
}
190192

193+
/**
194+
* @private
195+
*/
196+
forceLayout_() {
197+
// Don't do anything if the input is still focused.
198+
if (!this.isFocused_) {
199+
this.activateFocus_();
200+
this.deactivateFocus_();
201+
}
202+
}
203+
191204
/**
192205
* Makes the help text visible to screen readers.
193206
* @private
@@ -338,6 +351,49 @@ class MDCTextfieldFoundation extends MDCFoundation {
338351
this.useCustomValidityChecking_ = true;
339352
this.changeValidity_(isValid);
340353
}
354+
355+
/** @private */
356+
installPropertyChangeHooks_() {
357+
const {INPUT_PROTO_PROP} = MDCTextfieldFoundation.strings;
358+
const nativeInputElement = this.getNativeInput_();
359+
const inputElementProto = Object.getPrototypeOf(nativeInputElement);
360+
const desc = Object.getOwnPropertyDescriptor(inputElementProto, INPUT_PROTO_PROP);
361+
362+
// We have to check for this descriptor, since some browsers (Safari) don't support its return.
363+
// See: https://bugs.webkit.org/show_bug.cgi?id=49739
364+
if (validDescriptor(desc)) {
365+
const nativeInputElementDesc = ({
366+
get: desc.get,
367+
set: (value) => {
368+
desc.set.call(nativeInputElement, value);
369+
this.forceLayout_();
370+
},
371+
configurable: desc.configurable,
372+
enumerable: desc.enumerable,
373+
});
374+
Object.defineProperty(nativeInputElement, INPUT_PROTO_PROP, nativeInputElementDesc);
375+
}
376+
}
377+
378+
/** @private */
379+
uninstallPropertyChangeHooks_() {
380+
const {INPUT_PROTO_PROP} = MDCTextfieldFoundation.strings;
381+
const nativeInputElement = this.getNativeInput_();
382+
const inputElementProto = Object.getPrototypeOf(nativeInputElement);
383+
const desc = Object.getOwnPropertyDescriptor(inputElementProto, INPUT_PROTO_PROP);
384+
385+
if (validDescriptor(desc)) {
386+
Object.defineProperty(nativeInputElement, INPUT_PROTO_PROP, desc);
387+
}
388+
}
389+
}
390+
391+
/**
392+
* @param {ObjectPropertyDescriptor|undefined} inputPropDesc
393+
* @return {boolean}
394+
*/
395+
function validDescriptor(inputPropDesc) {
396+
return !!inputPropDesc && typeof inputPropDesc.set === 'function';
341397
}
342398

343399
export default MDCTextfieldFoundation;

test/unit/mdc-textfield/foundation.test.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616

1717
import {assert} from 'chai';
18+
import bel from 'bel';
19+
import lolex from 'lolex';
1820
import td from 'testdouble';
1921

2022
import {verifyDefaultAdapter} from '../helpers/foundation';
@@ -23,6 +25,37 @@ import MDCTextfieldFoundation from '../../../packages/mdc-textfield/foundation';
2325

2426
const {cssClasses} = MDCTextfieldFoundation;
2527

28+
const DESC_UNDEFINED = {
29+
get: undefined,
30+
set: undefined,
31+
enumerable: false,
32+
configurable: true,
33+
};
34+
35+
function setupHookTest() {
36+
const {foundation, mockAdapter} = setupFoundationTest(MDCTextfieldFoundation);
37+
const nativeInput = bel`<input type="text">`;
38+
td.when(mockAdapter.getNativeInput()).thenReturn(nativeInput);
39+
return {foundation, mockAdapter, nativeInput};
40+
}
41+
42+
// Shims Object.getOwnPropertyDescriptor for the checkbox's WebIDL attributes. Used to test
43+
// the behavior of overridding WebIDL properties in different browser environments. For example,
44+
// in Safari WebIDL attributes don't return get/set in descriptors.
45+
function withMockCheckboxDescriptorReturning(descriptor, runTests) {
46+
const originalDesc = Object.getOwnPropertyDescriptor(Object, 'getOwnPropertyDescriptor');
47+
const mockGetOwnPropertyDescriptor = td.func('.getOwnPropertyDescriptor');
48+
49+
td.when(mockGetOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'))
50+
.thenReturn(descriptor);
51+
52+
Object.defineProperty(Object, 'getOwnPropertyDescriptor', Object.assign({}, originalDesc, {
53+
value: mockGetOwnPropertyDescriptor,
54+
}));
55+
runTests(mockGetOwnPropertyDescriptor);
56+
Object.defineProperty(Object, 'getOwnPropertyDescriptor', originalDesc);
57+
}
58+
2659
suite('MDCTextfieldFoundation');
2760

2861
test('exports strings', () => {
@@ -137,6 +170,12 @@ test('#init adds event listeners', () => {
137170
td.verify(mockAdapter.registerTransitionEndHandler(td.matchers.isA(Function)));
138171
});
139172

173+
test('#destroy removes mdc-textfield--upgraded class', () => {
174+
const {foundation, mockAdapter} = setupTest();
175+
foundation.destroy();
176+
td.verify(mockAdapter.removeClass(cssClasses.UPGRADED));
177+
});
178+
140179
test('#destroy removes event listeners', () => {
141180
const {foundation, mockAdapter} = setupTest();
142181
foundation.destroy();
@@ -173,6 +212,31 @@ test('#init does not add mdc-textfield__label--float-above class if the input do
173212
td.verify(mockAdapter.addClassToLabel(cssClasses.LABEL_FLOAT_ABOVE), {times: 0});
174213
});
175214

215+
test('#init handles case when WebIDL attrs cannot be overridden (Safari)', () => {
216+
const {foundation, nativeInput} = setupHookTest();
217+
withMockCheckboxDescriptorReturning(DESC_UNDEFINED, () => {
218+
assert.doesNotThrow(() => {
219+
foundation.init();
220+
nativeInput.value = nativeInput.value + '_';
221+
});
222+
});
223+
});
224+
225+
test('#init handles case when property descriptors are not returned at all (Android Browser)', () => {
226+
const {foundation} = setupHookTest();
227+
withMockCheckboxDescriptorReturning(undefined, () => {
228+
assert.doesNotThrow(() => foundation.init());
229+
});
230+
});
231+
232+
test('#destroy handles case when WebIDL attrs cannot be overridden (Safari)', () => {
233+
const {foundation} = setupHookTest();
234+
withMockCheckboxDescriptorReturning(DESC_UNDEFINED, () => {
235+
assert.doesNotThrow(() => foundation.init(), 'init sanity check');
236+
assert.doesNotThrow(() => foundation.destroy());
237+
});
238+
});
239+
176240
test('on input focuses if input event occurs without any other events', () => {
177241
const {foundation, mockAdapter} = setupTest();
178242
let input;
@@ -478,3 +542,45 @@ test('interacting with text field does not emit custom events if input is disabl
478542

479543
td.verify(mockAdapter.notifyIconAction(), {times: 0});
480544
});
545+
546+
test('"value" property change hook removes mdc-textfield__label--float-above class', () => {
547+
const {foundation, mockAdapter, nativeInput} = setupHookTest();
548+
const clock = lolex.install();
549+
550+
withMockCheckboxDescriptorReturning({
551+
get: () => {},
552+
set: () => {},
553+
enumerable: false,
554+
configurable: true,
555+
}, () => {
556+
nativeInput.value = '_';
557+
foundation.init();
558+
nativeInput.value = '';
559+
td.verify(mockAdapter.removeClassFromLabel(cssClasses.LABEL_FLOAT_ABOVE));
560+
});
561+
562+
clock.uninstall();
563+
});
564+
565+
test('"value" property change hook does nothing if input is focused', () => {
566+
const {foundation, mockAdapter, nativeInput} = setupHookTest();
567+
const clock = lolex.install();
568+
569+
withMockCheckboxDescriptorReturning({
570+
get: () => {},
571+
set: () => {},
572+
enumerable: false,
573+
configurable: true,
574+
}, () => {
575+
let focus;
576+
td.when(mockAdapter.registerInputInteractionHandler('focus', td.matchers.isA(Function))).thenDo((type, handler) => {
577+
focus = handler;
578+
});
579+
foundation.init();
580+
focus();
581+
nativeInput.value = '';
582+
td.verify(mockAdapter.removeClassFromLabel(td.matchers.anything()), {times: 0});
583+
});
584+
585+
clock.uninstall();
586+
});

0 commit comments

Comments
 (0)