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;
}
}