diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 87b30b54..90f6c361 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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 diff --git a/src/components/utils/popover/components/hint/hint.const.ts b/src/components/utils/popover/components/hint/hint.const.ts new file mode 100644 index 00000000..8b5afd77 --- /dev/null +++ b/src/components/utils/popover/components/hint/hint.const.ts @@ -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'), +}; diff --git a/src/components/utils/popover/components/hint/hint.css b/src/components/utils/popover/components/hint/hint.css new file mode 100644 index 00000000..94d886bd --- /dev/null +++ b/src/components/utils/popover/components/hint/hint.css @@ -0,0 +1,10 @@ +.ce-hint { + &--align-left { + text-align: left; + } + + &__description { + opacity: 0.6; + margin-top: 3px; + } +} diff --git a/src/components/utils/popover/components/hint/hint.ts b/src/components/utils/popover/components/hint/hint.ts new file mode 100644 index 00000000..eb91de12 --- /dev/null +++ b/src/components/utils/popover/components/hint/hint.ts @@ -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; + } +} diff --git a/src/components/utils/popover/components/hint/hint.types.ts b/src/components/utils/popover/components/hint/hint.types.ts new file mode 100644 index 00000000..ca4b878c --- /dev/null +++ b/src/components/utils/popover/components/hint/hint.types.ts @@ -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'; diff --git a/src/components/utils/popover/components/hint/index.ts b/src/components/utils/popover/components/hint/index.ts new file mode 100644 index 00000000..9e4870ab --- /dev/null +++ b/src/components/utils/popover/components/hint/index.ts @@ -0,0 +1,2 @@ +export * from './hint'; +export * from './hint.types'; diff --git a/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts b/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts index 71cdb7b3..6a5d12d0 100644 --- a/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +++ b/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts @@ -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; } diff --git a/src/components/utils/popover/components/popover-item/popover-item-html/popover-item-html.ts b/src/components/utils/popover/components/popover-item/popover-item-html/popover-item-html.ts index 138c3032..3fed7004 100644 --- a/src/components/utils/popover/components/popover-item/popover-item-html/popover-item-html.ts +++ b/src/components/utils/popover/components/popover-item/popover-item-html/popover-item-html.ts @@ -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', + }); + } } /** diff --git a/src/components/utils/popover/components/popover-item/popover-item.ts b/src/components/utils/popover/components/popover-item/popover-item.ts index b0eb95d7..a9a4713a 100644 --- a/src/components/utils/popover/components/popover-item/popover-item.ts +++ b/src/components/utils/popover/components/popover-item/popover-item.ts @@ -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 */ diff --git a/src/components/utils/popover/components/popover-item/popover-item.types.ts b/src/components/utils/popover/components/popover-item/popover-item.types.ts index 8fa8d096..c0a5501e 100644 --- a/src/components/utils/popover/components/popover-item/popover-item.types.ts +++ b/src/components/utils/popover/components/popover-item/popover-item.types.ts @@ -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; + } + }; +}; diff --git a/src/components/utils/popover/popover-abstract.ts b/src/components/utils/popover/popover-abstract.ts index 418195b1..2bb09628 100644 --- a/src/components/utils/popover/popover-abstract.ts +++ b/src/components/utils/popover/popover-abstract.ts @@ -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 */ protected search: SearchInput | undefined; + /** * Messages that will be displayed in popover */ @@ -51,8 +52,13 @@ export abstract class PopoverAbstract * 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 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]); } }); } diff --git a/src/components/utils/popover/popover-desktop.ts b/src/components/utils/popover/popover-desktop.ts index 5d6440b0..1570d2fe 100644 --- a/src/components/utils/popover/popover-desktop.ts +++ b/src/components/utils/popover/popover-desktop.ts @@ -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 { /** diff --git a/src/components/utils/popover/popover-mobile.ts b/src/components/utils/popover/popover-mobile.ts index c309b95c..2bf849ee 100644 --- a/src/components/utils/popover/popover-mobile.ts +++ b/src/components/utils/popover/popover-mobile.ts @@ -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 { * @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 { /** * 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 */