diff --git a/.eslintrc.js b/.eslintrc.js
index 8f417ac1..599c3d16 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -25,6 +25,17 @@ module.exports = {
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
},
+ parserOptions: {
+ sourceType: 'script',
+ },
+ },
+ {
+ files: ['**.test.ts', '**.test.tsx'],
+ rules: {
+ '@typescript-eslint/no-non-null-assertion': 'off',
+ 'jsx-a11y/click-events-have-key-events': 'off',
+ 'jsx-a11y/no-static-element-interactions': 'off',
+ },
},
],
env: {
diff --git a/internal/playground/src/router.tsx b/internal/playground/src/router.tsx
index ae364d2d..74546b69 100644
--- a/internal/playground/src/router.tsx
+++ b/internal/playground/src/router.tsx
@@ -126,6 +126,11 @@ export const baseRouter = [
path: '/tag',
element: getDemos(import.meta.glob('~/tag/demo/*.tsx')),
},
+ {
+ name: 'select 选择器',
+ path: '/select',
+ element: getDemos(import.meta.glob('~/select/demo/*.tsx')),
+ },
/*insert target*/
];
diff --git a/internal/playground/vite.config.ts b/internal/playground/vite.config.ts
index 60a810af..fa3878a6 100644
--- a/internal/playground/vite.config.ts
+++ b/internal/playground/vite.config.ts
@@ -17,20 +17,26 @@ export default defineConfig(() => {
cacheDir: `./.cache`,
resolve: {
alias: {
- ...pkgs.reduce((prev, cur) => {
- prev['@pkg/' + cur] = Path.resolve(
- __dirname,
- `../../packages/${cur}/src`,
- );
- return prev;
- }, {} as Record),
- ...components.reduce((prev, cur) => {
- prev['~/' + cur] = Path.resolve(
- __dirname,
- `../../packages/components/src/${cur}`,
- );
- return prev;
- }, {} as Record),
+ ...pkgs.reduce(
+ (prev, cur) => {
+ prev['@pkg/' + cur] = Path.resolve(
+ __dirname,
+ `../../packages/${cur}/src`,
+ );
+ return prev;
+ },
+ {} as Record,
+ ),
+ ...components.reduce(
+ (prev, cur) => {
+ prev['~/' + cur] = Path.resolve(
+ __dirname,
+ `../../packages/components/src/${cur}`,
+ );
+ return prev;
+ },
+ {} as Record,
+ ),
'@tool-pack/react-ui': Path.resolve(
__dirname,
'../../packages/react-ui/src',
diff --git a/package.json b/package.json
index e35b10ad..e3d543f3 100644
--- a/package.json
+++ b/package.json
@@ -52,66 +52,66 @@
"react-dom": "^18.2.0"
},
"devDependencies": {
- "@commitlint/cli": "^17.6.5",
- "@commitlint/config-conventional": "^17.6.5",
+ "@commitlint/cli": "^17.7.1",
+ "@commitlint/config-conventional": "^17.7.0",
"@mxssfd/dumi-theme-antd-style": "^0.27.5-beta.9",
"@rollup/plugin-json": "^6.0.0",
- "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/jest-dom": "^6.1.2",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
- "@tool-pack/basic": "^0.1.1",
+ "@tool-pack/basic": "^0.1.2",
"@tool-pack/bom": "0.0.1-beta.0",
"@tool-pack/dom": "^0.0.13",
- "@tool-pack/types": "^0.0.6",
+ "@tool-pack/types": "^0.0.9",
"@types/fs-extra": "^11.0.1",
- "@types/jest": "^29.5.1",
- "@types/node": "^16.18.34",
- "@types/react": "^18.2.7",
- "@types/react-dom": "^18.2.4",
- "@types/testing-library__jest-dom": "^5.14.6",
- "@typescript-eslint/eslint-plugin": "^5.59.8",
- "@typescript-eslint/parser": "^5.59.8",
- "@umijs/lint": "^4.0.0",
- "@vitejs/plugin-react": "^4.0.0",
- "autoprefixer": "^10.4.14",
- "chalk": "^5.2.0",
+ "@types/jest": "^29.5.4",
+ "@types/node": "^20.5.6",
+ "@types/react": "^18.2.21",
+ "@types/react-dom": "^18.2.7",
+ "@types/testing-library__jest-dom": "^5.14.9",
+ "@typescript-eslint/eslint-plugin": "5.62.0",
+ "@typescript-eslint/parser": "^5.62.0",
+ "@umijs/lint": "^4.0.78",
+ "@vitejs/plugin-react": "^4.0.4",
+ "autoprefixer": "^10.4.15",
+ "chalk": "^5.3.0",
"conventional-changelog-cli": "^3.0.0",
"cssnano": "^6.0.1",
- "dumi": "^2.2.1",
- "enquirer": "^2.3.6",
- "eslint": "^8.41.0",
- "eslint-config-prettier": "^8.8.0",
- "eslint-plugin-prettier": "^4.2.1",
- "esno": "^0.16.3",
- "execa": "^7.1.1",
- "father": "^4.1.0",
+ "dumi": "^2.2.6",
+ "enquirer": "^2.4.1",
+ "eslint": "^8.47.0",
+ "eslint-config-prettier": "^9.0.0",
+ "eslint-plugin-prettier": "^5.0.0",
+ "esno": "^0.17.0",
+ "execa": "^8.0.1",
+ "father": "^4.3.1",
"fs-extra": "^11.1.1",
- "gh-pages": "^5.0.0",
+ "gh-pages": "^6.0.0",
"husky": "^8.0.3",
- "lint-staged": "^13.2.2",
- "npm-check-updates": "^16.10.12",
- "postcss": "^8.4.26",
+ "lint-staged": "^14.0.1",
+ "npm-check-updates": "^16.13.1",
+ "postcss": "^8.4.28",
"postcss-cli": "^10.1.0",
"postcss-merge-rules-plus": "^2.0.0",
- "prettier": "^2.8.8",
- "prettier-plugin-organize-imports": "^3.0.0",
- "prettier-plugin-packagejson": "^2.2.18",
+ "prettier": "^3.0.2",
+ "prettier-plugin-organize-imports": "^3.2.3",
+ "prettier-plugin-packagejson": "^2.4.5",
"react-scripts": "5.0.1",
- "rollup": "^3.23.0",
- "rollup-plugin-dts": "^5.3.0",
- "rollup-plugin-typescript2": "^0.34.1",
+ "rollup": "^3.28.1",
+ "rollup-plugin-dts": "^6.0.0",
+ "rollup-plugin-typescript2": "^0.35.0",
"rxjs": "^7.8.1",
- "sass": "^1.62.1",
- "serve": "^14.2.0",
- "stylelint": "^15.7.0",
+ "sass": "^1.66.1",
+ "serve": "^14.2.1",
+ "stylelint": "^15.10.3",
"stylelint-config-prettier": "^9.0.5",
- "stylelint-config-standard": "^33.0.0",
- "stylelint-config-standard-scss": "^9.0.0",
+ "stylelint-config-standard": "^34.0.0",
+ "stylelint-config-standard-scss": "^10.0.0",
"stylelint-order": "^6.0.3",
- "stylelint-scss": "^5.0.1",
- "tsc-alias": "^1.8.6",
- "typescript": "^5.1.5",
- "vite": "^4.3.9",
+ "stylelint-scss": "^5.1.0",
+ "tsc-alias": "^1.8.7",
+ "typescript": "^5.2.2",
+ "vite": "^4.4.9",
"yalc": "1.0.0-pre.53"
},
"browserslist": {
diff --git a/packages/components/package.json b/packages/components/package.json
index 25888534..c679c967 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -13,6 +13,9 @@
"test": "react-scripts test --watchAll=false"
},
"jest": {
+ "testMatch": [
+ "/src/**/*.(spec|test).(ts|tsx)"
+ ],
"moduleNameMapper": {
"^@pkg/(.+)$": "/../../packages/$1/src",
"^@tool-pack/react-ui$": "/../../packages/react-ui/src",
diff --git a/packages/components/src/dropdown/Dropdown.tsx b/packages/components/src/dropdown/Dropdown.tsx
index cdaac787..b982e970 100644
--- a/packages/components/src/dropdown/Dropdown.tsx
+++ b/packages/components/src/dropdown/Dropdown.tsx
@@ -1,16 +1,16 @@
-import React, { useEffect, useRef, useState } from 'react';
+import React, { useRef } from 'react';
import type { RequiredPart } from '@tool-pack/types';
import { getClassNames } from '@tool-pack/basic';
import type {
- DropdownDivider,
DropdownOptionsItem,
+ DropdownDivider,
+ DropdownOption,
DropdownProps,
} from './dropdown.types';
import { Popover } from '~/popover';
-import { Option } from '~/option';
-import { getComponentClass } from '@pkg/shared';
+import { DropdownInnerOption } from './DropdownInnerOption';
+import { getComponentClass, useStateWithTrailClear } from '@pkg/shared';
import { Divider } from '~/divider';
-import { DropdownOption } from './dropdown.types';
const defaultProps = {
placement: 'bottom-start',
@@ -34,22 +34,36 @@ export const Dropdown: React.FC = (props) => {
} = props as RequiredPart;
const boxRef = useRef(null);
- const [show, setShow] = useState();
-
- useEffect(() => {
- show !== undefined && setShow(undefined);
- }, [show]);
- useEffect(() => {
- setShow(visible);
- }, [visible]);
+ const [show, setShow] = useStateWithTrailClear(visible);
const name = 'dropdown';
const rootClass = getComponentClass(name);
- const handleOptions = (
+ const Box = (
+ <>
+ {header && {header}
}
+
+ {footer && {footer}
}
+ >
+ );
+
+ return (
+
+ {children}
+
+ );
+
+ function handleOptions(
options: DropdownOptionsItem[] = [],
parents: DropdownOption[] = [],
- ): React.ReactElement[] => {
+ ): React.ReactElement[] {
return options.map((opt) => {
if (isDivider(opt)) {
const { key, type: _type, tag = 'li', ...rest } = opt;
@@ -64,7 +78,6 @@ export const Dropdown: React.FC = (props) => {
type,
label,
key,
- tag,
children,
attrs: optAttrs = {},
...optRest
@@ -73,7 +86,7 @@ export const Dropdown: React.FC = (props) => {
if (type === 'group') {
return (
-
+
{handleOptions(children, [...parents, opt])}
@@ -103,15 +116,14 @@ export const Dropdown: React.FC = (props) => {
emit(opt, parents);
};
const option = (
-
+
);
if (!children || optRest.disabled) return option;
@@ -138,33 +150,11 @@ export const Dropdown: React.FC = (props) => {
);
});
- };
-
- const Box = (
- <>
- {header && {header}
}
-
- {footer && {footer}
}
- >
- );
-
- return (
-
- {children}
-
- );
+ }
+ function isDivider(opt: DropdownOptionsItem): opt is DropdownDivider {
+ return (opt as DropdownDivider).type === 'divider';
+ }
};
Dropdown.defaultProps = defaultProps;
Dropdown.displayName = 'Dropdown';
-
-function isDivider(opt: DropdownOptionsItem): opt is DropdownDivider {
- return (opt as DropdownDivider).type === 'divider';
-}
diff --git a/packages/components/src/dropdown/DropdownInnerOption.tsx b/packages/components/src/dropdown/DropdownInnerOption.tsx
new file mode 100644
index 00000000..11c7554e
--- /dev/null
+++ b/packages/components/src/dropdown/DropdownInnerOption.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { DropdownOptionProps } from '~/dropdown/dropdown.types';
+import { Option } from '~/option';
+import { getComponentClass } from '@pkg/shared';
+import { getClassNames } from '@tool-pack/basic';
+import { Icon } from '~/icon';
+import { Right as RightIcon } from '@pkg/icons';
+
+const rootClass = getComponentClass('dropdown-option');
+const defaultProps = {
+ tag: 'li',
+} satisfies Partial;
+
+export const DropdownInnerOption: React.FC =
+ React.forwardRef((props, ref) => {
+ const { expandable, children, extra, attrs = {}, ...rest } = props;
+ return (
+
+ );
+ });
+
+DropdownInnerOption.defaultProps = defaultProps;
diff --git a/packages/components/src/dropdown/__tests__/__snapshots__/Dropdown.test.tsx.snap b/packages/components/src/dropdown/__tests__/__snapshots__/Dropdown.test.tsx.snap
index 58bc1220..b71b6b04 100644
--- a/packages/components/src/dropdown/__tests__/__snapshots__/Dropdown.test.tsx.snap
+++ b/packages/components/src/dropdown/__tests__/__snapshots__/Dropdown.test.tsx.snap
@@ -2,7 +2,7 @@
exports[`Dropdown basic 1`] = `
- 黄金蛋炒饭
+
+ 黄金蛋炒饭
+
- 扬州炒饭
+
+ 扬州炒饭
+
@@ -38,7 +46,7 @@ exports[`Dropdown basic 1`] = `
exports[`Dropdown basic 2`] = `
- 黄金蛋炒饭
+
+ 黄金蛋炒饭
+
- 扬州炒饭
+
+ 扬州炒饭
+
@@ -74,7 +90,7 @@ exports[`Dropdown basic 2`] = `
exports[`Dropdown disabled 1`] = `
- 黄金蛋炒饭
+
+ 黄金蛋炒饭
+
- 扬州炒饭
+
+ 扬州炒饭
+
@@ -107,7 +131,7 @@ exports[`Dropdown disabled 1`] = `
exports[`Dropdown group 1`] = `
-
- 手撕鸡
+
+ 手撕鸡
+
-
-
- 黄金蛋炒饭
+
+ 黄金蛋炒饭
+
-
- 扬州炒饭
+
+ 扬州炒饭
+
- 其他
+
+ 其他
+
@@ -179,7 +227,7 @@ exports[`Dropdown group 1`] = `
exports[`Dropdown header footer 1`] = `
- 黄金蛋炒饭
+
+ 黄金蛋炒饭
+
- 扬州炒饭
+
+ 扬州炒饭
+
@@ -225,7 +281,7 @@ exports[`Dropdown header footer 1`] = `
exports[`Dropdown nest 1`] = `
- 手撕鸡
+
+ 手撕鸡
+
-
蛋炒饭
- 黄金蛋炒饭,黄金蛋炒饭
+
+ 黄金蛋炒饭,黄金蛋炒饭
+
- 扬州炒饭
+
+ 扬州炒饭
+
-
-
+
- 榴莲
+
+ 榴莲
+
diff --git a/packages/components/src/dropdown/demo/nest.tsx b/packages/components/src/dropdown/demo/nest.tsx
index e8b01f87..8e75ba68 100644
--- a/packages/components/src/dropdown/demo/nest.tsx
+++ b/packages/components/src/dropdown/demo/nest.tsx
@@ -27,6 +27,7 @@ const options: DropdownOptionsItem[] = [
蛋炒饭
),
+ extra: '推荐 ',
children: [
{
key: '4',
diff --git a/packages/components/src/dropdown/dropdown.types.ts b/packages/components/src/dropdown/dropdown.types.ts
index cffe2ae9..b0e2c2a3 100644
--- a/packages/components/src/dropdown/dropdown.types.ts
+++ b/packages/components/src/dropdown/dropdown.types.ts
@@ -4,11 +4,15 @@ import type { OptionProps } from '~/option';
import type { PopoverProps } from '~/popover';
import { DividerProps } from '~/divider';
+export interface DropdownOptionProps extends OptionProps {
+ expandable?: boolean;
+}
export interface DropdownDivider extends DividerProps {
type: 'divider';
key: React.Key;
}
-export interface DropdownOption extends Omit
{
+export interface DropdownOption
+ extends Omit {
type?: 'group' | 'option';
key: React.Key;
label: React.ReactNode;
diff --git a/packages/components/src/dropdown/index.scss b/packages/components/src/dropdown/index.scss
index c14d5bf1..bc6b368e 100644
--- a/packages/components/src/dropdown/index.scss
+++ b/packages/components/src/dropdown/index.scss
@@ -2,6 +2,7 @@
@use '../popover/index' as Pop;
$r: Name.$dropdown;
+$o: Name.$dropdown-option;
$balloon-content: #{Name.$word-balloon}__content;
$option: Name.$option;
@@ -36,3 +37,8 @@ $option: Name.$option;
margin-left: var(--t-radius);
}
}
+.#{$o} {
+ &__expand {
+ font-size: 0.8em;
+ }
+}
diff --git a/packages/components/src/dropdown/index.ts b/packages/components/src/dropdown/index.ts
index 79f70f61..3717e6d4 100644
--- a/packages/components/src/dropdown/index.ts
+++ b/packages/components/src/dropdown/index.ts
@@ -1,2 +1,8 @@
-export type { DropdownProps, DropdownOptionsItem } from './dropdown.types';
+export type {
+ DropdownOptionsItem,
+ DropdownOptionProps,
+ DropdownDivider,
+ DropdownOption,
+ DropdownProps,
+} from './dropdown.types';
export * from './Dropdown';
diff --git a/packages/components/src/dropdown/index.zh-CN.md b/packages/components/src/dropdown/index.zh-CN.md
index 398e9abb..1fd7b390 100644
--- a/packages/components/src/dropdown/index.zh-CN.md
+++ b/packages/components/src/dropdown/index.zh-CN.md
@@ -31,23 +31,26 @@ Dropdown 说明。
Dropdown 的属性说明如下:
```typescript
-export interface OptionProps {
+export interface OptionProps extends PropsBase {
tag?: keyof HTMLElementTagNameMap;
size?: Size;
disabled?: boolean;
- expandable?: boolean;
readonly?: boolean;
icon?: React.ReactNode;
- ref?: React.ForwardedRef;
- attrs?: Partial>;
- children: React.ReactNode;
+ extra?: React.ReactNode;
+}
+
+export interface DropdownOptionProps extends OptionProps {
+ expandable?: boolean;
}
export interface DropdownDivider extends DividerProps {
type: 'divider';
key: React.Key;
}
-export interface DropdownOption extends Omit {
+
+export interface DropdownOption
+ extends Omit {
type?: 'group' | 'option';
key: React.Key;
label: React.ReactNode;
diff --git a/packages/components/src/index.scss b/packages/components/src/index.scss
index 5422276b..1ba7e242 100644
--- a/packages/components/src/index.scss
+++ b/packages/components/src/index.scss
@@ -22,3 +22,4 @@
@import './switch';
@import './transition';
@import './tag';
+@import './select';
diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts
index 0f7b3244..96e30bbc 100644
--- a/packages/components/src/index.ts
+++ b/packages/components/src/index.ts
@@ -28,3 +28,4 @@ export * from './number-transition';
export * from './alert';
export * from './switch';
export * from './tag';
+export * from './select';
diff --git a/packages/components/src/layouts/useElement.tsx b/packages/components/src/layouts/useElement.tsx
index 30aa8d71..96e79cc1 100644
--- a/packages/components/src/layouts/useElement.tsx
+++ b/packages/components/src/layouts/useElement.tsx
@@ -7,7 +7,7 @@ export function useElement(
props: BaseLayoutsProps,
ref: React.ForwardedRef,
rootClass: string,
-) {
+): React.ReactElement {
const {
children,
className,
diff --git a/packages/components/src/namespace.scss b/packages/components/src/namespace.scss
index e300786c..0f39bcb5 100644
--- a/packages/components/src/namespace.scss
+++ b/packages/components/src/namespace.scss
@@ -25,8 +25,10 @@ $layout-header: '#{Var.$prefix}header';
$layout-footer: '#{Var.$prefix}footer';
$option: '#{Var.$prefix}option';
$dropdown: '#{Var.$prefix}dropdown';
+$dropdown-option: '#{Var.$prefix}dropdown-option';
$number-transition: '#{Var.$prefix}number-transition';
$alert: '#{Var.$prefix}alert';
$switch: '#{Var.$prefix}switch';
$transition: '#{Var.$prefix}transition';
$tag: '#{Var.$prefix}tag';
+$select: '#{Var.$prefix}select';
diff --git a/packages/components/src/option/Option.tsx b/packages/components/src/option/Option.tsx
index 74fb9a9e..976201ec 100644
--- a/packages/components/src/option/Option.tsx
+++ b/packages/components/src/option/Option.tsx
@@ -4,7 +4,6 @@ import { getComponentClass, getSizeClassName } from '@pkg/shared';
import type { RequiredPart } from '@tool-pack/types';
import { getClassNames } from '@tool-pack/basic';
import { Icon } from '~/icon';
-import { Right as RightIcon } from '@pkg/icons';
const defaultProps = {
tag: 'div',
@@ -18,7 +17,7 @@ export const Option: React.FC = React.forwardRef<
HTMLElement,
OptionProps
>((props, ref) => {
- const { children, icon, readonly, expandable, size, tag, disabled } =
+ const { children, icon, readonly, extra, size, tag, disabled } =
props as RequiredPart;
const attrs = {
@@ -29,16 +28,8 @@ export const Option: React.FC = React.forwardRef<
const Child = (
<>
{icon && {icon}}
- {!icon && !expandable ? (
- children
- ) : (
- {children}
- )}
- {expandable && (
-
-
-
- )}
+ {children}
+ {extra && {extra}
}
>
);
diff --git a/packages/components/src/option/__tests__/Option.test.tsx b/packages/components/src/option/__tests__/Option.test.tsx
index 32e9e61a..6a380573 100644
--- a/packages/components/src/option/__tests__/Option.test.tsx
+++ b/packages/components/src/option/__tests__/Option.test.tsx
@@ -16,16 +16,16 @@ describe('Option', () => {
expect(container.firstChild).toHaveAttribute('disabled', '');
});
- test('expandable', () => {
- const { container } = render();
+ test('extra', () => {
+ const { container } = render(
+ ,
+ );
expect(container.firstChild).toMatchSnapshot();
});
test('icon', () => {
const { container } = render(
- } expandable>
- foo bar
- ,
+ }>foo bar,
);
expect(container.firstChild).toMatchSnapshot();
});
diff --git a/packages/components/src/option/__tests__/__snapshots__/Option.test.tsx.snap b/packages/components/src/option/__tests__/__snapshots__/Option.test.tsx.snap
index b1305b4b..bf58338d 100644
--- a/packages/components/src/option/__tests__/__snapshots__/Option.test.tsx.snap
+++ b/packages/components/src/option/__tests__/__snapshots__/Option.test.tsx.snap
@@ -1,28 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Option expandable 1`] = `
+exports[`Option extra 1`] = `
-
foo bar
-
-
+
`;
@@ -48,24 +40,11 @@ exports[`Option icon 1`] = `
/>
-
foo bar
-
-
-
-
+
`;
@@ -74,7 +53,11 @@ exports[`Option readonly 1`] = `
class="t-option t--size-m t-option--readonly"
role="option"
>
- foo bar
+
+ foo bar
+
`;
@@ -83,6 +66,10 @@ exports[`Option snap 1`] = `
class="t-option t--size-m"
role="option"
>
- foo bar
+
+ foo bar
+
`;
diff --git a/packages/components/src/option/demo/expandable.tsx b/packages/components/src/option/demo/expandable.tsx
deleted file mode 100644
index f0cd3b8a..00000000
--- a/packages/components/src/option/demo/expandable.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * title: expandable
- */
-
-import React from 'react';
-import { Option } from '@tool-pack/react-ui';
-
-const App: React.FC = () => {
- return
;
-};
-
-export default App;
diff --git a/packages/components/src/option/demo/extra.tsx b/packages/components/src/option/demo/extra.tsx
new file mode 100644
index 00000000..0285203a
--- /dev/null
+++ b/packages/components/src/option/demo/extra.tsx
@@ -0,0 +1,21 @@
+/**
+ * title: extra
+ */
+
+import React from 'react';
+import { Option, Icon, Icons } from '@tool-pack/react-ui';
+
+const App: React.FC = () => {
+ return (
+
+ );
+};
+
+export default App;
diff --git a/packages/components/src/option/index.scss b/packages/components/src/option/index.scss
index 2b65f4df..0501ca0e 100644
--- a/packages/components/src/option/index.scss
+++ b/packages/components/src/option/index.scss
@@ -54,8 +54,9 @@ $r: Name.$option;
&__icon {
margin-right: 6px;
}
- &__expand {
+ &__extra {
+ display: flex;
+ align-items: center;
margin-left: 1em;
- font-size: 0.8em;
}
}
diff --git a/packages/components/src/option/index.zh-CN.md b/packages/components/src/option/index.zh-CN.md
index ffadd180..18a3fef6 100644
--- a/packages/components/src/option/index.zh-CN.md
+++ b/packages/components/src/option/index.zh-CN.md
@@ -18,7 +18,7 @@ Option 说明。
-
+
## API
diff --git a/packages/components/src/option/option.types.ts b/packages/components/src/option/option.types.ts
index 8bc336ec..39b42691 100644
--- a/packages/components/src/option/option.types.ts
+++ b/packages/components/src/option/option.types.ts
@@ -5,7 +5,7 @@ export interface OptionProps extends PropsBase {
tag?: keyof HTMLElementTagNameMap;
size?: Size;
disabled?: boolean;
- expandable?: boolean;
readonly?: boolean;
icon?: React.ReactNode;
+ extra?: React.ReactNode;
}
diff --git a/packages/components/src/pop-confirm/__tests__/PopConfirm.test.tsx b/packages/components/src/pop-confirm/__tests__/PopConfirm.test.tsx
index e3bb8d82..7d4af039 100644
--- a/packages/components/src/pop-confirm/__tests__/PopConfirm.test.tsx
+++ b/packages/components/src/pop-confirm/__tests__/PopConfirm.test.tsx
@@ -2,18 +2,6 @@ import { PopConfirm } from '..';
import { fireEvent, render } from '@testing-library/react';
describe('PopConfirm', () => {
- // 模拟 ResizeObserver,ResizeObserver 不存在于 jsdom 中
- const MockObserverInstance: ResizeObserver = {
- observe: jest.fn(),
- unobserve: jest.fn(),
- disconnect: jest.fn(),
- };
- beforeEach(() => {
- global.ResizeObserver = jest
- .fn()
- .mockImplementation(() => MockObserverInstance);
- });
-
test('attrs', () => {
const onClick = jest.fn();
const { container } = render(
diff --git a/packages/components/src/popover/Contextmenu.tsx b/packages/components/src/popover/Contextmenu.tsx
index 135939a5..317cc880 100644
--- a/packages/components/src/popover/Contextmenu.tsx
+++ b/packages/components/src/popover/Contextmenu.tsx
@@ -9,7 +9,7 @@ const rootClass = getComponentClass('popover');
export const Contextmenu: React.FC
= (props) => {
const { children, trigger: _, ...rest } = props;
- const triggerRef = useRef(null);
+ const triggerRef = useRef(null);
const forceUpdate = useForceUpdate();
const onContextMenuCapture = (e: React.MouseEvent) => {
@@ -35,8 +35,9 @@ export const Contextmenu: React.FC = (props) => {
children.props.children,
)}
{createPortal(
-
+
;
+export type PopoverRequiredPartProps = RequiredPart<
+ PopoverProps,
+ keyof typeof defaultProps
+>;
+
export const Popover: React.FC
= React.forwardRef<
HTMLDivElement,
PopoverProps
@@ -48,43 +53,56 @@ export const Popover: React.FC = React.forwardRef<
on,
appendTo,
viewport,
- childrenRef: kidRef,
showArrow,
delay,
leaveDelay,
+ onVisibleChange,
+ widthByTrigger,
attrs = {},
- } = props as RequiredPart;
+ } = props as PopoverRequiredPartProps;
const rootName = getComponentClass(name);
const [appendToTarget] = useAppendTo(appendTo, defaultProps.appendTo);
- const childrenRef = useForwardRef(kidRef);
+ const childrenRef = useForwardRef(
+ (children as React.RefAttributes).ref,
+ ) as React.MutableRefObject;
const [balloonRef, refreshBalloonRef] = useForwardRef(ref, true) as [
React.MutableRefObject,
() => void,
];
const [refreshPosition, resetPlacement] = usePosition(
- placement,
childrenRef,
balloonRef,
- appendTo,
- offset,
- viewport,
+ {
+ widthByTrigger,
+ placement,
+ viewport,
+ appendTo,
+ offset,
+ },
);
const show = useShowController(
- disabled,
- visible,
- trigger,
- children,
childrenRef,
balloonRef,
refreshPosition,
- delay,
- leaveDelay,
+ // 下面👇的对象属性都是在 props 中取的,为什么不直接传 props ?
+ // 因为这样在上面的 props 解构中就可以直观的看出到底有哪些属性是没有用到的;传 props 是不直观的。
+ // 如果看到没有按照这条规则弄的,那就是漏掉了,以该条规则为准。
+ {
+ delay,
+ visible,
+ trigger,
+ children,
+ disabled,
+ leaveDelay,
+ onVisibleChange,
+ },
);
useResizeObserver(show, balloonRef, refreshPosition);
+ useResizeObserver(show, childrenRef, refreshPosition);
useResizeEvent(show, refreshPosition);
const Balloon = (
diff --git a/packages/components/src/popover/__tests__/Popover.test.tsx b/packages/components/src/popover/__tests__/Popover.test.tsx
index fb3d504c..604b7236 100644
--- a/packages/components/src/popover/__tests__/Popover.test.tsx
+++ b/packages/components/src/popover/__tests__/Popover.test.tsx
@@ -2,20 +2,10 @@ import { Popover } from '..';
import { act, fireEvent, render } from '@testing-library/react';
import { Button } from '~/button';
import { nextTick } from '@tool-pack/basic';
+import { useRef, useState } from 'react';
describe('Popover', () => {
- // 模拟 ResizeObserver,ResizeObserver 不存在于 jsdom 中
- const MockObserverInstance: ResizeObserver = {
- observe: jest.fn(),
- unobserve: jest.fn(),
- disconnect: jest.fn(),
- };
- beforeEach(() => {
- global.ResizeObserver = jest
- .fn()
- .mockImplementation(() => MockObserverInstance);
- });
-
+ jest.useFakeTimers();
test('attrs', () => {
const onClick = jest.fn();
const { container } = render(
@@ -94,6 +84,7 @@ describe('Popover', () => {
expect(document.body).toMatchSnapshot();
fireEvent.click(container.querySelector('button')!);
+ act(() => jest.advanceTimersByTime(0));
expect(document.body).toMatchSnapshot();
});
@@ -133,7 +124,7 @@ describe('Popover', () => {
expect(document.body).toMatchSnapshot();
fireEvent.contextMenu(container.querySelector('.trigger')!);
await act(() => nextTick());
-
+ act(() => jest.advanceTimersByTime(0));
expect(document.body).toMatchSnapshot();
});
@@ -154,6 +145,7 @@ describe('Popover', () => {
,
);
fireEvent.click(container.querySelector('button')!);
+ act(() => jest.advanceTimersByTime(0));
expect(document.body).toMatchSnapshot();
});
});
@@ -209,18 +201,30 @@ describe('Popover', () => {
fireEvent.mouseEnter(container.querySelector('button')!);
expect(document.querySelector('.t-word-balloon')).not.toBeNull();
+ expect(document.querySelector('.t-word-balloon')).toHaveClass(
+ 't-popover-enter-active',
+ );
+
+ // Transition 超时300毫秒后会切换为 idle 或 invisible
+ act(() => jest.advanceTimersByTime(300));
+ expect(document.querySelector('.t-word-balloon')).not.toHaveClass(
+ 't-popover-enter-active',
+ );
+
expect(document.querySelector('.t-word-balloon')).not.toHaveClass(
't-popover-leave-active',
);
fireEvent.mouseLeave(document.querySelector('.t-word-balloon')!);
+ // 离开 200 毫秒后启动离开动画
act(() => jest.advanceTimersByTime(200));
- // todo: 这里暂时测不了,需要给 Transition 组件添加一个默认定时器触发 transitionend 事件
- // expect(document.querySelector('.t-word-balloon')).toHaveClass(
- // 't-popover-leave-active',
- // );
- expect(document.querySelector('.t-word-balloon')).not.toHaveClass(
+ expect(document.querySelector('.t-word-balloon')).toHaveClass(
't-popover-leave-active',
);
+ // 300 毫秒后 invisible
+ act(() => jest.advanceTimersByTime(300));
+ expect(document.querySelector('.t-word-balloon')).toHaveClass(
+ 't-transition--invisible',
+ );
});
test('leaveDelay 500', () => {
jest.useFakeTimers();
@@ -234,18 +238,157 @@ describe('Popover', () => {
fireEvent.mouseEnter(container.querySelector('button')!);
expect(document.querySelector('.t-word-balloon')).not.toBeNull();
+ expect(document.querySelector('.t-word-balloon')).toHaveClass(
+ 't-popover-enter-active',
+ );
+ act(() => jest.advanceTimersByTime(500));
+ expect(document.querySelector('.t-word-balloon')).not.toHaveClass(
+ 't-popover-enter-active',
+ );
+
expect(document.querySelector('.t-word-balloon')).not.toHaveClass(
't-popover-leave-active',
);
fireEvent.mouseLeave(document.querySelector('.t-word-balloon')!);
act(() => jest.advanceTimersByTime(500));
- // todo: 这里暂时测不了,需要给 Transition 组件添加一个默认定时器触发 transitionend 事件
- // expect(document.querySelector('.t-word-balloon')).toHaveClass(
- // 't-popover-leave-active',
- // );
+ expect(document.querySelector('.t-word-balloon')).toHaveClass(
+ 't-popover-leave-active',
+ );
+ act(() => jest.advanceTimersByTime(500));
expect(document.querySelector('.t-word-balloon')).not.toHaveClass(
't-popover-leave-active',
);
+ expect(document.querySelector('.t-word-balloon')).toHaveClass(
+ 't-transition--invisible',
+ );
});
});
+
+ describe('onVisibleChange', () => {
+ test('basic', () => {
+ jest.useFakeTimers();
+ const onVisibleChange = jest.fn();
+ const { container } = render(
+
+
+ ,
+ );
+ expect(onVisibleChange).not.toBeCalled();
+
+ fireEvent.mouseEnter(container.firstChild!);
+ expect(onVisibleChange).toBeCalled();
+ expect(onVisibleChange.mock.calls[0][0]).toBeTruthy();
+
+ act(() => jest.advanceTimersByTime(300));
+ fireEvent.mouseLeave(container.firstChild!);
+ act(() => jest.advanceTimersByTime(200));
+ expect(onVisibleChange.mock.calls[1][0]).toBeFalsy();
+ });
+
+ test('external', () => {
+ jest.useFakeTimers();
+ const onVisibleChange = jest.fn();
+
+ const App = () => {
+ const [visible, setVisible] = useState(false);
+ return (
+
+
+
+ );
+ };
+ const { container } = render();
+ expect(onVisibleChange).not.toBeCalled();
+
+ fireEvent.click(container.firstChild!);
+ expect(onVisibleChange).not.toBeCalled();
+ });
+ });
+
+ it('触发元素拦截点击事件后禁止触发', () => {
+ jest.useFakeTimers();
+ const App = () => {
+ return (
+
+
+ click
+
+
+
+ );
+ };
+ const { container } = render();
+
+ const onClick = jest.fn();
+ document.addEventListener('click', onClick);
+
+ // react 的合成事件无法阻止原生事件冒泡,反过来却可以,
+ // 因为 react 的事件是代理在 document 上的,实际已经冒泡或者捕获在 document 上了
+ // 除非是在捕获阶段拦截
+ fireEvent.click(container.querySelector('button')!);
+ act(() => jest.advanceTimersByTime(500));
+ expect(document.body).toMatchSnapshot();
+
+ expect(onClick).not.toBeCalled();
+ });
+
+ it('被触发元素拦截事件后点击外部事件仍然有效', () => {
+ jest.useFakeTimers();
+ const App = () => {
+ const visibleRef = useRef(false);
+ return (
+ (visibleRef.current = visible)}
+ content="1">
+ {
+ if (visibleRef.current) {
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }}>
+
+
+
+ );
+ };
+ const { container } = render();
+
+ expect(getBalloon()).toBeNull();
+
+ fireEvent.click(container.querySelector('button')!);
+ act(() => jest.advanceTimersByTime(0));
+ act(() => jest.advanceTimersByTime(300));
+
+ expect(getBalloon()).toMatchSnapshot();
+ expect(getBalloon()).not.toHaveClass('t-popover-enter-active');
+
+ fireEvent.click(container.querySelector('button')!);
+ expect(getBalloon()).not.toHaveClass('t-popover-leave-active');
+
+ act(() => jest.advanceTimersByTime(300));
+
+ // 未修复前这一步会无效,外部事件会无法被 div 拦截,导致外部点击事件监听被移除而无法关闭弹窗
+ fireEvent.click(document.body);
+ expect(getBalloon()).toHaveClass('t-popover-leave-active');
+
+ act(() => jest.advanceTimersByTime(0));
+ act(() => jest.advanceTimersByTime(300));
+ expect(getBalloon()).toMatchSnapshot();
+
+ function getBalloon() {
+ return document.querySelector('.t-word-balloon') as HTMLElement;
+ }
+ });
});
diff --git a/packages/components/src/popover/__tests__/__snapshots__/Popover.test.tsx.snap b/packages/components/src/popover/__tests__/__snapshots__/Popover.test.tsx.snap
index c59c1a8f..3e1253ca 100644
--- a/packages/components/src/popover/__tests__/__snapshots__/Popover.test.tsx.snap
+++ b/packages/components/src/popover/__tests__/__snapshots__/Popover.test.tsx.snap
@@ -406,3 +406,68 @@ exports[`Popover trigger hover 2`] = `
+
+
+ click
+
+
+
+