|
- import * as React from 'react';
- import { TextControl, tailwind } from '@tesseract-design/web-base';
- import { useFallbackId } from '@modal-sh/react-utils';
- import { PluginCreator } from 'tailwindcss/types/config';
-
- const { tw } = tailwind;
-
- const MenuSelectDerivedElementComponent = 'select' as const;
-
- /**
- * Derived HTML element of the {@link MenuSelect} component.
- */
- export type MenuSelectDerivedElement = HTMLElementTagNameMap[
- typeof MenuSelectDerivedElementComponent
- ];
-
- /**
- * Props of the {@link MenuSelect} component.
- */
- export interface MenuSelectProps extends Omit<React.HTMLProps<MenuSelectDerivedElement>, 'size' | 'style' | 'label' | 'multiple'> {
- /**
- * Short textual description indicating the nature of the component's value.
- */
- label?: React.ReactNode,
- /**
- * Short textual description as guidelines for valid input values.
- */
- hint?: React.ReactNode,
- /**
- * Size of the component.
- */
- size?: TextControl.Size,
- /**
- * Additional description, usually graphical, indicating the nature of the component's value.
- */
- indicator?: React.ReactNode,
- /**
- * Should the component display a border?
- */
- border?: boolean,
- /**
- * Should the component occupy the whole width of its parent?
- */
- block?: boolean,
- /**
- * Style of the component.
- */
- variant?: TextControl.Variant,
- /**
- * Is the label hidden?
- */
- hiddenLabel?: boolean,
- /**
- * Starting height of the component.
- */
- startingHeight?: number | string,
- }
-
- export const menuSelectPlugin: PluginCreator = ({ addComponents }) => {
- addComponents({
- '.menu-select': {
- '& optgroup': {
- 'color': 'rgb(var(--color-positive) / 50%)',
- 'text-transform': 'uppercase',
- 'font-size': '0.75em',
- 'margin-top': '0.5rem',
- 'user-select': 'none',
- },
- '& optgroup > option': {
- 'color': 'rgb(var(--color-positive))',
- 'text-transform': 'none',
- 'font-size': '1.333333em',
- },
- '& option': {
- 'user-select': 'none',
- },
- },
- });
- };
-
- /**
- * Component for selecting a single value from a menu.
- */
- export const MenuSelect = React.forwardRef<MenuSelectDerivedElement, MenuSelectProps>((
- {
- label,
- hint,
- indicator,
- size = 'medium' as const,
- border = false,
- block = false,
- variant = 'default' as const,
- hiddenLabel = false,
- className,
- startingHeight = '15rem',
- id: idProp,
- ...etcProps
- }: MenuSelectProps,
- forwardedRef,
- ) => {
- const labelId = React.useId();
- const id = useFallbackId(idProp);
-
- return (
- <div
- className={tw(
- 'relative rounded ring-secondary/50 group has-[:disabled]:opacity-50',
- 'focus-within:ring-4',
- {
- 'block': block,
- 'inline-block align-middle': !block,
- },
- className,
- )}
- data-testid="base"
- >
- {label && (
- <>
- <label
- data-testid="label"
- htmlFor={id}
- id={labelId}
- className={tw(
- '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',
- {
- 'sr-only': hiddenLabel,
- },
- {
- 'pr-1': !indicator,
- },
- {
- 'pr-10': indicator && size === 'small',
- 'pr-12': indicator && size === 'medium',
- 'pr-16': indicator && size === 'large',
- },
- )}
- >
- <span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
- {label}
- </span>
- </label>
- {' '}
- </>
- )}
- <MenuSelectDerivedElementComponent
- {...etcProps}
- ref={forwardedRef}
- id={id}
- aria-labelledby={labelId}
- data-testid="input"
- size={2}
- style={{
- height: startingHeight,
- }}
- className={tw(
- 'menu-select bg-negative rounded-inherit w-full peer block overflow-auto cursor-pointer font-inherit',
- 'focus:outline-0',
- 'disabled:opacity-50 disabled:cursor-not-allowed',
- {
- 'resize': !block,
- 'resize-y': block,
- },
- {
- 'text-xxs': size === 'small',
- 'text-xs': size === 'medium',
- },
- {
- 'pl-4': variant === 'default',
- 'pl-1.5': variant === 'alternate',
- },
- {
- 'pt-4': variant === 'alternate' && size === 'small',
- 'pt-5': variant === 'alternate' && size === 'medium',
- 'pt-8': variant === 'alternate' && size === 'large',
- },
- {
- 'py-2.5': variant === 'default' && size === 'small',
- 'py-3': variant === 'default' && size === 'medium',
- 'py-5': variant === 'default' && size === 'large',
- },
- {
- 'pr-4': variant === 'default' && !indicator,
- 'pr-1.5': variant === 'alternate' && !indicator,
- },
- {
- 'pr-10': indicator && size === 'small',
- 'pr-12': indicator && size === 'medium',
- 'pr-16': indicator && size === 'large',
- },
- {
- 'min-h-10': size === 'small',
- 'min-h-12': size === 'medium',
- 'min-h-16': size === 'large',
- },
- )}
- />
- {hint && (
- <div
- data-testid="hint"
- className={tw(
- 'absolute left-0 px-1 pointer-events-none text-xxs leading-none w-full bg-negative select-none',
- {
- 'bottom-0 pl-4 pb-1': variant === 'default',
- 'top-0.5': variant === 'alternate',
- },
- {
- 'pt-2': variant === 'alternate' && size === 'small',
- 'pt-3': variant === 'alternate' && size !== 'small',
- },
- {
- 'pr-4': !indicator && variant === 'default',
- 'pr-1': !indicator && variant === 'alternate',
- },
- {
- 'pr-10': indicator && size === 'small',
- 'pr-12': indicator && size === 'medium',
- 'pr-16': indicator && size === 'large',
- },
- )}
- >
- <div
- className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
- >
- {hint}
- </div>
- </div>
- )}
- {indicator && (
- <div
- data-testid="indicator"
- className={tw(
- 'text-center flex items-center justify-center aspect-square absolute bottom-0 right-0 pointer-events-none select-none',
- {
- 'w-10': size === 'small',
- 'w-12': size === 'medium',
- 'w-16': size === 'large',
- },
- )}
- >
- {indicator}
- </div>
- )}
- {border && (
- <span
- data-testid="border"
- className="absolute z-[1] inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
- />
- )}
- </div>
- );
- });
-
- MenuSelect.displayName = 'MenuSelect' as const;
-
- MenuSelect.defaultProps = {
- label: undefined,
- hint: undefined,
- indicator: undefined,
- size: 'medium' as const,
- border: false as const,
- block: false as const,
- variant: 'default' as const,
- hiddenLabel: false as const,
- startingHeight: '15rem' as const,
- };
|