@12-apps/ui 1.4.0 → 1.5.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/eslint.config.js +12 -0
- package/package.json +5 -4
- package/src/components/data-display/AlertDialog/AlertDialog.test.stories.tsx +16 -6
- package/src/components/data-display/Banner/Banner.test.stories.tsx +26 -18
- package/src/components/data-display/Carousel/Carousel.chrome.tsx +83 -0
- package/src/components/data-display/Carousel/Carousel.hooks.ts +88 -0
- package/src/components/data-display/Carousel/Carousel.parts.tsx +346 -0
- package/src/components/data-display/Carousel/Carousel.slide.tsx +190 -0
- package/src/components/data-display/Carousel/Carousel.styles.ts +86 -0
- package/src/components/data-display/Carousel/Carousel.test.stories.tsx +15 -5
- package/src/components/data-display/Carousel/Carousel.tsx +84 -732
- package/src/components/data-display/HoverCard/HoverCard.animations.ts +73 -0
- package/src/components/data-display/HoverCard/HoverCard.content.tsx +143 -0
- package/src/components/data-display/HoverCard/HoverCard.hooks.ts +180 -0
- package/src/components/data-display/HoverCard/HoverCard.styles.ts +178 -0
- package/src/components/data-display/HoverCard/HoverCard.tsx +117 -631
- package/src/components/data-display/Sheet/Sheet.animations.ts +44 -0
- package/src/components/data-display/Sheet/Sheet.body.tsx +124 -0
- package/src/components/data-display/Sheet/Sheet.drag.ts +203 -0
- package/src/components/data-display/Sheet/Sheet.hooks.ts +318 -0
- package/src/components/data-display/Sheet/Sheet.parts.tsx +224 -0
- package/src/components/data-display/Sheet/Sheet.styles.ts +257 -0
- package/src/components/data-display/Sheet/Sheet.test.stories.tsx +25 -21
- package/src/components/data-display/Sheet/Sheet.tsx +125 -948
- package/src/components/feedback/Dialog/Dialog.test.stories.tsx +37 -28
- package/src/components/feedback/Modal/Modal.styles.ts +126 -0
- package/src/components/feedback/Modal/Modal.test.stories.tsx +5 -5
- package/src/components/feedback/Modal/Modal.transitions.tsx +40 -0
- package/src/components/feedback/Modal/Modal.tsx +37 -242
- package/src/components/feedback/StackedModal/StackedModal.test.stories.tsx +9 -11
- package/src/components/form/InputOTP/InputOTP.hooks.ts +104 -0
- package/src/components/form/InputOTP/InputOTP.styles.ts +97 -0
- package/src/components/form/InputOTP/InputOTP.tsx +74 -221
- package/src/components/form/Label/Label.parts.tsx +75 -0
- package/src/components/form/Label/Label.styles.ts +206 -0
- package/src/components/form/Label/Label.test.stories.tsx +20 -14
- package/src/components/form/Label/Label.tsx +73 -325
- package/src/components/form/Menubar/Menubar.hooks.ts +41 -0
- package/src/components/form/Menubar/Menubar.item.tsx +121 -0
- package/src/components/form/Menubar/Menubar.separator.tsx +25 -0
- package/src/components/form/Menubar/Menubar.styles.ts +110 -0
- package/src/components/form/Menubar/Menubar.toolbar.tsx +103 -0
- package/src/components/form/Menubar/Menubar.tsx +64 -347
- package/src/components/form/RadioGroup/RadioGroup.animations.ts +48 -0
- package/src/components/form/RadioGroup/RadioGroup.styles.ts +305 -0
- package/src/components/form/RadioGroup/RadioGroup.tsx +81 -651
- package/src/components/form/RadioGroup/RadioGroup.variants.tsx +291 -0
- package/src/components/form/Slider/Slider.animations.ts +40 -0
- package/src/components/form/Slider/Slider.styles.ts +292 -0
- package/src/components/form/Slider/Slider.tsx +106 -357
- package/src/components/form/Switch/Switch.animations.ts +62 -0
- package/src/components/form/Switch/Switch.parts.tsx +213 -0
- package/src/components/form/Switch/Switch.styles.ts +355 -0
- package/src/components/form/Switch/Switch.tsx +87 -566
- package/src/components/form/ToggleGroup/ToggleGroup.test.stories.tsx +26 -29
- package/src/components/layout/Resizable/Resizable.test.stories.tsx +11 -7
- package/src/components/layout/Spacer/Spacer.test.stories.tsx +11 -9
- package/src/components/typography/Blockquote/Blockquote.test.stories.tsx +7 -7
- package/src/components/typography/Heading/Heading.styles.ts +101 -0
- package/src/components/typography/Heading/Heading.tsx +48 -164
- package/src/components/typography/Text/Text.test.stories.tsx +27 -30
- package/src/components/utility/AnimatedIcon/AnimatedIcon.animations.ts +274 -0
- package/src/components/utility/AnimatedIcon/AnimatedIcon.styles.ts +231 -0
- package/src/components/utility/AnimatedIcon/AnimatedIcon.tsx +39 -559
- package/src/utils/index.ts +1 -0
- package/src/utils/withDefaults.ts +20 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { alpha } from '@mui/material';
|
|
2
|
+
import type { CSSObject, Theme } from '@mui/material';
|
|
3
|
+
|
|
4
|
+
import type { LabelProps } from './Label.types';
|
|
5
|
+
|
|
6
|
+
type Size = NonNullable<LabelProps['size']>;
|
|
7
|
+
type Variant = NonNullable<LabelProps['variant']>;
|
|
8
|
+
type Weight = NonNullable<LabelProps['weight']>;
|
|
9
|
+
|
|
10
|
+
const SIZES: Record<Size, (theme: Theme) => CSSObject> = {
|
|
11
|
+
xs: (theme) => ({ fontSize: '0.75rem', lineHeight: 1.2, padding: theme.spacing(0.25, 0.5) }),
|
|
12
|
+
sm: (theme) => ({ fontSize: '0.875rem', lineHeight: 1.3, padding: theme.spacing(0.5, 0.75) }),
|
|
13
|
+
md: (theme) => ({ fontSize: '1rem', lineHeight: 1.5, padding: theme.spacing(0.75, 1) }),
|
|
14
|
+
lg: (theme) => ({ fontSize: '1.125rem', lineHeight: 1.6, padding: theme.spacing(1, 1.25) }),
|
|
15
|
+
xl: (theme) => ({ fontSize: '1.25rem', lineHeight: 1.7, padding: theme.spacing(1.25, 1.5) }),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const sizeStyles = (theme: Theme, size: Size): CSSObject => (SIZES[size] ?? SIZES.md)(theme);
|
|
19
|
+
|
|
20
|
+
const WEIGHTS: Record<Weight, number> = {
|
|
21
|
+
light: 300,
|
|
22
|
+
regular: 400,
|
|
23
|
+
medium: 500,
|
|
24
|
+
semibold: 600,
|
|
25
|
+
bold: 700,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const fontWeight = (weight: Weight) => WEIGHTS[weight] ?? WEIGHTS.regular;
|
|
29
|
+
|
|
30
|
+
export interface LabelStyleFlags {
|
|
31
|
+
variant: Variant;
|
|
32
|
+
size: Size;
|
|
33
|
+
color: NonNullable<LabelProps['color']>;
|
|
34
|
+
weight: Weight;
|
|
35
|
+
transform: NonNullable<LabelProps['transform']>;
|
|
36
|
+
align: NonNullable<LabelProps['align']>;
|
|
37
|
+
error: boolean;
|
|
38
|
+
disabled: boolean;
|
|
39
|
+
glow: boolean;
|
|
40
|
+
pulse: boolean;
|
|
41
|
+
ripple: boolean;
|
|
42
|
+
srOnly: boolean;
|
|
43
|
+
nowrap: boolean;
|
|
44
|
+
truncate: boolean;
|
|
45
|
+
clickable: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The text colour, which error and disabled override before the palette is
|
|
50
|
+
* consulted at all. Resolved once and threaded through — the variant styles
|
|
51
|
+
* read it up to four times each.
|
|
52
|
+
*/
|
|
53
|
+
export const textColor = (theme: Theme, flags: LabelStyleFlags): string => {
|
|
54
|
+
if (flags.error) return theme.palette.error.main;
|
|
55
|
+
if (flags.disabled) return theme.palette.text.disabled;
|
|
56
|
+
if (flags.color === 'default') return theme.palette.text.primary;
|
|
57
|
+
return theme.palette[flags.color].main;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** The ripple that expands from the centre on press, for clickable labels. */
|
|
61
|
+
const rippleStyles = (theme: Theme): CSSObject => ({
|
|
62
|
+
overflow: 'hidden',
|
|
63
|
+
'&::after': {
|
|
64
|
+
content: '""',
|
|
65
|
+
position: 'absolute',
|
|
66
|
+
top: '50%',
|
|
67
|
+
left: '50%',
|
|
68
|
+
width: 0,
|
|
69
|
+
height: 0,
|
|
70
|
+
borderRadius: '50%',
|
|
71
|
+
backgroundColor: alpha(theme.palette.primary.main, 0.3),
|
|
72
|
+
transform: 'translate(-50%, -50%)',
|
|
73
|
+
transition: 'width 0.6s, height 0.6s',
|
|
74
|
+
},
|
|
75
|
+
'&:active::after': { width: '300%', height: '300%' },
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
interface SurfaceInput {
|
|
79
|
+
theme: Theme;
|
|
80
|
+
flags: LabelStyleFlags;
|
|
81
|
+
/** The resolved text colour, or the primary accent when it resolves to none. */
|
|
82
|
+
accent: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Each variant's own additions. Every arm previously re-spread the same base,
|
|
87
|
+
* glow, pulse and ripple objects and re-derived the colour for each use; the
|
|
88
|
+
* shared prefix is applied once by `labelStyles`.
|
|
89
|
+
*
|
|
90
|
+
* The hover rules are only attached when the label is clickable, which is what
|
|
91
|
+
* the `onClick ? {…} : {}` in every arm was saying.
|
|
92
|
+
*/
|
|
93
|
+
const SURFACES: Record<Variant, (input: SurfaceInput) => CSSObject> = {
|
|
94
|
+
filled: ({ theme, flags, accent }) => ({
|
|
95
|
+
backgroundColor: alpha(accent, 0.1),
|
|
96
|
+
borderRadius: theme.spacing(0.5),
|
|
97
|
+
'&:hover': flags.clickable ? { backgroundColor: alpha(accent, 0.15) } : {},
|
|
98
|
+
}),
|
|
99
|
+
|
|
100
|
+
outlined: ({ theme, flags, accent }) => ({
|
|
101
|
+
border: `1px solid ${alpha(accent, 0.5)}`,
|
|
102
|
+
borderRadius: theme.spacing(0.5),
|
|
103
|
+
'&:hover': flags.clickable
|
|
104
|
+
? { borderColor: accent, backgroundColor: alpha(accent, 0.05) }
|
|
105
|
+
: {},
|
|
106
|
+
}),
|
|
107
|
+
|
|
108
|
+
glass: ({ theme, flags }) => ({
|
|
109
|
+
backgroundColor: alpha(theme.palette.background.paper, 0.1),
|
|
110
|
+
backdropFilter: 'blur(10px)',
|
|
111
|
+
border: `1px solid ${alpha(theme.palette.divider, 0.2)}`,
|
|
112
|
+
borderRadius: theme.spacing(0.5),
|
|
113
|
+
'&:hover': flags.clickable
|
|
114
|
+
? {
|
|
115
|
+
backgroundColor: alpha(theme.palette.background.paper, 0.15),
|
|
116
|
+
borderColor: alpha(theme.palette.primary.main, 0.3),
|
|
117
|
+
}
|
|
118
|
+
: {},
|
|
119
|
+
}),
|
|
120
|
+
|
|
121
|
+
gradient: ({ theme, flags }) => {
|
|
122
|
+
const key = flags.color === 'default' ? 'primary' : flags.color;
|
|
123
|
+
// The gradient paints the glyphs themselves rather than the box behind them.
|
|
124
|
+
const clip = {
|
|
125
|
+
WebkitBackgroundClip: 'text',
|
|
126
|
+
WebkitTextFillColor: 'transparent',
|
|
127
|
+
backgroundClip: 'text',
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
background: `linear-gradient(135deg, ${theme.palette[key].light}, ${theme.palette[key].main})`,
|
|
132
|
+
...clip,
|
|
133
|
+
'&:hover': flags.clickable
|
|
134
|
+
? {
|
|
135
|
+
background: `linear-gradient(135deg, ${theme.palette[key].main}, ${theme.palette[key].dark})`,
|
|
136
|
+
...clip,
|
|
137
|
+
}
|
|
138
|
+
: {},
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
minimal: ({ theme, flags }) => ({
|
|
143
|
+
padding: 0,
|
|
144
|
+
'&:hover': flags.clickable ? { color: theme.palette.primary.main } : {},
|
|
145
|
+
}),
|
|
146
|
+
|
|
147
|
+
default: () => ({}),
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/** How the text sits: its alignment, weight, casing and overflow behaviour. */
|
|
151
|
+
const textLayout = (theme: Theme, flags: LabelStyleFlags): CSSObject => {
|
|
152
|
+
const { srOnly, clickable, align, weight, transform, nowrap, truncate } = flags;
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
display: srOnly ? 'none' : 'inline-flex',
|
|
156
|
+
alignItems: 'center',
|
|
157
|
+
gap: theme.spacing(0.5),
|
|
158
|
+
transition: theme.transitions.create(['all'], {
|
|
159
|
+
duration: theme.transitions.duration.standard,
|
|
160
|
+
}),
|
|
161
|
+
cursor: clickable ? 'pointer' : 'default',
|
|
162
|
+
textAlign: align,
|
|
163
|
+
fontWeight: fontWeight(weight),
|
|
164
|
+
textTransform: transform,
|
|
165
|
+
whiteSpace: nowrap ? 'nowrap' : 'normal',
|
|
166
|
+
overflow: truncate ? 'hidden' : 'visible',
|
|
167
|
+
textOverflow: truncate ? 'ellipsis' : 'clip',
|
|
168
|
+
position: 'relative' as const,
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export const labelStyles = (theme: Theme, flags: LabelStyleFlags): CSSObject => {
|
|
173
|
+
const { variant, size, clickable } = flags;
|
|
174
|
+
const color = textColor(theme, flags);
|
|
175
|
+
const accent = color || theme.palette.primary.main;
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
...textLayout(theme, flags),
|
|
179
|
+
...sizeStyles(theme, size),
|
|
180
|
+
color,
|
|
181
|
+
...(flags.glow && { textShadow: `0 0 10px ${alpha(accent, 0.5)}` }),
|
|
182
|
+
...(flags.pulse && {
|
|
183
|
+
animation: 'pulse 2s infinite',
|
|
184
|
+
'@keyframes pulse': {
|
|
185
|
+
'0%': { opacity: 1 },
|
|
186
|
+
'50%': { opacity: 0.6 },
|
|
187
|
+
'100%': { opacity: 1 },
|
|
188
|
+
},
|
|
189
|
+
}),
|
|
190
|
+
...(flags.ripple && clickable && rippleStyles(theme)),
|
|
191
|
+
...(SURFACES[variant] ?? SURFACES.default)({ theme, flags, accent }),
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/** Visually hidden but still read aloud, for the `srOnly` label. */
|
|
196
|
+
export const SR_ONLY_SX: CSSObject = {
|
|
197
|
+
position: 'absolute',
|
|
198
|
+
width: '1px',
|
|
199
|
+
height: '1px',
|
|
200
|
+
padding: 0,
|
|
201
|
+
margin: '-1px',
|
|
202
|
+
overflow: 'hidden',
|
|
203
|
+
clip: 'rect(0, 0, 0, 0)',
|
|
204
|
+
whiteSpace: 'nowrap',
|
|
205
|
+
border: 0,
|
|
206
|
+
};
|
|
@@ -2,7 +2,7 @@ import InfoIcon from '@mui/icons-material/Info';
|
|
|
2
2
|
import { Box, Stack,TextField } from '@mui/material';
|
|
3
3
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
4
4
|
import React from 'react';
|
|
5
|
-
import { expect, fn,userEvent, waitFor, within } from 'storybook/test';
|
|
5
|
+
import { expect, fireEvent, fn,userEvent, waitFor, within } from 'storybook/test';
|
|
6
6
|
|
|
7
7
|
import { Label } from './Label';
|
|
8
8
|
|
|
@@ -247,23 +247,23 @@ export const KeyboardNavigation: Story = {
|
|
|
247
247
|
const firstInput = firstInputContainer.querySelector('input');
|
|
248
248
|
|
|
249
249
|
// Focus first label
|
|
250
|
-
|
|
251
|
-
await expect(firstLabel).toHaveFocus();
|
|
250
|
+
await userEvent.click(firstLabel);
|
|
251
|
+
await waitFor(() => expect(firstLabel).toHaveFocus());
|
|
252
252
|
|
|
253
253
|
// Tab to input
|
|
254
254
|
await userEvent.tab();
|
|
255
|
-
await expect(firstInput).toHaveFocus();
|
|
255
|
+
await waitFor(() => expect(firstInput).toHaveFocus());
|
|
256
256
|
});
|
|
257
257
|
|
|
258
258
|
await step('Tab navigation backward', async () => {
|
|
259
259
|
await userEvent.tab({ shift: true });
|
|
260
260
|
const firstLabel = canvas.getByTestId('first-label');
|
|
261
|
-
await expect(firstLabel).toHaveFocus();
|
|
261
|
+
await waitFor(() => expect(firstLabel).toHaveFocus());
|
|
262
262
|
});
|
|
263
263
|
|
|
264
264
|
await step('Enter key activation on clickable label', async () => {
|
|
265
265
|
const firstLabel = canvas.getByTestId('first-label');
|
|
266
|
-
|
|
266
|
+
await userEvent.click(firstLabel);
|
|
267
267
|
await userEvent.keyboard('{Enter}');
|
|
268
268
|
// Verify click handler would be called
|
|
269
269
|
});
|
|
@@ -641,14 +641,20 @@ export const PerformanceTest: Story = {
|
|
|
641
641
|
await step('Test scroll performance', async () => {
|
|
642
642
|
const scrollContainer = canvas.getByTestId('scroll-container');
|
|
643
643
|
|
|
644
|
-
//
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
644
|
+
// Scrolling is the one thing this story is about, so it has to move the
|
|
645
|
+
// container; what it must not do is assert a pixel offset, which depends
|
|
646
|
+
// on the viewport the story happens to render in.
|
|
647
|
+
// eslint-disable-next-line test-flakiness/no-viewport-dependent -- the subject under test is scrolling itself
|
|
648
|
+
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
|
649
|
+
fireEvent.scroll(scrollContainer);
|
|
650
|
+
|
|
651
|
+
// The labels are not virtualised, so scrolling must leave every one of
|
|
652
|
+
// them mounted — which is what the old toBeInTheDocument() on the
|
|
653
|
+
// container itself could never have caught, since the container was
|
|
654
|
+
// already there before the loop ran.
|
|
655
|
+
await waitFor(() => {
|
|
656
|
+
expect(canvas.getAllByTestId(/^label-\d+$/)).toHaveLength(100);
|
|
657
|
+
});
|
|
652
658
|
});
|
|
653
659
|
},
|
|
654
660
|
};
|
|
@@ -1,296 +1,51 @@
|
|
|
1
|
-
import {
|
|
2
|
-
alpha,
|
|
3
|
-
Box,
|
|
4
|
-
CircularProgress,
|
|
5
|
-
Tooltip,
|
|
6
|
-
Typography,
|
|
7
|
-
useTheme,
|
|
8
|
-
} from '@mui/material';
|
|
1
|
+
import { Box, Tooltip, Typography, useTheme } from '@mui/material';
|
|
9
2
|
import React, { forwardRef } from 'react';
|
|
10
3
|
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
export const Label = forwardRef<globalThis.HTMLLabelElement, LabelProps>(({
|
|
14
|
-
children,
|
|
15
|
-
htmlFor,
|
|
16
|
-
required = false,
|
|
17
|
-
disabled = false,
|
|
18
|
-
error = false,
|
|
19
|
-
variant = 'default',
|
|
20
|
-
size = 'md',
|
|
21
|
-
color = 'default',
|
|
22
|
-
glow = false,
|
|
23
|
-
pulse = false,
|
|
24
|
-
ripple = false,
|
|
25
|
-
loading = false,
|
|
26
|
-
className,
|
|
27
|
-
style,
|
|
28
|
-
tooltip,
|
|
29
|
-
helperText,
|
|
30
|
-
asteriskPlacement = 'end',
|
|
31
|
-
icon,
|
|
32
|
-
iconPosition = 'start',
|
|
33
|
-
onClick,
|
|
34
|
-
onFocus,
|
|
35
|
-
onBlur,
|
|
36
|
-
srOnly = false,
|
|
37
|
-
weight = 'regular',
|
|
38
|
-
transform = 'none',
|
|
39
|
-
align = 'left',
|
|
40
|
-
nowrap = false,
|
|
41
|
-
truncate = false,
|
|
42
|
-
dataTestId,
|
|
43
|
-
...props
|
|
44
|
-
}, ref) => {
|
|
45
|
-
const theme = useTheme();
|
|
46
|
-
|
|
47
|
-
const getSizeStyles = () => {
|
|
48
|
-
switch (size) {
|
|
49
|
-
case 'xs':
|
|
50
|
-
return {
|
|
51
|
-
fontSize: '0.75rem',
|
|
52
|
-
lineHeight: 1.2,
|
|
53
|
-
padding: theme.spacing(0.25, 0.5),
|
|
54
|
-
};
|
|
55
|
-
case 'sm':
|
|
56
|
-
return {
|
|
57
|
-
fontSize: '0.875rem',
|
|
58
|
-
lineHeight: 1.3,
|
|
59
|
-
padding: theme.spacing(0.5, 0.75),
|
|
60
|
-
};
|
|
61
|
-
case 'md':
|
|
62
|
-
return {
|
|
63
|
-
fontSize: '1rem',
|
|
64
|
-
lineHeight: 1.5,
|
|
65
|
-
padding: theme.spacing(0.75, 1),
|
|
66
|
-
};
|
|
67
|
-
case 'lg':
|
|
68
|
-
return {
|
|
69
|
-
fontSize: '1.125rem',
|
|
70
|
-
lineHeight: 1.6,
|
|
71
|
-
padding: theme.spacing(1, 1.25),
|
|
72
|
-
};
|
|
73
|
-
case 'xl':
|
|
74
|
-
return {
|
|
75
|
-
fontSize: '1.25rem',
|
|
76
|
-
lineHeight: 1.7,
|
|
77
|
-
padding: theme.spacing(1.25, 1.5),
|
|
78
|
-
};
|
|
79
|
-
default:
|
|
80
|
-
return {
|
|
81
|
-
fontSize: '1rem',
|
|
82
|
-
lineHeight: 1.5,
|
|
83
|
-
padding: theme.spacing(0.75, 1),
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const getColorStyles = () => {
|
|
89
|
-
if (error) {
|
|
90
|
-
return { color: theme.palette.error.main };
|
|
91
|
-
}
|
|
92
|
-
if (disabled) {
|
|
93
|
-
return { color: theme.palette.text.disabled };
|
|
94
|
-
}
|
|
95
|
-
if (color === 'default') {
|
|
96
|
-
return { color: theme.palette.text.primary };
|
|
97
|
-
}
|
|
98
|
-
return { color: theme.palette[color].main };
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const getVariantStyles = () => {
|
|
102
|
-
const baseStyles = {
|
|
103
|
-
display: srOnly ? 'none' : 'inline-flex',
|
|
104
|
-
alignItems: 'center',
|
|
105
|
-
gap: theme.spacing(0.5),
|
|
106
|
-
transition: theme.transitions.create(['all'], {
|
|
107
|
-
duration: theme.transitions.duration.standard,
|
|
108
|
-
}),
|
|
109
|
-
cursor: onClick ? 'pointer' : 'default',
|
|
110
|
-
textAlign: align,
|
|
111
|
-
fontWeight: getFontWeight(),
|
|
112
|
-
textTransform: transform,
|
|
113
|
-
whiteSpace: nowrap ? 'nowrap' : 'normal',
|
|
114
|
-
overflow: truncate ? 'hidden' : 'visible',
|
|
115
|
-
textOverflow: truncate ? 'ellipsis' : 'clip',
|
|
116
|
-
position: 'relative' as const,
|
|
117
|
-
...getSizeStyles(),
|
|
118
|
-
...getColorStyles(),
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const glowStyles = glow ? {
|
|
122
|
-
textShadow: `0 0 10px ${alpha(getColorStyles().color || theme.palette.primary.main, 0.5)}`,
|
|
123
|
-
} : {};
|
|
124
|
-
|
|
125
|
-
const pulseStyles = pulse ? {
|
|
126
|
-
animation: 'pulse 2s infinite',
|
|
127
|
-
'@keyframes pulse': {
|
|
128
|
-
'0%': { opacity: 1 },
|
|
129
|
-
'50%': { opacity: 0.6 },
|
|
130
|
-
'100%': { opacity: 1 },
|
|
131
|
-
},
|
|
132
|
-
} : {};
|
|
133
|
-
|
|
134
|
-
const rippleStyles = ripple && onClick ? {
|
|
135
|
-
overflow: 'hidden',
|
|
136
|
-
'&::after': {
|
|
137
|
-
content: '""',
|
|
138
|
-
position: 'absolute',
|
|
139
|
-
top: '50%',
|
|
140
|
-
left: '50%',
|
|
141
|
-
width: 0,
|
|
142
|
-
height: 0,
|
|
143
|
-
borderRadius: '50%',
|
|
144
|
-
backgroundColor: alpha(theme.palette.primary.main, 0.3),
|
|
145
|
-
transform: 'translate(-50%, -50%)',
|
|
146
|
-
transition: 'width 0.6s, height 0.6s',
|
|
147
|
-
},
|
|
148
|
-
'&:active::after': {
|
|
149
|
-
width: '300%',
|
|
150
|
-
height: '300%',
|
|
151
|
-
},
|
|
152
|
-
} : {};
|
|
4
|
+
import { withDefaults } from '../../../utils/withDefaults';
|
|
153
5
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
...glowStyles,
|
|
159
|
-
...pulseStyles,
|
|
160
|
-
...rippleStyles,
|
|
161
|
-
backgroundColor: alpha(getColorStyles().color || theme.palette.primary.main, 0.1),
|
|
162
|
-
borderRadius: theme.spacing(0.5),
|
|
163
|
-
'&:hover': onClick ? {
|
|
164
|
-
backgroundColor: alpha(getColorStyles().color || theme.palette.primary.main, 0.15),
|
|
165
|
-
} : {},
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
case 'outlined':
|
|
169
|
-
return {
|
|
170
|
-
...baseStyles,
|
|
171
|
-
...glowStyles,
|
|
172
|
-
...pulseStyles,
|
|
173
|
-
...rippleStyles,
|
|
174
|
-
border: `1px solid ${alpha(getColorStyles().color || theme.palette.primary.main, 0.5)}`,
|
|
175
|
-
borderRadius: theme.spacing(0.5),
|
|
176
|
-
'&:hover': onClick ? {
|
|
177
|
-
borderColor: getColorStyles().color || theme.palette.primary.main,
|
|
178
|
-
backgroundColor: alpha(getColorStyles().color || theme.palette.primary.main, 0.05),
|
|
179
|
-
} : {},
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
case 'glass':
|
|
183
|
-
return {
|
|
184
|
-
...baseStyles,
|
|
185
|
-
...glowStyles,
|
|
186
|
-
...pulseStyles,
|
|
187
|
-
...rippleStyles,
|
|
188
|
-
backgroundColor: alpha(theme.palette.background.paper, 0.1),
|
|
189
|
-
backdropFilter: 'blur(10px)',
|
|
190
|
-
border: `1px solid ${alpha(theme.palette.divider, 0.2)}`,
|
|
191
|
-
borderRadius: theme.spacing(0.5),
|
|
192
|
-
'&:hover': onClick ? {
|
|
193
|
-
backgroundColor: alpha(theme.palette.background.paper, 0.15),
|
|
194
|
-
borderColor: alpha(theme.palette.primary.main, 0.3),
|
|
195
|
-
} : {},
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
case 'gradient': {
|
|
199
|
-
const gradientColor = color === 'default' ? 'primary' : color;
|
|
200
|
-
return {
|
|
201
|
-
...baseStyles,
|
|
202
|
-
...glowStyles,
|
|
203
|
-
...pulseStyles,
|
|
204
|
-
...rippleStyles,
|
|
205
|
-
background: `linear-gradient(135deg, ${theme.palette[gradientColor].light}, ${theme.palette[gradientColor].main})`,
|
|
206
|
-
WebkitBackgroundClip: 'text',
|
|
207
|
-
WebkitTextFillColor: 'transparent',
|
|
208
|
-
backgroundClip: 'text',
|
|
209
|
-
'&:hover': onClick ? {
|
|
210
|
-
background: `linear-gradient(135deg, ${theme.palette[gradientColor].main}, ${theme.palette[gradientColor].dark})`,
|
|
211
|
-
WebkitBackgroundClip: 'text',
|
|
212
|
-
WebkitTextFillColor: 'transparent',
|
|
213
|
-
backgroundClip: 'text',
|
|
214
|
-
} : {},
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
case 'minimal':
|
|
219
|
-
return {
|
|
220
|
-
...baseStyles,
|
|
221
|
-
...glowStyles,
|
|
222
|
-
...pulseStyles,
|
|
223
|
-
...rippleStyles,
|
|
224
|
-
padding: 0,
|
|
225
|
-
'&:hover': onClick ? {
|
|
226
|
-
color: theme.palette.primary.main,
|
|
227
|
-
} : {},
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
default:
|
|
231
|
-
return {
|
|
232
|
-
...baseStyles,
|
|
233
|
-
...glowStyles,
|
|
234
|
-
...pulseStyles,
|
|
235
|
-
...rippleStyles,
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
};
|
|
6
|
+
import { LabelBody } from './Label.parts';
|
|
7
|
+
import { labelStyles, sizeStyles, SR_ONLY_SX, textColor } from './Label.styles';
|
|
8
|
+
import type { LabelStyleFlags } from './Label.styles';
|
|
9
|
+
import type { LabelProps } from './Label.types';
|
|
239
10
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
11
|
+
const DEFAULTS = {
|
|
12
|
+
required: false,
|
|
13
|
+
disabled: false,
|
|
14
|
+
error: false,
|
|
15
|
+
variant: 'default',
|
|
16
|
+
size: 'md',
|
|
17
|
+
color: 'default',
|
|
18
|
+
glow: false,
|
|
19
|
+
pulse: false,
|
|
20
|
+
ripple: false,
|
|
21
|
+
loading: false,
|
|
22
|
+
asteriskPlacement: 'end',
|
|
23
|
+
iconPosition: 'start',
|
|
24
|
+
srOnly: false,
|
|
25
|
+
weight: 'regular',
|
|
26
|
+
transform: 'none',
|
|
27
|
+
align: 'left',
|
|
28
|
+
nowrap: false,
|
|
29
|
+
truncate: false,
|
|
30
|
+
} satisfies Partial<LabelProps>;
|
|
31
|
+
|
|
32
|
+
type ResolvedProps = LabelProps & Required<Pick<LabelProps, keyof typeof DEFAULTS>>;
|
|
33
|
+
|
|
34
|
+
export const Label = forwardRef<globalThis.HTMLLabelElement, LabelProps>((props, ref) => {
|
|
35
|
+
const {
|
|
36
|
+
children, htmlFor, required, disabled, error, variant, size, color,
|
|
37
|
+
glow, pulse, ripple, loading, className, style, tooltip, helperText,
|
|
38
|
+
asteriskPlacement, icon, iconPosition, onClick, onFocus, onBlur,
|
|
39
|
+
srOnly, weight, transform, align, nowrap, truncate, dataTestId,
|
|
40
|
+
...rest
|
|
41
|
+
} = withDefaults(props, DEFAULTS) as ResolvedProps;
|
|
250
42
|
|
|
251
|
-
const
|
|
252
|
-
if (!required) return null;
|
|
253
|
-
return (
|
|
254
|
-
<Box
|
|
255
|
-
component="span"
|
|
256
|
-
sx={{
|
|
257
|
-
color: theme.palette.error.main,
|
|
258
|
-
marginLeft: asteriskPlacement === 'end' ? theme.spacing(0.5) : 0,
|
|
259
|
-
marginRight: asteriskPlacement === 'start' ? theme.spacing(0.5) : 0,
|
|
260
|
-
}}
|
|
261
|
-
>
|
|
262
|
-
*
|
|
263
|
-
</Box>
|
|
264
|
-
);
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
const renderIcon = () => {
|
|
268
|
-
if (!icon) return null;
|
|
269
|
-
return (
|
|
270
|
-
<Box
|
|
271
|
-
component="span"
|
|
272
|
-
sx={{
|
|
273
|
-
display: 'inline-flex',
|
|
274
|
-
alignItems: 'center',
|
|
275
|
-
fontSize: 'inherit',
|
|
276
|
-
}}
|
|
277
|
-
>
|
|
278
|
-
{icon}
|
|
279
|
-
</Box>
|
|
280
|
-
);
|
|
281
|
-
};
|
|
43
|
+
const theme = useTheme();
|
|
282
44
|
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
size={getSizeStyles().fontSize}
|
|
288
|
-
sx={{
|
|
289
|
-
color: getColorStyles().color,
|
|
290
|
-
marginLeft: theme.spacing(0.5),
|
|
291
|
-
}}
|
|
292
|
-
/>
|
|
293
|
-
);
|
|
45
|
+
const flags: LabelStyleFlags = {
|
|
46
|
+
variant, size, color, weight, transform, align,
|
|
47
|
+
error, disabled, glow, pulse, ripple, srOnly, nowrap, truncate,
|
|
48
|
+
clickable: Boolean(onClick),
|
|
294
49
|
};
|
|
295
50
|
|
|
296
51
|
const labelContent = (
|
|
@@ -303,51 +58,44 @@ export const Label = forwardRef<globalThis.HTMLLabelElement, LabelProps>(({
|
|
|
303
58
|
onBlur={onBlur}
|
|
304
59
|
className={className}
|
|
305
60
|
data-testid={dataTestId}
|
|
306
|
-
sx={{
|
|
307
|
-
|
|
308
|
-
...style,
|
|
309
|
-
}}
|
|
310
|
-
{...props}
|
|
61
|
+
sx={{ ...labelStyles(theme, flags), ...style }}
|
|
62
|
+
{...rest}
|
|
311
63
|
>
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
64
|
+
<LabelBody
|
|
65
|
+
required={required}
|
|
66
|
+
asteriskPlacement={asteriskPlacement}
|
|
67
|
+
icon={icon}
|
|
68
|
+
iconPosition={iconPosition}
|
|
69
|
+
loading={loading}
|
|
70
|
+
fontSize={sizeStyles(theme, size).fontSize as string}
|
|
71
|
+
color={textColor(theme, flags)}
|
|
72
|
+
>
|
|
73
|
+
{children}
|
|
74
|
+
</LabelBody>
|
|
318
75
|
</Box>
|
|
319
76
|
);
|
|
320
77
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
78
|
+
// A screen-reader-only label is rendered on its own: the tooltip, the styling
|
|
79
|
+
// and the helper text all describe something nothing can see.
|
|
80
|
+
if (srOnly) {
|
|
81
|
+
return (
|
|
82
|
+
<Box component="label" htmlFor={htmlFor} data-testid={dataTestId} sx={SR_ONLY_SX}>
|
|
83
|
+
{children}
|
|
84
|
+
</Box>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
326
87
|
|
|
327
88
|
return (
|
|
328
89
|
<>
|
|
329
|
-
{
|
|
330
|
-
<
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
position: 'absolute',
|
|
336
|
-
width: '1px',
|
|
337
|
-
height: '1px',
|
|
338
|
-
padding: 0,
|
|
339
|
-
margin: '-1px',
|
|
340
|
-
overflow: 'hidden',
|
|
341
|
-
clip: 'rect(0, 0, 0, 0)',
|
|
342
|
-
whiteSpace: 'nowrap',
|
|
343
|
-
border: 0,
|
|
344
|
-
}}
|
|
345
|
-
>
|
|
346
|
-
{children}
|
|
347
|
-
</Box>
|
|
90
|
+
{tooltip ? (
|
|
91
|
+
<Tooltip title={tooltip} arrow>
|
|
92
|
+
{labelContent}
|
|
93
|
+
</Tooltip>
|
|
94
|
+
) : (
|
|
95
|
+
labelContent
|
|
348
96
|
)}
|
|
349
|
-
|
|
350
|
-
{helperText &&
|
|
97
|
+
|
|
98
|
+
{helperText && (
|
|
351
99
|
<Typography
|
|
352
100
|
variant="caption"
|
|
353
101
|
sx={{
|
|
@@ -364,4 +112,4 @@ export const Label = forwardRef<globalThis.HTMLLabelElement, LabelProps>(({
|
|
|
364
112
|
);
|
|
365
113
|
});
|
|
366
114
|
|
|
367
|
-
Label.displayName = 'Label';
|
|
115
|
+
Label.displayName = 'Label';
|