Common front-end components for Web using the Tesseract design system, written for React. https://make.modal.sh/tesseract/web/react/common
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 
 

378 rader
9.2 KiB

  1. import * as React from 'react'
  2. import * as PropTypes from 'prop-types'
  3. import * as ReachSlider from '@reach/slider'
  4. import styled from 'styled-components'
  5. import stringify from '../../services/stringify'
  6. const Wrapper = styled('div')({
  7. position: 'relative',
  8. display: 'inline-block',
  9. verticalAlign: 'top',
  10. ':focus-within': {
  11. '--color-accent': 'var(--color-active, Highlight)',
  12. },
  13. })
  14. Wrapper.displayName = 'div'
  15. const Base = styled(ReachSlider.SliderInput)({
  16. boxSizing: 'border-box',
  17. position: 'absolute',
  18. bottom: 0,
  19. right: 0,
  20. ':active': {
  21. cursor: 'grabbing',
  22. },
  23. })
  24. Base.displayName = 'input'
  25. const Track = styled(ReachSlider.SliderTrack)({
  26. borderRadius: '0.125rem',
  27. position: 'relative',
  28. width: '100%',
  29. height: '100%',
  30. '::before': {
  31. display: 'block',
  32. position: 'absolute',
  33. content: "''",
  34. opacity: 0.25,
  35. width: '100%',
  36. height: '100%',
  37. backgroundColor: 'currentColor',
  38. borderRadius: '0.125rem',
  39. },
  40. })
  41. const Handle = styled(ReachSlider.SliderHandle)({
  42. cursor: 'grab',
  43. width: '1.25rem',
  44. height: '1.25rem',
  45. backgroundColor: 'var(--color-accent, blue)',
  46. borderRadius: '50%',
  47. zIndex: 1,
  48. transformOrigin: 'center',
  49. outline: 0,
  50. position: 'relative',
  51. '::before': {
  52. position: 'absolute',
  53. top: 0,
  54. left: 0,
  55. width: '100%',
  56. height: '100%',
  57. content: "''",
  58. borderRadius: '50%',
  59. opacity: 0.5,
  60. pointerEvents: 'none',
  61. },
  62. ':focus::before': {
  63. boxShadow: '0 0 0 0.25rem var(--color-accent, blue)',
  64. },
  65. })
  66. const Highlight = styled(ReachSlider.SliderTrackHighlight)({
  67. backgroundColor: 'var(--color-accent, blue)',
  68. borderRadius: '0.125rem',
  69. })
  70. const SizingWrapper = styled('span')({
  71. width: '100%',
  72. display: 'inline-block',
  73. verticalAlign: 'top',
  74. position: 'relative',
  75. })
  76. SizingWrapper.displayName = 'SizingWrapper'
  77. const TransformWrapper = styled('span')({
  78. display: 'inline-block',
  79. verticalAlign: 'top',
  80. transformOrigin: 'top left',
  81. })
  82. TransformWrapper.displayName = 'TransformWrapper'
  83. const LabelWrapper = styled('span')({
  84. color: 'var(--color-accent, blue)',
  85. boxSizing: 'border-box',
  86. fontSize: '0.75em',
  87. maxWidth: '100%',
  88. overflow: 'hidden',
  89. textOverflow: 'ellipsis',
  90. whiteSpace: 'nowrap',
  91. fontWeight: 'bolder',
  92. zIndex: 2,
  93. pointerEvents: 'none',
  94. transformOrigin: 'top left',
  95. position: 'absolute',
  96. top: 0,
  97. left: 0,
  98. transitionProperty: 'color',
  99. })
  100. LabelWrapper.displayName = 'span'
  101. const FallbackTrack = styled('span')({
  102. padding: '1.875rem 0.75rem 0.875rem',
  103. boxSizing: 'border-box',
  104. position: 'absolute',
  105. top: 0,
  106. left: 0,
  107. pointerEvents: 'none',
  108. '::before': {
  109. display: 'block',
  110. content: "''",
  111. opacity: 0.25,
  112. width: '100%',
  113. height: '100%',
  114. backgroundColor: 'currentColor',
  115. borderRadius: '0.125rem',
  116. },
  117. })
  118. const FallbackSlider = styled('input')({
  119. boxSizing: 'border-box',
  120. backgroundColor: 'transparent',
  121. verticalAlign: 'top',
  122. margin: 0,
  123. width: '100%',
  124. height: '2rem',
  125. marginTop: '1rem',
  126. appearance: 'none',
  127. outline: 0,
  128. position: 'absolute',
  129. left: 0,
  130. '::-moz-focus-inner': {
  131. outline: 0,
  132. border: 0,
  133. },
  134. '::-webkit-slider-thumb': {
  135. cursor: 'grab',
  136. width: '1.25rem',
  137. height: '1.25rem',
  138. backgroundColor: 'var(--color-accent, blue)',
  139. borderRadius: '50%',
  140. zIndex: 1,
  141. transformOrigin: 'center',
  142. outline: 0,
  143. position: 'relative',
  144. appearance: 'none',
  145. },
  146. '::-moz-range-thumb': {
  147. cursor: 'grab',
  148. border: 0,
  149. width: '1.25rem',
  150. height: '1.25rem',
  151. backgroundColor: 'var(--color-accent, blue)',
  152. borderRadius: '50%',
  153. zIndex: 1,
  154. transformOrigin: 'center',
  155. outline: 0,
  156. position: 'relative',
  157. appearance: 'none',
  158. },
  159. })
  160. FallbackSlider.displayName = 'input'
  161. const ClickArea = styled('label')({
  162. verticalAlign: 'top',
  163. width: '100%',
  164. height: '100%',
  165. })
  166. ClickArea.displayName = 'label'
  167. export type Orientation = 'vertical' | 'horizontal'
  168. type Dimension = 'width' | 'height'
  169. const propTypes = {
  170. /**
  171. * The component orientation.
  172. */
  173. orientation: PropTypes.oneOf<Orientation>(['vertical', 'horizontal']),
  174. /**
  175. * Short textual description indicating the nature of the component's value.
  176. */
  177. label: PropTypes.any,
  178. /**
  179. * CSS size for the component length.
  180. */
  181. length: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  182. /**
  183. * Is the component active?
  184. */
  185. disabled: PropTypes.bool,
  186. }
  187. type Props = PropTypes.InferProps<typeof propTypes>
  188. /**
  189. * Component for inputting numeric values in a graphical manner.
  190. *
  191. * The component is styled using client-side scripts. When the component is rendered server-side,
  192. * the component falls back into the original `<input type="range">` element.
  193. *
  194. * @see {@link //reacttraining.com/reach-ui/slider/#sliderinput|Reach UI Slider} for the client-side implementation.
  195. * @param {'vertical' | 'horizontal'} [orientation] - The component orientation.
  196. * @param {*} [label] - Short textual description indicating the nature of the component's value.
  197. * @param {string | number} [length] - CSS size for the component length.
  198. * @param {boolean} [disabled] - Is the component active?
  199. * @returns {React.ReactElement} The component elements.
  200. */
  201. const Slider: React.FC<Props> = ({
  202. orientation = 'horizontal',
  203. label = '',
  204. length = '16rem',
  205. disabled = false,
  206. }) => {
  207. const [isClient, setIsClient] = React.useState(false)
  208. React.useEffect(() => {
  209. window.document.body.style.setProperty('--reach-slider', '1')
  210. setIsClient(true)
  211. }, [])
  212. const parallelDimension: Dimension = orientation === 'horizontal' ? 'width' : 'height'
  213. const perpendicularDimension: Dimension = orientation === 'horizontal' ? 'height' : 'width'
  214. const perpendicularReference = orientation === 'horizontal' ? 'top' : 'left'
  215. const perpendicularTransform = orientation === 'horizontal' ? 'translateY' : 'translateX'
  216. const customBaseProps: any = {
  217. orientation,
  218. }
  219. const customFallbackProps: any = {
  220. orient: orientation,
  221. }
  222. if (isClient) {
  223. return (
  224. <Wrapper
  225. style={{
  226. opacity: disabled ? 0.5 : undefined,
  227. [parallelDimension]: length!,
  228. }}
  229. >
  230. <ClickArea>
  231. <SizingWrapper
  232. style={{
  233. [parallelDimension]: length!,
  234. [perpendicularDimension]: '3rem',
  235. }}
  236. >
  237. <TransformWrapper
  238. style={{
  239. [parallelDimension]: length!,
  240. [perpendicularDimension]: '100%',
  241. transform:
  242. orientation === 'horizontal'
  243. ? undefined
  244. : `rotate(-90deg) translateX(calc(${
  245. typeof length! === 'number' ? `${length as number}px` : (length as string)
  246. } * -1))`,
  247. }}
  248. >
  249. <LabelWrapper
  250. style={{
  251. maxWidth: length!,
  252. }}
  253. >
  254. {stringify(label)}
  255. </LabelWrapper>
  256. </TransformWrapper>
  257. </SizingWrapper>
  258. <Base
  259. {...customBaseProps}
  260. disabled={disabled!}
  261. style={{
  262. [parallelDimension]: length,
  263. [perpendicularDimension]: '2rem',
  264. padding: orientation === 'horizontal' ? '0.875rem 0.75rem' : '0.75rem 0.875rem',
  265. cursor: disabled ? 'not-allowed' : undefined,
  266. }}
  267. >
  268. <Track>
  269. <Highlight
  270. style={{
  271. [perpendicularDimension]: '100%',
  272. }}
  273. />
  274. <Handle
  275. style={{
  276. [perpendicularReference]: '50%',
  277. transform: `${perpendicularTransform}(-50%)`,
  278. cursor: disabled ? 'not-allowed' : undefined,
  279. }}
  280. />
  281. </Track>
  282. </Base>
  283. </ClickArea>
  284. </Wrapper>
  285. )
  286. }
  287. return (
  288. <Wrapper
  289. style={{
  290. opacity: disabled ? 0.5 : undefined,
  291. [parallelDimension]: length!,
  292. }}
  293. >
  294. <SizingWrapper
  295. style={{
  296. [parallelDimension]: length!,
  297. [perpendicularDimension]: '3rem',
  298. }}
  299. >
  300. <TransformWrapper
  301. style={{
  302. [parallelDimension]: length!,
  303. [perpendicularDimension]: '100%',
  304. transform:
  305. orientation === 'horizontal'
  306. ? undefined
  307. : `rotate(-90deg) translateX(calc(${typeof length! === 'number' ? `${length}px` : length} * -1))`,
  308. }}
  309. >
  310. <FallbackTrack
  311. style={{
  312. width: length!,
  313. height: '3rem',
  314. padding: '1.875rem 0.75rem 0.875rem',
  315. }}
  316. />
  317. <ClickArea>
  318. <LabelWrapper
  319. style={{
  320. maxWidth: length!,
  321. }}
  322. >
  323. {stringify(label)}
  324. </LabelWrapper>
  325. {stringify(label).length > 0 && ' '}
  326. <FallbackSlider
  327. {...customFallbackProps}
  328. style={{
  329. width: length!,
  330. }}
  331. disabled={disabled!}
  332. type="range"
  333. />
  334. </ClickArea>
  335. </TransformWrapper>
  336. </SizingWrapper>
  337. </Wrapper>
  338. )
  339. }
  340. Slider.propTypes = propTypes
  341. Slider.displayName = 'Slider'
  342. export default Slider