From 18d5a99af52503540662ea6d35104dd0bcbc866e Mon Sep 17 00:00:00 2001 From: Priyansh Gupta Date: Thu, 31 Aug 2023 23:45:55 +0530 Subject: [PATCH] feat(core): added code to handle keyboard inputs (#4006) Co-authored-by: Alex Yang --- .../components/affine/language-menu/index.tsx | 90 ++++++++++++++----- packages/component/src/ui/menu/menu-item.tsx | 1 + packages/component/src/ui/menu/styles.ts | 8 +- tests/affine-local/e2e/settings.spec.ts | 33 +++++++ 4 files changed, 111 insertions(+), 21 deletions(-) diff --git a/apps/core/src/components/affine/language-menu/index.tsx b/apps/core/src/components/affine/language-menu/index.tsx index 20ae9b00ed..0ee1e289e1 100644 --- a/apps/core/src/components/affine/language-menu/index.tsx +++ b/apps/core/src/components/affine/language-menu/index.tsx @@ -5,11 +5,10 @@ import { MenuTrigger, styled, } from '@affine/component'; -import { LOCALES } from '@affine/i18n'; -import { useI18N } from '@affine/i18n'; +import { LOCALES, useI18N } from '@affine/i18n'; +import { assertExists } from '@blocksuite/global/utils'; import type { ButtonProps } from '@toeverything/components/button'; -import type { ReactElement } from 'react'; -import { useCallback } from 'react'; +import { useCallback, useEffect, useState } from 'react'; export const StyledListItem = styled(MenuItem)(() => ({ height: '38px', @@ -17,30 +16,78 @@ export const StyledListItem = styled(MenuItem)(() => ({ })); interface LanguageMenuContentProps { - currentLanguage?: string; + currentLanguage: string; + currentLanguageIndex: number; } -const LanguageMenuContent = ({ currentLanguage }: LanguageMenuContentProps) => { +const LanguageMenuContent = ({ + currentLanguage, + currentLanguageIndex, +}: LanguageMenuContentProps) => { const i18n = useI18N(); const changeLanguage = useCallback( - (event: string) => { - return i18n.changeLanguage(event); + (targetLanguage: string) => { + console.assert( + LOCALES.some(item => item.tag === targetLanguage), + 'targetLanguage should be one of the LOCALES' + ); + i18n.changeLanguage(targetLanguage).catch(err => { + console.error('Failed to change language', err); + }); }, [i18n] ); + const [focusedOptionIndex, setFocusedOptionIndex] = useState( + currentLanguageIndex ?? 0 + ); + + const handleKeyDown = useCallback( + (event: KeyboardEvent) => { + switch (event.key) { + case 'ArrowUp': + event.preventDefault(); + setFocusedOptionIndex(prevIndex => + prevIndex > 0 ? prevIndex - 1 : 0 + ); + break; + case 'ArrowDown': + event.preventDefault(); + setFocusedOptionIndex(prevIndex => + prevIndex < LOCALES.length - 1 ? prevIndex + 1 : LOCALES.length + ); + break; + case 'Enter': + if (focusedOptionIndex !== -1) { + const selectedOption = LOCALES[focusedOptionIndex]; + changeLanguage(selectedOption.tag); + } + break; + default: + break; + } + }, + [changeLanguage, focusedOptionIndex] + ); + + useEffect(() => { + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [handleKeyDown]); + return ( <> - {LOCALES.map(option => { + {LOCALES.map((option, optionIndex) => { return ( { - changeLanguage(option.tag).catch(err => { - throw new Error('Failed to change language', err); - }); + changeLanguage(option.tag); }} > {option.originalName} @@ -61,16 +108,19 @@ export const LanguageMenu = ({ }: LanguageMenuProps) => { const i18n = useI18N(); - const currentLanguage = LOCALES.find(item => item.tag === i18n.language); + const currentLanguageIndex = LOCALES.findIndex( + item => item.tag === i18n.language + ); + const currentLanguage = LOCALES[currentLanguageIndex]; + assertExists(currentLanguage, 'currentLanguage should exist'); return ( - ) as ReactElement + } placement="bottom-end" trigger="click" @@ -82,7 +132,7 @@ export const LanguageMenu = ({ style={{ textTransform: 'capitalize' }} {...triggerProps} > - {currentLanguage?.originalName} + {currentLanguage.originalName} ); diff --git a/packages/component/src/ui/menu/menu-item.tsx b/packages/component/src/ui/menu/menu-item.tsx index c7829239dd..08e83447dc 100644 --- a/packages/component/src/ui/menu/menu-item.tsx +++ b/packages/component/src/ui/menu/menu-item.tsx @@ -15,6 +15,7 @@ export type IconMenuProps = PropsWithChildren<{ disabled?: boolean; active?: boolean; disableHover?: boolean; + userFocused?: boolean; gap?: string; fontSize?: string; }> & diff --git a/packages/component/src/ui/menu/styles.ts b/packages/component/src/ui/menu/styles.ts index 5b27fb5a24..d11258bd52 100644 --- a/packages/component/src/ui/menu/styles.ts +++ b/packages/component/src/ui/menu/styles.ts @@ -62,11 +62,13 @@ export const StyledMenuItem = styled('button')<{ disabled?: boolean; active?: boolean; disableHover?: boolean; + userFocused?: boolean; }>(({ isDir = false, disabled = false, active = false, disableHover = false, + userFocused = false, }) => { return { width: '100%', @@ -99,7 +101,11 @@ export const StyledMenuItem = styled('button')<{ : { backgroundColor: 'var(--affine-hover-color)', }, - + ...(userFocused && !disabled + ? { + backgroundColor: 'var(--affine-hover-color)', + } + : {}), ...(active && !disabled ? { backgroundColor: 'var(--affine-hover-color)', diff --git a/tests/affine-local/e2e/settings.spec.ts b/tests/affine-local/e2e/settings.spec.ts index 5b0d20b2b8..8cb985ed49 100644 --- a/tests/affine-local/e2e/settings.spec.ts +++ b/tests/affine-local/e2e/settings.spec.ts @@ -20,6 +20,39 @@ test('Open settings modal', async ({ page }) => { await expect(modal).toBeVisible(); }); +test('change language using keyboard', async ({ page }) => { + await openHomePage(page); + await waitEditorLoad(page); + await openSettingModal(page); + + const locator = page.getByTestId('language-menu-button'); + const oldName = await locator.textContent(); + await locator.click(); + await page.waitForTimeout(200); + await page.keyboard.press('ArrowDown', { + delay: 50, + }); + await page.keyboard.press('Enter', { + delay: 50, + }); + { + const newName = await locator.textContent(); + expect(oldName).not.toBe(newName); + } + await locator.click(); + await page.waitForTimeout(200); + await page.keyboard.press('ArrowUp', { + delay: 50, + }); + await page.keyboard.press('Enter', { + delay: 50, + }); + { + const newName = await locator.textContent(); + expect(oldName).toBe(newName); + } +}); + test('Change theme', async ({ page }) => { await openHomePage(page); await waitEditorLoad(page);