| ... | ... | @@ -108,13 +108,24 @@ import { basenameOfPath } from "../../pierre-icons"; |
| 108 | 108 | import { cn, randomUUID } from "~/lib/utils"; |
| 109 | 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 | 127 | function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children: ReactNode }) { |
| 112 | | const [position, setPosition] = useState<{ |
| 113 | | bottom: number; |
| 114 | | left: number; |
| 115 | | maxHeight: number; |
| 116 | | width: number; |
| 117 | | } | null>(null); |
| 128 | const [position, setPosition] = useState<ComposerCommandMenuPosition | null>(null); |
| 118 | 129 | |
| 119 | 130 | useLayoutEffect(() => { |
| 120 | 131 | const anchor = props.anchor; |
| ... | ... | @@ -125,12 +136,15 @@ function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children: |
| 125 | 136 | |
| 126 | 137 | const updatePosition = () => { |
| 127 | 138 | const rect = anchor.getBoundingClientRect(); |
| 128 | | setPosition({ |
| 139 | const next = { |
| 129 | 140 | bottom: window.innerHeight - rect.top + 8, |
| 130 | 141 | left: rect.left, |
| 131 | 142 | maxHeight: Math.max(96, rect.top - 24), |
| 132 | 143 | width: rect.width, |
| 133 | | }); |
| 144 | }; |
| 145 | setPosition((current) => |
| 146 | current && composerCommandMenuPositionsEqual(current, next) ? current : next, |
| 147 | ); |
| 134 | 148 | }; |
| 135 | 149 | |
| 136 | 150 | updatePosition(); |
| ... | ... | @@ -139,7 +153,16 @@ function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children: |
| 139 | 153 | |
| 140 | 154 | const observer = |
| 141 | 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 | 167 | return () => { |
| 145 | 168 | observer?.disconnect(); |