Design system.
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 

226 lignes
6.2 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. import { PluginCreator } from 'tailwindcss/types/config';
  5. const { tw } = tailwind;
  6. const DropdownSelectDerivedElementComponent = 'select' as const;
  7. /**
  8. * Derived HTML element of the {@link DropdownSelect} component.
  9. */
  10. export type DropdownSelectDerivedElement = HTMLElementTagNameMap[
  11. typeof DropdownSelectDerivedElementComponent
  12. ];
  13. /**
  14. * Props of the {@link DropdownSelect} component.
  15. */
  16. export interface DropdownSelectProps extends Omit<React.HTMLProps<DropdownSelectDerivedElement>, 'size' | 'type' | 'label' | 'list' | 'multiple'> {
  17. /**
  18. * Short textual description indicating the nature of the component's value.
  19. */
  20. label?: React.ReactNode,
  21. /**
  22. * Short textual description as guidelines for valid input values.
  23. */
  24. hint?: React.ReactNode,
  25. /**
  26. * Size of the component.
  27. */
  28. size?: TextControl.Size,
  29. /**
  30. * Should the component display a border?
  31. */
  32. border?: boolean,
  33. /**
  34. * Should the component occupy the whole width of its parent?
  35. */
  36. block?: boolean,
  37. /**
  38. * Style of the component.
  39. */
  40. variant?: TextControl.Variant,
  41. /**
  42. * Is the label hidden?
  43. */
  44. hiddenLabel?: boolean,
  45. }
  46. export const dropdownSelectPlugin: PluginCreator = ({ addComponents }) => {
  47. addComponents({
  48. '.dropdown-select': {
  49. '& optgroup': {
  50. 'color': 'rgb(var(--color-positive) / 50%)',
  51. 'text-transform': 'uppercase',
  52. 'font-size': '0.75em',
  53. 'margin-top': '0.5rem',
  54. 'user-select': 'none',
  55. },
  56. '& optgroup > option': {
  57. 'color': 'rgb(var(--color-positive))',
  58. 'text-transform': 'none',
  59. 'font-size': '1.333333em',
  60. },
  61. '& option': {
  62. 'user-select': 'none',
  63. },
  64. },
  65. });
  66. };
  67. /**
  68. * Component for selecting a single value from a dropdown.
  69. */
  70. export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, DropdownSelectProps>((
  71. {
  72. label,
  73. hint,
  74. size = 'medium' as const,
  75. border = false as const,
  76. block = false as const,
  77. variant = 'default' as const,
  78. hiddenLabel = false as const,
  79. className,
  80. children,
  81. id: idProp,
  82. style,
  83. ...etcProps
  84. }: DropdownSelectProps,
  85. forwardedRef,
  86. ) => {
  87. const labelId = React.useId();
  88. const id = useFallbackId(idProp);
  89. return (
  90. <div
  91. className={tw(
  92. 'relative rounded ring-secondary/50 min-w-48 group has-[:disabled]:opacity-50',
  93. 'focus-within:ring-4',
  94. {
  95. 'block': block,
  96. 'inline-block align-middle': !block,
  97. },
  98. className,
  99. )}
  100. data-testid="base"
  101. style={style}
  102. >
  103. {label && (
  104. <>
  105. <label
  106. htmlFor={id}
  107. data-testid="label"
  108. id={labelId}
  109. className={tw(
  110. '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',
  111. {
  112. 'sr-only': hiddenLabel,
  113. },
  114. {
  115. 'pr-10': size === 'small',
  116. 'pr-12': size === 'medium',
  117. 'pr-16': size === 'large',
  118. },
  119. )}
  120. >
  121. <span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
  122. {label}
  123. </span>
  124. </label>
  125. {' '}
  126. </>
  127. )}
  128. <DropdownSelectDerivedElementComponent
  129. {...etcProps}
  130. ref={forwardedRef}
  131. id={id}
  132. aria-labelledby={labelId}
  133. data-testid="input"
  134. role="combobox"
  135. className={tw(
  136. 'dropdown-select bg-negative rounded-inherit w-full peer block appearance-none cursor-pointer select-none font-inherit',
  137. 'focus:outline-0',
  138. 'disabled:opacity-50 disabled:cursor-not-allowed',
  139. {
  140. 'pl-4': variant === 'default',
  141. 'pl-1.5 pt-4': variant === 'alternate',
  142. },
  143. {
  144. 'pr-10 h-10 text-xxs': size === 'small',
  145. 'pr-12 h-12 text-xs': size === 'medium',
  146. 'pr-16 h-16': size === 'large',
  147. },
  148. )}
  149. >
  150. {children}
  151. </DropdownSelectDerivedElementComponent>
  152. {hint && (
  153. <div
  154. data-testid="hint"
  155. className={tw(
  156. 'absolute left-0 px-1 pointer-events-none text-xxs leading-none w-full bg-negative select-none',
  157. {
  158. 'bottom-0 pl-4 pb-1': variant === 'default',
  159. 'top-0.5': variant === 'alternate',
  160. },
  161. {
  162. 'pt-2': variant === 'alternate' && size === 'small',
  163. 'pt-3': variant === 'alternate' && size !== 'small',
  164. },
  165. {
  166. 'pr-10': size === 'small',
  167. 'pr-12': size === 'medium',
  168. 'pr-16': size === 'large',
  169. },
  170. )}
  171. >
  172. <div
  173. className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
  174. >
  175. {hint}
  176. </div>
  177. </div>
  178. )}
  179. <div
  180. data-testid="indicator"
  181. className={tw(
  182. 'text-center flex items-center justify-center aspect-square absolute bottom-0 right-0 pointer-events-none select-none text-primary peer-focus:text-secondary',
  183. {
  184. 'w-10': size === 'small',
  185. 'w-12': size === 'medium',
  186. 'w-16': size === 'large',
  187. },
  188. )}
  189. >
  190. <svg
  191. className="w-6 h-6 fill-none stroke-current stroke-2 linejoin-round linecap-round"
  192. viewBox="0 0 24 24"
  193. role="presentation"
  194. >
  195. <polyline points="6 9 12 15 18 9" />
  196. </svg>
  197. </div>
  198. {border && (
  199. <span
  200. data-testid="border"
  201. className="absolute z-[1] inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
  202. />
  203. )}
  204. </div>
  205. );
  206. });
  207. DropdownSelect.displayName = 'DropdownSelect' as const;
  208. DropdownSelect.defaultProps = {
  209. label: undefined,
  210. hint: undefined,
  211. size: 'medium' as const,
  212. border: false as const,
  213. block: false as const,
  214. variant: 'default' as const,
  215. hiddenLabel: false as const,
  216. };