From 10c871dd5105eb4d437fdd0beb757c2d985cb848 Mon Sep 17 00:00:00 2001 From: David Floegel Date: Fri, 14 Feb 2020 10:14:31 +0000 Subject: [PATCH] Release v7.1.0 (#239) * Add search icon (#226) * add an error & loading state to the Select (#235) * Auto-Scale select on open (#236) * add initialWidth prop to select for auto-scaling * re-order navbar items in mobile view (#237) Co-authored-by: Jamie Co-authored-by: Daniel --- .circleci/config.yml | 1 - package.json | 2 +- src/atoms/Icon/index.tsx | 3 + src/atoms/Spinner/README.md | 20 ++++ src/atoms/Spinner/index.tsx | 36 ++++++ src/index.tsx | 1 + src/molecules/Navbar/Collapsible.tsx | 6 +- src/molecules/Navbar/NavbarContainer.tsx | 33 +++--- src/molecules/Select/Input.test.tsx | 68 ++++++++++- src/molecules/Select/Input.tsx | 143 +++++++++++++++-------- src/molecules/Select/Options/index.tsx | 129 ++++++++++++-------- src/molecules/Select/README.md | 44 ++++--- src/molecules/Select/Select.test.tsx | 20 ++++ src/molecules/Select/Typeahead.test.tsx | 6 +- src/molecules/Select/index.tsx | 13 +++ storybook/stories/Header.stories.tsx | 33 ++---- storybook/stories/Select.stories.tsx | 23 +++- storybook/stories/Spinner.stories.tsx | 29 +++++ 18 files changed, 447 insertions(+), 163 deletions(-) create mode 100644 src/atoms/Spinner/README.md create mode 100644 src/atoms/Spinner/index.tsx create mode 100644 storybook/stories/Spinner.stories.tsx diff --git a/.circleci/config.yml b/.circleci/config.yml index 2823f089..62f06624 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -48,4 +48,3 @@ workflows: branches: only: - master - diff --git a/package.json b/package.json index 9f62eefd..72262b30 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sofarsounds/maestro", - "version": "7.0.0", + "version": "7.1.0", "description": "The official sofar sounds react uikit library", "main": "dist/index.js", "scripts": { diff --git a/src/atoms/Icon/index.tsx b/src/atoms/Icon/index.tsx index 51b17228..45c22082 100644 --- a/src/atoms/Icon/index.tsx +++ b/src/atoms/Icon/index.tsx @@ -58,6 +58,7 @@ interface IconProps { size?: string; color?: Colors; className?: string; + title?: string; 'data-qaid'?: string; id?: string; } @@ -65,6 +66,7 @@ const Icon: React.SFC = ({ name, size, color, + title, className = '', 'data-qaid': qaId, id @@ -74,6 +76,7 @@ const Icon: React.SFC = ({ className={`icon-${name} ${className}`} size={size} color={color} + title={title} data-qaid={qaId} /> ); diff --git a/src/atoms/Spinner/README.md b/src/atoms/Spinner/README.md new file mode 100644 index 00000000..bf51c50a --- /dev/null +++ b/src/atoms/Spinner/README.md @@ -0,0 +1,20 @@ +# Spinner + +To implement `Spinner` into your project you'll need to add this import +```js +import { Spinner } from '@sofarsounds/maestro' +``` + +After adding the import you can use it simply like this +```html + +``` + +## Props +Table below contains all types of props available in the Spinner component + +| Name | Type | Default | Description | +| :------------ | :----- | :-------------- | :------------------------------- | +| size | `string` | `25px` | The size of the spinner in pixels +| invert | `Boolean` | `false` | Whether the invert the color of the spinner +| data-qaid | `string` | | Optional prop for testing purposes diff --git a/src/atoms/Spinner/index.tsx b/src/atoms/Spinner/index.tsx new file mode 100644 index 00000000..c4d2d7eb --- /dev/null +++ b/src/atoms/Spinner/index.tsx @@ -0,0 +1,36 @@ +import styled, { css, keyframes } from '../../lib/styledComponents'; + +const load8 = keyframes` + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +`; + +interface Props { + size?: string; + invert?: boolean; + 'data-qaid'?: string; +} + +export default styled.div` + ${({ theme, size = '25px', invert }) => css` + &:after { + border-radius: 50%; + width: 10em; + height: 10em; + } + position: relative; + border-top: 2px solid transparent; + border-right: 2px solid transparent; + border-bottom: 2px solid transparent; + border-left: 2px solid ${theme.colors[invert ? 'whiteDenim' : 'macyGrey']}; + transform: translateZ(0); + animation: ${load8} 0.5s infinite linear; + border-radius: 50%; + width: ${size}; + height: ${size}; + `}; +`; diff --git a/src/index.tsx b/src/index.tsx index 9298d207..43f22220 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -21,6 +21,7 @@ export { default as Icons } from './atoms/Icon/registry'; export { default as Textfield } from './atoms/Textfield'; export { default as Textarea } from './atoms/Textarea'; export { default as Spacer } from './atoms/Spacer'; +export { default as Spinner } from './atoms/Spinner'; export { default as Radio } from './atoms/Radio'; export { default as Checkbox } from './atoms/Checkbox'; export { default as Responsive } from './atoms/Responsive'; diff --git a/src/molecules/Navbar/Collapsible.tsx b/src/molecules/Navbar/Collapsible.tsx index bfa01871..35483671 100644 --- a/src/molecules/Navbar/Collapsible.tsx +++ b/src/molecules/Navbar/Collapsible.tsx @@ -11,8 +11,8 @@ export default styled.div` ${open && css` display: flex; - flex-direction: column-reverse; - justify-content: flex-end; + flex-direction: column; + justify-content: flex-start; position: fixed; top: ${theme.dimensions.navbarHeight.xs}; left: 0; @@ -20,7 +20,7 @@ export default styled.div` bottom: 0; background: #000; border-top: 1px solid ${theme.colors.paintItBlack}; - padding: 0 15px; + padding: ${theme.ruler[4]}px; `} ${theme.media.md` diff --git a/src/molecules/Navbar/NavbarContainer.tsx b/src/molecules/Navbar/NavbarContainer.tsx index bdb7e780..571179e4 100644 --- a/src/molecules/Navbar/NavbarContainer.tsx +++ b/src/molecules/Navbar/NavbarContainer.tsx @@ -17,24 +17,25 @@ export default styled.div` align-items: center; justify-content: center; z-index: ${theme.zIndex.navbar}; - ${position === 'absolute' && - css` - position: absolute; - `} - ${position === 'fixed' && - css` - position: fixed; - top: 0; - left: 0; - right: 0; - `} - ${transparent && - theme.media.md` + ${position === 'absolute' && + css` + position: absolute; + `}; + ${position === 'fixed' && + css` + position: fixed; + top: 0; + left: 0; + right: 0; + `}; + ${transparent && + theme.media.md` background: transparent; - `} - ${theme.media.md` + `}; + ${theme.media.md` height: ${theme.dimensions.navbarHeight.md}; - `} ${theme.media.lg` + `}; + ${theme.media.lg` height: ${theme.dimensions.navbarHeight.lg}; `}; `} diff --git a/src/molecules/Select/Input.test.tsx b/src/molecules/Select/Input.test.tsx index c03d9d53..dec5c9c0 100644 --- a/src/molecules/Select/Input.test.tsx +++ b/src/molecules/Select/Input.test.tsx @@ -5,7 +5,7 @@ import Icon from '../../atoms/Icon'; import Input from './Input'; const setup = (props: any) => - renderWithTheme(); + renderWithTheme(); describe('', () => { @@ -82,6 +82,38 @@ describe('', () => { }); }); + it('has the correct style attributes when initialWidth is provided', () => { + const { container } = setup({ initialWidth: '200px' }); + const wrapper = container.firstChild; + + checkStyleRules(wrapper, { + width: '100%', + 'max-width': '200px' + }); + + checkStyleRules( + wrapper, + { + 'max-width': '100%' + }, + { modifier: ':hover' } + ); + + checkStyleRules( + wrapper, + { + 'max-width': '100%' + }, + { modifier: ':focus' } + ); + + checkStyleRules( + wrapper, + { + 'max-width': '100%' + }, + { modifier: ':active' } + ); + }); + it('has a pointer cursor when readOnly is true', () => { const { container } = setup({ inputProps: { readOnly: true } diff --git a/src/molecules/Select/Input.tsx b/src/molecules/Select/Input.tsx index d96c6bb4..3a3a0211 100644 --- a/src/molecules/Select/Input.tsx +++ b/src/molecules/Select/Input.tsx @@ -4,7 +4,9 @@ import styled, { css } from '../../lib/styledComponents'; import { withTextfieldStyle, withShadow } from '../../util'; import { InputProps } from '../../typings/input'; +import { SelectState } from './index'; import Icon from '../../atoms/Icon'; +import Spinner from '../../atoms/Spinner'; interface SelectInputProps { isOpen?: boolean; @@ -18,6 +20,7 @@ interface Wrapper { hasError?: boolean; invertColor?: boolean; readOnly?: boolean; + initialWidth?: string; } interface Props { @@ -27,6 +30,8 @@ interface Props { innerRef?: React.RefObject; hasError?: boolean; renderLeftIcon?: React.ReactNode; + state: SelectState; + initialWidth?: string; 'data-qaid'?: string; inputProps: { @@ -47,7 +52,7 @@ interface ButtonProps { } const Wrapper = styled.div` - ${({ theme, readOnly, invertColor, isOpen }) => css` + ${({ theme, readOnly, invertColor, isOpen, initialWidth }) => css` display: flex; flex: center; align-items: center; @@ -77,6 +82,23 @@ const Wrapper = styled.div` border-color: ${theme.colors.whiteDenim}; } `} + + ${initialWidth && + css` + width: 100%; + max-width: ${initialWidth}; + + &:focus, + &:hover, + &:active { + max-width: 100%; + } + + ${isOpen && + css` + max-width: 100%; + `} + `} `} `; @@ -164,61 +186,86 @@ const Input: React.SFC = ({ hasError, invertColor, renderLeftIcon, + state, + initialWidth, 'data-qaid': qaId -}) => ( - - {renderLeftIcon && ( - - {renderLeftIcon} - - )} - - { + const enableClearButton = + state === SelectState.ready && + !inputProps.readOnly && + inputProps.onClear && + inputProps.value; + + return ( + + initialWidth={initialWidth} + data-qaid={qaId} + > + {renderLeftIcon && ( + + {renderLeftIcon} + + )} + + + + {state === SelectState.loading && ( +
+ +
+ )} + + {state === SelectState.error && ( + + )} + + {enableClearButton && ( + + + + )} - {!inputProps.readOnly && inputProps.onClear && inputProps.value && ( - + - )} - - - - -
-); - +
+ ); +}; export default Input; diff --git a/src/molecules/Select/Options/index.tsx b/src/molecules/Select/Options/index.tsx index af3ebe7b..118a8a3e 100644 --- a/src/molecules/Select/Options/index.tsx +++ b/src/molecules/Select/Options/index.tsx @@ -6,6 +6,7 @@ import MenuHeader from '../../../atoms/MenuHeader'; import Popper from '../../../atoms/Popper'; import Portal from '../../../atoms/Portal'; +import { SelectState } from '../index'; import SimpleOptions from './Simple'; import GroupedOptions from './Grouped'; @@ -24,6 +25,7 @@ interface Props extends OptionsListProps { innerRef: React.RefObject; onOptionClick: (option: T) => void; groupBy?: (option: T) => string; + state?: SelectState; } const AdvancedMenu = styled(Menu)<{ isOpen: boolean; contactPoint: string }>` @@ -58,13 +60,90 @@ const Options = ({ popularOptions, getPopularOptionsTitle, userIsSearching, + state, groupBy }: Props) => { if (!isOpen) { return null; } - const showPopularOptions = !userIsSearching && popularOptions; + const showPopularOptions = popularOptions && !userIsSearching; + + const renderOptions = () => { + switch (state) { + case SelectState.loading: { + return ( + + Loading... + + ); + } + case SelectState.error: { + return ( + + Could not load options! + + ); + } + case SelectState.ready: { + if (options.length === 0) { + return ( + + No options... + + ); + } + + if (popularOptions && !userIsSearching) { + return ( + <> + + {getPopularOptionsTitle + ? getPopularOptionsTitle(popularOptions) + : `Top ${popularOptions.length}`} + ; + + + + + ); + } + + return ( + <> + {!groupBy && ( + + )} + + {groupBy && ( + + )} + + ); + } + } + + return null; + }; return ( @@ -88,53 +167,7 @@ const Options = ({ bordered data-qaid={`${qaId}-${showPopularOptions ? 'popular' : 'menu'}`} > - {options.length === 0 && ( - - No options... - - )} - - {showPopularOptions && popularOptions ? ( - <> - - {getPopularOptionsTitle - ? getPopularOptionsTitle(popularOptions) - : `Top ${popularOptions.length}`} - ; - - - - - ) : ( - <> - {!groupBy && ( - - )} - - {groupBy && ( - - )} - - )} + {renderOptions()} )} diff --git a/src/molecules/Select/README.md b/src/molecules/Select/README.md index 8573ad5d..c02c12c9 100644 --- a/src/molecules/Select/README.md +++ b/src/molecules/Select/README.md @@ -25,25 +25,35 @@ const cities = [ ## Implements -- [useDisableScroll](../../hooks/useDisableScroll) -- [useOutsideClick](../../hooks/useOutsideClick) -- [useKeyDown](../../hooks/useKeyDown) +- [useSelect](../../hooks/useSelect) ## Select Props Table below contains all types of props available in the Select component -| Name | Type | Default | Description | -| :------------ | :----- | :-------------- | :------------------------------- | -| **options** | `Function` | | An array of options to render in the select -| **placeholder** | `string` | | The placeholder/label to display before a value has been selected -| getOptionValue | `Function` | `id` | If the option key is a different field than `id` you can override the default behaviour -| getOptionLabel | `Function` | `title` | If the label you want to display is any other field than `title` you can override the default behaviour -| renderOption | `Function` | | Optional to render a custom option instead of the default one -| renderLeftIcon | `Function` | | Render an icon component on the left next to the label -| handleOptionClick | `Function` | | Optional funtion that's executed when selecting an option. Returns the options value -| positionFixed | `Boolean` | `false` | Set to true when the Select is positioned fixed on the screen. Otherwise the Menu will scroll away -| disableScrollWhenOpen | `Boolean` | | Disables body scroll when select is open -| id | `string` | | Default HTML id prop to identify the element -| data-qaid | `string` | | Optional prop for testing purposes - +| Name | Type | Default | Description | +| :------------ | :----- | :-------------- | :------------------------------- | +| **options** | `Array` | | An array of options to render in the select +| **onChange** | `Function` | | Callback for when an option has been selected. Returns the entire option object +| **placeholder** | `string` | | The placeholder/label to display before a value has been selected +| **getOptionLabel** | `Function` | | Required to determine what to render as the label. Returns the current option object +| defaultValue | `Option / null` | | If a default value has been set, pass in the reference matching object in. +| state | [Enum](#enum) | `ready` | The state of the select (ready, loading, or error) +| searchable | `Boolean` | | Whether the select is searchable +| renderOption | `Function` | | Optional to render a custom option instead of the default one +| renderLeftIcon | `Function` | | Render an icon component on the left next to the label +| popularOptions | `Array` | | A set of options that is displayed before the user starts filtering the list. Only works when `searchable` is `true`. +| getPopularOptionsTitle | `Function` | | Customise the title that is displayed for the group of popular options +| groupBy | `Function` | | A custom group by function to create a grouped select. +| disableScrollWhenOpen | `Boolean` | | Disables body scroll when select is open +| initialWidth | `String` | | Render the Select with a different width when closed to when opened +| id | `string` | | Default HTML id prop to identify the element by id +| name | `string` | | Default HTML id prop to identify the element by name +| data-qaid | `string` | | Optional prop for testing purposes + +### Enum +| type | +| :------- | +| ready | +| loading | +| error | diff --git a/src/molecules/Select/Select.test.tsx b/src/molecules/Select/Select.test.tsx index dff56f30..627c95d8 100644 --- a/src/molecules/Select/Select.test.tsx +++ b/src/molecules/Select/Select.test.tsx @@ -89,6 +89,26 @@ describe('