Common

useEscapeWhileOpen

Composable che, a pannello aperto, chiude il pannello con l'Escape premuto fuori da campo e pannello (focus sul body), fermandolo prima che chiuda anche il Dialog contenitore.

Composable comune — usato da useDropdownLifecycle (Select, MultiSelect, TreeSelect, AutoComplete) e da DatePicker, DateTimePicker e TimePicker. È esportato dal pacchetto per i componenti custom con un pannello teleportato.

Il problema

I campi con tendina fermano l'Escape nei propri keydown, così con la tendina aperta Escape chiude solo la tendina e il Dialog si chiude al tasto successivo (un livello alla volta). Ma quei listener ricevono l'evento solo se il focus è dentro il campo o il pannello: dopo il clic su un elemento non focalizzabile (un'opzione, lo sfondo del pannello, un pulsante che poi sparisce) il focus finisce sul body, l'Escape parte da lì e arriva dritto al listener del Dialog su document, chiudendo insieme tendina e dialog e perdendo quanto scritto.

Firma TypeScript

export function useEscapeWhileOpen(options: UseEscapeWhileOpenOptions): void

export interface UseEscapeWhileOpenOptions {
  /** Stato del pannello: il listener e' registrato solo a pannello aperto. */
  isOpen: WatchSource<boolean>
  /** Campo e pannello: un Escape nato qui dentro lo gestisce gia' il componente. */
  inside: () => (Node | null | undefined)[]
  /** Chiude il pannello; focusLost e' true se il focus era sul body. */
  close: (focusLost: boolean) => void
}

Parametri

ParametroTipoDescrizione
isOpenWatchSource<boolean>Stato del pannello (ref, computed o getter). Il listener esiste solo a pannello aperto
inside() => (Node | null | undefined)[]Elementi del componente — di norma campo e pannello teleportato. Gli Escape nati qui dentro restano ai gestori del componente; i valori nulli sono ignorati
close(focusLost: boolean) => voidChiude il pannello. Con focusLost a true il focus era sul body, e il componente può riportarlo sul campo senza toglierlo a un altro elemento

Comportamento

  • A pannello aperto registra un listener keydown su document in fase di cattura: parte prima dei listener in bubbling di Dialog e Drawer sullo stesso nodo, e lo stopPropagation() li salta.
  • Interviene solo per l'Escape nato fuori dagli elementi di inside; gli altri tasti e gli Escape interni passano senza modifiche.
  • Rimuove il listener alla chiusura del pannello e allo smontaggio del componente.
  • useDropdownLifecycle lo usa già: un componente costruito su quel composable non deve aggiungerlo.

Esempio

import { nextTick, ref } from 'vue'
import { useEscapeWhileOpen } from '@pzeta/vue-components'

const isOpen = ref(false)
const campoRef = ref<HTMLElement | null>(null)
const pannelloRef = ref<HTMLElement | null>(null) // pannello teleportato nel body
const inputRef = ref<HTMLInputElement | null>(null)

function chiudi() {
  isOpen.value = false
}

useEscapeWhileOpen({
  isOpen,
  inside: () => [campoRef.value, pannelloRef.value],
  close: (focusLost) => {
    chiudi()
    if (focusLost) nextTick(() => inputRef.value?.focus())
  },
})

Per gli Escape interni il componente resta responsabile: a pannello aperto chiama event.stopPropagation() sull'Escape che lo chiude (closeDropdownOnEscape di useDropdownLifecycle lo fa già).