import { css, html } from "lit";
import { BaseElement } from "../../core/base-element.ts";
/**
* @component ct-resizable-handle
* @description Drag handle component for resizing panels within a resizable panel group
*
* @tag ct-resizable-handle
*
* @attribute {boolean} with-handle - Whether to show the visual grip indicator. Defaults to true.
*
* @event {CustomEvent} ct-handle-adjust - Fired when handle is adjusted via keyboard
* @event-detail {Object} detail - Event detail object
* @event-detail {number} detail.delta - The adjustment amount (-100 to 100)
*
* @csspart handle - The handle container element
* @csspart grip - The visual grip indicator (when with-handle is true)
*
* @example
* ```html
*
*
*
* Panel 1
*
*
*
* Panel 2
*
*
*
*
*
* ```
*
* @accessibility
* - Uses role="separator" for screen reader support
* - Keyboard navigable with arrow keys
* - Arrow keys adjust size incrementally based on panel group direction
* - Home/End keys jump to minimum/maximum sizes
* - Provides aria-valuenow, aria-valuemin, aria-valuemax
*
* @keyboard
* - ArrowLeft/ArrowRight - Adjust horizontal panels
* - ArrowUp/ArrowDown - Adjust vertical panels
* - Home - Set to minimum size
* - End - Set to maximum size
*
* @note Must be used between ct-resizable-panel elements within a ct-resizable-panel-group
*/
export class CTResizableHandle extends BaseElement {
static override properties = {
withHandle: { type: Boolean, attribute: "with-handle" },
};
declare withHandle: boolean;
static override styles = css`
:host {
display: block;
position: relative;
background: var(--border, hsl(0, 0%, 89%));
transition: background-color 150ms ease;
}
:host(:hover) {
background: var(--border-hover, hsl(0, 0%, 78%));
}
:host(:focus-visible) {
outline: 2px solid var(--ring, hsl(212, 100%, 47%));
outline-offset: -1px;
}
.handle {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
/* Grip icon styles */
.grip-icon {
position: relative;
opacity: 0.5;
transition: opacity 150ms ease;
}
:host(:hover) .grip-icon {
opacity: 0.8;
}
/* Horizontal grip */
:host([data-orientation="horizontal"]) .grip-icon {
width: 2px;
height: 16px;
background: currentColor;
}
:host([data-orientation="horizontal"]) .grip-icon::before,
:host([data-orientation="horizontal"]) .grip-icon::after {
content: "";
position: absolute;
width: 2px;
height: 16px;
background: currentColor;
}
:host([data-orientation="horizontal"]) .grip-icon::before {
left: -3px;
}
:host([data-orientation="horizontal"]) .grip-icon::after {
right: -3px;
}
/* Vertical grip */
:host([data-orientation="vertical"]) .grip-icon {
width: 16px;
height: 2px;
background: currentColor;
}
:host([data-orientation="vertical"]) .grip-icon::before,
:host([data-orientation="vertical"]) .grip-icon::after {
content: "";
position: absolute;
width: 16px;
height: 2px;
background: currentColor;
}
:host([data-orientation="vertical"]) .grip-icon::before {
top: -3px;
}
:host([data-orientation="vertical"]) .grip-icon::after {
bottom: -3px;
}
/* Active/dragging state */
:host(.dragging) {
background: var(--ring, hsl(212, 100%, 47%));
opacity: 0.8;
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
:host {
background: var(--border, hsl(0, 0%, 20%));
}
:host(:hover) {
background: var(--border-hover, hsl(0, 0%, 25%));
}
}
`;
constructor() {
super();
this.withHandle = true;
}
override connectedCallback() {
super.connectedCallback();
// Set ARIA attributes
this.setAttribute("role", "separator");
this.setAttribute("aria-valuenow", "50");
this.setAttribute("aria-valuemin", "0");
this.setAttribute("aria-valuemax", "100");
this.setAttribute("tabindex", "0");
// Add keyboard support
this.addEventListener("keydown", this.handleKeyDown);
}
override disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener("keydown", this.handleKeyDown);
}
override render() {
return html`
${this.withHandle
? html`
`
: ""}
`;
}
private handleKeyDown = (e: KeyboardEvent): void => {
const panelGroup = this.closest("ct-resizable-panel-group");
if (!panelGroup) return;
const direction = panelGroup.getAttribute("direction") || "horizontal";
const isHorizontal = direction === "horizontal";
let handled = false;
switch (e.key) {
case "ArrowLeft":
if (isHorizontal) {
this.adjustSize(-1);
handled = true;
}
break;
case "ArrowRight":
if (isHorizontal) {
this.adjustSize(1);
handled = true;
}
break;
case "ArrowUp":
if (!isHorizontal) {
this.adjustSize(-1);
handled = true;
}
break;
case "ArrowDown":
if (!isHorizontal) {
this.adjustSize(1);
handled = true;
}
break;
case "Home":
this.adjustSize(-100);
handled = true;
break;
case "End":
this.adjustSize(100);
handled = true;
break;
}
if (handled) {
e.preventDefault();
e.stopPropagation();
}
};
private adjustSize(delta: number): void {
// Emit a custom event that the panel group can listen to
this.emit("ct-handle-adjust", { delta });
}
}
globalThis.customElements.define("ct-resizable-handle", CTResizableHandle);