Design system.
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 

304 行
7.7 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 MenuMultiSelectDerivedElementComponent = 'select' as const;
  6. /**
  7. * Derived HTML element of the {@link MenuMultiSelect} component.
  8. */
  9. export type MenuMultiSelectDerivedElement = HTMLElementTagNameMap[
  10. typeof MenuMultiSelectDerivedElementComponent
  11. ];
  12. /**
  13. * Props of the {@link MenuMultiSelect} component.
  14. */
  15. export interface MenuMultiSelectProps extends Omit<React.HTMLProps<MenuMultiSelectDerivedElement>, 'size' | '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. export const menuMultiSelectPlugin: tailwind.PluginCreator = ({ addComponents }) => {
  58. addComponents({
  59. '.menu-multi-select': {
  60. '&[data-variant="alternate"][data-size="small"]': {
  61. '--td-padding-top': '1.25rem',
  62. },
  63. '&[data-variant="alternate"][data-size="medium"]': {
  64. '--td-padding-top': '1.5rem',
  65. },
  66. '&[data-variant="alternate"][data-size="large"]': {
  67. '--td-padding-top': '2rem',
  68. },
  69. '&[data-variant="default"][data-size="small"]': {
  70. '--td-padding-top': '0.625rem',
  71. '--td-padding-bottom': '0.625rem',
  72. },
  73. '&[data-variant="default"][data-size="medium"]': {
  74. '--td-padding-top': '0.75rem',
  75. '--td-padding-bottom': '0.75rem',
  76. },
  77. '&[data-variant="default"][data-size="large"]': {
  78. '--td-padding-top': '1.25rem',
  79. '--td-padding-bottom': '1.25rem',
  80. },
  81. 'padding-top': 'var(--td-padding-top, 0)',
  82. 'padding-bottom': 'var(--td-padding-bottom, 0)',
  83. 'scrollbar-width': '0',
  84. '&::-webkit-scrollbar': {
  85. 'display': 'none',
  86. },
  87. '& optgroup': {
  88. 'color': 'rgb(var(--color-positive) / 50%)',
  89. 'text-transform': 'uppercase',
  90. 'font-size': '0.75em',
  91. 'margin-top': '0.5rem',
  92. 'user-select': 'none',
  93. },
  94. '& optgroup > option': {
  95. 'color': 'rgb(var(--color-positive))',
  96. 'text-transform': 'none',
  97. 'font-size': '1.333333em',
  98. },
  99. '& option': {
  100. 'user-select': 'none',
  101. },
  102. },
  103. });
  104. };
  105. /**
  106. * Component for inputting textual values.
  107. *
  108. * This component supports multiline input and adjusts its layout accordingly.
  109. */
  110. export const MenuMultiSelect = React.forwardRef<
  111. MenuMultiSelectDerivedElement,
  112. MenuMultiSelectProps
  113. >((
  114. {
  115. label,
  116. hint,
  117. indicator,
  118. size = 'medium' as const,
  119. border = false,
  120. block = false,
  121. variant = 'default' as const,
  122. hiddenLabel = false,
  123. className,
  124. startingHeight = '15rem',
  125. id: idProp,
  126. style,
  127. resizable = false as const,
  128. ...etcProps
  129. },
  130. forwardedRef,
  131. ) => {
  132. const labelId = React.useId();
  133. const id = useFallbackId(idProp);
  134. return (
  135. <div
  136. className={tw(
  137. 'bg-negative relative rounded ring-secondary/50 group has-[:disabled]:opacity-50',
  138. 'focus-within:ring-4',
  139. {
  140. 'block': block,
  141. 'inline-block align-middle': !block,
  142. },
  143. className,
  144. )}
  145. data-testid="base"
  146. style={style}
  147. >
  148. <div
  149. className={tw(
  150. 'w-full overflow-hidden min-h-16 relative min-h-16 rounded-inherit',
  151. {
  152. 'resize': !block && resizable,
  153. 'resize-y': block && resizable,
  154. },
  155. {
  156. 'min-h-10': size === 'small',
  157. 'min-h-12': size === 'medium',
  158. 'min-h-16': size === 'large',
  159. },
  160. )}
  161. style={{
  162. height: startingHeight,
  163. }}
  164. >
  165. {label && (
  166. <>
  167. <label
  168. data-testid="label"
  169. id={labelId}
  170. htmlFor={id}
  171. className={tw(
  172. 'bg-negative absolute z-[1] w-full top-0 pt-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold group-focus-within:text-secondary text-primary leading-none select-none',
  173. {
  174. 'sr-only': hiddenLabel,
  175. },
  176. {
  177. 'pr-1': !indicator,
  178. },
  179. {
  180. 'pr-10': indicator && size === 'small',
  181. 'pr-12': indicator && size === 'medium',
  182. 'pr-16': indicator && size === 'large',
  183. },
  184. )}
  185. >
  186. <span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
  187. {label}
  188. </span>
  189. </label>
  190. {' '}
  191. </>
  192. )}
  193. <MenuMultiSelectDerivedElementComponent
  194. {...etcProps}
  195. ref={forwardedRef}
  196. id={id}
  197. aria-labelledby={labelId}
  198. data-testid="input"
  199. size={2}
  200. multiple
  201. data-variant={variant}
  202. data-size={size}
  203. className={tw(
  204. 'menu-multi-select bg-negative rounded-inherit w-full h-full peer block overflow-auto cursor-pointer font-inherit',
  205. 'focus:outline-0',
  206. 'disabled:opacity-50 disabled:cursor-not-allowed',
  207. {
  208. 'text-xxs': size === 'small',
  209. 'text-xs': size === 'medium',
  210. },
  211. {
  212. 'pl-4': variant === 'default',
  213. 'pl-1.5': variant === 'alternate',
  214. },
  215. {
  216. 'pr-4': variant === 'default' && !indicator,
  217. 'pr-1.5': variant === 'alternate' && !indicator,
  218. },
  219. {
  220. 'pr-10': indicator && size === 'small',
  221. 'pr-12': indicator && size === 'medium',
  222. 'pr-16': indicator && size === 'large',
  223. },
  224. )}
  225. />
  226. {hint && (
  227. <div
  228. data-testid="hint"
  229. className={tw(
  230. 'bg-negative absolute left-0 px-1 pointer-events-none text-xxs leading-none w-full select-none',
  231. {
  232. 'bottom-0 pl-4 pb-1': variant === 'default',
  233. 'top-0.5': variant === 'alternate',
  234. },
  235. {
  236. 'pt-2': variant === 'alternate' && size === 'small',
  237. 'pt-3': variant === 'alternate' && size !== 'small',
  238. },
  239. {
  240. 'pr-4': !indicator && variant === 'default',
  241. 'pr-1': !indicator && variant === 'alternate',
  242. },
  243. {
  244. 'pr-10': indicator && size === 'small',
  245. 'pr-12': indicator && size === 'medium',
  246. 'pr-16': indicator && size === 'large',
  247. },
  248. )}
  249. >
  250. <div
  251. className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
  252. >
  253. {hint}
  254. </div>
  255. </div>
  256. )}
  257. {indicator && (
  258. <div
  259. data-testid="indicator"
  260. className={tw(
  261. 'text-center flex items-center justify-center aspect-square absolute bottom-0 right-0 pointer-events-none select-none',
  262. {
  263. 'w-10': size === 'small',
  264. 'w-12': size === 'medium',
  265. 'w-16': size === 'large',
  266. },
  267. )}
  268. >
  269. {indicator}
  270. </div>
  271. )}
  272. {border && (
  273. <span
  274. data-testid="border"
  275. className="absolute z-[1] inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
  276. />
  277. )}
  278. </div>
  279. </div>
  280. );
  281. });
  282. MenuMultiSelect.displayName = 'MenuMultiSelect';
  283. MenuMultiSelect.defaultProps = {
  284. label: undefined,
  285. hint: undefined,
  286. indicator: undefined,
  287. size: 'medium' as const,
  288. border: false as const,
  289. block: false as const,
  290. variant: 'default' as const,
  291. hiddenLabel: false as const,
  292. startingHeight: '15rem' as const,
  293. resizable: false as const,
  294. };