Skip to content

Commit d473ab6

Browse files
frano-mclaude
andauthored
fix!: filtertag overflow via resizeobserver (#949) (#951)
* fix: filtertag overflow via resizeobserver (#949) Replaces the ref-during-render measurement (which only computed isOverflowed once mid-render with a null ref, then never reacted to layout changes) with a ResizeObserver inside a dedicated useTooltipTitle hook that returns the chip ref and the tooltip title together. The tooltip now stays accurate across container resizes, font loads, and label changes. Hook lives at src/components/Filter/components/FilterTag/hooks/UseTooltipTitle/ following the existing per-folder hook convention. Companion to #941 (PR #950) — once that lands the react-hooks/refs suppression added there is no longer needed and will be dropped on rebase. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: drop unused superseded prop and SupersededTag styled (#949) Audit while landing #949 revealed superseded was always passed as false from every call site (filters.tsx, FilterTag/utils.ts, stories args, filterTags.stories.tsx) and the SupersededTag styled component was therefore never rendered. Strips the prop from CategoryTag, FilterTag, FilterTags, and the two stories files. Deletes filterTag.styles.ts (only contained SupersededTag). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: cover useTooltipTitle overflow detection (#949) Adds Jest/RTL coverage for the new FilterTag tooltip behaviour: - Suppresses the tooltip when offsetWidth === scrollWidth - Shows the label as the tooltip title when scrollWidth > offsetWidth - Re-evaluates on subsequent ResizeObserver fires (truncated then fits again) ResizeObserver is mocked to capture callbacks so the tests can trigger re-measurement deterministically; .MuiChip-label dimensions are overridden via Object.defineProperty so the overflow check returns a known result in jsdom. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Fran McDade <18710366+frano-m@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7ef5366 commit d473ab6

11 files changed

Lines changed: 148 additions & 45 deletions

File tree

src/common/entities.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ export type ClearAll = typeof CLEAR_ALL;
4040
export interface CategoryTag {
4141
label: string;
4242
onRemove: () => void;
43-
superseded: boolean;
4443
}
4544

4645
/**

src/components/Filter/components/FilterTag/filterTag.styles.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.

src/components/Filter/components/FilterTag/filterTag.tsx

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { CloseRounded } from "@mui/icons-material";
22
import { Chip, Tooltip } from "@mui/material";
3-
import { JSX, useRef } from "react";
4-
import { SupersededTag } from "./filterTag.styles";
3+
import { JSX } from "react";
4+
import { useTooltipTitle } from "./hooks/UseTooltipTitle/hook";
55

66
export interface FilterTagProps {
77
label: string;
88
onRemove: () => void;
9-
superseded: boolean;
109
}
1110

1211
const DEFAULT_SLOT_PROPS = {
@@ -26,34 +25,24 @@ const DEFAULT_SLOT_PROPS = {
2625
},
2726
};
2827

29-
export const FilterTag = ({
30-
label,
31-
onRemove,
32-
superseded,
33-
}: FilterTagProps): JSX.Element => {
34-
const Tag = superseded ? SupersededTag : Chip;
35-
const tagRef = useRef<HTMLDivElement>(null);
36-
/* eslint-disable react-hooks/refs -- ref-during-render measurement; needs proper ResizeObserver-in-effect rewrite. Tracked in #949. */
37-
const tagLabelElement =
38-
tagRef.current?.querySelector<HTMLElement>(".MuiChip-label");
39-
const isOverflowed =
40-
(tagLabelElement?.offsetWidth ?? 0) < (tagLabelElement?.scrollWidth ?? 0);
41-
/* eslint-enable react-hooks/refs -- end suppression for #949. */
28+
export const FilterTag = ({ label, onRemove }: FilterTagProps): JSX.Element => {
29+
const { ref, title } = useTooltipTitle(label);
30+
4231
return (
4332
<Tooltip
4433
arrow
4534
disableInteractive
4635
slotProps={DEFAULT_SLOT_PROPS}
47-
title={isOverflowed ? label : null}
36+
title={title}
4837
>
49-
<Tag
38+
<Chip
5039
clickable={false} // removes unwanted active and hover ui; "pointer" cursor added to "filterTag" variant in theme.
5140
color="primary"
5241
deleteIcon={<CloseRounded color="inherit" />}
5342
label={label}
5443
onClick={onRemove}
5544
onDelete={onRemove}
56-
ref={tagRef}
45+
ref={ref}
5746
variant="filterTag"
5847
/>
5948
</Tooltip>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { useEffect, useRef, useState } from "react";
2+
import { UseTooltipTitle } from "./types";
3+
4+
/**
5+
* Tracks whether the chip's `.MuiChip-label` is truncated by ellipsis,
6+
* returning the label string as a tooltip title only when it is. Uses
7+
* `ResizeObserver` on the label element so the title stays accurate
8+
* across container resizes, font loads, and label changes.
9+
* @param label - Filter tag label.
10+
* @returns The ref to attach to the chip element and the tooltip title
11+
* (the label when truncated, otherwise null).
12+
*/
13+
export function useTooltipTitle(label: string): UseTooltipTitle {
14+
const ref = useRef<HTMLDivElement>(null);
15+
const [isOverflowed, setIsOverflowed] = useState(false);
16+
17+
useEffect(() => {
18+
const el = ref.current?.querySelector<HTMLElement>(".MuiChip-label");
19+
if (!el) return;
20+
const update = (): void => setIsOverflowed(el.offsetWidth < el.scrollWidth);
21+
update();
22+
const observer = new ResizeObserver(update);
23+
observer.observe(el);
24+
return (): void => observer.disconnect();
25+
}, [label]);
26+
27+
return { ref, title: isOverflowed ? label : null };
28+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { RefObject } from "react";
2+
3+
export interface UseTooltipTitle {
4+
ref: RefObject<HTMLDivElement | null>;
5+
title: string | null;
6+
}

src/components/Filter/components/FilterTag/stories/args.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,14 @@ import { FilterTag } from "../filterTag";
66
export const DEFAULT_ARGS: ComponentProps<typeof FilterTag> = {
77
label: "male",
88
onRemove: fn(),
9-
superseded: false,
109
};
1110

1211
export const WITH_ELLIPSIS_ARGS: ComponentProps<typeof FilterTag> = {
1312
label: LOREM_IPSUM.LONG,
1413
onRemove: fn(),
15-
superseded: false,
1614
};
1715

1816
export const WITH_RANGE_ARGS: ComponentProps<typeof FilterTag> = {
1917
label: "10 - 34",
2018
onRemove: fn(),
21-
superseded: false,
2219
};

src/components/Filter/components/FilterTag/utils.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ export function buildRangeTag(
2828
undefined,
2929
VIEW_KIND.RANGE,
3030
),
31-
superseded: false,
3231
},
3332
];
3433
}

src/components/Filter/components/FilterTags/filterTags.stories.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,32 +31,26 @@ export const FilterTagsStory: Story = {
3131
{
3232
label: "Normal",
3333
onRemove: onRemove,
34-
superseded: false,
3534
},
3635
{
3736
label: "abscess",
3837
onRemove: onRemove,
39-
superseded: true,
4038
},
4139
{
4240
label: "acoustic neuroma",
4341
onRemove: onRemove,
44-
superseded: false,
4542
},
4643
{
4744
label: "acute kidney failure",
4845
onRemove: onRemove,
49-
superseded: false,
5046
},
5147
{
5248
label: "acute kidney tubular necrosis",
5349
onRemove: onRemove,
54-
superseded: false,
5550
},
5651
{
5752
label: "alcohol abuse",
5853
onRemove: onRemove,
59-
superseded: false,
6054
},
6155
],
6256
},

src/components/Filter/components/FilterTags/filterTags.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,8 @@ export interface FilterTagsProps {
1010
export const FilterTags = ({ tags }: FilterTagsProps): JSX.Element | null => {
1111
return tags && tags.length ? (
1212
<Tags>
13-
{tags.map(({ label, onRemove, superseded }, t) => (
14-
<Tag
15-
key={`${label}${t}`}
16-
label={label}
17-
onRemove={onRemove}
18-
superseded={superseded}
19-
/>
13+
{tags.map(({ label, onRemove }, t) => (
14+
<Tag key={`${label}${t}`} label={label} onRemove={onRemove} />
2015
))}
2116
</Tags>
2217
) : null;

src/components/Filter/components/Filters/filters.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ function buildFilterTags(
5252
return {
5353
label: label,
5454
onRemove: () => onFilter(categoryKey, categoryValueKey, !selected),
55-
superseded: false,
5655
};
5756
});
5857
}

0 commit comments

Comments
 (0)