Design system.
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 
 

296 рядки
7.6 KiB

  1. import * as React from 'react';
  2. import { TextControl, tailwind } from '@tesseract-design/web-base';
  3. import { useFallbackId } from '@modal-sh/react-utils';
  4. const { tw } = tailwind;
  5. const MenuSelectDerivedElementComponent = 'select' as const;
  6. /**
  7. * Derived HTML element of the {@link MenuSelect} component.
  8. */
  9. export type MenuSelectDerivedElement = HTMLElementTagNameMap[
  10. typeof MenuSelectDerivedElementComponent
  11. ];
  12. /**
  13. * Props of the {@link MenuSelect} component.
  14. */
  15. export interface MenuSelectProps extends Omit<React.HTMLProps<MenuSelectDerivedElement>, 'size' | 'style' | 'label' | 'multiple'> {
  16. /**
  17. * Short textual description indicating the nature of the component's value.
  18. */
  19. label?: React.ReactNode,
  20. /**
  21. * Short textual description as guidelines for valid input values.
  22. */
  23. hint?: React.ReactNode,
  24. /**
  25. * Size of the component.
  26. */
  27. size?: TextControl.Size,
  28. /**
  29. * Additional description, usually graphical, indicating the nature of the component's value.
  30. */
  31. indicator?: React.ReactNode,
  32. /**
  33. * Should the component display a border?
  34. */
  35. border?: boolean,
  36. /**
  37. * Should the component occupy the whole width of its parent?
  38. */
  39. block?: boolean,
  40. /**
  41. * Style of the component.
  42. */
  43. variant?: TextControl.Variant,
  44. /**
  45. * Is the label hidden?
  46. */
  47. hiddenLabel?: boolean,
  48. /**
  49. * Starting height of the component.
  50. */
  51. startingHeight?: number | string,
  52. /**
  53. * Should the component be resizable?
  54. */
  55. resizable?: boolean,
  56. }
  57. // TODO hide scrollbar
  58. // TODO apply same styles to menumultiselect
  59. export const menuSelectPlugin: tailwind.PluginCreator = ({ addComponents }) => {
  60. addComponents({
  61. '.menu-select': {
  62. '&[data-variant="alternate"][data-size="small"]': {
  63. '--td-padding-top': '1.25rem',
  64. },
  65. '&[data-variant="alternate"][data-size="medium"]': {
  66. '--td-padding-top': '1.5rem',
  67. },
  68. '&[data-variant="alternate"][data-size="large"]': {
  69. '--td-padding-top': '2rem',
  70. },
  71. '&[data-variant="default"][data-size="small"]': {
  72. '--td-padding-top': '0.625rem',
  73. '--td-padding-bottom': '0.625rem',
  74. },
  75. '&[data-variant="default"][data-size="medium"]': {
  76. '--td-padding-top': '0.75rem',
  77. '--td-padding-bottom': '0.75rem',
  78. },
  79. '&[data-variant="default"][data-size="large"]': {
  80. '--td-padding-top': '1.25rem',
  81. '--td-padding-bottom': '1.25rem',
  82. },
  83. 'clip-path': 'inset(var(--td-padding-top, 0) 0 var(--td-padding-bottom, 0) 0)',
  84. 'padding-top': 'var(--td-padding-top, 0)',
  85. 'padding-bottom': 'var(--td-padding-bottom, 0)',
  86. '& optgroup': {
  87. 'color': 'rgb(var(--color-positive) / 50%)',
  88. 'text-transform': 'uppercase',
  89. 'font-size': '0.75em',
  90. 'margin-top': '0.5rem',
  91. 'user-select': 'none',
  92. },
  93. '& optgroup > option': {
  94. 'color': 'rgb(var(--color-positive))',
  95. 'text-transform': 'none',
  96. 'font-size': '1.333333em',
  97. },
  98. '& option': {
  99. 'user-select': 'none',
  100. },
  101. },
  102. });
  103. };
  104. /**
  105. * Component for selecting a single value from a menu.
  106. */
  107. export const MenuSelect = React.forwardRef<MenuSelectDerivedElement, MenuSelectProps>((
  108. {
  109. label,
  110. hint,
  111. indicator,
  112. size = 'medium' as const,
  113. border = false as const,
  114. block = false as const,
  115. variant = 'default' as const,
  116. hiddenLabel = false as const,
  117. className,
  118. startingHeight = '15rem' as const,
  119. id: idProp,
  120. resizable = false as const,
  121. ...etcProps
  122. }: MenuSelectProps,
  123. forwardedRef,
  124. ) => {
  125. const labelId = React.useId();
  126. const id = useFallbackId(idProp);
  127. return (
  128. <div
  129. className={tw(
  130. 'relative rounded ring-secondary/50 group has-[:disabled]:opacity-50',
  131. 'focus-within:ring-4',
  132. {
  133. 'block': block,
  134. 'inline-block align-middle': !block,
  135. },
  136. className,
  137. )}
  138. data-testid="base"
  139. >
  140. <div
  141. className={tw(
  142. 'w-full overflow-hidden min-h-16 relativemin-h-16 rounded-inherit',
  143. {
  144. 'resize': !block && resizable,
  145. 'resize-y': block && resizable,
  146. },
  147. {
  148. 'min-h-10': size === 'small',
  149. 'min-h-12': size === 'medium',
  150. 'min-h-16': size === 'large',
  151. },
  152. )}
  153. style={{
  154. height: startingHeight,
  155. }}
  156. >
  157. {label && (
  158. <>
  159. <label
  160. data-testid="label"
  161. htmlFor={id}
  162. id={labelId}
  163. className={tw(
  164. 'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold group-focus-within:text-secondary text-primary leading-none select-none',
  165. {
  166. 'sr-only': hiddenLabel,
  167. },
  168. {
  169. 'pr-1': !indicator,
  170. },
  171. {
  172. 'pr-10': indicator && size === 'small',
  173. 'pr-12': indicator && size === 'medium',
  174. 'pr-16': indicator && size === 'large',
  175. },
  176. )}
  177. >
  178. <span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
  179. {label}
  180. </span>
  181. </label>
  182. {' '}
  183. </>
  184. )}
  185. <MenuSelectDerivedElementComponent
  186. {...etcProps}
  187. ref={forwardedRef}
  188. id={id}
  189. aria-labelledby={labelId}
  190. data-testid="input"
  191. size={2}
  192. data-variant={variant}
  193. data-size={size}
  194. className={tw(
  195. 'menu-select bg-transparent rounded-inherit w-full h-full peer block overflow-auto cursor-pointer font-inherit',
  196. 'focus:outline-0',
  197. 'disabled:opacity-50 disabled:cursor-not-allowed',
  198. {
  199. 'text-xxs': size === 'small',
  200. 'text-xs': size === 'medium',
  201. },
  202. {
  203. 'pl-4': variant === 'default',
  204. 'pl-1.5': variant === 'alternate',
  205. },
  206. {
  207. 'pr-4': variant === 'default' && !indicator,
  208. 'pr-1.5': variant === 'alternate' && !indicator,
  209. },
  210. {
  211. 'pr-10': indicator && size === 'small',
  212. 'pr-12': indicator && size === 'medium',
  213. 'pr-16': indicator && size === 'large',
  214. },
  215. )}
  216. />
  217. {hint && (
  218. <div
  219. data-testid="hint"
  220. className={tw(
  221. 'absolute left-0 px-1 pointer-events-none text-xxs leading-none w-full select-none',
  222. {
  223. 'bottom-0 pl-4 pb-1': variant === 'default',
  224. 'top-0.5': variant === 'alternate',
  225. },
  226. {
  227. 'pt-2': variant === 'alternate' && size === 'small',
  228. 'pt-3': variant === 'alternate' && size !== 'small',
  229. },
  230. {
  231. 'pr-4': !indicator && variant === 'default',
  232. 'pr-1': !indicator && variant === 'alternate',
  233. },
  234. {
  235. 'pr-10': indicator && size === 'small',
  236. 'pr-12': indicator && size === 'medium',
  237. 'pr-16': indicator && size === 'large',
  238. },
  239. )}
  240. >
  241. <div
  242. className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
  243. >
  244. {hint}
  245. </div>
  246. </div>
  247. )}
  248. {indicator && (
  249. <div
  250. data-testid="indicator"
  251. className={tw(
  252. 'text-center flex items-center justify-center aspect-square absolute bottom-0 right-0 pointer-events-none select-none',
  253. {
  254. 'w-10': size === 'small',
  255. 'w-12': size === 'medium',
  256. 'w-16': size === 'large',
  257. },
  258. )}
  259. >
  260. {indicator}
  261. </div>
  262. )}
  263. {border && (
  264. <span
  265. data-testid="border"
  266. className="absolute z-[1] inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
  267. />
  268. )}
  269. </div>
  270. </div>
  271. );
  272. });
  273. MenuSelect.displayName = 'MenuSelect' as const;
  274. MenuSelect.defaultProps = {
  275. label: undefined,
  276. hint: undefined,
  277. indicator: undefined,
  278. size: 'medium' as const,
  279. border: false as const,
  280. block: false as const,
  281. variant: 'default' as const,
  282. hiddenLabel: false as const,
  283. startingHeight: '15rem' as const,
  284. resizable: false as const,
  285. };