Saltar al contenido
zabloo

Collapse

Una sección que se abre y se cierra. Su contenido sale de verdad del layout al cerrarse, así que todo lo de debajo sube a ocupar el hueco.

primitivedesde v1sin focus

Un Collapse es una sección que el jugador puede abrir y cerrar. Su primer hijo es la cabecera —tocarla abre o cierra la sección— y todo lo que va detrás es el contenido, que sale de verdad del layout cuando la sección se cierra, así que los hermanos de abajo suben a llenar el hueco. Piensa en una página de ajustes larga donde solo ocupa sitio la sección en la que estás. Es además el nodo que demostró que este formato nunca podría publicar rectángulos ya cocinados: una pantalla que lleve uno se vuelve a medir mientras el jugador la está mirando.

collapse-accordion.viewIR v1
Viewport

Viewport: 960 × 360

An Options panel with two collapsible sections: Audio, open, showing Master volume 80 and Music 45, and Video, closed, showing only its header.
ESTADO
  • hover — inactivo
  • pressed — inactivo
  • focused — inactivo
  • selected — inactivo
  • disabled — inactivo

Se lee del último frame dibujado

Parado: pulsa Ejecutar para dibujarlo en la GPU.

import { Accordion, Button, Collapse, Column, Row, Text } from "@zabloo/react";


/** A header is `children[0]`, and tapping it toggles — whatever it is. */
function Header({ label }: { label: string }) {
  return (
    <Button
      variant="secondary"
      layout={{ padding: "{space.3}", justify: "start", align: "center" }}
    >
      <Text style={{ color: "{color.text}", fontSize: "{text.sm}" }}>{label}</Text>
    </Button>
  );
}

function Line({ name, value }: { name: string; value: string }) {
  return (
    <Row layout={{ justify: "space-between", align: "center" }}>
      <Text style={{ color: "{color.muted}", fontSize: "{text.sm}" }}>{name}</Text>
      <Text style={{ color: "{color.text}", fontSize: "{text.sm}" }}>{value}</Text>
    </Row>
  );
}

export default function CollapseAccordion() {
  return (
    <Column
      // `align: "stretch"` on the ROOT is what hands the panel the view's own
      // width; centring it instead would pin the panel to its content and no
      // change of viewport could reach it (ZAB-153).
      layout={{ grow: 1, justify: "center", align: "stretch", padding: "{space.6}" }}
      style={{ background: "{color.bg}" }}
    >
      <Column
        id="panel"
        layout={{ padding: "{space.5}", gap: "{space.4}", align: "stretch" }}
        style={{
          background: "{color.surface}",
          radius: "{radius.lg}",
          borderWidth: "{border.hairline}",
          borderColor: "{color.line}",
        }}
      >
        <Text style={{ color: "{color.text}", fontSize: "{text.lg}" }}>Options</Text>

        <Accordion layout={{ gap: "{space.2}", align: "stretch" }}>
          <Collapse
            id="audio"
            open
            transition={{ duration: "{motion.fast}" }}
            layout={{ gap: "{space.2}", align: "stretch" }}
          >
            <Header label="Audio" />
            {/* Everything from here down is content: it leaves the layout when
                the section closes, and the siblings below move up. */}
            <Column
              layout={{ padding: "{space.3}", gap: "{space.2}", align: "stretch" }}
              style={{ background: "{color.slot}", radius: "{radius.md}" }}
            >
              <Line name="Master volume" value="80" />
              <Line name="Music" value="45" />
            </Column>
          </Collapse>

          <Collapse
            id="video"
            open={false}
            transition={{ duration: "{motion.fast}" }}
            layout={{ gap: "{space.2}", align: "stretch" }}
          >
            <Header label="Video" />
            <Column
              layout={{ padding: "{space.3}", gap: "{space.2}", align: "stretch" }}
              style={{ background: "{color.slot}", radius: "{radius.md}" }}
            >
              <Line name="Resolution" value="1920 × 1080" />
              <Line name="Quality" value="High" />
            </Column>
          </Collapse>
        </Accordion>
      </Column>
    </Column>
  );
}
Dos secciones bajo un mismo acordeón. Pulsa Run y abre Video, y mira lo que se mueve: Audio se cierra, todo lo que hay debajo viaja hacia arriba, y el panel entero se vuelve a medir — en vivo, a partir de un documento que no dice la altura de ninguna de las dos secciones.

Las dos tablas de abajo son casi la misma, que en este nodo es justo lo interesante: lo único que lleva el formato es dónde empieza la sección. Si está abierta ahora mismo es estado de ejecución que guarda el SDK, y no está por ninguna parte de lo que se publica.

Props de autoría

Lo que escribes en @zabloo/react.

PropTipoPor defectoDescripción
openbooleantrueEstado de apertura inicial.
childrenReactNodeabsentPrimer hijo = cabecera; el resto = contenido.

Además de estas, todo componente acepta las props base del nodo —id, visible, disabled, layout, style, states, transition, autofocus, clip— más variant, que el theme resuelve al exportar y que nunca aparece en la IR.

Props de IR

Lo que llega al juego, cuando ya no queda capa de autoría.

PropTipoPor defectoDescripción
openbooleantrueEstado inicial. No enlazable — el estado de ejecución es del SDK.
childrenZNode[][]children[0] = cabecera; children[1..] = contenido plegable.
open es por dónde empieza, no dónde está

A partir de ahí es estado de ejecución que guarda el SDK —como el pressed de un Button— y no sobrevive ni a serializarse ni a una recarga. No es enlazable a propósito: un open enlazado haría al juego dueño de algo que está sosteniendo el jugador, y los dos se pelearían. Un juego que necesite manejarlo lo dice por fuera, con SetOpen(id, open).

Los slots

  • children[0] es la cabecera. Siempre está en el layout, y tocarla abre o cierra la región. Es focusable sea lo que sea: la cabecera de un Collapse entra en el conjunto de lo focusable igual que lo hace un Button.
  • children[1..] es el contenido. Entra y sale del layout con la semántica de display:none —el mismo y único mecanismo de ocultar que visible—, así que cerrar lo quita de verdad y los hermanos de abajo suben.

Comportamiento

Estados

El Collapse en sí no lleva ninguno propio; su cabecera lleva hover, pressed y focused. Ese pressed viene solo del teclado o del mando: una cabecera no es uno de los tipos que el puntero pulsa, así que un toque abre o cierra la sección sin llegar a encender el estado de “pulsado”. Los dos llevan disabled: declarado en el Collapse le llega a la cabecera, y una cabecera deshabilitada ya no abre ni cierra la sección.

Focusable

El nodo en sí no lo es; su cabecera sí, sea el tipo de nodo que sea. Es el único sitio del formato donde un hijo entra en el conjunto de lo focusable por su posición.

Movimiento

Con una transition, un Collapse anima su propia altura entre cerrado y la altura natural de su contenido, y recorta mientras lo hace — así que el contenido lo corta la caja que se cierra sobre él, sin que quien lo escribe tenga que pedir ningún clip.

Actions

El jugador toca la cabecera → la sección se abre o se cierra y el layout de debajo se recoloca → el juego no oye nada. Abrir no es una named action. Si el juego sí necesita enterarse, haz que la cabecera sea un Button con un onClick; el otro sentido es el host channel, con SetOpen(id, open).

Dentro de un acordeón

Dentro de un Container con group: "exclusive-open", abrir un Collapse cierra a sus hermanos. El comportamiento es del padre: el Collapse no declara nada al respecto, y un SDK que ignore el group deja collapses independientes. Ese reparto es el mismo que sigue cualquier comportamiento de grupo — el hijo sigue siendo un hijo, y la regla vive un nivel más arriba.

Degradación

En un SDK más antiguo

En un SDK más antiguo todas las secciones están abiertas y siguen abiertas. No se esconde nada — pero tampoco se puede recoger nada.

Como Container: se ve todo, abierto para siempre, y la cabecera es un hijo más. No se pierde nada — lo que se va es la capacidad de cerrarlo, que es la forma menos dañina en que puede fallar un control de plegado.

AudioVideo

AudioMaster volumeVideoResolution

Composición

La cabecera es un nodo normal, así que su aspecto es una cuestión de layout.

  • Un Button como cabecera — cuando el juego necesite oír que la sección se ha abierto, ya que el plegado en sí no envía nada.
  • Un Row con una etiqueta y una flecha — para el caso normal, en el que la cabecera es solo un asa.
  • Un <Accordion> alrededor de varios — cuando las secciones sean lo bastante largas como para que dos abiertas a la vez entierren al resto.
  • Una transition — cuando la sección sea corta; en una larga, animar la altura es una forma lenta de decir lo mismo.
// A section that starts closed. The header is children[0], whatever it is.
<Collapse open={false}>
<Button onClick="audio-toggled"><Text>Audio</Text></Button>
<Slider value={{ bind: "settings.volume" }} />
</Collapse>

// An accordion: the "only one open" rule is the parent's, not the children's.
<Accordion layout={{ gap: 8 }}>
<Collapse open={false}>…</Collapse>
<Collapse open={false}>…</Collapse>
</Accordion>

// With a transition it animates its own height, and clips while it does.
<Collapse open transition={{ duration: 160 }}>
<Text>Advanced</Text>
<Column layout={{ gap: 8 }}>…</Column>
</Collapse>

Relacionado