aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src/editor/AnimatedButton.tsx
blob: 24ec69be455d1cf019c7c212546a1f1cbd2d4332 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
/*
 * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/>
 *
 * SPDX-License-Identifier: EPL-2.0
 */

import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import { styled, type SxProps, type Theme } from '@mui/material/styles';
import { type ReactNode, useLayoutEffect, useState } from 'react';

const AnimatedButtonBase = styled(Button, {
  shouldForwardProp: (prop) => prop !== 'width',
})<{ width: string }>(({ theme, width }) => {
  // Transition copied from `@mui/material/Button`.
  const colorTransition = theme.transitions.create(
    ['background-color', 'box-shadow', 'border-color', 'color'],
    {
      duration: theme.transitions.duration.short,
    },
  );
  return {
    width,
    // Make sure the button does not change width if a number is updated.
    fontVariantNumeric: 'tabular-nums',
    transition: `
      ${colorTransition},
      ${theme.transitions.create(['width'], {
        duration: theme.transitions.duration.short,
      })}
    `,
    '@media (prefers-reduced-motion: reduce)': {
      transition: colorTransition,
    },
  };
});

export default function AnimatedButton({
  'aria-label': ariaLabel,
  onClick,
  color,
  disabled,
  startIcon,
  sx,
  children,
}: {
  'aria-label'?: string;
  onClick?: () => void;
  color: 'error' | 'warning' | 'primary' | 'inherit';
  disabled?: boolean;
  startIcon?: JSX.Element;
  sx?: SxProps<Theme> | undefined;
  children?: ReactNode;
}): JSX.Element {
  const [width, setWidth] = useState<string | undefined>();
  const [contentsElement, setContentsElement] = useState<HTMLDivElement | null>(
    null,
  );

  useLayoutEffect(() => {
    if (contentsElement !== null) {
      const updateWidth = () => {
        setWidth(window.getComputedStyle(contentsElement).width);
      };
      updateWidth();
      const observer = new ResizeObserver(updateWidth);
      observer.observe(contentsElement);
      return () => observer.unobserve(contentsElement);
    }
    return () => {};
  }, [setWidth, contentsElement]);

  return (
    <AnimatedButtonBase
      {...(ariaLabel === undefined ? {} : { 'aria-label': ariaLabel })}
      {...(onClick === undefined ? {} : { onClick })}
      {...(sx === undefined ? {} : { sx })}
      color={color}
      className="rounded shaded"
      disabled={disabled ?? false}
      startIcon={startIcon}
      width={
        width === undefined
          ? 'auto'
          : `calc(${width} + ${startIcon === undefined ? 28 : 50}px)`
      }
    >
      <Box
        display="flex"
        flexDirection="row"
        justifyContent="end"
        overflow="hidden"
        width="100%"
      >
        <Box whiteSpace="nowrap" ref={setContentsElement}>
          {children}
        </Box>
      </Box>
    </AnimatedButtonBase>
  );
}

AnimatedButton.defaultProps = {
  'aria-label': undefined,
  onClick: undefined,
  disabled: false,
  startIcon: undefined,
  sx: undefined,
  children: undefined,
};