From d89ea491d1d49a03e519ac30ba4ada09b92c5904 Mon Sep 17 00:00:00 2001 From: jossmac <2730833+jossmac@users.noreply.github.com> Date: Mon, 26 Aug 2024 20:06:48 +1000 Subject: [PATCH 01/18] support "isPending" prop on `Button` component --- design-system/pkg/src/button/Button.tsx | 65 +++++++++++++++++-- design-system/pkg/src/button/l10n.json | 36 ++++++++++ .../pkg/src/button/stories/Button.stories.tsx | 37 +++++++++++ design-system/pkg/src/button/types.ts | 6 +- .../pkg/src/button/useButtonStyles.tsx | 33 ++++++---- .../pkg/src/progress/ProgressCircle.tsx | 4 +- 6 files changed, 162 insertions(+), 19 deletions(-) create mode 100644 design-system/pkg/src/button/l10n.json diff --git a/design-system/pkg/src/button/Button.tsx b/design-system/pkg/src/button/Button.tsx index 3b842508e..866430a59 100644 --- a/design-system/pkg/src/button/Button.tsx +++ b/design-system/pkg/src/button/Button.tsx @@ -1,8 +1,9 @@ import { useButton } from '@react-aria/button'; +import { useLocalizedStringFormatter } from '@react-aria/i18n'; import { useHover } from '@react-aria/interactions'; import { useLink } from '@react-aria/link'; import { filterDOMProps, mergeProps, useObjectRef } from '@react-aria/utils'; -import { ForwardedRef, forwardRef, useMemo } from 'react'; +import { ForwardedRef, forwardRef, useEffect, useMemo, useState } from 'react'; import { useProviderProps } from '@keystar/ui/core'; import { SlotProvider, useSlotProps } from '@keystar/ui/slots'; @@ -10,6 +11,7 @@ import { FocusRing } from '@keystar/ui/style'; import { Text } from '@keystar/ui/typography'; import { isReactText } from '@keystar/ui/utils'; +import localizedMessages from './l10n.json'; import { ButtonElementProps, ButtonProps, @@ -17,6 +19,7 @@ import { LinkElementProps, } from './types'; import { buttonClassList, useButtonStyles } from './useButtonStyles'; +import { ProgressCircle } from '../progress'; /** * Buttons are pressable elements that are used to trigger actions, their label @@ -104,21 +107,59 @@ const BaseButton = forwardRef(function Button( props: ButtonElementProps, forwardedRef: ForwardedRef ) { - const { children, isDisabled, ...otherProps } = props; + props = disablePendingProps(props); + const { children, isDisabled, isPending, ...otherProps } = props; + const [isProgressVisible, setIsProgressVisible] = useState(false); + const stringFormatter = useLocalizedStringFormatter(localizedMessages); const domRef = useObjectRef(forwardedRef); const { buttonProps, isPressed } = useButton(props, domRef); const { hoverProps, isHovered } = useHover({ isDisabled }); - const styleProps = useButtonStyles(props, { isHovered, isPressed }); + const styleProps = useButtonStyles(props, { isHovered, isPending: isProgressVisible, isPressed }); + + // wait a second before showing the progress indicator. for actions that + // resolve quickly, this prevents a flash of the pending treatment. + useEffect(() => { + let timeout: ReturnType; + + if (isPending) { + timeout = setTimeout(() => { + setIsProgressVisible(true); + }, 1000); + } else { + setIsProgressVisible(false); + } + return () => { + clearTimeout(timeout); + }; + }, [isPending]); + + // prevent form submission when while pending + const pendingProps = isPending ? { + onClick: (e: MouseEvent) => e.preventDefault() + } : { + onClick: () => {} // satisfy TS expectations… + }; return ( ); }); @@ -126,6 +167,22 @@ const BaseButton = forwardRef(function Button( // Utils // ----------------------------------------------------------------------------- +function disablePendingProps(props: ButtonElementProps) { + // disallow interaction while the button is pending + if (props.isPending) { + props = { ...props }; + props.onKeyDown = undefined; + props.onKeyUp = undefined; + props.onPress = undefined; + props.onPressChange = undefined; + props.onPressEnd = undefined; + props.onPressStart = undefined; + props.onPressUp = undefined; + } + + return props; +} + export const useButtonChildren = (props: CommonButtonProps) => { const { children } = props; diff --git a/design-system/pkg/src/button/l10n.json b/design-system/pkg/src/button/l10n.json new file mode 100644 index 000000000..0d17f1584 --- /dev/null +++ b/design-system/pkg/src/button/l10n.json @@ -0,0 +1,36 @@ +{ + "ar-AE": { "pending": "قيد الانتظار" }, + "bg-BG": { "pending": "недовършено" }, + "cs-CZ": { "pending": "čeká na vyřízení" }, + "da-DK": { "pending": "afventende" }, + "de-DE": { "pending": "Ausstehend" }, + "el-GR": { "pending": "σε εκκρεμότητα" }, + "en-US": { "pending": "pending" }, + "es-ES": { "pending": "pendiente" }, + "et-EE": { "pending": "ootel" }, + "fi-FI": { "pending": "odottaa" }, + "fr-FR": { "pending": "En attente" }, + "he-IL": { "pending": "ממתין ל" }, + "hr-HR": { "pending": "u tijeku" }, + "hu-HU": { "pending": "függőben levő" }, + "it-IT": { "pending": "in sospeso" }, + "ja-JP": { "pending": "保留" }, + "ko-KR": { "pending": "보류 중" }, + "lt-LT": { "pending": "laukiama" }, + "lv-LV": { "pending": "gaida" }, + "nb-NO": { "pending": "avventer" }, + "nl-NL": { "pending": "in behandeling" }, + "pl-PL": { "pending": "oczekujące" }, + "pt-BR": { "pending": "pendente" }, + "pt-PT": { "pending": "pendente" }, + "ro-RO": { "pending": "în așteptare" }, + "ru-RU": { "pending": "в ожидании" }, + "sk-SK": { "pending": "čakajúce" }, + "sl-SI": { "pending": "v teku" }, + "sr-SP": { "pending": "nerešeno" }, + "sv-SE": { "pending": "väntande" }, + "tr-TR": { "pending": "beklemede" }, + "uk-UA": { "pending": "в очікуванні" }, + "zh-CN": { "pending": "待处理" }, + "zh-TW": { "pending": "待處理" } +} diff --git a/design-system/pkg/src/button/stories/Button.stories.tsx b/design-system/pkg/src/button/stories/Button.stories.tsx index 51e320353..0c0b6c183 100644 --- a/design-system/pkg/src/button/stories/Button.stories.tsx +++ b/design-system/pkg/src/button/stories/Button.stories.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { action } from '@keystar/ui-storybook'; import { plusCircleIcon } from '@keystar/ui/icon/icons/plusCircleIcon'; @@ -149,6 +150,42 @@ Anchor.story = { name: 'anchor', }; +export const Pending = () => { + return ( + + Default + Low prominence + High prominence +
{ + e.preventDefault(); + action('submit')(e); + }}> + Submit +
+
+ ); +}; + +function SimulatedPendingButton(props: any) { + let [isPending, setPending] = useState(false); + + let handlePress = (e: any) => { + action('press')(e); + setPending(true); + setTimeout(() => { + setPending(false); + }, 5000); + }; + + return ( +