diff --git a/changelog.md b/changelog.md index e099294..2d86131 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2025-01-16 - 1.4.0 - feat(toggle component) +Enhanced consent toggle component with drag functionality + +- Implemented drag functionality for the toggle knob. +- Added smooth transitions for the toggle knob movement. +- Handled drag state management to differentiate between actual click and drag. +- Improved user interaction by allowing click anywhere in the toggle area. + ## 2025-01-16 - 1.3.5 - fix(elements) Improved styling consistency across several components diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index df89732..ea3b311 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@consent.software/catalog', - version: '1.3.5', + version: '1.4.0', description: 'A library of web components designed to integrate robust consent management capabilities into web applications, ensuring compliance with privacy regulations.' } diff --git a/ts_web/elements/consentsoftware-toggle.ts b/ts_web/elements/consentsoftware-toggle.ts index 9c57248..4b45a83 100644 --- a/ts_web/elements/consentsoftware-toggle.ts +++ b/ts_web/elements/consentsoftware-toggle.ts @@ -1,19 +1,32 @@ import { LitElement, html, css, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators.js'; import { property } from 'lit/decorators/property.js'; - import { delayFor } from '@push.rocks/smartdelay'; @customElement('consentsoftware-toggle') export class ConsentsoftwareToggle extends LitElement { - public static demo = () => html``; - @property({ type: Boolean }) public required = false; @property({ type: Boolean, reflect: true }) public selected = false; + /** + * We always track the knob’s left offset in `currentX`. + * - 0 => fully left + * - 30 => fully right + */ + private currentX = 0; + + /** + * Drag state + */ + private isDragging = false; + private hasDragged = false; + private startX = 0; // pointerdown offset + private readonly knobWidth = 30; + private readonly trackWidth = 60; + public static styles = css` :host { display: block; @@ -24,17 +37,21 @@ export class ConsentsoftwareToggle extends LitElement { margin-bottom: 16px; } + .toggle { + user-select: none; /* helps avoid text selection on drag */ + } + .toggleKnobArea { - cursor: pointer; + position: relative; margin: auto; height: 30px; width: 60px; border-radius: 20px; background: rgba(255, 255, 255, 0.1); - position: relative; - overflow: hidden; - transition: all 0.2s; border: 1px solid rgba(255, 255, 255, 0); + overflow: hidden; + transition: background 0.2s ease; + cursor: pointer; } :host([selected]) .toggleKnobArea { @@ -42,19 +59,27 @@ export class ConsentsoftwareToggle extends LitElement { } .toggleKnobMover { - transition: all 0.2s; + position: relative; + height: 100%; + width: 100%; } .toggleKnobInner { - height: 30px; - width: 30px; - border-radius: 15px; - background: rgba(255, 255, 255, 0.5); position: absolute; top: 0; left: 0; - transition: all 0.2s; + width: 30px; + height: 30px; + border-radius: 15px; + background: rgba(255, 255, 255, 0.5); + transition: left 0.2s ease, background 0.2s ease; transform: scale(0.7); + /* Prevent scroll gestures on mobile */ + touch-action: none; + } + + .toggleKnobInner.dragging { + transition: background 0.2s ease; } :host([selected]) .toggleKnobInner { @@ -78,50 +103,151 @@ export class ConsentsoftwareToggle extends LitElement { public render(): TemplateResult { return html`
${this.getText()}
+
-
+ +
`; } - public async firstUpdated(_changedProperties: Map) { - super.firstUpdated(_changedProperties); + /** + * If required = true on first render, auto-select and set the knob to the right. + */ + public async firstUpdated() { if (this.required) { + // If "required" => always selected this.selected = true; - this.syncSelection(); + this.currentX = this.knobWidth; // 30 + this.requestUpdate(); + } else { + // If not required, set knob to 0 or 30 depending on `selected` + this.currentX = this.selected ? this.knobWidth : 0; + this.requestUpdate(); } } - public async handleClick(mouseEvent) { - if (this.required) { - const moverElement: HTMLDivElement = this.shadowRoot.querySelector('.toggleKnobMover'); - moverElement.style.transform = 'translateX(20px)'; - await delayFor(250); - moverElement.style.transform = 'translateX(30px)'; + /** + * CLICK HANDLER + */ + public async handleClick(event: MouseEvent) { + // If the user truly dragged the knob, skip the normal click toggle. + if (this.isDragging || this.hasDragged) { + event.stopPropagation(); + event.preventDefault(); return; } + + if (this.required) { + // small bounce from 30 -> 20 -> 30 + this.currentX = this.knobWidth; // ensure at 30 + this.requestUpdate(); + await new Promise((r) => setTimeout(r, 10)); + + this.currentX = 20; // small bounce left + this.requestUpdate(); + await delayFor(200); + + this.currentX = this.knobWidth; // back to 30 + this.requestUpdate(); + return; + } + + // Normal toggle if no drag & not required + event.stopPropagation(); + event.preventDefault(); + this.selected = !this.selected; - mouseEvent.stopPropagation(); - mouseEvent.preventDefault(); - this.syncSelection(); + this.currentX = this.selected ? this.knobWidth : 0; // snap knob left(0) or right(30) + this.requestUpdate(); + + this.dispatchEvent(new CustomEvent('toggle', { detail: { selected: this.selected } })); + delayFor(0).then(() => { + this.hasDragged = false; + }); + } + + /** + * DRAG HANDLERS (pointer events) + */ + private onPointerDown(event: PointerEvent) { + if (this.required) { + // If "required", disallow manual dragging + return; + } + + // Start dragging + this.isDragging = true; + this.hasDragged = false; + // The difference between the pointer’s X and the knob’s current position + this.startX = event.clientX - this.currentX; + + // capture pointer so we keep receiving pointermove/pointerup + (event.target as HTMLElement).setPointerCapture(event.pointerId); + } + + private onPointerMove(event: PointerEvent) { + if (!this.isDragging) return; + const newX = event.clientX - this.startX; + this.hasDragged = true; + const toggleKnobInner: HTMLDivElement = this.shadowRoot.querySelector('.toggleKnobInner'); + toggleKnobInner.classList.add('dragging'); + + // Clamp + this.currentX = Math.max(0, Math.min(newX, this.trackWidth - this.knobWidth)); + this.requestUpdate(); + } + + private onPointerUp(event: PointerEvent) { + if (!this.isDragging) return; + (event.target as HTMLElement).releasePointerCapture(event.pointerId); + this.isDragging = false; + + // If we didn’t truly drag, pointerup does nothing; click handler handles toggling. + if (!this.hasDragged) { + return; + } + const toggleKnobInner: HTMLDivElement = this.shadowRoot.querySelector('.toggleKnobInner'); + toggleKnobInner.classList.remove('dragging'); + + // Real drag => decide final side + const midpoint = (this.trackWidth - this.knobWidth) / 2; // 15 + this.selected = this.currentX > midpoint; + this.currentX = this.selected ? this.knobWidth : 0; // snap to edge + this.requestUpdate(); + + // Dispatch toggle event this.dispatchEvent(new CustomEvent('toggle', { detail: { selected: this.selected } })); } - public async syncSelection() { - console.log(`Selected ${this.selected}`); - const moverElement: HTMLDivElement = this.shadowRoot.querySelector('.toggleKnobMover'); - if (this.selected) { - moverElement.style.transform = 'translateX(30px)'; - } else { - moverElement.style.transform = 'translateX(0px)'; + /** + * If external code sets `selected = true/false`, we also sync the knob position + */ + protected updated(changedProperties: Map): void { + if ( + changedProperties.has('selected') && + !this.isDragging && + !this.hasDragged && + !this.required + ) { + this.currentX = this.selected ? this.knobWidth : 0; + this.requestUpdate(); } + super.updated(changedProperties); } - public getText() { + public getText(): string | null { return this.textContent; } }