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: picker max suggestions #6818

Closed
wants to merge 2 commits into from
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "picker max suggestions",
"packageName": "@microsoft/fast-foundation",
"email": "[email protected]",
"dependentChangeType": "prerelease"
}
2 changes: 2 additions & 0 deletions packages/web-components/fast-foundation/docs/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -1551,6 +1551,7 @@ export class FASTPicker extends FormAssociatedPicker {
protected listItemTemplateChanged(): void;
loadingText: string;
maxSelected: number | null;
maxSuggestions: number | undefined;
// @internal
menuConfig: AnchoredRegionConfig;
// @internal
Expand Down Expand Up @@ -1594,6 +1595,7 @@ export class FASTPicker extends FormAssociatedPicker {
// @internal
showNoOptions: boolean;
suggestionsAvailableText: string;
trimCount: number;
}

// @beta
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,10 +202,12 @@ export class FASTTextField extends TextField {}
| `label` | public | `string` | | Applied to the aria-label attribute of the input element | |
| `labelledBy` | public | `string` | | Applied to the aria-labelledby attribute of the input element | |
| `placeholder` | public | `string` | | Applied to the placeholder attribute of the input element | |
| `maxSuggestions` | public | `number or undefined` | | Limits how many suggestions can be displayed | |
| `menuPlacement` | public | `MenuPlacement` | | Controls menu placement | |
| `showLoading` | public | `boolean` | `false` | Whether to display a loading state if the menu is opened. | |
| `listItemTemplate` | public | `ViewTemplate` | | Template used to generate selected items. This is used in a repeat directive. | |
| `defaultListItemTemplate` | public | `ViewTemplate or undefined` | | Default template to use for selected items (usually specified in the component template). This is used in a repeat directive. | |
| `trimCount` | public | `number` | `0` | How many suggestions are being trimmed off by max-suggestions | |
| `menuOptionTemplate` | public | `ViewTemplate` | | Template to use for available options. This is used in a repeat directive. | |
| `defaultMenuOptionTemplate` | public | `ViewTemplate or undefined` | | Default template to use for available options (usually specified in the template). This is used in a repeat directive. | |
| `listItemContentsTemplate` | public | `ViewTemplate` | | Template to use for the contents of a selected list item | |
Expand Down Expand Up @@ -254,6 +256,7 @@ export class FASTTextField extends TextField {}
| `label` | label | |
| `labelledby` | labelledBy | |
| `placeholder` | placeholder | |
| `max-suggestions` | maxSuggestions | |
| `menu-placement` | menuPlacement | |

<hr/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,19 @@ export function pickerTemplate<T extends FASTPicker>(
</div>
`
)}
${when(
x => x.trimCount > 0,
html<T>`
<div
class="trimmed-suggestions-display"
part="trimmed-suggestions-display"
>
<slot name="trimmed-suggestions-region">
${x => x.trimmedSuggestionsText}
</slot>
</div>
`
)}
</${anchoredRegionTag}>
`
)}
Expand Down
106 changes: 63 additions & 43 deletions packages/web-components/fast-foundation/src/picker/picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,15 @@ export class FASTPicker extends FormAssociatedPicker {
@attr({ attribute: "loading-text" })
public loadingText: string = "Loading suggestions";

/**
* The text displayed when there are additional trimmed suggestions.
*
* @remarks
* HTML Attribute: loading-text
*/
@attr({ attribute: "trimmed-suggestions-text" })
public trimmedSuggestionsText: string = "Type to see more results";

/**
* Applied to the aria-label attribute of the input element
*
Expand Down Expand Up @@ -175,6 +184,15 @@ export class FASTPicker extends FormAssociatedPicker {
@attr({ attribute: "placeholder" })
public placeholder: string;

/**
* Limits how many suggestions can be displayed
*
* @remarks
* HTML Attribute: max-suggestions
*/
@attr({ attribute: "max-suggestions" })
public maxSuggestions: number | undefined;

/**
* Controls menu placement
*
Expand Down Expand Up @@ -225,6 +243,13 @@ export class FASTPicker extends FormAssociatedPicker {
this.updateListItemTemplate();
}

/**
* How many suggestions are being trimmed off by max-suggestions
*
*/
@observable
public trimCount: number = 0;

/**
* The item template currently in use.
*
Expand Down Expand Up @@ -284,7 +309,9 @@ export class FASTPicker extends FormAssociatedPicker {
@observable
public optionsList: string[] = [];
private optionsListChanged(): void {
this.updateFilteredOptions();
if (this.$fastController.isConnected) {
this.updateFilteredOptions();
}
}

/**
Expand All @@ -299,7 +326,7 @@ export class FASTPicker extends FormAssociatedPicker {
this.inputElement.value = this.query;
}
this.updateFilteredOptions();
this.$emit("querychange", { bubbles: false });
this.$emit("querychange");
}
}

Expand All @@ -310,15 +337,6 @@ export class FASTPicker extends FormAssociatedPicker {
*/
@observable
public filteredOptionsList: string[] = [];
protected filteredOptionsListChanged(): void {
if (this.$fastController.isConnected) {
Updates.enqueue(() => {
this.showNoOptions =
this.menuElement.querySelectorAll('[role="listitem"]').length === 0;
this.setFocusedOption(this.showNoOptions ? -1 : 0);
});
}
}

/**
* Indicates if the flyout menu is open or not
Expand All @@ -330,9 +348,10 @@ export class FASTPicker extends FormAssociatedPicker {
protected flyoutOpenChanged(): void {
if (this.flyoutOpen) {
Updates.enqueue(this.setRegionProps);
this.$emit("menuopening", { bubbles: false });
this.updateFilteredOptions();
this.$emit("menuopening", {}, { bubbles: false });
} else {
this.$emit("menuclosing", { bubbles: false });
this.$emit("menuclosing", {}, { bubbles: false });
}
}

Expand Down Expand Up @@ -440,6 +459,13 @@ export class FASTPicker extends FormAssociatedPicker {
*/
@observable
public selectedItems: string[] = [];
private selectedItemsChanged(): void {
if (this.$fastController.isConnected) {
if (this.maxSelected && this.selectedItems.length > this.maxSelected) {
this.selectedItems.splice(this.maxSelected, this.selectedItems.length);
}
}
}

private optionsPlaceholder: Node;
private inputElementView: HTMLView | null = null;
Expand Down Expand Up @@ -607,31 +633,6 @@ export class FASTPicker extends FormAssociatedPicker {
}
const activeElement = this.getRootActiveElement();
switch (e.key) {
// TODO: what should "home" and "end" keys do, exactly?
//
// case keyHome: {
// if (!this.flyoutOpen) {
// this.toggleFlyout(true);
// } else {
// if (this.menuElement.optionElements.length > 0) {
// this.setFocusedOption(0);
// }
// }
// return false;
// }

// case keyEnd: {
// if (!this.flyoutOpen) {
// this.toggleFlyout(true);
// } else {
// if (this.menuElement.optionElements.length > 0) {
// this.toggleFlyout(true);
// this.setFocusedOption(this.menuElement.optionElements.length - 1);
// }
// }
// return false;
// }

case keyArrowDown: {
if (!this.flyoutOpen) {
this.toggleFlyout(true);
Expand Down Expand Up @@ -771,7 +772,7 @@ export class FASTPicker extends FormAssociatedPicker {
Updates.enqueue(() => {
this.checkMaxItems();
});
this.$emit("selectionchange", { bubbles: false });
this.$emit("selectionchange");
}

/**
Expand All @@ -780,7 +781,7 @@ export class FASTPicker extends FormAssociatedPicker {
public handleRegionLoaded(e: Event): void {
Updates.enqueue(() => {
this.setFocusedOption(0);
this.$emit("menuloaded", { bubbles: false });
this.$emit("menuloaded", {}, { bubbles: false });
});
}

Expand Down Expand Up @@ -1002,19 +1003,38 @@ export class FASTPicker extends FormAssociatedPicker {
* Updates the filtered options array
*/
private updateFilteredOptions(): void {
this.filteredOptionsList = this.optionsList.slice(0);
let newFilteredOptionsList: string[] = this.optionsList.slice(0);

if (this.filterSelected) {
this.filteredOptionsList = this.filteredOptionsList.filter(
newFilteredOptionsList = newFilteredOptionsList.filter(
el => this.selectedItems.indexOf(el) === -1
);
}
if (this.filterQuery && this.query !== "" && this.query !== undefined) {
// compare case-insensitive
const filterQuery = this.query.toLowerCase();
this.filteredOptionsList = this.filteredOptionsList.filter(
newFilteredOptionsList = newFilteredOptionsList.filter(
el => el.toLowerCase().indexOf(filterQuery) !== -1
);
}
if (this.maxSuggestions && newFilteredOptionsList.length > this.maxSuggestions) {
this.trimCount = newFilteredOptionsList.length - this.maxSuggestions;
newFilteredOptionsList.splice(this.maxSuggestions);
} else {
this.trimCount = 0;
}

this.filteredOptionsList.splice(
0,
this.filteredOptionsList.length,
...newFilteredOptionsList
);

Updates.enqueue(() => {
this.showNoOptions =
this.menuElement.querySelectorAll('[role="listitem"]').length === 0;
this.setFocusedOption(this.showNoOptions ? -1 : 0);
});
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ const pickerStyles = css`
opacity: 1;
pointer-events: none;
}
.trimmed-suggestions-display,
.loading-display,
.no-options-display {
background: var(--neutral-layer-floating);
width: 100%;
width: 100% - calc((10 + (var(--design-unit) * 2 * var(--density))) * 1px);
min-height: calc(
(var(--base-height-multiplier) + var(--density)) * var(--design-unit) * 1px
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const storyTemplate = html<StoryArgs<FASTPicker>>`
?filter-selected="${x => x.filterSelected}"
?filter-query="${x => x.filterQuery}"
max-selected="${x => x.maxSelected}"
max-suggestions="${x => x.maxSuggestions}"
no-suggestions-text="${x => x.noSuggestionsText}"
suggestions-available-text="${x => x.suggestionsAvailableText}"
loading-text="${x => x.loadingText}"
Expand All @@ -35,6 +36,7 @@ export default {
labelledBy: { control: "text" },
loadingText: { control: "text" },
maxSelected: { control: "number" },
maxSuggestions: { control: "number" },
menuPlacement: { control: "select", options: Object.values(MenuPlacement) },
noSuggestionsText: { control: "text" },
placeholder: { control: "text" },
Expand All @@ -52,3 +54,17 @@ Picker.args = {
selection: "apple",
suggestionsAvailableText: "Found some fruit",
};

export const PickerLimitSuggestions: Story<FASTPicker> = renderComponent(
storyTemplate
).bind({});
PickerLimitSuggestions.args = {
label: "Fruit picker",
loadingText: "Loading",
noSuggestionsText: "No such fruit",
options: "apple, orange, banana, mango, strawberry, raspberry, blueberry",
placeholder: "Choose fruit",
selection: "apple",
suggestionsAvailableText: "Found some fruit",
maxSuggestions: 3,
};
Loading