Skip to content

Commit

Permalink
feat(input): implement password type to input (#381)
Browse files Browse the repository at this point in the history
Co-authored-by: Murat Çorlu <[email protected]>
Co-authored-by: Aykut Saraç <[email protected]>
Co-authored-by: Murat Çorlu <[email protected]>
closes #302
  • Loading branch information
AykutSarac authored Feb 7, 2023
1 parent c13dddf commit 0be574b
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 29 deletions.
31 changes: 25 additions & 6 deletions src/components/input/bl-input.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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);
Expand Down
9 changes: 8 additions & 1 deletion src/components/input/bl-input.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

<Canvas>
<Story name="Text Input" args={{ placeholder: 'Enter Your Name' }}>
Expand All @@ -121,6 +121,9 @@ Currently, input component supports `text` and `number` types, which default is
<Story name="Number Input" args={{ type: 'number', placeholder: 'Enter Your Age' }}>
{SingleInputTemplate.bind({})}
</Story>
<Story name="Password Input" args={{ type: 'password', placeholder: 'Enter Your Password' }}>
{SingleInputTemplate.bind({})}
</Story>
</Canvas>

## Input Labels
Expand Down Expand Up @@ -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({})}
</Story>
<Story name="Password Input With Icon"
args={{ type: 'password', placeholder: 'Enter Password', required: true, icon: 'lock' }}>
{SingleInputTemplate.bind({})}
</Story>
</Canvas>

## Input Validation
Expand Down
47 changes: 46 additions & 1 deletion src/components/input/bl-input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<BlInput>(html`<bl-input type="password"></bl-input>`);
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<BlInput>(html`<bl-input label="${labelText}"></bl-input>`);
Expand All @@ -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<BlInput>(html`<bl-input type="password"></bl-input>`);
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<BlInput>(html`<bl-input type="password"></bl-input>`);
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', () => {
Expand Down Expand Up @@ -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<BlInput>(html`<bl-input type="password"></bl-input>`);
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<BlInput>(html`<bl-input></bl-input>`);
const input = el.shadowRoot?.querySelector('input');
Expand Down Expand Up @@ -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 () => {
Expand Down
80 changes: 59 additions & 21 deletions src/components/input/bl-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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`<p id="errorMessage" aria-live="polite" class="invalid-text">${this.validationMessage}</p>`
? html`<p id="errorMessage" aria-live="polite" class="invalid-text">
${this.validationMessage}
</p>`
: ``;
const helpMessage = this.helpText
? html`<p id="helpText" class="help-text">${this.helpText}</p>`
: ``;
const helpMessage = this.helpText ? html`<p id="helpText" class="help-text">${this.helpText}</p>` : ``;

const icon = this.icon
? html`<bl-icon class="custom-icon" name="${this.icon}"></bl-icon>`
: '';
const icon = this.icon ? html`<bl-icon class="custom-icon" name="${this.icon}"></bl-icon>` : '';
const label = this.label ? html`<label for="input">${this.label}</label>` : '';

const revealButton = this.passwordInput
? html`<bl-button
size="small"
kind="neutral"
variant="tertiary"
class="${classMap({
'reveal-button': true,
'password-visible': this.passwordVisible,
})}"
aria-label="Toggle password reveal"
@bl-click="${this.textVisiblityToggle}"
>
<bl-icon class="reveal-icon" slot="icon" name="eye_on"></bl-icon>
<bl-icon class="reveal-icon" slot="icon" name="eye_off"></bl-icon>
</bl-button>`
: '';

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`<div class=${classMap(classes)}>
${label}
<div class="input-wrapper">
<input
id="input"
type=${this.type}
type=${inputType}
.value=${live(this.value)}
placeholder="${ifDefined(this.placeholder)}"
minlength="${ifDefined(this.minlength)}"
Expand All @@ -236,15 +274,15 @@ export default class BlInput extends FormControlMixin(LitElement) {
@change=${this.changeHandler}
@input=${this.inputHandler}
aria-invalid=${this.checkValidity() ? 'false' : 'true'}
aria-describedby=${ifDefined(this.helpText ? "helpText" : undefined)}
aria-errormessage=${ifDefined(this.checkValidity() ? undefined : "errorMessage")}
aria-describedby=${ifDefined(this.helpText ? 'helpText' : undefined)}
aria-errormessage=${ifDefined(this.checkValidity() ? undefined : 'errorMessage')}
/>
<div class="icon">${icon}<bl-icon class="error-icon" name="alert"></bl-icon></div>
</div>
<div class="hint">
${invalidMessage}
${helpMessage}
<div class="icon">
${revealButton} ${icon}
<bl-icon class="error-icon" name="alert"></bl-icon>
</div>
</div>
<div class="hint">${invalidMessage} ${helpMessage}</div>
</div>`;
}
}
Expand Down

0 comments on commit 0be574b

Please sign in to comment.