@financial-times/qanda-ui 0.0.1-beta.38 → 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 +1 -1
- package/dist/cjs/components/QandaProvider.js +31 -1
- package/dist/cjs/components/QuestionForm/index.js +38 -37
- package/dist/cjs/index.js +1 -1
- 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 +1 -1
- package/dist/esm/components/QandaProvider.js +30 -1
- package/dist/esm/components/QuestionForm/index.js +38 -37
- package/dist/esm/index.js +1 -1
- 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;
|
|
@@ -68,6 +68,6 @@ function QandaContainer({ embedFormFieldContainer, countdownTimerContainer, }) {
|
|
|
68
68
|
dispatch(comments_api_1.nextCommentsApi.util.resetApiState());
|
|
69
69
|
};
|
|
70
70
|
}, [dispatch, storyId]);
|
|
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 && ((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
|
+
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 })] }));
|
|
72
72
|
}
|
|
73
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
|
|
@@ -133,14 +133,15 @@ function QuestionForm() {
|
|
|
133
133
|
// are implemented by Origami team.
|
|
134
134
|
// Ticket CI-2770 was created to address this in the future.
|
|
135
135
|
(0, jsx_runtime_1.jsxs)(Overlay_1.default, { title: "Ask a question", closeCallback: () => {
|
|
136
|
+
focusDialogOpener?.();
|
|
136
137
|
setNewDisplayName(null);
|
|
137
138
|
dispatch((0, questionSlice_1.closeForm)());
|
|
138
|
-
}, 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)([
|
|
139
140
|
'question-form__displayname__input',
|
|
140
141
|
'o-forms-input',
|
|
141
142
|
'o-forms-input--text',
|
|
142
143
|
{ 'o-forms-input--invalid': !!displayNameError },
|
|
143
|
-
]), 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 ??
|
|
144
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)([
|
|
145
146
|
'o-forms-input',
|
|
146
147
|
'o-forms-input--textarea',
|
|
@@ -148,7 +149,7 @@ function QuestionForm() {
|
|
|
148
149
|
{
|
|
149
150
|
'input-textarea__error': question.length > MAX_QUESTION_LENGTH,
|
|
150
151
|
},
|
|
151
|
-
]), 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)([
|
|
152
153
|
'o3-type-detail',
|
|
153
154
|
{
|
|
154
155
|
'input--textarea__error-message': question.length > MAX_QUESTION_LENGTH,
|
|
@@ -168,35 +169,35 @@ function QuestionForm() {
|
|
|
168
169
|
},
|
|
169
170
|
// @ts-expect-error - o3 Button is currently accepting a JSX element as label
|
|
170
171
|
// but the type definition needs to be updated by Origami team
|
|
171
|
-
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: {
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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) })] }));
|
|
202
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
|
}
|
|
@@ -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;
|
|
@@ -63,6 +63,6 @@ function QandaContainer({ embedFormFieldContainer, countdownTimerContainer, }) {
|
|
|
63
63
|
dispatch(nextCommentsApi.util.resetApiState());
|
|
64
64
|
};
|
|
65
65
|
}, [dispatch, storyId]);
|
|
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 && (_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
|
+
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 })] }));
|
|
67
67
|
}
|
|
68
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
|
|
@@ -127,14 +127,15 @@ export default function QuestionForm() {
|
|
|
127
127
|
// are implemented by Origami team.
|
|
128
128
|
// Ticket CI-2770 was created to address this in the future.
|
|
129
129
|
_jsxs(Overlay, { title: "Ask a question", closeCallback: () => {
|
|
130
|
+
focusDialogOpener?.();
|
|
130
131
|
setNewDisplayName(null);
|
|
131
132
|
dispatch(closeForm());
|
|
132
|
-
}, 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([
|
|
133
134
|
'question-form__displayname__input',
|
|
134
135
|
'o-forms-input',
|
|
135
136
|
'o-forms-input--text',
|
|
136
137
|
{ 'o-forms-input--invalid': !!displayNameError },
|
|
137
|
-
]), 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 ??
|
|
138
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([
|
|
139
140
|
'o-forms-input',
|
|
140
141
|
'o-forms-input--textarea',
|
|
@@ -142,7 +143,7 @@ export default function QuestionForm() {
|
|
|
142
143
|
{
|
|
143
144
|
'input-textarea__error': question.length > MAX_QUESTION_LENGTH,
|
|
144
145
|
},
|
|
145
|
-
]), 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([
|
|
146
147
|
'o3-type-detail',
|
|
147
148
|
{
|
|
148
149
|
'input--textarea__error-message': question.length > MAX_QUESTION_LENGTH,
|
|
@@ -162,35 +163,35 @@ export default function QuestionForm() {
|
|
|
162
163
|
},
|
|
163
164
|
// @ts-expect-error - o3 Button is currently accepting a JSX element as label
|
|
164
165
|
// but the type definition needs to be updated by Origami team
|
|
165
|
-
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: {
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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) })] }));
|
|
196
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
|
}
|
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 {};
|