@dr.pogodin/react-utils 1.31.0 → 1.31.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/build/development/index.js +1 -1
- package/build/development/index.js.map +1 -1
- package/build/development/server/index.js +1 -1
- package/build/development/server/index.js.map +1 -1
- package/build/development/server/utils/index.js +1 -1
- package/build/development/shared/components/Modal/index.js +17 -0
- package/build/development/shared/components/Modal/index.js.map +1 -1
- package/build/development/shared/components/WithTooltip/index.js +1 -1
- package/build/development/shared/components/WithTooltip/index.js.map +1 -1
- package/build/development/shared/components/index.js +1 -1
- package/build/development/shared/components/index.js.map +1 -1
- package/build/development/shared/components/selectors/CustomDropdown/Options/index.js +30 -29
- package/build/development/shared/components/selectors/CustomDropdown/Options/index.js.map +1 -1
- package/build/development/shared/components/selectors/CustomDropdown/index.js +68 -14
- package/build/development/shared/components/selectors/CustomDropdown/index.js.map +1 -1
- package/build/development/shared/components/selectors/common.js +4 -1
- package/build/development/shared/components/selectors/common.js.map +1 -1
- package/build/development/shared/utils/index.js +1 -1
- package/build/development/shared/utils/index.js.map +1 -1
- package/build/development/shared/utils/jest/E2eSsrEnv.js +3 -0
- package/build/development/shared/utils/jest/E2eSsrEnv.js.map +1 -1
- package/build/development/shared/utils/jest/index.js +1 -1
- package/build/development/shared/utils/jest/index.js.map +1 -1
- package/build/development/style.css +18 -2
- package/build/development/web.bundle.js +15 -15
- package/build/production/index.js +1 -1
- package/build/production/index.js.map +1 -1
- package/build/production/server/index.js +1 -1
- package/build/production/server/index.js.map +1 -1
- package/build/production/server/utils/index.js +1 -1
- package/build/production/shared/components/Modal/index.js +3 -2
- package/build/production/shared/components/Modal/index.js.map +1 -1
- package/build/production/shared/components/WithTooltip/index.js +1 -1
- package/build/production/shared/components/WithTooltip/index.js.map +1 -1
- package/build/production/shared/components/index.js +1 -1
- package/build/production/shared/components/index.js.map +1 -1
- package/build/production/shared/components/selectors/CustomDropdown/Options/index.js +2 -2
- package/build/production/shared/components/selectors/CustomDropdown/Options/index.js.map +1 -1
- package/build/production/shared/components/selectors/CustomDropdown/index.js +7 -3
- package/build/production/shared/components/selectors/CustomDropdown/index.js.map +1 -1
- package/build/production/shared/components/selectors/common.js +3 -1
- package/build/production/shared/components/selectors/common.js.map +1 -1
- package/build/production/shared/utils/index.js +1 -1
- package/build/production/shared/utils/index.js.map +1 -1
- package/build/production/shared/utils/jest/E2eSsrEnv.js +3 -1
- package/build/production/shared/utils/jest/E2eSsrEnv.js.map +1 -1
- package/build/production/shared/utils/jest/index.js +1 -1
- package/build/production/shared/utils/jest/index.js.map +1 -1
- package/build/production/style.css +1 -1
- package/build/production/style.css.map +1 -1
- package/build/production/web.bundle.js +1 -1
- package/build/production/web.bundle.js.map +1 -1
- package/build/types-code/shared/components/Modal/index.d.ts +1 -0
- package/build/types-code/shared/components/selectors/CustomDropdown/Options/index.d.ts +11 -6
- package/build/types-code/shared/components/selectors/common.d.ts +1 -1
- package/build/types-scss/src/shared/components/selectors/CustomDropdown/theme.scss.d.ts +1 -0
- package/package.json +27 -27
- package/src/shared/components/Modal/index.tsx +18 -0
- package/src/shared/components/selectors/CustomDropdown/Options/index.tsx +40 -34
- package/src/shared/components/selectors/CustomDropdown/index.tsx +73 -12
- package/src/shared/components/selectors/CustomDropdown/theme.scss +33 -5
- package/src/shared/components/selectors/common.ts +4 -0
- package/src/shared/utils/jest/E2eSsrEnv.ts +5 -1
|
@@ -2,6 +2,7 @@ import { type ReactNode } from 'react';
|
|
|
2
2
|
import { type Theme } from '@dr.pogodin/react-themes';
|
|
3
3
|
declare const validThemeKeys: readonly ["container", "overlay"];
|
|
4
4
|
type PropsT = {
|
|
5
|
+
cancelOnScrolling?: boolean;
|
|
5
6
|
children?: ReactNode;
|
|
6
7
|
containerStyle?: React.CSSProperties;
|
|
7
8
|
dontDisableScrolling?: boolean;
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
2
|
import { type OptionT, type OptionsT } from '../../common';
|
|
3
|
+
export type ContainerPosT = {
|
|
4
|
+
left: number;
|
|
5
|
+
top: number;
|
|
6
|
+
width: number;
|
|
7
|
+
};
|
|
8
|
+
export declare function areEqual(a?: ContainerPosT, b?: ContainerPosT): boolean;
|
|
9
|
+
export type RefT = {
|
|
10
|
+
measure: () => DOMRect | undefined;
|
|
11
|
+
};
|
|
3
12
|
type PropsT = {
|
|
4
|
-
anchorRect: {
|
|
5
|
-
bottom: number;
|
|
6
|
-
left: number;
|
|
7
|
-
width: number;
|
|
8
|
-
};
|
|
9
13
|
containerClass: string;
|
|
14
|
+
containerStyle?: ContainerPosT;
|
|
10
15
|
filter?: (item: OptionT<React.ReactNode> | string) => boolean;
|
|
11
16
|
optionClass: string;
|
|
12
17
|
options: OptionsT<React.ReactNode>;
|
|
13
18
|
onCancel: () => void;
|
|
14
19
|
onChange: (value: string) => void;
|
|
15
20
|
};
|
|
16
|
-
declare const Options: React.
|
|
21
|
+
declare const Options: React.ForwardRefExoticComponent<PropsT & React.RefAttributes<RefT>>;
|
|
17
22
|
export default Options;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
2
|
import PT from 'prop-types';
|
|
3
3
|
import type { Theme } from '@dr.pogodin/react-themes';
|
|
4
|
-
export declare const validThemeKeys: readonly ["active", "arrow", "container", "dropdown", "hiddenOption", "label", "option", "select"];
|
|
4
|
+
export declare const validThemeKeys: readonly ["active", "arrow", "container", "dropdown", "hiddenOption", "label", "option", "select", "upward"];
|
|
5
5
|
export type OptionT<NameT> = {
|
|
6
6
|
name?: NameT | null;
|
|
7
7
|
value: string;
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.31.
|
|
2
|
+
"version": "1.31.2",
|
|
3
3
|
"bin": {
|
|
4
4
|
"react-utils-build": "bin/build.js",
|
|
5
5
|
"react-utils-setup": "bin/setup.js"
|
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
"url": "https://github.com/birdofpreyru/react-utils/issues"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@babel/runtime": "^7.24.
|
|
11
|
+
"@babel/runtime": "^7.24.1",
|
|
12
12
|
"@dr.pogodin/babel-plugin-react-css-modules": "^6.12.0",
|
|
13
13
|
"@dr.pogodin/csurf": "^1.13.0",
|
|
14
14
|
"@dr.pogodin/js-utils": "^0.0.9",
|
|
15
15
|
"@dr.pogodin/react-global-state": "^0.13.0",
|
|
16
16
|
"@dr.pogodin/react-themes": "^1.6.0",
|
|
17
17
|
"@jest/environment": "^29.7.0",
|
|
18
|
-
"axios": "^1.6.
|
|
18
|
+
"axios": "^1.6.8",
|
|
19
19
|
"commander": "^12.0.0",
|
|
20
20
|
"compression": "^1.7.4",
|
|
21
21
|
"config": "^3.3.11",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"cookie-parser": "^1.4.6",
|
|
24
24
|
"cross-env": "^7.0.3",
|
|
25
25
|
"dayjs": "^1.11.10",
|
|
26
|
-
"express": "^4.
|
|
26
|
+
"express": "^4.19.2",
|
|
27
27
|
"helmet": "^7.1.0",
|
|
28
28
|
"http-status-codes": "^2.3.0",
|
|
29
29
|
"joi": "^17.12.2",
|
|
@@ -43,27 +43,27 @@
|
|
|
43
43
|
"serve-favicon": "^2.5.0",
|
|
44
44
|
"source-map-support": "^0.5.21",
|
|
45
45
|
"uuid": "^9.0.1",
|
|
46
|
-
"winston": "^3.
|
|
46
|
+
"winston": "^3.13.0"
|
|
47
47
|
},
|
|
48
48
|
"description": "Collection of generic ReactJS components and utils",
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@babel/cli": "^7.
|
|
51
|
-
"@babel/core": "^7.24.
|
|
52
|
-
"@babel/eslint-parser": "^7.
|
|
50
|
+
"@babel/cli": "^7.24.1",
|
|
51
|
+
"@babel/core": "^7.24.3",
|
|
52
|
+
"@babel/eslint-parser": "^7.24.1",
|
|
53
53
|
"@babel/eslint-plugin": "^7.23.5",
|
|
54
54
|
"@babel/node": "^7.23.9",
|
|
55
|
-
"@babel/plugin-transform-runtime": "^7.24.
|
|
56
|
-
"@babel/preset-env": "^7.24.
|
|
57
|
-
"@babel/preset-react": "^7.
|
|
58
|
-
"@babel/preset-typescript": "^7.
|
|
55
|
+
"@babel/plugin-transform-runtime": "^7.24.3",
|
|
56
|
+
"@babel/preset-env": "^7.24.3",
|
|
57
|
+
"@babel/preset-react": "^7.24.1",
|
|
58
|
+
"@babel/preset-typescript": "^7.24.1",
|
|
59
59
|
"@babel/register": "^7.23.7",
|
|
60
60
|
"@dr.pogodin/babel-plugin-transform-assets": "^1.2.2",
|
|
61
61
|
"@dr.pogodin/babel-preset-svgr": "^1.8.0",
|
|
62
62
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
|
|
63
|
-
"@tsconfig/recommended": "^1.0.
|
|
64
|
-
"@tsd/typescript": "^5.
|
|
63
|
+
"@tsconfig/recommended": "^1.0.4",
|
|
64
|
+
"@tsd/typescript": "^5.4.3",
|
|
65
65
|
"@types/compression": "^1.7.5",
|
|
66
|
-
"@types/config": "^3.3.
|
|
66
|
+
"@types/config": "^3.3.4",
|
|
67
67
|
"@types/cookie": "^0.6.0",
|
|
68
68
|
"@types/cookie-parser": "^1.4.7",
|
|
69
69
|
"@types/csurf": "^1.11.5",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"@types/morgan": "^1.9.9",
|
|
74
74
|
"@types/node-forge": "^1.3.11",
|
|
75
75
|
"@types/pretty": "^2.0.3",
|
|
76
|
-
"@types/react": "^18.2.
|
|
76
|
+
"@types/react": "^18.2.72",
|
|
77
77
|
"@types/react-dom": "^18.2.22",
|
|
78
78
|
"@types/react-helmet": "^6.1.11",
|
|
79
79
|
"@types/react-test-renderer": "^18.0.7",
|
|
@@ -83,11 +83,11 @@
|
|
|
83
83
|
"@types/supertest": "^6.0.2",
|
|
84
84
|
"@types/uuid": "^9.0.8",
|
|
85
85
|
"@types/webpack": "^5.28.5",
|
|
86
|
-
"autoprefixer": "^10.4.
|
|
86
|
+
"autoprefixer": "^10.4.19",
|
|
87
87
|
"babel-jest": "^29.7.0",
|
|
88
88
|
"babel-loader": "^9.1.3",
|
|
89
89
|
"babel-plugin-module-resolver": "^5.0.0",
|
|
90
|
-
"core-js": "^3.36.
|
|
90
|
+
"core-js": "^3.36.1",
|
|
91
91
|
"css-loader": "^6.10.0",
|
|
92
92
|
"css-minimizer-webpack-plugin": "^6.0.0",
|
|
93
93
|
"eslint": "^8.57.0",
|
|
@@ -97,16 +97,16 @@
|
|
|
97
97
|
"eslint-plugin-import": "^2.29.1",
|
|
98
98
|
"eslint-plugin-jest": "^27.9.0",
|
|
99
99
|
"eslint-plugin-jsx-a11y": "^6.8.0",
|
|
100
|
-
"eslint-plugin-react": "^7.34.
|
|
100
|
+
"eslint-plugin-react": "^7.34.1",
|
|
101
101
|
"eslint-plugin-react-hooks": "^4.6.0",
|
|
102
102
|
"identity-obj-proxy": "^3.0.0",
|
|
103
103
|
"jest": "^29.7.0",
|
|
104
104
|
"jest-environment-jsdom": "^29.7.0",
|
|
105
|
-
"memfs": "^4.
|
|
105
|
+
"memfs": "^4.8.0",
|
|
106
106
|
"mini-css-extract-plugin": "^2.8.1",
|
|
107
107
|
"mockdate": "^3.0.5",
|
|
108
108
|
"nodelist-foreach-polyfill": "^1.2.0",
|
|
109
|
-
"postcss": "^8.4.
|
|
109
|
+
"postcss": "^8.4.38",
|
|
110
110
|
"postcss-loader": "^8.1.1",
|
|
111
111
|
"postcss-scss": "^4.0.9",
|
|
112
112
|
"pretty": "^2.0.0",
|
|
@@ -117,15 +117,15 @@
|
|
|
117
117
|
"sass": "^1.72.0",
|
|
118
118
|
"sass-loader": "^14.1.1",
|
|
119
119
|
"sitemap": "^7.1.1",
|
|
120
|
-
"stylelint": "^16.
|
|
120
|
+
"stylelint": "^16.3.1",
|
|
121
121
|
"stylelint-config-standard-scss": "^13.0.0",
|
|
122
122
|
"supertest": "^6.3.4",
|
|
123
123
|
"tsc-alias": "^1.8.8",
|
|
124
|
-
"typed-scss-modules": "^8.0.
|
|
125
|
-
"typescript": "^5.4.
|
|
126
|
-
"typescript-eslint": "^7.
|
|
127
|
-
"webpack": "^5.
|
|
128
|
-
"webpack-dev-middleware": "^7.
|
|
124
|
+
"typed-scss-modules": "^8.0.1",
|
|
125
|
+
"typescript": "^5.4.3",
|
|
126
|
+
"typescript-eslint": "^7.4.0",
|
|
127
|
+
"webpack": "^5.91.0",
|
|
128
|
+
"webpack-dev-middleware": "^7.1.1",
|
|
129
129
|
"webpack-hot-middleware": "^2.26.1",
|
|
130
130
|
"webpack-merge": "^5.10.0",
|
|
131
131
|
"workbox-core": "^7.0.0",
|
|
@@ -20,6 +20,7 @@ import S from './styles.scss';
|
|
|
20
20
|
const validThemeKeys = ['container', 'overlay'] as const;
|
|
21
21
|
|
|
22
22
|
type PropsT = {
|
|
23
|
+
cancelOnScrolling?: boolean;
|
|
23
24
|
children?: ReactNode;
|
|
24
25
|
containerStyle?: React.CSSProperties;
|
|
25
26
|
dontDisableScrolling?: boolean;
|
|
@@ -39,6 +40,7 @@ type PropsT = {
|
|
|
39
40
|
* @param {ModalTheme} [props.theme] _Ad hoc_ theme.
|
|
40
41
|
*/
|
|
41
42
|
const BaseModal: React.FunctionComponent<PropsT> = ({
|
|
43
|
+
cancelOnScrolling,
|
|
42
44
|
children,
|
|
43
45
|
containerStyle,
|
|
44
46
|
dontDisableScrolling,
|
|
@@ -58,6 +60,20 @@ const BaseModal: React.FunctionComponent<PropsT> = ({
|
|
|
58
60
|
};
|
|
59
61
|
}, []);
|
|
60
62
|
|
|
63
|
+
// Sets up modal cancellation of scrolling, if opted-in.
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (cancelOnScrolling && onCancel) {
|
|
66
|
+
window.addEventListener('scroll', onCancel);
|
|
67
|
+
window.addEventListener('wheel', onCancel);
|
|
68
|
+
}
|
|
69
|
+
return () => {
|
|
70
|
+
if (cancelOnScrolling && onCancel) {
|
|
71
|
+
window.removeEventListener('scroll', onCancel);
|
|
72
|
+
window.removeEventListener('wheel', onCancel);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}, [cancelOnScrolling, onCancel]);
|
|
76
|
+
|
|
61
77
|
// Disables window scrolling, if it is not opted-out.
|
|
62
78
|
useEffect(() => {
|
|
63
79
|
if (!dontDisableScrolling) {
|
|
@@ -139,6 +155,7 @@ const ThemedModal = themed(
|
|
|
139
155
|
);
|
|
140
156
|
|
|
141
157
|
BaseModal.propTypes = {
|
|
158
|
+
cancelOnScrolling: PT.bool,
|
|
142
159
|
children: PT.node,
|
|
143
160
|
containerStyle: PT.shape({}),
|
|
144
161
|
dontDisableScrolling: PT.bool,
|
|
@@ -147,6 +164,7 @@ BaseModal.propTypes = {
|
|
|
147
164
|
};
|
|
148
165
|
|
|
149
166
|
BaseModal.defaultProps = {
|
|
167
|
+
cancelOnScrolling: false,
|
|
150
168
|
children: null,
|
|
151
169
|
containerStyle: undefined,
|
|
152
170
|
dontDisableScrolling: false,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import PT from 'prop-types';
|
|
2
|
-
import {
|
|
2
|
+
import { forwardRef, useImperativeHandle, useRef } from 'react';
|
|
3
3
|
|
|
4
4
|
import { BaseModal } from 'components/Modal';
|
|
5
5
|
|
|
@@ -12,13 +12,23 @@ import {
|
|
|
12
12
|
optionValueName,
|
|
13
13
|
} from '../../common';
|
|
14
14
|
|
|
15
|
+
export type ContainerPosT = {
|
|
16
|
+
left: number;
|
|
17
|
+
top: number;
|
|
18
|
+
width: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function areEqual(a?: ContainerPosT, b?: ContainerPosT): boolean {
|
|
22
|
+
return a?.left === b?.left && a?.top === b?.top && a?.width === b?.width;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type RefT = {
|
|
26
|
+
measure: () => DOMRect | undefined;
|
|
27
|
+
};
|
|
28
|
+
|
|
15
29
|
type PropsT = {
|
|
16
|
-
anchorRect: {
|
|
17
|
-
bottom: number;
|
|
18
|
-
left: number;
|
|
19
|
-
width: number;
|
|
20
|
-
};
|
|
21
30
|
containerClass: string;
|
|
31
|
+
containerStyle?: ContainerPosT;
|
|
22
32
|
filter?: (item: OptionT<React.ReactNode> | string) => boolean;
|
|
23
33
|
optionClass: string;
|
|
24
34
|
options: OptionsT<React.ReactNode>;
|
|
@@ -26,29 +36,20 @@ type PropsT = {
|
|
|
26
36
|
onChange: (value: string) => void;
|
|
27
37
|
};
|
|
28
38
|
|
|
29
|
-
const Options
|
|
30
|
-
anchorRect,
|
|
39
|
+
const Options = forwardRef<RefT, PropsT>(({
|
|
31
40
|
containerClass,
|
|
41
|
+
containerStyle,
|
|
32
42
|
filter,
|
|
33
43
|
onCancel,
|
|
34
44
|
onChange,
|
|
35
45
|
optionClass,
|
|
36
46
|
options,
|
|
37
|
-
}) => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
const listener = () => {
|
|
45
|
-
onCancel();
|
|
46
|
-
};
|
|
47
|
-
window.addEventListener('scroll', listener);
|
|
48
|
-
return () => {
|
|
49
|
-
window.removeEventListener('scroll', listener);
|
|
50
|
-
};
|
|
51
|
-
}, [onCancel]);
|
|
47
|
+
}, ref) => {
|
|
48
|
+
const opsRef = useRef<HTMLDivElement>(null);
|
|
49
|
+
|
|
50
|
+
useImperativeHandle(ref, () => ({
|
|
51
|
+
measure: () => opsRef.current?.getBoundingClientRect(),
|
|
52
|
+
}), []);
|
|
52
53
|
|
|
53
54
|
const optionNodes: React.ReactNode[] = [];
|
|
54
55
|
for (let i = 0; i < options.length; ++i) {
|
|
@@ -76,11 +77,13 @@ const Options: React.FunctionComponent<PropsT> = ({
|
|
|
76
77
|
|
|
77
78
|
return (
|
|
78
79
|
<BaseModal
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
// Closes the dropdown (cancels the selection) on any page scrolling attempt.
|
|
81
|
+
// This is the same native <select> elements do on scrolling, and at least for
|
|
82
|
+
// now we have no reason to deal with complications needed to support open
|
|
83
|
+
// dropdowns during the scrolling (that would need to re-position it in
|
|
84
|
+
// response to the position changes of the root dropdown element).
|
|
85
|
+
cancelOnScrolling
|
|
86
|
+
containerStyle={containerStyle}
|
|
84
87
|
dontDisableScrolling
|
|
85
88
|
onCancel={onCancel}
|
|
86
89
|
theme={{
|
|
@@ -91,18 +94,20 @@ const Options: React.FunctionComponent<PropsT> = ({
|
|
|
91
94
|
overlay: S.overlay,
|
|
92
95
|
}}
|
|
93
96
|
>
|
|
94
|
-
{optionNodes}
|
|
97
|
+
<div ref={opsRef}>{optionNodes}</div>
|
|
95
98
|
</BaseModal>
|
|
96
99
|
);
|
|
97
|
-
};
|
|
100
|
+
});
|
|
98
101
|
|
|
99
102
|
Options.propTypes = {
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
containerClass: PT.string.isRequired,
|
|
104
|
+
|
|
105
|
+
containerStyle: PT.shape({
|
|
102
106
|
left: PT.number.isRequired,
|
|
107
|
+
top: PT.number.isRequired,
|
|
103
108
|
width: PT.number.isRequired,
|
|
104
|
-
})
|
|
105
|
-
|
|
109
|
+
}),
|
|
110
|
+
|
|
106
111
|
filter: PT.func,
|
|
107
112
|
onCancel: PT.func.isRequired,
|
|
108
113
|
onChange: PT.func.isRequired,
|
|
@@ -111,6 +116,7 @@ Options.propTypes = {
|
|
|
111
116
|
};
|
|
112
117
|
|
|
113
118
|
Options.defaultProps = {
|
|
119
|
+
containerStyle: undefined,
|
|
114
120
|
filter: undefined,
|
|
115
121
|
};
|
|
116
122
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import PT from 'prop-types';
|
|
2
|
-
import { useRef, useState } from 'react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
|
|
4
4
|
import themed from '@dr.pogodin/react-themes';
|
|
5
5
|
|
|
6
|
-
import Options from './Options';
|
|
6
|
+
import Options, { type ContainerPosT, type RefT, areEqual } from './Options';
|
|
7
7
|
|
|
8
8
|
import defaultTheme from './theme.scss';
|
|
9
9
|
|
|
@@ -26,14 +26,66 @@ PropsT<React.ReactNode, (value: string) => void>
|
|
|
26
26
|
}) => {
|
|
27
27
|
if (!options) throw Error('Internal error');
|
|
28
28
|
|
|
29
|
+
const [active, setActive] = useState(false);
|
|
30
|
+
|
|
29
31
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
32
|
+
const opsRef = useRef<RefT>(null);
|
|
33
|
+
|
|
34
|
+
const [opsPos, setOpsPos] = useState<ContainerPosT>();
|
|
35
|
+
const [upward, setUpward] = useState(false);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!active) return undefined;
|
|
39
|
+
|
|
40
|
+
let id: number;
|
|
41
|
+
const cb = () => {
|
|
42
|
+
const anchor = dropdownRef.current?.getBoundingClientRect();
|
|
43
|
+
const opsRect = opsRef.current?.measure();
|
|
44
|
+
if (anchor && opsRect) {
|
|
45
|
+
const fitsDown = anchor.bottom + opsRect.height
|
|
46
|
+
< (window.visualViewport?.height ?? 0);
|
|
47
|
+
const fitsUp = anchor.top - opsRect.height > 0;
|
|
48
|
+
|
|
49
|
+
const up = !fitsDown && fitsUp;
|
|
50
|
+
setUpward(up);
|
|
51
|
+
|
|
52
|
+
const pos = up ? {
|
|
53
|
+
top: anchor.top - opsRect.height - 1,
|
|
54
|
+
left: anchor.left,
|
|
55
|
+
width: anchor.width,
|
|
56
|
+
} : {
|
|
57
|
+
left: anchor.left,
|
|
58
|
+
top: anchor.bottom,
|
|
59
|
+
width: anchor.width,
|
|
60
|
+
};
|
|
30
61
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
62
|
+
setOpsPos((now) => (areEqual(now, pos) ? now : pos));
|
|
63
|
+
}
|
|
64
|
+
id = requestAnimationFrame(cb);
|
|
65
|
+
};
|
|
66
|
+
requestAnimationFrame(cb);
|
|
67
|
+
|
|
68
|
+
return () => {
|
|
69
|
+
cancelAnimationFrame(id);
|
|
70
|
+
};
|
|
71
|
+
}, [active]);
|
|
34
72
|
|
|
35
73
|
const openList = () => {
|
|
36
|
-
|
|
74
|
+
const view = window.visualViewport;
|
|
75
|
+
const rect = dropdownRef.current!.getBoundingClientRect();
|
|
76
|
+
setActive(true);
|
|
77
|
+
|
|
78
|
+
// NOTE: This first opens the dropdown off-screen, where it is measured
|
|
79
|
+
// by an effect declared above, and then positioned below, or above
|
|
80
|
+
// the original dropdown element, depending where it fits best
|
|
81
|
+
// (if we first open it downward, it would flick if we immediately
|
|
82
|
+
// move it above, at least with the current position update via local
|
|
83
|
+
// react state, and not imperatively).
|
|
84
|
+
setOpsPos({
|
|
85
|
+
left: view?.width || 0,
|
|
86
|
+
top: view?.height || 0,
|
|
87
|
+
width: rect.width,
|
|
88
|
+
});
|
|
37
89
|
};
|
|
38
90
|
|
|
39
91
|
let selected: React.ReactNode = <>‌</>;
|
|
@@ -49,7 +101,13 @@ PropsT<React.ReactNode, (value: string) => void>
|
|
|
49
101
|
}
|
|
50
102
|
|
|
51
103
|
let containerClassName = theme.container;
|
|
52
|
-
if (
|
|
104
|
+
if (active) containerClassName += ` ${theme.active}`;
|
|
105
|
+
|
|
106
|
+
let opsContainerClass = theme.select || '';
|
|
107
|
+
if (upward) {
|
|
108
|
+
containerClassName += ` ${theme.upward}`;
|
|
109
|
+
opsContainerClass += ` ${theme.upward}`;
|
|
110
|
+
}
|
|
53
111
|
|
|
54
112
|
return (
|
|
55
113
|
<div className={containerClassName}>
|
|
@@ -70,17 +128,20 @@ PropsT<React.ReactNode, (value: string) => void>
|
|
|
70
128
|
<div className={theme.arrow} />
|
|
71
129
|
</div>
|
|
72
130
|
{
|
|
73
|
-
|
|
131
|
+
active ? (
|
|
74
132
|
<Options
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
onCancel={() =>
|
|
133
|
+
containerClass={opsContainerClass}
|
|
134
|
+
containerStyle={opsPos}
|
|
135
|
+
onCancel={() => {
|
|
136
|
+
setActive(false);
|
|
137
|
+
}}
|
|
78
138
|
onChange={(newValue) => {
|
|
79
|
-
|
|
139
|
+
setActive(false);
|
|
80
140
|
if (onChange) onChange(newValue);
|
|
81
141
|
}}
|
|
82
142
|
optionClass={theme.option || ''}
|
|
83
143
|
options={options}
|
|
144
|
+
ref={opsRef}
|
|
84
145
|
/>
|
|
85
146
|
) : null
|
|
86
147
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
$border: 1px solid gray;
|
|
2
|
+
|
|
1
3
|
*,
|
|
2
4
|
.context,
|
|
3
5
|
.ad.hoc {
|
|
@@ -19,7 +21,7 @@
|
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
&.dropdown {
|
|
22
|
-
border:
|
|
24
|
+
border: $border;
|
|
23
25
|
border-radius: 0.3em;
|
|
24
26
|
cursor: pointer;
|
|
25
27
|
min-width: 200px;
|
|
@@ -52,19 +54,17 @@
|
|
|
52
54
|
|
|
53
55
|
&.select {
|
|
54
56
|
background: white;
|
|
55
|
-
border:
|
|
57
|
+
border: $border;
|
|
56
58
|
border-radius: 0 0 0.3em 0.3em;
|
|
57
59
|
border-top: none;
|
|
58
60
|
box-shadow: 0 6px 12px 3px lightgray;
|
|
59
61
|
position: fixed;
|
|
60
|
-
top: 20px;
|
|
61
|
-
left: 10px;
|
|
62
62
|
z-index: 1001;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
&.arrow {
|
|
66
66
|
background-image: linear-gradient(to top, lightgray, white 50%, white);
|
|
67
|
-
border-left:
|
|
67
|
+
border-left: $border;
|
|
68
68
|
border-radius: 0 0.3em 0.3em 0;
|
|
69
69
|
bottom: 0;
|
|
70
70
|
padding: 0.3em 0.6em;
|
|
@@ -87,4 +87,32 @@
|
|
|
87
87
|
border-radius: 0.3em 0.3em 0 0;
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
+
|
|
91
|
+
&.upward {
|
|
92
|
+
&.active {
|
|
93
|
+
// NOTE: Here StyleLint complains about order & specifity of selectors in
|
|
94
|
+
// the compiled CSS, but it should have no effect on the actual styling.
|
|
95
|
+
// stylelint-disable no-descending-specificity
|
|
96
|
+
.arrow {
|
|
97
|
+
border-radius: 0 0 0.3em;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.dropdown {
|
|
101
|
+
border-radius: 0 0 0.3em 0.3em;
|
|
102
|
+
}
|
|
103
|
+
// stylelint-enable no-descending-specificity
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
&.select {
|
|
107
|
+
border-bottom: none;
|
|
108
|
+
border-top: $border;
|
|
109
|
+
border-radius: 0.3em 0.3em 0 0;
|
|
110
|
+
|
|
111
|
+
// NOTE: Here a normal (downward) shadow would weirdly cast over
|
|
112
|
+
// the dropdown element, and other ways to cast the shadow result in
|
|
113
|
+
// "upward" shadow, which is also weird. Thus, better no shadow at all
|
|
114
|
+
// for the upward-opened dropdown.
|
|
115
|
+
box-shadow: none;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
90
118
|
}
|
|
@@ -13,6 +13,10 @@ export const validThemeKeys = [
|
|
|
13
13
|
'label',
|
|
14
14
|
'option',
|
|
15
15
|
'select',
|
|
16
|
+
|
|
17
|
+
// TODO: This is only valid for <CustomDropdown>, thus we need to re-factor it
|
|
18
|
+
// into a separate theme spec for that component.
|
|
19
|
+
'upward',
|
|
16
20
|
] as const;
|
|
17
21
|
|
|
18
22
|
export type OptionT<NameT> = {
|
|
@@ -87,7 +87,11 @@ export default class E2eSsrEnv extends JsdomEnv {
|
|
|
87
87
|
this.loadWebpackConfig();
|
|
88
88
|
|
|
89
89
|
const compiler = webpack(this.global.webpackConfig as webpack.Configuration);
|
|
90
|
-
|
|
90
|
+
|
|
91
|
+
// TODO: The "as typeof compiler.outputFileSystem" piece below is a workaround
|
|
92
|
+
// for the Webpack regression: https://github.com/webpack/webpack/issues/18242
|
|
93
|
+
compiler.outputFileSystem = this.global.webpackOutputFs as typeof compiler.outputFileSystem;
|
|
94
|
+
|
|
91
95
|
return new Promise<void>((done, fail) => {
|
|
92
96
|
compiler.run((err, stats) => {
|
|
93
97
|
if (err) fail(err);
|