@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,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file app-bar.types.ts
|
|
3
|
+
* MD3 Expressive App Bar — TypeScript prop definitions.
|
|
4
|
+
* Spec: https://m3.material.io/components/app-bars/overview
|
|
5
|
+
* Reference: docs/m3/app-bars/AppBar.kt (MD3 Expressive, May 2025)
|
|
6
|
+
*
|
|
7
|
+
* Note: Component name is "App Bar" (not "Top App Bar") per MD3 Expressive May 2025 rename.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type * as React from "react";
|
|
11
|
+
|
|
12
|
+
// ─── Scroll Behavior ─────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Controls how the App Bar responds to scroll events.
|
|
16
|
+
*
|
|
17
|
+
* - `pinned`: Always visible. Changes background color when scrolled.
|
|
18
|
+
* - `enterAlways`: Hides when scrolling down, reveals when scrolling up.
|
|
19
|
+
* - `exitUntilCollapsed`: Collapses from expandedHeight → collapsedHeight as user scrolls.
|
|
20
|
+
*/
|
|
21
|
+
export type AppBarScrollBehavior =
|
|
22
|
+
| "pinned"
|
|
23
|
+
| "enterAlways"
|
|
24
|
+
| "exitUntilCollapsed";
|
|
25
|
+
|
|
26
|
+
// ─── Title Alignment ─────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/** Horizontal alignment of the title (and subtitle) text. */
|
|
29
|
+
export type TitleAlignment = "start" | "center";
|
|
30
|
+
|
|
31
|
+
// ─── Color Overrides ─────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Optional color overrides for the App Bar.
|
|
35
|
+
* All values should be CSS color strings (var(--md-sys-color-*) or hex).
|
|
36
|
+
*
|
|
37
|
+
* By default, App Bars use MD3 system color tokens automatically.
|
|
38
|
+
*/
|
|
39
|
+
export interface AppBarColors {
|
|
40
|
+
/** Background when not scrolled. Default: `var(--md-sys-color-surface)` */
|
|
41
|
+
containerColor?: string;
|
|
42
|
+
/** Background when content has scrolled past top. Default: `var(--md-sys-color-surface-container)` */
|
|
43
|
+
scrolledContainerColor?: string;
|
|
44
|
+
/** Title text color. Default: `var(--md-sys-color-on-surface)` */
|
|
45
|
+
titleColor?: string;
|
|
46
|
+
/** Subtitle text color. Default: `var(--md-sys-color-on-surface-variant)` */
|
|
47
|
+
subtitleColor?: string;
|
|
48
|
+
/** Navigation icon color. Default: `var(--md-sys-color-on-surface)` */
|
|
49
|
+
navigationIconColor?: string;
|
|
50
|
+
/** Action icon color. Default: `var(--md-sys-color-on-surface-variant)` */
|
|
51
|
+
actionIconColor?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── Base Props ──────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Shared props for all top App Bar variants.
|
|
58
|
+
*/
|
|
59
|
+
export interface BaseAppBarProps {
|
|
60
|
+
/**
|
|
61
|
+
* Navigation icon slot (typically IconButton with back arrow or hamburger).
|
|
62
|
+
* Should have `aria-label` for accessibility.
|
|
63
|
+
*/
|
|
64
|
+
navigationIcon?: React.ReactNode;
|
|
65
|
+
/**
|
|
66
|
+
* Action icon slots rendered at the trailing end.
|
|
67
|
+
* Typically `<IconButton>` components. Use `<AppBarRow>` for overflow support.
|
|
68
|
+
*/
|
|
69
|
+
actions?: React.ReactNode;
|
|
70
|
+
/** Optional color overrides. Defaults use MD3 system color tokens. */
|
|
71
|
+
colors?: AppBarColors;
|
|
72
|
+
/**
|
|
73
|
+
* Scroll behavior that controls how the App Bar reacts to scroll events.
|
|
74
|
+
* @default "pinned"
|
|
75
|
+
*/
|
|
76
|
+
scrollBehavior?: AppBarScrollBehavior;
|
|
77
|
+
/**
|
|
78
|
+
* Ref to the scrollable element to observe.
|
|
79
|
+
* If not provided, listens to `window` scroll events.
|
|
80
|
+
*/
|
|
81
|
+
scrollElement?: React.RefObject<HTMLElement | null>;
|
|
82
|
+
/** Additional CSS class applied to the root header element. */
|
|
83
|
+
className?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── Small App Bar ────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Props for `<SmallAppBar>`.
|
|
90
|
+
*
|
|
91
|
+
* Single-row layout with navigation icon, title, and action buttons.
|
|
92
|
+
* Height: 64px. Title font: TitleLarge (22sp).
|
|
93
|
+
*/
|
|
94
|
+
export interface SmallAppBarProps extends BaseAppBarProps {
|
|
95
|
+
/** The main title content. */
|
|
96
|
+
title: React.ReactNode;
|
|
97
|
+
/**
|
|
98
|
+
* Optional subtitle displayed below the title.
|
|
99
|
+
* Font: LabelMedium (12sp).
|
|
100
|
+
*/
|
|
101
|
+
subtitle?: React.ReactNode;
|
|
102
|
+
/**
|
|
103
|
+
* Horizontal alignment of the title and subtitle.
|
|
104
|
+
* When `center`: title is centered, nav icon and actions balance both sides.
|
|
105
|
+
* @default "start"
|
|
106
|
+
*/
|
|
107
|
+
titleAlignment?: TitleAlignment;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── Flexible App Bars ────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Props for `<MediumFlexibleAppBar>` and `<LargeFlexibleAppBar>`.
|
|
114
|
+
*
|
|
115
|
+
* Two-row layout in expanded state (nav row + title row).
|
|
116
|
+
* Collapses to single row when scrolled.
|
|
117
|
+
* Only supports `exitUntilCollapsed` scroll behavior.
|
|
118
|
+
*/
|
|
119
|
+
export interface FlexibleAppBarProps extends BaseAppBarProps {
|
|
120
|
+
/** The main title content (rendered in both expanded and collapsed states). */
|
|
121
|
+
title: React.ReactNode;
|
|
122
|
+
/**
|
|
123
|
+
* Optional subtitle displayed in the expanded title row.
|
|
124
|
+
* Affects expanded height (adds extra rows).
|
|
125
|
+
*/
|
|
126
|
+
subtitle?: React.ReactNode;
|
|
127
|
+
/**
|
|
128
|
+
* Horizontal alignment of the title and subtitle.
|
|
129
|
+
* @default "start"
|
|
130
|
+
*/
|
|
131
|
+
titleAlignment?: TitleAlignment;
|
|
132
|
+
/**
|
|
133
|
+
* Collapsed height in px. The App Bar settles to this height after full scroll.
|
|
134
|
+
* @default 64
|
|
135
|
+
*/
|
|
136
|
+
collapsedHeight?: number;
|
|
137
|
+
/**
|
|
138
|
+
* Expanded (initial) height in px.
|
|
139
|
+
* Defaults depend on variant and subtitle presence.
|
|
140
|
+
*/
|
|
141
|
+
expandedHeight?: number;
|
|
142
|
+
/**
|
|
143
|
+
* Additional content rendered in the expanded title area.
|
|
144
|
+
* Supports images, circular avatars (32dp), Filled Buttons, or custom elements.
|
|
145
|
+
* Hidden when collapsed.
|
|
146
|
+
*/
|
|
147
|
+
headerContent?: React.ReactNode;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ─── Search App Bar ────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
/** Visual variant of the search bar. */
|
|
153
|
+
export type SearchBarVariant = "filled" | "outlined";
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Props for `<SearchAppBar>`.
|
|
157
|
+
*
|
|
158
|
+
* Replaces the title with a pill-shaped search input bar.
|
|
159
|
+
* New variant in MD3 Expressive (May 2025).
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```tsx
|
|
163
|
+
* <SearchAppBar
|
|
164
|
+
* searchPlaceholder="Search..."
|
|
165
|
+
* onSearchFocus={() => setSearchViewOpen(true)}
|
|
166
|
+
* trailingSearchActions={<IconButton aria-label="Voice search">...</IconButton>}
|
|
167
|
+
* externalActions={<Avatar />}
|
|
168
|
+
* />
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
export interface SearchAppBarProps extends BaseAppBarProps {
|
|
172
|
+
/** Placeholder text in the search bar. @default "Search" */
|
|
173
|
+
searchPlaceholder?: string;
|
|
174
|
+
/** Controlled search value. */
|
|
175
|
+
searchValue?: string;
|
|
176
|
+
/** Called when search input changes (inside the SearchView). */
|
|
177
|
+
onSearchChange?: (value: string) => void;
|
|
178
|
+
/**
|
|
179
|
+
* Called when the search bar is focused/clicked.
|
|
180
|
+
* Consumers should open a `<SearchView>` in response.
|
|
181
|
+
*/
|
|
182
|
+
onSearchFocus?: () => void;
|
|
183
|
+
/** Called when the search view is closed/blurred. */
|
|
184
|
+
onSearchBlur?: () => void;
|
|
185
|
+
/**
|
|
186
|
+
* Icon rendered at the leading edge of the search bar (inside the pill).
|
|
187
|
+
* Defaults to a search icon.
|
|
188
|
+
*/
|
|
189
|
+
leadingSearchIcon?: React.ReactNode;
|
|
190
|
+
/**
|
|
191
|
+
* Action icons rendered at the trailing edge of the search bar (inside the pill).
|
|
192
|
+
* Common examples: mic, camera, QR code.
|
|
193
|
+
*/
|
|
194
|
+
trailingSearchActions?: React.ReactNode;
|
|
195
|
+
/**
|
|
196
|
+
* Action icons rendered outside the search bar (at the trailing edge of the bar).
|
|
197
|
+
* Common example: profile avatar button.
|
|
198
|
+
*/
|
|
199
|
+
externalActions?: React.ReactNode;
|
|
200
|
+
/**
|
|
201
|
+
* Horizontal alignment of the search bar.
|
|
202
|
+
* @default "start"
|
|
203
|
+
*/
|
|
204
|
+
titleAlignment?: TitleAlignment;
|
|
205
|
+
/**
|
|
206
|
+
* Visual style of the search bar.
|
|
207
|
+
* @default "filled"
|
|
208
|
+
*/
|
|
209
|
+
searchBarVariant?: SearchBarVariant;
|
|
210
|
+
/**
|
|
211
|
+
* Unique identifier for the search bar — used as the Framer Motion `layoutId`
|
|
212
|
+
* to enable shared element transitions with `<SearchView>`.
|
|
213
|
+
* Must be unique if multiple SearchAppBars are on the page.
|
|
214
|
+
* @default "search-bar"
|
|
215
|
+
*/
|
|
216
|
+
searchBarId?: string;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ─── Bottom App Bar ───────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Props for `<BottomAppBar>`.
|
|
223
|
+
*
|
|
224
|
+
* Fixed to the bottom of the screen. Contains navigation actions and optional FAB.
|
|
225
|
+
* Height: 80px. Background: `surface-container`. Elevation: Level2.
|
|
226
|
+
*/
|
|
227
|
+
export interface BottomAppBarProps {
|
|
228
|
+
/**
|
|
229
|
+
* Action icon buttons rendered at the leading end.
|
|
230
|
+
* Typically 3-4 `<IconButton>` components.
|
|
231
|
+
*/
|
|
232
|
+
actions?: React.ReactNode;
|
|
233
|
+
/**
|
|
234
|
+
* Floating Action Button rendered at the trailing end.
|
|
235
|
+
* Consumer provides a `<FAB>` or `<FABPosition>` component.
|
|
236
|
+
*/
|
|
237
|
+
floatingActionButton?: React.ReactNode;
|
|
238
|
+
/**
|
|
239
|
+
* Scroll behavior.
|
|
240
|
+
* - `visible`: Always visible.
|
|
241
|
+
* - `hidden`: Slides down when scrolling down, appears when scrolling up.
|
|
242
|
+
* @default "visible"
|
|
243
|
+
*/
|
|
244
|
+
scrollBehavior?: "visible" | "hidden";
|
|
245
|
+
/**
|
|
246
|
+
* Ref to the scrollable element to observe.
|
|
247
|
+
* If not provided, listens to `window` scroll events.
|
|
248
|
+
*/
|
|
249
|
+
scrollElement?: React.RefObject<HTMLElement | null>;
|
|
250
|
+
/** Additional CSS class applied to the root nav element. */
|
|
251
|
+
className?: string;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ─── Docked Toolbar ────────────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Props for `<DockedToolbar>`.
|
|
258
|
+
*
|
|
259
|
+
* Secondary navigation bar. Not an App Bar — typically appears below the main App Bar.
|
|
260
|
+
* Height: 64px. Background: `surface-container`.
|
|
261
|
+
* Contains chips, segmented buttons, or filter actions.
|
|
262
|
+
*/
|
|
263
|
+
export interface DockedToolbarProps {
|
|
264
|
+
/** Toolbar content (chips, segmented buttons, filter actions, etc.) */
|
|
265
|
+
children: React.ReactNode;
|
|
266
|
+
/**
|
|
267
|
+
* Accessible label for the toolbar (required for accessibility).
|
|
268
|
+
* e.g., "Filter options" or "Content navigation"
|
|
269
|
+
*/
|
|
270
|
+
"aria-label": string;
|
|
271
|
+
/** Additional CSS class applied to the root element. */
|
|
272
|
+
className?: string;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ─── App Bar DSL ──────────────────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
/** Type of item in an App Bar Row/Column. */
|
|
278
|
+
export type AppBarItemType = "clickable" | "toggleable" | "custom";
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* State passed to custom overflow menu content renderers.
|
|
282
|
+
*/
|
|
283
|
+
export interface AppBarMenuState {
|
|
284
|
+
isOpen: boolean;
|
|
285
|
+
open: () => void;
|
|
286
|
+
close: () => void;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Defines a single action item for use in `<AppBarRow>` or `<AppBarColumn>`.
|
|
291
|
+
* Translated from `AppBarItem` sealed interface in Kotlin DSL.
|
|
292
|
+
*/
|
|
293
|
+
export interface AppBarItem {
|
|
294
|
+
/** Type determines which props are required. */
|
|
295
|
+
type: AppBarItemType;
|
|
296
|
+
/** Icon displayed in the App Bar (always shown). */
|
|
297
|
+
icon: React.ReactNode;
|
|
298
|
+
/** Accessible label for the icon button. Also shown in overflow menu. */
|
|
299
|
+
label: string;
|
|
300
|
+
/** Whether the item is interactive. @default true */
|
|
301
|
+
enabled?: boolean;
|
|
302
|
+
// Clickable item
|
|
303
|
+
/** Called when the item is pressed (required for `type: "clickable"`). */
|
|
304
|
+
onClick?: () => void;
|
|
305
|
+
// Toggleable item
|
|
306
|
+
/** Current checked state (for `type: "toggleable"`). */
|
|
307
|
+
checked?: boolean;
|
|
308
|
+
/** Called when checked state changes (for `type: "toggleable"`). */
|
|
309
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
310
|
+
// Custom item
|
|
311
|
+
/** Custom content rendered in the App Bar (for `type: "custom"`). */
|
|
312
|
+
appBarContent?: React.ReactNode;
|
|
313
|
+
/** Custom content rendered in the overflow dropdown menu. */
|
|
314
|
+
menuContent?: (state: AppBarMenuState) => React.ReactNode;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Props for `<AppBarRow>`.
|
|
319
|
+
*
|
|
320
|
+
* Displays App Bar items in a horizontal row.
|
|
321
|
+
* Items that overflow the available width collapse into a dropdown menu.
|
|
322
|
+
*/
|
|
323
|
+
export interface AppBarRowProps {
|
|
324
|
+
/** App Bar action items to display. */
|
|
325
|
+
items: AppBarItem[];
|
|
326
|
+
/**
|
|
327
|
+
* Maximum number of items to display before collapsing to overflow.
|
|
328
|
+
* If not set, uses available container width to determine count.
|
|
329
|
+
*/
|
|
330
|
+
maxItemCount?: number;
|
|
331
|
+
/** Additional CSS class applied to the row container. */
|
|
332
|
+
className?: string;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Props for `<AppBarColumn>`.
|
|
337
|
+
*
|
|
338
|
+
* Displays App Bar items in a vertical column.
|
|
339
|
+
* Items that overflow collapse into a dropdown menu.
|
|
340
|
+
*/
|
|
341
|
+
export interface AppBarColumnProps {
|
|
342
|
+
/** App Bar action items to display. */
|
|
343
|
+
items: AppBarItem[];
|
|
344
|
+
/**
|
|
345
|
+
* Maximum number of items to display before collapsing to overflow.
|
|
346
|
+
*/
|
|
347
|
+
maxItemCount?: number;
|
|
348
|
+
/** Additional CSS class applied to the column container. */
|
|
349
|
+
className?: string;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Props for `<AppBarOverflowIndicator>`.
|
|
354
|
+
*
|
|
355
|
+
* Renders a "More" (MoreVert) icon button that opens a dropdown menu
|
|
356
|
+
* containing overflow App Bar items.
|
|
357
|
+
*/
|
|
358
|
+
export interface AppBarOverflowIndicatorProps {
|
|
359
|
+
/** Items that did not fit in the row/column and should appear in the dropdown. */
|
|
360
|
+
items: AppBarItem[];
|
|
361
|
+
/** Additional CSS class applied to the trigger button. */
|
|
362
|
+
className?: string;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ─── Search View ──────────────────────────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Props for `<SearchView>`.
|
|
369
|
+
*
|
|
370
|
+
* Full-screen overlay shown when the search bar is activated.
|
|
371
|
+
* Designed to share a Framer Motion `layoutId` with the `<SearchAppBar>`
|
|
372
|
+
* search bar for smooth shared element transitions.
|
|
373
|
+
*
|
|
374
|
+
* Integration with SearchAppBar:
|
|
375
|
+
* ```tsx
|
|
376
|
+
* const [isOpen, setIsOpen] = useState(false);
|
|
377
|
+
*
|
|
378
|
+
* <SearchAppBar
|
|
379
|
+
* searchBarId="main-search"
|
|
380
|
+
* onSearchFocus={() => setIsOpen(true)}
|
|
381
|
+
* />
|
|
382
|
+
* <AnimatePresence>
|
|
383
|
+
* {isOpen && (
|
|
384
|
+
* <SearchView
|
|
385
|
+
* searchBarId="main-search"
|
|
386
|
+
* onClose={() => setIsOpen(false)}
|
|
387
|
+
* />
|
|
388
|
+
* )}
|
|
389
|
+
* </AnimatePresence>
|
|
390
|
+
* ```
|
|
391
|
+
*/
|
|
392
|
+
export interface SearchViewProps {
|
|
393
|
+
/**
|
|
394
|
+
* Must match the `searchBarId` of the triggering `<SearchAppBar>`.
|
|
395
|
+
* Used as the Framer Motion `layoutId` for shared element transition.
|
|
396
|
+
* @default "search-bar"
|
|
397
|
+
*/
|
|
398
|
+
searchBarId?: string;
|
|
399
|
+
/** Current search value. */
|
|
400
|
+
value?: string;
|
|
401
|
+
/** Called when search value changes. */
|
|
402
|
+
onChange?: (value: string) => void;
|
|
403
|
+
/** Called when the view is closed (Escape key or back navigation). */
|
|
404
|
+
onClose: () => void;
|
|
405
|
+
/** Placeholder text in the search input. @default "Search" */
|
|
406
|
+
placeholder?: string;
|
|
407
|
+
/**
|
|
408
|
+
* Content rendered below the search input (suggestions, history, results).
|
|
409
|
+
*/
|
|
410
|
+
children?: React.ReactNode;
|
|
411
|
+
/** Navigation icon (back arrow) rendered at the leading edge. */
|
|
412
|
+
leadingIcon?: React.ReactNode;
|
|
413
|
+
/** Trailing action in the search input (e.g., clear button). */
|
|
414
|
+
trailingAction?: React.ReactNode;
|
|
415
|
+
/** Additional CSS class applied to the overlay container. */
|
|
416
|
+
className?: string;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ─── Internal Hook Return ─────────────────────────────────────────────────────
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Return type for `useAppBarScroll`.
|
|
423
|
+
* @internal
|
|
424
|
+
*/
|
|
425
|
+
export interface UseAppBarScrollReturn {
|
|
426
|
+
/**
|
|
427
|
+
* True when the scroll position is greater than 0.
|
|
428
|
+
* Used by `pinned` behavior to change background color.
|
|
429
|
+
*/
|
|
430
|
+
isScrolled: boolean;
|
|
431
|
+
/**
|
|
432
|
+
* Fraction from 0 (fully expanded) to 1 (fully collapsed).
|
|
433
|
+
* Used by `exitUntilCollapsed` to drive collapse animations.
|
|
434
|
+
*/
|
|
435
|
+
collapsedFraction: number;
|
|
436
|
+
/**
|
|
437
|
+
* True when the App Bar should be visually hidden.
|
|
438
|
+
* Only relevant for `enterAlways` behavior.
|
|
439
|
+
*/
|
|
440
|
+
isHidden: boolean;
|
|
441
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { BottomAppBar } from "./bottom-app-bar";
|
|
4
|
+
|
|
5
|
+
// Mock IntersectionObserver for useAppBarScroll
|
|
6
|
+
const mockIntersectionObserver = vi.fn();
|
|
7
|
+
mockIntersectionObserver.mockReturnValue({
|
|
8
|
+
observe: () => null,
|
|
9
|
+
unobserve: () => null,
|
|
10
|
+
disconnect: () => null,
|
|
11
|
+
});
|
|
12
|
+
window.IntersectionObserver =
|
|
13
|
+
mockIntersectionObserver as unknown as typeof IntersectionObserver;
|
|
14
|
+
|
|
15
|
+
describe("BottomAppBar", () => {
|
|
16
|
+
it("renders correctly with actions and FAB", () => {
|
|
17
|
+
render(
|
|
18
|
+
<BottomAppBar
|
|
19
|
+
actions={<div data-testid="actions">Actions Content</div>}
|
|
20
|
+
floatingActionButton={<div data-testid="fab">FAB Content</div>}
|
|
21
|
+
/>,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const nav = screen.getByRole("navigation", { name: /bottom app bar/i });
|
|
25
|
+
expect(nav).toBeInTheDocument();
|
|
26
|
+
expect(nav).toHaveClass(
|
|
27
|
+
"fixed bottom-0 inset-x-0 z-50 flex items-center bg-m3-surface-container",
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
expect(screen.getByTestId("actions")).toBeInTheDocument();
|
|
31
|
+
expect(screen.getByTestId("actions")).toHaveTextContent("Actions Content");
|
|
32
|
+
|
|
33
|
+
expect(screen.getByTestId("fab")).toBeInTheDocument();
|
|
34
|
+
expect(screen.getByTestId("fab")).toHaveTextContent("FAB Content");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("applies custom classNames", () => {
|
|
38
|
+
render(<BottomAppBar className="custom-bottom-action-bar" />);
|
|
39
|
+
const nav = screen.getByRole("navigation");
|
|
40
|
+
expect(nav).toHaveClass("custom-bottom-action-bar");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file bottom-app-bar.tsx
|
|
3
|
+
* MD3 Expressive Bottom App Bar.
|
|
4
|
+
*
|
|
5
|
+
* Fixed to bottom of screen. Contains action icons and optional FAB.
|
|
6
|
+
* Height: 80px | Background: surface-container | Elevation: Level2 (always)
|
|
7
|
+
*
|
|
8
|
+
* @see docs/m3/app-bars/BottomAppBarTokens.kt
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { m, useReducedMotion } from "motion/react";
|
|
12
|
+
import { cn } from "../../lib/utils";
|
|
13
|
+
import { APP_BAR_BOTTOM_SPRING, AppBarTokens } from "./app-bar.tokens";
|
|
14
|
+
import type { BottomAppBarProps } from "./app-bar.types";
|
|
15
|
+
import { useAppBarScroll } from "./hooks/use-app-bar-scroll";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* MD3 Expressive Bottom App Bar.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* // With FAB
|
|
23
|
+
* <BottomAppBar
|
|
24
|
+
* actions={
|
|
25
|
+
* <>
|
|
26
|
+
* <IconButton aria-label="Check"><Icon>check_box</Icon></IconButton>
|
|
27
|
+
* <IconButton aria-label="Brush"><Icon>brush</Icon></IconButton>
|
|
28
|
+
* </>
|
|
29
|
+
* }
|
|
30
|
+
* floatingActionButton={<FAB aria-label="Compose">...</FAB>}
|
|
31
|
+
* />
|
|
32
|
+
*
|
|
33
|
+
* // Auto-hide on scroll
|
|
34
|
+
* <BottomAppBar scrollBehavior="hidden" actions={...} />
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function BottomAppBar({
|
|
38
|
+
actions,
|
|
39
|
+
floatingActionButton,
|
|
40
|
+
scrollBehavior = "visible",
|
|
41
|
+
scrollElement,
|
|
42
|
+
className,
|
|
43
|
+
}: BottomAppBarProps) {
|
|
44
|
+
const shouldReduceMotion = useReducedMotion();
|
|
45
|
+
|
|
46
|
+
const { isHidden } = useAppBarScroll({
|
|
47
|
+
scrollElement,
|
|
48
|
+
behavior: scrollBehavior === "hidden" ? "enterAlways" : "pinned",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const hideTransition = shouldReduceMotion
|
|
52
|
+
? { duration: 0 }
|
|
53
|
+
: APP_BAR_BOTTOM_SPRING;
|
|
54
|
+
|
|
55
|
+
const translateY = scrollBehavior === "hidden" && isHidden ? "100%" : "0%";
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<m.nav
|
|
59
|
+
role="navigation"
|
|
60
|
+
aria-label="Bottom app bar"
|
|
61
|
+
className={cn(
|
|
62
|
+
"fixed bottom-0 inset-x-0 z-50",
|
|
63
|
+
"flex items-center",
|
|
64
|
+
"bg-m3-surface-container",
|
|
65
|
+
"px-2", // minimal horizontal padding, icon buttons provide spacing
|
|
66
|
+
"elevation-2", // BottomAppBarTokens.ContainerElevation = Level2 (always)
|
|
67
|
+
className,
|
|
68
|
+
)}
|
|
69
|
+
style={{
|
|
70
|
+
height: AppBarTokens.heights.bottom,
|
|
71
|
+
}}
|
|
72
|
+
animate={{ y: translateY }}
|
|
73
|
+
transition={hideTransition}
|
|
74
|
+
>
|
|
75
|
+
{/* Actions — leading end */}
|
|
76
|
+
{actions && <div className="flex items-center flex-1">{actions}</div>}
|
|
77
|
+
|
|
78
|
+
{/* FAB — trailing end */}
|
|
79
|
+
{floatingActionButton && (
|
|
80
|
+
<div className="shrink-0 ml-auto mr-2">{floatingActionButton}</div>
|
|
81
|
+
)}
|
|
82
|
+
</m.nav>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { DockedToolbar } from "./docked-toolbar";
|
|
4
|
+
|
|
5
|
+
describe("DockedToolbar", () => {
|
|
6
|
+
it("renders successfully with given children and aria-label", () => {
|
|
7
|
+
render(
|
|
8
|
+
<DockedToolbar aria-label="Test Toolbar">
|
|
9
|
+
<div data-testid="toolbar-content">Toolbar Item</div>
|
|
10
|
+
</DockedToolbar>,
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const toolbar = screen.getByRole("toolbar", { name: "Test Toolbar" });
|
|
14
|
+
expect(toolbar).toBeInTheDocument();
|
|
15
|
+
expect(toolbar).toHaveClass(
|
|
16
|
+
"flex items-center w-full overflow-x-auto bg-m3-surface-container",
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const content = screen.getByTestId("toolbar-content");
|
|
20
|
+
expect(content).toBeInTheDocument();
|
|
21
|
+
expect(content).toHaveTextContent("Toolbar Item");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("applies additional classNames", () => {
|
|
25
|
+
render(
|
|
26
|
+
<DockedToolbar aria-label="Toolbar" className="custom-test-class">
|
|
27
|
+
<span />
|
|
28
|
+
</DockedToolbar>,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const toolbar = screen.getByRole("toolbar");
|
|
32
|
+
expect(toolbar).toHaveClass("custom-test-class");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file docked-toolbar.tsx
|
|
3
|
+
* MD3 Expressive Docked Toolbar.
|
|
4
|
+
*
|
|
5
|
+
* Secondary navigation component. NOT an App Bar.
|
|
6
|
+
* Usually appears directly below the main App Bar.
|
|
7
|
+
* Height: 64px | Background: surface-container
|
|
8
|
+
* Typical content: chips, segmented buttons, filter actions.
|
|
9
|
+
*
|
|
10
|
+
* @see docs/m3/app-bars/DockedToolbarTokens.kt
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { cn } from "../../lib/utils";
|
|
14
|
+
import { AppBarTokens } from "./app-bar.tokens";
|
|
15
|
+
import type { DockedToolbarProps } from "./app-bar.types";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* MD3 Expressive Docked Toolbar.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* <DockedToolbar aria-label="Filter options">
|
|
23
|
+
* <Chip label="All" selected onClick={() => setFilter('all')} />
|
|
24
|
+
* <Chip label="Unread" onClick={() => setFilter('unread')} />
|
|
25
|
+
* <Chip label="Starred" onClick={() => setFilter('starred')} />
|
|
26
|
+
* </DockedToolbar>
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function DockedToolbar({
|
|
30
|
+
children,
|
|
31
|
+
"aria-label": ariaLabel,
|
|
32
|
+
className,
|
|
33
|
+
}: DockedToolbarProps) {
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
role="toolbar"
|
|
37
|
+
aria-label={ariaLabel}
|
|
38
|
+
className={cn(
|
|
39
|
+
"flex items-center w-full overflow-x-auto bg-m3-surface-container",
|
|
40
|
+
className,
|
|
41
|
+
)}
|
|
42
|
+
style={{
|
|
43
|
+
height: AppBarTokens.heights.dockedToolbar,
|
|
44
|
+
// DockedToolbarTokens: LeadingSpace = 16dp, TrailingSpace = 16dp
|
|
45
|
+
paddingLeft: AppBarTokens.dockedToolbar.leadingSpace,
|
|
46
|
+
paddingRight: AppBarTokens.dockedToolbar.trailingSpace,
|
|
47
|
+
// MinSpacing = 4dp (gap between items)
|
|
48
|
+
gap: AppBarTokens.dockedToolbar.minSpacing,
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
{children}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|