@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.
Files changed (32) 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 +1 -1
  12. package/dist/cjs/components/QandaProvider.js +31 -1
  13. package/dist/cjs/components/QuestionForm/index.js +38 -37
  14. package/dist/cjs/index.js +1 -1
  15. package/dist/esm/components/Calendar/index.js +2 -1
  16. package/dist/esm/components/CountdownTimer/index.js +3 -2
  17. package/dist/esm/components/EmbedFormField/index.js +8 -2
  18. package/dist/esm/components/ErrorMessage/humanReadableError.js +7 -1
  19. package/dist/esm/components/ExpertDrawer/index.js +5 -2
  20. package/dist/esm/components/FloatingActionBar/index.js +16 -4
  21. package/dist/esm/components/MessageBox/index.js +1 -1
  22. package/dist/esm/components/Overlay/index.js +19 -2
  23. package/dist/esm/components/Qanda.js +2 -8
  24. package/dist/esm/components/QandaBlock/index.js +1 -1
  25. package/dist/esm/components/QandaContainer.js +1 -1
  26. package/dist/esm/components/QandaProvider.js +30 -1
  27. package/dist/esm/components/QuestionForm/index.js +38 -37
  28. package/dist/esm/index.js +1 -1
  29. package/dist/qanda.scss +13 -3
  30. package/dist/types/components/Overlay/index.d.ts +2 -2
  31. package/dist/types/components/QandaProvider.d.ts +9 -1
  32. 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;
@@ -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", { 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)([
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' : '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 ??
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", 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)([
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
- // Messages for specific error codes
173
- REPEAT_POST: {
174
- title: 'Question already submitted',
175
- message: "You've already submitted this question. Try asking something different and submit again.",
176
- },
177
- REJECTED: {
178
- title: 'Question rejected',
179
- message: 'This question has been rejected for language that violates our guidelines.',
180
- },
181
- RATE_LIMIT_EXCEEDED: {
182
- title: 'Too many questions',
183
- message: 'You are submitting too many questions in a short amount of time. Please wait sometime before submitting another question.',
184
- },
185
- TOXIC_COMMENT: {
186
- title: 'Question blocked',
187
- 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',
188
- },
189
- serverError: {
190
- title: 'Server error',
191
- message: 'There was a server error. Please try submitting your question again.',
192
- },
193
- connectionError: {
194
- title: 'Connection error',
195
- message: "You can't submit your question. Check your internet connection or browser settings and try again.",
196
- },
197
- timeoutError: {
198
- title: 'Timeout error',
199
- message: 'The request timed out. Please try submitting your question again.',
200
- },
201
- }, 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) })] }));
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
- 2000;
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-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;
@@ -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 { 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
@@ -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", { 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([
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' : '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 ??
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", 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([
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
- // Messages for specific error codes
167
- REPEAT_POST: {
168
- title: 'Question already submitted',
169
- message: "You've already submitted this question. Try asking something different and submit again.",
170
- },
171
- REJECTED: {
172
- title: 'Question rejected',
173
- message: 'This question has been rejected for language that violates our guidelines.',
174
- },
175
- RATE_LIMIT_EXCEEDED: {
176
- title: 'Too many questions',
177
- message: 'You are submitting too many questions in a short amount of time. Please wait sometime before submitting another question.',
178
- },
179
- TOXIC_COMMENT: {
180
- title: 'Question blocked',
181
- 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',
182
- },
183
- serverError: {
184
- title: 'Server error',
185
- message: 'There was a server error. Please try submitting your question again.',
186
- },
187
- connectionError: {
188
- title: 'Connection error',
189
- message: "You can't submit your question. Check your internet connection or browser settings and try again.",
190
- },
191
- timeoutError: {
192
- title: 'Timeout error',
193
- message: 'The request timed out. Please try submitting your question again.',
194
- },
195
- }, 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) })] }));
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
- 2000;
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 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.38",
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",