The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .dumirc.ts
├── .editorconfig
├── .eslintrc.js
├── .fatherrc.js
├── .github
    ├── FUNDING.yml
    ├── dependabot.yml
    └── workflows
    │   ├── codeql.yml
    │   └── main.yml
├── .gitignore
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── assets
    └── index.less
├── docs
    ├── changelog.md
    ├── demo
    │   ├── debug.md
    │   ├── editable.md
    │   ├── handle.md
    │   ├── marks.md
    │   ├── mulitple.md
    │   ├── range.md
    │   ├── slider.md
    │   └── vertical.md
    ├── examples
    │   ├── components
    │   │   └── TooltipSlider.tsx
    │   ├── debug.tsx
    │   ├── editable.tsx
    │   ├── handle.tsx
    │   ├── marks.tsx
    │   ├── multiple.tsx
    │   ├── range.tsx
    │   ├── slider.tsx
    │   └── vertical.tsx
    └── index.md
├── index.js
├── jest.config.js
├── now.json
├── package.json
├── script
    └── update-content.js
├── src
    ├── Handles
    │   ├── Handle.tsx
    │   └── index.tsx
    ├── Marks
    │   ├── Mark.tsx
    │   └── index.tsx
    ├── Slider.tsx
    ├── Steps
    │   ├── Dot.tsx
    │   └── index.tsx
    ├── Tracks
    │   ├── Track.tsx
    │   └── index.tsx
    ├── context.ts
    ├── hooks
    │   ├── useDrag.ts
    │   ├── useOffset.ts
    │   └── useRange.ts
    ├── index.tsx
    ├── interface.ts
    └── util.ts
├── tests
    ├── Range.test.tsx
    ├── Slider.test.js
    ├── Tooltip.test.js
    ├── __mocks__
    │   └── rc-trigger.js
    ├── __snapshots__
    │   ├── Range.test.tsx.snap
    │   └── Slider.test.js.snap
    ├── common.test.js
    ├── marks.test.js
    └── setup.js
├── tsconfig.json
└── typings.d.ts


/.dumirc.ts:
--------------------------------------------------------------------------------
 1 | import { defineConfig } from 'dumi';
 2 | import path from 'path';
 3 | 
 4 | export default defineConfig({
 5 |   alias: {
 6 |     'rc-slider
#39;: path.resolve('src'),
 7 |     'rc-slider/es': path.resolve('src'),
 8 |   },
 9 |   mfsu: false,
10 |   favicons: ['https://avatars0.githubusercontent.com/u/9441414?s=200&v=4'],
11 |   themeConfig: {
12 |     name: 'Slider',
13 |     logo: 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4',
14 |   },
15 |   styles: [``],
16 | });
17 | 


--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
 1 | # http://editorconfig.org
 2 | root = true
 3 | 
 4 | [*]
 5 | indent_style = space
 6 | indent_size = 2
 7 | end_of_line = lf
 8 | charset = utf-8
 9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 | 
12 | [*.md]
13 | trim_trailing_whitespace = false
14 | 
15 | [Makefile]
16 | indent_style = tab
17 | 


--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
 1 | const base = require('@umijs/fabric/dist/eslint');
 2 | 
 3 | module.exports = {
 4 |   ...base,
 5 |   rules: {
 6 |     ...base.rules,
 7 |     'react/no-array-index-key': 0,
 8 |     'react/sort-comp': 0,
 9 |     '@typescript-eslint/no-explicit-any': 1,
10 |     '@typescript-eslint/no-empty-interface': 1,
11 |     '@typescript-eslint/no-inferrable-types': 0,
12 |     'react/no-find-dom-node': 1,
13 |     'react/require-default-props': 0,
14 |     'no-confusing-arrow': 0,
15 |     'import/no-named-as-default-member': 0,
16 |     'import/no-extraneous-dependencies': 0,
17 |     'jsx-a11y/label-has-for': 0,
18 |     'jsx-a11y/label-has-associated-control': 0,
19 |   },
20 | };


--------------------------------------------------------------------------------
/.fatherrc.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'father';
2 | 
3 | export default defineConfig({
4 |   plugins: ['@rc-component/father-plugin'],
5 | });


--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
 1 | # These are supported funding model platforms
 2 | 
 3 | github: ant-design # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
 4 | patreon: # Replace with a single Patreon username
 5 | open_collective: ant-design # Replace with a single Open Collective username
 6 | ko_fi: # Replace with a single Ko-fi username
 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
 9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 | 


--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
 1 | version: 2
 2 | updates:
 3 | - package-ecosystem: npm
 4 |   directory: "/"
 5 |   schedule:
 6 |     interval: daily
 7 |     time: "21:00"
 8 |   open-pull-requests-limit: 10
 9 |   ignore:
10 |   - dependency-name: "@types/react"
11 |     versions:
12 |     - 17.0.0
13 |     - 17.0.1
14 |     - 17.0.2
15 |     - 17.0.3
16 |   - dependency-name: "@types/react-dom"
17 |     versions:
18 |     - 17.0.0
19 |     - 17.0.1
20 |     - 17.0.2
21 |   - dependency-name: less
22 |     versions:
23 |     - 4.1.0
24 | 


--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
 1 | name: "CodeQL"
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: [ "master" ]
 6 |   pull_request:
 7 |     branches: [ "master" ]
 8 |   schedule:
 9 |     - cron: "17 10 * * 1"
10 | 
11 | jobs:
12 |   analyze:
13 |     name: Analyze
14 |     runs-on: ubuntu-latest
15 |     permissions:
16 |       actions: read
17 |       contents: read
18 |       security-events: write
19 | 
20 |     strategy:
21 |       fail-fast: false
22 |       matrix:
23 |         language: [ javascript ]
24 | 
25 |     steps:
26 |       - name: Checkout
27 |         uses: actions/checkout@v3
28 | 
29 |       - name: Initialize CodeQL
30 |         uses: github/codeql-action/init@v2
31 |         with:
32 |           languages: ${{ matrix.language }}
33 |           queries: +security-and-quality
34 | 
35 |       - name: Autobuild
36 |         uses: github/codeql-action/autobuild@v2
37 | 
38 |       - name: Perform CodeQL Analysis
39 |         uses: github/codeql-action/analyze@v2
40 |         with:
41 |           category: "/language:${{ matrix.language }}"
42 | 


--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: ✅ test
2 | on: [push, pull_request]
3 | jobs:
4 |   test:
5 |     uses: react-component/rc-test/.github/workflows/test.yml@main
6 |     secrets: inherit
7 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | *.iml
 2 | *.log
 3 | *.log.*
 4 | .idea/
 5 | .ipr
 6 | .iws
 7 | *~
 8 | ~*
 9 | *.diff
10 | *.patch
11 | *.bak
12 | .DS_Store
13 | Thumbs.db
14 | .project
15 | .*proj
16 | .svn/
17 | *.swp
18 | *.swo
19 | *.pyc
20 | *.pyo
21 | .build
22 | node_modules
23 | .cache
24 | dist
25 | assets/**/*.css
26 | build
27 | lib
28 | es
29 | /coverage
30 | yarn.lock
31 | package-lock.json
32 | .doc
33 | .storybook
34 | 
35 | # umi
36 | .umi
37 | .umi-production
38 | .umi-test
39 | .env.local
40 | .dumi/


--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 |   "singleQuote": true,
3 |   "trailingComma": "all",
4 |   "proseWrap": "never",
5 |   "printWidth": 100
6 | }
7 | 


--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
  1 | # Changelog
  2 | 
  3 | ## 9.7.1
  4 | 
  5 | `2020-12-15`
  6 | 
  7 | - feat: add dragableTrack. [#722](https://github.com/react-component/slider/pull/722)
  8 | 
  9 | ## 9.6.5
 10 | 
 11 | `2020-12-01`
 12 | 
 13 | - chore: export slider、range and handle props interface. [#718](https://github.com/react-component/slider/pull/718)
 14 | 
 15 | ## 9.6.4
 16 | 
 17 | `2020-11-21`
 18 | 
 19 | - fix: slider cannot drag to max value. [#714](https://github.com/react-component/slider/pull/714)
 20 | 
 21 | ## 9.6.3
 22 | 
 23 | `2020-11-17`
 24 | 
 25 | - fix: forcePopupAlign null. [930ad6d](https://github.com/react-component/slider/commit/930ad6d117850505775956f26e025487073615dc) [69dc592](https://github.com/react-component/slider/commit/69dc59270ca46ae2d3c4b5aa073d2bc75dfc5b16)
 26 | 
 27 | ## 9.6.2
 28 | 
 29 | `2020-11-10`
 30 | 
 31 | - fix: extra onChange when value out of range. [#711](https://github.com/react-component/slider/pull/711)
 32 | 
 33 | ## 9.6.1
 34 | 
 35 | `2020-10-31`
 36 | 
 37 | - fix: update getLowerBound and getUpperBound to check startPoint prop. [#683](https://github.com/react-component/slider/pull/683)
 38 | 
 39 | ## 9.6.0
 40 | 
 41 | `2020-10-30`
 42 | 
 43 | - fix: keep tooltip align with handle when dragging. [#696](https://github.com/react-component/slider/pull/696)
 44 | 
 45 | ---
 46 | 
 47 | Middle check in [releases](https://github.com/react-component/slider/releases).
 48 | 
 49 | ---
 50 | 
 51 | ## 9.2.0
 52 | 
 53 | [Feature] createSliderWithTooltip support getTooltipContainer
 54 | 
 55 | ## 9.1.0
 56 | 
 57 | [Feature] Support `startPoint` prop.
 58 | 
 59 | ## 8.7.0
 60 | 
 61 | [Feature] Supprot `reverse` prop.
 62 | 
 63 | ## 8.6.0
 64 | 
 65 | [Feature] Allow tabIndex to be set explicitly on Handle. [#381](https://github.com/react-component/slider/pull/381)
 66 | 
 67 | ## 8.5.0
 68 | 
 69 | [Feature] Add focus() blur() and autoFocus.
 70 | 
 71 | ## 8.4.0 / 2017-11-09
 72 | 
 73 | Support React 16.
 74 | 
 75 | ## 8.3.0 / 2017-07-28
 76 | 
 77 | [Feature] Support keyboard accessibility.[#282](https://github.com/react-component/slider/pull/282)
 78 | 
 79 | ## 8.2.0 / 2017-07-04
 80 | 
 81 | [Feature] Support custom dot style with `dotStyle` & `activeDotStyle` api.[#292](https://github.com/react-component/slider/pull/292)
 82 | 
 83 | ## 8.1.0 / 2017-06-09
 84 | 
 85 | [Feature] rc-slider support custom style. [#281](https://github.com/react-component/slider/pull/281)
 86 | 
 87 | ## 8.0.0 / 2017-05-31
 88 | 
 89 | [Feature] rc-slider support aria. [#260](https://github.com/react-component/slider/pull/260/)
 90 | 
 91 | ## 6.0.0 / 2017-01-25
 92 | 
 93 | [Breaking Change] Re-design and refactor, almost a new UI component.
 94 | 
 95 | ## 5.0.0 / 2016-09-12
 96 | 
 97 | [#147](https://github.com/react-component/slider/issues/147) fix style conflicts with rc-tooltip [@benjycui](https://github.com/benjycui)
 98 | [#145](https://github.com/react-component/slider/pull/145) fix `onChange` will be triggered while mousemove [@Fuzzyma](https://github.com/Fuzzyma)
 99 | 
100 | ## 4.0.0 / 2016-08-12
101 | 
102 | [#133](https://github.com/react-component/slider/pull/133) support multi-range ([@sosz](https://github.com/sosz))
103 | 
104 | ## 3.6.0 / 2016-04-01
105 | 
106 | [#18](https://github.com/react-component/slider/issues/18) add `vertical` props ([@wnlee](https://github.com/WNLee))
107 | 
108 | ...
109 | 
110 | ## 1.2.5 / 2015-07-13
111 | 
112 | [#8](https://github.com/react-component/slider/issues/8) add `isIncluded` props   ([@simaQ](https://github.com/simaQ))
113 | 
114 | [#7](https://github.com/react-component/slider/issues/7) add tooltip for handler when slider has no `marks` props   ([@simaQ](https://github.com/simaQ))
115 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | The MIT License (MIT)
 2 | Copyright (c) 2015-present Alipay.com, https://www.alipay.com/
 3 | 
 4 | Permission is hereby granted, free of charge, to any person obtaining a copy
 5 | of this software and associated documentation files (the "Software"), to deal
 6 | in the Software without restriction, including without limitation the rights
 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 8 | copies of the Software, and to permit persons to whom the Software is
 9 | furnished to do so, subject to the following conditions:
10 | 
11 | The above copyright notice and this permission notice shall be included in
12 | all copies or substantial portions of the Software.
13 | 
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
15 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | # rc-slider
  2 | 
  3 | Slider UI component for React
  4 | 
  5 | [![NPM version][npm-image]][npm-url]
  6 | [![npm download][download-image]][download-url]
  7 | [![build status][github-actions-image]][github-actions-url]
  8 | [![Codecov][codecov-image]][codecov-url]
  9 | [![bundle size][bundlephobia-image]][bundlephobia-url]
 10 | [![dumi][dumi-image]][dumi-url]
 11 | 
 12 | [npm-image]: http://img.shields.io/npm/v/rc-slider.svg?style=flat-square
 13 | [npm-url]: http://npmjs.org/package/rc-slider
 14 | [travis-image]: https://img.shields.io/travis/react-component/slider/master?style=flat-square
 15 | [travis-url]: https://travis-ci.com/react-component/slider
 16 | [github-actions-image]: https://github.com/react-component/slider/workflows/CI/badge.svg
 17 | [github-actions-url]: https://github.com/react-component/slider/actions
 18 | [codecov-image]: https://img.shields.io/codecov/c/github/react-component/slider/master.svg?style=flat-square
 19 | [codecov-url]: https://app.codecov.io/gh/react-component/slider
 20 | [david-url]: https://david-dm.org/react-component/slider
 21 | [david-image]: https://david-dm.org/react-component/slider/status.svg?style=flat-square
 22 | [david-dev-url]: https://david-dm.org/react-component/slider?type=dev
 23 | [david-dev-image]: https://david-dm.org/react-component/slider/dev-status.svg?style=flat-square
 24 | [download-image]: https://img.shields.io/npm/dm/rc-slider.svg?style=flat-square
 25 | [download-url]: https://npmjs.org/package/rc-slider
 26 | [bundlephobia-url]: https://bundlephobia.com/package/rc-slider
 27 | [bundlephobia-image]: https://badgen.net/bundlephobia/minzip/rc-slider
 28 | [dumi-url]: https://github.com/umijs/dumi
 29 | [dumi-image]: https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square
 30 | ## Install
 31 | 
 32 | [![rc-slider](https://nodei.co/npm/rc-slider.png)](https://npmjs.org/package/rc-slider)
 33 | 
 34 | ## Example
 35 | 
 36 | `npm start` and then go to http://localhost:8000
 37 | 
 38 | Online examples: https://slider.react-component.now.sh/
 39 | 
 40 | ## Usage
 41 | 
 42 | ## Slider
 43 | ```js
 44 | import Slider from 'rc-slider';
 45 | import 'rc-slider/assets/index.css';
 46 | 
 47 | export default () => (
 48 |   <>
 49 |     <Slider />
 50 |   </>
 51 | );
 52 | ```
 53 | 
 54 | ## Range
 55 | Please refer to [#825](https://github.com/react-component/slider/issues/825) for information regarding usage of `Range`.
 56 | An example:
 57 | ```js
 58 | import Slider, { Range } from 'rc-slider';
 59 | import 'rc-slider/assets/index.css';
 60 | 
 61 | export default () => (
 62 |   <>
 63 |     <Slider range />
 64 |   </>
 65 | );
 66 | ```
 67 | 
 68 | ## Compatibility
 69 | 
 70 | | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/electron/electron_48x48.png" alt="Electron" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Electron |
 71 | | --- | --- | --- | --- | --- |
 72 | | IE11, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
 73 | 
 74 | ## API
 75 | 
 76 | ### createSliderWithTooltip(Slider | Range) => React.Component
 77 | 
 78 | An extension to make Slider or Range support Tooltip on handle.
 79 | 
 80 | ```js
 81 | const Slider = require('rc-slider');
 82 | const createSliderWithTooltip = Slider.createSliderWithTooltip;
 83 | const Range = createSliderWithTooltip(Slider.Range);
 84 | ```
 85 | 
 86 | [Online demo](http://react-component.github.io/slider/?path=/story/rc-slider--handle)
 87 | 
 88 | After Range or Slider was wrapped by createSliderWithTooltip, it will have the following props:
 89 | 
 90 | | Name         | Type    | Default | Description |
 91 | | ------------ | ------- | ------- | ----------- |
 92 | | tipFormatter | (value: number): React.ReactNode | `value => value` | A function to format tooltip's overlay |
 93 | | tipProps | Object | `{` <br>`placement: 'top',` <br> ` prefixCls: 'rc-slider-tooltip',` <br> `overlay: tipFormatter(value)` <br> `}` | A function to format tooltip's overlay |
 94 | 
 95 | ### Common API
 96 | 
 97 | The following APIs are shared by Slider and Range.
 98 | 
 99 | | Name         | Type    | Default | Description |
100 | | ------------ | ------- | ------- | ----------- |
101 | | className | string | `''` | Additional CSS class for the root DOM node |
102 | | min | number | `0` | The minimum value of the slider |
103 | | max | number | `100` | The maximum value of the slider |
104 | | id        | string  | `''`    | Unique identifier for the component, used for accessibility |
105 | | marks | `{number: ReactNode}` or`{number: { style, label }}` | `{}` | Marks on the slider. The key determines the position, and the value determines what will show. If you want to set the style of a specific mark point, the value should be an object which contains `style` and `label` properties. |
106 | | step | number or `null` | `1` | Value to be added or subtracted on each step the slider makes. Must be greater than zero, and `max` - `min` should be evenly divisible by the step value. <br /> When `marks` is not an empty object, `step` can be set to `null`, to make `marks` as steps. |
107 | | vertical | boolean | `false` | If vertical is `true`, the slider will be vertical. |
108 | | handle | (props) => React.ReactNode | | A handle generator which could be used to customized handle. |
109 | | included | boolean | `true` | If the value is `true`, it means a continuous value interval, otherwise, it is a independent value. |
110 | | reverse | boolean | `false` | If the value is `true`, it means the component is rendered reverse. |
111 | | disabled | boolean | `false` | If `true`, handles can't be moved. |
112 | | keyboard | boolean | `true` | Support using keyboard to move handlers. |
113 | | dots | boolean | `false` | When the `step` value is greater than 1, you can set the `dots` to  `true` if you want to render the slider with dots. |
114 | | onBeforeChange | Function | NOOP | `onBeforeChange` will be triggered when `ontouchstart` or `onmousedown` is triggered. |
115 | | onChange | Function | NOOP | `onChange` will be triggered while the value of Slider changing. |
116 | | onChangeComplete | Function | NOOP | `onChangeComplete` will be triggered when `ontouchend` or `onmouseup` is triggered. |
117 | | minimumTrackStyle | Object |  | please use  `trackStyle` instead. (`only used for slider, just for compatibility , will be deprecate at rc-slider@9.x `) |
118 | | maximumTrackStyle | Object |  | please use  `railStyle` instead (`only used for slider, just for compatibility , will be deprecate at rc-slider@9.x`) |
119 | | handleStyle | Array[Object] \| Object | `[{}]` | The style used for handle. (`both for slider(`Object`) and range(`Array of Object`), the array will be used for multi handle following element order`) |
120 | | trackStyle | Array[Object] \| Object | `[{}]` | The style used for track. (`both for slider(`Object`) and range(`Array of Object`), the array will be used for multi track following element order`)|
121 | | railStyle | Object | `{}` | The style used for the track base color.  |
122 | | dotStyle | Object \| (dotValue) => Object | `{}` | The style used for the dots. |
123 | | activeDotStyle | Object \| (dotValue) => Object | `{}` | The style used for the active dots. |
124 | 
125 | ### Slider
126 | 
127 | | Name         | Type    | Default | Description |
128 | | ------------ | ------- | ------- | ----------- |
129 | | defaultValue | number | `0` | Set initial value of slider. |
130 | | value | number | - | Set current value of slider. |
131 | | startPoint | number | `undefined` | Track starts from this value. If `undefined`, `min` is used. |
132 | | tabIndex | number | `0` | Set the tabIndex of the slider handle. |
133 | | ariaLabelForHandle | string | - | Set the `aria-label` attribute on the slider handle.  |
134 | | ariaLabelledByForHandle | string | - | Set the `aria-labelledby` attribute on the slider handle. |
135 | | ariaRequired | boolean | - | Set the `aria-required` attribute on the slider handle. |
136 | | ariaValueTextFormatterForHandle | (value) => string | - | A function to set the `aria-valuetext` attribute on the slider handle. It receives the current value of the slider and returns a formatted string describing the value. See [WAI-ARIA Authoring Practices 1.1](https://www.w3.org/TR/wai-aria-practices-1.1/#slider) for more information. |
137 | 
138 | ### Range
139 | 
140 | | Name         | Type    | Default | Description |
141 | | ------------ | ------- | ------- | ----------- |
142 | | defaultValue | `number[]` | `[0, 0]` | Set initial positions of handles. |
143 | | value | `number[]` | | Set current positions of handles. |
144 | | tabIndex | number[] | `[0, 0]` | Set the tabIndex of each handle. |
145 | | ariaLabelGroupForHandles | Array[string] | - | Set the `aria-label` attribute on each handle. |
146 | | ariaLabelledByGroupForHandles | Array[string] | - | Set the `aria-labelledby` attribute on each handle. |
147 | | ariaValueTextFormatterGroupForHandles | Array[(value) => string] | - | A function to set the `aria-valuetext` attribute on each handle. It receives the current value of the slider and returns a formatted string describing the value. See [WAI-ARIA Authoring Practices 1.1](https://www.w3.org/TR/wai-aria-practices-1.1/#slider) for more information. |
148 | | count | number | `1` | Determine how many ranges to render, and multiple handles will be rendered (number + 1). |
149 | | allowCross | boolean | `true` | `allowCross` could be set as `true` to allow those handles to cross. |
150 | | pushable | boolean or number | `false` | `pushable` could be set as `true` to allow pushing of surrounding handles when moving a handle. When set to a number, the number will be the minimum ensured distance between handles. Example: ![](http://i.giphy.com/l46Cs36c9HrHMExoc.gif) |
151 | | draggableTrack | boolean | `false` | Open the track drag. open after click on the track will be invalid. |
152 | 
153 | ### SliderTooltip
154 | 
155 | The Tooltip Component that keep following with content.
156 | 
157 | ## Development
158 | 
159 | ```
160 | npm install
161 | npm start
162 | ```
163 | 
164 | ## Test Case
165 | 
166 | `npm run test`
167 | 
168 | ## Coverage
169 | 
170 | `npm run coverage`
171 | ## License
172 | 
173 | `rc-slider` is released under the MIT license.
174 | 


--------------------------------------------------------------------------------
/assets/index.less:
--------------------------------------------------------------------------------
  1 | @prefixClass: rc-slider;
  2 | 
  3 | @disabledColor: #ccc;
  4 | @border-radius-base: 6px;
  5 | @primary-color: #2db7f5;
  6 | @tooltip-color: #fff;
  7 | @tooltip-bg: tint(#666, 4%);
  8 | @tooltip-arrow-width: 4px;
  9 | @tooltip-distance: @tooltip-arrow-width+4;
 10 | @tooltip-arrow-color: @tooltip-bg;
 11 | @ease-out-quint: cubic-bezier(0.23, 1, 0.32, 1);
 12 | @ease-in-quint: cubic-bezier(0.755, 0.05, 0.855, 0.06);
 13 | 
 14 | .borderBox() {
 15 |   box-sizing: border-box;
 16 |   -webkit-tap-highlight-color: rgba(0, 0, 0, 0); //  remove tap highlight color for mobile safari
 17 | 
 18 |   * {
 19 |     box-sizing: border-box;
 20 |     -webkit-tap-highlight-color: rgba(0, 0, 0, 0); //  remove tap highlight color for mobile safari
 21 |   }
 22 | }
 23 | 
 24 | .@{prefixClass} {
 25 |   position: relative;
 26 |   width: 100%;
 27 |   height: 14px;
 28 |   padding: 5px 0;
 29 |   border-radius: @border-radius-base;
 30 |   touch-action: none;
 31 |   .borderBox();
 32 | 
 33 |   &-rail {
 34 |     position: absolute;
 35 |     width: 100%;
 36 |     height: 4px;
 37 |     background-color: #e9e9e9;
 38 |     border-radius: @border-radius-base;
 39 |   }
 40 | 
 41 |   &-track,
 42 |   &-tracks {
 43 |     position: absolute;
 44 |     height: 4px;
 45 |     background-color: tint(@primary-color, 60%);
 46 |     border-radius: @border-radius-base;
 47 |   }
 48 | 
 49 |   &-track-draggable {
 50 |     z-index: 1;
 51 |     box-sizing: content-box;
 52 |     background-clip: content-box;
 53 |     border-top: 5px solid rgba(0, 0, 0, 0);
 54 |     border-bottom: 5px solid rgba(0, 0, 0, 0);
 55 |     transform: translateY(-5px);
 56 |   }
 57 | 
 58 |   &-handle {
 59 |     position: absolute;
 60 |     z-index: 1;
 61 |     width: 14px;
 62 |     height: 14px;
 63 |     margin-top: -5px;
 64 |     background-color: #fff;
 65 |     border: solid 2px tint(@primary-color, 50%);
 66 |     border-radius: 50%;
 67 |     cursor: pointer;
 68 |     cursor: -webkit-grab;
 69 |     cursor: grab;
 70 |     opacity: 0.8;
 71 |     user-select: none;
 72 |     touch-action: pan-x;
 73 | 
 74 |     &-dragging&-dragging&-dragging {
 75 |       border-color: tint(@primary-color, 20%);
 76 |       box-shadow: 0 0 0 5px tint(@primary-color, 50%);
 77 | 
 78 |       &-delete {
 79 |         opacity: 0;
 80 |       }
 81 |     }
 82 | 
 83 |     &:focus {
 84 |       outline: none;
 85 |       box-shadow: none;
 86 |     }
 87 | 
 88 |     &:focus-visible {
 89 |       border-color: @primary-color;
 90 |       box-shadow: 0 0 0 3px tint(@primary-color, 50%);
 91 |     }
 92 | 
 93 |     &-click-focused:focus {
 94 |       border-color: tint(@primary-color, 50%);
 95 |       box-shadow: unset;
 96 |     }
 97 | 
 98 |     &:hover {
 99 |       border-color: tint(@primary-color, 20%);
100 |     }
101 | 
102 |     &:active {
103 |       border-color: tint(@primary-color, 20%);
104 |       box-shadow: 0 0 5px tint(@primary-color, 20%);
105 |       cursor: -webkit-grabbing;
106 |       cursor: grabbing;
107 |     }
108 |   }
109 | 
110 |   &-mark {
111 |     position: absolute;
112 |     top: 18px;
113 |     left: 0;
114 |     width: 100%;
115 |     font-size: 12px;
116 |   }
117 | 
118 |   &-mark-text {
119 |     position: absolute;
120 |     display: inline-block;
121 |     color: #999;
122 |     text-align: center;
123 |     vertical-align: middle;
124 |     cursor: pointer;
125 | 
126 |     &-active {
127 |       color: #666;
128 |     }
129 |   }
130 | 
131 |   &-step {
132 |     position: absolute;
133 |     width: 100%;
134 |     height: 4px;
135 |     background: transparent;
136 |   }
137 | 
138 |   &-dot {
139 |     position: absolute;
140 |     bottom: -2px;
141 |     width: 8px;
142 |     height: 8px;
143 |     vertical-align: middle;
144 |     background-color: #fff;
145 |     border: 2px solid #e9e9e9;
146 |     border-radius: 50%;
147 |     cursor: pointer;
148 |     &-active {
149 |       border-color: tint(@primary-color, 50%);
150 |     }
151 |     &-reverse {
152 |       margin-right: -4px;
153 |     }
154 |   }
155 | 
156 |   &-disabled {
157 |     background-color: #e9e9e9;
158 | 
159 |     .@{prefixClass}-track {
160 |       background-color: @disabledColor;
161 |     }
162 | 
163 |     .@{prefixClass}-handle,
164 |     .@{prefixClass}-dot {
165 |       background-color: #fff;
166 |       border-color: @disabledColor;
167 |       box-shadow: none;
168 |       cursor: not-allowed;
169 |     }
170 | 
171 |     .@{prefixClass}-mark-text,
172 |     .@{prefixClass}-dot {
173 |       cursor: not-allowed !important;
174 |     }
175 |   }
176 | }
177 | 
178 | .@{prefixClass}-vertical {
179 |   width: 14px;
180 |   height: 100%;
181 |   padding: 0 5px;
182 | 
183 |   .@{prefixClass} {
184 |     &-rail {
185 |       width: 4px;
186 |       height: 100%;
187 |     }
188 | 
189 |     &-track {
190 |       bottom: 0;
191 |       left: 5px;
192 |       width: 4px;
193 |     }
194 | 
195 |     &-track-draggable {
196 |       border-top: 0;
197 |       border-right: 5px solid rgba(0, 0, 0, 0);
198 |       border-bottom: 0;
199 |       border-left: 5px solid rgba(0, 0, 0, 0);
200 |       transform: translateX(-5px);
201 |     }
202 | 
203 |     &-handle {
204 |       position: absolute;
205 |       z-index: 1;
206 |       margin-top: 0;
207 |       margin-left: -5px;
208 |       touch-action: pan-y;
209 |     }
210 | 
211 |     &-mark {
212 |       top: 0;
213 |       left: 18px;
214 |       height: 100%;
215 |     }
216 | 
217 |     &-step {
218 |       width: 4px;
219 |       height: 100%;
220 |     }
221 | 
222 |     &-dot {
223 |       margin-left: -2px;
224 |     }
225 |   }
226 | }
227 | 
228 | .motion-common() {
229 |   display: block !important;
230 |   animation-duration: 0.3s;
231 |   animation-fill-mode: both;
232 | }
233 | 
234 | .make-motion(@className, @keyframeName) {
235 |   .@{className}-enter,
236 |   .@{className}-appear {
237 |     .motion-common();
238 |     animation-play-state: paused;
239 |   }
240 |   .@{className}-leave {
241 |     .motion-common();
242 |     animation-play-state: paused;
243 |   }
244 |   .@{className}-enter.@{className}-enter-active,
245 |   .@{className}-appear.@{className}-appear-active {
246 |     animation-name: ~'@{keyframeName}In';
247 |     animation-play-state: running;
248 |   }
249 |   .@{className}-leave.@{className}-leave-active {
250 |     animation-name: ~'@{keyframeName}Out';
251 |     animation-play-state: running;
252 |   }
253 | }
254 | .zoom-motion(@className, @keyframeName) {
255 |   .make-motion(@className, @keyframeName);
256 |   .@{className}-enter,
257 |   .@{className}-appear {
258 |     transform: scale(0, 0); // need this by yiminghe
259 |     animation-timing-function: @ease-out-quint;
260 |   }
261 |   .@{className}-leave {
262 |     animation-timing-function: @ease-in-quint;
263 |   }
264 | }
265 | .zoom-motion(rc-slider-tooltip-zoom-down, rcSliderTooltipZoomDown);
266 | 
267 | @keyframes rcSliderTooltipZoomDownIn {
268 |   0% {
269 |     transform: scale(0, 0);
270 |     transform-origin: 50% 100%;
271 |     opacity: 0;
272 |   }
273 |   100% {
274 |     transform: scale(1, 1);
275 |     transform-origin: 50% 100%;
276 |   }
277 | }
278 | 
279 | @keyframes rcSliderTooltipZoomDownOut {
280 |   0% {
281 |     transform: scale(1, 1);
282 |     transform-origin: 50% 100%;
283 |   }
284 |   100% {
285 |     transform: scale(0, 0);
286 |     transform-origin: 50% 100%;
287 |     opacity: 0;
288 |   }
289 | }
290 | 
291 | .@{prefixClass}-tooltip {
292 |   position: absolute;
293 |   top: -9999px;
294 |   left: -9999px;
295 |   visibility: visible;
296 | 
297 |   .borderBox();
298 | 
299 |   &-hidden {
300 |     display: none;
301 |   }
302 | 
303 |   &-placement-top {
304 |     padding: @tooltip-arrow-width 0 @tooltip-distance 0;
305 |   }
306 | 
307 |   &-inner {
308 |     min-width: 24px;
309 |     height: 24px;
310 |     padding: 6px 2px;
311 |     color: @tooltip-color;
312 |     font-size: 12px;
313 |     line-height: 1;
314 |     text-align: center;
315 |     text-decoration: none;
316 |     background-color: @tooltip-bg;
317 |     border-radius: @border-radius-base;
318 |     box-shadow: 0 0 4px #d9d9d9;
319 |   }
320 | 
321 |   &-arrow {
322 |     position: absolute;
323 |     width: 0;
324 |     height: 0;
325 |     border-color: transparent;
326 |     border-style: solid;
327 |   }
328 | 
329 |   &-placement-top &-arrow {
330 |     bottom: @tooltip-distance - @tooltip-arrow-width;
331 |     left: 50%;
332 |     margin-left: -@tooltip-arrow-width;
333 |     border-width: @tooltip-arrow-width @tooltip-arrow-width 0;
334 |     border-top-color: @tooltip-arrow-color;
335 |   }
336 | }
337 | 


--------------------------------------------------------------------------------
/docs/changelog.md:
--------------------------------------------------------------------------------
1 | <embed src="../CHANGELOG.md"></embed>


--------------------------------------------------------------------------------
/docs/demo/debug.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Debug
3 | nav:
4 |   title: Demo
5 |   path: /demo
6 | ---
7 | 
8 | <code src="../examples/debug.tsx"></code>
9 | 


--------------------------------------------------------------------------------
/docs/demo/editable.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Editable
3 | nav:
4 |   title: Demo
5 |   path: /demo
6 | ---
7 | 
8 | <code src="../examples/editable.tsx"></code>
9 | 


--------------------------------------------------------------------------------
/docs/demo/handle.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Handle
3 | nav:
4 |   title: Demo
5 |   path: /demo
6 | ---
7 | 
8 | <code src="../examples/handle.tsx"></code>
9 | 


--------------------------------------------------------------------------------
/docs/demo/marks.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Marks
3 | nav:
4 |   title: Demo
5 |   path: /demo
6 | ---
7 | 
8 | <code src="../examples/marks.tsx"></code>
9 | 


--------------------------------------------------------------------------------
/docs/demo/mulitple.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Multiple
3 | nav:
4 |   title: Demo
5 |   path: /demo
6 | ---
7 | 
8 | <code src="../examples/multiple.tsx"></code>
9 | 


--------------------------------------------------------------------------------
/docs/demo/range.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Range
3 | nav:
4 |   title: Demo
5 |   path: /demo
6 | ---
7 | 
8 | <code src="../examples/range.tsx"></code>
9 | 


--------------------------------------------------------------------------------
/docs/demo/slider.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Slider
3 | nav:
4 |   title: Demo
5 |   path: /demo
6 | ---
7 | 
8 | <code src="../examples/slider.tsx"></code>
9 | 


--------------------------------------------------------------------------------
/docs/demo/vertical.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Vertical
3 | nav:
4 |   title: Demo
5 |   path: /demo
6 | ---
7 | 
8 | <code src="../examples/vertical.tsx"></code>
9 | 


--------------------------------------------------------------------------------
/docs/examples/components/TooltipSlider.tsx:
--------------------------------------------------------------------------------
 1 | import type { SliderProps } from 'rc-slider';
 2 | import Slider from 'rc-slider';
 3 | import type { TooltipRef } from 'rc-tooltip';
 4 | import Tooltip from 'rc-tooltip';
 5 | import 'rc-tooltip/assets/bootstrap.css';
 6 | import raf from 'rc-util/lib/raf';
 7 | import * as React from 'react';
 8 | 
 9 | interface HandleTooltipProps {
10 |   value: number;
11 |   children: React.ReactElement;
12 |   visible: boolean;
13 |   tipFormatter?: (value: number) => React.ReactNode;
14 | }
15 | 
16 | const HandleTooltip: React.FC<HandleTooltipProps> = (props) => {
17 |   const { value, children, visible, tipFormatter = (val) => `${val} %`, ...restProps } = props;
18 | 
19 |   const tooltipRef = React.useRef<TooltipRef>();
20 |   const rafRef = React.useRef<number | null>(null);
21 | 
22 |   function cancelKeepAlign() {
23 |     raf.cancel(rafRef.current!);
24 |   }
25 | 
26 |   function keepAlign() {
27 |     rafRef.current = raf(() => {
28 |       tooltipRef.current?.forceAlign();
29 |     });
30 |   }
31 | 
32 |   React.useEffect(() => {
33 |     if (visible) {
34 |       keepAlign();
35 |     } else {
36 |       cancelKeepAlign();
37 |     }
38 | 
39 |     return cancelKeepAlign;
40 |   }, [value, visible]);
41 | 
42 |   return (
43 |     <Tooltip
44 |       placement="top"
45 |       overlay={tipFormatter(value)}
46 |       overlayInnerStyle={{ minHeight: 'auto' }}
47 |       ref={tooltipRef}
48 |       visible={visible}
49 |       {...restProps}
50 |     >
51 |       {children}
52 |     </Tooltip>
53 |   );
54 | };
55 | 
56 | export const handleRender: SliderProps['handleRender'] = (node, props) => (
57 |   <HandleTooltip value={props.value} visible={props.dragging}>
58 |     {node}
59 |   </HandleTooltip>
60 | );
61 | 
62 | interface TooltipSliderProps extends SliderProps {
63 |   tipFormatter?: (value: number) => React.ReactNode;
64 |   tipProps?: any;
65 | }
66 | 
67 | const TooltipSlider: React.FC<TooltipSliderProps> = ({ tipFormatter, tipProps, ...props }) => {
68 |   const tipHandleRender: SliderProps['handleRender'] = (node, handleProps) => (
69 |     <HandleTooltip
70 |       value={handleProps.value}
71 |       visible={handleProps.dragging}
72 |       tipFormatter={tipFormatter}
73 |       {...tipProps}
74 |     >
75 |       {node}
76 |     </HandleTooltip>
77 |   );
78 | 
79 |   return <Slider {...props} handleRender={tipHandleRender} />;
80 | };
81 | 
82 | export default TooltipSlider;
83 | 


--------------------------------------------------------------------------------
/docs/examples/debug.tsx:
--------------------------------------------------------------------------------
 1 | import Slider from 'rc-slider';
 2 | import React from 'react';
 3 | import '../../assets/index.less';
 4 | 
 5 | export default () => {
 6 |   const [disabled, setDisabled] = React.useState(false);
 7 |   const [range, setRange] = React.useState(false);
 8 |   const [reverse, setReverse] = React.useState(false);
 9 |   const [vertical, setVertical] = React.useState(false);
10 | 
11 |   return (
12 |     <div style={{ transform: 'scale(1.5)', transformOrigin: 'top left' }}>
13 |       <div>
14 |         <label>
15 |           <input type="checkbox" checked={disabled} onChange={() => setDisabled(!disabled)} />
16 |           Disabled
17 |         </label>
18 |         <label>
19 |           <input type="checkbox" checked={range} onChange={() => setRange(!range)} />
20 |           Range
21 |         </label>
22 |         <label>
23 |           <input type="checkbox" checked={reverse} onChange={() => setReverse(!reverse)} />
24 |           Reverse
25 |         </label>
26 |         <label>
27 |           <input type="checkbox" checked={vertical} onChange={() => setVertical(!vertical)} />
28 |           Vertical
29 |         </label>
30 |       </div>
31 | 
32 |       <div style={{ height: 300, width: 600 }}>
33 |         <Slider
34 |           onChange={(nextValues) => {
35 |             console.log('Change:', nextValues);
36 |           }}
37 |           onChangeComplete={(v) => {
38 |             console.log('AfterChange:', v);
39 |           }}
40 |           min={0}
41 |           max={1}
42 |           defaultValue={0.81}
43 |           step={0.01}
44 |         />
45 |       </div>
46 |     </div>
47 |   );
48 | };
49 | 


--------------------------------------------------------------------------------
/docs/examples/editable.tsx:
--------------------------------------------------------------------------------
 1 | /* eslint react/no-multi-comp: 0, no-console: 0 */
 2 | import Slider, { UnstableContext } from 'rc-slider';
 3 | import React from 'react';
 4 | import '../../assets/index.less';
 5 | import type { UnstableContextProps } from '../../src/context';
 6 | 
 7 | const style: React.CSSProperties = {
 8 |   width: 400,
 9 |   margin: 50,
10 | };
11 | 
12 | export default () => {
13 |   const [value, setValue] = React.useState([0, 50, 80]);
14 | 
15 |   const onDragStart: UnstableContextProps['onDragStart'] = (info) => {
16 |     const { rawValues } = info;
17 |     console.log('Start:', rawValues);
18 |   };
19 | 
20 |   const onDragChange: UnstableContextProps['onDragChange'] = (info) => {
21 |     const { rawValues } = info;
22 |     console.log('Move:', rawValues);
23 |   };
24 | 
25 |   return (
26 |     <div>
27 |       <div style={style}>
28 |         <UnstableContext.Provider value={{ onDragStart, onDragChange }}>
29 |           <Slider
30 |             // range
31 |             range={{
32 |               editable: true,
33 |               minCount: 1,
34 |               maxCount: 4,
35 |             }}
36 |             // track={false}
37 |             min={0}
38 |             max={100}
39 |             value={value}
40 |             // defaultValue={null}
41 |             onChange={(nextValue) => {
42 |               console.error('Change:', nextValue);
43 |               setValue(nextValue as any);
44 |             }}
45 |             onChangeComplete={(nextValue) => {
46 |               console.log('Complete', nextValue);
47 |             }}
48 |             // handleRender={(ori, handleProps) => {
49 |             //   if (handleProps.index === 0) {
50 |             //     console.log('handleRender', ori, handleProps);
51 |             //   }
52 |             //   return ori;
53 |             // }}
54 |             styles={{
55 |               rail: {
56 |                 background: `linear-gradient(to right, blue, red)`,
57 |               },
58 |               track: {
59 |                 background: 'orange',
60 |               },
61 |             }}
62 |           />
63 |         </UnstableContext.Provider>
64 |       </div>
65 | 
66 |       <p>Here is a word that drag should not select it</p>
67 |     </div>
68 |   );
69 | };
70 | 


--------------------------------------------------------------------------------
/docs/examples/handle.tsx:
--------------------------------------------------------------------------------
 1 | import Slider from 'rc-slider';
 2 | import React from 'react';
 3 | import '../../assets/index.less';
 4 | import TooltipSlider, { handleRender } from './components/TooltipSlider';
 5 | 
 6 | const wrapperStyle: React.CSSProperties = {
 7 |   width: 400,
 8 |   margin: 50,
 9 | };
10 | 
11 | export default () => (
12 |   <div>
13 |     <div style={wrapperStyle}>
14 |       <p>Slider with custom handle</p>
15 |       <Slider min={0} max={20} defaultValue={3} handleRender={handleRender} />
16 |     </div>
17 |     <div style={wrapperStyle}>
18 |       <p>Reversed Slider with custom handle</p>
19 |       <Slider min={0} max={20} reverse defaultValue={3} handleRender={handleRender} />
20 |     </div>
21 |     <div style={wrapperStyle}>
22 |       <p>Slider with fixed values</p>
23 |       <Slider min={20} defaultValue={20} marks={{ 20: 20, 40: 40, 100: 100 }} step={null} />
24 |     </div>
25 |     <div style={wrapperStyle}>
26 |       <p>Range with custom tooltip</p>
27 |       <TooltipSlider
28 |         range
29 |         min={0}
30 |         max={20}
31 |         defaultValue={[3, 10]}
32 |         tipFormatter={(value) => `${value}!`}
33 |       />
34 |     </div>
35 |     <div style={wrapperStyle}>
36 |       <p>Keyboard events disabled</p>
37 |       <Slider defaultValue={3} keyboard={false} />
38 |     </div>
39 |   </div>
40 | );
41 | 


--------------------------------------------------------------------------------
/docs/examples/marks.tsx:
--------------------------------------------------------------------------------
 1 | import Slider from 'rc-slider';
 2 | import React from 'react';
 3 | import '../../assets/index.less';
 4 | 
 5 | const style: React.CSSProperties = {
 6 |   width: 400,
 7 |   margin: 50,
 8 | };
 9 | 
10 | const marks = {
11 |   '-10': '-10°C',
12 |   0: <strong>0°C</strong>,
13 |   26: '26°C',
14 |   37: '37°C',
15 |   50: '50°C',
16 |   100: {
17 |     style: {
18 |       color: 'red',
19 |     },
20 |     label: <strong>100°C</strong>,
21 |   },
22 | };
23 | 
24 | function log(value) {
25 |   console.log(value); //eslint-disable-line
26 | }
27 | 
28 | export default () => (
29 |   <div>
30 |     <div style={style}>
31 |       <p>Slider with marks, `step=null`</p>
32 |       <Slider
33 |         min={-10}
34 |         marks={marks}
35 |         step={null}
36 |         onChange={log}
37 |         defaultValue={20}
38 |         onChangeComplete={(v) => console.log('AfterChange:', v)}
39 |       />
40 |     </div>
41 | 
42 |     <div style={style}>
43 |       <p>Range Slider with marks, `step=null`, pushable, draggableTrack</p>
44 |       <Slider
45 |         range
46 |         min={-10}
47 |         marks={marks}
48 |         step={null}
49 |         onChange={log}
50 |         defaultValue={[-10, 0]}
51 |         allowCross={false}
52 |         pushable
53 |         onChangeComplete={(v) => console.log('AfterChange:', v)}
54 |       />
55 |     </div>
56 | 
57 |     <div style={style}>
58 |       <p>Slider with marks and steps</p>
59 |       <Slider dots min={-10} marks={marks} step={10} onChange={log} defaultValue={20} />
60 |     </div>
61 |     <div style={style}>
62 |       <p>Reversed Slider with marks and steps</p>
63 |       <Slider dots reverse min={-10} marks={marks} step={10} onChange={log} defaultValue={20} />
64 |     </div>
65 | 
66 |     <div style={style}>
67 |       <p>Slider with marks, `included=false`</p>
68 |       <Slider min={-10} marks={marks} included={false} defaultValue={20} />
69 |     </div>
70 |     <div style={style}>
71 |       <p>Slider with marks and steps, `included=false`</p>
72 |       <Slider min={-10} marks={marks} step={10} included={false} defaultValue={20} />
73 |     </div>
74 | 
75 |     <div style={style}>
76 |       <p>Range with marks</p>
77 |       <Slider range min={-10} marks={marks} onChange={log} defaultValue={[20, 25, 30, 40]} />
78 |     </div>
79 |     <div style={style}>
80 |       <p>Range with marks and steps</p>
81 |       <Slider range min={-10} marks={marks} step={10} onChange={log} defaultValue={[20, 40]} />
82 |     </div>
83 |   </div>
84 | );
85 | 


--------------------------------------------------------------------------------
/docs/examples/multiple.tsx:
--------------------------------------------------------------------------------
 1 | /* eslint react/no-multi-comp: 0, no-console: 0 */
 2 | import Slider from 'rc-slider';
 3 | import React from 'react';
 4 | import '../../assets/index.less';
 5 | 
 6 | const style: React.CSSProperties = {
 7 |   width: 400,
 8 |   margin: 50,
 9 | };
10 | 
11 | function log(value) {
12 |   console.log(value);
13 | }
14 | 
15 | const NodeWrapper = ({ children }: { children: React.ReactElement }) => {
16 |   return <div>{React.cloneElement(children, {}, <div>TOOLTIP</div>)}</div>;
17 | };
18 | 
19 | export default () => {
20 |   const [value, setValue] = React.useState([0, 50, 80]);
21 | 
22 |   return (
23 |     <div>
24 |       <div style={style}>
25 |         <Slider
26 |           range
27 |           // defaultValue={[0, 10, 30]}
28 |           // onChange={log}
29 |           min={0}
30 |           max={100}
31 |           value={value}
32 |           onChange={(nextValue) => {
33 |             // console.log('>>>', nextValue);
34 |             // setValue(nextValue as any);
35 |           }}
36 |           activeHandleRender={(node) => <NodeWrapper>{node}</NodeWrapper>}
37 |           styles={{
38 |             tracks: {
39 |               background: `linear-gradient(to right, blue, red)`,
40 |             },
41 |             track: {
42 |               background: 'transparent',
43 |             },
44 |           }}
45 |         />
46 |       </div>
47 |     </div>
48 |   );
49 | };
50 | 


--------------------------------------------------------------------------------
/docs/examples/range.tsx:
--------------------------------------------------------------------------------
  1 | /* eslint react/no-multi-comp: 0, no-console: 0 */
  2 | import Slider from 'rc-slider';
  3 | import React from 'react';
  4 | import '../../assets/index.less';
  5 | 
  6 | const style: React.CSSProperties = {
  7 |   width: 400,
  8 |   margin: 50,
  9 | };
 10 | 
 11 | function log(value) {
 12 |   console.log(value); //eslint-disable-line
 13 | }
 14 | 
 15 | class CustomizedRange extends React.Component<any, any> {
 16 |   constructor(props) {
 17 |     super(props);
 18 |     this.state = {
 19 |       lowerBound: 20,
 20 |       upperBound: 40,
 21 |       value: [20, 40],
 22 |     };
 23 |   }
 24 | 
 25 |   onLowerBoundChange = (e) => {
 26 |     this.setState({ lowerBound: +e.target.value });
 27 |   };
 28 | 
 29 |   onUpperBoundChange = (e) => {
 30 |     this.setState({ upperBound: +e.target.value });
 31 |   };
 32 | 
 33 |   onSliderChange = (value) => {
 34 |     log(value);
 35 |     this.setState({
 36 |       value,
 37 |     });
 38 |   };
 39 | 
 40 |   handleApply = () => {
 41 |     const { lowerBound, upperBound } = this.state;
 42 |     this.setState({ value: [lowerBound, upperBound] });
 43 |   };
 44 | 
 45 |   render() {
 46 |     return (
 47 |       <div>
 48 |         <label>LowerBound: </label>
 49 |         <input type="number" value={this.state.lowerBound} onChange={this.onLowerBoundChange} />
 50 |         <br />
 51 |         <label>UpperBound: </label>
 52 |         <input type="number" value={this.state.upperBound} onChange={this.onUpperBoundChange} />
 53 |         <br />
 54 |         <button type="button" onClick={this.handleApply}>
 55 |           Apply
 56 |         </button>
 57 |         <br />
 58 |         <br />
 59 |         <Slider range allowCross={false} value={this.state.value} onChange={this.onSliderChange} />
 60 |       </div>
 61 |     );
 62 |   }
 63 | }
 64 | 
 65 | class DynamicBounds extends React.Component<any, any> {
 66 |   constructor(props) {
 67 |     super(props);
 68 |     this.state = {
 69 |       min: 0,
 70 |       max: 100,
 71 |     };
 72 |   }
 73 | 
 74 |   onSliderChange = (value) => {
 75 |     log(value);
 76 |   };
 77 | 
 78 |   onMinChange = (e) => {
 79 |     this.setState({
 80 |       min: +e.target.value || 0,
 81 |     });
 82 |   };
 83 | 
 84 |   onMaxChange = (e) => {
 85 |     this.setState({
 86 |       max: +e.target.value || 100,
 87 |     });
 88 |   };
 89 | 
 90 |   render() {
 91 |     return (
 92 |       <div>
 93 |         <label>Min: </label>
 94 |         <input type="number" value={this.state.min} onChange={this.onMinChange} />
 95 |         <br />
 96 |         <label>Max: </label>
 97 |         <input type="number" value={this.state.max} onChange={this.onMaxChange} />
 98 |         <br />
 99 |         <br />
100 |         <Slider
101 |           range
102 |           defaultValue={[20, 50]}
103 |           min={this.state.min}
104 |           max={this.state.max}
105 |           onChange={this.onSliderChange}
106 |         />
107 |       </div>
108 |     );
109 |   }
110 | }
111 | 
112 | class ControlledRange extends React.Component<any, any> {
113 |   constructor(props) {
114 |     super(props);
115 |     this.state = {
116 |       value: [20, 40, 60, 80],
117 |     };
118 |   }
119 | 
120 |   handleChange = (value) => {
121 |     this.setState({
122 |       value,
123 |     });
124 |   };
125 | 
126 |   render() {
127 |     return <Slider range value={this.state.value} onChange={this.handleChange} />;
128 |   }
129 | }
130 | 
131 | class ControlledRangeDisableAcross extends React.Component<any, any> {
132 |   constructor(props) {
133 |     super(props);
134 |     this.state = {
135 |       value: [20, 40, 60, 80],
136 |     };
137 |   }
138 | 
139 |   handleChange = (value) => {
140 |     this.setState({
141 |       value,
142 |     });
143 |   };
144 | 
145 |   render() {
146 |     return (
147 |       <Slider
148 |         range
149 |         value={this.state.value}
150 |         onChange={this.handleChange}
151 |         allowCross={false}
152 |         {...this.props}
153 |       />
154 |     );
155 |   }
156 | }
157 | 
158 | // https://github.com/react-component/slider/issues/226
159 | class PureRenderRange extends React.Component<any, any> {
160 |   constructor(props) {
161 |     super(props);
162 |     this.state = {
163 |       foo: false,
164 |     };
165 |   }
166 | 
167 |   handleChange = (value) => {
168 |     console.log(value);
169 |     this.setState(({ foo }) => ({ foo: !foo }));
170 |   };
171 | 
172 |   render() {
173 |     return (
174 |       <Slider
175 |         range
176 |         defaultValue={[20, 40, 60, 80]}
177 |         onChange={this.handleChange}
178 |         allowCross={false}
179 |       />
180 |     );
181 |   }
182 | }
183 | 
184 | export default () => (
185 |   <div>
186 |     <div style={style}>
187 |       <p>Basic Range,`allowCross=false`</p>
188 |       <Slider range allowCross={false} defaultValue={[0, 20]} onChange={log} />
189 |     </div>
190 |     <div style={style}>
191 |       <p>Basic reverse Range`</p>
192 |       <Slider range allowCross={false} defaultValue={[0, 20]} onChange={log} reverse />
193 |     </div>
194 |     <div style={style}>
195 |       <p>Basic Range,`step=20` </p>
196 |       <Slider range step={20} defaultValue={[20, 20]} onBeforeChange={log} />
197 |     </div>
198 |     <div style={style}>
199 |       <p>Basic Range,`step=20, dots` </p>
200 |       <Slider range dots step={20} defaultValue={[20, 40]} onChangeComplete={log} />
201 |     </div>
202 |     <div style={style}>
203 |       <p>Basic Range,disabled</p>
204 |       <Slider range allowCross={false} defaultValue={[0, 20]} onChange={log} disabled />
205 |     </div>
206 |     <div style={style}>
207 |       <p>Controlled Range</p>
208 |       <ControlledRange />
209 |     </div>
210 |     <div style={style}>
211 |       <p>Controlled Range, not allow across</p>
212 |       <ControlledRangeDisableAcross />
213 |     </div>
214 |     <div style={style}>
215 |       <p>Controlled Range, not allow across, pushable=5</p>
216 |       <ControlledRangeDisableAcross pushable={5} />
217 |     </div>
218 |     <div style={style}>
219 |       <p>Multi Range, count=3 and pushable=true</p>
220 |       <Slider range count={3} defaultValue={[20, 40, 60, 80]} pushable />
221 |     </div>
222 |     <div style={style}>
223 |       <p>Multi Range with custom track and handle style and pushable</p>
224 |       <Slider
225 |         range
226 |         count={3}
227 |         defaultValue={[20, 40, 60, 80]}
228 |         pushable
229 |         trackStyle={[{ backgroundColor: 'red' }, { backgroundColor: 'green' }]}
230 |         handleStyle={[{ backgroundColor: 'yellow' }, { backgroundColor: 'gray' }]}
231 |         railStyle={{ backgroundColor: 'black' }}
232 |       />
233 |     </div>
234 |     <div style={style}>
235 |       <p>Customized Range</p>
236 |       <CustomizedRange />
237 |     </div>
238 |     <div style={style}>
239 |       <p>Range with dynamic `max` `min`</p>
240 |       <DynamicBounds />
241 |     </div>
242 |     <div style={style}>
243 |       <p>Range as child component</p>
244 |       <PureRenderRange />
245 |     </div>
246 |     <div style={style}>
247 |       <p>draggableTrack two points</p>
248 |       <Slider range={{ draggableTrack: true }} allowCross={false} defaultValue={[0, 40]} onChange={log} />
249 |     </div>
250 |     <div style={style}>
251 |       <p>draggableTrack two points(reverse)</p>
252 |       <Slider
253 |         range={{ draggableTrack: true }}
254 |         allowCross={false}
255 |         reverse
256 |         defaultValue={[0, 40]}
257 |         onChange={log}
258 |       />
259 |     </div>
260 |     <div style={style}>
261 |       <p>draggableTrack multiple points</p>
262 |       <Slider
263 |         range={{ draggableTrack: true }}
264 |         allowCross={false}
265 |         defaultValue={[0, 20, 30, 40, 50]}
266 |         onChange={log}
267 |       />
268 |     </div>
269 |   </div>
270 | );
271 | 


--------------------------------------------------------------------------------
/docs/examples/slider.tsx:
--------------------------------------------------------------------------------
  1 | /* eslint react/no-multi-comp: 0, max-len: 0 */
  2 | import Slider from 'rc-slider';
  3 | import React from 'react';
  4 | import '../../assets/index.less';
  5 | import TooltipSlider from './components/TooltipSlider';
  6 | 
  7 | const style: React.CSSProperties = {
  8 |   width: 600,
  9 |   margin: 50,
 10 | };
 11 | 
 12 | function log(value) {
 13 |   console.log(value); //eslint-disable-line
 14 | }
 15 | 
 16 | function percentFormatter(v) {
 17 |   return `${v} %`;
 18 | }
 19 | 
 20 | // const SliderWithTooltip = createSliderWithTooltip(Slider);
 21 | 
 22 | class NullableSlider extends React.Component<any, any> {
 23 |   constructor(props) {
 24 |     super(props);
 25 |     this.state = {
 26 |       value: null,
 27 |     };
 28 |   }
 29 | 
 30 |   onSliderChange = (value) => {
 31 |     log(value);
 32 |     this.setState({
 33 |       value,
 34 |     });
 35 |   };
 36 | 
 37 |   onAfterChange = (value) => {
 38 |     console.log(value); //eslint-disable-line
 39 |   };
 40 | 
 41 |   reset = () => {
 42 |     console.log('reset value'); // eslint-disable-line
 43 |     this.setState({ value: null });
 44 |   };
 45 | 
 46 |   render() {
 47 |     return (
 48 |       <div>
 49 |         <Slider
 50 |           value={this.state.value}
 51 |           onChange={this.onSliderChange}
 52 |           onChangeComplete={this.onAfterChange}
 53 |         />
 54 |         <button type="button" onClick={this.reset}>
 55 |           Reset
 56 |         </button>
 57 |       </div>
 58 |     );
 59 |   }
 60 | }
 61 | 
 62 | const NullableRangeSlider = () => {
 63 |   const [value, setValue] = React.useState(null);
 64 | 
 65 |   return (
 66 |     <div>
 67 |       <Slider range value={value} onChange={setValue} />
 68 |       <button type="button" onClick={() => setValue(null)}>
 69 |         Reset
 70 |       </button>
 71 |     </div>
 72 |   );
 73 | };
 74 | 
 75 | class CustomizedSlider extends React.Component<any, any> {
 76 |   constructor(props) {
 77 |     super(props);
 78 |     this.state = {
 79 |       value: 50,
 80 |     };
 81 |   }
 82 | 
 83 |   onSliderChange = (value) => {
 84 |     log(value);
 85 |     this.setState({
 86 |       value,
 87 |     });
 88 |   };
 89 | 
 90 |   onAfterChange = (value) => {
 91 |     console.log(value); //eslint-disable-line
 92 |   };
 93 | 
 94 |   render() {
 95 |     return (
 96 |       <Slider
 97 |         value={this.state.value}
 98 |         onChange={this.onSliderChange}
 99 |         onChangeComplete={this.onAfterChange}
100 |       />
101 |     );
102 |   }
103 | }
104 | 
105 | class DynamicBounds extends React.Component<any, any> {
106 |   constructor(props) {
107 |     super(props);
108 |     this.state = {
109 |       min: 1,
110 |       max: 100,
111 |       step: 10,
112 |       value: 1,
113 |     };
114 |   }
115 | 
116 |   onSliderChange = (value) => {
117 |     log(value);
118 |     this.setState({ value });
119 |   };
120 | 
121 |   onMinChange = (e) => {
122 |     this.setState({
123 |       min: +e.target.value || 0,
124 |     });
125 |   };
126 | 
127 |   onMaxChange = (e) => {
128 |     this.setState({
129 |       max: +e.target.value || 100,
130 |     });
131 |   };
132 | 
133 |   onStepChange = (e) => {
134 |     this.setState({
135 |       step: +e.target.value || 1,
136 |     });
137 |   };
138 | 
139 |   render() {
140 |     const labelStyle = { minWidth: '60px', display: 'inline-block' };
141 |     const inputStyle = { marginBottom: '10px' };
142 |     return (
143 |       <div>
144 |         <label style={labelStyle}>Min: </label>
145 |         <input
146 |           type="number"
147 |           value={this.state.min}
148 |           onChange={this.onMinChange}
149 |           style={inputStyle}
150 |         />
151 |         <br />
152 |         <label style={labelStyle}>Max: </label>
153 |         <input
154 |           type="number"
155 |           value={this.state.max}
156 |           onChange={this.onMaxChange}
157 |           style={inputStyle}
158 |         />
159 |         <br />
160 |         <label style={labelStyle}>Step: </label>
161 |         <input
162 |           type="number"
163 |           value={this.state.step}
164 |           onChange={this.onStepChange}
165 |           style={inputStyle}
166 |         />
167 |         <br />
168 |         <br />
169 |         <label style={labelStyle}>Value: </label>
170 |         <span>{this.state.value}</span>
171 |         <br />
172 |         <br />
173 |         <Slider
174 |           value={this.state.value}
175 |           min={this.state.min}
176 |           max={this.state.max}
177 |           step={this.state.step}
178 |           onChange={this.onSliderChange}
179 |         />
180 |       </div>
181 |     );
182 |   }
183 | }
184 | 
185 | export default () => (
186 |   <div>
187 |     <div style={style}>
188 |       <p>Basic Slider</p>
189 |       <Slider onChange={log} />
190 |     </div>
191 |     <div style={style}>
192 |       <p>Basic Slider, `startPoint=50`</p>
193 |       <Slider onChange={log} startPoint={50} />
194 |     </div>
195 |     <div style={style}>
196 |       <p>Slider reverse</p>
197 |       <Slider onChange={log} reverse min={20} max={60} />
198 |     </div>
199 |     <div style={style}>
200 |       <p>Basic Slider,`step=20`</p>
201 |       <Slider step={20} defaultValue={50} onBeforeChange={log} />
202 |     </div>
203 |     <div style={style}>
204 |       <p>Basic Slider,`step=20, dots`</p>
205 |       <Slider dots step={20} defaultValue={100} onChangeComplete={log} />
206 |     </div>
207 |     <div style={style}>
208 |       <p>
209 |         Basic Slider,`step=20, dots, dotStyle={"{borderColor: 'orange'}"}, activeDotStyle=
210 |         {"{borderColor: 'yellow'}"}`
211 |       </p>
212 |       <Slider
213 |         dots
214 |         step={20}
215 |         defaultValue={100}
216 |         onChangeComplete={log}
217 |         dotStyle={{ borderColor: 'orange' }}
218 |         activeDotStyle={{ borderColor: 'yellow' }}
219 |       />
220 |     </div>
221 |     <div style={style}>
222 |       <p>Slider with tooltip, with custom `tipFormatter`</p>
223 |       <TooltipSlider
224 |         tipFormatter={percentFormatter}
225 |         tipProps={{ overlayClassName: 'foo' }}
226 |         onChange={log}
227 |       />
228 |     </div>
229 |     <div style={style}>
230 |       <p>
231 |         Slider with custom handle and track style.<strong>(old api, will be deprecated)</strong>
232 |       </p>
233 |       <Slider
234 |         defaultValue={30}
235 |         railStyle={{ backgroundColor: 'red', height: 10 }}
236 |         trackStyle={{ backgroundColor: 'blue', height: 10 }}
237 |         handleStyle={{
238 |           borderColor: 'blue',
239 |           height: 28,
240 |           width: 28,
241 |           marginLeft: -14,
242 |           marginTop: -9,
243 |           backgroundColor: 'black',
244 |         }}
245 |       />
246 |     </div>
247 |     <div style={style}>
248 |       <p>
249 |         Slider with custom handle and track style.<strong>(The recommended new api)</strong>
250 |       </p>
251 |       <Slider
252 |         defaultValue={30}
253 |         trackStyle={{ backgroundColor: 'blue', height: 10 }}
254 |         handleStyle={{
255 |           borderColor: 'blue',
256 |           height: 28,
257 |           width: 28,
258 |           marginLeft: -14,
259 |           marginTop: -9,
260 |           backgroundColor: 'black',
261 |         }}
262 |         railStyle={{ backgroundColor: 'red', height: 10 }}
263 |       />
264 |     </div>
265 |     <div style={style}>
266 |       <p>
267 |         Reversed Slider with custom handle and track style.
268 |         <strong>(The recommended new api)</strong>
269 |       </p>
270 |       <Slider
271 |         defaultValue={30}
272 |         trackStyle={{ backgroundColor: 'blue', height: 10 }}
273 |         reverse
274 |         handleStyle={{
275 |           borderColor: 'blue',
276 |           height: 28,
277 |           width: 28,
278 |           marginLeft: -14,
279 |           marginTop: -9,
280 |           backgroundColor: 'black',
281 |         }}
282 |         railStyle={{ backgroundColor: 'red', height: 10 }}
283 |       />
284 |     </div>
285 |     <div style={style}>
286 |       <p>Basic Slider, disabled</p>
287 |       <Slider onChange={log} disabled />
288 |     </div>
289 |     <div style={style}>
290 |       <p>Controlled Slider</p>
291 |       <Slider value={50} />
292 |     </div>
293 |     <div style={style}>
294 |       <p>Customized Slider</p>
295 |       <CustomizedSlider />
296 |     </div>
297 |     <div style={style}>
298 |       <p>Slider with null value and reset button</p>
299 |       <NullableSlider />
300 |     </div>
301 |     <div style={style}>
302 |       <p>Range Slider with null value and reset button</p>
303 |       <NullableRangeSlider />
304 |     </div>
305 |     <div style={style}>
306 |       <p>Slider with dynamic `min` `max` `step`</p>
307 |       <DynamicBounds />
308 |     </div>
309 |   </div>
310 | );
311 | 


--------------------------------------------------------------------------------
/docs/examples/vertical.tsx:
--------------------------------------------------------------------------------
  1 | import Slider from 'rc-slider';
  2 | import React from 'react';
  3 | import '../../assets/index.less';
  4 | 
  5 | const style: React.CSSProperties = {
  6 |   float: 'left',
  7 |   width: 160,
  8 |   height: 400,
  9 |   marginBottom: 160,
 10 |   marginLeft: 50,
 11 | };
 12 | 
 13 | const parentStyle: React.CSSProperties = {
 14 |   overflow: 'hidden',
 15 | };
 16 | 
 17 | const marks = {
 18 |   '-10': '-10°C',
 19 |   0: <strong>0°C</strong>,
 20 |   26: '26°C',
 21 |   37: '37°C',
 22 |   50: '50°C',
 23 |   100: {
 24 |     style: {
 25 |       color: 'red',
 26 |     },
 27 |     label: <strong>100°C</strong>,
 28 |   },
 29 | };
 30 | 
 31 | function log(value) {
 32 |   console.log(value); //eslint-disable-line
 33 | }
 34 | 
 35 | export default () => (
 36 |   <div style={parentStyle}>
 37 |     <div style={style}>
 38 |       <p>Slider with marks, `step=null`</p>
 39 |       <Slider vertical min={-10} marks={marks} step={null} onChange={log} defaultValue={20} />
 40 |     </div>
 41 |     <div style={style}>
 42 |       <p>Slider with marks, `step=null` and `startPoint=0`</p>
 43 |       <Slider
 44 |         vertical
 45 |         min={-10}
 46 |         startPoint={0}
 47 |         marks={marks}
 48 |         step={null}
 49 |         onChange={log}
 50 |         defaultValue={20}
 51 |       />
 52 |     </div>
 53 |     <div style={style}>
 54 |       <p>Reverse Slider with marks, `step=null`</p>
 55 |       <Slider
 56 |         vertical
 57 |         min={-10}
 58 |         marks={marks}
 59 |         step={null}
 60 |         onChange={log}
 61 |         defaultValue={20}
 62 |         reverse
 63 |       />
 64 |     </div>
 65 |     <div style={style}>
 66 |       <p>Slider with marks and steps</p>
 67 |       <Slider vertical dots min={-10} marks={marks} step={10} onChange={log} defaultValue={20} />
 68 |     </div>
 69 |     <div style={style}>
 70 |       <p>Slider with marks, `included=false`</p>
 71 |       <Slider vertical min={-10} marks={marks} included={false} defaultValue={20} />
 72 |     </div>
 73 |     <div style={style}>
 74 |       <p>Slider with marks and steps, `included=false`</p>
 75 |       <Slider vertical min={-10} marks={marks} step={10} included={false} defaultValue={20} />
 76 |     </div>
 77 |     <div style={style}>
 78 |       <p>Range with marks</p>
 79 |       <Slider range vertical min={-10} marks={marks} onChange={log} defaultValue={[20, 40]} />
 80 |     </div>
 81 |     <div style={style}>
 82 |       <p>Range with marks and steps</p>
 83 |       <Slider
 84 |         range
 85 |         vertical
 86 |         min={-10}
 87 |         marks={marks}
 88 |         step={10}
 89 |         onChange={log}
 90 |         defaultValue={[20, 40]}
 91 |       />
 92 |     </div>
 93 |     <div style={style}>
 94 |       <p>Range with marks and draggableTrack</p>
 95 |       <Slider
 96 |         range={{ draggableTrack: true }}
 97 |         vertical
 98 |         min={-10}
 99 |         marks={marks}
100 |         onChange={log}
101 |         defaultValue={[20, 40]}
102 |       />
103 |     </div>
104 |     <div style={style}>
105 |       <p>Range with marks and draggableTrack(reverse)</p>
106 |       <Slider
107 |         range={{ draggableTrack: true }}
108 |         vertical
109 |         reverse
110 |         min={-10}
111 |         marks={marks}
112 |         onChange={log}
113 |         defaultValue={[20, 40]}
114 |       />
115 |     </div>
116 |   </div>
117 | );
118 | 


--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: rc-slider
3 | ---
4 | 
5 | <embed src="../README.md"></embed>
6 | 


--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./src/');
2 | 


--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |   setupFiles: ["./tests/setup.js"],
3 | };
4 | 


--------------------------------------------------------------------------------
/now.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "version": 2,
 3 |   "name": "rc-slider",
 4 |   "builds": [
 5 |     {
 6 |       "src": "package.json",
 7 |       "use": "@now/static-build",
 8 |       "config": { "distDir": "dist" }
 9 |     }
10 |   ]
11 | }


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "rc-slider",
 3 |   "version": "11.1.8",
 4 |   "description": "Slider UI component for React",
 5 |   "keywords": [
 6 |     "react",
 7 |     "react-component",
 8 |     "react-slider",
 9 |     "slider",
10 |     "input",
11 |     "range"
12 |   ],
13 |   "homepage": "http://github.com/react-component/slider/",
14 |   "bugs": {
15 |     "url": "http://github.com/react-component/slider/issues"
16 |   },
17 |   "repository": {
18 |     "type": "git",
19 |     "url": "git@github.com:react-component/slider.git"
20 |   },
21 |   "license": "MIT",
22 |   "main": "./lib/index",
23 |   "module": "./es/index",
24 |   "types": "./lib/index.d.ts",
25 |   "style": "./assets/index.css",
26 |   "files": [
27 |     "assets/*.css",
28 |     "lib",
29 |     "es"
30 |   ],
31 |   "scripts": {
32 |     "compile": "father build && lessc assets/index.less assets/index.css",
33 |     "coverage": "rc-test --coverage",
34 |     "docs:build": "dumi build",
35 |     "docs:deploy": "gh-pages -d .doc",
36 |     "lint": "eslint src/ --ext .ts,.tsx,.jsx,.js,.md",
37 |     "now-build": "npm run docs:build",
38 |     "prepublishOnly": "npm run compile && np --yolo --no-publish",
39 |     "prettier": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
40 |     "start": "dumi dev",
41 |     "test": "rc-test"
42 |   },
43 |   "dependencies": {
44 |     "@babel/runtime": "^7.10.1",
45 |     "classnames": "^2.2.5",
46 |     "rc-util": "^5.36.0"
47 |   },
48 |   "devDependencies": {
49 |     "@rc-component/father-plugin": "^1.0.2",
50 |     "@testing-library/jest-dom": "^6.1.5",
51 |     "@testing-library/react": "^12.1.3",
52 |     "@types/classnames": "^2.2.9",
53 |     "@types/jest": "^29.5.1",
54 |     "@types/node": "^22.5.0",
55 |     "@types/react": "^18.2.42",
56 |     "@types/react-dom": "^18.0.11",
57 |     "@umijs/fabric": "^4.0.1",
58 |     "cross-env": "^7.0.0",
59 |     "dumi": "^2.2.10",
60 |     "eslint": "^8.54.0",
61 |     "eslint-plugin-jest": "^28.2.0",
62 |     "eslint-plugin-unicorn": "^54.0.0",
63 |     "father": "^4.3.5",
64 |     "father-build": "^1.18.6",
65 |     "gh-pages": "^6.1.0",
66 |     "glob": "^7.1.6",
67 |     "less": "^4.1.3",
68 |     "np": "^10.0.4",
69 |     "rc-test": "^7.0.15",
70 |     "rc-tooltip": "^6.1.2",
71 |     "rc-trigger": "^5.3.4",
72 |     "react": "^16.0.0",
73 |     "react-dom": "^16.0.0",
74 |     "regenerator-runtime": "^0.14.0",
75 |     "typescript": "^5.1.6"
76 |   },
77 |   "peerDependencies": {
78 |     "react": ">=16.9.0",
79 |     "react-dom": ">=16.9.0"
80 |   },
81 |   "engines": {
82 |     "node": ">=8.x"
83 |   }
84 | }
85 | 


--------------------------------------------------------------------------------
/script/update-content.js:
--------------------------------------------------------------------------------
 1 | /*
 2 |   用于 dumi 改造使用,
 3 |   可用于将 examples 的文件批量修改为 demo 引入形式,
 4 |   其他项目根据具体情况使用。
 5 | */
 6 | 
 7 | const fs = require('fs');
 8 | const glob = require('glob');
 9 | 
10 | const paths = glob.sync('./docs/examples/*.jsx');
11 | 
12 | paths.forEach(path => {
13 |   const name = path.split('/').pop().split('.')[0];
14 |   fs.writeFile(
15 |     `./docs/demo/${name}.md`,
16 |     `## ${name}
17 | 
18 | <code src="../examples/${name}.jsx">
19 | `,
20 |     'utf8',
21 |     function(error) {
22 |       if(error){
23 |         console.log(error);
24 |         return false;
25 |       }
26 |       console.log(`${name} 更新成功~`);
27 |     }
28 |   )
29 | });
30 | 


--------------------------------------------------------------------------------
/src/Handles/Handle.tsx:
--------------------------------------------------------------------------------
  1 | import cls from 'classnames';
  2 | import KeyCode from 'rc-util/lib/KeyCode';
  3 | import * as React from 'react';
  4 | import SliderContext from '../context';
  5 | import type { OnStartMove } from '../interface';
  6 | import { getDirectionStyle, getIndex } from '../util';
  7 | 
  8 | interface RenderProps {
  9 |   index: number;
 10 |   prefixCls: string;
 11 |   value: number;
 12 |   dragging: boolean;
 13 |   draggingDelete: boolean;
 14 | }
 15 | 
 16 | export interface HandleProps
 17 |   extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onFocus' | 'onMouseEnter'> {
 18 |   prefixCls: string;
 19 |   style?: React.CSSProperties;
 20 |   value: number;
 21 |   valueIndex: number;
 22 |   dragging: boolean;
 23 |   draggingDelete: boolean;
 24 |   onStartMove: OnStartMove;
 25 |   onDelete?: (index: number) => void;
 26 |   onOffsetChange: (value: number | 'min' | 'max', valueIndex: number) => void;
 27 |   onFocus: (e: React.FocusEvent<HTMLDivElement>, index: number) => void;
 28 |   onMouseEnter: (e: React.MouseEvent<HTMLDivElement>, index: number) => void;
 29 |   render?: (
 30 |     origin: React.ReactElement<React.HTMLAttributes<HTMLDivElement>>,
 31 |     props: RenderProps,
 32 |   ) => React.ReactElement;
 33 |   onChangeComplete?: () => void;
 34 |   mock?: boolean;
 35 | }
 36 | 
 37 | const Handle = React.forwardRef<HTMLDivElement, HandleProps>((props, ref) => {
 38 |   const {
 39 |     prefixCls,
 40 |     value,
 41 |     valueIndex,
 42 |     onStartMove,
 43 |     onDelete,
 44 |     style,
 45 |     render,
 46 |     dragging,
 47 |     draggingDelete,
 48 |     onOffsetChange,
 49 |     onChangeComplete,
 50 |     onFocus,
 51 |     onMouseEnter,
 52 |     ...restProps
 53 |   } = props;
 54 |   const {
 55 |     min,
 56 |     max,
 57 |     direction,
 58 |     disabled,
 59 |     keyboard,
 60 |     range,
 61 |     tabIndex,
 62 |     ariaLabelForHandle,
 63 |     ariaLabelledByForHandle,
 64 |     ariaRequired,
 65 |     ariaValueTextFormatterForHandle,
 66 |     styles,
 67 |     classNames,
 68 |   } = React.useContext(SliderContext);
 69 | 
 70 |   const handlePrefixCls = `${prefixCls}-handle`;
 71 | 
 72 |   // ============================ Events ============================
 73 |   const onInternalStartMove = (e: React.MouseEvent | React.TouchEvent) => {
 74 |     if (!disabled) {
 75 |       onStartMove(e, valueIndex);
 76 |     }
 77 |   };
 78 | 
 79 |   const onInternalFocus = (e: React.FocusEvent<HTMLDivElement>) => {
 80 |     onFocus?.(e, valueIndex);
 81 |   };
 82 | 
 83 |   const onInternalMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {
 84 |     onMouseEnter(e, valueIndex);
 85 |   };
 86 | 
 87 |   // =========================== Keyboard ===========================
 88 |   const onKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (e) => {
 89 |     if (!disabled && keyboard) {
 90 |       let offset: number | 'min' | 'max' = null;
 91 | 
 92 |       // Change the value
 93 |       switch (e.which || e.keyCode) {
 94 |         case KeyCode.LEFT:
 95 |           offset = direction === 'ltr' || direction === 'btt' ? -1 : 1;
 96 |           break;
 97 | 
 98 |         case KeyCode.RIGHT:
 99 |           offset = direction === 'ltr' || direction === 'btt' ? 1 : -1;
100 |           break;
101 | 
102 |         // Up is plus
103 |         case KeyCode.UP:
104 |           offset = direction !== 'ttb' ? 1 : -1;
105 |           break;
106 | 
107 |         // Down is minus
108 |         case KeyCode.DOWN:
109 |           offset = direction !== 'ttb' ? -1 : 1;
110 |           break;
111 | 
112 |         case KeyCode.HOME:
113 |           offset = 'min';
114 |           break;
115 | 
116 |         case KeyCode.END:
117 |           offset = 'max';
118 |           break;
119 | 
120 |         case KeyCode.PAGE_UP:
121 |           offset = 2;
122 |           break;
123 | 
124 |         case KeyCode.PAGE_DOWN:
125 |           offset = -2;
126 |           break;
127 | 
128 |         case KeyCode.BACKSPACE:
129 |         case KeyCode.DELETE:
130 |           onDelete?.(valueIndex);
131 |           break;
132 |       }
133 | 
134 |       if (offset !== null) {
135 |         e.preventDefault();
136 |         onOffsetChange(offset, valueIndex);
137 |       }
138 |     }
139 |   };
140 | 
141 |   const handleKeyUp = (e: React.KeyboardEvent<HTMLDivElement>) => {
142 |     switch (e.which || e.keyCode) {
143 |       case KeyCode.LEFT:
144 |       case KeyCode.RIGHT:
145 |       case KeyCode.UP:
146 |       case KeyCode.DOWN:
147 |       case KeyCode.HOME:
148 |       case KeyCode.END:
149 |       case KeyCode.PAGE_UP:
150 |       case KeyCode.PAGE_DOWN:
151 |         onChangeComplete?.();
152 |         break;
153 |     }
154 |   };
155 | 
156 |   // ============================ Offset ============================
157 |   const positionStyle = getDirectionStyle(direction, value, min, max);
158 | 
159 |   // ============================ Render ============================
160 |   let divProps: React.HtmlHTMLAttributes<HTMLDivElement> = {};
161 | 
162 |   if (valueIndex !== null) {
163 |     divProps = {
164 |       tabIndex: disabled ? null : getIndex(tabIndex, valueIndex),
165 |       role: 'slider',
166 |       'aria-valuemin': min,
167 |       'aria-valuemax': max,
168 |       'aria-valuenow': value,
169 |       'aria-disabled': disabled,
170 |       'aria-label': getIndex(ariaLabelForHandle, valueIndex),
171 |       'aria-labelledby': getIndex(ariaLabelledByForHandle, valueIndex),
172 |       'aria-required': getIndex(ariaRequired, valueIndex),
173 |       'aria-valuetext': getIndex(ariaValueTextFormatterForHandle, valueIndex)?.(value),
174 |       'aria-orientation': direction === 'ltr' || direction === 'rtl' ? 'horizontal' : 'vertical',
175 |       onMouseDown: onInternalStartMove,
176 |       onTouchStart: onInternalStartMove,
177 |       onFocus: onInternalFocus,
178 |       onMouseEnter: onInternalMouseEnter,
179 |       onKeyDown,
180 |       onKeyUp: handleKeyUp,
181 |     };
182 |   }
183 | 
184 |   let handleNode = (
185 |     <div
186 |       ref={ref}
187 |       className={cls(
188 |         handlePrefixCls,
189 |         {
190 |           [`${handlePrefixCls}-${valueIndex + 1}`]: valueIndex !== null && range,
191 |           [`${handlePrefixCls}-dragging`]: dragging,
192 |           [`${handlePrefixCls}-dragging-delete`]: draggingDelete,
193 |         },
194 |         classNames.handle,
195 |       )}
196 |       style={{
197 |         ...positionStyle,
198 |         ...style,
199 |         ...styles.handle,
200 |       }}
201 |       {...divProps}
202 |       {...restProps}
203 |     />
204 |   );
205 | 
206 |   // Customize
207 |   if (render) {
208 |     handleNode = render(handleNode, {
209 |       index: valueIndex,
210 |       prefixCls,
211 |       value,
212 |       dragging,
213 |       draggingDelete,
214 |     });
215 |   }
216 | 
217 |   return handleNode;
218 | });
219 | 
220 | if (process.env.NODE_ENV !== 'production') {
221 |   Handle.displayName = 'Handle';
222 | }
223 | 
224 | export default Handle;
225 | 


--------------------------------------------------------------------------------
/src/Handles/index.tsx:
--------------------------------------------------------------------------------
  1 | import * as React from 'react';
  2 | import { flushSync } from 'react-dom';
  3 | import type { OnStartMove } from '../interface';
  4 | import { getIndex } from '../util';
  5 | import type { HandleProps } from './Handle';
  6 | import Handle from './Handle';
  7 | 
  8 | export interface HandlesProps {
  9 |   prefixCls: string;
 10 |   style?: React.CSSProperties | React.CSSProperties[];
 11 |   values: number[];
 12 |   onStartMove: OnStartMove;
 13 |   onOffsetChange: (value: number | 'min' | 'max', valueIndex: number) => void;
 14 |   onFocus?: (e: React.FocusEvent<HTMLDivElement>) => void;
 15 |   onBlur?: (e: React.FocusEvent<HTMLDivElement>) => void;
 16 |   onDelete?: (index: number) => void;
 17 |   handleRender?: HandleProps['render'];
 18 |   /**
 19 |    * When config `activeHandleRender`,
 20 |    * it will render another hidden handle for active usage.
 21 |    * This is useful for accessibility or tooltip usage.
 22 |    */
 23 |   activeHandleRender?: HandleProps['render'];
 24 |   draggingIndex: number;
 25 |   draggingDelete: boolean;
 26 |   onChangeComplete?: () => void;
 27 | }
 28 | 
 29 | export interface HandlesRef {
 30 |   focus: (index: number) => void;
 31 |   hideHelp: VoidFunction;
 32 | }
 33 | 
 34 | const Handles = React.forwardRef<HandlesRef, HandlesProps>((props, ref) => {
 35 |   const {
 36 |     prefixCls,
 37 |     style,
 38 |     onStartMove,
 39 |     onOffsetChange,
 40 |     values,
 41 |     handleRender,
 42 |     activeHandleRender,
 43 |     draggingIndex,
 44 |     draggingDelete,
 45 |     onFocus,
 46 |     ...restProps
 47 |   } = props;
 48 |   const handlesRef = React.useRef<Record<number, HTMLDivElement>>({});
 49 | 
 50 |   // =========================== Active ===========================
 51 |   const [activeVisible, setActiveVisible] = React.useState(false);
 52 |   const [activeIndex, setActiveIndex] = React.useState(-1);
 53 | 
 54 |   const onActive = (index: number) => {
 55 |     setActiveIndex(index);
 56 |     setActiveVisible(true);
 57 |   };
 58 | 
 59 |   const onHandleFocus = (e: React.FocusEvent<HTMLDivElement>, index: number) => {
 60 |     onActive(index);
 61 |     onFocus?.(e);
 62 |   };
 63 | 
 64 |   const onHandleMouseEnter = (e: React.MouseEvent<HTMLDivElement>, index: number) => {
 65 |     onActive(index);
 66 |   };
 67 | 
 68 |   // =========================== Render ===========================
 69 |   React.useImperativeHandle(ref, () => ({
 70 |     focus: (index: number) => {
 71 |       handlesRef.current[index]?.focus();
 72 |     },
 73 |     hideHelp: () => {
 74 |       flushSync(() => {
 75 |         setActiveVisible(false);
 76 |       });
 77 |     },
 78 |   }));
 79 | 
 80 |   // =========================== Render ===========================
 81 |   // Handle Props
 82 |   const handleProps = {
 83 |     prefixCls,
 84 |     onStartMove,
 85 |     onOffsetChange,
 86 |     render: handleRender,
 87 |     onFocus: onHandleFocus,
 88 |     onMouseEnter: onHandleMouseEnter,
 89 |     ...restProps,
 90 |   };
 91 | 
 92 |   return (
 93 |     <>
 94 |       {values.map<React.ReactNode>((value, index) => {
 95 |         const dragging = draggingIndex === index;
 96 | 
 97 |         return (
 98 |           <Handle
 99 |             ref={(node) => {
100 |               if (!node) {
101 |                 delete handlesRef.current[index];
102 |               } else {
103 |                 handlesRef.current[index] = node;
104 |               }
105 |             }}
106 |             dragging={dragging}
107 |             draggingDelete={dragging && draggingDelete}
108 |             style={getIndex(style, index)}
109 |             key={index}
110 |             value={value}
111 |             valueIndex={index}
112 |             {...handleProps}
113 |           />
114 |         );
115 |       })}
116 | 
117 |       {/* Used for render tooltip, this is not a real handle */}
118 |       {activeHandleRender && activeVisible && (
119 |         <Handle
120 |           key="a11y"
121 |           {...handleProps}
122 |           value={values[activeIndex]}
123 |           valueIndex={null}
124 |           dragging={draggingIndex !== -1}
125 |           draggingDelete={draggingDelete}
126 |           render={activeHandleRender}
127 |           style={{ pointerEvents: 'none' }}
128 |           tabIndex={null}
129 |           aria-hidden
130 |         />
131 |       )}
132 |     </>
133 |   );
134 | });
135 | 
136 | if (process.env.NODE_ENV !== 'production') {
137 |   Handles.displayName = 'Handles';
138 | }
139 | 
140 | export default Handles;
141 | 


--------------------------------------------------------------------------------
/src/Marks/Mark.tsx:
--------------------------------------------------------------------------------
 1 | import classNames from 'classnames';
 2 | import * as React from 'react';
 3 | import SliderContext from '../context';
 4 | import { getDirectionStyle } from '../util';
 5 | 
 6 | export interface MarkProps {
 7 |   prefixCls: string;
 8 |   children?: React.ReactNode;
 9 |   style?: React.CSSProperties;
10 |   value: number;
11 |   onClick: (value: number) => void;
12 | }
13 | 
14 | const Mark: React.FC<MarkProps> = (props) => {
15 |   const { prefixCls, style, children, value, onClick } = props;
16 |   const { min, max, direction, includedStart, includedEnd, included } =
17 |     React.useContext(SliderContext);
18 | 
19 |   const textCls = `${prefixCls}-text`;
20 | 
21 |   // ============================ Offset ============================
22 |   const positionStyle = getDirectionStyle(direction, value, min, max);
23 | 
24 |   return (
25 |     <span
26 |       className={classNames(textCls, {
27 |         [`${textCls}-active`]: included && includedStart <= value && value <= includedEnd,
28 |       })}
29 |       style={{ ...positionStyle, ...style }}
30 |       onMouseDown={(e) => {
31 |         e.stopPropagation();
32 |       }}
33 |       onClick={() => {
34 |         onClick(value);
35 |       }}
36 |     >
37 |       {children}
38 |     </span>
39 |   );
40 | };
41 | 
42 | export default Mark;
43 | 


--------------------------------------------------------------------------------
/src/Marks/index.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from 'react';
 2 | import Mark from './Mark';
 3 | 
 4 | export interface MarkObj {
 5 |   style?: React.CSSProperties;
 6 |   label?: React.ReactNode;
 7 | }
 8 | 
 9 | export interface InternalMarkObj extends MarkObj {
10 |   value: number;
11 | }
12 | 
13 | export interface MarksProps {
14 |   prefixCls: string;
15 |   marks?: InternalMarkObj[];
16 |   onClick: (value: number) => void;
17 | }
18 | 
19 | const Marks: React.FC<MarksProps> = (props) => {
20 |   const { prefixCls, marks, onClick } = props;
21 | 
22 |   const markPrefixCls = `${prefixCls}-mark`;
23 | 
24 |   // Not render mark if empty
25 |   if (!marks.length) {
26 |     return null;
27 |   }
28 | 
29 |   return (
30 |     <div className={markPrefixCls}>
31 |       {marks.map<React.ReactNode>(({ value, style, label }) => (
32 |         <Mark key={value} prefixCls={markPrefixCls} style={style} value={value} onClick={onClick}>
33 |           {label}
34 |         </Mark>
35 |       ))}
36 |     </div>
37 |   );
38 | };
39 | 
40 | export default Marks;
41 | 


--------------------------------------------------------------------------------
/src/Slider.tsx:
--------------------------------------------------------------------------------
  1 | import cls from 'classnames';
  2 | import useEvent from 'rc-util/lib/hooks/useEvent';
  3 | import useMergedState from 'rc-util/lib/hooks/useMergedState';
  4 | import isEqual from 'rc-util/lib/isEqual';
  5 | import warning from 'rc-util/lib/warning';
  6 | import * as React from 'react';
  7 | import type { HandlesProps, HandlesRef } from './Handles';
  8 | import Handles from './Handles';
  9 | import type { InternalMarkObj, MarkObj } from './Marks';
 10 | import Marks from './Marks';
 11 | import Steps from './Steps';
 12 | import Tracks from './Tracks';
 13 | import type { SliderContextProps } from './context';
 14 | import SliderContext from './context';
 15 | import useDrag from './hooks/useDrag';
 16 | import useOffset from './hooks/useOffset';
 17 | import useRange from './hooks/useRange';
 18 | import type {
 19 |   AriaValueFormat,
 20 |   Direction,
 21 |   OnStartMove,
 22 |   SliderClassNames,
 23 |   SliderStyles,
 24 | } from './interface';
 25 | 
 26 | /**
 27 |  * New:
 28 |  * - click mark to update range value
 29 |  * - handleRender
 30 |  * - Fix handle with count not correct
 31 |  * - Fix pushable not work in some case
 32 |  * - No more FindDOMNode
 33 |  * - Move all position related style into inline style
 34 |  * - Key: up is plus, down is minus
 35 |  * - fix Key with step = null not align with marks
 36 |  * - Change range should not trigger onChange
 37 |  * - keyboard support pushable
 38 |  */
 39 | 
 40 | export type RangeConfig = {
 41 |   editable?: boolean;
 42 |   draggableTrack?: boolean;
 43 |   /** Set min count when `editable` */
 44 |   minCount?: number;
 45 |   /** Set max count when `editable` */
 46 |   maxCount?: number;
 47 | };
 48 | 
 49 | export interface SliderProps<ValueType = number | number[]> {
 50 |   prefixCls?: string;
 51 |   className?: string;
 52 |   style?: React.CSSProperties;
 53 | 
 54 |   classNames?: SliderClassNames;
 55 |   styles?: SliderStyles;
 56 | 
 57 |   id?: string;
 58 | 
 59 |   // Status
 60 |   disabled?: boolean;
 61 |   keyboard?: boolean;
 62 |   autoFocus?: boolean;
 63 |   onFocus?: (e: React.FocusEvent<HTMLDivElement>) => void;
 64 |   onBlur?: (e: React.FocusEvent<HTMLDivElement>) => void;
 65 | 
 66 |   // Value
 67 |   range?: boolean | RangeConfig;
 68 |   /** @deprecated Use `range.minCount` or `range.maxCount` to handle this */
 69 |   count?: number;
 70 |   min?: number;
 71 |   max?: number;
 72 |   step?: number | null;
 73 |   value?: ValueType;
 74 |   defaultValue?: ValueType;
 75 |   onChange?: (value: ValueType) => void;
 76 |   /** @deprecated It's always better to use `onChange` instead */
 77 |   onBeforeChange?: (value: ValueType) => void;
 78 |   /** @deprecated Use `onChangeComplete` instead */
 79 |   onAfterChange?: (value: ValueType) => void;
 80 |   onChangeComplete?: (value: ValueType) => void;
 81 | 
 82 |   // Cross
 83 |   allowCross?: boolean;
 84 |   pushable?: boolean | number;
 85 | 
 86 |   // Direction
 87 |   reverse?: boolean;
 88 |   vertical?: boolean;
 89 | 
 90 |   // Style
 91 |   included?: boolean;
 92 |   startPoint?: number;
 93 |   /** @deprecated Please use `styles.track` instead */
 94 |   trackStyle?: React.CSSProperties | React.CSSProperties[];
 95 |   /** @deprecated Please use `styles.handle` instead */
 96 |   handleStyle?: React.CSSProperties | React.CSSProperties[];
 97 |   /** @deprecated Please use `styles.rail` instead */
 98 |   railStyle?: React.CSSProperties;
 99 |   dotStyle?: React.CSSProperties | ((dotValue: number) => React.CSSProperties);
100 |   activeDotStyle?: React.CSSProperties | ((dotValue: number) => React.CSSProperties);
101 | 
102 |   // Decorations
103 |   marks?: Record<string | number, React.ReactNode | MarkObj>;
104 |   dots?: boolean;
105 | 
106 |   // Components
107 |   handleRender?: HandlesProps['handleRender'];
108 |   activeHandleRender?: HandlesProps['handleRender'];
109 |   track?: boolean;
110 | 
111 |   // Accessibility
112 |   tabIndex?: number | number[];
113 |   ariaLabelForHandle?: string | string[];
114 |   ariaLabelledByForHandle?: string | string[];
115 |   ariaRequired?: boolean;
116 |   ariaValueTextFormatterForHandle?: AriaValueFormat | AriaValueFormat[];
117 | }
118 | 
119 | export interface SliderRef {
120 |   focus: () => void;
121 |   blur: () => void;
122 | }
123 | 
124 | const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((props, ref) => {
125 |   const {
126 |     prefixCls = 'rc-slider',
127 |     className,
128 |     style,
129 |     classNames,
130 |     styles,
131 | 
132 |     id,
133 | 
134 |     // Status
135 |     disabled = false,
136 |     keyboard = true,
137 |     autoFocus,
138 |     onFocus,
139 |     onBlur,
140 | 
141 |     // Value
142 |     min = 0,
143 |     max = 100,
144 |     step = 1,
145 |     value,
146 |     defaultValue,
147 |     range,
148 |     count,
149 |     onChange,
150 |     onBeforeChange,
151 |     onAfterChange,
152 |     onChangeComplete,
153 | 
154 |     // Cross
155 |     allowCross = true,
156 |     pushable = false,
157 | 
158 |     // Direction
159 |     reverse,
160 |     vertical,
161 | 
162 |     // Style
163 |     included = true,
164 |     startPoint,
165 |     trackStyle,
166 |     handleStyle,
167 |     railStyle,
168 |     dotStyle,
169 |     activeDotStyle,
170 | 
171 |     // Decorations
172 |     marks,
173 |     dots,
174 | 
175 |     // Components
176 |     handleRender,
177 |     activeHandleRender,
178 |     track,
179 | 
180 |     // Accessibility
181 |     tabIndex = 0,
182 |     ariaLabelForHandle,
183 |     ariaLabelledByForHandle,
184 |     ariaRequired,
185 |     ariaValueTextFormatterForHandle,
186 |   } = props;
187 | 
188 |   const handlesRef = React.useRef<HandlesRef>(null);
189 |   const containerRef = React.useRef<HTMLDivElement>(null);
190 | 
191 |   const direction = React.useMemo<Direction>(() => {
192 |     if (vertical) {
193 |       return reverse ? 'ttb' : 'btt';
194 |     }
195 |     return reverse ? 'rtl' : 'ltr';
196 |   }, [reverse, vertical]);
197 | 
198 |   // ============================ Range =============================
199 |   const [rangeEnabled, rangeEditable, rangeDraggableTrack, minCount, maxCount] = useRange(range);
200 | 
201 |   const mergedMin = React.useMemo(() => (isFinite(min) ? min : 0), [min]);
202 |   const mergedMax = React.useMemo(() => (isFinite(max) ? max : 100), [max]);
203 | 
204 |   // ============================= Step =============================
205 |   const mergedStep = React.useMemo(() => (step !== null && step <= 0 ? 1 : step), [step]);
206 | 
207 |   // ============================= Push =============================
208 |   const mergedPush = React.useMemo(() => {
209 |     if (typeof pushable === 'boolean') {
210 |       return pushable ? mergedStep : false;
211 |     }
212 |     return pushable >= 0 ? pushable : false;
213 |   }, [pushable, mergedStep]);
214 | 
215 |   // ============================ Marks =============================
216 |   const markList = React.useMemo<InternalMarkObj[]>(() => {
217 |     return Object.keys(marks || {})
218 |       .map<InternalMarkObj>((key) => {
219 |         const mark = marks[key];
220 |         const markObj: InternalMarkObj = {
221 |           value: Number(key),
222 |         };
223 | 
224 |         if (
225 |           mark &&
226 |           typeof mark === 'object' &&
227 |           !React.isValidElement(mark) &&
228 |           ('label' in mark || 'style' in mark)
229 |         ) {
230 |           markObj.style = mark.style;
231 |           markObj.label = mark.label;
232 |         } else {
233 |           markObj.label = mark as React.ReactNode;
234 |         }
235 | 
236 |         return markObj;
237 |       })
238 |       .filter(({ label }) => label || typeof label === 'number')
239 |       .sort((a, b) => a.value - b.value);
240 |   }, [marks]);
241 | 
242 |   // ============================ Format ============================
243 |   const [formatValue, offsetValues] = useOffset(
244 |     mergedMin,
245 |     mergedMax,
246 |     mergedStep,
247 |     markList,
248 |     allowCross,
249 |     mergedPush,
250 |   );
251 | 
252 |   // ============================ Values ============================
253 |   const [mergedValue, setValue] = useMergedState<number | number[], number[]>(defaultValue, {
254 |     value,
255 |   });
256 | 
257 |   const rawValues = React.useMemo(() => {
258 |     const valueList =
259 |       mergedValue === null || mergedValue === undefined
260 |         ? []
261 |         : Array.isArray(mergedValue)
262 |         ? mergedValue
263 |         : [mergedValue];
264 | 
265 |     const [val0 = mergedMin] = valueList;
266 |     let returnValues = mergedValue === null ? [] : [val0];
267 | 
268 |     // Format as range
269 |     if (rangeEnabled) {
270 |       returnValues = [...valueList];
271 | 
272 |       // When count provided or value is `undefined`, we fill values
273 |       if (count || mergedValue === undefined) {
274 |         const pointCount = count >= 0 ? count + 1 : 2;
275 |         returnValues = returnValues.slice(0, pointCount);
276 | 
277 |         // Fill with count
278 |         while (returnValues.length < pointCount) {
279 |           returnValues.push(returnValues[returnValues.length - 1] ?? mergedMin);
280 |         }
281 |       }
282 |       returnValues.sort((a, b) => a - b);
283 |     }
284 | 
285 |     // Align in range
286 |     returnValues.forEach((val, index) => {
287 |       returnValues[index] = formatValue(val);
288 |     });
289 | 
290 |     return returnValues;
291 |   }, [mergedValue, rangeEnabled, mergedMin, count, formatValue]);
292 | 
293 |   // =========================== onChange ===========================
294 |   const getTriggerValue = (triggerValues: number[]) =>
295 |     rangeEnabled ? triggerValues : triggerValues[0];
296 | 
297 |   const triggerChange = useEvent((nextValues: number[]) => {
298 |     // Order first
299 |     const cloneNextValues = [...nextValues].sort((a, b) => a - b);
300 | 
301 |     // Trigger event if needed
302 |     if (onChange && !isEqual(cloneNextValues, rawValues, true)) {
303 |       onChange(getTriggerValue(cloneNextValues));
304 |     }
305 | 
306 |     // We set this later since it will re-render component immediately
307 |     setValue(cloneNextValues);
308 |   });
309 | 
310 |   const finishChange = useEvent((draggingDelete?: boolean) => {
311 |     // Trigger from `useDrag` will tell if it's a delete action
312 |     if (draggingDelete) {
313 |       handlesRef.current.hideHelp();
314 |     }
315 | 
316 |     const finishValue = getTriggerValue(rawValues);
317 |     onAfterChange?.(finishValue);
318 |     warning(
319 |       !onAfterChange,
320 |       '[rc-slider] `onAfterChange` is deprecated. Please use `onChangeComplete` instead.',
321 |     );
322 |     onChangeComplete?.(finishValue);
323 |   });
324 | 
325 |   const onDelete = (index: number) => {
326 |     if (disabled || !rangeEditable || rawValues.length <= minCount) {
327 |       return;
328 |     }
329 | 
330 |     const cloneNextValues = [...rawValues];
331 |     cloneNextValues.splice(index, 1);
332 | 
333 |     onBeforeChange?.(getTriggerValue(cloneNextValues));
334 |     triggerChange(cloneNextValues);
335 | 
336 |     const nextFocusIndex = Math.max(0, index - 1);
337 |     handlesRef.current.hideHelp();
338 |     handlesRef.current.focus(nextFocusIndex);
339 |   };
340 | 
341 |   const [draggingIndex, draggingValue, draggingDelete, cacheValues, onStartDrag] = useDrag(
342 |     containerRef,
343 |     direction,
344 |     rawValues,
345 |     mergedMin,
346 |     mergedMax,
347 |     formatValue,
348 |     triggerChange,
349 |     finishChange,
350 |     offsetValues,
351 |     rangeEditable,
352 |     minCount,
353 |   );
354 | 
355 |   /**
356 |    * When `rangeEditable` will insert a new value in the values array.
357 |    * Else it will replace the value in the values array.
358 |    */
359 |   const changeToCloseValue = (newValue: number, e?: React.MouseEvent) => {
360 |     if (!disabled) {
361 |       // Create new values
362 |       const cloneNextValues = [...rawValues];
363 | 
364 |       let valueIndex = 0;
365 |       let valueBeforeIndex = 0; // Record the index which value < newValue
366 |       let valueDist = mergedMax - mergedMin;
367 | 
368 |       rawValues.forEach((val, index) => {
369 |         const dist = Math.abs(newValue - val);
370 |         if (dist <= valueDist) {
371 |           valueDist = dist;
372 |           valueIndex = index;
373 |         }
374 | 
375 |         if (val < newValue) {
376 |           valueBeforeIndex = index;
377 |         }
378 |       });
379 | 
380 |       let focusIndex = valueIndex;
381 | 
382 |       if (rangeEditable && valueDist !== 0 && (!maxCount || rawValues.length < maxCount)) {
383 |         cloneNextValues.splice(valueBeforeIndex + 1, 0, newValue);
384 |         focusIndex = valueBeforeIndex + 1;
385 |       } else {
386 |         cloneNextValues[valueIndex] = newValue;
387 |       }
388 | 
389 |       // Fill value to match default 2 (only when `rawValues` is empty)
390 |       if (rangeEnabled && !rawValues.length && count === undefined) {
391 |         cloneNextValues.push(newValue);
392 |       }
393 | 
394 |       const nextValue = getTriggerValue(cloneNextValues);
395 |       onBeforeChange?.(nextValue);
396 |       triggerChange(cloneNextValues);
397 | 
398 |       if (e) {
399 |         (document.activeElement as HTMLElement)?.blur?.();
400 |         handlesRef.current.focus(focusIndex);
401 |         onStartDrag(e, focusIndex, cloneNextValues);
402 |       } else {
403 |         // https://github.com/ant-design/ant-design/issues/49997
404 |         onAfterChange?.(nextValue);
405 |         warning(
406 |           !onAfterChange,
407 |           '[rc-slider] `onAfterChange` is deprecated. Please use `onChangeComplete` instead.',
408 |         );
409 |         onChangeComplete?.(nextValue);
410 |       }
411 |     }
412 |   };
413 | 
414 |   // ============================ Click =============================
415 |   const onSliderMouseDown: React.MouseEventHandler<HTMLDivElement> = (e) => {
416 |     e.preventDefault();
417 | 
418 |     const { width, height, left, top, bottom, right } =
419 |       containerRef.current.getBoundingClientRect();
420 |     const { clientX, clientY } = e;
421 | 
422 |     let percent: number;
423 |     switch (direction) {
424 |       case 'btt':
425 |         percent = (bottom - clientY) / height;
426 |         break;
427 | 
428 |       case 'ttb':
429 |         percent = (clientY - top) / height;
430 |         break;
431 | 
432 |       case 'rtl':
433 |         percent = (right - clientX) / width;
434 |         break;
435 | 
436 |       default:
437 |         percent = (clientX - left) / width;
438 |     }
439 | 
440 |     const nextValue = mergedMin + percent * (mergedMax - mergedMin);
441 |     changeToCloseValue(formatValue(nextValue), e);
442 |   };
443 | 
444 |   // =========================== Keyboard ===========================
445 |   const [keyboardValue, setKeyboardValue] = React.useState<number>(null);
446 | 
447 |   const onHandleOffsetChange = (offset: number | 'min' | 'max', valueIndex: number) => {
448 |     if (!disabled) {
449 |       const next = offsetValues(rawValues, offset, valueIndex);
450 | 
451 |       onBeforeChange?.(getTriggerValue(rawValues));
452 |       triggerChange(next.values);
453 | 
454 |       setKeyboardValue(next.value);
455 |     }
456 |   };
457 | 
458 |   React.useEffect(() => {
459 |     if (keyboardValue !== null) {
460 |       const valueIndex = rawValues.indexOf(keyboardValue);
461 |       if (valueIndex >= 0) {
462 |         handlesRef.current.focus(valueIndex);
463 |       }
464 |     }
465 | 
466 |     setKeyboardValue(null);
467 |   }, [keyboardValue]);
468 | 
469 |   // ============================= Drag =============================
470 |   const mergedDraggableTrack = React.useMemo(() => {
471 |     if (rangeDraggableTrack && mergedStep === null) {
472 |       if (process.env.NODE_ENV !== 'production') {
473 |         warning(false, '`draggableTrack` is not supported when `step` is `null`.');
474 |       }
475 |       return false;
476 |     }
477 |     return rangeDraggableTrack;
478 |   }, [rangeDraggableTrack, mergedStep]);
479 | 
480 |   const onStartMove: OnStartMove = useEvent((e, valueIndex) => {
481 |     onStartDrag(e, valueIndex);
482 | 
483 |     onBeforeChange?.(getTriggerValue(rawValues));
484 |   });
485 | 
486 |   // Auto focus for updated handle
487 |   const dragging = draggingIndex !== -1;
488 |   React.useEffect(() => {
489 |     if (!dragging) {
490 |       const valueIndex = rawValues.lastIndexOf(draggingValue);
491 |       handlesRef.current.focus(valueIndex);
492 |     }
493 |   }, [dragging]);
494 | 
495 |   // =========================== Included ===========================
496 |   const sortedCacheValues = React.useMemo(
497 |     () => [...cacheValues].sort((a, b) => a - b),
498 |     [cacheValues],
499 |   );
500 | 
501 |   // Provide a range values with included [min, max]
502 |   // Used for Track, Mark & Dot
503 |   const [includedStart, includedEnd] = React.useMemo(() => {
504 |     if (!rangeEnabled) {
505 |       return [mergedMin, sortedCacheValues[0]];
506 |     }
507 | 
508 |     return [sortedCacheValues[0], sortedCacheValues[sortedCacheValues.length - 1]];
509 |   }, [sortedCacheValues, rangeEnabled, mergedMin]);
510 | 
511 |   // ============================= Refs =============================
512 |   React.useImperativeHandle(ref, () => ({
513 |     focus: () => {
514 |       handlesRef.current.focus(0);
515 |     },
516 |     blur: () => {
517 |       const { activeElement } = document;
518 |       if (containerRef.current?.contains(activeElement)) {
519 |         (activeElement as HTMLElement)?.blur();
520 |       }
521 |     },
522 |   }));
523 | 
524 |   // ========================== Auto Focus ==========================
525 |   React.useEffect(() => {
526 |     if (autoFocus) {
527 |       handlesRef.current.focus(0);
528 |     }
529 |   }, []);
530 | 
531 |   // =========================== Context ============================
532 |   const context = React.useMemo<SliderContextProps>(
533 |     () => ({
534 |       min: mergedMin,
535 |       max: mergedMax,
536 |       direction,
537 |       disabled,
538 |       keyboard,
539 |       step: mergedStep,
540 |       included,
541 |       includedStart,
542 |       includedEnd,
543 |       range: rangeEnabled,
544 |       tabIndex,
545 |       ariaLabelForHandle,
546 |       ariaLabelledByForHandle,
547 |       ariaRequired,
548 |       ariaValueTextFormatterForHandle,
549 |       styles: styles || {},
550 |       classNames: classNames || {},
551 |     }),
552 |     [
553 |       mergedMin,
554 |       mergedMax,
555 |       direction,
556 |       disabled,
557 |       keyboard,
558 |       mergedStep,
559 |       included,
560 |       includedStart,
561 |       includedEnd,
562 |       rangeEnabled,
563 |       tabIndex,
564 |       ariaLabelForHandle,
565 |       ariaLabelledByForHandle,
566 |       ariaRequired,
567 |       ariaValueTextFormatterForHandle,
568 |       styles,
569 |       classNames,
570 |     ],
571 |   );
572 | 
573 |   // ============================ Render ============================
574 |   return (
575 |     <SliderContext.Provider value={context}>
576 |       <div
577 |         ref={containerRef}
578 |         className={cls(prefixCls, className, {
579 |           [`${prefixCls}-disabled`]: disabled,
580 |           [`${prefixCls}-vertical`]: vertical,
581 |           [`${prefixCls}-horizontal`]: !vertical,
582 |           [`${prefixCls}-with-marks`]: markList.length,
583 |         })}
584 |         style={style}
585 |         onMouseDown={onSliderMouseDown}
586 |         id={id}
587 |       >
588 |         <div
589 |           className={cls(`${prefixCls}-rail`, classNames?.rail)}
590 |           style={{ ...railStyle, ...styles?.rail }}
591 |         />
592 | 
593 |         {track !== false && (
594 |           <Tracks
595 |             prefixCls={prefixCls}
596 |             style={trackStyle}
597 |             values={rawValues}
598 |             startPoint={startPoint}
599 |             onStartMove={mergedDraggableTrack ? onStartMove : undefined}
600 |           />
601 |         )}
602 | 
603 |         <Steps
604 |           prefixCls={prefixCls}
605 |           marks={markList}
606 |           dots={dots}
607 |           style={dotStyle}
608 |           activeStyle={activeDotStyle}
609 |         />
610 | 
611 |         <Handles
612 |           ref={handlesRef}
613 |           prefixCls={prefixCls}
614 |           style={handleStyle}
615 |           values={cacheValues}
616 |           draggingIndex={draggingIndex}
617 |           draggingDelete={draggingDelete}
618 |           onStartMove={onStartMove}
619 |           onOffsetChange={onHandleOffsetChange}
620 |           onFocus={onFocus}
621 |           onBlur={onBlur}
622 |           handleRender={handleRender}
623 |           activeHandleRender={activeHandleRender}
624 |           onChangeComplete={finishChange}
625 |           onDelete={rangeEditable ? onDelete : undefined}
626 |         />
627 | 
628 |         <Marks prefixCls={prefixCls} marks={markList} onClick={changeToCloseValue} />
629 |       </div>
630 |     </SliderContext.Provider>
631 |   );
632 | });
633 | 
634 | if (process.env.NODE_ENV !== 'production') {
635 |   Slider.displayName = 'Slider';
636 | }
637 | 
638 | export default Slider;
639 | 


--------------------------------------------------------------------------------
/src/Steps/Dot.tsx:
--------------------------------------------------------------------------------
 1 | import classNames from 'classnames';
 2 | import * as React from 'react';
 3 | import SliderContext from '../context';
 4 | import { getDirectionStyle } from '../util';
 5 | 
 6 | export interface DotProps {
 7 |   prefixCls: string;
 8 |   value: number;
 9 |   style?: React.CSSProperties | ((dotValue: number) => React.CSSProperties);
10 |   activeStyle?: React.CSSProperties | ((dotValue: number) => React.CSSProperties);
11 | }
12 | 
13 | const Dot: React.FC<DotProps> = (props) => {
14 |   const { prefixCls, value, style, activeStyle } = props;
15 |   const { min, max, direction, included, includedStart, includedEnd } =
16 |     React.useContext(SliderContext);
17 | 
18 |   const dotClassName = `${prefixCls}-dot`;
19 |   const active = included && includedStart <= value && value <= includedEnd;
20 | 
21 |   // ============================ Offset ============================
22 |   let mergedStyle: React.CSSProperties = {
23 |     ...getDirectionStyle(direction, value, min, max),
24 |     ...(typeof style === 'function' ? style(value) : style),
25 |   };
26 | 
27 |   if (active) {
28 |     mergedStyle = {
29 |       ...mergedStyle,
30 |       ...(typeof activeStyle === 'function' ? activeStyle(value) : activeStyle),
31 |     };
32 |   }
33 | 
34 |   return (
35 |     <span
36 |       className={classNames(dotClassName, { [`${dotClassName}-active`]: active })}
37 |       style={mergedStyle}
38 |     />
39 |   );
40 | };
41 | 
42 | export default Dot;
43 | 


--------------------------------------------------------------------------------
/src/Steps/index.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from 'react';
 2 | import type { InternalMarkObj } from '../Marks';
 3 | import SliderContext from '../context';
 4 | import Dot from './Dot';
 5 | 
 6 | export interface StepsProps {
 7 |   prefixCls: string;
 8 |   marks: InternalMarkObj[];
 9 |   dots?: boolean;
10 |   style?: React.CSSProperties | ((dotValue: number) => React.CSSProperties);
11 |   activeStyle?: React.CSSProperties | ((dotValue: number) => React.CSSProperties);
12 | }
13 | 
14 | const Steps: React.FC<StepsProps> = (props) => {
15 |   const { prefixCls, marks, dots, style, activeStyle } = props;
16 |   const { min, max, step } = React.useContext(SliderContext);
17 | 
18 |   const stepDots = React.useMemo<number[]>(() => {
19 |     const dotSet = new Set<number>();
20 | 
21 |     // Add marks
22 |     marks.forEach((mark) => {
23 |       dotSet.add(mark.value);
24 |     });
25 | 
26 |     // Fill dots
27 |     if (dots && step !== null) {
28 |       let current = min;
29 |       while (current <= max) {
30 |         dotSet.add(current);
31 |         current += step;
32 |       }
33 |     }
34 | 
35 |     return Array.from(dotSet);
36 |   }, [min, max, step, dots, marks]);
37 | 
38 |   return (
39 |     <div className={`${prefixCls}-step`}>
40 |       {stepDots.map<React.ReactNode>((dotValue) => (
41 |         <Dot
42 |           prefixCls={prefixCls}
43 |           key={dotValue}
44 |           value={dotValue}
45 |           style={style}
46 |           activeStyle={activeStyle}
47 |         />
48 |       ))}
49 |     </div>
50 |   );
51 | };
52 | 
53 | export default Steps;
54 | 


--------------------------------------------------------------------------------
/src/Tracks/Track.tsx:
--------------------------------------------------------------------------------
 1 | import cls from 'classnames';
 2 | import * as React from 'react';
 3 | import SliderContext from '../context';
 4 | import type { OnStartMove } from '../interface';
 5 | import { getOffset } from '../util';
 6 | 
 7 | export interface TrackProps {
 8 |   prefixCls: string;
 9 |   style?: React.CSSProperties;
10 |   /** Replace with origin prefix concat className */
11 |   replaceCls?: string;
12 |   start: number;
13 |   end: number;
14 |   index: number;
15 |   onStartMove?: OnStartMove;
16 | }
17 | 
18 | const Track: React.FC<TrackProps> = (props) => {
19 |   const { prefixCls, style, start, end, index, onStartMove, replaceCls } = props;
20 |   const { direction, min, max, disabled, range, classNames } = React.useContext(SliderContext);
21 | 
22 |   const trackPrefixCls = `${prefixCls}-track`;
23 | 
24 |   const offsetStart = getOffset(start, min, max);
25 |   const offsetEnd = getOffset(end, min, max);
26 | 
27 |   // ============================ Events ============================
28 |   const onInternalStartMove = (e: React.MouseEvent | React.TouchEvent) => {
29 |     if (!disabled && onStartMove) {
30 |       onStartMove(e, -1);
31 |     }
32 |   };
33 | 
34 |   // ============================ Render ============================
35 |   const positionStyle: React.CSSProperties = {};
36 | 
37 |   switch (direction) {
38 |     case 'rtl':
39 |       positionStyle.right = `${offsetStart * 100}%`;
40 |       positionStyle.width = `${offsetEnd * 100 - offsetStart * 100}%`;
41 |       break;
42 | 
43 |     case 'btt':
44 |       positionStyle.bottom = `${offsetStart * 100}%`;
45 |       positionStyle.height = `${offsetEnd * 100 - offsetStart * 100}%`;
46 |       break;
47 | 
48 |     case 'ttb':
49 |       positionStyle.top = `${offsetStart * 100}%`;
50 |       positionStyle.height = `${offsetEnd * 100 - offsetStart * 100}%`;
51 |       break;
52 | 
53 |     default:
54 |       positionStyle.left = `${offsetStart * 100}%`;
55 |       positionStyle.width = `${offsetEnd * 100 - offsetStart * 100}%`;
56 |   }
57 | 
58 |   const className =
59 |     replaceCls ||
60 |     cls(
61 |       trackPrefixCls,
62 |       {
63 |         [`${trackPrefixCls}-${index + 1}`]: index !== null && range,
64 |         [`${prefixCls}-track-draggable`]: onStartMove,
65 |       },
66 |       classNames.track,
67 |     );
68 | 
69 |   return (
70 |     <div
71 |       className={className}
72 |       style={{ ...positionStyle, ...style }}
73 |       onMouseDown={onInternalStartMove}
74 |       onTouchStart={onInternalStartMove}
75 |     />
76 |   );
77 | };
78 | 
79 | export default Track;
80 | 


--------------------------------------------------------------------------------
/src/Tracks/index.tsx:
--------------------------------------------------------------------------------
 1 | import cls from 'classnames';
 2 | import * as React from 'react';
 3 | import SliderContext from '../context';
 4 | import type { OnStartMove } from '../interface';
 5 | import { getIndex } from '../util';
 6 | import Track from './Track';
 7 | 
 8 | export interface TrackProps {
 9 |   prefixCls: string;
10 |   style?: React.CSSProperties | React.CSSProperties[];
11 |   values: number[];
12 |   onStartMove?: OnStartMove;
13 |   startPoint?: number;
14 | }
15 | 
16 | const Tracks: React.FC<TrackProps> = (props) => {
17 |   const { prefixCls, style, values, startPoint, onStartMove } = props;
18 |   const { included, range, min, styles, classNames } = React.useContext(SliderContext);
19 | 
20 |   // =========================== List ===========================
21 |   const trackList = React.useMemo(() => {
22 |     if (!range) {
23 |       // null value do not have track
24 |       if (values.length === 0) {
25 |         return [];
26 |       }
27 | 
28 |       const startValue = startPoint ?? min;
29 |       const endValue = values[0];
30 | 
31 |       return [{ start: Math.min(startValue, endValue), end: Math.max(startValue, endValue) }];
32 |     }
33 | 
34 |     // Multiple
35 |     const list: { start: number; end: number }[] = [];
36 | 
37 |     for (let i = 0; i < values.length - 1; i += 1) {
38 |       list.push({ start: values[i], end: values[i + 1] });
39 |     }
40 | 
41 |     return list;
42 |   }, [values, range, startPoint, min]);
43 | 
44 |   if (!included) {
45 |     return null;
46 |   }
47 | 
48 |   // ========================== Render ==========================
49 |   const tracksNode =
50 |       trackList?.length && (classNames.tracks || styles.tracks) ? (
51 |       <Track
52 |         index={null}
53 |         prefixCls={prefixCls}
54 |         start={trackList[0].start}
55 |         end={trackList[trackList.length - 1].end}
56 |         replaceCls={cls(classNames.tracks, `${prefixCls}-tracks`)}
57 |         style={styles.tracks}
58 |       />
59 |     ) : null;
60 | 
61 |   return (
62 |     <>
63 |       {tracksNode}
64 |       {trackList.map<React.ReactNode>(({ start, end }, index) => (
65 |         <Track
66 |           index={index}
67 |           prefixCls={prefixCls}
68 |           style={{ ...getIndex(style, index), ...styles.track }}
69 |           start={start}
70 |           end={end}
71 |           key={index}
72 |           onStartMove={onStartMove}
73 |         />
74 |       ))}
75 |     </>
76 |   );
77 | };
78 | 
79 | export default Tracks;
80 | 


--------------------------------------------------------------------------------
/src/context.ts:
--------------------------------------------------------------------------------
 1 | import * as React from 'react';
 2 | import type { AriaValueFormat, Direction, SliderClassNames, SliderStyles } from './interface';
 3 | 
 4 | export interface SliderContextProps {
 5 |   min: number;
 6 |   max: number;
 7 |   includedStart: number;
 8 |   includedEnd: number;
 9 |   direction: Direction;
10 |   disabled?: boolean;
11 |   keyboard?: boolean;
12 |   included?: boolean;
13 |   step: number | null;
14 |   range?: boolean;
15 |   tabIndex: number | number[];
16 |   ariaLabelForHandle?: string | string[];
17 |   ariaLabelledByForHandle?: string | string[];
18 |   ariaRequired?: boolean;
19 |   ariaValueTextFormatterForHandle?: AriaValueFormat | AriaValueFormat[];
20 |   classNames: SliderClassNames;
21 |   styles: SliderStyles;
22 | }
23 | 
24 | const SliderContext = React.createContext<SliderContextProps>({
25 |   min: 0,
26 |   max: 0,
27 |   direction: 'ltr',
28 |   step: 1,
29 |   includedStart: 0,
30 |   includedEnd: 0,
31 |   tabIndex: 0,
32 |   keyboard: true,
33 |   styles: {},
34 |   classNames: {},
35 | });
36 | 
37 | export default SliderContext;
38 | 
39 | export interface UnstableContextProps {
40 |   onDragStart?: (info: {
41 |     rawValues: number[];
42 |     draggingIndex: number;
43 |     draggingValue: number;
44 |   }) => void;
45 |   onDragChange?: (info: {
46 |     rawValues: number[];
47 |     deleteIndex: number;
48 |     draggingIndex: number;
49 |     draggingValue: number;
50 |   }) => void;
51 | }
52 | 
53 | /** @private NOT PROMISE AVAILABLE. DO NOT USE IN PRODUCTION. */
54 | export const UnstableContext = React.createContext<UnstableContextProps>({});
55 | 


--------------------------------------------------------------------------------
/src/hooks/useDrag.ts:
--------------------------------------------------------------------------------
  1 | import * as React from 'react';
  2 | import useEvent from 'rc-util/lib/hooks/useEvent';
  3 | import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect';
  4 | import { UnstableContext } from '../context';
  5 | import type { Direction, OnStartMove } from '../interface';
  6 | import type { OffsetValues } from './useOffset';
  7 | 
  8 | /** Drag to delete offset. It's a user experience number for dragging out */
  9 | const REMOVE_DIST = 130;
 10 | 
 11 | function getPosition(e: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent) {
 12 |   const obj = 'targetTouches' in e ? e.targetTouches[0] : e;
 13 | 
 14 |   return { pageX: obj.pageX, pageY: obj.pageY };
 15 | }
 16 | 
 17 | function useDrag(
 18 |   containerRef: React.RefObject<HTMLDivElement>,
 19 |   direction: Direction,
 20 |   rawValues: number[],
 21 |   min: number,
 22 |   max: number,
 23 |   formatValue: (value: number) => number,
 24 |   triggerChange: (values: number[]) => void,
 25 |   finishChange: (draggingDelete: boolean) => void,
 26 |   offsetValues: OffsetValues,
 27 |   editable: boolean,
 28 |   minCount: number,
 29 | ): [
 30 |   draggingIndex: number,
 31 |   draggingValue: number,
 32 |   draggingDelete: boolean,
 33 |   returnValues: number[],
 34 |   onStartMove: OnStartMove,
 35 | ] {
 36 |   const [draggingValue, setDraggingValue] = React.useState(null);
 37 |   const [draggingIndex, setDraggingIndex] = React.useState(-1);
 38 |   const [draggingDelete, setDraggingDelete] = React.useState(false);
 39 |   const [cacheValues, setCacheValues] = React.useState(rawValues);
 40 |   const [originValues, setOriginValues] = React.useState(rawValues);
 41 | 
 42 |   const mouseMoveEventRef = React.useRef<(event: MouseEvent) => void>(null);
 43 |   const mouseUpEventRef = React.useRef<(event: MouseEvent) => void>(null);
 44 |   const touchEventTargetRef = React.useRef<EventTarget>(null);
 45 | 
 46 |   const { onDragStart, onDragChange } = React.useContext(UnstableContext);
 47 | 
 48 |   useLayoutEffect(() => {
 49 |     if (draggingIndex === -1) {
 50 |       setCacheValues(rawValues);
 51 |     }
 52 |   }, [rawValues, draggingIndex]);
 53 | 
 54 |   // Clean up event
 55 |   React.useEffect(
 56 |     () => () => {
 57 |       document.removeEventListener('mousemove', mouseMoveEventRef.current);
 58 |       document.removeEventListener('mouseup', mouseUpEventRef.current);
 59 |       if (touchEventTargetRef.current) {
 60 |         touchEventTargetRef.current.removeEventListener('touchmove', mouseMoveEventRef.current);
 61 |         touchEventTargetRef.current.removeEventListener('touchend', mouseUpEventRef.current);
 62 |       }
 63 |     },
 64 |     [],
 65 |   );
 66 | 
 67 |   const flushValues = (nextValues: number[], nextValue?: number, deleteMark?: boolean) => {
 68 |     // Perf: Only update state when value changed
 69 |     if (nextValue !== undefined) {
 70 |       setDraggingValue(nextValue);
 71 |     }
 72 |     setCacheValues(nextValues);
 73 | 
 74 |     let changeValues = nextValues;
 75 |     if (deleteMark) {
 76 |       changeValues = nextValues.filter((_, i) => i !== draggingIndex);
 77 |     }
 78 |     triggerChange(changeValues);
 79 | 
 80 |     if (onDragChange) {
 81 |       onDragChange({
 82 |         rawValues: nextValues,
 83 |         deleteIndex: deleteMark ? draggingIndex : -1,
 84 |         draggingIndex,
 85 |         draggingValue: nextValue,
 86 |       });
 87 |     }
 88 |   };
 89 | 
 90 |   const updateCacheValue = useEvent(
 91 |     (valueIndex: number, offsetPercent: number, deleteMark: boolean) => {
 92 |       if (valueIndex === -1) {
 93 |         // >>>> Dragging on the track
 94 |         const startValue = originValues[0];
 95 |         const endValue = originValues[originValues.length - 1];
 96 |         const maxStartOffset = min - startValue;
 97 |         const maxEndOffset = max - endValue;
 98 | 
 99 |         // Get valid offset
100 |         let offset = offsetPercent * (max - min);
101 |         offset = Math.max(offset, maxStartOffset);
102 |         offset = Math.min(offset, maxEndOffset);
103 | 
104 |         // Use first value to revert back of valid offset (like steps marks)
105 |         const formatStartValue = formatValue(startValue + offset);
106 |         offset = formatStartValue - startValue;
107 |         const cloneCacheValues = originValues.map<number>((val) => val + offset);
108 |         flushValues(cloneCacheValues);
109 |       } else {
110 |         // >>>> Dragging on the handle
111 |         const offsetDist = (max - min) * offsetPercent;
112 | 
113 |         // Always start with the valueIndex origin value
114 |         const cloneValues = [...cacheValues];
115 |         cloneValues[valueIndex] = originValues[valueIndex];
116 | 
117 |         const next = offsetValues(cloneValues, offsetDist, valueIndex, 'dist');
118 | 
119 |         flushValues(next.values, next.value, deleteMark);
120 |       }
121 |     },
122 |   );
123 | 
124 |   const onStartMove: OnStartMove = (e, valueIndex, startValues?: number[]) => {
125 |     e.stopPropagation();
126 | 
127 |     // 如果是点击 track 触发的,需要传入变化后的初始值,而不能直接用 rawValues
128 |     const initialValues = startValues || rawValues;
129 |     const originValue = initialValues[valueIndex];
130 | 
131 |     setDraggingIndex(valueIndex);
132 |     setDraggingValue(originValue);
133 |     setOriginValues(initialValues);
134 |     setCacheValues(initialValues);
135 |     setDraggingDelete(false);
136 | 
137 |     const { pageX: startX, pageY: startY } = getPosition(e);
138 | 
139 |     // We declare it here since closure can't get outer latest value
140 |     let deleteMark = false;
141 | 
142 |     // Internal trigger event
143 |     if (onDragStart) {
144 |       onDragStart({
145 |         rawValues: initialValues,
146 |         draggingIndex: valueIndex,
147 |         draggingValue: originValue,
148 |       });
149 |     }
150 | 
151 |     // Moving
152 |     const onMouseMove = (event: MouseEvent | TouchEvent) => {
153 |       event.preventDefault();
154 | 
155 |       const { pageX: moveX, pageY: moveY } = getPosition(event);
156 |       const offsetX = moveX - startX;
157 |       const offsetY = moveY - startY;
158 | 
159 |       const { width, height } = containerRef.current.getBoundingClientRect();
160 | 
161 |       let offSetPercent: number;
162 |       let removeDist: number;
163 | 
164 |       switch (direction) {
165 |         case 'btt':
166 |           offSetPercent = -offsetY / height;
167 |           removeDist = offsetX;
168 |           break;
169 | 
170 |         case 'ttb':
171 |           offSetPercent = offsetY / height;
172 |           removeDist = offsetX;
173 |           break;
174 | 
175 |         case 'rtl':
176 |           offSetPercent = -offsetX / width;
177 |           removeDist = offsetY;
178 |           break;
179 | 
180 |         default:
181 |           offSetPercent = offsetX / width;
182 |           removeDist = offsetY;
183 |       }
184 | 
185 |       // Check if need mark remove
186 |       deleteMark = editable
187 |         ? Math.abs(removeDist) > REMOVE_DIST && minCount < cacheValues.length
188 |         : false;
189 |       setDraggingDelete(deleteMark);
190 | 
191 |       updateCacheValue(valueIndex, offSetPercent, deleteMark);
192 |     };
193 | 
194 |     // End
195 |     const onMouseUp = (event: MouseEvent | TouchEvent) => {
196 |       event.preventDefault();
197 | 
198 |       document.removeEventListener('mouseup', onMouseUp);
199 |       document.removeEventListener('mousemove', onMouseMove);
200 |       if (touchEventTargetRef.current) {
201 |         touchEventTargetRef.current.removeEventListener('touchmove', mouseMoveEventRef.current);
202 |         touchEventTargetRef.current.removeEventListener('touchend', mouseUpEventRef.current);
203 |       }
204 |       mouseMoveEventRef.current = null;
205 |       mouseUpEventRef.current = null;
206 |       touchEventTargetRef.current = null;
207 | 
208 |       finishChange(deleteMark);
209 | 
210 |       setDraggingIndex(-1);
211 |       setDraggingDelete(false);
212 |     };
213 | 
214 |     document.addEventListener('mouseup', onMouseUp);
215 |     document.addEventListener('mousemove', onMouseMove);
216 |     e.currentTarget.addEventListener('touchend', onMouseUp);
217 |     e.currentTarget.addEventListener('touchmove', onMouseMove);
218 |     mouseMoveEventRef.current = onMouseMove;
219 |     mouseUpEventRef.current = onMouseUp;
220 |     touchEventTargetRef.current = e.currentTarget;
221 |   };
222 | 
223 |   // Only return cache value when it mapping with rawValues
224 |   const returnValues = React.useMemo(() => {
225 |     const sourceValues = [...rawValues].sort((a, b) => a - b);
226 |     const targetValues = [...cacheValues].sort((a, b) => a - b);
227 | 
228 |     const counts: Record<number, number> = {};
229 |     targetValues.forEach((val) => {
230 |       counts[val] = (counts[val] || 0) + 1;
231 |     });
232 |     sourceValues.forEach((val) => {
233 |       counts[val] = (counts[val] || 0) - 1;
234 |     });
235 | 
236 |     const maxDiffCount = editable ? 1 : 0;
237 |     const diffCount: number = Object.values(counts).reduce(
238 |       (prev, next) => prev + Math.abs(next),
239 |       0,
240 |     );
241 | 
242 |     return diffCount <= maxDiffCount ? cacheValues : rawValues;
243 |   }, [rawValues, cacheValues, editable]);
244 | 
245 |   return [draggingIndex, draggingValue, draggingDelete, returnValues, onStartMove];
246 | }
247 | 
248 | export default useDrag;
249 | 


--------------------------------------------------------------------------------
/src/hooks/useOffset.ts:
--------------------------------------------------------------------------------
  1 | import * as React from 'react';
  2 | import type { InternalMarkObj } from '../Marks';
  3 | 
  4 | /** Format the value in the range of [min, max] */
  5 | type FormatRangeValue = (value: number) => number;
  6 | 
  7 | /** Format value align with step */
  8 | type FormatStepValue = (value: number) => number;
  9 | 
 10 | /** Format value align with step & marks */
 11 | type FormatValue = (value: number) => number;
 12 | 
 13 | type OffsetMode = 'unit' | 'dist';
 14 | 
 15 | type OffsetValue = (
 16 |   values: number[],
 17 |   offset: number | 'min' | 'max',
 18 |   valueIndex: number,
 19 |   mode?: OffsetMode,
 20 | ) => number;
 21 | 
 22 | export type OffsetValues = (
 23 |   values: number[],
 24 |   offset: number | 'min' | 'max',
 25 |   valueIndex: number,
 26 |   mode?: OffsetMode,
 27 | ) => {
 28 |   value: number;
 29 |   values: number[];
 30 | };
 31 | 
 32 | export default function useOffset(
 33 |   min: number,
 34 |   max: number,
 35 |   step: number,
 36 |   markList: InternalMarkObj[],
 37 |   allowCross: boolean,
 38 |   pushable: false | number,
 39 | ): [FormatValue, OffsetValues] {
 40 |   const formatRangeValue: FormatRangeValue = React.useCallback(
 41 |     (val) => Math.max(min, Math.min(max, val)),
 42 |     [min, max],
 43 |   );
 44 | 
 45 |   const formatStepValue: FormatStepValue = React.useCallback(
 46 |     (val) => {
 47 |       if (step !== null) {
 48 |         const stepValue = min + Math.round((formatRangeValue(val) - min) / step) * step;
 49 | 
 50 |         // Cut number in case to be like 0.30000000000000004
 51 |         const getDecimal = (num: number) => (String(num).split('.')[1] || '').length;
 52 |         const maxDecimal = Math.max(getDecimal(step), getDecimal(max), getDecimal(min));
 53 |         const fixedValue = Number(stepValue.toFixed(maxDecimal));
 54 | 
 55 |         return min <= fixedValue && fixedValue <= max ? fixedValue : null;
 56 |       }
 57 |       return null;
 58 |     },
 59 |     [step, min, max, formatRangeValue],
 60 |   );
 61 | 
 62 |   const formatValue = React.useCallback<FormatValue>(
 63 |     (val) => {
 64 |       const formatNextValue = formatRangeValue(val);
 65 | 
 66 |       // List align values
 67 |       const alignValues = markList.map<number>((mark) => mark.value);
 68 |       if (step !== null) {
 69 |         alignValues.push(formatStepValue(val));
 70 |       }
 71 | 
 72 |       // min & max
 73 |       alignValues.push(min, max);
 74 | 
 75 |       // Align with marks
 76 |       let closeValue = alignValues[0];
 77 |       let closeDist = max - min;
 78 | 
 79 |       alignValues.forEach((alignValue) => {
 80 |         const dist = Math.abs(formatNextValue - alignValue);
 81 |         if (dist <= closeDist) {
 82 |           closeValue = alignValue;
 83 |           closeDist = dist;
 84 |         }
 85 |       });
 86 | 
 87 |       return closeValue;
 88 |     },
 89 |     [min, max, markList, step, formatRangeValue, formatStepValue],
 90 |   );
 91 | 
 92 |   // ========================== Offset ==========================
 93 |   // Single Value
 94 |   const offsetValue: OffsetValue = (values, offset, valueIndex, mode = 'unit') => {
 95 |     if (typeof offset === 'number') {
 96 |       let nextValue: number;
 97 |       const originValue = values[valueIndex];
 98 | 
 99 |       // Only used for `dist` mode
100 |       const targetDistValue = originValue + offset;
101 | 
102 |       // Compare next step value & mark value which is best match
103 |       let potentialValues: number[] = [];
104 |       markList.forEach((mark) => {
105 |         potentialValues.push(mark.value);
106 |       });
107 | 
108 |       // Min & Max
109 |       potentialValues.push(min, max);
110 | 
111 |       // In case origin value is align with mark but not with step
112 |       potentialValues.push(formatStepValue(originValue));
113 | 
114 |       // Put offset step value also
115 |       const sign = offset > 0 ? 1 : -1;
116 | 
117 |       if (mode === 'unit') {
118 |         potentialValues.push(formatStepValue(originValue + sign * step));
119 |       } else {
120 |         potentialValues.push(formatStepValue(targetDistValue));
121 |       }
122 | 
123 |       // Find close one
124 |       potentialValues = potentialValues
125 |         .filter((val) => val !== null)
126 |         // Remove reverse value
127 |         .filter((val) => (offset < 0 ? val <= originValue : val >= originValue));
128 | 
129 |       if (mode === 'unit') {
130 |         // `unit` mode can not contain itself
131 |         potentialValues = potentialValues.filter((val) => val !== originValue);
132 |       }
133 | 
134 |       const compareValue = mode === 'unit' ? originValue : targetDistValue;
135 | 
136 |       nextValue = potentialValues[0];
137 |       let valueDist = Math.abs(nextValue - compareValue);
138 | 
139 |       potentialValues.forEach((potentialValue) => {
140 |         const dist = Math.abs(potentialValue - compareValue);
141 |         if (dist < valueDist) {
142 |           nextValue = potentialValue;
143 |           valueDist = dist;
144 |         }
145 |       });
146 | 
147 |       // Out of range will back to range
148 |       if (nextValue === undefined) {
149 |         return offset < 0 ? min : max;
150 |       }
151 | 
152 |       // `dist` mode
153 |       if (mode === 'dist') {
154 |         return nextValue;
155 |       }
156 | 
157 |       // `unit` mode may need another round
158 |       if (Math.abs(offset) > 1) {
159 |         const cloneValues = [...values];
160 |         cloneValues[valueIndex] = nextValue;
161 | 
162 |         return offsetValue(cloneValues, offset - sign, valueIndex, mode);
163 |       }
164 | 
165 |       return nextValue;
166 |     } else if (offset === 'min') {
167 |       return min;
168 |     } else if (offset === 'max') {
169 |       return max;
170 |     }
171 |   };
172 | 
173 |   /** Same as `offsetValue` but return `changed` mark to tell value changed */
174 |   const offsetChangedValue = (
175 |     values: number[],
176 |     offset: number,
177 |     valueIndex: number,
178 |     mode: OffsetMode = 'unit',
179 |   ) => {
180 |     const originValue = values[valueIndex];
181 |     const nextValue = offsetValue(values, offset, valueIndex, mode);
182 |     return {
183 |       value: nextValue,
184 |       changed: nextValue !== originValue,
185 |     };
186 |   };
187 | 
188 |   const needPush = (dist: number) => {
189 |     return (pushable === null && dist === 0) || (typeof pushable === 'number' && dist < pushable);
190 |   };
191 | 
192 |   // Values
193 |   const offsetValues: OffsetValues = (values, offset, valueIndex, mode = 'unit') => {
194 |     const nextValues = values.map<number>(formatValue);
195 |     const originValue = nextValues[valueIndex];
196 |     const nextValue = offsetValue(nextValues, offset, valueIndex, mode);
197 |     nextValues[valueIndex] = nextValue;
198 | 
199 |     if (allowCross === false) {
200 |       // >>>>> Allow Cross
201 |       const pushNum = pushable || 0;
202 | 
203 |       // ============ AllowCross ===============
204 |       if (valueIndex > 0 && nextValues[valueIndex - 1] !== originValue) {
205 |         nextValues[valueIndex] = Math.max(
206 |           nextValues[valueIndex],
207 |           nextValues[valueIndex - 1] + pushNum,
208 |         );
209 |       }
210 | 
211 |       if (valueIndex < nextValues.length - 1 && nextValues[valueIndex + 1] !== originValue) {
212 |         nextValues[valueIndex] = Math.min(
213 |           nextValues[valueIndex],
214 |           nextValues[valueIndex + 1] - pushNum,
215 |         );
216 |       }
217 |     } else if (typeof pushable === 'number' || pushable === null) {
218 |       // >>>>> Pushable
219 |       // =============== Push ==================
220 | 
221 |       // >>>>>> Basic push
222 |       // End values
223 |       for (let i = valueIndex + 1; i < nextValues.length; i += 1) {
224 |         let changed = true;
225 |         while (needPush(nextValues[i] - nextValues[i - 1]) && changed) {
226 |           ({ value: nextValues[i], changed } = offsetChangedValue(nextValues, 1, i));
227 |         }
228 |       }
229 | 
230 |       // Start values
231 |       for (let i = valueIndex; i > 0; i -= 1) {
232 |         let changed = true;
233 |         while (needPush(nextValues[i] - nextValues[i - 1]) && changed) {
234 |           ({ value: nextValues[i - 1], changed } = offsetChangedValue(nextValues, -1, i - 1));
235 |         }
236 |       }
237 | 
238 |       // >>>>> Revert back to safe push range
239 |       // End to Start
240 |       for (let i = nextValues.length - 1; i > 0; i -= 1) {
241 |         let changed = true;
242 |         while (needPush(nextValues[i] - nextValues[i - 1]) && changed) {
243 |           ({ value: nextValues[i - 1], changed } = offsetChangedValue(nextValues, -1, i - 1));
244 |         }
245 |       }
246 | 
247 |       // Start to End
248 |       for (let i = 0; i < nextValues.length - 1; i += 1) {
249 |         let changed = true;
250 |         while (needPush(nextValues[i + 1] - nextValues[i]) && changed) {
251 |           ({ value: nextValues[i + 1], changed } = offsetChangedValue(nextValues, 1, i + 1));
252 |         }
253 |       }
254 |     }
255 | 
256 |     return {
257 |       value: nextValues[valueIndex],
258 |       values: nextValues,
259 |     };
260 |   };
261 | 
262 |   return [formatValue, offsetValues];
263 | }
264 | 


--------------------------------------------------------------------------------
/src/hooks/useRange.ts:
--------------------------------------------------------------------------------
 1 | import { warning } from 'rc-util/lib/warning';
 2 | import { useMemo } from 'react';
 3 | import type { SliderProps } from '../Slider';
 4 | 
 5 | export default function useRange(
 6 |   range?: SliderProps['range'],
 7 | ): [
 8 |   range: boolean,
 9 |   rangeEditable: boolean,
10 |   rangeDraggableTrack: boolean,
11 |   minCount: number,
12 |   maxCount?: number,
13 | ] {
14 |   return useMemo(() => {
15 |     if (range === true || !range) {
16 |       return [!!range, false, false, 0];
17 |     }
18 | 
19 |     const { editable, draggableTrack, minCount, maxCount } = range;
20 | 
21 |     if (process.env.NODE_ENV !== 'production') {
22 |       warning(!editable || !draggableTrack, '`editable` can not work with `draggableTrack`.');
23 |     }
24 | 
25 |     return [true, editable, !editable && draggableTrack, minCount || 0, maxCount];
26 |   }, [range]);
27 | }
28 | 


--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import type { SliderProps, SliderRef } from './Slider';
2 | import Slider from './Slider';
3 | export { UnstableContext } from './context';
4 | 
5 | export type { SliderProps, SliderRef };
6 | 
7 | export default Slider;
8 | 


--------------------------------------------------------------------------------
/src/interface.ts:
--------------------------------------------------------------------------------
 1 | import type React from 'react';
 2 | 
 3 | export type Direction = 'rtl' | 'ltr' | 'ttb' | 'btt';
 4 | 
 5 | export type OnStartMove = (
 6 |   e: React.MouseEvent | React.TouchEvent,
 7 |   valueIndex: number,
 8 |   startValues?: number[],
 9 | ) => void;
10 | 
11 | export type AriaValueFormat = (value: number) => string;
12 | 
13 | export type SemanticName = 'tracks' | 'track' | 'rail' | 'handle';
14 | 
15 | export type SliderClassNames = Partial<Record<SemanticName, string>>;
16 | 
17 | export type SliderStyles = Partial<Record<SemanticName, React.CSSProperties>>;
18 | 


--------------------------------------------------------------------------------
/src/util.ts:
--------------------------------------------------------------------------------
 1 | import type { Direction } from './interface';
 2 | 
 3 | export function getOffset(value: number, min: number, max: number) {
 4 |   return (value - min) / (max - min);
 5 | }
 6 | 
 7 | export function getDirectionStyle(direction: Direction, value: number, min: number, max: number) {
 8 |   const offset = getOffset(value, min, max);
 9 | 
10 |   const positionStyle: React.CSSProperties = {};
11 | 
12 |   switch (direction) {
13 |     case 'rtl':
14 |       positionStyle.right = `${offset * 100}%`;
15 |       positionStyle.transform = 'translateX(50%)';
16 |       break;
17 | 
18 |     case 'btt':
19 |       positionStyle.bottom = `${offset * 100}%`;
20 |       positionStyle.transform = 'translateY(50%)';
21 |       break;
22 | 
23 |     case 'ttb':
24 |       positionStyle.top = `${offset * 100}%`;
25 |       positionStyle.transform = 'translateY(-50%)';
26 |       break;
27 | 
28 |     default:
29 |       positionStyle.left = `${offset * 100}%`;
30 |       positionStyle.transform = 'translateX(-50%)';
31 |       break;
32 |   }
33 | 
34 |   return positionStyle;
35 | }
36 | 
37 | /** Return index value if is list or return value directly */
38 | export function getIndex<T>(value: T | T[], index: number) {
39 |   return Array.isArray(value) ? value[index] : value;
40 | }
41 | 


--------------------------------------------------------------------------------
/tests/Range.test.tsx:
--------------------------------------------------------------------------------
  1 | /* eslint-disable max-len, no-undef, react/no-string-refs, no-param-reassign, max-classes-per-file */
  2 | import '@testing-library/jest-dom';
  3 | import { createEvent, fireEvent, render } from '@testing-library/react';
  4 | import keyCode from 'rc-util/lib/KeyCode';
  5 | import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
  6 | import { resetWarned } from 'rc-util/lib/warning';
  7 | import React from 'react';
  8 | import Slider from '../src';
  9 | 
 10 | describe('Range', () => {
 11 |   beforeAll(() => {
 12 |     spyElementPrototypes(HTMLElement, {
 13 |       getBoundingClientRect: () => ({
 14 |         width: 100,
 15 |         height: 100,
 16 |         left: 0,
 17 |         top: 0,
 18 |         bottom: 100,
 19 |         right: 100,
 20 |       }),
 21 |     });
 22 |   });
 23 | 
 24 |   beforeEach(() => {
 25 |     resetWarned();
 26 |   });
 27 | 
 28 |   function doMouseDown(
 29 |     container: HTMLElement,
 30 |     start: number,
 31 |     element = 'rc-slider-handle',
 32 |     skipEventCheck = false,
 33 |   ) {
 34 |     const ele = container.getElementsByClassName(element)[0];
 35 |     const mouseDown = createEvent.mouseDown(ele);
 36 |     (mouseDown as any).pageX = start;
 37 |     (mouseDown as any).pageY = start;
 38 | 
 39 |     const preventDefault = jest.fn();
 40 | 
 41 |     Object.defineProperties(mouseDown, {
 42 |       clientX: { get: () => start },
 43 |       clientY: { get: () => start },
 44 |       preventDefault: { value: preventDefault },
 45 |     });
 46 | 
 47 |     fireEvent.mouseEnter(ele);
 48 |     fireEvent(ele, mouseDown);
 49 | 
 50 |     // Should not prevent default since focus will not change
 51 |     if (!skipEventCheck) {
 52 |       expect(preventDefault).not.toHaveBeenCalled();
 53 |     }
 54 |   }
 55 | 
 56 |   function doMouseDrag(end: number) {
 57 |     const mouseMove = createEvent.mouseMove(document);
 58 |     (mouseMove as any).pageX = end;
 59 |     (mouseMove as any).pageY = end;
 60 |     fireEvent(document, mouseMove);
 61 |   }
 62 | 
 63 |   function doMouseMove(
 64 |     container: HTMLElement,
 65 |     start: number,
 66 |     end: number,
 67 |     element = 'rc-slider-handle',
 68 |   ) {
 69 |     doMouseDown(container, start, element);
 70 | 
 71 |     // Drag
 72 |     doMouseDrag(end);
 73 |   }
 74 | 
 75 |   function doTouchMove(
 76 |     container: HTMLElement,
 77 |     start: number,
 78 |     end: number,
 79 |     element = 'rc-slider-handle',
 80 |   ) {
 81 |     const touchStart = createEvent.touchStart(container.getElementsByClassName(element)[0], {
 82 |       touches: [{}],
 83 |       targetTouches: [{}],
 84 |     });
 85 |     (touchStart as any).targetTouches[0].pageX = start;
 86 |     fireEvent(container.getElementsByClassName(element)[0], touchStart);
 87 | 
 88 |     // Drag
 89 |     const touchMove = createEvent.touchMove(container.getElementsByClassName(element)[0], {
 90 |       touches: [{}],
 91 |       targetTouches: [{}],
 92 |     });
 93 |     (touchMove as any).targetTouches[0].pageX = end;
 94 |     fireEvent(container.getElementsByClassName(element)[0], touchMove);
 95 |   }
 96 | 
 97 |   it('should render Range with correct DOM structure', () => {
 98 |     const { asFragment } = render(<Slider range />);
 99 |     expect(asFragment().firstChild).toMatchSnapshot();
100 |   });
101 | 
102 |   it('should render Multi-Range with correct DOM structure', () => {
103 |     const { asFragment } = render(<Slider range count={3} />);
104 |     expect(asFragment().firstChild).toMatchSnapshot();
105 |   });
106 | 
107 |   it('should render Range with value correctly', async () => {
108 |     const { container } = render(<Slider range value={[0, 50]} />);
109 | 
110 |     expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveStyle('left: 0%');
111 |     expect(container.getElementsByClassName('rc-slider-handle')[1]).toHaveStyle('left: 50%');
112 | 
113 |     expect(container.getElementsByClassName('rc-slider-track')[0]).toHaveStyle(
114 |       'left: 0%; width: 50%',
115 |     );
116 |   });
117 | 
118 |   it('should render reverse Range with value correctly', () => {
119 |     const { container } = render(<Slider range value={[0, 50]} reverse />);
120 | 
121 |     expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveStyle('right: 0%');
122 |     expect(container.getElementsByClassName('rc-slider-handle')[1]).toHaveStyle('right: 50%');
123 | 
124 |     expect(container.getElementsByClassName('rc-slider-track')[0]).toHaveStyle(
125 |       'right: 0%; width: 50%',
126 |     );
127 |   });
128 | 
129 |   it('should render Range with tabIndex correctly', () => {
130 |     const { container } = render(<Slider range tabIndex={[1, 2]} />);
131 | 
132 |     expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
133 |       'tabIndex',
134 |       '1',
135 |     );
136 |     expect(container.getElementsByClassName('rc-slider-handle')[1]).toHaveAttribute(
137 |       'tabIndex',
138 |       '2',
139 |     );
140 |   });
141 | 
142 |   it('should render Range without tabIndex (equal null) correctly', () => {
143 |     const { container } = render(<Slider range tabIndex={[null, null]} />);
144 |     expect(container.getElementsByClassName('rc-slider-handle')[0]).not.toHaveAttribute('tabIndex');
145 |     expect(container.getElementsByClassName('rc-slider-handle')[1]).not.toHaveAttribute('tabIndex');
146 |   });
147 | 
148 |   it('it should trigger onAfterChange when key pressed', () => {
149 |     const onAfterChange = jest.fn();
150 |     const { container } = render(
151 |       <Slider range defaultValue={[20, 50]} onChangeComplete={onAfterChange} />,
152 |     );
153 | 
154 |     fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[1], {
155 |       keyCode: keyCode.RIGHT,
156 |     });
157 |     expect(onAfterChange).not.toHaveBeenCalled();
158 | 
159 |     fireEvent.keyUp(container.getElementsByClassName('rc-slider-handle')[1], {
160 |       keyCode: keyCode.RIGHT,
161 |     });
162 | 
163 |     expect(onAfterChange).toHaveBeenCalled();
164 |   });
165 | 
166 |   it('should not change value from keyboard events when disabled', () => {
167 |     const onAfterChange = jest.fn();
168 |     const { container } = render(
169 |       <Slider range keyboard={false} defaultValue={[20, 50]} onChangeComplete={onAfterChange} />,
170 |     );
171 | 
172 |     fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[1], {
173 |       keyCode: keyCode.RIGHT,
174 |     });
175 | 
176 |     expect(onAfterChange).not.toBeCalled();
177 |   });
178 | 
179 |   it('should render Multi-Range with value correctly', () => {
180 |     const { container } = render(<Slider range count={3} value={[0, 25, 50, 75]} />);
181 | 
182 |     expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveStyle('left: 0%');
183 |     expect(container.getElementsByClassName('rc-slider-handle')[1]).toHaveStyle('left: 25%');
184 |     expect(container.getElementsByClassName('rc-slider-handle')[2]).toHaveStyle('left: 50%');
185 |     expect(container.getElementsByClassName('rc-slider-handle')[3]).toHaveStyle('left: 75%');
186 | 
187 |     expect(container.getElementsByClassName('rc-slider-track')[0]).toHaveStyle(
188 |       'left: 0%; width: 25%',
189 |     );
190 | 
191 |     expect(container.getElementsByClassName('rc-slider-track')[1]).toHaveStyle(
192 |       'left: 25%; width: 25%',
193 |     );
194 | 
195 |     expect(container.getElementsByClassName('rc-slider-track')[2]).toHaveStyle(
196 |       'left: 50%; width: 25%',
197 |     );
198 |   });
199 | 
200 |   it('should update Range correctly in controlled model', () => {
201 |     const { container, rerender } = render(<Slider range value={[2, 4, 6]} />);
202 |     expect(container.getElementsByClassName('rc-slider-handle')).toHaveLength(3);
203 | 
204 |     rerender(<Slider range value={[2, 4]} />);
205 |     expect(container.getElementsByClassName('rc-slider-handle')).toHaveLength(2);
206 |   });
207 | 
208 |   it('not moved if controlled', () => {
209 |     const onChange = jest.fn();
210 |     const { container } = render(<Slider range value={[2, 4, 6]} onChange={onChange} />);
211 |     doMouseMove(container, 0, 9999999);
212 | 
213 |     expect(onChange).toHaveBeenCalled();
214 | 
215 |     expect(container.querySelector('.rc-slider-handle-dragging')).toHaveStyle({
216 |       left: '2%',
217 |     });
218 |   });
219 | 
220 |   // Not trigger onChange anymore
221 |   // it('should only update bounds that are out of range', () => {
222 |   //   const props = { min: 0, max: 10000, value: [0.01, 10000], onChange: jest.fn() };
223 |   //   const range = mount(<Slider range {...props} step={0.1} />);
224 |   //   range.setProps({ min: 0, max: 500 });
225 | 
226 |   //   expect(props.onChange).toHaveBeenCalledWith([0.01, 500]);
227 |   // });
228 | 
229 |   // Not trigger onChange anymore
230 |   // it('should only update bounds if they are out of range', () => {
231 |   //   const props = { min: 0, max: 10000, value: [0.01, 10000], onChange: jest.fn() };
232 |   //   const range = mount(<Slider range {...props} />);
233 |   //   range.setProps({ min: 0, max: 500, value: [0.01, 466] });
234 | 
235 |   //   expect(props.onChange).toHaveBeenCalledTimes(0);
236 |   // });
237 | 
238 |   // https://github.com/react-component/slider/pull/256
239 |   // Move to antd instead
240 |   // it('should handle multi handle mouseEnter correctly', () => {
241 |   //   const wrapper = mount(<Slider range WithTooltip min={0} max={1000} defaultValue={[50, 55]} />);
242 |   //   wrapper.find('.rc-slider-handle').at(1).simulate('mouseEnter');
243 |   //   expect(wrapper.state().visibles[0]).toBe(true);
244 |   //   wrapper.find('.rc-slider-handle').at(3).simulate('mouseEnter');
245 |   //   expect(wrapper.state().visibles[1]).toBe(true);
246 |   //   wrapper.find('.rc-slider-handle').at(1).simulate('mouseLeave');
247 |   //   expect(wrapper.state().visibles[0]).toBe(false);
248 |   //   wrapper.find('.rc-slider-handle').at(3).simulate('mouseLeave');
249 |   //   expect(wrapper.state().visibles[1]).toBe(false);
250 |   // });
251 | 
252 |   it('should keep pushable when not allowCross', () => {
253 |     const onChange = jest.fn();
254 |     const { container } = render(
255 |       <Slider range allowCross={false} onChange={onChange} defaultValue={[29, 40]} pushable={10} />,
256 |     );
257 | 
258 |     fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
259 |       keyCode: keyCode.UP,
260 |     });
261 |     expect(onChange).toHaveBeenCalledWith([30, 40]);
262 | 
263 |     onChange.mockReset();
264 |     fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
265 |       keyCode: keyCode.UP,
266 |     });
267 |     expect(onChange).not.toHaveBeenCalled();
268 | 
269 |     onChange.mockReset();
270 |     fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[1], {
271 |       keyCode: keyCode.UP,
272 |     });
273 |     expect(onChange).toHaveBeenCalledWith([30, 41]);
274 | 
275 |     // Push to the edge
276 |     for (let i = 0; i < 99; i += 1) {
277 |       fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[1], {
278 |         keyCode: keyCode.DOWN,
279 |       });
280 |     }
281 |     expect(onChange).toHaveBeenCalledWith([30, 40]);
282 | 
283 |     onChange.mockReset();
284 |     fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[1], {
285 |       keyCode: keyCode.DOWN,
286 |     });
287 |     expect(onChange).not.toHaveBeenCalled();
288 |   });
289 | 
290 |   it('pushable & allowCross', () => {
291 |     const onChange = jest.fn();
292 |     const { container } = render(
293 |       <Slider range onChange={onChange} defaultValue={[10, 30, 50]} pushable={10} />,
294 |     );
295 | 
296 |     // Left to Right
297 |     for (let i = 0; i < 99; i += 1) {
298 |       fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
299 |         keyCode: keyCode.UP,
300 |       });
301 |     }
302 |     expect(onChange).toHaveBeenCalledWith([80, 90, 100]);
303 | 
304 |     // Center to Left
305 |     for (let i = 0; i < 99; i += 1) {
306 |       fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[1], {
307 |         keyCode: keyCode.DOWN,
308 |       });
309 |     }
310 |     expect(onChange).toHaveBeenCalledWith([0, 10, 100]);
311 | 
312 |     // Right to Right
313 |     for (let i = 0; i < 99; i += 1) {
314 |       fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[2], {
315 |         keyCode: keyCode.DOWN,
316 |       });
317 |     }
318 |     expect(onChange).toHaveBeenCalledWith([0, 10, 20]);
319 | 
320 |     // Center to Right
321 |     for (let i = 0; i < 99; i += 1) {
322 |       fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[1], {
323 |         keyCode: keyCode.UP,
324 |       });
325 |     }
326 |     expect(onChange).toHaveBeenCalledWith([0, 90, 100]);
327 |   });
328 | 
329 |   describe('should render correctly when allowCross', () => {
330 |     function testLTR(name, func) {
331 |       it(name, () => {
332 |         const onChange = jest.fn();
333 |         const { container, unmount } = render(
334 |           <Slider range onChange={onChange} defaultValue={[20, 40]} />,
335 |         );
336 | 
337 |         // Do move
338 |         func(container);
339 | 
340 |         expect(onChange).toHaveBeenCalledWith([40, 100]);
341 | 
342 |         unmount();
343 |       });
344 |     }
345 | 
346 |     testLTR('mouse', (container) => doMouseMove(container, 0, 9999));
347 |     testLTR('touch', (container) => doTouchMove(container, 0, 9999));
348 | 
349 |     it('reverse', () => {
350 |       const onChange = jest.fn();
351 |       const { container } = render(
352 |         <Slider range onChange={onChange} defaultValue={[20, 40]} reverse />,
353 |       );
354 | 
355 |       // Do move
356 |       doMouseMove(container, 0, -10);
357 | 
358 |       expect(onChange).toHaveBeenCalledWith([30, 40]);
359 |     });
360 | 
361 |     it('vertical', () => {
362 |       const onChange = jest.fn();
363 |       const { container } = render(
364 |         <Slider range onChange={onChange} defaultValue={[20, 40]} vertical />,
365 |       );
366 | 
367 |       // Do move
368 |       doMouseMove(container, 0, -10);
369 | 
370 |       expect(onChange).toHaveBeenCalledWith([30, 40]);
371 |     });
372 | 
373 |     it('vertical & reverse', () => {
374 |       const onChange = jest.fn();
375 |       const { container } = render(
376 |         <Slider range onChange={onChange} defaultValue={[20, 40]} vertical reverse />,
377 |       );
378 | 
379 |       // Do move
380 |       doMouseMove(container, 0, -10);
381 | 
382 |       expect(onChange).toHaveBeenCalledWith([10, 40]);
383 |     });
384 |   });
385 | 
386 |   describe('should keep pushable with pushable s defalutValue when not allowCross and setState', () => {
387 |     function test(name, func) {
388 |       it(name, () => {
389 |         const onChange = jest.fn();
390 | 
391 |         const Demo = () => {
392 |           const [value, setValue] = React.useState([20, 40]);
393 | 
394 |           return (
395 |             <Slider
396 |               range
397 |               onChange={(values: number[]) => {
398 |                 setValue(values);
399 |                 onChange(values);
400 |               }}
401 |               value={value}
402 |               allowCross={false}
403 |               pushable
404 |             />
405 |           );
406 |         };
407 | 
408 |         global.error = true;
409 |         const { container, unmount } = render(<Demo />);
410 | 
411 |         // Do move
412 |         func(container);
413 | 
414 |         expect(onChange).toHaveBeenCalledWith([39, 40]);
415 | 
416 |         unmount();
417 |       });
418 |     }
419 | 
420 |     test('mouse', (container) => doMouseMove(container, 0, 9999));
421 |     test('touch', (container) => doTouchMove(container, 0, 9999));
422 |   });
423 | 
424 |   describe('track draggable', () => {
425 |     function test(name, func) {
426 |       it(name, () => {
427 |         const onChange = jest.fn();
428 | 
429 |         const { container, unmount } = render(
430 |           <Slider range={{ draggableTrack: true }} defaultValue={[0, 30]} onChange={onChange} />,
431 |         );
432 | 
433 |         // Do move
434 |         func(container);
435 | 
436 |         expect(onChange).toHaveBeenCalledWith([20, 50]);
437 | 
438 |         unmount();
439 |       });
440 |     }
441 | 
442 |     test('mouse', (container) => doMouseMove(container, 0, 20, 'rc-slider-track'));
443 |     test('touch', (container) => doTouchMove(container, 0, 20, 'rc-slider-track'));
444 |   });
445 | 
446 |   it('sets aria-orientation to default on the handle', () => {
447 |     const { container } = render(<Slider range />);
448 |     expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
449 |       'aria-orientation',
450 |       'horizontal',
451 |     );
452 |   });
453 | 
454 |   it('sets aria-orientation to vertical on the handles of vertical Slider', () => {
455 |     const { container } = render(<Slider range vertical defaultValue={[0, 20]} />);
456 |     expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
457 |       'aria-orientation',
458 |       'vertical',
459 |     );
460 |     expect(container.getElementsByClassName('rc-slider-handle')[1]).toHaveAttribute(
461 |       'aria-orientation',
462 |       'vertical',
463 |     );
464 |   });
465 | 
466 |   it('sets aria-label on the handles', () => {
467 |     const { container } = render(
468 |       <Slider range ariaLabelForHandle={['Some Label', 'Some other Label']} />,
469 |     );
470 |     expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
471 |       'aria-label',
472 |       'Some Label',
473 |     );
474 |     expect(container.getElementsByClassName('rc-slider-handle')[1]).toHaveAttribute(
475 |       'aria-label',
476 |       'Some other Label',
477 |     );
478 |   });
479 | 
480 |   it('sets aria-labelledby on the handles', () => {
481 |     const { container } = render(
482 |       <Slider range ariaLabelledByForHandle={['some_id', 'some_other_id']} />,
483 |     );
484 |     expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
485 |       'aria-labelledby',
486 |       'some_id',
487 |     );
488 |     expect(container.getElementsByClassName('rc-slider-handle')[1]).toHaveAttribute(
489 |       'aria-labelledby',
490 |       'some_other_id',
491 |     );
492 |   });
493 | 
494 |   it('sets aria-valuetext on the handles', () => {
495 |     const { container } = render(
496 |       <Slider
497 |         range
498 |         min={0}
499 |         max={5}
500 |         defaultValue={[1, 3]}
501 |         ariaValueTextFormatterForHandle={[
502 |           (value) => `${value} of something`,
503 |           (value) => `${value} of something else`,
504 |         ]}
505 |       />,
506 |     );
507 |     expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
508 |       'aria-valuetext',
509 |       '1 of something',
510 |     );
511 |     expect(container.getElementsByClassName('rc-slider-handle')[1]).toHaveAttribute(
512 |       'aria-valuetext',
513 |       '3 of something else',
514 |     );
515 |   });
516 | 
517 |   // Corresponds to the issue described in https://github.com/react-component/slider/issues/690.
518 |   it('should correctly display a dynamically changed number of handles', () => {
519 |     const props = {
520 |       range: true,
521 |       allowCross: false,
522 |       marks: {
523 |         0: { label: '0', style: {} },
524 |         25: { label: '25', style: {} },
525 |         50: { label: '50', style: {} },
526 |         75: { label: '75', style: {} },
527 |         100: { label: '100', style: {} },
528 |       },
529 |       step: null,
530 |     };
531 | 
532 |     const { container, rerender } = render(<Slider {...props} value={[0, 25, 50, 75, 100]} />);
533 | 
534 |     const verifyHandles = (values) => {
535 |       // Has the number of handles that we set.
536 |       expect(container.getElementsByClassName('rc-slider-handle')).toHaveLength(values.length);
537 | 
538 |       // Handles have the values that we set.
539 |       Array.from(container.getElementsByClassName('rc-slider-handle')).forEach((ele, index) => {
540 |         expect(ele).toHaveAttribute('aria-valuenow', values[index].toString());
541 |       });
542 |     };
543 | 
544 |     // Assert that handles are correct initially.
545 |     verifyHandles([0, 25, 50, 75, 100]);
546 | 
547 |     // Assert that handles are correct after decreasing their number.
548 |     rerender(<Slider {...props} value={[0, 75, 100]} />);
549 |     verifyHandles([0, 75, 100]);
550 | 
551 |     // Assert that handles are correct after increasing their number.
552 |     rerender(<Slider {...props} value={[0, 25, 75, 100]} />);
553 |     verifyHandles([0, 25, 75, 100]);
554 |   });
555 | 
556 |   describe('focus & blur', () => {
557 |     it('focus()', () => {
558 |       const handleFocus = jest.fn();
559 |       const { container } = render(<Slider range min={0} max={20} onFocus={handleFocus} />);
560 |       container.querySelector<HTMLDivElement>('.rc-slider-handle').focus();
561 |       expect(handleFocus).toBeCalled();
562 |     });
563 | 
564 |     it('blur()', () => {
565 |       const handleBlur = jest.fn();
566 |       const { container } = render(<Slider range min={0} max={20} onBlur={handleBlur} />);
567 |       container.querySelector<HTMLDivElement>('.rc-slider-handle').focus();
568 |       container.querySelector<HTMLDivElement>('.rc-slider-handle').blur();
569 |       expect(handleBlur).toHaveBeenCalled();
570 |     });
571 |   });
572 | 
573 |   it('warning for `draggableTrack` and `mergedStep=null`', () => {
574 |     const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
575 | 
576 |     render(<Slider range={{ draggableTrack: true }} step={null} />);
577 | 
578 |     expect(errorSpy).toHaveBeenCalledWith(
579 |       'Warning: `draggableTrack` is not supported when `step` is `null`.',
580 |     );
581 |     errorSpy.mockRestore();
582 |   });
583 | 
584 |   it('Track should have the correct thickness', () => {
585 |     const { container } = render(
586 |       <Slider range={{ draggableTrack: true }} allowCross={false} reverse defaultValue={[0, 40]} />,
587 |     );
588 | 
589 |     const { container: containerVertical } = render(
590 |       <Slider
591 |         range={{ draggableTrack: true }}
592 |         allowCross={false}
593 |         reverse
594 |         defaultValue={[0, 40]}
595 |         vertical
596 |         style={{ height: '300px' }}
597 |       />,
598 |     );
599 |     expect(container.querySelector('.rc-slider-track-draggable')).toBeTruthy();
600 |     expect(containerVertical.querySelector('.rc-slider-track-draggable')).toBeTruthy();
601 |   });
602 | 
603 |   it('styles', () => {
604 |     const { container } = render(
605 |       <Slider
606 |         range
607 |         value={[0, 10]}
608 |         styles={{
609 |           tracks: { backgroundColor: '#654321' },
610 |           track: { backgroundColor: '#123456' },
611 |           handle: { backgroundColor: '#112233' },
612 |           rail: { backgroundColor: '#332211' },
613 |         }}
614 |       />,
615 |     );
616 | 
617 |     expect(container.querySelector('.rc-slider-tracks')).toHaveStyle({
618 |       backgroundColor: '#654321',
619 |     });
620 |     expect(container.querySelector('.rc-slider-track')).toHaveStyle({
621 |       backgroundColor: '#123456',
622 |     });
623 |     expect(container.querySelector('.rc-slider-handle')).toHaveStyle({
624 |       backgroundColor: '#112233',
625 |     });
626 |     expect(container.querySelector('.rc-slider-rail')).toHaveStyle({
627 |       backgroundColor: '#332211',
628 |     });
629 |   });
630 | 
631 |   it('classNames', () => {
632 |     const { container } = render(
633 |       <Slider
634 |         range
635 |         value={[0, 10]}
636 |         classNames={{
637 |           tracks: 'my-tracks',
638 |           track: 'my-track',
639 |           handle: 'my-handle',
640 |           rail: 'my-rail',
641 |         }}
642 |       />,
643 |     );
644 | 
645 |     expect(container.querySelector('.rc-slider-tracks')).toHaveClass('my-tracks');
646 |     expect(container.querySelector('.rc-slider-track')).toHaveClass('my-track');
647 |     expect(container.querySelector('.rc-slider-handle')).toHaveClass('my-handle');
648 |     expect(container.querySelector('.rc-slider-rail')).toHaveClass('my-rail');
649 |   });
650 | 
651 |   describe('editable', () => {
652 |     it('click to create', () => {
653 |       const onChange = jest.fn();
654 |       const { container } = render(
655 |         <Slider
656 |           onChange={onChange}
657 |           min={0}
658 |           max={100}
659 |           value={[0, 100]}
660 |           range={{ editable: true }}
661 |         />,
662 |       );
663 | 
664 |       doMouseDown(container, 50, 'rc-slider', true);
665 | 
666 |       expect(onChange).toHaveBeenCalledWith([0, 50, 100]);
667 |     });
668 | 
669 |     it('can not editable with draggableTrack at same time', () => {
670 |       const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
671 |       render(<Slider range={{ editable: true, draggableTrack: true }} />);
672 | 
673 |       expect(errorSpy).toHaveBeenCalledWith(
674 |         'Warning: `editable` can not work with `draggableTrack`.',
675 |       );
676 |       errorSpy.mockRestore();
677 |     });
678 | 
679 |     describe('drag out to remove', () => {
680 |       it('uncontrolled', () => {
681 |         const onChange = jest.fn();
682 |         const onChangeComplete = jest.fn();
683 |         const { container } = render(
684 |           <Slider
685 |             onChange={onChange}
686 |             onChangeComplete={onChangeComplete}
687 |             min={0}
688 |             max={100}
689 |             defaultValue={[0, 50, 100]}
690 |             range={{ editable: true }}
691 |           />,
692 |         );
693 | 
694 |         doMouseMove(container, 0, 1000);
695 |         expect(onChange).toHaveBeenCalledWith([50, 100]);
696 | 
697 |         expect(container.querySelectorAll('.rc-slider-track')).toHaveLength(1);
698 | 
699 |         // Fire mouse up
700 |         fireEvent.mouseUp(container.querySelector('.rc-slider-handle'));
701 |         expect(onChangeComplete).toHaveBeenCalledWith([50, 100]);
702 |       });
703 | 
704 |       it('out and back', () => {
705 |         const onChange = jest.fn();
706 |         const onChangeComplete = jest.fn();
707 |         const { container } = render(
708 |           <Slider
709 |             onChange={onChange}
710 |             onChangeComplete={onChangeComplete}
711 |             min={0}
712 |             max={100}
713 |             defaultValue={[0, 50]}
714 |             range={{ editable: true }}
715 |           />,
716 |         );
717 | 
718 |         doMouseMove(container, 0, 1000);
719 |         expect(onChange).toHaveBeenCalledWith([50]);
720 | 
721 |         doMouseDrag(0);
722 |         expect(onChange).toHaveBeenCalledWith([0, 50]);
723 | 
724 |         // Fire mouse up
725 |         fireEvent.mouseUp(container.querySelector('.rc-slider-handle'));
726 |         expect(onChangeComplete).toHaveBeenCalledWith([0, 50]);
727 |       });
728 | 
729 |       it('controlled', () => {
730 |         const onChange = jest.fn();
731 |         const onChangeComplete = jest.fn();
732 | 
733 |         const Demo = () => {
734 |           const [value, setValue] = React.useState([0, 50, 100]);
735 |           return (
736 |             <Slider
737 |               onChange={(nextValue: number[]) => {
738 |                 onChange(nextValue);
739 |                 setValue(nextValue);
740 |               }}
741 |               onChangeComplete={onChangeComplete}
742 |               min={0}
743 |               max={100}
744 |               value={value}
745 |               range={{ editable: true }}
746 |             />
747 |           );
748 |         };
749 | 
750 |         const { container } = render(<Demo />);
751 | 
752 |         doMouseMove(container, 0, 1000);
753 |         expect(onChange).toHaveBeenCalledWith([50, 100]);
754 | 
755 |         // Fire mouse up
756 |         fireEvent.mouseUp(container.querySelector('.rc-slider-handle'));
757 |         expect(onChangeComplete).toHaveBeenCalledWith([50, 100]);
758 |       });
759 |     });
760 | 
761 |     it('key to delete', () => {
762 |       const onChange = jest.fn();
763 | 
764 |       const { container } = render(
765 |         <Slider
766 |           onChange={onChange}
767 |           min={0}
768 |           max={100}
769 |           defaultValue={[0, 50, 100]}
770 |           range={{ editable: true }}
771 |           // Test for active handle render
772 |           activeHandleRender={(ori) => ori}
773 |         />,
774 |       );
775 | 
776 |       const handle = container.querySelectorAll('.rc-slider-handle')[1];
777 | 
778 |       fireEvent.mouseEnter(handle);
779 |       fireEvent.keyDown(handle, {
780 |         keyCode: keyCode.DELETE,
781 |       });
782 | 
783 |       expect(onChange).toHaveBeenCalledWith([0, 100]);
784 | 
785 |       // Clear all
786 |       fireEvent.keyDown(container.querySelector('.rc-slider-handle'), {
787 |         keyCode: keyCode.DELETE,
788 |       });
789 |       fireEvent.keyDown(container.querySelector('.rc-slider-handle'), {
790 |         keyCode: keyCode.DELETE,
791 |       });
792 |       expect(onChange).toHaveBeenCalledWith([]);
793 | 
794 |       // 2 handle
795 |       expect(container.querySelectorAll('.rc-slider-handle')).toHaveLength(0);
796 |     });
797 | 
798 |     it('not remove when minCount', () => {
799 |       const onChange = jest.fn();
800 | 
801 |       const { container } = render(
802 |         <Slider
803 |           onChange={onChange}
804 |           min={0}
805 |           max={100}
806 |           defaultValue={[0]}
807 |           range={{ editable: true, minCount: 1 }}
808 |           activeHandleRender={(ori) => ori}
809 |         />,
810 |       );
811 | 
812 |       const handle = container.querySelector('.rc-slider-handle');
813 | 
814 |       // Key
815 |       fireEvent.mouseEnter(handle);
816 |       fireEvent.keyDown(handle, {
817 |         keyCode: keyCode.DELETE,
818 |       });
819 |       expect(onChange).not.toHaveBeenCalled();
820 | 
821 |       // Mouse
822 |       doMouseMove(container, 0, 1000);
823 |       expect(onChange).toHaveBeenCalledWith([100]);
824 |     });
825 | 
826 |     it('maxCount not add', () => {
827 |       const onChange = jest.fn();
828 |       const { container } = render(
829 |         <Slider
830 |           onChange={onChange}
831 |           min={0}
832 |           max={100}
833 |           value={[0, 100]}
834 |           range={{ editable: true, maxCount: 2 }}
835 |         />,
836 |       );
837 | 
838 |       doMouseDown(container, 50, 'rc-slider', true);
839 |       expect(onChange).toHaveBeenCalledWith([0, 50]);
840 |     });
841 |   });
842 | });
843 | 


--------------------------------------------------------------------------------
/tests/Slider.test.js:
--------------------------------------------------------------------------------
  1 | import '@testing-library/jest-dom';
  2 | import { createEvent, fireEvent, render } from '@testing-library/react';
  3 | import classNames from 'classnames';
  4 | import keyCode from 'rc-util/lib/KeyCode';
  5 | import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
  6 | import React from 'react';
  7 | import Slider from '../src/Slider';
  8 | 
  9 | describe('Slider', () => {
 10 |   beforeAll(() => {
 11 |     spyElementPrototypes(HTMLElement, {
 12 |       getBoundingClientRect: () => ({
 13 |         top: 0,
 14 |         bottom: 100,
 15 |         left: 0,
 16 |         right: 100,
 17 |         width: 100,
 18 |         height: 100,
 19 |       }),
 20 |     });
 21 |   });
 22 | 
 23 |   it('should render Slider with correct DOM structure', () => {
 24 |     const { asFragment } = render(<Slider />);
 25 |     expect(asFragment().firstChild).toMatchSnapshot();
 26 |   });
 27 | 
 28 |   it('should render Slider with value correctly', () => {
 29 |     const { container } = render(<Slider value={50} />);
 30 |     expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveStyle({ left: '50%' });
 31 |     expect(container.getElementsByClassName('rc-slider-track')[0]).toHaveStyle({
 32 |       left: '0%',
 33 |       width: '50%',
 34 |     });
 35 |   });
 36 | 
 37 |   it('should render Slider correctly where value > startPoint', () => {
 38 |     const { container } = render(<Slider value={50} startPoint={20} />);
 39 |     expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveStyle({ left: '50%' });
 40 |     expect(container.getElementsByClassName('rc-slider-track')[0]).toHaveStyle({
 41 |       left: '20%',
 42 |       width: '30%',
 43 |     });
 44 |   });
 45 | 
 46 |   it('should render Slider correctly where value < startPoint', () => {
 47 |     const { container } = render(<Slider value={40} startPoint={60} />);
 48 |     expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveStyle({ left: '40%' });
 49 |     expect(container.getElementsByClassName('rc-slider-track')[0]).toHaveStyle({
 50 |       left: '40%',
 51 |       width: '20%',
 52 |     });
 53 |   });
 54 | 
 55 |   it('should render reverse Slider with value correctly', () => {
 56 |     const { container } = render(<Slider value={50} reverse />);
 57 |     expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveStyle({ right: '50%' });
 58 |     expect(container.getElementsByClassName('rc-slider-track')[0]).toHaveStyle({
 59 |       right: '0%',
 60 |       width: '50%',
 61 |     });
 62 |   });
 63 | 
 64 |   it('should render reverse Slider correctly where value > startPoint', () => {
 65 |     const { container } = render(<Slider value={50} startPoint={20} reverse />);
 66 |     expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveStyle({ right: '50%' });
 67 |     expect(container.getElementsByClassName('rc-slider-track')[0]).toHaveStyle({
 68 |       right: '20%',
 69 |       width: '30%',
 70 |     });
 71 |   });
 72 | 
 73 |   it('should render reverse Slider correctly where value < startPoint', () => {
 74 |     const { container } = render(<Slider value={30} startPoint={50} reverse />);
 75 |     expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveStyle({ right: '30%' });
 76 |     expect(container.getElementsByClassName('rc-slider-track')[0]).toHaveStyle({
 77 |       right: '30%',
 78 |       width: '20%',
 79 |     });
 80 |   });
 81 | 
 82 |   it('should render reverse Slider with marks correctly', () => {
 83 |     const marks = { 5: '5', 6: '6', 7: '7', 8: '8', 9: '9', 10: '10' };
 84 |     const { container } = render(<Slider value={0} marks={marks} min={5} max={10} reverse />);
 85 |     expect(container.getElementsByClassName('rc-slider-mark-text')[0]).toHaveStyle({ right: '0%' });
 86 |   });
 87 | 
 88 |   it('should render Slider without handle if value is null', () => {
 89 |     const { asFragment } = render(<Slider value={null} />);
 90 |     expect(asFragment().firstChild).toMatchSnapshot();
 91 |   });
 92 | 
 93 |   it('should allow tabIndex to be set on Handle via Slider', () => {
 94 |     const { container } = render(<Slider tabIndex={1} />);
 95 |     expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
 96 |       'tabIndex',
 97 |       '1',
 98 |     );
 99 |   });
100 | 
101 |   it('should allow tabIndex to be set on Handle via Slider and be equal null', () => {
102 |     const { container } = render(<Slider tabIndex={null} />);
103 |     expect(container.getElementsByClassName('rc-slider-handle')[0]).not.toHaveAttribute('tabIndex');
104 |   });
105 | 
106 |   it('increases the value when key "up" is pressed', () => {
107 |     const onChange = jest.fn();
108 |     const { container } = render(<Slider defaultValue={50} onChange={onChange} />);
109 | 
110 |     fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
111 |       keyCode: keyCode.UP,
112 |     });
113 | 
114 |     expect(onChange).toHaveBeenCalledWith(51);
115 |   });
116 | 
117 |   it('decreases the value for reverse-vertical when key "up" is pressed', () => {
118 |     const onChange = jest.fn();
119 |     const { container } = render(<Slider defaultValue={50} onChange={onChange} reverse vertical />);
120 | 
121 |     fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
122 |       keyCode: keyCode.UP,
123 |     });
124 | 
125 |     expect(onChange).toHaveBeenCalledWith(49);
126 |   });
127 | 
128 |   it('increases the value when key "right" is pressed', () => {
129 |     const onChange = jest.fn();
130 |     const { container } = render(<Slider defaultValue={50} onChange={onChange} />);
131 | 
132 |     fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
133 |       keyCode: keyCode.RIGHT,
134 |     });
135 | 
136 |     expect(onChange).toHaveBeenCalledWith(51);
137 |   });
138 | 
139 |   it('it should trigger onAfterChange when key pressed', () => {
140 |     const onAfterChange = jest.fn();
141 |     const { container } = render(<Slider defaultValue={50} onChangeComplete={onAfterChange} />);
142 | 
143 |     fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
144 |       keyCode: keyCode.RIGHT,
145 |     });
146 | 
147 |     expect(onAfterChange).not.toHaveBeenCalled();
148 | 
149 |     fireEvent.keyUp(container.getElementsByClassName('rc-slider-handle')[0], {
150 |       keyCode: keyCode.RIGHT,
151 |     });
152 | 
153 |     expect(onAfterChange).toHaveBeenCalled();
154 |   });
155 | 
156 |   it('decreases the value for reverse-horizontal when key "right" is pressed', () => {
157 |     const onChange = jest.fn();
158 |     const { container } = render(<Slider defaultValue={50} reverse onChange={onChange} />);
159 | 
160 |     fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
161 |       keyCode: keyCode.RIGHT,
162 |     });
163 | 
164 |     expect(onChange).toHaveBeenCalledWith(49);
165 |   });
166 | 
167 |   it('increases the value when key "page up" is pressed, by a factor 2', () => {
168 |     const onChange = jest.fn();
169 |     const { container } = render(<Slider defaultValue={50} onChange={onChange} />);
170 | 
171 |     fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
172 |       keyCode: keyCode.PAGE_UP,
173 |     });
174 | 
175 |     expect(onChange).toHaveBeenCalledWith(52);
176 |   });
177 | 
178 |   it('decreases the value when key "down" is pressed', () => {
179 |     const onChange = jest.fn();
180 |     const { container } = render(<Slider defaultValue={50} onChange={onChange} />);
181 | 
182 |     fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
183 |       keyCode: keyCode.DOWN,
184 |     });
185 | 
186 |     expect(onChange).toHaveBeenCalledWith(49);
187 |   });
188 | 
189 |   it('decreases the value when key "left" is pressed', () => {
190 |     const onChange = jest.fn();
191 |     const onChangeComplete = jest.fn();
192 |     const { container } = render(
193 |       <Slider defaultValue={50} onChange={onChange} onChangeComplete={onChangeComplete} />,
194 |     );
195 | 
196 |     fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
197 |       keyCode: keyCode.LEFT,
198 |     });
199 | 
200 |     expect(onChange).toHaveBeenCalledWith(49);
201 |     expect(onChangeComplete).not.toHaveBeenCalled();
202 | 
203 |     fireEvent.keyUp(container.getElementsByClassName('rc-slider-handle')[0], {
204 |       keyCode: keyCode.LEFT,
205 |     });
206 | 
207 |     expect(onChangeComplete).toHaveBeenCalled();
208 |   });
209 | 
210 |   it('it should work fine when arrow key is pressed', () => {
211 |     const onChange = jest.fn();
212 |     const { container } = render(<Slider range defaultValue={[20, 50]} onChange={onChange} />);
213 | 
214 |     fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[1], {
215 |       keyCode: keyCode.LEFT,
216 |     });
217 |     expect(onChange).toHaveBeenCalledWith([20, 49]);
218 | 
219 |     fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[1], {
220 |       keyCode: keyCode.RIGHT,
221 |     });
222 |     expect(onChange).toHaveBeenCalledWith([20, 50]);
223 | 
224 |     fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[1], {
225 |       keyCode: keyCode.UP,
226 |     });
227 |     expect(onChange).toHaveBeenCalledWith([20, 51]);
228 | 
229 |     fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[1], {
230 |       keyCode: keyCode.DOWN,
231 |     });
232 |     expect(onChange).toHaveBeenCalledWith([20, 50]);
233 |   });
234 | 
235 |   it('decreases the value when key "page down" is pressed, by a factor 2', () => {
236 |     const onChange = jest.fn();
237 |     const { container } = render(<Slider defaultValue={50} onChange={onChange} />);
238 | 
239 |     fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
240 |       keyCode: keyCode.PAGE_DOWN,
241 |     });
242 | 
243 |     expect(onChange).toHaveBeenCalledWith(48);
244 |   });
245 | 
246 |   it('sets the value to minimum when key "home" is pressed', () => {
247 |     const onChange = jest.fn();
248 |     const { container } = render(<Slider defaultValue={50} onChange={onChange} />);
249 | 
250 |     fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
251 |       keyCode: keyCode.HOME,
252 |     });
253 | 
254 |     expect(onChange).toHaveBeenCalledWith(0);
255 |   });
256 | 
257 |   it('sets the value to maximum when the key "end" is pressed', () => {
258 |     const onChange = jest.fn();
259 |     const { container } = render(<Slider defaultValue={50} onChange={onChange} />);
260 | 
261 |     fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
262 |       keyCode: keyCode.END,
263 |     });
264 | 
265 |     expect(onChange).toHaveBeenCalledWith(100);
266 |   });
267 | 
268 |   describe('when component has fixed values', () => {
269 |     it('increases the value when key "up" is pressed', () => {
270 |       const onChange = jest.fn();
271 |       const { container } = render(
272 |         <Slider
273 |           min={20}
274 |           defaultValue={40}
275 |           marks={{ 20: 20, 40: 40, 100: 100 }}
276 |           step={null}
277 |           onChange={onChange}
278 |         />,
279 |       );
280 |       fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
281 |         keyCode: keyCode.UP,
282 |       });
283 |       expect(onChange).toHaveBeenCalledWith(100);
284 |     });
285 | 
286 |     it('increases the value when key "right" is pressed', () => {
287 |       const onChange = jest.fn();
288 |       const { container } = render(
289 |         <Slider
290 |           min={20}
291 |           defaultValue={40}
292 |           marks={{ 20: 20, 40: 40, 100: 100 }}
293 |           step={null}
294 |           onChange={onChange}
295 |         />,
296 |       );
297 |       fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
298 |         keyCode: keyCode.RIGHT,
299 |       });
300 |       expect(onChange).toHaveBeenCalledWith(100);
301 |     });
302 | 
303 |     it('decreases the value when key "down" is pressed', () => {
304 |       const onChange = jest.fn();
305 |       const { container } = render(
306 |         <Slider
307 |           min={20}
308 |           defaultValue={40}
309 |           marks={{ 20: 20, 40: 40, 100: 100 }}
310 |           step={null}
311 |           onChange={onChange}
312 |         />,
313 |       );
314 |       fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
315 |         keyCode: keyCode.DOWN,
316 |       });
317 |       expect(onChange).toHaveBeenCalledWith(20);
318 |     });
319 | 
320 |     it('decreases the value when key "left" is pressed', () => {
321 |       const onChange = jest.fn();
322 |       const { container } = render(
323 |         <Slider
324 |           min={20}
325 |           defaultValue={40}
326 |           marks={{ 20: 20, 40: 40, 100: 100 }}
327 |           step={null}
328 |           onChange={onChange}
329 |         />,
330 |       );
331 |       fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
332 |         keyCode: keyCode.LEFT,
333 |       });
334 |       expect(onChange).toHaveBeenCalledWith(20);
335 |     });
336 | 
337 |     it('sets the value to minimum when key "home" is pressed', () => {
338 |       const onChange = jest.fn();
339 |       const { container } = render(
340 |         <Slider
341 |           min={20}
342 |           defaultValue={100}
343 |           marks={{ 20: 20, 40: 40, 100: 100 }}
344 |           step={null}
345 |           onChange={onChange}
346 |         />,
347 |       );
348 |       fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
349 |         keyCode: keyCode.HOME,
350 |       });
351 |       expect(onChange).toHaveBeenCalledWith(20);
352 |     });
353 | 
354 |     it('sets the value to maximum when the key "end" is pressed', () => {
355 |       const onChange = jest.fn();
356 |       const { container } = render(
357 |         <Slider
358 |           min={20}
359 |           defaultValue={20}
360 |           marks={{ 20: 20, 40: 40, 100: 100 }}
361 |           step={null}
362 |           onChange={onChange}
363 |         />,
364 |       );
365 |       fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
366 |         keyCode: keyCode.END,
367 |       });
368 |       expect(onChange).toHaveBeenCalledWith(100);
369 |     });
370 |   });
371 | 
372 |   it('keyboard mix with step & marks', () => {
373 |     const onChange = jest.fn();
374 | 
375 |     // [0], 3, 7, 10
376 |     const { container } = render(
377 |       <Slider
378 |         min={0}
379 |         max={10}
380 |         step={10}
381 |         defaultValue={0}
382 |         marks={{ 3: 3, 7: 7 }}
383 |         onChange={onChange}
384 |       />,
385 |     );
386 |     const handler = container.getElementsByClassName('rc-slider-handle')[0];
387 | 
388 |     // 0, [3], 7, 10
389 |     fireEvent.keyDown(handler, { keyCode: keyCode.UP });
390 |     expect(onChange).toHaveBeenCalledWith(3);
391 | 
392 |     // 0, 3, [7], 10
393 |     onChange.mockReset();
394 |     fireEvent.keyDown(handler, { keyCode: keyCode.UP });
395 |     expect(onChange).toHaveBeenCalledWith(7);
396 | 
397 |     // 0, 3, 7, [10]
398 |     onChange.mockReset();
399 |     fireEvent.keyDown(handler, { keyCode: keyCode.UP });
400 |     expect(onChange).toHaveBeenCalledWith(10);
401 | 
402 |     // 0, 3, 7, [10]
403 |     onChange.mockReset();
404 |     fireEvent.keyDown(handler, { keyCode: keyCode.UP });
405 |     expect(onChange).not.toHaveBeenCalled();
406 | 
407 |     // 0, 3, [7], 10
408 |     onChange.mockReset();
409 |     fireEvent.keyDown(handler, { keyCode: keyCode.DOWN });
410 |     expect(onChange).toHaveBeenCalledWith(7);
411 | 
412 |     // 0, [3], 7, 10
413 |     onChange.mockReset();
414 |     fireEvent.keyDown(handler, { keyCode: keyCode.DOWN });
415 |     expect(onChange).toHaveBeenCalledWith(3);
416 | 
417 |     // [0], 3, 7, 10
418 |     onChange.mockReset();
419 |     fireEvent.keyDown(handler, { keyCode: keyCode.DOWN });
420 |     expect(onChange).toHaveBeenCalledWith(0);
421 | 
422 |     // [0], 3, 7, 10
423 |     onChange.mockReset();
424 |     fireEvent.keyDown(handler, { keyCode: keyCode.DOWN });
425 |     expect(onChange).not.toHaveBeenCalled();
426 |   });
427 | 
428 |   it('sets aria-label on the handle', () => {
429 |     const { container } = render(<Slider ariaLabelForHandle="Some Label" />);
430 |     expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
431 |       'aria-label',
432 |       'Some Label',
433 |     );
434 |   });
435 | 
436 |   it('sets aria-labelledby on the handle', () => {
437 |     const { container } = render(<Slider ariaLabelledByForHandle="some_id" />);
438 |     expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
439 |       'aria-labelledby',
440 |       'some_id',
441 |     );
442 |   });
443 | 
444 |   it('sets aria-required on the handle', () => {
445 |     const { container } = render(<Slider ariaRequired={true} />);
446 |     expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
447 |       'aria-required',
448 |       'true',
449 |     );
450 |   });
451 | 
452 |   it('sets aria-valuetext on the handle', () => {
453 |     const { container } = render(
454 |       <Slider
455 |         min={0}
456 |         max={5}
457 |         defaultValue={3}
458 |         ariaValueTextFormatterForHandle={(value) => `${value} of something`}
459 |       />,
460 |     );
461 |     const handle = container.getElementsByClassName('rc-slider-handle')[0];
462 |     expect(handle).toHaveAttribute('aria-valuetext', '3 of something');
463 | 
464 |     fireEvent.keyDown(handle, { keyCode: keyCode.RIGHT });
465 |     expect(handle).toHaveAttribute('aria-valuetext', '4 of something');
466 |   });
467 | 
468 |   describe('focus & blur', () => {
469 |     it('focus', () => {
470 |       const handleFocus = jest.fn();
471 |       const { container, unmount } = render(
472 |         <Slider min={0} max={10} defaultValue={0} onFocus={handleFocus} />,
473 |       );
474 |       container.getElementsByClassName('rc-slider-handle')[0].focus();
475 |       expect(handleFocus).toBeCalled();
476 | 
477 |       unmount();
478 |     });
479 | 
480 |     it('blur', () => {
481 |       const handleBlur = jest.fn();
482 |       const { container, unmount } = render(
483 |         <Slider min={0} max={10} defaultValue={0} onBlur={handleBlur} />,
484 |       );
485 |       container.getElementsByClassName('rc-slider-handle')[0].focus();
486 |       container.getElementsByClassName('rc-slider-handle')[0].blur();
487 |       expect(handleBlur).toBeCalled();
488 | 
489 |       unmount();
490 |     });
491 | 
492 |     it('ref focus & blur', () => {
493 |       const onFocus = jest.fn();
494 |       const onBlur = jest.fn();
495 |       const ref = React.createRef();
496 |       render(<Slider ref={ref} onFocus={onFocus} onBlur={onBlur} />);
497 | 
498 |       ref.current.focus();
499 |       expect(onFocus).toBeCalled();
500 | 
501 |       ref.current.blur();
502 |       expect(onBlur).toBeCalled();
503 |     });
504 |   });
505 | 
506 |   it('should not be out of range when value is null', () => {
507 |     const { container, rerender } = render(<Slider value={null} min={1} max={10} />);
508 |     expect(container.getElementsByClassName('rc-slider-track')).toHaveLength(0);
509 | 
510 |     rerender(<Slider value={0} min={1} max={10} />);
511 |     expect(container.getElementsByClassName('rc-slider-track')).toHaveLength(1);
512 |   });
513 | 
514 |   describe('click slider to change value', () => {
515 |     it('ltr', () => {
516 |       const onChange = jest.fn();
517 |       const { container } = render(<Slider onChange={onChange} />);
518 |       fireEvent.mouseDown(container.querySelector('.rc-slider'), {
519 |         clientX: 20,
520 |       });
521 | 
522 |       expect(onChange).toHaveBeenCalledWith(20);
523 |     });
524 | 
525 |     it('rtl', () => {
526 |       const onChange = jest.fn();
527 |       const { container } = render(<Slider onChange={onChange} reverse />);
528 |       fireEvent.mouseDown(container.querySelector('.rc-slider'), {
529 |         clientX: 20,
530 |       });
531 | 
532 |       expect(onChange).toHaveBeenCalledWith(80);
533 |     });
534 | 
535 |     it('btt', () => {
536 |       const onChange = jest.fn();
537 |       const { container } = render(<Slider onChange={onChange} vertical />);
538 |       fireEvent.mouseDown(container.querySelector('.rc-slider'), {
539 |         clientY: 93,
540 |       });
541 | 
542 |       expect(onChange).toHaveBeenCalledWith(7);
543 |     });
544 | 
545 |     it('ttb', () => {
546 |       const onChange = jest.fn();
547 |       const { container } = render(<Slider onChange={onChange} vertical reverse />);
548 |       fireEvent.mouseDown(container.querySelector('.rc-slider'), {
549 |         clientY: 93,
550 |       });
551 | 
552 |       expect(onChange).toHaveBeenCalledWith(93);
553 |     });
554 | 
555 |     it('null value click to become 2 values', () => {
556 |       const onChange = jest.fn();
557 |       const { container } = render(<Slider defaultValue={null} range onChange={onChange} />);
558 |       fireEvent.mouseDown(container.querySelector('.rc-slider'), {
559 |         clientX: 20,
560 |       });
561 | 
562 |       expect(onChange).toHaveBeenCalledWith([20, 20]);
563 |     });
564 | 
565 |     it('should call onBeforeChange, onChange, and onAfterChange', () => {
566 |       const onBeforeChange = jest.fn();
567 |       const onChange = jest.fn();
568 |       const onAfterChange = jest.fn();
569 |       const { container } = render(
570 |         <Slider
571 |           onBeforeChange={onBeforeChange}
572 |           onChange={onChange}
573 |           onChangeComplete={onAfterChange}
574 |         />,
575 |       );
576 |       fireEvent.mouseDown(container.querySelector('.rc-slider'), {
577 |         clientX: 20,
578 |       });
579 | 
580 |       expect(onBeforeChange).toHaveBeenCalledWith(20);
581 |       expect(onChange).toHaveBeenCalledWith(20);
582 |       expect(onAfterChange).not.toHaveBeenCalled();
583 |       fireEvent.mouseUp(container.querySelector('.rc-slider'), {
584 |         clientX: 20,
585 |       });
586 |       expect(onAfterChange).toHaveBeenCalledWith(20);
587 |     });
588 |   });
589 | 
590 |   it('autoFocus', () => {
591 |     const onFocus = jest.fn();
592 |     render(<Slider autoFocus onFocus={onFocus} />);
593 | 
594 |     expect(onFocus).toHaveBeenCalled();
595 |   });
596 | 
597 |   it('custom handle', () => {
598 |     const { container } = render(
599 |       <Slider
600 |         handleRender={(node) =>
601 |           React.cloneElement(node, {
602 |             className: classNames(node.props.className, 'custom-handle'),
603 |           })
604 |         }
605 |       />,
606 |     );
607 | 
608 |     expect(container.querySelector('.custom-handle')).toBeTruthy();
609 |   });
610 | 
611 |   // https://github.com/ant-design/ant-design/issues/34020
612 |   it('max value not align with step', () => {
613 |     const onChange = jest.fn();
614 |     const { container } = render(
615 |       <Slider min={0.5} max={2} step={1} defaultValue={1.5} onChange={onChange} />,
616 |     );
617 |     fireEvent.keyDown(container.querySelector('.rc-slider-handle'), { keyCode: keyCode.RIGHT });
618 | 
619 |     expect(onChange).toHaveBeenCalledWith(2);
620 |     expect(container.querySelector('.rc-slider-handle').style.left).toBe('100%');
621 |   });
622 | 
623 |   it('not show decimal', () => {
624 |     const onChange = jest.fn();
625 |     const { container } = render(
626 |       <Slider min={0} max={1} step={0.01} defaultValue={0.81} onChange={onChange} />,
627 |     );
628 |     fireEvent.keyDown(container.querySelector('.rc-slider-handle'), { keyCode: keyCode.RIGHT });
629 |     expect(onChange).toHaveBeenCalledWith(0.82);
630 |   });
631 | 
632 |   it('onAfterChange should return number', () => {
633 |     const onAfterChange = jest.fn();
634 |     const { container } = render(<Slider onChangeComplete={onAfterChange} />);
635 |     fireEvent.mouseDown(container.querySelector('.rc-slider'), {
636 |       clientX: 20,
637 |     });
638 |     expect(onAfterChange).not.toHaveBeenCalled();
639 |     fireEvent.mouseUp(container.querySelector('.rc-slider'), {
640 |       clientX: 20,
641 |     });
642 |     expect(onAfterChange).toHaveBeenCalledWith(20);
643 |   });
644 | 
645 |   // https://github.com/react-component/slider/pull/948
646 |   it('could drag handler after click tracker', () => {
647 |     const onChange = jest.fn();
648 |     const { container } = render(<Slider onChange={onChange} />);
649 |     fireEvent.mouseDown(container.querySelector('.rc-slider'), {
650 |       clientX: 20,
651 |     });
652 |     expect(onChange).toHaveBeenLastCalledWith(20);
653 | 
654 |     // Drag
655 |     const mouseMove = createEvent.mouseMove(document);
656 |     mouseMove.pageX = 100;
657 |     fireEvent(document, mouseMove);
658 |     expect(onChange).toHaveBeenLastCalledWith(100);
659 |   });
660 | 
661 |   it('should render Slider with included=false', () => {
662 |     const { asFragment } = render(<Slider included={false} />);
663 |     expect(asFragment().firstChild).toMatchSnapshot();
664 |   });
665 | 
666 |   it('tipFormatter should not crash with undefined value', () => {
667 |     [undefined, null].forEach((value) => {
668 |       render(<Slider value={value} tooltip={{ open: true }} styles={{ tracks: {} }}/>);
669 |     });
670 |   });
671 | });
672 | 


--------------------------------------------------------------------------------
/tests/Tooltip.test.js:
--------------------------------------------------------------------------------
 1 | import '@testing-library/jest-dom';
 2 | import { fireEvent, render } from '@testing-library/react';
 3 | import React from 'react';
 4 | import Slider from '../src/Slider';
 5 | 
 6 | describe('Slider.Tooltip', () => {
 7 |   it('internal activeHandleRender support', () => {
 8 |     const { container } = render(
 9 |       <Slider
10 |         range
11 |         defaultValue={[20, 50]}
12 |         activeHandleRender={(node, info) =>
13 |           React.cloneElement(node, {
14 |             'data-test': 'bamboo',
15 |             'data-value': info.value,
16 |           })
17 |         }
18 |       />,
19 |     );
20 | 
21 |     // Click second
22 |     fireEvent.mouseEnter(container.querySelectorAll('.rc-slider-handle')[1]);
23 |     expect(container.querySelector('.rc-slider-handle[data-test]')).toBeTruthy();
24 |     expect(
25 |       container.querySelector('.rc-slider-handle[data-value]').getAttribute('data-value'),
26 |     ).toBe('50');
27 |   });
28 | });
29 | 


--------------------------------------------------------------------------------
/tests/__mocks__/rc-trigger.js:
--------------------------------------------------------------------------------
1 | import Trigger from 'rc-trigger/lib/mock';
2 | 
3 | export default Trigger;
4 | 


--------------------------------------------------------------------------------
/tests/__snapshots__/Range.test.tsx.snap:
--------------------------------------------------------------------------------
  1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
  2 | 
  3 | exports[`Range should render Multi-Range with correct DOM structure 1`] = `
  4 | <div
  5 |   class="rc-slider rc-slider-horizontal"
  6 | >
  7 |   <div
  8 |     class="rc-slider-rail"
  9 |   />
 10 |   <div
 11 |     class="rc-slider-track rc-slider-track-1"
 12 |     style="left: 0%; width: 0%;"
 13 |   />
 14 |   <div
 15 |     class="rc-slider-track rc-slider-track-2"
 16 |     style="left: 0%; width: 0%;"
 17 |   />
 18 |   <div
 19 |     class="rc-slider-track rc-slider-track-3"
 20 |     style="left: 0%; width: 0%;"
 21 |   />
 22 |   <div
 23 |     class="rc-slider-step"
 24 |   />
 25 |   <div
 26 |     aria-disabled="false"
 27 |     aria-orientation="horizontal"
 28 |     aria-valuemax="100"
 29 |     aria-valuemin="0"
 30 |     aria-valuenow="0"
 31 |     class="rc-slider-handle rc-slider-handle-1"
 32 |     role="slider"
 33 |     style="left: 0%; transform: translateX(-50%);"
 34 |     tabindex="0"
 35 |   />
 36 |   <div
 37 |     aria-disabled="false"
 38 |     aria-orientation="horizontal"
 39 |     aria-valuemax="100"
 40 |     aria-valuemin="0"
 41 |     aria-valuenow="0"
 42 |     class="rc-slider-handle rc-slider-handle-2"
 43 |     role="slider"
 44 |     style="left: 0%; transform: translateX(-50%);"
 45 |     tabindex="0"
 46 |   />
 47 |   <div
 48 |     aria-disabled="false"
 49 |     aria-orientation="horizontal"
 50 |     aria-valuemax="100"
 51 |     aria-valuemin="0"
 52 |     aria-valuenow="0"
 53 |     class="rc-slider-handle rc-slider-handle-3"
 54 |     role="slider"
 55 |     style="left: 0%; transform: translateX(-50%);"
 56 |     tabindex="0"
 57 |   />
 58 |   <div
 59 |     aria-disabled="false"
 60 |     aria-orientation="horizontal"
 61 |     aria-valuemax="100"
 62 |     aria-valuemin="0"
 63 |     aria-valuenow="0"
 64 |     class="rc-slider-handle rc-slider-handle-4"
 65 |     role="slider"
 66 |     style="left: 0%; transform: translateX(-50%);"
 67 |     tabindex="0"
 68 |   />
 69 | </div>
 70 | `;
 71 | 
 72 | exports[`Range should render Range with correct DOM structure 1`] = `
 73 | <div
 74 |   class="rc-slider rc-slider-horizontal"
 75 | >
 76 |   <div
 77 |     class="rc-slider-rail"
 78 |   />
 79 |   <div
 80 |     class="rc-slider-track rc-slider-track-1"
 81 |     style="left: 0%; width: 0%;"
 82 |   />
 83 |   <div
 84 |     class="rc-slider-step"
 85 |   />
 86 |   <div
 87 |     aria-disabled="false"
 88 |     aria-orientation="horizontal"
 89 |     aria-valuemax="100"
 90 |     aria-valuemin="0"
 91 |     aria-valuenow="0"
 92 |     class="rc-slider-handle rc-slider-handle-1"
 93 |     role="slider"
 94 |     style="left: 0%; transform: translateX(-50%);"
 95 |     tabindex="0"
 96 |   />
 97 |   <div
 98 |     aria-disabled="false"
 99 |     aria-orientation="horizontal"
100 |     aria-valuemax="100"
101 |     aria-valuemin="0"
102 |     aria-valuenow="0"
103 |     class="rc-slider-handle rc-slider-handle-2"
104 |     role="slider"
105 |     style="left: 0%; transform: translateX(-50%);"
106 |     tabindex="0"
107 |   />
108 | </div>
109 | `;
110 | 


--------------------------------------------------------------------------------
/tests/__snapshots__/Slider.test.js.snap:
--------------------------------------------------------------------------------
 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
 2 | 
 3 | exports[`Slider should render Slider with correct DOM structure 1`] = `
 4 | <div
 5 |   class="rc-slider rc-slider-horizontal"
 6 | >
 7 |   <div
 8 |     class="rc-slider-rail"
 9 |   />
10 |   <div
11 |     class="rc-slider-track"
12 |     style="left: 0%; width: 0%;"
13 |   />
14 |   <div
15 |     class="rc-slider-step"
16 |   />
17 |   <div
18 |     aria-disabled="false"
19 |     aria-orientation="horizontal"
20 |     aria-valuemax="100"
21 |     aria-valuemin="0"
22 |     aria-valuenow="0"
23 |     class="rc-slider-handle"
24 |     role="slider"
25 |     style="left: 0%; transform: translateX(-50%);"
26 |     tabindex="0"
27 |   />
28 | </div>
29 | `;
30 | 
31 | exports[`Slider should render Slider with included=false 1`] = `
32 | <div
33 |   class="rc-slider rc-slider-horizontal"
34 | >
35 |   <div
36 |     class="rc-slider-rail"
37 |   />
38 |   <div
39 |     class="rc-slider-step"
40 |   />
41 |   <div
42 |     aria-disabled="false"
43 |     aria-orientation="horizontal"
44 |     aria-valuemax="100"
45 |     aria-valuemin="0"
46 |     aria-valuenow="0"
47 |     class="rc-slider-handle"
48 |     role="slider"
49 |     style="left: 0%; transform: translateX(-50%);"
50 |     tabindex="0"
51 |   />
52 | </div>
53 | `;
54 | 
55 | exports[`Slider should render Slider without handle if value is null 1`] = `
56 | <div
57 |   class="rc-slider rc-slider-horizontal"
58 | >
59 |   <div
60 |     class="rc-slider-rail"
61 |   />
62 |   <div
63 |     class="rc-slider-step"
64 |   />
65 | </div>
66 | `;
67 | 


--------------------------------------------------------------------------------
/tests/common.test.js:
--------------------------------------------------------------------------------
  1 | /* eslint-disable max-len, no-undef */
  2 | import '@testing-library/jest-dom';
  3 | import { createEvent, fireEvent, render } from '@testing-library/react';
  4 | import KeyCode from 'rc-util/lib/KeyCode';
  5 | import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
  6 | import React from 'react';
  7 | import Slider from '../src';
  8 | 
  9 | // const setWidth = (object, width) => {
 10 | //   // https://github.com/tmpvar/jsdom/commit/0cdb2efcc69b6672dc2928644fc0172df5521176
 11 | //   Object.defineProperty(object, 'getBoundingClientRect', {
 12 | //     value: () => ({
 13 | //       width,
 14 | //       // Let all other values retain the JSDom default of `0`.
 15 | //       bottom: 0,
 16 | //       height: 0,
 17 | //       left: 0,
 18 | //       right: 0,
 19 | //       top: 0,
 20 | //     }),
 21 | //     enumerable: true,
 22 | //     configurable: true,
 23 | //   });
 24 | // };
 25 | 
 26 | describe('Common', () => {
 27 |   beforeAll(() => {
 28 |     spyElementPrototypes(HTMLElement, {
 29 |       getBoundingClientRect: () => ({
 30 |         width: 100,
 31 |         height: 100,
 32 |       }),
 33 |     });
 34 |   });
 35 | 
 36 |   it('should render vertical Slider/Range, when `vertical` is true', () => {
 37 |     const { container: container1 } = render(<Slider vertical />);
 38 |     expect(container1.getElementsByClassName('rc-slider-vertical')).toHaveLength(1);
 39 | 
 40 |     const { container: container2 } = render(<Slider range vertical />);
 41 |     expect(container2.getElementsByClassName('rc-slider-vertical')).toHaveLength(1);
 42 |   });
 43 | 
 44 |   it('should render dots correctly when `dots=true`', () => {
 45 |     const { container: container1 } = render(<Slider value={50} step={10} dots />);
 46 |     expect(container1.getElementsByClassName('rc-slider-dot')).toHaveLength(11);
 47 |     expect(container1.getElementsByClassName('rc-slider-dot-active')).toHaveLength(6);
 48 | 
 49 |     const { container: container2 } = render(<Slider range value={[20, 50]} step={10} dots />);
 50 |     expect(container2.getElementsByClassName('rc-slider-dot')).toHaveLength(11);
 51 |     expect(container2.getElementsByClassName('rc-slider-dot-active')).toHaveLength(4);
 52 |   });
 53 | 
 54 |   it('should render normally when `dots=true` and `step=null`', () => {
 55 |     const { container } = render(<Slider step={null} dots />);
 56 |     expect(() => container).not.toThrowError();
 57 |   });
 58 | 
 59 |   it('should render dots correctly when dotStyle is dynamic`', () => {
 60 |     const { container: container1 } = render(
 61 |       <Slider value={50} step={10} dots dotStyle={(dotValue) => ({ width: `${dotValue}px` })} />,
 62 |     );
 63 |     expect(container1.getElementsByClassName('rc-slider-dot')[1]).toHaveStyle(
 64 |       'left: 10%; transform: translateX(-50%); width: 10px',
 65 |     );
 66 |     expect(container1.getElementsByClassName('rc-slider-dot')[2]).toHaveStyle(
 67 |       'left: 20%; transform: translateX(-50%); width: 20px',
 68 |     );
 69 | 
 70 |     const { container: container2 } = render(
 71 |       <Slider
 72 |         range
 73 |         value={[20, 50]}
 74 |         step={10}
 75 |         dots
 76 |         activeDotStyle={(dotValue) => ({ width: `${dotValue}px` })}
 77 |       />,
 78 |     );
 79 |     expect(container2.getElementsByClassName('rc-slider-dot-active')[1]).toHaveStyle(
 80 |       'left: 30%; transform: translateX(-50%); width: 30px',
 81 |     );
 82 |     expect(container2.getElementsByClassName('rc-slider-dot-active')[2]).toHaveStyle(
 83 |       'left: 40%; transform: translateX(-50%); width: 40px',
 84 |     );
 85 |   });
 86 | 
 87 |   it('should not set value greater than `max` or smaller `min`', () => {
 88 |     const { container: container1 } = render(<Slider value={0} min={10} />);
 89 |     expect(
 90 |       container1.getElementsByClassName('rc-slider-handle')[0].getAttribute('aria-valuenow'),
 91 |     ).toBe('10');
 92 | 
 93 |     const { container: container2 } = render(<Slider value={100} max={90} />);
 94 |     expect(
 95 |       container2.getElementsByClassName('rc-slider-handle')[0].getAttribute('aria-valuenow'),
 96 |     ).toBe('90');
 97 | 
 98 |     const { container: container3 } = render(<Slider range value={[0, 100]} min={10} max={90} />);
 99 |     expect(
100 |       container3.getElementsByClassName('rc-slider-handle')[0].getAttribute('aria-valuenow'),
101 |     ).toBe('10');
102 |     expect(
103 |       container3.getElementsByClassName('rc-slider-handle')[1].getAttribute('aria-valuenow'),
104 |     ).toBe('90');
105 |   });
106 | 
107 |   it('should not set values when sending invalid numbers', () => {
108 |     const { container: container1 } = render(<Slider value={0} min={Math.min()} />);
109 |     expect(
110 |       container1.getElementsByClassName('rc-slider-handle')[0].getAttribute('aria-valuenow'),
111 |     ).toBe('0');
112 | 
113 |     const { container: container2 } = render(<Slider value={100} max={Math.max()} />);
114 |     expect(
115 |       container2.getElementsByClassName('rc-slider-handle')[0].getAttribute('aria-valuenow'),
116 |     ).toBe('100');
117 | 
118 |     const { container: container3 } = render(
119 |       <Slider range value={[0, 100]} min={Math.min()} max={Math.max()} />,
120 |     );
121 |     expect(
122 |       container3.getElementsByClassName('rc-slider-handle')[0].getAttribute('aria-valuenow'),
123 |     ).toBe('0');
124 |     expect(
125 |       container3.getElementsByClassName('rc-slider-handle')[1].getAttribute('aria-valuenow'),
126 |     ).toBe('100');
127 |   });
128 | 
129 |   it('should update value when it is out of range', () => {
130 |     const sliderOnChange = jest.fn();
131 |     const { container: container1, rerender: rerender1 } = render(
132 |       <Slider onChange={sliderOnChange} />,
133 |     );
134 |     rerender1(<Slider onChange={sliderOnChange} min={10} />);
135 |     expect(
136 |       container1.getElementsByClassName('rc-slider-handle')[0].getAttribute('aria-valuenow'),
137 |     ).toBe('10');
138 | 
139 |     const rangeOnChange = jest.fn();
140 |     const { container: container2, rerender: rerender2 } = render(
141 |       <Slider range onChange={rangeOnChange} />,
142 |     );
143 |     rerender2(<Slider range onChange={rangeOnChange} min={10} />);
144 |     expect(
145 |       container2.getElementsByClassName('rc-slider-handle')[0].getAttribute('aria-valuenow'),
146 |     ).toBe('10');
147 |   });
148 | 
149 |   it('should not trigger onChange when no min and max', () => {
150 |     const sliderOnChange = jest.fn();
151 |     const { container: container1, rerender: rerender1 } = render(
152 |       <Slider onChange={sliderOnChange} />,
153 |     );
154 |     rerender1(<Slider onChange={sliderOnChange} value={100} />);
155 |     expect(
156 |       container1.getElementsByClassName('rc-slider-handle')[0].getAttribute('aria-valuenow'),
157 |     ).toBe('100');
158 |     expect(sliderOnChange).not.toHaveBeenCalled();
159 | 
160 |     const rangeOnChange = jest.fn();
161 |     const { container: container2, rerender: rerender2 } = render(
162 |       <Slider range onChange={rangeOnChange} />,
163 |     );
164 |     rerender2(<Slider range onChange={rangeOnChange} value={[0, 200]} />);
165 |     expect(
166 |       container2.getElementsByClassName('rc-slider-handle')[0].getAttribute('aria-valuenow'),
167 |     ).toBe('0');
168 |     expect(
169 |       container2.getElementsByClassName('rc-slider-handle')[1].getAttribute('aria-valuenow'),
170 |     ).toBe('100');
171 |     expect(rangeOnChange).not.toHaveBeenCalled();
172 |   });
173 | 
174 |   it('should not trigger onChange when value is out of range', () => {
175 |     const sliderOnChange = jest.fn();
176 |     const { container: container1, rerender: rerender1 } = render(
177 |       <Slider value={9} max={10} onChange={sliderOnChange} />,
178 |     );
179 |     rerender1(<Slider value={11} max={10} onChange={sliderOnChange} />);
180 |     expect(
181 |       container1.getElementsByClassName('rc-slider-handle')[0].getAttribute('aria-valuenow'),
182 |     ).toBe('10');
183 |     expect(sliderOnChange).not.toHaveBeenCalled();
184 | 
185 |     const rangeOnChange = jest.fn();
186 |     const { container: container2, rerender: rerender2 } = render(
187 |       <Slider range max={10} onChange={rangeOnChange} />,
188 |     );
189 |     rerender2(<Slider range max={10} onChange={rangeOnChange} value={[0, 100]} />);
190 |     expect(
191 |       container2.getElementsByClassName('rc-slider-handle')[0].getAttribute('aria-valuenow'),
192 |     ).toBe('0');
193 |     expect(
194 |       container2.getElementsByClassName('rc-slider-handle')[1].getAttribute('aria-valuenow'),
195 |     ).toBe('10');
196 |     expect(rangeOnChange).not.toHaveBeenCalled();
197 |   });
198 | 
199 |   it('should not call onChange when value is the same', () => {
200 |     const handler = jest.fn();
201 | 
202 |     const { container: container1 } = render(<Slider onChange={handler} />);
203 |     const handle1 = container1.getElementsByClassName('rc-slider-handle')[0];
204 |     fireEvent.mouseDown(handle1);
205 |     fireEvent.mouseMove(handle1);
206 |     fireEvent.mouseUp(handle1);
207 | 
208 |     const { container: container2 } = render(<Slider range onChange={handler} />);
209 |     const handle2 = container2.getElementsByClassName('rc-slider-handle')[1];
210 |     fireEvent.mouseDown(handle2);
211 |     fireEvent.mouseMove(handle2);
212 |     fireEvent.mouseUp(handle2);
213 | 
214 |     expect(handler).not.toHaveBeenCalled();
215 |   });
216 | 
217 |   // TODO: should update the following test cases for it should test API instead implementation
218 |   // it('should set `dragOffset` to correct value when the left handle is clicked off-center', () => {
219 |   //   const { container } = render(<Slider />);
220 |   //   setWidth(wrapper.instance().sliderRef, 100);
221 |   //   const leftHandle = wrapper
222 |   //     .find('.rc-slider-handle')
223 |   //     .at(1)
224 |   //     .instance();
225 |   //   wrapper.simulate('mousedown', {
226 |   //     type: 'mousedown',
227 |   //     target: leftHandle,
228 |   //     pageX: 5,
229 |   //     button: 0,
230 |   //     stopPropagation() {},
231 |   //     preventDefault() {},
232 |   //   });
233 |   //   expect(wrapper.instance().dragOffset).toBe(5);
234 |   // });
235 | 
236 |   // it('should respect `dragOffset` while dragging the handle via MouseEvents', () => {
237 |   //   const { container } = render(<Slider />);
238 |   //   setWidth(wrapper.instance().sliderRef, 100);
239 |   //   const leftHandle = wrapper
240 |   //     .find('.rc-slider-handle')
241 |   //     .at(1)
242 |   //     .instance();
243 |   //   wrapper.simulate('mousedown', {
244 |   //     type: 'mousedown',
245 |   //     target: leftHandle,
246 |   //     pageX: 5,
247 |   //     button: 0,
248 |   //     stopPropagation() {},
249 |   //     preventDefault() {},
250 |   //   });
251 |   //   expect(wrapper.instance().dragOffset).toBe(5);
252 |   //   wrapper.instance().onMouseMove({
253 |   //     // to propagation
254 |   //     type: 'mousemove',
255 |   //     target: leftHandle,
256 |   //     pageX: 14,
257 |   //     button: 0,
258 |   //     stopPropagation() {},
259 |   //     preventDefault() {},
260 |   //   });
261 |   //   expect(wrapper.instance().getValue()).toBe(9);
262 |   // });
263 | 
264 |   it('should not go to right direction when mouse go to the left', () => {
265 |     const { container } = render(<Slider />);
266 |     const leftHandle = container.getElementsByClassName('rc-slider-handle')[0];
267 | 
268 |     const mouseDown = createEvent.mouseDown(leftHandle);
269 |     mouseDown.pageX = 5;
270 | 
271 |     expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
272 |       'aria-valuenow',
273 |       '0',
274 |     );
275 | 
276 |     const mouseMove = createEvent.mouseMove(leftHandle);
277 |     mouseMove.pageX = 0;
278 | 
279 |     expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
280 |       'aria-valuenow',
281 |       '0',
282 |     );
283 |   });
284 | 
285 |   it('should call onAfterChange when clicked on mark label', () => {
286 |     const labelId = 'to-be-clicked';
287 |     const marks = {
288 |       0: 'some other label',
289 |       100: <span id={labelId}>some label</span>,
290 |     };
291 | 
292 |     const sliderOnChange = jest.fn();
293 |     const sliderOnAfterChange = jest.fn();
294 |     const { container } = render(
295 |       <Slider
296 |         value={0}
297 |         marks={marks}
298 |         onChange={sliderOnChange}
299 |         onChangeComplete={sliderOnAfterChange}
300 |       />,
301 |     );
302 |     const sliderHandleWrapper = container.querySelector(`#${labelId}`);
303 |     fireEvent.mouseDown(sliderHandleWrapper);
304 |     // Simulate propagation
305 |     fireEvent.mouseDown(container.querySelector('.rc-slider'));
306 |     fireEvent.mouseUp(container.querySelector('.rc-slider'));
307 | 
308 |     fireEvent.click(sliderHandleWrapper);
309 |     expect(sliderOnChange).toHaveBeenCalled();
310 |     expect(sliderOnAfterChange).toHaveBeenCalled();
311 | 
312 |     const rangeOnAfterChange = jest.fn();
313 |     const { container: container2 } = render(
314 |       <Slider range value={[0, 1]} marks={marks} onChangeComplete={rangeOnAfterChange} />,
315 |     );
316 |     const rangeHandleWrapper = container2.querySelector(`#${labelId}`);
317 |     fireEvent.click(rangeHandleWrapper);
318 |     // Simulate propagation
319 |     fireEvent.mouseDown(container2.querySelector('.rc-slider'));
320 |     fireEvent.mouseUp(container2.querySelector('.rc-slider'));
321 |     expect(rangeOnAfterChange).toHaveBeenCalled();
322 |   });
323 | 
324 |   it('only call onAfterChange once', () => {
325 |     const sliderOnChange = jest.fn();
326 |     const sliderOnAfterChange = jest.fn();
327 |     const { container } = render(
328 |       <Slider value={0} onChange={sliderOnChange} onChangeComplete={sliderOnAfterChange} />,
329 |     );
330 | 
331 |     fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
332 |       keyCode: KeyCode.UP,
333 |     });
334 | 
335 |     expect(sliderOnChange).toHaveBeenCalled();
336 |     expect(sliderOnAfterChange).not.toHaveBeenCalled();
337 | 
338 |     fireEvent.keyUp(container.getElementsByClassName('rc-slider-handle')[0], {
339 |       keyCode: KeyCode.UP,
340 |     });
341 |     expect(sliderOnAfterChange).toHaveBeenCalled();
342 |     expect(sliderOnAfterChange).toHaveBeenCalledTimes(1);
343 |   });
344 | 
345 |   it('deprecate onAfterChange', () => {
346 |     const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
347 |     const onChangeComplete = jest.fn();
348 |     const onAfterChange = jest.fn();
349 |     const { container } = render(
350 |       <Slider value={0} onChangeComplete={onChangeComplete} onAfterChange={onAfterChange} />,
351 |     );
352 | 
353 |     fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
354 |       keyCode: KeyCode.UP,
355 |     });
356 | 
357 |     expect(onChangeComplete).not.toHaveBeenCalled();
358 |     expect(onAfterChange).not.toHaveBeenCalled();
359 | 
360 |     fireEvent.keyUp(container.getElementsByClassName('rc-slider-handle')[0], {
361 |       keyCode: KeyCode.UP,
362 |     });
363 |     expect(onChangeComplete).toHaveBeenCalledTimes(1);
364 |     expect(onAfterChange).toHaveBeenCalledTimes(1);
365 |     expect(errSpy).toHaveBeenCalledWith(
366 |       'Warning: [rc-slider] `onAfterChange` is deprecated. Please use `onChangeComplete` instead.',
367 |     );
368 |     errSpy.mockRestore();
369 |   });
370 | 
371 |   // Move to antd instead
372 |   // it('the tooltip should be attach to the container with the id tooltip', () => {
373 |   //   const SliderWithTooltip = createSliderWithTooltip(Slider);
374 |   //   const tooltipPrefixer = {
375 |   //     prefixCls: 'slider-tooltip',
376 |   //   };
377 |   //   const tooltipParent = document.createElement('div');
378 |   //   tooltipParent.setAttribute('id', 'tooltip');
379 |   //   const { container } = render(
380 |   //     <SliderWithTooltip
381 |   //       tipProps={tooltipPrefixer}
382 |   //       getTooltipContainer={() => document.getElementById('tooltip')}
383 |   //     />,
384 |   //   );
385 |   //   expect(wrapper.instance().props.getTooltipContainer).toBeTruthy();
386 |   // });
387 | });
388 | 


--------------------------------------------------------------------------------
/tests/marks.test.js:
--------------------------------------------------------------------------------
 1 | /* eslint-disable max-len, no-undef */
 2 | import '@testing-library/jest-dom';
 3 | import { fireEvent, render } from '@testing-library/react';
 4 | import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
 5 | import React from 'react';
 6 | import Slider from '../src';
 7 | 
 8 | describe('marks', () => {
 9 |   beforeAll(() => {
10 |     spyElementPrototypes(HTMLElement, {
11 |       getBoundingClientRect: () => ({
12 |         width: 100,
13 |         height: 100,
14 |       }),
15 |     });
16 |   });
17 | 
18 |   it('should render marks correctly when `marks` is not an empty object', () => {
19 |     const marks = { 0: 0, 30: '30', 99: '', 100: '100' };
20 | 
21 |     const { container } = render(<Slider value={30} marks={marks} />);
22 |     expect(container.getElementsByClassName('rc-slider-mark-text')).toHaveLength(3);
23 |     expect(container.getElementsByClassName('rc-slider-mark-text')[0].innerHTML).toBe('0');
24 |     expect(container.getElementsByClassName('rc-slider-mark-text')[1].innerHTML).toBe('30');
25 |     expect(container.getElementsByClassName('rc-slider-mark-text')[2].innerHTML).toBe('100');
26 | 
27 |     const { container: container2 } = render(<Slider range value={[0, 30]} marks={marks} />);
28 |     expect(container2.getElementsByClassName('rc-slider-mark-text')).toHaveLength(3);
29 |     expect(container2.getElementsByClassName('rc-slider-mark-text')[0].innerHTML).toBe('0');
30 |     expect(container2.getElementsByClassName('rc-slider-mark-text')[1].innerHTML).toBe('30');
31 |     expect(container2.getElementsByClassName('rc-slider-mark-text')[2].innerHTML).toBe('100');
32 | 
33 |     expect(container.querySelector('.rc-slider-with-marks')).toBeTruthy();
34 |   });
35 | 
36 |   it('should select correct value while click on marks', () => {
37 |     const marks = { 0: '0', 30: '30', 100: '100' };
38 |     const onChange = jest.fn();
39 |     const onChangeComplete = jest.fn();
40 |     const { container } = render(<Slider marks={marks} onChange={onChange} onChangeComplete={onChangeComplete} />);
41 |     fireEvent.click(container.getElementsByClassName('rc-slider-mark-text')[1]);
42 |     expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
43 |       'aria-valuenow',
44 |       '30',
45 |     );
46 |     expect(onChange).toHaveBeenCalledTimes(1);
47 |     expect(onChange).toHaveBeenCalledWith(30);
48 |     expect(onChangeComplete).toHaveBeenCalledTimes(1);
49 |     expect(onChangeComplete).toHaveBeenCalledWith(30);
50 |   });
51 | 
52 |   // TODO: not implement yet
53 |   // zombieJ: since this test leave years but not implement. Could we remove this?
54 |   // xit('should select correct value while click on marks in Ranger', () => {
55 |   //   const rangeWrapper = render(<Range marks={marks} />);
56 |   //   const rangeMark = rangeWrapper.find('.rc-slider-mark-text').at(1);
57 |   //   rangeMark.simulate('mousedown', {
58 |   //     type: 'mousedown',
59 |   //     target: rangeMark,
60 |   //     pageX: 25,
61 |   //     button: 0,
62 |   //     stopPropagation() {},
63 |   //     preventDefault() {},
64 |   //   });
65 |   //   expect(rangeWrapper.state('bounds')).toBe([0, 30]);
66 |   // });
67 | });
68 | 


--------------------------------------------------------------------------------
/tests/setup.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/react-component/slider/874875a809e6d7423449ba8a8277e3f4cb2277cb/tests/setup.js


--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "esnext",
 4 |     "moduleResolution": "node",
 5 |     "baseUrl": "./",
 6 |     "jsx": "react",
 7 |     "declaration": true,
 8 |     "skipLibCheck": true,
 9 |     "esModuleInterop": true,
10 |     "paths": {
11 |       "@/*": ["src/*"],
12 |       "@@/*": ["src/.umi/*"],
13 |       "rc-slider": ["src/index.tsx"]
14 |     }
15 |   }
16 | }
17 | 


--------------------------------------------------------------------------------
/typings.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.css';
2 | declare module '*.less';
3 | 


--------------------------------------------------------------------------------