Skip to content

Commit

Permalink
webui: Use grouped typeahead for the language selector
Browse files Browse the repository at this point in the history
  • Loading branch information
KKoukiou committed Feb 21, 2022
1 parent d00b206 commit 6ab4806
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 123 deletions.
187 changes: 70 additions & 117 deletions src/InstallationLanguage.jsx
Expand Up @@ -15,35 +15,35 @@
* along with This program; If not, see <http://www.gnu.org/licenses/>.
*/

import React, { useContext, useState } from "react";
import React, { useContext, useEffect, useState } from "react";
import cockpit from "cockpit";

import {
ActionGroup,
Button,
Form, FormGroup,
Menu, MenuContent, MenuList, MenuInput, MenuItem, Divider, DrilldownMenu,
PageSection,
TextInput, Title,
SelectGroup, SelectOption, Select, SelectVariant,
Title,
} from "@patternfly/react-core";

import { AddressContext } from "./Common.jsx";

import { useEvent, useObject } from "hooks";

import "./InstallationLanguage.scss";

const _ = cockpit.gettext;

const LanguageSelector = ({ onSelectLang }) => {
const [activeItem, setActiveItem] = useState();
const [activeMenu, setActiveMenu] = useState("languageMenu");
const [drilldownPath, setDrilldownPath] = useState([]);
const [filterText, setFilterText] = useState("");
const [menuDrilledIn, setMenuDrilledIn] = useState([]);
const [selectedItem, setSelectedItem] = useState();
const getLanguageEnglishName = lang => lang["english-name"].v;
const getLanguageId = lang => lang["language-id"].v;
const getLanguageNativeName = lang => lang["native-name"].v;
const getLocaleId = locale => locale["locale-id"].v;
const getLocaleNativeName = locale => locale["native-name"].v;

const LanguageSelector = ({ lang, onSelectLang }) => {
const [isOpen, setIsOpen] = useState();
const [languages, setLanguages] = useState([]);
const [locales, setLocales] = useState({});
const [selectedItem, setSelectedItem] = useState();
const address = useContext(AddressContext);

const localizationProxy = useObject(() => {
Expand All @@ -69,127 +69,79 @@ const LanguageSelector = ({ onSelectLang }) => {
})
);
})
.then(res => {
setLocales(res.reduce((a, v) => ({ ...a, [v[0]["language-id"].v]: v }), {}));
});
.then(setLocales);
});
});

const handleDrillIn = (fromMenuId, toMenuId, pathId) => {
setMenuDrilledIn([...menuDrilledIn, fromMenuId]);
setDrilldownPath([...drilldownPath, pathId]);
setActiveMenu(toMenuId);
};
const handleDrillOut = toMenuId => {
const menuDrilledInSansLast = menuDrilledIn.slice(0, menuDrilledIn.length - 1);
const pathSansLast = drilldownPath.slice(0, drilldownPath.length - 1);

setMenuDrilledIn(menuDrilledInSansLast);
setDrilldownPath(pathSansLast);
setActiveItem(toMenuId);
};
const handleOnSelect = (event, itemId) => {
if (Object.keys(menuItems).includes(itemId)) {
useEffect(() => {
// Once the component state contains the locale data from the API set the default selected language
if (selectedItem || !locales.length) {
return;
}

onSelectLang(itemId);
setActiveItem(itemId);
setSelectedItem(itemId);
};
const getNestedItemLabel = (itemLabel) => {
return itemLabel;
const languageId = lang.split("_")[0];
const currentLangLocales = locales.find(langLocales => getLanguageId(langLocales[0]) === languageId);
const currentLocale = currentLangLocales.find(locale => getLocaleId(locale) === lang);

setSelectedItem(getLocaleNativeName(currentLocale));
}, [locales, lang, selectedItem]);

const handleOnSelect = (event, lang) => {
onSelectLang(lang.localeId);
setSelectedItem(lang);
};

if (languages.length !== Object.keys(locales).length) {
return null;
}

const menuItems = {};
languages.forEach(lang => {
menuItems[lang["language-id"].v] = {
label: cockpit.format("$0 ($1)", lang["english-name"].v, lang["native-name"].v),
subgroup: Object.fromEntries(
locales[lang["language-id"].v].map(locale => [locale["locale-id"].v, {
label: locale["native-name"].v,
}])
),
};
});
const isLoading = languages.length !== locales.length;
const options = (
!isLoading
? locales.map(langLocales => {
const currentLang = languages.find(lang => getLanguageId(lang) === getLanguageId(langLocales[0]));

return (
<SelectGroup
label={cockpit.format("$0 ($1)", getLanguageNativeName(currentLang), getLanguageEnglishName(currentLang))}
key={getLanguageId(currentLang)}>
{langLocales.map(locale => (
<SelectOption
id={getLocaleId(locale).split(".UTF-8")[0]}
key={getLocaleId(locale)}
value={{
toString: () => getLocaleNativeName(locale),
// Add a compareTo for custom filtering - filter also by english name
localeId: getLocaleId(locale)
}}
/>
))}
</SelectGroup>
);
})
: []
);

return (
<Menu
id="languageMenu"
<Select
className="language-menu"
containsDrilldown
drilldownItemPath={drilldownPath}
drilledInMenus={menuDrilledIn}
activeMenu={activeMenu}
isGrouped
isOpen={isOpen}
maxHeight="30rem"
onClear={() => setSelectedItem(null)}
onSelect={handleOnSelect}
activeItemId={activeItem}
selected={selectedItem}
onDrillIn={handleDrillIn}
onDrillOut={handleDrillOut}
onToggle={setIsOpen}
selections={selectedItem}
toggleId="language-menu-toggle"
variant={SelectVariant.typeahead}
width="30rem"
{...(isLoading && { loadingVariant: "spinner" })}

>
<MenuInput>
<TextInput
aria-label="Filter menu items"
iconVariant="search"
onChange={setFilterText}
type="search"
value={filterText}
/>
</MenuInput>
<Divider />
<MenuContent>
<MenuList>
{Object.keys(menuItems)
.filter((groupKey, index) => !filterText || drilldownPath.length || menuItems[groupKey].label.toLowerCase().includes(filterText.toLowerCase()))
.map(groupKey => {
const group = menuItems[groupKey];
const groupLabel = group.label;

return (
<MenuItem
id={groupKey}
itemId={groupKey}
key={groupKey}
direction="down"
drilldownMenu={
<DrilldownMenu id={"drilldownMenu_" + groupKey}>
<MenuItem itemId={groupKey} direction="up">
{groupLabel}
</MenuItem>
<Divider component="li" />
{Object.keys(menuItems[groupKey].subgroup)
.filter((itemKey, index) => {
return (
!filterText || !drilldownPath.length ||
getNestedItemLabel(group.subgroup[itemKey].label).toLowerCase()
.includes(filterText.toLowerCase())
);
})
.map(itemKey => {
return (
<MenuItem id={itemKey.split(".UTF-8")[0]} itemId={itemKey} key={itemKey}>
{getNestedItemLabel(group.subgroup[itemKey].label)}
</MenuItem>
);
})}
</DrilldownMenu>
}>
{groupLabel}
</MenuItem>
);
})}
</MenuList>
</MenuContent>
</Menu>
{options}
</Select>
);
};

export const InstallationLanguage = ({ onSelectLang }) => {
const [lang, setLang] = useState("en-us");
const langCookie = (window.localStorage.getItem("cockpit.lang") || "en-us").split("-");
const [lang, setLang] = useState(langCookie[0] + "_" + langCookie[1].toUpperCase() + ".UTF-8");

const handleOnContinue = () => {
if (!lang) {
Expand All @@ -202,6 +154,7 @@ export const InstallationLanguage = ({ onSelectLang }) => {
*/
const cockpitLang = lang.split(".UTF-8")[0].replace(/_/g, "-").toLowerCase();
const cookie = "CockpitLang=" + encodeURIComponent(cockpitLang) + "; path=/; expires=Sun, 16 Jul 3567 06:23:41 GMT";

document.cookie = cookie;
window.localStorage.setItem("cockpit.lang", cockpitLang);
cockpit.location.go(["summary"]);
Expand All @@ -215,7 +168,7 @@ export const InstallationLanguage = ({ onSelectLang }) => {
WELCOME TO FEDORA...
</Title>
<FormGroup label={_("What language would you like to use during the installation process?")}>
<LanguageSelector onSelectLang={setLang} />
<LanguageSelector lang={lang} onSelectLang={setLang} />
</FormGroup>
<ActionGroup>
<Button id="continue-btn" variant="primary" onClick={handleOnContinue}>{_("Continue")}</Button>
Expand Down
4 changes: 1 addition & 3 deletions src/InstallationLanguage.scss
@@ -1,4 +1,2 @@
.language-menu.pf-c-menu {
width: 30rem;
height: 30rem;
.language-menu {
}
17 changes: 14 additions & 3 deletions test/check-installation-language
Expand Up @@ -36,10 +36,21 @@ class TestLanguage(MachineCase):
# Expect the default language - this is en at this point - adjust the test when better guess is implemented
b.wait_in_text("button.pf-m-link", "Quit")

b.wait_in_text("#de", "German (Deutsch)")
b.click("#de > button")
b.wait_in_text("#de_DE", "Deutsch (Deutschland)")
b.wait_val("#language-menu-toggle-select-typeahead", "English (United States)")

# Check that the language menu shows all menu entries when clicking the toggle button
b.click("#language-menu-toggle")
b.wait_visible("#English--English-")
b.wait_visible("#Deutsch--German-")

# Check that the language menu shows filtered menu entries
b.set_input_text("#language-menu-toggle-select-typeahead", "Deutsch")
b.wait_visible("#Deutsch--German-")
b.wait_not_present("#English--English-")

# Select the 'German' language
b.click("#de_DE > button")
b.wait_val("#language-menu-toggle-select-typeahead", "Deutsch (Deutschland)")

b.click("#continue-btn")

Expand Down

0 comments on commit 6ab4806

Please sign in to comment.