import { UX4GColors } from '../../foundation/colors'; import React from 'react'; import { Modal as RNModal, View, Text, StyleSheet, Pressable, ScrollView, Image, Dimensions, Platform, } from 'react-native'; import { useUx4gTheme } from '../../theme/Ux4gThemeContext'; import { Ux4gButton } from '../button'; import { Ux4gAvatar, Ux4gAvatarSize } from '../avatar'; import { Ux4gDivider } from '../divider'; import { Ux4gIcons } from '../../foundation/icons'; export type Ux4gModalAlignment = 'leftAligned' | 'centered'; export type Ux4gModalLeadingItem = 'none' | 'icon' | 'avatar' | 'image'; export type Ux4gModalHeaderImage = 'fullBleed' | 'padded' | 'none'; export type Ux4gModalFooterButtons = | 'oneButton' | 'twoButtons' | 'oneButtonWithIcon' | 'twoButtonsWithIcon'; export type Ux4gModalFooterAlign = 'left' | 'right' | 'center' | 'split'; export interface Ux4gModalProps { visible: boolean; onDismiss: () => void; headerImageContent?: React.ReactNode; headerImageUrl?: string; headerImageHeight?: number; headerImageStyle?: Ux4gModalHeaderImage; leadingItem?: Ux4gModalLeadingItem; leadingIcon?: React.ReactNode; leadingIconTint?: string; avatarInitials?: string; avatarImageUrl?: string; avatarIcon?: React.ReactNode; avatarSize?: Ux4gAvatarSize; avatarInlineSize?: Ux4gAvatarSize; avatarContainerColor?: string; avatarContentColor?: string; leadingImageUrl?: string; leadingImageContent?: React.ReactNode; showHeader?: boolean; headerTitle?: string; showDescription?: boolean; descriptionText?: string; showDividers?: boolean; showSubtitle?: boolean; subtitleText?: string; showBody?: boolean; bodyText?: string; bodyContent?: React.ReactNode; showFooter?: boolean; footerButtons?: Ux4gModalFooterButtons; footerAlign?: Ux4gModalFooterAlign; isDestructive?: boolean; primaryButtonText?: string; secondaryButtonText?: string; leadingActionIcon?: React.ReactNode; onPrimaryClick?: () => void; onSecondaryClick?: () => void; onLeadingIconPressed?: () => void; alignment?: Ux4gModalAlignment; showCloseButton?: boolean; backgroundColor?: string; cornerRadius?: number; } const getHexWithAlpha = (baseHex: string, alphaHex: string): string => { if (baseHex && baseHex.startsWith('#') && baseHex.length === 7) { return `${baseHex}${alphaHex}`; } return baseHex; }; export const Ux4gModal: React.FC = ({ visible, onDismiss, headerImageContent, headerImageUrl, headerImageHeight = 160, headerImageStyle = 'fullBleed', leadingItem = 'none', leadingIcon, leadingIconTint, avatarInitials, avatarImageUrl, avatarIcon, avatarSize = 'l', avatarInlineSize = 's', avatarContainerColor, avatarContentColor, leadingImageUrl, leadingImageContent, showHeader = true, headerTitle = 'Header', showDescription = false, descriptionText = 'Write description here', showDividers = true, showSubtitle = true, subtitleText = 'Subtitle', showBody = true, bodyText = 'A modal is a design element that pops up over the main content of a webpage. It demands the user\'s attention by temporarily disabling interaction with the rest of the page until the user addresses the content within the modal.', bodyContent, showFooter = true, footerButtons = 'twoButtons', footerAlign = 'right', isDestructive = false, primaryButtonText = 'Button', secondaryButtonText = 'Button', leadingActionIcon, onPrimaryClick, onSecondaryClick, onLeadingIconPressed, alignment = 'leftAligned', showCloseButton = true, backgroundColor, cornerRadius, }) => { const theme = useUx4gTheme(); const surface = backgroundColor ?? theme.colors.surface; const onSurface = theme.colors.onSurface; const primary = isDestructive ? theme.colors.error : theme.colors.primary; const onPrimary = isDestructive ? theme.colors.onError : theme.colors.onPrimary; const effCornerRadius = cornerRadius ?? theme.radius.radius16; const isCentered = alignment === 'centered'; const hasImage = headerImageStyle !== 'none' && (headerImageContent != null || headerImageUrl != null); const windowHeight = Dimensions.get('window').height; const maxModalHeight = windowHeight * 0.85; const handleDismiss = () => { onDismiss(); }; const renderHeaderImage = () => { if (!hasImage) return null; let content = null; if (headerImageContent) { content = headerImageContent; } else if (headerImageUrl) { content = ( ); } if (headerImageStyle === 'padded') { return ( {content} ); } return ( {content} ); }; const renderAvatar = (centered: boolean) => ( ); const renderLeadingImage = () => { if (leadingImageContent) { return ( {leadingImageContent} ); } if (leadingImageUrl) { return ( ); } return null; }; const renderHeaderSection = () => { return ( {leadingItem === 'icon' && ( {leadingIcon ?? Ux4gIcons.info({ size: 24, color: leadingIconTint ?? onSurface })} )} {leadingItem === 'avatar' && !isCentered && ( {renderAvatar(false)} )} {leadingItem === 'image' && (leadingImageContent || leadingImageUrl) && ( {renderLeadingImage()} )} {headerTitle} {showDescription && ( 80 in hex. Actually 80 is 50% marginTop: theme.space.space4, textAlign: isCentered ? 'center' : 'left', }, ]} > {descriptionText} )} {showDividers && ( // 0.2 alpha -> 33 )} ); }; const renderFooter = () => { const hasLeadingIcon = footerButtons === 'oneButtonWithIcon' || footerButtons === 'twoButtonsWithIcon'; const hasSecondary = footerButtons === 'twoButtons' || footerButtons === 'twoButtonsWithIcon'; let justifyContent: 'flex-start' | 'flex-end' | 'center' | 'space-between' = 'flex-end'; if (footerAlign === 'left') justifyContent = 'flex-start'; else if (footerAlign === 'center') justifyContent = 'center'; else if (footerAlign === 'split') justifyContent = 'space-between'; return ( {showDividers && ( )} {hasLeadingIcon && ( <> {})} style={{ width: 40, height: 40, borderRadius: 20, backgroundColor: getHexWithAlpha(theme.colors.primary, '14'), // 0.08 alpha -> ~13/14 alignItems: 'center', justifyContent: 'center', }} > {leadingActionIcon ?? Ux4gIcons.info({ size: 20, color: theme.colors.primary })} {footerAlign !== 'split' && } )} {hasSecondary && ( <> {footerAlign === 'split' ? ( ) : ( )} )} {footerAlign === 'split' && hasSecondary ? ( ) : ( )} ); }; return ( {renderHeaderImage()} {leadingItem === 'avatar' && isCentered && showHeader && ( {renderAvatar(true)} )} {showHeader && renderHeaderSection()} {(showSubtitle || showBody || bodyContent != null) && ( {showSubtitle && ( {subtitleText} )} {showBody && ( {bodyContent ? ( bodyContent ) : ( {bodyText} )} )} )} {showFooter && renderFooter()} {showCloseButton && headerImageStyle !== 'padded' && ( {hasImage ? ( {Ux4gIcons.close({ size: 16, color: theme.colors.primary })} ) : ( {Ux4gIcons.close({ size: 20, color: onSurface })} )} )} ); }; const styles = StyleSheet.create({ overlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.5)', justifyContent: 'center', alignItems: 'center', padding: 16, }, modalContainer: { width: '100%', maxWidth: 560, overflow: 'hidden', ...Platform.select({ ios: { shadowColor: UX4GColors.neutral1000black, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.15, shadowRadius: 12, }, android: { elevation: 8, }, }), }, leadingImageContainer: { width: 40, height: 40, borderRadius: 8, overflow: 'hidden', }, closeButtonWrapper: { position: 'absolute', zIndex: 10, }, closeButtonImageMode: { width: 32, height: 32, borderRadius: 16, alignItems: 'center', justifyContent: 'center', }, closeButtonNormalMode: { minWidth: 32, minHeight: 32, alignItems: 'center', justifyContent: 'center', }, });