Skip to content
Draft
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
19 changes: 11 additions & 8 deletions src/components/context-menu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,25 @@ import './style.css';

const IdCtx = createContext<string | undefined>(undefined);

let lastAnchor: HTMLElement | null = null;

function handleContextMenu(e: MouseEvent) {
e.preventDefault();
const el = e.currentTarget as HTMLElement;
if (lastAnchor) lastAnchor.style.anchorName = '';
lastAnchor = el;
el.style.anchorName = '--p-context-menu';
const target = el.ownerDocument.getElementById(
el.getAttribute('commandfor')!,
) as HTMLDialogElement;
if (!target) return;
const rect = el.getBoundingClientRect();
target.style.setProperty('--p-context-menu-x', `${e.clientX - rect.x}px`);
target.style.setProperty('--p-context-menu-y', `${e.clientY - rect.y}px`);
let x = e.clientX;
let y = e.clientY;
target.style.left = x + 'px';
target.style.top = y + 'px';
target.showModal();
const r = target.getBoundingClientRect();
if (x + r.width > innerWidth) x = innerWidth - r.width;
if (y + r.height > innerHeight) y = innerHeight - r.height;
if (x < 0) x = 0;
if (y < 0) y = 0;
target.style.left = x + 'px';
target.style.top = y + 'px';
}

export function ContextMenuTrigger({children}: JSX.ElementChildrenAttribute) {
Expand Down
10 changes: 4 additions & 6 deletions src/components/context-menu/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@

body [p="context-menu"] {
position: fixed;
position-anchor: --p-context-menu;
left: anchor(left);
top: anchor(top);
--tf-x: 0;
left: 0;
top: 0;
min-width: 6rem;
margin-left: var(--p-context-menu-x);
margin-top: var(--p-context-menu-y);
padding: 0.25rem 0;
--p-x: 0;
--p-y: 0;
}
[p="context-menu"]::backdrop {
opacity: 0;
Expand Down
4 changes: 1 addition & 3 deletions src/components/dropdown-menu/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@
@import "../popover/style.css";

[p="dropdown-content"] {
left: 0;
--tf-x: 0;
padding: 0.5rem 0;
min-width: 100%;
min-width: var(--p-anchor-width);
}

[p="dropdown-menu-item"],
Expand Down
53 changes: 39 additions & 14 deletions src/components/popover/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,40 +11,65 @@
[p="dropdown-content"],
[p="context-menu"],
[p="combobox-list"] {
position: absolute;
display: none;
top: 100%;
left: 50%;
--tf-x: -50%;
min-width: 100%;
transform-origin: top center;
background-color: hsl(var(--p-popover));
color: hsl(var(--p-popover-foreground));
border-radius: var(--p-radius);
border: 1px solid hsl(var(--p-border));
padding: 0;
/* padding: 0.5rem; */
margin: 0.25rem 0 0;
box-shadow: 0 4px 12px rgb(0 0 0 / 0.15);
pointer-events: none;
/* animation: popover-show 150ms cubic-bezier(0.4,0,0.2,1); */
animation: popover-show 200ms var(--p-ease);
z-index: 10;
outline: none;
margin: 0;
right: auto;
bottom: auto;
}

[p="popover-content"],
[p="dropdown-content"],
[p="combobox-list"] {
position: fixed;
display: none;
left: 0;
top: 0;
right: auto;
bottom: auto;
--p-gap: 0.25rem;
transform-origin: top left;
--p-x: var(--p-anchor-left);
--p-y: calc(var(--p-anchor-top) + var(--p-anchor-height) + var(--p-gap));
transform: translate(
max(0px, min(calc(100vw - var(--p-content-width, 0px)), var(--p-x))),
max(0px, min(calc(100vh - var(--p-content-height, 0px)), var(--p-y)))
);
}

[p="popover-content"][data-flip~="x"],
[p="dropdown-content"][data-flip~="x"],
[p="combobox-list"][data-flip~="x"] {
--p-x: calc(
var(--p-anchor-left) + var(--p-anchor-width) - var(--p-content-width)
);
}

[p="popover-content"][data-flip~="y"],
[p="dropdown-content"][data-flip~="y"],
[p="combobox-list"][data-flip~="y"] {
--p-y: calc(var(--p-anchor-top) - var(--p-content-height) - var(--p-gap));
}

[p="popover-content"][open],
[p="dropdown-content"][open],
[p="context-menu"][open],
[p="combobox-list"][open] {
[p="combobox-list"][open],
[p="context-menu"][open] {
display: inline-block;
pointer-events: auto;
transform: translateX(var(--tf-x)) scale(1);
}

@keyframes popover-show {
from {
opacity: 0;
transform: translateX(var(--tf-x)) scale(0.9);
transform: translate(var(--p-x), var(--p-y)) scale(0.9);
}
}
53 changes: 53 additions & 0 deletions src/lib/commands.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
let commandsInstalled: boolean;
const triggers = new WeakMap<Element, Element>();

export function installCommands() {
if (commandsInstalled) return;
commandsInstalled = true;
if ('commandFor' in HTMLButtonElement.prototype) return;
addEventListener('click', commandClickHandler);
addEventListener('toggle', dialogToggleHandler, true);
addEventListener('show', dialogToggleHandler, true);
}

function elementTarget(node: EventTarget) {
Expand All @@ -13,6 +16,46 @@ function elementTarget(node: EventTarget) {
: ((node as Node).parentNode as Element);
}

function positionDialog(target: HTMLDialogElement, trigger: Element) {
const a = trigger.getBoundingClientRect();
const t = target.style.transform;
target.style.setProperty('transform', 'none');
const d = target.getBoundingClientRect();
if (t) target.style.setProperty('transform', t);
else target.style.removeProperty('transform');
target.style.setProperty('--p-anchor-left', `${a.left}px`);
target.style.setProperty('--p-anchor-top', `${a.top}px`);
target.style.setProperty('--p-anchor-width', `${a.width}px`);
target.style.setProperty('--p-anchor-height', `${a.height}px`);
target.style.setProperty('--p-content-width', `${d.width}px`);
target.style.setProperty('--p-content-height', `${d.height}px`);
const flipX = a.left + d.width > innerWidth && a.left + a.width > d.width;
const flipY =
a.top + a.height + d.height > innerHeight && a.top > d.height;
let flip = '';
if (flipX) flip += ' x';
if (flipY) flip += ' y';
target.setAttribute('data-flip', flip);
}

let resizeInstalled: boolean;
function ensureResize() {
if (resizeInstalled) return;
resizeInstalled = true;
addEventListener(
'resize',
() => {
document
.querySelectorAll<HTMLDialogElement>('dialog[p][open]')
.forEach((el) => {
const trig = triggers.get(el);
if (trig) positionDialog(el, trig);
});
},
{passive: true},
);
}

function commandClickHandler(e: MouseEvent) {
const el = elementTarget(e.target!);
const trigger = el.closest<Element>('[command]');
Expand All @@ -37,9 +80,19 @@ function commandClickHandler(e: MouseEvent) {
}

const method = command.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
if (target instanceof HTMLDialogElement) triggers.set(target, trigger);
(target as any)[method]?.();
}

function dialogToggleHandler(e: Event) {
const target = e.target as Element;
if (target instanceof HTMLDialogElement && target.open) {
const trig = triggers.get(target);
if (trig) positionDialog(target, trig);
ensureResize();
}
}

let dialogsDropdownsInstalled: boolean;
export function installDialogsDropdowns() {
if (dialogsDropdownsInstalled) return;
Expand Down