feat(combox): Introduce singleton SioCombox attached to document.body with open/close/toggle API and animated show/hide; integrate SioFab to use the singleton and update styles/positioning

This commit is contained in:
2025-12-17 11:41:47 +00:00
parent 55e8e192c9
commit 053d0c8e8f
4 changed files with 121 additions and 95 deletions

View File

@@ -36,6 +36,27 @@ declare global {
export class SioCombox extends DeesElement {
public static demo = () => html` <sio-combox></sio-combox> `;
// Singleton instance
private static instance: SioCombox | null = null;
/**
* Creates and appends a singleton combox to document.body
*/
public static createOnBody(): SioCombox {
if (!SioCombox.instance) {
SioCombox.instance = new SioCombox();
document.body.appendChild(SioCombox.instance);
}
return SioCombox.instance;
}
/**
* Gets the singleton instance if it exists
*/
public static getInstance(): SioCombox | null {
return SioCombox.instance;
}
@property({ type: Object })
public accessor referenceObject: HTMLElement;
@@ -45,6 +66,9 @@ export class SioCombox extends DeesElement {
@state()
private accessor isKeyboardVisible: boolean = false;
@state()
private accessor isOpen: boolean = false;
private keyboardBlurTimeout?: number;
@state()
@@ -174,6 +198,48 @@ export class SioCombox extends DeesElement {
this.removeAttribute('keyboard-visible');
}
}
if (changedProperties.has('isOpen')) {
if (this.isOpen) {
this.classList.add('open');
this.dispatchEvent(new CustomEvent('opened', { bubbles: true, composed: true }));
} else {
this.classList.remove('open');
this.dispatchEvent(new CustomEvent('closed', { bubbles: true, composed: true }));
}
}
}
/**
* Opens the combox
*/
public open() {
this.isOpen = true;
}
/**
* Closes the combox
*/
public close() {
this.isOpen = false;
this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }));
}
/**
* Toggles the combox open/closed state
*/
public toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
/**
* Returns whether the combox is currently open
*/
public getIsOpen(): boolean {
return this.isOpen;
}
public static styles = [
@@ -181,6 +247,9 @@ export class SioCombox extends DeesElement {
css`
:host {
display: block;
position: fixed;
bottom: 100px;
right: 20px;
height: 600px;
width: 800px;
background: ${bdTheme('background')};
@@ -189,25 +258,22 @@ export class SioCombox extends DeesElement {
box-shadow: ${unsafeCSS(shadows.xl)};
overflow: hidden;
font-family: ${unsafeCSS(fontFamilies.sans)};
position: relative;
transform-origin: bottom right;
z-index: 10001;
/* Hidden by default */
opacity: 0;
pointer-events: none;
transform: scale(0.95) translateY(10px);
transition: opacity 200ms ease, transform 200ms ease;
}
:host(.animate-in) {
animation: scaleIn 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
:host(.open) {
opacity: 1;
pointer-events: all;
transform: scale(1) translateY(0);
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.9) translateY(10px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
:host::before {
content: '';
position: absolute;
@@ -243,9 +309,19 @@ export class SioCombox extends DeesElement {
// Mobile responsive layout - full screen with sliding mechanics
cssManager.cssForPhablet(css`
:host {
width: 100%;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100vw;
height: 100vh;
height: 100dvh;
border-radius: 0;
transform-origin: center center;
}
:host(.open) {
transform: scale(1) translateY(0);
}
:host::before {