@bug-on/md3-react 2.0.3 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +42 -0
- package/CHANGELOG.md +69 -0
- package/dist/index.css +178 -0
- package/dist/index.css.d.ts +2 -0
- package/dist/index.d.mts +6135 -0
- package/dist/index.d.ts +6135 -71
- package/dist/index.js +1688 -631
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1600 -564
- 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/plugin.d.mts +1 -0
- package/dist/plugin.d.ts +1 -0
- package/dist/plugin.js +13 -0
- package/dist/plugin.js.map +1 -0
- package/dist/plugin.mjs +3 -0
- package/dist/plugin.mjs.map +1 -0
- package/dist/typography.css.d.ts +2 -0
- package/package.json +28 -19
- package/scripts/copy-assets.js +115 -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 +195 -0
- package/src/lib/utils.ts +6 -0
- package/src/plugin.ts +12 -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 +306 -0
- package/src/ui/button.tsx +665 -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 +607 -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 +135 -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 +215 -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,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file icon.test.tsx
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for the Material Symbols <Icon /> component.
|
|
5
|
+
*
|
|
6
|
+
* Tests cover:
|
|
7
|
+
* - Correct text content (icon name as ligature)
|
|
8
|
+
* - Font-family for each variant (outlined / rounded / sharp)
|
|
9
|
+
* - font-variation-settings reflecting fill / weight / grade / opticalSize
|
|
10
|
+
* - aria-hidden attribute
|
|
11
|
+
* - className merging
|
|
12
|
+
* - Static render (plain span) vs animated render (motion span)
|
|
13
|
+
* - size prop overrides font-size independently of opticalSize
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { render, screen } from "@testing-library/react";
|
|
17
|
+
import { describe, expect, it } from "vitest";
|
|
18
|
+
import { Icon } from "./icon";
|
|
19
|
+
|
|
20
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
// Helpers
|
|
22
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/** Returns the rendered span's inline style object. */
|
|
25
|
+
function getStyle(element: HTMLElement): CSSStyleDeclaration {
|
|
26
|
+
return element.style;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
// Tests
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
describe("Icon", () => {
|
|
34
|
+
// ── Rendering ─────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
it("renders the icon name as text content", () => {
|
|
37
|
+
render(<Icon name="home" />);
|
|
38
|
+
expect(screen.getByText("home")).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("renders the icon name with underscores intact", () => {
|
|
42
|
+
render(<Icon name="arrow_forward" />);
|
|
43
|
+
expect(screen.getByText("arrow_forward")).toBeInTheDocument();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ── Accessibility ─────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
it("sets aria-hidden='true' by default", () => {
|
|
49
|
+
render(<Icon name="home" />);
|
|
50
|
+
const el = screen.getByText("home");
|
|
51
|
+
expect(el).toHaveAttribute("aria-hidden", "true");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ── Variant font-family ───────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
it("uses 'Material Symbols Outlined' font by default (outlined variant)", () => {
|
|
57
|
+
render(<Icon name="home" />);
|
|
58
|
+
const style = getStyle(screen.getByText("home"));
|
|
59
|
+
expect(style.fontFamily).toContain("Material Symbols Outlined");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("uses 'Material Symbols Rounded' for variant='rounded'", () => {
|
|
63
|
+
render(<Icon name="home" variant="rounded" />);
|
|
64
|
+
const style = getStyle(screen.getByText("home"));
|
|
65
|
+
expect(style.fontFamily).toContain("Material Symbols Rounded");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("uses 'Material Symbols Sharp' for variant='sharp'", () => {
|
|
69
|
+
render(<Icon name="home" variant="sharp" />);
|
|
70
|
+
const style = getStyle(screen.getByText("home"));
|
|
71
|
+
expect(style.fontFamily).toContain("Material Symbols Sharp");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ── Font size ─────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
it("defaults font-size to opticalSize (24px) when size is not provided", () => {
|
|
77
|
+
render(<Icon name="home" />);
|
|
78
|
+
const style = getStyle(screen.getByText("home"));
|
|
79
|
+
expect(style.fontSize).toBe("24px");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("uses explicit size prop for font-size (overrides opticalSize)", () => {
|
|
83
|
+
render(<Icon name="home" size={18} opticalSize={20} />);
|
|
84
|
+
const style = getStyle(screen.getByText("home"));
|
|
85
|
+
expect(style.fontSize).toBe("18px");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("applies opticalSize=48 as font-size when no explicit size given", () => {
|
|
89
|
+
render(<Icon name="home" opticalSize={48} />);
|
|
90
|
+
const style = getStyle(screen.getByText("home"));
|
|
91
|
+
expect(style.fontSize).toBe("48px");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ── font-variation-settings ───────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
it("applies default font-variation-settings (fill=0, wght=400, GRAD=0, opsz=24)", () => {
|
|
97
|
+
render(<Icon name="home" />);
|
|
98
|
+
const style = getStyle(screen.getByText("home"));
|
|
99
|
+
expect(style.fontVariationSettings).toBe(
|
|
100
|
+
"'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24",
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("reflects fill=1 in font-variation-settings", () => {
|
|
105
|
+
render(<Icon name="favorite" fill={1} />);
|
|
106
|
+
const style = getStyle(screen.getByText("favorite"));
|
|
107
|
+
expect(style.fontVariationSettings).toContain("'FILL' 1");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("reflects weight=700 in font-variation-settings", () => {
|
|
111
|
+
render(<Icon name="home" weight={700} />);
|
|
112
|
+
const style = getStyle(screen.getByText("home"));
|
|
113
|
+
expect(style.fontVariationSettings).toContain("'wght' 700");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("reflects grade=-25 in font-variation-settings", () => {
|
|
117
|
+
render(<Icon name="home" grade={-25} />);
|
|
118
|
+
const style = getStyle(screen.getByText("home"));
|
|
119
|
+
expect(style.fontVariationSettings).toContain("'GRAD' -25");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("reflects opticalSize=48 in font-variation-settings opsz axis", () => {
|
|
123
|
+
render(<Icon name="home" opticalSize={48} />);
|
|
124
|
+
const style = getStyle(screen.getByText("home"));
|
|
125
|
+
expect(style.fontVariationSettings).toContain("'opsz' 48");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("composes all axes correctly when all props are provided", () => {
|
|
129
|
+
render(
|
|
130
|
+
<Icon name="star" fill={1} weight={300} grade={200} opticalSize={40} />,
|
|
131
|
+
);
|
|
132
|
+
const style = getStyle(screen.getByText("star"));
|
|
133
|
+
expect(style.fontVariationSettings).toBe(
|
|
134
|
+
"'FILL' 1, 'wght' 300, 'GRAD' 200, 'opsz' 40",
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ── className merging ─────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
it("includes 'md-icon' base class by default", () => {
|
|
141
|
+
render(<Icon name="home" />);
|
|
142
|
+
const el = screen.getByText("home");
|
|
143
|
+
expect(el.className).toContain("md-icon");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("merges additional className with md-icon", () => {
|
|
147
|
+
render(<Icon name="home" className="text-primary" />);
|
|
148
|
+
const el = screen.getByText("home");
|
|
149
|
+
expect(el.className).toContain("md-icon");
|
|
150
|
+
expect(el.className).toContain("text-primary");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ── Static vs animated render ─────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
it("renders a plain <span> when animateFill is false (default)", () => {
|
|
156
|
+
const { container } = render(<Icon name="home" />);
|
|
157
|
+
// motion/react m.span renders as a <span> in DOM, but static span has no
|
|
158
|
+
// data-framer / motion attributes injected. We check the element is a span.
|
|
159
|
+
const el = container.querySelector("span");
|
|
160
|
+
expect(el).not.toBeNull();
|
|
161
|
+
expect(el?.textContent).toBe("home");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("still renders as a span element when animateFill=true (motion renders span)", () => {
|
|
165
|
+
const { container } = render(<Icon name="home" animateFill />);
|
|
166
|
+
// m.span renders as a real <span> in DOM
|
|
167
|
+
const el = container.querySelector("span");
|
|
168
|
+
expect(el).not.toBeNull();
|
|
169
|
+
expect(el?.textContent).toBe("home");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("sets aria-hidden on animated span too", () => {
|
|
173
|
+
render(<Icon name="home" animateFill />);
|
|
174
|
+
const el = screen.getByText("home");
|
|
175
|
+
expect(el).toHaveAttribute("aria-hidden", "true");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ── style prop passthrough ────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
it("merges custom style with computed style", () => {
|
|
181
|
+
render(<Icon name="home" style={{ color: "red" }} />);
|
|
182
|
+
const el = screen.getByText("home");
|
|
183
|
+
expect(el.style.color).toBe("red");
|
|
184
|
+
// Computed props still present
|
|
185
|
+
expect(el.style.fontFamily).toContain("Material Symbols Outlined");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ── displayName ───────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
it("has displayName 'Icon'", () => {
|
|
191
|
+
// React.memo wraps the component; access display name via the inner type
|
|
192
|
+
// biome-ignore lint/suspicious/noExplicitAny: accessing React internals for testing
|
|
193
|
+
expect((Icon as any).type?.displayName ?? (Icon as any).displayName).toBe(
|
|
194
|
+
"Icon",
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
});
|
package/src/ui/icon.tsx
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { domMax, LazyMotion, m } from "motion/react";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
import { SPRING_TRANSITION_FAST } from "./shared/constants";
|
|
5
|
+
|
|
6
|
+
// @internal — font must be loaded via '@bug-on/md3-react/material-symbols.css'
|
|
7
|
+
const VARIANT_FONT: Record<NonNullable<IconProps["variant"]>, string> = {
|
|
8
|
+
outlined: "'Material Symbols Outlined'",
|
|
9
|
+
rounded: "'Material Symbols Rounded'",
|
|
10
|
+
sharp: "'Material Symbols Sharp'",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Props cho component {@link Icon}.
|
|
15
|
+
*
|
|
16
|
+
* Tất cả các trục biến thiên (variable font axes) được map trực tiếp sang `font-variation-settings`.
|
|
17
|
+
*/
|
|
18
|
+
export interface IconProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
19
|
+
/**
|
|
20
|
+
* Tên của Material Symbol theo định dạng snake_case.
|
|
21
|
+
* @example "home", "arrow_forward", "settings"
|
|
22
|
+
* @see https://fonts.google.com/icons
|
|
23
|
+
*/
|
|
24
|
+
name: string;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Kiểu hình học (Geometric style variant) — tương ứng với font family được tải.
|
|
28
|
+
* @default "outlined"
|
|
29
|
+
*/
|
|
30
|
+
variant?: "outlined" | "rounded" | "sharp";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Trục FILL. `0` = outlined (viền), `1` = filled (tràn màu).
|
|
34
|
+
* Có hiệu ứng spring khi `animateFill` là true.
|
|
35
|
+
* @default 0
|
|
36
|
+
*/
|
|
37
|
+
fill?: 0 | 1;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Trục wght — độ dày của nét (stroke weight). Nên khớp với độ dày text xung quanh.
|
|
41
|
+
* @default 400
|
|
42
|
+
*/
|
|
43
|
+
weight?: 100 | 200 | 300 | 400 | 500 | 600 | 700;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Trục GRAD — tinh chỉnh độ dày thị giác mà không ảnh hưởng tới layout.
|
|
47
|
+
* Dùng mức `-25` trên nền tối để bù trừ hiệu ứng phát sáng (halation).
|
|
48
|
+
* @default 0
|
|
49
|
+
*/
|
|
50
|
+
grade?: -50 | -25 | 0 | 100 | 200;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Trục opsz — kích thước quang học (optical size) tính bằng dp. Dùng để thiết lập `font-size` nếu không truyền `size`.
|
|
54
|
+
* Hãy để giá trị khớp với pixel sẽ render ra để thấy chất lượng tốt nhất.
|
|
55
|
+
* @default 24
|
|
56
|
+
*/
|
|
57
|
+
opticalSize?: 20 | 24 | 40 | 48;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Ghi đè trực tiếp `font-size` bằng px. Trục `opsz` vẫn sẽ tuân theo thuộc tính `opticalSize`.
|
|
61
|
+
* @example size={18} opticalSize={20}
|
|
62
|
+
*/
|
|
63
|
+
size?: number | "inherit";
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Kích hoạt hiệu ứng spring mượt mà khi chuyển đổi giá trị FILL (sử dụng cấu hình `SPRING_TRANSITION_FAST`).
|
|
67
|
+
* Yêu cầu dependency `motion/react`.
|
|
68
|
+
* @default false
|
|
69
|
+
* @example <Icon name="favorite" fill={isLiked ? 1 : 0} animateFill />
|
|
70
|
+
*/
|
|
71
|
+
animateFill?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const IconComponent = React.forwardRef<HTMLSpanElement, IconProps>(
|
|
75
|
+
(
|
|
76
|
+
{
|
|
77
|
+
name,
|
|
78
|
+
variant = "outlined",
|
|
79
|
+
fill = 0,
|
|
80
|
+
weight = 400,
|
|
81
|
+
grade = 0,
|
|
82
|
+
opticalSize = 24,
|
|
83
|
+
size,
|
|
84
|
+
animateFill = false,
|
|
85
|
+
className,
|
|
86
|
+
style,
|
|
87
|
+
...restProps
|
|
88
|
+
},
|
|
89
|
+
ref,
|
|
90
|
+
) => {
|
|
91
|
+
const fontVariationSettings = `'FILL' ${fill}, 'wght' ${weight}, 'GRAD' ${grade}, 'opsz' ${opticalSize}`;
|
|
92
|
+
|
|
93
|
+
const computedStyle: React.CSSProperties = {
|
|
94
|
+
fontFamily: VARIANT_FONT[variant],
|
|
95
|
+
fontSize:
|
|
96
|
+
size === "inherit"
|
|
97
|
+
? "inherit"
|
|
98
|
+
: size != null
|
|
99
|
+
? `${size}px`
|
|
100
|
+
: `${opticalSize}px`,
|
|
101
|
+
fontVariationSettings,
|
|
102
|
+
...style,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
if (animateFill) {
|
|
106
|
+
return (
|
|
107
|
+
<LazyMotion features={domMax} strict>
|
|
108
|
+
<m.span
|
|
109
|
+
ref={ref}
|
|
110
|
+
className={cn(
|
|
111
|
+
"md-icon inline-flex items-center justify-center shrink-0 select-none",
|
|
112
|
+
className,
|
|
113
|
+
)}
|
|
114
|
+
aria-hidden="true"
|
|
115
|
+
animate={{ fontVariationSettings }}
|
|
116
|
+
transition={SPRING_TRANSITION_FAST}
|
|
117
|
+
style={computedStyle}
|
|
118
|
+
// biome-ignore lint/suspicious/noExplicitAny: motion v12 HTMLMotionProps conflicts with React's event types
|
|
119
|
+
{...(restProps as any)}
|
|
120
|
+
>
|
|
121
|
+
{name}
|
|
122
|
+
</m.span>
|
|
123
|
+
</LazyMotion>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<span
|
|
129
|
+
ref={ref}
|
|
130
|
+
className={cn(
|
|
131
|
+
"md-icon inline-flex items-center justify-center shrink-0 select-none",
|
|
132
|
+
className,
|
|
133
|
+
)}
|
|
134
|
+
aria-hidden="true"
|
|
135
|
+
style={computedStyle}
|
|
136
|
+
{...restProps}
|
|
137
|
+
>
|
|
138
|
+
{name}
|
|
139
|
+
</span>
|
|
140
|
+
);
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
IconComponent.displayName = "Icon";
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Component hiển thị Icon bằng Material Symbols (variable font).
|
|
148
|
+
*
|
|
149
|
+
* Hãy đảm bảo đã import CSS chứa font trước khi dùng:
|
|
150
|
+
* ```ts
|
|
151
|
+
* import '@bug-on/md3-react/material-symbols.css';
|
|
152
|
+
* ```
|
|
153
|
+
*
|
|
154
|
+
* @remarks
|
|
155
|
+
* - Đặt tên icon dùng snake_case: `"arrow_forward"`, KHÔNG PHẢI `"ArrowForward"`.
|
|
156
|
+
* - Thuộc tính `aria-hidden="true"` được tự động thêm vào — bạn cần thêm label đọc bằng giọng nói (accessible labels) ở phần tử cha.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```tsx
|
|
160
|
+
* // Icon cơ bản
|
|
161
|
+
* <Icon name="home" />
|
|
162
|
+
*
|
|
163
|
+
* // Tùy chỉnh trực quan (filled, nét dày)
|
|
164
|
+
* <Icon name="favorite" variant="rounded" fill={1} weight={300} />
|
|
165
|
+
*
|
|
166
|
+
* // Animate khi trạng thái thay đổi
|
|
167
|
+
* <Icon name="bookmark" fill={saved ? 1 : 0} animateFill />
|
|
168
|
+
*
|
|
169
|
+
* // Đổi kích thước icon cụ thể
|
|
170
|
+
* <Icon name="close" size={18} opticalSize={20} />
|
|
171
|
+
*
|
|
172
|
+
* // Kết hợp với các component khác
|
|
173
|
+
* <Button icon={<Icon name="add" />}>Thêm vào giỏ</Button>
|
|
174
|
+
* ```
|
|
175
|
+
*
|
|
176
|
+
* @see https://fonts.google.com/icons
|
|
177
|
+
* @see https://m3.material.io/styles/icons/overview
|
|
178
|
+
*/
|
|
179
|
+
export const Icon = React.memo(IconComponent);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { act, render } from "@testing-library/react";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { LoadingIndicator } from "./loading-indicator";
|
|
4
|
+
|
|
5
|
+
describe("LoadingIndicator Component", () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.useFakeTimers();
|
|
8
|
+
vi.spyOn(window, "requestAnimationFrame").mockImplementation(
|
|
9
|
+
(cb: FrameRequestCallback) => {
|
|
10
|
+
return setTimeout(() => cb(Date.now()), 16) as unknown as number;
|
|
11
|
+
},
|
|
12
|
+
);
|
|
13
|
+
vi.spyOn(window, "cancelAnimationFrame").mockImplementation(
|
|
14
|
+
(id: number) => {
|
|
15
|
+
clearTimeout(id);
|
|
16
|
+
},
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
vi.restoreAllMocks();
|
|
22
|
+
vi.useRealTimers();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("renders an <svg> element with correct structure", () => {
|
|
26
|
+
const { container } = render(
|
|
27
|
+
<LoadingIndicator size={24} aria-label="Loading" />,
|
|
28
|
+
);
|
|
29
|
+
const svg = container.querySelector("svg");
|
|
30
|
+
expect(svg).toBeInTheDocument();
|
|
31
|
+
expect(svg).toHaveAttribute("viewBox", "4 4 40 40");
|
|
32
|
+
expect(svg).toHaveAttribute("width", "24");
|
|
33
|
+
expect(svg).toHaveAttribute("height", "24");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("delays rendering of <animate> tags by 1 frame to prevent SMIL freezing", async () => {
|
|
37
|
+
const { container } = render(<LoadingIndicator aria-label="Loading" />);
|
|
38
|
+
const svg = container.querySelector("svg");
|
|
39
|
+
|
|
40
|
+
// Ngay Frame 0 (lúc thẻ SVG vừa vẽ lên DOM), chưa được phép có <animate>
|
|
41
|
+
// để tránh đụng độ Chromium layout optimization (culling) gây tê liệt animation
|
|
42
|
+
expect(svg?.querySelector("animate")).toBeNull();
|
|
43
|
+
expect(svg?.querySelector("animateTransform")).toBeNull();
|
|
44
|
+
|
|
45
|
+
// Tua nhanh thời gian vượt qua 16ms để giả lập hoàn tất requestAnimationFrame
|
|
46
|
+
await act(async () => {
|
|
47
|
+
vi.advanceTimersByTime(20);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Sang Frame 1, thẻ <animate> phải được inject vào trong DOM để khởi sinh đồng hồ SMIL
|
|
51
|
+
expect(svg?.querySelector("animate")).not.toBeNull();
|
|
52
|
+
expect(svg?.querySelector("animateTransform")).not.toBeNull();
|
|
53
|
+
expect(svg?.querySelector("animate")?.getAttribute("attributeName")).toBe(
|
|
54
|
+
"d",
|
|
55
|
+
);
|
|
56
|
+
expect(
|
|
57
|
+
svg?.querySelector("animateTransform")?.getAttribute("attributeName"),
|
|
58
|
+
).toBe("transform");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("supports custom size and color", () => {
|
|
62
|
+
const { container } = render(
|
|
63
|
+
<LoadingIndicator size={48} color="red" aria-label="Loading" />,
|
|
64
|
+
);
|
|
65
|
+
const wrapper = container.firstChild as HTMLElement;
|
|
66
|
+
const svg = container.querySelector("svg");
|
|
67
|
+
expect(svg).toHaveAttribute("width", "48");
|
|
68
|
+
expect(svg).toHaveAttribute("height", "48");
|
|
69
|
+
|
|
70
|
+
const styleStr = wrapper.getAttribute("style") || "";
|
|
71
|
+
expect(styleStr).toMatch(/color:\s*(red|rgb\(255,\s*0,\s*0\))/);
|
|
72
|
+
});
|
|
73
|
+
});
|