Skip to content

Commit

Permalink
Merge pull request #16766 from ckeditor/ck/16311-core-2
Browse files Browse the repository at this point in the history
Feature (ui): Introduced nested menu component for dropdowns. Closes #6399.
  • Loading branch information
scofalik authored Aug 14, 2024
2 parents 5c870eb + 1dea3c4 commit 4a43c56
Show file tree
Hide file tree
Showing 50 changed files with 3,452 additions and 84 deletions.
48 changes: 48 additions & 0 deletions docs/framework/architecture/ui-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,54 @@ toolbarDropdown.render();
document.getElementById( 'toolbar-button' ).append( toolbarDropdown.element );
```

### Menu

Finally, you can add a multi-level menu to a dropdown. Use the {@link module:ui/dropdown/utils~addMenuToDropdown `addMenuToDropdown()`} helper function to simplify the process.

```js
import {
addMenuToDropdown,
createDropdown
} from 'ckeditor5';

const locale = new Locale(); // Can be `editor.locale`.
const body = new BodyCollection(); // Can be `editor.ui.view.body`.

const menuDropdown = createDropdown( locale );

// The menu items definitions.
const definition = [
{
id: 'menu_1',
menu: 'Menu 1',
children: [
{
id: 'menu_1_a',
label: 'Item A'
},
{
id: 'menu_1_b',
label: 'Item B'
}
]
},
{
id: 'top_a',
label: 'Top Item A'
},
{
id: 'top_b',
label: 'Top Item B'
}
];

addMenuToDropdown( menuDropdown, body, definition );

menuDropdown.render();

document.getElementById( 'menu-dropdown' ).append( menuDropdown.element );
```

### Split button

Besides the standard button, you can also use the split button for a dropdown. It has two clickable sections: one for the main action and a second for expanding the dropdown with more options.
Expand Down
61 changes: 55 additions & 6 deletions docs/framework/architecture/ui-library.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,10 +310,11 @@ The button can be either:

The dropdown panel exposes its {@link module:ui/dropdown/dropdownpanelview~DropdownPanelView#children children} collection which aggregates the child {@link module:ui/view~View views}. The most common views displayed in the dropdown panel are:

* {@link module:ui/list/listview~ListView}
* {@link module:ui/toolbar/toolbarview~ToolbarView}
* {@link module:ui/list/listview~ListView} - dropdown list
* {@link module:ui/toolbar/toolbarview~ToolbarView} - dropdown toolbar
* {@link module:ui/dropdown/menu/dropdownmenurootlistview~DropdownMenuRootListView} - dropdown menu

The framework provides a set of helpers to make the dropdown creation process easier. It is still possible to compose a custom dropdown from scratch using the base classes.
The framework provides a set of helpers to make the dropdown creation process easier. It is still possible to compose a custom dropdown from scratch using the base classes. However, for most needs, we highly recommend using provided helper functions.

The {@link module:ui/dropdown/utils~createDropdown} helper creates a {@link module:ui/dropdown/dropdownview~DropdownView} with either a {@link module:ui/button/buttonview~ButtonView} or a {@link module:ui/dropdown/button/splitbuttonview~SplitButtonView}.

Expand Down Expand Up @@ -428,7 +429,6 @@ A {@link module:ui/toolbar/toolbarview~ToolbarView} can be added to a dropdown u
```js
import { ButtonView, SplitButtonView, addToolbarToDropdown, createDropdown } from 'ckeditor5';


const buttons = [];

// Add a simple button to the array of toolbar items.
Expand All @@ -452,6 +452,55 @@ dropdownView.bind( 'isEnabled' ).toMany( buttons, 'isEnabled',
);
```

#### Adding a menu to a dropdown

A multi-level menu can be added to a dropdown using the {@link module:ui/dropdown/utils~addMenuToDropdown} helper.

```js
import { addMenuToDropdown, createDropdown } from 'ckeditor5';

// The default dropdown.
const dropdownView = createDropdown( editor.locale );

// The menu items definitions.
const definition = [
{
id: 'menu_1',
menu: 'Menu 1',
children: [
{
id: 'menu_1_a',
label: 'Item A'
},
{
id: 'menu_1_b',
label: 'Item B'
}
]
},
{
id: 'top_a',
label: 'Top Item A'
},
{
id: 'top_b',
label: 'Top Item B'
}
];

addMenuToDropdown( dropdownView, editor.body.ui.view, definition );
```

Most probably you will want to perform some action when one of the defined buttons is pressed:

```js
dropdownView.on( 'execute', evt => {
const id = evt.source.id;

console.log( id ); // E.g. will print "menu_1_a" when "Item A" is pressed.
} );
```

### Dialogs and modals

The framework provides the UI dialog component. The dialog system in CKEditor 5 is brought by the {@link module:ui/dialog/dialog~Dialog `Dialog` plugin}. It offers API for displaying [views](#views) in dialogs. In a sense, this plugin corresponds to another one that manages views in balloons (popovers) across the UI ({@link module:ui/panel/balloon/contextualballoon~ContextualBalloon `ContextualBalloon` plugin}).
Expand Down Expand Up @@ -589,7 +638,7 @@ editor.plugins.get( 'Dialog' ).show( {
}
},
{
label: 'This button will be enabled in 5...'
label: 'This button will be enabled in 5...',
withText: true,
onCreate: buttonView => {
buttonView.isEnabled = false;
Expand Down Expand Up @@ -745,7 +794,7 @@ You can also pass such code directly in the `show()` method call in the `onShow`
```js
editor.plugins.get( 'Dialog' ).show( {
onShow: dialog => {
dialog.view!.on( 'close', ( evt, data ) => {
dialog.view.on( 'close', ( evt, data ) => {
if ( data.source === 'escKeyPress' ) {
evt.stop();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

@import "../../../../mixins/_disabled.css";
@import "../../../mixins/_button.css";
@import "@ckeditor/ckeditor5-ui/theme/mixins/_dir.css";

/*
* All menu buttons.
*/
.ck.ck-dropdown-menu-list__nested-menu__button {
width: 100%;
padding: var(--ck-list-button-padding);
border-radius: 0;

&:focus {
border-color: transparent;
box-shadow: none;

&:not(.ck-on) {
background: var(--ck-color-button-default-hover-background);
}
}

& > .ck-button__label {
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
}

&.ck-disabled > .ck-button__label {
@mixin ck-disabled;
}

/* Spacing in buttons that miss the icon. */
&.ck-icon-spacing:not(:has(.ck-button__icon)) > .ck-button__label {
margin-left: calc(var(--ck-icon-size) - var(--ck-spacing-small));
}

& > .ck-dropdown-menu-list__nested-menu__button__arrow {
width: var(--ck-dropdown-arrow-size);

@mixin ck-dir ltr {
transform: rotate(-90deg);
}

@mixin ck-dir rtl {
transform: rotate(90deg);
}
}

&.ck-disabled > .ck-dropdown-menu-list__nested-menu__button__arrow {
@mixin ck-disabled;
}

@mixin ck-dir ltr {
&:not(.ck-button_with-text) {
padding-left: var(--ck-spacing-small);
}

& > .ck-dropdown-menu-list__nested-menu__button__arrow {
right: var(--ck-spacing-standard);

/* A space to accommodate the triangle. */
margin-left: var(--ck-spacing-standard);
}
}

@mixin ck-dir rtl {
&:not(.ck-button_with-text) {
padding-right: var(--ck-spacing-small);
}

& > .ck-dropdown-menu-list__nested-menu__button__arrow {
left: var(--ck-spacing-standard);

/* A space to accommodate the triangle. */
margin-right: var(--ck-spacing-small);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

:root {
--ck-dropdown-menu-menu-item-min-width: 18em;
}

.ck.ck-dropdown-menu-list__nested-menu__item {
min-width: var(--ck-dropdown-menu-menu-item-min-width);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/*
* List item buttons.
*/
.ck-button.ck-dropdown-menu-list__nested-menu__item__button {
border-radius: 0;

& > .ck-spinner-container,
& > .ck-spinner-container .ck-spinner {
/* These styles correspond to .ck-icon so that the spinner seamlessly replaces the icon. */
--ck-toolbar-spinner-size: 20px;
}

& > .ck-spinner-container {
/* These margins are the same as for .ck-icon. */
margin-left: calc(-1 * var(--ck-spacing-small));
margin-right: var(--ck-spacing-small);
}

/*
* Hovered items automatically get focused. Default focus styles look odd
* while moving across a huge list of items so let's get rid of them
*/
&:focus {
border-color: transparent;
box-shadow: none;

&:not(.ck-on) {
background: var(--ck-color-button-default-hover-background);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

@import "../../../../mixins/_rounded.css";
@import "../../../../mixins/_shadow.css";

:root {
--ck-dropdown-menu-menu-panel-max-width: 75vw;
}

.ck.ck-dropdown-menu__nested-menu__panel {
@mixin ck-drop-shadow;

background: var(--ck-color-dropdown-panel-background);
border: 1px solid var(--ck-color-dropdown-panel-border);
bottom: 0;
height: fit-content;
max-width: var(--ck-dropdown-menu-menu-panel-max-width);

/* Reset balloon styling */
&::after,
&::before {
display: none;
}

/* Corner border radius consistent with the button. */
&.ck-balloon-panel_es,
&.ck-balloon-panel_se {
border-top-left-radius: 0;
}

&.ck-balloon-panel_ws,
&.ck-balloon-panel_sw {
border-top-right-radius: 0;
}

&.ck-balloon-panel_en,
&.ck-balloon-panel_ne {
border-bottom-left-radius: 0;
}

&.ck-balloon-panel_wn,
&.ck-balloon-panel_nw {
border-bottom-right-radius: 0;
}

&:focus {
outline: none;
}
}
4 changes: 4 additions & 0 deletions packages/ckeditor5-theme-lark/theme/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
@import "./ckeditor5-ui/components/dropdown/listdropdown.css";
@import "./ckeditor5-ui/components/dropdown/splitbutton.css";
@import "./ckeditor5-ui/components/dropdown/toolbardropdown.css";
@import "./ckeditor5-ui/components/dropdown/menu/dropdownmenubutton.css";
@import "./ckeditor5-ui/components/dropdown/menu/dropdownmenulistitem.css";
@import "./ckeditor5-ui/components/dropdown/menu/dropdownmenulistitembutton.css";
@import "./ckeditor5-ui/components/dropdown/menu/dropdownmenupanel.css";
@import "./ckeditor5-ui/components/editorui/accessibilityhelp.css";
@import "./ckeditor5-ui/components/editorui/editorui.css";
@import "./ckeditor5-ui/components/formheader/formheader.css";
Expand Down
1 change: 1 addition & 0 deletions packages/ckeditor5-ui/lang/contexts.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"Previous": "Label for a button showing the previous thing (tab, page, etc.).",
"Editor toolbar": "Label used by assistive technologies describing a generic editor toolbar.",
"Dropdown toolbar": "Label used by assistive technologies describing a toolbar displayed inside a dropdown.",
"Dropdown menu": "Label used by assistive technologies describing a menu displayed inside a dropdown.",
"Black": "Label of a button that applies a black color in color pickers.",
"Dim grey": "Label of a button that applies a dim grey color in color pickers.",
"Grey": "Label of a button that applies a grey color in color pickers.",
Expand Down
10 changes: 6 additions & 4 deletions packages/ckeditor5-ui/src/bindings/clickoutsidehandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @module ui/bindings/clickoutsidehandler
*/

import type { DomEmitter } from '@ckeditor/ckeditor5-utils';
import type { CallbackOptions, DomEmitter } from '@ckeditor/ckeditor5-utils';

/* global document */

Expand All @@ -24,13 +24,15 @@ import type { DomEmitter } from '@ckeditor/ckeditor5-utils';
* @param options.contextElements Array of HTML elements or a callback returning an array of HTML elements
* that determine the scope of the handler. Clicking any of them or their descendants will **not** fire the callback.
* @param options.callback An action executed by the handler.
* @param options.listenerOptions Additional options for the listener (like priority).
*/
export default function clickOutsideHandler(
{ emitter, activator, callback, contextElements }: {
{ emitter, activator, callback, contextElements, listenerOptions }: {
emitter: DomEmitter;
activator: () => boolean;
contextElements: Array<HTMLElement> | ( () => Array<HTMLElement> );
contextElements: Array<Element> | ( () => Array<Element> );
callback: () => void;
listenerOptions?: CallbackOptions;
}
): void {
emitter.listenTo( document, 'mousedown', ( evt, domEvt ) => {
Expand All @@ -51,5 +53,5 @@ export default function clickOutsideHandler(
}

callback();
} );
}, listenerOptions );
}
1 change: 0 additions & 1 deletion packages/ckeditor5-ui/src/dialog/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,6 @@ export default class Dialog extends Plugin {
} );

editor.ui.view.body.add( view );
editor.ui.focusTracker.add( view.element! );
editor.keystrokes.listenTo( view.element! );

// Unless the user specified a position, modals should always be centered on the screen.
Expand Down
Loading

0 comments on commit 4a43c56

Please sign in to comment.