Design system.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

259 lines
6.6 KiB

  1. import * as React from 'react';
  2. import { TextControl } from '@tesseract-design/web-base';
  3. import clsx from 'clsx';
  4. import { PluginCreator } from 'tailwindcss/types/config';
  5. import { useFallbackId } from '@modal-sh/react-utils';
  6. /**
  7. * Derived HTML element of the {@link DateDropdown} component.
  8. */
  9. export type DateDropdownDerivedElement = HTMLInputElement;
  10. /**
  11. * Props of the {@link DateDropdown} component.
  12. */
  13. export interface DateDropdownProps extends Omit<React.HTMLProps<DateDropdownDerivedElement>, 'size' | 'type' | 'label' | 'pattern'> {
  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. * Visual length of the input.
  44. */
  45. length?: number,
  46. }
  47. export const dateDropdownPlugin: PluginCreator = ({ addComponents }) => {
  48. addComponents({
  49. '.date-dropdown': {
  50. '& > input::-webkit-calendar-picker-indicator': {
  51. 'background-image': 'none',
  52. 'position': 'absolute',
  53. 'bottom': '0',
  54. 'right': '0',
  55. 'height': '100%',
  56. 'padding': '0',
  57. 'aspect-ratio': '1 / 1',
  58. 'cursor': 'inherit',
  59. },
  60. '&[data-size="small"] > input::-webkit-calendar-picker-indicator': {
  61. 'width': '2.5rem',
  62. },
  63. '&[data-size="medium"] > input::-webkit-calendar-picker-indicator': {
  64. 'width': '3rem',
  65. },
  66. '&[data-size="large"] > input::-webkit-calendar-picker-indicator': {
  67. 'width': '4rem',
  68. },
  69. },
  70. });
  71. };
  72. /**
  73. * Component for inputting date values.
  74. */
  75. export const DateDropdown = React.forwardRef<
  76. DateDropdownDerivedElement,
  77. DateDropdownProps
  78. >((
  79. {
  80. label,
  81. hint,
  82. size = 'medium' as const,
  83. border = false,
  84. block = false,
  85. variant = 'default' as const,
  86. hiddenLabel = false,
  87. className,
  88. id: idProp,
  89. style,
  90. length,
  91. ...etcProps
  92. }: DateDropdownProps,
  93. forwardedRef,
  94. ) => {
  95. const labelId = React.useId();
  96. const id = useFallbackId(idProp);
  97. return (
  98. <div
  99. className={clsx(
  100. 'relative rounded ring-secondary/50 overflow-hidden group has-[:disabled]:opacity-50 date-dropdown',
  101. 'focus-within:ring-4',
  102. {
  103. 'block': block,
  104. 'inline-block align-middle': !block,
  105. },
  106. className,
  107. )}
  108. style={style}
  109. data-size={size}
  110. data-testid="base"
  111. >
  112. {label && (
  113. <>
  114. <label
  115. data-testid="label"
  116. id={labelId}
  117. htmlFor={id}
  118. className={clsx(
  119. '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',
  120. {
  121. 'sr-only': hiddenLabel,
  122. },
  123. {
  124. 'pr-10': size === 'small',
  125. 'pr-12': size === 'medium',
  126. 'pr-16': size === 'large',
  127. },
  128. )}
  129. >
  130. <span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
  131. {label}
  132. </span>
  133. </label>
  134. {' '}
  135. </>
  136. )}
  137. <input
  138. {...etcProps}
  139. size={length}
  140. ref={forwardedRef}
  141. id={id}
  142. aria-labelledby={labelId}
  143. type="date"
  144. data-testid="input"
  145. pattern="\d{4}-\d{2}-\d{2}"
  146. className={clsx(
  147. 'bg-negative rounded-inherit w-full peer block font-inherit tabular-nums cursor-pointer',
  148. 'focus:outline-0',
  149. 'disabled:cursor-not-allowed',
  150. {
  151. 'pl-4': variant === 'default',
  152. 'pl-1.5 pt-4': variant === 'alternate',
  153. },
  154. {
  155. 'pr-10 h-10 text-xxs': size === 'small',
  156. 'pr-12 h-12 text-xs': size === 'medium',
  157. 'pr-16 h-16': size === 'large',
  158. },
  159. )}
  160. />
  161. {hint && (
  162. <div
  163. data-testid="hint"
  164. className={clsx(
  165. 'absolute left-0 px-1 pointer-events-none text-xxs leading-none w-full bg-negative select-none',
  166. {
  167. 'bottom-0 pl-4 pb-1': variant === 'default',
  168. 'top-0.5': variant === 'alternate',
  169. },
  170. {
  171. 'pt-2': variant === 'alternate' && size === 'small',
  172. 'pt-3': variant === 'alternate' && size !== 'small',
  173. },
  174. {
  175. 'pr-10': size === 'small',
  176. 'pr-12': size === 'medium',
  177. 'pr-16': size === 'large',
  178. },
  179. )}
  180. >
  181. <div
  182. className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
  183. >
  184. {hint}
  185. </div>
  186. </div>
  187. )}
  188. <div
  189. data-testid="indicator"
  190. className={clsx(
  191. 'text-center flex items-center justify-center aspect-square absolute bottom-0 right-0 pointer-events-none select-none text-primary group-focus-within:text-secondary',
  192. {
  193. 'w-10': size === 'small',
  194. 'w-12': size === 'medium',
  195. 'w-16': size === 'large',
  196. },
  197. )}
  198. >
  199. <svg
  200. className="w-6 h-6 fill-none stroke-current stroke-2 linejoin-round linecap-round"
  201. viewBox="0 0 24 24"
  202. role="presentation"
  203. >
  204. <rect
  205. x="3"
  206. y="4"
  207. width="18"
  208. height="18"
  209. rx="2"
  210. ry="2"
  211. />
  212. <line
  213. x1="16"
  214. y1="2"
  215. x2="16"
  216. y2="6"
  217. />
  218. <line
  219. x1="8"
  220. y1="2"
  221. x2="8"
  222. y2="6"
  223. />
  224. <line
  225. x1="3"
  226. y1="10"
  227. x2="21"
  228. y2="10"
  229. />
  230. </svg>
  231. </div>
  232. {border && (
  233. <span
  234. data-testid="border"
  235. className="absolute z-[1] inset-0 rounded-inherit border-2 border-primary pointer-events-none group-focus-within:border-secondary"
  236. />
  237. )}
  238. </div>
  239. );
  240. });
  241. DateDropdown.displayName = 'DateDropdown';
  242. DateDropdown.defaultProps = {
  243. label: undefined,
  244. hint: undefined,
  245. size: 'medium',
  246. border: false,
  247. block: false,
  248. variant: 'default',
  249. hiddenLabel: false,
  250. length: undefined,
  251. };