Design system.
25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 

221 satır
6.0 KiB

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