@kaizen/components 3.1.6 → 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 (61) hide show
  1. package/dist/cjs/src/Avatar/Avatar.cjs +18 -14
  2. package/dist/cjs/src/DateInput/DateInput/DateInput.cjs +1 -2
  3. package/dist/cjs/src/DatePicker/DatePicker.cjs +6 -4
  4. package/dist/cjs/src/Focusable/Focusable.cjs +5 -4
  5. package/dist/cjs/src/MultiSelect/MultiSelect.cjs +4 -2
  6. package/dist/cjs/src/RichTextEditor/RichTextEditor/RichTextEditor.cjs +4 -1
  7. package/dist/cjs/src/RichTextEditor/RichTextEditor/utils/inputrules.cjs +0 -1
  8. package/dist/cjs/src/RichTextEditor/utils/core/createRichTextEditor.cjs +2 -1
  9. package/dist/cjs/src/RichTextEditor/utils/core/hooks/useRichTextEditor.cjs +24 -9
  10. package/dist/cjs/src/SingleSelect/SingleSelect.cjs +4 -2
  11. package/dist/cjs/src/Tabs/subcomponents/TabList/TabList.cjs +8 -1
  12. package/dist/cjs/src/TimeField/TimeField.cjs +9 -4
  13. package/dist/cjs/src/TimeField/subcomponents/TimeSegment/TimeSegment.cjs +6 -4
  14. package/dist/esm/src/Avatar/Avatar.mjs +18 -15
  15. package/dist/esm/src/DateInput/DateInput/DateInput.mjs +1 -2
  16. package/dist/esm/src/DatePicker/DatePicker.mjs +6 -4
  17. package/dist/esm/src/Focusable/Focusable.mjs +5 -4
  18. package/dist/esm/src/MultiSelect/MultiSelect.mjs +4 -2
  19. package/dist/esm/src/RichTextEditor/RichTextEditor/RichTextEditor.mjs +4 -1
  20. package/dist/esm/src/RichTextEditor/RichTextEditor/utils/inputrules.mjs +0 -1
  21. package/dist/esm/src/RichTextEditor/utils/core/createRichTextEditor.mjs +2 -1
  22. package/dist/esm/src/RichTextEditor/utils/core/hooks/useRichTextEditor.mjs +24 -9
  23. package/dist/esm/src/SingleSelect/SingleSelect.mjs +4 -2
  24. package/dist/esm/src/Tabs/subcomponents/TabList/TabList.mjs +8 -1
  25. package/dist/esm/src/TimeField/TimeField.mjs +9 -4
  26. package/dist/esm/src/TimeField/subcomponents/TimeSegment/TimeSegment.mjs +6 -4
  27. package/dist/styles.css +15 -15
  28. package/dist/types/DateInput/DateInputWithIconButton/DateInputWithIconButton.d.ts +2 -2
  29. package/dist/types/DatePicker/DatePicker.d.ts +3 -2
  30. package/dist/types/Focusable/Focusable.d.ts +8 -3
  31. package/dist/types/Input/Input/Input.d.ts +1 -1
  32. package/dist/types/MultiSelect/MultiSelect.d.ts +3 -2
  33. package/dist/types/Notification/index.d.ts +1 -0
  34. package/dist/types/RichTextEditor/RichTextEditor/RichTextEditor.d.ts +2 -1
  35. package/dist/types/RichTextEditor/utils/core/createRichTextEditor.d.ts +1 -0
  36. package/dist/types/RichTextEditor/utils/core/hooks/useRichTextEditor.d.ts +3 -2
  37. package/dist/types/SingleSelect/SingleSelect.d.ts +3 -3
  38. package/dist/types/TextArea/TextArea.d.ts +1 -1
  39. package/dist/types/TimeField/TimeField.d.ts +1 -0
  40. package/dist/types/TimeField/subcomponents/TimeSegment/TimeSegment.d.ts +3 -1
  41. package/package.json +7 -7
  42. package/src/Avatar/Avatar.tsx +19 -11
  43. package/src/DateInput/DateInput/DateInput.tsx +1 -2
  44. package/src/DateInput/DateInputWithIconButton/DateInputWithIconButton.tsx +2 -2
  45. package/src/DatePicker/DatePicker.tsx +6 -3
  46. package/src/Focusable/Focusable.tsx +17 -7
  47. package/src/Input/Input/Input.tsx +1 -1
  48. package/src/MultiSelect/MultiSelect.tsx +4 -1
  49. package/src/Notification/index.ts +1 -0
  50. package/src/RichTextEditor/RichTextEditor/RichTextEditor.tsx +3 -0
  51. package/src/RichTextEditor/utils/core/createRichTextEditor.spec.ts +1 -1
  52. package/src/RichTextEditor/utils/core/createRichTextEditor.ts +2 -0
  53. package/src/RichTextEditor/utils/core/hooks/useRichTextEditor.spec.tsx +8 -3
  54. package/src/RichTextEditor/utils/core/hooks/useRichTextEditor.ts +20 -7
  55. package/src/SingleSelect/SingleSelect.tsx +5 -3
  56. package/src/Tabs/_docs/Tabs.spec.stories.tsx +39 -0
  57. package/src/Tabs/_docs/Tabs.stories.tsx +38 -2
  58. package/src/Tabs/subcomponents/TabList/TabList.tsx +9 -1
  59. package/src/TextArea/TextArea.tsx +1 -1
  60. package/src/TimeField/TimeField.tsx +17 -15
  61. package/src/TimeField/subcomponents/TimeSegment/TimeSegment.tsx +6 -3
@@ -7,8 +7,8 @@ export type DateInputWithIconButtonProps = {
7
7
  onButtonClick: React.MouseEventHandler<HTMLButtonElement>;
8
8
  } & Omit<DateInputProps, 'startIconAdornment' | 'endIconAdornment'>;
9
9
  export type DateInputWithIconButtonRefs = {
10
- inputRef?: React.RefObject<HTMLInputElement>;
11
- buttonRef?: React.RefObject<HTMLButtonElement>;
10
+ inputRef?: React.Ref<HTMLInputElement>;
11
+ buttonRef?: React.Ref<HTMLButtonElement>;
12
12
  };
13
13
  export declare const DateInputWithIconButton: React.ForwardRefExoticComponent<{
14
14
  /**
@@ -1,4 +1,4 @@
1
- import { type RefObject } from 'react';
1
+ import React, { type RefObject } from 'react';
2
2
  import { type CalendarSingleProps, type DisabledDayMatchers } from "../Calendar";
3
3
  import { type DateInputFieldProps } from './subcomponents/DateInputField';
4
4
  import type { ValidationResponse } from './types';
@@ -6,6 +6,7 @@ import { type DatePickerSupportedLocales } from './utils/getLocale';
6
6
  type OmittedDateInputFieldProps = 'onClick' | 'onFocus' | 'onChange' | 'onBlur' | 'onButtonClick' | 'value' | 'locale' | 'id';
7
7
  export type DatePickerProps = {
8
8
  id?: string;
9
+ inputRef?: React.Ref<HTMLInputElement>;
9
10
  buttonRef?: RefObject<HTMLButtonElement>;
10
11
  onInputClick?: DateInputFieldProps['onClick'];
11
12
  onInputFocus?: DateInputFieldProps['onFocus'];
@@ -49,7 +50,7 @@ export type DatePickerProps = {
49
50
  * {@link https://cultureamp.design/?path=/docs/components-date-controls-datepicker--docs Storybook}
50
51
  */
51
52
  export declare const DatePicker: {
52
- ({ id: propsId, buttonRef: propsButtonRef, locale: propsLocale, disabledDates, disabledDaysOfWeek, disabledRange, disabledBeforeAfter, disabledBefore, disabledAfter, weekStartsOn, defaultMonth, selectedDay, status, validationMessage, onInputClick, onInputFocus, onInputChange, onInputBlur, onButtonClick, onDayChange, onValidate, ...restDateInputFieldProps }: DatePickerProps): JSX.Element;
53
+ ({ id: propsId, inputRef: propsInputRef, buttonRef: propsButtonRef, locale: propsLocale, disabledDates, disabledDaysOfWeek, disabledRange, disabledBeforeAfter, disabledBefore, disabledAfter, weekStartsOn, defaultMonth, selectedDay, status, validationMessage, onInputClick, onInputFocus, onInputChange, onInputBlur, onButtonClick, onDayChange, onValidate, ...restDateInputFieldProps }: DatePickerProps): JSX.Element;
53
54
  displayName: string;
54
55
  };
55
56
  export {};
@@ -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;
@@ -4,7 +4,7 @@ import { type InputStatus, type InputTypes } from './types';
4
4
  export type InputType = (typeof InputTypes)[number];
5
5
  export type InputStatusType = (typeof InputStatus)[number];
6
6
  export type InputProps = {
7
- inputRef?: React.RefObject<HTMLInputElement>;
7
+ inputRef?: React.Ref<HTMLInputElement>;
8
8
  status?: InputStatusType;
9
9
  startIconAdornment?: React.ReactNode;
10
10
  endIconAdornment?: React.ReactNode;
@@ -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;
@@ -1,7 +1,7 @@
1
1
  import React, { type TextareaHTMLAttributes } from 'react';
2
2
  import { type OverrideClassName } from "../types/OverrideClassName";
3
3
  export type TextAreaProps = {
4
- textAreaRef?: React.RefObject<HTMLTextAreaElement>;
4
+ textAreaRef?: React.Ref<HTMLTextAreaElement>;
5
5
  status?: 'default' | 'error' | 'caution';
6
6
  /**
7
7
  * Grows the input height as more content is added
@@ -17,6 +17,7 @@ export type TimeFieldProps = {
17
17
  value: ValueType | null;
18
18
  status?: StatusType;
19
19
  validationMessage?: React.ReactNode;
20
+ inputRef?: React.Ref<HTMLSpanElement>;
20
21
  } & OverrideClassName<Omit<TimeFieldStateOptions, OmittedTimeFieldProps>>;
21
22
  export declare const TimeField: {
22
23
  (props: TimeFieldProps): JSX.Element;
@@ -1,10 +1,12 @@
1
+ import React from 'react';
1
2
  import { type DateFieldState, type DateSegment } from '@react-stately/datepicker';
2
3
  export type TimeSegmentProps = {
3
4
  segment: DateSegment;
4
5
  state: DateFieldState;
5
6
  hasPadding?: boolean;
7
+ inputRef?: React.Ref<HTMLSpanElement>;
6
8
  };
7
9
  export declare const TimeSegment: {
8
- ({ segment, state, hasPadding, }: TimeSegmentProps): JSX.Element;
10
+ ({ segment, state, hasPadding, inputRef, }: TimeSegmentProps): JSX.Element;
9
11
  displayName: string;
10
12
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaizen/components",
3
- "version": "3.1.6",
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",
@@ -99,20 +99,20 @@
99
99
  "@react-stately/select": "^3.6.14",
100
100
  "@react-types/shared": "^3.30.0",
101
101
  "classnames": "^2.5.1",
102
- "date-fns": "^4.3.0",
102
+ "date-fns": "^4.4.0",
103
103
  "lodash.debounce": "^4.0.8",
104
104
  "nanobus": "^4.5.0",
105
105
  "prosemirror-commands": "^1.7.1",
106
106
  "prosemirror-history": "^1.5.0",
107
107
  "prosemirror-inputrules": "^1.5.1",
108
108
  "prosemirror-keymap": "^1.2.3",
109
- "prosemirror-model": "^1.25.7",
109
+ "prosemirror-model": "^1.25.8",
110
110
  "prosemirror-schema-basic": "^1.2.4",
111
111
  "prosemirror-schema-list": "^1.5.1",
112
112
  "prosemirror-state": "^1.4.4",
113
113
  "prosemirror-transform": "^1.12.0",
114
114
  "prosemirror-utils": "^1.2.2",
115
- "prosemirror-view": "^1.41.8",
115
+ "prosemirror-view": "^1.41.9",
116
116
  "react-animate-height": "^3.2.4",
117
117
  "react-aria": "^3.41.1",
118
118
  "react-aria-components": "^1.10.1",
@@ -130,7 +130,7 @@
130
130
  },
131
131
  "devDependencies": {
132
132
  "@cultureamp/frontend-apis": "13.3.0",
133
- "@cultureamp/i18n-react-intl": "^4.1.4",
133
+ "@cultureamp/i18n-react-intl": "^4.1.5",
134
134
  "@cultureamp/package-bundler": "^4.0.1",
135
135
  "cssnano": "^7.1.9",
136
136
  "@testing-library/dom": "^10.4.1",
@@ -155,14 +155,14 @@
155
155
  "react-dom": "^19.2.7",
156
156
  "react-highlight": "^0.15.0",
157
157
  "react-intl": "^10.1.13",
158
- "rollup": "^4.60.4",
158
+ "rollup": "^4.61.1",
159
159
  "sass": "1.79.6",
160
160
  "serialize-query-params": "^2.0.4",
161
161
  "svgo": "^4.0.1",
162
162
  "ts-patch": "^3.3.0",
163
163
  "tslib": "^2.8.1",
164
164
  "tsx": "^4.22.4",
165
- "@kaizen/design-tokens": "11.0.10"
165
+ "@kaizen/design-tokens": "11.0.11"
166
166
  },
167
167
  "devDependenciesComments": {
168
168
  "sass": "Prevent deprecation warnings introduced in 1.80 as we plan to move away from sass",
@@ -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
  }
@@ -2,7 +2,6 @@ import React from 'react'
2
2
  import classnames from 'classnames'
3
3
  import { Input, type InputProps } from '~components/Input'
4
4
  import { Label } from '~components/Label'
5
- import { isRefObject } from '~components/utils/isRefObject'
6
5
  import styles from './DateInput.module.css'
7
6
 
8
7
  type OmittedInputProps = 'reversed' | 'type' | 'inputRef'
@@ -23,7 +22,7 @@ export const DateInput = React.forwardRef<HTMLInputElement, DateInputProps>(
23
22
  disabled={disabled}
24
23
  />
25
24
  <Input
26
- inputRef={isRefObject(ref) ? ref : undefined}
25
+ inputRef={ref}
27
26
  id={id}
28
27
  type="text"
29
28
  autoComplete="off"
@@ -13,8 +13,8 @@ export type DateInputWithIconButtonProps = {
13
13
  } & Omit<DateInputProps, 'startIconAdornment' | 'endIconAdornment'>
14
14
 
15
15
  export type DateInputWithIconButtonRefs = {
16
- inputRef?: React.RefObject<HTMLInputElement>
17
- buttonRef?: React.RefObject<HTMLButtonElement>
16
+ inputRef?: React.Ref<HTMLInputElement>
17
+ buttonRef?: React.Ref<HTMLButtonElement>
18
18
  }
19
19
 
20
20
  export const DateInputWithIconButton = React.forwardRef<
@@ -1,5 +1,6 @@
1
1
  import React, { useEffect, useId, useRef, useState, type RefObject } from 'react'
2
2
  import { useIntl } from '@cultureamp/i18n-react-intl'
3
+ import { mergeRefs } from '@react-aria/utils'
3
4
  import { type DayEventHandler } from 'react-day-picker'
4
5
  import { FocusOn } from 'react-focus-on'
5
6
  import {
@@ -35,6 +36,7 @@ type OmittedDateInputFieldProps =
35
36
 
36
37
  export type DatePickerProps = {
37
38
  id?: string
39
+ inputRef?: React.Ref<HTMLInputElement>
38
40
  buttonRef?: RefObject<HTMLButtonElement>
39
41
  onInputClick?: DateInputFieldProps['onClick']
40
42
  onInputFocus?: DateInputFieldProps['onFocus']
@@ -81,6 +83,7 @@ export type DatePickerProps = {
81
83
  */
82
84
  export const DatePicker = ({
83
85
  id: propsId,
86
+ inputRef: propsInputRef,
84
87
  buttonRef: propsButtonRef,
85
88
  locale: propsLocale = 'en-AU',
86
89
  disabledDates,
@@ -115,11 +118,11 @@ export const DatePicker = ({
115
118
  const id = propsId ?? reactId
116
119
 
117
120
  const containerRef = useRef<HTMLInputElement>(null)
118
- const inputRef = useRef<HTMLInputElement>(null)
121
+ const internalInputRef = useRef<HTMLInputElement>(null)
119
122
  const fallbackButtonRef = useRef<HTMLButtonElement>(null)
120
123
  const buttonRef = propsButtonRef ?? fallbackButtonRef
121
124
  const dateInputRefs = useRef({
122
- inputRef,
125
+ inputRef: mergeRefs<HTMLInputElement>(internalInputRef, propsInputRef),
123
126
  buttonRef,
124
127
  })
125
128
  const [inputValue, setInputValue] = useState<string>('')
@@ -239,7 +242,7 @@ export const DatePicker = ({
239
242
 
240
243
  const handleReturnFocus = (): void => {
241
244
  if (lastTrigger === 'inputKeydown' || lastTrigger === 'inputFocus') {
242
- return inputRef.current?.focus()
245
+ return internalInputRef.current?.focus()
243
246
  }
244
247
  buttonRef.current?.focus()
245
248
  }
@@ -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
  }
@@ -8,7 +8,7 @@ export type InputType = (typeof InputTypes)[number]
8
8
  export type InputStatusType = (typeof InputStatus)[number]
9
9
 
10
10
  export type InputProps = {
11
- inputRef?: React.RefObject<HTMLInputElement>
11
+ inputRef?: React.Ref<HTMLInputElement>
12
12
  status?: InputStatusType
13
13
  startIconAdornment?: React.ReactNode
14
14
  endIconAdornment?: React.ReactNode
@@ -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