Repository: wagtail/wagtail
File: client/src/controllers/RevealController.ts (TypeScript)

Code before:
  declare readonly hasOpenIconClass: string;
  declare readonly hasToggleTarget: boolean;
  declare readonly initialClasses: string[];
  declare readonly openedClasses: string[];
  declare readonly openedContentClasses: string[];
  declare readonly openIconClass: string;
  declare readonly peekTargetValue: string;
  declare readonly storeValue: string;
  declare readonly toggleTarget: HTMLButtonElement;
  declare readonly toggleTargets: HTMLButtonElement[];

  cleanUpPeekListener?: () => void;

  connect() {
    // If peeking is being used, set up listener and its removal on disconnect

    const peekZone = this.peekTargetValue
      ? this.element.closest<HTMLElement>(this.peekTargetValue)
      : false;

    if (peekZone) {
      const onMouseLeave = () => {
        if (this.peekingValue) this.close();
        this.peekingValue = false;
      };
      peekZone.addEventListener('mouseleave', onMouseLeave, { passive: true });
      this.cleanUpPeekListener = () => {
        peekZone.removeEventListener('mouseleave', onMouseLeave);
      };
    }

    // Dispatch initial event & class removal after timeout (allowing other JS content to load)

    new Promise((resolve) => {
      setTimeout(resolve);
    }).then(() => {
      this.dispatch('ready', {
        cancelable: false,
        detail: {
          closed: this.closedValue,
        },
      });
    });
  }

  closedValueChanged(shouldClose: boolean, previouslyClosed?: boolean) {
    if (previouslyClosed === shouldClose) return;

    const closedClasses = this.closedClasses;
    const openedClasses = this.openedClasses;
    const contentTargets = this.contentTargets;

Review comment (suggestion):
The other thing that's needed is actually reading out the stored value from local storage, not sure if it worked for you but from what I could test, it never actually had any code to read from local storage.

Here's an updated `connect` method that works for me, along with a bit of JSDoc help for us all future devs.

```ts

  /**
   * Connect the controller, setting up the peeking listener if required,
   * and setting the initial state based on the stored value if available.
   *
   * Finally, dispatch the ready event to signal the controller is ready.
   */
  connect() {
    // If peeking is being used, set up listener and its removal on disconnect

    const peekZone = this.peekTargetValue
      ? this.element.closest<HTMLElement>(this.peekTargetValue)
      : false;

    if (peekZone) {
      const onMouseLeave = () => {
        if (this.peekingValue) this.close();
        this.peekingValue = false;
      };
      peekZone.addEventListener('mouseleave', onMouseLeave, { passive: true });
      this.cleanUpPeekListener = () => {
        peekZone.removeEventListener('mouseleave', onMouseLeave);
      };
    }

    // Set initial state based on stored value if available

    if (this.storageKeyValue) {
      const isStoredAsClosed = this.stored;
      if (
        typeof isStoredAsClosed === 'boolean' &&
        isStoredAsClosed !== this.closedValue
      ) {
        this.closedValue = isStoredAsClosed;
      }
    }

    // Dispatch initial event & class removal after timeout (allowing other JS content to load)

    new Promise((resolve) => {
      setTimeout(resolve);
    }).then(() => {
      this.dispatch('ready', {
        cancelable: false,
        detail: { closed: this.closedValue },
      });
    });
  }
```

Let's add a JSDoc for the `closedValueChanged` method also, feel free to refine.

```ts
  /**
   * Process the change in the closed value, updating the classes and
   * aria-expanded attributes on the element and its toggles.
   *
   * Remember that this may not trigger when clicking the toggle button,
   * as the open state may already be set because it's in peeking mode.
   */
  closedValueChanged(shouldClose: boolean, previouslyClosed?: boolean) {
```

Code after:
    closed: { default: false, type: Boolean },
    peeking: { default: false, type: Boolean },
    peekTarget: { default: '', type: String },
    storageKey: { default: '', type: String },
  };

  declare closedValue: boolean;
  declare peekingValue: boolean;

  declare readonly closedClasses: string[];
  declare readonly closeIconClass: string;
  declare readonly hasCloseIconClass: string;
  declare readonly hasOpenIconClass: string;
  declare readonly openedClasses: string[];
  declare readonly openedContentClasses: string[];
  declare readonly openIconClass: string;

  /** Content element target, to be shown/hidden with classes when opened/closed. */
  declare readonly contentTargets: HTMLElement[];
  /** Global selector string to be used to determine the container to add the mouseleave listener to. */
  declare readonly peekTargetValue: string;
  /**  Local storage key to be used to backup the open state of this controller, this can be unique or shared across multiple controllers, it uses the controller identifier for the base.If not provided, the controller will not attempt to store the state to local storage. */
  declare readonly storageKeyValue: string;
  /**  Toggle button element(s) to have their classes and aria attributes updated. */
  declare readonly toggleTargets: HTMLButtonElement[];

  cleanUpPeekListener?: () => void;

  /**
   * Connect the controller, setting up the peeking listener if required,
   * and setting the initial state based on the stored value if available.
   *
   * Finally, dispatch the ready event to signal the controller is ready.
   */
  connect() {
    // If peeking is being used, set up listener and its removal on disconnect

    const peekZone = this.peekTargetValue
      ? this.element.closest<HTMLElement>(this.peekTargetValue)
      : false;

    if (peekZone) {
      const onMouseLeave = () => {
        if (this.peekingValue) this.close();
        this.peekingValue = false;
      };
      peekZone.addEventListener('mouseleave', onMouseLeave, { passive: true });
      this.cleanUpPeekListener = () => {
        peekZone.removeEventListener('mouseleave', onMouseLeave);
      };
    }