@basic-ui/core 0.0.40 → 0.0.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/build/cjs/index.js +1312 -464
  2. package/build/cjs/index.js.map +1 -1
  3. package/build/esm/Accordion/Accordion.d.ts +7 -7
  4. package/build/esm/Accordion/Accordion.js +4 -4
  5. package/build/esm/Accordion/Accordion.js.map +1 -1
  6. package/build/esm/Accordion/AccordionBody.d.ts +5 -5
  7. package/build/esm/Accordion/AccordionBody.js +4 -4
  8. package/build/esm/Accordion/AccordionBody.js.map +1 -1
  9. package/build/esm/Accordion/AccordionHeader.d.ts +6 -6
  10. package/build/esm/Accordion/AccordionHeader.js +4 -4
  11. package/build/esm/Accordion/AccordionHeader.js.map +1 -1
  12. package/build/esm/Accordion/AccordionItem.d.ts +7 -7
  13. package/build/esm/Accordion/AccordionItem.js +2 -2
  14. package/build/esm/Accordion/AccordionItem.js.map +1 -1
  15. package/build/esm/Accordion/context.d.ts +3 -3
  16. package/build/esm/Accordion/context.js.map +1 -1
  17. package/build/esm/Accordion/index.d.ts +0 -0
  18. package/build/esm/Accordion/index.js.map +1 -1
  19. package/build/esm/Accordion/scopeQuery.d.ts +0 -0
  20. package/build/esm/Accordion/scopeQuery.js.map +1 -1
  21. package/build/esm/CheckBox/CheckBox.d.ts +6 -6
  22. package/build/esm/CheckBox/CheckBox.js +2 -2
  23. package/build/esm/CheckBox/CheckBox.js.map +1 -1
  24. package/build/esm/CheckBox/index.d.ts +0 -0
  25. package/build/esm/CheckBox/index.js.map +1 -1
  26. package/build/esm/ComboBox/Combobox.d.ts +10 -10
  27. package/build/esm/ComboBox/Combobox.js +3 -3
  28. package/build/esm/ComboBox/Combobox.js.map +1 -1
  29. package/build/esm/ComboBox/ComboboxButton.d.ts +8 -8
  30. package/build/esm/ComboBox/ComboboxButton.js +4 -4
  31. package/build/esm/ComboBox/ComboboxButton.js.map +1 -1
  32. package/build/esm/ComboBox/ComboboxInput.d.ts +10 -10
  33. package/build/esm/ComboBox/ComboboxInput.js +3 -3
  34. package/build/esm/ComboBox/ComboboxInput.js.map +1 -1
  35. package/build/esm/ComboBox/ComboboxLabel.d.ts +5 -5
  36. package/build/esm/ComboBox/ComboboxLabel.js +2 -2
  37. package/build/esm/ComboBox/ComboboxLabel.js.map +1 -1
  38. package/build/esm/ComboBox/ComboboxList.d.ts +6 -6
  39. package/build/esm/ComboBox/ComboboxList.js +3 -3
  40. package/build/esm/ComboBox/ComboboxList.js.map +1 -1
  41. package/build/esm/ComboBox/ComboboxOption.d.ts +7 -7
  42. package/build/esm/ComboBox/ComboboxOption.js +4 -4
  43. package/build/esm/ComboBox/ComboboxOption.js.map +1 -1
  44. package/build/esm/ComboBox/ComboboxPopover.d.ts +8 -8
  45. package/build/esm/ComboBox/ComboboxPopover.js +5 -5
  46. package/build/esm/ComboBox/ComboboxPopover.js.map +1 -1
  47. package/build/esm/ComboBox/cities.d.ts +0 -0
  48. package/build/esm/ComboBox/cities.js.map +1 -1
  49. package/build/esm/ComboBox/context.d.ts +10 -10
  50. package/build/esm/ComboBox/context.js.map +1 -1
  51. package/build/esm/ComboBox/hooks.d.ts +3 -3
  52. package/build/esm/ComboBox/hooks.js +11 -11
  53. package/build/esm/ComboBox/hooks.js.map +1 -1
  54. package/build/esm/ComboBox/index.d.ts +0 -0
  55. package/build/esm/ComboBox/index.js.map +1 -1
  56. package/build/esm/ComboBox/makeHash.d.ts +0 -0
  57. package/build/esm/ComboBox/makeHash.js.map +1 -1
  58. package/build/esm/ComboBox/scopeQuery.d.ts +0 -0
  59. package/build/esm/ComboBox/scopeQuery.js.map +1 -1
  60. package/build/esm/FocusLock/FocusLock.d.ts +7 -7
  61. package/build/esm/FocusLock/FocusLock.js +2 -2
  62. package/build/esm/FocusLock/FocusLock.js.map +1 -1
  63. package/build/esm/FocusLock/index.d.ts +0 -0
  64. package/build/esm/FocusLock/index.js.map +1 -1
  65. package/build/esm/FocusLock/tabUtils.d.ts +0 -0
  66. package/build/esm/FocusLock/tabUtils.js.map +1 -1
  67. package/build/esm/FocusLock/useFocusLock.d.ts +4 -4
  68. package/build/esm/FocusLock/useFocusLock.js.map +1 -1
  69. package/build/esm/List/List.d.ts +6 -6
  70. package/build/esm/List/List.js +2 -2
  71. package/build/esm/List/List.js.map +1 -1
  72. package/build/esm/List/ListItem.d.ts +6 -6
  73. package/build/esm/List/ListItem.js +2 -2
  74. package/build/esm/List/ListItem.js.map +1 -1
  75. package/build/esm/List/context.d.ts +0 -0
  76. package/build/esm/List/context.js.map +1 -1
  77. package/build/esm/List/index.d.ts +0 -0
  78. package/build/esm/List/index.js.map +1 -1
  79. package/build/esm/Menu/Menu.d.ts +6 -6
  80. package/build/esm/Menu/Menu.js +2 -2
  81. package/build/esm/Menu/Menu.js.map +1 -1
  82. package/build/esm/Menu/MenuButton.d.ts +10 -10
  83. package/build/esm/Menu/MenuButton.js +3 -3
  84. package/build/esm/Menu/MenuButton.js.map +1 -1
  85. package/build/esm/Menu/MenuItem.d.ts +6 -6
  86. package/build/esm/Menu/MenuItem.js +4 -4
  87. package/build/esm/Menu/MenuItem.js.map +1 -1
  88. package/build/esm/Menu/MenuList.d.ts +5 -5
  89. package/build/esm/Menu/MenuList.js +6 -6
  90. package/build/esm/Menu/MenuList.js.map +1 -1
  91. package/build/esm/Menu/MenuPopover.d.ts +5 -5
  92. package/build/esm/Menu/MenuPopover.js +2 -2
  93. package/build/esm/Menu/MenuPopover.js.map +1 -1
  94. package/build/esm/Menu/context.d.ts +2 -2
  95. package/build/esm/Menu/context.js.map +1 -1
  96. package/build/esm/Menu/index.d.ts +0 -0
  97. package/build/esm/Menu/index.js.map +1 -1
  98. package/build/esm/Menu/scope.d.ts +0 -0
  99. package/build/esm/Menu/scope.js.map +1 -1
  100. package/build/esm/Modal/Modal.d.ts +6 -7
  101. package/build/esm/Modal/Modal.js +5 -5
  102. package/build/esm/Modal/Modal.js.map +1 -1
  103. package/build/esm/Modal/ModalBackdrop.d.ts +6 -6
  104. package/build/esm/Modal/ModalBackdrop.js +6 -6
  105. package/build/esm/Modal/ModalBackdrop.js.map +1 -1
  106. package/build/esm/Modal/index.d.ts +0 -0
  107. package/build/esm/Modal/index.js.map +1 -1
  108. package/build/esm/Popper/Popper.d.ts +7 -7
  109. package/build/esm/Popper/Popper.js +6 -6
  110. package/build/esm/Popper/Popper.js.map +1 -1
  111. package/build/esm/Popper/PopperArrow.d.ts +5 -5
  112. package/build/esm/Popper/PopperArrow.js +5 -6
  113. package/build/esm/Popper/PopperArrow.js.map +1 -1
  114. package/build/esm/Popper/context.d.ts +0 -0
  115. package/build/esm/Popper/context.js.map +1 -1
  116. package/build/esm/Popper/index.d.ts +0 -0
  117. package/build/esm/Popper/index.js.map +1 -1
  118. package/build/esm/Portal/Portal.d.ts +0 -0
  119. package/build/esm/Portal/Portal.js.map +1 -1
  120. package/build/esm/Portal/index.d.ts +0 -0
  121. package/build/esm/Portal/index.js.map +1 -1
  122. package/build/esm/RadioButton/RadioButton.d.ts +6 -6
  123. package/build/esm/RadioButton/RadioButton.js +5 -3
  124. package/build/esm/RadioButton/RadioButton.js.map +1 -1
  125. package/build/esm/RadioButton/RadioGroup.d.ts +7 -7
  126. package/build/esm/RadioButton/RadioGroup.js +2 -2
  127. package/build/esm/RadioButton/RadioGroup.js.map +1 -1
  128. package/build/esm/RadioButton/context.d.ts +2 -2
  129. package/build/esm/RadioButton/context.js.map +1 -1
  130. package/build/esm/RadioButton/index.d.ts +0 -0
  131. package/build/esm/RadioButton/index.js.map +1 -1
  132. package/build/esm/SkipNav/SkipNav.d.ts +6 -6
  133. package/build/esm/SkipNav/SkipNav.js +2 -2
  134. package/build/esm/SkipNav/SkipNav.js.map +1 -1
  135. package/build/esm/SkipNav/index.d.ts +0 -0
  136. package/build/esm/SkipNav/index.js.map +1 -1
  137. package/build/esm/Slider/Slider.d.ts +218 -0
  138. package/build/esm/Slider/Slider.js +842 -0
  139. package/build/esm/Slider/Slider.js.map +1 -0
  140. package/build/esm/Slider/index.d.ts +1 -0
  141. package/build/esm/Slider/index.js +2 -0
  142. package/build/esm/Slider/index.js.map +1 -0
  143. package/build/esm/Spinner/Spinner.d.ts +7 -7
  144. package/build/esm/Spinner/Spinner.js +2 -2
  145. package/build/esm/Spinner/Spinner.js.map +1 -1
  146. package/build/esm/Spinner/SpinnerButton.d.ts +6 -6
  147. package/build/esm/Spinner/SpinnerButton.js +3 -3
  148. package/build/esm/Spinner/SpinnerButton.js.map +1 -1
  149. package/build/esm/Spinner/context.d.ts +2 -2
  150. package/build/esm/Spinner/context.js.map +1 -1
  151. package/build/esm/Spinner/index.d.ts +0 -0
  152. package/build/esm/Spinner/index.js.map +1 -1
  153. package/build/esm/Tabs/Tab.d.ts +6 -6
  154. package/build/esm/Tabs/Tab.js +3 -3
  155. package/build/esm/Tabs/Tab.js.map +1 -1
  156. package/build/esm/Tabs/TabList.d.ts +6 -6
  157. package/build/esm/Tabs/TabList.js +4 -4
  158. package/build/esm/Tabs/TabList.js.map +1 -1
  159. package/build/esm/Tabs/TabPanel.d.ts +6 -6
  160. package/build/esm/Tabs/TabPanel.js +4 -4
  161. package/build/esm/Tabs/TabPanel.js.map +1 -1
  162. package/build/esm/Tabs/TabPanels.d.ts +5 -5
  163. package/build/esm/Tabs/TabPanels.js +4 -4
  164. package/build/esm/Tabs/TabPanels.js.map +1 -1
  165. package/build/esm/Tabs/Tabs.d.ts +6 -6
  166. package/build/esm/Tabs/Tabs.js +2 -2
  167. package/build/esm/Tabs/Tabs.js.map +1 -1
  168. package/build/esm/Tabs/context.d.ts +2 -2
  169. package/build/esm/Tabs/context.js.map +1 -1
  170. package/build/esm/Tabs/index.d.ts +0 -0
  171. package/build/esm/Tabs/index.js.map +1 -1
  172. package/build/esm/Tabs/scopeQuery.d.ts +0 -0
  173. package/build/esm/Tabs/scopeQuery.js.map +1 -1
  174. package/build/esm/Tooltip/Tooltip.d.ts +7 -8
  175. package/build/esm/Tooltip/Tooltip.js +5 -3
  176. package/build/esm/Tooltip/Tooltip.js.map +1 -1
  177. package/build/esm/Tooltip/index.d.ts +0 -0
  178. package/build/esm/Tooltip/index.js.map +1 -1
  179. package/build/esm/Tooltip/stateMachine.d.ts +0 -0
  180. package/build/esm/Tooltip/stateMachine.js +9 -9
  181. package/build/esm/Tooltip/stateMachine.js.map +1 -1
  182. package/build/esm/Tooltip/useTooltip.d.ts +7 -7
  183. package/build/esm/Tooltip/useTooltip.js +6 -5
  184. package/build/esm/Tooltip/useTooltip.js.map +1 -1
  185. package/build/esm/hooks/index.d.ts +0 -0
  186. package/build/esm/hooks/index.js.map +1 -1
  187. package/build/esm/hooks/useAutoFocus.d.ts +2 -2
  188. package/build/esm/hooks/useAutoFocus.js.map +1 -1
  189. package/build/esm/hooks/useChildrenCounter.d.ts +0 -0
  190. package/build/esm/hooks/useChildrenCounter.js.map +1 -1
  191. package/build/esm/hooks/useControlledState.d.ts +2 -6
  192. package/build/esm/hooks/useControlledState.js +8 -7
  193. package/build/esm/hooks/useControlledState.js.map +1 -1
  194. package/build/esm/hooks/useFocusReturn.d.ts +0 -0
  195. package/build/esm/hooks/useFocusReturn.js +2 -2
  196. package/build/esm/hooks/useFocusReturn.js.map +1 -1
  197. package/build/esm/hooks/useFocusState.d.ts +5 -5
  198. package/build/esm/hooks/useFocusState.js +1 -1
  199. package/build/esm/hooks/useFocusState.js.map +1 -1
  200. package/build/esm/hooks/useGestureHandlers.d.ts +5 -5
  201. package/build/esm/hooks/useGestureHandlers.js +23 -15
  202. package/build/esm/hooks/useGestureHandlers.js.map +1 -1
  203. package/build/esm/hooks/useMeasure.d.ts +2 -2
  204. package/build/esm/hooks/useMeasure.js +0 -1
  205. package/build/esm/hooks/useMeasure.js.map +1 -1
  206. package/build/esm/hooks/useOnClickOutside.d.ts +2 -2
  207. package/build/esm/hooks/useOnClickOutside.js.map +1 -1
  208. package/build/esm/hooks/useOnKeyDown.d.ts +1 -1
  209. package/build/esm/hooks/useOnKeyDown.js +5 -5
  210. package/build/esm/hooks/useOnKeyDown.js.map +1 -1
  211. package/build/esm/hooks/useReducerMachine.d.ts +0 -0
  212. package/build/esm/hooks/useReducerMachine.js +2 -1
  213. package/build/esm/hooks/useReducerMachine.js.map +1 -1
  214. package/build/esm/hooks/useRemoveBodyScroll.d.ts +2 -1
  215. package/build/esm/hooks/useRemoveBodyScroll.js +12 -8
  216. package/build/esm/hooks/useRemoveBodyScroll.js.map +1 -1
  217. package/build/esm/hooks/useScope.d.ts +0 -0
  218. package/build/esm/hooks/useScope.js.map +1 -1
  219. package/build/esm/hooks/useThrottle.d.ts +0 -0
  220. package/build/esm/hooks/useThrottle.js.map +1 -1
  221. package/build/esm/index.d.ts +1 -0
  222. package/build/esm/index.js +1 -0
  223. package/build/esm/index.js.map +1 -1
  224. package/build/esm/utils/assign-ref.d.ts +3 -0
  225. package/build/esm/utils/{assignRef.js → assign-ref.js} +1 -1
  226. package/build/esm/utils/assign-ref.js.map +1 -0
  227. package/build/esm/utils/can-use-dom.d.ts +1 -0
  228. package/build/esm/utils/can-use-dom.js +4 -0
  229. package/build/esm/utils/can-use-dom.js.map +1 -0
  230. package/build/esm/utils/clamp.d.ts +0 -0
  231. package/build/esm/utils/clamp.js.map +1 -1
  232. package/build/esm/utils/context.d.ts +7 -0
  233. package/build/esm/utils/context.js +41 -0
  234. package/build/esm/utils/context.js.map +1 -0
  235. package/build/esm/utils/{createSubscription.d.ts → create-subscription.d.ts} +0 -0
  236. package/build/esm/utils/{createSubscription.js → create-subscription.js} +1 -1
  237. package/build/esm/utils/create-subscription.js.map +1 -0
  238. package/build/esm/utils/{getCircularIndex.d.ts → get-circular-index.d.ts} +0 -0
  239. package/build/esm/utils/{getCircularIndex.js → get-circular-index.js} +1 -1
  240. package/build/esm/utils/get-circular-index.js.map +1 -0
  241. package/build/esm/utils/index.d.ts +10 -4
  242. package/build/esm/utils/index.js +10 -4
  243. package/build/esm/utils/index.js.map +1 -1
  244. package/build/esm/utils/is-right-click.d.ts +6 -0
  245. package/build/esm/utils/is-right-click.js +9 -0
  246. package/build/esm/utils/is-right-click.js.map +1 -0
  247. package/build/esm/utils/owner-document.d.ts +7 -0
  248. package/build/esm/utils/owner-document.js +12 -0
  249. package/build/esm/utils/owner-document.js.map +1 -0
  250. package/build/esm/utils/polymorphic.d.ts +32 -0
  251. package/build/esm/utils/polymorphic.js +2 -0
  252. package/build/esm/utils/polymorphic.js.map +1 -0
  253. package/build/esm/utils/{rubberBandClamp.d.ts → rubber-band-clamp.d.ts} +0 -0
  254. package/build/esm/utils/{rubberBandClamp.js → rubber-band-clamp.js} +1 -1
  255. package/build/esm/utils/rubber-band-clamp.js.map +1 -0
  256. package/build/esm/utils/use-stable-callback.d.ts +16 -0
  257. package/build/esm/utils/use-stable-callback.js +43 -0
  258. package/build/esm/utils/use-stable-callback.js.map +1 -0
  259. package/build/esm/utils/wrap-event.d.ts +3 -0
  260. package/build/esm/utils/{wrapEvent.js → wrap-event.js} +1 -1
  261. package/build/esm/utils/wrap-event.js.map +1 -0
  262. package/build/tsconfig-build.tsbuildinfo +1 -0
  263. package/package.json +5 -6
  264. package/src/Accordion/Accordion.tsx +12 -6
  265. package/src/Accordion/AccordionBody.tsx +4 -5
  266. package/src/Accordion/AccordionHeader.tsx +30 -27
  267. package/src/Accordion/AccordionItem.tsx +12 -6
  268. package/src/Accordion/context.ts +3 -2
  269. package/src/CheckBox/CheckBox.tsx +5 -6
  270. package/src/ComboBox/ComboBox.story.tsx +3 -3
  271. package/src/ComboBox/Combobox.tsx +5 -8
  272. package/src/ComboBox/ComboboxButton.tsx +15 -9
  273. package/src/ComboBox/ComboboxInput.tsx +18 -10
  274. package/src/ComboBox/ComboboxLabel.tsx +4 -4
  275. package/src/ComboBox/ComboboxList.tsx +5 -6
  276. package/src/ComboBox/ComboboxOption.tsx +12 -8
  277. package/src/ComboBox/ComboboxPopover.tsx +15 -10
  278. package/src/ComboBox/context.ts +10 -9
  279. package/src/ComboBox/hooks.tsx +4 -3
  280. package/src/FocusLock/FocusLock.tsx +13 -7
  281. package/src/FocusLock/useFocusLock.ts +4 -3
  282. package/src/List/List.tsx +5 -5
  283. package/src/List/ListItem.tsx +5 -5
  284. package/src/Menu/Menu.story.tsx +2 -2
  285. package/src/Menu/Menu.tsx +13 -7
  286. package/src/Menu/MenuButton.tsx +13 -10
  287. package/src/Menu/MenuItem.tsx +15 -11
  288. package/src/Menu/MenuList.tsx +6 -6
  289. package/src/Menu/MenuPopover.tsx +4 -4
  290. package/src/Menu/context.ts +9 -4
  291. package/src/Modal/Modal.tsx +11 -7
  292. package/src/Modal/ModalBackdrop.tsx +16 -11
  293. package/src/Popper/Popper.tsx +7 -7
  294. package/src/Popper/PopperArrow.tsx +6 -8
  295. package/src/RadioButton/RadioButton.tsx +11 -6
  296. package/src/RadioButton/RadioGroup.tsx +11 -9
  297. package/src/RadioButton/context.ts +2 -4
  298. package/src/SkipNav/SkipNav.tsx +5 -5
  299. package/src/Slider/Slider.story.tsx +45 -0
  300. package/src/Slider/Slider.tsx +1106 -0
  301. package/src/Slider/index.ts +1 -0
  302. package/src/Slider/styles.css +131 -0
  303. package/src/Spinner/Spinner.tsx +13 -9
  304. package/src/Spinner/SpinnerButton.tsx +11 -6
  305. package/src/Spinner/context.ts +2 -3
  306. package/src/Tabs/Tab.tsx +13 -8
  307. package/src/Tabs/TabList.tsx +5 -5
  308. package/src/Tabs/TabPanel.tsx +5 -5
  309. package/src/Tabs/TabPanels.tsx +4 -4
  310. package/src/Tabs/Tabs.tsx +5 -7
  311. package/src/Tabs/context.ts +2 -3
  312. package/src/Tooltip/Tooltip.story.tsx +3 -3
  313. package/src/Tooltip/Tooltip.tsx +13 -7
  314. package/src/Tooltip/stateMachine.ts +9 -13
  315. package/src/Tooltip/useTooltip.ts +17 -11
  316. package/src/hooks/useAutoFocus.ts +2 -1
  317. package/src/hooks/useControlledState.ts +14 -14
  318. package/src/hooks/useFocusState.ts +4 -3
  319. package/src/hooks/useGestureHandlers.ts +33 -19
  320. package/src/hooks/useMeasure.ts +2 -2
  321. package/src/hooks/useOnClickOutside.ts +2 -2
  322. package/src/hooks/useOnKeyDown.ts +4 -3
  323. package/src/hooks/useRemoveBodyScroll.ts +18 -8
  324. package/src/index.ts +1 -0
  325. package/src/utils/{assignRef.ts → assign-ref.ts} +4 -4
  326. package/src/utils/can-use-dom.ts +7 -0
  327. package/src/utils/context.tsx +48 -0
  328. package/src/utils/{createSubscription.ts → create-subscription.ts} +0 -0
  329. package/src/utils/{getCircularIndex.ts → get-circular-index.ts} +0 -0
  330. package/src/utils/index.ts +10 -4
  331. package/src/utils/is-right-click.ts +14 -0
  332. package/src/utils/owner-document.ts +13 -0
  333. package/src/utils/polymorphic.ts +72 -0
  334. package/src/utils/{rubberBandClamp.ts → rubber-band-clamp.ts} +0 -0
  335. package/src/utils/use-stable-callback.ts +58 -0
  336. package/src/utils/wrap-event.ts +22 -0
  337. package/build/esm/hooks/useId.d.ts +0 -3
  338. package/build/esm/hooks/useId.js +0 -16
  339. package/build/esm/hooks/useId.js.map +0 -1
  340. package/build/esm/utils/assignRef.d.ts +0 -3
  341. package/build/esm/utils/assignRef.js.map +0 -1
  342. package/build/esm/utils/createSubscription.js.map +0 -1
  343. package/build/esm/utils/getCircularIndex.js.map +0 -1
  344. package/build/esm/utils/rubberBandClamp.js.map +0 -1
  345. package/build/esm/utils/wrapEvent.d.ts +0 -3
  346. package/build/esm/utils/wrapEvent.js.map +0 -1
  347. package/build/tsconfig.tsbuildinfo +0 -1
  348. package/src/utils/wrapEvent.ts +0 -20
@@ -0,0 +1,1106 @@
1
+ /**
2
+ * Welcome to @reach/slider!
3
+ *
4
+ * A UI input component where the user selects a value from within a given
5
+ * range. A Slider has a handle that can be moved along a track to change its
6
+ * value. When the user's mouse or focus is on the Slider's handle, the value
7
+ * can be incremented with keyboard controls.
8
+ *
9
+ * Random thoughts/notes:
10
+ * - Currently testing this against the behavior of the native input range
11
+ * element to get our slider on par. We'll explore animated and multi-handle
12
+ * sliders next.
13
+ * - We may want to research some use cases for reversed sliders in RTL
14
+ * languages if that's a thing
15
+ *
16
+ * @see Docs https://reach.tech/slider
17
+ * @see Source https://github.com/reach/reach-ui/tree/main/packages/slider
18
+ * @see WAI-ARIA https://www.w3.org/TR/wai-aria-practices-1.2/#slider
19
+ * @see Example https://github.com/Stanko/aria-progress-range-slider
20
+ * @see Example http://www.oaa-accessibility.org/examplep/slider1/
21
+ */
22
+
23
+ import {
24
+ forwardRef,
25
+ memo,
26
+ useRef,
27
+ useState,
28
+ useCallback,
29
+ useEffect,
30
+ useLayoutEffect,
31
+ useId,
32
+ } from 'react';
33
+ import type { ReactNode, KeyboardEvent } from 'react';
34
+
35
+ import { createContext } from '../utils/context';
36
+ import type * as Polymorphic from '../utils/polymorphic';
37
+ import { isRightClick } from '../utils/is-right-click';
38
+ import { getOwnerDocument } from '../utils/owner-document';
39
+ import { useStableLayoutCallback } from '../utils/use-stable-callback';
40
+ import { assignMultipleRefs, wrapEvent } from '../utils';
41
+ import { useControlledState } from '../hooks';
42
+
43
+ const noop = () => {
44
+ /* noop */
45
+ };
46
+
47
+ type SliderOrientation = 'horizontal' | 'vertical';
48
+ type SliderHandleAlignment = 'center' | 'contain';
49
+
50
+ const [SliderProvider, useSliderContext] =
51
+ createContext<ISliderContext>('Slider');
52
+
53
+ ////////////////////////////////////////////////////////////////////////////////
54
+
55
+ /**
56
+ * Slider
57
+ *
58
+ * @see Docs https://reach.tech/slider#slider
59
+ */
60
+ const Slider = forwardRef(function Slider(
61
+ { children, ...props },
62
+ forwardedRef
63
+ ) {
64
+ return (
65
+ <SliderInput {...props} ref={forwardedRef} data-reach-slider="">
66
+ <SliderTrack>
67
+ <SliderRange />
68
+ <SliderHandle />
69
+ {children}
70
+ </SliderTrack>
71
+ </SliderInput>
72
+ );
73
+ }) as Polymorphic.ForwardRefComponent<'div', SliderProps>;
74
+
75
+ /**
76
+ * @see Docs https://reach.tech/slider#slider-props
77
+ */
78
+ interface SliderProps {
79
+ /**
80
+ * `Slider` can accept `SliderMarker` children to enhance display of specific
81
+ * values along the track.
82
+ *
83
+ * @see Docs https://reach.tech/slider#slider-children
84
+ */
85
+ children?: ReactNode;
86
+ /**
87
+ * The defaultValue is used to set an initial value for an uncontrolled
88
+ * Slider.
89
+ *
90
+ * @see Docs https://reach.tech/slider#slider-defaultvalue
91
+ */
92
+ defaultValue?: number;
93
+ /**
94
+ * @see Docs https://reach.tech/slider#slider-disabled
95
+ */
96
+ disabled?: boolean;
97
+ /**
98
+ * Whether or not the slider should be disabled from user interaction.
99
+ *
100
+ * @see Docs https://reach.tech/slider#slider-value
101
+ */
102
+ value?: number;
103
+ /**
104
+ * A function used to set a human-readable name for the slider.
105
+ *
106
+ * @see Docs https://reach.tech/slider#slider-getarialabel
107
+ */
108
+ getAriaLabel?(value: number): string;
109
+ /**
110
+ * A function used to set a human-readable value text based on the slider's
111
+ * current value.
112
+ *
113
+ * @see Docs https://reach.tech/slider#slider-getariavaluetext
114
+ */
115
+ getAriaValueText?(value: number): string;
116
+ /**
117
+ * When set to `center`, the slider's handle will be positioned directly
118
+ * centered over the slider's curremt value on the track. This means that when
119
+ * the slider is at its min or max value, a visiable slider handle will extend
120
+ * beyond the width (or height in vertical mode) of the slider track. When set
121
+ * to `contain`, the slider handle will always be contained within the bounds
122
+ * of the track, meaning its position will be slightly offset from the actual
123
+ * value depending on where it sits on the track.
124
+ *
125
+ * @see Docs https://reach.tech/slider#slider-handlealignment
126
+ */
127
+ handleAlignment?: SliderHandleAlignment;
128
+ /**
129
+ * The maximum value of the slider. Defaults to `100`.
130
+ *
131
+ * @see Docs https://reach.tech/slider#slider-max
132
+ */
133
+ max?: number;
134
+ /**
135
+ * The minimum value of the slider. Defaults to `0`.
136
+ *
137
+ * @see Docs https://reach.tech/slider#slider-min
138
+ */
139
+ min?: number;
140
+ /**
141
+ * If the slider is used as a form input, it should accept a `name` prop to
142
+ * identify its value in context of the form.
143
+ *
144
+ * @see Docs https://reach.tech/slider#slider-name
145
+ */
146
+ name?: string;
147
+ /**
148
+ * Callback that fires when the slider value changes. When the `value` prop is
149
+ * set, the Slider state becomes controlled and `onChange` must be used to
150
+ * update the value in response to user interaction.
151
+ *
152
+ * @see Docs https://reach.tech/slider#slider-onchange
153
+ */
154
+ onChange?(
155
+ e: SomePointerEvent | KeyboardEvent,
156
+ newValue: number,
157
+ props?: {
158
+ min?: number;
159
+ max?: number;
160
+ handlePosition?: string;
161
+ }
162
+ ): void;
163
+
164
+ // We use native DOM events for the slider since they are global
165
+ onMouseDown?(event: MouseEvent): void;
166
+ onMouseMove?(event: MouseEvent): void;
167
+ onMouseUp?(event: MouseEvent): void;
168
+ onPointerDown?(event: PointerEvent): void;
169
+ onPointerUp?(event: PointerEvent): void;
170
+ onTouchEnd?(event: TouchEvent): void;
171
+ onTouchMove?(event: TouchEvent): void;
172
+ onTouchStart?(event: TouchEvent): void;
173
+
174
+ /**
175
+ * Sets the slider to horizontal or vertical mode.
176
+ *
177
+ * @see Docs https://reach.tech/slider#slider-orientation
178
+ */
179
+ orientation?: SliderOrientation;
180
+ /**
181
+ * The step attribute is a number that specifies the granularity that the
182
+ * value must adhere to as it changes. Step sets minimum intervals of change,
183
+ * creating a "snap" effect when the handle is moved along the track.
184
+ *
185
+ * @see Docs https://reach.tech/slider#slider-step
186
+ */
187
+ step?: number;
188
+ }
189
+
190
+ ////////////////////////////////////////////////////////////////////////////////
191
+
192
+ /**
193
+ * SliderInput
194
+ *
195
+ * The parent component of the slider interface. This is a lower level component
196
+ * if you need more control over styles or rendering the slider's inner
197
+ * components.
198
+ *
199
+ * @see Docs https://reach.tech/slider#sliderinput
200
+ */
201
+ const SliderInput = forwardRef(function SliderInput(
202
+ {
203
+ 'aria-label': ariaLabel,
204
+ 'aria-labelledby': ariaLabelledBy,
205
+ 'aria-valuetext': ariaValueTextProp,
206
+ as: Comp = 'div',
207
+ defaultValue,
208
+ disabled = false,
209
+ value: controlledValue,
210
+ getAriaLabel,
211
+ getAriaValueText,
212
+ handleAlignment = 'center',
213
+ max = 100,
214
+ min = 0,
215
+ name,
216
+ onChange: onChangeProp,
217
+ onKeyDown,
218
+ onMouseDown,
219
+ onMouseMove,
220
+ onMouseUp,
221
+ onPointerDown,
222
+ onPointerUp,
223
+ onTouchEnd,
224
+ onTouchMove,
225
+ onTouchStart,
226
+ orientation = 'horizontal',
227
+ step = 1,
228
+ children,
229
+ ...rest
230
+ },
231
+ forwardedRef
232
+ ) {
233
+ const touchId: TouchIdRef = useRef();
234
+
235
+ const fallbackId = useId();
236
+ const id = rest.id || fallbackId;
237
+
238
+ // Track whether or not the pointer is down without updating the component
239
+ const pointerDownRef = useRef(false);
240
+
241
+ const trackRef: TrackRef = useRef(null);
242
+ const handleRef: HandleRef = useRef(null);
243
+ const sliderRef: SliderRef = useRef(null);
244
+
245
+ const [hasFocus, setHasFocus] = useState(false);
246
+
247
+ const { ref: x, ...handleDimensions } = useDimensions(handleRef);
248
+
249
+ const [_value, onChange] = useControlledState(
250
+ controlledValue,
251
+ onChangeProp,
252
+ defaultValue || min,
253
+ (setValue) => (e, v) => setValue(v)
254
+ );
255
+ const value = clamp(_value, min, max);
256
+ const trackPercent = valueToPercent(value, min, max);
257
+ const isVertical = orientation === 'vertical';
258
+
259
+ const handleSize = isVertical
260
+ ? handleDimensions.height
261
+ : handleDimensions.width;
262
+
263
+ // TODO: Consider removing the `handleAlignment` prop
264
+ // We may want to use accept a `handlePosition` prop instead and let apps
265
+ // define their own positioning logic, similar to how we do for popovers.
266
+ const handlePosition = `calc(${trackPercent}% - ${
267
+ handleAlignment === 'center'
268
+ ? `${handleSize}px / 2`
269
+ : `${handleSize}px * ${trackPercent * 0.01}`
270
+ })`;
271
+ const handlePositionRef = useRef(handlePosition);
272
+ useLayoutEffect(() => {
273
+ handlePositionRef.current = handlePosition;
274
+ }, [handlePosition]);
275
+
276
+ const getNewValueFromEvent = useCallback(
277
+ (event: SomePointerEvent) => {
278
+ return getNewValue(getPointerPosition(event, touchId), trackRef.current, {
279
+ step,
280
+ orientation,
281
+ min,
282
+ max,
283
+ });
284
+ },
285
+ [max, min, orientation, step]
286
+ );
287
+
288
+ // https://www.w3.org/TR/wai-aria-practices-1.2/#slider_kbd_interaction
289
+ const handleKeyDown = useStableLayoutCallback((event: KeyboardEvent) => {
290
+ if (disabled) {
291
+ return;
292
+ }
293
+
294
+ let newValue: number;
295
+ const tenSteps = (max - min) / 10;
296
+ const keyStep = step || (max - min) / 100;
297
+
298
+ switch (event.key) {
299
+ // Decrease the value of the slider by one step.
300
+ case 'ArrowLeft':
301
+ case 'ArrowDown':
302
+ newValue = value - keyStep;
303
+ break;
304
+ // Increase the value of the slider by one step
305
+ case 'ArrowRight':
306
+ case 'ArrowUp':
307
+ newValue = value + keyStep;
308
+ break;
309
+ // Decrement the slider by an amount larger than the step change made by
310
+ // `ArrowDown`.
311
+ case 'PageDown':
312
+ newValue = value - tenSteps;
313
+ break;
314
+ // Increment the slider by an amount larger than the step change made by
315
+ // `ArrowUp`.
316
+ case 'PageUp':
317
+ newValue = value + tenSteps;
318
+ break;
319
+ // Set the slider to the first allowed value in its range.
320
+ case 'Home':
321
+ newValue = min;
322
+ break;
323
+ // Set the slider to the last allowed value in its range.
324
+ case 'End':
325
+ newValue = max;
326
+ break;
327
+ default:
328
+ return;
329
+ }
330
+
331
+ newValue = clamp(
332
+ step ? roundValueToStep(newValue, step, min) : newValue,
333
+ min,
334
+ max
335
+ );
336
+ onChange(event, newValue);
337
+ });
338
+
339
+ const ariaValueText = getAriaValueText
340
+ ? getAriaValueText(value)
341
+ : ariaValueTextProp;
342
+
343
+ const rangeStyle = { [isVertical ? 'height' : 'width']: `${trackPercent}%` };
344
+
345
+ // Slide events!
346
+ // We will try to use pointer events if they are supported to leverage
347
+ // setPointerCapture and releasePointerCapture. We'll fall back to separate
348
+ // mouse and touch events.
349
+ // TODO: This could be more concise
350
+ const removeMoveEvents = useRef<() => void>(noop);
351
+ const removeStartEvents = useRef<() => void>(noop);
352
+ const removeEndEvents = useRef<() => void>(noop);
353
+
354
+ // Store our event handlers in refs so we aren't attaching/detaching events
355
+ // on every render if the user doesn't useCallback
356
+ const appEvents = useRef({
357
+ onMouseMove,
358
+ onMouseDown,
359
+ onMouseUp,
360
+ onTouchStart,
361
+ onTouchEnd,
362
+ onTouchMove,
363
+ onPointerDown,
364
+ onPointerUp,
365
+ });
366
+ useLayoutEffect(() => {
367
+ appEvents.current.onMouseMove = onMouseMove;
368
+ appEvents.current.onMouseDown = onMouseDown;
369
+ appEvents.current.onMouseUp = onMouseUp;
370
+ appEvents.current.onTouchStart = onTouchStart;
371
+ appEvents.current.onTouchEnd = onTouchEnd;
372
+ appEvents.current.onTouchMove = onTouchMove;
373
+ appEvents.current.onPointerDown = onPointerDown;
374
+ appEvents.current.onPointerUp = onPointerUp;
375
+ }, [
376
+ onMouseMove,
377
+ onMouseDown,
378
+ onMouseUp,
379
+ onTouchStart,
380
+ onTouchEnd,
381
+ onTouchMove,
382
+ onPointerDown,
383
+ onPointerUp,
384
+ ]);
385
+
386
+ const handleSlideStart = useStableLayoutCallback(
387
+ (event: SomePointerEvent) => {
388
+ if (isRightClick(event)) return;
389
+
390
+ if (disabled) {
391
+ pointerDownRef.current = false;
392
+ return;
393
+ }
394
+
395
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
396
+ const ownerDocument = getOwnerDocument(sliderRef.current)!;
397
+ const ownerWindow = ownerDocument.defaultView || window;
398
+ pointerDownRef.current = true;
399
+
400
+ if ((event as TouchEvent).changedTouches) {
401
+ // Prevent scrolling for touch events
402
+ event.preventDefault();
403
+ const touch = (event as TouchEvent).changedTouches?.[0];
404
+ if (touch != null) {
405
+ touchId.current = touch.identifier;
406
+ }
407
+ }
408
+
409
+ const newValue = getNewValueFromEvent(event);
410
+ if (newValue == null) {
411
+ return;
412
+ }
413
+ ownerWindow.requestAnimationFrame(() => handleRef.current?.focus());
414
+ onChange(event, newValue);
415
+
416
+ removeMoveEvents.current = addMoveListener();
417
+ removeEndEvents.current = addEndListener();
418
+ }
419
+ );
420
+
421
+ const setPointerCapture = useStableLayoutCallback((event: PointerEvent) => {
422
+ if (isRightClick(event)) return;
423
+ if (disabled) {
424
+ pointerDownRef.current = false;
425
+ return;
426
+ }
427
+ pointerDownRef.current = true;
428
+ sliderRef.current?.setPointerCapture(event.pointerId);
429
+ });
430
+
431
+ const releasePointerCapture = useStableLayoutCallback(
432
+ (event: PointerEvent) => {
433
+ if (isRightClick(event)) return;
434
+ sliderRef.current?.releasePointerCapture(event.pointerId);
435
+ pointerDownRef.current = false;
436
+ }
437
+ );
438
+
439
+ const handlePointerMove = useStableLayoutCallback(
440
+ (event: SomePointerEvent) => {
441
+ if (disabled || !pointerDownRef.current) {
442
+ pointerDownRef.current = false;
443
+ return;
444
+ }
445
+
446
+ const newValue = getNewValueFromEvent(event);
447
+ if (newValue == null) {
448
+ return;
449
+ }
450
+ onChange?.(event, newValue);
451
+ }
452
+ );
453
+
454
+ const handleSlideStop = useStableLayoutCallback((event: SomePointerEvent) => {
455
+ if (isRightClick(event)) return;
456
+
457
+ pointerDownRef.current = false;
458
+
459
+ const newValue = getNewValueFromEvent(event);
460
+ if (newValue == null) {
461
+ return;
462
+ }
463
+
464
+ touchId.current = undefined;
465
+
466
+ removeMoveEvents.current();
467
+ removeEndEvents.current();
468
+ });
469
+
470
+ const addMoveListener = useCallback(() => {
471
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
472
+ const ownerDocument = getOwnerDocument(sliderRef.current)!;
473
+ const touchListener = wrapEvent(
474
+ appEvents.current.onTouchMove,
475
+ handlePointerMove
476
+ );
477
+ const mouseListener = wrapEvent(
478
+ appEvents.current.onMouseMove,
479
+ handlePointerMove
480
+ );
481
+ ownerDocument.addEventListener('touchmove', touchListener);
482
+ ownerDocument.addEventListener('mousemove', mouseListener);
483
+ return () => {
484
+ ownerDocument.removeEventListener('touchmove', touchListener);
485
+ ownerDocument.removeEventListener('mousemove', mouseListener);
486
+ };
487
+ }, [handlePointerMove]);
488
+
489
+ const addEndListener = useCallback(() => {
490
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
491
+ const ownerDocument = getOwnerDocument(sliderRef.current)!;
492
+ const ownerWindow = ownerDocument.defaultView || window;
493
+ const pointerListener = wrapEvent(
494
+ appEvents.current.onPointerUp,
495
+ releasePointerCapture
496
+ );
497
+ const touchListener = wrapEvent(
498
+ appEvents.current.onTouchEnd,
499
+ handleSlideStop
500
+ );
501
+ const mouseListener = wrapEvent(
502
+ appEvents.current.onMouseUp,
503
+ handleSlideStop
504
+ );
505
+ if ('PointerEvent' in ownerWindow) {
506
+ ownerDocument.addEventListener('pointerup', pointerListener);
507
+ }
508
+ ownerDocument.addEventListener('touchend', touchListener);
509
+ ownerDocument.addEventListener('mouseup', mouseListener);
510
+ return () => {
511
+ if ('PointerEvent' in ownerWindow) {
512
+ ownerDocument.removeEventListener('pointerup', pointerListener);
513
+ }
514
+ ownerDocument.removeEventListener('touchend', touchListener);
515
+ ownerDocument.removeEventListener('mouseup', mouseListener);
516
+ };
517
+ }, [handleSlideStop, releasePointerCapture]);
518
+
519
+ const addStartListener = useCallback(() => {
520
+ // e.preventDefault is ignored by React's synthetic touchStart event, so
521
+ // we attach the listener directly to the DOM node
522
+ // https://github.com/facebook/react/issues/9809#issuecomment-413978405
523
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
524
+ const sliderElement = sliderRef.current!;
525
+ if (!sliderElement) {
526
+ return noop;
527
+ }
528
+
529
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
530
+ const ownerDocument = getOwnerDocument(sliderElement)!;
531
+ const ownerWindow = ownerDocument.defaultView || window;
532
+ const touchListener = wrapEvent(
533
+ appEvents.current.onTouchStart,
534
+ handleSlideStart
535
+ );
536
+ const mouseListener = wrapEvent(
537
+ appEvents.current.onMouseDown,
538
+ handleSlideStart
539
+ );
540
+ const pointerListener = wrapEvent(
541
+ appEvents.current.onPointerDown,
542
+ setPointerCapture
543
+ );
544
+ sliderElement.addEventListener('touchstart', touchListener);
545
+ sliderElement.addEventListener('mousedown', mouseListener);
546
+ if ('PointerEvent' in ownerWindow) {
547
+ sliderElement.addEventListener('pointerdown', pointerListener);
548
+ }
549
+ return () => {
550
+ sliderElement.removeEventListener('touchstart', touchListener);
551
+ sliderElement.removeEventListener('mousedown', mouseListener);
552
+ if ('PointerEvent' in ownerWindow) {
553
+ sliderElement.removeEventListener('pointerdown', pointerListener);
554
+ }
555
+ };
556
+ }, [setPointerCapture, handleSlideStart]);
557
+
558
+ useEffect(() => {
559
+ removeStartEvents.current = addStartListener();
560
+
561
+ return () => {
562
+ removeStartEvents.current();
563
+ removeEndEvents.current();
564
+ removeMoveEvents.current();
565
+ };
566
+ }, [addStartListener]);
567
+
568
+ const inputFallbackId = useId();
569
+ const inputId = id || inputFallbackId;
570
+
571
+ return (
572
+ <SliderProvider
573
+ ariaLabel={getAriaLabel ? getAriaLabel(value) : ariaLabel}
574
+ ariaLabelledBy={ariaLabelledBy}
575
+ ariaValueText={ariaValueText}
576
+ handleDimensions={handleDimensions}
577
+ handleKeyDown={handleKeyDown}
578
+ handlePosition={handlePosition}
579
+ handleRef={handleRef}
580
+ hasFocus={hasFocus}
581
+ onKeyDown={onKeyDown}
582
+ setHasFocus={setHasFocus}
583
+ sliderId={id}
584
+ sliderMax={max}
585
+ sliderMin={min}
586
+ value={value}
587
+ disabled={!!disabled}
588
+ isVertical={isVertical}
589
+ orientation={orientation}
590
+ trackPercent={trackPercent}
591
+ trackRef={trackRef}
592
+ rangeStyle={rangeStyle}
593
+ updateValue={onChange}
594
+ >
595
+ <Comp
596
+ {...rest}
597
+ ref={assignMultipleRefs(sliderRef, forwardedRef)}
598
+ data-reach-slider-input=""
599
+ data-disabled={disabled ? '' : undefined}
600
+ data-orientation={orientation}
601
+ tabIndex={-1}
602
+ >
603
+ {typeof children === 'function'
604
+ ? children({
605
+ hasFocus,
606
+ id,
607
+ max,
608
+ min,
609
+ value,
610
+ ariaValueText,
611
+ })
612
+ : children}
613
+ {name && (
614
+ // If the slider is used in a form we'll need an input field to
615
+ // capture the value. We'll assume this when the component is given a
616
+ // form field name (A `name` prop doesn't really make sense in any
617
+ // other context).
618
+ <input type="hidden" value={value} name={name} id={inputId} />
619
+ )}
620
+ </Comp>
621
+ </SliderProvider>
622
+ );
623
+ }) as Polymorphic.ForwardRefComponent<'div', SliderInputProps>;
624
+
625
+ /**
626
+ * @see Docs https://reach.tech/slider#sliderinput-props
627
+ */
628
+ type SliderInputProps = Omit<SliderProps, 'children'> & {
629
+ /**
630
+ * Slider expects `<SliderTrack>` as its child; The track will accept all
631
+ * additional slider sub-components as children. It can also accept a
632
+ * function/render prop as its child to expose some of its internal state
633
+ * variables.
634
+ *
635
+ * @see Docs https://reach.tech/slider#sliderinput-children
636
+ */
637
+ children: ReactNode | SliderChildrenRender;
638
+ };
639
+
640
+ ////////////////////////////////////////////////////////////////////////////////
641
+
642
+ /**
643
+ * SliderTrack
644
+ *
645
+ * @see Docs https://reach.tech/slider#slidertrack
646
+ */
647
+ const SliderTrackImpl = forwardRef(function SliderTrack(
648
+ { as: Comp = 'div', children, style = {}, ...props },
649
+ forwardedRef
650
+ ) {
651
+ const { disabled, orientation, trackRef } = useSliderContext('SliderTrack');
652
+
653
+ return (
654
+ <Comp
655
+ ref={assignMultipleRefs(trackRef, forwardedRef)}
656
+ style={{ ...style, position: 'relative' }}
657
+ {...props}
658
+ data-reach-slider-track=""
659
+ data-disabled={disabled ? '' : undefined}
660
+ data-orientation={orientation}
661
+ >
662
+ {children}
663
+ </Comp>
664
+ );
665
+ }) as Polymorphic.ForwardRefComponent<'div', SliderTrackProps>;
666
+
667
+ const SliderTrack = memo(SliderTrackImpl) as Polymorphic.MemoComponent<
668
+ 'div',
669
+ SliderTrackProps
670
+ >;
671
+
672
+ /**
673
+ * @see Docs https://reach.tech/slider#slidertrack-props
674
+ */
675
+ interface SliderTrackProps {
676
+ /**
677
+ * `SliderTrack` expects `<SliderHandle>`, at minimum, for the Slider to
678
+ * function. All other Slider subcomponents should be passed as children
679
+ * inside the `SliderTrack`.
680
+ *
681
+ * @see Docs https://reach.tech/slider#slidertrack-children
682
+ */
683
+ children: ReactNode;
684
+ }
685
+
686
+ ////////////////////////////////////////////////////////////////////////////////
687
+
688
+ /**
689
+ * SliderRange
690
+ *
691
+ * The (typically) highlighted portion of the track that represents the space
692
+ * between the slider's `min` value and its current value.
693
+ *
694
+ * @see Docs https://reach.tech/slider#sliderrange
695
+ */
696
+ const SliderRangeImpl = forwardRef(function SliderRange(
697
+ { as: Comp = 'div', children, style = {}, ...props },
698
+ forwardedRef
699
+ ) {
700
+ const { disabled, orientation, rangeStyle } = useSliderContext('SliderRange');
701
+ return (
702
+ <Comp
703
+ ref={forwardedRef}
704
+ style={{ position: 'absolute', ...rangeStyle, ...style }}
705
+ {...props}
706
+ data-reach-slider-range=""
707
+ data-disabled={disabled ? '' : undefined}
708
+ data-orientation={orientation}
709
+ />
710
+ );
711
+ }) as Polymorphic.ForwardRefComponent<'div', SliderRangeProps>;
712
+
713
+ const SliderRange = memo(SliderRangeImpl) as Polymorphic.MemoComponent<
714
+ 'div',
715
+ SliderRangeProps
716
+ >;
717
+
718
+ /**
719
+ * `SliderRange` accepts any props that a HTML div component accepts.
720
+ * `SliderRange` will not accept or render any children.
721
+ *
722
+ * @see Docs https://reach.tech/slider#sliderrange-props
723
+ */
724
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
725
+ interface SliderRangeProps {}
726
+
727
+ ////////////////////////////////////////////////////////////////////////////////
728
+
729
+ /**
730
+ * SliderHandle
731
+ *
732
+ * The handle that the user drags along the track to set the slider value.
733
+ *
734
+ * @see Docs https://reach.tech/slider#sliderhandle
735
+ */
736
+ const SliderHandleImpl = forwardRef(function SliderHandle(
737
+ {
738
+ // min,
739
+ // max,
740
+ as: Comp = 'div',
741
+ onBlur,
742
+ onFocus,
743
+ style = {},
744
+ onKeyDown,
745
+ ...props
746
+ },
747
+ forwardedRef
748
+ ) {
749
+ const {
750
+ ariaLabel,
751
+ ariaLabelledBy,
752
+ ariaValueText,
753
+ disabled,
754
+ handlePosition,
755
+ handleRef,
756
+ isVertical,
757
+ handleKeyDown,
758
+ orientation,
759
+ setHasFocus,
760
+ sliderMin,
761
+ sliderMax,
762
+ value,
763
+ } = useSliderContext('SliderHandle');
764
+
765
+ return (
766
+ <Comp
767
+ aria-disabled={disabled || undefined}
768
+ // If the slider has a visible label, it is referenced by
769
+ // `aria-labelledby` on the slider element. Otherwise, the slider
770
+ // element has a label provided by `aria-label`.
771
+ // https://www.w3.org/TR/wai-aria-practices-1.2/#slider_roles_states_props
772
+ aria-label={ariaLabel}
773
+ aria-labelledby={ariaLabel ? undefined : ariaLabelledBy}
774
+ // If the slider is vertically oriented, it has `aria-orientation` set
775
+ // to vertical. The default value of `aria-orientation` for a slider is
776
+ // horizontal.
777
+ // https://www.w3.org/TR/wai-aria-practices-1.2/#slider_roles_states_props
778
+ aria-orientation={orientation}
779
+ // The slider element has the `aria-valuemax` property set to a decimal
780
+ // value representing the maximum allowed value of the slider.
781
+ // https://www.w3.org/TR/wai-aria-practices-1.2/#slider_roles_states_props
782
+ aria-valuemax={sliderMax}
783
+ // The slider element has the `aria-valuemin` property set to a decimal
784
+ // value representing the minimum allowed value of the slider.
785
+ // https://www.w3.org/TR/wai-aria-practices-1.2/#slider_roles_states_props
786
+ aria-valuemin={sliderMin}
787
+ // The slider element has the `aria-valuenow` property set to a decimal
788
+ // value representing the current value of the slider.
789
+ // https://www.w3.org/TR/wai-aria-practices-1.2/#slider_roles_states_props
790
+ aria-valuenow={value}
791
+ // If the value of `aria-valuenow` is not user-friendly, e.g., the day
792
+ // of the week is represented by a number, the `aria-valuetext` property
793
+ // is set to a string that makes the slider value understandable, e.g.,
794
+ // "Monday".
795
+ // https://www.w3.org/TR/wai-aria-practices-1.2/#slider_roles_states_props
796
+ aria-valuetext={ariaValueText}
797
+ // The element serving as the focusable slider control has role
798
+ // `slider`.
799
+ // https://www.w3.org/TR/wai-aria-practices-1.2/#slider_roles_states_props
800
+ role="slider"
801
+ tabIndex={disabled ? -1 : 0}
802
+ {...props}
803
+ data-reach-slider-handle=""
804
+ ref={assignMultipleRefs(handleRef, forwardedRef)}
805
+ onBlur={wrapEvent(onBlur, () => {
806
+ setHasFocus(false);
807
+ })}
808
+ onFocus={wrapEvent(onFocus, () => {
809
+ setHasFocus(true);
810
+ })}
811
+ onKeyDown={wrapEvent(onKeyDown, handleKeyDown)}
812
+ style={{
813
+ position: 'absolute',
814
+ ...(isVertical ? { bottom: handlePosition } : { left: handlePosition }),
815
+ ...style,
816
+ }}
817
+ />
818
+ );
819
+ }) as Polymorphic.ForwardRefComponent<'div', SliderHandleProps>;
820
+
821
+ const SliderHandle = memo(SliderHandleImpl) as Polymorphic.MemoComponent<
822
+ 'div',
823
+ SliderHandleProps
824
+ >;
825
+
826
+ /**
827
+ * `SliderRange` accepts any props that a HTML div component accepts.
828
+ *
829
+ * @see Docs https://reach.tech/slider#sliderhandle-props
830
+ */
831
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
832
+ interface SliderHandleProps {}
833
+
834
+ ////////////////////////////////////////////////////////////////////////////////
835
+
836
+ /**
837
+ * SliderMarker
838
+ *
839
+ * A fixed value marker. These can be used to illustrate a range of steps or
840
+ * highlight important points along the slider track.
841
+ *
842
+ * @see Docs https://reach.tech/slider#slidermarker
843
+ */
844
+ const SliderMarkerImpl = forwardRef(function SliderMarker(
845
+ { as: Comp = 'div', children, style = {}, value, ...props },
846
+ forwardedRef
847
+ ) {
848
+ const {
849
+ disabled,
850
+ isVertical,
851
+ orientation,
852
+ sliderMin,
853
+ sliderMax,
854
+ value: sliderValue,
855
+ } = useSliderContext('SliderMarker');
856
+
857
+ const inRange = !(value < sliderMin || value > sliderMax);
858
+ const absoluteStartPosition = `${valueToPercent(
859
+ value,
860
+ sliderMin,
861
+ sliderMax
862
+ )}%`;
863
+
864
+ const state =
865
+ value < sliderValue
866
+ ? 'under-value'
867
+ : value === sliderValue
868
+ ? 'at-value'
869
+ : 'over-value';
870
+
871
+ return inRange ? (
872
+ <Comp
873
+ ref={forwardedRef}
874
+ style={{
875
+ position: 'absolute',
876
+ ...(isVertical
877
+ ? { bottom: absoluteStartPosition }
878
+ : { left: absoluteStartPosition }),
879
+ ...style,
880
+ }}
881
+ {...props}
882
+ data-reach-slider-marker=""
883
+ data-disabled={disabled ? '' : undefined}
884
+ data-orientation={orientation}
885
+ data-state={state}
886
+ data-value={value}
887
+ >
888
+ {children}
889
+ </Comp>
890
+ ) : null;
891
+ }) as Polymorphic.ForwardRefComponent<'div', SliderMarkerProps>;
892
+
893
+ const SliderMarker = memo(SliderMarkerImpl) as Polymorphic.MemoComponent<
894
+ 'div',
895
+ SliderMarkerProps
896
+ >;
897
+
898
+ /**
899
+ * @see Docs https://reach.tech/slider#slidermarker-props
900
+ */
901
+ interface SliderMarkerProps {
902
+ /**
903
+ * The value to denote where the marker should appear along the track.
904
+ *
905
+ * @see Docs https://reach.tech/slider#slidermarker-value
906
+ */
907
+ value: number;
908
+ }
909
+
910
+ ////////////////////////////////////////////////////////////////////////////////
911
+
912
+ function clamp(val: number, min: number, max: number) {
913
+ return val > max ? max : val < min ? min : val;
914
+ }
915
+
916
+ /**
917
+ * This handles the case when num is very small (0.00000001), js will turn
918
+ * this into 1e-8. When num is bigger than 1 or less than -1 it won't get
919
+ * converted to this notation so it's fine.
920
+ *
921
+ * @param num
922
+ * @see https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/Slider/Slider.js#L69
923
+ */
924
+ function getDecimalPrecision(num: number) {
925
+ if (Math.abs(num) < 1) {
926
+ const parts = num.toExponential().split('e-');
927
+ const matissaDecimalPart = parts[0].split('.')[1];
928
+ return (
929
+ (matissaDecimalPart ? matissaDecimalPart.length : 0) +
930
+ parseInt(parts[1], 10)
931
+ );
932
+ }
933
+
934
+ const decimalPart = num.toString().split('.')[1];
935
+ return decimalPart ? decimalPart.length : 0;
936
+ }
937
+
938
+ function percentToValue(percent: number, min: number, max: number) {
939
+ return (max - min) * percent + min;
940
+ }
941
+
942
+ function roundValueToStep(value: number, step: number, min: number) {
943
+ const nearest = Math.round((value - min) / step) * step + min;
944
+ return Number(nearest.toFixed(getDecimalPrecision(step)));
945
+ }
946
+
947
+ function getPointerPosition(event: SomePointerEvent, touchId: TouchIdRef) {
948
+ if (touchId.current !== undefined && (event as TouchEvent).changedTouches) {
949
+ for (let i = 0; i < (event as TouchEvent).changedTouches.length; i += 1) {
950
+ const touch = (event as TouchEvent).changedTouches[i];
951
+ if (touch.identifier === touchId.current) {
952
+ return {
953
+ x: touch.clientX,
954
+ y: touch.clientY,
955
+ };
956
+ }
957
+ }
958
+
959
+ return false;
960
+ }
961
+
962
+ return {
963
+ x: (event as PointerEvent | MouseEvent).clientX,
964
+ y: (event as PointerEvent | MouseEvent).clientY,
965
+ };
966
+ }
967
+
968
+ function getNewValue(
969
+ handlePosition:
970
+ | {
971
+ x: number;
972
+ y: number;
973
+ }
974
+ | false,
975
+ track: HTMLElement | null,
976
+ props: {
977
+ orientation: SliderOrientation;
978
+ min: number;
979
+ max: number;
980
+ step?: number;
981
+ }
982
+ ) {
983
+ const { orientation, min, max, step } = props;
984
+
985
+ if (!track || !handlePosition) {
986
+ return null;
987
+ }
988
+
989
+ const { left, width, bottom, height } = track.getBoundingClientRect();
990
+ const isVertical = orientation === 'vertical';
991
+ const diff = isVertical ? bottom - handlePosition.y : handlePosition.x - left;
992
+ const percent = diff / (isVertical ? height : width);
993
+ const newValue = percentToValue(percent, min, max);
994
+
995
+ return clamp(
996
+ step ? roundValueToStep(newValue, step, min) : newValue,
997
+ min,
998
+ max
999
+ );
1000
+ }
1001
+
1002
+ function useDimensions(ref: React.RefObject<HTMLElement | null>) {
1003
+ const [{ width, height }, setDimensions] = useState({
1004
+ width: 0,
1005
+ height: 0,
1006
+ });
1007
+ // Many existing `useDimensions` type hooks will use `getBoundingClientRect`
1008
+ // getBoundingClientRect does not work here when borders are applied.
1009
+ // getComputedStyle is not as performant so we may want to create a utility to
1010
+ // check for any conflicts with box sizing first and only use
1011
+ // `getComputedStyle` if neccessary.
1012
+ /* const { width, height } = ref.current
1013
+ ? ref.current.getBoundingClientRect()
1014
+ : 0; */
1015
+
1016
+ useLayoutEffect(() => {
1017
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1018
+ const ownerDocument = getOwnerDocument(ref.current)!;
1019
+ const ownerWindow = ownerDocument.defaultView || window;
1020
+ if (ref.current) {
1021
+ const { height: _newHeight, width: _newWidth } =
1022
+ ownerWindow.getComputedStyle(ref.current);
1023
+ const newHeight = parseFloat(_newHeight);
1024
+ const newWidth = parseFloat(_newWidth);
1025
+
1026
+ if (newHeight !== height || newWidth !== width) {
1027
+ setDimensions({ height: newHeight, width: newWidth });
1028
+ }
1029
+ }
1030
+ }, [ref, width, height]);
1031
+ return { ref, width, height };
1032
+ }
1033
+
1034
+ function valueToPercent(value: number, min: number, max: number) {
1035
+ return ((value - min) * 100) / (max - min);
1036
+ }
1037
+
1038
+ ////////////////////////////////////////////////////////////////////////////////
1039
+ // Types
1040
+
1041
+ type TrackRef = React.RefObject<HTMLDivElement | null>;
1042
+ type HandleRef = React.RefObject<HTMLDivElement | null>;
1043
+ type SliderRef = React.RefObject<HTMLDivElement | null>;
1044
+ type TouchIdRef = React.MutableRefObject<number | undefined>;
1045
+
1046
+ type SomePointerEvent = TouchEvent | MouseEvent;
1047
+
1048
+ interface ISliderContext {
1049
+ ariaLabel: string | undefined;
1050
+ ariaLabelledBy: string | undefined;
1051
+ ariaValueText: string | undefined;
1052
+ handleDimensions: {
1053
+ width: number;
1054
+ height: number;
1055
+ };
1056
+ handlePosition: string;
1057
+ handleRef: HandleRef;
1058
+ hasFocus: boolean;
1059
+ onKeyDown?: (event: React.KeyboardEvent<HTMLDivElement>) => void;
1060
+ handleKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void;
1061
+ setHasFocus: React.Dispatch<React.SetStateAction<boolean>>;
1062
+ sliderId: string | undefined;
1063
+ sliderMax: number;
1064
+ sliderMin: number;
1065
+ value: number;
1066
+ disabled: boolean;
1067
+ isVertical: boolean;
1068
+ orientation: SliderOrientation;
1069
+ trackPercent: number;
1070
+ trackRef: TrackRef;
1071
+ rangeStyle: React.CSSProperties;
1072
+ updateValue: (e: SomePointerEvent | KeyboardEvent, newValue: any) => void;
1073
+ }
1074
+
1075
+ type SliderChildrenRender = (props: {
1076
+ ariaValueText?: string | undefined;
1077
+ hasFocus?: boolean;
1078
+ id?: string | undefined;
1079
+ sliderId?: string | undefined;
1080
+ max?: number;
1081
+ min?: number;
1082
+ value?: number;
1083
+ }) => JSX.Element;
1084
+
1085
+ ////////////////////////////////////////////////////////////////////////////////
1086
+ // Exports
1087
+
1088
+ export default Slider;
1089
+ export type {
1090
+ SliderHandleProps,
1091
+ SliderInputProps,
1092
+ SliderMarkerProps,
1093
+ SliderProps,
1094
+ SliderRangeProps,
1095
+ SliderTrackProps,
1096
+ SliderHandleAlignment,
1097
+ SliderOrientation,
1098
+ };
1099
+ export {
1100
+ Slider,
1101
+ SliderHandle,
1102
+ SliderInput,
1103
+ SliderMarker,
1104
+ SliderTrack,
1105
+ SliderRange,
1106
+ };