diff --git a/changelogs/upcoming/7666.md b/changelogs/upcoming/7666.md new file mode 100644 index 00000000000..ea0f6ffa1fb --- /dev/null +++ b/changelogs/upcoming/7666.md @@ -0,0 +1,6 @@ +**Bug fixes** + +- Fixed `EuiFieldNumber`'s typing to accept an icon configuration shape +- Fixed `EuiFieldText` and `EuiFieldNumber` to render the correct paddings for icon shapes set to `side: 'right'` +- Fixed `EuiFieldText` and `EuiFieldNumber` to fully ignore `icon`/`prepend`/`append` when `controlOnly` is set to true +- Fixed `EuiColorPicker`'s input not setting the correct right padding for the number of icons displayed diff --git a/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap b/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap index 3a3b711332e..01a480b6704 100644 --- a/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap +++ b/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap @@ -27,7 +27,7 @@ exports[`renders EuiColorPicker 1`] = ` = ({ 'euiColorPicker__popoverPanel--customButton': button, }); const swatchClass = 'euiColorPicker__swatchSelect'; - const inputClasses = classNames('euiColorPicker__input', { - 'euiColorPicker__input--inGroup': prepend || append, + + const numIconsClass = getFormControlClassNameForIconCount({ + isDropdown: true, + clear: isClearable, + isInvalid, }); + const inputClasses = classNames( + 'euiColorPicker__input', + { 'euiColorPicker__input--inGroup': prepend || append }, + // Manually account for input padding, since `controlOnly` disables that logic + 'euiFieldText--withIcon', + numIconsClass + ); const handleOnChange = (text: string) => { const output = getOutput(text, showAlpha); diff --git a/src/components/form/field_number/field_number.stories.tsx b/src/components/form/field_number/field_number.stories.tsx index b283f0d8e5f..0aa0ab52b7e 100644 --- a/src/components/form/field_number/field_number.stories.tsx +++ b/src/components/form/field_number/field_number.stories.tsx @@ -7,6 +7,11 @@ */ import type { Meta, StoryObj } from '@storybook/react'; +import { + disableStorybookControls, + hideStorybookControls, + moveStorybookControlsToCategory, +} from '../../../../.storybook/utils'; import { EuiFieldNumber, EuiFieldNumberProps } from './field_number'; @@ -15,11 +20,28 @@ const meta: Meta = { component: EuiFieldNumber, argTypes: { step: { control: 'number' }, + // For quicker/easier QA + icon: { control: 'text' }, + prepend: { control: 'text' }, + append: { control: 'text' }, + }, + args: { + // Component defaults + compressed: false, + fullWidth: false, + isInvalid: false, + isLoading: false, + disabled: false, + readOnly: false, + controlOnly: false, + // Added for easier testing + placeholder: '0', }, }; export default meta; type Story = StoryObj; +disableStorybookControls(meta, ['inputRef']); export const Playground: Story = {}; @@ -32,3 +54,43 @@ export const ControlledComponent: Story = { onChange: () => {}, }, }; +// Hide props that don't impact this story +hideStorybookControls(ControlledComponent, [ + 'controlOnly', + 'inputRef', + 'compressed', + 'fullWidth', + 'icon', + 'isInvalid', + 'isLoading', + 'disabled', + 'readOnly', + 'placeholder', + 'prepend', + 'append', +]); + +export const IconShape: Story = { + argTypes: { icon: { control: 'object' } }, + args: { icon: { type: 'warning', color: 'warning', side: 'left' } }, +}; +// Move other props below the icon prop +moveStorybookControlsToCategory(IconShape, [ + 'compressed', + 'fullWidth', + 'isInvalid', + 'isLoading', + 'disabled', + 'readOnly', + 'placeholder', + 'prepend', + 'append', +]); +// Hide props that remove or won't affect the icon or its positioning +hideStorybookControls(IconShape, [ + 'controlOnly', + 'inputRef', + 'min', + 'max', + 'step', +]); diff --git a/src/components/form/field_number/field_number.tsx b/src/components/form/field_number/field_number.tsx index f7a6f9e003f..9b8bc28569a 100644 --- a/src/components/form/field_number/field_number.tsx +++ b/src/components/form/field_number/field_number.tsx @@ -19,14 +19,16 @@ import classNames from 'classnames'; import { useCombinedRefs } from '../../../services'; import { CommonProps } from '../../common'; -import { IconType } from '../../icon'; import { EuiValidatableControl } from '../validatable_control'; import { EuiFormControlLayout, EuiFormControlLayoutProps, } from '../form_control_layout'; -import { getFormControlClassNameForIconCount } from '../form_control_layout/_num_icons'; +import { + getFormControlClassNameForIconCount, + isRightSideIcon, +} from '../form_control_layout/_num_icons'; import { useFormContext } from '../eui_form_context'; export type EuiFieldNumberProps = Omit< @@ -34,7 +36,7 @@ export type EuiFieldNumberProps = Omit< 'min' | 'max' | 'readOnly' | 'step' > & CommonProps & { - icon?: IconType; + icon?: EuiFormControlLayoutProps['icon']; isInvalid?: boolean; /** * Expand to fill 100% of the parent. @@ -134,18 +136,22 @@ export const EuiFieldNumber: FunctionComponent = ( } }, [value, min, max, step, checkNativeValidity]); + const hasRightSideIcon = isRightSideIcon(icon); const numIconsClass = controlOnly ? false : getFormControlClassNameForIconCount({ isInvalid: isInvalid || isNativelyInvalid, isLoading, + icon: hasRightSideIcon, }); const classes = classNames('euiFieldNumber', className, numIconsClass, { - 'euiFieldNumber--withIcon': icon, 'euiFieldNumber--fullWidth': fullWidth, 'euiFieldNumber--compressed': compressed, - 'euiFieldNumber--inGroup': prepend || append, + ...(!controlOnly && { + 'euiFieldNumber--inGroup': prepend || append, + 'euiFieldNumber--withIcon': icon && !hasRightSideIcon, + }), 'euiFieldNumber-isLoading': isLoading, }); diff --git a/src/components/form/field_text/field_text.stories.tsx b/src/components/form/field_text/field_text.stories.tsx new file mode 100644 index 00000000000..4484f691814 --- /dev/null +++ b/src/components/form/field_text/field_text.stories.tsx @@ -0,0 +1,64 @@ +/* + * 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 { Meta, StoryObj } from '@storybook/react'; +import { + disableStorybookControls, + hideStorybookControls, + moveStorybookControlsToCategory, +} from '../../../../.storybook/utils'; + +import { EuiFieldText, EuiFieldTextProps } from './field_text'; + +const meta: Meta = { + title: 'Forms/EuiFieldText', + component: EuiFieldText, + argTypes: { + // For quicker/easier QA + icon: { control: 'text' }, + prepend: { control: 'text' }, + append: { control: 'text' }, + }, + args: { + // Component defaults + compressed: false, + fullWidth: false, + isInvalid: false, + isLoading: false, + disabled: false, + readOnly: false, + controlOnly: false, + // Added for easier testing + placeholder: 'EuiFieldText', + }, +}; + +export default meta; +type Story = StoryObj; +disableStorybookControls(meta, ['inputRef']); + +export const Playground: Story = {}; + +export const IconShape: Story = { + argTypes: { icon: { control: 'object' } }, + args: { icon: { type: 'warning', color: 'warning', side: 'left' } }, +}; +// Move other props below the icon prop +moveStorybookControlsToCategory(IconShape, [ + 'compressed', + 'fullWidth', + 'isInvalid', + 'isLoading', + 'disabled', + 'readOnly', + 'placeholder', + 'prepend', + 'append', +]); +// Hide props that remove or won't affect the icon or its positioning +hideStorybookControls(IconShape, ['controlOnly', 'inputRef']); diff --git a/src/components/form/field_text/field_text.tsx b/src/components/form/field_text/field_text.tsx index 3a40234d5ed..fb89e2263b4 100644 --- a/src/components/form/field_text/field_text.tsx +++ b/src/components/form/field_text/field_text.tsx @@ -16,7 +16,10 @@ import { } from '../form_control_layout'; import { EuiValidatableControl } from '../validatable_control'; -import { getFormControlClassNameForIconCount } from '../form_control_layout/_num_icons'; +import { + isRightSideIcon, + getFormControlClassNameForIconCount, +} from '../form_control_layout/_num_icons'; import { useFormContext } from '../eui_form_context'; export type EuiFieldTextProps = InputHTMLAttributes & @@ -78,18 +81,23 @@ export const EuiFieldText: FunctionComponent = (props) => { ...rest } = props; + const hasRightSideIcon = isRightSideIcon(icon); + const numIconsClass = controlOnly ? false : getFormControlClassNameForIconCount({ isInvalid, isLoading, + icon: hasRightSideIcon, }); const classes = classNames('euiFieldText', className, numIconsClass, { - 'euiFieldText--withIcon': icon, 'euiFieldText--fullWidth': fullWidth, 'euiFieldText--compressed': compressed, - 'euiFieldText--inGroup': prepend || append, + ...(!controlOnly && { + 'euiFieldText--withIcon': icon && !hasRightSideIcon, + 'euiFieldText--inGroup': prepend || append, + }), 'euiFieldText-isLoading': isLoading, }); diff --git a/src/components/form/form_control_layout/_num_icons.test.ts b/src/components/form/form_control_layout/_num_icons.test.ts index ae56dd29629..4b3f26693e7 100644 --- a/src/components/form/form_control_layout/_num_icons.test.ts +++ b/src/components/form/form_control_layout/_num_icons.test.ts @@ -6,7 +6,10 @@ * Side Public License, v 1. */ -import { getFormControlClassNameForIconCount } from './_num_icons'; +import { + getFormControlClassNameForIconCount, + isRightSideIcon, +} from './_num_icons'; describe('getFormControlClassNameForIconCount', () => { it('should return undefined if object is empty', () => { @@ -47,3 +50,27 @@ describe('getFormControlClassNameForIconCount', () => { expect(numberClass).toEqual('euiFormControlLayout--5icons'); }); }); + +describe('isRightSideIcon', () => { + it('returns true if side has been set to right', () => { + expect(isRightSideIcon({ side: 'right', type: 'warning' })).toEqual(true); + }); + + it('returns false if side has been set to left', () => { + expect(isRightSideIcon({ side: 'left', type: 'warning' })).toEqual(false); + }); + + it('returns false if icon is missing a side definition (defaults to left)', () => { + expect(isRightSideIcon({ type: 'warning', color: 'warning' })).toEqual( + false + ); + }); + + it('returns false if icon is undefined', () => { + expect(isRightSideIcon()).toEqual(false); + }); + + it('returns false if icon is not in an object shape', () => { + expect(isRightSideIcon('warning')).toEqual(false); + }); +}); diff --git a/src/components/form/form_control_layout/_num_icons.ts b/src/components/form/form_control_layout/_num_icons.ts index e9a15884e08..95c792c4cf2 100644 --- a/src/components/form/form_control_layout/_num_icons.ts +++ b/src/components/form/form_control_layout/_num_icons.ts @@ -6,6 +6,11 @@ * Side Public License, v 1. */ +import { + isIconShape, + type EuiFormControlLayoutIconsProps, +} from './form_control_layout_icons'; + /** * The `getFormControlClassNameForIconCount` function helps setup the className appendum * depending on the form control's current settings/state. @@ -26,17 +31,23 @@ export type _EuiFormControlLayoutNumIcons = { isDropdown?: boolean; }; -export function getFormControlClassNameForIconCount({ +export const getFormControlClassNameForIconCount = ({ icon, clear, isLoading, isInvalid, isDropdown, -}: _EuiFormControlLayoutNumIcons): string | undefined { +}: _EuiFormControlLayoutNumIcons): string | undefined => { const numIcons = [icon, clear, isInvalid, isLoading, isDropdown].filter( (item) => item === true ).length; // This className is also specifically used in `src/global_styling/mixins/_form.scss` return numIcons > 0 ? `euiFormControlLayout--${numIcons}icons` : undefined; -} +}; + +export const isRightSideIcon = ( + icon?: EuiFormControlLayoutIconsProps['icon'] +): boolean => { + return !!icon && isIconShape(icon) && icon.side === 'right'; +}; diff --git a/src/components/form/form_control_layout/form_control_layout_icons.tsx b/src/components/form/form_control_layout/form_control_layout_icons.tsx index e8d23bd2fc3..5cfd7ce1ac3 100644 --- a/src/components/form/form_control_layout/form_control_layout_icons.tsx +++ b/src/components/form/form_control_layout/form_control_layout_icons.tsx @@ -33,11 +33,11 @@ export type IconShape = DistributiveOmit< ref?: EuiFormControlLayoutCustomIconProps['iconRef']; }; -function isIconShape( +export const isIconShape = ( icon: EuiFormControlLayoutIconsProps['icon'] -): icon is IconShape { +): icon is IconShape => { return !!icon && icon.hasOwnProperty('type'); -} +}; export interface EuiFormControlLayoutIconsProps { icon?: IconType | IconShape;