@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.
Files changed (31) hide show
  1. package/build/development/shared/components/Modal/index.js +2 -0
  2. package/build/development/shared/components/Modal/index.js.map +1 -1
  3. package/build/development/shared/components/selectors/CustomDropdown/Options/index.js +23 -14
  4. package/build/development/shared/components/selectors/CustomDropdown/Options/index.js.map +1 -1
  5. package/build/development/shared/components/selectors/CustomDropdown/index.js +68 -14
  6. package/build/development/shared/components/selectors/CustomDropdown/index.js.map +1 -1
  7. package/build/development/shared/components/selectors/common.js +4 -1
  8. package/build/development/shared/components/selectors/common.js.map +1 -1
  9. package/build/development/style.css +18 -2
  10. package/build/development/web.bundle.js +5 -5
  11. package/build/production/shared/components/Modal/index.js +1 -1
  12. package/build/production/shared/components/Modal/index.js.map +1 -1
  13. package/build/production/shared/components/selectors/CustomDropdown/Options/index.js +2 -2
  14. package/build/production/shared/components/selectors/CustomDropdown/Options/index.js.map +1 -1
  15. package/build/production/shared/components/selectors/CustomDropdown/index.js +7 -3
  16. package/build/production/shared/components/selectors/CustomDropdown/index.js.map +1 -1
  17. package/build/production/shared/components/selectors/common.js +3 -1
  18. package/build/production/shared/components/selectors/common.js.map +1 -1
  19. package/build/production/style.css +1 -1
  20. package/build/production/style.css.map +1 -1
  21. package/build/production/web.bundle.js +1 -1
  22. package/build/production/web.bundle.js.map +1 -1
  23. package/build/types-code/shared/components/selectors/CustomDropdown/Options/index.d.ts +11 -6
  24. package/build/types-code/shared/components/selectors/common.d.ts +1 -1
  25. package/build/types-scss/src/shared/components/selectors/CustomDropdown/theme.scss.d.ts +1 -0
  26. package/package.json +3 -3
  27. package/src/shared/components/Modal/index.tsx +2 -0
  28. package/src/shared/components/selectors/CustomDropdown/Options/index.tsx +35 -19
  29. package/src/shared/components/selectors/CustomDropdown/index.tsx +73 -12
  30. package/src/shared/components/selectors/CustomDropdown/theme.scss +33 -5
  31. 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.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.1",
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.70",
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.0",
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: React.FunctionComponent<PropsT> = ({
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
- anchorRect: PT.shape({
91
- bottom: PT.number.isRequired,
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
- }).isRequired,
95
- containerClass: PT.string.isRequired,
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
- // 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> = {