@lotics/ui 7.19.2 → 7.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "7.19.2",
3
+ "version": "7.19.3",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./tokens": "./src/tokens.ts",
@@ -190,4 +190,43 @@ describe("useOptionList", () => {
190
190
  });
191
191
  expect(view.result.current.rows.map((r) => r.option.value)).toEqual(["z"]);
192
192
  });
193
+
194
+ it("controlled server-filtered mode browses recents on an empty query", () => {
195
+ const recentOptions = [
196
+ { value: "r1", label: "Recent one" },
197
+ { value: "r2", label: "Recent two" },
198
+ ];
199
+ // A server search has nothing for an empty term — recents are the idle list.
200
+ const params: UseOptionListParams<string, false> = {
201
+ options: [],
202
+ search: { mode: "controlled", query: "", serverFiltered: true },
203
+ recentOptions,
204
+ };
205
+ const view = renderHook((p: UseOptionListParams<string, false>) => useOptionList(p), {
206
+ initialProps: params,
207
+ });
208
+ expect(view.result.current.searching).toBe(false);
209
+ expect(view.result.current.rows.map((r) => r.option.value)).toEqual(["r1", "r2"]);
210
+ // Typing hands the list back to the server's results.
211
+ view.rerender({
212
+ ...params,
213
+ options: [{ value: "z", label: "Zucchini" }],
214
+ search: { mode: "controlled", query: "zu", serverFiltered: true },
215
+ });
216
+ expect(view.result.current.rows.map((r) => r.option.value)).toEqual(["z"]);
217
+ });
218
+
219
+ it("an empty controlled query without recents browses the full option set", () => {
220
+ // The `recentOptions ?? options` idle contract — a consumer that supplies
221
+ // browse options (server or local) and no recents keeps its full list.
222
+ for (const serverFiltered of [true, false]) {
223
+ const view = renderHook((p: UseOptionListParams<string, false>) => useOptionList(p), {
224
+ initialProps: {
225
+ options: OPTS,
226
+ search: { mode: "controlled" as const, query: "", serverFiltered },
227
+ },
228
+ });
229
+ expect(view.result.current.rows.map((r) => r.option.value)).toEqual(["a", "b", "c"]);
230
+ }
231
+ });
193
232
  });
@@ -172,12 +172,15 @@ export function useOptionList<T extends string, MULTI extends boolean = false, D
172
172
  [options],
173
173
  );
174
174
  const filtered = useMemo(() => {
175
- if (searchMode === "none" || serverFiltered) return cleanOptions;
175
+ if (searchMode === "none") return cleanOptions;
176
176
  if (!searching) {
177
177
  // Empty query: a controlled (Combobox) field browses recents, else its full
178
- // local set; an internal field shows everything.
178
+ // local set; an internal field shows everything. This must precede the
179
+ // server-filtered short-circuit — a server search returns nothing for an
180
+ // empty term, so recents are the only idle content it has.
179
181
  return searchMode === "controlled" ? (recentOptions ?? cleanOptions) : cleanOptions;
180
182
  }
183
+ if (serverFiltered) return cleanOptions;
181
184
  const q = normalizeForSearch(query);
182
185
  return cleanOptions.filter((o) => normalizeForSearch(o.label ?? o.value).includes(q));
183
186
  }, [searchMode, serverFiltered, searching, cleanOptions, recentOptions, query]);