@apify/ui-library 1.141.2 → 1.145.2-featverifieddeveloperbadge-bd59c6.13

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 (98) hide show
  1. package/README.md +90 -19
  2. package/dist/src/components/box/box.d.ts +15 -14
  3. package/dist/src/components/box/box.d.ts.map +1 -1
  4. package/dist/src/components/box/box.js +10 -33
  5. package/dist/src/components/box/box.js.map +1 -1
  6. package/dist/src/components/breadcrumb/breadcrumb.d.ts.map +1 -1
  7. package/dist/src/components/breadcrumb/breadcrumb.js +1 -0
  8. package/dist/src/components/breadcrumb/breadcrumb.js.map +1 -1
  9. package/dist/src/components/card/card.d.ts.map +1 -1
  10. package/dist/src/components/card/card.js +1 -0
  11. package/dist/src/components/card/card.js.map +1 -1
  12. package/dist/src/components/chip/chip.d.ts.map +1 -1
  13. package/dist/src/components/chip/chip.js +2 -0
  14. package/dist/src/components/chip/chip.js.map +1 -1
  15. package/dist/src/components/code/code_block/code_block.styled.d.ts.map +1 -1
  16. package/dist/src/components/code/code_block/code_block.styled.js +1 -2
  17. package/dist/src/components/code/code_block/code_block.styled.js.map +1 -1
  18. package/dist/src/components/code/one_light_theme.d.ts.map +1 -1
  19. package/dist/src/components/code/one_light_theme.js +1 -0
  20. package/dist/src/components/code/one_light_theme.js.map +1 -1
  21. package/dist/src/components/dropdown/dropdown.context.d.ts +6 -0
  22. package/dist/src/components/dropdown/dropdown.context.d.ts.map +1 -0
  23. package/dist/src/components/dropdown/dropdown.context.js +10 -0
  24. package/dist/src/components/dropdown/dropdown.context.js.map +1 -0
  25. package/dist/src/components/dropdown/dropdown.d.ts +48 -0
  26. package/dist/src/components/dropdown/dropdown.d.ts.map +1 -0
  27. package/dist/src/components/dropdown/dropdown.js +65 -0
  28. package/dist/src/components/dropdown/dropdown.js.map +1 -0
  29. package/dist/src/components/dropdown/dropdown.styled.d.ts +35 -0
  30. package/dist/src/components/dropdown/dropdown.styled.d.ts.map +1 -0
  31. package/dist/src/components/dropdown/dropdown.styled.js +241 -0
  32. package/dist/src/components/dropdown/dropdown.styled.js.map +1 -0
  33. package/dist/src/components/dropdown/dropdown.types.d.ts +17 -0
  34. package/dist/src/components/dropdown/dropdown.types.d.ts.map +1 -0
  35. package/dist/src/components/dropdown/dropdown.types.js +2 -0
  36. package/dist/src/components/dropdown/dropdown.types.js.map +1 -0
  37. package/dist/src/components/dropdown/dropdown_button.d.ts +10 -0
  38. package/dist/src/components/dropdown/dropdown_button.d.ts.map +1 -0
  39. package/dist/src/components/dropdown/dropdown_button.js +16 -0
  40. package/dist/src/components/dropdown/dropdown_button.js.map +1 -0
  41. package/dist/src/components/dropdown/dropdown_root.d.ts +11 -0
  42. package/dist/src/components/dropdown/dropdown_root.d.ts.map +1 -0
  43. package/dist/src/components/dropdown/dropdown_root.js +19 -0
  44. package/dist/src/components/dropdown/dropdown_root.js.map +1 -0
  45. package/dist/src/components/dropdown/dropdown_shell.d.ts +15 -0
  46. package/dist/src/components/dropdown/dropdown_shell.d.ts.map +1 -0
  47. package/dist/src/components/dropdown/dropdown_shell.js +69 -0
  48. package/dist/src/components/dropdown/dropdown_shell.js.map +1 -0
  49. package/dist/src/components/dropdown/index.d.ts +7 -0
  50. package/dist/src/components/dropdown/index.d.ts.map +1 -0
  51. package/dist/src/components/dropdown/index.js +5 -0
  52. package/dist/src/components/dropdown/index.js.map +1 -0
  53. package/dist/src/components/index.d.ts +1 -0
  54. package/dist/src/components/index.d.ts.map +1 -1
  55. package/dist/src/components/index.js +1 -0
  56. package/dist/src/components/index.js.map +1 -1
  57. package/dist/src/components/message/message.d.ts +2 -0
  58. package/dist/src/components/message/message.d.ts.map +1 -1
  59. package/dist/src/components/message/message.js +2 -0
  60. package/dist/src/components/message/message.js.map +1 -1
  61. package/dist/src/components/pagination/pagination.d.ts.map +1 -1
  62. package/dist/src/components/pagination/pagination.js +1 -0
  63. package/dist/src/components/pagination/pagination.js.map +1 -1
  64. package/dist/src/components/tabs/tab.js +1 -1
  65. package/dist/src/components/tabs/tab.js.map +1 -1
  66. package/dist/src/utils/css_utils.d.ts +17 -0
  67. package/dist/src/utils/css_utils.d.ts.map +1 -1
  68. package/dist/src/utils/css_utils.js +54 -0
  69. package/dist/src/utils/css_utils.js.map +1 -1
  70. package/dist/tsconfig.build.tsbuildinfo +1 -1
  71. package/package.json +10 -5
  72. package/src/components/box/box.stories.tsx +18 -0
  73. package/src/components/box/box.tsx +31 -62
  74. package/src/components/breadcrumb/breadcrumb.tsx +1 -0
  75. package/src/components/card/card.tsx +1 -0
  76. package/src/components/chip/chip.tsx +2 -0
  77. package/src/components/code/code_block/code_block.styled.tsx +1 -2
  78. package/src/components/code/one_light_theme.ts +1 -0
  79. package/src/components/dropdown/dropdown.context.tsx +14 -0
  80. package/src/components/dropdown/dropdown.stories.tsx +343 -0
  81. package/src/components/dropdown/dropdown.styled.ts +265 -0
  82. package/src/components/dropdown/dropdown.tsx +259 -0
  83. package/src/components/dropdown/dropdown.types.ts +18 -0
  84. package/src/components/dropdown/dropdown_button.tsx +45 -0
  85. package/src/components/dropdown/dropdown_root.tsx +41 -0
  86. package/src/components/dropdown/dropdown_shell.tsx +139 -0
  87. package/src/components/dropdown/index.ts +6 -0
  88. package/src/components/index.ts +1 -0
  89. package/src/components/message/message.stories.jsx +1 -0
  90. package/src/components/message/message.tsx +2 -0
  91. package/src/components/pagination/pagination.tsx +1 -0
  92. package/src/components/tabs/tab.tsx +1 -1
  93. package/src/design_system/colors/build_color_tokens.js +14 -0
  94. package/src/design_system/colors/colors.stories.tsx +395 -0
  95. package/src/utils/css_utils.ts +79 -0
  96. package/style/colors/tokens.dark.css +149 -0
  97. package/style/colors/tokens.light.css +149 -0
  98. package/src/design_system/colors/Colors.mdx +0 -50
@@ -0,0 +1,265 @@
1
+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
2
+ import styled from 'styled-components';
3
+
4
+ import { theme } from '../../design_system/theme.js';
5
+
6
+ export const DROPDOWN_ELEMENT_CLASSES = {
7
+ WRAPPER: 'dropdown-wrapper',
8
+ ITEM: 'dropdown-item',
9
+ TRIGGER_OVERLAY: 'dropdown-trigger-overlay',
10
+ RADIO_OPTION: 'dropdown-radio-option',
11
+ RADIO_OPTION_DISABLED: 'dropdown-radio-option-disabled',
12
+ RADIO_OPTION_SELECTED: 'dropdown-radio-option-selected',
13
+ RADIO_OPTION_ICON: 'dropdown-radio-option-icon',
14
+ RADIO_OPTION_CONTENT: 'dropdown-radio-option-content',
15
+ };
16
+
17
+ type StyledContentProps = {
18
+ $width?: string;
19
+ $height?: string;
20
+ $zIndex?: number;
21
+ };
22
+
23
+ export const StyledContent = styled(DropdownMenu.Content).attrs<
24
+ DropdownMenu.DropdownMenuContentProps & { 'data-test'?: string }
25
+ >((props) => ({
26
+ 'data-test': 'dropdown-wrapper',
27
+ className: [DROPDOWN_ELEMENT_CLASSES.WRAPPER, props.className].filter(Boolean).join(' '),
28
+ }))<StyledContentProps>`
29
+ background: ${theme.color.neutral.background};
30
+ border: 1px solid ${theme.color.neutral.separatorSubtle};
31
+ border-radius: ${theme.radius.radius12};
32
+ padding: ${theme.space.space6} 0;
33
+ min-width: 80px;
34
+ width: ${(props) => props.$width || 'auto'};
35
+ height: ${(props) => props.$height || 'auto'};
36
+ display: flex;
37
+ flex-direction: column;
38
+ overflow: hidden;
39
+ z-index: ${(props) => props.$zIndex ?? 100} !important;
40
+ position: relative;
41
+ `;
42
+
43
+ export const ScrollableContent = styled.div`
44
+ flex: 1;
45
+ overflow-y: auto;
46
+ overflow-x: hidden;
47
+ gap: ${theme.space.space4};
48
+ display: flex;
49
+ flex-direction: column;
50
+ scrollbar-width: thin;
51
+ scrollbar-color: ${theme.color.neutral.separatorSubtle} transparent;
52
+
53
+ .${DROPDOWN_ELEMENT_CLASSES.TRIGGER_OVERLAY} {
54
+ position: absolute;
55
+ top: 0;
56
+ left: 0;
57
+ width: 100%;
58
+ height: 100%;
59
+ cursor: pointer;
60
+ z-index: 1;
61
+ pointer-events: none;
62
+ }
63
+ .${DROPDOWN_ELEMENT_CLASSES.RADIO_OPTION} {
64
+ cursor: pointer;
65
+ width: 100%;
66
+ display: flex;
67
+ align-items: flex-start;
68
+ padding: ${theme.space.space4} ${theme.space.space8};
69
+ border-radius: ${theme.radius.radius4};
70
+ gap: ${theme.space.space8};
71
+ min-height: 28px;
72
+
73
+ &:hover {
74
+ background: ${theme.color.neutral.hover};
75
+ }
76
+ }
77
+ .${DROPDOWN_ELEMENT_CLASSES.RADIO_OPTION_DISABLED} {
78
+ cursor: not-allowed;
79
+ color: ${theme.color.neutral.textDisabled};
80
+
81
+ * {
82
+ color: ${theme.color.neutral.textDisabled} !important;
83
+ }
84
+ }
85
+ .${DROPDOWN_ELEMENT_CLASSES.RADIO_OPTION_SELECTED} {
86
+ background: none;
87
+ color: ${theme.color.neutral.text};
88
+ }
89
+ .${DROPDOWN_ELEMENT_CLASSES.RADIO_OPTION_ICON} {
90
+ display: flex;
91
+ align-items: flex-start;
92
+ justify-content: center;
93
+ padding: ${theme.space.space2} 0;
94
+ min-width: 16px;
95
+ min-height: 20px;
96
+ margin-right: ${theme.space.space4};
97
+ }
98
+ .${DROPDOWN_ELEMENT_CLASSES.RADIO_OPTION_CONTENT} {
99
+ flex: 1;
100
+ display: flex;
101
+ flex-direction: column;
102
+ min-width: 0;
103
+ max-width: 100%;
104
+ }
105
+ `;
106
+
107
+ export const StyledItemRow = styled.div`
108
+ display: flex;
109
+ align-items: flex-start;
110
+ width: 100%;
111
+ gap: ${theme.space.space4};
112
+ min-height: 28px;
113
+ padding: ${theme.space.space4} 0;
114
+ `;
115
+
116
+ export const StyledItemWrapper = styled.div`
117
+ margin: 0 ${theme.space.space6};
118
+ `;
119
+
120
+ export const StyledItem = styled(DropdownMenu.Item)`
121
+ min-width: 100%;
122
+ display: block;
123
+ padding: 0 ${theme.space.space8};
124
+ color: ${theme.color.neutral.text};
125
+ border-radius: ${theme.radius.radius6};
126
+ cursor: pointer;
127
+ background: none;
128
+ border: none;
129
+ transition: background 0.15s;
130
+ outline: none;
131
+ box-shadow: none;
132
+
133
+ &[data-highlighted],
134
+ &.dropdown-item-selected {
135
+ background: ${theme.color.neutral.hover};
136
+ }
137
+
138
+ &[data-disabled] {
139
+ color: ${theme.color.neutral.textDisabled};
140
+ cursor: not-allowed;
141
+ }
142
+ &.no-hover {
143
+ cursor: default;
144
+ &[data-highlighted],
145
+ &.dropdown-item-selected,
146
+ &:hover {
147
+ background: none !important;
148
+ }
149
+ }
150
+ `;
151
+
152
+ export const StyledCheckboxItem = styled(DropdownMenu.CheckboxItem)`
153
+ min-width: 100%;
154
+ display: block;
155
+ padding: 0 ${theme.space.space8};
156
+ color: ${theme.color.neutral.text};
157
+ border-radius: ${theme.radius.radius4};
158
+ cursor: pointer;
159
+ background: none;
160
+ border: none;
161
+ transition: background 0.15s;
162
+ outline: none;
163
+ box-shadow: none;
164
+
165
+ &[data-highlighted],
166
+ &:hover {
167
+ background: ${theme.color.neutral.hover};
168
+ }
169
+
170
+ &[data-disabled] {
171
+ color: ${theme.color.neutral.textDisabled};
172
+ cursor: not-allowed;
173
+ }
174
+
175
+ &[data-state='checked'] {
176
+ background: none;
177
+ }
178
+
179
+ &.no-hover {
180
+ cursor: default;
181
+
182
+ &[data-highlighted],
183
+ &:hover {
184
+ background: none !important;
185
+ }
186
+ }
187
+ `;
188
+
189
+ export const StyledLabel = styled(DropdownMenu.Label)`
190
+ padding: ${theme.space.space8};
191
+ padding-bottom: ${theme.space.space2};
192
+ color: ${theme.color.neutral.textSubtle};
193
+ ${theme.typography.shared.desktop.bodyS}
194
+ display: flex;
195
+ align-items: center;
196
+ gap: ${theme.space.space4};
197
+ `;
198
+
199
+ export const StyledDescription = styled.div`
200
+ color: ${theme.color.neutral.textSubtle};
201
+ ${theme.typography.shared.desktop.bodyS}
202
+ display: block;
203
+ width: 100%;
204
+ margin-top: ${theme.space.space2};
205
+
206
+ [data-disabled] & {
207
+ color: ${theme.color.neutral.textDisabled};
208
+ }
209
+ `;
210
+
211
+ export const StyledItemTitle = styled.span`
212
+ ${theme.typography.shared.desktop.bodyM}
213
+ color: ${theme.color.neutral.textMuted};
214
+ display: block;
215
+ width: 100%;
216
+ margin-right: ${theme.space.space32};
217
+
218
+ [data-disabled] & {
219
+ color: ${theme.color.neutral.textDisabled};
220
+ }
221
+ `;
222
+
223
+ export const StyledIconWrapper = styled.span`
224
+ display: flex;
225
+ justify-content: center;
226
+ align-items: flex-start;
227
+ flex-shrink: 0;
228
+ width: 20px;
229
+ padding-top: ${theme.space.space2};
230
+ color: ${theme.color.neutral.textMuted};
231
+ `;
232
+
233
+ export const StyledCheckboxIconWrapper = styled(StyledIconWrapper)`
234
+ margin-right: ${theme.space.space4};
235
+ `;
236
+
237
+ export const StyledContentWrapper = styled.div`
238
+ flex: 1;
239
+ display: flex;
240
+ flex-direction: column;
241
+ align-items: flex-start;
242
+ min-width: 0;
243
+ width: max-content;
244
+ overflow: hidden;
245
+ `;
246
+
247
+ export const StyledTagWrapper = styled.div`
248
+ display: flex;
249
+ gap: ${theme.space.space4};
250
+ `;
251
+
252
+ export const StyledControls = styled.div`
253
+ padding-top: ${theme.space.space6};
254
+ border-top: 1px solid ${theme.color.neutral.separatorSubtle};
255
+ gap: ${theme.space.space4};
256
+ display: flex;
257
+ flex-direction: column;
258
+ `;
259
+
260
+ export const StyledSeparator = styled(DropdownMenu.Separator)`
261
+ height: 1px;
262
+ min-height: 1px;
263
+ margin: ${theme.space.space4} ${theme.space.space8};
264
+ background-color: ${theme.color.neutral.separatorSubtle};
265
+ `;
@@ -0,0 +1,259 @@
1
+ import type * as DropdownMenu from '@radix-ui/react-dropdown-menu';
2
+ import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
3
+ import { clsx } from 'clsx';
4
+ import React from 'react';
5
+ import styled from 'styled-components';
6
+
7
+ import { CheckIcon } from '@apify/ui-icons';
8
+
9
+ import { theme } from '../../design_system/theme.js';
10
+ import { CheckboxPrimitive } from '../checkbox/checkbox.js';
11
+ import { radioButtonStyle } from '../radio_button/radio_button.style.js';
12
+ import { useDropdownContext } from './dropdown.context.js';
13
+ import {
14
+ DROPDOWN_ELEMENT_CLASSES,
15
+ StyledCheckboxIconWrapper,
16
+ StyledCheckboxItem,
17
+ StyledContentWrapper,
18
+ StyledControls,
19
+ StyledDescription,
20
+ StyledIconWrapper,
21
+ StyledItem,
22
+ StyledItemRow,
23
+ StyledItemTitle,
24
+ StyledItemWrapper,
25
+ StyledLabel,
26
+ StyledSeparator,
27
+ StyledTagWrapper,
28
+ } from './dropdown.styled.js';
29
+ import type { BaseDropdownItemProps } from './dropdown.types.js';
30
+ import { DropdownRoot } from './dropdown_root.js';
31
+
32
+ type DropdownItemProps = React.ComponentPropsWithRef<typeof DropdownMenu.Item> &
33
+ BaseDropdownItemProps & {
34
+ trailingIcon?: React.ReactNode;
35
+ selected?: boolean;
36
+ onSelect?: (event: Event) => void;
37
+ tags?: React.ReactNode[];
38
+ };
39
+
40
+ const DropdownItem = ({
41
+ children,
42
+ icon,
43
+ trailingIcon,
44
+ selected,
45
+ description,
46
+ onSelect,
47
+ className,
48
+ disabled,
49
+ tags = [],
50
+ ...props
51
+ }: DropdownItemProps) => {
52
+ const { handleSelectAndClose } = useDropdownContext();
53
+ const handleSelect = (event: Event) => {
54
+ if (onSelect) onSelect(event);
55
+ handleSelectAndClose(event);
56
+ };
57
+
58
+ return (
59
+ <StyledItemWrapper>
60
+ <StyledItem
61
+ onSelect={handleSelect}
62
+ disabled={disabled}
63
+ {...props}
64
+ className={clsx(DROPDOWN_ELEMENT_CLASSES.ITEM, className, selected && 'dropdown-item-selected')}
65
+ data-test={disabled ? 'dropdown-item-disabled' : 'dropdown-item'}
66
+ >
67
+ <StyledItemRow>
68
+ {icon && <StyledIconWrapper>{icon}</StyledIconWrapper>}
69
+ <StyledContentWrapper>
70
+ <StyledItemTitle>{children}</StyledItemTitle>
71
+ {description && <StyledDescription>{description}</StyledDescription>}
72
+ </StyledContentWrapper>
73
+ {(trailingIcon || selected) && (
74
+ <StyledIconWrapper>
75
+ {trailingIcon}
76
+ {!trailingIcon && selected && <CheckIcon size="16" color={theme.color.neutral.text} />}
77
+ </StyledIconWrapper>
78
+ )}
79
+ {tags.length > 0 && (
80
+ <StyledTagWrapper onClick={(e) => e.stopPropagation()}>{tags}</StyledTagWrapper>
81
+ )}
82
+ </StyledItemRow>
83
+ </StyledItem>
84
+ </StyledItemWrapper>
85
+ );
86
+ };
87
+
88
+ type DropdownCheckboxItemProps = React.ComponentPropsWithRef<typeof DropdownMenu.CheckboxItem> &
89
+ BaseDropdownItemProps & {
90
+ checked?: boolean;
91
+ trailingIcon?: React.ReactNode;
92
+ onCheckedChange?: (checked: boolean) => void;
93
+ onSelect?: (event: Event) => void;
94
+ };
95
+
96
+ const DropdownCheckboxItem = ({
97
+ checked,
98
+ children,
99
+ icon,
100
+ trailingIcon,
101
+ onCheckedChange,
102
+ description,
103
+ onSelect,
104
+ disabled,
105
+ className,
106
+ ...props
107
+ }: DropdownCheckboxItemProps) => {
108
+ const { handleSelectAndClose } = useDropdownContext();
109
+ const handleSelect = (event: Event) => {
110
+ if (disabled) return;
111
+ if (onSelect) onSelect(event);
112
+ handleSelectAndClose(event);
113
+ };
114
+
115
+ return (
116
+ <StyledItemWrapper>
117
+ <StyledCheckboxItem
118
+ checked={checked}
119
+ onSelect={handleSelect}
120
+ disabled={disabled}
121
+ {...props}
122
+ onCheckedChange={disabled ? undefined : onCheckedChange}
123
+ className={clsx(className)}
124
+ data-test={disabled ? 'dropdown-item-disabled' : 'dropdown-item'}
125
+ >
126
+ <StyledItemRow>
127
+ <StyledCheckboxIconWrapper>
128
+ <CheckboxPrimitive
129
+ value={!!checked}
130
+ setValue={disabled ? () => undefined : onCheckedChange || (() => undefined)}
131
+ readOnly={disabled || !onCheckedChange}
132
+ />
133
+ </StyledCheckboxIconWrapper>
134
+ {icon && <StyledIconWrapper>{icon}</StyledIconWrapper>}
135
+ <StyledContentWrapper>
136
+ <StyledItemTitle>{children}</StyledItemTitle>
137
+ {description && <StyledDescription>{description}</StyledDescription>}
138
+ </StyledContentWrapper>
139
+ {trailingIcon && <StyledIconWrapper>{trailingIcon}</StyledIconWrapper>}
140
+ </StyledItemRow>
141
+ </StyledCheckboxItem>
142
+ </StyledItemWrapper>
143
+ );
144
+ };
145
+
146
+ // --- Radio group: self-contained, uses radioButtonStyle from radio_button component ---
147
+
148
+ const StyledRadioGroupRoot = styled(RadioGroupPrimitive.Root)`
149
+ display: flex;
150
+ flex-direction: column;
151
+ `;
152
+
153
+ const StyledRadioGroupItem = styled(RadioGroupPrimitive.Item)`
154
+ ${radioButtonStyle}
155
+ flex-shrink: 0;
156
+ `;
157
+
158
+ type RadioOption = {
159
+ label: string;
160
+ description?: string;
161
+ disabled?: boolean;
162
+ value: string;
163
+ };
164
+
165
+ const DropdownRadioOptionLabel = ({ label, description }: { label: string; description?: string }) => (
166
+ <>
167
+ <StyledItemTitle>{label}</StyledItemTitle>
168
+ {description && <StyledDescription title={description}>{description}</StyledDescription>}
169
+ </>
170
+ );
171
+
172
+ type DropdownRadioOptionProps = {
173
+ option: RadioOption;
174
+ isSelected: boolean;
175
+ onSelect?: () => void;
176
+ children: React.ReactNode;
177
+ };
178
+
179
+ const DropdownRadioOption = ({ option, isSelected, onSelect, children }: DropdownRadioOptionProps) => (
180
+ <div
181
+ className={clsx(
182
+ DROPDOWN_ELEMENT_CLASSES.RADIO_OPTION,
183
+ option.disabled && DROPDOWN_ELEMENT_CLASSES.RADIO_OPTION_DISABLED,
184
+ isSelected && DROPDOWN_ELEMENT_CLASSES.RADIO_OPTION_SELECTED,
185
+ )}
186
+ onClick={onSelect}
187
+ aria-disabled={option.disabled}
188
+ >
189
+ {children}
190
+ </div>
191
+ );
192
+
193
+ type DropdownRadioGroupProps = {
194
+ options: RadioOption[];
195
+ value: string;
196
+ setValue: (value: string) => void;
197
+ };
198
+
199
+ const DropdownRadioGroup = ({ options, value, setValue }: DropdownRadioGroupProps) => {
200
+ const { handleSelectAndClose } = useDropdownContext();
201
+
202
+ const handleSelect = (newValue: string) => {
203
+ setValue(newValue);
204
+ handleSelectAndClose();
205
+ };
206
+
207
+ return (
208
+ <StyledItemWrapper>
209
+ <StyledRadioGroupRoot value={value} onValueChange={handleSelect}>
210
+ {options.map((option) => (
211
+ <DropdownRadioOption
212
+ key={option.value}
213
+ option={option}
214
+ isSelected={option.value === value}
215
+ onSelect={option.disabled ? undefined : () => handleSelect(option.value)}
216
+ >
217
+ <span className={DROPDOWN_ELEMENT_CLASSES.RADIO_OPTION_ICON}>
218
+ <StyledRadioGroupItem
219
+ value={option.value}
220
+ id={option.value}
221
+ disabled={option.disabled}
222
+ onClick={(e) => e.stopPropagation()}
223
+ />
224
+ </span>
225
+ <span className={DROPDOWN_ELEMENT_CLASSES.RADIO_OPTION_CONTENT}>
226
+ <DropdownRadioOptionLabel label={option.label} description={option.description} />
227
+ </span>
228
+ </DropdownRadioOption>
229
+ ))}
230
+ </StyledRadioGroupRoot>
231
+ </StyledItemWrapper>
232
+ );
233
+ };
234
+
235
+ type DropdownLabelProps = React.ComponentPropsWithRef<typeof DropdownMenu.Label> & {
236
+ children?: React.ReactNode | React.ReactNode[];
237
+ };
238
+
239
+ const DropdownLabel = (props: DropdownLabelProps) => <StyledLabel {...props} />;
240
+
241
+ type DropdownControlProps = React.ComponentPropsWithRef<typeof DropdownMenu.Label> & {
242
+ children?: React.ReactNode | React.ReactNode[];
243
+ };
244
+ const DropdownControls = (props: DropdownControlProps) => {
245
+ return <StyledControls>{props.children}</StyledControls>;
246
+ };
247
+
248
+ const DropdownSeparator = (props: React.ComponentPropsWithRef<typeof DropdownMenu.Separator>) => (
249
+ <StyledSeparator {...props} />
250
+ );
251
+
252
+ export const Dropdown = Object.assign(DropdownRoot, {
253
+ Item: DropdownItem,
254
+ CheckboxItem: DropdownCheckboxItem,
255
+ Label: DropdownLabel,
256
+ RadioGroup: DropdownRadioGroup,
257
+ Controls: DropdownControls,
258
+ Separator: DropdownSeparator,
259
+ });
@@ -0,0 +1,18 @@
1
+ import type * as DropdownMenu from '@radix-ui/react-dropdown-menu';
2
+ import type * as React from 'react';
3
+
4
+ export type BaseDropdownItemProps = {
5
+ children?: React.ReactNode;
6
+ icon?: React.ReactNode;
7
+ description?: React.ReactNode;
8
+ disabled?: boolean;
9
+ className?: string;
10
+ };
11
+
12
+ export type BaseDropdownContentProps = {
13
+ width?: string;
14
+ height?: string;
15
+ closeOnSelect?: boolean;
16
+ zIndex?: number;
17
+ contentProps?: React.ComponentPropsWithRef<typeof DropdownMenu.Content>;
18
+ };
@@ -0,0 +1,45 @@
1
+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
2
+ import { useState } from 'react';
3
+
4
+ import { Button } from '../button/button.js';
5
+ import { ScrollableContent, StyledContent } from './dropdown.styled.js';
6
+ import type { BaseDropdownContentProps } from './dropdown.types.js';
7
+ import { DropdownRoot } from './dropdown_root.js';
8
+
9
+ type DropdownButtonProps = BaseDropdownContentProps & {
10
+ buttonProps?: Partial<React.ComponentPropsWithoutRef<typeof Button>>;
11
+ buttonLabel: React.ReactNode;
12
+ children: React.ReactNode;
13
+ };
14
+
15
+ export const DropdownButton = ({
16
+ buttonProps,
17
+ buttonLabel,
18
+ children,
19
+ closeOnSelect = true,
20
+ contentProps,
21
+ width,
22
+ height,
23
+ zIndex,
24
+ }: DropdownButtonProps) => {
25
+ const [isOpen, setIsOpen] = useState(false);
26
+
27
+ const defaultButtonProps = {
28
+ onClick: () => undefined,
29
+ trackingId: 'dropdown-button',
30
+ ...buttonProps,
31
+ };
32
+
33
+ return (
34
+ <DropdownRoot isOpen={isOpen} onIsOpenChange={setIsOpen} closeOnSelect={closeOnSelect}>
35
+ <DropdownMenu.Trigger asChild>
36
+ <Button {...defaultButtonProps}>{buttonLabel}</Button>
37
+ </DropdownMenu.Trigger>
38
+ <DropdownMenu.Portal>
39
+ <StyledContent $width={width} $height={height} $zIndex={zIndex} {...contentProps}>
40
+ <ScrollableContent>{children}</ScrollableContent>
41
+ </StyledContent>
42
+ </DropdownMenu.Portal>
43
+ </DropdownRoot>
44
+ );
45
+ };
@@ -0,0 +1,41 @@
1
+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
2
+ import { useCallback, useState } from 'react';
3
+
4
+ import { DropdownContext } from './dropdown.context.js';
5
+
6
+ type DropdownRootProps = React.ComponentPropsWithRef<typeof DropdownMenu.Root> & {
7
+ closeOnSelect?: boolean;
8
+ children: React.ReactNode;
9
+ onIsOpenChange?: (isOpen: boolean) => void;
10
+ isOpen?: boolean;
11
+ contentProps?: React.ComponentPropsWithRef<typeof DropdownMenu.Content>;
12
+ };
13
+
14
+ export const DropdownRoot = ({
15
+ closeOnSelect = true,
16
+ children,
17
+ isOpen,
18
+ onIsOpenChange,
19
+ ...props
20
+ }: DropdownRootProps) => {
21
+ const [internalIsOpen, setInternalIsOpen] = useState(false);
22
+ const isControlled = isOpen !== undefined && onIsOpenChange !== undefined;
23
+ const effectiveIsOpen = isControlled ? isOpen : internalIsOpen;
24
+ const effectiveSetIsOpen = isControlled ? onIsOpenChange : setInternalIsOpen;
25
+ const closeDropdown = useCallback(() => effectiveSetIsOpen(false), [effectiveSetIsOpen]);
26
+ const handleSelectAndClose = useCallback(
27
+ (event?: Event) => {
28
+ if (closeOnSelect) closeDropdown();
29
+ if (event) event.preventDefault();
30
+ },
31
+ [closeOnSelect, closeDropdown],
32
+ );
33
+
34
+ return (
35
+ <DropdownContext.Provider value={{ handleSelectAndClose }}>
36
+ <DropdownMenu.Root open={effectiveIsOpen} onOpenChange={effectiveSetIsOpen} modal={false} {...props}>
37
+ {children}
38
+ </DropdownMenu.Root>
39
+ </DropdownContext.Provider>
40
+ );
41
+ };