Skip to content

Commit

Permalink
feat(input): icon slot for bl-input (#649)
Browse files Browse the repository at this point in the history
  • Loading branch information
ogunb authored Jul 14, 2023
1 parent fcf47a8 commit 3a01e54
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 14 deletions.
6 changes: 2 additions & 4 deletions src/components/input/bl-input.css
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@ input:-webkit-autofill {
margin-right: var(--label-padding);
}

bl-icon:not(.reveal-icon) {
bl-icon:not(.reveal-icon),
::slotted(bl-icon) {
font-size: var(--icon-size);
color: var(--icon-color);
height: var(--icon-size);
Expand Down Expand Up @@ -290,6 +291,3 @@ bl-icon[name='eye_on'] {
display: inline-block;
}

.dirty.invalid .custom-icon ~ .error-icon {
display: none;
}
38 changes: 37 additions & 1 deletion src/components/input/bl-input.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,30 @@ export const SingleInputTemplate = (args) => html`<bl-input
step='${ifDefined(args.step)}'
icon='${ifDefined(args.icon)}'
size='${ifDefined(args.size)}'
></bl-input>`
>${args.slot?.()}</bl-input>`

export const SingleInputTemplateWithSpinner = (args) => html`
<style>
.spinner {
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
${SingleInputTemplate({
...args,
slot: () => html`<bl-icon slot="icon" name="loading" class="spinner"></bl-icon>`,
})}
`

export const SizeVariantsTemplate = args => html`
${SingleInputTemplate({ size: 'large', ...args })}
Expand Down Expand Up @@ -202,6 +225,19 @@ Input can have an icon. This icon is showed with `bl-icon` component internally
</Story>
</Canvas>

Input also supports slot icons for more complex use cases. You can use `icon` slot for this.

<Canvas>
<Story name="Input With Slot Icon"
args={{ placeholder: 'Name', slot: () => html`<bl-icon slot="icon" name="flash"></bl-icon>` }}>
{SingleInputTemplate.bind({})}
</Story>
<Story name="Input With Spinner"
args={{ placeholder: 'Name' }}>
{SingleInputTemplateWithSpinner.bind({})}
</Story>
</Canvas>

## Input Validation

Input supports native HTML validation rules like `required`, `minlength`, `maxlength`, `min` and `max`. Other validation rules will come soon.
Expand Down
39 changes: 33 additions & 6 deletions src/components/input/bl-input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ describe('bl-input', () => {
type="text"
>
<div class="icon">
<bl-icon
class="error-icon"
name="alert"
>
</bl-icon>
<slot name="icon">
<bl-icon
class="error-icon"
name="alert"
>
</bl-icon>
</slot>
</div>
</fieldset>
<div class="hint"></div>
Expand Down Expand Up @@ -64,9 +66,24 @@ describe('bl-input', () => {
describe('input with icon', () => {
it('should show custom icon', async () => {
const el = await fixture<BlInput>(html`<bl-input icon="info"></bl-input>`);
const customIcon = el.shadowRoot?.querySelector('bl-icon.custom-icon');
const customIcon = el.shadowRoot?.querySelector('bl-icon[name="info"]');
expect(customIcon).to.exist;
expect(customIcon?.getAttribute('name')).to.equal('info');
expect(el.shadowRoot?.querySelector('.has-icon')).to.exist;
});

it('should show slot icon', async () => {
const el = await fixture<BlInput>(html`<bl-input><bl-icon slot="icon" name="info"></bl-icon></bl-input>`);
const slot = el.shadowRoot?.querySelector('slot[name="icon"]') as HTMLSlotElement;
const slotIcon = el.querySelector('bl-icon[name="info"]');

expect(slot.assignedNodes()).have.lengthOf(1);
expect(slot.assignedNodes()[0]).to.equal(slotIcon);

expect(slotIcon).to.exist;
expect(slotIcon?.getAttribute('name')).to.equal('info');

expect(el.shadowRoot?.querySelector('.has-icon')).to.exist;
});

it('should show reveal button on password type', async () => {
Expand Down Expand Up @@ -173,6 +190,16 @@ describe('bl-input', () => {
);
expect(errorMessageElement).to.not.exist;
});

it('should render alert icon when invalid and without custom icon', async () => {
const el = await fixture<BlInput>(html`<bl-input required></bl-input>`);
el.reportValidity();

await elementUpdated(el);

const alertIcon = el.shadowRoot?.querySelector('bl-icon[name="alert"]');
expect(alertIcon).to.exist;
});
});

describe('events', () => {
Expand Down
18 changes: 15 additions & 3 deletions src/components/input/bl-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,10 @@ export default class BlInput extends FormControlMixin(LitElement) {

private inputId = Math.random().toString(36).substring(2);

private get _hasIconSlot() {
return this.querySelector(':scope > [slot="icon"]') !== null;
}

render(): TemplateResult {
const invalidMessage = !this.checkValidity()
? html`<p id="errorMessage" aria-live="polite" class="invalid-text">
Expand All @@ -289,7 +293,15 @@ export default class BlInput extends FormControlMixin(LitElement) {
? 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 = html`
<slot name="icon">
${this.icon
? html`<bl-icon name="${this.icon}"></bl-icon>`
: html`<bl-icon class="error-icon" name="alert"></bl-icon>`
}
</slot>
`;

const label = this.label ? html`<label for=${this.inputId}>${this.label}</label>` : '';
const passwordInput = this.type === 'password';

Expand All @@ -310,11 +322,12 @@ export default class BlInput extends FormControlMixin(LitElement) {
</bl-button>`
: '';

const hasCustomIcon = this.icon || this._hasIconSlot;
const classes = {
'wrapper': true,
'dirty': this.dirty,
'invalid': !this.checkValidity(),
'has-icon': passwordInput || this.icon || (this.dirty && !this.checkValidity()),
'has-icon': passwordInput || hasCustomIcon || (this.dirty && !this.checkValidity()),
'has-value': this.value !== null && this.value !== '',
};

Expand Down Expand Up @@ -350,7 +363,6 @@ export default class BlInput extends FormControlMixin(LitElement) {
/>
<div class="icon">
${revealButton} ${icon}
<bl-icon class="error-icon" name="alert"></bl-icon>
</div>
</fieldset>
<div class="hint">${invalidMessage} ${helpMessage}</div>
Expand Down

0 comments on commit 3a01e54

Please sign in to comment.