diff --git a/rfcs/header-navigation-component.md b/rfcs/header-navigation-component.md index 687fc16e02..8ee2048d55 100644 --- a/rfcs/header-navigation-component.md +++ b/rfcs/header-navigation-component.md @@ -6,7 +6,7 @@ ```javascript import * as React from 'react'; -import {HeaderNavigation, NavigationItem, NavigationList} from 'baseui/header-navigation'; +import {HeaderNavigation, StyledNavigationItem as NavigationItem, StyledNavigationList as NavigationList} from 'baseui/header-navigation'; import {Button, KIND} from 'baseui/button'; export default () => @@ -25,7 +25,7 @@ export default () => ```javascript import * as React from 'react'; -import {HeaderNavigation, NavigationItem, NavigationList} from 'baseui/header-navigation'; +import {HeaderNavigation, StyledNavigationItem as NavigationItem, StyledNavigationList as NavigationList} from 'baseui/header-navigation'; import {Button, KIND} from 'baseui/button'; import {StatefulMenu as Menu} from 'baseui/menu'; const ITEMS = [{label: 'menu item 1'}, {label: 'menu item 2'}]; @@ -54,8 +54,8 @@ export default (props) => + + + + +`; diff --git a/src/header-navigation/__tests__/__snapshots__/styled-components.test.js.snap b/src/header-navigation/__tests__/__snapshots__/styled-components.test.js.snap new file mode 100644 index 0000000000..2637d0e937 --- /dev/null +++ b/src/header-navigation/__tests__/__snapshots__/styled-components.test.js.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Header Navigation styled components NavigationItem NavigationItem: NavigationItem has correct styles 1`] = ` +Object { + "alignSelf": "center", + "paddingLeft": "$theme.sizing.scale800", +} +`; + +exports[`Header Navigation styled components NavigationList NavigationList: NavigationList has correct styles when set to align center 1`] = ` +Array [ + "align", +] +`; + +exports[`Header Navigation styled components NavigationList NavigationList: NavigationList has correct styles when set to align flex-end 1`] = ` +Array [ + "align", +] +`; + +exports[`Header Navigation styled components NavigationList NavigationList: NavigationList has correct styles when set to align flex-start 1`] = ` +Array [ + "align", +] +`; + +exports[`Header Navigation styled components StyledRoot Root: StyledRoot has correct styles 1`] = ` +Object { + "borderBottom": "1px solid $theme.colors.border", + "cursor": "pointer", + "display": "flex", + "fontFamily": "$theme.typography.font400.fontFamily", + "fontSize": "$theme.typography.font400.fontSize", + "fontWeight": "$theme.typography.font400.fontWeight", + "lineHeight": "$theme.typography.font400.lineHeight", + "paddingBottom": "$theme.sizing.scale500", + "paddingTop": "$theme.sizing.scale500", +} +`; diff --git a/src/header-navigation/__tests__/header-navigation.test.js b/src/header-navigation/__tests__/header-navigation.test.js new file mode 100644 index 0000000000..1127175b41 --- /dev/null +++ b/src/header-navigation/__tests__/header-navigation.test.js @@ -0,0 +1,42 @@ +/* +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 {styled} from '../../styles'; +import {HeaderNavigation} from '../index'; + +describe('Stateless header navigation', function() { + let wrapper, children; + let allProps: any = {}; + + beforeEach(function() { + children = 'Some tag'; + }); + + afterEach(function() { + jest.restoreAllMocks(); + wrapper && wrapper.unmount(); + }); + + test('should render component', function() { + wrapper = mount( + {children}, + ); + expect(wrapper).toMatchSnapshot('Component has correct render'); + }); + + test('should replace overriden root', function() { + const newRoot = styled('div', {color: 'red'}); + allProps.overrides = {Root: newRoot}; + wrapper = mount( + {children}, + ); + const renderedRoot = wrapper.find(allProps.overrides.Root); + expect(renderedRoot).toHaveLength(1); + }); +}); diff --git a/src/header-navigation/__tests__/styled-components.test.js b/src/header-navigation/__tests__/styled-components.test.js new file mode 100644 index 0000000000..59b08a3fec --- /dev/null +++ b/src/header-navigation/__tests__/styled-components.test.js @@ -0,0 +1,60 @@ +/* +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, mount} from 'enzyme'; +import { + StyledRoot, + StyledNavigationList, + StyledNavigationItem, + ALIGN, +} from '../index'; + +describe('Header Navigation styled components', () => { + describe('StyledRoot', function() { + test('Root', () => { + const component = shallow( + +
+ , + ); + expect(component.instance().getStyles()).toMatchSnapshot( + 'StyledRoot has correct styles', + ); + }); + }); + describe('NavigationList', function() { + test('NavigationList', () => { + const component = mount( + +
+ , + ); + Object.values(ALIGN).forEach(align => { + component.setProps({ + align, + }); + expect(component.instance().getStyles()).toMatchSnapshot( + // $FlowFixMe + `NavigationList has correct styles when set to align ${align}`, + ); + }); + }); + }); + describe('NavigationItem', function() { + test('NavigationItem', () => { + const component = shallow( + +
+ , + ); + expect(component.instance().getStyles()).toMatchSnapshot( + 'NavigationItem has correct styles', + ); + }); + }); +}); diff --git a/src/header-navigation/constants.js b/src/header-navigation/constants.js new file mode 100644 index 0000000000..7741aaafb4 --- /dev/null +++ b/src/header-navigation/constants.js @@ -0,0 +1,13 @@ +/* +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 const ALIGN = { + right: 'flex-end', + left: 'flex-start', + center: 'center', +}; diff --git a/src/header-navigation/examples-icons.js b/src/header-navigation/examples-icons.js new file mode 100644 index 0000000000..1df6e700c8 --- /dev/null +++ b/src/header-navigation/examples-icons.js @@ -0,0 +1,11 @@ +/* +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 function HamburgerIcon(stroke: string = 'black') { + return ``; +} diff --git a/src/header-navigation/examples-list.js b/src/header-navigation/examples-list.js new file mode 100644 index 0000000000..6e2dd7bdf3 --- /dev/null +++ b/src/header-navigation/examples-list.js @@ -0,0 +1,17 @@ +/* +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. +*/ + +/* eslint-env node */ +/* eslint-disable flowtype/require-valid-file-annotation */ +module.exports = { + HEADER_NAVIGATION_WITH_MENU_ELEMENT: 'With Dropdown Menu', + HEADER_NAVIGATION_WITH_RIGHT_MENU: 'With Right Menu', + HEADER_NAVIGATION_WITH_OVERRIDES: 'With changed styles', + HEADER_NAVIGATION_DIFFERENT_ALIGNMENTS: 'With alignments', + HEADER_NAVIGATION_SEARCH_BAR: 'With search bar', + HEADER_NAVIGATION_STYLED_NAV_LIST: 'With different styled Navigstion List', +}; diff --git a/src/header-navigation/examples.js b/src/header-navigation/examples.js new file mode 100644 index 0000000000..2c7cdb8491 --- /dev/null +++ b/src/header-navigation/examples.js @@ -0,0 +1,316 @@ +/* +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 +/* eslint-disable react/display-name*/ + +import * as React from 'react'; +import {withProps} from '../helpers'; +import {Button} from '../button'; +import {StatefulMenu as Menu, KEY_STRINGS} from '../menu'; +import {StatefulSelect as Search} from '../select'; +import {styled} from '../styles'; +import COLORS from '../select/examples-colors'; +import {HamburgerIcon} from './examples-icons'; + +import { + HeaderNavigation, + ALIGN, + StyledNavigationItem as NavigationItem, + StyledNavigationList as NavigationList, +} from './index'; +import tests from './examples-list'; +import {TYPE} from '../select'; + +const Link = withProps( + styled('a', props => ({ + ':hover': {color: props.$theme.colors.primary400}, + })), + {tabIndex: '0'}, +); +// $FlowFixMe +Link.displayName = 'Link'; + +const Hamburger = styled('div', props => ({ + lineHeight: 'initial', +})); +Hamburger.displayName = 'Hamburger'; + +const StyledHamburgerIcon = styled('span', props => ({ + ':after': { + content: `url('data:image/svg+xml;utf8,${HamburgerIcon( + props.$theme.colors.foreground, + )}');`, + }, +})); + +const ExtraStyledNavigationList = styled(NavigationList, props => ({ + backgroundColor: 'red', +})); + +let ITEMS = []; +for (let i = 0; i < 100; i++) { + ITEMS.push({label: `Item ${i}`}); +} +const options = { + options: COLORS, + labelKey: 'id', + valueKey: 'color', + placeholder: 'Choose a color', + maxDropdownHeight: '300px', +}; +class MenuContainer extends React.Component< + {isOpen: boolean, align: string}, + {isOpen: boolean}, +> { + static defaultProps = { + isOpen: false, + align: '', + }; + state = { + isOpen: this.props.isOpen, + }; + toggleMenu() { + this.setState({ + isOpen: !this.state.isOpen, + }); + } + render() { + return ( + + { + switch (e.key) { + case KEY_STRINGS.ArrowDown: + case KEY_STRINGS.Space: + case KEY_STRINGS.Enter: + this.toggleMenu(); + return; + } + }} + onClick={() => this.toggleMenu()} + > + + + {this.state.isOpen && ( + + )} + + ); + } +} + +export default { + [tests.HEADER_NAVIGATION_WITH_MENU_ELEMENT]: () => { + return ( + +
+ + + + + + Uber + + + + + Tab Link One + + + Tab Link Two + + + Tab Link Three + + + + + + + + + + ); + }, + [tests.HEADER_NAVIGATION_WITH_RIGHT_MENU]: () => { + return ( + +
+ + + + Tab Link One + + + Tab Link Two + + + Tab Link Three + + + + Uber + + + + + + + ); + }, + [tests.HEADER_NAVIGATION_WITH_OVERRIDES]: () => { + return ( + +
+ + + + + + Uber + + + + Tab Link One + + + Tab Link Two + + + Tab Link Three + + + + + + + + + + ); + }, + [tests.HEADER_NAVIGATION_SEARCH_BAR]: () => { + return ( + +
+ + + + + + Uber + + + + Tab Link One + + + Tab Link Two + + + Tab Link Three + + + + + props.option.id} + onChange={() => {}} + rows={8} + multi + /> + + + + + ); + }, + [tests.HEADER_NAVIGATION_DIFFERENT_ALIGNMENTS]: () => { + return ( + +
+ + + Uber + + + + Centered Tab Link One + + + Centered Tab Link Two + + + Centered Tab Link Three + + + + + Right Aligned Link + + + + + + + + + + ); + }, + [tests.HEADER_NAVIGATION_STYLED_NAV_LIST]: () => { + return ( + +
+ + + Uber + + + + Centered Tab Link One + + + Centered Tab Link Two + + + Centered Tab Link Three + + + + + Right Aligned Link + + + + + + + + + + ); + }, +}; diff --git a/src/header-navigation/header-navigation.js b/src/header-navigation/header-navigation.js new file mode 100644 index 0000000000..56c71f885f --- /dev/null +++ b/src/header-navigation/header-navigation.js @@ -0,0 +1,32 @@ +/* +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 {getOverrides} from '../helpers/overrides'; +import type {PropsT} from './types'; +import {Root as StyledRoot} from './styled-components'; + +class HeaderNavigation extends React.Component { + static defaultProps = { + overrides: {}, + }; + componentDidMount() { + if (__DEV__) { + // eslint-disable-next-line no-console + console.warn( + 'HeaderNavigation component is in a beta state, and may change without notice in the near future', + ); + } + } + render() { + const {overrides, ...restProps} = this.props; + const [Root, rootProps] = getOverrides(overrides.Root, StyledRoot); + return ; + } +} + +export default HeaderNavigation; diff --git a/src/header-navigation/index.js b/src/header-navigation/index.js new file mode 100644 index 0000000000..69648215ae --- /dev/null +++ b/src/header-navigation/index.js @@ -0,0 +1,16 @@ +/* +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 HeaderNavigation} from './header-navigation'; +// Styled elements +export { + Root as StyledRoot, + NavigationItem as StyledNavigationItem, + NavigationList as StyledNavigationList, +} from './styled-components'; +export {ALIGN} from './constants'; +export * from './types'; diff --git a/src/header-navigation/stories.js b/src/header-navigation/stories.js new file mode 100644 index 0000000000..c42830f3f9 --- /dev/null +++ b/src/header-navigation/stories.js @@ -0,0 +1,22 @@ +/* +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'; + +// Styled elements +import examples from './examples'; +import {withReadme} from 'storybook-readme'; +//$FlowFixMe +import HeaderNavigationReadme from '../../rfcs/header-navigation-component.md'; + +Object.entries(examples).forEach(([description, example]) => + storiesOf('Header Navigation', module) + .addDecorator(withReadme(HeaderNavigationReadme)) + // $FlowFixMe + .add(description, example), +); diff --git a/src/header-navigation/styled-components.js b/src/header-navigation/styled-components.js new file mode 100644 index 0000000000..92d5d87c76 --- /dev/null +++ b/src/header-navigation/styled-components.js @@ -0,0 +1,64 @@ +/* +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, asPrimaryExport} from '../styles'; +import {ALIGN} from './index'; + +export const Root = styled('nav', props => { + const {$theme} = props; + const { + sizing: {scale500}, + typography: {font400}, + colors: {border}, + } = $theme; + return { + ...font400, + cursor: 'pointer', + display: 'flex', + paddingBottom: scale500, + paddingTop: scale500, + borderBottom: `1px solid ${border}`, + }; +}); + +export const NavigationItem = styled('div', props => { + const {$theme} = props; + const { + sizing: {scale800}, + } = $theme; + return { + alignSelf: 'center', + paddingLeft: scale800, + }; +}); + +export const NavigationList: React.ComponentType<{ + align: string, + children: React$Node, +}> = (asPrimaryExport( + styled('div', props => { + const {$align, $theme} = props; + const { + sizing: {scale800}, + } = $theme; + return { + display: 'flex', + ':first-child': { + padding: 0, + }, + ':last-child': { + padding: 0, + }, + flex: $align === ALIGN.right || $align === ALIGN.left ? 'none' : '1', + paddingLeft: scale800, + paddingRight: scale800, + justifySelf: $align, + justifyContent: $align, + }; + }), + ['align'], +): React.ComponentType<*>); diff --git a/src/header-navigation/types.js b/src/header-navigation/types.js new file mode 100644 index 0000000000..410d2564b0 --- /dev/null +++ b/src/header-navigation/types.js @@ -0,0 +1,16 @@ +/* +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 type {OverrideT} from '../helpers/overrides'; + +export type OverridesT = { + Root?: OverrideT<*>, +}; + +export type PropsT = { + overrides: OverridesT, +}; diff --git a/src/styles/__mocks__/as-primary-export-hoc.js b/src/styles/__mocks__/as-primary-export-hoc.js new file mode 100644 index 0000000000..ab10df4b50 --- /dev/null +++ b/src/styles/__mocks__/as-primary-export-hoc.js @@ -0,0 +1,10 @@ +/* +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 './styled'; +const asPrimaryExport = styled; +export default asPrimaryExport; diff --git a/src/styles/as-primary-export-hoc.js b/src/styles/as-primary-export-hoc.js new file mode 100644 index 0000000000..66f956b0d3 --- /dev/null +++ b/src/styles/as-primary-export-hoc.js @@ -0,0 +1,26 @@ +/* +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'; + +// transforms props for native styled components adding $ symbol to avoid to supported warning +export default function asPrimaryExport( + StyledComponent: React.ComponentType<*>, + propsTransformNames: Array, +) { + return function withStyledPropsHOC(props: {}) { + const styledProps = Object.keys(props).reduce((acc, key) => { + if (key[0] === '$' || propsTransformNames.indexOf(key) < 0) { + acc[key] = props[key]; + } else if (propsTransformNames.indexOf(key) >= 0) { + acc['$' + key] = props[key]; + } + return acc; + }, {}); + return ; + }; +} diff --git a/src/styles/index.js b/src/styles/index.js index a90ceb699b..039d8feffd 100644 --- a/src/styles/index.js +++ b/src/styles/index.js @@ -6,6 +6,7 @@ LICENSE file in the root directory of this source tree. */ // @flow export {default as styled} from './styled'; +export {default as asPrimaryExport} from './as-primary-export-hoc'; export {hexToRgb} from './util'; export {default as ThemeProvider} from './theme-provider'; export type {ThemeT} from './types'; diff --git a/src/test/test-framework-setup.js b/src/test/test-framework-setup.js index f73b74e766..9ab6a67a5b 100644 --- a/src/test/test-framework-setup.js +++ b/src/test/test-framework-setup.js @@ -11,4 +11,5 @@ import toHaveStyleRule from './expect-to-have-style-rule'; expect.extend({toHaveStyleRule}); jest.mock('../styles/styled.js'); +jest.mock('../styles/as-primary-export-hoc'); jest.mock('../utils/get-bui-id.js');