diff --git a/packages/password-field/LICENSE b/packages/password-field/LICENSE new file mode 100644 index 00000000000..b577ea38081 --- /dev/null +++ b/packages/password-field/LICENSE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2021 Vaadin Ltd. + + 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. diff --git a/packages/password-field/README.md b/packages/password-field/README.md new file mode 100644 index 00000000000..402430e2c41 --- /dev/null +++ b/packages/password-field/README.md @@ -0,0 +1,5 @@ +# @vaadin/password-field + +> ⚠️ Work in progress, please do not use this component yet. + +The new version of `vaadin-password-field` component using slotted ``. diff --git a/packages/password-field/package.json b/packages/password-field/package.json new file mode 100644 index 00000000000..95bf333968b --- /dev/null +++ b/packages/password-field/package.json @@ -0,0 +1,40 @@ +{ + "name": "@vaadin/password-field", + "version": "22.0.0-alpha1", + "description": "vaadin-password-field", + "main": "vaadin-password-field.js", + "module": "vaadin-password-field.js", + "repository": "vaadin/web-components", + "keywords": [ + "Vaadin", + "input", + "web-components", + "web-component" + ], + "author": "Vaadin Ltd", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/vaadin/web-components/issues" + }, + "homepage": "https://vaadin.com/components", + "files": [ + "vaadin-*.d.ts", + "vaadin-*.js", + "src", + "theme" + ], + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/text-field": "^22.0.0-alpha1", + "@vaadin/vaadin-lumo-styles": "^22.0.0-alpha1", + "@vaadin/vaadin-material-styles": "^22.0.0-alpha1" + }, + "devDependencies": { + "@esm-bundle/chai": "^4.1.5", + "@vaadin/testing-helpers": "^0.2.1", + "sinon": "^9.2.1" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/password-field/src/vaadin-password-field.d.ts b/packages/password-field/src/vaadin-password-field.d.ts new file mode 100644 index 00000000000..db769293370 --- /dev/null +++ b/packages/password-field/src/vaadin-password-field.d.ts @@ -0,0 +1,22 @@ +/** + * @license + * Copyright (c) 2021 Vaadin Ltd. + * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ + */ +import { TextField } from '@vaadin/text-field/src/vaadin-text-field.js'; + +declare class PasswordField extends TextField { + /** + * Set to true to hide the eye icon which toggles the password visibility. + * @attr {boolean} reveal-button-hidden + */ + revealButtonHidden: boolean; + + /** + * True if the password is visible ([type=text]). + * @attr {boolean} password-visible + */ + readonly passwordVisible: boolean; +} + +export { PasswordField }; diff --git a/packages/password-field/src/vaadin-password-field.js b/packages/password-field/src/vaadin-password-field.js new file mode 100644 index 00000000000..2bc9c0ffde6 --- /dev/null +++ b/packages/password-field/src/vaadin-password-field.js @@ -0,0 +1,214 @@ +/** + * @license + * Copyright (c) 2021 Vaadin Ltd. + * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ + */ +import { html } from '@polymer/polymer/lib/utils/html-tag.js'; +import { TextField } from '@vaadin/text-field/src/vaadin-text-field.js'; + +const ownTemplate = html` +
+ +
+`; + +let memoizedTemplate; + +export class PasswordField extends TextField { + static get is() { + return 'vaadin-password-field'; + } + + static get version() { + return '22.0.0-alpha1'; + } + + static get template() { + if (!memoizedTemplate) { + // Clone the superclass template + memoizedTemplate = super.template.cloneNode(true); + + // Retrieve this element's dom-module template + const revealButton = ownTemplate.content.querySelector('[part="reveal-button"]'); + + // Append reveal-button and styles to the text-field template + const inputField = memoizedTemplate.content.querySelector('[part="input-field"]'); + inputField.appendChild(revealButton); + } + + return memoizedTemplate; + } + + static get properties() { + return { + /** + * Set to true to hide the eye icon which toggles the password visibility. + * @attr {boolean} reveal-button-hidden + */ + revealButtonHidden: { + type: Boolean, + observer: '_revealButtonHiddenChanged', + value: false + }, + + /** + * True if the password is visible ([type=text]). + * @attr {boolean} password-visible + */ + passwordVisible: { + type: Boolean, + value: false, + reflectToAttribute: true, + observer: '_passwordVisibleChanged', + readOnly: true + } + }; + } + + get slots() { + return { + ...super.slots, + reveal: () => { + const btn = document.createElement('button'); + btn.setAttribute('type', 'button'); + btn.setAttribute('tabindex', '0'); + return btn; + } + }; + } + + /** @protected */ + get _revealNode() { + return this._getDirectSlotChild('reveal'); + } + + constructor() { + super(); + this._setType('password'); + this.__boundRevealButtonClick = this._onRevealButtonClick.bind(this); + this.__boundRevealButtonTouchend = this._onRevealButtonTouchend.bind(this); + } + + /** @protected */ + ready() { + super.ready(); + + this._revealPart = this.shadowRoot.querySelector('[part="reveal-button"]'); + } + + /** @protected */ + connectedCallback() { + super.connectedCallback(); + + if (this._revealNode) { + this._revealNode.setAttribute('aria-label', 'Show password'); + this._revealNode.addEventListener('click', this.__boundRevealButtonClick); + this._revealNode.addEventListener('touchend', this.__boundRevealButtonTouchend); + } + + if (this._inputNode) { + this._inputNode.autocapitalize = 'off'; + } + + this._toggleRevealHidden(this.revealButtonHidden); + this._updateToggleState(false); + } + + /** @protected */ + disconnectedCallback() { + super.disconnectedCallback(); + + if (this._revealNode) { + this._revealNode.removeEventListener('click', this.__boundRevealButtonClick); + this._revealNode.removeEventListener('touchend', this.__boundRevealButtonTouchend); + } + } + + /** + * Override method inherited from `FocusMixin` to mark field as focused + * when focus moves to the reveal button using Shift Tab. + * @param {Event} event + * @return {boolean} + * @protected + */ + _shouldSetFocus(event) { + return event.target === this._inputNode || event.target === this._revealNode; + } + + /** + * Override method inherited from `FocusMixin` to not hide password + * when focus moves to the reveal button or back to the input. + * @param {Event} event + * @return {boolean} + * @protected + */ + _shouldRemoveFocus(event) { + return !( + event.relatedTarget === this._revealNode || + (event.relatedTarget === this._inputNode && event.target === this._revealNode) + ); + } + + /** @protected */ + _setFocused(focused) { + super._setFocused(focused); + + if (!focused) { + this._setPasswordVisible(false); + } + } + + /** @private */ + _revealButtonHiddenChanged(hidden) { + this._toggleRevealHidden(hidden); + } + + /** @private */ + _togglePasswordVisibility() { + this._setPasswordVisible(!this.passwordVisible); + } + + /** @private */ + _onRevealButtonClick() { + this._togglePasswordVisibility(); + } + + /** @private */ + _onRevealButtonTouchend(e) { + // Cancel the following click event + e.preventDefault(); + this._togglePasswordVisibility(); + // Focus the input to avoid problem with password still visible + // when user clicks the reveal button and then clicks outside. + this._inputNode.focus(); + } + + /** @private */ + _toggleRevealHidden(hidden) { + if (this._revealNode) { + if (hidden) { + this._revealPart.setAttribute('hidden', ''); + this._revealNode.setAttribute('tabindex', '-1'); + this._revealNode.setAttribute('aria-hidden', 'true'); + } else { + this._revealPart.removeAttribute('hidden'); + this._revealNode.setAttribute('tabindex', '0'); + this._revealNode.removeAttribute('aria-hidden'); + } + } + } + + /** @private */ + _updateToggleState(passwordVisible) { + if (this._revealNode) { + this._revealNode.setAttribute('aria-pressed', passwordVisible ? 'true' : 'false'); + } + } + + /** @private */ + _passwordVisibleChanged(passwordVisible) { + this._setType(passwordVisible ? 'text' : 'password'); + + this._updateToggleState(passwordVisible); + } +} diff --git a/packages/password-field/test/.eslintrc.json b/packages/password-field/test/.eslintrc.json new file mode 100644 index 00000000000..7eeefc33b66 --- /dev/null +++ b/packages/password-field/test/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "env": { + "mocha": true + } +} diff --git a/packages/password-field/test/password-field.test.js b/packages/password-field/test/password-field.test.js new file mode 100644 index 00000000000..6cc525f9d1f --- /dev/null +++ b/packages/password-field/test/password-field.test.js @@ -0,0 +1,140 @@ +import { expect } from '@esm-bundle/chai'; +import sinon from 'sinon'; +import { fixtureSync, focusin, focusout, tabKeyDown } from '@vaadin/testing-helpers'; +import { PasswordField } from '../src/vaadin-password-field.js'; + +customElements.define('vaadin-password-field', PasswordField); + +describe('password-field', () => { + let passwordField, input, revealButton; + + beforeEach(() => { + passwordField = fixtureSync(''); + input = passwordField._inputNode; + revealButton = passwordField.querySelector('[slot=reveal]'); + }); + + it('should have [type=password]', () => { + expect(input.type).to.equal('password'); + }); + + it('should show reveal button by default', () => { + expect(revealButton.hidden).to.be.false; + }); + + it('should reveal the password on reveal button click', () => { + revealButton.click(); + expect(input.type).to.equal('text'); + + revealButton.click(); + expect(input.type).to.equal('password'); + }); + + it('should hide the password on field focusout', () => { + passwordField.focus(); + revealButton.click(); + expect(input.type).to.equal('text'); + + focusout(passwordField); + expect(input.type).to.equal('password'); + }); + + it('should not hide the password when focus moves to reveal button', () => { + passwordField.focus(); + revealButton.click(); + + focusout(passwordField, revealButton); + expect(input.type).to.equal('text'); + }); + + it('should not hide the password when focus moves back to the input', () => { + revealButton.focus(); + revealButton.click(); + + focusout(revealButton, input); + expect(input.type).to.equal('text'); + }); + + it('should set focus-ring attribute when focusing the input with Tab', () => { + tabKeyDown(document.body); + focusin(input); + + expect(passwordField.hasAttribute('focus-ring')).to.be.true; + }); + + it('should set focus-ring attribute when focusing reveal button with Shift Tab', () => { + tabKeyDown(document.body, ['shift']); + focusin(revealButton); + + expect(passwordField.hasAttribute('focus-ring')).to.be.true; + }); + + it('should prevent touchend event on reveal button', () => { + const e = new CustomEvent('touchend', { cancelable: true }); + + revealButton.dispatchEvent(e); + expect(e.defaultPrevented).to.be.true; + expect(input.type).to.equal('text'); + + revealButton.dispatchEvent(e); + expect(e.defaultPrevented).to.be.true; + expect(input.type).to.equal('password'); + }); + + it('should focus the input on reveal button touchend', () => { + const spy = sinon.spy(input, 'focus'); + + const e = new CustomEvent('touchend', { cancelable: true }); + revealButton.dispatchEvent(e); + + expect(spy.calledOnce).to.be.true; + }); + + it('should set aria-pressed attribute on reveal button to false', () => { + expect(revealButton.getAttribute('aria-pressed')).to.equal('false'); + }); + + it('should toggle aria-pressed attribute on reveal button click', () => { + revealButton.click(); + expect(revealButton.getAttribute('aria-pressed')).to.equal('true'); + + revealButton.click(); + expect(revealButton.getAttribute('aria-pressed')).to.equal('false'); + }); + + describe('revealButtonHidden', () => { + let revealPart; + + beforeEach(() => { + revealPart = passwordField.shadowRoot.querySelector('[part="reveal-button"]'); + passwordField.revealButtonHidden = true; + }); + + it('should hide reveal part when revealButtonHidden is set to true', () => { + expect(revealPart.hidden).to.be.true; + }); + + it('should set tabindex to -1 when revealButtonHidden set to true', () => { + expect(revealButton.tabIndex).to.equal(-1); + }); + + it('should set aria-hidden attribute when revealButtonHidden set to true', () => { + expect(revealButton.getAttribute('aria-hidden')).to.equal('true'); + }); + + it('should show reveal part when revealButtonHidden is set to false', () => { + passwordField.revealButtonHidden = false; + expect(revealPart.hidden).to.be.false; + }); + + it('should reset tabindex when revealButtonHidden is set to false', () => { + passwordField.revealButtonHidden = false; + expect(revealButton.tabIndex).to.equal(0); + }); + + it('should remove aria-hidden attribute when revealButtonHidden set to false', () => { + passwordField.revealButtonHidden = false; + expect(revealButton.hasAttribute('aria-hidden')).to.be.false; + }); + }); +}); diff --git a/packages/password-field/test/visual/lumo/password-field.test.js b/packages/password-field/test/visual/lumo/password-field.test.js new file mode 100644 index 00000000000..f568ee1aeed --- /dev/null +++ b/packages/password-field/test/visual/lumo/password-field.test.js @@ -0,0 +1,50 @@ +import { fixtureSync } from '@vaadin/testing-helpers/dist/fixture.js'; +import { visualDiff } from '@web/test-runner-visual-regression'; +import '../../../theme/lumo/vaadin-password-field.js'; +import { PasswordField } from '../../../src/vaadin-password-field.js'; + +customElements.define('vaadin-password-field', PasswordField); + +describe('password-field', () => { + let div, element; + + beforeEach(() => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + element = fixtureSync('', div); + }); + + ['ltr', 'rtl'].forEach((dir) => { + describe(dir, () => { + before(() => { + document.documentElement.setAttribute('dir', dir); + }); + + after(() => { + document.documentElement.removeAttribute('dir'); + }); + + it('basic', async () => { + await visualDiff(div, `${import.meta.url}_${dir}-basic`); + }); + + it('value', async () => { + element.value = 'value'; + await visualDiff(div, `${import.meta.url}_${dir}-value`); + }); + + it('clear button', async () => { + element.value = 'value'; + element.clearButtonVisible = true; + await visualDiff(div, `${import.meta.url}_${dir}-clear-button`); + }); + + it('reveal button hidden', async () => { + element.value = 'value'; + element.revealButtonHidden = true; + await visualDiff(div, `${import.meta.url}_${dir}-reveal-button-hidden`); + }); + }); + }); +}); diff --git a/packages/password-field/test/visual/lumo/screenshots/password-field/baseline/ltr-basic.png b/packages/password-field/test/visual/lumo/screenshots/password-field/baseline/ltr-basic.png new file mode 100644 index 00000000000..62e63ee2813 Binary files /dev/null and b/packages/password-field/test/visual/lumo/screenshots/password-field/baseline/ltr-basic.png differ diff --git a/packages/password-field/test/visual/lumo/screenshots/password-field/baseline/ltr-clear-button.png b/packages/password-field/test/visual/lumo/screenshots/password-field/baseline/ltr-clear-button.png new file mode 100644 index 00000000000..3830cf98af9 Binary files /dev/null and b/packages/password-field/test/visual/lumo/screenshots/password-field/baseline/ltr-clear-button.png differ diff --git a/packages/password-field/test/visual/lumo/screenshots/password-field/baseline/ltr-reveal-button-hidden.png b/packages/password-field/test/visual/lumo/screenshots/password-field/baseline/ltr-reveal-button-hidden.png new file mode 100644 index 00000000000..2171140bfb4 Binary files /dev/null and b/packages/password-field/test/visual/lumo/screenshots/password-field/baseline/ltr-reveal-button-hidden.png differ diff --git a/packages/password-field/test/visual/lumo/screenshots/password-field/baseline/ltr-value.png b/packages/password-field/test/visual/lumo/screenshots/password-field/baseline/ltr-value.png new file mode 100644 index 00000000000..0aa6cad5348 Binary files /dev/null and b/packages/password-field/test/visual/lumo/screenshots/password-field/baseline/ltr-value.png differ diff --git a/packages/password-field/test/visual/lumo/screenshots/password-field/baseline/rtl-basic.png b/packages/password-field/test/visual/lumo/screenshots/password-field/baseline/rtl-basic.png new file mode 100644 index 00000000000..f4b2c3397c1 Binary files /dev/null and b/packages/password-field/test/visual/lumo/screenshots/password-field/baseline/rtl-basic.png differ diff --git a/packages/password-field/test/visual/lumo/screenshots/password-field/baseline/rtl-clear-button.png b/packages/password-field/test/visual/lumo/screenshots/password-field/baseline/rtl-clear-button.png new file mode 100644 index 00000000000..6c8e10821af Binary files /dev/null and b/packages/password-field/test/visual/lumo/screenshots/password-field/baseline/rtl-clear-button.png differ diff --git a/packages/password-field/test/visual/lumo/screenshots/password-field/baseline/rtl-reveal-button-hidden.png b/packages/password-field/test/visual/lumo/screenshots/password-field/baseline/rtl-reveal-button-hidden.png new file mode 100644 index 00000000000..47bab552e7a Binary files /dev/null and b/packages/password-field/test/visual/lumo/screenshots/password-field/baseline/rtl-reveal-button-hidden.png differ diff --git a/packages/password-field/test/visual/lumo/screenshots/password-field/baseline/rtl-value.png b/packages/password-field/test/visual/lumo/screenshots/password-field/baseline/rtl-value.png new file mode 100644 index 00000000000..dfa62a738dd Binary files /dev/null and b/packages/password-field/test/visual/lumo/screenshots/password-field/baseline/rtl-value.png differ diff --git a/packages/password-field/test/visual/material/password-field.test.js b/packages/password-field/test/visual/material/password-field.test.js new file mode 100644 index 00000000000..b0bc9528304 --- /dev/null +++ b/packages/password-field/test/visual/material/password-field.test.js @@ -0,0 +1,50 @@ +import { fixtureSync } from '@vaadin/testing-helpers/dist/fixture.js'; +import { visualDiff } from '@web/test-runner-visual-regression'; +import '../../../theme/material/vaadin-password-field.js'; +import { PasswordField } from '../../../src/vaadin-password-field.js'; + +customElements.define('vaadin-password-field', PasswordField); + +describe('password-field', () => { + let div, element; + + beforeEach(() => { + div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.padding = '10px'; + element = fixtureSync('', div); + }); + + ['ltr', 'rtl'].forEach((dir) => { + describe(dir, () => { + before(() => { + document.documentElement.setAttribute('dir', dir); + }); + + after(() => { + document.documentElement.removeAttribute('dir'); + }); + + it('basic', async () => { + await visualDiff(div, `${import.meta.url}_${dir}-basic`); + }); + + it('value', async () => { + element.value = 'value'; + await visualDiff(div, `${import.meta.url}_${dir}-value`); + }); + + it('clear button', async () => { + element.value = 'value'; + element.clearButtonVisible = true; + await visualDiff(div, `${import.meta.url}_${dir}-clear-button`); + }); + + it('reveal button hidden', async () => { + element.value = 'value'; + element.revealButtonHidden = true; + await visualDiff(div, `${import.meta.url}_${dir}-reveal-button-hidden`); + }); + }); + }); +}); diff --git a/packages/password-field/test/visual/material/screenshots/password-field/baseline/ltr-basic.png b/packages/password-field/test/visual/material/screenshots/password-field/baseline/ltr-basic.png new file mode 100644 index 00000000000..16a3e637e36 Binary files /dev/null and b/packages/password-field/test/visual/material/screenshots/password-field/baseline/ltr-basic.png differ diff --git a/packages/password-field/test/visual/material/screenshots/password-field/baseline/ltr-clear-button.png b/packages/password-field/test/visual/material/screenshots/password-field/baseline/ltr-clear-button.png new file mode 100644 index 00000000000..75067de67a9 Binary files /dev/null and b/packages/password-field/test/visual/material/screenshots/password-field/baseline/ltr-clear-button.png differ diff --git a/packages/password-field/test/visual/material/screenshots/password-field/baseline/ltr-reveal-button-hidden.png b/packages/password-field/test/visual/material/screenshots/password-field/baseline/ltr-reveal-button-hidden.png new file mode 100644 index 00000000000..16ee061d2fe Binary files /dev/null and b/packages/password-field/test/visual/material/screenshots/password-field/baseline/ltr-reveal-button-hidden.png differ diff --git a/packages/password-field/test/visual/material/screenshots/password-field/baseline/ltr-value.png b/packages/password-field/test/visual/material/screenshots/password-field/baseline/ltr-value.png new file mode 100644 index 00000000000..9205d9b0597 Binary files /dev/null and b/packages/password-field/test/visual/material/screenshots/password-field/baseline/ltr-value.png differ diff --git a/packages/password-field/test/visual/material/screenshots/password-field/baseline/rtl-basic.png b/packages/password-field/test/visual/material/screenshots/password-field/baseline/rtl-basic.png new file mode 100644 index 00000000000..df254737b67 Binary files /dev/null and b/packages/password-field/test/visual/material/screenshots/password-field/baseline/rtl-basic.png differ diff --git a/packages/password-field/test/visual/material/screenshots/password-field/baseline/rtl-clear-button.png b/packages/password-field/test/visual/material/screenshots/password-field/baseline/rtl-clear-button.png new file mode 100644 index 00000000000..9726c85c9e6 Binary files /dev/null and b/packages/password-field/test/visual/material/screenshots/password-field/baseline/rtl-clear-button.png differ diff --git a/packages/password-field/test/visual/material/screenshots/password-field/baseline/rtl-reveal-button-hidden.png b/packages/password-field/test/visual/material/screenshots/password-field/baseline/rtl-reveal-button-hidden.png new file mode 100644 index 00000000000..fce9fcee685 Binary files /dev/null and b/packages/password-field/test/visual/material/screenshots/password-field/baseline/rtl-reveal-button-hidden.png differ diff --git a/packages/password-field/test/visual/material/screenshots/password-field/baseline/rtl-value.png b/packages/password-field/test/visual/material/screenshots/password-field/baseline/rtl-value.png new file mode 100644 index 00000000000..6dd0d3f2fe3 Binary files /dev/null and b/packages/password-field/test/visual/material/screenshots/password-field/baseline/rtl-value.png differ diff --git a/packages/password-field/theme/lumo/vaadin-password-field-styles.js b/packages/password-field/theme/lumo/vaadin-password-field-styles.js new file mode 100644 index 00000000000..528a55278d8 --- /dev/null +++ b/packages/password-field/theme/lumo/vaadin-password-field-styles.js @@ -0,0 +1,43 @@ +/** + * @license + * Copyright (c) 2021 Vaadin Ltd. + * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ + */ +import { registerStyles, css } from '@vaadin/vaadin-themable-mixin/register-styles.js'; +import '@vaadin/vaadin-lumo-styles/font-icons.js'; +import '@vaadin/vaadin-lumo-styles/sizing.js'; + +registerStyles( + 'vaadin-password-field', + css` + [part='reveal-button']::before { + content: var(--lumo-icons-eye); + } + + :host([password-visible]) [part='reveal-button']::before { + content: var(--lumo-icons-eye-disabled); + } + + /* Make it easy to hide the button across the whole app */ + [part='reveal-button'] { + position: relative; + display: var(--lumo-password-field-reveal-button-display, block); + } + + [part='reveal-button'][hidden] { + display: none !important; + } + + ::slotted([slot='reveal']) { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + width: 100%; + background: transparent; + border: none; + } + `, + { moduleId: 'lumo-password-field' } +); diff --git a/packages/password-field/theme/lumo/vaadin-password-field.js b/packages/password-field/theme/lumo/vaadin-password-field.js new file mode 100644 index 00000000000..cb7c1a66835 --- /dev/null +++ b/packages/password-field/theme/lumo/vaadin-password-field.js @@ -0,0 +1,8 @@ +/** + * @license + * Copyright (c) 2021 Vaadin Ltd. + * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ + */ +import '@vaadin/text-field/theme/lumo/vaadin-text-field.js'; +import './vaadin-password-field-styles.js'; +import '../../src/vaadin-password-field.js'; diff --git a/packages/password-field/theme/material/vaadin-password-field-styles.js b/packages/password-field/theme/material/vaadin-password-field-styles.js new file mode 100644 index 00000000000..9bb79cf0d5d --- /dev/null +++ b/packages/password-field/theme/material/vaadin-password-field-styles.js @@ -0,0 +1,50 @@ +/** + * @license + * Copyright (c) 2021 Vaadin Ltd. + * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ + */ +import { registerStyles, css } from '@vaadin/vaadin-themable-mixin/register-styles.js'; +import '@vaadin/vaadin-material-styles/font-icons.js'; + +registerStyles( + 'vaadin-password-field', + css` + [part='reveal-button']::before { + content: var(--material-icons-eye); + } + + :host([password-visible]) [part='reveal-button']::before { + content: var(--material-icons-eye-disabled); + } + + /* The reveal button works also in readonly mode */ + :host([readonly]) [part$='button'] { + color: var(--material-secondary-text-color); + } + + [part='reveal-button'] { + position: relative; + cursor: pointer; + } + + [part='reveal-button']:hover { + color: var(--material-text-color); + } + + [part='reveal-button'][hidden] { + display: none !important; + } + + ::slotted([slot='reveal']) { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + width: 100%; + background: transparent; + border: none; + } + `, + { moduleId: 'material-password-field' } +); diff --git a/packages/password-field/theme/material/vaadin-password-field.js b/packages/password-field/theme/material/vaadin-password-field.js new file mode 100644 index 00000000000..8d7e8a056f6 --- /dev/null +++ b/packages/password-field/theme/material/vaadin-password-field.js @@ -0,0 +1,8 @@ +/** + * @license + * Copyright (c) 2021 Vaadin Ltd. + * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ + */ +import '@vaadin/text-field/theme/material/vaadin-text-field.js'; +import './vaadin-password-field-styles.js'; +import '../../src/vaadin-password-field.js'; diff --git a/packages/password-field/vaadin-password-field.d.ts b/packages/password-field/vaadin-password-field.d.ts new file mode 100644 index 00000000000..0e8f1871f24 --- /dev/null +++ b/packages/password-field/vaadin-password-field.d.ts @@ -0,0 +1 @@ +export { PasswordField } from './src/vaadin-password-field.js'; diff --git a/packages/password-field/vaadin-password-field.js b/packages/password-field/vaadin-password-field.js new file mode 100644 index 00000000000..60858da88f9 --- /dev/null +++ b/packages/password-field/vaadin-password-field.js @@ -0,0 +1,3 @@ +import './theme/lumo/vaadin-password-field.js'; + +export { PasswordField } from './src/vaadin-password-field.js';