import React from 'react'; import { Tooltip } from '@availity/mui-tooltip'; import { HeartIcon, HeartEmptyIcon } from '@availity/mui-icon'; import { CircularProgress } from '@availity/mui-progress'; import { styled } from '@mui/material/styles'; import { useFavorites } from './Favorites'; const icons = { spinner: , unknownDisabledHeart: , favoritedDisabledHeart: , unfavoritedDisabledHeart: , favoritedHeart: , unfavoritedHeart: , }; const DEFAULT_SIZE = '2.5rem'; interface SizableHeartProps { customSize?: string } const FavoriteHeartContainer = styled('div', { name: 'AvFavoriteHeart', slot: 'Root', })(({ customSize }) => ({ height: customSize, width: customSize, })); const FavoriteInput = styled('input', { name: 'AvFavoriteHeart', slot: 'input', })(({ customSize }) => ({ height: customSize, width: customSize, minHeight: customSize, minWidth: customSize, })); const fontSize = (size = DEFAULT_SIZE) => { // Match the number and unit in the size string const match = size.match(/^(\d*\.?\d+)(px|rem)$/); if (!match) { throw new Error("Invalid size format. Must be in 'px' or 'rem'."); } const [, number, unit] = match; // Parse the number, halve it, and return the new size string const halvedNumber = Number.parseFloat(number) / 2; return `${halvedNumber}${unit}`; } const FavoriteIcon = styled('div', { name: 'AvFavoriteHeart', slot: 'icon', })(({ customSize }) => ({ fontSize: fontSize(customSize), })); // validates the size input is the correct format and at least 24px or 1.5rem const validateSize = (size: string) => { // Match the number and unit in the size string const match = size.match(/^(\d*\.?\d+)(px|rem)$/); if (!match) { return false; // Invalid format } const [, number, unit] = match; const value = Number.parseFloat(number); // Validate the size based on the unit if (unit === 'rem' && value >= 1.5) { return true; } if (unit === 'px' && value >= 24) { return true; } return false; } type FavoriteHeartProps = { /** The configuration's id */ id: string; /** The name of the configuration */ name: string; /** What to do on Favorite Toggle */ onChange?: ( isFavorited: boolean, event: React.ChangeEvent | React.KeyboardEvent ) => void; /** What to do on click */ onMouseDown?: (event: React.MouseEvent) => void; /** Whether or not the Favorite is disabled * @default false */ disabled?: boolean; /** The size of the icon in rem or px, minimum size is 1.5rem / 24px */ customSize?: string; }; export const FavoriteHeart = ({ id, name, onChange, onMouseDown, disabled = false, customSize = DEFAULT_SIZE, }: FavoriteHeartProps): React.JSX.Element => { const { isFavorited, isLastClickedFavorite, status, toggleFavorite } = useFavorites(id); const handleChange = (event: React.ChangeEvent) => { onChange?.(isFavorited, event); toggleFavorite(); }; const handleKeyPress = (event: React.KeyboardEvent) => { if (event.code === 'Enter' || event.key === 'Enter') { onChange?.(isFavorited, event); toggleFavorite(); } }; const iconKey = (() => { if (status === 'initLoading') return 'unknownDisabledHeart'; if (status === 'reloading') { if (isLastClickedFavorite) return 'spinner'; return isFavorited ? 'favoritedDisabledHeart' : 'unfavoritedDisabledHeart'; } if (disabled) { return isFavorited ? 'favoritedDisabledHeart' : 'unfavoritedDisabledHeart'; } if (isFavorited) return 'favoritedHeart'; return 'unfavoritedHeart'; })(); const cursor = disabled || (!isLastClickedFavorite && (status === 'initLoading' || status === 'reloading')) ? 'not-allowed' : undefined; const tooltipContent = `${isFavorited ? 'Remove from' : 'Add to'} My Favorites`; const favoriteInputProps = { onKeyDown: handleKeyPress, type: 'checkbox', 'aria-label': `Favorite ${name}`, id: `av-favorite-heart-${id}`, disabled, checked: isFavorited, onChange: handleChange, onMouseDown, style: { cursor }, }; const validSize = validateSize(customSize) ? customSize : DEFAULT_SIZE; return ( {icons[iconKey]} {isLastClickedFavorite && status === 'reloading' ? 'Loading...' : isLastClickedFavorite && status === 'error' ? 'An error has occurred. Please try again.' : ''} {disabled ? ( ) : ( )} ); };