├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── tests.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── LICENSE-MPL-2.0 ├── README.md ├── dist ├── range-slider-pips.css ├── range-slider-pips.js ├── range-slider-pips.mjs └── svelte │ ├── components │ ├── RangePips.svelte │ ├── RangePips.svelte.d.ts │ ├── RangeSlider.svelte │ └── RangeSlider.svelte.d.ts │ ├── index.d.ts │ ├── index.js │ ├── types.d.ts │ ├── types.js │ ├── utils.d.ts │ ├── utils.js │ ├── utils.test.d.ts │ └── utils.test.js ├── docs ├── contributing.md └── upgrade.md ├── package-lock.json ├── package.json ├── playwright.config.ts ├── public ├── icons │ ├── css-3-svgrepo-com.png │ ├── html-5-svgrepo-com.png │ ├── jquery-svgrepo-com.png │ ├── js-svgrepo-com.png │ ├── react-svgrepo-com.png │ ├── svelte-svgrepo-com.png │ └── vuejs-svgrepo-com.png ├── svelte-range-slider-features.png ├── svelte-range-slider-logo.png ├── svelte-range-slider-logo.svg └── svelte-range-slider-screenshot.png ├── rollup.config.mjs ├── src ├── app.d.ts ├── app.html ├── lib │ ├── components │ │ ├── RangePips.svelte │ │ └── RangeSlider.svelte │ ├── index.ts │ ├── types.ts │ ├── utils.test.ts │ └── utils.ts └── routes │ ├── +layout.svelte │ ├── +page.svelte │ ├── app.css │ ├── barebones.css │ └── test │ ├── nav │ ├── NavItem.svelte │ ├── Navigation.svelte │ ├── clickOutside.ts │ ├── feather.types.d.ts │ ├── gen-nav.js │ └── test-nav.json │ └── range-slider │ ├── +layout.svelte │ ├── +page.svelte │ ├── base │ └── +page.svelte │ ├── events │ └── +page.svelte │ ├── floats │ └── +page.svelte │ ├── formatters │ ├── handle │ │ └── +page.svelte │ ├── pips │ │ └── +page.svelte │ └── range │ │ └── +page.svelte │ ├── limits │ └── +page.svelte │ ├── minmax │ ├── +page.svelte │ ├── custom-min-max │ │ └── +page.svelte │ ├── decimal-min-max │ │ └── +page.svelte │ ├── explicit-value │ │ └── +page.svelte │ ├── invalid-min-max │ │ └── +page.svelte │ └── negative-min-max │ │ └── +page.svelte │ ├── pips │ ├── +page.svelte │ ├── labels │ │ └── +page.svelte │ ├── limits │ │ └── +page.svelte │ ├── pips │ │ └── +page.svelte │ ├── pipsteps-large │ │ └── +page.svelte │ ├── pipsteps │ │ └── +page.svelte │ └── steps │ │ └── +page.svelte │ ├── range │ ├── +page.svelte │ ├── draggy │ │ └── +page.svelte │ ├── false │ │ └── +page.svelte │ ├── gaps │ │ └── +page.svelte │ ├── max │ │ └── +page.svelte │ ├── min │ │ └── +page.svelte │ └── pushy │ │ └── +page.svelte │ ├── states │ ├── disabled │ │ └── +page.svelte │ ├── hoverable │ │ └── +page.svelte │ └── reversed │ │ └── +page.svelte │ ├── styles │ └── darkmode │ │ └── +page.svelte │ └── values │ ├── binding │ ├── multiple │ │ └── +page.svelte │ └── single │ │ └── +page.svelte │ ├── constrained-values │ └── +page.svelte │ ├── explicit-value │ └── +page.svelte │ ├── multiple-values │ └── +page.svelte │ ├── seven-values │ └── +page.svelte │ ├── single-value │ └── +page.svelte │ └── value-values │ └── +page.svelte ├── static └── favicon.png ├── svelte.config.js ├── tests ├── jquery │ ├── index.html │ ├── index.jquery.js │ ├── package-lock.json │ └── package.json ├── playwright │ ├── RangePips._.spec.ts │ ├── RangePips.formatters.spec.ts │ ├── RangePips.labels.spec.ts │ ├── RangePips.limits.spec.ts │ ├── RangePips.pips.spec.ts │ ├── RangePips.pipsteps.spec.ts │ ├── RangePips.steps.spec.ts │ ├── RangeSlider._.spec.ts │ ├── RangeSlider.darkmode.spec.ts │ ├── RangeSlider.events.spec.ts │ ├── RangeSlider.floats.spec.ts │ ├── RangeSlider.formatters.handle.spec.ts │ ├── RangeSlider.formatters.range.spec.ts │ ├── RangeSlider.interaction.spec.ts │ ├── RangeSlider.limits.spec.ts │ ├── RangeSlider.minmax.spec.ts │ ├── RangeSlider.range.draggy.spec.ts │ ├── RangeSlider.range.gaps.spec.ts │ ├── RangeSlider.range.pushy.spec.ts │ ├── RangeSlider.range.spec.ts │ ├── RangeSlider.reversed.spec.ts │ ├── RangeSlider.states.spec.ts │ ├── RangeSlider.values.spec.ts │ └── helpers │ │ ├── tools.ts │ │ └── utils.ts ├── reactjs │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── App.js │ │ └── index.js ├── setupTest.js ├── svelte4 │ ├── .gitignore │ ├── .vscode │ │ └── extensions.json │ ├── index.html │ ├── jsconfig.json │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.svelte │ │ ├── app.css │ │ ├── assets │ │ │ └── svelte.svg │ │ ├── lib │ │ │ └── Counter.svelte │ │ ├── main.js │ │ └── vite-env.d.ts │ ├── svelte.config.js │ └── vite.config.js ├── svelte5 │ ├── .gitignore │ ├── .npmrc │ ├── .prettierignore │ ├── .prettierrc │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── app.d.ts │ │ ├── app.html │ │ ├── lib │ │ │ └── index.ts │ │ └── routes │ │ │ ├── +page.svelte │ │ │ ├── regular │ │ │ └── +page.svelte │ │ │ └── runes │ │ │ └── +page.svelte │ ├── static │ │ └── favicon.png │ ├── svelte.config.js │ ├── tsconfig.json │ └── vite.config.ts ├── vanilla │ ├── barebones.css │ ├── esm.html │ ├── index.html │ ├── package-lock.json │ ├── package.json │ └── umd.html ├── vitest │ └── utils.test.ts └── vuejs │ ├── .gitignore │ ├── .vscode │ └── extensions.json │ ├── env.d.ts │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ └── favicon.ico │ ├── src │ ├── App.vue │ ├── assets │ │ ├── base.css │ │ ├── logo.svg │ │ └── main.css │ ├── components │ │ └── HelloWorld.vue │ └── main.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── tsconfig.json └── vite.config.ts /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug / error in the code 4 | title: '[bug] ...' 5 | labels: bug, investigating 6 | assignees: '' 7 | --- 8 | 9 | ### Describe the bug 10 | 11 | A clear and concise description of what the bug is. 12 | 13 | ### Link To Reproduce 14 | 15 | 20 | 21 | #### Versions 22 | 23 | - svelte: v4.2.10 24 | - svelte-range-slider-pips: v3.2.1 25 | 26 | #### Steps to reproduce the behavior: 27 | 28 | 1. Go to '...' 29 | 2. Click on '....' 30 | 3. See error 31 | 32 | #### Expected behavior 33 | 34 | A clear and concise description of what you expected to happen. 35 | 36 | #### Screenshots 37 | 38 | If applicable, add screenshots to help explain your problem. 39 | 40 | #### Device/Environtment 41 | 42 | Please describe the environment and device the bug was found on, if relevant. 43 | 44 | #### Additional context 45 | 46 | Add any other context about the problem here. 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Submit a request for a feature to be added 4 | title: '[feature] ...' 5 | labels: feature request, investigating 6 | assignees: '' 7 | --- 8 | 9 | ### Describe the feature 10 | 11 | A clear and concise description of what the bug is. 12 | 13 | #### Explain it's value / reasoning 14 | 15 | Some justification of why you think this feature would be valuable, either to yourself or to general users. 16 | 17 | #### Additional context 18 | 19 | If you have any use cases to describe, or demo (with image/link) please share 20 | Screenshots/Diagrams are very appreciated! 21 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: [main, next] 5 | pull_request: 6 | branches: [main, next] 7 | jobs: 8 | unit-tests: 9 | name: Unit Tests 10 | timeout-minutes: 10 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Install Node 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: lts/* 18 | - name: Install dependencies 19 | run: npm ci 20 | - name: Run Unit tests 21 | run: npm run test:unit 22 | 23 | component-tests: 24 | name: Component Tests 25 | needs: unit-tests 26 | timeout-minutes: 60 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | - uses: actions/setup-node@v4 31 | with: 32 | node-version: lts/* 33 | - name: Install dependencies 34 | run: npm ci 35 | - name: Build Components 36 | run: npm run build 37 | - name: Install Playwright Browsers 38 | run: npx playwright install --with-deps 39 | - name: Run Component tests 40 | run: npx playwright test 41 | - uses: actions/upload-artifact@v4 42 | if: ${{ !cancelled() }} 43 | with: 44 | name: playwright-report 45 | path: playwright-report/ 46 | retention-days: 30 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .astro 3 | node_modules 4 | build/ 5 | /tests/**/build 6 | /tests/**/dist 7 | /tests/**/node_modules 8 | /.svelte-kit 9 | /package 10 | */routes/isolate 11 | .env 12 | .env.* 13 | !.env.example 14 | vite.config.js.timestamp-* 15 | vite.config.ts.timestamp-* 16 | 17 | PENDING_CHANGES.md 18 | CHANGELOG.md 19 | yarn-error.log 20 | npm-error.log 21 | 22 | *.patch 23 | 24 | # Playwright 25 | /test-results/ 26 | /playwright-report/ 27 | /blob-report/ 28 | /playwright/.cache/ 29 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | tag-version-prefix="" -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore files for PNPM, NPM and YARN 2 | pnpm-lock.yaml 3 | package-lock.json 4 | yarn.lock 5 | yarn-error.log 6 | npm-error.log 7 | node_modules 8 | 9 | .vscode/ 10 | .DS_Store 11 | .astro 12 | dist/ 13 | **/build 14 | /docs 15 | /tests/**/build 16 | /tests/**/dist 17 | **/.svelte-kit 18 | **/package 19 | .env 20 | .env.* 21 | !.env.example 22 | vite.config.js.timestamp-* 23 | vite.config.ts.timestamp-* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "vueIndentScriptAndStyle": true, 6 | "printWidth": 120, 7 | "plugins": ["prettier-plugin-svelte"], 8 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 9 | } 10 | -------------------------------------------------------------------------------- /dist/range-slider-pips.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /dist/svelte/components/RangePips.svelte: -------------------------------------------------------------------------------- 1 | 68 | 69 |
77 | {#if (all && first !== false) || first} 78 | { 87 | labelDown(e); 88 | }} 89 | on:pointerup={(e) => { 90 | labelUp(min, e); 91 | }} 92 | > 93 | {#if all === 'label' || first === 'label'} 94 | 95 | {#if prefix}{prefix}{/if} 96 | {@html formatter(coerceFloat(min, precision), 0, 0)} 97 | {#if suffix}{suffix}{/if} 98 | 99 | {/if} 100 | 101 | {/if} 102 | 103 | {#if (all && rest !== false) || rest} 104 | {#each Array(pipCount) as _, i} 105 | {@const val = getValueFromIndex(i, min, max, finalPipStep, step, precision)} 106 | {#if val > min && val < max} 107 | { 116 | labelDown(e); 117 | }} 118 | on:pointerup={(e) => { 119 | labelUp(val, e); 120 | }} 121 | > 122 | {#if all === 'label' || rest === 'label'} 123 | 124 | {#if true || prefix}{prefix}{/if} 125 | {@html formatter(val, i, valueAsPercent(val, min, max, precision))} 126 | {#if true || suffix}{suffix}{/if} 127 | 128 | {/if} 129 | 130 | {/if} 131 | {/each} 132 | {/if} 133 | 134 | {#if (all && last !== false) || last} 135 | { 144 | labelDown(e); 145 | }} 146 | on:pointerup={(e) => { 147 | labelUp(max, e); 148 | }} 149 | > 150 | {#if all === 'label' || last === 'label'} 151 | 152 | {#if prefix}{prefix}{/if} 153 | {@html formatter(coerceFloat(max, precision), pipCount, 100)} 154 | {#if suffix}{suffix}{/if} 155 | 156 | {/if} 157 | 158 | {/if} 159 |
160 | 161 | 292 | -------------------------------------------------------------------------------- /dist/svelte/components/RangePips.svelte.d.ts: -------------------------------------------------------------------------------- 1 | import { SvelteComponent } from "svelte"; 2 | import type { Pip, Formatter } from '../types.js'; 3 | declare const __propDef: { 4 | props: { 5 | range?: boolean | "min" | "max" | undefined; 6 | min?: number | undefined; 7 | max?: number | undefined; 8 | step?: number | undefined; 9 | value?: number | undefined; 10 | values?: number[] | undefined; 11 | vertical?: boolean | undefined; 12 | reversed?: boolean | undefined; 13 | hoverable?: boolean | undefined; 14 | disabled?: boolean | undefined; 15 | limits?: [number, number] | null | undefined; 16 | pipstep?: number | undefined; 17 | all?: Pip; 18 | first?: Pip; 19 | last?: Pip; 20 | rest?: Pip; 21 | prefix?: string | undefined; 22 | suffix?: string | undefined; 23 | formatter?: Formatter | undefined; 24 | precision?: number | undefined; 25 | focus: boolean; 26 | orientationStart: 'left' | 'right' | 'top' | 'bottom'; 27 | moveHandle: (index: number | null, value: number) => void; 28 | }; 29 | events: { 30 | [evt: string]: CustomEvent; 31 | }; 32 | slots: {}; 33 | }; 34 | export type RangePipsProps = typeof __propDef.props; 35 | export type RangePipsEvents = typeof __propDef.events; 36 | export type RangePipsSlots = typeof __propDef.slots; 37 | export default class RangePips extends SvelteComponent { 38 | } 39 | export {}; 40 | -------------------------------------------------------------------------------- /dist/svelte/components/RangeSlider.svelte.d.ts: -------------------------------------------------------------------------------- 1 | import { SvelteComponent } from "svelte"; 2 | import { type SpringOpts } from 'svelte/motion'; 3 | import type { Pip, Formatter, RangeFormatter } from '../types.js'; 4 | declare const __propDef: { 5 | props: { 6 | slider?: HTMLDivElement | undefined; 7 | precision?: number | undefined; 8 | range?: boolean | "min" | "max" | undefined; 9 | pushy?: boolean | undefined; 10 | draggy?: boolean | undefined; 11 | min?: number | undefined; 12 | max?: number | undefined; 13 | step?: number | undefined; 14 | values?: number[] | undefined; 15 | value?: number | undefined; 16 | vertical?: boolean | undefined; 17 | float?: boolean | undefined; 18 | rangeFloat?: boolean | undefined; 19 | reversed?: boolean | undefined; 20 | hoverable?: boolean | undefined; 21 | disabled?: boolean | undefined; 22 | limits?: [number, number] | null | undefined; 23 | rangeGapMin?: number | undefined; 24 | rangeGapMax?: number | undefined; 25 | pips?: boolean | undefined; 26 | pipstep?: number | undefined; 27 | all?: Pip; 28 | first?: Pip; 29 | last?: Pip; 30 | rest?: Pip; 31 | prefix?: string | undefined; 32 | suffix?: string | undefined; 33 | formatter?: Formatter | undefined; 34 | handleFormatter?: Formatter | undefined; 35 | rangeFormatter?: RangeFormatter | null | undefined; 36 | ariaLabels?: string[] | undefined; 37 | id?: string | undefined; 38 | class?: string | undefined; 39 | style?: string | undefined; 40 | darkmode?: false | "auto" | "force" | undefined; 41 | springValues?: SpringOpts | undefined; 42 | spring?: boolean | undefined; 43 | }; 44 | events: { 45 | start: CustomEvent; 46 | stop: CustomEvent; 47 | change: CustomEvent; 48 | } & { 49 | [evt: string]: CustomEvent; 50 | }; 51 | slots: {}; 52 | }; 53 | export type RangeSliderProps = typeof __propDef.props; 54 | export type RangeSliderEvents = typeof __propDef.events; 55 | export type RangeSliderSlots = typeof __propDef.slots; 56 | export default class RangeSlider extends SvelteComponent { 57 | } 58 | export {}; 59 | -------------------------------------------------------------------------------- /dist/svelte/index.d.ts: -------------------------------------------------------------------------------- 1 | import RangeSlider from './components/RangeSlider.svelte'; 2 | export { RangeSlider }; 3 | export default RangeSlider; 4 | -------------------------------------------------------------------------------- /dist/svelte/index.js: -------------------------------------------------------------------------------- 1 | import RangeSlider from './components/RangeSlider.svelte'; 2 | export { RangeSlider }; 3 | export default RangeSlider; 4 | -------------------------------------------------------------------------------- /dist/svelte/types.d.ts: -------------------------------------------------------------------------------- 1 | export type NormalisedClient = { 2 | x: number; 3 | y: number; 4 | }; 5 | export type Formatter = (value: number, index?: number, percent?: number) => string | number; 6 | export type RangeFormatter = (value1: number, value2: number, percent1?: number, percent2?: number) => string; 7 | export type Pip = 'pip' | 'label' | boolean | undefined; 8 | -------------------------------------------------------------------------------- /dist/svelte/types.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/svelte/utils.d.ts: -------------------------------------------------------------------------------- 1 | import type { NormalisedClient } from './types.js'; 2 | /** 3 | * check if the value is a finite number 4 | * @param value the value to check 5 | * @returns true if the value is a finite number 6 | */ 7 | export declare function isFiniteNumber(value: number): value is number; 8 | /** 9 | * make sure the value is coerced to a float value 10 | * @param {number|string} value the value to fix 11 | * @param {number} precision the number of decimal places to fix to 12 | * @return {number} a float version of the input 13 | **/ 14 | export declare const coerceFloat: (value: number | string, precision?: number) => number; 15 | /** 16 | * clamp a value from a range so that it always 17 | * falls within the min/max values 18 | * @param {number} value the value to clamp 19 | * @param {number} min the minimum value 20 | * @param {number} max the maximum value 21 | * @return {number} the value after it's been clamped 22 | **/ 23 | export declare const clampValue: (value: number, min: number, max: number) => number; 24 | /** 25 | * take in a value, and then calculate that value's percentage 26 | * of the overall range (min-max); 27 | * @param {number} value the value we're getting percent for 28 | * @param {number} min the minimum value 29 | * @param {number} max the maximum value 30 | * @param {number} precision the number of decimal places to fix to (default 2) 31 | * @return {number} the percentage value 32 | **/ 33 | export declare const valueAsPercent: (value: number, min: number, max: number, precision?: number) => number; 34 | /** 35 | * convert a percentage to a value 36 | * @param {number} percent the percentage to convert 37 | * @param {number} min the minimum value 38 | * @param {number} max the maximum value 39 | * @return {number} the value after it's been converted 40 | **/ 41 | export declare const percentAsValue: (percent: number, min: number, max: number) => number; 42 | /** 43 | * align the value with the steps so that it 44 | * always sits on the closest (above/below) step 45 | * @param {number} value the value to align 46 | * @param {number} min the minimum value 47 | * @param {number} max the maximum value 48 | * @param {number} step the step value 49 | * @param {number} precision the number of decimal places to fix to 50 | * @param {number[]} limits the limits to check against 51 | * @return {number} the value after it's been aligned 52 | **/ 53 | export declare const constrainAndAlignValue: (value: number, min: number, max: number, step: number, precision?: number, limits?: [number, number] | null) => number; 54 | /** 55 | * helper to take a string of html and return only the text 56 | * @param {string} possibleHtml the string that may contain html 57 | * @return {string} the text from the input 58 | */ 59 | export declare const pureText: (possibleHtml?: string) => string; 60 | /** 61 | * normalise a mouse or touch event to return the 62 | * client (x/y) object for that event 63 | * @param {event} event a mouse/touch event to normalise 64 | * @returns {object} normalised event client object (x,y) 65 | **/ 66 | export declare const normalisedClient: (event: TouchEvent | MouseEvent) => NormalisedClient; 67 | /** 68 | * helper func to get the index of an element in it's DOM container 69 | * @param {Element} el dom object reference we want the index of 70 | * @returns {number} the index of the input element 71 | **/ 72 | export declare const elementIndex: (el: Element | null) => number; 73 | /** 74 | * helper to check if the given value is inside the range 75 | * @param value the value to check 76 | * @param range the range of values to check against 77 | * @param type the type of range to check against 78 | * @returns {boolean} true if the value is in the range 79 | */ 80 | export declare const isInRange: (value: number, range: number[], type: string | boolean) => boolean | undefined; 81 | /** 82 | * helper to check if the given value is outside of the limits 83 | * @param value the value to check 84 | * @param limits the limits to check against 85 | * @returns {boolean} true if the value is out of the limits 86 | */ 87 | export declare const isOutOfLimit: (value: number, limits: number[] | null) => boolean; 88 | /** 89 | * helper to check if the given value is selected 90 | * @param value the value to check if is selected 91 | * @param values the values to check against 92 | * @param precision the precision to check against 93 | * @returns {boolean} true if the value is selected 94 | */ 95 | export declare const isSelected: (value: number, values: number[], precision?: number) => boolean; 96 | /** 97 | * helper to return the value of a pip based on the index, and the min/max values, 98 | * and the step of the range slider 99 | * @param index the index of the pip 100 | * @param min the minimum value of the range slider 101 | * @param max the maximum value of the range slider 102 | * @param pipStep the step of the pips 103 | * @param step the step of the range slider 104 | * @param precision the precision to check against 105 | * @returns {number} the value of the pip 106 | */ 107 | export declare const getValueFromIndex: (index: number, min: number, max: number, pipStep: number, step: number, precision?: number) => number; 108 | /** 109 | * Calculate pointer position, percentage and value for a slider interaction 110 | * @param clientPos The normalized client position (x,y) 111 | * @param dims The slider's bounding rectangle dimensions 112 | * @param vertical Whether the slider is vertical 113 | * @param reversed Whether the slider is reversed 114 | * @param min The minimum value of the slider 115 | * @param max The maximum value of the slider 116 | * @returns Object containing pointer position, percentage and value 117 | */ 118 | export declare const calculatePointerValues: (slider: HTMLElement, clientPos: NormalisedClient, vertical: boolean, reversed: boolean, min: number, max: number) => { 119 | pointerVal: number; 120 | pointerPercent: number; 121 | }; 122 | -------------------------------------------------------------------------------- /dist/svelte/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * check if the value is a finite number 3 | * @param value the value to check 4 | * @returns true if the value is a finite number 5 | */ 6 | export function isFiniteNumber(value) { 7 | return typeof value === 'number' && !isNaN(value) && isFinite(value); 8 | } 9 | /** 10 | * make sure the value is coerced to a float value 11 | * @param {number|string} value the value to fix 12 | * @param {number} precision the number of decimal places to fix to 13 | * @return {number} a float version of the input 14 | **/ 15 | export const coerceFloat = (value, precision = 2) => { 16 | return parseFloat((+value).toFixed(precision)); 17 | }; 18 | /** 19 | * clamp a value from a range so that it always 20 | * falls within the min/max values 21 | * @param {number} value the value to clamp 22 | * @param {number} min the minimum value 23 | * @param {number} max the maximum value 24 | * @return {number} the value after it's been clamped 25 | **/ 26 | export const clampValue = function (value, min, max) { 27 | // return the min/max if outside of that range 28 | return value <= min ? min : value >= max ? max : value; 29 | }; 30 | /** 31 | * take in a value, and then calculate that value's percentage 32 | * of the overall range (min-max); 33 | * @param {number} value the value we're getting percent for 34 | * @param {number} min the minimum value 35 | * @param {number} max the maximum value 36 | * @param {number} precision the number of decimal places to fix to (default 2) 37 | * @return {number} the percentage value 38 | **/ 39 | export const valueAsPercent = function (value, min, max, precision = 2) { 40 | let percent = ((value - min) / (max - min)) * 100; 41 | if (isNaN(percent) || percent <= 0) { 42 | return 0; 43 | } 44 | else if (percent >= 100) { 45 | return 100; 46 | } 47 | else { 48 | return coerceFloat(percent, precision); 49 | } 50 | }; 51 | /** 52 | * convert a percentage to a value 53 | * @param {number} percent the percentage to convert 54 | * @param {number} min the minimum value 55 | * @param {number} max the maximum value 56 | * @return {number} the value after it's been converted 57 | **/ 58 | export const percentAsValue = function (percent, min, max) { 59 | return ((max - min) / 100) * percent + min; 60 | }; 61 | /** 62 | * align the value with the steps so that it 63 | * always sits on the closest (above/below) step 64 | * @param {number} value the value to align 65 | * @param {number} min the minimum value 66 | * @param {number} max the maximum value 67 | * @param {number} step the step value 68 | * @param {number} precision the number of decimal places to fix to 69 | * @param {number[]} limits the limits to check against 70 | * @return {number} the value after it's been aligned 71 | **/ 72 | export const constrainAndAlignValue = function (value, min, max, step, precision = 2, limits = null) { 73 | // if limits are provided, clamp the value between the limits 74 | // if no limits are provided, clamp the value between the min and max 75 | // before we start aligning the value 76 | value = clampValue(value, limits?.[0] ?? min, limits?.[1] ?? max); 77 | // escape early if the value is at/beyond the known limits 78 | if (limits?.[0] && value <= limits[0]) { 79 | return limits?.[0]; 80 | } 81 | else if (limits?.[1] && value >= limits[1]) { 82 | return limits?.[1]; 83 | } 84 | else if (max && value >= max) { 85 | return max; 86 | } 87 | else if (min && value <= min) { 88 | return min; 89 | } 90 | // find the middle-point between steps 91 | // and see if the value is closer to the 92 | // next step, or previous step 93 | let remainder = (value - min) % step; 94 | let aligned = value - remainder; 95 | if (Math.abs(remainder) * 2 >= step) { 96 | aligned += remainder > 0 ? step : -step; 97 | } 98 | else if (value >= max - remainder) { 99 | aligned = max; 100 | } 101 | // make sure the value is within acceptable limits 102 | aligned = clampValue(aligned, limits?.[0] ?? min, limits?.[1] ?? max); 103 | // make sure the returned value is set to the precision desired 104 | // this is also because javascript often returns weird floats 105 | // when dealing with odd numbers and percentages 106 | return coerceFloat(aligned, precision); 107 | }; 108 | /** 109 | * helper to take a string of html and return only the text 110 | * @param {string} possibleHtml the string that may contain html 111 | * @return {string} the text from the input 112 | */ 113 | export const pureText = (possibleHtml = '') => { 114 | return `${possibleHtml}`.replace(/<[^>]*>/g, ''); 115 | }; 116 | /** 117 | * normalise a mouse or touch event to return the 118 | * client (x/y) object for that event 119 | * @param {event} event a mouse/touch event to normalise 120 | * @returns {object} normalised event client object (x,y) 121 | **/ 122 | export const normalisedClient = (event) => { 123 | const { clientX, clientY } = 'touches' in event ? event.touches[0] || event.changedTouches[0] : event; 124 | return { x: clientX, y: clientY }; 125 | }; 126 | /** 127 | * helper func to get the index of an element in it's DOM container 128 | * @param {Element} el dom object reference we want the index of 129 | * @returns {number} the index of the input element 130 | **/ 131 | export const elementIndex = (el) => { 132 | if (!el) 133 | return -1; 134 | var i = 0; 135 | while ((el = el.previousElementSibling)) { 136 | i++; 137 | } 138 | return i; 139 | }; 140 | /** 141 | * helper to check if the given value is inside the range 142 | * @param value the value to check 143 | * @param range the range of values to check against 144 | * @param type the type of range to check against 145 | * @returns {boolean} true if the value is in the range 146 | */ 147 | export const isInRange = (value, range, type) => { 148 | if (type === 'min') { 149 | // if the range is 'min', then we're checking if the value is above the min value 150 | return range[0] > value; 151 | } 152 | else if (type === 'max') { 153 | // if the range is 'max', then we're checking if the value is below the max value 154 | return range[0] < value; 155 | } 156 | else if (type) { 157 | // if the range is a boolean of true, then we're checking if the value is in the range 158 | return range[0] < value && range[1] > value; 159 | } 160 | }; 161 | /** 162 | * helper to check if the given value is outside of the limits 163 | * @param value the value to check 164 | * @param limits the limits to check against 165 | * @returns {boolean} true if the value is out of the limits 166 | */ 167 | export const isOutOfLimit = (value, limits) => { 168 | if (!limits) 169 | return false; 170 | return value < limits[0] || value > limits[1]; 171 | }; 172 | /** 173 | * helper to check if the given value is selected 174 | * @param value the value to check if is selected 175 | * @param values the values to check against 176 | * @param precision the precision to check against 177 | * @returns {boolean} true if the value is selected 178 | */ 179 | export const isSelected = (value, values, precision = 2) => { 180 | return values.some((v) => coerceFloat(v, precision) === coerceFloat(value, precision)); 181 | }; 182 | /** 183 | * helper to return the value of a pip based on the index, and the min/max values, 184 | * and the step of the range slider 185 | * @param index the index of the pip 186 | * @param min the minimum value of the range slider 187 | * @param max the maximum value of the range slider 188 | * @param pipStep the step of the pips 189 | * @param step the step of the range slider 190 | * @param precision the precision to check against 191 | * @returns {number} the value of the pip 192 | */ 193 | export const getValueFromIndex = (index, min, max, pipStep, step, precision = 2) => { 194 | return coerceFloat(min + index * step * pipStep, precision); 195 | }; 196 | /** 197 | * Calculate pointer position, percentage and value for a slider interaction 198 | * @param clientPos The normalized client position (x,y) 199 | * @param dims The slider's bounding rectangle dimensions 200 | * @param vertical Whether the slider is vertical 201 | * @param reversed Whether the slider is reversed 202 | * @param min The minimum value of the slider 203 | * @param max The maximum value of the slider 204 | * @returns Object containing pointer position, percentage and value 205 | */ 206 | export const calculatePointerValues = (slider, clientPos, vertical, reversed, min, max) => { 207 | // first make sure we have the latest dimensions 208 | // of the slider, as it may have changed size 209 | const dims = slider.getBoundingClientRect(); 210 | // calculate the interaction position, percent and value 211 | let pointerPos = 0; 212 | let pointerPercent = 0; 213 | let pointerVal = 0; 214 | if (vertical) { 215 | pointerPos = clientPos.y - dims.top; 216 | pointerPercent = (pointerPos / dims.height) * 100; 217 | pointerPercent = reversed ? pointerPercent : 100 - pointerPercent; 218 | } 219 | else { 220 | pointerPos = clientPos.x - dims.left; 221 | pointerPercent = (pointerPos / dims.width) * 100; 222 | pointerPercent = reversed ? 100 - pointerPercent : pointerPercent; 223 | } 224 | pointerVal = percentAsValue(pointerPercent, min, max); 225 | return { pointerVal, pointerPercent }; 226 | }; 227 | -------------------------------------------------------------------------------- /dist/svelte/utils.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/svelte/utils.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { isFiniteNumber } from './utils.js'; 3 | describe('isFiniteNumber', () => { 4 | it('should return true for finite numbers', () => { 5 | expect(isFiniteNumber(42)).toBe(true); 6 | expect(isFiniteNumber(0)).toBe(true); 7 | expect(isFiniteNumber(-42)).toBe(true); 8 | expect(isFiniteNumber(3.14)).toBe(true); 9 | }); 10 | it('should return false for non-numbers', () => { 11 | // Test with type assertions to properly test the type guard 12 | const testValues = ['42', null, undefined, {}, []]; 13 | testValues.forEach((value) => { 14 | expect(isFiniteNumber(value)).toBe(false); 15 | }); 16 | }); 17 | it('should return false for NaN', () => { 18 | expect(isFiniteNumber(NaN)).toBe(false); 19 | }); 20 | it('should return false for Infinity', () => { 21 | expect(isFiniteNumber(Infinity)).toBe(false); 22 | expect(isFiniteNumber(-Infinity)).toBe(false); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # What can I contribute? 2 | 3 | ## 🌟 suggestions/requests for new features or changes 4 | 5 | Please be sure to give a thorough explanation of what your suggestion is, and why/how you think it is useful. If you have a specific use case, any information about it, and links to it will be very helpful. 6 | 7 | At the core, though, this is a component with a purpose. I will be the final judge of if the purpose is valid. Your adoption is your own perogative. You are free to fork and modify to suit your own needs. 8 | 9 | ## 🛠 pull-requests for bug fixes, or issue resolution 10 | 11 | - ⛔ **Use [this REPL as a reduced test case](https://svelte.dev/playground/e29b748dade74d33a238eefab9a5ce72)** for any bug/issue submissions! 12 | 13 | If there are any open issues, or you find a valid bug, please feel free to open a pull-request for review. I won't accept pull-requests for dependency updates, but those can be requested as an issue. 14 | 15 | **Please stick to the code formatting and style already in place.** 16 | 17 | When making changes/edits to the code you should use the `/tests/playwright` folder for testing all the functionality. 18 | 19 | ## 🧪 filling in gaps in the test-suite 20 | 21 | The test-suite is written in playwright, I've covered a large amount of the functionality, **but there are still some gaps**. Especially with edge cases, RTL Support, and interaction of different props with each other. _Any help to fill in some of these gaps would be greatly appreciated_. And 22 | it would be a **_great way for a junior automation / QA engineer to get some open-source experience_**. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-range-slider-pips", 3 | "version": "4.0.1", 4 | "description": "Multi-Thumb, Accessible, Beautiful Range Slider with Pips", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/simeydotme/svelte-range-slider-pips.git" 8 | }, 9 | "homepage": "https://simeydotme.github.io/svelte-range-slider-pips/", 10 | "author": "Simon Goellner ", 11 | "license": "MPL-2.0", 12 | "type": "module", 13 | "module": "./dist/range-slider-pips.mjs", 14 | "svelte": "./dist/svelte/index.js", 15 | "types": "./dist/svelte/index.d.ts", 16 | "exports": { 17 | ".": [ 18 | { 19 | "types": "./dist/svelte/index.d.ts", 20 | "svelte": "./dist/svelte/index.js", 21 | "import": "./dist/range-slider-pips.mjs", 22 | "require": "./dist/range-slider-pips.js", 23 | "style": "./dist/range-slider-pips.css", 24 | "default": "./dist/range-slider-pips.js" 25 | }, 26 | "./dist/range-slider-pips.js" 27 | ], 28 | "./dist/*.css": [ 29 | { 30 | "import": "./dist/range-slider-pips.css", 31 | "require": "./dist/range-slider-pips.css", 32 | "default": "./dist/range-slider-pips.css" 33 | }, 34 | "./dist/range-slider-pips.css" 35 | ], 36 | "./RangeSlider.svelte": "./src/lib/RangeSlider.svelte", 37 | "./RangePips.svelte": "./src/lib/RangePips.svelte" 38 | }, 39 | "files": [ 40 | "dist", 41 | "src/lib", 42 | "!dist/**/*.test.*", 43 | "!dist/**/*.spec.*" 44 | ], 45 | "scripts": { 46 | "dev": "vite dev --port 5173", 47 | "build": "npm run build:vanilla && vite build && npm run build:svelte", 48 | "build:svelte": "svelte-kit sync && svelte-package -o dist/svelte && publint", 49 | "build:vanilla": "rollup -c", 50 | "preview": "vite preview --port 5173", 51 | "prepublishOnly": "npm run build", 52 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 53 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 54 | "test": "npm run test:unit && npm run test:component", 55 | "test:unit": "vitest --reporter=verbose --watch=false", 56 | "test:unit:watch": "vitest --reporter=verbose --watch", 57 | "test:component": "playwright test", 58 | "lint": "prettier --check .", 59 | "format": "prettier --write .", 60 | "add:dist": "git add dist", 61 | "gen:nav": "node src/routes/test/nav/gen-nav.js" 62 | }, 63 | "pre-commit": [ 64 | "lint", 65 | "build", 66 | "add:dist" 67 | ], 68 | "peerDependencies": { 69 | "svelte": "^4.2.7 || ^5.0.0" 70 | }, 71 | "devDependencies": { 72 | "@playwright/test": "^1.51.0", 73 | "@rollup/plugin-commonjs": "^25.0.7", 74 | "@rollup/plugin-node-resolve": "^15.2.3", 75 | "@rollup/plugin-terser": "^0.4.4", 76 | "@rollup/plugin-typescript": "^11.1.6", 77 | "@sveltejs/adapter-auto": "^3.0.0", 78 | "@sveltejs/kit": "^2.0.0", 79 | "@sveltejs/package": "^2.0.0", 80 | "@sveltejs/vite-plugin-svelte": "^3.0.0", 81 | "@tailwindcss/typography": "^0.5.16", 82 | "@tailwindcss/vite": "^4.0.14", 83 | "@testing-library/jest-dom": "^6.6.3", 84 | "@testing-library/svelte": "^5.2.7", 85 | "@types/node": "^20.11.16", 86 | "@types/testing-library__jest-dom": "^5.14.9", 87 | "daisyui": "^5.0.6", 88 | "feather-icons": "^4.29.2", 89 | "globby": "^14.0.0", 90 | "jsdom": "^26.0.0", 91 | "pre-commit": "^1.2.2", 92 | "prettier": "^3.1.1", 93 | "prettier-plugin-svelte": "^3.1.2", 94 | "publint": "^0.1.9", 95 | "rollup": "^4.9.6", 96 | "rollup-plugin-css-only": "^4.5.2", 97 | "rollup-plugin-delete": "^2.0.0", 98 | "rollup-plugin-filesize": "^10.0.0", 99 | "rollup-plugin-svelte": "^7.1.6", 100 | "svelte": "^4.2.7 || ^5.0.0", 101 | "svelte-check": "^3.6.0", 102 | "svelte-preprocess": "^5.1.3", 103 | "tailwindcss": "^4.0.14", 104 | "tslib": "^2.4.1", 105 | "typescript": "^5.0.0", 106 | "vite": "^5.0.11", 107 | "vitest": "^1.2.0" 108 | }, 109 | "keywords": [ 110 | "svelte", 111 | "component", 112 | "ui", 113 | "input", 114 | "range", 115 | "slider", 116 | "thumb", 117 | "handle", 118 | "min", 119 | "max", 120 | "accessible", 121 | "pretty", 122 | "pip", 123 | "pips", 124 | "notch", 125 | "notches", 126 | "simey", 127 | "simeydotme", 128 | "react", 129 | "reactjs", 130 | "vue", 131 | "vuejs", 132 | "solid", 133 | "solidjs" 134 | ] 135 | } 136 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | // import dotenv from 'dotenv'; 8 | // import path from 'path'; 9 | // dotenv.config({ path: path.resolve(__dirname, '.env') }); 10 | 11 | /** 12 | * See https://playwright.dev/docs/test-configuration. 13 | */ 14 | export default defineConfig({ 15 | testDir: './tests/playwright', 16 | /* Run tests in files in parallel */ 17 | fullyParallel: true, 18 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 19 | forbidOnly: !!process.env.CI, 20 | /* Retry on CI only */ 21 | retries: process.env.CI ? 2 : 1, 22 | /* Opt out of parallel tests on CI. */ 23 | workers: process.env.CI ? 2 : 8, 24 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 25 | reporter: process.env.CI ? 'github' : 'line', 26 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 27 | use: { 28 | /* Base URL to use in actions like `await page.goto('/')`. */ 29 | // baseURL: 'http://127.0.0.1:3000', 30 | 31 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 32 | trace: 'on-first-retry', 33 | viewport: { width: 1800, height: 1000 } 34 | }, 35 | /* Configure projects for major browsers */ 36 | projects: [ 37 | { 38 | name: 'chromium', 39 | use: { ...devices['Desktop Chrome'], viewport: { width: 1800, height: 1000 } } 40 | }, 41 | 42 | { 43 | name: 'firefox', 44 | use: { ...devices['Desktop Firefox'], viewport: { width: 1800, height: 1000 } } 45 | } 46 | 47 | // { 48 | // name: 'webkit', 49 | // use: { ...devices['Desktop Safari'], viewport: { width: 1800, height: 1000 } } 50 | // } 51 | 52 | /* Test against mobile viewports. */ 53 | // { 54 | // name: 'Mobile Chrome', 55 | // use: { ...devices['Pixel 5'] }, 56 | // }, 57 | // { 58 | // name: 'Mobile Safari', 59 | // use: { ...devices['iPhone 12'] }, 60 | // }, 61 | 62 | /* Test against branded browsers. */ 63 | // { 64 | // name: 'Microsoft Edge', 65 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 66 | // }, 67 | // { 68 | // name: 'Google Chrome', 69 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 70 | // }, 71 | ], 72 | 73 | /* Run your local dev server before starting the tests */ 74 | webServer: { 75 | command: 'npm run preview', 76 | reuseExistingServer: !process.env.CI, 77 | port: 5173 78 | } 79 | }); 80 | -------------------------------------------------------------------------------- /public/icons/css-3-svgrepo-com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simeydotme/svelte-range-slider-pips/f0d144cf047d59381c204cd34ffdcac5a5b1896d/public/icons/css-3-svgrepo-com.png -------------------------------------------------------------------------------- /public/icons/html-5-svgrepo-com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simeydotme/svelte-range-slider-pips/f0d144cf047d59381c204cd34ffdcac5a5b1896d/public/icons/html-5-svgrepo-com.png -------------------------------------------------------------------------------- /public/icons/jquery-svgrepo-com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simeydotme/svelte-range-slider-pips/f0d144cf047d59381c204cd34ffdcac5a5b1896d/public/icons/jquery-svgrepo-com.png -------------------------------------------------------------------------------- /public/icons/js-svgrepo-com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simeydotme/svelte-range-slider-pips/f0d144cf047d59381c204cd34ffdcac5a5b1896d/public/icons/js-svgrepo-com.png -------------------------------------------------------------------------------- /public/icons/react-svgrepo-com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simeydotme/svelte-range-slider-pips/f0d144cf047d59381c204cd34ffdcac5a5b1896d/public/icons/react-svgrepo-com.png -------------------------------------------------------------------------------- /public/icons/svelte-svgrepo-com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simeydotme/svelte-range-slider-pips/f0d144cf047d59381c204cd34ffdcac5a5b1896d/public/icons/svelte-svgrepo-com.png -------------------------------------------------------------------------------- /public/icons/vuejs-svgrepo-com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simeydotme/svelte-range-slider-pips/f0d144cf047d59381c204cd34ffdcac5a5b1896d/public/icons/vuejs-svgrepo-com.png -------------------------------------------------------------------------------- /public/svelte-range-slider-features.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simeydotme/svelte-range-slider-pips/f0d144cf047d59381c204cd34ffdcac5a5b1896d/public/svelte-range-slider-features.png -------------------------------------------------------------------------------- /public/svelte-range-slider-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simeydotme/svelte-range-slider-pips/f0d144cf047d59381c204cd34ffdcac5a5b1896d/public/svelte-range-slider-logo.png -------------------------------------------------------------------------------- /public/svelte-range-slider-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | Svelte RangeSlider Logo 7 | 8 | 30 | 31 | 32 | 36 | 47 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /public/svelte-range-slider-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simeydotme/svelte-range-slider-pips/f0d144cf047d59381c204cd34ffdcac5a5b1896d/public/svelte-range-slider-screenshot.png -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import del from 'rollup-plugin-delete'; 3 | import svelte from 'rollup-plugin-svelte'; 4 | import commonjs from '@rollup/plugin-commonjs'; 5 | import resolve from '@rollup/plugin-node-resolve'; 6 | import filesize from 'rollup-plugin-filesize'; 7 | import autoPreprocess from 'svelte-preprocess'; 8 | import typescript from '@rollup/plugin-typescript'; 9 | import css from 'rollup-plugin-css-only'; 10 | 11 | import { globbySync } from 'globby'; 12 | 13 | const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8')); 14 | 15 | // for use with exports options 16 | const packageName = pkg.name.replace(/^(@\S+\/)?(svelte-)?(\S+)/, '$3'); 17 | 18 | // this is the name of the component when imported 19 | // like: import { Component } from 'package' or require('package').Component 20 | // and: const myComponent = new Component(); 21 | const moduleName = packageName.replace(/^\w/, (m) => m.toUpperCase()).replace(/-\w/g, (m) => m[1].toUpperCase()); 22 | 23 | // banner to be added to the top of each generated file 24 | const banner = `/** 25 | * ${pkg.name} ~ ${pkg.version} 26 | * ${pkg.description || ''} 27 | * ${pkg.homepage ? `Project home: ${pkg.homepage}` : ''} 28 | * © ${new Date().getFullYear()} ${pkg.author} ~ ${pkg.license} License 29 | * Published: ${new Date().getDate()}/${new Date().getMonth() + 1}/${new Date().getFullYear()} 30 | */`; 31 | 32 | const production = !process.env.ROLLUP_WATCH; 33 | const components = globbySync('src/lib/components/**/*.svelte').map((path) => { 34 | return path.split('/').at(-1).replace('.svelte', ''); 35 | }); 36 | 37 | const exports = components.map((component) => ({ 38 | input: `src/lib/components/${component}.svelte`, 39 | output: [ 40 | { 41 | file: pkg.exports['.'][0].import, 42 | format: 'es', 43 | name: moduleName, 44 | banner 45 | }, 46 | { 47 | file: pkg.exports['.'][0].require, 48 | format: 'umd', 49 | name: moduleName, 50 | banner 51 | } 52 | ], 53 | plugins: [ 54 | del({ targets: 'dist/*' }), 55 | svelte({ 56 | preprocess: autoPreprocess(), 57 | compilerOptions: { 58 | dev: !production, 59 | customElement: true, 60 | immutable: false 61 | } 62 | }), 63 | typescript({ 64 | exclude: ['node_modules/**', 'tests/**'] 65 | }), 66 | css({ output: pkg.exports['.'][0].style.split('/').at(-1) }), 67 | resolve({ 68 | browser: true, 69 | dedupe: ['svelte'] 70 | }), 71 | commonjs(), 72 | filesize() 73 | ], 74 | watch: { 75 | clearScreen: false 76 | } 77 | })); 78 | 79 | export default exports; 80 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | import RangeSlider from './components/RangeSlider.svelte'; 2 | export { RangeSlider }; 3 | export default RangeSlider; 4 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | export type NormalisedClient = { x: number; y: number }; 2 | export type Formatter = (value: number, index?: number, percent?: number) => string | number; 3 | export type RangeFormatter = (value1: number, value2: number, percent1?: number, percent2?: number) => string; 4 | export type Pip = 'pip' | 'label' | boolean | undefined; 5 | -------------------------------------------------------------------------------- /src/lib/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { isFiniteNumber } from './utils.js'; 3 | 4 | describe('isFiniteNumber', () => { 5 | it('should return true for finite numbers', () => { 6 | expect(isFiniteNumber(42)).toBe(true); 7 | expect(isFiniteNumber(0)).toBe(true); 8 | expect(isFiniteNumber(-42)).toBe(true); 9 | expect(isFiniteNumber(3.14)).toBe(true); 10 | }); 11 | 12 | it('should return false for non-numbers', () => { 13 | // Test with type assertions to properly test the type guard 14 | const testValues: unknown[] = ['42', null, undefined, {}, []]; 15 | testValues.forEach((value) => { 16 | expect(isFiniteNumber(value as number)).toBe(false); 17 | }); 18 | }); 19 | 20 | it('should return false for NaN', () => { 21 | expect(isFiniteNumber(NaN)).toBe(false); 22 | }); 23 | 24 | it('should return false for Infinity', () => { 25 | expect(isFiniteNumber(Infinity)).toBe(false); 26 | expect(isFiniteNumber(-Infinity)).toBe(false); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 87 | 88 | 89 |
90 | 91 |
92 | -------------------------------------------------------------------------------- /src/routes/app.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | @plugin "@tailwindcss/typography"; 3 | @plugin "daisyui" { 4 | themes: 5 | emerald --default, 6 | dracula --prefersdark; 7 | } 8 | 9 | @source "./isolate/+page.svelte"; 10 | 11 | @layer base { 12 | button:not(.open-nav) { 13 | @apply btn; 14 | } 15 | input[type='number'], 16 | input[type='text'] { 17 | @apply input; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/routes/test/nav/NavItem.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
  • 11 | {#if item.path} 12 | 13 | {#if item.children?.length} 14 | {@html feather.icons.folder.toSvg()} 15 | {:else} 16 | {@html feather.icons.file.toSvg()} 17 | {/if} 18 | {item.name} 19 | 20 | {:else} 21 | 22 | {#if item.children?.length} 23 | {@html feather.icons.folder.toSvg()} 24 | {:else} 25 | {@html feather.icons.file.toSvg()} 26 | {/if} 27 | {item.name} 28 | 29 | {/if} 30 | 31 | {#if item.children} 32 |
      33 | {#each item.children as child} 34 | 35 | {/each} 36 |
    37 | {/if} 38 |
  • 39 | -------------------------------------------------------------------------------- /src/routes/test/nav/Navigation.svelte: -------------------------------------------------------------------------------- 1 | 83 | 84 |
    89 | 92 | 93 | 100 |
    101 | 102 | 169 | -------------------------------------------------------------------------------- /src/routes/test/nav/clickOutside.ts: -------------------------------------------------------------------------------- 1 | type ClickOutsideEvent = CustomEvent; 2 | 3 | interface ClickOutsideOptions { 4 | enabled?: boolean; 5 | } 6 | 7 | export function clickOutside(node: HTMLElement, options: ClickOutsideOptions = {}) { 8 | const handleClick = (event: MouseEvent) => { 9 | const target = event.target as HTMLElement; 10 | 11 | if (options.enabled !== false && node && !node.contains(target)) { 12 | node.dispatchEvent(new CustomEvent('clickOutside')); 13 | } 14 | }; 15 | 16 | document.addEventListener('click', handleClick, true); 17 | 18 | return { 19 | update(newOptions: ClickOutsideOptions) { 20 | options = newOptions; 21 | }, 22 | destroy() { 23 | document.removeEventListener('click', handleClick, true); 24 | } 25 | }; 26 | } 27 | 28 | // Add TypeScript support for the custom event 29 | declare global { 30 | interface HTMLElementEventMap { 31 | clickOutside: ClickOutsideEvent; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/routes/test/nav/feather.types.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'feather-icons' { 2 | interface FeatherIcon { 3 | toSvg(): string; 4 | } 5 | 6 | interface FeatherIcons { 7 | [key: string]: FeatherIcon; 8 | } 9 | 10 | interface Feather { 11 | icons: FeatherIcons; 12 | } 13 | 14 | const feather: Feather; 15 | export default feather; 16 | } 17 | -------------------------------------------------------------------------------- /src/routes/test/nav/gen-nav.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | import path from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | 5 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 6 | const routesDir = path.join(__dirname, '..', 'range-slider'); 7 | const outputFile = path.join(__dirname, 'test-nav.json'); 8 | 9 | /** 10 | * @typedef {Object} NavItem 11 | * @property {string} name 12 | * @property {string} path 13 | * @property {NavItem[]} [children] 14 | */ 15 | 16 | /** 17 | * @param {string} dir 18 | * @param {string} [basePath=''] 19 | * @returns {Promise} 20 | */ 21 | async function scanDirectory(dir, basePath = '') { 22 | const entries = await fs.readdir(dir, { withFileTypes: true }); 23 | 24 | /** @type {NavItem[]} */ 25 | const result = []; 26 | 27 | for (const entry of entries) { 28 | const relativePath = path.join(basePath, entry.name); 29 | 30 | if (entry.isDirectory()) { 31 | // Skip directories that start with underscore or dot 32 | if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue; 33 | 34 | // Recursively scan subdirectories 35 | const children = await scanDirectory(path.join(dir, entry.name), relativePath); 36 | 37 | // Check if directory has a +page.svelte directly inside it 38 | const hasPage = await fs 39 | .access(path.join(dir, entry.name, '+page.svelte')) 40 | .then(() => true) 41 | .catch(() => false); 42 | 43 | // Only add directory if it has children or contains a +page.svelte 44 | if (children.length > 0 || hasPage) { 45 | /** @type {NavItem} */ 46 | const navItem = { 47 | name: entry.name, 48 | path: hasPage ? `/test/range-slider/${relativePath}` : '', 49 | ...(children.length > 0 ? { children } : {}) 50 | }; 51 | 52 | result.push(navItem); 53 | } 54 | } 55 | } 56 | 57 | return result; 58 | } 59 | 60 | async function generateNavigation() { 61 | try { 62 | const navigation = await scanDirectory(routesDir); 63 | 64 | // Sort by name 65 | navigation.sort((a, b) => a.name.localeCompare(b.name)); 66 | 67 | // Write to JSON file with proper indentation 68 | await fs.writeFile(outputFile, JSON.stringify(navigation, null, 2)); 69 | console.log(`Navigation data written to ${outputFile}`); 70 | } catch (error) { 71 | console.error('Error generating navigation:', error); 72 | process.exit(1); 73 | } 74 | } 75 | 76 | generateNavigation(); 77 | -------------------------------------------------------------------------------- /src/routes/test/nav/test-nav.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "base", 4 | "path": "/test/range-slider/base" 5 | }, 6 | { 7 | "name": "events", 8 | "path": "/test/range-slider/events" 9 | }, 10 | { 11 | "name": "floats", 12 | "path": "/test/range-slider/floats" 13 | }, 14 | { 15 | "name": "formatters", 16 | "path": "", 17 | "children": [ 18 | { 19 | "name": "handle", 20 | "path": "/test/range-slider/formatters/handle" 21 | }, 22 | { 23 | "name": "pips", 24 | "path": "/test/range-slider/formatters/pips" 25 | }, 26 | { 27 | "name": "range", 28 | "path": "/test/range-slider/formatters/range" 29 | } 30 | ] 31 | }, 32 | { 33 | "name": "limits", 34 | "path": "/test/range-slider/limits" 35 | }, 36 | { 37 | "name": "minmax", 38 | "path": "/test/range-slider/minmax", 39 | "children": [ 40 | { 41 | "name": "custom-min-max", 42 | "path": "/test/range-slider/minmax/custom-min-max" 43 | }, 44 | { 45 | "name": "decimal-min-max", 46 | "path": "/test/range-slider/minmax/decimal-min-max" 47 | }, 48 | { 49 | "name": "explicit-value", 50 | "path": "/test/range-slider/minmax/explicit-value" 51 | }, 52 | { 53 | "name": "invalid-min-max", 54 | "path": "/test/range-slider/minmax/invalid-min-max" 55 | }, 56 | { 57 | "name": "negative-min-max", 58 | "path": "/test/range-slider/minmax/negative-min-max" 59 | } 60 | ] 61 | }, 62 | { 63 | "name": "pips", 64 | "path": "/test/range-slider/pips", 65 | "children": [ 66 | { 67 | "name": "labels", 68 | "path": "/test/range-slider/pips/labels" 69 | }, 70 | { 71 | "name": "limits", 72 | "path": "/test/range-slider/pips/limits" 73 | }, 74 | { 75 | "name": "pips", 76 | "path": "/test/range-slider/pips/pips" 77 | }, 78 | { 79 | "name": "pipsteps", 80 | "path": "/test/range-slider/pips/pipsteps" 81 | }, 82 | { 83 | "name": "pipsteps-large", 84 | "path": "/test/range-slider/pips/pipsteps-large" 85 | }, 86 | { 87 | "name": "steps", 88 | "path": "/test/range-slider/pips/steps" 89 | } 90 | ] 91 | }, 92 | { 93 | "name": "range", 94 | "path": "/test/range-slider/range", 95 | "children": [ 96 | { 97 | "name": "draggy", 98 | "path": "/test/range-slider/range/draggy" 99 | }, 100 | { 101 | "name": "false", 102 | "path": "/test/range-slider/range/false" 103 | }, 104 | { 105 | "name": "gaps", 106 | "path": "/test/range-slider/range/gaps" 107 | }, 108 | { 109 | "name": "max", 110 | "path": "/test/range-slider/range/max" 111 | }, 112 | { 113 | "name": "min", 114 | "path": "/test/range-slider/range/min" 115 | }, 116 | { 117 | "name": "pushy", 118 | "path": "/test/range-slider/range/pushy" 119 | } 120 | ] 121 | }, 122 | { 123 | "name": "states", 124 | "path": "", 125 | "children": [ 126 | { 127 | "name": "disabled", 128 | "path": "/test/range-slider/states/disabled" 129 | }, 130 | { 131 | "name": "hoverable", 132 | "path": "/test/range-slider/states/hoverable" 133 | }, 134 | { 135 | "name": "reversed", 136 | "path": "/test/range-slider/states/reversed" 137 | } 138 | ] 139 | }, 140 | { 141 | "name": "styles", 142 | "path": "", 143 | "children": [ 144 | { 145 | "name": "darkmode", 146 | "path": "/test/range-slider/styles/darkmode" 147 | } 148 | ] 149 | }, 150 | { 151 | "name": "values", 152 | "path": "", 153 | "children": [ 154 | { 155 | "name": "binding", 156 | "path": "", 157 | "children": [ 158 | { 159 | "name": "multiple", 160 | "path": "/test/range-slider/values/binding/multiple" 161 | }, 162 | { 163 | "name": "single", 164 | "path": "/test/range-slider/values/binding/single" 165 | } 166 | ] 167 | }, 168 | { 169 | "name": "constrained-values", 170 | "path": "/test/range-slider/values/constrained-values" 171 | }, 172 | { 173 | "name": "explicit-value", 174 | "path": "/test/range-slider/values/explicit-value" 175 | }, 176 | { 177 | "name": "multiple-values", 178 | "path": "/test/range-slider/values/multiple-values" 179 | }, 180 | { 181 | "name": "seven-values", 182 | "path": "/test/range-slider/values/seven-values" 183 | }, 184 | { 185 | "name": "single-value", 186 | "path": "/test/range-slider/values/single-value" 187 | }, 188 | { 189 | "name": "value-values", 190 | "path": "/test/range-slider/values/value-values" 191 | } 192 | ] 193 | } 194 | ] 195 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/+layout.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
    ($isNavVisible = false)} 15 | > 16 | 17 | 18 | 21 |
    22 | 23 | 24 | 25 | 98 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/base/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    Basic slider with default settings
    6 | 7 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/events/+page.svelte: -------------------------------------------------------------------------------- 1 | 49 | 50 |
    51 |

    Range Slider Event Tests

    52 | 53 |
    54 | 58 | 62 |
    63 | 64 |
    65 |

    Test Scenarios

    66 | 67 | 68 |
    69 | 70 |
    71 |

    Single Handle Slider

    72 |
    73 | 82 |
    83 |
    84 | 85 |
    86 |

    Range Slider

    87 |
    88 | 100 |
    101 |
    102 | 103 |
    104 |

    Disabled Slider

    105 |
    106 | 118 |
    119 |
    120 | 121 |
    122 |

    Multi-Handle Slider

    123 |
    124 | 133 |
    134 |
    135 | 136 |
    137 |

    Last Event

    138 | {#if lastEvent} 139 |
    140 |

    type: {lastEvent.type}

    141 |
    {formatEventDetail(lastEvent.detail)}
    142 |
    143 | {:else} 144 |

    No events yet

    145 | {/if} 146 | 147 |

    Event History

    148 | {#if events.length > 0} 149 |
    150 | {#each events as event} 151 |
    152 |

    type: {event.type}

    153 |
    {formatEventDetail(event.detail)}
    154 |
    155 | {/each} 156 |
    157 | {:else} 158 |

    No events recorded

    159 | {/if} 160 |
    161 |
    162 | 163 | 224 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/floats/+page.svelte: -------------------------------------------------------------------------------- 1 | 30 | 31 |
    32 |
    Basic Float Tests
    33 | 34 | 35 | 36 |
    Basic RangeFloat Tests
    37 | 38 | 39 | 40 |
    Formatted Floats
    41 | 42 | 43 | 52 | 53 |
    Edge Cases
    54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
    Float + RangeFloat Interactions
    62 | 63 | 64 | 65 |
    Orientation Tests
    66 | 67 | 68 | 69 | 70 | 71 |
    Interactive State Tests
    72 | 73 | 74 | 75 | 76 | 77 |
    Accessibility Tests
    78 | 86 | 87 |
    Dynamic Toggle Tests
    88 |
    89 | 90 |
    91 | 92 | 95 | 98 | 101 |
    102 |
    103 |
    104 | 105 | 110 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/formatters/pips/+page.svelte: -------------------------------------------------------------------------------- 1 | 56 | 57 |
    58 |

    Basic Formatter Tests

    59 |

    Default formatter - no formatting applied

    60 | 61 | 62 |

    Number formatter - displays values with 2 decimal places

    63 | 64 | 65 |

    Currency formatter - displays values with $ prefix and 2 decimal places

    66 | 67 | 68 |

    Percent formatter - displays values as percentages with 1 decimal place

    69 | 70 | 71 |

    Index formatter - displays pip number and value

    72 | 73 | 74 |

    HTML formatter - displays values with HTML formatting

    75 | 76 | 77 |

    Locale formatter - displays values with locale formatting

    78 | 79 | 80 |

    Edge Cases

    81 |

    Large numbers - handles values in millions

    82 | new Intl.NumberFormat('en-US', { notation: 'compact', compactDisplay: 'short' }).format(v)} 89 | /> 90 | 91 |

    Negative numbers - handles negative values

    92 | 93 | 94 |

    Decimal numbers - handles values with decimal places

    95 | 96 | 97 |

    Extreme values - handles very large positive and negative values

    98 | 99 | 100 |

    Dynamic Formatter Tests

    101 |
    102 |

    Dynamic formatter - tests runtime formatter changes

    103 | 111 |

    Current formatter: {formatters[currentFormatter].name}

    112 |
    113 | 114 | 117 | 120 |
    121 |
    122 | 123 |

    Enable/Disable formatter - tests formatter toggle

    124 | v} /> 125 |
    126 | 129 |
    130 | 131 |

    Failing Cases

    132 |

    No pips - formatter should not be applied

    133 | 134 | 135 |

    Pips without labels - formatter should not be visible

    136 | 137 | 138 |

    Pips with all=false - formatter should not be visible

    139 | 140 | 141 |

    First pip only - should only show first pip label

    142 | 143 | 144 |

    Last pip only - should only show last pip label

    145 | 146 | 147 |

    Rest pips only - should only show rest pip labels

    148 | 149 | 150 |

    Pips with selective labels (first/last only)

    151 | 152 | 153 |

    Pips with rest only

    154 | 155 | 156 |

    Performance Tests

    157 |

    Heavy formatter - tests performance with computationally expensive formatter

    158 | 159 | 160 |

    Formatter Context Tests

    161 |

    Reversed mode - tests percentage calculation in reversed orientation

    162 | 163 | 164 |

    Vertical mode - tests formatting in vertical orientation

    165 | 166 | 167 |

    Custom range - tests formatting with non-standard min/max values

    168 | 169 | 170 |

    Step values - tests formatting with non-unit step values

    171 | 172 |
    173 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/limits/+page.svelte: -------------------------------------------------------------------------------- 1 | 35 | 36 |
    37 |
    Single handle with limits
    38 | 39 | 40 |
    Reversed horizontal slider with limits
    41 | 42 | 43 |
    Range slider with limits
    44 | 45 | 46 |
    Range slider with limits and pushy
    47 | 48 | 49 |
    Range slider with limits and rangeGap
    50 | 58 | 59 |
    Vertical slider with limits
    60 | 61 | 62 |
    Reversed vertical slider with limits
    63 | 73 | 74 |
    Dynamic controls
    75 |
    76 |
    77 | 78 | 79 |
    80 |
    81 | 82 | 83 |
    84 |
    85 | 86 | 87 |
    88 |
    89 | 90 | 91 |
    92 | 95 | 98 | 101 |
    102 | 111 |
    112 | 113 | 115 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/minmax/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    6 |

    Default Min/Max Tests

    7 |

    Default min/max (0/100)

    8 | 9 | 10 |

    Min Only Tests

    11 |

    Positive min (20)

    12 | 13 | 14 |

    Negative min (-50)

    15 | 16 | 17 |

    Decimal min (10.5)

    18 | 19 | 20 |

    Large min (1000)

    21 | 22 | 23 |

    Very large min (1000000)

    24 | 25 | 26 |

    Max Only Tests

    27 |

    Small max (50)

    28 | 29 | 30 |

    Negative max (-20)

    31 | 32 | 33 |

    Decimal max (75.5)

    34 | 35 | 36 |

    Large max (1000)

    37 | 38 | 39 |

    Very large max (1000000)

    40 | 41 | 42 |

    Combined Min/Max Tests

    43 |

    Positive range (20/80)

    44 | 45 | 46 |

    Negative range (-80/-20)

    47 | 48 | 49 |

    Cross-zero range (-50/50)

    50 | 51 | 52 |

    Decimal range (10.5/90.5)

    53 | 54 | 55 |

    Large range (1000/10000)

    56 | 57 | 58 |

    Very large range (1000000/2000000)

    59 | 60 | 61 |

    Asymmetric Range Tests

    62 |

    Small to large (1/1000)

    63 | 64 | 65 |

    Negative to large (-100/1000)

    66 | 67 | 68 |

    Very negative to small (-1000/10)

    69 | 70 | 71 |

    Decimal to whole (0.5/100)

    72 | 73 | 74 |

    Whole to decimal (0/99.9)

    75 | 76 | 77 |

    Edge Cases

    78 |

    Very close range (49.9/50.1)

    79 | 80 | 81 |

    Tiny decimal range (0.001/0.009)

    82 | 83 | 84 |

    Tiny decimal range (0.001/0.009) with precision set to 3

    85 | 95 | 96 |

    Huge range (-1000000/1000000)

    97 | 98 | 99 |

    Invalid Min/Max Tests

    100 | 101 |

    Zero-width range (50/50)

    102 | 103 | 104 |

    Min greater than max (80/20)

    105 | 106 | 107 |

    Max less than min (-20/-80)

    108 | 109 | 110 |

    NaN values

    111 | 112 | 113 |

    Infinite values

    114 | 115 |
    116 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/minmax/custom-min-max/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    Slider with custom min/max range (-50 to 50)
    6 | 7 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/minmax/decimal-min-max/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    Slider with decimal min/max range (0.5 to 2.5)
    6 | 7 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/minmax/explicit-value/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    Slider with explicit value (90) in range (0 to 200)
    6 | 7 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/minmax/invalid-min-max/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    Slider with invalid min/max (min > max)
    6 | 7 | 8 |
    Slider with invalid min/max (max < min)
    9 | 10 | 11 |
    Slider with NaN min
    12 | 13 | 14 |
    Slider with NaN max
    15 | 16 | 17 |
    Slider with Infinity min
    18 | 19 | 20 |
    Slider with Infinity max
    21 | 22 | 23 |
    Slider with -Infinity min
    24 | 25 | 26 |
    Slider with -Infinity max
    27 | 28 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/minmax/negative-min-max/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    Slider with negative min/max range (-100 to -50)
    6 | 7 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/pips/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    Basic slider with default pips
    6 | 7 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/pips/labels/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    6 |
    All pips with labels
    7 | 8 | 9 |
    Only first and last with labels
    10 | 11 | 12 |
    Only rest with labels
    13 | 14 | 15 |
    Only first with label
    16 | 17 | 18 |
    Only last with label
    19 | 20 | 21 |
    First and rest with labels
    22 | 23 | 24 |
    Last and rest with labels
    25 | 26 | 27 |
    No labels (all=false)
    28 | 29 | 30 |
    No labels (first=false, last=false, rest=false)
    31 | 32 | 33 |
    Labels with step=2.5
    34 | 35 | 36 |
    Labels with step=7.3
    37 | 38 | 39 |
    Labels with step=2.5 and custom min/max
    40 | 41 | 42 |
    Labels with step=0.1
    43 | 44 | 45 |
    Labels with max-min=1000
    46 | 47 | 48 |
    Labels with max-min=10000
    49 | 50 | 51 |
    Labels with max-min=100000
    52 | 53 |
    54 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/pips/limits/+page.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 |
    29 |
    Default range mode
    30 | 31 | 32 |
    Range="min"
    33 | 34 | 35 |
    Range="max"
    36 | 37 | 38 |
    Range={true}
    39 | 40 | 41 |
    Limits
    42 | 43 | 44 |
    Range="min" with limits
    45 | 46 | 47 |
    Range="max" with limits
    48 | 49 | 50 |
    Range={true} with limits
    51 | 52 | 53 | 61 |
    Dynamic range and limits controls
    62 |
    63 |
    64 | 65 | 66 |
    67 |
    68 | 69 | 70 |
    71 | 74 | 77 |
    78 |
    79 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/pips/pips/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    6 |
    All pips visible
    7 | 8 | 9 |
    Only first and last visible
    10 | 11 | 12 |
    Only rest visible
    13 | 14 | 15 |
    Only first visible
    16 | 17 | 18 |
    Only last visible
    19 | 20 | 21 |
    First and rest visible
    22 | 23 | 24 |
    Last and rest visible
    25 | 26 | 27 |
    No pips visible (all=false)
    28 | 29 | 30 |
    No pips visible (first=false, last=false, rest=false)
    31 | 32 |
    33 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/pips/pipsteps-large/+page.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
    17 |
    Very large number of pips, explicitly set
    18 | 19 |
    20 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/pips/pipsteps/+page.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
    17 |
    Default pipstep (no pipstep prop)
    18 | 19 | 20 |
    pipstep=2 (shows every other step)
    21 | 22 | 23 |
    pipstep=5 (shows every 5th step)
    24 | 25 | 26 |
    pipstep=10 (shows every 10th step)
    27 | 28 | 29 |
    pipstep=2.5 (shows every 2.5th step)
    30 | 31 | 32 |
    pipstep=7.3 (shows every 7.3rd step)
    33 | 34 | 35 |
    pipstep=0.1 (should reduce due to stepMax limit)
    36 | 37 | 38 |
    pipstep=0.01 (should reduce due to 1000 pip limit)
    39 | 40 | 41 |
    pipstep=0.0001 (should reduce due to 1000 pip limit)
    42 | 43 | 44 |
    pipstep=2 with custom min/max
    45 | 46 | 47 |
    pipstep=5 with custom min/max
    48 | 49 | 50 |
    pipstep=2.5 with custom min/max
    51 | 52 | 53 |
    pipstep=7.3 with custom min/max
    54 | 55 | 56 |
    pipstep=2 with large range (max=1000)
    57 | 58 | 59 |
    pipstep=5 with large range (max=1000)
    60 | 61 | 62 |
    pipstep=10 with large range (max=1000)
    63 | 64 | 65 |
    pipstep=200 with very large range (max=10000)
    66 | 67 | 68 |
    pipstep=500 with very large range (max=100000)
    69 | 70 | 71 |
    pipstep=10 with very large range (max=10000)
    72 | 73 | 74 |
    Dynamic pipstep control (toggle between no pipstep and pipstep=2.5)
    75 |
    76 | 79 |
    80 | 81 | 82 |
    Dynamic pipstep values (toggle between pipstep=5 and pipstep=10)
    83 |
    84 | 87 |
    88 | 89 |
    90 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/pips/steps/+page.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
    19 |
    Default step (max-min = 100)
    20 | 21 | 22 |
    Default step (max-min = 1000)
    23 | 24 | 25 |
    Default step (max-min = 10000)
    26 | 27 | 28 |
    Default step (max-min = 100000)
    29 | 30 | 31 |
    Default step (max = 90)
    32 | 33 | 34 |
    Default step (min = 20)
    35 | 36 | 37 |
    Default step (min = -20, max = 50)
    38 | 39 | 40 |
    Step of 5
    41 | 42 | 43 |
    Step of 10
    44 | 45 | 46 |
    Step of 2.5
    47 | 48 | 49 |
    Step of 7.3
    50 | 51 | 52 |
    Step of 13.7
    53 | 54 | 55 |
    Step of 2.5 with custom min/max
    56 | 57 | 58 |
    Step of 7.3 with custom min/max
    59 | 60 | 61 |
    Step of 17 with custom min/max (wholly divisible)
    62 | 63 | 64 |
    Step of 23 with custom min/max (not wholly divisible)
    65 | 66 | 67 |
    Step of 0.1 (1000 steps)
    68 | 69 | 70 |
    Step of 0.01 (10000 steps)
    71 | 72 | 73 |
    Step of 100 (only 1 & 100 selectable)
    74 | 75 | 76 |
    Step of 0.0001 (crazy number of steps to choose)
    77 | 78 | 79 |
    Dynamic step control
    80 |
    81 | 84 |
    85 | 86 | 87 |
    Dynamic min/max with step
    88 |
    89 | 92 |
    93 | 101 |
    102 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/range/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    6 |
    Single handle range sliders (should not be a range due to only one value)
    7 | 8 | 9 | 10 |
    Double handle range sliders
    11 | 12 | 13 | 14 | 15 |
    Triple handle range sliders (should trim to double handle)
    16 | 17 | 18 |
    19 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/range/draggy/+page.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
    10 |
    Basic range slider with draggy disabled
    11 | 12 | 13 |
    Range slider with draggy enabled
    14 | 15 | 16 |
    Range slider with fixed 10% gap and pushy behavior
    17 | 18 | 19 |
    20 |
    Range slider with toggleable draggy behavior ({draggyToggle ? 'true' : 'false'})
    21 | 22 | 25 |
    26 |
    27 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/range/false/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    6 |
    Single handle slider with range=false
    7 | 8 | 9 |
    Double handle slider with range=false
    10 | 11 | 12 |
    Triple handle slider with range=false
    13 | 14 |
    15 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/range/gaps/+page.svelte: -------------------------------------------------------------------------------- 1 | 43 | 44 |
    45 |

    Basic Range Gap Tests

    46 |

    Default behavior - no gap constraints

    47 | 48 | 49 |

    Minimum gap only - handles cannot be closer than 20 units

    50 | 51 | 52 |

    Maximum gap only - handles cannot be further than 40 units

    53 | 54 | 55 |

    Both gaps - handles must be between 20 and 40 units apart

    56 | 57 | 58 |

    Edge Cases and Limits

    59 |

    Equal gaps - handles must be exactly 30 units apart

    60 | 61 |
    62 | 69 |
    70 | 71 |

    Zero gaps - handles can be at the same position

    72 | 73 | 74 |

    Large gaps - works with huge min/max values

    75 | 84 | 85 |

    Invalid gap values - min greater than max

    86 | 87 | 88 |

    Negative gap values - should be ignored

    89 | 90 | 91 |

    Gaps larger than range - should be constrained

    92 | 93 | 94 |

    Interaction with Range Slider Features

    95 |

    Pushy mode with gaps - handles push each other when gap constraints are met

    96 | 97 | 98 |

    Limits with gaps - handles respect both limits and gaps

    99 | 100 | 101 |

    Step with gaps - handles align to step while respecting gaps

    102 | 103 | 104 |

    Precision with gaps - handles respect precision while maintaining gaps

    105 | 106 | 107 |

    Dynamic Gap Tests

    108 |

    Dynamic gaps - gap constraints can be updated at runtime

    109 | 110 |
    111 | 112 | 113 | 114 | 115 |
    116 | 117 |

    Accessibility Tests

    118 |

    Gaps with aria labels - verify screen reader compatibility

    119 | 120 | 121 |

    Keyboard navigation with gaps - verify arrow key behavior

    122 | 133 | {keyboardValues} 134 | 135 |

    Event Handling Tests

    136 |

    Events with gaps - verify event payloads

    137 | 147 | 148 |

    Error Handling and Fallbacks

    149 |

    Disabled state with gaps - verify constraints maintained

    150 | 151 | 152 |

    Performance Tests

    153 |

    Large gap values - test performance with extreme gap constraints

    154 | 155 |
    156 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/range/max/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    6 |
    Single handle max-range slider
    7 | 8 | 9 |
    Double handle max-range slider (should trim to single handle)
    10 | 11 | 12 |
    Triple handle max-range slider with custom max (should trim to single handle)
    13 | 14 |
    15 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/range/min/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    6 |
    Single handle min-range slider
    7 | 8 | 9 |
    Double handle min-range slider (should trim to single handle)
    10 | 11 | 12 |
    Triple handle min-range slider with custom min (should trim to single handle)
    13 | 14 |
    15 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/range/pushy/+page.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 |
    23 |
    Basic range slider with pushy disabled
    24 | 25 | 26 |
    Range slider with pushy enabled
    27 | 28 | 29 |
    30 |
    Range slider with toggleable pushy behavior ({pushyToggle ? 'true' : 'false'})
    31 | 32 | 35 |
    36 | 37 |
    Range slider with decimal steps (step=0.1)
    38 | 39 | 40 |
    Range slider in vertical orientation
    41 | 42 | 43 |
    Range slider with negative values
    44 | 45 | 46 |
    Range slider for edge cases
    47 | 48 |
    49 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/states/disabled/+page.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
    9 |
    Disabled Single Slider
    10 | 20 | 21 |
    Disabled Range Slider
    22 | 33 | 34 |
    Dynamic Disabled State
    35 | = 75} 39 | min={0} 40 | max={100} 41 | step={1} 42 | pips={true} 43 | all="label" 44 | /> 45 |
    46 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/states/hoverable/+page.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
    10 |
    Non-Hoverable Single Slider
    11 | 22 | 23 |
    Non-Hoverable Range Slider
    24 | 36 | 37 |
    Dynamic Hoverable State
    38 | 41 | 52 |
    53 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/states/reversed/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    6 |
    Single handle reversed slider
    7 | 8 | 9 |
    Single handle reversed slider with custom range
    10 | 11 | 12 |
    Double handle reversed range slider
    13 | 14 |
    15 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/styles/darkmode/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |

    RangeSlider Darkmode Test Page

    6 | 7 |
    8 |

    Darkmode: false (default)

    9 | 10 |
    11 | 12 |
    13 |

    Darkmode: auto (system)

    14 | 15 |
    16 | 17 |
    18 |

    Darkmode: force

    19 | 20 |
    21 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/values/binding/multiple/+page.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
    7 |
    Range slider with two-way binding on multiple values
    8 | 9 | 10 |
    11 |
    12 | 13 | 14 |
    15 |
    16 | 17 | 18 |
    19 |
    20 |
    21 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/values/binding/single/+page.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
    7 |
    Slider with two-way binding on single value
    8 | 9 | 10 |
    11 |
    12 | 16 | 17 |
    18 |
    19 |
    20 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/values/constrained-values/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    Slider with out-of-bounds values (should constrain to min/max)
    6 | 7 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/values/explicit-value/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    Slider with explicit value in custom range
    6 | 7 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/values/multiple-values/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    Slider with multiple values
    6 | 7 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/values/seven-values/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    Slider with seven handles
    6 | 7 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/values/single-value/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    Basic slider with single value
    6 | 7 | -------------------------------------------------------------------------------- /src/routes/test/range-slider/values/value-values/+page.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
    Slider with both value and values props (value should take precedence)
    9 | 10 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simeydotme/svelte-range-slider-pips/f0d144cf047d59381c204cd34ffdcac5a5b1896d/static/favicon.png -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-auto'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 12 | // If your environment is not supported or you settled on a specific environment, switch out the adapter. 13 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 14 | adapter: adapter(), 15 | alias: { 16 | $components: './src/lib/components', 17 | $test: './src/routes/test/range-slider' 18 | } 19 | }, 20 | compilerOptions: { 21 | customElement: true, 22 | immutable: false 23 | } 24 | }; 25 | 26 | export default config; 27 | -------------------------------------------------------------------------------- /tests/jquery/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |

    Testing loading jQuery script

    11 |
    12 | 13 |

    -7,7

    14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/jquery/index.jquery.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | const slider = document.querySelector('.my-slider'); 3 | const rangeSliderPips = new RangeSliderPips({ 4 | target: slider, 5 | props: { 6 | values: [-7, 7], 7 | pips: true, 8 | first: 'label', 9 | last: 'label', 10 | range: true, 11 | min: -10, 12 | max: 10 13 | } 14 | }); 15 | 16 | // listen for changes to the slider and update the output 17 | const $output = $('#value-output'); 18 | rangeSliderPips.$on('change', function (e) { 19 | $output.html(e.detail.values[0] + ',' + e.detail.values[1]); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/jquery/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-test", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "Test of jQuery execution", 6 | "main": "index.html", 7 | "scripts": { 8 | "dev": "serve .", 9 | "start": "serve ." 10 | }, 11 | "devDependencies": { 12 | "jquery": "^3.7.1", 13 | "range-slider-pips": "file:../..", 14 | "serve": "^14.2.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/playwright/RangePips._.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test.describe('Basic Pips Tests', () => { 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto('/test/range-slider/pips'); 6 | await page.waitForLoadState('networkidle'); 7 | }); 8 | 9 | test('should render pips correctly with default settings', async ({ page }) => { 10 | // Check that pips container exists 11 | const pipsContainer = page.locator('.rangePips'); 12 | await expect(pipsContainer).toBeVisible(); 13 | 14 | // Check that first and last pips exist (default behavior) 15 | const firstPip = page.locator('.rsPip--first'); 16 | const lastPip = page.locator('.rsPip--last'); 17 | await expect(firstPip).toBeVisible(); 18 | await expect(lastPip).toBeVisible(); 19 | 20 | // Check that first pip shows min value (0) 21 | await expect(firstPip).toHaveAttribute('data-val', '0'); 22 | 23 | // Check that last pip shows max value (100) 24 | await expect(lastPip).toHaveAttribute('data-val', '100'); 25 | 26 | // Check that some intermediate pips exist 27 | const intermediatePips = page.locator('.rsPip:not(.rsPip--first):not(.rsPip--last)'); 28 | await expect(intermediatePips).toHaveCount(await intermediatePips.count()); 29 | expect(await intermediatePips.count()).toBeGreaterThan(0); 30 | 31 | // Check that the selected pip (at 50) is highlighted 32 | const selectedPip = page.locator('.rsPip.rsSelected'); 33 | await expect(selectedPip).toBeVisible(); 34 | await expect(selectedPip).toHaveAttribute('data-val', '50'); 35 | 36 | // Check that pips are interactive by clicking one 37 | const handle = page.locator('.rangeHandle'); 38 | const initialValue = await handle.getAttribute('aria-valuenow'); 39 | 40 | // Click a pip value and verify the handle moves 41 | await page.locator('.rsPip[data-val="75"]').click(); 42 | await expect(handle).toHaveAttribute('aria-valuenow', '75'); 43 | await expect(handle, 'to be positioned at 75%').toHaveCSS('translate', '750px'); 44 | 45 | // Verify the selected pip updates 46 | await expect(selectedPip).toHaveAttribute('data-val', '75'); 47 | }); 48 | 49 | test('no values are rendered for default setup', async ({ page }) => { 50 | // Check that pip values are not rendered 51 | const pipValues = page.locator('.rsPipVal'); 52 | await expect(pipValues).toHaveCount(0); 53 | }); 54 | 55 | test('should apply all the correct css classes', async ({ page }) => { 56 | // Check that the slider has the pips class 57 | const slider = page.locator('.rangeSlider'); 58 | const pipsContainer = page.locator('.rangePips'); 59 | const selectedPip = page.locator('.rsPip.rsSelected'); 60 | const firstPip = page.locator('.rsPip--first'); 61 | const lastPip = page.locator('.rsPip--last'); 62 | 63 | // Check that the slider has the pips class 64 | await expect(slider).toHaveClass(/\brsPips\b/); 65 | 66 | // Check CSS classes on the pips container 67 | await expect(pipsContainer).toHaveClass(/\brsHoverable\b/); 68 | await expect(pipsContainer).not.toHaveClass(/\brsDisabled\b/); 69 | await expect(pipsContainer).not.toHaveClass(/\brsVertical\b/); 70 | await expect(pipsContainer).not.toHaveClass(/\brsReversed\b/); 71 | await expect(pipsContainer).not.toHaveClass(/\brsFocus\b/); 72 | 73 | // Check CSS classes on a regular pip 74 | const regularPip = page.locator('.rsPip:not(.rsPip--first):not(.rsPip--last):not(.rsSelected)').first(); 75 | await expect(regularPip).toHaveClass(/\brsPip\b/); 76 | await expect(regularPip).not.toHaveClass(/\brsSelected\b/); 77 | await expect(regularPip).not.toHaveClass(/\brsInRange\b/); 78 | await expect(regularPip).not.toHaveClass(/\brsOutOfLimit\b/); 79 | 80 | // Check CSS classes on the selected pip 81 | await expect(selectedPip).toHaveClass(/\brsPip\b/); 82 | await expect(selectedPip).toHaveClass(/\brsSelected\b/); 83 | await expect(selectedPip).not.toHaveClass(/\brsInRange\b/); 84 | await expect(selectedPip).not.toHaveClass(/\brsOutOfLimit\b/); 85 | 86 | // Check CSS classes on first and last pips 87 | await expect(firstPip).toHaveClass(/\brsPip\b/); 88 | await expect(firstPip).toHaveClass(/\brsPip--first\b/); 89 | await expect(lastPip).toHaveClass(/\brsPip\b/); 90 | await expect(lastPip).toHaveClass(/\brsPip--last\b/); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /tests/playwright/RangePips.pips.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test.describe('Pip Visibility Tests', () => { 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto('/test/range-slider/pips/pips'); 6 | await page.waitForLoadState('networkidle'); 7 | }); 8 | 9 | test('all pips should be visible when all="pip"', async ({ page }) => { 10 | const slider = page.locator('#all-pips'); 11 | const pips = slider.locator('.rsPip'); 12 | 13 | // All pips should be visible 14 | await expect(pips).toHaveCount(21); 15 | }); 16 | 17 | test('only first and last should be visible when first="pip" last="pip" rest={false}', async ({ page }) => { 18 | const slider = page.locator('#first-last-pips'); 19 | const firstPip = slider.locator('.rsPip--first'); 20 | const lastPip = slider.locator('.rsPip--last'); 21 | const restPips = slider.locator('.rsPip:not(.rsPip--first):not(.rsPip--last)'); 22 | 23 | // Only first and last should be visible 24 | await expect(firstPip).toBeVisible(); 25 | await expect(lastPip).toBeVisible(); 26 | await expect(restPips).toHaveCount(0); 27 | }); 28 | 29 | test('only rest should be visible when rest="pip"', async ({ page }) => { 30 | const slider = page.locator('#rest-pips'); 31 | const firstPip = slider.locator('.rsPip--first'); 32 | const lastPip = slider.locator('.rsPip--last'); 33 | const restPips = slider.locator('.rsPip:not(.rsPip--first):not(.rsPip--last)'); 34 | 35 | // Only rest should be visible 36 | await expect(firstPip).toHaveCount(0); 37 | await expect(lastPip).toHaveCount(0); 38 | await expect(restPips).toHaveCount(19); 39 | }); 40 | 41 | test('only first should be visible when first="pip"', async ({ page }) => { 42 | const slider = page.locator('#first-pip'); 43 | const firstPip = slider.locator('.rsPip--first'); 44 | const otherPips = slider.locator('.rsPip:not(.rsPip--first)'); 45 | 46 | // Only first should be visible 47 | await expect(firstPip).toBeVisible(); 48 | await expect(otherPips).toHaveCount(0); 49 | }); 50 | 51 | test('only last should be visible when last="pip"', async ({ page }) => { 52 | const slider = page.locator('#last-pip'); 53 | const lastPip = slider.locator('.rsPip--last'); 54 | const otherPips = slider.locator('.rsPip:not(.rsPip--last)'); 55 | 56 | // Only last should be visible 57 | await expect(lastPip).toBeVisible(); 58 | await expect(otherPips).toHaveCount(0); 59 | }); 60 | 61 | test('first and rest should be visible when first="pip" rest="pip"', async ({ page }) => { 62 | const slider = page.locator('#first-rest-pips'); 63 | const firstPip = slider.locator('.rsPip--first'); 64 | const lastPip = slider.locator('.rsPip--last'); 65 | const restPips = slider.locator('.rsPip:not(.rsPip--first):not(.rsPip--last)'); 66 | 67 | // First and rest should be visible 68 | await expect(firstPip).toBeVisible(); 69 | await expect(lastPip).toHaveCount(0); 70 | await expect(restPips).toHaveCount(19); 71 | }); 72 | 73 | test('last and rest should be visible when last="pip" rest="pip"', async ({ page }) => { 74 | const slider = page.locator('#last-rest-pips'); 75 | const firstPip = slider.locator('.rsPip--first'); 76 | const lastPip = slider.locator('.rsPip--last'); 77 | const restPips = slider.locator('.rsPip:not(.rsPip--first):not(.rsPip--last)'); 78 | 79 | // Last and rest should be visible 80 | await expect(firstPip).toHaveCount(0); 81 | await expect(lastPip).toBeVisible(); 82 | await expect(restPips).toHaveCount(19); 83 | }); 84 | 85 | test('no pips should be visible when all={false}', async ({ page }) => { 86 | const slider = page.locator('#no-pips'); 87 | const pips = slider.locator('.rsPip'); 88 | 89 | // No pips should be visible 90 | await expect(pips).toHaveCount(0); 91 | }); 92 | 93 | test('no pips should be visible when first={false} last={false} rest={false}', async ({ page }) => { 94 | const slider = page.locator('#no-pips-explicit'); 95 | const pips = slider.locator('.rsPip'); 96 | 97 | // No pips should be visible 98 | await expect(pips).toHaveCount(0); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /tests/playwright/RangeSlider._.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test.describe('Basic Tests', () => { 4 | test.describe('no props', () => { 5 | test('should render correctly', async ({ page }) => { 6 | await page.goto('/test/range-slider/base'); 7 | await page.waitForLoadState('networkidle'); 8 | 9 | // Check component exists 10 | await expect(page.locator('.rangeSlider')).toBeVisible(); 11 | await expect(page.locator('.rangeHandle')).toBeVisible(); 12 | await expect(page.locator('.rangeNub')).toBeVisible(); 13 | }); 14 | 15 | test('should have correct attributes', async ({ page }) => { 16 | await page.goto('/test/range-slider/base'); 17 | await page.waitForLoadState('networkidle'); 18 | const slider = page.locator('.rangeSlider'); 19 | const handle = page.locator('.rangeHandle'); 20 | 21 | // Check role 22 | await expect(slider).toHaveAttribute('role', 'none'); 23 | await expect(handle).toHaveAttribute('role', 'slider'); 24 | 25 | // Check min/max/value 26 | await expect(handle, 'to have min of 0').toHaveAttribute('aria-valuemin', '0'); 27 | await expect(handle, 'to have max of 100').toHaveAttribute('aria-valuemax', '100'); 28 | await expect(handle, 'to have value of 50').toHaveAttribute('aria-valuenow', '50'); 29 | await expect(handle, 'to have value of 50').toHaveAttribute('aria-valuetext', '50'); 30 | await expect(handle, 'to be positioned at 50%').toHaveCSS('translate', '500px'); 31 | 32 | // Check orientation and disabled state 33 | await expect(handle).toHaveAttribute('data-handle', '0'); 34 | await expect(handle).toHaveAttribute('aria-orientation', 'horizontal'); 35 | await expect(handle).toHaveAttribute('aria-disabled', 'false'); 36 | await expect(handle).toHaveAttribute('tabindex', '0'); 37 | 38 | // check classes 39 | await expect(slider).toHaveClass(/\brsHoverable\b/); 40 | await expect(slider).not.toHaveClass(/\brsMin\b/); 41 | await expect(slider).not.toHaveClass(/\brsMax\b/); 42 | await expect(slider).not.toHaveClass(/\brsRange\b/); 43 | await expect(slider).not.toHaveClass(/\brsDrag\b/); 44 | await expect(slider).not.toHaveClass(/\brsDisabled\b/); 45 | await expect(slider).not.toHaveClass(/\brsVertical\b/); 46 | await expect(slider).not.toHaveClass(/\brsReversed\b/); 47 | await expect(slider).not.toHaveClass(/\brsFocus\b/); 48 | await expect(slider).not.toHaveClass(/\brsPips\b/); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/playwright/RangeSlider.darkmode.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | import type { Page } from '@playwright/test'; 3 | 4 | const SLIDER_IDS = { 5 | light: '#slider-light', 6 | auto: '#slider-auto', 7 | dark: '#slider-dark' 8 | }; 9 | 10 | // These are the expected RGB colors from the CSS variables in RangeSlider.svelte 11 | const COLORS = { 12 | light: { 13 | slider: 'rgb(215, 218, 218)', // --slider-bg 14 | handle: 'rgb(153, 162, 162)' // --slider-base 15 | }, 16 | dark: { 17 | slider: 'rgb(63, 62, 79)', // --slider-dark-bg 18 | handle: 'rgb(130, 128, 159)' // --slider-dark-base 19 | } 20 | }; 21 | 22 | test.describe('RangeSlider darkmode property', () => { 23 | test.beforeEach(async ({ page }) => { 24 | await page.goto('/test/range-slider/styles/darkmode'); 25 | await page.waitForLoadState('networkidle'); 26 | }); 27 | 28 | test('should apply light mode (darkmode=false)', async ({ page }) => { 29 | const slider = page.locator(SLIDER_IDS.light); 30 | const sliderHandle = page.locator(`${SLIDER_IDS.light} .rangeHandle .rangeNub`); 31 | await expect(slider).not.toHaveClass(/\brsDark\b/); 32 | await expect(slider).not.toHaveClass(/\brsAutoDark\b/); 33 | await expect(slider).toHaveCSS('background-color', COLORS.light.slider); 34 | await expect(sliderHandle).toHaveCSS('background-color', COLORS.light.handle); 35 | }); 36 | 37 | test('should apply forced dark mode (darkmode="force")', async ({ page }) => { 38 | const slider = page.locator(SLIDER_IDS.dark); 39 | const sliderHandle = page.locator(`${SLIDER_IDS.dark} .rangeHandle .rangeNub`); 40 | await expect(slider).toHaveClass(/\brsDark\b/); 41 | await expect(slider).not.toHaveClass(/\brsAutoDark\b/); 42 | await expect(slider).toHaveCSS('background-color', COLORS.dark.slider); 43 | await expect(sliderHandle).toHaveCSS('background-color', COLORS.dark.handle); 44 | }); 45 | 46 | test.describe('darkmode={false} ignores system color scheme', () => { 47 | for (const scheme of ['light', 'dark'] as const) { 48 | test(`should always use light colors when system is ${scheme}`, async ({ page }) => { 49 | await page.emulateMedia({ colorScheme: scheme }); 50 | await page.reload(); 51 | const slider = page.locator('#slider-light'); 52 | const handle = page.locator('#slider-light .rangeHandle .rangeNub'); 53 | await expect(slider).not.toHaveClass(/\brsDark\b/); 54 | await expect(slider).not.toHaveClass(/\brsAutoDark\b/); 55 | await expect(slider).toHaveCSS('background-color', COLORS.light.slider); 56 | await expect(handle).toHaveCSS('background-color', COLORS.light.handle); 57 | }); 58 | } 59 | }); 60 | 61 | test.describe('darkmode="force" ignores system color scheme', () => { 62 | for (const scheme of ['light', 'dark'] as const) { 63 | test(`should always use dark colors when system is ${scheme}`, async ({ page }) => { 64 | await page.emulateMedia({ colorScheme: scheme }); 65 | await page.reload(); 66 | const slider = page.locator('#slider-dark'); 67 | const handle = page.locator('#slider-dark .rangeHandle .rangeNub'); 68 | await expect(slider).toHaveClass(/\brsDark\b/); 69 | await expect(slider).not.toHaveClass(/\brsAutoDark\b/); 70 | await expect(slider).toHaveCSS('background-color', COLORS.dark.slider); 71 | await expect(handle).toHaveCSS('background-color', COLORS.dark.handle); 72 | }); 73 | } 74 | }); 75 | 76 | test.describe('darkmode="auto" responds to system color scheme', () => { 77 | test('should use light colors when system is light', async ({ page, context }) => { 78 | await context.setExtraHTTPHeaders({ 'Accept-CH': 'Sec-CH-Prefers-Color-Scheme' }); 79 | await page.emulateMedia({ colorScheme: 'light' }); 80 | await page.reload(); 81 | const slider = page.locator(SLIDER_IDS.auto); 82 | const sliderHandle = page.locator(`${SLIDER_IDS.auto} .rangeHandle .rangeNub`); 83 | await expect(slider).toHaveClass(/\brsAutoDark\b/); 84 | await expect(slider).toHaveCSS('background-color', COLORS.light.slider); 85 | await expect(sliderHandle).toHaveCSS('background-color', COLORS.light.handle); 86 | }); 87 | test('should use dark colors when system is dark', async ({ page, context }) => { 88 | await context.setExtraHTTPHeaders({ 'Accept-CH': 'Sec-CH-Prefers-Color-Scheme' }); 89 | await page.emulateMedia({ colorScheme: 'dark' }); 90 | await page.reload(); 91 | const slider = page.locator(SLIDER_IDS.auto); 92 | const sliderHandle = page.locator(`${SLIDER_IDS.auto} .rangeHandle .rangeNub`); 93 | await expect(slider).toHaveClass(/\brsAutoDark\b/); 94 | await expect(slider).toHaveCSS('background-color', COLORS.dark.slider); 95 | await expect(sliderHandle).toHaveCSS('background-color', COLORS.dark.handle); 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /tests/playwright/RangeSlider.interaction.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | import { dragHandleTo } from './helpers/tools.js'; 3 | 4 | test.describe('Interactions', () => { 5 | test('has focus when clicked', async ({ page }) => { 6 | await page.goto('/test/range-slider/values/single-value'); 7 | await page.waitForLoadState('networkidle'); 8 | const slider = page.locator('.rangeSlider').nth(0); 9 | const handle = slider.getByRole('slider'); 10 | 11 | await slider.isVisible(); 12 | await handle.isVisible(); 13 | await handle.focus(); 14 | await expect(slider).toHaveClass(/\brsFocus\b/); 15 | await expect(handle).toHaveClass(/\brsActive\b/); 16 | }); 17 | 18 | test('should handle mouse clicks on range', async ({ page }) => { 19 | await page.goto('/test/range-slider/values/single-value'); 20 | await page.waitForLoadState('networkidle'); 21 | const slider = page.locator('.rangeSlider').nth(0); 22 | const handle = slider.getByRole('slider'); 23 | 24 | await slider.isVisible(); 25 | const sliderBounds = await slider.boundingBox(); 26 | if (!sliderBounds) throw new Error('Could not get slider bounds'); 27 | 28 | await page.mouse.click( 29 | sliderBounds.x + sliderBounds.width * 0.1, // Click at 10% from left 30 | sliderBounds.y + sliderBounds.height / 2 31 | ); 32 | 33 | await expect(handle).toHaveAttribute('aria-valuenow', '10'); 34 | await expect(handle, 'to be positioned at 10%').toHaveCSS('translate', '100px'); 35 | }); 36 | 37 | test('should handle drag handle operations', async ({ page }) => { 38 | await page.goto('/test/range-slider/values/binding/single'); 39 | await page.waitForLoadState('networkidle'); 40 | const slider = page.locator('.rangeSlider').nth(0); 41 | const handle = slider.getByRole('slider'); 42 | const input = page.getByLabel('Current value:'); 43 | 44 | await slider.isVisible(); 45 | const sliderBounds = await slider.boundingBox(); 46 | if (!sliderBounds) throw new Error('Could not get slider bounds'); 47 | 48 | // Drag the handle to 75% 49 | await dragHandleTo(page, slider, handle, 0.75); 50 | 51 | // Verify the handle and input value 52 | await expect(handle).toHaveAttribute('aria-valuenow', '75'); 53 | await expect(handle, 'to be positioned at 75%').toHaveCSS('translate', '750px'); 54 | await expect(input).toHaveValue('75'); 55 | }); 56 | 57 | test('should update handle position when dragging', async ({ page }) => { 58 | await page.goto('/test/range-slider/values/binding/single'); 59 | await page.waitForLoadState('networkidle'); 60 | const slider = page.locator('.rangeSlider').nth(0); 61 | const handle = slider.getByRole('slider'); 62 | const input = page.getByLabel('Current value:'); 63 | 64 | await slider.isVisible(); 65 | const sliderBounds = await slider.boundingBox(); 66 | if (!sliderBounds) throw new Error('Could not get slider bounds'); 67 | 68 | // Drag the handle to 50% 69 | await dragHandleTo(page, slider, handle, 0.5); 70 | 71 | // Verify the handle and input value 72 | await expect(handle).toHaveAttribute('aria-valuenow', '50'); 73 | await expect(handle, 'to be positioned at 50%').toHaveCSS('translate', '500px'); 74 | await expect(input).toHaveValue('50'); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /tests/playwright/RangeSlider.reversed.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test.describe('Reversed Slider Tests', () => { 4 | test.describe('single handle', () => { 5 | test('should render correctly with default min/max', async ({ page }) => { 6 | await page.goto('/test/range-slider/states/reversed'); 7 | await page.waitForLoadState('networkidle'); 8 | const slider = page.locator('.rangeSlider').nth(0); 9 | const handle = slider.getByRole('slider'); 10 | 11 | // Check component exists 12 | await expect(slider).toBeAttached(); 13 | await expect(handle).toBeAttached(); 14 | await expect(slider).toHaveClass(/\brsReversed\b/); 15 | 16 | // Value of 75 on reversed slider should position from right 17 | await expect(handle).toHaveAttribute('aria-valuenow', '75'); 18 | await expect(handle).toHaveCSS('translate', '250px'); 19 | }); 20 | 21 | test('should handle custom min/max values', async ({ page }) => { 22 | await page.goto('/test/range-slider/states/reversed'); 23 | await page.waitForLoadState('networkidle'); 24 | const slider = page.locator('.rangeSlider').nth(1); 25 | const handle = slider.getByRole('slider'); 26 | 27 | // Check min/max/value 28 | await expect(handle).toHaveAttribute('aria-valuemin', '-100'); 29 | await expect(handle).toHaveAttribute('aria-valuemax', '150'); 30 | await expect(handle).toHaveAttribute('aria-valuenow', '125'); 31 | 32 | // 125 on -100 to 150 range is 90% from right 33 | await expect(handle).toHaveCSS('translate', '100px'); 34 | }); 35 | 36 | test('should handle mouse interactions correctly', async ({ page }) => { 37 | await page.goto('/test/range-slider/states/reversed'); 38 | await page.waitForLoadState('networkidle'); 39 | const slider = page.locator('.rangeSlider').nth(0); 40 | const handle = slider.getByRole('slider'); 41 | 42 | await slider.isVisible(); 43 | const sliderBounds = await slider.boundingBox(); 44 | if (!sliderBounds) throw new Error('Could not get slider bounds'); 45 | 46 | // Click at 10% from left should set value to 90 47 | await page.mouse.click( 48 | sliderBounds.x + sliderBounds.width * 0.1, // Click at 10% from left 49 | sliderBounds.y + sliderBounds.height / 2 50 | ); 51 | // Verify the handle and input value 52 | await expect(handle).toHaveAttribute('aria-valuenow', '90'); 53 | await expect(handle).toHaveCSS('translate', '100px'); 54 | }); 55 | }); 56 | 57 | test.describe('range slider', () => { 58 | test('should render correctly with two handles', async ({ page }) => { 59 | await page.goto('/test/range-slider/states/reversed'); 60 | await page.waitForLoadState('networkidle'); 61 | const slider = page.locator('.rangeSlider').nth(2); 62 | const handles = slider.locator('.rangeHandle'); 63 | 64 | await slider.isVisible(); 65 | 66 | // First handle should be at 25% from right 67 | await expect(handles.nth(0)).toHaveAttribute('aria-valuenow', '0'); 68 | await expect(handles.nth(0)).toHaveCSS('translate', '1000px'); 69 | // Second handle should be at 75% from right 70 | await expect(handles.nth(1)).toHaveAttribute('aria-valuenow', '100'); 71 | await expect(handles.nth(1)).toHaveCSS('translate', '0px'); 72 | // Range bar should span between handles 73 | const rangeBar = page.locator('.rangeBar'); 74 | await expect(rangeBar).toHaveCSS('translate', '0px'); 75 | await expect(rangeBar).toHaveCSS('width', '1000px'); 76 | }); 77 | 78 | test('click at 10% should move the second handle to 90', async ({ page }) => { 79 | await page.goto('/test/range-slider/states/reversed'); 80 | await page.waitForLoadState('networkidle'); 81 | const slider = page.locator('.rangeSlider').nth(2); 82 | const handles = slider.locator('.rangeHandle'); 83 | 84 | await slider.isVisible(); 85 | 86 | // First handle should be at 25% from right 87 | await expect(handles.nth(0)).toHaveAttribute('aria-valuenow', '0'); 88 | await expect(handles.nth(0)).toHaveCSS('translate', '1000px'); 89 | // Second handle should be at 75% from right 90 | await expect(handles.nth(1)).toHaveAttribute('aria-valuenow', '100'); 91 | await expect(handles.nth(1)).toHaveCSS('translate', '0px'); 92 | 93 | // First handle should not be able to go beyond second handle 94 | const sliderBounds = await slider.boundingBox(); 95 | if (!sliderBounds) throw new Error('Could not get slider bounds'); 96 | 97 | // click at 10% should move the second handle to 90 98 | await page.mouse.click(sliderBounds.x + sliderBounds.width * 0.1, sliderBounds.y + sliderBounds.height / 2); 99 | 100 | // click at 90% should move the first handle to 10 101 | await page.mouse.click(sliderBounds.x + sliderBounds.width * 0.9, sliderBounds.y + sliderBounds.height / 2); 102 | 103 | // Handles should maintain their order 104 | await expect(handles.nth(0)).toHaveAttribute('aria-valuenow', '10'); 105 | await expect(handles.nth(1)).toHaveAttribute('aria-valuenow', '90'); 106 | await expect(handles.nth(0)).toHaveCSS('translate', '900px'); 107 | await expect(handles.nth(1)).toHaveCSS('translate', '100px'); 108 | // Range bar should span between handles 109 | const rangeBar = page.locator('.rangeBar'); 110 | await expect(rangeBar).toHaveCSS('translate', '100px'); 111 | await expect(rangeBar).toHaveCSS('width', '800px'); 112 | }); 113 | }); 114 | 115 | test.describe('keyboard interaction', () => { 116 | test('should handle arrow keys correctly in reversed mode', async ({ page }) => { 117 | await page.goto('/test/range-slider/states/reversed'); 118 | await page.waitForLoadState('networkidle'); 119 | const slider = page.locator('.rangeSlider').nth(0); 120 | const handle = slider.getByRole('slider'); 121 | 122 | // Initial value is 75 123 | await expect(handle).toHaveAttribute('aria-valuenow', '75'); 124 | 125 | // arrow keys should be unaffected in reversed mode 126 | await handle.focus(); 127 | await page.keyboard.press('ArrowRight'); 128 | await expect(handle).toHaveAttribute('aria-valuenow', '76'); 129 | 130 | // Left arrow should increase value in reversed mode 131 | await handle.focus(); 132 | await page.keyboard.press('ArrowLeft'); 133 | await page.keyboard.press('ArrowLeft'); 134 | await expect(handle).toHaveAttribute('aria-valuenow', '74'); 135 | }); 136 | 137 | test('should handle arrow keys correctly in reversed range mode', async ({ page }) => { 138 | await page.goto('/test/range-slider/states/reversed'); 139 | await page.waitForLoadState('networkidle'); 140 | const slider = page.locator('.rangeSlider').nth(2); 141 | const handles = slider.getByRole('slider'); 142 | 143 | // Initial values are 0 and 100 144 | await expect(handles.nth(0)).toHaveAttribute('aria-valuenow', '0'); 145 | await expect(handles.nth(1)).toHaveAttribute('aria-valuenow', '100'); 146 | 147 | // First handle: right arrow should decrease value in reversed mode 148 | await handles.nth(0).focus(); 149 | await page.keyboard.press('ArrowRight'); 150 | await expect(handles.nth(0)).toHaveAttribute('aria-valuenow', '1'); 151 | 152 | // First handle: left arrow should not allow value to go below 0 153 | await handles.nth(0).focus(); 154 | await page.keyboard.press('ArrowLeft'); 155 | await expect(handles.nth(0)).toHaveAttribute('aria-valuenow', '0'); 156 | for (let i = 0; i < 5; i++) await page.keyboard.press('ArrowRight'); 157 | await expect(handles.nth(0)).toHaveAttribute('aria-valuenow', '5'); 158 | 159 | // Second handle: right arrow should not allow value to go above 100 160 | await handles.nth(1).focus(); 161 | await page.keyboard.press('ArrowRight'); 162 | await expect(handles.nth(1)).toHaveAttribute('aria-valuenow', '100'); 163 | for (let i = 0; i < 5; i++) await page.keyboard.press('ArrowLeft'); 164 | await expect(handles.nth(1)).toHaveAttribute('aria-valuenow', '95'); 165 | 166 | // Second handle: left arrow should decrease value but not below first handle 167 | await handles.nth(1).focus(); 168 | for (let i = 0; i < 100; i++) await page.keyboard.press('ArrowLeft'); 169 | await expect(handles.nth(1)).toHaveAttribute('aria-valuenow', '5'); 170 | }); 171 | }); 172 | }); 173 | -------------------------------------------------------------------------------- /tests/playwright/RangeSlider.values.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | import { waitTime } from './helpers/utils.js'; 3 | 4 | test.describe('Values Tests', () => { 5 | test('single value set to: 75', async ({ page }) => { 6 | await page.goto('/test/range-slider/values/single-value'); 7 | await page.waitForLoadState('networkidle'); 8 | const handle = page.getByRole('slider'); 9 | 10 | await expect(handle).toHaveAttribute('aria-valuenow', '75'); 11 | await expect(handle, 'to be positioned at 75%').toHaveCSS('translate', '750px'); 12 | }); 13 | 14 | test('multiple handles set to: [25, 125]', async ({ page }) => { 15 | await page.goto('/test/range-slider/values/multiple-values'); 16 | await page.waitForLoadState('networkidle'); 17 | const handles = page.getByRole('slider'); 18 | 19 | // Check count of handles 20 | await expect(handles).toHaveCount(2); 21 | 22 | // First handle: 25 23 | await expect(handles.nth(0)).toHaveAttribute('aria-valuenow', '25'); 24 | await expect(handles.nth(0), 'handle should be positioned at 25%').toHaveCSS('translate', '250px'); 25 | 26 | // Second handle: 125 27 | // should be constrained to max=100 28 | await expect(handles.nth(1)).toHaveAttribute('aria-valuenow', '100'); 29 | await expect(handles.nth(1), 'handle should be positioned at 100%').toHaveCSS('translate', '1000px'); 30 | }); 31 | 32 | test('seven handles set to: [10, 20, 30, 40, 60, 80, 90]', async ({ page }) => { 33 | await page.goto('/test/range-slider/values/seven-values'); 34 | await page.waitForLoadState('networkidle'); 35 | const handles = page.getByRole('slider'); 36 | 37 | const expectedValues = [10, 20, 30, 40, 60, 80, 90]; 38 | 39 | // Check count of handles 40 | await expect(handles).toHaveCount(7); 41 | 42 | // Check each handle's value 43 | for (let i = 0; i < expectedValues.length; i++) { 44 | await expect(handles.nth(i)).toHaveAttribute('aria-valuenow', expectedValues[i].toString()); 45 | await expect(handles.nth(i), `handle ${i} should be positioned at ${expectedValues[i]}%`).toHaveCSS( 46 | 'translate', 47 | `${expectedValues[i] * 10}px` 48 | ); 49 | } 50 | }); 51 | 52 | test('values outside default min/max are constrained: [-20, 120] -> [0, 100]', async ({ page }) => { 53 | await page.goto('/test/range-slider/values/constrained-values'); 54 | await page.waitForLoadState('networkidle'); 55 | const handles = page.getByRole('slider'); 56 | 57 | // Check count of handles 58 | await expect(handles).toHaveCount(2); 59 | 60 | // First handle should be constrained to min=0 61 | await expect(handles.nth(0)).toHaveAttribute('aria-valuenow', '0'); 62 | await expect(handles.nth(0), 'handle should be positioned at 0%').toHaveCSS('translate', '0px'); 63 | 64 | // Second handle should be constrained to max=100 65 | await expect(handles.nth(1)).toHaveAttribute('aria-valuenow', '100'); 66 | await expect(handles.nth(1), 'handle should be positioned at 100%').toHaveCSS('translate', '1000px'); 67 | }); 68 | 69 | test('when both value/values props provided, value[0] = value, value[1] = values[1]', async ({ page }) => { 70 | await page.goto('/test/range-slider/values/value-values'); 71 | await page.waitForLoadState('networkidle'); 72 | const handles = page.getByRole('slider'); 73 | 74 | // Should have two handles since both value and values props are provided 75 | await expect(handles).toHaveCount(2); 76 | 77 | // Handle 1 should be set to value (75) 78 | await expect(handles.nth(0)).toHaveAttribute('aria-valuenow', '75'); 79 | await expect(handles.nth(0), 'handle should be positioned at 75%').toHaveCSS('translate', '750px'); 80 | 81 | // Handle 2 should be set to values[1] (90) 82 | await expect(handles.nth(1)).toHaveAttribute('aria-valuenow', '90'); 83 | await expect(handles.nth(1), 'handle should be positioned at 90%').toHaveCSS('translate', '900px'); 84 | }); 85 | 86 | test('two-way binding works between slider and number input', async ({ page }) => { 87 | await page.goto('/test/range-slider/values/binding/single'); 88 | await page.waitForLoadState('networkidle'); 89 | const handle = page.getByRole('slider'); 90 | const input = page.getByLabel('Current value:'); 91 | const v = '55'; 92 | 93 | // Test input -> slider binding 94 | await input.focus(); 95 | await input.fill(v); 96 | await input.blur(); 97 | await expect(input).toHaveValue(v); 98 | await expect(handle).toHaveAttribute('aria-valuenow', v); 99 | await expect(handle, 'handle should be positioned at 55%').toHaveCSS('translate', '550px'); 100 | 101 | // Test slider -> input binding 102 | await handle.focus(); 103 | await page.keyboard.press('ArrowRight'); // Move to 56 104 | await expect(input).toHaveValue((parseInt(v) + 1).toString()); 105 | }); 106 | 107 | test('input values are constrained to min/max bounds', async ({ page }) => { 108 | await page.goto('/test/range-slider/values/binding/single'); 109 | await page.waitForLoadState('networkidle'); 110 | const handle = page.getByRole('slider'); 111 | const input = page.getByLabel('Current value:'); 112 | 113 | // Test value below minimum 114 | await input.focus(); 115 | await input.fill('-10'); 116 | await input.blur(); 117 | await expect(input).toHaveValue('0'); 118 | await expect(handle).toHaveAttribute('aria-valuenow', '0'); 119 | await expect(handle, 'handle should be positioned at 0%').toHaveCSS('translate', '0px'); 120 | 121 | // Test value above maximum 122 | await input.focus(); 123 | await input.fill('150'); 124 | await input.blur(); 125 | await expect(input).toHaveValue('100'); 126 | await expect(handle).toHaveAttribute('aria-valuenow', '100'); 127 | await expect(handle, 'handle should be positioned at 100%').toHaveCSS('translate', '1000px'); 128 | }); 129 | 130 | test('two-way binding works between slider and number inputs (multiple values)', async ({ page }) => { 131 | await page.goto('/test/range-slider/values/binding/multiple'); 132 | await page.waitForLoadState('networkidle'); 133 | const handles = page.getByRole('slider'); 134 | const input1 = page.getByLabel('First value:'); 135 | const input2 = page.getByLabel('Second value:'); 136 | 137 | // Test inputs -> slider binding 138 | // Update first handle 139 | await input1.focus(); 140 | await input1.fill('30'); 141 | await input1.blur(); 142 | await expect(input1).toHaveValue('30'); 143 | await expect(handles.nth(0)).toHaveAttribute('aria-valuenow', '30'); 144 | await expect(handles.nth(0), 'first handle should be positioned at 30%').toHaveCSS('translate', '300px'); 145 | 146 | // Update second handle 147 | await input2.focus(); 148 | await input2.fill('80'); 149 | await input2.blur(); 150 | await expect(input2).toHaveValue('80'); 151 | await expect(handles.nth(1)).toHaveAttribute('aria-valuenow', '80'); 152 | await expect(handles.nth(1), 'second handle should be positioned at 80%').toHaveCSS('translate', '800px'); 153 | 154 | // Test slider -> inputs binding 155 | // Move first handle 156 | await handles.nth(0).focus(); 157 | await page.keyboard.press('ArrowRight'); // Move to 31 158 | await expect(input1).toHaveValue('31'); 159 | 160 | // Move second handle 161 | await handles.nth(1).focus(); 162 | await page.keyboard.press('ArrowLeft'); // Move to 79 163 | await expect(input2).toHaveValue('79'); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /tests/playwright/helpers/tools.ts: -------------------------------------------------------------------------------- 1 | import type { Locator, Page } from '@playwright/test'; 2 | 3 | /** 4 | * Drag a handle to a specific position 5 | * @param page - The page object 6 | * @param slider - The slider locator 7 | * @param handle - The handle locator 8 | * @param pos - The position (0-1) to drag the handle to 9 | * @param vertical - Whether to use y-coordinates (true) or x-coordinates (false) 10 | */ 11 | export const dragHandleTo = async ( 12 | page: Page, 13 | slider: Locator, 14 | handle: Locator, 15 | pos: number, 16 | vertical: boolean = false 17 | ) => { 18 | await slider.scrollIntoViewIfNeeded(); 19 | 20 | const sbox = await slider.boundingBox(); 21 | if (!sbox) throw new Error('Could not get slider bounds'); 22 | 23 | await handle.focus(); 24 | const hbox = await handle.boundingBox(); 25 | if (!hbox) throw new Error('Could not get handle bounds'); 26 | const handleCenter = { x: hbox.x + hbox.width / 2, y: hbox.y + hbox.height / 2 }; 27 | 28 | // Move mouse to handle center and press down 29 | await page.mouse.move(handleCenter.x, handleCenter.y); 30 | await page.mouse.down(); 31 | 32 | // Calculate target position based on orientation 33 | const targetX = vertical ? sbox.x + sbox.width / 2 : sbox.x + sbox.width * pos; 34 | const targetY = vertical ? sbox.y + sbox.height * pos : sbox.y + sbox.height / 2; 35 | 36 | // Ensure we end exactly at target position 37 | await page.mouse.move(targetX, targetY, { steps: 25 }); 38 | await page.waitForTimeout(100); 39 | await page.mouse.up(); 40 | }; 41 | -------------------------------------------------------------------------------- /tests/playwright/helpers/utils.ts: -------------------------------------------------------------------------------- 1 | export const waitTime = 400; 2 | 3 | export const $ = (selector: string, root: Element | Document = document) => root.querySelector(selector); 4 | export const $$ = (selector: string, root: Element | Document = document) => root.querySelectorAll(selector); 5 | -------------------------------------------------------------------------------- /tests/reactjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-range-slider-react-test", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^18.1.0", 7 | "react-dom": "^18.1.0", 8 | "svelte-range-slider-pips": "file:../.." 9 | }, 10 | "type": "module", 11 | "scripts": { 12 | "dev": "react-scripts start", 13 | "start": "react-scripts start", 14 | "build": "react-scripts build", 15 | "test": "react-scripts test --env=jsdom", 16 | "eject": "react-scripts eject" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.3.4", 20 | "react-scripts": "latest" 21 | }, 22 | "browserslist": { 23 | "production": [ 24 | ">0.2%", 25 | "not dead", 26 | "not op_mini all" 27 | ], 28 | "development": [ 29 | "last 1 chrome version", 30 | "last 1 firefox version", 31 | "last 1 safari version" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/reactjs/public/index.html: -------------------------------------------------------------------------------- 1 |
    2 | -------------------------------------------------------------------------------- /tests/reactjs/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import RangeSlider from 'svelte-range-slider-pips'; 3 | // import type { ComponentProps } from 'svelte'; 4 | // import type { RangeSlider as RangeSliderType } from 'svelte-range-slider-pips'; 5 | 6 | export default function MyComponent() { 7 | const [values, setValues] = useState([-7, 7]); 8 | const MySlider = useRef(); 9 | const $node = useRef(); 10 | 11 | useEffect(() => { 12 | if (!MySlider.current) { 13 | MySlider.current = new RangeSlider({ 14 | target: $node.current, 15 | props: { 16 | values: values, 17 | pips: true, 18 | first: 'label', 19 | last: 'label', 20 | range: true, 21 | min: -10, 22 | max: 10 23 | } 24 | }); 25 | MySlider.current.$on('change', (e) => { 26 | setValues(e.detail.values); 27 | }); 28 | } 29 | }, []); 30 | 31 | function handleClick() { 32 | const newVals = values.map((v) => v + 10); 33 | setValues(newVals); 34 | MySlider.current.$set({ values: newVals }); 35 | } 36 | 37 | return ( 38 |
    39 |

    {values.join(',')}

    40 |
    41 | 42 |
    43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /tests/reactjs/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { StrictMode } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import App from './App.js'; 5 | 6 | const rootElement = document.getElementById('root'); 7 | const root = createRoot(rootElement); 8 | 9 | root.render( 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /tests/setupTest.js: -------------------------------------------------------------------------------- 1 | import * as matchers from '@testing-library/jest-dom/matchers'; 2 | import { expect, vi } from 'vitest'; 3 | 4 | expect.extend(matchers); 5 | -------------------------------------------------------------------------------- /tests/svelte4/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /tests/svelte4/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["svelte.svelte-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /tests/svelte4/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + Svelte 8 | 9 | 10 |
    11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/svelte4/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "bundler", 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | /** 7 | * svelte-preprocess cannot figure out whether you have 8 | * a value or a type, so tell TypeScript to enforce using 9 | * `import type` instead of `import` for Types. 10 | */ 11 | "verbatimModuleSyntax": true, 12 | "isolatedModules": true, 13 | "resolveJsonModule": true, 14 | /** 15 | * To have warnings / errors of the Svelte compiler at the 16 | * correct position, enable source maps by default. 17 | */ 18 | "sourceMap": true, 19 | "esModuleInterop": true, 20 | "skipLibCheck": true, 21 | /** 22 | * Typecheck JS in `.svelte` and `.js` files by default. 23 | * Disable this if you'd like to use dynamic types. 24 | */ 25 | "checkJs": true 26 | }, 27 | /** 28 | * Use global.d.ts instead of compilerOptions.types 29 | * to avoid limiting type declarations. 30 | */ 31 | "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"] 32 | } 33 | -------------------------------------------------------------------------------- /tests/svelte4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-test", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite dev --host", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "devDependencies": { 12 | "@sveltejs/vite-plugin-svelte": "^3.0.1", 13 | "svelte": "^4.2.8", 14 | "vite": "^5.0.8", 15 | "svelte-range-slider-pips": "file:../.." 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/svelte4/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/svelte4/src/App.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
    11 | 19 |

    Vite + Svelte

    20 | 21 |
    22 | 23 |
    24 | 25 | console.log('start')} 33 | on:stop={() => console.log('stop')} 34 | on:change={({ detail }) => console.log('change', detail)} 35 | /> 36 | 37 | {values} 38 |
    39 | 40 | 57 | -------------------------------------------------------------------------------- /tests/svelte4/src/app.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | a { 17 | font-weight: 500; 18 | color: #646cff; 19 | text-decoration: inherit; 20 | } 21 | a:hover { 22 | color: #535bf2; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | display: flex; 28 | place-items: center; 29 | min-width: 320px; 30 | min-height: 100vh; 31 | } 32 | 33 | h1 { 34 | font-size: 3.2em; 35 | line-height: 1.1; 36 | } 37 | 38 | .card { 39 | padding: 2em; 40 | } 41 | 42 | #app { 43 | max-width: 1280px; 44 | margin: 0 auto; 45 | padding: 2rem; 46 | text-align: center; 47 | } 48 | 49 | button { 50 | border-radius: 8px; 51 | border: 1px solid transparent; 52 | padding: 0.6em 1.2em; 53 | font-size: 1em; 54 | font-weight: 500; 55 | font-family: inherit; 56 | background-color: #1a1a1a; 57 | cursor: pointer; 58 | transition: border-color 0.25s; 59 | } 60 | button:hover { 61 | border-color: #646cff; 62 | } 63 | button:focus, 64 | button:focus-visible { 65 | outline: 4px auto -webkit-focus-ring-color; 66 | } 67 | 68 | @media (prefers-color-scheme: light) { 69 | :root { 70 | color: #213547; 71 | background-color: #ffffff; 72 | } 73 | a:hover { 74 | color: #747bff; 75 | } 76 | button { 77 | background-color: #f9f9f9; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/svelte4/src/assets/svelte.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/svelte4/src/lib/Counter.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /tests/svelte4/src/main.js: -------------------------------------------------------------------------------- 1 | import './app.css'; 2 | import App from './App.svelte'; 3 | 4 | const app = new App({ 5 | target: document.getElementById('app') 6 | }); 7 | 8 | export default app; 9 | -------------------------------------------------------------------------------- /tests/svelte4/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /tests/svelte4/svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 2 | 3 | export default { 4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: vitePreprocess() 7 | }; 8 | -------------------------------------------------------------------------------- /tests/svelte4/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import { svelte } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [svelte()] 7 | }); 8 | -------------------------------------------------------------------------------- /tests/svelte5/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | -------------------------------------------------------------------------------- /tests/svelte5/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /tests/svelte5/.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore files for PNPM, NPM and YARN 2 | pnpm-lock.yaml 3 | package-lock.json 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /tests/svelte5/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 8 | } 9 | -------------------------------------------------------------------------------- /tests/svelte5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte5", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev --host", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 11 | "lint": "prettier --check .", 12 | "format": "prettier --write ." 13 | }, 14 | "devDependencies": { 15 | "@sveltejs/adapter-auto": "^3.0.0", 16 | "@sveltejs/kit": "^2.0.0", 17 | "@sveltejs/vite-plugin-svelte": "^3.0.0", 18 | "prettier": "^3.1.1", 19 | "prettier-plugin-svelte": "^3.1.2", 20 | "svelte": "^5.0.0", 21 | "svelte-check": "^3.6.0", 22 | "svelte-range-slider-pips": "file:../..", 23 | "tslib": "^2.4.1", 24 | "typescript": "^5.0.0", 25 | "vite": "^5.0.3" 26 | }, 27 | "type": "module" 28 | } 29 | -------------------------------------------------------------------------------- /tests/svelte5/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /tests/svelte5/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
    %sveltekit.body%
    11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/svelte5/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | -------------------------------------------------------------------------------- /tests/svelte5/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /tests/svelte5/src/routes/regular/+page.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 |

    Without Runes

    10 | 11 | 12 | 13 | {value} 14 | console.log('start')} 21 | on:stop={() => console.log('stop')} 22 | on:change={({ detail }) => console.log('change', detail)} 23 | /> 24 | 25 | {values} 26 | 27 | -------------------------------------------------------------------------------- /tests/svelte5/src/routes/runes/+page.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 |

    With Runes

    10 | 11 | 12 | 13 | {value} 14 | console.log('start')} 21 | on:stop={() => console.log('stop')} 22 | on:change={({ detail }) => console.log('change', detail)} 23 | /> 24 | 25 | {values} 26 | 27 | -------------------------------------------------------------------------------- /tests/svelte5/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simeydotme/svelte-range-slider-pips/f0d144cf047d59381c204cd34ffdcac5a5b1896d/tests/svelte5/static/favicon.png -------------------------------------------------------------------------------- /tests/svelte5/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-auto'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 12 | // If your environment is not supported or you settled on a specific environment, switch out the adapter. 13 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 14 | adapter: adapter() 15 | } 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /tests/svelte5/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 15 | // 16 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 17 | // from the referenced tsconfig.json - TypeScript does not merge them in 18 | } 19 | -------------------------------------------------------------------------------- /tests/svelte5/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()] 6 | }); 7 | -------------------------------------------------------------------------------- /tests/vanilla/esm.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | test 7 | 8 | 35 | 36 | 37 |

    Testing loading JS (esm) script

    38 |
    39 |

    -7,7

    40 | 41 | 42 | -------------------------------------------------------------------------------- /tests/vanilla/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | test 7 | 8 | 9 | 10 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/vanilla/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vanilla-js-esm-test", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "Test of vanilla JS with ES modules", 6 | "main": "index.html", 7 | "scripts": { 8 | "start": "serve .", 9 | "dev": "serve ." 10 | }, 11 | "devDependencies": { 12 | "svelte-range-slider-pips": "file:../..", 13 | "serve": "^14.2.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/vanilla/umd.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | test 7 | 8 | 9 | 35 | 36 | 37 |

    Testing loading JS (umd) script

    38 |
    39 |

    -7,7

    40 | 41 | 42 | -------------------------------------------------------------------------------- /tests/vuejs/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | 30 | *.tsbuildinfo 31 | -------------------------------------------------------------------------------- /tests/vuejs/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /tests/vuejs/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tests/vuejs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
    11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/vuejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-range-slider-vue-test", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "run-p type-check \"build-only {@}\" --", 9 | "preview": "vite preview", 10 | "build-only": "vite build", 11 | "type-check": "vue-tsc --build --force" 12 | }, 13 | "dependencies": { 14 | "vue": "^3.4.15" 15 | }, 16 | "devDependencies": { 17 | "@tsconfig/node20": "^20.1.2", 18 | "@types/node": "^20.11.10", 19 | "@vitejs/plugin-vue": "^5.0.3", 20 | "@vue/tsconfig": "^0.5.1", 21 | "npm-run-all2": "^6.1.1", 22 | "typescript": "~5.3.0", 23 | "vite": "^5.0.11", 24 | "vue-tsc": "^1.8.27", 25 | "svelte-range-slider-pips": "file:../.." 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/vuejs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simeydotme/svelte-range-slider-pips/f0d144cf047d59381c204cd34ffdcac5a5b1896d/tests/vuejs/public/favicon.ico -------------------------------------------------------------------------------- /tests/vuejs/src/App.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 48 | 49 | 67 | -------------------------------------------------------------------------------- /tests/vuejs/src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | font-weight: normal; 59 | } 60 | 61 | body { 62 | min-height: 100vh; 63 | color: var(--color-text); 64 | background: var(--color-background); 65 | transition: 66 | color 0.5s, 67 | background-color 0.5s; 68 | line-height: 1.6; 69 | font-family: 70 | Inter, 71 | -apple-system, 72 | BlinkMacSystemFont, 73 | 'Segoe UI', 74 | Roboto, 75 | Oxygen, 76 | Ubuntu, 77 | Cantarell, 78 | 'Fira Sans', 79 | 'Droid Sans', 80 | 'Helvetica Neue', 81 | sans-serif; 82 | font-size: 15px; 83 | text-rendering: optimizeLegibility; 84 | -webkit-font-smoothing: antialiased; 85 | -moz-osx-font-smoothing: grayscale; 86 | } 87 | -------------------------------------------------------------------------------- /tests/vuejs/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/vuejs/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import './base.css'; 2 | 3 | #app { 4 | max-width: 1280px; 5 | margin: 0 auto; 6 | padding: 2rem; 7 | font-weight: normal; 8 | } 9 | 10 | a, 11 | .green { 12 | text-decoration: none; 13 | color: hsla(160, 100%, 37%, 1); 14 | transition: 0.4s; 15 | padding: 3px; 16 | } 17 | 18 | @media (hover: hover) { 19 | a:hover { 20 | background-color: hsla(160, 100%, 37%, 0.2); 21 | } 22 | } 23 | 24 | @media (min-width: 1024px) { 25 | body { 26 | display: flex; 27 | place-items: center; 28 | } 29 | 30 | #app { 31 | display: grid; 32 | gap: 2rem; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/vuejs/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 42 | -------------------------------------------------------------------------------- /tests/vuejs/src/main.ts: -------------------------------------------------------------------------------- 1 | import './assets/main.css'; 2 | 3 | import { createApp } from 'vue'; 4 | import App from './App.vue'; 5 | 6 | createApp(App).mount('#app'); 7 | -------------------------------------------------------------------------------- /tests/vuejs/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "exclude": ["src/**/__tests__/*"], 5 | "compilerOptions": { 6 | "composite": true, 7 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 8 | 9 | "baseUrl": ".", 10 | "paths": { 11 | "@/*": ["./src/*"] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/vuejs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tests/vuejs/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node20/tsconfig.json", 3 | "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "nightwatch.conf.*", "playwright.config.*"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "noEmit": true, 7 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 8 | 9 | "module": "ESNext", 10 | "moduleResolution": "Bundler", 11 | "types": ["node"] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/vuejs/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url'; 2 | 3 | import { defineConfig } from 'vite'; 4 | import vue from '@vitejs/plugin-vue'; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [vue()], 9 | resolve: { 10 | alias: { 11 | '@': fileURLToPath(new URL('./src', import.meta.url)) 12 | } 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": false, 11 | "strict": true, 12 | "module": "NodeNext", 13 | "moduleResolution": "NodeNext", 14 | "types": ["vitest/globals", "@testing-library/jest-dom"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import tailwindcss from '@tailwindcss/vite'; 2 | import { sveltekit } from '@sveltejs/kit/vite'; 3 | import { defineConfig } from 'vitest/config'; 4 | 5 | export default defineConfig({ 6 | plugins: [tailwindcss(), sveltekit()], 7 | test: { 8 | include: ['tests/vitest/*.{test,spec}.{js,ts}'], 9 | exclude: ['node_modules', 'dist', 'tests'], 10 | environment: 'jsdom', 11 | setupFiles: ['./tests/setupTest.js'] 12 | }, 13 | server: { 14 | fs: { 15 | allow: ['/dist'] 16 | } 17 | } 18 | }); 19 | --------------------------------------------------------------------------------