@apify/ui-library 1.127.10 → 1.128.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 (45) hide show
  1. package/dist/src/components/code/code_block/code_block.styled.d.ts.map +1 -1
  2. package/dist/src/components/code/code_block/code_block.styled.js +1 -0
  3. package/dist/src/components/code/code_block/code_block.styled.js.map +1 -1
  4. package/dist/src/components/collapsible_card/collapsible_card.d.ts +1 -0
  5. package/dist/src/components/collapsible_card/collapsible_card.d.ts.map +1 -1
  6. package/dist/src/components/collapsible_card/collapsible_card.js +3 -3
  7. package/dist/src/components/collapsible_card/collapsible_card.js.map +1 -1
  8. package/dist/src/components/index.d.ts +2 -0
  9. package/dist/src/components/index.d.ts.map +1 -1
  10. package/dist/src/components/index.js +2 -0
  11. package/dist/src/components/index.js.map +1 -1
  12. package/dist/src/components/select/index.d.ts +2 -0
  13. package/dist/src/components/select/index.d.ts.map +1 -0
  14. package/dist/src/components/select/index.js +2 -0
  15. package/dist/src/components/select/index.js.map +1 -0
  16. package/dist/src/components/select/select.d.ts +57 -0
  17. package/dist/src/components/select/select.d.ts.map +1 -0
  18. package/dist/src/components/select/select.js +234 -0
  19. package/dist/src/components/select/select.js.map +1 -0
  20. package/dist/src/components/switch/index.d.ts +2 -0
  21. package/dist/src/components/switch/index.d.ts.map +1 -0
  22. package/dist/src/components/switch/index.js +2 -0
  23. package/dist/src/components/switch/index.js.map +1 -0
  24. package/dist/src/components/switch/switch.d.ts +11 -0
  25. package/dist/src/components/switch/switch.d.ts.map +1 -0
  26. package/dist/src/components/switch/switch.js +25 -0
  27. package/dist/src/components/switch/switch.js.map +1 -0
  28. package/dist/src/components/switch/switch.style.d.ts +2 -0
  29. package/dist/src/components/switch/switch.style.d.ts.map +1 -0
  30. package/dist/src/components/switch/switch.style.js +53 -0
  31. package/dist/src/components/switch/switch.style.js.map +1 -0
  32. package/dist/tsconfig.build.tsbuildinfo +1 -1
  33. package/package.json +5 -3
  34. package/src/components/checkbox/checkbox.stories.tsx +1 -1
  35. package/src/components/code/code_block/code_block.styled.tsx +1 -0
  36. package/src/components/collapsible_card/collapsible_card.stories.tsx +16 -0
  37. package/src/components/collapsible_card/collapsible_card.tsx +5 -3
  38. package/src/components/index.ts +2 -0
  39. package/src/components/select/index.ts +1 -0
  40. package/src/components/select/select.stories.tsx +76 -0
  41. package/src/components/select/select.tsx +402 -0
  42. package/src/components/switch/index.ts +1 -0
  43. package/src/components/switch/switch.stories.jsx +38 -0
  44. package/src/components/switch/switch.style.ts +54 -0
  45. package/src/components/switch/switch.tsx +54 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apify/ui-library",
3
- "version": "1.127.10",
3
+ "version": "1.128.1",
4
4
  "description": "React UI library used by apify.com",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -27,10 +27,11 @@
27
27
  "It's not nice, but helps us to get around the problem of multiple react instances."
28
28
  ],
29
29
  "dependencies": {
30
- "@apify/ui-icons": "^1.29.2",
30
+ "@apify/ui-icons": "^1.30.0",
31
31
  "@floating-ui/react": "^0.26.2",
32
32
  "@radix-ui/react-checkbox": "^1.3.3",
33
33
  "@radix-ui/react-collapsible": "^1.0.0",
34
+ "@radix-ui/react-switch": "^1.2.6",
34
35
  "@react-hook/resize-observer": "^2.0.2",
35
36
  "clsx": "^2.0.0",
36
37
  "dayjs": "1.11.9",
@@ -42,6 +43,7 @@
42
43
  "prismjs": "^1.30.0",
43
44
  "query-string": "^8.1.0",
44
45
  "react-markdown": "^10.1.0",
46
+ "react-select": "^5.10.2",
45
47
  "rehype-raw": "^7.0.0",
46
48
  "rehype-sanitize": "^6.0.0",
47
49
  "remark-gfm": "^4.0.1",
@@ -68,5 +70,5 @@
68
70
  "src",
69
71
  "style"
70
72
  ],
71
- "gitHead": "0bec6b696f986df3fa2f677f3b2a11023b02077d"
73
+ "gitHead": "de01e121f513532624987e05ef32f8ab73445a21"
72
74
  }
@@ -4,7 +4,7 @@ import styled from 'styled-components';
4
4
  import { CheckboxPrimitive, IndeterminateCheckbox } from './checkbox.js';
5
5
 
6
6
  export default {
7
- title: 'Components/Inputs/CheckboxPrimitive',
7
+ title: 'UI-Library/Inputs/CheckboxPrimitive',
8
8
  };
9
9
 
10
10
  const Grid = styled.div`
@@ -61,6 +61,7 @@ export const CodeBlockWrapper = styled(SyntaxHighlighterBaseStylesWrapper) <{
61
61
  transform: translateY(1px);
62
62
  padding-right: ${theme.space.space32};
63
63
  padding-left: ${theme.space.space8};
64
+ min-height: fit-content;
64
65
  overflow-x: auto;
65
66
  overflow-y: hidden;
66
67
  gap: ${theme.space.space4};
@@ -26,6 +26,10 @@ export default {
26
26
  control: 'boolean',
27
27
  description: 'Whether to hide the chevron icon',
28
28
  },
29
+ noDivider: {
30
+ control: 'boolean',
31
+ description: 'Whether to hide the divider between header and content',
32
+ },
29
33
  topSection: {
30
34
  control: 'text',
31
35
  description: 'Content rendered above the header',
@@ -120,6 +124,18 @@ export const GreyHeaderOnHover: Story = {
120
124
  },
121
125
  };
122
126
 
127
+ /**
128
+ * A collapsible card without the divider between header and content.
129
+ */
130
+ export const NoDivider: Story = {
131
+ args: {
132
+ header: 'No divider between header and content',
133
+ noDivider: true,
134
+ isExpanded: true,
135
+ children: 'This card has no divider line between the header and content areas.',
136
+ },
137
+ };
138
+
123
139
  /**
124
140
  * A collapsible card without the outer border.
125
141
  */
@@ -29,6 +29,7 @@ export type CollapsibleCardProps = {
29
29
  isExpanded?: boolean;
30
30
  onIsExpandedChanged?: (expanded: boolean) => void;
31
31
  noChevron?: boolean;
32
+ noDivider?: boolean;
32
33
  topSection?: ReactNode;
33
34
  isClosedHeaderGrey?: boolean;
34
35
  isHeaderGreyOnHover?: boolean;
@@ -101,8 +102,8 @@ const StyledCardContent = styled.div`
101
102
  overflow: auto;
102
103
  `;
103
104
 
104
- const CollapsibleContent = styled(Collapsible.Content)`
105
- border-top: 1px solid ${theme.color.neutral.border};
105
+ const CollapsibleContent = styled(Collapsible.Content)<{ $noDivider?: boolean }>`
106
+ ${({ $noDivider }) => ($noDivider ? '' : `border-top: 1px solid ${theme.color.neutral.border};`)}
106
107
  `;
107
108
 
108
109
  export const CollapsibleCard: FC<CollapsibleCardProps> = ({
@@ -111,6 +112,7 @@ export const CollapsibleCard: FC<CollapsibleCardProps> = ({
111
112
  isExpanded,
112
113
  onIsExpandedChanged,
113
114
  noChevron,
115
+ noDivider,
114
116
  topSection,
115
117
  hideOuterBorder,
116
118
  hasShadow,
@@ -148,7 +150,7 @@ export const CollapsibleCard: FC<CollapsibleCardProps> = ({
148
150
  <Collapsible.Root
149
151
  open={isUncontrolled ? isOpen : isExpanded}
150
152
  >
151
- <CollapsibleContent className={collapsibleCardClassNames.COLLAPSIBLE_CONTENT_WRAPPER}>
153
+ <CollapsibleContent $noDivider={noDivider} className={collapsibleCardClassNames.COLLAPSIBLE_CONTENT_WRAPPER}>
152
154
  <StyledCardContent className={collapsibleCardClassNames.CONTENT} data-test="card-content">
153
155
  {children}
154
156
  </StyledCardContent>
@@ -28,3 +28,5 @@ export * from './spinner.js';
28
28
  export * from './store/index.js';
29
29
  export * from './checkbox/index.js';
30
30
  export * from './collapsible_card/index.js';
31
+ export * from './select/index.js';
32
+ export * from './switch/index.js';
@@ -0,0 +1 @@
1
+ export * from './select.js';
@@ -0,0 +1,76 @@
1
+ import { useState } from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ import { EyeIcon } from '@apify/ui-icons';
5
+
6
+ import { SelectPrimitive as Select } from './select.js';
7
+
8
+ export default {
9
+ title: 'UI-Library/Inputs/SelectPrimitive',
10
+ };
11
+
12
+ const Grid = styled.div`
13
+ margin: 5rem 3rem;
14
+ display: grid;
15
+ grid-template-columns: repeat(2, auto);
16
+ gap: 1rem 5rem;
17
+
18
+ input{
19
+ width: 100%;
20
+ }
21
+ `;
22
+
23
+ const options = [{ label: 'Ahoj', value: 1 }, { label: 'Ondro!', value: 2 }, { label: '🚲', value: 3 }];
24
+
25
+ export const SelectPrimitive = () => {
26
+ const [multiValue, setMultiValue] = useState([1, 2]);
27
+ const [value, setValue] = useState(1);
28
+ return (
29
+ <Grid>
30
+ <Select options={options} value={value} setValue={setValue}/> <p>singleValue</p>
31
+ <Select isMulti
32
+ options={options}
33
+ value={multiValue}
34
+ setValue={setMultiValue}
35
+ /> <p>multiValue</p>
36
+ <Select isMulti
37
+ error='test Error'
38
+ options={options}
39
+ value={multiValue}
40
+ setValue={setMultiValue}
41
+ /> <p>multiValue error</p>
42
+ <Select isMulti
43
+ disabled
44
+ options={options}
45
+ value={multiValue}
46
+ setValue={setMultiValue}
47
+ /> <p>multiValue disabled</p>
48
+ <Select options={options}
49
+ value={value}
50
+ setValue={setValue}
51
+ indicatorsSlot={<EyeIcon size="16" />}
52
+ /> <p>withIcons</p>
53
+ <Select options={options}
54
+ value={value}
55
+ setValue={setValue}
56
+ disabled={true}
57
+ /> <p>disabled</p>
58
+ <Select options={options}
59
+ value={value}
60
+ setValue={setValue}
61
+ placeholder="Placeholder"
62
+ /> <p>placeholder</p>
63
+ <Select options={options}
64
+ value={value}
65
+ setValue={setValue}
66
+ error={true}
67
+ placeholder='Error select'
68
+ /> <p>error</p>
69
+ <Select options={options}
70
+ value={null}
71
+ setValue={setValue}
72
+ placeholder='Placeholder'
73
+ /> <p>placeholder</p>
74
+ </Grid>
75
+ );
76
+ };
@@ -0,0 +1,402 @@
1
+ import clsx from 'clsx';
2
+ import { type FC, forwardRef, type ReactNode, useCallback, useMemo } from 'react';
3
+ import type {
4
+ ClearIndicatorProps,
5
+ DropdownIndicatorProps,
6
+ IndicatorsContainerProps,
7
+ MultiValueRemoveProps,
8
+ OptionProps,
9
+ StylesConfig,
10
+ } from 'react-select';
11
+ import SelectComponent, { components as selectComponents, defaultTheme } from 'react-select';
12
+ import styled from 'styled-components';
13
+
14
+ import { CaretDownIcon, CaretUpIcon, CrossIcon, XCircleIcon } from '@apify/ui-icons';
15
+
16
+ import { theme } from '../../design_system/theme.js';
17
+ import { IconButton } from '../icon_button.js';
18
+ import { Text } from '../text/index.js';
19
+
20
+ const DEFAULT_Z_INDEX_IN_MODAL = 99;
21
+ const DEFAULT_Z_INDEX_OUTSIDE_MODAL = 9;
22
+
23
+ export type SelectOptionValue = string | number | boolean;
24
+
25
+ export type SelectOption = {
26
+ label: ReactNode;
27
+ value: SelectOptionValue;
28
+ description?: string;
29
+ };
30
+
31
+ // There is a prop collision in styled-components@6 and react-select. react-select uses `theme` prop but
32
+ // styled components do not pass this prop down. Because we only use the default theme, we can manually inject it.
33
+ // All components that just restyle the default component provided by the package MUST wrap it with this HOC.
34
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- react-select component props are untyped in this context
35
+ export const withReactSelectTheme = (Component: FC<any>) => {
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ const EnhancedComponent = (props: any) => {
38
+ return (
39
+ <Component {...props} theme={defaultTheme} />
40
+ );
41
+ };
42
+ const displayName = Component.name || (Component as { displayName?: string }).displayName;
43
+ EnhancedComponent.displayName = `withReactSelectTheme${displayName}`;
44
+ return EnhancedComponent;
45
+ };
46
+
47
+ // We need to use both styles and components in order to style select as we want.
48
+ // Sadly, not all items of style receive selectProps that are crucial for determining the right style.
49
+ // On the other hand, if styling for example multiValueLabel through its Component, it receives css classes of its parent
50
+ const selectStyles: StylesConfig<SelectOption> = {
51
+ multiValue: (base) => ({
52
+ ...base,
53
+ backgroundColor: theme.color.neutral.chipBackground,
54
+ borderRadius: 6,
55
+ height: 20,
56
+ alignItems: 'center',
57
+ }),
58
+ multiValueLabel: (base) => ({
59
+ ...base,
60
+ color: theme.color.neutral.text,
61
+ fontSize: 12,
62
+ }),
63
+ multiValueRemove: (base) => ({
64
+ ...base,
65
+ backgroundColor: 'transparent !important',
66
+
67
+ '&:hover svg': {
68
+ color: theme.color.neutral.iconSubtle,
69
+ },
70
+ }),
71
+ dropdownIndicator: (base, { selectProps: { menuIsOpen, isDisabled } }) => ({
72
+ ...base,
73
+ svg: {
74
+ transform: menuIsOpen ? 'rotate(180deg)' : '',
75
+ color: isDisabled ? theme.color.neutral.iconDisabled : theme.color.neutral.icon,
76
+ },
77
+ }),
78
+ valueContainer: (base) => ({
79
+ ...base,
80
+ padding: '2px 6px',
81
+ }),
82
+ menu: (base) => ({
83
+ ...base,
84
+ overflow: 'hidden',
85
+ backgroundColor: theme.color.neutral.background,
86
+ border: `1px solid ${theme.color.neutral.border}`,
87
+ boxShadow: 'none',
88
+ zIndex: 3,
89
+ }),
90
+ option: (base, state) => ({
91
+ ...base,
92
+ color: state.isSelected ? theme.color.neutral.textOnPrimary : theme.color.neutral.text,
93
+ backgroundColor: state.isSelected ? theme.color.primary.action : theme.color.neutral.background,
94
+ '& > p': {
95
+ color: state.isSelected ? theme.color.neutral.textOnPrimary : theme.color.neutral.textSubtle,
96
+ a: {
97
+ color: state.isSelected ? theme.color.neutral.textOnPrimary : theme.color.neutral.textSubtle,
98
+ '&:hover': {
99
+ color: state.isSelected ? theme.color.neutral.textOnPrimary : theme.color.neutral.textSubtle,
100
+ },
101
+ textDecoration: 'underline',
102
+ },
103
+ },
104
+
105
+ '&:hover': {
106
+ backgroundColor: state.isSelected ? undefined : theme.color.neutral.hover,
107
+ },
108
+ }),
109
+ noOptionsMessage: (base) => ({
110
+ ...base,
111
+ color: theme.color.neutral.textSubtle,
112
+ }),
113
+ singleValue: (base, { selectProps: { isDisabled } }) => ({
114
+ ...base,
115
+ color: isDisabled ? theme.color.neutral.textDisabled : theme.color.neutral.text,
116
+ }),
117
+ placeholder: (base) => ({
118
+ ...base,
119
+ color: theme.color.neutral.textPlaceholder,
120
+ fontStyle: 'italic',
121
+ }),
122
+ input: (base) => ({
123
+ ...base,
124
+ color: theme.color.neutral.text,
125
+ }),
126
+ };
127
+
128
+ type GetIsSelectedFn = (
129
+ options: SelectOption[],
130
+ value: SelectOptionValue | SelectOptionValue[] | null | undefined,
131
+ isMulti?: boolean,
132
+ ) => SelectOption | SelectOption[] | null;
133
+
134
+ /* eslint-disable @typescript-eslint/no-explicit-any */
135
+ export type SelectPrimitiveProps = {
136
+ options: SelectOption[];
137
+ value?: SelectOptionValue | SelectOptionValue[] | null;
138
+ setValue: (value: SelectOptionValue | SelectOptionValue[]) => void;
139
+ getIsSelected?: GetIsSelectedFn;
140
+ isMulti?: boolean;
141
+ isClearable?: boolean;
142
+ disabled?: boolean;
143
+ readOnly?: boolean;
144
+ error?: boolean | string;
145
+ components?: Record<string, React.ComponentType<any>>;
146
+ suffix?: string;
147
+ menuMinWidth?: number;
148
+ menuPosition?: 'fixed' | 'absolute';
149
+ className?: string;
150
+ as?: React.ElementType;
151
+ instanceId?: string;
152
+ /**
153
+ * Whether the select is rendered inside a modal. When `true`, the menu position defaults to `'fixed'`
154
+ * and scroll blocking is enabled to prevent the menu from scrolling away in large modals.
155
+ * Also affects the z-index of the menu portal (defaults to 99 inside modal, 9 outside).
156
+ */
157
+ isRenderedInsideModal?: boolean;
158
+ /**
159
+ * Custom z-index for the menu portal. When not provided, defaults to 99 when `isRenderedInsideModal`
160
+ * is `true`, and 9 otherwise.
161
+ */
162
+ menuPortalZIndex?: number;
163
+ // Allow additional props to be passed through to react-select
164
+ [key: string]: any;
165
+ };
166
+ /* eslint-enable @typescript-eslint/no-explicit-any */
167
+
168
+ export const selectDefaultGetIsSelected: GetIsSelectedFn = (options, value, isMulti) => {
169
+ if (!isMulti) return options.find((option) => option.value === value) || null;
170
+ // Map the value array to options while keeping order
171
+ return (value as SelectOptionValue[])
172
+ ?.map((val) => options.find((opt) => opt.value === val))
173
+ .filter(Boolean) as SelectOption[];
174
+ };
175
+
176
+ const StyledIndicatorsContainer = styled(withReactSelectTheme(selectComponents.IndicatorsContainer))`
177
+ & > div{
178
+ padding: 6px !important;
179
+ }
180
+
181
+ .indicator-slot {
182
+ padding: 6px;
183
+ display: flex;
184
+ align-items: center;
185
+
186
+ svg:not(.error-svg){
187
+ color: ${theme.color.neutral.icon};
188
+ }
189
+ }
190
+ `;
191
+
192
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
193
+ type ReactSelectStyledProps = Record<string, any>;
194
+
195
+ const getMenuPortalZIndex = (props: ReactSelectStyledProps) => props.selectProps?.menuPortalZIndex;
196
+ const getMenuPortalMinWidth = (props: ReactSelectStyledProps) => (props.selectProps?.menuMinWidth || 0);
197
+
198
+ const StyledMenuPortal = styled(withReactSelectTheme(selectComponents.MenuPortal))`
199
+ z-index: ${getMenuPortalZIndex} !important;
200
+ min-width: ${getMenuPortalMinWidth}px !important;
201
+ `;
202
+
203
+ const StyledControl = styled(withReactSelectTheme(selectComponents.Control))`
204
+ min-height: 36px !important;
205
+ background-color: ${theme.color.neutral.fieldBackground} !important;
206
+ border: 1px solid ${theme.color.neutral.fieldBorder} !important;
207
+ border-radius: 6px !important;
208
+ transition: border-color 120ms ease-out, background-color 120ms ease-out !important;
209
+ padding: 0 6px;
210
+
211
+ &:hover {
212
+ background-color: ${theme.color.neutral.hover} !important;
213
+ }
214
+
215
+ &:focus-within{
216
+ border-color: ${theme.color.primary.fieldBorderActive} !important;
217
+ background-color: ${theme.color.neutral.background} !important;
218
+ box-shadow: var(--shadow-active) !important;
219
+ }
220
+
221
+ .disabled & {
222
+ border-color: ${theme.color.neutral.disabled} !important;
223
+ background-color: ${theme.color.neutral.disabled} !important;
224
+ }
225
+
226
+ .error & {
227
+ background-color: ${theme.color.danger.backgroundSubtle} !important;
228
+ border: 1px solid ${theme.color.danger.fieldBorder} !important;
229
+
230
+ &:hover {
231
+ background-color: ${theme.color.danger.backgroundSubtleHover};
232
+ border: 1px solid ${theme.color.primary.chipText} !important;
233
+ }
234
+ }
235
+ `;
236
+
237
+ const StyledOption = styled(withReactSelectTheme(selectComponents.Option))`
238
+ ${theme.typography.shared.desktop.bodyM}
239
+ `;
240
+
241
+ const Option: FC<OptionProps<SelectOption>> = ({ children, data: { description }, ...props }) => (
242
+ <StyledOption {...props}>
243
+ {children}
244
+ {description && (
245
+ <Text size="small">{description}</Text>
246
+ )}
247
+ </StyledOption>
248
+ );
249
+
250
+ type SelectIndicatorsContainerProps = IndicatorsContainerProps<SelectOption> & {
251
+ selectProps: { indicator?: ReactNode; suffix?: string };
252
+ };
253
+
254
+ const IndicatorsContainer: FC<SelectIndicatorsContainerProps> = ({
255
+ selectProps: { indicator, suffix }, children, ...props
256
+ }) => {
257
+ return (
258
+ <StyledIndicatorsContainer {...props}>
259
+ {(indicator || suffix) && (
260
+ <div className='indicator-slot'>
261
+ <Text color={theme.color.neutral.textSubtle}>{suffix}</Text>
262
+ {indicator}
263
+ </div>
264
+ )}
265
+ {children}
266
+ </StyledIndicatorsContainer>
267
+ );
268
+ };
269
+
270
+ // TODO JK: Use DropdownIcon
271
+ const DropdownIndicator: FC<DropdownIndicatorProps<SelectOption>> = ({ innerProps, selectProps }) => (
272
+ <IconButton
273
+ Icon={selectProps.menuIsOpen ? CaretUpIcon : CaretDownIcon}
274
+ {...innerProps as React.HTMLAttributes<Element>}
275
+ />
276
+ );
277
+
278
+ const ClearIndicator: FC<ClearIndicatorProps<SelectOption>> = ({ innerProps, selectProps }) => {
279
+ return (
280
+ <IconButton
281
+ Icon={CrossIcon}
282
+ data-test={`${selectProps?.id ?? selectProps?.name}--clear`}
283
+ {...innerProps as React.HTMLAttributes<Element>}
284
+ />
285
+ );
286
+ };
287
+
288
+ const MultiValueRemoveButton = styled(IconButton)`
289
+ height: auto;
290
+ :hover {
291
+ color: ${theme.color.neutral.textSubtle};
292
+ background-color: transparent;
293
+ }
294
+ `;
295
+
296
+ export const SelectMultiValueRemove: FC<Pick<MultiValueRemoveProps<SelectOption>, 'innerProps'>> = ({ innerProps }) => (
297
+ <MultiValueRemoveButton
298
+ Icon={CrossIcon}
299
+ {...innerProps as React.HTMLAttributes<Element>}
300
+ />
301
+ );
302
+
303
+ const IndicatorSeparator = () => null; // we don't display anything
304
+
305
+ /**
306
+ * A context-free select component built on `react-select` with Apify styling.
307
+ *
308
+ * Use the `isRenderedInsideModal` prop to control modal-aware behavior (z-index, menu positioning,
309
+ * scroll blocking). Use `menuPortalZIndex` to override the default z-index values.
310
+ *
311
+ * In the console frontend, prefer using the `Select` wrapper from `primitives/select` which
312
+ * automatically injects these props via `ModalContext`.
313
+ */
314
+ export const SelectPrimitive = forwardRef<unknown, SelectPrimitiveProps>(({
315
+ options,
316
+ value,
317
+ setValue,
318
+ disabled,
319
+ readOnly,
320
+ error,
321
+ suffix,
322
+ className,
323
+ components,
324
+ isMulti,
325
+ isClearable = true,
326
+ menuPosition,
327
+ getIsSelected,
328
+ as,
329
+ isRenderedInsideModal = false,
330
+ menuPortalZIndex,
331
+ ...props
332
+ }, ref) => {
333
+ const selectedOptions = useMemo(
334
+ // this enables to override default selection mechanism (which compares option.value)
335
+ () => (getIsSelected
336
+ ? getIsSelected(options, value, isMulti)
337
+ : selectDefaultGetIsSelected(options, value, isMulti)),
338
+ [getIsSelected, options, value, isMulti],
339
+ );
340
+
341
+ const onSelect = useCallback((selected: SelectOption | SelectOption[] | null) => {
342
+ if (!isMulti) {
343
+ // We use empty string instead of null for empty values. The reason is that SimpleSchema field type
344
+ // is usually string so for initial values, we set '' as initial value.
345
+ // If we select & cancel some value, it should go back to ''
346
+ setValue(selected ? (selected as SelectOption).value : '');
347
+ return;
348
+ }
349
+ setValue((selected as SelectOption[]).map((option) => option.value));
350
+ }, [setValue, isMulti]);
351
+
352
+ const effectiveMenuPosition = menuPosition || (isRenderedInsideModal ? 'fixed' : 'absolute');
353
+ const resolvedMenuPortalZIndex = menuPortalZIndex
354
+ ?? (isRenderedInsideModal ? DEFAULT_Z_INDEX_IN_MODAL : DEFAULT_Z_INDEX_OUTSIDE_MODAL);
355
+
356
+ const indicator = error && <XCircleIcon size="16" color={theme.color.danger.icon} className='error-svg' />;
357
+
358
+ const Component = as || SelectComponent;
359
+
360
+ return (
361
+ <Component
362
+ components={{
363
+ DropdownIndicator,
364
+ ClearIndicator,
365
+ IndicatorSeparator,
366
+ MultiValueRemove: SelectMultiValueRemove, // react-select slot name
367
+ IndicatorsContainer,
368
+ Option,
369
+ Control: StyledControl,
370
+ MenuPortal: StyledMenuPortal,
371
+ ...components,
372
+ }}
373
+ ref={ref}
374
+ openMenuOnFocus
375
+ isDisabled={disabled || readOnly}
376
+ error={error}
377
+ options={options}
378
+ closeMenuOnSelect={!isMulti}
379
+ value={selectedOptions}
380
+ isMulti={isMulti}
381
+ isClearable={isClearable}
382
+ onChange={onSelect}
383
+ isRenderedInsideModal={isRenderedInsideModal}
384
+ // Used by getMenuPortalZIndex fnc of StyledMenuPortal to determine z-index of the menu portal
385
+ menuPortalZIndex={resolvedMenuPortalZIndex}
386
+ menuPosition={effectiveMenuPosition}
387
+ // If someone decides to debug this behavior, 'overflow: auto' on .Modal-content has huge effect on how menu renders
388
+ menuShouldBlockScroll={isRenderedInsideModal} // Otherwise menu scrolls away in big modal
389
+ indicator={indicator}
390
+ suffix={suffix}
391
+ styles={selectStyles}
392
+ {...props}
393
+ className={clsx(
394
+ className,
395
+ error && 'error',
396
+ (disabled || readOnly) && 'disabled',
397
+ )}
398
+ />
399
+ );
400
+ });
401
+
402
+ SelectPrimitive.displayName = 'SelectPrimitive';
@@ -0,0 +1 @@
1
+ export * from './switch.js';
@@ -0,0 +1,38 @@
1
+ import { useState } from 'react';
2
+ import styled from 'styled-components';
3
+ import { SwitchPrimitive as Switch } from './switch';
4
+
5
+ export default {
6
+ title: 'UI-Library/Inputs/Switch',
7
+ args: {},
8
+ };
9
+
10
+ const Grid = styled.div`
11
+ margin: 5rem 3rem;
12
+ display: grid;
13
+ grid-template-columns: repeat(2, auto);
14
+ gap: 1rem 5rem;
15
+
16
+ input{
17
+ width: 100%;
18
+ }
19
+ `;
20
+
21
+ export const SwitchPrimitive = () => {
22
+ const [value, setValue] = useState(1);
23
+ return (
24
+ <Grid>
25
+ <Switch value={value} setValue={setValue}/> <p>checked vs unchecked</p>
26
+ <Switch name="Disabled"
27
+ disabled
28
+ value={false}
29
+ setValue={() => {}}
30
+ /> <p>disabled</p>
31
+ <Switch name="Error"
32
+ error='Test error'
33
+ value={value}
34
+ setValue={setValue}
35
+ /> <p>error</p>
36
+ </Grid>
37
+ );
38
+ };