@bitrise/bitkit 10.9.1 → 10.9.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bitrise/bitkit",
3
3
  "description": "Bitrise React component library",
4
- "version": "10.9.1",
4
+ "version": "10.9.2",
5
5
  "repository": "git@github.com:bitrise-io/bitkit.git",
6
6
  "main": "src/index.ts",
7
7
  "license": "UNLICENSED",
@@ -4,11 +4,12 @@ import { createContext } from '@chakra-ui/react-utils';
4
4
 
5
5
  const [DropdownStylesProvider, useDropdownStyles] = createStylesContext('Dropdown');
6
6
 
7
- export type DropdownEventArgs = { value: string; index: number | undefined; label: ReactNode };
7
+ export type DropdownEventArgs<T> = { value: T; index: number | undefined; label: ReactNode };
8
8
 
9
- type DropdownContext = {
10
- formValue: string;
11
- onOptionSelected: (arg: DropdownEventArgs) => void;
9
+ type DropdownContext<T> = {
10
+ formValue: T;
11
+ onOptionSelected: (arg: DropdownEventArgs<T>) => void;
12
+ listRef: React.RefObject<(HTMLElement | null)[]>;
12
13
  searchValue: string;
13
14
  searchRef: React.Ref<HTMLInputElement>;
14
15
  searchOnChange: (sv: string) => void;
@@ -18,20 +19,24 @@ type DropdownContext = {
18
19
  getItemProps: (x: object) => object;
19
20
  };
20
21
 
21
- const [DropdownContextProvider, useDropdownContext] = createContext<DropdownContext>();
22
+ const [DropdownContextProvider, useDropdownContext] = createContext<DropdownContext<unknown>>();
22
23
 
23
- const DropdownProvider = ({
24
+ function useDropdownContextTyped<T>(): DropdownContext<T> {
25
+ return useDropdownContext() as unknown as DropdownContext<T>;
26
+ }
27
+
28
+ const DropdownProvider = <T,>({
24
29
  styles,
25
30
  context,
26
31
  children,
27
32
  }: {
28
33
  styles: ComponentProps<typeof DropdownStylesProvider>['value'];
29
34
  children: ReactNode;
30
- context: DropdownContext;
35
+ context: DropdownContext<T>;
31
36
  }) => (
32
- <DropdownContextProvider value={context}>
37
+ <DropdownContextProvider value={context as DropdownContext<unknown>}>
33
38
  <DropdownStylesProvider value={styles}>{children}</DropdownStylesProvider>
34
39
  </DropdownContextProvider>
35
40
  );
36
41
 
37
- export { DropdownProvider, useDropdownContext, useDropdownStyles };
42
+ export { DropdownProvider, useDropdownContextTyped as useDropdownContext, useDropdownStyles };
@@ -56,6 +56,27 @@ export const WithDetail = () => {
56
56
  );
57
57
  };
58
58
 
59
+ export const WithLongOptions = () => {
60
+ return (
61
+ <Dropdown defaultValue="y" w="150px">
62
+ <DropdownOption value="y">
63
+ some very long option text longer than the component with verylongwordwithoutbreakopportunities
64
+ </DropdownOption>
65
+ <DropdownOption>some very long option text longer than the component</DropdownOption>
66
+ </Dropdown>
67
+ );
68
+ };
69
+
70
+ export const WithoutSearch = () => {
71
+ return (
72
+ <Dropdown search={false}>
73
+ <DropdownOption>opt1</DropdownOption>
74
+ <DropdownOption>opt2</DropdownOption>
75
+ <DropdownOption>opt3</DropdownOption>
76
+ </Dropdown>
77
+ );
78
+ };
79
+
59
80
  export const Default: ComponentStoryFn<typeof Dropdown> = (args) => {
60
81
  return (
61
82
  <form
@@ -1,4 +1,4 @@
1
- import { forwardRef, MutableRefObject, StrictMode, useState } from 'react';
1
+ import { ComponentProps, forwardRef, MutableRefObject, StrictMode, useState } from 'react';
2
2
  import { setTimeout } from 'timers/promises';
3
3
  import { render as renderRTL, screen, waitFor } from '@testing-library/react';
4
4
  import userEvent from '@testing-library/user-event';
@@ -12,12 +12,15 @@ const render = (ui: React.ReactElement) => {
12
12
  return renderRTL(<StrictMode>{ui}</StrictMode>);
13
13
  };
14
14
 
15
- const TestComponent = forwardRef<HTMLFormElement, { submit: (data: unknown) => void }>(({ submit, ...props }, ref) => {
15
+ const TestComponent = forwardRef<
16
+ HTMLFormElement,
17
+ { submit: (data: unknown) => void } & ComponentProps<typeof Dropdown>
18
+ >(({ submit, ...props }, ref) => {
16
19
  const { register, handleSubmit } = useForm({ defaultValues: { dropdown: 'unset' } });
17
20
 
18
21
  return (
19
- <form {...props} onSubmit={handleSubmit(submit)} ref={ref}>
20
- <Dropdown aria-label="Test" {...register('dropdown')}>
22
+ <form onSubmit={handleSubmit(submit)} ref={ref}>
23
+ <Dropdown {...props} aria-label="Test" {...register('dropdown')}>
21
24
  <DropdownOption value="x">Test Opt1</DropdownOption>
22
25
  <DropdownOption value="y">Test Opt2</DropdownOption>
23
26
  <DropdownOption value="z">Test Opt3</DropdownOption>
@@ -37,6 +40,39 @@ describe('Dropdown', () => {
37
40
  expect(button).toHaveTextContent('Test Opt1');
38
41
  });
39
42
 
43
+ describe('supports null value', () => {
44
+ it('can be selected', async () => {
45
+ const handler = jest.fn();
46
+ const Test = () => {
47
+ return (
48
+ <Dropdown onChange={(ev) => handler(ev.target.value)}>
49
+ <DropdownOption value={null}>null</DropdownOption>
50
+ </Dropdown>
51
+ );
52
+ };
53
+ render(<Test />);
54
+ const button = await screen.findByRole('combobox');
55
+ await userEvent.click(button);
56
+ const opt = await screen.findByRole('listbox');
57
+ await userEvent.selectOptions(opt, 'null');
58
+ expect(handler).toHaveBeenCalledWith(null);
59
+ expect(handler).toHaveBeenCalledTimes(1);
60
+ });
61
+ it('works as defaultValue', async () => {
62
+ const handler = jest.fn();
63
+ const Test = () => {
64
+ return (
65
+ <Dropdown defaultValue={null} onChange={(ev) => handler(ev.target.value)}>
66
+ <DropdownOption value={null}>null</DropdownOption>
67
+ </Dropdown>
68
+ );
69
+ };
70
+ render(<Test />);
71
+ const button = await screen.findByRole('combobox');
72
+ expect(button).toHaveTextContent('null');
73
+ });
74
+ });
75
+
40
76
  it('search field can be disabled', async () => {
41
77
  render(
42
78
  <Dropdown search={false}>
@@ -77,8 +113,8 @@ describe('Dropdown', () => {
77
113
  });
78
114
 
79
115
  it('works in controlled mode (from the inside)', async () => {
80
- const Test = ({ valueRef }: { valueRef: MutableRefObject<string> }) => {
81
- const [value, setValue] = useState('x');
116
+ const Test = ({ valueRef }: { valueRef: MutableRefObject<string | null> }) => {
117
+ const [value, setValue] = useState<string | null>('x');
82
118
  valueRef.current = value;
83
119
  return (
84
120
  <Dropdown value={value} onChange={(ev) => setValue(ev.target.value)}>
@@ -215,33 +251,73 @@ describe('Dropdown', () => {
215
251
  expect(option1).toBeInTheDocument();
216
252
  expect(option2).not.toBeInTheDocument();
217
253
  });
218
- });
219
254
 
220
- describe('search', () => {
221
- it('supports custom search', async () => {
222
- render(<CustomSearch />);
255
+ it('works with keyboard navigation', async () => {
256
+ const handler = jest.fn();
257
+ const Test = () => {
258
+ return (
259
+ <Dropdown onChange={(ev) => handler(ev.target.value)}>
260
+ <DropdownDetailedOption value="y" icon={<Avatar name="hello" />} subtitle="ok" title="testx" />
261
+ <DropdownDetailedOption value="x" icon={<Avatar name="hello" />} subtitle="ok" title="test" />
262
+ <DropdownDetailedOption value="z" icon={<Avatar name="hello" />} subtitle="ok" title="test" />
263
+ </Dropdown>
264
+ );
265
+ };
266
+ render(<Test />);
223
267
  const button = await screen.findByRole('combobox');
224
-
225
268
  await userEvent.click(button);
226
269
  const search = await screen.findByRole('search');
227
- await userEvent.type(search, 'text1');
228
- let option1 = screen.queryByRole('option', { name: 'text1' });
229
- let option2 = screen.queryByRole('option', { name: 'text2' });
230
- let option3 = screen.queryByRole('option', { name: 'text3' });
231
- let option4 = screen.queryByRole('option', { name: 'text4' });
232
- expect(option1).toBeInTheDocument();
233
- expect(option2).not.toBeInTheDocument();
234
- expect(option3).not.toBeInTheDocument();
235
- expect(option4).not.toBeInTheDocument();
236
- await userEvent.clear(search);
237
- option1 = screen.queryByRole('option', { name: 'text1' });
238
- option2 = screen.queryByRole('option', { name: 'text2' });
239
- option3 = screen.queryByRole('option', { name: 'text3' });
240
- option4 = screen.queryByRole('option', { name: 'text4' });
241
- expect(option1).toBeInTheDocument();
242
- expect(option2).toBeInTheDocument();
243
- expect(option3).toBeInTheDocument();
244
- expect(option4).toBeInTheDocument();
270
+ await waitFor(() => expect(search).toHaveFocus());
271
+ await userEvent.keyboard('{ArrowDown}');
272
+ await userEvent.keyboard('{ArrowDown}');
273
+ await userEvent.keyboard('{Enter}');
274
+ expect(handler).toHaveBeenCalledWith('x');
275
+ expect(handler).toHaveBeenCalledTimes(1);
276
+ });
277
+ });
278
+
279
+ describe('search', () => {
280
+ describe('custom search', () => {
281
+ it('works', async () => {
282
+ render(<CustomSearch />);
283
+ const button = await screen.findByRole('combobox');
284
+
285
+ await userEvent.click(button);
286
+ const search = await screen.findByRole('search');
287
+ await userEvent.type(search, 'text1');
288
+ let option1 = screen.queryByRole('option', { name: 'text1' });
289
+ let option2 = screen.queryByRole('option', { name: 'text2' });
290
+ let option3 = screen.queryByRole('option', { name: 'text3' });
291
+ let option4 = screen.queryByRole('option', { name: 'text4' });
292
+ expect(option1).toBeInTheDocument();
293
+ expect(option2).not.toBeInTheDocument();
294
+ expect(option3).not.toBeInTheDocument();
295
+ expect(option4).not.toBeInTheDocument();
296
+ await userEvent.clear(search);
297
+ option1 = screen.queryByRole('option', { name: 'text1' });
298
+ option2 = screen.queryByRole('option', { name: 'text2' });
299
+ option3 = screen.queryByRole('option', { name: 'text3' });
300
+ option4 = screen.queryByRole('option', { name: 'text4' });
301
+ expect(option1).toBeInTheDocument();
302
+ expect(option2).toBeInTheDocument();
303
+ expect(option3).toBeInTheDocument();
304
+ expect(option4).toBeInTheDocument();
305
+ });
306
+
307
+ it('resets after closing', async () => {
308
+ render(<CustomSearch />);
309
+ const button = await screen.findByRole('combobox');
310
+
311
+ await userEvent.click(button);
312
+ const search = await screen.findByRole('search');
313
+ await userEvent.type(search, 'text1');
314
+ await userEvent.click(button);
315
+ await userEvent.click(button);
316
+ const option1 = screen.queryByRole('option', { name: 'text1' });
317
+ const option2 = screen.queryByRole('option', { name: 'text2' });
318
+ expect(option1).toBeInTheDocument();
319
+ expect(option2).toBeInTheDocument();
320
+ });
245
321
  });
246
322
 
247
323
  it('removes group when items are filtered', async () => {
@@ -396,6 +472,22 @@ describe('Dropdown', () => {
396
472
  expect(handler).toHaveBeenCalledTimes(1);
397
473
  });
398
474
 
475
+ it('works without search', async () => {
476
+ const handler = jest.fn();
477
+ render(<TestComponent search={false} submit={(data: unknown) => handler(data)} />);
478
+ const button = await screen.findByRole('combobox', { name: 'Test' });
479
+ const submit = await screen.findByRole('button', { name: 'Send' });
480
+
481
+ await userEvent.click(button);
482
+ await screen.findByRole('listbox');
483
+ await userEvent.keyboard('{ArrowDown}');
484
+ await userEvent.keyboard('{ArrowDown}');
485
+ await userEvent.keyboard('{Enter}');
486
+ await userEvent.click(submit);
487
+ expect(handler).toHaveBeenCalledWith({ dropdown: 'y' });
488
+ expect(handler).toHaveBeenCalledTimes(1);
489
+ });
490
+
399
491
  it('works with groups', async () => {
400
492
  const handler = jest.fn();
401
493
  const Test = () => {
@@ -43,6 +43,7 @@ const DropdownTheme = {
43
43
  item: {
44
44
  cursor: 'pointer',
45
45
  userSelect: 'none',
46
+ wordBreak: 'break-word',
46
47
  color: 'purple.10',
47
48
  paddingY: '8',
48
49
  paddingX: '16',
@@ -73,7 +74,7 @@ const DropdownTheme = {
73
74
  zIndex: '1',
74
75
  display: 'flex',
75
76
  minH: 0,
76
- maxH: 'min(calc(2.5rem * 6 + 1.5rem), var(--floating-available-height))',
77
+ maxH: 'max(min(var(--dropdown-floating-max, 16rem), var(--floating-available-height)), var(--dropdown-floating-min, 10rem))',
77
78
  flexShrink: 1,
78
79
  flexDir: 'column',
79
80
  },
@@ -21,22 +21,25 @@ import {
21
21
  import { FloatingFocusManager } from '@floating-ui/react-dom-interactions';
22
22
  import Icon from '../Icon/Icon';
23
23
  import { DropdownEventArgs, DropdownProvider, useDropdownContext, useDropdownStyles } from './Dropdown.context';
24
- import { DropdownOption, DropdownGroup, DropdownDetailedOption } from './DropdownOption';
24
+ import { DropdownOption, DropdownGroup, DropdownDetailedOption, DropdownOptionProps } from './DropdownOption';
25
25
  import DropdownButton from './DropdownButton';
26
26
  import useFloatingDropdown from './hooks/useFloatingDropdown';
27
27
  import { useSimpleSearch, NoResultsFound } from './hooks/useSimpleSearch';
28
28
  import { isSearchable } from './isNodeMatch';
29
29
 
30
- type DropdownSearchProps =
31
- | { placeholder?: string }
32
- | {
33
- placeholder?: string;
34
- value: string;
35
- onChange: (newValue: string) => void;
36
- };
37
- const DropdownSearch = ({ placeholder = 'Start typing to filter options', ...rest }: DropdownSearchProps) => {
30
+ type DropdownSearchCustomProps = {
31
+ value: string;
32
+ onChange: (newValue: string) => void;
33
+ };
34
+ type DropdownSearchProps = { placeholder?: string; reset?: boolean };
35
+ const DropdownSearch = ({
36
+ placeholder = 'Start typing to filter options',
37
+ reset = true,
38
+ ...rest
39
+ }: DropdownSearchProps | (DropdownSearchProps & DropdownSearchCustomProps)) => {
38
40
  const { search } = useDropdownStyles();
39
41
  const { searchValue, searchOnSubmit, searchOnChange, searchRef } = useDropdownContext();
42
+
40
43
  const { value, onChange } = 'onChange' in rest ? rest : { value: searchValue, onChange: searchOnChange };
41
44
 
42
45
  const onChangeCB = useCallback(
@@ -45,6 +48,13 @@ const DropdownSearch = ({ placeholder = 'Start typing to filter options', ...res
45
48
  },
46
49
  [onChange],
47
50
  );
51
+ useEffect(() => {
52
+ return () => {
53
+ if (reset) {
54
+ onChange('');
55
+ }
56
+ };
57
+ }, [reset]);
48
58
  const onKeyDown = useCallback(
49
59
  (ev: React.KeyboardEvent) => {
50
60
  if (ev.key === 'Enter') {
@@ -74,15 +84,17 @@ const DropdownSearch = ({ placeholder = 'Start typing to filter options', ...res
74
84
  };
75
85
  export { DropdownOption, DropdownGroup, DropdownSearch, NoResultsFound, DropdownDetailedOption };
76
86
 
77
- type DropdownInstance = { value: string; name?: string };
78
- type DropdownChangeEventHandler = (ev: { target: DropdownInstance }) => void;
79
- export interface DropdownProps extends ChakraProps {
87
+ type DropdownInstance<T> = { value: T; name?: string };
88
+ type DropdownChangeEventHandler<T> = (ev: { target: DropdownInstance<T> }) => void;
89
+ export interface DropdownProps<T> extends ChakraProps {
80
90
  name?: string;
81
- onChange?: DropdownChangeEventHandler;
82
- onBlur?: DropdownChangeEventHandler;
83
- value?: string;
84
- defaultValue?: string;
91
+ onChange?: DropdownChangeEventHandler<T>;
92
+ onBlur?: DropdownChangeEventHandler<T>;
93
+ value?: T;
94
+ defaultValue?: T;
85
95
  size?: 'small' | 'medium';
96
+ dropdownMaxHeight?: ChakraProps['maxH'];
97
+ dropdownMinHeight?: ChakraProps['minH'];
86
98
  readOnly?: boolean;
87
99
  disabled?: boolean;
88
100
  placeholder?: string;
@@ -91,31 +103,18 @@ export interface DropdownProps extends ChakraProps {
91
103
  children?: ReactNode;
92
104
  }
93
105
 
94
- function useOptionListWithRefs({
95
- children,
96
- listRef,
97
- getItemProps,
98
- }: {
99
- children: ReactNode;
100
- listRef: React.MutableRefObject<(HTMLElement | null)[]>;
101
- getItemProps: (userProps?: React.HTMLProps<HTMLElement> | undefined) => Record<string, unknown>;
102
- }) {
106
+ function useOptionListWithIndexes({ children }: { children: ReactNode }) {
103
107
  return useMemo(() => {
104
108
  const childList = React.Children.toArray(children);
105
109
  let idx = 0;
106
110
  const transform = (ch: ReactNode): ReactNode => {
107
111
  if (React.isValidElement(ch)) {
108
- if (ch.type === DropdownOption) {
112
+ if (ch.type === DropdownOption || ch.type === DropdownDetailedOption) {
109
113
  const index = idx;
110
114
  idx += 1;
111
115
  return cloneElement(ch, {
112
- ...getItemProps({
113
- ...ch.props,
114
- index,
115
- ref(node) {
116
- listRef.current[index] = node;
117
- },
118
- }),
116
+ ...ch.props,
117
+ index,
119
118
  });
120
119
  }
121
120
  if ('children' in ch.props) {
@@ -131,17 +130,18 @@ function useOptionListWithRefs({
131
130
  }, [children]);
132
131
  }
133
132
 
134
- type UseDropdownProps = {
135
- ref: React.Ref<DropdownInstance>;
133
+ type UseDropdownProps<T> = {
134
+ ref: React.Ref<DropdownInstance<T>>;
136
135
  optionsRef: React.RefObject<HTMLDivElement>;
137
136
  };
138
137
 
139
- function findOption(children: ReactNode, value: string): { label: ReactNode; index: number } | null {
138
+ function findOption<T>(children: ReactNode, value: T): { label: ReactNode; index: number } | null {
140
139
  const list = React.Children.toArray(children);
141
140
  for (let i = 0; i < list.length; i++) {
142
141
  const elem = list[i];
143
142
  if (React.isValidElement(elem)) {
144
- if (elem.type === DropdownOption && (elem.props.value || '') === value) {
143
+ const optValue = typeof elem.props.value === 'undefined' ? null : elem.props.value;
144
+ if (elem.type === DropdownOption && optValue === value) {
145
145
  return { label: elem.props.children, index: elem.props.index };
146
146
  }
147
147
  const ch =
@@ -154,7 +154,7 @@ function findOption(children: ReactNode, value: string): { label: ReactNode; ind
154
154
  return null;
155
155
  }
156
156
 
157
- function useDropdown({
157
+ function useDropdown<T>({
158
158
  name,
159
159
  onChange,
160
160
  value,
@@ -164,12 +164,12 @@ function useDropdown({
164
164
  children,
165
165
  readOnly,
166
166
  ...rest
167
- }: DropdownProps & UseDropdownProps) {
167
+ }: DropdownProps<T> & UseDropdownProps<T>) {
168
168
  const searchRef = useRef(null);
169
169
  const {
170
- onClose,
170
+ close,
171
171
  isOpen,
172
- referenceProps,
172
+ getReferenceProps,
173
173
  floatingProps,
174
174
  context: floatingContext,
175
175
  activeIndex,
@@ -178,7 +178,7 @@ function useDropdown({
178
178
  getItemProps,
179
179
  listRef,
180
180
  } = useFloatingDropdown({ enabled: !readOnly, optionsRef });
181
- const [formValue, setFormValue] = useControllableState({
181
+ const [formValue, setFormValue] = useControllableState<T>({
182
182
  onChange: (newValue) => onChange?.({ target: { value: newValue, name } }),
183
183
  defaultValue,
184
184
  value,
@@ -186,25 +186,33 @@ function useDropdown({
186
186
 
187
187
  const [formLabel, setFormLabel] = useState<ReactNode>();
188
188
  useImperativeHandle(ref, () => ({ value: formValue, name }), [formValue, name]);
189
- const refdChildren = useOptionListWithRefs({ children, listRef, getItemProps });
189
+ const refdChildren = useOptionListWithIndexes({ children });
190
190
 
191
191
  const searchOnSubmit = useCallback(() => {
192
192
  if (activeIndex !== null) {
193
193
  listRef.current[activeIndex]?.click();
194
194
  }
195
195
  }, [activeIndex]);
196
+ const referenceKeyDown = useCallback(
197
+ (ev: React.KeyboardEvent) => {
198
+ if (ev.key === 'Enter') {
199
+ ev.preventDefault();
200
+ searchOnSubmit();
201
+ }
202
+ },
203
+ [searchOnSubmit],
204
+ );
196
205
  const { searchValue, searchOnChange, ...searchResults } = useSimpleSearch({
197
206
  children: refdChildren,
198
207
  onSearch: () => setActiveIndex(null),
199
- isOpen,
200
208
  });
201
209
 
202
210
  const onOptionSelected = useCallback(
203
- (args: DropdownEventArgs) => {
211
+ (args: DropdownEventArgs<T>) => {
204
212
  setFormValue(args.value);
205
- onClose();
213
+ close();
206
214
  },
207
- [onClose, setFormValue],
215
+ [close, setFormValue],
208
216
  );
209
217
  const context = useMemo(
210
218
  () => ({
@@ -216,8 +224,19 @@ function useDropdown({
216
224
  searchValue,
217
225
  searchRef,
218
226
  searchOnSubmit,
227
+ listRef,
219
228
  }),
220
- [formValue, onOptionSelected, activeIndex, getItemProps, searchOnChange, searchValue, searchRef, searchOnSubmit],
229
+ [
230
+ listRef,
231
+ formValue,
232
+ onOptionSelected,
233
+ activeIndex,
234
+ getItemProps,
235
+ searchOnChange,
236
+ searchValue,
237
+ searchRef,
238
+ searchOnSubmit,
239
+ ],
221
240
  );
222
241
 
223
242
  useEffect(() => {
@@ -229,7 +248,9 @@ function useDropdown({
229
248
  }, [refdChildren, formValue]);
230
249
  return {
231
250
  isOpen,
232
- referenceProps,
251
+ referenceProps: getReferenceProps({
252
+ onKeyDown: referenceKeyDown,
253
+ }),
233
254
  floatingProps,
234
255
  floatingContext,
235
256
  searchOnSubmit,
@@ -242,8 +263,11 @@ function useDropdown({
242
263
  };
243
264
  }
244
265
 
245
- const Dropdown = forwardRef<DropdownInstance, DropdownProps>(
246
- ({ readOnly, onBlur, placeholder, name, search, size = 'medium', ...props }, ref) => {
266
+ const Dropdown = forwardRef<DropdownInstance<string | null>, DropdownProps<string | null>>(
267
+ (
268
+ { dropdownMaxHeight, dropdownMinHeight, readOnly, onBlur, placeholder, name, search, size = 'medium', ...props },
269
+ ref,
270
+ ) => {
247
271
  const optionsRef = useRef(null);
248
272
  const {
249
273
  context: dropdownCtx,
@@ -273,6 +297,14 @@ const Dropdown = forwardRef<DropdownInstance, DropdownProps>(
273
297
  onBlur({ target: { value: formValue, name } });
274
298
  }
275
299
  };
300
+ const listStyles = useMemo(
301
+ () => ({
302
+ ...dropdownStyles.list,
303
+ '--dropdown-floating-max': dropdownMaxHeight,
304
+ '--dropdown-floating-min': dropdownMinHeight,
305
+ }),
306
+ [dropdownMinHeight, dropdownMaxHeight, dropdownStyles.list],
307
+ );
276
308
  const searchElement = search === false ? null : search || <DropdownSearch />;
277
309
  return (
278
310
  <>
@@ -280,7 +312,7 @@ const Dropdown = forwardRef<DropdownInstance, DropdownProps>(
280
312
  <DropdownProvider context={dropdownCtx} styles={dropdownStyles}>
281
313
  <DropdownButton
282
314
  {...referenceProps}
283
- aria-label={props['aria-label']}
315
+ {...props}
284
316
  placeholder={placeholder}
285
317
  size={size}
286
318
  formLabel={formLabel}
@@ -289,7 +321,7 @@ const Dropdown = forwardRef<DropdownInstance, DropdownProps>(
289
321
  />
290
322
  {isOpen && (
291
323
  <FloatingFocusManager initialFocus={searchRef} context={floatingContext}>
292
- <chakra.div {...floatingProps} sx={dropdownStyles.list}>
324
+ <chakra.div {...floatingProps} sx={listStyles}>
293
325
  {searchElement}
294
326
  <chakra.div tabIndex={-1} ref={optionsRef} __css={dropdownStyles.options}>
295
327
  {children}
@@ -303,4 +335,11 @@ const Dropdown = forwardRef<DropdownInstance, DropdownProps>(
303
335
  },
304
336
  );
305
337
 
338
+ export function typedDropdown<T>() {
339
+ return {
340
+ Dropdown: Dropdown as React.ForwardRefExoticComponent<DropdownProps<T> & React.RefAttributes<DropdownInstance<T>>>,
341
+ DropdownOption: DropdownOption as (p: DropdownOptionProps<T>) => JSX.Element,
342
+ };
343
+ }
344
+
306
345
  export default Dropdown;
@@ -17,7 +17,9 @@ const DropdownButton = forwardRef<HTMLButtonElement, DropdownButtonProps>(
17
17
  const iconSize = size === 'medium' ? '24' : '16';
18
18
  return (
19
19
  <chakra.button aria-readonly={readOnly} type="button" ref={ref} onBlur={blurHandler} __css={field} {...rest}>
20
- <chakra.span flexGrow={1}>{formLabel || placeholder}</chakra.span>
20
+ <chakra.span overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap" flexGrow={1}>
21
+ {formLabel || placeholder}
22
+ </chakra.span>
21
23
  <Icon aria-hidden="true" __css={icon} name="DropdownArrows" fontSize={iconSize} size={iconSize} />
22
24
  </chakra.button>
23
25
  );
@@ -1,21 +1,28 @@
1
- import { forwardRef, ReactNode, useId } from 'react';
1
+ import { ReactNode, useId } from 'react';
2
2
  import { chakra } from '@chakra-ui/react';
3
3
  import { cx } from '@chakra-ui/utils';
4
4
  import Text from '../Text/Text';
5
5
  import Divider from '../Divider/Divider';
6
6
  import { useDropdownContext, useDropdownStyles } from './Dropdown.context';
7
7
 
8
- const DropdownOption = forwardRef<
9
- HTMLDivElement,
10
- { value?: string; children?: ReactNode; index?: number; 'aria-label'?: string }
11
- >(({ value = '', children, index, ...rest }, ref) => {
8
+ export type DropdownOptionProps<T> = {
9
+ value?: T | null;
10
+ children?: ReactNode;
11
+ 'aria-label'?: string;
12
+ };
13
+ const DropdownOption = <T = string,>({ value = null, children, ...rest }: DropdownOptionProps<T>) => {
12
14
  const { item } = useDropdownStyles();
13
- const ctx = useDropdownContext();
15
+ const ctx = useDropdownContext<T | null>();
16
+ const { index } = rest as { index?: number };
14
17
  return (
15
18
  <chakra.div
16
19
  role="option"
17
20
  id={useId()}
18
- ref={ref}
21
+ ref={(node) => {
22
+ if (ctx.listRef.current) {
23
+ ctx.listRef.current[index!] = node;
24
+ }
25
+ }}
19
26
  {...rest}
20
27
  className={cx(
21
28
  value === ctx.formValue && 'bitkit-select__option-active',
@@ -24,14 +31,14 @@ const DropdownOption = forwardRef<
24
31
  __css={item}
25
32
  {...ctx.getItemProps({
26
33
  onClick() {
27
- ctx.onOptionSelected({ value: value || '', index, label: children });
34
+ ctx.onOptionSelected({ value, index, label: children });
28
35
  },
29
36
  })}
30
37
  >
31
38
  {children}
32
39
  </chakra.div>
33
40
  );
34
- });
41
+ };
35
42
 
36
43
  const DropdownGroup = ({ children, label }: { children: ReactNode; label: string }) => {
37
44
  const { group } = useDropdownStyles();
@@ -54,16 +61,15 @@ const DropdownDetailedOption = ({
54
61
  title,
55
62
  subtitle,
56
63
  value,
57
- index,
64
+ ...rest
58
65
  }: {
59
66
  icon: ReactNode;
60
67
  title: string;
61
68
  subtitle: string;
62
69
  value?: string;
63
- index?: number;
64
70
  }) => {
65
71
  return (
66
- <DropdownOption value={value} index={index} aria-label={title}>
72
+ <DropdownOption value={value} {...rest} aria-label={title}>
67
73
  <chakra.div alignItems="center" gap="12" display="flex" flexDir="row">
68
74
  {icon}
69
75
  <chakra.div>