HextaUI

BêtaConstruit sur l’UI SDK 0.1.0-beta.2 de Vercel. Son API peut changer.

Time input

Un champ d’heure sur l’UI SDK de Vercel saisi partie par partie, sur une horloge de 12 ou 24 heures avec secondes en option, minimum et maximum, et une valeur sur 24 heures pour les formulaires.

Dépend de
  • @ui-sdk/core
  • @ui-sdk/react
import { IconClock } from "@tabler/icons-react"

import { Label } from "@/components/ui/label"
import { TimeInput, TimeInputSegments } from "@/components/ui/ui-sdk-time-input"

function UiSdkTimeInputDemo() {
  return (
    <div className="flex w-full max-w-40 flex-col gap-2">
      <Label id="meeting-time">Meeting time</Label>
      <TimeInput aria-labelledby="meeting-time" defaultValue="09:30">
        <IconClock aria-hidden="true" />
        <TimeInputSegments />
      </TimeInput>
    </div>
  )
}

export { UiSdkTimeInputDemo }
pnpm dlx shadcn@latest add https://hextaui.com/r/ui-sdk-time-input.json

Ajoute le composant, les tokens de thème HextaUI et les composants HextaUI dont il dépend.

UI SDK ajoute un peu de CSS dans une couche de cascade nommée ui-sdk. Déclarez cette couche en haut de votre feuille de style globale, au-dessus de l’import Tailwind, pour que vos classes l’emportent.

@layer ui-sdk;

@import "tailwindcss";
import { TimeInput } from "@/components/ui/ui-sdk-time-input"
<Label id="meeting-time">Meeting time</Label>
<TimeInput aria-labelledby="meeting-time" defaultValue="09:30" />

La valeur est une heure sur 24 heures comme "14:30", comme celle d’un champ d’heure natif, quelle que soit l’horloge affichée. Elle reste null tant que chaque partie n’est pas remplie. Chaque partie est un petit champ : tapez des chiffres et il passe à la suite, ou appuyez sur A ou P pour la moitié de la journée.

Le champ est un groupe : nommez-le avec aria-labelledby ou aria-label. Un clic n’importe où dans le cadre cible la partie la plus proche.

TimeInput
└── TimeInputSegments
    └── TimeInputSegment

TimeInput affiche ses parties tout seul. Ajoutez TimeInputSegments vous-même pour placer une icône ou un autre contenu à côté.

Le champ affiche l’horloge de la langue. hourCycle choisit 12 ou 24 heures, et granularity ajoute les secondes ou n’affiche que l’heure. Tous envoient des valeurs sur 24 heures.

import { Label } from "@/components/ui/label"
import { TimeInput } from "@/components/ui/ui-sdk-time-input"

function UiSdkTimeInputClock() {
  return (
    <div className="flex w-full max-w-40 flex-col gap-4">
      <div className="flex flex-col gap-2">
        <Label id="clock-12">12-hour clock</Label>
        <TimeInput
          aria-labelledby="clock-12"
          hourCycle={12}
          defaultValue="18:45"
          locale="en-US"
        />
      </div>
      <div className="flex flex-col gap-2">
        <Label id="clock-24">24-hour clock</Label>
        <TimeInput
          aria-labelledby="clock-24"
          hourCycle={24}
          defaultValue="18:45"
        />
      </div>
      <div className="flex flex-col gap-2">
        <Label id="clock-seconds">With seconds</Label>
        <TimeInput
          aria-labelledby="clock-seconds"
          granularity="second"
          hourCycle={24}
          defaultValue="18:45:30"
        />
      </div>
      <div className="flex flex-col gap-2">
        <Label id="clock-hour">Hour only</Label>
        <TimeInput
          aria-labelledby="clock-hour"
          granularity="hour"
          hourCycle={24}
          defaultValue="18:00"
        />
      </div>
    </div>
  )
}

export { UiSdkTimeInputClock }

Une heure avant min ou après max marque le champ comme invalide, et son formulaire ne s’envoie pas.

"use client"

import * as React from "react"

import { Label } from "@/components/ui/label"
import { TimeInput } from "@/components/ui/ui-sdk-time-input"

const opens = "09:00"
const closes = "17:30"

function UiSdkTimeInputMinMax() {
  const [time, setTime] = React.useState<string | null>("16:00")
  const outside = time !== null && (time < opens || time > closes)

  return (
    <div className="flex w-full max-w-40 flex-col gap-2">
      <Label id="pickup-time">Pickup</Label>
      <TimeInput
        aria-labelledby="pickup-time"
        aria-describedby="pickup-time-message"
        defaultValue="16:00"
        min={opens}
        max={closes}
        hourCycle={24}
        onValueChange={(value) => setTime(value)}
      />
      <p
        id="pickup-time-message"
        className={
          outside
            ? "m-0 text-xs text-destructive"
            : "m-0 text-xs text-muted-foreground"
        }
      >
        {outside ? "We’re closed then." : "Open 09:00 to 17:30."}
      </p>
    </div>
  )
}

export { UiSdkTimeInputMinMax }

invalid marque le champ pour vos propres vérifications, comme une heure de fin avant le début.

"use client"

import * as React from "react"

import { Label } from "@/components/ui/label"
import { TimeInput } from "@/components/ui/ui-sdk-time-input"

function UiSdkTimeInputRange() {
  const [start, setStart] = React.useState<string | null>("13:00")
  const [end, setEnd] = React.useState<string | null>("12:30")
  const invalid = start !== null && end !== null && end <= start

  return (
    <div className="flex w-full max-w-72 flex-col gap-2">
      <div className="grid grid-cols-2 gap-2">
        <div className="flex flex-col gap-2">
          <Label id="shift-start">Starts</Label>
          <TimeInput
            aria-labelledby="shift-start"
            defaultValue="13:00"
            onValueChange={(value) => setStart(value)}
          />
        </div>
        <div className="flex flex-col gap-2">
          <Label id="shift-end">Ends</Label>
          <TimeInput
            aria-labelledby="shift-end"
            aria-describedby="shift-message"
            defaultValue="12:30"
            invalid={invalid}
            onValueChange={(value) => setEnd(value)}
          />
        </div>
      </div>
      <p
        id="shift-message"
        className={
          invalid
            ? "m-0 text-xs text-destructive"
            : "m-0 text-xs text-muted-foreground"
        }
      >
        {invalid ? "The shift has to end after it starts." : "One shift a day."}
      </p>
    </div>
  )
}

export { UiSdkTimeInputRange }

locale fixe l’ordre, les séparateurs et les noms des moitiés de journée. Sans elle, le champ suit la langue de la page une fois interactif.

import { Label } from "@/components/ui/label"
import { TimeInput } from "@/components/ui/ui-sdk-time-input"

const locales = [
  { locale: "en-US", label: "Time (en-US)" },
  { locale: "de-DE", label: "Uhrzeit (de-DE)" },
  { locale: "ja-JP", label: "時刻 (ja-JP)" },
  { locale: "ko-KR", label: "시간 (ko-KR)" },
]

function UiSdkTimeInputLocale() {
  return (
    <div className="flex w-full max-w-40 flex-col gap-4">
      {locales.map(({ locale, label }) => (
        <div key={locale} lang={locale} className="flex flex-col gap-2">
          <Label id={`time-${locale}`}>{label}</Label>
          <TimeInput
            aria-labelledby={`time-${locale}`}
            locale={locale}
            defaultValue="18:45"
          />
        </div>
      ))}
    </div>
  )
}

export { UiSdkTimeInputLocale }

Un champ caché envoie l’heure sous name. required bloque une heure vide ou à moitié saisie, et la réinitialisation rétablit la valeur par défaut.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { TimeInput } from "@/components/ui/ui-sdk-time-input"

function UiSdkTimeInputForm() {
  const [submitted, setSubmitted] = React.useState<string | null>(null)

  return (
    <form
      className="flex w-full max-w-40 flex-col gap-3"
      onSubmit={(event) => {
        event.preventDefault()
        const data = new FormData(event.currentTarget)
        setSubmitted(String(data.get("reminder")))
      }}
    >
      <div className="flex flex-col gap-2">
        <Label id="reminder-time">Remind me at</Label>
        <TimeInput
          aria-labelledby="reminder-time"
          name="reminder"
          defaultValue="08:15"
          required
        />
      </div>
      <div className="flex items-center gap-3">
        <Button type="submit" size="sm">
          Save
        </Button>
        <Button type="reset" size="sm" variant="ghost">
          Reset
        </Button>
      </div>
      <p className="m-0 text-sm text-muted-foreground tabular-nums">
        {submitted ? `Submitted ${submitted}` : "Nothing submitted yet."}
      </p>
    </form>
  )
}

export { UiSdkTimeInputForm }

createTimeInputController() définit, vide ou cible le champ depuis n’importe où. Ciblez une partie par son unité, comme "minute".

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import {
  TimeInput,
  createTimeInputController,
} from "@/components/ui/ui-sdk-time-input"

function now() {
  const date = new Date()
  return `${String(date.getHours()).padStart(2, "0")}:${String(
    date.getMinutes()
  ).padStart(2, "0")}`
}

function UiSdkTimeInputController() {
  const [controller] = React.useState(createTimeInputController)
  const [value, setValue] = React.useState<string | null>(null)

  return (
    <div className="flex w-full max-w-56 flex-col gap-2">
      <Label id="log-time">Logged at</Label>
      <TimeInput
        aria-labelledby="log-time"
        controller={controller}
        onValueChange={(next) => setValue(next)}
      />
      <div className="flex items-center gap-2 pt-1">
        <Button
          size="sm"
          variant="outline"
          onClick={() => controller.setValue(now())}
        >
          Now
        </Button>
        <Button
          size="sm"
          variant="ghost"
          onClick={() => controller.focus("minute")}
        >
          Edit minutes
        </Button>
        <Button size="sm" variant="ghost" onClick={() => controller.clear()}>
          Clear
        </Button>
      </div>
      <p className="m-0 text-xs text-muted-foreground tabular-nums">
        {value ? `Value ${value}` : "Value null"}
      </p>
    </div>
  )
}

export { UiSdkTimeInputController }

Un champ désactivé est ignoré par le focus et n’est pas envoyé.

import { Label } from "@/components/ui/label"
import { TimeInput } from "@/components/ui/ui-sdk-time-input"

function UiSdkTimeInputDisabled() {
  return (
    <div className="flex w-full max-w-40 flex-col gap-2">
      <Label id="closed-time">Last entry</Label>
      <TimeInput aria-labelledby="closed-time" defaultValue="22:00" disabled />
    </div>
  )
}

export { UiSdkTimeInputDisabled }

Un champ en lecture seule prend le focus et est envoyé, mais les touches et la saisie ne le modifient pas.

import { Label } from "@/components/ui/label"
import { TimeInput } from "@/components/ui/ui-sdk-time-input"

function UiSdkTimeInputReadOnly() {
  return (
    <div className="flex w-full max-w-40 flex-col gap-2">
      <Label id="checkout-time">Checkout</Label>
      <TimeInput
        aria-labelledby="checkout-time"
        defaultValue="11:00"
        readOnly
      />
    </div>
  )
}

export { UiSdkTimeInputReadOnly }

Les parties gardent l’ordre de la langue, et les flèches les suivent à l’écran.

import { IconClock } from "@tabler/icons-react"

import { Label } from "@/components/ui/label"
import { TimeInput, TimeInputSegments } from "@/components/ui/ui-sdk-time-input"

function UiSdkTimeInputRtl() {
  return (
    <div dir="rtl" lang="ar" className="flex w-full max-w-40 flex-col gap-2">
      <Label id="rtl-time">وقت الاجتماع</Label>
      <TimeInput aria-labelledby="rtl-time" locale="ar-EG" defaultValue="18:45">
        <IconClock aria-hidden="true" />
        <TimeInputSegments />
      </TimeInput>
    </div>
  )
}

export { UiSdkTimeInputRtl }
ToucheAction
0–9Remplit la partie, puis passe à la suivante.
↑↓Modifie la partie d’une unité, en boucle. Une partie vide part de l’heure actuelle.
PageUpPageDownModifie par grands pas, comme 15 minutes.
HomeEndVa à la première ou à la dernière valeur de la partie.
←→Passe à la partie précédente ou suivante.
BackspaceEfface le dernier chiffre, puis vide la partie, puis revient en arrière.
APChoisit AM ou PM sur une horloge de 12 heures.
EnterEnvoie le formulaire avec la saisie.

La racine est un groupe. Chaque partie est un spinbutton nommé par son unité, comme « hour », avec sa valeur et sa plage, et une partie vide se lit « Empty ». Le texte entre les parties est masqué aux lecteurs d’écran.

Les parties sont de vrais champs, donc les téléphones affichent un pavé numérique et la saisie fonctionne avant le script de la page. Elles passent à 16px sur écran tactile pour éviter le zoom, et la partie ciblée est surlignée au lieu d’afficher un curseur.

Chaque partie enveloppe la partie UI SDK du même nom et accepte toutes ses props, y compris render et un className qui peut être une fonction de son état.

PropTypePar défaut
value
string | null–
defaultValue
string | null–
onValueChangeS’exécute quand chaque partie est remplie, et avec null quand l’une est vidée.
(value: string | null, details) => void–
hourCyclePar défaut, l’horloge de la langue.
12 | 24–
granularity
"hour" | "minute" | "second""minute"
min
string–
max
string–
localePar défaut, la langue de la page.
string–
invalid
booleanfalse
disabled
booleanfalse
readOnly
booleanfalse
required
booleanfalse
name
string–
form
string–
controllerIssu de createTimeInputController().
TimeInputController–
childrenPar défaut, TimeInputSegments.
ReactNode | (state) => ReactNode–
render
ReactElement | (props, state) => ReactElement–
AttributDescription
data-slot="time-input"Pour cibler le champ d’heure en CSS.
data-invalidPrésent quand l’heure est hors de min et max, ou quand invalid est défini.
data-emptyPrésent tant que toutes les parties sont vides.
data-disabledPrésent lorsque l’élément est désactivé.
data-readonlyPrésent en lecture seule.
data-requiredPrésent lorsqu’elle est requise.

Affiche un TimeInputSegment pour chaque partie, dans l’ordre de la langue, avec le texte entre elles.

PropTypePar défaut
segmentL’un des segments de l’état de la racine.
TimeInputSegmentData–
render
ReactElement | (props, state) => ReactElement–
AttributDescription
data-slot="time-input-segment"Pour cibler une partie en CSS.
data-typehour, minute, second, dayPeriod, ou literal pour le texte entre les parties.
data-emptyPrésent tant que la partie est vide.