Repository: uber-web/baseui
Pull request #304 (2018-09-25 16:21:20)

feat(spinner): spinner component implementation


Patch:
diff --git a/rfcs/spinner-component.md b/rfcs/spinner-component.md
index c953ecab6..60580c2d1 100644
--- a/rfcs/spinner-component.md
+++ b/rfcs/spinner-component.md
@@ -10,25 +10,47 @@ import {Spinner} from 'baseui/spinner';
 
 export default () => {
   return
-    <Spinner size={10}/>;
+    <Spinner size={36}/>;
+};
+```
+
+### Advanced usage
+
+```javascript
+import * as React from 'react';
+import {Spinner} from 'baseui/spinner';
+
+export default () => {
+  return
+    <Spinner
+      overrides={{
+        Svg: {
+          style: ({$theme}) => ({
+            borderRadius: '50%',
+            backgroundColor: $theme.colors.primary50,
+          }),
+        },
+      }}
+    />;
 };
 ```
 
 ## Exports
 
 * `Spinner`
+* `StyledSvg`
 
 ## `Spinner` API
 
-* `size: ?number` - Optional
-  The size of the circle. Defaults to `44`.
-* `color: ?string` - Optional
-  The color to be used for the spinner
-* `backgroundColor: ?string` - Optional
-  The color to be used for the background circle
+* `size: number|string` - Optional, Defaults to 44
+  The size of the circle. Is passed to the underlying svg width/height style. Can also be a baseui `theme.sizing` constant like `scale500`
+* `color: string` - Optional, Defaults to `theme.primary450`
+  The color to be used for the spinner. Is used as an svg fill for the underlying svg icon
+* `title: string` - Optional, Defaults to `'Loading'`
+  Sets the SVG `<title>` label, which is used for accessibility
 
 ## Accessibility
 
-The following attribute should be applied to the element: `role="progressbar"`. To learn more check out the [MDN ProgressBar Role] article
+The following attribute is applied to the element: `role="progressbar"`. To learn more check out the [MDN ProgressBar Role] article
 
 [MDN ProgressBar Role]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_progressbar_role
diff --git a/src/icon/styled-components.js b/src/icon/styled-components.js
index ab52a78a2..68aebac3b 100644
--- a/src/icon/styled-components.js
+++ b/src/icon/styled-components.js
@@ -8,23 +8,22 @@ LICENSE file in the root directory of this source tree.
 import {styled} from '../styles';
 import type {StyledComponentParamsT} from './types';
 
-export const Svg = styled(
-  'svg',
-  ({$theme, $size, $color}: StyledComponentParamsT) => {
-    if ($size) {
-      if ($theme.sizing.hasOwnProperty($size)) {
-        $size = $theme.sizing[$size];
-      } else if (typeof $size === 'number') {
-        $size = `${$size}px`;
-      }
-    } else {
-      $size = $theme.sizing.scale800;
+export function getSvgStyles({$theme, $size, $color}: StyledComponentParamsT) {
+  if ($size) {
+    if ($theme.sizing.hasOwnProperty($size)) {
+      $size = $theme.sizing[$size];
+    } else if (typeof $size === 'number') {
+      $size = `${$size}px`;
     }
-    return {
-      display: 'inline-block',
-      fill: $color || 'currentColor',
-      height: $size,
-      width: $size,
-    };
-  },
-);
+  } else {
+    $size = $theme.sizing.scale800;
+  }
+  return {
+    display: 'inline-block',
+    fill: $color || 'currentColor',
+    height: $size,
+    width: $size,
+  };
+}
+
+export const Svg = styled('svg', getSvgStyles);
diff --git a/src/spinner/examples.js b/src/spinner/examples.js
new file mode 100644
index 000000000..30f07c02e
--- /dev/null
+++ b/src/spinner/examples.js
@@ -0,0 +1,67 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+import * as React from 'react';
+import {styled} from '../styles';
+import {Spinner} from './index';
+
+export const suite = 'Spinner Test Suite';
+export const tests = {
+  SIMPLE_EXAMPLE: 'Spinner component',
+  STYLE_PROPS_OVERRIDES: 'Spinner style and props overrides',
+};
+
+const Centered = styled('div', {
+  position: 'relative',
+  display: 'flex',
+  flexDirection: 'row',
+  justifyContent: 'center',
+  alignItems: 'center',
+  height: '90vh',
+  lineHeight: 1.5,
+});
+
+const Spacing = styled('span', {
+  margin: '10px',
+});
+
+export default {
+  [tests.SIMPLE_EXAMPLE]: function Story1() {
+    return (
+      <Centered>
+        <Spacing>
+          <Spinner size={22} />
+        </Spacing>
+        <Spacing>
+          <Spinner />
+        </Spacing>
+        <Spacing>
+          <Spinner size={88} />
+        </Spacing>
+      </Centered>
+    );
+  },
+  [tests.STYLE_PROPS_OVERRIDES]: function Story3() {
+    return (
+      <Centered>
+        <Spinner
+          overrides={{
+            Svg: {
+              props: {
+                'data-label': 'data-label',
+              },
+              style: ({$theme}) => ({
+                borderRadius: '50%',
+                backgroundColor: $theme.colors.primary50,
+              }),
+            },
+          }}
+        />
+      </Centered>
+    );
+  },
+};
diff --git a/src/spinner/index.js b/src/spinner/index.js
new file mode 100644
index 000000000..e450b6f49
--- /dev/null
+++ b/src/spinner/index.js
@@ -0,0 +1,12 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+export {default as Spinner} from './spinner';
+// Styled elements
+export {Svg as StyledSvg} from './styled-components';
+// Flow
+export * from './types';
diff --git a/src/spinner/spinner.js b/src/spinner/spinner.js
new file mode 100644
index 000000000..af3c8587c
--- /dev/null
+++ b/src/spinner/spinner.js
@@ -0,0 +1,36 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+import * as React from 'react';
+import {mergeOverrides} from '../helpers/overrides';
+import {Spinner as SpinnerIcon} from '../icon';
+import {Svg as StyledSvg} from './styled-components';
+
+import type {SpinnerPropsT} from './types';
+
+class Spinner extends React.Component<SpinnerPropsT> {
+  static defaultProps: $Shape<SpinnerPropsT> = {
+    color: '',
+    size: 44,
+    title: 'Loading',
+    overrides: {},
+  };
+
+  render() {
+    const overrides = mergeOverrides({Svg: StyledSvg}, this.props.overrides);
+    return (
+      <SpinnerIcon
+        role="progressbar"
+        viewBox="3 3 18 18"
+        {...this.props}
+        overrides={overrides}
+      />
+    );
+  }
+}
+
+export default Spinner;
diff --git a/src/spinner/stories.js b/src/spinner/stories.js
new file mode 100644
index 000000000..9c77e2323
--- /dev/null
+++ b/src/spinner/stories.js
@@ -0,0 +1,21 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+/* global module */
+import {storiesOf} from '@storybook/react';
+import {withReadme} from 'storybook-readme';
+import examples from './examples';
+
+//$FlowFixMe
+import SpinnerReadme from '../../rfcs/spinner-component.md';
+
+Object.entries(examples).forEach(([description, example]) =>
+  storiesOf('Spinner', module)
+    .addDecorator(withReadme(SpinnerReadme))
+    // $FlowFixMe
+    .add(description, example),
+);
diff --git a/src/spinner/styled-components.js b/src/spinner/styled-components.js
new file mode 100644
index 000000000..e29d7b788
--- /dev/null
+++ b/src/spinner/styled-components.js
@@ -0,0 +1,34 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+import {styled} from '../styles';
+import {getSvgStyles} from '../icon/styled-components';
+
+const spin = {
+  from: {
+    transform: 'rotate(0deg)',
+  },
+  to: {
+    transform: 'rotate(360deg)',
+  },
+};
+
+/**
+ * Spinner icon overrides
+ */
+export const Svg = styled('svg', props => {
+  const {$theme, $color} = props;
+  return {
+    ...getSvgStyles(props),
+    fill: $color || $theme.colors.primary450,
+    cursor: 'wait',
+    animationName: spin,
+    animationDuration: $theme.animation.timing700,
+    animationIterationCount: 'infinite',
+    animationTimingFunction: 'linear',
+  };
+});
diff --git a/src/spinner/types.js b/src/spinner/types.js
new file mode 100644
index 000000000..53da01c8c
--- /dev/null
+++ b/src/spinner/types.js
@@ -0,0 +1,8 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+export type {IconPropsT as SpinnerPropsT} from '../icon/types';


Test patch:
diff --git a/src/spinner/__tests__/__snapshots__/styled-components.test.js.snap b/src/spinner/__tests__/__snapshots__/styled-components.test.js.snap
new file mode 100644
index 000000000..be9cd03a1
--- /dev/null
+++ b/src/spinner/__tests__/__snapshots__/styled-components.test.js.snap
@@ -0,0 +1,64 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Spinner styled components StyledSvg - basic render: StyledSvg has correct default styles 1`] = `
+Object {
+  "animationDuration": "$theme.animation.timing700",
+  "animationIterationCount": "infinite",
+  "animationName": Object {
+    "from": Object {
+      "transform": "rotate(0deg)",
+    },
+    "to": Object {
+      "transform": "rotate(360deg)",
+    },
+  },
+  "animationTimingFunction": "linear",
+  "cursor": "wait",
+  "display": "inline-block",
+  "fill": "$theme.colors.primary450",
+  "height": "$theme.sizing.scale800",
+  "width": "$theme.sizing.scale800",
+}
+`;
+
+exports[`Spinner styled components StyledSvg - custom color: StyledSvg has correct fill value 1`] = `
+Object {
+  "animationDuration": "$theme.animation.timing700",
+  "animationIterationCount": "infinite",
+  "animationName": Object {
+    "from": Object {
+      "transform": "rotate(0deg)",
+    },
+    "to": Object {
+      "transform": "rotate(360deg)",
+    },
+  },
+  "animationTimingFunction": "linear",
+  "cursor": "wait",
+  "display": "inline-block",
+  "fill": "$theme.colors.primary450",
+  "height": "$theme.sizing.scale800",
+  "width": "$theme.sizing.scale800",
+}
+`;
+
+exports[`Spinner styled components StyledSvg - custom size: StyledSvg has correct height and width styles 1`] = `
+Object {
+  "animationDuration": "$theme.animation.timing700",
+  "animationIterationCount": "infinite",
+  "animationName": Object {
+    "from": Object {
+      "transform": "rotate(0deg)",
+    },
+    "to": Object {
+      "transform": "rotate(360deg)",
+    },
+  },
+  "animationTimingFunction": "linear",
+  "cursor": "wait",
+  "display": "inline-block",
+  "fill": "$theme.colors.primary450",
+  "height": "$theme.sizing.scale800",
+  "width": "$theme.sizing.scale800",
+}
+`;
diff --git a/src/spinner/__tests__/component.test.js b/src/spinner/__tests__/component.test.js
new file mode 100644
index 000000000..f2edb9d99
--- /dev/null
+++ b/src/spinner/__tests__/component.test.js
@@ -0,0 +1,43 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+
+import React from 'react';
+import {mount} from 'enzyme';
+import {Spinner} from '../index';
+import {Spinner as SpinnerIcon} from '../../icon';
+
+describe('Spinner', () => {
+  test('renders spinner icon', () => {
+    let renderedIcon;
+    const spinner = mount(<Spinner />);
+
+    // renderedRoot = wrapper.find(StyledSvg).first();
+    renderedIcon = spinner.find(SpinnerIcon).first();
+    expect(renderedIcon).toExist();
+
+    // pass new size value set to 56
+    spinner.setProps({size: 56});
+
+    renderedIcon = spinner.find(SpinnerIcon).first();
+    expect(renderedIcon.props().size).toBe(56);
+  });
+
+  test('component overrides', () => {
+    const overrides = {
+      Svg: jest
+        .fn()
+        .mockImplementation(({children}) => <span>{children}</span>),
+    };
+    const wrapper = mount(
+      // $FlowFixMe
+      <Spinner overrides={overrides} />,
+    );
+    const root = wrapper.find(overrides.Svg);
+    expect(root).toHaveLength(1);
+  });
+});
diff --git a/src/spinner/__tests__/styled-components.test.js b/src/spinner/__tests__/styled-components.test.js
new file mode 100644
index 000000000..ddce9191d
--- /dev/null
+++ b/src/spinner/__tests__/styled-components.test.js
@@ -0,0 +1,31 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+import React from 'react';
+import {shallow} from 'enzyme';
+import {StyledSvg} from '../index';
+
+describe('Spinner styled components', () => {
+  test('StyledSvg - basic render', () => {
+    const component = shallow(<StyledSvg />);
+    expect(component.instance().getStyles()).toMatchSnapshot(
+      'StyledSvg has correct default styles',
+    );
+  });
+  test('StyledSvg - custom size', () => {
+    const component = shallow(<StyledSvg size={56} />);
+    expect(component.instance().getStyles()).toMatchSnapshot(
+      'StyledSvg has correct height and width styles',
+    );
+  });
+  test('StyledSvg - custom color', () => {
+    const component = shallow(<StyledSvg color="#00FF00" />);
+    expect(component.instance().getStyles()).toMatchSnapshot(
+      'StyledSvg has correct fill value',
+    );
+  });
+});