summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src/mui/Button.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-webextension/src/mui/Button.tsx')
-rw-r--r--packages/taler-wallet-webextension/src/mui/Button.tsx405
1 files changed, 405 insertions, 0 deletions
diff --git a/packages/taler-wallet-webextension/src/mui/Button.tsx b/packages/taler-wallet-webextension/src/mui/Button.tsx
new file mode 100644
index 000000000..1af281d42
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/Button.tsx
@@ -0,0 +1,405 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { ComponentChildren, h, VNode, JSX } from "preact";
+import { css } from "@linaria/core";
+// eslint-disable-next-line import/extensions
+import {
+ theme,
+ Colors,
+ rippleEnabled,
+ rippleEnabledOutlined,
+} from "./style.js";
+// eslint-disable-next-line import/extensions
+import { alpha } from "./colors/manipulation.js";
+import { useState } from "preact/hooks";
+import { SafeHandler } from "./handlers.js";
+
+export const buttonBaseStyle = css`
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ box-sizing: border-box;
+ background-color: transparent;
+ outline: 0;
+ border: 0;
+ margin: 0;
+ border-radius: 0;
+ padding: 0;
+ cursor: pointer;
+ user-select: none;
+ vertical-align: middle;
+ text-decoration: none;
+ color: inherit;
+`;
+
+interface Props {
+ children?: ComponentChildren;
+ disabled?: boolean;
+ disableElevation?: boolean;
+ disableFocusRipple?: boolean;
+ endIcon?: string | VNode;
+ fullWidth?: boolean;
+ style?: h.JSX.CSSProperties;
+ href?: string;
+ size?: "small" | "medium" | "large";
+ startIcon?: VNode | string;
+ variant?: "contained" | "outlined" | "text";
+ tooltip?: string;
+ color?: Colors;
+ onClick?: () => Promise<void>;
+ // onClick?: SafeHandler<void>;
+}
+
+const button = css`
+ min-width: 64px;
+ &:hover {
+ text-decoration: none;
+ background-color: var(--text-primary-alpha-opacity);
+ @media (hover: none) {
+ background-color: transparent;
+ }
+ }
+ &:disabled {
+ color: ${theme.palette.action.disabled};
+ }
+`;
+const colorIconVariant = {
+ outlined: css`
+ fill: var(--color-main);
+ `,
+ contained: css`
+ fill: var(--color-contrastText);
+ `,
+ text: css`
+ fill: var(--color-main);
+ `,
+};
+
+const colorVariant = {
+ outlined: css`
+ color: var(--color-main);
+ border: 1px solid var(--color-main-alpha-half);
+ background-color: var(--color-contrastText);
+ &:hover {
+ border: 1px solid var(--color-main);
+ background-color: var(--color-main-alpha-opacity);
+ }
+ &:disabled {
+ border: 1px solid ${theme.palette.action.disabledBackground};
+ }
+ `,
+ contained: css`
+ color: var(--color-contrastText);
+ background-color: var(--color-main);
+ box-shadow: ${theme.shadows[2]};
+ &:hover {
+ background-color: var(--color-grey-or-dark);
+ }
+ &:active {
+ box-shadow: ${theme.shadows[8]};
+ }
+ &:focus-visible {
+ box-shadow: ${theme.shadows[6]};
+ }
+ &:disabled {
+ color: ${theme.palette.action.disabled};
+ box-shadow: ${theme.shadows[0]};
+ background-color: ${theme.palette.action.disabledBackground};
+ }
+ `,
+ text: css`
+ color: var(--color-main);
+ &:hover {
+ background-color: var(--color-main-alpha-opacity);
+ }
+ `,
+};
+
+const sizeIconVariant = {
+ outlined: {
+ small: css`
+ padding: 3px;
+ font-size: ${theme.pxToRem(7)};
+ `,
+ medium: css`
+ padding: 5px;
+ `,
+ large: css`
+ padding: 7px;
+ font-size: ${theme.pxToRem(10)};
+ `,
+ },
+ contained: {
+ small: css`
+ padding: 4px;
+ font-size: ${theme.pxToRem(13)};
+ `,
+ medium: css`
+ padding: 6px;
+ `,
+ large: css`
+ padding: 8px;
+ font-size: ${theme.pxToRem(10)};
+ `,
+ },
+ text: {
+ small: css`
+ padding: 4px;
+ font-size: ${theme.pxToRem(13)};
+ `,
+ medium: css`
+ padding: 6px;
+ `,
+ large: css`
+ padding: 8px;
+ font-size: ${theme.pxToRem(15)};
+ `,
+ },
+};
+const sizeVariant = {
+ outlined: {
+ small: css`
+ padding: 3px 9px;
+ font-size: ${theme.pxToRem(13)};
+ `,
+ medium: css`
+ padding: 5px 15px;
+ `,
+ large: css`
+ padding: 7px 21px;
+ font-size: ${theme.pxToRem(15)};
+ `,
+ },
+ contained: {
+ small: css`
+ padding: 4px 10px;
+ font-size: ${theme.pxToRem(13)};
+ `,
+ medium: css`
+ padding: 6px 16px;
+ `,
+ large: css`
+ padding: 8px 22px;
+ font-size: ${theme.pxToRem(15)};
+ `,
+ },
+ text: {
+ small: css`
+ padding: 4px 5px;
+ font-size: ${theme.pxToRem(13)};
+ `,
+ medium: css`
+ padding: 6px 8px;
+ `,
+ large: css`
+ padding: 8px 11px;
+ font-size: ${theme.pxToRem(15)};
+ `,
+ },
+};
+
+const fullWidthStyle = css`
+ width: 100%;
+`;
+
+export function Button({
+ children,
+ disabled,
+ startIcon: sip,
+ endIcon: eip,
+ fullWidth,
+ tooltip,
+ variant = "text",
+ size = "medium",
+ style: parentStyle,
+ color = "primary",
+ onClick: doClick,
+}: Props): VNode {
+ const style = css`
+ user-select: none;
+ width: 24px;
+ height: 24px;
+ display: inline-block;
+ fill: currentColor;
+ flex-shrink: 0;
+ transition: fill 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
+
+ & > svg {
+ font-size: 20;
+ }
+ `;
+
+ const startIcon = sip && (
+ <span
+ class={[
+ css`
+ margin-right: 8px;
+ margin-left: -4px;
+ mask: var(--image) no-repeat center;
+ `,
+ colorIconVariant[variant],
+ sizeIconVariant[variant][size],
+ style,
+ ].join(" ")}
+ //FIXME: check when sip can be a vnode
+ dangerouslySetInnerHTML={{ __html: sip as string }}
+ style={{
+ "--color-main": theme.palette[color].main,
+ "--color-contrastText": theme.palette[color].contrastText,
+ }}
+ />
+ );
+ const endIcon = eip && (
+ <span
+ class={[
+ css`
+ margin-right: -4px;
+ margin-left: 8px;
+ mask: var(--image) no-repeat center;
+ `,
+ colorIconVariant[variant],
+ sizeIconVariant[variant][size],
+ style,
+ ].join(" ")}
+ dangerouslySetInnerHTML={{ __html: eip as string }}
+ style={{
+ "--color-main": theme.palette[color].main,
+ "--color-contrastText": theme.palette[color].contrastText,
+ "--color-dark": theme.palette[color].dark,
+ }}
+ />
+ );
+ const [running, setRunning] = useState(false);
+
+ async function onClick(): Promise<void> {
+ if (!doClick || disabled || running) return;
+ setRunning(true);
+ try {
+ await doClick();
+ } finally {
+ setRunning(false);
+ }
+ }
+
+ return (
+ <ButtonBase
+ disabled={disabled || running || !doClick}
+ class={[
+ theme.typography.button,
+ theme.shape.roundBorder,
+ button,
+ fullWidth && fullWidthStyle,
+ colorVariant[variant],
+ sizeVariant[variant][size],
+ ].join(" ")}
+ containedRipple={variant === "contained"}
+ onClick={onClick}
+ style={{
+ ...parentStyle,
+ "--color-main": theme.palette[color].main,
+ "--color-contrastText": theme.palette[color].contrastText,
+ "--color-main-alpha-half": alpha(theme.palette[color].main, 0.5),
+ "--color-dark": theme.palette[color].dark,
+ "--color-light": theme.palette[color].light,
+ "--color-main-alpha-opacity": alpha(
+ theme.palette[color].main,
+ theme.palette.action.hoverOpacity,
+ ),
+ "--text-primary-alpha-opacity": alpha(
+ theme.palette.text.primary,
+ theme.palette.action.hoverOpacity,
+ ),
+ "--color-grey-or-dark": !color
+ ? theme.palette.grey.A100
+ : theme.palette[color].dark,
+ }}
+ title={tooltip}
+ >
+ {startIcon}
+ {children}
+ {endIcon}
+ </ButtonBase>
+ );
+}
+
+interface BaseProps extends JSX.HTMLAttributes<HTMLButtonElement> {
+ class: string;
+ onClick?: () => Promise<void>;
+ containedRipple?: boolean;
+ children?: ComponentChildren;
+ svg?: any;
+}
+
+function ButtonBase({
+ class: _class,
+ children,
+ containedRipple,
+ onClick,
+ svg,
+ ...rest
+}: BaseProps): VNode {
+ function doClick(): void {
+ if (onClick) onClick();
+ }
+ const classNames = [
+ buttonBaseStyle,
+ _class,
+ containedRipple ? rippleEnabled : rippleEnabledOutlined,
+ ].join(" ");
+ if (svg) {
+ return (
+ <button
+ onClick={doClick}
+ class={classNames}
+ dangerouslySetInnerHTML={{ __html: svg }}
+ {...rest}
+ />
+ );
+ }
+ return (
+ <button onClick={doClick} class={classNames} {...rest}>
+ {children}
+ </button>
+ );
+}
+
+export function IconButton({
+ svg,
+ onClick,
+}: {
+ svg: any;
+ onClick?: () => Promise<void>;
+}): VNode {
+ return (
+ <ButtonBase
+ onClick={onClick}
+ class={[
+ css`
+ text-align: center;
+ flex: 0 0 auto;
+ font-size: ${theme.typography.pxToRem(24)};
+ padding: 8px;
+ border-radius: 50%;
+ overflow: visible;
+ color: "inherit";
+ fill: currentColor;
+ `,
+ ].join(" ")}
+ svg={svg}
+ />
+ );
+}