Fix Svelte regressions related to some input widgets

This commit is contained in:
Keavon Chambers 2023-02-19 22:57:11 -08:00
parent a993938d80
commit 964cf6df15
8 changed files with 80 additions and 93 deletions

View File

@ -306,7 +306,7 @@
value={newColor.toHexOptionalAlpha() || "-"}
on:commitText={({ detail }) => setColorCode(detail)}
centered={true}
tooltip="Color code in hexadecimal format. 6 digits if opaque, 8 with alpha.\nAccepts input of CSS color values including named colors."
tooltip={"Color code in hexadecimal format. 6 digits if opaque, 8 with alpha.\nAccepts input of CSS color values including named colors."}
/>
</LayoutRow>
</LayoutRow>
@ -347,8 +347,8 @@
unit={channel === "h" ? "°" : "%"}
minWidth={56}
tooltip={{
h: "Hue component, the &quot;color&quot; along the rainbow",
s: "Saturation component, the &quot;colorfulness&quot; from gray to vivid",
h: `Hue component, the "color" along the rainbow`,
s: `Saturation component, the "colorfulness" from gray to vivid`,
v: "Value (or Brightness), the distance away from being darkened to black",
}[channel]}
/>

View File

@ -1,3 +1,5 @@
<svelte:options accessors={true} />
<script lang="ts">
import { createEventDispatcher } from "svelte";
@ -28,32 +30,23 @@
export let virtualScrollingEntryHeight = 0;
export let tooltip: string | undefined = undefined;
let isOpen = open;
let highlighted = activeEntry as MenuListEntry | undefined;
let virtualScrollingEntriesStart = 0;
// Called only when `open` is changed from outside this component (with v-model)
$: watchOpen(open);
$: dispatch("open", isOpen);
$: watchEntries(entries, self);
$: watchDrawIcon(drawIcon, self);
$: watchRemeasureWidth(entries, drawIcon);
$: virtualScrollingTotalHeight = entries.length === 0 ? 0 : entries[0].length * virtualScrollingEntryHeight;
$: virtualScrollingStartIndex = Math.floor(virtualScrollingEntriesStart / virtualScrollingEntryHeight) || 0;
$: virtualScrollingEndIndex = entries.length === 0 ? 0 : Math.min(entries[0].length, virtualScrollingStartIndex + 1 + 400 / virtualScrollingEntryHeight);
function watchOpen(open: boolean) {
isOpen = open;
highlighted = activeEntry;
dispatch("open", open);
}
// TODO: Svelte: fix infinite loop and reenable
function watchEntries(_: MenuListEntry[][], floatingMenu: FloatingMenu) {
// floatingMenu?.div().measureAndEmitNaturalWidth();
}
// TODO: Svelte: fix infinite loop and reenable
function watchDrawIcon(_: boolean, floatingMenu: FloatingMenu) {
// floatingMenu?.div().measureAndEmitNaturalWidth();
function watchRemeasureWidth(_: MenuListEntry[][], __: boolean) {
self?.measureAndEmitNaturalWidth();
}
function onScroll(e: Event) {
@ -69,29 +62,29 @@
dispatch("activeEntry", menuListEntry);
// Close the containing menu
if (menuListEntry.ref) menuListEntry.ref.isOpen = false;
if (menuListEntry.ref) menuListEntry.ref.open = false;
dispatch("open", false);
isOpen = false; // TODO: This is a hack for MenuBarInput submenus, remove it when we get rid of using `ref`
open = false;
}
function onEntryPointerEnter(menuListEntry: MenuListEntry): void {
if (!menuListEntry.children?.length) return;
if (menuListEntry.ref) menuListEntry.ref.isOpen = true;
if (menuListEntry.ref) menuListEntry.ref.open = true;
else dispatch("open", true);
}
function onEntryPointerLeave(menuListEntry: MenuListEntry): void {
if (!menuListEntry.children?.length) return;
if (menuListEntry.ref) menuListEntry.ref.isOpen = false;
if (menuListEntry.ref) menuListEntry.ref.open = false;
else dispatch("open", false);
}
function isEntryOpen(menuListEntry: MenuListEntry): boolean {
if (!menuListEntry.children?.length) return false;
return open;
return menuListEntry.ref?.open || false;
}
/// Handles keyboard navigation for the menu. Returns if the entire menu stack should be dismissed
@ -99,13 +92,13 @@
// Interactive menus should keep the active entry the same as the highlighted one
if (interactive) highlighted = activeEntry;
const menuOpen = isOpen;
const menuOpen = open;
const flatEntries = entries.flat().filter((entry) => !entry.disabled);
const openChild = flatEntries.findIndex((entry) => entry.children?.length && entry.ref?.isOpen);
const openChild = flatEntries.findIndex((entry) => entry.children?.length && entry.ref?.open);
const openSubmenu = (highlighted: MenuListEntry): void => {
if (highlighted.ref && highlighted.children?.length) {
highlighted.ref.isOpen = true;
highlighted.ref.open = true;
// Highlight first item
highlighted.ref.setHighlighted(highlighted.children[0][0]);
@ -114,7 +107,7 @@
if (!menuOpen && (e.key === " " || e.key === "Enter")) {
// Allow opening menu with space or enter
isOpen = true;
open = true;
highlighted = activeEntry;
} else if (menuOpen && openChild >= 0) {
// Redirect the keyboard navigation to a submenu if one is open
@ -125,7 +118,7 @@
// Handle the child closing the entire menu stack
if (shouldCloseStack) {
isOpen = false;
open = false;
return true;
}
} else if ((menuOpen || interactive) && (e.key === "ArrowUp" || e.key === "ArrowDown")) {
@ -145,7 +138,7 @@
setHighlighted(newEntry);
} else if (menuOpen && e.key === "Escape") {
// Close menu with escape key
isOpen = false;
open = false;
// Reset active to before open
setHighlighted(activeEntry);
@ -164,7 +157,7 @@
openSubmenu(highlighted);
} else if (menuOpen && e.key === "ArrowLeft") {
// Left arrow closes a submenu
if (submenu) isOpen = false;
if (submenu) open = false;
}
// By default, keep the menu stack open
@ -177,20 +170,15 @@
if (interactive && newHighlight?.value !== activeEntry?.value && newHighlight) dispatch("activeEntry", newHighlight);
}
// TODO: Svelte: Re-enable the `export` prefix
export function scrollViewTo(distanceDown: number): void {
scroller.div().scrollTo(0, distanceDown);
}
export function menuIsOpen(): boolean {
return open;
}
</script>
<FloatingMenu
class="menu-list"
open={isOpen}
on:open={({ detail }) => (isOpen = detail)}
{open}
on:open={({ detail }) => (open = detail)}
on:naturalWidth
type="Dropdown"
windowEdgeMargin={0}
@ -248,7 +236,7 @@
{/if}
{#if entry.children}
<svelte:self on:naturalWidth open={entry.ref?.menuIsOpen() || false} direction="TopRight" entries={entry.children} {minWidth} {drawIcon} {scrollableY} bind:this={entry.ref} />
<svelte:self on:naturalWidth open={entry.ref?.open || false} direction="TopRight" entries={entry.children} {minWidth} {drawIcon} {scrollableY} bind:this={entry.ref} />
{/if}
</LayoutRow>
{/each}

View File

@ -104,10 +104,8 @@
// Gets the client bounds of the elements and apply relevant styles to them
// TODO: Use the Vue :style attribute more whilst not causing recursive updates
afterUpdate(() => {
// Turning measuring on and off both cause the component to change, which causes the `updated()` Vue event to fire extraneous times (hurting performance and sometimes causing an infinite loop)
if (measuringOngoingGuard) return;
positionAndStyleFloatingMenu();
// Turning measuring on and off both causes the component to change, which causes the `updated()` Vue event to fire extraneous times (hurting performance and sometimes causing an infinite loop)
if (!measuringOngoingGuard) positionAndStyleFloatingMenu();
});
function resizeObserverCallback(entries: ResizeObserverEntry[]) {
@ -204,13 +202,13 @@
// To be called by the parent component. Measures the actual width of the floating menu content element and returns it in a promise.
export async function measureAndEmitNaturalWidth(): Promise<void> {
if (!measuringOngoingGuard) return;
// Wait for the changed content which fired the `updated()` Vue event to be put into the DOM
await tick();
// Wait until all fonts have been loaded and rendered so measurements of content involving text are accurate
// API is experimental but supported in all browsers - https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await (document as any).fonts.ready;
await document.fonts.ready;
// Make the component show itself with 0 min-width so it can be measured, and wait until the values have been updated to the DOM
measuringOngoing = true;

View File

@ -60,25 +60,20 @@
let layerTreeOptionsLayout = defaultWidgetLayout();
onMount(() => {
editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerTreeStructureJs, (updateDocumentLayerTreeStructure) => {
rebuildLayerTree(updateDocumentLayerTreeStructure);
});
editor.subscriptions.subscribeJsMessage(UpdateLayerTreeOptionsLayout, (updateLayerTreeOptionsLayout) => {
patchWidgetLayout(layerTreeOptionsLayout, updateLayerTreeOptionsLayout);
layerTreeOptionsLayout = layerTreeOptionsLayout;
});
editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerDetails, (updateDocumentLayerDetails) => {
const targetPath = updateDocumentLayerDetails.data.path;
const targetLayer = updateDocumentLayerDetails.data;
editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerTreeStructureJs, (updateDocumentLayerTreeStructure) => {
rebuildLayerTree(updateDocumentLayerTreeStructure);
});
const layer = layerCache.get(targetPath.toString());
if (layer) {
Object.assign(layer, targetLayer);
} else {
layerCache.set(targetPath.toString(), targetLayer);
}
editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerDetails, (updateDocumentLayerDetails) => {
const targetLayer = updateDocumentLayerDetails.data;
const targetPath = targetLayer.path;
updateLayerInTree(targetPath, targetLayer);
});
});
@ -134,34 +129,23 @@
window.getSelection()?.removeAllRanges();
}
// TODO: Svelte: test this works
function selectLayerWithModifiers(e: MouseEvent, listing: LayerListingInfo) {
const ctrl = e.ctrlKey;
const meta = e.metaKey;
const shift = e.shiftKey;
const alt = e.altKey;
// Get the pressed state of the modifier keys
const [ctrl, meta, shift, alt] = [e.ctrlKey, e.metaKey, e.shiftKey, e.altKey];
// Get the state of the platform's accel key and its opposite platform's accel key
const [accel, oppositeAccel] = platformIsMac() ? [meta, ctrl] : [ctrl, meta];
if (!ctrl && !meta && !shift && !alt) selectLayer(false, false, false, listing, e);
else if (!ctrl && !meta && shift && !alt) selectLayer(false, false, true, listing, e);
else if (ctrl && !meta && !shift && !alt) selectLayer(true, false, false, listing, e);
else if (ctrl && !meta && shift && !alt) selectLayer(true, false, true, listing, e);
else if (!ctrl && meta && !shift && !alt) selectLayer(false, true, false, listing, e);
else if (!ctrl && meta && shift && !alt) selectLayer(false, true, true, listing, e);
else if ((ctrl && meta) || alt) e.stopPropagation();
// Select the layer only if the accel and/or shift keys are pressed
if (!oppositeAccel && !alt) selectLayer(accel, shift, listing);
e.stopPropagation();
}
async function selectLayer(ctrl: boolean, cmd: boolean, shift: boolean, listing: LayerListingInfo, event: Event) {
function selectLayer(accel: boolean, shift: boolean, listing: LayerListingInfo) {
// Don't select while we are entering text to rename the layer
if (listing.editingName) return;
const ctrlOrCmd = platformIsMac() ? cmd : ctrl;
// Pressing the Ctrl key on a Mac, or the Cmd key on another platform, is a violation of the `.exact` qualifier so we filter it out here
const opposite = platformIsMac() ? ctrl : cmd;
if (!opposite) editor.instance.selectLayer(listing.entry.path, ctrlOrCmd, shift);
// We always want to stop propagation so the click event doesn't pass through the layer and cause a deselection by clicking the layer panel background
// This is also why we cover the remaining cases not considered by the `.exact` qualifier, in the last two bindings on the layer element, with a `stopPropagation()` call
event.stopPropagation();
editor.instance.selectLayer(listing.entry.path, accel, shift);
}
async function deselectAllLayers() {
@ -243,7 +227,7 @@
fakeHighlight = [layer.path];
}
const select = (): void => {
if (!layer.layerMetadata.selected) selectLayer(false, false, false, listing, event);
if (!layer.layerMetadata.selected) selectLayer(false, false, listing);
};
const target = (event.target || undefined) as HTMLElement | undefined;
@ -283,16 +267,19 @@
const layerWithNameBeingEdited = layers.find((layer: LayerListingInfo) => layer.editingName);
const layerPathWithNameBeingEdited = layerWithNameBeingEdited?.entry.path;
const layerIdWithNameBeingEdited = layerPathWithNameBeingEdited?.slice(-1)[0];
const path = [] as bigint[];
layers = [] as LayerListingInfo[];
const path: bigint[] = [];
const recurse = (folder: UpdateDocumentLayerTreeStructureJs, layers: LayerListingInfo[], cache: Map<string, LayerPanelEntry>): void => {
// Clear the layer tree before rebuilding it
layers = [];
// Build the new layer tree
const recurse = (folder: UpdateDocumentLayerTreeStructureJs): void => {
folder.children.forEach((item, index) => {
// TODO: fix toString
const layerId = BigInt(item.layerId.toString());
path.push(layerId);
const mapping = cache.get(path.toString());
const mapping = layerCache.get(path.toString());
if (mapping) {
layers.push({
folderIndex: index,
@ -303,13 +290,24 @@
}
// Call self recursively if there are any children
if (item.children.length >= 1) recurse(item, layers, cache);
if (item.children.length >= 1) recurse(item);
path.pop();
});
};
recurse(updateDocumentLayerTreeStructure);
layers = layers;
}
recurse(updateDocumentLayerTreeStructure, layers, layerCache);
function updateLayerInTree(targetPath: BigUint64Array, targetLayer: LayerPanelEntry) {
const path = targetPath.toString();
layerCache.set(path, targetLayer);
const layer = layers.find((layer: LayerListingInfo) => layer.entry.path.toString() === path);
if (layer) {
layer.entry = targetLayer;
layers = layers;
}
}
function getLayerTypeData(layerType: LayerType): LayerTypeData {

View File

@ -52,5 +52,9 @@
.sections {
flex: 1 1 100%;
}
.text-button {
flex-basis: 0;
}
}
</style>

View File

@ -41,7 +41,7 @@
display: flex;
justify-content: center;
align-items: center;
flex: 0 0 0;
flex: 0 0 auto;
height: 24px;
margin: 0;
padding: 0 8px;

View File

@ -21,7 +21,6 @@
const editor = getContext<Editor>("editor");
let self: HTMLDivElement;
let entries: MenuListEntry[] = [];
function clickEntry(menuListEntry: MenuListEntry, e: MouseEvent) {
@ -36,7 +35,7 @@
(e.target as HTMLElement | undefined)?.focus();
if (menuListEntry.ref) {
menuListEntry.ref.isOpen = true;
menuListEntry.ref.open = true;
entries = entries;
} else {
throw new Error("The menu bar floating menu has no associated ref");
@ -74,7 +73,7 @@
});
</script>
<div class="menu-bar-input" bind:this={self} data-menu-bar-input>
<div class="menu-bar-input" data-menu-bar-input>
{#each entries as entry, index (index)}
<div class="entry-container">
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
@ -82,7 +81,7 @@
on:click={(e) => clickEntry(entry, e)}
on:keydown={(e) => entry.ref?.keydown(e, false)}
class="entry"
class:open={entry.ref?.isOpen}
class:open={entry.ref?.open}
tabindex="0"
data-floating-menu-spawner={entry.children && entry.children.length > 0 ? "" : "no-hover-transfer"}
>
@ -96,9 +95,9 @@
{#if entry.children && entry.children.length > 0}
<MenuList
on:open={(e) => {
if (entry.ref) entry.ref.isOpen = e.detail;
if (entry.ref) entry.ref.open = e.detail;
}}
open={entry.ref?.isOpen || false}
open={entry.ref?.open || false}
entries={entry.children || []}
direction="Bottom"
minWidth={240}

View File

@ -1232,7 +1232,7 @@ export function defaultWidgetLayout(): WidgetLayout {
}
// Updates a widget layout based on a list of updates, returning the new layout
export function patchWidgetLayout(layout: WidgetLayout, updates: WidgetDiffUpdate): void {
export function patchWidgetLayout(/* mut */ layout: WidgetLayout, updates: WidgetDiffUpdate): void {
layout.layoutTarget = updates.layoutTarget;
updates.diff.forEach((update) => {