Skip to content

Commit dd39a60

Browse files
committed
fix(ui): refresh overflow after font loads
1 parent 43070a4 commit dd39a60

2 files changed

Lines changed: 61 additions & 4 deletions

File tree

packages/emcn/src/components/overflow-text/overflow-text.test.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,20 @@ let host: HTMLDivElement
1010
let root: Root
1111
let resizeObserverCallback: ResizeObserverCallback
1212
let resizeObserverCount: number
13+
let originalFontsDescriptor: PropertyDescriptor | undefined
1314

1415
beforeEach(() => {
1516
resizeObserverCount = 0
17+
originalFontsDescriptor = Object.getOwnPropertyDescriptor(document, 'fonts')
18+
const fontEvents = new EventTarget()
19+
Object.defineProperty(fontEvents, 'ready', {
20+
configurable: true,
21+
value: Promise.resolve(fontEvents),
22+
})
23+
Object.defineProperty(document, 'fonts', {
24+
configurable: true,
25+
value: fontEvents,
26+
})
1627
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
1728
vi.stubGlobal(
1829
'ResizeObserver',
@@ -34,6 +45,11 @@ beforeEach(() => {
3445
afterEach(() => {
3546
act(() => root.unmount())
3647
host.remove()
48+
if (originalFontsDescriptor) {
49+
Object.defineProperty(document, 'fonts', originalFontsDescriptor)
50+
} else {
51+
Reflect.deleteProperty(document, 'fonts')
52+
}
3753
vi.unstubAllGlobals()
3854
})
3955

@@ -154,4 +170,21 @@ describe('OverflowText', () => {
154170
act(() => root.render(<OverflowText label='Short again' />))
155171
expect(label.className).not.toContain('mask-image:linear-gradient')
156172
})
173+
174+
it('remeasures when loaded fonts change text width', () => {
175+
act(() => root.render(<OverflowText label='Workflow name' />))
176+
const label = host.querySelector<HTMLElement>('span')
177+
if (!label) throw new Error('Overflow label did not render')
178+
179+
setWidths(label, 100, 60)
180+
expect(label.className).not.toContain('mask-image:linear-gradient')
181+
182+
Object.defineProperty(label, 'scrollWidth', { configurable: true, value: 180 })
183+
act(() => document.fonts.dispatchEvent(new Event('loadingdone')))
184+
expect(label.className).toContain('mask-image:linear-gradient')
185+
186+
Object.defineProperty(label, 'scrollWidth', { configurable: true, value: 60 })
187+
act(() => document.fonts.dispatchEvent(new Event('loadingerror')))
188+
expect(label.className).not.toContain('mask-image:linear-gradient')
189+
})
157190
})

packages/emcn/src/components/tooltip/tooltip.tsx

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,14 +230,37 @@ export function useFloatingTooltip(
230230
const overflowMeasureByElement = new WeakMap<Element, () => void>()
231231
const observedOverflowElements = new Set<Element>()
232232
let sharedOverflowObserver: ResizeObserver | null = null
233+
let sharedOverflowFontSet: FontFaceSet | null = null
234+
235+
function measureObservedOverflow() {
236+
for (const element of observedOverflowElements) overflowMeasureByElement.get(element)?.()
237+
}
238+
239+
function observeOverflowFontChanges() {
240+
if (typeof document === 'undefined' || !document.fonts || sharedOverflowFontSet) return
241+
const fontSet = document.fonts
242+
sharedOverflowFontSet = fontSet
243+
fontSet.addEventListener('loadingdone', measureObservedOverflow)
244+
fontSet.addEventListener('loadingerror', measureObservedOverflow)
245+
void fontSet.ready.then(() => {
246+
if (sharedOverflowFontSet === fontSet) measureObservedOverflow()
247+
})
248+
}
249+
250+
function unobserveOverflowFontChanges() {
251+
sharedOverflowFontSet?.removeEventListener('loadingdone', measureObservedOverflow)
252+
sharedOverflowFontSet?.removeEventListener('loadingerror', measureObservedOverflow)
253+
sharedOverflowFontSet = null
254+
}
233255

234256
function observeOverflow(element: Element, measure: () => void): boolean {
257+
overflowMeasureByElement.set(element, measure)
258+
observedOverflowElements.add(element)
259+
observeOverflowFontChanges()
235260
if (typeof ResizeObserver === 'undefined') return false
236261
sharedOverflowObserver ??= new ResizeObserver((entries) => {
237262
for (const entry of entries) overflowMeasureByElement.get(entry.target)?.()
238263
})
239-
overflowMeasureByElement.set(element, measure)
240-
observedOverflowElements.add(element)
241264
sharedOverflowObserver.observe(element)
242265
return true
243266
}
@@ -250,13 +273,14 @@ function unobserveOverflow(element: Element | null) {
250273
if (observedOverflowElements.size === 0) {
251274
sharedOverflowObserver?.disconnect()
252275
sharedOverflowObserver = null
276+
unobserveOverflowFontChanges()
253277
}
254278
}
255279

256280
/**
257281
* Tracks whether an element's text is horizontally clipped, re-measuring when
258-
* `measurementKey` changes and via a shared `ResizeObserver` (or window resizes
259-
* when the API is unavailable).
282+
* `measurementKey` or loaded fonts change and via a shared `ResizeObserver` (or
283+
* window resizes when the API is unavailable).
260284
*
261285
* Returns a callback `ref` to attach to the element — the observer follows the
262286
* element across mount, unmount, and reassignment, so it is safe to use on

0 commit comments

Comments
 (0)