@financial-times/qanda-ui 0.0.2-beta.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,13 +40,17 @@ function Expander({ className = '', children = [], expandLabel = 'Expand', colla
40
40
  }
41
41
  const numberOfCharacters = element.textContent?.length || 300;
42
42
  const windowWidth = window.visualViewport?.width || window.innerWidth || 1200; // Fallback to desktop configuration.
43
- // Check if the element's height exceeds 3 lines for mobile or 1 line for above mobile
44
- // Note: This need to stay aligned with the CSS for the expander
45
- const mobileNumberOfCharacters = 100;
46
- const desktopNumberOfCharacters = 200;
43
+ // Approximate cut-off using character counts
44
+ // On average: ~176 characters on mobile (4 lines) and ~374 on desktop (5 lines)
45
+ // We set thresholds slightly higher (250 / 425) to ensure some text is hidden
46
+ // before showing the expander
47
+ // Note: This needs to stay aligned with the CSS for the expander
48
+ const mobileNumberOfCharacters = 225;
49
+ const desktopNumberOfCharacters = 425;
47
50
  const characterForExpander = windowWidth <= 490
48
51
  ? mobileNumberOfCharacters
49
52
  : desktopNumberOfCharacters;
53
+ // Show the expander if character count exceeds the threshold
50
54
  setExpander(numberOfCharacters > characterForExpander);
51
55
  };
52
56
  const debouncedCheck = debounce(checkNeedForExpanderButton, 250);
@@ -20,6 +20,7 @@ function ExpertDrawer() {
20
20
  if (isExpertFormOpen) {
21
21
  try {
22
22
  o_comments_1.default.init();
23
+ reliability_1.qandaReliability.success('expert-load-coral-ui');
23
24
  }
24
25
  catch (error) {
25
26
  reliability_1.qandaReliability.failure('expert-load-coral-ui', error);
@@ -9,11 +9,30 @@ const useIsMobile_1 = require("../../hooks/useIsMobile");
9
9
  const comments_api_1 = require("../../services/comments-api");
10
10
  const updateComments_1 = require("../../store/updateComments");
11
11
  const react_1 = require("react");
12
+ const o_utils_1 = require("@financial-times/o-utils");
12
13
  const QandaProvider_1 = require("../QandaProvider");
13
14
  const tracking_1 = require("../../utils/tracking");
14
15
  function LiveIndicator() {
15
16
  return ((0, jsx_runtime_1.jsx)("div", { className: "o-labels-indicator--live o3-type-label floating-action-bar__live-container", children: (0, jsx_runtime_1.jsx)("span", { className: "o-labels-indicator__status floating-action-bar__live-label", children: "Live" }) }));
16
17
  }
18
+ function useAlignToQanda(ref) {
19
+ (0, react_1.useEffect)(() => {
20
+ const align = () => {
21
+ const container = ref.current;
22
+ const qandaRoot = container?.closest('.qanda-container');
23
+ const target = qandaRoot?.querySelector('.qanda__blocks-container');
24
+ if (!target || !container)
25
+ return;
26
+ const rect = target.getBoundingClientRect();
27
+ container.style.left = `${rect.left}px`;
28
+ container.style.width = `${rect.width}px`;
29
+ };
30
+ align();
31
+ const debounced = (0, o_utils_1.debounce)(align, 100);
32
+ window.addEventListener('resize', debounced);
33
+ return () => window.removeEventListener('resize', debounced);
34
+ }, [ref]);
35
+ }
17
36
  function FloatingActionBar({ isLive = true }) {
18
37
  const dispatch = (0, react_redux_1.useDispatch)();
19
38
  const isMobile = (0, useIsMobile_1.useIsMobile)();
@@ -58,17 +77,19 @@ function FloatingActionBar({ isLive = true }) {
58
77
  dataTrackable: 'open_expert_drawer',
59
78
  };
60
79
  const view = showExpertView ? expertConfig : userConfig;
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: {
80
+ const containerRef = (0, react_1.useRef)(null);
81
+ useAlignToQanda(containerRef);
82
+ return ((0, jsx_runtime_1.jsxs)("div", { ref: containerRef, 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: {
62
83
  ariaLabel: view.description,
63
84
  'data-trackable': (0, tracking_1.trackingNS)(view.dataTrackable),
64
85
  'data-trackable-context-action': view.description,
65
86
  'data-testid': `floating-action-bar__${view.testIdSuffix}`,
66
87
  ariaHasPopup: 'dialog',
67
88
  }, label: view.description, type: "ghost", theme: "mono", icon: "edit", iconOnly: isMobile, onClick: view.onClick }), (0, jsx_runtime_1.jsx)(o3_button_1.Button, { attributes: {
68
- ariaLabel: 'Back to top',
69
- 'data-trackable': (0, tracking_1.trackingNS)('back_to_top'),
70
- 'data-trackable-context-trigger_action': 'Back to top',
71
- 'data-testid': 'floating-action-bar__back-to-top-button',
72
- }, label: "Back to top", type: "ghost", theme: "mono", icon: "scroll-to", iconOnly: isMobile, onClick: handleShowUpdates })] }), updatedComments?.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "floating-action-bar__updates-counter o3-type-detail", "aria-label": `${updatedComments.length} new ${updatedComments.length === 1 ? 'update' : 'updates'} available`, role: "status", "aria-live": "polite", children: updatedComments.length }))] })] }));
89
+ ariaLabel: 'Latest answers',
90
+ 'data-trackable': (0, tracking_1.trackingNS)('latest-answers'),
91
+ 'data-trackable-context-trigger_action': 'Latest answers',
92
+ 'data-testid': 'floating-action-bar__latest-answers-button',
93
+ }, label: "Latest answers", type: "ghost", theme: "mono", icon: "scroll-to", iconOnly: isMobile, onClick: handleShowUpdates })] }), updatedComments?.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "floating-action-bar__updates-counter o3-type-detail", "aria-label": `${updatedComments.length} new ${updatedComments.length === 1 ? 'update' : 'updates'} available`, role: "status", "aria-live": "polite", children: updatedComments.length }))] })] }));
73
94
  }
74
95
  exports.default = FloatingActionBar;
@@ -50,7 +50,7 @@ function Stream({ status = 'pre-live' }) {
50
50
  });
51
51
  }, [data, status]);
52
52
  return ((0, jsx_runtime_1.jsxs)("section", { className: "qanda__stream", "data-trackable-context-trigger_action": "view qanda container", children: [data && data.qandas.length > 0 && ((0, jsx_runtime_1.jsx)("h2", { className: "o3-type-title-md qanda__stream-title", id: "qanda__stream-title", children: data.status && data.status === 'pre-live'
53
- ? 'Top questions so far'
53
+ ? 'Need inspiration? A selection of reader questions so far'
54
54
  : 'What our readers are asking' })), (0, jsx_runtime_1.jsxs)("div", { className: "qanda__blocks-container", "data-testid": "qanda__blocks-container", id: "qanda__blocks-container", "aria-busy": isLoading, children: [isLoading && ((0, jsx_runtime_1.jsx)(Loader_1.default, { theme: "dark", extraClassNames: "qanda__stream-loader" })), error && ((0, jsx_runtime_1.jsx)(ErrorMessage_1.default, { error: error, storyId: storyId, operation: "loading-stream" })), data &&
55
55
  data.qandas &&
56
56
  data.qandas.map((qandaBlock) => ((0, jsx_runtime_1.jsx)(index_1.default, { id: qandaBlock.id, body: qandaBlock.body, type: "comment", publishedDate: qandaBlock.publishedDate, author: qandaBlock.author, byline: qandaBlock.byline, showAnswerTime: data.status === 'live', children: qandaBlock.children }, qandaBlock.id)))] })] }));
@@ -38,13 +38,17 @@ function Expander({ className = '', children = [], expandLabel = 'Expand', colla
38
38
  }
39
39
  const numberOfCharacters = element.textContent?.length || 300;
40
40
  const windowWidth = window.visualViewport?.width || window.innerWidth || 1200; // Fallback to desktop configuration.
41
- // Check if the element's height exceeds 3 lines for mobile or 1 line for above mobile
42
- // Note: This need to stay aligned with the CSS for the expander
43
- const mobileNumberOfCharacters = 100;
44
- const desktopNumberOfCharacters = 200;
41
+ // Approximate cut-off using character counts
42
+ // On average: ~176 characters on mobile (4 lines) and ~374 on desktop (5 lines)
43
+ // We set thresholds slightly higher (250 / 425) to ensure some text is hidden
44
+ // before showing the expander
45
+ // Note: This needs to stay aligned with the CSS for the expander
46
+ const mobileNumberOfCharacters = 225;
47
+ const desktopNumberOfCharacters = 425;
45
48
  const characterForExpander = windowWidth <= 490
46
49
  ? mobileNumberOfCharacters
47
50
  : desktopNumberOfCharacters;
51
+ // Show the expander if character count exceeds the threshold
48
52
  setExpander(numberOfCharacters > characterForExpander);
49
53
  };
50
54
  const debouncedCheck = debounce(checkNeedForExpanderButton, 250);
@@ -15,6 +15,7 @@ function ExpertDrawer() {
15
15
  if (isExpertFormOpen) {
16
16
  try {
17
17
  OComments.init();
18
+ qandaReliability.success('expert-load-coral-ui');
18
19
  }
19
20
  catch (error) {
20
21
  qandaReliability.failure('expert-load-coral-ui', error);
@@ -6,12 +6,31 @@ 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, useRef } from 'react';
9
+ import { useContext, useRef, useEffect } from 'react';
10
+ import { debounce } from '@financial-times/o-utils';
10
11
  import { QandaContext } from '../QandaProvider';
11
12
  import { trackingNS } from '../../utils/tracking';
12
13
  function LiveIndicator() {
13
14
  return (_jsx("div", { className: "o-labels-indicator--live o3-type-label floating-action-bar__live-container", children: _jsx("span", { className: "o-labels-indicator__status floating-action-bar__live-label", children: "Live" }) }));
14
15
  }
16
+ function useAlignToQanda(ref) {
17
+ useEffect(() => {
18
+ const align = () => {
19
+ const container = ref.current;
20
+ const qandaRoot = container?.closest('.qanda-container');
21
+ const target = qandaRoot?.querySelector('.qanda__blocks-container');
22
+ if (!target || !container)
23
+ return;
24
+ const rect = target.getBoundingClientRect();
25
+ container.style.left = `${rect.left}px`;
26
+ container.style.width = `${rect.width}px`;
27
+ };
28
+ align();
29
+ const debounced = debounce(align, 100);
30
+ window.addEventListener('resize', debounced);
31
+ return () => window.removeEventListener('resize', debounced);
32
+ }, [ref]);
33
+ }
15
34
  function FloatingActionBar({ isLive = true }) {
16
35
  const dispatch = useDispatch();
17
36
  const isMobile = useIsMobile();
@@ -56,17 +75,19 @@ function FloatingActionBar({ isLive = true }) {
56
75
  dataTrackable: 'open_expert_drawer',
57
76
  };
58
77
  const view = showExpertView ? expertConfig : userConfig;
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: {
78
+ const containerRef = useRef(null);
79
+ useAlignToQanda(containerRef);
80
+ return (_jsxs("div", { ref: containerRef, 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: {
60
81
  ariaLabel: view.description,
61
82
  'data-trackable': trackingNS(view.dataTrackable),
62
83
  'data-trackable-context-action': view.description,
63
84
  'data-testid': `floating-action-bar__${view.testIdSuffix}`,
64
85
  ariaHasPopup: 'dialog',
65
86
  }, label: view.description, type: "ghost", theme: "mono", icon: "edit", iconOnly: isMobile, onClick: view.onClick }), _jsx(Button, { attributes: {
66
- ariaLabel: 'Back to top',
67
- 'data-trackable': trackingNS('back_to_top'),
68
- 'data-trackable-context-trigger_action': 'Back to top',
69
- 'data-testid': 'floating-action-bar__back-to-top-button',
70
- }, label: "Back to top", type: "ghost", theme: "mono", icon: "scroll-to", iconOnly: isMobile, onClick: handleShowUpdates })] }), updatedComments?.length > 0 && (_jsx("div", { className: "floating-action-bar__updates-counter o3-type-detail", "aria-label": `${updatedComments.length} new ${updatedComments.length === 1 ? 'update' : 'updates'} available`, role: "status", "aria-live": "polite", children: updatedComments.length }))] })] }));
87
+ ariaLabel: 'Latest answers',
88
+ 'data-trackable': trackingNS('latest-answers'),
89
+ 'data-trackable-context-trigger_action': 'Latest answers',
90
+ 'data-testid': 'floating-action-bar__latest-answers-button',
91
+ }, label: "Latest answers", type: "ghost", theme: "mono", icon: "scroll-to", iconOnly: isMobile, onClick: handleShowUpdates })] }), updatedComments?.length > 0 && (_jsx("div", { className: "floating-action-bar__updates-counter o3-type-detail", "aria-label": `${updatedComments.length} new ${updatedComments.length === 1 ? 'update' : 'updates'} available`, role: "status", "aria-live": "polite", children: updatedComments.length }))] })] }));
71
92
  }
72
93
  export default FloatingActionBar;
@@ -45,7 +45,7 @@ function Stream({ status = 'pre-live' }) {
45
45
  });
46
46
  }, [data, status]);
47
47
  return (_jsxs("section", { className: "qanda__stream", "data-trackable-context-trigger_action": "view qanda container", children: [data && data.qandas.length > 0 && (_jsx("h2", { className: "o3-type-title-md qanda__stream-title", id: "qanda__stream-title", children: data.status && data.status === 'pre-live'
48
- ? 'Top questions so far'
48
+ ? 'Need inspiration? A selection of reader questions so far'
49
49
  : 'What our readers are asking' })), _jsxs("div", { className: "qanda__blocks-container", "data-testid": "qanda__blocks-container", id: "qanda__blocks-container", "aria-busy": isLoading, children: [isLoading && (_jsx(Loader, { theme: "dark", extraClassNames: "qanda__stream-loader" })), error && (_jsx(ErrorMessage, { error: error, storyId: storyId, operation: "loading-stream" })), data &&
50
50
  data.qandas &&
51
51
  data.qandas.map((qandaBlock) => (_jsx(QandaBlock, { id: qandaBlock.id, body: qandaBlock.body, type: "comment", publishedDate: qandaBlock.publishedDate, author: qandaBlock.author, byline: qandaBlock.byline, showAnswerTime: data.status === 'live', children: qandaBlock.children }, qandaBlock.id)))] })] }));
package/dist/qanda.scss CHANGED
@@ -38,6 +38,7 @@ $app-header-color: #007acc;
38
38
 
39
39
  .qanda-container {
40
40
  .live-qa-calendar-container {
41
+ position: relative;
41
42
  width: 100%;
42
43
  }
43
44
 
@@ -55,6 +56,11 @@ $app-header-color: #007acc;
55
56
 
56
57
  .live-qa-calendar-list {
57
58
  display: flex;
59
+ position: absolute;
60
+ top: 100%;
61
+ left: 0;
62
+ z-index: 1000;
63
+ width: 100%;
58
64
  margin-top: 0;
59
65
  flex-direction: column;
60
66
  align-items: flex-start;
@@ -167,9 +173,9 @@ $app-header-color: #007acc;
167
173
  .floating-action-bar__container {
168
174
  position: fixed;
169
175
  bottom: 0;
170
- left: 0;
171
- right: 0;
172
- display: flex;
176
+ width: auto;
177
+ left: auto;
178
+ right: auto;
173
179
  /* Ensure FAB is on top of ...
174
180
  1. some dinamically loaded teasers at the bottom of the article
175
181
  2. the sharing bar on mobile
@@ -198,8 +204,9 @@ $app-header-color: #007acc;
198
204
  height: 48px;
199
205
  background: var(--o3-color-palette-white-60);
200
206
  border-radius: 100px;
201
- margin: auto;
202
- margin-bottom: var(--o3-spacing-s);
207
+ width: max-content;
208
+ max-width: 100%;
209
+ margin: 0 auto var(--o3-spacing-s);
203
210
  padding: 0 var(--o3-spacing-2xs);
204
211
  display: flex;
205
212
  align-items: center;
@@ -601,33 +608,24 @@ $app-header-color: #007acc;
601
608
  }
602
609
  }
603
610
 
604
- /* Mobile will show 3 lines of text before ellipse */
605
- @container (max-width: 490px) {
606
- .qanda-block__text.expander--collapsed
607
- .expander__text--multiline
608
- > p:first-of-type {
609
- display: block;
610
- display: -webkit-box;
611
- /* Note: This need to stay aligned with the JS for the expander */
612
- line-clamp: 3;
613
- -webkit-line-clamp: 3;
614
- -webkit-box-orient: vertical;
615
- overflow: hidden;
616
- text-overflow: ellipsis;
617
- /* Without this the letter like `g` may be visually cut.
611
+ .qanda-block__text.expander--collapsed
612
+ .expander__text--multiline
613
+ > p:first-of-type {
614
+ display: block;
615
+ display: -webkit-box;
616
+ /* Note: This need to stay aligned with the JS for the expander */
617
+ line-clamp: 4;
618
+ -webkit-line-clamp: 4;
619
+ -webkit-box-orient: vertical;
620
+ overflow: hidden;
621
+ text-overflow: ellipsis;
622
+ /* Without this the letter like `g` may be visually cut.
618
623
  To be tested with final font */
619
- padding-bottom: 1px;
620
- }
621
- }
622
-
623
- /* Above mobile it will show 1 line of text before ellipse */
624
- @container (min-width: 491px) {
625
- .qanda-block__text.expander--collapsed
626
- .expander__text--multiline
627
- > p:first-of-type {
628
- white-space: nowrap;
629
- text-overflow: ellipsis;
630
- overflow: hidden;
624
+ padding-bottom: 1px;
625
+ @container (min-width: 491px) {
626
+ /* Note: This need to stay aligned with the JS for the expander */
627
+ line-clamp: 5;
628
+ -webkit-line-clamp: 5;
631
629
  }
632
630
  }
633
631
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@financial-times/qanda-ui",
3
- "version": "0.0.2-beta.1",
3
+ "version": "1.0.1",
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",
@@ -40,6 +40,7 @@
40
40
  },
41
41
  "homepage": "https://github.com/Financial-Times/qanda-ui#readme",
42
42
  "dependencies": {
43
+ "@financial-times/o-utils": "^2.2.1",
43
44
  "@reduxjs/toolkit": "^1.9.5",
44
45
  "classnames": "^2.5.1"
45
46
  },
@@ -49,7 +50,6 @@
49
50
  "@babel/preset-env": "^7.26.0",
50
51
  "@babel/preset-react": "^7.26.3",
51
52
  "@babel/preset-typescript": "^7.26.0",
52
- "@types/react-redux": "^7.1.34",
53
53
  "@dotcom-tool-kit/component": "^5.1.12",
54
54
  "@dotcom-tool-kit/jest": "^4.3.1",
55
55
  "@financial-times/o-colors": "^6.7.0",
@@ -59,6 +59,7 @@
59
59
  "@testing-library/preact": "^3.2.4",
60
60
  "@types/jest": "^29.5.14",
61
61
  "@types/jest-axe": "^3.5.9",
62
+ "@types/react-redux": "^7.1.34",
62
63
  "@typescript-eslint/eslint-plugin": "^8.18.0",
63
64
  "@typescript-eslint/parser": "^8.18.0",
64
65
  "babel-jest": "^29.7.0",