Design system.
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.
 
 
 

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