import { Component, Prop, State, Watch, Event, EventEmitter, Listen, h, Element } from '@stencil/core'; import { ApplicationHeaderInfo, HeaderMenuToggleDetail, LanguageToggleOptions, OntarioMenuItems, OntarioHeaderType, } from './ontario-header.interface'; import OntarioIconClose from '../ontario-icon/assets/ontario-icon-close-header.svg'; import OntarioIconMenu from '../ontario-icon/assets/ontario-icon-menu-header.svg'; import OntarioIconSearch from '../ontario-icon/assets/ontario-icon-search.svg'; import OntarioIconSearchWhite from '../ontario-icon/assets/ontario-icon-search-white.svg'; import OntarioIconDropdownArrow from '../ontario-icon/assets/ontario-icon-dropdown-arrow.svg'; import OntarioHeaderDefaultData from './ontario-header-default-data.json'; import { Language } from '../../utils/common/language-types'; import { MenuItem } from '../../utils/common/common.interface'; import { DeviceTypes } from '../../utils/common/common.enum'; import { ScreenBreakpoints, standardFontSizePx } from '../../utils/common/common.variables'; import { isClientSideRendering } from '../../utils/common/environment'; import { Input } from '../../utils/common/input/input'; import { generateMenuItem } from '../../utils/components/header/header-menu-items'; import { ConsoleMessageClass } from '../../utils/console-message/console-message'; import { ConsoleType } from '../../utils/console-message/console-message.enum'; import { getImageAssetSrcPath } from '../../utils/helper/assets'; import { DeviceType } from '../../utils/helper/utils-types'; import { validateLanguage } from '../../utils/validation/validation-functions'; import translations from '../../translations/global.i18n.json'; import config from '../../config.json'; /** * Ontario Header renders Ontario.ca, application, and ServiceOntario header variants. * * For component guidance, see: * - https://designsystem.ontario.ca/components/detail/ontario-header.html * - https://designsystem.ontario.ca/components/detail/application-header.html * - https://designsystem.ontario.ca/components/detail/service-ontario-header.html * - https://designsystem.ontario.ca/developer-docs/components/ontario-header/ */ @Component({ tag: 'ontario-header', styleUrls: ['ontario-header.scss', 'ontario-application-header.scss', 'service-ontario-header.scss'], shadow: true, assetsDirs: ['./assets'], }) export class OntarioHeader { @Element() el: HTMLElement; /** * The type of header. */ @Prop() type?: OntarioHeaderType = 'application'; /** * Information pertaining to the application and ServiceOntario headers. * * For the 'application' header type, this includes the application name, URL and optional props for the number of links in the subheader for desktop, tablet, and mobile views. * * For the 'serviceOntario' header type, the 'title' property is used as the service name displayed in the subheader. * * @example * * * * * */ @Prop() applicationHeaderInfo: ApplicationHeaderInfo | string; /** * The items that will go inside the menu dropdown. * * For the 'ontario' header type, these items are displayed in the overflow menu. If `disableDynamicMenu` is false, static items will be overridden by dynamically fetched items from the Ontario Header API. * * For the 'application' and 'serviceOntario' header types, these items are displayed in the subheader menu and overflow menu. */ @Prop() menuItems: MenuItem[] | string; /** * Information pertaining to the sign-in menu items for the Ontario header. */ @Prop() signInMenuItems?: MenuItem[] | string; /** * A custom function to pass to the sign-in button. */ @Prop() customSignInToggle?: (event: globalThis.Event) => void; /** * Option to disable fetching of the dynamic menu from the Ontario Header API. * * When set to true, the static `menuItems` prop will be used instead of fetching from the API. * When set to false (default), menu items are fetched dynamically from the Ontario Header API endpoint. * * This property only applies to the 'ontario' header type. The 'application' and 'serviceOntario' types always use static menu items. * * @default false * * @example * * */ @Prop() disableDynamicMenu: boolean = false; /** * Information pertaining to the language toggle links. * * @example * * */ @Prop() languageToggleOptions?: LanguageToggleOptions | string; /** * A custom function to pass to the language toggle button. */ @Prop() customLanguageToggle?: (event: globalThis.Event) => void; /** * The language of the component. * This is used for translations, and is by default set through event listeners checking for a language property from the header. If none is passed, it will default to English. */ @Prop({ mutable: true }) language?: Language = 'en'; /** * The base path to an assets folder containing the Design System assets */ @Prop() assetBasePath: string; /** * The application header information is reassigned to applicationHeaderInfoState for parsing */ @State() private applicationHeaderInfoState: ApplicationHeaderInfo; /** * The menuItems is reassigned to itemState for parsing * * @example * * */ @State() private menuItemState: MenuItem[]; /** * The parsed sign-in menu items state */ @State() private signInMenuItemsState: MenuItem[]; /** * A boolean state to handle the toggling of the sign-in menu */ @State() signInToggled: boolean = false; /** * Check to see if menu is dynamic or static */ @State() private isDynamicMenu: boolean = false; /** * The languageToggleOptions is reassigned to languageState for parsing * * @example * */ @State() private languageState: LanguageToggleOptions; /** * Toggler for the menu and the search button */ @State() menuToggled: boolean = false; @State() searchToggle?: boolean = false; @State() translations: any = translations; @State() breakpointDeviceState: DeviceType; /** * Tracks the current value of the search input field. * * This state is updated as the user types and is used as the single * source of truth for the search input content (instead of directly * manipulating the DOM). Clearing the field via Escape now resets this * state, which triggers the UI to update automatically. * * Also used as the value submitted when performing a header search, * improving consistency and reliability of the search behaviour. */ @State() private searchBoxTextState: string = ''; private shouldFocusMenuOnOpen = false; private pendingMenuToggleTrigger: HeaderMenuToggleDetail['trigger'] | null = null; /** * Header-specific device detection. */ private getHeaderDeviceType(): DeviceType { const width = typeof window !== 'undefined' ? window.innerWidth : 1200; const tablet = ScreenBreakpoints.Large * standardFontSizePx; if (width < tablet - 1) return DeviceTypes.Mobile; if (width === tablet - 1) return DeviceTypes.Tablet; return DeviceTypes.Desktop; } /** * Helper to check if current breakpoint is mobile or tablet (not desktop) */ private get isMobileOrTablet(): boolean { return (this.breakpointDeviceState ?? DeviceTypes.Desktop) !== DeviceTypes.Desktop; } private createMenuToggleDetail(isOpen: boolean, trigger: HeaderMenuToggleDetail['trigger']): HeaderMenuToggleDetail { return { isOpen, trigger }; } private emitMenuToggle(isOpen: boolean, trigger: HeaderMenuToggleDetail['trigger']) { this.menuButtonToggled.emit(this.createMenuToggleDetail(isOpen, trigger)); } private consumePendingMenuToggleTrigger(defaultTrigger: HeaderMenuToggleDetail['trigger']) { const trigger = this.pendingMenuToggleTrigger ?? defaultTrigger; this.pendingMenuToggleTrigger = null; return trigger; } private isMenuOpenButtonKey(key: string): boolean { return key === 'Enter' || key === ' ' || key === 'Spacebar' || key === 'ArrowDown'; } private moveFocusIntoOpenMenu() { window.dispatchEvent(new CustomEvent('menuButtonTabPressed', { bubbles: true, composed: true })); } private focusDesktopMenuAfterKeyboardOpen() { if (this.isMobileOrTablet) return; requestAnimationFrame(() => { if (this.menuToggled) { this.moveFocusIntoOpenMenu(); } }); } private handleMenuButtonKeyDown = (event: KeyboardEvent) => { if (!this.isMenuOpenButtonKey(event.key)) return; this.pendingMenuToggleTrigger = 'keyboard'; if (event.key === 'ArrowDown') { event.preventDefault(); if (this.menuToggled) { this.moveFocusIntoOpenMenu(); return; } if (this.signInToggled) { this.signInToggled = false; } this.menuToggled = true; this.emitMenuToggle(this.menuToggled, 'keyboard'); this.searchToggle = undefined; this.focusDesktopMenuAfterKeyboardOpen(); } }; private handleMenuButtonClick = () => { this.handlemenuToggled(this.consumePendingMenuToggleTrigger('click')); }; private handleSignInButtonKeyDown = (event: KeyboardEvent) => { if (!this.isMenuOpenButtonKey(event.key)) return; this.pendingMenuToggleTrigger = 'keyboard'; if (event.key === 'ArrowDown') { event.preventDefault(); if (this.signInToggled) { this.moveFocusIntoOpenMenu(); return; } if (this.menuToggled) { this.menuToggled = false; } this.signInToggled = true; this.emitMenuToggle(this.signInToggled, 'keyboard'); } }; private handleSignInButtonClick = () => { this.handleSignInToggled(this.consumePendingMenuToggleTrigger('click')); }; @Watch('applicationHeaderInfo') private parseApplicationHeaderInfo() { const applicationHeaderInfo = this.applicationHeaderInfo; if (applicationHeaderInfo) { try { if (typeof applicationHeaderInfo === 'string') this.applicationHeaderInfoState = JSON.parse(applicationHeaderInfo); else this.applicationHeaderInfoState = applicationHeaderInfo; } catch (error) { const message = new ConsoleMessageClass(); message .addDesignSystemTag() .addRegularText(' failed to parse props for ') .addMonospaceText('') .addRegularText(' in ') .addMonospaceText('parseApplicationHeaderInfo()') .addRegularText(' method \n ') .addMonospaceText(error.stack) .printMessage(ConsoleType.Error); this.applicationHeaderInfoState = { title: '', href: '/', }; // fallback on error } } } @Watch('menuItems') parseMenuItems() { const isEnglish = this.language === 'en'; try { if (!Array.isArray(this.menuItems) && typeof this.menuItems === 'string') { this.menuItemState = JSON.parse(this.menuItems); this.isDynamicMenu = false; } else if (Array.isArray(this.menuItems) && this.type === 'application') { this.menuItemState = this.menuItems; this.isDynamicMenu = false; } else { this.menuItemState = isEnglish ? OntarioHeaderDefaultData.en : OntarioHeaderDefaultData.fr; this.isDynamicMenu = false; } } catch (error) { const message = new ConsoleMessageClass(); message .addDesignSystemTag() .addRegularText(' failed to parse props for ') .addMonospaceText('') .addRegularText(' in ') .addMonospaceText('parseMenuItems()') .addRegularText(' method \n ') .addMonospaceText(error.stack) .printMessage(ConsoleType.Error); this.menuItemState = []; } } @Watch('signInMenuItems') parseSignInMenuItems() { try { if (!Array.isArray(this.signInMenuItems) && typeof this.signInMenuItems === 'string') { this.signInMenuItemsState = JSON.parse(this.signInMenuItems); } else if (Array.isArray(this.signInMenuItems)) { this.signInMenuItemsState = this.signInMenuItems; } else { this.signInMenuItemsState = []; } } catch (error) { const message = new ConsoleMessageClass(); message .addDesignSystemTag() .addRegularText(' failed to parse props for ') .addMonospaceText('') .addRegularText(' in ') .addMonospaceText('parseSignInMenuItems()') .addRegularText(' method \n ') .addMonospaceText(error.stack) .printMessage(ConsoleType.Error); this.signInMenuItemsState = []; } } @Watch('languageToggleOptions') private parseLanguage() { const languageToggleOptions = this.languageToggleOptions; try { if (languageToggleOptions) { if (typeof languageToggleOptions === 'string') { this.languageState = JSON.parse(languageToggleOptions); } else { this.languageState = languageToggleOptions; } } } catch (error) { const message = new ConsoleMessageClass(); message .addDesignSystemTag() .addRegularText(' failed to parse props for ') .addMonospaceText('') .addRegularText(' in ') .addMonospaceText('parseLanguage()') .addRegularText(' method \n ') .addMonospaceText(error.stack) .printMessage(ConsoleType.Error); this.languageState = { englishLink: '/en', frenchLink: '/fr', }; // fallback on error } } /** * Watch for changes to the disableDynamicMenu prop to reset the fetch state. * This allows the menu to be fetched again if the prop is changed after initial load. */ @Watch('disableDynamicMenu') // @ts-ignore - Watcher is called by Stencil framework on prop changes private handleDisableDynamicMenuChange(newValue: boolean) { // Only reset the dynamic menu flag if disableDynamicMenu is being changed to false (re-enabling) // This allows the fetch to happen again when the user toggles dynamic menu back on if (newValue === false) { this.isDynamicMenu = false; } } @Listen('keydown', { target: 'window' }) handleKeyDown(event: KeyboardEvent) { // Handle Tab from menu button -> ask menu to take focus if (event.key === 'Tab' && !event.shiftKey) { if (document.activeElement === this.menuButton && this.menuToggled) { event.preventDefault(); window.dispatchEvent(new CustomEvent('menuButtonTabPressed', { bubbles: true, composed: true })); return; } if (document.activeElement === this.signInButton && this.signInToggled) { event.preventDefault(); window.dispatchEvent(new CustomEvent('menuButtonTabPressed', { bubbles: true, composed: true })); return; } } if (event.key === 'Escape') { if (this.menuToggled) { this.menuToggled = false; this.emitMenuToggle(this.menuToggled, 'programmatic'); this.focusMenuButton(); } if (this.signInToggled) { this.signInToggled = false; this.emitMenuToggle(this.signInToggled, 'programmatic'); this.signInButton.focus(); } } } /** * Logic to close the menu when anything outside the menu is clicked */ @Listen('click', { capture: true, target: 'window' }) handleClick(event: any) { // Check if clicking inside overflow menu const overflowMenu = this.el.shadowRoot?.querySelector('ontario-header-overflow-menu'); if (overflowMenu && event.composedPath().includes(overflowMenu)) { return; } // Check if clicking inside tabs component (NEW!) const tabsMenu = this.el.shadowRoot?.querySelector('ontario-header-menu-tabs'); if (tabsMenu && event.composedPath().includes(tabsMenu)) { return; } // if the sign-in button is clicked, return if (this.signInButton && event.composedPath().includes(this.signInButton)) { return; } // if the menu button is clicked, return if (event.composedPath().includes(this.menuButton)) { return; } // Close both menus when clicking outside if (this.menuToggled || this.signInToggled) { this.menuToggled = false; this.signInToggled = false; this.emitMenuToggle(false, 'programmatic'); } } /** * Logic to close the menu when the focus leaves the menu */ @Listen('focusout', { target: 'window' }) handleFocusOut(event: FocusEvent) { if (this.menuToggled && !this.el.contains(event.relatedTarget as Node)) { this.menuToggled = false; this.emitMenuToggle(this.menuToggled, 'programmatic'); } } /** * Logic to set breakpointDeviceState to the appropriate device when the screen resizes */ @Listen('resize', { target: 'window' }) handleResize() { const previousBreakpoint = this.breakpointDeviceState; // Use header-specific device detection here so the header's UI logic stays consistent this.breakpointDeviceState = isClientSideRendering() ? this.getHeaderDeviceType() : DeviceTypes.Desktop; // Close all menus when breakpoint changes if (previousBreakpoint && previousBreakpoint !== this.breakpointDeviceState) { this.menuToggled = false; this.signInToggled = false; this.emitMenuToggle(false, 'programmatic'); } // If we enter tabbed mode, signInToggled is no longer used. // Reset it so the internal state doesn’t stay "stuck" when returning to desktop. const nowTabbed = this.isMobileOrTablet && this.signInMenuItemsState?.length > 0; if (nowTabbed && this.signInToggled) { this.signInToggled = false; } } /** * This listens for the `setAppLanguage` event sent from the language toggle when it is is connected to the DOM. * It is used for the initial language when the input component loads. */ @Listen('setAppLanguage', { target: 'window' }) handleSetAppLanguage(event: CustomEvent | Language) { this.language = validateLanguage(event); this.parseMenuItems(); } /** * This listens for the `headerLanguageToggled` event sent from the language toggle when it is is connected to the DOM. * It is used for changing the component language after the language toggle has been activated. */ @Listen('headerLanguageToggled', { target: 'window' }) handleLanguageToggle(event: CustomEvent<{ oldLanguage: Language; newLanguage: Language }>) { this.handleSetAppLanguage(event.detail.newLanguage); } /** * Listen for menu closed event from overflow menu */ @Listen('menuClosed', { target: 'window' }) handleMenuClosed() { this.menuToggled = false; this.signInToggled = false; } /** * Listen for overflow menu requesting menu button focus. * Happens when user presses Shift+Tab from first menu item. */ @Listen('focusMenuButton', { target: 'window' }) handleFocusMenuButton() { if (this.signInToggled && this.signInButton) { this.signInButton.focus(); // Emit event so menu knows button is focused (prevents auto-close) window.dispatchEvent(new CustomEvent('menuButtonFocused', { bubbles: true, composed: true })); return; } if (this.menuToggled && this.menuButton) { this.menuButton.focus(); window.dispatchEvent(new CustomEvent('menuButtonFocused', { bubbles: true, composed: true })); } } /** * This event is toggled when the menu button is pressed. * The `` sub-component listens for this event * To trigger the showing and hiding of the overflow menu. */ @Event() menuButtonToggled: EventEmitter; /** * Logic to handle the menu toggling */ handlemenuToggled = (trigger: HeaderMenuToggleDetail['trigger'] = 'keyboard') => { // Close sign-in menu if it's open if (this.signInToggled) { this.signInToggled = false; } this.menuToggled = !this.menuToggled; this.emitMenuToggle(this.menuToggled, trigger); this.searchToggle = undefined; if (this.menuToggled && trigger === 'keyboard') { this.focusDesktopMenuAfterKeyboardOpen(); } }; /** * Logic to handle the search toggling */ handleSearchToggle = () => { this.searchToggle = !this.searchToggle; }; /** * Logic to handle the sign-in toggling */ handleSignInToggled = (trigger: HeaderMenuToggleDetail['trigger'] = 'keyboard') => { // Close main menu if it's open if (this.menuToggled) { this.menuToggled = false; } this.signInToggled = !this.signInToggled; // Emit the menuButtonToggled event for either dropdown this.emitMenuToggle(this.signInToggled, trigger); }; /** * event.preventDefault(): https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault * location.href: https://developer.mozilla.org/en-US/docs/Web/API/Location/href */ handleSubmit = (event: any) => { event.preventDefault(); const query = this.searchBoxTextState.trim(); if (!query) return; const baseUrl = this.translations.header.ontarioSearchURL[this.language ? this.language : 'en']; location.href = `${baseUrl}${encodeURIComponent(query)}`; }; /** * Logic to make the focus go back to the menu button when the list ends */ @Listen('endOfMenuReached', { target: 'window' }) focusMenuButton() { this.menuButton.focus(); } /** * Call to Ontario Menu API to fetch linksets to populate header component */ async fetchOntarioMenu() { const isEnglish = this.language === 'en'; // If menu has already been fetched and contains dynamic menu items, do not run fetch again if (!this.isDynamicMenu) { const apiUrl = isEnglish ? (config.ONTARIO_HEADER_API_URL_EN as string) : (config.ONTARIO_HEADER_API_URL_FR as string); const response = await fetch(apiUrl) .then((response) => response.json()) .then((json) => json.linkset[0].item as OntarioMenuItems[]) .catch(() => { console.error('Unable to retrieve data from Ontario Menu API'); return []; }); if (response.length > 0) { const externalMenuItems = response.map((item) => { return { href: item.href, title: item.title }; }); this.menuItemState = externalMenuItems; this.isDynamicMenu = true; } } return; } /** * Hydration Guard Flag * * This flag is used to determine if the component has been hydrated in the browser. * It prevents certain browser-only operations, like fetching from APIs, from running * during Server-Side Rendering (SSR), where `window` and `fetch` are not available. * * The `isHydrated` flag is set to true in `componentDidLoad()` and checked before * triggering logic that should only run in the browser (e.g., `fetchOntarioMenu()`). * * Not reactive - should not be stored in State. */ private isHydrated = false; /** * Generate the full path to an image asset based on the base asset path. * * - If `assetBasePath` is provided, it is used as the base path. * - If not, attempts to use Stencil's `getAssetPath` (for Stencil/Angular builds). * - If that fails (e.g., in React), falls back to `/assets/`, assuming assets are in the public folder. * * This allows the component to work across multiple environments (Stencil, Angular, React). * * @param imageName - The name of the image file. * @returns The full image path as a string. */ private getImageAssetSrcPath(imageName: string): string { return getImageAssetSrcPath(imageName, this.assetBasePath); } /** * This function generates the menu dropdown button for the ontario header component. * It now derives viewport from component state (this.breakpointDeviceState) instead * of relying on a caller-provided string. */ private renderMenuButton() { const viewportSize = this.breakpointDeviceState as string; if (!this.isMenuVisible(viewportSize)) { return; } const isApplicationOrServiceHeader = this.type === 'application' || this.type === 'serviceOntario'; const isOntarioHeader = this.type === 'ontario'; const shouldShowOutline = isApplicationOrServiceHeader || this.isMobileOrTablet; const useMenuCloseToggle = isApplicationOrServiceHeader || this.isMobileOrTablet; const buttonClasses = [ 'ontario-header__menu-toggle', 'ontario-header-button', isOntarioHeader && 'ontario-header-button--desktop', shouldShowOutline && 'ontario-header-button--with-outline', this.menuToggled && 'ontario-header-button--toggled-open', ] .filter(Boolean) .join(' '); const getButtonContent = () => { if (useMenuCloseToggle) { return (
Menu
); } return (
{this.isMobileOrTablet ? this.translations.header.menu[`${this.language}`] : this.translations.header.topics[`${this.language}`]}
); }; return ( ); } /** * This function generates the sign-in button for the ontario header component. */ private renderSignInButton() { if (!this.signInMenuItemsState || this.signInMenuItemsState.length === 0) { return; } // Only render sign-in button on desktop if (this.breakpointDeviceState !== 'desktop') { return; } return ( ); } /** * The onEscapePressed function clears the searchbar form when Escape is pressed */ private onEscapePressed = (event: KeyboardEvent) => { if (event.key === 'Escape') { this.searchBoxTextState = ''; } }; private isMenuVisible(viewportSize: string) { if (this.type !== 'ontario') { const { menuItemState, applicationHeaderInfoState } = this; const { mobile = 0, tablet = 0, desktop = 0 } = applicationHeaderInfoState?.maxSubheaderLinks ?? {}; const numOfMenuItems = menuItemState?.length ?? 0; if (numOfMenuItems <= 0) { return false; } if (viewportSize === DeviceTypes.Mobile) { return numOfMenuItems - mobile > 0; } if (viewportSize === DeviceTypes.Tablet) { return numOfMenuItems - tablet > 0; } if (viewportSize === DeviceTypes.Desktop) { return numOfMenuItems - desktop > 0; } } return true; } componentWillLoad() { this.parseApplicationHeaderInfo(); this.parseMenuItems(); this.parseSignInMenuItems(); this.parseLanguage(); } componentDidLoad() { this.isHydrated = true; } componentDidRender() { if (this.isHydrated && this.disableDynamicMenu === false && this.type === 'ontario') { this.fetchOntarioMenu(); } this.handleResize(); } /** * Handles the search focus when the search toggle button is clicked. * When search button is clicked, the search bar is in focus, * when the closed button is clicked, the search button is back into focus. */ componentDidUpdate() { if (this.type == 'ontario') { if (this.searchToggle === true) this.searchBar.focus(); if (this.searchToggle === false) this.searchButton.focus(); } } /** * Assigning values to elements to use them as ref */ header!: HTMLElement; menuButton!: HTMLElement; signInButton!: HTMLElement; searchBar!: HTMLInputElement; searchButton!: HTMLInputElement; render() { const isServiceOntarioType = this.type === 'serviceOntario'; if (this.type == 'ontario') { // Check if we should show tabbed interface const shouldShowTabs = this.isMobileOrTablet && this.signInMenuItemsState && this.signInMenuItemsState.length > 0; return (
(this.header = el as HTMLInputElement)}>
{/* Ontario header logo */} {/* Ontario header search input */}
(this.searchBar = el as HTMLInputElement)} onInput={(event: Event) => { const target = event.target as HTMLInputElement; this.searchBoxTextState = target.value; }} onKeyDown={this.onEscapePressed} >
{/* Ontario header language toggle + menu button */}
{this.renderMenuButton()} {this.renderSignInButton()}
{/* Ontario header navigation */} {/* Show overflow menu on all devices, but with different props based on breakpoint */} {/* Ontario header navigation */} {shouldShowTabs ? ( // Mobile/Tablet with sign-in items → Use tabbed interface ) : ( // Desktop OR no sign-in items → Use simple overflow menu )}
{this.menuToggled &&
}
); } else { return (
(this.header = el as HTMLInputElement)} > {/* Ontario application header black bar */}
{/* Ontario application header subheader */}
{!isServiceOntarioType ? (

{this.applicationHeaderInfoState?.href ? ( {this.applicationHeaderInfoState?.title} ) : ( this.applicationHeaderInfoState?.title )}

) : (

{this.translations.header.serviceOntario[`${this.language}`]}

{this.applicationHeaderInfoState?.title}

)}
{!!this.applicationHeaderInfoState?.maxSubheaderLinks?.[this.breakpointDeviceState] && (
    {this.menuItemState ?.slice(0, this.applicationHeaderInfoState?.maxSubheaderLinks?.[this.breakpointDeviceState]) .map((item) => generateMenuItem( item.href, item.title, item.linkIsActive ?? false, item.description, this.language, undefined, item.onClickHandler, ), )}
)} {/* Render menu button if menuItemState exists, and if there are items to display in a dropdown menu */} {this.menuItemState !== undefined && this.applicationHeaderInfoState?.maxSubheaderLinks?.[this.breakpointDeviceState] !== this.menuItemState.length && this.renderMenuButton()}
{this.menuToggled &&
}
); } } }