Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 103 additions & 33 deletions src/cm/touchSelectionMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ class TouchSelectionMenuController {
#menuAnchor = null;
#menuAnimation = null;
#viewAnimations = [];
#overflowExpanded = false;

constructor(view, options = {}) {
this.#view = view;
Expand Down Expand Up @@ -680,7 +681,8 @@ class TouchSelectionMenuController {
const menuKey = `${hasSelection}:${items
.map((item) => item.id || this.#getItemLabel(item))
.join("|")}`;
if (menuKey !== this.#renderedMenuKey) {
const menuChanged = menuKey !== this.#renderedMenuKey;
if (menuChanged) {
const groups = partitionSelectionMenuItems(items, { hasSelection });
this.#renderMenu(groups.primary, groups.overflow);
this.#renderedMenuKey = menuKey;
Expand All @@ -689,6 +691,8 @@ class TouchSelectionMenuController {
if (!this.$menu.isConnected) {
this.#container.append(this.$menu);
}
this.$menu.style.removeProperty("width");
this.$menu.style.removeProperty("height");
const isOpening = !this.#menuActive;
this.#positionMenu(anchor);
this.#menuActive = true;
Expand All @@ -697,6 +701,11 @@ class TouchSelectionMenuController {
}

#renderMenu(primaryItems, overflowItems) {
for (const animation of this.#viewAnimations) animation.cancel?.();
this.#viewAnimations = [];
this.#overflowExpanded = false;
this.$menu.style.removeProperty("width");
this.$menu.style.removeProperty("height");
this.$menu.replaceChildren();
this.$menu.setAttribute("aria-label", "Text selection actions");

Expand Down Expand Up @@ -822,68 +831,121 @@ class TouchSelectionMenuController {
}

#setOverflowExpanded($primary, $overflow, expanded) {
if (expanded === this.#overflowExpanded) return;
const initialRect = this.$menu.getBoundingClientRect();
this.#menuAnimation?.cancel?.();
this.#menuAnimation = null;
for (const animation of this.#viewAnimations) animation.cancel?.();
this.#viewAnimations = [];
const initialRect = this.$menu.getBoundingClientRect();
const outgoing = expanded ? $primary : $overflow;
$primary.hidden = this.#overflowExpanded;
$overflow.hidden = !this.#overflowExpanded;
const outgoing = this.#overflowExpanded ? $overflow : $primary;
const incoming = expanded ? $overflow : $primary;
const direction = expanded ? 1 : -1;
this.$menu.style.width = `${this.$menu.getBoundingClientRect().width}px`;
incoming.hidden = false;
incoming.style.visibility = "";
incoming.style.pointerEvents = "auto";
for (const $view of [$primary, $overflow]) {
$view.style.opacity = "";
$view.style.transform = "";
$view.style.visibility = "";
$view.style.position = "";
$view.style.inset = "";
$view.style.pointerEvents = "";
$view.style.transformOrigin = "";
}
this.#overflowExpanded = expanded;
outgoing.style.position = "absolute";
outgoing.style.inset = "0 auto auto 0";
outgoing.style.pointerEvents = "none";

const usesGrid = $overflow.classList.contains(
"cursor-menu__overflow--grid",
);
const targetHeight =
expanded && usesGrid ? $overflow.getBoundingClientRect().height : 40;
this.$menu.style.height = `${targetHeight}px`;
if (usesGrid && this.#menuAnchor) this.#positionMenu(this.#menuAnchor);
incoming.hidden = false;
this.$menu.style.removeProperty("width");
this.$menu.style.removeProperty("height");
this.$menu.style.transform = "";
if (this.#menuAnchor) this.#positionMenu(this.#menuAnchor);
const finalRect = this.$menu.getBoundingClientRect();
const menuScaleX = finalRect.width
? initialRect.width / finalRect.width
: 1;
const menuScaleY = finalRect.height
? initialRect.height / finalRect.height
: 1;
const viewScaleX = menuScaleX ? 1 / menuScaleX : 1;
const viewScaleY = menuScaleY ? 1 / menuScaleY : 1;
this.$menu.style.transformOrigin = "left top";
for (const $view of [outgoing, incoming]) {
$view.style.transformOrigin = "left top";
}

const clearMenuGeometry = () => {
this.$menu.style.removeProperty("width");
this.$menu.style.removeProperty("height");
this.$menu.style.transform = "";
this.$menu.style.transformOrigin = "";
};
const finish = () => {
outgoing.hidden = true;
outgoing.style.opacity = "";
outgoing.style.transform = "";
outgoing.style.visibility = "";
incoming.style.opacity = "";
incoming.style.transform = "";
this.$menu.style.height = `${targetHeight}px`;
this.$menu.style.transform = "";
for (const $view of [$primary, $overflow]) {
$view.style.opacity = "";
$view.style.transform = "";
$view.style.position = "";
$view.style.inset = "";
$view.style.pointerEvents = "";
$view.style.transformOrigin = "";
}
clearMenuGeometry();
this.#viewAnimations = [];
};

if (animationsDisabled()) {
finish();
return;
}

const outgoingAnimation = animate(
outgoing,
{ opacity: [1, 0], x: [0, -6 * direction] },
{ duration: 0.1, ease: "easeIn" },
{
opacity: [1, 0],
x: [0, -6 * direction],
scaleX: [viewScaleX, 1],
scaleY: [viewScaleY, 1],
},
{ duration: 0.12, ease: "easeIn" },
);
const incomingAnimation = animate(
incoming,
{ opacity: [0, 1], x: [6 * direction, 0] },
{ duration: 0.14, ease: "easeOut" },
{
opacity: [0, 1],
x: [6 * direction, 0],
scaleX: [viewScaleX, 1],
scaleY: [viewScaleY, 1],
},
{ duration: 0.18, ease: "easeOut" },
);
const animations = [outgoingAnimation, incomingAnimation];
if (usesGrid && initialRect.height !== finalRect.height) {
if (
initialRect.width !== finalRect.width ||
initialRect.height !== finalRect.height ||
initialRect.left !== finalRect.left ||
initialRect.top !== finalRect.top
) {
animations.push(
animate(
this.$menu,
{
height: [initialRect.height, finalRect.height],
x: [initialRect.left - finalRect.left, 0],
y: [initialRect.top - finalRect.top, 0],
scaleX: [menuScaleX, 1],
scaleY: [menuScaleY, 1],
},
{
duration: 0.18,
ease: [0.2, 0, 0, 1],
onComplete: clearMenuGeometry,
},
{ duration: 0.16, ease: "easeOut" },
),
);
}
this.#viewAnimations = animations;
Promise.allSettled(animations).then(() => {
Promise.allSettled(
animations.map((animation) => animation.finished ?? animation),
).then(() => {
if (this.#viewAnimations !== animations) return;
finish();
});
Expand Down Expand Up @@ -1044,22 +1106,30 @@ class TouchSelectionMenuController {
overflow.style.opacity = "";
overflow.style.transform = "";
overflow.style.pointerEvents = "";
overflow.style.position = "";
overflow.style.inset = "";
overflow.style.transformOrigin = "";
}
const primary = this.$menu.querySelector(".cursor-menu__primary");
if (primary) {
primary.hidden = false;
primary.style.opacity = "";
primary.style.transform = "";
primary.style.pointerEvents = "";
primary.style.position = "";
primary.style.inset = "";
primary.style.transformOrigin = "";
}
this.$menu.style.opacity = "";
this.$menu.style.transform = "";
this.$menu.style.width = "";
this.$menu.style.height = "40px";
this.$menu.style.transformOrigin = "";
this.$menu.style.removeProperty("width");
this.$menu.style.removeProperty("height");
this.$menu
.querySelector(".cursor-menu__more")
?.setAttribute("aria-expanded", "false");
this.#renderedMenuKey = "";
this.#overflowExpanded = false;
this.#menuAnchor = null;
this.#menuActive = false;
}
Expand Down
30 changes: 13 additions & 17 deletions src/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,8 @@ textarea {
left: 0;
width: max-content;
max-width: var(--cursor-menu-max-width);
height: 40px;
height: auto;
min-height: 40px;
margin: 0;
padding: 0;
box-sizing: border-box;
Expand All @@ -503,12 +504,8 @@ textarea {

&__primary {
display: flex;
height: 100%;
height: 40px;
align-items: stretch;

&[hidden] {
visibility: hidden;
}
}

&__action {
Expand Down Expand Up @@ -558,27 +555,21 @@ textarea {
}

&__overflow {
position: absolute;
inset: 0;
z-index: 1;
display: flex;
height: 100%;
width: max-content;
max-width: var(--cursor-menu-max-width);
height: 40px;
min-width: 0;
color: inherit;
background-color: inherit;
border-radius: var(--popup-border-radius);

&[hidden] {
visibility: hidden;
}

&--grid {
inset: 0 auto auto 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(50px, 1fr));
grid-auto-rows: 40px;
align-content: start;
width: 100%;
width: min(250px, var(--cursor-menu-max-width));
height: max-content;
max-height: var(--cursor-menu-grid-max-height);
overflow-x: hidden;
Expand All @@ -593,6 +584,11 @@ textarea {
}
}

&__primary[hidden],
&__overflow[hidden] {
display: none;
}

&__back {
flex: 0 0 50px;
border-inline-end: solid 1px var(--border-color);
Expand All @@ -601,7 +597,7 @@ textarea {
&__overflow-actions {
display: flex;
min-width: 0;
flex: 1;
flex: 0 1 auto;
overflow-x: auto;
overflow-y: hidden;
overscroll-behavior-x: contain;
Expand Down