|
1 | 1 | import * as React from 'react'; |
2 | 2 | import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; |
3 | | -import { useIsomorphicLayoutEffect } from '@fluentui/react-utilities'; |
| 3 | +import { isHTMLElement } from '@fluentui/react-utilities'; |
4 | 4 |
|
5 | 5 | export function useMessageBarReflow(enabled: boolean = false) { |
6 | 6 | const { targetDocument } = useFluent(); |
7 | | - const prevInlineSizeRef = React.useRef(-1); |
8 | | - const messageBarRef = React.useRef<HTMLElement | null>(null); |
9 | | - |
10 | | - const [reflowing, setReflowing] = React.useState(false); |
| 7 | + const forceUpdate = React.useReducer(() => ({}), {})[1]; |
| 8 | + const reflowingRef = React.useRef(false); |
| 9 | + // TODO: exclude types from this lint rule: https://github.com/microsoft/fluentui/issues/31286 |
11 | 10 |
|
12 | | - // This layout effect 'sanity checks' what observers have done |
13 | | - // since DOM has not been flushed when observers run |
14 | | - useIsomorphicLayoutEffect(() => { |
15 | | - if (!messageBarRef.current) { |
16 | | - return; |
17 | | - } |
| 11 | + const resizeObserverRef = React.useRef<ResizeObserver | null>(null); |
| 12 | + const prevInlineSizeRef = React.useRef(-1); |
18 | 13 |
|
19 | | - setReflowing(prevReflowing => { |
20 | | - if (!prevReflowing && messageBarRef.current && isReflowing(messageBarRef.current)) { |
21 | | - return true; |
| 14 | + const handleResize: ResizeObserverCallback = React.useCallback( |
| 15 | + entries => { |
| 16 | + // Resize observer is only owned by this component - one resize observer entry expected |
| 17 | + // No need to support multiple fragments - one border box entry expected |
| 18 | + if (process.env.NODE_ENV !== 'production' && entries.length > 1) { |
| 19 | + // eslint-disable-next-line no-console |
| 20 | + console.error( |
| 21 | + [ |
| 22 | + 'useMessageBarReflow: Resize observer should only have one entry. ', |
| 23 | + 'If multiple entries are observed, the first entry will be used.', |
| 24 | + 'This is a bug, please report it to the Fluent UI team.', |
| 25 | + ].join(' '), |
| 26 | + ); |
22 | 27 | } |
23 | 28 |
|
24 | | - return prevReflowing; |
25 | | - }); |
26 | | - }, [reflowing]); |
| 29 | + const entry = entries[0]; |
| 30 | + // `borderBoxSize` is not supported before Chrome 84, Firefox 92, nor Safari 15.4 |
| 31 | + const inlineSize = entry?.borderBoxSize?.[0]?.inlineSize ?? entry?.target.getBoundingClientRect().width; |
27 | 32 |
|
28 | | - const handleResize: ResizeObserverCallback = React.useCallback(() => { |
29 | | - if (!messageBarRef.current) { |
30 | | - return; |
31 | | - } |
32 | | - |
33 | | - const inlineSize = messageBarRef.current.getBoundingClientRect().width; |
34 | | - const scrollWidth = messageBarRef.current.scrollWidth; |
| 33 | + if (inlineSize === undefined || !entry) { |
| 34 | + return; |
| 35 | + } |
35 | 36 |
|
36 | | - const expanding = prevInlineSizeRef.current < inlineSize; |
37 | | - const overflowing = inlineSize < scrollWidth; |
| 37 | + const { target } = entry; |
38 | 38 |
|
39 | | - setReflowing(!expanding || overflowing); |
40 | | - }, []); |
| 39 | + if (!isHTMLElement(target)) { |
| 40 | + return; |
| 41 | + } |
41 | 42 |
|
42 | | - const handleIntersection: IntersectionObserverCallback = React.useCallback(entries => { |
43 | | - if (entries[0].intersectionRatio < 1) { |
44 | | - setReflowing(true); |
45 | | - } |
46 | | - }, []); |
| 43 | + let nextReflowing: boolean | undefined; |
| 44 | + |
| 45 | + // No easy way to really determine when the single line layout will fit |
| 46 | + // Just keep try to set single line layout as long as the size is growing |
| 47 | + // Will cause flickering when size is being adjusted gradually (i.e. drag) - but this should not be a common case |
| 48 | + if (reflowingRef.current) { |
| 49 | + if (prevInlineSizeRef.current < inlineSize) { |
| 50 | + nextReflowing = false; |
| 51 | + } |
| 52 | + } else { |
| 53 | + const scrollWidth = target.scrollWidth; |
| 54 | + if (inlineSize < scrollWidth) { |
| 55 | + nextReflowing = true; |
| 56 | + } |
| 57 | + } |
47 | 58 |
|
48 | | - const ref = React.useMemo(() => { |
49 | | - let resizeObserver: ResizeObserver | null = null; |
50 | | - let intersectionObserer: IntersectionObserver | null = null; |
| 59 | + prevInlineSizeRef.current = inlineSize; |
| 60 | + if (typeof nextReflowing !== 'undefined' && reflowingRef.current !== nextReflowing) { |
| 61 | + reflowingRef.current = nextReflowing; |
| 62 | + forceUpdate(); |
| 63 | + } |
| 64 | + }, |
| 65 | + [forceUpdate], |
| 66 | + ); |
51 | 67 |
|
52 | | - return (el: HTMLElement | null) => { |
| 68 | + const ref = React.useCallback( |
| 69 | + (el: HTMLElement | null) => { |
53 | 70 | if (!enabled || !el || !targetDocument?.defaultView) { |
54 | | - resizeObserver?.disconnect(); |
55 | | - intersectionObserer?.disconnect(); |
56 | 71 | return; |
57 | 72 | } |
58 | 73 |
|
59 | | - messageBarRef.current = el; |
| 74 | + resizeObserverRef.current?.disconnect(); |
60 | 75 |
|
61 | 76 | const win = targetDocument.defaultView; |
62 | | - resizeObserver = new win.ResizeObserver(handleResize); |
63 | | - intersectionObserer = new win.IntersectionObserver(handleIntersection, { threshold: 1 }); |
64 | | - |
65 | | - intersectionObserer.observe(el); |
| 77 | + const resizeObserver = new win.ResizeObserver(handleResize); |
| 78 | + resizeObserverRef.current = resizeObserver; |
66 | 79 | resizeObserver.observe(el, { box: 'border-box' }); |
| 80 | + }, |
| 81 | + [targetDocument, handleResize, enabled], |
| 82 | + ); |
| 83 | + |
| 84 | + React.useEffect(() => { |
| 85 | + return () => { |
| 86 | + resizeObserverRef.current?.disconnect(); |
67 | 87 | }; |
68 | | - }, [handleResize, handleIntersection, enabled, targetDocument]); |
| 88 | + }, []); |
69 | 89 |
|
70 | | - return { ref, reflowing }; |
| 90 | + return { ref, reflowing: reflowingRef.current }; |
71 | 91 | } |
72 | | - |
73 | | -const isReflowing = (el: HTMLElement) => { |
74 | | - return el.scrollWidth > el.offsetWidth || !isFullyInViewport(el); |
75 | | -}; |
76 | | - |
77 | | -const isFullyInViewport = (el: HTMLElement) => { |
78 | | - const rect = el.getBoundingClientRect(); |
79 | | - const doc = el.ownerDocument; |
80 | | - const win = doc.defaultView; |
81 | | - |
82 | | - if (!win) { |
83 | | - return true; |
84 | | - } |
85 | | - |
86 | | - return ( |
87 | | - rect.top >= 0 && |
88 | | - rect.left >= 0 && |
89 | | - rect.bottom <= (win.innerHeight || doc.documentElement.clientHeight) && |
90 | | - rect.right <= (win.innerWidth || doc.documentElement.clientWidth) |
91 | | - ); |
92 | | -}; |
0 commit comments