@financial-times/qanda-ui 0.0.1-beta.37 → 0.0.1-beta.39
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/components/Calendar/index.js +2 -1
- package/dist/cjs/components/CountdownTimer/index.js +2 -1
- package/dist/cjs/components/EmbedFormField/index.js +7 -1
- package/dist/cjs/components/ErrorMessage/humanReadableError.js +7 -1
- package/dist/cjs/components/ExpertDrawer/index.js +5 -2
- package/dist/cjs/components/FloatingActionBar/index.js +15 -3
- package/dist/cjs/components/MessageBox/index.js +1 -1
- package/dist/cjs/components/Overlay/index.js +19 -2
- package/dist/cjs/components/Qanda.js +1 -7
- package/dist/cjs/components/QandaBlock/index.js +1 -1
- package/dist/cjs/components/QandaContainer.js +3 -2
- package/dist/cjs/components/QandaProvider.js +31 -1
- package/dist/cjs/components/QuestionForm/index.js +42 -37
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/services/comments-api.js +2 -2
- package/dist/esm/components/Calendar/index.js +2 -1
- package/dist/esm/components/CountdownTimer/index.js +3 -2
- package/dist/esm/components/EmbedFormField/index.js +8 -2
- package/dist/esm/components/ErrorMessage/humanReadableError.js +7 -1
- package/dist/esm/components/ExpertDrawer/index.js +5 -2
- package/dist/esm/components/FloatingActionBar/index.js +16 -4
- package/dist/esm/components/MessageBox/index.js +1 -1
- package/dist/esm/components/Overlay/index.js +19 -2
- package/dist/esm/components/Qanda.js +2 -8
- package/dist/esm/components/QandaBlock/index.js +1 -1
- package/dist/esm/components/QandaContainer.js +4 -3
- package/dist/esm/components/QandaProvider.js +30 -1
- package/dist/esm/components/QuestionForm/index.js +42 -37
- package/dist/esm/index.js +1 -1
- package/dist/esm/services/comments-api.js +2 -2
- package/dist/qanda.scss +13 -3
- package/dist/types/components/Overlay/index.d.ts +2 -2
- package/dist/types/components/QandaProvider.d.ts +9 -1
- package/package.json +1 -1
|
@@ -39,7 +39,8 @@ function Calendar() {
|
|
|
39
39
|
className: 'live-qa-calendar-button',
|
|
40
40
|
'aria-pressed': dropDownVisible,
|
|
41
41
|
'aria-haspopup': 'true',
|
|
42
|
-
'aria-
|
|
42
|
+
'aria-expanded': dropDownVisible,
|
|
43
|
+
'aria-controls': 'live-qa-calendar-list-menu',
|
|
43
44
|
id: 'live-qa-calendar-button-menu',
|
|
44
45
|
'data-testid': 'live-qa-calendar-button',
|
|
45
46
|
'data-trackable': (0, tracking_1.trackingNS)('add-to-calendar-button'),
|
|
@@ -4,6 +4,7 @@ exports.formatTime = formatTime;
|
|
|
4
4
|
const jsx_runtime_1 = require("preact/jsx-runtime");
|
|
5
5
|
const react_1 = require("react");
|
|
6
6
|
const QandaProvider_1 = require("../QandaProvider");
|
|
7
|
+
const qanda_1 = require("../../config/qanda");
|
|
7
8
|
const comments_api_1 = require("../../services/comments-api");
|
|
8
9
|
function formatTime(remainingTime) {
|
|
9
10
|
const days = Math.floor(remainingTime / (1000 * 60 * 60 * 24));
|
|
@@ -85,7 +86,7 @@ function CountdownTimer({ container }) {
|
|
|
85
86
|
}, []);
|
|
86
87
|
const portalElement = data &&
|
|
87
88
|
((status === 'pre-live' && startDateTime) ||
|
|
88
|
-
(status === 'live' && endDateTime)) && ((0, jsx_runtime_1.jsx)("div", { className: "qanda-portal countdown-wrapper", children: (0, jsx_runtime_1.jsx)("div", { className: "countdown-block", children: (0, jsx_runtime_1.jsx)("p", { className: "countdown", children: count && ((0, jsx_runtime_1.
|
|
89
|
+
(status === 'live' && endDateTime)) && ((0, jsx_runtime_1.jsx)("div", { className: "qanda-portal countdown-wrapper", children: (0, jsx_runtime_1.jsx)("div", { className: "countdown-block", children: (0, jsx_runtime_1.jsx)("p", { className: "countdown", children: count && ((0, jsx_runtime_1.jsxs)("time", { datetime: status === 'pre-live' ? startDateTime : endDateTime, children: [(0, jsx_runtime_1.jsx)("span", { className: "o3-visually-hidden", children: qanda_1.PRODUCT_NAME }), ' ', count] })) }) }) }));
|
|
89
90
|
return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, react_1.createPortal)(portalElement, portalTarget || document.body) });
|
|
90
91
|
}
|
|
91
92
|
exports.default = CountdownTimer;
|
|
@@ -9,11 +9,14 @@ const react_redux_1 = require("react-redux");
|
|
|
9
9
|
const questionSlice_1 = require("../../store/questionSlice");
|
|
10
10
|
const react_1 = require("react");
|
|
11
11
|
const react_dom_1 = require("react-dom");
|
|
12
|
+
const QandaProvider_1 = require("../QandaProvider");
|
|
12
13
|
const qanda_1 = require("../../config/qanda");
|
|
13
14
|
const tracking_1 = require("../../utils/tracking");
|
|
14
15
|
function EmbedFormField({ container, status = 'pre-live', }) {
|
|
15
16
|
const dispatch = (0, react_redux_1.useDispatch)();
|
|
16
17
|
const [portalTarget, setPortalTarget] = (0, react_1.useState)();
|
|
18
|
+
const modalOpenerRef = (0, react_1.useRef)(null);
|
|
19
|
+
const { setDialogOpener } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
|
|
17
20
|
(0, react_1.useEffect)(() => {
|
|
18
21
|
if (container) {
|
|
19
22
|
const portalContainer = document.createElement('div');
|
|
@@ -25,7 +28,10 @@ function EmbedFormField({ container, status = 'pre-live', }) {
|
|
|
25
28
|
};
|
|
26
29
|
}
|
|
27
30
|
}, []);
|
|
28
|
-
const portalElement = ((0, jsx_runtime_1.jsxs)("div", { className: "qanda-portal embed-form-field__container o3-type-body-highlight", children: ["Submit your question for ", qanda_1.PRODUCT_NAME, (0, jsx_runtime_1.jsxs)("
|
|
31
|
+
const portalElement = ((0, jsx_runtime_1.jsxs)("div", { className: "qanda-portal embed-form-field__container o3-type-body-highlight", children: ["Submit your question for ", qanda_1.PRODUCT_NAME, (0, jsx_runtime_1.jsxs)("button", { onClick: () => {
|
|
32
|
+
dispatch((0, questionSlice_1.openForm)());
|
|
33
|
+
setDialogOpener?.(modalOpenerRef.current);
|
|
34
|
+
}, className: "embed-form-field__text-input o3-type-body-lg", "data-testid": "embed-form-field", "data-trackable": (0, tracking_1.trackingNS)(`embed-form-field-${status}`), "data-component": "embed-form-field", "aria-haspopup": "dialog", ref: modalOpenerRef, type: "button", children: [(0, jsx_runtime_1.jsx)("span", { className: "embed-form-field__icon", children: (0, jsx_runtime_1.jsx)("i", { style: { maskImage: 'var(--o3-icon-user)' } }) }), "Enter your question"] })] }));
|
|
29
35
|
return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, react_dom_1.createPortal)(portalElement, portalTarget || document.body) });
|
|
30
36
|
}
|
|
31
37
|
exports.default = EmbedFormField;
|
|
@@ -17,6 +17,10 @@ const baseErrorMessages = {
|
|
|
17
17
|
title: 'Timeout error',
|
|
18
18
|
message: 'The request timed out. You may be on a slow connection. Please try again later.',
|
|
19
19
|
},
|
|
20
|
+
maintenanceMode: {
|
|
21
|
+
title: 'Sorry — this "Ask an Expert" page isn’t loading',
|
|
22
|
+
message: 'We’re working to fix the problem. Please check back soon.',
|
|
23
|
+
},
|
|
20
24
|
};
|
|
21
25
|
class HumanReadableError {
|
|
22
26
|
constructor(error, customErrorMessages = {}) {
|
|
@@ -33,7 +37,9 @@ class HumanReadableError {
|
|
|
33
37
|
this._humanReadableError = errorMessages.connectionError;
|
|
34
38
|
}
|
|
35
39
|
else if (typeof error.status === 'number' && error.status >= 500) {
|
|
36
|
-
|
|
40
|
+
const typedError = error;
|
|
41
|
+
this._humanReadableError =
|
|
42
|
+
errorMessages[typedError.data?.error?.errorCode] || errorMessages.serverError;
|
|
37
43
|
}
|
|
38
44
|
else if (typeof error.status === 'number' && error.status === 400) {
|
|
39
45
|
// Assuming as an error code otherwise will throw an error and we will fallback to generic error
|
|
@@ -14,7 +14,7 @@ const Overlay_1 = __importDefault(require("../Overlay"));
|
|
|
14
14
|
const qanda_1 = require("../../config/qanda");
|
|
15
15
|
function ExpertDrawer() {
|
|
16
16
|
const dispatch = (0, react_redux_1.useDispatch)();
|
|
17
|
-
const { storyId, useStaging, title } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
|
|
17
|
+
const { storyId, useStaging, title, focusDialogOpener } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
|
|
18
18
|
const { isExpertFormOpen, expertView } = (0, react_redux_1.useSelector)((state) => state.expert);
|
|
19
19
|
(0, react_1.useEffect)(() => {
|
|
20
20
|
if (isExpertFormOpen) {
|
|
@@ -29,7 +29,10 @@ function ExpertDrawer() {
|
|
|
29
29
|
if (!isExpertFormOpen) {
|
|
30
30
|
return null;
|
|
31
31
|
}
|
|
32
|
-
return ((0, jsx_runtime_1.jsx)(Overlay_1.default, { title: qanda_1.PRODUCT_NAME, closeCallback: () =>
|
|
32
|
+
return ((0, jsx_runtime_1.jsx)(Overlay_1.default, { title: qanda_1.PRODUCT_NAME, closeCallback: () => {
|
|
33
|
+
dispatch((0, expertSlice_1.closeExpertForm)());
|
|
34
|
+
focusDialogOpener?.();
|
|
35
|
+
}, options: {
|
|
33
36
|
shadow: true,
|
|
34
37
|
allowInteraction: true,
|
|
35
38
|
}, children: (0, jsx_runtime_1.jsx)("div", { className: "o-comments", id: "o-comments-stream", "data-o-component": "o-comments", "data-o-comments-article-id": storyId, "data-o-comments-article-url": `https://www.ft.com/content/${storyId}`, "data-o-comments-use-staging-environment": useStaging, "data-o-comments-title": title }) }));
|
|
@@ -17,7 +17,8 @@ function LiveIndicator() {
|
|
|
17
17
|
function FloatingActionBar({ isLive = true }) {
|
|
18
18
|
const dispatch = (0, react_redux_1.useDispatch)();
|
|
19
19
|
const isMobile = (0, useIsMobile_1.useIsMobile)();
|
|
20
|
-
const { storyId, useStaging, commentsAPIUrl } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
|
|
20
|
+
const { storyId, useStaging, commentsAPIUrl, setDialogOpener } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
|
|
21
|
+
const modalOpenerRef = (0, react_1.useRef)(null);
|
|
21
22
|
const updatedComments = (0, comments_api_1.useUpdatedComments)({
|
|
22
23
|
storyId,
|
|
23
24
|
useStaging,
|
|
@@ -30,6 +31,8 @@ function FloatingActionBar({ isLive = true }) {
|
|
|
30
31
|
// Scrolling to start of qanda stream title
|
|
31
32
|
const element = document.getElementById('qanda__stream-title');
|
|
32
33
|
if (element) {
|
|
34
|
+
element.setAttribute('tabindex', '-1'); // Make it focusable
|
|
35
|
+
element.focus();
|
|
33
36
|
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
34
37
|
}
|
|
35
38
|
};
|
|
@@ -37,7 +40,15 @@ function FloatingActionBar({ isLive = true }) {
|
|
|
37
40
|
const userConfig = {
|
|
38
41
|
description: 'Ask a question',
|
|
39
42
|
testIdSuffix: 'ask-a-question-button',
|
|
40
|
-
onClick: () =>
|
|
43
|
+
onClick: () => {
|
|
44
|
+
// modalOpener set on div as Button does not support refs, therefore we need to find the button inside it
|
|
45
|
+
const { current: modalOpener } = modalOpenerRef;
|
|
46
|
+
const openerButton = modalOpener?.querySelector('button');
|
|
47
|
+
if (openerButton) {
|
|
48
|
+
setDialogOpener?.(openerButton);
|
|
49
|
+
}
|
|
50
|
+
dispatch((0, questionSlice_1.openForm)());
|
|
51
|
+
},
|
|
41
52
|
dataTrackable: 'open_question_form',
|
|
42
53
|
};
|
|
43
54
|
const expertConfig = {
|
|
@@ -47,11 +58,12 @@ function FloatingActionBar({ isLive = true }) {
|
|
|
47
58
|
dataTrackable: 'open_expert_drawer',
|
|
48
59
|
};
|
|
49
60
|
const view = showExpertView ? expertConfig : userConfig;
|
|
50
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "floating-action-bar__container", "data-trackable": (0, tracking_1.trackingNS)('floating-action-bar'), children: [(0, jsx_runtime_1.jsx)("div", { className: "floating-action-bar__fade-overlay" }), (0, jsx_runtime_1.jsxs)("div", { className: "floating-action-bar", "data-testid": "floating-action-bar", children: [isLive && (0, jsx_runtime_1.jsx)(LiveIndicator, {}), (0, jsx_runtime_1.jsxs)("div", { className: "floating-action-bar__buttons", children: [(0, jsx_runtime_1.jsx)(o3_button_1.Button, { attributes: {
|
|
61
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "floating-action-bar__container", "data-trackable": (0, tracking_1.trackingNS)('floating-action-bar'), children: [(0, jsx_runtime_1.jsx)("div", { className: "floating-action-bar__fade-overlay" }), (0, jsx_runtime_1.jsxs)("div", { className: "floating-action-bar", "data-testid": "floating-action-bar", children: [(0, jsx_runtime_1.jsx)("span", { className: "o3-visually-hidden", "aria-live": "polite", role: "alert", children: isLive ? 'This session is now live' : '' }), isLive && (0, jsx_runtime_1.jsx)(LiveIndicator, {}), (0, jsx_runtime_1.jsxs)("div", { className: "floating-action-bar__buttons", ref: modalOpenerRef, children: [(0, jsx_runtime_1.jsx)(o3_button_1.Button, { attributes: {
|
|
51
62
|
ariaLabel: view.description,
|
|
52
63
|
'data-trackable': (0, tracking_1.trackingNS)(view.dataTrackable),
|
|
53
64
|
'data-trackable-context-action': view.description,
|
|
54
65
|
'data-testid': `floating-action-bar__${view.testIdSuffix}`,
|
|
66
|
+
ariaHasPopup: 'dialog',
|
|
55
67
|
}, label: view.description, type: "ghost", theme: "mono", icon: "edit", iconOnly: isMobile, onClick: view.onClick }), (0, jsx_runtime_1.jsx)(o3_button_1.Button, { attributes: {
|
|
56
68
|
ariaLabel: 'Back to top',
|
|
57
69
|
'data-trackable': (0, tracking_1.trackingNS)('back_to_top'),
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const jsx_runtime_1 = require("preact/jsx-runtime");
|
|
4
4
|
function MessageBox({ color, extraClassNames = '', text, title, id = '', }) {
|
|
5
|
-
return ((0, jsx_runtime_1.jsx)("div", {
|
|
5
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: `o-message o-message--inner message-box__container ${extraClassNames}`, "data-o-message-close": "false", "data-o-component": "o-message", id: id, children: (0, jsx_runtime_1.jsx)("div", { className: "o-message__container", children: (0, jsx_runtime_1.jsxs)("div", { className: "message-box__content", children: [(0, jsx_runtime_1.jsx)("span", { style: { color }, className: "o3-type-body-highlight", children: title }), (0, jsx_runtime_1.jsx)("span", { style: { color }, className: "o3-type-body-base", children: text })] }) }) }));
|
|
6
6
|
}
|
|
7
7
|
exports.default = MessageBox;
|
|
@@ -4,12 +4,29 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const jsx_runtime_1 = require("preact/jsx-runtime");
|
|
7
|
+
const react_1 = require("react");
|
|
7
8
|
const o3_button_1 = require("@financial-times/o3-button");
|
|
8
9
|
const classnames_1 = __importDefault(require("classnames"));
|
|
9
10
|
function Overlay({ title, children, closeCallback, options = {}, }) {
|
|
10
|
-
|
|
11
|
+
// Trap keyboard focus within the overlay for accessibility
|
|
12
|
+
const trapFocus = (event) => {
|
|
13
|
+
if (event.key !== 'Tab')
|
|
14
|
+
return;
|
|
15
|
+
if (event.target === endFocusBtn.current) {
|
|
16
|
+
titleRef.current?.focus();
|
|
17
|
+
}
|
|
18
|
+
else if (event.target === titleRef.current && event.shiftKey) {
|
|
19
|
+
endFocusBtn.current?.focus();
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const titleRef = (0, react_1.useRef)(null);
|
|
23
|
+
const endFocusBtn = (0, react_1.useRef)(null);
|
|
24
|
+
(0, react_1.useEffect)(() => {
|
|
25
|
+
titleRef.current?.focus();
|
|
26
|
+
}, []);
|
|
27
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "overlay", role: "dialog", "aria-modal": "true", "aria-labelledby": "overlay-title", children: [!options.allowInteraction && (0, jsx_runtime_1.jsx)("div", { className: "overlay__outer" }), (0, jsx_runtime_1.jsxs)("div", { className: (0, classnames_1.default)([
|
|
11
28
|
'overlay__inner',
|
|
12
29
|
{ 'overlay__inner--shadow': options.shadow },
|
|
13
|
-
]), children: [(0, jsx_runtime_1.jsx)("div", { className: "overlay__header", children: (0, jsx_runtime_1.jsxs)("div", { className: "overlay__header-bar", children: [(0, jsx_runtime_1.jsx)("span", { className: "o3-type-body-highlight overlay__header-title", children: title }), (0, jsx_runtime_1.jsx)(o3_button_1.Button, { attributes: { className: 'overlay__header-close' }, iconOnly: true, icon: "cross", label: "Close", type: "ghost", theme: "mono", onClick: () => closeCallback() })] }) }), (0, jsx_runtime_1.jsx)("div", { className: "overlay__content", children: children })] })] }));
|
|
30
|
+
]), children: [(0, jsx_runtime_1.jsx)("div", { className: "overlay__header", children: (0, jsx_runtime_1.jsxs)("div", { className: "overlay__header-bar", children: [(0, jsx_runtime_1.jsx)("span", { className: "o3-type-body-highlight overlay__header-title", id: "overlay-title", tabIndex: 0, ref: titleRef, onKeyDown: trapFocus, children: title }), (0, jsx_runtime_1.jsx)(o3_button_1.Button, { attributes: { className: 'overlay__header-close' }, iconOnly: true, icon: "cross", label: "Close", type: "ghost", theme: "mono", onClick: () => closeCallback() })] }) }), (0, jsx_runtime_1.jsx)("div", { className: "overlay__content", children: children })] }), (0, jsx_runtime_1.jsx)("button", { className: "o3-visually-hidden", "aria-label": "Close", onClick: () => closeCallback(), ref: endFocusBtn, onKeyDown: trapFocus, type: "button", children: "Close" })] }));
|
|
14
31
|
}
|
|
15
32
|
exports.default = Overlay;
|
|
@@ -26,12 +26,6 @@ function Qanda({ storyId, useStaging, title, commentsAPIHost, commentsAPIVersion
|
|
|
26
26
|
console.error('Missing required props');
|
|
27
27
|
return null;
|
|
28
28
|
}
|
|
29
|
-
return ((0, jsx_runtime_1.jsx)(react_redux_1.Provider, { store: store_1.store, children: (0, jsx_runtime_1.jsx)(QandaProvider_1.
|
|
30
|
-
storyId,
|
|
31
|
-
useStaging,
|
|
32
|
-
commentsAPIUrl: cleanCommentsAPIUrl,
|
|
33
|
-
title,
|
|
34
|
-
maxQuestionLength,
|
|
35
|
-
}, children: (0, jsx_runtime_1.jsx)(QandaContainer_1.default, { embedFormFieldContainer: embedFormFieldContainer, countdownTimerContainer: countdownTimerContainer }) }) }));
|
|
29
|
+
return ((0, jsx_runtime_1.jsx)(react_redux_1.Provider, { store: store_1.store, children: (0, jsx_runtime_1.jsx)(QandaProvider_1.QandaProvider, { storyId: storyId, useStaging: useStaging, commentsAPIUrl: cleanCommentsAPIUrl, title: title, maxQuestionLength: maxQuestionLength, children: (0, jsx_runtime_1.jsx)(QandaContainer_1.default, { embedFormFieldContainer: embedFormFieldContainer, countdownTimerContainer: countdownTimerContainer }) }) }));
|
|
36
30
|
}
|
|
37
31
|
exports.default = Qanda;
|
|
@@ -74,6 +74,6 @@ function QandaBlock({ id, body, children, byline, summary, showAnswerTime = fals
|
|
|
74
74
|
observer.disconnect();
|
|
75
75
|
};
|
|
76
76
|
}, []);
|
|
77
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "qanda-block", children: [(0, jsx_runtime_1.jsxs)("div", { className: "qanda-block__comment qanda-block__question", id: id, "data-testid": `qanda-block__question-${id}`, "data-trackable": (0, tracking_1.trackingNS)('question'), children: [summary && ((0, jsx_runtime_1.jsx)("div", { className: "o3-type-headline-sm qanda-block__question-summary", children: summary })), (0, jsx_runtime_1.
|
|
77
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "qanda-block", children: [(0, jsx_runtime_1.jsxs)("div", { className: "qanda-block__comment qanda-block__question", id: id, "data-testid": `qanda-block__question-${id}`, "data-trackable": (0, tracking_1.trackingNS)('question'), children: [summary && ((0, jsx_runtime_1.jsx)("div", { className: "o3-type-headline-sm qanda-block__question-summary", children: summary })), (0, jsx_runtime_1.jsxs)("div", { className: "o3-type-body-highlight qanda-block__comment-byline", children: [(0, jsx_runtime_1.jsx)("span", { className: "o3-visually-hidden", children: "Question by " }), (0, jsx_runtime_1.jsx)(Byline_1.default, { structuredContent: byline, showEditedBy: false })] }), (0, jsx_runtime_1.jsx)("div", { className: "o3-type-body-content-base qanda-block__comment-text", children: (0, jsx_runtime_1.jsx)(index_1.default, { className: "qanda-block__text", expandLabel: "Expand question", collapseLabel: "Collapse question", id: id, children: (0, jsx_runtime_1.jsx)(RichText_1.default, { structuredContent: body.structured }) }) })] }, id), children?.map((child) => child.body && ((0, jsx_runtime_1.jsxs)("div", { className: "qanda-block__comment qanda-block__answer", "data-testid": `qanda-block__answer-${child.id}`, "data-trackable": (0, tracking_1.trackingNS)('answer'), children: [(0, jsx_runtime_1.jsxs)("div", { className: "qanda-block__answer-header", children: [(0, jsx_runtime_1.jsxs)("div", { className: "o3-type-body-highlight qanda-block__comment-byline", children: [(0, jsx_runtime_1.jsx)("span", { className: "o3-visually-hidden", children: "Reply from " }), child.author ? ((0, jsx_runtime_1.jsx)(Author_1.default, { byline: child.byline, headshot: child.author.headshot, prefLabel: child.author.prefLabel, role: child.author.role, streamPage: child.author.streamPage })) : ((0, jsx_runtime_1.jsx)(Byline_1.default, { structuredContent: child.byline, showEditedBy: false }))] }), showAnswerTime && ((0, jsx_runtime_1.jsxs)("span", { className: "o3-type-detail", children: [(0, jsx_runtime_1.jsx)("span", { className: "o3-visually-hidden", children: "Published at " }), renderTime(child.publishedDate)] }))] }), (0, jsx_runtime_1.jsx)("div", { className: "o3-type-body-content-base qanda-block__comment-text", children: (0, jsx_runtime_1.jsx)(index_1.default, { className: "qanda-block__text", expandLabel: "Expand answer", collapseLabel: "Collapse answer", id: id, children: (0, jsx_runtime_1.jsx)(RichText_1.default, { structuredContent: child.body.structured }) }) })] }, child.id)))] }));
|
|
78
78
|
}
|
|
79
79
|
exports.default = QandaBlock;
|
|
@@ -16,6 +16,7 @@ const Calendar_1 = __importDefault(require("./Calendar"));
|
|
|
16
16
|
const CountdownTimer_1 = __importDefault(require("./CountdownTimer"));
|
|
17
17
|
const FloatingActionBar_1 = __importDefault(require("./FloatingActionBar"));
|
|
18
18
|
const EmbedFormField_1 = __importDefault(require("./EmbedFormField"));
|
|
19
|
+
const ErrorMessage_1 = __importDefault(require("./ErrorMessage"));
|
|
19
20
|
const QandaProvider_1 = require("./QandaProvider");
|
|
20
21
|
const comments_api_1 = require("../services/comments-api");
|
|
21
22
|
const qanda_1 = require("../config/qanda");
|
|
@@ -47,7 +48,7 @@ function QandaContainer({ embedFormFieldContainer, countdownTimerContainer, }) {
|
|
|
47
48
|
});
|
|
48
49
|
const status = updatedQA?.status || data?.status || 'pre-live';
|
|
49
50
|
const profile = (0, react_redux_1.useSelector)((state) => state.user.profile);
|
|
50
|
-
const { data: roleData } = (0, comments_api_1.useGetUserRolesQuery)({
|
|
51
|
+
const { data: roleData, error: roleError } = (0, comments_api_1.useGetUserRolesQuery)({
|
|
51
52
|
storyId,
|
|
52
53
|
useStaging,
|
|
53
54
|
commentsAPIUrl,
|
|
@@ -67,6 +68,6 @@ function QandaContainer({ embedFormFieldContainer, countdownTimerContainer, }) {
|
|
|
67
68
|
dispatch(comments_api_1.nextCommentsApi.util.resetApiState());
|
|
68
69
|
};
|
|
69
70
|
}, [dispatch, storyId]);
|
|
70
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "qanda-container", children: [error && ((0, jsx_runtime_1.
|
|
71
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "qanda-container", children: [error && ((0, jsx_runtime_1.jsx)("div", { style: { marginBottom: '16px' }, children: (0, jsx_runtime_1.jsx)(ErrorMessage_1.default, { error: error, storyId: storyId, operation: "stream-load" }) })), roleError && !error && ((0, jsx_runtime_1.jsx)("div", { style: { marginBottom: '16px' }, children: (0, jsx_runtime_1.jsx)(ErrorMessage_1.default, { error: roleError, storyId: storyId, operation: "profile-load" }) })), !isLoading && !error && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [status !== 'close' && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [showExpertView ? (0, jsx_runtime_1.jsx)(ExpertDrawer_1.default, {}) : (0, jsx_runtime_1.jsx)(QuestionForm_1.default, {}), (0, jsx_runtime_1.jsx)(CountdownTimer_1.default, { container: countdownTimerContainer })] })), status === 'pre-live' && (0, jsx_runtime_1.jsx)(Calendar_1.default, {}), (0, jsx_runtime_1.jsx)(Stream_1.default, { status: status }), (0, jsx_runtime_1.jsx)(Survey_1.default, { storyId: storyId, status: status }), (status === 'live' || (status === 'pre-live' && showExpertView)) && ((0, jsx_runtime_1.jsx)(FloatingActionBar_1.default, { isLive: status === 'live' })), status !== 'close' && !showExpertView && ((0, jsx_runtime_1.jsx)(EmbedFormField_1.default, { container: embedFormFieldContainer, status: status }))] })), (0, jsx_runtime_1.jsx)("div", { "aria-live": "polite", "aria-atomic": "true", className: "o3-visually-hidden", children: ariaMessage })] }));
|
|
71
72
|
}
|
|
72
73
|
exports.default = QandaContainer;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.QandaContext = void 0;
|
|
3
|
+
exports.QandaProvider = exports.QandaContext = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("preact/jsx-runtime");
|
|
4
5
|
const react_1 = require("react");
|
|
5
6
|
exports.QandaContext = (0, react_1.createContext)({
|
|
6
7
|
storyId: '',
|
|
@@ -9,3 +10,32 @@ exports.QandaContext = (0, react_1.createContext)({
|
|
|
9
10
|
title: '',
|
|
10
11
|
maxQuestionLength: 0,
|
|
11
12
|
});
|
|
13
|
+
const QandaProvider = ({ children, storyId, useStaging, commentsAPIUrl, title, maxQuestionLength = 0, }) => {
|
|
14
|
+
const dialogOpenerRef = (0, react_1.useRef)(null);
|
|
15
|
+
const setDialogOpener = (0, react_1.useCallback)((opener) => {
|
|
16
|
+
dialogOpenerRef.current = opener;
|
|
17
|
+
}, []);
|
|
18
|
+
const focusDialogOpener = (0, react_1.useCallback)(() => {
|
|
19
|
+
dialogOpenerRef.current?.focus();
|
|
20
|
+
dialogOpenerRef.current = null;
|
|
21
|
+
}, []);
|
|
22
|
+
const contextValues = (0, react_1.useMemo)(() => ({
|
|
23
|
+
storyId,
|
|
24
|
+
useStaging,
|
|
25
|
+
commentsAPIUrl,
|
|
26
|
+
title,
|
|
27
|
+
maxQuestionLength,
|
|
28
|
+
setDialogOpener,
|
|
29
|
+
focusDialogOpener,
|
|
30
|
+
}), [
|
|
31
|
+
storyId,
|
|
32
|
+
useStaging,
|
|
33
|
+
commentsAPIUrl,
|
|
34
|
+
title,
|
|
35
|
+
maxQuestionLength,
|
|
36
|
+
setDialogOpener,
|
|
37
|
+
focusDialogOpener,
|
|
38
|
+
]);
|
|
39
|
+
return ((0, jsx_runtime_1.jsx)(exports.QandaContext.Provider, { value: contextValues, children: children }));
|
|
40
|
+
};
|
|
41
|
+
exports.QandaProvider = QandaProvider;
|
|
@@ -21,16 +21,16 @@ const user_1 = require("../../store/user");
|
|
|
21
21
|
const validate_display_name_1 = __importDefault(require("../../utils/validate-display-name"));
|
|
22
22
|
const messagesByUserType_1 = require("./messagesByUserType");
|
|
23
23
|
const tracking_1 = require("../../utils/tracking");
|
|
24
|
-
const MIN_QUESTION_LENGTH = 20;
|
|
25
|
-
const MAX_QUESTION_LENGTH = 300;
|
|
26
24
|
function QuestionForm() {
|
|
27
25
|
const dispatch = (0, react_redux_1.useDispatch)();
|
|
28
|
-
const { storyId, useStaging, commentsAPIUrl, maxQuestionLength } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
|
|
26
|
+
const { storyId, useStaging, commentsAPIUrl, maxQuestionLength, focusDialogOpener, } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
|
|
29
27
|
const [postAnonymously, setPostAnonymously] = (0, react_1.useState)(false);
|
|
30
28
|
const [newDisplayName, setNewDisplayName] = (0, react_1.useState)(null);
|
|
31
29
|
const [submitEnabled, setSubmitEnabled] = (0, react_1.useState)(false);
|
|
32
30
|
const { isFormOpen, question, displayNameError } = (0, react_redux_1.useSelector)((state) => state.question);
|
|
33
31
|
const { profile, error: userDisplayNameError } = (0, react_redux_1.useSelector)((state) => state.user);
|
|
32
|
+
const MIN_QUESTION_LENGTH = 20;
|
|
33
|
+
const MAX_QUESTION_LENGTH = maxQuestionLength || 300;
|
|
34
34
|
// NB: reset will have the effect of setting things like error to null and isSuccess to empty
|
|
35
35
|
// you use this when you want to clear the state of the usePostQuestionMutation hook as if you had just started on the page
|
|
36
36
|
// as we allow users to see things like error messages and success messages, we do not clear those until we try to send another mutation, otherwise those things will clear too early
|
|
@@ -103,12 +103,16 @@ function QuestionForm() {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
try {
|
|
106
|
+
// set nudge to false if the error is a suspect word, as the user can choose to submit it for moderator review
|
|
107
|
+
const errorCode = error?.data?.error?.errorCode ?? undefined;
|
|
108
|
+
const nudge = errorCode !== 'TOXIC_COMMENT';
|
|
106
109
|
await postQuestion({
|
|
107
110
|
token: updatedProfile?.token || profile?.token || '',
|
|
108
111
|
storyId,
|
|
109
112
|
useStaging,
|
|
110
113
|
commentsAPIUrl,
|
|
111
114
|
question: question,
|
|
115
|
+
nudge,
|
|
112
116
|
postAnonymously,
|
|
113
117
|
}).unwrap();
|
|
114
118
|
}
|
|
@@ -129,14 +133,15 @@ function QuestionForm() {
|
|
|
129
133
|
// are implemented by Origami team.
|
|
130
134
|
// Ticket CI-2770 was created to address this in the future.
|
|
131
135
|
(0, jsx_runtime_1.jsxs)(Overlay_1.default, { title: "Ask a question", closeCallback: () => {
|
|
136
|
+
focusDialogOpener?.();
|
|
132
137
|
setNewDisplayName(null);
|
|
133
138
|
dispatch((0, questionSlice_1.closeForm)());
|
|
134
|
-
}, children: [profile.isSubscribed && ((0, jsx_runtime_1.jsx)("div", { className: "overlay__content-box overlay__form-box", "data-testid": "question-form-overlay", "data-trackable-context-trigger_action": "open question form", children: (0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, "data-trackable": (0, tracking_1.trackingNS)(`submit_question_form_${postAnonymously ? 'anonymous' : 'displayname'}`), children: [(0, jsx_runtime_1.jsxs)("div", { className: "overlay__user-container", children: [!postAnonymously && profile?.displayName && ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsxs)("span", { className: "overlay__user-container-display-name o3-type-body-highlight", children: [(0, jsx_runtime_1.jsx)("i", { className: "overlay__message-icon", style: { maskImage: 'var(--o3-icon-user)' } }), profile.displayName] }) })), !profile?.displayName && ((0, jsx_runtime_1.jsxs)("label", {
|
|
139
|
+
}, children: [profile.isSubscribed && ((0, jsx_runtime_1.jsx)("div", { className: "overlay__content-box overlay__form-box", "data-testid": "question-form-overlay", "data-trackable-context-trigger_action": "open question form", children: (0, jsx_runtime_1.jsxs)("form", { "aria-busy": isLoading, onSubmit: handleSubmit, "data-trackable": (0, tracking_1.trackingNS)(`submit_question_form_${postAnonymously ? 'anonymous' : 'displayname'}`), children: [(0, jsx_runtime_1.jsxs)("div", { className: "overlay__user-container", children: [!postAnonymously && profile?.displayName && ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsxs)("span", { className: "overlay__user-container-display-name o3-type-body-highlight", children: [(0, jsx_runtime_1.jsx)("i", { className: "overlay__message-icon", style: { maskImage: 'var(--o3-icon-user)' } }), (0, jsx_runtime_1.jsxs)("span", { className: "o3-visually-hidden", children: ["Your user name is", ' '] }), profile.displayName] }) })), !profile?.displayName && ((0, jsx_runtime_1.jsxs)("label", { htmlFor: "display-name", className: "question-form__displayname-input o-forms-field", "aria-live": "polite", "aria-atomic": "true", children: [(0, jsx_runtime_1.jsx)("span", { className: "o-forms-title", children: (0, jsx_runtime_1.jsx)("span", { className: "o3-type-body-highlight", children: "Choose a display name" }) }), (0, jsx_runtime_1.jsxs)("span", { className: (0, classnames_1.default)([
|
|
135
140
|
'question-form__displayname__input',
|
|
136
141
|
'o-forms-input',
|
|
137
142
|
'o-forms-input--text',
|
|
138
143
|
{ 'o-forms-input--invalid': !!displayNameError },
|
|
139
|
-
]), children: [(0, jsx_runtime_1.jsx)("input", { className: "question-form__displayname__input__field o3-type-body-lg", id: "display-name", value: newDisplayName || '', onChange: (e) => setNewDisplayName(e.currentTarget.value), type: postAnonymously ? 'password' : 'text', "aria-invalid": displayNameError ? 'true' :
|
|
144
|
+
]), children: [(0, jsx_runtime_1.jsx)("input", { className: "question-form__displayname__input__field o3-type-body-lg", id: "display-name", value: newDisplayName || '', onChange: (e) => setNewDisplayName(e.currentTarget.value), type: postAnonymously ? 'password' : 'text', "aria-describedby": "question-form-disclaimer", "aria-invalid": displayNameError ? 'true' : undefined, "aria-errormessage": displayNameError ??
|
|
140
145
|
'question_form__displayname-error-message' }), displayNameError && ((0, jsx_runtime_1.jsx)("span", { className: "question-form__displayname__input__error o3-type-detail", id: "question_form__displayname-error-message", children: (0, jsx_runtime_1.jsx)("span", { children: displayNameError }) }))] })] }))] }), (0, jsx_runtime_1.jsx)("div", { className: "o-forms-field", children: (0, jsx_runtime_1.jsxs)("label", { className: "o-forms-input o-forms-input--checkbox", htmlFor: "post-anonymously", children: [(0, jsx_runtime_1.jsx)("input", { type: "checkbox", name: "post-anonymously", id: "post-anonymously", value: "true", onChange: handleHideDisplayName, "data-trackable-context-action": `Post anonymously ${postAnonymously ? 'off' : 'on'}`, "data-trackable": (0, tracking_1.trackingNS)(`question-form__post_anonymously-${postAnonymously ? 'off' : 'on'}`) }), (0, jsx_runtime_1.jsx)("span", { className: "o-forms-input__label", children: "Post anonymously (optional)" })] }) }), (0, jsx_runtime_1.jsxs)("label", { for: "question-text", className: "o-forms-field question-form__question__input", children: [(0, jsx_runtime_1.jsx)("span", { className: "o-forms-title", children: (0, jsx_runtime_1.jsx)("span", { className: "o3-type-body-highlight", children: "Enter your question" }) }), (0, jsx_runtime_1.jsxs)("div", { className: (0, classnames_1.default)([
|
|
141
146
|
'o-forms-input',
|
|
142
147
|
'o-forms-input--textarea',
|
|
@@ -144,7 +149,7 @@ function QuestionForm() {
|
|
|
144
149
|
{
|
|
145
150
|
'input-textarea__error': question.length > MAX_QUESTION_LENGTH,
|
|
146
151
|
},
|
|
147
|
-
]), children: [(0, jsx_runtime_1.jsx)("textarea", { id: "question-text", name: "question", className: "input-textarea", value: question, onChange: (e) => dispatch((0, questionSlice_1.updateQuestion)(e.currentTarget.value)), "aria-label": "Your question", placeholder: "Type your question",
|
|
152
|
+
]), children: [(0, jsx_runtime_1.jsx)("textarea", { id: "question-text", name: "question", className: "input-textarea", value: question, onChange: (e) => dispatch((0, questionSlice_1.updateQuestion)(e.currentTarget.value)), "aria-label": "Your question", "aria-describedby": "question-form-description", placeholder: "Type your question", "aria-invalid": error ? 'true' : undefined, "aria-errormessage": error ? 'question-form-error-message' : undefined }), (0, jsx_runtime_1.jsx)("span", { className: "o3-type-detail input-textarea__counter", "aria-label": `Your question is ${question.length} characters long, maximum allowed is ${MAX_QUESTION_LENGTH}`, children: `${question.length}/${MAX_QUESTION_LENGTH}` })] })] }), (0, jsx_runtime_1.jsx)("span", { id: "question-form-description", "aria-live": "polite", className: (0, classnames_1.default)([
|
|
148
153
|
'o3-type-detail',
|
|
149
154
|
{
|
|
150
155
|
'input--textarea__error-message': question.length > MAX_QUESTION_LENGTH,
|
|
@@ -164,35 +169,35 @@ function QuestionForm() {
|
|
|
164
169
|
},
|
|
165
170
|
// @ts-expect-error - o3 Button is currently accepting a JSX element as label
|
|
166
171
|
// but the type definition needs to be updated by Origami team
|
|
167
|
-
label: isLoading ? (0, jsx_runtime_1.jsx)(Loader_1.default, {}) : 'Submit your question', type: "primary", theme: "mono" }), !profile?.token && ((0, jsx_runtime_1.jsx)("span", { className: "o3-type-detail", children: "Submitting a question will create a community profile with FT." }))] }) })), isSuccess && ((0, jsx_runtime_1.jsx)(MessageBox_1.default, { color: "#00572C", extraClassNames: "o-message--alert o-message--success", title: "Question sent", text: "Your question is in the queue and will be reviewed by our editors." })), error && ((0, jsx_runtime_1.jsx)(ErrorMessage_1.default, { error: error, storyId: storyId, errorMapping: {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
172
|
+
label: isLoading ? (0, jsx_runtime_1.jsx)(Loader_1.default, {}) : 'Submit your question', type: "primary", theme: "mono" }), !profile?.token && ((0, jsx_runtime_1.jsx)("span", { className: "o3-type-detail", id: "question-form-disclaimer", children: "Submitting a question will create a community profile with FT." }))] }) })), isSuccess && ((0, jsx_runtime_1.jsx)("div", { role: "alert", "aria-live": "polite", children: (0, jsx_runtime_1.jsx)(MessageBox_1.default, { color: "#00572C", extraClassNames: "o-message--alert o-message--success", title: "Question sent", text: "Your question is in the queue and will be reviewed by our editors." }) })), (0, jsx_runtime_1.jsx)("div", { "aria-live": "polite", role: "alert", id: "question-form-error-message", children: error && ((0, jsx_runtime_1.jsx)(ErrorMessage_1.default, { error: error, storyId: storyId, errorMapping: {
|
|
173
|
+
// Messages for specific error codes
|
|
174
|
+
REPEAT_POST: {
|
|
175
|
+
title: 'Question already submitted',
|
|
176
|
+
message: "You've already submitted this question. Try asking something different and submit again.",
|
|
177
|
+
},
|
|
178
|
+
REJECTED: {
|
|
179
|
+
title: 'Question rejected',
|
|
180
|
+
message: 'This question has been rejected for language that violates our guidelines.',
|
|
181
|
+
},
|
|
182
|
+
RATE_LIMIT_EXCEEDED: {
|
|
183
|
+
title: 'Too many questions',
|
|
184
|
+
message: 'You are submitting too many questions in a short amount of time. Please wait sometime before submitting another question.',
|
|
185
|
+
},
|
|
186
|
+
TOXIC_COMMENT: {
|
|
187
|
+
title: 'Question blocked',
|
|
188
|
+
message: 'Are you sure? The language in your question might violate our community guidelines. You can edit it, or if you submit, it will need moderator review. This could slow down a reply from our experts',
|
|
189
|
+
},
|
|
190
|
+
serverError: {
|
|
191
|
+
title: 'Server error',
|
|
192
|
+
message: 'There was a server error. Please try submitting your question again.',
|
|
193
|
+
},
|
|
194
|
+
connectionError: {
|
|
195
|
+
title: 'Connection error',
|
|
196
|
+
message: "You can't submit your question. Check your internet connection or browser settings and try again.",
|
|
197
|
+
},
|
|
198
|
+
timeoutError: {
|
|
199
|
+
title: 'Timeout error',
|
|
200
|
+
message: 'The request timed out. Please try submitting your question again.',
|
|
201
|
+
},
|
|
202
|
+
}, operation: "submit-question" })) }), (0, jsx_runtime_1.jsx)(MessageBox_1.default, { extraClassNames: "o-message--notice o-message--inform", title: (0, messagesByUserType_1.getTitleByUser)(profile), text: (0, messagesByUserType_1.getTextByUser)(profile) })] }));
|
|
198
203
|
}
|
package/dist/cjs/index.js
CHANGED
|
@@ -31,7 +31,7 @@ class QandaUI {
|
|
|
31
31
|
(containerElement.dataset.qandaMaxquestionlength
|
|
32
32
|
? parseInt(containerElement.dataset.qandaMaxquestionlength, 10)
|
|
33
33
|
: undefined) ??
|
|
34
|
-
|
|
34
|
+
300;
|
|
35
35
|
if (!storyId || !title || !commentsAPIHost || !commentsAPIVersion) {
|
|
36
36
|
throw new Error('Required properties are missing');
|
|
37
37
|
}
|
|
@@ -157,14 +157,14 @@ exports.nextCommentsApi = (0, react_1.createApi)({
|
|
|
157
157
|
}),
|
|
158
158
|
// POST A QUESTION
|
|
159
159
|
postQuestion: builder.mutation({
|
|
160
|
-
query: ({ storyId, useStaging, commentsAPIUrl, token, question, postAnonymously, }) => ({
|
|
160
|
+
query: ({ storyId, useStaging, commentsAPIUrl, token, question, nudge = true, postAnonymously, }) => ({
|
|
161
161
|
url: `${commentsAPIUrl}/story/${storyId}/question${useStaging ? '?staging=1' : ''}`,
|
|
162
162
|
method: 'POST',
|
|
163
163
|
headers: {
|
|
164
164
|
'Content-Type': 'application/json',
|
|
165
165
|
Authorization: `Bearer ${token}`,
|
|
166
166
|
},
|
|
167
|
-
body: JSON.stringify({ question, postAnonymously, token }),
|
|
167
|
+
body: JSON.stringify({ question, postAnonymously, token, nudge }),
|
|
168
168
|
}),
|
|
169
169
|
transformResponse: (response) => {
|
|
170
170
|
return response.data;
|
|
@@ -34,7 +34,8 @@ function Calendar() {
|
|
|
34
34
|
className: 'live-qa-calendar-button',
|
|
35
35
|
'aria-pressed': dropDownVisible,
|
|
36
36
|
'aria-haspopup': 'true',
|
|
37
|
-
'aria-
|
|
37
|
+
'aria-expanded': dropDownVisible,
|
|
38
|
+
'aria-controls': 'live-qa-calendar-list-menu',
|
|
38
39
|
id: 'live-qa-calendar-button-menu',
|
|
39
40
|
'data-testid': 'live-qa-calendar-button',
|
|
40
41
|
'data-trackable': trackingNS('add-to-calendar-button'),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { jsx as _jsx, Fragment as _Fragment } from "preact/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
|
|
2
2
|
import { useEffect, useState, useContext, createPortal } from 'react';
|
|
3
3
|
import { QandaContext } from '../QandaProvider';
|
|
4
|
+
import { PRODUCT_NAME } from '../../config/qanda';
|
|
4
5
|
import { useGetQandAStreamQuery, useUpdatedQA, } from '../../services/comments-api';
|
|
5
6
|
export function formatTime(remainingTime) {
|
|
6
7
|
const days = Math.floor(remainingTime / (1000 * 60 * 60 * 24));
|
|
@@ -82,7 +83,7 @@ function CountdownTimer({ container }) {
|
|
|
82
83
|
}, []);
|
|
83
84
|
const portalElement = data &&
|
|
84
85
|
((status === 'pre-live' && startDateTime) ||
|
|
85
|
-
(status === 'live' && endDateTime)) && (_jsx("div", { className: "qanda-portal countdown-wrapper", children: _jsx("div", { className: "countdown-block", children: _jsx("p", { className: "countdown", children: count && (
|
|
86
|
+
(status === 'live' && endDateTime)) && (_jsx("div", { className: "qanda-portal countdown-wrapper", children: _jsx("div", { className: "countdown-block", children: _jsx("p", { className: "countdown", children: count && (_jsxs("time", { datetime: status === 'pre-live' ? startDateTime : endDateTime, children: [_jsx("span", { className: "o3-visually-hidden", children: PRODUCT_NAME }), ' ', count] })) }) }) }));
|
|
86
87
|
return _jsx(_Fragment, { children: createPortal(portalElement, portalTarget || document.body) });
|
|
87
88
|
}
|
|
88
89
|
export default CountdownTimer;
|
|
@@ -2,13 +2,16 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-ru
|
|
|
2
2
|
import oTracking from '@financial-times/o-tracking';
|
|
3
3
|
import { useDispatch } from 'react-redux';
|
|
4
4
|
import { openForm } from '../../store/questionSlice';
|
|
5
|
-
import { useEffect, useState } from 'react';
|
|
5
|
+
import { useRef, useEffect, useState, useContext } from 'react';
|
|
6
6
|
import { createPortal } from 'react-dom';
|
|
7
|
+
import { QandaContext } from '../QandaProvider';
|
|
7
8
|
import { PRODUCT_NAME } from '../../config/qanda';
|
|
8
9
|
import { trackingNS } from '../../utils/tracking';
|
|
9
10
|
function EmbedFormField({ container, status = 'pre-live', }) {
|
|
10
11
|
const dispatch = useDispatch();
|
|
11
12
|
const [portalTarget, setPortalTarget] = useState();
|
|
13
|
+
const modalOpenerRef = useRef(null);
|
|
14
|
+
const { setDialogOpener } = useContext(QandaContext);
|
|
12
15
|
useEffect(() => {
|
|
13
16
|
if (container) {
|
|
14
17
|
const portalContainer = document.createElement('div');
|
|
@@ -20,7 +23,10 @@ function EmbedFormField({ container, status = 'pre-live', }) {
|
|
|
20
23
|
};
|
|
21
24
|
}
|
|
22
25
|
}, []);
|
|
23
|
-
const portalElement = (_jsxs("div", { className: "qanda-portal embed-form-field__container o3-type-body-highlight", children: ["Submit your question for ", PRODUCT_NAME, _jsxs("
|
|
26
|
+
const portalElement = (_jsxs("div", { className: "qanda-portal embed-form-field__container o3-type-body-highlight", children: ["Submit your question for ", PRODUCT_NAME, _jsxs("button", { onClick: () => {
|
|
27
|
+
dispatch(openForm());
|
|
28
|
+
setDialogOpener?.(modalOpenerRef.current);
|
|
29
|
+
}, className: "embed-form-field__text-input o3-type-body-lg", "data-testid": "embed-form-field", "data-trackable": trackingNS(`embed-form-field-${status}`), "data-component": "embed-form-field", "aria-haspopup": "dialog", ref: modalOpenerRef, type: "button", children: [_jsx("span", { className: "embed-form-field__icon", children: _jsx("i", { style: { maskImage: 'var(--o3-icon-user)' } }) }), "Enter your question"] })] }));
|
|
24
30
|
return _jsx(_Fragment, { children: createPortal(portalElement, portalTarget || document.body) });
|
|
25
31
|
}
|
|
26
32
|
export default EmbedFormField;
|
|
@@ -15,6 +15,10 @@ const baseErrorMessages = {
|
|
|
15
15
|
title: 'Timeout error',
|
|
16
16
|
message: 'The request timed out. You may be on a slow connection. Please try again later.',
|
|
17
17
|
},
|
|
18
|
+
maintenanceMode: {
|
|
19
|
+
title: 'Sorry — this "Ask an Expert" page isn’t loading',
|
|
20
|
+
message: 'We’re working to fix the problem. Please check back soon.',
|
|
21
|
+
},
|
|
18
22
|
};
|
|
19
23
|
export default class HumanReadableError {
|
|
20
24
|
constructor(error, customErrorMessages = {}) {
|
|
@@ -31,7 +35,9 @@ export default class HumanReadableError {
|
|
|
31
35
|
this._humanReadableError = errorMessages.connectionError;
|
|
32
36
|
}
|
|
33
37
|
else if (typeof error.status === 'number' && error.status >= 500) {
|
|
34
|
-
|
|
38
|
+
const typedError = error;
|
|
39
|
+
this._humanReadableError =
|
|
40
|
+
errorMessages[typedError.data?.error?.errorCode] || errorMessages.serverError;
|
|
35
41
|
}
|
|
36
42
|
else if (typeof error.status === 'number' && error.status === 400) {
|
|
37
43
|
// Assuming as an error code otherwise will throw an error and we will fallback to generic error
|
|
@@ -9,7 +9,7 @@ import Overlay from '../Overlay';
|
|
|
9
9
|
import { PRODUCT_NAME } from '../../config/qanda';
|
|
10
10
|
function ExpertDrawer() {
|
|
11
11
|
const dispatch = useDispatch();
|
|
12
|
-
const { storyId, useStaging, title } = useContext(QandaContext);
|
|
12
|
+
const { storyId, useStaging, title, focusDialogOpener } = useContext(QandaContext);
|
|
13
13
|
const { isExpertFormOpen, expertView } = useSelector((state) => state.expert);
|
|
14
14
|
useEffect(() => {
|
|
15
15
|
if (isExpertFormOpen) {
|
|
@@ -24,7 +24,10 @@ function ExpertDrawer() {
|
|
|
24
24
|
if (!isExpertFormOpen) {
|
|
25
25
|
return null;
|
|
26
26
|
}
|
|
27
|
-
return (_jsx(Overlay, { title: PRODUCT_NAME, closeCallback: () =>
|
|
27
|
+
return (_jsx(Overlay, { title: PRODUCT_NAME, closeCallback: () => {
|
|
28
|
+
dispatch(closeExpertForm());
|
|
29
|
+
focusDialogOpener?.();
|
|
30
|
+
}, options: {
|
|
28
31
|
shadow: true,
|
|
29
32
|
allowInteraction: true,
|
|
30
33
|
}, children: _jsx("div", { className: "o-comments", id: "o-comments-stream", "data-o-component": "o-comments", "data-o-comments-article-id": storyId, "data-o-comments-article-url": `https://www.ft.com/content/${storyId}`, "data-o-comments-use-staging-environment": useStaging, "data-o-comments-title": title }) }));
|
|
@@ -6,7 +6,7 @@ import { openExpertForm } from '../../store/expertSlice';
|
|
|
6
6
|
import { useIsMobile } from '../../hooks/useIsMobile';
|
|
7
7
|
import { useUpdatedComments } from '../../services/comments-api';
|
|
8
8
|
import { updateComments } from '../../store/updateComments';
|
|
9
|
-
import { useContext } from 'react';
|
|
9
|
+
import { useContext, useRef } from 'react';
|
|
10
10
|
import { QandaContext } from '../QandaProvider';
|
|
11
11
|
import { trackingNS } from '../../utils/tracking';
|
|
12
12
|
function LiveIndicator() {
|
|
@@ -15,7 +15,8 @@ function LiveIndicator() {
|
|
|
15
15
|
function FloatingActionBar({ isLive = true }) {
|
|
16
16
|
const dispatch = useDispatch();
|
|
17
17
|
const isMobile = useIsMobile();
|
|
18
|
-
const { storyId, useStaging, commentsAPIUrl } = useContext(QandaContext);
|
|
18
|
+
const { storyId, useStaging, commentsAPIUrl, setDialogOpener } = useContext(QandaContext);
|
|
19
|
+
const modalOpenerRef = useRef(null);
|
|
19
20
|
const updatedComments = useUpdatedComments({
|
|
20
21
|
storyId,
|
|
21
22
|
useStaging,
|
|
@@ -28,6 +29,8 @@ function FloatingActionBar({ isLive = true }) {
|
|
|
28
29
|
// Scrolling to start of qanda stream title
|
|
29
30
|
const element = document.getElementById('qanda__stream-title');
|
|
30
31
|
if (element) {
|
|
32
|
+
element.setAttribute('tabindex', '-1'); // Make it focusable
|
|
33
|
+
element.focus();
|
|
31
34
|
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
32
35
|
}
|
|
33
36
|
};
|
|
@@ -35,7 +38,15 @@ function FloatingActionBar({ isLive = true }) {
|
|
|
35
38
|
const userConfig = {
|
|
36
39
|
description: 'Ask a question',
|
|
37
40
|
testIdSuffix: 'ask-a-question-button',
|
|
38
|
-
onClick: () =>
|
|
41
|
+
onClick: () => {
|
|
42
|
+
// modalOpener set on div as Button does not support refs, therefore we need to find the button inside it
|
|
43
|
+
const { current: modalOpener } = modalOpenerRef;
|
|
44
|
+
const openerButton = modalOpener?.querySelector('button');
|
|
45
|
+
if (openerButton) {
|
|
46
|
+
setDialogOpener?.(openerButton);
|
|
47
|
+
}
|
|
48
|
+
dispatch(openForm());
|
|
49
|
+
},
|
|
39
50
|
dataTrackable: 'open_question_form',
|
|
40
51
|
};
|
|
41
52
|
const expertConfig = {
|
|
@@ -45,11 +56,12 @@ function FloatingActionBar({ isLive = true }) {
|
|
|
45
56
|
dataTrackable: 'open_expert_drawer',
|
|
46
57
|
};
|
|
47
58
|
const view = showExpertView ? expertConfig : userConfig;
|
|
48
|
-
return (_jsxs("div", { className: "floating-action-bar__container", "data-trackable": trackingNS('floating-action-bar'), children: [_jsx("div", { className: "floating-action-bar__fade-overlay" }), _jsxs("div", { className: "floating-action-bar", "data-testid": "floating-action-bar", children: [isLive && _jsx(LiveIndicator, {}), _jsxs("div", { className: "floating-action-bar__buttons", children: [_jsx(Button, { attributes: {
|
|
59
|
+
return (_jsxs("div", { className: "floating-action-bar__container", "data-trackable": trackingNS('floating-action-bar'), children: [_jsx("div", { className: "floating-action-bar__fade-overlay" }), _jsxs("div", { className: "floating-action-bar", "data-testid": "floating-action-bar", children: [_jsx("span", { className: "o3-visually-hidden", "aria-live": "polite", role: "alert", children: isLive ? 'This session is now live' : '' }), isLive && _jsx(LiveIndicator, {}), _jsxs("div", { className: "floating-action-bar__buttons", ref: modalOpenerRef, children: [_jsx(Button, { attributes: {
|
|
49
60
|
ariaLabel: view.description,
|
|
50
61
|
'data-trackable': trackingNS(view.dataTrackable),
|
|
51
62
|
'data-trackable-context-action': view.description,
|
|
52
63
|
'data-testid': `floating-action-bar__${view.testIdSuffix}`,
|
|
64
|
+
ariaHasPopup: 'dialog',
|
|
53
65
|
}, label: view.description, type: "ghost", theme: "mono", icon: "edit", iconOnly: isMobile, onClick: view.onClick }), _jsx(Button, { attributes: {
|
|
54
66
|
ariaLabel: 'Back to top',
|
|
55
67
|
'data-trackable': trackingNS('back_to_top'),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
2
|
function MessageBox({ color, extraClassNames = '', text, title, id = '', }) {
|
|
3
|
-
return (_jsx("div", {
|
|
3
|
+
return (_jsx("div", { className: `o-message o-message--inner message-box__container ${extraClassNames}`, "data-o-message-close": "false", "data-o-component": "o-message", id: id, children: _jsx("div", { className: "o-message__container", children: _jsxs("div", { className: "message-box__content", children: [_jsx("span", { style: { color }, className: "o3-type-body-highlight", children: title }), _jsx("span", { style: { color }, className: "o3-type-body-base", children: text })] }) }) }));
|
|
4
4
|
}
|
|
5
5
|
export default MessageBox;
|
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
2
3
|
import { Button } from '@financial-times/o3-button';
|
|
3
4
|
import classNames from 'classnames';
|
|
4
5
|
function Overlay({ title, children, closeCallback, options = {}, }) {
|
|
5
|
-
|
|
6
|
+
// Trap keyboard focus within the overlay for accessibility
|
|
7
|
+
const trapFocus = (event) => {
|
|
8
|
+
if (event.key !== 'Tab')
|
|
9
|
+
return;
|
|
10
|
+
if (event.target === endFocusBtn.current) {
|
|
11
|
+
titleRef.current?.focus();
|
|
12
|
+
}
|
|
13
|
+
else if (event.target === titleRef.current && event.shiftKey) {
|
|
14
|
+
endFocusBtn.current?.focus();
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const titleRef = useRef(null);
|
|
18
|
+
const endFocusBtn = useRef(null);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
titleRef.current?.focus();
|
|
21
|
+
}, []);
|
|
22
|
+
return (_jsxs("div", { className: "overlay", role: "dialog", "aria-modal": "true", "aria-labelledby": "overlay-title", children: [!options.allowInteraction && _jsx("div", { className: "overlay__outer" }), _jsxs("div", { className: classNames([
|
|
6
23
|
'overlay__inner',
|
|
7
24
|
{ 'overlay__inner--shadow': options.shadow },
|
|
8
|
-
]), children: [_jsx("div", { className: "overlay__header", children: _jsxs("div", { className: "overlay__header-bar", children: [_jsx("span", { className: "o3-type-body-highlight overlay__header-title", children: title }), _jsx(Button, { attributes: { className: 'overlay__header-close' }, iconOnly: true, icon: "cross", label: "Close", type: "ghost", theme: "mono", onClick: () => closeCallback() })] }) }), _jsx("div", { className: "overlay__content", children: children })] })] }));
|
|
25
|
+
]), children: [_jsx("div", { className: "overlay__header", children: _jsxs("div", { className: "overlay__header-bar", children: [_jsx("span", { className: "o3-type-body-highlight overlay__header-title", id: "overlay-title", tabIndex: 0, ref: titleRef, onKeyDown: trapFocus, children: title }), _jsx(Button, { attributes: { className: 'overlay__header-close' }, iconOnly: true, icon: "cross", label: "Close", type: "ghost", theme: "mono", onClick: () => closeCallback() })] }) }), _jsx("div", { className: "overlay__content", children: children })] }), _jsx("button", { className: "o3-visually-hidden", "aria-label": "Close", onClick: () => closeCallback(), ref: endFocusBtn, onKeyDown: trapFocus, type: "button", children: "Close" })] }));
|
|
9
26
|
}
|
|
10
27
|
export default Overlay;
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "preact/jsx-runtime";
|
|
|
2
2
|
import { Provider } from 'react-redux';
|
|
3
3
|
import { store } from '../store';
|
|
4
4
|
import QandaContainer from './QandaContainer';
|
|
5
|
-
import {
|
|
5
|
+
import { QandaProvider } from './QandaProvider';
|
|
6
6
|
function Qanda({ storyId, useStaging, title, commentsAPIHost, commentsAPIVersion, maxQuestionLength, embedFormFieldContainer, countdownTimerContainer, }) {
|
|
7
7
|
// check we have a valid ft.com url and remove any trailing slashes
|
|
8
8
|
const allowedHostRegx = /^[\w\-\.]+(\.ft\.com|\.ftweb\.tech)$/i;
|
|
@@ -21,12 +21,6 @@ function Qanda({ storyId, useStaging, title, commentsAPIHost, commentsAPIVersion
|
|
|
21
21
|
console.error('Missing required props');
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
24
|
-
return (_jsx(Provider, { store: store, children: _jsx(
|
|
25
|
-
storyId,
|
|
26
|
-
useStaging,
|
|
27
|
-
commentsAPIUrl: cleanCommentsAPIUrl,
|
|
28
|
-
title,
|
|
29
|
-
maxQuestionLength,
|
|
30
|
-
}, children: _jsx(QandaContainer, { embedFormFieldContainer: embedFormFieldContainer, countdownTimerContainer: countdownTimerContainer }) }) }));
|
|
24
|
+
return (_jsx(Provider, { store: store, children: _jsx(QandaProvider, { storyId: storyId, useStaging: useStaging, commentsAPIUrl: cleanCommentsAPIUrl, title: title, maxQuestionLength: maxQuestionLength, children: _jsx(QandaContainer, { embedFormFieldContainer: embedFormFieldContainer, countdownTimerContainer: countdownTimerContainer }) }) }));
|
|
31
25
|
}
|
|
32
26
|
export default Qanda;
|
|
@@ -69,6 +69,6 @@ function QandaBlock({ id, body, children, byline, summary, showAnswerTime = fals
|
|
|
69
69
|
observer.disconnect();
|
|
70
70
|
};
|
|
71
71
|
}, []);
|
|
72
|
-
return (_jsxs("div", { className: "qanda-block", children: [_jsxs("div", { className: "qanda-block__comment qanda-block__question", id: id, "data-testid": `qanda-block__question-${id}`, "data-trackable": trackingNS('question'), children: [summary && (_jsx("div", { className: "o3-type-headline-sm qanda-block__question-summary", children: summary })),
|
|
72
|
+
return (_jsxs("div", { className: "qanda-block", children: [_jsxs("div", { className: "qanda-block__comment qanda-block__question", id: id, "data-testid": `qanda-block__question-${id}`, "data-trackable": trackingNS('question'), children: [summary && (_jsx("div", { className: "o3-type-headline-sm qanda-block__question-summary", children: summary })), _jsxs("div", { className: "o3-type-body-highlight qanda-block__comment-byline", children: [_jsx("span", { className: "o3-visually-hidden", children: "Question by " }), _jsx(Byline, { structuredContent: byline, showEditedBy: false })] }), _jsx("div", { className: "o3-type-body-content-base qanda-block__comment-text", children: _jsx(Expander, { className: "qanda-block__text", expandLabel: "Expand question", collapseLabel: "Collapse question", id: id, children: _jsx(RichText, { structuredContent: body.structured }) }) })] }, id), children?.map((child) => child.body && (_jsxs("div", { className: "qanda-block__comment qanda-block__answer", "data-testid": `qanda-block__answer-${child.id}`, "data-trackable": trackingNS('answer'), children: [_jsxs("div", { className: "qanda-block__answer-header", children: [_jsxs("div", { className: "o3-type-body-highlight qanda-block__comment-byline", children: [_jsx("span", { className: "o3-visually-hidden", children: "Reply from " }), child.author ? (_jsx(Author, { byline: child.byline, headshot: child.author.headshot, prefLabel: child.author.prefLabel, role: child.author.role, streamPage: child.author.streamPage })) : (_jsx(Byline, { structuredContent: child.byline, showEditedBy: false }))] }), showAnswerTime && (_jsxs("span", { className: "o3-type-detail", children: [_jsx("span", { className: "o3-visually-hidden", children: "Published at " }), renderTime(child.publishedDate)] }))] }), _jsx("div", { className: "o3-type-body-content-base qanda-block__comment-text", children: _jsx(Expander, { className: "qanda-block__text", expandLabel: "Expand answer", collapseLabel: "Collapse answer", id: id, children: _jsx(RichText, { structuredContent: child.body.structured }) }) })] }, child.id)))] }));
|
|
73
73
|
}
|
|
74
74
|
export default QandaBlock;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
2
|
import { useState, useEffect, useContext } from 'react';
|
|
3
3
|
import { useDispatch, useSelector } from 'react-redux';
|
|
4
4
|
import { getUserProfile } from '../store/user';
|
|
@@ -11,6 +11,7 @@ import Calendar from './Calendar';
|
|
|
11
11
|
import CountdownTimer from './CountdownTimer';
|
|
12
12
|
import FloatingActionBar from './FloatingActionBar';
|
|
13
13
|
import EmbedFormField from './EmbedFormField';
|
|
14
|
+
import ErrorMessage from './ErrorMessage';
|
|
14
15
|
import { QandaContext } from './QandaProvider';
|
|
15
16
|
import { useGetQandAStreamQuery, useUpdatedQA, useGetUserRolesQuery, nextCommentsApi, } from '../services/comments-api';
|
|
16
17
|
import { PRODUCT_NAME } from '../config/qanda';
|
|
@@ -42,7 +43,7 @@ function QandaContainer({ embedFormFieldContainer, countdownTimerContainer, }) {
|
|
|
42
43
|
});
|
|
43
44
|
const status = updatedQA?.status || data?.status || 'pre-live';
|
|
44
45
|
const profile = useSelector((state) => state.user.profile);
|
|
45
|
-
const { data: roleData } = useGetUserRolesQuery({
|
|
46
|
+
const { data: roleData, error: roleError } = useGetUserRolesQuery({
|
|
46
47
|
storyId,
|
|
47
48
|
useStaging,
|
|
48
49
|
commentsAPIUrl,
|
|
@@ -62,6 +63,6 @@ function QandaContainer({ embedFormFieldContainer, countdownTimerContainer, }) {
|
|
|
62
63
|
dispatch(nextCommentsApi.util.resetApiState());
|
|
63
64
|
};
|
|
64
65
|
}, [dispatch, storyId]);
|
|
65
|
-
return (_jsxs("div", { className: "qanda-container", children: [error && (
|
|
66
|
+
return (_jsxs("div", { className: "qanda-container", children: [error && (_jsx("div", { style: { marginBottom: '16px' }, children: _jsx(ErrorMessage, { error: error, storyId: storyId, operation: "stream-load" }) })), roleError && !error && (_jsx("div", { style: { marginBottom: '16px' }, children: _jsx(ErrorMessage, { error: roleError, storyId: storyId, operation: "profile-load" }) })), !isLoading && !error && (_jsxs(_Fragment, { children: [status !== 'close' && (_jsxs(_Fragment, { children: [showExpertView ? _jsx(ExpertDrawer, {}) : _jsx(QuestionForm, {}), _jsx(CountdownTimer, { container: countdownTimerContainer })] })), status === 'pre-live' && _jsx(Calendar, {}), _jsx(Stream, { status: status }), _jsx(Survey, { storyId: storyId, status: status }), (status === 'live' || (status === 'pre-live' && showExpertView)) && (_jsx(FloatingActionBar, { isLive: status === 'live' })), status !== 'close' && !showExpertView && (_jsx(EmbedFormField, { container: embedFormFieldContainer, status: status }))] })), _jsx("div", { "aria-live": "polite", "aria-atomic": "true", className: "o3-visually-hidden", children: ariaMessage })] }));
|
|
66
67
|
}
|
|
67
68
|
export default QandaContainer;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx } from "preact/jsx-runtime";
|
|
2
|
+
import { createContext, useRef, useCallback, useMemo } from 'react';
|
|
2
3
|
export const QandaContext = createContext({
|
|
3
4
|
storyId: '',
|
|
4
5
|
useStaging: true,
|
|
@@ -6,3 +7,31 @@ export const QandaContext = createContext({
|
|
|
6
7
|
title: '',
|
|
7
8
|
maxQuestionLength: 0,
|
|
8
9
|
});
|
|
10
|
+
export const QandaProvider = ({ children, storyId, useStaging, commentsAPIUrl, title, maxQuestionLength = 0, }) => {
|
|
11
|
+
const dialogOpenerRef = useRef(null);
|
|
12
|
+
const setDialogOpener = useCallback((opener) => {
|
|
13
|
+
dialogOpenerRef.current = opener;
|
|
14
|
+
}, []);
|
|
15
|
+
const focusDialogOpener = useCallback(() => {
|
|
16
|
+
dialogOpenerRef.current?.focus();
|
|
17
|
+
dialogOpenerRef.current = null;
|
|
18
|
+
}, []);
|
|
19
|
+
const contextValues = useMemo(() => ({
|
|
20
|
+
storyId,
|
|
21
|
+
useStaging,
|
|
22
|
+
commentsAPIUrl,
|
|
23
|
+
title,
|
|
24
|
+
maxQuestionLength,
|
|
25
|
+
setDialogOpener,
|
|
26
|
+
focusDialogOpener,
|
|
27
|
+
}), [
|
|
28
|
+
storyId,
|
|
29
|
+
useStaging,
|
|
30
|
+
commentsAPIUrl,
|
|
31
|
+
title,
|
|
32
|
+
maxQuestionLength,
|
|
33
|
+
setDialogOpener,
|
|
34
|
+
focusDialogOpener,
|
|
35
|
+
]);
|
|
36
|
+
return (_jsx(QandaContext.Provider, { value: contextValues, children: children }));
|
|
37
|
+
};
|
|
@@ -15,16 +15,16 @@ import { getUserProfile } from '../../store/user';
|
|
|
15
15
|
import validateDisplayName from '../../utils/validate-display-name';
|
|
16
16
|
import { getTitleByUser, getTextByUser } from './messagesByUserType';
|
|
17
17
|
import { trackEvent, trackingNS, baseTrackingData } from '../../utils/tracking';
|
|
18
|
-
const MIN_QUESTION_LENGTH = 20;
|
|
19
|
-
const MAX_QUESTION_LENGTH = 300;
|
|
20
18
|
export default function QuestionForm() {
|
|
21
19
|
const dispatch = useDispatch();
|
|
22
|
-
const { storyId, useStaging, commentsAPIUrl, maxQuestionLength } = useContext(QandaContext);
|
|
20
|
+
const { storyId, useStaging, commentsAPIUrl, maxQuestionLength, focusDialogOpener, } = useContext(QandaContext);
|
|
23
21
|
const [postAnonymously, setPostAnonymously] = useState(false);
|
|
24
22
|
const [newDisplayName, setNewDisplayName] = useState(null);
|
|
25
23
|
const [submitEnabled, setSubmitEnabled] = useState(false);
|
|
26
24
|
const { isFormOpen, question, displayNameError } = useSelector((state) => state.question);
|
|
27
25
|
const { profile, error: userDisplayNameError } = useSelector((state) => state.user);
|
|
26
|
+
const MIN_QUESTION_LENGTH = 20;
|
|
27
|
+
const MAX_QUESTION_LENGTH = maxQuestionLength || 300;
|
|
28
28
|
// NB: reset will have the effect of setting things like error to null and isSuccess to empty
|
|
29
29
|
// you use this when you want to clear the state of the usePostQuestionMutation hook as if you had just started on the page
|
|
30
30
|
// as we allow users to see things like error messages and success messages, we do not clear those until we try to send another mutation, otherwise those things will clear too early
|
|
@@ -97,12 +97,16 @@ export default function QuestionForm() {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
try {
|
|
100
|
+
// set nudge to false if the error is a suspect word, as the user can choose to submit it for moderator review
|
|
101
|
+
const errorCode = error?.data?.error?.errorCode ?? undefined;
|
|
102
|
+
const nudge = errorCode !== 'TOXIC_COMMENT';
|
|
100
103
|
await postQuestion({
|
|
101
104
|
token: updatedProfile?.token || profile?.token || '',
|
|
102
105
|
storyId,
|
|
103
106
|
useStaging,
|
|
104
107
|
commentsAPIUrl,
|
|
105
108
|
question: question,
|
|
109
|
+
nudge,
|
|
106
110
|
postAnonymously,
|
|
107
111
|
}).unwrap();
|
|
108
112
|
}
|
|
@@ -123,14 +127,15 @@ export default function QuestionForm() {
|
|
|
123
127
|
// are implemented by Origami team.
|
|
124
128
|
// Ticket CI-2770 was created to address this in the future.
|
|
125
129
|
_jsxs(Overlay, { title: "Ask a question", closeCallback: () => {
|
|
130
|
+
focusDialogOpener?.();
|
|
126
131
|
setNewDisplayName(null);
|
|
127
132
|
dispatch(closeForm());
|
|
128
|
-
}, children: [profile.isSubscribed && (_jsx("div", { className: "overlay__content-box overlay__form-box", "data-testid": "question-form-overlay", "data-trackable-context-trigger_action": "open question form", children: _jsxs("form", { onSubmit: handleSubmit, "data-trackable": trackingNS(`submit_question_form_${postAnonymously ? 'anonymous' : 'displayname'}`), children: [_jsxs("div", { className: "overlay__user-container", children: [!postAnonymously && profile?.displayName && (_jsx(_Fragment, { children: _jsxs("span", { className: "overlay__user-container-display-name o3-type-body-highlight", children: [_jsx("i", { className: "overlay__message-icon", style: { maskImage: 'var(--o3-icon-user)' } }), profile.displayName] }) })), !profile?.displayName && (_jsxs("label", {
|
|
133
|
+
}, children: [profile.isSubscribed && (_jsx("div", { className: "overlay__content-box overlay__form-box", "data-testid": "question-form-overlay", "data-trackable-context-trigger_action": "open question form", children: _jsxs("form", { "aria-busy": isLoading, onSubmit: handleSubmit, "data-trackable": trackingNS(`submit_question_form_${postAnonymously ? 'anonymous' : 'displayname'}`), children: [_jsxs("div", { className: "overlay__user-container", children: [!postAnonymously && profile?.displayName && (_jsx(_Fragment, { children: _jsxs("span", { className: "overlay__user-container-display-name o3-type-body-highlight", children: [_jsx("i", { className: "overlay__message-icon", style: { maskImage: 'var(--o3-icon-user)' } }), _jsxs("span", { className: "o3-visually-hidden", children: ["Your user name is", ' '] }), profile.displayName] }) })), !profile?.displayName && (_jsxs("label", { htmlFor: "display-name", className: "question-form__displayname-input o-forms-field", "aria-live": "polite", "aria-atomic": "true", children: [_jsx("span", { className: "o-forms-title", children: _jsx("span", { className: "o3-type-body-highlight", children: "Choose a display name" }) }), _jsxs("span", { className: classNames([
|
|
129
134
|
'question-form__displayname__input',
|
|
130
135
|
'o-forms-input',
|
|
131
136
|
'o-forms-input--text',
|
|
132
137
|
{ 'o-forms-input--invalid': !!displayNameError },
|
|
133
|
-
]), children: [_jsx("input", { className: "question-form__displayname__input__field o3-type-body-lg", id: "display-name", value: newDisplayName || '', onChange: (e) => setNewDisplayName(e.currentTarget.value), type: postAnonymously ? 'password' : 'text', "aria-invalid": displayNameError ? 'true' :
|
|
138
|
+
]), children: [_jsx("input", { className: "question-form__displayname__input__field o3-type-body-lg", id: "display-name", value: newDisplayName || '', onChange: (e) => setNewDisplayName(e.currentTarget.value), type: postAnonymously ? 'password' : 'text', "aria-describedby": "question-form-disclaimer", "aria-invalid": displayNameError ? 'true' : undefined, "aria-errormessage": displayNameError ??
|
|
134
139
|
'question_form__displayname-error-message' }), displayNameError && (_jsx("span", { className: "question-form__displayname__input__error o3-type-detail", id: "question_form__displayname-error-message", children: _jsx("span", { children: displayNameError }) }))] })] }))] }), _jsx("div", { className: "o-forms-field", children: _jsxs("label", { className: "o-forms-input o-forms-input--checkbox", htmlFor: "post-anonymously", children: [_jsx("input", { type: "checkbox", name: "post-anonymously", id: "post-anonymously", value: "true", onChange: handleHideDisplayName, "data-trackable-context-action": `Post anonymously ${postAnonymously ? 'off' : 'on'}`, "data-trackable": trackingNS(`question-form__post_anonymously-${postAnonymously ? 'off' : 'on'}`) }), _jsx("span", { className: "o-forms-input__label", children: "Post anonymously (optional)" })] }) }), _jsxs("label", { for: "question-text", className: "o-forms-field question-form__question__input", children: [_jsx("span", { className: "o-forms-title", children: _jsx("span", { className: "o3-type-body-highlight", children: "Enter your question" }) }), _jsxs("div", { className: classNames([
|
|
135
140
|
'o-forms-input',
|
|
136
141
|
'o-forms-input--textarea',
|
|
@@ -138,7 +143,7 @@ export default function QuestionForm() {
|
|
|
138
143
|
{
|
|
139
144
|
'input-textarea__error': question.length > MAX_QUESTION_LENGTH,
|
|
140
145
|
},
|
|
141
|
-
]), children: [_jsx("textarea", { id: "question-text", name: "question", className: "input-textarea", value: question, onChange: (e) => dispatch(updateQuestion(e.currentTarget.value)), "aria-label": "Your question", placeholder: "Type your question",
|
|
146
|
+
]), children: [_jsx("textarea", { id: "question-text", name: "question", className: "input-textarea", value: question, onChange: (e) => dispatch(updateQuestion(e.currentTarget.value)), "aria-label": "Your question", "aria-describedby": "question-form-description", placeholder: "Type your question", "aria-invalid": error ? 'true' : undefined, "aria-errormessage": error ? 'question-form-error-message' : undefined }), _jsx("span", { className: "o3-type-detail input-textarea__counter", "aria-label": `Your question is ${question.length} characters long, maximum allowed is ${MAX_QUESTION_LENGTH}`, children: `${question.length}/${MAX_QUESTION_LENGTH}` })] })] }), _jsx("span", { id: "question-form-description", "aria-live": "polite", className: classNames([
|
|
142
147
|
'o3-type-detail',
|
|
143
148
|
{
|
|
144
149
|
'input--textarea__error-message': question.length > MAX_QUESTION_LENGTH,
|
|
@@ -158,35 +163,35 @@ export default function QuestionForm() {
|
|
|
158
163
|
},
|
|
159
164
|
// @ts-expect-error - o3 Button is currently accepting a JSX element as label
|
|
160
165
|
// but the type definition needs to be updated by Origami team
|
|
161
|
-
label: isLoading ? _jsx(Loader, {}) : 'Submit your question', type: "primary", theme: "mono" }), !profile?.token && (_jsx("span", { className: "o3-type-detail", children: "Submitting a question will create a community profile with FT." }))] }) })), isSuccess && (_jsx(MessageBox, { color: "#00572C", extraClassNames: "o-message--alert o-message--success", title: "Question sent", text: "Your question is in the queue and will be reviewed by our editors." })), error && (_jsx(ErrorMessage, { error: error, storyId: storyId, errorMapping: {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
166
|
+
label: isLoading ? _jsx(Loader, {}) : 'Submit your question', type: "primary", theme: "mono" }), !profile?.token && (_jsx("span", { className: "o3-type-detail", id: "question-form-disclaimer", children: "Submitting a question will create a community profile with FT." }))] }) })), isSuccess && (_jsx("div", { role: "alert", "aria-live": "polite", children: _jsx(MessageBox, { color: "#00572C", extraClassNames: "o-message--alert o-message--success", title: "Question sent", text: "Your question is in the queue and will be reviewed by our editors." }) })), _jsx("div", { "aria-live": "polite", role: "alert", id: "question-form-error-message", children: error && (_jsx(ErrorMessage, { error: error, storyId: storyId, errorMapping: {
|
|
167
|
+
// Messages for specific error codes
|
|
168
|
+
REPEAT_POST: {
|
|
169
|
+
title: 'Question already submitted',
|
|
170
|
+
message: "You've already submitted this question. Try asking something different and submit again.",
|
|
171
|
+
},
|
|
172
|
+
REJECTED: {
|
|
173
|
+
title: 'Question rejected',
|
|
174
|
+
message: 'This question has been rejected for language that violates our guidelines.',
|
|
175
|
+
},
|
|
176
|
+
RATE_LIMIT_EXCEEDED: {
|
|
177
|
+
title: 'Too many questions',
|
|
178
|
+
message: 'You are submitting too many questions in a short amount of time. Please wait sometime before submitting another question.',
|
|
179
|
+
},
|
|
180
|
+
TOXIC_COMMENT: {
|
|
181
|
+
title: 'Question blocked',
|
|
182
|
+
message: 'Are you sure? The language in your question might violate our community guidelines. You can edit it, or if you submit, it will need moderator review. This could slow down a reply from our experts',
|
|
183
|
+
},
|
|
184
|
+
serverError: {
|
|
185
|
+
title: 'Server error',
|
|
186
|
+
message: 'There was a server error. Please try submitting your question again.',
|
|
187
|
+
},
|
|
188
|
+
connectionError: {
|
|
189
|
+
title: 'Connection error',
|
|
190
|
+
message: "You can't submit your question. Check your internet connection or browser settings and try again.",
|
|
191
|
+
},
|
|
192
|
+
timeoutError: {
|
|
193
|
+
title: 'Timeout error',
|
|
194
|
+
message: 'The request timed out. Please try submitting your question again.',
|
|
195
|
+
},
|
|
196
|
+
}, operation: "submit-question" })) }), _jsx(MessageBox, { extraClassNames: "o-message--notice o-message--inform", title: getTitleByUser(profile), text: getTextByUser(profile) })] }));
|
|
192
197
|
}
|
package/dist/esm/index.js
CHANGED
|
@@ -24,7 +24,7 @@ class QandaUI {
|
|
|
24
24
|
(containerElement.dataset.qandaMaxquestionlength
|
|
25
25
|
? parseInt(containerElement.dataset.qandaMaxquestionlength, 10)
|
|
26
26
|
: undefined) ??
|
|
27
|
-
|
|
27
|
+
300;
|
|
28
28
|
if (!storyId || !title || !commentsAPIHost || !commentsAPIVersion) {
|
|
29
29
|
throw new Error('Required properties are missing');
|
|
30
30
|
}
|
|
@@ -154,14 +154,14 @@ export const nextCommentsApi = createApi({
|
|
|
154
154
|
}),
|
|
155
155
|
// POST A QUESTION
|
|
156
156
|
postQuestion: builder.mutation({
|
|
157
|
-
query: ({ storyId, useStaging, commentsAPIUrl, token, question, postAnonymously, }) => ({
|
|
157
|
+
query: ({ storyId, useStaging, commentsAPIUrl, token, question, nudge = true, postAnonymously, }) => ({
|
|
158
158
|
url: `${commentsAPIUrl}/story/${storyId}/question${useStaging ? '?staging=1' : ''}`,
|
|
159
159
|
method: 'POST',
|
|
160
160
|
headers: {
|
|
161
161
|
'Content-Type': 'application/json',
|
|
162
162
|
Authorization: `Bearer ${token}`,
|
|
163
163
|
},
|
|
164
|
-
body: JSON.stringify({ question, postAnonymously, token }),
|
|
164
|
+
body: JSON.stringify({ question, postAnonymously, token, nudge }),
|
|
165
165
|
}),
|
|
166
166
|
transformResponse: (response) => {
|
|
167
167
|
return response.data;
|
package/dist/qanda.scss
CHANGED
|
@@ -268,7 +268,6 @@ $app-header-color: #007acc;
|
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
.qanda-portal.embed-form-field__container {
|
|
271
|
-
max-width: 300px;
|
|
272
271
|
display: flex;
|
|
273
272
|
flex-direction: column;
|
|
274
273
|
gap: var(--o3-spacing-4xs);
|
|
@@ -390,7 +389,7 @@ $app-header-color: #007acc;
|
|
|
390
389
|
right: 0;
|
|
391
390
|
bottom: 0;
|
|
392
391
|
z-index: 19;
|
|
393
|
-
animation: QandaOverlayFadeIn
|
|
392
|
+
animation: QandaOverlayFadeIn 300ms ease forwards;
|
|
394
393
|
background: var(--o3-color-palette-black-90);
|
|
395
394
|
margin-top: calc(var(--safe-area-inset-top, 0px) / 2);
|
|
396
395
|
}
|
|
@@ -402,7 +401,7 @@ $app-header-color: #007acc;
|
|
|
402
401
|
height: 100%;
|
|
403
402
|
width: 100%;
|
|
404
403
|
z-index: 20;
|
|
405
|
-
animation: QandaOverlaySlideUp
|
|
404
|
+
animation: QandaOverlaySlideUp 300ms ease forwards;
|
|
406
405
|
|
|
407
406
|
@media (min-width: 740px) {
|
|
408
407
|
width: 375px;
|
|
@@ -452,6 +451,13 @@ $app-header-color: #007acc;
|
|
|
452
451
|
flex-grow: 1;
|
|
453
452
|
align-content: center;
|
|
454
453
|
text-align: left;
|
|
454
|
+
outline: 0;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.overlay__header-title:focus,
|
|
458
|
+
.overlay__header-title:focus-visible {
|
|
459
|
+
outline: 0;
|
|
460
|
+
box-shadow: none;
|
|
455
461
|
}
|
|
456
462
|
|
|
457
463
|
.overlay__header-close {
|
|
@@ -739,6 +745,10 @@ $app-header-color: #007acc;
|
|
|
739
745
|
.message-box__container {
|
|
740
746
|
width: 100%;
|
|
741
747
|
text-align: left;
|
|
748
|
+
/* o3 forms is currently marked as experimental, we are using o-message, whose standard bg needs overwritten for accessibility */
|
|
749
|
+
&.o-message--error {
|
|
750
|
+
background-color: var(--o3-color-use-case-error-background);
|
|
751
|
+
}
|
|
742
752
|
}
|
|
743
753
|
|
|
744
754
|
.message-box__content {
|
|
@@ -3,10 +3,10 @@ type OverlayOptions = {
|
|
|
3
3
|
shadow?: boolean;
|
|
4
4
|
};
|
|
5
5
|
type OverlayProps = {
|
|
6
|
-
title:
|
|
6
|
+
title: string;
|
|
7
7
|
children: React.ReactNode;
|
|
8
8
|
closeCallback: () => void;
|
|
9
9
|
options?: OverlayOptions;
|
|
10
10
|
};
|
|
11
|
-
declare function Overlay({ title, children, closeCallback, options, }: OverlayProps): import("
|
|
11
|
+
declare function Overlay({ title, children, closeCallback, options, }: OverlayProps): import("react").JSX.Element;
|
|
12
12
|
export default Overlay;
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
type
|
|
1
|
+
type QandaSettings = {
|
|
2
2
|
storyId: string;
|
|
3
3
|
useStaging: boolean;
|
|
4
4
|
commentsAPIUrl: string;
|
|
5
5
|
title: string;
|
|
6
6
|
maxQuestionLength?: number;
|
|
7
7
|
};
|
|
8
|
+
type QandaContextType = QandaSettings & {
|
|
9
|
+
setDialogOpener?: (opener: HTMLButtonElement | null) => void;
|
|
10
|
+
focusDialogOpener?: () => void;
|
|
11
|
+
};
|
|
12
|
+
type QandaProviderProps = QandaSettings & {
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
};
|
|
8
15
|
export declare const QandaContext: import("preact").Context<QandaContextType>;
|
|
16
|
+
export declare const QandaProvider: ({ children, storyId, useStaging, commentsAPIUrl, title, maxQuestionLength, }: QandaProviderProps) => import("react").JSX.Element;
|
|
9
17
|
export {};
|