Repository: exadel-inc/esl
Pull request #2878 (2025-01-22 11:28:53)

feat(esl-media): control mixin
An external control mixin element for `esl-media` was created in the scope of this pull request.
Looks for target media using `ESLTraversingQuery`

Supported actions:
- Play
- Pause
- Toggle
- Stop

Observes and reflects media state through active class

Closes: #2876 

codeclimate[bot]: Code Climate has analyzed commit dd220e44 and detected **0 issues** on this pull request.

The test coverage on the diff in this pull request is **46.1%** (**50%** is the threshold).

This pull request will bring the total coverage in the repository to **63.3%** (**-0.1%** change).

View more on [Code Climate](https://codeclimate.com/github/exadel-inc/esl/pull/2878).

fshovchko: > Please update https://esl-ui.com/examples/embedded-media/ page buttons to use new control as well

Should documentation updates be included in current PR or opened is a separate one?
ala-n: > Should documentation updates be included in current PR or opened in a separate one?

Documentation could be done separately + there is a point to discuss how it should look like (probably a subpage for esl-media)

My request here is mostly regarding examples (to simplify check for reviewers in particular)
codeclimate[bot]: Code Climate has analyzed commit be4dd467 and detected **0 issues** on this pull request.

The test coverage on the diff in this pull request is **46.1%** (**50%** is the threshold).

This pull request will bring the total coverage in the repository to **63.3%** (**-0.1%** change).

View more on [Code Climate](https://codeclimate.com/github/exadel-inc/esl/pull/2878).

codeclimate[bot]: Code Climate has analyzed commit ade884df and detected **0 issues** on this pull request.

The test coverage on the diff in this pull request is **46.1%** (**50%** is the threshold).

This pull request will bring the total coverage in the repository to **63.3%** (**-0.1%** change).

View more on [Code Climate](https://codeclimate.com/github/exadel-inc/esl/pull/2878).

codeclimate[bot]: Code Climate has analyzed commit 46be55b8 and detected **0 issues** on this pull request.

The test coverage on the diff in this pull request is **48.1%** (**50%** is the threshold).

This pull request will bring the total coverage in the repository to **63.3%** (**-0.1%** change).

View more on [Code Climate](https://codeclimate.com/github/exadel-inc/esl/pull/2878).

codeclimate[bot]: Code Climate has analyzed commit 7e1a7fd4 and detected **0 issues** on this pull request.

The test coverage on the diff in this pull request is **42.3%** (**50%** is the threshold).

This pull request will bring the total coverage in the repository to **63.3%** (**-0.1%** change).

View more on [Code Climate](https://codeclimate.com/github/exadel-inc/esl/pull/2878).

codeclimate[bot]: Code Climate has analyzed commit 0a6c174d and detected **0 issues** on this pull request.

The test coverage on the diff in this pull request is **42.3%** (**50%** is the threshold).

This pull request will bring the total coverage in the repository to **63.3%** (**-0.1%** change).

View more on [Code Climate](https://codeclimate.com/github/exadel-inc/esl/pull/2878).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 054af0c64..5e10f84c2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,14 @@
+## [5.0.1](https://github.com/exadel-inc/esl/compare/v5.0.0...v5.0.1) (2025-02-05)
+
+### Bug Fixes
+
+* **esl-carousel:** ability to provide slide size fallback trough `--esl-slide-fallback-size` variable ([78ae62f](https://github.com/exadel-inc/esl/commit/78ae62f01ce651a24aa165115382ff13d96a0b84)), closes [#2879](https://github.com/exadel-inc/esl/issues/2879)
+* **esl-carousel:** fix initial rendering layout shift to eliminate CLS degradation ([7d65335](https://github.com/exadel-inc/esl/commit/7d6533548545744502191b761773e6be9c5081eb)), closes [#2879](https://github.com/exadel-inc/esl/issues/2879)
+* **esl-image-utils:** omit console warnings in case of missing inner image ([ca13a8a](https://github.com/exadel-inc/esl/commit/ca13a8a467b769240c7b5e9fd740c01bff75ef4e))
+* **esl-media:** fix play preconditions for toggle method ([#2906](https://github.com/exadel-inc/esl/issues/2906)) ([1f7b8dc](https://github.com/exadel-inc/esl/commit/1f7b8dc82a4dfc156fb810a8c289afe0d1001f69))
+* **esl-media:** postpone BrightcoveProvider ready state notification until metadata is available ([e3d4159](https://github.com/exadel-inc/esl/commit/e3d41590a93169168fe699e60fab1e7df6e56d1b))
+* **eslint-plugin:** remove peer dependency for esl (handled via warnings) ([c06fbde](https://github.com/exadel-inc/esl/commit/c06fbdee050ff5608eda8d3e8bb8c4fba000b981))
+
 ## [5.0.1-beta.2](https://github.com/exadel-inc/esl/compare/v5.0.1-beta.1...v5.0.1-beta.2) (2025-01-24)
 
 ### Bug Fixes
diff --git a/eslint-config/package.json b/eslint-config/package.json
index 39f37a125..84c8e2bb4 100644
--- a/eslint-config/package.json
+++ b/eslint-config/package.json
@@ -1,7 +1,7 @@
 {
   "name": "@exadel/eslint-config-esl",
   "private": false,
-  "version": "5.0.0",
+  "version": "5.0.1",
   "keywords": [
     "eslint",
     "eslint-config",
diff --git a/eslint-plugin/package.json b/eslint-plugin/package.json
index cdd7bfb61..afb7afe50 100644
--- a/eslint-plugin/package.json
+++ b/eslint-plugin/package.json
@@ -1,7 +1,7 @@
 {
   "name": "@exadel/eslint-plugin-esl",
   "private": false,
-  "version": "5.0.0",
+  "version": "5.0.1",
   "keywords": [
     "eslint",
     "eslint-plugin",
diff --git a/package-lock.json b/package-lock.json
index f3e1164b1..c59160845 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "@exadel/esl",
-  "version": "5.0.0",
+  "version": "5.0.1",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "@exadel/esl",
-      "version": "5.0.0",
+      "version": "5.0.1",
       "license": "MIT",
       "workspaces": [
         "e2e",
@@ -56,7 +56,7 @@
     },
     "e2e": {
       "name": "@exadel/esl-snapshot-tests",
-      "version": "5.0.0",
+      "version": "5.0.1",
       "license": "MIT",
       "dependencies": {
         "@types/jest-environment-puppeteer": "^5.0.6",
@@ -64,7 +64,7 @@
         "@types/puppeteer": "^7.0.4",
         "jest-puppeteer": "^11.0.0",
         "pixelmatch": "^5.3.0",
-        "puppeteer": "^24.1.1",
+        "puppeteer": "^24.2.0",
         "rimraf": "^6.0.1",
         "sharp": "^0.33.5",
         "stucumber": "^0.19.0"
@@ -75,7 +75,7 @@
     },
     "eslint-config": {
       "name": "@exadel/eslint-config-esl",
-      "version": "5.0.0",
+      "version": "5.0.1",
       "license": "MIT",
       "dependencies": {
         "@eslint/js": "^9.19.0",
@@ -257,7 +257,7 @@
     },
     "eslint-plugin": {
       "name": "@exadel/eslint-plugin-esl",
-      "version": "5.0.0",
+      "version": "5.0.1",
       "license": "MIT",
       "dependencies": {
         "kleur": "^4.1.5",
@@ -3332,18 +3332,17 @@
       "license": "MIT"
     },
     "node_modules/@puppeteer/browsers": {
-      "version": "2.7.0",
-      "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.7.0.tgz",
-      "integrity": "sha512-bO61XnTuopsz9kvtfqhVbH6LTM1koxK0IlBR+yuVrM2LB7mk8+5o1w18l5zqd5cs8xlf+ntgambqRqGifMDjog==",
+      "version": "2.7.1",
+      "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.7.1.tgz",
+      "integrity": "sha512-MK7rtm8JjaxPN7Mf1JdZIZKPD2Z+W7osvrC1vjpvfOX1K0awDIHYbNi89f7eotp7eMUn2shWnt03HwVbriXtKQ==",
       "license": "Apache-2.0",
       "dependencies": {
         "debug": "^4.4.0",
         "extract-zip": "^2.0.1",
         "progress": "^2.0.3",
         "proxy-agent": "^6.5.0",
-        "semver": "^7.6.3",
-        "tar-fs": "^3.0.6",
-        "unbzip2-stream": "^1.4.3",
+        "semver": "^7.7.0",
+        "tar-fs": "^3.0.8",
         "yargs": "^17.7.2"
       },
       "bin": {
@@ -5526,9 +5525,9 @@
       }
     },
     "node_modules/bare-stream": {
-      "version": "2.6.4",
-      "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.4.tgz",
-      "integrity": "sha512-G6i3A74FjNq4nVrrSTUz5h3vgXzBJnjmWAVlBWaZETkgu+LgKd7AiyOml3EDJY1AHlIbBHKDXE+TUT53Ff8OaA==",
+      "version": "2.6.5",
+      "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz",
+      "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==",
       "license": "Apache-2.0",
       "optional": true,
       "dependencies": {
@@ -5551,6 +5550,7 @@
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
       "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+      "dev": true,
       "funding": [
         {
           "type": "github",
@@ -6185,13 +6185,13 @@
       }
     },
     "node_modules/chromium-bidi": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-1.1.0.tgz",
-      "integrity": "sha512-HislCEczCuamWm3+55Lig9XKmMF13K+BGKum9rwtDAzgUAHT4h5jNwhDmD4U20VoVUG8ujnv9UZ89qiIf5uF8w==",
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-1.2.0.tgz",
+      "integrity": "sha512-XtdJ1GSN6S3l7tO7F77GhNsw0K367p0IsLYf2yZawCVAKKC3lUvDhPdMVrB2FNhmhfW43QGYbEX3Wg6q0maGwQ==",
       "license": "Apache-2.0",
       "dependencies": {
-        "mitt": "3.0.1",
-        "zod": "3.24.1"
+        "mitt": "^3.0.1",
+        "zod": "^3.24.1"
       },
       "peerDependencies": {
         "devtools-protocol": "*"
@@ -7780,9 +7780,9 @@
       }
     },
     "node_modules/devtools-protocol": {
-      "version": "0.0.1380148",
-      "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1380148.tgz",
-      "integrity": "sha512-1CJABgqLxbYxVI+uJY/UDUHJtJ0KZTSjNYJYKqd9FRoXT33WDakDHNxRapMEgzeJ/C3rcs01+avshMnPmKQbvA==",
+      "version": "0.0.1402036",
+      "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1402036.tgz",
+      "integrity": "sha512-JwAYQgEvm3yD45CHB+RmF5kMbWtXBaOGwuxa87sZogHcLCv8c/IqnThaoQ1y60d7pXWjSKWQphPEc+1rAScVdg==",
       "license": "BSD-3-Clause"
     },
     "node_modules/diff-sequences": {
@@ -11004,6 +11004,7 @@
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
       "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+      "dev": true,
       "funding": [
         {
           "type": "github",
@@ -19568,17 +19569,17 @@
       }
     },
     "node_modules/puppeteer": {
-      "version": "24.1.1",
-      "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.1.1.tgz",
-      "integrity": "sha512-fuhceZ5HZuDXVuaMIRxUuDHfCJLmK0pXh8FlzVQ0/+OApStevxZhU5kAVeYFOEqeCF5OoAyZjcWbdQK27xW/9A==",
+      "version": "24.2.0",
+      "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.2.0.tgz",
+      "integrity": "sha512-z8vv7zPEgrilIbOo3WNvM+2mXMnyM9f4z6zdrB88Fzeuo43Oupmjrzk3EpuvuCtyK0A7Lsllfx7Z+4BvEEGJcQ==",
       "hasInstallScript": true,
       "license": "Apache-2.0",
       "dependencies": {
-        "@puppeteer/browsers": "2.7.0",
-        "chromium-bidi": "1.1.0",
+        "@puppeteer/browsers": "2.7.1",
+        "chromium-bidi": "1.2.0",
         "cosmiconfig": "^9.0.0",
-        "devtools-protocol": "0.0.1380148",
-        "puppeteer-core": "24.1.1",
+        "devtools-protocol": "0.0.1402036",
+        "puppeteer-core": "24.2.0",
         "typed-query-selector": "^2.12.0"
       },
       "bin": {
@@ -19589,15 +19590,15 @@
       }
     },
     "node_modules/puppeteer-core": {
-      "version": "24.1.1",
-      "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.1.1.tgz",
-      "integrity": "sha512-7FF3gq6bpIsbq3I8mfbodXh3DCzXagoz3l2eGv1cXooYU4g0P4mcHQVHuBD4iSZPXNg8WjzlP5kmRwK9UvwF0A==",
+      "version": "24.2.0",
+      "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.2.0.tgz",
+      "integrity": "sha512-e4A4/xqWdd4kcE6QVHYhJ+Qlx/+XpgjP4d8OwBx0DJoY/nkIRhSgYmKQnv7+XSs1ofBstalt+XPGrkaz4FoXOQ==",
       "license": "Apache-2.0",
       "dependencies": {
-        "@puppeteer/browsers": "2.7.0",
-        "chromium-bidi": "1.1.0",
+        "@puppeteer/browsers": "2.7.1",
+        "chromium-bidi": "1.2.0",
         "debug": "^4.4.0",
-        "devtools-protocol": "0.0.1380148",
+        "devtools-protocol": "0.0.1402036",
         "typed-query-selector": "^2.12.0",
         "ws": "^8.18.0"
       },
@@ -19658,12 +19659,6 @@
       ],
       "license": "MIT"
     },
-    "node_modules/queue-tick": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
-      "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
-      "license": "MIT"
-    },
     "node_modules/randombytes": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -21472,13 +21467,12 @@
       }
     },
     "node_modules/streamx": {
-      "version": "2.21.1",
-      "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.21.1.tgz",
-      "integrity": "sha512-PhP9wUnFLa+91CPy3N6tiQsK+gnYyUNuk15S3YG/zjYE7RuPeCjJngqnzpC31ow0lzBHQ+QGO4cNJnd0djYUsw==",
+      "version": "2.22.0",
+      "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz",
+      "integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==",
       "license": "MIT",
       "dependencies": {
         "fast-fifo": "^1.3.2",
-        "queue-tick": "^1.0.1",
         "text-decoder": "^1.1.0"
       },
       "optionalDependencies": {
@@ -22480,6 +22474,7 @@
       "version": "2.3.8",
       "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
       "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/through2": {
@@ -22945,40 +22940,6 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/unbzip2-stream": {
-      "version": "1.4.3",
-      "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
-      "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==",
-      "license": "MIT",
-      "dependencies": {
-        "buffer": "^5.2.1",
-        "through": "^2.3.8"
-      }
-    },
-    "node_modules/unbzip2-stream/node_modules/buffer": {
-      "version": "5.7.1",
-      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
-      "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ],
-      "license": "MIT",
-      "dependencies": {
-        "base64-js": "^1.3.1",
-        "ieee754": "^1.1.13"
-      }
-    },
     "node_modules/undici-types": {
       "version": "6.20.0",
       "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
@@ -24004,7 +23965,7 @@
     },
     "site": {
       "name": "@exadel/esl-website",
-      "version": "5.0.0",
+      "version": "5.0.1",
       "license": "MIT",
       "dependencies": {
         "@11ty/eleventy": "^2.0.1",
diff --git a/package.json b/package.json
index 5833be69f..b90b1330a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@exadel/esl",
-  "version": "5.0.0",
+  "version": "5.0.1",
   "private": false,
   "description": "Exadel Smart Library (ESL) is the lightweight custom elements library that provide a set of super-flexible components",
   "license": "MIT",
diff --git a/site/package.json b/site/package.json
index a3104f9b6..50ed00a28 100644
--- a/site/package.json
+++ b/site/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@exadel/esl-website",
-  "version": "5.0.0",
+  "version": "5.0.1",
   "private": true,
   "description": "ESL Website source",
   "homepage": "https://esl-ui.com/",
diff --git a/site/src/site.ts b/site/src/site.ts
index 096da8857..0f854662e 100644
--- a/site/src/site.ts
+++ b/site/src/site.ts
@@ -12,6 +12,7 @@ import {
   ESLVSizeCSSProxy,
   ESLImageContainerMixin,
   ESLMedia,
+  ESLMediaControlMixin,
   ESLToggleable,
   ESLPopup,
   ESLPopupPlaceholder,
@@ -88,6 +89,8 @@ ESLRandomText.register('lorem-ipsum');
 ESLImageContainerMixin.register();
 ESLMedia.register();
 
+ESLMediaControlMixin.register();
+
 ESLToggleableDispatcher.init();
 ESLToggleable.register();
 ESLPopup.register();
diff --git a/site/views/examples/embedded-media.njk b/site/views/examples/embedded-media.njk
index 4cd20584b..17007790e 100644
--- a/site/views/examples/embedded-media.njk
+++ b/site/views/examples/embedded-media.njk
@@ -22,11 +22,11 @@ aside:
     </div>
 
     <div class="btn-group btn-group-centered">
-      <button class="btn btn-sec-blue" onclick="this.closest('uip-root').querySelector('.esl-media').play()">Play
+      <button class="btn btn-sec-blue" esl-media-contol="{target: '::closest(uip-root)::find(.esl-media)', action: 'play'}">Play
       </button>
-      <button class="btn btn-sec-blue" onclick="this.closest('uip-root').querySelector('.esl-media').pause()">Pause
+      <button class="btn btn-sec-blue" esl-media-contol="{target: '::closest(uip-root)::find(.esl-media)', action: 'pause'}">Pause
       </button>
-      <button class="btn btn-sec-blue" onclick="this.closest('uip-root').querySelector('.esl-media').stop()">Stop
+      <button class="btn btn-sec-blue" esl-media-contol="{target: '::closest(uip-root)::find(.esl-media)', action: 'stop'}">Stop
       </button>
     </div>
   </script>
@@ -45,11 +45,11 @@ aside:
     </esl-media>
 
     <div class="btn-group btn-group-centered">
-      <button class="btn btn-sec-blue" onclick="this.closest('uip-root').querySelector('.esl-media').play()">Play
+      <button class="btn btn-sec-blue" esl-media-contol="{target: '::closest(uip-root)::find(.esl-media)', action: 'play'}">Play
       </button>
-      <button class="btn btn-sec-blue" onclick="this.closest('uip-root').querySelector('.esl-media').pause()">Pause
+      <button class="btn btn-sec-blue" esl-media-contol="{target: '::closest(uip-root)::find(.esl-media)', action: 'pause'}">Pause
       </button>
-      <button class="btn btn-sec-blue" onclick="this.closest('uip-root').querySelector('.esl-media').stop()">Stop
+      <button class="btn btn-sec-blue" esl-media-contol="{target: '::closest(uip-root)::find(.esl-media)', action: 'stop'}">Stop
       </button>
     </div>
   </script>
@@ -72,11 +72,11 @@ aside:
     </div>
 
     <div class="btn-group btn-group-centered">
-      <button class="btn btn-sec-blue" onclick="this.closest('uip-root').querySelector('.esl-media').play()">Play
+      <button class="btn btn-sec-blue" esl-media-contol="{target: '::closest(uip-root)::find(.esl-media)', action: 'play'}">Play
       </button>
-      <button class="btn btn-sec-blue" onclick="this.closest('uip-root').querySelector('.esl-media').pause()">Pause
+      <button class="btn btn-sec-blue" esl-media-contol="{target: '::closest(uip-root)::find(.esl-media)', action: 'pause'}">Pause
       </button>
-      <button class="btn btn-sec-blue" onclick="this.closest('uip-root').querySelector('.esl-media').stop()">Stop
+      <button class="btn btn-sec-blue" esl-media-contol="{target: '::closest(uip-root)::find(.esl-media)', action: 'stop'}">Stop
       </button>
     </div>
   </script>
@@ -102,11 +102,11 @@ aside:
     </div>
 
     <div class="btn-group btn-group-centered">
-      <button class="btn btn-sec-blue" onclick="this.closest('uip-root').querySelector('.esl-media').play()">Play
+      <button class="btn btn-sec-blue" esl-media-contol="{target: '::closest(uip-root)::find(.esl-media)', action: 'play'}">Play
       </button>
-      <button class="btn btn-sec-blue" onclick="this.closest('uip-root').querySelector('.esl-media').pause()">Pause
+      <button class="btn btn-sec-blue" esl-media-contol="{target: '::closest(uip-root)::find(.esl-media)', action: 'pause'}">Pause
       </button>
-      <button class="btn btn-sec-blue" onclick="this.closest('uip-root').querySelector('.esl-media').stop()">Stop
+      <button class="btn btn-sec-blue" esl-media-contol="{target: '::closest(uip-root)::find(.esl-media)', action: 'stop'}">Stop
       </button>
     </div>
 
@@ -140,9 +140,9 @@ aside:
     </div>
 
     <div class="btn-group btn-group-centered">
-      <button class="btn btn-sec-blue" onclick="this.closest('uip-root').querySelector('.esl-media').play()">Play
+      <button class="btn btn-sec-blue" esl-media-contol="{target: '::closest(uip-root)::find(.esl-media)', action: 'play'}">Play
       </button>
-      <button class="btn btn-sec-blue" onclick="this.closest('uip-root').querySelector('.esl-media').stop()">Stop
+      <button class="btn btn-sec-blue" esl-media-contol="{target: '::closest(uip-root)::find(.esl-media)', action: 'stop'}">Stop
       </button>
     </div>
   </script>
diff --git a/src/modules/esl-media/control/esl-media-control-mixin.ts b/src/modules/esl-media/control/esl-media-control-mixin.ts
new file mode 100644
index 000000000..bf16f2951
--- /dev/null
+++ b/src/modules/esl-media/control/esl-media-control-mixin.ts
@@ -0,0 +1,66 @@
+import {ESLMixinElement} from '../../esl-mixin-element/core';
+import {ESLTraversingQuery} from '../../esl-traversing-query/core/esl-traversing-query';
+import {listen, memoize} from '../../esl-utils/decorators';
+import {ExportNs} from '../../esl-utils/environment/export-ns';
+import {evaluate} from '../../esl-utils/misc/format';
+
+import type {ESLMedia} from '../core/esl-media';
+
+export interface ESLMediaControlConfig {
+  target?: string;
+  action?: 'play' | 'pause' | 'stop' | 'toggle';
+  activeCls?: string;
+}
+
+@ExportNs('MediaControlMixin')
+export class ESLMediaControlMixin extends ESLMixinElement {
+  public static override is = 'esl-media-contol';
+
+  public static readonly DEFAULT_CONFIG: ESLMediaControlConfig = {
+    action: 'toggle'
+  };
+
+  @memoize()
+  public get config(): ESLMediaControlConfig {
+    const attrVal = this.$$attr(ESLMediaControlMixin.is) || '{}';
+    const userConfig = attrVal.trim().startsWith('{') ? evaluate(attrVal, {}) : {target: attrVal};
+    return {...ESLMediaControlMixin.DEFAULT_CONFIG, ...userConfig};
+  }
+
+  public set config(value: string | ESLMediaControlConfig) {
+    const serialized = typeof value === 'string' ? value : JSON.stringify(value);
+    this.$$attr(ESLMediaControlMixin.is, serialized);
+  }
+
+  public get $target(): ESLMedia | null {
+    if (!this.config.target) return null;
+    return ESLTraversingQuery.first(this.config.target, this.$host) as ESLMedia | null;
+  }
+
+  protected override attributeChangedCallback(): void {
+    memoize.clear(this, 'config');
+    this.$$on(this.onStateChange);
+  }
+
+  @listen('click')
+  protected onClick(): void {
+    const {$target, config} = this;
+    if (!$target || !config.action) return;
+    if (config.action in $target && typeof $target[config.action] === 'function') $target[config.action]();
+  }
+
+  @listen({
+    event: 'esl:media:play esl:media:paused esl:media:ended',
+    target: (control: ESLMediaControlMixin) => control.$target,
+    condition: (control: ESLMediaControlMixin) => !!control.config.activeCls
+  })
+  protected onStateChange(e: CustomEvent): void {
+    this.$$cls(this.config.activeCls!, e.type === 'esl:media:play');
+  }
+}
+
+declare global {
+  export interface ESLLibrary {
+    MediaControlMixin: typeof ESLMediaControlMixin;
+  }
+}
diff --git a/src/modules/esl-media/core.ts b/src/modules/esl-media/core.ts
index 6e0b1de77..bc227f6b1 100644
--- a/src/modules/esl-media/core.ts
+++ b/src/modules/esl-media/core.ts
@@ -3,3 +3,4 @@ export type {ESLMediaTagShape} from './core/esl-media.shape';
 export * from './core/esl-media';
 export * from './core/esl-media-provider';
 export * from './core/esl-media-registry';
+export * from './control/esl-media-control-mixin';


Test patch:
diff --git a/e2e/package.json b/e2e/package.json
index 7ef229f5b..b633f8c93 100644
--- a/e2e/package.json
+++ b/e2e/package.json
@@ -1,7 +1,7 @@
 {
   "private": true,
   "name": "@exadel/esl-snapshot-tests",
-  "version": "5.0.0",
+  "version": "5.0.1",
   "description": "Integration snapshot tests for ESL",
   "homepage": "https://esl-ui.com/",
   "license": "MIT",
@@ -19,7 +19,7 @@
     "@types/puppeteer": "^7.0.4",
     "jest-puppeteer": "^11.0.0",
     "pixelmatch": "^5.3.0",
-    "puppeteer": "^24.1.1",
+    "puppeteer": "^24.2.0",
     "rimraf": "^6.0.1",
     "sharp": "^0.33.5",
     "stucumber": "^0.19.0"