From f789dde7aaaab0c6a83e9f4c7a08b514d84f302e Mon Sep 17 00:00:00 2001 From: Andrew Polk Date: Tue, 25 Aug 2026 21:45:21 -0700 Subject: [PATCH] chore(language-chooser): cover defaultDisplayName and the host integration contract Two gaps turned up when comparing Bloom's regression test cards against EthnoLib's automated tests. defaultDisplayName is public API of the react-mui package (Bloom calls it directly to name a language) and had no unit test at all. Its whole point is the precedence rule -- a script's languageNameInScript wins over the autonym, which is what makes picking a script change the displayed name -- plus the empty-string cases for the unlisted and manually-entered-tag languages and the stripping of search-result match markers. None of that was pinned down, so any of it could have been changed silently. Separately, nothing asserted on what the chooser tells a host application. Every existing e2e file drives DialogDemo, whose LanguageChooserDialog wrapper supplies its own OK button and stands in for the host. Bloom instead passes its own actionButtons, no rightPanelComponent, and learns about the selection solely through onSelectionChange. That props arrangement was not unprecedented -- ThemeDemo has rendered it for over a year, and PageDemo renders the chooser outside a dialog too -- but neither observes the callback: ThemeDemo passes no onSelectionChange at all and PageDemo keeps only the language tag. So the host half of the contract had demos but no coverage. HostIntegrationDemo shows that callback in a panel beside the chooser, covering everything our main client consumes: Bloom's getLanguageData maps a selection onto a tag, a default name, the name to actually use, the script's reading direction and a country, so all five are shown next to the raw fields they come from. Three details are deliberate: - it shows three names, not one. Showing only customDisplayName is misleading, because the chooser leaves it empty until the user edits the name field, so an ordinary selection looks like it reported nothing. - reading direction is spelled out as true / false / "not stated". A script can decline to say, Bloom keeps that distinction (its IsRtl is a bool?), and conflating "not stated" with false is what BL-13982 was about. - the OK button commits, so clicking it visibly does something. The chooser is boxed at 1000x580 to match the WinForms dialog Bloom hosts it in, which also stops the page growing a scrollbar; the row wraps rather than shrinking that box, so a narrow window stacks instead of scrolling sideways. main.tsx selects the demo with demo=host-integration alongside the query params it already reads for e2e; the default path is untouched. It also gets a Storybook story like the other demos, so it is visible next to them and does not rot unnoticed behind a URL parameter only Playwright uses. Co-Authored-By: Claude Opus 5 (1M context) --- .../find-language/languageTagUtils.spec.ts | 103 +++++++ .../e2e/e2eHelpers.ts | 14 + .../e2e/hostIntegration.e2e.ts | 178 +++++++++++ .../src/demos/HostIntegrationDemo.stories.tsx | 32 ++ .../src/demos/HostIntegrationDemo.tsx | 282 ++++++++++++++++++ .../language-chooser-react-mui/src/main.tsx | 22 +- 6 files changed, 625 insertions(+), 6 deletions(-) create mode 100644 components/language-chooser/react/language-chooser-react-mui/e2e/hostIntegration.e2e.ts create mode 100644 components/language-chooser/react/language-chooser-react-mui/src/demos/HostIntegrationDemo.stories.tsx create mode 100644 components/language-chooser/react/language-chooser-react-mui/src/demos/HostIntegrationDemo.tsx diff --git a/components/language-chooser/common/find-language/languageTagUtils.spec.ts b/components/language-chooser/common/find-language/languageTagUtils.spec.ts index 4a9fdd56..0256bbde 100644 --- a/components/language-chooser/common/find-language/languageTagUtils.spec.ts +++ b/components/language-chooser/common/find-language/languageTagUtils.spec.ts @@ -13,7 +13,9 @@ import { isValidBcp47Tag, languageForManuallyEnteredTag, UNLISTED_LANGUAGE, + defaultDisplayName, } from "./languageTagUtils"; +import { demarcateResults } from "./matchingSubstringDemarcation"; import { defaultSearchResultModifier } from "./searchResultModifiers"; import { parseLangtagFromLangChooser } from "./searchForLanguage"; @@ -38,6 +40,7 @@ import { SERBIAN_LANGUAGE, ARABIC_MACROLANGUAGE, AYMARA_MACROLANGUAGE, + createTestLanguageEntry, } from "./testUtils"; describe("Tag creation", () => { @@ -849,3 +852,103 @@ describe("formatting dialect codes", () => { ); }); }); + +describe("defaultDisplayName", () => { + // Helper for the fixtures, whose scripts are listed in an order we don't want tests to depend on + function scriptOf(language: ILanguage, scriptCode: string): IScript { + const script = language.scripts.find((s) => s.code === scriptCode); + expect(script, `${scriptCode} script of ${language.exonym}`).toBeDefined(); + return script as IScript; + } + + it("should return empty string when there is no language", () => { + expect(defaultDisplayName(undefined)).toEqual(""); + expect( + defaultDisplayName(undefined, { code: "Latn", name: "Latin" }) + ).toEqual(""); + }); + + it("should use the autonym when no script is given", () => { + expect(defaultDisplayName(BOSNIAN_LANGUAGE)).toEqual("Bosanski jezik"); + expect(defaultDisplayName(SERBIAN_LANGUAGE)).toEqual("српски"); + }); + + it("should fall back to the exonym when there is no script and no autonym", () => { + expect(defaultDisplayName(ENGLISH_LANGUAGE)).toEqual("English"); + expect(defaultDisplayName(NORTHERN_UZBEK_LANGUAGE)).toEqual( + "Northern Uzbek" + ); + }); + + // This precedence is the whole point of passing the script: choosing a different script for a + // language is what changes the name we display for it, e.g. Serbian shows as "српски" in + // Cyrillic but "srpski" in Latin. + it("should prefer the script's languageNameInScript over the autonym", () => { + expect( + defaultDisplayName(SERBIAN_LANGUAGE, scriptOf(SERBIAN_LANGUAGE, "Latn")) + ).toEqual("srpski"); + expect( + defaultDisplayName(SERBIAN_LANGUAGE, scriptOf(SERBIAN_LANGUAGE, "Cyrl")) + ).toEqual("српски"); + expect( + defaultDisplayName(BOSNIAN_LANGUAGE, scriptOf(BOSNIAN_LANGUAGE, "Cyrl")) + ).toEqual("босански"); + }); + + it("should fall back to the autonym for a script with no languageNameInScript", () => { + // Norwegian's Braille and Runic entries carry no languageNameInScript + expect( + defaultDisplayName( + NORWEGIAN_MACROLANGUAGE, + scriptOf(NORWEGIAN_MACROLANGUAGE, "Brai") + ) + ).toEqual("Norsk"); + expect( + defaultDisplayName(BOSNIAN_LANGUAGE, scriptOf(BOSNIAN_LANGUAGE, "Arab")) + ).toEqual("Bosanski jezik"); + }); + + it("should fall back to the exonym for a script with no languageNameInScript on a language with no autonym", () => { + expect( + defaultDisplayName(ENGLISH_LANGUAGE, scriptOf(ENGLISH_LANGUAGE, "Latn")) + ).toEqual("English"); + }); + + it("should return empty string for the unlisted language, even with a script", () => { + expect(defaultDisplayName(UNLISTED_LANGUAGE)).toEqual(""); + expect( + defaultDisplayName(UNLISTED_LANGUAGE, { code: "Latn", name: "Latin" }) + ).toEqual(""); + }); + + it("should return empty string for a manually entered tag language", () => { + const manualLanguage = languageForManuallyEnteredTag("qxy-Latn-ZZ"); + expect(defaultDisplayName(manualLanguage)).toEqual(""); + expect( + defaultDisplayName(manualLanguage, { code: "Latn", name: "Latin" }) + ).toEqual(""); + }); + + it("should strip the match demarcation that search results carry", () => { + // Languages coming out of a search have the matched substring marked for bolding, + // e.g. searching "san" marks Bosnian's autonym as "Bo[san]ski jezik" + const [demarcatedBosnian] = demarcateResults([BOSNIAN_LANGUAGE], "san"); + expect(demarcatedBosnian.autonym).toEqual("Bo[san]ski jezik"); + expect(defaultDisplayName(demarcatedBosnian)).toEqual("Bosanski jezik"); + + expect( + defaultDisplayName(demarcatedBosnian, { + code: "Cyrl", + name: "Cyrillic", + languageNameInScript: "бо[сан]ски", + }) + ).toEqual("босански"); + + const demarcatedExonymOnlyLanguage = createTestLanguageEntry({ + exonym: "E[ngl]ish", + iso639_3_code: "eng", + languageSubtag: "en", + }); + expect(defaultDisplayName(demarcatedExonymOnlyLanguage)).toEqual("English"); + }); +}); diff --git a/components/language-chooser/react/language-chooser-react-mui/e2e/e2eHelpers.ts b/components/language-chooser/react/language-chooser-react-mui/e2e/e2eHelpers.ts index 7fb34077..e986e8ec 100644 --- a/components/language-chooser/react/language-chooser-react-mui/e2e/e2eHelpers.ts +++ b/components/language-chooser/react/language-chooser-react-mui/e2e/e2eHelpers.ts @@ -10,6 +10,20 @@ export async function loadLanguageChooser(page) { await page.goto("/", { waitUntil: "load" }); } +// Loads src/demos/HostIntegrationDemo.tsx, which renders LanguageChooser with host-supplied +// actionButtons and no rightPanelComponent -- the arrangement Bloom ships -- and puts everything +// onSelectionChange reports on screen. The chooser itself is the same component here, so the +// card/search helpers below work in this demo too. +export async function loadHostIntegrationDemo(page) { + await page.goto("/?demo=host-integration", { waitUntil: "load" }); +} + +export async function createPageAndLoadHostIntegrationDemo(browser) { + const page = await browser.newPage(); + await loadHostIntegrationDemo(page); + return page; +} + export function scriptCardTestId(scriptCode: string) { return `script-card-${scriptCode}`; } diff --git a/components/language-chooser/react/language-chooser-react-mui/e2e/hostIntegration.e2e.ts b/components/language-chooser/react/language-chooser-react-mui/e2e/hostIntegration.e2e.ts new file mode 100644 index 00000000..26e4c5f5 --- /dev/null +++ b/components/language-chooser/react/language-chooser-react-mui/e2e/hostIntegration.e2e.ts @@ -0,0 +1,178 @@ +import { test, expect, Page } from "@playwright/test"; +import { + clickLanguageCard, + createPageAndLoadHostIntegrationDemo, + loadHostIntegrationDemo, + scriptCardTestId, + search, +} from "./e2eHelpers"; + +let page: Page; + +// What the chooser tells a host application, which is the half of the contract no other e2e file +// touches: they all drive the LanguageChooserDialog wrapper, whose own OK button stands in for the +// host. Bloom instead supplies its own actionButtons, passes no rightPanelComponent, and learns +// about the selection solely through onSelectionChange -- so these tests assert on what that +// callback reports, via src/demos/HostIntegrationDemo.tsx. They deliberately do not re-test search +// or card behavior, which the other files already cover. +test.describe("What onSelectionChange reports to the host", () => { + test.beforeAll(async ({ browser }) => { + page = await createPageAndLoadHostIntegrationDemo(browser); + }); + + // These tests change the selection, and there is no host UI here to reset it, so reload between + // them rather than trying to click our way back to a pristine state. + test.beforeEach(async () => { + await loadHostIntegrationDemo(page); + }); + + test("chooser renders and is usable with no rightPanelComponent supplied", async () => { + // The right pane itself still exists (it holds the display name bar, tag preview and the + // host's action buttons); it's the client-supplied slot within it that is empty. + await expect(page.locator("#right-panel-component-container")).toBeEmpty(); + + await search(page, "russian"); + await clickLanguageCard(page, "rus"); + + // The parts of the right pane the component owns still work + await expect(page.locator("#language-name-bar")).toHaveValue( + "русский язык" + ); + await expect(page.getByTestId("right-panel-langtag-preview")).toContainText( + "ru" + ); + }); + + test("onSelectionChange reports the tag and orthography once a language and script are chosen", async () => { + const reportedTag = page.getByTestId("host-integration-reported-tag"); + const reportedScript = page.getByTestId("host-integration-reported-script"); + + // Nothing is reported before there is a complete selection + await expect(page.getByTestId("host-integration-report-count")).toHaveText( + "0" + ); + + await search(page, "chechen"); + await clickLanguageCard(page, "che"); + // Chechen has several scripts, so the selection is not complete until one is picked + await expect(reportedTag).toBeEmpty(); + + await page.getByTestId(scriptCardTestId("Cyrl")).click(); + await expect(reportedTag).toHaveText("ce"); + await expect( + page.getByTestId("host-integration-reported-language") + ).toHaveText("ce"); + await expect(reportedScript).toHaveText("Cyrl"); + + // Switching script re-reports, which is how the host learns the tag changed + await page.getByTestId(scriptCardTestId("Arab")).click(); + await expect(reportedTag).toHaveText("ce-Arab"); + await expect(reportedScript).toHaveText("Arab"); + }); + + test("onSelectionChange reports an undefined selection when the selection is cleared", async () => { + const reportCount = page.getByTestId("host-integration-report-count"); + const reportedTag = page.getByTestId("host-integration-reported-tag"); + + await search(page, "russian"); + await clickLanguageCard(page, "rus"); // Russian has a single script, so this alone is complete + await expect(reportedTag).toHaveText("ru"); + const countWhenSelected = await reportCount.textContent(); + + // Re-clicking the selected card unselects it + await clickLanguageCard(page, "rus"); + // The host must be told the selection went away, not merely left holding the stale one, so + // check that a report actually happened rather than only that the fields went empty. + await expect(reportCount).not.toHaveText(countWhenSelected as string); + await expect(reportedTag).toBeEmpty(); + await expect( + page.getByTestId("host-integration-reported-language") + ).toBeEmpty(); + }); + + test("host-supplied action button enables and disables off the reported selection", async () => { + const okButton = page.getByTestId("host-integration-ok-button"); + await expect(okButton).toBeDisabled(); + + await search(page, "russian"); + await clickLanguageCard(page, "rus"); + await expect(okButton).toBeEnabled(); + + await clickLanguageCard(page, "rus"); + await expect(okButton).toBeDisabled(); + }); + + // A plain selection carries NO custom display name -- the chooser only fills that in once the + // user edits the name field. A host that reads only customDisplayName therefore gets nothing for + // the ordinary case and has to fall back to defaultDisplayName, which is what Bloom does. + test("a selection the user did not rename reports a default name but no custom one", async () => { + await search(page, "arabic"); + await clickLanguageCard(page, "arb"); + await page.getByTestId(scriptCardTestId("Arab")).click(); + + await expect(page.getByTestId("host-integration-reported-tag")).toHaveText( + "arb" + ); + await expect( + page.getByTestId("host-integration-reported-display-name") + ).toBeEmpty(); + await expect( + page.getByTestId("host-integration-default-display-name") + ).not.toBeEmpty(); + // ...so the name a host would actually use comes from the default, not the custom field. + await expect( + page.getByTestId("host-integration-name-a-host-would-use") + ).not.toBeEmpty(); + }); + + // A host has to be able to tell "this script reads right-to-left", "this one reads + // left-to-right", and "this script does not say" apart -- Bloom stores the difference (its IsRtl + // is nullable) and conflating the last two is what BL-13982 was about. + test("the script's reading direction is reported, and distinguishes unknown from false", async () => { + const direction = page.getByTestId("host-integration-script-is-rtl"); + await expect(direction).toBeEmpty(); // nothing selected yet + + await search(page, "chechen"); + await clickLanguageCard(page, "che"); + await page.getByTestId(scriptCardTestId("Cyrl")).click(); + await expect(direction).toHaveText("false"); + + await page.getByTestId(scriptCardTestId("Arab")).click(); + await expect(direction).toHaveText("true"); + }); + + test("the country Bloom would store is derived from the reported tag", async () => { + await expect(page.getByTestId("host-integration-country")).toBeEmpty(); + + await search(page, "russian"); + await clickLanguageCard(page, "rus"); + await expect(page.getByTestId("host-integration-country")).not.toBeEmpty(); + }); + + test("the host action button commits what it was told", async () => { + await expect(page.getByTestId("host-integration-submitted")).toBeEmpty(); + + await search(page, "russian"); + await clickLanguageCard(page, "rus"); + await page.getByTestId("host-integration-ok-button").click(); + + await expect(page.getByTestId("host-integration-submitted")).toContainText( + "ru" + ); + }); + + test("edits to the display name are reported to the host", async () => { + // Bloom reads customDetails.customDisplayName off the reported selection to name the language, + // so the report has to keep up with the display name field, not just the tag. + await search(page, "russian"); + await clickLanguageCard(page, "rus"); + await expect(page.getByTestId("host-integration-reported-tag")).toHaveText( + "ru" + ); + + await page.locator("#language-name-bar").fill("Ruso"); + await expect( + page.getByTestId("host-integration-reported-display-name") + ).toHaveText("Ruso"); + }); +}); diff --git a/components/language-chooser/react/language-chooser-react-mui/src/demos/HostIntegrationDemo.stories.tsx b/components/language-chooser/react/language-chooser-react-mui/src/demos/HostIntegrationDemo.stories.tsx new file mode 100644 index 00000000..79465391 --- /dev/null +++ b/components/language-chooser/react/language-chooser-react-mui/src/demos/HostIntegrationDemo.stories.tsx @@ -0,0 +1,32 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { HostIntegrationDemo } from "./HostIntegrationDemo"; + +const meta: Meta = { + title: "Demos/Host Integration Demo", + component: HostIntegrationDemo, +}; + +export default meta; +type Story = StoryObj; + +// Nothing selected yet, so the readout across the top starts empty with a report count of 0. +// Pick a language and a script and watch what the host is told, and when. +export const Primary: Story = { + args: {}, + render: (args, context) => ( + + ), +}; + +// Opened with a selection already in hand, the way a host reopens the chooser on a language the +// user picked earlier. Shows that the host is told about that starting selection rather than +// having to remember it. +export const ReopenedWithASelection: Story = { + args: { + initialLanguageTag: "uz-Cyrl", + initialCustomDisplayName: "ÖzbekCustomizedName", + }, + render: (args, context) => ( + + ), +}; diff --git a/components/language-chooser/react/language-chooser-react-mui/src/demos/HostIntegrationDemo.tsx b/components/language-chooser/react/language-chooser-react-mui/src/demos/HostIntegrationDemo.tsx new file mode 100644 index 00000000..42ef71b4 --- /dev/null +++ b/components/language-chooser/react/language-chooser-react-mui/src/demos/HostIntegrationDemo.tsx @@ -0,0 +1,282 @@ +/** @jsxImportSource @emotion/react */ +import { css } from "@emotion/react"; +import { + IOrthography, + defaultDisplayName, + defaultRegionForLangTag, + defaultSearchResultModifier, +} from "@ethnolib/find-language"; +import { Button } from "@mui/material"; +import React from "react"; +import { LanguageChooser } from "../LanguageChooser"; + +// What a host application actually receives from the chooser. +// +// Each of the other demos answers a different question: DialogDemo shows the modal use case and +// reopening with a prior selection, PageDemo shows how the chooser responds to the space it is +// given, ThemeDemo shows theming. None of them shows the host side of the contract -- ThemeDemo +// happens to render the same props arrangement Bloom uses, but passes no onSelectionChange at all, +// and PageDemo keeps only the language tag. +// +// So this demo puts the callback on screen. The panel deliberately covers everything our main +// client consumes: Bloom's getLanguageData maps a selection onto exactly five things -- the +// language tag, a default name, the name to actually use, the script's reading direction, and a +// country -- so all five are here, next to the raw fields they are derived from. +export const HostIntegrationDemo: React.FunctionComponent<{ + uiLanguage?: string; + initialLanguageTag?: string; + initialSearchString?: string; + initialCustomDisplayName?: string; +}> = ({ + uiLanguage, + initialLanguageTag, + initialSearchString, + initialCustomDisplayName, +}) => { + const [reportedSelection, setReportedSelection] = React.useState< + IOrthography | undefined + >(undefined); + const [reportedLanguageTag, setReportedLanguageTag] = React.useState< + string | undefined + >(undefined); + // Lets a reader -- and the tests -- tell "never reported" apart from "reported as cleared", since + // both leave the fields below empty. + const [reportCount, setReportCount] = React.useState(0); + const [submitted, setSubmitted] = React.useState( + undefined + ); + + function onSelectionChange( + orthographyInfo: IOrthography | undefined, + languageTag: string | undefined + ) { + setReportedSelection(orthographyInfo); + setReportedLanguageTag(languageTag); + setReportCount((count) => count + 1); + } + + const language = reportedSelection?.language; + const script = reportedSelection?.script; + + // The chooser leaves customDisplayName empty until the user edits the name field, so a host that + // reads only that gets nothing for an ordinary selection and has to fall back to + // defaultDisplayName. Showing both, and the combination, makes that visible rather than looking + // like a missing value. + const customDisplayName = reportedSelection?.customDetails?.customDisplayName; + const defaultName = language + ? defaultDisplayName(language, script) + : undefined; + const nameAHostWouldUse = customDisplayName || defaultName; + + // Three states, not two: a script can say right-to-left, say left-to-right, or say nothing at + // all. Bloom keeps that distinction (its IsRtl is a bool?), and conflating "false" with "unknown" + // is what BL-13982 was about, so spell it out rather than showing a blank. + const scriptDirection = !script + ? undefined + : script.isRtl === undefined + ? "not stated" + : `${script.isRtl}`; + + const country = reportedLanguageTag + ? defaultRegionForLangTag(reportedLanguageTag, language)?.name + : undefined; + + // Supplied by the host rather than by the chooser, and enabled off the reported selection -- + // the same condition Bloom uses for its own OK button. Clicking it commits, the way a host + // would; the chooser itself has no notion of "OK". + const hostActionButtons = ( +
+ +
+ ); + + const groups: { + heading: string; + rows: { testId: string; label: string; value: string | undefined }[]; + }[] = [ + { + heading: "As reported", + rows: [ + { + testId: "report-count", + label: "times reported", + value: `${reportCount}`, + }, + { + testId: "reported-tag", + label: "tag", + value: reportedLanguageTag, + }, + { + testId: "reported-language", + label: "subtag", + value: language?.languageSubtag, + }, + { testId: "reported-script", label: "script", value: script?.code }, + { + testId: "script-is-rtl", + label: "right-to-left", + value: scriptDirection, + }, + { + testId: "reported-display-name", + label: "custom name", + value: customDisplayName, + }, + ], + }, + { + heading: "What a host does with it", + rows: [ + { + testId: "default-display-name", + label: "default name", + value: defaultName, + }, + { + testId: "name-a-host-would-use", + label: "name to use", + value: nameAHostWouldUse, + }, + { testId: "country", label: "country", value: country }, + { testId: "submitted", label: "committed", value: submitted }, + ], + }, + ]; + + return ( +
+ {/* + The selection as reported to the host: readable for a person, assertable for the tests. + Values are rendered exactly as reported, so an empty one really is empty in the DOM (the + tests assert on that); the dash a reader sees for an empty value comes from CSS, so it does + not pollute the text content. + */} +
div + div { + margin-top: 16px; + } + `} + > + {groups.map((group) => ( +
+

{group.heading}

+
+ {group.rows.map((row) => ( + +
{row.label}
+
+ {row.value || ""} +
+
+ ))} +
+
+ ))} +
+ {/* + Bloom hosts the chooser in a WinForms dialog sized 1000x580 -- see the SetScaledSize call in + CollectionSettingsDialog.ChangeLanguage -- so give it a box that size here. LanguageChooser + is height:100%, so it needs a parent with a definite height; letting it fill the viewport + instead both misrepresents what users see and pushes this page into a scrollbar. The row + wraps rather than shrinking the box, so a narrow window stacks the panel above it instead of + scrolling sideways. + Note: no rightPanelComponent, matching Bloom. + */} +
+ +
+
+ ); +}; + +export default HostIntegrationDemo; diff --git a/components/language-chooser/react/language-chooser-react-mui/src/main.tsx b/components/language-chooser/react/language-chooser-react-mui/src/main.tsx index 395b1b34..3ba32cc9 100644 --- a/components/language-chooser/react/language-chooser-react-mui/src/main.tsx +++ b/components/language-chooser/react/language-chooser-react-mui/src/main.tsx @@ -1,5 +1,6 @@ import ReactDOM from "react-dom"; import DialogDemo from "./demos/DialogDemo"; +import HostIntegrationDemo from "./demos/HostIntegrationDemo"; import React from "react"; // Read parameters from URL query parameters (for e2e testing) @@ -9,15 +10,24 @@ const initialLanguageTag = urlParams.get("initialLanguageTag") || undefined; const initialSearchString = urlParams.get("initialSearchString") || undefined; const initialCustomDisplayName = urlParams.get("initialCustomDisplayName") || undefined; +// Which demo to serve. Defaults to DialogDemo, which is what every URL without this parameter has +// always got; "host-integration" serves HostIntegrationDemo instead. +const demo = urlParams.get("demo") || undefined; + +const demoProps = { + uiLanguage, + initialLanguageTag, + initialSearchString, + initialCustomDisplayName, +}; ReactDOM.render( - + {demo === "host-integration" ? ( + + ) : ( + + )} , document.getElementById("root") );