2022-06-06 17:06:25 +02:00
|
|
|
/*
|
|
|
|
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/>
|
|
|
|
*/
|
2022-04-05 17:16:09 +02:00
|
|
|
import { ComponentChildren, h, VNode, JSX } from "preact";
|
2022-03-09 18:00:02 +01:00
|
|
|
import { css } from "@linaria/core";
|
2022-03-29 04:41:07 +02:00
|
|
|
// eslint-disable-next-line import/extensions
|
2023-02-17 20:29:09 +01:00
|
|
|
import {
|
|
|
|
theme,
|
|
|
|
Colors,
|
|
|
|
rippleEnabled,
|
|
|
|
rippleEnabledOutlined,
|
|
|
|
} from "./style.js";
|
2022-03-29 04:41:07 +02:00
|
|
|
// eslint-disable-next-line import/extensions
|
2023-02-16 03:23:42 +01:00
|
|
|
import { alpha } from "./colors/manipulation.js";
|
2022-08-18 17:48:05 +02:00
|
|
|
import { useState } from "preact/hooks";
|
2023-01-13 20:09:33 +01:00
|
|
|
import { SafeHandler } from "./handlers.js";
|
2022-03-09 18:00:02 +01:00
|
|
|
|
2022-06-24 16:42:21 +02:00
|
|
|
export const buttonBaseStyle = css`
|
2022-03-09 18:00:02 +01:00
|
|
|
display: inline-flex;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: center;
|
|
|
|
position: relative;
|
|
|
|
box-sizing: border-box;
|
|
|
|
background-color: transparent;
|
|
|
|
outline: 0;
|
|
|
|
border: 0;
|
|
|
|
margin: 0;
|
2022-04-05 17:16:09 +02:00
|
|
|
border-radius: 0;
|
2022-03-09 18:00:02 +01:00
|
|
|
padding: 0;
|
|
|
|
cursor: pointer;
|
|
|
|
user-select: none;
|
|
|
|
vertical-align: middle;
|
|
|
|
text-decoration: none;
|
|
|
|
color: inherit;
|
|
|
|
`;
|
|
|
|
|
2022-04-05 17:16:09 +02:00
|
|
|
interface Props {
|
|
|
|
children?: ComponentChildren;
|
|
|
|
disabled?: boolean;
|
|
|
|
disableElevation?: boolean;
|
|
|
|
disableFocusRipple?: boolean;
|
|
|
|
endIcon?: string | VNode;
|
|
|
|
fullWidth?: boolean;
|
2022-06-01 20:47:47 +02:00
|
|
|
style?: h.JSX.CSSProperties;
|
2022-04-05 17:16:09 +02:00
|
|
|
href?: string;
|
|
|
|
size?: "small" | "medium" | "large";
|
|
|
|
startIcon?: VNode | string;
|
|
|
|
variant?: "contained" | "outlined" | "text";
|
2022-08-17 21:12:21 +02:00
|
|
|
tooltip?: string;
|
2022-04-05 17:16:09 +02:00
|
|
|
color?: Colors;
|
2022-06-01 20:47:47 +02:00
|
|
|
onClick?: () => Promise<void>;
|
2023-01-13 20:09:33 +01:00
|
|
|
// onClick?: SafeHandler<void>;
|
2022-04-05 17:16:09 +02:00
|
|
|
}
|
|
|
|
|
2022-03-09 18:00:02 +01:00
|
|
|
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};
|
|
|
|
}
|
|
|
|
`;
|
2022-03-18 21:52:46 +01:00
|
|
|
const colorIconVariant = {
|
|
|
|
outlined: css`
|
2022-04-05 17:16:09 +02:00
|
|
|
fill: var(--color-main);
|
2022-03-18 21:52:46 +01:00
|
|
|
`,
|
|
|
|
contained: css`
|
2022-04-05 17:16:09 +02:00
|
|
|
fill: var(--color-contrastText);
|
2022-03-18 21:52:46 +01:00
|
|
|
`,
|
|
|
|
text: css`
|
2022-04-05 17:16:09 +02:00
|
|
|
fill: var(--color-main);
|
2022-03-18 21:52:46 +01:00
|
|
|
`,
|
|
|
|
};
|
2022-03-09 18:00:02 +01:00
|
|
|
|
|
|
|
const colorVariant = {
|
|
|
|
outlined: css`
|
|
|
|
color: var(--color-main);
|
|
|
|
border: 1px solid var(--color-main-alpha-half);
|
2022-04-05 17:16:09 +02:00
|
|
|
background-color: var(--color-contrastText);
|
2022-03-09 18:00:02 +01:00
|
|
|
&: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 {
|
2022-04-05 17:16:09 +02:00
|
|
|
background-color: var(--color-grey-or-dark);
|
2022-03-09 18:00:02 +01:00
|
|
|
}
|
|
|
|
&: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);
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
};
|
|
|
|
|
2022-03-18 21:52:46 +01:00
|
|
|
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)};
|
|
|
|
`,
|
|
|
|
},
|
|
|
|
};
|
2022-03-09 18:00:02 +01:00
|
|
|
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)};
|
|
|
|
`,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2022-04-05 17:16:09 +02:00
|
|
|
const fullWidthStyle = css`
|
|
|
|
width: 100%;
|
|
|
|
`;
|
|
|
|
|
2022-03-09 18:00:02 +01:00
|
|
|
export function Button({
|
|
|
|
children,
|
|
|
|
disabled,
|
|
|
|
startIcon: sip,
|
|
|
|
endIcon: eip,
|
2022-04-05 17:16:09 +02:00
|
|
|
fullWidth,
|
2022-08-17 21:12:21 +02:00
|
|
|
tooltip,
|
2022-03-09 18:00:02 +01:00
|
|
|
variant = "text",
|
|
|
|
size = "medium",
|
2022-06-01 20:47:47 +02:00
|
|
|
style: parentStyle,
|
2022-03-09 18:00:02 +01:00
|
|
|
color = "primary",
|
2022-08-18 17:48:05 +02:00
|
|
|
onClick: doClick,
|
2022-03-09 18:00:02 +01:00
|
|
|
}: Props): VNode {
|
|
|
|
const style = css`
|
|
|
|
user-select: none;
|
2022-04-05 17:16:09 +02:00
|
|
|
width: 24px;
|
|
|
|
height: 24px;
|
2022-03-09 18:00:02 +01:00
|
|
|
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;
|
2022-03-18 21:52:46 +01:00
|
|
|
mask: var(--image) no-repeat center;
|
2022-03-09 18:00:02 +01:00
|
|
|
`,
|
2022-03-18 21:52:46 +01:00
|
|
|
colorIconVariant[variant],
|
|
|
|
sizeIconVariant[variant][size],
|
2022-03-09 18:00:02 +01:00
|
|
|
style,
|
|
|
|
].join(" ")}
|
2022-04-05 17:16:09 +02:00
|
|
|
//FIXME: check when sip can be a vnode
|
|
|
|
dangerouslySetInnerHTML={{ __html: sip as string }}
|
2022-03-18 21:52:46 +01:00
|
|
|
style={{
|
|
|
|
"--color-main": theme.palette[color].main,
|
|
|
|
"--color-contrastText": theme.palette[color].contrastText,
|
|
|
|
}}
|
|
|
|
/>
|
2022-03-09 18:00:02 +01:00
|
|
|
);
|
|
|
|
const endIcon = eip && (
|
|
|
|
<span
|
|
|
|
class={[
|
|
|
|
css`
|
|
|
|
margin-right: -4px;
|
|
|
|
margin-left: 8px;
|
2022-03-18 21:52:46 +01:00
|
|
|
mask: var(--image) no-repeat center;
|
2022-03-09 18:00:02 +01:00
|
|
|
`,
|
2022-03-18 21:52:46 +01:00
|
|
|
colorIconVariant[variant],
|
|
|
|
sizeIconVariant[variant][size],
|
2022-03-09 18:00:02 +01:00
|
|
|
style,
|
|
|
|
].join(" ")}
|
2022-04-05 17:16:09 +02:00
|
|
|
dangerouslySetInnerHTML={{ __html: eip as string }}
|
2022-03-18 21:52:46 +01:00
|
|
|
style={{
|
|
|
|
"--color-main": theme.palette[color].main,
|
|
|
|
"--color-contrastText": theme.palette[color].contrastText,
|
|
|
|
"--color-dark": theme.palette[color].dark,
|
|
|
|
}}
|
|
|
|
/>
|
2022-03-09 18:00:02 +01:00
|
|
|
);
|
2022-08-18 17:48:05 +02:00
|
|
|
const [running, setRunning] = useState(false);
|
|
|
|
|
|
|
|
async function onClick(): Promise<void> {
|
|
|
|
if (!doClick || disabled || running) return;
|
|
|
|
setRunning(true);
|
|
|
|
try {
|
|
|
|
await doClick();
|
|
|
|
} finally {
|
|
|
|
setRunning(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-09 18:00:02 +01:00
|
|
|
return (
|
2022-04-05 17:16:09 +02:00
|
|
|
<ButtonBase
|
2022-11-08 17:00:34 +01:00
|
|
|
disabled={disabled || running || !doClick}
|
2022-03-09 18:00:02 +01:00
|
|
|
class={[
|
|
|
|
theme.typography.button,
|
2022-03-11 03:13:10 +01:00
|
|
|
theme.shape.roundBorder,
|
2022-03-09 18:00:02 +01:00
|
|
|
button,
|
2022-04-05 17:16:09 +02:00
|
|
|
fullWidth && fullWidthStyle,
|
2022-03-09 18:00:02 +01:00
|
|
|
colorVariant[variant],
|
|
|
|
sizeVariant[variant][size],
|
|
|
|
].join(" ")}
|
2022-06-01 20:47:47 +02:00
|
|
|
containedRipple={variant === "contained"}
|
2022-04-05 17:16:09 +02:00
|
|
|
onClick={onClick}
|
2022-03-09 18:00:02 +01:00
|
|
|
style={{
|
2022-06-01 20:47:47 +02:00
|
|
|
...parentStyle,
|
2022-03-09 18:00:02 +01:00
|
|
|
"--color-main": theme.palette[color].main,
|
|
|
|
"--color-contrastText": theme.palette[color].contrastText,
|
2022-03-18 21:52:46 +01:00
|
|
|
"--color-main-alpha-half": alpha(theme.palette[color].main, 0.5),
|
2022-03-09 18:00:02 +01:00
|
|
|
"--color-dark": theme.palette[color].dark,
|
2022-06-01 20:47:47 +02:00
|
|
|
"--color-light": theme.palette[color].light,
|
2022-03-09 18:00:02 +01:00
|
|
|
"--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,
|
|
|
|
),
|
2022-04-05 17:16:09 +02:00
|
|
|
"--color-grey-or-dark": !color
|
|
|
|
? theme.palette.grey.A100
|
|
|
|
: theme.palette[color].dark,
|
2022-03-09 18:00:02 +01:00
|
|
|
}}
|
2022-08-17 21:12:21 +02:00
|
|
|
title={tooltip}
|
2022-03-09 18:00:02 +01:00
|
|
|
>
|
|
|
|
{startIcon}
|
|
|
|
{children}
|
|
|
|
{endIcon}
|
2022-04-05 17:16:09 +02:00
|
|
|
</ButtonBase>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
interface BaseProps extends JSX.HTMLAttributes<HTMLButtonElement> {
|
|
|
|
class: string;
|
2022-06-01 20:47:47 +02:00
|
|
|
onClick?: () => Promise<void>;
|
|
|
|
containedRipple?: boolean;
|
2022-04-05 17:16:09 +02:00
|
|
|
children?: ComponentChildren;
|
2022-06-24 16:42:21 +02:00
|
|
|
svg?: any;
|
2022-04-05 17:16:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function ButtonBase({
|
|
|
|
class: _class,
|
|
|
|
children,
|
2022-06-01 20:47:47 +02:00
|
|
|
containedRipple,
|
2022-04-05 17:16:09 +02:00
|
|
|
onClick,
|
2022-06-24 16:42:21 +02:00
|
|
|
svg,
|
2022-04-05 17:16:09 +02:00
|
|
|
...rest
|
|
|
|
}: BaseProps): VNode {
|
|
|
|
function doClick(): void {
|
|
|
|
if (onClick) onClick();
|
|
|
|
}
|
2022-06-01 20:47:47 +02:00
|
|
|
const classNames = [
|
|
|
|
buttonBaseStyle,
|
|
|
|
_class,
|
2022-06-24 16:42:21 +02:00
|
|
|
containedRipple ? rippleEnabled : rippleEnabledOutlined,
|
2022-06-01 20:47:47 +02:00
|
|
|
].join(" ");
|
2022-06-24 16:42:21 +02:00
|
|
|
if (svg) {
|
2022-04-05 17:16:09 +02:00
|
|
|
return (
|
|
|
|
<button
|
|
|
|
onClick={doClick}
|
|
|
|
class={classNames}
|
2022-06-24 16:42:21 +02:00
|
|
|
dangerouslySetInnerHTML={{ __html: svg }}
|
2022-04-05 17:16:09 +02:00
|
|
|
{...rest}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
<button onClick={doClick} class={classNames} {...rest}>
|
|
|
|
{children}
|
2022-03-09 18:00:02 +01:00
|
|
|
</button>
|
|
|
|
);
|
|
|
|
}
|
2022-04-05 17:16:09 +02:00
|
|
|
|
|
|
|
export function IconButton({
|
|
|
|
svg,
|
|
|
|
onClick,
|
|
|
|
}: {
|
|
|
|
svg: any;
|
2022-06-01 20:47:47 +02:00
|
|
|
onClick?: () => Promise<void>;
|
2022-04-05 17:16:09 +02:00
|
|
|
}): 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(" ")}
|
2022-06-24 16:42:21 +02:00
|
|
|
svg={svg}
|
2022-04-05 17:16:09 +02:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|