mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-17 14:27:02 +08:00
feat(mobile): use native select for mobile setting (#9236)

This commit is contained in:
@@ -6,6 +6,7 @@ export const root = style({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 8,
|
gap: 8,
|
||||||
|
position: 'relative',
|
||||||
});
|
});
|
||||||
export const label = style([
|
export const label = style([
|
||||||
bodyRegular,
|
bodyRegular,
|
||||||
@@ -15,3 +16,11 @@ export const icon = style({
|
|||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
color: cssVarV2('icon/primary'),
|
color: cssVarV2('icon/primary'),
|
||||||
});
|
});
|
||||||
|
export const nativeSelect = style({
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
opacity: 0,
|
||||||
|
});
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import {
|
|||||||
type CSSProperties,
|
type CSSProperties,
|
||||||
type HTMLAttributes,
|
type HTMLAttributes,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
|
useCallback,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import * as styles from './dropdown-select.css';
|
import * as styles from './dropdown-select.css';
|
||||||
@@ -28,6 +30,7 @@ export interface SettingDropdownSelectProps<
|
|||||||
) => void;
|
) => void;
|
||||||
emitValue?: E;
|
emitValue?: E;
|
||||||
menuOptions?: Omit<MenuProps, 'items' | 'children'>;
|
menuOptions?: Omit<MenuProps, 'items' | 'children'>;
|
||||||
|
native?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SettingDropdownSelect = <
|
export const SettingDropdownSelect = <
|
||||||
@@ -40,12 +43,26 @@ export const SettingDropdownSelect = <
|
|||||||
onChange,
|
onChange,
|
||||||
className,
|
className,
|
||||||
menuOptions,
|
menuOptions,
|
||||||
|
native = true,
|
||||||
...attrs
|
...attrs
|
||||||
}: SettingDropdownSelectProps<V, E>) => {
|
}: SettingDropdownSelectProps<V, E>) => {
|
||||||
const selectedItem = useMemo(
|
const selectedItem = useMemo(
|
||||||
() => options.find(opt => opt.value === value),
|
() => options.find(opt => opt.value === value),
|
||||||
[options, value]
|
[options, value]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (native) {
|
||||||
|
return (
|
||||||
|
<NativeSettingDropdownSelect
|
||||||
|
options={options}
|
||||||
|
value={value}
|
||||||
|
emitValue={emitValue as any}
|
||||||
|
onChange={onChange}
|
||||||
|
{...attrs}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MobileMenu
|
<MobileMenu
|
||||||
items={options.map(opt => (
|
items={options.map(opt => (
|
||||||
@@ -76,3 +93,59 @@ export const SettingDropdownSelect = <
|
|||||||
</MobileMenu>
|
</MobileMenu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const NativeSettingDropdownSelect = <
|
||||||
|
V extends string = string,
|
||||||
|
E extends boolean | undefined = true,
|
||||||
|
>({
|
||||||
|
options = [],
|
||||||
|
value,
|
||||||
|
emitValue = true,
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
...attrs
|
||||||
|
}: Omit<SettingDropdownSelectProps<V, E>, 'native'>) => {
|
||||||
|
const nativeSelectRef = useRef<HTMLSelectElement>(null);
|
||||||
|
const selectedItem = useMemo(
|
||||||
|
() => options.find(opt => opt.value === value),
|
||||||
|
[options, value]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
const opt = options.find(opt => opt.value === value);
|
||||||
|
if (emitValue) {
|
||||||
|
onChange?.(e.target.value as any);
|
||||||
|
} else {
|
||||||
|
onChange?.(opt as any);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[emitValue, onChange, options]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-testid="dropdown-select-trigger"
|
||||||
|
className={clsx(styles.root, className)}
|
||||||
|
{...attrs}
|
||||||
|
>
|
||||||
|
<span className={styles.label}>{selectedItem?.label ?? ''}</span>
|
||||||
|
|
||||||
|
<ArrowDownSmallIcon className={styles.icon} />
|
||||||
|
<select
|
||||||
|
className={styles.nativeSelect}
|
||||||
|
ref={nativeSelectRef}
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
data-testid="native-dropdown-select-trigger"
|
||||||
|
>
|
||||||
|
{options.map(opt => (
|
||||||
|
<option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -13,48 +13,15 @@ test('can open settings', async ({ page }) => {
|
|||||||
|
|
||||||
test('can change theme', async ({ page }) => {
|
test('can change theme', async ({ page }) => {
|
||||||
await openSettings(page);
|
await openSettings(page);
|
||||||
await page
|
const select = page
|
||||||
.getByTestId('setting-row')
|
.getByTestId('setting-row')
|
||||||
.filter({
|
.filter({
|
||||||
hasText: 'Color mode',
|
hasText: 'Color mode',
|
||||||
})
|
})
|
||||||
.getByTestId('dropdown-select-trigger')
|
.getByTestId('native-dropdown-select-trigger');
|
||||||
.click();
|
|
||||||
|
|
||||||
await expect(
|
await select.selectOption('light');
|
||||||
page.getByRole('dialog').filter({
|
await select.selectOption('dark');
|
||||||
has: page.getByRole('menuitem', { name: 'Light' }),
|
|
||||||
})
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await page.getByRole('menuitem', { name: 'Dark' }).click();
|
|
||||||
|
|
||||||
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
|
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can close change theme popover by clicking outside', async ({ page }) => {
|
|
||||||
await openSettings(page);
|
|
||||||
await page
|
|
||||||
.getByTestId('setting-row')
|
|
||||||
.filter({
|
|
||||||
hasText: 'Color mode',
|
|
||||||
})
|
|
||||||
.getByTestId('dropdown-select-trigger')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
const themePopover = page.getByRole('dialog').filter({
|
|
||||||
has: page.getByRole('menuitem', { name: 'Light' }),
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(themePopover).toBeVisible();
|
|
||||||
|
|
||||||
// get a mouse position that is 10px offset to the top of theme popover
|
|
||||||
// and click
|
|
||||||
const mousePosition = await themePopover.boundingBox();
|
|
||||||
if (!mousePosition) {
|
|
||||||
throw new Error('theme popover is not visible');
|
|
||||||
}
|
|
||||||
await page.mouse.click(mousePosition.x, mousePosition.y - 10);
|
|
||||||
|
|
||||||
await expect(themePopover).not.toBeVisible();
|
|
||||||
});
|
|
||||||
|
|||||||
Reference in New Issue
Block a user