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.cy.ts b/src/components/Slider/Slider.cy.ts index b52aefef3..5c35c1c68 100644 --- a/src/components/Slider/Slider.cy.ts +++ b/src/components/Slider/Slider.cy.ts @@ -102,4 +102,33 @@ describe('Slider', () => { cy.get('#sl-err').should('have.attr', 'data-state', 'invalid') }) }) + + 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.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/Slider.vue b/src/components/Slider/Slider.vue index bfdcb2d2a..91e959bcf 100644 --- a/src/components/Slider/Slider.vue +++ b/src/components/Slider/Slider.vue @@ -53,6 +53,21 @@ const { disabled: () => props.disabled, }) +const isBidirectional = computed(() => props.min < 0 && props.max > 0) + +const bidirectionalRangeStyles = computed(() => { + const { min, max } = props + const range = max - min + const zeroPos = -min / range + const thumbPositions = sliderValue.value.map((v) => (v - min) / range) + const allPositions = + thumbPositions.length === 1 ? [zeroPos, thumbPositions[0]] : thumbPositions + return { + left: `${Math.min(...allPositions) * 100}%`, + right: `${(1 - Math.max(...allPositions)) * 100}%`, + } +}) + const trackClasses = computed(() => { return [ 'relative grow rounded', @@ -68,6 +83,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', @@ -85,10 +107,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, ) }) @@ -110,7 +132,7 @@ const hasLabeling = computed(() => { { @value-commit="onValueCommit" > - + +
+import { ref } from 'vue' +import { Slider } from 'frappe-ui' + +const value = ref([25]) + + + 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. */