Преглед на файлове

Feature: Button component completed.

lfabl преди 3 месеца
родител
ревизия
be79006169
променени са 8 файла, в които са добавени 608 реда и са изтрити 29 реда
  1. 8 0
      example/src/index.tsx
  2. 34 26
      src/assets/svg/loadingIcon/index.tsx
  3. 186 0
      src/components/button/index.tsx
  4. 277 0
      src/components/button/stylesheet.ts
  5. 96 0
      src/components/button/type.ts
  6. 2 2
      src/components/index.ts
  7. 1 1
      src/index.tsx
  8. 4 0
      src/types/index.ts

+ 8 - 0
example/src/index.tsx

@@ -5,6 +5,7 @@ import {
 import {
     setupNCoreUIKit,
     NCoreUIKitTheme,
+    Button,
     Text
 } from "ncore-ui-kit-mobile";
 import {
@@ -30,6 +31,13 @@ const App = () => {
         ]}
     >
         <Text>Result: </Text>
+        <Button
+            onPress={() => {
+            }}
+            type="success"
+            title="Ahmet"
+            variant="ghost"
+        />
     </View>;
 };
 

+ 34 - 26
src/assets/svg/loadingIcon/index.tsx

@@ -4,7 +4,8 @@ import {
 } from "react";
 import {
     Animated,
-    Easing
+    Easing,
+    View
 } from "react-native";
 import {
     NCoreUIKitTheme
@@ -17,8 +18,6 @@ import {
     Svg
 } from "react-native-svg";
 
-const AnimatedSvg = Animated.createAnimatedComponent(Svg);
-
 const SvgLoadingIcon = ({
     color = "onPrimary",
     customColor,
@@ -36,14 +35,18 @@ const SvgLoadingIcon = ({
     const pathScale = 38 / size;
 
     useEffect(() => {
-        Animated.loop(
+        const animation = Animated.loop(
             Animated.timing(animValue, {
                 toValue: 1,
                 duration: 2000,
                 easing: Easing.linear,
                 useNativeDriver: true
             })
-        ).start();
+        );
+
+        animation.start();
+
+        return () => animation.stop();
     }, [animValue]);
 
     const rotate = animValue.interpolate({
@@ -52,39 +55,44 @@ const SvgLoadingIcon = ({
             1
         ],
         outputRange: [
-            "0deg",
-            "360deg"
+            "360deg",
+            "0deg"
         ]
     });
 
     return (
-        <span
+        <View
             style={{
                 height: size,
                 width: size
             }}
         >
-            <AnimatedSvg
-                height={size}
-                width={size}
-                fill="none"
+            <Animated.View
                 style={{
-                    ...props.style,
-                    ...rotate
+                    transform: [{
+                        rotate: rotate
+                    }]
                 }}
-                {...props}
             >
-                <Path
-                    d="M11.885 7.303 19 3.194l7.115 4.109v8.215l7.095 4.09v8.215l-7.115 4.108-7.076-4.108-7.114 4.108-7.115-4.108v-8.216l7.095-4.089V7.303Z"
-                    stroke={customColor ? customColor : colors.content.icon[color]}
-                    transform={`scale(${1 / pathScale})`}
-                    strokeLinejoin="round"
-                    strokeLinecap="round"
-                    clipRule="evenodd"
-                    strokeWidth={3}
-                />
-            </AnimatedSvg>
-        </span>
+                <Svg
+                    height={size}
+                    width={size}
+                    fill="none"
+                    style={props.style}
+                    {...props}
+                >
+                    <Path
+                        d="M11.885 7.303 19 3.194l7.115 4.109v8.215l7.095 4.09v8.215l-7.115 4.108-7.076-4.108-7.114 4.108-7.115-4.108v-8.216l7.095-4.089V7.303Z"
+                        stroke={customColor ? customColor : colors.content.icon[color]}
+                        transform={`scale(${1 / pathScale})`}
+                        strokeLinejoin="round"
+                        strokeLinecap="round"
+                        clipRule="evenodd"
+                        strokeWidth={3}
+                    />
+                </Svg>
+            </Animated.View>
+        </View>
     );
 };
 export default SvgLoadingIcon;

+ 186 - 0
src/components/button/index.tsx

@@ -0,0 +1,186 @@
+import {
+    type FC
+} from "react";
+import {
+    TouchableOpacity,
+    View
+} from "react-native";
+import type IButtonProps from "./type";
+import stylesheet, {
+    getButtonVariant,
+    getButtonSize,
+    getButtonType,
+    useStyles
+} from "./stylesheet";
+import {
+    NCoreUIKitTheme
+} from "../../core/hooks";
+import type {
+    INCoreUIKitIconProps
+} from "../../types";
+import type ITextProps from "../text/type";
+import Loading from "../loading";
+import Text from "../text";
+
+/**
+ * A generic button
+ * @param props {@link IButtonProps}
+ * @returns Element
+ */
+const Button: FC<IButtonProps> = ({
+    displayBehaviourWhileLoading = "disabled",
+    spreadBehaviour = "baseline",
+    icon: IconComponentProp,
+    iconDirection = "left",
+    variant = "filled",
+    isDisabled = false,
+    type = "primary",
+    size = "medium",
+    titleStyle,
+    isLoading,
+    onPress,
+    title,
+    style
+}) => {
+    const {
+        typography,
+        radiuses,
+        borders,
+        colors,
+        spaces
+    } = NCoreUIKitTheme.useContext();
+
+    const currentSize = getButtonSize({
+        spaces,
+        size
+    });
+
+    const currentVariant = getButtonVariant({
+        variant,
+    });
+
+    const currentType = getButtonType({
+        type,
+    });
+
+    const {
+        container: containerDynamicStyle,
+        loading: loadingDynamicStyle,
+        overlay: overlayDynamicStyle,
+        title: titleDynamicStyle
+    } = useStyles({
+        displayBehaviourWhileLoading,
+        icon: IconComponentProp,
+        spreadBehaviour,
+        currentVariant,
+        iconDirection,
+        currentType,
+        currentSize,
+        isDisabled,
+        isLoading,
+        radiuses,
+        variant,
+        borders,
+        colors,
+        spaces,
+        title,
+        type
+    });
+
+    const titleProps: ITextProps = {
+        color: currentType.titleColor,
+    };
+
+    const iconProps: INCoreUIKitIconProps = {
+        size: Number(typography[currentSize.fontSize].fontSize),
+        color: currentType.iconColor
+    };
+
+    if (currentVariant.titleColor !== "type") {
+        titleProps.color = currentType.titleColor;
+    }
+
+    if (currentVariant.iconColor !== "type") {
+        iconProps.color = currentType.iconColor;
+    }
+
+    if (type === "primary" && variant === "filled") {
+        iconProps.color = "onPrimary";
+        titleProps.color = "onPrimary";
+    }
+
+    if (type === "primary" && variant !== "filled") {
+        iconProps.color = "emphasized";
+        titleProps.color = "emphasized";
+    }
+
+    if (isDisabled || isLoading) {
+        const stateType = type === "danger" ? "error" : type;
+
+        titleProps.customColor = colors.system.state.content.disabled[stateType];
+        iconProps.customColor = colors.system.state.content.disabled[stateType];
+    }
+
+    const renderIcon = () => {
+        if (isLoading) {
+            return <Loading
+                {...iconProps}
+                style={[
+                    loadingDynamicStyle
+                ]}
+            />;
+        }
+
+        if (!IconComponentProp) {
+            return null;
+        }
+
+        return <IconComponentProp {...iconProps} />;
+    };
+
+    const renderTitle = () => {
+        if (!title) {
+            return null;
+        }
+
+        return <Text
+            variant={currentSize.fontSize}
+            style={[
+                titleStyle,
+                stylesheet.title,
+                titleDynamicStyle
+            ]}
+            {...titleProps}
+        >
+            {title}
+        </Text>;
+    };
+
+    const renderOverlay = () => {
+        return <View
+            style={[
+                stylesheet.loading,
+                stylesheet.overlay,
+                overlayDynamicStyle
+            ]}
+        />;
+    };
+
+    return (
+        <TouchableOpacity
+            onPress={isDisabled || isLoading ? () => null : onPress}
+            disabled={isDisabled || isLoading}
+            style={[
+                style,
+                stylesheet.container,
+                containerDynamicStyle
+            ]}
+        >
+            {renderIcon()}
+            {renderTitle()}
+
+            {renderOverlay()}
+        </TouchableOpacity>
+    );
+};
+export default Button;

+ 277 - 0
src/components/button/stylesheet.ts

@@ -0,0 +1,277 @@
+import {
+    type TextStyle,
+    type ViewStyle,
+    StyleSheet
+} from "react-native";
+import {
+    type ButtonVariantConstantType,
+    type ButtonTypeConstantType,
+    type ButtonSizeConstantType,
+    type ButtonDynamicStyleType,
+    type ButtonMeasuresKeys,
+    type ButtonMeasures,
+    type ButtonVariants,
+    type ButtonVariant,
+    type ButtonTypes,
+    type ButtonSize,
+    type ButtonType
+} from "./type";
+import type {
+    Mutable
+} from "../../types";
+
+export const BUTTON_SIZES: Record<ButtonSize, ButtonMeasuresKeys> = {
+    small: {
+        paddingHorizontal: "spacingMd",
+        paddingVertical: "spacingSm",
+        fontSize: "labelSmallSize"
+    },
+    medium: {
+        paddingHorizontal: "spacingLg",
+        paddingVertical: "spacingMd",
+        fontSize: "labelMediumSize"
+    },
+    large: {
+        paddingHorizontal: "spacingXl",
+        paddingVertical: "spacingLg",
+        fontSize: "labelLargeSize"
+    }
+};
+
+export const BUTTON_VARIANT_STYLES: Record<
+    ButtonVariant,
+    {
+        titleColor: "type" | keyof NCoreUIKit.TextContentColors;
+        iconColor: "type" | keyof NCoreUIKit.ColorsMerged;
+        borderColor: "transparent" | "type" | "container";
+        iconColorScheme?: keyof NCoreUIKit.ThemeTokens;
+        containerColor: "transparent" | "type";
+    }
+> = {
+    filled: {
+        borderColor: "container",
+        containerColor: "type",
+        titleColor: "type",
+        iconColor: "type"
+    },
+    outline: {
+        containerColor: "transparent",
+        borderColor: "type",
+        titleColor: "type",
+        iconColor: "type"
+    },
+    ghost: {
+        containerColor: "transparent",
+        borderColor: "transparent",
+        titleColor: "type",
+        iconColor: "type"
+    }
+};
+
+export const BUTTON_TYPE_STYLES: Record<
+    ButtonType,
+    {
+        containerColor: keyof NCoreUIKit.ContainerContentColors;
+        borderColor: keyof NCoreUIKit.BorderContentColors;
+        titleColor: keyof NCoreUIKit.TextContentColors;
+        iconColor: keyof NCoreUIKit.IconContentColors;
+    }
+> = {
+    primary: {
+        containerColor: "primary",
+        borderColor: "emphasized",
+        titleColor: "onPrimary",
+        iconColor: "onPrimary"
+    },
+    danger: {
+        containerColor: "danger",
+        borderColor: "danger",
+        titleColor: "danger",
+        iconColor: "danger"
+    },
+    success: {
+        containerColor: "success",
+        borderColor: "success",
+        titleColor: "success",
+        iconColor: "success"
+    },
+    warning: {
+        containerColor: "warning",
+        borderColor: "warning",
+        titleColor: "warning",
+        iconColor: "warning"
+    },
+    info: {
+        containerColor: "info",
+        borderColor: "info",
+        titleColor: "info",
+        iconColor: "info"
+    },
+    neutral: {
+        containerColor: "subtle",
+        borderColor: "subtle",
+        titleColor: "mid",
+        iconColor: "mid"
+    }
+};
+
+export const getButtonType = ({
+    type
+}: ButtonTypeConstantType): ButtonTypes => {
+    const currentType = BUTTON_TYPE_STYLES[type];
+
+    return currentType;
+};
+
+export const getButtonVariant = ({
+    variant
+}: ButtonVariantConstantType): ButtonVariants => {
+    const currentVariant = BUTTON_VARIANT_STYLES[variant];
+
+    return currentVariant;
+};
+
+export const getButtonSize = ({
+    spaces,
+    size
+}: ButtonSizeConstantType): ButtonMeasures => {
+    const currentSize = BUTTON_SIZES[size];
+
+    return {
+        paddingHorizontal: spaces[currentSize.paddingHorizontal],
+        paddingVertical: spaces[currentSize.paddingVertical],
+        fontSize: currentSize.fontSize
+    };
+};
+
+const stylesheet = StyleSheet.create({
+    container: {
+        backgroundColor: "transparent",
+        borderColor: "transparent",
+        flexDirection: "row",
+        borderStyle: "solid",
+        alignItems: "center",
+        position: "relative",
+        userSelect: "none",
+        display: "flex"
+    },
+    title: {
+        margin: "0 auto",
+    },
+    loading: {},
+    overlay: {
+        position: "absolute",
+        display: "none",
+        bottom: 0,
+        right: 0,
+        left: 0,
+        top: 0
+    }
+});
+
+export const useStyles = ({
+    displayBehaviourWhileLoading,
+    spreadBehaviour,
+    currentVariant,
+    iconDirection,
+    currentType,
+    currentSize,
+    isDisabled,
+    isLoading,
+    radiuses,
+    variant,
+    borders,
+    colors,
+    spaces,
+    title,
+    icon,
+    type
+}: ButtonDynamicStyleType) => {
+    const styleType = type === "danger" ? "error" : type;
+
+    const styles = {
+        container: {
+            paddingRight: currentSize.paddingHorizontal,
+            paddingBottom: currentSize.paddingVertical,
+            paddingLeft: currentSize.paddingHorizontal,
+            paddingTop: currentSize.paddingVertical,
+            borderRadius: radiuses.md,
+            borderWidth: borders.line
+        } as Mutable<ViewStyle>,
+        title: {
+        } as Mutable<TextStyle>,
+        loading: {
+        } as Mutable<ViewStyle>,
+        overlay: {
+            borderRadius: radiuses.md - 2
+        } as Mutable<ViewStyle>
+    };
+
+    if (currentVariant.containerColor === "type") {
+        styles.container.backgroundColor = colors.content.container[currentType.containerColor];
+    } else {
+        styles.container.backgroundColor = "transparent";
+    }
+
+    if (currentVariant.borderColor === "type") {
+        styles.container.borderColor = colors.content.border[currentType.borderColor];
+    } else if (currentVariant.borderColor === "container") {
+        styles.container.borderColor = styles.container.backgroundColor;
+    } else {
+        styles.container.borderColor = "transparent";
+    }
+
+    if (isLoading) {
+        styles.title.marginLeft = spaces.spacingSm;
+
+        if (displayBehaviourWhileLoading === "disabled") {
+            if (variant !== "ghost") {
+                styles.container.borderColor = colors.system.state.overlay.disabled[styleType];
+
+                if (variant === "filled") {
+                    styles.overlay.backgroundColor = colors.system.state.overlay.disabled[styleType];
+                }
+            }
+        }
+    }
+
+    if (isLoading && spreadBehaviour === "stretch") {
+        styles.title.marginLeft = spaces.spacingSm;
+        styles.title.margin = "initial";
+    }
+
+    if (icon && !isLoading) {
+        styles.title.margin = "initial";
+        styles.title.marginLeft = spaces.spacingSm;
+    }
+
+    if (spreadBehaviour === "baseline") {
+        styles.container.alignSelf = spreadBehaviour;
+        styles.container.width = "auto";
+    } else if (spreadBehaviour === "stretch") {
+        styles.container.justifyContent = "center";
+        // styles.container.alignSelf = spreadBehaviour; TODO: It was required but now is not. Why ?
+        styles.container.width = "100%";
+    }
+
+    if (isDisabled) {
+        if (variant !== "ghost") {
+            styles.container.borderColor = colors.system.state.overlay.disabled[styleType];
+
+            if (variant === "filled") {
+                styles.overlay.backgroundColor = colors.system.state.overlay.disabled[styleType];
+            }
+        }
+    }
+
+    if (icon && title) {
+        if (iconDirection === "left") {
+            styles.title.marginLeft = spaces.spacingSm;
+        } else {
+            styles.title.marginRight = spaces.spacingSm;
+        }
+    }
+
+    return styles;
+};
+export default stylesheet;

+ 96 - 0
src/components/button/type.ts

@@ -0,0 +1,96 @@
+import {
+    type ViewStyle,
+    type StyleProp,
+    type TextStyle
+} from "react-native";
+import {
+    type NCoreUIKitIcon
+} from "../../types";
+
+export type ButtonDynamicStyleType = {
+    displayBehaviourWhileLoading?: ButtonDisplayBehaviourWhileLoading;
+    radiuses: NCoreUIKit.ActivePalette["radiuses"];
+    spaces: NCoreUIKit.ActivePalette["spaces"];
+    colors: NCoreUIKit.ActivePalette["colors"];
+    spreadBehaviour?: ButtonSpreadBehaviour;
+    iconDirection?: "left" | "right";
+    currentVariant: ButtonVariants;
+    borders: NCoreUIKit.Borders;
+    currentSize: ButtonMeasures;
+    currentType: ButtonTypes;
+    variant: ButtonVariant;
+    icon?: NCoreUIKitIcon;
+    isDisabled?: boolean;
+    isLoading?: boolean;
+    theme?: undefined;
+    type: ButtonType;
+    title?: string;
+};
+
+export type ButtonMeasuresKeys = {
+    paddingHorizontal: keyof NCoreUIKit.Spaces;
+    paddingVertical: keyof NCoreUIKit.Spaces;
+    fontSize: keyof NCoreUIKit.Typography;
+};
+
+export type ButtonVariants = {
+    iconColor: keyof NCoreUIKit.IconContentColors | string | "type";
+    titleColor: keyof NCoreUIKit.TextContentColors | "type";
+    borderColor: "transparent" | "type" | "container";
+    containerColor: "transparent" | "type";
+};
+
+export type ButtonTypes = {
+    containerColor: keyof NCoreUIKit.ContainerContentColors;
+    borderColor: keyof NCoreUIKit.BorderContentColors;
+    titleColor: keyof NCoreUIKit.TextContentColors;
+    iconColor: keyof NCoreUIKit.IconContentColors;
+};
+
+export type ButtonMeasures = {
+    fontSize: keyof NCoreUIKit.Typography;
+    paddingHorizontal: number;
+    paddingVertical: number;
+};
+
+export type ButtonVariantConstantType = {
+    variant: ButtonVariant;
+};
+
+export type ButtonTypeConstantType = {
+    type: ButtonType;
+};
+
+export type ButtonSizeConstantType = {
+    spaces: NCoreUIKit.ActivePalette["spaces"];
+    size: ButtonSize;
+};
+
+export type ButtonType = "primary" | "danger" | "warning" | "neutral" | "success" | "info";
+
+export type ButtonDisplayBehaviourWhileLoading = "none" | "disabled";
+
+export type ButtonSpreadBehaviour = "baseline" | "stretch" | "free";
+
+export type ButtonVariant = "filled" | "outline" | "ghost";
+
+export type ButtonSize = "small" | "medium" | "large";
+
+interface IButtonProps {
+    displayBehaviourWhileLoading?: ButtonDisplayBehaviourWhileLoading;
+    titleStyle?: StyleProp<TextStyle>[] | StyleProp<TextStyle>;
+    style?: StyleProp<ViewStyle>[] | StyleProp<ViewStyle>;
+    spreadBehaviour?: ButtonSpreadBehaviour;
+    iconDirection?: "left" | "right";
+    variant?: ButtonVariant;
+    icon?: NCoreUIKitIcon;
+    isDisabled?: boolean;
+    onPress: () => void;
+    isLoading?: boolean;
+    size?: ButtonSize;
+    type?: ButtonType;
+    title?: string;
+}
+export type {
+    IButtonProps as default
+};

+ 2 - 2
src/components/index.ts

@@ -1,11 +1,11 @@
 export {
     default as Text
 } from "./text";
-/*
+
 export {
     default as Button
 } from "./button";
-
+/*
 export {
     default as Dialog
 } from "./dialog";

+ 1 - 1
src/index.tsx

@@ -12,7 +12,7 @@ export {
     // TextInput,
     Loading,
     // Dialog,
-    // Button,
+    Button,
     Modal,
     Text
 } from "./components";

+ 4 - 0
src/types/index.ts

@@ -60,6 +60,10 @@ export type RecursiveRecord = {
 
 export interface RefForwardingComponent<T, P = object> extends ForwardRefRenderFunction<T, P> {};
 
+export type Mutable<T> = {
+    -readonly [P in keyof T]: T[P];
+};
+
 type NCoreUIKitConfig = ThemesType & LocalizeType & ModalType;
 
 declare global {