Skip to content

Commit

Permalink
feat: spinner component (#877)
Browse files Browse the repository at this point in the history
closes #866
  • Loading branch information
MertOzbudak authored Jun 27, 2024
1 parent f3dafd9 commit 9fad8ca
Show file tree
Hide file tree
Showing 15 changed files with 480 additions and 92 deletions.
1 change: 1 addition & 0 deletions src/baklava.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export { default as BlDropdown } from "./components/dropdown/bl-dropdown";
export { default as BlDropdownItem } from "./components/dropdown/item/bl-dropdown-item";
export { default as BlDropdownGroup } from "./components/dropdown/group/bl-dropdown-group";
export { default as BlSwitch } from "./components/switch/bl-switch";
export { default as BlSpinner } from "./components/spinner/bl-spinner";
export { default as BlNotification } from "./components/notification/bl-notification";
export { default as BlNotificationCard } from "./components/notification/card/bl-notification-card";
export { default as BlTable } from "./components/table/bl-table";
Expand Down
19 changes: 0 additions & 19 deletions src/components/button/bl-button.css
Original file line number Diff line number Diff line change
@@ -1,13 +1,3 @@
@keyframes spin {
from {
transform: rotate(0deg);
}

to {
transform: rotate(359deg);
}
}

:host {
display: var(--bl-button-display, inline-block);
max-width: 100%;
Expand Down Expand Up @@ -91,11 +81,6 @@
inset: -4px;
}

.loading-icon {
animation: spin 1s linear infinite;
font-size: var(--icon-size);
}

:host ::slotted(bl-icon) {
font-size: var(--icon-size);
}
Expand Down Expand Up @@ -143,10 +128,6 @@
cursor: wait;
}

:host([loading]) bl-icon:not(.loading-icon) {
display: none;
}

:host .button[aria-disabled="true"] {
--main-color: var(--bl-color-neutral-lightest);
--main-hover-color: var(--bl-color-neutral-lightest);
Expand Down
6 changes: 3 additions & 3 deletions src/components/button/bl-button.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ import { Meta, Canvas, ArgsTable, Story } from '@storybook/addon-docs';
}
},
href: {
control: {
control: {
type: 'text'
}
},
target: {
control: {
control: {
type: 'text'
}
},
Expand Down Expand Up @@ -206,7 +206,7 @@ If button has a limited width and a long text that can not fit in a single line,

## Loading Buttons

Button can be set in loading state. In this state button becomes disabled with a loading indicator. You can set this state by setting `loading` attribute. Additionally, button icons are overridden by the spinner during the loading state.
Button can be set in loading state. In this state button becomes disabled with a loading indicator. You can set this state by setting `loading` attribute. Additionally, button icons are overridden by the bl-spinner during the loading state.

A custom loading text can be also set with `loading-label` attribute. It's suggested to use `loading-label` to inform the user about the process.

Expand Down
56 changes: 52 additions & 4 deletions src/components/button/bl-button.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,19 +95,23 @@ describe("bl-button", () => {
expect(el.getAttribute("target")).to.eq("_self");
});

it("is disabled button during loading state", async () => {
it("is disabled button during loading state with spinner", async () => {
const el = await fixture<typeOfBlButton>(html`<bl-button loading>Test</bl-button>`);

expect(el.shadowRoot?.querySelector(".loading-icon")).to.exist;
expect(el).to.have.attribute("loading");
expect(el.shadowRoot?.querySelector("button")).to.have.attribute("disabled");
const spinner = el.shadowRoot?.querySelector("bl-spinner");

expect(spinner).to.exist;

el.removeAttribute("loading");
await elementUpdated(el);

expect(el.shadowRoot?.querySelector(".loading-icon")).not.to.exist;
expect(el).not.have.attribute("loading");
expect(el.shadowRoot?.querySelector("button")).not.have.attribute("disabled");
expect(el.shadowRoot?.querySelector("button")).not.to.have.attribute("disabled");
const spinnerAfterLoading = el.shadowRoot?.querySelector("bl-spinner");

expect(spinnerAfterLoading).not.to.exist;
});
});
describe("Slot", () => {
Expand Down Expand Up @@ -225,4 +229,48 @@ describe("bl-button", () => {
expect(ev).to.exist;
});
});

describe("Spinner on bl-button", () => {

it("should render bl-spinner when loading is true", async () => {
const el = await fixture<BlButton>(html`
<bl-button loading loading-label="Loading...">Submit</bl-button>
`);

const spinner = el.shadowRoot?.querySelector(".loading-spinner");

expect(spinner).to.exist;
});

it("should not render bl-spinner when loading is false", async () => {
const el = await fixture<BlButton>(html`
<bl-button>Submit</bl-button>
`);

const spinner = el.shadowRoot?.querySelector(".loading-spinner");

expect(spinner).to.not.exist;
});

it("should render loading label when loading is true and loadingLabel is set", async () => {
const el = await fixture<BlButton>(html`
<bl-button loading loading-label="Loading...">Submit</bl-button>
`);

const label = el.shadowRoot?.querySelector(".label")?.textContent;

expect(label).to.equal("Loading...");
});

it("should not render loading label when loading is false", async () => {
const el = await fixture<BlButton>(html`
<bl-button loading-label="Loading...">Submit</bl-button>
`);

const label = el.shadowRoot?.querySelector(".label")?.textContent;

expect(label).not.to.equal("Loading...");
});

});
});
16 changes: 10 additions & 6 deletions src/components/button/bl-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { submit } from "@open-wc/form-helpers";
import { event, EventDispatcher } from "../../utilities/event";
import "../icon/bl-icon";
import { BaklavaIcon } from "../icon/icon-list";
import "../spinner/bl-spinner";
import style from "./bl-button.css";

export type ButtonVariant = "primary" | "secondary" | "tertiary";
Expand Down Expand Up @@ -191,10 +192,13 @@ export default class BlButton extends LitElement {
const label = this.loading && this.loadingLabel ? this.loadingLabel : html`<slot></slot>`;
const isAnchor = !!this.href;
const icon = this.icon ? html`<bl-icon name=${this.icon}></bl-icon>` : "";
const loadingIcon = this.loading
? html`<bl-icon class="loading-icon" name="loading"></bl-icon>`
: "";
const slots = html`<slot name="icon">${icon}</slot> <span class="label">${label}</span>`;
const loadingIcon = html`<bl-spinner
class="loading-spinner"
?disabled="${isDisabled}"
size="${this.size}"
></bl-spinner>`;
const slots = html`<slot name="icon">${this.loading ? loadingIcon : icon}</slot>
<span class="label">${label}</span>`;
const caret = this.dropdown ? this.caretTemplate() : "";
const classes = classMap({
"button": true,
Expand All @@ -212,7 +216,7 @@ export default class BlButton extends LitElement {
href=${ifDefined(this.href)}
target=${ifDefined(this.target)}
role="button"
>${loadingIcon} ${slots}
>${slots}
</a>`
: html`<button
class=${classes}
Expand All @@ -222,7 +226,7 @@ export default class BlButton extends LitElement {
?disabled=${isDisabled}
@click="${this._handleClick}"
>
${loadingIcon} ${slots} ${caret}
${slots} ${caret}
</button>`;
}
}
Expand Down
49 changes: 21 additions & 28 deletions src/components/input/bl-input.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export const SingleInputTemplate = (args) => html`<bl-input
label='${ifDefined(args.label)}'
placeholder='${ifDefined(args.placeholder)}'
value='${ifDefined(args.value)}'
?loading=${ifDefined(args.loading)}
?required='${args.required}'
?disabled='${args.disabled}'
?readonly='${args.readonly}'
Expand All @@ -107,29 +108,6 @@ export const SingleInputTemplate = (args) => html`<bl-input
size='${ifDefined(args.size)}'
>${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 })}
${SingleInputTemplate({ size: 'medium', ...args })}
Expand Down Expand Up @@ -229,13 +207,9 @@ Input also supports slot icons for more complex use cases. You can use `icon` sl

<Canvas>
<Story name="Input With Slot Icon"
args={{ placeholder: 'Name', slot: () => html`<bl-icon slot="icon" name="flash"></bl-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>

Inputs with type of date, time, datetime-local, month, week and search have default icons. You can override these icons with `icon` attribute.
Expand Down Expand Up @@ -333,6 +307,25 @@ Input can be set as disabled by adding `disabled` attribute.
</Story>
</Canvas>

## Search Input with Loading Attribute

This example demonstrates how to use the `bl-spinner` component inside a search input field. The spinner is displayed to indicate that a search operation is in progress.
The `loading` attribute is set to `true` to show the spinner inside the input field.

<Canvas>
<Story name="Input with Loading Spinner" args={{ placeholder: 'Search Loading Example', type: 'search', loading: true }}>
{args => html`
<bl-input
id="searchInput"
placeholder=${ifDefined(args.placeholder)}
type=${ifDefined(args.type)}
label=${ifDefined(args.label)}
loading=${ifDefined(args.loading)}
></bl-input>
`}
</Story>
</Canvas>

## Using within a form

Input component uses [ElementInternals](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) to associate with it's parent form automatically. When you use `bl-input` within a form with a `name` attribute, input's value will be automatically set parent form's FormData. Check the example below:
Expand Down
47 changes: 47 additions & 0 deletions src/components/input/bl-input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,4 +352,51 @@ describe("bl-input", () => {
expect(ev).to.exist;
});
});

describe("loading state and custom icons", () => {
it("shows spinner when loading and type is search with non-empty value", async () => {
const el = await fixture<BlInput>(html`<bl-input loading type="search" value="test"></bl-input>`);
const spinner = el.shadowRoot?.querySelector("bl-spinner");

expect(spinner).to.exist;
expect(spinner?.getAttribute("size")).to.equal("var(--bl-font-size-m)");
});

it("shows custom icon when loading is false", async () => {
const el = await fixture<BlInput>(html`<bl-input icon="info"></bl-input>`);
const customIcon = el.shadowRoot?.querySelector('bl-icon[name="info"]');

expect(customIcon).to.exist;
expect(customIcon?.getAttribute("name")).to.equal("info");
});

it("shows error icon when no custom icon is set and loading is false", async () => {
const el = await fixture<BlInput>(html`<bl-input></bl-input>`);
const errorIcon = el.shadowRoot?.querySelector('bl-icon[name="alert"]');

expect(errorIcon).to.exist;
expect(errorIcon?.getAttribute("name")).to.equal("alert");
});

it("does not show spinner when loading is true but type is not search", async () => {
const el = await fixture<BlInput>(html`<bl-input loading type="text" value="test"></bl-input>`);
const spinner = el.shadowRoot?.querySelector("bl-spinner");

expect(spinner).to.not.exist;
});

it("does not show spinner when loading is true but value is empty", async () => {
const el = await fixture<BlInput>(html`<bl-input loading type="search" value=""></bl-input>`);
const spinner = el.shadowRoot?.querySelector("bl-spinner");

expect(spinner).to.not.exist;
});

it("does not show spinner when loading is false", async () => {
const el = await fixture<BlInput>(html`<bl-input type="search" value="test"></bl-input>`);
const spinner = el.shadowRoot?.querySelector("bl-spinner");

expect(spinner).to.not.exist;
});
});
});
10 changes: 9 additions & 1 deletion src/components/input/bl-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ export default class BlInput extends FormControlMixin(LitElement) {
@property({ reflect: true })
min?: number | string;

/**
* Sets the loading value for the input
*/
@property({ type: Boolean, reflect: true })
loading = false;

/**
* Sets the maximum acceptable value for the input
*/
Expand Down Expand Up @@ -325,7 +331,9 @@ export default class BlInput extends FormControlMixin(LitElement) {

const icon = html`
<slot name="icon">
${this.icon
${this.loading && this.type === "search" && this.value !== "" && this.value !== null
? html`<bl-spinner></bl-spinner>`
: this.icon
? html`<bl-icon name="${this.icon}"></bl-icon>`
: html`<bl-icon class="error-icon" name="alert"></bl-icon>`}
</slot>
Expand Down
14 changes: 2 additions & 12 deletions src/components/select/bl-select.css
Original file line number Diff line number Diff line change
Expand Up @@ -396,8 +396,8 @@ legend span {
outline: none;
}

.search-loading-icon {
animation: spin 1s linear infinite;
.search-spinner {
padding-inline-end: var(--bl-font-size-2xs);
}

.action-divider {
Expand All @@ -411,16 +411,6 @@ legend span {
background-color: var(--bl-color-neutral-lighter);
}

@keyframes spin {
from {
transform: rotate(0deg);
}

to {
transform: rotate(360deg);
}
}

.actions bl-icon {
padding: 4px;
}
Loading

0 comments on commit 9fad8ca

Please sign in to comment.