From d76f1ab00cfd01a16fcde6e062520c1d050aa2d1 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Sun, 7 Jun 2026 13:05:52 +0530 Subject: [PATCH 1/5] feat: show fill for negative values in slider --- src/components/Slider/Slider.vue | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/components/Slider/Slider.vue b/src/components/Slider/Slider.vue index bfdcb2d2a..7d42c8da0 100644 --- a/src/components/Slider/Slider.vue +++ b/src/components/Slider/Slider.vue @@ -53,6 +53,20 @@ const { disabled: () => props.disabled, }) +const isBidirectional = computed(() => props.min < 0) + +const bidirectionalRangeStyles = computed(() => { + const { min, max } = props + const value = sliderValue.value[0] ?? 0 + const range = max - min + const zeroPos = -min / range + const valuePos = (value - min) / range + return { + left: `${Math.min(zeroPos, valuePos) * 100}%`, + right: `${(1 - Math.max(zeroPos, valuePos)) * 100}%`, + } +}) + const trackClasses = computed(() => { return [ 'relative grow rounded', @@ -85,10 +99,10 @@ const onValueCommit = (value: SliderValue) => { const hasLabeling = computed(() => { return Boolean( props.label || - slots.label || - showDescription.value || - slots.description || - hasError.value, + slots.label || + showDescription.value || + slots.description || + hasError.value, ) }) @@ -125,7 +139,8 @@ const hasLabeling = computed(() => { @value-commit="onValueCommit" > - + +
Date: Sun, 7 Jun 2026 13:36:47 +0530 Subject: [PATCH 2/5] fix: slider should fully contain slider thumb height-wise --- src/components/Slider/Slider.vue | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/Slider/Slider.vue b/src/components/Slider/Slider.vue index 7d42c8da0..98c5c306a 100644 --- a/src/components/Slider/Slider.vue +++ b/src/components/Slider/Slider.vue @@ -82,6 +82,13 @@ const rangeClasses = computed(() => { ] }) +const rootClasses = computed(() => { + return [ + 'relative flex w-full select-none touch-none items-center', + props.size === 'md' ? 'h-5' : 'h-4', + ] +}) + const thumbClasses = computed(() => { return [ 'rounded-full bg-surface-white shadow-md ring-gray-600/20 transition-shadow duration-200 ease-out hover:ring-[6px] focus:outline-none dark:bg-surface-gray-7 dark:ring-gray-100/20', @@ -124,7 +131,7 @@ const hasLabeling = computed(() => { Date: Sun, 7 Jun 2026 14:51:48 +0530 Subject: [PATCH 3/5] test: negative values and value-commit in slider --- src/components/Slider/Slider.cy.ts | 36 ++++++++++++++++++++++++++++++ src/components/Slider/Slider.vue | 4 +++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/components/Slider/Slider.cy.ts b/src/components/Slider/Slider.cy.ts index b52aefef3..96032a2d8 100644 --- a/src/components/Slider/Slider.cy.ts +++ b/src/components/Slider/Slider.cy.ts @@ -102,4 +102,40 @@ describe('Slider', () => { cy.get('#sl-err').should('have.attr', 'data-state', 'invalid') }) }) + + it('emits value-commit once when interaction ends, not on every step', () => { + const onValueCommit = cy.stub().as('valueCommit') + cy.mount(Slider, { props: { modelValue: [25], onValueCommit } }) + cy.get('[role="slider"]').focus().type('{rightarrow}{rightarrow}{rightarrow}') + cy.get('@valueCommit').should('have.been.calledThrice') + }) + + describe('bidirectional fill', () => { + // Targets the range element: the only absolutely-positioned element inside the control. + const range = () => cy.get('[data-slot="control"] .absolute') + + it('fills from zero-crossing to a positive value', () => { + cy.mount(Slider, { props: { modelValue: [50], min: -100, max: 100 } }) + range() + .should('have.attr', 'style') + .and('include', 'left: 50%') + .and('include', 'right: 25%') + }) + + it('fills from a negative value to the zero-crossing', () => { + cy.mount(Slider, { props: { modelValue: [-50], min: -100, max: 100 } }) + range() + .should('have.attr', 'style') + .and('include', 'left: 25%') + .and('include', 'right: 50%') + }) + + it('renders zero-width fill when value is at zero', () => { + cy.mount(Slider, { props: { modelValue: [0], min: -100, max: 100 } }) + range() + .should('have.attr', 'style') + .and('include', 'left: 50%') + .and('include', 'right: 50%') + }) + }) }) diff --git a/src/components/Slider/Slider.vue b/src/components/Slider/Slider.vue index 98c5c306a..dedb1744e 100644 --- a/src/components/Slider/Slider.vue +++ b/src/components/Slider/Slider.vue @@ -147,7 +147,9 @@ const hasLabeling = computed(() => { > -
+ +
+ Date: Sun, 7 Jun 2026 14:55:04 +0530 Subject: [PATCH 4/5] docs: add story for negative values and update props --- src/components/Slider/Slider.api.md | 2 +- src/components/Slider/Slider.md | 6 ++++++ src/components/Slider/stories/NegativeValues.vue | 12 ++++++++++++ src/components/Slider/types.ts | 2 +- 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 src/components/Slider/stories/NegativeValues.vue diff --git a/src/components/Slider/Slider.api.md b/src/components/Slider/Slider.api.md index 9135c43e1..97f4d48ac 100644 --- a/src/components/Slider/Slider.api.md +++ b/src/components/Slider/Slider.api.md @@ -21,7 +21,7 @@ }, { name: 'min', - description: 'Minimum allowed slider value.', + description: 'Minimum allowed slider value. Negative values enable bidirectional fill from zero.', required: false, type: 'number', default: '0' diff --git a/src/components/Slider/Slider.md b/src/components/Slider/Slider.md index 1befeb282..c7e4e52ae 100644 --- a/src/components/Slider/Slider.md +++ b/src/components/Slider/Slider.md @@ -14,6 +14,12 @@ Use a two-element `modelValue` to render two thumbs. +## Negative Values + +When `min` is negative the slider fills bidirectionally from the zero-crossing, so positive and negative values are visually distinct. + + + ## Labeling diff --git a/src/components/Slider/stories/NegativeValues.vue b/src/components/Slider/stories/NegativeValues.vue new file mode 100644 index 000000000..d99a31028 --- /dev/null +++ b/src/components/Slider/stories/NegativeValues.vue @@ -0,0 +1,12 @@ + + + diff --git a/src/components/Slider/types.ts b/src/components/Slider/types.ts index e2838475f..201a053d6 100644 --- a/src/components/Slider/types.ts +++ b/src/components/Slider/types.ts @@ -20,7 +20,7 @@ export interface SliderProps extends InputLabelingProps { /** Maximum allowed slider value. */ max?: number - /** Minimum allowed slider value. */ + /** Minimum allowed slider value. Negative values enable bidirectional fill from zero. */ min?: number /** Visual size of the slider. */ From 421f7ef563defd620f5af05f695badab6e254764 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 8 Jun 2026 17:13:19 +0530 Subject: [PATCH 5/5] fix: zero-crossing condition --- src/components/Slider/Slider.cy.ts | 7 ------- src/components/Slider/Slider.vue | 21 +++++++++++++-------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/components/Slider/Slider.cy.ts b/src/components/Slider/Slider.cy.ts index 96032a2d8..5c35c1c68 100644 --- a/src/components/Slider/Slider.cy.ts +++ b/src/components/Slider/Slider.cy.ts @@ -103,13 +103,6 @@ describe('Slider', () => { }) }) - it('emits value-commit once when interaction ends, not on every step', () => { - const onValueCommit = cy.stub().as('valueCommit') - cy.mount(Slider, { props: { modelValue: [25], onValueCommit } }) - cy.get('[role="slider"]').focus().type('{rightarrow}{rightarrow}{rightarrow}') - cy.get('@valueCommit').should('have.been.calledThrice') - }) - describe('bidirectional fill', () => { // Targets the range element: the only absolutely-positioned element inside the control. const range = () => cy.get('[data-slot="control"] .absolute') diff --git a/src/components/Slider/Slider.vue b/src/components/Slider/Slider.vue index dedb1744e..91e959bcf 100644 --- a/src/components/Slider/Slider.vue +++ b/src/components/Slider/Slider.vue @@ -53,17 +53,18 @@ const { disabled: () => props.disabled, }) -const isBidirectional = computed(() => props.min < 0) +const isBidirectional = computed(() => props.min < 0 && props.max > 0) const bidirectionalRangeStyles = computed(() => { const { min, max } = props - const value = sliderValue.value[0] ?? 0 const range = max - min const zeroPos = -min / range - const valuePos = (value - min) / range + const thumbPositions = sliderValue.value.map((v) => (v - min) / range) + const allPositions = + thumbPositions.length === 1 ? [zeroPos, thumbPositions[0]] : thumbPositions return { - left: `${Math.min(zeroPos, valuePos) * 100}%`, - right: `${(1 - Math.max(zeroPos, valuePos)) * 100}%`, + left: `${Math.min(...allPositions) * 100}%`, + right: `${(1 - Math.max(...allPositions)) * 100}%`, } }) @@ -147,9 +148,13 @@ const hasLabeling = computed(() => { > - -
- +