Skip to content

Commit 078be28

Browse files
committed
feat(design-system): put measurment wrapper in portal to avoid duplicated dom [AR-41960]
1 parent a03f7b7 commit 078be28

File tree

6 files changed

+193
-135
lines changed

6 files changed

+193
-135
lines changed

.changeset/cyan-kids-taste.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@drivenets/design-system': minor
3+
---
4+
5+
- Added `DsTag` component
6+
- Added `DsTagFilter` component
7+
- Deprecated `DsChip` component
8+
- Deprecated `DsChipGroup` component

packages/design-system/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@
6666
"classnames": "^2.5.1"
6767
},
6868
"peerDependencies": {
69-
"react": "^19"
69+
"react": "^19",
70+
"react-dom": "^19"
7071
},
7172
"devDependencies": {
7273
"@arethetypeswrong/cli": "^0.18.2",
@@ -84,6 +85,7 @@
8485
"@tanstack/react-router": "^1.141.2",
8586
"@types/eslint-plugin-jsx-a11y": "^6.10.1",
8687
"@types/react": "^19.2.7",
88+
"@types/react-dom": "^19.2.3",
8789
"@vitest/browser": "^4.0.16",
8890
"@vitest/browser-playwright": "^4.0.15",
8991
"babel-plugin-react-compiler": "^1.0.0",
@@ -97,6 +99,7 @@
9799
"postcss-modules": "^6.0.1",
98100
"publint": "^0.3.16",
99101
"react": "^19.2.3",
102+
"react-dom": "^19.2.3",
100103
"react-hook-form": "^7.55.0",
101104
"rollup-plugin-sass": "^1.15.3",
102105
"sass-embedded": "^1.97.0",

packages/design-system/src/components/ds-form-control/stories/ds-form-control-input-number.stories.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ const sanityCheck = async (canvasElement: HTMLElement) => {
7777

7878
// Test typing
7979
await userEvent.clear(input);
80+
await userEvent.click(input); // ensure focus + caret placement
8081
await userEvent.type(input, '25');
8182
await waitFor(async () => {
8283
await expect(input).toHaveValue('25');

packages/design-system/src/components/ds-tag-filter/ds-tag-filter.stories.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ const sampleFilters: TagFilterItem[] = [
5757
{ id: '9', label: 'Version: 000.0001-4' },
5858
{ id: '10', label: 'Version: 000.0001-5' },
5959
{ id: '11', label: 'Version: 000.0001-6' },
60-
{ id: '12', label: 'Last editor: Maren Levin' },
61-
{ id: '13', label: 'Last editor: Emery Franci' },
60+
{ id: '12', label: 'Last editor: Kevin Levin' },
61+
{ id: '13', label: 'Last editor: Emery Dance' },
6262
];
6363

6464
/**
@@ -125,7 +125,7 @@ export const Default: Story = {
125125
await expect(canvas.getByRole('button', { name: 'Status: Active' })).toBeInTheDocument();
126126
});
127127

128-
await expect(canvas.getAllByText('Filtered by:')[0]).toBeInTheDocument();
128+
await expect(canvas.getByText('Filtered by:')).toBeInTheDocument();
129129
await expect(canvas.getByRole('button', { name: /Clear all filters/ })).toBeInTheDocument();
130130

131131
const firstTag = canvas.getByRole('button', { name: 'Status: Active' });
@@ -297,11 +297,13 @@ export const ReadOnly: Story = {
297297
play: async ({ canvasElement }) => {
298298
const canvas = within(canvasElement);
299299

300-
// Verify custom label is shown (use getAllByText and take first to avoid measurement container duplicate)
301-
await expect(canvas.getAllByText('Applied filters:')[0]).toBeInTheDocument();
300+
// Wait for layout calculation to complete and tags to be rendered
301+
await waitFor(async () => {
302+
await expect(canvas.getByText('Status: Active')).toBeInTheDocument();
303+
});
302304

303-
// Verify filters are visible
304-
await expect(canvas.getAllByText('Status: Active')[0]).toBeInTheDocument();
305+
// Verify custom label is shown
306+
await expect(canvas.getByText('Applied filters:')).toBeInTheDocument();
305307

306308
// Verify delete buttons are NOT visible (read-only)
307309
await expect(canvas.queryByRole('button', { name: 'Delete tag' })).not.toBeInTheDocument();
@@ -402,8 +404,8 @@ export const CustomLocale: Story = {
402404
await expect(canvas.getByRole('button', { name: 'Status: Active' })).toBeInTheDocument();
403405
});
404406

405-
// Verify custom label is rendered (use getAllByText and take first to avoid measurement container duplicate)
406-
await expect(canvas.getAllByText('Active criteria:')[0]).toBeInTheDocument();
407+
// Verify custom label is rendered
408+
await expect(canvas.getByText('Active criteria:')).toBeInTheDocument();
407409

408410
await expect(canvas.getByRole('button', { name: /Reset all/ })).toBeInTheDocument();
409411

packages/design-system/src/components/ds-tag-filter/ds-tag-filter.tsx

Lines changed: 61 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useState, useRef, useMemo } from 'react';
2+
import { createPortal } from 'react-dom';
23
import classNames from 'classnames';
34
import styles from './ds-tag-filter.module.scss';
45
import type { DsTagFilterProps } from './ds-tag-filter.types';
@@ -66,90 +67,98 @@ const DsTagFilter = ({
6667
/>
6768
);
6869

69-
return (
70-
<div
71-
ref={containerRef}
72-
className={classNames(styles.container, expanded && styles.expanded, className)}
73-
style={style}
74-
>
75-
<div className={styles.row1}>
76-
{label && (
70+
// Measurement container rendered via portal to keep it outside component's DOM tree
71+
// This prevents Testing Library from finding duplicate elements
72+
const measurementContainer = (
73+
<div ref={measurementRef} className={styles.measurementContainer} aria-hidden="true">
74+
{label && (
75+
<span data-measure-label="">
7776
<DsTypography variant="body-sm-reg" className={styles.label}>
7877
{label}
7978
</DsTypography>
80-
)}
81-
{row1Tags.map((item) => renderTag(item))}
82-
{onClearAll && (
79+
</span>
80+
)}
81+
{items.map((item) => (
82+
<span key={item.id} data-measure-tag="">
83+
<DsTag
84+
label={item.label}
85+
selected={item.selected}
86+
onClick={onItemSelect ? () => onItemSelect(item) : undefined}
87+
onDelete={onItemDelete ? () => onItemDelete(item) : undefined}
88+
/>
89+
</span>
90+
))}
91+
{onClearAll && (
92+
<span data-measure-clear="">
8393
<DsButton
8494
design="v1.2"
8595
buttonType="tertiary"
8696
variant="ghost"
8797
className={styles.clearButton}
8898
contentClassName={styles.clearContent}
8999
size="small"
90-
onClick={onClearAll}
91100
>
92101
<DsIcon icon="close" />
93102
{clearButton}
94103
</DsButton>
95-
)}
96-
</div>
97-
98-
{hasRow2Content && (
99-
<div className={styles.row2}>
100-
{row2Tags.map((item) => renderTag(item))}
101-
{hasOverflow && (
102-
<DsTag
103-
label={
104-
expanded ? 'Collapse' : `+${String(hiddenCount)} ${hiddenCount === 1 ? 'filter' : 'filters'}`
105-
}
106-
selected={expanded}
107-
className={styles.expandTag}
108-
onClick={() => setExpanded((prev) => !prev)}
109-
/>
110-
)}
111-
</div>
104+
</span>
112105
)}
106+
<span data-measure-expand="">
107+
<DsTag label="+99 filters" className={styles.expandTag} />
108+
</span>
109+
</div>
110+
);
113111

114-
{/* Hidden measurement container - used to measure all elements for layout calculation */}
115-
<div ref={measurementRef} className={styles.measurementContainer} aria-hidden="true">
116-
{label && (
117-
<span data-measure-label="">
112+
return (
113+
<>
114+
<div
115+
ref={containerRef}
116+
className={classNames(styles.container, expanded && styles.expanded, className)}
117+
style={style}
118+
>
119+
<div className={styles.row1}>
120+
{label && (
118121
<DsTypography variant="body-sm-reg" className={styles.label}>
119122
{label}
120123
</DsTypography>
121-
</span>
122-
)}
123-
{items.map((item) => (
124-
<span key={item.id} data-measure-tag="">
125-
<DsTag
126-
label={item.label}
127-
selected={item.selected}
128-
onClick={onItemSelect ? () => onItemSelect(item) : undefined}
129-
onDelete={onItemDelete ? () => onItemDelete(item) : undefined}
130-
/>
131-
</span>
132-
))}
133-
{onClearAll && (
134-
<span data-measure-clear="">
124+
)}
125+
{row1Tags.map((item) => renderTag(item))}
126+
{onClearAll && (
135127
<DsButton
136128
design="v1.2"
137129
buttonType="tertiary"
138130
variant="ghost"
139131
className={styles.clearButton}
140132
contentClassName={styles.clearContent}
141133
size="small"
134+
onClick={onClearAll}
142135
>
143136
<DsIcon icon="close" />
144137
{clearButton}
145138
</DsButton>
146-
</span>
139+
)}
140+
</div>
141+
142+
{hasRow2Content && (
143+
<div className={styles.row2}>
144+
{row2Tags.map((item) => renderTag(item))}
145+
{hasOverflow && (
146+
<DsTag
147+
label={
148+
expanded
149+
? 'Collapse'
150+
: `+${String(hiddenCount)} ${hiddenCount === 1 ? 'filter' : 'filters'}`
151+
}
152+
selected={expanded}
153+
className={styles.expandTag}
154+
onClick={() => setExpanded((prev) => !prev)}
155+
/>
156+
)}
157+
</div>
147158
)}
148-
<span data-measure-expand="">
149-
<DsTag label="+99 filters" className={styles.expandTag} />
150-
</span>
151159
</div>
152-
</div>
160+
{createPortal(measurementContainer, document.body)}
161+
</>
153162
);
154163
};
155164

0 commit comments

Comments
 (0)