mirror of
https://github.com/moodle/moodle.git
synced 2025-04-13 20:42:22 +02:00
MDL-78665 output: add subpanels to action menu
Many times the action menu item triggers modals to show more information to the user. In most cases this is enough, however, a modal will close the menu and the user is not able to see the modal content in the page context. To solve this now menus can define subpanels that are displayed next the the menu item when the item is focused or hover. This will be used to group options like the group mode in activities or to replace the adhoc solution implemented to select language in the user menu.
This commit is contained in:
parent
d04ef6c5e3
commit
105324e6fd
10
lib/amd/build/local/action_menu/subpanel.min.js
vendored
Normal file
10
lib/amd/build/local/action_menu/subpanel.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
lib/amd/build/local/action_menu/subpanel.min.js.map
Normal file
1
lib/amd/build/local/action_menu/subpanel.min.js.map
Normal file
File diff suppressed because one or more lines are too long
343
lib/amd/src/local/action_menu/subpanel.js
Normal file
343
lib/amd/src/local/action_menu/subpanel.js
Normal file
@ -0,0 +1,343 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Action menu subpanel JS controls.
|
||||
*
|
||||
* @module core/local/action_menu/subpanel
|
||||
* @copyright 2023 Mikel Martín <mikel@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
import jQuery from 'jquery';
|
||||
import {debounce} from 'core/utils';
|
||||
import {
|
||||
isExtraSmall,
|
||||
firstFocusableElement,
|
||||
previousFocusableElement,
|
||||
nextFocusableElement,
|
||||
} from 'core/pagehelpers';
|
||||
import Pending from 'core/pending';
|
||||
|
||||
const Selectors = {
|
||||
mainMenu: '[role="menu"]',
|
||||
dropdownRight: '.dropdown-menu-right',
|
||||
subPanel: '.dropdown-subpanel',
|
||||
subPanelMenuItem: '.dropdown-subpanel > .dropdown-item',
|
||||
subPanelContent: '.dropdown-subpanel > .dropdown-menu',
|
||||
drawer: '[data-region="fixed-drawer"]',
|
||||
};
|
||||
|
||||
const Classes = {
|
||||
dropRight: 'dropright',
|
||||
dropLeft: 'dropleft',
|
||||
dropDown: 'dropdown',
|
||||
forceLeft: 'downleft',
|
||||
contentDisplayed: 'content-displayed',
|
||||
};
|
||||
|
||||
const BootstrapEvents = {
|
||||
hideDropdown: 'hidden.bs.dropdown',
|
||||
};
|
||||
|
||||
let initialized = false;
|
||||
|
||||
/**
|
||||
* Initialize all delegated events into the page.
|
||||
*/
|
||||
const initPageEvents = () => {
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
// Hide all subpanels when hidind a dropdown.
|
||||
// This is using JQuery because of BS4 events. JQuery won't be needed with BS5.
|
||||
jQuery(document).on(BootstrapEvents.hideDropdown, () => {
|
||||
document.querySelectorAll(`${Selectors.subPanelContent}.show`).forEach(visibleSubPanel => {
|
||||
const dropdownSubPanel = visibleSubPanel.closest(Selectors.subPanel);
|
||||
const subPanel = new SubPanel(dropdownSubPanel);
|
||||
subPanel.setVisibility(false);
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('resize', debounce(updateAllPanelsPosition, 400));
|
||||
|
||||
initialized = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update all the panels position.
|
||||
*/
|
||||
const updateAllPanelsPosition = () => {
|
||||
document.querySelectorAll(Selectors.subPanel).forEach(dropdown => {
|
||||
const subpanel = new SubPanel(dropdown);
|
||||
subpanel.updatePosition();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Subpanel class.
|
||||
* @private
|
||||
*/
|
||||
class SubPanel {
|
||||
/**
|
||||
* Constructor.
|
||||
* @param {HTMLElement} element The element to initialize.
|
||||
*/
|
||||
constructor(element) {
|
||||
this.element = element;
|
||||
this.menuItem = element.querySelector(Selectors.subPanelMenuItem);
|
||||
this.panelContent = element.querySelector(Selectors.subPanelContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the subpanel element.
|
||||
*
|
||||
* This method adds the event listeners to the subpanel and the position classes.
|
||||
*/
|
||||
init() {
|
||||
if (this.element.dataset.subPanelInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updatePosition();
|
||||
|
||||
// Full element events.
|
||||
this.element.addEventListener('focusin', this._mainElementFocusInHandler.bind(this));
|
||||
// Menu Item events.
|
||||
this.menuItem.addEventListener('click', this._menuItemClickHandler.bind(this));
|
||||
this.menuItem.addEventListener('keydown', this._menuItemKeyHandler.bind(this));
|
||||
this.menuItem.addEventListener('mouseover', this._menuItemHoverHandler.bind(this));
|
||||
this.menuItem.addEventListener('mouseout', this._menuItemHoverOutHandler.bind(this));
|
||||
// Subpanel content events.
|
||||
this.panelContent.addEventListener('keydown', this._panelContentKeyHandler.bind(this));
|
||||
|
||||
this.element.dataset.subPanelInitialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the subpanel has enough space.
|
||||
*
|
||||
* In general there are two scenarios were the subpanel must be interacted differently:
|
||||
* - Extra small screens: The subpanel is displayed below the menu item.
|
||||
* - Drawer: The subpanel is displayed one of the drawers.
|
||||
*
|
||||
* @returns {Boolean} true if the subpanel should be displayed in small screens.
|
||||
*/
|
||||
_needSmallSpaceBehaviour() {
|
||||
return isExtraSmall() || this.element.closest(Selectors.drawer) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main element focus in handler.
|
||||
*/
|
||||
_mainElementFocusInHandler() {
|
||||
if (this._needSmallSpaceBehaviour()) {
|
||||
return;
|
||||
}
|
||||
this.setVisibility(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu item click handler.
|
||||
* @param {Event} event
|
||||
*/
|
||||
_menuItemClickHandler(event) {
|
||||
// Avoid dropdowns being closed after clicking a subemnu.
|
||||
// This won't be needed with BS5 (data-bs-auto-close handles it).
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if (this._needSmallSpaceBehaviour()) {
|
||||
this.setVisibility(!this.getVisibility());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu item hover handler.
|
||||
* @private
|
||||
*/
|
||||
_menuItemHoverHandler() {
|
||||
if (this._needSmallSpaceBehaviour()) {
|
||||
return;
|
||||
}
|
||||
this.setVisibility(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu item hover out handler.
|
||||
* @private
|
||||
*/
|
||||
_menuItemHoverOutHandler() {
|
||||
if (this._needSmallSpaceBehaviour()) {
|
||||
return;
|
||||
}
|
||||
this._hideOtherSubPanels();
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu item key handler.
|
||||
* @param {Event} event
|
||||
* @private
|
||||
*/
|
||||
_menuItemKeyHandler(event) {
|
||||
// In small sizes te down key will focus on the panel.
|
||||
if (event.key === 'ArrowUp' || (event.key === 'ArrowDown' && !this._needSmallSpaceBehaviour())) {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Keys to move focus to the panel.
|
||||
let focusPanel = false;
|
||||
|
||||
if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
|
||||
focusPanel = true;
|
||||
}
|
||||
if ((event.key === 'Enter' || event.key === ' ') && !this._needSmallSpaceBehaviour()) {
|
||||
focusPanel = true;
|
||||
}
|
||||
// In extra small screen the panel is shown below the item.
|
||||
if (event.key === 'ArrowDown' && this._needSmallSpaceBehaviour() && this.getVisibility()) {
|
||||
focusPanel = true;
|
||||
}
|
||||
if (focusPanel) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.setVisibility(true);
|
||||
this._focusPanelContent();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sub panel content key handler.
|
||||
* @param {Event} event
|
||||
* @private
|
||||
*/
|
||||
_panelContentKeyHandler(event) {
|
||||
// In extra small devices the panel is displayed under the menu item
|
||||
// so the arrow up/down switch between subpanel and the menu item.
|
||||
const canLoop = !this._needSmallSpaceBehaviour();
|
||||
let isBrowsingSubPanel = false;
|
||||
let newFocus = null;
|
||||
if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
|
||||
newFocus = this.menuItem;
|
||||
}
|
||||
if (event.key === 'ArrowUp') {
|
||||
newFocus = previousFocusableElement(this.panelContent, canLoop);
|
||||
isBrowsingSubPanel = true;
|
||||
}
|
||||
if (event.key === 'ArrowDown') {
|
||||
newFocus = nextFocusableElement(this.panelContent, canLoop);
|
||||
isBrowsingSubPanel = true;
|
||||
}
|
||||
// If the user cannot loop and arrive to the start/end of the subpanel
|
||||
// we focus on the menu item.
|
||||
if (newFocus === null && isBrowsingSubPanel && !canLoop) {
|
||||
newFocus = this.menuItem;
|
||||
}
|
||||
if (newFocus !== null) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
newFocus.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus on the first focusable element of the subpanel.
|
||||
* @private
|
||||
*/
|
||||
_focusPanelContent() {
|
||||
const pendingPromise = new Pending('core/action_menu/subpanel:focuscontent');
|
||||
// Some Bootstrap events are triggered after the click event.
|
||||
// To prevent this from affecting the focus we wait a bit.
|
||||
setTimeout(() => {
|
||||
const firstFocusable = firstFocusableElement(this.panelContent);
|
||||
if (firstFocusable) {
|
||||
firstFocusable.focus();
|
||||
}
|
||||
pendingPromise.resolve();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the visibility of a subpanel.
|
||||
* @param {Boolean} visible true if the subpanel should be visible.
|
||||
*/
|
||||
setVisibility(visible) {
|
||||
if (visible) {
|
||||
this._hideOtherSubPanels();
|
||||
}
|
||||
this.menuItem.setAttribute('aria-expanded', visible ? 'true' : 'false');
|
||||
this.panelContent.classList.toggle('show', visible);
|
||||
this.element.classList.toggle(Classes.contentDisplayed, visible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide all other subpanels in the parent menu.
|
||||
* @private
|
||||
*/
|
||||
_hideOtherSubPanels() {
|
||||
const dropdown = this.element.closest(Selectors.mainMenu);
|
||||
dropdown.querySelectorAll(`${Selectors.subPanelContent}.show`).forEach(visibleSubPanel => {
|
||||
const dropdownSubPanel = visibleSubPanel.closest(Selectors.subPanel);
|
||||
if (dropdownSubPanel === this.element) {
|
||||
return;
|
||||
}
|
||||
const subPanel = new SubPanel(dropdownSubPanel);
|
||||
subPanel.setVisibility(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the visibility of a subpanel.
|
||||
* @returns {Boolean} true if the subpanel is visible.
|
||||
*/
|
||||
getVisibility() {
|
||||
return this.menuItem.getAttribute('aria-expanded') === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the panels position depending on the screen size and panel position.
|
||||
*/
|
||||
updatePosition() {
|
||||
const dropdownRight = this.element.closest(Selectors.dropdownRight);
|
||||
if (this._needSmallSpaceBehaviour()) {
|
||||
this.element.classList.remove(Classes.dropRight);
|
||||
this.element.classList.remove(Classes.dropLeft);
|
||||
this.element.classList.add(Classes.dropDown);
|
||||
this.element.classList.toggle(Classes.forceLeft, dropdownRight !== null);
|
||||
return;
|
||||
}
|
||||
this.element.classList.remove(Classes.dropDown);
|
||||
this.element.classList.remove(Classes.forceLeft);
|
||||
this.element.classList.toggle(Classes.dropRight, dropdownRight === null);
|
||||
this.element.classList.toggle(Classes.dropLeft, dropdownRight !== null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise module for given report
|
||||
*
|
||||
* @method
|
||||
* @param {string} selector The query selector to init.
|
||||
*/
|
||||
export const init = (selector) => {
|
||||
initPageEvents();
|
||||
const subMenu = document.querySelector(selector);
|
||||
if (!subMenu) {
|
||||
throw new Error(`Sub panel element not found: ${selector}`);
|
||||
}
|
||||
const subPanel = new SubPanel(subMenu);
|
||||
subPanel.init();
|
||||
};
|
82
lib/classes/output/local/action_menu/subpanel.php
Normal file
82
lib/classes/output/local/action_menu/subpanel.php
Normal file
@ -0,0 +1,82 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core\output\local\action_menu;
|
||||
|
||||
use action_link;
|
||||
use pix_icon;
|
||||
use renderable;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Interface to a subpanel implementation.
|
||||
*
|
||||
* @package core_admin
|
||||
* @copyright 2023 Ferran Recio <ferran@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class subpanel extends action_link implements renderable {
|
||||
/**
|
||||
* The subpanel content.
|
||||
* @var renderable
|
||||
*/
|
||||
protected $subpanel;
|
||||
|
||||
/**
|
||||
* The number of instances of this action menu link (and its subclasses).
|
||||
* @var int
|
||||
*/
|
||||
protected static $instance = 1;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param string $text the text to display
|
||||
* @param renderable $subpanel the subpanel content
|
||||
* @param array|null $attributes an optional array of attributes
|
||||
* @param pix_icon|null $icon an optional icon
|
||||
*/
|
||||
public function __construct(
|
||||
$text,
|
||||
renderable $subpanel,
|
||||
array $attributes = null,
|
||||
pix_icon $icon = null
|
||||
) {
|
||||
$this->text = $text;
|
||||
$this->subpanel = $subpanel;
|
||||
if (empty($attributes['id'])) {
|
||||
$attributes['id'] = \html_writer::random_id('action_menu_submenu');
|
||||
}
|
||||
$this->attributes = (array) $attributes;
|
||||
$this->icon = $icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export this object for template rendering.
|
||||
* @param \renderer_base $output the output renderer
|
||||
* @return stdClass
|
||||
*/
|
||||
public function export_for_template(\renderer_base $output): stdClass {
|
||||
$data = parent::export_for_template($output);
|
||||
$data->instance = self::$instance++;
|
||||
$data->subpanelcontent = $output->render($this->subpanel);
|
||||
// The menu trigger icon collides with the subpanel item icon. Unlike regular menu items,
|
||||
// subpanel items usually does not use icons. To prevent the collision, subpanels use a diferent
|
||||
// context variable for item icon.
|
||||
$data->itemicon = $data->icon;
|
||||
unset($data->icon);
|
||||
return $data;
|
||||
}
|
||||
}
|
@ -26,6 +26,8 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
use core\output\local\action_menu\subpanel;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
@ -4469,10 +4471,13 @@ class action_menu implements renderable, templatable {
|
||||
/**
|
||||
* Adds an action to this action menu.
|
||||
*
|
||||
* @param action_menu_link|pix_icon|string $action
|
||||
* @param action_menu_link|pix_icon|subpanel|string $action
|
||||
*/
|
||||
public function add($action) {
|
||||
if ($action instanceof action_link) {
|
||||
|
||||
if ($action instanceof subpanel) {
|
||||
$this->add_secondary_subpanel($action);
|
||||
} else if ($action instanceof action_link) {
|
||||
if ($action->primary) {
|
||||
$this->add_primary_action($action);
|
||||
} else {
|
||||
@ -4485,6 +4490,14 @@ class action_menu implements renderable, templatable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a secondary subpanel.
|
||||
* @param subpanel $subpanel
|
||||
*/
|
||||
public function add_secondary_subpanel(subpanel $subpanel) {
|
||||
$this->secondaryactions[] = $subpanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a primary action to the action menu.
|
||||
*
|
||||
@ -4754,8 +4767,6 @@ class action_menu implements renderable, templatable {
|
||||
$this->attributes['role'] = 'menubar';
|
||||
}
|
||||
$attributes = $this->attributes;
|
||||
$attributesprimary = $this->attributesprimary;
|
||||
$attributessecondary = $this->attributessecondary;
|
||||
|
||||
$data->instance = $this->instance;
|
||||
|
||||
@ -4766,17 +4777,34 @@ class action_menu implements renderable, templatable {
|
||||
return [ 'name' => $key, 'value' => $value ];
|
||||
}, array_keys($attributes), $attributes);
|
||||
|
||||
$data->primary = $this->export_primary_actions_for_template($output);
|
||||
$data->secondary = $this->export_secondary_actions_for_template($output);
|
||||
$data->dropdownalignment = $this->dropdownalignment;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the primary actions for the template.
|
||||
* @param renderer_base $output
|
||||
* @return stdClass
|
||||
*/
|
||||
protected function export_primary_actions_for_template(renderer_base $output): stdClass {
|
||||
$attributes = $this->attributes;
|
||||
$attributesprimary = $this->attributesprimary;
|
||||
|
||||
$primary = new stdClass();
|
||||
$primary->title = '';
|
||||
$primary->prioritise = $this->prioritise;
|
||||
|
||||
$primary->classes = isset($attributesprimary['class']) ? $attributesprimary['class'] : '';
|
||||
unset($attributesprimary['class']);
|
||||
$primary->attributes = array_map(function($key, $value) {
|
||||
return [ 'name' => $key, 'value' => $value ];
|
||||
|
||||
$primary->attributes = array_map(function ($key, $value) {
|
||||
return ['name' => $key, 'value' => $value];
|
||||
}, array_keys($attributesprimary), $attributesprimary);
|
||||
$primary->triggerattributes = array_map(function($key, $value) {
|
||||
return [ 'name' => $key, 'value' => $value ];
|
||||
$primary->triggerattributes = array_map(function ($key, $value) {
|
||||
return ['name' => $key, 'value' => $value];
|
||||
}, array_keys($this->triggerattributes), $this->triggerattributes);
|
||||
|
||||
$actionicon = $this->actionicon;
|
||||
@ -4812,7 +4840,7 @@ class action_menu implements renderable, templatable {
|
||||
}
|
||||
|
||||
$primary->actiontext = $this->actiontext ? (string) $this->actiontext : '';
|
||||
$primary->items = array_map(function($item) use ($output) {
|
||||
$primary->items = array_map(function ($item) use ($output) {
|
||||
$data = (object) [];
|
||||
if ($item instanceof action_menu_link) {
|
||||
$data->actionmenulink = $item->export_for_template($output);
|
||||
@ -4827,19 +4855,36 @@ class action_menu implements renderable, templatable {
|
||||
}
|
||||
return $data;
|
||||
}, $this->primaryactions);
|
||||
return $primary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the secondary actions for the template.
|
||||
* @param renderer_base $output
|
||||
* @return stdClass
|
||||
*/
|
||||
protected function export_secondary_actions_for_template(renderer_base $output): stdClass {
|
||||
$attributessecondary = $this->attributessecondary;
|
||||
$secondary = new stdClass();
|
||||
$secondary->classes = isset($attributessecondary['class']) ? $attributessecondary['class'] : '';
|
||||
unset($attributessecondary['class']);
|
||||
$secondary->attributes = array_map(function($key, $value) {
|
||||
return [ 'name' => $key, 'value' => $value ];
|
||||
|
||||
$secondary->attributes = array_map(function ($key, $value) {
|
||||
return ['name' => $key, 'value' => $value];
|
||||
}, array_keys($attributessecondary), $attributessecondary);
|
||||
$secondary->items = array_map(function($item) use ($output) {
|
||||
$data = (object) [];
|
||||
$secondary->items = array_map(function ($item) use ($output) {
|
||||
$data = (object) [
|
||||
'simpleitem' => true,
|
||||
];
|
||||
if ($item instanceof action_menu_link) {
|
||||
$data->actionmenulink = $item->export_for_template($output);
|
||||
$data->simpleitem = false;
|
||||
} else if ($item instanceof action_menu_filler) {
|
||||
$data->actionmenufiller = $item->export_for_template($output);
|
||||
$data->simpleitem = false;
|
||||
} else if ($item instanceof subpanel) {
|
||||
$data->subpanel = $item->export_for_template($output);
|
||||
$data->simpleitem = false;
|
||||
} else if ($item instanceof action_link) {
|
||||
$data->actionlink = $item->export_for_template($output);
|
||||
} else if ($item instanceof pix_icon) {
|
||||
@ -4849,14 +4894,8 @@ class action_menu implements renderable, templatable {
|
||||
}
|
||||
return $data;
|
||||
}, $this->secondaryactions);
|
||||
|
||||
$data->primary = $primary;
|
||||
$data->secondary = $secondary;
|
||||
$data->dropdownalignment = $this->dropdownalignment;
|
||||
|
||||
return $data;
|
||||
return $secondary;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -121,11 +121,12 @@
|
||||
{{#actionmenufiller}}
|
||||
<div class="dropdown-divider" role="presentation"><span class="filler"> </span></div>
|
||||
{{/actionmenufiller}}
|
||||
{{^actionmenulink}}
|
||||
{{^actionmenufiller}}
|
||||
<div class="dropdown-item">{{> core/action_menu_item }}</div>
|
||||
{{/actionmenufiller}}
|
||||
{{/actionmenulink}}
|
||||
{{#subpanel}}
|
||||
{{> core/local/action_menu/subpanel}}
|
||||
{{/subpanel}}
|
||||
{{#simpleitem}}
|
||||
<div class="dropdown-item">{{> core/action_menu_item }}</div>
|
||||
{{/simpleitem}}
|
||||
{{/items}}
|
||||
</div>
|
||||
{{/secondary}}
|
||||
|
68
lib/templates/local/action_menu/subpanel.mustache
Normal file
68
lib/templates/local/action_menu/subpanel.mustache
Normal file
@ -0,0 +1,68 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/local/action_menu/subpanel
|
||||
Action menu link.
|
||||
Example context (json):
|
||||
{
|
||||
"id": "exampleId12345678",
|
||||
"text": "Example link text",
|
||||
"subpanelcontent": "Example subpanel content",
|
||||
"url": "http://example.com",
|
||||
"attributes": [
|
||||
{
|
||||
"name": "data-attribute",
|
||||
"value": "example"
|
||||
}
|
||||
],
|
||||
"itemicon": {
|
||||
"key": "t/groups",
|
||||
"component": "core",
|
||||
"title": "Example icon title"
|
||||
},
|
||||
"instance": 1,
|
||||
"disabled": false
|
||||
}
|
||||
}}
|
||||
<div
|
||||
class="dropdown-subpanel position-relative dropright"
|
||||
id="{{id}}"
|
||||
>
|
||||
<a
|
||||
class="dropdown-item dropdown-toggle {{#disabled}} disabled {{/disabled}}"
|
||||
href="{{url}}"
|
||||
data-toggle="dropdown-subpanel"
|
||||
role="menuitem"
|
||||
aria-haspopup="true"
|
||||
tabindex="-1"
|
||||
aria-expanded="false"
|
||||
aria-label="{{text}}"
|
||||
{{#attributes}}
|
||||
{{name}}="{{value}}"
|
||||
{{/attributes}}
|
||||
>
|
||||
{{#itemicon}}
|
||||
{{#pix}}{{key}}, {{component}}, {{title}}{{/pix}}
|
||||
{{/itemicon}}
|
||||
<span class="menu-action-text" id="actionmenuactionsubpanel-{{instance}}">{{{text}}}</span>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-subpanel-content" role="menu">
|
||||
{{{subpanelcontent}}}
|
||||
</div>
|
||||
</div>
|
||||
{{#js}}
|
||||
require(['core/local/action_menu/subpanel'], function(Module) {
|
||||
Module.init('#' + '{{id}}');
|
||||
});
|
||||
{{/js}}
|
227
lib/tests/behat/action_menu_subpanel.feature
Normal file
227
lib/tests/behat/action_menu_subpanel.feature
Normal file
@ -0,0 +1,227 @@
|
||||
@core @javascript
|
||||
Feature: Navigate action menu subpanels
|
||||
In order to navigate an action menu subpanel
|
||||
As a user
|
||||
I need to be able to use both keyboard and mouse to open the subpanel
|
||||
|
||||
Background:
|
||||
Given I log in as "admin"
|
||||
And I am on fixture page "/lib/tests/behat/fixtures/action_menu_subpanel_output_testpage.php"
|
||||
|
||||
Scenario: Navigate several action menus subpanels with mouse
|
||||
Given I click on "Actions menu" "button" in the "regularscenario" "region"
|
||||
And I click on "Subpanel example" "menuitem" in the "regularscenario" "region"
|
||||
And I should see "Status A" in the "regularscenario" "region"
|
||||
And I should see "Status B" in the "regularscenario" "region"
|
||||
And I should not see "Status C" in the "regularscenario" "region"
|
||||
And I should not see "Status D" in the "regularscenario" "region"
|
||||
When I click on "Another subpanel" "menuitem" in the "regularscenario" "region"
|
||||
Then I should not see "Status A" in the "regularscenario" "region"
|
||||
And I should not see "Status B" in the "regularscenario" "region"
|
||||
And I should see "Status C" in the "regularscenario" "region"
|
||||
And I should see "Status D" in the "regularscenario" "region"
|
||||
And I click on "Status D" "link" in the "regularscenario" "region"
|
||||
And I should see "Foo param value: Donkey" in the "paramcheck" "region"
|
||||
|
||||
Scenario: Check extra data in subpanel action menu items
|
||||
When I should see "Adding data attributes to menu item" in the "dataattributes" "region"
|
||||
# the page have a javascript script to check that for us.
|
||||
Then "[data-extra='some other value']" "css_element" should exist in the "dataattributes" "region"
|
||||
And "[data-extra='some other value']" "css_element" should exist in the "dataattributes" "region"
|
||||
And I should see "Extra data attribute detected: some extra value" in the "datachecks" "region"
|
||||
And I should see "Extra data attribute detected: some other value" in the "datachecks" "region"
|
||||
|
||||
Scenario: User can navigate left menus subpanels
|
||||
Given I click on "Actions menu" "button" in the "menuleft" "region"
|
||||
And I click on "Subpanel example" "menuitem" in the "menuleft" "region"
|
||||
And I should see "Status A" in the "menuleft" "region"
|
||||
And I should see "Status B" in the "menuleft" "region"
|
||||
And I should not see "Status C" in the "menuleft" "region"
|
||||
And I should not see "Status D" in the "menuleft" "region"
|
||||
When I click on "Another subpanel" "menuitem" in the "menuleft" "region"
|
||||
Then I should not see "Status A" in the "menuleft" "region"
|
||||
And I should not see "Status B" in the "menuleft" "region"
|
||||
And I should see "Status C" in the "menuleft" "region"
|
||||
And I should see "Status D" in the "menuleft" "region"
|
||||
And I click on "Status D" "link" in the "menuleft" "region"
|
||||
And I should see "Foo param value: Donkey" in the "paramcheck" "region"
|
||||
|
||||
Scenario: User can show the subpanels content using keyboard
|
||||
Given I click on "Actions menu" "button" in the "regularscenario" "region"
|
||||
# Move to the first subpanel element.
|
||||
And I press the down key
|
||||
And I press the down key
|
||||
And I press the down key
|
||||
And I should see "Status A" in the "regularscenario" "region"
|
||||
And I should see "Status B" in the "regularscenario" "region"
|
||||
And I should not see "Status C" in the "regularscenario" "region"
|
||||
And I should not see "Status D" in the "regularscenario" "region"
|
||||
# Move to the next subpanel.
|
||||
When I press the down key
|
||||
Then I should not see "Status A" in the "regularscenario" "region"
|
||||
And I should not see "Status B" in the "regularscenario" "region"
|
||||
And I should see "Status C" in the "regularscenario" "region"
|
||||
And I should see "Status D" in the "regularscenario" "region"
|
||||
|
||||
Scenario: User can browse the subpanel content using the arrow keys
|
||||
Given I click on "Actions menu" "button" in the "regularscenario" "region"
|
||||
# Move to the first subpanel element.
|
||||
And I press the down key
|
||||
And I press the down key
|
||||
And I press the down key
|
||||
# Move in the subpanel with arrow keys and loop the links with up and down.
|
||||
When I press the right key
|
||||
And the focused element is "Status A" "link" in the "regularscenario" "region"
|
||||
And I press the down key
|
||||
And the focused element is "Status B" "link" in the "regularscenario" "region"
|
||||
And I press the down key
|
||||
And the focused element is "Status A" "link" in the "regularscenario" "region"
|
||||
And I press the up key
|
||||
And the focused element is "Status B" "link" in the "regularscenario" "region"
|
||||
# Leave the subpanel with right and left key.
|
||||
Then I press the right key
|
||||
And the focused element is "Subpanel example" "menuitem" in the "regularscenario" "region"
|
||||
And I press the right key
|
||||
And the focused element is "Status A" "link" in the "regularscenario" "region"
|
||||
And I press the left key
|
||||
And the focused element is "Subpanel example" "menuitem" in the "regularscenario" "region"
|
||||
And I press the left key
|
||||
And the focused element is "Status A" "link" in the "regularscenario" "region"
|
||||
And I press the left key
|
||||
And the focused element is "Subpanel example" "menuitem" in the "regularscenario" "region"
|
||||
# Move to the next subpanel with enter.
|
||||
And I press the down key
|
||||
And I press the right key
|
||||
And the focused element is "Status C" "link" in the "regularscenario" "region"
|
||||
And I press the down key
|
||||
And the focused element is "Status D" "link" in the "regularscenario" "region"
|
||||
# Select the current link of the panel with enter.
|
||||
And I press the enter key
|
||||
And I should see "Foo param value: Donkey" in the "paramcheck" "region"
|
||||
|
||||
Scenario: User can open and close subpanels in mobile
|
||||
Given I change the viewport size to "mobile"
|
||||
And I click on "Actions menu" "button" in the "regularscenario" "region"
|
||||
And I should not see "Status A" in the "regularscenario" "region"
|
||||
And I should not see "Status B" in the "regularscenario" "region"
|
||||
And I should not see "Status C" in the "regularscenario" "region"
|
||||
And I should not see "Status D" in the "regularscenario" "region"
|
||||
When I click on "Subpanel example" "menuitem" in the "regularscenario" "region"
|
||||
And I should see "Status A" in the "regularscenario" "region"
|
||||
And I should see "Status B" in the "regularscenario" "region"
|
||||
And I should not see "Status C" in the "regularscenario" "region"
|
||||
And I should not see "Status D" in the "regularscenario" "region"
|
||||
# In mobile click the menu item toggles the subpanel.
|
||||
Then I click on "Subpanel example" "menuitem" in the "regularscenario" "region"
|
||||
And I should not see "Status A" in the "regularscenario" "region"
|
||||
And I should not see "Status B" in the "regularscenario" "region"
|
||||
And I should not see "Status C" in the "regularscenario" "region"
|
||||
And I should not see "Status D" in the "regularscenario" "region"
|
||||
And I click on "Another subpanel" "menuitem" in the "regularscenario" "region"
|
||||
And I should not see "Status A" in the "regularscenario" "region"
|
||||
And I should not see "Status B" in the "regularscenario" "region"
|
||||
And I should see "Status C" in the "regularscenario" "region"
|
||||
And I should see "Status D" in the "regularscenario" "region"
|
||||
And I click on "Status D" "link" in the "regularscenario" "region"
|
||||
And I should see "Foo param value: Donkey" in the "paramcheck" "region"
|
||||
|
||||
Scenario: User can browse the subpanels using keys in extra small windows
|
||||
Given I change the viewport size to "mobile"
|
||||
And I click on "Actions menu" "button" in the "regularscenario" "region"
|
||||
# Go to the seconds subpanel and open it with enter.
|
||||
And I press the down key
|
||||
And I press the down key
|
||||
And I press the down key
|
||||
And I press the down key
|
||||
And the focused element is "Another subpanel" "menuitem" in the "regularscenario" "region"
|
||||
And I press the enter key
|
||||
When I should not see "Status A" in the "regularscenario" "region"
|
||||
And I should not see "Status B" in the "regularscenario" "region"
|
||||
And I should see "Status C" in the "regularscenario" "region"
|
||||
And I should see "Status D" in the "regularscenario" "region"
|
||||
# Loop the subpanel links wand the menu item with up and down.
|
||||
Then I press the down key
|
||||
And the focused element is "Status C" "link" in the "regularscenario" "region"
|
||||
And I press the down key
|
||||
And the focused element is "Status D" "link" in the "regularscenario" "region"
|
||||
And I press the down key
|
||||
And the focused element is "Another subpanel" "menuitem" in the "regularscenario" "region"
|
||||
And I press the down key
|
||||
And the focused element is "Status C" "link" in the "regularscenario" "region"
|
||||
And I press the down key
|
||||
And the focused element is "Status D" "link" in the "regularscenario" "region"
|
||||
And I press the up key
|
||||
And the focused element is "Status C" "link" in the "regularscenario" "region"
|
||||
And I press the up key
|
||||
And the focused element is "Another subpanel" "menuitem" in the "regularscenario" "region"
|
||||
# Use up in the item to close the panel.
|
||||
And I press the up key
|
||||
And I should not see "Status A" in the "regularscenario" "region"
|
||||
And I should not see "Status B" in the "regularscenario" "region"
|
||||
And I should not see "Status C" in the "regularscenario" "region"
|
||||
And I should not see "Status D" in the "regularscenario" "region"
|
||||
And the focused element is "Subpanel example" "menuitem" in the "regularscenario" "region"
|
||||
# Enter the panel and select the second link.
|
||||
And I press the enter key
|
||||
And I press the down key
|
||||
And the focused element is "Status A" "link" in the "regularscenario" "region"
|
||||
And I press the enter key
|
||||
And I should see "Foo param value: Aardvark" in the "paramcheck" "region"
|
||||
|
||||
Scenario: action menu subpanels can display optional icons in the menu item
|
||||
Given I click on "Actions menu" "button" in the "regularscenario" "region"
|
||||
And "Locked icon" "icon" should not exist in the "regularscenario" "region"
|
||||
And "Message icon" "icon" should not exist in the "regularscenario" "region"
|
||||
And I click on "Actions menu" "button" in the "menuleft" "region"
|
||||
And "Locked icon" "icon" should not exist in the "menuleft" "region"
|
||||
And "Message icon" "icon" should not exist in the "menuleft" "region"
|
||||
When I click on "Actions menu" "button" in the "itemicon" "region"
|
||||
Then "Locked icon" "icon" should exist in the "itemicon" "region"
|
||||
And "Message icon" "icon" should exist in the "itemicon" "region"
|
||||
And I click on "Actions menu" "button" in the "itemiconleft" "region"
|
||||
And "Locked icon" "icon" should exist in the "itemiconleft" "region"
|
||||
And "Message icon" "icon" should exist in the "itemiconleft" "region"
|
||||
|
||||
@accessibility
|
||||
Scenario: User can browse the subpanels using keys in a drawer action menu
|
||||
Given I click on "Actions menu" "button" in the "drawersimulation" "region"
|
||||
# Go to the seconds subpanel and open it with enter.
|
||||
And I press the down key
|
||||
And I press the down key
|
||||
And I press the down key
|
||||
And I press the down key
|
||||
And the focused element is "Another subpanel" "menuitem" in the "drawersimulation" "region"
|
||||
And I press the enter key
|
||||
When I should not see "Status A" in the "drawersimulation" "region"
|
||||
And I should not see "Status B" in the "drawersimulation" "region"
|
||||
And I should see "Status C" in the "drawersimulation" "region"
|
||||
And I should see "Status D" in the "drawersimulation" "region"
|
||||
# Loop the subpanel links wand the menu item with up and down.
|
||||
Then I press the down key
|
||||
And the focused element is "Status C" "link" in the "drawersimulation" "region"
|
||||
And I press the down key
|
||||
And the focused element is "Status D" "link" in the "drawersimulation" "region"
|
||||
And I press the down key
|
||||
And the focused element is "Another subpanel" "menuitem" in the "drawersimulation" "region"
|
||||
And I press the down key
|
||||
And the focused element is "Status C" "link" in the "drawersimulation" "region"
|
||||
And I press the down key
|
||||
And the focused element is "Status D" "link" in the "drawersimulation" "region"
|
||||
And I press the up key
|
||||
And the focused element is "Status C" "link" in the "drawersimulation" "region"
|
||||
And I press the up key
|
||||
And the focused element is "Another subpanel" "menuitem" in the "drawersimulation" "region"
|
||||
# Use up in the item to close the panel.
|
||||
And I press the up key
|
||||
And I should not see "Status A" in the "drawersimulation" "region"
|
||||
And I should not see "Status B" in the "drawersimulation" "region"
|
||||
And I should not see "Status C" in the "drawersimulation" "region"
|
||||
And I should not see "Status D" in the "drawersimulation" "region"
|
||||
And the focused element is "Subpanel example" "menuitem" in the "drawersimulation" "region"
|
||||
And the page should meet accessibility standards with "wcag143" extra tests
|
||||
# Enter the panel and select the second link.
|
||||
And I press the enter key
|
||||
And I press the down key
|
||||
And the focused element is "Status A" "link" in the "drawersimulation" "region"
|
||||
And I press the enter key
|
||||
And I should see "Foo param value: Aardvark" in the "paramcheck" "region"
|
@ -0,0 +1,253 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Test page for action menu subpanel output component.
|
||||
*
|
||||
* @copyright 2023 Ferran Recio <ferran@moodle.com>
|
||||
* @package core
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../../../../config.php');
|
||||
|
||||
defined('BEHAT_SITE_RUNNING') || die();
|
||||
|
||||
$foo = optional_param('foo', 'none', PARAM_TEXT);
|
||||
|
||||
global $CFG, $PAGE, $OUTPUT;
|
||||
$PAGE->set_url('/lib/tests/behat/fixtures/action_menu_subpanel_output_testpage.php');
|
||||
$PAGE->add_body_class('limitedwidth');
|
||||
require_login();
|
||||
$PAGE->set_context(core\context\system::instance());
|
||||
$PAGE->set_title('Action menu subpanel test page');
|
||||
|
||||
echo $OUTPUT->header();
|
||||
|
||||
$choice1 = new core\output\choicelist('Choice example');
|
||||
$choice1->add_option("statusa", "Status A", [
|
||||
'url' => new moodle_url($PAGE->url, ['foo' => 'Aardvark']),
|
||||
'description' => 'Status A description',
|
||||
'icon' => new pix_icon('t/user', '', ''),
|
||||
]);
|
||||
$choice1->add_option("statusb", "Status B", [
|
||||
'url' => new moodle_url($PAGE->url, ['foo' => 'Beetle']),
|
||||
'description' => 'Status B description',
|
||||
'icon' => new pix_icon('t/groupv', '', ''),
|
||||
]);
|
||||
$choice1->set_selected_value('statusb');
|
||||
|
||||
$choice2 = new core\output\choicelist('Choice example');
|
||||
$choice2->add_option("statusc", "Status C", [
|
||||
'url' => new moodle_url($PAGE->url, ['foo' => 'Caterpillar']),
|
||||
'description' => 'Status C description',
|
||||
'icon' => new pix_icon('t/groups', '', ''),
|
||||
]);
|
||||
$choice2->add_option("statusd", "Status D", [
|
||||
'url' => new moodle_url($PAGE->url, ['foo' => 'Donkey']),
|
||||
'description' => 'Status D description',
|
||||
'icon' => new pix_icon('t/hide', '', ''),
|
||||
]);
|
||||
$choice2->set_selected_value('statusc');
|
||||
|
||||
$normalactionlink = new action_menu_link(
|
||||
new moodle_url($PAGE->url, ['foo' => 'bar']),
|
||||
new pix_icon('t/emptystar', ''),
|
||||
'Action link example',
|
||||
false
|
||||
);
|
||||
|
||||
echo "<h2>Action menu subpanel test page</h2>";
|
||||
|
||||
echo '<div id="paramcheck" class="mb-4">';
|
||||
echo "<p>Foo param value: $foo</p>";
|
||||
echo '</div>';
|
||||
|
||||
echo '<div id="regularscenario" class="mb-4">';
|
||||
echo "<h3>Basic example</h3>";
|
||||
$menu = new action_menu();
|
||||
$menu->add($normalactionlink);
|
||||
$menu->add($normalactionlink);
|
||||
$menu->add(
|
||||
new core\output\local\action_menu\subpanel(
|
||||
'Subpanel example',
|
||||
$choice1
|
||||
)
|
||||
);
|
||||
$menu->add(
|
||||
new core\output\local\action_menu\subpanel(
|
||||
'Another subpanel',
|
||||
$choice2
|
||||
)
|
||||
);
|
||||
echo '<div class="border p-2 d-flex flex-row">';
|
||||
echo '<div class="flex-fill">Menu right example</div><div>';
|
||||
echo $OUTPUT->render($menu);
|
||||
echo '</div></div>';
|
||||
|
||||
echo '</div>';
|
||||
|
||||
echo '<div id="menuleft" class="mb-4">';
|
||||
echo "<h3>Menu left</h3>";
|
||||
|
||||
$menu = new action_menu();
|
||||
$menu->set_menu_left();
|
||||
$menu->add($normalactionlink);
|
||||
$menu->add($normalactionlink);
|
||||
$menu->add(
|
||||
new core\output\local\action_menu\subpanel(
|
||||
'Subpanel example',
|
||||
$choice1,
|
||||
null,
|
||||
null
|
||||
)
|
||||
);
|
||||
$menu->add(
|
||||
new core\output\local\action_menu\subpanel(
|
||||
'Another subpanel',
|
||||
$choice2,
|
||||
null,
|
||||
null
|
||||
)
|
||||
);
|
||||
echo '<div class="border p-2 d-flex flex-row"><div>';
|
||||
echo $OUTPUT->render($menu);
|
||||
echo '</div><div class="flex-fill ml-2">Menu left example</div></div>';
|
||||
|
||||
echo '</div>';
|
||||
|
||||
echo '<div id="itemicon" class="mb-4">';
|
||||
echo "<h3>Menu item with icon</h3>";
|
||||
|
||||
$menu = new action_menu();
|
||||
$menu->add($normalactionlink);
|
||||
$menu->add($normalactionlink);
|
||||
$menu->add(
|
||||
new core\output\local\action_menu\subpanel(
|
||||
'Subpanel example',
|
||||
$choice1,
|
||||
null,
|
||||
new pix_icon('t/locked', 'Locked icon')
|
||||
)
|
||||
);
|
||||
$menu->add(
|
||||
new core\output\local\action_menu\subpanel(
|
||||
'Another subpanel',
|
||||
$choice2,
|
||||
null,
|
||||
new pix_icon('t/message', 'Message icon')
|
||||
)
|
||||
);
|
||||
echo '<div class="border p-2 d-flex flex-row">';
|
||||
echo '<div class="flex-fill">Menu right example</div><div>';
|
||||
echo $OUTPUT->render($menu);
|
||||
echo '</div></div>';
|
||||
|
||||
echo '</div>';
|
||||
|
||||
|
||||
echo '<div id="itemiconleft" class="mb-4">';
|
||||
echo "<h3>Left menu with item icon</h3>";
|
||||
|
||||
$menu = new action_menu();
|
||||
$menu->set_menu_left();
|
||||
$menu->add($normalactionlink);
|
||||
$menu->add($normalactionlink);
|
||||
$menu->add(
|
||||
new core\output\local\action_menu\subpanel(
|
||||
'Subpanel example',
|
||||
$choice1,
|
||||
null,
|
||||
new pix_icon('t/locked', 'Locked icon')
|
||||
)
|
||||
);
|
||||
$menu->add(
|
||||
new core\output\local\action_menu\subpanel(
|
||||
'Another subpanel',
|
||||
$choice2,
|
||||
null,
|
||||
new pix_icon('t/message', 'Message icon')
|
||||
)
|
||||
);
|
||||
echo '<div class="border p-2 d-flex flex-row"><div>';
|
||||
echo $OUTPUT->render($menu);
|
||||
echo '</div><div class="flex-fill ml-2">Menu left example</div></div>';
|
||||
|
||||
echo '</div>';
|
||||
|
||||
echo '<div id="dataattributes" class="mb-4">';
|
||||
echo "<h3>Adding data attributes to menu item</h3>";
|
||||
$menu = new action_menu();
|
||||
$menu->add($normalactionlink);
|
||||
$menu->add($normalactionlink);
|
||||
$menu->add(
|
||||
new core\output\local\action_menu\subpanel(
|
||||
'Subpanel example',
|
||||
$choice1,
|
||||
['data-extra' => 'some extra value']
|
||||
)
|
||||
);
|
||||
$menu->add(
|
||||
new core\output\local\action_menu\subpanel(
|
||||
'Another subpanel',
|
||||
$choice2,
|
||||
['data-extra' => 'some other value']
|
||||
)
|
||||
);
|
||||
echo '<div class="border p-2 d-flex flex-row">';
|
||||
echo '<div class="flex-fill">Menu right example</div><div>';
|
||||
echo $OUTPUT->render($menu);
|
||||
echo '</div></div>';
|
||||
echo '<div class="mt-1 p-2 border" id="datachecks">Nothing here.</div>';
|
||||
echo '</div>';
|
||||
|
||||
$inlinejs = <<<EOF
|
||||
const datachecks = document.getElementById('datachecks');
|
||||
const dataitems = document.querySelectorAll('[data-extra]');
|
||||
let dataitemshtml = '';
|
||||
for (let i = 0; i < dataitems.length; i++) {
|
||||
dataitemshtml += '<p>Extra data attribute detected: ' + dataitems[i].getAttribute('data-extra') + '</p>';
|
||||
}
|
||||
datachecks.innerHTML = dataitemshtml;
|
||||
EOF;
|
||||
|
||||
$PAGE->requires->js_amd_inline($inlinejs);
|
||||
|
||||
echo '<div id="drawersimulation" class="mb-4">';
|
||||
echo "<h3>Drawer like example</h3>";
|
||||
$menu = new action_menu();
|
||||
$menu->add($normalactionlink);
|
||||
$menu->add($normalactionlink);
|
||||
$menu->add(
|
||||
new core\output\local\action_menu\subpanel(
|
||||
'Subpanel example',
|
||||
$choice1
|
||||
)
|
||||
);
|
||||
$menu->add(
|
||||
new core\output\local\action_menu\subpanel(
|
||||
'Another subpanel',
|
||||
$choice2
|
||||
)
|
||||
);
|
||||
echo '<div class="border p-2 d-flex flex-row" data-region="fixed-drawer" data-behat-fake-drawer="true" style="width: 350px;">';
|
||||
echo '<div class="flex-fill">Drawer example</div><div>';
|
||||
echo $OUTPUT->render($menu);
|
||||
echo '</div></div>';
|
||||
|
||||
echo '</div>';
|
||||
|
||||
echo $OUTPUT->footer();
|
@ -370,6 +370,23 @@ img.resize {
|
||||
.action-menu {
|
||||
white-space: nowrap;
|
||||
display: inline;
|
||||
|
||||
.dropdown.downleft .dropdown-subpanel-content {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.dropdown-subpanel.content-displayed {
|
||||
background-color: $gray-200;
|
||||
}
|
||||
|
||||
.dropdown-subpanel-content {
|
||||
max-width: $modal-sm;
|
||||
|
||||
a:focus {
|
||||
outline: solid $primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.block img.resize {
|
||||
@ -3059,12 +3076,21 @@ body.dragging {
|
||||
width: 9px;
|
||||
}
|
||||
|
||||
.dir-rtl .dropleft .dropdown-toggle::before {
|
||||
content: fa-content($fa-var-chevron-right);
|
||||
}
|
||||
|
||||
.dropright .dropdown-toggle::after {
|
||||
border: 0;
|
||||
@extend .fa-solid;
|
||||
content: fa-content($fa-var-chevron-right);
|
||||
}
|
||||
|
||||
.dir-rtl .dropright .dropdown-toggle::after {
|
||||
content: fa-content($fa-var-chevron-left);
|
||||
}
|
||||
|
||||
|
||||
.dropup .dropdown-toggle::after {
|
||||
border: 0;
|
||||
@extend .fa-solid;
|
||||
@ -3205,3 +3231,21 @@ blockquote {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Choice list component. */
|
||||
.choicelist {
|
||||
min-width: $modal-sm;
|
||||
}
|
||||
|
||||
[data-region="fixed-drawer"] {
|
||||
.choicelist {
|
||||
min-width: calc(#{$modal-sm} - 25px);
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
// Modal small are larger than xs breakpoint.
|
||||
.choicelist {
|
||||
min-width: calc(#{$modal-sm} - 25px);
|
||||
}
|
||||
}
|
||||
|
@ -23246,6 +23246,19 @@ img.resize {
|
||||
white-space: nowrap;
|
||||
display: inline;
|
||||
}
|
||||
.action-menu .dropdown.downleft .dropdown-subpanel-content {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
.action-menu .dropdown-subpanel.content-displayed {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
.action-menu .dropdown-subpanel-content {
|
||||
max-width: 300px;
|
||||
}
|
||||
.action-menu .dropdown-subpanel-content a:focus {
|
||||
outline: solid #0f6cbf;
|
||||
}
|
||||
|
||||
.block img.resize {
|
||||
height: 0.9em;
|
||||
@ -25872,11 +25885,19 @@ body.dragging .dragging {
|
||||
width: 9px;
|
||||
}
|
||||
|
||||
.dir-rtl .dropleft .dropdown-toggle::before {
|
||||
content: "\f054";
|
||||
}
|
||||
|
||||
.dropright .dropdown-toggle::after {
|
||||
border: 0;
|
||||
content: "\f054";
|
||||
}
|
||||
|
||||
.dir-rtl .dropright .dropdown-toggle::after {
|
||||
content: "\f053";
|
||||
}
|
||||
|
||||
.dropup .dropdown-toggle::after {
|
||||
border: 0;
|
||||
content: "\f077";
|
||||
@ -26004,6 +26025,20 @@ blockquote {
|
||||
width: 48px !important;
|
||||
}
|
||||
|
||||
/* Choice list component. */
|
||||
.choicelist {
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
[data-region=fixed-drawer] .choicelist {
|
||||
min-width: calc(300px - 25px);
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.choicelist {
|
||||
min-width: calc(300px - 25px);
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
|
@ -23246,6 +23246,19 @@ img.resize {
|
||||
white-space: nowrap;
|
||||
display: inline;
|
||||
}
|
||||
.action-menu .dropdown.downleft .dropdown-subpanel-content {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
.action-menu .dropdown-subpanel.content-displayed {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
.action-menu .dropdown-subpanel-content {
|
||||
max-width: 300px;
|
||||
}
|
||||
.action-menu .dropdown-subpanel-content a:focus {
|
||||
outline: solid #0f6cbf;
|
||||
}
|
||||
|
||||
.block img.resize {
|
||||
height: 0.9em;
|
||||
@ -25872,11 +25885,19 @@ body.dragging .dragging {
|
||||
width: 9px;
|
||||
}
|
||||
|
||||
.dir-rtl .dropleft .dropdown-toggle::before {
|
||||
content: "\f054";
|
||||
}
|
||||
|
||||
.dropright .dropdown-toggle::after {
|
||||
border: 0;
|
||||
content: "\f054";
|
||||
}
|
||||
|
||||
.dir-rtl .dropright .dropdown-toggle::after {
|
||||
content: "\f053";
|
||||
}
|
||||
|
||||
.dropup .dropdown-toggle::after {
|
||||
border: 0;
|
||||
content: "\f077";
|
||||
@ -26004,6 +26025,20 @@ blockquote {
|
||||
width: 48px !important;
|
||||
}
|
||||
|
||||
/* Choice list component. */
|
||||
.choicelist {
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
[data-region=fixed-drawer] .choicelist {
|
||||
min-width: calc(300px - 25px);
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.choicelist {
|
||||
min-width: calc(300px - 25px);
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
|
Loading…
x
Reference in New Issue
Block a user