From 19bbc64f279df6c1a58c5bb26e0eaf23ea83726c Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Mon, 15 Nov 2021 12:48:48 -0800 Subject: [PATCH 01/27] DRY out code used by both inline code & code blocks to utils + write unit tests + organize utils with comment blocks --- .../code/__snapshots__/utils.test.tsx.snap | 35 ++-- src/components/code/_code_block.tsx | 33 ++-- src/components/code/utils.test.tsx | 176 ++++++++++++------ src/components/code/utils.tsx | 85 ++++++--- 4 files changed, 213 insertions(+), 116 deletions(-) diff --git a/src/components/code/__snapshots__/utils.test.tsx.snap b/src/components/code/__snapshots__/utils.test.tsx.snap index ca820521f73..839b65cffe3 100644 --- a/src/components/code/__snapshots__/utils.test.tsx.snap +++ b/src/components/code/__snapshots__/utils.test.tsx.snap @@ -1,6 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`highlightByLine with line numbers renders two elements per line: .euiCodeBlock__lineNumber and .euiCodeBlock__lineText 1`] = ` +exports[`shared utils nodeToHtml recursively converts refactor nodes to React JSX 1`] = ` +
+ + Hello world + +
+`; + +exports[`line utils highlightByLine with line numbers renders two elements per line: .euiCodeBlock__lineNumber and .euiCodeBlock__lineText 1`] = ` Array [ Object { "children": Array [ @@ -93,7 +104,7 @@ Array [ "lineEnd": 2, "lineStart": 2, "type": "text", - "value": " ", + "value": " ", }, Object { "children": Array [ @@ -233,7 +244,7 @@ Array [ "lineEnd": 3, "lineStart": 3, "type": "text", - "value": "", + "value": " ", }, Object { "children": Array [ @@ -280,7 +291,7 @@ Array [ ] `; -exports[`highlightByLine with line numbers with a custom starting number adds the starting lineNumber to each node 1`] = ` +exports[`line utils highlightByLine with line numbers with a custom starting number adds the starting lineNumber to each node 1`] = ` Array [ Object { "children": Array [ @@ -373,7 +384,7 @@ Array [ "lineEnd": 11, "lineStart": 11, "type": "text", - "value": " ", + "value": " ", }, Object { "children": Array [ @@ -513,7 +524,7 @@ Array [ "lineEnd": 12, "lineStart": 12, "type": "text", - "value": "", + "value": " ", }, Object { "children": Array [ @@ -560,7 +571,7 @@ Array [ ] `; -exports[`highlightByLine with line numbers with highlighted lines adds a class to the specified lines 1`] = ` +exports[`line utils highlightByLine with line numbers with highlighted lines adds a class to the specified lines 1`] = ` Array [ Object { "children": Array [ @@ -653,7 +664,7 @@ Array [ "lineEnd": 2, "lineStart": 2, "type": "text", - "value": " ", + "value": " ", }, Object { "children": Array [ @@ -793,7 +804,7 @@ Array [ "lineEnd": 3, "lineStart": 3, "type": "text", - "value": "", + "value": " ", }, Object { "children": Array [ @@ -840,7 +851,7 @@ Array [ ] `; -exports[`highlightByLine without line numbers renders a single .euiCodeBlock__line element per line 1`] = ` +exports[`line utils highlightByLine without line numbers renders a single .euiCodeBlock__line element per line 1`] = ` Array [ Object { "children": Array [ @@ -886,7 +897,7 @@ Array [ "lineEnd": 2, "lineStart": 2, "type": "text", - "value": " ", + "value": " ", }, Object { "children": Array [ @@ -996,7 +1007,7 @@ Array [ "lineEnd": 3, "lineStart": 3, "type": "text", - "value": "", + "value": " ", }, Object { "children": Array [ diff --git a/src/components/code/_code_block.tsx b/src/components/code/_code_block.tsx index 672ab627062..2290037ea80 100644 --- a/src/components/code/_code_block.tsx +++ b/src/components/code/_code_block.tsx @@ -11,8 +11,6 @@ import React, { HTMLAttributes, FunctionComponent, KeyboardEvent, - ReactElement, - ReactNode, memo, forwardRef, useEffect, @@ -20,7 +18,7 @@ import React, { useState, } from 'react'; import classNames from 'classnames'; -import { highlight, RefractorNode, listLanguages } from 'refractor'; +import { highlight, RefractorNode } from 'refractor'; import { FixedSizeList, ListChildComponentProps } from 'react-window'; import { keys, useCombinedRefs } from '../../services'; import { EuiAutoSizer } from '../auto_sizer'; @@ -33,7 +31,13 @@ import { useInnerText } from '../inner_text'; import { useMutationObserver } from '../observer/mutation_observer'; import { useResizeObserver } from '../observer/resize_observer'; import { EuiOverlayMask } from '../overlay_mask'; -import { highlightByLine, nodeToHtml } from './utils'; +import { + DEFAULT_LANGUAGE, + checkSupportedLanguage, + getHtmlContent, + nodeToHtml, + highlightByLine, +} from './utils'; // eslint-disable-next-line local/forward-ref const virtualizedOuterElement = ({ @@ -62,9 +66,6 @@ const ListRow = ({ data, index, style }: ListChildComponentProps) => { return nodeToHtml(row, index, data, 0); }; -const SUPPORTED_LANGUAGES = listLanguages(); -const DEFAULT_LANGUAGE = 'text'; - // Based on observed line height for non-virtualized code blocks const fontSizeToRowHeightMap = { s: 18, @@ -176,11 +177,9 @@ export const EuiCodeBlockImpl: FunctionComponent = ({ lineNumbers = false, ...rest }) => { - const language: string = useMemo( - () => - SUPPORTED_LANGUAGES.includes(_language) ? _language : DEFAULT_LANGUAGE, - [_language] - ); + const language = useMemo(() => checkSupportedLanguage(_language), [ + _language, + ]); const [isFullScreen, setIsFullScreen] = useState(false); const [wrapperRef, setWrapperRef] = useState(null); const [innerTextRef, _innerText] = useInnerText(''); @@ -218,12 +217,10 @@ export const EuiCodeBlockImpl: FunctionComponent = ({ ]); // Used by `pre` when `isVirtualized=false` or `children` is not parsable (`isVirtualized=false`) - const content: ReactElement[] | ReactNode = useMemo(() => { - if (!Array.isArray(data) || data.length < 1) { - return children; - } - return data.map(nodeToHtml); - }, [data, children]); + const content = useMemo(() => getHtmlContent(data, children), [ + data, + children, + ]); const doesOverflow = () => { if (!wrapperRef) return; diff --git a/src/components/code/utils.test.tsx b/src/components/code/utils.test.tsx index fd092c03d1b..823427455ce 100644 --- a/src/components/code/utils.test.tsx +++ b/src/components/code/utils.test.tsx @@ -6,86 +6,150 @@ * Side Public License, v 1. */ -import { highlightByLine, parseLineRanges } from './utils'; +import React from 'react'; +import { shallow } from 'enzyme'; -const jsonCode = `{ - "id": "1", -}`; +import { + checkSupportedLanguage, + getHtmlContent, + isAstElement, + nodeToHtml, + highlightByLine, + parseLineRanges, +} from './utils'; -describe('highlightByLine', () => { - describe('without line numbers', () => { - it('renders a single .euiCodeBlock__line element per line', () => { - const highlight = highlightByLine(jsonCode, 'json', { - show: false, - start: 1, - }); - expect(highlight).toMatchSnapshot(); - // @ts-expect-error RefractorNode - expect(highlight[0].children[0].children.length).toBe(1); +describe('shared utils', () => { + describe('checkSupportedLanguage', () => { + it('returns the language if included in the list of supported languages', () => { + expect(checkSupportedLanguage('html')).toEqual('html'); + }); + + it('otherwise, returns text language as a default', () => { + expect(checkSupportedLanguage('fake language')).toEqual('text'); }); }); - describe('with line numbers', () => { - it('renders two elements per line: .euiCodeBlock__lineNumber and .euiCodeBlock__lineText', () => { - const highlight = highlightByLine(jsonCode, 'json', { - show: true, - start: 1, - }); - expect(highlight).toMatchSnapshot(); - // @ts-expect-error RefractorNode - expect(highlight[0].children.length).toBe(2); + describe('getHtmlContent', () => { + it('returns the passed children as-is if data is not an array', () => { + // @ts-expect-error we're passing a type we don't expect to get in prod for the sake of the test + expect(getHtmlContent(undefined, 'children')).toEqual('children'); }); - describe('with a custom starting number', () => { - it('adds the starting lineNumber to each node', () => { + it('returns the passed children as-is if data is empty', () => { + expect(getHtmlContent([], 'children')).toEqual('children'); + }); + }); + + describe('isAstElement', () => { + it('checks if a passed node type is `element`', () => { + expect(isAstElement({ type: 'element' } as any)).toEqual(true); + expect(isAstElement({ type: 'text' } as any)).toEqual(false); + }); + }); + + describe('nodeToHtml', () => { + it('recursively converts refactor nodes to React JSX', () => { + const output = nodeToHtml( + { + type: 'element', + tagName: 'span', + children: [ + { + type: 'text', + value: 'Hello world', + }, + ], + properties: { className: ['hello-world'] }, + }, + 0, + [] + ); + const component = shallow(
{output}
); + expect(component).toMatchSnapshot(); + }); + }); +}); + +describe('line utils', () => { + const jsonCode = `{ + "id": "1", + }`; + + describe('highlightByLine', () => { + describe('without line numbers', () => { + it('renders a single .euiCodeBlock__line element per line', () => { const highlight = highlightByLine(jsonCode, 'json', { - show: true, - start: 10, + show: false, + start: 1, }); expect(highlight).toMatchSnapshot(); - expect( - // @ts-expect-error RefractorNode - highlight[0].children[0].properties['data-line-number'] - ).toBe(10); + // @ts-expect-error RefractorNode + expect(highlight[0].children[0].children.length).toBe(1); }); }); - describe('with highlighted lines', () => { - it('adds a class to the specified lines', () => { + describe('with line numbers', () => { + it('renders two elements per line: .euiCodeBlock__lineNumber and .euiCodeBlock__lineText', () => { const highlight = highlightByLine(jsonCode, 'json', { show: true, start: 1, - highlight: '1-2', }); expect(highlight).toMatchSnapshot(); - expect( - // @ts-expect-error RefractorNode - highlight[0].properties.className[0].includes( - 'euiCodeBlock__line--isHighlighted' - ) - ).toBe(true); + // @ts-expect-error RefractorNode + expect(highlight[0].children.length).toBe(2); + }); + + describe('with a custom starting number', () => { + it('adds the starting lineNumber to each node', () => { + const highlight = highlightByLine(jsonCode, 'json', { + show: true, + start: 10, + }); + expect(highlight).toMatchSnapshot(); + expect( + // @ts-expect-error RefractorNode + highlight[0].children[0].properties['data-line-number'] + ).toBe(10); + }); + }); + + describe('with highlighted lines', () => { + it('adds a class to the specified lines', () => { + const highlight = highlightByLine(jsonCode, 'json', { + show: true, + start: 1, + highlight: '1-2', + }); + expect(highlight).toMatchSnapshot(); + expect( + // @ts-expect-error RefractorNode + highlight[0].properties.className[0].includes( + 'euiCodeBlock__line--isHighlighted' + ) + ).toBe(true); + }); }); }); }); -}); -describe('parseLineRanges', () => { - describe('given a comma-separated string of numbers', () => { - it('outputs an array of numbers', () => { - const array = parseLineRanges('1, 3, 5, 9'); - expect(array).toEqual([1, 3, 5, 9]); + describe('parseLineRanges', () => { + describe('given a comma-separated string of numbers', () => { + it('outputs an array of numbers', () => { + const array = parseLineRanges('1, 3, 5, 9'); + expect(array).toEqual([1, 3, 5, 9]); + }); }); - }); - describe('given a comma-separated string of ranges', () => { - it('outputs an array of numbers', () => { - const array = parseLineRanges('1-5'); - expect(array).toEqual([1, 2, 3, 4, 5]); + describe('given a comma-separated string of ranges', () => { + it('outputs an array of numbers', () => { + const array = parseLineRanges('1-5'); + expect(array).toEqual([1, 2, 3, 4, 5]); + }); }); - }); - describe('given a comma-separated string of numbers and ranges', () => { - it('outputs an array of numbers', () => { - const array = parseLineRanges('1, 3-10, 15'); - expect(array).toEqual([1, 3, 4, 5, 6, 7, 8, 9, 10, 15]); + describe('given a comma-separated string of numbers and ranges', () => { + it('outputs an array of numbers', () => { + const array = parseLineRanges('1, 3-10, 15'); + expect(array).toEqual([1, 3, 4, 5, 6, 7, 8, 9, 10, 15]); + }); }); }); }); diff --git a/src/components/code/utils.tsx b/src/components/code/utils.tsx index 2eff804e557..5ae2ea0c364 100644 --- a/src/components/code/utils.tsx +++ b/src/components/code/utils.tsx @@ -6,10 +6,63 @@ * Side Public License, v 1. */ -import React, { createElement, ReactElement } from 'react'; -import { highlight, AST, RefractorNode } from 'refractor'; +import React, { createElement, ReactElement, ReactNode } from 'react'; +import { listLanguages, highlight, AST, RefractorNode } from 'refractor'; import classNames from 'classnames'; +/** + * Utils shared between EuiCode and EuiCodeBlock + */ + +export const SUPPORTED_LANGUAGES = listLanguages(); +export const DEFAULT_LANGUAGE = 'text'; + +export const checkSupportedLanguage = (language: string): string => { + return SUPPORTED_LANGUAGES.includes(language) ? language : DEFAULT_LANGUAGE; +}; + +export const getHtmlContent = ( + data: RefractorNode[], + children: ReactNode +): ReactElement[] | ReactNode => { + if (!Array.isArray(data) || data.length < 1) { + return children; + } + return data.map(nodeToHtml); +}; + +export const isAstElement = (node: RefractorNode): node is AST.Element => + node.hasOwnProperty('type') && node.type === 'element'; + +export const nodeToHtml = ( + node: RefractorNode, + idx: number, + nodes: RefractorNode[], + depth: number = 0 +): ReactElement => { + const key = `node-${depth}-${idx}`; + + if (isAstElement(node)) { + const { properties, tagName, children } = node; + + return createElement( + tagName, + { + ...properties, + key, + className: classNames(properties.className), + }, + children && children.map((el, i) => nodeToHtml(el, i, nodes, depth + 1)) + ); + } + + return {node.value}; +}; + +/** + * Line utils specific to EuiCodeBlock + */ + type ExtendedRefractorNode = RefractorNode & { lineStart?: number; lineEnd?: number; @@ -25,9 +78,6 @@ interface LineNumbersConfig { const CHAR_SIZE = 8; const $euiSizeS = 8; -const isAstElement = (node: RefractorNode): node is AST.Element => - node.hasOwnProperty('type') && node.type === 'element'; - // Creates an array of numbers from comma-separeated // string of numbers or number ranges using `-` // (e.g., "1, 3-10, 15") @@ -162,31 +212,6 @@ function wrapLines( return wrapped; } -export const nodeToHtml = ( - node: RefractorNode, - idx: number, - nodes: RefractorNode[], - depth: number = 0 -): ReactElement => { - const key = `node-${depth}-${idx}`; - - if (isAstElement(node)) { - const { properties, tagName, children } = node; - - return createElement( - tagName, - { - ...properties, - key, - className: classNames(properties.className), - }, - children && children.map((el, i) => nodeToHtml(el, i, nodes, depth + 1)) - ); - } - - return {node.value}; -}; - export const highlightByLine = ( children: string, language: string, From 670930109bdfdb8c81c8ca41d2ca71cf93653be6 Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Mon, 15 Nov 2021 13:02:33 -0800 Subject: [PATCH 02/27] DRY out shared props between inline code & code blocks + fix external link + remove prop already listed in CommonProps --- src/components/code/_code_block.tsx | 13 +++---------- src/components/code/utils.tsx | 10 ++++++++++ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/components/code/_code_block.tsx b/src/components/code/_code_block.tsx index 2290037ea80..43ec51f279e 100644 --- a/src/components/code/_code_block.tsx +++ b/src/components/code/_code_block.tsx @@ -23,7 +23,7 @@ import { FixedSizeList, ListChildComponentProps } from 'react-window'; import { keys, useCombinedRefs } from '../../services'; import { EuiAutoSizer } from '../auto_sizer'; import { EuiButtonIcon } from '../button'; -import { keysOf, CommonProps, ExclusiveUnion } from '../common'; +import { keysOf, ExclusiveUnion } from '../common'; import { EuiCopy } from '../copy'; import { EuiFocusTrap } from '../focus_trap'; import { EuiI18n } from '../i18n'; @@ -32,6 +32,7 @@ import { useMutationObserver } from '../observer/mutation_observer'; import { useResizeObserver } from '../observer/resize_observer'; import { EuiOverlayMask } from '../overlay_mask'; import { + EuiCodeSharedProps, DEFAULT_LANGUAGE, checkSupportedLanguage, getHtmlContent, @@ -120,8 +121,7 @@ interface LineNumbersConfig { highlight?: string; } -export type EuiCodeBlockImplProps = CommonProps & { - className?: string; +export type EuiCodeBlockImplProps = EuiCodeSharedProps & { fontSize?: FontSize; /** @@ -134,14 +134,7 @@ export type EuiCodeBlockImplProps = CommonProps & { */ isCopyable?: boolean; - /** - * Sets the syntax highlighting for a specific language - * @see https://prismjs.com/#supported-languages - * for options - */ - language?: string; paddingSize?: PaddingSize; - transparentBackground?: boolean; /** * Specify how `white-space` inside the element is handled. diff --git a/src/components/code/utils.tsx b/src/components/code/utils.tsx index 5ae2ea0c364..5c27ce1b113 100644 --- a/src/components/code/utils.tsx +++ b/src/components/code/utils.tsx @@ -9,11 +9,21 @@ import React, { createElement, ReactElement, ReactNode } from 'react'; import { listLanguages, highlight, AST, RefractorNode } from 'refractor'; import classNames from 'classnames'; +import { CommonProps } from '../common'; /** * Utils shared between EuiCode and EuiCodeBlock */ +export type EuiCodeSharedProps = CommonProps & { + /** + * Sets the syntax highlighting for a specific language + * @see [https://prismjs.com/#supported-languages](https://prismjs.com/#supported-languages) for options + */ + language?: string; + transparentBackground?: boolean; +}; + export const SUPPORTED_LANGUAGES = listLanguages(); export const DEFAULT_LANGUAGE = 'text'; From ed3ad1c911b411c616ad636ad7ccd3c8540cf440 Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Mon, 15 Nov 2021 13:05:19 -0800 Subject: [PATCH 03/27] Refactor EuiCode to not use EuiCodeBlockImpl + remove prismjs class and language-* class - they didn't seem to be doing anything :shrug: + remove language class - it also wasn't doing anything, and maybe belongs better as a data attribute + add unit tests for language and background props --- .../code/__snapshots__/code.test.tsx.snap | 90 ++++++++++++++++++- src/components/code/code.test.tsx | 22 ++++- src/components/code/code.tsx | 59 ++++++++++-- 3 files changed, 160 insertions(+), 11 deletions(-) diff --git a/src/components/code/__snapshots__/code.test.tsx.snap b/src/components/code/__snapshots__/code.test.tsx.snap index fd3673fa2e9..8c19a4aec7e 100644 --- a/src/components/code/__snapshots__/code.test.tsx.snap +++ b/src/components/code/__snapshots__/code.test.tsx.snap @@ -2,11 +2,97 @@ exports[`EuiCode renders a code snippet 1`] = ` + var some = 'code'; +console.log(some); + + +`; + +exports[`EuiCode renders languages 1`] = ` + + + + var + + some + + = + + + + 'code' + + + ; + + + + + console + + + . + + + log + + + ( + + some + + ) + + + ; + + + +`; + +exports[`EuiCode renders transparent backgrounds 1`] = ` + + var some = 'code'; diff --git a/src/components/code/code.test.tsx b/src/components/code/code.test.tsx index 14340c6c6d1..91a67df5514 100644 --- a/src/components/code/code.test.tsx +++ b/src/components/code/code.test.tsx @@ -16,9 +16,29 @@ const code = `var some = 'code'; console.log(some);`; describe('EuiCode', () => { - test('renders a code snippet', () => { + it('renders a code snippet', () => { const component = render({code}); expect(component).toMatchSnapshot(); }); + + it('renders transparent backgrounds', () => { + const component = render( + + {code} + + ); + + expect(component).toMatchSnapshot(); + }); + + it('renders languages', () => { + const component = render( + + {code} + + ); + + expect(component).toMatchSnapshot(); + }); }); diff --git a/src/components/code/code.tsx b/src/components/code/code.tsx index c139120e5b3..7f1827735c8 100644 --- a/src/components/code/code.tsx +++ b/src/components/code/code.tsx @@ -6,16 +6,59 @@ * Side Public License, v 1. */ -import { CommonProps } from '../common'; +import React, { useMemo, FunctionComponent, HTMLAttributes } from 'react'; +import { highlight, RefractorNode } from 'refractor'; +import classNames from 'classnames'; +import { + EuiCodeSharedProps, + DEFAULT_LANGUAGE, + checkSupportedLanguage, + getHtmlContent, +} from './utils'; -import React, { FunctionComponent, HTMLAttributes } from 'react'; +export type EuiCodeProps = EuiCodeSharedProps & HTMLAttributes; -import { EuiCodeBlockImpl, EuiCodeBlockImplProps } from './_code_block'; +export const EuiCode: FunctionComponent = ({ + transparentBackground = false, + language: _language = DEFAULT_LANGUAGE, + children, + className, + ...rest +}) => { + const language = useMemo(() => checkSupportedLanguage(_language), [ + _language, + ]); -export type EuiCodeProps = CommonProps & - Pick & - HTMLAttributes; + const data: RefractorNode[] = useMemo(() => { + if (typeof children !== 'string') { + return []; + } + return highlight(children, language); + }, [children, language]); -export const EuiCode: FunctionComponent = ({ ...rest }) => { - return ; + const content = useMemo(() => getHtmlContent(data, children), [ + data, + children, + ]); + + const classes = classNames( + 'euiCodeBlock', + 'euiCodeBlock--inline', + { + 'euiCodeBlock--transparentBackground': transparentBackground, + }, + className + ); + + return ( + + + {content} + + + ); }; From f59f473f021ebafc2e1379e0148e6ef03a89946b Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Mon, 15 Nov 2021 13:40:35 -0800 Subject: [PATCH 04/27] Refactor EuiCodeBlockImpl to EuiCodeBlock + removing any `inline` conditional logic + cleaning up props type/order --- src/components/code/_code_block.tsx | 467 ---------------------------- src/components/code/code_block.tsx | 441 +++++++++++++++++++++++++- 2 files changed, 434 insertions(+), 474 deletions(-) delete mode 100644 src/components/code/_code_block.tsx diff --git a/src/components/code/_code_block.tsx b/src/components/code/_code_block.tsx deleted file mode 100644 index 43ec51f279e..00000000000 --- a/src/components/code/_code_block.tsx +++ /dev/null @@ -1,467 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { - CSSProperties, - HTMLAttributes, - FunctionComponent, - KeyboardEvent, - memo, - forwardRef, - useEffect, - useMemo, - useState, -} from 'react'; -import classNames from 'classnames'; -import { highlight, RefractorNode } from 'refractor'; -import { FixedSizeList, ListChildComponentProps } from 'react-window'; -import { keys, useCombinedRefs } from '../../services'; -import { EuiAutoSizer } from '../auto_sizer'; -import { EuiButtonIcon } from '../button'; -import { keysOf, ExclusiveUnion } from '../common'; -import { EuiCopy } from '../copy'; -import { EuiFocusTrap } from '../focus_trap'; -import { EuiI18n } from '../i18n'; -import { useInnerText } from '../inner_text'; -import { useMutationObserver } from '../observer/mutation_observer'; -import { useResizeObserver } from '../observer/resize_observer'; -import { EuiOverlayMask } from '../overlay_mask'; -import { - EuiCodeSharedProps, - DEFAULT_LANGUAGE, - checkSupportedLanguage, - getHtmlContent, - nodeToHtml, - highlightByLine, -} from './utils'; - -// eslint-disable-next-line local/forward-ref -const virtualizedOuterElement = ({ - className, -}: HTMLAttributes) => - memo( - forwardRef((props, ref) => ( -
-    ))
-  );
-
-// eslint-disable-next-line local/forward-ref
-const virtualizedInnerElement = ({
-  className,
-  onKeyDown,
-}: HTMLAttributes) =>
-  memo(
-    forwardRef((props, ref) => (
-      
-    ))
-  );
-
-const ListRow = ({ data, index, style }: ListChildComponentProps) => {
-  const row = data[index];
-  row.properties.style = style;
-  return nodeToHtml(row, index, data, 0);
-};
-
-// Based on observed line height for non-virtualized code blocks
-const fontSizeToRowHeightMap = {
-  s: 18,
-  m: 21,
-  l: 21,
-};
-
-const fontSizeToClassNameMap = {
-  s: 'euiCodeBlock--fontSmall',
-  m: 'euiCodeBlock--fontMedium',
-  l: 'euiCodeBlock--fontLarge',
-};
-
-type PaddingSize = 'none' | 's' | 'm' | 'l';
-type FontSize = 's' | 'm' | 'l';
-
-export const FONT_SIZES = keysOf(fontSizeToClassNameMap);
-
-const paddingSizeToClassNameMap: { [paddingSize in PaddingSize]: string } = {
-  none: '',
-  s: 'euiCodeBlock--paddingSmall',
-  m: 'euiCodeBlock--paddingMedium',
-  l: 'euiCodeBlock--paddingLarge',
-};
-
-export const PADDING_SIZES = keysOf(paddingSizeToClassNameMap);
-
-// overflowHeight is required when using virtualization
-type VirtualizedOptionProps = ExclusiveUnion<
-  {
-    /**
-     * Renders code block lines virtually.
-     * Useful for improving load times of large code blocks.
-     * `overflowHeight` is required when using this configuration.
-     */
-    isVirtualized: true;
-    /**
-     * Sets the maximum container height.
-     * Accepts a pixel value (`300`) or a percentage (`'100%'`)
-     * Ensure the container has calcuable height when using a percentage
-     */
-    overflowHeight: number | string;
-  },
-  {
-    isVirtualized?: boolean;
-    overflowHeight?: number | string;
-  }
->;
-
-interface LineNumbersConfig {
-  start?: number;
-  highlight?: string;
-}
-
-export type EuiCodeBlockImplProps = EuiCodeSharedProps & {
-  fontSize?: FontSize;
-
-  /**
-   * Displays the passed code in an inline format. Also removes any margins set.
-   */
-  inline?: boolean;
-
-  /**
-   * Displays an icon button to copy the code snippet to the clipboard.
-   */
-  isCopyable?: boolean;
-
-  paddingSize?: PaddingSize;
-
-  /**
-   * Specify how `white-space` inside the element is handled.
-   * `pre` respects line breaks/white space but doesn't force them to wrap the line
-   * `pre-wrap` respects line breaks/white space but does force them to wrap the line when necessary.
-   */
-  whiteSpace?: 'pre' | 'pre-wrap';
-
-  /**
-   * Displays line numbers.
-   * Optionally accepts a configuration object for setting the starting number and visual highlighting ranges:
-   * `{ start: 100, highlight: '1, 5-10, 20-30, 40' }`
-   */
-  lineNumbers?: boolean | LineNumbersConfig;
-} & VirtualizedOptionProps;
-
-/**
- * This is the base component extended by EuiCode and EuiCodeBlock.
- * These components share the same propTypes definition with EuiCodeBlockImpl.
- */
-export const EuiCodeBlockImpl: FunctionComponent = ({
-  transparentBackground = false,
-  paddingSize = 'l',
-  fontSize = 's',
-  isCopyable = false,
-  whiteSpace = 'pre-wrap',
-  language: _language = DEFAULT_LANGUAGE,
-  inline,
-  children,
-  className,
-  overflowHeight,
-  isVirtualized: _isVirtualized,
-  lineNumbers = false,
-  ...rest
-}) => {
-  const language = useMemo(() => checkSupportedLanguage(_language), [
-    _language,
-  ]);
-  const [isFullScreen, setIsFullScreen] = useState(false);
-  const [wrapperRef, setWrapperRef] = useState(null);
-  const [innerTextRef, _innerText] = useInnerText('');
-  const innerText = useMemo(
-    () => _innerText?.replace(/[\r\n?]{2}|\n\n/g, '\n'),
-    [_innerText]
-  );
-  const [tabIndex, setTabIndex] = useState<-1 | 0>(-1);
-  const combinedRef = useCombinedRefs([
-    innerTextRef,
-    setWrapperRef,
-  ]);
-  const { width, height } = useResizeObserver(wrapperRef);
-  const rowHeight = useMemo(() => fontSizeToRowHeightMap[fontSize], [fontSize]);
-  const lineNumbersConfig = useMemo(() => {
-    const config = typeof lineNumbers === 'object' ? lineNumbers : {};
-    return lineNumbers
-      ? { start: 1, show: true, ...config }
-      : { start: 1, show: false };
-  }, [lineNumbers]);
-
-  // Used by `FixedSizeList` when `isVirtualized=true` or `children` is parsable (`isVirtualized=true`)
-  const data: RefractorNode[] = useMemo(() => {
-    if (typeof children !== 'string') {
-      return [];
-    }
-    return inline
-      ? highlight(children, language)
-      : highlightByLine(children, language, lineNumbersConfig);
-  }, [children, language, inline, lineNumbersConfig]);
-
-  const isVirtualized = useMemo(() => _isVirtualized && Array.isArray(data), [
-    _isVirtualized,
-    data,
-  ]);
-
-  // Used by `pre` when `isVirtualized=false` or `children` is not parsable (`isVirtualized=false`)
-  const content = useMemo(() => getHtmlContent(data, children), [
-    data,
-    children,
-  ]);
-
-  const doesOverflow = () => {
-    if (!wrapperRef) return;
-
-    const { clientWidth, clientHeight, scrollWidth, scrollHeight } = wrapperRef;
-    const doesOverflow =
-      scrollHeight > clientHeight || scrollWidth > clientWidth;
-
-    setTabIndex(doesOverflow ? 0 : -1);
-  };
-
-  useMutationObserver(wrapperRef, doesOverflow, {
-    subtree: true,
-    childList: true,
-  });
-
-  useEffect(doesOverflow, [width, height, wrapperRef]);
-
-  const onKeyDown = (event: KeyboardEvent) => {
-    if (event.key === keys.ESCAPE) {
-      event.preventDefault();
-      event.stopPropagation();
-      closeFullScreen();
-    }
-  };
-
-  const toggleFullScreen = () => {
-    setIsFullScreen(!isFullScreen);
-  };
-
-  const closeFullScreen = () => {
-    setIsFullScreen(false);
-  };
-
-  const classes = classNames(
-    'euiCodeBlock',
-    fontSizeToClassNameMap[fontSize],
-    paddingSizeToClassNameMap[paddingSize],
-    {
-      'euiCodeBlock--transparentBackground': transparentBackground,
-      'euiCodeBlock--inline': inline,
-      'euiCodeBlock--hasControl': isCopyable || overflowHeight,
-      'euiCodeBlock--hasBothControls': isCopyable && overflowHeight,
-      'euiCodeBlock--hasLineNumbers': lineNumbersConfig.show,
-    },
-    {
-      prismjs: !className?.includes('prismjs'),
-      [`language-${language || 'none'}`]: !className?.includes('language'),
-    },
-    className
-  );
-
-  const codeClasses = classNames('euiCodeBlock__code', language);
-
-  const preClasses = classNames('euiCodeBlock__pre', {
-    'euiCodeBlock__pre--whiteSpacePre': whiteSpace === 'pre',
-    'euiCodeBlock__pre--whiteSpacePreWrap': whiteSpace === 'pre-wrap',
-    'euiCodeBlock__pre--isVirtualized': isVirtualized,
-  });
-
-  const optionalStyles: CSSProperties = {};
-
-  if (overflowHeight) {
-    const property =
-      typeof overflowHeight === 'string' ? 'height' : 'maxHeight';
-    optionalStyles[property] = overflowHeight;
-  }
-
-  const codeSnippet = (
-    
-      {content}
-    
-  );
-
-  const wrapperProps = {
-    className: classes,
-    style: optionalStyles,
-  };
-
-  if (inline) {
-    return {codeSnippet};
-  }
-
-  const getCopyButton = (_textToCopy?: string) => {
-    let copyButton: JSX.Element | undefined;
-    // Fallback to `children` in the case of virtualized blocks.
-    const textToCopy = _textToCopy || `${children}`;
-
-    if (isCopyable && textToCopy) {
-      copyButton = (
-        
- - {(copyButton: string) => ( - - {(copy) => ( - - )} - - )} - -
- ); - } - - return copyButton; - }; - - let fullScreenButton: JSX.Element | undefined; - - if (!inline && overflowHeight) { - fullScreenButton = ( - - {([fullscreenCollapse, fullscreenExpand]: string[]) => ( - - )} - - ); - } - - const getCodeBlockControls = (textToCopy?: string) => { - let codeBlockControls; - const copyButton = getCopyButton(textToCopy); - - if (copyButton || fullScreenButton) { - codeBlockControls = ( -
- {fullScreenButton} - {copyButton} -
- ); - } - - return codeBlockControls; - }; - - const getFullScreenDisplay = (codeBlockControls?: JSX.Element) => { - let fullScreenDisplay; - - if (isFullScreen) { - // Force fullscreen to use large font and padding. - const fullScreenClasses = classNames( - 'euiCodeBlock', - fontSizeToClassNameMap[fontSize], - 'euiCodeBlock-paddingLarge', - 'euiCodeBlock-isFullScreen', - className - ); - - fullScreenDisplay = ( - - -
- {isVirtualized ? ( - - {({ height, width }) => ( - - {ListRow} - - )} - - ) : ( -
-                  
-                    {content}
-                  
-                
- )} - - {codeBlockControls} -
-
-
- ); - } - - return fullScreenDisplay; - }; - - const codeBlockControls = getCodeBlockControls(innerText); - return ( -
- {isVirtualized ? ( - - {({ height, width }) => ( - - {ListRow} - - )} - - ) : ( -
-          {codeSnippet}
-        
- )} - {/* - If the below fullScreen code renders, it actually attaches to the body because of - EuiOverlayMask's React portal usage. - */} - {codeBlockControls} - {getFullScreenDisplay(codeBlockControls)} -
- ); -}; diff --git a/src/components/code/code_block.tsx b/src/components/code/code_block.tsx index 6c9586e9afd..9d924b6206f 100644 --- a/src/components/code/code_block.tsx +++ b/src/components/code/code_block.tsx @@ -6,17 +6,444 @@ * Side Public License, v 1. */ -import React, { FunctionComponent, HTMLAttributes } from 'react'; -import { CommonProps } from '../common'; +import React, { + CSSProperties, + HTMLAttributes, + FunctionComponent, + KeyboardEvent, + memo, + forwardRef, + useEffect, + useMemo, + useState, +} from 'react'; +import classNames from 'classnames'; +import { RefractorNode } from 'refractor'; +import { FixedSizeList, ListChildComponentProps } from 'react-window'; +import { keys, useCombinedRefs } from '../../services'; +import { EuiAutoSizer } from '../auto_sizer'; +import { EuiButtonIcon } from '../button'; +import { keysOf, ExclusiveUnion } from '../common'; +import { EuiCopy } from '../copy'; +import { EuiFocusTrap } from '../focus_trap'; +import { EuiI18n } from '../i18n'; +import { useInnerText } from '../inner_text'; +import { useMutationObserver } from '../observer/mutation_observer'; +import { useResizeObserver } from '../observer/resize_observer'; +import { EuiOverlayMask } from '../overlay_mask'; +import { + EuiCodeSharedProps, + DEFAULT_LANGUAGE, + checkSupportedLanguage, + getHtmlContent, + nodeToHtml, + highlightByLine, +} from './utils'; -import { EuiCodeBlockImpl, EuiCodeBlockImplProps } from './_code_block'; +// eslint-disable-next-line local/forward-ref +const virtualizedOuterElement = ({ + className, +}: HTMLAttributes) => + memo( + forwardRef((props, ref) => ( +
+    ))
+  );
 
-export type EuiCodeBlockProps = CommonProps &
-  Omit &
-  HTMLAttributes;
+// eslint-disable-next-line local/forward-ref
+const virtualizedInnerElement = ({
+  className,
+  onKeyDown,
+}: HTMLAttributes) =>
+  memo(
+    forwardRef((props, ref) => (
+      
+    ))
+  );
+
+const ListRow = ({ data, index, style }: ListChildComponentProps) => {
+  const row = data[index];
+  row.properties.style = style;
+  return nodeToHtml(row, index, data, 0);
+};
+
+// Based on observed line height for non-virtualized code blocks
+const fontSizeToRowHeightMap = {
+  s: 18,
+  m: 21,
+  l: 21,
+};
+
+const fontSizeToClassNameMap = {
+  s: 'euiCodeBlock--fontSmall',
+  m: 'euiCodeBlock--fontMedium',
+  l: 'euiCodeBlock--fontLarge',
+};
+
+type PaddingSize = 'none' | 's' | 'm' | 'l';
+type FontSize = 's' | 'm' | 'l';
+
+export const FONT_SIZES = keysOf(fontSizeToClassNameMap);
+
+const paddingSizeToClassNameMap: { [paddingSize in PaddingSize]: string } = {
+  none: '',
+  s: 'euiCodeBlock--paddingSmall',
+  m: 'euiCodeBlock--paddingMedium',
+  l: 'euiCodeBlock--paddingLarge',
+};
+
+export const PADDING_SIZES = keysOf(paddingSizeToClassNameMap);
+
+// overflowHeight is required when using virtualization
+type VirtualizedOptionProps = ExclusiveUnion<
+  {
+    /**
+     * Renders code block lines virtually.
+     * Useful for improving load times of large code blocks.
+     * `overflowHeight` is required when using this configuration.
+     */
+    isVirtualized: true;
+    /**
+     * Sets the maximum container height.
+     * Accepts a pixel value (`300`) or a percentage (`'100%'`)
+     * Ensure the container has calcuable height when using a percentage
+     */
+    overflowHeight: number | string;
+  },
+  {
+    isVirtualized?: boolean;
+    overflowHeight?: number | string;
+  }
+>;
+
+interface LineNumbersConfig {
+  start?: number;
+  highlight?: string;
+}
+
+export type EuiCodeBlockProps = EuiCodeSharedProps & {
+  paddingSize?: PaddingSize;
+  fontSize?: FontSize;
+
+  /**
+   * Specify how `white-space` inside the element is handled.
+   * `pre` respects line breaks/white space but doesn't force them to wrap the line
+   * `pre-wrap` respects line breaks/white space but does force them to wrap the line when necessary.
+   */
+  whiteSpace?: 'pre' | 'pre-wrap';
+
+  /**
+   * Displays an icon button to copy the code snippet to the clipboard.
+   */
+  isCopyable?: boolean;
+
+  /**
+   * Displays line numbers.
+   * Optionally accepts a configuration object for setting the starting number and visual highlighting ranges:
+   * `{ start: 100, highlight: '1, 5-10, 20-30, 40' }`
+   */
+  lineNumbers?: boolean | LineNumbersConfig;
+} & VirtualizedOptionProps;
 
 export const EuiCodeBlock: FunctionComponent = ({
+  language: _language = DEFAULT_LANGUAGE,
+  transparentBackground = false,
+  paddingSize = 'l',
+  fontSize = 's',
+  isCopyable = false,
+  whiteSpace = 'pre-wrap',
+  children,
+  className,
+  overflowHeight,
+  isVirtualized: _isVirtualized,
+  lineNumbers = false,
   ...rest
 }) => {
-  return ;
+  const language = useMemo(() => checkSupportedLanguage(_language), [
+    _language,
+  ]);
+  const [isFullScreen, setIsFullScreen] = useState(false);
+  const [wrapperRef, setWrapperRef] = useState(null);
+  const [innerTextRef, _innerText] = useInnerText('');
+  const innerText = useMemo(
+    () => _innerText?.replace(/[\r\n?]{2}|\n\n/g, '\n'),
+    [_innerText]
+  );
+  const [tabIndex, setTabIndex] = useState<-1 | 0>(-1);
+  const combinedRef = useCombinedRefs([
+    innerTextRef,
+    setWrapperRef,
+  ]);
+  const { width, height } = useResizeObserver(wrapperRef);
+  const rowHeight = useMemo(() => fontSizeToRowHeightMap[fontSize], [fontSize]);
+  const lineNumbersConfig = useMemo(() => {
+    const config = typeof lineNumbers === 'object' ? lineNumbers : {};
+    return lineNumbers
+      ? { start: 1, show: true, ...config }
+      : { start: 1, show: false };
+  }, [lineNumbers]);
+
+  // Used by `FixedSizeList` when `isVirtualized=true` or `children` is parsable (`isVirtualized=true`)
+  const data: RefractorNode[] = useMemo(() => {
+    if (typeof children !== 'string') {
+      return [];
+    }
+    return highlightByLine(children, language, lineNumbersConfig);
+  }, [children, language, lineNumbersConfig]);
+
+  const isVirtualized = useMemo(() => _isVirtualized && Array.isArray(data), [
+    _isVirtualized,
+    data,
+  ]);
+
+  // Used by `pre` when `isVirtualized=false` or `children` is not parsable (`isVirtualized=false`)
+  const content = useMemo(() => getHtmlContent(data, children), [
+    data,
+    children,
+  ]);
+
+  const doesOverflow = () => {
+    if (!wrapperRef) return;
+
+    const { clientWidth, clientHeight, scrollWidth, scrollHeight } = wrapperRef;
+    const doesOverflow =
+      scrollHeight > clientHeight || scrollWidth > clientWidth;
+
+    setTabIndex(doesOverflow ? 0 : -1);
+  };
+
+  useMutationObserver(wrapperRef, doesOverflow, {
+    subtree: true,
+    childList: true,
+  });
+
+  useEffect(doesOverflow, [width, height, wrapperRef]);
+
+  const onKeyDown = (event: KeyboardEvent) => {
+    if (event.key === keys.ESCAPE) {
+      event.preventDefault();
+      event.stopPropagation();
+      closeFullScreen();
+    }
+  };
+
+  const toggleFullScreen = () => {
+    setIsFullScreen(!isFullScreen);
+  };
+
+  const closeFullScreen = () => {
+    setIsFullScreen(false);
+  };
+
+  const classes = classNames(
+    'euiCodeBlock',
+    fontSizeToClassNameMap[fontSize],
+    paddingSizeToClassNameMap[paddingSize],
+    {
+      'euiCodeBlock--transparentBackground': transparentBackground,
+      'euiCodeBlock--hasControl': isCopyable || overflowHeight,
+      'euiCodeBlock--hasBothControls': isCopyable && overflowHeight,
+      'euiCodeBlock--hasLineNumbers': lineNumbersConfig.show,
+    },
+    {
+      prismjs: !className?.includes('prismjs'),
+      [`language-${language || 'none'}`]: !className?.includes('language'),
+    },
+    className
+  );
+
+  const codeClasses = classNames('euiCodeBlock__code', language);
+
+  const preClasses = classNames('euiCodeBlock__pre', {
+    'euiCodeBlock__pre--whiteSpacePre': whiteSpace === 'pre',
+    'euiCodeBlock__pre--whiteSpacePreWrap': whiteSpace === 'pre-wrap',
+    'euiCodeBlock__pre--isVirtualized': isVirtualized,
+  });
+
+  const optionalStyles: CSSProperties = {};
+
+  if (overflowHeight) {
+    const property =
+      typeof overflowHeight === 'string' ? 'height' : 'maxHeight';
+    optionalStyles[property] = overflowHeight;
+  }
+
+  const codeSnippet = (
+    
+      {content}
+    
+  );
+
+  const wrapperProps = {
+    className: classes,
+    style: optionalStyles,
+  };
+
+  const getCopyButton = (_textToCopy?: string) => {
+    let copyButton: JSX.Element | undefined;
+    // Fallback to `children` in the case of virtualized blocks.
+    const textToCopy = _textToCopy || `${children}`;
+
+    if (isCopyable && textToCopy) {
+      copyButton = (
+        
+ + {(copyButton: string) => ( + + {(copy) => ( + + )} + + )} + +
+ ); + } + + return copyButton; + }; + + let fullScreenButton: JSX.Element | undefined; + + if (overflowHeight) { + fullScreenButton = ( + + {([fullscreenCollapse, fullscreenExpand]: string[]) => ( + + )} + + ); + } + + const getCodeBlockControls = (textToCopy?: string) => { + let codeBlockControls; + const copyButton = getCopyButton(textToCopy); + + if (copyButton || fullScreenButton) { + codeBlockControls = ( +
+ {fullScreenButton} + {copyButton} +
+ ); + } + + return codeBlockControls; + }; + + const getFullScreenDisplay = (codeBlockControls?: JSX.Element) => { + let fullScreenDisplay; + + if (isFullScreen) { + // Force fullscreen to use large font and padding. + const fullScreenClasses = classNames( + 'euiCodeBlock', + fontSizeToClassNameMap[fontSize], + 'euiCodeBlock-paddingLarge', + 'euiCodeBlock-isFullScreen', + className + ); + + fullScreenDisplay = ( + + +
+ {isVirtualized ? ( + + {({ height, width }) => ( + + {ListRow} + + )} + + ) : ( +
+                  
+                    {content}
+                  
+                
+ )} + + {codeBlockControls} +
+
+
+ ); + } + + return fullScreenDisplay; + }; + + const codeBlockControls = getCodeBlockControls(innerText); + return ( +
+ {isVirtualized ? ( + + {({ height, width }) => ( + + {ListRow} + + )} + + ) : ( +
+          {codeSnippet}
+        
+ )} + {/* + If the below fullScreen code renders, it actually attaches to the body because of + EuiOverlayMask's React portal usage. + */} + {codeBlockControls} + {getFullScreenDisplay(codeBlockControls)} +
+ ); }; From 27d72782b834f6d9914a4188691fc3498479f367 Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Mon, 15 Nov 2021 13:57:42 -0800 Subject: [PATCH 05/27] Delete _code_block.test.tsx and move relevant tests to code_block.test.tsx --- .../__snapshots__/_code_block.test.tsx.snap | 410 ---------------- .../__snapshots__/code_block.test.tsx.snap | 443 +++++++++++++----- src/components/code/_code_block.test.tsx | 147 ------ src/components/code/code_block.test.tsx | 51 +- 4 files changed, 384 insertions(+), 667 deletions(-) delete mode 100644 src/components/code/__snapshots__/_code_block.test.tsx.snap delete mode 100644 src/components/code/_code_block.test.tsx diff --git a/src/components/code/__snapshots__/_code_block.test.tsx.snap b/src/components/code/__snapshots__/_code_block.test.tsx.snap deleted file mode 100644 index 87b636f6e33..00000000000 --- a/src/components/code/__snapshots__/_code_block.test.tsx.snap +++ /dev/null @@ -1,410 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EuiCodeBlockImpl block highlights javascript code, adding "js" class 1`] = ` -
-
-    
-  
-
-`; - -exports[`EuiCodeBlockImpl block renders a pre block tag 1`] = ` -
-
-    
-      
-        var some = 'code';
-
-      
-      
-        console.log(some);
-      
-    
-  
-
-`; - -exports[`EuiCodeBlockImpl block renders a pre block tag with a css class modifier 1`] = ` -
-
-    
-      
-        var some = 'code';
-
-      
-      
-        console.log(some);
-      
-    
-  
-
-`; - -exports[`EuiCodeBlockImpl block renders a virtualized code block 1`] = ` -
-
-
-      
-        
-          var some = 'code';
-
-        
-        
-          console.log(some);
-        
-      
-    
-
-
- -
-
-`; - -exports[`EuiCodeBlockImpl block renders highlighted line numbers 1`] = ` -
-
-    
-      
-        
-      
-        
-    
-  
-
- -
-
-`; - -exports[`EuiCodeBlockImpl block renders line numbers 1`] = ` -
-
-    
-      
-        
-      
-        
-    
-  
-
- -
-
-`; - -exports[`EuiCodeBlockImpl block renders line numbers with a start value 1`] = ` -
-
-    
-      
-        
-      
-        
-    
-  
-
- -
-
-`; - -exports[`EuiCodeBlockImpl block renders with transparent background 1`] = ` -
-
-    
-  
-
-`; - -exports[`EuiCodeBlockImpl inline gracefully falls back to \`text\` language 1`] = ` -
-
-    
-      
-        var some = 'code';
-
-      
-      
-        console.log(some);
-      
-    
-  
-
-`; - -exports[`EuiCodeBlockImpl inline highlights javascript code, adding "js" class 1`] = ` - - - -`; - -exports[`EuiCodeBlockImpl inline renders an inline code tag 1`] = ` - - - var some = 'code'; -console.log(some); - - -`; - -exports[`EuiCodeBlockImpl inline renders with transparent background 1`] = ` - - - -`; diff --git a/src/components/code/__snapshots__/code_block.test.tsx.snap b/src/components/code/__snapshots__/code_block.test.tsx.snap index 159dfe7daaf..20a861826b2 100644 --- a/src/components/code/__snapshots__/code_block.test.tsx.snap +++ b/src/components/code/__snapshots__/code_block.test.tsx.snap @@ -1,38 +1,75 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`EuiCodeBlock dynamic content renders a virtualized code block 1`] = ` +exports[`EuiCodeBlock dynamic content updates DOM when input changes 1`] = ` +"
+
+
+      const value = 'State 1'
+    
+
+
" +`; + +exports[`EuiCodeBlock dynamic content updates DOM when input changes 2`] = ` +"
+
+
+      const value = 'State 2'
+    
+
+
" +`; + +exports[`EuiCodeBlock line numbers renders highlighted line numbers 1`] = `
-
-
-      
         
+      
         
-    
-
+ +
+
@@ -52,24 +89,142 @@ exports[`EuiCodeBlock dynamic content renders a virtualized code block 1`] = `
`; -exports[`EuiCodeBlock dynamic content updates DOM when input changes 1`] = ` -"
-
-
-      const value = 'State 1'
-    
+exports[`EuiCodeBlock line numbers renders line numbers 1`] = ` +
+
+    
+      
+        
+      
+        
+    
+  
+
+
-
" +
`; -exports[`EuiCodeBlock dynamic content updates DOM when input changes 2`] = ` -"
-
-
-      const value = 'State 2'
-    
+exports[`EuiCodeBlock line numbers renders line numbers with a start value 1`] = ` +
+
+    
+      
+        
+      
+        
+    
+  
+
+
-
" +
`; exports[`EuiCodeBlock props fontSize l is rendered 1`] = ` @@ -157,105 +312,100 @@ exports[`EuiCodeBlock props isCopyable is rendered 1`] = ` - -
-
-        
-          
-            var some = 'code';
+          var some = 'code';
 
-          
-          
-            console.log(some);
-          
-        
-      
+ + + console.log(some); + +
+
+
-
- - + - - - - - - - - - -
+ /> + + + + + + +
-
+ `; @@ -466,6 +616,35 @@ exports[`EuiCodeBlock props transparentBackground is rendered 1`] = ` `; +exports[`EuiCodeBlock props whiteSpace renders a pre block tag with a css class modifier 1`] = ` +
+
+    
+      
+        var some = 'code';
+
+      
+      
+        console.log(some);
+      
+    
+  
+
+`; + exports[`EuiCodeBlock renders a code block 1`] = `
`; + +exports[`EuiCodeBlock virtualization renders a virtualized code block 1`] = ` +
+
+
+      
+        
+          var some = 'code';
+
+        
+        
+          console.log(some);
+        
+      
+    
+
+
+ +
+
+`; diff --git a/src/components/code/_code_block.test.tsx b/src/components/code/_code_block.test.tsx deleted file mode 100644 index bd6e3f4d4b6..00000000000 --- a/src/components/code/_code_block.test.tsx +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { render } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; - -import { EuiCodeBlockImpl } from './_code_block'; - -const code = `var some = 'code'; -console.log(some);`; - -describe('EuiCodeBlockImpl', () => { - describe('inline', () => { - test('renders an inline code tag', () => { - const component = render( - - {code} - - ); - - expect(component).toMatchSnapshot(); - }); - - test('highlights javascript code, adding "js" class', () => { - const component = render( - - ); - - expect(component).toMatchSnapshot(); - }); - - test('renders with transparent background', () => { - const component = render( - - ); - - expect(component).toMatchSnapshot(); - }); - - test('gracefully falls back to `text` language', () => { - const component = render( - {code} - ); - - expect(component).toMatchSnapshot(); - }); - }); - - describe('block', () => { - test('renders a pre block tag', () => { - const component = render( - - {code} - - ); - - expect(component).toMatchSnapshot(); - }); - - test('highlights javascript code, adding "js" class', () => { - const component = render( - - ); - - expect(component).toMatchSnapshot(); - }); - - test('renders with transparent background', () => { - const component = render( - - ); - - expect(component).toMatchSnapshot(); - }); - - test('renders a pre block tag with a css class modifier', () => { - const component = render( - - {code} - - ); - expect(component).toMatchSnapshot(); - }); - - test('renders a virtualized code block', () => { - const component = render( - - {code} - - ); - expect(component).toMatchSnapshot(); - }); - - test('renders line numbers', () => { - const component = render( - - {code} - - ); - expect(component).toMatchSnapshot(); - }); - - test('renders line numbers with a start value', () => { - const component = render( - - {code} - - ); - expect(component).toMatchSnapshot(); - }); - - test('renders highlighted line numbers', () => { - const component = render( - - {code} - - ); - expect(component).toMatchSnapshot(); - }); - }); -}); diff --git a/src/components/code/code_block.test.tsx b/src/components/code/code_block.test.tsx index b029dbdf5d8..c0538e67268 100644 --- a/src/components/code/code_block.test.tsx +++ b/src/components/code/code_block.test.tsx @@ -13,14 +13,13 @@ import html from 'html'; import { act } from 'react-dom/test-utils'; import { requiredProps } from '../../test/required_props'; -import { EuiCodeBlock } from './code_block'; -import { FONT_SIZES, PADDING_SIZES } from './_code_block'; +import { EuiCodeBlock, FONT_SIZES, PADDING_SIZES } from './code_block'; const code = `var some = 'code'; console.log(some);`; describe('EuiCodeBlock', () => { - test('renders a code block', () => { + it('renders a code block', () => { const component = render( {code} ); @@ -90,6 +89,17 @@ describe('EuiCodeBlock', () => { }); }); }); + + describe('whiteSpace', () => { + it('renders a pre block tag with a css class modifier', () => { + const component = render( + + {code} + + ); + expect(component).toMatchSnapshot(); + }); + }); }); describe('dynamic content', () => { @@ -139,7 +149,9 @@ describe('EuiCodeBlock', () => { ReactDOM.render(, appDiv); }); + }); + describe('full screen', () => { it('displays content in fullscreen mode', () => { const component = mount( @@ -154,8 +166,10 @@ describe('EuiCodeBlock', () => { 'const value = "hello"' ); }); + }); - test('renders a virtualized code block', () => { + describe('virtualization', () => { + it('renders a virtualized code block', () => { const component = render( { expect(component).toMatchSnapshot(); }); }); + + describe('line numbers', () => { + it('renders line numbers', () => { + const component = render( + + {code} + + ); + expect(component).toMatchSnapshot(); + }); + + it('renders line numbers with a start value', () => { + const component = render( + + {code} + + ); + expect(component).toMatchSnapshot(); + }); + + it('renders highlighted line numbers', () => { + const component = render( + + {code} + + ); + expect(component).toMatchSnapshot(); + }); + }); }); From 0054a930a546c4673e236a8ccee7aba1e9355373 Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Mon, 15 Nov 2021 15:50:06 -0800 Subject: [PATCH 06/27] Delete EuiCodeBlockImpl export and testenv - The export shouldn't have been exported in the first place most likely, and hopefully wasn't being used by any consumers - The testenv I'm less certain about but it looks like we used to `createPortal()` directly at one point and no longer do so/use EuiMaskOverlay, so it shouldn't be needed anymore --- src/components/code/_code_block.testenv.tsx | 31 --------------------- src/components/code/index.ts | 1 - 2 files changed, 32 deletions(-) delete mode 100644 src/components/code/_code_block.testenv.tsx diff --git a/src/components/code/_code_block.testenv.tsx b/src/components/code/_code_block.testenv.tsx deleted file mode 100644 index 1dea378712b..00000000000 --- a/src/components/code/_code_block.testenv.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -export const EuiCodeBlockImpl = ({ - children, - inline, - 'data-test-subj': dataTestSubj, -}: any) => { - const snippet = {children}; - return inline ? ( - {snippet} - ) : ( -
-
{snippet}
-
- ); -}; - -export const FONT_SIZES: Array<'s' | 'm' | 'l'> = ['s', 'm', 'l']; -export const PADDING_SIZES: Array<'s' | 'm' | 'l' | 'none'> = [ - 'none', - 's', - 'm', - 'l', -]; diff --git a/src/components/code/index.ts b/src/components/code/index.ts index c64d864c645..a5ab4442b36 100644 --- a/src/components/code/index.ts +++ b/src/components/code/index.ts @@ -8,4 +8,3 @@ export { EuiCode, EuiCodeProps } from './code'; export { EuiCodeBlock, EuiCodeBlockProps } from './code_block'; -export { EuiCodeBlockImpl } from './_code_block'; From 50cb9e3647872e816c0f77791d2565c8238b3c72 Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Mon, 15 Nov 2021 15:54:21 -0800 Subject: [PATCH 07/27] Delete unnecessary html.d.ts - By switching to render instead of html for snapshotting (tbh this seems easier to read in any case) + fix another instance in EuiDraggable that was bogarting this html.d.ts --- .../__snapshots__/code_block.test.tsx.snap | 98 ++++++------------- src/components/code/code_block.test.tsx | 19 ++-- src/components/code/html.d.ts | 9 -- .../__snapshots__/draggable.test.tsx.snap | 76 ++++++++++---- .../drag_and_drop/draggable.test.tsx | 12 +-- 5 files changed, 101 insertions(+), 113 deletions(-) delete mode 100644 src/components/code/html.d.ts diff --git a/src/components/code/__snapshots__/code_block.test.tsx.snap b/src/components/code/__snapshots__/code_block.test.tsx.snap index 20a861826b2..39e09a79cd8 100644 --- a/src/components/code/__snapshots__/code_block.test.tsx.snap +++ b/src/components/code/__snapshots__/code_block.test.tsx.snap @@ -1,33 +1,49 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EuiCodeBlock dynamic content updates DOM when input changes 1`] = ` -"
-
-
-      const value = 'State 1'
+
+
+
+      
+        const value = 'State 1'
+      
     
-
" +
`; exports[`EuiCodeBlock dynamic content updates DOM when input changes 2`] = ` -"
-
-
-      const value = 'State 2'
+
+
+
+      
+        const value = 'State 2'
+      
     
-
" +
`; exports[`EuiCodeBlock line numbers renders highlighted line numbers 1`] = `
     
     
   
-
- -
`; exports[`EuiCodeBlock line numbers renders line numbers 1`] = `
     
     
   
-
- -
`; exports[`EuiCodeBlock line numbers renders line numbers with a start value 1`] = `
     
     
   
-
- -
`; diff --git a/src/components/code/code_block.test.tsx b/src/components/code/code_block.test.tsx index c0538e67268..4be3d5d4e1f 100644 --- a/src/components/code/code_block.test.tsx +++ b/src/components/code/code_block.test.tsx @@ -9,7 +9,6 @@ import React, { useState, useEffect } from 'react'; import ReactDOM from 'react-dom'; import { mount, render } from 'enzyme'; -import html from 'html'; import { act } from 'react-dom/test-utils'; import { requiredProps } from '../../test/required_props'; @@ -107,12 +106,10 @@ describe('EuiCodeBlock', () => { expect.assertions(2); function takeSnapshot() { - expect( - html.prettyPrint(appDiv.innerHTML, { - indent_size: 2, - unformatted: [], // Expand all tags, including spans - }) - ).toMatchSnapshot(); + const snapshot = render( +
+ ); + expect(snapshot).toMatchSnapshot(); } // enzyme does not recreate enough of the React<->DOM interaction to reproduce this bug @@ -139,11 +136,9 @@ describe('EuiCodeBlock', () => { }, [value]); return ( -
- - const value = '{value}' - -
+ + const value = '{value}' + ); } diff --git a/src/components/code/html.d.ts b/src/components/code/html.d.ts deleted file mode 100644 index 1db84a2e69f..00000000000 --- a/src/components/code/html.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -declare module 'html'; diff --git a/src/components/drag_and_drop/__snapshots__/draggable.test.tsx.snap b/src/components/drag_and_drop/__snapshots__/draggable.test.tsx.snap index 0e70b7e31b5..660c4ce2894 100644 --- a/src/components/drag_and_drop/__snapshots__/draggable.test.tsx.snap +++ b/src/components/drag_and_drop/__snapshots__/draggable.test.tsx.snap @@ -1,31 +1,71 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EuiDraggable can be given ReactElement children 1`] = ` -"
-
-
Hello
+
+
+
+
+ Hello +
+
+
-
-
" +
`; exports[`EuiDraggable can be given ReactElement children 2`] = `undefined`; exports[`EuiDraggable is rendered 1`] = ` -"
-
-
Hello
+
+
+
+
+ Hello +
+
+
-
-
" +
`; exports[`EuiDraggable is rendered 2`] = `undefined`; diff --git a/src/components/drag_and_drop/draggable.test.tsx b/src/components/drag_and_drop/draggable.test.tsx index 8e985c67da4..a2d0948c7cf 100644 --- a/src/components/drag_and_drop/draggable.test.tsx +++ b/src/components/drag_and_drop/draggable.test.tsx @@ -8,18 +8,16 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { render } from 'enzyme'; import { resetServerContext } from 'react-beautiful-dnd'; -import html from 'html'; import { requiredProps } from '../../test/required_props'; import { EuiDragDropContext, EuiDraggable, EuiDroppable } from './'; function takeSnapshot(element: HTMLElement) { - expect( - html.prettyPrint(element.innerHTML, { - indent_size: 2, - unformatted: [], // Expand all tags, including spans - }) - ).toMatchSnapshot(); + const snapshot = render( +
+ ); + expect(snapshot).toMatchSnapshot(); } describe('EuiDraggable', () => { From 22d53519378b97d93a41272db2ce0a36379c3d71 Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Mon, 15 Nov 2021 17:42:53 -0800 Subject: [PATCH 08/27] Remove unused prismjs and language classes - As far as I can tell, the prismjs CSS isn't doing anything in FF or Chrome (selections still work fine), and the language class is not doing anything either --- .../__snapshots__/code_block.test.tsx.snap | 38 +++++++++---------- src/components/code/_code_block.scss | 8 ---- src/components/code/code_block.tsx | 4 -- 3 files changed, 19 insertions(+), 31 deletions(-) diff --git a/src/components/code/__snapshots__/code_block.test.tsx.snap b/src/components/code/__snapshots__/code_block.test.tsx.snap index 39e09a79cd8..ce57e1ee5b7 100644 --- a/src/components/code/__snapshots__/code_block.test.tsx.snap +++ b/src/components/code/__snapshots__/code_block.test.tsx.snap @@ -3,7 +3,7 @@ exports[`EuiCodeBlock dynamic content updates DOM when input changes 1`] = `
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
*::selection { - // Only change the color if the variable IS a color - // or else no highlight color shows up at all - @if type-of($euiCodeBlockSelectedBackgroundColor) == color { - background-color: $euiCodeBlockSelectedBackgroundColor; - } - } - .token.punctuation:not(.interpolation-punctuation):not([class*='attr-']) { opacity: .7; } diff --git a/src/components/code/code_block.tsx b/src/components/code/code_block.tsx index 9d924b6206f..5f97c6b8a8a 100644 --- a/src/components/code/code_block.tsx +++ b/src/components/code/code_block.tsx @@ -245,10 +245,6 @@ export const EuiCodeBlock: FunctionComponent = ({ 'euiCodeBlock--hasBothControls': isCopyable && overflowHeight, 'euiCodeBlock--hasLineNumbers': lineNumbersConfig.show, }, - { - prismjs: !className?.includes('prismjs'), - [`language-${language || 'none'}`]: !className?.includes('language'), - }, className ); From 9bdbd49300d44bd03b3420e54b856e4f29f83e96 Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Mon, 15 Nov 2021 14:44:23 -0800 Subject: [PATCH 09/27] Fix missing ...rest not spreading to fullscreen/virtualized elements + Update full screen unit test to snapshot to show aria-label/data-test-subj correctly cascading --- CHANGELOG.md | 4 +- .../__snapshots__/code_block.test.tsx.snap | 154 +++++++++++++++--- src/components/code/code_block.test.tsx | 10 +- src/components/code/code_block.tsx | 22 +-- 4 files changed, 155 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a98c980db6..051abcc58a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## [`main`](https://github.com/elastic/eui/tree/main) -No public interface changes since `41.1.0`. +**Bug fixes** + +- Fixed `EuiCodeBlock` not passing `data-test-subj` or `aria-label` to virtualized & full-screen code blocks ([#TODO](https://github.com/elastic/eui/pull/TODO)) ## [`41.1.0`](https://github.com/elastic/eui/tree/v41.1.0) diff --git a/src/components/code/__snapshots__/code_block.test.tsx.snap b/src/components/code/__snapshots__/code_block.test.tsx.snap index ce57e1ee5b7..7694f2eeb8a 100644 --- a/src/components/code/__snapshots__/code_block.test.tsx.snap +++ b/src/components/code/__snapshots__/code_block.test.tsx.snap @@ -10,7 +10,8 @@ exports[`EuiCodeBlock dynamic content updates DOM when input changes 1`] = ` tabindex="-1" > const value = 'State 1' @@ -29,7 +30,8 @@ exports[`EuiCodeBlock dynamic content updates DOM when input changes 2`] = ` tabindex="-1" > const value = 'State 2' @@ -38,6 +40,101 @@ exports[`EuiCodeBlock dynamic content updates DOM when input changes 2`] = `
`; +exports[`EuiCodeBlock full screen displays content in fullscreen mode 1`] = ` +
+
+    
+      
+        
+          const
+        
+         value 
+        
+          =
+        
+         
+        
+          "hello"
+        
+      
+    
+  
+
+ + + + + +
+
+`; + exports[`EuiCodeBlock line numbers renders highlighted line numbers 1`] = `
{ describe('full screen', () => { it('displays content in fullscreen mode', () => { const component = mount( - + const value = "hello" ); @@ -157,9 +161,7 @@ describe('EuiCodeBlock', () => { component.find('EuiButtonIcon[iconType="fullScreen"]').simulate('click'); component.update(); - expect(component.find('.euiCodeBlock-isFullScreen').text()).toBe( - 'const value = "hello"' - ); + expect(component.find('.euiCodeBlock-isFullScreen')).toMatchSnapshot(); }); }); diff --git a/src/components/code/code_block.tsx b/src/components/code/code_block.tsx index 5f97c6b8a8a..cd1f6c92c9a 100644 --- a/src/components/code/code_block.tsx +++ b/src/components/code/code_block.tsx @@ -52,12 +52,12 @@ const virtualizedOuterElement = ({ // eslint-disable-next-line local/forward-ref const virtualizedInnerElement = ({ - className, onKeyDown, + ...codeProps }: HTMLAttributes) => memo( forwardRef((props, ref) => ( - + )) ); @@ -248,7 +248,11 @@ export const EuiCodeBlock: FunctionComponent = ({ className ); - const codeClasses = classNames('euiCodeBlock__code', language); + const codeProps = { + className: 'euiCodeBlock__code', + 'data-code-language': language, + ...rest, + }; const preClasses = classNames('euiCodeBlock__pre', { 'euiCodeBlock__pre--whiteSpacePre': whiteSpace === 'pre', @@ -264,11 +268,7 @@ export const EuiCodeBlock: FunctionComponent = ({ optionalStyles[property] = overflowHeight; } - const codeSnippet = ( - - {content} - - ); + const codeSnippet = {content}; const wrapperProps = { className: classes, @@ -374,7 +374,7 @@ export const EuiCodeBlock: FunctionComponent = ({ className: preClasses, })} innerElementType={virtualizedInnerElement({ - className: codeClasses, + ...codeProps, onKeyDown, })} > @@ -384,7 +384,7 @@ export const EuiCodeBlock: FunctionComponent = ({ ) : (
-                  
+                  
                     {content}
                   
                 
@@ -416,7 +416,7 @@ export const EuiCodeBlock: FunctionComponent = ({ className: preClasses, })} innerElementType={virtualizedInnerElement({ - className: codeClasses, + ...codeProps, onKeyDown, })} > From f22b0272c05a8c83aae2861f9a294d097a44e41e Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Mon, 15 Nov 2021 15:05:47 -0800 Subject: [PATCH 10/27] Fix broken fullscreen escape key - the keydown was on the `` element, but it needs to be on an element with `tabindex="0"` which is the `
` element
---
 CHANGELOG.md                                  |  1 +
 .../__snapshots__/code_block.test.tsx.snap    |  4 ++-
 src/components/code/code_block.test.tsx       | 21 +++++++++++
 src/components/code/code_block.tsx            | 36 ++++++++++---------
 4 files changed, 45 insertions(+), 17 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 051abcc58a9..aefce63c004 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
 **Bug fixes**
 
 - Fixed `EuiCodeBlock` not passing `data-test-subj` or `aria-label` to virtualized & full-screen code blocks ([#TODO](https://github.com/elastic/eui/pull/TODO))
+- Fixed `EuiCodeBlock` not closing full-screen mode when the Escape key is pressed ([#TODO](https://github.com/elastic/eui/pull/TODO))
 
 ## [`41.1.0`](https://github.com/elastic/eui/tree/v41.1.0)
 
diff --git a/src/components/code/__snapshots__/code_block.test.tsx.snap b/src/components/code/__snapshots__/code_block.test.tsx.snap
index 7694f2eeb8a..3266d2cc69a 100644
--- a/src/components/code/__snapshots__/code_block.test.tsx.snap
+++ b/src/components/code/__snapshots__/code_block.test.tsx.snap
@@ -46,6 +46,8 @@ exports[`EuiCodeBlock full screen displays content in fullscreen mode 1`] = `
 >