diff --git a/packages/base/Slider/package.json b/packages/base/Slider/package.json index 478732abbd..3b8a620ebb 100644 --- a/packages/base/Slider/package.json +++ b/packages/base/Slider/package.json @@ -22,7 +22,7 @@ }, "homepage": "https://github.com/toptal/picasso/tree/master/packages/picasso#readme", "dependencies": { - "@mui/base": "5.0.0-beta.58", + "@base-ui/react": "1.2.0", "@toptal/picasso-shared": "15.0.0", "@toptal/picasso-tooltip": "2.0.5", "@toptal/picasso-utils": "4.0.0" diff --git a/packages/base/Slider/src/Slider/Slider.tsx b/packages/base/Slider/src/Slider/Slider.tsx index 91774370d0..d545e95ebd 100644 --- a/packages/base/Slider/src/Slider/Slider.tsx +++ b/packages/base/Slider/src/Slider/Slider.tsx @@ -1,6 +1,6 @@ -// import type { ComponentProps } from 'react' -import React, { forwardRef, useRef } from 'react' -import { Slider as MUIBaseSlider } from '@mui/base/Slider' +import type { ReactNode } from 'react' +import React, { forwardRef, useMemo, useRef } from 'react' +import { Slider as BaseUISlider } from '@base-ui/react/slider' import { useCombinedRefs, useOnScreen } from '@toptal/picasso-utils' import { twJoin, twMerge } from '@toptal/picasso-tailwind-merge' import type { BaseProps } from '@toptal/picasso-shared' @@ -56,7 +56,64 @@ export interface Props extends BaseProps { id?: string } -export const Slider = forwardRef(function Slider( +const computeMarkValues = ({ + marks, + min, + max, + step, +}: { + marks: boolean | undefined + min: number + max: number + step?: number +}): number[] => { + if (!marks) { + return [] + } + const inc = step && step > 0 ? step : 1 + const values: number[] = [] + + for (let next = min; next <= max; next += inc) { + values.push(next) + } + + return values +} + +const isMarkActive = ( + markValue: number, + value: number | number[] | undefined +): boolean => { + if (value === undefined || value === null) { + return false + } + if (Array.isArray(value)) { + const [first, second] = value + const lo = Math.min(first, second) + const hi = Math.max(first, second) + + return markValue >= lo && markValue <= hi + } + + return markValue <= value +} + +const formatValue = ( + raw: number, + index: number, + tooltipFormat: Props['tooltipFormat'] +): ReactNode => { + if (typeof tooltipFormat === 'function') { + return tooltipFormat(raw, index) + } + if (typeof tooltipFormat === 'string') { + return tooltipFormat + } + + return raw +} + +export const Slider = forwardRef(function Slider( { defaultValue = 0, min = 0, max = 100, tooltip = 'off', ...props }, ref ) { @@ -79,7 +136,10 @@ export const Slider = forwardRef(function Slider( 'data-testid': dataTestid, } = props const containerRef = useRef(null) - const sliderRef = useCombinedRefs(ref, useRef(null)) + const sliderRef = useCombinedRefs( + ref, + useRef(null) + ) // The rootMargin is not working correctly in the storybooks iframe // To test properly we can open the iframe in new window @@ -98,69 +158,119 @@ export const Slider = forwardRef(function Slider( const isThumbHidden = hideThumbOnEmpty && (typeof value === 'undefined' || value === null) + const markValues = useMemo( + () => computeMarkValues({ marks, min, max, step }), + [marks, min, max, step] + ) + + const thumbCount = Array.isArray(value) + ? value.length + : Array.isArray(defaultValue) + ? defaultValue.length + : 1 + + const handleValueChange = ( + newValue: number | readonly number[], + eventDetails: BaseUISlider.Root.ChangeEventDetails + ) => { + if (!onChange) { + return + } + + const mapped = Array.isArray(newValue) + ? ([...newValue] as number[]) + : (newValue as number) + + onChange( + eventDetails.event as unknown as Event, + mapped, + eventDetails.activeThumbIndex + ) + } + + const thumbClassName = twJoin( + 'group/thumb flex justify-center items-center w-[15px] h-[15px]', + 'rounded-[50%] bg-blue-500 border-[2px] border-solid border-white', + '-mt-[7px] -ml-[6px] outline-0 [&_input]:outline-none absolute transition-shadow cursor-pointer', + '!translate-none contain-layout', + isThumbHidden && 'hidden' + ) + return (
- + className='block cursor-pointer width-full relative py-[6px] -my-[6px]' + > + + + + + {markValues.map((markValue, index) => { + const percent = ((markValue - min) / (max - min)) * 100 + + return ( + + ) + })} + {(thumbCount === 2 ? ['range-low', 'range-high'] : ['single']).map( + (thumbKey, index) => { + const currentVal = Array.isArray(value) + ? (value[index] as number) + : (value as number | undefined) ?? min + + return ( + + + {formatValue(currentVal, index, tooltipFormat)} + + + ) + } + )} + +
) }) diff --git a/packages/base/Slider/src/Slider/__snapshots__/test.tsx.snap b/packages/base/Slider/src/Slider/__snapshots__/test.tsx.snap index 15ad979d69..72d5fd5507 100644 --- a/packages/base/Slider/src/Slider/__snapshots__/test.tsx.snap +++ b/packages/base/Slider/src/Slider/__snapshots__/test.tsx.snap @@ -5,46 +5,60 @@ exports[`Slider renders 1`] = `
- - - - - +
+
+
- - - + +
+
+ `; @@ -54,46 +68,60 @@ exports[`Slider with initial value 1`] = `
- - - - - +
+
+
- - - + +
+
+ `; diff --git a/packages/base/Slider/src/SliderMark/SliderMark.tsx b/packages/base/Slider/src/SliderMark/SliderMark.tsx index 7b6db22a00..5dffd4d068 100644 --- a/packages/base/Slider/src/SliderMark/SliderMark.tsx +++ b/packages/base/Slider/src/SliderMark/SliderMark.tsx @@ -5,7 +5,7 @@ import { getBgColor } from '../utils' export type SliderMarkProps = { markActive: boolean - ownerState: { value: number } + value?: number | readonly number[] style: React.CSSProperties 'data-index': number forceInactive: boolean @@ -15,7 +15,7 @@ export type SliderMarkProps = { // different bg color based on value of the Slider const SliderMark = ({ markActive, - ownerState, + value, 'data-index': dataIndex, style, forceInactive, @@ -26,7 +26,7 @@ const SliderMark = ({ style={style} className={twJoin( 'absolute w-[6px] h-[6px] rounded-[50%] border-[2px] top-[1.5px] border-solid border-white opacity-100 -translate-x-2/4 box-content', - getBgColor({ markActive, forceInactive, value: ownerState.value }) + getBgColor({ markActive, forceInactive, value }) )} /> ) diff --git a/packages/base/Slider/src/SliderValueLabel/SliderValueLabel.tsx b/packages/base/Slider/src/SliderValueLabel/SliderValueLabel.tsx index 9de8b83af2..60343e8a4d 100644 --- a/packages/base/Slider/src/SliderValueLabel/SliderValueLabel.tsx +++ b/packages/base/Slider/src/SliderValueLabel/SliderValueLabel.tsx @@ -1,5 +1,4 @@ -import type { SliderValueLabelSlotProps } from '@mui/base/Slider' -import type { RefObject } from 'react' +import type { ReactNode, RefObject } from 'react' import React, { useEffect, useRef, useState } from 'react' import { twJoin } from '@toptal/picasso-tailwind-merge' @@ -26,21 +25,26 @@ const yPlacementClasses = { top: 'bottom-[calc(100%+2px)]', } as const -const SliderValueLabel = ({ - children, - index = -1, - tooltip = 'off', - onRender, - yPlacement, - isOverlaped, - ownerState: { value }, -}: SliderValueLabelSlotProps & { +export type SliderValueLabelProps = { + children: ReactNode + index: number + value: number tooltip: ValueLabelDisplay yPlacement: 'top' | 'bottom' /** indicates if there are two SliderValueLabels that overlap each other */ isOverlaped: boolean onRender: (index: number, ref: RefObject) => void -}) => { +} + +const SliderValueLabel = ({ + children, + index, + value, + tooltip, + yPlacement, + isOverlaped, + onRender, +}: SliderValueLabelProps) => { const ref = useRef(null) // we need to change the placement of the label if it is overlaped diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9fdf6e4358..48e6d515db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2619,9 +2619,9 @@ importers: packages/base/Slider: dependencies: - '@mui/base': - specifier: 5.0.0-beta.58 - version: 5.0.0-beta.58(@types/react@17.0.39)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@base-ui/react': + specifier: 1.2.0 + version: 1.2.0(@types/react@17.0.39)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@toptal/picasso-shared': specifier: 15.0.0 version: link:../../shared @@ -4840,6 +4840,10 @@ packages: resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + '@babel/standalone@7.23.1': resolution: {integrity: sha512-a4muOYz1qUaSoybuUKwK90mRG4sf5rBeUbuzpuGLzG32ZDE/Y2YEebHDODFJN+BtyOKi19hrLfq2qbNyKMx0TA==} engines: {node: '>=6.9.0'} @@ -4856,6 +4860,27 @@ packages: resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} engines: {node: '>=6.9.0'} + '@base-ui/react@1.2.0': + resolution: {integrity: sha512-O6aEQHcm+QyGTFY28xuwRD3SEJGZOBDpyjN2WvpfWYFVhg+3zfXPysAILqtM0C1kWC82MccOE/v1j+GHXE4qIw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': '17' + react: ^18.2.0 + react-dom: ^18.2.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@base-ui/utils@0.2.5': + resolution: {integrity: sha512-oYC7w0gp76RI5MxprlGLV0wze0SErZaRl3AAkeP3OnNB/UBMb6RqNf6ZSIlxOc9Qp68Ab3C2VOcJQyRs7Xc7Vw==} + peerDependencies: + '@types/react': '17' + react: ^18.2.0 + react-dom: ^18.2.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@base2/pretty-print-object@1.0.1': resolution: {integrity: sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==} @@ -5207,15 +5232,30 @@ packages: '@floating-ui/core@1.6.8': resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==} + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + '@floating-ui/dom@1.6.11': resolution: {integrity: sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==} + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + '@floating-ui/react-dom@2.1.2': resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==} peerDependencies: react: ^18.2.0 react-dom: ^18.2.0 + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: ^18.2.0 + react-dom: ^18.2.0 + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@floating-ui/utils@0.2.8': resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==} @@ -7823,6 +7863,7 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@vercel/ncc@0.38.3': resolution: {integrity: sha512-rnK6hJBS6mwc+Bkab+PGPs9OiS0i/3kdTO+CkI8V0/VrW3vmz7O2Pxjw/owOlmo6PKEIxRSeZKv/kuL9itnpYA==} @@ -15649,6 +15690,9 @@ packages: reselect@4.1.8: resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} @@ -16532,6 +16576,9 @@ packages: engines: {node: '>=18.18.0'} hasBin: true + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + table@6.8.1: resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==} engines: {node: '>=10.0.0'} @@ -17236,6 +17283,11 @@ packages: peerDependencies: react: ^18.2.0 + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^18.2.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -18913,6 +18965,8 @@ snapshots: '@babel/runtime@7.28.4': {} + '@babel/runtime@7.29.2': {} + '@babel/standalone@7.23.1': {} '@babel/template@7.27.2': @@ -18962,6 +19016,30 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@base-ui/react@1.2.0(@types/react@17.0.39)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.29.2 + '@base-ui/utils': 0.2.5(@types/react@17.0.39)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@floating-ui/react-dom': 2.1.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@floating-ui/utils': 0.2.11 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tabbable: 6.4.0 + use-sync-external-store: 1.6.0(react@18.2.0) + optionalDependencies: + '@types/react': 17.0.39 + + '@base-ui/utils@0.2.5(@types/react@17.0.39)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.29.2 + '@floating-ui/utils': 0.2.11 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + reselect: 5.1.1 + use-sync-external-store: 1.6.0(react@18.2.0) + optionalDependencies: + '@types/react': 17.0.39 + '@base2/pretty-print-object@1.0.1': {} '@bcoe/v8-coverage@0.2.3': {} @@ -19498,17 +19576,34 @@ snapshots: dependencies: '@floating-ui/utils': 0.2.8 + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + '@floating-ui/dom@1.6.11': dependencies: '@floating-ui/core': 1.6.8 '@floating-ui/utils': 0.2.8 + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + '@floating-ui/react-dom@2.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@floating-ui/dom': 1.6.11 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + '@floating-ui/react-dom@2.1.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + '@floating-ui/utils@0.2.11': {} + '@floating-ui/utils@0.2.8': {} '@gar/promise-retry@1.0.3': {} @@ -33583,6 +33678,8 @@ snapshots: reselect@4.1.8: {} + reselect@5.1.1: {} + resize-observer-polyfill@1.5.1: {} resolve-alpn@1.2.1: {} @@ -34663,6 +34760,8 @@ snapshots: transitivePeerDependencies: - typescript + tabbable@6.4.0: {} + table@6.8.1: dependencies: ajv: 8.17.1 @@ -35422,6 +35521,10 @@ snapshots: react: 18.2.0 tslib: 1.14.1 + use-sync-external-store@1.6.0(react@18.2.0): + dependencies: + react: 18.2.0 + util-deprecate@1.0.2: {} util.promisify@1.0.0: