Skip to content

Commit a33f054

Browse files
author
Garrett Jones
committed
Implement customization modal
1 parent dc673a2 commit a33f054

File tree

7 files changed

+414
-9
lines changed

7 files changed

+414
-9
lines changed

components/language-chooser/common/language-chooser-controller/src/view-models/language-chooser.ts

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import {
22
asyncSearchForLanguage,
33
createTagFromOrthography,
44
defaultDisplayName,
5-
ICustomizableLanguageDetails,
6-
ILanguage,
7-
IOrthography,
8-
IScript,
5+
type ICustomizableLanguageDetails,
6+
type ILanguage,
7+
type IOrthography,
8+
type IRegion,
9+
type IScript,
910
isManuallyEnteredTagLanguage,
1011
isUnlistedLanguage,
1112
isValidBcp47Tag,
@@ -72,6 +73,19 @@ export class LanguageChooserViewModel {
7273

7374
readonly translations: Field<LanguageChooserTranslations>;
7475

76+
readonly showUnlistedLanguageModal = new Field<
77+
((populateWith: { name?: string; region?: IRegion }) => void) | undefined
78+
>(undefined);
79+
80+
readonly showCustomizeLanguageModal = new Field<
81+
| ((populateWith: {
82+
script?: IScript;
83+
region?: IRegion;
84+
dialect?: string;
85+
}) => void)
86+
| undefined
87+
>(undefined);
88+
7589
#currentSearchId = 0;
7690

7791
private onSearchStringUpdated() {
@@ -221,6 +235,54 @@ export class LanguageChooserViewModel {
221235
this.listedLanguages.value = [];
222236
this.appendLanguages(languages);
223237
}
238+
239+
public onCustomizeButtonClicked() {
240+
if (
241+
this.selectedLanguage.value &&
242+
this.selectedLanguage.value.languageSubtag !== "qaa" &&
243+
this.showCustomizeLanguageModal.value
244+
) {
245+
this.showCustomizeLanguageModal.value({
246+
script: this.selectedScript.value,
247+
});
248+
} else if (this.showUnlistedLanguageModal.value) {
249+
this.showUnlistedLanguageModal.value({
250+
name: this.customizations.value?.dialect,
251+
region: this.customizations.value?.region,
252+
});
253+
}
254+
}
255+
256+
public submitUnlistedLanguageModal({
257+
name,
258+
region,
259+
}: {
260+
name: string;
261+
region: IRegion;
262+
}) {
263+
this.customizations.requestUpdate({
264+
customDisplayName: name,
265+
dialect: name,
266+
region,
267+
});
268+
}
269+
270+
public submitCustomizeLangaugeModal({
271+
script,
272+
region,
273+
dialect,
274+
}: {
275+
script?: IScript;
276+
region?: IRegion;
277+
dialect?: string;
278+
}) {
279+
this.customizations.requestUpdate({
280+
region,
281+
dialect,
282+
customDisplayName: this.customizations.value?.customDisplayName,
283+
});
284+
this.selectedScript.requestUpdate(script);
285+
}
224286
}
225287

226288
function hasValidDisplayName(selection: IOrthography) {

components/language-chooser/common/language-chooser-controller/test/language-chooser.spec.ts

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { describe, expect, it } from "vitest";
1+
import { describe, expect, it, vi } from "vitest";
22
import { LanguageChooserViewModel } from "../src/view-models/language-chooser";
33
import { fakeLanguage } from "./fake-utils";
4-
import { ILanguage, UNLISTED_LANGUAGE } from "@ethnolib/find-language";
4+
import { type ILanguage, UNLISTED_LANGUAGE } from "@ethnolib/find-language";
55
import { NorthernUzbekLanguage, WaataLanguage } from "./sample-data/languages";
66
import { AndorraRegion } from "./sample-data/regions";
77
import { LanguageChooserTranslations } from "../src/view-models/translations";
@@ -457,3 +457,121 @@ describe("translations", () => {
457457
).toBe("Una");
458458
});
459459
});
460+
461+
describe("unlisted language modal", () => {
462+
it("opens on customize button clicked when no language is selected", () => {
463+
const t = new TestHeper();
464+
const spy = vi.fn();
465+
t.viewModel.showUnlistedLanguageModal.requestUpdate(spy);
466+
t.viewModel.onCustomizeButtonClicked();
467+
expect(spy).toHaveBeenCalledWith({});
468+
});
469+
470+
it("sets language to unlisted language on submit", () => {
471+
const t = new TestHeper();
472+
t.viewModel.submitUnlistedLanguageModal({
473+
name: "hello",
474+
region: AndorraRegion,
475+
});
476+
expect(t.viewModel.selectedLanguage.value).toEqual(UNLISTED_LANGUAGE);
477+
});
478+
479+
it("sets display name on submit", () => {
480+
const t = new TestHeper();
481+
t.viewModel.submitUnlistedLanguageModal({
482+
name: "hello",
483+
region: AndorraRegion,
484+
});
485+
expect(t.viewModel.displayName.value).toBe("hello");
486+
});
487+
488+
it("sets dialect on submit", () => {
489+
const t = new TestHeper();
490+
t.viewModel.submitUnlistedLanguageModal({
491+
name: "hello",
492+
region: AndorraRegion,
493+
});
494+
expect(t.viewModel.customizations.value?.dialect).toBe("hello");
495+
});
496+
497+
it("sets region on submit", () => {
498+
const t = new TestHeper();
499+
t.viewModel.submitUnlistedLanguageModal({
500+
name: "hello",
501+
region: AndorraRegion,
502+
});
503+
expect(t.viewModel.customizations.value?.region).toEqual(AndorraRegion);
504+
});
505+
506+
it("opens on customize button clicked when unlisted language is selected", () => {
507+
const t = new TestHeper();
508+
const spy = vi.fn();
509+
t.viewModel.showUnlistedLanguageModal.requestUpdate(spy);
510+
t.viewModel.submitUnlistedLanguageModal({
511+
name: "hello",
512+
region: AndorraRegion,
513+
});
514+
t.viewModel.onCustomizeButtonClicked();
515+
expect(spy).toHaveBeenCalledWith({ name: "hello", region: AndorraRegion });
516+
});
517+
});
518+
519+
describe("customize language modal", () => {
520+
it("opens customize language modal when a language is selected", () => {
521+
const t = new TestHeper({ initialLanguages: [NorthernUzbekLanguage] });
522+
const spy = vi.fn();
523+
t.viewModel.showCustomizeLanguageModal.requestUpdate(spy);
524+
525+
t.viewModel.listedLanguages.value[0].isSelected.requestUpdate(true);
526+
t.viewModel.onCustomizeButtonClicked();
527+
528+
expect(spy).toHaveBeenCalledWith({});
529+
});
530+
531+
it("populates script when a script is selected", () => {
532+
const t = new TestHeper({ initialLanguages: [NorthernUzbekLanguage] });
533+
const spy = vi.fn();
534+
t.viewModel.showCustomizeLanguageModal.requestUpdate(spy);
535+
536+
t.viewModel.listedLanguages.value[0].isSelected.requestUpdate(true);
537+
const scriptViewModel = t.viewModel.listedScripts.value[0];
538+
scriptViewModel.isSelected.requestUpdate(true);
539+
t.viewModel.onCustomizeButtonClicked();
540+
541+
expect(spy).toHaveBeenCalledWith({ script: scriptViewModel.script });
542+
});
543+
544+
it("sets customizations on submit", () => {
545+
const t = new TestHeper({ initialLanguages: [NorthernUzbekLanguage] });
546+
t.viewModel.listedLanguages.value[0].isSelected.requestUpdate(true);
547+
t.viewModel.displayName.requestUpdate("mylang");
548+
549+
t.viewModel.submitCustomizeLangaugeModal({
550+
region: AndorraRegion,
551+
dialect: "abc",
552+
});
553+
554+
expect(t.viewModel.customizations.value).toEqual({
555+
customDisplayName: "mylang",
556+
dialect: "abc",
557+
region: AndorraRegion,
558+
});
559+
});
560+
561+
it("sets script on submit", () => {
562+
const t = new TestHeper({ initialLanguages: [NorthernUzbekLanguage] });
563+
t.viewModel.listedLanguages.value[0].isSelected.requestUpdate(true);
564+
565+
t.viewModel.submitCustomizeLangaugeModal({
566+
script: {
567+
code: "abc",
568+
name: "ABC Script",
569+
},
570+
});
571+
572+
expect(t.viewModel.selectedScript.value).toEqual({
573+
code: "abc",
574+
name: "ABC Script",
575+
});
576+
});
577+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<script lang="ts">
2+
import {
3+
getAllRegions,
4+
type IScript,
5+
type IRegion,
6+
getAllScripts,
7+
} from "@ethnolib/find-language";
8+
9+
let {
10+
onSubmitClicked = $bindable(),
11+
submit,
12+
}: {
13+
onSubmitClicked: () => void;
14+
submit: (script?: IScript, region?: IRegion, name?: string) => void;
15+
} = $props();
16+
17+
const regions = getAllRegions().sort((a, b) => a.name.localeCompare(b.name));
18+
const scripts = getAllScripts().sort((a, b) => a.name.localeCompare(b.name));
19+
20+
let scriptCode: string | undefined = $state();
21+
let regionCode: string | undefined = $state();
22+
let name: string | undefined = $state();
23+
24+
onSubmitClicked = () => {
25+
const region = regions.find((r) => r.code === regionCode);
26+
const script = scripts.find((s) => s.code === scriptCode);
27+
submit(script, region, name);
28+
};
29+
</script>
30+
31+
<div class="mb-4">
32+
<label>
33+
<span class="font-semibold opacity-70">Script</span>
34+
<select class="select w-full" bind:value={scriptCode}>
35+
<option selected value=""></option>
36+
{#each scripts as script}
37+
<option value={script.code}>{script.name}</option>
38+
{/each}
39+
</select>
40+
</label>
41+
</div>
42+
43+
<div class="mb-4">
44+
<label>
45+
<span class="font-semibold opacity-70">Country</span>
46+
<select class="select w-full" bind:value={regionCode}>
47+
<option selected value=""></option>
48+
{#each regions as region}
49+
<option value={region.code}>{region.name}</option>
50+
{/each}
51+
</select>
52+
</label>
53+
</div>
54+
55+
<div class="mb-4">
56+
<label>
57+
<span class="font-semibold opacity-70">Variant (dialect)</span>
58+
<input class="input w-full" bind:value={name} />
59+
</label>
60+
</div>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<script lang="ts">
2+
import type { IRegion, IScript } from "@ethnolib/find-language";
3+
import UnlistedLanguageForm from "./UnlistedLanguageForm.svelte";
4+
import CustomizationForm from "./CustomizationForm.svelte";
5+
import type { SvelteViewModel } from "@ethnolib/state-management-svelte";
6+
import { LanguageChooserViewModel } from "@ethnolib/language-chooser-controller";
7+
8+
let modal: HTMLDialogElement;
9+
10+
let {
11+
languageChooser,
12+
}: {
13+
languageChooser: SvelteViewModel<LanguageChooserViewModel>;
14+
} = $props();
15+
16+
let isCreatingUnlisted = $state(false);
17+
18+
languageChooser.showUnlistedLanguageModal = () => {
19+
isCreatingUnlisted = true;
20+
modal.showModal();
21+
};
22+
23+
languageChooser.showCustomizeLanguageModal = () => {
24+
isCreatingUnlisted = false;
25+
modal.showModal();
26+
};
27+
28+
let title = $derived(
29+
isCreatingUnlisted ? "Unlisted Language Tag" : "Custom Language Tag"
30+
);
31+
32+
function onDismiss() {
33+
modal.close();
34+
}
35+
36+
let onOk = $state(() => {});
37+
38+
function submitUnlisted(name: string, region: IRegion) {
39+
languageChooser.submitUnlistedLanguageModal({ name, region });
40+
modal.close();
41+
}
42+
43+
function submitCustomization(
44+
script?: IScript,
45+
region?: IRegion,
46+
dialect?: string
47+
) {
48+
languageChooser.submitCustomizeLangaugeModal({ script, region, dialect });
49+
modal.close();
50+
}
51+
</script>
52+
53+
<dialog bind:this={modal} class="modal">
54+
<div class="modal-box">
55+
<h3 class="text-xl font-semibold mb-4">{title}</h3>
56+
57+
{#if isCreatingUnlisted}
58+
<UnlistedLanguageForm
59+
bind:onSubmitClicked={onOk}
60+
submit={submitUnlisted}
61+
/>
62+
{:else}
63+
<CustomizationForm
64+
bind:onSubmitClicked={onOk}
65+
submit={submitCustomization}
66+
/>
67+
{/if}
68+
69+
<div class="modal-action">
70+
<button class="btn btn-primary w-24" onclick={onOk}>Ok</button>
71+
<button class="btn w-24" onclick={onDismiss}>Cancel</button>
72+
</div>
73+
</div>
74+
</dialog>

components/language-chooser/svelte/language-chooser-svelte/src/lib/LanguageChooser.svelte

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import SearchIcon from "./SearchIcon.svelte";
99
import { LanguageChooserViewModel } from "@ethnolib/language-chooser-controller";
1010
import { useViewModel } from "@ethnolib/state-management-svelte";
11+
import CustomizationModal from "./CustomizationModal.svelte";
1112
1213
const {
1314
onDismiss,
@@ -70,8 +71,21 @@
7071
<div
7172
class="card card-xs card-border border-base-300 bg-base-100 hover:bg-base-300 shadow-xl w-48 pl-2"
7273
>
73-
<button class="card-body text-left" onclick={() => console.log("hi")}>
74-
<p class="card-title uppercase">Customize</p>
74+
<button
75+
class="card-body text-left"
76+
onclick={() => {
77+
console.log(viewModel.onCustomizeButtonClicked);
78+
console.log(viewModel.displayName);
79+
viewModel.onCustomizeButtonClicked();
80+
}}
81+
>
82+
<p class="card-title uppercase">
83+
{#if viewModel.selectedLanguage}
84+
Customize
85+
{:else}
86+
Unlisted Language
87+
{/if}
88+
</p>
7589
<div class="flex">
7690
<p class="flex-1 font-mono text-sm opacity-60">
7791
{viewModel.tagPreview}
@@ -113,3 +127,5 @@
113127
</div>
114128
</div>
115129
</div>
130+
131+
<CustomizationModal languageChooser={viewModel} />

0 commit comments

Comments
 (0)