mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 01:09:54 +08:00
feat(core): added code to handle keyboard inputs (#4006)
Co-authored-by: Alex Yang <himself65@outlook.com>
This commit is contained in:
@@ -5,11 +5,10 @@ import {
|
|||||||
MenuTrigger,
|
MenuTrigger,
|
||||||
styled,
|
styled,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { LOCALES } from '@affine/i18n';
|
import { LOCALES, useI18N } from '@affine/i18n';
|
||||||
import { useI18N } from '@affine/i18n';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import type { ButtonProps } from '@toeverything/components/button';
|
import type { ButtonProps } from '@toeverything/components/button';
|
||||||
import type { ReactElement } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
export const StyledListItem = styled(MenuItem)(() => ({
|
export const StyledListItem = styled(MenuItem)(() => ({
|
||||||
height: '38px',
|
height: '38px',
|
||||||
@@ -17,30 +16,78 @@ export const StyledListItem = styled(MenuItem)(() => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
interface LanguageMenuContentProps {
|
interface LanguageMenuContentProps {
|
||||||
currentLanguage?: string;
|
currentLanguage: string;
|
||||||
|
currentLanguageIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LanguageMenuContent = ({ currentLanguage }: LanguageMenuContentProps) => {
|
const LanguageMenuContent = ({
|
||||||
|
currentLanguage,
|
||||||
|
currentLanguageIndex,
|
||||||
|
}: LanguageMenuContentProps) => {
|
||||||
const i18n = useI18N();
|
const i18n = useI18N();
|
||||||
const changeLanguage = useCallback(
|
const changeLanguage = useCallback(
|
||||||
(event: string) => {
|
(targetLanguage: string) => {
|
||||||
return i18n.changeLanguage(event);
|
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]
|
[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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{LOCALES.map(option => {
|
{LOCALES.map((option, optionIndex) => {
|
||||||
return (
|
return (
|
||||||
<StyledListItem
|
<StyledListItem
|
||||||
key={option.name}
|
key={option.name}
|
||||||
active={currentLanguage === option.originalName}
|
active={option.tag === currentLanguage}
|
||||||
|
userFocused={optionIndex == focusedOptionIndex}
|
||||||
title={option.name}
|
title={option.name}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
changeLanguage(option.tag).catch(err => {
|
changeLanguage(option.tag);
|
||||||
throw new Error('Failed to change language', err);
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{option.originalName}
|
{option.originalName}
|
||||||
@@ -61,16 +108,19 @@ export const LanguageMenu = ({
|
|||||||
}: LanguageMenuProps) => {
|
}: LanguageMenuProps) => {
|
||||||
const i18n = useI18N();
|
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 (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
content={
|
content={
|
||||||
(
|
<LanguageMenuContent
|
||||||
<LanguageMenuContent
|
currentLanguage={currentLanguage.tag}
|
||||||
currentLanguage={currentLanguage?.originalName}
|
currentLanguageIndex={currentLanguageIndex}
|
||||||
/>
|
/>
|
||||||
) as ReactElement
|
|
||||||
}
|
}
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
@@ -82,7 +132,7 @@ export const LanguageMenu = ({
|
|||||||
style={{ textTransform: 'capitalize' }}
|
style={{ textTransform: 'capitalize' }}
|
||||||
{...triggerProps}
|
{...triggerProps}
|
||||||
>
|
>
|
||||||
{currentLanguage?.originalName}
|
{currentLanguage.originalName}
|
||||||
</MenuTrigger>
|
</MenuTrigger>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export type IconMenuProps = PropsWithChildren<{
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
disableHover?: boolean;
|
disableHover?: boolean;
|
||||||
|
userFocused?: boolean;
|
||||||
gap?: string;
|
gap?: string;
|
||||||
fontSize?: string;
|
fontSize?: string;
|
||||||
}> &
|
}> &
|
||||||
|
|||||||
@@ -62,11 +62,13 @@ export const StyledMenuItem = styled('button')<{
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
disableHover?: boolean;
|
disableHover?: boolean;
|
||||||
|
userFocused?: boolean;
|
||||||
}>(({
|
}>(({
|
||||||
isDir = false,
|
isDir = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
active = false,
|
active = false,
|
||||||
disableHover = false,
|
disableHover = false,
|
||||||
|
userFocused = false,
|
||||||
}) => {
|
}) => {
|
||||||
return {
|
return {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -99,7 +101,11 @@ export const StyledMenuItem = styled('button')<{
|
|||||||
: {
|
: {
|
||||||
backgroundColor: 'var(--affine-hover-color)',
|
backgroundColor: 'var(--affine-hover-color)',
|
||||||
},
|
},
|
||||||
|
...(userFocused && !disabled
|
||||||
|
? {
|
||||||
|
backgroundColor: 'var(--affine-hover-color)',
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
...(active && !disabled
|
...(active && !disabled
|
||||||
? {
|
? {
|
||||||
backgroundColor: 'var(--affine-hover-color)',
|
backgroundColor: 'var(--affine-hover-color)',
|
||||||
|
|||||||
@@ -20,6 +20,39 @@ test('Open settings modal', async ({ page }) => {
|
|||||||
await expect(modal).toBeVisible();
|
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 }) => {
|
test('Change theme', async ({ page }) => {
|
||||||
await openHomePage(page);
|
await openHomePage(page);
|
||||||
await waitEditorLoad(page);
|
await waitEditorLoad(page);
|
||||||
|
|||||||
Reference in New Issue
Block a user