@kaizen/components 3.2.0 → 3.3.0

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 (35) hide show
  1. package/dist/cjs/src/Avatar/Avatar.cjs +18 -14
  2. package/dist/cjs/src/Focusable/Focusable.cjs +5 -4
  3. package/dist/cjs/src/MultiSelect/MultiSelect.cjs +4 -2
  4. package/dist/cjs/src/RichTextEditor/RichTextEditor/RichTextEditor.cjs +4 -1
  5. package/dist/cjs/src/RichTextEditor/RichTextEditor/utils/inputrules.cjs +0 -1
  6. package/dist/cjs/src/RichTextEditor/utils/core/createRichTextEditor.cjs +2 -1
  7. package/dist/cjs/src/RichTextEditor/utils/core/hooks/useRichTextEditor.cjs +24 -9
  8. package/dist/cjs/src/SingleSelect/SingleSelect.cjs +4 -2
  9. package/dist/esm/src/Avatar/Avatar.mjs +18 -15
  10. package/dist/esm/src/Focusable/Focusable.mjs +5 -4
  11. package/dist/esm/src/MultiSelect/MultiSelect.mjs +4 -2
  12. package/dist/esm/src/RichTextEditor/RichTextEditor/RichTextEditor.mjs +4 -1
  13. package/dist/esm/src/RichTextEditor/RichTextEditor/utils/inputrules.mjs +0 -1
  14. package/dist/esm/src/RichTextEditor/utils/core/createRichTextEditor.mjs +2 -1
  15. package/dist/esm/src/RichTextEditor/utils/core/hooks/useRichTextEditor.mjs +24 -9
  16. package/dist/esm/src/SingleSelect/SingleSelect.mjs +4 -2
  17. package/dist/styles.css +15 -15
  18. package/dist/types/Focusable/Focusable.d.ts +8 -3
  19. package/dist/types/MultiSelect/MultiSelect.d.ts +3 -2
  20. package/dist/types/Notification/index.d.ts +1 -0
  21. package/dist/types/RichTextEditor/RichTextEditor/RichTextEditor.d.ts +2 -1
  22. package/dist/types/RichTextEditor/utils/core/createRichTextEditor.d.ts +1 -0
  23. package/dist/types/RichTextEditor/utils/core/hooks/useRichTextEditor.d.ts +3 -2
  24. package/dist/types/SingleSelect/SingleSelect.d.ts +3 -3
  25. package/package.json +1 -1
  26. package/src/Avatar/Avatar.tsx +19 -11
  27. package/src/Focusable/Focusable.tsx +17 -7
  28. package/src/MultiSelect/MultiSelect.tsx +4 -1
  29. package/src/Notification/index.ts +1 -0
  30. package/src/RichTextEditor/RichTextEditor/RichTextEditor.tsx +3 -0
  31. package/src/RichTextEditor/utils/core/createRichTextEditor.spec.ts +1 -1
  32. package/src/RichTextEditor/utils/core/createRichTextEditor.ts +2 -0
  33. package/src/RichTextEditor/utils/core/hooks/useRichTextEditor.spec.tsx +8 -3
  34. package/src/RichTextEditor/utils/core/hooks/useRichTextEditor.ts +20 -7
  35. package/src/SingleSelect/SingleSelect.tsx +5 -3
@@ -1,6 +1,11 @@
1
- import { type HTMLAttributes, type ReactNode } from 'react';
1
+ import { type ElementType, type HTMLAttributes, type ReactNode } from 'react';
2
2
  import { type FocusableOptions } from 'react-aria';
3
- export type FocusableProps = {
3
+ export type FocusableProps<T extends ElementType = 'div'> = {
4
4
  children: ReactNode;
5
+ /**
6
+ * The HTML element to render as. Defaults to `div` to avoid an api change
7
+ * but should be set to 'span' if rendering within something like a 'p' tag to avoid invalid HTML.
8
+ */
9
+ tag?: T;
5
10
  } & FocusableOptions & HTMLAttributes<HTMLDivElement>;
6
- export declare const Focusable: ({ children, className, ...props }: FocusableProps) => JSX.Element;
11
+ export declare const Focusable: <T extends ElementType = "div">({ children, className, tag, ...props }: FocusableProps<T>) => JSX.Element;
@@ -1,9 +1,10 @@
1
- import { type HTMLAttributes } from 'react';
1
+ import React, { type HTMLAttributes } from 'react';
2
2
  import { type FieldMessageProps } from "../FieldMessage";
3
3
  import { type OverrideClassName } from "../types/OverrideClassName";
4
4
  import { type MultiSelectOptionsProps } from './subcomponents/MultiSelectOptions';
5
5
  import { type MultiSelectOption, type ValidationMessage } from './types';
6
6
  export type MultiSelectProps = {
7
+ inputRef?: React.Ref<HTMLButtonElement>;
7
8
  label: string;
8
9
  items: MultiSelectOptionsProps['options'];
9
10
  selectedValues: Set<MultiSelectOption['value']>;
@@ -18,6 +19,6 @@ export type MultiSelectProps = {
18
19
  validationMessage?: ValidationMessage;
19
20
  } & OverrideClassName<HTMLAttributes<HTMLDivElement>>;
20
21
  export declare const MultiSelect: {
21
- ({ id: propsId, label, items, selectedValues, description, onSelectedValuesChange, isOpen, onOpenChange, classNameOverride, validationMessage, ...restProps }: MultiSelectProps): JSX.Element;
22
+ ({ id: propsId, label, items, selectedValues, description, onSelectedValuesChange, isOpen, onOpenChange, classNameOverride, validationMessage, inputRef, ...restProps }: MultiSelectProps): JSX.Element;
22
23
  displayName: string;
23
24
  };
@@ -1,3 +1,4 @@
1
1
  export * from './InlineNotification';
2
2
  export * from './GlobalNotification';
3
3
  export * from './ToastNotification';
4
+ export * from './types';
@@ -4,6 +4,7 @@ import type { EditorContentArray, EditorRows, ToolbarItems } from '../types';
4
4
  import { ProseMirrorState } from '../utils/prosemirror';
5
5
  type BaseRichTextEditorProps = {
6
6
  id?: string;
7
+ inputRef?: React.Ref<HTMLElement>;
7
8
  onChange: (content: ProseMirrorState.EditorState) => void;
8
9
  defaultValue: EditorContentArray;
9
10
  controls?: ToolbarItems[];
@@ -38,7 +39,7 @@ export type RichTextEditorProps = BaseRichTextEditorProps & (WithLabelText | Wit
38
39
  * {@link https://cultureamp.design/?path=/docs/components-richtexteditor--docs Storybook}
39
40
  */
40
41
  export declare const RichTextEditor: {
41
- ({ id, onChange, defaultValue, labelText, "aria-labelledby": labelledBy, "aria-describedby": describedBy, classNameOverride, controls, rows, dataError, onDataError, validationMessage, description, status, ...restProps }: RichTextEditorProps): JSX.Element;
42
+ ({ id, inputRef, onChange, defaultValue, labelText, "aria-labelledby": labelledBy, "aria-describedby": describedBy, classNameOverride, controls, rows, dataError, onDataError, validationMessage, description, status, ...restProps }: RichTextEditorProps): JSX.Element;
42
43
  displayName: string;
43
44
  };
44
45
  export {};
@@ -3,6 +3,7 @@ import { type CommandOrTransaction } from './types';
3
3
  type EditorAPI = {
4
4
  destroy: () => void;
5
5
  dispatchTransaction: (maybeCommand: CommandOrTransaction) => void;
6
+ dom: HTMLElement;
6
7
  };
7
8
  type EditorArgs = {
8
9
  initialEditorState: EditorState;
@@ -1,7 +1,8 @@
1
1
  import { type EditorState } from 'prosemirror-state';
2
2
  import { type CommandOrTransaction } from '../types';
3
3
  type RTEOptions = {
4
- editable: boolean;
4
+ editable?: boolean;
5
+ inputRef?: React.Ref<HTMLElement>;
5
6
  };
6
7
  type SetEditableStatus = (status: boolean) => void;
7
8
  type UseRichTextEditorReturnValue = [
@@ -18,5 +19,5 @@ type UseRichTextEditorReturnValue = [
18
19
  * @param {initialEditorState} ProseMirror state
19
20
  * @returns {Array}
20
21
  */
21
- export declare const useRichTextEditor: (initialEditorState: EditorState, attributes?: Record<string, string>, options?: RTEOptions) => UseRichTextEditorReturnValue;
22
+ export declare const useRichTextEditor: (initialEditorState: EditorState, attributes?: Record<string, string>, { editable, inputRef }?: RTEOptions) => UseRichTextEditorReturnValue;
22
23
  export {};
@@ -1,5 +1,4 @@
1
1
  import React from 'react';
2
- import { type UseFloatingReturn } from '@floating-ui/react-dom';
3
2
  import { type SelectProps as AriaSelectProps } from '@react-stately/select';
4
3
  import { type Key } from '@react-types/shared';
5
4
  import { type OverrideClassName } from "../types/OverrideClassName";
@@ -12,13 +11,14 @@ export type SingleSelectProps<Option extends SingleSelectOption = SingleSelectOp
12
11
  */
13
12
  items: SingleSelectItem<Option>[];
14
13
  id?: string;
14
+ inputRef?: React.Ref<HTMLButtonElement>;
15
15
  /**
16
16
  * Optional render function that allows custom rendering of the trigger button.
17
17
  * The function receives the `selectToggleProps` and a `ref` to be applied
18
18
  * to the button element.
19
19
  */
20
20
  trigger?: (selectToggleProps: SelectToggleProps & {
21
- ref: UseFloatingReturn<HTMLButtonElement>['refs']['setReference'];
21
+ ref: React.Ref<HTMLButtonElement>;
22
22
  }) => JSX.Element;
23
23
  /**
24
24
  * Optional render function that allows custom rendering of the items in the dropdown.
@@ -56,7 +56,7 @@ export type SingleSelectProps<Option extends SingleSelectOption = SingleSelectOp
56
56
  * {@link https://cultureamp.design/?path=/docs/components-select--docs Storybook}
57
57
  */
58
58
  export declare const SingleSelect: {
59
- <Option extends SingleSelectOption = SingleSelectOption>({ label, items, id: propsId, trigger, children, status, validationMessage, isReversed, isRequired, isFullWidth, classNameOverride, selectedKey, description, isDisabled, onSelectionChange, portalContainerId, ...restProps }: SingleSelectProps<Option>): JSX.Element;
59
+ <Option extends SingleSelectOption = SingleSelectOption>({ label, items, id: propsId, trigger, children, status, validationMessage, isReversed, isRequired, isFullWidth, classNameOverride, selectedKey, description, isDisabled, onSelectionChange, portalContainerId, inputRef, ...restProps }: SingleSelectProps<Option>): JSX.Element;
60
60
  displayName: string;
61
61
  Section: {
62
62
  <Option extends SingleSelectOption = SingleSelectOption>({ section, }: import("./subcomponents").ListBoxSectionProps<Option>): JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaizen/components",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "Kaizen component library",
5
5
  "author": "Geoffrey Chong <geoff.chong@cultureamp.com>",
6
6
  "homepage": "https://cultureamp.design",
@@ -91,21 +91,29 @@ const renderInitials = (
91
91
  const isLongName = initials.length > 2 && size !== 'small'
92
92
  const renderFallback = disableInitials || initials === ''
93
93
 
94
- return renderFallback ? (
95
- <FallbackIcon alt={alt} />
96
- ) : (
97
- <abbr className={classnames(styles.initials, isLongName && styles.longName)} title={alt}>
98
- {isLongName ? (
99
- // Only called if 3 or more initials, fits text width for long names
100
- //
94
+ if (renderFallback) {
95
+ return <FallbackIcon alt={alt} />
96
+ }
97
+
98
+ if (isLongName) {
99
+ return (
100
+ <abbr
101
+ className={classnames(styles.initials, styles.longName)}
102
+ title={alt}
101
103
  // Ignore Chromatic diffs since the font-size calculation has shown itself to be slightly non-deterministic,
102
104
  // causing flaky tests.
103
- <Textfit mode="single" max={getMaxFontSizePixels(size)} data-chromatic="ignore">
105
+ data-chromatic="ignore"
106
+ >
107
+ <Textfit mode="single" max={getMaxFontSizePixels(size)}>
104
108
  {initials}
105
109
  </Textfit>
106
- ) : (
107
- getInitials(fullName, size === 'small')
108
- )}
110
+ </abbr>
111
+ )
112
+ }
113
+
114
+ return (
115
+ <abbr className={styles.initials} title={alt}>
116
+ {getInitials(fullName, size === 'small')}
109
117
  </abbr>
110
118
  )
111
119
  }
@@ -1,26 +1,36 @@
1
- import React, { useRef, type HTMLAttributes, type ReactNode } from 'react'
1
+ import React, { useRef, type ElementType, type HTMLAttributes, type ReactNode } from 'react'
2
2
  import classnames from 'classnames'
3
3
  import { useFocusable, type FocusableOptions } from 'react-aria'
4
4
  import styles from './Focusable.module.css'
5
5
 
6
- export type FocusableProps = {
6
+ export type FocusableProps<T extends ElementType = 'div'> = {
7
7
  children: ReactNode
8
+ /**
9
+ * The HTML element to render as. Defaults to `div` to avoid an api change
10
+ * but should be set to 'span' if rendering within something like a 'p' tag to avoid invalid HTML.
11
+ */
12
+ tag?: T
8
13
  } & FocusableOptions &
9
14
  HTMLAttributes<HTMLDivElement>
10
15
 
11
- export const Focusable = ({ children, className, ...props }: FocusableProps): JSX.Element => {
16
+ export const Focusable = <T extends ElementType = 'div'>({
17
+ children,
18
+ className,
19
+ tag,
20
+ ...props
21
+ }: FocusableProps<T>): JSX.Element => {
12
22
  const ref = useRef<HTMLDivElement>(null)
13
23
  const { focusableProps } = useFocusable(props, ref)
24
+ const Element = tag ?? 'div'
14
25
 
15
26
  return (
16
- <div
27
+ <Element
17
28
  ref={ref}
18
29
  className={classnames(styles.focusableWrapper, className)}
19
30
  {...focusableProps}
20
31
  data-inline-hidden-content
21
- // We want the div to be focusable for keyboard users,
32
+ // We want the element to be focusable for keyboard users,
22
33
  // but screen readers will have the VisuallyHidden content
23
- // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
24
34
  tabIndex={0}
25
35
  // aria-describedby on div doesn't do anthing so we instead render the content in VisuallyHidden from tooltip
26
36
  // but because RAC adds it as it assumes it's interactive element we remove it here
@@ -28,6 +38,6 @@ export const Focusable = ({ children, className, ...props }: FocusableProps): JS
28
38
  {...props}
29
39
  >
30
40
  {children}
31
- </div>
41
+ </Element>
32
42
  )
33
43
  }
@@ -1,4 +1,5 @@
1
1
  import React, { useId, useRef, type HTMLAttributes } from 'react'
2
+ import { mergeRefs } from '@react-aria/utils'
2
3
  import classnames from 'classnames'
3
4
  import { type ReactFocusOnProps } from 'react-focus-on/dist/es5/types'
4
5
  import { FieldMessage, type FieldMessageProps } from '~components/FieldMessage'
@@ -14,6 +15,7 @@ import { type MultiSelectOption, type ValidationMessage } from './types'
14
15
  import styles from './MultiSelect.module.scss'
15
16
 
16
17
  export type MultiSelectProps = {
18
+ inputRef?: React.Ref<HTMLButtonElement>
17
19
  label: string
18
20
  items: MultiSelectOptionsProps['options']
19
21
  selectedValues: Set<MultiSelectOption['value']>
@@ -39,6 +41,7 @@ export const MultiSelect = ({
39
41
  onOpenChange,
40
42
  classNameOverride,
41
43
  validationMessage,
44
+ inputRef,
42
45
  ...restProps
43
46
  }: MultiSelectProps): JSX.Element => {
44
47
  const fallbackId = useId()
@@ -88,7 +91,7 @@ export const MultiSelect = ({
88
91
 
89
92
  <div ref={refs.setReference} className={styles.toggleContainer}>
90
93
  <MultiSelectToggle
91
- ref={toggleButtonRef}
94
+ ref={mergeRefs(toggleButtonRef, inputRef)}
92
95
  id={`${id}--toggle`}
93
96
  aria-labelledby={`${id}--label`}
94
97
  aria-describedby={`${validationId} ${descriptionId}`}
@@ -1,3 +1,4 @@
1
1
  export * from './InlineNotification'
2
2
  export * from './GlobalNotification'
3
3
  export * from './ToastNotification'
4
+ export * from './types'
@@ -22,6 +22,7 @@ import styles from './RichTextEditor.module.scss'
22
22
 
23
23
  type BaseRichTextEditorProps = {
24
24
  id?: string
25
+ inputRef?: React.Ref<HTMLElement>
25
26
  onChange: (content: ProseMirrorState.EditorState) => void
26
27
  defaultValue: EditorContentArray
27
28
  controls?: ToolbarItems[]
@@ -60,6 +61,7 @@ export type RichTextEditorProps = BaseRichTextEditorProps & (WithLabelText | Wit
60
61
  */
61
62
  export const RichTextEditor = ({
62
63
  id,
64
+ inputRef,
63
65
  onChange,
64
66
  defaultValue,
65
67
  labelText,
@@ -105,6 +107,7 @@ export const RichTextEditor = ({
105
107
  'role': 'textbox',
106
108
  'aria-describedby': ariaDescribedBy,
107
109
  },
110
+ { inputRef },
108
111
  )
109
112
  } catch {
110
113
  return new Error('Bad data error')
@@ -32,7 +32,7 @@ describe('createRichTextEditor()', () => {
32
32
  initialEditorState: testEditorState,
33
33
  })
34
34
 
35
- expect(Object.keys(returnValue)).toEqual(['destroy', 'dispatchTransaction'])
35
+ expect(Object.keys(returnValue)).toEqual(['destroy', 'dispatchTransaction', 'dom'])
36
36
  expect(typeof returnValue.destroy).toEqual('function')
37
37
  expect(typeof returnValue.dispatchTransaction).toEqual('function')
38
38
  })
@@ -5,6 +5,7 @@ import { type CommandOrTransaction } from './types'
5
5
  type EditorAPI = {
6
6
  destroy: () => void
7
7
  dispatchTransaction: (maybeCommand: CommandOrTransaction) => void
8
+ dom: HTMLElement
8
9
  }
9
10
 
10
11
  type EditorArgs = {
@@ -60,5 +61,6 @@ export const createRichTextEditor = ({
60
61
  return {
61
62
  destroy: () => editorView.destroy(),
62
63
  dispatchTransaction: dispatchCommandOrTransaction,
64
+ dom: editorView.dom,
63
65
  }
64
66
  }
@@ -10,9 +10,11 @@ const user = userEvent.setup()
10
10
  const Scenario = ({
11
11
  onChange = () => undefined,
12
12
  editable = true,
13
+ inputRef,
13
14
  }: {
14
15
  onChange?: (editorState: EditorState) => void
15
16
  editable?: boolean
17
+ inputRef?: React.Ref<HTMLElement>
16
18
  }): JSX.Element => {
17
19
  const command: Command = (state, dispatch) => {
18
20
  // Insert text at the current selection point, which is the start because
@@ -25,7 +27,7 @@ const Scenario = ({
25
27
  const [ref, editorState, dispatchTransaction, setEditableStatus] = useRichTextEditor(
26
28
  testEditorState,
27
29
  { 'aria-labelledby': 'label-ref-id', 'data-testid': '12345678' },
28
- { editable },
30
+ { editable, inputRef },
29
31
  )
30
32
 
31
33
  useEffect(() => {
@@ -151,14 +153,17 @@ describe('useRichTextEditor()', () => {
151
153
  })
152
154
  })
153
155
 
154
- it('is removed from DOM when unmounted', async () => {
155
- const { unmount } = render(<Scenario />)
156
+ it('assigns ref on mount and cleans up DOM/ref on unmount', async () => {
157
+ const inputRef = React.createRef<HTMLElement>()
158
+ const { unmount } = render(<Scenario inputRef={inputRef} />)
156
159
 
157
160
  const editor = screen.getByTestId('testid--editor')
158
161
  expect(editor).toBeInTheDocument()
162
+ expect(inputRef.current).toHaveAttribute('contenteditable', 'true')
159
163
 
160
164
  unmount()
161
165
 
162
166
  expect(screen.queryByTestId('testid--editor')).not.toBeInTheDocument()
167
+ expect(inputRef.current).toBeNull()
163
168
  })
164
169
  })
@@ -1,10 +1,12 @@
1
1
  import { useCallback, useRef, useState } from 'react'
2
2
  import { type EditorState } from 'prosemirror-state'
3
+ import { isRefObject } from '~components/utils/isRefObject'
3
4
  import { createRichTextEditor } from '../createRichTextEditor'
4
5
  import { type CommandOrTransaction } from '../types'
5
6
 
6
7
  type RTEOptions = {
7
- editable: boolean
8
+ editable?: boolean
9
+ inputRef?: React.Ref<HTMLElement>
8
10
  }
9
11
 
10
12
  type SetEditableStatus = (status: boolean) => void
@@ -30,12 +32,8 @@ export const useRichTextEditor = (
30
32
  * Pass in HTML attributes into the parent RTE node
31
33
  */
32
34
  attributes?: Record<string, string>,
33
- options?: RTEOptions,
35
+ { editable = true, inputRef }: RTEOptions = {},
34
36
  ): UseRichTextEditorReturnValue => {
35
- options = {
36
- editable: true,
37
- ...options,
38
- }
39
37
  const [editorState, setEditorState] = useState<EditorState>(initialEditorState)
40
38
  // Refs to hold the methods returned from ProseMirror’s initialization
41
39
  const destroyEditorRef = useRef<() => void>()
@@ -53,7 +51,11 @@ export const useRichTextEditor = (
53
51
  )
54
52
 
55
53
  // Hold editableStatus as a ref so we can toggle its status
56
- const editableStatusRef = useRef<boolean>(options.editable)
54
+ const editableStatusRef = useRef<boolean>(editable)
55
+
56
+ // Stable ref to avoid recreating editorRef when inputRef callback identity changes
57
+ const inputRefRef = useRef(inputRef)
58
+ inputRefRef.current = inputRef
57
59
  const setEditableStatus = useCallback<SetEditableStatus>(
58
60
  (status) => {
59
61
  editableStatusRef.current = status
@@ -76,6 +78,11 @@ export const useRichTextEditor = (
76
78
  destroyEditorRef.current()
77
79
  destroyEditorRef.current = undefined
78
80
  }
81
+ if (inputRefRef.current && isRefObject(inputRefRef.current)) {
82
+ ;(inputRefRef.current as React.MutableRefObject<HTMLElement | null>).current = null
83
+ } else {
84
+ inputRefRef.current?.(null)
85
+ }
79
86
  return
80
87
  }
81
88
 
@@ -88,6 +95,12 @@ export const useRichTextEditor = (
88
95
  })
89
96
  destroyEditorRef.current = instance.destroy
90
97
  dispatchTransactionRef.current = instance.dispatchTransaction
98
+
99
+ if (inputRefRef.current && isRefObject(inputRefRef.current)) {
100
+ ;(inputRefRef.current as React.MutableRefObject<HTMLElement | null>).current = instance.dom
101
+ } else {
102
+ inputRefRef.current?.(instance.dom)
103
+ }
91
104
  },
92
105
 
93
106
  // Including editorState in the dependencies here will cause an endless
@@ -1,7 +1,7 @@
1
1
  import React, { useEffect, useId, useState } from 'react'
2
- import { type UseFloatingReturn } from '@floating-ui/react-dom'
3
2
  import { useButton } from '@react-aria/button'
4
3
  import { HiddenSelect, useSelect } from '@react-aria/select'
4
+ import { mergeRefs } from '@react-aria/utils'
5
5
  import { useSelectState, type SelectProps as AriaSelectProps } from '@react-stately/select'
6
6
  import { type Key } from '@react-types/shared'
7
7
  import classnames from 'classnames'
@@ -32,6 +32,7 @@ export type SingleSelectProps<Option extends SingleSelectOption = SingleSelectOp
32
32
  */
33
33
  items: SingleSelectItem<Option>[]
34
34
  id?: string
35
+ inputRef?: React.Ref<HTMLButtonElement>
35
36
  /**
36
37
  * Optional render function that allows custom rendering of the trigger button.
37
38
  * The function receives the `selectToggleProps` and a `ref` to be applied
@@ -39,7 +40,7 @@ export type SingleSelectProps<Option extends SingleSelectOption = SingleSelectOp
39
40
  */
40
41
  trigger?: (
41
42
  selectToggleProps: SelectToggleProps & {
42
- ref: UseFloatingReturn<HTMLButtonElement>['refs']['setReference']
43
+ ref: React.Ref<HTMLButtonElement>
43
44
  },
44
45
  ) => JSX.Element
45
46
  /**
@@ -95,6 +96,7 @@ export const SingleSelect = <Option extends SingleSelectOption = SingleSelectOpt
95
96
  isDisabled,
96
97
  onSelectionChange,
97
98
  portalContainerId,
99
+ inputRef,
98
100
  ...restProps
99
101
  }: SingleSelectProps<Option>): JSX.Element => {
100
102
  const { refs } = useFloating<HTMLButtonElement>()
@@ -153,7 +155,7 @@ export const SingleSelect = <Option extends SingleSelectOption = SingleSelectOpt
153
155
  status,
154
156
  'isDisabled': triggerProps.isDisabled,
155
157
  isReversed,
156
- 'ref': refs.setReference,
158
+ 'ref': mergeRefs(inputRef, refs.setReference),
157
159
  'aria-describedby': classnames(validationMessage && validationId, description && descriptionId),
158
160
  'aria-required': isRequired,
159
161
  }