@baishuyun/ai-design 2.0.47 → 2.0.49
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/CHANGELOG.md +18 -0
- package/package.json +4 -4
- package/src/components/agent-drawer.tsx +40 -6
- package/src/components/agent-helper/agent-helper-card/index.tsx +3 -3
- package/src/components/agent-helper/app-select/index.tsx +7 -7
- package/src/components/agent-helper/condition-item/index.tsx +1 -0
- package/src/components/agent-helper/field-value-input/dept-select.tsx +447 -0
- package/src/components/agent-helper/field-value-input/index.tsx +3 -2
- package/src/components/agent-helper/field-value-input/picker-inputs.tsx +1 -1
- package/src/components/agent-helper/form-select/index.tsx +8 -5
- package/src/components/agent-helper/relational-operator-select/index.tsx +37 -23
- package/src/components/agent-helper/table/index.tsx +6 -2
- package/src/components/agent-select.tsx +115 -35
- package/src/components/card.tsx +19 -6
- package/src/components/fields-portal.tsx +2 -2
- package/src/components/fields-previewer.tsx +1 -0
- package/src/components/legacy-slot.tsx +11 -16
- package/src/components/output-config-editor.tsx +32 -19
- package/src/components/shadow-content.tsx +19 -0
- package/src/components/shadow-portal.tsx +23 -0
- package/src/components/ui/dialog.tsx +4 -1
- package/src/components/ui/drawer.tsx +3 -1
- package/src/components/ui/popover.tsx +3 -2
- package/src/components/ui/select.tsx +3 -2
- package/src/components/ui/tooltip.tsx +3 -2
- package/src/const/shadow.ts +12 -0
- package/src/hooks/use-portal-container.ts +6 -3
- package/src/hooks/use-shadow-boundary.ts +64 -0
- package/src/hooks/use-shadow.ts +24 -7
- package/src/index.ts +2 -0
- package/src/stories/AppSelect.stories.tsx +44 -0
- package/src/stories/FieldValueInput.stories.tsx +6 -3
- package/src/stories/FormSelect.stories.tsx +40 -0
- package/src/stories/RelationalOperatorSelect.stories.tsx +27 -0
- package/src/style.css +28 -1
- package/style.css +153 -126
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useLayoutEffect, type ElementType, type ReactNode } from 'react';
|
|
2
|
+
import { usePortalContainer } from '../hooks/use-portal-container';
|
|
3
|
+
|
|
4
|
+
export const ShadowPortal = ({ component: Portal, container, ...props }: {
|
|
5
|
+
component: ElementType;
|
|
6
|
+
container?: Element | DocumentFragment | null;
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
forceMount?: true;
|
|
9
|
+
'data-slot'?: string;
|
|
10
|
+
}) => {
|
|
11
|
+
const { portalContainer, setTriggerRef, portalReady } = usePortalContainer('data-bs-portal-container');
|
|
12
|
+
useLayoutEffect(() => {
|
|
13
|
+
if (container instanceof Element && container.parentNode instanceof ShadowRoot) {
|
|
14
|
+
container.setAttribute('data-bs-portal-container', '');
|
|
15
|
+
}
|
|
16
|
+
}, [container]);
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
<span ref={setTriggerRef} style={{ display: 'none' }} aria-hidden />
|
|
20
|
+
{portalReady && <Portal {...props} container={container === undefined ? portalContainer ?? undefined : container} />}
|
|
21
|
+
</>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
@@ -2,12 +2,15 @@ import * as React from 'react';
|
|
|
2
2
|
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
|
3
3
|
|
|
4
4
|
import { cn } from '../../lib/utils';
|
|
5
|
+
import { ShadowPortal } from '../shadow-portal';
|
|
5
6
|
|
|
6
7
|
const Dialog = DialogPrimitive.Root;
|
|
7
8
|
|
|
8
9
|
const DialogTrigger = DialogPrimitive.Trigger;
|
|
9
10
|
|
|
10
|
-
const DialogPortal = DialogPrimitive.Portal
|
|
11
|
+
const DialogPortal = (props: React.ComponentProps<typeof DialogPrimitive.Portal>) => (
|
|
12
|
+
<ShadowPortal component={DialogPrimitive.Portal} {...props} />
|
|
13
|
+
);
|
|
11
14
|
|
|
12
15
|
const DialogClose = DialogPrimitive.Close;
|
|
13
16
|
|
|
@@ -4,6 +4,7 @@ import { Drawer as DrawerPrimitive } from "vaul"
|
|
|
4
4
|
import { cn } from "../../lib/utils"
|
|
5
5
|
import { IconBtn } from "../icon-btn"
|
|
6
6
|
import { CloseIcon } from "../bs-icons"
|
|
7
|
+
import { ShadowPortal } from '../shadow-portal';
|
|
7
8
|
|
|
8
9
|
function Drawer({
|
|
9
10
|
...props
|
|
@@ -24,7 +25,8 @@ function DrawerPortal({
|
|
|
24
25
|
container?: HTMLElement | null;
|
|
25
26
|
}) {
|
|
26
27
|
return (
|
|
27
|
-
<
|
|
28
|
+
<ShadowPortal
|
|
29
|
+
component={DrawerPrimitive.Portal}
|
|
28
30
|
data-slot="drawer-portal"
|
|
29
31
|
container={container ?? undefined}
|
|
30
32
|
{...props}
|
|
@@ -2,6 +2,7 @@ import * as React from 'react';
|
|
|
2
2
|
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
|
3
3
|
|
|
4
4
|
import { cn } from '../../lib/utils';
|
|
5
|
+
import { ShadowPortal } from '../shadow-portal';
|
|
5
6
|
|
|
6
7
|
const Popover = PopoverPrimitive.Root;
|
|
7
8
|
|
|
@@ -52,7 +53,7 @@ const PopoverContent = React.forwardRef<
|
|
|
52
53
|
});
|
|
53
54
|
|
|
54
55
|
return (
|
|
55
|
-
<PopoverPrimitive.Portal container={portalContainer ?? undefined}>
|
|
56
|
+
<ShadowPortal component={PopoverPrimitive.Portal} container={portalContainer ?? undefined}>
|
|
56
57
|
<PopoverPrimitive.Content
|
|
57
58
|
ref={ref}
|
|
58
59
|
align={align}
|
|
@@ -76,7 +77,7 @@ const PopoverContent = React.forwardRef<
|
|
|
76
77
|
}}
|
|
77
78
|
{...props}
|
|
78
79
|
/>
|
|
79
|
-
</
|
|
80
|
+
</ShadowPortal>
|
|
80
81
|
);
|
|
81
82
|
}
|
|
82
83
|
);
|
|
@@ -3,6 +3,7 @@ import * as SelectPrimitive from '@radix-ui/react-select';
|
|
|
3
3
|
import { ChevronDown, ChevronUp } from 'lucide-react';
|
|
4
4
|
|
|
5
5
|
import { cn } from '../../lib/utils';
|
|
6
|
+
import { ShadowPortal } from '../shadow-portal';
|
|
6
7
|
|
|
7
8
|
const Select = SelectPrimitive.Root;
|
|
8
9
|
|
|
@@ -112,7 +113,7 @@ const SelectContent = React.forwardRef<
|
|
|
112
113
|
{containerProp === undefined && (
|
|
113
114
|
<span ref={setDetectorRef} style={{ display: 'none' }} aria-hidden />
|
|
114
115
|
)}
|
|
115
|
-
<SelectPrimitive.Portal container={effectiveContainer ?? undefined}>
|
|
116
|
+
<ShadowPortal component={SelectPrimitive.Portal} container={effectiveContainer ?? undefined}>
|
|
116
117
|
<SelectPrimitive.Content
|
|
117
118
|
ref={ref}
|
|
118
119
|
className={cn(
|
|
@@ -161,7 +162,7 @@ const SelectContent = React.forwardRef<
|
|
|
161
162
|
</SelectPrimitive.Viewport>
|
|
162
163
|
<SelectScrollDownButton />
|
|
163
164
|
</SelectPrimitive.Content>
|
|
164
|
-
</
|
|
165
|
+
</ShadowPortal>
|
|
165
166
|
</>
|
|
166
167
|
);
|
|
167
168
|
}
|
|
@@ -2,6 +2,7 @@ import * as React from 'react';
|
|
|
2
2
|
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
|
3
3
|
|
|
4
4
|
import { cn } from '../../lib/utils';
|
|
5
|
+
import { ShadowPortal } from '../shadow-portal';
|
|
5
6
|
|
|
6
7
|
const TooltipZIndexContext = React.createContext<number | undefined>(undefined);
|
|
7
8
|
|
|
@@ -30,7 +31,7 @@ const TooltipContent = React.forwardRef<
|
|
|
30
31
|
const zIndexFromProvider = React.useContext(TooltipZIndexContext);
|
|
31
32
|
|
|
32
33
|
return (
|
|
33
|
-
<TooltipPrimitive.Portal container={portalContainer ?? undefined}>
|
|
34
|
+
<ShadowPortal component={TooltipPrimitive.Portal} container={portalContainer ?? undefined}>
|
|
34
35
|
<TooltipPrimitive.Content
|
|
35
36
|
ref={ref}
|
|
36
37
|
sideOffset={sideOffset}
|
|
@@ -44,7 +45,7 @@ const TooltipContent = React.forwardRef<
|
|
|
44
45
|
{props.children}
|
|
45
46
|
<TooltipPrimitive.Arrow className="fill-[#525867] stroke-[#525867] stroke-[2px] stroke-linejoin-round [paint-order:stroke_fill]" />
|
|
46
47
|
</TooltipPrimitive.Content>
|
|
47
|
-
</
|
|
48
|
+
</ShadowPortal>
|
|
48
49
|
);
|
|
49
50
|
});
|
|
50
51
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const scopeSdkStyles = (css: string) => css.replace(/:host(?![-(])/g, ':is(:host, :host > div, [data-bs-shadow-content], [data-bs-portal-container])');
|
|
2
|
+
|
|
3
|
+
export const SDK_SHADOW_BASELINE = `
|
|
4
|
+
:host, :host > div {
|
|
5
|
+
font: 14px/1.5 ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
|
6
|
+
color: #121111;
|
|
7
|
+
letter-spacing: normal;
|
|
8
|
+
text-align: start;
|
|
9
|
+
text-transform: none;
|
|
10
|
+
color-scheme: light;
|
|
11
|
+
}
|
|
12
|
+
`;
|
|
@@ -2,7 +2,9 @@ import { useCallback, useState } from 'react';
|
|
|
2
2
|
|
|
3
3
|
export const usePortalContainer = (portalName: string) => {
|
|
4
4
|
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
|
|
5
|
+
const [portalReady, setPortalReady] = useState(false);
|
|
5
6
|
const setTriggerRef = useCallback((node: HTMLElement | null) => {
|
|
7
|
+
setPortalReady(!!node);
|
|
6
8
|
if (!node) {
|
|
7
9
|
setPortalContainer(null);
|
|
8
10
|
return;
|
|
@@ -14,14 +16,15 @@ export const usePortalContainer = (portalName: string) => {
|
|
|
14
16
|
setPortalContainer(existing);
|
|
15
17
|
return;
|
|
16
18
|
}
|
|
17
|
-
const container =
|
|
19
|
+
const container = node.ownerDocument.createElement('div');
|
|
18
20
|
container.setAttribute(portalName, 'true');
|
|
21
|
+
container.setAttribute('data-bs-portal-container', '');
|
|
19
22
|
rootNode.appendChild(container);
|
|
20
23
|
setPortalContainer(container);
|
|
21
24
|
} else {
|
|
22
25
|
setPortalContainer(null);
|
|
23
26
|
}
|
|
24
|
-
}, []);
|
|
27
|
+
}, [portalName]);
|
|
25
28
|
|
|
26
|
-
return { portalContainer, setTriggerRef };
|
|
29
|
+
return { portalContainer, setTriggerRef, portalReady };
|
|
27
30
|
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useState, type RefObject } from 'react';
|
|
2
|
+
import { syncHostStylesIntoShadow } from '@baishuyun/ai-feat-utils';
|
|
3
|
+
import { SDK_SHADOW_BASELINE, scopeSdkStyles } from '../const/shadow';
|
|
4
|
+
|
|
5
|
+
export type ShadowStyleMode = 'sdk' | 'host';
|
|
6
|
+
export type ShadowStyle = string | (() => string);
|
|
7
|
+
|
|
8
|
+
export const useShadowBoundary = (mode: ShadowStyleMode, styles?: ShadowStyle) => {
|
|
9
|
+
const [host, setHostRef] = useState<HTMLDivElement | null>(null);
|
|
10
|
+
const [boundary, setBoundary] = useState<{
|
|
11
|
+
root: ShadowRoot;
|
|
12
|
+
container: HTMLDivElement;
|
|
13
|
+
style: HTMLStyleElement;
|
|
14
|
+
} | null>(null);
|
|
15
|
+
|
|
16
|
+
useLayoutEffect(() => {
|
|
17
|
+
if (!host) return;
|
|
18
|
+
const root = host.shadowRoot || host.attachShadow({ mode: 'open' });
|
|
19
|
+
const style = host.ownerDocument.createElement('style');
|
|
20
|
+
const container = host.ownerDocument.createElement('div');
|
|
21
|
+
container.setAttribute('data-bs-shadow-content', '');
|
|
22
|
+
container.style.display = 'contents';
|
|
23
|
+
root.append(style, container);
|
|
24
|
+
setBoundary({ root, container, style });
|
|
25
|
+
return () => {
|
|
26
|
+
style.remove();
|
|
27
|
+
container.remove();
|
|
28
|
+
setBoundary(null);
|
|
29
|
+
};
|
|
30
|
+
}, [host]);
|
|
31
|
+
|
|
32
|
+
useLayoutEffect(() => {
|
|
33
|
+
if (!boundary || !host) return;
|
|
34
|
+
host.setAttribute('data-bs-style-mode', mode);
|
|
35
|
+
if (mode === 'sdk') boundary.container.className = '';
|
|
36
|
+
return mode === 'host' ? syncHostStylesIntoShadow(boundary.root, host) : undefined;
|
|
37
|
+
}, [boundary, host, mode]);
|
|
38
|
+
|
|
39
|
+
useLayoutEffect(() => {
|
|
40
|
+
if (!boundary) return;
|
|
41
|
+
boundary.style.toggleAttribute('data-bs-sdk-styles', mode === 'sdk');
|
|
42
|
+
boundary.style.toggleAttribute('data-bs-custom-styles', mode === 'host');
|
|
43
|
+
const css = typeof styles === 'function' ? styles() : styles || '';
|
|
44
|
+
boundary.style.textContent = mode === 'sdk' ? SDK_SHADOW_BASELINE + scopeSdkStyles(css) : css;
|
|
45
|
+
}, [boundary, mode, styles]);
|
|
46
|
+
|
|
47
|
+
return { setHostRef, container: boundary?.container ?? null };
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const useInjectedShadowStyle = (host: RefObject<HTMLElement | null>, css: string | undefined, attribute: string) => {
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const root = host.current?.shadowRoot;
|
|
53
|
+
if (!root) return;
|
|
54
|
+
let style = root.querySelector<HTMLStyleElement>(`style[${attribute}]`);
|
|
55
|
+
if (!css) { style?.remove(); return; }
|
|
56
|
+
if (!style) {
|
|
57
|
+
style = root.ownerDocument.createElement('style');
|
|
58
|
+
style.setAttribute(attribute, '');
|
|
59
|
+
root.appendChild(style);
|
|
60
|
+
}
|
|
61
|
+
style.textContent = css;
|
|
62
|
+
return () => style?.remove();
|
|
63
|
+
}, [host, css, attribute]);
|
|
64
|
+
};
|
package/src/hooks/use-shadow.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { SDK_SHADOW_BASELINE, scopeSdkStyles } from '../const/shadow';
|
|
3
|
+
import sdkStyles from '../style.css?inline';
|
|
2
4
|
|
|
3
5
|
interface UseShadowOptions {
|
|
4
6
|
/** Shadow DOM挂载的根元素(默认挂到body) */
|
|
@@ -15,6 +17,7 @@ interface UseShadowOptions {
|
|
|
15
17
|
extraHtml?: string;
|
|
16
18
|
|
|
17
19
|
id: string;
|
|
20
|
+
enabled?: boolean;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
/**
|
|
@@ -24,6 +27,7 @@ interface UseShadowOptions {
|
|
|
24
27
|
*/
|
|
25
28
|
export const useShadow = ({
|
|
26
29
|
id,
|
|
30
|
+
enabled = true,
|
|
27
31
|
style = '',
|
|
28
32
|
rootEl,
|
|
29
33
|
containerClasses = [],
|
|
@@ -57,10 +61,13 @@ export const useShadow = ({
|
|
|
57
61
|
wrapperRef.current = null;
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
if (!enabled) return;
|
|
65
|
+
|
|
60
66
|
try {
|
|
61
67
|
// 1. 创建外层包裹元素(挂载Shadow Root的载体)
|
|
62
68
|
const wrapperEl = document.createElement('div');
|
|
63
|
-
wrapperEl.classList.add(...wrapperClasses, 'wrapper');
|
|
69
|
+
wrapperEl.classList.add(...wrapperClasses, 'bs-sdk-shadow-wrapper');
|
|
70
|
+
wrapperEl.setAttribute('data-bs-style-mode', 'sdk');
|
|
64
71
|
// wrapperEl.style.height = '100%';
|
|
65
72
|
wrapperRef.current = wrapperEl;
|
|
66
73
|
|
|
@@ -81,15 +88,15 @@ export const useShadow = ({
|
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
// 3. 注入样式到Shadow DOM
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
91
|
+
const styleEl = document.createElement('style');
|
|
92
|
+
styleEl.setAttribute('data-bs-sdk-styles', '');
|
|
93
|
+
styleEl.textContent = SDK_SHADOW_BASELINE + scopeSdkStyles(sdkStyles + style);
|
|
94
|
+
shadowRoot.appendChild(styleEl);
|
|
89
95
|
|
|
90
96
|
// 4. 创建Shadow内部的容器(最终挂载React Portal的元素)
|
|
91
97
|
const containerEl = document.createElement('div');
|
|
92
98
|
containerEl.classList.add(...containerClasses, 'shadow-container');
|
|
99
|
+
containerEl.setAttribute('data-bs-shadow-content', '');
|
|
93
100
|
shadowRoot.appendChild(containerEl);
|
|
94
101
|
|
|
95
102
|
// 5. 将外层元素挂载到指定根节点
|
|
@@ -109,7 +116,17 @@ export const useShadow = ({
|
|
|
109
116
|
// wrapperRef.current = null;
|
|
110
117
|
// }
|
|
111
118
|
// };
|
|
112
|
-
|
|
119
|
+
return () => {
|
|
120
|
+
wrapperRef.current?.remove();
|
|
121
|
+
wrapperRef.current = null;
|
|
122
|
+
setShadowContainer(null);
|
|
123
|
+
};
|
|
124
|
+
}, [rootEl, enabled]); // Recreate when rootEl changes
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
const styleEl = wrapperRef.current?.shadowRoot?.querySelector('style[data-bs-sdk-styles]');
|
|
128
|
+
if (styleEl) styleEl.textContent = SDK_SHADOW_BASELINE + scopeSdkStyles(sdkStyles + style);
|
|
129
|
+
}, [style, shadowContainer]);
|
|
113
130
|
|
|
114
131
|
if (shadowContainer) {
|
|
115
132
|
shadowContainer.id = id;
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,8 @@ export { cn, copyTextToClipboard, toHumanReadable, isSubformFieldEqual } from '.
|
|
|
3
3
|
|
|
4
4
|
// Hooks
|
|
5
5
|
export { useShadow } from './hooks/use-shadow';
|
|
6
|
+
export { ShadowContent } from './components/shadow-content';
|
|
7
|
+
export type { ShadowContentProps } from './components/shadow-content';
|
|
6
8
|
|
|
7
9
|
// Constants
|
|
8
10
|
export {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
|
+
import { expect, userEvent, waitFor, within } from 'storybook/test';
|
|
3
4
|
import { AppSelect, groupsToAppTreeNodes } from '../components/agent-helper/app-select';
|
|
4
5
|
import type { AppTreeNode } from '../components/agent-helper/app-select';
|
|
5
6
|
import type { IAppGroup } from '@baishuyun/types';
|
|
@@ -125,6 +126,49 @@ type Story = StoryObj<typeof meta>;
|
|
|
125
126
|
|
|
126
127
|
export const Default: Story = {};
|
|
127
128
|
|
|
129
|
+
export const ConstrainedHeight: Story = {
|
|
130
|
+
args: { allowEmpty: true, showAll: true },
|
|
131
|
+
play: async ({ canvasElement }) => {
|
|
132
|
+
await userEvent.click(within(canvasElement).getByRole('button'));
|
|
133
|
+
const popover = await within(canvasElement.ownerDocument.body).findByRole('dialog');
|
|
134
|
+
await userEvent.clear(within(popover).getByRole('textbox'));
|
|
135
|
+
const content = popover.firstElementChild as HTMLElement;
|
|
136
|
+
const tree = content.lastElementChild as HTMLElement;
|
|
137
|
+
await waitFor(() => {
|
|
138
|
+
expect(popover.getBoundingClientRect().height).toBeLessThanOrEqual(parseFloat(getComputedStyle(popover).maxHeight));
|
|
139
|
+
expect(content.getBoundingClientRect().bottom).toBeLessThanOrEqual(popover.getBoundingClientRect().bottom);
|
|
140
|
+
expect(popover.scrollHeight).toBeLessThanOrEqual(popover.clientHeight);
|
|
141
|
+
expect(tree.scrollHeight).toBeGreaterThan(tree.clientHeight);
|
|
142
|
+
expect(content.firstElementChild!.getBoundingClientRect().height).toBe(30);
|
|
143
|
+
expect(within(popover).getByRole('button', { name: '无' }).getBoundingClientRect().height).toBe(34);
|
|
144
|
+
expect(within(popover).getByRole('button', { name: '全部应用' }).getBoundingClientRect().height).toBe(34);
|
|
145
|
+
});
|
|
146
|
+
tree.scrollTop = tree.scrollHeight;
|
|
147
|
+
expect(tree.scrollTop).toBeGreaterThan(0);
|
|
148
|
+
await userEvent.type(within(popover).getByRole('textbox'), 'API接口管理');
|
|
149
|
+
await userEvent.click(within(popover).getByRole('button', { name: 'API接口管理' }));
|
|
150
|
+
expect(within(canvasElement).getByRole('button')).toHaveTextContent('API接口管理');
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export const EmbeddedConstrainedHeight: Story = {
|
|
155
|
+
args: { popoverContentOnly: true, disableMaxHeight: true, showAll: true, contentCls: 'h-full' },
|
|
156
|
+
render: (args) => <div style={{ height: 200 }}><AppSelect {...args} /></div>,
|
|
157
|
+
play: async ({ canvasElement }) => {
|
|
158
|
+
const content = within(canvasElement).getByRole('textbox').parentElement!.parentElement!;
|
|
159
|
+
const tree = content.lastElementChild as HTMLElement;
|
|
160
|
+
await waitFor(() => {
|
|
161
|
+
expect(content.getBoundingClientRect().height).toBe(200);
|
|
162
|
+
expect(tree.getBoundingClientRect().bottom).toBeLessThanOrEqual(content.getBoundingClientRect().bottom);
|
|
163
|
+
expect(tree.scrollHeight).toBeGreaterThan(tree.clientHeight);
|
|
164
|
+
expect(within(content).getByRole('button', { name: '全部应用' }).getBoundingClientRect().height).toBe(34);
|
|
165
|
+
});
|
|
166
|
+
expect(content.firstElementChild!.getBoundingClientRect().height).toBe(30);
|
|
167
|
+
tree.scrollTop = tree.scrollHeight;
|
|
168
|
+
expect(tree.scrollTop).toBeGreaterThan(0);
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
128
172
|
export const SingleSelected: Story = {
|
|
129
173
|
args: { defaultValue: 'app2' },
|
|
130
174
|
};
|
|
@@ -21,9 +21,12 @@ const mockService: IFieldValueService = {
|
|
|
21
21
|
searchDepts: async (keyword) => {
|
|
22
22
|
await new Promise((r) => setTimeout(r, 500));
|
|
23
23
|
const depts = [
|
|
24
|
-
{ id: '
|
|
25
|
-
{ id: '
|
|
26
|
-
{ id: '
|
|
24
|
+
{ id: 'dept-root', name: '百数科技' },
|
|
25
|
+
{ id: 'dept1', name: '技术部', parentId: 'dept-root' },
|
|
26
|
+
{ id: 'dept1-1', name: '前端组', parentId: 'dept1' },
|
|
27
|
+
{ id: 'dept1-2', name: '后端组', parentId: 'dept1' },
|
|
28
|
+
{ id: 'dept2', name: '产品部', parentId: 'dept-root' },
|
|
29
|
+
{ id: 'dept3', name: '设计部', parentId: 'dept-root' },
|
|
27
30
|
];
|
|
28
31
|
return keyword
|
|
29
32
|
? depts.filter((d) => d.name.includes(keyword))
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
|
+
import { expect, userEvent, waitFor, within } from 'storybook/test';
|
|
3
4
|
import { FormSelect } from '../components/agent-helper/form-select';
|
|
4
5
|
import type { FormSelectItem, FormSelectChangeResult } from '../components/agent-helper/form-select';
|
|
5
6
|
|
|
@@ -91,6 +92,45 @@ export const ManyItems: Story = {
|
|
|
91
92
|
},
|
|
92
93
|
};
|
|
93
94
|
|
|
95
|
+
export const ConstrainedHeight: Story = {
|
|
96
|
+
args: { items: manyFormItems, allowEmpty: true },
|
|
97
|
+
play: async ({ canvasElement, args }) => {
|
|
98
|
+
if (!args.popoverOnly) {
|
|
99
|
+
await userEvent.click(within(canvasElement).getByText(/^(无|测试表单 30)$/));
|
|
100
|
+
}
|
|
101
|
+
const search = await within(canvasElement.ownerDocument.body).findByRole('textbox');
|
|
102
|
+
await userEvent.clear(search);
|
|
103
|
+
const content = search.parentElement!.parentElement!;
|
|
104
|
+
const list = content.lastElementChild as HTMLElement;
|
|
105
|
+
await waitFor(() => {
|
|
106
|
+
expect(content.getBoundingClientRect().height).toBeLessThanOrEqual(300);
|
|
107
|
+
expect(content.scrollHeight).toBeLessThanOrEqual(content.clientHeight);
|
|
108
|
+
expect(search.parentElement!.getBoundingClientRect().height).toBe(30);
|
|
109
|
+
expect(within(content).getByText('无').parentElement!.getBoundingClientRect().height).toBe(34);
|
|
110
|
+
expect(list.scrollHeight).toBeGreaterThan(list.clientHeight);
|
|
111
|
+
if (!args.popoverOnly) {
|
|
112
|
+
const portal = content.parentElement!;
|
|
113
|
+
expect(parseFloat(getComputedStyle(portal).maxHeight)).toBeLessThanOrEqual(300);
|
|
114
|
+
expect(content.getBoundingClientRect().bottom).toBeLessThanOrEqual(portal.getBoundingClientRect().bottom);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
const searchTop = search.getBoundingClientRect().top;
|
|
118
|
+
list.scrollTop = list.scrollHeight;
|
|
119
|
+
expect(list.scrollTop).toBeGreaterThan(0);
|
|
120
|
+
expect(search.getBoundingClientRect().top).toBe(searchTop);
|
|
121
|
+
await userEvent.type(search, '测试表单 30');
|
|
122
|
+
await userEvent.click(within(content).getByText('测试表单 30'));
|
|
123
|
+
if (!args.popoverOnly) {
|
|
124
|
+
expect(within(canvasElement).getByText('测试表单 30')).toBeVisible();
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const PopoverOnlyConstrainedHeight: Story = {
|
|
130
|
+
...ConstrainedHeight,
|
|
131
|
+
args: { ...ConstrainedHeight.args, popoverOnly: true },
|
|
132
|
+
};
|
|
133
|
+
|
|
94
134
|
export const Empty: Story = {
|
|
95
135
|
args: {
|
|
96
136
|
items: [],
|
|
@@ -45,6 +45,33 @@ export const Disabled: Story = {
|
|
|
45
45
|
},
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
export const DeptGroupLabels: Story = {
|
|
49
|
+
args: {
|
|
50
|
+
defaultValue: 'in',
|
|
51
|
+
operators: ['in', 'all', 'eq', 'empty', 'not_empty'],
|
|
52
|
+
fieldType: 'deptgroup',
|
|
53
|
+
},
|
|
54
|
+
name: '按字段类型显示描述(部门多选)',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const AddressLabels: Story = {
|
|
58
|
+
args: {
|
|
59
|
+
defaultValue: 'in',
|
|
60
|
+
operators: ['in', 'nin', 'empty', 'not_empty'],
|
|
61
|
+
fieldType: 'address',
|
|
62
|
+
},
|
|
63
|
+
name: '按字段类型显示描述(地址)',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const CustomOperatorLabels: Story = {
|
|
67
|
+
args: {
|
|
68
|
+
defaultValue: 'eq',
|
|
69
|
+
operators: ['eq', 'neq'],
|
|
70
|
+
operatorLabels: { eq: '是', neq: '不是' },
|
|
71
|
+
},
|
|
72
|
+
name: '自定义文案(operatorLabels)',
|
|
73
|
+
};
|
|
74
|
+
|
|
48
75
|
export const Controlled: Story = {
|
|
49
76
|
render: () => {
|
|
50
77
|
const [value, setValue] = useState<relationalOperator>('eq');
|
package/src/style.css
CHANGED
|
@@ -82,7 +82,34 @@
|
|
|
82
82
|
--color-sidebar-border: var(--sidebar-border);
|
|
83
83
|
--color-sidebar-ring: var(--sidebar-ring);
|
|
84
84
|
|
|
85
|
-
--
|
|
85
|
+
--spacing: 3.5px;
|
|
86
|
+
--radius: 8.75px;
|
|
87
|
+
--text-xs: 10.5px;
|
|
88
|
+
--text-xs--line-height: 14px;
|
|
89
|
+
--text-sm: 12.25px;
|
|
90
|
+
--text-sm--line-height: 17.5px;
|
|
91
|
+
--text-base: 14px;
|
|
92
|
+
--text-base--line-height: 21px;
|
|
93
|
+
--text-lg: 15.75px;
|
|
94
|
+
--text-lg--line-height: 24.5px;
|
|
95
|
+
--text-xl: 17.5px;
|
|
96
|
+
--text-xl--line-height: 24.5px;
|
|
97
|
+
--text-2xl: 21px;
|
|
98
|
+
--text-2xl--line-height: 28px;
|
|
99
|
+
--text-3xl: 26.25px;
|
|
100
|
+
--text-3xl--line-height: 31.5px;
|
|
101
|
+
--text-4xl: 31.5px;
|
|
102
|
+
--text-4xl--line-height: 35px;
|
|
103
|
+
--text-5xl: 42px;
|
|
104
|
+
--text-5xl--line-height: 1;
|
|
105
|
+
--text-6xl: 52.5px;
|
|
106
|
+
--text-6xl--line-height: 1;
|
|
107
|
+
--text-7xl: 63px;
|
|
108
|
+
--text-7xl--line-height: 1;
|
|
109
|
+
--text-8xl: 84px;
|
|
110
|
+
--text-8xl--line-height: 1;
|
|
111
|
+
--text-9xl: 112px;
|
|
112
|
+
--text-9xl--line-height: 1;
|
|
86
113
|
--background: oklch(1 0 0);
|
|
87
114
|
--foreground: oklch(0.129 0.042 264.695);
|
|
88
115
|
--card: oklch(1 0 0);
|