diff --git a/src/components/input/bl-input.css b/src/components/input/bl-input.css index cec0abc4..624bad5a 100644 --- a/src/components/input/bl-input.css +++ b/src/components/input/bl-input.css @@ -85,11 +85,36 @@ input { } .icon { + display: flex; + align-items: center; + gap: 8px; flex-basis: var(--icon-size); align-self: center; height: var(--icon-size); } +bl-icon:not(.reveal-icon) { + font-size: var(--icon-size); + color: var(--icon-color); + height: var(--icon-size); +} + +.reveal-button bl-icon { + display: none; +} + +bl-icon[name='eye_on'] { + display: inline-block; +} + +.password-visible bl-icon[name='eye_on'] { + display: none; +} + +.password-visible bl-icon[name='eye_off'] { + display: inline-block; +} + .wrapper:not(.has-icon) .icon { display: none; } @@ -105,12 +130,6 @@ input { margin: 0; } -bl-icon { - font-size: var(--icon-size); - color: var(--icon-color); - height: var(--icon-size); -} - ::placeholder { color: var(--bl-color-content-tertiary); -webkit-text-fill-color: var(--bl-color-content-tertiary); diff --git a/src/components/input/bl-input.stories.mdx b/src/components/input/bl-input.stories.mdx index ce1be704..f8b049be 100644 --- a/src/components/input/bl-input.stories.mdx +++ b/src/components/input/bl-input.stories.mdx @@ -112,7 +112,7 @@ Input component is the component for taking text input from user. ## Basic Usage -Currently, input component supports `text` and `number` types, which default is `text`. +Currently, input component supports `text`, `number` and `password` types, which default is `text`. @@ -121,6 +121,9 @@ Currently, input component supports `text` and `number` types, which default is {SingleInputTemplate.bind({})} + + {SingleInputTemplate.bind({})} + ## Input Labels @@ -183,6 +186,10 @@ Input can have an icon. This icon is showed with `bl-icon` component internally args={{ type: 'text', placeholder: 'Enter Name', required: true, icon: 'calendar' }}> {SingleInputTemplate.bind({})} + + {SingleInputTemplate.bind({})} + ## Input Validation diff --git a/src/components/input/bl-input.test.ts b/src/components/input/bl-input.test.ts index 83b74a91..11bcb490 100644 --- a/src/components/input/bl-input.test.ts +++ b/src/components/input/bl-input.test.ts @@ -39,6 +39,12 @@ describe('bl-input', () => { expect(el.shadowRoot?.querySelector('input')?.getAttribute('type')).to.equal('number'); }); + it('should set type password', async () => { + const el = await fixture(html``); + expect(el.type).to.equal('password'); + expect(el.shadowRoot?.querySelector('input')?.getAttribute('type')).to.equal('password'); + }); + it('should set label', async () => { const labelText = 'Some Label'; const el = await fixture(html``); @@ -62,6 +68,32 @@ describe('bl-input', () => { expect(customIcon).to.exist; expect(customIcon?.getAttribute('name')).to.equal('info'); }); + + it('should show reveal button on password type', async () => { + const el = await fixture(html``); + const revealIcon = el.shadowRoot?.querySelector('bl-icon[name="eye_on"]'); + const hiddenRevealIcon = el.shadowRoot?.querySelector('bl-icon[name="eye_off"]'); + + expect(revealIcon).to.exist; + expect(hiddenRevealIcon).to.exist; + + expect(revealIcon).to.be.visible; + expect(hiddenRevealIcon).to.have.style('display', 'none'); + }); + + it('should toggle reveal icon on click', async () => { + const el = await fixture(html``); + const revealButton = el?.shadowRoot?.querySelector( + 'bl-icon[name="eye_on"]' + ) as HTMLElement | null; + expect(revealButton).to.exist; + expect(revealButton).to.be.visible; + + revealButton?.click(); + await elementUpdated(el); + + expect(revealButton).to.have.style('display', 'none'); + }); }); describe('validation', () => { @@ -127,6 +159,20 @@ describe('bl-input', () => { expect(ev.detail).to.be.equal('some value'); }); + it('should toggle input type on reveal button click', async () => { + const el = await fixture(html``); + const revealButton = el?.shadowRoot?.querySelector('bl-icon') as HTMLElement | null; + const input = el?.shadowRoot?.querySelector('input'); + + expect(input).to.attr('type', 'password'); + expect(revealButton).to.exist; + + revealButton?.click(); + await elementUpdated(el); + + expect(input).to.attr('type', 'text'); + }); + it('should fire bl-input event when input value changes', async () => { const el = await fixture(html``); const input = el.shadowRoot?.querySelector('input'); @@ -164,7 +210,6 @@ describe('bl-input', () => { expect(blInput?.validity.valid).to.be.false; expect(errorMessageElement).to.exist; - }); it('should submit parent form when pressed Enter key', async () => { diff --git a/src/components/input/bl-input.ts b/src/components/input/bl-input.ts index db10859c..0b3ac6b2 100644 --- a/src/components/input/bl-input.ts +++ b/src/components/input/bl-input.ts @@ -9,6 +9,7 @@ import { event, EventDispatcher } from '../../utilities/event'; import { innerInputValidators } from '../../utilities/form-control'; import 'element-internals-polyfill'; import '../icon/bl-icon'; +import '../button/bl-button'; import style from './bl-input.css'; @@ -29,10 +30,16 @@ export default class BlInput extends FormControlMixin(LitElement) { validationTarget: HTMLInputElement; /** - * Type of the input. It's used to set `type` attribute of native input inside. Only `text` and `number` is supported for now. + * Sets name of the input */ @property({}) - type: 'text' | 'number' = 'text'; + name?: string; + + /** + * Type of the input. It's used to set `type` attribute of native input inside. Only `text`, `number` and `password` is supported for now. + */ + @property({}) + type: 'text' | 'password' | 'number' = 'text'; /** * Sets label of the input @@ -159,14 +166,22 @@ export default class BlInput extends FormControlMixin(LitElement) { if (event.code === 'Enter' && this.form) { submit(this.form); } - } + }; private onError = (): void => { this.onInvalid(this.internals.validity); - } + }; @state() private dirty = false; + @state() private passwordVisible = false; + + @state() private passwordInput = false; + + private textVisiblityToggle() { + this.passwordVisible = !this.passwordVisible; + } + validityCallback(): string | void { return this.customInvalidText || this.validationTarget?.validationMessage; } @@ -196,34 +211,57 @@ export default class BlInput extends FormControlMixin(LitElement) { } firstUpdated() { + this.passwordInput = this.type === 'password'; this.setValue(this.value); } render(): TemplateResult { const invalidMessage = !this.checkValidity() - ? html`

${this.validationMessage}

` + ? html`

+ ${this.validationMessage} +

` + : ``; + const helpMessage = this.helpText + ? html`

${this.helpText}

` : ``; - const helpMessage = this.helpText ? html`

${this.helpText}

` : ``; - const icon = this.icon - ? html`` - : ''; + const icon = this.icon ? html`` : ''; const label = this.label ? html`` : ''; + const revealButton = this.passwordInput + ? html` + + + ` + : ''; + const classes = { - wrapper: true, - dirty: this.dirty, - invalid: !this.checkValidity(), - 'has-icon': this.icon || (this.dirty && !this.checkValidity()), + 'wrapper': true, + 'dirty': this.dirty, + 'invalid': !this.checkValidity(), + 'has-icon': this.passwordInput || this.icon || (this.dirty && !this.checkValidity()), 'has-value': this.value !== null && this.value !== '', }; + const passwordType = this.passwordVisible ? 'text' : 'password'; + const inputType = this.passwordInput ? passwordType : this.type; + return html`
${label}
-
${icon}
-
-
- ${invalidMessage} - ${helpMessage} +
+ ${revealButton} ${icon} + +
+
${invalidMessage} ${helpMessage}
`; } }