Component: Vue Widget Slider (new) (#5516)

* feat: Initial shadcn configuration

* component: Add Slider component from shadcn-vue

* deps: Add tw-animate-css

* component: Align slider with Figma styles

* component: Set the step value for the slider, update styles

* fix: update component tests to work with Array of values

* vite: Don't reload dev server for test changes

* component: Swap text for a number input kept in sync with the slider

* cleanup: Don't need the override if the input isn't type="number"

* test: add step size tests

* cleanup: Don't need cn for these

* css: Update token names to match new Figma Variables

* lint: Fix camelCase vs train-case in passthrough

* feat: If the value is deleted, revert to the slider state cc: @PabloWiedemann

* feat: Improve cursor styles, grabbable thumb, clickable track

* lint: temporarily disable some warnings

* feat: Grabbing while sliding (most of the time)
This commit is contained in:
Alexander Brown
2025-09-12 18:52:18 -07:00
committed by GitHub
parent c588f2f457
commit 1845708ddb
12 changed files with 402 additions and 150 deletions

View File

@@ -1,62 +1,63 @@
import { mount } from '@vue/test-utils'
import PrimeVue from 'primevue/config'
import InputText from 'primevue/inputtext'
import Slider from 'primevue/slider'
import type { SliderProps } from 'primevue/slider'
import InputNumber from 'primevue/inputnumber'
import { describe, expect, it } from 'vitest'
import Slider from '@/components/ui/slider/Slider.vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import WidgetInputNumberSlider from './WidgetInputNumberSlider.vue'
describe('WidgetInputNumberSlider Value Binding', () => {
const createMockWidget = (
value: number = 5,
options: Partial<SliderProps & { precision?: number }> = {},
callback?: (value: number) => void
): SimplifiedWidget<number> => ({
function createMockWidget(
value: number = 5,
options: SimplifiedWidget['options'] = {},
callback?: (value: number) => void
): SimplifiedWidget<number> {
return {
name: 'test_slider',
type: 'float',
value,
options: { min: 0, max: 100, step: 1, precision: 0, ...options },
callback
})
const mountComponent = (
widget: SimplifiedWidget<number>,
modelValue: number,
readonly = false
) => {
return mount(WidgetInputNumberSlider, {
global: {
plugins: [PrimeVue],
components: { InputText, Slider }
},
props: {
widget,
modelValue,
readonly
}
})
}
}
const getNumberInput = (wrapper: ReturnType<typeof mount>) => {
const input = wrapper.find('input[type="number"]')
if (!(input.element instanceof HTMLInputElement)) {
throw new Error(
'Number input element not found or is not an HTMLInputElement'
)
function mountComponent(
widget: SimplifiedWidget<number>,
modelValue: number,
readonly = false
) {
return mount(WidgetInputNumberSlider, {
global: {
plugins: [PrimeVue],
components: { InputNumber, Slider }
},
props: {
widget,
modelValue,
readonly
}
return { element: input.element }
}
})
}
function getNumberInput(wrapper: ReturnType<typeof mount>) {
const input = wrapper.find('input[inputmode="numeric"]')
if (!(input.element instanceof HTMLInputElement)) {
throw new Error(
'Number input element not found or is not an HTMLInputElement'
)
}
return input.element
}
describe('WidgetInputNumberSlider Value Binding', () => {
describe('Props and Values', () => {
it('passes modelValue to slider component', () => {
const widget = createMockWidget(5)
const wrapper = mountComponent(widget, 5)
const slider = wrapper.findComponent({ name: 'Slider' })
expect(slider.props('modelValue')).toBe(5)
expect(slider.props('modelValue')).toEqual([5])
})
it('handles different initial values', () => {
@@ -67,10 +68,10 @@ describe('WidgetInputNumberSlider Value Binding', () => {
const wrapper2 = mountComponent(widget2, 10)
const slider1 = wrapper1.findComponent({ name: 'Slider' })
expect(slider1.props('modelValue')).toBe(5)
expect(slider1.props('modelValue')).toEqual([5])
const slider2 = wrapper2.findComponent({ name: 'Slider' })
expect(slider2.props('modelValue')).toBe(10)
expect(slider2.props('modelValue')).toEqual([10])
})
})
@@ -85,8 +86,9 @@ describe('WidgetInputNumberSlider Value Binding', () => {
it('renders input field', () => {
const widget = createMockWidget(5)
const wrapper = mountComponent(widget, 5)
console.log(wrapper.html())
expect(wrapper.find('input[type="number"]').exists()).toBe(true)
expect(wrapper.find('input[inputmode="numeric"]').exists()).toBe(true)
})
it('displays initial value in input field', () => {
@@ -94,7 +96,7 @@ describe('WidgetInputNumberSlider Value Binding', () => {
const wrapper = mountComponent(widget, 42)
const input = getNumberInput(wrapper)
expect(input.element.value).toBe('42')
expect(input.value).toBe('42')
})
it('disables components in readonly mode', () => {
@@ -105,7 +107,7 @@ describe('WidgetInputNumberSlider Value Binding', () => {
expect(slider.props('disabled')).toBe(true)
const input = getNumberInput(wrapper)
expect(input.element.disabled).toBe(true)
expect(input.disabled).toBe(true)
})
})
@@ -127,5 +129,47 @@ describe('WidgetInputNumberSlider Value Binding', () => {
expect(slider.props('min')).toBe(-100)
expect(slider.props('max')).toBe(100)
})
describe('Step Size', () => {
it('should default to 1', () => {
const widget = createMockWidget(5)
const wrapper = mountComponent(widget, 5)
const slider = wrapper.findComponent({ name: 'Slider' })
expect(slider.props('step')).toBe(1)
})
it('should get the step2 value if present', () => {
const widget = createMockWidget(5, { step2: 0.01 })
const wrapper = mountComponent(widget, 5)
const slider = wrapper.findComponent({ name: 'Slider' })
expect(slider.props('step')).toBe(0.01)
})
it('should be 1 for precision 0', () => {
const widget = createMockWidget(5, { precision: 0 })
const wrapper = mountComponent(widget, 5)
const slider = wrapper.findComponent({ name: 'Slider' })
expect(slider.props('step')).toBe(1)
})
it('should be .1 for precision 1', () => {
const widget = createMockWidget(5, { precision: 1 })
const wrapper = mountComponent(widget, 5)
const slider = wrapper.findComponent({ name: 'Slider' })
expect(slider.props('step')).toBe(0.1)
})
it('should be .00001 for precision 5', () => {
const widget = createMockWidget(5, { precision: 5 })
const wrapper = mountComponent(widget, 5)
const slider = wrapper.findComponent({ name: 'Slider' })
expect(slider.props('step')).toBe(0.00001)
})
})
})
})