From 07ee9de5552c4604d216174f3791fa0f8a58bf47 Mon Sep 17 00:00:00 2001 From: Bree Hall Date: Fri, 8 Sep 2023 15:56:41 -0400 Subject: [PATCH 1/6] [Storybook] Create Storybook for EuiSideNav that includes a playground and example specific to EuiSideNavHeader --- src/components/side_nav/side_nav.stories.tsx | 163 +++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 src/components/side_nav/side_nav.stories.tsx diff --git a/src/components/side_nav/side_nav.stories.tsx b/src/components/side_nav/side_nav.stories.tsx new file mode 100644 index 00000000000..a75d1b0cf7c --- /dev/null +++ b/src/components/side_nav/side_nav.stories.tsx @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; + +import { + EuiSideNav, + EuiSideNavProps, + EuiSideNavHeadingProps, +} from './side_nav'; +import { EuiIcon } from '../icon'; + +const meta: Meta = { + title: 'EuiSideNav', + component: EuiSideNav, +}; + +export default meta; +type Story = StoryObj; + +const componentDefaults: EuiSideNavProps = { + mobileBreakpoints: ['xs', 's'], + items: [], + // Aria-label and mobileTitle do not have defaults; they are being set here as they are shared between examples + 'aria-label': 'Side navigation example', + mobileTitle: 'Mobile navigation header', +}; + +// Heading props shared across examples +const _sharedHeadingProps: EuiSideNavHeadingProps = { + element: 'h1', + screenReaderOnly: false, +}; + +export const Playground: Story = { + args: { + ...componentDefaults, + heading: 'Elastic', + headingProps: _sharedHeadingProps, + isOpenOnMobile: false, + truncate: false, + }, + render: ({ ...args }) => , +}; + +export const SideNavHeader: Story = { + args: { + ...componentDefaults, + items: [ + { + name: 'Root item', + id: 'rootItem', + items: [ + { + name: 'Child item', + id: 'childItem', + onClick: () => {}, + }, + ], + }, + ], + heading: 'Navigation header', + headingProps: _sharedHeadingProps, + }, + argTypes: { + // This story demos the header props; removing other props to prevent confusion + toggleOpenOnMobile: { table: { disable: true } }, + isOpenOnMobile: { table: { disable: true } }, + mobileBreakpoints: { table: { disable: true } }, + items: { table: { disable: true } }, + renderItem: { table: { disable: true } }, + truncate: { table: { disable: true } }, + }, + render: ({ ...args }) => , +}; + +const StatefulSideNav = (props: Partial) => { + const [isSideNavOpenOnMobile, setIsSideNavOpenOnMobile] = useState(false); + const [selectedItemName, setSelectedItem] = useState('Time stuff'); + + const toggleOpenOnMobile = () => { + setIsSideNavOpenOnMobile(!isSideNavOpenOnMobile); + }; + + const sideNav = [ + { + name: 'Kibana', + id: 'kibana', + onClick: undefined, + icon: , + items: [ + { + name: 'Has nested children', + id: 'normal_children', + isSelected: selectedItemName === 'Has nested children', + onClick: () => setSelectedItem('Has nested children'), + items: [ + { + name: 'Child 1', + id: 'child_1', + isSelected: selectedItemName === 'Child 1', + onClick: () => setSelectedItem('Child 1'), + items: [ + { + name: 'Child 2', + id: 'child_2', + isSelected: selectedItemName === 'Child 2', + onClick: () => setSelectedItem('Child 2'), + items: [], + }, + ], + }, + ], + }, + { + name: 'Has forceOpen: true', + id: 'force_open', + isSelected: selectedItemName === 'Has forceOpen: true', + onClick: () => setSelectedItem('Has forceOpen: true'), + forceOpen: true, + items: [ + { + name: 'Child 3', + id: 'child_3', + isSelected: selectedItemName === 'Child 3', + onClick: () => setSelectedItem('Child 3'), + }, + ], + }, + { + name: 'Children only without link', + id: 'children_only', + isSelected: selectedItemName === 'Children only without link', + onClick: undefined, + items: [ + { + name: 'Child 4', + id: 'child_4', + isSelected: selectedItemName === 'Child 4', + onClick: () => setSelectedItem('Child 4'), + }, + ], + }, + ], + }, + ]; + + return ( + + ); +}; From b42d650c21df61d71697d547fa821b04eeef7464 Mon Sep 17 00:00:00 2001 From: Bree Hall Date: Tue, 19 Sep 2023 15:52:02 -0400 Subject: [PATCH 2/6] [Storybook] Refactor stories and playground for EuiSideNav --- src/components/side_nav/side_nav.stories.tsx | 122 ++++++++++++++++--- 1 file changed, 107 insertions(+), 15 deletions(-) diff --git a/src/components/side_nav/side_nav.stories.tsx b/src/components/side_nav/side_nav.stories.tsx index a75d1b0cf7c..3010d5040ac 100644 --- a/src/components/side_nav/side_nav.stories.tsx +++ b/src/components/side_nav/side_nav.stories.tsx @@ -14,7 +14,9 @@ import { EuiSideNavProps, EuiSideNavHeadingProps, } from './side_nav'; + import { EuiIcon } from '../icon'; +import { EuiText } from '../text'; const meta: Meta = { title: 'EuiSideNav', @@ -44,44 +46,134 @@ export const Playground: Story = { heading: 'Elastic', headingProps: _sharedHeadingProps, isOpenOnMobile: false, - truncate: false, + items: [ + { + name: 'Kibana', + id: 'kibana', + icon: , + items: [ + { + name: 'Has nested children', + id: 'normal_children', + items: [ + { + name: 'Child 1', + id: 'child_1', + items: [ + { + name: 'Selected item', + id: 'selected_item', + onClick: () => {}, + isSelected: true, + items: [], + }, + ], + }, + ], + }, + { + name: 'Has forceOpen: true', + id: 'force_open', + forceOpen: true, + items: [ + { + name: 'Child 3', + id: 'child_3', + }, + ], + }, + { + name: 'Children only without link', + id: 'children_only', + onClick: undefined, + items: [ + { + name: 'Child 4', + id: 'child_4', + }, + ], + }, + ], + }, + ], + }, + render: ({ ...args }) => ( + + ), +}; + +export const MobileSideNav: Story = { + args: { + ...componentDefaults, + heading: 'Elastic', + headingProps: _sharedHeadingProps, + isOpenOnMobile: true, + truncate: true, + }, + argTypes: { + // This story demos the side nav on smaller screens; removing other props to prevent confusion + 'aria-label': { table: { disable: true } }, + heading: { table: { disable: true } }, + headingProps: { table: { disable: true } }, + toggleOpenOnMobile: { table: { disable: true } }, + isOpenOnMobile: { table: { disable: true } }, + items: { table: { disable: true } }, + renderItem: { table: { disable: true } }, + truncate: { table: { disable: true } }, + }, + parameters: { + viewport: { + defaultViewport: 'mobile1', + }, }, render: ({ ...args }) => , }; -export const SideNavHeader: Story = { +export const RenderItem: Story = { args: { ...componentDefaults, items: [ { - name: 'Root item', - id: 'rootItem', - items: [ - { - name: 'Child item', - id: 'childItem', - onClick: () => {}, - }, - ], + name: 'Kibana', + id: 'kibana', + renderItem: ({ children }) => ( + {children} + ), + }, + { + name: 'Observability', + id: 'observability', + }, + { + name: 'Security', + id: 'security', }, ], + renderItem: ({ children }) => {children}, heading: 'Navigation header', headingProps: _sharedHeadingProps, }, argTypes: { - // This story demos the header props; removing other props to prevent confusion + // This story demos the renderItem prop; removing other props to prevent confusion + 'aria-label': { table: { disable: true } }, + heading: { table: { disable: true } }, + headingProps: { table: { disable: true } }, toggleOpenOnMobile: { table: { disable: true } }, isOpenOnMobile: { table: { disable: true } }, mobileBreakpoints: { table: { disable: true } }, - items: { table: { disable: true } }, - renderItem: { table: { disable: true } }, + mobileTitle: { table: { disable: true } }, truncate: { table: { disable: true } }, }, render: ({ ...args }) => , }; const StatefulSideNav = (props: Partial) => { - const [isSideNavOpenOnMobile, setIsSideNavOpenOnMobile] = useState(false); + const [isSideNavOpenOnMobile, setIsSideNavOpenOnMobile] = useState( + props.isOpenOnMobile + ); const [selectedItemName, setSelectedItem] = useState('Time stuff'); const toggleOpenOnMobile = () => { From bbfea96b8ffb7ac7c3ae87d2cff6a19bc3ac01dd Mon Sep 17 00:00:00 2001 From: Bree Hall Date: Tue, 19 Sep 2023 15:52:51 -0400 Subject: [PATCH 3/6] Update Storybook configuration to allow us to set default device sizes on playgrounds and stories for components --- .storybook/preview.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 45415c2d447..d30ab639df8 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -32,6 +32,8 @@ appendIconComponentCache(iconCache); import { EuiProvider } from '../src/components/provider'; import { writingModeStyles } from './writing_mode.styles'; +import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport'; + // Import light theme for components still using Sass styling // TODO: Remove this import and the `yarn compile-scss &&` command // once all EUI components are converted to Emotion @@ -93,6 +95,9 @@ const preview: Preview = { date: /Date$/, }, }, + viewport: { + viewports: MINIMAL_VIEWPORTS, + }, }, // Due to CommonProps, these props appear on almost every Story, but generally // aren't super useful to test - let's disable them by default and (if needed) From 89df53e3fc981b90b7e21502afa5c5e1dd7ca381 Mon Sep 17 00:00:00 2001 From: Bree Hall Date: Fri, 22 Sep 2023 14:59:09 -0400 Subject: [PATCH 4/6] [PR Feedback] Prefer Storybook decorator over inline styling on components. --- src/components/side_nav/side_nav.stories.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/components/side_nav/side_nav.stories.tsx b/src/components/side_nav/side_nav.stories.tsx index 3010d5040ac..324d39d0c1e 100644 --- a/src/components/side_nav/side_nav.stories.tsx +++ b/src/components/side_nav/side_nav.stories.tsx @@ -21,6 +21,14 @@ import { EuiText } from '../text'; const meta: Meta = { title: 'EuiSideNav', component: EuiSideNav, + decorators: [ + (Story) => ( +
+ {/* The side nav is visually easier to see with the width set */} + +
+ ), + ], }; export default meta; @@ -97,12 +105,6 @@ export const Playground: Story = { }, ], }, - render: ({ ...args }) => ( - - ), }; export const MobileSideNav: Story = { @@ -249,7 +251,6 @@ const StatefulSideNav = (props: Partial) => { toggleOpenOnMobile={toggleOpenOnMobile} isOpenOnMobile={isSideNavOpenOnMobile} items={sideNav} - css={{ width: '200px' }} // Required to view text truncation /> ); }; From bb55b25a731e032a20fce4ad329c98d0fb5c3f34 Mon Sep 17 00:00:00 2001 From: Bree Hall Date: Fri, 22 Sep 2023 15:03:17 -0400 Subject: [PATCH 5/6] [PR Feedback] Import order --- .storybook/preview.tsx | 3 +-- src/components/side_nav/side_nav.stories.tsx | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index d30ab639df8..e2ba33fff7d 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -10,6 +10,7 @@ import React from 'react'; import type { Preview } from '@storybook/react'; +import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport'; /* * Preload all EuiIcons - Storybook does not support dynamic icon loading @@ -32,8 +33,6 @@ appendIconComponentCache(iconCache); import { EuiProvider } from '../src/components/provider'; import { writingModeStyles } from './writing_mode.styles'; -import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport'; - // Import light theme for components still using Sass styling // TODO: Remove this import and the `yarn compile-scss &&` command // once all EUI components are converted to Emotion diff --git a/src/components/side_nav/side_nav.stories.tsx b/src/components/side_nav/side_nav.stories.tsx index 324d39d0c1e..f992385e548 100644 --- a/src/components/side_nav/side_nav.stories.tsx +++ b/src/components/side_nav/side_nav.stories.tsx @@ -9,15 +9,15 @@ import React, { useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; +import { EuiIcon } from '../icon'; +import { EuiText } from '../text'; + import { EuiSideNav, EuiSideNavProps, EuiSideNavHeadingProps, } from './side_nav'; -import { EuiIcon } from '../icon'; -import { EuiText } from '../text'; - const meta: Meta = { title: 'EuiSideNav', component: EuiSideNav, From 23e6c3beeeed6ebcda32503f390d0cfa7b0ffb9a Mon Sep 17 00:00:00 2001 From: Bree Hall Date: Fri, 22 Sep 2023 15:32:24 -0400 Subject: [PATCH 6/6] [PR Feedback] Code clean up and hone in on story focus --- src/components/side_nav/side_nav.stories.tsx | 215 +++++-------------- 1 file changed, 54 insertions(+), 161 deletions(-) diff --git a/src/components/side_nav/side_nav.stories.tsx b/src/components/side_nav/side_nav.stories.tsx index f992385e548..fdbfae7c9e1 100644 --- a/src/components/side_nav/side_nav.stories.tsx +++ b/src/components/side_nav/side_nav.stories.tsx @@ -6,17 +6,12 @@ * Side Public License, v 1. */ -import React, { useState } from 'react'; +import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; -import { EuiIcon } from '../icon'; import { EuiText } from '../text'; -import { - EuiSideNav, - EuiSideNavProps, - EuiSideNavHeadingProps, -} from './side_nav'; +import { EuiSideNav, EuiSideNavProps } from './side_nav'; const meta: Meta = { title: 'EuiSideNav', @@ -37,91 +32,76 @@ type Story = StoryObj; const componentDefaults: EuiSideNavProps = { mobileBreakpoints: ['xs', 's'], items: [], - // Aria-label and mobileTitle do not have defaults; they are being set here as they are shared between examples - 'aria-label': 'Side navigation example', + // mobileTitle does not have defaults; they are being set here as they are shared between examples mobileTitle: 'Mobile navigation header', + isOpenOnMobile: false, }; -// Heading props shared across examples -const _sharedHeadingProps: EuiSideNavHeadingProps = { - element: 'h1', - screenReaderOnly: false, -}; - -export const Playground: Story = { - args: { - ...componentDefaults, - heading: 'Elastic', - headingProps: _sharedHeadingProps, - isOpenOnMobile: false, +const sharedSideNavItems = [ + { + name: 'Has nested children', + id: 'normal_children', items: [ { - name: 'Kibana', - id: 'kibana', - icon: , + name: 'Child 1', + id: 'child_1', items: [ { - name: 'Has nested children', - id: 'normal_children', - items: [ - { - name: 'Child 1', - id: 'child_1', - items: [ - { - name: 'Selected item', - id: 'selected_item', - onClick: () => {}, - isSelected: true, - items: [], - }, - ], - }, - ], - }, - { - name: 'Has forceOpen: true', - id: 'force_open', - forceOpen: true, - items: [ - { - name: 'Child 3', - id: 'child_3', - }, - ], - }, - { - name: 'Children only without link', - id: 'children_only', - onClick: undefined, - items: [ - { - name: 'Child 4', - id: 'child_4', - }, - ], + name: 'Selected item', + id: 'selected_item', + onClick: () => {}, + isSelected: true, + items: [], }, ], }, ], }, + { + name: 'Has forceOpen: true', + id: 'force_open', + forceOpen: true, + items: [ + { + name: 'Child 3', + id: 'child_3', + }, + ], + }, + { + name: 'Children only without link', + id: 'children_only', + onClick: undefined, + items: [ + { + name: 'Child 4', + id: 'child_4', + }, + ], + }, +]; + +export const Playground: Story = { + args: { + ...componentDefaults, + heading: 'Elastic', + headingProps: { element: 'h1', screenReaderOnly: false }, + items: sharedSideNavItems, + }, }; export const MobileSideNav: Story = { args: { ...componentDefaults, - heading: 'Elastic', - headingProps: _sharedHeadingProps, isOpenOnMobile: true, - truncate: true, + items: sharedSideNavItems, + mobileTitle: 'Toggle isOpenOnMobile in the controls panel', }, argTypes: { - // This story demos the side nav on smaller screens; removing other props to prevent confusion + // This story demos the side nav on smaller screens; removing other props to streamline controls 'aria-label': { table: { disable: true } }, heading: { table: { disable: true } }, headingProps: { table: { disable: true } }, - toggleOpenOnMobile: { table: { disable: true } }, - isOpenOnMobile: { table: { disable: true } }, items: { table: { disable: true } }, renderItem: { table: { disable: true } }, truncate: { table: { disable: true } }, @@ -131,19 +111,16 @@ export const MobileSideNav: Story = { defaultViewport: 'mobile1', }, }, - render: ({ ...args }) => , }; export const RenderItem: Story = { args: { ...componentDefaults, + renderItem: ({ children }) => {children}, items: [ { name: 'Kibana', id: 'kibana', - renderItem: ({ children }) => ( - {children} - ), }, { name: 'Observability', @@ -152,14 +129,14 @@ export const RenderItem: Story = { { name: 'Security', id: 'security', + renderItem: ({ children }) => ( + {children} + ), }, ], - renderItem: ({ children }) => {children}, - heading: 'Navigation header', - headingProps: _sharedHeadingProps, }, argTypes: { - // This story demos the renderItem prop; removing other props to prevent confusion + // This story demos the renderItem prop; removing other props to streamline controls 'aria-label': { table: { disable: true } }, heading: { table: { disable: true } }, headingProps: { table: { disable: true } }, @@ -169,88 +146,4 @@ export const RenderItem: Story = { mobileTitle: { table: { disable: true } }, truncate: { table: { disable: true } }, }, - render: ({ ...args }) => , -}; - -const StatefulSideNav = (props: Partial) => { - const [isSideNavOpenOnMobile, setIsSideNavOpenOnMobile] = useState( - props.isOpenOnMobile - ); - const [selectedItemName, setSelectedItem] = useState('Time stuff'); - - const toggleOpenOnMobile = () => { - setIsSideNavOpenOnMobile(!isSideNavOpenOnMobile); - }; - - const sideNav = [ - { - name: 'Kibana', - id: 'kibana', - onClick: undefined, - icon: , - items: [ - { - name: 'Has nested children', - id: 'normal_children', - isSelected: selectedItemName === 'Has nested children', - onClick: () => setSelectedItem('Has nested children'), - items: [ - { - name: 'Child 1', - id: 'child_1', - isSelected: selectedItemName === 'Child 1', - onClick: () => setSelectedItem('Child 1'), - items: [ - { - name: 'Child 2', - id: 'child_2', - isSelected: selectedItemName === 'Child 2', - onClick: () => setSelectedItem('Child 2'), - items: [], - }, - ], - }, - ], - }, - { - name: 'Has forceOpen: true', - id: 'force_open', - isSelected: selectedItemName === 'Has forceOpen: true', - onClick: () => setSelectedItem('Has forceOpen: true'), - forceOpen: true, - items: [ - { - name: 'Child 3', - id: 'child_3', - isSelected: selectedItemName === 'Child 3', - onClick: () => setSelectedItem('Child 3'), - }, - ], - }, - { - name: 'Children only without link', - id: 'children_only', - isSelected: selectedItemName === 'Children only without link', - onClick: undefined, - items: [ - { - name: 'Child 4', - id: 'child_4', - isSelected: selectedItemName === 'Child 4', - onClick: () => setSelectedItem('Child 4'), - }, - ], - }, - ], - }, - ]; - - return ( - - ); };