mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Refactor floating panel styling hooks
This commit is contained in:
1 parent
ae932cb968
commit
e614acdee2
10 files changed
+268
-57
No files matched your search
@@ -1,3 +1,3 @@
|
||||
:root{--color-page:#fff;--color-page-alt:#f9fafb;--color-surface:#0f172a;--color-surface-alt:#111827;--color-bg:var(--color-surface);--color-card:#1e293b;--color-card-private:#3b1d74;--color-card-critical:#3f0b13;--color-panel:#1f2937;--color-panel-hover:#334155;--color-border:#374151;--color-border-subtle:#d1d5db;--color-primary:#fe0032;--color-secondary:#ff93a5;--color-logo-accent:var(--color-primary);--color-logo-wordmark:var(--color-neutral-700);--color-logo-wordmark-inverse:var(--color-white);--color-logo-background:var(--color-neutral-700);--color-logo-background-inverse:var(--color-white);--color-info:#60a5fa;--color-info-strong:#2563eb;--color-info-pressed:#1d4ed8;--color-info-glass:rgba(37,99,235,.18);--color-muted-strong:#6b7280;--color-text:#f8fafc;--color-text-strong:#0f172a;--color-text-muted:var(--color-muted);--color-muted:#94a3b8;--color-muted-alt:#9ca3af;--color-subtle-text:#e2e8f0;--color-subtle:#e5e7eb;--color-critical:#7f1d1d;--color-critical-strong:#991b1b;--color-warning:#facc15;--color-warning-strong:#d97706;--color-warning-surface:rgba(146,64,14,.24);--color-warning-glass:rgba(250,204,21,.18);--color-neutral-700:#3f3f46;--color-neutral-600:#52525b;--color-white:#fff;--color-black:#000;--color-overlay:#1a2234;--color-overlay-black:rgba(0,0,0,.9);--color-overlay-muted:rgba(15,23,42,.55);--color-overlay-strong:rgba(15,23,42,.75);--color-overlay-darker:rgba(15,23,42,.88);--color-overlay-panel:rgba(26,34,52,.82);--color-overlay-panel-soft:rgba(26,34,52,.65);--color-overlay-panel-tint:rgba(31,41,55,.5);--color-overlay-panel-glass:rgba(28,37,58,.72);--color-overlay-panel-glass-soft:rgba(31,41,55,.55);--color-modal-backdrop:rgba(8,12,24,.58);--color-modal-surface:rgba(20,28,46,.78);--color-overlay-veil:rgba(11,17,32,.96);--color-overlay-banner:rgba(51,65,85,.95);--color-sidebar-surface:rgba(17,24,39,.94);--color-sidebar-surface-alt:rgba(11,22,48,.78);--color-sidebar-border:rgba(148,163,184,.28);--color-sidebar-border-strong:rgba(148,163,184,.42);--color-sidebar-hover:rgba(56,189,248,.18);--color-sidebar-hover-strong:rgba(129,140,248,.28);--color-sidebar-glow:rgba(56,189,248,.35);--color-sidebar-accent-start:rgba(56,189,248,.9);--color-sidebar-accent-end:rgba(129,140,248,.85);--color-sidebar-text-muted:rgba(203,213,225,.75);--color-overlay-panel-gradient-start:rgba(30,41,59,.92);--color-overlay-panel-gradient-end:rgba(15,23,42,.92);--color-surface-tint-subtle:rgba(148,163,184,.08);--color-border-translucent:rgba(148,163,184,.18);--color-border-translucent-strong:rgba(148,163,184,.25);--color-border-translucent-bright:rgba(148,163,184,.32);--color-border-translucent-stronger:rgba(148,163,184,.35);--color-border-translucent-strongest:rgba(148,163,184,.4);--color-border-translucent-glow:rgba(148,163,184,.55);--color-border-translucent-medium:rgba(148,163,184,.2);--color-border-neutral-soft:rgba(229,231,235,.35);--color-border-info-soft:rgba(96,165,250,.35);--color-border-sidebar-strong:rgba(55,65,81,.9);--color-ring-private:rgba(168,85,247,.55);--color-ring-private-hover:rgba(192,132,252,.65);--color-ring-critical:rgba(239,68,68,.4);--color-ring-critical-soft:rgba(239,68,68,.35);--color-ring-critical-hover:rgba(239,68,68,.45);--color-ring-critical-strong:rgba(239,68,68,.55);--color-ring-alert:rgba(220,38,38,.6);--color-gradient-visibility-start:rgba(129,140,248,.4);--color-gradient-visibility-end:rgba(192,132,252,.32);--color-white-soft:hsla(0,0%,100%,.1);--color-white-softer:hsla(0,0%,100%,.3);--color-white-softest:hsla(0,0%,100%,.4);--color-shadow-floating:rgba(8,11,30,.55);--color-shadow-elevated:rgba(8,11,30,.45);--color-shadow-panel:rgba(15,23,42,.35);--color-shadow-notification:rgba(15,23,42,.3);--color-shadow-sidebar:rgba(0,0,0,.45);--color-divider-strong:rgba(15,23,42,.35);--color-text-frosted-strong:rgba(226,232,240,.94);--color-text-frosted:rgba(226,232,240,.9);--color-text-frosted-soft:rgba(226,232,240,.82);--scrollbar-width:0.5rem;--scrollbar-track:var(--color-white-soft);--scrollbar-thumb:var(--color-white-softer);--scrollbar-thumb-hover:var(--color-white-softest);--z-sidebar-overlay:10;--z-sidebar:20;--z-modal-root:100;--z-modal-overlay:110;--z-modal-content:120;--z-modal-player:130;--sidebar-width-expanded:16rem;--sidebar-width-collapsed:4rem;--sidebar-width:var(--sidebar-width-expanded);--sidebar-control-size:2.75rem;--sidebar-content-gap:1rem;--space-sidebar-shell-inline:var(--space-lg);--space-sidebar-shell-block-start:var(--space-lg);--space-sidebar-shell-block-end:1.75rem;--space-sidebar-scroll-gap:1.75rem;--space-sidebar-panel-gap:1.35rem;--space-sidebar-panel-padding-block:1.35rem;--space-sidebar-panel-padding-inline:1.1rem;--space-sidebar-footer-gap:0.85rem;--space-sidebar-trigger-padding-block:0.78rem;--space-sidebar-popover-offset:0.85rem;--space-sidebar-popover-padding-block:0.85rem;--space-sidebar-popover-padding-inline:0.65rem;--space-sidebar-popover-gap:0.45rem;--space-sidebar-nav-padding-block:0.7rem;--space-sidebar-subnav-padding-block:0.55rem;--space-sidebar-nav-Line truncated
|
||||
|
||||
/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:var(--font-family-sans);font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--font-family-mono);font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{font-size:100%}@media (min-width:1024px){html{font-size:75%}}*,:after,:before{scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb) transparent}::-webkit-scrollbar{width:var(--scrollbar-width);height:var(--scrollbar-width)}::-webkit-scrollbar-track{background:var(--scrollbar-track)}::-webkit-scrollbar-thumb{background:var(--scrollbar-thumb);border-radius:var(--radius-sm)}::-webkit-scrollbar-thumb:hover{background:var(--scrollbar-thumb-hover)}body{font-family:var(--font-family-sans);background-color:var(--color-page);color:var(--color-text-strong);line-height:1.5;margin:0;padding:0;overflow-x:hidden}#editNip71TagsFieldset>legend,#nip71TagsFieldset>legend{margin-left:0;padding:0}#app{background-color:var(--color-page);color:inherit}body.modal-open,html.modal-open{overflow:hidden;height:100%}body.modal-open #app{overflow:hidden}body.modal-open #sidebar,body.modal-open #sidebarOverlay{pointer-events:none;opacity:0;visibility:hidden}body.modal-open #sidebar{z-index:var(--z-sidebar-overlay)}body.modal-open #sidebarOverlay{z-index:calc(var(--z-sidebar-overlay) - 1)}.ng-cloak,.ng-hide:not(.ng-hide-animate),.x-ng-cloak,[data-ng-cloak],[ng-cloak],[ng\:cloak],[x-ng-cloak]{display:none!important}ng\:form{display:block}.ng-animate-shim{visibility:hidden}.ng-anchor{position:absolute}.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}#playerModal.ds-header{display:flex}.ds-header{display:flex;align-items:center;justify-content:flex-start;padding-top:var(--space-md);padding-bottom:var(--space-md)}.focus-ring{outline:2px solid transparent;outline-offset:2px}.focus-ring:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:var(--color-info);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--color-surface);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transitionLine truncated
|
||||
/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:var(--font-family-sans);font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--font-family-mono);font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{font-size:100%}@media (min-width:1024px){html{font-size:75%}}*,:after,:before{scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb) transparent}::-webkit-scrollbar{width:var(--scrollbar-width);height:var(--scrollbar-width)}::-webkit-scrollbar-track{background:var(--scrollbar-track)}::-webkit-scrollbar-thumb{background:var(--scrollbar-thumb);border-radius:var(--radius-sm)}::-webkit-scrollbar-thumb:hover{background:var(--scrollbar-thumb-hover)}body{font-family:var(--font-family-sans);background-color:var(--color-page);color:var(--color-text-strong);line-height:1.5;margin:0;padding:0;overflow-x:hidden}#editNip71TagsFieldset>legend,#nip71TagsFieldset>legend{margin-left:0;padding:0}#app{background-color:var(--color-page);color:inherit}body.modal-open,html.modal-open{overflow:hidden;height:100%}body.modal-open #app{overflow:hidden}body.modal-open #sidebar,body.modal-open #sidebarOverlay{pointer-events:none;opacity:0;visibility:hidden}body.modal-open #sidebar{z-index:var(--z-sidebar-overlay)}body.modal-open #sidebarOverlay{z-index:calc(var(--z-sidebar-overlay) - 1)}.ng-cloak,.ng-hide:not(.ng-hide-animate),.x-ng-cloak,[data-ng-cloak],[ng-cloak],[ng\:cloak],[x-ng-cloak]{display:none!important}ng\:form{display:block}.ng-animate-shim{visibility:hidden}.ng-anchor{position:absolute}.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}#playerModal.ds-header{display:flex}.ds-header{display:flex;align-items:center;justify-content:flex-start;padding-top:var(--space-md);padding-bottom:var(--space-md)}.focus-ring{outline:2px solid transparent;outline-offset:2px}.focus-ring:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:var(--color-info);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--color-surface);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transitionLine truncated
|
||||
+10
-5
@@ -160,6 +160,10 @@
|
||||
@apply font-mono;
|
||||
}
|
||||
|
||||
[data-floating-anchor="true"] {
|
||||
anchor-name: --floating-panel;
|
||||
}
|
||||
|
||||
[data-floating-panel] {
|
||||
--floating-transform-origin-inline: 50%;
|
||||
--floating-transform-origin-block: 50%;
|
||||
@@ -167,6 +171,10 @@
|
||||
--floating-fallback-left: auto;
|
||||
position: fixed;
|
||||
inset: auto;
|
||||
top: var(--floating-fallback-top);
|
||||
left: var(--floating-fallback-left);
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
transform-origin: var(--floating-transform-origin-inline)
|
||||
var(--floating-transform-origin-block);
|
||||
}
|
||||
@@ -175,11 +183,8 @@
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
[data-floating-panel] {
|
||||
top: var(--floating-fallback-top);
|
||||
left: var(--floating-fallback-left);
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
[data-floating-panel][data-floating-mode="anchor"] {
|
||||
position-anchor: --floating-panel;
|
||||
}
|
||||
|
||||
[data-floating-panel][data-floating-placement="top"] {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { accessControl } from "./accessControl.js";
|
||||
import { getApplication } from "./applicationContext.js";
|
||||
import { escapeHTML } from "./utils/domUtils.js";
|
||||
import positionFloatingPanel from "./ui/utils/positionFloatingPanel.js";
|
||||
import { createFloatingPanelStyles } from "./ui/utils/floatingPanelStyles.js";
|
||||
import { VideoCard } from "./ui/components/VideoCard.js";
|
||||
import { ALLOW_NSFW_CONTENT } from "./config.js";
|
||||
import {
|
||||
@@ -397,11 +398,13 @@ function openZapControls({ focus = false } = {}) {
|
||||
return false;
|
||||
}
|
||||
if (!zapControlsPositioner) {
|
||||
const styles = createFloatingPanelStyles(controls);
|
||||
zapControlsPositioner = positionFloatingPanel(zapButton, controls, {
|
||||
placement: "bottom",
|
||||
alignment: "end",
|
||||
offset: 8,
|
||||
viewportPadding: 16,
|
||||
styles,
|
||||
});
|
||||
}
|
||||
if (!zapControlsOpen) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { normalizeDesignSystemContext } from "../../designSystem.js";
|
||||
import positionFloatingPanel from "../utils/positionFloatingPanel.js";
|
||||
import { createFloatingPanelStyles } from "../utils/floatingPanelStyles.js";
|
||||
|
||||
export class VideoCard {
|
||||
constructor({
|
||||
@@ -661,6 +662,7 @@ export class VideoCard {
|
||||
|
||||
this.settingsButton = button;
|
||||
this.settingsDropdown = dropdown;
|
||||
this.settingsDropdownStyles = createFloatingPanelStyles(dropdown);
|
||||
this.settingsDropdownPositioner = positionFloatingPanel(
|
||||
button,
|
||||
dropdown,
|
||||
@@ -669,6 +671,7 @@ export class VideoCard {
|
||||
alignment: "end",
|
||||
offset: 8,
|
||||
viewportPadding: 16,
|
||||
styles: this.settingsDropdownStyles,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -873,11 +876,13 @@ export class VideoCard {
|
||||
|
||||
this.moreMenuButton = button;
|
||||
this.moreMenu = dropdown;
|
||||
this.moreMenuStyles = createFloatingPanelStyles(dropdown);
|
||||
this.moreMenuPositioner = positionFloatingPanel(button, dropdown, {
|
||||
placement: "bottom",
|
||||
alignment: "end",
|
||||
offset: 8,
|
||||
viewportPadding: 16,
|
||||
styles: this.moreMenuStyles,
|
||||
});
|
||||
|
||||
return wrapper;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createModalAccessibility } from "./modalAccessibility.js";
|
||||
import positionFloatingPanel from "../utils/positionFloatingPanel.js";
|
||||
import { createFloatingPanelStyles } from "../utils/floatingPanelStyles.js";
|
||||
import { applyDesignSystemAttributes } from "../../designSystem.js";
|
||||
|
||||
export class VideoModal {
|
||||
@@ -260,6 +261,7 @@ export class VideoModal {
|
||||
this.modalMoreBtn = playerModal.querySelector("#modalMoreBtn") || null;
|
||||
this.modalMoreMenu =
|
||||
playerModal.querySelector("#moreDropdown-modal") || null;
|
||||
this.modalMoreMenuStyles = null;
|
||||
|
||||
this.modalZapDialog = playerModal.querySelector("#modalZapDialog") || null;
|
||||
this.modalZapForm = playerModal.querySelector("#modalZapForm") || null;
|
||||
@@ -281,6 +283,7 @@ export class VideoModal {
|
||||
playerModal.querySelector("#modalZapWalletPrompt") || null;
|
||||
this.modalZapWalletLink =
|
||||
playerModal.querySelector("#modalZapWalletLink") || null;
|
||||
this.modalZapStyles = null;
|
||||
|
||||
if (this.modalZapDialog) {
|
||||
if (!this.modalZapDialog.dataset.state) {
|
||||
@@ -294,6 +297,7 @@ export class VideoModal {
|
||||
this.modalZapDialog.setAttribute("aria-hidden", "true");
|
||||
}
|
||||
if (this.modalZapBtn) {
|
||||
this.modalZapStyles = createFloatingPanelStyles(this.modalZapDialog);
|
||||
this.modalZapPositioner = positionFloatingPanel(
|
||||
this.modalZapBtn,
|
||||
this.modalZapDialog,
|
||||
@@ -302,6 +306,7 @@ export class VideoModal {
|
||||
alignment: "end",
|
||||
offset: 8,
|
||||
viewportPadding: 16,
|
||||
styles: this.modalZapStyles,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -319,6 +324,7 @@ export class VideoModal {
|
||||
this.modalMoreMenu.setAttribute("aria-hidden", "true");
|
||||
}
|
||||
if (this.modalMoreBtn) {
|
||||
this.modalMoreMenuStyles = createFloatingPanelStyles(this.modalMoreMenu);
|
||||
this.modalMoreMenuPositioner = positionFloatingPanel(
|
||||
this.modalMoreBtn,
|
||||
this.modalMoreMenu,
|
||||
@@ -327,6 +333,7 @@ export class VideoModal {
|
||||
alignment: "end",
|
||||
offset: 8,
|
||||
viewportPadding: 16,
|
||||
styles: this.modalMoreMenuStyles,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { normalizeDesignSystemContext } from "../designSystem.js";
|
||||
import positionFloatingPanel from "./utils/positionFloatingPanel.js";
|
||||
import { createFloatingPanelStyles } from "./utils/floatingPanelStyles.js";
|
||||
|
||||
export default class MoreMenuController {
|
||||
constructor(options = {}) {
|
||||
@@ -287,11 +288,13 @@ export default class MoreMenuController {
|
||||
}
|
||||
|
||||
if (dropdown && !this.dropdownPositioners.has(dropdown)) {
|
||||
const styles = createFloatingPanelStyles(dropdown);
|
||||
const positioner = positionFloatingPanel(button, dropdown, {
|
||||
placement: "bottom",
|
||||
alignment: "end",
|
||||
offset: 8,
|
||||
viewportPadding: 16,
|
||||
styles,
|
||||
});
|
||||
this.dropdownPositioners.set(dropdown, positioner);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
const DEFAULT_STATE = {
|
||||
placement: "bottom",
|
||||
alignment: "start",
|
||||
strategy: "fixed",
|
||||
direction: "ltr",
|
||||
mode: "fallback",
|
||||
fallbackTop: null,
|
||||
fallbackLeft: null,
|
||||
};
|
||||
|
||||
const FALLBACK_TOP_VAR = "--floating-fallback-top";
|
||||
const FALLBACK_LEFT_VAR = "--floating-fallback-left";
|
||||
|
||||
function toCssPixel(value) {
|
||||
if (Number.isFinite(value)) {
|
||||
return `${value}px`;
|
||||
}
|
||||
if (typeof value === "string" && value.trim() !== "") {
|
||||
return value;
|
||||
}
|
||||
return "auto";
|
||||
}
|
||||
|
||||
function normalizeDirection(value) {
|
||||
return value === "rtl" ? "rtl" : "ltr";
|
||||
}
|
||||
|
||||
function ensureDatasetValue(element, key, value) {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
if (value === undefined || value === null || value === "") {
|
||||
delete element.dataset[key];
|
||||
return;
|
||||
}
|
||||
element.dataset[key] = value;
|
||||
}
|
||||
|
||||
export function createFloatingPanelStyles(panel) {
|
||||
const safePanel = panel || null;
|
||||
const state = { ...DEFAULT_STATE };
|
||||
|
||||
if (safePanel) {
|
||||
safePanel.dataset.floatingPanel = "true";
|
||||
safePanel.style.setProperty(FALLBACK_TOP_VAR, "auto");
|
||||
safePanel.style.setProperty(FALLBACK_LEFT_VAR, "auto");
|
||||
}
|
||||
|
||||
const api = {
|
||||
element: safePanel,
|
||||
setPlacement(value) {
|
||||
state.placement = typeof value === "string" && value ? value : DEFAULT_STATE.placement;
|
||||
ensureDatasetValue(safePanel, "floatingPlacement", state.placement);
|
||||
return state.placement;
|
||||
},
|
||||
getPlacement() {
|
||||
return state.placement;
|
||||
},
|
||||
setAlignment(value) {
|
||||
state.alignment = typeof value === "string" && value ? value : DEFAULT_STATE.alignment;
|
||||
ensureDatasetValue(safePanel, "floatingAlignment", state.alignment);
|
||||
return state.alignment;
|
||||
},
|
||||
getAlignment() {
|
||||
return state.alignment;
|
||||
},
|
||||
setStrategy(value) {
|
||||
state.strategy = value === "absolute" ? "absolute" : DEFAULT_STATE.strategy;
|
||||
ensureDatasetValue(safePanel, "floatingStrategy", state.strategy);
|
||||
return state.strategy;
|
||||
},
|
||||
getStrategy() {
|
||||
return state.strategy;
|
||||
},
|
||||
setDirection(value) {
|
||||
state.direction = normalizeDirection(value);
|
||||
ensureDatasetValue(safePanel, "floatingDir", state.direction);
|
||||
return state.direction;
|
||||
},
|
||||
getDirection() {
|
||||
return state.direction;
|
||||
},
|
||||
setMode(value) {
|
||||
state.mode = value === "anchor" ? "anchor" : DEFAULT_STATE.mode;
|
||||
ensureDatasetValue(safePanel, "floatingMode", state.mode);
|
||||
return state.mode;
|
||||
},
|
||||
getMode() {
|
||||
return state.mode;
|
||||
},
|
||||
setFallbackPosition({ top, left }) {
|
||||
state.fallbackTop = Number.isFinite(top) ? Number(top) : null;
|
||||
state.fallbackLeft = Number.isFinite(left) ? Number(left) : null;
|
||||
if (safePanel) {
|
||||
safePanel.style.setProperty(FALLBACK_TOP_VAR, toCssPixel(state.fallbackTop));
|
||||
safePanel.style.setProperty(FALLBACK_LEFT_VAR, toCssPixel(state.fallbackLeft));
|
||||
}
|
||||
return { ...api.getFallbackPosition() };
|
||||
},
|
||||
getFallbackPosition() {
|
||||
return {
|
||||
top: state.fallbackTop,
|
||||
left: state.fallbackLeft,
|
||||
};
|
||||
},
|
||||
teardown() {
|
||||
if (!safePanel) {
|
||||
return;
|
||||
}
|
||||
safePanel.style.removeProperty(FALLBACK_TOP_VAR);
|
||||
safePanel.style.removeProperty(FALLBACK_LEFT_VAR);
|
||||
safePanel.style.removeProperty("right");
|
||||
safePanel.style.removeProperty("bottom");
|
||||
safePanel.style.removeProperty("position");
|
||||
delete safePanel.dataset.floatingPanel;
|
||||
delete safePanel.dataset.floatingPlacement;
|
||||
delete safePanel.dataset.floatingAlignment;
|
||||
delete safePanel.dataset.floatingStrategy;
|
||||
delete safePanel.dataset.floatingDir;
|
||||
delete safePanel.dataset.floatingMode;
|
||||
state.placement = DEFAULT_STATE.placement;
|
||||
state.alignment = DEFAULT_STATE.alignment;
|
||||
state.strategy = DEFAULT_STATE.strategy;
|
||||
state.direction = DEFAULT_STATE.direction;
|
||||
state.mode = DEFAULT_STATE.mode;
|
||||
state.fallbackTop = DEFAULT_STATE.fallbackTop;
|
||||
state.fallbackLeft = DEFAULT_STATE.fallbackLeft;
|
||||
},
|
||||
};
|
||||
|
||||
api.setPlacement(state.placement);
|
||||
api.setAlignment(state.alignment);
|
||||
api.setStrategy(state.strategy);
|
||||
api.setDirection(state.direction);
|
||||
api.setMode(state.mode);
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
export default createFloatingPanelStyles;
|
||||
@@ -1,3 +1,5 @@
|
||||
import { createFloatingPanelStyles } from "./floatingPanelStyles.js";
|
||||
|
||||
const DEFAULT_OPTIONS = {
|
||||
placement: "bottom",
|
||||
alignment: "start",
|
||||
@@ -9,20 +11,6 @@ const DEFAULT_OPTIONS = {
|
||||
onUpdate: null,
|
||||
};
|
||||
|
||||
const PANEL_DATA_KEYS = [
|
||||
"floatingPanel",
|
||||
"floatingPlacement",
|
||||
"floatingAlignment",
|
||||
"floatingStrategy",
|
||||
"floatingDir",
|
||||
"floatingMode",
|
||||
];
|
||||
|
||||
const PANEL_STYLE_PROPS = [
|
||||
"--floating-fallback-top",
|
||||
"--floating-fallback-left",
|
||||
];
|
||||
|
||||
const OPPOSITE_PLACEMENT = {
|
||||
top: "bottom",
|
||||
bottom: "top",
|
||||
@@ -213,9 +201,10 @@ export function positionFloatingPanel(anchor, panel, options = {}) {
|
||||
const documentRef = safeAnchor.ownerDocument || safePanel.ownerDocument || globalThis.document;
|
||||
const windowRef = documentRef?.defaultView || globalThis;
|
||||
|
||||
const { styles: providedStyles, ...optionOverrides } = options || {};
|
||||
const config = {
|
||||
...DEFAULT_OPTIONS,
|
||||
...options,
|
||||
...optionOverrides,
|
||||
};
|
||||
config.placement = normalizePlacement(config.placement);
|
||||
config.alignment = normalizeAlignment(config.alignment);
|
||||
@@ -235,20 +224,21 @@ export function positionFloatingPanel(anchor, panel, options = {}) {
|
||||
cleanup: [],
|
||||
};
|
||||
|
||||
const styles =
|
||||
providedStyles && providedStyles.element === safePanel
|
||||
? providedStyles
|
||||
: createFloatingPanelStyles(safePanel);
|
||||
|
||||
safeAnchor.dataset.floatingAnchor = "true";
|
||||
safePanel.dataset.floatingPanel = "true";
|
||||
safePanel.dataset.floatingAlignment = config.alignment;
|
||||
safePanel.dataset.floatingStrategy = config.strategy;
|
||||
safePanel.dataset.floatingDir = rtl ? "rtl" : "ltr";
|
||||
safePanel.dataset.floatingMode = anchorSupported ? "anchor" : "fallback";
|
||||
styles.setAlignment(config.alignment);
|
||||
styles.setStrategy(config.strategy);
|
||||
styles.setDirection(rtl ? "rtl" : "ltr");
|
||||
styles.setMode(anchorSupported ? "anchor" : "fallback");
|
||||
styles.setPlacement(config.placement);
|
||||
|
||||
const applyPosition = ({ top, left, placement }) => {
|
||||
safePanel.dataset.floatingPlacement = placement;
|
||||
safePanel.style.position = config.strategy;
|
||||
safePanel.style.removeProperty("right");
|
||||
safePanel.style.removeProperty("bottom");
|
||||
safePanel.style.setProperty("--floating-fallback-top", `${top}px`);
|
||||
safePanel.style.setProperty("--floating-fallback-left", `${left}px`);
|
||||
styles.setPlacement(placement);
|
||||
styles.setFallbackPosition({ top, left });
|
||||
if (typeof config.onUpdate === "function") {
|
||||
config.onUpdate({ top, left, placement, alignment: config.alignment, rtl });
|
||||
}
|
||||
@@ -395,18 +385,10 @@ export function positionFloatingPanel(anchor, panel, options = {}) {
|
||||
// Ignore cleanup errors to avoid breaking consumer teardown flows.
|
||||
}
|
||||
}
|
||||
PANEL_STYLE_PROPS.forEach((prop) => {
|
||||
safePanel.style.removeProperty(prop);
|
||||
});
|
||||
safePanel.style.removeProperty("right");
|
||||
safePanel.style.removeProperty("bottom");
|
||||
safePanel.style.removeProperty("position");
|
||||
safePanel.style.position = "";
|
||||
PANEL_DATA_KEYS.forEach((key) => {
|
||||
delete safePanel.dataset[key];
|
||||
});
|
||||
styles.teardown();
|
||||
delete safeAnchor.dataset.floatingAnchor;
|
||||
},
|
||||
styles,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ const VIOLATION_ALLOWLIST = new Map([
|
||||
"torrent/dist/beacon.vendor.js",
|
||||
new Set(["Direct .style usage", "style.cssText usage"]),
|
||||
],
|
||||
["js/ui/utils/positionFloatingPanel.js", new Set(["Direct .style usage"])],
|
||||
["js/ui/utils/floatingPanelStyles.js", new Set(["Direct .style usage"])],
|
||||
]);
|
||||
|
||||
const TEXT_EXTENSIONS = new Set([
|
||||
|
||||
@@ -2,14 +2,7 @@ import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { JSDOM } from "jsdom";
|
||||
import positionFloatingPanel from "../js/ui/utils/positionFloatingPanel.js";
|
||||
|
||||
function getInlineStyleValue(element, property) {
|
||||
const styleAttr = element.getAttribute("style") || "";
|
||||
const escapedProperty = property.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
||||
const pattern = new RegExp(`(?:^|;)\\s*${escapedProperty}\\s*:\\s*([^;]+)`, "i");
|
||||
const match = styleAttr.match(pattern);
|
||||
return match ? match[1].trim() : "";
|
||||
}
|
||||
import { createFloatingPanelStyles } from "../js/ui/utils/floatingPanelStyles.js";
|
||||
|
||||
test("flips to top when bottom placement collides with viewport", () => {
|
||||
const dom = new JSDOM(
|
||||
@@ -52,9 +45,12 @@ test("flips to top when bottom placement collides with viewport", () => {
|
||||
offsetHeight: { value: 120 },
|
||||
});
|
||||
|
||||
const styles = createFloatingPanelStyles(panel);
|
||||
|
||||
const positioner = positionFloatingPanel(trigger, panel, {
|
||||
offset: 8,
|
||||
viewportPadding: 12,
|
||||
styles,
|
||||
});
|
||||
|
||||
panel.hidden = false;
|
||||
@@ -62,9 +58,9 @@ test("flips to top when bottom placement collides with viewport", () => {
|
||||
positioner.update();
|
||||
|
||||
assert.equal(panel.dataset.floatingPlacement, "top");
|
||||
assert.equal(getInlineStyleValue(panel, "position"), "fixed");
|
||||
assert.equal(getInlineStyleValue(panel, "--floating-fallback-top"), "132px");
|
||||
assert.equal(getInlineStyleValue(panel, "--floating-fallback-left"), "100px");
|
||||
assert.equal(styles.getStrategy(), "fixed");
|
||||
assert.deepEqual(styles.getFallbackPosition(), { top: 132, left: 100 });
|
||||
assert.equal(panel.dataset.floatingMode, "fallback");
|
||||
|
||||
positioner.destroy();
|
||||
assert.equal(panel.dataset.floatingPanel, undefined);
|
||||
@@ -114,9 +110,12 @@ test("respects RTL alignment and clamps within the viewport", () => {
|
||||
offsetHeight: { value: 80 },
|
||||
});
|
||||
|
||||
const styles = createFloatingPanelStyles(panel);
|
||||
|
||||
const positioner = positionFloatingPanel(trigger, panel, {
|
||||
alignment: "start",
|
||||
viewportPadding: 16,
|
||||
styles,
|
||||
});
|
||||
|
||||
panel.hidden = false;
|
||||
@@ -126,7 +125,7 @@ test("respects RTL alignment and clamps within the viewport", () => {
|
||||
// In RTL mode, start alignment hugs the right edge of the trigger.
|
||||
assert.equal(panel.dataset.floatingPlacement, "bottom");
|
||||
assert.equal(panel.dataset.floatingDir, "rtl");
|
||||
assert.equal(getInlineStyleValue(panel, "--floating-fallback-left"), "140px");
|
||||
assert.deepEqual(styles.getFallbackPosition(), { top: 72 + 8, left: 140 });
|
||||
|
||||
positioner.destroy();
|
||||
});
|
||||
@@ -182,21 +181,88 @@ test("updates when scroll containers move the trigger", async () => {
|
||||
return originalAddEventListener.call(window, type, listener, options);
|
||||
};
|
||||
|
||||
const styles = createFloatingPanelStyles(panel);
|
||||
|
||||
const positioner = positionFloatingPanel(trigger, panel, {
|
||||
offset: 12,
|
||||
styles,
|
||||
});
|
||||
|
||||
panel.hidden = false;
|
||||
panel.dataset.state = "open";
|
||||
positioner.update();
|
||||
|
||||
assert.equal(getInlineStyleValue(panel, "--floating-fallback-top"), "152px");
|
||||
assert.deepEqual(styles.getFallbackPosition(), { top: 152, left: 40 });
|
||||
assert.ok(scrollHandler, "scroll listener should be registered");
|
||||
|
||||
anchorTop = 60;
|
||||
scrollHandler?.({ type: "scroll" });
|
||||
assert.equal(getInlineStyleValue(panel, "--floating-fallback-top"), "112px");
|
||||
assert.deepEqual(styles.getFallbackPosition(), { top: 112, left: 40 });
|
||||
|
||||
positioner.destroy();
|
||||
window.addEventListener = originalAddEventListener;
|
||||
});
|
||||
|
||||
test("prefers anchor positioning when supported", () => {
|
||||
const dom = new JSDOM(
|
||||
`
|
||||
<div class="popover">
|
||||
<button id="trigger">Open</button>
|
||||
<div id="panel" class="popover__panel" data-state="closed" hidden></div>
|
||||
</div>
|
||||
`,
|
||||
{ pretendToBeVisual: true },
|
||||
);
|
||||
const { window } = dom;
|
||||
const { document } = window;
|
||||
|
||||
window.innerWidth = 640;
|
||||
window.innerHeight = 480;
|
||||
|
||||
const trigger = document.getElementById("trigger");
|
||||
const panel = document.getElementById("panel");
|
||||
|
||||
trigger.getBoundingClientRect = () => ({
|
||||
top: 100,
|
||||
bottom: 140,
|
||||
left: 200,
|
||||
right: 240,
|
||||
width: 40,
|
||||
height: 40,
|
||||
});
|
||||
|
||||
panel.getBoundingClientRect = () => ({
|
||||
top: 0,
|
||||
bottom: 90,
|
||||
left: 0,
|
||||
right: 200,
|
||||
width: 200,
|
||||
height: 90,
|
||||
});
|
||||
Object.defineProperties(panel, {
|
||||
offsetWidth: { value: 200 },
|
||||
offsetHeight: { value: 90 },
|
||||
});
|
||||
|
||||
const originalCSS = window.CSS;
|
||||
window.CSS = {
|
||||
supports: (value) =>
|
||||
value === "anchor-name: --floating-panel" || value === "position-anchor: --floating-panel",
|
||||
};
|
||||
|
||||
const positioner = positionFloatingPanel(trigger, panel, {
|
||||
offset: 10,
|
||||
});
|
||||
|
||||
panel.hidden = false;
|
||||
panel.dataset.state = "open";
|
||||
positioner.update();
|
||||
|
||||
assert.equal(panel.dataset.floatingMode, "anchor");
|
||||
assert.equal(positioner.styles.getMode(), "anchor");
|
||||
assert.equal(panel.dataset.floatingPlacement, "bottom");
|
||||
assert.deepEqual(positioner.styles.getFallbackPosition(), { top: 150, left: 200 });
|
||||
|
||||
positioner.destroy();
|
||||
window.CSS = originalCSS;
|
||||
});
|
||||
Reference in new issue
Block a user