feat(popover): Add hint support (#2711)

* Add custom item

* Remove customcontent parameter from popover

* Tests

* Cleanup

* Cleanup

* Lint

* Cleanup

* Rename custom to html, add enum with item types

* Fix tests

* Support hint

* Rename hint content to hint

* Align hint left

* Move types and exports

* Update changelog

* Cleanup

* Add todos

* Change the way hint is disabled for mobile

* Get rid of buildItems override

* Update comment
This commit is contained in:
Tatiana Fomina 2024-05-16 15:26:25 +03:00 committed by GitHub
parent 50f43bb35d
commit d18eeb5dc8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 202 additions and 21 deletions

View file

@ -16,6 +16,8 @@
- `Improvement` - The API `blocks.convert()` now returns the new block API
- `Improvement` - The API `caret.setToBlock()` now can accept either BlockAPI or block index or block id
- `New` *Menu Config* New item type HTML
`Refactoring` Switched to Vite as Cypress bundler
`New` *Menu Config* Default and HTML items now support hints
### 2.29.1

View file

@ -0,0 +1,16 @@
import { bem } from '../../../bem';
/**
* Hint block CSS class constructor
*/
const className = bem('ce-hint');
/**
* CSS class names to be used in hint class
*/
export const css = {
root: className(),
alignedLeft: className(null, 'align-left'),
title: className('title'),
description: className('description'),
};

View file

@ -0,0 +1,10 @@
.ce-hint {
&--align-left {
text-align: left;
}
&__description {
opacity: 0.6;
margin-top: 3px;
}
}

View file

@ -0,0 +1,46 @@
import Dom from '../../../../dom';
import { css } from './hint.const';
import { HintParams } from './hint.types';
import './hint.css';
/**
* Represents the hint content component
*/
export class Hint {
/**
* Html element used to display hint content on screen
*/
private nodes: {
root: HTMLElement;
title: HTMLElement;
description?: HTMLElement;
};
/**
* Constructs the hint content instance
*
* @param params - hint content parameters
*/
constructor(params: HintParams) {
this.nodes = {
root: Dom.make('div', [css.root, css.alignedLeft]),
title: Dom.make('div', css.title, { textContent: params.title }),
};
this.nodes.root.appendChild(this.nodes.title);
if (params.description !== undefined) {
this.nodes.description = Dom.make('div', css.description, { textContent: params.description });
this.nodes.root.appendChild(this.nodes.description);
}
}
/**
* Returns the root element of the hint content
*/
public getElement(): HTMLElement {
return this.nodes.root;
}
}

View file

@ -0,0 +1,19 @@
/**
* Hint parameters
*/
export interface HintParams {
/**
* Title of the hint
*/
title: string;
/**
* Secondary text to be displayed below the title
*/
description?: string;
}
/**
* Possible hint positions
*/
export type HintPosition = 'top' | 'bottom' | 'left' | 'right';

View file

@ -0,0 +1,2 @@
export * from './hint';
export * from './hint.types';

View file

@ -2,7 +2,9 @@ import Dom from '../../../../../dom';
import { IconDotCircle, IconChevronRight } from '@codexteam/icons';
import {
PopoverItemDefaultParams as PopoverItemDefaultParams,
PopoverItemParams as PopoverItemParams
PopoverItemParams as PopoverItemParams,
PopoverItemRenderParamsMap,
PopoverItemType
} from '../popover-item.types';
import { PopoverItem } from '../popover-item';
import { css } from './popover-item-default.const';
@ -11,8 +13,9 @@ import { css } from './popover-item-default.const';
* Represents sigle popover item node
*
* @todo move nodes initialization to constructor
* @todo replace multiple make() usages with constructing separate instaces
* @todo replace multiple make() usages with constructing separate instances
* @todo split regular popover item and popover item with confirmation to separate classes
* @todo display icon on the right side of the item for rtl languages
*/
export class PopoverItemDefault extends PopoverItem {
/**
@ -72,10 +75,6 @@ export class PopoverItemDefault extends PopoverItem {
icon: null,
};
/**
* Popover item params
*/
private params: PopoverItemDefaultParams;
/**
* If item is in confirmation state, stores confirmation params such as icon, label, onActivate callback and so on
@ -86,12 +85,13 @@ export class PopoverItemDefault extends PopoverItem {
* Constructs popover item instance
*
* @param params - popover item construction params
* @param renderParams - popover item render params.
* The parameters that are not set by user via popover api but rather depend on technical implementation
*/
constructor(params: PopoverItemDefaultParams) {
constructor(private readonly params: PopoverItemDefaultParams, renderParams?: PopoverItemRenderParamsMap[PopoverItemType.Default]) {
super();
this.params = params;
this.nodes.root = this.make(params);
this.nodes.root = this.make(params, renderParams);
}
/**
@ -159,8 +159,9 @@ export class PopoverItemDefault extends PopoverItem {
* Constructs HTML element corresponding to popover item params
*
* @param params - item construction params
* @param renderParams - popover item render params
*/
private make(params: PopoverItemDefaultParams): HTMLElement {
private make(params: PopoverItemDefaultParams, renderParams?: PopoverItemRenderParamsMap[PopoverItemType.Default]): HTMLElement {
const el = Dom.make('div', css.container);
if (params.name) {
@ -197,6 +198,13 @@ export class PopoverItemDefault extends PopoverItem {
el.classList.add(css.disabled);
}
if (params.hint !== undefined && renderParams?.hint?.enabled !== false) {
this.addHint(el, {
...params.hint,
position: renderParams?.hint?.position || 'right',
});
}
return el;
}

View file

@ -1,5 +1,5 @@
import { PopoverItem } from '../popover-item';
import { PopoverItemHtmlParams } from '../popover-item.types';
import { PopoverItemHtmlParams, PopoverItemRenderParamsMap, PopoverItemType } from '../popover-item.types';
import { css } from './popover-item-html.const';
import Dom from '../../../../../dom';
@ -16,8 +16,10 @@ export class PopoverItemHtml extends PopoverItem {
* Constructs the instance
*
* @param params instance parameters
* @param renderParams popover item render params.
* The parameters that are not set by user via popover api but rather depend on technical implementation
*/
constructor(params: PopoverItemHtmlParams) {
constructor(params: PopoverItemHtmlParams, renderParams?: PopoverItemRenderParamsMap[PopoverItemType.Html]) {
super();
this.nodes = {
@ -25,6 +27,13 @@ export class PopoverItemHtml extends PopoverItem {
};
this.nodes.root.appendChild(params.element);
if (params.hint !== undefined && renderParams?.hint?.enabled !== false) {
this.addHint(this.nodes.root, {
...params.hint,
position: renderParams?.hint?.position || 'right',
});
}
}
/**

View file

@ -1,7 +1,25 @@
import * as tooltip from '../../../../utils/tooltip';
import { type HintPosition, Hint } from '../hint';
/**
* Popover item abstract class
*/
export abstract class PopoverItem {
/**
* Adds hint to the item element if hint data is provided
*
* @param itemElement - popover item root element to add hint to
* @param hintData - hint data
*/
protected addHint(itemElement: HTMLElement, hintData: { title: string, description?: string; position: HintPosition }): void {
const content = new Hint(hintData);
tooltip.onHover(itemElement, content.getElement(), {
placement: hintData.position,
hidingDelay: 100,
});
}
/**
* Returns popover item root element
*/

View file

@ -1,3 +1,5 @@
import { HintParams, HintPosition } from '../hint';
/**
* Popover item types
*/
@ -35,7 +37,12 @@ export interface PopoverItemHtmlParams {
/**
* Custom html content to be displayed in the popover
*/
element: HTMLElement
element: HTMLElement;
/**
* Hint data to be displayed on item hover
*/
hint?: HintParams;
}
/**
@ -89,6 +96,11 @@ interface PopoverItemDefaultBaseParams {
* In case of string, works like radio buttons group and highlights as inactive any other item that has same toggle key value.
*/
toggle?: boolean | string;
/**
* Hint data to be displayed on item hover
*/
hint?: HintParams;
}
/**
@ -117,7 +129,6 @@ export interface PopoverItemWithoutConfirmationParams extends PopoverItemDefault
* @param event - event that initiated item activation
*/
onActivate: (item: PopoverItemParams, event?: PointerEvent) => void;
}
@ -152,3 +163,28 @@ export type PopoverItemParams =
PopoverItemSeparatorParams |
PopoverItemHtmlParams;
/**
* Popover item render params.
* The parameters that are not set by user via popover api but rather depend on technical implementation
*/
export type PopoverItemRenderParamsMap = {
[key in PopoverItemType.Default | PopoverItemType.Html]?: {
/**
* Hint render params
*/
hint?: {
/**
* Hint position relative to the item
*/
position?: HintPosition;
/**
* If false, hint will not be rendered.
* True by default.
* Used to disable hints on mobile popover
*/
enabled: boolean;
}
};
};

View file

@ -1,4 +1,4 @@
import { PopoverItem, PopoverItemDefault, PopoverItemSeparator, PopoverItemType } from './components/popover-item';
import { PopoverItem, PopoverItemDefault, PopoverItemRenderParamsMap, PopoverItemSeparator, PopoverItemType } from './components/popover-item';
import Dom from '../../dom';
import { SearchInput, SearchInputEvent, SearchableItem } from './components/search-input';
import EventsDispatcher from '../events';
@ -39,6 +39,7 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
*/
protected search: SearchInput | undefined;
/**
* Messages that will be displayed in popover
*/
@ -51,8 +52,13 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
* Constructs the instance
*
* @param params - popover construction params
* @param itemsRenderParams - popover item render params.
* The parameters that are not set by user via popover api but rather depend on technical implementation
*/
constructor(protected readonly params: PopoverParams) {
constructor(
protected readonly params: PopoverParams,
protected readonly itemsRenderParams: PopoverItemRenderParamsMap = {}
) {
super();
this.items = this.buildItems(params.items);
@ -154,9 +160,9 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
case PopoverItemType.Separator:
return new PopoverItemSeparator();
case PopoverItemType.Html:
return new PopoverItemHtml(item);
return new PopoverItemHtml(item, this.itemsRenderParams[PopoverItemType.Html]);
default:
return new PopoverItemDefault(item);
return new PopoverItemDefault(item, this.itemsRenderParams[PopoverItemType.Default]);
}
});
}

View file

@ -12,6 +12,8 @@ import { PopoverItemHtml } from './components/popover-item/popover-item-html/pop
/**
* Desktop popover.
* On desktop devices popover behaves like a floating element. Nested popover appears at right or left side.
*
* @todo support rtl for nested popovers and search
*/
export class PopoverDesktop extends PopoverAbstract {
/**

View file

@ -3,10 +3,11 @@ import ScrollLocker from '../scroll-locker';
import { PopoverHeader } from './components/popover-header';
import { PopoverStatesHistory } from './utils/popover-states-history';
import { PopoverMobileNodes, PopoverParams } from './popover.types';
import { PopoverItemDefault, PopoverItemParams } from './components/popover-item';
import { PopoverItemDefault, PopoverItemParams, PopoverItemType } from './components/popover-item';
import { css } from './popover.const';
import Dom from '../../dom';
/**
* Mobile Popover.
* On mobile devices Popover behaves like a fixed panel at the bottom of screen. Nested item appears like "pages" with the "back" button
@ -41,7 +42,13 @@ export class PopoverMobile extends PopoverAbstract<PopoverMobileNodes> {
* @param params - popover params
*/
constructor(params: PopoverParams) {
super(params);
super(params, {
[PopoverItemType.Default]: {
hint: {
enabled: false,
},
},
});
this.nodes.overlay = Dom.make('div', [css.overlay, css.overlayHidden]);
this.nodes.popover.insertBefore(this.nodes.overlay, this.nodes.popover.firstChild);
@ -112,8 +119,8 @@ export class PopoverMobile extends PopoverAbstract<PopoverMobileNodes> {
/**
* Removes rendered popover items and header and displays new ones
*
* @param title - new popover header text
* @param items - new popover items
* @param title - new popover header text
*/
private updateItemsAndHeader(items: PopoverItemParams[], title?: string ): void {
/** Re-render header */