-
-
Notifications
You must be signed in to change notification settings - Fork 364
new TogglePassword component #1000
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
/.gitattributes export-ignore | ||
/.gitignore export-ignore | ||
/.symfony.bundle.yaml export-ignore | ||
/phpunit.xml.dist export-ignore | ||
/assets/src export-ignore | ||
/assets/test export-ignore | ||
/assets/jest.config.js export-ignore | ||
/tests export-ignore |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
vendor/ | ||
composer.lock | ||
.phpunit.result.cache |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
branches: ["2.x"] | ||
maintained_branches: ["2.x"] | ||
doc_dir: "doc" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
Copyright (c) 2023-present Fabien Potencier | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is furnished | ||
to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
THE SOFTWARE. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# Symfony UX TogglePassword | ||
|
||
Symfony UX TogglePassword is a Symfony bundle providing visibility toggle for password inputs | ||
in Symfony Forms. It is part of [the Symfony UX initiative](https://symfony.com/ux). | ||
|
||
It allows visitors to switch the type of password field to text and vice versa. | ||
|
||
**This repository is a READ-ONLY sub-tree split**. See | ||
https://github.com/symfony/ux to create issues or submit pull requests. | ||
|
||
## Sponsor | ||
|
||
The Symfony UX packages are [backed][1] by [Mercure.rocks][2]. | ||
|
||
Create real-time experiences in minutes! Mercure.rocks provides a realtime API service | ||
that is tightly integrated with Symfony: create UIs that update in live with UX Turbo, | ||
send notifications with the Notifier component, expose async APIs with API Platform and | ||
create low level stuffs with the Mercure component. We maintain and scale the complex | ||
infrastructure for you! | ||
|
||
Help Symfony by [sponsoring][3] its development! | ||
|
||
## Resources | ||
|
||
- [Documentation](https://symfony.com/bundles/ux-toggle-password/current/index.html) | ||
- [Report issues](https://github.com/symfony/ux/issues) and | ||
[send Pull Requests](https://github.com/symfony/ux/pulls) | ||
in the [main Symfony UX repository](https://github.com/symfony/ux) | ||
|
||
[1]: https://symfony.com/backers | ||
[2]: https://mercure.rocks | ||
[3]: https://symfony.com/sponsor |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { Controller } from '@hotwired/stimulus'; | ||
export default class extends Controller<HTMLInputElement> { | ||
readonly visibleLabelValue: string; | ||
readonly visibleIconValue: string; | ||
readonly hiddenLabelValue: string; | ||
readonly hiddenIconValue: string; | ||
readonly buttonClassesValue: Array<string>; | ||
static values: { | ||
visibleLabel: StringConstructor; | ||
visibleIcon: StringConstructor; | ||
hiddenLabel: StringConstructor; | ||
hiddenIcon: StringConstructor; | ||
buttonClasses: ArrayConstructor; | ||
}; | ||
isDisplayed: boolean; | ||
visibleIcon: string; | ||
hiddenIcon: string; | ||
connect(): void; | ||
private createButton; | ||
toggle(event: any): void; | ||
private dispatchEvent; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { Controller } from '@hotwired/stimulus'; | ||
|
||
class default_1 extends Controller { | ||
constructor() { | ||
super(...arguments); | ||
this.isDisplayed = false; | ||
this.visibleIcon = `<svg xmlns="http://www.w3.org/2000/svg" class="toggle-password-icon" viewBox="0 0 20 20" fill="currentColor"> | ||
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" /> | ||
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd" /> | ||
</svg>`; | ||
this.hiddenIcon = `<svg xmlns="http://www.w3.org/2000/svg" class="toggle-password-icon" viewBox="0 0 20 20" fill="currentColor"> | ||
<path fill-rule="evenodd" d="M3.707 2.293a1 1 0 00-1.414 1.414l14 14a1 1 0 001.414-1.414l-1.473-1.473A10.014 10.014 0 0019.542 10C18.268 5.943 14.478 3 10 3a9.958 9.958 0 00-4.512 1.074l-1.78-1.781zm4.261 4.26l1.514 1.515a2.003 2.003 0 012.45 2.45l1.514 1.514a4 4 0 00-5.478-5.478z" clip-rule="evenodd" /> | ||
<path d="M12.454 16.697L9.75 13.992a4 4 0 01-3.742-3.741L2.335 6.578A9.98 9.98 0 00.458 10c1.274 4.057 5.065 7 9.542 7 .847 0 1.669-.105 2.454-.303z" /> | ||
</svg>`; | ||
} | ||
connect() { | ||
if (this.visibleIconValue !== 'Default') { | ||
this.visibleIcon = this.visibleIconValue; | ||
} | ||
if (this.hiddenIconValue !== 'Default') { | ||
this.hiddenIcon = this.hiddenIconValue; | ||
} | ||
const button = this.createButton(); | ||
this.element.insertAdjacentElement('afterend', button); | ||
this.dispatchEvent('connect', { element: this.element, button: button }); | ||
} | ||
createButton() { | ||
const button = document.createElement('button'); | ||
button.type = 'button'; | ||
button.classList.add(...this.buttonClassesValue); | ||
button.setAttribute('tabindex', '-1'); | ||
button.addEventListener('click', this.toggle.bind(this)); | ||
button.innerHTML = this.visibleIcon + ' ' + this.visibleLabelValue; | ||
return button; | ||
} | ||
toggle(event) { | ||
this.isDisplayed = !this.isDisplayed; | ||
const toggleButtonElement = event.currentTarget; | ||
toggleButtonElement.innerHTML = this.isDisplayed | ||
? this.hiddenIcon + ' ' + this.hiddenLabelValue | ||
: this.visibleIcon + ' ' + this.visibleLabelValue; | ||
this.element.setAttribute('type', this.isDisplayed ? 'text' : 'password'); | ||
this.dispatchEvent(this.isDisplayed ? 'show' : 'hide', { element: this.element, button: toggleButtonElement }); | ||
} | ||
dispatchEvent(name, payload) { | ||
this.dispatch(name, { detail: payload, prefix: 'toggle-password' }); | ||
} | ||
} | ||
default_1.values = { | ||
visibleLabel: String, | ||
visibleIcon: String, | ||
hiddenLabel: String, | ||
hiddenIcon: String, | ||
buttonClasses: Array, | ||
}; | ||
|
||
export { default_1 as default }; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module.exports = require('../../../jest.config.js'); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
{ | ||
"name": "@symfony/ux-toggle-password", | ||
"description": "Toggle visibility of password inputs for Symfony Forms", | ||
"license": "MIT", | ||
"version": "1.0.0", | ||
"main": "dist/controller.js", | ||
"types": "dist/controller.d.ts", | ||
"config": { | ||
"css_source": "src/style.css" | ||
}, | ||
"symfony": { | ||
"controllers": { | ||
"toggle-password": { | ||
"main": "dist/controller.js", | ||
"fetch": "eager", | ||
"enabled": true, | ||
"autoimport": { | ||
"@symfony/ux-toggle-password/dist/style.min.css": true | ||
} | ||
} | ||
}, | ||
"importmap": { | ||
"@hotwired/stimulus": "^3.0.0" | ||
} | ||
}, | ||
"peerDependencies": { | ||
"@hotwired/stimulus": "^3.0.0" | ||
}, | ||
"devDependencies": { | ||
"@hotwired/stimulus": "^3.0.0" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
'use strict'; | ||
|
||
import { Controller } from '@hotwired/stimulus'; | ||
|
||
export default class extends Controller<HTMLInputElement> { | ||
declare readonly visibleLabelValue: string; | ||
declare readonly visibleIconValue: string; | ||
declare readonly hiddenLabelValue: string; | ||
declare readonly hiddenIconValue: string; | ||
declare readonly buttonClassesValue: Array<string>; | ||
|
||
static values = { | ||
visibleLabel: String, | ||
visibleIcon: String, | ||
hiddenLabel: String, | ||
hiddenIcon: String, | ||
buttonClasses: Array, | ||
}; | ||
|
||
isDisplayed = false; | ||
visibleIcon = `<svg xmlns="http://www.w3.org/2000/svg" class="toggle-password-icon" viewBox="0 0 20 20" fill="currentColor"> | ||
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" /> | ||
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd" /> | ||
</svg>`; | ||
hiddenIcon = `<svg xmlns="http://www.w3.org/2000/svg" class="toggle-password-icon" viewBox="0 0 20 20" fill="currentColor"> | ||
<path fill-rule="evenodd" d="M3.707 2.293a1 1 0 00-1.414 1.414l14 14a1 1 0 001.414-1.414l-1.473-1.473A10.014 10.014 0 0019.542 10C18.268 5.943 14.478 3 10 3a9.958 9.958 0 00-4.512 1.074l-1.78-1.781zm4.261 4.26l1.514 1.515a2.003 2.003 0 012.45 2.45l1.514 1.514a4 4 0 00-5.478-5.478z" clip-rule="evenodd" /> | ||
<path d="M12.454 16.697L9.75 13.992a4 4 0 01-3.742-3.741L2.335 6.578A9.98 9.98 0 00.458 10c1.274 4.057 5.065 7 9.542 7 .847 0 1.669-.105 2.454-.303z" /> | ||
</svg>`; | ||
|
||
connect() { | ||
if (this.visibleIconValue !== 'Default') { | ||
this.visibleIcon = this.visibleIconValue; | ||
} | ||
if (this.hiddenIconValue !== 'Default') { | ||
this.hiddenIcon = this.hiddenIconValue; | ||
} | ||
const button = this.createButton(); | ||
this.element.insertAdjacentElement('afterend', button); | ||
this.dispatchEvent('connect', { element: this.element, button: button }); | ||
} | ||
|
||
/** | ||
* @returns {HTMLButtonElement} | ||
*/ | ||
feymo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
private createButton(): HTMLButtonElement { | ||
const button: HTMLButtonElement = document.createElement('button'); | ||
button.type = 'button'; | ||
button.classList.add(...this.buttonClassesValue); | ||
button.setAttribute('tabindex', '-1'); | ||
button.addEventListener('click', this.toggle.bind(this)); | ||
button.innerHTML = this.visibleIcon + ' ' + this.visibleLabelValue; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you test with the registration form on ux.symfony.com, you'll notice the hide/show button will disappear after filling in the password field. That is not a problem with this code - I need to make the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the warning, i would have been really confused otherwise 🤣 |
||
|
||
return button; | ||
} | ||
|
||
/** | ||
* Toggle input type between "text" or "password" and update label accordingly | ||
*/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But this IS a good comment - it adds a bit more info :) |
||
toggle(event: any): void { | ||
this.isDisplayed = !this.isDisplayed; | ||
const toggleButtonElement: HTMLButtonElement = event.currentTarget; | ||
toggleButtonElement.innerHTML = this.isDisplayed | ||
? this.hiddenIcon + ' ' + this.hiddenLabelValue | ||
: this.visibleIcon + ' ' + this.visibleLabelValue; | ||
this.element.setAttribute('type', this.isDisplayed ? 'text' : 'password'); | ||
this.dispatchEvent(this.isDisplayed ? 'show' : 'hide', { element: this.element, button: toggleButtonElement }); | ||
} | ||
|
||
private dispatchEvent(name: string, payload: any): void { | ||
this.dispatch(name, { detail: payload, prefix: 'toggle-password' }); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -0,0 +1,22 @@ | ||||
.toggle-password-container { | ||||
position: relative; | ||||
} | ||||
.toggle-password-icon { | ||||
height: 1rem; | ||||
width: 1rem; | ||||
} | ||||
.toggle-password-button { | ||||
align-items: center; | ||||
background-color: transparent; | ||||
border: none; | ||||
column-gap: 0.25rem; | ||||
display: flex; | ||||
flex-direction: row; | ||||
font-size: 0.875rem; | ||||
justify-items: center; | ||||
height: 1rem; | ||||
line-height: 1.25rem; | ||||
position: absolute; | ||||
right: 0.5rem; | ||||
top: -1.25rem; | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is going to be the trickiest part of this PR. Can we make some CSS that works in "most" cases. I was testing locally on https://ux.symfony.com/live-component/demos/auto-validating-form I see the form theme template, which contains the I'm not convinced I have the correct solution for this. But my idea is: A) Remove the prepend and instead document the user adding the form theme manually. It kinda sucks to do this... but then the user can opt out of it. B) Document that, instead of the form theme, you can also style manually. Another idea, might be to:
The idea would be that, if -{ block('form_widget') }}
+{ block('password_widget') }} So in the event the user is overriding the Also, we should document how this all works a bit - basically communicating: A) There is a custom form theme that's activated, which wraps your widget in a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice catch ! Indeed, that's tricky but absolutely relevant !! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, done ! |
||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
'use strict'; | ||
|
||
import { Application, Controller } from '@hotwired/stimulus'; | ||
import { getByTestId, waitFor, getByText } from '@testing-library/dom'; | ||
import user from '@testing-library/user-event'; | ||
import { clearDOM, mountDOM } from '@symfony/stimulus-testing'; | ||
import TogglePasswordController from '../src/controller'; | ||
|
||
// Controller used to check the actual controller was properly booted | ||
class CheckController extends Controller { | ||
connect() { | ||
this.element.addEventListener('toggle-password:connect', () => { | ||
this.element.classList.add('connected'); | ||
}); | ||
} | ||
} | ||
|
||
const startStimulus = () => { | ||
const application = Application.start(); | ||
application.register('check', CheckController); | ||
application.register('toggle-password', TogglePasswordController); | ||
} | ||
|
||
describe('TogglePasswordController', () => { | ||
let container; | ||
|
||
beforeEach(() => { | ||
container = mountDOM(` | ||
<div class="toggle-password-container"> | ||
<input type="password" | ||
data-testid="input" | ||
data-controller="check toggle-password" | ||
data-toggle-password-hidden-label-value="Hide" | ||
data-toggle-password-visible-label-value="Show" /> | ||
</div> | ||
`); | ||
startStimulus(); | ||
}); | ||
|
||
afterEach(() => { | ||
clearDOM(); | ||
}); | ||
|
||
it('should toggle the input type', async () => { | ||
const input = getByTestId(container, 'input'); | ||
const button = getByText(container, 'Show'); | ||
|
||
expect(input.type).toBe('password'); | ||
|
||
user.click(button); | ||
|
||
await waitFor(() => { | ||
expect(input.type).toBe('text'); | ||
}); | ||
|
||
user.click(button); | ||
|
||
await waitFor(() => { | ||
expect(input.type).toBe('password'); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can remove this
config
key. I had never noticed that we had this on a few other, old packages. I believe it was added by accident there.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure, i'll remove it 👍🏼
I thought it was needed for the css file provided with the bundle !
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It probably was in a very early WIP version of UX + Flex - that's my best guess :)