├── .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 |
{formatEventDetail(lastEvent.detail)}142 |
No events yet
145 | {/if} 146 | 147 |{formatEventDetail(event.detail)}154 |
No events recorded
159 | {/if} 160 |-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 |