@dr.pogodin/react-utils 1.31.1 → 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/shared/components/Modal/index.js +2 -0
- package/build/development/shared/components/Modal/index.js.map +1 -1
- package/build/development/shared/components/selectors/CustomDropdown/Options/index.js +23 -14
- 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/style.css +18 -2
- package/build/development/web.bundle.js +5 -5
- package/build/production/shared/components/Modal/index.js +1 -1
- package/build/production/shared/components/Modal/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/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/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 +3 -3
- package/src/shared/components/Modal/index.tsx +2 -0
- package/src/shared/components/selectors/CustomDropdown/Options/index.tsx +35 -19
- 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
|
@@ -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"
|
|
@@ -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",
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
"sass": "^1.72.0",
|
|
118
118
|
"sass-loader": "^14.1.1",
|
|
119
119
|
"sitemap": "^7.1.1",
|
|
120
|
-
"stylelint": "^16.3.
|
|
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",
|
|
@@ -64,10 +64,12 @@ const BaseModal: React.FunctionComponent<PropsT> = ({
|
|
|
64
64
|
useEffect(() => {
|
|
65
65
|
if (cancelOnScrolling && onCancel) {
|
|
66
66
|
window.addEventListener('scroll', onCancel);
|
|
67
|
+
window.addEventListener('wheel', onCancel);
|
|
67
68
|
}
|
|
68
69
|
return () => {
|
|
69
70
|
if (cancelOnScrolling && onCancel) {
|
|
70
71
|
window.removeEventListener('scroll', onCancel);
|
|
72
|
+
window.removeEventListener('wheel', onCancel);
|
|
71
73
|
}
|
|
72
74
|
};
|
|
73
75
|
}, [cancelOnScrolling, onCancel]);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import PT from 'prop-types';
|
|
2
|
+
import { forwardRef, useImperativeHandle, useRef } from 'react';
|
|
2
3
|
|
|
3
4
|
import { BaseModal } from 'components/Modal';
|
|
4
5
|
|
|
@@ -11,13 +12,23 @@ import {
|
|
|
11
12
|
optionValueName,
|
|
12
13
|
} from '../../common';
|
|
13
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
|
+
|
|
14
29
|
type PropsT = {
|
|
15
|
-
anchorRect: {
|
|
16
|
-
bottom: number;
|
|
17
|
-
left: number;
|
|
18
|
-
width: number;
|
|
19
|
-
};
|
|
20
30
|
containerClass: string;
|
|
31
|
+
containerStyle?: ContainerPosT;
|
|
21
32
|
filter?: (item: OptionT<React.ReactNode> | string) => boolean;
|
|
22
33
|
optionClass: string;
|
|
23
34
|
options: OptionsT<React.ReactNode>;
|
|
@@ -25,15 +36,21 @@ type PropsT = {
|
|
|
25
36
|
onChange: (value: string) => void;
|
|
26
37
|
};
|
|
27
38
|
|
|
28
|
-
const Options
|
|
29
|
-
anchorRect,
|
|
39
|
+
const Options = forwardRef<RefT, PropsT>(({
|
|
30
40
|
containerClass,
|
|
41
|
+
containerStyle,
|
|
31
42
|
filter,
|
|
32
43
|
onCancel,
|
|
33
44
|
onChange,
|
|
34
45
|
optionClass,
|
|
35
46
|
options,
|
|
36
|
-
}) => {
|
|
47
|
+
}, ref) => {
|
|
48
|
+
const opsRef = useRef<HTMLDivElement>(null);
|
|
49
|
+
|
|
50
|
+
useImperativeHandle(ref, () => ({
|
|
51
|
+
measure: () => opsRef.current?.getBoundingClientRect(),
|
|
52
|
+
}), []);
|
|
53
|
+
|
|
37
54
|
const optionNodes: React.ReactNode[] = [];
|
|
38
55
|
for (let i = 0; i < options.length; ++i) {
|
|
39
56
|
const option = options[i];
|
|
@@ -66,11 +83,7 @@ const Options: React.FunctionComponent<PropsT> = ({
|
|
|
66
83
|
// dropdowns during the scrolling (that would need to re-position it in
|
|
67
84
|
// response to the position changes of the root dropdown element).
|
|
68
85
|
cancelOnScrolling
|
|
69
|
-
containerStyle={
|
|
70
|
-
left: anchorRect.left,
|
|
71
|
-
top: anchorRect.bottom,
|
|
72
|
-
width: anchorRect.width,
|
|
73
|
-
}}
|
|
86
|
+
containerStyle={containerStyle}
|
|
74
87
|
dontDisableScrolling
|
|
75
88
|
onCancel={onCancel}
|
|
76
89
|
theme={{
|
|
@@ -81,18 +94,20 @@ const Options: React.FunctionComponent<PropsT> = ({
|
|
|
81
94
|
overlay: S.overlay,
|
|
82
95
|
}}
|
|
83
96
|
>
|
|
84
|
-
{optionNodes}
|
|
97
|
+
<div ref={opsRef}>{optionNodes}</div>
|
|
85
98
|
</BaseModal>
|
|
86
99
|
);
|
|
87
|
-
};
|
|
100
|
+
});
|
|
88
101
|
|
|
89
102
|
Options.propTypes = {
|
|
90
|
-
|
|
91
|
-
|
|
103
|
+
containerClass: PT.string.isRequired,
|
|
104
|
+
|
|
105
|
+
containerStyle: PT.shape({
|
|
92
106
|
left: PT.number.isRequired,
|
|
107
|
+
top: PT.number.isRequired,
|
|
93
108
|
width: PT.number.isRequired,
|
|
94
|
-
})
|
|
95
|
-
|
|
109
|
+
}),
|
|
110
|
+
|
|
96
111
|
filter: PT.func,
|
|
97
112
|
onCancel: PT.func.isRequired,
|
|
98
113
|
onChange: PT.func.isRequired,
|
|
@@ -101,6 +116,7 @@ Options.propTypes = {
|
|
|
101
116
|
};
|
|
102
117
|
|
|
103
118
|
Options.defaultProps = {
|
|
119
|
+
containerStyle: undefined,
|
|
104
120
|
filter: undefined,
|
|
105
121
|
};
|
|
106
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> = {
|