@bug-on/md3-react 2.0.3 → 3.0.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/.turbo/turbo-build.log +33 -0
- package/CHANGELOG.md +55 -0
- package/dist/index.css.d.ts +2 -0
- package/dist/index.d.mts +6127 -0
- package/dist/index.d.ts +6127 -71
- package/dist/index.js +1653 -614
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1566 -547
- package/dist/index.mjs.map +1 -1
- package/dist/material-symbols-cdn.css.d.ts +2 -0
- package/dist/material-symbols-self-hosted.css.d.ts +2 -0
- package/dist/typography.css.d.ts +2 -0
- package/package.json +22 -19
- package/scripts/copy-assets.js +82 -0
- package/src/assets/fonts/GoogleSansFlex-VariableFont.woff2 +0 -0
- package/src/assets/fonts/MaterialSymbolsOutlined-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
- package/src/assets/fonts/MaterialSymbolsRounded-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
- package/src/assets/fonts/MaterialSymbolsSharp-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
- package/src/assets/loading-indicator.svg +19 -0
- package/src/assets/material-symbols-cdn.css +65 -0
- package/src/assets/material-symbols-self-hosted.css +90 -0
- package/src/css.d.ts +20 -0
- package/src/hooks/useClickOutside.ts +37 -0
- package/src/hooks/useMediaQuery.ts +28 -0
- package/src/hooks/useRipple.ts +88 -0
- package/src/index.css +23 -0
- package/src/index.ts +349 -0
- package/src/lib/material-symbols-preconnect.tsx +82 -0
- package/src/lib/theme-utils.ts +180 -0
- package/src/lib/utils.ts +6 -0
- package/src/test/button.test.tsx +59 -0
- package/src/test/icon.test.tsx +91 -0
- package/src/test/loading-indicator.test.tsx +128 -0
- package/src/test/progress-indicator.test.tsx +306 -0
- package/src/test/setup.ts +80 -0
- package/src/test/typography.test.tsx +206 -0
- package/src/types/index.ts +7 -0
- package/src/types/md3.ts +31 -0
- package/src/ui/Text.tsx +60 -0
- package/src/ui/__snapshots__/divider.test.tsx.snap +63 -0
- package/src/ui/app-bar/app-bar-column.tsx +99 -0
- package/src/ui/app-bar/app-bar-item-button.tsx +71 -0
- package/src/ui/app-bar/app-bar-items.test.tsx +89 -0
- package/src/ui/app-bar/app-bar-overflow-indicator.tsx +108 -0
- package/src/ui/app-bar/app-bar-row.tsx +104 -0
- package/src/ui/app-bar/app-bar.test.tsx +87 -0
- package/src/ui/app-bar/app-bar.tokens.ts +223 -0
- package/src/ui/app-bar/app-bar.types.ts +441 -0
- package/src/ui/app-bar/bottom-app-bar.test.tsx +42 -0
- package/src/ui/app-bar/bottom-app-bar.tsx +84 -0
- package/src/ui/app-bar/docked-toolbar.test.tsx +34 -0
- package/src/ui/app-bar/docked-toolbar.tsx +54 -0
- package/src/ui/app-bar/flexible-app-bar.test.tsx +75 -0
- package/src/ui/app-bar/hooks/use-app-bar-scroll.ts +110 -0
- package/src/ui/app-bar/hooks/use-flexible-app-bar.ts +123 -0
- package/{dist/ui/app-bar/index.d.ts → src/ui/app-bar/index.ts} +35 -2
- package/src/ui/app-bar/large-flexible-app-bar.tsx +165 -0
- package/src/ui/app-bar/medium-flexible-app-bar.tsx +167 -0
- package/src/ui/app-bar/search-app-bar.test.tsx +49 -0
- package/src/ui/app-bar/search-app-bar.tsx +176 -0
- package/src/ui/app-bar/search-view.tsx +227 -0
- package/src/ui/app-bar/small-app-bar.test.tsx +48 -0
- package/src/ui/app-bar/small-app-bar.tsx +203 -0
- package/src/ui/badge.test.tsx +345 -0
- package/src/ui/badge.tsx +282 -0
- package/src/ui/button-group.test.tsx +71 -0
- package/src/ui/button-group.tsx +350 -0
- package/src/ui/button.test.tsx +297 -0
- package/src/ui/button.tsx +669 -0
- package/src/ui/card.test.tsx +187 -0
- package/src/ui/card.tsx +259 -0
- package/src/ui/checkbox.test.tsx +423 -0
- package/src/ui/checkbox.tsx +525 -0
- package/src/ui/chip.test.tsx +292 -0
- package/src/ui/chip.tsx +548 -0
- package/src/ui/code-block.tsx +219 -0
- package/src/ui/dialog.test.tsx +300 -0
- package/src/ui/dialog.tsx +384 -0
- package/src/ui/divider.test.tsx +314 -0
- package/src/ui/divider.tsx +412 -0
- package/src/ui/drawer.tsx +240 -0
- package/src/ui/fab-menu.test.tsx +494 -0
- package/src/ui/fab-menu.tsx +739 -0
- package/src/ui/fab.test.tsx +232 -0
- package/src/ui/fab.tsx +505 -0
- package/src/ui/icon-button.test.tsx +515 -0
- package/src/ui/icon-button.tsx +525 -0
- package/src/ui/icon.test.tsx +197 -0
- package/src/ui/icon.tsx +179 -0
- package/src/ui/loading-indicator.test.tsx +73 -0
- package/src/ui/loading-indicator.tsx +312 -0
- package/src/ui/menu/context-menu.tsx +275 -0
- package/src/ui/menu/index.ts +77 -0
- package/src/ui/menu/menu-animations.ts +102 -0
- package/src/ui/menu/menu-context.tsx +99 -0
- package/src/ui/menu/menu-divider.tsx +47 -0
- package/src/ui/menu/menu-group.tsx +200 -0
- package/src/ui/menu/menu-item.tsx +294 -0
- package/src/ui/menu/menu-tokens.ts +208 -0
- package/src/ui/menu/menu-types.ts +313 -0
- package/src/ui/menu/menu.test.tsx +624 -0
- package/src/ui/menu/menu.tsx +289 -0
- package/src/ui/menu/sub-menu.tsx +223 -0
- package/src/ui/menu/vertical-menu.tsx +382 -0
- package/src/ui/navigation-rail.test.tsx +404 -0
- package/src/ui/navigation-rail.tsx +604 -0
- package/src/ui/progress-indicator/circular.tsx +248 -0
- package/src/ui/progress-indicator/hooks.ts +51 -0
- package/{dist/ui/progress-indicator/index.d.ts → src/ui/progress-indicator/index.tsx} +20 -2
- package/src/ui/progress-indicator/linear-flat.tsx +83 -0
- package/src/ui/progress-indicator/linear-wavy.tsx +243 -0
- package/src/ui/progress-indicator/linear.tsx +143 -0
- package/src/ui/progress-indicator/types.ts +158 -0
- package/src/ui/progress-indicator/utils.ts +73 -0
- package/src/ui/radio-button.test.tsx +407 -0
- package/src/ui/radio-button.tsx +551 -0
- package/src/ui/ripple.test.tsx +72 -0
- package/src/ui/ripple.tsx +234 -0
- package/src/ui/scroll-area.test.tsx +58 -0
- package/src/ui/scroll-area.tsx +139 -0
- package/src/ui/search/animated-placeholder.tsx +145 -0
- package/src/ui/search/hooks/use-search-keyboard.test.ts +202 -0
- package/src/ui/search/hooks/use-search-keyboard.ts +104 -0
- package/src/ui/search/hooks/use-search-view-focus.test.ts +96 -0
- package/src/ui/search/hooks/use-search-view-focus.ts +24 -0
- package/src/ui/search/index.ts +44 -0
- package/src/ui/search/search-bar.tsx +220 -0
- package/src/ui/search/search-context.tsx +42 -0
- package/src/ui/search/search-view-docked.tsx +194 -0
- package/src/ui/search/search-view-fullscreen.tsx +247 -0
- package/src/ui/search/search.test.tsx +233 -0
- package/src/ui/search/search.tokens.ts +134 -0
- package/src/ui/search/search.tsx +131 -0
- package/src/ui/search/search.types.ts +154 -0
- package/src/ui/search/trailing-action.tsx +49 -0
- package/src/ui/shared/constants.ts +122 -0
- package/{dist/ui/shared/touch-target.d.ts → src/ui/shared/touch-target.tsx} +13 -1
- package/src/ui/slider/hooks/useSliderMath.ts +195 -0
- package/{dist/ui/slider/index.d.ts → src/ui/slider/index.ts} +12 -1
- package/src/ui/slider/range-slider.tsx +561 -0
- package/src/ui/slider/slider-thumb.tsx +379 -0
- package/src/ui/slider/slider-track.tsx +912 -0
- package/src/ui/slider/slider.tokens.ts +189 -0
- package/src/ui/slider/slider.tsx +259 -0
- package/src/ui/slider/slider.types.ts +288 -0
- package/src/ui/snackbar/index.ts +20 -0
- package/src/ui/snackbar/snackbar.test.tsx +338 -0
- package/src/ui/snackbar/snackbar.tsx +476 -0
- package/{dist/ui/switch/index.d.ts → src/ui/switch/index.ts} +1 -0
- package/src/ui/switch/switch.stories.tsx +309 -0
- package/src/ui/switch/switch.test.tsx +243 -0
- package/src/ui/switch/switch.tokens.ts +89 -0
- package/src/ui/switch/switch.tsx +504 -0
- package/src/ui/switch/switch.types.ts +62 -0
- package/{dist/ui/tabs/index.d.ts → src/ui/tabs/index.ts} +8 -1
- package/src/ui/tabs/tab.tsx +407 -0
- package/src/ui/tabs/tabs-content.tsx +89 -0
- package/src/ui/tabs/tabs-list.tsx +146 -0
- package/src/ui/tabs/tabs.test.tsx +290 -0
- package/src/ui/tabs/tabs.tokens.ts +121 -0
- package/src/ui/tabs/tabs.tsx +229 -0
- package/src/ui/tabs/tabs.types.ts +185 -0
- package/{dist/ui/text-field/index.d.ts → src/ui/text-field/index.ts} +8 -1
- package/src/ui/text-field/subcomponents/active-indicator.tsx +67 -0
- package/src/ui/text-field/subcomponents/floating-label.tsx +161 -0
- package/src/ui/text-field/subcomponents/leading-icon.tsx +46 -0
- package/src/ui/text-field/subcomponents/outline-container.tsx +170 -0
- package/src/ui/text-field/subcomponents/prefix-suffix.tsx +59 -0
- package/src/ui/text-field/subcomponents/supporting-text.tsx +145 -0
- package/src/ui/text-field/subcomponents/trailing-icon.tsx +199 -0
- package/src/ui/text-field/text-field.test.tsx +454 -0
- package/src/ui/text-field/text-field.tokens.ts +104 -0
- package/src/ui/text-field/text-field.tsx +548 -0
- package/src/ui/text-field/text-field.types.ts +180 -0
- package/src/ui/theme-provider/index.tsx +190 -0
- package/src/ui/toc.test.tsx +108 -0
- package/src/ui/toc.tsx +172 -0
- package/src/ui/tooltip/plain-tooltip.tsx +63 -0
- package/src/ui/tooltip/rich-tooltip.tsx +94 -0
- package/src/ui/tooltip/tooltip-box.tsx +266 -0
- package/src/ui/tooltip/tooltip-caret-shape.tsx +68 -0
- package/src/ui/tooltip/tooltip.tokens.ts +26 -0
- package/src/ui/tooltip/tooltip.types.ts +70 -0
- package/src/ui/tooltip/use-tooltip-position.ts +208 -0
- package/src/ui/tooltip/use-tooltip-state.ts +41 -0
- package/src/ui/typography/__tests__/typography.test.tsx +170 -0
- package/{dist/ui/typography/index.d.ts → src/ui/typography/index.ts} +21 -3
- package/src/ui/typography/type-scale-tokens.ts +205 -0
- package/src/ui/typography/typography-key-tokens.ts +43 -0
- package/src/ui/typography/typography-tokens.ts +360 -0
- package/src/ui/typography/typography.css +22 -0
- package/src/ui/typography/typography.tsx +559 -0
- package/test-render.tsx +4 -0
- package/test-shadow.html +26 -0
- package/test_output.txt +164 -0
- package/test_output_v2.txt +5 -0
- package/tsconfig.build.json +10 -0
- package/tsconfig.json +18 -0
- package/tsup.config.ts +20 -0
- package/vitest.config.ts +11 -0
- package/dist/hooks/useClickOutside.d.ts +0 -8
- package/dist/hooks/useMediaQuery.d.ts +0 -11
- package/dist/hooks/useRipple.d.ts +0 -26
- package/dist/lib/material-symbols-preconnect.d.ts +0 -42
- package/dist/lib/theme-utils.d.ts +0 -63
- package/dist/lib/utils.d.ts +0 -2
- package/dist/types/index.d.ts +0 -1
- package/dist/types/md3.d.ts +0 -14
- package/dist/ui/app-bar/app-bar-column.d.ts +0 -28
- package/dist/ui/app-bar/app-bar-item-button.d.ts +0 -16
- package/dist/ui/app-bar/app-bar-overflow-indicator.d.ts +0 -18
- package/dist/ui/app-bar/app-bar-row.d.ts +0 -36
- package/dist/ui/app-bar/app-bar.tokens.d.ts +0 -184
- package/dist/ui/app-bar/app-bar.types.d.ts +0 -392
- package/dist/ui/app-bar/bottom-app-bar.d.ts +0 -31
- package/dist/ui/app-bar/docked-toolbar.d.ts +0 -25
- package/dist/ui/app-bar/hooks/use-app-bar-scroll.d.ts +0 -42
- package/dist/ui/app-bar/hooks/use-flexible-app-bar.d.ts +0 -37
- package/dist/ui/app-bar/large-flexible-app-bar.d.ts +0 -26
- package/dist/ui/app-bar/medium-flexible-app-bar.d.ts +0 -28
- package/dist/ui/app-bar/search-app-bar.d.ts +0 -43
- package/dist/ui/app-bar/search-view.d.ts +0 -54
- package/dist/ui/app-bar/small-app-bar.d.ts +0 -37
- package/dist/ui/badge.d.ts +0 -125
- package/dist/ui/button-group.d.ts +0 -59
- package/dist/ui/button.d.ts +0 -148
- package/dist/ui/card.d.ts +0 -62
- package/dist/ui/checkbox.d.ts +0 -82
- package/dist/ui/chip.d.ts +0 -110
- package/dist/ui/code-block.d.ts +0 -14
- package/dist/ui/dialog.d.ts +0 -111
- package/dist/ui/divider.d.ts +0 -164
- package/dist/ui/drawer.d.ts +0 -39
- package/dist/ui/dropdown.d.ts +0 -29
- package/dist/ui/fab-menu.d.ts +0 -204
- package/dist/ui/fab.d.ts +0 -162
- package/dist/ui/icon-button.d.ts +0 -131
- package/dist/ui/icon.d.ts +0 -88
- package/dist/ui/loading-indicator.d.ts +0 -42
- package/dist/ui/navigation-rail.d.ts +0 -29
- package/dist/ui/progress-indicator/circular.d.ts +0 -3
- package/dist/ui/progress-indicator/hooks.d.ts +0 -3
- package/dist/ui/progress-indicator/linear-flat.d.ts +0 -10
- package/dist/ui/progress-indicator/linear-wavy.d.ts +0 -18
- package/dist/ui/progress-indicator/linear.d.ts +0 -3
- package/dist/ui/progress-indicator/types.d.ts +0 -151
- package/dist/ui/progress-indicator/utils.d.ts +0 -3
- package/dist/ui/radio-button.d.ts +0 -106
- package/dist/ui/ripple.d.ts +0 -126
- package/dist/ui/scroll-area.d.ts +0 -27
- package/dist/ui/search/animated-placeholder.d.ts +0 -54
- package/dist/ui/search/hooks/use-search-keyboard.d.ts +0 -32
- package/dist/ui/search/hooks/use-search-view-focus.d.ts +0 -6
- package/dist/ui/search/index.d.ts +0 -27
- package/dist/ui/search/search-bar.d.ts +0 -32
- package/dist/ui/search/search-context.d.ts +0 -24
- package/dist/ui/search/search-view-docked.d.ts +0 -25
- package/dist/ui/search/search-view-fullscreen.d.ts +0 -36
- package/dist/ui/search/search.d.ts +0 -50
- package/dist/ui/search/search.tokens.d.ts +0 -112
- package/dist/ui/search/search.types.d.ts +0 -131
- package/dist/ui/search/trailing-action.d.ts +0 -9
- package/dist/ui/shared/constants.d.ts +0 -86
- package/dist/ui/slider/hooks/useSliderMath.d.ts +0 -101
- package/dist/ui/slider/range-slider.d.ts +0 -47
- package/dist/ui/slider/slider-thumb.d.ts +0 -33
- package/dist/ui/slider/slider-track.d.ts +0 -25
- package/dist/ui/slider/slider.d.ts +0 -60
- package/dist/ui/slider/slider.tokens.d.ts +0 -151
- package/dist/ui/slider/slider.types.d.ts +0 -259
- package/dist/ui/snackbar/index.d.ts +0 -6
- package/dist/ui/snackbar/snackbar.d.ts +0 -197
- package/dist/ui/switch/switch.d.ts +0 -30
- package/dist/ui/switch/switch.stories.d.ts +0 -48
- package/dist/ui/switch/switch.tokens.d.ts +0 -67
- package/dist/ui/switch/switch.types.d.ts +0 -59
- package/dist/ui/tabs/tab.d.ts +0 -43
- package/dist/ui/tabs/tabs-content.d.ts +0 -36
- package/dist/ui/tabs/tabs-list.d.ts +0 -40
- package/dist/ui/tabs/tabs.d.ts +0 -60
- package/dist/ui/tabs/tabs.tokens.d.ts +0 -94
- package/dist/ui/tabs/tabs.types.d.ts +0 -172
- package/dist/ui/text-field/subcomponents/active-indicator.d.ts +0 -24
- package/dist/ui/text-field/subcomponents/floating-label.d.ts +0 -43
- package/dist/ui/text-field/subcomponents/leading-icon.d.ts +0 -23
- package/dist/ui/text-field/subcomponents/outline-container.d.ts +0 -42
- package/dist/ui/text-field/subcomponents/prefix-suffix.d.ts +0 -24
- package/dist/ui/text-field/subcomponents/supporting-text.d.ts +0 -37
- package/dist/ui/text-field/subcomponents/trailing-icon.d.ts +0 -41
- package/dist/ui/text-field/text-field.d.ts +0 -49
- package/dist/ui/text-field/text-field.tokens.d.ts +0 -76
- package/dist/ui/text-field/text-field.types.d.ts +0 -126
- package/dist/ui/theme-provider/index.d.ts +0 -48
- package/dist/ui/toc.d.ts +0 -80
- package/dist/ui/tooltip/plain-tooltip.d.ts +0 -2
- package/dist/ui/tooltip/rich-tooltip.d.ts +0 -2
- package/dist/ui/tooltip/tooltip-box.d.ts +0 -2
- package/dist/ui/tooltip/tooltip-caret-shape.d.ts +0 -9
- package/dist/ui/tooltip/tooltip.tokens.d.ts +0 -26
- package/dist/ui/tooltip/tooltip.types.d.ts +0 -56
- package/dist/ui/tooltip/use-tooltip-position.d.ts +0 -8
- package/dist/ui/tooltip/use-tooltip-state.d.ts +0 -2
- package/dist/ui/typography/type-scale-tokens.d.ts +0 -162
- package/dist/ui/typography/typography-key-tokens.d.ts +0 -40
- package/dist/ui/typography/typography-tokens.d.ts +0 -220
- package/dist/ui/typography/typography.d.ts +0 -265
- /package/{dist/hooks/index.d.ts → src/hooks/index.ts} +0 -0
- /package/{dist/ui/tooltip/index.d.ts → src/ui/tooltip/index.ts} +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { SearchAppBar } from "./search-app-bar";
|
|
4
|
+
import { SearchView } from "./search-view";
|
|
5
|
+
|
|
6
|
+
describe("SearchAppBar and SearchView", () => {
|
|
7
|
+
describe("SearchAppBar", () => {
|
|
8
|
+
it("renders correctly with search bar", () => {
|
|
9
|
+
render(<SearchAppBar searchPlaceholder="Search here..." />);
|
|
10
|
+
|
|
11
|
+
expect(screen.getByText("Search here...")).toBeInTheDocument();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("triggers onSearchFocus when clicked and via Enter key", () => {
|
|
15
|
+
const onSearchFocus = vi.fn();
|
|
16
|
+
render(<SearchAppBar onSearchFocus={onSearchFocus} />);
|
|
17
|
+
|
|
18
|
+
const searchBar = screen.getByRole("search");
|
|
19
|
+
fireEvent.click(searchBar);
|
|
20
|
+
expect(onSearchFocus).toHaveBeenCalledTimes(1);
|
|
21
|
+
|
|
22
|
+
fireEvent.keyDown(searchBar, { key: "Enter" });
|
|
23
|
+
expect(onSearchFocus).toHaveBeenCalledTimes(2);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("SearchView", () => {
|
|
28
|
+
it("renders overlay correctly", () => {
|
|
29
|
+
const onClose = vi.fn();
|
|
30
|
+
render(
|
|
31
|
+
<SearchView onClose={onClose} placeholder="Type to search">
|
|
32
|
+
<div data-testid="search-results">Results</div>
|
|
33
|
+
</SearchView>,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
expect(screen.getByPlaceholderText("Type to search")).toBeInTheDocument();
|
|
37
|
+
expect(screen.getByTestId("search-results")).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("calls onClose when back button is pressed", () => {
|
|
41
|
+
const onClose = vi.fn();
|
|
42
|
+
render(<SearchView onClose={onClose} />);
|
|
43
|
+
|
|
44
|
+
const btn = screen.getByRole("button", { name: "Close search" });
|
|
45
|
+
fireEvent.click(btn);
|
|
46
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file search-app-bar.tsx
|
|
3
|
+
* MD3 Expressive Search App Bar.
|
|
4
|
+
*
|
|
5
|
+
* New variant in MD3 Expressive (May 2025).
|
|
6
|
+
* Replaces the title area with a pill-shaped search bar.
|
|
7
|
+
* Uses Framer Motion layoutId to enable shared element transition with <SearchView>.
|
|
8
|
+
*
|
|
9
|
+
* @see docs/m3/app-bars/AppBar.kt — SearchAppBar
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { m, useReducedMotion } from "motion/react";
|
|
13
|
+
import * as React from "react";
|
|
14
|
+
import { cn } from "../../lib/utils";
|
|
15
|
+
import {
|
|
16
|
+
APP_BAR_COLOR_TRANSITION,
|
|
17
|
+
APP_BAR_COLORS,
|
|
18
|
+
AppBarTokens,
|
|
19
|
+
} from "./app-bar.tokens";
|
|
20
|
+
import type { SearchAppBarProps } from "./app-bar.types";
|
|
21
|
+
import { useAppBarScroll } from "./hooks/use-app-bar-scroll";
|
|
22
|
+
|
|
23
|
+
/** Built-in search icon (Material Symbols) used as default. */
|
|
24
|
+
function SearchIcon({ className }: { className?: string }) {
|
|
25
|
+
return (
|
|
26
|
+
<span
|
|
27
|
+
className={cn(
|
|
28
|
+
"material-symbols-rounded text-[20px] leading-none select-none",
|
|
29
|
+
className,
|
|
30
|
+
)}
|
|
31
|
+
aria-hidden="true"
|
|
32
|
+
>
|
|
33
|
+
search
|
|
34
|
+
</span>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* MD3 Expressive Search App Bar.
|
|
40
|
+
*
|
|
41
|
+
* When the search bar is clicked, callers should open a `<SearchView>` overlay.
|
|
42
|
+
* Uses Framer Motion `layoutId` (via `searchBarId`) for a smooth shared-element
|
|
43
|
+
* transition between this bar and the search view.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```tsx
|
|
47
|
+
* const [searchOpen, setSearchOpen] = useState(false);
|
|
48
|
+
*
|
|
49
|
+
* <SearchAppBar
|
|
50
|
+
* searchBarId="main-search"
|
|
51
|
+
* searchPlaceholder="Search messages..."
|
|
52
|
+
* onSearchFocus={() => setSearchOpen(true)}
|
|
53
|
+
* trailingSearchActions={
|
|
54
|
+
* <IconButton aria-label="Voice search"><Icon>mic</Icon></IconButton>
|
|
55
|
+
* }
|
|
56
|
+
* externalActions={<Avatar src={user.avatar} />}
|
|
57
|
+
* />
|
|
58
|
+
*
|
|
59
|
+
* <AnimatePresence>
|
|
60
|
+
* {searchOpen && (
|
|
61
|
+
* <SearchView
|
|
62
|
+
* searchBarId="main-search"
|
|
63
|
+
* onClose={() => setSearchOpen(false)}
|
|
64
|
+
* />
|
|
65
|
+
* )}
|
|
66
|
+
* </AnimatePresence>
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export function SearchAppBar({
|
|
70
|
+
searchPlaceholder = "Search",
|
|
71
|
+
searchValue,
|
|
72
|
+
onSearchFocus,
|
|
73
|
+
leadingSearchIcon,
|
|
74
|
+
trailingSearchActions,
|
|
75
|
+
externalActions,
|
|
76
|
+
navigationIcon,
|
|
77
|
+
colors,
|
|
78
|
+
scrollBehavior = "pinned",
|
|
79
|
+
scrollElement,
|
|
80
|
+
searchBarId = "search-bar",
|
|
81
|
+
className,
|
|
82
|
+
}: SearchAppBarProps) {
|
|
83
|
+
const shouldReduceMotion = useReducedMotion();
|
|
84
|
+
const [isSearchOpen, setIsSearchOpen] = React.useState(false);
|
|
85
|
+
|
|
86
|
+
const { isScrolled } = useAppBarScroll({
|
|
87
|
+
scrollElement,
|
|
88
|
+
behavior:
|
|
89
|
+
scrollBehavior === "exitUntilCollapsed" ? "pinned" : scrollBehavior,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const containerBg = colors?.containerColor ?? APP_BAR_COLORS.container;
|
|
93
|
+
const scrolledBg =
|
|
94
|
+
colors?.scrolledContainerColor ?? APP_BAR_COLORS.scrolledContainer;
|
|
95
|
+
const currentBg = isScrolled ? scrolledBg : containerBg;
|
|
96
|
+
|
|
97
|
+
const cssTransition = shouldReduceMotion
|
|
98
|
+
? undefined
|
|
99
|
+
: `background-color ${APP_BAR_COLOR_TRANSITION.duration}s cubic-bezier(${APP_BAR_COLOR_TRANSITION.ease.join(",")})`;
|
|
100
|
+
|
|
101
|
+
const handleSearchClick = () => {
|
|
102
|
+
setIsSearchOpen(true);
|
|
103
|
+
onSearchFocus?.();
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
107
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
handleSearchClick();
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<m.header
|
|
115
|
+
role="banner"
|
|
116
|
+
className={cn(
|
|
117
|
+
"fixed top-0 inset-x-0 z-50 flex items-center gap-2 px-4",
|
|
118
|
+
className,
|
|
119
|
+
)}
|
|
120
|
+
style={{
|
|
121
|
+
height: AppBarTokens.heights.small,
|
|
122
|
+
backgroundColor: currentBg,
|
|
123
|
+
transition: cssTransition,
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
126
|
+
{/* Optional navigation icon — leading edge */}
|
|
127
|
+
{navigationIcon && (
|
|
128
|
+
<div
|
|
129
|
+
className="shrink-0 flex items-center justify-center"
|
|
130
|
+
style={{
|
|
131
|
+
width: AppBarTokens.iconButtonTouchTarget,
|
|
132
|
+
height: AppBarTokens.iconButtonTouchTarget,
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
{navigationIcon}
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
<m.div
|
|
140
|
+
layoutId={shouldReduceMotion ? undefined : searchBarId}
|
|
141
|
+
role="search"
|
|
142
|
+
aria-label={searchPlaceholder}
|
|
143
|
+
aria-expanded={isSearchOpen}
|
|
144
|
+
tabIndex={0}
|
|
145
|
+
className="relative flex flex-1 items-center gap-2 rounded-full cursor-text h-10 px-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
146
|
+
onClick={handleSearchClick}
|
|
147
|
+
onKeyDown={handleKeyDown}
|
|
148
|
+
>
|
|
149
|
+
<div className="absolute inset-0 rounded-full bg-m3-surface-container-high -z-10" />
|
|
150
|
+
<span
|
|
151
|
+
className="shrink-0"
|
|
152
|
+
style={{ color: APP_BAR_COLORS.searchBarContent }}
|
|
153
|
+
>
|
|
154
|
+
{leadingSearchIcon ?? <SearchIcon />}
|
|
155
|
+
</span>
|
|
156
|
+
|
|
157
|
+
<span
|
|
158
|
+
className="flex-1 text-[16px] leading-6 truncate select-none"
|
|
159
|
+
style={{ color: APP_BAR_COLORS.searchBarContent }}
|
|
160
|
+
>
|
|
161
|
+
{searchValue ?? searchPlaceholder}
|
|
162
|
+
</span>
|
|
163
|
+
|
|
164
|
+
{trailingSearchActions && (
|
|
165
|
+
<div className="flex items-center shrink-0">
|
|
166
|
+
{trailingSearchActions}
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
</m.div>
|
|
170
|
+
|
|
171
|
+
{externalActions && (
|
|
172
|
+
<div className="flex items-center shrink-0">{externalActions}</div>
|
|
173
|
+
)}
|
|
174
|
+
</m.header>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file search-view.tsx
|
|
3
|
+
* MD3 Expressive Search View.
|
|
4
|
+
*
|
|
5
|
+
* Full-screen overlay activated when a SearchAppBar's search bar is clicked.
|
|
6
|
+
* Shares a Framer Motion `layoutId` with the SearchAppBar search bar for a
|
|
7
|
+
* smooth shared element transition.
|
|
8
|
+
*
|
|
9
|
+
* Usage pattern:
|
|
10
|
+
* ```tsx
|
|
11
|
+
* const [open, setOpen] = useState(false);
|
|
12
|
+
*
|
|
13
|
+
* // In render:
|
|
14
|
+
* <SearchAppBar searchBarId="main" onSearchFocus={() => setOpen(true)} />
|
|
15
|
+
* <AnimatePresence>
|
|
16
|
+
* {open && <SearchView searchBarId="main" onClose={() => setOpen(false)} />}
|
|
17
|
+
* </AnimatePresence>
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Design notes:
|
|
21
|
+
* - The SearchView is intentionally separate from SearchAppBar to allow consumers
|
|
22
|
+
* to customize the results/suggestions content without coupling.
|
|
23
|
+
* - The `searchBarId` prop must match between SearchAppBar and SearchView to
|
|
24
|
+
* enable the shared element transition.
|
|
25
|
+
* - Focus is moved to the search input when the view opens.
|
|
26
|
+
* - Escape key closes the view and returns focus to the search bar.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { AnimatePresence, m, useReducedMotion } from "motion/react";
|
|
30
|
+
import * as React from "react";
|
|
31
|
+
import { cn } from "../../lib/utils";
|
|
32
|
+
import {
|
|
33
|
+
APP_BAR_COLORS,
|
|
34
|
+
AppBarTokens,
|
|
35
|
+
SEARCH_VIEW_SPRING,
|
|
36
|
+
} from "./app-bar.tokens";
|
|
37
|
+
import type { SearchViewProps } from "./app-bar.types";
|
|
38
|
+
|
|
39
|
+
/** Built-in back arrow icon. */
|
|
40
|
+
function ArrowBackIcon() {
|
|
41
|
+
return (
|
|
42
|
+
<span
|
|
43
|
+
className="material-symbols-rounded text-[24px] leading-none select-none"
|
|
44
|
+
aria-hidden="true"
|
|
45
|
+
>
|
|
46
|
+
arrow_back
|
|
47
|
+
</span>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Built-in clear icon. */
|
|
52
|
+
function CloseIcon() {
|
|
53
|
+
return (
|
|
54
|
+
<span
|
|
55
|
+
className="material-symbols-rounded text-[24px] leading-none select-none"
|
|
56
|
+
aria-hidden="true"
|
|
57
|
+
>
|
|
58
|
+
close
|
|
59
|
+
</span>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* MD3 Expressive Search View.
|
|
65
|
+
*
|
|
66
|
+
* Renders a full-screen search overlay with a shared element transition
|
|
67
|
+
* from the triggering `<SearchAppBar>` search bar.
|
|
68
|
+
*
|
|
69
|
+
* Mount/unmount this component via `<AnimatePresence>` in the consumer.
|
|
70
|
+
*/
|
|
71
|
+
export function SearchView({
|
|
72
|
+
searchBarId = "search-bar",
|
|
73
|
+
value = "",
|
|
74
|
+
onChange,
|
|
75
|
+
onClose,
|
|
76
|
+
placeholder = "Search",
|
|
77
|
+
children,
|
|
78
|
+
leadingIcon,
|
|
79
|
+
trailingAction,
|
|
80
|
+
className,
|
|
81
|
+
}: SearchViewProps) {
|
|
82
|
+
const shouldReduceMotion = useReducedMotion();
|
|
83
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
84
|
+
|
|
85
|
+
// Move focus to the search input when the view opens
|
|
86
|
+
React.useEffect(() => {
|
|
87
|
+
const timer = window.setTimeout(() => {
|
|
88
|
+
inputRef.current?.focus();
|
|
89
|
+
}, 50);
|
|
90
|
+
return () => window.clearTimeout(timer);
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
// Close on Escape key
|
|
94
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
95
|
+
if (e.key === "Escape") {
|
|
96
|
+
onClose();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const viewTransition = shouldReduceMotion
|
|
101
|
+
? { duration: 0 }
|
|
102
|
+
: SEARCH_VIEW_SPRING;
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<m.div
|
|
106
|
+
role="dialog"
|
|
107
|
+
aria-modal="true"
|
|
108
|
+
aria-label="Search"
|
|
109
|
+
className={cn(
|
|
110
|
+
"fixed inset-0 z-60 flex flex-col bg-m3-surface",
|
|
111
|
+
className,
|
|
112
|
+
)}
|
|
113
|
+
initial={shouldReduceMotion ? {} : { opacity: 0 }}
|
|
114
|
+
animate={{ opacity: 1 }}
|
|
115
|
+
exit={shouldReduceMotion ? {} : { opacity: 0 }}
|
|
116
|
+
transition={viewTransition}
|
|
117
|
+
onKeyDown={handleKeyDown}
|
|
118
|
+
>
|
|
119
|
+
{/* Search Bar — matches layoutId of SearchAppBar search bar */}
|
|
120
|
+
<m.div
|
|
121
|
+
layoutId={shouldReduceMotion ? undefined : searchBarId}
|
|
122
|
+
className="flex items-center gap-2 px-4 shrink-0 bg-m3-surface"
|
|
123
|
+
style={{
|
|
124
|
+
height: AppBarTokens.heights.small,
|
|
125
|
+
}}
|
|
126
|
+
>
|
|
127
|
+
{/* Leading: back button or custom icon */}
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
className={cn(
|
|
131
|
+
"shrink-0 flex items-center justify-center rounded-full",
|
|
132
|
+
"focus-visible:outline-none focus-visible:ring-2",
|
|
133
|
+
)}
|
|
134
|
+
style={{
|
|
135
|
+
width: AppBarTokens.iconButtonTouchTarget,
|
|
136
|
+
height: AppBarTokens.iconButtonTouchTarget,
|
|
137
|
+
color: APP_BAR_COLORS.navigationIcon,
|
|
138
|
+
}}
|
|
139
|
+
aria-label="Close search"
|
|
140
|
+
onClick={onClose}
|
|
141
|
+
>
|
|
142
|
+
{leadingIcon ?? <ArrowBackIcon />}
|
|
143
|
+
</button>
|
|
144
|
+
|
|
145
|
+
{/* Search input */}
|
|
146
|
+
<div
|
|
147
|
+
className="relative flex flex-1 items-center rounded-full px-4 gap-2"
|
|
148
|
+
style={{
|
|
149
|
+
height: 40,
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
152
|
+
{/* Background Layer */}
|
|
153
|
+
<div className="absolute inset-0 rounded-full bg-m3-surface-container-high -z-10" />
|
|
154
|
+
<input
|
|
155
|
+
ref={inputRef}
|
|
156
|
+
aria-label={placeholder}
|
|
157
|
+
type="search"
|
|
158
|
+
value={value}
|
|
159
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
160
|
+
placeholder={placeholder}
|
|
161
|
+
className={cn(
|
|
162
|
+
"flex-1 bg-transparent border-none outline-none",
|
|
163
|
+
"text-[16px] leading-6",
|
|
164
|
+
"placeholder:text-m3-on-surface-variant",
|
|
165
|
+
)}
|
|
166
|
+
style={{ color: APP_BAR_COLORS.title }}
|
|
167
|
+
/>
|
|
168
|
+
|
|
169
|
+
{/* Clear button — show when there's a value */}
|
|
170
|
+
{value && (
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
className={cn(
|
|
174
|
+
"shrink-0 flex items-center justify-center rounded-full",
|
|
175
|
+
"focus-visible:outline-none focus-visible:ring-2",
|
|
176
|
+
)}
|
|
177
|
+
style={{
|
|
178
|
+
width: 40,
|
|
179
|
+
height: 40,
|
|
180
|
+
color: APP_BAR_COLORS.searchBarContent,
|
|
181
|
+
}}
|
|
182
|
+
aria-label="Clear search"
|
|
183
|
+
onClick={() => onChange?.("")}
|
|
184
|
+
>
|
|
185
|
+
{trailingAction ?? <CloseIcon />}
|
|
186
|
+
</button>
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
</m.div>
|
|
190
|
+
|
|
191
|
+
{/* Results / suggestions area */}
|
|
192
|
+
{children && (
|
|
193
|
+
<m.div
|
|
194
|
+
className="flex-1 overflow-y-auto"
|
|
195
|
+
initial={shouldReduceMotion ? {} : { opacity: 0, y: -8 }}
|
|
196
|
+
animate={{ opacity: 1, y: 0 }}
|
|
197
|
+
transition={{ delay: 0.1, duration: 0.15 }}
|
|
198
|
+
>
|
|
199
|
+
{children}
|
|
200
|
+
</m.div>
|
|
201
|
+
)}
|
|
202
|
+
</m.div>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Convenience wrapper that handles the AnimatePresence + open state.
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```tsx
|
|
211
|
+
* <SearchBar
|
|
212
|
+
* isOpen={searchOpen}
|
|
213
|
+
* onClose={() => setSearchOpen(false)}
|
|
214
|
+
* searchBarId="main-search"
|
|
215
|
+
* >
|
|
216
|
+
* <SearchResultsList results={results} />
|
|
217
|
+
* </SearchBar>
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
export function SearchViewContainer({
|
|
221
|
+
isOpen,
|
|
222
|
+
...props
|
|
223
|
+
}: SearchViewProps & { isOpen: boolean }) {
|
|
224
|
+
return (
|
|
225
|
+
<AnimatePresence>{isOpen && <SearchView {...props} />}</AnimatePresence>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { SmallAppBar } from "./small-app-bar";
|
|
4
|
+
|
|
5
|
+
describe("SmallAppBar", () => {
|
|
6
|
+
it("renders correctly with basic props", () => {
|
|
7
|
+
render(<SmallAppBar title="Inbox" />);
|
|
8
|
+
|
|
9
|
+
const header = screen.getByRole("banner");
|
|
10
|
+
expect(header).toBeInTheDocument();
|
|
11
|
+
|
|
12
|
+
const titleElement = screen.getByText("Inbox");
|
|
13
|
+
expect(titleElement).toBeInTheDocument();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("renders subtitle, actions, and navigationIcon", () => {
|
|
17
|
+
render(
|
|
18
|
+
<SmallAppBar
|
|
19
|
+
title="Profile"
|
|
20
|
+
subtitle="@username"
|
|
21
|
+
navigationIcon={
|
|
22
|
+
<button type="button" data-testid="nav-icon">
|
|
23
|
+
Back
|
|
24
|
+
</button>
|
|
25
|
+
}
|
|
26
|
+
actions={
|
|
27
|
+
<button type="button" data-testid="actions-btn">
|
|
28
|
+
Search
|
|
29
|
+
</button>
|
|
30
|
+
}
|
|
31
|
+
/>,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(screen.getByText("Profile")).toBeInTheDocument();
|
|
35
|
+
expect(screen.getByText("@username")).toBeInTheDocument();
|
|
36
|
+
expect(screen.getByTestId("nav-icon")).toBeInTheDocument();
|
|
37
|
+
expect(screen.getByTestId("actions-btn")).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("applies custom classNames", () => {
|
|
41
|
+
render(<SmallAppBar title="Custom" className="test-small-bar" />);
|
|
42
|
+
|
|
43
|
+
// Container has the class
|
|
44
|
+
const _container = screen.getByText("Custom").closest("div");
|
|
45
|
+
// Actually banner is inside or might be the returned container
|
|
46
|
+
expect(document.querySelector(".test-small-bar")).toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file small-app-bar.tsx
|
|
3
|
+
* MD3 Expressive Small App Bar.
|
|
4
|
+
*
|
|
5
|
+
* Single-row layout: [navigationIcon][title + subtitle][actions]
|
|
6
|
+
* Height: 64px | Title: TitleLarge (22sp) | Subtitle: LabelMedium (12sp)
|
|
7
|
+
*
|
|
8
|
+
* Scroll behaviors:
|
|
9
|
+
* - pinned: changes background color surface → surface-container
|
|
10
|
+
* - enterAlways: slides up when scrolling down, slides down when scrolling up
|
|
11
|
+
*
|
|
12
|
+
* @see docs/m3/app-bars/AppBarSmallTokens.kt
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { AnimatePresence, m, useReducedMotion } from "motion/react";
|
|
16
|
+
import { cn } from "../../lib/utils";
|
|
17
|
+
import {
|
|
18
|
+
APP_BAR_COLOR_TRANSITION,
|
|
19
|
+
APP_BAR_COLORS,
|
|
20
|
+
APP_BAR_ENTER_ALWAYS_SPRING,
|
|
21
|
+
AppBarTokens,
|
|
22
|
+
appBarTypography,
|
|
23
|
+
} from "./app-bar.tokens";
|
|
24
|
+
import type { SmallAppBarProps } from "./app-bar.types";
|
|
25
|
+
import { useAppBarScroll } from "./hooks/use-app-bar-scroll";
|
|
26
|
+
|
|
27
|
+
interface SmallAppBarInnerProps
|
|
28
|
+
extends Pick<
|
|
29
|
+
SmallAppBarProps,
|
|
30
|
+
| "title"
|
|
31
|
+
| "subtitle"
|
|
32
|
+
| "titleAlignment"
|
|
33
|
+
| "navigationIcon"
|
|
34
|
+
| "actions"
|
|
35
|
+
| "colors"
|
|
36
|
+
| "className"
|
|
37
|
+
> {
|
|
38
|
+
currentBg: string;
|
|
39
|
+
cssTransition: string | undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Inner content shared between pinned and enterAlways variants. */
|
|
43
|
+
function SmallAppBarInner({
|
|
44
|
+
title,
|
|
45
|
+
subtitle,
|
|
46
|
+
titleAlignment = "start",
|
|
47
|
+
navigationIcon,
|
|
48
|
+
actions,
|
|
49
|
+
colors,
|
|
50
|
+
className,
|
|
51
|
+
currentBg,
|
|
52
|
+
cssTransition,
|
|
53
|
+
}: SmallAppBarInnerProps) {
|
|
54
|
+
const isCentered = titleAlignment === "center";
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<m.header
|
|
58
|
+
role="banner"
|
|
59
|
+
className={cn("flex items-center px-1 h-full w-full", className)}
|
|
60
|
+
style={{ backgroundColor: currentBg, transition: cssTransition }}
|
|
61
|
+
>
|
|
62
|
+
{navigationIcon && (
|
|
63
|
+
<div
|
|
64
|
+
className="shrink-0 flex items-center justify-center"
|
|
65
|
+
style={{
|
|
66
|
+
width: AppBarTokens.iconButtonTouchTarget,
|
|
67
|
+
height: AppBarTokens.iconButtonTouchTarget,
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
{navigationIcon}
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
|
|
74
|
+
<div
|
|
75
|
+
className={cn(
|
|
76
|
+
"flex-1 flex flex-col justify-center min-w-0",
|
|
77
|
+
isCentered ? "items-center" : "items-start",
|
|
78
|
+
!navigationIcon && "pl-4",
|
|
79
|
+
)}
|
|
80
|
+
>
|
|
81
|
+
<span
|
|
82
|
+
className={cn(
|
|
83
|
+
appBarTypography.titleLarge,
|
|
84
|
+
"truncate w-full",
|
|
85
|
+
isCentered && "text-center",
|
|
86
|
+
)}
|
|
87
|
+
style={{ color: colors?.titleColor ?? APP_BAR_COLORS.title }}
|
|
88
|
+
>
|
|
89
|
+
{title}
|
|
90
|
+
</span>
|
|
91
|
+
{subtitle && (
|
|
92
|
+
<span
|
|
93
|
+
className={cn(
|
|
94
|
+
appBarTypography.labelMedium,
|
|
95
|
+
"truncate w-full",
|
|
96
|
+
isCentered && "text-center",
|
|
97
|
+
)}
|
|
98
|
+
style={{ color: colors?.subtitleColor ?? APP_BAR_COLORS.subtitle }}
|
|
99
|
+
>
|
|
100
|
+
{subtitle}
|
|
101
|
+
</span>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{actions && <div className="flex items-center shrink-0">{actions}</div>}
|
|
106
|
+
</m.header>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* MD3 Expressive Small App Bar.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```tsx
|
|
115
|
+
* // Left-aligned (default)
|
|
116
|
+
* <SmallAppBar
|
|
117
|
+
* title="Inbox"
|
|
118
|
+
* navigationIcon={<IconButton aria-label="Go back"><Icon>arrow_back</Icon></IconButton>}
|
|
119
|
+
* actions={<IconButton aria-label="Search"><Icon>search</Icon></IconButton>}
|
|
120
|
+
* scrollBehavior="pinned"
|
|
121
|
+
* />
|
|
122
|
+
*
|
|
123
|
+
* // Center-aligned with subtitle
|
|
124
|
+
* <SmallAppBar
|
|
125
|
+
* title="Profile"
|
|
126
|
+
* subtitle="@username"
|
|
127
|
+
* titleAlignment="center"
|
|
128
|
+
* scrollBehavior="enterAlways"
|
|
129
|
+
* />
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export function SmallAppBar({
|
|
133
|
+
title,
|
|
134
|
+
subtitle,
|
|
135
|
+
titleAlignment = "start",
|
|
136
|
+
navigationIcon,
|
|
137
|
+
actions,
|
|
138
|
+
colors,
|
|
139
|
+
scrollBehavior = "pinned",
|
|
140
|
+
scrollElement,
|
|
141
|
+
className,
|
|
142
|
+
}: SmallAppBarProps) {
|
|
143
|
+
const shouldReduceMotion = useReducedMotion();
|
|
144
|
+
|
|
145
|
+
const { isScrolled, isHidden } = useAppBarScroll({
|
|
146
|
+
scrollElement,
|
|
147
|
+
behavior:
|
|
148
|
+
scrollBehavior === "exitUntilCollapsed" ? "pinned" : scrollBehavior,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const containerBg = colors?.containerColor ?? APP_BAR_COLORS.container;
|
|
152
|
+
const scrolledBg =
|
|
153
|
+
colors?.scrolledContainerColor ?? APP_BAR_COLORS.scrolledContainer;
|
|
154
|
+
const currentBg = isScrolled ? scrolledBg : containerBg;
|
|
155
|
+
|
|
156
|
+
const cssTransition = shouldReduceMotion
|
|
157
|
+
? undefined
|
|
158
|
+
: `background-color ${APP_BAR_COLOR_TRANSITION.duration}s cubic-bezier(${APP_BAR_COLOR_TRANSITION.ease.join(",")})`;
|
|
159
|
+
|
|
160
|
+
const innerProps: SmallAppBarInnerProps = {
|
|
161
|
+
title,
|
|
162
|
+
subtitle,
|
|
163
|
+
titleAlignment,
|
|
164
|
+
navigationIcon,
|
|
165
|
+
actions,
|
|
166
|
+
colors,
|
|
167
|
+
currentBg,
|
|
168
|
+
cssTransition,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const barHeight = AppBarTokens.heights.small;
|
|
172
|
+
|
|
173
|
+
if (scrollBehavior !== "enterAlways") {
|
|
174
|
+
return (
|
|
175
|
+
<div
|
|
176
|
+
className={cn("fixed top-0 inset-x-0 z-50", className)}
|
|
177
|
+
style={{ height: barHeight }}
|
|
178
|
+
>
|
|
179
|
+
<SmallAppBarInner {...innerProps} />
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<AnimatePresence initial={false}>
|
|
186
|
+
{!isHidden && (
|
|
187
|
+
<m.div
|
|
188
|
+
key="small-app-bar"
|
|
189
|
+
initial={{ y: "-100%" }}
|
|
190
|
+
animate={{ y: 0 }}
|
|
191
|
+
exit={{ y: "-100%" }}
|
|
192
|
+
transition={
|
|
193
|
+
shouldReduceMotion ? { duration: 0 } : APP_BAR_ENTER_ALWAYS_SPRING
|
|
194
|
+
}
|
|
195
|
+
className={cn("fixed top-0 inset-x-0 z-50", className)}
|
|
196
|
+
style={{ height: barHeight }}
|
|
197
|
+
>
|
|
198
|
+
<SmallAppBarInner {...innerProps} />
|
|
199
|
+
</m.div>
|
|
200
|
+
)}
|
|
201
|
+
</AnimatePresence>
|
|
202
|
+
);
|
|
203
|
+
}
|