Skip to content

Commit c29d244

Browse files
feat(toolbox): popover adapted for mobile devices (#2004)
* 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 <[email protected]>
1 parent 2bc1427 commit c29d244

File tree

14 files changed

+243
-49
lines changed

14 files changed

+243
-49
lines changed

src/components/modules/toolbar/index.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,10 @@ export default class Toolbar extends Module<ToolbarNodes> {
181181
} {
182182
return {
183183
opened: this.toolboxInstance.opened,
184-
close: (): void => this.toolboxInstance.close(),
184+
close: (): void => {
185+
this.toolboxInstance.close();
186+
this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock);
187+
},
185188
open: (): void => {
186189
/**
187190
* Set current block to cover the case when the Toolbar showed near hovered Block but caret is set to another Block.
@@ -279,7 +282,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
279282
/**
280283
* Move Toolbar to the Top coordinate of Block
281284
*/
282-
this.nodes.wrapper.style.transform = `translate3D(0, ${Math.floor(toolbarY)}px, 0)`;
285+
this.nodes.wrapper.style.top = `${Math.floor(toolbarY)}px`;
283286

284287
/**
285288
* Plus Button should be shown only for __empty__ __default__ block
@@ -506,6 +509,15 @@ export default class Toolbar extends Module<ToolbarNodes> {
506509
* Subscribe to the 'block-hovered' event
507510
*/
508511
this.eventsDispatcher.on(this.Editor.UI.events.blockHovered, (data: {block: Block}) => {
512+
/**
513+
* Do not move Toolbar by hover on mobile view
514+
*
515+
* @see https://github.com/codex-team/editor.js/issues/1972
516+
*/
517+
if (_.isMobile()) {
518+
return;
519+
}
520+
509521
/**
510522
* Do not move toolbar if Block Settings or Toolbox opened
511523
*/

src/components/ui/toolbox.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import BlockTool from '../tools/block';
55
import ToolsCollection from '../tools/collection';
66
import { API } from '../../../types';
77
import EventsDispatcher from '../utils/events';
8-
import Popover from '../utils/popover';
8+
import Popover, { PopoverEvent } from '../utils/popover';
99

1010
/**
1111
* @todo check small tools number — there should not be a scroll
@@ -136,6 +136,7 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
136136
return {
137137
icon: tool.toolbox.icon,
138138
label: tool.toolbox.title,
139+
name: tool.name,
139140
onClick: (item): void => {
140141
this.toolButtonActivated(tool.name);
141142
},
@@ -144,6 +145,10 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
144145
}),
145146
});
146147

148+
this.popover.on(PopoverEvent.OverlayClicked, () => {
149+
this.close();
150+
});
151+
147152
/**
148153
* Enable tools shortcuts
149154
*/

src/components/utils.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,3 +762,10 @@ export function cacheable<Target, Value, Arguments extends unknown[] = unknown[]
762762

763763
return descriptor;
764764
};
765+
766+
/**
767+
* True if screen has mobile size
768+
*/
769+
export function isMobile(): boolean {
770+
return window.matchMedia('(max-width: 650px)').matches;
771+
}

src/components/utils/popover.ts

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import Dom from '../dom';
22
import Listeners from './listeners';
33
import Flipper from '../flipper';
4-
import SearchInput from "./search-input";
4+
import SearchInput from './search-input';
5+
import EventsDispatcher from './events';
6+
import { isMobile } from '../utils';
57

68
/**
79
* Describe parameters for rendering the single item of Popover
@@ -17,6 +19,12 @@ export interface PopoverItem {
1719
*/
1820
label: string;
1921

22+
/**
23+
* Item name
24+
* Used in data attributes needed for cypress tests
25+
*/
26+
name?: string;
27+
2028
/**
2129
* Additional displayed text
2230
*/
@@ -30,10 +38,20 @@ export interface PopoverItem {
3038
onClick: (item: PopoverItem) => void;
3139
}
3240

41+
/**
42+
* Event that can be triggered by the Popover
43+
*/
44+
export enum PopoverEvent {
45+
/**
46+
* When popover overlay is clicked
47+
*/
48+
OverlayClicked = 'overlay-clicked',
49+
}
50+
3351
/**
3452
* Popover is the UI element for displaying vertical lists
3553
*/
36-
export default class Popover {
54+
export default class Popover extends EventsDispatcher<PopoverEvent> {
3755
/**
3856
* Items list to be displayed
3957
*/
@@ -44,12 +62,16 @@ export default class Popover {
4462
*/
4563
private nodes: {
4664
wrapper: HTMLElement;
65+
popover: HTMLElement;
4766
items: HTMLElement;
4867
nothingFound: HTMLElement;
68+
overlay: HTMLElement;
4969
} = {
5070
wrapper: null,
71+
popover: null,
5172
items: null,
5273
nothingFound: null,
74+
overlay: null,
5375
}
5476

5577
/**
@@ -90,6 +112,9 @@ export default class Popover {
90112
itemSecondaryLabel: string;
91113
noFoundMessage: string;
92114
noFoundMessageShown: string;
115+
popoverOverlay: string;
116+
popoverOverlayHidden: string;
117+
documentScrollLocked: string;
93118
} {
94119
return {
95120
popover: 'ce-popover',
@@ -103,6 +128,9 @@ export default class Popover {
103128
itemSecondaryLabel: 'ce-popover__item-secondary-label',
104129
noFoundMessage: 'ce-popover__no-found',
105130
noFoundMessageShown: 'ce-popover__no-found--shown',
131+
popoverOverlay: 'ce-popover__overlay',
132+
popoverOverlayHidden: 'ce-popover__overlay--hidden',
133+
documentScrollLocked: 'ce-scroll-locked',
106134
};
107135
}
108136

@@ -122,6 +150,7 @@ export default class Popover {
122150
filterLabel: string;
123151
nothingFoundLabel: string;
124152
}) {
153+
super();
125154
this.items = items;
126155
this.className = className || '';
127156
this.searchable = searchable;
@@ -145,22 +174,32 @@ export default class Popover {
145174
* Shows the Popover
146175
*/
147176
public show(): void {
148-
this.nodes.wrapper.classList.add(Popover.CSS.popoverOpened);
177+
this.nodes.popover.classList.add(Popover.CSS.popoverOpened);
178+
this.nodes.overlay.classList.remove(Popover.CSS.popoverOverlayHidden);
149179
this.flipper.activate();
150180

151181
if (this.searchable) {
152182
window.requestAnimationFrame(() => {
153183
this.search.focus();
154184
});
155185
}
186+
187+
if (isMobile()) {
188+
document.documentElement.classList.add(Popover.CSS.documentScrollLocked);
189+
}
156190
}
157191

158192
/**
159193
* Hides the Popover
160194
*/
161195
public hide(): void {
162-
this.nodes.wrapper.classList.remove(Popover.CSS.popoverOpened);
196+
this.nodes.popover.classList.remove(Popover.CSS.popoverOpened);
197+
this.nodes.overlay.classList.add(Popover.CSS.popoverOverlayHidden);
163198
this.flipper.deactivate();
199+
200+
if (isMobile()) {
201+
document.documentElement.classList.remove(Popover.CSS.documentScrollLocked);
202+
}
164203
}
165204

166205
/**
@@ -181,32 +220,40 @@ export default class Popover {
181220
* Makes the UI
182221
*/
183222
private render(): void {
184-
this.nodes.wrapper = Dom.make('div', [Popover.CSS.popover, this.className]);
223+
this.nodes.wrapper = Dom.make('div', this.className);
224+
this.nodes.popover = Dom.make('div', Popover.CSS.popover);
225+
this.nodes.wrapper.appendChild(this.nodes.popover);
226+
227+
this.nodes.overlay = Dom.make('div', [Popover.CSS.popoverOverlay, Popover.CSS.popoverOverlayHidden]);
228+
this.nodes.wrapper.appendChild(this.nodes.overlay);
185229

186230
if (this.searchable) {
187-
this.addSearch(this.nodes.wrapper);
231+
this.addSearch(this.nodes.popover);
188232
}
189233

190234
this.nodes.items = Dom.make('div', Popover.CSS.itemsWrapper);
191-
192235
this.items.forEach(item => {
193236
this.nodes.items.appendChild(this.createItem(item));
194237
});
195238

196-
this.nodes.wrapper.appendChild(this.nodes.items);
197-
this.nodes.nothingFound = Dom.make('div', [Popover.CSS.noFoundMessage], {
239+
this.nodes.popover.appendChild(this.nodes.items);
240+
this.nodes.nothingFound = Dom.make('div', [ Popover.CSS.noFoundMessage ], {
198241
textContent: this.nothingFoundLabel,
199242
});
200243

201-
this.nodes.wrapper.appendChild(this.nodes.nothingFound);
244+
this.nodes.popover.appendChild(this.nodes.nothingFound);
202245

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

206249
if (clickedItem) {
207250
this.itemClicked(clickedItem);
208251
}
209252
});
253+
254+
this.listeners.on(this.nodes.overlay, 'click', () => {
255+
this.emit(PopoverEvent.OverlayClicked);
256+
});
210257
}
211258

212259
/**
@@ -255,6 +302,8 @@ export default class Popover {
255302
*/
256303
private createItem(item: PopoverItem): HTMLElement {
257304
const el = Dom.make('div', Popover.CSS.item);
305+
306+
el.setAttribute('data-item-name', item.name);
258307
const label = Dom.make('div', Popover.CSS.itemLabel, {
259308
innerHTML: item.label,
260309
});

src/components/utils/search-input.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Dom from '../dom';
22
import Listeners from './listeners';
3-
import $ from "../dom";
3+
import $ from '../dom';
44

55
/**
66
* Item that could be searched
@@ -13,7 +13,6 @@ interface SearchableItem {
1313
* Provides search input element and search logic
1414
*/
1515
export default class SearchInput {
16-
1716
private wrapper: HTMLElement;
1817
private input: HTMLInputElement;
1918
private listeners: Listeners;
@@ -26,10 +25,12 @@ export default class SearchInput {
2625
*/
2726
private static get CSS(): {
2827
input: string;
28+
icon: string;
2929
wrapper: string;
3030
} {
3131
return {
3232
wrapper: 'cdx-search-field',
33+
icon: 'cdx-search-field__icon',
3334
input: 'cdx-search-field__input',
3435
};
3536
}
@@ -80,13 +81,15 @@ export default class SearchInput {
8081
*/
8182
private render(placeholder: string): void {
8283
this.wrapper = Dom.make('div', SearchInput.CSS.wrapper);
84+
const iconWrapper = Dom.make('div', SearchInput.CSS.icon);
8385
const icon = $.svg('search', 16, 16);
8486

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

89-
this.wrapper.appendChild(icon);
91+
iconWrapper.appendChild(icon);
92+
this.wrapper.appendChild(iconWrapper);
9093
this.wrapper.appendChild(this.input);
9194

9295
this.listeners.on(this.input, 'input', () => {

src/styles/animations.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,20 @@
117117
transform: translateY(0);
118118
}
119119
}
120+
121+
@keyframes panelShowingMobile {
122+
from {
123+
opacity: 0;
124+
transform: translateY(14px) scale(0.98);
125+
}
126+
127+
70% {
128+
opacity: 1;
129+
transform: translateY(-4px);
130+
}
131+
132+
to {
133+
134+
transform: translateY(0);
135+
}
136+
}

src/styles/input.css

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,29 @@ select, button, progress { max-width: 100%; }
44
.cdx-search-field {
55
background: rgba(232,232,235,0.49);
66
border: 1px solid rgba(226,226,229,0.20);
7-
border-radius: 5px;
8-
padding: 4px 7px;
9-
display: flex;
10-
align-items: center;
7+
border-radius: 6px;
8+
padding: 3px;
9+
display: grid;
10+
grid-template-columns: auto auto 1fr;
11+
grid-template-rows: auto;
1112

12-
.icon {
13-
width: 14px;
14-
height: 14px;
15-
margin-right: 17px;
16-
margin-left: 2px;
17-
color: var(--grayText);
13+
&__icon {
14+
width: var(--toolbox-buttons-size);
15+
height: var(--toolbox-buttons-size);
16+
display: flex;
17+
align-items: center;
18+
justify-content: center;
19+
margin-right: 10px;
20+
21+
.icon {
22+
width: 14px;
23+
height: 14px;
24+
color: var(--grayText);
25+
flex-shrink: 0;
26+
}
1827
}
1928

29+
2030
&__input {
2131
font-size: 14px;
2232
outline: none;

0 commit comments

Comments
 (0)