HextaUI

BetaFeito sobre o UI SDK 0.1.0-beta.2 da Vercel. A API pode mudar.

Time input

Um campo de hora no UI SDK da Vercel digitado parte por parte, em relógio de 12 ou 24 horas com segundos opcionais, mínimo e máximo, e um valor de 24 horas para formulários.

Depende 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

Adiciona o componente, os tokens de tema do HextaUI e quaisquer componentes do HextaUI dos quais ele depende.

O UI SDK adiciona um pouco de CSS próprio em uma camada de cascata chamada ui-sdk. Declare essa camada no topo da sua folha de estilos global, acima do import do Tailwind, para que suas classes vençam.

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

O valor é uma hora de 24 horas como "14:30", igual à de um input de hora nativo, seja qual for o relógio que o campo mostra. Ele continua null até que todas as partes tenham algo. Cada parte é um input pequeno: digite números e ele avança, ou pressione A ou P para a metade do dia.

O campo é um grupo, então nomeie-o com aria-labelledby ou aria-label. Clicar em qualquer ponto da caixa foca a parte mais próxima.

TimeInput
└── TimeInputSegments
    └── TimeInputSegment

TimeInput renderiza suas partes sozinho. Adicione TimeInputSegments você mesmo para colocar um ícone ou outro conteúdo ao lado.

O campo mostra o relógio do idioma. hourCycle escolhe 12 ou 24 horas, e granularity adiciona segundos ou mostra só a hora. Todos enviam valores de 24 horas.

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 }

Uma hora antes de min ou depois de max marca o campo como inválido, e o formulário não é enviado.

"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 marca o campo para verificações próprias, como um horário de término antes do início.

"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 define a ordem, os separadores e os nomes da metade do dia. Sem ele, o campo segue o idioma da página quando fica interativo.

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 }

Um input oculto envia a hora sob name. required impede uma hora vazia ou digitada pela metade, e redefinir traz de volta o padrão.

"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() define, limpa ou foca o campo de qualquer lugar. Foque uma parte pela unidade, como "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 }

Um campo desativado é pulado pelo foco e não é enviado.

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 }

Um campo somente leitura recebe foco e é enviado, mas teclas e digitação não o alteram.

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 }

As partes mantêm a ordem do idioma, e as setas as seguem na tela.

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 }
TeclaAção
0–9Preenche a parte e passa para a próxima.
↑↓Muda a parte em um, dando a volta. Uma parte vazia começa na hora atual.
PageUpPageDownMuda mais, como 15 minutos.
HomeEndVai ao primeiro ou último valor da parte.
←→Vai para a parte anterior ou seguinte.
BackspaceApaga o último dígito, depois esvazia a parte e então volta.
APEscolhe AM ou PM num relógio de 12 horas.
EnterEnvia o formulário com o que foi digitado.

A raiz é um grupo. Cada parte é um spinbutton nomeado pela unidade, como “hour”, com seu valor e intervalo, e uma parte vazia é lida como “Empty”. O texto entre as partes fica oculto para leitores de tela.

As partes são inputs reais, então os celulares mostram um teclado numérico e a digitação funciona antes do script da página rodar. Elas crescem para 16px em telas de toque para os celulares não darem zoom, e a parte focada fica destacada em vez de mostrar um cursor.

Cada parte envolve a parte do UI SDK de mesmo nome e aceita todas as suas props, incluindo render e um className que pode ser uma função do estado da parte.

PropTipoPadrão
value
string | null–
defaultValue
string | null–
onValueChangeExecuta quando todas as partes têm algo, e com null quando uma é esvaziada.
(value: string | null, details) => void–
hourCyclePor padrão, o relógio do idioma.
12 | 24–
granularity
"hour" | "minute" | "second""minute"
min
string–
max
string–
localePor padrão, o idioma da página.
string–
invalid
booleanfalse
disabled
booleanfalse
readOnly
booleanfalse
required
booleanfalse
name
string–
form
string–
controllerDe createTimeInputController().
TimeInputController–
childrenPor padrão, TimeInputSegments.
ReactNode | (state) => ReactNode–
render
ReactElement | (props, state) => ReactElement–
AtributoDescrição
data-slot="time-input"Para mirar o campo de hora no CSS.
data-invalidPresente quando a hora está fora de min e max, ou quando invalid está definido.
data-emptyPresente enquanto todas as partes estão vazias.
data-disabledPresente quando desabilitado.
data-readonlyPresente quando somente leitura.
data-requiredPresente quando obrigatório.

Renderiza um TimeInputSegment para cada parte, na ordem do idioma, com o texto entre elas.

PropTipoPadrão
segmentUm dos segments do estado da raiz.
TimeInputSegmentData–
render
ReactElement | (props, state) => ReactElement–
AtributoDescrição
data-slot="time-input-segment"Para mirar uma parte no CSS.
data-typehour, minute, second, dayPeriod, ou literal para o texto entre as partes.
data-emptyPresente enquanto a parte está vazia.