Skip to main content

Popup

Popup is an aggregate container whose renderer mounts in the global popupHolder instead of its normal parent's renderer. This lets dropdowns and other floating content draw above the ordinary view tree.

Lumen settings popup

Source: hosanna-ui-samples-public/src/hosanna-ui-examples/rigs/wow-samples/LumenTextToSpeechDetailsPage.ts
private buildSettingsPopup(accentColor: string): ViewStruct<ViewState> {
    return Popup([
      Rectangle({ color: '#02070DEB', width: 1920, height: 1080 }),
      Group([
        Rectangle({
          color: '#0B1C27',
          width: 820,
          height: 970,
        }),
        Rectangle({
          color: accentColor,
          width: 8,
          height: 970,
        }),
        Label({
          text: 'Playback & spoken guidance',
          color: '#F3F5F7',
          width: 700,
          height: 54,
          fontKey: '~theme.fonts.heading-40',
          translation: [58, 42],
        }),
        Label({
          text: 'Every control announces its role, value, position, and result.',
          color: '#9FB0BC',
          width: 700,
          height: 36,
          fontKey: '~theme.fonts.text-regular-20',
          translation: [58, 105],
        }),
        Label({
          text: 'SPOKEN BEHAVIOUR',
          color: accentColor,
          width: 680,
          height: 28,
          fontKey: '~theme.fonts.text-bold-14',
          translation: [58, 164],
        }),
        CheckBox({
          id: 'lumenSpeechAutoNarrateCheck',
          text: 'Narrate new screens automatically',
          isChecked: this.autoNarrate,
          labelStyleKey: 'controls.Label.checkbox',
          width: 680,
          height: 52,
          translation: [58, 208],
        })
          .audioGuideHint('Press OK to toggle automatic screen introductions.')
          .nextFocusMap({ up: 'lumenSpeechDoneButton', down: 'lumenSpeechQuietCheck' })
          .onCheckChange((event) => this.setAutoNarrate(event)),
        CheckBox({
          id: 'lumenSpeechQuietCheck',
          text: 'Lower background audio while speaking',
          isChecked: this.quietBackground,
          labelStyleKey: 'controls.Label.checkbox',
          width: 680,
          height: 52,
          translation: [58, 276],
        })
          .audioGuideHint('Press OK to toggle background audio silence during narration.')
          .nextFocusMap({ up: 'lumenSpeechAutoNarrateCheck', down: 'lumenSpeechMilestonesCheck' })
          .onCheckChange((event) => this.setQuietBackground(event)),
        CheckBox({
          id: 'lumenSpeechMilestonesCheck',
          text: 'Announce playback milestones',
          isChecked: this.announceMilestones,
          labelStyleKey: 'controls.Label.checkbox',
          width: 680,
          height: 52,
          translation: [58, 344],
        })
          .audioGuideHint('Press OK to toggle spoken playback progress announcements.')
          .nextFocusMap({ up: 'lumenSpeechQuietCheck', down: 'lumenSpeechDetailCombo' })
          .onCheckChange((event) => this.setAnnounceMilestones(event)),
        Label({
          text: 'DESCRIPTION DETAIL',
          color: accentColor,
          width: 680,
          height: 28,
          fontKey: '~theme.fonts.text-bold-14',
          translation: [58, 426],
        }),
        ComboBox({
          id: 'lumenSpeechDetailCombo',
          items: DETAIL_ITEMS,
          selectedIndex: this.detailIndex,
          width: 680,
          height: 64,
          styleKey: 'controls.ComboBox.default',
          maxVisibleItems: 3,
          translation: [58, 460],
        })
          .audioGuideHint('Description detail. Press OK to choose concise, balanced, or rich narration.')
          .nextFocusMap({ up: 'lumenSpeechMilestonesCheck', down: 'lumenSpeechQueueCombo' })
          .onSelectionChange((event) => this.selectDetail(event)),
        Label({
          text: 'WHEN NEW GUIDANCE ARRIVES',
          color: accentColor,
          width: 680,
          height: 28,
          fontKey: '~theme.fonts.text-bold-14',
          translation: [58, 550],
        }),
        ComboBox({
          id: 'lumenSpeechQueueCombo',
          items: QUEUE_ITEMS,
          selectedIndex: this.queueIndex,
          width: 680,
          height: 64,
          styleKey: 'controls.ComboBox.default',
          maxVisibleItems: 2,
          translation: [58, 584],
        })
          .audioGuideHint('Speech queue behaviour. Press OK to choose whether new guidance interrupts or waits.')
          .nextFocusMap({ up: 'lumenSpeechDetailCombo', down: 'lumenSpeechPreviewSettingsButton' })
          .onSelectionChange((event) => this.selectQueueMode(event)),
        Rectangle({
          color: '#FFFFFF24',
          width: 680,
          height: 1,
          translation: [58, 684],
        }),
        Label({
          text: 'Preview speaks the current story using these exact choices.',
          color: '#9FB0BC',
          width: 680,
          height: 34,
          fontKey: '~theme.fonts.text-medium-18',
          translation: [58, 716],
        }),
        Button({
          id: 'lumenSpeechPreviewSettingsButton',
          text: 'PLAY  Preview voice',
          width: 326,
          height: 64,
          styleKey: 'controls.Button.lumenAiSearchCta',
          translation: [58, 782],
        })
          .audioGuideItem('Preview voice. Press OK to hear the current story with these playback and spoken guidance settings.')
          .nextFocusMap({ up: 'lumenSpeechQueueCombo', right: 'lumenSpeechDoneButton', down: 'lumenSpeechAutoNarrateCheck' })
          .onClick(() => this.playSpokenPreview()),
        Button({
          id: 'lumenSpeechDoneButton',
          text: 'Done',
          width: 326,
          height: 64,
          styleKey: 'controls.Button.lumenAiSearchCta',
          translation: [412, 782],
        })
          .audioGuideItem('Done. Press OK to save these settings and return to the story details screen.')
          .nextFocusMap({ up: 'lumenSpeechQueueCombo', left: 'lumenSpeechPreviewSettingsButton', down: 'lumenSpeechAutoNarrateCheck' })
          .onClick(() => this.closeSettings()),
        Label({
          text: 'BACK closes this popup  /  OK changes the focused control',
          color: '#FFFFFF70',
          width: 680,
          height: 28,
          fontKey: '~theme.fonts.text-bold-14',
          translation: [58, 898],
        }),
      ])
        .id('lumenSpeechSettingsPanel')
        .width(820)
        .height(970)
        .translation([1020, 54]),
    ])
      .id('lumenSpeechSettingsPopup')
      .width(1920)
      .height(1080);
  }
Popup([
Image({
imageUri: 'pkg:/assets/images/popup@res.9.png',
width: 480,
height: 240,
}),
VGroup(menuButtons)
.translation([24, 20])
.itemSpacing(12),
])
.visible(this.isMenuOpen);

The popup renderer is anchored to the owning view's bounds. Place its children relative to that anchor, and drive visibility from owning state.

Popup reports a measured width and height of zero so it does not occupy space in the parent's layout. It is only a floating render container; it does not provide dialog focus locking, dismissal, or a backdrop. Use Hosanna's dialog manager for modal UI.

Talk to us