diff --git a/site/views/_includes/popup.njk b/site/views/_includes/popup.njk index 23507fc28..e96f2b170 100644 --- a/site/views/_includes/popup.njk +++ b/site/views/_includes/popup.njk @@ -1,29 +1,25 @@
-
-
-
-
- +
- +
- +
- +
diff --git a/site/views/draft/popup-scroll-parents.njk b/site/views/draft/popup-scroll-parents.njk index 84eeead9c..4a51d1cad 100644 --- a/site/views/draft/popup-scroll-parents.njk +++ b/site/views/draft/popup-scroll-parents.njk @@ -77,19 +77,15 @@ aside:
-
-
-
-
diff --git a/src/modules/esl-popup/README.md b/src/modules/esl-popup/README.md index f01790249..86c5f431e 100644 --- a/src/modules/esl-popup/README.md +++ b/src/modules/esl-popup/README.md @@ -9,24 +9,94 @@ Authors: *Dmytro Shovchko*, *Alexey Stsefanovich (ala'n)*. **ESLPopup** is a custom element used as a wrapper for content that can be displayed as a pop-up GUI element. -`ESLPopup` is based on `ESLToggleable` and works in pair with `ESLTrigger` that allows triggering ESLPopup instances state changes. +`ESLPopup` is based on `ESLToggleable` and works in pairs with `ESLTrigger` which allows triggering ESLPopup instances state changes. ESLPopup can only function correctly if it's placed directly inside the `document.body`. Therefore, when it connects to the DOM, it checks if it's in the body and, if it's not, it moves itself to the `document.body` and leaves an `ESLPopupPlaceholder` element in its place. -### ESLPopup Attributes | Properties: +### ESLPopup -- `position` (string) - popup position relative to the trigger (currently supported: 'top', 'bottom', 'left', 'right' ) ('top' by default) +What is a popup window? It is a rectangular area with content that is displayed above the main content of the page right next to the element that activated the popup. In other words, it's just a rectangle whose position relative to the trigger element is calculated according to some rules. To use a popup element with ease, you need to know these rules, so here is a brief description of them. + +#### Popups arrow + +A popup is an element that is displayed quite often, along with an arrow that focuses on the element that triggered the popup. An arrow is a regular span element inside a popup that has a special class (see `ESLPopup Attributes` section for how to define a custom arrow class) on it. The `ESLPopup` element always checks for the presence of an arrow inside and creates it in case of its absence. So if you need a popup without an arrow, add the `disable-arrow` class to `ESLPopup` element. + +#### Popups positioning + +The position of the popup window is set relative to the element that activates it. The minimum necessary way to set the position is to specify which side of the trigger element you want the popup to be placed on. This can be done using the `position` attribute, in which you can specify one of four values: `top`, `bottom`, `left`, or `right`. + +Positioning on the page works in a two-dimensional coordinate system. So we have two axes. One of them is called the major axis. It will be the vertical axis if we specify the `top` or `bottom` position, or it will be the horizontal axis if we specify `left` or `right`. + +The other axis is called the minor axis. It is also possible to position a popup along it. The popup has a size: width or height, so we can specify a percentage offset of the popup from the center of the trigger element. For example, `0` means that the start edge of the popup is positioned opposite to the center of the trigger, `100` means the end edge of the popup is opposite the center of the trigger, and `50` means the center of the popup is opposite the center of the trigger. This is like left, right, and center for horizontal alignment, or top, bottom, and middle for vertical alignment. This way we have an additional option for setting the position of the popup. To do this, we use an attribute called `offset-arrow` in which we specify the percentage of offset. For pages with RTL text direction, the start and end edges of the popup are reversed. + +When a popup is displayed with an arrow, the arrow may extend beyond the popup when using the `0` and `100` thresholds or values close to them. To prevent this, we use the concept of margins on the edges of the popup. It sets the value in pixels using the `margin-arrow` attribute. This means that the extreme positions of the popup when positioned along the minor axis should be at a distance specified by the `margin-arrow` attribute from the edges of the popup. This way, the popup arrow will never look bad at the edge positions. + +Finally, the last parameter that directly affects the positioning of the popup is the distance between the edge of the popup and the edge of the trigger element. The `offsetTrigger` property of the popup parameters determines this distance. By default, it is 3 pixels. To set your value, you must define the `default-params` attribute with the value `"{offsetTrigger: your_value}"` on the popup element. + +#### Behavioral positioning of the popup + +With the positioning of the popup window, everything seems to be clear - there are features for setting the position precisely. But... + +The browser viewport has a certain size and the page author does not always have the opportunity to position the trigger element conveniently. Therefore, there is always a certain possibility that, according to all the rules of positioning, the popup will go beyond the browser viewport. In such situations, the popup should have instructions: + - whether it can go beyond the viewport; + - if not, what behavioral strategy to use to adjust the positioning rules; + - instructions for fine-tuning the behavioral positioning; + - whether the popup should observe the intersection of the viewport for the entire time it is activated. + +##### Optimistic scenario + +If there is no space where you set it, then let's assume that you just guessed wrong and all the free space is on the other side or somewhere nearby. So let's let the popup find the optimal location on its own. + +Here it is important to choose the behavioral strategy that the popup should follow. It is specified by the `behavior` attribute. Possible values: `fit`, `fit-major`, `fit-minor`, `none`. + +The `fit-major` strategy instructs the popup to always try to stay within the viewport but to adjust its positioning only along the major coordinate axis. In practice, it looks like this: if you specify which side of the trigger the popup should be on, and there is no place for it, the popup seems to flip relative to the element trigger. If the position was top, it flips and becomes bottom, if it was left, it becomes right, and vice versa. + +The `fit-minor` strategy instructs the popup to always try to stay within the viewport but to adjust the positioning only along the minor coordinate axis. In practice, it looks like this: if you specify an `offset-arrow` attribute (like alignment relative to the trigger element), but the popup goes outside the viewport, the `offset-arrow` will be changed to the minimum value sufficient to keep the popup within the viewport. For example, your popup has a default setting of showing an arrow in the middle. But the trigger is near the edge of the window, so the popup will be forced to move to the left to fit in the viewport. Since the arrow is centered on the trigger, it looks like the popup arrow has moved to the right. + +The strategy called `fit` instructs the popup to adjust its positioning along both (major and minor) coordinate axes so that it always tries to stay within the viewport. This is like applying the `fit-major` and `fit-minor` strategies at the same time. + +The `none` strategy instructs the popup to do nothing to prevent it from moving outside the viewport. + +The updated position of the popup relative to the trigger after checking and applying refinement strategies is displayed in the read-only attribute `placed-at`. + +##### Pessimistic scenario + +Your popup is too large. So large that it creates problems in finding the position of the popup so that it fits all the specified criteria. What will happen and how will the popup behave? + +If the size of the popup is so large and the trigger element is positioned so inconveniently that there is not enough space along the major axis to place it. At first, the popup will try to place itself where you specify via the `position` attribute. If there is not enough space, the popup will change the `position` value to the opposite. After that, it will not check for free space, everything will remain as it is. If there is no space here, then so be it! The popup should be displayed anyway. + +If the size of the popup is inconvenient for correct placement along the minor axis (the popup arrow, or the popup itself has large margins or size), the following will happen. First, the popup will try to position itself as specified by the `offset-arrow` attribute. After that, it checks its starting side (left or top) to see if it is out of bounds. If it does, then the position is adjusted towards the end side by the amount of the overrun. At the same time, the position of the popup arrow is checked. If it goes beyond the permitted limits (popup boundaries + arrow indents), the position adjustment is canceled. After that, the same check is performed for the end side (right or bottom). Does it need to make adjustments? If it is necessary, will this adjustment break the position of the popup arrow? If the adjustment would break the popup arrow, the adjustment is canceled. The popup will be displayed as it is. + +##### Additional setup + +You can set the minimum distance between the popup and the viewport. And your popup will always be at a distance from the edge of the viewport if there is enough free space. The `offsetContainer` property of the popup parameters sets this distance. By default, it is 15 pixels. To set your value, you must define the `default-params` attribute with the value `"{offsetContainer: your_value}"` on the popup element. + +You can specify an element that will be used as the viewport. The attribute called `container` is used for this purpose. The value is an [ESLTraversingQuery](../esl-traversing-query/README.md) selector that specifies an element whose borders serve as the boundaries of the popup's visibility. By default, `window` is used. + +To prevent the popup window with the activator element (trigger) from going outside the viewport during certain events (scrolling, resizing, or something similar), the IntersectionObserver API is used. The trigger element is observed. If it has left or is about to leave (partial leaving is detected) the viewport, the popup will be deactivated and hidden. It is possible to change the area near the edges of the viewport so that the observer reacts more gently or more severely to the approach of the element trigger to the edges. This is accomplished by using the margins at the edges of the viewport. The margins are set using the `intersectionMargin` property of the popup window parameters. The default is "0px". To set your value, you must define the `default-params` attribute with the value `"{intersectionMargin: your_value}"` on the popup element. + +It is possible to disable the observation of the popup leaving the viewport. To do this, use the `disable-activator-observation` attribute. If you set this attribute on the popup, the popup will not adjust its position along the major axis (deactivating the `fit-major` behavior) and will not hide the popup when the triggered element goes outside the viewport when scrolling or resizing. + +#### A few useful customization options + +It is possible to configure so that when a popup is activated and displayed, the focus is transferred to it, i.e. it is selected to receive an input event from the user, and when the popup is deactivated and hidden, the focus returns to the activator element (trigger). This is handled by the `autofocus` property of the popup parameters. It is disabled by default. Therefore, to enable it, define the `default-params` attribute on the popup element with the following content `"{autofocus: true}"` and the `tabindex` attribute to set in 0. + +You can also add additional classes and styles when activating and displaying the popup. This is set using the `extraClass` and `extraStyle` properties of the popup parameters. For example, on the popup element, we define the `default-params` attribute with the following content `"{extraClass: 'your-class', extraStyle: 'color: red'}"`. + +#### Attributes | Properties: + +- `position` - popup position relative to the trigger (currently supported: 'top', 'bottom', 'left', 'right' ) ('top' by default) -- `behavior` (string) - popup behavior if it does not fit in the window ('fit' by default). Available options: +- `behavior` - popup behavior if it does not fit in the window ('fit' by default). Available options: - `fit` - default, the popup will always be positioned in the right place. Position dynamically updates so it will always be visible - `fit-major` - same as fit, but position dynamically updates only on major axes. Looks like a flip in relation to the trigger - `fit-minor` - same as fit, but position dynamically updates only on minor axes. Looks like alignment to the arrow - - empty or unsupported value - will not be prevented from overflowing clipping boundaries, such as the viewport + - `none`, empty or unsupported value - will not be prevented from overflowing clipping boundaries, such as the viewport -- `container` - defines container element ([ESLTraversingQuery](../esl-traversing-query/README.md) selector) to determinate bounds of popup visibility (window by default) +- `container` - defines container element ([ESLTraversingQuery](../esl-traversing-query/README.md) selector) to determine bounds of popup visibility (window by default) - `disable-activator-observation` (boolean) - disable hiding the popup depending on the visibility of the activator @@ -38,6 +108,24 @@ it moves itself to the `document.body` and leaves an `ESLPopupPlaceholder` eleme ESLPopup extends [ESLToggleable](../esl-toggleable/README.md) you can find other supported options in its documentation. -### Readonly Attributes +#### Readonly Attributes + +- `placed-at` - popup updated position relative to the trigger. In other words, this is the real position of the popup relative to the trigger after the position update in the case when 'fit' behavior is enabled + +#### Public API + + - `$placeholder` - the placeholder element that replaces this popup that was moved to `document.body` + - `$arrow` - getter that returns popups arrow element + +### ESLPopupPlaceholder + +This is a special element that has the sole responsibility of being a substitute for the original `ESLPopup` element that has been moved inside `document.body`. That is if the backend generated a page with `ESLPopup` in a certain place and it automatically moved to `document.body`, you do not have to search for this popup because `ESLPopupPlaceholder` stores a link to the original element. + +#### Attributes | Properties: + +When you add a placeholder to the DOM tree, all attributes are copied from the original to the placeholder. But there is an exception - the `id` attribute is converted to the `original-id` attribute. + +#### Public API -- `placed-at` (string) - popup updated position relative to the trigger. In other words, this is the real position of the popup relative to the trigger after the position update in the case when 'fit' behavior is enabled + - `from` - static method that creates a placeholder for a given popup element + - `$origin` - the original element that was here before it was replaced by a placeholder diff --git a/src/modules/esl-popup/core/esl-popup.ts b/src/modules/esl-popup/core/esl-popup.ts index 2318753d7..7f269f3ac 100644 --- a/src/modules/esl-popup/core/esl-popup.ts +++ b/src/modules/esl-popup/core/esl-popup.ts @@ -100,7 +100,7 @@ export class ESLPopup extends ESLToggleable { /** Target to container element {@link ESLTraversingQuery} to define bounds of popups visibility (window by default) */ @attr() public container: string; - /** Default show/hide params for current ESLAlert instance */ + /** Default show/hide params for current ESLPopup instance */ @jsonAttr() public override defaultParams: ESLPopupActionParams; @@ -174,7 +174,7 @@ export class ESLPopup extends ESLToggleable { } /** Appends arrow to Popup */ - public appendArrow(): HTMLElement { + protected appendArrow(): HTMLElement { const $arrow = document.createElement('span'); $arrow.className = this.arrowClass; this.appendChild($arrow); diff --git a/src/modules/esl-share/README.md b/src/modules/esl-share/README.md index 9a87e6b03..4c98dead5 100644 --- a/src/modules/esl-share/README.md +++ b/src/modules/esl-share/README.md @@ -281,11 +281,15 @@ The principle of cascading is similar to CSS variables. The value is searched fr ### ESLShareConfig +#### Static API + + - `instance` - getter that returns shared instance of `ESLShareConfig` + - `append` - appends single button or group to current configuration + - `set` - updates the configuration with promise resolved to `ESLShareConfigInit` or promise provider function + - `update` - updates items configuration from the list with the specified partial config + #### Public API - - `instance` - static getter that returns shared instance of `ESLShareConfig` - - `append` - static method that appends single button or group to current configuration - - `set` - static method that updates the configuration with promise resolved to `ESLShareConfigInit` or promise provider function - `buttons` - getter that returns list of all available buttons - `groups` - getter that returns list of all available groups - `get` - selects the buttons for the given list and returns their configuration @@ -293,6 +297,7 @@ The principle of cascading is similar to CSS variables. The value is searched fr - `getButton` - gets the button configuration for the given name - `getGroup` - gets the group of buttons configuration for the given name - `append` - updates the configuration with a `ESLShareButtonConfig`(s) or `ESLShareGroupConfig`(s) + - `update` - updates items configuration from the list with the specified partial config #### Events @@ -323,15 +328,17 @@ The component is notified of any configuration changes. And, if during the check ### 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 `ESLShare` 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. +`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. + +Since an `ESLSharePopup` element is created dynamically you may need to style some individual popups differently, for example on components with dark or light backgrounds. To do this, you can define an additional class or style that will be applied to the popup when it is opened. Simply define the `popup-params` attribute in the `ESLShare` element, in which you specify the desired `extraClass` or `extraStyle` property. You can define any other popup parameters in the same way. For a complete list of available properties of popup parameters, see the [ESLPopup](../esl-popup/README.md) component documentation. #### 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 +#### Static API - - `sharedInstance` - static getter that returns shared instance of ESLSharePopup + - `sharedInstance` - getter that returns shared instance of ESLSharePopup ### ESLShare @@ -340,7 +347,7 @@ A trigger element is based on [ESLTrigger](../esl-trigger/README.md) to activate #### 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-params` - initial params to pass into popup on show action (Default: `{position: 'top', hideDelay: 220}`) + - `popup-params` - initial params to pass into popup on show action (Default: `{position: 'top', hideDelay: 220}`). For a complete list of available properties of popup parameters, see the [ESLPopup](../esl-popup/README.md) component documentation - `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`) diff --git a/src/modules/esl-share/core/esl-share-config.ts b/src/modules/esl-share/core/esl-share-config.ts index eceddba5b..53d7ce82f 100644 --- a/src/modules/esl-share/core/esl-share-config.ts +++ b/src/modules/esl-share/core/esl-share-config.ts @@ -77,9 +77,9 @@ export class ESLShareConfig extends SyntheticEventTarget { return ESLShareConfig.instance; } - /** Updates items by the name and passed changes */ - public static update(name: string, changes: Partial): ESLShareConfig { - return ESLShareConfig.instance.update(name, changes); + /** Updates items configuration from the list with the specified partial config */ + public static update(query: string, changes: Partial): ESLShareConfig { + return ESLShareConfig.instance.update(query, changes); } /** Appends single button or group to current configuration */ @@ -173,9 +173,9 @@ export class ESLShareConfig extends SyntheticEventTarget { return this; } - /** Updates items by the name and passed changes */ - public update(name: string, changes: Partial): ESLShareConfig { - for (const btn of this.get(name)) { + /** Updates items configuration from the list with the specified partial config */ + public update(query: string, changes: Partial): ESLShareConfig { + for (const btn of this.get(query)) { this.append(deepMerge({}, btn, changes)); } return this;