antd-solid 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.cjs +36 -0
- package/.prettierrc +11 -0
- package/.vscode/settings.json +13 -0
- package/README.md +11 -0
- package/docs/.vitepress/components/Code.vue +59 -0
- package/docs/.vitepress/config.ts +49 -0
- package/docs/.vitepress/theme/index.css +15 -0
- package/docs/.vitepress/theme/index.ts +13 -0
- package/docs/components/Button.tsx +20 -0
- package/docs/components/Table.tsx +34 -0
- package/docs/components/button.md +23 -0
- package/docs/components/table.md +23 -0
- package/docs/index.md +28 -0
- package/package.json +62 -0
- package/rollup.config.js +25 -0
- package/src/Button.css +14 -0
- package/src/Button.tsx +86 -0
- package/src/ColorPicker.tsx +66 -0
- package/src/DatePicker.tsx +12 -0
- package/src/Form.tsx +98 -0
- package/src/Image.tsx +29 -0
- package/src/Input.tsx +110 -0
- package/src/InputNumber.test.tsx +46 -0
- package/src/InputNumber.tsx +119 -0
- package/src/Modal.tsx +168 -0
- package/src/Popconfirm.tsx +73 -0
- package/src/Popover.tsx +30 -0
- package/src/Progress.tsx +4 -0
- package/src/Radio.tsx +132 -0
- package/src/Result.tsx +38 -0
- package/src/Select.tsx +6 -0
- package/src/Skeleton.tsx +14 -0
- package/src/Spin.tsx +23 -0
- package/src/Switch.tsx +34 -0
- package/src/Table.tsx +46 -0
- package/src/Tabs.tsx +88 -0
- package/src/Timeline.tsx +33 -0
- package/src/Tooltip.tsx +209 -0
- package/src/Tree.tsx +246 -0
- package/src/Upload.tsx +10 -0
- package/src/hooks/createControllableValue.ts +65 -0
- package/src/hooks/createUpdateEffect.ts +16 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useClickAway.ts +18 -0
- package/src/hooks/useSize.ts +26 -0
- package/src/index.css +21 -0
- package/src/index.ts +37 -0
- package/src/utils/ReactToSolid.tsx +38 -0
- package/src/utils/SolidToReact.tsx +27 -0
- package/src/utils/array.ts +21 -0
- package/src/utils/component.tsx +85 -0
- package/src/utils/solid.ts +48 -0
- package/tsconfig.json +23 -0
- package/unocss.config.ts +92 -0
package/src/Radio.tsx
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Component,
|
|
3
|
+
type JSXElement,
|
|
4
|
+
type ParentProps,
|
|
5
|
+
type JSX,
|
|
6
|
+
untrack,
|
|
7
|
+
For,
|
|
8
|
+
createSelector,
|
|
9
|
+
mergeProps,
|
|
10
|
+
} from 'solid-js'
|
|
11
|
+
import { Dynamic } from 'solid-js/web'
|
|
12
|
+
import cs from 'classnames'
|
|
13
|
+
import createControllableValue from './hooks/createControllableValue'
|
|
14
|
+
|
|
15
|
+
export interface RadioProps extends ParentProps {
|
|
16
|
+
defaultChecked?: boolean
|
|
17
|
+
checked?: boolean
|
|
18
|
+
/**
|
|
19
|
+
* input 的 value
|
|
20
|
+
*/
|
|
21
|
+
value?: string
|
|
22
|
+
onChange?: JSX.ChangeEventHandler<HTMLInputElement, Event>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RadioGroupProps {
|
|
26
|
+
defaultValue?: string
|
|
27
|
+
value?: string
|
|
28
|
+
onChange?: JSX.ChangeEventHandler<HTMLInputElement, Event>
|
|
29
|
+
optionType?: 'default' | 'button'
|
|
30
|
+
options: Array<{ label: JSXElement; value: string; disabled?: boolean }>
|
|
31
|
+
block?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const Radio: Component<RadioProps> & {
|
|
35
|
+
Group: Component<RadioGroupProps>
|
|
36
|
+
Button: Component<RadioProps>
|
|
37
|
+
} = props => {
|
|
38
|
+
const [checked, setChecked] = createControllableValue(props, {
|
|
39
|
+
defaultValue: false,
|
|
40
|
+
defaultValuePropName: 'defaultChecked',
|
|
41
|
+
valuePropName: 'checked',
|
|
42
|
+
trigger: undefined,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<label class="ant-inline-flex ant-gap-4px">
|
|
47
|
+
<input
|
|
48
|
+
checked={checked()}
|
|
49
|
+
value={props.value ?? ''}
|
|
50
|
+
type="radio"
|
|
51
|
+
onInput={e => {
|
|
52
|
+
setChecked(e.target.checked)
|
|
53
|
+
untrack(() => props.onChange?.(e))
|
|
54
|
+
}}
|
|
55
|
+
/>
|
|
56
|
+
{props.children}
|
|
57
|
+
</label>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
Radio.Button = props => {
|
|
62
|
+
const [checked, setChecked] = createControllableValue(props, {
|
|
63
|
+
defaultValue: false,
|
|
64
|
+
defaultValuePropName: 'defaultChecked',
|
|
65
|
+
valuePropName: 'checked',
|
|
66
|
+
trigger: undefined,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<label
|
|
71
|
+
class={cs(
|
|
72
|
+
'ant-px-15px ant-[border:1px_solid_rgb(217,217,217)] first:ant-rounded-l-6px last:ant-rounded-r-6px ant-h-32px ant-inline-flex ant-items-center hover:ant-text-[var(--primary-color)] not[:last-child]:ant-border-r-transparent ant-cursor-pointer ant-flex-grow ant-justify-center',
|
|
73
|
+
checked() &&
|
|
74
|
+
'ant-text-[var(--primary-color)] ant-[border:1px_solid_var(--primary-color)] !ant-border-r-[var(--primary-color)]',
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
<input
|
|
78
|
+
class="ant-w-0 ant-h-0"
|
|
79
|
+
checked={checked()}
|
|
80
|
+
value={props.value ?? ''}
|
|
81
|
+
type="radio"
|
|
82
|
+
onInput={e => {
|
|
83
|
+
setChecked(e.target.checked)
|
|
84
|
+
untrack(() => props.onChange?.(e))
|
|
85
|
+
}}
|
|
86
|
+
/>
|
|
87
|
+
{props.children}
|
|
88
|
+
</label>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
Radio.Group = _props => {
|
|
93
|
+
const props = mergeProps(
|
|
94
|
+
{
|
|
95
|
+
optionType: 'default',
|
|
96
|
+
} as RadioGroupProps,
|
|
97
|
+
_props,
|
|
98
|
+
)
|
|
99
|
+
const [value, setValue] = createControllableValue<string>(props, {
|
|
100
|
+
trigger: undefined,
|
|
101
|
+
})
|
|
102
|
+
const isChecked = createSelector(value)
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div
|
|
106
|
+
class={cs(
|
|
107
|
+
props.block ? 'ant-flex' : 'ant-inline-flex',
|
|
108
|
+
props.optionType === 'default' ? 'ant-gap-8px' : '',
|
|
109
|
+
)}
|
|
110
|
+
>
|
|
111
|
+
<For each={props.options}>
|
|
112
|
+
{option => (
|
|
113
|
+
<Dynamic
|
|
114
|
+
component={props.optionType === 'default' ? Radio : Radio.Button}
|
|
115
|
+
checked={isChecked(option.value)}
|
|
116
|
+
value={option.value}
|
|
117
|
+
onChange={
|
|
118
|
+
(e => {
|
|
119
|
+
setValue(option.value)
|
|
120
|
+
untrack(() => props.onChange?.(e))
|
|
121
|
+
}) as JSX.ChangeEventHandler<HTMLInputElement, Event>
|
|
122
|
+
}
|
|
123
|
+
>
|
|
124
|
+
{option.label}
|
|
125
|
+
</Dynamic>
|
|
126
|
+
)}
|
|
127
|
+
</For>
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export default Radio
|
package/src/Result.tsx
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type JSXElement, type Component, type ParentProps } from 'solid-js'
|
|
2
|
+
import cs from 'classnames'
|
|
3
|
+
|
|
4
|
+
type ResultStatusType = 'success' | 'error' | 'info' | 'warning'
|
|
5
|
+
|
|
6
|
+
export interface ResultProps extends ParentProps {
|
|
7
|
+
status?: ResultStatusType
|
|
8
|
+
title?: JSXElement
|
|
9
|
+
subTitle?: JSXElement
|
|
10
|
+
extra?: JSXElement
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const statusIconMap = {
|
|
14
|
+
success: 'ant-text-#52c41a i-ant-design:check-circle-filled',
|
|
15
|
+
info: 'ant-text-[var(--primary-color)] i-ant-design:exclamation-circle-filled',
|
|
16
|
+
warning: 'ant-text-#faad14 i-ant-design:warning-filled',
|
|
17
|
+
error: 'ant-text-#ff4d4f i-ant-design:close-circle-filled',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const Result: Component<ResultProps> = props => {
|
|
21
|
+
return (
|
|
22
|
+
<div class="ant-text-center ant-px-32px ant-py-48px">
|
|
23
|
+
<div class="ant-mb-24px">
|
|
24
|
+
<span class={cs(statusIconMap[props.status!], 'ant-text-72px')} />
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="ant-my-8px ant-text-[rgba(0,0,0,.88)] ant-text-24px">{props.title}</div>
|
|
28
|
+
|
|
29
|
+
<div class="ant-text-[rgba(0,0,0,.45)] ant-text-14px">{props.subTitle}</div>
|
|
30
|
+
|
|
31
|
+
<div class="ant-mt-24px">{props.extra}</div>
|
|
32
|
+
|
|
33
|
+
<div class="ant-mt-24px">{props.children}</div>
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default Result
|
package/src/Select.tsx
ADDED
package/src/Skeleton.tsx
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Skeleton as SkeletonAntd } from 'antd'
|
|
2
|
+
import { reactToSolidComponent, replaceClassName } from './utils/component'
|
|
3
|
+
|
|
4
|
+
const _Skeleton = replaceClassName(reactToSolidComponent(SkeletonAntd))
|
|
5
|
+
|
|
6
|
+
const Image = replaceClassName(reactToSolidComponent(SkeletonAntd.Image))
|
|
7
|
+
|
|
8
|
+
const Skeleton = _Skeleton as typeof _Skeleton & {
|
|
9
|
+
Image: typeof Image
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
Skeleton.Image = Image
|
|
13
|
+
|
|
14
|
+
export default Skeleton
|
package/src/Spin.tsx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Show, type Component, type ParentProps } from 'solid-js'
|
|
2
|
+
|
|
3
|
+
interface SpinProps extends ParentProps {
|
|
4
|
+
/**
|
|
5
|
+
* 是否为加载中状态
|
|
6
|
+
*/
|
|
7
|
+
spinning?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const Spin: Component<SpinProps> = props => {
|
|
11
|
+
return (
|
|
12
|
+
<div class="ant-relative ant-min-h-32px">
|
|
13
|
+
{props.children}
|
|
14
|
+
<Show when={props.spinning}>
|
|
15
|
+
<div class="ant-absolute ant-inset-0 ant-flex ant-items-center ant-justify-center ant-bg-[rgba(255,255,255,.5)]">
|
|
16
|
+
<span class="i-ant-design:loading keyframes-spin ant-[animation:spin_1s_linear_infinite] ant-text-32px ant-text-[var(--primary-color)]" />
|
|
17
|
+
</div>
|
|
18
|
+
</Show>
|
|
19
|
+
</div>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default Spin
|
package/src/Switch.tsx
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type Component } from 'solid-js'
|
|
2
|
+
import createControllableValue from './hooks/createControllableValue'
|
|
3
|
+
import cs from 'classnames'
|
|
4
|
+
|
|
5
|
+
export interface SwitchProps {
|
|
6
|
+
defaultChecked?: boolean
|
|
7
|
+
checked?: boolean
|
|
8
|
+
onChange?: (checked: boolean) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const Switch: Component<SwitchProps> = props => {
|
|
12
|
+
const [checked, setChecked] = createControllableValue(props, {
|
|
13
|
+
defaultValuePropName: 'defaultChecked',
|
|
14
|
+
valuePropName: 'checked',
|
|
15
|
+
})
|
|
16
|
+
return (
|
|
17
|
+
<button
|
|
18
|
+
class={cs(
|
|
19
|
+
'ant-w-44px ant-h-22px ant-rounded-100px ant-relative',
|
|
20
|
+
checked() ? 'ant-bg-[var(--primary-color)]' : 'ant-bg-[rgba(0,0,0,0.45)]',
|
|
21
|
+
)}
|
|
22
|
+
onClick={() => setChecked(c => !c)}
|
|
23
|
+
>
|
|
24
|
+
<div
|
|
25
|
+
class={cs(
|
|
26
|
+
'ant-w-18px ant-h-18px ant-rounded-50% ant-bg-white ant-absolute ant-top-1/2 -ant-translate-y-1/2 ant-transition-left',
|
|
27
|
+
checked() ? 'ant-left-[calc(100%-20px)]' : 'ant-left-2px',
|
|
28
|
+
)}
|
|
29
|
+
/>
|
|
30
|
+
</button>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default Switch
|
package/src/Table.tsx
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type JSXElement, For } from 'solid-js'
|
|
2
|
+
|
|
3
|
+
export interface Column<R extends {}> {
|
|
4
|
+
title: JSXElement
|
|
5
|
+
render: (row: R) => JSXElement
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface TableProps<R extends {}> {
|
|
9
|
+
columns: Array<Column<R>>
|
|
10
|
+
dataSource: R[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const Table = <R extends {}>(props: TableProps<R>) => {
|
|
14
|
+
return (
|
|
15
|
+
<table class="ant-w-full">
|
|
16
|
+
<thead>
|
|
17
|
+
<tr>
|
|
18
|
+
<For each={props.columns}>
|
|
19
|
+
{item => (
|
|
20
|
+
<th class="ant-p-16px ant-bg-[var(--light-bg-color)] ant-font-bold ant-[border-bottom:1px_solid_var(--secondary-border-color)] ant-text-left">
|
|
21
|
+
{item.title}
|
|
22
|
+
</th>
|
|
23
|
+
)}
|
|
24
|
+
</For>
|
|
25
|
+
</tr>
|
|
26
|
+
</thead>
|
|
27
|
+
<tbody>
|
|
28
|
+
<For each={props.dataSource}>
|
|
29
|
+
{row => (
|
|
30
|
+
<tr class="hover:ant-bg-[var(--light-bg-color)]">
|
|
31
|
+
<For each={props.columns}>
|
|
32
|
+
{item => (
|
|
33
|
+
<td class="ant-p-16px ant-[border-bottom:1px_solid_var(--secondary-border-color)]">
|
|
34
|
+
{item.render(row)}
|
|
35
|
+
</td>
|
|
36
|
+
)}
|
|
37
|
+
</For>
|
|
38
|
+
</tr>
|
|
39
|
+
)}
|
|
40
|
+
</For>
|
|
41
|
+
</tbody>
|
|
42
|
+
</table>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default Table
|
package/src/Tabs.tsx
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Component,
|
|
3
|
+
For,
|
|
4
|
+
type JSXElement,
|
|
5
|
+
createSelector,
|
|
6
|
+
createSignal,
|
|
7
|
+
onMount,
|
|
8
|
+
untrack,
|
|
9
|
+
type JSX,
|
|
10
|
+
Show,
|
|
11
|
+
} from 'solid-js'
|
|
12
|
+
import cs from 'classnames'
|
|
13
|
+
import { isNil } from 'lodash-es'
|
|
14
|
+
|
|
15
|
+
export interface Tab {
|
|
16
|
+
key: string
|
|
17
|
+
label: JSXElement
|
|
18
|
+
children?: JSXElement
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TabsProps {
|
|
22
|
+
class?: string
|
|
23
|
+
navWrapClass?: string
|
|
24
|
+
navItemClass?: string
|
|
25
|
+
items: Tab[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const Tabs: Component<TabsProps> = props => {
|
|
29
|
+
const [selectedItem, setSelectedItem] = createSignal<Tab | undefined>(untrack(() => props.items[0]))
|
|
30
|
+
const isSelectedItem = createSelector(() => selectedItem()?.key)
|
|
31
|
+
const [selectedBarStyle, setSelectedBarStyle] = createSignal<JSX.CSSProperties>({
|
|
32
|
+
left: '0px',
|
|
33
|
+
width: '0px',
|
|
34
|
+
})
|
|
35
|
+
const updateSelectedBarStyle = (el: HTMLElement) => {
|
|
36
|
+
setSelectedBarStyle({
|
|
37
|
+
left: `${el.offsetLeft}px`,
|
|
38
|
+
width: `${el.clientWidth}px`,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let navWrap: HTMLDivElement
|
|
43
|
+
onMount(() => {
|
|
44
|
+
updateSelectedBarStyle(navWrap.children[0] as HTMLElement)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div class={cs(props.class, 'ant-grid ant-[grid-template-rows:auto_1fr]')}>
|
|
49
|
+
<div
|
|
50
|
+
ref={navWrap!}
|
|
51
|
+
class={cs(
|
|
52
|
+
'ant-flex ant-gap-32px ant-[border-bottom:solid_1px_rgba(5,5,5,0.1)] ant-relative',
|
|
53
|
+
props.navWrapClass,
|
|
54
|
+
)}
|
|
55
|
+
>
|
|
56
|
+
<For each={props.items}>
|
|
57
|
+
{item => (
|
|
58
|
+
<div
|
|
59
|
+
class={cs(
|
|
60
|
+
'ant-py-12px ant-cursor-pointer',
|
|
61
|
+
props.navItemClass,
|
|
62
|
+
isSelectedItem(item.key) && 'ant-text-[var(--primary-color)]',
|
|
63
|
+
)}
|
|
64
|
+
onClick={e => {
|
|
65
|
+
setSelectedItem(item)
|
|
66
|
+
updateSelectedBarStyle(e.currentTarget)
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
{item.label}
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
</For>
|
|
73
|
+
|
|
74
|
+
<div
|
|
75
|
+
role={'selected-bar' as any}
|
|
76
|
+
class="ant-absolute ant-bottom-0 ant-bg-[var(--primary-color)] ant-h-2px ant-transition-left"
|
|
77
|
+
style={selectedBarStyle()}
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<Show when={!isNil(selectedItem()?.children)}>
|
|
82
|
+
<div class="ant-px-12px ant-py-16px ant-overflow-auto">{selectedItem()?.children}</div>
|
|
83
|
+
</Show>
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default Tabs
|
package/src/Timeline.tsx
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type Accessor, type Component, For, type JSXElement } from 'solid-js'
|
|
2
|
+
import { type TimelineItemProps as TimelineItemAntdProps } from 'antd'
|
|
3
|
+
|
|
4
|
+
interface TimelineItemProps extends Omit<TimelineItemAntdProps, 'children' | 'dot' | 'label'> {
|
|
5
|
+
dot?: JSXElement
|
|
6
|
+
label?: JSXElement
|
|
7
|
+
children?: Accessor<JSXElement>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface TimelineProps {
|
|
11
|
+
class?: string
|
|
12
|
+
items: TimelineItemProps[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const Timeline: Component<TimelineProps> = props => {
|
|
16
|
+
return (
|
|
17
|
+
<div class="ant-flex ant-flex-col ant-gap-[16px]">
|
|
18
|
+
<For each={props.items}>
|
|
19
|
+
{(item, i) => (
|
|
20
|
+
<div class="ant-flex ant-relative">
|
|
21
|
+
{i() !== props.items.length - 1 && (
|
|
22
|
+
<div class="ant-absolute ant-top-[8px] ant-bottom-[-24px] ant-left-[4px] ant-w-[2px] ant-bg-[rgba(5,5,5,.06)]" />
|
|
23
|
+
)}
|
|
24
|
+
<div class="ant-w-[10px] ant-h-[10px] ant-border-solid ant-border-width-[3px] ant-border-[var(--primary-color)] ant-bg-white ant-rounded-[50%] ant-mt-[8px]" />
|
|
25
|
+
<div class="ant-ml-[8px]">{item.children?.()}</div>
|
|
26
|
+
</div>
|
|
27
|
+
)}
|
|
28
|
+
</For>
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default Timeline
|
package/src/Tooltip.tsx
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { compact } from 'lodash-es'
|
|
2
|
+
import {
|
|
3
|
+
type Component,
|
|
4
|
+
type JSXElement,
|
|
5
|
+
children,
|
|
6
|
+
createEffect,
|
|
7
|
+
Show,
|
|
8
|
+
mergeProps,
|
|
9
|
+
onCleanup,
|
|
10
|
+
createMemo,
|
|
11
|
+
} from 'solid-js'
|
|
12
|
+
import { Portal } from 'solid-js/web'
|
|
13
|
+
import cs from 'classnames'
|
|
14
|
+
import createControllableValue from './hooks/createControllableValue'
|
|
15
|
+
import { useClickAway } from './hooks'
|
|
16
|
+
|
|
17
|
+
type ActionType = 'hover' | 'focus' | 'click' | 'contextMenu'
|
|
18
|
+
type TooltipPlacement =
|
|
19
|
+
| 'top'
|
|
20
|
+
| 'left'
|
|
21
|
+
| 'right'
|
|
22
|
+
| 'bottom'
|
|
23
|
+
| 'topLeft'
|
|
24
|
+
| 'topRight'
|
|
25
|
+
| 'bottomLeft'
|
|
26
|
+
| 'bottomRight'
|
|
27
|
+
| 'leftTop'
|
|
28
|
+
| 'leftBottom'
|
|
29
|
+
| 'rightTop'
|
|
30
|
+
| 'rightBottom'
|
|
31
|
+
|
|
32
|
+
export interface TooltipProps {
|
|
33
|
+
/**
|
|
34
|
+
* 默认: hover
|
|
35
|
+
*/
|
|
36
|
+
trigger?: ActionType
|
|
37
|
+
/**
|
|
38
|
+
* 默认: top
|
|
39
|
+
*/
|
|
40
|
+
placement?: TooltipPlacement
|
|
41
|
+
content?: JSXElement | ((close: () => void) => JSXElement)
|
|
42
|
+
children?: JSXElement
|
|
43
|
+
open?: boolean
|
|
44
|
+
onOpenChange?: (open: boolean) => void
|
|
45
|
+
/**
|
|
46
|
+
* 默认: dark
|
|
47
|
+
*/
|
|
48
|
+
mode?: 'dark' | 'light'
|
|
49
|
+
/**
|
|
50
|
+
* 默认: true
|
|
51
|
+
*/
|
|
52
|
+
arrow?: boolean | { pointAtCenter: boolean }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const Content: Component<{
|
|
56
|
+
content: TooltipProps['content']
|
|
57
|
+
close: () => void
|
|
58
|
+
}> = props => {
|
|
59
|
+
return (
|
|
60
|
+
<Show when={typeof props.content === 'function'} fallback={props.content as JSXElement}>
|
|
61
|
+
{typeof props.content === 'function' && props.content(props.close)}
|
|
62
|
+
</Show>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const Tooltip: Component<TooltipProps> = _props => {
|
|
67
|
+
const props = mergeProps(
|
|
68
|
+
{
|
|
69
|
+
trigger: 'hover',
|
|
70
|
+
placement: 'top',
|
|
71
|
+
mode: 'dark',
|
|
72
|
+
arrow: true,
|
|
73
|
+
} as TooltipProps,
|
|
74
|
+
_props,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
const resolvedChildren = children(() => _props.children)
|
|
78
|
+
let contentWrap: HTMLDivElement
|
|
79
|
+
let arrow: HTMLDivElement
|
|
80
|
+
const [open, setOpen] = createControllableValue(_props, {
|
|
81
|
+
defaultValue: false,
|
|
82
|
+
valuePropName: 'open',
|
|
83
|
+
trigger: 'onOpenChange',
|
|
84
|
+
})
|
|
85
|
+
const reverseOpen = () => setOpen(v => !v)
|
|
86
|
+
|
|
87
|
+
createEffect(() => {
|
|
88
|
+
const _children = resolvedChildren() as Element
|
|
89
|
+
switch (props.trigger) {
|
|
90
|
+
case 'hover':
|
|
91
|
+
_children.addEventListener('mouseenter', reverseOpen)
|
|
92
|
+
onCleanup(() => {
|
|
93
|
+
_children.removeEventListener('mouseenter', reverseOpen)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
_children.addEventListener('mouseleave', reverseOpen)
|
|
97
|
+
onCleanup(() => {
|
|
98
|
+
_children.removeEventListener('mouseleave', reverseOpen)
|
|
99
|
+
})
|
|
100
|
+
break
|
|
101
|
+
case 'click':
|
|
102
|
+
_children.addEventListener('click', reverseOpen)
|
|
103
|
+
onCleanup(() => {
|
|
104
|
+
_children.removeEventListener('click', reverseOpen)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
useClickAway(
|
|
108
|
+
() => setOpen(false),
|
|
109
|
+
() => compact([contentWrap, _children]),
|
|
110
|
+
)
|
|
111
|
+
break
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
createEffect(() => {
|
|
116
|
+
if (open()) {
|
|
117
|
+
const _children = resolvedChildren() as Element
|
|
118
|
+
const childrenRect = _children.getBoundingClientRect()
|
|
119
|
+
const pointAtCenter = typeof props.arrow === 'object' ? props.arrow.pointAtCenter : false
|
|
120
|
+
const arrowOffset = 8
|
|
121
|
+
|
|
122
|
+
switch (props.placement) {
|
|
123
|
+
case 'top':
|
|
124
|
+
contentWrap.style.top = `${childrenRect.top}px`
|
|
125
|
+
contentWrap.style.left = `${childrenRect.left + childrenRect.width / 2}px`
|
|
126
|
+
contentWrap.style.transform = 'translate(-50%, -100%)'
|
|
127
|
+
break
|
|
128
|
+
case 'topRight':
|
|
129
|
+
contentWrap.style.top = `${childrenRect.top}px`
|
|
130
|
+
contentWrap.style.left = `${childrenRect.right}px`
|
|
131
|
+
contentWrap.style.transform = 'translate(-100%, -100%)'
|
|
132
|
+
if (arrow) arrow.style.right = `${arrowOffset}px`
|
|
133
|
+
break
|
|
134
|
+
case 'bottom':
|
|
135
|
+
contentWrap.style.top = `${childrenRect.top + childrenRect.height}px`
|
|
136
|
+
contentWrap.style.left = `${childrenRect.left + childrenRect.width / 2}px`
|
|
137
|
+
contentWrap.style.transform = 'translate(-50%, 0)'
|
|
138
|
+
break
|
|
139
|
+
case 'bottomLeft':
|
|
140
|
+
contentWrap.style.top = `${childrenRect.top + childrenRect.height}px`
|
|
141
|
+
contentWrap.style.left = `${childrenRect.left}px`
|
|
142
|
+
if (arrow) arrow.style.left = `${arrowOffset}px`
|
|
143
|
+
break
|
|
144
|
+
case 'bottomRight':
|
|
145
|
+
contentWrap.style.top = `${childrenRect.top + childrenRect.height}px`
|
|
146
|
+
contentWrap.style.left = `${childrenRect.right + (pointAtCenter ? arrowOffset : 0)}px`
|
|
147
|
+
contentWrap.style.transform = 'translate(-100%, 0)'
|
|
148
|
+
if (arrow) arrow.style.right = `${arrowOffset}px`
|
|
149
|
+
break
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const direction = createMemo(() => {
|
|
155
|
+
if (props.placement?.startsWith('top')) return 'top'
|
|
156
|
+
if (props.placement?.startsWith('bottom')) return 'bottom'
|
|
157
|
+
if (props.placement?.startsWith('left')) return 'left'
|
|
158
|
+
if (props.placement?.startsWith('right')) return 'right'
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<>
|
|
163
|
+
{resolvedChildren}
|
|
164
|
+
|
|
165
|
+
<Show when={open()}>
|
|
166
|
+
<Portal>
|
|
167
|
+
{/* Portal 存在缺陷,onClick 依然会沿着 solid 的组件树向上传播,因此需要 stopPropagation */}
|
|
168
|
+
<div
|
|
169
|
+
ref={contentWrap!}
|
|
170
|
+
class={cs(
|
|
171
|
+
'ant-z-1000 ant-fixed after:ant-content-empty',
|
|
172
|
+
props.arrow ? '[--padding:8px]' : '[--padding:4px]',
|
|
173
|
+
direction() === 'top' && 'ant-pb-[var(--padding)]',
|
|
174
|
+
direction() === 'bottom' && 'ant-pt-[var(--padding)]',
|
|
175
|
+
)}
|
|
176
|
+
onClick={e => {
|
|
177
|
+
e.stopPropagation()
|
|
178
|
+
}}
|
|
179
|
+
>
|
|
180
|
+
<div
|
|
181
|
+
class={cs(
|
|
182
|
+
'ant-p-12px ant-rounded-8px ant-[box-shadow:0_6px_16px_0_rgba(0,0,0,0.08),0_3px_6px_-4px_rgba(0,0,0,0.12),0_9px_28px_8px_rgba(0,0,0,0.05)]',
|
|
183
|
+
props.mode === 'dark' ? 'ant-bg-[rgba(0,0,0,0.85)] ant-text-white' : 'ant-bg-white',
|
|
184
|
+
)}
|
|
185
|
+
>
|
|
186
|
+
<Content content={props.content} close={() => setOpen(false)} />
|
|
187
|
+
</div>
|
|
188
|
+
<Show when={props.arrow}>
|
|
189
|
+
<div
|
|
190
|
+
ref={arrow!}
|
|
191
|
+
class={cs(
|
|
192
|
+
'ant-w-8px ant-h-8px ant-rotate-45 ant-absolute',
|
|
193
|
+
props.mode === 'dark' ? 'ant-bg-[rgba(0,0,0,0.85)]' : 'ant-bg-white',
|
|
194
|
+
direction() === 'top' &&
|
|
195
|
+
'ant-bottom-0 -ant-translate-x-1/2 -ant-translate-y-1/2 ant-[filter:drop-shadow(3px_2px_2px_rgba(0,0,0,0.08))]',
|
|
196
|
+
direction() === 'bottom' &&
|
|
197
|
+
'ant-top-0 -ant-translate-x-1/2 ant-translate-y-1/2 ant-[filter:drop-shadow(-3px_-2px_2px_rgba(0,0,0,0.08))]',
|
|
198
|
+
(props.placement === 'top' || props.placement === 'bottom') && 'left-1/2',
|
|
199
|
+
)}
|
|
200
|
+
/>
|
|
201
|
+
</Show>
|
|
202
|
+
</div>
|
|
203
|
+
</Portal>
|
|
204
|
+
</Show>
|
|
205
|
+
</>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export default Tooltip
|