@helpwave/hightide
Version:
helpwave's component and theming library
268 lines (264 loc) • 8.8 kB
JavaScript
// src/components/user-action/ScrollPicker.tsx
import { useCallback, useEffect, useState } from "react";
import clsx from "clsx";
// src/util/noop.ts
var noop = () => void 0;
// src/util/array.ts
var defaultRangeOptions = {
allowEmptyRange: false,
stepSize: 1,
exclusiveStart: false,
exclusiveEnd: true
};
var range = (endOrRange, options) => {
const { allowEmptyRange, stepSize, exclusiveStart, exclusiveEnd } = { ...defaultRangeOptions, ...options };
let start = 0;
let end;
if (typeof endOrRange === "number") {
end = endOrRange;
} else {
start = endOrRange[0];
end = endOrRange[1];
}
if (!exclusiveEnd) {
end -= 1;
}
if (exclusiveStart) {
start += 1;
}
if (end - 1 < start) {
if (!allowEmptyRange) {
console.warn(`range: end (${end}) < start (${start}) should be allowed explicitly, set options.allowEmptyRange to true`);
}
return [];
}
return Array.from({ length: end - start }, (_, index) => index * stepSize + start);
};
var getNeighbours = (list, item, neighbourDistance = 2) => {
const index = list.indexOf(item);
const totalItems = neighbourDistance * 2 + 1;
if (list.length < totalItems) {
console.warn("List is to short");
return list;
}
if (index === -1) {
console.error("item not found in list");
return list.splice(0, totalItems);
}
let start = index - neighbourDistance;
if (start < 0) {
start += list.length;
}
const end = (index + neighbourDistance + 1) % list.length;
const result = [];
let ignoreOnce = list.length === totalItems;
for (let i = start; i !== end || ignoreOnce; i = (i + 1) % list.length) {
result.push(list[i]);
if (end === i && ignoreOnce) {
ignoreOnce = false;
}
}
return result;
};
// src/util/math.ts
var clamp = (value, min = 0, max = 1) => {
return Math.min(Math.max(value, min), max);
};
// src/components/user-action/ScrollPicker.tsx
import { jsx, jsxs } from "react/jsx-runtime";
var up = 1;
var down = -1;
var ScrollPicker = ({
options,
mapping,
selected,
onChange = noop,
disabled = false
}) => {
let selectedIndex = 0;
if (selected && options.indexOf(selected) !== -1) {
selectedIndex = options.indexOf(selected);
}
const [{
currentIndex,
transition,
items,
lastTimeStamp
}, setAnimation] = useState({
targetIndex: selectedIndex,
currentIndex: disabled ? selectedIndex : 0,
velocity: 0,
animationVelocity: Math.floor(options.length / 2),
transition: 0,
items: options
});
const itemsShownCount = 5;
const shownItems = getNeighbours(range(items.length), currentIndex).map((index) => ({
name: mapping(items[index]),
index
}));
const itemHeight = 40;
const distance = 8;
const containerHeight = itemHeight * (itemsShownCount - 2) + distance * (itemsShownCount - 2 + 1);
const getDirection = useCallback((targetIndex, currentIndex2, transition2, length) => {
if (targetIndex === currentIndex2) {
return transition2 > 0 ? up : down;
}
let distanceForward = targetIndex - currentIndex2;
if (distanceForward < 0) {
distanceForward += length;
}
return distanceForward >= length / 2 ? down : up;
}, []);
const animate = useCallback((timestamp, startTime) => {
setAnimation((prevState) => {
const {
targetIndex,
currentIndex: currentIndex2,
transition: transition2,
animationVelocity,
velocity,
items: items2,
lastScrollTimeStamp
} = prevState;
if (disabled) {
return { ...prevState, currentIndex: targetIndex, velocity: 0, lastTimeStamp: timestamp };
}
if (targetIndex === currentIndex2 && velocity === 0 && transition2 === 0 || !startTime) {
return { ...prevState, lastTimeStamp: timestamp };
}
const progress = (timestamp - startTime) / 1e3;
const direction = getDirection(targetIndex, currentIndex2, transition2, items2.length);
let newVelocity = velocity;
let usedVelocity;
let newCurrentIndex = currentIndex2;
const isAutoScrolling = velocity === 0 && (!lastScrollTimeStamp || timestamp - lastScrollTimeStamp > 300);
const newLastScrollTimeStamp = velocity !== 0 ? timestamp : lastScrollTimeStamp;
if (isAutoScrolling) {
usedVelocity = direction * animationVelocity;
} else {
usedVelocity = velocity;
newVelocity = velocity * 0.5;
if (Math.abs(newVelocity) <= 0.05) {
newVelocity = 0;
}
}
let newTransition = transition2 + usedVelocity * progress;
const changeThreshold = 0.5;
while (newTransition >= changeThreshold) {
if (newCurrentIndex === targetIndex && newTransition >= changeThreshold && isAutoScrolling) {
newTransition = 0;
break;
}
newCurrentIndex = (currentIndex2 + 1) % items2.length;
newTransition -= 1;
}
if (newTransition >= changeThreshold) {
newTransition = 0;
}
while (newTransition <= -changeThreshold) {
if (newCurrentIndex === targetIndex && newTransition <= -changeThreshold && isAutoScrolling) {
newTransition = 0;
break;
}
newCurrentIndex = currentIndex2 === 0 ? items2.length - 1 : currentIndex2 - 1;
newTransition += 1;
}
let newTargetIndex = targetIndex;
if (!isAutoScrolling) {
newTargetIndex = newCurrentIndex;
}
if ((currentIndex2 !== newTargetIndex || newTargetIndex !== targetIndex) && newTargetIndex === newCurrentIndex) {
onChange(items2[newCurrentIndex]);
}
return {
targetIndex: newTargetIndex,
currentIndex: newCurrentIndex,
animationVelocity,
transition: newTransition,
velocity: newVelocity,
items: items2,
lastTimeStamp: timestamp,
lastScrollTimeStamp: newLastScrollTimeStamp
};
});
}, [disabled, getDirection, onChange]);
useEffect(() => {
requestAnimationFrame((timestamp) => animate(timestamp, lastTimeStamp));
});
const opacity = (transition2, index, itemsCount) => {
const max = 100;
const min = 0;
const distance2 = max - min;
let opacityValue = min;
const unitTransition = clamp(transition2 / 0.5);
if (index === 1 || index === itemsCount - 2) {
if (index === 1 && transition2 > 0) {
opacityValue += Math.floor(unitTransition * distance2);
}
if (index === itemsCount - 2 && transition2 < 0) {
opacityValue += Math.floor(unitTransition * distance2);
}
} else {
opacityValue = max;
}
return clamp(1 - opacityValue / max);
};
return /* @__PURE__ */ jsx(
"div",
{
className: "relative overflow-hidden",
style: { height: containerHeight },
onWheel: (event) => {
if (event.deltaY !== 0) {
setAnimation(({ velocity, ...animationData }) => ({ ...animationData, velocity: velocity + event.deltaY }));
}
},
children: /* @__PURE__ */ jsxs("div", { className: "absolute top-1/2 -translate-y-1/2 -translate-x-1/2 left-1/2", children: [
/* @__PURE__ */ jsx(
"div",
{
className: "absolute z-[1] top-1/2 -translate-y-1/2 -translate-x-1/2 left-1/2 w-full min-w-[40px] border border-divider/50 border-y-2 border-x-0 ",
style: { height: `${itemHeight}px` }
}
),
/* @__PURE__ */ jsx(
"div",
{
className: "flex-col-2 select-none",
style: {
transform: `translateY(${-transition * (distance + itemHeight)}px)`,
columnGap: `${distance}px`
},
children: shownItems.map(({ name, index }, arrayIndex) => /* @__PURE__ */ jsx(
"div",
{
className: clsx(
`flex-col-2 items-center justify-center rounded-md`,
{
"text-primary font-bold": currentIndex === index,
"text-on-background": currentIndex === index,
"cursor-pointer": !disabled,
"cursor-not-allowed": disabled
}
),
style: {
opacity: currentIndex !== index ? opacity(transition, arrayIndex, shownItems.length) : void 0,
height: `${itemHeight}px`,
maxHeight: `${itemHeight}px`
},
onClick: () => !disabled && setAnimation((prevState) => ({ ...prevState, targetIndex: index })),
children: name
},
index
))
}
)
] })
}
);
};
export {
ScrollPicker
};
//# sourceMappingURL=ScrollPicker.mjs.map