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

Modal components update #85

Merged
merged 35 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
85d11ec
Rework Dialog component + focus outline refactoring.
Dec 30, 2024
36562e6
Implement new ModalProvider component (first draft).
Dec 30, 2024
628cde9
Implement useControlledDialog().
Dec 30, 2024
21ec9bd
Refactor useControlledDialog().
Dec 31, 2024
e9de2df
Update ModalProvider.
Dec 31, 2024
c4d208c
Tweak plopfile.
Dec 31, 2024
71d4127
New component: DialogModal.
Dec 31, 2024
a40a837
Update DialogModal transitions.
Jan 1, 2025
a94260e
Work on DialogModal.
Jan 2, 2025
eb331f7
Remove biome exception.
Jan 2, 2025
99da117
Remove overflow-y: auto.
Jan 2, 2025
2244274
Update Dialog styling.
Jan 3, 2025
8ad56a8
Update DialogModal styling.
Jan 3, 2025
4d56ea4
Merge branch 'master' into mkrause/241230-modal-update
Jan 3, 2025
6d4bab0
Fix fractional px value rounding issues.
Jan 3, 2025
dbd42da
Fix broken sizing calculations.
Jan 3, 2025
57b760b
Implement slideOverPosition.
Jan 3, 2025
cf7a3f9
Implement ModalProviderRef.
Jan 3, 2025
5635351
Disable DialogModal animations when user has prefers-reduced-motion.
Jan 3, 2025
6c2eb63
Fix Dialog footer 1px bleed through.
Jan 4, 2025
bb0be0b
Refactor useControlledDialog -> useModalDialog.
Jan 4, 2025
4fe0d9f
Implement useModalRefWithSubject().
Jan 4, 2025
a724071
Implement DialogModal.useConfirmationModal()
Jan 4, 2025
e5d82c5
Fix build errors.
Jan 4, 2025
03710a1
Disable no-global-function-names stylelint rule until round() issue i…
Jan 4, 2025
8d62917
Add missing license header.
Jan 4, 2025
e200789
Implement SpinnerModal + allow ModalProvider to be controlled.
Jan 4, 2025
ccca5a8
Allow slide-over modals to have a customizable width.
Jan 4, 2025
22d6564
Add story for nested dialogs + fix close bubbling issue.
Jan 4, 2025
42d610b
Replace Modal component with DialogModal.
Jan 5, 2025
3828066
Work on DialogModal.
Jan 5, 2025
b3e1007
Refactor useConfirmationModal().
Jan 5, 2025
29d7446
Fix type error.
Jan 5, 2025
fa6cd5f
Merge branch 'master' into mkrause/241230-modal-update
mkrause Jan 6, 2025
6c66420
Fix review comments.
mkrause Jan 6, 2025
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
7 changes: 5 additions & 2 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,13 @@ const preview = {
],
'overlays',
[
'ModalProvider',
'SpinnerModal',
'DialogModal',
'Tooltip',
'TooltipProvider',
'Dropdown',
'Modal',
'DropdownMenu',
'Toast',
],
'lists',
[
Expand Down
4 changes: 3 additions & 1 deletion app/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,11 @@ export { Stepper } from '../src/components/navigations/Stepper/Stepper.tsx';
export { Tab, Tabs } from '../src/components/navigations/Tabs/Tabs.tsx';

// Overlays
export { ModalProvider } from '../src/components/overlays/ModalProvider/ModalProvider.tsx';
export { SpinnerModal } from '../src/components/overlays/SpinnerModal/SpinnerModal.tsx';
export { DialogModal } from '../src/components/overlays/DialogModal/DialogModal.tsx';
export { DropdownMenu } from '../src/components/overlays/DropdownMenu/DropdownMenu.tsx';
export { DropdownMenuProvider } from '../src/components/overlays/DropdownMenu/DropdownMenuProvider.tsx';
export { Modal } from '../src/components/overlays/Modal/Modal.tsx';
export { type ToastContent, ToastProvider, ToastMessage } from '../src/components/overlays/Toast/Toast.tsx';
export { Tooltip } from '../src/components/overlays/Tooltip/Tooltip.tsx';
export { TooltipProvider } from '../src/components/overlays/Tooltip/TooltipProvider.tsx';
Expand Down
1 change: 1 addition & 0 deletions package.json.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ const packageConfig = {
'react': '^19.0.0',
'react-dom': '^19.0.0',
'react-error-boundary': '^4.1.2',
//'@uidotdev/usehooks': '^2.4.1',

'@floating-ui/react': '^0.26.28',
'react-toastify': '^10.0.6',
Expand Down
28 changes: 16 additions & 12 deletions plopfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const componentTemplate = {
/** Whether this component should be unstyled. */
unstyled?: undefined | boolean,

/** Some property specific to \`{{{component-name}}}\` */
/** Some property specific to \`{{{component-name}}}\`. */
variant?: undefined | 'x' | 'y',
}>;
{{"\\n"~}}
Expand All @@ -78,12 +78,13 @@ const componentTemplate = {
return (
<{{{element-type}}}
{...propsRest}
className={cx({
bk: true,
[cl['bk-{{{kebabCase component-name}}}']]: !unstyled,
[cl['bk-{{{kebabCase component-name}}}--x']]: variant === 'x',
[cl['bk-{{{kebabCase component-name}}}--y']]: variant === 'y',
}, propsRest.className)}
className={cx(
'bk',
{ [cl['bk-{{{kebabCase component-name}}}']]: !unstyled },
{ [cl['bk-{{{kebabCase component-name}}}--x']]: variant === 'x' },
{ [cl['bk-{{{kebabCase component-name}}}--y']]: variant === 'y' },
propsRest.className,
)}
/>
);
};
Expand Down Expand Up @@ -173,21 +174,24 @@ const componentTemplate = {

export default {
component: {{{component-name}}},
tags: ['autodocs'],
parameters: {
layout: '{{storybook-layout}}',
},
tags: ['autodocs'],
argTypes: {
},
argTypes: {},
args: {
children: 'Example',
},
render: (args) => <{{{component-name}}} {...args}/>,
} satisfies Meta<{{{component-name}}}Args>;


export const Standard: Story = {
name: '{{{component-name}}}',
export const {{{component-name}}}Standard: Story = {};

export const {{{component-name}}}WithVariant: Story = {
args: {
variant: 'x',
},
};
` + '\n',
};
Expand Down
32 changes: 24 additions & 8 deletions src/components/actions/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,16 @@ export type ButtonProps = React.PropsWithChildren<Omit<ComponentProps<'button'>,
/** What variant the button is, from higher prominance to lower. */
variant?: undefined | 'primary' | 'secondary' | 'tertiary',

/**
* Whether the button is disabled. This is meant for essentially permanent disabled buttons, not for buttons that
* are just temporarily non-interactive. Use `nonactive` for the latter. Disabled buttons cannot be focused.
*/
disabled?: undefined | boolean,

/**
* Whether the button is in "nonactive" state. This is a variant of `disabled`, but instead of completely graying
* out the button, it only becomes a muted variation of the button's appearance. When true, also implies `disabled`.
* out the button, it only becomes a muted variation of the button's appearance. Nonactive buttons are useful for
* temporarily states such as while an action is currently ongoing. Nonactive buttons are still focusable.
*/
nonactive?: undefined | boolean,

Expand All @@ -53,6 +60,7 @@ export const Button = (props: ButtonProps) => {
unstyled = false,
trimmed = false,
label,
disabled = false,
nonactive = false,
variant = 'tertiary',
onPress,
Expand All @@ -63,24 +71,31 @@ export const Button = (props: ButtonProps) => {
const [isPressPending, startPressTransition] = React.useTransition();

const isPending = isPressPending;
const isInteractive = !propsRest.disabled && !nonactive && !isPending;
const isInteractive = !disabled && !nonactive && !isPending;
const isNonactive = nonactive || isPending;

const handlePress = React.useCallback(() => {
if (typeof onPress !== 'function') { return; }

startPressTransition(async () => {
await Promise.race([onPress(), timeout(asyncTimeout)]);
});
const onPressResult = onPress();

// Note: do not start a transition unless `onPress` is actually async, because otherwise sync press actions
// will cause a brief rerender with disabled state and loading indicator, which messes with things like
// button focus.
if (onPressResult instanceof Promise) {
startPressTransition(async () => {
await Promise.race([onPressResult, timeout(asyncTimeout)]);
});
}
}, [onPress, asyncTimeout]);

const handleClick = React.useCallback(async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
if (!isInteractive) { return; }

// `onClick` should not be used in most cases, only if the consumer needs low level control over click events.
// Instead, use `onPress` or a `<form>` component with `action`.
props.onClick?.(event); // Call this first, to allow cancellation

if (!isInteractive) { return; }

if (typeof onPress === 'function') {
event.preventDefault();
handlePress();
Expand Down Expand Up @@ -122,9 +137,10 @@ export const Button = (props: ButtonProps) => {
return (
<button
aria-label={accessibleName}
aria-disabled={!isInteractive}
disabled={disabled}
{...propsRest}
type={buttonType} // Not overridable
disabled={!isInteractive}
className={cx({
bk: true,
[cl['bk-button']]: !unstyled,
Expand Down
2 changes: 1 addition & 1 deletion src/components/containers/Banner/Banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const ActionIcon = ({ tooltip, ...buttonProps }: ActionIconProps) => {


export type BannerProps = Omit<ComponentProps<'div'>, 'title'> & {
/** Whether the component should include the default styling. Defaults to false. */
/** Whether this component should be unstyled. */
unstyled?: undefined | boolean,

/** Whether to trim this component (strip any spacing around the element). */
Expand Down
15 changes: 11 additions & 4 deletions src/components/containers/Dialog/Dialog.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
--bk-dialog-padding-block: #{bk.$spacing-8};
--bk-dialog-padding-inline: #{bk.$spacing-8};

// The default `auto` inherits from the parent, which we do not want. In the future if `contain: user-select`
// becomes supported, we could use that instead.
user-select: text;

margin: 0;
padding: 0; // Do not apply padding on the outermost container (do it on the lower level elements instead)

Expand All @@ -22,6 +26,8 @@
border: none;
border-radius: 0;
}
background: var(--bk-dialog-background-color);
color: bk.$theme-modal-text-default;

display: flex;
flex-direction: column;
Expand All @@ -32,10 +38,11 @@

.bk-dialog__header {
position: sticky;
top: 0;
top: -1px; // -1px to prevent bleed through due to pixel rounding
z-index: 1;

padding-block: var(--bk-dialog-padding-block) calc(bk.$spacing-7 / 2);
padding-block-start: calc(var(--bk-dialog-padding-block) + 1px); // 1px to compensate for `top: -1px`
padding-inline: var(--bk-dialog-padding-inline);
background: var(--bk-dialog-background-color);

Expand All @@ -56,6 +63,7 @@
}

.bk-dialog__content {
scroll-margin-top: 100lh; // Workaround for an issue where the content scroll behind the header on focus
flex: 1;

margin-block: calc(bk.$spacing-7 / 2) calc(bk.$spacing-7 / 2);
Expand All @@ -64,10 +72,10 @@

.bk-dialog__actions {
position: sticky;
bottom: 0;
bottom: -1px; // -1px to prevent bleed through due to pixel rounding
z-index: 1;

padding-block: calc(bk.$spacing-7 / 2) bk.$spacing-9;
padding-block: calc(bk.$spacing-7 / 2) calc(bk.$spacing-9 + 1px); // 1px to compensate for `bottom: -1px`
padding-inline: var(--bk-dialog-padding-inline);
// In Figma, actions have an indent. But that might be only if the content itself has an indent, need to check.
//padding-inline-start: calc(var(--bk-dialog-padding-inline) + bk.$spacing-8);
Expand All @@ -81,7 +89,6 @@

.bk-dialog__action { --keep: ; }
.bk-dialog__action--icon { --keep: ; }
.bk-dialog__action--button { --keep: ; }
}

// State: focus
Expand Down
6 changes: 1 addition & 5 deletions src/components/containers/Dialog/Dialog.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import type { Meta, StoryObj } from '@storybook/react';
import { LayoutDecorator } from '../../../util/storybook/LayoutDecorator.tsx';
import { loremIpsum, LoremIpsum } from '../../../util/storybook/LoremIpsum.tsx';

import { Button } from '../../actions/Button/Button.tsx';

import { Dialog } from './Dialog.tsx';


Expand All @@ -28,9 +26,7 @@ export default {
args: {
title: 'Dialog title',
children: <LoremIpsum paragraphs={3}/>,
actions: (
<Button variant="primary" label="Submit"/>
),
actions: <Dialog.SubmitAction/>,
mkrause marked this conversation as resolved.
Show resolved Hide resolved
onRequestClose: () => {},
},
} satisfies Meta<DialogArgs>;
Expand Down
Loading
Loading