Skip to content
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f39b690
refactor(dialog): migrate Dialog component to use Modal base
misama-ct Sep 8, 2025
e4ad4cd
refactor(dialog): replace LegacyDialog with Modal in navigation compo…
misama-ct Sep 8, 2025
82aa186
refactor(modal): streamline component structure and enhance accessibi…
misama-ct Sep 9, 2025
062230b
feat(intl): integrate IntlProvider for internationalization support
misama-ct Sep 9, 2025
c7b3fff
refactor(dialog): transition Modal components to Dialog structure
misama-ct Sep 9, 2025
d88162a
refactor(search): replace Modal with Dialog components in AppNavBarSe…
misama-ct Sep 9, 2025
baf8746
chore(settings): remove deprecated settings.json file
misama-ct Sep 10, 2025
eaea330
refactor(dialog): update Dialog components to use local refs and impr…
misama-ct Sep 10, 2025
6aa9bc5
refactor(dialog): simplify Dialog component structure and remove size…
misama-ct Sep 10, 2025
344a4fe
feat(dialog): add ButtonAsTrigger story for custom button integration
misama-ct Sep 10, 2025
7e6140e
refactor(dialog): implement Dialog context for improved configuration…
misama-ct Sep 10, 2025
43f5ed0
refactor(dialog): remove motion presets and streamline dialog configu…
misama-ct Sep 11, 2025
5dbe91e
feat(heading): enhance Heading component with Chakra integration and …
misama-ct Sep 11, 2025
6480d53
feat(dialog): add backdrop filter to dialog slot recipe
misama-ct Sep 11, 2025
68515a6
refactor(dialog): remove Dialog.Description component and update rela…
misama-ct Sep 11, 2025
7de66a7
feat(dialog): enhance Dialog.Body with scroll behavior support
misama-ct Sep 12, 2025
dc3ccc2
refactor(dialog): replace backdrop with modal overlay and update dial…
misama-ct Sep 12, 2025
041bf96
refactor(dialog): enhance modal overlay with improved styling and ani…
misama-ct Sep 19, 2025
eafd865
refactor(dialog): enhance dialog component with size variations and i…
misama-ct Sep 19, 2025
0a4e568
refactor(drawer): remove Drawer component and related files for simpl…
misama-ct Sep 19, 2025
b3453a7
refactor(app-nav-bar-search): update search component for improved fu…
misama-ct Sep 19, 2025
5e538d8
refactor(dialog): remove Dialog.Backdrop component and update documen…
misama-ct Sep 19, 2025
f5e3be6
refactor(dialog): update dialog component structure and types
misama-ct Sep 23, 2025
41356f4
refactor(dialog): simplify dialog close trigger and context types
misama-ct Sep 23, 2025
ace4334
refactor(dialog): update DialogCloseTriggerProps to extend IconButton…
misama-ct Sep 23, 2025
bed6d71
refactor(dialog): enhance dialog component with improved structure an…
misama-ct Sep 25, 2025
edbe856
refactor(dialog): clean up imports in dialog stories
misama-ct Sep 25, 2025
c49b9b2
refactor(app-nav-bar-create-button): enhance button dialog structure …
misama-ct Sep 25, 2025
93cc3dd
refactor(app): rearrange IntlProvider and RouterProvider for improved…
misama-ct Sep 25, 2025
7d0d742
refactor(app): add locale prop to NimbusProvider for improved interna…
misama-ct Sep 25, 2025
04d8ff1
refactor(dialog): adjust modalOverlay background colors for improved …
misama-ct Sep 25, 2025
bbf14fe
refactor(dialog): streamline dialog component props and improve struc…
misama-ct Sep 30, 2025
2f5a6ef
refactor(dialog): enhance documentation and structure of dialog compo…
misama-ct Sep 30, 2025
71fd0b6
feat(dialog): add new Dialog component for enhanced user interaction
misama-ct Sep 30, 2025
1ca3fac
feat(dialog): add internationalization support for dialog close trigger
misama-ct Sep 30, 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
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { Info, Add } from "@commercetools/nimbus-icons";
import { Button, Dialog, TextInput, Stack, Text } from "@commercetools/nimbus";
import {
Button,
Dialog,
TextInput,
Stack,
Flex,
Text,
Icon,
} from "@commercetools/nimbus";
import { useCreateDocument } from "@/hooks/useCreateDocument";

/**
Expand All @@ -21,25 +29,20 @@ export const AppNavBarCreateButton = () => {
} = useCreateDocument();

return (
<Dialog.Root open={isOpen} onEscapeKeyDown={() => setIsOpen(false)}>
<Dialog.Backdrop />
<Dialog.Root isOpen={isOpen} onOpenChange={setIsOpen}>
<Dialog.Trigger asChild>
<Button
colorPalette="primary"
size="xs"
variant="ghost"
onPress={() => setIsOpen(true)}
>
<Button colorPalette="primary" size="xs" variant="ghost">
<Add />
New document
</Button>
</Dialog.Trigger>
<Dialog.Content divideY="1px">
<Dialog.Header>
<Dialog.Title>Create New Document</Dialog.Title>
<Dialog.Description>
<Text color="neutral.11" textStyle="sm">
Fill in the details to create a new document.
</Dialog.Description>
</Text>
<Dialog.CloseTrigger />
</Dialog.Header>
<Dialog.Body>
{!isLoading ? (
Expand Down Expand Up @@ -80,23 +83,20 @@ export const AppNavBarCreateButton = () => {
placeholder="What people will click, no pressure."
autoComplete="off"
/>
<Stack
<Flex
colorPalette="info"
direction="row"
alignItems="center"
bg="colorPalette.3"
p="400"
gap="400"
mt="200"
borderRadius="200"
>
<Text color="colorPalette.11">
<Info size="2em" />
</Text>
<Icon fontSize="1.5em" as={Info} color="colorPalette.11" />
<Text color="colorPalette.11">
The new document item will become a child of the current
document.
</Text>
</Stack>
</Flex>
</Stack>
</Stack>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Box,
useHotkeys,
Dialog,
Separator,
TextInput,
Text,
Kbd,
Expand Down Expand Up @@ -40,20 +41,18 @@ export const AppNavBarSearch = () => {
if (selectedItem) {
setOpen(false);
setActiveRoute(selectedItem.route);
setQuery("");
}
};

return (
<Flex grow="1">
<Dialog.Root
open={open}
isOpen={open}
placement="top"
motionPreset="slide-in-bottom"
onOpenChange={() => setOpen(!open)}
scrollBehavior="outside"
size="xl"
scrollBehavior="inside"
>
<Dialog.Backdrop />
<Dialog.Trigger>
<Box position="relative">
<TextInput
Expand All @@ -70,12 +69,12 @@ export const AppNavBarSearch = () => {
</Box>
</Box>
</Dialog.Trigger>
<Dialog.Content divideY="1px" backdropBlur="5px">
<Dialog.Content width="3xl">
<Dialog.Header>
<Dialog.Title fontWeight="600">
Search the Documentation
</Dialog.Title>
<Dialog.Title>Search the Documentation</Dialog.Title>
<Dialog.CloseTrigger />
</Dialog.Header>
<Separator />
<Dialog.Body>
<ComboBox
inputValue={query}
Expand All @@ -99,10 +98,11 @@ export const AppNavBarSearch = () => {
asChild
>
{/** TODO: TextInput should actually work here, try again once it's fixed*/}
<Input placeholder="Type to search..." />
<Input autoFocus placeholder="Type to search..." />
</Box>
</Flex>
<Box mx="-600" borderTop="1px solid" borderColor="neutral.6">
<Box mx="-600">
<Separator />
<ListBox items={results} selectionMode="single">
{(item) => (
<Flex
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ type ModalState = {

const InfoModal = ({ isOpen, onClose, title, children }: ModalState) => (
<Dialog.Root
open={isOpen}
onOpenChange={({ open }) => !open && onClose && onClose()}
isOpen={isOpen}
onOpenChange={(open) => !open && onClose && onClose()}
>
<Dialog.Content>
<Dialog.Header>
Expand Down Expand Up @@ -135,8 +135,8 @@ const ProductDetailsModal = ({
};

return (
<Dialog.Root open={isOpen}>
<Dialog.Content maxWidth="600px">
<Dialog.Root isOpen={isOpen}>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>{formData.name as string}</Dialog.Title>
</Dialog.Header>
Expand Down
51 changes: 51 additions & 0 deletions packages/nimbus/src/components/dialog/components/dialog.body.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useRef } from "react";
import { useObjectRef } from "react-aria";
import { mergeRefs } from "@chakra-ui/react";
import { DialogBodySlot } from "../dialog.slots";
import type { DialogBodyProps } from "../dialog.types";
import { useDialogRootContext } from "./dialog.context";

/**
* # Dialog.Body
*
* The main body content section of the dialog.
* Contains the primary dialog content and handles overflow/scrolling.
*
* @example
* ```tsx
* <Dialog.Content>
* <Dialog.Header>...</Dialog.Header>
* <Dialog.Body>
* <p>This is the main content of the dialog.</p>
* </Dialog.Body>
* <Dialog.Footer>...</Dialog.Footer>
* </Dialog.Content>
* ```
*/
export const DialogBody = (props: DialogBodyProps) => {
const { ref: forwardedRef, children, ...restProps } = props;

// create a local ref (because the consumer may not provide a forwardedRef)
const localRef = useRef<HTMLDivElement>(null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the consumer doesn't provide a ref, why do we need the local ref? we aren't using it here

Copy link
Collaborator Author

@misama-ct misama-ct Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically its not needed, you're right. It's just that during development, when starting, I don't know what is needed and I don't want to spend time reasoning about it with every subcomponent, so I just cover all cases with this 2 lines.

If you're bothered I can get rid of the localRef's, but I think it's also great to have the same implementation everywhere and not having to think about it every time.

I simplified dialog.body and all other sub-components. Apparently it's not even necessary anymore to transform a chakra forwardedRef into a react-aria compatible ref as the forwardedRef just works.

// merge the local ref with a potentially forwarded ref
const ref = useObjectRef(mergeRefs(localRef, forwardedRef));

const { scrollBehavior } = useDialogRootContext();
Copy link
Contributor

@jaikamat jaikamat Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be missing why this is configurable, but a part of me feels like we should be opinionated about scroll behavior. Do we need to expose this to consumers?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a story illustrating the difference. Sometimes you want the action-buttons to always be visible, sometimes, you want to force people to read everything and only then take an action.

Not dying on that hill though, @commercetools/craft-team-fe if someone else feels this is unnecessary I can also get rid of it.


const defaultProps = {
/**
* if scrollBehavior is set to "inside", set tabIndex to 0 to allow the body to
* receive focus, effectively enabling scrolling via keyboard
* arrow keys.
*/
tabIndex: scrollBehavior === "inside" ? 0 : undefined,
};

return (
<DialogBodySlot ref={ref} {...defaultProps} {...restProps}>
{children}
</DialogBodySlot>
);
};

DialogBody.displayName = "Dialog.Body";
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useRef } from "react";
import { useObjectRef } from "react-aria";
import { mergeRefs } from "@chakra-ui/react";
import { Close } from "@commercetools/nimbus-icons";
import { DialogCloseTriggerSlot } from "../dialog.slots";
import type { DialogCloseTriggerProps } from "../dialog.types";
import { IconButton } from "@/components";

/**
* # Dialog.CloseTrigger
*
* A button that closes the dialog when activated.
* Displays an IconButton with a close (X) icon by default.
*
* The component automatically handles the close behavior through React Aria's
* context, so no additional onPress handler is needed.
*
* @example
* ```tsx
* <Dialog.Root>
* <Dialog.Trigger>Open Dialog</Dialog.Trigger>
* <Dialog.Content>
* <Dialog.Header>
* <Dialog.Title>Title</Dialog.Title>
* <Dialog.CloseTrigger aria-label="Close dialog" />
* </Dialog.Header>
* <Dialog.Body>Content</Dialog.Body>
* </Dialog.Content>
* </Dialog.Root>
* ```
*/
export const DialogCloseTrigger = (props: DialogCloseTriggerProps) => {
const {
ref: forwardedRef,
"aria-label": ariaLabel = "Close dialog",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the default label should be an intl message

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...restProps
} = props;

// create a local ref (because the consumer may not provide a forwardedRef)
const localRef = useRef<HTMLButtonElement>(null);
// merge the local ref with a potentially forwarded ref
const ref = useObjectRef(mergeRefs(localRef, forwardedRef));

return (
<DialogCloseTriggerSlot>
<IconButton
ref={ref}
slot="close"
size="xs"
variant="ghost"
aria-label={ariaLabel}
{...restProps}
>
<Close />
</IconButton>
</DialogCloseTriggerSlot>
);
};

DialogCloseTrigger.displayName = "Dialog.CloseTrigger";
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { useRef } from "react";
import {
Modal as RaModal,
ModalOverlay as RaModalOverlay,
Dialog as RaDialog,
} from "react-aria-components";
import { useObjectRef } from "react-aria";
import { mergeRefs } from "@chakra-ui/react";
import {
DialogModalOverlaySlot,
DialogModalSlot,
DialogContentSlot,
} from "../dialog.slots";
import type { DialogContentProps } from "../dialog.types";
import { extractStyleProps } from "@/utils/extractStyleProps";
import { useDialogRootContext } from "./dialog.context";

/**
* # Dialog.Content
*
* The main dialog content container that wraps React Aria's Modal and Dialog.
* Handles portalling, backdrop, positioning, and content styling.
*
* This component creates the dialog overlay, positions the content, and provides
* accessibility features like focus management and keyboard dismissal.
*
* @example
* ```tsx
* <Dialog.Root>
* <Dialog.Trigger>Open Dialog</Dialog.Trigger>
* <Dialog.Content size="md" placement="center">
* <Dialog.Header>
* <Dialog.Title>Title</Dialog.Title>
* </Dialog.Header>
* <Dialog.Body>Content</Dialog.Body>
* <Dialog.Footer>Actions</Dialog.Footer>
* </Dialog.Content>
* </Dialog.Root>
* ```
*/
export const DialogContent = (props: DialogContentProps) => {
const { ref: forwardedRef, children, ...restProps } = props;

// Get recipe configuration from context instead of props
const {
defaultOpen,
isDismissable,
isKeyboardDismissDisabled,
shouldCloseOnInteractOutside,
isOpen,
onOpenChange,
} = useDialogRootContext();

const modalProps = {
defaultOpen,
isDismissable,
isKeyboardDismissDisabled,
shouldCloseOnInteractOutside,
isOpen,
onOpenChange,
};

// create a local ref (because the consumer may not provide a forwardedRef)
const localRef = useRef<HTMLDivElement>(null);
// merge the local ref with a potentially forwarded ref
const ref = useObjectRef(mergeRefs(localRef, forwardedRef));
Copy link
Contributor

@jaikamat jaikamat Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel stupid asking this, but I have to because it's a pattern I see everywhere and it almost feels automatic to me to do it.

Why do we do this?! Or rather, why do we use RAC's and Chakra's utils, what problem does this solve?

Copy link
Collaborator Author

@misama-ct misama-ct Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I remember correctly, there was some difference in how react-aria handles refs and chakra does that.

So you couldn't really put the forwardedRef directly on a react-aria component. And since the user may or may not have provided this forwardedRef, you create a local ref that merges a potential provided one and attach this one to the RA component instead. This localRef also allows you to work with the element internally (e.g. measure dimensions), while still providing access to that element to consumers via the forwardedRef.

@ByronDWall please sanity check what I just said.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, not getting any squiggly lines anymore when wiring the forwardedRef to a ra-component directly. I'll chekc where I can simplify things.


const [styleProps] = extractStyleProps(restProps);

return (
<DialogModalOverlaySlot asChild>
<RaModalOverlay {...modalProps}>
<DialogModalSlot asChild>
<RaModal>
<DialogContentSlot asChild {...styleProps}>
<RaDialog ref={ref}>{children}</RaDialog>
</DialogContentSlot>
</RaModal>
</DialogModalSlot>
</RaModalOverlay>
</DialogModalOverlaySlot>
);
};

DialogContent.displayName = "Dialog.Content";
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { createContext, useContext } from "react";
import type { DialogRootProps } from "../dialog.types";

/**
* Context value containing dialog configuration passed from Root to child components
*/
export type DialogContextValue = DialogRootProps;

export const DialogContext = createContext<DialogContextValue | undefined>(
undefined
);

/**
* Hook to access dialog configuration from DialogContext
* @returns Dialog configuration from context
* @throws Error if used outside of Dialog.Root
*/
export const useDialogRootContext = (): DialogContextValue => {
const context = useContext(DialogContext);
if (!context) {
throw new Error("useDialogContext must be used within Dialog.Root");
}
return context;
};

/**
* Provider component that passes dialog configuration down to child components
*/
export const DialogProvider = ({
children,
value,
}: {
children: React.ReactNode;
value: DialogContextValue;
}) => {
return <DialogContext value={value}>{children}</DialogContext>;
};
Loading