@stapel/search-react 0.23.0 → 0.24.0
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 +65 -0
- package/README.md +39 -1
- package/dist/default/FilterChips.d.ts +86 -2
- package/dist/default/FilterChips.d.ts.map +1 -1
- package/dist/default/FilterChips.js +208 -2
- package/dist/default/FilterChips.js.map +1 -1
- package/dist/default/SearchPage.d.ts +16 -1
- package/dist/default/SearchPage.d.ts.map +1 -1
- package/dist/default/SearchPage.js +4 -3
- package/dist/default/SearchPage.js.map +1 -1
- package/dist/default/index.d.ts +2 -2
- 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 +6 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/keys.d.ts +17 -0
- package/dist/i18n/keys.d.ts.map +1 -1
- package/dist/i18n/keys.js +24 -0
- package/dist/i18n/keys.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +6 -0
- package/dist/i18n/ru.js.map +1 -1
- package/llms.txt +1 -1
- package/manifest.json +7 -1
- package/nav-manifest.json +1 -1
- package/package.json +5 -5
- package/src/analytics/generated/events.json +1 -1
- package/src/default/FilterChips.tsx +308 -4
- package/src/default/SearchPage.tsx +33 -1
- package/src/default/index.ts +15 -1
- package/src/i18n/es.ts +6 -0
- package/src/i18n/keys.ts +25 -0
- package/src/i18n/ru.ts +6 -0
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `<FilterChips>` — the
|
|
3
|
-
*
|
|
2
|
+
* `<FilterChips>` — the filter row, in its two modes.
|
|
3
|
+
*
|
|
4
|
+
* ## `mode="openers"` (the default): the phone's row
|
|
5
|
+
*
|
|
6
|
+
* One horizontally scrolling line of chips, each of which opens its OWN
|
|
7
|
+
* picker. Everything below this heading describes that mode.
|
|
8
|
+
*
|
|
9
|
+
* ## `mode="applied"`: the row a rail on screen needs
|
|
10
|
+
*
|
|
11
|
+
* Where the panel is already drawn — the desktop column — a row of openers is
|
|
12
|
+
* the wrong shape twice over: it prints the whole rail a second time, and no
|
|
13
|
+
* chip in it REMOVES anything without opening a modal over the results. A
|
|
14
|
+
* storefront hit exactly that (a 2377px filter column; picking two values left
|
|
15
|
+
* nothing between the header and the first card, and dropping one of them
|
|
16
|
+
* meant scrolling the rail until its button came back) and wrote its own row.
|
|
17
|
+
* This mode is that row, in the pair:
|
|
18
|
+
*
|
|
19
|
+
* - one chip per applied facet VALUE and per applied numeric range, never per
|
|
20
|
+
* axis — "Brand" with three values chosen is three chips, three removals;
|
|
21
|
+
* - each chip names the axis AND the value ("Brand: Bosch", "Price: from 100
|
|
22
|
+
* to 500"), because beside a dozen axes a bare value names nothing;
|
|
23
|
+
* - each chip is a real `<button>` whose press removes THAT constraint, and
|
|
24
|
+
* whose accessible name says so — not an antd `Tag closable`, whose close
|
|
25
|
+
* icon is a `<span>` with no tab stop, i.e. a constraint a keyboard can
|
|
26
|
+
* read and cannot drop;
|
|
27
|
+
* - the same clear-all the rail's footer runs, beside the chips instead of a
|
|
28
|
+
* column-height away;
|
|
29
|
+
* - nothing applied, nothing drawn. An empty band above the results is
|
|
30
|
+
* furniture, and this row's whole claim is that it is there only when it
|
|
31
|
+
* has something to say.
|
|
32
|
+
*
|
|
33
|
+
* Both modes read the SAME bag (`useFacetPanel`, `buildRangeGroups` over the
|
|
34
|
+
* page's own state) the rail reads, so no two surfaces can disagree about what
|
|
35
|
+
* is applied or about what a value is called — including the label path:
|
|
36
|
+
* the answer's `facet_labels` first, the category schema second, the raw slug
|
|
37
|
+
* marked as such third. Every applied chip stamps `data-label-source` and
|
|
38
|
+
* `data-value-label-source` so a storefront's test fails on a raw index term
|
|
39
|
+
* rather than eyeballing one.
|
|
4
40
|
*
|
|
5
41
|
* ## What it replaces, and why the replacement is not cosmetic
|
|
6
42
|
*
|
|
@@ -137,7 +173,7 @@ import {
|
|
|
137
173
|
compareFacetsByEvidence,
|
|
138
174
|
facetGroupIsDrawable,
|
|
139
175
|
} from "../state/facets.js";
|
|
140
|
-
import type { FacetGroup } from "../state/facets.js";
|
|
176
|
+
import type { FacetGroup, FacetLabelSource } from "../state/facets.js";
|
|
141
177
|
import { SEARCH_I18N_KEYS } from "../i18n/keys.js";
|
|
142
178
|
import { FacetGroupControl } from "./FacetGroupControl.js";
|
|
143
179
|
import { RangeFilterRow } from "./RangeFilterRow.js";
|
|
@@ -290,7 +326,11 @@ export function capChipRow(
|
|
|
290
326
|
return { visible: specs.slice(0, cut), overflow: specs.length - cut };
|
|
291
327
|
}
|
|
292
328
|
|
|
293
|
-
|
|
329
|
+
/** What the row is FOR — see this module's opening note. */
|
|
330
|
+
export type FilterChipsMode = "openers" | "applied";
|
|
331
|
+
|
|
332
|
+
/** What both modes read the search through. */
|
|
333
|
+
export interface FilterChipsCommonProps {
|
|
294
334
|
/** The category's feature schema — the source of option labels, of which
|
|
295
335
|
* slugs get a range chip, of which slugs are choosable at all, and of how
|
|
296
336
|
* each group is drawn. */
|
|
@@ -302,6 +342,11 @@ export interface FilterChipsProps {
|
|
|
302
342
|
* value cannot read one way on a chip and another way inside the sheet.
|
|
303
343
|
*/
|
|
304
344
|
readonly resolveFacetLabels?: FacetLabelResolver;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export interface FilterChipsOpenerProps extends FilterChipsCommonProps {
|
|
348
|
+
/** The default. Every chip OPENS a picker; see the module note. */
|
|
349
|
+
readonly mode?: "openers";
|
|
305
350
|
/**
|
|
306
351
|
* The catalogue picker (`categories-react`), same slot the panel takes.
|
|
307
352
|
*
|
|
@@ -335,6 +380,20 @@ export interface FilterChipsProps {
|
|
|
335
380
|
readonly maxRowChips?: number | null;
|
|
336
381
|
}
|
|
337
382
|
|
|
383
|
+
/**
|
|
384
|
+
* The applied row: one chip per constraint, each of which removes it.
|
|
385
|
+
*
|
|
386
|
+
* None of the opener props are here and that is the point — a row that draws
|
|
387
|
+
* only what is applied has no picker to open, no capped tail to stand in front
|
|
388
|
+
* of and no category chip (narrowing the category is a navigation, not a
|
|
389
|
+
* filter this row can drop).
|
|
390
|
+
*/
|
|
391
|
+
export interface FilterChipsAppliedProps extends FilterChipsCommonProps {
|
|
392
|
+
readonly mode: "applied";
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export type FilterChipsProps = FilterChipsOpenerProps | FilterChipsAppliedProps;
|
|
396
|
+
|
|
338
397
|
/**
|
|
339
398
|
* The label a chip carries: the group's name alone when nothing is chosen,
|
|
340
399
|
* and the CHOICE when something is — "Brand" becomes "Bosch", "Brand, +2".
|
|
@@ -373,7 +432,22 @@ export function categoryLeaf(path: string): string | undefined {
|
|
|
373
432
|
return /^\d+$/.test(leaf) ? undefined : leaf;
|
|
374
433
|
}
|
|
375
434
|
|
|
435
|
+
/**
|
|
436
|
+
* The row, in whichever mode was asked for.
|
|
437
|
+
*
|
|
438
|
+
* Two components rather than one with a branch in it: the modes share the
|
|
439
|
+
* search they read and share nothing they DRAW, and a single body would have
|
|
440
|
+
* to run both sets of hooks on every render of either.
|
|
441
|
+
*/
|
|
376
442
|
export function FilterChips(props: FilterChipsProps): ReactElement | null {
|
|
443
|
+
return props.mode === "applied" ? (
|
|
444
|
+
<AppliedChipRow {...props} />
|
|
445
|
+
) : (
|
|
446
|
+
<OpenerChipRow {...props} />
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function OpenerChipRow(props: FilterChipsOpenerProps): ReactElement | null {
|
|
377
451
|
const t = useT();
|
|
378
452
|
const { state, setCategory } = useSearchState();
|
|
379
453
|
const bag = useFacetPanel({
|
|
@@ -644,6 +718,236 @@ export function FilterChips(props: FilterChipsProps): ReactElement | null {
|
|
|
644
718
|
);
|
|
645
719
|
}
|
|
646
720
|
|
|
721
|
+
/* ── the applied row ────────────────────────────────────────────────────── */
|
|
722
|
+
|
|
723
|
+
type Translate = (key: string, params?: Record<string, unknown>) => string;
|
|
724
|
+
|
|
725
|
+
/** Which constraint one applied chip drops. */
|
|
726
|
+
export type AppliedChipTarget =
|
|
727
|
+
| { readonly kind: "facet"; readonly slug: string; readonly value: string }
|
|
728
|
+
| { readonly kind: "range"; readonly slug: string };
|
|
729
|
+
|
|
730
|
+
/** One applied constraint, resolved to the words it prints and what it drops. */
|
|
731
|
+
export interface AppliedChip {
|
|
732
|
+
readonly key: string;
|
|
733
|
+
readonly target: AppliedChipTarget;
|
|
734
|
+
/** What the AXIS is called, and which source named it. */
|
|
735
|
+
readonly name: string;
|
|
736
|
+
readonly nameSource: FacetLabelSource;
|
|
737
|
+
/** What the VALUE is called, and which source named it. */
|
|
738
|
+
readonly value: string;
|
|
739
|
+
readonly valueSource: FacetLabelSource;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** The chip's own test hook — the constraint it drops, spelled out. */
|
|
743
|
+
export function appliedChipTestId(target: AppliedChipTarget): string {
|
|
744
|
+
return target.kind === "facet"
|
|
745
|
+
? `search-applied-chip-${target.slug}-${target.value}`
|
|
746
|
+
: `search-applied-chip-range-${target.slug}`;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* A numeric constraint as one phrase.
|
|
751
|
+
*
|
|
752
|
+
* The bounds print exactly as the URL carries them — the wire never promised a
|
|
753
|
+
* number and reformatting one would rewrite the link — except on a core MONEY
|
|
754
|
+
* axis, where the currency the answer's own cards carry turns "1000" into
|
|
755
|
+
* money, the same way the cards do.
|
|
756
|
+
*/
|
|
757
|
+
export function rangeChipText(range: RangeGroup, t: Translate): string {
|
|
758
|
+
const one = (bound: string): string => {
|
|
759
|
+
const n = Number(bound);
|
|
760
|
+
if (range.currency !== undefined && Number.isFinite(n)) {
|
|
761
|
+
return new Intl.NumberFormat(undefined, {
|
|
762
|
+
style: "currency",
|
|
763
|
+
currency: range.currency,
|
|
764
|
+
maximumFractionDigits: 0,
|
|
765
|
+
}).format(n);
|
|
766
|
+
}
|
|
767
|
+
return range.unit === undefined ? bound : `${bound} ${range.unit}`;
|
|
768
|
+
};
|
|
769
|
+
if (range.from !== undefined && range.to !== undefined) {
|
|
770
|
+
return t(SEARCH_I18N_KEYS.filtersChipRangeBetween, {
|
|
771
|
+
from: one(range.from),
|
|
772
|
+
to: one(range.to),
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
if (range.from !== undefined) {
|
|
776
|
+
return t(SEARCH_I18N_KEYS.filtersChipRangeFrom, { from: one(range.from) });
|
|
777
|
+
}
|
|
778
|
+
return t(SEARCH_I18N_KEYS.filtersChipRangeTo, { to: one(range.to ?? "") });
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Where a range axis got its NAME — the same three-source question the facet
|
|
783
|
+
* groups answer for themselves, asked of the other half of the model.
|
|
784
|
+
*
|
|
785
|
+
* A core axis is named by the answer declaring it (`facet_meta.core_ranges`)
|
|
786
|
+
* and this package's own catalogue; a category attribute is named by the
|
|
787
|
+
* schema; a slug the URL constrains that neither explains prints as itself,
|
|
788
|
+
* and that is `"none"` — the same honest bottom the rail marks.
|
|
789
|
+
*/
|
|
790
|
+
export function rangeLabelSource(range: RangeGroup): FacetLabelSource {
|
|
791
|
+
if (range.core) return "server";
|
|
792
|
+
return range.feature === undefined ? "none" : "schema";
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Every applied constraint of the current search, in the order the rail states
|
|
797
|
+
* them: the facet values first, in the order the groups came in, then the
|
|
798
|
+
* numeric ranges.
|
|
799
|
+
*
|
|
800
|
+
* Pure, and exported, because "one chip per VALUE" is the whole shape of the
|
|
801
|
+
* row — a build that collapsed three chosen brands into one chip would remove
|
|
802
|
+
* three filters with one press and no test that only counts chips would see
|
|
803
|
+
* it.
|
|
804
|
+
*/
|
|
805
|
+
export function buildAppliedChips(input: {
|
|
806
|
+
readonly groups: readonly FacetGroup[];
|
|
807
|
+
readonly ranges: readonly RangeGroup[];
|
|
808
|
+
readonly t: Translate;
|
|
809
|
+
}): readonly AppliedChip[] {
|
|
810
|
+
const chips: AppliedChip[] = [];
|
|
811
|
+
for (const group of input.groups) {
|
|
812
|
+
for (const option of group.options) {
|
|
813
|
+
if (!option.selected) continue;
|
|
814
|
+
chips.push({
|
|
815
|
+
key: `f.${group.slug}=${option.value}`,
|
|
816
|
+
target: { kind: "facet", slug: group.slug, value: option.value },
|
|
817
|
+
name: group.label,
|
|
818
|
+
nameSource: group.labelSource,
|
|
819
|
+
value: option.label,
|
|
820
|
+
valueSource: option.labelSource,
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
for (const range of input.ranges) {
|
|
825
|
+
if (!range.active) continue;
|
|
826
|
+
const source = rangeLabelSource(range);
|
|
827
|
+
chips.push({
|
|
828
|
+
key: `r.${range.slug}`,
|
|
829
|
+
target: { kind: "range", slug: range.slug },
|
|
830
|
+
name: range.label,
|
|
831
|
+
nameSource: source,
|
|
832
|
+
value: rangeChipText(range, input.t),
|
|
833
|
+
// The bounds are the person's own numbers — nobody had to name them.
|
|
834
|
+
valueSource: source,
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
return chips;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/** The applied row wraps: it stands beside a rail, not on a 390px scroller,
|
|
841
|
+
* and a constraint pushed off the line is a constraint with no control. */
|
|
842
|
+
const APPLIED_ROW: CSSProperties = {
|
|
843
|
+
display: "flex",
|
|
844
|
+
alignItems: "center",
|
|
845
|
+
gap: spacing[2],
|
|
846
|
+
flexWrap: "wrap",
|
|
847
|
+
// Room for the focus ring of the first chip, which a flush edge clips.
|
|
848
|
+
paddingBlock: spacing[1],
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
function AppliedChipRow(props: FilterChipsAppliedProps): ReactElement | null {
|
|
852
|
+
const t = useT();
|
|
853
|
+
const { state } = useSearchState();
|
|
854
|
+
const bag = useFacetPanel({
|
|
855
|
+
...(props.categoryFeatures !== undefined
|
|
856
|
+
? { categoryFeatures: props.categoryFeatures }
|
|
857
|
+
: {}),
|
|
858
|
+
...(props.locale !== undefined ? { locale: props.locale } : {}),
|
|
859
|
+
...(props.resolveFacetLabels !== undefined
|
|
860
|
+
? { resolveFacetLabels: props.resolveFacetLabels }
|
|
861
|
+
: {}),
|
|
862
|
+
});
|
|
863
|
+
const ranges = buildRangeGroups({
|
|
864
|
+
state,
|
|
865
|
+
...(props.categoryFeatures !== undefined
|
|
866
|
+
? { categoryFeatures: props.categoryFeatures }
|
|
867
|
+
: {}),
|
|
868
|
+
coreRanges: bag.coreRanges,
|
|
869
|
+
...(bag.currency !== undefined ? { currency: bag.currency } : {}),
|
|
870
|
+
t,
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
/* NOTHING IS DRAWN UNTIL THE ANSWER LANDS.
|
|
874
|
+
*
|
|
875
|
+
* Both halves of a chip are named by the envelope: the heading path
|
|
876
|
+
* (`facet_labels[<slug>]`), and — for a range — the answer's own declaration
|
|
877
|
+
* of which axes are CORE columns (`facet_meta.core_ranges`). Before it, an
|
|
878
|
+
* `r.price` the URL carries is a slug this row has no name for, so it would
|
|
879
|
+
* draw a chip captioned with that slug and rename it a moment later. A
|
|
880
|
+
* caption that changes under the reader is worse than a caption that
|
|
881
|
+
* arrives; the panel is the control in the meantime.
|
|
882
|
+
*/
|
|
883
|
+
const answered = bag.state.status === "ready";
|
|
884
|
+
const chips = buildAppliedChips({
|
|
885
|
+
groups: answered ? bag.state.data : [],
|
|
886
|
+
ranges: answered ? ranges : [],
|
|
887
|
+
t,
|
|
888
|
+
});
|
|
889
|
+
if (chips.length === 0) return null;
|
|
890
|
+
|
|
891
|
+
const remove = (target: AppliedChipTarget): void => {
|
|
892
|
+
if (target.kind === "facet") bag.toggle(target.slug, target.value);
|
|
893
|
+
else bag.setRange(target.slug, null);
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
return (
|
|
897
|
+
<div
|
|
898
|
+
style={APPLIED_ROW}
|
|
899
|
+
role="group"
|
|
900
|
+
aria-label={t(SEARCH_I18N_KEYS.filtersAppliedLabel)}
|
|
901
|
+
data-testid="search-applied-chips"
|
|
902
|
+
>
|
|
903
|
+
{chips.map((chip) => {
|
|
904
|
+
const text = t(SEARCH_I18N_KEYS.filtersChipPair, {
|
|
905
|
+
name: chip.name,
|
|
906
|
+
value: chip.value,
|
|
907
|
+
});
|
|
908
|
+
return (
|
|
909
|
+
<Button
|
|
910
|
+
key={chip.key}
|
|
911
|
+
shape="round"
|
|
912
|
+
size="small"
|
|
913
|
+
style={CHIP}
|
|
914
|
+
data-testid={appliedChipTestId(chip.target)}
|
|
915
|
+
/* The two halves of the caption, marked with what named each of
|
|
916
|
+
them. `[data-label-source="none"]` is the assertion that a raw
|
|
917
|
+
index term ever reached this row. */
|
|
918
|
+
data-label-source={chip.nameSource}
|
|
919
|
+
data-value-label-source={chip.valueSource}
|
|
920
|
+
data-analytics="none"
|
|
921
|
+
data-analytics-reason="dropping a filter is a read, not a flow step"
|
|
922
|
+
aria-label={t(SEARCH_I18N_KEYS.filtersChipRemove, { filter: text })}
|
|
923
|
+
onClick={() => {
|
|
924
|
+
remove(chip.target);
|
|
925
|
+
}}
|
|
926
|
+
>
|
|
927
|
+
<span>{text}</span>
|
|
928
|
+
<span aria-hidden="true">{"×"}</span>
|
|
929
|
+
</Button>
|
|
930
|
+
);
|
|
931
|
+
})}
|
|
932
|
+
{/* The SAME action the rail's own footer runs, beside the chips instead
|
|
933
|
+
of a column-height down the page. */}
|
|
934
|
+
<Button
|
|
935
|
+
type="link"
|
|
936
|
+
size="small"
|
|
937
|
+
style={{ paddingInline: 0 }}
|
|
938
|
+
data-testid="search-applied-chips-clear"
|
|
939
|
+
data-analytics="none"
|
|
940
|
+
data-analytics-reason="dropping the filters is a read, not a flow step"
|
|
941
|
+
onClick={() => {
|
|
942
|
+
bag.clearAll();
|
|
943
|
+
}}
|
|
944
|
+
>
|
|
945
|
+
{t(SEARCH_I18N_KEYS.facetsClearAll, { count: bag.activeFilters })}
|
|
946
|
+
</Button>
|
|
947
|
+
</div>
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
|
|
647
951
|
/** The mark on the "all filters" chip: something is applied. Not a count —
|
|
648
952
|
* the counts are on the chips beside it, and a number inside a 32px circle is
|
|
649
953
|
* a number nobody reads. */
|
|
@@ -240,7 +240,7 @@ export interface SearchPageProps extends ThemeModeProp, ParseSearchStateOptions
|
|
|
240
240
|
* What the current category is CALLED — the chip's own text. The pair holds
|
|
241
241
|
* a path of slugs and no way to turn one into a catalogue name; absent, the
|
|
242
242
|
* chip states the path's last segment. See
|
|
243
|
-
* {@link
|
|
243
|
+
* {@link FilterChipsOpenerProps.categoryLabel}.
|
|
244
244
|
*/
|
|
245
245
|
readonly categoryLabel?: ReactNode;
|
|
246
246
|
/** The location control slot (`geo-react`). */
|
|
@@ -305,6 +305,21 @@ export interface SearchPageProps extends ThemeModeProp, ParseSearchStateOptions
|
|
|
305
305
|
* describes the whole page, not the results column of it.
|
|
306
306
|
*/
|
|
307
307
|
readonly resultsHeader?: ReactNode;
|
|
308
|
+
/**
|
|
309
|
+
* Draw the APPLIED filter row in the results header — one chip per applied
|
|
310
|
+
* value and per applied range, each of which removes it
|
|
311
|
+
* (`<FilterChips mode="applied">`).
|
|
312
|
+
*
|
|
313
|
+
* `"desktop"` is the shape this exists for: where the rail is on screen a
|
|
314
|
+
* choice otherwise leaves no trace above the results and dropping one of
|
|
315
|
+
* two constraints means hunting its button back down the column, while on
|
|
316
|
+
* the phone the opener row below already states every applied filter on its
|
|
317
|
+
* own chips. `true` draws it in both layouts; omitted, nothing changes.
|
|
318
|
+
*
|
|
319
|
+
* It renders itself away when nothing is applied, so a host never has to
|
|
320
|
+
* ask.
|
|
321
|
+
*/
|
|
322
|
+
readonly appliedChips?: boolean | "desktop";
|
|
308
323
|
/** What this surface calls its result list. See
|
|
309
324
|
* {@link SearchResultsPaneProps.heading}. */
|
|
310
325
|
readonly resultsHeading?: ReactNode;
|
|
@@ -401,6 +416,7 @@ interface SearchPageBodyProps {
|
|
|
401
416
|
readonly footer?: ReactNode;
|
|
402
417
|
readonly filtersHeader?: ReactNode;
|
|
403
418
|
readonly resultsHeader?: ReactNode;
|
|
419
|
+
readonly appliedChips?: boolean | "desktop";
|
|
404
420
|
readonly resultsHeading?: ReactNode;
|
|
405
421
|
readonly degradationNotice?: DegradationNoticeVariant;
|
|
406
422
|
readonly filtersLayout?: SearchFiltersLayout;
|
|
@@ -637,6 +653,20 @@ function SearchPageBody(props: SearchPageBodyProps): ReactElement {
|
|
|
637
653
|
<div data-testid="search-results-header">{props.resultsHeader}</div>
|
|
638
654
|
)}
|
|
639
655
|
|
|
656
|
+
{/* What the search is NARROWED to, above the results, each constraint
|
|
657
|
+
beside the control that drops it. Drawn in the same band as the
|
|
658
|
+
results header because that is where a host would otherwise hand-mount
|
|
659
|
+
it — and it draws nothing at all when nothing is applied. */}
|
|
660
|
+
{(props.appliedChips === true ||
|
|
661
|
+
(props.appliedChips === "desktop" && layout !== "sheet")) && (
|
|
662
|
+
<FilterChips
|
|
663
|
+
mode="applied"
|
|
664
|
+
{...(categoryFeatures !== undefined ? { categoryFeatures } : {})}
|
|
665
|
+
{...(locale !== undefined ? { locale } : {})}
|
|
666
|
+
{...(resolveFacetLabels !== undefined ? { resolveFacetLabels } : {})}
|
|
667
|
+
/>
|
|
668
|
+
)}
|
|
669
|
+
|
|
640
670
|
{showFilters && layout === "sheet" ? (
|
|
641
671
|
<>
|
|
642
672
|
{/* The phone's filter row. It REPLACES the full-width "Filters (3)"
|
|
@@ -732,6 +762,7 @@ export function SearchPage(props: SearchPageProps): ReactElement {
|
|
|
732
762
|
footer,
|
|
733
763
|
filtersHeader,
|
|
734
764
|
resultsHeader,
|
|
765
|
+
appliedChips,
|
|
735
766
|
resultsHeading,
|
|
736
767
|
degradationNotice,
|
|
737
768
|
filtersLayout,
|
|
@@ -766,6 +797,7 @@ export function SearchPage(props: SearchPageProps): ReactElement {
|
|
|
766
797
|
{...(footer !== undefined ? { footer } : {})}
|
|
767
798
|
{...(filtersHeader !== undefined ? { filtersHeader } : {})}
|
|
768
799
|
{...(resultsHeader !== undefined ? { resultsHeader } : {})}
|
|
800
|
+
{...(appliedChips !== undefined ? { appliedChips } : {})}
|
|
769
801
|
{...(resultsHeading !== undefined ? { resultsHeading } : {})}
|
|
770
802
|
{...(degradationNotice !== undefined ? { degradationNotice } : {})}
|
|
771
803
|
{...(filtersLayout !== undefined ? { filtersLayout } : {})}
|
package/src/default/index.ts
CHANGED
|
@@ -61,12 +61,26 @@ export {
|
|
|
61
61
|
CHIP_ROW_CAP,
|
|
62
62
|
CHIP_ROW_CLASS,
|
|
63
63
|
CHIP_ROW_STYLE_HREF,
|
|
64
|
+
appliedChipTestId,
|
|
65
|
+
buildAppliedChips,
|
|
64
66
|
capChipRow,
|
|
65
67
|
categoryLeaf,
|
|
66
68
|
chipRowCss,
|
|
67
69
|
orderChipFilters,
|
|
70
|
+
rangeChipText,
|
|
71
|
+
rangeLabelSource,
|
|
72
|
+
} from "./FilterChips.js";
|
|
73
|
+
export type {
|
|
74
|
+
AppliedChip,
|
|
75
|
+
AppliedChipTarget,
|
|
76
|
+
ChipBand,
|
|
77
|
+
ChipSpec,
|
|
78
|
+
FilterChipsAppliedProps,
|
|
79
|
+
FilterChipsCommonProps,
|
|
80
|
+
FilterChipsMode,
|
|
81
|
+
FilterChipsOpenerProps,
|
|
82
|
+
FilterChipsProps,
|
|
68
83
|
} from "./FilterChips.js";
|
|
69
|
-
export type { ChipBand, ChipSpec, FilterChipsProps } from "./FilterChips.js";
|
|
70
84
|
export { EmptyExits, RADIUS_WIDEN_FACTOR, parentCategory } from "./EmptyExits.js";
|
|
71
85
|
export type { EmptyExitsProps } from "./EmptyExits.js";
|
|
72
86
|
|
package/src/i18n/es.ts
CHANGED
|
@@ -119,6 +119,12 @@ export const searchI18nBundleEs: I18nDictionary = {
|
|
|
119
119
|
"search.filters.short": "Filtros",
|
|
120
120
|
"search.filters.chip_more": ", +{count}",
|
|
121
121
|
"search.filters.chips_overflow": "Más · {count}",
|
|
122
|
+
"search.filters.applied_label": "Filtros aplicados",
|
|
123
|
+
"search.filters.chip_pair": "{name}: {value}",
|
|
124
|
+
"search.filters.chip_remove": "Quitar el filtro {filter}",
|
|
125
|
+
"search.filters.chip_range_between": "de {from} a {to}",
|
|
126
|
+
"search.filters.chip_range_from": "desde {from}",
|
|
127
|
+
"search.filters.chip_range_to": "hasta {to}",
|
|
122
128
|
|
|
123
129
|
"search.empty.exits_title": "Prueba a ampliar la búsqueda",
|
|
124
130
|
"search.empty.up_a_level": "Subir un nivel",
|
package/src/i18n/keys.ts
CHANGED
|
@@ -232,6 +232,25 @@ export const SEARCH_I18N_KEYS = {
|
|
|
232
232
|
* `{count}` is how many chips it hides (D16). */
|
|
233
233
|
filtersChipOverflow: "search.filters.chips_overflow",
|
|
234
234
|
|
|
235
|
+
// ── the APPLIED row (`<FilterChips mode="applied">`) ─────────────────────
|
|
236
|
+
/** The accessible name of the applied row. Distinct from
|
|
237
|
+
* `filtersChipsLabel`: that row OPENS pickers, this one DROPS filters, and
|
|
238
|
+
* a screen reader announcing both as "Filters" states neither. */
|
|
239
|
+
filtersAppliedLabel: "search.filters.applied_label",
|
|
240
|
+
/** One applied chip's caption: the axis AND the value — "Brand: Bosch".
|
|
241
|
+
* A chip printing the value alone states a constraint without saying what
|
|
242
|
+
* it constrains, which beside a rail of a dozen axes names nothing. */
|
|
243
|
+
filtersChipPair: "search.filters.chip_pair",
|
|
244
|
+
/** The accessible name of an applied chip: pressing it REMOVES the filter,
|
|
245
|
+
* and the caption alone never says so. `{filter}` is the caption. */
|
|
246
|
+
filtersChipRemove: "search.filters.chip_remove",
|
|
247
|
+
/** A numeric constraint with both bounds, as one phrase. */
|
|
248
|
+
filtersChipRangeBetween: "search.filters.chip_range_between",
|
|
249
|
+
/** A lower bound alone. */
|
|
250
|
+
filtersChipRangeFrom: "search.filters.chip_range_from",
|
|
251
|
+
/** An upper bound alone. */
|
|
252
|
+
filtersChipRangeTo: "search.filters.chip_range_to",
|
|
253
|
+
|
|
235
254
|
// ── a partition: one template split by a value its children name ─────────
|
|
236
255
|
/**
|
|
237
256
|
* The first chip of a partition row — the parent itself, unnarrowed.
|
|
@@ -542,6 +561,12 @@ export const searchI18nBundleEn: Record<string, string> = {
|
|
|
542
561
|
"search.filters.short": "Filters",
|
|
543
562
|
"search.filters.chip_more": ", +{count}",
|
|
544
563
|
"search.filters.chips_overflow": "More · {count}",
|
|
564
|
+
"search.filters.applied_label": "Applied filters",
|
|
565
|
+
"search.filters.chip_pair": "{name}: {value}",
|
|
566
|
+
"search.filters.chip_remove": "Remove filter {filter}",
|
|
567
|
+
"search.filters.chip_range_between": "from {from} to {to}",
|
|
568
|
+
"search.filters.chip_range_from": "from {from}",
|
|
569
|
+
"search.filters.chip_range_to": "up to {to}",
|
|
545
570
|
|
|
546
571
|
"search.empty.exits_title": "Try widening the search",
|
|
547
572
|
"search.empty.up_a_level": "Go up a level",
|
package/src/i18n/ru.ts
CHANGED
|
@@ -154,6 +154,12 @@ export const searchI18nBundleRu: I18nDictionary = {
|
|
|
154
154
|
"search.filters.short": "Фильтры",
|
|
155
155
|
"search.filters.chip_more": ", +{count}",
|
|
156
156
|
"search.filters.chips_overflow": "Ещё {count}",
|
|
157
|
+
"search.filters.applied_label": "Выбранные фильтры",
|
|
158
|
+
"search.filters.chip_pair": "{name}: {value}",
|
|
159
|
+
"search.filters.chip_remove": "Убрать фильтр «{filter}»",
|
|
160
|
+
"search.filters.chip_range_between": "от {from} до {to}",
|
|
161
|
+
"search.filters.chip_range_from": "от {from}",
|
|
162
|
+
"search.filters.chip_range_to": "до {to}",
|
|
157
163
|
|
|
158
164
|
"search.empty.exits_title": "Попробуйте расширить поиск",
|
|
159
165
|
"search.empty.up_a_level": "Подняться на уровень выше",
|