UNPKG

@chayns-components/core

Version:

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

398 lines (397 loc) • 14.2 kB
import { setRefreshScrollEnabled } from 'chayns-api'; import { AnimatePresence, useAnimate } from 'motion/react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useElementSize } from '../../hooks/element'; import { useKeyboardFocusHighlighting } from '../../hooks/useKeyboardFocusHighlighting'; import { useColorScheme } from '../color-scheme-provider/ColorSchemeProvider'; import { calculateBiggestWidth } from '../../utils/calculate'; import { getNearestPoint, getThumbPosition } from '../../utils/sliderButton'; import Icon from '../icon/Icon'; import Popup from '../popup/Popup'; import { StyledMotionSliderButtonThumb, StyledSliderButton, StyledSliderButtonButtonsWrapper, StyledSliderButtonItem, StyledSliderButtonPopupContent, StyledSliderButtonPopupContentItem, StyledSliderButtonWrapper } from './SliderButton.styles'; import { useTheme } from 'styled-components'; import { useSliderButtonPopupKeyboard } from './useSliderButtonPopupKeyboard'; import { useSliderButtonThumbKeyboard } from './useSliderButtonThumbKeyboard'; const SliderButton = ({ isDisabled, isSecondary, items, onChange, selectedButtonId, isRounded = false, shouldEnableKeyboardHighlighting }) => { const [dragRange, setDragRange] = useState({ left: 0, right: 0 }); const [shownItemsCount, setShownItemsCount] = useState(items.length); const [sliderSize, setSliderSize] = useState({ width: 0 }); const [currentId, setCurrentId] = useState(''); const [currentPopupId, setCurrentPopupId] = useState(''); const [currentIndex, setCurrentIndex] = useState(0); const [isPopupOpen, setIsPopupOpen] = useState(false); const sliderButtonRef = useRef(null); const sliderButtonWrapperRef = useRef(null); const popupRef = useRef(null); const popupItemRefs = useRef([]); const [scope, animate] = useAnimate(); const theme = useTheme(); const colorScheme = useColorScheme(); const shouldEnableKeyboardHighlightingEffective = shouldEnableKeyboardHighlighting ?? colorScheme?.shouldEnableKeyboardHighlighting ?? false; const shouldShowKeyboardHighlighting = useKeyboardFocusHighlighting(shouldEnableKeyboardHighlightingEffective && !isDisabled); const initialItemWidth = useMemo(() => calculateBiggestWidth(items), [items]); const elementSize = useElementSize(sliderButtonRef); useEffect(() => { if (elementSize) setSliderSize(elementSize); }, [elementSize]); const setPopupId = useCallback(selectedId => { const ids = items.slice(shownItemsCount - 1).map(({ id }) => id); const newId = ids.find(id => id === selectedId); if (newId) { setCurrentId('more'); setCurrentPopupId(newId); return; } setCurrentId(selectedId); }, [items, shownItemsCount]); const isSliderBigger = useMemo(() => sliderSize && Math.floor(sliderSize.width / initialItemWidth) < items.length - 1, [initialItemWidth, items.length, sliderSize]); const maxShownItemsCount = useMemo(() => { let totalWidth = 0; let count = 0; while (count < items.length) { const visibleItems = items.slice(0, count + 1); const currentMaxWidth = calculateBiggestWidth(visibleItems) + 8; if (totalWidth + currentMaxWidth > sliderSize.width) break; totalWidth += currentMaxWidth; count++; } return count; }, [items, sliderSize.width]); const itemWidth = useMemo(() => { const sliderWidth = sliderSize?.width || 0; const itemCount = items.length || 1; setShownItemsCount(isSliderBigger ? maxShownItemsCount : itemCount); return sliderWidth / (isSliderBigger ? maxShownItemsCount : itemCount); }, [isSliderBigger, items.length, maxShownItemsCount, sliderSize?.width]); useEffect(() => { if (sliderSize) { const sliderWidth = itemWidth * (items.length - 1); const count = Math.floor(sliderSize.width / itemWidth); setDragRange({ left: 0, right: isSliderBigger ? itemWidth * count : sliderWidth }); } }, [isSliderBigger, itemWidth, items.length, sliderSize]); const animation = useCallback(async x => { await animate(scope.current, { x }, { type: 'tween', duration: 0.2 }); }, [animate, scope]); const setItemPosition = useCallback(index => { setCurrentIndex(index); void animation(itemWidth * index); }, [animation, itemWidth]); useEffect(() => { if (typeof selectedButtonId === 'string') { let index = items.findIndex(({ id }) => id === selectedButtonId); setCurrentId(selectedButtonId); setPopupId(selectedButtonId); if (items.length > shownItemsCount && index > shownItemsCount - 1) { index = shownItemsCount - 1; } if (index >= 0) { setItemPosition(index); } } }, [animation, dragRange.right, isSliderBigger, itemWidth, items, selectedButtonId, setItemPosition, setPopupId, shownItemsCount]); const handleClick = useCallback((id, index) => { if (isDisabled) { return; } if (currentIndex === index && items.length === 2) { const otherItem = items.find((_, findIndex) => index !== findIndex); if (!otherItem) return; setPopupId(otherItem.id); setItemPosition(items.indexOf(otherItem)); onChange?.(otherItem.id); return; } setPopupId(id); if (typeof onChange === 'function' && id !== 'more') { onChange(id); } if (popupRef.current) { if (id === 'more') { popupRef.current.show(); } else { popupRef.current.hide(); } } setItemPosition(index); }, [currentIndex, isDisabled, items, onChange, setItemPosition, setPopupId]); const backgroundColor = useMemo(() => { let color; if (isSecondary) { color = theme['202']; } else { color = theme.buttonBackgroundColor ?? theme['408']; } if (theme.buttonDesign === '2') { color = `rgba(${theme['102-rgb'] ?? ''}, 0)`; } return color; }, [isSecondary, theme]); const thumbBackgroundColor = useMemo(() => { let color; if (isSecondary) { color = theme['207']; } else { color = `rgba(${theme['405-rgb'] ?? ''}, 0.75)`; } if (theme.buttonDesign === '2') { color = `rgba(${theme['102-rgb'] ?? ''}, 0)`; } return color; }, [isSecondary, theme]); const popupItems = useMemo(() => items.slice(shownItemsCount - 1), [items, shownItemsCount]); const focusThumb = useCallback(() => { const thumb = sliderButtonRef.current?.querySelector('[data-slider-button-thumb="true"]'); thumb?.focus(); }, []); const { handlePopupKeyDown } = useSliderButtonPopupKeyboard({ isPopupOpen, popupItems, currentPopupId, shownItemsCount, popupItemRefs, popupRef, focusThumb, onSelectPopupItem: handleClick }); const { handleThumbKeyDown } = useSliderButtonThumbKeyboard({ currentId, currentIndex, shownItemsCount, items, onSelectThumbItem: handleClick }); const buttons = useMemo(() => { if (items.length > shownItemsCount) { const newItems = items.slice(0, shownItemsCount - 1); const otherItems = items.slice(shownItemsCount - 1); const elements = newItems.map(({ id, text }, index) => /*#__PURE__*/React.createElement(StyledSliderButtonItem, { $isSecondary: isSecondary, $width: itemWidth, key: `slider-button-${id}`, onClick: () => handleClick(id, index) }, text)); const popupContent = otherItems.map(({ id, text }, popupIndex) => /*#__PURE__*/React.createElement(StyledSliderButtonPopupContentItem, { key: `slider-button-${id}`, onClick: () => handleClick(id, newItems.length), onKeyDown: handlePopupKeyDown, ref: element => { popupItemRefs.current[popupIndex] = element; }, tabIndex: -1, role: "button", $isSelected: id === currentPopupId }, text)); const id = 'more'; elements.push(/*#__PURE__*/React.createElement(StyledSliderButtonItem, { $isSecondary: isSecondary, $width: itemWidth, key: `slider-button-${id}` }, /*#__PURE__*/React.createElement(Popup, { ref: popupRef, onShow: () => { setIsPopupOpen(true); }, onHide: () => { setIsPopupOpen(false); popupItemRefs.current = []; }, content: /*#__PURE__*/React.createElement(StyledSliderButtonPopupContent, { onKeyDown: handlePopupKeyDown }, popupContent) }, /*#__PURE__*/React.createElement(Icon, { icons: ['fa fa-ellipsis'], color: "white" })))); return elements; } return items.map(({ id, text }) => /*#__PURE__*/React.createElement(StyledSliderButtonItem, { $isSecondary: isSecondary, $width: itemWidth, key: `slider-button-${id}` }, text)); }, [currentPopupId, handleClick, handlePopupKeyDown, isSecondary, itemWidth, items, shownItemsCount]); const pseudoButtons = useMemo(() => { if (items.length > shownItemsCount) { const newItems = items.slice(0, shownItemsCount - 1); const elements = newItems.map(({ id, text }, index) => /*#__PURE__*/React.createElement(StyledSliderButtonItem, { $isSecondary: isSecondary, $width: itemWidth, key: `pseudo-slider-button-${id}`, onClick: () => handleClick(id, index) }, text)); const id = 'more'; elements.push(/*#__PURE__*/React.createElement(StyledSliderButtonItem, { $isSecondary: isSecondary, $width: itemWidth, key: `pseudo-slider-button-${id}`, onClick: () => handleClick(id, newItems.length) }, /*#__PURE__*/React.createElement(Icon, { icons: ['fa fa-ellipsis'] }))); return elements; } return items.map(({ id, text }, index) => /*#__PURE__*/React.createElement(StyledSliderButtonItem, { $isSecondary: isSecondary, $width: itemWidth, key: `pseudo-slider-button-${id}`, onClick: () => handleClick(id, index) }, text)); }, [handleClick, isSecondary, itemWidth, items, shownItemsCount]); /** * Creates an array with the snap points relative to the width of the items */ const snapPoints = useMemo(() => { const points = [0]; for (let i = 1; i < items.length; i++) { points.push(itemWidth * i); } return points; }, [itemWidth, items.length]); const handleDragStart = useCallback(() => { void setRefreshScrollEnabled(false); }, []); const handleDragEnd = useCallback(() => { void setRefreshScrollEnabled(true); const position = getThumbPosition({ scope, itemWidth }); if (!position) { return; } const { middle, left } = position; let scrollLeft = 0; if (sliderButtonWrapperRef.current) { scrollLeft = sliderButtonWrapperRef.current.scrollLeft; sliderButtonWrapperRef.current.scrollLeft = getNearestPoint({ snapPoints, position: middle, scrollLeft: scrollLeft - left }).nearestPoint; } const { nearestIndex } = getNearestPoint({ snapPoints, position: middle, scrollLeft }); const { nearestPoint } = getNearestPoint({ snapPoints, position: middle, scrollLeft: 0 }); const hasMoreItems = items.length > shownItemsCount; if (nearestPoint >= 0 && nearestIndex >= 0) { void animation(nearestPoint); let id; if (hasMoreItems && nearestIndex === shownItemsCount - 1) { id = 'more'; } else { id = items[nearestIndex]?.id; } if (popupRef.current) { if (id === 'more') { popupRef.current.show(); } else { popupRef.current.hide(); } } if (typeof onChange === 'function' && id) { setPopupId(id); setCurrentIndex(nearestIndex); if (id !== 'more') onChange(id); } } }, [animation, itemWidth, items, onChange, scope, setPopupId, shownItemsCount, snapPoints]); return useMemo(() => /*#__PURE__*/React.createElement(StyledSliderButton, { $isDisabled: isDisabled, ref: sliderButtonRef }, /*#__PURE__*/React.createElement(StyledSliderButtonButtonsWrapper, { $isInvisible: true }, pseudoButtons), /*#__PURE__*/React.createElement(StyledMotionSliderButtonThumb, { $isRounded: isRounded, ref: scope, drag: isDisabled ? false : 'x', dragElastic: 0, dragConstraints: isSliderBigger ? { ...dragRange, right: dragRange.right - itemWidth } : { ...dragRange }, $width: itemWidth, onDragEnd: handleDragEnd, onDragStart: handleDragStart, onClick: () => handleClick(currentId, currentIndex), style: { backgroundColor: thumbBackgroundColor }, "data-slider-button-thumb": "true", tabIndex: shouldEnableKeyboardHighlightingEffective && !isDisabled ? 0 : undefined, role: shouldEnableKeyboardHighlightingEffective && !isDisabled ? 'button' : undefined, onKeyDown: shouldEnableKeyboardHighlightingEffective && !isDisabled ? handleThumbKeyDown : undefined, $shouldShowKeyboardHighlighting: shouldShowKeyboardHighlighting }), /*#__PURE__*/React.createElement(StyledSliderButtonWrapper, { $isRounded: isRounded, $isDisabled: isDisabled, $width: !isSliderBigger ? dragRange.right + itemWidth : dragRange.right, ref: sliderButtonWrapperRef, style: { backgroundColor } }, /*#__PURE__*/React.createElement(AnimatePresence, null, /*#__PURE__*/React.createElement(StyledSliderButtonButtonsWrapper, null, buttons)))), [backgroundColor, buttons, currentId, currentIndex, dragRange, handleClick, handleDragEnd, handleDragStart, handleThumbKeyDown, isDisabled, isRounded, isSliderBigger, itemWidth, pseudoButtons, scope, shouldEnableKeyboardHighlightingEffective, shouldShowKeyboardHighlighting, thumbBackgroundColor]); }; SliderButton.displayName = 'SliderButton'; export default SliderButton; //# sourceMappingURL=SliderButton.js.map