aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src/editor/AnimatedButton.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/frontend/src/editor/AnimatedButton.tsx')
-rw-r--r--subprojects/frontend/src/editor/AnimatedButton.tsx95
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 @@
1import Box from '@mui/material/Box';
2import Button from '@mui/material/Button';
3import { styled } from '@mui/material/styles';
4import React, { type ReactNode, useLayoutEffect, useState } from 'react';
5
6const 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
31export 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
90AnimatedButton.defaultProps = {
91 'aria-label': undefined,
92 onClick: undefined,
93 disabled: false,
94 children: undefined,
95};