import * as React from 'react'; import { tailwind } from '@tesseract-design/web-base'; import { useBrowser } from '@modal-sh/react-utils'; const { tw } = tailwind; const filterOptions = (children: React.ReactNode): React.ReactNode => { const childrenArray = Array.isArray(children) ? children : [children]; return childrenArray.reduce( (optionChildren, child) => { if (!(typeof child === 'object' && child)) { return optionChildren; } if (child.type === 'option') { return [ ...optionChildren, child, ]; } if (child.type === 'optgroup') { return [ ...optionChildren, ...filterOptions(child.props.children) as unknown[], ]; } return optionChildren; }, [], ) as React.ReactNode; }; /* * Caveat for slider: * * Since sliders are not as customizable as other components, especially with orientations, * prefer using sliders where using horizontal sliders would be as acceptable as vertical * sliders and vv. */ export const AVAILABLE_SLIDER_ORIENTATIONS = ['horizontal', 'vertical'] as const; /** * Orientation of the {@link Slider} component. */ export type SliderOrientation = typeof AVAILABLE_SLIDER_ORIENTATIONS[number]; const SliderDerivedElementComponent = 'input'; /** * Derived HTML element of the {@link Slider} component. */ export type SliderDerivedElement = HTMLElementTagNameMap[ typeof SliderDerivedElementComponent ]; /** * Props of the {@link Slider} component. */ export interface SliderProps extends Omit, 'type'> { /** * Orientation of the component. */ orient?: SliderOrientation; /** * Options of the component. */ children?: React.ReactNode; /** * Length of the component. */ length?: React.CSSProperties['width']; } export const sliderPlugin: tailwind.PluginCreator = ({ addComponents }) => { addComponents({ '.slider': { '& > input': { appearance: 'none', cursor: 'pointer', position: 'relative', overflow: 'hidden', height: '1em', 'box-sizing': 'border-box', color: 'rgb(var(--color-primary))', }, '& > input::-webkit-slider-container': { width: '100%', height: '100%', 'min-block-size': '0', 'background-color': 'rgb(var(--color-primary) / 50%)', 'border-radius': '9999px', display: 'block', 'box-sizing': 'border-box', 'background-clip': 'content-box', padding: '0.25em', appearance: 'none', }, '& > input::-webkit-slider-runnable-track': { appearance: 'none', 'border-radius': '9999px', display: 'block', width: '100%', height: '100%', margin: '-0.25em', 'box-sizing': 'border-box', 'background-clip': 'content-box', }, '& > input::-webkit-slider-thumb': { width: '1em', height: '1em', margin: '-0.25em 0 0 0', 'border-radius': '9999px', 'background-color': 'currentColor', appearance: 'none', 'aspect-ratio': '1 / 1', 'z-index': '1', position: 'relative', 'box-shadow': 'calc(-200001em / 2) 0 0 100000em rgb(var(--color-primary) / 50%)', }, '& > input:focus::-webkit-slider-container': { 'background-color': 'rgb(var(--color-secondary) / 50%)', }, '& > input:active::-webkit-slider-container': { 'background-color': 'rgb(var(--color-tertiary) / 50%)', }, '& > input:focus::-webkit-slider-thumb': { 'box-shadow': 'calc(-200001em / 2) 0 0 100000em rgb(var(--color-secondary) / 50%)', }, '& > input:active::-webkit-slider-thumb': { 'box-shadow': 'calc(-200001em / 2) 0 0 100000em rgb(var(--color-tertiary) / 50%)', }, '&[data-orient="horizontal"]': { 'flex-direction': 'column', }, '&[data-firefox]': { 'flex-direction': 'column', }, '&[data-firefox="vertical"]': { 'flex-direction': 'row', }, '&[data-orient="vertical"]': { 'flex-direction': 'column', 'rotate': '-90deg', 'translate': 'calc(-100% + 0.5em * 2)', 'transform-origin': 'calc(100% - 0.5em) 0.5em', }, '& > input::-moz-range-track': { appearance: 'none', 'border-radius': '9999px', 'content': '""', display: 'block', position: 'absolute', 'background-color': 'currentColor', 'opacity': '0.5', width: 'calc(100% - 0.5em)', height: '50%', top: '25%', margin: '-0.25em', }, '& > input::-moz-range-thumb': { height: '100%', outline: '0', border: '0', 'border-radius': '9999px', 'background-color': 'currentColor', appearance: 'none', 'aspect-ratio': '1 / 1', 'z-index': '1', position: 'relative', 'box-shadow': 'calc(-200001em / 2) 0 0 100000em rgb(var(--color-primary) / 50%)', }, '& > input:focus::-moz-range-thumb': { 'box-shadow': 'calc(-200001em / 2) 0 0 100000em rgb(var(--color-secondary) / 50%)', }, '& > input:active::-moz-range-thumb': { 'box-shadow': 'calc(-200001em / 2) 0 0 100000em rgb(var(--color-tertiary) / 50%)', }, '& > input[orient="vertical"]': { width: '1em', height: '16em', }, '& > input[orient="vertical"]::-moz-range-track': { width: '50%', height: 'calc(100% - 0.5em)', }, '& > input[orient="vertical"]::-moz-range-thumb': { width: '100%', height: '1em', 'box-shadow': '0 calc(200001em / 2) 0 100000em rgb(var(--color-primary) / 50%)', }, '& > input[orient="vertical"]:focus::-moz-range-thumb': { 'box-shadow': '0 calc(200001em / 2) 0 100000em rgb(var(--color-secondary) / 50%)', }, '& > input[orient="vertical"]:active::-moz-range-thumb': { 'box-shadow': '0 calc(200001em / 2) 0 100000em rgb(var(--color-tertiary) / 50%)', }, '&[data-chrome] > input + *': { 'padding': '0 0.5em', 'height': '1.5em', }, '&[data-firefox="horizontal"] > input + *': { 'padding': '0 0.5em', 'height': '1.5em', }, '&[data-firefox="vertical"] > input + *': { 'padding': '0.5em 0', 'width': '1.5em', }, }, }); }; /** * Component for inputting continuous numeric values. */ export const Slider = React.forwardRef(( { className, style, children, orient = 'horizontal', length, min = 0, max = 100, ...etcProps }, forwardedRef, ) => { const browser = useBrowser(); const defaultRef = React.useRef(null); const ref = forwardedRef ?? defaultRef; const tickMarkId = React.useId(); React.useEffect(() => { const setupGecko = (theOrient: string, theChildren: unknown, theBrowser?: string) => { if (!(typeof ref === 'object' && ref)) { return; } const { current: slider } = ref; if (!slider) { return; } const isFirefox = theBrowser === 'firefox'; const wrapper = slider?.parentElement as HTMLElement; const parent = wrapper?.parentElement as HTMLElement; const grandParent = parent?.parentElement as HTMLElement; if (isFirefox) { slider.setAttribute('orient', theOrient); wrapper.dataset[theBrowser] = theOrient; wrapper.removeAttribute('data-orient'); grandParent.style.width = theChildren ? '2.5em' : '1em'; } }; setupGecko(orient, children, browser); return () => { if (!(typeof ref === 'object' && ref)) { return; } const { current: slider } = ref; if (!slider) { return; } const isFirefox = browser === 'firefox'; const wrapper = slider?.parentElement as HTMLElement; const parent = wrapper?.parentElement as HTMLElement; const grandParent = parent?.parentElement as HTMLElement; if (slider && isFirefox && parent && grandParent && wrapper) { grandParent.style.width = 'auto'; wrapper.removeAttribute(`data-${browser}`); wrapper.dataset.orient = slider.getAttribute(orient) ?? undefined; slider.removeAttribute('orient'); } }; }, [ref, orient, children, browser]); React.useEffect(() => { let shouldEffectExecute: boolean; const setupNonGecko = (theOrient: string, theBrowser?: string) => { if (!(typeof ref === 'object' && ref)) { return; } const { current: slider } = ref; if (!slider) { return; } if (!theBrowser) { return; } const wrapper = slider?.parentElement as HTMLElement; const parent = wrapper?.parentElement as HTMLElement; const grandParent = parent?.parentElement as HTMLElement; const isNotFirefox = theBrowser !== 'firefox'; if (isNotFirefox) { wrapper.dataset[theBrowser] = theOrient; } shouldEffectExecute = isNotFirefox && theOrient === 'vertical' && Boolean(slider) && Boolean(parent) && Boolean(grandParent); if (shouldEffectExecute) { const trueHeight = parent.clientWidth; const trueWidth = parent.clientHeight; grandParent.dataset.height = grandParent.clientHeight.toString(); grandParent.dataset.width = grandParent.clientWidth.toString(); grandParent.style.height = `${trueHeight}px`; grandParent.style.width = `${trueWidth}px`; } }; setupNonGecko(orient, browser); return () => { if (!(typeof ref === 'object' && ref)) { return; } const { current: slider } = ref; if (!slider) { return; } const wrapper = slider?.parentElement as HTMLElement; const parent = wrapper?.parentElement as HTMLElement; const grandParent = parent?.parentElement as HTMLElement; if (shouldEffectExecute) { grandParent.style.height = `${grandParent.dataset.height ?? 0}px`; grandParent.style.width = `${grandParent.dataset.width ?? 0}px`; grandParent.removeAttribute('data-height'); grandParent.removeAttribute('data-width'); } wrapper.removeAttribute(`data-${browser ?? 'unknown'}`); }; }, [ref, orient, browser]); React.useEffect(() => { if (!(typeof ref === 'object' && ref)) { return; } const { current: slider } = ref; if (!slider) { return; } const isFirefox = browser === 'firefox'; const isNotFirefox = typeof browser === 'string' && browser !== 'firefox'; const tickMarkContainer = slider.nextElementSibling; if (tickMarkContainer) { const tickMarks = tickMarkContainer.children[0].children; Array.from(tickMarks).forEach((tickMarkRaw) => { const tickMark = tickMarkRaw as HTMLElement; const offset = tickMark.dataset.offset as string; const tickMarkWrapper = tickMark.children[0] as HTMLElement; const tickMarkLine = tickMarkWrapper.children[0] as HTMLElement; const tickMarkLabel = tickMarkLine.nextElementSibling as HTMLElement | null; if (isNotFirefox) { tickMark.style.left = offset; tickMark.style.bottom = ''; tickMarkWrapper.style.translate = '-50%'; tickMarkWrapper.style.flexDirection = 'column'; tickMarkLine.style.width = '1px'; tickMarkLine.style.height = '0.5em'; if (tickMarkLabel) { tickMarkLabel.style.writingMode = 'initial'; } } else if (isFirefox && orient === 'horizontal') { tickMark.style.left = offset; tickMark.style.bottom = ''; tickMarkWrapper.style.translate = '-50%'; tickMarkWrapper.style.flexDirection = 'column'; tickMarkLine.style.width = '1px'; tickMarkLine.style.height = '0.5em'; if (tickMarkLabel) { tickMarkLabel.style.writingMode = 'initial'; } } else { tickMark.style.bottom = offset; tickMark.style.left = ''; tickMarkWrapper.style.translate = '0 50%'; tickMarkWrapper.style.flexDirection = 'row'; tickMarkLine.style.width = '0.5em'; tickMarkLine.style.height = '1px'; if (tickMarkLabel) { tickMarkLabel.style.writingMode = 'sideways-lr'; } } }); } }, [ref, orient, browser, children]); const block = typeof length === 'string' && length.trim() === '100%'; const minValue = Number(min); const maxValue = Number(max); if (minValue >= maxValue) { return null; } const childrenValues = children ? filterOptions(children) : undefined; return ( <> {children && ( {children} )}
{Array.isArray(childrenValues) && (
{ childrenValues .filter((c) => { const value = Number(c.props.value); return minValue <= value && value <= maxValue; }) .map((c) => { const value = Number(c.props.value); return (
{c.props.children}
); }) }
)}
); }); Slider.displayName = 'Slider'; Slider.defaultProps = { orient: 'horizontal', length: undefined, children: undefined, };