Browse Source

Refactor Button With Style Reducers, Without Icons

Francesco Baccetti 4 years ago
parent
commit
7ca12b0a0c

+ 60 - 35
packages/components/src/components/Button/Button.style.ts

@@ -1,11 +1,11 @@
-import { css } from "@emotion/core";
 import { typography, colors } from "../../theme";
-import { Reducer, StyleFn, stripInline } from "../../utils";
+import { makeStyles, StyleFn, StyleObj } from "../../utils";
+import { disabled, dimensionsFromProps, log } from "../../theme/fragments";
 
 export type ButtonStyleProps = {
 	text?: string;
 	type?: "primary" | "secondary";
-	width?: "normal" | "full";
+	full?: boolean;
 	size?: "regular" | "small" | "smaller";
 	disabled?: boolean;
 };
@@ -13,6 +13,8 @@ export type ButtonStyleProps = {
 const baseStyles: StyleFn = () => ({
 	borderWidth: "1px",
 	borderStyle: "solid",
+	fontFamily: typography.fonts.base,
+	fontWeight: typography.weights.medium,
 	display: "inline-flex",
 	justifyContent: "center",
 	alignItems: "center",
@@ -21,7 +23,6 @@ const baseStyles: StyleFn = () => ({
 		background: "transparent",
 	},
 });
-
 const colorFromType: StyleFn = (styles, { type }: ButtonStyleProps) => {
 	switch (type) {
 		case "primary":
@@ -45,56 +46,81 @@ const colorFromType: StyleFn = (styles, { type }: ButtonStyleProps) => {
 		case "secondary":
 			return {
 				...styles,
-				backgroundColor: colors.blue[500],
+				backgroundColor: colors.black,
 				borderColor: colors.blue[500],
 
 				"&:hover": {
-					backgroundColor: colors.black,
 					borderColor: colors.blue[700],
 					color: colors.blue[300],
 				},
 
 				"&:active": {
-					backgroundColor: colors.black,
 					borderColor: colors.blue[700],
 					color: colors.blue[700],
 				},
 			};
 	}
 };
-const widthFromSize: StyleFn = (styles, { width }) => ({
-	...styles,
-	display:
-		width === "full" && styles.display.includes("inline") ? stripInline(styles.display as string) : styles.display,
-	width: width === "full" ? "100%" : styles.width,
-});
+const paddingFromType: StyleFn = (
+	styles,
+	{ size, children, full }: { size: "regular" | "small" | "smaller"; children?: React.ReactNode; full: boolean }
+) => {
+	const buttonHeight = size === "regular" ? "20px" : size === "small" ? "15px" : "10px";
+	return {
+		...styles,
+		margin: `0 ${full ? "0" : "15px"} 0 0`,
+		padding:
+			size === "regular"
+				? !!children
+					? "14px 17px"
+					: "14px"
+				: size === "small"
+				? !!children
+					? "12px 14px"
+					: "12px"
+				: "10px",
+		fontSize:
+			size === "regular"
+				? typography.sizes.button.large
+				: size === "small"
+				? typography.sizes.button.medium
+				: typography.sizes.button.small,
+
+		height: buttonHeight,
+		maxHeight: buttonHeight,
+	};
+};
 
-export let makeStyles = ({
-	text,
-	type = "primary",
-	width = "normal",
-	size = "regular",
-	disabled = false,
-}: ButtonStyleProps) => {
-	const colorReducer = Reducer(colorFromType);
-	const widthReducer = Reducer(widthFromSize);
-	const baseReducer = Reducer(baseStyles);
-
-	let finalStyles = baseReducer
-		.concat(colorReducer)
-		.concat(widthReducer)
-		.run({}, { text, type, width, size, disabled });
-
-	return css(finalStyles);
+const iconStyles: StyleFn = (styles, { children, size }) => {
+	return {
+		...styles,
+		marginRight: !!children ? "10px" : "0",
+		fontSize:
+			size === "regular"
+				? typography.sizes.icon.large
+				: size === "small"
+				? typography.sizes.icon.medium
+				: typography.sizes.icon.small,
+
+		"& > path:nth-of-type(1)": {
+			color: "inherit",
+			flexShrink: 0,
+		},
+	};
 };
-// export let makeStyles = ({
+
+export const useCSS = (props: ButtonStyleProps) => ({
+	container: makeStyles([baseStyles, colorFromType, dimensionsFromProps, paddingFromType, disabled])(props),
+	icon: makeStyles([iconStyles])(props),
+});
+
 // 	text,
 // 	type = "primary",
 // 	width = "normal",
 // 	size = "regular",
 // 	disabled = false,
 // }: ButtonStyleProps) => {
-// 	const buttonHeight = size === "regular" ? "20px" : size === "small" ? "15px" : "10px";
+//
 
 // 	const primaryButton = {
 // 		container: css`
@@ -116,9 +142,8 @@ export let makeStyles = ({
 // 				: size === "small"
 // 				? typography.sizes.button.medium
 // 				: typography.sizes.button.small};
-// 			margin: 0 ${width === "normal" ? "15px" : "0"} 0 0;
-// 			height: ${buttonHeight};
-// 			max-height: ${buttonHeight};
+//
+//
 // 		`,
 // 	};
 

+ 14 - 9
packages/components/src/components/Button/Button.tsx

@@ -1,21 +1,26 @@
 import React from "react";
-import { makeStyles, ButtonStyleProps } from "./Button.style";
+import { ButtonStyleProps, useCSS } from "./Button.style";
 import { IconProp } from "@fortawesome/fontawesome-svg-core";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 
 type ButtonProps = {
-	text?: string;
+	children?: React.ReactNode;
 	icon?: IconProp;
 	disabled?: boolean;
 	onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
 } & ButtonStyleProps;
 
-export default function Button({ text = "", icon, disabled = false, onClick, ...styleProps }: ButtonProps) {
-	let styles = makeStyles({ text, disabled, ...styleProps });
-	console.log("styles", styles);
+export default function Button({
+	children,
+	icon,
+	type = "primary",
+	disabled = false,
+	onClick,
+	...styleProps
+}: ButtonProps) {
+	let styles = useCSS({ disabled, type, ...styleProps });
 	return (
-		<div css={styles} onClick={disabled ? null : onClick}>
-			{text}
-		</div>
+		<button css={styles.container} onClick={disabled ? null : onClick}>
+			{children}
+		</button>
 	);
 }

+ 3 - 3
packages/components/src/components/Button/index.ts

@@ -1,3 +1,3 @@
-import { memo } from "react"
-import Button from "./Button"
-export default memo(Button)
+import { memo } from "react";
+import Button from "./Button";
+export default memo(Button);

+ 67 - 15
packages/components/src/theme/fragments.ts

@@ -1,21 +1,73 @@
+import { StyleFn } from "./../utils/style-reducer";
 import { css } from "@emotion/core";
 import spacing from "./spacing";
 import typography from "./typography";
+import colors from "./colors";
+import { StyleObj, stripInline } from "../utils";
 
 export function withSize(size: string) {
-  return css`
-    padding: ${size === "large"
-      ? `${spacing.s4} ${spacing.s2}`
-      : size === "normal" || size === "full"
-      ? `${spacing.s3} ${spacing.s2}`
-      : `${spacing.s2} ${spacing.s1}`};
-
-    font-size: ${size === "large"
-      ? typography.sizes.large
-      : size === "normal" || size === "full"
-      ? typography.sizes.normal
-      : typography.sizes.small};
-
-    width: ${size === "full" ? "100%" : "auto"};
-  `;
+	return css`
+		padding: ${size === "large"
+			? `${spacing.s4} ${spacing.s2}`
+			: size === "normal" || size === "full"
+			? `${spacing.s3} ${spacing.s2}`
+			: `${spacing.s2} ${spacing.s1}`};
+
+		font-size: ${size === "large"
+			? typography.sizes.large
+			: size === "normal" || size === "full"
+			? typography.sizes.normal
+			: typography.sizes.small};
+
+		width: ${size === "full" ? "100%" : "auto"};
+	`;
+}
+
+export function log(styles: StyleObj, props: any) {
+	console.log("styles", styles);
+	console.log("props", props);
+	return styles;
+}
+
+export function dimensionsFromProps(styles: StyleObj, { full }: { full: boolean }) {
+	let display: string;
+	if (styles.display == null) {
+		display = "block";
+	}
+	display = styles.display as string;
+
+	return {
+		...styles,
+		display: full && display.includes("inline") ? stripInline(display) : display,
+		width: full ? "100%" : styles.width || "",
+	};
+}
+
+export function disabled(styles: StyleObj, { disabled }: { disabled: boolean }): StyleObj {
+	if (!disabled) {
+		return styles;
+	}
+
+	return {
+		...unsetStyles(styles),
+		backgroundColor: colors.gray[100],
+		color: colors.white,
+	};
+}
+
+function unsetStyles(styles: StyleObj): StyleObj {
+	// Filter and unset all properties that give color, on all states.
+	// Need to add more?
+	const colorProperties = ["color", "backgroundColor", "borderColor", "boxShadow", "fill", "stroke"];
+
+	const filteredEntries = Object.entries(styles).map(([key, value]) => {
+		// If it has a psuedo selector, we're going to disable color from that as well.
+		if (key.includes("&:hover") || key.includes("&:active") || key.includes("&:focus")) {
+			return unsetStyles(value as StyleObj);
+		} else if (colorProperties.includes(key)) {
+			return [key, "unset"];
+		}
+		return [key, value];
+	});
+	return Object.fromEntries(filteredEntries as any);
 }

+ 1 - 0
packages/components/src/theme/index.ts

@@ -2,3 +2,4 @@ export { default as typography } from "./typography";
 export { default as colors } from "./colors";
 export { default as spacing } from "./spacing";
 export { default as breakpoints } from "./breakpoints";
+export * from "./fragments";

+ 20 - 1
packages/components/src/utils/style-reducer.ts

@@ -1,8 +1,12 @@
 import * as CSS from "csstype";
+import { css } from "@emotion/core";
 
-type StyleObj = { [k in keyof CSS.Properties]: number | string };
+//FIXME: This is not correctly typed, since it doesn't
+export type StyleObj = { [k in keyof CSS.Properties]: number | string | StyleObj };
 export type StyleFn = (style: StyleObj, x: any) => StyleObj;
 
+export type Modifiers = { [k: string]: StyleFn };
+
 // TODO: Properly type this
 type StyleMonad = (
 	run: StyleFn
@@ -19,3 +23,18 @@ export const Reducer = (run: StyleFn) => ({
 	map: (f: (x: any) => any) => Reducer((styles: StyleObj, props: any) => f(run(styles, props))),
 	contramap: (f: (x: any) => any) => Reducer((styles: StyleObj, props: any) => run(styles, f(props))),
 });
+
+export function combineReducers(...reducers: StyleFn[]) {
+	return reducers.reduce(
+		(acc, reducer) => acc.concat(Reducer(reducer)),
+		Reducer(() => ({}))
+	);
+}
+
+export function makeStyles(reducers: StyleFn[]) {
+	const reducer = combineReducers(...reducers);
+	return function(props: any) {
+		const styles: any = reducer.run({}, props);
+		return css(styles);
+	};
+}

+ 73 - 53
packages/components/stories/01-Button.stories.tsx

@@ -1,72 +1,92 @@
-import React from "react"
-import { Button } from "../src"
-import { faBan } from "@fortawesome/free-solid-svg-icons"
+import React from "react";
+import { Button } from "../src";
+import { faBan } from "@fortawesome/free-solid-svg-icons";
 
 export default {
-  title: "Button",
-  component: Button,
-}
+	title: "Button",
+	component: Button,
+};
 
 export const Primary = () => (
-  <>
-    <Button text="Button" onClick={() => console.log("Button clicked!")} />
-    <Button text="Button" size="small" onClick={() => console.log("Button clicked!")} />
-    <Button text="Button" size="smaller" onClick={() => console.log("Button clicked!")} />
-  </>
-)
+	<>
+		<Button onClick={() => console.log("Button clicked!")}>Regular</Button>
+		<Button size="small" onClick={() => console.log("Button clicked!")}>
+			Small
+		</Button>
+		<Button size="smaller" onClick={() => console.log("Button clicked!")}>
+			Smaller
+		</Button>
+	</>
+);
 
 export const Secondary = () => (
-  <>
-    <Button text="Button" type="secondary" />
-    <Button text="Button" type="secondary" size="small" />
-    <Button text="Button" type="secondary" size="smaller" />
-  </>
-)
+	<>
+		<Button type="secondary">Regular</Button>
+		<Button type="secondary" size="small">
+			Small
+		</Button>
+		<Button type="secondary" size="smaller">
+			Smaller
+		</Button>
+	</>
+);
 
-export const PrimaryFullSize = () => (
-  <Button text="Button" width="full" />
-)
+export const PrimaryFullSize = () => <Button full>Primary Full Size</Button>;
 
 export const SecondaryFullSize = () => (
-  <Button text="Button" width="full" type="secondary" />
-)
+	<Button full type="secondary">
+		Secondary Full Size
+	</Button>
+);
 
 export const PrimaryWithIcon = () => (
-  <>
-    <Button text="Button" icon={faBan} />
-    <Button text="Button" icon={faBan} size="small" />
-    <Button text="Button" icon={faBan} size="smaller" />
-  </>
-)
+	<>
+		<Button icon={faBan}>Regular</Button>
+		<Button icon={faBan} size="small">
+			Small
+		</Button>
+		<Button icon={faBan} size="smaller">
+			Smaller
+		</Button>
+	</>
+);
 
 export const SecondaryWithIcon = () => (
-  <>
-    <Button text="Button" type="secondary" icon={faBan} />
-    <Button text="Button" type="secondary" icon={faBan} size="small" />
-    <Button text="Button" type="secondary" icon={faBan} size="smaller" />
-  </>
-)
+	<>
+		<Button type="secondary" icon={faBan}>
+			Regular
+		</Button>
+		<Button type="secondary" icon={faBan} size="small">
+			Small
+		</Button>
+		<Button type="secondary" icon={faBan} size="smaller">
+			Smaller
+		</Button>
+	</>
+);
 
 export const PrimaryWithoutText = () => (
-  <>
-    <Button icon={faBan} />
-    <Button icon={faBan} size="small" />
-    <Button icon={faBan} size="smaller" />
-  </>
-)
+	<>
+		<Button icon={faBan} />
+		<Button icon={faBan} size="small" />
+		<Button icon={faBan} size="smaller" />
+	</>
+);
 
 export const SecondaryWithoutText = () => (
-  <>
-    <Button type="secondary" icon={faBan} />
-    <Button type="secondary" icon={faBan} size="small" />
-    <Button type="secondary" icon={faBan} size="smaller" />
-  </>
-)
+	<>
+		<Button type="secondary" icon={faBan} />
+		<Button type="secondary" icon={faBan} size="small" />
+		<Button type="secondary" icon={faBan} size="smaller" />
+	</>
+);
 
 export const Disabled = () => (
-  <>
-    <Button disabled={true} text="Button" onClick={() => console.log("Button clicked!")} />
-    <Button disabled={true} text="Button" icon={faBan} onClick={() => console.log("Button clicked!")} />
-    <Button disabled={true} icon={faBan} onClick={() => console.log("Button clicked!")} />
-  </>
-)
+	<>
+		<Button disabled={true}>Disabled</Button>
+		<Button disabled={true} icon={faBan}>
+			Disabled with icon
+		</Button>
+		<Button disabled={true} icon={faBan} />
+	</>
+);

+ 11 - 10
packages/components/tsconfig.json

@@ -1,14 +1,15 @@
 {
-  "extends": "../../tsconfig.json",
+	"extends": "../../tsconfig.json",
 
-  "compilerOptions": {
-    "module": "esnext",
-    "rootDirs": ["src", "stories"],
-    "baseUrl": "src",
-    "jsx": "preserve",
-    "declaration": true,
-    "declarationDir": "dist/types"
-  },
+	"compilerOptions": {
+		"module": "esnext",
+		"lib": ["ES2019"],
+		"rootDirs": ["src", "stories"],
+		"baseUrl": "src",
+		"jsx": "preserve",
+		"declaration": true,
+		"declarationDir": "dist/types"
+	},
 
-  "exclude": ["node_modules", "dist", "stories"]
+	"exclude": ["node_modules", "dist", "stories"]
 }