|
- 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<React.HTMLProps<SliderDerivedElement>, '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<SliderDerivedElement, SliderProps>((
- {
- className,
- style,
- children,
- orient = 'horizontal',
- length,
- min = 0,
- max = 100,
- ...etcProps
- },
- forwardedRef,
- ) => {
- const browser = useBrowser();
- const defaultRef = React.useRef<SliderDerivedElement>(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
- && (
- <datalist
- id={tickMarkId}
- >
- {children}
- </datalist>
- )}
- <div
- className={tw(
- block && orient !== 'vertical' && 'w-full',
- !block && 'align-middle',
- className,
- orient !== 'vertical' && 'inline-block min-h-[1rem]',
- orient === 'vertical' && 'inline-block min-w-[1rem]',
- orient !== 'vertical' && !block && 'min-w-[16rem]',
- )}
- style={style}
- >
- <div
- style={{
- width: orient === 'vertical' ? (length ?? '16rem') : undefined,
- position: 'relative',
- }}
- >
- <div
- className="flex slider"
- data-orient={orient}
- >
- <SliderDerivedElementComponent
- {...etcProps}
- ref={ref}
- min={minValue}
- max={maxValue}
- type="range"
- list={childrenValues ? tickMarkId : undefined}
- className={tw(
- 'w-full h-full bg-inherit block text-primary ring-secondary/50 rounded-full',
- 'focus:text-secondary focus:outline-0 focus:ring-4',
- 'active:text-tertiary active:ring-tertiary/50',
- )}
- />
- {Array.isArray(childrenValues) && (
- <div className="select-none">
- <div className="relative w-full h-full">
- {
- childrenValues
- .filter((c) => {
- const value = Number(c.props.value);
- return minValue <= value && value <= maxValue;
- })
- .map((c) => {
- const value = Number(c.props.value);
- return (
- <div
- key={c.props.value}
- className="absolute w-full h-full box-border"
- data-offset={`${((value - minValue) / (maxValue - minValue)) * 100}%`}
- >
- <div
- className="flex h-full text-xs items-center justify-between"
- >
- <div
- className="bg-current"
- />
- <div
- className={c.props.className}
- >
- {c.props.children}
- </div>
- </div>
- </div>
- );
- })
- }
- </div>
- </div>
- )}
- </div>
- </div>
- </div>
- </>
- );
- });
-
- Slider.displayName = 'Slider';
-
- Slider.defaultProps = {
- orient: 'horizontal',
- length: undefined,
- children: undefined,
- };
|