import { PureComponent } from 'react'; import Badge from '@mui/material/Badge'; import Box from '@mui/material/Box'; import ClickAwayListener from '@mui/material/ClickAwayListener'; import IconButton from '@mui/material/IconButton'; import Paper from '@mui/material/Paper'; import Popper from '@mui/material/Popper'; import Typography from '@mui/material/Typography'; import CloseIcon from '@mui/icons-material/Close'; import helpAutomat from '../automats/helpAutomat'; import { RED, RED_HOVER, SHADOW_BADGE, SHADOW_POPOVER, WHITE, } from '../colors'; let nextHelpBadgeId = 0; /** * Reusable reactive badge that appears on UI elements (navigation icons, tabs, etc.) * when systemwide Help mode is enabled via helpAutomat. * * Can wrap an element (using MUI Badge) or render standalone with custom positioning. * The badge is circular, red, and stays firmly in place. * Clicking the "?" badge opens an explanatory Popper anchored to the badge. * Only one help popper can be open at a time across the entire system. * When Help mode is disabled, it transparently renders its children (or null) with zero overhead. */ class HelpBadge extends PureComponent { constructor(props) { super(props); this.badgeRef = null; this.helpId = props.id || `${props.title || 'help'}_${++nextHelpBadgeId}`; const isInitiallyOpen = Boolean( helpAutomat.state?.enabled && helpAutomat.state?.activeHelpId && helpAutomat.state.activeHelpId === this.helpId ); this.state = { enabled: helpAutomat.state?.enabled ?? false, isOpen: isInitiallyOpen, anchorEl: null, }; this.handleBadgeClick = this.handleBadgeClick.bind(this); this.handleBadgeKeyDown = this.handleBadgeKeyDown.bind(this); this.handleClosePopper = this.handleClosePopper.bind(this); } componentDidMount() { this.unsub = helpAutomat.subscribe(this, (state) => { const enabled = Boolean(state?.enabled); const isOpen = Boolean(enabled && state?.activeHelpId && state.activeHelpId === this.helpId); return { enabled, isOpen, anchorEl: isOpen ? (this.badgeRef || this.state.anchorEl) : null, }; }); } componentWillUnmount() { if (this.state.isOpen) { helpAutomat.actions.closePopper(); } this.unsub?.(); this.unsub = null; } handleBadgeClick(event) { event.preventDefault(); event.stopPropagation(); const currentTarget = event.currentTarget; this.badgeRef = currentTarget; this.setState({ anchorEl: currentTarget }, () => { helpAutomat.actions.togglePopper(this.helpId); }); } handleBadgeKeyDown(event) { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); event.stopPropagation(); const currentTarget = event.currentTarget; this.badgeRef = currentTarget; this.setState({ anchorEl: currentTarget }, () => { helpAutomat.actions.togglePopper(this.helpId); }); } else if (event.key === 'Escape' && this.state.isOpen) { event.preventDefault(); event.stopPropagation(); helpAutomat.actions.closePopper(); } } handleClosePopper(event) { if (this.badgeRef && event?.target && this.badgeRef.contains(event.target)) { return; } if (this.state.isOpen) { helpAutomat.actions.closePopper(); } } render() { const { enabled, isOpen, anchorEl } = this.state; const { children, title, text, help, placement = 'right-start', badgeContent = '?', anchorOrigin = { vertical: 'top', horizontal: 'right' }, badgeSx, sx, } = this.props; const helpText = text || help; if (!enabled || !helpText) { return children ?? null; } const currentAnchor = anchorEl || this.badgeRef; const isPopperVisible = Boolean(isOpen && currentAnchor); const popperElement = ( { if (e.key === 'Escape') this.handleClosePopper(e); }} sx={{ p: 1.5, width: 260, maxWidth: 'calc(100vw - 32px)', bgcolor: 'background.paper', borderRadius: 2, border: '1px solid', borderColor: 'divider', boxShadow: SHADOW_POPOVER, }} > ? {title ? ( {title} ) : null} {helpText} ); // Standalone badge (e.g. absolute corner placement inside a tab) if (!children) { return ( <> { this.badgeRef = el; }} role="button" tabIndex={0} aria-label={`${title ? `${title}: ` : ''}Hilfe anzeigen`} title="Hilfe anzeigen" onClick={this.handleBadgeClick} onKeyDown={this.handleBadgeKeyDown} sx={{ bgcolor: RED, color: WHITE, fontWeight: 800, fontSize: '0.78rem', width: 20, height: 20, minWidth: 20, borderRadius: '50%', boxShadow: SHADOW_BADGE, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', userSelect: 'none', transition: 'background-color 0.15s ease', '&:hover': { bgcolor: RED_HOVER, }, ...badgeSx, ...sx, }} > {badgeContent} {popperElement} ); } // Wrapped element (e.g. icon in navigation sidebar) return ( <> { this.badgeRef = el; }} role="button" tabIndex={0} aria-label={`${title ? `${title}: ` : ''}Hilfe anzeigen`} title="Hilfe anzeigen" onClick={this.handleBadgeClick} onKeyDown={this.handleBadgeKeyDown} style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%', cursor: 'pointer', userSelect: 'none', borderRadius: '50%', }} > {badgeContent} } anchorOrigin={anchorOrigin} sx={{ display: 'inline-flex', '& .MuiBadge-badge': { bgcolor: RED, color: WHITE, fontWeight: 800, fontSize: '0.78rem', width: 20, height: 20, minWidth: 20, borderRadius: '50%', boxShadow: SHADOW_BADGE, cursor: 'pointer', p: 0, transition: 'background-color 0.15s ease', '&:hover': { bgcolor: RED_HOVER, }, ...badgeSx, }, ...sx, }} > {children} {popperElement} ); } } export default HelpBadge;