From 15b83e8a441b1b3f5454109c35e04be6f9dcfb84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aykut=20Sara=C3=A7?= Date: Thu, 9 Mar 2023 12:25:53 +0300 Subject: [PATCH] feat(input): add loading state (#422) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Murat Çorlu <127687+muratcorlu@users.noreply.github.com> Co-authored-by: Aykut Saraç --- src/components/button/bl-button.css | 34 +++++++++++++++++---- src/components/button/bl-button.stories.mdx | 27 ++++++++++++++++ src/components/button/bl-button.test.ts | 25 +++++++++++++++ src/components/button/bl-button.ts | 27 ++++++++++++---- 4 files changed, 101 insertions(+), 12 deletions(-) diff --git a/src/components/button/bl-button.css b/src/components/button/bl-button.css index db884f08..d4ebb619 100644 --- a/src/components/button/bl-button.css +++ b/src/components/button/bl-button.css @@ -1,3 +1,8 @@ +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(359deg); } +} + :host { display: var(--bl-button-display, inline-block); max-width: 100%; @@ -23,7 +28,7 @@ justify-content: var(--bl-button-justify, center); align-items: center; box-sizing: border-box; - width:100%; + width: 100%; height: var(--height); border: solid 1px var(--border-color); border-radius: 6px; @@ -76,15 +81,24 @@ .button:focus-visible::after { border: 2px solid var(--main-color); border-radius: var(--bl-border-radius-l); - content: ""; + content: ''; position: absolute; inset: -4px; } +.loading-icon { + animation: spin 1s linear infinite; + font-size: var(--icon-size); +} + :host ::slotted(bl-icon) { font-size: var(--icon-size); } +:host([loading]) ::slotted(bl-icon) { + display: none; +} + :host .has-icon:not(.has-content) { --padding-horizontal: var(--padding-vertical); --margin-icon: 0; @@ -120,7 +134,15 @@ cursor: not-allowed; } -:host([disabled]) .button { +:host([loading]) { + cursor: wait; +} + +:host([loading]) bl-icon:not(.loading-icon) { + display: none; +} + +:host .button[aria-disabled='true'] { --main-color: var(--bl-color-tertiary); --main-hover-color: var(--bl-color-tertiary); --content-color: var(--bl-color-content-passive); @@ -130,16 +152,16 @@ text-decoration: none; } -:host([variant='tertiary'][disabled]) .button { +:host([variant='tertiary']) .button[aria-disabled='true'] { --main-color: transparent; } -:host([variant='secondary']:hover:not([disabled])) .button { +:host([variant='secondary']:hover) .button[aria-disabled='false'] { --content-color: var(--bl-color-content-primary-contrast); --bg-color: var(--main-hover-color); } -:host([variant='tertiary']:hover:not([disabled])) .button { +:host([variant='tertiary']:hover) .button[aria-disabled='false'] { --content-color: var(--main-hover-color); --bg-color: var(--text-hover-color); } diff --git a/src/components/button/bl-button.stories.mdx b/src/components/button/bl-button.stories.mdx index 36952c59..23925d56 100644 --- a/src/components/button/bl-button.stories.mdx +++ b/src/components/button/bl-button.stories.mdx @@ -23,6 +23,12 @@ import { Meta, Canvas, ArgsTable, Story, Preview, Source } from '@storybook/addo default: 'default', control: { type: 'select' } }, + loadingLabel: { + control: 'text' + }, + loading: { + control: 'boolean' + }, disabled: { control: 'boolean' }, @@ -57,7 +63,9 @@ export const SingleButtonTemplate = (args) => html`${unsafeHTML(args.content)}` @@ -77,6 +85,13 @@ ${SingleButtonTemplate({size: 'large', ...args})} ${SingleButtonTemplate({size: 'medium', ...args})} ${SingleButtonTemplate({size: 'small', ...args})}` +export const LoadingStateTemplate = (args) => html` +${SingleButtonTemplate({ size: 'large', loading: true, icon: "info", ...args})} +${SingleButtonTemplate({ size: 'large', loading: true, loadingLabel: 'Custom Loading Label...', content: 'Login', icon: 'account', ...args})} +${SingleButtonTemplate({ loading: true, disabled: true, content: 'Disabled' })} +${SingleButtonTemplate({ size: 'small', loading: true, content: 'Create' })} +` + # Button ADR @@ -189,6 +204,18 @@ 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. + +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. + + + + {LoadingStateTemplate.bind({})} + + + ## Disabled Buttons We have 2 types of disabled buttons: Disable version of Primary and Secondary buttons is the same. diff --git a/src/components/button/bl-button.test.ts b/src/components/button/bl-button.test.ts index 1b0746f1..2aee640a 100644 --- a/src/components/button/bl-button.test.ts +++ b/src/components/button/bl-button.test.ts @@ -86,6 +86,23 @@ describe('bl-button', () => { expect(el.getAttribute('target')).to.eq('_self'); }); + + it('is disabled button during loading state', async () => { + const el = await fixture( + html`Test` + ); + expect(el.shadowRoot?.querySelector('.loading-icon')).to.exist; + expect(el).to.have.attribute('loading'); + expect(el.shadowRoot?.querySelector('button')).to.have.attribute('disabled'); + + 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'); + + }); }); describe('Slot', () => { it('renders default slot with element', async () => { @@ -94,6 +111,14 @@ describe('bl-button', () => { ); expect(el.shadowRoot?.querySelector('button')).to.exist; }); + + it('renders loading label when set and loading', async () => { + const el = await fixture( + html`Login` + ); + + expect(el.shadowRoot?.querySelector('.label')).to.have.text('Loading...'); + }); }); describe('Link button', () => { it('renders element with anchor tag', async () => { diff --git a/src/components/button/bl-button.ts b/src/components/button/bl-button.ts index 51e5c3cf..e1e5a27f 100644 --- a/src/components/button/bl-button.ts +++ b/src/components/button/bl-button.ts @@ -50,6 +50,18 @@ export default class BlButton extends LitElement { @property({ type: String }) label: string; + /** + * Sets the button label for loading status. + */ + @property({ type: String, attribute: 'loading-label' }) + loadingLabel: string; + + /** + * Sets loading state of button + */ + @property({ type: Boolean, reflect: true }) + loading = false; + /** * Sets button as disabled */ @@ -151,9 +163,12 @@ export default class BlButton extends LitElement { } render(): TemplateResult { + const isDisabled = this.loading || this.disabled; + const label = (this.loading && this.loadingLabel) ? this.loadingLabel : html``; const isAnchor = !!this.href; const icon = this.icon ? html`` : ''; - const slots = html`${icon} `; + const loadingIcon = this.loading ? html`` : ''; + const slots = html`${icon} ${label}`; const caret = this.dropdown ? this.caretTemplate() : ''; const classes = classMap({ 'button': true, @@ -165,21 +180,21 @@ export default class BlButton extends LitElement { return isAnchor ? html`${slots} + >${loadingIcon} ${slots} ` : html``; } }