Skip to content

Commit

Permalink
fix(textfield): Update label position on value changed via JS
Browse files Browse the repository at this point in the history
  • Loading branch information
anton-kachurin committed Oct 14, 2017
1 parent b77895b commit 5db7c8e
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/mdc-textfield/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const strings = {
ICON_SELECTOR: '.mdc-textfield__icon',
ICON_EVENT: 'MDCTextfield:icon',
BOTTOM_LINE_SELECTOR: '.mdc-textfield__bottom-line',
INPUT_PROTO_PROP: 'value',
};

/** @enum {string} */
Expand Down
56 changes: 56 additions & 0 deletions packages/mdc-textfield/foundation.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ class MDCTextfieldFoundation extends MDCFoundation {
this.adapter_.registerTextFieldInteractionHandler(evtType, this.textFieldInteractionHandler_);
});
this.adapter_.registerTransitionEndHandler(this.transitionEndHandler_);
this.installPropertyChangeHooks_();
}

destroy() {
Expand All @@ -123,6 +124,7 @@ class MDCTextfieldFoundation extends MDCFoundation {
this.adapter_.deregisterTextFieldInteractionHandler(evtType, this.textFieldInteractionHandler_);
});
this.adapter_.deregisterTransitionEndHandler(this.transitionEndHandler_);
this.uninstallPropertyChangeHooks_();
}

/**
Expand Down Expand Up @@ -188,6 +190,17 @@ class MDCTextfieldFoundation extends MDCFoundation {
}
}

/**
* @private
*/
forceLayout_() {
// Don't do anything if the input is still focused.
if (!this.isFocused_) {
this.activateFocus_();
this.deactivateFocus_();
}
}

/**
* Makes the help text visible to screen readers.
* @private
Expand Down Expand Up @@ -338,6 +351,49 @@ class MDCTextfieldFoundation extends MDCFoundation {
this.useCustomValidityChecking_ = true;
this.changeValidity_(isValid);
}

/** @private */
installPropertyChangeHooks_() {
const {INPUT_PROTO_PROP} = MDCTextfieldFoundation.strings;
const nativeInputElement = this.getNativeInput_();
const inputElementProto = Object.getPrototypeOf(nativeInputElement);
const desc = Object.getOwnPropertyDescriptor(inputElementProto, INPUT_PROTO_PROP);

// We have to check for this descriptor, since some browsers (Safari) don't support its return.
// See: https://bugs.webkit.org/show_bug.cgi?id=49739
if (validDescriptor(desc)) {
const nativeInputElementDesc = ({
get: desc.get,
set: (value) => {
desc.set.call(nativeInputElement, value);
this.forceLayout_();
},
configurable: desc.configurable,
enumerable: desc.enumerable,
});
Object.defineProperty(nativeInputElement, INPUT_PROTO_PROP, nativeInputElementDesc);
}
}

/** @private */
uninstallPropertyChangeHooks_() {
const {INPUT_PROTO_PROP} = MDCTextfieldFoundation.strings;
const nativeInputElement = this.getNativeInput_();
const inputElementProto = Object.getPrototypeOf(nativeInputElement);
const desc = Object.getOwnPropertyDescriptor(inputElementProto, INPUT_PROTO_PROP);

if (validDescriptor(desc)) {
Object.defineProperty(nativeInputElement, INPUT_PROTO_PROP, desc);
}
}
}

/**
* @param {ObjectPropertyDescriptor|undefined} inputPropDesc
* @return {boolean}
*/
function validDescriptor(inputPropDesc) {
return !!inputPropDesc && typeof inputPropDesc.set === 'function';
}

export default MDCTextfieldFoundation;
106 changes: 106 additions & 0 deletions test/unit/mdc-textfield/foundation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/

import {assert} from 'chai';
import bel from 'bel';
import lolex from 'lolex';
import td from 'testdouble';

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

const {cssClasses} = MDCTextfieldFoundation;

const DESC_UNDEFINED = {
get: undefined,
set: undefined,
enumerable: false,
configurable: true,
};

function setupHookTest() {
const {foundation, mockAdapter} = setupFoundationTest(MDCTextfieldFoundation);
const nativeInput = bel`<input type="text">`;
td.when(mockAdapter.getNativeInput()).thenReturn(nativeInput);
return {foundation, mockAdapter, nativeInput};
}

// Shims Object.getOwnPropertyDescriptor for the checkbox's WebIDL attributes. Used to test
// the behavior of overridding WebIDL properties in different browser environments. For example,
// in Safari WebIDL attributes don't return get/set in descriptors.
function withMockCheckboxDescriptorReturning(descriptor, runTests) {
const originalDesc = Object.getOwnPropertyDescriptor(Object, 'getOwnPropertyDescriptor');
const mockGetOwnPropertyDescriptor = td.func('.getOwnPropertyDescriptor');

td.when(mockGetOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'))
.thenReturn(descriptor);

Object.defineProperty(Object, 'getOwnPropertyDescriptor', Object.assign({}, originalDesc, {
value: mockGetOwnPropertyDescriptor,
}));
runTests(mockGetOwnPropertyDescriptor);
Object.defineProperty(Object, 'getOwnPropertyDescriptor', originalDesc);
}

suite('MDCTextfieldFoundation');

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

test('#destroy removes mdc-textfield--upgraded class', () => {
const {foundation, mockAdapter} = setupTest();
foundation.destroy();
td.verify(mockAdapter.removeClass(cssClasses.UPGRADED));
});

test('#destroy removes event listeners', () => {
const {foundation, mockAdapter} = setupTest();
foundation.destroy();
Expand Down Expand Up @@ -173,6 +212,31 @@ test('#init does not add mdc-textfield__label--float-above class if the input do
td.verify(mockAdapter.addClassToLabel(cssClasses.LABEL_FLOAT_ABOVE), {times: 0});
});

test('#init handles case when WebIDL attrs cannot be overridden (Safari)', () => {
const {foundation, nativeInput} = setupHookTest();
withMockCheckboxDescriptorReturning(DESC_UNDEFINED, () => {
assert.doesNotThrow(() => {
foundation.init();
nativeInput.value = nativeInput.value + '_';
});
});
});

test('#init handles case when property descriptors are not returned at all (Android Browser)', () => {
const {foundation} = setupHookTest();
withMockCheckboxDescriptorReturning(undefined, () => {
assert.doesNotThrow(() => foundation.init());
});
});

test('#destroy handles case when WebIDL attrs cannot be overridden (Safari)', () => {
const {foundation} = setupHookTest();
withMockCheckboxDescriptorReturning(DESC_UNDEFINED, () => {
assert.doesNotThrow(() => foundation.init(), 'init sanity check');
assert.doesNotThrow(() => foundation.destroy());
});
});

test('on input focuses if input event occurs without any other events', () => {
const {foundation, mockAdapter} = setupTest();
let input;
Expand Down Expand Up @@ -478,3 +542,45 @@ test('interacting with text field does not emit custom events if input is disabl

td.verify(mockAdapter.notifyIconAction(), {times: 0});
});

test('"value" property change hook removes mdc-textfield__label--float-above class', () => {
const {foundation, mockAdapter, nativeInput} = setupHookTest();
const clock = lolex.install();

withMockCheckboxDescriptorReturning({
get: () => {},
set: () => {},
enumerable: false,
configurable: true,
}, () => {
nativeInput.value = '_';
foundation.init();
nativeInput.value = '';
td.verify(mockAdapter.removeClassFromLabel(cssClasses.LABEL_FLOAT_ABOVE));
});

clock.uninstall();
});

test('"value" property change hook does nothing if input is focused', () => {
const {foundation, mockAdapter, nativeInput} = setupHookTest();
const clock = lolex.install();

withMockCheckboxDescriptorReturning({
get: () => {},
set: () => {},
enumerable: false,
configurable: true,
}, () => {
let focus;
td.when(mockAdapter.registerInputInteractionHandler('focus', td.matchers.isA(Function))).thenDo((type, handler) => {
focus = handler;
});
foundation.init();
focus();
nativeInput.value = '';
td.verify(mockAdapter.removeClassFromLabel(td.matchers.anything()), {times: 0});
});

clock.uninstall();
});

0 comments on commit 5db7c8e

Please sign in to comment.