@razorpay/blade-mcp
Version:
Model Context Protocol server for Blade
711 lines (650 loc) • 20.6 kB
Markdown
## Component Name
Modal
## Description
Modal is a dialog component that appears in front of the app content to provide critical information or request user input. It's designed to focus user attention, disabling all other interactions until explicitly dismissed. Modals are accessible, can be dismissed via escape key, clicking outside, or a close button (when dismissible), and come in three sizes: small, medium, and large.
## Important Constraints
- `ModalHeader`'s `trailing` prop only accepts `Button`, `IconButton`, `Badge`, `Link`, `Text` & `Amount` components.
## TypeScript Types
These types represent the props that the Modal component and its subcomponents accept. When using the Modal component along with its subcomponents, you'll need these type definitions to understand the available props.
```typescript
// The main Modal component props
type ModalProps = {
/**
* Children of Modal
* Only ModalHeader, ModalBody and ModalFooter are allowed as children
*/
children: React.ReactNode;
/**
Sets the modal to open or close
* @default false
*/
isOpen: boolean;
/**
* Callback function when user clicks on close button or outside the modal or on pressing escape key.
*/
onDismiss: () => void;
/**
* Whether the modal can be dismissed by clicking outside or pressing escape key
* @default true
* @note
* If isDismissible is false, the modal will not be dismissed when the user clicks outside the modal or presses the escape key and the close button will not be shown. you need to handle the closing of the modal from your own code. also onDismiss will not be called.
*/
isDismissible?: boolean;
/**
* Ref to the element to be focused on opening the modal.
*/
initialFocusRef?: React.MutableRefObject<any>;
/**
* Size of the modal
* @default 'small'
*/
size?: 'small' | 'medium' | 'large' | 'full';
/**
* Accessibility label for the modal
*/
accessibilityLabel?: string;
/**
* Sets the z-index of the modal
* @default 1000
*/
zIndex?: number;
} & DataAnalyticsAttribute;
// The ModalHeader component props
type ModalHeaderProps = {
/**
* Title of the header
*/
title?: React.ReactNode;
/**
* Subtitle of the header
*/
subtitle?: React.ReactNode;
/**
* Content to be displayed before the title
*/
leading?: React.ReactNode;
/**
* Content to be displayed after the title
*/
trailing?: React.ReactNode;
/**
* Content to be displayed as a suffix to the title
*/
titleSuffix?: React.ReactNode;
} & DataAnalyticsAttribute;
// The ModalBody component props
type ModalBodyProps = {
children: React.ReactNode;
/**
* Sets the padding equally on all sides. Only few `spacing` tokens are allowed deliberately
* @default `spacing.6`
*/
padding?: Extract<SpacingValueType, 'spacing.0' | 'spacing.6'>;
} & DataAnalyticsAttribute;
// The ModalFooter component props
type ModalFooterProps = {
children: React.ReactNode;
} & DataAnalyticsAttribute;
```
## Example
Below is a comprehensive example showcasing the Modal component with various configurations:
```tsx
import React, { useState, useRef } from 'react';
import {
Modal,
ModalHeader,
ModalBody,
ModalFooter,
Box,
Button,
Radio,
RadioGroup,
Text,
TextArea,
Checkbox,
} from '@razorpay/blade/components';
const ModalExample = () => {
// State to control modal visibility
const [isModalOpen, setIsModalOpen] = useState(false);
// State for form values
const [selectedAddress, setSelectedAddress] = useState('home');
const [selectedSize, setSelectedSize] = useState<'small' | 'medium' | 'large'>('medium');
const [saveAsDefault, setSaveAsDefault] = useState(false);
// Ref for initial focus when modal opens
const addAddressButtonRef = useRef(null);
// Handlers
const openModal = () => setIsModalOpen(true);
const closeModal = () => setIsModalOpen(false);
const handleSave = () => {
// Handle save logic here
closeModal();
};
return (
<>
{/* Buttons to demonstrate different modal sizes */}
<Box display="flex" gap="spacing.5" flexWrap="wrap" marginBottom="spacing.5">
<Button
onClick={() => {
setSelectedSize('small');
openModal();
}}
>
Open Small Modal
</Button>
</Box>
{/* Modal component */}
<Modal
isOpen={isModalOpen}
onDismiss={closeModal}
size={selectedSize}
initialFocusRef={addAddressButtonRef}
accessibilityLabel="Address selection modal"
>
<ModalHeader
title="Select Delivery Address"
subtitle="Choose where you'd like your order delivered"
/>
<ModalBody>
<Box display="flex" flexDirection="column" gap="spacing.5">
<Box marginTop="spacing.5">
<Text size="small" color="surface.text.gray.subtle">
Need to add a new address?
</Text>
<Box marginTop="spacing.4">
<TextArea
label="Address Line 1"
placeholder="Enter street address"
marginBottom="spacing.4"
/>
<TextArea
label="Address Line 2"
placeholder="Apt, Suite, Building (optional)"
marginBottom="spacing.4"
/>
</Box>
</Box>
</Box>
</ModalBody>
<ModalFooter>
<Box display="flex" gap="spacing.3" justifyContent="flex-end" width="100%">
<Button variant="tertiary" onClick={closeModal}>
Cancel
</Button>
<Button ref={addAddressButtonRef} onClick={handleSave}>
Save Address
</Button>
</Box>
</ModalFooter>
</Modal>
</>
);
};
export default ModalExample;
```
### OTP Modal Example
This example demonstrates a modal for OTP verification with input fields, timer, and resend functionality.
```tsx
import React from 'react';
import {
Box,
Button,
Modal,
ModalHeader,
ModalBody,
ModalFooter,
BottomSheet,
BottomSheetHeader,
BottomSheetBody,
BottomSheetFooter,
Text,
OTPInput,
Link,
LockIcon,
} from '@razorpay/blade/components';
import { useBreakpoint, useTheme } from '@razorpay/blade/utils';
// Responsive wrapper component for handling mobile/desktop modal display
function ResponsiveModalWrapper({
children,
footer,
isOpen,
onDismiss,
modalBodyPadding,
modalSize = 'small',
wrapInBottomSheetFooter = false,
customSnapPoints = [0.35, 0.5, 0.85],
}: {
children: React.ReactElement | React.ReactElement[];
footer?: React.ReactElement;
isOpen: boolean;
onDismiss: () => void;
modalBodyPadding?: 'spacing.0' | 'spacing.6';
modalSize?: 'small' | 'medium' | 'large' | 'full';
wrapInBottomSheetFooter?: boolean;
customSnapPoints?: [number, number, number];
}): React.ReactNode {
const { theme } = useTheme();
const { matchedDeviceType } = useBreakpoint(theme);
const isMobile = matchedDeviceType === 'mobile';
if (isMobile) {
return (
<BottomSheet isOpen={isOpen} onDismiss={onDismiss} snapPoints={customSnapPoints}>
<BottomSheetHeader />
<BottomSheetBody padding="spacing.5">
{children}
{footer && !wrapInBottomSheetFooter && <Box marginTop="spacing.6">{footer}</Box>}
</BottomSheetBody>
{footer && wrapInBottomSheetFooter && <BottomSheetFooter>{footer}</BottomSheetFooter>}
</BottomSheet>
);
}
return (
<Modal isOpen={isOpen} onDismiss={onDismiss} size={modalSize}>
<ModalHeader />
<ModalBody padding={modalBodyPadding}>{children}</ModalBody>
{footer && <ModalFooter>{footer}</ModalFooter>}
</Modal>
);
}
function OTPModal(): React.ReactElement {
const [isOpen, setIsOpen] = React.useState(false);
const [otp, setOtp] = React.useState('');
const [error, setError] = React.useState<string | null>(null);
const [isResendOtpTimerRunning, setIsResendOtpTimerRunning] = React.useState(false);
const [resendOtpTimer, setResendOtpTimer] = React.useState(30);
const { theme } = useTheme();
const { matchedDeviceType } = useBreakpoint(theme);
const isMobile = matchedDeviceType === 'mobile';
// Start timer when modal opens
React.useEffect(() => {
if (isOpen) {
setIsResendOtpTimerRunning(true);
setResendOtpTimer(30);
} else {
setIsResendOtpTimerRunning(false);
setResendOtpTimer(30);
}
}, [isOpen]);
// Handle timer countdown
React.useEffect(() => {
let timer: ReturnType<typeof setInterval>;
if (isResendOtpTimerRunning && resendOtpTimer > 0) {
timer = setInterval(() => {
setResendOtpTimer((prev) => {
if (prev <= 1) {
setIsResendOtpTimerRunning(false);
return 0;
}
return prev - 1;
});
}, 1000);
}
return () => clearInterval(timer);
}, [isResendOtpTimerRunning, resendOtpTimer]);
const handleVerify = (): void => {
if (otp.length !== 6) {
setError('Please enter a valid 6-digit OTP');
return;
}
// Verify OTP logic here
setError(null);
setIsOpen(false);
};
return (
<Box>
<Button onClick={() => setIsOpen(true)}>Verify Phone Number</Button>
<ResponsiveModalWrapper
isOpen={isOpen}
onDismiss={() => setIsOpen(false)}
footer={
<Box display="flex" gap="spacing.5" justifyContent="flex-end" width="100%">
<Button variant="tertiary" isFullWidth={isMobile} onClick={() => setIsOpen(false)}>
Cancel
</Button>
<Button isFullWidth={isMobile} onClick={handleVerify} isDisabled={otp.length !== 6}>
Verify
</Button>
</Box>
}
customSnapPoints={[0.5, 0.6, 0.75]}
>
<Box
display="flex"
justifyContent="center"
alignItems="center"
borderRadius="medium"
padding="spacing.4"
width="48px"
height="48px"
// backgroundColor="feedback.background.neutral.subtle"
>
<LockIcon color="surface.icon.gray.subtle" size="xlarge" />
</Box>
<Box marginTop="spacing.4" display="flex" flexDirection="column" gap="spacing.2">
<Text size="large" weight="semibold">
2 Step Verification
</Text>
<Text size="medium" weight="regular" color="surface.text.gray.subtle">
This action requires 2-step verification. A 6-digit OTP has been sent via SMS to
8757450923. The OTP will expire in 5 minutes.
</Text>
</Box>
<Box marginY="spacing.5">
<OTPInput
label="Enter the code"
otpLength={6}
size="large"
aria-label="Enter verification code"
value={otp}
onChange={(value) => setOtp(value.rawValue ?? '')}
errorText={error ?? undefined}
/>
</Box>
<Box
marginTop="spacing.5"
display="flex"
flexDirection="row"
gap="spacing.2"
justifyContent="flex-start"
alignItems="center"
>
<Text size="medium" weight="regular" color="surface.text.gray.subtle">
{isResendOtpTimerRunning
? `Resend OTP in ${resendOtpTimer} seconds`
: "Didn't receive OTP?"}
</Text>
{!isResendOtpTimerRunning && (
<Link
onClick={() => {
setIsResendOtpTimerRunning(true);
setResendOtpTimer(30);
}}
size="medium"
variant="button"
aria-label="Resend verification code"
>
Resend OTP
</Link>
)}
</Box>
</ResponsiveModalWrapper>
</Box>
);
}
export default OTPModal;
```
### Share Modal Example
This example shows a modal for sharing content with social media options and copy link functionality.
```tsx
import React from 'react';
import {
Box,
Button,
Modal,
ModalHeader,
ModalBody,
ModalFooter,
BottomSheet,
BottomSheetHeader,
BottomSheetBody,
BottomSheetFooter,
Text,
TextInput,
ActionList,
ActionListItem,
Alert,
TwitterIcon,
FacebookIcon,
WhatsAppIcon,
CopyIcon,
} from '@razorpay/blade/components';
import { useBreakpoint, useTheme } from '@razorpay/blade/utils';
// [ResponsiveModalWrapper component code remains the same as above in otp modal example]
function ResponsiveModalWrapper({
children,
footer,
isOpen,
onDismiss,
modalBodyPadding,
modalSize = 'small',
wrapInBottomSheetFooter = false,
customSnapPoints = [0.35, 0.5, 0.85],
}: {
children: React.ReactElement | React.ReactElement[];
footer?: React.ReactElement;
isOpen: boolean;
onDismiss: () => void;
modalBodyPadding?: 'spacing.0' | 'spacing.6';
modalSize?: 'small' | 'medium' | 'large' | 'full';
wrapInBottomSheetFooter?: boolean;
customSnapPoints?: [number, number, number];
}): React.ReactNode {
const { theme } = useTheme();
const { matchedDeviceType } = useBreakpoint(theme);
const isMobile = matchedDeviceType === 'mobile';
if (isMobile) {
return (
<BottomSheet isOpen={isOpen} onDismiss={onDismiss} snapPoints={customSnapPoints}>
<BottomSheetHeader />
<BottomSheetBody padding="spacing.5">
{children}
{footer && !wrapInBottomSheetFooter && <Box marginTop="spacing.6">{footer}</Box>}
</BottomSheetBody>
{footer && wrapInBottomSheetFooter && <BottomSheetFooter>{footer}</BottomSheetFooter>}
</BottomSheet>
);
}
return (
<Modal isOpen={isOpen} onDismiss={onDismiss} size={modalSize}>
<ModalHeader />
<ModalBody padding={modalBodyPadding}>{children}</ModalBody>
{footer && <ModalFooter>{footer}</ModalFooter>}
</Modal>
);
}
function ShareModal(): React.ReactElement {
const [isOpen, setIsOpen] = React.useState(false);
const [showCopiedAlert, setShowCopiedAlert] = React.useState(false);
const shareUrl = 'https://example.com/share-link';
const shareOptions = [
{
icon: TwitterIcon,
label: 'Twitter',
url: `https://twitter.com/intent/tweet?url=${shareUrl}`,
},
{
icon: FacebookIcon,
label: 'Facebook',
url: `https://www.facebook.com/sharer/sharer.php?u=${shareUrl}`,
},
{ icon: WhatsAppIcon, label: 'WhatsApp', url: `https://wa.me/?text=${shareUrl}` },
];
const handleShare = (url: string): void => {
window.open(url, '_blank');
};
const handleCopy = async (): Promise<void> => {
await navigator.clipboard.writeText(shareUrl);
setShowCopiedAlert(true);
setTimeout(() => setShowCopiedAlert(false), 3000);
};
return (
<Box>
<Button onClick={() => setIsOpen(true)}>Share</Button>
<ResponsiveModalWrapper
isOpen={isOpen}
onDismiss={() => setIsOpen(false)}
modalSize="small"
footer={
<Box display="flex" justifyContent="flex-end">
<Button variant="tertiary" onClick={() => setIsOpen(false)}>
Close
</Button>
</Box>
}
>
<Box display="flex" flexDirection="column" gap="spacing.4">
<Text size="large" weight="semibold">
Share this content
</Text>
{showCopiedAlert && (
<Alert
color="positive"
title="Link copied!"
description="The link has been copied to your clipboard."
isDismissible
onDismiss={() => setShowCopiedAlert(false)}
/>
)}
<Box>
<ActionList>
{shareOptions.map((option) => (
<ActionListItem
key={option.label}
title={option.label}
onClick={() => handleShare(option.url)}
value={option.label}
/>
))}
</ActionList>
</Box>
<Box display="flex" gap="spacing.3">
<TextInput value={shareUrl} accessibilityLabel="Share URL" />
<Button
variant="secondary"
icon={CopyIcon}
onClick={handleCopy}
accessibilityLabel="Copy link"
/>
</Box>
</Box>
</ResponsiveModalWrapper>
</Box>
);
}
export default ShareModal;
```
### Informational Modal Example
This example demonstrates a modal for displaying important information or announcements with rich content.
```tsx
import React from 'react';
import {
Box,
Button,
Modal,
ModalHeader,
ModalBody,
ModalFooter,
BottomSheet,
BottomSheetHeader,
BottomSheetBody,
BottomSheetFooter,
Text,
Heading,
Badge,
List,
ListItem,
InfoIcon,
CheckIcon,
} from '@razorpay/blade/components';
import { useBreakpoint, useTheme } from '@razorpay/blade/utils';
// [ResponsiveModalWrapper component code remains the same as in otp modal example]
function ResponsiveModalWrapper({
children,
footer,
isOpen,
onDismiss,
modalBodyPadding,
modalSize = 'small',
wrapInBottomSheetFooter = false,
customSnapPoints = [0.35, 0.5, 0.85],
}: {
children: React.ReactElement | React.ReactElement[];
footer?: React.ReactElement;
isOpen: boolean;
onDismiss: () => void;
modalBodyPadding?: 'spacing.0' | 'spacing.6';
modalSize?: 'small' | 'medium' | 'large' | 'full';
wrapInBottomSheetFooter?: boolean;
customSnapPoints?: [number, number, number];
}): React.ReactNode {
const { theme } = useTheme();
const { matchedDeviceType } = useBreakpoint(theme);
const isMobile = matchedDeviceType === 'mobile';
if (isMobile) {
return (
<BottomSheet isOpen={isOpen} onDismiss={onDismiss} snapPoints={customSnapPoints}>
<BottomSheetHeader />
<BottomSheetBody padding="spacing.5">
{children}
{footer && !wrapInBottomSheetFooter && <Box marginTop="spacing.6">{footer}</Box>}
</BottomSheetBody>
{footer && wrapInBottomSheetFooter && <BottomSheetFooter>{footer}</BottomSheetFooter>}
</BottomSheet>
);
}
return (
<Modal isOpen={isOpen} onDismiss={onDismiss} size={modalSize}>
<ModalHeader />
<ModalBody padding={modalBodyPadding}>{children}</ModalBody>
{footer && <ModalFooter>{footer}</ModalFooter>}
</Modal>
);
}
function InformationalModal(): React.ReactElement {
const [isOpen, setIsOpen] = React.useState(false);
const { theme } = useTheme();
const { matchedDeviceType } = useBreakpoint(theme);
const isMobile = matchedDeviceType === 'mobile';
const features = [
'Enhanced security with 2FA',
'Improved dashboard performance',
'New analytics features',
'Better mobile responsiveness',
];
return (
<Box>
<Button onClick={() => setIsOpen(true)} icon={InfoIcon}>
What's New
</Button>
<ResponsiveModalWrapper
isOpen={isOpen}
onDismiss={() => setIsOpen(false)}
modalSize="medium"
footer={
<Box display="flex" gap="spacing.5" justifyContent="flex-end" width="100%">
<Button variant="primary" isFullWidth={isMobile} onClick={() => setIsOpen(false)}>
Got it
</Button>
</Box>
}
>
<Box display="flex" flexDirection="column" gap="spacing.6">
<Box display="flex" alignItems="center" gap="spacing.3">
<Heading size="large">Platform Updates</Heading>
<Badge color="notice">New</Badge>
</Box>
<Text size="medium" color="surface.text.gray.muted">
We've made several improvements to enhance your experience. Here are the key updates:
</Text>
<List>
{features.map((feature) => (
<ListItem key={feature} icon={CheckIcon}>
{feature}
</ListItem>
))}
</List>
<Box
padding="spacing.4"
backgroundColor="surface.background.gray.subtle"
borderRadius="medium"
>
<Text size="small" color="surface.text.gray.muted">
These updates are now live. For more details, please visit our documentation.
</Text>
</Box>
</Box>
</ResponsiveModalWrapper>
</Box>
);
}
export default InformationalModal;
```