From 9253865fb979d195f0877840ef8d79674224a1ec Mon Sep 17 00:00:00 2001 From: yonatankra Date: Tue, 10 Aug 2021 15:02:27 +0300 Subject: [PATCH 1/5] Add the no-actions-sync attribute --- components/textfield/readme.md | 1 + components/textfield/src/vwc-textfield.ts | 211 ++++++++++-------- .../textfield/test/textfield-action.test.js | 83 ++++++- 3 files changed, 204 insertions(+), 91 deletions(-) diff --git a/components/textfield/readme.md b/components/textfield/readme.md index 6594e06ca..cb163e17c 100644 --- a/components/textfield/readme.md +++ b/components/textfield/readme.md @@ -42,6 +42,7 @@ This component is an extension of [\](https://github.com/materia | `validityTransform` | | `((value: string, nativeValidity: ValidityState) => Partial) \| null` | | | `value` | | `string` | | | `willValidate` | readonly | `boolean` | | +| `noActionsSync` | | `boolean` | Prevents auto sync between textfield attributes and action icon buttons attributes | #### Methods diff --git a/components/textfield/src/vwc-textfield.ts b/components/textfield/src/vwc-textfield.ts index e144c4ed0..db417393d 100644 --- a/components/textfield/src/vwc-textfield.ts +++ b/components/textfield/src/vwc-textfield.ts @@ -45,23 +45,40 @@ const MDC_FLOAT_ABOVE_CLASS_NAME = 'mdc-floating-label--float-above'; @customElement('vwc-textfield') export class VWCTextField extends MWCTextField { - @property({ type: Boolean, reflect: true }) + @property({ + type: Boolean, + reflect: true, + attribute: 'no-actions-sync' + }) + noActionsSync = false; + + @property({ + type: Boolean, + reflect: true + }) dense = false; - @property({ type: String, reflect: true }) + @property({ + type: String, + reflect: true + }) shape?: TextfieldShape; - @property({ type: String, reflect: true }) + @property({ + type: String, + reflect: true + }) form: string | undefined; - @property({ type: String, reflect: true, converter: v => v || ' ' }) + @property({ + type: String, + reflect: true, + converter: v => v || ' ' + }) placeholder = ' '; - + @query('.mdc-text-field__input') protected inputElementWrapper!: HTMLInputElement; @internalProperty() private hasActionButtons = false; - - @query('.mdc-text-field__input') protected inputElementWrapper!: HTMLInputElement; - @queryAssignedNodes('action', true, VALID_BUTTON_ELEMENTS.join(', ')) private actionButtons?: NodeListOf; @@ -101,8 +118,10 @@ export class VWCTextField extends MWCTextField { await super.firstUpdated(); this.shadowRoot ?.querySelector('.mdc-notched-outline') - ?.shadowRoot?.querySelector('.mdc-notched-outline') - ?.classList.add('vvd-notch'); + ?.shadowRoot + ?.querySelector('.mdc-notched-outline') + ?.classList + .add('vvd-notch'); this.floatLabel(); handleAutofocus(this); this.observeInputSize(); @@ -127,16 +146,38 @@ export class VWCTextField extends MWCTextField { } } - @debounced(50) - private syncInputSize() { - const { width: hostWidth, left: hostLeft } = this.getBoundingClientRect(); - const { width: wrapperWidth, left: wrapperLeft } = this.inputElementWrapper.getBoundingClientRect(); - const paddingLeft = wrapperLeft - hostLeft; - const paddingRight = hostWidth - wrapperWidth - paddingLeft; - requestAnimationFrame(() => { - this.formElement.style.paddingLeft = `${paddingLeft}px`; - this.formElement.style.paddingRight = `${paddingRight}px`; - }); + /** @soyTemplate */ + render(): TemplateResult { + const shouldRenderCharCounter = this.charCounter && this.maxLength !== -1; + const shouldRenderHelperText = + !!this.helper || !!this.validationMessage || shouldRenderCharCounter; + + /** @classMap */ + const classes = { + 'mdc-text-field--disabled': this.disabled, + 'mdc-text-field--no-label': !this.label, + 'mdc-text-field--filled': !this.outlined, + 'mdc-text-field--outlined': this.outlined, + 'mdc-text-field--with-leading-icon': this.icon, + 'mdc-text-field--with-trailing-icon': this.iconTrailing, + 'mdc-text-field--end-aligned': this.endAligned, + 'vvd-text-field--with-action': this.hasActionButtons, + }; + + return html` + + ${this.renderHelperText(shouldRenderHelperText)} + `; } protected observeInputSize(): void { @@ -170,9 +211,10 @@ export class VWCTextField extends MWCTextField { protected renderOutline(): TemplateResult | string { return !this.outlined ? '' - : html` + : html` + ${this.renderLabel()} - `; + `; } protected renderHelperText( @@ -185,13 +227,15 @@ export class VWCTextField extends MWCTextField { const isError = this.validationMessage && !this.isUiValid; const text = isError ? this.validationMessage : this.helper; - return html`${text}`; + return html` + ${text} + `; } protected renderRipple(): TemplateResult { @@ -202,6 +246,50 @@ export class VWCTextField extends MWCTextField { return html``; } + protected onActionSlotChange(): void { + this.hasActionButtons = Boolean(this.actionButtons?.length); + this.enforcePropsOnActionNodes(); + } + + protected enforcePropsOnActionNodes(): void { + if (this.noActionsSync) { + return; + } + const buttons = Array.from(this.actionButtons || []); + + buttons.forEach((button) => { + button.toggleAttribute('disabled', this.disabled); + button.toggleAttribute('dense', this.dense); + + const buttonShape = this.shape == 'pill' + ? 'circled' + : this.shape; + if (buttonShape) { + button.setAttribute('shape', buttonShape); + } else { + button.removeAttribute('shape'); + } + }); + } + + @debounced(50) + private syncInputSize() { + const { + width: hostWidth, + left: hostLeft + } = this.getBoundingClientRect(); + const { + width: wrapperWidth, + left: wrapperLeft + } = this.inputElementWrapper.getBoundingClientRect(); + const paddingLeft = wrapperLeft - hostLeft; + const paddingRight = hostWidth - wrapperWidth - paddingLeft; + requestAnimationFrame(() => { + this.formElement.style.paddingLeft = `${paddingLeft}px`; + this.formElement.style.paddingRight = `${paddingRight}px`; + }); + } + private createInputElement(): HTMLInputElement { const element = document.createElement('input'); const defaultValue = this.getAttribute('value'); @@ -275,63 +363,6 @@ export class VWCTextField extends MWCTextField { fle.classList.remove(MDC_FLOAT_ABOVE_CLASS_NAME); } } - - protected onActionSlotChange(): void { - this.hasActionButtons = Boolean(this.actionButtons?.length); - this.enforcePropsOnActionNodes(); - } - - protected enforcePropsOnActionNodes(): void { - const buttons = Array.from(this.actionButtons || []); - - buttons.forEach((button) => { - button.toggleAttribute('disabled', this.disabled); - button.toggleAttribute('dense', this.dense); - - const buttonShape = this.shape == 'pill' - ? 'circled' - : this.shape; - if (buttonShape) { - button.setAttribute('shape', buttonShape); - } else { - button.removeAttribute('shape'); - } - }); - } - - /** @soyTemplate */ - render(): TemplateResult { - const shouldRenderCharCounter = this.charCounter && this.maxLength !== -1; - const shouldRenderHelperText = - !!this.helper || !!this.validationMessage || shouldRenderCharCounter; - - /** @classMap */ - const classes = { - 'mdc-text-field--disabled': this.disabled, - 'mdc-text-field--no-label': !this.label, - 'mdc-text-field--filled': !this.outlined, - 'mdc-text-field--outlined': this.outlined, - 'mdc-text-field--with-leading-icon': this.icon, - 'mdc-text-field--with-trailing-icon': this.iconTrailing, - 'mdc-text-field--end-aligned': this.endAligned, - 'vvd-text-field--with-action': this.hasActionButtons, - }; - - return html` - - ${this.renderHelperText(shouldRenderHelperText)} - `; - } } const setAttributeByValue = (function () { @@ -342,11 +373,13 @@ const setAttributeByValue = (function () { target: HTMLInputElement, asEmpty = false ): void { - const newValue:unknown = value - ? (() => { return asEmpty ? '' : String(value); })() + const newValue: unknown = value + ? (() => { + return asEmpty ? '' : String(value); + })() : NOT_ASSIGNED; - const currentValue:unknown = target.hasAttribute(attributeName) + const currentValue: unknown = target.hasAttribute(attributeName) ? target.getAttribute(attributeName) : NOT_ASSIGNED; diff --git a/components/textfield/test/textfield-action.test.js b/components/textfield/test/textfield-action.test.js index 0e9c3e848..2f161aada 100644 --- a/components/textfield/test/textfield-action.test.js +++ b/components/textfield/test/textfield-action.test.js @@ -2,7 +2,7 @@ import { VALID_BUTTON_ELEMENTS } from '../vwc-textfield.js'; import { waitNextTask, - textToDomToParent, + textToDomToParent, isolatedElementsCreation, } from '../../../test/test-helpers.js'; import { chaiDomDiff } from '@open-wc/semantic-dom-diff'; @@ -12,10 +12,15 @@ chai.use(chaiDomDiff); const COMPONENT_NAME = 'vwc-textfield'; describe('textfield action', () => { + const addElement = isolatedElementsCreation(); const [iconButton] = VALID_BUTTON_ELEMENTS; + function getInternalButtons(element) { + return Array.from(element.querySelectorAll(iconButton)); + } + async function createElement() { - const [actualElement] = ( + const [actualElement] = addElement( textToDomToParent(` <${COMPONENT_NAME}> <${iconButton} slot="action"> @@ -131,4 +136,78 @@ describe('textfield action', () => { await waitNextTask(); expect(newIconButton.dense).to.equal(false); }); + + describe(`noActionsSync`, function () { + function getButtonsHTML(actualElement) { + const buttons = getInternalButtons(actualElement); + const expectedButtonsHTML = buttons.reduce((html, button) => { + html += button.outerHTML; + return html; + }, ''); + return expectedButtonsHTML; + } + + function setElementAttributes(actualElement) { + actualElement.disabled = true; + actualElement.shape = 'pill'; + actualElement.toggleAttribute('dense', true); + } + + function createElementWithIconButtons() { + const [actualElement] = addElement( + textToDomToParent(` + <${COMPONENT_NAME}> + <${iconButton} slot="action" disabled shape="circled"> + <${iconButton} slot="action" shape="pilled" dense> + <${iconButton} slot="action" disabled enlarged> + + `) + ); + return actualElement; + } + + let actualElement; + + beforeEach(async function () { + actualElement = createElementWithIconButtons(); + actualElement.noActionsSync = true; + }); + + it(`should not enforce attributes on child nodes`, async function () { + const expectedButtonsHTML = getButtonsHTML(actualElement); + setElementAttributes(actualElement); + await waitNextTask(); + await actualElement.updateComplete; + + const eventualButtonsHTML = getButtonsHTML(actualElement); + expect(expectedButtonsHTML).to.equal(eventualButtonsHTML); + }); + + it(`should not dynamically enforce attributes on child nodes`, async function () { + function generateNewButton() { + const newButtonWrapper = document.createElement('div'); + newButtonWrapper.innerHTML = `<${iconButton} slot="action" shape="circled" enlarged>`; + const newButton = newButtonWrapper.firstChild; + const expectedButtonHTML = newButton.outerHTML; + return [newButton, expectedButtonHTML]; + } + + const [newButton, expectedButtonHTML] = generateNewButton(); + + const expectedButtonsHTML = getButtonsHTML(actualElement); + + setElementAttributes(actualElement); + await waitNextTask(); + await actualElement.updateComplete; + + actualElement.appendChild(newButton); + + const eventualButtonsHTML = getInternalButtons(actualElement).reduce((html, button) => { + html += button.outerHTML; + return html; + }, ''); + + expect(expectedButtonsHTML + expectedButtonHTML).to.equal(eventualButtonsHTML); + }); + }); }); From 1efc9f9140c819d926459f43190863026b3914d7 Mon Sep 17 00:00:00 2001 From: yonatankra Date: Tue, 10 Aug 2021 16:41:51 +0300 Subject: [PATCH 2/5] Fix tests for Safari --- components/textfield/test/textfield-action.test.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/components/textfield/test/textfield-action.test.js b/components/textfield/test/textfield-action.test.js index 2f161aada..3cdb9b78a 100644 --- a/components/textfield/test/textfield-action.test.js +++ b/components/textfield/test/textfield-action.test.js @@ -153,7 +153,7 @@ describe('textfield action', () => { actualElement.toggleAttribute('dense', true); } - function createElementWithIconButtons() { + async function createElementWithIconButtons() { const [actualElement] = addElement( textToDomToParent(` <${COMPONENT_NAME}> @@ -163,13 +163,14 @@ describe('textfield action', () => { `) ); + await actualElement.updateComplete; return actualElement; } let actualElement; beforeEach(async function () { - actualElement = createElementWithIconButtons(); + actualElement = await createElementWithIconButtons(); actualElement.noActionsSync = true; }); From a81f80e829d7922a5e295f3d9606702687227313 Mon Sep 17 00:00:00 2001 From: yonatankra Date: Tue, 10 Aug 2021 16:43:07 +0300 Subject: [PATCH 3/5] Add safari local testing command --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 018a3c893..2c64f0c38 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "test:chrome": "karma start --coverage --browsers=ChromeHeadless", "test:firefox": "karma start --coverage --browsers=FirefoxHeadless", "test:safari": "karma start --coverage --browsers=SafariNative", + "test:safari-local": "karma start --coverage --browsers=SafariNative --autoWatch=true --singleRun=false", "test:dev": "yarn compile && concurrently -r \"yarn watch\" \"karma start karma.conf.spec.js\"", "test:dev:ci": "yarn compile && concurrently -r \"yarn watch\" \"RUN=CI karma start karma.conf.spec.js\"", "test:update-snapshots": "karma start --update-snapshots", From 9c6363dd1d322c92e2af24d1093b3c7eea9a5e77 Mon Sep 17 00:00:00 2001 From: yonatankra Date: Wed, 11 Aug 2021 11:29:02 +0300 Subject: [PATCH 4/5] Change noSync to handle disabled state only --- components/textfield/src/vwc-textfield.ts | 7 +- .../textfield/test/textfield-action.test.js | 82 +++++++++++-------- 2 files changed, 52 insertions(+), 37 deletions(-) diff --git a/components/textfield/src/vwc-textfield.ts b/components/textfield/src/vwc-textfield.ts index db417393d..295bdceb1 100644 --- a/components/textfield/src/vwc-textfield.ts +++ b/components/textfield/src/vwc-textfield.ts @@ -252,13 +252,12 @@ export class VWCTextField extends MWCTextField { } protected enforcePropsOnActionNodes(): void { - if (this.noActionsSync) { - return; - } const buttons = Array.from(this.actionButtons || []); buttons.forEach((button) => { - button.toggleAttribute('disabled', this.disabled); + if (!this.noActionsSync) { + button.toggleAttribute('disabled', this.disabled); + } button.toggleAttribute('dense', this.dense); const buttonShape = this.shape == 'pill' diff --git a/components/textfield/test/textfield-action.test.js b/components/textfield/test/textfield-action.test.js index 3cdb9b78a..34056edc7 100644 --- a/components/textfield/test/textfield-action.test.js +++ b/components/textfield/test/textfield-action.test.js @@ -104,7 +104,9 @@ describe('textfield action', () => { actualElement.disabled = true; await waitNextTask(); - expect(newIconButton.disabled).to.equal(true); + expect(newIconButton.disabled) + .to + .equal(true); }); it(`should shape dynamically added child node's`, async function () { @@ -115,11 +117,15 @@ describe('textfield action', () => { actualElement.shape = 'rounded'; await waitNextTask(); - expect(newIconButton.shape).to.equal('rounded'); + expect(newIconButton.shape) + .to + .equal('rounded'); actualElement.shape = 'pill'; await waitNextTask(); - expect(newIconButton.shape).to.equal('circled'); + expect(newIconButton.shape) + .to + .equal('circled'); }); it(`should set density dynamically to added child node's`, async function () { @@ -130,21 +136,21 @@ describe('textfield action', () => { actualElement.dense = true; await waitNextTask(); - expect(newIconButton.dense).to.equal(true); + expect(newIconButton.dense) + .to + .equal(true); actualElement.dense = false; await waitNextTask(); - expect(newIconButton.dense).to.equal(false); + expect(newIconButton.dense) + .to + .equal(false); }); describe(`noActionsSync`, function () { - function getButtonsHTML(actualElement) { + function getButtonsDisabledStates(actualElement) { const buttons = getInternalButtons(actualElement); - const expectedButtonsHTML = buttons.reduce((html, button) => { - html += button.outerHTML; - return html; - }, ''); - return expectedButtonsHTML; + return buttons.map(button => button.disabled); } function setElementAttributes(actualElement) { @@ -153,7 +159,7 @@ describe('textfield action', () => { actualElement.toggleAttribute('dense', true); } - async function createElementWithIconButtons() { + function createElementWithIconButtons() { const [actualElement] = addElement( textToDomToParent(` <${COMPONENT_NAME}> @@ -163,52 +169,62 @@ describe('textfield action', () => { `) ); - await actualElement.updateComplete; return actualElement; } let actualElement; beforeEach(async function () { - actualElement = await createElementWithIconButtons(); + actualElement = createElementWithIconButtons(); actualElement.noActionsSync = true; + await waitNextTask(); + await actualElement.updateComplete; }); - it(`should not enforce attributes on child nodes`, async function () { - const expectedButtonsHTML = getButtonsHTML(actualElement); - setElementAttributes(actualElement); + it(`should not enforce disabled on child nodes`, async function () { + const expectedButtonsDisabled = [true, false, true]; + const buttonsDisabledBefore = getButtonsDisabledStates(actualElement); + actualElement.disabled = true; + await waitNextTask(); await actualElement.updateComplete; - const eventualButtonsHTML = getButtonsHTML(actualElement); - expect(expectedButtonsHTML).to.equal(eventualButtonsHTML); + const buttonsDisabledAfter = getButtonsDisabledStates(actualElement); + + expectedButtonsDisabled.forEach((val, index) => { + expect(val) + .to + .equal(buttonsDisabledBefore[index]); + expect(val) + .to + .equal(buttonsDisabledAfter[index]); + }); }); - it(`should not dynamically enforce attributes on child nodes`, async function () { + it(`should not dynamically enforce disabled on child nodes`, async function () { function generateNewButton() { const newButtonWrapper = document.createElement('div'); newButtonWrapper.innerHTML = `<${iconButton} slot="action" shape="circled" enlarged>`; - const newButton = newButtonWrapper.firstChild; - const expectedButtonHTML = newButton.outerHTML; - return [newButton, expectedButtonHTML]; + return newButtonWrapper.firstChild; } - const [newButton, expectedButtonHTML] = generateNewButton(); + const newButton = generateNewButton(); - const expectedButtonsHTML = getButtonsHTML(actualElement); + const expectedButtonsDisabled = [true, false, true, false]; + + actualElement.disabled = true; + actualElement.appendChild(newButton); - setElementAttributes(actualElement); await waitNextTask(); await actualElement.updateComplete; - actualElement.appendChild(newButton); - - const eventualButtonsHTML = getInternalButtons(actualElement).reduce((html, button) => { - html += button.outerHTML; - return html; - }, ''); + const buttonsDisabledAfter = getButtonsDisabledStates(actualElement); - expect(expectedButtonsHTML + expectedButtonHTML).to.equal(eventualButtonsHTML); + expectedButtonsDisabled.forEach((val, index) => { + expect(val) + .to + .equal(buttonsDisabledAfter[index]); + }); }); }); }); From 4941ac4cae34a1068ad690970fc922e3ebcd5fdc Mon Sep 17 00:00:00 2001 From: yonatankra Date: Wed, 11 Aug 2021 11:35:18 +0300 Subject: [PATCH 5/5] Delete duplication --- components/textfield/src/vwc-textfield.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/textfield/src/vwc-textfield.ts b/components/textfield/src/vwc-textfield.ts index 295bdceb1..2b197bba9 100644 --- a/components/textfield/src/vwc-textfield.ts +++ b/components/textfield/src/vwc-textfield.ts @@ -117,8 +117,6 @@ export class VWCTextField extends MWCTextField { async firstUpdated(): Promise { await super.firstUpdated(); this.shadowRoot - ?.querySelector('.mdc-notched-outline') - ?.shadowRoot ?.querySelector('.mdc-notched-outline') ?.classList .add('vvd-notch');