@bearmenu/ui 0.8.20 → 0.8.21
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/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/card.d.ts +2 -2
- package/dist/components/command.d.ts +15 -15
- package/dist/components/item.d.ts +2 -2
- package/dist/components/menu-filter-chips.d.ts +10 -7
- package/dist/components/menu-filter-chips.js +23 -31
- package/dist/components/spinner.d.ts +2 -2
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/text.d.ts +3 -3
- package/package.json +1 -1
- package/src/components/menu-filter-chips.stories.tsx +5 -1
- package/src/components/menu-filter-chips.test.tsx +63 -0
- package/src/components/menu-filter-chips.tsx +27 -39
|
@@ -4,7 +4,7 @@ import * as React from 'react';
|
|
|
4
4
|
import { VariantProps } from 'class-variance-authority';
|
|
5
5
|
|
|
6
6
|
declare const badgeVariants: (props?: ({
|
|
7
|
-
variant?: "default" | "destructive" | "
|
|
7
|
+
variant?: "default" | "destructive" | "outline" | "secondary" | "glass" | "overlay" | "brown" | "warning" | "success" | "onPrimary" | null | undefined;
|
|
8
8
|
live?: boolean | null | undefined;
|
|
9
9
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
10
10
|
interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {
|
|
@@ -3,7 +3,7 @@ import * as React from 'react';
|
|
|
3
3
|
import { VariantProps } from 'class-variance-authority';
|
|
4
4
|
|
|
5
5
|
declare const buttonVariants: (props?: ({
|
|
6
|
-
variant?: "
|
|
6
|
+
variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | "glass" | "overlay" | "ink" | "brown" | "warning" | null | undefined;
|
|
7
7
|
size?: "default" | "xs" | "sm" | "lg" | "pill" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
|
|
8
8
|
fullWidth?: boolean | null | undefined;
|
|
9
9
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
@@ -3,12 +3,12 @@ import * as React from 'react';
|
|
|
3
3
|
import { VariantProps } from 'class-variance-authority';
|
|
4
4
|
|
|
5
5
|
declare const cardVariants: (props?: ({
|
|
6
|
-
variant?: "
|
|
6
|
+
variant?: "outline" | "glass" | "muted" | "surface" | "elevated" | "elevatedPanel" | null | undefined;
|
|
7
7
|
size?: "default" | "sm" | "lg" | null | undefined;
|
|
8
8
|
interactive?: boolean | null | undefined;
|
|
9
9
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
10
10
|
declare const Card: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & VariantProps<(props?: ({
|
|
11
|
-
variant?: "
|
|
11
|
+
variant?: "outline" | "glass" | "muted" | "surface" | "elevated" | "elevatedPanel" | null | undefined;
|
|
12
12
|
size?: "default" | "sm" | "lg" | null | undefined;
|
|
13
13
|
interactive?: boolean | null | undefined;
|
|
14
14
|
} & class_variance_authority_types.ClassProp) | undefined) => string> & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -4,11 +4,11 @@ import { DialogProps } from '@radix-ui/react-dialog';
|
|
|
4
4
|
|
|
5
5
|
declare const Command: React.ForwardRefExoticComponent<Omit<{
|
|
6
6
|
children?: React.ReactNode;
|
|
7
|
-
} & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof React.HTMLAttributes<HTMLDivElement
|
|
7
|
+
} & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
|
|
8
8
|
ref?: React.Ref<HTMLDivElement>;
|
|
9
9
|
} & {
|
|
10
10
|
asChild?: boolean;
|
|
11
|
-
}, keyof React.HTMLAttributes<HTMLDivElement> | "asChild"
|
|
11
|
+
}, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & {
|
|
12
12
|
label?: string;
|
|
13
13
|
shouldFilter?: boolean;
|
|
14
14
|
filter?: (value: string, search: string, keywords?: string[]) => number;
|
|
@@ -28,61 +28,61 @@ declare const CommandInput: React.ForwardRefExoticComponent<Omit<Omit<Pick<Pick<
|
|
|
28
28
|
ref?: React.Ref<HTMLInputElement>;
|
|
29
29
|
} & {
|
|
30
30
|
asChild?: boolean;
|
|
31
|
-
}, "
|
|
31
|
+
}, "key" | "asChild" | keyof React.InputHTMLAttributes<HTMLInputElement>>, "onChange" | "type" | "value"> & {
|
|
32
32
|
value?: string;
|
|
33
33
|
onValueChange?: (search: string) => void;
|
|
34
34
|
} & React.RefAttributes<HTMLInputElement>, "ref"> & React.RefAttributes<HTMLInputElement>>;
|
|
35
35
|
declare const CommandList: React.ForwardRefExoticComponent<Omit<{
|
|
36
36
|
children?: React.ReactNode;
|
|
37
|
-
} & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof React.HTMLAttributes<HTMLDivElement
|
|
37
|
+
} & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
|
|
38
38
|
ref?: React.Ref<HTMLDivElement>;
|
|
39
39
|
} & {
|
|
40
40
|
asChild?: boolean;
|
|
41
|
-
}, keyof React.HTMLAttributes<HTMLDivElement> | "asChild"
|
|
41
|
+
}, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & {
|
|
42
42
|
label?: string;
|
|
43
43
|
} & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
44
44
|
declare const CommandEmpty: React.ForwardRefExoticComponent<Omit<{
|
|
45
45
|
children?: React.ReactNode;
|
|
46
|
-
} & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof React.HTMLAttributes<HTMLDivElement
|
|
46
|
+
} & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
|
|
47
47
|
ref?: React.Ref<HTMLDivElement>;
|
|
48
48
|
} & {
|
|
49
49
|
asChild?: boolean;
|
|
50
|
-
}, keyof React.HTMLAttributes<HTMLDivElement> | "asChild"
|
|
50
|
+
}, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
51
51
|
declare const CommandLoading: React.ForwardRefExoticComponent<Omit<{
|
|
52
52
|
children?: React.ReactNode;
|
|
53
|
-
} & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof React.HTMLAttributes<HTMLDivElement
|
|
53
|
+
} & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
|
|
54
54
|
ref?: React.Ref<HTMLDivElement>;
|
|
55
55
|
} & {
|
|
56
56
|
asChild?: boolean;
|
|
57
|
-
}, keyof React.HTMLAttributes<HTMLDivElement> | "asChild"
|
|
57
|
+
}, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & {
|
|
58
58
|
progress?: number;
|
|
59
59
|
label?: string;
|
|
60
60
|
} & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
61
61
|
declare const CommandGroup: React.ForwardRefExoticComponent<Omit<{
|
|
62
62
|
children?: React.ReactNode;
|
|
63
|
-
} & Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof React.HTMLAttributes<HTMLDivElement
|
|
63
|
+
} & Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
|
|
64
64
|
ref?: React.Ref<HTMLDivElement>;
|
|
65
65
|
} & {
|
|
66
66
|
asChild?: boolean;
|
|
67
|
-
}, keyof React.HTMLAttributes<HTMLDivElement> | "asChild"
|
|
67
|
+
}, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild">, "heading" | "value"> & {
|
|
68
68
|
heading?: React.ReactNode;
|
|
69
69
|
value?: string;
|
|
70
70
|
forceMount?: boolean;
|
|
71
71
|
} & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
72
|
-
declare const CommandSeparator: React.ForwardRefExoticComponent<Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof React.HTMLAttributes<HTMLDivElement
|
|
72
|
+
declare const CommandSeparator: React.ForwardRefExoticComponent<Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
|
|
73
73
|
ref?: React.Ref<HTMLDivElement>;
|
|
74
74
|
} & {
|
|
75
75
|
asChild?: boolean;
|
|
76
|
-
}, keyof React.HTMLAttributes<HTMLDivElement> | "asChild"
|
|
76
|
+
}, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & {
|
|
77
77
|
alwaysRender?: boolean;
|
|
78
78
|
} & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
79
79
|
declare const CommandItem: React.ForwardRefExoticComponent<Omit<{
|
|
80
80
|
children?: React.ReactNode;
|
|
81
|
-
} & Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof React.HTMLAttributes<HTMLDivElement
|
|
81
|
+
} & Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
|
|
82
82
|
ref?: React.Ref<HTMLDivElement>;
|
|
83
83
|
} & {
|
|
84
84
|
asChild?: boolean;
|
|
85
|
-
}, keyof React.HTMLAttributes<HTMLDivElement> | "asChild"
|
|
85
|
+
}, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild">, "onSelect" | "disabled" | "value"> & {
|
|
86
86
|
disabled?: boolean;
|
|
87
87
|
onSelect?: (value: string) => void;
|
|
88
88
|
value?: string;
|
|
@@ -3,12 +3,12 @@ import * as React from 'react';
|
|
|
3
3
|
import { VariantProps } from 'class-variance-authority';
|
|
4
4
|
|
|
5
5
|
declare const itemVariants: (props?: ({
|
|
6
|
-
variant?: "default" | "
|
|
6
|
+
variant?: "default" | "outline" | "muted" | null | undefined;
|
|
7
7
|
size?: "default" | "sm" | "lg" | null | undefined;
|
|
8
8
|
interactive?: boolean | null | undefined;
|
|
9
9
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
10
10
|
declare const Item: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & VariantProps<(props?: ({
|
|
11
|
-
variant?: "default" | "
|
|
11
|
+
variant?: "default" | "outline" | "muted" | null | undefined;
|
|
12
12
|
size?: "default" | "sm" | "lg" | null | undefined;
|
|
13
13
|
interactive?: boolean | null | undefined;
|
|
14
14
|
} & class_variance_authority_types.ClassProp) | undefined) => string> & {
|
|
@@ -39,22 +39,25 @@ type MenuFilterChipsProps = {
|
|
|
39
39
|
onToggle: (key: string) => void;
|
|
40
40
|
/** The unit and the current total, composed by the caller, e.g. "KITCHENS · 247". */
|
|
41
41
|
unitLabel?: string;
|
|
42
|
-
/** Opens the full filter
|
|
42
|
+
/** Opens the full filter panel. Omit for a row where every facet is a chip. */
|
|
43
43
|
onMore?: () => void;
|
|
44
44
|
moreLabel?: string;
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
46
|
+
* The glyph on the "more filters" pill.
|
|
47
47
|
*
|
|
48
|
-
* The
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
48
|
+
* The pill is a chip like the others in the row — same border, same height —
|
|
49
|
+
* so the way out of the row reads as part of the row, not as a link beside
|
|
50
|
+
* it. The label is the widest fixed element in the row ("More filters" is
|
|
51
|
+
* ~80px of a 358px viewport) and the one thing that never scrolls, so below
|
|
52
|
+
* `lg` the pill collapses to a square carrying only the glyph, with
|
|
53
|
+
* `moreLabel` as its accessible name; from `lg` it shows both.
|
|
52
54
|
*
|
|
53
55
|
* A node rather than a name because the app owns its iconography, and this
|
|
54
56
|
* one is animated on press — the DS neither knows the glyph nor drives it.
|
|
57
|
+
* Without a glyph the pill shows the label at every width.
|
|
55
58
|
*/
|
|
56
59
|
moreIcon?: React.ReactNode;
|
|
57
|
-
/** Press handler for the
|
|
60
|
+
/** Press handler for the pill, e.g. to replay the glyph's animation. */
|
|
58
61
|
onMorePointerDown?: () => void;
|
|
59
62
|
/** Locale-aware integer formatter for the counts. Defaults to `String`. */
|
|
60
63
|
formatCount?: (value: number) => string;
|
|
@@ -3,7 +3,7 @@ import { Skeleton } from '../chunk-XBBEX4SF.js';
|
|
|
3
3
|
import { cn } from '../chunk-FEK5XGZW.js';
|
|
4
4
|
import '../chunk-MMUBH76A.js';
|
|
5
5
|
import { X } from 'lucide-react';
|
|
6
|
-
import { jsxs, jsx
|
|
6
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
7
7
|
|
|
8
8
|
function MenuFilterChips({
|
|
9
9
|
chips,
|
|
@@ -39,36 +39,28 @@ function MenuFilterChips({
|
|
|
39
39
|
)) })
|
|
40
40
|
}
|
|
41
41
|
),
|
|
42
|
-
onMore && /* @__PURE__ */ jsxs(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
className: cn(
|
|
65
|
-
"shrink-0 rounded-md pl-2 text-[12.5px] font-bold text-accent transition-colors hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none",
|
|
66
|
-
moreIcon && "hidden lg:inline"
|
|
67
|
-
),
|
|
68
|
-
children: moreLabel
|
|
69
|
-
}
|
|
70
|
-
)
|
|
71
|
-
] })
|
|
42
|
+
onMore && /* @__PURE__ */ jsxs(
|
|
43
|
+
"button",
|
|
44
|
+
{
|
|
45
|
+
type: "button",
|
|
46
|
+
onClick: onMore,
|
|
47
|
+
onPointerDown: onMorePointerDown,
|
|
48
|
+
"aria-label": moreLabel,
|
|
49
|
+
"aria-haspopup": "dialog",
|
|
50
|
+
className: cn(
|
|
51
|
+
// The chip's own chrome, so the pinned control and the scrolling
|
|
52
|
+
// chips read as one row. `shrink-0` against the scrolling chips.
|
|
53
|
+
"inline-flex shrink-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-full border border-border bg-card text-sm text-foreground transition-colors hover:border-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none",
|
|
54
|
+
chipSize === "lg" ? "h-11" : "h-9",
|
|
55
|
+
// With a glyph: a square below `lg`, the glyph and the label from it.
|
|
56
|
+
moreIcon ? chipSize === "lg" ? "w-11 lg:w-auto lg:px-4" : "w-9 lg:w-auto lg:px-4" : "px-4"
|
|
57
|
+
),
|
|
58
|
+
children: [
|
|
59
|
+
moreIcon,
|
|
60
|
+
/* @__PURE__ */ jsx("span", { className: cn(moreIcon && "hidden lg:inline"), children: moreLabel })
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
)
|
|
72
64
|
] });
|
|
73
65
|
}
|
|
74
66
|
function Chip({
|
|
@@ -4,11 +4,11 @@ import { VariantProps } from 'class-variance-authority';
|
|
|
4
4
|
|
|
5
5
|
declare const spinnerVariants: (props?: ({
|
|
6
6
|
size?: "default" | "xs" | "sm" | "lg" | "xl" | null | undefined;
|
|
7
|
-
tone?: "
|
|
7
|
+
tone?: "inherit" | "default" | "destructive" | "primary" | null | undefined;
|
|
8
8
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
9
9
|
declare const Spinner: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLSpanElement> & VariantProps<(props?: ({
|
|
10
10
|
size?: "default" | "xs" | "sm" | "lg" | "xl" | null | undefined;
|
|
11
|
-
tone?: "
|
|
11
|
+
tone?: "inherit" | "default" | "destructive" | "primary" | null | undefined;
|
|
12
12
|
} & class_variance_authority_types.ClassProp) | undefined) => string> & {
|
|
13
13
|
/** Visually-hidden label for screen readers. Defaults to "Loading". */
|
|
14
14
|
label?: string;
|
package/dist/components/tag.d.ts
CHANGED
|
@@ -3,11 +3,11 @@ import * as React from 'react';
|
|
|
3
3
|
import { VariantProps } from 'class-variance-authority';
|
|
4
4
|
|
|
5
5
|
declare const tagVariants: (props?: ({
|
|
6
|
-
variant?: "default" | "destructive" | "
|
|
6
|
+
variant?: "default" | "destructive" | "outline" | "warning" | "success" | "primary" | "quiet" | null | undefined;
|
|
7
7
|
size?: "default" | "sm" | "lg" | null | undefined;
|
|
8
8
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
9
9
|
declare const Tag: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLSpanElement> & VariantProps<(props?: ({
|
|
10
|
-
variant?: "default" | "destructive" | "
|
|
10
|
+
variant?: "default" | "destructive" | "outline" | "warning" | "success" | "primary" | "quiet" | null | undefined;
|
|
11
11
|
size?: "default" | "sm" | "lg" | null | undefined;
|
|
12
12
|
} & class_variance_authority_types.ClassProp) | undefined) => string> & {
|
|
13
13
|
onRemove?: () => void;
|
|
@@ -22,9 +22,9 @@ import { VariantProps } from 'class-variance-authority';
|
|
|
22
22
|
* Weights are 400 / 600 / 700. 500 is not in the system.
|
|
23
23
|
*/
|
|
24
24
|
declare const textVariants: (props?: ({
|
|
25
|
-
variant?: "
|
|
26
|
-
color?: "
|
|
27
|
-
weight?: "bold" | "
|
|
25
|
+
variant?: "h2" | "h3" | "label" | "body" | "caption" | "h1" | "h4" | "small" | "display" | "body-lg" | "body-sm" | "overline" | null | undefined;
|
|
26
|
+
color?: "inherit" | "default" | "destructive" | "muted" | "primary" | "primary-foreground" | null | undefined;
|
|
27
|
+
weight?: "bold" | "medium" | "normal" | "semibold" | null | undefined;
|
|
28
28
|
leading?: "none" | "normal" | "tight" | "snug" | "relaxed" | "loose" | null | undefined;
|
|
29
29
|
tracking?: "normal" | "tight" | "wide" | "wider" | null | undefined;
|
|
30
30
|
wrap?: "balance" | "pretty" | "nowrap" | null | undefined;
|
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import { useState } from "react";
|
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
3
|
import { Clock, Leaf, MapPin, Moon, Sun, Wallet } from "lucide-react";
|
|
4
4
|
import { MenuFilterChips, MenuFilterSummary } from "./menu-filter-chips";
|
|
5
|
+
import { MotionSliders } from "./motion-icon-controls";
|
|
5
6
|
import { mockFilterChips } from "./menu-mocks";
|
|
6
7
|
|
|
7
8
|
const icons: Record<string, React.ReactNode> = {
|
|
@@ -30,7 +31,7 @@ const meta: Meta<typeof MenuFilterChips> = {
|
|
|
30
31
|
docs: {
|
|
31
32
|
description: {
|
|
32
33
|
component:
|
|
33
|
-
"Every chip counts KITCHENS, whatever facet is behind it — a dietary chip reads *has a vegetarian dish* and counts venues, not dishes. The unit is stated once at the head of the row and never again; the earlier version mixed venue counts and item counts and you could not tell what a number meant without re-reading the label that produced it. One line in every state: the chips scroll under a `ScrollFade`, and *More filters* is pinned opposite the unit label rather than wrapping.",
|
|
34
|
+
"Every chip counts KITCHENS, whatever facet is behind it — a dietary chip reads *has a vegetarian dish* and counts venues, not dishes. The unit is stated once at the head of the row and never again; the earlier version mixed venue counts and item counts and you could not tell what a number meant without re-reading the label that produced it. One line in every state: the chips scroll under a `ScrollFade`, and *More filters* is a chip-shaped pill pinned opposite the unit label rather than wrapping — a square carrying only its glyph below `lg`, glyph and label from it.",
|
|
34
35
|
},
|
|
35
36
|
},
|
|
36
37
|
},
|
|
@@ -41,6 +42,7 @@ type Story = StoryObj<typeof MenuFilterChips>;
|
|
|
41
42
|
|
|
42
43
|
function Interactive({ initial }: { initial: typeof chips }) {
|
|
43
44
|
const [state, setState] = useState(initial);
|
|
45
|
+
const [replayToken, setReplayToken] = useState(0);
|
|
44
46
|
const active = state.filter((chip) => chip.active).length;
|
|
45
47
|
|
|
46
48
|
return (
|
|
@@ -49,6 +51,8 @@ function Interactive({ initial }: { initial: typeof chips }) {
|
|
|
49
51
|
chips={state}
|
|
50
52
|
unitLabel="KITCHENS · 247"
|
|
51
53
|
moreLabel="More filters"
|
|
54
|
+
moreIcon={<MotionSliders replayToken={replayToken} className="size-4" />}
|
|
55
|
+
onMorePointerDown={() => setReplayToken((n) => n + 1)}
|
|
52
56
|
onMore={() => {}}
|
|
53
57
|
ariaLabel="Filter kitchens"
|
|
54
58
|
onToggle={(key) =>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
3
|
+
import { MenuFilterChips } from "./menu-filter-chips";
|
|
4
|
+
import { mockFilterChips } from "./menu-mocks";
|
|
5
|
+
|
|
6
|
+
describe("MenuFilterChips", () => {
|
|
7
|
+
/**
|
|
8
|
+
* The "more filters" control is ONE button at every width, shaped like the
|
|
9
|
+
* chips beside it. It used to be two elements — an icon square below `lg`
|
|
10
|
+
* and a bare text link from it — and the link read as a footnote rather
|
|
11
|
+
* than as the row's last chip.
|
|
12
|
+
*/
|
|
13
|
+
it("renders the more control as a single chip-shaped pill carrying the glyph and the label", () => {
|
|
14
|
+
const onMore = vi.fn();
|
|
15
|
+
const onMorePointerDown = vi.fn();
|
|
16
|
+
render(
|
|
17
|
+
<MenuFilterChips
|
|
18
|
+
chips={mockFilterChips}
|
|
19
|
+
onToggle={() => {}}
|
|
20
|
+
moreLabel="More filters"
|
|
21
|
+
moreIcon={<svg data-testid="glyph" />}
|
|
22
|
+
onMore={onMore}
|
|
23
|
+
onMorePointerDown={onMorePointerDown}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const pills = screen.getAllByRole("button", { name: "More filters" });
|
|
28
|
+
expect(pills).toHaveLength(1);
|
|
29
|
+
const [pill] = pills;
|
|
30
|
+
expect(pill).toHaveAttribute("aria-haspopup", "dialog");
|
|
31
|
+
expect(pill.className).toContain("rounded-full");
|
|
32
|
+
expect(pill.className).toContain("border-border");
|
|
33
|
+
expect(pill.className).toContain("h-9");
|
|
34
|
+
expect(pill.className).toContain("lg:px-4");
|
|
35
|
+
expect(pill.querySelector("[data-testid='glyph']")).not.toBeNull();
|
|
36
|
+
expect(pill.querySelector("span")?.className).toContain("hidden lg:inline");
|
|
37
|
+
|
|
38
|
+
fireEvent.pointerDown(pill);
|
|
39
|
+
fireEvent.click(pill);
|
|
40
|
+
expect(onMorePointerDown).toHaveBeenCalledTimes(1);
|
|
41
|
+
expect(onMore).toHaveBeenCalledTimes(1);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("shows the label at every width when no glyph is given", () => {
|
|
45
|
+
render(
|
|
46
|
+
<MenuFilterChips
|
|
47
|
+
chips={mockFilterChips}
|
|
48
|
+
onToggle={() => {}}
|
|
49
|
+
moreLabel="More filters"
|
|
50
|
+
onMore={() => {}}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const pill = screen.getByRole("button", { name: "More filters" });
|
|
55
|
+
expect(pill.className).toContain("px-4");
|
|
56
|
+
expect(pill.querySelector("span")?.className ?? "").not.toContain("hidden");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("renders no more control without a handler", () => {
|
|
60
|
+
render(<MenuFilterChips chips={mockFilterChips} onToggle={() => {}} moreLabel="More filters" />);
|
|
61
|
+
expect(screen.queryByRole("button", { name: "More filters" })).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -40,22 +40,25 @@ export type MenuFilterChipsProps = {
|
|
|
40
40
|
onToggle: (key: string) => void;
|
|
41
41
|
/** The unit and the current total, composed by the caller, e.g. "KITCHENS · 247". */
|
|
42
42
|
unitLabel?: string;
|
|
43
|
-
/** Opens the full filter
|
|
43
|
+
/** Opens the full filter panel. Omit for a row where every facet is a chip. */
|
|
44
44
|
onMore?: () => void;
|
|
45
45
|
moreLabel?: string;
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
47
|
+
* The glyph on the "more filters" pill.
|
|
48
48
|
*
|
|
49
|
-
* The
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
49
|
+
* The pill is a chip like the others in the row — same border, same height —
|
|
50
|
+
* so the way out of the row reads as part of the row, not as a link beside
|
|
51
|
+
* it. The label is the widest fixed element in the row ("More filters" is
|
|
52
|
+
* ~80px of a 358px viewport) and the one thing that never scrolls, so below
|
|
53
|
+
* `lg` the pill collapses to a square carrying only the glyph, with
|
|
54
|
+
* `moreLabel` as its accessible name; from `lg` it shows both.
|
|
53
55
|
*
|
|
54
56
|
* A node rather than a name because the app owns its iconography, and this
|
|
55
57
|
* one is animated on press — the DS neither knows the glyph nor drives it.
|
|
58
|
+
* Without a glyph the pill shows the label at every width.
|
|
56
59
|
*/
|
|
57
60
|
moreIcon?: React.ReactNode;
|
|
58
|
-
/** Press handler for the
|
|
61
|
+
/** Press handler for the pill, e.g. to replay the glyph's animation. */
|
|
59
62
|
onMorePointerDown?: () => void;
|
|
60
63
|
/** Locale-aware integer formatter for the counts. Defaults to `String`. */
|
|
61
64
|
formatCount?: (value: number) => string;
|
|
@@ -117,39 +120,24 @@ export function MenuFilterChips({
|
|
|
117
120
|
</ScrollFade>
|
|
118
121
|
|
|
119
122
|
{onMore && (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
{
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
className={cn(
|
|
134
|
-
"inline-flex shrink-0 items-center justify-center rounded-full border border-border text-accent transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none lg:hidden",
|
|
135
|
-
// The one control that never scrolls keeps the chips' height.
|
|
136
|
-
chipSize === "lg" ? "size-11" : "size-10"
|
|
137
|
-
)}
|
|
138
|
-
>
|
|
139
|
-
{moreIcon}
|
|
140
|
-
</button>
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
onClick={onMore}
|
|
126
|
+
onPointerDown={onMorePointerDown}
|
|
127
|
+
aria-label={moreLabel}
|
|
128
|
+
aria-haspopup="dialog"
|
|
129
|
+
className={cn(
|
|
130
|
+
// The chip's own chrome, so the pinned control and the scrolling
|
|
131
|
+
// chips read as one row. `shrink-0` against the scrolling chips.
|
|
132
|
+
"inline-flex shrink-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-full border border-border bg-card text-sm text-foreground transition-colors hover:border-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none",
|
|
133
|
+
chipSize === "lg" ? "h-11" : "h-9",
|
|
134
|
+
// With a glyph: a square below `lg`, the glyph and the label from it.
|
|
135
|
+
moreIcon ? (chipSize === "lg" ? "w-11 lg:w-auto lg:px-4" : "w-9 lg:w-auto lg:px-4") : "px-4"
|
|
141
136
|
)}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
"shrink-0 rounded-md pl-2 text-[12.5px] font-bold text-accent transition-colors hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none",
|
|
147
|
-
moreIcon && "hidden lg:inline"
|
|
148
|
-
)}
|
|
149
|
-
>
|
|
150
|
-
{moreLabel}
|
|
151
|
-
</button>
|
|
152
|
-
</>
|
|
137
|
+
>
|
|
138
|
+
{moreIcon}
|
|
139
|
+
<span className={cn(moreIcon && "hidden lg:inline")}>{moreLabel}</span>
|
|
140
|
+
</button>
|
|
153
141
|
)}
|
|
154
142
|
</div>
|
|
155
143
|
);
|