chore: Add button v2 under feature flag (#25106)

This commit is contained in:
Pawan Kumar 2023-07-26 18:10:44 +05:30 committed by GitHub
parent 7c75100e58
commit 2fd0f6f3c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1243 additions and 257 deletions

View File

@ -31,6 +31,9 @@ module.exports = {
"design-system-old": "<rootDir>/node_modules/design-system-old/build",
"@design-system/widgets-old":
"<rootDir>/node_modules/@design-system/widgets-old",
"@design-system/widgets": "<rootDir>/node_modules/@design-system/widgets",
"@design-system/headless": "<rootDir>/node_modules/@design-system/headless",
"@design-system/theming": "<rootDir>/node_modules/@design-system/theming",
"design-system": "<rootDir>/node_modules/design-system/build",
"^proxy-memoize$": "<rootDir>/node_modules/proxy-memoize/dist/wrapper.cjs",
// @blueprintjs packages need to be resolved to the `esnext` directory. The default `esm` directory

View File

@ -1,11 +1,11 @@
import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
import { Tooltip, TooltipTrigger, TooltipContent } from "./";
import { TooltipRoot, TooltipTrigger, TooltipContent } from "./";
import { Button } from "../Button";
<Meta
title="Design-system/headless/Tooltip"
component={Tooltip}
component={TooltipRoot}
args={{
open: undefined,
onOpenChange: undefined,
@ -14,12 +14,12 @@ import { Button } from "../Button";
export const Template = (args) => {
return (
<Tooltip {...args}>
<TooltipRoot {...args}>
<TooltipTrigger>
<button>My trigger</button>
</TooltipTrigger>
<TooltipContent>My tooltip</TooltipContent>
</Tooltip>
</TooltipRoot>
);
};
@ -37,30 +37,30 @@ The placement of the tooltip can be changed by passing the `placement` prop.
<Canvas>
<Story name="Tooltip placement">
<Tooltip placement="left">
<TooltipRoot placement="left">
<TooltipTrigger>
<button>Left</button>
</TooltipTrigger>
<TooltipContent>My tooltip</TooltipContent>
</Tooltip>
<Tooltip placement="top">
</TooltipRoot>
<TooltipRoot placement="top">
<TooltipTrigger>
<button>Top</button>
</TooltipTrigger>
<TooltipContent>My tooltip</TooltipContent>
</Tooltip>
<Tooltip placement="bottom">
</TooltipRoot>
<TooltipRoot placement="bottom">
<TooltipTrigger>
<button>Bottom</button>
</TooltipTrigger>
<TooltipContent>My tooltip</TooltipContent>
</Tooltip>
<Tooltip placement="right">
</TooltipRoot>
<TooltipRoot placement="right">
<TooltipTrigger>
<button>Right</button>
</TooltipTrigger>
<TooltipContent>My tooltip</TooltipContent>
</Tooltip>
</TooltipRoot>
</Story>
</Canvas>
@ -70,12 +70,12 @@ If the trigger is disabled, the tooltip will not be displayed.
<Canvas>
<Story name="Tooltip disabled">
<Tooltip>
<TooltipRoot>
<TooltipTrigger>
<Button isDisabled>Disabled</Button>
</TooltipTrigger>
<TooltipContent disabled>My tooltip</TooltipContent>
</Tooltip>
</TooltipRoot>
</Story>
</Canvas>

View File

@ -5,9 +5,9 @@ import { useTooltip } from "./useTooltip";
import { TooltipContext } from "./TooltipContext";
import type { TooltipOptions } from "./useTooltip";
type TooltipProps = { children: ReactNode } & TooltipOptions;
type TooltipRootProps = { children: ReactNode } & TooltipOptions;
export function Tooltip({ children, ...options }: TooltipProps) {
export function TooltipRoot({ children, ...options }: TooltipRootProps) {
const tooltip = useTooltip(options);
return (

View File

@ -1,4 +1,4 @@
export { Tooltip } from "./Tooltip";
export { TooltipRoot } from "./TooltipRoot";
export { TooltipTrigger } from "./TooltipTrigger";
export { TooltipContent } from "./TooltipContent";

View File

@ -1,3 +1,4 @@
export * from "./ThemeContext";
export * from "./ThemeProvider";
export * from "./types";
export * from "./useTheme";

View File

@ -1,4 +1,7 @@
import type { ReactNode } from "react";
import type { ColorMode } from "../color";
import type { FontFamily } from "../typography";
import type { RootUnit, ThemeToken } from "../token";
export type Theme = ThemeToken & {
@ -15,3 +18,11 @@ export interface ThemeProviderProps {
children: ReactNode;
className?: string;
}
export type UseThemeProps = {
seedColor?: string;
colorMode?: ColorMode;
borderRadius?: string;
fontFamily?: FontFamily;
rootUnitRatio?: number;
};

View File

@ -0,0 +1,137 @@
import Color from "colorjs.io";
import { useEffect, useState } from "react";
import {
TokensAccessor,
defaultTokens,
useFluidTokens,
} from "@design-system/theming";
import type { ColorMode, FontFamily } from "@design-system/theming";
import type { UseThemeProps } from "./types";
export function useTheme(props: UseThemeProps) {
const {
borderRadius,
colorMode = "light",
fontFamily,
rootUnitRatio: rootUnitRatioProp,
seedColor,
} = props;
const [rootUnitRatio, setRootUnitRatio] = useState(1);
const { fluid, ...restDefaultTokens } = defaultTokens;
const { rootUnit, sizing, spacing, typography } = useFluidTokens(
fluid,
rootUnitRatio,
);
const tokensAccessor = new TokensAccessor({
...restDefaultTokens,
rootUnit,
spacing,
sizing,
typography,
colorMode: colorMode as ColorMode,
});
const [theme, setTheme] = useState(tokensAccessor.getAllTokens());
const updateFontFamily = (fontFamily: FontFamily) => {
tokensAccessor.updateFontFamily(fontFamily);
setTheme((prevState) => {
return {
...prevState,
typography: tokensAccessor.getTypography(),
};
});
};
useEffect(() => {
if (colorMode) {
tokensAccessor.updateColorMode(colorMode);
setTheme((prevState) => {
return {
...prevState,
...tokensAccessor.getColors(),
};
});
}
}, [colorMode]);
useEffect(() => {
if (borderRadius) {
tokensAccessor.updateBorderRadius({
1: borderRadius,
});
setTheme((prevState) => {
return {
...prevState,
...tokensAccessor.getBorderRadius(),
};
});
}
}, [borderRadius]);
useEffect(() => {
if (seedColor) {
let color;
try {
color = Color.parse(seedColor);
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
}
if (color) {
tokensAccessor.updateSeedColor(seedColor);
setTheme((prevState) => {
return {
...prevState,
...tokensAccessor.getColors(),
};
});
}
}
}, [seedColor]);
useEffect(() => {
if (fontFamily) {
updateFontFamily(fontFamily);
}
}, [fontFamily]);
useEffect(() => {
if (rootUnitRatioProp) {
setRootUnitRatio(rootUnitRatioProp);
tokensAccessor.updateRootUnit(rootUnit);
tokensAccessor.updateSpacing(spacing);
setTheme((prevState) => {
return {
...prevState,
rootUnit: tokensAccessor.getRootUnit(),
...tokensAccessor.getSpacing(),
};
});
}
}, [rootUnitRatioProp]);
useEffect(() => {
tokensAccessor.updateTypography(typography);
setTheme((prevState) => {
return {
...prevState,
typography: tokensAccessor.getTypography(),
};
});
}, [typography]);
return { theme, setTheme };
}

View File

@ -24,6 +24,7 @@ export class TokensAccessor {
private fontFamily?: FontFamily;
private spacing?: TokenObj;
private sizing?: TokenObj;
private zIndex?: TokenObj;
constructor({
borderRadius,
@ -37,6 +38,7 @@ export class TokensAccessor {
sizing,
spacing,
typography,
zIndex,
}: TokenSource) {
this.seedColor = seedColor;
this.colorMode = colorMode;
@ -49,6 +51,7 @@ export class TokensAccessor {
this.sizing = sizing;
this.spacing = spacing;
this.typography = typography;
this.zIndex = zIndex;
}
updateRootUnit = (rootUnit: RootUnit) => {
@ -87,6 +90,10 @@ export class TokensAccessor {
this.opacity = opacity;
};
updateZIndex = (zIndex: TokenObj) => {
this.zIndex = zIndex;
};
updateSpacing = (spacing: TokenObj) => {
this.spacing = spacing;
};
@ -107,6 +114,7 @@ export class TokensAccessor {
...this.getBoxShadow(),
...this.getBorderWidth(),
...this.getOpacity(),
...this.getZIndex(),
};
};
@ -178,6 +186,12 @@ export class TokensAccessor {
return this.createTokenObject(this.opacity, "opacity");
};
getZIndex = () => {
if (this.zIndex == null) return {} as ThemeToken;
return this.createTokenObject(this.zIndex, "zIndex");
};
private get isLightMode() {
return this.colorMode === "light";
}

View File

@ -47,5 +47,11 @@
},
"opacity": {
"disabled": 0.3
},
"zIndex": {
"1": 3,
"2": 4,
"3": 10,
"99": 9999
}
}

View File

@ -12,7 +12,8 @@ export type TokenType =
| "borderRadius"
| "boxShadow"
| "borderWidth"
| "opacity";
| "opacity"
| "zIndex";
export interface Token {
value: string | number;
@ -31,6 +32,7 @@ export interface TokenSource {
borderWidth?: TokenObj;
opacity?: TokenObj;
fontFamily?: FontFamily;
zIndex?: TokenObj;
sizing?: TokenObj;
spacing?: TokenObj;
}

View File

@ -8,6 +8,7 @@ import { useVisuallyHidden } from "@react-aria/visually-hidden";
import { Text } from "../Text";
import { Spinner } from "../Spinner";
import type { ButtonColor, ButtonIconPosition, ButtonVariant } from "./types";
import { DragContainer, StyledButton } from "./index.styled";
export interface ButtonProps extends Omit<HeadlessButtonProps, "className"> {
@ -15,9 +16,9 @@ export interface ButtonProps extends Omit<HeadlessButtonProps, "className"> {
*
* @default "filled"
*/
variant?: "filled" | "outlined" | "ghost";
variant?: ButtonVariant;
/** Color tone of the button */
color?: "accent" | "neutral" | "positive" | "negative" | "warning";
color?: ButtonColor;
/** When true, makes the button occupy all the space available */
isFitContainer?: boolean;
/** Indicates the loading state of the button */
@ -25,8 +26,8 @@ export interface ButtonProps extends Omit<HeadlessButtonProps, "className"> {
/** Icon to be used in the button of the button */
icon?: React.ReactNode;
/** Indicates the position of icon of the button */
iconPosition?: "start" | "end";
/** Makes the button visually and functionally disabled but focusable */
iconPosition?: ButtonIconPosition;
/** Makes the button visually and functionaly disabled but focusable */
visuallyDisabled?: boolean;
}
@ -64,7 +65,9 @@ export const Button = forwardRef(
return (
<>
{icon}
<Text lineClamp={1}>{children}</Text>
<Text lineClamp={1} textAlign="center">
{children}
</Text>
</>
);
};
@ -80,7 +83,7 @@ export const Button = forwardRef(
data-button=""
data-color={color}
data-fit-container={isFitContainer ? "" : undefined}
data-icon-position={iconPosition === "start" ? undefined : "end"}
data-icon-position={iconPosition === "start" ? "start" : "end"}
data-loading={isLoading ? "" : undefined}
data-variant={variant}
draggable

View File

@ -1,8 +1,8 @@
import styled, { css } from "styled-components";
import { Button as HeadlessButton } from "@design-system/headless";
import type { PickRename } from "../../utils";
import type { ButtonProps } from "./Button";
import type { PickRename } from "../../utils";
type StyledButtonProps = PickRename<
ButtonProps,
@ -73,16 +73,19 @@ export const StyledButton = styled(HeadlessButton)<StyledButtonProps>`
cursor: pointer;
outline: 0;
padding: var(--spacing-2) var(--spacing-4);
block-size: var(--sizing-8);
border-radius: var(--border-radius-1);
user-select: none;
height: var(--sizing-8);
min-width: var(--sizing-8);
text-align: center;
min-inline-size: var(--sizing-8);
position: relative;
font-weight: 600;
& *:not([data-hidden]) + *:not([data-hidden]) {
margin: 0 var(--spacing-1);
&[data-icon-position="start"] *:not([data-hidden]) + *:not([data-hidden]) {
margin-inline-start: var(--spacing-1);
}
&[data-icon-position="end"] *:not([data-hidden]) + *:not([data-hidden]) {
margin-inline-end: var(--spacing-1);
}
${buttonStyles}
@ -96,8 +99,14 @@ export const StyledButton = styled(HeadlessButton)<StyledButtonProps>`
display: flex;
justify-content: center;
align-items: center;
height: var(--sizing-5);
width: var(--sizing-5);
height: var(--sizing-4);
width: var(--sizing-4);
}
// Note: adding important here as ADS is overriding the color of blueprint icon globally
// TODO(pawan): Remove this once ADS team removes the global override
&[data-button] .bp3-icon {
color: currentColor !important;
}
/**

View File

@ -1,2 +1,3 @@
export { Button } from "./Button";
export type { ButtonProps } from "./Button";
export * from "./types";

View File

@ -0,0 +1,26 @@
export const BUTTON_VARIANTS = {
FILLED: "filled",
OUTLINED: "outlined",
GHOST: "ghost",
} as const;
export type ButtonVariant =
(typeof BUTTON_VARIANTS)[keyof typeof BUTTON_VARIANTS];
export const BUTTON_ICON_POSITIONS = {
START: "start",
END: "end",
} as const;
export type ButtonIconPosition =
(typeof BUTTON_ICON_POSITIONS)[keyof typeof BUTTON_ICON_POSITIONS];
export const BUTTON_COLORS = {
ACCENT: "accent",
NEUTRAL: "neutral",
POSITIVE: "positive",
NEGATIVE: "negative",
WARNING: "warning",
} as const;
export type ButtonColor = (typeof BUTTON_COLORS)[keyof typeof BUTTON_COLORS];

View File

@ -33,7 +33,7 @@ export const StyledText = styled.div<StyledTextProp>`
font-weight: ${({ $isBold }) => ($isBold ? "bold" : "normal")};
font-style: ${({ $isItalic }) => ($isItalic ? "italic" : "normal")};
text-align: ${({ $textAlign }) => $textAlign};
max-width: 100%;
width: 100%;
color: ${({ color }) => {
switch (true) {

View File

@ -2,11 +2,11 @@ import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
import { Button } from "../Button";
import { ButtonGroup } from "../ButtonGroup";
import { Tooltip, TooltipTrigger, TooltipContent } from "./";
import { TooltipRoot, TooltipTrigger, TooltipContent } from "./";
<Meta
title="Design-system/widgets/Tooltip"
component={Tooltip}
component={TooltipRoot}
args={{
open: undefined,
onOpenChange: undefined,
@ -15,12 +15,12 @@ import { Tooltip, TooltipTrigger, TooltipContent } from "./";
export const Template = (args) => {
return (
<Tooltip {...args}>
<TooltipRoot {...args}>
<TooltipTrigger>
<Button>My trigger</Button>
</TooltipTrigger>
<TooltipContent>My tooltip</TooltipContent>
</Tooltip>
</TooltipRoot>
);
};
@ -41,30 +41,30 @@ The placement of the tooltip can be changed by passing the `placement` prop.
<Canvas>
<Story name="Tooltip placement">
<ButtonGroup>
<Tooltip placement="left">
<TooltipRoot placement="left">
<TooltipTrigger>
<Button variant="outlined">Left</Button>
</TooltipTrigger>
<TooltipContent>My tooltip</TooltipContent>
</Tooltip>
<Tooltip placement="top">
</TooltipRoot>
<TooltipRoot placement="top">
<TooltipTrigger>
<Button variant="outlined">Top</Button>
</TooltipTrigger>
<TooltipContent>My tooltip</TooltipContent>
</Tooltip>
<Tooltip placement="bottom">
</TooltipRoot>
<TooltipRoot placement="bottom">
<TooltipTrigger>
<Button variant="outlined">Bottom</Button>
</TooltipTrigger>
<TooltipContent>My tooltip</TooltipContent>
</Tooltip>
<Tooltip placement="right">
</TooltipRoot>
<TooltipRoot placement="right">
<TooltipTrigger>
<Button variant="outlined">Right</Button>
</TooltipTrigger>
<TooltipContent>My tooltip</TooltipContent>
</Tooltip>
</TooltipRoot>
</ButtonGroup>
</Story>
</Canvas>
@ -75,7 +75,7 @@ If the trigger is disabled, the tooltip will still be displayed.
<Canvas>
<Story name="Tooltip disabled">
<Tooltip>
<TooltipRoot>
<TooltipTrigger>
<Button
isDisabled
@ -87,6 +87,16 @@ If the trigger is disabled, the tooltip will still be displayed.
</Button>
</TooltipTrigger>
<TooltipContent>My tooltip</TooltipContent>
</TooltipRoot>
</Story>
</Canvas>
# Tooltip Component
<Canvas>
<Story name="Tooltip Component">
<Tooltip>
<Button tooltip="Tooltip">My trigger</Button>
</Tooltip>
</Story>
</Canvas>

View File

@ -0,0 +1,21 @@
import React from "react";
import { TooltipRoot, TooltipContent, TooltipTrigger } from "./";
export type TooltipProps = {
tooltip?: React.ReactNode;
children: React.ReactElement;
};
export function Tooltip(props: TooltipProps) {
const { children, tooltip } = props;
if (!tooltip) return children;
return (
<TooltipRoot>
<TooltipTrigger>{children}</TooltipTrigger>
<TooltipContent>{tooltip}</TooltipContent>
</TooltipRoot>
);
}

View File

@ -15,6 +15,7 @@ export const StyledTooltipContent = styled(
color: var(--color-fg-on-assistive);
padding: var(--spacing-2) var(--spacing-3);
border-radius: var(--border-radius-1);
z-index: var(--z-index-99);
[data-tooltip-trigger-arrow] {
fill: var(--color-bg-assistive);

View File

@ -1,3 +1,4 @@
export { Tooltip } from "@design-system/headless";
export { Tooltip } from "./Tooltip";
export { TooltipRoot } from "@design-system/headless";
export { TooltipTrigger } from "./TooltipTrigger";
export { TooltipContent } from "./TooltipContent";

View File

@ -1,4 +1,6 @@
// components
export { Icon } from "@design-system/headless";
export * from "./components/Button";
export * from "./components/Checkbox";
export * from "./components/Text";

View File

@ -4,7 +4,7 @@ import {
Text,
CheckboxGroup,
Checkbox,
Tooltip,
TooltipRoot,
TooltipTrigger,
TooltipContent,
} from "../";
@ -34,14 +34,14 @@ export const ComplexForm = () => {
gap: "var(--spacing-2)",
}}
>
<Tooltip>
<TooltipRoot>
<TooltipTrigger>
<Button variant="outlined">Cancel</Button>
</TooltipTrigger>
<TooltipContent>
If you cancel, you will lose your order
</TooltipContent>
</Tooltip>
</TooltipRoot>
<Button>Ok</Button>
</div>
</div>

View File

@ -3,7 +3,5 @@ export type OmitRename<
TOmitKeys extends keyof TObj,
TSymbol extends string = "$",
> = {
[K in keyof TObj as K extends TOmitKeys
? never
: `${TSymbol}${string & K}`]: TObj[K];
} & Omit<TObj, TOmitKeys>;
[K in keyof Omit<TObj, TOmitKeys> as `${TSymbol}${string & K}`]: TObj[K];
} & Pick<TObj, TOmitKeys>;

View File

@ -7,6 +7,7 @@ import {
TokensAccessor,
defaultTokens,
useFluidTokens,
useTheme,
} from "@design-system/theming";
import Color from "colorjs.io";
@ -22,132 +23,14 @@ const StyledThemeProvider = styled(ThemeProvider)`
`;
export const theming = (Story, args) => {
const [rootUnitRatio, setRootUnitRatio] = useState(1);
const { fluid, ...restDefaultTokens } = defaultTokens;
const { typography, rootUnit, spacing, sizing } = useFluidTokens(
fluid,
rootUnitRatio,
);
const tokensAccessor = new TokensAccessor({
...restDefaultTokens,
rootUnit,
spacing,
sizing,
typography,
const { theme } = useTheme({
seedColor: args.globals.accentColor,
colorMode: args.globals.colorMode,
borderRadius: args.globals.borderRadius,
fontFamily: args.globals.fontFamily,
rootUnitRatio: args.globals.rootUnitRatio,
});
const [theme, setTheme] = useState(tokensAccessor.getAllTokens());
const updateFontFamily = (fontFamily) => {
tokensAccessor.updateFontFamily(fontFamily);
setTheme((prevState) => {
return {
...prevState,
typography: tokensAccessor.getTypography(),
};
});
};
useEffect(() => {
if (args.globals.colorMode) {
tokensAccessor.updateColorMode(args.globals.colorMode);
setTheme((prevState) => {
return {
...prevState,
...tokensAccessor.getColors(),
};
});
}
}, [args.globals.colorMode]);
useEffect(() => {
if (args.globals.borderRadius) {
tokensAccessor.updateBorderRadius({
1: args.globals.borderRadius,
});
setTheme((prevState) => {
return {
...prevState,
...tokensAccessor.getBorderRadius(),
};
});
}
}, [args.globals.borderRadius]);
useEffect(() => {
if (args.globals.accentColor) {
let color;
try {
color = Color.parse(args.globals.accentColor);
} catch (error) {
console.error(error);
}
if (color) {
tokensAccessor.updateSeedColor(args.globals.accentColor);
setTheme((prevState) => {
return {
...prevState,
...tokensAccessor.getColors(),
};
});
}
}
}, [args.globals.accentColor]);
useEffect(() => {
if (
args.globals.fontFamily &&
args.globals.fontFamily !== "Arial" &&
args.globals.fontFamily !== "System Default"
) {
webfontloader.load({
google: {
families: [`${args.globals.fontFamily}:300,400,500,700`],
},
active: () => {
updateFontFamily(args.globals.fontFamily);
},
});
} else {
updateFontFamily(args.globals.fontFamily);
}
}, [args.globals.fontFamily]);
useEffect(() => {
if (args.globals.rootUnitRatio) {
setRootUnitRatio(args.globals.rootUnitRatio);
tokensAccessor.updateRootUnit(rootUnit);
tokensAccessor.updateSpacing(spacing);
setTheme((prevState) => {
return {
...prevState,
rootUnit: tokensAccessor.getRootUnit(),
...tokensAccessor.getSpacing(),
};
});
}
}, [args.globals.rootUnitRatio]);
useEffect(() => {
tokensAccessor.updateTypography(typography);
setTheme((prevState) => {
return {
...prevState,
typography: tokensAccessor.getTypography(),
};
});
}, [typography]);
return (
<StyledThemeProvider theme={theme}>
<Story />

View File

@ -15,3 +15,14 @@ body,
.innerZoomElementWrapper > * {
overflow: hidden;
}
/* fonts */
@import url("https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap");

View File

@ -13,6 +13,7 @@ export const FEATURE_FLAG = {
ab_ds_schema_enabled: "ab_ds_schema_enabled",
ab_ds_binding_enabled: "ab_ds_binding_enabled",
release_scim_provisioning_enabled: "release_scim_provisioning_enabled",
ab_wds_enabled: "ab_wds_enabled",
release_widgetdiscovery_enabled: "release_widgetdiscovery_enabled",
} as const;
@ -31,6 +32,7 @@ export const DEFAULT_FEATURE_FLAG_VALUE: FeatureFlags = {
ab_ds_schema_enabled: false,
ab_ds_binding_enabled: false,
release_scim_provisioning_enabled: false,
ab_wds_enabled: false,
release_widgetdiscovery_enabled: false,
};

View File

@ -1,6 +1,6 @@
import { createSelector } from "reselect";
import type { AppState } from "@appsmith/reducers";
import type { FeatureFlag } from "@appsmith/entities/FeatureFlag";
import { createSelector } from "reselect";
export const selectFeatureFlags = (state: AppState) =>
state.ui.users.featureFlag.data;

View File

@ -0,0 +1,3 @@
export const WDS_V2_WIDGET_MAP = {
BUTTON_WIDGET: "BUTTON_WIDGET_V2",
};

View File

@ -37,13 +37,18 @@ import { WidgetGlobaStyles } from "globalStyles/WidgetGlobalStyles";
import { getAppsmithConfigs } from "@appsmith/configs";
import useWidgetFocus from "utils/hooks/useWidgetFocus/useWidgetFocus";
import HtmlTitle from "./AppViewerHtmlTitle";
import BottomBar from "components/BottomBar";
import type { ApplicationPayload } from "@appsmith/constants/ReduxActionConstants";
import { getCurrentApplication } from "@appsmith/selectors/applicationSelectors";
import { editorInitializer } from "../../utils/editor/EditorUtils";
import { widgetInitialisationSuccess } from "../../actions/widgetActions";
import BottomBar from "components/BottomBar";
import { areEnvironmentsFetched } from "@appsmith/selectors/environmentSelectors";
import { datasourceEnvEnabled } from "@appsmith/selectors/featureFlagsSelectors";
import {
ThemeProvider as WDSThemeProvider,
useTheme,
} from "@design-system/theming";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
const AppViewerBody = styled.section<{
hasPages: boolean;
@ -98,7 +103,10 @@ function AppViewer(props: Props) {
const currentApplicationDetails: ApplicationPayload | undefined = useSelector(
getCurrentApplication,
);
const { theme } = useTheme({
borderRadius: selectedTheme.properties.borderRadius.appBorderRadius,
seedColor: selectedTheme.properties.colors.primaryColor,
});
const focusRef = useWidgetFocus();
const workspaceId = currentApplicationDetails?.workspaceId || "";
@ -176,46 +184,53 @@ function AppViewer(props: Props) {
};
}, [selectedTheme.properties.fontFamily.appFont]);
const isWDSV2Enabled = useFeatureFlag("ab_wds_enabled");
const backgroundForBody = isWDSV2Enabled
? "var(--color-bg)"
: selectedTheme.properties.colors.backgroundColor;
return (
<ThemeProvider theme={lightTheme}>
<EditorContextProvider renderMode="PAGE">
<WidgetGlobaStyles
fontFamily={selectedTheme.properties.fontFamily.appFont}
primaryColor={selectedTheme.properties.colors.primaryColor}
/>
<HtmlTitle
description={pageDescription}
name={currentApplicationDetails?.name}
/>
<AppViewerBodyContainer
backgroundColor={selectedTheme.properties.colors.backgroundColor}
>
<AppViewerBody
className={CANVAS_SELECTOR}
hasPages={pages.length > 1}
headerHeight={headerHeight}
ref={focusRef}
showBottomBar={showBottomBar}
showGuidedTourMessage={showGuidedTourMessage}
>
{isInitialized && <AppViewerPageContainer />}
</AppViewerBody>
{showBottomBar && <BottomBar viewMode />}
{!hideWatermark && (
<a
className={`fixed hidden right-8 ${
showBottomBar ? "bottom-12" : "bottom-4"
} z-3 hover:no-underline md:flex`}
href="https://appsmith.com"
rel="noreferrer"
target="_blank"
>
<BrandingBadge />
</a>
<WDSThemeProvider theme={theme}>
<ThemeProvider theme={lightTheme}>
<EditorContextProvider renderMode="PAGE">
{!isWDSV2Enabled && (
<WidgetGlobaStyles
fontFamily={selectedTheme.properties.fontFamily.appFont}
primaryColor={selectedTheme.properties.colors.primaryColor}
/>
)}
</AppViewerBodyContainer>
</EditorContextProvider>
</ThemeProvider>
<HtmlTitle
description={pageDescription}
name={currentApplicationDetails?.name}
/>
<AppViewerBodyContainer backgroundColor={backgroundForBody}>
<AppViewerBody
className={CANVAS_SELECTOR}
hasPages={pages.length > 1}
headerHeight={headerHeight}
ref={focusRef}
showBottomBar={showBottomBar}
showGuidedTourMessage={showGuidedTourMessage}
>
{isInitialized && <AppViewerPageContainer />}
</AppViewerBody>
{showBottomBar && <BottomBar viewMode />}
{!hideWatermark && (
<a
className={`fixed hidden right-8 ${
showBottomBar ? "bottom-12" : "bottom-4"
} z-3 hover:no-underline md:flex`}
href="https://appsmith.com"
rel="noreferrer"
target="_blank"
>
<BrandingBadge />
</a>
)}
</AppViewerBodyContainer>
</EditorContextProvider>
</ThemeProvider>
</WDSThemeProvider>
);
}

View File

@ -1,16 +1,21 @@
import * as Sentry from "@sentry/react";
import log from "loglevel";
import React from "react";
import styled from "styled-components";
import * as Sentry from "@sentry/react";
import { useSelector } from "react-redux";
import WidgetFactory from "utils/WidgetFactory";
import type { CanvasWidgetStructure } from "widgets/constants";
import { RenderModes } from "constants/WidgetConstants";
import { useSelector } from "react-redux";
import { getSelectedAppTheme } from "selectors/appThemingSelectors";
import { previewModeSelector } from "selectors/editorSelectors";
import useWidgetFocus from "utils/hooks/useWidgetFocus";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { previewModeSelector } from "selectors/editorSelectors";
import { getSelectedAppTheme } from "selectors/appThemingSelectors";
import { getViewportClassName } from "utils/autoLayout/AutoLayoutUtils";
import {
ThemeProvider as WDSThemeProvider,
useTheme,
} from "@design-system/theming";
import { getIsAppSettingsPaneWithNavigationTabOpen } from "selectors/appSettingsPaneSelectors";
interface CanvasProps {
@ -36,6 +41,11 @@ const Canvas = (props: CanvasProps) => {
getIsAppSettingsPaneWithNavigationTabOpen,
);
const selectedTheme = useSelector(getSelectedAppTheme);
const isWDSV2Enabled = useFeatureFlag("ab_wds_enabled");
const { theme } = useTheme({
borderRadius: selectedTheme.properties.borderRadius.appBorderRadius,
seedColor: selectedTheme.properties.colors.primaryColor,
});
/**
* background for canvas
@ -43,9 +53,17 @@ const Canvas = (props: CanvasProps) => {
let backgroundForCanvas;
if (isPreviewMode || isAppSettingsPaneWithNavigationTabOpen) {
backgroundForCanvas = "initial";
if (isWDSV2Enabled) {
backgroundForCanvas = "var(--color-bg)";
} else {
backgroundForCanvas = "initial";
}
} else {
backgroundForCanvas = selectedTheme.properties.colors.backgroundColor;
if (isWDSV2Enabled) {
backgroundForCanvas = "var(--color-bg)";
} else {
backgroundForCanvas = selectedTheme.properties.colors.backgroundColor;
}
}
const focusRef = useWidgetFocus();
@ -54,23 +72,25 @@ const Canvas = (props: CanvasProps) => {
const paddingBottomClass = props.isAutoLayout ? "" : "pb-52";
try {
return (
<Container
$isAutoLayout={!!props.isAutoLayout}
background={backgroundForCanvas}
className={`relative t--canvas-artboard ${paddingBottomClass} ${marginHorizontalClass} ${getViewportClassName(
canvasWidth,
)}`}
data-testid="t--canvas-artboard"
id="art-board"
ref={focusRef}
width={canvasWidth}
>
{props.widgetsStructure.widgetId &&
WidgetFactory.createWidget(
props.widgetsStructure,
RenderModes.CANVAS,
)}
</Container>
<WDSThemeProvider theme={theme}>
<Container
$isAutoLayout={!!props.isAutoLayout}
background={backgroundForCanvas}
className={`relative t--canvas-artboard ${paddingBottomClass} ${marginHorizontalClass} ${getViewportClassName(
canvasWidth,
)}`}
data-testid="t--canvas-artboard"
id="art-board"
ref={focusRef}
width={canvasWidth}
>
{props.widgetsStructure.widgetId &&
WidgetFactory.createWidget(
props.widgetsStructure,
RenderModes.CANVAS,
)}
</Container>
</WDSThemeProvider>
);
} catch (error) {
log.error("Error rendering DSL", error);

View File

@ -40,6 +40,7 @@ export const excludeList: WidgetType[] = [
"FILE_PICKER_WIDGET",
"FILE_PICKER_WIDGET_V2",
"TABLE_WIDGET_V2",
"BUTTON_WIDGET_V2",
];
function PropertyPaneView(
@ -184,7 +185,7 @@ function PropertyPaneView(
return (
<div
className="w-full overflow-y-scroll h-full"
className="w-full h-full overflow-y-scroll"
key={`property-pane-${widgetProperties.widgetId}`}
ref={containerRef}
>

View File

@ -25,15 +25,16 @@ import {
getSelectedAppTheme,
} from "selectors/appThemingSelectors";
import { getIsAutoLayout } from "selectors/canvasSelectors";
import { getCanvasWidgetsStructure } from "selectors/entitiesSelector";
import { getCurrentThemeDetails } from "selectors/themeSelectors";
import { getCanvasWidgetsStructure } from "selectors/entitiesSelector";
import {
AUTOLAYOUT_RESIZER_WIDTH_BUFFER,
useDynamicAppLayout,
} from "utils/hooks/useDynamicAppLayout";
import Canvas from "../Canvas";
import { CanvasResizer } from "widgets/CanvasResizer";
import type { AppState } from "@appsmith/reducers";
import { CanvasResizer } from "widgets/CanvasResizer";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { getIsAnonymousDataPopupVisible } from "selectors/onboardingSelectors";
type CanvasContainerProps = {
@ -119,9 +120,9 @@ function CanvasContainer(props: CanvasContainerProps) {
const isAppThemeChanging = useSelector(getAppThemeIsChanging);
const showCanvasTopSection = useSelector(showCanvasTopSectionSelector);
const showAnonymousDataPopup = useSelector(getIsAnonymousDataPopupVisible);
const isLayoutingInitialized = useDynamicAppLayout();
const isPageInitializing = isFetchingPage || !isLayoutingInitialized;
const isWDSV2Enabled = useFeatureFlag("ab_wds_enabled");
useEffect(() => {
return () => {
@ -184,7 +185,9 @@ function CanvasContainer(props: CanvasContainerProps) {
$isAutoLayout={isAutoLayout}
background={
isPreviewMode || isAppSettingsPaneWithNavigationTabOpen
? selectedTheme.properties.colors.backgroundColor
? isWDSV2Enabled
? "var(--bg-color)"
: selectedTheme.properties.colors.backgroundColor
: "initial"
}
className={classNames({
@ -214,10 +217,12 @@ function CanvasContainer(props: CanvasContainerProps) {
pointerEvents: isAutoCanvasResizing ? "none" : "auto",
}}
>
<WidgetGlobaStyles
fontFamily={selectedTheme.properties.fontFamily.appFont}
primaryColor={selectedTheme.properties.colors.primaryColor}
/>
{!isWDSV2Enabled && (
<WidgetGlobaStyles
fontFamily={selectedTheme.properties.fontFamily.appFont}
primaryColor={selectedTheme.properties.colors.primaryColor}
/>
)}
{isAppThemeChanging && (
<div className="fixed top-0 bottom-0 left-0 right-0 flex items-center justify-center bg-white/70 z-[2]">
<Spinner size="md" />

View File

@ -42,7 +42,7 @@ const initialState: AppThemingState = {
properties: {
colors: {
backgroundColor: "#F8FAFC",
primaryColor: "",
primaryColor: "#000",
secondaryColor: "",
},
borderRadius: {},

View File

@ -9,7 +9,6 @@ import type {
AppLayoutConfig,
PageListReduxState,
} from "reducers/entityReducers/pageListReducer";
import type { WidgetConfigReducerState } from "reducers/entityReducers/widgetConfigReducer";
import type { WidgetCardProps, WidgetProps } from "widgets/BaseWidget";
import type { Page } from "@appsmith/constants/ReduxActionConstants";
@ -61,6 +60,9 @@ import WidgetFactory from "utils/WidgetFactory";
import { isAirgapped } from "@appsmith/utils/airgapHelpers";
import { nestDSL } from "@shared/dsl";
import { getIsAnonymousDataPopupVisible } from "./onboardingSelectors";
import { WDS_V2_WIDGET_MAP } from "components/wds/constants";
import { selectFeatureFlagCheck } from "@appsmith/selectors/featureFlagsSelectors";
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
const getIsDraggingOrResizing = (state: AppState) =>
state.ui.widgetDragResize.isResizing || state.ui.widgetDragResize.isDragging;
@ -336,12 +338,29 @@ export const getCurrentPageName = createSelector(
export const getWidgetCards = createSelector(
getWidgetConfigs,
getIsAutoLayout,
(widgetConfigs: WidgetConfigReducerState, isAutoLayout: boolean) => {
(_state) => selectFeatureFlagCheck(_state, FEATURE_FLAG.ab_wds_enabled),
(widgetConfigs, isAutoLayout, isWDSEnabled) => {
const cards = Object.values(widgetConfigs.config).filter((config) => {
return isAirgapped()
? config.widgetName !== "Map" && !config.hideCard
: !config.hideCard;
if (isAirgapped()) {
return config.widgetName !== "Map" && !config.hideCard;
}
// if wds_vs is not enabled, hide all wds_v2 widgets
if (
Object.values(WDS_V2_WIDGET_MAP).includes(config.type) &&
isWDSEnabled === false
) {
return false;
}
// if wds is enabled, only show the wds_v2 widgets
if (isWDSEnabled === true) {
return Object.values(WDS_V2_WIDGET_MAP).includes(config.type);
}
return !config.hideCard;
});
const _cards: WidgetCardProps[] = cards.map((config) => {
const {
detachFromLayout = false,
@ -374,6 +393,7 @@ export const getWidgetCards = createSelector(
};
});
const sortedCards = sortBy(_cards, ["displayName"]);
return sortedCards;
},
);

View File

@ -164,6 +164,10 @@ import CodeScannerWidget, {
import ListWidgetV2, {
CONFIG as LIST_WIDGET_CONFIG_V2,
} from "widgets/ListWidgetV2";
import {
ButtonWidget as ButtonWidgetV2,
CONFIG as BUTTON_WIDGET_CONFIG_V2,
} from "widgets/ButtonWidgetV2";
export const ALL_WIDGETS_AND_CONFIG: [any, WidgetConfiguration][] = [
[CanvasWidget, CANVAS_WIDGET_CONFIG],
@ -215,6 +219,7 @@ export const ALL_WIDGETS_AND_CONFIG: [any, WidgetConfiguration][] = [
[CategorySliderWidget, CATEGORY_SLIDER_WIDGET_CONFIG],
[CodeScannerWidget, CODE_SCANNER_WIDGET_CONFIG],
[ListWidgetV2, LIST_WIDGET_CONFIG_V2],
[ButtonWidgetV2, BUTTON_WIDGET_CONFIG_V2],
//Deprecated Widgets
[InputWidget, INPUT_WIDGET_CONFIG],

View File

@ -0,0 +1,11 @@
import { useSelector } from "react-redux";
import type { FeatureFlag } from "@appsmith/entities/FeatureFlag";
import { selectFeatureFlags } from "@appsmith/selectors/featureFlagsSelectors";
export function useFeatureFlag(flagName: FeatureFlag): boolean {
const flagValues = useSelector(selectFeatureFlags);
if (flagName in flagValues) {
return flagValues[flagName];
}
return false;
}

View File

@ -0,0 +1,39 @@
import React from "react";
import styled, { css } from "styled-components";
import type { RenderMode } from "constants/WidgetConstants";
const StyledContainer = styled.div<ContainerProps>`
height: 100%;
position: relative;
${({ maxWidth, minHeight, minWidth }) =>
css`
& [data-button] {
display: flex;
width: auto;
${minWidth ? `min-width: ${minWidth}px;` : ""}
${minHeight ? `min-height: ${minHeight}px;` : ""}
${maxWidth ? `max-width: ${maxWidth}px;` : ""}
}
`}
.grecaptcha-badge {
visibility: hidden;
}
`;
type ContainerProps = {
children?: React.ReactNode;
renderMode?: RenderMode;
showInAllModes?: boolean;
minWidth?: number;
maxWidth?: number;
minHeight?: number;
};
export function Container(props: ContainerProps) {
const { children, ...rest } = props;
return <StyledContainer {...rest}>{children}</StyledContainer>;
}

View File

@ -0,0 +1,71 @@
import React from "react";
import { noop } from "lodash";
import { useRef, useState } from "react";
import ReCAPTCHA from "react-google-recaptcha";
import {
GOOGLE_RECAPTCHA_KEY_ERROR,
GOOGLE_RECAPTCHA_DOMAIN_ERROR,
createMessage,
} from "@appsmith/constants/messages";
import type { RecaptchaProps } from "./useRecaptcha";
export type RecaptchaV2Props = RecaptchaProps;
export function RecaptchaV2(props: RecaptchaV2Props) {
const recaptchaRef = useRef<ReCAPTCHA>(null);
const [isInvalidKey, setInvalidKey] = useState(false);
const handleRecaptchaLoading = (isloading: boolean) => {
props.handleRecaptchaV2Loading && props.handleRecaptchaV2Loading(isloading);
};
const {
isLoading,
isDisabled,
recaptchaKey,
onRecaptchaSubmitSuccess,
onRecaptchaSubmitError = noop,
onPress: onClickProp,
} = props;
const onClick = () => {
if (isDisabled) return onClickProp;
if (isLoading) return onClickProp;
if (isInvalidKey) {
// Handle incorrent google recaptcha site key
onRecaptchaSubmitError(createMessage(GOOGLE_RECAPTCHA_KEY_ERROR));
} else {
handleRecaptchaLoading(true);
recaptchaRef?.current?.reset();
recaptchaRef?.current
?.executeAsync()
.then((token: any) => {
if (token) {
if (typeof onRecaptchaSubmitSuccess === "function") {
onRecaptchaSubmitSuccess(token);
}
} else {
// Handle incorrent google recaptcha site key
onRecaptchaSubmitError(createMessage(GOOGLE_RECAPTCHA_KEY_ERROR));
}
handleRecaptchaLoading(false);
})
.catch(() => {
handleRecaptchaLoading(false);
// Handle error due to google recaptcha key of different domain
onRecaptchaSubmitError(createMessage(GOOGLE_RECAPTCHA_DOMAIN_ERROR));
});
}
};
const recaptcha = (
<ReCAPTCHA
onErrored={() => setInvalidKey(true)}
ref={recaptchaRef}
sitekey={recaptchaKey || ""}
size="invisible"
/>
);
return { onClick, recaptcha };
}

View File

@ -0,0 +1,64 @@
import { noop } from "lodash";
import {
GOOGLE_RECAPTCHA_KEY_ERROR,
GOOGLE_RECAPTCHA_DOMAIN_ERROR,
createMessage,
} from "@appsmith/constants/messages";
import type { ButtonComponentProps } from ".";
import type { RecaptchaProps } from "./useRecaptcha";
import { useScript, ScriptStatus, AddScriptTo } from "utils/hooks/useScript";
type RecaptchaV3Props = RecaptchaProps;
export function RecaptchaV3(props: RecaptchaV3Props) {
const checkValidJson = (inputString: string): boolean => {
return !inputString.includes('"');
};
const {
recaptchaKey,
onPress: onClickProp,
onRecaptchaSubmitSuccess,
onRecaptchaSubmitError = noop,
} = props;
const onClick: ButtonComponentProps["onPress"] = () => {
if (props.isDisabled) return onClickProp;
if (props.isLoading) return onClickProp;
if (status === ScriptStatus.READY) {
(window as any).grecaptcha.ready(() => {
try {
(window as any).grecaptcha
.execute(recaptchaKey, {
action: "submit",
})
.then((token: any) => {
if (typeof onRecaptchaSubmitSuccess === "function") {
onRecaptchaSubmitSuccess(token);
}
})
.catch(() => {
// Handle incorrent google recaptcha site key
onRecaptchaSubmitError(createMessage(GOOGLE_RECAPTCHA_KEY_ERROR));
});
} catch (err) {
// Handle error due to google recaptcha key of different domain
onRecaptchaSubmitError(createMessage(GOOGLE_RECAPTCHA_DOMAIN_ERROR));
}
});
}
};
let validGoogleRecaptchaKey = recaptchaKey;
if (validGoogleRecaptchaKey && !checkValidJson(validGoogleRecaptchaKey)) {
validGoogleRecaptchaKey = undefined;
}
const status = useScript(
`https://www.google.com/recaptcha/api.js?render=${validGoogleRecaptchaKey}`,
AddScriptTo.HEAD,
);
return { onClick };
}

View File

@ -0,0 +1,53 @@
import React from "react";
import { Icon as BIcon } from "@blueprintjs/core";
import type { IconName } from "@blueprintjs/icons";
import { Container } from "./Container";
import { useRecaptcha } from "./useRecaptcha";
import type { UseRecaptchaProps } from "./useRecaptcha";
import type { ButtonProps } from "@design-system/widgets";
import { Button, Icon, Tooltip } from "@design-system/widgets";
export type ButtonComponentProps = {
text?: string;
tooltip?: string;
minWidth?: number;
maxWidth?: number;
minHeight?: number;
isVisible?: boolean;
isLoading: boolean;
iconName?: IconName;
isDisabled?: boolean;
variant?: ButtonProps["variant"];
color?: ButtonProps["color"];
type: ButtonProps["type"];
onPress?: ButtonProps["onPress"];
iconPosition?: ButtonProps["iconPosition"];
};
function ButtonComponent(props: ButtonComponentProps & UseRecaptchaProps) {
const { iconName, maxWidth, minHeight, minWidth, text, tooltip, ...rest } =
props;
const containerProps = { maxWidth, minHeight, minWidth };
const icon = iconName && (
<Icon>
<BIcon icon={iconName} />
</Icon>
);
const { onClick, recpatcha } = useRecaptcha(props);
return (
<Container {...containerProps}>
<Tooltip tooltip={tooltip}>
<Button icon={icon} {...rest} onPress={onClick}>
{text}
</Button>
</Tooltip>
{recpatcha}
</Container>
);
}
export default ButtonComponent;

View File

@ -0,0 +1,37 @@
import { RecaptchaV2 } from "./RecaptchaV2";
import { RecaptchaV3 } from "./RecaptchaV3";
import type { ButtonComponentProps } from ".";
import type { RecaptchaType } from "components/constants";
export type UseRecaptchaProps = {
recaptchaKey?: string;
recaptchaType?: RecaptchaType;
onRecaptchaSubmitError?: (error: string) => void;
onRecaptchaSubmitSuccess?: (token: string) => void;
handleRecaptchaV2Loading?: (isLoading: boolean) => void;
};
export type RecaptchaProps = ButtonComponentProps & UseRecaptchaProps;
type UseRecaptchaReturn = {
onClick?: (...args: any[]) => void;
recpatcha?: React.ReactElement;
};
export const useRecaptcha = (props: RecaptchaProps): UseRecaptchaReturn => {
const { onPress: onClickProp, recaptchaKey } = props;
if (!recaptchaKey) {
return { onClick: onClickProp };
}
if (props.recaptchaType === "V2") {
return RecaptchaV2(props);
}
if (props.recaptchaType === "V3") {
return RecaptchaV3(props);
}
return { onClick: onClickProp };
};

View File

@ -0,0 +1 @@
<svg fill="none" height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><path clip-rule="evenodd" d="m24 19v-8h-16v10h8v-2h-6v-6h12v6zm-9.4727 3.0555c1.1911-.4051 1.3299-.3724 2.2775.6936.0151.0138.0302.0277.0453.0416.239.2192.4717.4326.7838.3138.3327-.1256.4008-.3158.4008-.6439v-6.4815c0-.4464.5133-.953 1.0043-.953.4911 0 .9938.5066.9938.953v3.4791l2.7653 1.2297c.4139.184.6524.6233.5813 1.0707l-.5152 3.2414h-6.8642l-.4913-1z" fill="#4c5664" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@ -0,0 +1,76 @@
import { ButtonWidget } from "./widget";
import IconSVG from "./icon.svg";
import { WIDGET_TAGS } from "constants/WidgetConstants";
import { ButtonPlacementTypes, RecaptchaTypes } from "components/constants";
import { BUTTON_MIN_WIDTH } from "constants/minWidthConstants";
import { ResponsiveBehavior } from "utils/autoLayout/constants";
import { BUTTON_COLORS, BUTTON_VARIANTS } from "@design-system/widgets";
export const CONFIG = {
type: ButtonWidget.getWidgetType(),
name: "Button",
iconSVG: IconSVG,
needsMeta: false,
isCanvas: false,
tags: [WIDGET_TAGS.BUTTONS],
searchTags: ["click", "submit"],
features: {
dynamicHeight: {
sectionIndex: 0,
active: false,
},
},
defaults: {
animateLoading: true,
text: "Submit",
buttonVariant: BUTTON_VARIANTS.FILLED,
buttonColor: BUTTON_COLORS.ACCENT,
placement: ButtonPlacementTypes.CENTER,
rows: 4,
columns: 16,
widgetName: "Button",
isDisabled: false,
isVisible: true,
isDefaultClickDisabled: true,
disabledWhenInvalid: false,
resetFormOnClick: false,
recaptchaType: RecaptchaTypes.V3,
version: 1,
responsiveBehavior: ResponsiveBehavior.Hug,
minWidth: BUTTON_MIN_WIDTH,
},
properties: {
derived: ButtonWidget.getDerivedPropertiesMap(),
default: ButtonWidget.getDefaultPropertiesMap(),
meta: ButtonWidget.getMetaPropertiesMap(),
contentConfig: ButtonWidget.getPropertyPaneContentConfig(),
styleConfig: ButtonWidget.getPropertyPaneStyleConfig(),
},
autoLayout: {
defaults: {
rows: 4,
columns: 6.453,
},
autoDimension: {
width: true,
},
widgetSize: [
{
viewportMinWidth: 0,
configuration: () => {
return {
minWidth: "120px",
maxWidth: "360px",
minHeight: "40px",
};
},
},
],
disableResizeHandles: {
horizontal: true,
vertical: true,
},
},
};
export { ButtonWidget };

View File

@ -0,0 +1,145 @@
import { RecaptchaTypes } from "components/constants";
import { isAirgapped } from "@appsmith/utils/airgapHelpers";
import { ValidationTypes } from "constants/WidgetValidation";
export const propertyPaneContentConfig = [
{
sectionName: "Basic",
children: [
{
propertyName: "text",
label: "Label",
helpText: "Sets the label of the button",
controlType: "INPUT_TEXT",
placeholderText: "Submit",
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
},
{
helpText: "when the button is clicked",
propertyName: "onClick",
label: "onClick",
controlType: "ACTION_SELECTOR",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: true,
},
],
},
{
sectionName: "General",
children: [
{
helpText: "Show helper text with button on hover",
propertyName: "tooltip",
label: "Tooltip",
controlType: "INPUT_TEXT",
placeholderText: "Submits Form",
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
},
{
propertyName: "isVisible",
label: "Visible",
helpText: "Controls the visibility of the widget",
controlType: "SWITCH",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.BOOLEAN },
},
{
propertyName: "isDisabled",
label: "Disabled",
controlType: "SWITCH",
helpText: "Disables clicks to this widget",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.BOOLEAN },
},
{
propertyName: "animateLoading",
label: "Animate loading",
controlType: "SWITCH",
helpText: "Controls the loading of the widget",
defaultValue: true,
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.BOOLEAN },
},
],
},
{
sectionName: "Validation",
hidden: isAirgapped,
children: [
{
propertyName: "googleRecaptchaKey",
label: "Google reCAPTCHA key",
helpText: "Sets Google reCAPTCHA site key for the button",
controlType: "INPUT_TEXT",
placeholderText: "reCAPTCHA Key",
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
},
{
propertyName: "recaptchaType",
label: "Google reCAPTCHA version",
controlType: "DROP_DOWN",
helpText: "Select reCAPTCHA version",
options: [
{
label: "reCAPTCHA v3",
value: RecaptchaTypes.V3,
},
{
label: "reCAPTCHA v2",
value: RecaptchaTypes.V2,
},
],
isBindProperty: true,
isTriggerProperty: false,
validation: {
type: ValidationTypes.TEXT,
params: {
allowedValues: [RecaptchaTypes.V3, RecaptchaTypes.V2],
default: RecaptchaTypes.V3,
},
},
},
],
},
// TODO: refactor widgetParentProps implementation when we address #10659
{
sectionName: "Form settings",
children: [
{
helpText:
"Disabled if the form is invalid, if this widget exists directly within a Form widget.",
propertyName: "disabledWhenInvalid",
label: "Disabled invalid forms",
controlType: "SWITCH",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.BOOLEAN },
},
{
helpText:
"Resets the fields of the form, on click, if this widget exists directly within a Form widget.",
propertyName: "resetFormOnClick",
label: "Reset form on success",
controlType: "SWITCH",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.BOOLEAN },
},
],
},
];

View File

@ -0,0 +1,182 @@
import React from "react";
import { toast } from "design-system";
import BaseWidget from "widgets/BaseWidget";
import ButtonComponent from "../component";
import { propertyPaneStyleConfig } from "./styleConfig";
import type { ButtonComponentProps } from "../component";
import type { RecaptchaType } from "components/constants";
import type { WidgetType } from "constants/WidgetConstants";
import { propertyPaneContentConfig } from "./contentConfig";
import type { DerivedPropertiesMap } from "utils/WidgetFactory";
import type { WidgetProps, WidgetState } from "widgets/BaseWidget";
import type { AutocompletionDefinitions } from "widgets/constants";
import { DefaultAutocompleteDefinitions } from "widgets/WidgetUtils";
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
import type { ExecutionResult } from "constants/AppsmithActionConstants/ActionConstants";
class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
onButtonClickBound: () => void;
constructor(props: ButtonWidgetProps) {
super(props);
this.onButtonClickBound = this.onButtonClick.bind(this);
this.onRecaptchaSubmitError = this.onRecaptchaSubmitError.bind(this);
this.onRecaptchaSubmitSuccess = this.onRecaptchaSubmitSuccess.bind(this);
this.state = {
isLoading: false,
};
}
static getAutocompleteDefinitions(): AutocompletionDefinitions {
return {
"!doc":
"Buttons are used to capture user intent and trigger actions based on that intent",
"!url": "https://docs.appsmith.com/widget-reference/button",
isVisible: DefaultAutocompleteDefinitions.isVisible,
text: "string",
isDisabled: "bool",
recaptchaToken: "string",
};
}
static getPropertyPaneContentConfig() {
return propertyPaneContentConfig;
}
static getPropertyPaneStyleConfig() {
return propertyPaneStyleConfig;
}
static getMetaPropertiesMap(): Record<string, any> {
return {
recaptchaToken: undefined,
};
}
static getDerivedPropertiesMap(): DerivedPropertiesMap {
return {};
}
onButtonClick() {
if (this.props.onClick) {
this.setState({ isLoading: true });
super.executeAction({
triggerPropertyName: "onClick",
dynamicString: this.props.onClick,
event: {
type: EventType.ON_CLICK,
callback: this.handleActionComplete,
},
});
return;
}
if (this.props.resetFormOnClick && this.props.onReset) {
this.props.onReset();
return;
}
}
hasOnClickAction = () => {
const { isDisabled, onClick, onReset, resetFormOnClick } = this.props;
return Boolean((onClick || onReset || resetFormOnClick) && !isDisabled);
};
onRecaptchaSubmitSuccess(token: string) {
this.props.updateWidgetMetaProperty("recaptchaToken", token, {
triggerPropertyName: "onClick",
dynamicString: this.props.onClick,
event: {
type: EventType.ON_CLICK,
callback: this.handleActionComplete,
},
});
}
onRecaptchaSubmitError = (error: string) => {
toast.show(error, { kind: "error" });
if (this.hasOnClickAction()) {
this.onButtonClickBound();
}
};
handleRecaptchaV2Loading = (isLoading: boolean) => {
if (this.props.onClick) {
this.setState({ isLoading });
}
};
handleActionComplete = (result: ExecutionResult) => {
this.setState({
isLoading: false,
});
if (result.success) {
if (this.props.resetFormOnClick && this.props.onReset)
this.props.onReset();
}
};
getPageView() {
const disabled =
this.props.disabledWhenInvalid &&
"isFormValid" in this.props &&
!this.props.isFormValid;
const isDisabled = this.props.isDisabled || disabled;
return (
<ButtonComponent
color={this.props.buttonColor}
handleRecaptchaV2Loading={this.handleRecaptchaV2Loading}
iconName={this.props.iconName}
iconPosition={this.props.iconAlign}
isDisabled={isDisabled}
isLoading={this.props.isLoading || this.state.isLoading}
key={this.props.widgetId}
maxWidth={this.props.maxWidth}
minHeight={this.props.minHeight}
minWidth={this.props.minWidth}
onPress={this.hasOnClickAction() ? this.onButtonClickBound : undefined}
onRecaptchaSubmitError={this.onRecaptchaSubmitError}
onRecaptchaSubmitSuccess={this.onRecaptchaSubmitSuccess}
recaptchaKey={this.props.googleRecaptchaKey}
recaptchaType={this.props.recaptchaType}
text={this.props.text}
tooltip={this.props.tooltip}
type={this.props.buttonType || "button"}
variant={this.props.buttonVariant}
/>
);
}
static getWidgetType(): WidgetType {
return "BUTTON_WIDGET_V2";
}
}
export interface ButtonWidgetProps extends WidgetProps {
text?: string;
isVisible?: boolean;
isDisabled?: boolean;
resetFormOnClick?: boolean;
googleRecaptchaKey?: string;
recaptchaType?: RecaptchaType;
disabledWhenInvalid?: boolean;
buttonType?: ButtonComponentProps["type"];
iconName?: ButtonComponentProps["iconName"];
buttonVariant?: ButtonComponentProps["variant"];
iconAlign?: ButtonComponentProps["iconPosition"];
buttonColor?: ButtonComponentProps["color"];
}
interface ButtonWidgetState extends WidgetState {
isLoading: boolean;
}
export { ButtonWidget };

View File

@ -0,0 +1,96 @@
import { capitalize } from "lodash";
import { BUTTON_COLORS, BUTTON_VARIANTS } from "@design-system/widgets";
import { ValidationTypes } from "constants/WidgetValidation";
export const propertyPaneStyleConfig = [
{
sectionName: "General",
children: [
{
propertyName: "buttonVariant",
label: "Button variant",
controlType: "ICON_TABS",
fullWidth: true,
helpText: "Sets the variant of the button",
options: Object.values(BUTTON_VARIANTS).map((variant) => ({
label: capitalize(variant),
value: variant,
})),
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: {
type: ValidationTypes.TEXT,
params: {
allowedValues: Object.values(BUTTON_VARIANTS),
default: BUTTON_VARIANTS.FILLED,
},
},
},
{
propertyName: "buttonColor",
label: "Button color",
controlType: "DROP_DOWN",
fullWidth: true,
helpText: "Sets the semantic color of the button",
options: Object.values(BUTTON_COLORS).map((semantic) => ({
label: capitalize(semantic),
value: semantic,
})),
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: {
type: ValidationTypes.TEXT,
params: {
allowedValues: Object.values(BUTTON_COLORS),
default: BUTTON_COLORS.ACCENT,
},
},
},
],
},
{
sectionName: "Icon",
children: [
{
propertyName: "iconName",
label: "Select icon",
helpText: "Sets the icon to be used for the button",
controlType: "ICON_SELECT",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: {
type: ValidationTypes.TEXT,
},
},
{
propertyName: "iconAlign",
label: "Position",
helpText: "Sets the icon alignment of the button",
controlType: "ICON_TABS",
fullWidth: false,
options: [
{
startIcon: "skip-left-line",
value: "start",
},
{
startIcon: "skip-right-line",
value: "end",
},
],
isBindProperty: false,
isTriggerProperty: false,
validation: {
type: ValidationTypes.TEXT,
params: {
allowedValues: ["start", "end"],
},
},
},
],
},
];