Repository: exadel-inc/esl
Pull request #2006 (2023-10-24 03:10:07)

refactor(esl-share): revisit `ESLShareConfig` API, create tests and documentation


Patch:
diff --git a/.codeclimate.yml b/.codeclimate.yml
index b6f038e83..4190d9c0f 100644
--- a/.codeclimate.yml
+++ b/.codeclimate.yml
@@ -12,7 +12,7 @@ checks:
       threshold: 350
   method-complexity:
     config:
-      threshold: 10
+      threshold: 6
   method-count:
     config:
       threshold: 40
diff --git a/pages/src/localdev.ts b/pages/src/localdev.ts
index 974d82acc..e453cd22e 100644
--- a/pages/src/localdev.ts
+++ b/pages/src/localdev.ts
@@ -117,6 +117,6 @@ ESLRelatedTarget.register();
 // Share component loading
 (async (): Promise<void> => {
   await import (/* webpackChunkName: 'esl-share/buttons' */'@exadel/esl/modules/esl-share/buttons/all');
-  ESLShareConfig.set(() => fetch('/assets/share/config.json').then((response) => response.json()));
+  await ESLShareConfig.set(() => fetch('/assets/share/config.json').then((response) => response.json()));
   ESLShare.register();
 })();
diff --git a/pages/views/components/esl-share.njk b/pages/views/components/esl-share.njk
index eb8635a95..1e974649c 100644
--- a/pages/views/components/esl-share.njk
+++ b/pages/views/components/esl-share.njk
@@ -8,6 +8,7 @@ aside:
   components:
     - esl-popup
     - esl-toggleable
+    - esl-trigger
   examples:
     - share
     - popup
diff --git a/pages/views/draft/share-regression.njk b/pages/views/draft/share-regression.njk
index ac865c16e..8a6128e2b 100644
--- a/pages/views/draft/share-regression.njk
+++ b/pages/views/draft/share-regression.njk
@@ -58,10 +58,11 @@ aside:
   <esl-share list="linkedin alladin group:alladin group:demo"></esl-share>
   <hr>
 
-  <h3>Displays list of share buttons with repeating buttons</h3>
-  <p>Shows component with repeating buttons. (should display 3 buttons LinkedIn)</p>
-  <esl-share list="linkedin linkedin linkedin"></esl-share>
-  <hr>
+{#  TODO: discuss behaviour #}
+{#  <h3>Displays list of share buttons with repeating buttons</h3>#}
+{#  <p>Shows component with repeating buttons. (should display 3 buttons LinkedIn)</p>#}
+{#  <esl-share list="linkedin linkedin linkedin"></esl-share>#}
+{#  <hr>#}
 
   <h3>Displays list of share buttons via incorrect list</h3>
   <p>Shows buttons only with the correct names. (should display only LinkedIn button)</p>
diff --git a/src/modules/esl-share/README.md b/src/modules/esl-share/README.md
index a204ca121..d9ae3f921 100644
--- a/src/modules/esl-share/README.md
+++ b/src/modules/esl-share/README.md
@@ -2,7 +2,7 @@
 
 Version: *1.0.0-beta*.
 
-Authors: *Dmytro Shovchko*.
+Authors: *Dmytro Shovchko*, *Alexey Stsefanovich (ala'n)*.
 
 ***Important Notice: the component is under beta version, it is tested and ready to use but be aware of its potential critical API changes.***
 
@@ -10,90 +10,167 @@ Authors: *Dmytro Shovchko*.
 
 The ESL Share component provides the capability of integrating into a web page a sharing mechanism of the page on social media platforms, such as Facebook, Twitter, and Pinterest. The available share actions depend on the device but might include the clipboard, email applications, websites, social media, etc.
 
-`ESLShareButton` is a custom element that is used for displaying the "Share on social media" button. It is intended to share the page using the action specified on the button.
+`<esl-share-button>` is a custom element that is used for displaying the "Share on social media" button. It is intended to share the page using the action specified on the button.
 
-`ESLShare` is a custom element that is used for showing the list of social media buttons. The element's content (a set of `ESLShareButtons`) is created automatically by specifying a list of networks or groups to display. Available social networks and their groups are listed in the configuration file.
+`<esl-share-list>` is a custom element that is used for showing the list of social media buttons. The element's content (a set of `<esl-share-button>`) is created automatically by specifying a list of networks or groups to display. Available social networks and their groups are listed in the configuration file.
 
-### Usage
+`<esl-share-popup>` is a custom element used as a wrapper for content that can be displayed as a pop-up element. The content of the element consists of `<esl-share-button>`. `<esl-share-popup>` exists in a single instance and it refreshes its content every time its state changes to "open".
 
-Make sure you register the share actions that you are going to use on your pages. You don't need to do anything special for this, just import the necessary share actions - the registration will be done automatically.
+`<esl-share-popup-trigger>` is a custom element, that allows triggering `<esl-share-popup>` instance state changes.
 
-Next, you have two options for using the share component. The first option is to use only the ESLShareButton. In this case, you simply add this element to the markup and set the required configuration with the attributes. The element needs to be registered with `ESLShareButton.register()`. And that's it.
+`<esl-share>` is a versatile element that can function as both a list and a pop-up element. Depending on the specified value of the "mode" attribute, either an `<esl-share-list>` or an `<esl-share-popup-trigger>` is built inside. If you intend to use only one type of sharing component, it's not advisable to use this element in order to reduce the number of dependencies.
 
-The second option to use the share component is to define the component configuration and bind it to the ESLShare. After that, all you have to do is to add an element with names or groups of social networks to the ESLShare markup. The item content, consisting of a set of buttons, will be generated automatically. It's the same as you would add each button to the markup and prescribe its configuration manually.
+### Usage
 
-To use this option it is necessary to set the configuration for the list of buttons and then register the item
-`ESLShare.register();`
+Make sure you register the share actions that you are going to use on your pages. You don't need to do anything special for this, just import the necessary share actions - the registration will be done automatically.
 
-### Binding a component configuration
+Now you need to configure the share buttons and their groups (if you plan to use them). There are several ways to do this. These will be described further in the relevant section.
 
-If you want to define the behavior of the share components only by specifying a name or a group of buttons, you will have to specify the configuration of the components.
+***Notice. If you don't plan to use any own custom button settings, you might be fine with the configuration from our library. Simply import the button you need from the `esl-share/buttons` folder. An additional bonus will be that in this case there is no need to register the action - the button will perform it on its own.***
 
-There are several different ways to do this. And you can easily determine the most suitable way for you.
+Next, you have several options for using the share component. The first option is to use only the `<esl-share-button>`. In this case, you simply add this element to the markup and set the required configuration using attributes. The element needs to be registered with `ESLShareButton.register()`. And that's it.
 
-The first option is an asynchronous way to load the config. You prepare a configuration file or service in advance that will return a configuration object. You just should write the provider function of the config
-```
-() => fetch('/assets/share/config.json').then((response) => response.json())
+```html
+<esl-share-button name="facebook" default-icon></esl-share-button>
 ```
-and setup configuration in this manner
-```
-ESLShareConfig.set(() => fetch('/assets/share/config.json').then((response) => response.json()));
-```
-or can use the alias
+
+The second option to use the share component is to define the component configuration and bind it to the `<esl-share-list>`. After that, all you have to do is to add an element with names or groups of social networks to the `ESLShareList` markup. The item content, consisting of a set of buttons, will be generated automatically. It's the same as adding each button to the markup and prescribe its configuration manually. To use this option it is necessary to set the configuration for the list of buttons and then register the element.
+`ESLShareList.register()` (it will register `ESLShareButton` automatically).
+
+```html
+<esl-share-list list="facebook linkedin mail copy"></esl-share-list>
 ```
-ESLShare.config(() => fetch('/assets/share/config.json').then((response) => response.json()));
+
+The third option for using the share component is the same as the previous one, only the list of buttons is shown inside the pop-up window. All actions are similar to the previous one, except that you add the `<esl-share-popup-trigger>` element to the markup. The list of buttons that will be displayed in the pop-up window is set in the markup as a list of names and groups, absolutely identical to `<esl-share-list>`. To use this option it is necessary to set the configuration for the list of buttons and then register the element `ESLSharePopupTrigger.register()` (it will register `ESLShareButton` and `ESLSharePopup` automatically).
+
+```html
+<esl-share-popup-trigger list="facebook linkedin mail copy"></esl-share-popup-trigger>
 ```
 
-The following option is very similar to the first, but instead of an asynchronous configuration provider function, you use a pre-prepared configuration object.
+The last option for using the share component includes the previous two. That is one element, but behaves differently. You add an `<esl-share>` element to the markup, specify a list of buttons, and add the "mode" attribute, in which you specify in which of the modes ("list" or "popup") it should work, and that's it. To use this option it is necessary to set the configuration for the list of buttons and then register the element `ESLShare.register()` (it will register `ESLShareButton`, `ESLShareList`, `ESLSharePopup` and `ESLSharePopupTrigger` automatically).
+
+```html
+<esl-share list="facebook linkedin mail copy" mode="list"></esl-share>
 ```
-ESLShareConfig.create(myShareConfigurationObject);
+
+### Configuring the ESLShare components
+
+ESLShare provides the ability to setup a component configuration globally. 
+This means that you do not need to configure each button separately. 
+You can set buttons and groups config globally and then it will be available for each `esl-share`, `esl-share-list`, or `esl-share-popup-trigger`. 
+All of mentioned components use the same configuration instance stored in `ESLShareConfig.instance`.
+
+To add a single button, group to the configuration, you can use the `append` method of the configuration:
+```typescript
+// Add 'facebook' button to the configuration
+ESLShareConfig.append({
+   name: 'facebook',
+   action: 'media',
+   // ...
+});
+// Add 'mygroup' group to the configuration
+ESLShareConfig.append({
+   name: 'mygroup',
+   list: 'facebook twitter linkedin'
+});
 ```
-or can use 
+
+The `ESLShareConfig.append` accepts a list of buttons or groups as well:
+```typescript
+ESLShareConfig.append([
+  {
+    name: 'facebook',
+    action: 'media',
+    // ...
+  },
+  {
+    name: 'linkedin',
+    action: 'media',
+    // ...
+  }
+]);
 ```
-ESLShareConfig.set(myShareConfigurationObject);
+
+The `esl-share` module provides an out of the box configuration for the most popular social networks and sharing options.
+To use it, you need to import required configuration from the `esl-share/buttons` directory it will automatically register in the `ESLShareConfig.instance`:
+
+```typescript
+// Register 'facebook' button ootb configuration
+import '@exadel/esl/modules/esl-share/buttons/facebook';
+
+// Register all buttons ootb configurations
+import '@exadel/esl/modules/esl-share/buttons/all';
 ```
 
-The difference between these two methods is obtained only in the fact that in the first case, the configuration instance is returned and in the second case promise is returned.
+Note: configuration of the button or group appends to the existing configuration but overrides the existing one if the button or group with the same name already exists.
 
-There is also a third way to set the configuration. You do not need to prepare an object with the definition of the configuration in advance. You just need to create an empty configuration object and add the commands you need. You can also add groups to the configuration. For example
+In case you need just a little fix up the existing configuration you can use ootb button config and then override it:
+
+```typescript
+import {facebook} from '@exadel/esl/modules/esl-share/buttons/facebook';
+// Change icon of the facebook button
+ESLShareConfig.append({
+  ...facebook,
+  icon: '<svg>...</svg>'
+});
 ```
-ESLShareConfig.create().add(
-  [
-    facebook,
-    linkedin,
-    copy
-  ],
-  [
-    {name: 'mygroup', list: 'facebook linkedin copy'}
-  ]
-);
+
+You can also override the configuration of the button or group in the runtime. 
+
+The `esl-share` module supports configuration by the `ESLShareConfigInit` object
+```typescript
+export interface ESLShareConfigInit {
+  /** List of share buttons configurations */
+  buttons?: ESLShareButtonConfig[];
+  /** List of share buttons groups configurations */
+  groups?: ESLShareGroupConfig[];
+}
 ```
 
-Thus, we added 3 buttons `facebook`, `linkedin`, `copy` and described the `mygroup` group containing these buttons. You can prepare the button configurations yourself, or you can import them from the `/src/modules/esl-share/config/` directory. There we have prepared for you in advance about 20 configurations for various actions to share.
+The `ESLShareConfigInit` object can be passed to the `ESLShareConfig.set` method.
+It is usefull when you have group you configuration in a single json file, or generate it in the runtime 
+(depends on the user's locale, for example).
 
-You can also override the settings of any buttons or groups in the runtime.
-For example, a different set of share buttons for the Japanese locale and override the copy button.
+```typescript
+import config from './config.json';
+ESLShareConfig.set(config);
 ```
-ESLShare.config(() => fetch('/assets/share/config.json').then((response) => response.json()))
-  .then((config) => {
-    if (locale === 'ja') config.add([anotherCopy], [{name: 'mygroup', list: 'hatena linkedin copy'}]);
-  });
+
+The `ESLShareConfig.set` method accepts a function that returns a promise of `ESLShareConfigInit` object 
+or a Promise of the `ESLShareConfigInit` object:
+
+```typescript
+const configProvider = () => {
+  const buttons = [];
+  if (allowPrint) buttons.push(print);
+  if (allowCopy) buttons.push(copy);
+  return {buttons};
+};
+// ...
+ESLShareConfig.set(configProvider);
+```
+
+```typescript
+// Retrive config from the server based on the user's locale
+const countryCode = document.;
+ESLShareConfig.set(fetch(`/assets/share/config.${countryCode}.json`).then((response) => response.json()));
 ```
 
-In the example above, the configuration will be received by an asynchronous request, and then, if the condition is met, the button and the group `mygroup` will be inserted or replaced (if the configuration for an entity with the same name already exists).
 
-### ESLShare config
+#### ESLShare config objects
 
-Above it was told and shown how you can set the configuration of the component. But what is a configuration object?
+Above it was told and shown how you can set the configuration of the component. 
+But how does the configuration for the button or group look like?
 
-Config is a javascript object that contains two properties. The first one is `buttons` describing the configuration of the buttons. The second is `groups` which configures the groups. Both properties are arrays containing objects describing buttons and groups respectively.
+As was mentioned above the cumulative `ESLShareConfigInit` config object consists of two properties: 
+ - `buttons` - array of button configurations
+ - `groups` - array of group configurations.
 
 An example of a description of the button configuration:
 ```json
 {
     "action": "media",
-    "icon": "\u003csvg xmlns\u003d\"http://www.w3.org/2000/svg\" aria-hidden\u003d\"true\" focusable\u003d\"false\" role\u003d\"presentation\" viewBox\u003d\"0 0 27.99 28\"\u003e\u003cpath d\u003d\"M23 17.11l.55-4.24h-4.2v-2.71c0-1.23.34-2.06 2.1-2.06h2.25V4.29a31.62 31.62 0 00-3.28-.16c-3.24 0-5.46 2-5.46 5.61v3.13h-3.63v4.24H15V28h4.39V17.11z\"/\u003e\u003c/svg\u003e",
-    "iconBackground": "#3c5996",
+    "icon": "\u003csvg xmlns\u003d\"http://www.w3.org/2000/svg\" aria-hidden\u003d\"true\" fill=\"#fff\" focusable\u003d\"false\" role\u003d\"presentation\" style=\"background: #3c5996;\" viewBox\u003d\"0 0 27.99 28\"\u003e\u003cpath d\u003d\"M23 17.11l.55-4.24h-4.2v-2.71c0-1.23.34-2.06 2.1-2.06h2.25V4.29a31.62 31.62 0 00-3.28-.16c-3.24 0-5.46 2-5.46 5.61v3.13h-3.63v4.24H15V28h4.39V17.11z\"/\u003e\u003c/svg\u003e",
     "link": "//www.facebook.com/sharer.php?u\u003d{u}",
     "name": "facebook",
     "title": "Facebook"
@@ -103,8 +180,7 @@ or for example the configuration of the copy button
 ```json
 {
     "action": "copy",
-    "icon": "\u003csvg xmlns\u003d\"http://www.w3.org/2000/svg\" aria-hidden\u003d\"true\" focusable\u003d\"false\" role\u003d\"presentation\" viewBox\u003d\"0 0 28 28\"\u003e\u003cpath d\u003d\"M17 9.69l-7.43 7.43 1.18 1.18 7.43-7.43L17 9.69z\"/\u003e\u003cpath d\u003d\"M4.31 17.8c-.481.481-.48 1.29.00138 1.77l4.02 4.02c.481.481 1.29.483 1.77.00138l4.95-4.95c.481-.481.481-1.29-7e-7-1.78l-4.02-4.02c-.481-.481-1.29-.481-1.78 0l-4.95 4.95zm1.47.887l4.36-4.36 3.44 3.44-4.36 4.36-3.44-3.44zm7-9.37c-.481.481-.481 1.29 2.8e-7 1.78l4.02 4.02c.481.481 1.29.481 1.78 0l4.95-4.95c.481-.481.48-1.29-.00138-1.77l-4.02-4.02c-.481-.481-1.29-.483-1.77-.00138l-4.95 4.95zm1.47.889l4.36-4.36 3.44 3.44-4.36 4.36-3.44-3.44z\"/\u003e\u003c/svg\u003e",
-    "iconBackground": "#a0522d",
+    "icon": "\u003csvg xmlns\u003d\"http://www.w3.org/2000/svg\" aria-hidden\u003d\"true\" fill=\"#fff\" focusable\u003d\"false\" role\u003d\"presentation\" style=\"background: #a0522d;\" viewBox\u003d\"0 0 28 28\"\u003e\u003cpath d\u003d\"M17 9.69l-7.43 7.43 1.18 1.18 7.43-7.43L17 9.69z\"/\u003e\u003cpath d\u003d\"M4.31 17.8c-.481.481-.48 1.29.00138 1.77l4.02 4.02c.481.481 1.29.483 1.77.00138l4.95-4.95c.481-.481.481-1.29-7e-7-1.78l-4.02-4.02c-.481-.481-1.29-.481-1.78 0l-4.95 4.95zm1.47.887l4.36-4.36 3.44 3.44-4.36 4.36-3.44-3.44zm7-9.37c-.481.481-.481 1.29 2.8e-7 1.78l4.02 4.02c.481.481 1.29.481 1.78 0l4.95-4.95c.481-.481.48-1.29-.00138-1.77l-4.02-4.02c-.481-.481-1.29-.483-1.77-.00138l-4.95 4.95zm1.47.889l4.36-4.36 3.44 3.44-4.36 4.36-3.44-3.44z\"/\u003e\u003c/svg\u003e",
     "link": "",
     "name": "copy",
     "title": "Copy",
@@ -113,16 +189,16 @@ or for example the configuration of the copy button
     }
 }
 ```
+
 What the properties of the button description object mean:
  - `action` - the name of the action to be performed by clicking on the button (recall that the action must be registered, the import was executed)
  - `icon` - the HTML content of the share icon
- - `iconBackground` - the color of the icon background (the value -  CSS data type represents a color). If you do not need to set the color, set the value to `transparent`
  - `link` - URL link (with placeholders) to share on a social network. Can contain the next placeholders:
     - {u} or {url} - URL to share on social network (shareUrl property on the ESLShareButton instance)
     - {t} or {title} - title to share on social network (shareTitle property on the ESLShareButton instance)
  - `name` - string identifier of the button (no spaces)
  - `title` - button title
- - `additional` - additional params to pass into a button (can be used by share actions)
+ - `additional` - optional additional params to pass into a button (can be used by share actions)
 
 The configuration object of the group is simple, consisting of two properties:
  - `name` - string identifier of the group (no spaces)
@@ -162,6 +238,13 @@ ESLShare provides several actions available for you to use:
 
 For using actions you should import the required actions before setting up the configuration and registering ESLShare components. When an unregistered action is specified for a button, the button will not be able to perform the share action and will be marked as 'unavailable'. The same behavior occurs if the action is unavailable on the user's device, e.g. native action on the user's desktop.
 
+Note: if default configuration does not used, you should import the required actions manually:
+```typescript
+  import '@exadel/esl/modules/esl-share/actions/copy';
+  import '@exadel/esl/modules/esl-share/actions/media';
+```
+
+
 ### ESLShareButton
 
 #### Attributes / Properties
@@ -172,8 +255,13 @@ For using actions you should import the required actions before setting up the c
  - `share-url` - URL to share (current page URL by default)
  - `share-title` - title to share (current document title by default)
  - `additional` - additional params to pass into a button (can be used by share actions)
+ - `default-icon` - marker to render default icon inside button on init
  - `unavailable` - marker of availability of share button
 
+#### Observing changes in configuration
+
+The button component is notified of any configuration changes. The button will update its action if the configuration has changed. Also, the button will redraw its content if the button contains an instruction to render the default icon.
+
 #### Attributes cascading
 
 If you want to utilize URL and title overrides within any parts of the page to share using the `share-url` and `share-title` attributes, there's no need to write these attributes on each button. You can write them once on the root element of the part for which these values are valid. Alternatively, if you want to override the values for the entire document, you can set them on the body of the HTML document.
@@ -184,6 +272,52 @@ The principle of cascading is similar to CSS variables. The value is searched fr
 
  - `share` - the same as clicking the button, i.e. perform the share action
 
+### ESLShareList
+
+#### Attributes / Properties
+
+ - `list` - list of social networks or groups of them to display (all by default). The value - a string containing the names of the buttons or groups (specified with the prefix group:) separated by spaces. For example: `"facebook reddit group:default"`
+ - `share-url` - URL to share (current page URL by default)
+ - `share-title` - title to share (current document title by default)
+ - `ready` - ready state marker
+
+#### Observing changes in configuration
+
+The component is notified of any configuration changes. And, if during the check it turns out that the list of buttons inside the component changes, the component updates its content. Accordingly, in this case, the component itself also throws an event about its own content change.
+
+#### Public API
+
+ - `buttonsConfig` - getter that returns config of buttons specified by the list attribute
+
+#### Events
+
+ - `esl:share:changed` - event to dispatch on change of ESLShareList
+
+### ESLSharePopup
+
+This element is based on [ESLPopup](../esl-popup/README.md) element and exists in a single instance. Its shared instance adds directly to the document's body when any of `ESLSharePopupTrigger` requires showing this popup. It removes from the document's body on hide action. 
+`ESLSharePopup` renders buttons from the list on show action. If an `ESLSharePopup` element with the desired set of buttons already exists in the document body, the existing one will be reused. 
+
+#### Observing changes in configuration
+
+The popup is notified of any configuration changes. In this case, the component is simply hidden if it is in an open state, i.e. there is a set of rendered buttons inside. So, when you open it again, it will simply redraw the new buttons inside.
+
+#### Public API
+
+ - `sharedInstance` - static getter that returns shared instance of ESLSharePopup
+
+### ESLSharePopupTrigger
+
+A trigger element is based on [ESLTrigger](../esl-trigger/README.md) to activate the popup with share buttons, which will activate the popup when you hover over it. Also, one additional activity of the `ESLSharePopupTrigger` is to forward the `share-title` and `share-url` attributes from the root `ESLSharePopupTrigger` component (or its parents if not defined on the trigger element) to the popup. So it's possible for components with the same set of buttons but different URLs and titles to share to use the same popup.
+
+#### Attributes / Properties
+
+ - `list` - list of social networks or groups of them to display (all by default). The value - a string containing the names of the buttons or groups (specified with the prefix group:) separated by spaces. For example: `"facebook reddit group:default"`
+ - `popup-initial-params` - initial params to pass into popup on show action (Default: `{position: 'top', hideDelay: 220}`)
+ - `share-url` - URL to share (current page URL by default)
+ - `share-title` - title to share (current document title by default)
+ - `track-hover` - [MediaQuery](../esl-media-query/README.md) to define allowed to track hover event media. (Default: `all`)
+
 ### ESLShare
 
 #### Attributes / Properties
@@ -191,6 +325,7 @@ The principle of cascading is similar to CSS variables. The value is searched fr
  - `list` - list of social networks or groups of them to display (all by default). The value - a string containing the names of the buttons or groups (specified with the prefix group:) separated by spaces. For example: `"facebook reddit group:default"`
  - `share-url` - URL to share (current page URL by default)
  - `share-title` - title to share (current document title by default)
+ - `trigger-initial-params` - initial params for the `ESLSharePopupTrigger` to be created (Default: `{trackClick: true, trackHover: true}`)
  - `mode` - rendering mode of the share buttons. The `list` and `popup` are available (list by default)
  - `ready` - ready state marker
 
@@ -198,18 +333,13 @@ The principle of cascading is similar to CSS variables. The value is searched fr
 
 There are two modes available to render buttons.
 
-In `list` mode, the buttons are drawn inside the component as a list. Nothing special.
+There are two modes available to render buttons. In list mode, the `ESLShareList` is drawn inside the component as a list. Nothing special. 
 
-When `popup` mode is specified, the buttons are created inside of a [ESLPopup](../esl-popup/README.md) element, which is built directly into the document's body. If a [ESLPopup](../esl-popup/README.md) element with the desired set of buttons already exists in the document body, the existing one will be reused. A trigger element is created inside the ESLShare component to activate the popup with share buttons, which will activate the popup when you hover over it. Also, one additional activity of the ESLSharePopupTrigger is to forward the `share-title` and `share-url` attributes from the root ESLShare component to the popup. So it's possible for components with the same set of buttons but different URLs and title to share to use the same popup.
+Similarly, in popup mode, the `ESLSharePopupTrigger`` is drawn inside component and shows a popup with the specified list of buttons.
 
 #### Public API
 
- - `config` - static method to get or update config with a promise of a new config object or using a config provider function
- - `build` - builds content of component
-
-#### Events
-
- - `esl:share:ready` - event to dispatch on ready state of ESLShare
+ - `config` - static method to get or update config with a promise of a new config object or using a config provider function (***Deprecated***)
 
 ### ESLShareAction
 
diff --git a/src/modules/esl-share/buttons/blogger.ts b/src/modules/esl-share/buttons/blogger.ts
index dd3138d72..cde7e4459 100644
--- a/src/modules/esl-share/buttons/blogger.ts
+++ b/src/modules/esl-share/buttons/blogger.ts
@@ -11,4 +11,4 @@ export const blogger: ESLShareButtonConfig = {
   name: 'blogger',
   title: 'Blogger'
 };
-ESLShareConfig.instance.appendButton(blogger);
+ESLShareConfig.append(blogger);
diff --git a/src/modules/esl-share/buttons/copy.ts b/src/modules/esl-share/buttons/copy.ts
index 8d8708981..217647599 100644
--- a/src/modules/esl-share/buttons/copy.ts
+++ b/src/modules/esl-share/buttons/copy.ts
@@ -14,4 +14,4 @@ export const copy: ESLShareButtonConfig = {
     alertText: 'Copied to clipboard'
   }
 };
-ESLShareConfig.instance.appendButton(copy);
+ESLShareConfig.append(copy);
diff --git a/src/modules/esl-share/buttons/facebook.ts b/src/modules/esl-share/buttons/facebook.ts
index 063b6446a..e25dcebd6 100644
--- a/src/modules/esl-share/buttons/facebook.ts
+++ b/src/modules/esl-share/buttons/facebook.ts
@@ -11,4 +11,4 @@ export const facebook: ESLShareButtonConfig = {
   name: 'facebook',
   title: 'Facebook'
 };
-ESLShareConfig.instance.appendButton(facebook);
+ESLShareConfig.append(facebook);
diff --git a/src/modules/esl-share/buttons/hatena.ts b/src/modules/esl-share/buttons/hatena.ts
index ca7c580eb..19ec1d0a1 100644
--- a/src/modules/esl-share/buttons/hatena.ts
+++ b/src/modules/esl-share/buttons/hatena.ts
@@ -11,4 +11,4 @@ export const hatena: ESLShareButtonConfig = {
   name: 'hatena',
   title: 'Hatena'
 };
-ESLShareConfig.instance.appendButton(hatena);
+ESLShareConfig.append(hatena);
diff --git a/src/modules/esl-share/buttons/kakao.ts b/src/modules/esl-share/buttons/kakao.ts
index c5cf925ff..92c0a745d 100644
--- a/src/modules/esl-share/buttons/kakao.ts
+++ b/src/modules/esl-share/buttons/kakao.ts
@@ -11,4 +11,4 @@ export const kakao: ESLShareButtonConfig = {
   name: 'kakao',
   title: 'Kakao'
 };
-ESLShareConfig.instance.appendButton(kakao);
+ESLShareConfig.append(kakao);
diff --git a/src/modules/esl-share/buttons/line.ts b/src/modules/esl-share/buttons/line.ts
index 82df5dbdf..a43544d1c 100644
--- a/src/modules/esl-share/buttons/line.ts
+++ b/src/modules/esl-share/buttons/line.ts
@@ -11,4 +11,4 @@ export const line: ESLShareButtonConfig = {
   name: 'line',
   title: 'LINE'
 };
-ESLShareConfig.instance.appendButton(line);
+ESLShareConfig.append(line);
diff --git a/src/modules/esl-share/buttons/linkedin.ts b/src/modules/esl-share/buttons/linkedin.ts
index 9a5d9de61..9582c2c4e 100644
--- a/src/modules/esl-share/buttons/linkedin.ts
+++ b/src/modules/esl-share/buttons/linkedin.ts
@@ -11,4 +11,4 @@ export const linkedin: ESLShareButtonConfig = {
   name: 'linkedin',
   title: 'LinkedIn'
 };
-ESLShareConfig.instance.appendButton(linkedin);
+ESLShareConfig.append(linkedin);
diff --git a/src/modules/esl-share/buttons/mail.ts b/src/modules/esl-share/buttons/mail.ts
index f74caf641..9f83f3875 100644
--- a/src/modules/esl-share/buttons/mail.ts
+++ b/src/modules/esl-share/buttons/mail.ts
@@ -11,4 +11,4 @@ export const mail: ESLShareButtonConfig = {
   name: 'mail',
   title: 'Mail'
 };
-ESLShareConfig.instance.appendButton(mail);
+ESLShareConfig.append(mail);
diff --git a/src/modules/esl-share/buttons/mix.ts b/src/modules/esl-share/buttons/mix.ts
index c7cea1a63..17c6ae85e 100644
--- a/src/modules/esl-share/buttons/mix.ts
+++ b/src/modules/esl-share/buttons/mix.ts
@@ -11,4 +11,4 @@ export const mix: ESLShareButtonConfig = {
   name: 'mix',
   title: 'MIX'
 };
-ESLShareConfig.instance.appendButton(mix);
+ESLShareConfig.append(mix);
diff --git a/src/modules/esl-share/buttons/mixi.ts b/src/modules/esl-share/buttons/mixi.ts
index fad5cb962..dfcfd583c 100644
--- a/src/modules/esl-share/buttons/mixi.ts
+++ b/src/modules/esl-share/buttons/mixi.ts
@@ -11,4 +11,4 @@ export const mixi: ESLShareButtonConfig = {
   name: 'mixi',
   title: 'Mixi'
 };
-ESLShareConfig.instance.appendButton(mixi);
+ESLShareConfig.append(mixi);
diff --git a/src/modules/esl-share/buttons/myspace.ts b/src/modules/esl-share/buttons/myspace.ts
index bdb48efa8..ecbb03d4c 100644
--- a/src/modules/esl-share/buttons/myspace.ts
+++ b/src/modules/esl-share/buttons/myspace.ts
@@ -11,4 +11,4 @@ export const myspace: ESLShareButtonConfig = {
   name: 'myspace',
   title: 'Myspace'
 };
-ESLShareConfig.instance.appendButton(myspace);
+ESLShareConfig.append(myspace);
diff --git a/src/modules/esl-share/buttons/native-share.ts b/src/modules/esl-share/buttons/native-share.ts
index e9d44ff83..9fd1fbb29 100644
--- a/src/modules/esl-share/buttons/native-share.ts
+++ b/src/modules/esl-share/buttons/native-share.ts
@@ -11,4 +11,4 @@ export const nativeShare: ESLShareButtonConfig = {
   name: 'native-share',
   title: 'Share page'
 };
-ESLShareConfig.instance.appendButton(nativeShare);
+ESLShareConfig.append(nativeShare);
diff --git a/src/modules/esl-share/buttons/pinterest.ts b/src/modules/esl-share/buttons/pinterest.ts
index 56eed15a3..54eec1890 100644
--- a/src/modules/esl-share/buttons/pinterest.ts
+++ b/src/modules/esl-share/buttons/pinterest.ts
@@ -11,4 +11,4 @@ export const pinterest: ESLShareButtonConfig = {
   name: 'pinterest',
   title: 'Pinterest'
 };
-ESLShareConfig.instance.appendButton(pinterest);
+ESLShareConfig.append(pinterest);
diff --git a/src/modules/esl-share/buttons/print.ts b/src/modules/esl-share/buttons/print.ts
index 38796be40..5724ce17a 100644
--- a/src/modules/esl-share/buttons/print.ts
+++ b/src/modules/esl-share/buttons/print.ts
@@ -11,4 +11,4 @@ export const print: ESLShareButtonConfig = {
   name: 'print',
   title: 'Print page'
 };
-ESLShareConfig.instance.appendButton(print);
+ESLShareConfig.append(print);
diff --git a/src/modules/esl-share/buttons/pusha.ts b/src/modules/esl-share/buttons/pusha.ts
index a08b8ef85..9fa2c9df3 100644
--- a/src/modules/esl-share/buttons/pusha.ts
+++ b/src/modules/esl-share/buttons/pusha.ts
@@ -11,4 +11,4 @@ export const pusha: ESLShareButtonConfig = {
   name: 'pusha',
   title: 'Pusha'
 };
-ESLShareConfig.instance.appendButton(pusha);
+ESLShareConfig.append(pusha);
diff --git a/src/modules/esl-share/buttons/reddit.ts b/src/modules/esl-share/buttons/reddit.ts
index c624a24a4..806b4f2d7 100644
--- a/src/modules/esl-share/buttons/reddit.ts
+++ b/src/modules/esl-share/buttons/reddit.ts
@@ -11,4 +11,4 @@ export const reddit: ESLShareButtonConfig = {
   name: 'reddit',
   title: 'Reddit'
 };
-ESLShareConfig.instance.appendButton(reddit);
+ESLShareConfig.append(reddit);
diff --git a/src/modules/esl-share/buttons/sina-weibo.ts b/src/modules/esl-share/buttons/sina-weibo.ts
index eab4bda36..789220240 100644
--- a/src/modules/esl-share/buttons/sina-weibo.ts
+++ b/src/modules/esl-share/buttons/sina-weibo.ts
@@ -11,4 +11,4 @@ export const sinaWeibo: ESLShareButtonConfig = {
   name: 'sina-weibo',
   title: 'Sina Weibo'
 };
-ESLShareConfig.instance.appendButton(sinaWeibo);
+ESLShareConfig.append(sinaWeibo);
diff --git a/src/modules/esl-share/buttons/telegram.ts b/src/modules/esl-share/buttons/telegram.ts
index d42e2ccc2..bbc00323c 100644
--- a/src/modules/esl-share/buttons/telegram.ts
+++ b/src/modules/esl-share/buttons/telegram.ts
@@ -11,4 +11,4 @@ export const telegram: ESLShareButtonConfig = {
   name: 'telegram',
   title: 'Telegram'
 };
-ESLShareConfig.instance.appendButton(telegram);
+ESLShareConfig.append(telegram);
diff --git a/src/modules/esl-share/buttons/twitter.ts b/src/modules/esl-share/buttons/twitter.ts
index c72ce81ab..ffc43b6a7 100644
--- a/src/modules/esl-share/buttons/twitter.ts
+++ b/src/modules/esl-share/buttons/twitter.ts
@@ -11,4 +11,4 @@ export const twitter: ESLShareButtonConfig = {
   name: 'twitter',
   title: 'X'
 };
-ESLShareConfig.instance.appendButton(twitter);
+ESLShareConfig.append(twitter);
diff --git a/src/modules/esl-share/buttons/wykop.ts b/src/modules/esl-share/buttons/wykop.ts
index a19775621..e960e1069 100644
--- a/src/modules/esl-share/buttons/wykop.ts
+++ b/src/modules/esl-share/buttons/wykop.ts
@@ -11,4 +11,4 @@ export const wykop: ESLShareButtonConfig = {
   name: 'wykop',
   title: 'Wykop'
 };
-ESLShareConfig.instance.appendButton(wykop);
+ESLShareConfig.append(wykop);
diff --git a/src/modules/esl-share/core/esl-share-button.ts b/src/modules/esl-share/core/esl-share-button.ts
index a9bc2f5d3..17188b9fa 100644
--- a/src/modules/esl-share/core/esl-share-button.ts
+++ b/src/modules/esl-share/core/esl-share-button.ts
@@ -59,7 +59,7 @@ export class ESLShareButton extends ESLBaseElement {
   /** @returns config of button specified by the name attribute */
   @memoize()
   public get config(): ESLShareButtonConfig | undefined {
-    return ESLShareConfig.getButton(this.name);
+    return ESLShareConfig.instance.getButton(this.name);
   }
 
   /** Gets a property from attribute, or from button config if not set attribute */
@@ -116,13 +116,13 @@ export class ESLShareButton extends ESLBaseElement {
   }
 
   /** Initializes the button */
-  protected init(): void {
-    if (this.ready) return;
+  protected init(force?: boolean): void {
+    if (this.ready && !force) return;
 
     if (this.defaultIcon) this.initIcon();
     this.initA11y();
     this.updateAction();
-    this.toggleAttribute('ready', true);
+    this.$$attr('ready', true);
   }
 
   /** Sets initial a11y attributes */
@@ -138,7 +138,7 @@ export class ESLShareButton extends ESLBaseElement {
     if (!this.config) return;
     const {title, icon} = this.config;
     this.title = title;
-    this.innerHTML = icon;
+    this.innerHTML = icon || '';
     const $icon = this.firstElementChild as HTMLElement;
     if ($icon?.tagName !== 'svg') return;
     $icon.classList.add('esl-share-icon');
@@ -185,7 +185,7 @@ export class ESLShareButton extends ESLBaseElement {
     const {config} = this;
     memoize.clear(this, 'config');
     if (isEqual(this.config, config)) return;
-    this.updateAction();
+    this.init(true);
     this.$$fire(this.SHARE_BUTTON_CHANGED_EVENT, {bubbles: false});
   }
 }
diff --git a/src/modules/esl-share/core/esl-share-config.ts b/src/modules/esl-share/core/esl-share-config.ts
index 80c9c86dd..ab0cbca4a 100644
--- a/src/modules/esl-share/core/esl-share-config.ts
+++ b/src/modules/esl-share/core/esl-share-config.ts
@@ -1,16 +1,17 @@
 import {decorate, memoize} from '../../esl-utils/decorators';
 import {microtask} from '../../esl-utils/async/microtask';
+import {isObject} from '../../esl-utils/misc/object/types';
+import {uniq} from '../../esl-utils/misc/array';
 import {SyntheticEventTarget} from '../../esl-utils/dom/events/target';
 
-/** {@link ESLShareConfig} provider type definition */
-export type ESLShareConfigProviderType = () => Promise<ESLShareConfig>;
+import type {PropertyProvider} from '../../esl-utils/misc/functions';
 
 /** The definition of the sharing button */
 export interface ESLShareButtonConfig {
   /** Name of the share action, which is performed after the button is pressed */
   action: string;
   /** HTML content of the share icon */
-  icon: string;
+  icon?: string;
   /**
    * URL link (with placeholders) to share. Can contain the following placeholders:
    * - `{u}` or `{url}` - URL to share (`shareUrl` property on the {@link ESLShareButton} instance)
@@ -33,94 +34,110 @@ export interface ESLShareGroupConfig {
   list: string;
 }
 
-/** Gets item of the section by name */
-function getConfigSectionItem<T extends ESLShareButtonConfig | ESLShareGroupConfig>(section: T[], name: string): T | undefined {
-  return section.find((item) => item.name === name);
+/** Object containing {@link ESLShareButtonConfig} and {@link ESLShareGroupConfig} definitions */
+export interface ESLShareConfigInit {
+  /** List of share buttons configurations */
+  buttons?: ESLShareButtonConfig[];
+  /** List of share buttons groups configurations */
+  groups?: ESLShareGroupConfig[];
 }
 
-/** Inserts item into section or updates if exists */
-function setConfigSectionItem<T extends ESLShareButtonConfig | ESLShareGroupConfig>(section: T[], item: T): void {
-  const index = section.findIndex((i) => i.name === item.name);
-  if (index >= 0) section[index] = item;
-  else section.push(item);
-}
+const isGroupCfg = (cfg: any): cfg is ESLShareGroupConfig => isObject(cfg) && typeof cfg.name === 'string' && typeof cfg.list === 'string';
+const isButtonCfg = (cfg: any): cfg is ESLShareButtonConfig => isObject(cfg) && typeof cfg.name === 'string' && typeof cfg.action === 'string';
 
 /** Class for managing share buttons configurations */
 export class ESLShareConfig extends SyntheticEventTarget {
+  /** @returns ESLShareConfig shared instance */
   @memoize()
   public static get instance(): ESLShareConfig {
     return new ESLShareConfig();
   }
 
   /**
-   * Gets the button configuration.
-   * @returns config of button
-   */
-  public static getButton(name: string): ESLShareButtonConfig | undefined {
-    return ESLShareConfig.instance.getButton(name);
-  }
-
-  /**
-   * Selects the buttons for the given list and returns their configuration.
-   * @returns config of buttons
+   * Updates the configuration with either a {@link ESLShareConfigInit} object or provider function.
+   * Every button and group specified in the new config will be added to the current configuration.
+   * @returns ESLShareConfig instance
    */
-  public static getList(list: string): ESLShareButtonConfig[] {
-    return ESLShareConfig.instance.getList(list);
-  }
-
+  public static set(cfg: ESLShareConfigInit | PropertyProvider<ESLShareConfigInit>): ESLShareConfig;
   /**
-   * Updates the configuration with either a new config object or by using a configuration provider function.
+   * Updates the configuration with promise resolved to {@link ESLShareConfigInit} or promise provider function.
    * Every button and group specified in the new config will be added to the current configuration.
-   * @returns ESLShareConfig instance
+   * @returns Promise<ESLShareConfig> instance
    */
   public static set(
-    provider?: ESLShareConfigProviderType | Promise<Partial<ESLShareConfig>> | Partial<ESLShareConfig>
-  ): ESLShareConfig {
-    const {instance} = ESLShareConfig;
+    provider: Promise<Partial<ESLShareConfig>> | PropertyProvider<Promise<ESLShareConfigInit>>
+  ): Promise<ESLShareConfig>;
+  public static set(provider: any): ESLShareConfig | Promise<ESLShareConfig> {
     if (typeof provider === 'function') return this.set(provider());
-    if (typeof provider === 'object' && provider instanceof Promise) {
-      provider.then((cfg) => this.set(cfg));
-      return instance;
-    }
-    if (typeof provider === 'object') {
-      if (provider?.groups) provider.groups.forEach(instance.appendGroup, instance);
-      if (provider?.buttons) provider.buttons.forEach(instance.appendButton, instance);
-    }
-    return instance;
+    if (!isObject(provider)) return ESLShareConfig.instance;
+    if (provider instanceof Promise) return provider.then((cfg: ESLShareConfigInit) => this.set(cfg));
+    if (Array.isArray(provider.groups)) ESLShareConfig.instance.append(provider.groups);
+    if (Array.isArray(provider.buttons)) ESLShareConfig.instance.append(provider.buttons);
+    return ESLShareConfig.instance;
+  }
+
+  /** Appends single button or group to current configuration */
+  public static append(cfg: ESLShareButtonConfig | ESLShareGroupConfig | ESLShareButtonConfig[] | ESLShareGroupConfig[]): ESLShareConfig {
+    return ESLShareConfig.instance.append(cfg);
   }
 
-  public readonly buttons: ESLShareButtonConfig[] = [];
-  public readonly groups: ESLShareGroupConfig[] = [];
+  protected readonly _groups: Map<string, string> = new Map();
+  protected readonly _buttons: Map<string, ESLShareButtonConfig> = new Map();
 
   protected constructor() {
     super();
   }
 
+  /** @returns list of all available groups */
+  public get groups(): ESLShareGroupConfig[] {
+    return Array.from(this._groups.entries()).map(([name, list]) => ({name, list}));
+  }
+
+  /** @returns list of all available buttons */
+  public get buttons(): ESLShareButtonConfig[] {
+    return Array.from(this._buttons.values());
+  }
+
+  /** Normalize list string by removing groups and extra whitespaces */
+  protected resolve(query: string): string[] {
+    if (/\sall\s/.test(query)) return Array.from(this._buttons.keys());
+    const groups = new Set<string>(); // Deduplicate groups
+    while (query.includes('group:')) {
+      query = query.replace(/group:(\S+)/gi, (term: string, name: string) => {
+        if (groups.has(name)) return '';
+        groups.add(name);
+        return this._groups.get(name) || '';
+      });
+    }
+    return query.split(' ').filter(Boolean);
+  }
+
   /**
    * Selects the buttons for the given list and returns their configuration.
    * @returns config of buttons
    */
-  public getList(list: string): ESLShareButtonConfig[] {
-    if (list === 'all') return this.buttons;
-    return list.split(' ').reduce((res, item) => {
-      const [btnName, groupName] = item.split('group:');
-      if (groupName) {
-        const groupConfig = this.getGroup(groupName);
-        if (groupConfig) return res.concat(this.getList(groupConfig.list));
-      } else {
-        const btnConfig = this.getButton(btnName);
-        if (btnConfig) res.push(btnConfig);
-      }
-      return res;
-    }, [] as ESLShareButtonConfig[]);
+  public get(query: string): ESLShareButtonConfig[] {
+    const terms = this.resolve(query);
+    if (terms.includes('all')) return this.buttons;
+    const list = terms.map((name) => this.getButton(name));
+    return uniq(list.filter(Boolean) as ESLShareButtonConfig[]);
+  }
+
+  /** Clears the configuration */
+  public clear(): ESLShareConfig {
+    this._buttons.clear();
+    this._groups.clear();
+    this._onUpdate();
+    return this;
   }
 
   /**
    * Gets the group of buttons configuration.
    * @returns config of group
    */
-  public getGroup(groupName: string): ESLShareGroupConfig | undefined {
-    return getConfigSectionItem(this.groups, groupName);
+  public getGroup(name: string): ESLShareGroupConfig | undefined {
+    const list = this._groups.get(name);
+    return list ? {name, list} : undefined;
   }
 
   /**
@@ -128,25 +145,22 @@ export class ESLShareConfig extends SyntheticEventTarget {
    * @returns config of button
    */
   public getButton(name: string): ESLShareButtonConfig | undefined {
-    return getConfigSectionItem(this.buttons, name);
+    return this._buttons.get(name);
   }
 
-  /**
-   * Appends a button (inserts or updates) to the current config.
-   * @returns config instance
-   */
-  public appendButton(button: ESLShareButtonConfig): ESLShareConfig {
-    setConfigSectionItem(this.buttons, button);
-    this._onUpdate();
-    return this;
+  /** Updates the configuration with a {@link ESLShareButtonConfig} or {@link ESLShareGroupConfig} */
+  protected add(config: ESLShareButtonConfig | ESLShareGroupConfig): void {
+    if (isGroupCfg(config)) this._groups.set(config.name, config.list);
+    if (isButtonCfg(config)) this._buttons.set(config.name, config);
   }
 
-  /**
-   * Appends a group (inserts or updates) to the current config.
-   * @returns config instance
-   */
-  public appendGroup(group: ESLShareGroupConfig): ESLShareConfig {
-    setConfigSectionItem(this.groups, group);
+  /** Updates the configuration with a {@link ESLShareButtonConfig}(s) or {@link ESLShareGroupConfig}(s) */
+  public append(config: ESLShareButtonConfig | ESLShareGroupConfig | ESLShareButtonConfig[] | ESLShareGroupConfig[]): ESLShareConfig {
+    if (Array.isArray(config)) {
+      config.forEach(this.add, this);
+    } else {
+      this.add(config);
+    }
     this._onUpdate();
     return this;
   }
diff --git a/src/modules/esl-share/core/esl-share-list.ts b/src/modules/esl-share/core/esl-share-list.ts
index def75f250..167b44992 100644
--- a/src/modules/esl-share/core/esl-share-list.ts
+++ b/src/modules/esl-share/core/esl-share-list.ts
@@ -38,7 +38,7 @@ export class ESLShareList extends ESLBaseElement {
   /** @returns config of buttons specified by the list attribute */
   @memoize()
   public get buttonsConfig(): ESLShareButtonConfig[] {
-    return ESLShareConfig.getList(this.list);
+    return ESLShareConfig.instance.get(this.list);
   }
 
   public override connectedCallback(): void {
diff --git a/src/modules/esl-share/core/esl-share-popup.ts b/src/modules/esl-share/core/esl-share-popup.ts
index 3a1c8f9ae..0e273a9da 100644
--- a/src/modules/esl-share/core/esl-share-popup.ts
+++ b/src/modules/esl-share/core/esl-share-popup.ts
@@ -53,7 +53,7 @@ export class ESLSharePopup extends ESLTooltip {
   protected override onBeforeShow(params: ESLToggleableActionParams): boolean | void {
     const result = super.onBeforeShow(params);
     if (params.list) {
-      const buttonsList = ESLShareConfig.getList(params.list);
+      const buttonsList = ESLShareConfig.instance.get(params.list);
       this.appendButtonsFromList(buttonsList);
     }
     this.forwardAttributes();


Test patch:
diff --git a/src/modules/esl-share/test/esl-share-config.test.ts b/src/modules/esl-share/test/esl-share-config.test.ts
new file mode 100644
index 000000000..78f0be572
--- /dev/null
+++ b/src/modules/esl-share/test/esl-share-config.test.ts
@@ -0,0 +1,252 @@
+import {promisifyTimeout} from '../../esl-utils/async/promise/timeout';
+import {ESLShareConfig} from '../core/esl-share-config';
+import type {ESLShareButtonConfig, ESLShareGroupConfig} from '../core/esl-share-config';
+
+describe('ESLShareConfig tests', () => {
+  const SAMPLE_GROUP_1: ESLShareGroupConfig = {name: 'group1', list: 'sn1 sn2'};
+  const SAMPLE_GROUP_2: ESLShareGroupConfig = {name: 'group2', list: 'sn2 sn3 sn4'};
+  const SAMPLE_GROUP_3: ESLShareGroupConfig = {name: 'group3', list: 'group:group1 group:group2'};
+  const SAMPLE_BUTTON_1: ESLShareButtonConfig = {name: 'sn1', title: 'SN1', action: 'media', link: 'https://sn1.com'};
+  const SAMPLE_BUTTON_2: ESLShareButtonConfig = {name: 'sn2', title: 'SN2', action: 'media', link: 'https://sn2.com', icon: '<svg id="svg-2"></svg>'};
+  const SAMPLE_BUTTON_3: ESLShareButtonConfig = {name: 'sn3', title: 'SN3', action: 'media', link: 'https://sn3.com'};
+  const SAMPLE_BUTTON_4: ESLShareButtonConfig = {name: 'sn4', title: 'SN4', action: 'media', link: 'https://sn4.com', icon: '<svg id="svg-4"></svg>'};
+  const SAMPLE_BUTTON_5: ESLShareButtonConfig = {name: 'sn-5', title: 'SN5', action: 'media', link: 'https://sn5.com'};
+
+  describe('Getting ESLShareConfig state', () => {
+    const instance: ESLShareConfig = new (ESLShareConfig as any)();
+    const groups: ESLShareGroupConfig[] = [SAMPLE_GROUP_1, SAMPLE_GROUP_2, SAMPLE_GROUP_3];
+    const buttons: ESLShareButtonConfig[] = [SAMPLE_BUTTON_1, SAMPLE_BUTTON_2, SAMPLE_BUTTON_3, SAMPLE_BUTTON_4, SAMPLE_BUTTON_5];
+    instance.append(groups).append(buttons);
+
+    test.each([
+      ['sn1', SAMPLE_BUTTON_1],
+      ['sn2', SAMPLE_BUTTON_2],
+      ['sn3', SAMPLE_BUTTON_3],
+      ['sn4', SAMPLE_BUTTON_4],
+      ['sn-5', SAMPLE_BUTTON_5]
+    ])('ESLShareConfig.prototype.getButton resolves existing group %s', (name: string, button: any) => {
+      expect(instance.getButton(name)).toEqual(button);
+    });
+
+    test.each([
+      '', 'sn6', 'group1'
+    ])('ESLShareConfig.prototype.getButton does not resolves non-existing group %s', (name: string) => {
+      expect(instance.getButton(name)).toBeUndefined();
+    });
+
+    test.each([
+      ['group1', SAMPLE_GROUP_1],
+      ['group2', SAMPLE_GROUP_2],
+      ['group3', SAMPLE_GROUP_3]
+    ])('ESLShareConfig.prototype.getGroup resolves existing group %s', (name: string, group: any) => {
+      expect(instance.getGroup(name)).toEqual(group);
+    });
+
+    test.each([
+      '', 'group0', 'sn1'
+    ])('ESLShareConfig.prototype.getGroup does not resolves non-existing group %s', (name: string) => {
+      expect(instance.getGroup(name)).toBeUndefined();
+    });
+
+    test('ESLShareConfig.prototype.get(\'all\') resolves all buttons', () => {
+      expect(instance.get('all')).toEqual(buttons);
+    });
+
+    test.each([
+      'allstart',
+      'middleallterm',
+      'endall',
+      'hello-all-1',
+      'group:all'
+    ])('ESLShareConfig.prototype.get(%s) does not resolves to all buttons as nonexistent', (name) => {
+      expect(instance.get(name)).toEqual([]);
+    });
+
+    test.each([
+      ['all', ['sn1', 'sn2', 'sn3', 'sn4', 'sn-5']],
+      ['group1', []],
+      ['group2', []],
+      ['group:group1', ['sn1', 'sn2']],
+      ['group:group2', ['sn2', 'sn3', 'sn4']],
+      ['group:group3', ['sn1', 'sn2', 'sn3', 'sn4']],
+      ['sn1', ['sn1']],
+      ['sn2', ['sn2']],
+      ['', []]
+    ])('ESLShareConfig.prototype.get resolves %s query to list %s', (name: string, list: string[]) => {
+      const result = instance.get(name);
+      expect(result.map((cfg) => cfg.name)).toEqual(list);
+      for (const res of result) {
+        expect(res).toEqual(expect.objectContaining({
+          name: expect.any(String),
+          title: expect.any(String),
+          action: expect.any(String),
+          link: expect.any(String)
+        }));
+      }
+    });
+  });
+
+  describe('ESLShareConfig resolve recursive config', () => {
+    const instance: ESLShareConfig = new (ESLShareConfig as any)();
+
+    beforeEach(() => instance.clear());
+
+    test('ESLShareConfig.prototype.get resolves group referencing itself to empty list', () => {
+      instance.append({name: 'group', list: 'group:group'});
+      expect(instance.get('group:group')).toEqual([]);
+    });
+
+    test('ESLShareConfig.prototype.get ignors non-existing groups', () => {
+      instance.append(SAMPLE_BUTTON_1);
+      instance.append({name: 'group', list: 'group:group'});
+      expect(instance.get('sn1 group:group')).toContain(SAMPLE_BUTTON_1);
+    });
+  });
+
+  describe('Setting ESLShareConfig state', () => {
+    const instance: ESLShareConfig = new (ESLShareConfig as any)();
+    beforeEach(() => instance.clear());
+
+    test('ESLShareConfig.prototype.append appends buttons', () => {
+      instance.append([SAMPLE_BUTTON_1, SAMPLE_BUTTON_2]);
+      expect(instance.buttons.length).toBe(2);
+      expect(instance.groups.length).toBe(0);
+    });
+
+    test('ESLShareConfig.prototype.append appends groups', () => {
+      instance.append([SAMPLE_GROUP_1, SAMPLE_GROUP_2]);
+      expect(instance.buttons.length).toBe(0);
+      expect(instance.groups.length).toBe(2);
+    });
+
+    test('ESLShareConfig.prototype.append does not create duplicates', () => {
+      instance.append([SAMPLE_BUTTON_1, SAMPLE_BUTTON_1]).append(SAMPLE_BUTTON_1);
+      expect(instance.buttons.length).toBe(1);
+      expect(instance.groups.length).toBe(0);
+    });
+
+    test('ESLShareConfig.prototype.append accepts single button config', () => {
+      instance.append(SAMPLE_BUTTON_1);
+      expect(instance.buttons.length).toBe(1);
+      expect(instance.groups.length).toBe(0);
+    });
+
+    test('ESLShareConfig.prototype.append accepts single group config', () => {
+      instance.append(SAMPLE_GROUP_1);
+      expect(instance.buttons.length).toBe(0);
+      expect(instance.groups.length).toBe(1);
+    });
+  });
+
+
+  describe('ESLShareConfig notify about changes', () => {
+    const instance: ESLShareConfig = new (ESLShareConfig as any)();
+    const callback = jest.fn();
+
+    beforeEach(async () => {
+      callback.mockClear();
+      instance.clear();
+      await Promise.resolve();
+      instance.addEventListener(callback);
+    });
+    afterEach(() => instance.removeEventListener(callback));
+
+    test('ESLShareConfig.prototype.append notifies about buttons change', async () => {
+      instance.addEventListener(callback);
+      expect(callback).toBeCalledTimes(0);
+      instance.append(SAMPLE_BUTTON_1);
+      expect(callback).toBeCalledTimes(0);
+      await Promise.resolve();
+      expect(callback).toBeCalledTimes(1);
+    });
+
+    test('ESLShareConfig.prototype.append notifies about groups change', async () => {
+      instance.addEventListener(callback);
+      expect(callback).toBeCalledTimes(0);
+      instance.append(SAMPLE_GROUP_1);
+      expect(callback).toBeCalledTimes(0);
+      await Promise.resolve();
+      expect(callback).toBeCalledTimes(1);
+    });
+
+    test('ESLShareConfig.prototype.append notifies about buttons change', async () => {
+      instance.append([SAMPLE_BUTTON_1, SAMPLE_BUTTON_2]);
+      await Promise.resolve();
+      expect(callback).toBeCalledTimes(1);
+    });
+
+    test('ESLShareConfig.prototype.append notifies about groups change', async () => {
+      instance.append([SAMPLE_GROUP_1, SAMPLE_GROUP_2]);
+      await Promise.resolve();
+      expect(callback).toBeCalledTimes(1);
+    });
+
+    test('ESLShareConfig.prototype.append notifies about multiple change in macro-task once', async () => {
+      instance.append(SAMPLE_BUTTON_1);
+      instance.append(SAMPLE_GROUP_1);
+      instance.append(SAMPLE_BUTTON_2);
+      instance.append(SAMPLE_GROUP_2);
+      expect(callback).toBeCalledTimes(0);
+      await Promise.resolve();
+      expect(callback).toBeCalledTimes(1);
+    });
+
+    test('ESLShareConfig.prototype.append notifies about multiple change of separate tasks', async () => {
+      instance.append(SAMPLE_BUTTON_1);
+      await Promise.resolve();
+      expect(callback).toBeCalledTimes(1);
+      await promisifyTimeout(0);
+      instance.append(SAMPLE_GROUP_1);
+      await Promise.resolve();
+      expect(callback).toBeCalledTimes(2);
+    });
+
+    test('ESLShareConfig.prototype.append notifies about config reset', async () => {
+      instance.append(SAMPLE_BUTTON_1);
+      await Promise.resolve();
+      expect(callback).toBeCalledTimes(1);
+      instance.clear();
+      await Promise.resolve();
+      expect(callback).toBeCalledTimes(2);
+    });
+  });
+
+  describe('ESLShareConfig static API', () => {
+    test('ESLShareConfig instance is a singleton', () => {
+      expect(ESLShareConfig.instance).toBeInstanceOf(ESLShareConfig);
+      expect(ESLShareConfig.instance).toBe(ESLShareConfig.instance);
+    });
+
+    test('ESLShareConfig.set appends a button', () => {
+      ESLShareConfig.set({buttons: [SAMPLE_BUTTON_1]});
+      expect(ESLShareConfig.instance.buttons.length).toBe(1);
+      expect(ESLShareConfig.instance.groups.length).toBe(0);
+    });
+
+    test('ESLShareConfig.set appends a group', () => {
+      ESLShareConfig.set({groups: [SAMPLE_GROUP_1]});
+      expect(ESLShareConfig.instance.buttons.length).toBe(1);
+      expect(ESLShareConfig.instance.groups.length).toBe(1);
+    });
+
+    test('ESLShareConfig.set accepts empty config', () => {
+      ESLShareConfig.set({});
+      expect(ESLShareConfig.instance.buttons.length).toBe(1);
+      expect(ESLShareConfig.instance.groups.length).toBe(1);
+    });
+
+    test('ESLShareConfig.set accepts provider function', () => {
+      ESLShareConfig.set(() => ({buttons: [SAMPLE_BUTTON_2]}));
+      expect(ESLShareConfig.instance.buttons.length).toBe(2);
+      expect(ESLShareConfig.instance.groups.length).toBe(1);
+    });
+
+    test('ESLShareConfig.set accepts provider promise', async () => {
+      const promise = Promise.resolve({buttons: [SAMPLE_BUTTON_3]});
+      ESLShareConfig.set(promise);
+      await promise;
+      expect(ESLShareConfig.instance.buttons.length).toBe(3);
+      expect(ESLShareConfig.instance.groups.length).toBe(1);
+    });
+  });
+});