diff --git a/CHANGELOG.md b/CHANGELOG.md index 512f21a90..5a775b009 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added `activated` and `selected` states for ripple - Added documentation for ripple - Prefix and suffix to mwc-textfield +- `mwc-formfield` now has a nowrap property ### Changed @@ -27,6 +28,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Refactor `mwc-ripple` - Normalized API to `start${state}` `end${state}` naming - **BREAKING:VISUAL:** mwc-list-item now internally uses mwc-ripple instead of styling ripple on host +- `mwc-menu`'s `quick` variant now opens synchronously ### Fixed diff --git a/packages/formfield/README.md b/packages/formfield/README.md index 7aecb9418..545149543 100644 --- a/packages/formfield/README.md +++ b/packages/formfield/README.md @@ -36,6 +36,26 @@ npm install @material/mwc-formfield ```html + + + + +``` + +### nowrap label with checkbox + + + +```html + + @@ -98,9 +118,10 @@ Name | Description Name | Type | Description ------- | -------- | ---------------------------------- -`label` | `string` | The text to display for the label. +`label` | `string` | The text to display for the label and sets a11y label on input. (visually overriden by slotted label) `alignEnd` | `boolean` | Align the component at the end of the label. `spaceBetween` | `boolean` | Add space between the component and the label as the formfield grows. +`nowrap` | `boolean` | Prevents the label from wrapping and overflow text is ellipsed. ### Methods diff --git a/packages/formfield/images/nowrap.png b/packages/formfield/images/nowrap.png new file mode 100644 index 000000000..03c995d50 Binary files /dev/null and b/packages/formfield/images/nowrap.png differ diff --git a/packages/formfield/src/mwc-formfield-base.ts b/packages/formfield/src/mwc-formfield-base.ts index fad60471a..9dd50aec9 100644 --- a/packages/formfield/src/mwc-formfield-base.ts +++ b/packages/formfield/src/mwc-formfield-base.ts @@ -1,31 +1,35 @@ /** -@license -Copyright 2018 Google Inc. All Rights Reserved. + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// tslint:disable:no-new-decorators -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -import {MDCFormFieldAdapter} from '@material/form-field/adapter.js'; -import MDCFormFieldFoundation from '@material/form-field/foundation.js'; -import {BaseElement, EventType, SpecificEventListener} from '@material/mwc-base/base-element.js'; -import {FormElement} from '@material/mwc-base/form-element.js'; -import {observer} from '@material/mwc-base/observer.js'; -import {findAssignedElement} from '@material/mwc-base/utils.js'; +import {MDCFormFieldAdapter} from '@material/form-field/adapter'; +import MDCFormFieldFoundation from '@material/form-field/foundation'; +import {BaseElement, EventType, SpecificEventListener} from '@material/mwc-base/base-element'; +import {FormElement} from '@material/mwc-base/form-element'; +import {observer} from '@material/mwc-base/observer'; +import {findAssignedElement} from '@material/mwc-base/utils'; import {html, property, query} from 'lit-element'; -import {classMap} from 'lit-html/directives/class-map.js'; +import {classMap} from 'lit-html/directives/class-map'; + export class FormfieldBase extends BaseElement { @property({type: Boolean}) alignEnd = false; @property({type: Boolean}) spaceBetween = false; + @property({type: Boolean}) nowrap = false; @property({type: String}) @observer(async function(this: FormfieldBase, label: string) { @@ -93,7 +97,8 @@ export class FormfieldBase extends BaseElement { protected render() { const classes = { 'mdc-form-field--align-end': this.alignEnd, - 'mdc-form-field--space-between': this.spaceBetween + 'mdc-form-field--space-between': this.spaceBetween, + 'mdc-form-field--nowrap': this.nowrap }; return html` diff --git a/packages/formfield/src/mwc-formfield.scss b/packages/formfield/src/mwc-formfield.scss index 991ddc0fe..4dd851052 100644 --- a/packages/formfield/src/mwc-formfield.scss +++ b/packages/formfield/src/mwc-formfield.scss @@ -15,7 +15,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -@import '@material/form-field/mdc-form-field.scss'; +@use '@material/form-field' as formfield; +@use '@material/typography'; +@use '@material/theme'; +@use '@material/rtl'; + +@include formfield.core-styles(); :host { display: inline-flex; @@ -26,13 +31,13 @@ limitations under the License. } ::slotted(*) { - @include mdc-typography(body2); - @include mdc-theme-prop(color, text-primary-on-background); + @include typography.typography(body2); + @include theme.prop(color, text-primary-on-background); } ::slotted(mwc-switch) { margin-right: 10px; - @include mdc-rtl { + @include rtl.rtl { margin-left: 10px; } } diff --git a/packages/formfield/src/test/mwc-formfield.test.ts b/packages/formfield/src/test/mwc-formfield.test.ts index bc8161ca8..87eb061ad 100644 --- a/packages/formfield/src/test/mwc-formfield.test.ts +++ b/packages/formfield/src/test/mwc-formfield.test.ts @@ -15,21 +15,258 @@ * limitations under the License. */ -import {Formfield} from '@material/mwc-formfield'; +import '@material/mwc-formfield'; +import '@material/mwc-checkbox'; +import '@material/mwc-radio'; +import '@material/mwc-switch'; +import {Checkbox} from '@material/mwc-checkbox'; +import {Formfield} from '@material/mwc-formfield'; +import {Radio} from '@material/mwc-radio'; +import {Switch} from '@material/mwc-switch'; +import {html} from 'lit-html'; + +import {fixture, TestFixture} from '../../../../test/src/util/helpers'; + +const defaultEl = html``; + +const defaultFormfieldProps = { + alignEnd: false, + spaceBetween: false, + label: '', + content: html``, +}; + +type FormfieldProps = typeof defaultFormfieldProps; + +const formfield = (propsInit: Partial) => { + const props: FormfieldProps = {...defaultFormfieldProps, ...propsInit}; + + return html` + + ${props.content} + + `; +}; + suite('mwc-formfield', () => { + let fixt: TestFixture; let element: Formfield; - setup(() => { - element = document.createElement('mwc-formfield'); - document.body.appendChild(element); - }); teardown(() => { - document.body.removeChild(element); + fixt.remove(); }); - test('initializes as an mwc-formfield', () => { - assert.instanceOf(element, Formfield); + suite('basic', () => { + setup(async () => { + fixt = await fixture(defaultEl); + element = fixt.root.querySelector('mwc-formfield')!; + }); + + test('initializes as an mwc-formfield', () => { + assert.instanceOf(element, Formfield); + assert.isFalse(element.alignEnd); + assert.isFalse(element.spaceBetween); + assert.equal(element.label, ''); + }); + }); + + suite('with checkbox', () => { + let control: Checkbox; + + suite('prop label', () => { + setup(async () => { + fixt = await fixture(formfield( + {label: 'label', content: html``})); + element = fixt.root.querySelector('mwc-formfield')!; + await element.updateComplete; + control = fixt.root.querySelector('mwc-checkbox')!; + await control.updateComplete; + }); + + test('sets the aria-label on the control', async () => { + const internalInput = control.shadowRoot!.querySelector('input')!; + assert.equal(internalInput.getAttribute('aria-label'), 'label'); + }); + + test('label click propagates click and focus to control', async () => { + const labelEl = element.shadowRoot!.querySelector('label')!; + let numClicks = 0; + const origClick = control.click; + + control.click = () => { + numClicks += 1; + origClick.call(control); + }; + + assert.isFalse(control.checked); + assert.equal(fixt.shadowRoot!.activeElement, null); + assert.equal(numClicks, 0); + + labelEl.click(); + + await element.updateComplete; + await control.updateComplete; + + assert.isTrue(control.checked); + assert.equal(fixt.shadowRoot!.activeElement, control); + assert.equal(numClicks, 1); + }); + + test('formfield will not double click control', async () => { + let numClicks = 0; + const origClick = control.click; + + control.click = () => { + numClicks += 1; + origClick.call(control); + }; + + assert.isFalse(control.checked); + assert.equal(numClicks, 0); + + control.click(); + + await element.updateComplete; + await control.updateComplete; + + assert.equal(numClicks, 1); + assert.isTrue(control.checked); + }); + }); + }); + + suite('with switch', () => { + let control: Switch; + + suite('prop label', () => { + setup(async () => { + fixt = await fixture(formfield( + {label: 'label', content: html``})); + element = fixt.root.querySelector('mwc-formfield')!; + await element.updateComplete; + control = fixt.root.querySelector('mwc-switch')!; + await control.updateComplete; + }); + + test('sets the aria-label on the control', async () => { + const internalInput = control.shadowRoot!.querySelector('input')!; + assert.equal(internalInput.getAttribute('aria-label'), 'label'); + }); + + test('label click propagates click and focus to control', async () => { + const labelEl = element.shadowRoot!.querySelector('label')!; + let numClicks = 0; + const origClick = control.click; + + control.click = () => { + numClicks += 1; + origClick.call(control); + }; + + assert.isFalse(control.checked); + assert.equal(fixt.shadowRoot!.activeElement, null); + assert.equal(numClicks, 0); + + labelEl.click(); + + await element.updateComplete; + await control.updateComplete; + + assert.isTrue(control.checked); + assert.equal(fixt.shadowRoot!.activeElement, control); + assert.equal(numClicks, 1); + }); + + test('formfield will not double click control', async () => { + let numClicks = 0; + const origClick = control.click; + + control.click = () => { + numClicks += 1; + origClick.call(control); + }; + + assert.isFalse(control.checked); + assert.equal(numClicks, 0); + + control.click(); + + await element.updateComplete; + await control.updateComplete; + + assert.equal(numClicks, 1); + assert.isTrue(control.checked); + }); + }); + }); + + suite('with radio', () => { + let control: Radio; + + suite('prop label', () => { + setup(async () => { + fixt = await fixture(formfield( + {label: 'label', content: html``})); + element = fixt.root.querySelector('mwc-formfield')!; + await element.updateComplete; + control = fixt.root.querySelector('mwc-radio')!; + await control.updateComplete; + }); + + test('sets the aria-label on the control', async () => { + const internalInput = control.shadowRoot!.querySelector('input')!; + assert.equal(internalInput.getAttribute('aria-label'), 'label'); + }); + + test('label click propagates click and focus to control', async () => { + const labelEl = element.shadowRoot!.querySelector('label')!; + let numClicks = 0; + const origClick = control.click; + + control.click = () => { + numClicks += 1; + origClick.call(control); + }; + + assert.isFalse(control.checked); + assert.equal(fixt.shadowRoot!.activeElement, null); + assert.equal(numClicks, 0); + + labelEl.click(); + + await element.updateComplete; + await control.updateComplete; + + assert.isTrue(control.checked); + assert.equal(fixt.shadowRoot!.activeElement, control); + assert.equal(numClicks, 1); + }); + + test('formfield will not double click control', async () => { + let numClicks = 0; + const origClick = control.click; + + control.click = () => { + numClicks += 1; + origClick.call(control); + }; + + assert.isFalse(control.checked); + assert.equal(numClicks, 0); + + control.click(); + + await element.updateComplete; + await control.updateComplete; + + assert.equal(numClicks, 1); + assert.isTrue(control.checked); + }); + }); }); });