Skip to content

Commit

Permalink
refactor(admin/components/button): 采用 popover 代替 dropdown 组件
Browse files Browse the repository at this point in the history
  • Loading branch information
caixw committed Sep 17, 2024
1 parent 3260fe5 commit e101e84
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 65 deletions.
4 changes: 4 additions & 0 deletions admin/src/components/button/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { JSX, mergeProps } from 'solid-js';

import { Props as BaseProps, ButtonType, ClickFunc, defaultProps as defaultBaseProps } from './types';

export type Ref = HTMLButtonElement;

export interface Props extends BaseProps {
/**
* 是否为图标按钮
Expand All @@ -25,6 +27,7 @@ export interface Props extends BaseProps {
accessKey?: string;
autofocus?: boolean;
value?: string;
ref?: { (el: Ref): void; };
}

export const defaultProps: Readonly<Partial<Props>> = {
Expand All @@ -39,6 +42,7 @@ export default function(props: Props) {
props = mergeProps(defaultProps, props);

return <button value={props.value} accessKey={props.accessKey} autofocus={props.autofocus} disabled={props.disabled}
ref={(el) => { if (props.ref) { props.ref(el); }}}
type={props.type} title={props.title} onClick={props.onClick} classList={{
'c--button': true,
'c--icon-container': true,
Expand Down
62 changes: 34 additions & 28 deletions admin/src/components/button/confirm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@
//
// SPDX-License-Identifier: MIT

import { createSignal, JSX, mergeProps, splitProps } from 'solid-js';
import { JSX, mergeProps, onCleanup, onMount, splitProps } from 'solid-js';

import { useApp } from '@/app/context';
import { Corner, handleEvent } from '@/components/base';
import { Dropdown } from '@/components/dropdown';
import Button, { Props as BaseProps, defaultProps as defaultBaseProps } from './button';
import { handleEvent } from '@/components/base';
import Button, { Props as BaseProps, Ref as ButtonRef, defaultProps } from './button';
import { ClickFunc } from './types';

export interface Props extends BaseProps {
Expand All @@ -16,11 +15,6 @@ export interface Props extends BaseProps {
*/
prompt?: JSX.Element;

/**
* 弹出框的位置
*/
pos?: Corner

/**
* 自定义确定按钮上的内容
*/
Expand All @@ -39,37 +33,49 @@ export interface Props extends BaseProps {
onClick: ClickFunc; // 此处重新声明只是为了将可选字段变为必填字段。
}


const defaultProps: Readonly<Partial<Props>> = {
...defaultBaseProps,
pos: 'bottomleft'
};

/**
* 带确认功能的按钮
*/
export default function(props: Props) {
props = mergeProps(defaultProps, props);

const ctx = useApp();
let pop: HTMLDivElement;
let btn: ButtonRef;

const [_, btnProps] = splitProps(props, ['children', 'onClick', 'prompt', 'palette', 'ok', 'cancel', 'pos']);
const [visible, setVisible] = createSignal(false);
const [_, btnProps] = splitProps(props, ['children', 'onClick', 'prompt', 'palette', 'ok', 'cancel']);

const handleClick = (e: MouseEvent) => {
if (!pop.contains(e.target as Node) && !btn.contains(e.target as Node)) {
pop.hidePopover();
}
};
onMount(() => {
document.body.addEventListener('click', handleClick);
});
onCleanup(() => {
document.body.removeEventListener('click', handleClick);
});

const confirm: ClickFunc = (e) => {
handleEvent(props.onClick, e);
setVisible(false);
pop.hidePopover();
};

const activator = <Button onClick={() => setVisible(!visible())} {...btnProps}>{props.children}</Button>;
return <>
<Button ref={(el)=>btn=el} {...btnProps} onClick={() => {
pop.togglePopover();

return <Dropdown class='c--confirm-button-panel'
pos={props.pos} visible={visible()} setVisible={setVisible}
palette={props.palette} activator={activator}>
{props.prompt ?? ctx.t('_i.areYouSure')}
<div class="actions">
<Button palette='secondary' onClick={()=>setVisible(false)}>{ props.cancel ?? ctx.t('_i.cancel') }</Button>
<Button palette='primary' autofocus onClick={confirm}>{ props.ok ?? ctx.t('_i.ok') }</Button>
// TODO: [CSS anchor](https://caniuse.com/?search=anchor) 支持全面的话,可以用 CSS 代替。
const rect = btn.getBoundingClientRect();
pop.style.top = rect.bottom + 'px';
pop.style.left = rect.left + 'px';
}}>{props.children}</Button>
<div popover="manual" ref={el=>pop=el} classList={{'c--confirm-button-panel':true, [`palette--${props.palette}`]:!!props.palette }}>
{props.prompt ?? ctx.t('_i.areYouSure')}
<div class="actions">
<Button palette='secondary' onClick={() => pop.hidePopover()}>{props.cancel ?? ctx.t('_i.cancel')}</Button>
<Button palette='primary' autofocus onClick={confirm}>{props.ok ?? ctx.t('_i.ok')}</Button>
</div>
</div>
</Dropdown>;
</>;
}
6 changes: 5 additions & 1 deletion admin/src/components/button/group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,21 @@ import { JSX, mergeProps } from 'solid-js';

import { Props as BaseProps, defaultProps } from './types';

export type Ref = HTMLFieldSetElement;

export interface Props extends BaseProps {
/**
* 子元素,必须得是 Button 或是 LinkButton 类型。
*/
children: JSX.Element;

ref?: { (el: Ref): void; };
}

export default function (props: Props) {
props = mergeProps(defaultProps, props);

return <fieldset role="group" disabled={props.disabled} classList={{
return <fieldset role="group" ref={(el) => { if (props.ref) { props.ref(el); }}} disabled={props.disabled} classList={{
'c--button-group': true,
'c--button-group-rounded': props.rounded,
[`c--button-group-${props.style}`]: true,
Expand Down
4 changes: 2 additions & 2 deletions admin/src/components/button/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT

export { default as Button } from './button';
export type { Props as ButtonProps } from './button';
export type { Props as ButtonProps, Ref as ButtonRef } from './button';

export { default as LinkButton } from './link';
export type { Props as LinkButtonProps } from './link';
Expand All @@ -12,7 +12,7 @@ export { default as ConfirmButton } from './confirm';
export type { Props as ConfirmButtonProps } from './confirm';

export { default as ButtonGroup } from './group';
export type { Props as ButtonGroupProps } from './group';
export type { Props as ButtonGroupProps, Ref as ButtonGroupRef } from './group';

export { default as SplitButton } from './split';
export type { Props as SplitButtonProps } from './split';
Expand Down
79 changes: 47 additions & 32 deletions admin/src/components/button/split.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@
//
// SPDX-License-Identifier: MIT

import { createSignal, For, JSX, Match, mergeProps, splitProps, Switch } from 'solid-js';
import { For, JSX, Match, mergeProps, onCleanup, splitProps, Switch } from 'solid-js';

import { Corner } from '@/components/base';
import { Dropdown } from '@/components/dropdown';
import { Props as BaseProps, default as Button, defaultProps as defaultBaseProps } from './button';
import { default as Group } from './group';
import { onMount } from 'solid-js';
import { Props as BaseProps, default as Button, defaultProps } from './button';
import { default as Group, Ref as GroupRef } from './group';
import { ClickFunc } from './types';

type Item = { type: 'divider' } | {
Expand All @@ -18,47 +17,63 @@ type Item = { type: 'divider' } | {
};

export interface Props extends BaseProps {
pos?: Corner;
menus: Array<Item>;
}

const defaultProps = {
...defaultBaseProps,
pos: 'bottomleft' as Corner
};

/**
* 带有一个默认操作的一组按钮
*
* 属性中,除了 menus 用于表示所有的子命令外,其它属性都是用于描述默认按钮的属性的。
*/
export default function(props: Props) {
props = mergeProps(defaultProps, props);
let pop: HTMLDivElement;
let group: GroupRef;

const [visible, setVisible] = createSignal(false);
const handleClick = (e: MouseEvent) => {
if (!pop.contains(e.target as Node) && !group.contains(e.target as Node)) {
pop.hidePopover();
}
};
onMount(() => {
document.body.addEventListener('click', handleClick);
});
onCleanup(() => {
document.body.removeEventListener('click', handleClick);
});

const [_, btnProps] = splitProps(props, ['style', 'rounded', 'disabled', 'palette', 'pos']);
const [_, btnProps] = splitProps(props, ['style', 'rounded', 'disabled', 'palette']);

const activator = <Group style={props.style} rounded={props.rounded} disabled={props.disabled}>
const activator = <Group palette={props.palette} ref={el=>group=el} style={props.style} rounded={props.rounded} disabled={props.disabled}>
<Button {...btnProps}>{props.children}</Button>
<Button icon={/*@once*/true} onClick={()=>setVisible(!visible())}>keyboard_arrow_down</Button>
<Button icon={/*@once*/true} onClick={() => {
pop.togglePopover();

// TODO: [CSS anchor](https://caniuse.com/?search=anchor) 支持全面的话,可以用 CSS 代替。
const rect = group.getBoundingClientRect();
pop.style.top = rect.bottom + 'px';
pop.style.left = rect.left + 'px';
}}>keyboard_arrow_down</Button>
</Group>;

return <Dropdown class="c--split-button_content" pos={props.pos} palette={props.palette} activator={activator} visible={visible()} setVisible={setVisible}>
<For each={props.menus}>
{(item)=>(
<Switch>
<Match when={item.type === 'divider'}>
<hr class="text-palette-bg-low" />
</Match>
<Match when={item.type === 'item'}>
<button disabled={(item as any).disabled} class="whitespace-nowrap c--icon-container" onClick={() => {
(item as any).onClick();
setVisible(false);
}}>{(item as any).label}</button>
</Match>
</Switch>
)}
</For>
</Dropdown>;
return <>
{activator}
<div ref={el=>pop=el} popover="manual" classList={{ 'c--split-button_content':true, [`palette--${props.palette}`]:!!props.palette}}>
<For each={props.menus}>
{(item) => (
<Switch>
<Match when={item.type === 'divider'}>
<hr class="text-palette-bg-low" />
</Match>
<Match when={item.type === 'item'}>
<button disabled={(item as any).disabled} class="whitespace-nowrap c--icon-container" onClick={() => {
(item as any).onClick();
pop.hidePopover();
}}>{(item as any).label}</button>
</Match>
</Switch>
)}
</For>
</div>
</>;
}
12 changes: 10 additions & 2 deletions admin/src/components/button/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -132,17 +132,21 @@
/************************* confirm-button ******************************/

.c--confirm-button-panel {
@apply flex flex-col p-4 gap-2 bg-palette-bg text-palette-fg mt-1 rounded-md;
@apply p-4 gap-2 bg-palette-bg text-palette-fg mt-1 rounded-md m-0;

.actions {
@apply flex justify-end gap-2 mt-4;
}
}

.c--confirm-button-panel:popover-open {
@apply flex flex-col;
}

/************************* confirm-button ******************************/

.c--split-button_content {
@apply flex flex-col bg-palette-bg p-1 gap-y-1 rounded-md mt-1 border border-palette-bg-low;
@apply bg-palette-bg p-1 gap-y-1 rounded-md mt-1 border border-palette-bg-low m-0;

button {
@apply py-1 px-2 rounded-md text-left;
Expand All @@ -152,4 +156,8 @@
@apply bg-palette-bg-high text-palette-fg-high;
}
}

.c--split-button_content:popover-open {
@apply flex flex-col;
}
}

0 comments on commit e101e84

Please sign in to comment.