Hidden content renders when first shown

A Chameleon component that sits inside something closed doesn't appear until the user, or your code, shows it. Plan for that when your code looks inside a closed menu, dialog, or any other hidden surface.

What renders when⁠

Chameleon components inside a closed surface render the first time the surface is shown, not when the page loads. The same goes for the component's own open part: a menu's list, a popover's panel, a select's list, a tooltip's bubble, a dialog's panel, and a modal drawer's panel. Until the first showing, the open part doesn't exist, so there's nothing in it to measure or focus. To reach it from code, show the surface and await updateComplete, as Reaching hidden content from code shows.

Inside a hidden element⁠

A Chameleon component placed inside an element with the hidden attribute waits to render until the attribute is removed. The same holds for hidden="until-found", which keeps a closed tab panel searchable. Remove the attribute and the components inside appear.

  • To show the content, remove hidden. Page CSS that overrides hidden to show an element doesn't render the Chameleon components inside it; they stay empty until the attribute goes. Await updateComplete if you must show them that way.
  • Awaiting updateComplete renders that component at once, while it's still hidden, so your code can read it without showing it. Components inside it still wait; await each one you need.
  • Printing renders every waiting component first, and so does opening find in page with its keyboard shortcut.
  • A select is the exception and renders anyway, so a form in a closed panel still submits its value.

What reopening keeps⁠

A second opening looks and behaves like the first: the same starting item, the same scroll position, and nothing left pressed or highlighted from last time. Everything the first opening created is still there. Two things deliberately carry over.

  • Your app's state stays. A component never clears what your app put in it. A half-filled form in a dialog is still half filled when the dialog opens again, and it's the same element. A menu keeps its chosen value, and navigation keeps its selected destination.
  • A list opens where the user left off. A select opens on its chosen option, or on the first option when none is chosen, the first time and every time after.

Reaching hidden content from code⁠

Show the surface, then wait for the component's updateComplete. Its promise settles once the content and the component's open part exist, so they're there to query, measure, or focus at once, without a timer or a retry loop. Wait after setting open as well as after calling show().

Web components⁠

const menu = document.querySelector("cw-menu");
menu.open = true;
await menu.updateComplete;

const firstItem = menu.querySelector("cw-menu-item");
firstItem.focus();
content_copy

React⁠

const menuRef = useRef(null);

async function openAndFocus() {
  const menu = menuRef.current;
  menu.open = true;
  await menu.updateComplete;
  menu.querySelector("cw-menu-item").focus();
}
content_copy

What answers before the first showing⁠

Some things answer at once, with nothing shown:

  • A select's options, selectedOption, value, and label, including options you supplied as data with expressContent.
  • A menu's items, whether they're your own elements or supplied as data with expressContent. You can measure and focus them once the menu opens.
  • Your own content. The elements you put inside a component are yours, and they exist from the start, whether or not the component has shown them.

Starting each opening empty⁠

Because the component keeps your content, a dialog or side sheet that should start empty each time is yours to reset. Reset it when the component reports it has closed: listen for toggle and act when newState is "closed". A user who dismisses the dialog by accident, with Escape or a click outside, keeps their work, and an edit dialog can arrive filled before it opens.

Web components⁠

const dialog = document.querySelector("cw-dialog");
dialog.addEventListener("toggle", (event) => {
  if (event.newState === "closed") {
    dialog.querySelector("form")?.reset();
  }
});
content_copy

React⁠

const formRef = useRef(null);

<CrDialog
  onToggle={(event) => {
    if (event.newState === "closed") formRef.current?.reset();
  }}
>
  <form slot="content" ref={formRef}>...</form>
</CrDialog>
content_copy

Blazor⁠

<CbDialog OnToggle="ResetOnClose">...</CbDialog>

@code {
    private void ResetOnClose(ToggleEventArgs args)
    {
        if (args.NewState == ToggleState.Closed) draft = new();
    }
}
content_copy

Which components hide content⁠

Component What stays hidden until shown What reopening keeps
Popover The popover's panel and content Your content, exactly as you left it
Menu and sub-menu The list and its items The last chosen value
FAB menu The action buttons Nothing of its own
Select The option list, and options supplied as data The chosen option, which the list opens on
Plain and rich tooltip The bubble and its content Nothing of its own
Dialog The panel and everything in it The return value, and your form content
Navigation A closed modal drawer's panel; destinations left out at the current screen size The selected destination
Any component except a select, inside an element with hidden The whole component, until the element is shown Everything it rendered