@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.
Files changed (34) hide show
  1. package/dist/cjs/components/Calendar/index.js +2 -1
  2. package/dist/cjs/components/CountdownTimer/index.js +2 -1
  3. package/dist/cjs/components/EmbedFormField/index.js +7 -1
  4. package/dist/cjs/components/ErrorMessage/humanReadableError.js +7 -1
  5. package/dist/cjs/components/ExpertDrawer/index.js +5 -2
  6. package/dist/cjs/components/FloatingActionBar/index.js +15 -3
  7. package/dist/cjs/components/MessageBox/index.js +1 -1
  8. package/dist/cjs/components/Overlay/index.js +19 -2
  9. package/dist/cjs/components/Qanda.js +1 -7
  10. package/dist/cjs/components/QandaBlock/index.js +1 -1
  11. package/dist/cjs/components/QandaContainer.js +3 -2
  12. package/dist/cjs/components/QandaProvider.js +31 -1
  13. package/dist/cjs/components/QuestionForm/index.js +42 -37
  14. package/dist/cjs/index.js +1 -1
  15. package/dist/cjs/services/comments-api.js +2 -2
  16. package/dist/esm/components/Calendar/index.js +2 -1
  17. package/dist/esm/components/CountdownTimer/index.js +3 -2
  18. package/dist/esm/components/EmbedFormField/index.js +8 -2
  19. package/dist/esm/components/ErrorMessage/humanReadableError.js +7 -1
  20. package/dist/esm/components/ExpertDrawer/index.js +5 -2
  21. package/dist/esm/components/FloatingActionBar/index.js +16 -4
  22. package/dist/esm/components/MessageBox/index.js +1 -1
  23. package/dist/esm/components/Overlay/index.js +19 -2
  24. package/dist/esm/components/Qanda.js +2 -8
  25. package/dist/esm/components/QandaBlock/index.js +1 -1
  26. package/dist/esm/components/QandaContainer.js +4 -3
  27. package/dist/esm/components/QandaProvider.js +30 -1
  28. package/dist/esm/components/QuestionForm/index.js +42 -37
  29. package/dist/esm/index.js +1 -1
  30. package/dist/esm/services/comments-api.js +2 -2
  31. package/dist/qanda.scss +13 -3
  32. package/dist/types/components/Overlay/index.d.ts +2 -2
  33. package/dist/types/components/QandaProvider.d.ts +9 -1
  34. 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-controls': 'menu',
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.jsx)("time", { datetime: status === 'pre-live' ? startDateTime : endDateTime, children: count })) }) }) }));
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)("div", { onClick: () => dispatch((0, questionSlice_1.openForm)()), 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", 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"] })] }));
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
- this._humanReadableError = errorMessages.serverError;
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: () => dispatch((0, expertSlice_1.closeExpertForm)()), options: {
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: () => dispatch((0, questionSlice_1.openForm)()),
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", { class: `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", { class: "o-message__container", children: (0, jsx_runtime_1.jsxs)("div", { class: "message-box__content", children: [(0, jsx_runtime_1.jsx)("span", { style: { color }, class: "o3-type-body-highlight", children: title }), (0, jsx_runtime_1.jsx)("span", { style: { color }, class: "o3-type-body-base", children: text })] }) }) }));
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
- return ((0, jsx_runtime_1.jsxs)("div", { className: "overlay", children: [!options.allowInteraction && (0, jsx_runtime_1.jsx)("div", { className: "overlay__outer" }), (0, jsx_runtime_1.jsxs)("div", { className: (0, classnames_1.default)([
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.QandaContext.Provider, { value: {
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.jsx)("div", { className: "o3-type-body-highlight qanda-block__comment-byline", children: (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.jsx)("div", { className: "o3-type-body-highlight qanda-block__comment-byline", children: 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.jsx)("span", { className: "o3-type-detail", children: 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)))] }));
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.jsxs)("p", { children: ["We are not able to load ", qanda_1.PRODUCT_NAME, " at the moment. Please try again later. Error:", error] })), !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 })] }));
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", { for: "display-name", className: "question-form__displayname-input o-forms-field", 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
+ }, 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' : 'false', "aria-errormessage": displayNameError ??
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", maxLength: maxQuestionLength, "aria-invalid": error ? 'true' : undefined, "aria-errormessage": "question-form-error-message" }), (0, jsx_runtime_1.jsx)("span", { className: "o3-type-detail input-textarea__counter", children: `${question.length}/${MAX_QUESTION_LENGTH}` })] })] }), (0, jsx_runtime_1.jsx)("span", { className: (0, classnames_1.default)([
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
- // Messages for specific error codes
169
- REPEAT_POST: {
170
- title: 'Question already submitted',
171
- message: "You've already submitted this question. Try asking something different and submit again.",
172
- },
173
- REJECTED: {
174
- title: 'Question rejected',
175
- message: 'This question has been rejected for language that violates our guidelines.',
176
- },
177
- RATE_LIMIT_EXCEEDED: {
178
- title: 'Too many questions',
179
- message: 'You are submitting too many questions in a short amount of time. Please wait sometime before submitting another question.',
180
- },
181
- TOXIC_COMMENT: {
182
- title: 'Question blocked',
183
- message: 'Are you sure? The language in your question might violate our community guidelines. You can edit it or submit it for moderator review.',
184
- },
185
- serverError: {
186
- title: 'Server error',
187
- message: 'There was a server error. Please try submitting your question again.',
188
- },
189
- connectionError: {
190
- title: 'Connection error',
191
- message: "You can't submit your question. Check your internet connection or browser settings and try again.",
192
- },
193
- timeoutError: {
194
- title: 'Timeout error',
195
- message: 'The request timed out. Please try submitting your question again.',
196
- },
197
- }, operation: "submit-question", id: "question-form-error-message" })), (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) })] }));
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
- 2000;
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-controls': 'menu',
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 && (_jsx("time", { datetime: status === 'pre-live' ? startDateTime : endDateTime, 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("div", { onClick: () => dispatch(openForm()), 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", children: [_jsx("span", { className: "embed-form-field__icon", children: _jsx("i", { style: { maskImage: 'var(--o3-icon-user)' } }) }), "Enter your question"] })] }));
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
- this._humanReadableError = errorMessages.serverError;
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: () => dispatch(closeExpertForm()), options: {
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: () => dispatch(openForm()),
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", { class: `o-message o-message--inner message-box__container ${extraClassNames}`, "data-o-message-close": "false", "data-o-component": "o-message", id: id, children: _jsx("div", { class: "o-message__container", children: _jsxs("div", { class: "message-box__content", children: [_jsx("span", { style: { color }, class: "o3-type-body-highlight", children: title }), _jsx("span", { style: { color }, class: "o3-type-body-base", children: text })] }) }) }));
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
- return (_jsxs("div", { className: "overlay", children: [!options.allowInteraction && _jsx("div", { className: "overlay__outer" }), _jsxs("div", { className: classNames([
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 { QandaContext } from './QandaProvider';
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(QandaContext.Provider, { value: {
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 })), _jsx("div", { className: "o3-type-body-highlight qanda-block__comment-byline", children: _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: [_jsx("div", { className: "o3-type-body-highlight qanda-block__comment-byline", children: 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 && (_jsx("span", { className: "o3-type-detail", children: 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)))] }));
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 { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "preact/jsx-runtime";
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 && (_jsxs("p", { children: ["We are not able to load ", PRODUCT_NAME, " at the moment. Please try again later. Error:", error] })), !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 })] }));
66
67
  }
67
68
  export default QandaContainer;
@@ -1,4 +1,5 @@
1
- import { createContext } from 'react';
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", { for: "display-name", className: "question-form__displayname-input o-forms-field", 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
+ }, 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' : 'false', "aria-errormessage": displayNameError ??
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", maxLength: maxQuestionLength, "aria-invalid": error ? 'true' : undefined, "aria-errormessage": "question-form-error-message" }), _jsx("span", { className: "o3-type-detail input-textarea__counter", children: `${question.length}/${MAX_QUESTION_LENGTH}` })] })] }), _jsx("span", { className: classNames([
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
- // Messages for specific error codes
163
- REPEAT_POST: {
164
- title: 'Question already submitted',
165
- message: "You've already submitted this question. Try asking something different and submit again.",
166
- },
167
- REJECTED: {
168
- title: 'Question rejected',
169
- message: 'This question has been rejected for language that violates our guidelines.',
170
- },
171
- RATE_LIMIT_EXCEEDED: {
172
- title: 'Too many questions',
173
- message: 'You are submitting too many questions in a short amount of time. Please wait sometime before submitting another question.',
174
- },
175
- TOXIC_COMMENT: {
176
- title: 'Question blocked',
177
- message: 'Are you sure? The language in your question might violate our community guidelines. You can edit it or submit it for moderator review.',
178
- },
179
- serverError: {
180
- title: 'Server error',
181
- message: 'There was a server error. Please try submitting your question again.',
182
- },
183
- connectionError: {
184
- title: 'Connection error',
185
- message: "You can't submit your question. Check your internet connection or browser settings and try again.",
186
- },
187
- timeoutError: {
188
- title: 'Timeout error',
189
- message: 'The request timed out. Please try submitting your question again.',
190
- },
191
- }, operation: "submit-question", id: "question-form-error-message" })), _jsx(MessageBox, { extraClassNames: "o-message--notice o-message--inform", title: getTitleByUser(profile), text: getTextByUser(profile) })] }));
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
- 2000;
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 1s ease forwards;
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 1s ease forwards;
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: String;
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("preact").JSX.Element;
11
+ declare function Overlay({ title, children, closeCallback, options, }: OverlayProps): import("react").JSX.Element;
12
12
  export default Overlay;
@@ -1,9 +1,17 @@
1
- type QandaContextType = {
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 {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@financial-times/qanda-ui",
3
- "version": "0.0.1-beta.37",
3
+ "version": "0.0.1-beta.39",
4
4
  "description": "Components for the Live Q&A (AKA Ask an Expert) UI",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",