From 07373705d9b64c6db74333433e61707556a6aaf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Fern=C3=A1ndez?= Date: Mon, 24 Apr 2023 12:42:42 +0200 Subject: [PATCH] Grafana UI: Improve AutoSaveField component (#66751) --- .../AutoSaveField/AutoSaveField.tsx | 33 ++++--- .../AutoSaveField/EllipsisAnimated.tsx | 91 +++++++++++++++++++ .../components/InlineToast/InlineToast.tsx | 2 +- 3 files changed, 112 insertions(+), 14 deletions(-) create mode 100644 packages/grafana-ui/src/components/AutoSaveField/EllipsisAnimated.tsx diff --git a/packages/grafana-ui/src/components/AutoSaveField/AutoSaveField.tsx b/packages/grafana-ui/src/components/AutoSaveField/AutoSaveField.tsx index cd4d5308941..9aa84af51e0 100644 --- a/packages/grafana-ui/src/components/AutoSaveField/AutoSaveField.tsx +++ b/packages/grafana-ui/src/components/AutoSaveField/AutoSaveField.tsx @@ -1,20 +1,12 @@ +import { css } from '@emotion/css'; import { debounce } from 'lodash'; import React, { useCallback, useMemo, useRef } from 'react'; +import { useStyles2 } from '../../themes'; import { Field, FieldProps } from '../Forms/Field'; import { InlineToast } from '../InlineToast/InlineToast'; -/** -1.- Use the Input component as a base -2.- Just save if there is any change and when it loses focus -3.- Set the loading to true while the backend is saving -4.- Be aware of the backend response. If there is an error show a proper message and return the focus to the input. -5.- Add aria-live="polite" and check how it works in a screen-reader. -Debounce instead of working with onBlur? -import debouncePromise from 'debounce-promise'; -or -import { debounce} from 'lodash'; - */ +import { EllipsisAnimated } from './EllipsisAnimated'; const SHOW_SUCCESS_DURATION = 2 * 1000; @@ -88,20 +80,22 @@ export function AutoSaveField(props: Props) { const lodashDebounce = useMemo(() => debounce(handleChange, 600, { leading: false }), [handleChange]); //We never want to pass false to field, because it won't be deleted with deleteUndefinedProps() being false const isInvalid = invalid || fieldState.showError || undefined; - const isLoading = loading || fieldState.isLoading || undefined; /** * use Field around input to pass the error message * use InlineToast.tsx to show the save message */ + const styles = useStyles2(getStyles); + return ( <> {React.cloneElement( children((newValue) => { @@ -109,6 +103,11 @@ export function AutoSaveField(props: Props) { }) )} + {fieldState.isLoading && ( + + Saving + + )} {fieldState.showSuccess && ( (props: Props) { } AutoSaveField.displayName = 'AutoSaveField'; + +const getStyles = () => { + return { + widthFitContent: css({ + width: 'fit-content', + }), + }; +}; diff --git a/packages/grafana-ui/src/components/AutoSaveField/EllipsisAnimated.tsx b/packages/grafana-ui/src/components/AutoSaveField/EllipsisAnimated.tsx new file mode 100644 index 00000000000..1a91db3120d --- /dev/null +++ b/packages/grafana-ui/src/components/AutoSaveField/EllipsisAnimated.tsx @@ -0,0 +1,91 @@ +import { css, keyframes } from '@emotion/css'; +import React from 'react'; + +import { useStyles2 } from '../../themes'; + +export const EllipsisAnimated = React.memo(() => { + const styles = useStyles2(getStyles); + return ( +
+ . + . + . +
+ ); +}); + +EllipsisAnimated.displayName = 'EllipsisAnimated'; + +const getStyles = () => { + return { + ellipsis: css({ + display: 'inline', + }), + firstDot: css({ + animation: `${firstDot} 2s linear infinite`, + }), + secondDot: css({ + animation: `${secondDot} 2s linear infinite`, + }), + thirdDot: css({ + animation: `${thirdDot} 2s linear infinite`, + }), + }; +}; + +const firstDot = keyframes` + 0% { + opacity: 1; + } + 65% { + opacity: 1; + } + 66% { + opacity: 0.5; + } + 100% { + opacity: 0; + } + `; + +const secondDot = keyframes` + 0% { + opacity: 0; + } + 21% { + opacity: 0.5; + } + 22% { + opacity: 1; + } + 65% { + opacity: 1; + } + 66% { + opacity: 0.5; + } + 100% { + opacity: 0; + } + `; + +const thirdDot = keyframes` + 0% { + opacity: 0; + } + 43% { + opacity: 0.5; + } + 44% { + opacity: 1; + } + 65% { + opacity: 1; + } + 66% { + opacity: 0.5; + } + 100% { + opacity: 0; + } + `; diff --git a/packages/grafana-ui/src/components/InlineToast/InlineToast.tsx b/packages/grafana-ui/src/components/InlineToast/InlineToast.tsx index 67fa6dab9a4..8488682a3d2 100644 --- a/packages/grafana-ui/src/components/InlineToast/InlineToast.tsx +++ b/packages/grafana-ui/src/components/InlineToast/InlineToast.tsx @@ -15,7 +15,7 @@ export interface InlineToastProps { suffixIcon?: IconName; referenceElement: HTMLElement | null; placement: BasePlacement; - // Placement to use if there is not enough space to show the full toast with the original placement + /** Placement to use if there is not enough space to show the full toast with the original placement*/ alternativePlacement?: BasePlacement; }