@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
@@ -17,6 +17,8 @@ export interface ChatbotHeaderSelectorDropdownProps extends Omit<DropdownProps,
|
|
17
17
|
isCompact?: boolean;
|
18
18
|
/** Additional props passed to toggle */
|
19
19
|
toggleProps?: MenuToggleProps;
|
20
|
+
/** Custom width for the dropdown */
|
21
|
+
dropdownWidth?: string;
|
20
22
|
}
|
21
23
|
export declare const ChatbotHeaderSelectorDropdown: FunctionComponent<ChatbotHeaderSelectorDropdownProps>;
|
22
24
|
export default ChatbotHeaderSelectorDropdown;
|
@@ -16,12 +16,14 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
16
16
|
const react_1 = require("react");
|
17
17
|
const react_core_1 = require("@patternfly/react-core");
|
18
18
|
const ChatbotHeaderSelectorDropdown = (_a) => {
|
19
|
-
var { value, className, children, onSelect, tooltipProps, tooltipContent = 'Select model', menuToggleAriaLabel, isCompact, toggleProps } = _a, props = __rest(_a, ["value", "className", "children", "onSelect", "tooltipProps", "tooltipContent", "menuToggleAriaLabel", "isCompact", "toggleProps"]);
|
19
|
+
var { value, className, children, onSelect, tooltipProps, tooltipContent = 'Select model', menuToggleAriaLabel, isCompact, toggleProps, dropdownWidth } = _a, props = __rest(_a, ["value", "className", "children", "onSelect", "tooltipProps", "tooltipContent", "menuToggleAriaLabel", "isCompact", "toggleProps", "dropdownWidth"]);
|
20
20
|
const [isOptionsMenuOpen, setIsOptionsMenuOpen] = (0, react_1.useState)(false);
|
21
21
|
const [defaultAriaLabel, setDefaultAriaLabel] = (0, react_1.useState)('Select model');
|
22
22
|
const toggle = (toggleRef) => ((0, jsx_runtime_1.jsx)(react_core_1.Tooltip, Object.assign({ className: "pf-chatbot__tooltip", content: tooltipContent, position: "bottom",
|
23
23
|
// prevents VO announcements of both aria label and tooltip
|
24
|
-
aria: "none" }, tooltipProps, { children: (0, jsx_runtime_1.jsx)(react_core_1.MenuToggle, Object.assign({ variant: "secondary", "aria-label": menuToggleAriaLabel !== null && menuToggleAriaLabel !== void 0 ? menuToggleAriaLabel : defaultAriaLabel, ref: toggleRef, isExpanded: isOptionsMenuOpen, onClick: () => setIsOptionsMenuOpen(!isOptionsMenuOpen), size: isCompact ? 'sm' : undefined, className: `${isCompact ? 'pf-m-compact' : ''}` }, toggleProps, {
|
24
|
+
aria: "none" }, tooltipProps, { children: (0, jsx_runtime_1.jsx)(react_core_1.MenuToggle, Object.assign({ variant: "secondary", "aria-label": menuToggleAriaLabel !== null && menuToggleAriaLabel !== void 0 ? menuToggleAriaLabel : defaultAriaLabel, ref: toggleRef, isExpanded: isOptionsMenuOpen, onClick: () => setIsOptionsMenuOpen(!isOptionsMenuOpen), size: isCompact ? 'sm' : undefined, className: `${isCompact ? 'pf-m-compact' : ''}` }, toggleProps, { style: {
|
25
|
+
width: dropdownWidth
|
26
|
+
}, children: value })) })));
|
25
27
|
return ((0, jsx_runtime_1.jsx)(react_core_1.Dropdown, Object.assign({ className: `pf-chatbot__selections ${className !== null && className !== void 0 ? className : ''}`, isOpen: isOptionsMenuOpen, onSelect: (e, value) => {
|
26
28
|
onSelect && onSelect(e, value);
|
27
29
|
setDefaultAriaLabel(`Select model: ${value}`);
|
@@ -1,14 +1,14 @@
|
|
1
|
-
import
|
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:
|
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
|
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
|
-
|
22
|
-
var { announcement, ariaLabel = 'Scrollable message log', children,
|
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
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
79
|
-
|
80
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
});
|
@@ -17,6 +17,8 @@ export interface ChatbotHeaderSelectorDropdownProps extends Omit<DropdownProps,
|
|
17
17
|
isCompact?: boolean;
|
18
18
|
/** Additional props passed to toggle */
|
19
19
|
toggleProps?: MenuToggleProps;
|
20
|
+
/** Custom width for the dropdown */
|
21
|
+
dropdownWidth?: string;
|
20
22
|
}
|
21
23
|
export declare const ChatbotHeaderSelectorDropdown: FunctionComponent<ChatbotHeaderSelectorDropdownProps>;
|
22
24
|
export default ChatbotHeaderSelectorDropdown;
|
@@ -13,12 +13,14 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
13
13
|
import { useState } from 'react';
|
14
14
|
import { Tooltip, Dropdown, MenuToggle } from '@patternfly/react-core';
|
15
15
|
export const ChatbotHeaderSelectorDropdown = (_a) => {
|
16
|
-
var { value, className, children, onSelect, tooltipProps, tooltipContent = 'Select model', menuToggleAriaLabel, isCompact, toggleProps } = _a, props = __rest(_a, ["value", "className", "children", "onSelect", "tooltipProps", "tooltipContent", "menuToggleAriaLabel", "isCompact", "toggleProps"]);
|
16
|
+
var { value, className, children, onSelect, tooltipProps, tooltipContent = 'Select model', menuToggleAriaLabel, isCompact, toggleProps, dropdownWidth } = _a, props = __rest(_a, ["value", "className", "children", "onSelect", "tooltipProps", "tooltipContent", "menuToggleAriaLabel", "isCompact", "toggleProps", "dropdownWidth"]);
|
17
17
|
const [isOptionsMenuOpen, setIsOptionsMenuOpen] = useState(false);
|
18
18
|
const [defaultAriaLabel, setDefaultAriaLabel] = useState('Select model');
|
19
19
|
const toggle = (toggleRef) => (_jsx(Tooltip, Object.assign({ className: "pf-chatbot__tooltip", content: tooltipContent, position: "bottom",
|
20
20
|
// prevents VO announcements of both aria label and tooltip
|
21
|
-
aria: "none" }, tooltipProps, { children: _jsx(MenuToggle, Object.assign({ variant: "secondary", "aria-label": menuToggleAriaLabel !== null && menuToggleAriaLabel !== void 0 ? menuToggleAriaLabel : defaultAriaLabel, ref: toggleRef, isExpanded: isOptionsMenuOpen, onClick: () => setIsOptionsMenuOpen(!isOptionsMenuOpen), size: isCompact ? 'sm' : undefined, className: `${isCompact ? 'pf-m-compact' : ''}` }, toggleProps, {
|
21
|
+
aria: "none" }, tooltipProps, { children: _jsx(MenuToggle, Object.assign({ variant: "secondary", "aria-label": menuToggleAriaLabel !== null && menuToggleAriaLabel !== void 0 ? menuToggleAriaLabel : defaultAriaLabel, ref: toggleRef, isExpanded: isOptionsMenuOpen, onClick: () => setIsOptionsMenuOpen(!isOptionsMenuOpen), size: isCompact ? 'sm' : undefined, className: `${isCompact ? 'pf-m-compact' : ''}` }, toggleProps, { style: {
|
22
|
+
width: dropdownWidth
|
23
|
+
}, children: value })) })));
|
22
24
|
return (_jsx(Dropdown, Object.assign({ className: `pf-chatbot__selections ${className !== null && className !== void 0 ? className : ''}`, isOpen: isOptionsMenuOpen, onSelect: (e, value) => {
|
23
25
|
onSelect && onSelect(e, value);
|
24
26
|
setDefaultAriaLabel(`Select model: ${value}`);
|
@@ -1,14 +1,14 @@
|
|
1
|
-
import
|
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:
|
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
|
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;
|