Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui5-timeline-group-item): introduce new component #9407

Merged
merged 18 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 95 additions & 25 deletions packages/fiori/src/Timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@ import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
import { getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js";
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";
import { isTabNext, isTabPrevious } from "@ui5/webcomponents-base/dist/Keys.js";
import ItemNavigation from "@ui5/webcomponents-base/dist/delegate/ItemNavigation.js";
import {
isTabNext,
isTabPrevious,
} from "@ui5/webcomponents-base/dist/Keys.js";
import type { ITabbable } from "@ui5/webcomponents-base/dist/delegate/ItemNavigation.js";
import type ToggleButton from "@ui5/webcomponents/dist/ToggleButton.js";
import ItemNavigation from "@ui5/webcomponents-base/dist/delegate/ItemNavigation.js";
import NavigationMode from "@ui5/webcomponents-base/dist/types/NavigationMode.js";
import { getEventMark } from "@ui5/webcomponents-base/dist/MarkedEvents.js";
import { TIMELINE_ARIA_LABEL } from "./generated/i18n/i18n-defaults.js";
import TimelineTemplate from "./generated/templates/TimelineTemplate.lit.js";
import TimelineItem from "./TimelineItem.js";
import TimelineGroupItem from "./TimelineGroupItem.js";

// Styles
import TimelineCss from "./generated/themes/Timeline.css.js";
Expand All @@ -23,11 +28,18 @@ import TimelineLayout from "./types/TimelineLayout.js";
* @public
*/
interface ITimelineItem extends UI5Element, ITabbable {
layout: `${TimelineLayout}`,
icon?: string,
forcedLineWidth?: string,
nameClickable: boolean,
focusLink: () => void,
layout: `${TimelineLayout}`;
isGroupItem: boolean;
forcedLineWidth?: string;
icon?: string;
nameClickable?: boolean;
positionInGroup?: number;
collapsed?: boolean;
items?: Array<ITimelineItem>;
focusLink?(): void;
_lastItem: boolean;
hinzzx marked this conversation as resolved.
Show resolved Hide resolved
_isNextItemGroup?: boolean;
_firstItemInTimeline?: boolean;
}

const SHORT_LINE_WIDTH = "ShortLineWidth";
Expand All @@ -54,7 +66,7 @@ const LARGE_LINE_WIDTH = "LargeLineWidth";
renderer: litRender,
styles: TimelineCss,
template: TimelineTemplate,
dependencies: [TimelineItem],
dependencies: [TimelineItem, TimelineGroupItem],
})
class Timeline extends UI5Element {
/**
Expand Down Expand Up @@ -90,7 +102,7 @@ class Timeline extends UI5Element {
super();

this._itemNavigation = new ItemNavigation(this, {
getItemsCallback: () => this.items,
getItemsCallback: () => this._navigatableItems,
});
}

Expand All @@ -105,14 +117,22 @@ class Timeline extends UI5Element {
}

_onfocusin(e: FocusEvent) {
const target = e.target as TimelineItem;
let target = e.target as ITimelineItem | ToggleButton;
hinzzx marked this conversation as resolved.
Show resolved Hide resolved

if ((target as ITimelineItem).isGroupItem) {
target = target.shadowRoot!.querySelector<ToggleButton>("ui5-toggle-button")!;
hinzzx marked this conversation as resolved.
Show resolved Hide resolved
}

this._itemNavigation.setCurrentItem(target);
}

onBeforeRendering() {
this._itemNavigation._navigationMode = this.layout === TimelineLayout.Horizontal ? NavigationMode.Horizontal : NavigationMode.Vertical;

if (!this.items.length) {
return;
}

for (let i = 0; i < this.items.length; i++) {
this.items[i].layout = this.layout;
if (this.items[i + 1] && !!this.items[i + 1].icon) {
Expand All @@ -121,35 +141,85 @@ class Timeline extends UI5Element {
this.items[i].forcedLineWidth = LARGE_LINE_WIDTH;
}
}

this._setLastItem();
hinzzx marked this conversation as resolved.
Show resolved Hide resolved
this._setIsNextItemGroup();
this.items[0]._firstItemInTimeline = true;
hinzzx marked this conversation as resolved.
Show resolved Hide resolved
}

_setLastItem() {
const items = this.items;
hinzzx marked this conversation as resolved.
Show resolved Hide resolved
if (items && items.length > 0) {
items[items.length - 1]._lastItem = true;
}
}

_setIsNextItemGroup() {
for (let i = 0; i < this.items.length; i++) {
if (this.items[i + 1] && this.items[i + 1].isGroupItem) {
this.items[i]._isNextItemGroup = true;
}
}
}

_onkeydown(e: KeyboardEvent) {
const target = e.target as TimelineItem;
const target = e.target as ITimelineItem;

if (target.nameClickable && getEventMark(e) !== "link") {
return;
}

if (isTabNext(e)) {
if (!target.nameClickable || getEventMark(e) === "link") {
this._handleTabNextOrPrevious(e, isTabNext(e));
}
this._handleNextOrPreviousItem(e, true);
} else if (isTabPrevious(e)) {
this._handleTabNextOrPrevious(e);
this._handleNextOrPreviousItem(e);
}
}

_handleTabNextOrPrevious(e: KeyboardEvent, isNext?: boolean) {
const target = e.target as TimelineItem;
const nextTargetIndex = isNext ? this.items.indexOf(target) + 1 : this.items.indexOf(target) - 1;
const nextTarget = this.items[nextTargetIndex] as TimelineItem;
_handleNextOrPreviousItem(e: KeyboardEvent, isNext?: boolean) {
const target = e.target as ITimelineItem | ToggleButton;
let updatedTarget = target;

if ((target as ITimelineItem).isGroupItem) {
updatedTarget = target.shadowRoot!.querySelector<ToggleButton>("ui5-toggle-button")!;
}

const nextTargetIndex = isNext ? this._navigatableItems.indexOf(updatedTarget) + 1 : this._navigatableItems.indexOf(updatedTarget) - 1;
const nextTarget = this._navigatableItems[nextTargetIndex];

if (!nextTarget) {
return;
}
if (nextTarget.nameClickable && !isNext) {

if (nextTarget) {
e.preventDefault();
nextTarget.focusLink();
return;
nextTarget.focus();
this._itemNavigation.setCurrentItem(nextTarget);
}
}

get _navigatableItems() {
const navigatableItems: Array<ITimelineItem | ToggleButton> = [];

if (!this.items.length) {
return [];
}
e.preventDefault();
nextTarget.focus();
this._itemNavigation.setCurrentItem(nextTarget);

this.items.forEach(item => {
hinzzx marked this conversation as resolved.
Show resolved Hide resolved
hinzzx marked this conversation as resolved.
Show resolved Hide resolved
if (!item.isGroupItem) {
hinzzx marked this conversation as resolved.
Show resolved Hide resolved
navigatableItems.push(item);
} else {
navigatableItems.push(item.shadowRoot!.querySelector<ToggleButton>("ui5-toggle-button")!);
}

if (item.isGroupItem && !item.collapsed) {
item.items?.forEach(groupItem => {
navigatableItems.push(groupItem);
});
}
});

return navigatableItems;
}
}

Expand Down
25 changes: 25 additions & 0 deletions packages/fiori/src/TimelineGroupItem.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<div class="ui5-tlgi-root">
<div class="ui5-tlgi-btn-root">
<div class="ui5-tlgi-icon-placeholder">
<div class="ui5-tlgi-icon-dot"></div>
</div>

<div class="ui5-tlgi-line-placeholder">
<div class="ui5-tlgi-line"></div>
</div>

<ui5-toggle-button
icon="{{groupItemIcon}}"
@click="{{onGroupItemClick}}"
class="ui5-tlgi-btn"
.pressed="{{collapsed}}"
>{{groupName}}</ui5-toggle-button>
</div>
<ul class="ui5-tl-group-item">
{{#each items}}
<li class="ui5-timeline-group-list-item">
<slot name="{{this._individualSlot}}"></slot>
</li>
{{/each}}
</ul>
</div>
160 changes: 160 additions & 0 deletions packages/fiori/src/TimelineGroupItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";
import ToggleButton from "@ui5/webcomponents/dist/ToggleButton.js";
import TimelineLayout from "./types/TimelineLayout.js";
import TimelineItem from "./TimelineItem.js";
import type { ITimelineItem } from "./Timeline.js";

import TimelineGroupItemTemplate from "./generated/templates/TimelineGroupItemTemplate.lit.js";

// Styles
import TimelineGroupItemCss from "./generated/themes/TimelineGroupItem.css.js";

const SHORT_LINE_WIDTH = "ShortLineWidth";
const LARGE_LINE_WIDTH = "LargeLineWidth";

/**
* @class
*
* ### Overview
*
* An entry posted on the timeline.
* It is intented to represent a group of `<ui5-timeline-item>`s.
*
* **Note**: Please do not use empty groups in order to preserve the intended design.
*
* @constructor
* @extends UI5Element
hinzzx marked this conversation as resolved.
Show resolved Hide resolved
* @public
* @since 2.1.0
*/
@customElement({
tag: "ui5-timeline-group-item",
renderer: litRender,
styles: TimelineGroupItemCss,
template: TimelineGroupItemTemplate,
dependencies: [TimelineItem, ToggleButton],
})
class TimelineGroupItem extends UI5Element implements ITimelineItem {
/**
* Defines the text of the button that expands and collapses the group.
* @public
*/
@property()
groupName = "";
hinzzx marked this conversation as resolved.
Show resolved Hide resolved

/**
* Determines if the group is collapsed or expanded.
* @public
*/
@property({ type: Boolean })
collapsed = false;

/**
* Determines the content of the `ui5-timeline-group-item`.
* @public
*/
@slot({ type: HTMLElement, individualSlots: true, "default": true })
items!: Array<ITimelineItem>;

/**
* Defines the items orientation.
* @default "Vertical"
* @private
*/
@property()
layout: `${TimelineLayout}` = "Vertical";

/**
* Shows the number of items in the group.
* @private
*/
@property({ type: Number })
itemsCount: number = 0;
hinzzx marked this conversation as resolved.
Show resolved Hide resolved

/**
* Applies to the last item in the group.
* @private
*/
@property({ type: Boolean })
_lastItem = false;
hinzzx marked this conversation as resolved.
Show resolved Hide resolved

/**
* Determines if the item afterwards is a group item.
* Intended for styling purposes.
* @private
*/
@property({ type: Boolean })
_isNextItemGroup = false;
hinzzx marked this conversation as resolved.
Show resolved Hide resolved

@property({ type: Boolean })
hidden = false;

@property({ type: Boolean })
_firstItemInTimeline = false;

@property({ noAttribute: true })
forcedTabIndex = "-1";

isGroupItem = true;

onBeforeRendering() {
if (!this.items.length) {
return;
}

this.itemsCount = this.items.length;
this._setGroupItemProps();
}

_setGroupItemProps() {
const items = this.items;
const itemsLength = items.length;

if (itemsLength && this._firstItemInTimeline) {
items[0]._firstItemInTimeline = true;
}

if (this.collapsed) {
items[itemsLength - 1]._lastItem = false;
} else if (this._lastItem) {
items[itemsLength - 1]._lastItem = true;
}

for (let i = 0; i < itemsLength; i++) {
const item = items[i];
item.positionInGroup = i + 1;
item.hidden = !!this.collapsed;
item.layout = this.layout;

if (items[i + 1] && !!items[i + 1].icon) {
item.forcedLineWidth = SHORT_LINE_WIDTH;
} else if (item.icon && items[i + 1] && !items[i + 1].icon) {
item.forcedLineWidth = LARGE_LINE_WIDTH;
}
}
}

onGroupItemClick() {
this.collapsed = !this.collapsed;
hinzzx marked this conversation as resolved.
Show resolved Hide resolved
}

get _groupName() {
return this.groupName;
}

get groupItemIcon() {
if (this.layout === TimelineLayout.Vertical) {
return this.collapsed ? "slim-arrow-left" : "slim-arrow-down";
}

return this.collapsed ? "slim-arrow-up" : "slim-arrow-right";
}
}

TimelineGroupItem.define();

export default TimelineGroupItem;
Loading