Skip to content

Commit

Permalink
[HLD] Select custom filtering. (#2125)
Browse files Browse the repository at this point in the history
# Pull Request

## 🤨 Rationale

- #1866
  • Loading branch information
atmgrifter00 authored May 31, 2024
1 parent 06b0317 commit 7f81793
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "none",
"comment": "Update spec with new APIs for Select custom filtering.",
"packageName": "@ni/nimble-components",
"email": "26874831+atmgrifter00@users.noreply.github.com",
"dependentChangeType": "none"
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,66 @@ Our clients have a need of a filterable dropdown component that does not allow a

#### Select

We will provide a means for clients to enable this feature with a new attribute:
We will provide a means for clients to enable this feature with some new attributes:

```ts
export class Select() {
...
@attr({ attribute: 'filter-mode' })
public filterMode = FilterMode.none;

/**
* Displays a message at the bottom of the dropdown to indicate more options
* are currently being loaded.
*/
@attr({ attribute: 'loading-visible', mode: 'boolean' })
public loadingVisible = false;
...
}

// Provide enum for filterMode to allow for future modes
export const FilterMode = {
none: undefined;
standard: 'standard';
manual: 'manual';
} as const;
```

- The `standard` filterMode will result in case-insensitive, diacritic-insensitive filtering.
- `filterMode` will default to `none` so as not to affect existing clients.
- The `manual` filterMode will display a search input, but it won't automatically filter the options as the user types. Instead, clients can use the `filter-input` event described in the ['Dynamic Options'](#dynamic-options) section below to implement their own filtering logic.
- The `none` filter mode results in no search input being shown in the dropdown.
- `filterMode` will default to `none` so as not to affect existing clients.

_Note: The `filterMode` isn't meant to mirror the `Combobox` `autocomplete` API, as they do serve slightly different purposes: The `autocomplete` for the `Combobox` ultimately helps set the actual value of the `Combobox` as the user types, and isn't necessarily performing any filtering (e.g. the `inline` mode). One possible concern, however, is that we are presenting an API that will allow different types of filter behaviors (i.e. case sensitive) that the `Combobox` does not support. Additionally, I am proposing diacritic insensitive filtering, which the `Combobox` also does not currently support, but I feel this is quite likely a better default experience._

#### Dynamic options

The `filterMode: manual` option expects clients to dynamically supply `list-option`s to the `Select` component in response to user search text, by subscribing to the `filter-input` event.

```ts
interface SelectFilterInputEventDetail {
filterText: string;
}
```

Then, when handling the `filter-input` event, clients should perform the desired filtering using the provided filter text. Additionally, they should set the `loading` attribute to `true` while the options are being loaded into the DOM, and then set it back to `false` when the loading process is complete. The `filter-input` event is emitted any time the user types a character in the filter input and also when the dropdown is closed (in which case the `filterText` in the details will always be an empty string).

_IMPORTANT_: When using the `manual` filter mode, clients are responsible for the following:

- Either adding/removing options to/from the DOM that do or do not match the filter and/or marking existing options as `hidden` when they don't match.
- The currently selected option is included in the set of options placed in the DOM regardless of match against filter (and be marked as `hidden` if it doesn't match the filter)
- If a placeholder option exists for the select, it should never be filtered out.

_Notes_:

- When the `loading-visible` attribute is set, we will display localizable text at the bottom of the dropdown (defaulting to "Loading"), along with the spinner icon.
- The `Select` will not peform any debouncing as the user types into the filter input. It is expected that clients can perform any debouncing that is needed easily at the app level.
- The `filter-input` event will be emitted for all filter modes (except `none`)

##### Groups

When using the `filterMode: manual` option along with [groups](./option-groups-hld.md), clients will also need to determine the appropriate matching behavior against the `ListOptionGroup` elements. Our guidance will be that filter text should be able to be matched against _any_ text in the group label, and when that matches, _all_ options within that group should be visible. Conversely, the select will automatically hide groups with no items in them. This means clients only need to conditionally remove items, not groups (though it's fine if they manually remove empty groups too).

#### LabelProviderCore

We will be showing text that will require localization as part of this feature. It seems reasonable to add these APIs to the existing `LabelProviderCore` class as opposed to creating a new provider:
Expand All @@ -55,12 +92,16 @@ export class LabelProviderCore

@attr({ attribute: 'select-filter-no-results' })
public selectFilterNoResults: string | undefined;

@attr({ attribute: 'loading' })
public loading: string | undefined;
```

The English strings used for these labels will be:

- selectFilterSearch: "Search"
- selectFilterNoResults: "No items found"
- loading: "Loading"

### Implementation details

Expand Down Expand Up @@ -88,25 +129,19 @@ The accessibility tree will report that the search `input` element should have i

### Future considerations

#### Grouping/Metadata
We may want to improve/alter the discoverability of the fact that more options are available to be loaded when using the `manual` filtering option. Currently, before a user begins typing there is nothing to suggest that there could be other options that are available to select.

One feature that we intend to add to the `Select` is the ability to specify "groups" of options, where each group will have non-selectable header text followed by the options under that group. Ultimately, this feature will have to work nicely with filtering, but I don't believe there are aspects of this that would interfere with the current proposed API in this HLD of a single attribute that specifies how the filter text is applied to a target.
#### Combobox alignment

There is also a desire to allow the [`ListOption` to contain complex content](https://github.com/ni/nimble/issues/1135). This could include content that also supplies some metadata that _could_ be used for filtering purposes. The current proposed API is meant to inform _how_ the filter text is applied to a target, not _what_ the target is, so I suspect if we ever need to provide a means to the client to change what the target for the filter is, then that would be a different API.
It's reasonable to think that a client may want to have the same UX and Nimble-provided APIs as the `Select` for dynamic options.

#### More filter modes
#### Complex content

It may be desireable to have other filter modes in the future, such as case sensitive, or even regular expressions. By making the new API an enum, we can easily add new modes as needed.

#### Dynamic fetching of options

We know that there is a use-case with the `Combobox` to dynamically fetch options from a server that match the pattern provided in the input field, and so it isn't a stretch that a client might want the same capability in the `Select`. However, this is currently accomplished through turning off the `Combobox` `autocomplete` mode, and essentially having the client provide a custom behavior.
There is a desire to allow the [`ListOption` to contain complex content](https://github.com/ni/nimble/issues/1135). This could include content that also supplies some metadata that _could_ be used for filtering purposes. The current proposed API is meant to inform _how_ the filter text is applied to a target, not _what_ the target is, so I suspect if we ever need to provide a means to the client to change what the target for the filter is, then that would be a different API.

The `Select` presents its own challenges for providing a similar ability:
#### More filter modes

- Should the `Combobox` and `Select` provide mirrored APIs for this? Currently, this doesn't seem possible in Angular as the `Combobox` relies on users accessing its `value` property from the `nativeElement` to use for the filter, in combination with listening to native `input` events. The `Select` would either need to work differently, or the `Combobox` would have to be updated.
- Would this feature be enabled through another mode on the `filterMode` enum (i.e. a `dynamic` or `custom` mode), or is it orthogonal to the `filterMode` API?
- Are there challenges in having the filter work against local options in addition to retrieving new ones?
It may be desireable to have other filter modes in the future, such as case sensitive, or even regular expressions. By making the new API an enum, we can easily add new modes as needed.

## Alternative Implementations / Designs

Expand Down

0 comments on commit 7f81793

Please sign in to comment.