| ... | @@ -108,13 +108,24 @@ import { basenameOfPath } from "../../pierre-icons"; | ... | @@ -108,13 +108,24 @@ import { basenameOfPath } from "../../pierre-icons"; |
| 108 | import { cn, randomUUID } from "~/lib/utils"; | 108 | import { cn, randomUUID } from "~/lib/utils"; |
| 109 | import { Separator } from "../ui/separator"; | 109 | import { Separator } from "../ui/separator"; |
| 110 | | 110 | |
| | 111 | type ComposerCommandMenuPosition = { |
| | 112 | bottom: number; |
| | 113 | left: number; |
| | 114 | maxHeight: number; |
| | 115 | width: number; |
| | 116 | }; |
| | 117 | |
| | 118 | function composerCommandMenuPositionsEqual( |
| | 119 | a: ComposerCommandMenuPosition, |
| | 120 | b: ComposerCommandMenuPosition, |
| | 121 | ): boolean { |
| | 122 | return ( |
| | 123 | a.bottom === b.bottom && a.left === b.left && a.maxHeight === b.maxHeight && a.width === b.width |
| | 124 | ); |
| | 125 | } |
| | 126 | |
| 111 | function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children: ReactNode }) { | 127 | function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children: ReactNode }) { |
| 112 | const [position, setPosition] = useState<{ | 128 | const [position, setPosition] = useState<ComposerCommandMenuPosition | null>(null); |
| 113 | bottom: number; | | |
| 114 | left: number; | | |
| 115 | maxHeight: number; | | |
| 116 | width: number; | | |
| 117 | } | null>(null); | | |
| 118 | | 129 | |
| 119 | useLayoutEffect(() => { | 130 | useLayoutEffect(() => { |
| 120 | const anchor = props.anchor; | 131 | const anchor = props.anchor; |
| ... | @@ -125,12 +136,15 @@ function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children: | ... | @@ -125,12 +136,15 @@ function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children: |
| 125 | | 136 | |
| 126 | const updatePosition = () => { | 137 | const updatePosition = () => { |
| 127 | const rect = anchor.getBoundingClientRect(); | 138 | const rect = anchor.getBoundingClientRect(); |
| 128 | setPosition({ | 139 | const next = { |
| 129 | bottom: window.innerHeight - rect.top + 8, | 140 | bottom: window.innerHeight - rect.top + 8, |
| 130 | left: rect.left, | 141 | left: rect.left, |
| 131 | maxHeight: Math.max(96, rect.top - 24), | 142 | maxHeight: Math.max(96, rect.top - 24), |
| 132 | width: rect.width, | 143 | width: rect.width, |
| 133 | }); | 144 | }; |
| | 145 | setPosition((current) => |
| | 146 | current && composerCommandMenuPositionsEqual(current, next) ? current : next, |
| | 147 | ); |
| 134 | }; | 148 | }; |
| 135 | | 149 | |
| 136 | updatePosition(); | 150 | updatePosition(); |
| ... | @@ -139,7 +153,16 @@ function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children: | ... | @@ -139,7 +153,16 @@ function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children: |
| 139 | | 153 | |
| 140 | const observer = | 154 | const observer = |
| 141 | typeof ResizeObserver === "undefined" ? null : new ResizeObserver(updatePosition); | 155 | typeof ResizeObserver === "undefined" ? null : new ResizeObserver(updatePosition); |
| 142 | observer?.observe(anchor); | 156 | if (observer) { |
| | 157 | // The composer is centered and capped at a max width, so opening a side |
| | 158 | // panel slides it sideways without ever resizing it. Watching the anchor |
| | 159 | // alone would leave the menu behind; the ancestors are what shrink, and |
| | 160 | // they resize on every frame of the panel animation. |
| | 161 | observer.observe(anchor); |
| | 162 | for (let element = anchor.parentElement; element; element = element.parentElement) { |
| | 163 | observer.observe(element); |
| | 164 | } |
| | 165 | } |
| 143 | | 166 | |
| 144 | return () => { | 167 | return () => { |
| 145 | observer?.disconnect(); | 168 | observer?.disconnect(); |