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; +};