diff --git a/cypress/components/__image_snapshots__/Button Basic Rendering renders without default theme #0.png b/cypress/components/__image_snapshots__/Button Basic Rendering renders without default theme #0.png new file mode 100644 index 00000000..20b7a725 Binary files /dev/null and b/cypress/components/__image_snapshots__/Button Basic Rendering renders without default theme #0.png differ diff --git a/cypress/components/__image_snapshots__/Button renders and shows inner text without a provider #0.png b/cypress/components/__image_snapshots__/Button renders and shows inner text without a provider #0.png deleted file mode 100644 index 4b20f282..00000000 Binary files a/cypress/components/__image_snapshots__/Button renders and shows inner text without a provider #0.png and /dev/null differ diff --git a/cypress/components/__image_snapshots__/Button renders with a theme provider #0.png b/cypress/components/__image_snapshots__/Button renders with a theme provider #0.png deleted file mode 100644 index 59a7cc6f..00000000 Binary files a/cypress/components/__image_snapshots__/Button renders with a theme provider #0.png and /dev/null differ diff --git a/cypress/components/__image_snapshots__/Default Theme Button with Variants #0.png b/cypress/components/__image_snapshots__/Default Theme Button with Variants #0.png new file mode 100644 index 00000000..7b4e9423 Binary files /dev/null and b/cypress/components/__image_snapshots__/Default Theme Button with Variants #0.png differ diff --git a/cypress/components/button.spec.js b/cypress/components/button.spec.js new file mode 100644 index 00000000..61d38cd4 --- /dev/null +++ b/cypress/components/button.spec.js @@ -0,0 +1,131 @@ +/// + +import React from 'react'; +import { mount } from 'cypress-react-unit-test'; +import { + Button, + ThemeProvider, + GlobalStyles, + defaultTheme, +} from '../../dist/minerva-ui.esm'; + +import { createGlobalStyle } from 'styled-components'; + +const text = 'Button'; + +const defaultFont = 'Open Sans'; + +// by default, we are using the native font stack +// but this font is different on macOS, Linux and Windows +// to make sure our screenshots are consistent, we force them all to use the same font family +const StandardizeFont = createGlobalStyle` + html, * { + font-family: defaultFont; + } +`; + +const customTheme = { + ...defaultTheme, + fonts: { + ...defaultTheme.fonts, + body: defaultFont, + heading: defaultFont, + }, +}; + +const MinervaProvider = ({ children, theme = customTheme }) => ( + + + +
+ + +
+ {children} +
+); + +describe(' + + + + + ); + + cy.contains(text).should('be.visible'); + + cy.document() + .its('fonts.status') + .should('equal', 'loaded'); + + cy.get('#container').toMatchImageSnapshot({ + name: `Default Theme: Button with Variants`, + }); + }); + + it('renders without default theme', () => { + mount( + + + + ); + + cy.contains(text).should('be.visible'); + cy.document() + .its('fonts.status') + .should('equal', 'loaded'); + + cy.get('button').toMatchImageSnapshot(); + }); + }); + + context('Style Props', () => { + it('should be able to pass basic style props', () => { + const color = 'rgb(227, 227, 227)'; + const backgroundColor = 'rgb(51, 51, 51)'; + mount( + + + + ); + + cy.get('button').should('have.css', 'color', color); + cy.get('button').should('have.css', 'background-color', backgroundColor); + }); + + it('should be able to use pseudo style props', () => { + const backgroundColor = 'rgb(227, 227, 227)'; + mount( + + + + ); + + cy.get('button').should('have.css', 'background-color', backgroundColor); + }); + }); +}); diff --git a/cypress/components/button.spec.tsx b/cypress/components/button.spec.tsx deleted file mode 100644 index e5acbfc5..00000000 --- a/cypress/components/button.spec.tsx +++ /dev/null @@ -1,52 +0,0 @@ -/// - -import React from 'react'; -import { mount } from 'cypress-react-unit-test'; -import { - Button, - ThemeProvider, - GlobalStyles, - defaultTheme, -} from '../../dist/minerva-ui.esm'; -import { createGlobalStyle } from 'styled-components'; - -const text = 'Button'; - -// by default, we are using the native font stack -// but this font is different on macOS, Linux and Windows -// to make sure our screenshots are consistent, we force them all to use the same font family -const StandardizeFont = createGlobalStyle` - html { - font-family: Helvetica; - } -`; - -const customTheme = { - ...defaultTheme, - fonts: { - ...defaultTheme.fonts, - body: 'Helvetica', - heading: 'Helvetica', - }, -}; - -const MinervaProvider = ({ children, theme = customTheme }) => ( - - - - {children} - -); - -describe(' - - ); - - cy.contains(text).should('be.visible'); - cy.get('button').toMatchImageSnapshot(); - }); -}); diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index 8b4ff642..e4c11852 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -1,3 +1,16 @@ { - "extends": "../tsconfig.json" + "compilerOptions": { + "target": "es5", + "lib": [ + "es5", + "dom" + ], + "types": [ + "cypress", "cypress-plugin-snapshots" + ] + }, + "include": [ + "**/*.ts", + "cypress-plugin-snapshots" + ] } \ No newline at end of file diff --git a/package.json b/package.json index 37932853..b042ae5d 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,10 @@ "(.d.ts)$", ".stories.tsx$" ], - "testPathIgnorePatterns": ["/node_modules/", "cypress"], + "testPathIgnorePatterns": [ + "/node_modules/", + "cypress" + ], "moduleNameMapper": { "\\.(css|less)$": "/test/__mocks__/styleMock.js" }, diff --git a/src/Button/index.tsx b/src/Button/index.tsx index dd898113..f46985ab 100644 --- a/src/Button/index.tsx +++ b/src/Button/index.tsx @@ -25,9 +25,9 @@ export const buttonVariants = { }, }, tertiary: { - bg: 'white', - borderColor: 'transparent', - color: 'indigo.800', + bg: '#333', + borderColor: '#333', + color: '#333', _hover: { textDecoration: 'underline', }, @@ -89,25 +89,8 @@ export const Button = forwardRef(function Button( as={Comp} disabled={disabled || isLoading} role="button" - transition="all 150ms ease 0s" - outline="none" cursor="pointer" - fontFamily="body" - _hover={{ - backgroundColor: '#f9fafb', - }} - _focus={{ - borderColor: '#a4cafe', - boxShadow: '0 0 0 3px rgba(118,169,250,.45)', - outline: 0, - }} - _active={{ - borderColor: '#a4cafe', - boxShadow: '0 0 0 3px rgba(118,169,250,.45)', - outline: 0, - }} _disabled={{ - opacity: 0.4, cursor: 'not-allowed', }} aria-busy={isLoading} diff --git a/src/index.tsx b/src/index.tsx index a00f68b5..ecb085cb 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -69,7 +69,7 @@ export { default as Stack } from './Stack'; /** * Inputs */ -export { default as Button } from './Button'; +export * from './Button'; export { default as Checkbox } from './Checkbox'; export { default as Switch } from './Switch'; export { default as Input } from './Input'; diff --git a/src/theme.ts b/src/theme.ts index 1bc1aea3..ea5dd9e8 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -9,7 +9,7 @@ const logoColor = '#551A8B'; export interface ThemeComponent extends React.CSSProperties, PseudoBoxProps {} export interface MinervaTheme extends Theme { - Button?: React.CSSProperties; + Button?: ThemeComponent; Drawer?: React.CSSProperties; Heading?: React.CSSProperties; Modal?: React.CSSProperties; @@ -67,6 +67,26 @@ const defaultTheme: MinervaTheme = { borderRadius: '5px', borderStyle: 'solid', borderColor: '#d2d6dc', + fontFamily: 'body', + outline: 'none', + transition: 'all 150 ms ease 0s', + _hover: { + backgroundColor: '#f9fafb', + }, + _focus: { + borderColor: '#a4cafe', + boxShadow: '0 0 0 3px rgba(118,169,250,.45)', + outline: 0, + }, + _active: { + borderColor: '#a4cafe', + boxShadow: '0 0 0 3px rgba(118,169,250,.45)', + outline: 0, + }, + _disabled: { + opacity: 0.4, + cursor: 'not-allowed', + }, }, Checkbox: {}, Drawer: {}, diff --git a/src/types.tsx b/src/types.tsx new file mode 100644 index 00000000..00bb9644 --- /dev/null +++ b/src/types.tsx @@ -0,0 +1,272 @@ +// These utility types are from Reach UI and make it easier to deal with typing for React components that accept the "as" prop +// https://github.com/reach/reach-ui/blob/main/packages/utils/src/types.tsx + +import * as React from 'react'; +// import { forwardRefWithAs } from './index'; +// import styled from 'styled-components'; + +/** + * React.Ref uses the readonly type `React.RefObject` instead of + * `React.MutableRefObject`, We pretty much always assume ref objects are + * mutable (at least when we create them), so this type is a workaround so some + * of the weird mechanics of using refs with TS. + */ +export type AssignableRef = + | { + bivarianceHack(instance: ValueType | null): void; + }['bivarianceHack'] + | React.MutableRefObject; + +/** + * Type can be either a single `ValueType` or an array of `ValueType` + */ +export type SingleOrArray = ValueType[] | ValueType; + +/** + * The built-in utility type `Omit` does not distribute over unions. So if you + * have: + * + * type A = { a: 'whatever' } + * + * and you want to do a union with: + * + * type B = A & { b: number } | { b: string; c: number } + * + * you might expect `Omit` to give you: + * + * type B = + * | Omit<{ a: "whatever"; b: number }, "a"> + | Omit<{ a: "whatever"; b: string; c: number }, "a">; + * + * This is not the case, unfortunately, so we need to create our own version of + * `Omit` that distributes over unions with a distributive conditional type. If + * you have a generic type parameter `T`, then the construct + * `T extends any ? F : never` will end up distributing the `F<>` operation + * over `T` when `T` is a union type. + * + * @link https://stackoverflow.com/a/59796484/1792019 + * @link http://www.typescriptlang.org/docs/handbook/advanced-types.html#distributive-conditional-types + */ +export type DistributiveOmit< + BaseType, + Key extends PropertyKey +> = BaseType extends any ? Omit : never; + +/** + * Returns the type inferred by a promise's return value. + * + * @example + * async function getThing() { + * // return type is a number + * let result: number = await fetchValueSomewhere(); + * return result; + * } + * + * type Thing = ThenArg>; + * // number + */ +export type ThenArg = T extends PromiseLike ? U : T; + +//////////////////////////////////////////////////////////////////////////////// +// The following types help us deal with the `as` prop. + +export type As = React.ElementType; + +export type PropsWithAs< + ComponentType extends As, + ComponentProps +> = ComponentProps & + Omit< + React.ComponentPropsWithRef, + 'as' | keyof ComponentProps + > & { + as?: ComponentType; + }; + +export type PropsFromAs< + ComponentType extends As, + ComponentProps +> = (PropsWithAs & { as: ComponentType }) & + PropsWithAs; + +// TODO: Remove in 1.0 +export type ComponentWithForwardedRef< + ElementType extends React.ElementType, + ComponentProps +> = React.ForwardRefExoticComponent< + ComponentProps & + React.HTMLProps> & + React.ComponentPropsWithRef +>; + +export interface FunctionComponentWithAs< + DefaultComponentType extends As, + ComponentProps +> { + /** + * Inherited from React.FunctionComponent with modifications to support `as` + */ + ( + props: PropsWithAs, + context?: any + ): React.ReactElement | null; + ( + props: PropsWithAs, + context?: any + ): React.ReactElement | null; + + /** + * Inherited from React.FunctionComponent + */ + displayName?: string; + propTypes?: React.WeakValidationMap< + PropsWithAs + >; + contextTypes?: React.ValidationMap; + defaultProps?: Partial>; +} + +// TODO: Remove in 1.0 +export interface ComponentWithAs + extends FunctionComponentWithAs {} + +interface ExoticComponentWithAs< + DefaultComponentType extends As, + ComponentProps +> { + /** + * **NOTE**: Exotic components are not callable. + * Inherited from React.ExoticComponent with modifications to support `as` + */ + ( + props: PropsWithAs + ): React.ReactElement | null; + ( + props: PropsWithAs & { + as: ComponentType; + } + ): React.ReactElement | null; + + /** + * Inherited from React.ExoticComponent + */ + readonly $$typeof: symbol; +} + +interface NamedExoticComponentWithAs< + DefaultComponentType extends As, + ComponentProps +> extends ExoticComponentWithAs { + /** + * Inherited from React.NamedExoticComponent + */ + displayName?: string; +} + +export interface ForwardRefExoticComponentWithAs< + DefaultComponentType extends As, + ComponentProps +> extends NamedExoticComponentWithAs { + /** + * Inherited from React.ForwardRefExoticComponent + * Will show `ForwardRef(${Component.displayName || Component.name})` in devtools by default, + * but can be given its own specific name + */ + defaultProps?: Partial>; + propTypes?: React.WeakValidationMap< + PropsWithAs + >; +} + +export interface MemoExoticComponentWithAs< + DefaultComponentType extends As, + ComponentProps +> extends NamedExoticComponentWithAs { + readonly type: DefaultComponentType extends React.ComponentType + ? DefaultComponentType + : FunctionComponentWithAs; +} + +export interface ForwardRefWithAsRenderFunction< + DefaultComponentType extends As, + ComponentProps = {} +> { + ( + props: React.PropsWithChildren< + PropsFromAs + >, + ref: + | (( + instance: + | (DefaultComponentType extends keyof ElementTagNameMap + ? ElementTagNameMap[DefaultComponentType] + : any) + | null + ) => void) + | React.MutableRefObject< + | (DefaultComponentType extends keyof ElementTagNameMap + ? ElementTagNameMap[DefaultComponentType] + : any) + | null + > + | null + ): React.ReactElement | null; + displayName?: string; + // explicit rejected with `never` required due to + // https://github.com/microsoft/TypeScript/issues/36826 + /** + * defaultProps are not supported on render functions + */ + defaultProps?: never; + /** + * propTypes are not supported on render functions + */ + propTypes?: never; +} + +export type ElementTagNameMap = HTMLElementTagNameMap & + Pick< + SVGElementTagNameMap, + Exclude + >; + +/* +Test components to make sure our dynamic As prop components work as intended +type PopupProps = { + lol: string; + children?: React.ReactNode | ((value?: number) => JSX.Element); +}; +export const Popup = forwardRefWithAs( + ({ as: Comp = "input", lol, className, children, ...props }, ref) => { + return ( + + {typeof children === "function" ? children(56) : children} + + ); + } +); +export const TryMe1: React.FC = () => { + return ; +}; +export const TryMe2: React.FC = () => { + let ref = React.useRef(null); + return ; +}; +export const TryMe3: React.FC = () => { + return ; +}; +export const TryMe4: React.FC = () => { + return ; +}; +export const Whoa: React.FC<{ + help?: boolean; + lol: string; + name: string; + test: string; +}> = props => { + return ; +}; +let Cool = styled(Whoa)` + padding: 10px; +` +*/ diff --git a/src/utils.ts b/src/utils.ts index 159b42f8..1028013e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,10 @@ +import { forwardRef } from 'react'; import { system } from 'styled-system'; +import { + As, + ForwardRefExoticComponentWithAs, + ForwardRefWithAsRenderFunction, +} from './types'; // const isNumber = n => typeof n === 'number' && !isNaN(n); // const getWidth = (n, scale) => @@ -258,3 +264,23 @@ export const filteredArgs = filterProps.reduce((result, propName) => { result[propName] = { table: { disable: true } }; return result; }, {}); + +/** + * From: https://github.com/reach/reach-ui/blob/97b32791ce33f822f6bc9f07f6cebfb343d8032d/packages/utils/src/index.tsx#L195 + * This is a hack for sure. The thing is, getting a component to intelligently + * infer props based on a component or JSX string passed into an `as` prop is + * kind of a huge pain. Getting it to work and satisfy the constraints of + * `forwardRef` seems dang near impossible. To avoid needing to do this awkward + * type song-and-dance every time we want to forward a ref into a component + * that accepts an `as` prop, we abstract all of that mess to this function for + * the time time being. + */ +export function forwardRefWithAs< + Props, + DefaultComponentType extends As = 'div' +>(render: ForwardRefWithAsRenderFunction) { + return forwardRef(render) as ForwardRefExoticComponentWithAs< + DefaultComponentType, + Props + >; +} diff --git a/test/__snapshots__/accordion.test.tsx.snap b/test/__snapshots__/accordion.test.tsx.snap index b21f8006..1605a753 100644 --- a/test/__snapshots__/accordion.test.tsx.snap +++ b/test/__snapshots__/accordion.test.tsx.snap @@ -30,11 +30,7 @@ exports[` should render 1`] = ` box-sizing: border-box; min-width: 0; color: #374151; - -webkit-transition: all 150ms ease 0s; - transition: all 150ms ease 0s; - outline: none; cursor: pointer; - font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; background-color: #fff; border-width: 1px; color: #374151; @@ -67,6 +63,10 @@ exports[` should render 1`] = ` border-radius: 5px; border-style: solid; border-color: transparent; + font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; + outline: none; + -webkit-transition: all 150 ms ease 0s; + transition: all 150 ms ease 0s; width: 100%; -webkit-flex-direction: row; -ms-flex-direction: row; diff --git a/test/__snapshots__/button.test.tsx.snap b/test/__snapshots__/button.test.tsx.snap index 2bfd5317..45cb203a 100644 --- a/test/__snapshots__/button.test.tsx.snap +++ b/test/__snapshots__/button.test.tsx.snap @@ -5,11 +5,7 @@ exports[`