Skip to content

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

Merged
merged 47 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
8635650
fix(input, textarea, select): account for multiple start/end slot ele…
liamdebeasi Mar 18, 2024
5ac8d29
chore(): add updated snapshots
Ionitron Mar 18, 2024
de678b5
fix selector
liamdebeasi Mar 18, 2024
fc3837f
Add component
liamdebeasi Mar 18, 2024
d9a2468
test(input-password-toggle): add failing tests
liamdebeasi Mar 18, 2024
d3cef3d
add implementation
liamdebeasi Mar 18, 2024
4ae25c2
Merge branch 'feature-8.0' into ld/input-slot-margin
liamdebeasi Mar 18, 2024
38e66cf
Revert "fix selector"
liamdebeasi Mar 18, 2024
a7f006b
chore: clean up
liamdebeasi Mar 18, 2024
94a8ac3
Merge remote-tracking branch 'origin/ld/input-slot-margin' into ld/pa…
liamdebeasi Mar 18, 2024
2efcddc
chore: build and lint
liamdebeasi Mar 19, 2024
fba9b5f
fix(input-password-toggle): hide when input is disabled or readonly
liamdebeasi Mar 19, 2024
33076f6
chore: remove todo
liamdebeasi Mar 19, 2024
4587cc2
chore: improve docs explanation
liamdebeasi Mar 19, 2024
00960a2
refactor: warn about unsupported input types
liamdebeasi Mar 19, 2024
d570514
add more tests
liamdebeasi Mar 19, 2024
78e4fc2
Merge remote-tracking branch 'origin/feature-8.0' into ld/password-to…
liamdebeasi Mar 19, 2024
a9c72f4
fix typo
liamdebeasi Mar 19, 2024
28abf98
fix(input-password-toggle): re-render when input type changes
liamdebeasi Mar 19, 2024
3273a1d
run build
liamdebeasi Mar 19, 2024
6238237
lint
liamdebeasi Mar 19, 2024
a08df08
div-be-gone
liamdebeasi Mar 19, 2024
7e66c47
test(input): add screenshot test
liamdebeasi Mar 19, 2024
b87c6e9
lint
liamdebeasi Mar 19, 2024
37aa257
use attribute
liamdebeasi Mar 19, 2024
e22fc9c
Merge branch 'feature-8.0' into ld/password-toggle
liamdebeasi Mar 19, 2024
4cc81ac
chore: add ground truths
liamdebeasi Mar 19, 2024
a2e25f8
fix test
liamdebeasi Mar 19, 2024
c61aa1f
test: add a11y test
liamdebeasi Mar 19, 2024
a36134d
clean up
liamdebeasi Mar 19, 2024
90e62e3
refactor: rename to showIcon and hideIcon
liamdebeasi Mar 19, 2024
b8c22c2
typo
liamdebeasi Mar 19, 2024
4bcc59f
refactor: input sets password toggle type
liamdebeasi Mar 20, 2024
f145bc8
clarify message
liamdebeasi Mar 20, 2024
ecefeba
refactor: leverage existing styles
liamdebeasi Mar 20, 2024
243fb2b
build and lint
liamdebeasi Mar 20, 2024
7025105
Merge branch 'feature-8.0' into ld/password-toggle
liamdebeasi Mar 20, 2024
60e61ec
re-add per-mode stylesheet for now
liamdebeasi Mar 20, 2024
83d2423
Merge branch 'feature-8.0' into ld/password-toggle
liamdebeasi Mar 20, 2024
ab75160
Merge remote-tracking branch 'origin/feature-8.0' into ld/password-to…
liamdebeasi Mar 21, 2024
f0aa12b
Merge branch 'feature-8.0' into ld/password-toggle
liamdebeasi Mar 21, 2024
14a33bb
refactor: add comment, use single file
liamdebeasi Mar 21, 2024
e7248cf
update template
liamdebeasi Mar 21, 2024
6352186
refactor: use shape=“round” instead
liamdebeasi Mar 21, 2024
6d1a328
chore: update screenshots
liamdebeasi Mar 21, 2024
c4e094d
Merge branch 'feature-8.0' into ld/password-toggle
liamdebeasi Mar 22, 2024
2ec9bd0
chore(): add updated snapshots
Ionitron Mar 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@ ion-input,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secon
ion-input,prop,counter,boolean,false,false,false
ion-input,prop,counterFormatter,((inputLength: number, maxLength: number) => string) | undefined,undefined,false,false
ion-input,prop,debounce,number | undefined,undefined,false,false
ion-input,prop,disabled,boolean,false,false,false
ion-input,prop,disabled,boolean,false,false,true
ion-input,prop,enterkeyhint,"done" | "enter" | "go" | "next" | "previous" | "search" | "send" | undefined,undefined,false,false
ion-input,prop,errorText,string | undefined,undefined,false,false
ion-input,prop,fill,"outline" | "solid" | undefined,undefined,false,false
Expand All @@ -575,7 +575,7 @@ ion-input,prop,multiple,boolean | undefined,undefined,false,false
ion-input,prop,name,string,this.inputId,false,false
ion-input,prop,pattern,string | undefined,undefined,false,false
ion-input,prop,placeholder,string | undefined,undefined,false,false
ion-input,prop,readonly,boolean,false,false,false
ion-input,prop,readonly,boolean,false,false,true
ion-input,prop,required,boolean,false,false,false
ion-input,prop,shape,"round" | undefined,undefined,false,false
ion-input,prop,spellcheck,boolean,false,false,false
Expand Down Expand Up @@ -607,6 +607,13 @@ ion-input,css-prop,--placeholder-font-style
ion-input,css-prop,--placeholder-font-weight
ion-input,css-prop,--placeholder-opacity

ion-input-password-toggle,shadow
ion-input-password-toggle,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
ion-input-password-toggle,prop,hideIcon,string | undefined,undefined,false,false
ion-input-password-toggle,prop,mode,"ios" | "md",undefined,false,false
ion-input-password-toggle,prop,showIcon,string | undefined,undefined,false,false
ion-input-password-toggle,part,button

ion-item,shadow
ion-item,prop,button,boolean,false,false,false
ion-item,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
Expand Down
47 changes: 47 additions & 0 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1287,6 +1287,25 @@ export namespace Components {
*/
"value"?: string | number | null;
}
interface IonInputPasswordToggle {
/**
* 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).
*/
"color"?: Color;
/**
* The icon that can be used to represent hiding a password. If not set, the "eyeOff" Ionicon will be used.
*/
"hideIcon"?: string;
/**
* The mode determines which platform styles to use.
*/
"mode"?: "ios" | "md";
/**
* The icon that can be used to represent showing a password. If not set, the "eye" Ionicon will be used.
*/
"showIcon"?: string;
"type": TextFieldTypes;
}
interface IonItem {
/**
* If `true`, a button tag will be rendered and the item will be tappable.
Expand Down Expand Up @@ -3811,6 +3830,12 @@ declare global {
prototype: HTMLIonInputElement;
new (): HTMLIonInputElement;
};
interface HTMLIonInputPasswordToggleElement extends Components.IonInputPasswordToggle, HTMLStencilElement {
}
var HTMLIonInputPasswordToggleElement: {
prototype: HTMLIonInputPasswordToggleElement;
new (): HTMLIonInputPasswordToggleElement;
};
interface HTMLIonItemElement extends Components.IonItem, HTMLStencilElement {
}
var HTMLIonItemElement: {
Expand Down Expand Up @@ -4635,6 +4660,7 @@ declare global {
"ion-infinite-scroll": HTMLIonInfiniteScrollElement;
"ion-infinite-scroll-content": HTMLIonInfiniteScrollContentElement;
"ion-input": HTMLIonInputElement;
"ion-input-password-toggle": HTMLIonInputPasswordToggleElement;
"ion-item": HTMLIonItemElement;
"ion-item-divider": HTMLIonItemDividerElement;
"ion-item-group": HTMLIonItemGroupElement;
Expand Down Expand Up @@ -5993,6 +6019,25 @@ declare namespace LocalJSX {
*/
"value"?: string | number | null;
}
interface IonInputPasswordToggle {
/**
* 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).
*/
"color"?: Color;
/**
* The icon that can be used to represent hiding a password. If not set, the "eyeOff" Ionicon will be used.
*/
"hideIcon"?: string;
/**
* The mode determines which platform styles to use.
*/
"mode"?: "ios" | "md";
/**
* The icon that can be used to represent showing a password. If not set, the "eye" Ionicon will be used.
*/
"showIcon"?: string;
"type"?: TextFieldTypes;
}
interface IonItem {
/**
* If `true`, a button tag will be rendered and the item will be tappable.
Expand Down Expand Up @@ -8040,6 +8085,7 @@ declare namespace LocalJSX {
"ion-infinite-scroll": IonInfiniteScroll;
"ion-infinite-scroll-content": IonInfiniteScrollContent;
"ion-input": IonInput;
"ion-input-password-toggle": IonInputPasswordToggle;
"ion-item": IonItem;
"ion-item-divider": IonItemDivider;
"ion-item-group": IonItemGroup;
Expand Down Expand Up @@ -8138,6 +8184,7 @@ declare module "@stencil/core" {
"ion-infinite-scroll": LocalJSX.IonInfiniteScroll & JSXBase.HTMLAttributes<HTMLIonInfiniteScrollElement>;
"ion-infinite-scroll-content": LocalJSX.IonInfiniteScrollContent & JSXBase.HTMLAttributes<HTMLIonInfiniteScrollContentElement>;
"ion-input": LocalJSX.IonInput & JSXBase.HTMLAttributes<HTMLIonInputElement>;
"ion-input-password-toggle": LocalJSX.IonInputPasswordToggle & JSXBase.HTMLAttributes<HTMLIonInputPasswordToggleElement>;
"ion-item": LocalJSX.IonItem & JSXBase.HTMLAttributes<HTMLIonItemElement>;
"ion-item-divider": LocalJSX.IonItemDivider & JSXBase.HTMLAttributes<HTMLIonItemDividerElement>;
"ion-item-group": LocalJSX.IonItemGroup & JSXBase.HTMLAttributes<HTMLIonItemGroupElement>;
Expand Down
Empty file.
Empty file.
151 changes: 151 additions & 0 deletions core/src/components/input-password-toggle/input-password-toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
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',
styleUrls: {
ios: 'input-password-toggle.ios.scss',
md: 'input-password-toggle.md.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.`,
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,
})}
>
{/*
This part is intentionally undocumented. It only exists so that Input
can style the button when InputPasswordToggle is slotted inside of the Input.
*/}
<ion-button
part="button"
mode={mode}
color={color}
fill="clear"
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([]);
});
});
});
76 changes: 76 additions & 0 deletions core/src/components/input-password-toggle/test/basic/index.html
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 mode="ios" 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>
Loading