diff options
Diffstat (limited to 'subprojects/frontend/src/editor/AnimatedButton.tsx')
-rw-r--r-- | subprojects/frontend/src/editor/AnimatedButton.tsx | 95 |
1 files changed, 95 insertions, 0 deletions
diff --git a/subprojects/frontend/src/editor/AnimatedButton.tsx b/subprojects/frontend/src/editor/AnimatedButton.tsx new file mode 100644 index 00000000..d08decbc --- /dev/null +++ b/subprojects/frontend/src/editor/AnimatedButton.tsx | |||
@@ -0,0 +1,95 @@ | |||
1 | import Box from '@mui/material/Box'; | ||
2 | import Button from '@mui/material/Button'; | ||
3 | import { styled } from '@mui/material/styles'; | ||
4 | import React, { type ReactNode, useLayoutEffect, useState } from 'react'; | ||
5 | |||
6 | const AnimatedButtonBase = styled(Button, { | ||
7 | shouldForwardProp: (prop) => prop !== 'width', | ||
8 | })<{ width: string }>(({ theme, width }) => { | ||
9 | // Transition copied from `@mui/material/Button`. | ||
10 | const colorTransition = theme.transitions.create( | ||
11 | ['background-color', 'box-shadow', 'border-color', 'color'], | ||
12 | { duration: theme.transitions.duration.short }, | ||
13 | ); | ||
14 | return { | ||
15 | width, | ||
16 | // Make sure the button does not change width if a number is updated. | ||
17 | fontVariantNumeric: 'tabular-nums', | ||
18 | transition: ` | ||
19 | ${colorTransition}, | ||
20 | ${theme.transitions.create(['width'], { | ||
21 | duration: theme.transitions.duration.short, | ||
22 | easing: theme.transitions.easing.easeOut, | ||
23 | })} | ||
24 | `, | ||
25 | '@media (prefers-reduced-motion: reduce)': { | ||
26 | transition: colorTransition, | ||
27 | }, | ||
28 | }; | ||
29 | }); | ||
30 | |||
31 | export default function AnimatedButton({ | ||
32 | 'aria-label': ariaLabel, | ||
33 | onClick, | ||
34 | color, | ||
35 | disabled, | ||
36 | startIcon, | ||
37 | children, | ||
38 | }: { | ||
39 | 'aria-label'?: string; | ||
40 | onClick?: () => void; | ||
41 | color: 'error' | 'warning' | 'primary' | 'inherit'; | ||
42 | disabled?: boolean; | ||
43 | startIcon: JSX.Element; | ||
44 | children?: ReactNode; | ||
45 | }): JSX.Element { | ||
46 | const [width, setWidth] = useState<string | undefined>(); | ||
47 | const [contentsElement, setContentsElement] = useState<HTMLDivElement | null>( | ||
48 | null, | ||
49 | ); | ||
50 | |||
51 | useLayoutEffect(() => { | ||
52 | if (contentsElement !== null) { | ||
53 | const updateWidth = () => { | ||
54 | setWidth(window.getComputedStyle(contentsElement).width); | ||
55 | }; | ||
56 | updateWidth(); | ||
57 | const observer = new ResizeObserver(updateWidth); | ||
58 | observer.observe(contentsElement); | ||
59 | return () => observer.unobserve(contentsElement); | ||
60 | } | ||
61 | return () => {}; | ||
62 | }, [setWidth, contentsElement]); | ||
63 | |||
64 | return ( | ||
65 | <AnimatedButtonBase | ||
66 | {...(ariaLabel === undefined ? {} : { 'aria-label': ariaLabel })} | ||
67 | {...(onClick === undefined ? {} : { onClick })} | ||
68 | color={color} | ||
69 | variant="outlined" | ||
70 | className="rounded" | ||
71 | disabled={disabled ?? false} | ||
72 | startIcon={startIcon} | ||
73 | width={width === undefined ? 'auto' : `calc(${width} + 50px)`} | ||
74 | > | ||
75 | <Box | ||
76 | display="flex" | ||
77 | flexDirection="row" | ||
78 | justifyContent="end" | ||
79 | overflow="hidden" | ||
80 | width="100%" | ||
81 | > | ||
82 | <Box whiteSpace="nowrap" ref={setContentsElement}> | ||
83 | {children} | ||
84 | </Box> | ||
85 | </Box> | ||
86 | </AnimatedButtonBase> | ||
87 | ); | ||
88 | } | ||
89 | |||
90 | AnimatedButton.defaultProps = { | ||
91 | 'aria-label': undefined, | ||
92 | onClick: undefined, | ||
93 | disabled: false, | ||
94 | children: undefined, | ||
95 | }; | ||