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..9aa49d367b2 --- /dev/null +++ b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.story.tsx @@ -0,0 +1,73 @@ +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'), + 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 { buttonText, confirmText, confirmVariant, disabled } = getKnobs(); + return ( + <> +
+
+ { + action('Saved')('save!'); + }} + > + {buttonText} + +
+
+ + ); + }) + .add('with custom button', () => { + const { buttonText, confirmText, confirmVariant, disabled } = 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..9882bff3526 --- /dev/null +++ b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.tsx @@ -0,0 +1,148 @@ +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 { ButtonVariant } 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; + confirmText?: string; + disabled?: boolean; + confirmButtonVariant?: ButtonVariant; + + onConfirm(): void; + onClick?(): void; + onCancel?(): void; +} + +interface State { + showConfirm: boolean; +} + +class UnThemedConfirmButton extends PureComponent { + static defaultProps: Partial = { + confirmText: 'Save', + disabled: false, + confirmButtonVariant: '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 { onConfirm, disabled, confirmText, confirmButtonVariant, className, theme, 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} + + )} + + + + + + + + ); + } +} + +export const ConfirmButton = withTheme(UnThemedConfirmButton); +ConfirmButton.displayName = 'ConfirmButton'; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index f85af5c28f2..7f314ee569c 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -1,4 +1,5 @@ export { DeleteButton } from './DeleteButton/DeleteButton'; +export { ConfirmButton } from './ConfirmButton/ConfirmButton'; export { Tooltip, PopoverContent } from './Tooltip/Tooltip'; export { PopoverController } from './Tooltip/PopoverController'; export { Popover } from './Tooltip/Popover';