Skip to content

Commit febb4f7

Browse files
authored
fix(editor): stabilize selection menu sizing and transitions (#2833)
1 parent 7357c01 commit febb4f7

2 files changed

Lines changed: 116 additions & 50 deletions

File tree

‎src/cm/touchSelectionMenu.js‎

Lines changed: 103 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ class TouchSelectionMenuController {
174174
#menuAnchor = null;
175175
#menuAnimation = null;
176176
#viewAnimations = [];
177+
#overflowExpanded = false;
177178

178179
constructor(view, options = {}) {
179180
this.#view = view;
@@ -680,7 +681,8 @@ class TouchSelectionMenuController {
680681
const menuKey = `${hasSelection}:${items
681682
.map((item) => item.id || this.#getItemLabel(item))
682683
.join("|")}`;
683-
if (menuKey !== this.#renderedMenuKey) {
684+
const menuChanged = menuKey !== this.#renderedMenuKey;
685+
if (menuChanged) {
684686
const groups = partitionSelectionMenuItems(items, { hasSelection });
685687
this.#renderMenu(groups.primary, groups.overflow);
686688
this.#renderedMenuKey = menuKey;
@@ -689,6 +691,8 @@ class TouchSelectionMenuController {
689691
if (!this.$menu.isConnected) {
690692
this.#container.append(this.$menu);
691693
}
694+
this.$menu.style.removeProperty("width");
695+
this.$menu.style.removeProperty("height");
692696
const isOpening = !this.#menuActive;
693697
this.#positionMenu(anchor);
694698
this.#menuActive = true;
@@ -697,6 +701,11 @@ class TouchSelectionMenuController {
697701
}
698702

699703
#renderMenu(primaryItems, overflowItems) {
704+
for (const animation of this.#viewAnimations) animation.cancel?.();
705+
this.#viewAnimations = [];
706+
this.#overflowExpanded = false;
707+
this.$menu.style.removeProperty("width");
708+
this.$menu.style.removeProperty("height");
700709
this.$menu.replaceChildren();
701710
this.$menu.setAttribute("aria-label", "Text selection actions");
702711

@@ -822,68 +831,121 @@ class TouchSelectionMenuController {
822831
}
823832

824833
#setOverflowExpanded($primary, $overflow, expanded) {
834+
if (expanded === this.#overflowExpanded) return;
835+
const initialRect = this.$menu.getBoundingClientRect();
836+
this.#menuAnimation?.cancel?.();
837+
this.#menuAnimation = null;
825838
for (const animation of this.#viewAnimations) animation.cancel?.();
826839
this.#viewAnimations = [];
827-
const initialRect = this.$menu.getBoundingClientRect();
828-
const outgoing = expanded ? $primary : $overflow;
840+
$primary.hidden = this.#overflowExpanded;
841+
$overflow.hidden = !this.#overflowExpanded;
842+
const outgoing = this.#overflowExpanded ? $overflow : $primary;
829843
const incoming = expanded ? $overflow : $primary;
830844
const direction = expanded ? 1 : -1;
831-
this.$menu.style.width = `${this.$menu.getBoundingClientRect().width}px`;
832-
incoming.hidden = false;
833-
incoming.style.visibility = "";
834-
incoming.style.pointerEvents = "auto";
845+
for (const $view of [$primary, $overflow]) {
846+
$view.style.opacity = "";
847+
$view.style.transform = "";
848+
$view.style.visibility = "";
849+
$view.style.position = "";
850+
$view.style.inset = "";
851+
$view.style.pointerEvents = "";
852+
$view.style.transformOrigin = "";
853+
}
854+
this.#overflowExpanded = expanded;
855+
outgoing.style.position = "absolute";
856+
outgoing.style.inset = "0 auto auto 0";
835857
outgoing.style.pointerEvents = "none";
836-
837-
const usesGrid = $overflow.classList.contains(
838-
"cursor-menu__overflow--grid",
839-
);
840-
const targetHeight =
841-
expanded && usesGrid ? $overflow.getBoundingClientRect().height : 40;
842-
this.$menu.style.height = `${targetHeight}px`;
843-
if (usesGrid && this.#menuAnchor) this.#positionMenu(this.#menuAnchor);
858+
incoming.hidden = false;
859+
this.$menu.style.removeProperty("width");
860+
this.$menu.style.removeProperty("height");
861+
this.$menu.style.transform = "";
862+
if (this.#menuAnchor) this.#positionMenu(this.#menuAnchor);
844863
const finalRect = this.$menu.getBoundingClientRect();
864+
const menuScaleX = finalRect.width
865+
? initialRect.width / finalRect.width
866+
: 1;
867+
const menuScaleY = finalRect.height
868+
? initialRect.height / finalRect.height
869+
: 1;
870+
const viewScaleX = menuScaleX ? 1 / menuScaleX : 1;
871+
const viewScaleY = menuScaleY ? 1 / menuScaleY : 1;
872+
this.$menu.style.transformOrigin = "left top";
873+
for (const $view of [outgoing, incoming]) {
874+
$view.style.transformOrigin = "left top";
875+
}
845876

877+
const clearMenuGeometry = () => {
878+
this.$menu.style.removeProperty("width");
879+
this.$menu.style.removeProperty("height");
880+
this.$menu.style.transform = "";
881+
this.$menu.style.transformOrigin = "";
882+
};
846883
const finish = () => {
847884
outgoing.hidden = true;
848-
outgoing.style.opacity = "";
849-
outgoing.style.transform = "";
850-
outgoing.style.visibility = "";
851-
incoming.style.opacity = "";
852-
incoming.style.transform = "";
853-
this.$menu.style.height = `${targetHeight}px`;
854-
this.$menu.style.transform = "";
885+
for (const $view of [$primary, $overflow]) {
886+
$view.style.opacity = "";
887+
$view.style.transform = "";
888+
$view.style.position = "";
889+
$view.style.inset = "";
890+
$view.style.pointerEvents = "";
891+
$view.style.transformOrigin = "";
892+
}
893+
clearMenuGeometry();
855894
this.#viewAnimations = [];
856895
};
896+
857897
if (animationsDisabled()) {
858898
finish();
859899
return;
860900
}
861-
862901
const outgoingAnimation = animate(
863902
outgoing,
864-
{ opacity: [1, 0], x: [0, -6 * direction] },
865-
{ duration: 0.1, ease: "easeIn" },
903+
{
904+
opacity: [1, 0],
905+
x: [0, -6 * direction],
906+
scaleX: [viewScaleX, 1],
907+
scaleY: [viewScaleY, 1],
908+
},
909+
{ duration: 0.12, ease: "easeIn" },
866910
);
867911
const incomingAnimation = animate(
868912
incoming,
869-
{ opacity: [0, 1], x: [6 * direction, 0] },
870-
{ duration: 0.14, ease: "easeOut" },
913+
{
914+
opacity: [0, 1],
915+
x: [6 * direction, 0],
916+
scaleX: [viewScaleX, 1],
917+
scaleY: [viewScaleY, 1],
918+
},
919+
{ duration: 0.18, ease: "easeOut" },
871920
);
872921
const animations = [outgoingAnimation, incomingAnimation];
873-
if (usesGrid && initialRect.height !== finalRect.height) {
922+
if (
923+
initialRect.width !== finalRect.width ||
924+
initialRect.height !== finalRect.height ||
925+
initialRect.left !== finalRect.left ||
926+
initialRect.top !== finalRect.top
927+
) {
874928
animations.push(
875929
animate(
876930
this.$menu,
877931
{
878-
height: [initialRect.height, finalRect.height],
932+
x: [initialRect.left - finalRect.left, 0],
879933
y: [initialRect.top - finalRect.top, 0],
934+
scaleX: [menuScaleX, 1],
935+
scaleY: [menuScaleY, 1],
936+
},
937+
{
938+
duration: 0.18,
939+
ease: [0.2, 0, 0, 1],
940+
onComplete: clearMenuGeometry,
880941
},
881-
{ duration: 0.16, ease: "easeOut" },
882942
),
883943
);
884944
}
885945
this.#viewAnimations = animations;
886-
Promise.allSettled(animations).then(() => {
946+
Promise.allSettled(
947+
animations.map((animation) => animation.finished ?? animation),
948+
).then(() => {
887949
if (this.#viewAnimations !== animations) return;
888950
finish();
889951
});
@@ -1044,22 +1106,30 @@ class TouchSelectionMenuController {
10441106
overflow.style.opacity = "";
10451107
overflow.style.transform = "";
10461108
overflow.style.pointerEvents = "";
1109+
overflow.style.position = "";
1110+
overflow.style.inset = "";
1111+
overflow.style.transformOrigin = "";
10471112
}
10481113
const primary = this.$menu.querySelector(".cursor-menu__primary");
10491114
if (primary) {
10501115
primary.hidden = false;
10511116
primary.style.opacity = "";
10521117
primary.style.transform = "";
10531118
primary.style.pointerEvents = "";
1119+
primary.style.position = "";
1120+
primary.style.inset = "";
1121+
primary.style.transformOrigin = "";
10541122
}
10551123
this.$menu.style.opacity = "";
10561124
this.$menu.style.transform = "";
1057-
this.$menu.style.width = "";
1058-
this.$menu.style.height = "40px";
1125+
this.$menu.style.transformOrigin = "";
1126+
this.$menu.style.removeProperty("width");
1127+
this.$menu.style.removeProperty("height");
10591128
this.$menu
10601129
.querySelector(".cursor-menu__more")
10611130
?.setAttribute("aria-expanded", "false");
10621131
this.#renderedMenuKey = "";
1132+
this.#overflowExpanded = false;
10631133
this.#menuAnchor = null;
10641134
this.#menuActive = false;
10651135
}

‎src/main.scss‎

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,8 @@ textarea {
482482
left: 0;
483483
width: max-content;
484484
max-width: var(--cursor-menu-max-width);
485-
height: 40px;
485+
height: auto;
486+
min-height: 40px;
486487
margin: 0;
487488
padding: 0;
488489
box-sizing: border-box;
@@ -503,12 +504,8 @@ textarea {
503504

504505
&__primary {
505506
display: flex;
506-
height: 100%;
507+
height: 40px;
507508
align-items: stretch;
508-
509-
&[hidden] {
510-
visibility: hidden;
511-
}
512509
}
513510

514511
&__action {
@@ -558,27 +555,21 @@ textarea {
558555
}
559556

560557
&__overflow {
561-
position: absolute;
562-
inset: 0;
563-
z-index: 1;
564558
display: flex;
565-
height: 100%;
559+
width: max-content;
560+
max-width: var(--cursor-menu-max-width);
561+
height: 40px;
566562
min-width: 0;
567563
color: inherit;
568564
background-color: inherit;
569565
border-radius: var(--popup-border-radius);
570566

571-
&[hidden] {
572-
visibility: hidden;
573-
}
574-
575567
&--grid {
576-
inset: 0 auto auto 0;
577568
display: grid;
578569
grid-template-columns: repeat(auto-fill, minmax(50px, 1fr));
579570
grid-auto-rows: 40px;
580571
align-content: start;
581-
width: 100%;
572+
width: min(250px, var(--cursor-menu-max-width));
582573
height: max-content;
583574
max-height: var(--cursor-menu-grid-max-height);
584575
overflow-x: hidden;
@@ -593,6 +584,11 @@ textarea {
593584
}
594585
}
595586

587+
&__primary[hidden],
588+
&__overflow[hidden] {
589+
display: none;
590+
}
591+
596592
&__back {
597593
flex: 0 0 50px;
598594
border-inline-end: solid 1px var(--border-color);
@@ -601,7 +597,7 @@ textarea {
601597
&__overflow-actions {
602598
display: flex;
603599
min-width: 0;
604-
flex: 1;
600+
flex: 0 1 auto;
605601
overflow-x: auto;
606602
overflow-y: hidden;
607603
overscroll-behavior-x: contain;

0 commit comments

Comments
 (0)