@patternfly/chatbot 6.3.0-prerelease.15 → 6.3.0-prerelease.16

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.
@@ -1,14 +1,14 @@
1
- import type { HTMLProps } from 'react';
1
+ import { HTMLProps, ReactNode } from 'react';
2
2
  export interface MessageBoxProps extends HTMLProps<HTMLDivElement> {
3
3
  /** Content that can be announced, such as a new message, for screen readers */
4
4
  announcement?: string;
5
5
  /** Custom aria-label for scrollable portion of message box */
6
6
  ariaLabel?: string;
7
7
  /** Content to be displayed in the message box */
8
- children: React.ReactNode;
8
+ children: ReactNode;
9
9
  /** Custom classname for the MessageBox component */
10
10
  className?: string;
11
- /** Ref applied to message box */
11
+ /** @deprecated innerRef has been deprecated. Use ref instead. Ref applied to message box */
12
12
  innerRef?: React.Ref<HTMLDivElement>;
13
13
  /** Modifier that controls how content in MessageBox is positioned within the container */
14
14
  position?: 'top' | 'bottom';
@@ -16,6 +16,18 @@ export interface MessageBoxProps extends HTMLProps<HTMLDivElement> {
16
16
  onScrollToTopClick?: () => void;
17
17
  /** Click handler for additional logic for when scroll to bottom jump button is clicked */
18
18
  onScrollToBottomClick?: () => void;
19
+ /** Flag to enable automatic scrolling when new messages are added */
20
+ enableSmartScroll?: boolean;
19
21
  }
20
- export declare const MessageBox: import("react").ForwardRefExoticComponent<Omit<MessageBoxProps, "ref"> & import("react").RefAttributes<any>>;
22
+ export interface MessageBoxHandle extends HTMLDivElement {
23
+ /** Scrolls to the top of the message box */
24
+ scrollToTop: (options?: ScrollOptions) => void;
25
+ /** Scrolls to the bottom of the message box */
26
+ scrollToBottom: (options?: {
27
+ resumeSmartScroll?: boolean;
28
+ } & ScrollOptions) => void;
29
+ /** Returns whether the smart scroll feature is currently active */
30
+ isSmartScrollActive: () => boolean;
31
+ }
32
+ export declare const MessageBox: import("react").ForwardRefExoticComponent<Omit<MessageBoxProps, "ref"> & import("react").RefAttributes<MessageBoxHandle | null>>;
21
33
  export default MessageBox;
@@ -16,29 +16,62 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.MessageBox = void 0;
18
18
  const jsx_runtime_1 = require("react/jsx-runtime");
19
+ // ============================================================================
20
+ // Chatbot Main - Messages
21
+ // ============================================================================
19
22
  const react_1 = require("react");
20
23
  const JumpButton_1 = __importDefault(require("./JumpButton"));
21
- const MessageBoxBase = (_a) => {
22
- var { announcement, ariaLabel = 'Scrollable message log', children, innerRef, className, position = 'top', onScrollToTopClick, onScrollToBottomClick } = _a, props = __rest(_a, ["announcement", "ariaLabel", "children", "innerRef", "className", "position", "onScrollToTopClick", "onScrollToBottomClick"]);
24
+ exports.MessageBox = (0, react_1.forwardRef)((_a, ref) => {
25
+ var { announcement, ariaLabel = 'Scrollable message log', children, className, position = 'top', onScrollToTopClick, onScrollToBottomClick, enableSmartScroll = false } = _a, props = __rest(_a, ["announcement", "ariaLabel", "children", "className", "position", "onScrollToTopClick", "onScrollToBottomClick", "enableSmartScroll"]);
23
26
  const [atTop, setAtTop] = (0, react_1.useState)(false);
24
27
  const [atBottom, setAtBottom] = (0, react_1.useState)(true);
25
28
  const [isOverflowing, setIsOverflowing] = (0, react_1.useState)(false);
26
- const defaultRef = (0, react_1.useRef)(null);
27
- let messageBoxRef;
28
- if (innerRef) {
29
- messageBoxRef = innerRef;
30
- }
31
- else {
32
- messageBoxRef = defaultRef;
33
- }
29
+ const [autoScroll, setAutoScroll] = (0, react_1.useState)(true);
30
+ const lastScrollTop = (0, react_1.useRef)(0);
31
+ const animationFrame = (0, react_1.useRef)(null);
32
+ const debounceTimeout = (0, react_1.useRef)(null);
33
+ const pauseAutoScrollRef = (0, react_1.useRef)(false);
34
+ const messageBoxRef = (0, react_1.useRef)(null);
35
+ const scrollQueued = (0, react_1.useRef)(false);
36
+ const resetUserScrollIntentTimeout = (0, react_1.useRef)();
34
37
  // Configure handlers
35
38
  const handleScroll = (0, react_1.useCallback)(() => {
36
39
  const element = messageBoxRef.current;
37
- if (element) {
38
- const { scrollTop, scrollHeight, clientHeight } = element;
39
- setAtTop(scrollTop === 0);
40
- setAtBottom(Math.round(scrollTop) + Math.round(clientHeight) >= Math.round(scrollHeight) - 1); // rounding means it could be within a pixel of the bottom
40
+ if (!element) {
41
+ return;
42
+ }
43
+ const { scrollTop, scrollHeight, clientHeight } = element;
44
+ const roundedScrollTop = Math.round(scrollTop);
45
+ const roundedClientHeight = Math.round(clientHeight);
46
+ const roundedScrollHeight = Math.round(scrollHeight);
47
+ const distanceFromBottom = roundedScrollHeight - roundedScrollTop - roundedClientHeight;
48
+ const isScrollingDown = roundedScrollTop > lastScrollTop.current;
49
+ const DELTA_UP = 10;
50
+ const DELTA_DOWN = 50;
51
+ const DEBOUNCE_DELAY = 200;
52
+ const delta = isScrollingDown ? DELTA_DOWN : DELTA_UP;
53
+ const isAtBottom = distanceFromBottom <= delta;
54
+ setAtTop(roundedScrollTop === 0);
55
+ setAtBottom(roundedScrollTop + roundedClientHeight >= roundedScrollHeight - 1); // rounding means it could be within a pixel of the bottom
56
+ if (!enableSmartScroll || scrollQueued.current) {
57
+ return;
58
+ }
59
+ if (roundedScrollTop === 0) {
60
+ pauseAutoScrollRef.current = false;
61
+ }
62
+ if (debounceTimeout.current) {
63
+ clearTimeout(debounceTimeout.current);
64
+ }
65
+ if (!isAtBottom && !pauseAutoScrollRef.current) {
66
+ setAutoScroll(false);
41
67
  }
68
+ // User is near bottom and scrolling down - debounce re-enabling auto-scroll
69
+ if (isAtBottom && isScrollingDown && !pauseAutoScrollRef.current) {
70
+ debounceTimeout.current = setTimeout(() => {
71
+ setAutoScroll(true);
72
+ }, DEBOUNCE_DELAY);
73
+ }
74
+ lastScrollTop.current = roundedScrollTop;
42
75
  }, [messageBoxRef]);
43
76
  const checkOverflow = (0, react_1.useCallback)(() => {
44
77
  const element = messageBoxRef.current;
@@ -47,35 +80,132 @@ const MessageBoxBase = (_a) => {
47
80
  setIsOverflowing(scrollHeight >= clientHeight);
48
81
  }
49
82
  }, [messageBoxRef]);
50
- const scrollToTop = (0, react_1.useCallback)(() => {
83
+ const resumeAutoScroll = (0, react_1.useCallback)(() => {
84
+ if (!enableSmartScroll) {
85
+ return;
86
+ }
87
+ pauseAutoScrollRef.current = false;
88
+ setAutoScroll(true);
89
+ }, [enableSmartScroll]);
90
+ const pauseAutoScroll = (0, react_1.useCallback)(() => {
91
+ if (!enableSmartScroll) {
92
+ return;
93
+ }
94
+ pauseAutoScrollRef.current = true;
95
+ setAutoScroll(false);
96
+ }, [enableSmartScroll]);
97
+ /**
98
+ * Scrolls to the top of the message box.
99
+ *
100
+ */
101
+ const scrollToTop = (0, react_1.useCallback)((options) => {
102
+ const { behavior = 'smooth' } = options || {};
51
103
  const element = messageBoxRef.current;
52
- if (element) {
53
- element.scrollTo({ top: 0, behavior: 'smooth' });
104
+ if (!element || scrollQueued.current) {
105
+ return;
106
+ }
107
+ scrollQueued.current = true;
108
+ pauseAutoScroll();
109
+ if (animationFrame.current) {
110
+ cancelAnimationFrame(animationFrame.current);
111
+ animationFrame.current = null;
54
112
  }
113
+ animationFrame.current = requestAnimationFrame(() => {
114
+ element.scrollTo({ top: 0, behavior });
115
+ scrollQueued.current = false;
116
+ });
55
117
  onScrollToTopClick && onScrollToTopClick();
56
118
  }, [messageBoxRef]);
57
- const scrollToBottom = (0, react_1.useCallback)(() => {
119
+ /**
120
+ * Scrolls to the bottom of the message box.
121
+ *
122
+ * @param options.resumeSmartScroll - If true, resumes smart scroll behavior;
123
+ * if false or omitted, scrolls without resuming auto-scroll.
124
+ * @param options.scrollOptions - Additional scroll options. behavior can be 'smooth' or 'auto'.
125
+ */
126
+ const scrollToBottom = (0, react_1.useCallback)((options) => {
127
+ const { behavior = 'smooth', resumeSmartScroll = false } = options || {};
128
+ resumeSmartScroll && resumeAutoScroll();
58
129
  const element = messageBoxRef.current;
59
- if (element) {
60
- element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' });
130
+ if (!element || pauseAutoScrollRef.current || scrollQueued.current) {
131
+ return;
132
+ }
133
+ scrollQueued.current = true;
134
+ if (animationFrame.current) {
135
+ cancelAnimationFrame(animationFrame.current);
61
136
  }
137
+ animationFrame.current = requestAnimationFrame(() => {
138
+ element.scrollTo({ top: element.scrollHeight, behavior });
139
+ resumeAutoScroll();
140
+ scrollQueued.current = false;
141
+ });
62
142
  onScrollToBottomClick && onScrollToBottomClick();
63
- }, [messageBoxRef]);
143
+ }, [messageBoxRef, enableSmartScroll]);
64
144
  // Detect scroll position
65
145
  (0, react_1.useEffect)(() => {
66
146
  const element = messageBoxRef.current;
67
- if (element) {
68
- // Listen for scroll events
69
- element.addEventListener('scroll', handleScroll);
70
- // Check initial position and overflow
71
- handleScroll();
72
- checkOverflow();
73
- return () => {
74
- element.removeEventListener('scroll', handleScroll);
75
- };
147
+ if (!element) {
148
+ return;
76
149
  }
150
+ // Listen for scroll events
151
+ element.addEventListener('scroll', handleScroll);
152
+ // Check initial position and overflow
153
+ handleScroll();
154
+ checkOverflow();
155
+ return () => {
156
+ element.removeEventListener('scroll', handleScroll);
157
+ };
77
158
  }, [checkOverflow, handleScroll, messageBoxRef]);
78
- return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(JumpButton_1.default, { position: "top", isHidden: isOverflowing && atTop, onClick: scrollToTop }), (0, jsx_runtime_1.jsxs)("div", Object.assign({ role: "region", tabIndex: 0, "aria-label": ariaLabel, className: `pf-chatbot__messagebox ${position === 'bottom' && 'pf-chatbot__messagebox--bottom'} ${className !== null && className !== void 0 ? className : ''}`, ref: innerRef !== null && innerRef !== void 0 ? innerRef : messageBoxRef }, props, { children: [children, (0, jsx_runtime_1.jsx)("div", { className: "pf-chatbot__messagebox-announcement", "aria-live": "polite", children: announcement })] })), (0, jsx_runtime_1.jsx)(JumpButton_1.default, { position: "bottom", isHidden: isOverflowing && atBottom, onClick: scrollToBottom })] }));
79
- };
80
- exports.MessageBox = (0, react_1.forwardRef)((props, ref) => ((0, jsx_runtime_1.jsx)(MessageBoxBase, Object.assign({ innerRef: ref }, props))));
159
+ (0, react_1.useImperativeHandle)(ref, () => {
160
+ const node = messageBoxRef.current;
161
+ // Attach custom methods to the element
162
+ node.scrollToTop = scrollToTop;
163
+ node.scrollToBottom = scrollToBottom;
164
+ node.isSmartScrollActive = () => enableSmartScroll && autoScroll;
165
+ return node;
166
+ });
167
+ let lastTouchY = null;
168
+ const onTouchEnd = (event) => {
169
+ lastTouchY = null;
170
+ props.onTouchEnd && props.onTouchEnd(event);
171
+ };
172
+ const handleUserScroll = (isScrollingDown) => {
173
+ const container = messageBoxRef.current;
174
+ if (!enableSmartScroll || !container) {
175
+ return;
176
+ }
177
+ if (!isScrollingDown) {
178
+ pauseAutoScrollRef.current = true;
179
+ clearTimeout(resetUserScrollIntentTimeout.current);
180
+ return;
181
+ }
182
+ const { scrollTop, scrollHeight, clientHeight } = container;
183
+ const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
184
+ if (distanceFromBottom < 100) {
185
+ pauseAutoScrollRef.current = false;
186
+ setAutoScroll(true);
187
+ }
188
+ };
189
+ const onWheel = (event) => {
190
+ const isScrollingDown = event.deltaY > 0;
191
+ handleUserScroll(isScrollingDown);
192
+ props.onWheel && props.onWheel(event);
193
+ };
194
+ const onTouchMove = (event) => {
195
+ const currentTouchY = event.touches[0].clientY;
196
+ let isScrollingDown = false;
197
+ if (lastTouchY !== null) {
198
+ isScrollingDown = currentTouchY < lastTouchY;
199
+ }
200
+ lastTouchY = currentTouchY;
201
+ handleUserScroll(isScrollingDown);
202
+ props.onTouchMove && props.onTouchMove(event);
203
+ };
204
+ const smartScrollHandlers = {
205
+ onWheel,
206
+ onTouchMove,
207
+ onTouchEnd
208
+ };
209
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(JumpButton_1.default, { position: "top", isHidden: isOverflowing && atTop, onClick: scrollToTop }), (0, jsx_runtime_1.jsxs)("div", Object.assign({ role: "region", tabIndex: 0, "aria-label": ariaLabel, className: `pf-chatbot__messagebox ${position === 'bottom' && 'pf-chatbot__messagebox--bottom'} ${className !== null && className !== void 0 ? className : ''}`, ref: messageBoxRef }, props, (enableSmartScroll ? Object.assign({}, smartScrollHandlers) : {}), { children: [children, (0, jsx_runtime_1.jsx)("div", { className: "pf-chatbot__messagebox-announcement", "aria-live": "polite", children: announcement })] })), (0, jsx_runtime_1.jsx)(JumpButton_1.default, { position: "bottom", isHidden: isOverflowing && atBottom, onClick: () => scrollToBottom({ resumeSmartScroll: true }) })] }));
210
+ });
81
211
  exports.default = exports.MessageBox;
@@ -18,17 +18,33 @@ const react_2 = require("@testing-library/react");
18
18
  const MessageBox_1 = require("./MessageBox");
19
19
  const user_event_1 = __importDefault(require("@testing-library/user-event"));
20
20
  describe('MessageBox', () => {
21
+ beforeEach(() => {
22
+ jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
23
+ cb(0); // Immediately call the callback
24
+ return 0;
25
+ });
26
+ });
27
+ afterEach(() => {
28
+ jest.restoreAllMocks();
29
+ });
21
30
  it('should render Message box', () => {
22
31
  (0, react_2.render)((0, jsx_runtime_1.jsx)(MessageBox_1.MessageBox, { children: (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: "Chatbot Messages" }) }));
23
32
  expect(react_2.screen.getByText('Chatbot Messages')).toBeTruthy();
24
33
  });
25
34
  it('should assign ref to Message box', () => {
35
+ var _a, _b, _c, _d;
26
36
  const ref = (0, react_1.createRef)();
27
- (0, react_2.render)((0, jsx_runtime_1.jsx)(MessageBox_1.MessageBox, { ref: ref, children: (0, jsx_runtime_1.jsx)("div", { children: "Test message content" }) }));
37
+ (0, react_2.render)((0, jsx_runtime_1.jsx)(MessageBox_1.MessageBox, { "data-testid": "message-box", ref: ref, children: (0, jsx_runtime_1.jsx)("div", { children: "Test message content" }) }));
38
+ react_2.screen.getByText('Test message content');
28
39
  expect(ref.current).not.toBeNull();
40
+ // should contain custom methods exposed by the ref
41
+ expect(typeof ((_a = ref.current) === null || _a === void 0 ? void 0 : _a.isSmartScrollActive)).toBe('function');
42
+ expect(typeof ((_b = ref.current) === null || _b === void 0 ? void 0 : _b.scrollToTop)).toBe('function');
43
+ expect(typeof ((_c = ref.current) === null || _c === void 0 ? void 0 : _c.scrollToBottom)).toBe('function');
44
+ expect((_d = ref.current) === null || _d === void 0 ? void 0 : _d.isSmartScrollActive()).toBe(false);
29
45
  expect(ref.current).toBeInstanceOf(HTMLDivElement);
30
46
  });
31
- it('should call onScrollToBottomClick when scroll to top button is clicked', () => __awaiter(void 0, void 0, void 0, function* () {
47
+ it('should call onScrollToBottomClick when scroll to bottom button is clicked', () => __awaiter(void 0, void 0, void 0, function* () {
32
48
  const spy = jest.fn();
33
49
  (0, react_2.render)((0, jsx_runtime_1.jsx)(MessageBox_1.MessageBox, { onScrollToBottomClick: spy, children: (0, jsx_runtime_1.jsx)("div", { children: "Test message content" }) }));
34
50
  // this forces button to show
@@ -36,7 +52,9 @@ describe('MessageBox', () => {
36
52
  Object.defineProperty(region, 'scrollHeight', { configurable: true, value: 1000 });
37
53
  Object.defineProperty(region, 'clientHeight', { configurable: true, value: 500 });
38
54
  Object.defineProperty(region, 'scrollTop', { configurable: true, value: 0 });
39
- region.dispatchEvent(new Event('scroll'));
55
+ (0, react_2.act)(() => {
56
+ region.dispatchEvent(new Event('scroll'));
57
+ });
40
58
  yield (0, react_2.waitFor)(() => {
41
59
  user_event_1.default.click(react_2.screen.getByRole('button', { name: /Jump bottom/i }));
42
60
  expect(spy).toHaveBeenCalled();
@@ -53,10 +71,165 @@ describe('MessageBox', () => {
53
71
  configurable: true,
54
72
  value: 500
55
73
  });
56
- region.dispatchEvent(new Event('scroll'));
74
+ (0, react_2.act)(() => {
75
+ region.dispatchEvent(new Event('scroll'));
76
+ });
57
77
  yield (0, react_2.waitFor)(() => {
58
78
  user_event_1.default.click(react_2.screen.getByRole('button', { name: /Jump top/i }));
59
79
  expect(spy).toHaveBeenCalled();
60
80
  });
61
81
  }));
82
+ it('should call user defined onWheel, onTouchMove and onTouchEnd handlers', () => __awaiter(void 0, void 0, void 0, function* () {
83
+ const ref = (0, react_1.createRef)();
84
+ const onWheel = jest.fn();
85
+ const onTouchMove = jest.fn();
86
+ const onTouchEnd = jest.fn();
87
+ (0, react_2.render)((0, jsx_runtime_1.jsx)(MessageBox_1.MessageBox, { ref: ref, enableSmartScroll: true, onWheel: onWheel, onTouchMove: onTouchMove, onTouchEnd: onTouchEnd, children: (0, jsx_runtime_1.jsx)("div", { children: "Test message content" }) }));
88
+ const element = ref.current;
89
+ (0, react_2.act)(() => {
90
+ react_2.fireEvent.wheel(element, { deltaY: 10 });
91
+ react_2.fireEvent.touchMove(element, { touches: [{ clientY: 700 }] });
92
+ react_2.fireEvent.touchEnd(element);
93
+ });
94
+ expect(onWheel).toHaveBeenCalled();
95
+ expect(onTouchMove).toHaveBeenCalled();
96
+ expect(onTouchEnd).toHaveBeenCalled();
97
+ }));
98
+ it('should scroll to the bottom when the method is called ', () => __awaiter(void 0, void 0, void 0, function* () {
99
+ var _a;
100
+ const ref = (0, react_1.createRef)();
101
+ (0, react_2.render)((0, jsx_runtime_1.jsx)(MessageBox_1.MessageBox, { ref: ref, enableSmartScroll: true, children: (0, jsx_runtime_1.jsx)("div", { children: "Test message content" }) }));
102
+ const element = ref.current;
103
+ const scrollSpy = jest.spyOn(element, 'scrollTo');
104
+ (0, react_2.act)(() => {
105
+ var _a, _b, _c;
106
+ (_a = ref.current) === null || _a === void 0 ? void 0 : _a.scrollToBottom();
107
+ (_b = ref.current) === null || _b === void 0 ? void 0 : _b.scrollToBottom();
108
+ (_c = ref.current) === null || _c === void 0 ? void 0 : _c.scrollToBottom();
109
+ });
110
+ expect(scrollSpy).toHaveBeenCalledWith({ top: element.scrollHeight, behavior: 'smooth' });
111
+ expect((_a = ref.current) === null || _a === void 0 ? void 0 : _a.isSmartScrollActive()).toBe(true);
112
+ }));
113
+ it('should scroll to the top when the method is called ', () => __awaiter(void 0, void 0, void 0, function* () {
114
+ var _a;
115
+ const ref = (0, react_1.createRef)();
116
+ (0, react_2.render)((0, jsx_runtime_1.jsx)(MessageBox_1.MessageBox, { ref: ref, enableSmartScroll: true, children: (0, jsx_runtime_1.jsx)("div", { children: "Test message content" }) }));
117
+ const element = ref.current;
118
+ const scrollSpy = jest.spyOn(element, 'scrollTo');
119
+ (0, react_2.act)(() => {
120
+ var _a;
121
+ (_a = ref.current) === null || _a === void 0 ? void 0 : _a.scrollToTop();
122
+ });
123
+ expect(scrollSpy).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
124
+ expect((_a = ref.current) === null || _a === void 0 ? void 0 : _a.isSmartScrollActive()).toBe(false);
125
+ }));
126
+ it('should pause automatic scrolling when user scrolls up ', () => __awaiter(void 0, void 0, void 0, function* () {
127
+ var _a, _b;
128
+ const ref = (0, react_1.createRef)();
129
+ (0, react_2.render)((0, jsx_runtime_1.jsx)(MessageBox_1.MessageBox, { ref: ref, enableSmartScroll: true, children: (0, jsx_runtime_1.jsx)("div", { children: "Test message content" }) }));
130
+ expect((_a = ref.current) === null || _a === void 0 ? void 0 : _a.isSmartScrollActive()).toBe(true);
131
+ const element = ref.current;
132
+ // Manually set scrollHeight and clientHeight for calculations
133
+ Object.defineProperty(element, 'scrollHeight', { configurable: true, value: 1000 });
134
+ Object.defineProperty(element, 'clientHeight', { configurable: true, value: 300 });
135
+ // Simulate scroll up by changing scrollTop
136
+ element.scrollTop = 200;
137
+ const scrollEvent = new Event('scroll', { bubbles: true });
138
+ (0, react_2.act)(() => {
139
+ element.dispatchEvent(scrollEvent);
140
+ });
141
+ expect((_b = ref.current) === null || _b === void 0 ? void 0 : _b.isSmartScrollActive()).toBe(false);
142
+ }));
143
+ it('should resume automatic scrolling when user scrolls down to the bottom using scroll event', () => __awaiter(void 0, void 0, void 0, function* () {
144
+ var _a, _b;
145
+ jest.useFakeTimers();
146
+ const ref = (0, react_1.createRef)();
147
+ (0, react_2.render)((0, jsx_runtime_1.jsx)(MessageBox_1.MessageBox, { ref: ref, enableSmartScroll: true, children: (0, jsx_runtime_1.jsx)("div", { children: "Test message content" }) }));
148
+ const element = ref.current;
149
+ // Manually set scrollHeight and clientHeight for calculations
150
+ Object.defineProperty(element, 'scrollHeight', { configurable: true, value: 1000 });
151
+ Object.defineProperty(element, 'clientHeight', { configurable: true, value: 300 });
152
+ // Simulate scroll up by changing scrollTop
153
+ element.scrollTop = 100;
154
+ const scrollEvent = new Event('scroll', { bubbles: true });
155
+ (0, react_2.act)(() => {
156
+ element.dispatchEvent(scrollEvent);
157
+ });
158
+ expect((_a = ref.current) === null || _a === void 0 ? void 0 : _a.isSmartScrollActive()).toBe(false);
159
+ (0, react_2.act)(() => {
160
+ // Simulate scroll down by changing scrollTop
161
+ element.scrollTop = 650; // scrollHeight - scrollTop - clientHeight - DELTA_DOWN (50) = 0
162
+ const scrollEvent = new Event('scroll', { bubbles: true });
163
+ element.dispatchEvent(scrollEvent);
164
+ jest.advanceTimersByTime(250);
165
+ });
166
+ expect((_b = ref.current) === null || _b === void 0 ? void 0 : _b.isSmartScrollActive()).toBe(true);
167
+ jest.useRealTimers();
168
+ }));
169
+ it('should resume automatic scrolling when scrollToBottom method is used', () => __awaiter(void 0, void 0, void 0, function* () {
170
+ var _a, _b;
171
+ const ref = (0, react_1.createRef)();
172
+ (0, react_2.render)((0, jsx_runtime_1.jsx)(MessageBox_1.MessageBox, { ref: ref, enableSmartScroll: true, children: (0, jsx_runtime_1.jsx)("div", { children: "Test message content" }) }));
173
+ const element = ref.current;
174
+ // Manually set scrollHeight and clientHeight for calculations
175
+ Object.defineProperty(element, 'scrollHeight', { configurable: true, value: 1000 });
176
+ Object.defineProperty(element, 'clientHeight', { configurable: true, value: 300 });
177
+ // Simulate scroll up by changing scrollTop
178
+ element.scrollTop = 100;
179
+ const scrollEvent = new Event('scroll', { bubbles: true });
180
+ (0, react_2.act)(() => {
181
+ element.dispatchEvent(scrollEvent);
182
+ });
183
+ expect((_a = ref.current) === null || _a === void 0 ? void 0 : _a.isSmartScrollActive()).toBe(false);
184
+ (0, react_2.act)(() => {
185
+ var _a;
186
+ (_a = ref.current) === null || _a === void 0 ? void 0 : _a.scrollToBottom({ resumeSmartScroll: true, behavior: 'auto' }); // resumes auto scroll and scrolls to bottom.
187
+ });
188
+ expect((_b = ref.current) === null || _b === void 0 ? void 0 : _b.isSmartScrollActive()).toBe(true);
189
+ }));
190
+ it('should resume automatic scrolling when mouse wheel event is used', () => __awaiter(void 0, void 0, void 0, function* () {
191
+ var _a, _b;
192
+ const ref = (0, react_1.createRef)();
193
+ (0, react_2.render)((0, jsx_runtime_1.jsx)(MessageBox_1.MessageBox, { ref: ref, enableSmartScroll: true, children: (0, jsx_runtime_1.jsx)("div", { children: "Test message content" }) }));
194
+ const element = ref.current;
195
+ // Manually set scrollHeight and clientHeight for calculations
196
+ Object.defineProperty(element, 'scrollHeight', { configurable: true, value: 1000 });
197
+ Object.defineProperty(element, 'clientHeight', { configurable: true, value: 300 });
198
+ Object.defineProperty(element, 'scrollTop', { configurable: true, value: 350 });
199
+ const scrollEvent = new Event('scroll', { bubbles: true });
200
+ (0, react_2.act)(() => {
201
+ element.dispatchEvent(scrollEvent);
202
+ });
203
+ expect((_a = ref.current) === null || _a === void 0 ? void 0 : _a.isSmartScrollActive()).toBe(false);
204
+ // Simulate mouse wheel event
205
+ (0, react_2.act)(() => {
206
+ Object.defineProperty(element, 'scrollTop', { configurable: true, value: 650 });
207
+ react_2.fireEvent.wheel(element, { deltaY: 10 });
208
+ });
209
+ expect((_b = ref.current) === null || _b === void 0 ? void 0 : _b.isSmartScrollActive()).toBe(true);
210
+ }));
211
+ it('should resume automatic scrolling when user swipes up in touch screen', () => __awaiter(void 0, void 0, void 0, function* () {
212
+ var _a, _b;
213
+ const ref = (0, react_1.createRef)();
214
+ (0, react_2.render)((0, jsx_runtime_1.jsx)(MessageBox_1.MessageBox, { ref: ref, enableSmartScroll: true, children: (0, jsx_runtime_1.jsx)("div", { children: "Test message content" }) }));
215
+ const element = ref.current;
216
+ // Manually set scrollHeight and clientHeight for calculations
217
+ Object.defineProperty(element, 'scrollHeight', { configurable: true, value: 1000 });
218
+ Object.defineProperty(element, 'clientHeight', { configurable: true, value: 300 });
219
+ Object.defineProperty(element, 'scrollTop', { configurable: true, value: 350 });
220
+ const scrollEvent = new Event('scroll', { bubbles: true });
221
+ (0, react_2.act)(() => {
222
+ element.dispatchEvent(scrollEvent);
223
+ });
224
+ expect((_a = ref.current) === null || _a === void 0 ? void 0 : _a.isSmartScrollActive()).toBe(false);
225
+ // Simulate touch event - swipe up
226
+ (0, react_2.act)(() => {
227
+ Object.defineProperty(element, 'scrollTop', { configurable: true, value: 650 });
228
+ react_2.fireEvent.touchStart(element, { touches: [{ clientY: 700 }] });
229
+ react_2.fireEvent.touchMove(element, { touches: [{ clientY: 700 }] });
230
+ react_2.fireEvent.touchMove(element, { touches: [{ clientY: 600 }] });
231
+ react_2.fireEvent.touchEnd(element);
232
+ });
233
+ expect((_b = ref.current) === null || _b === void 0 ? void 0 : _b.isSmartScrollActive()).toBe(true);
234
+ }));
62
235
  });
@@ -1,14 +1,14 @@
1
- import type { HTMLProps } from 'react';
1
+ import { HTMLProps, ReactNode } from 'react';
2
2
  export interface MessageBoxProps extends HTMLProps<HTMLDivElement> {
3
3
  /** Content that can be announced, such as a new message, for screen readers */
4
4
  announcement?: string;
5
5
  /** Custom aria-label for scrollable portion of message box */
6
6
  ariaLabel?: string;
7
7
  /** Content to be displayed in the message box */
8
- children: React.ReactNode;
8
+ children: ReactNode;
9
9
  /** Custom classname for the MessageBox component */
10
10
  className?: string;
11
- /** Ref applied to message box */
11
+ /** @deprecated innerRef has been deprecated. Use ref instead. Ref applied to message box */
12
12
  innerRef?: React.Ref<HTMLDivElement>;
13
13
  /** Modifier that controls how content in MessageBox is positioned within the container */
14
14
  position?: 'top' | 'bottom';
@@ -16,6 +16,18 @@ export interface MessageBoxProps extends HTMLProps<HTMLDivElement> {
16
16
  onScrollToTopClick?: () => void;
17
17
  /** Click handler for additional logic for when scroll to bottom jump button is clicked */
18
18
  onScrollToBottomClick?: () => void;
19
+ /** Flag to enable automatic scrolling when new messages are added */
20
+ enableSmartScroll?: boolean;
19
21
  }
20
- export declare const MessageBox: import("react").ForwardRefExoticComponent<Omit<MessageBoxProps, "ref"> & import("react").RefAttributes<any>>;
22
+ export interface MessageBoxHandle extends HTMLDivElement {
23
+ /** Scrolls to the top of the message box */
24
+ scrollToTop: (options?: ScrollOptions) => void;
25
+ /** Scrolls to the bottom of the message box */
26
+ scrollToBottom: (options?: {
27
+ resumeSmartScroll?: boolean;
28
+ } & ScrollOptions) => void;
29
+ /** Returns whether the smart scroll feature is currently active */
30
+ isSmartScrollActive: () => boolean;
31
+ }
32
+ export declare const MessageBox: import("react").ForwardRefExoticComponent<Omit<MessageBoxProps, "ref"> & import("react").RefAttributes<MessageBoxHandle | null>>;
21
33
  export default MessageBox;