@stapel/search-react 0.16.0 → 0.18.1
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/CHANGELOG.md +122 -0
- package/dist/api/generated/schema.d.ts +18 -10
- package/dist/api/generated/schema.d.ts.map +1 -1
- package/dist/default/FacetGroupControl.d.ts +10 -0
- package/dist/default/FacetGroupControl.d.ts.map +1 -1
- package/dist/default/FacetGroupControl.js +122 -27
- package/dist/default/FacetGroupControl.js.map +1 -1
- package/dist/default/FacetPanelPane.d.ts +25 -38
- package/dist/default/FacetPanelPane.d.ts.map +1 -1
- package/dist/default/FacetPanelPane.js +157 -6
- package/dist/default/FacetPanelPane.js.map +1 -1
- package/dist/default/FilterChips.d.ts +25 -0
- package/dist/default/FilterChips.d.ts.map +1 -1
- package/dist/default/FilterChips.js +39 -3
- package/dist/default/FilterChips.js.map +1 -1
- package/dist/default/SearchPage.d.ts.map +1 -1
- package/dist/default/SearchPage.js +10 -1
- package/dist/default/SearchPage.js.map +1 -1
- package/dist/default/SearchResultsPane.d.ts +9 -1
- package/dist/default/SearchResultsPane.d.ts.map +1 -1
- package/dist/default/SearchResultsPane.js +20 -7
- package/dist/default/SearchResultsPane.js.map +1 -1
- package/dist/default/SortSelect.d.ts.map +1 -1
- package/dist/default/SortSelect.js +4 -1
- package/dist/default/SortSelect.js.map +1 -1
- package/dist/default/index.d.ts +1 -1
- package/dist/default/index.d.ts.map +1 -1
- package/dist/default/index.js +1 -1
- package/dist/default/index.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +5 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/keys.d.ts +19 -0
- package/dist/i18n/keys.d.ts.map +1 -1
- package/dist/i18n/keys.js +25 -0
- package/dist/i18n/keys.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +7 -0
- package/dist/i18n/ru.js.map +1 -1
- package/dist/state/facets.d.ts +14 -0
- package/dist/state/facets.d.ts.map +1 -1
- package/dist/state/facets.js +19 -0
- package/dist/state/facets.js.map +1 -1
- package/llms.txt +2 -2
- package/manifest.json +8 -2
- package/nav-manifest.json +1 -1
- package/package.json +7 -7
- package/src/analytics/generated/events.json +1 -1
- package/src/api/generated/schema.ts +18 -10
- package/src/default/FacetGroupControl.tsx +217 -60
- package/src/default/FacetPanelPane.tsx +226 -25
- package/src/default/FilterChips.tsx +69 -2
- package/src/default/SearchPage.tsx +15 -1
- package/src/default/SearchResultsPane.tsx +20 -7
- package/src/default/SortSelect.tsx +3 -1
- package/src/default/index.ts +2 -0
- package/src/i18n/es.ts +5 -0
- package/src/i18n/keys.ts +25 -0
- package/src/i18n/ru.ts +7 -0
- package/src/state/facets.ts +19 -0
|
@@ -137,25 +137,29 @@ export interface components {
|
|
|
137
137
|
schemas: {
|
|
138
138
|
/** @description One destination in the dropdown, ready to render and ready to follow. */
|
|
139
139
|
CategorySuggestion: {
|
|
140
|
-
/** @description Category id. */
|
|
140
|
+
/** @description Category id. A `listings`-graded row derives it from the path's leaf segment. */
|
|
141
141
|
id: number;
|
|
142
|
-
/** @description Category slug. */
|
|
142
|
+
/** @description Category slug. Empty on a `listings`-graded row: no comm Function in the fleet resolves a path id to its slug or name yet. */
|
|
143
143
|
slug: string;
|
|
144
|
-
/** @description The category's own display name. */
|
|
144
|
+
/** @description The category's own display name. On a `listings`-graded row this is the leaf id as a truthful segment — render such rows by their `count` instead. */
|
|
145
145
|
name: string;
|
|
146
|
-
/** @description Display names root->leaf, e.g. ['Мужская одежда', 'Шорты']. This is what distinguishes three categories that share a name. */
|
|
146
|
+
/** @description Display names root->leaf, e.g. ['Мужская одежда', 'Шорты']. This is what distinguishes three categories that share a name. `listings`-graded rows carry the id segments here. */
|
|
147
147
|
path: string[];
|
|
148
148
|
/** @description The ancestry as ids joined with '/'. Pass it verbatim as the `category` parameter of /query — do not re-join path segments yourself. */
|
|
149
149
|
category: string;
|
|
150
|
-
/** @description Live listings a buyer would see under this category, descendants included — the same number the SERP reports for it. */
|
|
150
|
+
/** @description Live listings a buyer would see under this category, descendants included — the same number the SERP reports for it. On a `listings`-graded row: how many of them match the typed query, which is the count a `?q=…&category=…` tap will show. */
|
|
151
151
|
count: number;
|
|
152
152
|
/** @description Number of segments in `path`. */
|
|
153
153
|
depth: number;
|
|
154
154
|
/**
|
|
155
|
-
* @description How the name
|
|
155
|
+
* @description How the row earned its place: the four name grades from `categories.suggest`, or `listings` — a goods-driven row, offered because documents matching the query live there even though no category name says the word. Ranking: strong grades (exact/prefix/word) and `listings` sort as one class above `substring`; within a class, stocked before empty, then grade, then count desc, then depth, then name. `vector` rows — embedding-space neighbours offered when nothing first-class matched — always sit below every deterministic row, ordered by similarity.
|
|
156
156
|
*
|
|
157
|
+
* * `exact` - exact
|
|
157
158
|
* * `prefix` - prefix
|
|
159
|
+
* * `word` - word
|
|
158
160
|
* * `substring` - substring
|
|
161
|
+
* * `listings` - listings
|
|
162
|
+
* * `vector` - vector
|
|
159
163
|
*/
|
|
160
164
|
match: components["schemas"]["MatchEnum"];
|
|
161
165
|
};
|
|
@@ -193,11 +197,15 @@ export interface components {
|
|
|
193
197
|
stale_reason?: string;
|
|
194
198
|
};
|
|
195
199
|
/**
|
|
196
|
-
* @description * `
|
|
200
|
+
* @description * `exact` - exact
|
|
201
|
+
* * `prefix` - prefix
|
|
202
|
+
* * `word` - word
|
|
197
203
|
* * `substring` - substring
|
|
204
|
+
* * `listings` - listings
|
|
205
|
+
* * `vector` - vector
|
|
198
206
|
* @enum {string}
|
|
199
207
|
*/
|
|
200
|
-
MatchEnum: "prefix" | "substring";
|
|
208
|
+
MatchEnum: "exact" | "prefix" | "word" | "substring" | "listings" | "vector";
|
|
201
209
|
/** @description The P2B Art. 5 disclosure, generated from the scorer registry. */
|
|
202
210
|
RankingResponse: {
|
|
203
211
|
doc_type: string;
|
|
@@ -285,7 +293,7 @@ export interface components {
|
|
|
285
293
|
took_ms: number;
|
|
286
294
|
};
|
|
287
295
|
SuggestResponse: {
|
|
288
|
-
/** @description Destinations, ranked by live listing count desc, then depth, then name. */
|
|
296
|
+
/** @description Destinations, ranked by match class (strong name grades and goods-driven rows above substring), then stocked before empty, then grade, then live listing count desc, then depth, then name. */
|
|
289
297
|
categories: components["schemas"]["CategorySuggestion"][];
|
|
290
298
|
/** @description Title prefixes from the index. */
|
|
291
299
|
terms: string[];
|
|
@@ -293,7 +301,7 @@ export interface components {
|
|
|
293
301
|
items: string[];
|
|
294
302
|
/** @description Which dictionary answered — the same resolution /query reports. */
|
|
295
303
|
language: string;
|
|
296
|
-
/** @description What this answer could not do: `category_suggestions` (no provider for category names), `category_rollup` (no ancestry, so counts would read 0). */
|
|
304
|
+
/** @description What this answer could not do: `category_suggestions` (no provider for category names), `category_rollup` (no ancestry, so counts would read 0), `category_listing_suggestions` (no name matched and the configured engine does not implement the optional goods-driven verb, or it failed). */
|
|
297
305
|
degraded: string[];
|
|
298
306
|
backend: string;
|
|
299
307
|
};
|
|
@@ -31,6 +31,31 @@
|
|
|
31
31
|
* A group with NO schema (the host passed no `categoryFeatures`, or the slug
|
|
32
32
|
* is not in it) is a flat checkbox list — the honest default, and the shape
|
|
33
33
|
* every group had before.
|
|
34
|
+
*
|
|
35
|
+
* ── The group can be a disclosure, and closed it is NOT in the DOM ─────────
|
|
36
|
+
*
|
|
37
|
+
* Measured on a live classified deployment's cars leaf at 1440×900: the 280px
|
|
38
|
+
* rail carried 5717px of content — 40 groups, 118 checkboxes, 66 fields — as
|
|
39
|
+
* one flat column. No amount of per-group folding fixes forty headings' worth
|
|
40
|
+
* of open controls, so the group itself can close: `collapsible` turns the
|
|
41
|
+
* label into a real `<button aria-expanded>` with a chevron and, when values
|
|
42
|
+
* are chosen inside, the count of them — the one fact a closed group owes its
|
|
43
|
+
* header. Closed means the options are not rendered at all: a hundred
|
|
44
|
+
* `display:none` checkboxes are still a hundred stops for a screen reader.
|
|
45
|
+
* Both props default to today's behaviour (always open, no disclosure), so no
|
|
46
|
+
* existing host changes; WHICH groups open is the panel's decision, not this
|
|
47
|
+
* component's — see `FacetPanelPane`.
|
|
48
|
+
*
|
|
49
|
+
* ── Uncounted options are the fold's tail, not the group's face ────────────
|
|
50
|
+
*
|
|
51
|
+
* The same walk found "not counted" printed 100+ times down the default view.
|
|
52
|
+
* The sentence is honest and it stays — but an option with no evidence behind
|
|
53
|
+
* it must not stand in front of one that has some, so options with
|
|
54
|
+
* `count: null` and nothing chosen sort AFTER every counted one and live
|
|
55
|
+
* behind the existing "Show all (N)" fold. A group whose options are ALL
|
|
56
|
+
* uncounted (a schema-only group — the server never counted the slug) keeps
|
|
57
|
+
* them visible as before: folding everything would leave a heading over
|
|
58
|
+
* nothing. Chosen options are always visible, wherever their count went.
|
|
34
59
|
*/
|
|
35
60
|
import { useState } from "react";
|
|
36
61
|
import type { CSSProperties, ReactElement } from "react";
|
|
@@ -114,28 +139,50 @@ export function facetGroupShape(group: FacetGroup): FacetGroupShape {
|
|
|
114
139
|
return singleChoice(feature) ? "segmented" : "checkbox";
|
|
115
140
|
}
|
|
116
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Is this node in the group's uncounted tail — an option with no evidence
|
|
144
|
+
* behind it AND no choice on it? Only meaningful in a group that has counted
|
|
145
|
+
* evidence at all: see {@link facetOptionNodes}.
|
|
146
|
+
*/
|
|
147
|
+
function uncountedUnchosen(node: FacetOptionNode): boolean {
|
|
148
|
+
return node.option.count === null && !node.option.selected;
|
|
149
|
+
}
|
|
150
|
+
|
|
117
151
|
/** The group's options in render order, carrying the depth the schema gives. */
|
|
118
152
|
export function facetOptionNodes(group: FacetGroup): readonly FacetOptionNode[] {
|
|
153
|
+
let nodes: readonly FacetOptionNode[];
|
|
119
154
|
if (facetGroupShape(group) !== "nested" || group.feature === undefined) {
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
155
|
+
nodes = group.options.map((option) => ({ option, depth: 0 }));
|
|
156
|
+
} else {
|
|
157
|
+
const depths = optionDepths(featureConfig(group.feature)["options"]);
|
|
158
|
+
// Depth alone would leave children beside strangers: the options are
|
|
159
|
+
// re-ordered so each child follows its own parent, and a value the schema
|
|
160
|
+
// does not know keeps depth 0 rather than being hidden under someone.
|
|
161
|
+
const byValue = new Map(group.options.map((option) => [option.value, option]));
|
|
162
|
+
const out: FacetOptionNode[] = [];
|
|
163
|
+
const emitted = new Set<string>();
|
|
164
|
+
for (const [value, depth] of depths) {
|
|
165
|
+
const option = byValue.get(value);
|
|
166
|
+
if (option === undefined) continue;
|
|
167
|
+
out.push({ option, depth });
|
|
168
|
+
emitted.add(value);
|
|
169
|
+
}
|
|
170
|
+
for (const option of group.options) {
|
|
171
|
+
if (!emitted.has(option.value)) out.push({ option, depth: 0 });
|
|
172
|
+
}
|
|
173
|
+
nodes = out;
|
|
137
174
|
}
|
|
138
|
-
|
|
175
|
+
|
|
176
|
+
// An option nobody counted must not stand in front of one with evidence:
|
|
177
|
+
// the uncounted, unchosen tail sorts after everything else (stable — both
|
|
178
|
+
// halves keep their own order) and folds behind "Show all" in the
|
|
179
|
+
// component below. Only in a group that HAS counted evidence: a schema-only
|
|
180
|
+
// group is all `null`, and demoting all of it would reorder nothing while
|
|
181
|
+
// telling the fold to hide the entire group behind its own heading.
|
|
182
|
+
if (!group.options.some((option) => option.count !== null)) return nodes;
|
|
183
|
+
const tail = nodes.filter(uncountedUnchosen);
|
|
184
|
+
if (tail.length === 0) return nodes;
|
|
185
|
+
return [...nodes.filter((node) => !uncountedUnchosen(node)), ...tail];
|
|
139
186
|
}
|
|
140
187
|
|
|
141
188
|
/** A count, or the honest "we did not count this". Never a zero standing in
|
|
@@ -229,6 +276,57 @@ function OptionPill(props: {
|
|
|
229
276
|
);
|
|
230
277
|
}
|
|
231
278
|
|
|
279
|
+
/**
|
|
280
|
+
* The disclosure header, as a NATIVE button with the chrome stripped.
|
|
281
|
+
*
|
|
282
|
+
* Native rather than antd's `Button`, because this is a heading that happens
|
|
283
|
+
* to toggle, not an action: it must inherit the heading's type and colour and
|
|
284
|
+
* sit flush with the group's left edge, and undoing a themed button's
|
|
285
|
+
* padding, border, background and hover for every surface it lands on is more
|
|
286
|
+
* style than the button brings. `aria-expanded` on a real `<button>` is the
|
|
287
|
+
* whole disclosure pattern; the chevron only draws what it already says.
|
|
288
|
+
*/
|
|
289
|
+
const DISCLOSURE_HEADER: CSSProperties = {
|
|
290
|
+
display: "flex",
|
|
291
|
+
alignItems: "center",
|
|
292
|
+
gap: spacing[1],
|
|
293
|
+
width: "100%",
|
|
294
|
+
padding: 0,
|
|
295
|
+
border: "none",
|
|
296
|
+
background: "none",
|
|
297
|
+
color: "inherit",
|
|
298
|
+
font: "inherit",
|
|
299
|
+
textAlign: "start",
|
|
300
|
+
cursor: "pointer",
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/** The disclosure's state, drawn. Points down over a closed group ("there is
|
|
304
|
+
* more below") and flips once it is open. */
|
|
305
|
+
function ChevronGlyph(props: { readonly open: boolean }): ReactElement {
|
|
306
|
+
return (
|
|
307
|
+
<svg
|
|
308
|
+
width="16"
|
|
309
|
+
height="16"
|
|
310
|
+
viewBox="0 0 24 24"
|
|
311
|
+
fill="none"
|
|
312
|
+
stroke="currentColor"
|
|
313
|
+
strokeWidth="2"
|
|
314
|
+
strokeLinecap="round"
|
|
315
|
+
strokeLinejoin="round"
|
|
316
|
+
role="img"
|
|
317
|
+
aria-hidden="true"
|
|
318
|
+
focusable="false"
|
|
319
|
+
style={{
|
|
320
|
+
marginInlineStart: "auto",
|
|
321
|
+
flex: "0 0 auto",
|
|
322
|
+
...(props.open ? { transform: "rotate(180deg)" } : {}),
|
|
323
|
+
}}
|
|
324
|
+
>
|
|
325
|
+
<path d="m6 9 6 6 6-6" />
|
|
326
|
+
</svg>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
232
330
|
export interface FacetGroupControlProps {
|
|
233
331
|
readonly group: FacetGroup;
|
|
234
332
|
readonly onToggle: (slug: string, value: string) => void;
|
|
@@ -238,23 +336,48 @@ export interface FacetGroupControlProps {
|
|
|
238
336
|
/** How many options before "Show all". Default {@link FACET_VISIBLE_OPTIONS};
|
|
239
337
|
* `null` shows every option (a sheet devoted to one group has the room). */
|
|
240
338
|
readonly visibleOptions?: number | null;
|
|
339
|
+
/**
|
|
340
|
+
* Make the group a disclosure: the heading becomes a real button and the
|
|
341
|
+
* options can leave the DOM entirely. Default `false` — today's always-open
|
|
342
|
+
* group, so no existing host changes. Meaningless with `heading: false`:
|
|
343
|
+
* a disclosure with no header is a group nothing can reopen.
|
|
344
|
+
*/
|
|
345
|
+
readonly collapsible?: boolean;
|
|
346
|
+
/** Whether a `collapsible` group STARTS open. Default `true`. The initial
|
|
347
|
+
* value only — the person owns the state after the first click. */
|
|
348
|
+
readonly defaultOpen?: boolean;
|
|
241
349
|
}
|
|
242
350
|
|
|
243
351
|
export function FacetGroupControl(props: FacetGroupControlProps): ReactElement {
|
|
244
352
|
const t = useT();
|
|
245
353
|
const { group } = props;
|
|
246
354
|
const [expanded, setExpanded] = useState(false);
|
|
355
|
+
const [openState, setOpenState] = useState(props.defaultOpen !== false);
|
|
247
356
|
const shape = facetGroupShape(group);
|
|
248
357
|
const nodes = facetOptionNodes(group);
|
|
249
358
|
|
|
359
|
+
const disclosure = props.collapsible === true && props.heading !== false;
|
|
360
|
+
const open = !disclosure || openState;
|
|
361
|
+
|
|
250
362
|
const limit =
|
|
251
363
|
props.visibleOptions === null
|
|
252
364
|
? null
|
|
253
365
|
: (props.visibleOptions ?? FACET_VISIBLE_OPTIONS);
|
|
366
|
+
// The demoted tail `facetOptionNodes` sorted to the end: uncounted,
|
|
367
|
+
// unchosen options in a group that has counted evidence. They are ALWAYS
|
|
368
|
+
// behind the fold — the "one row over the limit" exemption below is about
|
|
369
|
+
// not hiding one counted row, and these rows say "not counted".
|
|
370
|
+
const demoted = group.options.some((option) => option.count !== null)
|
|
371
|
+
? nodes.filter(uncountedUnchosen).length
|
|
372
|
+
: 0;
|
|
254
373
|
// A group one row over the limit is not truncated: hiding a single option
|
|
255
374
|
// behind "Show all (9)" costs a tap to reveal exactly one thing.
|
|
256
|
-
const
|
|
257
|
-
|
|
375
|
+
const folded =
|
|
376
|
+
limit !== null && (nodes.length > limit + 1 || demoted > 0);
|
|
377
|
+
const shown =
|
|
378
|
+
folded && !expanded
|
|
379
|
+
? nodes.slice(0, Math.min(limit ?? nodes.length, nodes.length - demoted))
|
|
380
|
+
: nodes;
|
|
258
381
|
|
|
259
382
|
return (
|
|
260
383
|
<Flex
|
|
@@ -264,48 +387,82 @@ export function FacetGroupControl(props: FacetGroupControlProps): ReactElement {
|
|
|
264
387
|
data-counted={group.counted ? "true" : "false"}
|
|
265
388
|
data-shape={shape}
|
|
266
389
|
>
|
|
267
|
-
{props.heading !== false &&
|
|
268
|
-
|
|
269
|
-
|
|
390
|
+
{props.heading !== false &&
|
|
391
|
+
(disclosure ? (
|
|
392
|
+
<button
|
|
393
|
+
type="button"
|
|
394
|
+
style={DISCLOSURE_HEADER}
|
|
395
|
+
aria-expanded={open}
|
|
396
|
+
data-testid={`facet-toggle-${group.slug}`}
|
|
397
|
+
data-analytics="none"
|
|
398
|
+
data-analytics-reason="opening a filter group is a read, not a flow step"
|
|
399
|
+
onClick={() => {
|
|
400
|
+
setOpenState((was) => !was);
|
|
401
|
+
}}
|
|
402
|
+
>
|
|
403
|
+
<Typography.Text strong>{group.label}</Typography.Text>
|
|
404
|
+
{/* The one fact a closed group owes its header: how many of its
|
|
405
|
+
values are CHOSEN. Not the option count — that is what the
|
|
406
|
+
fold's own "Show all (N)" answers once the group is open. */}
|
|
407
|
+
{group.selected.length > 0 && (
|
|
408
|
+
<Typography.Text
|
|
409
|
+
type="secondary"
|
|
410
|
+
data-testid={`facet-toggle-count-${group.slug}`}
|
|
411
|
+
>
|
|
412
|
+
{group.selected.length}
|
|
413
|
+
</Typography.Text>
|
|
414
|
+
)}
|
|
415
|
+
<ChevronGlyph open={open} />
|
|
416
|
+
</button>
|
|
417
|
+
) : (
|
|
418
|
+
<Typography.Text strong>{group.label}</Typography.Text>
|
|
419
|
+
))}
|
|
270
420
|
|
|
271
|
-
{
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
421
|
+
{/* Closed means NOT RENDERED, not hidden: a hundred `display:none`
|
|
422
|
+
checkboxes are still a hundred stops for a screen reader, and the
|
|
423
|
+
measured rail held 118 of them. */}
|
|
424
|
+
{open && (
|
|
425
|
+
<>
|
|
426
|
+
{shape === "segmented" ? (
|
|
427
|
+
<Flex wrap gap={spacing[2]}>
|
|
428
|
+
{shown.map((node) => (
|
|
429
|
+
<OptionPill
|
|
430
|
+
key={node.option.value}
|
|
431
|
+
group={group}
|
|
432
|
+
option={node.option}
|
|
433
|
+
onToggle={props.onToggle}
|
|
434
|
+
/>
|
|
435
|
+
))}
|
|
436
|
+
</Flex>
|
|
437
|
+
) : (
|
|
438
|
+
shown.map((node) => (
|
|
439
|
+
<CheckboxRow
|
|
440
|
+
key={node.option.value}
|
|
441
|
+
group={group}
|
|
442
|
+
node={node}
|
|
443
|
+
onToggle={props.onToggle}
|
|
444
|
+
/>
|
|
445
|
+
))
|
|
446
|
+
)}
|
|
292
447
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
448
|
+
{folded && (
|
|
449
|
+
<Button
|
|
450
|
+
type="link"
|
|
451
|
+
size="small"
|
|
452
|
+
style={{ alignSelf: "flex-start", paddingInline: 0 }}
|
|
453
|
+
data-testid={`facet-more-${group.slug}`}
|
|
454
|
+
data-analytics="none"
|
|
455
|
+
data-analytics-reason="expanding a filter group is a read, not a flow step"
|
|
456
|
+
onClick={() => {
|
|
457
|
+
setExpanded((was) => !was);
|
|
458
|
+
}}
|
|
459
|
+
>
|
|
460
|
+
{expanded
|
|
461
|
+
? t(SEARCH_I18N_KEYS.facetsShowLess)
|
|
462
|
+
: t(SEARCH_I18N_KEYS.facetsShowAll, { count: nodes.length })}
|
|
463
|
+
</Button>
|
|
464
|
+
)}
|
|
465
|
+
</>
|
|
309
466
|
)}
|
|
310
467
|
</Flex>
|
|
311
468
|
);
|