Skip to content

Commit

Permalink
feat(ui5-side-navigation): Add new overflow behaviour to collapsed mo…
Browse files Browse the repository at this point in the history
…de (#8019)

* feat(ui5-side-navigation): overflow

* feat(ui5-side-navigation): overflow
Added accessibility

* feat(ui5-side-navigation): overflow

* feat(ui5-side-navigation): overflow added

fixed since and button is changed to <div>

* feat(ui5-side-navigation): overflow

overflow is now a div

* feat(ui5-side-navigation): overflow

* feat(ui5-side-navigation): added tooltip to overflow icon
  • Loading branch information
GerganaKremenska authored Jan 10, 2024

Verified

This commit was signed with the committer’s verified signature.
Nerixyz nerix
1 parent 0a35d6b commit e5f8edd
Showing 14 changed files with 809 additions and 34 deletions.
21 changes: 21 additions & 0 deletions packages/fiori/src/SideNavigation.hbs
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@
{{#each items}}
{{> menuitem }}
{{/each}}
{{> overflowItem}}
</div>
{{else}}
<ul role="tree"
@@ -211,3 +212,23 @@
{{/if}}
</li>
{{/inline}}

{{#*inline overflowItem}}
<div id="{{_id}}-sn-overflow-item"
class="ui5-sn-item ui5-sn-item-level1 ui5-sn-item-overflow"
role="menuitem"
data-sap-focus-ref
@keydown="{{_onkeydownOverflow}}"
@keyup="{{_onkeyupOverflow}}"
@click="{{_handleOverflowClick}}"
aria-haspopup="menu"
tabindex="0"
title="{{overflowAccessibleName}}"
>
<ui5-icon
class="ui5-sn-item-overflow-icon"
name="overflow"
>
</ui5-icon>
</div>
{{/inline}}
208 changes: 204 additions & 4 deletions packages/fiori/src/SideNavigation.ts
Original file line number Diff line number Diff line change
@@ -2,6 +2,9 @@ import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";
import ResponsivePopover from "@ui5/webcomponents/dist/ResponsivePopover.js";
import NavigationMenu from "@ui5/webcomponents/dist/NavigationMenu.js";
import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
import { renderFinished } from "@ui5/webcomponents-base/dist/Render.js";
import event from "@ui5/webcomponents-base/dist/decorators/event.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
@@ -14,6 +17,10 @@ import {
isTablet,
isCombi,
} from "@ui5/webcomponents-base/dist/Device.js";
import {
isSpace,
isEnter,
} from "@ui5/webcomponents-base/dist/Keys.js";
import NavigationMode from "@ui5/webcomponents-base/dist/types/NavigationMode.js";
import Icon from "@ui5/webcomponents/dist/Icon.js";
import "@ui5/webcomponents-icons/dist/circle-task-2.js";
@@ -24,10 +31,12 @@ import SideNavigationItem from "./SideNavigationItem.js";
import SideNavigationSubItem from "./SideNavigationSubItem.js";
import SideNavigationTemplate from "./generated/templates/SideNavigationTemplate.lit.js";
import SideNavigationPopoverTemplate from "./generated/templates/SideNavigationPopoverTemplate.lit.js";

import {
SIDE_NAVIGATION_POPOVER_HIDDEN_TEXT,
SIDE_NAVIGATION_COLLAPSED_LIST_ARIA_ROLE_DESC,
SIDE_NAVIGATION_LIST_ARIA_ROLE_DESC,
SIDE_NAVIGATION_OVERFLOW_ACCESSIBLE_NAME,
} from "./generated/i18n/i18n-defaults.js";

// Styles
@@ -52,6 +61,15 @@ type PopupClickEventDetail = {
}
};

// used for the inner side navigation used in the SideNavigationPopoverTemplate
type NavigationMenuClickEventDetail = {
detail: {
item: {
associatedItem: SideNavigationItemBase
}
}
};

/**
* @class
*
@@ -106,6 +124,7 @@ type PopupClickEventDetail = {
SideNavigationItem,
SideNavigationSubItem,
Icon,
NavigationMenu,
],
})
/**
@@ -165,7 +184,6 @@ class SideNavigation extends UI5Element {
*/
@slot({ type: HTMLElement, invalidateOnChildChange: true })
fixedItems!: Array<SideNavigationItem>;

/**
* @private
*/
@@ -174,10 +192,13 @@ class SideNavigation extends UI5Element {

@property({ type: Boolean })
_inPopover!: boolean;

_isOverflow!: boolean;
_flexibleItemNavigation: ItemNavigation;
_fixedItemNavigation: ItemNavigation;

@property({ type: Object, multiple: true })
_menuPopoverItems!: Array<HTMLElement>;

static i18nBundle: I18nBundle;

constructor() {
@@ -194,8 +215,13 @@ class SideNavigation extends UI5Element {
navigationMode: NavigationMode.Vertical,
getItemsCallback: () => this.getEnabledFixedItems(),
});

this._handleResizeBound = this.handleResize.bind(this);
this._isOverflow = false;
}

_handleResizeBound: () => void;

async _onAfterOpen() {
// as the tree/list inside the popover is never destroyed,
// item navigation index should be managed, because items are
@@ -222,6 +248,10 @@ class SideNavigation extends UI5Element {
return SideNavigation.i18nBundle.getText(key);
}

get overflowAccessibleName() {
return SideNavigation.i18nBundle.getText(SIDE_NAVIGATION_OVERFLOW_ACCESSIBLE_NAME);
}

handlePopupItemClick(e: PopupClickEventDetail) {
const associatedItem = e.target.associatedItem;

@@ -235,6 +265,32 @@ class SideNavigation extends UI5Element {
this.closePicker();
}

handleOverflowItemClick(e: NavigationMenuClickEventDetail) {
const associatedItem = e.detail?.item.associatedItem;

associatedItem.fireEvent("click");
if (associatedItem.selected) {
this.closeMenu();
return;
}

this._selectItem(associatedItem);

// When subitem is selected in collapsed mode parent element should be focused
if (associatedItem.nodeName.toLowerCase() === "ui5-side-navigation-sub-item") {
const parent = associatedItem.parentElement as SideNavigationItem;
this._flexibleItemNavigation.setCurrentItem(parent);
} else {
this._flexibleItemNavigation.setCurrentItem(associatedItem);
}

this.closeMenu();
}

async getOverflowPopover() {
return (await this.getStaticAreaItemDomRef())!.querySelector<NavigationMenu>(".ui5-side-navigation-overflow-menu")!;
}

async getPicker() {
return (await this.getStaticAreaItemDomRef())!.querySelector<ResponsivePopover>("[ui5-responsive-popover]")!;
}
@@ -244,11 +300,22 @@ class SideNavigation extends UI5Element {
responsivePopover.showAt(opener);
}

async openOverflowMenu(opener: HTMLElement) {
const menu = await this.getOverflowPopover();
menu.showAt(opener);
menu.opener = opener;
}

async closePicker() {
const responsivePopover = await this.getPicker();
responsivePopover.close();
}

async closeMenu() {
const menu = await this.getOverflowPopover();
menu.close();
}

async getPickerTree() {
const picker = await this.getPicker();
return picker.querySelector<SideNavigation>("[ui5-side-navigation]")!;
@@ -287,13 +354,21 @@ class SideNavigation extends UI5Element {
}

getEnabledFlexibleItems() : Array<ITabbable> {
return this.getEnabledItems(this.items);
if (!this._overflowDom) {
return this.getEnabledItems(this.items);
}
this._overflowDom._tabIndex = "0";
return [...this.getEnabledItems(this.items), this._overflowDom];
}

getEnabledItems(items: Array<SideNavigationItem>) : Array<ITabbable> {
let result = new Array<ITabbable>();

items.forEach(item => {
if (item.getDomRef()?.classList.contains("ui5-sn-item-hidden")) {
return;
}

if (!item.disabled) {
result.push(item);
}
@@ -302,7 +377,6 @@ class SideNavigation extends UI5Element {
result = result.concat(item.items.filter(el => !el.disabled));
}
});

return result;
}

@@ -341,6 +415,78 @@ class SideNavigation extends UI5Element {
}
}
}
if (this.collapsed) {
this.handleResize();
}
}

onEnterDOM() {
ResizeHandler.register(this, this._handleResizeBound);
}

onExitDOM() {
ResizeHandler.deregister(this, this._handleResizeBound);
}

handleResize() {
const domRef = this.getDomRef(),
overflowItemRef = domRef?.querySelector(".ui5-sn-item-overflow");

this._updateOverflowItems();

if (this._getOverflowItems().length > 0 && this.collapsed) {
overflowItemRef?.classList.remove("ui5-sn-item-hidden");
} else {
overflowItemRef?.classList.add("ui5-sn-item-hidden");
}
}

_updateOverflowItems() {
const domRef = this.getDomRef();
if (!this.collapsed || !domRef) {
return null;
}

const overflowItemRef:HTMLElement = domRef.querySelector(".ui5-sn-item-overflow")!;
const flexibleContentDomRef:HTMLElement = domRef.querySelector(".ui5-sn-flexible")!;
if (!overflowItemRef) {
return null;
}

overflowItemRef.classList.add("ui5-sn-item-hidden");

const itemsRefs = [...domRef.querySelectorAll<HTMLElement>(".ui5-sn-flexible .ui5-sn-item-level1:not(.ui5-sn-item-overflow)")];

let itemsHeight = itemsRefs.reduce<number>((sum, itemRef) => {
itemRef.classList.remove("ui5-sn-item-hidden");
return sum + itemRef.offsetHeight;
}, 0);

const { paddingTop, paddingBottom } = window.getComputedStyle(flexibleContentDomRef);
const listHeight = flexibleContentDomRef?.offsetHeight - parseInt(paddingTop) - parseInt(paddingBottom);

overflowItemRef.classList.remove("ui5-sn-item-hidden");

itemsHeight = overflowItemRef.offsetHeight;
const oSelectedItemRef = domRef.querySelector(".ui5-sn-item-selected") as HTMLElement;
if (oSelectedItemRef) {
const { marginTop, marginBottom } = window.getComputedStyle(oSelectedItemRef);

itemsHeight += oSelectedItemRef.offsetHeight + parseFloat(marginTop) + parseFloat(marginBottom);
}

itemsRefs.forEach(itemRef => {
if (itemRef === oSelectedItemRef) {
return;
}

const { marginTop, marginBottom } = window.getComputedStyle(itemRef);
itemsHeight += itemRef.offsetHeight + parseFloat(marginTop) + parseFloat(marginBottom);

if (itemsHeight >= listHeight) {
itemRef.classList.add("ui5-sn-item-hidden");
}
});
}

_findFocusedItem(items: Array<SideNavigationItem>) : SideNavigationItemBase | undefined {
@@ -389,6 +535,7 @@ class SideNavigation extends UI5Element {

if (this.collapsed && item instanceof SideNavigationItem && item.items.length) {
e.preventDefault();
this._isOverflow = false;

this._popoverContents = {
item,
@@ -405,6 +552,32 @@ class SideNavigation extends UI5Element {
}
}

_handleOverflowClick() {
this._isOverflow = true;
this._menuPopoverItems = this._getOverflowItems();

this.openOverflowMenu(this._overflowDom as HTMLElement);
}

_getOverflowItems(): Array<SideNavigationItem> {
const overflowClass = "ui5-sn-item-hidden";
const result: Array<SideNavigationItem> = [];

this.items.forEach(item => {
if (item.getDomRef().classList.contains(overflowClass)) {
result.push(item);
}
});
return result;
}

async _afterMenuClose() {
const selectedItem = this._findSelectedItem(this.items)!;

await renderFinished();
selectedItem.getDomRef().focus();
}

_selectItem(item: SideNavigationItemBase) {
if (item.disabled) {
return;
@@ -422,8 +595,35 @@ class SideNavigation extends UI5Element {
});

item.selected = true;

if (this.collapsed && item.getDomRef()?.classList.contains("ui5-sn-item-hidden")) {
item.getDomRef().classList.remove("ui5-sn-item-hidden");
}
}

get _overflowDom() {
return this.shadowRoot!.querySelector<SideNavigationItem>(".ui5-sn-item-overflow");
}

get isOverflow() {
return this._isOverflow;
}

_onkeydownOverflow(e: KeyboardEvent) {
if (isSpace(e)) {
e.preventDefault();
}

if (isEnter(e)) {
this._handleOverflowClick();
}
}

_onkeyupOverflow(e: KeyboardEvent) {
if (isSpace(e)) {
this._handleOverflowClick();
}
}
static async onDefine() {
[SideNavigation.i18nBundle] = await Promise.all([
getI18nBundle("@ui5/webcomponents-fiori"),
Loading

0 comments on commit e5f8edd

Please sign in to comment.