Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 13.3k
feat(input): add input-password-toggle component#29175
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
86356505ac8d29de678b5fc3837fd9a2468d3cef3d4ae25c238e66cfa7f006b94a8ac32efcddcfba9b5f33076f64587cc200960a2d57051478e4fc2a9c72f428abf983273a1d6238237a08df087e66c47b87c6e937aa257e22fc9c4cc81aca2e25f8c61aa1fa36134d90e62e3b8c22c24bcc59ff145bc8ecefeba243fb2b702510560e61ec83d2423ab75160f0aa12b14a33bbe7248cf63521866d1a328c4e094d2ec9bd0File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| import type { ComponentInterface } from '@stencil/core'; | ||
| import { Component, Element, Host, Prop, h, Watch } from '@stencil/core'; | ||
| import { printIonWarning } from '@utils/logging'; | ||
| import { createColorClasses } from '@utils/theme'; | ||
| import { eyeOff, eye } from 'ionicons/icons'; | ||
| import { getIonMode } from '../../global/ionic-global'; | ||
| import type { Color, TextFieldTypes } from '../../interface'; | ||
| /** | ||
| * @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use. | ||
| */ | ||
| @Component({ | ||
| tag: 'ion-input-password-toggle', | ||
liamdebeasi marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| /** | ||
| * Empty CSS files are required in order for the mode to be inherited to the | ||
| * inner ion-button. Otherwise, the setMode callback provided to Stencil will not get called | ||
| * and we will default to MD mode. | ||
| */ | ||
| styleUrls: { | ||
| ios: 'input-password-toggle.scss', | ||
| md: 'input-password-toggle.scss', | ||
| }, | ||
| shadow: true, | ||
| }) | ||
| export class InputPasswordToggle implements ComponentInterface { | ||
| private inputElRef!: HTMLIonInputElement | null; | ||
| @Element() el!: HTMLIonInputElement; | ||
| /** | ||
| * The color to use from your application's color palette. | ||
| * Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. | ||
| * For more information on colors, see [theming](/docs/theming/basics). | ||
| */ | ||
| @Prop({ reflect: true }) color?: Color; | ||
| /** | ||
| * The icon that can be used to represent showing a password. If not set, the "eye" Ionicon will be used. | ||
| */ | ||
| @Prop() showIcon?: string; | ||
| /** | ||
| * The icon that can be used to represent hiding a password. If not set, the "eyeOff" Ionicon will be used. | ||
| */ | ||
| @Prop() hideIcon?: string; | ||
| /** | ||
| * @internal | ||
| */ | ||
| @Prop({ mutable: true }) type: TextFieldTypes = 'password'; | ||
| /** | ||
| * Whenever the input type changes we need to re-run validation to ensure the password | ||
| * toggle is being used with the correct input type. If the application changes the type | ||
| * outside of this component we also need to re-render so the correct icon is shown. | ||
| */ | ||
| @Watch('type') | ||
| onTypeChange(newValue: TextFieldTypes) { | ||
| if (newValue !== 'text' && newValue !== 'password') { | ||
| printIonWarning( | ||
| `ion-input-password-toggle only supports inputs of type "text" or "password". Input of type "${newValue}" is not compatible.`, | ||
liamdebeasi marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| this.el | ||
| ); | ||
| return; | ||
| } | ||
| } | ||
| connectedCallback() { | ||
| const { el } = this; | ||
| const inputElRef = (this.inputElRef = el.closest('ion-input')); | ||
| if (!inputElRef) { | ||
| printIonWarning( | ||
| 'No ancestor ion-input found for ion-input-password-toggle. This component must be slotted inside of an ion-input.', | ||
| el | ||
| ); | ||
| return; | ||
| } | ||
| /** | ||
| * Important: Set the type in connectedCallback because the default value | ||
| * of this.type may not always be accurate. Usually inputs have the "password" type | ||
| * but it is possible to have the input to initially have the "text" type. In that scenario | ||
| * the wrong icon will show briefly before switching to the correct icon. Setting the | ||
| * type here allows us to avoid that flicker. | ||
| */ | ||
| this.type = inputElRef.type; | ||
| } | ||
| disconnectedCallback() { | ||
| this.inputElRef = null; | ||
| } | ||
| private togglePasswordVisibility = () => { | ||
| const { inputElRef } = this; | ||
| if (!inputElRef) { | ||
| return; | ||
| } | ||
| inputElRef.type = inputElRef.type === 'text' ? 'password' : 'text'; | ||
| }; | ||
| render() { | ||
| const { color, type } = this; | ||
| const mode = getIonMode(this); | ||
| const showPasswordIcon = this.showIcon ?? eye; | ||
| const hidePasswordIcon = this.hideIcon ?? eyeOff; | ||
| const isPasswordVisible = type === 'text'; | ||
| return ( | ||
| <Host | ||
| class={createColorClasses(color, { | ||
| [mode]: true, | ||
| })} | ||
| > | ||
| <ion-button | ||
liamdebeasi marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| mode={mode} | ||
| color={color} | ||
| fill="clear" | ||
| shape="round" | ||
| aria-checked={isPasswordVisible ? 'true' : 'false'} | ||
| aria-label="show password" | ||
| role="switch" | ||
| type="button" | ||
| onPointerDown={(ev) => { | ||
| /** | ||
| * This prevents mobile browsers from | ||
| * blurring the input when the password toggle | ||
| * button is activated. | ||
| */ | ||
| ev.preventDefault(); | ||
| }} | ||
| onClick={this.togglePasswordVisibility} | ||
| > | ||
| <ion-icon | ||
| slot="icon-only" | ||
| aria-hidden="true" | ||
| icon={isPasswordVisible ? hidePasswordIcon : showPasswordIcon} | ||
| ></ion-icon> | ||
| </ion-button> | ||
| </Host> | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import AxeBuilder from '@axe-core/playwright'; | ||
| import { expect } from '@playwright/test'; | ||
| import { configs, test } from '@utils/test/playwright'; | ||
| configs({ directions: ['ltr'] }).forEach(({ title, config }) => { | ||
| test.describe(title('input password toggle: a11y'), () => { | ||
| test('should not have accessibility violations', async ({ page }) => { | ||
| await page.setContent( | ||
| ` | ||
| <main> | ||
| <ion-input label="input" type="password"> | ||
| <ion-input-password-toggle slot="end"></ion-input-password-toggle> | ||
| </ion-input> | ||
| </main> | ||
| `, | ||
| config | ||
| ); | ||
| const results = await new AxeBuilder({ page }).analyze(); | ||
| expect(results.violations).toEqual([]); | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en" dir="ltr"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <title>Input - Toggle Password</title> | ||
| <meta | ||
| name="viewport" | ||
| content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" | ||
| /> | ||
| <link href="../../../../../css/ionic.bundle.css" rel="stylesheet" /> | ||
| <link href="../../../../../scripts/testing/styles.css" rel="stylesheet" /> | ||
| <script src="../../../../../scripts/testing/scripts.js"></script> | ||
| <script nomodule src="../../../../../dist/ionic/ionic.js"></script> | ||
| <script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script> | ||
| <style> | ||
| .grid { | ||
| display: grid; | ||
| grid-template-columns: repeat(5, minmax(250px, 1fr)); | ||
| grid-row-gap: 20px; | ||
| grid-column-gap: 20px; | ||
| } | ||
| h2 { | ||
| font-size: 12px; | ||
| font-weight: normal; | ||
| color: #6f7378; | ||
| margin-top: 10px; | ||
| } | ||
| @media screen and (max-width: 800px) { | ||
| .grid { | ||
| grid-template-columns: 1fr; | ||
| padding: 0; | ||
| } | ||
| } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <ion-app> | ||
| <ion-header> | ||
| <ion-toolbar> | ||
| <ion-title>Input - Basic</ion-title> | ||
| </ion-toolbar> | ||
| </ion-header> | ||
| <ion-content id="content" class="ion-padding"> | ||
| <div class="grid"> | ||
| <div class="grid-item"> | ||
| <h2>Default</h2> | ||
| <ion-input type="password" value="supersecurepassword" label="Password"> | ||
| <ion-input-password-toggle slot="end"></ion-input-password-toggle> | ||
| </ion-input> | ||
| </div> | ||
| <div class="grid-item"> | ||
| <h2>Custom Icon</h2> | ||
| <ion-input type="password" value="supersecurepassword" label="Password"> | ||
| <ion-input-password-toggle show-icon="trash" slot="end"></ion-input-password-toggle> | ||
| </ion-input> | ||
| </div> | ||
| <div class="grid-item"> | ||
| <h2>Custom Mode/Color</h2> | ||
| <ion-input type="password" value="supersecurepassword" label="Password"> | ||
| <ion-input-password-toggle | ||
| color="danger" | ||
| mode="ios" | ||
| show-icon="trash" | ||
| slot="end" | ||
| ></ion-input-password-toggle> | ||
| </ion-input> | ||
| </div> | ||
| </div> | ||
| </ion-content> | ||
| </ion-app> | ||
| </body> | ||
| </html> |
Uh oh!
There was an error while loading. Please reload this page.
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.
This is correct. The team made this screenshot update in
nextbut it never made its way intofeature-8.0