@constructor-io/constructorio-ui-pia 2.17.0 → 2.18.1
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/README.md +7 -273
- package/dist/constructorio-ui-pia-bundled.js +33 -33
- package/lib/cjs/components/Answer/Answer.js +2 -2
- package/lib/cjs/components/CioPia/CioPiaQna.js +12 -2
- package/lib/cjs/components/CioPiaRecs/RecsPodRefinement.js +13 -3
- package/lib/cjs/components/Input/Input.js +3 -2
- package/lib/cjs/components/PiaConversation/PiaConversation.js +19 -3
- package/lib/cjs/components/PiaConversation/PiaModal.js +24 -1
- package/lib/cjs/hooks/useReturnFocusToInput.js +28 -0
- package/lib/cjs/stories/assets/storybook-styles.css +0 -135
- package/lib/cjs/utils/contentTransformers.js +52 -6
- package/lib/cjs/version.js +1 -1
- package/lib/mjs/components/Answer/Answer.js +2 -2
- package/lib/mjs/components/CioPia/CioPiaQna.js +12 -2
- package/lib/mjs/components/CioPiaRecs/RecsPodRefinement.js +13 -3
- package/lib/mjs/components/Input/Input.js +3 -2
- package/lib/mjs/components/PiaConversation/PiaConversation.js +19 -3
- package/lib/mjs/components/PiaConversation/PiaModal.js +23 -2
- package/lib/mjs/hooks/useReturnFocusToInput.js +25 -0
- package/lib/mjs/stories/assets/storybook-styles.css +0 -135
- package/lib/mjs/utils/contentTransformers.js +55 -5
- package/lib/mjs/version.js +1 -1
- package/lib/types/components/Input/Input.d.ts +2 -1
- package/lib/types/components/PiaConversation/PiaModal.d.ts +2 -0
- package/lib/types/hooks/useReturnFocusToInput.d.ts +9 -0
- package/lib/types/types.d.ts +5 -0
- package/lib/types/utils/contentTransformers.d.ts +10 -0
- package/lib/types/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@ function Answer({ text, componentOverride }) {
|
|
|
8
8
|
if (!text) {
|
|
9
9
|
return null;
|
|
10
10
|
}
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
const html = (0, contentTransformers_1.renderMarkdown)(text);
|
|
12
|
+
return (react_1.default.createElement(constructorio_ui_components_1.RenderPropsWrapper, { props: { text }, override: componentOverride === null || componentOverride === void 0 ? void 0 : componentOverride.reactNode }, html ? (react_1.default.createElement("div", { className: 'cio-pia-answer', "data-testid": 'answer-text', dangerouslySetInnerHTML: { __html: html } })) : (react_1.default.createElement("div", { className: 'cio-pia-answer', "data-testid": 'answer-text' }, text))));
|
|
13
13
|
}
|
|
14
14
|
exports.default = Answer;
|
|
@@ -12,6 +12,7 @@ const usePersistedConversation_1 = tslib_1.__importDefault(require("../../hooks/
|
|
|
12
12
|
const useTracking_1 = tslib_1.__importDefault(require("../../hooks/useTracking"));
|
|
13
13
|
const useViewportTracking_1 = tslib_1.__importDefault(require("../../hooks/useViewportTracking"));
|
|
14
14
|
const useViewportCallbacks_1 = tslib_1.__importDefault(require("../../hooks/useViewportCallbacks"));
|
|
15
|
+
const useReturnFocusToInput_1 = tslib_1.__importDefault(require("../../hooks/useReturnFocusToInput"));
|
|
15
16
|
const ErrorBlock_1 = tslib_1.__importDefault(require("../Error/ErrorBlock"));
|
|
16
17
|
const LoadingSkeleton_1 = tslib_1.__importDefault(require("../LoadingSkeleton/LoadingSkeleton"));
|
|
17
18
|
const translate_1 = require("../../utils/translate");
|
|
@@ -64,6 +65,15 @@ function CioPiaQna(props) {
|
|
|
64
65
|
initialConversationHistory,
|
|
65
66
|
persisted,
|
|
66
67
|
});
|
|
68
|
+
const { inputRef, returnFocusAfterLoading } = (0, useReturnFocusToInput_1.default)(isLoading);
|
|
69
|
+
const handleInlineSubmit = (0, react_1.useCallback)((question) => {
|
|
70
|
+
handleSubmitQuestion(question);
|
|
71
|
+
returnFocusAfterLoading();
|
|
72
|
+
}, [handleSubmitQuestion, returnFocusAfterLoading]);
|
|
73
|
+
const handleInlineQuestionClick = (0, react_1.useCallback)((question) => {
|
|
74
|
+
handleQuestionClick(question);
|
|
75
|
+
returnFocusAfterLoading();
|
|
76
|
+
}, [handleQuestionClick, returnFocusAfterLoading]);
|
|
67
77
|
const { containerRef: viewportContainerRef } = (0, useViewportTracking_1.default)({
|
|
68
78
|
tracking,
|
|
69
79
|
questions: displayedQuestions,
|
|
@@ -127,12 +137,12 @@ function CioPiaQna(props) {
|
|
|
127
137
|
return (react_1.default.createElement("div", { ref: containerRef, className: 'cio-pia-container', "data-testid": 'cio-pia-container' },
|
|
128
138
|
react_1.default.createElement(constructorio_ui_components_1.RenderPropsWrapper, { props: renderProps, override: children || (componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.reactNode) },
|
|
129
139
|
react_1.default.createElement("p", { className: 'cio-pia-title', "data-testid": 'cio-pia-title' }, (0, translate_1.translate)('Any questions about this product?', translations)),
|
|
130
|
-
react_1.default.createElement(Input_1.default, { onSubmit:
|
|
140
|
+
react_1.default.createElement(Input_1.default, { onSubmit: handleInlineSubmit, onFocus: handleInputFocus, inputRef: inputRef, disabled: isLoading, value: currentQuestion, translations: translations, componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.input }),
|
|
131
141
|
isAnswerLoading && react_1.default.createElement(LoadingSkeleton_1.default, { componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.loading }),
|
|
132
142
|
!isAnswerLoading && error && (react_1.default.createElement(ErrorBlock_1.default, { key: error.message || (0, translate_1.translate)('Unexpected error', translations), message: error.message, translations: translations })),
|
|
133
143
|
!isAnswerLoading && !error && currentAnswer && (react_1.default.createElement(PiaInlineAnswer_1.default, { currentAnswer: currentAnswer, currentItems: currentItems, showFeedback: showFeedback, learnMoreUrl: learnMoreUrl, disclaimerPosition: disclaimerPosition, translations: translations, callbacks: callbacks, componentOverrides: componentOverrides, onFeedback: handleFeedback, onResultClick: tracking.trackResultClick, question: currentQuestion, qnaResultId: qnaResultId, priceCurrency: priceCurrency })),
|
|
134
144
|
isLoading && react_1.default.createElement(SuggestedQuestionsSkeleton_1.default, null),
|
|
135
|
-
!isLoading && !error && (react_1.default.createElement(SuggestedQuestionsContainer_1.default, { questions: displayedQuestions, onQuestionClick:
|
|
145
|
+
!isLoading && !error && (react_1.default.createElement(SuggestedQuestionsContainer_1.default, { questions: displayedQuestions, onQuestionClick: handleInlineQuestionClick, componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.suggestedQuestions })),
|
|
136
146
|
checkoutElement),
|
|
137
147
|
react_1.default.createElement(StatusRegion_1.default, { message: answerStatus, "data-testid": 'answer-status' })));
|
|
138
148
|
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const tslib_1 = require("tslib");
|
|
4
|
-
const react_1 = tslib_1.
|
|
4
|
+
const react_1 = tslib_1.__importStar(require("react"));
|
|
5
5
|
const Input_1 = tslib_1.__importDefault(require("../Input/Input"));
|
|
6
6
|
const SuggestedQuestion_1 = tslib_1.__importDefault(require("../SuggestedQuestion/SuggestedQuestion"));
|
|
7
7
|
const icons_1 = require("../icons");
|
|
8
8
|
const RecsOptionsSkeleton_1 = tslib_1.__importDefault(require("./RecsOptionsSkeleton"));
|
|
9
9
|
const recs_1 = require("../../utils/recs");
|
|
10
10
|
const constants_1 = require("../../constants");
|
|
11
|
+
const useReturnFocusToInput_1 = tslib_1.__importDefault(require("../../hooks/useReturnFocusToInput"));
|
|
11
12
|
/**
|
|
12
13
|
* The row under the products: a prompt line, the short options the API suggested, and a box for
|
|
13
14
|
* anything it did not think of. The prompt line is held still while the rest reloads, so the row
|
|
@@ -15,11 +16,20 @@ const constants_1 = require("../../constants");
|
|
|
15
16
|
*/
|
|
16
17
|
function RecsPodRefinement({ refinement, isLoading, inputError, showInput, translations, componentOverrides, onRefine, onSubmit, onInputFocus, }) {
|
|
17
18
|
const options = (refinement === null || refinement === void 0 ? void 0 : refinement.options) || [];
|
|
19
|
+
const { inputRef, returnFocusAfterLoading } = (0, useReturnFocusToInput_1.default)(isLoading);
|
|
20
|
+
const refine = (0, react_1.useCallback)((text) => {
|
|
21
|
+
onRefine(text);
|
|
22
|
+
returnFocusAfterLoading();
|
|
23
|
+
}, [onRefine, returnFocusAfterLoading]);
|
|
24
|
+
const submit = (0, react_1.useCallback)((value) => {
|
|
25
|
+
onSubmit(value);
|
|
26
|
+
returnFocusAfterLoading();
|
|
27
|
+
}, [onSubmit, returnFocusAfterLoading]);
|
|
18
28
|
return (react_1.default.createElement("div", { className: 'cio-pia-recs-pod__refinement' },
|
|
19
29
|
react_1.default.createElement("span", { className: 'cio-pia-recs-pod__refinement-label' }, (0, recs_1.resolveRefinementQuestion)(refinement, translations)),
|
|
20
|
-
isLoading ? (react_1.default.createElement(RecsOptionsSkeleton_1.default, { count: options.length || undefined })) : (options.map((option) => (react_1.default.createElement(SuggestedQuestion_1.default, { key: option, question: option, icon: react_1.default.createElement(icons_1.SparklesIcon, null), onClick: () =>
|
|
30
|
+
isLoading ? (react_1.default.createElement(RecsOptionsSkeleton_1.default, { count: options.length || undefined })) : (options.map((option) => (react_1.default.createElement(SuggestedQuestion_1.default, { key: option, question: option, icon: react_1.default.createElement(icons_1.SparklesIcon, null), onClick: () => refine(option) })))),
|
|
21
31
|
showInput && (react_1.default.createElement("div", { className: 'cio-pia-recs-pod__input' },
|
|
22
|
-
react_1.default.createElement(Input_1.default, { onSubmit:
|
|
32
|
+
react_1.default.createElement(Input_1.default, { onSubmit: submit, onFocus: onInputFocus, inputRef: inputRef, disabled: isLoading, error: inputError !== null && inputError !== void 0 ? inputError : undefined, placeholderKey: constants_1.RECS_INPUT_PLACEHOLDER,
|
|
23
33
|
// The pod's box sits in a row beside the options and submits on Enter. Q&A keeps
|
|
24
34
|
// its button; this one has none in the mocks.
|
|
25
35
|
showSendButton: false, translations: translations, componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.input })))));
|
|
@@ -16,7 +16,7 @@ const icons_1 = require("../icons");
|
|
|
16
16
|
* across its re-renders.
|
|
17
17
|
*/
|
|
18
18
|
let inputIdCounter = 0;
|
|
19
|
-
function Input({ value: providedValue, placeholder, disabled = false, onSubmit, onFocus, translations, componentOverride, error, placeholderKey = 'Ask anything', showSendButton = true, }) {
|
|
19
|
+
function Input({ value: providedValue, placeholder, disabled = false, onSubmit, onFocus, translations, componentOverride, error, inputRef, placeholderKey = 'Ask anything', showSendButton = true, }) {
|
|
20
20
|
const [value, setValue] = (0, react_1.useState)(providedValue || '');
|
|
21
21
|
const [errorId] = (0, react_1.useState)(() => {
|
|
22
22
|
inputIdCounter += 1;
|
|
@@ -53,10 +53,11 @@ function Input({ value: providedValue, placeholder, disabled = false, onSubmit,
|
|
|
53
53
|
onFocus,
|
|
54
54
|
translations,
|
|
55
55
|
error,
|
|
56
|
+
inputRef,
|
|
56
57
|
}, override: componentOverride === null || componentOverride === void 0 ? void 0 : componentOverride.reactNode },
|
|
57
58
|
react_1.default.createElement(react_1.default.Fragment, null,
|
|
58
59
|
react_1.default.createElement("div", { className: 'cio-pia-input-container' },
|
|
59
|
-
react_1.default.createElement("input", { type: 'text', value: value, onChange: (e) => setValue(e.target.value), onKeyDown: handleSubmitOnEnter, onFocus: onFocus, placeholder: resolvedPlaceholder, "aria-label": accessibleName, disabled: disabled, className: (0, classNames_1.cx)('cio-pia-input', error && 'cio-pia-input--error', !showSendButton && 'cio-pia-input--no-send'), "aria-invalid": error ? true : undefined, "aria-describedby": error ? errorId : undefined }),
|
|
60
|
+
react_1.default.createElement("input", { ref: inputRef, type: 'text', value: value, onChange: (e) => setValue(e.target.value), onKeyDown: handleSubmitOnEnter, onFocus: onFocus, placeholder: resolvedPlaceholder, "aria-label": accessibleName, disabled: disabled, className: (0, classNames_1.cx)('cio-pia-input', error && 'cio-pia-input--error', !showSendButton && 'cio-pia-input--no-send'), "aria-invalid": error ? true : undefined, "aria-describedby": error ? errorId : undefined }),
|
|
60
61
|
showSendButton && (react_1.default.createElement("button", { type: 'button', onClick: () => handleSubmit(value), className: 'cio-pia-send-button', disabled: disabled },
|
|
61
62
|
(0, translate_1.translate)('Send', translations),
|
|
62
63
|
react_1.default.createElement(icons_1.SendIcon, null)))),
|
|
@@ -1,21 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const tslib_1 = require("tslib");
|
|
4
|
-
const react_1 = tslib_1.
|
|
4
|
+
const react_1 = tslib_1.__importStar(require("react"));
|
|
5
5
|
const Input_1 = tslib_1.__importDefault(require("../Input/Input"));
|
|
6
6
|
const SuggestedQuestionsContainer_1 = tslib_1.__importDefault(require("../SuggestedQuestionsContainer/SuggestedQuestionsContainer"));
|
|
7
7
|
const SuggestedQuestionsSkeleton_1 = tslib_1.__importDefault(require("../SuggestedQuestionsContainer/SuggestedQuestionsSkeleton"));
|
|
8
8
|
const translate_1 = require("../../utils/translate");
|
|
9
|
+
const useReturnFocusToInput_1 = tslib_1.__importDefault(require("../../hooks/useReturnFocusToInput"));
|
|
10
|
+
const PiaModal_1 = require("./PiaModal");
|
|
9
11
|
const ConversationHistory_1 = tslib_1.__importDefault(require("../ConversationHistory/ConversationHistory"));
|
|
10
12
|
function PiaConversation({ conversationHistory, isLoading, error, currentItems, showFeedback, showPreviousItems, learnMoreUrl, disclaimerPosition, translations, callbacks, componentOverrides, displayedQuestions, handleSubmitQuestion, handleQuestionClick, containerRef, handleFeedback, onResultClick, qnaResultId, onInputFocus, priceCurrency, checkoutElement, }) {
|
|
11
13
|
const hasHistory = conversationHistory.length > 0;
|
|
14
|
+
const { inputRef, returnFocusAfterLoading } = (0, useReturnFocusToInput_1.default)(isLoading);
|
|
15
|
+
const modalInputRef = (0, react_1.useContext)(PiaModal_1.ModalInputRefContext);
|
|
16
|
+
const attachInput = (0, react_1.useCallback)((node) => {
|
|
17
|
+
inputRef(node);
|
|
18
|
+
modalInputRef === null || modalInputRef === void 0 ? void 0 : modalInputRef(node);
|
|
19
|
+
}, [inputRef, modalInputRef]);
|
|
20
|
+
const submitQuestion = (0, react_1.useCallback)((question) => {
|
|
21
|
+
handleSubmitQuestion(question);
|
|
22
|
+
returnFocusAfterLoading();
|
|
23
|
+
}, [handleSubmitQuestion, returnFocusAfterLoading]);
|
|
24
|
+
const clickQuestion = (0, react_1.useCallback)((question) => {
|
|
25
|
+
handleQuestionClick(question);
|
|
26
|
+
returnFocusAfterLoading();
|
|
27
|
+
}, [handleQuestionClick, returnFocusAfterLoading]);
|
|
12
28
|
return (react_1.default.createElement("div", { ref: containerRef, className: 'cio-pia-container cio-pia-conversation', "data-testid": 'cio-pia-container' },
|
|
13
29
|
!hasHistory && (react_1.default.createElement("p", { className: 'cio-pia-title', "data-testid": 'cio-pia-title' }, (0, translate_1.translate)('Any questions about this product?', translations))),
|
|
14
30
|
react_1.default.createElement(ConversationHistory_1.default, { conversationHistory: conversationHistory, isLoading: isLoading, error: error, currentItems: currentItems, showFeedback: showFeedback, showPreviousItems: showPreviousItems, learnMoreUrl: learnMoreUrl, disclaimerPosition: disclaimerPosition, translations: translations, callbacks: callbacks, componentOverrides: componentOverrides, handleFeedback: handleFeedback, onResultClick: onResultClick, qnaResultId: qnaResultId, priceCurrency: priceCurrency }),
|
|
15
31
|
checkoutElement && react_1.default.createElement("div", { className: 'cio-pia-conversation-checkout' }, checkoutElement),
|
|
16
32
|
react_1.default.createElement("div", { className: 'cio-pia-conversation-footer' },
|
|
17
33
|
isLoading && !error && react_1.default.createElement(SuggestedQuestionsSkeleton_1.default, null),
|
|
18
|
-
!isLoading && !error && (react_1.default.createElement(SuggestedQuestionsContainer_1.default, { questions: displayedQuestions, onQuestionClick:
|
|
19
|
-
react_1.default.createElement(Input_1.default, { onSubmit:
|
|
34
|
+
!isLoading && !error && (react_1.default.createElement(SuggestedQuestionsContainer_1.default, { questions: displayedQuestions, onQuestionClick: clickQuestion, componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.suggestedQuestions })),
|
|
35
|
+
react_1.default.createElement(Input_1.default, { onSubmit: submitQuestion, onFocus: onInputFocus, inputRef: attachInput, disabled: isLoading, translations: translations, componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.input }))));
|
|
20
36
|
}
|
|
21
37
|
exports.default = PiaConversation;
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ModalInputRefContext = void 0;
|
|
3
4
|
const tslib_1 = require("tslib");
|
|
4
5
|
const react_1 = tslib_1.__importStar(require("react"));
|
|
5
6
|
const Input_1 = tslib_1.__importDefault(require("../Input/Input"));
|
|
6
7
|
const translate_1 = require("../../utils/translate");
|
|
7
8
|
const SuggestedQuestionsContainer_1 = tslib_1.__importDefault(require("../SuggestedQuestionsContainer/SuggestedQuestionsContainer"));
|
|
8
9
|
const OVERFLOW_HIDDEN_CLASS = 'cio-pia-modal-open';
|
|
10
|
+
/** Lets the conversation rendered as the modal's children hand it the node of its input. */
|
|
11
|
+
exports.ModalInputRefContext = (0, react_1.createContext)(undefined);
|
|
9
12
|
function CloseIcon() {
|
|
10
13
|
return (react_1.default.createElement("svg", { width: '20', height: '20', viewBox: '0 0 20 20', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', "aria-hidden": 'true', focusable: 'false' },
|
|
11
14
|
react_1.default.createElement("path", { d: 'M15.8334 5.34166L14.6584 4.16666L10.0001 8.82499L5.34175 4.16666L4.16675 5.34166L8.82508 9.99999L4.16675 14.6583L5.34175 15.8333L10.0001 11.175L14.6584 15.8333L15.8334 14.6583L11.1751 9.99999L15.8334 5.34166Z', fill: 'currentColor' })));
|
|
@@ -14,6 +17,11 @@ function PiaModal({ initialQuestions, handleSubmitQuestion, handleQuestionClick,
|
|
|
14
17
|
const [isOpen, setIsOpen] = (0, react_1.useState)(false);
|
|
15
18
|
const dialogRef = (0, react_1.useRef)(null);
|
|
16
19
|
const triggerRef = (0, react_1.useRef)(null);
|
|
20
|
+
const initialFocusRef = (0, react_1.useRef)(null);
|
|
21
|
+
const inputNodeRef = (0, react_1.useRef)(null);
|
|
22
|
+
const setInputNode = (0, react_1.useCallback)((node) => {
|
|
23
|
+
inputNodeRef.current = node;
|
|
24
|
+
}, []);
|
|
17
25
|
const openModal = (0, react_1.useCallback)(() => {
|
|
18
26
|
const active = document.activeElement;
|
|
19
27
|
const isRealTrigger = !!active && active !== document.body && active !== document.documentElement;
|
|
@@ -30,6 +38,7 @@ function PiaModal({ initialQuestions, handleSubmitQuestion, handleQuestionClick,
|
|
|
30
38
|
return undefined;
|
|
31
39
|
if (isOpen) {
|
|
32
40
|
dialog.showModal();
|
|
41
|
+
initialFocusRef.current = document.activeElement;
|
|
33
42
|
document.body.classList.add(OVERFLOW_HIDDEN_CLASS);
|
|
34
43
|
}
|
|
35
44
|
else {
|
|
@@ -41,6 +50,19 @@ function PiaModal({ initialQuestions, handleSubmitQuestion, handleQuestionClick,
|
|
|
41
50
|
document.body.classList.remove(OVERFLOW_HIDDEN_CLASS);
|
|
42
51
|
};
|
|
43
52
|
}, [isOpen]);
|
|
53
|
+
// The input is disabled when the dialog opens, so showModal() parks focus on the first enabled
|
|
54
|
+
// control. Hand it to the input once the answer has loaded, unless the shopper has moved it.
|
|
55
|
+
(0, react_1.useEffect)(() => {
|
|
56
|
+
var _a;
|
|
57
|
+
if (!isOpen || isLoading || !initialFocusRef.current)
|
|
58
|
+
return;
|
|
59
|
+
const placed = initialFocusRef.current;
|
|
60
|
+
initialFocusRef.current = null;
|
|
61
|
+
const active = document.activeElement;
|
|
62
|
+
if (active !== placed && active !== document.body)
|
|
63
|
+
return;
|
|
64
|
+
(_a = inputNodeRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
65
|
+
}, [isOpen, isLoading]);
|
|
44
66
|
// Restore focus to the trigger. Re-runs on `isLoading`: the input trigger is
|
|
45
67
|
// disabled while a request is in flight and `focus()` would no-op.
|
|
46
68
|
(0, react_1.useEffect)(() => {
|
|
@@ -92,6 +114,7 @@ function PiaModal({ initialQuestions, handleSubmitQuestion, handleQuestionClick,
|
|
|
92
114
|
react_1.default.createElement("p", { className: 'cio-pia-title', id: 'cio-pia-modal-title' }, (0, translate_1.translate)('Ask about this product', translations)),
|
|
93
115
|
react_1.default.createElement("button", { type: 'button', className: 'cio-pia-modal-close-button', "aria-label": (0, translate_1.translate)('Close', translations), onClick: closeModal },
|
|
94
116
|
react_1.default.createElement(CloseIcon, null))),
|
|
95
|
-
react_1.default.createElement("div", { className: 'cio-pia-modal-body' },
|
|
117
|
+
react_1.default.createElement("div", { className: 'cio-pia-modal-body' },
|
|
118
|
+
react_1.default.createElement(exports.ModalInputRefContext.Provider, { value: setInputNode }, children))))));
|
|
96
119
|
}
|
|
97
120
|
exports.default = PiaModal;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const react_1 = require("react");
|
|
4
|
+
/**
|
|
5
|
+
* While an answer loads, the control the shopper used to ask is disabled or swapped for a
|
|
6
|
+
* skeleton, and the browser drops focus to <body>.
|
|
7
|
+
*/
|
|
8
|
+
function useReturnFocusToInput(isLoading) {
|
|
9
|
+
const inputNodeRef = (0, react_1.useRef)(null);
|
|
10
|
+
const pendingRef = (0, react_1.useRef)(false);
|
|
11
|
+
(0, react_1.useEffect)(() => {
|
|
12
|
+
if (isLoading || !pendingRef.current)
|
|
13
|
+
return;
|
|
14
|
+
pendingRef.current = false;
|
|
15
|
+
const active = document.activeElement;
|
|
16
|
+
const focusWasDropped = !active || active === document.body;
|
|
17
|
+
if (inputNodeRef.current && focusWasDropped)
|
|
18
|
+
inputNodeRef.current.focus();
|
|
19
|
+
}, [isLoading]);
|
|
20
|
+
const inputRef = (0, react_1.useCallback)((node) => {
|
|
21
|
+
inputNodeRef.current = node;
|
|
22
|
+
}, []);
|
|
23
|
+
const returnFocusAfterLoading = (0, react_1.useCallback)(() => {
|
|
24
|
+
pendingRef.current = true;
|
|
25
|
+
}, []);
|
|
26
|
+
return { inputRef, returnFocusAfterLoading };
|
|
27
|
+
}
|
|
28
|
+
exports.default = useReturnFocusToInput;
|
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
div.subheading {
|
|
2
|
-
--mediumdark: '#999999';
|
|
3
|
-
font-weight: 700;
|
|
4
|
-
font-size: 13px;
|
|
5
|
-
color: #999;
|
|
6
|
-
letter-spacing: 6px;
|
|
7
|
-
line-height: 24px;
|
|
8
|
-
text-transform: uppercase;
|
|
9
|
-
margin-bottom: 12px;
|
|
10
|
-
margin-top: 40px;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
1
|
div.link-list {
|
|
14
2
|
display: grid;
|
|
15
3
|
grid-template-columns: 1fr;
|
|
@@ -78,126 +66,3 @@ a.link-item p {
|
|
|
78
66
|
line-height: 20px;
|
|
79
67
|
}
|
|
80
68
|
|
|
81
|
-
span.badge {
|
|
82
|
-
display: inline-block;
|
|
83
|
-
border-radius: 1em;
|
|
84
|
-
font-size: 11px;
|
|
85
|
-
line-height: 12px;
|
|
86
|
-
font-weight: 700;
|
|
87
|
-
background: #e7fdd8;
|
|
88
|
-
color: #66bf3c;
|
|
89
|
-
padding: 4px 12px;
|
|
90
|
-
margin-right: 10px;
|
|
91
|
-
vertical-align: top;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
div.badge-wrapper {
|
|
95
|
-
font-size: 13px;
|
|
96
|
-
line-height: 20px;
|
|
97
|
-
margin-top: 40px;
|
|
98
|
-
margin-bottom: 40px;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
div.badge-wrapper code {
|
|
102
|
-
font-size: 12px;
|
|
103
|
-
display: inline-block;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
.badge.tip {
|
|
107
|
-
background: #e7fdd8;
|
|
108
|
-
color: #66bf3c;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
.badge.note {
|
|
112
|
-
background: #d8e8fd;
|
|
113
|
-
color: #3c70bf;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
.cio-flow-diagram {
|
|
117
|
-
display: flex;
|
|
118
|
-
flex-direction: column;
|
|
119
|
-
gap: 4px;
|
|
120
|
-
margin: 24px 0 0;
|
|
121
|
-
font-family: system-ui, sans-serif;
|
|
122
|
-
font-size: 14px;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
.cio-flow-diagram .flow-step {
|
|
126
|
-
display: flex;
|
|
127
|
-
align-items: center;
|
|
128
|
-
gap: 12px;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
.cio-flow-diagram .flow-box {
|
|
132
|
-
padding: 10px 16px;
|
|
133
|
-
border-radius: 8px;
|
|
134
|
-
min-width: 220px;
|
|
135
|
-
text-align: center;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
.cio-flow-diagram .flow-box--consumer {
|
|
139
|
-
background: #eff6ff;
|
|
140
|
-
border: 1px solid #bfdbfe;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
.cio-flow-diagram .flow-box--internal {
|
|
144
|
-
background: #f5f3ff;
|
|
145
|
-
border: 1px solid #ddd6fe;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
.cio-flow-diagram .flow-box--output {
|
|
149
|
-
background: #f0fdf4;
|
|
150
|
-
border: 1px solid #bbf7d0;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
.cio-flow-diagram .flow-arrow {
|
|
154
|
-
margin-left: 110px;
|
|
155
|
-
color: #9ca3af;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
.cio-flow-diagram .flow-label {
|
|
159
|
-
font-size: 12px;
|
|
160
|
-
color: #9ca3af;
|
|
161
|
-
font-style: italic;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
.cio-flow-diagram .flow-callback {
|
|
165
|
-
font-size: 12px;
|
|
166
|
-
color: #059669;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
.cio-flow-diagram .flow-box-subtitle {
|
|
170
|
-
font-size: 12px;
|
|
171
|
-
color: #6b7280;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
.cio-flow-legend {
|
|
175
|
-
display: flex;
|
|
176
|
-
gap: 16px;
|
|
177
|
-
margin: 32px 0 16px;
|
|
178
|
-
font-size: 12px;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
.cio-flow-legend .legend-swatch {
|
|
182
|
-
display: inline-block;
|
|
183
|
-
width: 12px;
|
|
184
|
-
height: 12px;
|
|
185
|
-
border-radius: 3px;
|
|
186
|
-
margin-right: 4px;
|
|
187
|
-
vertical-align: middle;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
.cio-flow-legend .legend-swatch--consumer {
|
|
191
|
-
background: #eff6ff;
|
|
192
|
-
border: 1px solid #bfdbfe;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
.cio-flow-legend .legend-swatch--internal {
|
|
196
|
-
background: #f5f3ff;
|
|
197
|
-
border: 1px solid #ddd6fe;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
.cio-flow-legend .legend-swatch--output {
|
|
201
|
-
background: #f0fdf4;
|
|
202
|
-
border: 1px solid #bbf7d0;
|
|
203
|
-
}
|
|
@@ -4,25 +4,71 @@ exports.renderMarkdown = exports.sanitizeHtml = void 0;
|
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const dompurify_1 = tslib_1.__importDefault(require("dompurify"));
|
|
6
6
|
const marked_1 = require("marked");
|
|
7
|
+
// For catalog HTML (product descriptions).
|
|
7
8
|
const purifyConfig = {
|
|
8
9
|
FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'form'],
|
|
9
10
|
};
|
|
11
|
+
// For the answer, which an LLM writes from text a shopper can steer, so only the tags `marked`
|
|
12
|
+
// outputs are kept. Images, media and SVG would load a URL with no click and could leak data;
|
|
13
|
+
// form controls and `style` could fake a form.
|
|
14
|
+
const markdownPurifyConfig = {
|
|
15
|
+
ALLOWED_TAGS: 'p br strong em del a ul ol li blockquote hr code pre table thead tbody tr th td h1 h2 h3 h4 h5 h6'.split(' '),
|
|
16
|
+
ALLOWED_ATTR: ['href', 'align', 'start'],
|
|
17
|
+
};
|
|
18
|
+
// Text the agent reads (catalog, knowledge base, reviews) could plant a phishing link, so only
|
|
19
|
+
// links to this site, its subdomains, email and phone stay clickable. A link a custom `sanitize`
|
|
20
|
+
// deliberately keeps (e.g. `sms:`) stays too, but web links to other sites never do.
|
|
21
|
+
function isOnThisSite(href, { keepOtherSchemes = false } = {}) {
|
|
22
|
+
try {
|
|
23
|
+
const url = new URL(href, window.location.href);
|
|
24
|
+
if (url.protocol === 'mailto:' || url.protocol === 'tel:')
|
|
25
|
+
return true;
|
|
26
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
27
|
+
return keepOtherSchemes;
|
|
28
|
+
const site = window.location.hostname.replace(/^www\./, '');
|
|
29
|
+
return url.hostname === site || url.hostname.endsWith(`.${site}`);
|
|
30
|
+
}
|
|
31
|
+
catch (_a) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function unlinkOffSiteLinks(html, options) {
|
|
36
|
+
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
37
|
+
doc.querySelectorAll('a[href]').forEach((link) => {
|
|
38
|
+
if (!isOnThisSite(link.getAttribute('href'), options)) {
|
|
39
|
+
link.replaceWith(...Array.from(link.childNodes));
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
return doc.body.innerHTML;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Sanitizes catalog HTML such as a product description. Browser only: without a DOM to sanitize
|
|
46
|
+
* with (server rendering) it returns `''`.
|
|
47
|
+
*/
|
|
10
48
|
function sanitizeHtml(html) {
|
|
11
|
-
if (!html)
|
|
49
|
+
if (!html || !dompurify_1.default.isSupported)
|
|
12
50
|
return '';
|
|
13
51
|
return dompurify_1.default.sanitize(html, purifyConfig);
|
|
14
52
|
}
|
|
15
53
|
exports.sanitizeHtml = sanitizeHtml;
|
|
54
|
+
/**
|
|
55
|
+
* Turns the answer's markdown into sanitized HTML. Only the tags markdown produces are kept, and
|
|
56
|
+
* only links to this site, its subdomains, `mailto:` and `tel:` stay clickable. A custom `sanitize`
|
|
57
|
+
* can keep other schemes, but web links to other sites always become plain text. Browser only:
|
|
58
|
+
* without a DOM to sanitize with (server rendering) it returns `''`, so render the plain text.
|
|
59
|
+
*/
|
|
16
60
|
function renderMarkdown(content, options) {
|
|
17
|
-
var _a;
|
|
18
|
-
if (!content)
|
|
61
|
+
var _a, _b;
|
|
62
|
+
if (!content || !dompurify_1.default.isSupported)
|
|
19
63
|
return '';
|
|
20
64
|
const html = marked_1.marked.parse(content, { async: false, breaks: true });
|
|
21
65
|
if (options === null || options === void 0 ? void 0 : options.sanitize) {
|
|
22
66
|
const purifier = (0, dompurify_1.default)(window);
|
|
23
|
-
const config = Object.assign(Object.assign({},
|
|
24
|
-
return options.sanitize(purifier, html, { config })
|
|
67
|
+
const config = Object.assign(Object.assign({}, markdownPurifyConfig), { ALLOWED_TAGS: [...((_a = markdownPurifyConfig.ALLOWED_TAGS) !== null && _a !== void 0 ? _a : [])], ALLOWED_ATTR: [...((_b = markdownPurifyConfig.ALLOWED_ATTR) !== null && _b !== void 0 ? _b : [])] });
|
|
68
|
+
return unlinkOffSiteLinks(options.sanitize(purifier, html, { config }), {
|
|
69
|
+
keepOtherSchemes: true,
|
|
70
|
+
});
|
|
25
71
|
}
|
|
26
|
-
return dompurify_1.default.sanitize(html,
|
|
72
|
+
return unlinkOffSiteLinks(dompurify_1.default.sanitize(html, markdownPurifyConfig));
|
|
27
73
|
}
|
|
28
74
|
exports.renderMarkdown = renderMarkdown;
|
package/lib/cjs/version.js
CHANGED
|
@@ -5,7 +5,7 @@ function Answer({ text, componentOverride }) {
|
|
|
5
5
|
if (!text) {
|
|
6
6
|
return null;
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
const html = renderMarkdown(text);
|
|
9
|
+
return (React.createElement(RenderPropsWrapper, { props: { text }, override: componentOverride?.reactNode }, html ? (React.createElement("div", { className: 'cio-pia-answer', "data-testid": 'answer-text', dangerouslySetInnerHTML: { __html: html } })) : (React.createElement("div", { className: 'cio-pia-answer', "data-testid": 'answer-text' }, text))));
|
|
10
10
|
}
|
|
11
11
|
export default Answer;
|
|
@@ -9,6 +9,7 @@ import usePersistedConversation from '../../hooks/usePersistedConversation';
|
|
|
9
9
|
import useTracking from '../../hooks/useTracking';
|
|
10
10
|
import useViewportTracking from '../../hooks/useViewportTracking';
|
|
11
11
|
import useViewportCallbacks from '../../hooks/useViewportCallbacks';
|
|
12
|
+
import useReturnFocusToInput from '../../hooks/useReturnFocusToInput';
|
|
12
13
|
import ErrorBlock from '../Error/ErrorBlock';
|
|
13
14
|
import LoadingSkeleton from '../LoadingSkeleton/LoadingSkeleton';
|
|
14
15
|
import { translate } from '../../utils/translate';
|
|
@@ -60,6 +61,15 @@ export default function CioPiaQna(props) {
|
|
|
60
61
|
initialConversationHistory,
|
|
61
62
|
persisted,
|
|
62
63
|
});
|
|
64
|
+
const { inputRef, returnFocusAfterLoading } = useReturnFocusToInput(isLoading);
|
|
65
|
+
const handleInlineSubmit = useCallback((question) => {
|
|
66
|
+
handleSubmitQuestion(question);
|
|
67
|
+
returnFocusAfterLoading();
|
|
68
|
+
}, [handleSubmitQuestion, returnFocusAfterLoading]);
|
|
69
|
+
const handleInlineQuestionClick = useCallback((question) => {
|
|
70
|
+
handleQuestionClick(question);
|
|
71
|
+
returnFocusAfterLoading();
|
|
72
|
+
}, [handleQuestionClick, returnFocusAfterLoading]);
|
|
63
73
|
const { containerRef: viewportContainerRef } = useViewportTracking({
|
|
64
74
|
tracking,
|
|
65
75
|
questions: displayedQuestions,
|
|
@@ -123,12 +133,12 @@ export default function CioPiaQna(props) {
|
|
|
123
133
|
return (React.createElement("div", { ref: containerRef, className: 'cio-pia-container', "data-testid": 'cio-pia-container' },
|
|
124
134
|
React.createElement(RenderPropsWrapper, { props: renderProps, override: children || componentOverrides?.reactNode },
|
|
125
135
|
React.createElement("p", { className: 'cio-pia-title', "data-testid": 'cio-pia-title' }, translate('Any questions about this product?', translations)),
|
|
126
|
-
React.createElement(Input, { onSubmit:
|
|
136
|
+
React.createElement(Input, { onSubmit: handleInlineSubmit, onFocus: handleInputFocus, inputRef: inputRef, disabled: isLoading, value: currentQuestion, translations: translations, componentOverride: componentOverrides?.input }),
|
|
127
137
|
isAnswerLoading && React.createElement(LoadingSkeleton, { componentOverride: componentOverrides?.loading }),
|
|
128
138
|
!isAnswerLoading && error && (React.createElement(ErrorBlock, { key: error.message || translate('Unexpected error', translations), message: error.message, translations: translations })),
|
|
129
139
|
!isAnswerLoading && !error && currentAnswer && (React.createElement(PiaInlineAnswer, { currentAnswer: currentAnswer, currentItems: currentItems, showFeedback: showFeedback, learnMoreUrl: learnMoreUrl, disclaimerPosition: disclaimerPosition, translations: translations, callbacks: callbacks, componentOverrides: componentOverrides, onFeedback: handleFeedback, onResultClick: tracking.trackResultClick, question: currentQuestion, qnaResultId: qnaResultId, priceCurrency: priceCurrency })),
|
|
130
140
|
isLoading && React.createElement(SuggestedQuestionsSkeleton, null),
|
|
131
|
-
!isLoading && !error && (React.createElement(SuggestedQuestionsContainer, { questions: displayedQuestions, onQuestionClick:
|
|
141
|
+
!isLoading && !error && (React.createElement(SuggestedQuestionsContainer, { questions: displayedQuestions, onQuestionClick: handleInlineQuestionClick, componentOverride: componentOverrides?.suggestedQuestions })),
|
|
132
142
|
checkoutElement),
|
|
133
143
|
React.createElement(StatusRegion, { message: answerStatus, "data-testid": 'answer-status' })));
|
|
134
144
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
2
|
import Input from '../Input/Input';
|
|
3
3
|
import SuggestedQuestion from '../SuggestedQuestion/SuggestedQuestion';
|
|
4
4
|
import { SparklesIcon } from '../icons';
|
|
5
5
|
import RecsOptionsSkeleton from './RecsOptionsSkeleton';
|
|
6
6
|
import { resolveRefinementQuestion } from '../../utils/recs';
|
|
7
7
|
import { RECS_INPUT_PLACEHOLDER } from '../../constants';
|
|
8
|
+
import useReturnFocusToInput from '../../hooks/useReturnFocusToInput';
|
|
8
9
|
/**
|
|
9
10
|
* The row under the products: a prompt line, the short options the API suggested, and a box for
|
|
10
11
|
* anything it did not think of. The prompt line is held still while the rest reloads, so the row
|
|
@@ -12,11 +13,20 @@ import { RECS_INPUT_PLACEHOLDER } from '../../constants';
|
|
|
12
13
|
*/
|
|
13
14
|
export default function RecsPodRefinement({ refinement, isLoading, inputError, showInput, translations, componentOverrides, onRefine, onSubmit, onInputFocus, }) {
|
|
14
15
|
const options = refinement?.options || [];
|
|
16
|
+
const { inputRef, returnFocusAfterLoading } = useReturnFocusToInput(isLoading);
|
|
17
|
+
const refine = useCallback((text) => {
|
|
18
|
+
onRefine(text);
|
|
19
|
+
returnFocusAfterLoading();
|
|
20
|
+
}, [onRefine, returnFocusAfterLoading]);
|
|
21
|
+
const submit = useCallback((value) => {
|
|
22
|
+
onSubmit(value);
|
|
23
|
+
returnFocusAfterLoading();
|
|
24
|
+
}, [onSubmit, returnFocusAfterLoading]);
|
|
15
25
|
return (React.createElement("div", { className: 'cio-pia-recs-pod__refinement' },
|
|
16
26
|
React.createElement("span", { className: 'cio-pia-recs-pod__refinement-label' }, resolveRefinementQuestion(refinement, translations)),
|
|
17
|
-
isLoading ? (React.createElement(RecsOptionsSkeleton, { count: options.length || undefined })) : (options.map((option) => (React.createElement(SuggestedQuestion, { key: option, question: option, icon: React.createElement(SparklesIcon, null), onClick: () =>
|
|
27
|
+
isLoading ? (React.createElement(RecsOptionsSkeleton, { count: options.length || undefined })) : (options.map((option) => (React.createElement(SuggestedQuestion, { key: option, question: option, icon: React.createElement(SparklesIcon, null), onClick: () => refine(option) })))),
|
|
18
28
|
showInput && (React.createElement("div", { className: 'cio-pia-recs-pod__input' },
|
|
19
|
-
React.createElement(Input, { onSubmit:
|
|
29
|
+
React.createElement(Input, { onSubmit: submit, onFocus: onInputFocus, inputRef: inputRef, disabled: isLoading, error: inputError ?? undefined, placeholderKey: RECS_INPUT_PLACEHOLDER,
|
|
20
30
|
// The pod's box sits in a row beside the options and submits on Enter. Q&A keeps
|
|
21
31
|
// its button; this one has none in the mocks.
|
|
22
32
|
showSendButton: false, translations: translations, componentOverride: componentOverrides?.input })))));
|
|
@@ -13,7 +13,7 @@ import { SendIcon } from '../icons';
|
|
|
13
13
|
* across its re-renders.
|
|
14
14
|
*/
|
|
15
15
|
let inputIdCounter = 0;
|
|
16
|
-
function Input({ value: providedValue, placeholder, disabled = false, onSubmit, onFocus, translations, componentOverride, error, placeholderKey = 'Ask anything', showSendButton = true, }) {
|
|
16
|
+
function Input({ value: providedValue, placeholder, disabled = false, onSubmit, onFocus, translations, componentOverride, error, inputRef, placeholderKey = 'Ask anything', showSendButton = true, }) {
|
|
17
17
|
const [value, setValue] = useState(providedValue || '');
|
|
18
18
|
const [errorId] = useState(() => {
|
|
19
19
|
inputIdCounter += 1;
|
|
@@ -50,10 +50,11 @@ function Input({ value: providedValue, placeholder, disabled = false, onSubmit,
|
|
|
50
50
|
onFocus,
|
|
51
51
|
translations,
|
|
52
52
|
error,
|
|
53
|
+
inputRef,
|
|
53
54
|
}, override: componentOverride?.reactNode },
|
|
54
55
|
React.createElement(React.Fragment, null,
|
|
55
56
|
React.createElement("div", { className: 'cio-pia-input-container' },
|
|
56
|
-
React.createElement("input", { type: 'text', value: value, onChange: (e) => setValue(e.target.value), onKeyDown: handleSubmitOnEnter, onFocus: onFocus, placeholder: resolvedPlaceholder, "aria-label": accessibleName, disabled: disabled, className: cx('cio-pia-input', error && 'cio-pia-input--error', !showSendButton && 'cio-pia-input--no-send'), "aria-invalid": error ? true : undefined, "aria-describedby": error ? errorId : undefined }),
|
|
57
|
+
React.createElement("input", { ref: inputRef, type: 'text', value: value, onChange: (e) => setValue(e.target.value), onKeyDown: handleSubmitOnEnter, onFocus: onFocus, placeholder: resolvedPlaceholder, "aria-label": accessibleName, disabled: disabled, className: cx('cio-pia-input', error && 'cio-pia-input--error', !showSendButton && 'cio-pia-input--no-send'), "aria-invalid": error ? true : undefined, "aria-describedby": error ? errorId : undefined }),
|
|
57
58
|
showSendButton && (React.createElement("button", { type: 'button', onClick: () => handleSubmit(value), className: 'cio-pia-send-button', disabled: disabled },
|
|
58
59
|
translate('Send', translations),
|
|
59
60
|
React.createElement(SendIcon, null)))),
|
|
@@ -1,17 +1,33 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useCallback, useContext } from 'react';
|
|
2
2
|
import Input from '../Input/Input';
|
|
3
3
|
import SuggestedQuestionsContainer from '../SuggestedQuestionsContainer/SuggestedQuestionsContainer';
|
|
4
4
|
import SuggestedQuestionsSkeleton from '../SuggestedQuestionsContainer/SuggestedQuestionsSkeleton';
|
|
5
5
|
import { translate } from '../../utils/translate';
|
|
6
|
+
import useReturnFocusToInput from '../../hooks/useReturnFocusToInput';
|
|
7
|
+
import { ModalInputRefContext } from './PiaModal';
|
|
6
8
|
import ConversationHistory from '../ConversationHistory/ConversationHistory';
|
|
7
9
|
export default function PiaConversation({ conversationHistory, isLoading, error, currentItems, showFeedback, showPreviousItems, learnMoreUrl, disclaimerPosition, translations, callbacks, componentOverrides, displayedQuestions, handleSubmitQuestion, handleQuestionClick, containerRef, handleFeedback, onResultClick, qnaResultId, onInputFocus, priceCurrency, checkoutElement, }) {
|
|
8
10
|
const hasHistory = conversationHistory.length > 0;
|
|
11
|
+
const { inputRef, returnFocusAfterLoading } = useReturnFocusToInput(isLoading);
|
|
12
|
+
const modalInputRef = useContext(ModalInputRefContext);
|
|
13
|
+
const attachInput = useCallback((node) => {
|
|
14
|
+
inputRef(node);
|
|
15
|
+
modalInputRef?.(node);
|
|
16
|
+
}, [inputRef, modalInputRef]);
|
|
17
|
+
const submitQuestion = useCallback((question) => {
|
|
18
|
+
handleSubmitQuestion(question);
|
|
19
|
+
returnFocusAfterLoading();
|
|
20
|
+
}, [handleSubmitQuestion, returnFocusAfterLoading]);
|
|
21
|
+
const clickQuestion = useCallback((question) => {
|
|
22
|
+
handleQuestionClick(question);
|
|
23
|
+
returnFocusAfterLoading();
|
|
24
|
+
}, [handleQuestionClick, returnFocusAfterLoading]);
|
|
9
25
|
return (React.createElement("div", { ref: containerRef, className: 'cio-pia-container cio-pia-conversation', "data-testid": 'cio-pia-container' },
|
|
10
26
|
!hasHistory && (React.createElement("p", { className: 'cio-pia-title', "data-testid": 'cio-pia-title' }, translate('Any questions about this product?', translations))),
|
|
11
27
|
React.createElement(ConversationHistory, { conversationHistory: conversationHistory, isLoading: isLoading, error: error, currentItems: currentItems, showFeedback: showFeedback, showPreviousItems: showPreviousItems, learnMoreUrl: learnMoreUrl, disclaimerPosition: disclaimerPosition, translations: translations, callbacks: callbacks, componentOverrides: componentOverrides, handleFeedback: handleFeedback, onResultClick: onResultClick, qnaResultId: qnaResultId, priceCurrency: priceCurrency }),
|
|
12
28
|
checkoutElement && React.createElement("div", { className: 'cio-pia-conversation-checkout' }, checkoutElement),
|
|
13
29
|
React.createElement("div", { className: 'cio-pia-conversation-footer' },
|
|
14
30
|
isLoading && !error && React.createElement(SuggestedQuestionsSkeleton, null),
|
|
15
|
-
!isLoading && !error && (React.createElement(SuggestedQuestionsContainer, { questions: displayedQuestions, onQuestionClick:
|
|
16
|
-
React.createElement(Input, { onSubmit:
|
|
31
|
+
!isLoading && !error && (React.createElement(SuggestedQuestionsContainer, { questions: displayedQuestions, onQuestionClick: clickQuestion, componentOverride: componentOverrides?.suggestedQuestions })),
|
|
32
|
+
React.createElement(Input, { onSubmit: submitQuestion, onFocus: onInputFocus, inputRef: attachInput, disabled: isLoading, translations: translations, componentOverride: componentOverrides?.input }))));
|
|
17
33
|
}
|