@thecb/components 11.11.0-beta.7 → 11.11.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thecb/components",
3
- "version": "11.11.0-beta.7",
3
+ "version": "11.11.0-beta.9",
4
4
  "description": "Common lib for CityBase react components",
5
5
  "main": "dist/index.cjs.js",
6
6
  "typings": "dist/index.d.ts",
@@ -23,6 +23,7 @@ const EditableList = ({
23
23
  titleWeight = "400",
24
24
  canAdd = true,
25
25
  addItem,
26
+ addItemDestination,
26
27
  removeItem,
27
28
  editItem,
28
29
  itemName,
@@ -168,6 +169,8 @@ const EditableList = ({
168
169
  <Box padding={items.length === 0 ? "0" : "1rem 0 0"}>
169
170
  <Placeholder
170
171
  text={addText}
172
+ isLink={!!addItemDestination}
173
+ destination={addItemDestination}
171
174
  action={addItem}
172
175
  dataQa={"Add " + qaPrefix}
173
176
  aria-label={addText}
@@ -8,6 +8,8 @@ import ButtonWithAction from "../../atoms/button-with-action";
8
8
  import { noop, arrowBorder } from "../../../util/general";
9
9
  import { fallbackValues } from "./Tooltip.theme";
10
10
 
11
+ const TOOLTIP_THEME_SOURCE = "Popover";
12
+
11
13
  const Tooltip = ({
12
14
  tooltipID,
13
15
  children,
@@ -33,15 +35,24 @@ const Tooltip = ({
33
35
  arrowBottom: "-8px",
34
36
  arrowLeft: "auto"
35
37
  },
38
+ customTriggerRole,
36
39
  backgroundColor = WHITE
37
40
  }) => {
41
+ /**
42
+ * closeTimeoutRef is used internally to store a timer ID for delaying tooltip close. It will have a `.current` property (the timer ID) when the effect or event handlers set it.
43
+ */
38
44
  const closeTimeoutRef = useRef(null);
45
+ /**
46
+ * containerRef is used to store a reference to the container element. It will have a `.current` property (the container element) when the effect or event handlers set it.
47
+ */
48
+ const containerRef = useRef(null);
49
+
39
50
  const [tooltipOpen, setTooltipOpen] = useState(false);
40
51
  const themeContext = useContext(ThemeContext);
41
52
  const themeValues = createThemeValues(
42
53
  themeContext,
43
54
  fallbackValues,
44
- "Tooltip"
55
+ TOOLTIP_THEME_SOURCE
45
56
  );
46
57
 
47
58
  const { top, right, bottom, left } = contentPosition;
@@ -59,38 +70,50 @@ const Tooltip = ({
59
70
  }
60
71
  };
61
72
 
62
- const handleMouseEnter = () => {
63
- if (closeTimeoutRef.current) {
64
- clearTimeout(closeTimeoutRef.current);
65
- closeTimeoutRef.current = null;
73
+ /**
74
+ * @function renderTrigger
75
+ * Renders the tooltip trigger element.
76
+ *
77
+ * When `hasCustomTrigger` is true, the provided child element is cloned and
78
+ * injected with the event handlers needed to control tooltip visibility:
79
+ * - onFocus/onBlur: open and close for keyboard users
80
+ * - onKeyDown: allows Escape to dismiss the tooltip
81
+ * - onTouchStart: open on tap for touch/mobile users (onFocus is unreliable on touch)
82
+ *
83
+ * Mouse interactions (hover) are handled at the container level via
84
+ * onMouseEnter/onMouseLeave, so they do not need to be injected here.
85
+ *
86
+ * Any existing event handlers on the child are preserved and called first,
87
+ * so the child's own behavior is not overridden.
88
+ *
89
+ * When no custom trigger is provided, a default ButtonWithAction is rendered
90
+ * using `triggerText` and `triggerButtonVariant`.
91
+ */
92
+ const renderTrigger = () => {
93
+ if (hasCustomTrigger && !children) {
94
+ console.warn(
95
+ "Tooltip: children prop is required when hasCustomTrigger is true"
96
+ );
66
97
  }
67
- handleToggleTooltip(true);
68
- };
69
-
70
- const handleMouseLeave = () => {
71
- closeTimeoutRef.current = setTimeout(() => {
72
- handleToggleTooltip(false);
73
- }, 300);
74
- };
75
98
 
76
- useEffect(() => {
77
- return () => {
78
- if (closeTimeoutRef.current) {
79
- clearTimeout(closeTimeoutRef.current);
80
- }
81
- };
82
- }, []);
83
-
84
- const renderTrigger = () => {
85
99
  if (hasCustomTrigger && children) {
86
100
  const child = React.Children.only(children);
101
+ /**
102
+ * Capture the child's existing handlers before overwriting
103
+ */
87
104
  const {
88
105
  onFocus: childOnFocus,
89
106
  onBlur: childOnBlur,
90
- onKeyDown: childOnKeyDown
107
+ onKeyDown: childOnKeyDown,
108
+ onTouchStart: childOnTouchStart
91
109
  } = child.props ?? {};
110
+
111
+ /**
112
+ * Clone the child element and add the necessary event handlers
113
+ */
92
114
  return React.cloneElement(child, {
93
- "aria-describedby": tooltipID,
115
+ tabIndex: child.props?.tabIndex ?? 0,
116
+ style: { cursor: `pointer`, ...child.props?.style },
94
117
  onFocus: e => {
95
118
  childOnFocus?.(e);
96
119
  handleToggleTooltip(true);
@@ -103,35 +126,90 @@ const Tooltip = ({
103
126
  childOnKeyDown?.(e);
104
127
  handleKeyDown(e);
105
128
  },
106
- tabIndex: "0",
107
- style: { ...child.props?.style, cursor: "pointer" }
129
+ onTouchStart: e => {
130
+ childOnTouchStart?.(e);
131
+ handleToggleTooltip(true);
132
+ },
133
+ role: customTriggerRole || child.props?.role,
134
+ "aria-describedby": tooltipID,
135
+ "data-qa": `tooltip-trigger-${tooltipID}`
108
136
  });
137
+ } else {
138
+ return (
139
+ <ButtonWithAction
140
+ action={noop}
141
+ onKeyDown={handleKeyDown}
142
+ variant={triggerButtonVariant}
143
+ text={triggerText}
144
+ tabIndex={0}
145
+ ariaDescribedby={tooltipID}
146
+ onFocus={() => handleToggleTooltip(true)}
147
+ onBlur={() => handleToggleTooltip(false)}
148
+ onTouchStart={() => handleToggleTooltip(true)}
149
+ dataQa={`tooltip-trigger-${tooltipID}`}
150
+ extraStyles={`
151
+ color: ${themeValues.linkColor};
152
+ &:hover { color: ${themeValues.hoverColor}; text-decoration: none;}
153
+ &:active, &:focus { color: ${themeValues.activeColor};text-decoration: none;}
154
+ button, span, &:hover span { text-decoration: none; }
155
+ `}
156
+ />
157
+ );
109
158
  }
159
+ };
110
160
 
111
- return (
112
- <ButtonWithAction
113
- action={noop}
114
- aria-describedby={tooltipID}
115
- onKeyDown={handleKeyDown}
116
- variant={triggerButtonVariant}
117
- onFocus={() => handleToggleTooltip(true)}
118
- onBlur={() => handleToggleTooltip(false)}
119
- onTouchStart={() => handleToggleTooltip(true)}
120
- data-qa={`tooltip-trigger-${tooltipID}`}
121
- text={triggerText}
122
- tabIndex="0"
123
- extraStyles={`
124
- color: ${themeValues.linkColor};
125
- &:hover { color: ${themeValues.hoverColor}; text-decoration: none;}
126
- &:active, &:focus { color: ${themeValues.activeColor};text-decoration: none;}
127
- button, span, &:hover span { text-decoration: none; }
128
- `}
129
- />
130
- );
161
+ /**
162
+ * @function handleMouseEnter
163
+ * Handles the mouse enter event for the tooltip container.
164
+ * It clears any existing timeout and opens the tooltip.
165
+ */
166
+ const handleMouseEnter = () => {
167
+ if (closeTimeoutRef.current) {
168
+ clearTimeout(closeTimeoutRef.current);
169
+ closeTimeoutRef.current = null;
170
+ }
171
+ handleToggleTooltip(true);
172
+ };
173
+
174
+ /**
175
+ * @function handleMouseLeave
176
+ * Handles the mouse leave event for the tooltip container.
177
+ * It sets a timeout to close the tooltip after 200ms.
178
+ */
179
+ const handleMouseLeave = () => {
180
+ closeTimeoutRef.current = setTimeout(() => {
181
+ handleToggleTooltip(false);
182
+ }, 200);
131
183
  };
132
184
 
185
+ /**
186
+ * Handles the touch start event for the tooltip container.
187
+ * It closes the tooltip if the touch is outside the container.
188
+ */
189
+ useEffect(() => {
190
+ if (!tooltipOpen) return;
191
+
192
+ const handleOutsideTouch = e => {
193
+ if (containerRef.current && !containerRef.current.contains(e.target)) {
194
+ setTooltipOpen(false);
195
+ }
196
+ };
197
+ document.addEventListener("touchstart", handleOutsideTouch);
198
+ return () => document.removeEventListener("touchstart", handleOutsideTouch);
199
+ }, [tooltipOpen]);
200
+
201
+ /**
202
+ * Cleans up the timeout when the component unmounts.
203
+ */
204
+ useEffect(() => {
205
+ return () => {
206
+ if (closeTimeoutRef?.current) clearTimeout(closeTimeoutRef.current);
207
+ };
208
+ }, []);
209
+
133
210
  return (
134
211
  <Box
212
+ ref={containerRef}
135
213
  padding="0"
136
214
  extraStyles={`position: relative; ${containerExtraStyles}`}
137
215
  onMouseEnter={handleMouseEnter}
@@ -162,11 +240,11 @@ const Tooltip = ({
162
240
  minWidth={minWidth}
163
241
  maxWidth={maxWidth}
164
242
  >
165
- {typeof content === "string" ? (
243
+ {typeof content === "string" && content !== "" ? (
166
244
  <Text color={themeValues.textColor}>{content}</Text>
167
- ) : (
245
+ ) : content !== undefined && content !== null ? (
168
246
  content
169
- )}
247
+ ) : null}
170
248
  <Box
171
249
  padding="0"
172
250
  extraStyles={`
@@ -52,6 +52,15 @@ const meta = {
52
52
  defaultValue: { summary: false }
53
53
  }
54
54
  },
55
+ customTriggerRole: {
56
+ description:
57
+ "Role for the custom trigger element for accessibility purposes. Defaults to undefined.",
58
+ control: { type: "text" },
59
+ table: {
60
+ type: { summary: "string" },
61
+ defaultValue: { summary: undefined }
62
+ }
63
+ },
55
64
  children: {
56
65
  description:
57
66
  "Optional trigger element. When provided, it replaces the default ButtonWithAction trigger. The child element will receive aria-describedby, focus, blur, and keydown handlers.",
@@ -135,7 +144,7 @@ const meta = {
135
144
  description: "Maximum width of the tooltip content box.",
136
145
  table: {
137
146
  type: { summary: "string" },
138
- defaultValue: { summary: "300px" }
147
+ defaultValue: { summary: "100%" }
139
148
  }
140
149
  },
141
150
  height: {
@@ -222,14 +231,16 @@ export const RichTooltipContent = {
222
231
  bottom: "auto",
223
232
  left: "-225px"
224
233
  },
225
- content: (
226
- <div style={{ padding: "8px" }}>
227
- <strong>Bold title</strong>
228
- <p>
229
- With <em>an italic text detail</em> below.
230
- </p>
231
- </div>
232
- )
234
+ content: React.createElement("div", { style: { padding: "8px" } }, [
235
+ React.createElement("strong", null, "Bold title"),
236
+ React.createElement(
237
+ "p",
238
+ null,
239
+ "With ",
240
+ React.createElement("em", null, "an italic text detail"),
241
+ " below."
242
+ )
243
+ ])
233
244
  }
234
245
  };
235
246
 
@@ -3,7 +3,7 @@ import Expand from "../../../util/expand";
3
3
 
4
4
  export interface TooltipProps {
5
5
  children?: React.ReactNode;
6
- content: string | React.ReactNode;
6
+ content: React.ReactNode;
7
7
  tooltipID: string;
8
8
  hasCustomTrigger?: boolean;
9
9
  triggerText?: string;
@@ -27,6 +27,7 @@ export interface TooltipProps {
27
27
  containerExtraStyles?: string;
28
28
  contentExtraStyles?: string;
29
29
  backgroundColor?: string;
30
+ customTriggerRole?: string;
30
31
  }
31
32
 
32
33
  export const Tooltip: React.FC<Expand<TooltipProps> &