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/Form.tsx
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { set } from 'lodash-es'
|
|
2
|
+
import {
|
|
3
|
+
type JSXElement,
|
|
4
|
+
type Component,
|
|
5
|
+
type JSX,
|
|
6
|
+
mergeProps,
|
|
7
|
+
Show,
|
|
8
|
+
Index,
|
|
9
|
+
createMemo,
|
|
10
|
+
} from 'solid-js'
|
|
11
|
+
import { Dynamic } from 'solid-js/web'
|
|
12
|
+
import { toArray } from './utils/array'
|
|
13
|
+
import cs from 'classnames'
|
|
14
|
+
|
|
15
|
+
export interface FormInstance<T extends {} = {}> {
|
|
16
|
+
validateFields: () => Promise<T>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface FormProps<T extends {} = {}> {
|
|
20
|
+
ref?: (form: FormInstance<T>) => void;
|
|
21
|
+
/**
|
|
22
|
+
* 表单布局
|
|
23
|
+
* 默认: horizontal
|
|
24
|
+
*/
|
|
25
|
+
layout?: 'horizontal' | 'vertical' | 'inline'
|
|
26
|
+
/**
|
|
27
|
+
* 提交按钮
|
|
28
|
+
* @deprecated
|
|
29
|
+
*/
|
|
30
|
+
submit?: (form: FormInstance<T>) => JSXElement
|
|
31
|
+
children: JSXElement
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FormItemComponentProps<T = any> {
|
|
35
|
+
defaultValue?: T
|
|
36
|
+
onChange?: (value: T) => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface FormItemProps {
|
|
40
|
+
class?: string
|
|
41
|
+
style?: JSX.CSSProperties
|
|
42
|
+
required?: boolean
|
|
43
|
+
label?: JSXElement
|
|
44
|
+
name?: string
|
|
45
|
+
initialValue?: any
|
|
46
|
+
component: Component<FormItemComponentProps>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function Form<T extends {} = {}>(_props: FormProps<T>) {
|
|
50
|
+
const props = mergeProps({ layout: 'horizontal' } as FormProps, _props)
|
|
51
|
+
|
|
52
|
+
const resolvedChildren = createMemo(() => {
|
|
53
|
+
return toArray(props.children) as unknown as FormItemProps[]
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const values = Object.fromEntries(
|
|
57
|
+
resolvedChildren().map(child => [child.name, child.initialValue]),
|
|
58
|
+
) as T
|
|
59
|
+
const formInstance: FormInstance<T> = {
|
|
60
|
+
async validateFields() {
|
|
61
|
+
return await Promise.resolve(values)
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
_props.ref?.(formInstance)
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div>
|
|
68
|
+
<Index each={resolvedChildren()}>
|
|
69
|
+
{item => (
|
|
70
|
+
<div class={cs('ant-flex ant-items-center ant-mb-16px', item().class)} style={item().style}>
|
|
71
|
+
<span class="ant-flex-shrink-0 ant-mr-8px">
|
|
72
|
+
<Show when={item().required}>
|
|
73
|
+
<span class='ant-mr-4px ant-text-[var(--error-color)]'>*</span>
|
|
74
|
+
</Show>
|
|
75
|
+
<label>{item().label}</label>
|
|
76
|
+
</span>
|
|
77
|
+
|
|
78
|
+
<Dynamic
|
|
79
|
+
component={item().component}
|
|
80
|
+
defaultValue={item().initialValue}
|
|
81
|
+
onChange={(value: any) => {
|
|
82
|
+
set(values, item().name!, value)
|
|
83
|
+
}}
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
</Index>
|
|
88
|
+
|
|
89
|
+
{props.submit?.(formInstance as FormInstance<T>)}
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
Form.Item = (props: FormItemProps) => props as any
|
|
95
|
+
|
|
96
|
+
Form.createForm = () => {}
|
|
97
|
+
|
|
98
|
+
export default Form
|
package/src/Image.tsx
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Image as ImageAntd } from 'antd'
|
|
2
|
+
import { configProvider, reactToSolidComponent, replaceClassName } from './utils/component'
|
|
3
|
+
import { solidToReact } from './utils/solid'
|
|
4
|
+
import { type JSXElement, createMemo } from 'solid-js'
|
|
5
|
+
import { mapValues } from 'lodash-es'
|
|
6
|
+
|
|
7
|
+
const _Image = replaceClassName(
|
|
8
|
+
reactToSolidComponent(configProvider(ImageAntd), () => (<div class="ant-inline-flex" />) as any),
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
type ImageProps = Omit<Parameters<typeof _Image>[0], 'placeholder'> & {
|
|
12
|
+
placeholder?: JSXElement
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function Image(_props: ImageProps) {
|
|
16
|
+
const props = createMemo(() =>
|
|
17
|
+
mapValues(_props, (value, key) => {
|
|
18
|
+
switch (key) {
|
|
19
|
+
case 'placeholder':
|
|
20
|
+
return solidToReact(value)
|
|
21
|
+
default:
|
|
22
|
+
return value
|
|
23
|
+
}
|
|
24
|
+
}),
|
|
25
|
+
)
|
|
26
|
+
return <_Image {...(props() as any)} />
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default Image
|
package/src/Input.tsx
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { omit } from 'lodash-es'
|
|
2
|
+
import { Show, splitProps } from 'solid-js'
|
|
3
|
+
import type { JSX, JSXElement, Component } from 'solid-js'
|
|
4
|
+
import cs from 'classnames'
|
|
5
|
+
import createControllableValue from './hooks/createControllableValue'
|
|
6
|
+
import { Dynamic } from 'solid-js/web'
|
|
7
|
+
|
|
8
|
+
type CommonInputProps<T extends HTMLInputElement | HTMLTextAreaElement = HTMLInputElement> = JSX.CustomAttributes<T> & {
|
|
9
|
+
textarea?: boolean
|
|
10
|
+
defaultValue?: string | undefined
|
|
11
|
+
value?: string | undefined
|
|
12
|
+
addonBefore?: JSXElement
|
|
13
|
+
addonAfter?: JSXElement
|
|
14
|
+
/**
|
|
15
|
+
* 仅供 InputNumber 使用
|
|
16
|
+
*/
|
|
17
|
+
inputAfter?: JSXElement
|
|
18
|
+
placeholder?: string
|
|
19
|
+
onChange?: JSX.InputEventHandler<T, InputEvent>
|
|
20
|
+
onPressEnter?: JSX.EventHandler<T, KeyboardEvent>
|
|
21
|
+
onKeyDown?: JSX.EventHandler<T, KeyboardEvent>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function CommonInput<T extends HTMLInputElement | HTMLTextAreaElement = HTMLInputElement>(props: CommonInputProps<T> &
|
|
25
|
+
Omit<JSX.InputHTMLAttributes<T>, 'onChange' | 'onInput' | 'onKeyDown'>) {
|
|
26
|
+
const [{ onChange, onPressEnter, onKeyDown }, inputProps] = splitProps(props, [
|
|
27
|
+
'defaultValue',
|
|
28
|
+
'value',
|
|
29
|
+
'class',
|
|
30
|
+
'addonBefore',
|
|
31
|
+
'addonAfter',
|
|
32
|
+
'inputAfter',
|
|
33
|
+
'onChange',
|
|
34
|
+
'onPressEnter',
|
|
35
|
+
'onKeyDown',
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
const [_, controllableProps] = splitProps(props, ['onChange'])
|
|
39
|
+
const [value, setValue] = createControllableValue(controllableProps)
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div class="ant-flex ant-w-full">
|
|
43
|
+
<Show when={props.addonBefore}>
|
|
44
|
+
<div class="ant-shrink-0 ant-flex ant-justify-center ant-items-center ant-px-11px ant-bg-[rgba(0,0,0,.02)] ant-[border:1px_solid_#d9d9d9] ant-border-r-0 ant-rounded-l-6px ant-text-14px">
|
|
45
|
+
{props.addonBefore}
|
|
46
|
+
</div>
|
|
47
|
+
</Show>
|
|
48
|
+
|
|
49
|
+
<div class="ant-w-full ant-relative ant-[--input-after-display:none] hover:ant-[--input-after-display:block] p-hover-child[input]:ant-border-[var(--primary-color)]">
|
|
50
|
+
<Dynamic<Component<JSX.InputHTMLAttributes<HTMLInputElement>>>
|
|
51
|
+
component={
|
|
52
|
+
(props.textarea ? 'textarea' : 'input') as unknown as Component<
|
|
53
|
+
JSX.InputHTMLAttributes<HTMLInputElement>
|
|
54
|
+
>
|
|
55
|
+
}
|
|
56
|
+
{...inputProps as JSX.InputHTMLAttributes<HTMLInputElement>}
|
|
57
|
+
class={cs(
|
|
58
|
+
'ant-w-full ant-py-0 ant-px-11px ant-[outline:none] ant-text-14px ant-rounded-6px ant-[border:1px_solid_#d9d9d9] focus:ant-border-[var(--primary-color)] focus:ant-[box-shadow:0_0_0_2px_rgba(5,145,255,0.1)] ant-py-8px',
|
|
59
|
+
!props.textarea && 'ant-h-32px',
|
|
60
|
+
props.class,
|
|
61
|
+
props.addonBefore && 'ant-rounded-l-0',
|
|
62
|
+
props.addonAfter && 'ant-rounded-r-0',
|
|
63
|
+
)}
|
|
64
|
+
value={value() ?? ''}
|
|
65
|
+
onInput={e => {
|
|
66
|
+
setValue(e.target.value)
|
|
67
|
+
onChange?.(e as any)
|
|
68
|
+
}}
|
|
69
|
+
onKeyDown={e => {
|
|
70
|
+
if (e.key === 'Enter') {
|
|
71
|
+
onPressEnter?.(e as any)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
onKeyDown?.(e as any)
|
|
75
|
+
}}
|
|
76
|
+
/>
|
|
77
|
+
|
|
78
|
+
<Show when={props.inputAfter}>
|
|
79
|
+
<div class="ant-[display:var(--input-after-display)] ant-absolute ant-top-0 ant-bottom-0 ant-right-0 ant-h-[calc(100%-2px)] ant-translate-y-1px -ant-translate-x-1px">
|
|
80
|
+
{props.inputAfter}
|
|
81
|
+
</div>
|
|
82
|
+
</Show>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<Show when={props.addonAfter}>
|
|
86
|
+
<div class="ant-shrink-0 ant-flex ant-justify-center ant-items-center ant-px-11px ant-bg-[rgba(0,0,0,.02)] ant-[border:1px_solid_#d9d9d9] ant-border-l-0 ant-rounded-r-6px ant-text-14px">
|
|
87
|
+
{props.addonAfter}
|
|
88
|
+
</div>
|
|
89
|
+
</Show>
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type InputProps = Omit<CommonInputProps, 'inputAfter' | 'textarea'> &
|
|
95
|
+
Omit<JSX.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'onInput' | 'onKeyDown'>
|
|
96
|
+
|
|
97
|
+
export type TextAreaProps = Omit<CommonInputProps<HTMLTextAreaElement>, 'inputAfter' | 'textarea'> &
|
|
98
|
+
Omit<JSX.TextareaHTMLAttributes<HTMLTextAreaElement>, 'onChange' | 'onInput' | 'onKeyDown'>
|
|
99
|
+
|
|
100
|
+
const Input: Component<InputProps> & {
|
|
101
|
+
TextArea: Component<TextAreaProps>
|
|
102
|
+
} = props => {
|
|
103
|
+
return <CommonInput {...omit(props, ['inputAfter'])} />
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
Input.TextArea = props => {
|
|
107
|
+
return <CommonInput<HTMLTextAreaElement> textarea {...omit(props, ['inputAfter'])} />
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export default Input
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { fireEvent, render } from '@solidjs/testing-library'
|
|
3
|
+
import InputNumber from './InputNumber'
|
|
4
|
+
import '@testing-library/jest-dom'
|
|
5
|
+
|
|
6
|
+
describe('InputNumber component', () => {
|
|
7
|
+
it('onChange', () => {
|
|
8
|
+
const onChange = vi.fn()
|
|
9
|
+
const { getByPlaceholderText } = render(() => (
|
|
10
|
+
<InputNumber placeholder="input-number" onChange={onChange} />
|
|
11
|
+
))
|
|
12
|
+
|
|
13
|
+
const input: HTMLInputElement = getByPlaceholderText('input-number')
|
|
14
|
+
input.value = '123'
|
|
15
|
+
fireEvent.input(input)
|
|
16
|
+
expect(onChange).toHaveBeenLastCalledWith(123)
|
|
17
|
+
|
|
18
|
+
input.value = '1234'
|
|
19
|
+
fireEvent.input(input)
|
|
20
|
+
expect(onChange).toBeCalledTimes(2)
|
|
21
|
+
expect(onChange).toHaveBeenLastCalledWith(1234)
|
|
22
|
+
|
|
23
|
+
input.value = '1234'
|
|
24
|
+
fireEvent.input(input)
|
|
25
|
+
expect(onChange).toBeCalledTimes(2)
|
|
26
|
+
|
|
27
|
+
input.value = '1234.'
|
|
28
|
+
fireEvent.input(input)
|
|
29
|
+
expect(onChange).toBeCalledTimes(2)
|
|
30
|
+
|
|
31
|
+
input.value = '1234.0'
|
|
32
|
+
fireEvent.input(input)
|
|
33
|
+
expect(onChange).toBeCalledTimes(2)
|
|
34
|
+
|
|
35
|
+
input.value = '1234.01'
|
|
36
|
+
fireEvent.input(input)
|
|
37
|
+
expect(onChange).toBeCalledTimes(3)
|
|
38
|
+
|
|
39
|
+
input.value = '123x'
|
|
40
|
+
fireEvent.input(input)
|
|
41
|
+
expect(onChange).toBeCalledTimes(3)
|
|
42
|
+
|
|
43
|
+
fireEvent.blur(input)
|
|
44
|
+
expect(onChange).toBeCalledTimes(3)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { type Component, createEffect, on, splitProps } from 'solid-js'
|
|
2
|
+
import { CommonInput, type InputProps } from './Input'
|
|
3
|
+
import { isNil } from 'lodash-es'
|
|
4
|
+
import createControllableValue from './hooks/createControllableValue'
|
|
5
|
+
import { dispatchEventHandlerUnion } from './utils/solid'
|
|
6
|
+
|
|
7
|
+
export interface InputNumberProps
|
|
8
|
+
extends Omit<InputProps, 'defaultValue' | 'value' | 'onChange' | 'inputAfter' | 'onKeyDown'> {
|
|
9
|
+
defaultValue?: number | null | undefined
|
|
10
|
+
value?: number | null | undefined
|
|
11
|
+
onChange?: (value: number | null) => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const isEmptyValue = (value: number | string | null | undefined) => isNil(value) || value === ''
|
|
15
|
+
|
|
16
|
+
const formatNum = (
|
|
17
|
+
v: number | string | null | undefined,
|
|
18
|
+
prev?: number | null | undefined,
|
|
19
|
+
): number | null => {
|
|
20
|
+
if (isEmptyValue(v)) {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const num = Number(v)
|
|
25
|
+
if (prev !== undefined && Number.isNaN(num)) {
|
|
26
|
+
return prev
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return num
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const actionBtnClass =
|
|
33
|
+
'ant-text-12px ant-flex ant-justify-center ant-items-center ant-h-1/2 ant-cursor-pointer ant-opacity-70 hover:ant-h-100% hover:ant-text-[var(--primary-color)] ant-transition-color ant-transition-height ant-transition-duration-500'
|
|
34
|
+
|
|
35
|
+
const InputNumber: Component<InputNumberProps> = props => {
|
|
36
|
+
const [{ onChange, onBlur }, inputProps] = splitProps(props, [
|
|
37
|
+
'defaultValue',
|
|
38
|
+
'value',
|
|
39
|
+
'onChange',
|
|
40
|
+
'onBlur',
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
const [_, controllableProps] = splitProps(props, ['onChange'])
|
|
44
|
+
const [value, setValue] = createControllableValue<number | string | null | undefined>(
|
|
45
|
+
controllableProps,
|
|
46
|
+
)
|
|
47
|
+
const add = (addon: number) => {
|
|
48
|
+
setValue(v => {
|
|
49
|
+
if (isEmptyValue(v)) {
|
|
50
|
+
return addon
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const num = Number(v)
|
|
54
|
+
if (Number.isNaN(num)) {
|
|
55
|
+
return v
|
|
56
|
+
}
|
|
57
|
+
return num + addon
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
const up = () => { add(1); }
|
|
61
|
+
const down = () => { add(-1); }
|
|
62
|
+
|
|
63
|
+
createEffect(
|
|
64
|
+
on(
|
|
65
|
+
value,
|
|
66
|
+
(input, __, prev: number | null | undefined) => {
|
|
67
|
+
const num = formatNum(input, prev)
|
|
68
|
+
if (num !== prev) {
|
|
69
|
+
prev = num
|
|
70
|
+
onChange?.(num)
|
|
71
|
+
}
|
|
72
|
+
return num
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
defer: true,
|
|
76
|
+
},
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<CommonInput
|
|
82
|
+
{...inputProps}
|
|
83
|
+
inputAfter={
|
|
84
|
+
<div class="ant-flex ant-flex-col ant-h-full ant-w-24px ant-[border-left:1px_solid_#d9d9d9]">
|
|
85
|
+
<div class={actionBtnClass} onClick={up}>
|
|
86
|
+
<div class="i-ant-design:up-outlined" />
|
|
87
|
+
</div>
|
|
88
|
+
<div class={`ant-[border-top:1px_solid_#d9d9d9] ${actionBtnClass}`} onClick={down}>
|
|
89
|
+
<div class="i-ant-design:down-outlined" />
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
}
|
|
93
|
+
value={`${value() ?? ''}`}
|
|
94
|
+
onKeyDown={e => {
|
|
95
|
+
switch (e.key) {
|
|
96
|
+
case 'ArrowUp':
|
|
97
|
+
up()
|
|
98
|
+
e.preventDefault()
|
|
99
|
+
return
|
|
100
|
+
case 'ArrowDown':
|
|
101
|
+
down()
|
|
102
|
+
e.preventDefault()
|
|
103
|
+
}
|
|
104
|
+
}}
|
|
105
|
+
onChange={e => {
|
|
106
|
+
const newValue = e.target.value || null
|
|
107
|
+
setValue(newValue)
|
|
108
|
+
}}
|
|
109
|
+
onBlur={e => {
|
|
110
|
+
const newValue = e.target.value || null
|
|
111
|
+
setValue(formatNum(newValue))
|
|
112
|
+
|
|
113
|
+
dispatchEventHandlerUnion(onBlur, e)
|
|
114
|
+
}}
|
|
115
|
+
/>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export default InputNumber
|
package/src/Modal.tsx
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { type JSXElement, Show, createSignal, untrack, Ref } from 'solid-js'
|
|
2
|
+
import { Portal, render } from 'solid-js/web'
|
|
3
|
+
import Button from './Button'
|
|
4
|
+
import cs from 'classnames'
|
|
5
|
+
|
|
6
|
+
export interface ModalInstance {
|
|
7
|
+
open: () => void
|
|
8
|
+
close: () => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ModalProps {
|
|
12
|
+
ref?: Ref<ModalInstance>
|
|
13
|
+
title?: JSXElement
|
|
14
|
+
initialOpen?: boolean
|
|
15
|
+
width?: string
|
|
16
|
+
height?: string
|
|
17
|
+
// open?: boolean
|
|
18
|
+
children?: JSXElement
|
|
19
|
+
/**
|
|
20
|
+
* 垂直居中展示 Modal
|
|
21
|
+
*/
|
|
22
|
+
centered?: boolean
|
|
23
|
+
/**
|
|
24
|
+
* 点击蒙层是否允许关闭
|
|
25
|
+
* 默认 true
|
|
26
|
+
*/
|
|
27
|
+
maskClosable?: boolean
|
|
28
|
+
/**
|
|
29
|
+
* 设置为 false 时隐藏关闭按钮
|
|
30
|
+
*/
|
|
31
|
+
closeIcon?: boolean
|
|
32
|
+
/**
|
|
33
|
+
* 返回 true,会自动关闭 modal
|
|
34
|
+
*/
|
|
35
|
+
onOk?: () => (boolean | Promise<boolean>)
|
|
36
|
+
afterClose?: () => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface MethodProps
|
|
40
|
+
extends Pick<ModalProps, 'title' | 'children' | 'onOk' | 'afterClose'> {}
|
|
41
|
+
|
|
42
|
+
function Modal(props: ModalProps) {
|
|
43
|
+
const [open, setOpen] = createSignal(props.initialOpen ?? false)
|
|
44
|
+
const close = () => {
|
|
45
|
+
setOpen(false)
|
|
46
|
+
props.afterClose?.()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const instance: ModalInstance = {
|
|
50
|
+
open() {
|
|
51
|
+
setOpen(true)
|
|
52
|
+
},
|
|
53
|
+
close() {
|
|
54
|
+
setOpen(false)
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
untrack(() => {
|
|
58
|
+
if (typeof props.ref === 'function') {
|
|
59
|
+
props.ref?.(instance)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const [confirmLoading, setConfirmLoading] = createSignal(false)
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Show when={open()}>
|
|
67
|
+
<Portal>
|
|
68
|
+
<div
|
|
69
|
+
class={cs(
|
|
70
|
+
'ant-fixed ant-justify-center ant-inset-0 ant-bg-[rgba(0,0,0,.45)] ant-flex ant-z-1000',
|
|
71
|
+
props.centered && 'ant-items-center',
|
|
72
|
+
)}
|
|
73
|
+
onClick={() => {
|
|
74
|
+
if (props.maskClosable ?? true) {
|
|
75
|
+
close()
|
|
76
|
+
}
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
<div
|
|
80
|
+
class={cs(
|
|
81
|
+
'ant-absolute ant-px-24px ant-py-20px ant-rounded-8px ant-overflow-hidden ant-bg-white ant-flex ant-flex-col',
|
|
82
|
+
// '!ant-[animation-duration:.5s]',
|
|
83
|
+
!props.centered && 'ant-top-100px',
|
|
84
|
+
)}
|
|
85
|
+
onClick={e => {
|
|
86
|
+
e.stopPropagation()
|
|
87
|
+
}}
|
|
88
|
+
style={{
|
|
89
|
+
width: props.width ?? '520px',
|
|
90
|
+
height: props.height,
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
{/* 关闭按钮 */}
|
|
94
|
+
<Show when={props.closeIcon !== false}>
|
|
95
|
+
<Button
|
|
96
|
+
type="text"
|
|
97
|
+
class={cs(
|
|
98
|
+
'ant-rm-size-btn !ant-w-22px !ant-h-22px !ant-flex !ant-justify-center !ant-items-center ant-text-center ant-text-18px !ant-absolute !ant-top-16px !ant-right-16px ant-z-1000 ant-text-[rgba(0,0,0,.45)] hover:!ant-text-[rgba(0,0,0,.88)]',
|
|
99
|
+
)}
|
|
100
|
+
onClick={close}
|
|
101
|
+
>
|
|
102
|
+
<span class="i-ant-design:close-outlined" />
|
|
103
|
+
</Button>
|
|
104
|
+
</Show>
|
|
105
|
+
|
|
106
|
+
<div class="ant-text-[rgba(0,0,0,.88)] ant-text-16px ant-font-600 ant-mb-8px">{props.title}</div>
|
|
107
|
+
<div class='ant-grow'>{props.children}</div>
|
|
108
|
+
|
|
109
|
+
<div class="ant-text-right ant-mt-12px">
|
|
110
|
+
<Button onClick={close}>取消</Button>
|
|
111
|
+
<Button
|
|
112
|
+
type="primary"
|
|
113
|
+
class="!ant-ml-8px"
|
|
114
|
+
loading={confirmLoading()}
|
|
115
|
+
// eslint-disable-next-line solid/reactivity, @typescript-eslint/no-misused-promises
|
|
116
|
+
onClick={async () => {
|
|
117
|
+
if (!props.onOk) return
|
|
118
|
+
|
|
119
|
+
let res = props.onOk?.()
|
|
120
|
+
if (res instanceof Promise) {
|
|
121
|
+
setConfirmLoading(true)
|
|
122
|
+
res = await res.finally(() => setConfirmLoading(false))
|
|
123
|
+
}
|
|
124
|
+
if (res) {
|
|
125
|
+
instance.close()
|
|
126
|
+
}
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
确定
|
|
130
|
+
</Button>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</Portal>
|
|
135
|
+
</Show>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
Modal.warning = (props: MethodProps) => {
|
|
140
|
+
const div = document.createElement('div')
|
|
141
|
+
document.body.appendChild(div)
|
|
142
|
+
const dispose = render(
|
|
143
|
+
() => (
|
|
144
|
+
<Modal
|
|
145
|
+
width="416px"
|
|
146
|
+
maskClosable={false}
|
|
147
|
+
closeIcon={false}
|
|
148
|
+
{...props}
|
|
149
|
+
title={
|
|
150
|
+
<>
|
|
151
|
+
<span class="i-ant-design:exclamation-circle ant-text-22px ant-mr-12px ant-text-[var(--warning-color)]" />
|
|
152
|
+
{props.title}
|
|
153
|
+
</>
|
|
154
|
+
}
|
|
155
|
+
children={<div class="ant-ml-34px">{props.children}</div>}
|
|
156
|
+
initialOpen
|
|
157
|
+
afterClose={() => {
|
|
158
|
+
document.body.removeChild(div)
|
|
159
|
+
dispose()
|
|
160
|
+
props.afterClose?.()
|
|
161
|
+
}}
|
|
162
|
+
/>
|
|
163
|
+
),
|
|
164
|
+
div,
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export default Modal
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Component, JSXElement, createSignal, mergeProps, untrack } from 'solid-js'
|
|
2
|
+
import Button from './Button'
|
|
3
|
+
import Tooltip from './Tooltip'
|
|
4
|
+
|
|
5
|
+
interface PopconfirmProps {
|
|
6
|
+
title?: JSXElement
|
|
7
|
+
content?: JSXElement
|
|
8
|
+
children: JSXElement
|
|
9
|
+
onCancel?: () => void
|
|
10
|
+
onConfirm?: () => void
|
|
11
|
+
/**
|
|
12
|
+
* 确认按钮文字
|
|
13
|
+
* 默认:确定
|
|
14
|
+
*/
|
|
15
|
+
okText?: string
|
|
16
|
+
/**
|
|
17
|
+
* 取消按钮文字
|
|
18
|
+
* 默认:取消
|
|
19
|
+
*/
|
|
20
|
+
cancelText?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const Popconfirm: Component<PopconfirmProps> = props => {
|
|
24
|
+
const mergedProps = mergeProps({ okText: '确定', cancelText: '取消' }, props)
|
|
25
|
+
const [open, setOpen] = createSignal(false)
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Tooltip
|
|
29
|
+
mode="light"
|
|
30
|
+
trigger="click"
|
|
31
|
+
open={open()}
|
|
32
|
+
onOpenChange={setOpen}
|
|
33
|
+
content={
|
|
34
|
+
<div>
|
|
35
|
+
<div class="ant-mb-8px ant-flex ant-items-center">
|
|
36
|
+
<span class="i-ant-design:exclamation-circle-fill ant-text-#faad14" />
|
|
37
|
+
<span class="ant-ml-8px ant-text-[rgba(0,0,0,0.88)] ant-font-600">{mergedProps.title}</span>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="ant-ml-22px ant-mb-8px ant-text-[rgba(0,0,0,0.88)]">{mergedProps.content}</div>
|
|
41
|
+
|
|
42
|
+
<div class="ant-text-right">
|
|
43
|
+
<Button
|
|
44
|
+
class="ant-ml-8px"
|
|
45
|
+
size="small"
|
|
46
|
+
onClick={() => {
|
|
47
|
+
setOpen(false)
|
|
48
|
+
untrack(() => mergedProps.onCancel?.())
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
{mergedProps.cancelText}
|
|
52
|
+
</Button>
|
|
53
|
+
<Button
|
|
54
|
+
class="ant-ml-8px"
|
|
55
|
+
type="primary"
|
|
56
|
+
size="small"
|
|
57
|
+
onClick={() => {
|
|
58
|
+
setOpen(false)
|
|
59
|
+
untrack(() => mergedProps.onConfirm?.())
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
{mergedProps.okText}
|
|
63
|
+
</Button>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
}
|
|
67
|
+
>
|
|
68
|
+
{mergedProps.children}
|
|
69
|
+
</Tooltip>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default Popconfirm
|
package/src/Popover.tsx
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type Component, type JSXElement, Show } from 'solid-js'
|
|
2
|
+
import Tooltip, { Content, type TooltipProps } from './Tooltip'
|
|
3
|
+
|
|
4
|
+
interface PopoverProps extends TooltipProps {
|
|
5
|
+
title?: JSXElement
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const Popover: Component<PopoverProps> = props => {
|
|
9
|
+
return (
|
|
10
|
+
<Tooltip
|
|
11
|
+
mode="light"
|
|
12
|
+
{...props}
|
|
13
|
+
content={close =>
|
|
14
|
+
<div>
|
|
15
|
+
<Show when={props.title}>
|
|
16
|
+
<div class="ant-mb-8px ant-flex ant-items-center">
|
|
17
|
+
<span class="ant-text-[rgba(0,0,0,0.88)] ant-font-600">{props.title}</span>
|
|
18
|
+
</div>
|
|
19
|
+
</Show>
|
|
20
|
+
|
|
21
|
+
<div class="ant-text-[rgba(0,0,0,0.88)]">
|
|
22
|
+
<Content content={props.content} close={close} />
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
}
|
|
26
|
+
/>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default Popover
|
package/src/Progress.tsx
ADDED