diff --git a/CHANGELOG.md b/CHANGELOG.md
index b8f9b953f99..840b37377a1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,10 @@
## [`master`](https://github.com/elastic/eui/tree/master)
-No public interface changes since `23.2.0`.
+- Added `testenv` mock for `EuiCode` and `EuiCodeBlock` ([#3405](https://github.com/elastic/eui/pull/3405))
+
+**Bug Fixes**
+
+- Fixed `EuiCode` and `EuiCodeBlock` from erroring in environments without a DOM implementation ([#3405](https://github.com/elastic/eui/pull/3405))
## [`23.2.0`](https://github.com/elastic/eui/tree/v23.2.0)
diff --git a/src/components/code/_code_block.testenv.tsx b/src/components/code/_code_block.testenv.tsx
new file mode 100644
index 00000000000..b1480600eb5
--- /dev/null
+++ b/src/components/code/_code_block.testenv.tsx
@@ -0,0 +1,38 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+export const EuiCodeBlockImpl = ({ children, inline }: any) => {
+ const snippet = {children}
;
+ return inline ? (
+ {snippet}
+ ) : (
+
{snippet}+
;
- render() {
- const {
- inline,
- children,
- className,
- fontSize,
- language,
- overflowHeight,
- paddingSize,
- transparentBackground,
- isCopyable,
- whiteSpace,
- ...otherProps
- } = this.props;
-
- const classes = classNames(
- 'euiCodeBlock',
- fontSizeToClassNameMap[fontSize],
- paddingSizeToClassNameMap[paddingSize],
- {
- 'euiCodeBlock--transparentBackground': transparentBackground,
- 'euiCodeBlock--inline': inline,
- 'euiCodeBlock--hasControls': isCopyable || overflowHeight,
- },
- className
- );
-
- const codeClasses = classNames('euiCodeBlock__code', language);
-
- const preClasses = classNames('euiCodeBlock__pre', {
- 'euiCodeBlock__pre--whiteSpacePre': whiteSpace === 'pre',
- 'euiCodeBlock__pre--whiteSpacePreWrap': whiteSpace === 'pre-wrap',
- });
+ const wrapperProps = {
+ className: classes,
+ style: optionalStyles,
+ };
- const optionalStyles: CSSProperties = {};
+ if (inline) {
+ return isPortalTargetReady ? (
+ <>
+ {createPortal(children, codeTarget.current!)}
+ {codeSnippet}
+ >
+ ) : null;
+ }
- if (overflowHeight) {
- optionalStyles.maxHeight = overflowHeight;
+ const getCopyButton = (textToCopy?: string) => {
+ let copyButton: JSX.Element | undefined;
+
+ if (isCopyable && textToCopy) {
+ copyButton = (
+ {
- this.code = ref;
- }}
- className={codeClasses}
- {...otherProps}
- />
- );
+ return copyButton;
+ };
- const wrapperProps = {
- className: classes,
- style: optionalStyles,
- };
-
- if (inline) {
- return (
- <>
- {createPortal(children, this.codeTarget)}
- {codeSnippet}
- >
- );
- }
+ let fullScreenButton: JSX.Element | undefined;
+
+ if (!inline && overflowHeight) {
+ fullScreenButton = (
+
+ {([fullscreenCollapse, fullscreenExpand]: string[]) => (
+
+ )}
+
+ );
+ }
- const getCopyButton = (textToCopy?: string) => {
- let copyButton: JSX.Element | undefined;
-
- if (isCopyable && textToCopy) {
- copyButton = (
-
-
- {(copyButton: string) => (
-
- {copy => (
-
- )}
-
- )}
-
-
- );
- }
+ const getCodeBlockControls = (textToCopy?: string) => {
+ let codeBlockControls;
+ const copyButton = getCopyButton(textToCopy);
- return copyButton;
- };
-
- let fullScreenButton: JSX.Element | undefined;
-
- if (!inline && overflowHeight) {
- fullScreenButton = (
-
- {([fullscreenCollapse, fullscreenExpand]: string[]) => (
-
- )}
-
+ if (copyButton || fullScreenButton) {
+ codeBlockControls = (
+
+ {fullScreenButton}
+ {copyButton}
+
);
}
- const getCodeBlockControls = (textToCopy?: string) => {
- let codeBlockControls;
- const copyButton = getCopyButton(textToCopy);
-
- if (copyButton || fullScreenButton) {
- codeBlockControls = (
-
- {fullScreenButton}
- {copyButton}
-
- );
- }
+ return codeBlockControls;
+ };
- return codeBlockControls;
- };
-
- const getFullScreenDisplay = (codeBlockControls?: JSX.Element) => {
- let fullScreenDisplay;
-
- if (this.state.isFullScreen) {
- // Force fullscreen to use large font and padding.
- const fullScreenClasses = classNames(
- 'euiCodeBlock',
- fontSizeToClassNameMap[fontSize],
- 'euiCodeBlock-paddingLarge',
- 'euiCodeBlock-isFullScreen',
- className
- );
-
- fullScreenDisplay = (
-
-
-
-
- {
- this.codeFullScreen = ref;
- }}
- className={codeClasses}
- tabIndex={0}
- onKeyDown={this.onKeyDown}
- />
-
+ 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
+ );
- {codeBlockControls}
-
-
-
- );
- }
+ fullScreenDisplay = (
+
+
+
+
+
+
+
+ {codeBlockControls}
+
+
+
+ );
+ }
- return fullScreenDisplay;
- };
+ return fullScreenDisplay;
+ };
- return (
- <>
- {createPortal(children, this.codeTarget)}
-
- {(innerTextRef, innerText) => {
- const codeBlockControls = getCodeBlockControls(innerText);
- return (
-
-
- {codeSnippet}
-
-
- {/*
+ return isPortalTargetReady ? (
+ <>
+ {createPortal(children, codeTarget.current!)}
+
+ {(innerTextRef, innerText) => {
+ const codeBlockControls = getCodeBlockControls(innerText);
+ return (
+
+
+ {codeSnippet}
+
+
+ {/*
If the below fullScreen code renders, it actually attaches to the body because of
EuiOverlayMask's React portal usage.
*/}
- {codeBlockControls}
- {getFullScreenDisplay(codeBlockControls)}
-
- );
- }}
-
- >
- );
- }
-}
+ {codeBlockControls}
+ {getFullScreenDisplay(codeBlockControls)}
+
+ );
+ }}
+
+ >
+ ) : null;
+};
diff --git a/src/components/code/code_block.test.tsx b/src/components/code/code_block.test.tsx
index f9fae994d58..68f8929891e 100644
--- a/src/components/code/code_block.test.tsx
+++ b/src/components/code/code_block.test.tsx
@@ -21,6 +21,7 @@ import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
import { mount, ReactWrapper } from 'enzyme';
import html from 'html';
+import { act } from 'react-dom/test-utils';
import { requiredProps } from '../../test/required_props';
import { EuiCodeBlock } from './code_block';
@@ -130,8 +131,13 @@ describe('EuiCodeBlock', () => {
const [value, setValue] = useState('State 1');
useEffect(() => {
- takeSnapshot();
- setValue('State 2');
+ // Wait a tick for EuiCodeBlock internal state to update on mount
+ setTimeout(() => {
+ takeSnapshot();
+ act(() => {
+ setValue('State 2');
+ });
+ });
}, []);
useEffect(() => {