+
diff --git a/src/modules/all.less b/src/modules/all.less
index bc75ab234..996f7c23c 100644
--- a/src/modules/all.less
+++ b/src/modules/all.less
@@ -5,6 +5,7 @@
@import './esl-media/core.less';
@import './esl-scrollbar/core.less';
+@import './esl-drag-to-scroll/core.less';
@import './esl-a11y-group/core.less';
@import './esl-toggleable/core.less';
diff --git a/src/modules/all.ts b/src/modules/all.ts
index 4c764ffa5..85be1b728 100644
--- a/src/modules/all.ts
+++ b/src/modules/all.ts
@@ -31,6 +31,7 @@ export * from './esl-open-state/core';
// Scrollbar
export * from './esl-scrollbar/core';
+export * from './esl-drag-to-scroll/core';
// Alert
export * from './esl-alert/core';
diff --git a/src/modules/esl-drag-to-scroll/README.md b/src/modules/esl-drag-to-scroll/README.md
new file mode 100644
index 000000000..37dfad5b3
--- /dev/null
+++ b/src/modules/esl-drag-to-scroll/README.md
@@ -0,0 +1,54 @@
+# [ESL](../../../) Drag to Scroll
+
+Version: *1.0.0*
+
+Authors: *Anna Barmina*, *Alexey Stsefanovich (ala'n)*
+
+
+
+## ESL Drag To Scroll Mixin
+
+`ESLDragToScrollMixin` (`esl-drag-to-scroll`) is a custom attribute that enables drag-to-scroll functionality for any scrollable container element.
+This mixin enhances user experience by allowing intuitive drag-based scrolling, making it easier to navigate through content.
+
+### Configuration
+The mixin uses a primary attribute, `esl-drag-to-scroll`, with optional configuration passed as a JSON attribute value.
+
+**Configuration options:**
+- `axis` (string) - determines the scrolling axis to control. Possible values:
+ - `'both'` - both horizontal and vertical scrolling (by default);
+ - `'x'` - horizontal scrolling only;
+ - `'y'` - vertical scrolling only.
+- `cls` (string) - class to apply to the element during dragging to indicate the drag state.
+ By default, the class `dragging` is applied.
+- `tolerance` (number) - a minimum distance to move before the drag action starts.
+ By default, the value is 10.
+- `selection` (boolean) - Determines whether the text should prevent the drag-scroll action. The default value is true.
+
+**Default Configuration"**
+The default configuration for the mixin is as follows:
+```json
+{
+ "axis": "both",
+ "cls": "dragging",
+ "tolerance": 10,
+ "selection": true
+}
+```
+
+### Usage
+
+To use the mixin, apply the `esl-drag-to-scroll` attribute to your scrollable container element.
+```html
+
+
+
+```
+
+You can also provide **custom configuration** through the JSON attribute.
+For example, to enable horizontal scrolling only, disable text selection, and use a custom class `is-dragging` during the drag.
+```html
+
+
+
+```
diff --git a/src/modules/esl-drag-to-scroll/core.less b/src/modules/esl-drag-to-scroll/core.less
new file mode 100644
index 000000000..a8204a6a4
--- /dev/null
+++ b/src/modules/esl-drag-to-scroll/core.less
@@ -0,0 +1 @@
+@import './core/esl-drag-to-scroll.less';
diff --git a/src/modules/esl-drag-to-scroll/core.ts b/src/modules/esl-drag-to-scroll/core.ts
new file mode 100644
index 000000000..4011cbe7e
--- /dev/null
+++ b/src/modules/esl-drag-to-scroll/core.ts
@@ -0,0 +1 @@
+export * from './core/esl-drag-to-scroll.mixin';
diff --git a/src/modules/esl-drag-to-scroll/core/esl-drag-to-scroll.less b/src/modules/esl-drag-to-scroll/core/esl-drag-to-scroll.less
new file mode 100644
index 000000000..5aa4381af
--- /dev/null
+++ b/src/modules/esl-drag-to-scroll/core/esl-drag-to-scroll.less
@@ -0,0 +1,9 @@
+[esl-drag-to-scroll] {
+ cursor: grab;
+
+ &.dragging {
+ cursor: grabbing;
+ user-select: none;
+ scroll-behavior: auto !important;
+ }
+}
diff --git a/src/modules/esl-drag-to-scroll/core/esl-drag-to-scroll.mixin.ts b/src/modules/esl-drag-to-scroll/core/esl-drag-to-scroll.mixin.ts
new file mode 100644
index 000000000..6f13254f2
--- /dev/null
+++ b/src/modules/esl-drag-to-scroll/core/esl-drag-to-scroll.mixin.ts
@@ -0,0 +1,144 @@
+import {ESLMixinElement} from '../../esl-mixin-element/core';
+import {listen, memoize} from '../../esl-utils/decorators';
+import {ExportNs} from '../../esl-utils/environment/export-ns';
+
+import type {Point} from '../../esl-utils/dom/point';
+import {evaluate} from '../../esl-utils/misc/format';
+
+/**
+ * ESLDragToScrollConfig - configuration options for the ESLDragToScrollMixin
+ */
+export interface ESLDragToScrollConfig {
+ /** Determines the scrolling axis. Options are 'x', 'y', or 'both' (default) */
+ axis: 'x' | 'y' | 'both';
+ /** Class name to apply during dragging. Defaults to 'dragging' */
+ cls: string;
+ /** Min distance in pixels to activate dragging mode. Defaults to 10 */
+ tolerance: number;
+ /** Prevent dragging if text is selected or not. Defaults to true */
+ selection: boolean;
+}
+
+/**
+ * ESLDragToScrollMixin - mixin to enable drag-to-scroll functionality for any scrollable container element
+ * @author Anna Barmina, Alexey Stsefanovich (ala'n)
+ *
+ * Use example:
+ * ```
+ *
+ *
+ *
+ * ```
+ */
+@ExportNs('DragToScrollMixin')
+export class ESLDragToScrollMixin extends ESLMixinElement {
+ public static override is = 'esl-drag-to-scroll';
+
+ /** Default configuration object */
+ public static DEFAULT_CONFIG: ESLDragToScrollConfig = {
+ axis: 'both',
+ cls: 'dragging',
+ tolerance: 10,
+ selection: true
+ };
+
+ /** Initial pointer event when dragging starts */
+ private startEvent: PointerEvent;
+
+ private startScrollLeft: number = 0;
+ private startScrollTop: number = 0;
+
+ private _isDragging: boolean = false;
+
+ /** Flag indicating whether dragging is in progress */
+ public get isDragging(): boolean {
+ return this._isDragging;
+ }
+ private set isDragging(value: boolean) {
+ this._isDragging = value;
+ this.$$cls(this.config.cls, value);
+ }
+
+ /**
+ * Mixin configuration (merged with default)
+ * @see ESLDragToScrollConfig
+ */
+ @memoize()
+ public get config(): ESLDragToScrollConfig {
+ const config = evaluate(this.$$attr(ESLDragToScrollMixin.is)!, {});
+ return {...ESLDragToScrollMixin.DEFAULT_CONFIG, ...config};
+ }
+ public set config(value: ESLDragToScrollConfig | string) {
+ const serialized = typeof value === 'string' ? value : JSON.stringify(value);
+ this.$$attr(ESLDragToScrollMixin.is, serialized);
+ }
+
+ /** Flag indicating whether text is selected */
+ public get hasSelection(): boolean {
+ const selection = document.getSelection();
+ if (!selection || !this.config.selection) return false;
+ // Prevents draggable state if the text is selected
+ return !selection.isCollapsed && this.$host.contains(selection.anchorNode);
+ }
+
+ protected override attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
+ memoize.clear(this, 'config');
+ }
+
+ /** @returns the offset of the pointer event relative to the start event */
+ public getEventOffset(event: PointerEvent): Point {
+ if (!this.startEvent || event.type === 'pointercancel') return {x: 0, y: 0};
+ const x = this.startEvent.clientX - event.clientX;
+ const y = this.startEvent.clientY - event.clientY;
+ return {x, y};
+ }
+
+ /** Scrolls the host element by the specified offset */
+ public scrollBy(offset: Point): void {
+ if (this.config.axis === 'x' || this.config.axis === 'both') {
+ this.$host.scrollLeft = this.startScrollLeft + offset.x;
+ }
+
+ if (this.config.axis === 'y' || this.config.axis === 'both') {
+ this.$host.scrollTop = this.startScrollTop + offset.y;
+ }
+ }
+
+ /** Handles the pointerdown event to start dragging */
+ @listen('pointerdown')
+ private onPointerDown(event: PointerEvent): void {
+ this.startEvent = event;
+ this.startScrollLeft = this.$host.scrollLeft;
+ this.startScrollTop = this.$host.scrollTop;
+
+ this.$$on({group: 'pointer'});
+ }
+
+ /** Handles the pointermove event to perform scrolling while dragging */
+ @listen({auto: false, event: 'pointermove', group: 'pointer'})
+ private onPointerMove(event: PointerEvent): void {
+ const offset = this.getEventOffset(event);
+
+ if (!this.isDragging) {
+ // Stop tracking if there was a selection before dragging started
+ if (this.hasSelection) return this.onPointerUp(event);
+ // Does not start dragging mode if offset have not reached tolerance
+ if (Math.abs(Math.max(Math.abs(offset.x), Math.abs(offset.y))) < this.config.tolerance) return;
+ this.isDragging = true;
+ }
+
+ this.$host.setPointerCapture(event.pointerId);
+ this.scrollBy(offset);
+ }
+
+ /** Handles the pointerup and pointercancel events to stop dragging */
+ @listen({auto: false, event: 'pointerup pointercancel', group: 'pointer'})
+ private onPointerUp(event: PointerEvent): void {
+ this.$$off({group: 'pointer'});
+ if (this.$host.hasPointerCapture(event.pointerId)) {
+ this.$host.releasePointerCapture(event.pointerId);
+ }
+ this.scrollBy(this.getEventOffset(event));
+ this.isDragging = false;
+ }
+}