|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264 |
- import * as React from 'react';
- import { TextControl, tailwind } from '@tesseract-design/web-base';
- import { useFallbackId } from '@modal-sh/react-utils';
-
- const { tw } = tailwind;
-
- const TimeSpinnerDerivedElementComponent = 'input';
-
- /**
- * Derived HTML element of the {@link TimeSpinner} component.
- */
- export type TimeSpinnerDerivedElement = HTMLElementTagNameMap[
- typeof TimeSpinnerDerivedElementComponent
- ];
-
- type Digit = (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9);
-
- type Segment = `${Digit}${Digit}`;
-
- type StepHhMm = `${Segment}:${Segment}`;
-
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- // type StepHhMmSs = `${StepHhMm}:${Segment}`;
-
- type StepHhMmSs = string;
-
- type Step = StepHhMm | StepHhMmSs;
-
- /**
- * Props of the {@link TimeSpinner} component.
- */
- export interface TimeSpinnerProps extends Omit<React.HTMLProps<TimeSpinnerDerivedElement>, 'size' | 'type' | 'label' | 'step' | 'pattern'> {
- /**
- * 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,
- /**
- * 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,
- /**
- * Should the component display seconds?
- */
- displaySeconds?: boolean,
- /**
- * Step size for the component.
- */
- step?: Step | string,
- }
-
- export const timeSpinnerPlugin: tailwind.PluginCreator = ({ addComponents }) => {
- addComponents({
- '.time-spinner': {
- '& > input::-webkit-calendar-picker-indicator': {
- 'background-image': 'none',
- 'position': 'absolute',
- 'bottom': '0',
- 'right': '0',
- 'height': '100%',
- 'padding': '0',
- 'aspect-ratio': '1 / 1',
- 'cursor': 'inherit',
- },
- '&[data-size="small"] > input::-webkit-calendar-picker-indicator': {
- 'width': '2.5rem',
- },
- '&[data-size="medium"] > input::-webkit-calendar-picker-indicator': {
- 'width': '3rem',
- },
- '&[data-size="large"] > input::-webkit-calendar-picker-indicator': {
- 'width': '4rem',
- },
- },
- });
- };
-
- /**
- * Component for inputting time values.
- */
- export const TimeSpinner = React.forwardRef<
- TimeSpinnerDerivedElement,
- TimeSpinnerProps
- >((
- {
- label,
- hint,
- size = 'medium' as const,
- border = false as const,
- block = false as const,
- variant = 'default' as const,
- hiddenLabel = false as const,
- className,
- id: idProp,
- style,
- displaySeconds = false as const,
- step = '00:01' as const,
- ...etcProps
- }: TimeSpinnerProps,
- forwardedRef,
- ) => {
- const labelId = React.useId();
- const id = useFallbackId(idProp);
-
- const [hh, mm, ss = 0] = step.split(':').map((s: string) => parseInt(s, 10));
- const stepValue = ss + (mm * 60) + (hh * 3600);
-
- return (
- <div
- className={tw(
- 'time-spinner relative rounded ring-secondary/50 overflow-hidden group has-[:disabled]:opacity-50',
- 'focus-within:ring-4',
- {
- 'block': block,
- 'inline-block align-middle': !block,
- },
- className,
- )}
- style={style}
- data-testid="base"
- data-size={size}
- >
- {label && (
- <>
- <label
- data-testid="label"
- id={labelId}
- htmlFor={id}
- 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-10': size === 'small',
- 'pr-12': size === 'medium',
- 'pr-16': size === 'large',
- },
- )}
- >
- <span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
- {label}
- </span>
- </label>
- {' '}
- </>
- )}
- <TimeSpinnerDerivedElementComponent
- {...etcProps}
- ref={forwardedRef}
- id={id}
- aria-labelledby={labelId}
- type="time"
- data-testid="input"
- step={displaySeconds && stepValue > 60 ? 1 : stepValue}
- pattern="\d{2}:\d{2}(:\d{2})?"
- className={tw(
- 'bg-negative rounded-inherit w-full peer block font-inherit tabular-nums cursor-pointer',
- 'focus:outline-0',
- 'disabled:cursor-not-allowed',
- {
- 'pl-4': variant === 'default',
- 'pl-1.5 pt-4': variant === 'alternate',
- },
- {
- 'pr-10 h-10 text-xxs': size === 'small',
- 'pr-12 h-12 text-xs': size === 'medium',
- 'pr-16 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-10': size === 'small',
- 'pr-12': size === 'medium',
- 'pr-16': size === 'large',
- },
- )}
- >
- <div
- className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
- >
- {hint}
- </div>
- </div>
- )}
- <div
- className={tw(
- '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',
- {
- 'w-10': size === 'small',
- 'w-12': size === 'medium',
- 'w-16': size === 'large',
- },
- )}
- >
- <svg
- className="w-6 h-6 fill-none stroke-current stroke-2 linejoin-round linecap-round"
- viewBox="0 0 24 24"
- role="presentation"
- >
- <circle
- cx="12"
- cy="12"
- r="10"
- />
- <polyline points="12 6 12 12 16 14" />
- </svg>
- </div>
- {border && (
- <span
- data-testid="border"
- className="absolute z-[1] inset-0 rounded-inherit border-2 border-primary pointer-events-none group-focus-within:border-secondary"
- />
- )}
- </div>
- );
- });
-
- TimeSpinner.displayName = 'TimeSpinner';
-
- TimeSpinner.defaultProps = {
- label: undefined,
- hint: undefined,
- size: 'medium' as const,
- border: false as const,
- block: false as const,
- variant: 'default' as const,
- hiddenLabel: false as const,
- displaySeconds: false as const,
- step: '00:01' as const,
- };
|