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
95 changes: 95 additions & 0 deletions packages/fiori/src/SearchField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
import type Button from "@ui5/webcomponents/dist/Button.js";
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
import type { IOption, SelectChangeEventDetail } from "@ui5/webcomponents/dist/Select.js";
import { isPhone } from "@ui5/webcomponents-base/dist/Device.js";
import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
import type { ResizeObserverCallback } from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
import type { ListItemClickEventDetail } from "@ui5/webcomponents/dist/List.js";

import {
isEnter,
Expand All @@ -23,6 +27,8 @@
SEARCH_FIELD_LABEL,
} from "./generated/i18n/i18n-defaults.js";

const SCREEN_WIDTH_BREAKPOINT = 450;

/**
* Interface for components that may be slotted inside a `ui5-search`
* @public
Expand Down Expand Up @@ -207,11 +213,53 @@
@property({ type: Boolean })
_effectiveShowClearIcon = false;

/**
* Indicates whether the component is rendering on a small screen (mobile).
* @private
*/
@property({ type: Boolean })
_isMobileView = false;


Check failure on line 223 in packages/fiori/src/SearchField.ts

View workflow job for this annotation

GitHub Actions / check

More than 1 blank line not allowed
/**
* Indicates whether the scope selection popover is open on mobile.
* @private
*/
@property({ type: Boolean })
_scopePopoverOpen = false;

_scopeIconButton?: HTMLElement;

@i18n("@ui5/webcomponents-fiori")
static i18nBundle: I18nBundle;

onEnterDOM() {
ResizeHandler.register(this, this._handleResize.bind(this) as ResizeObserverCallback);
this._isMobileView = this._isSmallScreen();
}

onExitDOM() {
ResizeHandler.deregister(this, this._handleResize.bind(this) as ResizeObserverCallback);
}

onBeforeRendering() {
this._effectiveShowClearIcon = (this.showClearIcon && !!this.value);
this._isMobileView = this._isSmallScreen();
}

private _isSmallScreen(): boolean {
return isPhone() || window.innerWidth < SCREEN_WIDTH_BREAKPOINT;
}

private _handleResize() {
const newMobileView = this._isSmallScreen();
if (this._isMobileView !== newMobileView) {
this._isMobileView = newMobileView;
// Close popover when switching modes to prevent state issues
if (this._scopePopoverOpen) {
this._scopePopoverOpen = false;
}
}
}

_onkeydown(e:KeyboardEvent) {
Expand Down Expand Up @@ -276,6 +324,38 @@
});
}

_handleScopeIconPress() {
if (!this.scopes?.length) {
return;
}
this._scopePopoverOpen = !this._scopePopoverOpen;
}

_handleScopePopoverClose() {
this._scopePopoverOpen = false;
}

_handleScopeItemClick(e: CustomEvent<ListItemClickEventDetail>) {
const listItem = e.detail.item;
if (!listItem) {
return;
}

const scopeValue = listItem.getAttribute("data-scope-value");
const scopeItem = this.scopes.find((scope: ISearchScope) =>
scope.value === scopeValue

Check failure on line 346 in packages/fiori/src/SearchField.ts

View workflow job for this annotation

GitHub Actions / check

Missing trailing comma

Check failure on line 346 in packages/fiori/src/SearchField.ts

View workflow job for this annotation

GitHub Actions / check

Expected no linebreak before this expression
);

Check failure on line 347 in packages/fiori/src/SearchField.ts

View workflow job for this annotation

GitHub Actions / check

Unexpected newline before ')'

if (scopeItem) {
this.scopeValue = scopeItem.value;
this.fireDecoratorEvent("scope-change", {
scope: scopeItem,
});
}

this._scopePopoverOpen = false;
}

get _isSearchIcon() {
return this.value.length && this.focusedInnerInput;
}
Expand All @@ -295,6 +375,15 @@
};
}

get _scopeIconAccessibleName(): string {
const selectedScope = this.scopes.find((scope: ISearchScope) =>
scope.value === this.scopeValue

Check failure on line 380 in packages/fiori/src/SearchField.ts

View workflow job for this annotation

GitHub Actions / check

Missing trailing comma

Check failure on line 380 in packages/fiori/src/SearchField.ts

View workflow job for this annotation

GitHub Actions / check

Expected no linebreak before this expression
);

Check failure on line 381 in packages/fiori/src/SearchField.ts

View workflow job for this annotation

GitHub Actions / check

Unexpected newline before ')'
return selectedScope
? `${this._translations.scope}: ${selectedScope.text}`
: this._translations.scope;
}

get _effectiveIconTooltip() {
return this._translations.searchIcon;
}
Expand All @@ -304,6 +393,12 @@
ref.scopeOption = this;
}
}

captureScopeIconRef(ref: HTMLElement | null) {
if (ref) {
this._scopeIconButton = ref;
}
}
}

SearchField.define();
Expand Down
41 changes: 41 additions & 0 deletions packages/fiori/src/SearchFieldScopePopoverTemplate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import ResponsivePopover from "@ui5/webcomponents/dist/ResponsivePopover.js";
import List from "@ui5/webcomponents/dist/List.js";
import ListItemStandard from "@ui5/webcomponents/dist/ListItemStandard.js";
import ListSeparator from "@ui5/webcomponents/dist/types/ListSeparator.js";
import PopoverPlacement from "@ui5/webcomponents/dist/types/PopoverPlacement.js";
import PopoverHorizontalAlign from "@ui5/webcomponents/dist/types/PopoverHorizontalAlign.js";
import type SearchField from "./SearchField.js";

export default function SearchFieldScopePopoverTemplate(this: SearchField) {
if (!this._isMobileView || !this.scopes?.length || !this._scopePopoverOpen) {
return null;
}

return (
<ResponsivePopover
id={`${this._id}-scope-popover`}
class="ui5-search-field-scope-popover"
hideArrow={true}
modal={false}
placement={PopoverPlacement.Bottom}
horizontalAlign={PopoverHorizontalAlign.Start}
open={this._scopePopoverOpen}
opener={this._scopeIconButton || this}
onClose={this._handleScopePopoverClose}
>
<List
separators={ListSeparator.None}
onItemClick={this._handleScopeItemClick.bind(this)}
>
{this.scopes.map(scopeOption => (
<ListItemStandard
data-scope-value={scopeOption.value}
selected={scopeOption.value === this.scopeValue}
>
{scopeOption.text}
</ListItemStandard>
))}
</List>
</ResponsivePopover>
);
}
60 changes: 43 additions & 17 deletions packages/fiori/src/SearchFieldTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import Select from "@ui5/webcomponents/dist/Select.js";
import type SearchField from "./SearchField.js";
import decline from "@ui5/webcomponents-icons/dist/decline.js";
import search from "@ui5/webcomponents-icons/dist/search.js";
import slimArrowDown from "@ui5/webcomponents-icons/dist/slim-arrow-down.js";
import ButtonDesign from "@ui5/webcomponents/dist/types/ButtonDesign.js";
import BusyIndicator from "@ui5/webcomponents/dist/BusyIndicator.js";
import SearchFieldScopePopoverTemplate from "./SearchFieldScopePopoverTemplate.js";

export type SearchFieldTemplateOptions = {
/**
Expand Down Expand Up @@ -35,23 +37,46 @@ export default function SearchFieldTemplate(this: SearchField, options?: SearchF
<div class="ui5-search-field-content">
{this.scopes?.length ? (
<>
<Select
onChange={this._handleScopeChange}
class="ui5-search-field-select"
accessibleName={this._translations.scope}
tooltip={this._translations.scope}
value={this.scopeValue}
>
{this.scopes.map(scopeOption => (
<Option
value={scopeOption.value}
data-ui5-stable={scopeOption.stableDomRef}
ref={this.captureRef.bind(scopeOption)}
>{scopeOption.text}
</Option>
))}
</Select>
<div class="ui5-search-field-separator"></div>
{!this._isMobileView ? (
// Desktop/Tablet: Show full Select component
<>
<Select
onChange={this._handleScopeChange}
class="ui5-search-field-select"
accessibleName={this._translations.scope}
tooltip={this._translations.scope}
value={this.scopeValue}
>
{this.scopes.map(scopeOption => (
<Option
value={scopeOption.value}
data-ui5-stable={scopeOption.stableDomRef}
ref={this.captureRef.bind(scopeOption)}
>{scopeOption.text}
</Option>
))}
</Select>
<div class="ui5-search-field-separator"></div>
</>
) : (
// Mobile/Phone: Show icon button only
<>
<Button
ref={this.captureScopeIconRef.bind(this)}
class="ui5-search-field-scope-button"
icon={slimArrowDown}
design={ButtonDesign.Transparent}
onClick={this._handleScopeIconPress}
tooltip={this._scopeIconAccessibleName}
accessibleName={this._scopeIconAccessibleName}
accessibilityAttributes={{
hasPopup: "listbox",
expanded: this._scopePopoverOpen,
}}
/>
<div class="ui5-search-field-separator"></div>
</>
)}
</>
) : this.filterButton?.length ? (
<>
Expand Down Expand Up @@ -100,6 +125,7 @@ export default function SearchFieldTemplate(this: SearchField, options?: SearchF
></Icon>
</div>
</div>
{SearchFieldScopePopoverTemplate.call(this)}
</BusyIndicator>
)
);
Expand Down
56 changes: 53 additions & 3 deletions packages/fiori/src/themes/SearchField.css
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,11 @@
/* scope select */
[ui5-select] {
outline: none;
margin: var(--_ui5_search_input_scope_margin);
max-width: 10rem;
margin: 0 0.25rem;
width: fit-content;
min-width: auto;
max-width: 18rem;
flex: 0 1 auto;
border-radius: var(--_ui5_search_input_border_radius);
border: var(--_ui5-search-border);
box-shadow: none;
Expand Down Expand Up @@ -298,4 +301,51 @@
.ui5-search-field-inner-input::selection {
background: var(--sapSelectedColor);
color: var(--sapContent_ContrastTextColor);
}
}

/* Mobile scope selector button */
.ui5-search-field-scope-button {
min-width: 2rem;
width: 2rem;
height: var(--_ui5-search-select-height);
margin: 0 0.25rem;
border-radius: var(--_ui5_search_input_border_radius);
border: var(--_ui5-search-border);
box-shadow: none;
background: var(--_ui5-search-elements-background);
color: var(--sapShell_InteractiveTextColor);
}

.ui5-search-field-scope-button::part(button)::before,
.ui5-search-field-scope-button::part(button)::after {
display: none;
}

.ui5-search-field-scope-button:hover {
box-shadow: var(--_ui5-search_input_scope_hover_shadow);
background-color: var(--sapShell_Hover_Background);
}

.ui5-search-field-scope-button:active,
.ui5-search-field-scope-button:focus-within {
box-shadow: var(--_ui5-search_input_scope_active_shadow);
background: var(--sapShell_Active_Background);
color: var(--sapShell_Active_TextColor);
}

/* Scope popover on mobile */
.ui5-search-field-scope-popover {
min-width: 12rem;
}

.ui5-search-field-scope-popover::part(content) {
padding: 0;
}

.ui5-search-field-scope-popover [ui5-list] {
border: none;
}

.ui5-search-field-scope-popover [ui5-li-standard][selected] {
background-color: var(--sapList_SelectionBackgroundColor);
}
18 changes: 18 additions & 0 deletions packages/fiori/test/pages/Search.html
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,8 @@
{ name: 'Manage Products', scope: 'apps' },
{ name: 'Mobile Phones', scope: 'products' },
{ name: 'Tablet', scope: 'products' },
{ name: 'Long Scope', scope: 'long' },
{ name: 'Very long scope', scope: 'long' },
];

function createScopeItems(scope) {
Expand Down Expand Up @@ -584,5 +586,21 @@
}
});
</script>

<div class="container last">
<ui5-label>Search with Long Scope Labels - Testing scope dropdown with extensive text</ui5-label>
<ui5-search id="search-long-scopes" show-clear-icon scope-value="all-categories" placeholder="Search by category">
<ui5-search-scope text="All Categories and Subcategories" value="all-categories" slot="scopes"></ui5-search-scope>
<ui5-search-scope text="Enterprise Resource Planning Systems" value="erp" slot="scopes"></ui5-search-scope>
<ui5-search-scope text="Customer Relationship Management" value="crm" slot="scopes"></ui5-search-scope>
<ui5-search-scope text="Supply Chain Management Solutions" value="scm" slot="scopes"></ui5-search-scope>
<ui5-search-scope text="Human Capital Management and Workforce Analytics" value="hcm" slot="scopes"></ui5-search-scope>
<ui5-search-scope text="Business Intelligence and Data Warehousing" value="bi" slot="scopes"></ui5-search-scope>
</ui5-search>
<ui5-text style="padding-top: 0.25rem; font-style: italic;">
This example demonstrates how the scope dropdown handles long text labels and proper truncation/wrapping behavior.
</ui5-text>
</div>

</body>
</html>
Loading
Loading