import * as React from 'react'; import { useState } from 'react'; import { css } from '@patternfly/react-styles'; import styles from '@patternfly/react-styles/css/components/Alert/alert'; import { AlertIcon } from './AlertIcon'; import { capitalize, useOUIAProps, OUIAProps } from '../../helpers'; import { AlertContext } from './AlertContext'; import maxLines from '@patternfly/react-tokens/dist/esm/c_alert__title_max_lines'; import { Tooltip, TooltipPosition } from '../Tooltip'; import { AlertToggleExpandButton } from './AlertToggleExpandButton'; export enum AlertVariant { success = 'success', danger = 'danger', warning = 'warning', info = 'info', custom = 'custom' } /** The main alert component. */ export interface AlertProps extends Omit, 'action' | 'title'>, OUIAProps { /** Close button; use the alert action close button component. */ actionClose?: React.ReactNode; /** Action links; use a single alert action link component or multiple wrapped in an array * or React.Fragment. */ actionLinks?: React.ReactNode; /** Content rendered inside the alert. */ children?: React.ReactNode; /** Additional classes to add to the alert. */ className?: string; /** Set a custom icon to the alert. If not set the icon is set according to the variant. */ customIcon?: React.ReactNode; /** Uniquely identifies the alert. */ id?: string; /** Flag indicating that the alert is expandable. */ isExpandable?: boolean; /** Flag to indicate if the alert is inline. */ isInline?: boolean; /** Flag to indicate if the alert is in a live region. */ isLiveRegion?: boolean; /** Flag to indicate if the alert is plain. */ isPlain?: boolean; /** Function to be executed on alert timeout. Relevant when the timeout prop is set. */ onTimeout?: () => void; /** If set to true, the timeout is 8000 milliseconds. If a number is provided, alert will * be dismissed after that amount of time in milliseconds. */ timeout?: number | boolean; /** If the user hovers over the alert and `timeout` expires, this is how long to wait * before finally dismissing the alert. */ timeoutAnimation?: number; /** Title of the alert. */ title: React.ReactNode; /** Sets the element to use as the alert title. Default is h4. */ component?: keyof JSX.IntrinsicElements; /** Adds accessible text to the alert toggle. */ toggleAriaLabel?: string; /** Position of the tooltip which is displayed if text is truncated. */ tooltipPosition?: | TooltipPosition | 'auto' | 'top' | 'bottom' | 'left' | 'right' | 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end' | 'left-start' | 'left-end' | 'right-start' | 'right-end'; /** Truncate title to number of lines. */ truncateTitle?: number; /** Adds alert variant styles. */ variant?: 'success' | 'danger' | 'warning' | 'info' | 'custom'; /** Variant label text for screen readers. */ variantLabel?: string; /** Value to overwrite the randomly generated data-ouia-component-id.*/ ouiaId?: number | string; /** Set the value of data-ouia-safe. Only set to true when the component is in a static state, i.e. no animations are occurring. At all other times, this value must be false. */ ouiaSafe?: boolean; } export const Alert: React.FunctionComponent = ({ variant = AlertVariant.custom, isInline = false, isPlain = false, isLiveRegion = false, variantLabel = `${capitalize(variant)} alert:`, actionClose, actionLinks, title, component = 'h4', children = '', className = '', ouiaId, ouiaSafe = true, timeout = false, timeoutAnimation = 3000, onTimeout = () => {}, truncateTitle = 0, tooltipPosition, customIcon, isExpandable = false, toggleAriaLabel = `${capitalize(variant)} alert details`, onMouseEnter = () => {}, onMouseLeave = () => {}, id, ...props }: AlertProps) => { const ouiaProps = useOUIAProps(Alert.displayName, ouiaId, ouiaSafe, variant); const getHeadingContent = ( {variantLabel} {title} ); const titleRef = React.useRef(null); const TitleComponent = component as any; const divRef = React.useRef(); const [isTooltipVisible, setIsTooltipVisible] = useState(false); React.useEffect(() => { if (!titleRef.current || !truncateTitle) { return; } titleRef.current.style.setProperty(maxLines.name, truncateTitle.toString()); const showTooltip = titleRef.current && titleRef.current.offsetHeight < titleRef.current.scrollHeight; if (isTooltipVisible !== showTooltip) { setIsTooltipVisible(showTooltip); } }, [titleRef, truncateTitle, isTooltipVisible]); const [timedOut, setTimedOut] = useState(false); const [timedOutAnimation, setTimedOutAnimation] = useState(true); const [isMouseOver, setIsMouseOver] = useState(); const [containsFocus, setContainsFocus] = useState(); const dismissed = timedOut && timedOutAnimation && !isMouseOver && !containsFocus; React.useEffect(() => { const calculatedTimeout = timeout === true ? 8000 : Number(timeout); if (calculatedTimeout > 0) { const timer = setTimeout(() => setTimedOut(true), calculatedTimeout); return () => clearTimeout(timer); } }, [timeout]); React.useEffect(() => { const onDocumentFocus = () => { if (divRef.current) { if (divRef.current.contains(document.activeElement)) { setContainsFocus(true); setTimedOutAnimation(false); } else if (containsFocus) { setContainsFocus(false); } } }; document.addEventListener('focus', onDocumentFocus, true); return () => document.removeEventListener('focus', onDocumentFocus, true); }, [containsFocus]); React.useEffect(() => { if (containsFocus === false || isMouseOver === false) { const timer = setTimeout(() => setTimedOutAnimation(true), timeoutAnimation); return () => clearTimeout(timer); } }, [containsFocus, isMouseOver, timeoutAnimation]); React.useEffect(() => { dismissed && onTimeout(); }, [dismissed, onTimeout]); const [isExpanded, setIsExpanded] = useState(false); const onToggleExpand = () => { setIsExpanded(!isExpanded); }; const myOnMouseEnter = (ev: React.MouseEvent) => { setIsMouseOver(true); setTimedOutAnimation(false); onMouseEnter(ev); }; const myOnMouseLeave = (ev: React.MouseEvent) => { setIsMouseOver(false); onMouseLeave(ev); }; if (dismissed) { return null; } const Title = ( {getHeadingContent} ); return (
{isExpandable && (
)} {isTooltipVisible ? ( {Title} ) : ( Title )} {actionClose && (
{actionClose}
)} {children && (!isExpandable || (isExpandable && isExpanded)) && (
{children}
)} {actionLinks &&
{actionLinks}
}
); }; Alert.displayName = 'Alert';