import { knobComponents, KnobsSnippet } from '@fluentui/code-sandbox'; import { CopyToClipboard, KnobInspector, KnobProvider, LogInspector, Editor, EDITOR_BACKGROUND_COLOR, EDITOR_GUTTER_COLOR, } from '@fluentui/docs-components'; import { ComponentVariablesInput, Flex, ICSSInJSStyle, Image, Menu, Provider, Segment, ThemeInput, } from '@fluentui/react-northstar'; import * as _ from 'lodash'; import * as React from 'react'; import { RouteComponentProps, withRouter } from 'react-router-dom'; import * as copyToClipboard from 'copy-to-clipboard'; import qs from 'qs'; import { examplePathToHash, getFormattedHash, scrollToAnchor } from '../../../utils'; import { babelConfig, importResolver } from '../../Playground/renderConfig'; import ExampleContext, { ExampleContextValue } from '../../../context/ExampleContext'; import { SourceRender } from '../SourceRender'; import ComponentControls from '../ComponentControls'; import ComponentExampleTitle from './ComponentExampleTitle'; import ComponentSourceManager, { ComponentSourceManagerRenderProps } from '../ComponentSourceManager'; import VariableResolver from '../../VariableResolver/VariableResolver'; import ComponentExampleVariables from './ComponentExampleVariables'; // TODO: find replacement import { ReplyIcon, AcceptIcon, EditIcon } from '@fluentui/react-icons-northstar'; import config from '../../../config'; const ERROR_COLOR = '#D34'; export interface ComponentExampleProps extends RouteComponentProps, ComponentSourceManagerRenderProps, ExampleContextValue { error: Error | null; onError: (error: Error | null) => void; title: string; titleForAriaLabel?: string; description?: React.ReactNode; examplePath: string; resetTheme?: boolean; } interface ComponentExampleState { anchorName: string; componentVariables: ComponentVariablesInput; isActive: boolean; isActiveHash: boolean; usedVariables: Record; showCode: boolean; showRtl: boolean; showTransparent: boolean; showVariables: boolean; } const childrenStyle: ICSSInJSStyle = { paddingTop: 0, paddingBottom: '10px', }; /** * Renders a `component` and the raw `code` that produced it. * Allows toggling the the raw `code` code block. */ class ComponentExample extends React.Component { kebabExamplePath: string; static getClearedActiveState = () => ({ showCode: false, showRtl: false, showVariables: false, showTransparent: false, }); static getAnchorName = props => examplePathToHash(props.examplePath); static isActiveHash = props => { const anchorName = ComponentExample.getAnchorName(props); const formattedHash = getFormattedHash(props.location.hash); return anchorName === formattedHash; }; static getStateFromURL = props => { return qs.parse(props.location.search, { ignoreQueryPrefix: true, decoder: (raw, parse) => { const result = parse(raw); return result === 'false' ? false : result === 'true' ? true : result; }, }); }; static setStateToURL = (props, state) => { const nextQueryState = { showCode: state.showCode, showRtl: state.showRtl, showTransparent: state.showTransparent, showVariables: state.showVariables, }; const prevQueryState = ComponentExample.getStateFromURL(props); // don't trigger re-renders if the state in the query string is the same as the state // that is trying to be set if (_.isEqual(prevQueryState, nextQueryState)) { return; } const nextQueryString = qs.stringify(nextQueryState); props.history.replace({ ...props.history.location, search: `?${nextQueryString}` }); }; static getDerivedStateFromProps(props, state) { const anchorName = ComponentExample.getAnchorName(props); const isActiveHash = ComponentExample.isActiveHash(props); const isActive = !!state.showCode || !!state.showVariables; const nextHash = props.location.hash !== state.prevHash ? props.location.hash : state.prevHash; const nextState = { anchorName, isActive, isActiveHash, prevHash: nextHash, }; // deactivate examples when switching from one to the next if (!isActiveHash && state.prevHash !== nextHash) { Object.assign(nextState, ComponentExample.getClearedActiveState()); } return nextState; } componentDidUpdate( prevProps: Readonly, prevState: Readonly, snapshot?: any, ): void { if (this.state.isActiveHash) { ComponentExample.setStateToURL(this.props, this.state); } } constructor(props) { super(props); const isActiveHash = ComponentExample.isActiveHash(props); this.state = { componentVariables: {}, usedVariables: {}, showCode: isActiveHash, showRtl: false, showTransparent: false, showVariables: false, ...(isActiveHash && ComponentExample.getStateFromURL(props)), ...(/\.rtl$/.test(props.examplePath) && { showRtl: true }), }; } updateHash = () => { const { isActive } = this.state; if (isActive) this.setHashAndScroll(); }; setHashAndScroll = () => { const { anchorName } = this.state; const { history } = this.props; history.replace({ ...history.location, hash: anchorName }); scrollToAnchor(); }; handleDirectLinkClick = () => { this.setHashAndScroll(); copyToClipboard(window.location.href); }; handleShowRtlClick = (e: React.SyntheticEvent) => { e.preventDefault(); this.setState(prevState => ({ showRtl: !prevState.showRtl })); }; handleShowCodeClick = (e: React.SyntheticEvent) => { e.preventDefault(); const { showCode } = this.state; this.setState({ showCode: !showCode }, this.updateHash); }; handleShowVariablesClick = (e: React.SyntheticEvent) => { e.preventDefault(); const { showVariables } = this.state; this.setState({ showVariables: !showVariables }, this.updateHash); }; handleShowTransparentClick = (e: React.SyntheticEvent) => { e.preventDefault(); const { showTransparent } = this.state; this.setState({ showTransparent: !showTransparent }); }; resetSourceCode = () => { if (confirm('Lose your changes?')) { this.props.handleCodeReset(); } }; getKebabExamplePath = () => { if (!this.kebabExamplePath) this.kebabExamplePath = _.kebabCase(this.props.examplePath); return this.kebabExamplePath; }; getSourceFileNameHint = () => { return `Source code: ${this.props.examplePath.split('/').pop()}`; }; handleCodeApiChange = apiType => () => { this.props.handleCodeAPIChange(apiType); }; handleCodeLanguageChange = language => () => { const { handleCodeLanguageChange, wasCodeChanged } = this.props; if (wasCodeChanged) { if (confirm('Lose your changes?')) { handleCodeLanguageChange(language); } } else { handleCodeLanguageChange(language); } }; exampleMenuVariables = siteVars => ({ backgroundColorActive: 'transparent', borderColorActive: siteVars.colors.white, colorActive: siteVars.colors.white, primaryBorderColor: siteVars.colors.white, color: siteVars.colors.white, }); renderAPIsMenu = (): JSX.Element => { const { componentAPIs, currentCodeAPI } = this.props; const menuItems = _.map(componentAPIs, ({ name, supported }, type) => ({ active: currentCodeAPI === type, content: ( {name} {!supported && (not supported)} ), disabled: !supported, key: type, onClick: this.handleCodeApiChange(type), })); return ; }; renderLanguagesMenu = (): JSX.Element => { const { currentCodeLanguage } = this.props; const menuItems = [ { active: currentCodeLanguage === 'js', content: 'JavaScript', key: 'js', onClick: this.handleCodeLanguageChange('js'), }, { active: currentCodeLanguage === 'ts', content: 'TypeScript', key: 'ts', onClick: this.handleCodeLanguageChange('ts'), }, ]; return ; }; renderCodeEditorMenu = (): JSX.Element => { const { canCodeBeFormatted, currentCode, currentCodeLanguage, currentCodePath, handleCodeFormat, wasCodeChanged, } = this.props; const codeEditorStyle: ICSSInJSStyle = { position: 'relative', margin: '0 0 0 .5rem', top: '2px', border: '0', paddingTop: '.5rem', float: 'right', borderBottom: 0, }; // get component name from file path: // elements/Button/Types/ButtonButtonExample const pathParts = currentCodePath.split(__PATH_SEP__); const filename = pathParts[pathParts.length - 1]; const ghEditHref = [ `${config.repoURL}/edit/master/docs/src/examples/${currentCodePath}.tsx`, `?message=docs(${filename}): your description`, ].join(''); const menuItems = [ { icon: canCodeBeFormatted ? : , // active: !!error, content: 'Prettier', key: 'prettier', onClick: handleCodeFormat, disabled: !canCodeBeFormatted, }, { content: 'Reset', icon: , key: 'reset', onClick: this.resetSourceCode, disabled: !wasCodeChanged, }, { content: 'Copy', children: (Component, props) => ( {(active, onClick) => ( } onClick={onClick} /> )} ), }, { disabled: currentCodeLanguage !== 'ts', icon: , content: 'Edit', href: ghEditHref, rel: 'noopener noreferrer', target: '_blank', title: currentCodeLanguage !== 'ts' ? 'You can edit source only in TypeScript' : undefined, key: 'withtslanguage', }, ]; return ( ); }; renderSourceCode = () => { const { currentCode = '', handleCodeChange } = this.props; const lineCount = currentCode.match(/^/gm)!.length; return ( // match code editor background and gutter size and colors
9 ? 41 : 34}px solid ${EDITOR_GUTTER_COLOR}`, paddingBottom: '2.6rem', } as React.CSSProperties } > {this.renderAPIsMenu()} {this.renderLanguagesMenu()} {this.renderCodeEditorMenu()}
); }; handleVariableChange = (componentName: string, variableName: string, variableValue: string) => { this.setState(state => ({ componentVariables: { ...state.componentVariables, [componentName]: { ...state.componentVariables[componentName], [variableName]: variableValue, }, }, })); }; handleVariableResolve = variables => { // Remove Provider to hide it in variables delete variables['Provider']; this.setState({ usedVariables: variables }); }; render() { const { children, currentCode, currentCodeLanguage, currentCodePath, error, description, defaultExport, onError, title, titleForAriaLabel, wasCodeChanged, resetTheme, } = this.props; const { anchorName, componentVariables, usedVariables, showCode, showRtl, showTransparent, showVariables, } = this.state; const newTheme: ThemeInput = { componentVariables: { ...componentVariables, Provider: { background: showTransparent ? 'initial' : undefined }, }, }; const exampleStyles = { padding: '2rem', ...(showTransparent && { backgroundImage: 'url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAKUlEQVQoU2NkYGAwZkAD////RxdiYBwKCv///4/hGUZGkNNRAeMQUAgAtxof+nLDzyUAAAAASUVORK5CYII=")', backgroundRepeat: 'repeat', }), }; return ( {/* Ensure anchor links don't occlude card shadow effect */}
{knobs => knobs && {knobs}} {children && {children}} {showCode || wasCodeChanged ? ( ) : ( React.createElement(defaultExport) )} {showCode && (
{this.renderSourceCode()} {error && (
                    {error.toString()}
                  
)}
)} {showVariables && ( )} ); } } const ComponentExampleWithTheme = props => { const exampleProps = React.useContext(ExampleContext); // This must be under ComponentExample: // React handles setState() in hooks and classes differently: it performs strict equal check in hooks const [error, setError] = React.useState(null); return ( {codeProps => } ); }; export default withRouter(ComponentExampleWithTheme);