@chayns-components/core
Version:
A set of beautiful React components for developing your own applications with chayns.
398 lines (397 loc) • 14.2 kB
JavaScript
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