import React, { useMemo, useRef, useState } from 'react'; import ClipboardJS from 'clipboard'; import debounce from 'lodash/debounce'; import { useIsomorphicLayoutEffect } from '@leafygreen-ui/hooks'; import ChevronDown from '@leafygreen-ui/icon/dist/ChevronDown'; import ChevronUp from '@leafygreen-ui/icon/dist/ChevronUp'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; import { CodeSkeleton } from '@leafygreen-ui/skeleton-loader'; import { useUpdatedBaseFontSize } from '@leafygreen-ui/typography'; import CodeContextProvider from '../CodeContext/CodeContext'; import CopyButton from '../CopyButton/CopyButton'; import { Syntax } from '../Syntax'; import { Language } from '../types'; import { getLgIds } from '../utils/getLgIds'; import { getCodeStyles, getCodeWrapperStyles, getCopyButtonWithoutPanelStyles, getExpandedButtonStyles, getLoadingStyles, getWrapperStyles, } from './Code.styles'; import { CodeProps, CopyButtonAppearance, DetailedElementProps, ScrollState, } from './Code.types'; import { getHorizontalScrollbarHeight, hasMultipleLines } from './utils'; /** * Code components are used to show snippets of code. */ function Code({ language: languageProp, darkMode: darkModeProp, showLineNumbers = false, lineNumberStart = 1, expandable = false, collapsedLines = 5, isLoading = false, highlightLines = [], copyButtonAppearance = CopyButtonAppearance.Hover, children = '', className, onCopy, panel, customKeywords, 'data-lgid': dataLgId, baseFontSize: baseFontSizeProp, ...rest }: CodeProps) { const scrollableElementRef = useRef(null); const [scrollState, setScrollState] = useState(ScrollState.None); const [expanded, setExpanded] = useState(!expandable); const [numOfLinesOfCode, setNumOfLinesOfCode] = useState(); const [codeHeight, setCodeHeight] = useState(0); const [collapsedCodeHeight, setCollapsedCodeHeight] = useState(0); const isMultiline = useMemo(() => hasMultipleLines(children), [children]); const { theme, darkMode } = useDarkMode(darkModeProp); const baseFontSize = useUpdatedBaseFontSize(baseFontSizeProp); const lgIds = getLgIds(dataLgId); const showPanel = !!panel; useIsomorphicLayoutEffect(() => { const scrollableElement = scrollableElementRef.current; if ( scrollableElement != null && scrollableElement.scrollWidth > scrollableElement.clientWidth ) { setScrollState(ScrollState.Right); } }, []); function setExpandableState() { if (!expandable || !scrollableElementRef.current) return; const scrollableElement = scrollableElementRef.current; const scrollbarHeight = getHorizontalScrollbarHeight(scrollableElement); const codeHeight = scrollableElement.scrollHeight + scrollbarHeight; const linesOfCode = scrollableElement.querySelectorAll('tr'); let collapsedCodeHeight = codeHeight; if (linesOfCode.length > collapsedLines) { const topOfCode = scrollableElement.getBoundingClientRect().top; const lastVisisbleLineOfCode = linesOfCode[collapsedLines - 1]; const bottomOfLastVisibleLineOfCode = lastVisisbleLineOfCode.getBoundingClientRect().bottom; collapsedCodeHeight = bottomOfLastVisibleLineOfCode - topOfCode + scrollbarHeight; } setCodeHeight(codeHeight); setCollapsedCodeHeight(collapsedCodeHeight); setNumOfLinesOfCode(linesOfCode.length); } useIsomorphicLayoutEffect(setExpandableState, [ expandable, scrollableElementRef, baseFontSize, // will cause changes in code height collapsedLines, ]); const renderedSyntaxComponent = ( {children} ); function handleScroll(e: React.UIEvent) { const { scrollWidth, clientWidth: elementWidth } = e.target as HTMLElement; const isScrollable = scrollWidth > elementWidth; if (isScrollable) { const scrollPosition = (e.target as HTMLElement).scrollLeft; const maxPosition = scrollWidth - elementWidth; if (scrollPosition > 0 && scrollPosition < maxPosition) { setScrollState(ScrollState.Both); } else if (scrollPosition > 0) { setScrollState(ScrollState.Left); } else if (scrollPosition < maxPosition) { setScrollState(ScrollState.Right); } } } function handleExpandButtonClick() { setExpanded(prev => !prev); } const debounceScroll = debounce(handleScroll, 50, { leading: true }); const onScroll: React.UIEventHandler = e => { e.persist(); debounceScroll(e); }; const showExpandButton = !!( expandable && numOfLinesOfCode && numOfLinesOfCode > collapsedLines && !isLoading ); const showCopyButtonWithoutPanel = !showPanel && copyButtonAppearance !== CopyButtonAppearance.None && ClipboardJS.isSupported() && !isLoading; return (
{!isLoading && (
)}
              className={getCodeWrapperStyles({
                theme,
                showPanel,
                expanded,
                codeHeight,
                collapsedCodeHeight,
                isMultiline,
                showExpandButton,
              })}
              onScroll={onScroll}
              ref={scrollableElementRef}
              // Adds to Tab order when content is scrollable, otherwise overflowing content is inaccessible via keyboard navigation
              // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
              tabIndex={scrollState !== ScrollState.None ? 0 : -1}
            >
              {renderedSyntaxComponent}
            
)} {isLoading && ( )} {/* This div is below the pre tag so that we can target it using the css sibiling selector when the pre tag is hovered */} {showCopyButtonWithoutPanel && ( )} {!!panel && panel} {showExpandButton && ( )}
); } Code.displayName = 'Code'; export default Code;