mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
feat: replace menu with new design (#4012)
Co-authored-by: Alex Yang <himself65@outlook.com>
This commit is contained in:
@@ -1,138 +1,69 @@
|
||||
import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
type MenuProps,
|
||||
MenuTrigger,
|
||||
styled,
|
||||
} from '@affine/component';
|
||||
import { LOCALES, useI18N } from '@affine/i18n';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { ButtonProps } from '@toeverything/components/button';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export const StyledListItem = styled(MenuItem)(() => ({
|
||||
height: '38px',
|
||||
textTransform: 'capitalize',
|
||||
}));
|
||||
|
||||
interface LanguageMenuContentProps {
|
||||
currentLanguage: string;
|
||||
currentLanguageIndex: number;
|
||||
}
|
||||
import { LOCALES } from '@affine/i18n';
|
||||
import { useI18N } from '@affine/i18n';
|
||||
import { Menu, MenuItem, MenuTrigger } from '@toeverything/components/menu';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
|
||||
// Fixme: keyboard focus should be supported by Menu component
|
||||
const LanguageMenuContent = ({
|
||||
currentLanguage,
|
||||
currentLanguageIndex,
|
||||
}: LanguageMenuContentProps) => {
|
||||
const i18n = useI18N();
|
||||
const changeLanguage = useCallback(
|
||||
(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]);
|
||||
|
||||
onSelect,
|
||||
}: {
|
||||
currentLanguage?: string;
|
||||
onSelect: (value: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{LOCALES.map((option, optionIndex) => {
|
||||
{LOCALES.map(option => {
|
||||
return (
|
||||
<StyledListItem
|
||||
<MenuItem
|
||||
key={option.name}
|
||||
active={option.tag === currentLanguage}
|
||||
userFocused={optionIndex == focusedOptionIndex}
|
||||
selected={currentLanguage === option.originalName}
|
||||
title={option.name}
|
||||
onClick={() => {
|
||||
changeLanguage(option.tag);
|
||||
}}
|
||||
onSelect={() => onSelect(option.tag)}
|
||||
>
|
||||
{option.originalName}
|
||||
</StyledListItem>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface LanguageMenuProps extends Omit<MenuProps, 'children'> {
|
||||
triggerProps?: ButtonProps;
|
||||
}
|
||||
|
||||
export const LanguageMenu = ({
|
||||
triggerProps,
|
||||
...menuProps
|
||||
}: LanguageMenuProps) => {
|
||||
export const LanguageMenu = () => {
|
||||
const i18n = useI18N();
|
||||
|
||||
const currentLanguageIndex = LOCALES.findIndex(
|
||||
item => item.tag === i18n.language
|
||||
const ref = useRef(null);
|
||||
const currentLanguage = useMemo(
|
||||
() => LOCALES.find(item => item.tag === i18n.language),
|
||||
[i18n.language]
|
||||
);
|
||||
const onSelect = useCallback(
|
||||
(event: string) => {
|
||||
return i18n.changeLanguage(event);
|
||||
},
|
||||
[i18n]
|
||||
);
|
||||
const currentLanguage = LOCALES[currentLanguageIndex];
|
||||
assertExists(currentLanguage, 'currentLanguage should exist');
|
||||
|
||||
return (
|
||||
<Menu
|
||||
content={
|
||||
<LanguageMenuContent
|
||||
currentLanguage={currentLanguage.tag}
|
||||
currentLanguageIndex={currentLanguageIndex}
|
||||
/>
|
||||
items={
|
||||
(
|
||||
<LanguageMenuContent
|
||||
currentLanguage={currentLanguage?.originalName}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
) as ReactElement
|
||||
}
|
||||
placement="bottom-end"
|
||||
trigger="click"
|
||||
disablePortal={true}
|
||||
{...menuProps}
|
||||
portalOptions={{
|
||||
container: ref.current,
|
||||
}}
|
||||
>
|
||||
<MenuTrigger
|
||||
ref={ref}
|
||||
data-testid="language-menu-button"
|
||||
style={{ textTransform: 'capitalize' }}
|
||||
{...triggerProps}
|
||||
style={{ textTransform: 'capitalize', fontWeight: 600 }}
|
||||
block={true}
|
||||
>
|
||||
{currentLanguage.originalName}
|
||||
{currentLanguage?.originalName || ''}
|
||||
</MenuTrigger>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const hoveredLanguageItem = style({
|
||||
background: 'var(--affine-hover-color)',
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Menu, MenuItem } from '@affine/component';
|
||||
import {
|
||||
InviteModal,
|
||||
type InviteModalProps,
|
||||
@@ -12,6 +11,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { MoreVerticalIcon } from '@blocksuite/icons';
|
||||
import { Avatar } from '@toeverything/components/avatar';
|
||||
import { Button, IconButton } from '@toeverything/components/button';
|
||||
import { Menu, MenuItem } from '@toeverything/components/menu';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
import clsx from 'clsx';
|
||||
import { useSetAtom } from 'jotai/react';
|
||||
@@ -190,14 +190,11 @@ const MemberItem = ({
|
||||
: 'Pending'}
|
||||
</div>
|
||||
<Menu
|
||||
content={
|
||||
items={
|
||||
<MenuItem data-member-id={member.id} onClick={handleRevoke}>
|
||||
{operationButtonInfo.leaveOrRevokeText}
|
||||
</MenuItem>
|
||||
}
|
||||
placement="bottom"
|
||||
disablePortal={true}
|
||||
trigger="click"
|
||||
>
|
||||
<IconButton
|
||||
disabled={!operationButtonInfo.show}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Menu, MenuItem, MenuTrigger } from '@affine/component';
|
||||
import { Menu, MenuItem, MenuTrigger } from '@toeverything/components/menu';
|
||||
import dayjs from 'dayjs';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import {
|
||||
dateFormatOptions,
|
||||
@@ -23,10 +23,8 @@ const DateFormatMenuContent = ({
|
||||
return (
|
||||
<MenuItem
|
||||
key={option}
|
||||
active={currentOption === option}
|
||||
onClick={() => {
|
||||
onSelect(option);
|
||||
}}
|
||||
selected={currentOption === option}
|
||||
onSelect={() => onSelect(option)}
|
||||
>
|
||||
{dayjs(new Date()).format(option)}
|
||||
</MenuItem>
|
||||
@@ -37,6 +35,7 @@ const DateFormatMenuContent = ({
|
||||
};
|
||||
|
||||
export const DateFormatSetting = () => {
|
||||
const ref = useRef(null);
|
||||
const [appearanceSettings, setAppSettings] = useAppSetting();
|
||||
const handleSelect = useCallback(
|
||||
(option: DateFormats) => {
|
||||
@@ -47,17 +46,15 @@ export const DateFormatSetting = () => {
|
||||
|
||||
return (
|
||||
<Menu
|
||||
content={
|
||||
items={
|
||||
<DateFormatMenuContent
|
||||
onSelect={handleSelect}
|
||||
currentOption={appearanceSettings.dateFormat}
|
||||
/>
|
||||
}
|
||||
placement="bottom-end"
|
||||
trigger="click"
|
||||
disablePortal={true}
|
||||
portalOptions={{ container: ref.current }}
|
||||
>
|
||||
<MenuTrigger data-testid="date-format-menu-trigger">
|
||||
<MenuTrigger ref={ref} data-testid="date-format-menu-trigger" block>
|
||||
{dayjs(new Date()).format(appearanceSettings.dateFormat)}
|
||||
</MenuTrigger>
|
||||
</Menu>
|
||||
|
||||
@@ -113,17 +113,7 @@ export const AppearanceSettings = () => {
|
||||
desc={t['com.affine.settings.appearance.language-description']()}
|
||||
>
|
||||
<div className={settingWrapper}>
|
||||
<LanguageMenu
|
||||
triggerContainerStyle={{ width: '100%' }}
|
||||
triggerProps={{
|
||||
style: {
|
||||
width: '100%',
|
||||
justifyContent: 'space-between',
|
||||
fontWeight: 600,
|
||||
padding: '0 10px',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<LanguageMenu />
|
||||
</div>
|
||||
</SettingRow>
|
||||
{environment.isDesktop ? (
|
||||
|
||||
Reference in New Issue
Block a user