diff --git a/packages/eui-theme-common/src/global_styling/variables/typography.ts b/packages/eui-theme-common/src/global_styling/variables/typography.ts index 205d6deb220..9587f4243a0 100644 --- a/packages/eui-theme-common/src/global_styling/variables/typography.ts +++ b/packages/eui-theme-common/src/global_styling/variables/typography.ts @@ -97,11 +97,11 @@ export type _EuiThemeFontWeights = { light: CSSProperties['fontWeight']; /** - Default value: 400 */ regular: CSSProperties['fontWeight']; - /** - Default value: 500 */ + /** - Default value: 450 */ medium: CSSProperties['fontWeight']; - /** - Default value: 600 */ + /** - Default value: 500 */ semiBold: CSSProperties['fontWeight']; - /** - Default value: 700 */ + /** - Default value: 600 */ bold: CSSProperties['fontWeight']; }; diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiAutoRefresh_EuiAutoRefresh_Playground.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiAutoRefresh_EuiAutoRefresh_Playground.png index 24da76c570a..a6e68c3f293 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiAutoRefresh_EuiAutoRefresh_Playground.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiAutoRefresh_EuiAutoRefresh_Playground.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayoutDelimited_High_Contrast.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayoutDelimited_High_Contrast.png index 0b271367361..fbd50280462 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayoutDelimited_High_Contrast.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayoutDelimited_High_Contrast.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayoutDelimited_High_Contrast_Dark_Mode.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayoutDelimited_High_Contrast_Dark_Mode.png index 60bee7e46e4..bd16aaafb19 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayoutDelimited_High_Contrast_Dark_Mode.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayoutDelimited_High_Contrast_Dark_Mode.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayout_Append_Prepend.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayout_Append_Prepend.png index 65b9e8a3d3d..46fa34bfc1d 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayout_Append_Prepend.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayout_Append_Prepend.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayout_High_Contrast.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayout_High_Contrast.png index 91c13bca230..ab18426c154 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayout_High_Contrast.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayout_High_Contrast.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayout_High_Contrast_Dark_Mode.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayout_High_Contrast_Dark_Mode.png index 137a10feff0..25e3b396f6c 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayout_High_Contrast_Dark_Mode.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayout_High_Contrast_Dark_Mode.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayout_Subcomponents_EuiFormAppend_Playground.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayout_Subcomponents_EuiFormAppend_Playground.png new file mode 100644 index 00000000000..d4c8d505b2d Binary files /dev/null and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayout_Subcomponents_EuiFormAppend_Playground.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayout_Subcomponents_EuiFormPrepend_Playground.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayout_Subcomponents_EuiFormPrepend_Playground.png new file mode 100644 index 00000000000..811ab19576b Binary files /dev/null and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayout_Subcomponents_EuiFormPrepend_Playground.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Custom_Quick_Select_Panel.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Custom_Quick_Select_Panel.png index 033a7ad61f8..cc45746ff8c 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Custom_Quick_Select_Panel.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Custom_Quick_Select_Panel.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Overflowing_Children.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Overflowing_Children.png index f2dbaedb060..b3f23a6e576 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Overflowing_Children.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Overflowing_Children.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Playground.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Playground.png index 4a15f8f24ca..b5496d2de2e 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Playground.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Playground.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Quick_Select_Only.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Quick_Select_Only.png index 2a105461355..bf7efcda18e 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Quick_Select_Only.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Quick_Select_Only.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Restricted_Range.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Restricted_Range.png index 66708327a83..2d4a5bbeac1 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Restricted_Range.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Restricted_Range.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiAutoRefresh_EuiAutoRefresh_Playground.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiAutoRefresh_EuiAutoRefresh_Playground.png index b52bc8826f2..967b7b04aba 100644 Binary files a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiAutoRefresh_EuiAutoRefresh_Playground.png and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiAutoRefresh_EuiAutoRefresh_Playground.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormControlLayout_Append_Prepend.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormControlLayout_Append_Prepend.png index 87987e64b46..e6181042c99 100644 Binary files a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormControlLayout_Append_Prepend.png and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormControlLayout_Append_Prepend.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormControlLayout_High_Contrast.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormControlLayout_High_Contrast.png index a44c147c575..f13c6443025 100644 Binary files a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormControlLayout_High_Contrast.png and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormControlLayout_High_Contrast.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormControlLayout_High_Contrast_Dark_Mode.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormControlLayout_High_Contrast_Dark_Mode.png index 41106b678ad..6fd69c5395d 100644 Binary files a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormControlLayout_High_Contrast_Dark_Mode.png and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormControlLayout_High_Contrast_Dark_Mode.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormControlLayout_Subcomponents_EuiFormAppend_Playground.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormControlLayout_Subcomponents_EuiFormAppend_Playground.png new file mode 100644 index 00000000000..b687bf8a18b Binary files /dev/null and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormControlLayout_Subcomponents_EuiFormAppend_Playground.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormControlLayout_Subcomponents_EuiFormPrepend_Playground.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormControlLayout_Subcomponents_EuiFormPrepend_Playground.png new file mode 100644 index 00000000000..0a628532a0d Binary files /dev/null and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormControlLayout_Subcomponents_EuiFormPrepend_Playground.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Custom_Quick_Select_Panel.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Custom_Quick_Select_Panel.png index aadcbb3be89..6fe7cc9022f 100644 Binary files a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Custom_Quick_Select_Panel.png and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Custom_Quick_Select_Panel.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Overflowing_Children.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Overflowing_Children.png index 525a255e0c4..b777532bf4e 100644 Binary files a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Overflowing_Children.png and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Overflowing_Children.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Playground.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Playground.png index 54fa863032c..7a0d46e7fc8 100644 Binary files a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Playground.png and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Playground.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Quick_Select_Only.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Quick_Select_Only.png index f7fe0ab3e40..ee6510258fd 100644 Binary files a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Quick_Select_Only.png and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Quick_Select_Only.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Restricted_Range.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Restricted_Range.png index 54fa863032c..7a0d46e7fc8 100644 Binary files a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Restricted_Range.png and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Restricted_Range.png differ diff --git a/packages/eui/changelogs/upcoming/9014.md b/packages/eui/changelogs/upcoming/9014.md new file mode 100644 index 00000000000..4f2e18d305b --- /dev/null +++ b/packages/eui/changelogs/upcoming/9014.md @@ -0,0 +1,12 @@ +- Added `EuiFormAppend` and `EuiFormPrepend` components +- Added support for `type="span"` on `EuiFormLabel` to support using visual-only form labels +- Updated `EuiFormControlLayout` to use `EuiFormAppend` and `EuiFormPrepend` +- Updated `EuiAutoRefresh` and `EuiColorPicker` to use `EuiFormPrepend` + +**Breaking changes** + +- Updated `EuiQuickSelectPopover` in `EuiSuperDatePicker` to use `EuiFormPrepend`. This results in more restricted `buttonProps` as they reflect `EuiFormPrepend` instead of generic `EuiButtonEmpty` props. + +**Bug fixes** + +- Updated `EuiColorPicker` to ensure `id` is correctly passed onto the internal `EuiFormControlLayout` diff --git a/packages/eui/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap b/packages/eui/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap index 8ebe78405ba..2d6db435181 100644 --- a/packages/eui/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap +++ b/packages/eui/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap @@ -616,11 +616,15 @@ exports[`EuiColorPicker prepend and append 1`] = `
- + +
- + +
diff --git a/packages/eui/src/components/color_picker/color_picker.styles.ts b/packages/eui/src/components/color_picker/color_picker.styles.ts index 25d86472346..b8881cfdeff 100644 --- a/packages/eui/src/components/color_picker/color_picker.styles.ts +++ b/packages/eui/src/components/color_picker/color_picker.styles.ts @@ -46,7 +46,7 @@ export const euiColorPickerStyles = (euiThemeContext: UseEuiTheme) => { padding-inline: ${euiTheme.size.xs}; } - .euiFormControlLayout__append { + .euiFormAppend { padding-inline: ${euiTheme.size.xxs} !important; } diff --git a/packages/eui/src/components/color_picker/color_picker.tsx b/packages/eui/src/components/color_picker/color_picker.tsx index 2521673beb7..e5babd1c12d 100644 --- a/packages/eui/src/components/color_picker/color_picker.tsx +++ b/packages/eui/src/components/color_picker/color_picker.tsx @@ -602,6 +602,7 @@ export const EuiColorPicker: FunctionComponent = ({ isInvalid={isInvalid} isDisabled={disabled} isDropdown + inputId={id} > @@ -60,25 +52,17 @@ exports[`EuiAutoRefresh isPaused is false 1`] = ` class="euiFormControlLayout__prepend emotion-euiFormControlLayout__side-prepend" > @@ -108,25 +92,17 @@ exports[`EuiAutoRefresh refreshInterval is rendered 1`] = ` class="euiFormControlLayout__prepend emotion-euiFormControlLayout__side-prepend" > diff --git a/packages/eui/src/components/date_picker/auto_refresh/auto_refresh.tsx b/packages/eui/src/components/date_picker/auto_refresh/auto_refresh.tsx index d8b08dd5366..227900d8d88 100644 --- a/packages/eui/src/components/date_picker/auto_refresh/auto_refresh.tsx +++ b/packages/eui/src/components/date_picker/auto_refresh/auto_refresh.tsx @@ -8,7 +8,7 @@ import React, { FunctionComponent, useState } from 'react'; import classNames from 'classnames'; -import { EuiFieldText, EuiFieldTextProps } from '../../form'; +import { EuiFieldText, EuiFieldTextProps, EuiFormPrepend } from '../../form'; import { EuiButtonEmpty, CommonEuiButtonEmptyProps, @@ -61,18 +61,14 @@ export const EuiAutoRefresh: FunctionComponent = ({ aria-label={autoRefeshLabel} onClick={() => setIsPopoverOpen((isOpen) => !isOpen)} prepend={ - setIsPopoverOpen((isOpen) => !isOpen)} - size="s" - color="text" - iconType="timeRefresh" + element="button" + label={{autoRefeshLabel}} + iconLeft="timeRefresh" isDisabled={isDisabled} - > - - {autoRefeshLabel} - - + onClick={() => setIsPopoverOpen((isOpen) => !isOpen)} + /> } readOnly={readOnly} disabled={isDisabled} diff --git a/packages/eui/src/components/date_picker/date_picker.stories.tsx b/packages/eui/src/components/date_picker/date_picker.stories.tsx index 9aea2690272..aca241ddb11 100644 --- a/packages/eui/src/components/date_picker/date_picker.stories.tsx +++ b/packages/eui/src/components/date_picker/date_picker.stories.tsx @@ -150,6 +150,7 @@ export const Playground: Story = { // setting a selected date to ensure VRT does not // automatically updated based on the current date selected: moment('Tue Mar 19 2024 18:54:51 GMT+0100'), + id: 'foo', }, render: (args) => , }; diff --git a/packages/eui/src/components/date_picker/super_date_picker/__snapshots__/super_date_picker.test.tsx.snap b/packages/eui/src/components/date_picker/super_date_picker/__snapshots__/super_date_picker.test.tsx.snap index 60586a65c10..fc3421c5a0d 100644 --- a/packages/eui/src/components/date_picker/super_date_picker/__snapshots__/super_date_picker.test.tsx.snap +++ b/packages/eui/src/components/date_picker/super_date_picker/__snapshots__/super_date_picker.test.tsx.snap @@ -16,26 +16,16 @@ exports[`EuiSuperDatePicker props accepts data-test-subj and passes to EuiFormCo > @@ -89,26 +79,16 @@ exports[`EuiSuperDatePicker props compressed is rendered 1`] = ` > @@ -161,25 +141,17 @@ exports[`EuiSuperDatePicker props isAutoRefreshOnly is rendered 1`] = ` class="euiFormControlLayout__prepend emotion-euiFormControlLayout__side-prepend" > @@ -214,26 +186,18 @@ exports[`EuiSuperDatePicker props isAutoRefreshOnly passes required props 1`] = class="euiFormControlLayout__prepend emotion-euiFormControlLayout__side-prepend" > @@ -284,27 +248,17 @@ exports[`EuiSuperDatePicker props isDisabled true 1`] = ` > @@ -360,26 +314,16 @@ exports[`EuiSuperDatePicker props isQuickSelectOnly is rendered 1`] = ` > @@ -425,26 +369,16 @@ exports[`EuiSuperDatePicker props showUpdateButton can be false 1`] = ` > @@ -478,26 +412,16 @@ exports[`EuiSuperDatePicker props showUpdateButton can be iconOnly 1`] = ` > @@ -554,26 +478,16 @@ exports[`EuiSuperDatePicker props width can be auto 1`] = ` > @@ -627,26 +541,16 @@ exports[`EuiSuperDatePicker props width can be full 1`] = ` > @@ -701,26 +605,16 @@ exports[`EuiSuperDatePicker renders 1`] = ` > @@ -778,26 +672,16 @@ exports[`EuiSuperDatePicker renders an EuiDatePickerRange 1`] = ` > diff --git a/packages/eui/src/components/date_picker/super_date_picker/quick_select_popover/__snapshots__/quick_select_popover.test.tsx.snap b/packages/eui/src/components/date_picker/super_date_picker/quick_select_popover/__snapshots__/quick_select_popover.test.tsx.snap index bd97e997353..814693281a3 100644 --- a/packages/eui/src/components/date_picker/super_date_picker/quick_select_popover/__snapshots__/quick_select_popover.test.tsx.snap +++ b/packages/eui/src/components/date_picker/super_date_picker/quick_select_popover/__snapshots__/quick_select_popover.test.tsx.snap @@ -416,26 +416,16 @@ exports[`EuiQuickSelectPopover is rendered 1`] = ` > `; diff --git a/packages/eui/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.tsx b/packages/eui/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.tsx index e1469d7d5af..7848881ac46 100644 --- a/packages/eui/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.tsx +++ b/packages/eui/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.tsx @@ -18,11 +18,8 @@ import React, { import { useEuiMemoizedStyles } from '../../../../services'; import { useEuiI18n } from '../../../i18n'; -import { EuiButtonEmpty } from '../../../button'; -import { EuiButtonEmptyPropsForButton } from '../../../button/button_empty/button_empty'; -import { EuiIcon } from '../../../icon'; import { EuiPopover } from '../../../popover'; - +import { EuiFormAppendPrependButtonProps, EuiFormPrepend } from '../../../form'; import { euiQuickSelectPopoverStyles } from './quick_select_popover.styles'; import { EuiQuickSelectPanel } from './quick_select_panel'; import { EuiQuickSelect } from './quick_select'; @@ -41,7 +38,7 @@ import { } from '../../types'; export type EuiQuickSelectButtonProps = Partial< - Omit + Omit >; export type CustomQuickSelectRenderOptions = { @@ -76,11 +73,7 @@ export interface EuiQuickSelectPopoverProps { export const EuiQuickSelectPopover: FunctionComponent< EuiQuickSelectPopoverProps > = ({ applyTime: _applyTime, buttonProps = {}, ...props }) => { - const { - contentProps: buttonContentProps, - onClick: buttonOnClick, - ...quickSelectButtonProps - } = buttonProps; + const { onClick: buttonOnClick, ...quickSelectButtonProps } = buttonProps; const [prevQuickSelect, setQuickSelect] = useState(); const [isOpen, setIsOpen] = useState(false); @@ -105,8 +98,6 @@ export const EuiQuickSelectPopover: FunctionComponent< 'Date quick select' ); - const styles = useEuiMemoizedStyles(euiQuickSelectPopoverStyles); - const quickSelectButtonOnClick = ( e: MouseEvent & MouseEvent ) => { @@ -115,24 +106,17 @@ export const EuiQuickSelectPopover: FunctionComponent< }; const quickSelectButton = ( - - - + /> ); return ( diff --git a/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.stories.tsx b/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.stories.tsx index 32c0874d86a..8149721082a 100644 --- a/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.stories.tsx +++ b/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.stories.tsx @@ -61,7 +61,13 @@ enableFunctionToggleControls(meta, ['onTimeChange']); export default meta; type Story = StoryObj; -export const Playground: Story = {}; +export const Playground: Story = { + args: { + quickSelectButtonProps: { + color: 'danger', + }, + }, +}; enableFunctionToggleControls(Playground, [ 'onFocus', 'onRefresh', diff --git a/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.test.tsx b/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.test.tsx index afed819a614..727e8d98ef6 100644 --- a/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.test.tsx +++ b/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.test.tsx @@ -157,7 +157,7 @@ describe('EuiSuperDatePicker', () => { const quickSelectButtonProps: EuiSuperDatePickerProps['quickSelectButtonProps'] = { onMouseDown, - color: 'danger', + 'aria-label': 'Quick Select', }; const { getByTestSubject } = render( @@ -169,7 +169,10 @@ describe('EuiSuperDatePicker', () => { const quickSelectButton = getByTestSubject( 'superDatePickerToggleQuickMenuButton' )!; - expect(quickSelectButton.className).toContain('danger'); + expect(quickSelectButton).toHaveAttribute( + 'aria-label', + quickSelectButtonProps['aria-label'] + ); fireEvent.mouseDown(quickSelectButton); expect(onMouseDown).toHaveBeenCalledTimes(1); diff --git a/packages/eui/src/components/form/field_password/__snapshots__/field_password.test.tsx.snap b/packages/eui/src/components/form/field_password/__snapshots__/field_password.test.tsx.snap index 260aeb8580b..c67b67667b4 100644 --- a/packages/eui/src/components/form/field_password/__snapshots__/field_password.test.tsx.snap +++ b/packages/eui/src/components/form/field_password/__snapshots__/field_password.test.tsx.snap @@ -298,11 +298,15 @@ exports[`EuiFieldPassword props prepend and append is rendered 1`] = `
- + +
- + +
`; diff --git a/packages/eui/src/components/form/field_password/field_password.stories.tsx b/packages/eui/src/components/form/field_password/field_password.stories.tsx index e0ffabd8293..a63950576e9 100644 --- a/packages/eui/src/components/form/field_password/field_password.stories.tsx +++ b/packages/eui/src/components/form/field_password/field_password.stories.tsx @@ -13,8 +13,8 @@ import { disableStorybookControls, enableFunctionToggleControls, } from '../../../../.storybook/utils'; -import { EuiIcon } from '../../icon'; import { EuiFieldPassword, EuiFieldPasswordProps } from './field_password'; +import { EuiFormAppend, EuiFormPrepend } from '../form_control_layout'; const meta: Meta = { title: 'Forms/EuiFieldPassword', @@ -24,7 +24,7 @@ const meta: Meta = { control: 'radio', options: [undefined, 'icon', 'text'], mapping: { - icon: , + icon: , text: 'Appended', undefined: undefined, }, @@ -33,7 +33,7 @@ const meta: Meta = { control: 'radio', options: [undefined, 'icon', 'text'], mapping: { - icon: , + icon: , text: 'Prepended', undefined: undefined, }, diff --git a/packages/eui/src/components/form/field_search/field_search.stories.tsx b/packages/eui/src/components/form/field_search/field_search.stories.tsx index 40f95f854ba..d07e782e551 100644 --- a/packages/eui/src/components/form/field_search/field_search.stories.tsx +++ b/packages/eui/src/components/form/field_search/field_search.stories.tsx @@ -13,8 +13,8 @@ import { disableStorybookControls, enableFunctionToggleControls, } from '../../../../.storybook/utils'; -import { EuiIcon } from '../../icon'; import { EuiFieldSearch, EuiFieldSearchProps } from './field_search'; +import { EuiFormAppend, EuiFormPrepend } from '../form_control_layout'; const meta: Meta = { title: 'Forms/EuiFieldSearch', @@ -24,7 +24,7 @@ const meta: Meta = { control: 'radio', options: [undefined, 'icon', 'text'], mapping: { - icon: , + icon: , text: 'Appended', undefined: undefined, }, @@ -33,7 +33,7 @@ const meta: Meta = { control: 'radio', options: [undefined, 'icon', 'text'], mapping: { - icon: , + icon: , text: 'Prepended', undefined: undefined, }, diff --git a/packages/eui/src/components/form/form_control_layout/__snapshots__/form_control_layout.test.tsx.snap b/packages/eui/src/components/form/form_control_layout/__snapshots__/form_control_layout.test.tsx.snap index 2440cca85fa..429247c632b 100644 --- a/packages/eui/src/components/form/form_control_layout/__snapshots__/form_control_layout.test.tsx.snap +++ b/packages/eui/src/components/form/form_control_layout/__snapshots__/form_control_layout.test.tsx.snap @@ -368,11 +368,15 @@ exports[`EuiFormControlLayout props one append string is rendered 1`] = `
- + +
`; @@ -420,11 +424,15 @@ exports[`EuiFormControlLayout props one prepend string is rendered 1`] = `
- + +
+ +
+`; + +exports[`EuiFormAppendPrepend is rendered 1`] = ` +
+ +
+`; + +exports[`EuiFormPrepend is rendered 1`] = ` +
+ +
+`; diff --git a/packages/eui/src/components/form/form_control_layout/append_prepend/form_append.stories.tsx b/packages/eui/src/components/form/form_control_layout/append_prepend/form_append.stories.tsx new file mode 100644 index 00000000000..5c170d3c180 --- /dev/null +++ b/packages/eui/src/components/form/form_control_layout/append_prepend/form_append.stories.tsx @@ -0,0 +1,76 @@ +/* + * 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 type { Meta, StoryObj } from '@storybook/react'; + +import { + hideStorybookControls, + enableFunctionToggleControls, +} from '../../../../../.storybook/utils'; + +import { EuiFieldText } from '../../field_text'; +import { EuiNotificationBadge } from '../../../badge'; +import { EuiFormAppend, type EuiFormAppendProps } from './form_append_prepend'; + +const meta: Meta = { + title: 'Forms/EuiForm/EuiFormControlLayout/Subcomponents/EuiFormAppend', + component: EuiFormAppend, + argTypes: { + label: { control: 'text' }, + iconLeft: { control: 'text' }, + iconRight: { control: 'text' }, + children: { + control: 'radio', + options: [undefined, 'badge', 'text'], + mapping: { + badge: 1, + text: 'Content', + undefined: undefined, + }, + }, + isDisabled: { control: 'boolean' }, + }, + args: { + inputId: '', + element: 'div', + compressed: false, + label: '', + iconLeft: '', + iconRight: '', + children: undefined, + // @ts-expect-error - ignore exclusive union + isDisabled: false, + }, +}; +hideStorybookControls(meta, ['aria-label']); +enableFunctionToggleControls(meta, ['onClick']); + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + args: { + label: 'Append', + // @ts-expect-error - onClick is optional but the toggle is enabled + onClick: false, + }, + render: ({ compressed, inputId, ...args }: EuiFormAppendProps) => { + const textFieldProps = { + compressed, + id: inputId, + }; + + return ( + } + /> + ); + }, +}; diff --git a/packages/eui/src/components/form/form_control_layout/append_prepend/form_append_prepend.styles.ts b/packages/eui/src/components/form/form_control_layout/append_prepend/form_append_prepend.styles.ts new file mode 100644 index 00000000000..3b33b86ed76 --- /dev/null +++ b/packages/eui/src/components/form/form_control_layout/append_prepend/form_append_prepend.styles.ts @@ -0,0 +1,111 @@ +/* + * 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 { css } from '@emotion/react'; +import { UseEuiTheme } from '@elastic/eui-theme-common'; + +import { isEuiThemeRefreshVariant } from '../../../../services'; +import { logicalCSS } from '../../../../global_styling'; +import { buttonSelectors } from '../form_control_layout.styles'; +import { euiFormVariables } from '../../form.styles'; + +export const euiFormAppendPrependStyles = (euiThemeContext: UseEuiTheme) => { + const { euiTheme } = euiThemeContext; + const isRefreshVariant = isEuiThemeRefreshVariant( + euiThemeContext, + 'formVariant' + ); + const form = euiFormVariables(euiThemeContext); + + const buttons = buttonSelectors; + + return { + side: css` + position: relative; + display: flex; + align-items: center; + gap: ${euiTheme.size.xs}; + block-size: 100%; + max-inline-size: 100%; + `, + uncompressed: css` + &:not(:has(> ${buttons}:first-child, > *:first-child ${buttons})) { + ${logicalCSS( + 'padding-left', + isRefreshVariant ? euiTheme.size.m : euiTheme.size.s + )} + } + + &:not(:has(> ${buttons}:last-child, > *:last-child ${buttons})) { + ${logicalCSS( + 'padding-right', + isRefreshVariant ? euiTheme.size.m : euiTheme.size.s + )} + } + `, + compressed: css` + &:not(:has(> ${buttons}:first-child, > *:first-child ${buttons})) { + ${logicalCSS('padding-left', euiTheme.size.s)} + } + + &:not(:has(> ${buttons}:last-child, > *:last-child ${buttons})) { + ${logicalCSS('padding-right', euiTheme.size.s)} + } + `, + append: css` + border-radius: 0; + border-start-end-radius: ${euiTheme.border.radius.small}; + border-end-end-radius: ${euiTheme.border.radius.small}; + `, + prepend: css` + border-radius: 0; + border-start-start-radius: ${euiTheme.border.radius.small}; + border-end-start-radius: ${euiTheme.border.radius.small}; + `, + isInteractive: css` + color: ${euiTheme.colors.textPrimary}; + + &:hover { + background-color: ${euiTheme.colors.backgroundBaseInteractiveHover}; + } + + &:focus-visible { + outline: none; + + /* apply a focus style that matches input focus more closely */ + &::after { + content: ''; + position: absolute; + inset: 0; + border: ${euiTheme.border.width.thick} solid + ${euiTheme.components.forms.borderFocused}; + /* ensure it stays on top of hovered borders */ + z-index: 2; + pointer-events: none; + border-radius: inherit; + } + } + + .euiFormLabel { + color: currentColor; + cursor: pointer; + } + + * { + cursor: pointer; + } + `, + disabled: css` + color: ${form.textColorDisabled}; + + .euiFormLabel { + color: ${form.textColorDisabled}; + } + `, + }; +}; diff --git a/packages/eui/src/components/form/form_control_layout/append_prepend/form_append_prepend.test.tsx b/packages/eui/src/components/form/form_control_layout/append_prepend/form_append_prepend.test.tsx new file mode 100644 index 00000000000..37d1dbca083 --- /dev/null +++ b/packages/eui/src/components/form/form_control_layout/append_prepend/form_append_prepend.test.tsx @@ -0,0 +1,266 @@ +/* + * 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 { fireEvent } from '@testing-library/react'; + +import { shouldRenderCustomStyles } from '../../../../test/internal'; +import { + EuiFormAppend, + EuiFormAppendPrepend, + EuiFormAppendPrependProps, + EuiFormPrepend, +} from './form_append_prepend'; +import { requiredProps } from '../../../../test'; +import { render } from '../../../../test/rtl'; +import { EuiFieldText } from '../../field_text'; + +const sharedProps = { + label: 'Label', + 'data-test-subj': 'euiFormAppendPrepend', +}; + +const defaultProps = { + ...sharedProps, + side: 'append' as EuiFormAppendPrependProps['side'], +}; + +describe('EuiFormAppend', () => { + shouldRenderCustomStyles(); + + it('is rendered', () => { + const { container, getByTestSubject } = render( + + ); + + const classes = Object.values( + getByTestSubject(requiredProps['data-test-subj']).classList + ); + + expect(container.firstChild).toMatchSnapshot(); + expect(classes[0]).toBe('euiFormAppend'); + }); +}); + +describe('EuiFormPrepend', () => { + shouldRenderCustomStyles(); + + it('is rendered', () => { + const { container, getByTestSubject } = render( + + ); + + const classes = Object.values( + getByTestSubject(requiredProps['data-test-subj']).classList + ); + + expect(container.firstChild).toMatchSnapshot(); + expect(classes[0]).toBe('euiFormPrepend'); + }); +}); + +describe('EuiFormAppendPrepend', () => { + shouldRenderCustomStyles(); + + it('is rendered', () => { + const { container } = render( + + ); + + expect(container.firstChild).toMatchSnapshot(); + }); + + describe('props', () => { + describe('element', () => { + it('renders a div', () => { + const { getByTestSubject } = render( + + ); + + const element = getByTestSubject(defaultProps['data-test-subj']); + + expect(element).toBeInTheDocument(); + expect(element.tagName).toBe('DIV'); + }); + + it('renders a button', () => { + const { getByTestSubject } = render( + + ); + + const element = getByTestSubject(defaultProps['data-test-subj']); + + expect(element).toBeInTheDocument(); + expect(element.tagName).toBe('BUTTON'); + }); + }); + + describe('label', () => { + it('renders a label element', () => { + const { getByText } = render( + + ); + + const element = getByText(defaultProps.label); + + expect(element).toBeInTheDocument(); + expect(element.tagName).toBe('LABEL'); + }); + + it('renders a span element for buttons', () => { + const { getByText } = render( + + ); + + const element = getByText(defaultProps.label); + + expect(element).toBeInTheDocument(); + expect(element.tagName).toBe('SPAN'); + }); + }); + + describe('iconLeft', () => { + it('renders an icon on the left side', () => { + const { getByTestSubject } = render( + + ); + + expect( + getByTestSubject(defaultProps['data-test-subj']).firstChild + ).toHaveAttribute('data-euiicon-type', 'faceHappy'); + }); + }); + + describe('iconRight', () => { + it('renders an icon on the left side', () => { + const { getByTestSubject } = render( + + ); + + expect( + getByTestSubject(defaultProps['data-test-subj']).lastChild + ).toHaveAttribute('data-euiicon-type', 'faceHappy'); + }); + }); + + describe('children', () => { + it('renders', () => { + const { side, 'data-test-subj': dataTestSubj } = defaultProps; + + const { getByTestSubject } = render( + + Content + + ); + + const element = getByTestSubject(dataTestSubj); + + expect(element.children.length).toBe(1); + expect(element.firstChild).toHaveTextContent('Content'); + }); + + it('renders `children` as last child', () => { + const { getByTestSubject } = render( + + Content + + ); + + const element = getByTestSubject(defaultProps['data-test-subj']); + + expect(element.firstChild).toHaveTextContent(defaultProps.label); + expect(element.lastChild).toHaveTextContent('Content'); + }); + }); + + describe('inputId', () => { + it('renders `for` attribute when `inputId` is passed', () => { + const { getByText } = render( + + ); + + expect(getByText(defaultProps.label)).toHaveAttribute('for', 'testId'); + }); + + it('does not render `for` attribute for buttons when `inputId` is passed', () => { + const { getByText } = render( + + ); + + expect(getByText(defaultProps.label)).not.toHaveAttribute('for'); + }); + + it('renders `for` attribute when `id` is set on the parent form element', () => { + const { getByText } = render( + } + /> + ); + + expect(getByText(defaultProps.label)).toHaveAttribute('for', 'testId'); + }); + }); + + describe('compressed', () => { + it('renders compressed styles', () => { + const { getByTestSubject } = render( + + ); + + const classes = Object.values( + getByTestSubject(defaultProps['data-test-subj']).classList + ); + + expect(classes.some((clx) => clx.includes('compressed'))).toBe(true); + }); + + it('renders compressed styles when the parent form element is compressed', () => { + const { getByTestSubject } = render( + } + /> + ); + + const classes = Object.values( + getByTestSubject(defaultProps['data-test-subj']).classList + ); + + expect(classes.some((clx) => clx.includes('compressed'))).toBe(true); + }); + }); + + describe('onClick', () => { + it('renders a button with click handler', () => { + const onClick = jest.fn(); + + const { getByTestSubject } = render( + + ); + + const element = getByTestSubject(defaultProps['data-test-subj']); + + expect(element).toBeInTheDocument(); + expect(element.tagName).toBe('BUTTON'); + + fireEvent.click(element); + + expect(onClick).toHaveBeenCalledTimes(1); + }); + }); + }); +}); diff --git a/packages/eui/src/components/form/form_control_layout/append_prepend/form_append_prepend.tsx b/packages/eui/src/components/form/form_control_layout/append_prepend/form_append_prepend.tsx new file mode 100644 index 00000000000..2453b55dbe5 --- /dev/null +++ b/packages/eui/src/components/form/form_control_layout/append_prepend/form_append_prepend.tsx @@ -0,0 +1,180 @@ +/* + * 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, { + ButtonHTMLAttributes, + FunctionComponent, + HTMLAttributes, + ReactNode, + useContext, +} from 'react'; +import classNames from 'classnames'; + +import { useEuiMemoizedStyles } from '../../../../services'; +import { CommonProps, ExclusiveUnion } from '../../../common'; +import { EuiIcon, IconType } from '../../../icon'; +import { EuiFormLabel } from '../../form_label'; +import { + _EuiFormLabelProps, + _EuiFormLabelSpanProps, +} from '../../form_label/form_label'; +import { euiFormAppendPrependStyles } from './form_append_prepend.styles'; +import { EuiFormControlLayoutContext } from '../form_control_layout_context'; + +export type EuiFormAppendProps = EuiFormAppendPrependBaseProps; + +export const EuiFormAppend = ({ className, ...rest }: EuiFormAppendProps) => { + const classes = classNames('euiFormAppend', className); + + return ; +}; + +export type EuiFormPrependProps = EuiFormAppendPrependBaseProps; + +export const EuiFormPrepend = ({ className, ...rest }: EuiFormPrependProps) => { + const classes = classNames('euiFormPrepend', className); + + return ; +}; + +export type EuiFormAppendPrependCommonProps = CommonProps & { + /** + * Main content label + */ + label?: ReactNode; + /** + * Left side icon + */ + iconLeft?: IconType; + /** + * Right side icon + */ + iconRight?: IconType; + /** + * Optional content that will be appended to `label` and icons + */ + children?: ReactNode; + /** + * id of the input element that the form label is linked to via `htmlFor` attribute + */ + inputId?: string; + /** + * Renders the element with smaller height and padding + */ + compressed?: boolean; +}; + +export type EuiFormAppendPrependButtonProps = + EuiFormAppendPrependCommonProps & { + /** + * Defines the rendered HTML element + */ + element?: 'button'; + isDisabled?: boolean; + } & ButtonHTMLAttributes; + +export type EuiFormAppendPrependDivProps = EuiFormAppendPrependCommonProps & { + /** + * Defines the rendered HTML element + */ + element?: 'div'; +} & HTMLAttributes; + +export type EuiFormAppendPrependBaseProps = ExclusiveUnion< + EuiFormAppendPrependButtonProps, + EuiFormAppendPrependDivProps +>; + +export type EuiFormAppendPrependProps = { + side: 'append' | 'prepend'; +} & EuiFormAppendPrependBaseProps; + +/* Internal component */ + +export const EuiFormAppendPrepend: FunctionComponent< + EuiFormAppendPrependProps +> = ({ + element = 'div', + side, + children, + className, + inputId: _inputId, + compressed: _compressed, + iconLeft: _iconLeft, + iconRight: _iconRight, + label: _label, + isDisabled: _isDisabled, + disabled, + ...rest +}) => { + const styles = useEuiMemoizedStyles(euiFormAppendPrependStyles); + + const { compressed: formLayoutCompressed, inputId: formLayoutInputId } = + useContext(EuiFormControlLayoutContext); + const compressed = _compressed ?? formLayoutCompressed; + const inputId = _inputId ?? formLayoutInputId; + + // Adding automatic check on onClick for DevX convinience, this doesn't replace defining `element` + const isButton = element === 'button' || typeof rest.onClick === 'function'; + const isDisabled = _isDisabled || disabled; + + const iconLeft = _iconLeft && ; + const iconRight = _iconRight && ; + + const cssStyles = [ + styles.side, + compressed ? styles.compressed : styles.uncompressed, + isButton && !isDisabled && styles.isInteractive, + isDisabled && styles.disabled, + isButton && styles[side], + ]; + + const labelProps = isButton + ? ({ + type: 'span', + className: 'eui-textTruncate', + } as _EuiFormLabelSpanProps) + : ({ + type: 'label', + htmlFor: inputId || undefined, + } as _EuiFormLabelProps); + + const label = _label && {_label}; + + const content = ( + <> + {iconLeft} + {label} + {iconRight} + {children} + + ); + + if (isButton) { + return ( + + ); + } + + return ( +
)} + > + {content} +
+ ); +}; diff --git a/packages/eui/src/components/form/form_control_layout/append_prepend/form_prepend.stories.tsx b/packages/eui/src/components/form/form_control_layout/append_prepend/form_prepend.stories.tsx new file mode 100644 index 00000000000..b0df65c5860 --- /dev/null +++ b/packages/eui/src/components/form/form_control_layout/append_prepend/form_prepend.stories.tsx @@ -0,0 +1,78 @@ +/* + * 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 type { Meta, StoryObj } from '@storybook/react'; + +import { + hideStorybookControls, + enableFunctionToggleControls, +} from '../../../../../.storybook/utils'; + +import { EuiFieldText } from '../../field_text'; +import { EuiNotificationBadge } from '../../../badge'; +import { + EuiFormPrepend, + type EuiFormPrependProps, +} from './form_append_prepend'; + +const meta: Meta = { + title: 'Forms/EuiForm/EuiFormControlLayout/Subcomponents/EuiFormPrepend', + component: EuiFormPrepend, + argTypes: { + label: { control: 'text' }, + iconLeft: { control: 'text' }, + iconRight: { control: 'text' }, + children: { + control: 'radio', + options: [undefined, 'badge', 'text'], + mapping: { + badge: 1, + text: 'Content', + undefined: undefined, + }, + }, + }, + args: { + inputId: '', + element: 'div', + compressed: false, + label: '', + iconLeft: '', + iconRight: '', + children: undefined, + // @ts-expect-error - ignore exclusive union + isDisabled: false, + }, +}; +hideStorybookControls(meta, ['aria-label']); +enableFunctionToggleControls(meta, ['onClick']); + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + args: { + label: 'Prepend', + // @ts-expect-error - onClick is optional but the toggle is enabled + onClick: false, + }, + render: ({ compressed, inputId, ...args }: EuiFormPrependProps) => { + const textFieldProps = { + compressed, + id: inputId, + }; + + return ( + } + /> + ); + }, +}; diff --git a/packages/eui/src/components/form/form_control_layout/append_prepend/index.ts b/packages/eui/src/components/form/form_control_layout/append_prepend/index.ts new file mode 100644 index 00000000000..0d43a856e6a --- /dev/null +++ b/packages/eui/src/components/form/form_control_layout/append_prepend/index.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ + +export { + EuiFormAppend, + EuiFormPrepend, + type EuiFormAppendProps, + type EuiFormPrependProps, + type EuiFormAppendPrependButtonProps, + type EuiFormAppendPrependDivProps, +} from './form_append_prepend'; diff --git a/packages/eui/src/components/form/form_control_layout/form_control_layout.stories.tsx b/packages/eui/src/components/form/form_control_layout/form_control_layout.stories.tsx index a0917906521..10597a91476 100644 --- a/packages/eui/src/components/form/form_control_layout/form_control_layout.stories.tsx +++ b/packages/eui/src/components/form/form_control_layout/form_control_layout.stories.tsx @@ -6,8 +6,10 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { useState } from 'react'; +import { css } from '@emotion/react'; import type { Meta, StoryObj } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; import { hideStorybookControls } from '../../../../.storybook/utils'; @@ -19,11 +21,23 @@ import { EuiIconTip, EuiToolTip } from '../../tool_tip'; import { EuiPopover } from '../../popover'; import { EuiButtonEmpty, EuiButtonIcon } from '../../button'; import { EuiText } from '../../text'; +import { EuiNotificationBadge } from '../../badge'; +import { EuiCopy } from '../../copy'; +import { EuiFlexGroup } from '../../flex'; +import { + EuiDragDropContext, + EuiDraggable, + EuiDroppable, +} from '../../drag_and_drop'; +import { EuiFormLabel } from '../form_label'; +import { EuiFilterButton } from '../../filter_group'; import { EuiFormControlLayout, EuiFormControlLayoutProps, } from './form_control_layout'; +import { EuiFormAppend, EuiFormPrepend } from './append_prepend'; +import { UseEuiTheme } from '@elastic/eui-theme-common'; const meta: Meta = { title: 'Forms/EuiForm/EuiFormControlLayout', @@ -73,7 +87,7 @@ export const Playground: Story = { const { readOnly, isDisabled, fullWidth, compressed } = args; const childProps = { readOnly, - isDisabled, + disabled: isDisabled, fullWidth, compressed, isInvalid: args.isInvalid, @@ -121,8 +135,11 @@ export const IconShape: Story = { }, }; -export const AppendPrepend: Story = { - tags: ['vrt-only'], +/* TODO: remove testing story */ +export const AppendPrepend_REVIEW_EXAMPLE: Story = { + parameters: { + loki: { skip: true }, + }, render: function Render() { const isDesktop = useIsWithinMinBreakpoint('xl'); return ( @@ -272,6 +289,777 @@ export const AppendPrepend: Story = { }, }; +export const AppendPrepend: Story = { + tags: ['vrt-only'], + render: function Render(args) { + const isDesktop = useIsWithinMinBreakpoint('xl'); + + const renderContent = (compressed: boolean = false) => { + const idBase = `input-${compressed ? 'compressed' : ''}`; + + return ( + + } + autoFocus + /> + + } + append={ + + } + /> + + + } + append={ + + } + /> + + + 1 +
+ } + append={ + + 1 + + } + /> + + + + + } + append={ + + + + } + /> + + + } + append={ + + } + /> + } + /> + + } + append={ + + } + /> + + + } + /> + + } + append="String" + /> + + ); + }; + + return ( + + {renderContent(false)} + {renderContent(true)} + + ); + }, +}; + +/* TODO: remove testing story */ +export const Kitchensink: Story = { + parameters: { + codeSnippet: { + skip: true, + }, + loki: { skip: true }, + }, + render: function Render(args) { + const { readOnly, isDisabled, fullWidth, compressed } = args; + const isDesktop = useIsWithinMinBreakpoint('xl'); + + const [isPopoverOpenA, setPopoverOpenA] = useState(false); + const [isPopoverOpenB, setPopoverOpenB] = useState(false); + + const formStyles = ({ euiTheme }: UseEuiTheme) => css` + display: flex; + flex-direction: column; + gap: ${euiTheme.size.m}; + `; + + const childProps = { + readOnly, + disabled: isDisabled, + fullWidth, + compressed, + isInvalid: args.isInvalid, + }; + + return ( + + + +

Styled wrapper API

+
+ + + + + 1 +
+ } + /> + + } + append={ + + + + } + /> + + + 1 +
+ } + /> + + + {(copy) => ( + + )} + + } + /> + + setPopoverOpenA(false)} + button={ + setPopoverOpenA(!isPopoverOpenA)} + /> + } + > + Popover content + + } + append={ + + + + } + /> + + + + 1 + + + } + closePopover={() => setPopoverOpenA(false)} + /> + } + /> + + + } + /> + + + } + append={ + + } + /> + + {/* Drag examples */} + + +

Drag examples

+
+ + {}}> + + + {(provided) => ( + + } + /> + )} + + + + + +

With custom styles (reduce spacing and hover styles)

+
+ + {}}> + + + {(provided) => ( +
css` + .euiFormControlLayout__prepend { + &:is(:hover, :active) { + &::before { + content: ''; + position: absolute; + inset: 0; + background-color: ${euiTheme.colors + .backgroundBaseInteractiveHover}; + } + + .euiFormAppendPrepend__dragHandle { + color: ${euiTheme.colors.textParagraph}; + } + } + } + `} + > + + + + + + + + } + /> +
+ )} +
+
+
+ + + {/* split here */} + + + +

Custom content API

+
+ + + + , + 'Prepended', + , + 1, + ]} + /> + + } + append={ + + Tooltip + + } + /> + + + + Appended + + 1 + + + } + /> + + + {(copy) => ( + + Copy + + )} + + } + /> + + setPopoverOpenB(!isPopoverOpenB)} + > + + + } + closePopover={() => setPopoverOpenB(false)} + /> + } + append={ + + Tooltip + + } + /> + + setPopoverOpenB(!isPopoverOpenB)} + > + + + } + closePopover={() => setPopoverOpenB(false)} + /> + } + /> + + } + /> + + + + , + ]} + append={[ + + } + closePopover={() => {}} + />, + 'String', + ]} + /> + + {/* Drag examples */} + + +

Drag examples

+
+ + {}}> + + + {(provided) => ( + + + + String + + + } + /> + )} + + + + + +

With custom styles (reduce spacing and hover styles)

+
+ + {}}> + + + {(provided) => ( +
css` + .euiFormControlLayout__prepend { + &:is(:hover, :active) { + &::before { + content: ''; + position: absolute; + inset: 0; + background-color: ${euiTheme.colors + .backgroundBaseInteractiveHover}; + } + + .euiFormAppendPrepend__dragHandle { + color: ${euiTheme.colors.textParagraph}; + } + } + } + `} + > + + + + + String + + + + } + /> +
+ )} +
+
+
+
+ + ); + }, +}; + export const HighContrast: Story = { ...AppendPrepend, tags: ['vrt-only'], diff --git a/packages/eui/src/components/form/form_control_layout/form_control_layout.styles.ts b/packages/eui/src/components/form/form_control_layout/form_control_layout.styles.ts index 5625cc6af86..ee5e5c3b2f4 100644 --- a/packages/eui/src/components/form/form_control_layout/form_control_layout.styles.ts +++ b/packages/eui/src/components/form/form_control_layout/form_control_layout.styles.ts @@ -20,6 +20,9 @@ import { type EuiButtonDisplaySizes } from '../../button/button_display/_button_ import { euiFormVariables } from '../form.styles'; +export const buttonSelectors = + '*:is(.euiButton, .euiButtonEmpty, .euiButtonIcon, .euiFormAppend, .euiFormPrepend)'; + export const euiFormControlLayoutStyles = (euiThemeContext: UseEuiTheme) => { const { euiTheme } = euiThemeContext; const isRefreshVariant = isEuiThemeRefreshVariant( @@ -180,7 +183,8 @@ export const euiFormControlLayoutSideNodeStyles = ( (x, y) => (isRefreshVariant ? x : x - y * 2) ); - const buttons = '*:is(.euiButton, .euiButtonEmpty, .euiButtonIcon)'; + const appendPrepend = '*:is(.euiFormAppend, .euiFormPrepend)'; + const buttons = buttonSelectors; const text = '*:is(.euiFormLabel, .euiText)'; const appendStyles = ` @@ -241,7 +245,13 @@ export const euiFormControlLayoutSideNodeStyles = ( /* Overrides */ + :has(${appendPrepend}) > *, + ${appendPrepend} > ${buttons} { + block-size: 100%; + } + ${buttons} { + block-size: 100%; /* Override button hover/active transform */ transform: none !important; /* stylelint-disable-line declaration-no-important */ @@ -257,16 +267,6 @@ export const euiFormControlLayoutSideNodeStyles = ( overflow: hidden; text-overflow: ellipsis; } - - /* Account for button padding when spacing children */ - /* Second > * selector accounts for buttons inside popover & tooltip wrappers */ - &:not(:has(> ${buttons}:first-child, > *:first-child > ${buttons})) { - ${logicalCSS('padding-left', euiTheme.size.s)} - } - - &:not(:has(> ${buttons}:last-child, > *:last-child > ${buttons})) { - ${logicalCSS('padding-right', euiTheme.size.s)} - } `, append: css( highContrastModeStyles(euiThemeContext, { @@ -285,14 +285,19 @@ export const euiFormControlLayoutSideNodeStyles = ( }) ), uncompressed: ` - &:not(:has(> ${buttons}:first-child, > *:first-child > ${buttons})) { + /* Legacy padding styles to handle content without */ + &:not(:has(${appendPrepend})):not( + :has(> ${buttons}:first-child, > *:first-child ${buttons}) + ) { ${logicalCSS( 'padding-left', isRefreshVariant ? euiTheme.size.m : euiTheme.size.s )} } - &:not(:has(> ${buttons}:last-child, > *:last-child > ${buttons})) { + &:not(:has(${appendPrepend})):not( + :has(> ${buttons}:last-child, > *:last-child ${buttons}) + ) { ${logicalCSS( 'padding-right', isRefreshVariant ? euiTheme.size.m : euiTheme.size.s @@ -300,7 +305,7 @@ export const euiFormControlLayoutSideNodeStyles = ( } ${text} { - ${logicalCSS('padding-horizontal', euiTheme.size.xs)} + ${logicalCSS('padding-horizontal', euiTheme.size.xs)} line-height: ${uncompressedHeight}; } @@ -320,6 +325,19 @@ export const euiFormControlLayoutSideNodeStyles = ( } `, compressed: css` + /* Legacy padding styles to handle content without */ + &:not(:has(${appendPrepend})):not( + :has(> ${buttons}:first-child, > *:first-child ${buttons}) + ) { + ${logicalCSS('padding-left', euiTheme.size.s)} + } + + &:not(:has(${appendPrepend})):not( + :has(> ${buttons}:last-child, > *:last-child ${buttons}) + ) { + ${logicalCSS('padding-right', euiTheme.size.s)} + } + ${text} { ${logicalCSS('padding-horizontal', euiTheme.size.xxs)} line-height: ${compressedHeight}; diff --git a/packages/eui/src/components/form/form_control_layout/form_control_layout.tsx b/packages/eui/src/components/form/form_control_layout/form_control_layout.tsx index 1c7438f648e..0bf9bd51658 100644 --- a/packages/eui/src/components/form/form_control_layout/form_control_layout.tsx +++ b/packages/eui/src/components/form/form_control_layout/form_control_layout.tsx @@ -29,6 +29,8 @@ import { euiFormControlLayoutStyles, euiFormControlLayoutSideNodeStyles, } from './form_control_layout.styles'; +import { EuiFormAppend, EuiFormPrepend } from './append_prepend'; +import { EuiFormControlLayoutContextProvider } from './form_control_layout_context'; type StringOrReactElement = string | ReactElement; type PrependAppendType = StringOrReactElement | StringOrReactElement[]; @@ -162,53 +164,57 @@ export const EuiFormControlLayout: FunctionComponent< return (
- -
- {hasLeftIcon && ( - - )} - - {children} - - {hasRightIcons && ( - - )} -
- + +
+ {hasLeftIcon && ( + + )} + + {children} + + {hasRightIcons && ( + + )} +
+ +
); }; @@ -221,7 +227,8 @@ const EuiFormControlLayoutSideNodes: FunctionComponent<{ nodes?: PrependAppendType; // For some bizarre reason if you make this the `children` prop instead, React doesn't properly override cloned keys :| inputId?: string; compressed?: boolean; -}> = ({ side, nodes, inputId, compressed }) => { +}> = (props) => { + const { side, nodes, inputId, compressed } = props; const className = `euiFormControlLayout__${side}`; const styles = useEuiMemoizedStyles(euiFormControlLayoutSideNodeStyles); const cssStyles = [ @@ -232,15 +239,34 @@ const EuiFormControlLayoutSideNodes: FunctionComponent<{ if (!nodes) return null; + let content; + + const AppendOrPrepend = side === 'append' ? EuiFormAppend : EuiFormPrepend; + + if (Array.isArray(nodes)) { + content = React.Children.map(nodes, (node) => + typeof node === 'string' ? ( + {node} + ) : ( + node + ) + ); + } else { + content = + typeof nodes === 'string' ? ( + + ) : ( + nodes + ); + } + return (
- {React.Children.map(nodes, (node) => - typeof node === 'string' ? ( - {node} - ) : ( - node - ) - )} + {content}
); }; diff --git a/packages/eui/src/components/form/form_control_layout/form_control_layout_context.tsx b/packages/eui/src/components/form/form_control_layout/form_control_layout_context.tsx new file mode 100644 index 00000000000..25d831e47d6 --- /dev/null +++ b/packages/eui/src/components/form/form_control_layout/form_control_layout_context.tsx @@ -0,0 +1,27 @@ +/* + * 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 { createContext } from 'react'; +import { EuiFormControlLayoutProps } from './form_control_layout'; + +type FormControlLayoutContext = Pick< + EuiFormControlLayoutProps, + 'compressed' | 'inputId' +>; + +/** + * Context to share props between `EuiFormControlLayout` and passed children like e.g. EuiFormAppend/Prepend + */ +export const EuiFormControlLayoutContext = + createContext({ + compressed: false, + inputId: '', + }); + +export const EuiFormControlLayoutContextProvider = + EuiFormControlLayoutContext.Provider; diff --git a/packages/eui/src/components/form/form_control_layout/form_control_layout_delimited.stories.tsx b/packages/eui/src/components/form/form_control_layout/form_control_layout_delimited.stories.tsx index c7d392cefe5..244c0d41708 100644 --- a/packages/eui/src/components/form/form_control_layout/form_control_layout_delimited.stories.tsx +++ b/packages/eui/src/components/form/form_control_layout/form_control_layout_delimited.stories.tsx @@ -23,6 +23,7 @@ import { EuiFormControlLayoutDelimitedProps, } from './form_control_layout_delimited'; import { EuiFormControlLayoutProps } from './form_control_layout'; +import { EuiFormAppend, EuiFormPrepend } from './append_prepend'; // re-declaring the component with props as the Partial // of EuiFormControlLayoutDelimitedProps is otherwise not resolved @@ -46,7 +47,7 @@ const meta: Meta = { control: 'radio', options: [undefined, 'icon', 'text'], mapping: { - icon: , + icon: , text: 'Appended', undefined: undefined, }, @@ -55,7 +56,7 @@ const meta: Meta = { control: 'radio', options: [undefined, 'icon', 'text'], mapping: { - icon: , + icon: , text: 'Prepended', undefined: undefined, }, @@ -106,6 +107,7 @@ export const Playground: Story = { controlOnly placeholder="0" aria-label="EuiFormControlLayoutDelimited demo - start control" + id="foo" /> ), endControl: ( diff --git a/packages/eui/src/components/form/form_control_layout/index.ts b/packages/eui/src/components/form/form_control_layout/index.ts index 243fa6fb296..d25bb63fcbf 100644 --- a/packages/eui/src/components/form/form_control_layout/index.ts +++ b/packages/eui/src/components/form/form_control_layout/index.ts @@ -14,3 +14,4 @@ export { EuiFormControlLayoutIcons, type EuiFormControlLayoutIconsProps, } from './form_control_layout_icons'; +export * from './append_prepend'; diff --git a/packages/eui/src/components/form/form_label/form_label.tsx b/packages/eui/src/components/form/form_label/form_label.tsx index dff8e7bd401..1df491da3f2 100644 --- a/packages/eui/src/components/form/form_label/form_label.tsx +++ b/packages/eui/src/components/form/form_label/form_label.tsx @@ -29,7 +29,7 @@ interface EuiFormLabelCommonProps { * Default type is a `label` but can be changed to a `legend` * if using inside a `fieldset`. */ - type?: 'label' | 'legend'; + type?: 'label' | 'legend' | 'span'; } export type _EuiFormLabelProps = { @@ -44,9 +44,15 @@ export type _EuiFormLegendProps = { CommonProps & HTMLAttributes; +export type _EuiFormLabelSpanProps = { + type: 'span'; +} & EuiFormLabelCommonProps & + CommonProps & + HTMLAttributes; + export type EuiFormLabelProps = ExclusiveUnion< - _EuiFormLabelProps, - _EuiFormLegendProps + ExclusiveUnion<_EuiFormLabelProps, _EuiFormLegendProps>, + _EuiFormLabelSpanProps >; export const EuiFormLabel: FunctionComponent = ({ @@ -82,6 +88,16 @@ export const EuiFormLabel: FunctionComponent = ({ {children} ); + } else if (type === 'span') { + return ( + )} + > + {children} + + ); } else { return (