Skip to content
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

feat: spinner component #877

Merged
merged 7 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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>`
MertOzbudak marked this conversation as resolved.
Show resolved Hide resolved
: "";
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
Loading