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

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.8",
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,18 @@ const Tooltip = ({
33
35
  arrowBottom: "-8px",
34
36
  arrowLeft: "auto"
35
37
  },
38
+ customTriggerRole,
36
39
  backgroundColor = WHITE
37
40
  }) => {
38
41
  const closeTimeoutRef = useRef(null);
42
+ const containerRef = useRef(null);
43
+
39
44
  const [tooltipOpen, setTooltipOpen] = useState(false);
40
45
  const themeContext = useContext(ThemeContext);
41
46
  const themeValues = createThemeValues(
42
47
  themeContext,
43
48
  fallbackValues,
44
- "Tooltip"
49
+ TOOLTIP_THEME_SOURCE
45
50
  );
46
51
 
47
52
  const { top, right, bottom, left } = contentPosition;
@@ -59,38 +64,44 @@ const Tooltip = ({
59
64
  }
60
65
  };
61
66
 
62
- const handleMouseEnter = () => {
63
- if (closeTimeoutRef.current) {
64
- clearTimeout(closeTimeoutRef.current);
65
- closeTimeoutRef.current = null;
67
+ /**
68
+ * Renders the tooltip trigger element.
69
+ *
70
+ * When `hasCustomTrigger` is true, the provided child element is cloned and
71
+ * injected with the event handlers needed to control tooltip visibility:
72
+ * - onFocus/onBlur: open and close for keyboard users
73
+ * - onKeyDown: allows Escape to dismiss the tooltip
74
+ * - onTouchStart: open on tap for touch/mobile users (onFocus is unreliable on touch)
75
+ *
76
+ * Mouse interactions (hover) are handled at the container level via
77
+ * onMouseEnter/onMouseLeave, so they do not need to be injected here.
78
+ *
79
+ * Any existing event handlers on the child are preserved and called first,
80
+ * so the child's own behavior is not overridden.
81
+ *
82
+ * When no custom trigger is provided, a default ButtonWithAction is rendered
83
+ * using `triggerText` and `triggerButtonVariant`.
84
+ */
85
+ const renderTrigger = () => {
86
+ if (hasCustomTrigger && !children) {
87
+ console.warn(
88
+ "Tooltip: children prop is required when hasCustomTrigger is true"
89
+ );
66
90
  }
67
- handleToggleTooltip(true);
68
- };
69
-
70
- const handleMouseLeave = () => {
71
- closeTimeoutRef.current = setTimeout(() => {
72
- handleToggleTooltip(false);
73
- }, 300);
74
- };
75
-
76
- useEffect(() => {
77
- return () => {
78
- if (closeTimeoutRef.current) {
79
- clearTimeout(closeTimeoutRef.current);
80
- }
81
- };
82
- }, []);
83
91
 
84
- const renderTrigger = () => {
85
92
  if (hasCustomTrigger && children) {
86
93
  const child = React.Children.only(children);
94
+ // Capture the child's existing handlers before overwriting
87
95
  const {
88
96
  onFocus: childOnFocus,
89
97
  onBlur: childOnBlur,
90
- onKeyDown: childOnKeyDown
98
+ onKeyDown: childOnKeyDown,
99
+ onTouchStart: childOnTouchStart
91
100
  } = child.props ?? {};
101
+
92
102
  return React.cloneElement(child, {
93
- "aria-describedby": tooltipID,
103
+ tabIndex: child.props?.tabIndex ?? 0,
104
+ style: { cursor: `pointer`, ...child.props?.style },
94
105
  onFocus: e => {
95
106
  childOnFocus?.(e);
96
107
  handleToggleTooltip(true);
@@ -103,35 +114,73 @@ const Tooltip = ({
103
114
  childOnKeyDown?.(e);
104
115
  handleKeyDown(e);
105
116
  },
106
- tabIndex: "0",
107
- style: { ...child.props?.style, cursor: "pointer" }
117
+ onTouchStart: e => {
118
+ childOnTouchStart?.(e);
119
+ handleToggleTooltip(true);
120
+ },
121
+ role: customTriggerRole || child.props?.role,
122
+ "aria-describedby": tooltipID,
123
+ "data-qa": `tooltip-trigger-${tooltipID}`
108
124
  });
125
+ } else {
126
+ return (
127
+ <ButtonWithAction
128
+ action={noop}
129
+ onKeyDown={handleKeyDown}
130
+ variant={triggerButtonVariant}
131
+ text={triggerText}
132
+ tabIndex={0}
133
+ ariaDescribedby={tooltipID}
134
+ onFocus={() => handleToggleTooltip(true)}
135
+ onBlur={() => handleToggleTooltip(false)}
136
+ onTouchStart={() => handleToggleTooltip(true)}
137
+ dataQa={`tooltip-trigger-${tooltipID}`}
138
+ extraStyles={`
139
+ color: ${themeValues.linkColor};
140
+ &:hover { color: ${themeValues.hoverColor}; text-decoration: none;}
141
+ &:active, &:focus { color: ${themeValues.activeColor};text-decoration: none;}
142
+ button, span, &:hover span { text-decoration: none; }
143
+ `}
144
+ />
145
+ );
109
146
  }
110
-
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
- );
131
147
  };
148
+ const handleMouseEnter = () => {
149
+ if (closeTimeoutRef.current) {
150
+ clearTimeout(closeTimeoutRef.current);
151
+ closeTimeoutRef.current = null;
152
+ }
153
+ handleToggleTooltip(true);
154
+ };
155
+ const handleMouseLeave = () => {
156
+ closeTimeoutRef.current = setTimeout(() => {
157
+ handleToggleTooltip(false);
158
+ }, 300);
159
+ };
160
+
161
+ // Touch listener effect
162
+ useEffect(() => {
163
+ if (!tooltipOpen) return;
164
+
165
+ const handleOutsideTouch = e => {
166
+ if (containerRef.current && !containerRef.current.contains(e.target)) {
167
+ setTooltipOpen(false);
168
+ }
169
+ };
170
+ document.addEventListener("touchstart", handleOutsideTouch);
171
+ return () => document.removeEventListener("touchstart", handleOutsideTouch);
172
+ }, [tooltipOpen]);
173
+
174
+ // Unmount cleanup only
175
+ useEffect(() => {
176
+ return () => {
177
+ if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current);
178
+ };
179
+ }, []);
132
180
 
133
181
  return (
134
182
  <Box
183
+ ref={containerRef}
135
184
  padding="0"
136
185
  extraStyles={`position: relative; ${containerExtraStyles}`}
137
186
  onMouseEnter={handleMouseEnter}
@@ -162,11 +211,11 @@ const Tooltip = ({
162
211
  minWidth={minWidth}
163
212
  maxWidth={maxWidth}
164
213
  >
165
- {typeof content === "string" ? (
214
+ {typeof content === "string" && content !== "" ? (
166
215
  <Text color={themeValues.textColor}>{content}</Text>
167
- ) : (
216
+ ) : content !== undefined && content !== null ? (
168
217
  content
169
- )}
218
+ ) : null}
170
219
  <Box
171
220
  padding="0"
172
221
  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> &