@dr.pogodin/react-utils 1.30.2 → 1.31.1
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/bin/build.js +5 -0
- package/build/development/client/index.js +1 -1
- package/build/development/client/index.js.map +1 -1
- package/build/development/index.js +8 -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/Checkbox/index.js +2 -2
- package/build/development/shared/components/Checkbox/index.js.map +1 -1
- package/build/development/shared/components/Input/index.js +2 -2
- package/build/development/shared/components/Input/index.js.map +1 -1
- package/build/development/shared/components/Modal/index.js +40 -5
- package/build/development/shared/components/Modal/index.js.map +1 -1
- package/build/development/shared/components/TextArea/index.js +5 -0
- package/build/development/shared/components/TextArea/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/YouTubeVideo/index.js +1 -3
- package/build/development/shared/components/YouTubeVideo/index.js.map +1 -1
- package/build/development/shared/components/index.js +28 -15
- package/build/development/shared/components/index.js.map +1 -1
- package/build/development/shared/components/selectors/CustomDropdown/Options/index.js +85 -0
- package/build/development/shared/components/selectors/CustomDropdown/Options/index.js.map +1 -0
- package/build/development/shared/components/selectors/CustomDropdown/index.js +105 -0
- package/build/development/shared/components/selectors/CustomDropdown/index.js.map +1 -0
- package/build/development/shared/components/{Dropdown → selectors/NativeDropdown}/index.js +25 -34
- package/build/development/shared/components/selectors/NativeDropdown/index.js.map +1 -0
- package/build/development/shared/components/selectors/Switch/index.js +76 -0
- package/build/development/shared/components/selectors/Switch/index.js.map +1 -0
- package/build/development/shared/components/selectors/common.js +24 -0
- package/build/development/shared/components/selectors/common.js.map +1 -0
- package/build/development/shared/components/selectors/index.js +28 -0
- package/build/development/shared/components/selectors/index.js.map +1 -0
- 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 +387 -225
- package/build/development/web.bundle.js +113 -53
- package/build/production/client/index.js +1 -1
- package/build/production/client/index.js.map +1 -1
- 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/Checkbox/index.js +2 -2
- package/build/production/shared/components/Checkbox/index.js.map +1 -1
- package/build/production/shared/components/Input/index.js +1 -1
- package/build/production/shared/components/Input/index.js.map +1 -1
- package/build/production/shared/components/Modal/index.js +4 -2
- package/build/production/shared/components/Modal/index.js.map +1 -1
- package/build/production/shared/components/TextArea/index.js +3 -3
- package/build/production/shared/components/TextArea/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/YouTubeVideo/index.js +2 -2
- package/build/production/shared/components/YouTubeVideo/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 +7 -0
- package/build/production/shared/components/selectors/CustomDropdown/Options/index.js.map +1 -0
- package/build/production/shared/components/selectors/CustomDropdown/index.js +4 -0
- package/build/production/shared/components/selectors/CustomDropdown/index.js.map +1 -0
- package/build/production/shared/components/selectors/NativeDropdown/index.js +25 -0
- package/build/production/shared/components/selectors/NativeDropdown/index.js.map +1 -0
- package/build/production/shared/components/selectors/Switch/index.js +2 -0
- package/build/production/shared/components/selectors/Switch/index.js.map +1 -0
- package/build/production/shared/components/selectors/common.js +3 -0
- package/build/production/shared/components/selectors/common.js.map +1 -0
- package/build/production/shared/components/selectors/index.js +2 -0
- package/build/production/shared/components/selectors/index.js.map +1 -0
- 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/client/index.d.ts +1 -0
- package/build/types-code/index.d.ts +1 -1
- package/build/types-code/shared/components/Checkbox/index.d.ts +1 -1
- package/build/types-code/shared/components/Input/index.d.ts +1 -1
- package/build/types-code/shared/components/Modal/index.d.ts +3 -1
- package/build/types-code/shared/components/TextArea/index.d.ts +1 -0
- package/build/types-code/shared/components/index.d.ts +1 -2
- package/build/types-code/shared/components/selectors/CustomDropdown/Options/index.d.ts +17 -0
- package/build/types-code/shared/components/selectors/CustomDropdown/index.d.ts +4 -0
- package/build/types-code/shared/components/selectors/NativeDropdown/index.d.ts +3 -0
- package/build/types-code/shared/components/selectors/Switch/index.d.ts +13 -0
- package/build/types-code/shared/components/selectors/common.d.ts +27 -0
- package/build/types-code/shared/components/selectors/index.d.ts +3 -0
- package/build/types-scss/src/shared/components/Modal/styles.scss.d.ts +1 -0
- package/build/types-scss/src/shared/components/selectors/CustomDropdown/Options/style.scss.d.ts +1 -0
- package/build/types-scss/src/shared/components/selectors/CustomDropdown/theme.scss.d.ts +10 -0
- package/build/types-scss/src/shared/components/{Dropdown → selectors/NativeDropdown}/theme.scss.d.ts +1 -0
- package/build/types-scss/src/shared/components/selectors/Switch/theme.scss.d.ts +6 -0
- package/package.json +30 -30
- package/src/client/index.tsx +2 -1
- package/src/index.ts +1 -0
- package/src/shared/components/Button/style.scss +1 -0
- package/src/shared/components/Checkbox/index.tsx +3 -3
- package/src/shared/components/Input/index.tsx +3 -3
- package/src/shared/components/Modal/base-theme.scss +1 -1
- package/src/shared/components/Modal/index.tsx +40 -5
- package/src/shared/components/Modal/styles.scss +2 -4
- package/src/shared/components/TextArea/index.tsx +5 -0
- package/src/shared/components/TextArea/style.scss +8 -0
- package/src/shared/components/YouTubeVideo/base.scss +3 -1
- package/src/shared/components/YouTubeVideo/index.tsx +2 -3
- package/src/shared/components/index.ts +2 -2
- package/src/shared/components/selectors/CustomDropdown/Options/index.tsx +107 -0
- package/src/shared/components/selectors/CustomDropdown/Options/style.scss +6 -0
- package/src/shared/components/selectors/CustomDropdown/index.tsx +115 -0
- package/src/shared/components/selectors/CustomDropdown/theme.scss +90 -0
- package/src/shared/components/{Dropdown → selectors/NativeDropdown}/index.tsx +21 -50
- package/src/shared/components/{Dropdown → selectors/NativeDropdown}/theme.scss +5 -0
- package/src/shared/components/selectors/Switch/index.tsx +94 -0
- package/src/shared/components/selectors/Switch/theme.scss +39 -0
- package/src/shared/components/selectors/common.ts +54 -0
- package/src/shared/components/selectors/index.ts +3 -0
- package/src/shared/utils/jest/E2eSsrEnv.ts +5 -1
- package/build/development/shared/components/Dropdown/index.js.map +0 -1
- package/build/development/shared/components/ScalableRect/index.js +0 -80
- package/build/development/shared/components/ScalableRect/index.js.map +0 -1
- package/build/production/shared/components/Dropdown/index.js +0 -24
- package/build/production/shared/components/Dropdown/index.js.map +0 -1
- package/build/production/shared/components/ScalableRect/index.js +0 -21
- package/build/production/shared/components/ScalableRect/index.js.map +0 -1
- package/build/types-code/shared/components/Dropdown/index.d.ts +0 -17
- package/build/types-code/shared/components/ScalableRect/index.d.ts +0 -19
- package/build/types-scss/src/shared/components/ScalableRect/style.scss.d.ts +0 -2
- package/src/shared/components/ScalableRect/index.tsx +0 -84
- package/src/shared/components/ScalableRect/style.scss +0 -10
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
* Just an aggregation of all exported components into a single module.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
export * from 'components/selectors';
|
|
6
|
+
|
|
5
7
|
export { default as Button } from 'components/Button';
|
|
6
8
|
export { default as Checkbox } from 'components/Checkbox';
|
|
7
|
-
export { default as Dropdown } from 'components/Dropdown';
|
|
8
9
|
export { default as Input } from 'components/Input';
|
|
9
10
|
export { default as Link } from 'components/Link';
|
|
10
11
|
export { default as PageLayout } from 'components/PageLayout';
|
|
11
12
|
export { default as MetaTags } from 'components/MetaTags';
|
|
12
13
|
export { default as Modal, BaseModal } from 'components/Modal';
|
|
13
14
|
export { default as NavLink } from 'components/NavLink';
|
|
14
|
-
export { default as ScalableRect } from 'components/ScalableRect';
|
|
15
15
|
export { default as Throbber } from 'components/Throbber';
|
|
16
16
|
export { default as WithTooltip } from 'components/WithTooltip';
|
|
17
17
|
export { default as YouTubeVideo } from 'components/YouTubeVideo';
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import PT from 'prop-types';
|
|
2
|
+
|
|
3
|
+
import { BaseModal } from 'components/Modal';
|
|
4
|
+
|
|
5
|
+
import S from './style.scss';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
type OptionT,
|
|
9
|
+
type OptionsT,
|
|
10
|
+
optionsValidator,
|
|
11
|
+
optionValueName,
|
|
12
|
+
} from '../../common';
|
|
13
|
+
|
|
14
|
+
type PropsT = {
|
|
15
|
+
anchorRect: {
|
|
16
|
+
bottom: number;
|
|
17
|
+
left: number;
|
|
18
|
+
width: number;
|
|
19
|
+
};
|
|
20
|
+
containerClass: string;
|
|
21
|
+
filter?: (item: OptionT<React.ReactNode> | string) => boolean;
|
|
22
|
+
optionClass: string;
|
|
23
|
+
options: OptionsT<React.ReactNode>;
|
|
24
|
+
onCancel: () => void;
|
|
25
|
+
onChange: (value: string) => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const Options: React.FunctionComponent<PropsT> = ({
|
|
29
|
+
anchorRect,
|
|
30
|
+
containerClass,
|
|
31
|
+
filter,
|
|
32
|
+
onCancel,
|
|
33
|
+
onChange,
|
|
34
|
+
optionClass,
|
|
35
|
+
options,
|
|
36
|
+
}) => {
|
|
37
|
+
const optionNodes: React.ReactNode[] = [];
|
|
38
|
+
for (let i = 0; i < options.length; ++i) {
|
|
39
|
+
const option = options[i];
|
|
40
|
+
if (!filter || filter(option)) {
|
|
41
|
+
const [iValue, iName] = optionValueName(option);
|
|
42
|
+
optionNodes.push(
|
|
43
|
+
<div
|
|
44
|
+
className={optionClass}
|
|
45
|
+
onClick={() => onChange(iValue)}
|
|
46
|
+
onKeyDown={(e) => {
|
|
47
|
+
if (e.key === 'Enter') {
|
|
48
|
+
onChange(iValue);
|
|
49
|
+
}
|
|
50
|
+
}}
|
|
51
|
+
key={iValue}
|
|
52
|
+
role="button"
|
|
53
|
+
tabIndex={0}
|
|
54
|
+
>
|
|
55
|
+
{iName}
|
|
56
|
+
</div>,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<BaseModal
|
|
63
|
+
// Closes the dropdown (cancels the selection) on any page scrolling attempt.
|
|
64
|
+
// This is the same native <select> elements do on scrolling, and at least for
|
|
65
|
+
// now we have no reason to deal with complications needed to support open
|
|
66
|
+
// dropdowns during the scrolling (that would need to re-position it in
|
|
67
|
+
// response to the position changes of the root dropdown element).
|
|
68
|
+
cancelOnScrolling
|
|
69
|
+
containerStyle={{
|
|
70
|
+
left: anchorRect.left,
|
|
71
|
+
top: anchorRect.bottom,
|
|
72
|
+
width: anchorRect.width,
|
|
73
|
+
}}
|
|
74
|
+
dontDisableScrolling
|
|
75
|
+
onCancel={onCancel}
|
|
76
|
+
theme={{
|
|
77
|
+
ad: '',
|
|
78
|
+
hoc: '',
|
|
79
|
+
container: containerClass,
|
|
80
|
+
context: '',
|
|
81
|
+
overlay: S.overlay,
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
{optionNodes}
|
|
85
|
+
</BaseModal>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
Options.propTypes = {
|
|
90
|
+
anchorRect: PT.shape({
|
|
91
|
+
bottom: PT.number.isRequired,
|
|
92
|
+
left: PT.number.isRequired,
|
|
93
|
+
width: PT.number.isRequired,
|
|
94
|
+
}).isRequired,
|
|
95
|
+
containerClass: PT.string.isRequired,
|
|
96
|
+
filter: PT.func,
|
|
97
|
+
onCancel: PT.func.isRequired,
|
|
98
|
+
onChange: PT.func.isRequired,
|
|
99
|
+
optionClass: PT.string.isRequired,
|
|
100
|
+
options: optionsValidator.isRequired,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
Options.defaultProps = {
|
|
104
|
+
filter: undefined,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export default Options;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import PT from 'prop-types';
|
|
2
|
+
import { useRef, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import themed from '@dr.pogodin/react-themes';
|
|
5
|
+
|
|
6
|
+
import Options from './Options';
|
|
7
|
+
|
|
8
|
+
import defaultTheme from './theme.scss';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
type PropsT,
|
|
12
|
+
optionValidator,
|
|
13
|
+
optionValueName,
|
|
14
|
+
validThemeKeys,
|
|
15
|
+
} from '../common';
|
|
16
|
+
|
|
17
|
+
const BaseCustomDropdown: React.FunctionComponent<
|
|
18
|
+
PropsT<React.ReactNode, (value: string) => void>
|
|
19
|
+
> = ({
|
|
20
|
+
filter,
|
|
21
|
+
label,
|
|
22
|
+
onChange,
|
|
23
|
+
options,
|
|
24
|
+
theme,
|
|
25
|
+
value,
|
|
26
|
+
}) => {
|
|
27
|
+
if (!options) throw Error('Internal error');
|
|
28
|
+
|
|
29
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
30
|
+
|
|
31
|
+
// If "null" the dropdown is closed, otherwise it is displayed
|
|
32
|
+
// at the specified coordinates.
|
|
33
|
+
const [anchor, setAnchor] = useState<DOMRect | null>(null);
|
|
34
|
+
|
|
35
|
+
const openList = () => {
|
|
36
|
+
setAnchor(dropdownRef.current!.getBoundingClientRect());
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
let selected: React.ReactNode = <>‌</>;
|
|
40
|
+
for (let i = 0; i < options.length; ++i) {
|
|
41
|
+
const option = options[i];
|
|
42
|
+
if (!filter || filter(option)) {
|
|
43
|
+
const [iValue, iName] = optionValueName(option);
|
|
44
|
+
if (iValue === value) {
|
|
45
|
+
selected = iName;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let containerClassName = theme.container;
|
|
52
|
+
if (anchor) containerClassName += ` ${theme.active}`;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className={containerClassName}>
|
|
56
|
+
{label === undefined ? null : (
|
|
57
|
+
<div className={theme.label}>{label}</div>
|
|
58
|
+
)}
|
|
59
|
+
<div
|
|
60
|
+
className={theme.dropdown}
|
|
61
|
+
onClick={openList}
|
|
62
|
+
onKeyDown={(e) => {
|
|
63
|
+
if (e.key === 'Enter') openList();
|
|
64
|
+
}}
|
|
65
|
+
ref={dropdownRef}
|
|
66
|
+
role="listbox"
|
|
67
|
+
tabIndex={0}
|
|
68
|
+
>
|
|
69
|
+
{selected}
|
|
70
|
+
<div className={theme.arrow} />
|
|
71
|
+
</div>
|
|
72
|
+
{
|
|
73
|
+
anchor ? (
|
|
74
|
+
<Options
|
|
75
|
+
anchorRect={anchor}
|
|
76
|
+
containerClass={theme.select || ''}
|
|
77
|
+
onCancel={() => setAnchor(null)}
|
|
78
|
+
onChange={(newValue) => {
|
|
79
|
+
setAnchor(null);
|
|
80
|
+
if (onChange) onChange(newValue);
|
|
81
|
+
}}
|
|
82
|
+
optionClass={theme.option || ''}
|
|
83
|
+
options={options}
|
|
84
|
+
/>
|
|
85
|
+
) : null
|
|
86
|
+
}
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const ThemedCustomDropdown = themed(
|
|
92
|
+
BaseCustomDropdown,
|
|
93
|
+
'CustomDropdown',
|
|
94
|
+
validThemeKeys,
|
|
95
|
+
defaultTheme,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
BaseCustomDropdown.propTypes = {
|
|
99
|
+
filter: PT.func,
|
|
100
|
+
label: PT.node,
|
|
101
|
+
onChange: PT.func,
|
|
102
|
+
options: PT.arrayOf(optionValidator.isRequired),
|
|
103
|
+
theme: ThemedCustomDropdown.themeType.isRequired,
|
|
104
|
+
value: PT.string,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
BaseCustomDropdown.defaultProps = {
|
|
108
|
+
filter: undefined,
|
|
109
|
+
label: undefined,
|
|
110
|
+
onChange: undefined,
|
|
111
|
+
options: [],
|
|
112
|
+
value: undefined,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export default ThemedCustomDropdown;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
*,
|
|
2
|
+
.context,
|
|
3
|
+
.ad.hoc {
|
|
4
|
+
// The outermost dropdown container, holding together the label (if any),
|
|
5
|
+
// and the select element with arrow. Note, that the dropdown option list,
|
|
6
|
+
// when opened, exists completely outside the dropdown DOM hierarchy, and
|
|
7
|
+
// is aligned into the correct position by JS.
|
|
8
|
+
&.container {
|
|
9
|
+
align-items: center;
|
|
10
|
+
display: inline-flex;
|
|
11
|
+
margin: 0.1em;
|
|
12
|
+
position: relative;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Styling of default label next to the dropdown (has no effect on custom
|
|
16
|
+
// non-string label node, if provided).
|
|
17
|
+
&.label {
|
|
18
|
+
margin: 0 0.6em 0 1.2em;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
&.dropdown {
|
|
22
|
+
border: 1px solid gray;
|
|
23
|
+
border-radius: 0.3em;
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
min-width: 200px;
|
|
26
|
+
outline: none;
|
|
27
|
+
padding: 0.3em 3.0em 0.3em 0.6em;
|
|
28
|
+
position: relative;
|
|
29
|
+
user-select: none;
|
|
30
|
+
|
|
31
|
+
&:focus {
|
|
32
|
+
border-color: blue;
|
|
33
|
+
box-shadow: 0 0 3px 1px lightblue;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
&.option {
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
outline: none ;
|
|
40
|
+
padding: 0 0.6em;
|
|
41
|
+
|
|
42
|
+
&:focus {
|
|
43
|
+
background: royalblue;
|
|
44
|
+
color: white;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
&:hover {
|
|
48
|
+
background: royalblue;
|
|
49
|
+
color: white;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
&.select {
|
|
54
|
+
background: white;
|
|
55
|
+
border: 1px solid gray;
|
|
56
|
+
border-radius: 0 0 0.3em 0.3em;
|
|
57
|
+
border-top: none;
|
|
58
|
+
box-shadow: 0 6px 12px 3px lightgray;
|
|
59
|
+
position: fixed;
|
|
60
|
+
top: 20px;
|
|
61
|
+
left: 10px;
|
|
62
|
+
z-index: 1001;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
&.arrow {
|
|
66
|
+
background-image: linear-gradient(to top, lightgray, white 50%, white);
|
|
67
|
+
border-left: 1px solid gray;
|
|
68
|
+
border-radius: 0 0.3em 0.3em 0;
|
|
69
|
+
bottom: 0;
|
|
70
|
+
padding: 0.3em 0.6em;
|
|
71
|
+
position: absolute;
|
|
72
|
+
right: 0;
|
|
73
|
+
top: 0;
|
|
74
|
+
|
|
75
|
+
&::after {
|
|
76
|
+
content: '▼';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
&.active {
|
|
81
|
+
.arrow {
|
|
82
|
+
border-radius: 0 0.3em 0 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.dropdown {
|
|
86
|
+
border-color: blue;
|
|
87
|
+
border-radius: 0.3em 0.3em 0 0;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -1,32 +1,17 @@
|
|
|
1
|
+
// Implements dropdown based on the native HTML <select> element.
|
|
2
|
+
|
|
1
3
|
import PT from 'prop-types';
|
|
2
4
|
|
|
3
|
-
import themed
|
|
5
|
+
import themed from '@dr.pogodin/react-themes';
|
|
4
6
|
|
|
5
7
|
import defaultTheme from './theme.scss';
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
'option',
|
|
14
|
-
'select',
|
|
15
|
-
] as const;
|
|
16
|
-
|
|
17
|
-
type DropdownOptionT = {
|
|
18
|
-
name?: string | null;
|
|
19
|
-
value: string;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
type PropsT = {
|
|
23
|
-
filter?: (item: DropdownOptionT | string) => boolean;
|
|
24
|
-
label?: string;
|
|
25
|
-
onChange?: React.ChangeEventHandler<HTMLSelectElement>;
|
|
26
|
-
options?: Array<DropdownOptionT | string>;
|
|
27
|
-
theme: Theme<typeof validThemeKeys>;
|
|
28
|
-
value?: string;
|
|
29
|
-
};
|
|
9
|
+
import {
|
|
10
|
+
type PropsT,
|
|
11
|
+
optionsValidator,
|
|
12
|
+
optionValueName,
|
|
13
|
+
validThemeKeys,
|
|
14
|
+
} from '../common';
|
|
30
15
|
|
|
31
16
|
/**
|
|
32
17
|
* Implements a themeable dropdown list. Internally it is rendered with help of
|
|
@@ -47,33 +32,27 @@ type PropsT = {
|
|
|
47
32
|
* @param [props....]
|
|
48
33
|
* [Other theming properties](https://www.npmjs.com/package/@dr.pogodin/react-themes#themed-component-properties)
|
|
49
34
|
*/
|
|
50
|
-
const Dropdown: React.FunctionComponent<PropsT
|
|
35
|
+
const Dropdown: React.FunctionComponent<PropsT<string>> = ({
|
|
51
36
|
filter,
|
|
52
37
|
label,
|
|
53
38
|
onChange,
|
|
54
|
-
options
|
|
39
|
+
options,
|
|
55
40
|
theme,
|
|
56
41
|
value,
|
|
57
42
|
}) => {
|
|
43
|
+
if (!options) throw Error('Internal error');
|
|
44
|
+
|
|
58
45
|
let isValidValue = false;
|
|
59
46
|
const optionElements = [];
|
|
60
47
|
|
|
61
48
|
for (let i = 0; i < options.length; ++i) {
|
|
62
49
|
const option = options[i];
|
|
63
50
|
if (!filter || filter(option)) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (typeof option === 'string') {
|
|
67
|
-
optionName = option;
|
|
68
|
-
optionValue = option;
|
|
69
|
-
} else {
|
|
70
|
-
optionName = option.name || option.value;
|
|
71
|
-
optionValue = option.value;
|
|
72
|
-
}
|
|
73
|
-
isValidValue ||= optionValue === value;
|
|
51
|
+
const [iValue, iName] = optionValueName(option);
|
|
52
|
+
isValidValue ||= iValue === value;
|
|
74
53
|
optionElements.push(
|
|
75
|
-
<option className={theme.option} key={
|
|
76
|
-
{
|
|
54
|
+
<option className={theme.option} key={iValue} value={iValue}>
|
|
55
|
+
{iName}
|
|
77
56
|
</option>,
|
|
78
57
|
);
|
|
79
58
|
}
|
|
@@ -96,7 +75,7 @@ const Dropdown: React.FunctionComponent<PropsT> = ({
|
|
|
96
75
|
|
|
97
76
|
return (
|
|
98
77
|
<div className={theme.container}>
|
|
99
|
-
{ label === undefined ? null : <
|
|
78
|
+
{ label === undefined ? null : <div className={theme.label}>{label}</div> }
|
|
100
79
|
<div className={theme.dropdown}>
|
|
101
80
|
<select
|
|
102
81
|
className={theme.select}
|
|
@@ -106,7 +85,7 @@ const Dropdown: React.FunctionComponent<PropsT> = ({
|
|
|
106
85
|
{hiddenOption}
|
|
107
86
|
{optionElements}
|
|
108
87
|
</select>
|
|
109
|
-
<div className={theme.arrow}
|
|
88
|
+
<div className={theme.arrow} />
|
|
110
89
|
</div>
|
|
111
90
|
</div>
|
|
112
91
|
);
|
|
@@ -121,17 +100,9 @@ const ThemedDropdown = themed(
|
|
|
121
100
|
|
|
122
101
|
Dropdown.propTypes = {
|
|
123
102
|
filter: PT.func,
|
|
124
|
-
label: PT.
|
|
103
|
+
label: PT.node,
|
|
125
104
|
onChange: PT.func,
|
|
126
|
-
options:
|
|
127
|
-
PT.oneOfType([
|
|
128
|
-
PT.shape({
|
|
129
|
-
name: PT.string,
|
|
130
|
-
value: PT.string.isRequired,
|
|
131
|
-
}),
|
|
132
|
-
PT.string,
|
|
133
|
-
]).isRequired,
|
|
134
|
-
),
|
|
105
|
+
options: optionsValidator,
|
|
135
106
|
theme: ThemedDropdown.themeType.isRequired,
|
|
136
107
|
value: PT.string,
|
|
137
108
|
};
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
position: absolute;
|
|
18
18
|
right: 0;
|
|
19
19
|
top: 0;
|
|
20
|
+
|
|
21
|
+
&::after {
|
|
22
|
+
content: '▼';
|
|
23
|
+
}
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
&.container {
|
|
@@ -26,6 +30,7 @@
|
|
|
26
30
|
position: relative;
|
|
27
31
|
}
|
|
28
32
|
|
|
33
|
+
.active + &.arrow,
|
|
29
34
|
:active + &.arrow {
|
|
30
35
|
background-image: linear-gradient(to bottom, lightgray, white 50%, white);
|
|
31
36
|
border-bottom-right-radius: 0;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import PT from 'prop-types';
|
|
2
|
+
import themed, { type Theme } from '@dr.pogodin/react-themes';
|
|
3
|
+
|
|
4
|
+
import { type OptionsT, optionsValidator, optionValueName } from '../common';
|
|
5
|
+
|
|
6
|
+
import defaultTheme from './theme.scss';
|
|
7
|
+
|
|
8
|
+
const validThemeKeys = [
|
|
9
|
+
'container',
|
|
10
|
+
'label',
|
|
11
|
+
'option',
|
|
12
|
+
'selected',
|
|
13
|
+
'switch',
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
type PropsT = {
|
|
17
|
+
label?: React.ReactNode;
|
|
18
|
+
onChange?: (value: string) => void;
|
|
19
|
+
options?: Readonly<OptionsT<React.ReactNode>>;
|
|
20
|
+
theme: Theme<typeof validThemeKeys>;
|
|
21
|
+
value?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const BaseSwitch: React.FunctionComponent<PropsT> = ({
|
|
25
|
+
label,
|
|
26
|
+
onChange,
|
|
27
|
+
options,
|
|
28
|
+
theme,
|
|
29
|
+
value,
|
|
30
|
+
}) => {
|
|
31
|
+
if (!options || !theme.option) throw Error('Internal error');
|
|
32
|
+
|
|
33
|
+
const optionNodes: React.ReactNode[] = [];
|
|
34
|
+
for (let i = 0; i < options?.length; ++i) {
|
|
35
|
+
const [iValue, iName] = optionValueName(options[i]);
|
|
36
|
+
|
|
37
|
+
let className: string = theme.option;
|
|
38
|
+
let onPress: (() => void) | undefined;
|
|
39
|
+
if (iValue === value) className += ` ${theme.selected}`;
|
|
40
|
+
else if (onChange) onPress = () => onChange(iValue);
|
|
41
|
+
|
|
42
|
+
optionNodes.push(
|
|
43
|
+
onPress ? (
|
|
44
|
+
<div
|
|
45
|
+
className={className}
|
|
46
|
+
onClick={onPress}
|
|
47
|
+
onKeyDown={(e) => {
|
|
48
|
+
if (onPress && e.key === 'Enter') onPress();
|
|
49
|
+
}}
|
|
50
|
+
key={iValue}
|
|
51
|
+
role="button"
|
|
52
|
+
tabIndex={0}
|
|
53
|
+
>
|
|
54
|
+
{iName}
|
|
55
|
+
</div>
|
|
56
|
+
) : (
|
|
57
|
+
<div className={className} key={iValue}>{iName}</div>
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className={theme.container}>
|
|
64
|
+
{label ? <div className={theme.label}>{label}</div> : null}
|
|
65
|
+
<div className={theme.switch}>
|
|
66
|
+
{optionNodes}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const ThemedSwitch = themed(
|
|
73
|
+
BaseSwitch,
|
|
74
|
+
'Switch',
|
|
75
|
+
validThemeKeys,
|
|
76
|
+
defaultTheme,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
BaseSwitch.propTypes = {
|
|
80
|
+
label: PT.node,
|
|
81
|
+
onChange: PT.func,
|
|
82
|
+
options: optionsValidator,
|
|
83
|
+
theme: ThemedSwitch.themeType.isRequired,
|
|
84
|
+
value: PT.string,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
BaseSwitch.defaultProps = {
|
|
88
|
+
label: undefined,
|
|
89
|
+
onChange: undefined,
|
|
90
|
+
options: [],
|
|
91
|
+
value: undefined,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export default ThemedSwitch;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
*,
|
|
2
|
+
.context,
|
|
3
|
+
.ad.hoc {
|
|
4
|
+
&.container {
|
|
5
|
+
align-items: center;
|
|
6
|
+
display: flex;
|
|
7
|
+
gap: 0.6em;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
&.option {
|
|
11
|
+
border: 1px solid transparent;
|
|
12
|
+
border-radius: 0.3em;
|
|
13
|
+
cursor: pointer;
|
|
14
|
+
outline: none;
|
|
15
|
+
padding: 0 0.9em;
|
|
16
|
+
|
|
17
|
+
&:focus {
|
|
18
|
+
border-color: blue;
|
|
19
|
+
box-shadow: 0 0 3px 1px lightblue;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
&.selected {
|
|
24
|
+
border: 1px solid gray;
|
|
25
|
+
background: white;
|
|
26
|
+
cursor: default;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
&.switch {
|
|
30
|
+
align-items: center;
|
|
31
|
+
background: whitesmoke;
|
|
32
|
+
border: 1px solid gray;
|
|
33
|
+
border-radius: 0.3em;
|
|
34
|
+
display: flex;
|
|
35
|
+
gap: 0.3em;
|
|
36
|
+
padding: 0.3em;
|
|
37
|
+
user-select: none;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// The stuff common between different dropdown implementations.
|
|
2
|
+
|
|
3
|
+
import PT from 'prop-types';
|
|
4
|
+
|
|
5
|
+
import type { Theme } from '@dr.pogodin/react-themes';
|
|
6
|
+
|
|
7
|
+
export const validThemeKeys = [
|
|
8
|
+
'active',
|
|
9
|
+
'arrow',
|
|
10
|
+
'container',
|
|
11
|
+
'dropdown',
|
|
12
|
+
'hiddenOption',
|
|
13
|
+
'label',
|
|
14
|
+
'option',
|
|
15
|
+
'select',
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export type OptionT<NameT> = {
|
|
19
|
+
name?: NameT | null;
|
|
20
|
+
value: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type OptionsT<NameT> = Array<OptionT<NameT> | string>;
|
|
24
|
+
|
|
25
|
+
export type PropsT<
|
|
26
|
+
NameT,
|
|
27
|
+
OnChangeT = React.ChangeEventHandler<HTMLSelectElement>,
|
|
28
|
+
> = {
|
|
29
|
+
filter?: (item: OptionT<NameT> | string) => boolean;
|
|
30
|
+
label?: React.ReactNode;
|
|
31
|
+
onChange?: OnChangeT;
|
|
32
|
+
options?: OptionsT<NameT>;
|
|
33
|
+
theme: Theme<typeof validThemeKeys>;
|
|
34
|
+
value?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const optionValidator = PT.oneOfType([
|
|
38
|
+
PT.shape({
|
|
39
|
+
name: PT.string,
|
|
40
|
+
value: PT.string.isRequired,
|
|
41
|
+
}).isRequired,
|
|
42
|
+
PT.string.isRequired,
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
export const optionsValidator = PT.arrayOf(optionValidator.isRequired);
|
|
46
|
+
|
|
47
|
+
/** Returns option value and name as a tuple. */
|
|
48
|
+
export function optionValueName<NameT>(
|
|
49
|
+
option: OptionT<NameT> | string,
|
|
50
|
+
): [string, NameT | string] {
|
|
51
|
+
return typeof option === 'string'
|
|
52
|
+
? [option, option]
|
|
53
|
+
: [option.value, option.name ?? option.value];
|
|
54
|
+
}
|
|
@@ -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);
|