@lumx/react 3.9.2-alpha.5 → 3.9.2-alpha.7
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/_internal/ClickAwayProvider.js +2 -2
- package/_internal/ClickAwayProvider.js.map +1 -1
- package/index.d.ts +5 -1
- package/index.js +1624 -1611
- package/index.js.map +1 -1
- package/package.json +4 -4
- package/src/components/image-lightbox/ImageLightbox.test.tsx +8 -1
- package/src/components/image-lightbox/useImageLightbox.tsx +20 -13
- package/src/components/tooltip/Tooltip.stories.tsx +5 -0
- package/src/components/tooltip/Tooltip.test.tsx +160 -61
- package/src/components/tooltip/Tooltip.tsx +16 -4
- package/src/components/tooltip/constants.ts +1 -0
- package/src/components/tooltip/useInjectTooltipRef.tsx +27 -21
- package/src/utils/type.ts +0 -1
package/package.json
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
"url": "https://github.com/lumapps/design-system/issues"
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@lumx/core": "^3.9.2-alpha.
|
|
10
|
-
"@lumx/icons": "^3.9.2-alpha.
|
|
9
|
+
"@lumx/core": "^3.9.2-alpha.7",
|
|
10
|
+
"@lumx/icons": "^3.9.2-alpha.7",
|
|
11
11
|
"@popperjs/core": "^2.5.4",
|
|
12
12
|
"body-scroll-lock": "^3.1.5",
|
|
13
13
|
"classnames": "^2.3.2",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"jest-environment-jsdom": "29.1.2",
|
|
58
58
|
"react": "^17.0.2",
|
|
59
59
|
"react-dom": "^17.0.2",
|
|
60
|
-
"rollup": "2.
|
|
60
|
+
"rollup": "2.79.2",
|
|
61
61
|
"rollup-plugin-analyzer": "^3.3.0",
|
|
62
62
|
"rollup-plugin-babel": "^4.4.0",
|
|
63
63
|
"rollup-plugin-cleaner": "^1.0.0",
|
|
@@ -110,5 +110,5 @@
|
|
|
110
110
|
"build:storybook": "storybook build"
|
|
111
111
|
},
|
|
112
112
|
"sideEffects": false,
|
|
113
|
-
"version": "3.9.2-alpha.
|
|
113
|
+
"version": "3.9.2-alpha.7"
|
|
114
114
|
}
|
|
@@ -145,11 +145,18 @@ describe(`<${ImageLightbox.displayName}>`, () => {
|
|
|
145
145
|
|
|
146
146
|
// Focus moved to the close button
|
|
147
147
|
const imageLightbox = queries.getImageLightbox();
|
|
148
|
-
|
|
148
|
+
const closeButton = queries.queryCloseButton(imageLightbox);
|
|
149
|
+
expect(closeButton).toHaveFocus();
|
|
150
|
+
const tooltip = screen.getByRole('tooltip', { name: 'Close' });
|
|
151
|
+
expect(tooltip).toBeInTheDocument();
|
|
149
152
|
|
|
150
153
|
// Image lightbox opened on the correct image
|
|
151
154
|
expect(queries.queryImage(imageLightbox, 'Image 2')).toBeInTheDocument();
|
|
152
155
|
|
|
156
|
+
// Close tooltip
|
|
157
|
+
await userEvent.keyboard('{escape}');
|
|
158
|
+
expect(tooltip).not.toBeInTheDocument();
|
|
159
|
+
|
|
153
160
|
// Close on escape
|
|
154
161
|
await userEvent.keyboard('{escape}');
|
|
155
162
|
expect(imageLightbox).not.toBeInTheDocument();
|
|
@@ -41,13 +41,12 @@ export function useImageLightbox<P extends Partial<ImageLightboxProps>>(
|
|
|
41
41
|
const propsRef = React.useRef(props);
|
|
42
42
|
|
|
43
43
|
React.useEffect(() => {
|
|
44
|
-
|
|
45
|
-
if (newProps?.images) {
|
|
46
|
-
newProps.images = newProps.images.map((image) => ({ imgRef: React.createRef(), ...image }));
|
|
47
|
-
}
|
|
48
|
-
propsRef.current = newProps;
|
|
44
|
+
propsRef.current = props;
|
|
49
45
|
}, [props]);
|
|
50
46
|
|
|
47
|
+
// Keep reference for each image elements
|
|
48
|
+
const imageRefsRef = React.useRef<Array<React.RefObject<HTMLImageElement>>>([]);
|
|
49
|
+
|
|
51
50
|
const currentImageRef = React.useRef<HTMLImageElement>(null);
|
|
52
51
|
const [imageLightboxProps, setImageLightboxProps] = React.useState(
|
|
53
52
|
() => ({ ...EMPTY_PROPS, ...props }) as ManagedProps & P,
|
|
@@ -61,8 +60,8 @@ export function useImageLightbox<P extends Partial<ImageLightboxProps>>(
|
|
|
61
60
|
if (!currentImage) {
|
|
62
61
|
return;
|
|
63
62
|
}
|
|
64
|
-
const currentIndex =
|
|
65
|
-
(
|
|
63
|
+
const currentIndex = imageRefsRef.current.findIndex(
|
|
64
|
+
(imageRef) => imageRef.current === currentImage,
|
|
66
65
|
) as number;
|
|
67
66
|
|
|
68
67
|
await startViewTransition({
|
|
@@ -83,12 +82,20 @@ export function useImageLightbox<P extends Partial<ImageLightboxProps>>(
|
|
|
83
82
|
// If we find an image inside the trigger, animate it in transition with the opening image
|
|
84
83
|
const triggerImage = triggerImageRefs[activeImageIndex as any]?.current || findImage(triggerElement);
|
|
85
84
|
|
|
86
|
-
// Inject
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
// Inject refs to improve transition and loading style
|
|
86
|
+
const images = propsRef.current?.images?.map((image, idx) => {
|
|
87
|
+
// Get or create image reference
|
|
88
|
+
let imgRef = imageRefsRef.current[idx];
|
|
89
|
+
if (!imgRef) {
|
|
90
|
+
imgRef = React.createRef();
|
|
91
|
+
imageRefsRef.current[idx] = imgRef;
|
|
90
92
|
}
|
|
91
|
-
|
|
93
|
+
|
|
94
|
+
// Try to use the trigger image as the loading placeholder
|
|
95
|
+
const loadingPlaceholderImageRef =
|
|
96
|
+
triggerImage && idx === activeImageIndex ? { current: triggerImage } : undefined;
|
|
97
|
+
|
|
98
|
+
return { loadingPlaceholderImageRef, ...image, imgRef };
|
|
92
99
|
});
|
|
93
100
|
|
|
94
101
|
await startViewTransition({
|
|
@@ -104,7 +111,7 @@ export function useImageLightbox<P extends Partial<ImageLightboxProps>>(
|
|
|
104
111
|
close();
|
|
105
112
|
prevProps?.onClose?.();
|
|
106
113
|
},
|
|
107
|
-
images
|
|
114
|
+
images,
|
|
108
115
|
activeImageIndex: activeImageIndex || 0,
|
|
109
116
|
}));
|
|
110
117
|
},
|
|
@@ -2,8 +2,10 @@ import { Button, Dialog, Dropdown, Placement, Tooltip } from '@lumx/react';
|
|
|
2
2
|
import React, { useState } from 'react';
|
|
3
3
|
import { getSelectArgType } from '@lumx/react/stories/controls/selectArgType';
|
|
4
4
|
import { withChromaticForceScreenSize } from '@lumx/react/stories/decorators/withChromaticForceScreenSize';
|
|
5
|
+
import { ARIA_LINK_MODES } from '@lumx/react/components/tooltip/constants';
|
|
5
6
|
|
|
6
7
|
const placements = [Placement.TOP, Placement.BOTTOM, Placement.RIGHT, Placement.LEFT];
|
|
8
|
+
const CLOSE_MODES = ['hide', 'unmount'];
|
|
7
9
|
|
|
8
10
|
export default {
|
|
9
11
|
title: 'LumX components/tooltip/Tooltip',
|
|
@@ -11,6 +13,9 @@ export default {
|
|
|
11
13
|
args: Tooltip.defaultProps,
|
|
12
14
|
argTypes: {
|
|
13
15
|
placement: getSelectArgType(placements),
|
|
16
|
+
children: { control: false },
|
|
17
|
+
closeMode: { control: { type: 'inline-radio' }, options: CLOSE_MODES },
|
|
18
|
+
ariaLinkMode: { control: { type: 'inline-radio' }, options: ARIA_LINK_MODES },
|
|
14
19
|
},
|
|
15
20
|
decorators: [
|
|
16
21
|
// Force minimum chromatic screen size to make sure the dialog appears in view.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
-
import { Button
|
|
3
|
+
import { Button } from '@lumx/react';
|
|
4
4
|
import { screen, render } from '@testing-library/react';
|
|
5
5
|
import { queryAllByTagName, queryByClassName } from '@lumx/react/testing/utils/queries';
|
|
6
6
|
import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
|
|
@@ -51,7 +51,6 @@ describe(`<${Tooltip.displayName}>`, () => {
|
|
|
51
51
|
// Default placement
|
|
52
52
|
expect(tooltip).toHaveAttribute('data-popper-placement', 'bottom');
|
|
53
53
|
expect(anchorWrapper).toBeInTheDocument();
|
|
54
|
-
expect(anchorWrapper).toHaveAttribute('aria-describedby', tooltip?.id);
|
|
55
54
|
});
|
|
56
55
|
|
|
57
56
|
it('should render with custom placement', async () => {
|
|
@@ -65,25 +64,6 @@ describe(`<${Tooltip.displayName}>`, () => {
|
|
|
65
64
|
expect(tooltip).toHaveAttribute('data-popper-placement', 'top');
|
|
66
65
|
});
|
|
67
66
|
|
|
68
|
-
it('should wrap unknown children and not add aria-describedby when closed', async () => {
|
|
69
|
-
const { anchorWrapper } = await setup({
|
|
70
|
-
label: 'Tooltip label',
|
|
71
|
-
children: 'Anchor',
|
|
72
|
-
forceOpen: false,
|
|
73
|
-
});
|
|
74
|
-
expect(anchorWrapper).not.toHaveAttribute('aria-describedby');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('should not wrap Button and not add aria-describedby when closed', async () => {
|
|
78
|
-
await setup({
|
|
79
|
-
label: 'Tooltip label',
|
|
80
|
-
children: <Button>Anchor</Button>,
|
|
81
|
-
forceOpen: false,
|
|
82
|
-
});
|
|
83
|
-
const button = screen.queryByRole('button', { name: 'Anchor' });
|
|
84
|
-
expect(button).not.toHaveAttribute('aria-describedby');
|
|
85
|
-
});
|
|
86
|
-
|
|
87
67
|
it('should not wrap Button', async () => {
|
|
88
68
|
const { tooltip, anchorWrapper } = await setup({
|
|
89
69
|
label: 'Tooltip label',
|
|
@@ -96,35 +76,6 @@ describe(`<${Tooltip.displayName}>`, () => {
|
|
|
96
76
|
expect(button).toHaveAttribute('aria-describedby', tooltip?.id);
|
|
97
77
|
});
|
|
98
78
|
|
|
99
|
-
it('should not add aria-describedby if button label is the same as tooltip label', async () => {
|
|
100
|
-
const label = 'Tooltip label';
|
|
101
|
-
render(<IconButton label={label} tooltipProps={{ forceOpen: true }} />);
|
|
102
|
-
const tooltip = screen.queryByRole('tooltip', { name: label });
|
|
103
|
-
expect(tooltip).toBeInTheDocument();
|
|
104
|
-
const button = screen.queryByRole('button', { name: label });
|
|
105
|
-
expect(button).not.toHaveAttribute('aria-describedby');
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('should keep anchor aria-describedby if button label is the same as tooltip label', async () => {
|
|
109
|
-
const label = 'Tooltip label';
|
|
110
|
-
render(<IconButton label={label} aria-describedby=":header-1:" tooltipProps={{ forceOpen: true }} />);
|
|
111
|
-
const tooltip = screen.queryByRole('tooltip', { name: label });
|
|
112
|
-
expect(tooltip).toBeInTheDocument();
|
|
113
|
-
const button = screen.queryByRole('button', { name: label });
|
|
114
|
-
expect(button).toHaveAttribute('aria-describedby', ':header-1:');
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('should concat aria-describedby if already exists', async () => {
|
|
118
|
-
const { tooltip } = await setup({
|
|
119
|
-
label: 'Tooltip label',
|
|
120
|
-
children: <Button aria-describedby=":header-1:">Anchor</Button>,
|
|
121
|
-
forceOpen: true,
|
|
122
|
-
});
|
|
123
|
-
expect(tooltip).toBeInTheDocument();
|
|
124
|
-
const button = screen.queryByRole('button', { name: 'Anchor' });
|
|
125
|
-
expect(button).toHaveAttribute('aria-describedby', `:header-1: ${tooltip?.id}`);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
79
|
it('should wrap disabled Button', async () => {
|
|
129
80
|
const { tooltip, anchorWrapper } = await setup({
|
|
130
81
|
label: 'Tooltip label',
|
|
@@ -172,17 +123,166 @@ describe(`<${Tooltip.displayName}>`, () => {
|
|
|
172
123
|
expect(ref.current === element).toBe(true);
|
|
173
124
|
});
|
|
174
125
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
126
|
+
describe('closeMode="hide"', () => {
|
|
127
|
+
it('should not render with empty label', async () => {
|
|
128
|
+
const { tooltip, anchorWrapper } = await setup({
|
|
129
|
+
label: undefined,
|
|
130
|
+
forceOpen: true,
|
|
131
|
+
closeMode: 'hide',
|
|
132
|
+
});
|
|
133
|
+
expect(tooltip).not.toBeInTheDocument();
|
|
134
|
+
expect(anchorWrapper).not.toBeInTheDocument();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should render hidden', async () => {
|
|
138
|
+
const { tooltip } = await setup({
|
|
139
|
+
label: 'Tooltip label',
|
|
140
|
+
children: <Button>Anchor</Button>,
|
|
141
|
+
closeMode: 'hide',
|
|
142
|
+
forceOpen: false,
|
|
143
|
+
});
|
|
144
|
+
expect(tooltip).toBeInTheDocument();
|
|
145
|
+
expect(tooltip).toHaveClass('lumx-tooltip--is-hidden');
|
|
146
|
+
|
|
147
|
+
const anchor = screen.getByRole('button', { name: 'Anchor' });
|
|
148
|
+
await userEvent.hover(anchor);
|
|
149
|
+
expect(tooltip).not.toHaveClass('lumx-tooltip--is-hidden');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('ariaLinkMode="aria-describedby"', () => {
|
|
154
|
+
it('should add aria-describedby on anchor on open', async () => {
|
|
155
|
+
await setup({
|
|
156
|
+
label: 'Tooltip label',
|
|
157
|
+
forceOpen: false,
|
|
158
|
+
children: <Button aria-describedby=":description1:">Anchor</Button>,
|
|
159
|
+
});
|
|
160
|
+
const anchor = screen.getByRole('button', { name: 'Anchor' });
|
|
161
|
+
expect(anchor).toHaveAttribute('aria-describedby', ':description1:');
|
|
162
|
+
|
|
163
|
+
await userEvent.hover(anchor);
|
|
164
|
+
const tooltip = screen.queryByRole('tooltip');
|
|
165
|
+
expect(anchor).toHaveAttribute('aria-describedby', `:description1: ${tooltip?.id}`);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should always add aria-describedby on anchor with closeMode="hide"', async () => {
|
|
169
|
+
const { tooltip } = await setup({
|
|
170
|
+
label: 'Tooltip label',
|
|
171
|
+
forceOpen: false,
|
|
172
|
+
children: <Button aria-describedby=":description1:">Anchor</Button>,
|
|
173
|
+
closeMode: 'hide',
|
|
174
|
+
});
|
|
175
|
+
const anchor = screen.getByRole('button', { name: 'Anchor' });
|
|
176
|
+
expect(anchor).toHaveAttribute('aria-describedby', `:description1: ${tooltip?.id}`);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should skip aria-describedby if anchor has label', async () => {
|
|
180
|
+
const { tooltip } = await setup({
|
|
181
|
+
label: 'Tooltip label',
|
|
182
|
+
forceOpen: true,
|
|
183
|
+
children: (
|
|
184
|
+
<Button aria-describedby=":description1:" aria-label="Tooltip label">
|
|
185
|
+
Anchor
|
|
186
|
+
</Button>
|
|
187
|
+
),
|
|
188
|
+
});
|
|
189
|
+
expect(tooltip).toBeInTheDocument();
|
|
190
|
+
expect(screen.getByRole('button')).toHaveAttribute('aria-describedby', `:description1:`);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should add aria-describedby on anchor wrapper on open', async () => {
|
|
194
|
+
const { anchorWrapper } = await setup({
|
|
195
|
+
label: 'Tooltip label',
|
|
196
|
+
forceOpen: false,
|
|
197
|
+
children: 'Anchor',
|
|
198
|
+
});
|
|
199
|
+
expect(anchorWrapper).not.toHaveAttribute('aria-describedby');
|
|
200
|
+
|
|
201
|
+
await userEvent.hover(anchorWrapper as any);
|
|
202
|
+
const tooltip = screen.queryByRole('tooltip');
|
|
203
|
+
expect(anchorWrapper).toHaveAttribute('aria-describedby', tooltip?.id);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should always add aria-describedby on anchor wrapper with closeMode="hide"', async () => {
|
|
207
|
+
const { tooltip, anchorWrapper } = await setup({
|
|
208
|
+
label: 'Tooltip label',
|
|
209
|
+
forceOpen: false,
|
|
210
|
+
children: 'Anchor',
|
|
211
|
+
closeMode: 'hide',
|
|
212
|
+
});
|
|
213
|
+
expect(anchorWrapper).toHaveAttribute('aria-describedby', `${tooltip?.id}`);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('ariaLinkMode="aria-labelledby"', () => {
|
|
218
|
+
it('should add aria-labelledby on anchor on open', async () => {
|
|
219
|
+
await setup({
|
|
220
|
+
label: 'Tooltip label',
|
|
221
|
+
forceOpen: false,
|
|
222
|
+
children: <Button aria-labelledby=":label1:">Anchor</Button>,
|
|
223
|
+
ariaLinkMode: 'aria-labelledby',
|
|
224
|
+
});
|
|
225
|
+
const anchor = screen.getByRole('button', { name: 'Anchor' });
|
|
226
|
+
expect(anchor).toHaveAttribute('aria-labelledby', ':label1:');
|
|
227
|
+
|
|
228
|
+
await userEvent.hover(anchor);
|
|
229
|
+
const tooltip = screen.queryByRole('tooltip');
|
|
230
|
+
expect(anchor).toHaveAttribute('aria-labelledby', `:label1: ${tooltip?.id}`);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should always add aria-labelledby on anchor with closeMode="hide"', async () => {
|
|
234
|
+
const label = 'Tooltip label';
|
|
235
|
+
const { tooltip } = await setup({
|
|
236
|
+
label,
|
|
237
|
+
forceOpen: false,
|
|
238
|
+
children: <Button aria-labelledby=":label1:">Anchor</Button>,
|
|
239
|
+
ariaLinkMode: 'aria-labelledby',
|
|
240
|
+
closeMode: 'hide',
|
|
241
|
+
});
|
|
242
|
+
const anchor = screen.queryByRole('button', { name: label });
|
|
243
|
+
expect(anchor).toBeInTheDocument();
|
|
244
|
+
expect(anchor).toHaveAttribute('aria-labelledby', `:label1: ${tooltip?.id}`);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should skip aria-labelledby if anchor has label', async () => {
|
|
248
|
+
const { tooltip } = await setup({
|
|
249
|
+
label: 'Tooltip label',
|
|
250
|
+
forceOpen: true,
|
|
251
|
+
children: (
|
|
252
|
+
<Button aria-labelledby=":label1:" aria-label="Tooltip label">
|
|
253
|
+
Anchor
|
|
254
|
+
</Button>
|
|
255
|
+
),
|
|
256
|
+
ariaLinkMode: 'aria-labelledby',
|
|
257
|
+
});
|
|
258
|
+
expect(tooltip).toBeInTheDocument();
|
|
259
|
+
expect(screen.getByRole('button')).toHaveAttribute('aria-labelledby', `:label1:`);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('should add aria-labelledby on anchor wrapper on open', async () => {
|
|
263
|
+
const { anchorWrapper } = await setup({
|
|
264
|
+
label: 'Tooltip label',
|
|
265
|
+
forceOpen: false,
|
|
266
|
+
children: 'Anchor',
|
|
267
|
+
ariaLinkMode: 'aria-labelledby',
|
|
268
|
+
});
|
|
269
|
+
expect(anchorWrapper).not.toHaveAttribute('aria-labelledby');
|
|
270
|
+
|
|
271
|
+
await userEvent.hover(anchorWrapper as any);
|
|
272
|
+
const tooltip = screen.queryByRole('tooltip');
|
|
273
|
+
expect(anchorWrapper).toHaveAttribute('aria-labelledby', tooltip?.id);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should always add aria-labelledby on anchor wrapper with closeMode="hide"', async () => {
|
|
277
|
+
const { tooltip, anchorWrapper } = await setup({
|
|
278
|
+
label: 'Tooltip label',
|
|
279
|
+
forceOpen: false,
|
|
280
|
+
children: 'Anchor',
|
|
281
|
+
ariaLinkMode: 'aria-labelledby',
|
|
282
|
+
closeMode: 'hide',
|
|
283
|
+
});
|
|
284
|
+
expect(anchorWrapper).toHaveAttribute('aria-labelledby', `${tooltip?.id}`);
|
|
181
285
|
});
|
|
182
|
-
expect(tooltip).toBeInTheDocument();
|
|
183
|
-
expect(tooltip).toHaveClass('lumx-tooltip--is-hidden');
|
|
184
|
-
const button = screen.queryByRole('button', { name: 'Anchor' });
|
|
185
|
-
expect(button).toHaveAttribute('aria-describedby', tooltip?.id);
|
|
186
286
|
});
|
|
187
287
|
});
|
|
188
288
|
|
|
@@ -203,7 +303,6 @@ describe(`<${Tooltip.displayName}>`, () => {
|
|
|
203
303
|
// Tooltip opened
|
|
204
304
|
tooltip = await screen.findByRole('tooltip', { name: 'Tooltip label' });
|
|
205
305
|
expect(tooltip).toBeInTheDocument();
|
|
206
|
-
expect(button).toHaveAttribute('aria-describedby', tooltip?.id);
|
|
207
306
|
|
|
208
307
|
// Un-hover anchor button
|
|
209
308
|
await userEvent.unhover(button);
|
|
@@ -11,10 +11,11 @@ import { useMergeRefs } from '@lumx/react/utils/mergeRefs';
|
|
|
11
11
|
import { Placement } from '@lumx/react/components/popover';
|
|
12
12
|
import { TooltipContextProvider } from '@lumx/react/components/tooltip/context';
|
|
13
13
|
import { useId } from '@lumx/react/hooks/useId';
|
|
14
|
+
import { usePopper } from '@lumx/react/hooks/usePopper';
|
|
14
15
|
|
|
16
|
+
import { ARIA_LINK_MODES } from '@lumx/react/components/tooltip/constants';
|
|
15
17
|
import { useInjectTooltipRef } from './useInjectTooltipRef';
|
|
16
18
|
import { useTooltipOpen } from './useTooltipOpen';
|
|
17
|
-
import { usePopper } from '@lumx/react/hooks/usePopper';
|
|
18
19
|
|
|
19
20
|
/** Position of the tooltip relative to the anchor element. */
|
|
20
21
|
export type TooltipPlacement = Extract<Placement, 'top' | 'right' | 'bottom' | 'left'>;
|
|
@@ -33,6 +34,8 @@ export interface TooltipProps extends GenericProps, HasCloseMode {
|
|
|
33
34
|
label?: string | null | false;
|
|
34
35
|
/** Placement of the tooltip relative to the anchor. */
|
|
35
36
|
placement?: TooltipPlacement;
|
|
37
|
+
/** Choose how the tooltip text should link to the anchor */
|
|
38
|
+
ariaLinkMode?: (typeof ARIA_LINK_MODES)[number];
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
/**
|
|
@@ -51,6 +54,7 @@ const CLASSNAME = getRootClassName(COMPONENT_NAME);
|
|
|
51
54
|
const DEFAULT_PROPS: Partial<TooltipProps> = {
|
|
52
55
|
placement: Placement.BOTTOM,
|
|
53
56
|
closeMode: 'unmount',
|
|
57
|
+
ariaLinkMode: 'aria-describedby',
|
|
54
58
|
};
|
|
55
59
|
|
|
56
60
|
/**
|
|
@@ -66,7 +70,8 @@ const ARROW_SIZE = 8;
|
|
|
66
70
|
* @return React element.
|
|
67
71
|
*/
|
|
68
72
|
export const Tooltip: Comp<TooltipProps, HTMLDivElement> = forwardRef((props, ref) => {
|
|
69
|
-
const { label, children, className, delay, placement, forceOpen, closeMode, ...forwardedProps } =
|
|
73
|
+
const { label, children, className, delay, placement, forceOpen, closeMode, ariaLinkMode, ...forwardedProps } =
|
|
74
|
+
props;
|
|
70
75
|
// Disable in SSR.
|
|
71
76
|
if (!DOCUMENT) {
|
|
72
77
|
return <>{children}</>;
|
|
@@ -89,8 +94,15 @@ export const Tooltip: Comp<TooltipProps, HTMLDivElement> = forwardRef((props, re
|
|
|
89
94
|
const position = attributes?.popper?.['data-popper-placement'] ?? placement;
|
|
90
95
|
const { isOpen: isActivated, onPopperMount } = useTooltipOpen(delay, anchorElement);
|
|
91
96
|
const isOpen = (isActivated || forceOpen) && !!label;
|
|
92
|
-
const isMounted = isOpen || closeMode === 'hide';
|
|
93
|
-
const wrappedChildren = useInjectTooltipRef(
|
|
97
|
+
const isMounted = !!label && (isOpen || closeMode === 'hide');
|
|
98
|
+
const wrappedChildren = useInjectTooltipRef({
|
|
99
|
+
children,
|
|
100
|
+
setAnchorElement,
|
|
101
|
+
isMounted,
|
|
102
|
+
id,
|
|
103
|
+
label,
|
|
104
|
+
ariaLinkMode: ariaLinkMode as any,
|
|
105
|
+
});
|
|
94
106
|
|
|
95
107
|
const labelLines = label ? label.split('\n') : [];
|
|
96
108
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ARIA_LINK_MODES = ['aria-describedby', 'aria-labelledby'] as const;
|
|
@@ -2,27 +2,30 @@ import React, { cloneElement, ReactNode, useMemo } from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { mergeRefs } from '@lumx/react/utils/mergeRefs';
|
|
4
4
|
|
|
5
|
+
interface Options {
|
|
6
|
+
/** Original tooltip anchor */
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
/** Set tooltip anchor element */
|
|
9
|
+
setAnchorElement: (e: HTMLDivElement) => void;
|
|
10
|
+
/** Whether the tooltip is open or not */
|
|
11
|
+
isMounted: boolean | undefined;
|
|
12
|
+
/** Tooltip id */
|
|
13
|
+
id: string;
|
|
14
|
+
/** Tooltip label*/
|
|
15
|
+
label?: string | null | false;
|
|
16
|
+
/** Choose how the tooltip text should link to the anchor */
|
|
17
|
+
ariaLinkMode: 'aria-describedby' | 'aria-labelledby';
|
|
18
|
+
}
|
|
19
|
+
|
|
5
20
|
/**
|
|
6
21
|
* Add ref and ARIA attribute(s) in tooltip children or wrapped children.
|
|
7
22
|
* Button, IconButton, Icon and React HTML elements don't need to be wrapped but any other kind of children (array, fragment, custom components)
|
|
8
23
|
* will be wrapped in a <span>.
|
|
9
|
-
*
|
|
10
|
-
* @param children Original tooltip anchor.
|
|
11
|
-
* @param setAnchorElement Set tooltip anchor element.
|
|
12
|
-
* @param isOpen Whether the tooltip is open or not.
|
|
13
|
-
* @param id Tooltip id.
|
|
14
|
-
* @param label Tooltip label.
|
|
15
|
-
* @return tooltip anchor.
|
|
16
24
|
*/
|
|
17
|
-
export const useInjectTooltipRef = (
|
|
18
|
-
children
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
id: string,
|
|
22
|
-
label?: string | null | false,
|
|
23
|
-
): ReactNode => {
|
|
24
|
-
// Only add description when open
|
|
25
|
-
const describedBy = isOpen ? id : undefined;
|
|
25
|
+
export const useInjectTooltipRef = (options: Options): ReactNode => {
|
|
26
|
+
const { children, setAnchorElement, isMounted, id, label, ariaLinkMode } = options;
|
|
27
|
+
// Only add link when mounted
|
|
28
|
+
const linkId = isMounted ? id : undefined;
|
|
26
29
|
|
|
27
30
|
return useMemo(() => {
|
|
28
31
|
if (!label) return children;
|
|
@@ -32,18 +35,21 @@ export const useInjectTooltipRef = (
|
|
|
32
35
|
const ref = mergeRefs((children as any).ref, setAnchorElement);
|
|
33
36
|
const props = { ...children.props, ref };
|
|
34
37
|
|
|
35
|
-
//
|
|
36
|
-
if (label !== props['aria-label']
|
|
37
|
-
props[
|
|
38
|
+
// Do not add label/description if the tooltip label is already in aria-label
|
|
39
|
+
if (linkId && label !== props['aria-label']) {
|
|
40
|
+
if (props[ariaLinkMode]) props[ariaLinkMode] += ' ';
|
|
41
|
+
else props[ariaLinkMode] = '';
|
|
42
|
+
props[ariaLinkMode] += linkId;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
return cloneElement(children, props);
|
|
41
46
|
}
|
|
42
47
|
|
|
48
|
+
const aria = linkId ? { [ariaLinkMode]: linkId } : undefined;
|
|
43
49
|
return (
|
|
44
|
-
<div className="lumx-tooltip-anchor-wrapper" ref={setAnchorElement} aria
|
|
50
|
+
<div className="lumx-tooltip-anchor-wrapper" ref={setAnchorElement} {...aria}>
|
|
45
51
|
{children}
|
|
46
52
|
</div>
|
|
47
53
|
);
|
|
48
|
-
}, [children, setAnchorElement,
|
|
54
|
+
}, [label, children, setAnchorElement, linkId, ariaLinkMode]);
|
|
49
55
|
};
|