From fd2131c1e32e7d47aad5d35ad0babdb396d6f3c4 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 13 Dec 2019 14:42:18 +0300 Subject: [PATCH] UI: ConfirmButton component (#20993) * UI: ConfirmButton component * UI: add link button variant * UI: add ConfirmButton story with delete option * Chore: use ConfirmButton instead of DeleteButton * UI: remove DeleteButton * UI: rename confirmButtonVariant to confirmVariant * UI: use Form.Button in ConfirmButton * Chore: use sm ConfirmButton size after changing defaults * Revert "UI: add link button variant" This reverts commit 4372350daa72fffa386f27efbea552318f79475e. * Chore: add 'link' variant type to the Button * UI: DeleteButton component on top of ConfirmButton * Chore: use DeleteButton instead of ConfirmButton * Chore: DeleteButton, use md size by default * Chore: update test snapshots --- .../grafana-ui/src/components/Button/types.ts | 2 +- .../ConfirmButton/ConfirmButton.story.tsx | 76 +++++++++ .../ConfirmButton/ConfirmButton.test.tsx | 34 ++++ .../ConfirmButton/ConfirmButton.tsx | 160 ++++++++++++++++++ .../ConfirmButton/DeleteButton.story.tsx | 34 ++++ .../components/ConfirmButton/DeleteButton.tsx | 24 +++ .../DeleteButton/DeleteButton.story.tsx | 17 -- .../DeleteButton/DeleteButton.test.tsx | 45 ----- .../components/DeleteButton/DeleteButton.tsx | 64 ------- .../DeleteButton/_DeleteButton.scss | 50 ------ .../src/components/Forms/Button.story.tsx | 2 +- .../src/components/Forms/Button.tsx | 24 ++- .../grafana-ui/src/components/Forms/index.ts | 2 + packages/grafana-ui/src/components/index.scss | 1 - packages/grafana-ui/src/components/index.ts | 3 +- public/app/features/api-keys/ApiKeysPage.tsx | 4 +- public/app/features/teams/TeamList.tsx | 2 +- public/app/features/teams/TeamMemberRow.tsx | 4 +- .../__snapshots__/TeamList.test.tsx.snap | 21 ++- .../__snapshots__/TeamMemberRow.test.tsx.snap | 12 +- 20 files changed, 381 insertions(+), 200 deletions(-) create mode 100644 packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.story.tsx create mode 100644 packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.test.tsx create mode 100644 packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.tsx create mode 100644 packages/grafana-ui/src/components/ConfirmButton/DeleteButton.story.tsx create mode 100644 packages/grafana-ui/src/components/ConfirmButton/DeleteButton.tsx delete mode 100644 packages/grafana-ui/src/components/DeleteButton/DeleteButton.story.tsx delete mode 100644 packages/grafana-ui/src/components/DeleteButton/DeleteButton.test.tsx delete mode 100644 packages/grafana-ui/src/components/DeleteButton/DeleteButton.tsx delete mode 100644 packages/grafana-ui/src/components/DeleteButton/_DeleteButton.scss diff --git a/packages/grafana-ui/src/components/Button/types.ts b/packages/grafana-ui/src/components/Button/types.ts index c56a9b0f5cb..a163a06e8ee 100644 --- a/packages/grafana-ui/src/components/Button/types.ts +++ b/packages/grafana-ui/src/components/Button/types.ts @@ -1,6 +1,6 @@ import { GrafanaTheme } from '@grafana/data'; -export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'inverse' | 'transparent' | 'destructive'; +export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'inverse' | 'transparent' | 'destructive' | 'link'; export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg'; diff --git a/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.story.tsx b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.story.tsx new file mode 100644 index 00000000000..c2868af2505 --- /dev/null +++ b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.story.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { text, boolean, select } from '@storybook/addon-knobs'; +import { ConfirmButton } from './ConfirmButton'; +import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; +import { action } from '@storybook/addon-actions'; +import { Button } from '../Button/Button'; + +const getKnobs = () => { + return { + buttonText: text('Button text', 'Edit'), + confirmText: text('Confirm text', 'Save'), + size: select('Size', ['sm', 'md', 'lg'], 'md'), + confirmVariant: select( + 'Confirm variant', + { + primary: 'primary', + secondary: 'secondary', + danger: 'danger', + inverse: 'inverse', + transparent: 'transparent', + }, + 'primary' + ), + disabled: boolean('Disabled', false), + }; +}; + +storiesOf('UI/ConfirmButton', module) + .addDecorator(withCenteredStory) + .add('default', () => { + const { size, buttonText, confirmText, confirmVariant, disabled } = getKnobs(); + return ( + <> +
+
+ { + action('Saved')('save!'); + }} + > + {buttonText} + +
+
+ + ); + }) + .add('with custom button', () => { + const { buttonText, confirmText, confirmVariant, disabled, size } = getKnobs(); + return ( + <> +
+
+ { + action('Saved')('save!'); + }} + > + + +
+
+ + ); + }); diff --git a/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.test.tsx b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.test.tsx new file mode 100644 index 00000000000..b708e3cf226 --- /dev/null +++ b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.test.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { ConfirmButton } from './ConfirmButton'; +import { mount, ShallowWrapper } from 'enzyme'; +import { Button } from '../Button/Button'; + +describe('ConfirmButton', () => { + let wrapper: any; + let deleted: any; + + beforeAll(() => { + deleted = false; + + function deleteItem() { + deleted = true; + } + + wrapper = mount( + deleteItem()}> + Delete + + ); + }); + + it('should show confirm delete when clicked', () => { + expect(deleted).toBe(false); + wrapper + .find(Button) + .findWhere((n: ShallowWrapper) => { + return n.text() === 'Confirm delete' && n.type() === Button; + }) + .simulate('click'); + expect(deleted).toBe(true); + }); +}); diff --git a/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.tsx b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.tsx new file mode 100644 index 00000000000..5963d3024ee --- /dev/null +++ b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.tsx @@ -0,0 +1,160 @@ +import React, { PureComponent, SyntheticEvent } from 'react'; +import { cx, css } from 'emotion'; +import { stylesFactory, withTheme } from '../../themes'; +import { GrafanaTheme } from '@grafana/data'; +import { Themeable } from '../../types'; +import { Button } from '../Button/Button'; +import Forms from '../Forms'; +import { ButtonVariant, ButtonSize } from '../Button/types'; + +const getStyles = stylesFactory((theme: GrafanaTheme) => { + return { + buttonContainer: css` + direction: rtl; + display: flex; + align-items: center; + `, + buttonDisabled: css` + text-decoration: none; + color: ${theme.colors.text}; + opacity: 0.65; + cursor: not-allowed; + pointer-events: none; + `, + buttonShow: css` + opacity: 1; + transition: opacity 0.1s ease; + z-index: 2; + `, + buttonHide: css` + opacity: 0; + transition: opacity 0.1s ease; + z-index: 0; + `, + confirmButtonContainer: css` + overflow: hidden; + position: absolute; + z-index: 1; + `, + confirmButton: css` + display: flex; + align-items: flex-start; + `, + confirmButtonShow: css` + opacity: 1; + transition: opacity 0.08s ease-out, transform 0.1s ease-out; + transform: translateX(0); + `, + confirmButtonHide: css` + opacity: 0; + transition: opacity 0.12s ease-in, transform 0.14s ease-in; + transform: translateX(100px); + `, + }; +}); + +interface Props extends Themeable { + className?: string; + size?: ButtonSize; + confirmText?: string; + disabled?: boolean; + confirmVariant?: ButtonVariant; + + onConfirm(): void; + onClick?(): void; + onCancel?(): void; +} + +interface State { + showConfirm: boolean; +} + +class UnThemedConfirmButton extends PureComponent { + static defaultProps: Partial = { + size: 'md', + confirmText: 'Save', + disabled: false, + confirmVariant: 'primary', + }; + + state: State = { + showConfirm: false, + }; + + onClickButton = (event: SyntheticEvent) => { + if (event) { + event.preventDefault(); + } + + this.setState({ + showConfirm: true, + }); + + if (this.props.onClick) { + this.props.onClick(); + } + }; + + onClickCancel = (event: SyntheticEvent) => { + if (event) { + event.preventDefault(); + } + this.setState({ + showConfirm: false, + }); + if (this.props.onCancel) { + this.props.onCancel(); + } + }; + + render() { + const { + theme, + className, + size, + disabled, + confirmText, + confirmVariant: confirmButtonVariant, + onConfirm, + children, + } = this.props; + const styles = getStyles(theme); + const buttonClass = cx( + className, + this.state.showConfirm ? styles.buttonHide : styles.buttonShow, + disabled && styles.buttonDisabled + ); + const confirmButtonClass = cx( + styles.confirmButton, + this.state.showConfirm ? styles.confirmButtonShow : styles.confirmButtonHide + ); + const onClick = disabled ? () => {} : this.onClickButton; + + return ( + + {typeof children === 'string' ? ( + + {children} + + ) : ( + + {children} + + )} + + + + + + + + ); + } +} + +export const ConfirmButton = withTheme(UnThemedConfirmButton); +ConfirmButton.displayName = 'ConfirmButton'; diff --git a/packages/grafana-ui/src/components/ConfirmButton/DeleteButton.story.tsx b/packages/grafana-ui/src/components/ConfirmButton/DeleteButton.story.tsx new file mode 100644 index 00000000000..b4d5f62ae8b --- /dev/null +++ b/packages/grafana-ui/src/components/ConfirmButton/DeleteButton.story.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { boolean, select } from '@storybook/addon-knobs'; +import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; +import { action } from '@storybook/addon-actions'; +import { DeleteButton } from './DeleteButton'; + +const getKnobs = () => { + return { + size: select('Size', ['sm', 'md', 'lg'], 'md'), + disabled: boolean('Disabled', false), + }; +}; + +storiesOf('UI/ConfirmButton', module) + .addDecorator(withCenteredStory) + .add('delete button', () => { + const { disabled, size } = getKnobs(); + return ( + <> +
+
+ { + action('Deleted')('delete!'); + }} + /> +
+
+ + ); + }); diff --git a/packages/grafana-ui/src/components/ConfirmButton/DeleteButton.tsx b/packages/grafana-ui/src/components/ConfirmButton/DeleteButton.tsx new file mode 100644 index 00000000000..2e0fe5b02ff --- /dev/null +++ b/packages/grafana-ui/src/components/ConfirmButton/DeleteButton.tsx @@ -0,0 +1,24 @@ +import React, { FC } from 'react'; +import { ConfirmButton } from './ConfirmButton'; +import { Button } from '../Button/Button'; +import { ButtonSize } from '../Button/types'; + +interface Props { + size?: ButtonSize; + disabled?: boolean; + onConfirm(): void; +} + +export const DeleteButton: FC = ({ size, disabled, onConfirm }) => { + return ( + +