Skip to content

Commit c17daf8

Browse files
ayaka14732claudeCopilot
authored
Replace language select box with hover dropdown (#74)
* Replace language select box with hover dropdown Removes the globe icon + native <select> and replaces it with a 「文A」SVG icon button that reveals a styled dropdown on hover (desktop) or tap (mobile). Closes on outside click via mousedown listener. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix Prettier formatting Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Address accessibility and style review feedback - pointerdown instead of mousedown (covers touch/pen) - LangSwitcherBtn extends ShowButton to avoid style duplication - type="button", aria-label, aria-haspopup, aria-controls, aria-expanded on switcher button - role="menu" + id on dropdown, role="menuitem" + tabIndex + onKeyDown on items - Add app.language i18n key Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Sync menu visibility with React state; fix Space key default - Remove CSS :hover rule; drive display entirely via langMenuOpen - Add onMouseEnter/Leave on LangSwitcher for desktop hover (mouseleave doesn't fire when moving to absolutely-positioned DOM children, so no debounce or ::before bridge needed) - Remove ::before gap bridge (no longer needed) - Add e.preventDefault() in onKeyDown handlers to suppress page scroll on Space Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Remove menu/menuitem roles to avoid false ARIA contract role="menu" implies arrow-key navigation and roving tabindex which we don't implement. Removing the roles (and aria-haspopup="menu") lets the native ul/li semantics speak; aria-expanded + aria-controls on the button still communicate the popup relationship. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 7e386a1 commit c17daf8

3 files changed

Lines changed: 111 additions & 29 deletions

File tree

public/locales/en/translation.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"app.defaultFileName": "Unnamed",
55
"app.info.about.title": "About",
66
"app.info.userGuide.title": "User Guide",
7+
"app.language": "Language",
78

89
"action.createSchema": "Add or Load Derivation Scheme",
910
"action.deleteSchema": "Delete Derivation Scheme",

public/locales/zh/translation.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"app.defaultFileName": "無標題",
55
"app.info.about.title": "關於",
66
"app.info.userGuide.title": "使用說明",
7+
"app.language": "語言",
78

89
"action.createSchema": "新增或載入推導方案",
910
"action.deleteSchema": "刪除推導方案",

src/Components/App.tsx

Lines changed: 109 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect, useRef } from "react";
1+
import { useCallback, useEffect, useRef, useState } from "react";
22

33
import "purecss/build/pure.css";
44
import { useTranslation } from "react-i18next";
@@ -7,7 +7,7 @@ import "sweetalert2/dist/sweetalert2.css";
77

88
import { css as stylesheet, injectGlobal } from "@emotion/css";
99
import styled from "@emotion/styled";
10-
import { faCirclePlay, faExternalLink, faGlobe, faInfo, faQuestion } from "@fortawesome/free-solid-svg-icons";
10+
import { faCirclePlay, faExternalLink, faInfo, faQuestion } from "@fortawesome/free-solid-svg-icons";
1111
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
1212

1313
import Main from "./Main";
@@ -582,23 +582,54 @@ const LinkToLegacy = styled.span`
582582
}
583583
}
584584
`;
585-
const LanguageDropdownWrapper = styled.label`
586-
font-size: 1.25rem;
587-
display: inline-flex;
585+
const LangSwitcher = styled.div`
586+
position: relative;
587+
display: flex;
588588
align-items: center;
589-
vertical-align: bottom;
590-
gap: 0.5rem;
589+
height: 1.5rem;
590+
margin-left: 0.5rem;
591591
`;
592-
const LanguageIcon = styled(FontAwesomeIcon)`
593-
border-radius: 9999px;
594-
width: 1.25rem;
595-
height: 1.25rem;
596-
color: #666;
597-
border: 0.125rem solid #666;
598-
padding: 0.125rem;
592+
const LangSwitcherBtn = styled(ShowButton)`
593+
margin-left: 0;
594+
justify-content: center;
595+
`;
596+
const LangIconSvg = () => (
597+
<svg viewBox="0 0 18 15" width="0.9em" height="0.75em" fill="currentColor" aria-hidden="true">
598+
<text x="0" y="11" fontSize="12" fontWeight="600">
599+
600+
</text>
601+
<text x="9.5" y="14.5" fontSize="8.5" fontWeight="700">
602+
A
603+
</text>
604+
</svg>
605+
);
606+
const LangMenu = styled.ul`
607+
display: none;
608+
position: absolute;
609+
top: 100%;
610+
left: 0;
611+
background: white;
612+
border: 1px solid #ddd;
613+
border-radius: 0.375rem;
614+
list-style: none;
615+
margin: 0;
616+
padding: 0.25rem 0;
617+
min-width: 5rem;
618+
z-index: 100;
619+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
620+
white-space: nowrap;
599621
`;
600-
const LanguageDropdown = styled.select`
601-
margin: -0.125rem 0;
622+
const LangMenuItem = styled.li<{ $active: boolean }>`
623+
padding: 0.375rem 0.75rem;
624+
cursor: pointer;
625+
font-size: 0.875rem;
626+
color: ${({ $active }) => ($active ? "#0078e7" : "#333")};
627+
font-weight: ${({ $active }) => ($active ? "600" : "normal")};
628+
&:hover,
629+
&:focus {
630+
background: #f0f4f8;
631+
outline: none;
632+
}
602633
`;
603634
const FontPreload = styled.span`
604635
position: absolute;
@@ -612,6 +643,17 @@ const FontPreload = styled.span`
612643
export default function App() {
613644
const { t, i18n } = useTranslation();
614645
const evaluateHandlerRef = useRef(noop);
646+
const [langMenuOpen, setLangMenuOpen] = useState(false);
647+
const langSwitcherRef = useRef<HTMLDivElement>(null);
648+
649+
useEffect(() => {
650+
if (!langMenuOpen) return;
651+
const handler = (e: PointerEvent) => {
652+
if (!langSwitcherRef.current?.contains(e.target as Node)) setLangMenuOpen(false);
653+
};
654+
document.addEventListener("pointerdown", handler);
655+
return () => document.removeEventListener("pointerdown", handler);
656+
}, [langMenuOpen]);
615657

616658
useEffect(() => {
617659
const langCode = i18n.language === "en" ? "en-GB" : "zh-HK";
@@ -645,20 +687,58 @@ export default function App() {
645687
<ShowButton title={t("app.info.userGuide.title")} onClick={useCallback(() => showHelp(t), [t])}>
646688
<FontAwesomeIcon icon={faQuestion} fixedWidth />
647689
</ShowButton>
690+
<LangSwitcher
691+
ref={langSwitcherRef}
692+
onMouseEnter={() => setLangMenuOpen(true)}
693+
onMouseLeave={() => setLangMenuOpen(false)}>
694+
<LangSwitcherBtn
695+
type="button"
696+
aria-label={t("app.language")}
697+
aria-controls="lang-menu"
698+
aria-expanded={langMenuOpen}
699+
onClick={() => setLangMenuOpen((v: boolean) => !v)}>
700+
<LangIconSvg />
701+
</LangSwitcherBtn>
702+
<LangMenu id="lang-menu" style={langMenuOpen ? { display: "block" } : undefined}>
703+
<LangMenuItem
704+
role="button"
705+
tabIndex={0}
706+
lang="zh-HK"
707+
$active={i18n.language === "zh"}
708+
onClick={() => {
709+
i18n.changeLanguage("zh");
710+
setLangMenuOpen(false);
711+
}}
712+
onKeyDown={e => {
713+
if (e.key === "Enter" || e.key === " ") {
714+
e.preventDefault();
715+
i18n.changeLanguage("zh");
716+
setLangMenuOpen(false);
717+
}
718+
}}>
719+
中文
720+
</LangMenuItem>
721+
<LangMenuItem
722+
role="button"
723+
tabIndex={0}
724+
lang="en-GB"
725+
$active={i18n.language === "en"}
726+
onClick={() => {
727+
i18n.changeLanguage("en");
728+
setLangMenuOpen(false);
729+
}}
730+
onKeyDown={e => {
731+
if (e.key === "Enter" || e.key === " ") {
732+
e.preventDefault();
733+
i18n.changeLanguage("en");
734+
setLangMenuOpen(false);
735+
}
736+
}}>
737+
English
738+
</LangMenuItem>
739+
</LangMenu>
740+
</LangSwitcher>
648741
</Buttons>
649-
<LanguageDropdownWrapper className="pure-form">
650-
<LanguageIcon icon={faGlobe} />
651-
<LanguageDropdown
652-
onChange={event => i18n.changeLanguage(event.currentTarget.value)}
653-
value={i18n.language}>
654-
<option value="zh" lang="zh-HK">
655-
中文
656-
</option>
657-
<option value="en" lang="en-GB">
658-
English
659-
</option>
660-
</LanguageDropdown>
661-
</LanguageDropdownWrapper>
662742
</Heading>
663743
</nav>
664744
</header>

0 commit comments

Comments
 (0)