Skip to content
Merged
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
4 changes: 4 additions & 0 deletions etc/lime-elements.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,7 @@ export namespace Components {
"emptyResultMessage"?: string;
"gridLayout": boolean;
"items": Array<MenuItem | ListSeparator>;
"keepOpenOnSelect": boolean;
"loading": boolean;
"open": boolean;
"openDirection": OpenDirection;
Expand Down Expand Up @@ -2834,6 +2835,7 @@ export namespace JSX {
"emptyResultMessage"?: string;
"gridLayout"?: boolean;
"items"?: Array<MenuItem | ListSeparator>;
"keepOpenOnSelect"?: boolean;
"onCancel"?: (event: LimelMenuCustomEvent<void>) => void;
"onNavigateMenu"?: (event: LimelMenuCustomEvent<MenuItem | null>) => void;
"onSelect"?: (event: LimelMenuCustomEvent<MenuItem>) => void;
Expand All @@ -2856,6 +2858,8 @@ export namespace JSX {
// (undocumented)
"gridLayout": boolean;
// (undocumented)
"keepOpenOnSelect": boolean;
// (undocumented)
"open": boolean;
// (undocumented)
"openDirection": OpenDirection;
Expand Down
168 changes: 168 additions & 0 deletions src/components/menu/examples/menu-keep-open.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import {
MenuItem,
ListSeparator,
LimelMenuCustomEvent,
} from '@limetech/lime-elements';
import { Component, Host, State, h } from '@stencil/core';

const isApple = /Mac|iPhone|iPad|iPod/i.test(
(navigator as any).userAgentData?.platform ?? navigator.platform ?? ''

Check warning on line 9 in src/components/menu/examples/menu-keep-open.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'platform' is deprecated.

See more on https://sonarcloud.io/project/issues?id=Lundalogik_lime-elements&issues=AZ1ohkPO0ph7A0Quibsa&open=AZ1ohkPO0ph7A0Quibsa&pullRequest=4004
);
Comment thread
john-traas marked this conversation as resolved.

type SuggestionValue = {
propertyName?: string;
searchTerm?: string;
applyAll?: boolean;
};

const ALL_PROPERTIES = [
'Name',
'Company',
'City',
'Country',
'Email',
'Phone',
'Website',
];

/**
* Keeping the menu open after selection
*
* By default, a menu closes as soon as the user selects an item.
* This is the expected behavior in most cases.
*
* But in some workflows, the design may require a different interaction
* pattern: the menu stays open after selection, allowing the user to make
* multiple selections in a row.
*
* For instance, when applying multiple filters
* to a dataset: the user types a keyword, selects an item to filter on,
* then selects another item — all without leaving the keyboard.
*
* Setting `keepOpenOnSelect` to `true` keeps the menu open after
* each selection. When a `searcher` is provided, the search query
* is preserved and the `searcher` is called again, so the consumer
* can update the result list (e.g. remove the item that was just applied).
*/
@Component({
tag: 'limel-example-menu-keep-open',
shadow: true,
})
export class MenuKeepOpenExample {
@State()
private appliedFilters: string[] = [];

private get primaryHotkey() {
return isApple ? 'meta+enter' : 'ctrl+enter';
}

private get applyAllHotkey() {
return isApple ? 'meta+shift+enter' : 'ctrl+shift+enter';
}

public render() {
return (
<Host>
<limel-menu
keepOpenOnSelect={true}
searcher={this.handleSearch}
searchPlaceholder="Find property…"
onSelect={this.handleSelect}
emptyResultMessage="No matching properties"
>
<limel-chip
text="Filter"
icon={{
name: 'plus_math',
color: 'rgb(var(--contrast-800))',
title: 'Add',
}}
slot="trigger"
/>
</limel-menu>
<limel-example-value
label="Applied filters"
value={this.appliedFilters.join(', ') || '(none)'}
/>
</Host>
);
}

private readonly handleSearch = async (
queryString: string
): Promise<Array<MenuItem | ListSeparator>> => {
const searchTerm = queryString?.trim() ?? '';
if (searchTerm.length === 0) {
return [];
}

const menuItems = ALL_PROPERTIES.filter((item) => {
return (
item.toLowerCase().includes(searchTerm.toLowerCase()) &&
!this.appliedFilters.includes(item)
);
}).map((propertyName) => {
return {
text: propertyName,
value: {
propertyName,
searchTerm,
} satisfies SuggestionValue,
} as MenuItem;
});

if (menuItems.length === 0) {
return [];
}

menuItems[0] = {
...menuItems[0],
hotkey: this.primaryHotkey,
};

if (menuItems.length <= 1) {
return menuItems;
}

const applyAllMenuItem: MenuItem = {
text: 'Apply all',
hotkey: this.applyAllHotkey,
value: {
applyAll: true,
searchTerm,
} satisfies SuggestionValue,
};

return [...menuItems, { separator: true }, applyAllMenuItem];
};

private handleSelect = (

Check warning on line 139 in src/components/menu/examples/menu-keep-open.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'handleSelect' is never reassigned; mark it as `readonly`.

See more on https://sonarcloud.io/project/issues?id=Lundalogik_lime-elements&issues=AZ1ohkPO0ph7A0Quibsb&open=AZ1ohkPO0ph7A0Quibsb&pullRequest=4004
event: LimelMenuCustomEvent<MenuItem<SuggestionValue>>
) => {
const selectedItem = event.detail;
if (!selectedItem?.value) {
return;
}

if (selectedItem.value.applyAll) {
const searchTerm = selectedItem.value.searchTerm ?? '';
const remaining = ALL_PROPERTIES.filter((item) => {
return (
item.toLowerCase().includes(searchTerm.toLowerCase()) &&
!this.appliedFilters.includes(item)
);
});

this.appliedFilters = [...this.appliedFilters, ...remaining];

return;
}

if (selectedItem.value.propertyName) {
this.appliedFilters = [
...this.appliedFilters,
selectedItem.value.propertyName,
];
}
};
}
30 changes: 30 additions & 0 deletions src/components/menu/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const DEFAULT_ROOT_BREADCRUMBS_ITEM: BreadcrumbsItem = {
* @exampleComponent limel-example-menu-searchable
* @exampleComponent limel-example-menu-hotkeys
* @exampleComponent limel-example-menu-searchable-hotkeys
* @exampleComponent limel-example-menu-keep-open
* @exampleComponent limel-example-menu-composite
*/
@Component({
Expand Down Expand Up @@ -188,6 +189,12 @@ export class Menu {
@Prop()
public emptyResultMessage?: string;

/**
* When `true`, the menu stays open after an item is selected.
*/
@Prop({ reflect: true })
public keepOpenOnSelect = false;
Comment thread
Kiarokh marked this conversation as resolved.

/**
* Is emitted when a menu item with a sub-menu is selected.
*/
Expand Down Expand Up @@ -771,6 +778,20 @@ export class Menu {
}
};

private readonly refreshSearch = async () => {
const query = this.searchValue;
this.loadingSubItems = true;

const result = await this.searcher(query);
if (this.searchValue !== query) {
return;
}

this.searchResults = result;
this.loadingSubItems = false;
this.setFocus();
};
Comment thread
Kiarokh marked this conversation as resolved.

private readonly clearSearch = () => {
this.searchValue = '';
this.searchResults = null;
Expand Down Expand Up @@ -913,6 +934,15 @@ export class Menu {
this.loadingSubItems = false;

this.select.emit(menuItem);

if (this.keepOpenOnSelect) {
if (isFunction(this.searcher) && this.searchValue) {
this.refreshSearch();
}

return;
}

this.open = false;
this.currentSubMenu = null;
setTimeout(this.focusTrigger, 0);
Expand Down
Loading