@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.
Files changed (63) hide show
  1. package/build/development/index.js +1 -1
  2. package/build/development/index.js.map +1 -1
  3. package/build/development/server/index.js +1 -1
  4. package/build/development/server/index.js.map +1 -1
  5. package/build/development/server/utils/index.js +1 -1
  6. package/build/development/shared/components/Modal/index.js +17 -0
  7. package/build/development/shared/components/Modal/index.js.map +1 -1
  8. package/build/development/shared/components/WithTooltip/index.js +1 -1
  9. package/build/development/shared/components/WithTooltip/index.js.map +1 -1
  10. package/build/development/shared/components/index.js +1 -1
  11. package/build/development/shared/components/index.js.map +1 -1
  12. package/build/development/shared/components/selectors/CustomDropdown/Options/index.js +30 -29
  13. package/build/development/shared/components/selectors/CustomDropdown/Options/index.js.map +1 -1
  14. package/build/development/shared/components/selectors/CustomDropdown/index.js +68 -14
  15. package/build/development/shared/components/selectors/CustomDropdown/index.js.map +1 -1
  16. package/build/development/shared/components/selectors/common.js +4 -1
  17. package/build/development/shared/components/selectors/common.js.map +1 -1
  18. package/build/development/shared/utils/index.js +1 -1
  19. package/build/development/shared/utils/index.js.map +1 -1
  20. package/build/development/shared/utils/jest/E2eSsrEnv.js +3 -0
  21. package/build/development/shared/utils/jest/E2eSsrEnv.js.map +1 -1
  22. package/build/development/shared/utils/jest/index.js +1 -1
  23. package/build/development/shared/utils/jest/index.js.map +1 -1
  24. package/build/development/style.css +18 -2
  25. package/build/development/web.bundle.js +15 -15
  26. package/build/production/index.js +1 -1
  27. package/build/production/index.js.map +1 -1
  28. package/build/production/server/index.js +1 -1
  29. package/build/production/server/index.js.map +1 -1
  30. package/build/production/server/utils/index.js +1 -1
  31. package/build/production/shared/components/Modal/index.js +3 -2
  32. package/build/production/shared/components/Modal/index.js.map +1 -1
  33. package/build/production/shared/components/WithTooltip/index.js +1 -1
  34. package/build/production/shared/components/WithTooltip/index.js.map +1 -1
  35. package/build/production/shared/components/index.js +1 -1
  36. package/build/production/shared/components/index.js.map +1 -1
  37. package/build/production/shared/components/selectors/CustomDropdown/Options/index.js +2 -2
  38. package/build/production/shared/components/selectors/CustomDropdown/Options/index.js.map +1 -1
  39. package/build/production/shared/components/selectors/CustomDropdown/index.js +7 -3
  40. package/build/production/shared/components/selectors/CustomDropdown/index.js.map +1 -1
  41. package/build/production/shared/components/selectors/common.js +3 -1
  42. package/build/production/shared/components/selectors/common.js.map +1 -1
  43. package/build/production/shared/utils/index.js +1 -1
  44. package/build/production/shared/utils/index.js.map +1 -1
  45. package/build/production/shared/utils/jest/E2eSsrEnv.js +3 -1
  46. package/build/production/shared/utils/jest/E2eSsrEnv.js.map +1 -1
  47. package/build/production/shared/utils/jest/index.js +1 -1
  48. package/build/production/shared/utils/jest/index.js.map +1 -1
  49. package/build/production/style.css +1 -1
  50. package/build/production/style.css.map +1 -1
  51. package/build/production/web.bundle.js +1 -1
  52. package/build/production/web.bundle.js.map +1 -1
  53. package/build/types-code/shared/components/Modal/index.d.ts +1 -0
  54. package/build/types-code/shared/components/selectors/CustomDropdown/Options/index.d.ts +11 -6
  55. package/build/types-code/shared/components/selectors/common.d.ts +1 -1
  56. package/build/types-scss/src/shared/components/selectors/CustomDropdown/theme.scss.d.ts +1 -0
  57. package/package.json +27 -27
  58. package/src/shared/components/Modal/index.tsx +18 -0
  59. package/src/shared/components/selectors/CustomDropdown/Options/index.tsx +40 -34
  60. package/src/shared/components/selectors/CustomDropdown/index.tsx +73 -12
  61. package/src/shared/components/selectors/CustomDropdown/theme.scss +33 -5
  62. package/src/shared/components/selectors/common.ts +4 -0
  63. 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.FunctionComponent<PropsT>;
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;
@@ -8,3 +8,4 @@ export declare const hoc: string;
8
8
  export declare const label: string;
9
9
  export declare const option: string;
10
10
  export declare const select: string;
11
+ export declare const upward: string;
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.31.0",
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.0",
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.7",
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.18.3",
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.12.0"
46
+ "winston": "^3.13.0"
47
47
  },
48
48
  "description": "Collection of generic ReactJS components and utils",
49
49
  "devDependencies": {
50
- "@babel/cli": "^7.23.9",
51
- "@babel/core": "^7.24.0",
52
- "@babel/eslint-parser": "^7.23.10",
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.0",
56
- "@babel/preset-env": "^7.24.0",
57
- "@babel/preset-react": "^7.23.3",
58
- "@babel/preset-typescript": "^7.23.3",
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.3",
64
- "@tsd/typescript": "^5.3.3",
63
+ "@tsconfig/recommended": "^1.0.4",
64
+ "@tsd/typescript": "^5.4.3",
65
65
  "@types/compression": "^1.7.5",
66
- "@types/config": "^3.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.66",
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.18",
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.0",
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.0",
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.7.7",
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.35",
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.2.1",
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.0",
125
- "typescript": "^5.4.2",
126
- "typescript-eslint": "^7.2.0",
127
- "webpack": "^5.90.3",
128
- "webpack-dev-middleware": "^7.0.0",
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 { useEffect } from 'react';
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: React.FunctionComponent<PropsT> = ({
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
- // Closes the dropdown (cancels the selection) on any page scrolling attempt.
39
- // This is the same native <select> elements do on scrolling, and at least for
40
- // now we have no reason to deal with complications needed to support open
41
- // dropdowns during the scrolling (that would need to re-position it in
42
- // response to the position changes of the root dropdown element).
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
- containerStyle={{
80
- left: anchorRect.left,
81
- top: anchorRect.bottom,
82
- width: anchorRect.width,
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
- anchorRect: PT.shape({
101
- bottom: PT.number.isRequired,
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
- }).isRequired,
105
- containerClass: PT.string.isRequired,
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
- // If "null" the dropdown is closed, otherwise it is displayed
32
- // at the specified coordinates.
33
- const [anchor, setAnchor] = useState<DOMRect | null>(null);
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
- setAnchor(dropdownRef.current!.getBoundingClientRect());
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 = <>&zwnj;</>;
@@ -49,7 +101,13 @@ PropsT<React.ReactNode, (value: string) => void>
49
101
  }
50
102
 
51
103
  let containerClassName = theme.container;
52
- if (anchor) containerClassName += ` ${theme.active}`;
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
- anchor ? (
131
+ active ? (
74
132
  <Options
75
- anchorRect={anchor}
76
- containerClass={theme.select || ''}
77
- onCancel={() => setAnchor(null)}
133
+ containerClass={opsContainerClass}
134
+ containerStyle={opsPos}
135
+ onCancel={() => {
136
+ setActive(false);
137
+ }}
78
138
  onChange={(newValue) => {
79
- setAnchor(null);
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: 1px solid gray;
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: 1px solid gray;
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: 1px solid gray;
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
- compiler.outputFileSystem = this.global.webpackOutputFs;
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);