UNPKG

@chayns-components/core

Version:

A set of beautiful React components for developing your own applications with chayns.

355 lines (345 loc) • 14.1 kB
import clsx from 'clsx'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useElementSize, useMeasuredClone } from '../../hooks/element'; import { useKeyboardFocusHighlighting } from '../../hooks/useKeyboardFocusHighlighting'; import { useIsTouch } from '../../utils/environment'; import ContextMenu from '../context-menu/ContextMenu'; import ActionButton from './action-button/ActionButton'; import { StyledMultiActionButton, StyledSeparator } from './MultiActionButton.styles'; import { getMinimumPrimaryLabelVisibleWidth, getMultiActionButtonAutoCollapseMode, getSecondaryContextMenuTriggerStyle, MultiActionButtonAutoCollapseMode } from './MultiActionButton.utils'; import { MultiActionButtonHeight } from './MultiActionButton.types'; const SECONDARY_CONTEXT_MENU_ACTION = { icon: 'fa fa-chevron-down', label: undefined }; const createMeasuredMultiActionButtonContent = ({ backgroundColor, gapColor, height, primaryAction, secondaryAction }) => /*#__PURE__*/React.createElement(StyledMultiActionButton, { style: { width: 'fit-content' } }, /*#__PURE__*/React.createElement(ActionButton, { action: primaryAction, actionType: "primary", backgroundColor: primaryAction.backgroundColor ?? backgroundColor, isCollapsed: false, isDisabled: false, isShrunk: false, isSolo: !secondaryAction, showLabel: true, shouldUseContentWidth: true, height: height }), secondaryAction && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(StyledSeparator, { $gapColor: gapColor, $isHidden: false }), /*#__PURE__*/React.createElement(ActionButton, { action: secondaryAction, actionType: "secondary", backgroundColor: secondaryAction.backgroundColor ?? backgroundColor, isCollapsed: false, isDisabled: false, isExpanded: false, isHidden: false, showLabel: false, shouldUseContentWidth: true, height: height }))); /** * Multi-action button with optional secondary action that can expand on hover/click. */ const MultiActionButton = ({ backgroundColor, className, extendedTimeoutMs = 3000, gapColor, height = MultiActionButtonHeight.Medium, isCollapsed = false, isDisabled = false, primaryAction, secondaryAction, secondaryContextMenu, shouldAutoCollapse = false, shouldUseFullWidth, shouldEnableKeyboardHighlighting, width }) => { const [isExtendedByClick, setIsExtendedByClick] = useState(false); const [isSecondaryExpanded, setIsSecondaryExpanded] = useState(false); const [isSecondaryHovered, setIsSecondaryHovered] = useState(false); const [isSecondaryFocused, setIsSecondaryFocused] = useState(false); const autoCollapseTimeoutRef = useRef(null); const multiActionButtonRef = useRef(null); const secondaryContextMenuRef = useRef(null); const [autoCollapseMode, setAutoCollapseMode] = useState(MultiActionButtonAutoCollapseMode.Expanded); const isTouch = useIsTouch(); const parentSize = useElementSize(multiActionButtonRef, { shouldUseParentElement: true }); const hasSecondaryContextMenu = Boolean(secondaryContextMenu?.length); const secondaryTriggerAction = hasSecondaryContextMenu ? SECONDARY_CONTEXT_MENU_ACTION : secondaryAction; const hasExpandableSecondaryAction = Boolean(secondaryAction) && !hasSecondaryContextMenu; const hasSecondaryAction = Boolean(secondaryTriggerAction); const shouldUseContentWidth = !width && !shouldUseFullWidth; const shouldShowKeyboardHighlighting = useKeyboardFocusHighlighting(shouldEnableKeyboardHighlighting && !isDisabled); const expandedMeasuredContent = useMemo(() => createMeasuredMultiActionButtonContent({ backgroundColor, gapColor, height, primaryAction, secondaryAction: secondaryTriggerAction }), [backgroundColor, gapColor, height, primaryAction, secondaryTriggerAction]); const { measuredElement, width: expandedMeasuredWidth } = useMeasuredClone({ content: expandedMeasuredContent }); const availableWidth = useMemo(() => { const parentWidth = parentSize?.width; if (typeof width === 'number') { return typeof parentWidth === 'number' ? Math.min(parentWidth, width) : width; } return parentWidth; }, [parentSize?.width, width]); useEffect(() => { if (!shouldAutoCollapse || isCollapsed) { setAutoCollapseMode(MultiActionButtonAutoCollapseMode.Expanded); return; } if (expandedMeasuredWidth <= 0) { return; } setAutoCollapseMode(previousMode => getMultiActionButtonAutoCollapseMode({ availableWidth, expandedWidth: expandedMeasuredWidth, hasSecondaryAction, height, previousMode })); }, [availableWidth, expandedMeasuredWidth, hasSecondaryAction, height, isCollapsed, shouldAutoCollapse]); const isAutoIconsOnly = shouldAutoCollapse && autoCollapseMode === MultiActionButtonAutoCollapseMode.IconsOnly; const isAutoPrimaryOnly = shouldAutoCollapse && autoCollapseMode === MultiActionButtonAutoCollapseMode.PrimaryOnly; const shouldKeepFullWidth = Boolean(shouldUseFullWidth); const shouldHideSecondaryForAutoCollapse = hasSecondaryAction && isAutoPrimaryOnly; const effectiveIsCollapsed = isCollapsed || !shouldKeepFullWidth && isAutoPrimaryOnly; const resolvedSecondaryAction = hasSecondaryAction ? secondaryTriggerAction : undefined; const isSecondaryHidden = effectiveIsCollapsed || shouldHideSecondaryForAutoCollapse; const shouldPreventSecondaryExpansion = isAutoIconsOnly || isSecondaryHidden; const shouldKeepPrimaryLabelVisible = shouldKeepFullWidth && shouldAutoCollapse; const minimumPrimaryLabelVisibleWidth = getMinimumPrimaryLabelVisibleWidth({ hasVisibleSecondaryAction: hasSecondaryAction, height }); const shouldHidePrimaryLabelForMinimumWidth = shouldKeepPrimaryLabelVisible && typeof availableWidth === 'number' && availableWidth <= minimumPrimaryLabelVisibleWidth; const resolvedWidth = useMemo(() => { if (shouldKeepFullWidth) { return '100%'; } if (effectiveIsCollapsed) { return height; } return width ?? 'fit-content'; }, [effectiveIsCollapsed, height, shouldKeepFullWidth, width]); const secondaryContextMenuTriggerStyle = getSecondaryContextMenuTriggerStyle({ height, isCollapsed: isSecondaryHidden, isExpanded: false, shouldUseContentWidth }); /** * Clears the current auto-collapse timer without changing visual state. */ const clearAutoCollapseTimeout = useCallback(() => { if (autoCollapseTimeoutRef.current) { window.clearTimeout(autoCollapseTimeoutRef.current); autoCollapseTimeoutRef.current = null; } }, []); /** * Clears and restarts the auto-collapse timer used after click-triggered expansion. */ const resetAutoCollapseTimeout = useCallback(() => { clearAutoCollapseTimeout(); if (extendedTimeoutMs <= 0) { return; } autoCollapseTimeoutRef.current = window.setTimeout(() => { setIsSecondaryExpanded(false); setIsExtendedByClick(false); }, extendedTimeoutMs); }, [clearAutoCollapseTimeout, extendedTimeoutMs]); /** * Expands the secondary action and remembers that it originated from a click. */ const expandSecondaryByClick = useCallback(() => { setIsSecondaryExpanded(true); setIsExtendedByClick(true); resetAutoCollapseTimeout(); }, [resetAutoCollapseTimeout]); /** * Cleanup timers on unmount. */ useEffect(() => () => { clearAutoCollapseTimeout(); }, [clearAutoCollapseTimeout]); /** * Collapsing the control should also reset any temporary expansion state. */ useEffect(() => { if (effectiveIsCollapsed || shouldPreventSecondaryExpansion) { clearAutoCollapseTimeout(); setIsSecondaryExpanded(false); setIsExtendedByClick(false); setIsSecondaryHovered(false); setIsSecondaryFocused(false); } }, [clearAutoCollapseTimeout, effectiveIsCollapsed, shouldPreventSecondaryExpansion]); /** * Handler for the primary action button. */ const handlePrimaryClick = useCallback(event => { if (isDisabled || primaryAction.isDisabled) { return; } const payload = { action: 'primary', event, isExtended: isSecondaryExpanded, isTouch }; primaryAction.onClick?.(payload); }, [isDisabled, isSecondaryExpanded, isTouch, primaryAction]); /** * Handler for the secondary action button. */ const handleSecondaryClick = useCallback(event => { if (!resolvedSecondaryAction || isSecondaryHidden || isDisabled || resolvedSecondaryAction.isDisabled) { return; } if (hasSecondaryContextMenu) { secondaryContextMenuRef.current?.show(); return; } const payload = { action: 'secondary', event, isExtended: isSecondaryExpanded, isTouch }; resolvedSecondaryAction.onClick?.(payload); if (!shouldPreventSecondaryExpansion) { expandSecondaryByClick(); } }, [expandSecondaryByClick, hasSecondaryContextMenu, isSecondaryHidden, isDisabled, isSecondaryExpanded, isTouch, resolvedSecondaryAction, shouldPreventSecondaryExpansion]); /** * Desktop hover behavior keeps the secondary action expanded while hovered. */ const handleSecondaryMouseEnter = useCallback(() => { if (!secondaryAction || isSecondaryHidden || isTouch || isDisabled || shouldPreventSecondaryExpansion || secondaryAction.isDisabled) { return; } setIsSecondaryHovered(true); if (!isExtendedByClick) { setIsSecondaryExpanded(true); } }, [isSecondaryHidden, isDisabled, isExtendedByClick, isTouch, secondaryAction, shouldPreventSecondaryExpansion]); const handleSecondaryMouseLeave = useCallback(() => { if (isTouch) { return; } setIsSecondaryHovered(false); if (!isExtendedByClick && !effectiveIsCollapsed) { setIsSecondaryExpanded(false); } }, [effectiveIsCollapsed, isExtendedByClick, isTouch]); const handleSecondaryFocus = useCallback(() => { if (!secondaryAction || isSecondaryHidden || isDisabled || shouldPreventSecondaryExpansion || secondaryAction.isDisabled) { return; } setIsSecondaryFocused(true); if (!isExtendedByClick) { setIsSecondaryExpanded(true); } }, [isDisabled, isExtendedByClick, isSecondaryHidden, secondaryAction, shouldPreventSecondaryExpansion]); const handleSecondaryBlur = useCallback(() => { setIsSecondaryFocused(false); if (!isExtendedByClick && !effectiveIsCollapsed) { setIsSecondaryExpanded(false); } }, [effectiveIsCollapsed, isExtendedByClick]); /** * Secondary label is visible when expanded or when hovered/focused. */ const isSecondaryLabelVisible = isSecondaryExpanded || !isTouch && isSecondaryHovered || isSecondaryFocused; return /*#__PURE__*/React.createElement(React.Fragment, null, measuredElement, /*#__PURE__*/React.createElement(StyledMultiActionButton, { className: clsx('beta-chayns-multi-action', className), ref: multiActionButtonRef, style: { maxWidth: '100%', width: resolvedWidth } }, /*#__PURE__*/React.createElement(ActionButton, { action: primaryAction, actionType: "primary", backgroundColor: primaryAction.backgroundColor ?? backgroundColor, isCollapsed: effectiveIsCollapsed, isDisabled: isDisabled, isShrunk: hasSecondaryAction && (!shouldKeepFullWidth && isAutoIconsOnly || isSecondaryExpanded), isSolo: !hasSecondaryAction || isSecondaryHidden || effectiveIsCollapsed, onClick: handlePrimaryClick, showLabel: !effectiveIsCollapsed && !shouldHidePrimaryLabelForMinimumWidth && (shouldKeepPrimaryLabelVisible || !isAutoIconsOnly) && (shouldKeepPrimaryLabelVisible || !shouldHideSecondaryForAutoCollapse) && (!hasExpandableSecondaryAction || !isSecondaryExpanded), shouldShowKeyboardHighlighting: shouldShowKeyboardHighlighting, shouldUseContentWidth: shouldUseContentWidth, height: height }), hasSecondaryAction && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(StyledSeparator, { $gapColor: gapColor, $isHidden: isSecondaryHidden }), hasSecondaryContextMenu ? /*#__PURE__*/React.createElement(ContextMenu, { items: secondaryContextMenu ?? [], ref: secondaryContextMenuRef, shouldDisableClick: true, shouldUseDefaultTriggerStyles: false, shouldHidePopupArrow: true, yOffset: -6, style: secondaryContextMenuTriggerStyle }, /*#__PURE__*/React.createElement(ActionButton, { action: resolvedSecondaryAction ?? SECONDARY_CONTEXT_MENU_ACTION, actionType: "secondary", backgroundColor: resolvedSecondaryAction?.backgroundColor ?? backgroundColor, isCollapsed: false, isDisabled: isDisabled, isExpanded: false, isHidden: isSecondaryHidden, key: "multi-action-secondary-context-menu", onClick: handleSecondaryClick, showLabel: false, shouldShowKeyboardHighlighting: shouldShowKeyboardHighlighting, shouldUseContentWidth: shouldUseContentWidth, height: height })) : /*#__PURE__*/React.createElement(ActionButton, { action: resolvedSecondaryAction ?? secondaryAction ?? SECONDARY_CONTEXT_MENU_ACTION, actionType: "secondary", backgroundColor: resolvedSecondaryAction?.backgroundColor ?? backgroundColor, isCollapsed: false, isDisabled: isDisabled, isExpanded: !isAutoIconsOnly && isSecondaryExpanded, isHidden: isSecondaryHidden, key: "multi-action-secondary-button", onClick: handleSecondaryClick, onMouseEnter: handleSecondaryMouseEnter, onMouseLeave: handleSecondaryMouseLeave, onFocus: handleSecondaryFocus, onBlur: handleSecondaryBlur, showLabel: !isSecondaryHidden && !isAutoIconsOnly && isSecondaryLabelVisible, shouldShowKeyboardHighlighting: shouldShowKeyboardHighlighting, shouldUseContentWidth: shouldUseContentWidth, height: height })))); }; MultiActionButton.displayName = 'MultiActionButton'; export default MultiActionButton; //# sourceMappingURL=MultiActionButton.js.map