mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat: add data style attributes to popover (#62)
This commit is contained in:
@@ -8,6 +8,7 @@ export class MediaPopoverRoot extends HTMLElement {
|
||||
#open = false;
|
||||
#hoverTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
#cleanup: (() => void) | null = null;
|
||||
#transitionStatus: 'initial' | 'open' | 'close' | 'unmounted' = 'initial';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -22,6 +23,9 @@ export class MediaPopoverRoot extends HTMLElement {
|
||||
disconnectedCallback(): void {
|
||||
this.#clearHoverTimeout();
|
||||
this.#cleanup?.();
|
||||
|
||||
this.#transitionStatus = 'unmounted';
|
||||
this.#updateVisibility();
|
||||
}
|
||||
|
||||
static get observedAttributes(): string[] {
|
||||
@@ -60,6 +64,17 @@ export class MediaPopoverRoot extends HTMLElement {
|
||||
if (this.#open === open) return;
|
||||
|
||||
this.#open = open;
|
||||
|
||||
if (open) {
|
||||
this.#transitionStatus = 'initial';
|
||||
requestAnimationFrame(() => {
|
||||
this.#transitionStatus = 'open';
|
||||
this.#updateVisibility();
|
||||
});
|
||||
} else {
|
||||
this.#transitionStatus = 'close';
|
||||
}
|
||||
|
||||
this.#updateVisibility();
|
||||
|
||||
if (open) {
|
||||
@@ -71,11 +86,22 @@ export class MediaPopoverRoot extends HTMLElement {
|
||||
}
|
||||
|
||||
#updateVisibility(): void {
|
||||
this.toggleAttribute('data-open', this.#open);
|
||||
this.style.display = 'contents';
|
||||
|
||||
if (this.#popupElement) {
|
||||
this.#popupElement.style.display = this.#open ? 'block' : 'none';
|
||||
const placement = this.#positionerElement?.side ?? 'top';
|
||||
this.#popupElement.setAttribute('data-side', placement);
|
||||
|
||||
this.#popupElement.toggleAttribute('data-starting-style', this.#transitionStatus === 'initial');
|
||||
this.#popupElement.toggleAttribute('data-open', this.#transitionStatus === 'initial' || this.#transitionStatus === 'open');
|
||||
this.#popupElement.toggleAttribute('data-ending-style', this.#transitionStatus === 'close' || this.#transitionStatus === 'unmounted');
|
||||
this.#popupElement.toggleAttribute('data-closed', this.#transitionStatus === 'close' || this.#transitionStatus === 'unmounted');
|
||||
}
|
||||
|
||||
const triggerElement = this.#triggerElement?.firstElementChild as HTMLElement;
|
||||
if (triggerElement) {
|
||||
triggerElement.setAttribute('aria-expanded', this.#open.toString());
|
||||
triggerElement.toggleAttribute('data-popup-open', this.#open);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +166,43 @@ export class MediaPopoverTrigger extends HTMLElement {
|
||||
if (triggerElement) {
|
||||
triggerElement.setAttribute('aria-haspopup', 'true');
|
||||
triggerElement.setAttribute('aria-expanded', 'false');
|
||||
|
||||
const mutationObserver = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'attributes') {
|
||||
const rootElement = this.closest('media-popover-root') as MediaPopoverRoot;
|
||||
let popupElement = rootElement.querySelector('media-popover-popup') as MediaPopoverPopup;
|
||||
|
||||
if (!popupElement) {
|
||||
const portalElement = rootElement.querySelector('media-popover-portal') as MediaPopoverPortal;
|
||||
if (!portalElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
popupElement = portalElement.querySelector('media-popover-popup') as MediaPopoverPopup;
|
||||
if (!popupElement) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const attributeName = mutation.attributeName;
|
||||
if (!attributeName || !attributeName.startsWith('data-')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attributeValue = triggerElement.getAttribute(attributeName);
|
||||
if (attributeValue !== null) {
|
||||
popupElement.setAttribute(attributeName, attributeValue);
|
||||
} else {
|
||||
popupElement.removeAttribute(attributeName);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
mutationObserver.observe(triggerElement, {
|
||||
attributes: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,6 +181,19 @@ export function getTemplateHTML() {
|
||||
media-popover-popup {
|
||||
background: rgb(20 20 30 / .7);
|
||||
padding: 14px 0;
|
||||
--transition: .15s ease-in-out;
|
||||
transition: transform var(--transition), scale var(--transition), opacity var(--transition);
|
||||
}
|
||||
|
||||
media-popover-popup[data-starting-style] {
|
||||
transition-duration: 0s;
|
||||
transform: scale(0.9) translateY(8px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
media-popover-popup[data-ending-style] {
|
||||
transform: scale(0.9) translateY(8px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* VolumeSlider Component Styles */
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
useHover,
|
||||
useInteractions,
|
||||
useRole,
|
||||
useTransitionStatus,
|
||||
} from '@floating-ui/react';
|
||||
|
||||
import {
|
||||
@@ -36,6 +37,7 @@ interface PopoverContextType {
|
||||
getFloatingProps: ReturnType<typeof useInteractions>['getFloatingProps'];
|
||||
context: ReturnType<typeof useFloating>['context'];
|
||||
updatePositioning: (placement: Placement, sideOffset: number) => void;
|
||||
transitionStatus: ReturnType<typeof useTransitionStatus>['status'];
|
||||
}
|
||||
|
||||
interface PopoverRootProps {
|
||||
@@ -89,6 +91,8 @@ function PopoverRoot({ openOnHover = false, delay = 0, closeDelay = 0, children
|
||||
whileElementsMounted: autoUpdate,
|
||||
});
|
||||
|
||||
const { status: transitionStatus } = useTransitionStatus(context);
|
||||
|
||||
const hover = useHover(context, {
|
||||
enabled: openOnHover,
|
||||
delay: {
|
||||
@@ -117,33 +121,31 @@ function PopoverRoot({ openOnHover = false, delay = 0, closeDelay = 0, children
|
||||
getFloatingProps,
|
||||
context,
|
||||
updatePositioning,
|
||||
}), [open, refs, floatingStyles, getReferenceProps, getFloatingProps, context, updatePositioning]);
|
||||
transitionStatus,
|
||||
}), [open, refs, floatingStyles, getReferenceProps, getFloatingProps, context, updatePositioning, transitionStatus]);
|
||||
|
||||
return <PopoverContext.Provider value={value}>{children}</PopoverContext.Provider>;
|
||||
}
|
||||
|
||||
function PopoverTrigger({ children }: PopoverTriggerProps): JSX.Element {
|
||||
const { refs, getReferenceProps } = usePopoverContext();
|
||||
const { refs, getReferenceProps, open } = usePopoverContext();
|
||||
|
||||
// eslint-disable-next-line react/no-clone-element, react/no-children-only
|
||||
return cloneElement(Children.only(children) as JSX.Element, {
|
||||
ref: refs.setReference,
|
||||
...getReferenceProps(),
|
||||
'data-popup-open': open ? '' : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function PopoverPositioner({ side = 'top', sideOffset = 5, children }: PopoverPositionerProps): JSX.Element | null {
|
||||
const { open, refs, floatingStyles, updatePositioning } = usePopoverContext();
|
||||
const { refs, floatingStyles, updatePositioning } = usePopoverContext();
|
||||
|
||||
// Update positioning when props change
|
||||
useEffect(() => {
|
||||
updatePositioning(side, sideOffset);
|
||||
}, [side, sideOffset, updatePositioning]);
|
||||
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={refs.setFloating} style={floatingStyles}>
|
||||
{children}
|
||||
@@ -152,16 +154,36 @@ function PopoverPositioner({ side = 'top', sideOffset = 5, children }: PopoverPo
|
||||
}
|
||||
|
||||
function PopoverPopup({ className, children }: PopoverPopupProps): JSX.Element {
|
||||
const { getFloatingProps } = usePopoverContext();
|
||||
const { getFloatingProps, context, transitionStatus } = usePopoverContext();
|
||||
const { refs, placement } = context;
|
||||
const triggerElement = refs.reference.current as HTMLElement | null;
|
||||
|
||||
// Copy data attributes from trigger element
|
||||
const dataAttributes = triggerElement?.attributes
|
||||
? Object.fromEntries(
|
||||
Array.from(triggerElement.attributes)
|
||||
.filter(attr => attr.name.startsWith('data-'))
|
||||
.map(attr => [attr.name, attr.value]),
|
||||
)
|
||||
: {};
|
||||
|
||||
return (
|
||||
<div className={className} {...getFloatingProps()}>
|
||||
<div
|
||||
className={className}
|
||||
{...getFloatingProps()}
|
||||
{...dataAttributes}
|
||||
data-side={placement}
|
||||
data-starting-style={transitionStatus === 'initial' ? '' : undefined}
|
||||
data-open={transitionStatus === 'initial' || transitionStatus === 'open' ? '' : undefined}
|
||||
data-ending-style={transitionStatus === 'close' || transitionStatus === 'unmounted' ? '' : undefined}
|
||||
data-closed={transitionStatus === 'close' || transitionStatus === 'unmounted' ? '' : undefined}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PopoverPortal({ children, root, rootId }: PopoverPortalProps): JSX.Element {
|
||||
function PopoverPortal({ children, root, rootId = '@default_portal_id' }: PopoverPortalProps): JSX.Element {
|
||||
return (
|
||||
<FloatingPortal root={root as HTMLElement} id={rootId as string}>
|
||||
{children}
|
||||
|
||||
@@ -157,6 +157,8 @@ const styles: FrostedSkinStyles = {
|
||||
'relative px-1.5 py-3.5 rounded-full',
|
||||
'bg-white/10 backdrop-blur-3xl backdrop-saturate-150 backdrop-brightness-90',
|
||||
'ring ring-white/10 ring-inset shadow-sm shadow-black/15',
|
||||
// Animation
|
||||
'transition-[transform,scale,opacity] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[instant]:duration-0 data-[starting-style]:scale-90 data-[starting-style]:opacity-0',
|
||||
// Border to enhance contrast on lighter videos
|
||||
'after:absolute after:inset-0 after:ring after:rounded-[inherit] after:ring-black/15 after:pointer-events-none after:z-10',
|
||||
// Reduced transparency for users with preference
|
||||
|
||||
Reference in New Issue
Block a user