Tucano v0.37.2

Date picker

Transforma um <input type="text"> num campo de data com calendário, período e hora. Na tela aparece o formato do idioma da página; no formulário vai ISO, que é o que o Django espera. A digitação com máscara continua valendo, para quem digita mais rápido do que clica.

Exemplos

O mesmo <input> com atributos diferentes. Embaixo de cada campo aparece o valor ISO, que é o que chega no servidor.

Data e hora

data-time="true" acrescenta as colunas de hora e minuto, de 5 em 5.

Período com hora

Com hora o painel não fecha sozinho: o botão Aplicar confirma.

Minuto a minuto, com segundos

data-minute-step="1" lista de 00 a 59; data-seconds="true" põe a terceira coluna.

Limites

data-min e data-max em aaaa-mm-dd. Fora deles o dia fica desativado.

Só dias úteis, a partir de hoje

disabledDates recebe uma função e não existe como atributo — vai em JavaScript.

Atalhos de período

data-presets="true" liga Hoje, Últimos 7 dias, Este mês e os outros.

Atalhos próprios

Uma lista de { label, value() }, em JavaScript.

Número da semana

data-week-numbers="true" mostra a semana ISO-8601 à esquerda.

Outro idioma

data-locale="en-US": mês antes do dia, semana começando no domingo, nomes em inglês.

Formato próprio

data-format="dd.MM.yyyy". A máscara acompanha o formato sozinha.

Com ícone

O .tuc-input-group do formulário, com um SVG antes do campo.

Com erro

aria-invalid="true" no campo — o Django 5 já escreve.

A data precisa ser depois de hoje.

Como usar

Marque o <input> e ele inicializa sozinho no carregamento e a cada htmx:afterSwap. O componente veste a classe .tuc-input no campo, então não precisa escrevê-la.

<input type="text" name="date" data-tuc-datepicker>
<input type="text" name="when" data-tuc-datepicker data-time="true">
<input type="text" name="period" data-tuc-datepicker data-mode="range">
<input type="text" name="window" data-tuc-datepicker data-mode="range" data-time="true">
AtributoPadrãoPara quê
data-modesinglerange para período
data-timefalseAcrescenta o seletor de hora
data-minute-step5Degrau da coluna de minutos; 1 lista todos
data-secondsfalseColuna de segundos
data-min data-max—Limites, em aaaa-mm-dd
data-months1, ou 2 em períodoQuantos meses lado a lado
data-presetsfalsetrue liga os atalhos — só no modo período
data-week-numbersfalseColuna com a semana ISO
data-locale<html lang>Nomes, ordem de dia e mês, início da semana e relógio de 12 ou 24 horas
data-formatdo idiomaFormato de exibição, como dd/MM/yyyy
data-iso-nameo namename do campo escondido com o ISO
data-placementbottom-centerLado e alinhamento do painel
data-nativefalsetrue usa o seletor do sistema

No formulário do Django

class EventForm(forms.ModelForm):
    class Meta:
        model = Event
        fields = ["date", "start", "period"]
        widgets = {
            "date": forms.TextInput(attrs={"data-tuc-datepicker": ""}),
            "start": forms.TextInput(attrs={"data-tuc-datepicker": "", "data-time": "true"}),
            "period": forms.TextInput(attrs={"data-tuc-datepicker": "", "data-mode": "range"}),
        }

Não use type="date" no widget: ele ligaria o calendário do navegador por cima do nosso. O valor inicial pode vir em ISO ou no formato do idioma — o componente lê os dois.

Em JavaScript

Para o que não cabe em atributo — disabledDates, atalhos próprios, firstDayOfWeek, autoApply, clearable, onChange — e para ler e escrever o valor.

const dp = new Tucano.DatePicker('#delivery', {
  mode: 'range',
  time: true,
  minuteStep: 15,
  min: '2026-01-01',
  disabledDates: (d) => d.getDay() === 0,   // domingo fechado
  onChange: (value, { iso }) => console.log(value, iso),
});

dp.setValue({ start: '2026-03-01', end: '2026-03-15' });
dp.getValue();   // { start: Date, end: Date } — no modo simples, um Date
dp.clear();
dp.open();
dp.destroy();
// Atalhos próprios: value() devolve { start, end } e é chamado a cada abertura.
new Tucano.DatePicker('#period', {
  mode: 'range',
  presets: [
    { label: 'Próximos 7 dias', value: () => {
      const today = Tucano.dates.startOfDay(new Date());
      return { start: today, end: Tucano.dates.addDays(today, 6) };
    } },
  ],
});

Valor e formulário

O campo visível mostra 25/12/2026; um <input type="hidden"> criado ao lado leva o name e o valor em ISO. O servidor recebe ISO e nunca o texto formatado, que mudaria com o idioma de quem preencheu.

POST  date    = 2026-12-25
POST  start   = 2026-12-25T09:30          # DateTimeField lê direto
POST  exact   = 2026-12-25T09:30:15       # com data-seconds
POST  period  = 2026-03-01,2026-03-15     # início e fim, separados por vírgula

DateField e DateTimeField fazem o parse disso sem configuração. O período chega num campo só; separe no clean_:

class BookingForm(forms.Form):
    period = forms.CharField()

    def clean_period(self):
        start, _, end = self.cleaned_data["period"].partition(",")
        return date.fromisoformat(start), date.fromisoformat(end)

Para editar, devolva o valor em ISO no value — o componente lê ISO e também o formato do idioma. Com data-iso-name o campo visível fica com o próprio name e o escondido ganha outro, e os dois são postados. Campo sem name e sem data-iso-name não ganha escondido nenhum.

Cada escolha dispara tucano:change no campo visível, e também o change nativo, então validação e hx-trigger="change" enxergam o valor.

document.querySelector('#due_date').addEventListener('tucano:change', (e) => {
  e.detail.value;      // Date, ou { start, end } no período
  e.detail.iso;        // '2026-12-25' — o mesmo texto do campo escondido
  e.detail.instance;   // o DatePicker
});

Aqui e.target.name vem vazio

O evento sai do campo visível, e o name mudou para o escondido. Use e.detail.iso, ou leia o escondido, que fica logo depois do campo.

Digitação

O campo tem máscara tirada do próprio formato de exibição, então ela acompanha o idioma sem configuração. Digitam-se só os números; barra, dois-pontos e o travessão do período entram sozinhos.

ModoMáscara em pt-BR
Datadd/mm/aaaa
Data e horadd/mm/aaaa hh:mm
Períododd/mm/aaaa — dd/mm/aaaa
Período com horadd/mm/aaaa hh:mm — dd/mm/aaaa hh:mm

Com a máscara completa o calendário já pula para a data digitada, sem fechar e sem mudar o valor. O valor se confirma ao sair do campo ou com Enter enquanto o painel está aberto, que também fecha; Esc descarta o que foi digitado. Texto que não vira data volta ao valor anterior, e data fora de min, max ou disabledDates não é aceita — nem puxada para o limite. Período sem um fim válido é recusado inteiro. Apagar em cima de um separador remove o dígito vizinho, em vez de travar.

Formato com nome de mês (MMMM) ou AM/PM não tem como ser mascarado; ali o campo fica livre e vale a leitura tolerante, que aceita 25/12/26, 25-12-2026, 25122026, 2512 (ano corrente) e 12/25/2026 2:05 pm. Digitar só a data num campo com hora mantém a hora que já estava escolhida.

Clicar no mês do cabeçalho troca para a grade de meses, e clicar no ano, para a de anos — assim uma data de nascimento não custa trinta cliques na seta.

No celular

O painel é o mesmo do desktop, adaptado ao toque: texto de 16px, células maiores, um mês embaixo do outro no período e os atalhos numa faixa que rola na horizontal.

Os 16px não são estética: o Safari do iOS dá zoom na página inteira ao focar campo com fonte menor. Pelo mesmo motivo, em tela estreita com toque o campo não recebe foco nem máscara — tocar abre o painel, e o teclado do sistema não sobe para cobrir o calendário.

O seletor do próprio sistema é opcional:

native: false     // padrão: o painel em todo lugar
native: 'auto'    // o seletor do sistema onde o ponteiro é de toque
native: true      // sempre o seletor do sistema — por atributo, data-native="true"

O modo nativo não troca o type do seu campo

Trocar para type="date" fazia todo CSS escrito como input[type=text] parar de casar, e o campo virava um input cru do navegador. Agora um input nativo transparente fica por cima, do tamanho do campo, e o POST continua sendo o do escondido. Período nunca usa o nativo: não existe seletor de intervalo em HTML.

Teclado e acessibilidade

O campo anuncia que abre um dialog, a grade é role="grid" e cada dia tem aria-selected e o nome por extenso — "sexta-feira, 25 de dezembro de 2026". Chegar de Tab não abre o painel.

TeclaOndeAção
↓CampoAbre o calendário
EspaçoCampo vazioAbre o calendário
EnterCampo, painel abertoConfirma o que foi digitado e fecha
← →GradeDia anterior e seguinte
↑ ↓GradeMesmo dia na semana anterior e na seguinte
PageUp PageDownGradeMês anterior e seguinte; com Shift, ano
Home EndGradeInício e fim da semana
Enter EspaçoGradeEscolhe o dia
↑ ↓Coluna de horaValor anterior e seguinte da coluna
Home EndColuna de horaPrimeiro e último valor da coluna
Enter EspaçoColuna de horaEscolhe o valor
TabCampo, painel abertoSegue para o próximo campo e fecha o painel
TabDentro do painelAnda entre os controles sem sair do painel; cada coluna de hora é uma parada só
EscCampo ou painelFecha, descarta o que não foi confirmado e devolve o foco ao campo

Por que Tab não abre, e Enter também não

Quem tabula por um formulário até o botão de salvar não deveria levar um calendário na cara a cada campo, cobrindo o seguinte — era o que empilhava painéis. E este é um campo de texto dentro de um <form>: Enter ali envia o formulário, que é o que se espera depois de digitar a data. Por isso quem abre é o Espaço, e só com o campo vazio, porque com hora se digita 07/09/2026 14:30.

Clique fora fecha sem roubar o foco de onde se clicou; um período escolhido pela metade é descartado ao fechar, porque não existe meio intervalo, e o período que já estava escolhido volta. Com o botão Aplicar — o padrão com hora, ou autoApply: false —, dia, hora e atalho ficam pendentes: só o Aplicar confirma e dispara tucano:change, uma vez, e fechar com Esc ou clique fora descarta a escolha. Sem ele, cada escolha vale na hora. Com prefers-reduced-motion o painel só esmaece, sem deslizar.

API

Gerada do código a cada build — se algo não está aqui, não existe.

Marcação
[data-tuc-datepicker]
Em JS
new Tucano.DatePicker(alvo, opcoes)
Atributos
data-format data-iso-name data-locale data-max data-min data-minute-step data-mode data-months data-native data-placement data-presets data-seconds data-time data-week-numbers
Métodos
getValue setValue clear open close toggle destroy
Eventos
tucano:change

Opções

OpçãoPadrãoPara quê
mode'single''single' | 'range'
timefalsetrue habilita seletor de hora
secondsfalse
minuteStep5
localeundefineddefault: locale do documento/navegador
formatundefineddefault: padrao numerico do locale
firstDayOfWeekundefined
monthsundefineddefault: 2 em range, 1 em single
minnull
maxnull
disabledDatesnull(date) => boolean
presetsfalseatalhos de periodo (Hoje, Ultimos 7 dias...): opt-in
autoApplyundefineddefault: true sem hora, false com hora. Sem autoApply, a escolha so vale no Aplicar
clearabletrue
weekNumbersfalse
placement'bottom-center'centralizado no campo; as bordas da tela ainda mandam
appendToundefined
isoNameundefinedname do input hidden com o valor ISO
nativefalse
onChangenull
onOpennull
onClosenull