@patternfly/chatbot 6.3.0-prerelease.14 → 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.
- package/dist/cjs/ChatbotHeader/ChatbotHeaderSelectorDropdown.d.ts +2 -0
- package/dist/cjs/ChatbotHeader/ChatbotHeaderSelectorDropdown.js +4 -2
- package/dist/cjs/MessageBox/MessageBox.d.ts +16 -4
- package/dist/cjs/MessageBox/MessageBox.js +163 -33
- package/dist/cjs/MessageBox/MessageBox.test.js +177 -4
- package/dist/esm/ChatbotHeader/ChatbotHeaderSelectorDropdown.d.ts +2 -0
- package/dist/esm/ChatbotHeader/ChatbotHeaderSelectorDropdown.js +4 -2
- package/dist/esm/MessageBox/MessageBox.d.ts +16 -4
- package/dist/esm/MessageBox/MessageBox.js +164 -34
- package/dist/esm/MessageBox/MessageBox.test.js +178 -5
- package/package.json +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/demos/AttachmentDemos.md +36 -0
- package/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotScrolling.tsx +536 -0
- package/src/ChatbotHeader/ChatbotHeaderSelectorDropdown.tsx +6 -0
- package/src/MessageBox/MessageBox.test.tsx +254 -8
- package/src/MessageBox/MessageBox.tsx +295 -88
@@ -10,29 +10,62 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
10
10
|
return t;
|
11
11
|
};
|
12
12
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
13
|
-
|
13
|
+
// ============================================================================
|
14
|
+
// Chatbot Main - Messages
|
15
|
+
// ============================================================================
|
16
|
+
import { useState, useRef, useCallback, useEffect, forwardRef, useImperativeHandle } from 'react';
|
14
17
|
import JumpButton from './JumpButton';
|
15
|
-
const
|
16
|
-
var { announcement, ariaLabel = 'Scrollable message log', children,
|
18
|
+
export const MessageBox = forwardRef((_a, ref) => {
|
19
|
+
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"]);
|
17
20
|
const [atTop, setAtTop] = useState(false);
|
18
21
|
const [atBottom, setAtBottom] = useState(true);
|
19
22
|
const [isOverflowing, setIsOverflowing] = useState(false);
|
20
|
-
const
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
23
|
+
const [autoScroll, setAutoScroll] = useState(true);
|
24
|
+
const lastScrollTop = useRef(0);
|
25
|
+
const animationFrame = useRef(null);
|
26
|
+
const debounceTimeout = useRef(null);
|
27
|
+
const pauseAutoScrollRef = useRef(false);
|
28
|
+
const messageBoxRef = useRef(null);
|
29
|
+
const scrollQueued = useRef(false);
|
30
|
+
const resetUserScrollIntentTimeout = useRef();
|
28
31
|
// Configure handlers
|
29
32
|
const handleScroll = useCallback(() => {
|
30
33
|
const element = messageBoxRef.current;
|
31
|
-
if (element) {
|
32
|
-
|
33
|
-
|
34
|
-
|
34
|
+
if (!element) {
|
35
|
+
return;
|
36
|
+
}
|
37
|
+
const { scrollTop, scrollHeight, clientHeight } = element;
|
38
|
+
const roundedScrollTop = Math.round(scrollTop);
|
39
|
+
const roundedClientHeight = Math.round(clientHeight);
|
40
|
+
const roundedScrollHeight = Math.round(scrollHeight);
|
41
|
+
const distanceFromBottom = roundedScrollHeight - roundedScrollTop - roundedClientHeight;
|
42
|
+
const isScrollingDown = roundedScrollTop > lastScrollTop.current;
|
43
|
+
const DELTA_UP = 10;
|
44
|
+
const DELTA_DOWN = 50;
|
45
|
+
const DEBOUNCE_DELAY = 200;
|
46
|
+
const delta = isScrollingDown ? DELTA_DOWN : DELTA_UP;
|
47
|
+
const isAtBottom = distanceFromBottom <= delta;
|
48
|
+
setAtTop(roundedScrollTop === 0);
|
49
|
+
setAtBottom(roundedScrollTop + roundedClientHeight >= roundedScrollHeight - 1); // rounding means it could be within a pixel of the bottom
|
50
|
+
if (!enableSmartScroll || scrollQueued.current) {
|
51
|
+
return;
|
52
|
+
}
|
53
|
+
if (roundedScrollTop === 0) {
|
54
|
+
pauseAutoScrollRef.current = false;
|
55
|
+
}
|
56
|
+
if (debounceTimeout.current) {
|
57
|
+
clearTimeout(debounceTimeout.current);
|
58
|
+
}
|
59
|
+
if (!isAtBottom && !pauseAutoScrollRef.current) {
|
60
|
+
setAutoScroll(false);
|
35
61
|
}
|
62
|
+
// User is near bottom and scrolling down - debounce re-enabling auto-scroll
|
63
|
+
if (isAtBottom && isScrollingDown && !pauseAutoScrollRef.current) {
|
64
|
+
debounceTimeout.current = setTimeout(() => {
|
65
|
+
setAutoScroll(true);
|
66
|
+
}, DEBOUNCE_DELAY);
|
67
|
+
}
|
68
|
+
lastScrollTop.current = roundedScrollTop;
|
36
69
|
}, [messageBoxRef]);
|
37
70
|
const checkOverflow = useCallback(() => {
|
38
71
|
const element = messageBoxRef.current;
|
@@ -41,35 +74,132 @@ const MessageBoxBase = (_a) => {
|
|
41
74
|
setIsOverflowing(scrollHeight >= clientHeight);
|
42
75
|
}
|
43
76
|
}, [messageBoxRef]);
|
44
|
-
const
|
77
|
+
const resumeAutoScroll = useCallback(() => {
|
78
|
+
if (!enableSmartScroll) {
|
79
|
+
return;
|
80
|
+
}
|
81
|
+
pauseAutoScrollRef.current = false;
|
82
|
+
setAutoScroll(true);
|
83
|
+
}, [enableSmartScroll]);
|
84
|
+
const pauseAutoScroll = useCallback(() => {
|
85
|
+
if (!enableSmartScroll) {
|
86
|
+
return;
|
87
|
+
}
|
88
|
+
pauseAutoScrollRef.current = true;
|
89
|
+
setAutoScroll(false);
|
90
|
+
}, [enableSmartScroll]);
|
91
|
+
/**
|
92
|
+
* Scrolls to the top of the message box.
|
93
|
+
*
|
94
|
+
*/
|
95
|
+
const scrollToTop = useCallback((options) => {
|
96
|
+
const { behavior = 'smooth' } = options || {};
|
45
97
|
const element = messageBoxRef.current;
|
46
|
-
if (element) {
|
47
|
-
|
98
|
+
if (!element || scrollQueued.current) {
|
99
|
+
return;
|
100
|
+
}
|
101
|
+
scrollQueued.current = true;
|
102
|
+
pauseAutoScroll();
|
103
|
+
if (animationFrame.current) {
|
104
|
+
cancelAnimationFrame(animationFrame.current);
|
105
|
+
animationFrame.current = null;
|
48
106
|
}
|
107
|
+
animationFrame.current = requestAnimationFrame(() => {
|
108
|
+
element.scrollTo({ top: 0, behavior });
|
109
|
+
scrollQueued.current = false;
|
110
|
+
});
|
49
111
|
onScrollToTopClick && onScrollToTopClick();
|
50
112
|
}, [messageBoxRef]);
|
51
|
-
|
113
|
+
/**
|
114
|
+
* Scrolls to the bottom of the message box.
|
115
|
+
*
|
116
|
+
* @param options.resumeSmartScroll - If true, resumes smart scroll behavior;
|
117
|
+
* if false or omitted, scrolls without resuming auto-scroll.
|
118
|
+
* @param options.scrollOptions - Additional scroll options. behavior can be 'smooth' or 'auto'.
|
119
|
+
*/
|
120
|
+
const scrollToBottom = useCallback((options) => {
|
121
|
+
const { behavior = 'smooth', resumeSmartScroll = false } = options || {};
|
122
|
+
resumeSmartScroll && resumeAutoScroll();
|
52
123
|
const element = messageBoxRef.current;
|
53
|
-
if (element) {
|
54
|
-
|
124
|
+
if (!element || pauseAutoScrollRef.current || scrollQueued.current) {
|
125
|
+
return;
|
126
|
+
}
|
127
|
+
scrollQueued.current = true;
|
128
|
+
if (animationFrame.current) {
|
129
|
+
cancelAnimationFrame(animationFrame.current);
|
55
130
|
}
|
131
|
+
animationFrame.current = requestAnimationFrame(() => {
|
132
|
+
element.scrollTo({ top: element.scrollHeight, behavior });
|
133
|
+
resumeAutoScroll();
|
134
|
+
scrollQueued.current = false;
|
135
|
+
});
|
56
136
|
onScrollToBottomClick && onScrollToBottomClick();
|
57
|
-
}, [messageBoxRef]);
|
137
|
+
}, [messageBoxRef, enableSmartScroll]);
|
58
138
|
// Detect scroll position
|
59
139
|
useEffect(() => {
|
60
140
|
const element = messageBoxRef.current;
|
61
|
-
if (element) {
|
62
|
-
|
63
|
-
element.addEventListener('scroll', handleScroll);
|
64
|
-
// Check initial position and overflow
|
65
|
-
handleScroll();
|
66
|
-
checkOverflow();
|
67
|
-
return () => {
|
68
|
-
element.removeEventListener('scroll', handleScroll);
|
69
|
-
};
|
141
|
+
if (!element) {
|
142
|
+
return;
|
70
143
|
}
|
144
|
+
// Listen for scroll events
|
145
|
+
element.addEventListener('scroll', handleScroll);
|
146
|
+
// Check initial position and overflow
|
147
|
+
handleScroll();
|
148
|
+
checkOverflow();
|
149
|
+
return () => {
|
150
|
+
element.removeEventListener('scroll', handleScroll);
|
151
|
+
};
|
71
152
|
}, [checkOverflow, handleScroll, messageBoxRef]);
|
72
|
-
|
73
|
-
|
74
|
-
|
153
|
+
useImperativeHandle(ref, () => {
|
154
|
+
const node = messageBoxRef.current;
|
155
|
+
// Attach custom methods to the element
|
156
|
+
node.scrollToTop = scrollToTop;
|
157
|
+
node.scrollToBottom = scrollToBottom;
|
158
|
+
node.isSmartScrollActive = () => enableSmartScroll && autoScroll;
|
159
|
+
return node;
|
160
|
+
});
|
161
|
+
let lastTouchY = null;
|
162
|
+
const onTouchEnd = (event) => {
|
163
|
+
lastTouchY = null;
|
164
|
+
props.onTouchEnd && props.onTouchEnd(event);
|
165
|
+
};
|
166
|
+
const handleUserScroll = (isScrollingDown) => {
|
167
|
+
const container = messageBoxRef.current;
|
168
|
+
if (!enableSmartScroll || !container) {
|
169
|
+
return;
|
170
|
+
}
|
171
|
+
if (!isScrollingDown) {
|
172
|
+
pauseAutoScrollRef.current = true;
|
173
|
+
clearTimeout(resetUserScrollIntentTimeout.current);
|
174
|
+
return;
|
175
|
+
}
|
176
|
+
const { scrollTop, scrollHeight, clientHeight } = container;
|
177
|
+
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
178
|
+
if (distanceFromBottom < 100) {
|
179
|
+
pauseAutoScrollRef.current = false;
|
180
|
+
setAutoScroll(true);
|
181
|
+
}
|
182
|
+
};
|
183
|
+
const onWheel = (event) => {
|
184
|
+
const isScrollingDown = event.deltaY > 0;
|
185
|
+
handleUserScroll(isScrollingDown);
|
186
|
+
props.onWheel && props.onWheel(event);
|
187
|
+
};
|
188
|
+
const onTouchMove = (event) => {
|
189
|
+
const currentTouchY = event.touches[0].clientY;
|
190
|
+
let isScrollingDown = false;
|
191
|
+
if (lastTouchY !== null) {
|
192
|
+
isScrollingDown = currentTouchY < lastTouchY;
|
193
|
+
}
|
194
|
+
lastTouchY = currentTouchY;
|
195
|
+
handleUserScroll(isScrollingDown);
|
196
|
+
props.onTouchMove && props.onTouchMove(event);
|
197
|
+
};
|
198
|
+
const smartScrollHandlers = {
|
199
|
+
onWheel,
|
200
|
+
onTouchMove,
|
201
|
+
onTouchEnd
|
202
|
+
};
|
203
|
+
return (_jsxs(_Fragment, { children: [_jsx(JumpButton, { position: "top", isHidden: isOverflowing && atTop, onClick: scrollToTop }), _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, _jsx("div", { className: "pf-chatbot__messagebox-announcement", "aria-live": "polite", children: announcement })] })), _jsx(JumpButton, { position: "bottom", isHidden: isOverflowing && atBottom, onClick: () => scrollToBottom({ resumeSmartScroll: true }) })] }));
|
204
|
+
});
|
75
205
|
export default MessageBox;
|
@@ -9,21 +9,37 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
9
9
|
};
|
10
10
|
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
11
11
|
import { createRef } from 'react';
|
12
|
-
import { render, screen, waitFor } from '@testing-library/react';
|
12
|
+
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
13
13
|
import { MessageBox } from './MessageBox';
|
14
14
|
import userEvent from '@testing-library/user-event';
|
15
15
|
describe('MessageBox', () => {
|
16
|
+
beforeEach(() => {
|
17
|
+
jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
|
18
|
+
cb(0); // Immediately call the callback
|
19
|
+
return 0;
|
20
|
+
});
|
21
|
+
});
|
22
|
+
afterEach(() => {
|
23
|
+
jest.restoreAllMocks();
|
24
|
+
});
|
16
25
|
it('should render Message box', () => {
|
17
26
|
render(_jsx(MessageBox, { children: _jsx(_Fragment, { children: "Chatbot Messages" }) }));
|
18
27
|
expect(screen.getByText('Chatbot Messages')).toBeTruthy();
|
19
28
|
});
|
20
29
|
it('should assign ref to Message box', () => {
|
30
|
+
var _a, _b, _c, _d;
|
21
31
|
const ref = createRef();
|
22
|
-
render(_jsx(MessageBox, { ref: ref, children: _jsx("div", { children: "Test message content" }) }));
|
32
|
+
render(_jsx(MessageBox, { "data-testid": "message-box", ref: ref, children: _jsx("div", { children: "Test message content" }) }));
|
33
|
+
screen.getByText('Test message content');
|
23
34
|
expect(ref.current).not.toBeNull();
|
35
|
+
// should contain custom methods exposed by the ref
|
36
|
+
expect(typeof ((_a = ref.current) === null || _a === void 0 ? void 0 : _a.isSmartScrollActive)).toBe('function');
|
37
|
+
expect(typeof ((_b = ref.current) === null || _b === void 0 ? void 0 : _b.scrollToTop)).toBe('function');
|
38
|
+
expect(typeof ((_c = ref.current) === null || _c === void 0 ? void 0 : _c.scrollToBottom)).toBe('function');
|
39
|
+
expect((_d = ref.current) === null || _d === void 0 ? void 0 : _d.isSmartScrollActive()).toBe(false);
|
24
40
|
expect(ref.current).toBeInstanceOf(HTMLDivElement);
|
25
41
|
});
|
26
|
-
it('should call onScrollToBottomClick when scroll to
|
42
|
+
it('should call onScrollToBottomClick when scroll to bottom button is clicked', () => __awaiter(void 0, void 0, void 0, function* () {
|
27
43
|
const spy = jest.fn();
|
28
44
|
render(_jsx(MessageBox, { onScrollToBottomClick: spy, children: _jsx("div", { children: "Test message content" }) }));
|
29
45
|
// this forces button to show
|
@@ -31,7 +47,9 @@ describe('MessageBox', () => {
|
|
31
47
|
Object.defineProperty(region, 'scrollHeight', { configurable: true, value: 1000 });
|
32
48
|
Object.defineProperty(region, 'clientHeight', { configurable: true, value: 500 });
|
33
49
|
Object.defineProperty(region, 'scrollTop', { configurable: true, value: 0 });
|
34
|
-
|
50
|
+
act(() => {
|
51
|
+
region.dispatchEvent(new Event('scroll'));
|
52
|
+
});
|
35
53
|
yield waitFor(() => {
|
36
54
|
userEvent.click(screen.getByRole('button', { name: /Jump bottom/i }));
|
37
55
|
expect(spy).toHaveBeenCalled();
|
@@ -48,10 +66,165 @@ describe('MessageBox', () => {
|
|
48
66
|
configurable: true,
|
49
67
|
value: 500
|
50
68
|
});
|
51
|
-
|
69
|
+
act(() => {
|
70
|
+
region.dispatchEvent(new Event('scroll'));
|
71
|
+
});
|
52
72
|
yield waitFor(() => {
|
53
73
|
userEvent.click(screen.getByRole('button', { name: /Jump top/i }));
|
54
74
|
expect(spy).toHaveBeenCalled();
|
55
75
|
});
|
56
76
|
}));
|
77
|
+
it('should call user defined onWheel, onTouchMove and onTouchEnd handlers', () => __awaiter(void 0, void 0, void 0, function* () {
|
78
|
+
const ref = createRef();
|
79
|
+
const onWheel = jest.fn();
|
80
|
+
const onTouchMove = jest.fn();
|
81
|
+
const onTouchEnd = jest.fn();
|
82
|
+
render(_jsx(MessageBox, { ref: ref, enableSmartScroll: true, onWheel: onWheel, onTouchMove: onTouchMove, onTouchEnd: onTouchEnd, children: _jsx("div", { children: "Test message content" }) }));
|
83
|
+
const element = ref.current;
|
84
|
+
act(() => {
|
85
|
+
fireEvent.wheel(element, { deltaY: 10 });
|
86
|
+
fireEvent.touchMove(element, { touches: [{ clientY: 700 }] });
|
87
|
+
fireEvent.touchEnd(element);
|
88
|
+
});
|
89
|
+
expect(onWheel).toHaveBeenCalled();
|
90
|
+
expect(onTouchMove).toHaveBeenCalled();
|
91
|
+
expect(onTouchEnd).toHaveBeenCalled();
|
92
|
+
}));
|
93
|
+
it('should scroll to the bottom when the method is called ', () => __awaiter(void 0, void 0, void 0, function* () {
|
94
|
+
var _a;
|
95
|
+
const ref = createRef();
|
96
|
+
render(_jsx(MessageBox, { ref: ref, enableSmartScroll: true, children: _jsx("div", { children: "Test message content" }) }));
|
97
|
+
const element = ref.current;
|
98
|
+
const scrollSpy = jest.spyOn(element, 'scrollTo');
|
99
|
+
act(() => {
|
100
|
+
var _a, _b, _c;
|
101
|
+
(_a = ref.current) === null || _a === void 0 ? void 0 : _a.scrollToBottom();
|
102
|
+
(_b = ref.current) === null || _b === void 0 ? void 0 : _b.scrollToBottom();
|
103
|
+
(_c = ref.current) === null || _c === void 0 ? void 0 : _c.scrollToBottom();
|
104
|
+
});
|
105
|
+
expect(scrollSpy).toHaveBeenCalledWith({ top: element.scrollHeight, behavior: 'smooth' });
|
106
|
+
expect((_a = ref.current) === null || _a === void 0 ? void 0 : _a.isSmartScrollActive()).toBe(true);
|
107
|
+
}));
|
108
|
+
it('should scroll to the top when the method is called ', () => __awaiter(void 0, void 0, void 0, function* () {
|
109
|
+
var _a;
|
110
|
+
const ref = createRef();
|
111
|
+
render(_jsx(MessageBox, { ref: ref, enableSmartScroll: true, children: _jsx("div", { children: "Test message content" }) }));
|
112
|
+
const element = ref.current;
|
113
|
+
const scrollSpy = jest.spyOn(element, 'scrollTo');
|
114
|
+
act(() => {
|
115
|
+
var _a;
|
116
|
+
(_a = ref.current) === null || _a === void 0 ? void 0 : _a.scrollToTop();
|
117
|
+
});
|
118
|
+
expect(scrollSpy).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
|
119
|
+
expect((_a = ref.current) === null || _a === void 0 ? void 0 : _a.isSmartScrollActive()).toBe(false);
|
120
|
+
}));
|
121
|
+
it('should pause automatic scrolling when user scrolls up ', () => __awaiter(void 0, void 0, void 0, function* () {
|
122
|
+
var _a, _b;
|
123
|
+
const ref = createRef();
|
124
|
+
render(_jsx(MessageBox, { ref: ref, enableSmartScroll: true, children: _jsx("div", { children: "Test message content" }) }));
|
125
|
+
expect((_a = ref.current) === null || _a === void 0 ? void 0 : _a.isSmartScrollActive()).toBe(true);
|
126
|
+
const element = ref.current;
|
127
|
+
// Manually set scrollHeight and clientHeight for calculations
|
128
|
+
Object.defineProperty(element, 'scrollHeight', { configurable: true, value: 1000 });
|
129
|
+
Object.defineProperty(element, 'clientHeight', { configurable: true, value: 300 });
|
130
|
+
// Simulate scroll up by changing scrollTop
|
131
|
+
element.scrollTop = 200;
|
132
|
+
const scrollEvent = new Event('scroll', { bubbles: true });
|
133
|
+
act(() => {
|
134
|
+
element.dispatchEvent(scrollEvent);
|
135
|
+
});
|
136
|
+
expect((_b = ref.current) === null || _b === void 0 ? void 0 : _b.isSmartScrollActive()).toBe(false);
|
137
|
+
}));
|
138
|
+
it('should resume automatic scrolling when user scrolls down to the bottom using scroll event', () => __awaiter(void 0, void 0, void 0, function* () {
|
139
|
+
var _a, _b;
|
140
|
+
jest.useFakeTimers();
|
141
|
+
const ref = createRef();
|
142
|
+
render(_jsx(MessageBox, { ref: ref, enableSmartScroll: true, children: _jsx("div", { children: "Test message content" }) }));
|
143
|
+
const element = ref.current;
|
144
|
+
// Manually set scrollHeight and clientHeight for calculations
|
145
|
+
Object.defineProperty(element, 'scrollHeight', { configurable: true, value: 1000 });
|
146
|
+
Object.defineProperty(element, 'clientHeight', { configurable: true, value: 300 });
|
147
|
+
// Simulate scroll up by changing scrollTop
|
148
|
+
element.scrollTop = 100;
|
149
|
+
const scrollEvent = new Event('scroll', { bubbles: true });
|
150
|
+
act(() => {
|
151
|
+
element.dispatchEvent(scrollEvent);
|
152
|
+
});
|
153
|
+
expect((_a = ref.current) === null || _a === void 0 ? void 0 : _a.isSmartScrollActive()).toBe(false);
|
154
|
+
act(() => {
|
155
|
+
// Simulate scroll down by changing scrollTop
|
156
|
+
element.scrollTop = 650; // scrollHeight - scrollTop - clientHeight - DELTA_DOWN (50) = 0
|
157
|
+
const scrollEvent = new Event('scroll', { bubbles: true });
|
158
|
+
element.dispatchEvent(scrollEvent);
|
159
|
+
jest.advanceTimersByTime(250);
|
160
|
+
});
|
161
|
+
expect((_b = ref.current) === null || _b === void 0 ? void 0 : _b.isSmartScrollActive()).toBe(true);
|
162
|
+
jest.useRealTimers();
|
163
|
+
}));
|
164
|
+
it('should resume automatic scrolling when scrollToBottom method is used', () => __awaiter(void 0, void 0, void 0, function* () {
|
165
|
+
var _a, _b;
|
166
|
+
const ref = createRef();
|
167
|
+
render(_jsx(MessageBox, { ref: ref, enableSmartScroll: true, children: _jsx("div", { children: "Test message content" }) }));
|
168
|
+
const element = ref.current;
|
169
|
+
// Manually set scrollHeight and clientHeight for calculations
|
170
|
+
Object.defineProperty(element, 'scrollHeight', { configurable: true, value: 1000 });
|
171
|
+
Object.defineProperty(element, 'clientHeight', { configurable: true, value: 300 });
|
172
|
+
// Simulate scroll up by changing scrollTop
|
173
|
+
element.scrollTop = 100;
|
174
|
+
const scrollEvent = new Event('scroll', { bubbles: true });
|
175
|
+
act(() => {
|
176
|
+
element.dispatchEvent(scrollEvent);
|
177
|
+
});
|
178
|
+
expect((_a = ref.current) === null || _a === void 0 ? void 0 : _a.isSmartScrollActive()).toBe(false);
|
179
|
+
act(() => {
|
180
|
+
var _a;
|
181
|
+
(_a = ref.current) === null || _a === void 0 ? void 0 : _a.scrollToBottom({ resumeSmartScroll: true, behavior: 'auto' }); // resumes auto scroll and scrolls to bottom.
|
182
|
+
});
|
183
|
+
expect((_b = ref.current) === null || _b === void 0 ? void 0 : _b.isSmartScrollActive()).toBe(true);
|
184
|
+
}));
|
185
|
+
it('should resume automatic scrolling when mouse wheel event is used', () => __awaiter(void 0, void 0, void 0, function* () {
|
186
|
+
var _a, _b;
|
187
|
+
const ref = createRef();
|
188
|
+
render(_jsx(MessageBox, { ref: ref, enableSmartScroll: true, children: _jsx("div", { children: "Test message content" }) }));
|
189
|
+
const element = ref.current;
|
190
|
+
// Manually set scrollHeight and clientHeight for calculations
|
191
|
+
Object.defineProperty(element, 'scrollHeight', { configurable: true, value: 1000 });
|
192
|
+
Object.defineProperty(element, 'clientHeight', { configurable: true, value: 300 });
|
193
|
+
Object.defineProperty(element, 'scrollTop', { configurable: true, value: 350 });
|
194
|
+
const scrollEvent = new Event('scroll', { bubbles: true });
|
195
|
+
act(() => {
|
196
|
+
element.dispatchEvent(scrollEvent);
|
197
|
+
});
|
198
|
+
expect((_a = ref.current) === null || _a === void 0 ? void 0 : _a.isSmartScrollActive()).toBe(false);
|
199
|
+
// Simulate mouse wheel event
|
200
|
+
act(() => {
|
201
|
+
Object.defineProperty(element, 'scrollTop', { configurable: true, value: 650 });
|
202
|
+
fireEvent.wheel(element, { deltaY: 10 });
|
203
|
+
});
|
204
|
+
expect((_b = ref.current) === null || _b === void 0 ? void 0 : _b.isSmartScrollActive()).toBe(true);
|
205
|
+
}));
|
206
|
+
it('should resume automatic scrolling when user swipes up in touch screen', () => __awaiter(void 0, void 0, void 0, function* () {
|
207
|
+
var _a, _b;
|
208
|
+
const ref = createRef();
|
209
|
+
render(_jsx(MessageBox, { ref: ref, enableSmartScroll: true, children: _jsx("div", { children: "Test message content" }) }));
|
210
|
+
const element = ref.current;
|
211
|
+
// Manually set scrollHeight and clientHeight for calculations
|
212
|
+
Object.defineProperty(element, 'scrollHeight', { configurable: true, value: 1000 });
|
213
|
+
Object.defineProperty(element, 'clientHeight', { configurable: true, value: 300 });
|
214
|
+
Object.defineProperty(element, 'scrollTop', { configurable: true, value: 350 });
|
215
|
+
const scrollEvent = new Event('scroll', { bubbles: true });
|
216
|
+
act(() => {
|
217
|
+
element.dispatchEvent(scrollEvent);
|
218
|
+
});
|
219
|
+
expect((_a = ref.current) === null || _a === void 0 ? void 0 : _a.isSmartScrollActive()).toBe(false);
|
220
|
+
// Simulate touch event - swipe up
|
221
|
+
act(() => {
|
222
|
+
Object.defineProperty(element, 'scrollTop', { configurable: true, value: 650 });
|
223
|
+
fireEvent.touchStart(element, { touches: [{ clientY: 700 }] });
|
224
|
+
fireEvent.touchMove(element, { touches: [{ clientY: 700 }] });
|
225
|
+
fireEvent.touchMove(element, { touches: [{ clientY: 600 }] });
|
226
|
+
fireEvent.touchEnd(element);
|
227
|
+
});
|
228
|
+
expect((_b = ref.current) === null || _b === void 0 ? void 0 : _b.isSmartScrollActive()).toBe(true);
|
229
|
+
}));
|
57
230
|
});
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@patternfly/chatbot",
|
3
|
-
"version": "6.3.0-prerelease.
|
3
|
+
"version": "6.3.0-prerelease.16",
|
4
4
|
"description": "This library provides React components based on PatternFly 6 that can be used to build chatbots.",
|
5
5
|
"main": "dist/cjs/index.js",
|
6
6
|
"module": "dist/esm/index.js",
|
@@ -45,6 +45,8 @@ import PFIconLogoReverse from '../UI/PF-IconLogo-Reverse.svg';
|
|
45
45
|
import userAvatar from '../Messages/user_avatar.svg';
|
46
46
|
import patternflyAvatar from '../Messages/patternfly_avatar.jpg';
|
47
47
|
import { FunctionComponent, useState, useRef, isValidElement, cloneElement, Children, ReactNode, MouseEvent } from 'react';
|
48
|
+
import { getTrackingProviders } from "@patternfly/chatbot/dist/dynamic/tracking";
|
49
|
+
import ChatbotConversationHistoryNav from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav';
|
48
50
|
|
49
51
|
## Demos
|
50
52
|
|
@@ -62,6 +64,40 @@ It is also important to announce when new content appears onscreen for accessibi
|
|
62
64
|
|
63
65
|
```
|
64
66
|
|
67
|
+
### Message auto-scrolling
|
68
|
+
|
69
|
+
This demo shows auto-scrolling functionality, which automatically scrolls to the bottom of the active chat.
|
70
|
+
|
71
|
+
To enable auto-scroll behavior pass the `enableSmartScroll` prop to the [`<MessageBox>`](/patternfly-ai/chatbot/ui#message-box) component.
|
72
|
+
|
73
|
+
When enabled:
|
74
|
+
|
75
|
+
- Scroll position is automatically managed based on user interaction.
|
76
|
+
- Scrolling is _not_ forced to the bottom when new messages arrive, unless explicitly triggered via the `scrollToBottom()` method.
|
77
|
+
- If the user scrolls up or interacts with UI controls like "Back to top" or "Back to bottom", the component pauses auto-scroll to respect user intent.
|
78
|
+
- Auto-scroll resumes only when the user scrolls back down manually or programmatically via the `scrollToBottom({resumeSmartScroll: true})` method.
|
79
|
+
|
80
|
+
#### Imperative methods via `ref`
|
81
|
+
|
82
|
+
When using `ref`, the `<MessageBox>` component exposes the following methods:
|
83
|
+
|
84
|
+
- `scrollToBottom()`: Scrolls to the bottom of the message container.
|
85
|
+
- `scrollToTop()`: Scrolls to the top of the message container.
|
86
|
+
- `isSmartScrollActive()`: Returns `true` if smart auto-scroll is currently active.
|
87
|
+
- Native `HTMLDivElement` methods like `scrollTo()`.
|
88
|
+
|
89
|
+
This demo includes broader ChatBot features, including:
|
90
|
+
|
91
|
+
1. A [`<ChatbotToggle>`](/patternfly-ai/chatbot/ui#toggle) that controls the [`<Chatbot>`](/patternfly-ai/chatbot/ui#container) container.
|
92
|
+
2. A `<ChatbotContent>` and [`<MessageBox>`](/patternfly-ai/chatbot/ui#content-and-message-box) with:
|
93
|
+
- A `<ChatbotWelcomePrompt>`
|
94
|
+
- An initial user message and initial bot message
|
95
|
+
3. A [`<ChatbotFooter>`](/patternfly-ai/chatbot/ui#footer) with a [`<ChatbotFootnote>`](/patternfly-ai/chatbot/ui#footnote-with-popover) and a `<MessageBar>`
|
96
|
+
|
97
|
+
```js file="./ChatbotScrolling.tsx" isFullscreen
|
98
|
+
|
99
|
+
```
|
100
|
+
|
65
101
|
### Attach via upload button in message bar
|
66
102
|
|
67
103
|
This demo displays unique attachment features, including:
|