diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0d19eae3..bb241443 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,6 +3,7 @@ ### 2.30.1 – `New` – Block Tunes now supports nesting items +– `New` – Block Tunes now supports separator items ### 2.30.0 diff --git a/src/components/block/index.ts b/src/components/block/index.ts index a9977f2a..57631471 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -6,7 +6,7 @@ import { SanitizerConfig, ToolConfig, ToolboxConfigEntry, - PopoverItem + PopoverItemParams } from '../../../types'; import { SavedData } from '../../../types/data-formats'; @@ -614,7 +614,7 @@ export default class Block extends EventsDispatcher { * Returns data to render in tunes menu. * Splits block tunes settings into 2 groups: popover items and custom html. */ - public getTunes(): [PopoverItem[], HTMLElement] { + public getTunes(): [PopoverItemParams[], HTMLElement] { const customHtmlTunesContainer = document.createElement('div'); const tunesItems: TunesMenuConfigItem[] = []; diff --git a/src/components/utils/bem.ts b/src/components/utils/bem.ts index eea146d7..264c2bf5 100644 --- a/src/components/utils/bem.ts +++ b/src/components/utils/bem.ts @@ -13,7 +13,7 @@ const MODIFIER_DELIMITER = '--'; * @param modifier - modifier to be appended */ export function bem(blockName: string) { - return (elementName?: string, modifier?: string) => { + return (elementName?: string | null, modifier?: string) => { const className = [blockName, elementName] .filter(x => !!x) .join(ELEMENT_DELIMITER); diff --git a/src/components/utils/events.ts b/src/components/utils/events.ts index 2599f0b7..295474da 100644 --- a/src/components/utils/events.ts +++ b/src/components/utils/events.ts @@ -3,7 +3,7 @@ import { isEmpty } from '../utils'; /** * Event Dispatcher event listener */ -type Listener = (data?: Data) => void; +type Listener = (data: Data) => void; /** * Mapped type with subscriptions list diff --git a/src/components/utils/popover/components/popover-item/index.ts b/src/components/utils/popover/components/popover-item/index.ts index 09b97e0d..12c91d40 100644 --- a/src/components/utils/popover/components/popover-item/index.ts +++ b/src/components/utils/popover/components/popover-item/index.ts @@ -1,2 +1,12 @@ -export * from './popover-item'; -export * from './popover-item.const'; +import { PopoverItemDefault } from './popover-item-default/popover-item-default'; +import { PopoverItemSeparator } from './popover-item-separator/popover-item-separator'; +import { PopoverItem } from './popover-item'; + +export * from './popover-item-default/popover-item-default.const'; +export * from './popover-item.types'; + +export { + PopoverItemDefault, + PopoverItemSeparator, + PopoverItem +}; diff --git a/src/components/utils/popover/components/popover-item/popover-item.const.ts b/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.const.ts similarity index 94% rename from src/components/utils/popover/components/popover-item/popover-item.const.ts rename to src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.const.ts index 515e0428..e5929b78 100644 --- a/src/components/utils/popover/components/popover-item/popover-item.const.ts +++ b/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.const.ts @@ -1,4 +1,4 @@ -import { bem } from '../../../bem'; +import { bem } from '../../../../bem'; /** * Popover item block CSS class constructor 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 new file mode 100644 index 00000000..71cdb7b3 --- /dev/null +++ b/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts @@ -0,0 +1,318 @@ +import Dom from '../../../../../dom'; +import { IconDotCircle, IconChevronRight } from '@codexteam/icons'; +import { + PopoverItemDefaultParams as PopoverItemDefaultParams, + PopoverItemParams as PopoverItemParams +} from '../popover-item.types'; +import { PopoverItem } from '../popover-item'; +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 split regular popover item and popover item with confirmation to separate classes + */ +export class PopoverItemDefault extends PopoverItem { + /** + * True if item is disabled and hence not clickable + */ + public get isDisabled(): boolean { + return this.params.isDisabled === true; + } + + /** + * Exposes popover item toggle parameter + */ + public get toggle(): boolean | string | undefined { + return this.params.toggle; + } + + /** + * Item title + */ + public get title(): string | undefined { + return this.params.title; + } + + /** + * True if popover should close once item is activated + */ + public get closeOnActivate(): boolean | undefined { + return this.params.closeOnActivate; + } + + /** + * True if confirmation state is enabled for popover item + */ + public get isConfirmationStateEnabled(): boolean { + return this.confirmationState !== null; + } + + /** + * True if item is focused in keyboard navigation process + */ + public get isFocused(): boolean { + if (this.nodes.root === null) { + return false; + } + + return this.nodes.root.classList.contains(css.focused); + } + + /** + * Item html elements + */ + private nodes: { + root: null | HTMLElement, + icon: null | HTMLElement + } = { + root: null, + 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 + */ + private confirmationState: PopoverItemDefaultParams | null = null; + + /** + * Constructs popover item instance + * + * @param params - popover item construction params + */ + constructor(params: PopoverItemDefaultParams) { + super(); + + this.params = params; + this.nodes.root = this.make(params); + } + + /** + * Returns popover item root element + */ + public getElement(): HTMLElement | null { + return this.nodes.root; + } + + /** + * Called on popover item click + */ + public handleClick(): void { + if (this.isConfirmationStateEnabled && this.confirmationState !== null) { + this.activateOrEnableConfirmationMode(this.confirmationState); + + return; + } + + this.activateOrEnableConfirmationMode(this.params); + } + + /** + * Toggles item active state + * + * @param isActive - true if item should strictly should become active + */ + public toggleActive(isActive?: boolean): void { + this.nodes.root?.classList.toggle(css.active, isActive); + } + + /** + * Toggles item hidden state + * + * @param isHidden - true if item should be hidden + */ + public override toggleHidden(isHidden: boolean): void { + this.nodes.root?.classList.toggle(css.hidden, isHidden); + } + + /** + * Resets popover item to its original state + */ + public reset(): void { + if (this.isConfirmationStateEnabled) { + this.disableConfirmationMode(); + } + } + + /** + * Method called once item becomes focused during keyboard navigation + */ + public onFocus(): void { + this.disableSpecialHoverAndFocusBehavior(); + } + + /** + * Returns list of item children + */ + public get children(): PopoverItemParams[] { + return 'children' in this.params && this.params.children?.items !== undefined ? this.params.children.items : []; + } + + /** + * Constructs HTML element corresponding to popover item params + * + * @param params - item construction params + */ + private make(params: PopoverItemDefaultParams): HTMLElement { + const el = Dom.make('div', css.container); + + if (params.name) { + el.dataset.itemName = params.name; + } + + this.nodes.icon = Dom.make('div', [css.icon, css.iconTool], { + innerHTML: params.icon || IconDotCircle, + }); + + el.appendChild(this.nodes.icon); + + el.appendChild(Dom.make('div', css.title, { + innerHTML: params.title || '', + })); + + if (params.secondaryLabel) { + el.appendChild(Dom.make('div', css.secondaryTitle, { + textContent: params.secondaryLabel, + })); + } + + if (this.children.length > 0) { + el.appendChild(Dom.make('div', [css.icon, css.iconChevronRight], { + innerHTML: IconChevronRight, + })); + } + + if (params.isActive) { + el.classList.add(css.active); + } + + if (params.isDisabled) { + el.classList.add(css.disabled); + } + + return el; + } + + /** + * Activates confirmation mode for the item. + * + * @param newState - new popover item params that should be applied + */ + private enableConfirmationMode(newState: PopoverItemDefaultParams): void { + if (this.nodes.root === null) { + return; + } + + const params = { + ...this.params, + ...newState, + confirmation: newState.confirmation, + } as PopoverItemDefaultParams; + const confirmationEl = this.make(params); + + this.nodes.root.innerHTML = confirmationEl.innerHTML; + this.nodes.root.classList.add(css.confirmationState); + + this.confirmationState = newState; + + this.enableSpecialHoverAndFocusBehavior(); + } + + /** + * Returns item to its original state + */ + private disableConfirmationMode(): void { + if (this.nodes.root === null) { + return; + } + const itemWithOriginalParams = this.make(this.params); + + this.nodes.root.innerHTML = itemWithOriginalParams.innerHTML; + this.nodes.root.classList.remove(css.confirmationState); + + this.confirmationState = null; + + this.disableSpecialHoverAndFocusBehavior(); + } + + /** + * Enables special focus and hover behavior for item in confirmation state. + * This is needed to prevent item from being highlighted as hovered/focused just after click. + */ + private enableSpecialHoverAndFocusBehavior(): void { + this.nodes.root?.classList.add(css.noHover); + this.nodes.root?.classList.add(css.noFocus); + + this.nodes.root?.addEventListener('mouseleave', this.removeSpecialHoverBehavior, { once: true }); + } + + /** + * Disables special focus and hover behavior + */ + private disableSpecialHoverAndFocusBehavior(): void { + this.removeSpecialFocusBehavior(); + this.removeSpecialHoverBehavior(); + + this.nodes.root?.removeEventListener('mouseleave', this.removeSpecialHoverBehavior); + } + + /** + * Removes class responsible for special focus behavior on an item + */ + private removeSpecialFocusBehavior = (): void => { + this.nodes.root?.classList.remove(css.noFocus); + }; + + /** + * Removes class responsible for special hover behavior on an item + */ + private removeSpecialHoverBehavior = (): void => { + this.nodes.root?.classList.remove(css.noHover); + }; + + /** + * Executes item's onActivate callback if the item has no confirmation configured + * + * @param item - item to activate or bring to confirmation mode + */ + private activateOrEnableConfirmationMode(item: PopoverItemDefaultParams): void { + if (item.confirmation === undefined) { + try { + item.onActivate?.(item); + this.disableConfirmationMode(); + } catch { + this.animateError(); + } + } else { + this.enableConfirmationMode(item.confirmation); + } + } + + /** + * Animates item which symbolizes that error occured while executing 'onActivate()' callback + */ + private animateError(): void { + if (this.nodes.icon?.classList.contains(css.wobbleAnimation)) { + return; + } + + this.nodes.icon?.classList.add(css.wobbleAnimation); + + this.nodes.icon?.addEventListener('animationend', this.onErrorAnimationEnd); + } + + /** + * Handles finish of error animation + */ + private onErrorAnimationEnd = (): void => { + this.nodes.icon?.classList.remove(css.wobbleAnimation); + this.nodes.icon?.removeEventListener('animationend', this.onErrorAnimationEnd); + }; +} diff --git a/src/components/utils/popover/components/popover-item/popover-item-separator/popover-item-separator.const.ts b/src/components/utils/popover/components/popover-item/popover-item-separator/popover-item-separator.const.ts new file mode 100644 index 00000000..386f686a --- /dev/null +++ b/src/components/utils/popover/components/popover-item/popover-item-separator/popover-item-separator.const.ts @@ -0,0 +1,15 @@ +import { bem } from '../../../../bem'; + +/** + * Popover separator block CSS class constructor + */ +const className = bem('ce-popover-item-separator'); + +/** + * CSS class names to be used in popover separator class + */ +export const css = { + container: className(), + line: className('line'), + hidden: className(null, 'hidden'), +}; diff --git a/src/components/utils/popover/components/popover-item/popover-item-separator/popover-item-separator.ts b/src/components/utils/popover/components/popover-item/popover-item-separator/popover-item-separator.ts new file mode 100644 index 00000000..4e091c1a --- /dev/null +++ b/src/components/utils/popover/components/popover-item/popover-item-separator/popover-item-separator.ts @@ -0,0 +1,43 @@ +import Dom from '../../../../../dom'; +import { PopoverItem } from '../popover-item'; +import { css } from './popover-item-separator.const'; + +/** + * Represents popover separator node + */ +export class PopoverItemSeparator extends PopoverItem { + /** + * Html elements + */ + private nodes: { root: HTMLElement; line: HTMLElement }; + + /** + * Constructs the instance + */ + constructor() { + super(); + + this.nodes = { + root: Dom.make('div', css.container), + line: Dom.make('div', css.line), + }; + + this.nodes.root.appendChild(this.nodes.line); + } + + /** + * Returns popover separator root element + */ + public getElement(): HTMLElement { + return this.nodes.root; + } + + /** + * Toggles item hidden state + * + * @param isHidden - true if item should be hidden + */ + public toggleHidden(isHidden: boolean): void { + this.nodes.root?.classList.toggle(css.hidden, isHidden); + } +} 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 5c72669b..b0eb95d7 100644 --- a/src/components/utils/popover/components/popover-item/popover-item.ts +++ b/src/components/utils/popover/components/popover-item/popover-item.ts @@ -1,312 +1,16 @@ -import Dom from '../../../../dom'; -import { IconDotCircle, IconChevronRight } from '@codexteam/icons'; -import { PopoverItem as PopoverItemParams } from '../../../../../../types'; -import { css } from './popover-item.const'; - /** - * Represents sigle popover item node - * - * @todo move nodes initialization to constructor - * @todo replace multiple make() usages with constructing separate instaces - * @todo split regular popover item and popover item with confirmation to separate classes + * Popover item abstract class */ -export class PopoverItem { - /** - * True if item is disabled and hence not clickable - */ - public get isDisabled(): boolean { - return this.params.isDisabled === true; - } - - /** - * Exposes popover item toggle parameter - */ - public get toggle(): boolean | string | undefined { - return this.params.toggle; - } - - /** - * Item title - */ - public get title(): string | undefined { - return this.params.title; - } - - /** - * True if popover should close once item is activated - */ - public get closeOnActivate(): boolean | undefined { - return this.params.closeOnActivate; - } - - /** - * True if confirmation state is enabled for popover item - */ - public get isConfirmationStateEnabled(): boolean { - return this.confirmationState !== null; - } - - /** - * True if item is focused in keyboard navigation process - */ - public get isFocused(): boolean { - if (this.nodes.root === null) { - return false; - } - - return this.nodes.root.classList.contains(css.focused); - } - - /** - * Item html elements - */ - private nodes: { - root: null | HTMLElement, - icon: null | HTMLElement - } = { - root: null, - icon: null, - }; - - /** - * Popover item params - */ - private params: PopoverItemParams; - - /** - * If item is in confirmation state, stores confirmation params such as icon, label, onActivate callback and so on - */ - private confirmationState: PopoverItemParams | null = null; - - /** - * Constructs popover item instance - * - * @param params - popover item construction params - */ - constructor(params: PopoverItemParams) { - this.params = params; - this.nodes.root = this.make(params); - } - +export abstract class PopoverItem { /** * Returns popover item root element */ - public getElement(): HTMLElement | null { - return this.nodes.root; - } - - /** - * Called on popover item click - */ - public handleClick(): void { - if (this.isConfirmationStateEnabled && this.confirmationState !== null) { - this.activateOrEnableConfirmationMode(this.confirmationState); - - return; - } - - this.activateOrEnableConfirmationMode(this.params); - } - - /** - * Toggles item active state - * - * @param isActive - true if item should strictly should become active - */ - public toggleActive(isActive?: boolean): void { - this.nodes.root?.classList.toggle(css.active, isActive); - } + public abstract getElement(): HTMLElement | null; /** * Toggles item hidden state * * @param isHidden - true if item should be hidden */ - public toggleHidden(isHidden: boolean): void { - this.nodes.root?.classList.toggle(css.hidden, isHidden); - } - - /** - * Resets popover item to its original state - */ - public reset(): void { - if (this.isConfirmationStateEnabled) { - this.disableConfirmationMode(); - } - } - - /** - * Method called once item becomes focused during keyboard navigation - */ - public onFocus(): void { - this.disableSpecialHoverAndFocusBehavior(); - } - - /** - * Returns list of item children - */ - public get children(): PopoverItemParams[] { - return 'children' in this.params && this.params.children?.items !== undefined ? this.params.children.items : []; - } - - /** - * Constructs HTML element corresponding to popover item params - * - * @param params - item construction params - */ - private make(params: PopoverItemParams): HTMLElement { - const el = Dom.make('div', css.container); - - if (params.name) { - el.dataset.itemName = params.name; - } - - this.nodes.icon = Dom.make('div', [css.icon, css.iconTool], { - innerHTML: params.icon || IconDotCircle, - }); - - el.appendChild(this.nodes.icon); - - el.appendChild(Dom.make('div', css.title, { - innerHTML: params.title || '', - })); - - if (params.secondaryLabel) { - el.appendChild(Dom.make('div', css.secondaryTitle, { - textContent: params.secondaryLabel, - })); - } - - if (this.children.length > 0) { - el.appendChild(Dom.make('div', [css.icon, css.iconChevronRight], { - innerHTML: IconChevronRight, - })); - } - - if (params.isActive) { - el.classList.add(css.active); - } - - if (params.isDisabled) { - el.classList.add(css.disabled); - } - - return el; - } - - /** - * Activates confirmation mode for the item. - * - * @param newState - new popover item params that should be applied - */ - private enableConfirmationMode(newState: PopoverItemParams): void { - if (this.nodes.root === null) { - return; - } - - const params = { - ...this.params, - ...newState, - confirmation: newState.confirmation, - } as PopoverItemParams; - const confirmationEl = this.make(params); - - this.nodes.root.innerHTML = confirmationEl.innerHTML; - this.nodes.root.classList.add(css.confirmationState); - - this.confirmationState = newState; - - this.enableSpecialHoverAndFocusBehavior(); - } - - /** - * Returns item to its original state - */ - private disableConfirmationMode(): void { - if (this.nodes.root === null) { - return; - } - const itemWithOriginalParams = this.make(this.params); - - this.nodes.root.innerHTML = itemWithOriginalParams.innerHTML; - this.nodes.root.classList.remove(css.confirmationState); - - this.confirmationState = null; - - this.disableSpecialHoverAndFocusBehavior(); - } - - /** - * Enables special focus and hover behavior for item in confirmation state. - * This is needed to prevent item from being highlighted as hovered/focused just after click. - */ - private enableSpecialHoverAndFocusBehavior(): void { - this.nodes.root?.classList.add(css.noHover); - this.nodes.root?.classList.add(css.noFocus); - - this.nodes.root?.addEventListener('mouseleave', this.removeSpecialHoverBehavior, { once: true }); - } - - /** - * Disables special focus and hover behavior - */ - private disableSpecialHoverAndFocusBehavior(): void { - this.removeSpecialFocusBehavior(); - this.removeSpecialHoverBehavior(); - - this.nodes.root?.removeEventListener('mouseleave', this.removeSpecialHoverBehavior); - } - - /** - * Removes class responsible for special focus behavior on an item - */ - private removeSpecialFocusBehavior = (): void => { - this.nodes.root?.classList.remove(css.noFocus); - }; - - /** - * Removes class responsible for special hover behavior on an item - */ - private removeSpecialHoverBehavior = (): void => { - this.nodes.root?.classList.remove(css.noHover); - }; - - /** - * Executes item's onActivate callback if the item has no confirmation configured - * - * @param item - item to activate or bring to confirmation mode - */ - private activateOrEnableConfirmationMode(item: PopoverItemParams): void { - if (item.confirmation === undefined) { - try { - item.onActivate?.(item); - this.disableConfirmationMode(); - } catch { - this.animateError(); - } - } else { - this.enableConfirmationMode(item.confirmation); - } - } - - /** - * Animates item which symbolizes that error occured while executing 'onActivate()' callback - */ - private animateError(): void { - if (this.nodes.icon?.classList.contains(css.wobbleAnimation)) { - return; - } - - this.nodes.icon?.classList.add(css.wobbleAnimation); - - this.nodes.icon?.addEventListener('animationend', this.onErrorAnimationEnd); - } - - /** - * Handles finish of error animation - */ - private onErrorAnimationEnd = (): void => { - this.nodes.icon?.classList.remove(css.wobbleAnimation); - this.nodes.icon?.removeEventListener('animationend', this.onErrorAnimationEnd); - }; + public abstract toggleHidden(isHidden: boolean): void; } diff --git a/types/configs/popover.d.ts b/src/components/utils/popover/components/popover-item/popover-item.types.ts similarity index 56% rename from types/configs/popover.d.ts rename to src/components/utils/popover/components/popover-item/popover-item.types.ts index ab53e521..15ea856b 100644 --- a/types/configs/popover.d.ts +++ b/src/components/utils/popover/components/popover-item/popover-item.types.ts @@ -1,7 +1,24 @@ + /** - * Common parameters for both types of popover items: with or without confirmation + * Represents popover item separator. + * Special item type that is used to separate items in the popover. */ -interface PopoverItemBase { +export interface PopoverItemSeparatorParams { + /** + * Item type + */ + type: 'separator' +} + +/** + * Common parameters for all kinds of default popover items: with or without confirmation + */ +interface PopoverItemDefaultBaseParams { + /** + * Item type + */ + type: 'default'; + /** * Displayed text */ @@ -39,8 +56,8 @@ interface PopoverItemBase { name?: string; /** - * Defines whether item should toggle on click. - * Can be represented as boolean value or a string key. + * Defines whether item should toggle on click. + * Can be represented as boolean value or a string key. * 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; @@ -49,12 +66,12 @@ interface PopoverItemBase { /** * Represents popover item with confirmation state configuration */ -export interface PopoverItemWithConfirmation extends PopoverItemBase { +export interface PopoverItemWithConfirmationParams extends PopoverItemDefaultBaseParams { /** * Popover item parameters that should be applied on item activation. * May be used to ask user for confirmation before executing popover item activation handler. */ - confirmation: PopoverItem; + confirmation: PopoverItemDefaultParams; onActivate?: never; } @@ -62,7 +79,7 @@ export interface PopoverItemWithConfirmation extends PopoverItemBase { /** * Represents popover item without confirmation state configuration */ -export interface PopoverItemWithoutConfirmation extends PopoverItemBase { +export interface PopoverItemWithoutConfirmationParams extends PopoverItemDefaultBaseParams { confirmation?: never; /** @@ -71,7 +88,7 @@ export interface PopoverItemWithoutConfirmation extends PopoverItemBase { * @param item - activated item * @param event - event that initiated item activation */ - onActivate: (item: PopoverItem, event?: PointerEvent) => void; + onActivate: (item: PopoverItemParams, event?: PointerEvent) => void; } @@ -79,7 +96,7 @@ export interface PopoverItemWithoutConfirmation extends PopoverItemBase { /** * Represents popover item with children (nested popover items) */ -export interface PopoverItemWithChildren extends PopoverItemBase { +export interface PopoverItemWithChildrenParams extends PopoverItemDefaultBaseParams { confirmation?: never; onActivate?: never; @@ -87,12 +104,20 @@ export interface PopoverItemWithChildren extends PopoverItemBase { * Items of nested popover that should be open on the current item hover/click (depending on platform) */ children?: { - items: PopoverItem[] + items: PopoverItemParams[] } } +/** + * Default, non-separator popover item type + */ +export type PopoverItemDefaultParams = + PopoverItemWithConfirmationParams | + PopoverItemWithoutConfirmationParams | + PopoverItemWithChildrenParams; + /** * Represents single popover item */ -export type PopoverItem = PopoverItemWithConfirmation | PopoverItemWithoutConfirmation | PopoverItemWithChildren +export type PopoverItemParams = PopoverItemDefaultParams | PopoverItemSeparatorParams; diff --git a/src/components/utils/popover/components/search-input/search-input.ts b/src/components/utils/popover/components/search-input/search-input.ts index 49db1061..b726ce5a 100644 --- a/src/components/utils/popover/components/search-input/search-input.ts +++ b/src/components/utils/popover/components/search-input/search-input.ts @@ -1,13 +1,14 @@ import Dom from '../../../../dom'; import Listeners from '../../../listeners'; import { IconSearch } from '@codexteam/icons'; -import { SearchableItem } from './search-input.types'; +import { SearchInputEvent, SearchInputEventMap, SearchableItem } from './search-input.types'; import { css } from './search-input.const'; +import EventsDispatcher from '../../../events'; /** * Provides search input element and search logic */ -export class SearchInput { +export class SearchInput extends EventsDispatcher { /** * Input wrapper element */ @@ -33,25 +34,19 @@ export class SearchInput { */ private searchQuery: string | undefined; - /** - * Externally passed callback for the search - */ - private readonly onSearch: (query: string, items: SearchableItem[]) => void; - /** * @param options - available config * @param options.items - searchable items list - * @param options.onSearch - search callback * @param options.placeholder - input placeholder */ - constructor({ items, onSearch, placeholder }: { + constructor({ items, placeholder }: { items: SearchableItem[]; - onSearch: (query: string, items: SearchableItem[]) => void; placeholder?: string; }) { + super(); + this.listeners = new Listeners(); this.items = items; - this.onSearch = onSearch; /** Build ui */ this.wrapper = Dom.make('div', css.wrapper); @@ -76,7 +71,10 @@ export class SearchInput { this.listeners.on(this.input, 'input', () => { this.searchQuery = this.input.value; - this.onSearch(this.searchQuery, this.foundItems); + this.emit(SearchInputEvent.Search, { + query: this.searchQuery, + items: this.foundItems, + }); }); } @@ -101,7 +99,10 @@ export class SearchInput { this.input.value = ''; this.searchQuery = ''; - this.onSearch('', this.foundItems); + this.emit(SearchInputEvent.Search, { + query: '', + items: this.foundItems, + }); } /** diff --git a/src/components/utils/popover/components/search-input/search-input.types.ts b/src/components/utils/popover/components/search-input/search-input.types.ts index bbe78f8f..ecddc47b 100644 --- a/src/components/utils/popover/components/search-input/search-input.types.ts +++ b/src/components/utils/popover/components/search-input/search-input.types.ts @@ -7,3 +7,24 @@ export interface SearchableItem { */ title?: string; } + + +/** + * Event that can be triggered by the Search Input + */ +export enum SearchInputEvent { + /** + * When search quert applied + */ + Search = 'search' +} + +/** + * Events fired by the Search Input + */ +export interface SearchInputEventMap { + /** + * Fired when search quert applied + */ + [SearchInputEvent.Search]: { query: string; items: SearchableItem[]}; +} diff --git a/src/components/utils/popover/index.ts b/src/components/utils/popover/index.ts index 6299dee9..6c2cbb26 100644 --- a/src/components/utils/popover/index.ts +++ b/src/components/utils/popover/index.ts @@ -1,6 +1,8 @@ import { PopoverDesktop } from './popover-desktop'; import { PopoverMobile } from './popover-mobile'; + export * from './popover.types'; +export * from './components/popover-item/popover-item.types'; /** * Union type for all popovers diff --git a/src/components/utils/popover/popover-abstract.ts b/src/components/utils/popover/popover-abstract.ts index c97b08d2..0191dcd6 100644 --- a/src/components/utils/popover/popover-abstract.ts +++ b/src/components/utils/popover/popover-abstract.ts @@ -1,10 +1,11 @@ -import { PopoverItem } from './components/popover-item'; +import { PopoverItem, PopoverItemDefault, PopoverItemSeparator } from './components/popover-item'; import Dom from '../../dom'; -import { SearchInput, SearchableItem } from './components/search-input'; +import { SearchInput, SearchInputEvent, SearchableItem } from './components/search-input'; import EventsDispatcher from '../events'; import Listeners from '../listeners'; import { PopoverEventMap, PopoverMessages, PopoverParams, PopoverEvent, PopoverNodes } from './popover.types'; import { css } from './popover.const'; +import { PopoverItemParams } from './components/popover-item'; /** * Class responsible for rendering popover and handling its behaviour @@ -13,7 +14,7 @@ export abstract class PopoverAbstract /** * List of popover items */ - protected items: PopoverItem[]; + protected items: Array; /** * Listeners util instance @@ -25,10 +26,18 @@ export abstract class PopoverAbstract */ protected nodes: Nodes; + /** + * List of usual interactive popover items that can be clicked, hovered, etc. + * (excluding separators) + */ + protected get itemsInteractive(): PopoverItemDefault[] { + return this.items.filter(item => item instanceof PopoverItemDefault) as PopoverItemDefault[]; + } + /** * Instance of the Search Input */ - private search: SearchInput | undefined; + protected search: SearchInput | undefined; /** * Messages that will be displayed in popover @@ -46,7 +55,7 @@ export abstract class PopoverAbstract constructor(protected readonly params: PopoverParams) { super(); - this.items = params.items.map(item => new PopoverItem(item)); + this.items = this.buildItems(params.items); if (params.messages) { this.messages = { @@ -122,7 +131,7 @@ export abstract class PopoverAbstract this.nodes.popover.classList.remove(css.popoverOpened); this.nodes.popover.classList.remove(css.popoverOpenTop); - this.items.forEach(item => item.reset()); + this.itemsInteractive.forEach(item => item.reset()); if (this.search !== undefined) { this.search.clear(); @@ -139,29 +148,28 @@ export abstract class PopoverAbstract } /** - * Handles input inside search field + * Factory method for creating popover items * - * @param query - search query text - * @param result - search results + * @param items - list of items params */ - protected onSearch = (query: string, result: SearchableItem[]): void => { - this.items.forEach(item => { - const isHidden = !result.includes(item); - - item.toggleHidden(isHidden); + protected buildItems(items: PopoverItemParams[]): Array { + return items.map(item => { + switch (item.type) { + case 'separator': + return new PopoverItemSeparator(); + default: + return new PopoverItemDefault(item); + } }); - this.toggleNothingFoundMessage(result.length === 0); - this.toggleCustomContent(query !== ''); - }; - + } /** * Retrieves popover item that is the target of the specified event * * @param event - event to retrieve popover item from */ - protected getTargetItem(event: Event): PopoverItem | undefined { - return this.items.find(el => { + protected getTargetItem(event: Event): PopoverItemDefault | undefined { + return this.itemsInteractive.find(el => { const itemEl = el.getElement(); if (itemEl === null) { @@ -172,16 +180,44 @@ export abstract class PopoverAbstract }); } + /** + * Handles input inside search field + * + * @param data - search input event data + * @param data.query - search query text + * @param data.result - search results + */ + private onSearch = (data: { query: string, items: SearchableItem[] }): void => { + const isEmptyQuery = data.query === ''; + const isNothingFound = data.items.length === 0; + + this.items + .forEach((item) => { + let isHidden = false; + + if (item instanceof PopoverItemDefault) { + isHidden = !data.items.includes(item); + } else if (item instanceof PopoverItemSeparator) { + /** Should hide separators if nothing found message displayed or if there is some search query applied */ + isHidden = isNothingFound || !isEmptyQuery; + } + item.toggleHidden(isHidden); + }); + this.toggleNothingFoundMessage(isNothingFound); + this.toggleCustomContent(isEmptyQuery); + }; + /** * Adds search to the popover */ private addSearch(): void { this.search = new SearchInput({ - items: this.items, + items: this.itemsInteractive, placeholder: this.messages.search, - onSearch: this.onSearch, }); + this.search.on(SearchInputEvent.Search, this.onSearch); + const searchElement = this.search.getElement(); searchElement.classList.add(css.search); @@ -223,7 +259,7 @@ export abstract class PopoverAbstract } /** Cleanup other items state */ - this.items.filter(x => x !== item).forEach(x => x.reset()); + this.itemsInteractive.filter(x => x !== item).forEach(x => x.reset()); item.handleClick(); @@ -260,13 +296,13 @@ export abstract class PopoverAbstract * * @param clickedItem - popover item that was clicked */ - private toggleItemActivenessIfNeeded(clickedItem: PopoverItem): void { + private toggleItemActivenessIfNeeded(clickedItem: PopoverItemDefault): void { if (clickedItem.toggle === true) { clickedItem.toggleActive(); } if (typeof clickedItem.toggle === 'string') { - const itemsInToggleGroup = this.items.filter(item => item.toggle === clickedItem.toggle); + const itemsInToggleGroup = this.itemsInteractive.filter(item => item.toggle === clickedItem.toggle); /** If there's only one item in toggle group, toggle it */ if (itemsInToggleGroup.length === 1) { @@ -287,5 +323,5 @@ export abstract class PopoverAbstract * * @param item – item to show nested popover for */ - protected abstract showNestedItems(item: PopoverItem): void; + protected abstract showNestedItems(item: PopoverItemDefault): void; } diff --git a/src/components/utils/popover/popover-desktop.ts b/src/components/utils/popover/popover-desktop.ts index df337349..8e056eaa 100644 --- a/src/components/utils/popover/popover-desktop.ts +++ b/src/components/utils/popover/popover-desktop.ts @@ -4,8 +4,9 @@ import { PopoverItem, css as popoverItemCls } from './components/popover-item'; import { PopoverParams } from './popover.types'; import { keyCodes } from '../../utils'; import { css } from './popover.const'; -import { SearchableItem } from './components/search-input'; +import { SearchInputEvent, SearchableItem } from './components/search-input'; import { cacheable } from '../../utils'; +import { PopoverItemDefault } from './components/popover-item'; /** * Desktop popover. @@ -86,6 +87,8 @@ export class PopoverDesktop extends PopoverAbstract { }); this.flipper.onFlip(this.onFlip); + + this.search?.on(SearchInputEvent.Search, this.handleSearch); } /** @@ -161,16 +164,28 @@ export class PopoverDesktop extends PopoverAbstract { } /** - * Handles input inside search field + * Handles displaying nested items for the item. * - * @param query - search query text - * @param result - search results + * @param item – item to show nested popover for */ - protected override onSearch = (query: string, result: SearchableItem[]): void => { - super.onSearch(query, result); + protected override showNestedItems(item: PopoverItemDefault): void { + if (this.nestedPopover !== null && this.nestedPopover !== undefined) { + return; + } + this.showNestedPopoverForItem(item); + } + /** + * Additionaly handles input inside search field. + * Updates flipper items considering search query applied. + * + * @param data - search event data + * @param data.query - search query text + * @param data.result - search results + */ + private handleSearch = (data: { query: string, items: SearchableItem[] }): void => { /** List of elements available for keyboard navigation considering search query applied */ - const flippableElements = query === '' ? this.flippableElements : result.map(item => (item as PopoverItem).getElement()); + const flippableElements = data.query === '' ? this.flippableElements : data.items.map(item => (item as PopoverItem).getElement()); if (this.flipper.isActivated) { /** Update flipper items with only visible */ @@ -179,18 +194,6 @@ export class PopoverDesktop extends PopoverAbstract { } }; - /** - * Handles displaying nested items for the item. - * - * @param item – item to show nested popover for - */ - protected override showNestedItems(item: PopoverItem): void { - if (this.nestedPopover !== null && this.nestedPopover !== undefined) { - return; - } - this.showNestedPopoverForItem(item); - } - /** * Checks if popover should be opened bottom. * It should happen when there is enough space below or not enough space above @@ -283,7 +286,7 @@ export class PopoverDesktop extends PopoverAbstract { * Contains both usual popover items elements and custom html content. */ private get flippableElements(): HTMLElement[] { - const popoverItemsElements = this.items.map(item => item.getElement()); + const popoverItemsElements = this.itemsInteractive.map(item => item.getElement()); const customContentControlsElements = this.customContentFlippableItems || []; /** @@ -296,7 +299,7 @@ export class PopoverDesktop extends PopoverAbstract { * Called on flipper navigation */ private onFlip = (): void => { - const focusedItem = this.items.find(item => item.isFocused); + const focusedItem = this.itemsInteractive.find(item => item.isFocused); focusedItem?.onFocus(); }; @@ -307,7 +310,7 @@ export class PopoverDesktop extends PopoverAbstract { * * @param item - item to display nested popover by */ - private showNestedPopoverForItem(item: PopoverItem): void { + private showNestedPopoverForItem(item: PopoverItemDefault): void { this.nestedPopover = new PopoverDesktop({ items: item.children, nestingLevel: this.nestingLevel + 1, diff --git a/src/components/utils/popover/popover-mobile.ts b/src/components/utils/popover/popover-mobile.ts index ac0e7ae1..5dd324d8 100644 --- a/src/components/utils/popover/popover-mobile.ts +++ b/src/components/utils/popover/popover-mobile.ts @@ -3,8 +3,7 @@ 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 { PopoverItem } from './components/popover-item'; -import { PopoverItem as PopoverItemParams } from '../../../../types'; +import { PopoverItemDefault, PopoverItemParams } from './components/popover-item'; import { css } from './popover.const'; import Dom from '../../dom'; @@ -87,7 +86,7 @@ export class PopoverMobile extends PopoverAbstract { * * @param item – item to show nested popover for */ - protected override showNestedItems(item: PopoverItem): void { + protected override showNestedItems(item: PopoverItemDefault): void { /** Show nested items */ this.updateItemsAndHeader(item.children, item.title); @@ -128,7 +127,7 @@ export class PopoverMobile extends PopoverAbstract { /** Re-render items */ this.items.forEach(item => item.getElement()?.remove()); - this.items = items.map(params => new PopoverItem(params)); + this.items = this.buildItems(items); this.items.forEach(item => { const itemEl = item.getElement(); diff --git a/src/components/utils/popover/popover.types.ts b/src/components/utils/popover/popover.types.ts index 515ec436..8b52c54e 100644 --- a/src/components/utils/popover/popover.types.ts +++ b/src/components/utils/popover/popover.types.ts @@ -1,4 +1,4 @@ -import { PopoverItem as PopoverItemParams } from '../../../../types'; +import { PopoverItemParams } from '../../../../types'; /** * Params required to render popover diff --git a/src/styles/popover.css b/src/styles/popover.css index a5982638..3a99fe16 100644 --- a/src/styles/popover.css +++ b/src/styles/popover.css @@ -194,7 +194,23 @@ /** * Popover item styles */ -.ce-popover-item { + + + .ce-popover-item-separator { + padding: 4px 3px; + + &--hidden { + display: none; + } + + &__line { + height: 1px; + background: var(--color-border); + width: 100%; + } + } + + .ce-popover-item { --border-radius: 6px; border-radius: var(--border-radius); display: flex; diff --git a/test/cypress/tests/utils/popover.cy.ts b/test/cypress/tests/utils/popover.cy.ts index 1e5f2032..7103ec71 100644 --- a/test/cypress/tests/utils/popover.cy.ts +++ b/test/cypress/tests/utils/popover.cy.ts @@ -1,5 +1,5 @@ import { PopoverDesktop as Popover } from '../../../../src/components/utils/popover'; -import { PopoverItem } from '../../../../types'; +import { PopoverItemParams } from '../../../../types'; import { TunesMenuConfig } from '../../../../types/tools'; /* eslint-disable @typescript-eslint/no-empty-function */ @@ -15,14 +15,16 @@ describe('Popover', () => { * Confirmation is moved to separate variable to be able to test it's callback execution. * (Inside popover null value is set to confirmation property, so, object becomes unavailable otherwise) */ - const confirmation = { + const confirmation: PopoverItemParams = { + type: 'default', icon: confirmActionIcon, title: confirmActionTitle, onActivate: cy.stub(), }; - const items: PopoverItem[] = [ + const items: PopoverItemParams[] = [ { + type: 'default', icon: actionIcon, title: actionTitle, name: 'testItem', @@ -69,8 +71,9 @@ describe('Popover', () => { }); it('should render the items with true isActive property value as active', () => { - const items: PopoverItem[] = [ + const items = [ { + type: 'default', icon: 'Icon', title: 'Title', isActive: true, @@ -93,8 +96,9 @@ describe('Popover', () => { }); it('should not execute item\'s onActivate callback if the item is disabled', () => { - const items: PopoverItem[] = [ + const items: PopoverItemParams[] = [ { + type: 'default', icon: 'Icon', title: 'Title', isDisabled: true, @@ -115,6 +119,9 @@ describe('Popover', () => { .should('have.class', 'ce-popover-item--disabled') .click() .then(() => { + if (items[0].type !== 'default') { + return; + } // Check onActivate callback has never been called expect(items[0].onActivate).to.have.not.been.called; }); @@ -122,8 +129,9 @@ describe('Popover', () => { }); it('should close once item with closeOnActivate property set to true is activated', () => { - const items: PopoverItem[] = [ + const items = [ { + type: 'default', icon: 'Icon', title: 'Title', closeOnActivate: true, @@ -149,8 +157,9 @@ describe('Popover', () => { }); it('should highlight as active the item with toggle property set to true once activated', () => { - const items: PopoverItem[] = [ + const items = [ { + type: 'default', icon: 'Icon', title: 'Title', toggle: true, @@ -173,8 +182,9 @@ describe('Popover', () => { }); it('should perform radiobutton-like behavior among the items that have toggle property value set to the same string value', () => { - const items: PopoverItem[] = [ + const items = [ { + type: 'default', icon: 'Icon 1', title: 'Title 1', toggle: 'group-name', @@ -183,6 +193,7 @@ describe('Popover', () => { onActivate: (): void => {}, }, { + type: 'default', icon: 'Icon 2', title: 'Title 2', toggle: 'group-name', @@ -218,8 +229,9 @@ describe('Popover', () => { }); it('should toggle item if it is the only item in toggle group', () => { - const items: PopoverItem[] = [ + const items = [ { + type: 'default', icon: 'Icon', title: 'Title', toggle: 'key', @@ -267,6 +279,7 @@ describe('Popover', () => { /** Tool data displayed in block tunes popover */ public render(): TunesMenuConfig { return { + type: 'default', icon: 'Icon', title: 'Title', toggle: 'key', @@ -274,6 +287,7 @@ describe('Popover', () => { children: { items: [ { + type: 'default', icon: 'Icon', title: 'Title', name: 'nested-test-item', @@ -343,6 +357,7 @@ describe('Popover', () => { /** Tool data displayed in block tunes popover */ public render(): TunesMenuConfig { return { + type: 'default', icon: 'Icon', title: 'Tune', toggle: 'key', @@ -350,6 +365,7 @@ describe('Popover', () => { children: { items: [ { + type: 'default', icon: 'Icon', title: 'Title', name: 'nested-test-item', @@ -441,4 +457,315 @@ describe('Popover', () => { .get('.ce-popover-header') .should('not.exist'); }); + + + it('should display default (non-separator) items without specifying type: default', () => { + /** Tool class to test how it is displayed inside block tunes popover */ + class TestTune { + public static isTune = true; + + /** Tool data displayed in block tunes popover */ + public render(): TunesMenuConfig { + return { + // @ts-expect-error type is not specified on purpose to test the back compatibility + onActivate: (): void => {}, + icon: 'Icon', + title: 'Tune', + toggle: 'key', + name: 'test-item', + }; + } + } + + + /** Create editor instance */ + cy.createEditor({ + tools: { + testTool: TestTune, + }, + tunes: [ 'testTool' ], + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: 'Hello', + }, + }, + ], + }, + }); + + /** Open block tunes menu */ + cy.get('[data-cy=editorjs]') + .get('.cdx-block') + .click(); + + cy.get('[data-cy=editorjs]') + .get('.ce-toolbar__settings-btn') + .click(); + + /** Check item displayed */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('[data-item-name="test-item"]') + .should('be.visible'); + }); + + it('should display separator', () => { + /** Tool class to test how it is displayed inside block tunes popover */ + class TestTune { + public static isTune = true; + + /** Tool data displayed in block tunes popover */ + public render(): TunesMenuConfig { + return [ + { + type: 'default', + onActivate: (): void => {}, + icon: 'Icon', + title: 'Tune', + toggle: 'key', + name: 'test-item', + }, + { + type: 'separator', + }, + ]; + } + } + + + /** Create editor instance */ + cy.createEditor({ + tools: { + testTool: TestTune, + }, + tunes: [ 'testTool' ], + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: 'Hello', + }, + }, + ], + }, + }); + + /** Open block tunes menu */ + cy.get('[data-cy=editorjs]') + .get('.cdx-block') + .click(); + + cy.get('[data-cy=editorjs]') + .get('.ce-toolbar__settings-btn') + .click(); + + /** Check item displayed */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('[data-item-name="test-item"]') + .should('be.visible'); + + /** Check separator displayed */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('.ce-popover-item-separator') + .should('be.visible'); + }); + + it('should perform keyboard navigation between items ignoring separators', () => { + /** Tool class to test how it is displayed inside block tunes popover */ + class TestTune { + public static isTune = true; + + /** Tool data displayed in block tunes popover */ + public render(): TunesMenuConfig { + return [ + { + type: 'default', + onActivate: (): void => {}, + icon: 'Icon', + title: 'Tune 1', + name: 'test-item-1', + }, + { + type: 'separator', + }, + { + type: 'default', + onActivate: (): void => {}, + icon: 'Icon', + title: 'Tune 2', + name: 'test-item-2', + }, + ]; + } + } + + /** Create editor instance */ + cy.createEditor({ + tools: { + testTool: TestTune, + }, + tunes: [ 'testTool' ], + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: 'Hello', + }, + }, + ], + }, + }); + + /** Open block tunes menu */ + cy.get('[data-cy=editorjs]') + .get('.cdx-block') + .click(); + + cy.get('[data-cy=editorjs]') + .get('.ce-toolbar__settings-btn') + .click(); + + /** Press Tab */ + cy.tab(); + + /** Check first item is focused */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('[data-item-name="test-item-1"].ce-popover-item--focused') + .should('exist'); + + /** Check second item is not focused */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('[data-item-name="test-item-2"].ce-popover-item--focused') + .should('not.exist'); + + /** Press Tab */ + cy.tab(); + + /** Check first item is not focused */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('[data-item-name="test-item-1"].ce-popover-item--focused') + .should('not.exist'); + + /** Check second item is focused */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('[data-item-name="test-item-2"].ce-popover-item--focused') + .should('exist'); + }); + + it('should perform keyboard navigation between items ignoring separators when search query is applied', () => { + /** Tool class to test how it is displayed inside block tunes popover */ + class TestTune { + public static isTune = true; + + /** Tool data displayed in block tunes popover */ + public render(): TunesMenuConfig { + return [ + { + type: 'default', + onActivate: (): void => {}, + icon: 'Icon', + title: 'Tune 1', + name: 'test-item-1', + }, + { + type: 'separator', + }, + { + type: 'default', + onActivate: (): void => {}, + icon: 'Icon', + title: 'Tune 2', + name: 'test-item-2', + }, + ]; + } + } + + /** Create editor instance */ + cy.createEditor({ + tools: { + testTool: TestTune, + }, + tunes: [ 'testTool' ], + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: 'Hello', + }, + }, + ], + }, + }); + + /** Open block tunes menu */ + cy.get('[data-cy=editorjs]') + .get('.cdx-block') + .click(); + + cy.get('[data-cy=editorjs]') + .get('.ce-toolbar__settings-btn') + .click(); + + /** Check separator displayed */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('.ce-popover-item-separator') + .should('be.visible'); + + /** Enter search query */ + cy.get('[data-cy=editorjs]') + .get('[data-cy=block-tunes] .cdx-search-field__input') + .type('Tune'); + + /** Check separator not displayed */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('.ce-popover-item-separator') + .should('not.be.visible'); + + /** Press Tab */ + // eslint-disable-next-line cypress/require-data-selectors -- cy.tab() not working here + cy.get('body').tab(); + + /** Check first item is focused */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('[data-item-name="test-item-1"].ce-popover-item--focused') + .should('exist'); + + /** Check second item is not focused */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('[data-item-name="test-item-2"].ce-popover-item--focused') + .should('not.exist'); + + /** Press Tab */ + // eslint-disable-next-line cypress/require-data-selectors -- cy.tab() not working here + cy.get('body').tab(); + + /** Check first item is not focused */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('[data-item-name="test-item-1"].ce-popover-item--focused') + .should('not.exist'); + + /** Check second item is focused */ + cy.get('[data-cy=editorjs]') + .get('.ce-popover__container') + .get('[data-item-name="test-item-2"].ce-popover-item--focused') + .should('exist'); + }); }); diff --git a/types/configs/index.d.ts b/types/configs/index.d.ts index 3b847a31..4468fca9 100644 --- a/types/configs/index.d.ts +++ b/types/configs/index.d.ts @@ -5,4 +5,4 @@ export * from './conversion-config'; export * from './log-levels'; export * from './i18n-config'; export * from './i18n-dictionary'; -export * from './popover' +export * from '../../src/components/utils/popover'; diff --git a/types/index.d.ts b/types/index.d.ts index c26aa223..fc38802b 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -77,10 +77,15 @@ export { Dictionary, DictValue, I18nConfig, - PopoverItem, - PopoverItemWithConfirmation, - PopoverItemWithoutConfirmation } from './configs'; + +export { + PopoverItemParams, + PopoverItemDefaultParams, + PopoverItemWithConfirmationParams, + PopoverItemWithoutConfirmationParams +} from '../src/components/utils/popover'; + export { OutputData, OutputBlockData} from './data-formats/output-data'; export { BlockId } from './data-formats/block-id'; export { BlockAPI } from './api' diff --git a/types/tools/tool-settings.d.ts b/types/tools/tool-settings.d.ts index fa26c882..79922401 100644 --- a/types/tools/tool-settings.d.ts +++ b/types/tools/tool-settings.d.ts @@ -1,6 +1,6 @@ import { ToolConfig } from './tool-config'; import { ToolConstructable, BlockToolData } from './index'; -import { PopoverItem } from '../configs'; +import { PopoverItemDefaultParams, PopoverItemSeparatorParams, PopoverItemParams } from '../configs'; /** * Tool may specify its toolbox configuration @@ -28,11 +28,10 @@ export interface ToolboxConfigEntry { data?: BlockToolData } - /** - * Represents single Tunes Menu item + * Represents single interactive (non-separator) Tunes Menu item */ -export type TunesMenuConfigItem = PopoverItem & { +export type TunesMenuConfigDefaultItem = PopoverItemDefaultParams & { /** * Tune displayed text. */ @@ -50,9 +49,19 @@ export type TunesMenuConfigItem = PopoverItem & { * Menu item parameters that should be applied on item activation. * May be used to ask user for confirmation before executing menu item activation handler. */ - confirmation?: TunesMenuConfigItem; + confirmation?: TunesMenuConfigDefaultItem; } +/** + * Represents single separator Tunes Menu item + */ +export type TunesMenuConfigSeparatorItem = PopoverItemSeparatorParams; + +/** + * Union of all Tunes Menu item types + */ +export type TunesMenuConfigItem = TunesMenuConfigDefaultItem | TunesMenuConfigSeparatorItem; + /** * Tool may specify its tunes configuration * that can contain either one or multiple entries