@workday/canvas-kit-docs
Version:
Documentation components of Canvas Kit components
247 lines (246 loc) • 11.5 kB
JavaScript
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));
};