Skip to content

Commit

Permalink
feat(inline-loading): introduce error state (#3772)
Browse files Browse the repository at this point in the history
This change introduces a new state (error) to the inline loading
component, in addition to active loading state and finished (complete)
state we have had.

Given the inline loading state is no longer binary, `success` prop in
React one has been deprecated and replaced with new `status` prop,
which takes `inactive`, `active`, `finished` and `error`.

Fixes #3760.
  • Loading branch information
asudoh committed Aug 27, 2019
1 parent 6c1221b commit a197681
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 41 deletions.
9 changes: 5 additions & 4 deletions packages/components/demo/js/inline-loading-demo-button.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ class InlineLoadingDemoButton extends mixin(
*/
toggle() {
if (this.target) {
this.state =
this.state === InlineLoading.states.ACTIVE
? InlineLoading.states.FINISHED
: InlineLoading.states.ACTIVE;
this.state = {
[InlineLoading.states.ACTIVE]: InlineLoading.states.FINISHED,
[InlineLoading.states.FINISHED]: InlineLoading.states.ERROR,
[InlineLoading.states.ERROR]: InlineLoading.states.ACTIVE,
}[this.state];
this.target.setState(this.state);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
.#{$prefix}--inline-loading {
display: flex;
width: 100%;
min-height: 2rem;
align-items: center;

.#{$prefix}--loading__svg circle {
Expand All @@ -45,9 +46,14 @@
}

.#{$prefix}--inline-loading__checkmark-container {
width: 0.75rem;
position: absolute;
top: 0.75rem;
fill: $interactive-04;

// For deprecated older markup
&.#{$prefix}--inline-loading__svg {
width: 0.75rem;
position: absolute;
top: 0.75rem;
}

&[hidden] {
display: none;
Expand All @@ -66,6 +72,16 @@
animation-fill-mode: forwards;
}

.#{$prefix}--inline-loading--error {
fill: $support-01;
width: rem(16px);
height: rem(16px);

&[hidden] {
display: none;
}
}

.#{$prefix}--loading--small .#{$prefix}--inline-loading__svg {
stroke: $interactive-04;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,12 @@
<circle class="{{@root.prefix}}--loading__stroke" cx="0" cy="0" r="30"/>
</svg>
</div>
<svg data-inline-loading-finished hidden
class="{{@root.prefix}}--inline-loading__checkmark-container {{@root.prefix}}--inline-loading__svg"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
<polyline class="{{@root.prefix}}--inline-loading__checkmark" points="0.74 3.4 3.67 6.34 9.24 0.74"></polyline>
</svg>
{{ carbon-icon "CheckmarkFilled16" data-inline-loading-finished='' hidden='' class=(add @root.prefix '--inline-loading__checkmark-container') }}
{{ carbon-icon "Error20" data-inline-loading-error='' hidden='' class=(add @root.prefix '--inline-loading--error') }}
</div>
<p data-inline-loading-text-active class="{{@root.prefix}}--inline-loading__text">Loading data...</p>
<p data-inline-loading-text-finished hidden class="{{@root.prefix}}--inline-loading__text">Data loaded.</p>
<p data-inline-loading-text-error hidden class="{{@root.prefix}}--inline-loading__text">Loading data failed.</p>
</div>
{{#if showToggleButton}}
<button data-inline-loading-demo-button class="{{@root.prefix}}--btn {{@root.prefix}}--btn--primary">Toggle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,26 +54,38 @@ class InlineLoading extends mixin(
const {
selectorSpinner,
selectorFinished,
selectorError,
selectorTextActive,
selectorTextFinished,
selectorTextError,
} = this.options;
const spinner = elem.querySelector(selectorSpinner);
const finished = elem.querySelector(selectorFinished);
const error = elem.querySelector(selectorError);
const textActive = elem.querySelector(selectorTextActive);
const textFinished = elem.querySelector(selectorTextFinished);
const textError = elem.querySelector(selectorTextError);

if (spinner) {
spinner.classList.toggle(
this.options.classLoadingStop,
state !== states.ACTIVE
);
toggleAttribute(spinner, 'hidden', state === states.FINISHED);
toggleAttribute(
spinner,
'hidden',
state !== states.INACTIVE && state !== states.ACTIVE
);
}

if (finished) {
toggleAttribute(finished, 'hidden', state !== states.FINISHED);
}

if (error) {
toggleAttribute(error, 'hidden', state !== states.ERROR);
}

if (textActive) {
toggleAttribute(textActive, 'hidden', state !== states.ACTIVE);
}
Expand All @@ -82,6 +94,10 @@ class InlineLoading extends mixin(
toggleAttribute(textFinished, 'hidden', state !== states.FINISHED);
}

if (textError) {
toggleAttribute(textError, 'hidden', state !== states.ERROR);
}

return this;
}

Expand All @@ -93,6 +109,7 @@ class InlineLoading extends mixin(
INACTIVE: 'inactive',
ACTIVE: 'active',
FINISHED: 'finished',
ERROR: 'error',
};

/**
Expand All @@ -112,8 +129,10 @@ class InlineLoading extends mixin(
* @property {string} selectorInit The CSS selector to find inline loading components.
* @property {string} selectorSpinner The CSS selector to find the spinner.
* @property {string} selectorFinished The CSS selector to find the "finished" icon.
* @property {string} selectorError The CSS selector to find the "error" icon.
* @property {string} selectorTextActive The CSS selector to find the text describing the active state.
* @property {string} selectorTextFinished The CSS selector to find the text describing the finished state.
* @property {string} selectorTextError The CSS selector to find the text describing the error state.
* @property {string} classLoadingStop The CSS class for spinner's stopped state.
*/
static get options() {
Expand All @@ -122,8 +141,10 @@ class InlineLoading extends mixin(
selectorInit: '[data-inline-loading]',
selectorSpinner: '[data-inline-loading-spinner]',
selectorFinished: '[data-inline-loading-finished]',
selectorError: '[data-inline-loading-error]',
selectorTextActive: '[data-inline-loading-text-active]',
selectorTextFinished: '[data-inline-loading-text-finished]',
selectorTextError: '[data-inline-loading-text-error]',
classLoadingStop: `${prefix}--loading--stop`,
};
}
Expand Down
75 changes: 73 additions & 2 deletions packages/components/tests/spec/inline-loading_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ describe('Test Inline Loading', function() {
.querySelector('[data-inline-loading-finished]')
.hasAttribute('hidden')
).toBe(true);
expect(
elem.querySelector('[data-inline-loading-error]').hasAttribute('hidden')
).toBe(true);
expect(
elem
.querySelector('[data-inline-loading-text-active]')
Expand All @@ -61,6 +64,11 @@ describe('Test Inline Loading', function() {
.querySelector('[data-inline-loading-text-finished]')
.hasAttribute('hidden')
).toBe(true);
expect(
elem
.querySelector('[data-inline-loading-text-error]')
.hasAttribute('hidden')
).toBe(true);
});

it('Should hide everything but spinner when the state is set to inactive', function() {
Expand All @@ -82,6 +90,9 @@ describe('Test Inline Loading', function() {
.querySelector('[data-inline-loading-finished]')
.hasAttribute('hidden')
).toBe(true);
expect(
elem.querySelector('[data-inline-loading-error]').hasAttribute('hidden')
).toBe(true);
expect(
elem
.querySelector('[data-inline-loading-text-active]')
Expand All @@ -92,6 +103,11 @@ describe('Test Inline Loading', function() {
.querySelector('[data-inline-loading-text-finished]')
.hasAttribute('hidden')
).toBe(true);
expect(
elem
.querySelector('[data-inline-loading-text-error]')
.hasAttribute('hidden')
).toBe(true);
});

it('Should hide elements for finished states when the state is set to active', function() {
Expand All @@ -113,6 +129,9 @@ describe('Test Inline Loading', function() {
.querySelector('[data-inline-loading-finished]')
.hasAttribute('hidden')
).toBe(true);
expect(
elem.querySelector('[data-inline-loading-error]').hasAttribute('hidden')
).toBe(true);
expect(
elem
.querySelector('[data-inline-loading-text-active]')
Expand All @@ -123,6 +142,11 @@ describe('Test Inline Loading', function() {
.querySelector('[data-inline-loading-text-finished]')
.hasAttribute('hidden')
).toBe(true);
expect(
elem
.querySelector('[data-inline-loading-text-error]')
.hasAttribute('hidden')
).toBe(true);
});

it('Should hide elements for active states when the state is set to finished', function() {
Expand All @@ -144,6 +168,9 @@ describe('Test Inline Loading', function() {
.querySelector('[data-inline-loading-finished]')
.hasAttribute('hidden')
).toBe(false);
expect(
elem.querySelector('[data-inline-loading-error]').hasAttribute('hidden')
).toBe(true);
expect(
elem
.querySelector('[data-inline-loading-text-active]')
Expand All @@ -154,17 +181,61 @@ describe('Test Inline Loading', function() {
.querySelector('[data-inline-loading-text-finished]')
.hasAttribute('hidden')
).toBe(false);
expect(
elem
.querySelector('[data-inline-loading-text-error]')
.hasAttribute('hidden')
).toBe(true);
});

it('Should hide elements for active states when the state is set to error', function() {
instance = new InlineLoading(
elem.querySelector('[data-inline-loading]')
).setState('error');
expect(
elem
.querySelector('[data-inline-loading-spinner]')
.classList.contains('bx--loading--stop')
).toBe(true);
expect(
elem
.querySelector('[data-inline-loading-spinner]')
.hasAttribute('hidden')
).toBe(true);
expect(
elem
.querySelector('[data-inline-loading-finished]')
.hasAttribute('hidden')
).toBe(true);
expect(
elem.querySelector('[data-inline-loading-error]').hasAttribute('hidden')
).toBe(false);
expect(
elem
.querySelector('[data-inline-loading-text-active]')
.hasAttribute('hidden')
).toBe(true);
expect(
elem
.querySelector('[data-inline-loading-text-finished]')
.hasAttribute('hidden')
).toBe(true);
expect(
elem
.querySelector('[data-inline-loading-text-error]')
.hasAttribute('hidden')
).toBe(false);
});

it('Should throw if a wrong state is passed in', function() {
instance = new InlineLoading(document.createElement('div'));
expect(() => instance.setState()).toThrowError(
Error,
'One of the following value should be given as the state: inactive, active, finished'
'One of the following value should be given as the state: inactive, active, finished, error'
);
expect(() => instance.setState('foo')).toThrowError(
Error,
'One of the following value should be given as the state: inactive, active, finished'
'One of the following value should be given as the state: inactive, active, finished, error'
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@
import React, { useState } from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { withKnobs, boolean, number, text } from '@storybook/addon-knobs';
import { withKnobs, number, select, text } from '@storybook/addon-knobs';
import Button from '../Button';
import InlineLoading from '../InlineLoading';

const props = () => ({
success: boolean('Loading successful state (success)', false),
status: select(
'Loading status (status)',
['inactive', 'active', 'finished', 'error'],
'active'
),
iconDescription: text(
'Icon description (iconDescription)',
'Active loading indicator'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,8 @@ describe('Loading', () => {

it('should render the success animation', () => {
expect(
wrapper.find(`.${prefix}--inline-loading__checkmark-container`).length
).toEqual(1);
});

it('should render the checkmark within the success animation', () => {
expect(
wrapper.find(`.${prefix}--inline-loading__checkmark`).length
wrapper.find(`svg.${prefix}--inline-loading__checkmark-container`)
.length
).toEqual(1);
});

Expand Down
Loading

0 comments on commit a197681

Please sign in to comment.