Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
26 changes: 26 additions & 0 deletions packages/fiori/src/ShellBarSearch.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import Search from "./Search.js";
import { isPhone } from "@ui5/webcomponents-base/dist/Device.js";
Expand All @@ -10,6 +11,7 @@ import {
SHELLBAR_SEARCH_EXPANDED,
SHELLBAR_SEARCH_COLLAPSED,
} from "./generated/i18n/i18n-defaults.js";
import type BusyIndicator from "@ui5/webcomponents/dist/BusyIndicator.js";

/**
* @class
Expand All @@ -19,6 +21,7 @@ import {
* @public
* @since 2.10.0
* @experimental
* @slot {Array<HTMLElement>} busyIndicator - Defines the busy indicator to be displayed as an overlay.
*/
@customElement({
tag: "ui5-shellbar-search",
Expand All @@ -38,6 +41,24 @@ class ShellBarSearch extends Search {
@property({ type: Boolean })
autoOpen = false;

/**
* Defines the busy indicator to be displayed as an overlay.
* @public
*/
@slot({
type: HTMLElement,
invalidateOnChildChange: true,
})
busyIndicator!: Array<BusyIndicator>;

/**
* Tracks if the slotted BusyIndicator is active.
* Used to apply reduced opacity to the search field content when busy.
* @private
*/
@property({ type: Boolean })
_isBusy = false;

_handleSearchIconPress() {
super._handleSearchIconPress();

Expand Down Expand Up @@ -94,6 +115,11 @@ class ShellBarSearch extends Search {
onBeforeRendering(): void {
super.onBeforeRendering();

// Track if busy indicator is active to apply opacity to search content.
// The busy indicator renders as an overlay in the shadow DOM, so we reduce
// opacity of the underlying search field to show the busy state visually.
this._isBusy = this.busyIndicator.length > 0 && this.busyIndicator[0].hasAttribute("active");

if (isPhone()) {
this.collapsed = true;
}
Expand Down
5 changes: 4 additions & 1 deletion packages/fiori/src/ShellBarSearchTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import ShellBarSearchPopoverTemplate from "./ShellBarSearchPopoverTemplate.js";
export default function ShellBarSearchTemplate(this: ShellBarSearch) {
return (
<>
{ SearchFieldTemplate.call(this) }
<div class="ui5-shellbar-search-busy-wrapper">
{ SearchFieldTemplate.call(this) }
<slot name="busyIndicator"></slot>
</div>
{ ShellBarSearchPopoverTemplate.call(this) }
</>
);
Expand Down
12 changes: 12 additions & 0 deletions packages/fiori/src/themes/ShellBarSearch.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
:host(:not([collapsed])) {
min-width: 13rem;
}

.ui5-shellbar-search-busy-wrapper {
position: relative;
display: contents;
}

/* Apply reduced opacity when busy indicator is active.
The busy indicator is slotted and renders as an overlay on top,
so we reduce opacity of the search field underneath to indicate busy state. */
:host([_is-busy]) .ui5-search-field-root {
opacity: var(--sapContent_DisabledOpacity);
}
131 changes: 131 additions & 0 deletions packages/fiori/test/pages/ShellBar_busy_search.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>ShellBar - Busy Search Demo</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

<script data-ui5-config type="application/json">
{
"rtl": false
}
</script>

<script src="%VITE_BUNDLE_PATH%" type="module"></script>

<style>
body {
font-family: "72", "72full", Arial, Helvetica, sans-serif;
padding: 1rem;
}

.demo-section {
margin-bottom: 2rem;
}

h2 {
margin-bottom: 1rem;
}

.controls {
margin-bottom: 1rem;
}
</style>
</head>

<body>
<div class="demo-section">
<h2>ShellBar with Busy Search - POC</h2>
<p>This demo shows a BusyIndicator slotted inside ShellBarSearch, rendering as an overlay.</p>

<div class="controls">
<ui5-button id="toggleBusy">Toggle Busy State</ui5-button>
</div>

<ui5-shellbar
id="shellbar"
primary-title="Corporate Portal"
secondary-title="secondary title"
show-notifications
notifications-count="22">

<ui5-shellbar-search
id="searchField"
placeholder="Search"
slot="searchField">
<ui5-busy-indicator
id="busyIndicator"
slot="busyIndicator"
size="S"
delay="0">
</ui5-busy-indicator>
</ui5-shellbar-search>

<ui5-avatar slot="profile" initials="JD"></ui5-avatar>
</ui5-shellbar>
</div>

<div class="demo-section">
<h2>Standalone ShellBarSearch with Busy Indicator</h2>
<p>ShellBarSearch outside of ShellBar for easier testing.</p>

<div class="controls">
<ui5-button id="toggleBusyStandalone">Toggle Busy State</ui5-button>
</div>

<ui5-shellbar-search
id="standaloneSearch"
placeholder="Search items...">
<ui5-busy-indicator
id="standaloneBusy"
slot="busyIndicator"
size="M"
delay="0">
</ui5-busy-indicator>
</ui5-shellbar-search>
</div>

<div class="demo-section">
<h2>Comparison: Wrapped in BusyIndicator (Old Approach)</h2>
<p>For comparison: ShellBarSearch wrapped in BusyIndicator instead of slotted.</p>

<div class="controls">
<ui5-button id="toggleBusyWrapped">Toggle Busy State</ui5-button>
</div>

<ui5-busy-indicator
id="wrappedBusy"
size="M"
delay="0">
<ui5-shellbar-search
id="wrappedSearch"
placeholder="Search wrapped...">
</ui5-shellbar-search>
</ui5-busy-indicator>
</div>

<script>
const busyIndicator = document.getElementById("busyIndicator");
const toggleButton = document.getElementById("toggleBusy");

toggleButton.addEventListener("click", () => {
busyIndicator.active = !busyIndicator.active;
});

const standaloneBusy = document.getElementById("standaloneBusy");
const toggleStandaloneButton = document.getElementById("toggleBusyStandalone");

toggleStandaloneButton.addEventListener("click", () => {
standaloneBusy.active = !standaloneBusy.active;
});

const wrappedBusy = document.getElementById("wrappedBusy");
const toggleWrappedButton = document.getElementById("toggleBusyWrapped");

toggleWrappedButton.addEventListener("click", () => {
wrappedBusy.active = !wrappedBusy.active;
});
</script>
</body>
</html>

30 changes: 30 additions & 0 deletions packages/main/src/BusyIndicatorTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,36 @@ import type BusyIndicator from "./BusyIndicator.js";
import Label from "./Label.js";

export default function BusyIndicatorTemplate(this: BusyIndicator) {
// Overlay mode: When BusyIndicator has no slotted content, render only the busy area
// as an absolute positioned overlay. This allows it to be slotted inside other components
// without wrapping them, avoiding slot contract violations.
if (!this.hasContent) {
return this._isBusy ? (
<div
class={{
"ui5-busy-indicator-busy-area": true,
"ui5-busy-indicator-busy-area-overlay": true,
}}
title={this.ariaTitle}
tabindex={0}
role="progressbar"
aria-valuemin={0}
aria-valuemax={100}
aria-valuetext="Busy"
aria-labelledby={this.labelId}
data-sap-focus-ref
>
{this.textPosition.top && BusyIndicatorBusyText.call(this)}
<div class="ui5-busy-indicator-circles-wrapper">
<div class="ui5-busy-indicator-circle circle-animation-0"></div>
<div class="ui5-busy-indicator-circle circle-animation-1"></div>
<div class="ui5-busy-indicator-circle circle-animation-2"></div>
</div>
{this.textPosition.bottom && BusyIndicatorBusyText.call(this)}
</div>
) : <></>;
}

return (
<div class="ui5-busy-indicator-root">
{this._isBusy && (
Expand Down
6 changes: 6 additions & 0 deletions packages/main/src/themes/BusyIndicator.css
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@
z-index: 99;
}

.ui5-busy-indicator-busy-area.ui5-busy-indicator-busy-area-overlay {
position: absolute;
inset: 0;
z-index: 99;
}

.ui5-busy-indicator-busy-area {
display: flex;
justify-content: center;
Expand Down
Loading