Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions components/ContentstackRichText/ContentstackEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ExpandableCard from '@leafygreen-ui/expandable-card';
import ArrowRight from '@leafygreen-ui/icon/dist/ArrowRight';
import { spacing } from '@leafygreen-ui/tokens';

import LiveExampleBlock from './LiveExampleBlock/LiveExampleBlock';
import AnnotatedImageBlock from './AnnotatedImageBlock';
import BasicUsageBlock from './BasicUsageBlock';
import ExampleCardBlock from './ExampleCardBlock';
Expand Down Expand Up @@ -82,6 +83,9 @@ const blockToElementMap: {
</ExpandableCard>
),
horizontal_layout: props => <HorizontalLayout {...props} />,
live_example_block: props => (
<LiveExampleBlock storyName={props.story_name} />
),
} as const;

const ContentstackEntry = <T extends ContentTypeUID>({
Expand Down
14 changes: 13 additions & 1 deletion components/ContentstackRichText/ContentstackText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Fragment } from 'react';

import { HTMLElementProps } from '@leafygreen-ui/lib';
import { Polymorph } from '@leafygreen-ui/polymorphic';
import { InlineCode } from '@leafygreen-ui/typography';

import { CSTextNode } from './types';

Expand All @@ -10,9 +11,20 @@ interface CSRichTextProps extends HTMLElementProps<'span'> {
}

const ContentstackText = ({ node, ...rest }: CSRichTextProps) => {
const renderAs = node.bold ? 'b' : rest.className ? 'span' : Fragment;
let renderAs;

if (node.bold) {
renderAs = 'b';
} else if (node.inlineCode) {
renderAs = InlineCode;
} else if (rest.className) {
renderAs = 'span';
} else {
renderAs = Fragment;
}

return (
// @ts-ignore href not needed as no links will be rendered as a result of this component's logic
<Polymorph as={renderAs} {...rest}>
{node.text}
</Polymorph>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import ImageContainer from './ImageContainer';

const ExampleCardBlockWrapper = styled.div`
margin-block: ${spacing[5] + spacing[2]}px;
width: 100%;
`;

const TextContainer = styled.div`
Expand Down
18 changes: 14 additions & 4 deletions components/ContentstackRichText/HeaderContent.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { useEffect } from 'react';
import styled from '@emotion/styled';
import {
BreadcrumbHeader,
useGuidelinesContext,
} from 'contexts/GuidelinesContext';
import { kebabCase } from 'lodash';
import Link from 'next/link';

import Icon from '@leafygreen-ui/icon';
import { palette } from '@leafygreen-ui/palette';

import ContentstackChildren from './ContentstackChildren';
import { CSNode } from './types';
import { getCSNodeTextContent } from './utils';

Expand Down Expand Up @@ -53,14 +57,20 @@ const StyledIcon = styled(Icon)`
* Content of headers in rich text markup need to be wrapped in links and anchors for hashed links.
*/
const HeaderContent = ({ node }: { node: CSNode }) => {
const nodeText = getCSNodeTextContent(node);
const headerId = kebabCase(getCSNodeTextContent(node));
const { pushHeader } = useGuidelinesContext();

useEffect(() => {
if (node.type === 'h2' || node.type === 'h4') {
pushHeader(node.type as BreadcrumbHeader, nodeText);
}
}, []);

return (
<Link href={`#${headerId}`} passHref>
<StyledAnchor id={headerId}>
<LinkContent>
<ContentstackChildren nodeChildren={node.children} />
</LinkContent>
<LinkContent>{nodeText}</LinkContent>
<StyledIcon glyph="Link" fill={palette.gray.light1} />
</StyledAnchor>
</Link>
Expand Down
113 changes: 113 additions & 0 deletions components/ContentstackRichText/LiveExampleBlock/LiveExampleBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { useState } from 'react';
import styled from '@emotion/styled';
import { ComponentStoryFn } from '@storybook/react';
import { useGuidelinesContext } from 'contexts/GuidelinesContext';
import { getComponentStories } from 'utils/getComponentStories';

import { useAsyncEffect } from 'components/pages/example/useAsyncEffect';

import Button from '@leafygreen-ui/button';
import Card from '@leafygreen-ui/card';
import Code from '@leafygreen-ui/code';
import Icon from '@leafygreen-ui/icon';
import { spacing } from '@leafygreen-ui/tokens';

const CodeWrapper = styled('div')`
> div {
border-left: 0;
border-right: 0;
border-bottom: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
overflow: hidden;

& > div:before,
& > div:after {
z-index: 0;
}
}
`;

const LiveExampleBlock = ({ storyName }: { storyName: string }) => {
const [StoryFn, setStoryFn] = useState<
ComponentStoryFn<any> & React.FunctionComponent<any>
>();
const [args, setArgs] = useState<any>({});
const [sourceCode, setSourceCode] = useState<string>();
const { componentName } = useGuidelinesContext();
const [showCode, setShowCode] = useState<boolean>(true);
const toggleShowCode = () => setShowCode(sc => !sc);

useAsyncEffect(
// @ts-ignore conditional checked in useAsyncEffect
() => getComponentStories(componentName),
{
then: module => {
if (module) {
const { default: meta, ...stories } = module;
setStoryFn(() => stories[storyName]);
setArgs({ ...meta.args, ...stories[storyName].args });
setSourceCode(stories[storyName]?.parameters?.sourceCode);
} else {
console.error('Error parsing module', module);
}
},
catch: err => {
console.error('Error loading LiveExample');
console.error(err);
},
},
!!componentName,
[componentName],
);

return (
<>
{StoryFn ? (
<div>
<Card style={{ padding: 0, boxShadow: 'none' }}>
<div
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}
>
<div style={{ padding: `${spacing[5]}px` }}>
<StoryFn {...args} />
</div>
<div
style={{
width: '100%',
display: 'flex',
justifyContent: 'flex-end',
padding: `${spacing[3]}px`,
}}
>
<Button
size="xsmall"
onClick={toggleShowCode}
leftGlyph={
<Icon glyph={showCode ? 'VisibilityOff' : 'Visibility'} />
}
>
{showCode ? 'Hide' : 'Show'} Code
</Button>
</div>
</div>
{showCode && sourceCode && (
<CodeWrapper>
<Code language="js">{sourceCode}</Code>
</CodeWrapper>
)}
</Card>
</div>
) : (
<>Loading...</>
)}
</>
);
};

export default LiveExampleBlock;
75 changes: 46 additions & 29 deletions components/ContentstackRichText/componentMap.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { css } from '@emotion/react';
import { useGuidelinesContext } from 'contexts/GuidelinesContext';

import Card from '@leafygreen-ui/card';
import { palette } from '@leafygreen-ui/palette';
Expand All @@ -17,6 +18,7 @@ import ContentstackChildren from './ContentstackChildren';
import ContentstackReference from './ContentstackReference';
import HeaderContent from './HeaderContent';
import { CSNode, CSNodeType, CSTextNode } from './types';
import { getCSNodeTextContent } from './utils';

type CSNodeTypeMapFunction = (
node: CSNode | CSTextNode,
Expand Down Expand Up @@ -48,23 +50,29 @@ export const nodeTypeToElementMap: {
<HeaderContent node={node} />
</H1>
),
[CSNodeType.HEADING_2]: (node, props) => (
<H2
css={
!props.isNested &&
css`
margin-bottom: ${spacing[2]}px;
[CSNodeType.HEADING_2]: (node, props) => {
const { componentName, getHeaderRef } = useGuidelinesContext();
const nodeText = getCSNodeTextContent(node);
const headerRef = getHeaderRef(`${componentName}-${nodeText}`);
return (
<H2
css={
!props.isNested &&
css`
margin-bottom: ${spacing[2]}px;

&:not(:first-child) {
margin-top: ${spacing[6]}px;
}
`
}
{...props}
>
<HeaderContent node={node} />
</H2>
),
&:not(:first-child) {
margin-top: ${spacing[6]}px;
}
`
}
ref={headerRef}
{...props}
>
<HeaderContent node={node} />
</H2>
);
},
[CSNodeType.HEADING_3]: (node, props) => (
<H3
css={
Expand All @@ -78,19 +86,26 @@ export const nodeTypeToElementMap: {
<HeaderContent node={node} />
</H3>
),
[CSNodeType.HEADING_4]: (node, props) => (
<Subtitle
css={
!props.isNested &&
css`
margin-top: ${spacing[4]}px;
`
}
{...props}
>
<HeaderContent node={node} />
</Subtitle>
),
[CSNodeType.HEADING_4]: (node, props) => {
const { getHeaderRef, componentName } = useGuidelinesContext();
const nodeText = getCSNodeTextContent(node);
const headerRef = getHeaderRef(`${componentName}-${nodeText}`);
// useEffect(() => { console.log(headerRef?.current)}, [headerRef])
return (
<Subtitle
css={
!props.isNested &&
css`
margin-top: ${spacing[4]}px;
`
}
ref={headerRef}
{...props}
>
<HeaderContent node={node} />
</Subtitle>
);
},
[CSNodeType.HEADING_5]: (node, props) => (
<Overline {...props}>
<ContentstackChildren nodeChildren={node.children} />
Expand All @@ -108,6 +123,7 @@ export const nodeTypeToElementMap: {
css`
& {
margin-top: ${spacing[2]}px;
margin-bottom: ${spacing[4]}px;
}
`
}
Expand Down Expand Up @@ -273,6 +289,7 @@ export const nodeTypeToElementMap: {
[CSNodeType.REFERENCE]: (node, props) => (
<ContentstackReference content={node} {...props} />
),
[CSNodeType.CODE]: (node, props) => <code content={node} {...props} />,
[CSNodeType.FRAGMENT]: (node, props) => (
<ContentstackChildren nodeChildren={node.children} {...props} />
),
Expand Down
8 changes: 7 additions & 1 deletion components/ContentstackRichText/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { BadgeProps } from '@leafygreen-ui/badge/dist/Badge/types';
import { ButtonProps } from '@leafygreen-ui/button';
import { CalloutProps } from '@leafygreen-ui/callout/dist/Callout/types';

type AnyNode = CSNode | CSTextNode;
export type AnyNode = CSNode | CSTextNode;

/** Contentstack is missing props in their type definitions */
export interface CSNode extends Node {
Expand All @@ -26,6 +26,7 @@ export enum CSNodeType {
DOCUMENT = 'doc',
FRAGMENT = 'fragment',
PARAGRAPH = 'p',
CODE = 'code',
ANCHOR = 'a',
REFERENCE = 'reference',
HEADING_1 = 'h1',
Expand Down Expand Up @@ -147,6 +148,10 @@ export interface TwoColumnExampleCardBlockProps {
column_2_image: CSImage;
}

export interface LiveExampleBlockProps {
story_name: string;
}

export interface BlockPropsMap {
annotated_image_block: AnnotatedImageBlockProps;
badge_block: BadgeBlockProps;
Expand All @@ -158,6 +163,7 @@ export interface BlockPropsMap {
expandable_card_block: ExpandableCardBlockProps;
horizontal_layout: HorizontalLayoutBlockProps;
example_card_block_2_column_: TwoColumnExampleCardBlockProps;
live_example_block: LiveExampleBlockProps;
}

export type ContentTypeUID = keyof BlockPropsMap;
Loading