HextaUI

BetaGebaut auf Vercels UI SDK 0.1.0-beta.2. Die API kann sich ändern.

Time input

Ein Zeitfeld auf Vercels UI SDK, das Teil für Teil getippt wird, mit 12- oder 24-Stunden-Uhr, optionalen Sekunden, Minimum und Maximum und einem 24-Stunden-Wert für Formulare.

Abhängig von
  • @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

Fügt die Komponente, die HextaUI-Theme-Tokens und alle HextaUI-Komponenten hinzu, von denen sie abhängt.

UI SDK bringt etwas eigenes CSS in einem Cascade Layer namens ui-sdk mit. Deklariere diesen Layer ganz oben im globalen Stylesheet, über dem Tailwind-Import, damit deine Klassen gewinnen.

@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" />

Der Wert ist eine 24-Stunden-Zeit wie "14:30", wie bei einem nativen Zeitfeld, egal welche Uhr das Feld zeigt. Er bleibt null, bis jeder Teil etwas enthält. Jeder Teil ist ein eigenes kleines Eingabefeld: Tippe Ziffern und es springt weiter, oder drücke A oder P für die Tageshälfte.

Das Feld ist eine Gruppe, also benenne es mit aria-labelledby oder aria-label. Ein Klick irgendwo in den Rahmen fokussiert den nächstgelegenen Teil.

TimeInput
└── TimeInputSegments
    └── TimeInputSegment

TimeInput rendert seine Teile selbst. Füge TimeInputSegments selbst ein, um ein Symbol oder anderen Inhalt daneben zu setzen.

Das Feld zeigt die Uhr der Locale. hourCycle wählt 12 oder 24 Stunden, und granularity fügt Sekunden hinzu oder zeigt nur die Stunde. Alle senden 24-Stunden-Werte.

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 }

Eine Zeit vor min oder nach max markiert das Feld als ungültig, und sein Formular wird nicht gesendet.

"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 markiert das Feld für eigene Prüfungen, etwa eine Endzeit vor dem Beginn.

"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 bestimmt Reihenfolge, Trennzeichen und die Namen der Tageshälften. Ohne sie folgt das Feld der Sprache der Seite, sobald es interaktiv ist.

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 }

Ein verstecktes Feld sendet die Zeit unter name. required stoppt eine leere oder halb getippte Zeit, und ein Zurücksetzen stellt den Standardwert her.

"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() setzt, leert oder fokussiert das Feld von überall. Fokussiere einen Teil über seine Einheit, etwa "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 }

Ein deaktiviertes Feld wird beim Fokussieren übersprungen und nicht gesendet.

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 }

Ein schreibgeschütztes Feld nimmt den Fokus an und wird gesendet, aber Tasten und Tippen ändern es nicht.

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 }

Die Teile behalten die Reihenfolge der Locale, und die Pfeiltasten folgen ihnen auf dem Bildschirm.

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 }
TasteAktion
0–9Füllt den Teil und springt zum nächsten.
↑↓Ändert den Teil um eins und läuft im Kreis. Ein leerer Teil beginnt bei der aktuellen Uhrzeit.
PageUpPageDownÄndert in größeren Schritten, etwa 15 Minuten.
HomeEndSpringt zum ersten oder letzten Wert des Teils.
←→Wechselt zum vorherigen oder nächsten Teil.
BackspaceLöscht die letzte Ziffer, leert dann den Teil und springt dann zurück.
APWählt AM oder PM bei einer 12-Stunden-Uhr.
EnterSendet das Formular mit der Eingabe ab.

Die Wurzel ist eine Gruppe. Jeder Teil ist ein Spinbutton, benannt nach seiner Einheit wie „hour“, mit Wert und Bereich, und ein leerer Teil wird als „Empty“ vorgelesen. Der Text zwischen den Teilen ist für Screenreader verborgen.

Die Teile sind echte Eingabefelder, daher zeigen Handys einen Ziffernblock und Tippen funktioniert, bevor das Skript der Seite läuft. Auf Touchscreens wachsen sie auf 16px, damit Handys nicht zoomen, und der fokussierte Teil wird hervorgehoben statt eine Einfügemarke zu zeigen.

Jeder Teil umhüllt den gleichnamigen UI-SDK-Teil und nimmt alle seine Props an, auch render und einen className, der eine Funktion des Zustands sein kann.

PropTypStandard
value
string | null–
defaultValue
string | null–
onValueChangeLäuft, sobald jeder Teil etwas enthält, und mit null, wenn einer geleert wird.
(value: string | null, details) => void–
hourCycleStandardmäßig die Uhr der Locale.
12 | 24–
granularity
"hour" | "minute" | "second""minute"
min
string–
max
string–
localeStandardmäßig die Sprache der Seite.
string–
invalid
booleanfalse
disabled
booleanfalse
readOnly
booleanfalse
required
booleanfalse
name
string–
form
string–
controllerAus createTimeInputController().
TimeInputController–
childrenStandardmäßig TimeInputSegments.
ReactNode | (state) => ReactNode–
render
ReactElement | (props, state) => ReactElement–
AttributBeschreibung
data-slot="time-input"Zum Ansprechen des Zeitfelds in CSS.
data-invalidVorhanden, wenn die Zeit außerhalb von min und max liegt oder invalid gesetzt ist.
data-emptyVorhanden, solange alle Teile leer sind.
data-disabledVorhanden, wenn deaktiviert.
data-readonlyVorhanden, wenn schreibgeschützt.
data-requiredVorhanden, wenn erforderlich.

Rendert für jeden Teil ein TimeInputSegment in der Reihenfolge der Locale, mit dem Text dazwischen.

PropTypStandard
segmentEines der segments aus dem Zustand der Wurzel.
TimeInputSegmentData–
render
ReactElement | (props, state) => ReactElement–
AttributBeschreibung
data-slot="time-input-segment"Zum Ansprechen eines Teils in CSS.
data-typehour, minute, second, dayPeriod oder literal für den Text zwischen den Teilen.
data-emptyVorhanden, solange der Teil leer ist.