@kaizen/components 3.1.5 → 3.2.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 (30) hide show
  1. package/dist/cjs/src/Avatar/Avatar.cjs +5 -1
  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/Tabs/subcomponents/TabList/TabList.cjs +8 -1
  5. package/dist/cjs/src/TimeField/TimeField.cjs +9 -4
  6. package/dist/cjs/src/TimeField/subcomponents/TimeSegment/TimeSegment.cjs +6 -4
  7. package/dist/esm/src/Avatar/Avatar.mjs +5 -1
  8. package/dist/esm/src/DateInput/DateInput/DateInput.mjs +1 -2
  9. package/dist/esm/src/DatePicker/DatePicker.mjs +6 -4
  10. package/dist/esm/src/Tabs/subcomponents/TabList/TabList.mjs +8 -1
  11. package/dist/esm/src/TimeField/TimeField.mjs +9 -4
  12. package/dist/esm/src/TimeField/subcomponents/TimeSegment/TimeSegment.mjs +6 -4
  13. package/dist/types/DateInput/DateInputWithIconButton/DateInputWithIconButton.d.ts +2 -2
  14. package/dist/types/DatePicker/DatePicker.d.ts +3 -2
  15. package/dist/types/Input/Input/Input.d.ts +1 -1
  16. package/dist/types/TextArea/TextArea.d.ts +1 -1
  17. package/dist/types/TimeField/TimeField.d.ts +1 -0
  18. package/dist/types/TimeField/subcomponents/TimeSegment/TimeSegment.d.ts +3 -1
  19. package/package.json +7 -7
  20. package/src/Avatar/Avatar.tsx +4 -1
  21. package/src/DateInput/DateInput/DateInput.tsx +1 -2
  22. package/src/DateInput/DateInputWithIconButton/DateInputWithIconButton.tsx +2 -2
  23. package/src/DatePicker/DatePicker.tsx +6 -3
  24. package/src/Input/Input/Input.tsx +1 -1
  25. package/src/Tabs/_docs/Tabs.spec.stories.tsx +39 -0
  26. package/src/Tabs/_docs/Tabs.stories.tsx +38 -2
  27. package/src/Tabs/subcomponents/TabList/TabList.tsx +9 -1
  28. package/src/TextArea/TextArea.tsx +1 -1
  29. package/src/TimeField/TimeField.tsx +17 -15
  30. package/src/TimeField/subcomponents/TimeSegment/TimeSegment.tsx +6 -3
@@ -61,9 +61,13 @@ var renderInitials = function (fullName, alt, size, disableInitials) {
61
61
  title: alt
62
62
  }, isLongName ?
63
63
  // Only called if 3 or more initials, fits text width for long names
64
+ //
65
+ // Ignore Chromatic diffs since the font-size calculation has shown itself to be slightly non-deterministic,
66
+ // causing flaky tests.
64
67
  React__default.default.createElement(reactTextfit.Textfit, {
65
68
  mode: "single",
66
- max: getMaxFontSizePixels(size)
69
+ max: getMaxFontSizePixels(size),
70
+ "data-chromatic": "ignore"
67
71
  }, initials) : getInitials(fullName, size === 'small'));
68
72
  };
69
73
  /**
@@ -7,7 +7,6 @@ var Input = require('../../Input/Input/Input.cjs');
7
7
  require('../../Input/InputRange/InputRange.cjs');
8
8
  require('../../Input/InputSearch/InputSearch.cjs');
9
9
  var Label = require('../../Label/Label.cjs');
10
- var isRefObject = require('../../utils/isRefObject.cjs');
11
10
  var DateInput_module = require('./DateInput.module.css.cjs');
12
11
  function _interopDefault(e) {
13
12
  return e && e.__esModule ? e : {
@@ -33,7 +32,7 @@ var DateInput = React__default.default.forwardRef(function (_a, ref) {
33
32
  reversed: isReversed,
34
33
  disabled: disabled
35
34
  }), React__default.default.createElement(Input.Input, tslib.__assign({
36
- inputRef: isRefObject.isRefObject(ref) ? ref : undefined,
35
+ inputRef: ref,
37
36
  id: id,
38
37
  type: "text",
39
38
  autoComplete: "off",
@@ -3,6 +3,7 @@
3
3
  var tslib = require('tslib');
4
4
  var React = require('react');
5
5
  var i18nReactIntl = require('@cultureamp/i18n-react-intl');
6
+ var utils = require('@react-aria/utils');
6
7
  var reactFocusOn = require('react-focus-on');
7
8
  var CalendarSingle = require('../Calendar/CalendarSingle/CalendarSingle.cjs');
8
9
  require('../Calendar/CalendarRange/CalendarRange.cjs');
@@ -33,6 +34,7 @@ var React__default = /*#__PURE__*/_interopDefault(React);
33
34
  */
34
35
  var DatePicker = function (_a) {
35
36
  var propsId = _a.id,
37
+ propsInputRef = _a.inputRef,
36
38
  propsButtonRef = _a.buttonRef,
37
39
  _b = _a.locale,
38
40
  propsLocale = _b === void 0 ? 'en-AU' : _b,
@@ -54,7 +56,7 @@ var DatePicker = function (_a) {
54
56
  onButtonClick = _a.onButtonClick,
55
57
  onDayChange = _a.onDayChange,
56
58
  onValidate = _a.onValidate,
57
- restDateInputFieldProps = tslib.__rest(_a, ["id", "buttonRef", "locale", "disabledDates", "disabledDaysOfWeek", "disabledRange", "disabledBeforeAfter", "disabledBefore", "disabledAfter", "weekStartsOn", "defaultMonth", "selectedDay", "status", "validationMessage", "onInputClick", "onInputFocus", "onInputChange", "onInputBlur", "onButtonClick", "onDayChange", "onValidate"]);
59
+ restDateInputFieldProps = tslib.__rest(_a, ["id", "inputRef", "buttonRef", "locale", "disabledDates", "disabledDaysOfWeek", "disabledRange", "disabledBeforeAfter", "disabledBefore", "disabledAfter", "weekStartsOn", "defaultMonth", "selectedDay", "status", "validationMessage", "onInputClick", "onInputFocus", "onInputChange", "onInputBlur", "onButtonClick", "onDayChange", "onValidate"]);
58
60
  var formatMessage = i18nReactIntl.useIntl().formatMessage;
59
61
  var calendarLabelDesc = formatMessage({
60
62
  id: 'datePicker.calendarLabelDescription',
@@ -64,11 +66,11 @@ var DatePicker = function (_a) {
64
66
  var reactId = React.useId();
65
67
  var id = propsId !== null && propsId !== void 0 ? propsId : reactId;
66
68
  var containerRef = React.useRef(null);
67
- var inputRef = React.useRef(null);
69
+ var internalInputRef = React.useRef(null);
68
70
  var fallbackButtonRef = React.useRef(null);
69
71
  var buttonRef = propsButtonRef !== null && propsButtonRef !== void 0 ? propsButtonRef : fallbackButtonRef;
70
72
  var dateInputRefs = React.useRef({
71
- inputRef: inputRef,
73
+ inputRef: utils.mergeRefs(internalInputRef, propsInputRef),
72
74
  buttonRef: buttonRef
73
75
  });
74
76
  var _c = React.useState(''),
@@ -177,7 +179,7 @@ var DatePicker = function (_a) {
177
179
  var handleReturnFocus = function () {
178
180
  var _a, _b;
179
181
  if (lastTrigger === 'inputKeydown' || lastTrigger === 'inputFocus') {
180
- return (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
182
+ return (_a = internalInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
181
183
  }
182
184
  (_b = buttonRef.current) === null || _b === void 0 ? void 0 : _b.focus();
183
185
  };
@@ -46,6 +46,7 @@ var TabList = function (props) {
46
46
  setContainerElement = _f[1];
47
47
  var tabListContext = React.useContext(reactAriaComponents.TabListStateContext);
48
48
  var selectedKey = tabListContext === null || tabListContext === void 0 ? void 0 : tabListContext.selectedKey;
49
+ var prevSelectedKey = React.useRef(selectedKey);
49
50
  React.useEffect(function () {
50
51
  if (!isDocumentReady) {
51
52
  setIsDocumentReady(true);
@@ -95,7 +96,13 @@ var TabList = function (props) {
95
96
  if (!isDocumentReady) {
96
97
  return;
97
98
  }
98
- // Scroll selected tab into view
99
+ // Only scroll the selected tab into view when the selection actually changes
100
+ // (i.e. user interaction). Skipping the no-op runs avoids scrolling the page
101
+ // on mount when the Tabs sit below the fold.
102
+ if (prevSelectedKey.current === selectedKey) {
103
+ return;
104
+ }
105
+ prevSelectedKey.current = selectedKey;
99
106
  (_a = containerElement === null || containerElement === void 0 ? void 0 : containerElement.querySelector('[role="tab"][data-selected=true]')) === null || _a === void 0 ? void 0 : _a.scrollIntoView({
100
107
  block: 'nearest',
101
108
  inline: 'center'
@@ -38,7 +38,8 @@ var TimeFieldComponent = function (_a) {
38
38
  validationMessage = _a.validationMessage,
39
39
  isDisabled = _a.isDisabled,
40
40
  classNameOverride = _a.classNameOverride,
41
- restProps = tslib.__rest(_a, ["id", "label", "locale", "onChange", "value", "status", "validationMessage", "isDisabled", "classNameOverride"]);
41
+ inputRef = _a.inputRef,
42
+ restProps = tslib.__rest(_a, ["id", "label", "locale", "onChange", "value", "status", "validationMessage", "isDisabled", "classNameOverride", "inputRef"]);
42
43
  var reactId = React.useId();
43
44
  var id = propsId !== null && propsId !== void 0 ? propsId : reactId;
44
45
  var handleOnChange = function (timeValue) {
@@ -64,14 +65,17 @@ var TimeFieldComponent = function (_a) {
64
65
  }));
65
66
  var hasError = !!validationMessage && status === 'error';
66
67
  var descriptionId = hasError ? "".concat(id, "-field-message") : undefined;
67
- var inputRef = React__default.default.useRef(null);
68
+ var internalRef = React__default.default.useRef(null);
68
69
  var _c = datepicker$1.useTimeField(tslib.__assign(tslib.__assign({}, restProps), {
69
70
  label: label,
70
71
  isDisabled: isDisabled,
71
72
  'aria-describedby': descriptionId
72
- }), state, inputRef),
73
+ }), state, internalRef),
73
74
  fieldProps = _c.fieldProps,
74
75
  labelProps = _c.labelProps;
76
+ var firstEditableIndex = state.segments.findIndex(function (s) {
77
+ return s.isEditable;
78
+ });
75
79
  return React__default.default.createElement("div", {
76
80
  className: classNameOverride
77
81
  }, React__default.default.createElement(Label.Label, tslib.__assign({
@@ -82,13 +86,14 @@ var TimeFieldComponent = function (_a) {
82
86
  className: TimeField_module.wrapper
83
87
  }, React__default.default.createElement("div", tslib.__assign({}, fieldProps, {
84
88
  id: id,
85
- ref: inputRef,
89
+ ref: internalRef,
86
90
  className: classnames__default.default(TimeField_module.input, state.isDisabled && TimeField_module.isDisabled, status === 'error' && TimeField_module.error)
87
91
  }), state.segments.map(function (segment, i) {
88
92
  return React__default.default.createElement(TimeSegment.TimeSegment, {
89
93
  key: i,
90
94
  segment: segment,
91
95
  state: state,
96
+ inputRef: i === firstEditableIndex ? inputRef : undefined,
92
97
  hasPadding: ![8294, 8297].includes(segment.text.charCodeAt(0))
93
98
  });
94
99
  }), React__default.default.createElement("div", {
@@ -3,6 +3,7 @@
3
3
  var tslib = require('tslib');
4
4
  var React = require('react');
5
5
  var datepicker = require('@react-aria/datepicker');
6
+ var utils = require('@react-aria/utils');
6
7
  var classnames = require('classnames');
7
8
  var generateSegmentDisplayText = require('./utils/generateSegmentDisplayText.cjs');
8
9
  var TimeSegment_module = require('./TimeSegment.module.css.cjs');
@@ -17,9 +18,10 @@ var TimeSegment = function (_a) {
17
18
  var segment = _a.segment,
18
19
  state = _a.state,
19
20
  _b = _a.hasPadding,
20
- hasPadding = _b === void 0 ? true : _b;
21
- var ref = React__default.default.useRef(null);
22
- var segmentProps = datepicker.useDateSegment(segment, state, ref).segmentProps;
21
+ hasPadding = _b === void 0 ? true : _b,
22
+ inputRef = _a.inputRef;
23
+ var internalRef = React__default.default.useRef(null);
24
+ var segmentProps = datepicker.useDateSegment(segment, state, internalRef).segmentProps;
23
25
  // Chrome has a bug where `contenteditable` elements receive focus from external clicks.
24
26
  // This (in combination with the invisible character ​) creates boundaries
25
27
  // for the element.
@@ -27,7 +29,7 @@ var TimeSegment = function (_a) {
27
29
  return React__default.default.createElement("span", {
28
30
  className: TimeSegment_module.timeSegmentWrapper
29
31
  }, "\u200B", React__default.default.createElement("span", tslib.__assign({}, segmentProps, {
30
- ref: ref,
32
+ ref: utils.mergeRefs(internalRef, inputRef),
31
33
  className: classnames__default.default(TimeSegment_module.timeSegment, segment.type === 'literal' && TimeSegment_module.literal, segment.isPlaceholder && TimeSegment_module.placeholder, segment.type === 'dayPeriod' && TimeSegment_module.dayPeriod, hasPadding && TimeSegment_module.hasPadding)
32
34
  }), generateSegmentDisplayText.generateSegmentDisplayText(segment)), "\u200B");
33
35
  };
@@ -53,9 +53,13 @@ var renderInitials = function (fullName, alt, size, disableInitials) {
53
53
  }, isLongName ? (
54
54
  /*#__PURE__*/
55
55
  // Only called if 3 or more initials, fits text width for long names
56
+ //
57
+ // Ignore Chromatic diffs since the font-size calculation has shown itself to be slightly non-deterministic,
58
+ // causing flaky tests.
56
59
  React.createElement(Textfit, {
57
60
  mode: "single",
58
- max: getMaxFontSizePixels(size)
61
+ max: getMaxFontSizePixels(size),
62
+ "data-chromatic": "ignore"
59
63
  }, initials)) : getInitials(fullName, size === 'small')));
60
64
  };
61
65
  /**
@@ -5,7 +5,6 @@ import { Input } from '../../Input/Input/Input.mjs';
5
5
  import '../../Input/InputRange/InputRange.mjs';
6
6
  import '../../Input/InputSearch/InputSearch.mjs';
7
7
  import { Label } from '../../Label/Label.mjs';
8
- import { isRefObject } from '../../utils/isRefObject.mjs';
9
8
  import modules_30be64ed from './DateInput.module.css.mjs';
10
9
  const DateInput = /*#__PURE__*/function () {
11
10
  const DateInput = /*#__PURE__*/React.forwardRef(function (_a, ref) {
@@ -25,7 +24,7 @@ const DateInput = /*#__PURE__*/function () {
25
24
  reversed: isReversed,
26
25
  disabled: disabled
27
26
  }), /*#__PURE__*/React.createElement(Input, __assign({
28
- inputRef: isRefObject(ref) ? ref : undefined,
27
+ inputRef: ref,
29
28
  id: id,
30
29
  type: "text",
31
30
  autoComplete: "off",
@@ -1,6 +1,7 @@
1
1
  import { __rest, __assign } from 'tslib';
2
2
  import React, { useId, useRef, useState, useEffect } from 'react';
3
3
  import { useIntl } from '@cultureamp/i18n-react-intl';
4
+ import { mergeRefs } from '@react-aria/utils';
4
5
  import { FocusOn } from 'react-focus-on';
5
6
  import { CalendarSingle } from '../Calendar/CalendarSingle/CalendarSingle.mjs';
6
7
  import '../Calendar/CalendarRange/CalendarRange.mjs';
@@ -26,6 +27,7 @@ import { validateDate } from './utils/validateDate.mjs';
26
27
  const DatePicker = /*#__PURE__*/function () {
27
28
  const DatePicker = function (_a) {
28
29
  var propsId = _a.id,
30
+ propsInputRef = _a.inputRef,
29
31
  propsButtonRef = _a.buttonRef,
30
32
  _b = _a.locale,
31
33
  propsLocale = _b === void 0 ? 'en-AU' : _b,
@@ -47,7 +49,7 @@ const DatePicker = /*#__PURE__*/function () {
47
49
  onButtonClick = _a.onButtonClick,
48
50
  onDayChange = _a.onDayChange,
49
51
  onValidate = _a.onValidate,
50
- restDateInputFieldProps = __rest(_a, ["id", "buttonRef", "locale", "disabledDates", "disabledDaysOfWeek", "disabledRange", "disabledBeforeAfter", "disabledBefore", "disabledAfter", "weekStartsOn", "defaultMonth", "selectedDay", "status", "validationMessage", "onInputClick", "onInputFocus", "onInputChange", "onInputBlur", "onButtonClick", "onDayChange", "onValidate"]);
52
+ restDateInputFieldProps = __rest(_a, ["id", "inputRef", "buttonRef", "locale", "disabledDates", "disabledDaysOfWeek", "disabledRange", "disabledBeforeAfter", "disabledBefore", "disabledAfter", "weekStartsOn", "defaultMonth", "selectedDay", "status", "validationMessage", "onInputClick", "onInputFocus", "onInputChange", "onInputBlur", "onButtonClick", "onDayChange", "onValidate"]);
51
53
  var formatMessage = useIntl().formatMessage;
52
54
  var calendarLabelDesc = formatMessage({
53
55
  id: 'datePicker.calendarLabelDescription',
@@ -57,11 +59,11 @@ const DatePicker = /*#__PURE__*/function () {
57
59
  var reactId = useId();
58
60
  var id = propsId !== null && propsId !== void 0 ? propsId : reactId;
59
61
  var containerRef = useRef(null);
60
- var inputRef = useRef(null);
62
+ var internalInputRef = useRef(null);
61
63
  var fallbackButtonRef = useRef(null);
62
64
  var buttonRef = propsButtonRef !== null && propsButtonRef !== void 0 ? propsButtonRef : fallbackButtonRef;
63
65
  var dateInputRefs = useRef({
64
- inputRef: inputRef,
66
+ inputRef: mergeRefs(internalInputRef, propsInputRef),
65
67
  buttonRef: buttonRef
66
68
  });
67
69
  var _c = useState(''),
@@ -170,7 +172,7 @@ const DatePicker = /*#__PURE__*/function () {
170
172
  var handleReturnFocus = function () {
171
173
  var _a, _b;
172
174
  if (lastTrigger === 'inputKeydown' || lastTrigger === 'inputFocus') {
173
- return (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
175
+ return (_a = internalInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
174
176
  }
175
177
  (_b = buttonRef.current) === null || _b === void 0 ? void 0 : _b.focus();
176
178
  };
@@ -37,6 +37,7 @@ var TabList = function (props) {
37
37
  setContainerElement = _f[1];
38
38
  var tabListContext = useContext(TabListStateContext);
39
39
  var selectedKey = tabListContext === null || tabListContext === void 0 ? void 0 : tabListContext.selectedKey;
40
+ var prevSelectedKey = useRef(selectedKey);
40
41
  useEffect(function () {
41
42
  if (!isDocumentReady) {
42
43
  setIsDocumentReady(true);
@@ -86,7 +87,13 @@ var TabList = function (props) {
86
87
  if (!isDocumentReady) {
87
88
  return;
88
89
  }
89
- // Scroll selected tab into view
90
+ // Only scroll the selected tab into view when the selection actually changes
91
+ // (i.e. user interaction). Skipping the no-op runs avoids scrolling the page
92
+ // on mount when the Tabs sit below the fold.
93
+ if (prevSelectedKey.current === selectedKey) {
94
+ return;
95
+ }
96
+ prevSelectedKey.current = selectedKey;
90
97
  (_a = containerElement === null || containerElement === void 0 ? void 0 : containerElement.querySelector('[role="tab"][data-selected=true]')) === null || _a === void 0 ? void 0 : _a.scrollIntoView({
91
98
  block: 'nearest',
92
99
  inline: 'center'
@@ -33,7 +33,8 @@ const TimeFieldComponent = /*#__PURE__*/function () {
33
33
  validationMessage = _a.validationMessage,
34
34
  isDisabled = _a.isDisabled,
35
35
  classNameOverride = _a.classNameOverride,
36
- restProps = __rest(_a, ["id", "label", "locale", "onChange", "value", "status", "validationMessage", "isDisabled", "classNameOverride"]);
36
+ inputRef = _a.inputRef,
37
+ restProps = __rest(_a, ["id", "label", "locale", "onChange", "value", "status", "validationMessage", "isDisabled", "classNameOverride", "inputRef"]);
37
38
  var reactId = useId();
38
39
  var id = propsId !== null && propsId !== void 0 ? propsId : reactId;
39
40
  var handleOnChange = function (timeValue) {
@@ -59,14 +60,17 @@ const TimeFieldComponent = /*#__PURE__*/function () {
59
60
  }));
60
61
  var hasError = !!validationMessage && status === 'error';
61
62
  var descriptionId = hasError ? "".concat(id, "-field-message") : undefined;
62
- var inputRef = React.useRef(null);
63
+ var internalRef = React.useRef(null);
63
64
  var _c = useTimeField(__assign(__assign({}, restProps), {
64
65
  label: label,
65
66
  isDisabled: isDisabled,
66
67
  'aria-describedby': descriptionId
67
- }), state, inputRef),
68
+ }), state, internalRef),
68
69
  fieldProps = _c.fieldProps,
69
70
  labelProps = _c.labelProps;
71
+ var firstEditableIndex = state.segments.findIndex(function (s) {
72
+ return s.isEditable;
73
+ });
70
74
  return /*#__PURE__*/React.createElement("div", {
71
75
  className: classNameOverride
72
76
  }, /*#__PURE__*/React.createElement(Label, __assign({
@@ -77,13 +81,14 @@ const TimeFieldComponent = /*#__PURE__*/function () {
77
81
  className: modules_0db24a3e.wrapper
78
82
  }, /*#__PURE__*/React.createElement("div", __assign({}, fieldProps, {
79
83
  id: id,
80
- ref: inputRef,
84
+ ref: internalRef,
81
85
  className: classnames(modules_0db24a3e.input, state.isDisabled && modules_0db24a3e.isDisabled, status === 'error' && modules_0db24a3e.error)
82
86
  }), state.segments.map(function (segment, i) {
83
87
  return /*#__PURE__*/React.createElement(TimeSegment, {
84
88
  key: i,
85
89
  segment: segment,
86
90
  state: state,
91
+ inputRef: i === firstEditableIndex ? inputRef : undefined,
87
92
  hasPadding: ![8294, 8297].includes(segment.text.charCodeAt(0))
88
93
  });
89
94
  }), /*#__PURE__*/React.createElement("div", {
@@ -1,6 +1,7 @@
1
1
  import { __assign } from 'tslib';
2
2
  import React from 'react';
3
3
  import { useDateSegment } from '@react-aria/datepicker';
4
+ import { mergeRefs } from '@react-aria/utils';
4
5
  import classnames from 'classnames';
5
6
  import { generateSegmentDisplayText } from './utils/generateSegmentDisplayText.mjs';
6
7
  import modules_0ac56f95 from './TimeSegment.module.css.mjs';
@@ -9,9 +10,10 @@ const TimeSegment = /*#__PURE__*/function () {
9
10
  var segment = _a.segment,
10
11
  state = _a.state,
11
12
  _b = _a.hasPadding,
12
- hasPadding = _b === void 0 ? true : _b;
13
- var ref = React.useRef(null);
14
- var segmentProps = useDateSegment(segment, state, ref).segmentProps;
13
+ hasPadding = _b === void 0 ? true : _b,
14
+ inputRef = _a.inputRef;
15
+ var internalRef = React.useRef(null);
16
+ var segmentProps = useDateSegment(segment, state, internalRef).segmentProps;
15
17
  // Chrome has a bug where `contenteditable` elements receive focus from external clicks.
16
18
  // This (in combination with the invisible character ​) creates boundaries
17
19
  // for the element.
@@ -19,7 +21,7 @@ const TimeSegment = /*#__PURE__*/function () {
19
21
  return /*#__PURE__*/React.createElement("span", {
20
22
  className: modules_0ac56f95.timeSegmentWrapper
21
23
  }, "\u200B", /*#__PURE__*/React.createElement("span", __assign({}, segmentProps, {
22
- ref: ref,
24
+ ref: mergeRefs(internalRef, inputRef),
23
25
  className: classnames(modules_0ac56f95.timeSegment, segment.type === 'literal' && modules_0ac56f95.literal, segment.isPlaceholder && modules_0ac56f95.placeholder, segment.type === 'dayPeriod' && modules_0ac56f95.dayPeriod, hasPadding && modules_0ac56f95.hasPadding)
24
26
  }), generateSegmentDisplayText(segment)), "\u200B");
25
27
  };
@@ -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 {};
@@ -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,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.5",
3
+ "version": "3.2.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",
@@ -97,7 +97,10 @@ const renderInitials = (
97
97
  <abbr className={classnames(styles.initials, isLongName && styles.longName)} title={alt}>
98
98
  {isLongName ? (
99
99
  // Only called if 3 or more initials, fits text width for long names
100
- <Textfit mode="single" max={getMaxFontSizePixels(size)}>
100
+ //
101
+ // Ignore Chromatic diffs since the font-size calculation has shown itself to be slightly non-deterministic,
102
+ // causing flaky tests.
103
+ <Textfit mode="single" max={getMaxFontSizePixels(size)} data-chromatic="ignore">
101
104
  {initials}
102
105
  </Textfit>
103
106
  ) : (
@@ -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
  }
@@ -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
@@ -117,6 +117,45 @@ export const ArrowsShowingAndHidingRTL: Story = {
117
117
  },
118
118
  }
119
119
 
120
+ let scrollIntoViewCalls = 0
121
+ const originalScrollIntoView = HTMLElement.prototype.scrollIntoView
122
+
123
+ /**
124
+ * The selected tab must NOT be scrolled into view on mount (KZN-3363) — only when
125
+ * the selection changes via user interaction. This protects users who render Tabs
126
+ * below the fold from having the page jump on load.
127
+ */
128
+ export const ScrollsIntoViewOnSelectionOnly: Story = {
129
+ name: 'Scrolls into view on selection only',
130
+ beforeEach: () => {
131
+ scrollIntoViewCalls = 0
132
+ HTMLElement.prototype.scrollIntoView = function (): void {
133
+ scrollIntoViewCalls += 1
134
+ }
135
+ return () => {
136
+ HTMLElement.prototype.scrollIntoView = originalScrollIntoView
137
+ }
138
+ },
139
+ render: (args) => (
140
+ <div style={{ maxWidth: '500px' }}>
141
+ <Tabs defaultSelectedKey="one" {...args} />
142
+ </div>
143
+ ),
144
+ play: async ({ canvasElement, step }) => {
145
+ const canvas = within(canvasElement.parentElement!)
146
+
147
+ await step('No scroll into view on mount', async () => {
148
+ expect(scrollIntoViewCalls).toBe(0)
149
+ })
150
+
151
+ await step('Scrolls into view when a tab is selected', async () => {
152
+ const tabFour = await canvas.findByRole('tab', { name: 'Tab 4' })
153
+ await userEvent.click(tabFour)
154
+ await waitFor(() => expect(scrollIntoViewCalls).toBeGreaterThan(0))
155
+ })
156
+ },
157
+ }
158
+
120
159
  export const AsyncLoaded: Story = {
121
160
  render: () => {
122
161
  const [selectedKey, setSelectedKey] = useState<Key>(0)
@@ -1,6 +1,6 @@
1
1
  import React, { useState } from 'react'
2
2
  import { type Meta, type StoryObj } from '@storybook/react'
3
- import { Button } from '~components/ButtonV1'
3
+ import { Button } from '~components/Button'
4
4
  import { Text } from '~components/Text'
5
5
  import { Tab, TabList, TabPanel, Tabs, type Key } from '../index'
6
6
 
@@ -82,8 +82,44 @@ export const Controlled: Story = {
82
82
  <Text variant="body">Content 2</Text>
83
83
  </TabPanel>
84
84
  </Tabs>
85
- <Button label="Switch to tab 2" onClick={(): void => setSelectedKey('two')} />
85
+ <Button onClick={(): void => setSelectedKey('two')}>Switch to tab 2</Button>
86
86
  </>
87
87
  )
88
88
  },
89
89
  }
90
+
91
+ /**
92
+ * When a tab is selected via user interaction, `TabList` scrolls the selected tab
93
+ * into view within its horizontal strip. It deliberately does *not* do this on
94
+ * mount — so when `Tabs` render below the fold, landing on the page does not scroll
95
+ * the tabs into view or jump the page.
96
+ *
97
+ * This story pushes the `Tabs` below the fold with a full-viewport spacer. On load
98
+ * the page stays at the top. Scroll down and select a tab — only then does the
99
+ * selected tab scroll into view within the strip.
100
+ */
101
+ export const BelowTheFold: Story = {
102
+ render: () => (
103
+ <div style={{ maxWidth: '760px' }}>
104
+ <div style={{ height: '100dvh' }}>
105
+ <Text variant="body">Scroll down — the tabs are rendered below the fold.</Text>
106
+ </div>
107
+ <Tabs defaultSelectedKey="tab-0">
108
+ <TabList aria-label="Lorem ipsum tabs">
109
+ {Array.from({ length: 8 }, (_, i) => (
110
+ <Tab key={i} id={`tab-${i}`}>
111
+ Lorem {i + 1}
112
+ </Tab>
113
+ ))}
114
+ </TabList>
115
+ {Array.from({ length: 8 }, (_, i) => (
116
+ <TabPanel key={i} id={`tab-${i}`} className="p-24">
117
+ <Text variant="body">
118
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit — panel {i + 1}.
119
+ </Text>
120
+ </TabPanel>
121
+ ))}
122
+ </Tabs>
123
+ </div>
124
+ ),
125
+ }
@@ -44,6 +44,7 @@ export const TabList = (props: TabListProps): JSX.Element => {
44
44
  const [containerElement, setContainerElement] = useState<HTMLElement | null>()
45
45
  const tabListContext = useContext(TabListStateContext)
46
46
  const selectedKey = tabListContext?.selectedKey
47
+ const prevSelectedKey = useRef(selectedKey)
47
48
 
48
49
  useEffect(() => {
49
50
  if (!isDocumentReady) {
@@ -107,7 +108,14 @@ export const TabList = (props: TabListProps): JSX.Element => {
107
108
  return
108
109
  }
109
110
 
110
- // Scroll selected tab into view
111
+ // Only scroll the selected tab into view when the selection actually changes
112
+ // (i.e. user interaction). Skipping the no-op runs avoids scrolling the page
113
+ // on mount when the Tabs sit below the fold.
114
+ if (prevSelectedKey.current === selectedKey) {
115
+ return
116
+ }
117
+ prevSelectedKey.current = selectedKey
118
+
111
119
  containerElement
112
120
  ?.querySelector('[role="tab"][data-selected=true]')
113
121
  ?.scrollIntoView({ block: 'nearest', inline: 'center' })
@@ -4,7 +4,7 @@ import { type OverrideClassName } from '~components/types/OverrideClassName'
4
4
  import styles from './TextArea.module.css'
5
5
 
6
6
  export type TextAreaProps = {
7
- textAreaRef?: React.RefObject<HTMLTextAreaElement>
7
+ textAreaRef?: React.Ref<HTMLTextAreaElement>
8
8
  status?: 'default' | 'error' | 'caution'
9
9
  /**
10
10
  * Grows the input height as more content is added
@@ -27,6 +27,7 @@ export type TimeFieldProps = {
27
27
  value: ValueType | null
28
28
  status?: StatusType
29
29
  validationMessage?: React.ReactNode
30
+ inputRef?: React.Ref<HTMLSpanElement>
30
31
  } & OverrideClassName<Omit<TimeFieldStateOptions, OmittedTimeFieldProps>>
31
32
 
32
33
  // This needed to be placed directly below the props because
@@ -49,6 +50,7 @@ const TimeFieldComponent = ({
49
50
  validationMessage,
50
51
  isDisabled,
51
52
  classNameOverride,
53
+ inputRef,
52
54
  ...restProps
53
55
  }: TimeFieldProps): JSX.Element => {
54
56
  const reactId = useId()
@@ -79,7 +81,7 @@ const TimeFieldComponent = ({
79
81
  const hasError = !!validationMessage && status === 'error'
80
82
  const descriptionId = hasError ? `${id}-field-message` : undefined
81
83
 
82
- const inputRef = React.useRef(null)
84
+ const internalRef = React.useRef<HTMLDivElement>(null)
83
85
  const { fieldProps, labelProps } = useTimeField(
84
86
  {
85
87
  ...restProps,
@@ -88,36 +90,36 @@ const TimeFieldComponent = ({
88
90
  'aria-describedby': descriptionId,
89
91
  },
90
92
  state,
91
- inputRef,
93
+ internalRef,
92
94
  )
95
+ const firstEditableIndex = state.segments.findIndex((s) => s.isEditable)
96
+
93
97
  return (
94
98
  <div className={classNameOverride}>
95
99
  <Label disabled={state.isDisabled} {...labelProps} classNameOverride={styles.label}>
96
100
  {label}
97
101
  </Label>
98
102
  <div className={styles.wrapper}>
99
- {}
100
103
  <div
101
104
  {...fieldProps}
102
105
  id={id}
103
- ref={inputRef}
106
+ ref={internalRef}
104
107
  className={classnames(
105
108
  styles.input,
106
109
  state.isDisabled && styles.isDisabled,
107
110
  status === 'error' && styles.error,
108
111
  )}
109
112
  >
110
- {state.segments.map((segment, i) => {
111
- return (
112
- <TimeSegment
113
- key={i}
114
- segment={segment}
115
- state={state}
116
- hasPadding={![8294, 8297].includes(segment.text.charCodeAt(0))}
117
- // ^react-aria includes these characters to ensure correct RTL behaviour, but we want to avoid these adding random spacing
118
- />
119
- )
120
- })}
113
+ {state.segments.map((segment, i) => (
114
+ <TimeSegment
115
+ key={i}
116
+ segment={segment}
117
+ state={state}
118
+ inputRef={i === firstEditableIndex ? inputRef : undefined}
119
+ hasPadding={![8294, 8297].includes(segment.text.charCodeAt(0))}
120
+ // ^react-aria includes these characters to ensure correct RTL behaviour, but we want to avoid these adding random spacing
121
+ />
122
+ ))}
121
123
  <div className={styles.focusRing} />
122
124
  </div>
123
125
  </div>
@@ -1,5 +1,6 @@
1
1
  import React from 'react'
2
2
  import { useDateSegment } from '@react-aria/datepicker'
3
+ import { mergeRefs } from '@react-aria/utils'
3
4
  import { type DateFieldState, type DateSegment } from '@react-stately/datepicker'
4
5
  import classnames from 'classnames'
5
6
  import { generateSegmentDisplayText } from './utils/generateSegmentDisplayText'
@@ -9,15 +10,17 @@ export type TimeSegmentProps = {
9
10
  segment: DateSegment
10
11
  state: DateFieldState
11
12
  hasPadding?: boolean
13
+ inputRef?: React.Ref<HTMLSpanElement>
12
14
  }
13
15
 
14
16
  export const TimeSegment = ({
15
17
  segment,
16
18
  state,
17
19
  hasPadding = true,
20
+ inputRef,
18
21
  }: TimeSegmentProps): JSX.Element => {
19
- const ref = React.useRef<HTMLDivElement>(null)
20
- const { segmentProps } = useDateSegment(segment, state, ref)
22
+ const internalRef = React.useRef<HTMLSpanElement>(null)
23
+ const { segmentProps } = useDateSegment(segment, state, internalRef)
21
24
 
22
25
  // Chrome has a bug where `contenteditable` elements receive focus from external clicks.
23
26
  // This (in combination with the invisible character &#8203;) creates boundaries
@@ -28,7 +31,7 @@ export const TimeSegment = ({
28
31
  &#8203;
29
32
  <span
30
33
  {...segmentProps}
31
- ref={ref}
34
+ ref={mergeRefs<HTMLSpanElement>(internalRef, inputRef)}
32
35
  className={classnames(
33
36
  styles.timeSegment,
34
37
  segment.type === 'literal' && styles.literal,