UNPKG

@workday/canvas-kit-docs

Version:

Documentation components of Canvas Kit components

247 lines (246 loc) • 11.5 kB
import React from 'react'; import styled from '@emotion/styled'; import { createComponent } from '@workday/canvas-kit-react/common'; import { useDialogModel, Dialog } from '@workday/canvas-kit-react/dialog'; import { colors, space, type } from '@workday/canvas-kit-react/tokens'; import { Breadcrumbs } from '@workday/canvas-kit-react/breadcrumbs'; import { MDX, MdxJSToJSX } from './MDXElements'; import { Hyperlink } from '@workday/canvas-kit-react/button'; import { docs } from './docs'; import { Value } from './Value'; /** * This context allows us to keep track if we're within a nested stack of dialog */ const NestedContext = React.createContext(false); /** * Context to help keep track of breadcrumbs and update them */ const SymbolDocBreadcrumbsContext = React.createContext(undefined); /** React context to track the current rendering context to avoid tables inside tables */ export const RenderContext = React.createContext('table'); /** * React context to track the current heading level. This is useful to relatively change heading * levels of a subtree */ export const HeadingLevelContext = React.createContext(3); /** * React context to keep track of the current indent level */ export const IndentLevelContext = React.createContext(0); /** * Utility function for getting non-breaking space for code formatting. It will print a string of * non-breaking spaces according to the level provided. These characters are safe for a React render * function. */ export function indent(level) { return [...Array(level * 2)].map(v => '\u00A0').join(''); } export function capitalize(input) { return input.charAt(0).toUpperCase() + input.slice(1); } /** * Special heading component that uses @mdx-js/react heading components, but is also aware of * relative heading levels. */ export const Heading = createComponent('h4')({ displayName: 'MDXHeading', Component({ headingOffset = 0, ...elemProps }) { const headingLevel = React.useContext(HeadingLevelContext); const as = `h${headingLevel + headingOffset}`; // Make Typescript happy return React.createElement(MDX, { as: as, ...elemProps }); }, }); export function renderTypeParameters(typeParameters) { return typeParameters && typeParameters.length ? (React.createElement(RenderContext.Provider, { value: "inline" }, React.createElement("span", null, "<"), typeParameters.map((p, index) => { return (React.createElement(React.Fragment, { key: index }, index !== 0 && React.createElement("span", { className: "token punctuation" }, ", "), React.createElement(Value, { value: p }))); }), React.createElement("span", null, ">"))) : (''); } const ButtonHyperLink = Hyperlink.as('button'); export const SymbolDialog = ({ value }) => { const [symbol, setSymbol] = React.useState(undefined); const { breadcrumbsList, updateBreadcrumbs } = React.useContext(SymbolDocBreadcrumbsContext); const nestedContext = React.useContext(NestedContext); const model = useDialogModel({ onShow() { setSymbol(docs.find(d => value.name === d.name) || undefined); }, onHide() { // Reset breadcrumbs when the dialog is closed updateBreadcrumbs([]); }, }); const handleTargetClick = (e) => { e.preventDefault(); updateBreadcrumbs(breadcrumbsList.concat(value.name)); }; const handleBreadcrumbClick = (e, index) => { e.preventDefault(); if (e.currentTarget.textContent && breadcrumbsList.length && breadcrumbsList.indexOf(e.currentTarget.textContent) > -1) { // Slice breadcrumbs list up to but not including the current item clicked updateBreadcrumbs(breadcrumbsList.slice(0, index + 1)); } }; // If we're inside a nested context, we render a Hyperlink. This button will update the dialog instead of rendering a new one. if (nestedContext) { return (React.createElement(ButtonHyperLink, { style: { border: 'none', background: 'transparent', fontSize: 'inherit', fontFamily: 'inherit', }, onClick: handleTargetClick }, value.displayName || value.name)); } return (React.createElement(NestedContext.Provider, { value: true }, React.createElement(Dialog, { model: model }, React.createElement(Dialog.Target, { as: ButtonHyperLink, className: "token symbol", onClick: handleTargetClick, style: { border: 'none', background: 'transparent', fontSize: 'inherit', fontFamily: 'inherit', }, "aria-haspopup": "true" }, value.displayName || value.name), renderTypeParameters(value.typeParameters), React.createElement(Dialog.Popper, null, React.createElement(Dialog.Card, { maxHeight: "50vh", maxWidth: "90vh", minWidth: '600px' }, React.createElement(Dialog.CloseIcon, null), React.createElement(Dialog.Heading, null, value.name, " "), breadcrumbsList.length > 1 && (React.createElement(Breadcrumbs, { "aria-label": "Breadcrumbs" }, React.createElement(Breadcrumbs.List, { paddingX: "xxs" }, breadcrumbsList.map((item, index) => { return (React.createElement(React.Fragment, null, index === breadcrumbsList.length - 1 ? (React.createElement(Breadcrumbs.CurrentItem, { key: index }, item)) : (React.createElement(Breadcrumbs.Item, { key: index }, React.createElement(Breadcrumbs.Link, { onClick: e => handleBreadcrumbClick(e, index), href: '#' }, item))))); })))), React.createElement(Dialog.Body, null, React.createElement(RenderContext.Provider, { value: "table" }, React.createElement(IndentLevelContext.Provider, { value: 0 }, symbol ? (React.createElement(SymbolDoc, { name: breadcrumbsList.length >= 1 ? breadcrumbsList[breadcrumbsList.length - 1] : value.name, headingStart: 3, hideHeading: true })) : (React.createElement(React.Fragment, null, React.createElement("p", null, "Basic type information:"), React.createElement("pre", null, React.createElement("code", null, value.name)))))))))))); }; function createColor(color) { return { color: colors[color], }; } const StyledSymbolDoc = styled('div')({ marginBottom: space.m, 'button[data-symbol]': { border: 'none', background: 'transparent', fontSize: 'inherit', fontFamily: 'inherit', }, code: { fontSize: type.properties.fontSizes[14], lineHeight: 1.5, fontFamily: type.properties.fontFamilies.monospace, whiteSpace: 'nowrap', '.token': { '&.string': createColor('juicyPear600'), '&.symbol': createColor('grapeSoda600'), '&.property': createColor('berrySmoothie600'), '&.primitive': createColor('pomegranate600'), '&.number': createColor('pomegranate600'), '&.boolean': createColor('pomegranate600'), '&.variable': createColor('pomegranate600'), '&.keyword': createColor('pomegranate600'), '&.punctuation': createColor('blackPepper600'), '&.operator': createColor('blackPepper600'), '&.bold': { fontWeight: 700 }, }, }, }); function getSymbolDocChildren(doc, meta, name) { if (!doc) { // We're within a symbol doc context, but there's no information, so we still want to render the basic type information return (React.createElement(React.Fragment, null, React.createElement("p", null, "Basic type information:"), React.createElement("pre", null, React.createElement("code", null, name)))); } if (doc && doc.type) { return React.createElement(Value, { value: doc.type, doc: doc, meta: meta }); } return React.createElement("div", null, "Not found"); } function findDoc({ name, fileName }) { const doc = (docs || []).find(d => { return d.name === name && (fileName ? d.fileName.includes(fileName) : true); }); if ((doc === null || doc === void 0 ? void 0 : doc.type.kind) === 'alias') { return findDoc({ name: doc.type.name }); } return doc; } export const useDoc = (criteria) => { // Listen to criteria.name and criteria.fileName so that we can re-fetch docs in the dialog const doc = React.useMemo(() => findDoc(criteria), [criteria.name, criteria.fileName || '']); return doc; }; /** * Renders the `<Value>` of a exported symbol without any wrapper. This should be used when nesting * inside a `<SymbolDoc>` component. */ export const SymbolValue = (props) => { const doc = useDoc(props); return getSymbolDocChildren(doc); }; /** * Renders just the description of an exported symbol. */ export const SymbolDescription = (props) => { const doc = useDoc(props); return doc ? React.createElement(MdxJSToJSX, null, doc.description) : null; }; /** * Renders an exported symbol as a doc object. This will render JSDoc tags, description and setup * rendering contexts for headers. */ export const SymbolDoc = ({ name, fileName, headingStart = 3, hideDescription = false, hideHeading = false, descriptionOverride, meta, ...elemProps }) => { const doc = useDoc({ name, fileName }); const symbolDocBreadcrumb = React.useContext(SymbolDocBreadcrumbsContext); const children = getSymbolDocChildren(doc, meta, name); const requiresCodeWrapper = [ 'symbol', 'generic', 'array', 'indexedAccess', 'qualifiedName', 'parenthesis', 'typeParameter', 'keyof', 'type', 'conditional', 'indexSignature', 'tuple', 'external', 'number', 'string', 'boolean', 'primitive', 'unknown', 'union', 'intersection', 'function', 'callExpression', ].includes((doc === null || doc === void 0 ? void 0 : doc.type.kind) || 'notFound'); const contents = requiresCodeWrapper ? (React.createElement("code", { style: { display: 'block' } }, children)) : (children); const symbolDocContents = (React.createElement(StyledSymbolDoc, { ...elemProps }, React.createElement(HeadingLevelContext.Provider, { value: headingStart }, !hideHeading && React.createElement(Heading, { id: `${name.toLowerCase()}-api` }, name), !hideDescription && doc && (React.createElement(MdxJSToJSX, null, descriptionOverride || doc.description)), contents))); return symbolDocBreadcrumb === undefined ? (React.createElement(SymbolDocWrapper, null, symbolDocContents)) : (symbolDocContents); }; const SymbolDocWrapper = ({ children }) => { const [breadcrumbsList, updateBreadcrumbs] = React.useState([]); return (React.createElement(SymbolDocBreadcrumbsContext.Provider, { value: { breadcrumbsList, updateBreadcrumbs } }, children)); };