From ba42930b0a0ae601389c3615be20f6782e0dbaef Mon Sep 17 00:00:00 2001
From: Lene Gadewoll <lene.gadewoll@elastic.co>
Date: Fri, 6 Sep 2024 10:13:53 +0200
Subject: [PATCH] [EuiText] Support component prop (#7993)

Co-authored-by: Cee Chen <constance.chen@elastic.co>
---
 packages/eui/changelogs/upcoming/7993.md      |  1 +
 .../eui/src/components/text/text.stories.tsx  |  1 +
 .../eui/src/components/text/text.test.tsx     | 10 ++++
 packages/eui/src/components/text/text.tsx     | 42 +++++++++-------
 .../components/text/text_align.stories.tsx    |  1 +
 .../src/components/text/text_align.test.tsx   | 10 ++++
 .../eui/src/components/text/text_align.tsx    | 24 +++-------
 .../src/components/text/text_color.test.tsx   | 10 ++++
 .../eui/src/components/text/text_color.tsx    | 34 ++-----------
 packages/eui/src/components/text/types.ts     | 48 +++++++++++++++++++
 10 files changed, 116 insertions(+), 65 deletions(-)
 create mode 100644 packages/eui/changelogs/upcoming/7993.md
 create mode 100644 packages/eui/src/components/text/types.ts

diff --git a/packages/eui/changelogs/upcoming/7993.md b/packages/eui/changelogs/upcoming/7993.md
new file mode 100644
index 00000000000..41df76e3168
--- /dev/null
+++ b/packages/eui/changelogs/upcoming/7993.md
@@ -0,0 +1 @@
+- Updated `EuiText`, `EuiTextColor`, and `EuiTextAlign` with a new `component` prop that allows changing the default rendered `<div>` wrapper to a `<span>` or `<p>` tag.
diff --git a/packages/eui/src/components/text/text.stories.tsx b/packages/eui/src/components/text/text.stories.tsx
index b3cf2b479a9..e8a613c1ea5 100644
--- a/packages/eui/src/components/text/text.stories.tsx
+++ b/packages/eui/src/components/text/text.stories.tsx
@@ -25,6 +25,7 @@ const meta: Meta<EuiTextProps> = {
     grow: true,
     color: 'default',
     textAlign: 'left',
+    component: 'div',
   },
 };
 moveStorybookControlsToCategory(meta, ['color'], 'EuiTextColor props');
diff --git a/packages/eui/src/components/text/text.test.tsx b/packages/eui/src/components/text/text.test.tsx
index 25fd808ec06..e1806621661 100644
--- a/packages/eui/src/components/text/text.test.tsx
+++ b/packages/eui/src/components/text/text.test.tsx
@@ -64,5 +64,15 @@ describe('EuiText', () => {
 
       expect(container.firstChild).toMatchSnapshot();
     });
+
+    test('component', () => {
+      const { container } = render(
+        <EuiText {...requiredProps} component="span">
+          Content
+        </EuiText>
+      );
+
+      expect(container.firstChild?.nodeName).toBe('SPAN');
+    });
   });
 });
diff --git a/packages/eui/src/components/text/text.tsx b/packages/eui/src/components/text/text.tsx
index e264e6c9ca3..bc675e7cb91 100644
--- a/packages/eui/src/components/text/text.tsx
+++ b/packages/eui/src/components/text/text.tsx
@@ -6,36 +6,31 @@
  * Side Public License, v 1.
  */
 
-import React, { FunctionComponent, HTMLAttributes, CSSProperties } from 'react';
+import React, { FunctionComponent } from 'react';
 import classNames from 'classnames';
-import { CommonProps } from '../common';
 
 import { useEuiMemoizedStyles } from '../../services';
-import { euiTextStyles } from './text.styles';
-
-import { TextColor, EuiTextColor } from './text_color';
 
-import { EuiTextAlign, TextAlignment } from './text_align';
+import type { SharedTextProps, EuiTextColors, EuiTextAlignment } from './types';
+import { EuiTextColor } from './text_color';
+import { EuiTextAlign } from './text_align';
+import { euiTextStyles } from './text.styles';
 
 export const TEXT_SIZES = ['xs', 's', 'm', 'relative'] as const;
 export type TextSize = (typeof TEXT_SIZES)[number];
 
-export type EuiTextProps = CommonProps &
-  Omit<HTMLAttributes<HTMLDivElement>, 'color'> & {
-    textAlign?: TextAlignment;
+export type EuiTextProps = SharedTextProps &
+  EuiTextColors &
+  EuiTextAlignment & {
     /**
      * Determines the text size. Choose `relative` to control the `font-size` based on the value of a parent container.
      */
     size?: TextSize;
-    /**
-     * Any of our named colors or a `hex`, `rgb` or `rgba` value.
-     * @default inherit
-     */
-    color?: TextColor | CSSProperties['color'];
     grow?: boolean;
   };
 
 export const EuiText: FunctionComponent<EuiTextProps> = ({
+  component = 'div',
   size = 'm',
   color,
   grow = true,
@@ -52,16 +47,22 @@ export const EuiText: FunctionComponent<EuiTextProps> = ({
   ];
 
   const classes = classNames('euiText', className);
+  const Component = component;
 
   let text = (
-    <div css={cssStyles} className={classes} {...rest}>
+    <Component css={cssStyles} className={classes} {...rest}>
       {children}
-    </div>
+    </Component>
   );
 
   if (color) {
     text = (
-      <EuiTextColor color={color} className={classes} cloneElement>
+      <EuiTextColor
+        component={component}
+        color={color}
+        className={classes}
+        cloneElement
+      >
         {text}
       </EuiTextColor>
     );
@@ -69,7 +70,12 @@ export const EuiText: FunctionComponent<EuiTextProps> = ({
 
   if (textAlign) {
     text = (
-      <EuiTextAlign textAlign={textAlign} className={classes} cloneElement>
+      <EuiTextAlign
+        component={component}
+        textAlign={textAlign}
+        className={classes}
+        cloneElement
+      >
         {text}
       </EuiTextAlign>
     );
diff --git a/packages/eui/src/components/text/text_align.stories.tsx b/packages/eui/src/components/text/text_align.stories.tsx
index 67f1fb4ad9d..8adcaab5226 100644
--- a/packages/eui/src/components/text/text_align.stories.tsx
+++ b/packages/eui/src/components/text/text_align.stories.tsx
@@ -21,6 +21,7 @@ const meta: Meta<EuiTextAlignProps> = {
   args: {
     textAlign: 'left',
     cloneElement: false,
+    component: 'div',
   },
 };
 hideStorybookControls(meta, ['aria-label']);
diff --git a/packages/eui/src/components/text/text_align.test.tsx b/packages/eui/src/components/text/text_align.test.tsx
index ea99b3358ef..e31bb5c3641 100644
--- a/packages/eui/src/components/text/text_align.test.tsx
+++ b/packages/eui/src/components/text/text_align.test.tsx
@@ -50,5 +50,15 @@ describe('EuiTextAlign', () => {
 
       shouldRenderCustomStyles(<EuiTextAlign cloneElement textAlign="right" />);
     });
+
+    test('component', () => {
+      const { container } = render(
+        <EuiTextAlign {...requiredProps} component="span">
+          Content
+        </EuiTextAlign>
+      );
+
+      expect(container.firstChild?.nodeName).toBe('SPAN');
+    });
   });
 });
diff --git a/packages/eui/src/components/text/text_align.tsx b/packages/eui/src/components/text/text_align.tsx
index 5b3c44736c4..aa8fbad54a6 100644
--- a/packages/eui/src/components/text/text_align.tsx
+++ b/packages/eui/src/components/text/text_align.tsx
@@ -6,31 +6,21 @@
  * Side Public License, v 1.
  */
 
-import React, {
-  FunctionComponent,
-  HTMLAttributes,
-  isValidElement,
-} from 'react';
-import { CommonProps } from '../common';
+import React, { FunctionComponent, isValidElement } from 'react';
 import { cloneElementWithCss } from '../../services';
-
+import type { SharedTextProps, CloneElement, EuiTextAlignment } from './types';
 import { euiTextAlignStyles as styles } from './text_align.styles';
 
 export const ALIGNMENTS = ['left', 'right', 'center'] as const;
 export type TextAlignment = (typeof ALIGNMENTS)[number];
 
-export type EuiTextAlignProps = CommonProps &
-  HTMLAttributes<HTMLDivElement> & {
-    textAlign?: TextAlignment;
-    /**
-     * Applies text styling to the child element instead of rendering a parent wrapper `div`.
-     * Can only be used when wrapping a *single* child element/tag, and not raw text.
-     */
-    cloneElement?: boolean;
-  };
+export type EuiTextAlignProps = SharedTextProps &
+  CloneElement &
+  EuiTextAlignment;
 
 export const EuiTextAlign: FunctionComponent<EuiTextAlignProps> = ({
   children,
+  component: Component = 'div',
   textAlign = 'left',
   cloneElement = false,
   ...rest
@@ -42,6 +32,6 @@ export const EuiTextAlign: FunctionComponent<EuiTextAlignProps> = ({
   if (isValidElement(children) && cloneElement) {
     return cloneElementWithCss(children, props);
   } else {
-    return <div {...props}>{children}</div>;
+    return <Component {...props}>{children}</Component>;
   }
 };
diff --git a/packages/eui/src/components/text/text_color.test.tsx b/packages/eui/src/components/text/text_color.test.tsx
index 876d94ce14c..ba0bf2f4f3d 100644
--- a/packages/eui/src/components/text/text_color.test.tsx
+++ b/packages/eui/src/components/text/text_color.test.tsx
@@ -62,5 +62,15 @@ describe('EuiTextColor', () => {
         </EuiTextColor>
       );
     });
+
+    test('component', () => {
+      const { container } = render(
+        <EuiTextColor {...requiredProps} component="span">
+          Content
+        </EuiTextColor>
+      );
+
+      expect(container.firstChild?.nodeName).toBe('SPAN');
+    });
   });
 });
diff --git a/packages/eui/src/components/text/text_color.tsx b/packages/eui/src/components/text/text_color.tsx
index bbc5ef2fb30..a31615eb40e 100644
--- a/packages/eui/src/components/text/text_color.tsx
+++ b/packages/eui/src/components/text/text_color.tsx
@@ -6,16 +6,9 @@
  * Side Public License, v 1.
  */
 
-import React, {
-  FunctionComponent,
-  HTMLAttributes,
-  CSSProperties,
-  isValidElement,
-} from 'react';
-
-import { CommonProps } from '../common';
+import React, { FunctionComponent, isValidElement } from 'react';
 import { useEuiMemoizedStyles, cloneElementWithCss } from '../../services';
-
+import type { SharedTextProps, CloneElement, EuiTextColors } from './types';
 import { euiTextColorStyles } from './text_color.styles';
 
 export const COLORS = [
@@ -32,30 +25,12 @@ export type TextColor = (typeof COLORS)[number];
 export const _isNamedColor = (color: any): color is TextColor =>
   COLORS.includes(color);
 
-export type EuiTextColorProps = CommonProps &
-  Omit<
-    HTMLAttributes<HTMLDivElement> & HTMLAttributes<HTMLSpanElement>,
-    'color'
-  > & {
-    /**
-     * Any of our named colors or a `hex`, `rgb` or `rgba` value.
-     */
-    color?: TextColor | CSSProperties['color'];
-    /**
-     * Determines the root element
-     */
-    component?: 'div' | 'span';
-    /**
-     * Applies text styling to the child element instead of rendering a parent wrapper `span`/`div`.
-     * Can only be used when wrapping a *single* child element/tag, and not raw text.
-     */
-    cloneElement?: boolean;
-  };
+export type EuiTextColorProps = SharedTextProps & CloneElement & EuiTextColors;
 
 export const EuiTextColor: FunctionComponent<EuiTextColorProps> = ({
   children,
   color = 'default',
-  component = 'span',
+  component: Component = 'span',
   cloneElement = false,
   style,
   ...rest
@@ -84,7 +59,6 @@ export const EuiTextColor: FunctionComponent<EuiTextColorProps> = ({
     const childrenStyle = { ...children.props.style, ...euiTextStyle };
     return cloneElementWithCss(children, { ...props, style: childrenStyle });
   } else {
-    const Component = component;
     return <Component {...props}>{children}</Component>;
   }
 };
diff --git a/packages/eui/src/components/text/types.ts b/packages/eui/src/components/text/types.ts
new file mode 100644
index 00000000000..26b37508c7d
--- /dev/null
+++ b/packages/eui/src/components/text/types.ts
@@ -0,0 +1,48 @@
+/*
+ * 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 type { HTMLAttributes, CSSProperties } from 'react';
+import type { CommonProps } from '../common';
+
+import type { TextColor } from './text_color';
+import type { TextAlignment } from './text_align';
+
+export type SharedTextProps = CommonProps &
+  Omit<HTMLAttributes<HTMLElement>, 'color'> & {
+    /**
+     * The HTML element/tag to render.
+     * Use with care when nesting multiple components to ensure valid XHTML:
+     * - `<div>` and other block tags are not valid to use inside `<p>`. If using the `<p>` tag, we recommend passing strings/text only.
+     * - `<span>` is valid to be nested in any tag, and can have any tag nested within it.
+     */
+    component?: 'div' | 'span' | 'p';
+  };
+
+export type CloneElement = {
+  /**
+   * Applies text styling to the child element instead of rendering a parent wrapper.
+   * Can only be used when wrapping a *single* child element/tag, and not raw text.
+   */
+  cloneElement?: boolean;
+};
+
+export type EuiTextColors = {
+  /**
+   * Any of our named colors or a `hex`, `rgb` or `rgba` value.
+   * @default inherit
+   */
+  color?: TextColor | CSSProperties['color'];
+};
+
+export type EuiTextAlignment = {
+  /**
+   * Applies horizontal text alignment
+   * @default left
+   */
+  textAlign?: TextAlignment;
+};