From b695131d536daa5b24491707fd8455ee0e8b0791 Mon Sep 17 00:00:00 2001 From: Ivan Drinchev Date: Wed, 27 Nov 2019 15:45:23 +0100 Subject: [PATCH] feat: navigation (#91) --- README.md | 8 +- resources/lightelligence.svg | 6 + src/index.js | 8 +- src/layout/Frame/Frame.md | 8 -- src/layout/Frame/index.js | 1 - src/layout/RootContainer/RootContainer.js | 38 ++++++ src/layout/RootContainer/RootContainer.md | 121 ++++++++++++++++++ .../RootContainer/RootContainer.test.js | 34 +++++ src/layout/RootContainer/RootMainContainer.js | 44 +++++++ src/layout/RootContainer/RootMainContainer.md | 48 +++++++ .../RootContainer/RootMainContainer.test.js | 41 ++++++ src/layout/RootContainer/index.js | 2 + src/layout/Theme/Theme.js | 21 ++- src/layout/Theme/Theme.md | 4 +- src/navigation/Header/Header.js | 109 ++++++++++++++++ src/navigation/Header/Header.md | 23 ++++ src/navigation/Header/Header.test.js | 65 ++++++++++ src/navigation/Header/index.js | 1 + .../SecondarySidebar/SecondarySidebar.js | 108 ++++++++++++++++ .../SecondarySidebar/SecondarySidebar.md | 44 +++++++ .../SecondarySidebar/SecondarySidebar.test.js | 81 ++++++++++++ src/navigation/SecondarySidebar/index.js | 1 + src/navigation/Sidebar/Sidebar.js | 88 +++++++++++++ src/navigation/Sidebar/Sidebar.md | 56 ++++++++ src/navigation/Sidebar/Sidebar.test.js | 62 +++++++++ src/navigation/Sidebar/SidebarSeparator.js | 26 ++++ src/navigation/Sidebar/SidebarSeparator.md | 24 ++++ .../Sidebar/SidebarSeparator.test.js | 27 ++++ src/navigation/Sidebar/index.js | 2 + .../SidebarNavigation/SidebarNavigation.js | 30 +++++ .../SidebarNavigation/SidebarNavigation.md | 29 +++++ .../SidebarNavigation.test.js | 27 ++++ .../SidebarNavigationItem.js | 92 +++++++++++++ .../SidebarNavigationItem.md | 2 + .../SidebarNavigationItem.test.js | 84 ++++++++++++ .../SidebarSubNavigationItem.js | 56 ++++++++ .../SidebarSubNavigationItem.md | 2 + .../SidebarSubNavigationItem.test.js | 64 +++++++++ src/navigation/SidebarNavigation/index.js | 3 + .../SidebarSelector/SidebarSelectorFilter.js | 81 ++++++++++++ .../SidebarSelector/SidebarSelectorFilter.md | 15 +++ .../SidebarSelectorFilter.test.js | 67 ++++++++++ .../SidebarSelectorFilterItem.js | 48 +++++++ .../SidebarSelectorFilterItem.md | 2 + .../SidebarSelectorFilterItem.test.js | 50 ++++++++ .../SidebarSelectorProperty.js | 65 ++++++++++ .../SidebarSelectorProperty.md | 12 ++ .../SidebarSelectorProperty.test.js | 71 ++++++++++ .../SidebarSelector/SidebarSelectorTenant.js | 61 +++++++++ .../SidebarSelector/SidebarSelectorTenant.md | 8 ++ .../SidebarSelectorTenant.test.js | 70 ++++++++++ src/navigation/SidebarSelector/index.js | 4 + styleguide.config.js | 7 +- {src/layout/Frame => styleguide}/Frame.js | 0 54 files changed, 2032 insertions(+), 19 deletions(-) create mode 100644 resources/lightelligence.svg delete mode 100644 src/layout/Frame/Frame.md delete mode 100644 src/layout/Frame/index.js create mode 100644 src/layout/RootContainer/RootContainer.js create mode 100644 src/layout/RootContainer/RootContainer.md create mode 100644 src/layout/RootContainer/RootContainer.test.js create mode 100644 src/layout/RootContainer/RootMainContainer.js create mode 100644 src/layout/RootContainer/RootMainContainer.md create mode 100644 src/layout/RootContainer/RootMainContainer.test.js create mode 100644 src/layout/RootContainer/index.js create mode 100644 src/navigation/Header/Header.js create mode 100644 src/navigation/Header/Header.md create mode 100644 src/navigation/Header/Header.test.js create mode 100644 src/navigation/Header/index.js create mode 100644 src/navigation/SecondarySidebar/SecondarySidebar.js create mode 100644 src/navigation/SecondarySidebar/SecondarySidebar.md create mode 100644 src/navigation/SecondarySidebar/SecondarySidebar.test.js create mode 100644 src/navigation/SecondarySidebar/index.js create mode 100644 src/navigation/Sidebar/Sidebar.js create mode 100644 src/navigation/Sidebar/Sidebar.md create mode 100644 src/navigation/Sidebar/Sidebar.test.js create mode 100644 src/navigation/Sidebar/SidebarSeparator.js create mode 100644 src/navigation/Sidebar/SidebarSeparator.md create mode 100644 src/navigation/Sidebar/SidebarSeparator.test.js create mode 100644 src/navigation/Sidebar/index.js create mode 100644 src/navigation/SidebarNavigation/SidebarNavigation.js create mode 100644 src/navigation/SidebarNavigation/SidebarNavigation.md create mode 100644 src/navigation/SidebarNavigation/SidebarNavigation.test.js create mode 100644 src/navigation/SidebarNavigation/SidebarNavigationItem.js create mode 100644 src/navigation/SidebarNavigation/SidebarNavigationItem.md create mode 100644 src/navigation/SidebarNavigation/SidebarNavigationItem.test.js create mode 100644 src/navigation/SidebarNavigation/SidebarSubNavigationItem.js create mode 100644 src/navigation/SidebarNavigation/SidebarSubNavigationItem.md create mode 100644 src/navigation/SidebarNavigation/SidebarSubNavigationItem.test.js create mode 100644 src/navigation/SidebarNavigation/index.js create mode 100644 src/navigation/SidebarSelector/SidebarSelectorFilter.js create mode 100644 src/navigation/SidebarSelector/SidebarSelectorFilter.md create mode 100644 src/navigation/SidebarSelector/SidebarSelectorFilter.test.js create mode 100644 src/navigation/SidebarSelector/SidebarSelectorFilterItem.js create mode 100644 src/navigation/SidebarSelector/SidebarSelectorFilterItem.md create mode 100644 src/navigation/SidebarSelector/SidebarSelectorFilterItem.test.js create mode 100644 src/navigation/SidebarSelector/SidebarSelectorProperty.js create mode 100644 src/navigation/SidebarSelector/SidebarSelectorProperty.md create mode 100644 src/navigation/SidebarSelector/SidebarSelectorProperty.test.js create mode 100644 src/navigation/SidebarSelector/SidebarSelectorTenant.js create mode 100644 src/navigation/SidebarSelector/SidebarSelectorTenant.md create mode 100644 src/navigation/SidebarSelector/SidebarSelectorTenant.test.js create mode 100644 src/navigation/SidebarSelector/index.js rename {src/layout/Frame => styleguide}/Frame.js (100%) diff --git a/README.md b/README.md index 811b8b8..ff92426 100644 --- a/README.md +++ b/README.md @@ -26,19 +26,19 @@ design system. It is a React.js implementation of ## Usage Make sure to include the bundled CSS in your React Application as well as -wrapping your content in [``](https://lightelligence-io.github.io/react/#/Components/Frame) +wrapping your content in [``](https://lightelligence-io.github.io/react/#/Layout/RootContainer) component. ```jsx import React from 'react'; import ReactDOM from 'react-dom'; import '@lightelligence/react/dist/index.css'; -import { Button, Frame, COLOR_PRIMARY } from '@lightelligence/react'; +import { Button, RootContainer, COLOR_PRIMARY } from '@lightelligence/react'; const App = () => ( - + - + ); ReactDOM.render(, document.getElementById('root')); diff --git a/resources/lightelligence.svg b/resources/lightelligence.svg new file mode 100644 index 0000000..f812d70 --- /dev/null +++ b/resources/lightelligence.svg @@ -0,0 +1,6 @@ + + + + diff --git a/src/index.js b/src/index.js index 3336d36..0a82058 100644 --- a/src/index.js +++ b/src/index.js @@ -39,9 +39,15 @@ export * from './controls/TextArea'; export * from './controls/Input'; export * from './layout/Container'; -export * from './layout/Frame'; export * from './layout/Grid'; export * from './layout/Theme'; +export * from './layout/RootContainer'; + +export * from './navigation/Header'; +export * from './navigation/Sidebar'; +export * from './navigation/SidebarNavigation'; +export * from './navigation/SidebarSelector'; +export * from './navigation/SecondarySidebar'; export * from './constants'; diff --git a/src/layout/Frame/Frame.md b/src/layout/Frame/Frame.md deleted file mode 100644 index 585cf75..0000000 --- a/src/layout/Frame/Frame.md +++ /dev/null @@ -1,8 +0,0 @@ -```jsx -import { Frame, Card } from '@lightelligence/react'; - - - Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. - - -``` diff --git a/src/layout/Frame/index.js b/src/layout/Frame/index.js deleted file mode 100644 index 70ff725..0000000 --- a/src/layout/Frame/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './Frame'; diff --git a/src/layout/RootContainer/RootContainer.js b/src/layout/RootContainer/RootContainer.js new file mode 100644 index 0000000..c5956d9 --- /dev/null +++ b/src/layout/RootContainer/RootContainer.js @@ -0,0 +1,38 @@ +import { string, node } from 'prop-types'; +import React from 'react'; +import classnames from 'classnames'; +import * as olt from '@lightelligence/styles'; + +/** + * Use the RootContainer component to build a layout of your application. + * + * It gives you a wrapper that will properly position : + * + * - [Header Component](#/Navigation/Header) + * - [Sidebar Component](#/Navigation/Sidebar) + * - [SecondarySidebar Component](#/Navigation/SecondarySidebar) + * - [RootMainContainer Component](#/Layout/RootMainContainer) + */ +export const RootContainer = ({ className, children, ...props }) => ( +
+ {children} +
+); + +RootContainer.propTypes = { + /** + * Forward an additional className to the underlying element + */ + className: string, + /** + * The body of the layout. To work correctly you must include + * [Header](#/Navigation/Header), [Sidebar](#/Navigation/Sidebar) and + * [RootMainContainer](#/Layout/RootMainContainer) + */ + children: node, +}; + +RootContainer.defaultProps = { + className: null, + children: null, +}; diff --git a/src/layout/RootContainer/RootContainer.md b/src/layout/RootContainer/RootContainer.md new file mode 100644 index 0000000..bda3369 --- /dev/null +++ b/src/layout/RootContainer/RootContainer.md @@ -0,0 +1,121 @@ +### Example + +Note that in this example we take care of triggering the +[SecondarySidebar](#/Navigation/SecondarySidebar) with a simple use of +`React.useState`. The user has total control of the interaction between the +[Header](#/Navigation/Header), [Sidebar](#/Navigation/Sidebar) and +[SecondarySidebar](#/Navigation/SecondarySidebar) + +```js +import { MemoryRouter } from 'react-router'; +import { + RootContainer, + Header, + ActionButton, + Tabs, + Tab, + Sidebar, + SidebarSeparator, + SidebarSelectorProperty, + SidebarSelectorTenant, + SidebarSelectorFilter, + SidebarSelectorFilterItem, + SidebarNavigation, + SidebarNavigationItem, + SidebarSubNavigationItem, + RootMainContainer, + SecondarySidebar, + Card, +} from '@lightelligence/react'; + +const logo = require('../../../resources/lightelligence.svg'); + +const [currentNavigation, setCurrentNavigation] = React.useState(false); + +const MyHeader = () => ( +
} + left={ + + } + right={ + + } + > + + + + + + +
+); + +const MySidebar = () => ( + + setCurrentNavigation( + currentNavigation === 'property' ? null : 'property', + ) + } + />, + alert('tenant selector')} + />, + ]} + > + + setCurrentNavigation(currentNavigation === 'filter' ? null : 'filter') + } + > + + + + + + + + + + + + + + + + + +); + +
+ + + + + + + Content + + +
; +``` diff --git a/src/layout/RootContainer/RootContainer.test.js b/src/layout/RootContainer/RootContainer.test.js new file mode 100644 index 0000000..82d506f --- /dev/null +++ b/src/layout/RootContainer/RootContainer.test.js @@ -0,0 +1,34 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { RootContainer } from './RootContainer'; +import { oltStyles } from '../..'; + +const renderComponent = (props) => { + return render(); +}; + +describe('RootContainer', () => { + test('has oltStyles.Layout and oltStyles.Frame', () => { + const { getByTestId } = renderComponent(); + + const component = getByTestId('root-container'); + expect(component.classList.contains(oltStyles.Layout)).toBe(true); + expect(component.classList.contains(oltStyles.Frame)).toBe(true); + }); + test('forwards className', () => { + const { getByTestId } = renderComponent({ + className: 'myClass', + }); + + const component = getByTestId('root-container'); + expect(component.classList.contains('myClass')).toBe(true); + }); + test('renders children', () => { + const { getByText } = renderComponent({ + children: 'Foo', + }); + + const component = getByText('Foo'); + expect(component).toBeTruthy(); + }); +}); diff --git a/src/layout/RootContainer/RootMainContainer.js b/src/layout/RootContainer/RootMainContainer.js new file mode 100644 index 0000000..3dd2cd4 --- /dev/null +++ b/src/layout/RootContainer/RootMainContainer.js @@ -0,0 +1,44 @@ +import { node, string } from 'prop-types'; +import React from 'react'; +import classnames from 'classnames'; +import * as olt from '@lightelligence/styles'; + +/** + * The RootMainContainer is used in the + * [RootContainer Component](#/Layout/RootContainer) as the container of your + * body. + * + * It includes an Overlay, which is used to "blur" the content whenever + * a [SecondarySidebar](#/Navigation/SecondarySidebar) is active. + * + * The RootMainContainer passes all props to the container of the content, + * which is using the semantic `main` HTML element. + * + * The RootMainContainer also has predefined padding, according to the + * RootContainer's [Header](#/Navigation/Header) and + * [Sidebar](#/Navigation/Sidebar). + */ +export const RootMainContainer = ({ className, children, ...props }) => ( + <> +
+
+ {children} +
+ +); + +RootMainContainer.propTypes = { + /** + * Forward an additional className to the underlying element + */ + className: string, + /** + * Body of the layout is the main content of your application + */ + children: node, +}; + +RootMainContainer.defaultProps = { + className: null, + children: null, +}; diff --git a/src/layout/RootContainer/RootMainContainer.md b/src/layout/RootContainer/RootMainContainer.md new file mode 100644 index 0000000..fe9477b --- /dev/null +++ b/src/layout/RootContainer/RootMainContainer.md @@ -0,0 +1,48 @@ +### Example + +For better understanding on the interaction between the +[Header](#/Navigation/Header), [Sidebar](#/Navigation/Sidebar) and +[SecondarySidebar](#/Navigation/SecondarySidebar) please check the +[RootContainer](#/Layout/RootContainer). + +_Please note that the blurred overlay doesn't work in this example_ + +```js +import { + RootContainer, + Header, + Sidebar, + SidebarSeparator, + SidebarSelectorFilter, + SidebarSelectorFilterItem, + RootMainContainer, + SecondarySidebar, + Card, +} from '@lightelligence/react'; + +const [open, setOpen] = React.useState(false); + +
+ +
+ + setOpen(!open)}> + + + + + + + + My Application's content + + +
; +``` diff --git a/src/layout/RootContainer/RootMainContainer.test.js b/src/layout/RootContainer/RootMainContainer.test.js new file mode 100644 index 0000000..55ae050 --- /dev/null +++ b/src/layout/RootContainer/RootMainContainer.test.js @@ -0,0 +1,41 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { RootMainContainer } from './RootMainContainer'; +import { oltStyles } from '../..'; + +const renderComponent = (props) => { + return render( + , + ); +}; + +describe('RootMainContainer', () => { + test('has oltStyles.LayoutBody', () => { + const { getByTestId } = renderComponent(); + + const component = getByTestId('root-main-container'); + expect(component.classList.contains(oltStyles.LayoutBody)).toBe(true); + }); + test('forwards className', () => { + const { getByTestId } = renderComponent({ + className: 'myClass', + }); + + const component = getByTestId('root-main-container'); + expect(component.classList.contains('myClass')).toBe(true); + }); + test('renders children', () => { + const { getByText } = renderComponent({ + children: 'Foo', + }); + + const component = getByText('Foo'); + expect(component).toBeTruthy(); + }); + test('renders overlay', () => { + const { getByTestId } = renderComponent(); + const component = getByTestId('root-main-container').parentNode + .childNodes[0]; + expect(component.classList.contains(oltStyles.LayoutOverlay)).toBe(true); + }); +}); diff --git a/src/layout/RootContainer/index.js b/src/layout/RootContainer/index.js new file mode 100644 index 0000000..2999c57 --- /dev/null +++ b/src/layout/RootContainer/index.js @@ -0,0 +1,2 @@ +export * from './RootContainer'; +export * from './RootMainContainer'; diff --git a/src/layout/Theme/Theme.js b/src/layout/Theme/Theme.js index 403a460..60c5d35 100644 --- a/src/layout/Theme/Theme.js +++ b/src/layout/Theme/Theme.js @@ -9,6 +9,13 @@ import tinycolor from 'tinycolor2'; */ const primaryColorProperty = `--olt-primaryColor`; +/** + * The property name of the sidebar color + * + * @type {string} + */ +const sidebarColorProperty = '--olt-sidebarColor'; + /** * @typedef Mix * @property name {string} The name of the color to mix with @@ -56,7 +63,7 @@ const generateProperties = (color) => .reduce((result, item) => result.concat(item), []) .concat([{ name: primaryColorProperty, value: color }]); -export const Theme = ({ primaryColor, children }) => { +export const Theme = ({ primaryColor, sidebarColor, children }) => { const elementRef = useRef(null); useEffect(() => { @@ -69,7 +76,12 @@ export const Theme = ({ primaryColor, children }) => { ? style.setProperty(name, value) : style.removeProperty(name); }); - }, [elementRef, primaryColor]); + if (sidebarColor) { + style.setProperty(sidebarColorProperty, sidebarColor); + } else { + style.removeProperty(sidebarColorProperty); + } + }, [elementRef, primaryColor, sidebarColor]); return
{children}
; }; @@ -79,6 +91,10 @@ Theme.propTypes = { * Sets the primary color */ primaryColor: string, + /** + * Sets the sidebar color + */ + sidebarColor: string, /** * Children */ @@ -87,5 +103,6 @@ Theme.propTypes = { Theme.defaultProps = { primaryColor: null, + sidebarColor: null, children: null, }; diff --git a/src/layout/Theme/Theme.md b/src/layout/Theme/Theme.md index f56c12a..13af93d 100644 --- a/src/layout/Theme/Theme.md +++ b/src/layout/Theme/Theme.md @@ -1,5 +1,5 @@ _Theme_ gives you the opportunity to use white-labeling in your application, -by changing the `primaryColor` of the components. +by changing the `primaryColor` and the `sidebarColor` of the components. The white-labeling works only under the wrapped components inside the _Theme_ component, so a good place for that component would be in the root of your @@ -9,7 +9,7 @@ application. import { Theme, Chip, Button, Toggle } from '@lightelligence/react';
{/** Try changing the value here */} - + Chip diff --git a/src/navigation/Header/Header.js b/src/navigation/Header/Header.js new file mode 100644 index 0000000..7203860 --- /dev/null +++ b/src/navigation/Header/Header.js @@ -0,0 +1,109 @@ +import React from 'react'; +import { func, node, string, shape } from 'prop-types'; +import classnames from 'classnames'; +import * as olt from '@lightelligence/styles'; +import { ActionButton } from '../../components/ActionButton'; + +/** + * The Header of your application. + * + * It consist of + * + * - Logo container, used for placing an image with your logo + * - Left container, usually used with an Action Button rendered on the left + * - Right container, usually used with an Action Button rendered on the right + * - Content, which is rendered in the middle of the header + * - Mobile menu button, which is visible only on mobile devices + * + * The Header should be used in conjunction with + * [RootContainer](#/Layout/RootContainer) component. + * + * The Header component uses the semantic `header` HTML tag and passes all + * props to the `header` React Element. + * + * The Header by default also implements Action Button's proximity area, so + * whenever the user hovers on the header the action buttons are highlighted. + */ +export const Header = ({ + className, + children, + logo, + left, + right, + onClickMobileMenu, + menuButtonProps, + ...props +}) => ( +
+
{logo}
+
{left}
+
{children}
+
{right}
+
+ +
+
+); + +Header.propTypes = { + /** + * The body of the navigation. You can use any type of children there, + * however, most useful navigation component for this part of your + * header are [Tabs](#/Components/Tabs). + * + * The body of the header is positioned in the center. + */ + children: node, + /** + * Forward an additional className to the underlying element + */ + className: string, + /** + * Logo container is rendered on the far left side of the Header. It is a + * placeholder for your logo. Best used with `img` element. + */ + logo: node, + /** + * Left container is rendered on the left side of the Header, next to the + * Logo. It is usually filled with [ActionButton](#/Components/ActionButton). + * Useful for adding a Back button there giving the user a way to return to + * the previous navigation hierarchy. + */ + left: node, + /** + * Right container is rendered on the far right side of the Header. Can be + * any element, like an [ActionButton](#/Components/ActionButton), giving + * a way to the user for quickly logging out. + */ + right: node, + /** + * On clicking the menu button for mobile. The menu button is visible only + * on mobile and you can wire it to display the + * [Sidebar](#/Navigation/Sidebar). + */ + onClickMobileMenu: func, + /** + * Props passed to the mobile menu action button + */ + menuButtonProps: shape({}), +}; + +Header.defaultProps = { + children: null, + className: null, + logo: null, + left: null, + right: null, + onClickMobileMenu: null, + menuButtonProps: null, +}; diff --git a/src/navigation/Header/Header.md b/src/navigation/Header/Header.md new file mode 100644 index 0000000..a3d7917 --- /dev/null +++ b/src/navigation/Header/Header.md @@ -0,0 +1,23 @@ +### Example + +```jsx harmony +import { Header, ActionButton, Tabs, Tab } from '@lightelligence/react'; +const logo = require('../../../resources/lightelligence.svg'); +
} + left={ + + } + right={ + + } +> + + + + + + +
+``` diff --git a/src/navigation/Header/Header.test.js b/src/navigation/Header/Header.test.js new file mode 100644 index 0000000..3a34527 --- /dev/null +++ b/src/navigation/Header/Header.test.js @@ -0,0 +1,65 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { Header } from './Header'; +import { oltStyles } from '../..'; + +const renderComponent = (props) => { + return render(
); +}; + +describe('Header', () => { + test('forwards className', () => { + const { getByTestId } = renderComponent({ + className: 'myClass', + }); + + const component = getByTestId('header'); + expect(component.classList.contains('myClass')).toBe(true); + }); + + test('should be action button proximity area', () => { + const { getByTestId } = renderComponent(); + + const component = getByTestId('header'); + expect( + component.classList.contains(oltStyles.ActionButtonProximityArea), + ).toBe(true); + }); + + test('renders containers content', () => { + const { getByText } = renderComponent({ + left: 'Left Content', + right: 'Right Content', + logo: 'Logo Content', + children: 'Body Content', + }); + + const leftComponent = getByText('Left Content'); + const rightComponent = getByText('Right Content'); + const logoComponent = getByText('Logo Content'); + const bodyComponent = getByText('Body Content'); + + expect( + leftComponent.classList.contains(oltStyles.HeaderLeftContainer), + ).toBe(true); + expect( + rightComponent.classList.contains(oltStyles.HeaderRightContainer), + ).toBe(true); + expect(logoComponent).toBeTruthy(); + expect(bodyComponent.classList.contains(oltStyles.HeaderBody)).toBe(true); + }); + + test('should be able to catch mobile menu button', () => { + const onClickMobileMenu = jest.fn(); + const { getByTestId } = renderComponent({ + menuButtonProps: { + 'data-testid': 'mobile-menu', + }, + onClickMobileMenu, + }); + + const actionButton = getByTestId('mobile-menu'); + fireEvent.click(actionButton); + expect(onClickMobileMenu).toBeCalled(); + }); +}); diff --git a/src/navigation/Header/index.js b/src/navigation/Header/index.js new file mode 100644 index 0000000..266dec8 --- /dev/null +++ b/src/navigation/Header/index.js @@ -0,0 +1 @@ +export * from './Header'; diff --git a/src/navigation/SecondarySidebar/SecondarySidebar.js b/src/navigation/SecondarySidebar/SecondarySidebar.js new file mode 100644 index 0000000..5eb90e1 --- /dev/null +++ b/src/navigation/SecondarySidebar/SecondarySidebar.js @@ -0,0 +1,108 @@ +import React from 'react'; +import { func, node, string, bool, shape } from 'prop-types'; +import classnames from 'classnames'; +import * as olt from '@lightelligence/styles'; +import { ActionButton } from '../../components/ActionButton'; + +/** + * Secondary Sidebar is used when you want to render another sidebar next to + * the main [Sidebar](#/Navigation/Sidebar) component. + * + * It is rendered as fixed positioned `aside` html element and passes the + * corresponding `props` of the SecondarySidebar component. + * + * The Secondary Sidebar has an optional header that can be used, as well as + * mobile navigation controls, which can be used to create interaction + * when the user's device is a mobile screen. + * + * By default the sidebar doesn't have additional padding, so the children + * can reach full width of the container. + * + * Whenever a secondary sidebar is opened, it will "blur" the RootMainContainer + * component by modifying the overlay. + */ +export const SecondarySidebar = ({ + className, + children, + onClickMobileBack, + onClickMobileClose, + header, + open, + backButtonProps, + closeButtonProps, + ...props +}) => ( + +); + +SecondarySidebar.propTypes = { + /** + * Forward an additional className to the underlying element + */ + className: string, + /** + * The body of the sidebar. It can be any additional content. + */ + children: node, + /** + * On clicking the back button for mobile navigation + */ + onClickMobileBack: func, + /** + * On clicking the close button for mobile navigation + */ + onClickMobileClose: func, + /** + * Optional header of the sidebar that is rendered at the top + */ + header: node, + /** + * Controls if the secondary sidebar should be visible + */ + open: bool, + /** + * Props passed to the mobile back action button + */ + backButtonProps: shape({}), + /** + * Props passed to the mobile close action button + */ + closeButtonProps: shape({}), +}; + +SecondarySidebar.defaultProps = { + children: null, + className: null, + onClickMobileBack: null, + onClickMobileClose: null, + header: null, + open: false, + backButtonProps: null, + closeButtonProps: null, +}; diff --git a/src/navigation/SecondarySidebar/SecondarySidebar.md b/src/navigation/SecondarySidebar/SecondarySidebar.md new file mode 100644 index 0000000..f00c3e1 --- /dev/null +++ b/src/navigation/SecondarySidebar/SecondarySidebar.md @@ -0,0 +1,44 @@ +### Example + +The implementation of the secondary's sidebar behaviour is left to the user. + +Please check the corresponding [RootContainer](#/Layout/RootContainer) to see a +better example of how the sidebar work together with RootContainers's overlay +and RootMainContainer. + +```js +import { MemoryRouter } from 'react-router'; +import { + SidebarNavigation, + SidebarNavigationItem, + SidebarSelectorFilter, + SidebarSelectorFilterItem, + SidebarSeparator, + SecondarySidebar, + Sidebar, +} from '@lightelligence/react'; + +const [open, setOpen] = React.useState(false); + +
+ + setOpen(!open)} active={open}> + + + + + + + + + + + + setOpen(false)} + /> +
; +``` diff --git a/src/navigation/SecondarySidebar/SecondarySidebar.test.js b/src/navigation/SecondarySidebar/SecondarySidebar.test.js new file mode 100644 index 0000000..551b1a1 --- /dev/null +++ b/src/navigation/SecondarySidebar/SecondarySidebar.test.js @@ -0,0 +1,81 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { SecondarySidebar } from './SecondarySidebar'; +import { oltStyles } from '../..'; + +const renderComponent = (props) => { + return render( + , + ); +}; + +describe('Secondary Sidebar', () => { + test('forwards className', () => { + const { getByTestId } = renderComponent({ + className: 'myClass', + }); + + const component = getByTestId('secondary-sidebar'); + expect(component.classList.contains('myClass')).toBe(true); + }); + + test('renders properly when is open', () => { + const { getByTestId } = renderComponent({ + open: true, + }); + + const component = getByTestId('secondary-sidebar'); + expect(component.classList.contains('is-open')).toBe(true); + }); + + test('renders header', () => { + const { getByText } = renderComponent({ + header: 'Header', + }); + + const headerComponent = getByText('Header'); + + expect( + headerComponent.classList.contains(oltStyles.SidebarSecondaryHeader), + ).toBe(true); + }); + + test('renders content', () => { + const { getByText } = renderComponent({ + open: true, + children: 'Body', + }); + + const component = getByText('Body'); + + expect(component.classList.contains('is-open')).toBe(true); + }); + + test('should be able to catch mobile back button', () => { + const onClickMobileBack = jest.fn(); + const { getByTestId } = renderComponent({ + backButtonProps: { + 'data-testid': 'back-button', + }, + onClickMobileBack, + }); + + const actionButton = getByTestId('back-button'); + fireEvent.click(actionButton); + expect(onClickMobileBack).toBeCalled(); + }); + + test('should be able to catch mobile close button', () => { + const onClickMobileClose = jest.fn(); + const { getByTestId } = renderComponent({ + closeButtonProps: { + 'data-testid': 'close-button', + }, + onClickMobileClose, + }); + + const actionButton = getByTestId('close-button'); + fireEvent.click(actionButton); + expect(onClickMobileClose).toBeCalled(); + }); +}); diff --git a/src/navigation/SecondarySidebar/index.js b/src/navigation/SecondarySidebar/index.js new file mode 100644 index 0000000..557ec52 --- /dev/null +++ b/src/navigation/SecondarySidebar/index.js @@ -0,0 +1 @@ +export * from './SecondarySidebar'; diff --git a/src/navigation/Sidebar/Sidebar.js b/src/navigation/Sidebar/Sidebar.js new file mode 100644 index 0000000..ef97171 --- /dev/null +++ b/src/navigation/Sidebar/Sidebar.js @@ -0,0 +1,88 @@ +import React from 'react'; +import { func, node, string, bool, shape } from 'prop-types'; +import classnames from 'classnames'; +import * as olt from '@lightelligence/styles'; +import { ActionButton } from '../../components/ActionButton'; + +/** + * The Sidebar component is used to render a sidebar on the left side of the user's + * browser. + * + * It consists of body, which is rendered on the top side of the sidebar and + * an additional `bottom` property which is rendered on the bottom side of the + * sidebar. + * + * On mobile screens the sidebar also displays a close button, that can be used + * to hide the sidebar when it's displayed over a mobile device. + * + * The `open` prop can control if the Sidebar is visible on mobile screens. + */ +export const Sidebar = ({ + className, + children, + onClickMobileClose, + bottom, + open, + closeButtonProps, + ...props +}) => ( + +); + +Sidebar.propTypes = { + /** + * Forward an additional className to the underlying element + */ + className: string, + /** + * The body of the sidebar. Perfect fit for + * [SidebarNavigation](#/Navigation/SidebarNavigation) and + * [SidebarSelectorFilter](#/Navigation/SidebarSelectorFilter). + */ + children: node, + /** + * Bottom container. Usually used to render + * [SidebarSelectorTenant](#/Navigation/SidebarSelectorTenant) and + * [SidebarSelectorProperty](#/Navigation/SidebarSelectorProperty). + */ + bottom: node, + /** + * On clicking the close button for mobile sidebar + */ + onClickMobileClose: func, + /** + * Controls if the sidebar should be visible on mobile devices + */ + open: bool, + /** + * Props passed to the mobile close action button + */ + closeButtonProps: shape({}), +}; + +Sidebar.defaultProps = { + children: null, + className: null, + bottom: null, + onClickMobileClose: null, + open: false, + closeButtonProps: null, +}; diff --git a/src/navigation/Sidebar/Sidebar.md b/src/navigation/Sidebar/Sidebar.md new file mode 100644 index 0000000..9496e2f --- /dev/null +++ b/src/navigation/Sidebar/Sidebar.md @@ -0,0 +1,56 @@ +### Example + +The mobile behaviour should be implemented in your application + +```js +import { MemoryRouter } from 'react-router'; +import { + SidebarNavigation, + SidebarNavigationItem, + SidebarSubNavigationItem, + SidebarSelectorFilter, + SidebarSelectorFilterItem, + SidebarSelectorProperty, + SidebarSelectorTenant, + SidebarSeparator, + Sidebar, +} from '@lightelligence/react'; + + alert('property selector')} + /> + alert('tenant selector')} + /> + + } +> + alert('filter selector')}> + + + + + + + + + + + + + + + + +; +``` diff --git a/src/navigation/Sidebar/Sidebar.test.js b/src/navigation/Sidebar/Sidebar.test.js new file mode 100644 index 0000000..4e7d0a4 --- /dev/null +++ b/src/navigation/Sidebar/Sidebar.test.js @@ -0,0 +1,62 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { Sidebar } from './Sidebar'; + +const renderComponent = (props) => { + return render(); +}; + +describe('Sidebar', () => { + test('forwards className', () => { + const { getByTestId } = renderComponent({ + className: 'myClass', + }); + + const component = getByTestId('sidebar'); + expect(component.classList.contains('myClass')).toBe(true); + }); + + test('renders properly when is open', () => { + const { getByTestId } = renderComponent({ + open: true, + }); + + const component = getByTestId('sidebar'); + expect(component.classList.contains('is-open')).toBe(true); + }); + + test('renders bottom', () => { + const { getByText } = renderComponent({ + bottom: 'Bottom', + }); + + const bottomComponent = getByText('Bottom'); + + expect(bottomComponent).toBeTruthy(); + }); + + test('renders content', () => { + const { getByText } = renderComponent({ + open: true, + children: 'Body', + }); + + const component = getByText('Body'); + + expect(component).toBeTruthy(); + }); + + test('should be able to catch mobile close button', () => { + const onClickMobileClose = jest.fn(); + const { getByTestId } = renderComponent({ + closeButtonProps: { + 'data-testid': 'close-button', + }, + onClickMobileClose, + }); + + const actionButton = getByTestId('close-button'); + fireEvent.click(actionButton); + expect(onClickMobileClose).toBeCalled(); + }); +}); diff --git a/src/navigation/Sidebar/SidebarSeparator.js b/src/navigation/Sidebar/SidebarSeparator.js new file mode 100644 index 0000000..b273f0f --- /dev/null +++ b/src/navigation/Sidebar/SidebarSeparator.js @@ -0,0 +1,26 @@ +import { string } from 'prop-types'; +import React from 'react'; +import classnames from 'classnames'; +import * as olt from '@lightelligence/styles'; + +/** + * Sidebar separator is used to separate content inside the + * [Sidebar](#/Navigation/Sidebar)'s body. + * + * It implements the semantic `hr` element and passes all `props` to the + * underlying React element. + */ +export const SidebarSeparator = ({ className, ...props }) => ( +
+); + +SidebarSeparator.propTypes = { + /** + * Forward an additional className to the underlying element + */ + className: string, +}; + +SidebarSeparator.defaultProps = { + className: null, +}; diff --git a/src/navigation/Sidebar/SidebarSeparator.md b/src/navigation/Sidebar/SidebarSeparator.md new file mode 100644 index 0000000..66384c2 --- /dev/null +++ b/src/navigation/Sidebar/SidebarSeparator.md @@ -0,0 +1,24 @@ +### Example + +```js +import { MemoryRouter } from 'react-router'; +import { + SidebarNavigation, + SidebarNavigationItem, + SidebarSeparator, + Sidebar, +} from '@lightelligence/react'; + + + + + + + + + + + + +; +``` diff --git a/src/navigation/Sidebar/SidebarSeparator.test.js b/src/navigation/Sidebar/SidebarSeparator.test.js new file mode 100644 index 0000000..9b0f078 --- /dev/null +++ b/src/navigation/Sidebar/SidebarSeparator.test.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { SidebarSeparator } from './SidebarSeparator'; +import { oltStyles } from '../..'; + +const renderComponent = (props) => { + return render( + , + ); +}; + +describe('SidebarSeparator', () => { + test('has oltStyles.SidebarSeparator', () => { + const { getByTestId } = renderComponent(); + + const component = getByTestId('sidebar-separator'); + expect(component.classList.contains(oltStyles.SidebarSeparator)).toBe(true); + }); + test('forwards className', () => { + const { getByTestId } = renderComponent({ + className: 'myClass', + }); + + const component = getByTestId('sidebar-separator'); + expect(component.classList.contains('myClass')).toBe(true); + }); +}); diff --git a/src/navigation/Sidebar/index.js b/src/navigation/Sidebar/index.js new file mode 100644 index 0000000..14bb8db --- /dev/null +++ b/src/navigation/Sidebar/index.js @@ -0,0 +1,2 @@ +export * from './Sidebar'; +export * from './SidebarSeparator'; diff --git a/src/navigation/SidebarNavigation/SidebarNavigation.js b/src/navigation/SidebarNavigation/SidebarNavigation.js new file mode 100644 index 0000000..bd3969e --- /dev/null +++ b/src/navigation/SidebarNavigation/SidebarNavigation.js @@ -0,0 +1,30 @@ +import { oneOf, arrayOf, shape, oneOfType } from 'prop-types'; +import React from 'react'; +import { SidebarNavigationItem } from './SidebarNavigationItem'; + +/** + * Sidebar navigation is the navigation for the middle of the sidebar. It + * consists of [SidebarNavigationItem](#/Navigation/SidebarNavigationItem) and + * uses the semantic `nav` HTML element. + * + * It passes additional `props` to the underlying React element and is + * implemented via the semantic `nav` tag. + */ +export const SidebarNavigation = ({ children, ...props }) => ( + +); + +SidebarNavigation.propTypes = { + /** + * Content of the element should always be consisted of + * [SidebarNavigationItem](/#/Navigation/SidebarNavigationItem) components. + */ + children: oneOfType([ + shape({ type: oneOf([SidebarNavigationItem]) }), + arrayOf(shape({ type: oneOf([SidebarNavigationItem]) })), + ]), +}; + +SidebarNavigation.defaultProps = { + children: null, +}; diff --git a/src/navigation/SidebarNavigation/SidebarNavigation.md b/src/navigation/SidebarNavigation/SidebarNavigation.md new file mode 100644 index 0000000..8ea869c --- /dev/null +++ b/src/navigation/SidebarNavigation/SidebarNavigation.md @@ -0,0 +1,29 @@ +### Example + +```jsx +import { MemoryRouter } from 'react-router'; +import { + SidebarNavigation, + SidebarNavigationItem, + SidebarSubNavigationItem, + Sidebar, +} from '@lightelligence/react'; + + + + + + + + + + + + + +; +``` diff --git a/src/navigation/SidebarNavigation/SidebarNavigation.test.js b/src/navigation/SidebarNavigation/SidebarNavigation.test.js new file mode 100644 index 0000000..049fa67 --- /dev/null +++ b/src/navigation/SidebarNavigation/SidebarNavigation.test.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { SidebarNavigation } from './SidebarNavigation'; +import { SidebarNavigationItem } from './SidebarNavigationItem'; + +const renderComponent = (props) => { + return render( + , + ); +}; + +describe('Sidebar Navigation', () => { + test('renders children', () => { + const { getByText } = renderComponent({ + children: ( + + ), + }); + + const component = getByText('Home'); + expect(component).toBeTruthy(); + }); +}); diff --git a/src/navigation/SidebarNavigation/SidebarNavigationItem.js b/src/navigation/SidebarNavigation/SidebarNavigationItem.js new file mode 100644 index 0000000..83c90ac --- /dev/null +++ b/src/navigation/SidebarNavigation/SidebarNavigationItem.js @@ -0,0 +1,92 @@ +import classnames from 'classnames'; +import { pascalize } from 'humps'; +import { string, oneOfType, shape, oneOf, arrayOf } from 'prop-types'; +import React from 'react'; +import * as olt from '@lightelligence/styles'; +import { matchPath } from 'react-router-dom'; +import { Link } from '../../content/Link'; +import { SidebarSubNavigationItem } from './SidebarSubNavigationItem'; + +/** + * Navigation item for the [SidebarNavigation](#/Navigation/SidebarNavigation) + * Component. + * + * The component also passes all other `props` to the underlying + * [Link](#/Content/Link) component. + * + * It always displays an icon on the left side of the navigation item. + * + * If children are provided the navigation item renders additional `nav` + * HTML element that consists of + * [SidebarSubNavigationItem](#/Navigation/SidebarSubNavigationItem). Whenever + * the navigation item is active, it will also display the sub navigation. + * + * @example ./SidebarNavigation.md + */ +export const SidebarNavigationItem = ({ + className, + to, + title, + icon, + children, + ...props +}) => { + const match = matchPath(to, { + path: '/', + exact: false, + }); + return ( + <> + + {title} + + {children && ( + + )} + + ); +}; + +SidebarNavigationItem.propTypes = { + /** + * Forward an additional className to the underlying element + */ + className: string, + /** + * The title of the item + */ + title: string.isRequired, + /** + * Where will this navigation item point to + */ + to: string.isRequired, + /** + * The icon to display + */ + icon: string.isRequired, + /** + * Sub navigation items + * + * Content of the element should always be consisted of + * [SidebarSubNavigationItem](/#/Navigation/SidebarSubNavigationItem) + * components. + */ + children: oneOfType([ + shape({ type: oneOf([SidebarSubNavigationItem]) }), + arrayOf(shape({ type: oneOf([SidebarSubNavigationItem]) })), + ]), +}; + +SidebarNavigationItem.defaultProps = { + className: null, + children: null, +}; diff --git a/src/navigation/SidebarNavigation/SidebarNavigationItem.md b/src/navigation/SidebarNavigation/SidebarNavigationItem.md new file mode 100644 index 0000000..c986ae0 --- /dev/null +++ b/src/navigation/SidebarNavigation/SidebarNavigationItem.md @@ -0,0 +1,2 @@ +Example is taken from [SidebarNavigation](#/Navigation/SidebarNavigation) +component. diff --git a/src/navigation/SidebarNavigation/SidebarNavigationItem.test.js b/src/navigation/SidebarNavigation/SidebarNavigationItem.test.js new file mode 100644 index 0000000..f31cf06 --- /dev/null +++ b/src/navigation/SidebarNavigation/SidebarNavigationItem.test.js @@ -0,0 +1,84 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { oltStyles, SidebarSubNavigationItem } from '../../index'; +import { SidebarNavigationItem } from './SidebarNavigationItem'; + +const renderComponent = (props) => { + return render( + , + ); +}; + +describe('Sidebar Navigation Item', () => { + test('has oltStyles.SidebarNavigationItem', () => { + const { getByTestId } = renderComponent(); + + const component = getByTestId('sidebar-navigation-item'); + expect(component.classList.contains(oltStyles.SidebarNavigationItem)).toBe( + true, + ); + }); + test('forwards className', () => { + const { getByTestId } = renderComponent({ + className: 'myClass', + }); + + const component = getByTestId('sidebar-navigation-item'); + expect(component.classList.contains('myClass')).toBe(true); + }); + test('renders an icon', () => { + const { getByTestId } = renderComponent({ + icon: 'home', + }); + + const component = getByTestId('sidebar-navigation-item'); + expect(component.classList.contains(oltStyles.IconHome)).toBe(true); + }); + test('renders sub navigation', () => { + const { getByText } = renderComponent({ + children: ( + + ), + }); + + const component = getByText('Bar'); + expect(component).toBeTruthy(); + }); + test('passes is active for router', () => { + const { getByTestId } = render( + + + , + ); + + const component = getByTestId('sidebar-navigation-item'); + expect(component.classList.contains('is-active')).toBe(true); + }); + test('passes inactive when route is not selected', () => { + const { getByTestId } = render( + + + , + ); + + const component = getByTestId('sidebar-navigation-item'); + expect(component.classList.contains('is-active')).toBe(false); + }); +}); diff --git a/src/navigation/SidebarNavigation/SidebarSubNavigationItem.js b/src/navigation/SidebarNavigation/SidebarSubNavigationItem.js new file mode 100644 index 0000000..79e2107 --- /dev/null +++ b/src/navigation/SidebarNavigation/SidebarSubNavigationItem.js @@ -0,0 +1,56 @@ +import classnames from 'classnames'; +import { string } from 'prop-types'; +import React from 'react'; +import * as olt from '@lightelligence/styles'; +import { matchPath } from 'react-router-dom'; +import { Link } from '../../content/Link'; + +/** + * Sub Navigation item for the + * [SidebarNavigationItem](#/Navigation/SidebarNavigationItem) component. + * + * The component passes all other `props` to the underlying `Link` + * component + * + * @example ./SidebarNavigation.md + */ +export const SidebarSubNavigationItem = ({ + className, + to, + title, + ...props +}) => { + const match = matchPath(to, { + path: '/', + exact: false, + }); + return ( + + {title} + + ); +}; + +SidebarSubNavigationItem.propTypes = { + /** + * Forward an additional className to the underlying element + */ + className: string, + /** + * The title of the item + */ + title: string.isRequired, + /** + * Where will this navigation item point to + */ + to: string.isRequired, +}; + +SidebarSubNavigationItem.defaultProps = { + className: null, +}; diff --git a/src/navigation/SidebarNavigation/SidebarSubNavigationItem.md b/src/navigation/SidebarNavigation/SidebarSubNavigationItem.md new file mode 100644 index 0000000..c986ae0 --- /dev/null +++ b/src/navigation/SidebarNavigation/SidebarSubNavigationItem.md @@ -0,0 +1,2 @@ +Example is taken from [SidebarNavigation](#/Navigation/SidebarNavigation) +component. diff --git a/src/navigation/SidebarNavigation/SidebarSubNavigationItem.test.js b/src/navigation/SidebarNavigation/SidebarSubNavigationItem.test.js new file mode 100644 index 0000000..1f77fb8 --- /dev/null +++ b/src/navigation/SidebarNavigation/SidebarSubNavigationItem.test.js @@ -0,0 +1,64 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { oltStyles } from '../../index'; +import { SidebarSubNavigationItem } from './SidebarSubNavigationItem'; + +const renderComponent = (props) => { + return render( + , + ); +}; + +describe('Sidebar Sub Navigation Item', () => { + test('has oltStyles.SidebarSubNavigationItem', () => { + const { getByTestId } = renderComponent(); + + const component = getByTestId('sidebar-sub-navigation-item'); + expect( + component.classList.contains(oltStyles.SidebarSubnavigationItem), + ).toBe(true); + }); + test('forwards className', () => { + const { getByTestId } = renderComponent({ + className: 'myClass', + }); + + const component = getByTestId('sidebar-sub-navigation-item'); + expect(component.classList.contains('myClass')).toBe(true); + }); + test('passes is active for router', () => { + const { getByTestId } = render( + + + , + ); + + const component = getByTestId('sidebar-sub-navigation-item'); + expect(component.classList.contains('is-active')).toBe(true); + }); + test('passes inactive when route is not selected', () => { + const { getByTestId } = render( + + + , + ); + + const component = getByTestId('sidebar-sub-navigation-item'); + expect(component.classList.contains('is-active')).toBe(false); + }); +}); diff --git a/src/navigation/SidebarNavigation/index.js b/src/navigation/SidebarNavigation/index.js new file mode 100644 index 0000000..2bd378e --- /dev/null +++ b/src/navigation/SidebarNavigation/index.js @@ -0,0 +1,3 @@ +export * from './SidebarNavigation'; +export * from './SidebarNavigationItem'; +export * from './SidebarSubNavigationItem'; diff --git a/src/navigation/SidebarSelector/SidebarSelectorFilter.js b/src/navigation/SidebarSelector/SidebarSelectorFilter.js new file mode 100644 index 0000000..fff87d1 --- /dev/null +++ b/src/navigation/SidebarSelector/SidebarSelectorFilter.js @@ -0,0 +1,81 @@ +import { + bool, + arrayOf, + func, + oneOf, + oneOfType, + shape, + string, +} from 'prop-types'; +import React from 'react'; +import classnames from 'classnames'; +import * as olt from '@lightelligence/styles'; +import { SidebarSelectorFilterItem } from './SidebarSelectorFilterItem'; + +/** + * Sidebar selector is a selector component that is used in the Sidebar to + * show a filter button. + * + * You can wire this to a [SecondarySidebar](#/Navigation/SecondarySidebar) + * component. Please check [RootContainer](#/Layout/RootContainer) for + * implementation details. + */ +export const SidebarSelectorFilter = ({ + title, + children, + className, + onClick, + active, + ...props +}) => ( + +); + +SidebarSelectorFilter.propTypes = { + /** + * Forward an additional className to the underlying element + */ + className: string, + /** + * Filters for the filter are rendered as a list with icons. The children + * must be of + * [SidebarSelectorFilterItem](#/Navigation/SidebarSelectorFilterItem) type. + */ + children: oneOfType([ + shape({ type: oneOf([SidebarSelectorFilterItem]) }), + arrayOf(shape({ type: oneOf([SidebarSelectorFilterItem]) })), + ]).isRequired, + /** + * The title of this component + */ + title: string, + /** + * On click handler + */ + onClick: func.isRequired, + /** + * Is the selector active + */ + active: bool, +}; + +SidebarSelectorFilter.defaultProps = { + className: null, + title: 'Filter', + active: false, +}; diff --git a/src/navigation/SidebarSelector/SidebarSelectorFilter.md b/src/navigation/SidebarSelector/SidebarSelectorFilter.md new file mode 100644 index 0000000..14eb58d --- /dev/null +++ b/src/navigation/SidebarSelector/SidebarSelectorFilter.md @@ -0,0 +1,15 @@ +### Example + +```jsx +import { + Sidebar, + SidebarSelectorFilter, + SidebarSelectorFilterItem, +} from '@lightelligence/react'; + + alert('Selected')}> + + + +; +``` diff --git a/src/navigation/SidebarSelector/SidebarSelectorFilter.test.js b/src/navigation/SidebarSelector/SidebarSelectorFilter.test.js new file mode 100644 index 0000000..ed3ecc1 --- /dev/null +++ b/src/navigation/SidebarSelector/SidebarSelectorFilter.test.js @@ -0,0 +1,67 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { SidebarSelectorFilter } from './SidebarSelectorFilter'; +import { oltStyles, SidebarSelectorFilterItem } from '../../index'; + +const renderComponent = (props) => { + return render( + {}} {...props}> + + , + ); +}; + +describe('SidebarSelectorFilter', () => { + test('forwards className', () => { + const { getByTestId } = renderComponent({ + className: 'myClass', + }); + + const component = getByTestId('filter'); + expect(component.classList.contains('myClass')).toBe(true); + }); + + test('renders properly when is active', () => { + const { getByTestId } = renderComponent({ + active: true, + }); + + const component = getByTestId('filter'); + expect(component.classList.contains('is-active')).toBe(true); + }); + + test('renders title', () => { + const { getByText } = renderComponent({ + title: 'Title', + }); + + const component = getByText('Title'); + + expect( + component.classList.contains(oltStyles.SidebarSelectorFilterTitle), + ).toBe(true); + }); + + test('renders filters', () => { + const { getByText } = renderComponent({}); + + const component = getByText('foo'); + + expect( + component.classList.contains( + oltStyles.SidebarSelectorFilterFiltersFilter, + ), + ).toBe(true); + }); + + test('should be able to button click', () => { + const onClick = jest.fn(); + const { getByTestId } = renderComponent({ + onClick, + }); + + const component = getByTestId('filter'); + fireEvent.click(component); + expect(onClick).toBeCalled(); + }); +}); diff --git a/src/navigation/SidebarSelector/SidebarSelectorFilterItem.js b/src/navigation/SidebarSelector/SidebarSelectorFilterItem.js new file mode 100644 index 0000000..e0165f1 --- /dev/null +++ b/src/navigation/SidebarSelector/SidebarSelectorFilterItem.js @@ -0,0 +1,48 @@ +import { pascalize } from 'humps'; +import { string } from 'prop-types'; +import React from 'react'; +import classnames from 'classnames'; +import * as olt from '@lightelligence/styles'; + +/** + * Sidebar selector filter item is used in the sidebar filter selector to show + * the active filters + * + * @example ./SidebarSelectorFilter.md + */ +export const SidebarSelectorFilterItem = ({ + icon, + name, + className, + ...props +}) => ( +
+ {name} +
+); + +SidebarSelectorFilterItem.propTypes = { + /** + * Forward an additional className to the underlying element + */ + className: string, + /** + * The name of the filter + */ + name: string.isRequired, + /** + * The icon to display + */ + icon: string.isRequired, +}; + +SidebarSelectorFilterItem.defaultProps = { + className: null, +}; diff --git a/src/navigation/SidebarSelector/SidebarSelectorFilterItem.md b/src/navigation/SidebarSelector/SidebarSelectorFilterItem.md new file mode 100644 index 0000000..10518d3 --- /dev/null +++ b/src/navigation/SidebarSelector/SidebarSelectorFilterItem.md @@ -0,0 +1,2 @@ +Example is taken from [SidebarSelectorFilter](#/Navigation/SidebarSelectorFilter) +component. diff --git a/src/navigation/SidebarSelector/SidebarSelectorFilterItem.test.js b/src/navigation/SidebarSelector/SidebarSelectorFilterItem.test.js new file mode 100644 index 0000000..21196a0 --- /dev/null +++ b/src/navigation/SidebarSelector/SidebarSelectorFilterItem.test.js @@ -0,0 +1,50 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { SidebarSelectorFilterItem } from './SidebarSelectorFilterItem'; +import { oltStyles } from '../../index'; + +const renderComponent = (props) => { + return render( + , + ); +}; + +describe('SidebarSelectorFilterItem', () => { + test('forwards className', () => { + const { getByTestId } = renderComponent({ + className: 'myClass', + }); + + const component = getByTestId('filter-item'); + expect(component.classList.contains('myClass')).toBe(true); + }); + + test('renders name', () => { + const { getByText } = renderComponent({ + name: 'Name', + }); + + const component = getByText('Name'); + + expect( + component.classList.contains( + oltStyles.SidebarSelectorFilterFiltersFilter, + ), + ).toBe(true); + }); + + test('renders icon', () => { + const { getByTestId } = renderComponent({ + icon: 'sensor', + }); + + const component = getByTestId('filter-item'); + + expect(component.classList.contains(oltStyles.IconSensor)).toBe(true); + }); +}); diff --git a/src/navigation/SidebarSelector/SidebarSelectorProperty.js b/src/navigation/SidebarSelector/SidebarSelectorProperty.js new file mode 100644 index 0000000..9eb0a6b --- /dev/null +++ b/src/navigation/SidebarSelector/SidebarSelectorProperty.js @@ -0,0 +1,65 @@ +import { bool, func, string } from 'prop-types'; +import React from 'react'; +import classnames from 'classnames'; +import * as olt from '@lightelligence/styles'; + +/** + * Sidebar Property selector is a selector component that is used in the + * Sidebar to select a property + * + * You can wire this to a [SecondarySidebar](#/Navigation/SecondarySidebar) + * component. Please check [RootContainer](#/Layout/RootContainer) for + * implementation details. + */ +export const SidebarSelectorProperty = ({ + title, + location, + className, + onClick, + active, + ...props +}) => ( + +); + +SidebarSelectorProperty.propTypes = { + /** + * Forward an additional className to the underlying element + */ + className: string, + /** + * The property name + */ + title: string.isRequired, + /** + * The location + */ + location: string.isRequired, + /** + * On click handler + */ + onClick: func.isRequired, + /** + * Is the selector active + */ + active: bool, +}; + +SidebarSelectorProperty.defaultProps = { + className: null, + active: false, +}; diff --git a/src/navigation/SidebarSelector/SidebarSelectorProperty.md b/src/navigation/SidebarSelector/SidebarSelectorProperty.md new file mode 100644 index 0000000..d5ddfcb --- /dev/null +++ b/src/navigation/SidebarSelector/SidebarSelectorProperty.md @@ -0,0 +1,12 @@ +### Example + +```jsx +import { Sidebar, SidebarSelectorProperty } from '@lightelligence/react'; + + alert('selected')} + /> +; +``` diff --git a/src/navigation/SidebarSelector/SidebarSelectorProperty.test.js b/src/navigation/SidebarSelector/SidebarSelectorProperty.test.js new file mode 100644 index 0000000..db850af --- /dev/null +++ b/src/navigation/SidebarSelector/SidebarSelectorProperty.test.js @@ -0,0 +1,71 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { SidebarSelectorProperty } from './SidebarSelectorProperty'; +import { oltStyles } from '../../index'; + +const renderComponent = (props) => { + return render( + {}} + title="foo" + location="bar" + {...props} + />, + ); +}; + +describe('SidebarSelectorProperty', () => { + test('forwards className', () => { + const { getByTestId } = renderComponent({ + className: 'myClass', + }); + + const component = getByTestId('property'); + expect(component.classList.contains('myClass')).toBe(true); + }); + + test('renders properly when is active', () => { + const { getByTestId } = renderComponent({ + active: true, + }); + + const component = getByTestId('property'); + expect(component.classList.contains('is-active')).toBe(true); + }); + + test('renders title', () => { + const { getByText } = renderComponent({ + title: 'Title', + }); + + const component = getByText('Title'); + + expect( + component.classList.contains(oltStyles.SidebarSelectorPropertyTitle), + ).toBe(true); + }); + + test('renders location', () => { + const { getByText } = renderComponent({ + location: 'Location', + }); + + const component = getByText('Location'); + + expect( + component.classList.contains(oltStyles.SidebarSelectorPropertyValue), + ).toBe(true); + }); + + test('should be able to button click', () => { + const onClick = jest.fn(); + const { getByTestId } = renderComponent({ + onClick, + }); + + const component = getByTestId('property'); + fireEvent.click(component); + expect(onClick).toBeCalled(); + }); +}); diff --git a/src/navigation/SidebarSelector/SidebarSelectorTenant.js b/src/navigation/SidebarSelector/SidebarSelectorTenant.js new file mode 100644 index 0000000..c03cdd6 --- /dev/null +++ b/src/navigation/SidebarSelector/SidebarSelectorTenant.js @@ -0,0 +1,61 @@ +import { bool, func, string } from 'prop-types'; +import React from 'react'; +import classnames from 'classnames'; +import * as olt from '@lightelligence/styles'; + +/** + * Sidebar selector is a selector component that is used in the Sidebar to + * show a tenant switch + * + * You can wire this to a [SecondarySidebar](#/Navigation/SecondarySidebar) + * component. Please check [RootContainer](#/Layout/RootContainer) for + * implementation details. + */ +export const SidebarSelectorTenant = ({ + onClick, + active, + tenant, + className, + ...props +}) => ( + +); + +SidebarSelectorTenant.propTypes = { + /** + * Forward an additional className to the underlying element + */ + className: string, + /** + * The tenant name + */ + tenant: string.isRequired, + /** + * On click handler + */ + onClick: func.isRequired, + /** + * Is the selector active + */ + active: bool, +}; + +SidebarSelectorTenant.defaultProps = { + className: null, + active: false, +}; diff --git a/src/navigation/SidebarSelector/SidebarSelectorTenant.md b/src/navigation/SidebarSelector/SidebarSelectorTenant.md new file mode 100644 index 0000000..eeda4f5 --- /dev/null +++ b/src/navigation/SidebarSelector/SidebarSelectorTenant.md @@ -0,0 +1,8 @@ +### Example + +```jsx +import { Sidebar, SidebarSelectorTenant } from '@lightelligence/react'; + + alert('selector')} /> +; +``` diff --git a/src/navigation/SidebarSelector/SidebarSelectorTenant.test.js b/src/navigation/SidebarSelector/SidebarSelectorTenant.test.js new file mode 100644 index 0000000..ca1cd11 --- /dev/null +++ b/src/navigation/SidebarSelector/SidebarSelectorTenant.test.js @@ -0,0 +1,70 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { SidebarSelectorTenant } from './SidebarSelectorTenant'; +import { oltStyles } from '../../index'; + +const renderComponent = (props) => { + return render( + {}} + tenant="Foo" + {...props} + />, + ); +}; + +describe('SidebarSelectorTenant', () => { + test('forwards className', () => { + const { getByTestId } = renderComponent({ + className: 'myClass', + }); + + const component = getByTestId('tenant'); + expect(component.classList.contains('myClass')).toBe(true); + }); + + test('renders properly when is active', () => { + const { getByTestId } = renderComponent({ + active: true, + }); + + const component = getByTestId('tenant'); + expect(component.classList.contains('is-active')).toBe(true); + }); + + test('renders tenant', () => { + const { getByText } = renderComponent({ + tenant: 'Tenant', + }); + + const component = getByText('Tenant'); + + expect( + component.classList.contains(oltStyles.SidebarSelectorTenantName), + ).toBe(true); + }); + + test('renders avatar', () => { + const { getByText } = renderComponent({ + tenant: 'My Tenant', + }); + + const component = getByText('M'); + + expect( + component.classList.contains(oltStyles.SidebarSelectorTenantAvatar), + ).toBe(true); + }); + + test('should be able to button click', () => { + const onClick = jest.fn(); + const { getByTestId } = renderComponent({ + onClick, + }); + + const component = getByTestId('tenant'); + fireEvent.click(component); + expect(onClick).toBeCalled(); + }); +}); diff --git a/src/navigation/SidebarSelector/index.js b/src/navigation/SidebarSelector/index.js new file mode 100644 index 0000000..8a19015 --- /dev/null +++ b/src/navigation/SidebarSelector/index.js @@ -0,0 +1,4 @@ +export * from './SidebarSelectorTenant'; +export * from './SidebarSelectorProperty'; +export * from './SidebarSelectorFilter'; +export * from './SidebarSelectorFilterItem'; diff --git a/styleguide.config.js b/styleguide.config.js index 52401e4..16ce754 100644 --- a/styleguide.config.js +++ b/styleguide.config.js @@ -48,6 +48,11 @@ module.exports = { components: 'src/layout/**/[A-Z]*.js', // exclude index.js files sectionDepth: 2, }, + { + name: 'Navigation', + components: 'src/navigation/**/[A-Z]*.js', + sectionDepth: 2, + }, { name: 'Hooks', components: 'src/hooks/**/[A-Z]*.js', // exclude index.js files @@ -141,7 +146,7 @@ module.exports = { }, }, styleguideComponents: { - Wrapper: path.join(__dirname, 'src/layout/Frame/Frame'), + Wrapper: path.join(__dirname, 'styleguide/Frame'), Logo: path.join(__dirname, 'styleguide/Logo'), VersionDropdown: path.join(__dirname, 'styleguide/VersionDropdown'), StyleGuideRenderer: path.join(__dirname, 'styleguide/StyleGuide'), diff --git a/src/layout/Frame/Frame.js b/styleguide/Frame.js similarity index 100% rename from src/layout/Frame/Frame.js rename to styleguide/Frame.js