@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.
Files changed (140) hide show
  1. package/bin/build.js +5 -0
  2. package/build/development/client/index.js +1 -1
  3. package/build/development/client/index.js.map +1 -1
  4. package/build/development/index.js +8 -1
  5. package/build/development/index.js.map +1 -1
  6. package/build/development/server/index.js +1 -1
  7. package/build/development/server/index.js.map +1 -1
  8. package/build/development/server/utils/index.js +1 -1
  9. package/build/development/shared/components/Checkbox/index.js +2 -2
  10. package/build/development/shared/components/Checkbox/index.js.map +1 -1
  11. package/build/development/shared/components/Input/index.js +2 -2
  12. package/build/development/shared/components/Input/index.js.map +1 -1
  13. package/build/development/shared/components/Modal/index.js +40 -5
  14. package/build/development/shared/components/Modal/index.js.map +1 -1
  15. package/build/development/shared/components/TextArea/index.js +5 -0
  16. package/build/development/shared/components/TextArea/index.js.map +1 -1
  17. package/build/development/shared/components/WithTooltip/index.js +1 -1
  18. package/build/development/shared/components/WithTooltip/index.js.map +1 -1
  19. package/build/development/shared/components/YouTubeVideo/index.js +1 -3
  20. package/build/development/shared/components/YouTubeVideo/index.js.map +1 -1
  21. package/build/development/shared/components/index.js +28 -15
  22. package/build/development/shared/components/index.js.map +1 -1
  23. package/build/development/shared/components/selectors/CustomDropdown/Options/index.js +85 -0
  24. package/build/development/shared/components/selectors/CustomDropdown/Options/index.js.map +1 -0
  25. package/build/development/shared/components/selectors/CustomDropdown/index.js +105 -0
  26. package/build/development/shared/components/selectors/CustomDropdown/index.js.map +1 -0
  27. package/build/development/shared/components/{Dropdown → selectors/NativeDropdown}/index.js +25 -34
  28. package/build/development/shared/components/selectors/NativeDropdown/index.js.map +1 -0
  29. package/build/development/shared/components/selectors/Switch/index.js +76 -0
  30. package/build/development/shared/components/selectors/Switch/index.js.map +1 -0
  31. package/build/development/shared/components/selectors/common.js +24 -0
  32. package/build/development/shared/components/selectors/common.js.map +1 -0
  33. package/build/development/shared/components/selectors/index.js +28 -0
  34. package/build/development/shared/components/selectors/index.js.map +1 -0
  35. package/build/development/shared/utils/index.js +1 -1
  36. package/build/development/shared/utils/index.js.map +1 -1
  37. package/build/development/shared/utils/jest/E2eSsrEnv.js +3 -0
  38. package/build/development/shared/utils/jest/E2eSsrEnv.js.map +1 -1
  39. package/build/development/shared/utils/jest/index.js +1 -1
  40. package/build/development/shared/utils/jest/index.js.map +1 -1
  41. package/build/development/style.css +387 -225
  42. package/build/development/web.bundle.js +113 -53
  43. package/build/production/client/index.js +1 -1
  44. package/build/production/client/index.js.map +1 -1
  45. package/build/production/index.js +1 -1
  46. package/build/production/index.js.map +1 -1
  47. package/build/production/server/index.js +1 -1
  48. package/build/production/server/index.js.map +1 -1
  49. package/build/production/server/utils/index.js +1 -1
  50. package/build/production/shared/components/Checkbox/index.js +2 -2
  51. package/build/production/shared/components/Checkbox/index.js.map +1 -1
  52. package/build/production/shared/components/Input/index.js +1 -1
  53. package/build/production/shared/components/Input/index.js.map +1 -1
  54. package/build/production/shared/components/Modal/index.js +4 -2
  55. package/build/production/shared/components/Modal/index.js.map +1 -1
  56. package/build/production/shared/components/TextArea/index.js +3 -3
  57. package/build/production/shared/components/TextArea/index.js.map +1 -1
  58. package/build/production/shared/components/WithTooltip/index.js +1 -1
  59. package/build/production/shared/components/WithTooltip/index.js.map +1 -1
  60. package/build/production/shared/components/YouTubeVideo/index.js +2 -2
  61. package/build/production/shared/components/YouTubeVideo/index.js.map +1 -1
  62. package/build/production/shared/components/index.js +1 -1
  63. package/build/production/shared/components/index.js.map +1 -1
  64. package/build/production/shared/components/selectors/CustomDropdown/Options/index.js +7 -0
  65. package/build/production/shared/components/selectors/CustomDropdown/Options/index.js.map +1 -0
  66. package/build/production/shared/components/selectors/CustomDropdown/index.js +4 -0
  67. package/build/production/shared/components/selectors/CustomDropdown/index.js.map +1 -0
  68. package/build/production/shared/components/selectors/NativeDropdown/index.js +25 -0
  69. package/build/production/shared/components/selectors/NativeDropdown/index.js.map +1 -0
  70. package/build/production/shared/components/selectors/Switch/index.js +2 -0
  71. package/build/production/shared/components/selectors/Switch/index.js.map +1 -0
  72. package/build/production/shared/components/selectors/common.js +3 -0
  73. package/build/production/shared/components/selectors/common.js.map +1 -0
  74. package/build/production/shared/components/selectors/index.js +2 -0
  75. package/build/production/shared/components/selectors/index.js.map +1 -0
  76. package/build/production/shared/utils/index.js +1 -1
  77. package/build/production/shared/utils/index.js.map +1 -1
  78. package/build/production/shared/utils/jest/E2eSsrEnv.js +3 -1
  79. package/build/production/shared/utils/jest/E2eSsrEnv.js.map +1 -1
  80. package/build/production/shared/utils/jest/index.js +1 -1
  81. package/build/production/shared/utils/jest/index.js.map +1 -1
  82. package/build/production/style.css +1 -1
  83. package/build/production/style.css.map +1 -1
  84. package/build/production/web.bundle.js +1 -1
  85. package/build/production/web.bundle.js.map +1 -1
  86. package/build/types-code/client/index.d.ts +1 -0
  87. package/build/types-code/index.d.ts +1 -1
  88. package/build/types-code/shared/components/Checkbox/index.d.ts +1 -1
  89. package/build/types-code/shared/components/Input/index.d.ts +1 -1
  90. package/build/types-code/shared/components/Modal/index.d.ts +3 -1
  91. package/build/types-code/shared/components/TextArea/index.d.ts +1 -0
  92. package/build/types-code/shared/components/index.d.ts +1 -2
  93. package/build/types-code/shared/components/selectors/CustomDropdown/Options/index.d.ts +17 -0
  94. package/build/types-code/shared/components/selectors/CustomDropdown/index.d.ts +4 -0
  95. package/build/types-code/shared/components/selectors/NativeDropdown/index.d.ts +3 -0
  96. package/build/types-code/shared/components/selectors/Switch/index.d.ts +13 -0
  97. package/build/types-code/shared/components/selectors/common.d.ts +27 -0
  98. package/build/types-code/shared/components/selectors/index.d.ts +3 -0
  99. package/build/types-scss/src/shared/components/Modal/styles.scss.d.ts +1 -0
  100. package/build/types-scss/src/shared/components/selectors/CustomDropdown/Options/style.scss.d.ts +1 -0
  101. package/build/types-scss/src/shared/components/selectors/CustomDropdown/theme.scss.d.ts +10 -0
  102. package/build/types-scss/src/shared/components/{Dropdown → selectors/NativeDropdown}/theme.scss.d.ts +1 -0
  103. package/build/types-scss/src/shared/components/selectors/Switch/theme.scss.d.ts +6 -0
  104. package/package.json +30 -30
  105. package/src/client/index.tsx +2 -1
  106. package/src/index.ts +1 -0
  107. package/src/shared/components/Button/style.scss +1 -0
  108. package/src/shared/components/Checkbox/index.tsx +3 -3
  109. package/src/shared/components/Input/index.tsx +3 -3
  110. package/src/shared/components/Modal/base-theme.scss +1 -1
  111. package/src/shared/components/Modal/index.tsx +40 -5
  112. package/src/shared/components/Modal/styles.scss +2 -4
  113. package/src/shared/components/TextArea/index.tsx +5 -0
  114. package/src/shared/components/TextArea/style.scss +8 -0
  115. package/src/shared/components/YouTubeVideo/base.scss +3 -1
  116. package/src/shared/components/YouTubeVideo/index.tsx +2 -3
  117. package/src/shared/components/index.ts +2 -2
  118. package/src/shared/components/selectors/CustomDropdown/Options/index.tsx +107 -0
  119. package/src/shared/components/selectors/CustomDropdown/Options/style.scss +6 -0
  120. package/src/shared/components/selectors/CustomDropdown/index.tsx +115 -0
  121. package/src/shared/components/selectors/CustomDropdown/theme.scss +90 -0
  122. package/src/shared/components/{Dropdown → selectors/NativeDropdown}/index.tsx +21 -50
  123. package/src/shared/components/{Dropdown → selectors/NativeDropdown}/theme.scss +5 -0
  124. package/src/shared/components/selectors/Switch/index.tsx +94 -0
  125. package/src/shared/components/selectors/Switch/theme.scss +39 -0
  126. package/src/shared/components/selectors/common.ts +54 -0
  127. package/src/shared/components/selectors/index.ts +3 -0
  128. package/src/shared/utils/jest/E2eSsrEnv.ts +5 -1
  129. package/build/development/shared/components/Dropdown/index.js.map +0 -1
  130. package/build/development/shared/components/ScalableRect/index.js +0 -80
  131. package/build/development/shared/components/ScalableRect/index.js.map +0 -1
  132. package/build/production/shared/components/Dropdown/index.js +0 -24
  133. package/build/production/shared/components/Dropdown/index.js.map +0 -1
  134. package/build/production/shared/components/ScalableRect/index.js +0 -21
  135. package/build/production/shared/components/ScalableRect/index.js.map +0 -1
  136. package/build/types-code/shared/components/Dropdown/index.d.ts +0 -17
  137. package/build/types-code/shared/components/ScalableRect/index.d.ts +0 -19
  138. package/build/types-scss/src/shared/components/ScalableRect/style.scss.d.ts +0 -2
  139. package/src/shared/components/ScalableRect/index.tsx +0 -84
  140. 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,6 @@
1
+ .overlay {
2
+ inset: 0;
3
+ opacity: 0.2;
4
+ position: fixed;
5
+ z-index: 1000;
6
+ }
@@ -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 = <>&zwnj;</>;
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, { type Theme } from '@dr.pogodin/react-themes';
5
+ import themed from '@dr.pogodin/react-themes';
4
6
 
5
7
  import defaultTheme from './theme.scss';
6
8
 
7
- const validThemeKeys = [
8
- 'arrow',
9
- 'container',
10
- 'dropdown',
11
- 'hiddenOption',
12
- 'label',
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
- let optionValue: string;
65
- let optionName: string;
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={optionValue} value={optionValue}>
76
- {optionName}
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 : <p className={theme.label}>{label}</p> }
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}>▼</div>
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.string,
103
+ label: PT.node,
125
104
  onChange: PT.func,
126
- options: PT.arrayOf(
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
+ }
@@ -0,0 +1,3 @@
1
+ export { default as CustomDropdown } from './CustomDropdown';
2
+ export { default as Dropdown } from './NativeDropdown';
3
+ export { default as Switch } from './Switch';
@@ -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);