@bug-on/md3-react 2.0.2 → 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 +23 -0
- package/dist/index.css.d.ts +2 -0
- package/dist/index.d.mts +6127 -0
- package/dist/index.d.ts +6127 -69
- package/dist/index.js +2536 -665
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2443 -603
- 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 +23 -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/{dist/hooks/index.d.ts → src/hooks/index.ts} +1 -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/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/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/ui/tooltip/index.d.ts → src/ui/tooltip/index.ts} +0 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file supporting-text.tsx
|
|
3
|
+
* Supporting text, error text, and character counter for MD3 TextField.
|
|
4
|
+
*
|
|
5
|
+
* Animates in/out using AnimatePresence. Uses aria-live for accessibility.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { AnimatePresence, m } from "motion/react";
|
|
9
|
+
import * as React from "react";
|
|
10
|
+
import {
|
|
11
|
+
MD3_STANDARD_EASING,
|
|
12
|
+
MD3_SUPPORTING_DURATION,
|
|
13
|
+
} from "../../shared/constants";
|
|
14
|
+
import { TF_COLORS } from "../text-field.tokens";
|
|
15
|
+
|
|
16
|
+
export interface SupportingTextProps {
|
|
17
|
+
/** Helper text shown in normal state. */
|
|
18
|
+
supportingText?: string;
|
|
19
|
+
/** Error message — shown instead of supportingText when isError=true. */
|
|
20
|
+
errorText?: string;
|
|
21
|
+
/** Whether field is in error state. */
|
|
22
|
+
isError: boolean;
|
|
23
|
+
/** Current character count (value.length). */
|
|
24
|
+
charCount?: number;
|
|
25
|
+
/** Maximum character limit. Counter shown only when maxLength is set. */
|
|
26
|
+
maxLength?: number;
|
|
27
|
+
/** ID for aria-describedby linking from the input. */
|
|
28
|
+
id: string;
|
|
29
|
+
/** Disable animations. */
|
|
30
|
+
prefersReduced: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* AnimatePresence wrapper for text content changes.
|
|
35
|
+
* Fades out old text, fades in new text.
|
|
36
|
+
*/
|
|
37
|
+
function AnimatedText({
|
|
38
|
+
text,
|
|
39
|
+
motionKey,
|
|
40
|
+
className,
|
|
41
|
+
ariaLive,
|
|
42
|
+
duration,
|
|
43
|
+
}: {
|
|
44
|
+
text: string;
|
|
45
|
+
motionKey: string;
|
|
46
|
+
className: string;
|
|
47
|
+
ariaLive?: "polite" | "off";
|
|
48
|
+
duration: number;
|
|
49
|
+
}) {
|
|
50
|
+
return (
|
|
51
|
+
<m.span
|
|
52
|
+
key={motionKey}
|
|
53
|
+
className={className}
|
|
54
|
+
initial={{ opacity: 0, y: -4 }}
|
|
55
|
+
animate={{ opacity: 1, y: 0 }}
|
|
56
|
+
exit={{ opacity: 0, y: -4 }}
|
|
57
|
+
transition={{ duration, ease: MD3_STANDARD_EASING }}
|
|
58
|
+
aria-live={ariaLive}
|
|
59
|
+
>
|
|
60
|
+
{text}
|
|
61
|
+
</m.span>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* MD3 Supporting Text area.
|
|
67
|
+
*
|
|
68
|
+
* Layout: [helper/error text] [character counter]
|
|
69
|
+
* - Error text replaces supporting text when isError=true.
|
|
70
|
+
* - Character counter shows only when maxLength is provided.
|
|
71
|
+
* - Both use aria-live="polite" to announce changes to screen readers.
|
|
72
|
+
* - Animates in/out with opacity + y-offset via AnimatePresence.
|
|
73
|
+
*
|
|
74
|
+
* @accessibility
|
|
75
|
+
* - Error text: `aria-live="polite"` — screen readers announce when error appears.
|
|
76
|
+
* - Counter: `aria-live="polite"` — announces count changes.
|
|
77
|
+
*/
|
|
78
|
+
export const SupportingText = React.memo(function SupportingText({
|
|
79
|
+
supportingText,
|
|
80
|
+
errorText,
|
|
81
|
+
isError,
|
|
82
|
+
charCount,
|
|
83
|
+
maxLength,
|
|
84
|
+
id,
|
|
85
|
+
prefersReduced,
|
|
86
|
+
}: SupportingTextProps) {
|
|
87
|
+
const duration = prefersReduced ? 0 : MD3_SUPPORTING_DURATION;
|
|
88
|
+
|
|
89
|
+
const activeText = isError && errorText ? errorText : supportingText;
|
|
90
|
+
const isOverLimit = maxLength !== undefined && (charCount ?? 0) > maxLength;
|
|
91
|
+
|
|
92
|
+
// Determine counter color
|
|
93
|
+
const counterColor = isOverLimit
|
|
94
|
+
? TF_COLORS.error
|
|
95
|
+
: TF_COLORS.onSurfaceVariant;
|
|
96
|
+
|
|
97
|
+
if (!activeText && maxLength === undefined) return null;
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<m.div
|
|
101
|
+
className="flex items-start justify-between gap-2 mt-1 px-4"
|
|
102
|
+
initial={{ opacity: 0, height: 0 }}
|
|
103
|
+
animate={{ opacity: 1, height: "auto" }}
|
|
104
|
+
exit={{ opacity: 0, height: 0 }}
|
|
105
|
+
transition={{ duration, ease: MD3_STANDARD_EASING }}
|
|
106
|
+
>
|
|
107
|
+
{/* Left: helper or error text */}
|
|
108
|
+
<div id={id} className="flex-1 min-w-0">
|
|
109
|
+
<AnimatePresence mode="wait">
|
|
110
|
+
{activeText && (
|
|
111
|
+
<AnimatedText
|
|
112
|
+
key={isError && errorText ? "error" : "helper"}
|
|
113
|
+
motionKey={
|
|
114
|
+
isError && errorText
|
|
115
|
+
? `error-${errorText}`
|
|
116
|
+
: `helper-${supportingText}`
|
|
117
|
+
}
|
|
118
|
+
text={activeText}
|
|
119
|
+
className={`text-xs leading-4 block ${
|
|
120
|
+
isError ? "text-m3-error" : "text-m3-on-surface-variant"
|
|
121
|
+
}`}
|
|
122
|
+
ariaLive="polite"
|
|
123
|
+
duration={duration}
|
|
124
|
+
/>
|
|
125
|
+
)}
|
|
126
|
+
</AnimatePresence>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
{/* Right: character counter */}
|
|
130
|
+
{maxLength !== undefined && typeof charCount === "number" && (
|
|
131
|
+
<m.span
|
|
132
|
+
className="text-xs leading-4 tabular-nums shrink-0"
|
|
133
|
+
animate={{ color: counterColor }}
|
|
134
|
+
transition={{ duration, ease: MD3_STANDARD_EASING }}
|
|
135
|
+
aria-live="polite"
|
|
136
|
+
aria-atomic="true"
|
|
137
|
+
>
|
|
138
|
+
{charCount} / {maxLength}
|
|
139
|
+
</m.span>
|
|
140
|
+
)}
|
|
141
|
+
</m.div>
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
SupportingText.displayName = "SupportingText";
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file trailing-icon.tsx
|
|
3
|
+
* Trailing icon slot for MD3 TextField.
|
|
4
|
+
*
|
|
5
|
+
* Supports three built-in modes:
|
|
6
|
+
* - 'clear': ✕ button, visible when field has value
|
|
7
|
+
* - 'password-toggle': eye icon, toggles password visibility
|
|
8
|
+
* - 'custom': renders the `children` prop
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { AnimatePresence, m } from "motion/react";
|
|
12
|
+
import * as React from "react";
|
|
13
|
+
import {
|
|
14
|
+
MD3_ICON_SWAP_DURATION,
|
|
15
|
+
MD3_STANDARD_EASING,
|
|
16
|
+
} from "../../shared/constants";
|
|
17
|
+
import { TF_COLORS } from "../text-field.tokens";
|
|
18
|
+
import type { TextFieldTrailingIconMode } from "../text-field.types";
|
|
19
|
+
|
|
20
|
+
export interface TrailingIconProps {
|
|
21
|
+
mode: TextFieldTrailingIconMode;
|
|
22
|
+
/** Custom icon content (used when mode='custom'). */
|
|
23
|
+
children?: React.ReactNode;
|
|
24
|
+
/** Current input value — used to determine if clear button is visible. */
|
|
25
|
+
value: string;
|
|
26
|
+
/** Whether password is currently visible (for password-toggle mode). */
|
|
27
|
+
showPassword?: boolean;
|
|
28
|
+
/** Fires when clear button is clicked. */
|
|
29
|
+
onClear?: () => void;
|
|
30
|
+
/** Fires when password visibility toggle is clicked. */
|
|
31
|
+
onPasswordToggle?: () => void;
|
|
32
|
+
/** Whether the field is in error state. */
|
|
33
|
+
isError: boolean;
|
|
34
|
+
/** Whether the field is disabled. */
|
|
35
|
+
isDisabled: boolean;
|
|
36
|
+
/** Disable animations. */
|
|
37
|
+
prefersReduced: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Animated icon wrapper with scale + opacity transition. */
|
|
41
|
+
function AnimatedIconSlot({
|
|
42
|
+
children,
|
|
43
|
+
motionKey,
|
|
44
|
+
duration,
|
|
45
|
+
}: {
|
|
46
|
+
children: React.ReactNode;
|
|
47
|
+
motionKey: string;
|
|
48
|
+
duration: number;
|
|
49
|
+
}) {
|
|
50
|
+
return (
|
|
51
|
+
<m.span
|
|
52
|
+
key={motionKey}
|
|
53
|
+
className="flex items-center justify-center"
|
|
54
|
+
initial={{ opacity: 0, scale: 0 }}
|
|
55
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
56
|
+
exit={{ opacity: 0, scale: 0 }}
|
|
57
|
+
transition={{ duration, ease: MD3_STANDARD_EASING }}
|
|
58
|
+
>
|
|
59
|
+
{children}
|
|
60
|
+
</m.span>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Eye icon (password visible). */
|
|
65
|
+
function EyeOpenIcon() {
|
|
66
|
+
return (
|
|
67
|
+
<svg
|
|
68
|
+
width="24"
|
|
69
|
+
height="24"
|
|
70
|
+
viewBox="0 0 24 24"
|
|
71
|
+
fill="currentColor"
|
|
72
|
+
aria-hidden="true"
|
|
73
|
+
>
|
|
74
|
+
<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" />
|
|
75
|
+
</svg>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Eye-off icon (password hidden). */
|
|
80
|
+
function EyeOffIcon() {
|
|
81
|
+
return (
|
|
82
|
+
<svg
|
|
83
|
+
width="24"
|
|
84
|
+
height="24"
|
|
85
|
+
viewBox="0 0 24 24"
|
|
86
|
+
fill="currentColor"
|
|
87
|
+
aria-hidden="true"
|
|
88
|
+
>
|
|
89
|
+
<path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z" />
|
|
90
|
+
</svg>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** ✕ Clear icon. */
|
|
95
|
+
function ClearIcon() {
|
|
96
|
+
return (
|
|
97
|
+
<svg
|
|
98
|
+
width="20"
|
|
99
|
+
height="20"
|
|
100
|
+
viewBox="0 0 24 24"
|
|
101
|
+
fill="currentColor"
|
|
102
|
+
aria-hidden="true"
|
|
103
|
+
>
|
|
104
|
+
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
|
|
105
|
+
</svg>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* MD3 Trailing Icon.
|
|
111
|
+
*
|
|
112
|
+
* Touch target: 48×48px (padding extends the hit area beyond the 24×24 icon).
|
|
113
|
+
*
|
|
114
|
+
* @accessibility
|
|
115
|
+
* - Clear button: `aria-label="Clear input"`
|
|
116
|
+
* - Password toggle: `aria-label="Show password"` / `"Hide password"`
|
|
117
|
+
* - Custom: no aria — consumer provides accessible markup
|
|
118
|
+
*/
|
|
119
|
+
export const TrailingIcon = React.memo(function TrailingIcon({
|
|
120
|
+
mode,
|
|
121
|
+
children,
|
|
122
|
+
value,
|
|
123
|
+
showPassword = false,
|
|
124
|
+
onClear,
|
|
125
|
+
onPasswordToggle,
|
|
126
|
+
isError,
|
|
127
|
+
isDisabled,
|
|
128
|
+
prefersReduced,
|
|
129
|
+
}: TrailingIconProps) {
|
|
130
|
+
const duration = prefersReduced ? 0 : MD3_ICON_SWAP_DURATION;
|
|
131
|
+
|
|
132
|
+
const iconColor =
|
|
133
|
+
isError && !isDisabled ? TF_COLORS.error : TF_COLORS.onSurfaceVariant;
|
|
134
|
+
|
|
135
|
+
// Touch target button styles: 48×48px, centered icon
|
|
136
|
+
const btnClass =
|
|
137
|
+
"relative flex items-center justify-center w-12 h-12 -mr-1 rounded-full cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-m3-primary)] transition-colors hover:bg-[var(--color-m3-on-surface)]/8 active:bg-[var(--color-m3-on-surface)]/12 disabled:pointer-events-none";
|
|
138
|
+
|
|
139
|
+
if (mode === "clear") {
|
|
140
|
+
const hasValue = value.length > 0;
|
|
141
|
+
return (
|
|
142
|
+
<AnimatePresence>
|
|
143
|
+
{hasValue && (
|
|
144
|
+
<AnimatedIconSlot motionKey="clear" duration={duration}>
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
aria-label="Clear input"
|
|
148
|
+
onClick={onClear}
|
|
149
|
+
disabled={isDisabled}
|
|
150
|
+
tabIndex={isDisabled ? -1 : 0}
|
|
151
|
+
className={btnClass}
|
|
152
|
+
style={{ color: iconColor }}
|
|
153
|
+
>
|
|
154
|
+
<ClearIcon />
|
|
155
|
+
</button>
|
|
156
|
+
</AnimatedIconSlot>
|
|
157
|
+
)}
|
|
158
|
+
</AnimatePresence>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (mode === "password-toggle") {
|
|
163
|
+
return (
|
|
164
|
+
<AnimatePresence mode="wait">
|
|
165
|
+
<AnimatedIconSlot
|
|
166
|
+
motionKey={showPassword ? "eye-off" : "eye-on"}
|
|
167
|
+
duration={duration}
|
|
168
|
+
>
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
aria-label={showPassword ? "Hide password" : "Show password"}
|
|
172
|
+
onClick={onPasswordToggle}
|
|
173
|
+
disabled={isDisabled}
|
|
174
|
+
tabIndex={isDisabled ? -1 : 0}
|
|
175
|
+
className={btnClass}
|
|
176
|
+
style={{ color: iconColor }}
|
|
177
|
+
>
|
|
178
|
+
{showPassword ? <EyeOffIcon /> : <EyeOpenIcon />}
|
|
179
|
+
</button>
|
|
180
|
+
</AnimatedIconSlot>
|
|
181
|
+
</AnimatePresence>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (mode === "custom" && children) {
|
|
186
|
+
return (
|
|
187
|
+
<div
|
|
188
|
+
className="flex items-center justify-center w-6 h-6 mr-3"
|
|
189
|
+
style={{ color: iconColor }}
|
|
190
|
+
>
|
|
191
|
+
{children}
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return null;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
TrailingIcon.displayName = "TrailingIcon";
|