diff --git a/.changeset/pink-cougars-invent.md b/.changeset/pink-cougars-invent.md new file mode 100644 index 000000000..43f7fccd7 --- /dev/null +++ b/.changeset/pink-cougars-invent.md @@ -0,0 +1,21 @@ +--- +'@keystar/ui': patch +--- + +Misc. fixes and updates. + +Fixes: + +- Allow "focus" method on `Picker` ref +- Defensive "current" selector on `NavItem` styles +- Fix text truncation on `Picker` selected text +- Clear slots of `Content` children—resolves issue with `Calendar` elements within `Dialog` receiving incorrect props +- Fix issue with `Tray` when "size" provided to `Dialog` component + +Updates: + +- Support "isPending" prop on `Button` +- Support "low" prominence `Checkbox` +- Emphasise "selected" state on `ActionButton` +- More prominent `ActionBar` +- Increase `TextArea` min-height to 3 lines \ No newline at end of file diff --git a/design-system/pkg/src/action-bar/ActionBar.tsx b/design-system/pkg/src/action-bar/ActionBar.tsx index d3af825b4..3a46e66cf 100644 --- a/design-system/pkg/src/action-bar/ActionBar.tsx +++ b/design-system/pkg/src/action-bar/ActionBar.tsx @@ -31,8 +31,6 @@ import localizedMessages from './l10n.json'; import { ActionBarProps } from './types'; import { actionbarClassList } from './class-list'; -const styles = {}; - function ActionBar( props: ActionBarProps, forwardedRef: ForwardedRef @@ -97,6 +95,7 @@ function ActionBarInner( } }, [stringFormatter]); + // FIXME: style props are passed to both the root and the bar elements return (
( className={classNames( css({ alignItems: 'center', - backgroundColor: tokenSchema.color.background.surface, - border: `${tokenSchema.size.border.regular} solid ${tokenSchema.color.border.neutral}`, - borderRadius: tokenSchema.size.radius.medium, + backgroundColor: tokenSchema.color.background.canvas, + border: `${tokenSchema.size.border.regular} solid ${tokenSchema.color.border.emphasis}`, + borderRadius: tokenSchema.size.radius.regular, + boxShadow: `0 1px 4px ${tokenSchema.color.shadow.regular}`, display: 'grid', gap: tokenSchema.size.space.small, gridTemplateAreas: '"clear selected . actiongroup"', @@ -150,8 +150,7 @@ function ActionBarInner( transform: 'translateY(0)', }, }), - actionbarClassList.element('bar'), - styleProps.className + actionbarClassList.element('bar') )} > ( buttonLabelBehavior="collapse" onAction={onAction} gridArea="actiongroup" - UNSAFE_className={classNames( - styles, - 'react-spectrum-ActionBar-actionGroup' - )} > {children} diff --git a/design-system/pkg/src/action-group/ActionGroup.tsx b/design-system/pkg/src/action-group/ActionGroup.tsx index 82dd67e25..9c7588fb6 100644 --- a/design-system/pkg/src/action-group/ActionGroup.tsx +++ b/design-system/pkg/src/action-group/ActionGroup.tsx @@ -329,10 +329,11 @@ function ActionGroup( '&:not(:last-of-type)': { marginRight: `calc(${tokenSchema.size.border.regular} * -1)`, }, - '&.is-hovered, &.is-focused, &.is-pressed': { - zIndex: 1, - }, - '&.is-selected': { + '&[data-interaction=hover], &[data-focus=visible], &[data-interaction=press]': + { + zIndex: 1, + }, + '&[data-selected]': { zIndex: 2, }, }, diff --git a/design-system/pkg/src/button/Button.tsx b/design-system/pkg/src/button/Button.tsx index 3b842508e..42adbcb62 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,65 @@ 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 +173,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/FieldButton.tsx b/design-system/pkg/src/button/FieldButton.tsx index 27ada12e2..605bfff86 100644 --- a/design-system/pkg/src/button/FieldButton.tsx +++ b/design-system/pkg/src/button/FieldButton.tsx @@ -74,7 +74,7 @@ export function useFieldButton( ) { let { isHovered, isPressed } = state; const styleProps = useActionButtonStyles(props, { isHovered, isPressed }); - let slots = useMemo(() => ({ text: { flex: true, truncate: true } }), []); + let slots = useMemo(() => ({ text: { flex: true } }), []); let children = useActionButtonChildren(props, slots); return { children, styleProps }; 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..a4db48168 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