From a7576c6f5d02e8ca5fc3bdbf5a097ae71e5dd957 Mon Sep 17 00:00:00 2001 From: Andrey Morozov Date: Mon, 20 May 2024 02:38:28 +0300 Subject: [PATCH] feat(Button): allow to pass all corresponding html props --- src/components/Alert/Alert.tsx | 4 +- src/components/Alert/types.ts | 2 +- src/components/Button/Button.tsx | 142 ++++++++++-------- src/components/Button/README.md | 42 ++---- .../Button/__stories__/Button.stories.tsx | 7 +- .../Button/__tests__/Button.test.tsx | 8 +- .../ClipboardButton/ClipboardButton.tsx | 15 +- .../Dialog/ButtonClose/ButtonClose.tsx | 4 +- .../Dialog/DialogFooter/DialogFooter.tsx | 4 +- src/components/Label/Label.tsx | 4 +- src/components/Palette/Palette.tsx | 2 +- src/components/Popover/Popover.tsx | 4 +- .../Popover/__stories__/Popover.stories.tsx | 14 +- .../Select/__stories__/SelectShowcase.tsx | 10 +- .../WithTableSettingsCustomActions.tsx | 5 +- .../hoc/withTableActions/withTableActions.tsx | 8 +- .../TableColumnSetup/TableColumnSetup.tsx | 2 +- .../withTableSettings/withTableSettings.tsx | 2 +- src/components/Toaster/Toast/Toast.tsx | 2 +- .../Tooltip/__stories__/Tooltip.stories.tsx | 2 +- .../WithGroupSelectionAndCustomIconStory.tsx | 6 +- .../stories/WithItemLinksAndActionsStory.tsx | 6 +- .../__stories__/TextInputShowcase.tsx | 2 +- .../common/ClearButton/ClearButton.tsx | 6 +- src/demo/colors/ColorPanel.tsx | 4 +- .../BrandingConfigurator.tsx | 4 +- .../PaletteGenerator/PaletteGenerator.tsx | 4 +- 27 files changed, 138 insertions(+), 177 deletions(-) diff --git a/src/components/Alert/Alert.tsx b/src/components/Alert/Alert.tsx index 97e75f23ae..25295ee371 100644 --- a/src/components/Alert/Alert.tsx +++ b/src/components/Alert/Alert.tsx @@ -68,9 +68,7 @@ export const Alert = (props: AlertProps) => { view="flat" className={bAlert('close-btn')} onClick={onClose} - extraProps={{ - 'aria-label': i18n('label_close'), - }} + aria-label={i18n('label_close')} > | React.AnchorHTMLAttributes; - onClick?: React.MouseEventHandler; - onMouseEnter?: React.MouseEventHandler; - onMouseLeave?: React.MouseEventHandler; - onFocus?: React.FocusEventHandler; - onBlur?: React.FocusEventHandler; /** Button content. You can mix button text with `` component */ children?: React.ReactNode; } +type AnchorOnlyHTMLAttributes = Omit< + React.AnchorHTMLAttributes, + keyof React.ButtonHTMLAttributes +>; + +export type ButtonButtonProps = ButtonBaseProps & + React.ButtonHTMLAttributes & { + [K in keyof AnchorOnlyHTMLAttributes]: never; + } & { + type?: React.ButtonHTMLAttributes['type']; + }; + +export type ButtonLinkProps = ButtonBaseProps & + React.AnchorHTMLAttributes & { + href: string; + }; + +export type ButtonProps = ButtonButtonProps | ButtonLinkProps; + const b = block('button'); -const ButtonWithHandlers = React.forwardRef(function Button( - { +const ButtonWithHandlers = React.forwardRef(function Button(props, ref) { + const { view = 'normal', size = 'm', pin = 'round-round', selected, - disabled = false, + disabled: disabledProp = false, loading = false, width, - title, - tabIndex, - type = 'button', - component, - href, - target, - rel, - extraProps, - onClick, - onMouseEnter, - onMouseLeave, - onFocus, - onBlur, children, - id, - style, - className, qa, - }, - ref, -) { + onClickCapture, + className, + component, + extraProps, + ...restProps + } = props; const handleClickCapture = React.useCallback( - (event: React.SyntheticEvent) => { + (event: React.MouseEvent) => { eventBroker.publish({ componentId: 'Button', eventId: 'click', @@ -125,28 +122,24 @@ const ButtonWithHandlers = React.forwardRef(function B view, }, }); + + if (onClickCapture) { + onClickCapture(event as any); + } }, - [view], + [view, onClickCapture], ); + const disabled = disabledProp || loading; const commonProps = { - title, - tabIndex, - onClick, onClickCapture: handleClickCapture, - onMouseEnter, - onMouseLeave, - onFocus, - onBlur, - id, - style, className: b( { view, size, pin, selected, - disabled: disabled || loading, + disabled, loading, width, }, @@ -154,35 +147,56 @@ const ButtonWithHandlers = React.forwardRef(function B ), 'data-qa': qa, }; + const content = prepareChildren(children); - if (typeof href === 'string' || component) { - const linkProps = { - href, - target, - rel: target === '_blank' && !rel ? 'noopener noreferrer' : rel, - }; + if (component) { return React.createElement( - component || 'a', + component, { - ...extraProps, ...commonProps, - ...(component ? {} : linkProps), - ref: ref as React.Ref, - 'aria-disabled': disabled || loading, + ...restProps, + ...extraProps, + ref, + tabIndex: disabled ? undefined : 0, + role: 'button', + 'aria-disabled': disabled, + 'aria-pressed': selected, }, - prepareChildren(children), + content, + ); + } else if (restProps.href) { + const linkProps = restProps as ButtonLinkProps; + + return ( + )} + ref={ref as React.Ref} + rel={ + linkProps.target === '_blank' && !linkProps.rel + ? 'noopener noreferrer' + : linkProps.rel + } + aria-disabled={disabled} + > + {content} + ); } else { + const buttonProps = restProps as ButtonButtonProps; + return ( ); } diff --git a/src/components/Button/README.md b/src/components/Button/README.md index ad8a85289b..90157eda81 100644 --- a/src/components/Button/README.md +++ b/src/components/Button/README.md @@ -449,33 +449,21 @@ LANDING_BLOCK--> ## Properties -| Name | Description | Type | Default | -| :----------- | :-------------------------------------------------------- | :-----------------------------: | :-------------: | -| children | Button content. You can mix text with `` component | `ReactNode` | | -| className | HTML `class` attribute | `string` | | -| component | Overrides the root component | `ElementType` | `"button"` | -| disabled | Toggles `disabled` state | `false` | `false` | -| extraProps | Any additional props | `Record` | | -| href | HTML `href` attribute | `string` | | -| id | HTML `id` attribute | `string` | | -| loading | Toggles `loading` state | `false` | `false` | -| onBlur | `blur` event handler | `Function` | | -| onClick | `click` event handler | `Function` | | -| onFocus | `focus` event handler | `Function` | | -| onMouseEnter | `mouseenter` event handler | `Function` | | -| onMouseLeave | `mouseleave` event handler | `Function` | | -| pin | Sets button edges style | `string` | `"round-round"` | -| qa | HTML `data-qa` attribute, used in tests | `string` | | -| rel | HTML `rel` attribute | `string` | | -| selected | Toggles `selected` state | | | -| size | Sets button size | `string` | `"m"` | -| style | HTML `style` attribute | `React.CSSProperties` | | -| tabIndex | HTML `tabIndex` attribute | `number` | | -| target | HTML `target` attribute | `string` | | -| title | HTML `title` attribute | `string` | | -| type | HTML `type` attribute | `"button"` `"submit"` `"reset"` | `"button"` | -| view | Sets button appearance | `string` | `"normal"` | -| width | `"auto"` `"max"` | `"auto"` `"max"` | | +`ButtonProps` extends `React.ButtonHTMLAttributes` or `React.AnchorHTMLAttributes` based on passed `href` prop. + +| Name | Description | Type | Default | +| :-------- | :-------------------------------------------------------- | :----------------: | :-------------: | +| children | Button content. You can mix text with `` component | `ReactNode` | | +| component | Overrides the root component | `ElementType` | `"button"` | +| disabled | Toggles `disabled` state | `false` | `false` | +| href | HTML `href` attribute, forces to render an `` element | `string` | | +| loading | Toggles `loading` state | `false` | `false` | +| pin | Sets button edges style | `string` | `"round-round"` | +| qa | HTML `data-qa` attribute, used in tests | `string` | | +| selected | Toggles `selected` state | | | +| size | Sets button size | `string` | `"m"` | +| view | Sets button appearance | `string` | `"normal"` | +| width | `"auto"` `"max"` | `"auto"` `"max"` | | ## CSS API diff --git a/src/components/Button/__stories__/Button.stories.tsx b/src/components/Button/__stories__/Button.stories.tsx index 68a5363f5a..087637dc54 100644 --- a/src/components/Button/__stories__/Button.stories.tsx +++ b/src/components/Button/__stories__/Button.stories.tsx @@ -12,6 +12,7 @@ import type {Meta, StoryObj} from '@storybook/react'; import {Showcase} from '../../../demo/Showcase'; import {Icon as IconComponent} from '../../Icon/Icon'; +import type {ButtonLinkProps} from '../Button'; import {Button} from '../Button'; import {ButtonViewShowcase} from './ButtonViewShowcase'; @@ -37,7 +38,7 @@ export default { }, }, }, -} as Meta; +} as Meta; type Story = StoryObj; @@ -158,7 +159,7 @@ export const Pin: Story = { }, }; -export const Link: Story = { +export const Link: StoryObj = { args: { ...Default.args, children: ['Link Button', ], @@ -180,7 +181,7 @@ export const InsideText: Story = { {' '} amet diff --git a/src/components/Button/__tests__/Button.test.tsx b/src/components/Button/__tests__/Button.test.tsx index 939202e670..6deea76531 100644 --- a/src/components/Button/__tests__/Button.test.tsx +++ b/src/components/Button/__tests__/Button.test.tsx @@ -5,7 +5,7 @@ import userEvent from '@testing-library/user-event'; import {render, screen} from '../../../../test-utils/utils'; import {Button} from '../Button'; -import type {ButtonPin, ButtonProps, ButtonSize, ButtonView} from '../Button'; +import type {ButtonPin, ButtonSize, ButtonView} from '../Button'; const qaId = 'button-component'; @@ -105,11 +105,11 @@ describe('Button', () => { test('should render custom component', () => { const text = 'Button with custom component'; - const ButtonComponent = (props: ButtonProps) => { + const ButtonComponent = (props: React.HTMLAttributes) => { return ( - + ); }; diff --git a/src/components/ClipboardButton/ClipboardButton.tsx b/src/components/ClipboardButton/ClipboardButton.tsx index e260292e37..db3acc6f22 100644 --- a/src/components/ClipboardButton/ClipboardButton.tsx +++ b/src/components/ClipboardButton/ClipboardButton.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {ActionTooltip} from '../ActionTooltip'; import {Button} from '../Button'; -import type {ButtonProps, ButtonSize} from '../Button'; +import type {ButtonButtonProps, ButtonSize} from '../Button'; import {ClipboardIcon} from '../ClipboardIcon'; import {CopyToClipboard} from '../CopyToClipboard'; import type {CopyToClipboardProps, CopyToClipboardStatus} from '../CopyToClipboard/types'; @@ -14,7 +14,7 @@ export interface ClipboardButtonProps Omit {} interface ClipboardButtonComponentProps - extends Omit { + extends Omit { status: CopyToClipboardStatus; /** Disable tooltip. Tooltip won't be shown */ hasTooltip?: boolean; @@ -42,7 +42,6 @@ const ClipboardButtonComponent = (props: ClipboardButtonComponentProps) => { tooltipSuccessText = i18n('endCopy'), status, view = 'flat', - extraProps = {}, ...rest } = props; @@ -51,15 +50,7 @@ const ClipboardButtonComponent = (props: ClipboardButtonComponentProps) => { disabled={!hasTooltip} title={status === 'success' ? tooltipSuccessText : tooltipInitialText} > - diff --git a/src/components/Dialog/DialogFooter/DialogFooter.tsx b/src/components/Dialog/DialogFooter/DialogFooter.tsx index 1834b5f848..56b91dc284 100644 --- a/src/components/Dialog/DialogFooter/DialogFooter.tsx +++ b/src/components/Dialog/DialogFooter/DialogFooter.tsx @@ -16,8 +16,8 @@ interface DialogFooterOwnProps { onClickButtonCancel?: (event: React.MouseEvent) => void; textButtonCancel?: string; textButtonApply?: string; - propsButtonCancel?: Partial; - propsButtonApply?: Partial; + propsButtonCancel?: ButtonProps; + propsButtonApply?: ButtonProps; loading?: boolean; children?: React.ReactNode; errorText?: string; diff --git a/src/components/Label/Label.tsx b/src/components/Label/Label.tsx index 9bbb955373..4891c5d5a5 100644 --- a/src/components/Label/Label.tsx +++ b/src/components/Label/Label.tsx @@ -118,9 +118,9 @@ export const Label = React.forwardRef(function Label( actionButton = ( diff --git a/src/components/Popover/__stories__/Popover.stories.tsx b/src/components/Popover/__stories__/Popover.stories.tsx index 2fb26348b9..9fa6dd8ae9 100644 --- a/src/components/Popover/__stories__/Popover.stories.tsx +++ b/src/components/Popover/__stories__/Popover.stories.tsx @@ -213,11 +213,9 @@ const AccessibleTemplate: StoryFn = () => { {({onClick, open}) => ( @@ -244,12 +242,10 @@ const AccessibleTemplate: StoryFn = () => { {({onClick}) => ( diff --git a/src/components/Select/__stories__/SelectShowcase.tsx b/src/components/Select/__stories__/SelectShowcase.tsx index b4bb839588..9860f73fa9 100644 --- a/src/components/Select/__stories__/SelectShowcase.tsx +++ b/src/components/Select/__stories__/SelectShowcase.tsx @@ -272,9 +272,7 @@ export const SelectShowcase = (props: SelectProps) => { ref={ref} view="action" onClick={onClick} - extraProps={{ - onKeyDown, - }} + onKeyDown={onKeyDown} className={b({'has-clear': props.hasClear})} > User control @@ -306,10 +304,8 @@ export const SelectShowcase = (props: SelectProps) => { ref={ref} view="action" onClick={onClick} - extraProps={{ - onKeyDown, - 'aria-label': 'Add', - }} + onKeyDown={onKeyDown} + aria-label="Add" > diff --git a/src/components/Table/__stories__/WithTableSettingsCustomActions/WithTableSettingsCustomActions.tsx b/src/components/Table/__stories__/WithTableSettingsCustomActions/WithTableSettingsCustomActions.tsx index 59b73ee674..bc6097e177 100644 --- a/src/components/Table/__stories__/WithTableSettingsCustomActions/WithTableSettingsCustomActions.tsx +++ b/src/components/Table/__stories__/WithTableSettingsCustomActions/WithTableSettingsCustomActions.tsx @@ -4,7 +4,6 @@ import {ArrowRotateLeft} from '@gravity-ui/icons'; import _isEqual from 'lodash/isEqual'; import {Button} from '../../../Button'; -import type {ButtonProps} from '../../../Button'; import {Icon} from '../../../Icon'; import {Flex} from '../../../layout'; import type {TableProps} from '../../Table'; @@ -72,11 +71,11 @@ export const WithTableSettingsCustomActionsShowcase = ({ ); }; -function SelectAllButton({onClick}: T) { +function SelectAllButton({onClick}: {onClick: React.MouseEventHandler}) { return ; } -function ResetButton({onClick}: T) { +function ResetButton({onClick}: {onClick: React.MouseEventHandler}) { return ( diff --git a/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx b/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx index 4f01d1ffa2..0d1457c676 100644 --- a/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx +++ b/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx @@ -349,7 +349,7 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => { return ( renderSwitcher?.({onClick: toggleOpen, onKeyDown}) || ( - diff --git a/src/components/Table/hoc/withTableSettings/withTableSettings.tsx b/src/components/Table/hoc/withTableSettings/withTableSettings.tsx index 68871c0adf..08d8f485a9 100644 --- a/src/components/Table/hoc/withTableSettings/withTableSettings.tsx +++ b/src/components/Table/hoc/withTableSettings/withTableSettings.tsx @@ -205,8 +205,8 @@ export function withTableSettings( diff --git a/src/components/Toaster/Toast/Toast.tsx b/src/components/Toaster/Toast/Toast.tsx index d678c70f9c..73e7b2f652 100644 --- a/src/components/Toaster/Toast/Toast.tsx +++ b/src/components/Toaster/Toast/Toast.tsx @@ -119,7 +119,7 @@ export const Toast = React.forwardRef(function view="flat" className={b('btn-close')} onClick={onClose} - extraProps={{'aria-label': i18n('label_close-button')}} + aria-label={i18n('label_close-button')} > diff --git a/src/components/Tooltip/__stories__/Tooltip.stories.tsx b/src/components/Tooltip/__stories__/Tooltip.stories.tsx index 58d2017d95..63108c8be1 100644 --- a/src/components/Tooltip/__stories__/Tooltip.stories.tsx +++ b/src/components/Tooltip/__stories__/Tooltip.stories.tsx @@ -18,7 +18,7 @@ export const Default: Story = { render: (args) => { return ( - + ); }, diff --git a/src/components/TreeList/__stories__/stories/WithGroupSelectionAndCustomIconStory.tsx b/src/components/TreeList/__stories__/stories/WithGroupSelectionAndCustomIconStory.tsx index 80769a6a18..d59bfe7770 100644 --- a/src/components/TreeList/__stories__/stories/WithGroupSelectionAndCustomIconStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithGroupSelectionAndCustomIconStory.tsx @@ -93,11 +93,7 @@ export const WithGroupSelectionAndCustomIconStory = ({ : false, })); }} - extraProps={{ - 'aria-label': expanded - ? closeButtonLabel - : expandButtonLabel, - }} + aria-label={expanded ? closeButtonLabel : expandButtonLabel} > diff --git a/src/components/TreeList/__stories__/stories/WithItemLinksAndActionsStory.tsx b/src/components/TreeList/__stories__/stories/WithItemLinksAndActionsStory.tsx index 1db46db3f8..0b56dfb0d5 100644 --- a/src/components/TreeList/__stories__/stories/WithItemLinksAndActionsStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithItemLinksAndActionsStory.tsx @@ -98,11 +98,7 @@ export const WithItemLinksAndActionsStory = (props: WithItemLinksAndActionsStory : false, })); }} - extraProps={{ - 'aria-label': expanded - ? closeButtonLabel - : expandButtonLabel, - }} + aria-label={expanded ? closeButtonLabel : expandButtonLabel} > diff --git a/src/components/controls/TextInput/__stories__/TextInputShowcase.tsx b/src/components/controls/TextInput/__stories__/TextInputShowcase.tsx index b87deb528e..3322a8a1da 100644 --- a/src/components/controls/TextInput/__stories__/TextInputShowcase.tsx +++ b/src/components/controls/TextInput/__stories__/TextInputShowcase.tsx @@ -34,7 +34,7 @@ const EyeButton = (props: { view="flat" disabled={disabled} onClick={onClick} - extraProps={{'aria-label': opened ? showLabel : hideLabel}} + aria-label={opened ? showLabel : hideLabel} > diff --git a/src/components/controls/common/ClearButton/ClearButton.tsx b/src/components/controls/common/ClearButton/ClearButton.tsx index e435114b1c..4c7dfc4a3e 100644 --- a/src/components/controls/common/ClearButton/ClearButton.tsx +++ b/src/components/controls/common/ClearButton/ClearButton.tsx @@ -56,10 +56,8 @@ export const ClearButton = (props: Props) => { size={size} className={b(null, className)} onClick={onClick} - extraProps={{ - onMouseDown: preventDefaultHandler, - 'aria-label': i18n('label_clear-button'), - }} + onMouseDown={preventDefaultHandler} + aria-label={i18n('label_clear-button')} > diff --git a/src/demo/colors/ColorPanel.tsx b/src/demo/colors/ColorPanel.tsx index b6a70c430e..a4c8c80a21 100644 --- a/src/demo/colors/ColorPanel.tsx +++ b/src/demo/colors/ColorPanel.tsx @@ -70,9 +70,7 @@ export function ColorPanel(props: ColorPanelProps) { } className="color-panel__bg-switcher" onClick={() => rotateBackground()} - extraProps={{ - 'aria-labelledby': tooltipId, - }} + aria-labelledby={tooltipId} > diff --git a/src/stories/Branding/BrandingConfugurator/BrandingConfigurator.tsx b/src/stories/Branding/BrandingConfugurator/BrandingConfigurator.tsx index 9c6ee96eb8..b9b3e2354c 100644 --- a/src/stories/Branding/BrandingConfugurator/BrandingConfigurator.tsx +++ b/src/stories/Branding/BrandingConfugurator/BrandingConfigurator.tsx @@ -113,9 +113,7 @@ export function BrandingConfigurator({theme}: BrandingConfiguratorProps) { view="outlined" size="xl" onClick={handleRandomClick} - extraProps={{ - 'aria-label': 'Regenerate colors', - }} + aria-label="Regenerate colors" > diff --git a/src/stories/Branding/PaletteGenerator/PaletteGenerator.tsx b/src/stories/Branding/PaletteGenerator/PaletteGenerator.tsx index cce15ba98b..f5b975d2e3 100644 --- a/src/stories/Branding/PaletteGenerator/PaletteGenerator.tsx +++ b/src/stories/Branding/PaletteGenerator/PaletteGenerator.tsx @@ -167,9 +167,7 @@ export function PaletteGenerator({theme}: BrandingConfiguratorProps) { view="outlined" size="l" onClick={handleSwapContrastClick} - extraProps={{ - 'aria-label': 'Switch colors', - }} + aria-label="Switch colors" >