Design system.
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 

280 строки
8.4 KiB

  1. import * as React from 'react';
  2. import clsx from 'clsx';
  3. import { PluginCreator } from 'tailwindcss/types/config';
  4. import { useFallbackId } from '@modal-sh/react-utils';
  5. /**
  6. * Derived HTML element of the {@link ToggleSwitch} component.
  7. */
  8. export type ToggleSwitchDerivedElement = HTMLInputElement;
  9. /**
  10. * Props of the {@link ToggleSwitch} component.
  11. */
  12. export interface ToggleSwitchProps extends Omit<React.InputHTMLAttributes<ToggleSwitchDerivedElement>, 'type' | 'size' | 'children'> {
  13. /**
  14. * Should the component occupy the whole width of its parent?
  15. */
  16. block?: boolean;
  17. /**
  18. * Complementary content of the component.
  19. */
  20. subtext?: React.ReactNode;
  21. /**
  22. * Is the component in an indeterminate state?
  23. */
  24. indeterminate?: boolean;
  25. /**
  26. * Label to display when the component is checked.
  27. */
  28. checkedLabel?: React.ReactNode;
  29. /**
  30. * Label to display when the component is unchecked.
  31. */
  32. uncheckedLabel?: React.ReactNode;
  33. }
  34. export const toggleSwitchPlugin: PluginCreator = ({ addComponents }) => {
  35. addComponents({
  36. '.toggle-switch': {
  37. '& + label + label + label > :first-child': {
  38. appearance: 'none',
  39. cursor: 'pointer',
  40. position: 'relative',
  41. overflow: 'hidden',
  42. height: '1.5em',
  43. width: '3em',
  44. display: 'block',
  45. 'box-sizing': 'border-box',
  46. 'border-radius': '9999px',
  47. color: 'rgb(var(--color-primary))',
  48. },
  49. '&:checked + label + label + label > :first-child': {
  50. color: 'rgb(var(--color-tertiary))',
  51. },
  52. '&:focus + label + label + label > :first-child': {
  53. color: 'rgb(var(--color-secondary))',
  54. },
  55. '& + label:active + label + label > :first-child': {
  56. color: 'rgb(var(--color-tertiary))',
  57. },
  58. '& + label + label:active + label > :first-child': {
  59. color: 'rgb(var(--color-tertiary))',
  60. },
  61. '& + label + label + label:active > :first-child': {
  62. color: 'rgb(var(--color-tertiary))',
  63. },
  64. '& + label + label + label > :first-child > :first-child': {
  65. width: '100%',
  66. height: '100%',
  67. 'background-color': 'rgb(var(--color-primary) / 50%)',
  68. 'border-radius': '9999px',
  69. display: 'block',
  70. 'box-sizing': 'border-box',
  71. 'background-clip': 'content-box',
  72. padding: '0.25em',
  73. appearance: 'none',
  74. },
  75. '&:checked + label + label + label > :first-child > :first-child': {
  76. 'background-color': 'rgb(var(--color-tertiary) / 50%)',
  77. },
  78. '&:focus + label + label + label > :first-child > :first-child': {
  79. 'background-color': 'rgb(var(--color-secondary) / 50%)',
  80. },
  81. '& + label:active + label + label > :first-child > :first-child': {
  82. 'background-color': 'rgb(var(--color-tertiary) / 50%)',
  83. },
  84. '& + label + label:active + label > :first-child > :first-child': {
  85. 'background-color': 'rgb(var(--color-tertiary) / 50%)',
  86. },
  87. '& + label + label + label:active > :first-child > :first-child': {
  88. 'background-color': 'rgb(var(--color-tertiary) / 50%)',
  89. },
  90. '& + label + label + label > :first-child > :first-child > :first-child': {
  91. appearance: 'none',
  92. 'border-radius': '9999px',
  93. display: 'block',
  94. width: '100%',
  95. height: '100%',
  96. margin: '-0.25em',
  97. 'box-sizing': 'border-box',
  98. 'background-clip': 'content-box',
  99. },
  100. '& + label + label + label > :first-child > :first-child > :first-child > :first-child': {
  101. width: '1.5em',
  102. height: '1.5em',
  103. margin: '-0.25em 0 0 0',
  104. display: 'block',
  105. 'border-radius': '9999px',
  106. 'background-color': 'currentColor',
  107. appearance: 'none',
  108. 'aspect-ratio': '1 / 1',
  109. 'z-index': '1',
  110. position: 'relative',
  111. 'box-shadow': '-100000.5em 0 0 100000em rgb(var(--color-primary) / 50%)',
  112. },
  113. '&:checked + label + label + label > :first-child > :first-child > :first-child > :first-child': {
  114. 'margin-left': 'calc(100% - 1em)',
  115. 'box-shadow': '-100000.5em 0 0 100000em rgb(var(--color-tertiary) / 50%)',
  116. },
  117. '&:indeterminate + label + label + label > :first-child > :first-child > :first-child > :first-child': {
  118. 'margin-left': 'calc(50% - 0.5em)',
  119. },
  120. '&:focus + label + label + label > :first-child > :first-child > :first-child > :first-child': {
  121. 'box-shadow': '-100000.5em 0 0 100000em rgb(var(--color-secondary) / 50%)',
  122. },
  123. '& + label:active + label + label > :first-child > :first-child > :first-child > :first-child': {
  124. 'box-shadow': '-100000.5em 0 0 100000em rgb(var(--color-tertiary) / 50%)',
  125. },
  126. '& + label + label:active + label > :first-child > :first-child > :first-child > :first-child': {
  127. 'box-shadow': '-100000.5em 0 0 100000em rgb(var(--color-tertiary) / 50%)',
  128. },
  129. '& + label + label + label:active > :first-child > :first-child > :first-child > :first-child': {
  130. 'box-shadow': '-100000.5em 0 0 100000em rgb(var(--color-tertiary) / 50%)',
  131. },
  132. },
  133. });
  134. };
  135. /**
  136. * Component for toggling a Boolean value in an appearance of a toggle switch.
  137. */
  138. export const ToggleSwitch = React.forwardRef<ToggleSwitchDerivedElement, ToggleSwitchProps>((
  139. {
  140. uncheckedLabel,
  141. checkedLabel,
  142. block = false,
  143. id: idProp,
  144. className,
  145. subtext,
  146. style,
  147. indeterminate = false,
  148. ...etcProps
  149. },
  150. forwardedRef,
  151. ) => {
  152. const defaultRef = React.useRef<ToggleSwitchDerivedElement>(null);
  153. const ref = forwardedRef ?? defaultRef;
  154. const id = useFallbackId(idProp);
  155. React.useEffect(() => {
  156. if (typeof ref === 'function') {
  157. const defaultElement = defaultRef.current as ToggleSwitchDerivedElement;
  158. defaultElement.indeterminate = indeterminate;
  159. ref(defaultElement);
  160. return;
  161. }
  162. const element = ref.current as ToggleSwitchDerivedElement;
  163. element.indeterminate = indeterminate;
  164. }, [indeterminate, defaultRef, ref]);
  165. return (
  166. <div
  167. className={clsx(
  168. 'gap-x-4 flex-wrap',
  169. block && 'flex',
  170. !block && 'inline-flex align-center',
  171. className,
  172. )}
  173. style={style}
  174. data-testid="base"
  175. >
  176. <input
  177. {...etcProps}
  178. ref={typeof ref === 'function' ? defaultRef : ref}
  179. type="checkbox"
  180. id={id}
  181. className="sr-only peer/radio toggle-switch"
  182. />
  183. <label
  184. htmlFor={id}
  185. className="peer/children order-3 cursor-pointer peer-disabled/radio:cursor-not-allowed"
  186. >
  187. <span
  188. data-testid="children"
  189. >
  190. {checkedLabel}
  191. </span>
  192. </label>
  193. <label
  194. htmlFor={id}
  195. className="peer/children order-1 cursor-pointer peer-disabled/radio:cursor-not-allowed"
  196. >
  197. {uncheckedLabel && (
  198. <>
  199. <span className="sr-only">
  200. {' / '}
  201. </span>
  202. <span
  203. data-testid="uncheckedLabel"
  204. >
  205. {uncheckedLabel}
  206. </span>
  207. </>
  208. )}
  209. </label>
  210. <label
  211. htmlFor={id}
  212. className={clsx(
  213. 'order-2 block rounded-full ring-secondary/50 overflow-hidden gap-4 leading-none select-none cursor-pointer',
  214. 'peer-focus/radio:outline-0 peer-focus/radio:ring-4 peer-focus/radio:ring-secondary/50',
  215. 'active:ring-tertiary/50 active:ring-4',
  216. 'peer-active/children:ring-tertiary/50 peer-active/children:ring-4 peer-active/children:text-tertiary',
  217. 'peer-disabled/radio:opacity-50 peer-disabled/radio:cursor-not-allowed peer-disabled/radio:ring-0',
  218. 'text-primary peer-disabled/radio:text-primary peer-focus/radio:text-secondary peer-checked/radio:text-tertiary active:text-tertiary',
  219. !uncheckedLabel && '-ml-4',
  220. )}
  221. >
  222. <span>
  223. <span>
  224. <span>
  225. <span />
  226. </span>
  227. </span>
  228. </span>
  229. </label>
  230. {subtext && (
  231. <div
  232. className={clsx(
  233. 'block w-full text-xs order-4',
  234. !uncheckedLabel && 'pl-16',
  235. uncheckedLabel && 'pt-2',
  236. )}
  237. data-testid="subtext"
  238. >
  239. {subtext}
  240. </div>
  241. )}
  242. </div>
  243. );
  244. });
  245. ToggleSwitch.displayName = 'ToggleSwitch';
  246. ToggleSwitch.defaultProps = {
  247. block: false,
  248. subtext: undefined,
  249. indeterminate: false,
  250. checkedLabel: undefined,
  251. uncheckedLabel: undefined,
  252. };