Skip to content

Commit

Permalink
feat(toolbox): popover adapted for mobile devices (#2004)
Browse files Browse the repository at this point in the history
* FIx mobile popover fixed positioning

* Add mobile popover overlay

* Hide mobile popover on scroll

* Alter toolbox buttons hover

* Fix closing popover on overlay click

* Tests fix

* Fix onchange test

* restore focus after toolbox closing by ESC

* don't move toolbar by block-hover on mobile

Resolves #1972

* popover mobile styles improved

* Cleanup

* Remove scroll event listener

* Lock scroll on mobile

* don't show shortcuts in mobile popover

* Change data attr name

* Remove unused styles

* Remove unused listeners

* disable hover on mobile popover

* Scroll fix

* Lint

* Revert "Scroll fix"

This reverts commit 82deae5.

* Return back background color for active state of toolbox buttons

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
  • Loading branch information
TatianaFomina and neSpecc committed Apr 7, 2022
1 parent 2bc1427 commit c29d244
Show file tree
Hide file tree
Showing 14 changed files with 243 additions and 49 deletions.
16 changes: 14 additions & 2 deletions src/components/modules/toolbar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,10 @@ export default class Toolbar extends Module<ToolbarNodes> {
} {
return {
opened: this.toolboxInstance.opened,
close: (): void => this.toolboxInstance.close(),
close: (): void => {
this.toolboxInstance.close();
this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock);
},
open: (): void => {
/**
* Set current block to cover the case when the Toolbar showed near hovered Block but caret is set to another Block.
Expand Down Expand Up @@ -279,7 +282,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
/**
* Move Toolbar to the Top coordinate of Block
*/
this.nodes.wrapper.style.transform = `translate3D(0, ${Math.floor(toolbarY)}px, 0)`;
this.nodes.wrapper.style.top = `${Math.floor(toolbarY)}px`;

/**
* Plus Button should be shown only for __empty__ __default__ block
Expand Down Expand Up @@ -506,6 +509,15 @@ export default class Toolbar extends Module<ToolbarNodes> {
* Subscribe to the 'block-hovered' event
*/
this.eventsDispatcher.on(this.Editor.UI.events.blockHovered, (data: {block: Block}) => {
/**
* Do not move Toolbar by hover on mobile view
*
* @see https://github.com/codex-team/editor.js/issues/1972
*/
if (_.isMobile()) {
return;
}

/**
* Do not move toolbar if Block Settings or Toolbox opened
*/
Expand Down
7 changes: 6 additions & 1 deletion src/components/ui/toolbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import BlockTool from '../tools/block';
import ToolsCollection from '../tools/collection';
import { API } from '../../../types';
import EventsDispatcher from '../utils/events';
import Popover from '../utils/popover';
import Popover, { PopoverEvent } from '../utils/popover';

/**
* @todo check small tools number — there should not be a scroll
Expand Down Expand Up @@ -136,6 +136,7 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
return {
icon: tool.toolbox.icon,
label: tool.toolbox.title,
name: tool.name,
onClick: (item): void => {
this.toolButtonActivated(tool.name);
},
Expand All @@ -144,6 +145,10 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
}),
});

this.popover.on(PopoverEvent.OverlayClicked, () => {
this.close();
});

/**
* Enable tools shortcuts
*/
Expand Down
7 changes: 7 additions & 0 deletions src/components/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -762,3 +762,10 @@ export function cacheable<Target, Value, Arguments extends unknown[] = unknown[]

return descriptor;
};

/**
* True if screen has mobile size
*/
export function isMobile(): boolean {
return window.matchMedia('(max-width: 650px)').matches;
}
71 changes: 60 additions & 11 deletions src/components/utils/popover.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import Dom from '../dom';
import Listeners from './listeners';
import Flipper from '../flipper';
import SearchInput from "./search-input";
import SearchInput from './search-input';
import EventsDispatcher from './events';
import { isMobile } from '../utils';

/**
* Describe parameters for rendering the single item of Popover
Expand All @@ -17,6 +19,12 @@ export interface PopoverItem {
*/
label: string;

/**
* Item name
* Used in data attributes needed for cypress tests
*/
name?: string;

/**
* Additional displayed text
*/
Expand All @@ -30,10 +38,20 @@ export interface PopoverItem {
onClick: (item: PopoverItem) => void;
}

/**
* Event that can be triggered by the Popover
*/
export enum PopoverEvent {
/**
* When popover overlay is clicked
*/
OverlayClicked = 'overlay-clicked',
}

/**
* Popover is the UI element for displaying vertical lists
*/
export default class Popover {
export default class Popover extends EventsDispatcher<PopoverEvent> {
/**
* Items list to be displayed
*/
Expand All @@ -44,12 +62,16 @@ export default class Popover {
*/
private nodes: {
wrapper: HTMLElement;
popover: HTMLElement;
items: HTMLElement;
nothingFound: HTMLElement;
overlay: HTMLElement;
} = {
wrapper: null,
popover: null,
items: null,
nothingFound: null,
overlay: null,
}

/**
Expand Down Expand Up @@ -90,6 +112,9 @@ export default class Popover {
itemSecondaryLabel: string;
noFoundMessage: string;
noFoundMessageShown: string;
popoverOverlay: string;
popoverOverlayHidden: string;
documentScrollLocked: string;
} {
return {
popover: 'ce-popover',
Expand All @@ -103,6 +128,9 @@ export default class Popover {
itemSecondaryLabel: 'ce-popover__item-secondary-label',
noFoundMessage: 'ce-popover__no-found',
noFoundMessageShown: 'ce-popover__no-found--shown',
popoverOverlay: 'ce-popover__overlay',
popoverOverlayHidden: 'ce-popover__overlay--hidden',
documentScrollLocked: 'ce-scroll-locked',
};
}

Expand All @@ -122,6 +150,7 @@ export default class Popover {
filterLabel: string;
nothingFoundLabel: string;
}) {
super();
this.items = items;
this.className = className || '';
this.searchable = searchable;
Expand All @@ -145,22 +174,32 @@ export default class Popover {
* Shows the Popover
*/
public show(): void {
this.nodes.wrapper.classList.add(Popover.CSS.popoverOpened);
this.nodes.popover.classList.add(Popover.CSS.popoverOpened);
this.nodes.overlay.classList.remove(Popover.CSS.popoverOverlayHidden);
this.flipper.activate();

if (this.searchable) {
window.requestAnimationFrame(() => {
this.search.focus();
});
}

if (isMobile()) {
document.documentElement.classList.add(Popover.CSS.documentScrollLocked);
}
}

/**
* Hides the Popover
*/
public hide(): void {
this.nodes.wrapper.classList.remove(Popover.CSS.popoverOpened);
this.nodes.popover.classList.remove(Popover.CSS.popoverOpened);
this.nodes.overlay.classList.add(Popover.CSS.popoverOverlayHidden);
this.flipper.deactivate();

if (isMobile()) {
document.documentElement.classList.remove(Popover.CSS.documentScrollLocked);
}
}

/**
Expand All @@ -181,32 +220,40 @@ export default class Popover {
* Makes the UI
*/
private render(): void {
this.nodes.wrapper = Dom.make('div', [Popover.CSS.popover, this.className]);
this.nodes.wrapper = Dom.make('div', this.className);
this.nodes.popover = Dom.make('div', Popover.CSS.popover);
this.nodes.wrapper.appendChild(this.nodes.popover);

this.nodes.overlay = Dom.make('div', [Popover.CSS.popoverOverlay, Popover.CSS.popoverOverlayHidden]);
this.nodes.wrapper.appendChild(this.nodes.overlay);

if (this.searchable) {
this.addSearch(this.nodes.wrapper);
this.addSearch(this.nodes.popover);
}

this.nodes.items = Dom.make('div', Popover.CSS.itemsWrapper);

this.items.forEach(item => {
this.nodes.items.appendChild(this.createItem(item));
});

this.nodes.wrapper.appendChild(this.nodes.items);
this.nodes.nothingFound = Dom.make('div', [Popover.CSS.noFoundMessage], {
this.nodes.popover.appendChild(this.nodes.items);
this.nodes.nothingFound = Dom.make('div', [ Popover.CSS.noFoundMessage ], {
textContent: this.nothingFoundLabel,
});

this.nodes.wrapper.appendChild(this.nodes.nothingFound);
this.nodes.popover.appendChild(this.nodes.nothingFound);

this.listeners.on(this.nodes.wrapper, 'click', (event: KeyboardEvent|MouseEvent) => {
this.listeners.on(this.nodes.popover, 'click', (event: KeyboardEvent|MouseEvent) => {
const clickedItem = (event.target as HTMLElement).closest(`.${Popover.CSS.item}`) as HTMLElement;

if (clickedItem) {
this.itemClicked(clickedItem);
}
});

this.listeners.on(this.nodes.overlay, 'click', () => {
this.emit(PopoverEvent.OverlayClicked);
});
}

/**
Expand Down Expand Up @@ -255,6 +302,8 @@ export default class Popover {
*/
private createItem(item: PopoverItem): HTMLElement {
const el = Dom.make('div', Popover.CSS.item);

el.setAttribute('data-item-name', item.name);
const label = Dom.make('div', Popover.CSS.itemLabel, {
innerHTML: item.label,
});
Expand Down
9 changes: 6 additions & 3 deletions src/components/utils/search-input.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Dom from '../dom';
import Listeners from './listeners';
import $ from "../dom";
import $ from '../dom';

/**
* Item that could be searched
Expand All @@ -13,7 +13,6 @@ interface SearchableItem {
* Provides search input element and search logic
*/
export default class SearchInput {

private wrapper: HTMLElement;
private input: HTMLInputElement;
private listeners: Listeners;
Expand All @@ -26,10 +25,12 @@ export default class SearchInput {
*/
private static get CSS(): {
input: string;
icon: string;
wrapper: string;
} {
return {
wrapper: 'cdx-search-field',
icon: 'cdx-search-field__icon',
input: 'cdx-search-field__input',
};
}
Expand Down Expand Up @@ -80,13 +81,15 @@ export default class SearchInput {
*/
private render(placeholder: string): void {
this.wrapper = Dom.make('div', SearchInput.CSS.wrapper);
const iconWrapper = Dom.make('div', SearchInput.CSS.icon);
const icon = $.svg('search', 16, 16);

this.input = Dom.make('input', SearchInput.CSS.input, {
placeholder,
}) as HTMLInputElement;

this.wrapper.appendChild(icon);
iconWrapper.appendChild(icon);
this.wrapper.appendChild(iconWrapper);
this.wrapper.appendChild(this.input);

this.listeners.on(this.input, 'input', () => {
Expand Down
17 changes: 17 additions & 0 deletions src/styles/animations.css
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,20 @@
transform: translateY(0);
}
}

@keyframes panelShowingMobile {
from {
opacity: 0;
transform: translateY(14px) scale(0.98);
}

70% {
opacity: 1;
transform: translateY(-4px);
}

to {

transform: translateY(0);
}
}
30 changes: 20 additions & 10 deletions src/styles/input.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,29 @@ select, button, progress { max-width: 100%; }
.cdx-search-field {
background: rgba(232,232,235,0.49);
border: 1px solid rgba(226,226,229,0.20);
border-radius: 5px;
padding: 4px 7px;
display: flex;
align-items: center;
border-radius: 6px;
padding: 3px;
display: grid;
grid-template-columns: auto auto 1fr;
grid-template-rows: auto;

.icon {
width: 14px;
height: 14px;
margin-right: 17px;
margin-left: 2px;
color: var(--grayText);
&__icon {
width: var(--toolbox-buttons-size);
height: var(--toolbox-buttons-size);
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;

.icon {
width: 14px;
height: 14px;
color: var(--grayText);
flex-shrink: 0;
}
}


&__input {
font-size: 14px;
outline: none;
Expand Down
Loading

0 comments on commit c29d244

Please sign in to comment.