Unverified Commit 5381de39 authored by Marcos Iglesias's avatar Marcos Iglesias Committed by GitHub

feat: Update TagList Loading State with a shimmer loader (#486)

* Adds taglist shimmer loader

* Adds test

* Updating border radius value by sharing a sass variable

* Setting explicit values for the numItems prop

* alt attribute on some images
parent 37ff51f7
...@@ -108,6 +108,7 @@ $priority-bg-color: $rose80; ...@@ -108,6 +108,7 @@ $priority-bg-color: $rose80;
// Tags // Tags
$tag-bg: $gray5; $tag-bg: $gray5;
$tag-bg-hover: $gray10; $tag-bg-hover: $gray10;
$tag-border-radius: 4px;
// TODO Temp Colors // TODO Temp Colors
$resource-title-color: $indigo60; $resource-title-color: $indigo60;
......
...@@ -249,7 +249,7 @@ export class ProfilePage extends React.Component< ...@@ -249,7 +249,7 @@ export class ProfilePage extends React.Component<
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
<img className="icon icon-dark icon-mail" /> <img className="icon icon-dark icon-mail" alt="" />
<span className="email-link-label body-2">{user.email}</span> <span className="email-link-label body-2">{user.email}</span>
</a> </a>
); );
...@@ -289,7 +289,7 @@ export class ProfilePage extends React.Component< ...@@ -289,7 +289,7 @@ export class ProfilePage extends React.Component<
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
<img className="icon icon-dark icon-github" /> <img className="icon icon-dark icon-github" alt="" />
<span className="github-link-label body-2">{GITHUB_LINK_TEXT}</span> <span className="github-link-label body-2">{GITHUB_LINK_TEXT}</span>
</a> </a>
); );
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.btn.tag-button { .btn.tag-button {
background-color: $tag-bg; background-color: $tag-bg;
border: 0; border: 0;
border-radius: 4px; border-radius: $tag-border-radius;
color: $text-primary; color: $text-primary;
margin: 0 8px 8px 0; margin: 0 8px 8px 0;
overflow: hidden; overflow: hidden;
......
import * as React from 'react';
import { mount } from 'enzyme';
import ShimmeringTagListLoader, {
ShimmeringTagItem,
ShimmeringTagListLoaderProps,
} from '.';
const setup = (propOverrides?: Partial<ShimmeringTagListLoaderProps>) => {
const props: ShimmeringTagListLoaderProps = {
...propOverrides,
};
const wrapper = mount<ShimmeringTagListLoaderProps>(
<ShimmeringTagListLoader {...props} />
);
return { props, wrapper };
};
describe('ShimmeringTagListLoader', () => {
let wrapper;
describe('render', () => {
beforeAll(() => {
({ wrapper } = setup());
});
it('renders a container', () => {
const actual = wrapper.find('.shimmer-tag-list-loader').length;
const expected = 1;
expect(actual).toEqual(expected);
});
it('renders ten tags by default', () => {
const actual = wrapper.find(ShimmeringTagItem).length;
const expected = 10;
expect(actual).toEqual(expected);
});
describe('when passing a numItems value', () => {
it('renders as many tags as requested', () => {
const expected = 5;
({ wrapper } = setup({ numItems: expected }));
const actual = wrapper.find(ShimmeringTagItem).length;
expect(actual).toEqual(expected);
});
});
});
});
import * as React from 'react';
import * as times from 'lodash/times';
import './styles.scss';
const DEFAULT_REPETITION = 10;
type ShimmeringTagItemProps = {
index: number;
};
export const ShimmeringTagItem: React.SFC<ShimmeringTagItemProps> = ({
index,
}: ShimmeringTagItemProps) => {
return (
<span
className={`shimmer-tag-loader-item shimmer-tag-loader-item--${index} is-shimmer-animated`}
/>
);
};
export interface ShimmeringTagListLoaderProps {
numItems?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15;
}
const ShimmeringTagListLoader: React.SFC<ShimmeringTagListLoaderProps> = ({
numItems = DEFAULT_REPETITION,
}: ShimmeringTagListLoaderProps) => {
return (
<div className="shimmer-tag-list-loader">
{times(numItems, (idx) => (
<ShimmeringTagItem key={idx} index={idx} />
))}
</div>
);
};
export default ShimmeringTagListLoader;
@import 'variables';
$shimmer-loader-tag-height: 36px;
$shimmer-loader-items: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14;
$shimmer-loader-tag-max-width: 130;
$shimmer-loader-tag-min-width: 50;
.shimmer-tag-list-loader {
}
.shimmer-tag-loader-item {
margin: 0 $spacer-1 $spacer-1 0;
height: $shimmer-loader-tag-height;
width: $shimmer-loader-tag-min-width + px;
border-radius: $tag-border-radius;
display: inline-block;
}
@each $item in $shimmer-loader-items {
.shimmer-tag-loader-item--#{$item} {
width:
(
random($shimmer-loader-tag-max-width - $shimmer-loader-tag-min-width) +
$shimmer-loader-tag-min-width
) +
px;
}
}
...@@ -2,7 +2,7 @@ import * as React from 'react'; ...@@ -2,7 +2,7 @@ import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import LoadingSpinner from 'components/common/LoadingSpinner'; import ShimmeringTagListLoader from 'components/common/ShimmeringTagListLoader';
import globalState from 'fixtures/globalState'; import globalState from 'fixtures/globalState';
...@@ -25,61 +25,63 @@ jest.mock('config/config-utils', () => ({ ...@@ -25,61 +25,63 @@ jest.mock('config/config-utils', () => ({
}, },
})); }));
describe('TagsList', () => { const setup = (propOverrides?: Partial<TagsListProps>) => {
const setup = (propOverrides?: Partial<TagsListProps>) => { const props: TagsListProps = {
const props: TagsListProps = { curatedTags: [
curatedTags: [ {
{ tag_count: 2,
tag_count: 2, tag_name: 'test1',
tag_name: 'test1', },
}, ],
], otherTags: [
otherTags: [ {
{ tag_count: 1,
tag_count: 1, tag_name: 'test2',
tag_name: 'test2', },
}, ],
], isLoading: false,
isLoading: false, getAllTags: jest.fn(),
getAllTags: jest.fn(), ...propOverrides,
...propOverrides,
};
const wrapper = shallow(<TagsList {...props} />);
return { props, wrapper };
}; };
const wrapper = shallow(<TagsList {...props} />);
return { props, wrapper };
};
describe('TagsList', () => {
describe('componentDidMount', () => { describe('componentDidMount', () => {
it('calls props.getAllTags', () => { it('calls props.getAllTags', () => {
const { props, wrapper } = setup(); const { props } = setup();
expect(props.getAllTags).toHaveBeenCalled(); expect(props.getAllTags).toHaveBeenCalled();
}); });
}); });
describe('render', () => { describe('render', () => {
it('renders LoadingSpinner if props.isLoading is true', () => { it('renders a shimmering loader if props.isLoading is true', () => {
const { props, wrapper } = setup({ isLoading: true }); const { wrapper } = setup({ isLoading: true });
expect(wrapper.find(LoadingSpinner).exists()).toBe(true);
expect(wrapper.find(ShimmeringTagListLoader).exists()).toBe(true);
}); });
it('renders <hr> if curatedTags.length > 0 & otherTags.length > 0 & showAllTags == true', () => { it('renders <hr> if curatedTags.length > 0 & otherTags.length > 0 & showAllTags == true', () => {
// @ts-ignore // @ts-ignore
showAllTags.mockImplementation(() => true); showAllTags.mockImplementation(() => true);
const { props, wrapper } = setup(); const { wrapper } = setup();
expect(wrapper.find('hr').exists()).toBe(true); expect(wrapper.find('hr').exists()).toBe(true);
}); });
it('does not render <hr> if showAllTags is false', () => { it('does not render <hr> if showAllTags is false', () => {
// @ts-ignore // @ts-ignore
showAllTags.mockImplementation(() => false); showAllTags.mockImplementation(() => false);
const { props, wrapper } = setup(); const { wrapper } = setup();
expect(wrapper.find('hr').exists()).toBe(false); expect(wrapper.find('hr').exists()).toBe(false);
}); });
it('does not render an <hr> if otherTags is empty', () => { it('does not render an <hr> if otherTags is empty', () => {
// @ts-ignore // @ts-ignore
showAllTags.mockImplementation(() => true); showAllTags.mockImplementation(() => true);
const { props, wrapper } = setup(); const { wrapper } = setup();
expect(wrapper.find('#tags-list').find('hr').exists()).toBe(true); expect(wrapper.find('#tags-list').find('hr').exists()).toBe(true);
}); });
......
...@@ -4,7 +4,8 @@ import { bindActionCreators } from 'redux'; ...@@ -4,7 +4,8 @@ import { bindActionCreators } from 'redux';
import './styles.scss'; import './styles.scss';
import LoadingSpinner from 'components/common/LoadingSpinner'; import ShimmeringTagListLoader from 'components/common/ShimmeringTagListLoader';
import TagInfo from 'components/Tags/TagInfo'; import TagInfo from 'components/Tags/TagInfo';
import { Tag } from 'interfaces'; import { Tag } from 'interfaces';
...@@ -37,24 +38,28 @@ export class TagsList extends React.Component<TagsListProps> { ...@@ -37,24 +38,28 @@ export class TagsList extends React.Component<TagsListProps> {
} }
render() { render() {
if (this.props.isLoading) { const { isLoading, curatedTags, otherTags } = this.props;
return <LoadingSpinner />;
if (isLoading) {
return <ShimmeringTagListLoader />;
} }
return ( return (
<div id="tags-list" className="tags-list"> <div id="tags-list" className="tags-list">
{this.generateTagInfo(this.props.curatedTags)} {this.generateTagInfo(curatedTags)}
{showAllTags() && {showAllTags() && curatedTags.length > 0 && otherTags.length > 0 && (
this.props.curatedTags.length > 0 && <hr />
this.props.otherTags.length > 0 && <hr />} )}
{showAllTags() && {showAllTags() &&
this.props.otherTags.length > 0 && otherTags.length > 0 &&
this.generateTagInfo(this.props.otherTags)} this.generateTagInfo(otherTags)}
</div> </div>
); );
} }
} }
export const mapStateToProps = (state: GlobalState) => { export const mapStateToProps = (state: GlobalState) => {
// TODO: These functions are selectors, consider moving them into the ducks
const curatedTagsList = getCuratedTags(); const curatedTagsList = getCuratedTags();
const allTags = state.tags.allTags.tags; const allTags = state.tags.allTags.tags;
const curatedTags = allTags.filter( const curatedTags = allTags.filter(
......
...@@ -25111,6 +25111,15 @@ ...@@ -25111,6 +25111,15 @@
"stylelint-config-recommended": "^3.0.0" "stylelint-config-recommended": "^3.0.0"
} }
}, },
"stylelint-prettier": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/stylelint-prettier/-/stylelint-prettier-1.1.2.tgz",
"integrity": "sha512-8QZ+EtBpMCXYB6cY0hNE3aCDKMySIx4Q8/malLaqgU/KXXa6Cj2KK8ulG1AJvUMD5XSSP8rOotqaCzR/BW6qAA==",
"dev": true,
"requires": {
"prettier-linter-helpers": "^1.0.0"
}
},
"stylelint-scss": { "stylelint-scss": {
"version": "3.17.2", "version": "3.17.2",
"resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-3.17.2.tgz", "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-3.17.2.tgz",
...@@ -93,6 +93,7 @@ ...@@ -93,6 +93,7 @@
"style-loader": "^0.20.3", "style-loader": "^0.20.3",
"stylelint": "^13.6.0", "stylelint": "^13.6.0",
"stylelint-config-standard": "^20.0.0", "stylelint-config-standard": "^20.0.0",
"stylelint-prettier": "^1.1.2",
"stylelint-scss": "^3.17.2", "stylelint-scss": "^3.17.2",
"terser-webpack-plugin": "^2.3.6", "terser-webpack-plugin": "^2.3.6",
"ts-jest": "^24.3.0", "ts-jest": "^24.3.0",
...@@ -492,11 +493,14 @@ ...@@ -492,11 +493,14 @@
}, },
"stylelint": { "stylelint": {
"plugins": [ "plugins": [
"stylelint-scss" "stylelint-scss",
"stylelint-prettier"
], ],
"rules": { "rules": {
"prettier/prettier": true,
"scss/dollar-variable-colon-space-after": "always", "scss/dollar-variable-colon-space-after": "always",
"no-descending-specificity": null, "no-descending-specificity": null,
"block-no-empty": null,
"at-rule-no-unknown": [ "at-rule-no-unknown": [
true, true,
{ {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment