-
Notifications
You must be signed in to change notification settings - Fork 0
CRAFT-1734: Add Dialog component #428
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
Changes from 31 commits
f39b690
e4ad4cd
82aa186
062230b
c7b3fff
d88162a
baf8746
eaea330
6aa9bc5
344a4fe
7e6140e
43f5ed0
5dbe91e
6480d53
68515a6
7de66a7
dc3ccc2
041bf96
eafd865
0a4e568
b3453a7
5e538d8
f5e3be6
41356f4
ace4334
bed6d71
edbe856
c49b9b2
93cc3dd
7d0d742
04d8ff1
bbf14fe
2f5a6ef
71fd0b6
1ca3fac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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); | ||
|
||
// merge the local ref with a potentially forwarded ref | ||
const ref = useObjectRef(mergeRefs(localRef, forwardedRef)); | ||
|
||
const { scrollBehavior } = useDialogRootContext(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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", | ||
|
||
...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)); | ||
|
||
|
||
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>; | ||
}; |
Uh oh!
There was an error while loading. Please reload this page.