Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DS fixes and updates #1275

Merged
merged 18 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/pink-cougars-invent.md
Original file line number Diff line number Diff line change
@@ -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
17 changes: 6 additions & 11 deletions design-system/pkg/src/action-bar/ActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ import localizedMessages from './l10n.json';
import { ActionBarProps } from './types';
import { actionbarClassList } from './class-list';

const styles = {};

function ActionBar<T extends object>(
props: ActionBarProps<T>,
forwardedRef: ForwardedRef<HTMLDivElement>
Expand Down Expand Up @@ -97,6 +95,7 @@ function ActionBarInner<T>(
}
}, [stringFormatter]);

// FIXME: style props are passed to both the root and the bar elements
return (
<FocusScope restoreFocus>
<div
Expand Down Expand Up @@ -129,9 +128,10 @@ function ActionBarInner<T>(
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"',
Expand All @@ -150,8 +150,7 @@ function ActionBarInner<T>(
transform: 'translateY(0)',
},
}),
actionbarClassList.element('bar'),
styleProps.className
actionbarClassList.element('bar')
)}
>
<ActionGroup
Expand All @@ -162,10 +161,6 @@ function ActionBarInner<T>(
buttonLabelBehavior="collapse"
onAction={onAction}
gridArea="actiongroup"
UNSAFE_className={classNames(
styles,
'react-spectrum-ActionBar-actionGroup'
)}
>
{children}
</ActionGroup>
Expand Down
9 changes: 5 additions & 4 deletions design-system/pkg/src/action-group/ActionGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -329,10 +329,11 @@ function ActionGroup<T extends object>(
'&: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,
},
},
Expand Down
71 changes: 67 additions & 4 deletions design-system/pkg/src/button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
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';
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,
CommonButtonProps,
LinkElementProps,
} from './types';
import { buttonClassList, useButtonStyles } from './useButtonStyles';
import { ProgressCircle } from '../progress';

/**
* Buttons are pressable elements that are used to trigger actions, their label
Expand Down Expand Up @@ -104,28 +107,88 @@ const BaseButton = forwardRef(function Button(
props: ButtonElementProps,
forwardedRef: ForwardedRef<HTMLButtonElement>
) {
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<typeof setTimeout>;

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 (
<button
ref={domRef}
{...styleProps}
{...filterDOMProps(otherProps, { propNames: new Set(['form']) })}
{...mergeProps(buttonProps, hoverProps)}
{...mergeProps(buttonProps, hoverProps, pendingProps)}
aria-disabled={isPending ? 'true' : undefined}
>
{children}
{isProgressVisible && (
<ProgressCircle
aria-atomic="false"
aria-live="assertive"
aria-label={stringFormatter.format('pending')}
isIndeterminate
size="small"
UNSAFE_style={{ position: 'absolute' }}
/>
)}
</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;

Expand Down
2 changes: 1 addition & 1 deletion design-system/pkg/src/button/FieldButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
36 changes: 36 additions & 0 deletions design-system/pkg/src/button/l10n.json
Original file line number Diff line number Diff line change
@@ -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": "待處理" }
}
37 changes: 37 additions & 0 deletions design-system/pkg/src/button/stories/Button.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useState } from 'react';
import { action } from '@keystar/ui-storybook';

import { plusCircleIcon } from '@keystar/ui/icon/icons/plusCircleIcon';
Expand Down Expand Up @@ -149,6 +150,42 @@ Anchor.story = {
name: 'anchor',
};

export const Pending = () => {
return (
<Flex direction="column" gap="regular" alignItems="start">
<SimulatedPendingButton>Default</SimulatedPendingButton>
<SimulatedPendingButton prominence="low">
Low prominence
</SimulatedPendingButton>
<SimulatedPendingButton prominence="high">
High prominence
</SimulatedPendingButton>
<form
onSubmit={e => {
e.preventDefault();
action('submit')(e);
}}
>
<SimulatedPendingButton type="submit">Submit</SimulatedPendingButton>
</form>
</Flex>
);
};

function SimulatedPendingButton(props: any) {
let [isPending, setPending] = useState(false);

let handlePress = (e: any) => {
action('press')(e);
setPending(true);
setTimeout(() => {
setPending(false);
}, 5000);
};

return <Button {...props} isPending={isPending} onPress={handlePress} />;
}

function render(label = 'Default', props: ButtonProps = {}) {
return (
<Flex gap="regular">
Expand Down
6 changes: 5 additions & 1 deletion design-system/pkg/src/button/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,11 @@ export type CommonButtonProps = {
AriaLabelingProps &
BaseStyleProps;

export type ButtonElementProps = CommonButtonProps & AriaProps;
export type ButtonElementProps = CommonButtonProps &
AriaProps & {
/** Disable events and display a progress indicator. */
isPending?: boolean;
};

export type LinkElementProps = CommonButtonProps & AnchorDOMProps;

Expand Down
32 changes: 22 additions & 10 deletions design-system/pkg/src/button/useActionButtonStyles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function useActionButtonStyles(
cursor: 'default',
display: 'inline-flex',
flexShrink: 0,
fontWeight: tokenSchema.typography.fontWeight.medium,
fontWeight: tokenSchema.typography.fontWeight.regular,
height: tokenSchema.size.element.regular,
justifyContent: 'center',
minWidth: tokenSchema.size.element.regular,
Expand Down Expand Up @@ -108,6 +108,7 @@ export function useActionButtonStyles(
'&[data-interaction=hover]': {
backgroundColor: tokenSchema.color.alias.backgroundHovered,
borderColor: tokenSchema.color.alias.borderHovered,
// boxShadow: `${tokenSchema.size.shadow.small} ${tokenSchema.color.shadow.regular}`,
color: tokenSchema.color.alias.foregroundHovered,
},
'&[data-interaction=press]': {
Expand All @@ -118,12 +119,17 @@ export function useActionButtonStyles(

// states
'&[data-selected]': {
backgroundColor: tokenSchema.color.alias.backgroundSelected,
color: tokenSchema.color.foreground.neutralEmphasis,
backgroundColor: tokenSchema.color.foreground.neutralSecondary,
borderColor: tokenSchema.color.foreground.neutralSecondary,
color: tokenSchema.color.foreground.inverse,

'&[data-interaction=hover]': {
backgroundColor:
tokenSchema.color.alias.backgroundSelectedHovered,
backgroundColor: tokenSchema.color.foreground.neutral,
borderColor: tokenSchema.color.foreground.neutral,
},
'&[data-interaction=press]': {
backgroundColor: tokenSchema.color.foreground.neutralEmphasis,
borderColor: tokenSchema.color.foreground.neutralEmphasis,
},
},
'&:disabled, &[aria-disabled=true], &[data-disabled=true]': {
Expand Down Expand Up @@ -179,20 +185,26 @@ export function useActionButtonStyles(
// interactions
'&[data-interaction=hover]': {
backgroundColor: tokenSchema.color.alias.backgroundHovered,
color: tokenSchema.color.foreground.neutralEmphasis,
color: tokenSchema.color.alias.foregroundHovered,
},
'&[data-interaction=press]': {
backgroundColor: tokenSchema.color.alias.backgroundPressed,
color: tokenSchema.color.alias.foregroundPressed,
},

// states
'&[data-selected]': {
backgroundColor: tokenSchema.color.alias.backgroundSelected,
color: tokenSchema.color.alias.foregroundSelected,
backgroundColor: tokenSchema.color.foreground.neutralSecondary,
borderColor: tokenSchema.color.foreground.neutralSecondary,
color: tokenSchema.color.foreground.inverse,

'&[data-interaction=hover]': {
backgroundColor:
tokenSchema.color.alias.backgroundSelectedHovered,
backgroundColor: tokenSchema.color.foreground.neutral,
borderColor: tokenSchema.color.foreground.neutral,
},
'&[data-interaction=press]': {
backgroundColor: tokenSchema.color.foreground.neutralEmphasis,
borderColor: tokenSchema.color.foreground.neutralEmphasis,
},
},
'&:disabled, &[aria-disabled=true], &[data-disabled=true]': {
Expand Down
Loading
Loading