From d08304e06f2631e28303938a00772b28d8b892cb Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Wed, 22 May 2024 20:44:20 +0000 Subject: [PATCH 1/4] Add floating menu to example --- examples/index.html | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/examples/index.html b/examples/index.html index 3ed6fe2..2cd56e0 100644 --- a/examples/index.html +++ b/examples/index.html @@ -4,8 +4,15 @@ text-expander demo @@ -30,6 +37,7 @@

Multiword text-expander element

const {key, provide, text} = event.detail if (key === '#') { const menu = document.createElement('ul') + menu.classList.add('menu') menu.role = 'listbox' for (const issue of [ '#1 Implement a text-expander element', From d628227860f0df952883d5ed8d9d81681b964ea6 Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Wed, 22 May 2024 20:44:28 +0000 Subject: [PATCH 2/4] Fix top position below caret --- src/text-expander-element.ts | 45 +++++++++++------------------------- 1 file changed, 14 insertions(+), 31 deletions(-) diff --git a/src/text-expander-element.ts b/src/text-expander-element.ts index a03be12..45b31ed 100644 --- a/src/text-expander-element.ts +++ b/src/text-expander-element.ts @@ -20,25 +20,6 @@ type Key = { const states = new WeakMap() -function isTopLayer(el: Element) { - try { - if (el.matches(':popover-open')) return true - } catch { - /* fall through */ - } - try { - if (el.matches('dialog:modal')) return true - } catch { - /* fall through */ - } - try { - if (el.matches(':fullscreen')) return true - } catch { - /* fall through */ - } - return false -} - class TextExpander { expander: TextExpanderElement input: HTMLInputElement | HTMLTextAreaElement @@ -103,18 +84,20 @@ class TextExpander { this.expander.dispatchEvent(new Event('text-expander-activate')) - let {top, left} = new InputRange(this.input, match.position).getBoundingClientRect() - if (isTopLayer(menu)) { - const rect = this.input.getBoundingClientRect() - top += rect.top - left += rect.left - if (getComputedStyle(menu).position === 'absolute') { - top += window.scrollY - left += window.scrollX - } - } - menu.style.top = `${top}px` - menu.style.left = `${left}px` + const caretRect = new InputRange(this.input, match.position).getBoundingClientRect() + const nominalPosition = {top: caretRect.top + caretRect.height, left: caretRect.left} + menu.style.top = `${nominalPosition.top}px` + menu.style.left = `${nominalPosition.left}px` + + // Nominal position is relative to entire document, but the menu could be positioned relative to a container if + // it is not `fixed` or on the top layer + const actualPosition = menu.getBoundingClientRect() + + const topDelta = actualPosition.top - nominalPosition.top + if (topDelta !== 0) menu.style.top = `${nominalPosition.top - topDelta}px` + + const leftDelta = actualPosition.left - nominalPosition.left + if (leftDelta !== 0) menu.style.left = `${nominalPosition.left - leftDelta}px` this.combobox.start() menu.addEventListener('combobox-commit', this.oncommit) From 912db2b158bae46725ca2e934416ddb7a8e2d3fc Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Thu, 23 May 2024 19:28:27 +0000 Subject: [PATCH 3/4] Use the delta with current position to execute a single positioning --- src/text-expander-element.ts | 39 +++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/src/text-expander-element.ts b/src/text-expander-element.ts index 45b31ed..852cca7 100644 --- a/src/text-expander-element.ts +++ b/src/text-expander-element.ts @@ -84,20 +84,7 @@ class TextExpander { this.expander.dispatchEvent(new Event('text-expander-activate')) - const caretRect = new InputRange(this.input, match.position).getBoundingClientRect() - const nominalPosition = {top: caretRect.top + caretRect.height, left: caretRect.left} - menu.style.top = `${nominalPosition.top}px` - menu.style.left = `${nominalPosition.left}px` - - // Nominal position is relative to entire document, but the menu could be positioned relative to a container if - // it is not `fixed` or on the top layer - const actualPosition = menu.getBoundingClientRect() - - const topDelta = actualPosition.top - nominalPosition.top - if (topDelta !== 0) menu.style.top = `${nominalPosition.top - topDelta}px` - - const leftDelta = actualPosition.left - nominalPosition.left - if (leftDelta !== 0) menu.style.left = `${nominalPosition.left - leftDelta}px` + this.positionMenu(menu, match.position) this.combobox.start() menu.addEventListener('combobox-commit', this.oncommit) @@ -107,6 +94,30 @@ class TextExpander { this.combobox.navigate(1) } + private positionMenu(menu: HTMLElement, position: number) { + const caretRect = new InputRange(this.input, position).getBoundingClientRect() + const targetPosition = {left: caretRect.left, top: caretRect.top + caretRect.height} + + const currentPosition = menu.getBoundingClientRect() + + const delta = { + left: targetPosition.left - currentPosition.left, + top: targetPosition.top - currentPosition.top + } + + // eslint-disable-next-line no-console + console.log(delta) + + if (delta.left !== 0 || delta.top !== 0) { + // Use computedStyle to avoid nesting calc() deeper and deeper + const currentStyle = getComputedStyle(menu) + + // Using `calc` avoids having to parse the current pixel value + menu.style.left = currentStyle.left ? `calc(${currentStyle.left} + ${delta.left}px)` : `${delta.left}px` + menu.style.top = currentStyle.top ? `calc(${currentStyle.top} + ${delta.top}px)` : `${delta.top}px` + } + } + private deactivate() { const menu = this.menu if (!menu || !this.combobox) return false From c0387e8b3e2a8eaf91a282d8779f6831b32a99bd Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Thu, 23 May 2024 19:32:11 +0000 Subject: [PATCH 4/4] Delete log statement --- src/text-expander-element.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/text-expander-element.ts b/src/text-expander-element.ts index 852cca7..48a413a 100644 --- a/src/text-expander-element.ts +++ b/src/text-expander-element.ts @@ -105,9 +105,6 @@ class TextExpander { top: targetPosition.top - currentPosition.top } - // eslint-disable-next-line no-console - console.log(delta) - if (delta.left !== 0 || delta.top !== 0) { // Use computedStyle to avoid nesting calc() deeper and deeper const currentStyle = getComputedStyle(menu)