Skip to content

Commit

Permalink
feat(mobile): use native select for mobile setting
Browse files Browse the repository at this point in the history
  • Loading branch information
CatsJuice committed Dec 22, 2024
1 parent 3a82da0 commit 0b209bf
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const root = style({
display: 'flex',
alignItems: 'center',
gap: 8,
position: 'relative',
});
export const label = style([
bodyRegular,
Expand All @@ -15,3 +16,11 @@ export const icon = style({
fontSize: 24,
color: cssVarV2('icon/primary'),
});
export const nativeSelect = style({
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
opacity: 0,
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {
type CSSProperties,
type HTMLAttributes,
type ReactNode,
useCallback,
useMemo,
useRef,
} from 'react';

import * as styles from './dropdown-select.css';
Expand All @@ -28,6 +30,7 @@ export interface SettingDropdownSelectProps<
) => void;
emitValue?: E;
menuOptions?: Omit<MenuProps, 'items' | 'children'>;
native?: boolean;
}

export const SettingDropdownSelect = <
Expand All @@ -40,12 +43,26 @@ export const SettingDropdownSelect = <
onChange,
className,
menuOptions,
native = true,
...attrs
}: SettingDropdownSelectProps<V, E>) => {
const selectedItem = useMemo(
() => options.find(opt => opt.value === value),
[options, value]
);

if (native) {
return (
<NativeSettingDropdownSelect
options={options}
value={value}
emitValue={emitValue as any}
onChange={onChange}
{...attrs}
/>
);
}

return (
<MobileMenu
items={options.map(opt => (
Expand Down Expand Up @@ -76,3 +93,59 @@ export const SettingDropdownSelect = <
</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>
);
};
41 changes: 4 additions & 37 deletions tests/affine-mobile/e2e/settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,48 +13,15 @@ test('can open settings', async ({ page }) => {

test('can change theme', async ({ page }) => {
await openSettings(page);
await page
const select = page
.getByTestId('setting-row')
.filter({
hasText: 'Color mode',
})
.getByTestId('dropdown-select-trigger')
.click();
.getByTestId('native-dropdown-select-trigger');

await expect(
page.getByRole('dialog').filter({
has: page.getByRole('menuitem', { name: 'Light' }),
})
).toBeVisible();

await page.getByRole('menuitem', { name: 'Dark' }).click();
await select.selectOption('light');
await select.selectOption('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();
});

0 comments on commit 0b209bf

Please sign in to comment.