From 9b6b6f60051f170dcb7f548fb114420a48a8e1e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Barth=C3=A9l=C3=A9my=20Ledoux?= Date: Wed, 22 Jul 2020 01:22:11 -0500 Subject: [PATCH] fix: Correct behavior of the link closing isolated view (#1627) Fixes #1479 --- .../ComponentsList/ComponentsList.spec.tsx | 143 +----- .../ComponentsList/ComponentsList.tsx | 35 +- .../ComponentsList/ComponentsListRenderer.tsx | 1 + .../ReactComponent/ReactComponent.spec.tsx | 1 + .../ReactComponent/ReactComponent.tsx | 3 +- .../rsg-components/Section/Section.spec.tsx | 4 +- .../SectionHeading/SectionHeading.spec.tsx | 24 +- .../SectionHeading/SectionHeading.tsx | 8 +- .../SectionHeading/SectionHeadingRenderer.tsx | 4 +- .../rsg-components/Sections/Sections.tsx | 2 +- .../StyleGuide/StyleGuide.spec.tsx | 4 +- .../TableOfContents/TableOfContents.spec.tsx | 119 ++--- .../TableOfContents/TableOfContents.tsx | 30 +- .../TableOfContents.spec.tsx.snap | 427 ------------------ .../slots/IsolateButton.spec.tsx | 6 +- .../rsg-components/slots/IsolateButton.tsx | 13 +- src/client/utils/__tests__/getUrl.spec.ts | 6 +- .../utils/__tests__/processComponents.spec.ts | 27 +- .../utils/__tests__/processSections.spec.ts | 19 +- src/client/utils/getUrl.ts | 8 +- src/client/utils/processComponents.ts | 21 +- src/client/utils/processSections.ts | 40 +- src/client/utils/renderStyleguide.tsx | 4 +- src/typings/RsgSection.ts | 2 + test/cypress/integration/component_spec.js | 4 +- 25 files changed, 192 insertions(+), 763 deletions(-) delete mode 100644 src/client/rsg-components/TableOfContents/__snapshots__/TableOfContents.spec.tsx.snap diff --git a/src/client/rsg-components/ComponentsList/ComponentsList.spec.tsx b/src/client/rsg-components/ComponentsList/ComponentsList.spec.tsx index 3844718b0..450a44c73 100644 --- a/src/client/rsg-components/ComponentsList/ComponentsList.spec.tsx +++ b/src/client/rsg-components/ComponentsList/ComponentsList.spec.tsx @@ -13,115 +13,6 @@ const context = { const Provider = (props: any) => ; -it('should set the correct href for items', () => { - const components = [ - { - visibleName: 'Button', - name: 'Button', - slug: 'button', - }, - { - visibleName: 'Input', - name: 'Input', - slug: 'input', - }, - ]; - - const { getAllByRole } = render( - - - - ); - - expect(Array.from(getAllByRole('link')).map(node => (node as HTMLAnchorElement).href)).toEqual([ - 'http://localhost/#button', - 'http://localhost/#input', - ]); -}); - -it('if a custom href is provided, should use it instead of generating internal link', () => { - const components = [ - { - visibleName: 'External example', - name: 'External example', - href: 'http://example.com/', - }, - { - visibleName: 'Input', - name: 'Input', - slug: 'input', - }, - ]; - - const { getAllByRole } = render( - - - - ); - - expect(Array.from(getAllByRole('link')).map(node => (node as HTMLAnchorElement).href)).toEqual([ - 'http://example.com/', - 'http://localhost/#input', - ]); -}); - -it('should set an id parameter on link when useHashId is activated', () => { - const components = [ - { - visibleName: 'Button', - name: 'Button', - slug: 'button', - }, - { - visibleName: 'Input', - name: 'Input', - slug: 'input', - }, - ]; - - const { getAllByRole } = render( - - - - ); - - expect(Array.from(getAllByRole('link')).map(node => (node as HTMLAnchorElement).href)).toEqual([ - 'http://localhost/#/Components?id=button', - 'http://localhost/#/Components?id=input', - ]); -}); - -it('should set a sub route on link when useHashId is deactivated', () => { - const components = [ - { - visibleName: 'Button', - name: 'Button', - slug: 'button', - }, - { - visibleName: 'Input', - name: 'Input', - slug: 'input', - }, - ]; - - const { getAllByRole } = render( - - - - ); - - expect(Array.from(getAllByRole('link')).map(node => (node as HTMLAnchorElement).href)).toEqual([ - 'http://localhost/#/Components/Button', - 'http://localhost/#/Components/Input', - ]); -}); - it('should not render any links when the list is empty', () => { const { queryAllByRole } = render( @@ -147,12 +38,7 @@ it('should ignore items without visibleName', () => { const { getAllByRole } = render( - + ); @@ -167,24 +53,21 @@ it('should show content of items that are open and not what is closed', () => { visibleName: 'Button', name: 'Button', slug: 'button', + href: '#buttton', content:
Content for Button
, }, { visibleName: 'Input', name: 'Input', slug: 'input', + href: '#input', content:
Content for Input
, }, ]; const { getAllByTestId, getByText } = render( - + ); @@ -201,12 +84,14 @@ it('should show content of initialOpen items even if they are not active', () => visibleName: 'Button', name: 'Button', slug: 'button', + href: '#button', content:
Content for Button
, }, { visibleName: 'Input', name: 'Input', slug: 'input', + href: '#input', content:
Content for Input
, initialOpen: true, }, @@ -214,12 +99,7 @@ it('should show content of initialOpen items even if they are not active', () => const { getAllByTestId, getByText } = render( - + ); @@ -236,6 +116,7 @@ it('should show content of forcedOpen items even if they are initially collapsed visibleName: 'Button', name: 'Button', slug: 'button', + href: '#button', content:
Content for Button
, initialOpen: true, }, @@ -243,6 +124,7 @@ it('should show content of forcedOpen items even if they are initially collapsed visibleName: 'Input', name: 'Input', slug: 'input', + href: '#input', content:
Content for Input
, initialOpen: true, forcedOpen: true, @@ -251,12 +133,7 @@ it('should show content of forcedOpen items even if they are initially collapsed const { getAllByTestId, getByText } = render( - + ); diff --git a/src/client/rsg-components/ComponentsList/ComponentsList.tsx b/src/client/rsg-components/ComponentsList/ComponentsList.tsx index 83b338066..b6bf5fe67 100644 --- a/src/client/rsg-components/ComponentsList/ComponentsList.tsx +++ b/src/client/rsg-components/ComponentsList/ComponentsList.tsx @@ -1,49 +1,20 @@ import React from 'react'; import PropTypes from 'prop-types'; import ComponentsListRenderer from 'rsg-components/ComponentsList/ComponentsListRenderer'; -import getUrl from '../../utils/getUrl'; import * as Rsg from '../../../typings'; interface ComponentsListProps { items: Rsg.TOCItem[]; - hashPath?: string[]; - useRouterLinks?: boolean; - useHashId?: boolean; } -const ComponentsList: React.FunctionComponent = ({ - items, - useRouterLinks = false, - useHashId, - hashPath, -}) => { - const mappedItems = items - .map(item => { - const href = item.href - ? item.href - : getUrl({ - name: item.name, - slug: item.slug, - anchor: !useRouterLinks, - hashPath: useRouterLinks ? hashPath : false, - id: useRouterLinks ? useHashId : false, - }); +const ComponentsList: React.FunctionComponent = ({ items }) => { + const visibleItems = items.filter(item => item.visibleName); - return { - ...item, - href, - }; - }) - .filter(item => item.visibleName); - - return mappedItems.length > 0 ? : null; + return visibleItems.length > 0 ? : null; }; ComponentsList.propTypes = { items: PropTypes.array.isRequired, - hashPath: PropTypes.array, - useRouterLinks: PropTypes.bool, - useHashId: PropTypes.bool, }; export default ComponentsList; diff --git a/src/client/rsg-components/ComponentsList/ComponentsListRenderer.tsx b/src/client/rsg-components/ComponentsList/ComponentsListRenderer.tsx index da772749d..3c7f3b0a8 100644 --- a/src/client/rsg-components/ComponentsList/ComponentsListRenderer.tsx +++ b/src/client/rsg-components/ComponentsList/ComponentsListRenderer.tsx @@ -84,6 +84,7 @@ const ComponentsListSectionRenderer: React.FunctionComponent setOpen(!open)} target={shouldOpenInNewTab ? '_blank' : undefined} + data-testid="rsg-toc-link" > {visibleName} diff --git a/src/client/rsg-components/ReactComponent/ReactComponent.spec.tsx b/src/client/rsg-components/ReactComponent/ReactComponent.spec.tsx index 80d63ca42..e262fc712 100644 --- a/src/client/rsg-components/ReactComponent/ReactComponent.spec.tsx +++ b/src/client/rsg-components/ReactComponent/ReactComponent.spec.tsx @@ -24,6 +24,7 @@ const component = { name: 'Foo', visibleName: 'Foo', slug: 'foo', + href: '#foo', pathLine: 'foo/bar.js', props: { description: 'Bar', diff --git a/src/client/rsg-components/ReactComponent/ReactComponent.tsx b/src/client/rsg-components/ReactComponent/ReactComponent.tsx index 5e909c246..5c57893d2 100644 --- a/src/client/rsg-components/ReactComponent/ReactComponent.tsx +++ b/src/client/rsg-components/ReactComponent/ReactComponent.tsx @@ -53,7 +53,7 @@ export default class ReactComponent extends Component {visibleName} diff --git a/src/client/rsg-components/Section/Section.spec.tsx b/src/client/rsg-components/Section/Section.spec.tsx index 8f1816af7..9255f4c71 100644 --- a/src/client/rsg-components/Section/Section.spec.tsx +++ b/src/client/rsg-components/Section/Section.spec.tsx @@ -100,7 +100,7 @@ test('should not render section in isolation mode by default', () => { }); test('should render section in isolation mode', () => { - const { getByLabelText } = render( + const { queryByLabelText } = render( { /> ); - expect(getByLabelText(/show all components/i)).toBeInTheDocument(); + expect(queryByLabelText(/open isolated/i)).toBeNull(); }); diff --git a/src/client/rsg-components/SectionHeading/SectionHeading.spec.tsx b/src/client/rsg-components/SectionHeading/SectionHeading.spec.tsx index 6cbf4c62a..7a6a01b76 100644 --- a/src/client/rsg-components/SectionHeading/SectionHeading.spec.tsx +++ b/src/client/rsg-components/SectionHeading/SectionHeading.spec.tsx @@ -8,7 +8,13 @@ describe('SectionHeading', () => { test('should forward slot properties to the toolbar', () => { const actual = shallow( - + A Section ); @@ -51,20 +57,4 @@ describe('SectionHeading', () => { expect(actual.find('h6')).toHaveLength(1); }); - - test('the href have id=section query parameter ', () => { - const actual = shallow( - - A Section - - ); - - expect(actual.prop('href')).toEqual('/?id=section'); - }); }); diff --git a/src/client/rsg-components/SectionHeading/SectionHeading.tsx b/src/client/rsg-components/SectionHeading/SectionHeading.tsx index fe61cb63d..804414c89 100644 --- a/src/client/rsg-components/SectionHeading/SectionHeading.tsx +++ b/src/client/rsg-components/SectionHeading/SectionHeading.tsx @@ -2,7 +2,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import Slot from 'rsg-components/Slot'; import SectionHeadingRenderer from 'rsg-components/SectionHeading/SectionHeadingRenderer'; -import getUrl from '../../utils/getUrl'; interface SectionHeadingProps { children?: React.ReactNode; @@ -10,6 +9,7 @@ interface SectionHeadingProps { slotName: string; slotProps: object; depth: number; + href?: string; deprecated?: boolean; pagePerSection?: boolean; } @@ -19,13 +19,9 @@ const SectionHeading: React.FunctionComponent = ({ slotProps, children, id, - pagePerSection, + href, ...rest }) => { - const href = pagePerSection - ? getUrl({ slug: id, id: rest.depth !== 1, takeHash: true }) - : getUrl({ slug: id, anchor: true }); - return ( } diff --git a/src/client/rsg-components/SectionHeading/SectionHeadingRenderer.tsx b/src/client/rsg-components/SectionHeading/SectionHeadingRenderer.tsx index 59208ffd2..46bcd8911 100644 --- a/src/client/rsg-components/SectionHeading/SectionHeadingRenderer.tsx +++ b/src/client/rsg-components/SectionHeading/SectionHeadingRenderer.tsx @@ -34,7 +34,7 @@ interface SectionHeadingRendererProps extends JssInjectedProps { children?: React.ReactNode; toolbar?: React.ReactNode; id: string; - href: string; + href?: string; depth: number; deprecated?: boolean; } @@ -70,7 +70,7 @@ SectionHeadingRenderer.propTypes = { children: PropTypes.node, toolbar: PropTypes.node, id: PropTypes.string.isRequired, - href: PropTypes.string.isRequired, + href: PropTypes.string, depth: PropTypes.number.isRequired, deprecated: PropTypes.bool, }; diff --git a/src/client/rsg-components/Sections/Sections.tsx b/src/client/rsg-components/Sections/Sections.tsx index 0a424b872..8ae27c947 100644 --- a/src/client/rsg-components/Sections/Sections.tsx +++ b/src/client/rsg-components/Sections/Sections.tsx @@ -12,7 +12,7 @@ const Sections: React.FunctionComponent<{ return ( {sections - .filter(section => !section.href) + .filter(section => !section.externalLink) .map((section, idx) => (
))} diff --git a/src/client/rsg-components/StyleGuide/StyleGuide.spec.tsx b/src/client/rsg-components/StyleGuide/StyleGuide.spec.tsx index 9b2659d5b..adbedc731 100644 --- a/src/client/rsg-components/StyleGuide/StyleGuide.spec.tsx +++ b/src/client/rsg-components/StyleGuide/StyleGuide.spec.tsx @@ -14,7 +14,7 @@ const sections: Rsg.Section[] = [ { name: 'Foo', visibleName: 'Foo', - slug: 'foo', + href: '#foo', pathLine: 'components/foo.js', filepath: 'components/foo.js', props: { @@ -24,7 +24,7 @@ const sections: Rsg.Section[] = [ { name: 'Bar', visibleName: 'Bar', - slug: 'bar', + href: '#bar', pathLine: 'components/bar.js', filepath: 'components/bar.js', props: { diff --git a/src/client/rsg-components/TableOfContents/TableOfContents.spec.tsx b/src/client/rsg-components/TableOfContents/TableOfContents.spec.tsx index d26f5ca6e..c2c27a6a8 100644 --- a/src/client/rsg-components/TableOfContents/TableOfContents.spec.tsx +++ b/src/client/rsg-components/TableOfContents/TableOfContents.spec.tsx @@ -1,121 +1,95 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, fireEvent } from '@testing-library/react'; import { shallow } from 'enzyme'; -import noop from 'lodash/noop'; import TableOfContents from './TableOfContents'; import { TableOfContentsRenderer } from './TableOfContentsRenderer'; import Context from '../Context'; const components = [ { + visibleName: 'Button', name: 'Button', - slug: 'button', + href: '#button', }, { + visibleName: 'Input', name: 'Input', - slug: 'input', + href: '#input', }, { + visibleName: 'Textarea', name: 'Textarea', - slug: 'textarea', + href: '#textarea', }, ]; const sections = [ { + visibleName: 'Introduction', name: 'Introduction', - slug: 'introduction', + href: '#introduction', content: 'intro.md', }, { + visibleName: 'Buttons', name: 'Buttons', - slug: 'buttons', + href: '#buttons', components: [ { + visibleName: 'Button', name: 'Button', - slug: 'button', + href: '#button', }, ], }, { + visibleName: 'Forms', name: 'Forms', - slug: 'forms', + href: '#forms', components: [ { + visibleName: 'Input', name: 'Input', - slug: 'input', + href: '#input', }, { + visibleName: 'Textarea', name: 'Textarea', - slug: 'textarea', + href: '#textarea', }, ], }, ]; -it('should render a renderer', () => { - const actual = shallow( - - ); - - expect(actual).toMatchSnapshot(); -}); - -it('should render renderer with sections with nested components', () => { - const actual = shallow(); - - expect(actual).toMatchSnapshot(); -}); - it('should filter list when search field contains a query', () => { - const searchTerm = 'but'; - const actual = shallow( + const searchTerm = 'put'; + const { getByPlaceholderText, getAllByTestId, getByTestId } = render( ); - - expect(actual).toMatchSnapshot(); - - actual.setState({ searchTerm }); - - expect(actual).toMatchSnapshot(); + expect(getAllByTestId('rsg-toc-link').length).toBe(3); + fireEvent.change(getByPlaceholderText('Filter by name'), { target: { value: searchTerm } }); + expect(getAllByTestId('rsg-toc-link')).toHaveLength(1); + expect(getByTestId('rsg-toc-link')).toHaveTextContent('Input'); }); it('should filter section names', () => { const searchTerm = 'frm'; - const actual = shallow(); - - expect(actual).toMatchSnapshot(); - - actual.setState({ searchTerm }); - - expect(actual).toMatchSnapshot(); -}); - -it('renderer should render table of contents', () => { - const searchTerm = 'foo'; - const actual = shallow( - -
foo
-
+ const { getByPlaceholderText, getAllByTestId, getByTestId } = render( + ); - - expect(actual).toMatchSnapshot(); + expect(getAllByTestId('rsg-toc-link').length).toBe(6); + fireEvent.change(getByPlaceholderText('Filter by name'), { target: { value: searchTerm } }); + expect(getAllByTestId('rsg-toc-link')).toHaveLength(1); + expect(getByTestId('rsg-toc-link')).toHaveTextContent('Forms'); }); it('should call a callback when input value changed', () => { @@ -184,36 +158,39 @@ it('should render components of a single top section as root', () => { "content": undefined, "forcedOpen": false, "heading": false, + "href": "#button", "initialOpen": true, "name": "Button", "sections": Array [], "selected": false, "shouldOpenInNewTab": false, - "slug": "button", + "visibleName": "Button", }, Object { "components": Array [], "content": undefined, "forcedOpen": false, "heading": false, + "href": "#input", "initialOpen": true, "name": "Input", "sections": Array [], "selected": false, "shouldOpenInNewTab": false, - "slug": "input", + "visibleName": "Input", }, Object { "components": Array [], "content": undefined, "forcedOpen": false, "heading": false, + "href": "#textarea", "initialOpen": true, "name": "Textarea", "sections": Array [], "selected": false, "shouldOpenInNewTab": false, - "slug": "textarea", + "visibleName": "Textarea", }, ] `); @@ -256,7 +233,7 @@ it('should render as the link will open in a new window only if external present "initialOpen": true, "sections": Array [], "selected": false, - "shouldOpenInNewTab": true, + "shouldOpenInNewTab": false, }, ] `); @@ -272,8 +249,8 @@ it('should render components with useRouterLinks', () => { sections={[ { sections: [ - { visibleName: '1', name: 'Components', slug: 'Components', content: 'intro.md' }, - { visibleName: '2', content: 'chapter.md', slug: 'chap' }, + { visibleName: '1', name: 'Components', href: '#/Components', content: 'intro.md' }, + { visibleName: '2', content: 'chapter.md', href: '#/Chap' }, ], }, ]} @@ -305,20 +282,20 @@ it('should detect sections containing current selection when tocMode is collapse sections: [ { visibleName: '1', - slug: 'Components', - sections: [{ visibleName: '1.1', slug: 'Button' }], + href: '#/components', + sections: [{ visibleName: '1.1', href: '#/button' }], }, { visibleName: '2', - slug: 'chap', + href: '#/chap', content: 'chapter.md', - sections: [{ visibleName: '2.1', slug: 'Chapter #1' }], + sections: [{ visibleName: '2.1', href: '#/chapter-1' }], }, { visibleName: '3', href: 'http://react-styleguidist.com' }, ], }, ]} - loc={{ pathname: '', hash: '/#Button' }} + loc={{ pathname: '', hash: 'button' }} /> ); diff --git a/src/client/rsg-components/TableOfContents/TableOfContents.tsx b/src/client/rsg-components/TableOfContents/TableOfContents.tsx index 5b11bf74f..3ee7f2a85 100644 --- a/src/client/rsg-components/TableOfContents/TableOfContents.tsx +++ b/src/client/rsg-components/TableOfContents/TableOfContents.tsx @@ -4,7 +4,6 @@ import ComponentsList from 'rsg-components/ComponentsList'; import TableOfContentsRenderer from 'rsg-components/TableOfContents/TableOfContentsRenderer'; import filterSectionsByName from '../../utils/filterSectionsByName'; import { getHash } from '../../utils/handleHash'; -import getUrl from '../../utils/getUrl'; import * as Rsg from '../../../typings'; interface TableOfContentsProps { @@ -54,18 +53,8 @@ export default class TableOfContents extends Component { ? this.renderLevel(children, useRouterLinks, childHashPath, sectionDepth === 0) : { content: undefined, containsSelected: false }; - // get href - const href = section.href - ? section.href - : getUrl({ - name: section.name, - slug: section.slug, - anchor: !useRouterLinks, - hashPath: useRouterLinks ? hashPath : false, - id: useRouterLinks ? useHashId : false, - }); - - const selected = href === windowHash; + const selected = + (!useRouterLinks && section.href ? getHash(section.href) : section.href) === windowHash; if (containsSelected || selected) { childrenContainSelected = true; @@ -76,20 +65,13 @@ export default class TableOfContents extends Component { heading: !!section.name && children.length > 0, content, selected, - shouldOpenInNewTab: !!section.external && !!section.href, + shouldOpenInNewTab: !!section.external && !!section.externalLink, initialOpen: this.props.tocMode !== 'collapse' || containsSelected, forcedOpen: !!this.state.searchTerm.length, }; }); return { - content: ( - - ), + content: , containsSelected: childrenContainSelected, }; } @@ -109,9 +91,9 @@ export default class TableOfContents extends Component { ? sections[0].sections : sections[0].components : sections; - const filtered = firstLevel ? filterSectionsByName(firstLevel, searchTerm) : firstLevel; + const filtered = firstLevel ? filterSectionsByName(firstLevel, searchTerm) : firstLevel || []; - return filtered ? this.renderLevel(filtered, useRouterLinks).content : null; + return this.renderLevel(filtered, useRouterLinks).content; } public render() { diff --git a/src/client/rsg-components/TableOfContents/__snapshots__/TableOfContents.spec.tsx.snap b/src/client/rsg-components/TableOfContents/__snapshots__/TableOfContents.spec.tsx.snap deleted file mode 100644 index 8997a3541..000000000 --- a/src/client/rsg-components/TableOfContents/__snapshots__/TableOfContents.spec.tsx.snap +++ /dev/null @@ -1,427 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renderer should render table of contents 1`] = ` -
-
- -
-
-`; - -exports[`should filter list when search field contains a query 1`] = ` - - - -`; - -exports[`should filter list when search field contains a query 2`] = ` - - - -`; - -exports[`should filter section names 1`] = ` - - , - "forcedOpen": false, - "heading": true, - "initialOpen": true, - "name": "Buttons", - "sections": Array [], - "selected": false, - "shouldOpenInNewTab": false, - "slug": "buttons", - }, - Object { - "components": Array [ - Object { - "name": "Input", - "slug": "input", - }, - Object { - "name": "Textarea", - "slug": "textarea", - }, - ], - "content": , - "forcedOpen": false, - "heading": true, - "initialOpen": true, - "name": "Forms", - "sections": Array [], - "selected": false, - "shouldOpenInNewTab": false, - "slug": "forms", - }, - ] - } - useHashId={false} - useRouterLinks={false} - /> - -`; - -exports[`should filter section names 2`] = ` - - - -`; - -exports[`should render a renderer 1`] = ` - - - -`; - -exports[`should render renderer with sections with nested components 1`] = ` - - , - "forcedOpen": false, - "heading": true, - "initialOpen": true, - "name": "Buttons", - "sections": Array [], - "selected": false, - "shouldOpenInNewTab": false, - "slug": "buttons", - }, - Object { - "components": Array [ - Object { - "name": "Input", - "slug": "input", - }, - Object { - "name": "Textarea", - "slug": "textarea", - }, - ], - "content": , - "forcedOpen": false, - "heading": true, - "initialOpen": true, - "name": "Forms", - "sections": Array [], - "selected": false, - "shouldOpenInNewTab": false, - "slug": "forms", - }, - ] - } - useHashId={false} - useRouterLinks={false} - /> - -`; diff --git a/src/client/rsg-components/slots/IsolateButton.spec.tsx b/src/client/rsg-components/slots/IsolateButton.spec.tsx index 02b6b9140..28862a654 100644 --- a/src/client/rsg-components/slots/IsolateButton.spec.tsx +++ b/src/client/rsg-components/slots/IsolateButton.spec.tsx @@ -3,19 +3,19 @@ import { shallow } from 'enzyme'; import IsolateButton from './IsolateButton'; it('should renderer a link to isolated mode', () => { - const actual = shallow(); + const actual = shallow(); expect(actual).toMatchSnapshot(); }); it('should renderer a link to example isolated mode', () => { - const actual = shallow(); + const actual = shallow(); expect(actual).toMatchSnapshot(); }); it('should renderer a link home in isolated mode', () => { - const actual = shallow(); + const actual = shallow(); expect(actual).toMatchSnapshot(); }); diff --git a/src/client/rsg-components/slots/IsolateButton.tsx b/src/client/rsg-components/slots/IsolateButton.tsx index 8d85bc069..38f9c071d 100644 --- a/src/client/rsg-components/slots/IsolateButton.tsx +++ b/src/client/rsg-components/slots/IsolateButton.tsx @@ -8,17 +8,18 @@ export interface IsolateButtonProps { name: string; example?: number; isolated?: boolean; + href: string; } -const IsolateButton = ({ name, example, isolated }: IsolateButtonProps) => { +const IsolateButton = ({ name, example, isolated, href }: IsolateButtonProps) => { + if (isolated && !href) { + return null; + } + const testID = example ? `${name}-${example}-isolate-button` : `${name}-isolate-button`; return isolated ? ( - + ) : ( diff --git a/src/client/utils/__tests__/getUrl.spec.ts b/src/client/utils/__tests__/getUrl.spec.ts index 867c142d0..f55e5aa0d 100644 --- a/src/client/utils/__tests__/getUrl.spec.ts +++ b/src/client/utils/__tests__/getUrl.spec.ts @@ -104,18 +104,18 @@ describe('getUrl', () => { }); it('should return a route path with a param id=foobar', () => { - const result = getUrl({ name, slug, hashPath: ['Documentation'], id: true }, loc); + const result = getUrl({ name, slug, hashPath: ['Documentation'], useSlugAsIdParam: true }, loc); expect(result).toBe('/styleguide/#/Documentation?id=foobar'); }); it('should return a param id=foobar', () => { - const result = getUrl({ name, slug, takeHash: true, id: true }, loc); + const result = getUrl({ name, slug, takeHash: true, useSlugAsIdParam: true }, loc); expect(result).toBe('/styleguide/#/Components?id=foobar'); }); it('should return to param id = foobar even if the hash has parameters', () => { const result = getUrl( - { name, slug, takeHash: true, id: true }, + { name, slug, takeHash: true, useSlugAsIdParam: true }, { ...loc, hash: '#/Components?foo=foobar', diff --git a/src/client/utils/__tests__/processComponents.spec.ts b/src/client/utils/__tests__/processComponents.spec.ts index 8885c3190..2c573c141 100644 --- a/src/client/utils/__tests__/processComponents.spec.ts +++ b/src/client/utils/__tests__/processComponents.spec.ts @@ -1,6 +1,8 @@ import deepfreeze from 'deepfreeze'; import processComponents from '../processComponents'; +const options = { useRouterLinks: false }; + describe('processComponents', () => { it('should set components’ displayName to a name property', () => { const components = deepfreeze([ @@ -8,13 +10,25 @@ describe('processComponents', () => { props: { displayName: 'Foo', }, - module: 13, }, ]); - const result = processComponents(components); + const result = processComponents(components, options); expect(result[0].name).toBe('Foo'); }); + it('should calculate href', () => { + const components = deepfreeze([ + { + slug: 'foo', + props: { + displayName: 'Foo', + }, + }, + ]); + const result = processComponents(components, options); + expect(result[0].href).toBe('/#foo'); + }); + describe('should set visibleName property on the component', () => { it('from an visibleName component prop if available', () => { const components = deepfreeze([ @@ -23,10 +37,9 @@ describe('processComponents', () => { displayName: 'Foo', visibleName: 'Foo Bar', }, - module: 13, }, ]); - const result = processComponents(components); + const result = processComponents(components, options); expect(result[0].visibleName).toBe('Foo Bar'); }); @@ -36,10 +49,9 @@ describe('processComponents', () => { props: { displayName: 'Foo', }, - module: 13, }, ]); - const result = processComponents(components); + const result = processComponents(components, options); expect(result[0].visibleName).toBe('Foo'); }); }); @@ -52,10 +64,9 @@ describe('processComponents', () => { examples: [1, 2] as any[], example: [3, 4] as any[], }, - module: 11, }, ]); - const result = processComponents(components); + const result = processComponents(components, options); expect(result[0].props && result[0].props.examples).toEqual([1, 2, 3, 4]); }); }); diff --git a/src/client/utils/__tests__/processSections.spec.ts b/src/client/utils/__tests__/processSections.spec.ts index a29ab19af..8e791925e 100644 --- a/src/client/utils/__tests__/processSections.spec.ts +++ b/src/client/utils/__tests__/processSections.spec.ts @@ -6,12 +6,13 @@ const sections = deepfreeze([ sections: [ { name: 'Components', + slug: 'components', components: [ { + slug: 'button', props: { displayName: 'Button', }, - module: 1, }, ], }, @@ -19,19 +20,21 @@ const sections = deepfreeze([ }, ]); +const options = { useRouterLinks: false, hashPath: [] }; + describe('processSections', () => { it('should recursively process all sections and components', () => { - const result = processSections(sections); + const result = processSections(sections, options); const sectionsExpected = result[0].sections || []; - expect( - sectionsExpected.length && - sectionsExpected[0].components && - sectionsExpected[0].components[0].name - ).toBe('Button'); + const comp = sectionsExpected.length + ? sectionsExpected[0].components && sectionsExpected[0].components[0] + : undefined; + expect(comp?.name).toBe('Button'); + expect(comp?.href).toBe('/#button'); }); it('should set visibleName property on each section', () => { - const result = processSections(sections); + const result = processSections(sections, options); const sectionsExpected = result[0].sections || []; expect(sectionsExpected[0].visibleName).toBe('Components'); }); diff --git a/src/client/utils/getUrl.ts b/src/client/utils/getUrl.ts index e84962f75..664ed4a22 100644 --- a/src/client/utils/getUrl.ts +++ b/src/client/utils/getUrl.ts @@ -65,7 +65,7 @@ interface GetUrlOptions { */ absolute: boolean; hashPath: string[] | false; - id: boolean; + useSlugAsIdParam: boolean; takeHash: boolean; } @@ -86,7 +86,7 @@ export default function getUrl( nochrome, absolute, hashPath, - id, + useSlugAsIdParam, takeHash, }: Partial = {}, { @@ -121,13 +121,13 @@ export default function getUrl( if (hashPath) { let encodedHashPath = hashPath.map(encodeURIComponent); - if (!id) { + if (!useSlugAsIdParam) { encodedHashPath = [...encodedHashPath, encodedName]; } url += `#/${encodedHashPath.join('/')}`; } - if (id) { + if (useSlugAsIdParam) { url += `?id=${slug}`; } diff --git a/src/client/utils/processComponents.ts b/src/client/utils/processComponents.ts index d537bc0cc..4abc188ba 100644 --- a/src/client/utils/processComponents.ts +++ b/src/client/utils/processComponents.ts @@ -1,4 +1,11 @@ import * as Rsg from '../../typings'; +import getUrl from './getUrl'; + +export interface HrefOptions { + hashPath?: string[]; + useRouterLinks: boolean; + useHashId?: boolean; +} /** * Do things that are hard or impossible to do in a loader: we don’t have access to component name @@ -7,7 +14,10 @@ import * as Rsg from '../../typings'; * @param {Array} components * @return {Array} */ -export default function processComponents(components: Rsg.Component[]): Rsg.Component[] { +export default function processComponents( + components: Rsg.Component[], + { useRouterLinks, useHashId, hashPath }: HrefOptions +): Rsg.Component[] { return components.map(component => { const newComponent: Rsg.Component = component.props ? { @@ -22,6 +32,15 @@ export default function processComponents(components: Rsg.Component[]): Rsg.Comp // Append @example doclet to all examples examples: [...(component.props.examples || []), ...(component.props.example || [])], }, + href: + component.href || + getUrl({ + name: component.props.displayName, + slug: component.slug, + anchor: !useRouterLinks, + hashPath: useRouterLinks ? hashPath : false, + useSlugAsIdParam: useRouterLinks ? useHashId : false, + }), } : {}; diff --git a/src/client/utils/processSections.ts b/src/client/utils/processSections.ts index 6c6574f84..63096d44f 100644 --- a/src/client/utils/processSections.ts +++ b/src/client/utils/processSections.ts @@ -1,5 +1,6 @@ -import processComponents from './processComponents'; import * as Rsg from '../../typings'; +import processComponents, { HrefOptions } from './processComponents'; +import getUrl from './getUrl'; /** * Recursively process each component in all sections. @@ -7,11 +8,34 @@ import * as Rsg from '../../typings'; * @param {Array} sections * @return {Array} */ -export default function processSections(sections: Rsg.Section[]): Rsg.Section[] { - return sections.map(section => ({ - ...section, - visibleName: section.name, - components: processComponents(section.components || []), - sections: processSections(section.sections || []), - })); +export default function processSections( + sections: Rsg.Section[], + { useRouterLinks, useHashId = false, hashPath = [] }: HrefOptions +): Rsg.Section[] { + return sections.map(section => { + const options = { + useRouterLinks, + useHashId: section.sectionDepth === 0, + hashPath: [...hashPath, section.name ? section.name : '-'], + }; + const href = + section.href || + getUrl({ + name: section.name, + slug: section.slug, + anchor: !useRouterLinks, + hashPath: useRouterLinks ? hashPath : false, + useSlugAsIdParam: useRouterLinks ? useHashId : false, + }); + + return { + ...section, + // flag the section as an external link to avoid rendering it later + externalLink: !!section.href, + href, + visibleName: section.name, + components: processComponents(section.components || [], options), + sections: processSections(section.sections || [], options), + }; + }); } diff --git a/src/client/utils/renderStyleguide.tsx b/src/client/utils/renderStyleguide.tsx index 17c8091ea..2f2ae67cc 100644 --- a/src/client/utils/renderStyleguide.tsx +++ b/src/client/utils/renderStyleguide.tsx @@ -29,7 +29,9 @@ export default function renderStyleguide( doc: { title: string } = document, hist: { replaceState: (name: string, title: string, url: string) => void } = window.history ): React.ReactElement { - const allSections = processSections(styleguide.sections); + const allSections = processSections(styleguide.sections, { + useRouterLinks: styleguide.config.pagePerSection, + }); const { title, pagePerSection, theme, styles } = styleguide.config; const { sections, displayMode } = getRouteData(allSections, loc.hash, pagePerSection); diff --git a/src/typings/RsgSection.ts b/src/typings/RsgSection.ts index 1fe24c638..a80dcad28 100644 --- a/src/typings/RsgSection.ts +++ b/src/typings/RsgSection.ts @@ -17,6 +17,8 @@ export interface BaseSection { export interface ProcessedSection extends BaseSection { visibleName?: string; filepath?: string; + externalLink?: boolean; + href?: string; } /** diff --git a/test/cypress/integration/component_spec.js b/test/cypress/integration/component_spec.js index 738607728..0b56c52f7 100644 --- a/test/cypress/integration/component_spec.js +++ b/test/cypress/integration/component_spec.js @@ -97,9 +97,7 @@ describe('Single component', () => { .should('have.length', 1); // Toggle out of isolated example mode - cy.get('@componentExamples') - .find('[data-testid$="-isolate-button"]') - .click(); + cy.get('[data-testid$="-isolate-button"]').click(); // Assert the other examples are showing again cy.get('@componentExamples')