diff --git a/.circleci/config.yml b/.circleci/config.yml index bd7493f6b..dd98d8c41 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -21,6 +21,12 @@ jobs: at: ~/coralproject/talk - restore_cache: key: dependency-cache-{{ checksum "package-lock.json" }} + - run: + name: Update NPM + command: sudo npm update -g npm + - run: + name: Audit dependencies + command: npm audit - run: name: Install dependencies command: npm install @@ -46,6 +52,10 @@ jobs: # unit_tests will run the unit tests. unit_tests: <<: *job_defaults + environment: + <<: *job_environment + CI: true + JEST_JUNIT_OUTPUT: "reports/junit/js-test-results.xml" steps: - checkout - attach_workspace: @@ -55,7 +65,13 @@ jobs: command: npm run compile - run: name: Perform testing - command: npm run test + # We're running these tests in band to avoid errors where the circleci + # test runner runs out of RAM trying to run them all in parallel. + command: npm run test -- --ci --runInBand --testResultsProcessor="jest-junit" + - store_test_results: + path: reports/junit + - store_artifacts: + path: reports/junit # build will build the static assets and server typescript files. build: diff --git a/config/watcher.ts b/config/watcher.ts index f974e4283..1235e0e9c 100644 --- a/config/watcher.ts +++ b/config/watcher.ts @@ -42,7 +42,9 @@ const config: Config = { }, runDocz: { paths: [], - executor: new LongRunningExecutor("npm run docz -- dev"), + executor: new LongRunningExecutor( + "NODE_ENV=development npm run docz -- dev" + ), }, }, defaultSet: "client", diff --git a/doczrc.js b/doczrc.js index 9d7ef1b12..8f78be5b9 100644 --- a/doczrc.js +++ b/doczrc.js @@ -25,6 +25,10 @@ export default { options: { modules: true, importLoaders: 1, + localIdentName: + process.env.NODE_ENV === "production" + ? "[hash:base64]" + : "[name]-[local]-[hash:base64:5]", }, }, { diff --git a/package-lock.json b/package-lock.json index f4ddb8d3b..4f66c4e68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12421,6 +12421,35 @@ "pretty-format": "^23.2.0" } }, + "jest-junit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-5.1.0.tgz", + "integrity": "sha512-3EVf1puv2ox5wybQDfLX3AEn3IKOgDV4E76y4pO2hBu46DEtAFZZAm//X1pzPQpqKji0zqgMIzqzF/K+uGAX9A==", + "dev": true, + "requires": { + "jest-validate": "^23.0.1", + "mkdirp": "^0.5.1", + "strip-ansi": "^4.0.0", + "xml": "^1.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, "jest-leak-detector": { "version": "23.2.0", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-23.2.0.tgz", @@ -14289,6 +14318,12 @@ "css-mediaquery": "^0.1.2" } }, + "material-design-icons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/material-design-icons/-/material-design-icons-3.0.1.tgz", + "integrity": "sha1-mnHEh0chjrylHlGmbaaCA4zct78=", + "dev": true + }, "math-expression-evaluator": { "version": "1.2.17", "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz", @@ -21758,6 +21793,18 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "typeface-manuale": { + "version": "0.0.54", + "resolved": "https://registry.npmjs.org/typeface-manuale/-/typeface-manuale-0.0.54.tgz", + "integrity": "sha512-wWgWFPSvoxEDdm8Dmq0cZzgmBfRXho5WhxU6x4dKBk7WIBsyvRgcjT+j7cQ3ah/9VYp1+nJzQWhCue+TiqA7FA==", + "dev": true + }, + "typeface-source-sans-pro": { + "version": "0.0.54", + "resolved": "https://registry.npmjs.org/typeface-source-sans-pro/-/typeface-source-sans-pro-0.0.54.tgz", + "integrity": "sha512-4nsvldyZsOBuvR4oEuUMTquJFVssP4iIVnMEIU4paCWo2940b6r/t95sk7KwDpCablS8DprM/YlhpgqnS7gpKg==", + "dev": true + }, "typescript": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", @@ -23133,6 +23180,12 @@ "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", "dev": true }, + "xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=", + "dev": true + }, "xml-name-validator": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", diff --git a/package.json b/package.json index b7e66c3d5..b3f29fe42 100644 --- a/package.json +++ b/package.json @@ -113,8 +113,10 @@ "graphql-playground-middleware-express": "^1.7.2", "html-webpack-plugin": "^3.2.0", "jest": "^23.4.1", + "jest-junit": "^5.1.0", "jsdom": "^11.11.0", "loader-utils": "^1.1.0", + "material-design-icons": "^3.0.1", "npm-run-all": "^4.1.3", "postcss-advanced-variables": "^2.3.3", "postcss-css-variables": "^0.9.0", @@ -157,6 +159,8 @@ "tslint-plugin-prettier": "^1.3.0", "tslint-react": "^3.6.0", "typed-css-modules": "^0.3.4", + "typeface-manuale": "0.0.54", + "typeface-source-sans-pro": "0.0.54", "typescript": "^2.9.2", "uglifyjs-webpack-plugin": "^1.2.5", "webpack": "4.12.0", diff --git a/scripts/test.js b/scripts/test.js index f08b18de6..59c8a93cc 100644 --- a/scripts/test.js +++ b/scripts/test.js @@ -22,7 +22,11 @@ let argv = process.argv.slice(2); argv.push("--config", paths.appJestConfig); // Watch unless on CI or in coverage mode -if (!process.env.CI && argv.indexOf("--coverage") < 0) { +if ( + !process.env.CI && // ensure that the ci env var is not set. + argv.indexOf("--ci") < 0 && // ensure that the ci flag is not passed + argv.indexOf("--coverage") < 0 // ensure that the coverage flag is not passed +) { argv.push("--watch"); } diff --git a/src/core/client/framework/lib/relay/createMutationContainer.tsx b/src/core/client/framework/lib/relay/createMutationContainer.tsx index d0a91e92d..0446e7d17 100644 --- a/src/core/client/framework/lib/relay/createMutationContainer.tsx +++ b/src/core/client/framework/lib/relay/createMutationContainer.tsx @@ -22,10 +22,10 @@ function createMutationContainer( ): InferableComponentEnhancer<{ [P in T]: (input: I) => Promise }> { return compose( withContext(({ relayEnvironment }) => ({ relayEnvironment })), - hoistStatics((WrappedComponent: React.ComponentType) => { + hoistStatics((BaseComponent: React.ComponentType) => { class CreateMutationContainer extends React.Component { public static displayName = wrapDisplayName( - WrappedComponent, + BaseComponent, "createMutationContainer" ); @@ -38,7 +38,7 @@ function createMutationContainer( const inject = { [propName]: this.commit, }; - return ; + return ; } } return CreateMutationContainer as React.ComponentType; diff --git a/src/core/client/framework/lib/relay/withLocalStateContainer.tsx b/src/core/client/framework/lib/relay/withLocalStateContainer.tsx index 9de4180bb..9eaeb0ce8 100644 --- a/src/core/client/framework/lib/relay/withLocalStateContainer.tsx +++ b/src/core/client/framework/lib/relay/withLocalStateContainer.tsx @@ -34,7 +34,7 @@ function withLocalStateContainer( ): InferableComponentEnhancer<{ local: T }> { return compose( withContext(({ relayEnvironment }) => ({ relayEnvironment })), - hoistStatics((WrappedComponent: React.ComponentType) => { + hoistStatics((BaseComponent: React.ComponentType) => { class LocalStateContainer extends React.Component { constructor(props: Props) { super(props); @@ -65,7 +65,7 @@ function withLocalStateContainer( public render() { const { relayEnvironment: _, ...rest } = this.props; - return ; + return ; } } return LocalStateContainer as React.ComponentType; diff --git a/src/core/client/framework/types.ts b/src/core/client/framework/types.ts index 6ea1faf15..60a609441 100644 --- a/src/core/client/framework/types.ts +++ b/src/core/client/framework/types.ts @@ -1,2 +1,2 @@ // TODO: (@cvle) Extract useful common types into its own package. -export { Diff, Omit, Overwrite, PropTypesOf } from "talk-ui/types"; +export { Omit, Overwrite, PropTypesOf } from "talk-ui/types"; diff --git a/src/core/client/stream/components/ReplyList.spec.tsx b/src/core/client/stream/components/ReplyList.spec.tsx index 093d96049..76750c8a0 100644 --- a/src/core/client/stream/components/ReplyList.spec.tsx +++ b/src/core/client/stream/components/ReplyList.spec.tsx @@ -39,4 +39,11 @@ describe("when there is more", () => { .simulate("click"); expect((props.onShowAll as SinonSpy).calledOnce).toBe(true); }); + + const wrapperDisabledButton = shallow( + + ); + it("disables load more button", () => { + expect(wrapperDisabledButton).toMatchSnapshot(); + }); }); diff --git a/src/core/client/stream/components/__snapshots__/App.spec.tsx.snap b/src/core/client/stream/components/__snapshots__/App.spec.tsx.snap index 6a12e4e09..2fc77d00c 100644 --- a/src/core/client/stream/components/__snapshots__/App.spec.tsx.snap +++ b/src/core/client/stream/components/__snapshots__/App.spec.tsx.snap @@ -1,14 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders correctly 1`] = ` - - + `; exports[`renders correctly when asset is null 1`] = ` diff --git a/src/core/client/stream/components/__snapshots__/ReplyList.spec.tsx.snap b/src/core/client/stream/components/__snapshots__/ReplyList.spec.tsx.snap index e48482470..5707b755f 100644 --- a/src/core/client/stream/components/__snapshots__/ReplyList.spec.tsx.snap +++ b/src/core/client/stream/components/__snapshots__/ReplyList.spec.tsx.snap @@ -2,7 +2,7 @@ exports[`renders correctly 1`] = ` - - + + +`; + +exports[`when there is more disables load more button 1`] = ` + + + + + + + Show All Replies + + + `; exports[`when there is more renders a load more button 1`] = ` - - + `; diff --git a/src/core/client/stream/components/__snapshots__/Stream.spec.tsx.snap b/src/core/client/stream/components/__snapshots__/Stream.spec.tsx.snap index a8210fd99..441437216 100644 --- a/src/core/client/stream/components/__snapshots__/Stream.spec.tsx.snap +++ b/src/core/client/stream/components/__snapshots__/Stream.spec.tsx.snap @@ -10,14 +10,14 @@ exports[`renders correctly 1`] = ` - - - - + - - + + `; @@ -71,14 +71,14 @@ exports[`when there is more disables load more button 1`] = ` - - - - + - + @@ -133,7 +133,7 @@ exports[`when there is more disables load more button 1`] = ` Load More - + `; @@ -147,14 +147,14 @@ exports[`when there is more renders a load more button 1`] = ` - - - - + - + @@ -209,6 +209,6 @@ exports[`when there is more renders a load more button 1`] = ` Load More - + `; diff --git a/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap b/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap index b3d193c41..56a90c377 100644 --- a/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap +++ b/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap @@ -33,6 +33,9 @@ exports[`loads more comments 1`] = ` onBlur={[Function]} onFocus={[Function]} onMouseDown={[Function]} + onMouseOut={[Function]} + onMouseOver={[Function]} + onTouchEnd={[Function]} > Post @@ -169,6 +172,9 @@ exports[`renders comment stream 1`] = ` onBlur={[Function]} onFocus={[Function]} onMouseDown={[Function]} + onMouseOut={[Function]} + onMouseOver={[Function]} + onTouchEnd={[Function]} > Post @@ -247,6 +253,9 @@ exports[`renders comment stream 1`] = ` onClick={[Function]} onFocus={[Function]} onMouseDown={[Function]} + onMouseOut={[Function]} + onMouseOver={[Function]} + onTouchEnd={[Function]} > Load More diff --git a/src/core/client/stream/test/__snapshots__/renderReplies.spec.tsx.snap b/src/core/client/stream/test/__snapshots__/renderReplies.spec.tsx.snap index 24fe4fe1d..56758219a 100644 --- a/src/core/client/stream/test/__snapshots__/renderReplies.spec.tsx.snap +++ b/src/core/client/stream/test/__snapshots__/renderReplies.spec.tsx.snap @@ -33,6 +33,9 @@ exports[`renders comment stream 1`] = ` onBlur={[Function]} onFocus={[Function]} onMouseDown={[Function]} + onMouseOut={[Function]} + onMouseOver={[Function]} + onTouchEnd={[Function]} > Post diff --git a/src/core/client/stream/test/__snapshots__/renderStream.spec.tsx.snap b/src/core/client/stream/test/__snapshots__/renderStream.spec.tsx.snap index 3a3599547..433957511 100644 --- a/src/core/client/stream/test/__snapshots__/renderStream.spec.tsx.snap +++ b/src/core/client/stream/test/__snapshots__/renderStream.spec.tsx.snap @@ -33,6 +33,9 @@ exports[`renders comment stream 1`] = ` onBlur={[Function]} onFocus={[Function]} onMouseDown={[Function]} + onMouseOut={[Function]} + onMouseOver={[Function]} + onTouchEnd={[Function]} > Post diff --git a/src/core/client/stream/test/__snapshots__/showAllReplies.spec.tsx.snap b/src/core/client/stream/test/__snapshots__/showAllReplies.spec.tsx.snap index 2762b7dae..a8570269a 100644 --- a/src/core/client/stream/test/__snapshots__/showAllReplies.spec.tsx.snap +++ b/src/core/client/stream/test/__snapshots__/showAllReplies.spec.tsx.snap @@ -33,6 +33,9 @@ exports[`renders comment stream 1`] = ` onBlur={[Function]} onFocus={[Function]} onMouseDown={[Function]} + onMouseOut={[Function]} + onMouseOver={[Function]} + onTouchEnd={[Function]} > Post @@ -114,6 +117,9 @@ exports[`renders comment stream 1`] = ` onClick={[Function]} onFocus={[Function]} onMouseDown={[Function]} + onMouseOut={[Function]} + onMouseOver={[Function]} + onTouchEnd={[Function]} > Show All Replies @@ -158,6 +164,9 @@ exports[`show all replies 1`] = ` onBlur={[Function]} onFocus={[Function]} onMouseDown={[Function]} + onMouseOut={[Function]} + onMouseOver={[Function]} + onTouchEnd={[Function]} > Post diff --git a/src/core/client/ui/components/BaseButton/BaseButton.css b/src/core/client/ui/components/BaseButton/BaseButton.css index be2f0fa20..14b111258 100644 --- a/src/core/client/ui/components/BaseButton/BaseButton.css +++ b/src/core/client/ui/components/BaseButton/BaseButton.css @@ -8,3 +8,6 @@ outline-color: -webkit-focus-ring-color; outline-style: auto; } + +.mouseHover { +} diff --git a/src/core/client/ui/components/BaseButton/BaseButton.spec.tsx b/src/core/client/ui/components/BaseButton/BaseButton.spec.tsx new file mode 100644 index 000000000..b7754c865 --- /dev/null +++ b/src/core/client/ui/components/BaseButton/BaseButton.spec.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import TestRenderer from "react-test-renderer"; + +import { PropTypesOf } from "talk-framework/types"; + +import BaseButton from "./BaseButton"; + +it("renders correctly", () => { + const props: PropTypesOf = { + className: "my-class", + children: "Push Me", + }; + const renderer = TestRenderer.create(); + expect(renderer.toJSON()).toMatchSnapshot(); +}); + +it("renders as anchor", () => { + const props: PropTypesOf = { + anchor: true, + children: "Push Me", + }; + const renderer = TestRenderer.create(); + expect(renderer.toJSON()).toMatchSnapshot(); +}); diff --git a/src/core/client/ui/components/BaseButton/BaseButton.tsx b/src/core/client/ui/components/BaseButton/BaseButton.tsx index 1d97a9d0c..45c2c7985 100644 --- a/src/core/client/ui/components/BaseButton/BaseButton.tsx +++ b/src/core/client/ui/components/BaseButton/BaseButton.tsx @@ -1,8 +1,13 @@ import cn from "classnames"; -import React from "react"; +import React, { Ref } from "react"; import { ButtonHTMLAttributes, StatelessComponent } from "react"; -import { withKeyboardFocus, withStyles } from "talk-ui/hocs"; +import { + withForwardRef, + withKeyboardFocus, + withMouseHover, + withStyles, +} from "talk-ui/hocs"; import { PropTypesOf } from "talk-ui/types"; import * as styles from "./BaseButton.css"; @@ -10,7 +15,6 @@ import * as styles from "./BaseButton.css"; interface InnerProps extends ButtonHTMLAttributes { /** If set renders an anchor tag instead */ anchor?: boolean; - /** * This prop can be used to add custom classnames. * It is handled by the `withStyles `HOC. @@ -18,7 +22,16 @@ interface InnerProps extends ButtonHTMLAttributes { classes: typeof styles; /** This is passed by the `withKeyboardFocus` HOC */ - keyboardFocus: boolean; + keyboardFocus?: boolean; + + /** This is passed by the `withMouseHover` HOC */ + mouseHover?: boolean; + + /** ref to the HTMLButtonElement */ + ref?: Ref; + + /** Internal: Forwarded Ref */ + forwardRef?: Ref; } /** @@ -30,6 +43,8 @@ const BaseButton: StatelessComponent = ({ className, classes, keyboardFocus, + mouseHover, + forwardRef, type: typeProp, ...rest }) => { @@ -51,11 +66,14 @@ const BaseButton: StatelessComponent = ({ const rootClassName = cn(classes.root, className, { [classes.keyboardFocus]: keyboardFocus, + [classes.mouseHover]: mouseHover, }); - return ; + return ; }; -const enhanced = withStyles(styles)(withKeyboardFocus(BaseButton)); +const enhanced = withForwardRef( + withStyles(styles)(withMouseHover(withKeyboardFocus(BaseButton))) +); export type BaseButtonProps = PropTypesOf; export default enhanced; diff --git a/src/core/client/ui/components/BaseButton/__snapshots__/BaseButton.spec.tsx.snap b/src/core/client/ui/components/BaseButton/__snapshots__/BaseButton.spec.tsx.snap new file mode 100644 index 000000000..c6fd97a78 --- /dev/null +++ b/src/core/client/ui/components/BaseButton/__snapshots__/BaseButton.spec.tsx.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders as anchor 1`] = ` + + Push Me + +`; + +exports[`renders correctly 1`] = ` + +`; diff --git a/src/core/client/ui/components/Button/Button.css b/src/core/client/ui/components/Button/Button.css index 17283a819..9124a859f 100644 --- a/src/core/client/ui/components/Button/Button.css +++ b/src/core/client/ui/components/Button/Button.css @@ -1,35 +1,50 @@ .root { composes: button from "talk-ui/shared/typography.css"; - &:enabled, - &:disabled { - padding: 5px 20px; - border-radius: var(--round-corners); - background-color: transparent; - /* TODO: hover styles for the default button */ + padding: 5px 15px; + border-radius: var(--round-corners); + background-color: transparent; + position: relative; + display: flex; + justify-content: center; + align-items: center; + + & > * { + margin: 0 calc(0.5 * var(--spacing-unit)) 0 0; + } + & > *:last-child { + margin: 0; } } -.root:disabled { +.root.disabled { opacity: 0.4; cursor: default; } -.fullWidth:enabled, -.fullWidth:disabled { +.fullWidth { display: block; width: 100%; box-sizing: border-box; } -.primary:enabled, -.primary:disabled { +.regular:not(.disabled) { + &.mouseHover { + color: var(--palette-secondary-light); + } + &:active { + color: var(--palette-secondary-lighter); + } +} + +.primary, +.primary:not(.disabled) { background-color: var(--palette-primary-main); color: #fff; } -.primary:enabled { - &:hover { +.primary:not(.disabled) { + &.mouseHover { background-color: var(--palette-primary-light); } @@ -38,15 +53,15 @@ } } -.primary:enabled.invert, -.primary:disabled.invert { +.primary.invert, +.primary.invert:not(.disabled) { background-color: transparent; border: 1px solid var(--palette-primary-main); color: var(--palette-primary-main); } -.primary:enabled.invert { - &:hover { +.primary.invert:not(.disabled) { + &.mouseHover { border-color: var(--palette-primary-light); color: var(--palette-primary-light); } @@ -57,14 +72,14 @@ } } -.secondary:enabled, -.secondary:disabled { +.secondary, +.secondary:not(.disabled) { background-color: var(--palette-secondary-main); color: #fff; } -.secondary:enabled { - &:hover { +.secondary:not(.disabled) { + &.mouseHover { background-color: var(--palette-secondary-light); } &:active { @@ -72,15 +87,15 @@ } } -.secondary:enabled.invert, -.secondary:disabled.invert { +.secondary.invert, +.secondary:not(.disabled).invert { background-color: transparent; border: 1px solid var(--palette-secondary-main); color: var(--palette-secondary-main); } -.secondary:enabled.invert { - &:hover { +.secondary:not(.disabled).invert { + &.mouseHover { border-color: var(--palette-secondary-light); color: var(--palette-secondary-light); } @@ -90,10 +105,3 @@ color: var(--palette-secondary-lighter); } } - -/** - * This seems to be the best way to target modern touch device browsers. - */ -@media (-moz-touch-enabled: 1), (pointer: coarse) { - /* TODO: Remove hover styles */ -} diff --git a/src/core/client/ui/components/Button/Button.mdx b/src/core/client/ui/components/Button/Button.mdx index 5be82c540..011e47ea5 100644 --- a/src/core/client/ui/components/Button/Button.mdx +++ b/src/core/client/ui/components/Button/Button.mdx @@ -5,23 +5,38 @@ menu: UI Kit import { Playground } from 'docz' import Button from './Button' +import Icon from '../Icon' +import Flex from '../Flex' # Button ## Basic usage - - - - - - - - - - - - - + + + + + + + + + + + + + + + + +## Button with Icon + + + + + + + + + diff --git a/src/core/client/ui/components/Button/Button.tsx b/src/core/client/ui/components/Button/Button.tsx index dadb7a35d..aaeaeb518 100644 --- a/src/core/client/ui/components/Button/Button.tsx +++ b/src/core/client/ui/components/Button/Button.tsx @@ -1,8 +1,8 @@ import cn from "classnames"; import { pick } from "lodash"; -import React, { ButtonHTMLAttributes } from "react"; +import React, { ButtonHTMLAttributes, Ref } from "react"; -import { withStyles } from "talk-ui/hocs"; +import { withForwardRef, withStyles } from "talk-ui/hocs"; import { PropTypesOf } from "talk-ui/types"; import BaseButton, { BaseButtonProps } from "../BaseButton"; @@ -28,6 +28,12 @@ interface InnerProps extends ButtonHTMLAttributes { /** If set renders a button with secondary colors */ secondary?: boolean; + + /** ref to the HTMLButtonElement */ + ref?: Ref; + + /** Internal: Forwarded Ref */ + forwardRef?: Ref; } class Button extends React.Component { @@ -39,6 +45,8 @@ class Button extends React.Component { invert, primary, secondary, + disabled, + forwardRef, ...rest } = this.props; @@ -47,18 +55,22 @@ class Button extends React.Component { [classes.fullWidth]: fullWidth, [classes.primary]: primary, [classes.secondary]: secondary, + [classes.regular]: !primary && !secondary, + [classes.disabled]: disabled, }); return ( ); } } -const enhanced = withStyles(styles)(Button); +const enhanced = withForwardRef(withStyles(styles)(Button)); export type ButtonProps = PropTypesOf; export default enhanced; diff --git a/src/core/client/ui/components/Flex/Flex.css b/src/core/client/ui/components/Flex/Flex.css index 13cb05be5..b28d3aa0f 100644 --- a/src/core/client/ui/components/Flex/Flex.css +++ b/src/core/client/ui/components/Flex/Flex.css @@ -1,5 +1,8 @@ .root { display: flex; + & > * { + flex-shrink: 0; + } } .halfItemGutter { @@ -50,6 +53,51 @@ } } +.wrap { + flex-wrap: wrap; +} + +.wrapReverse { + flex-wrap: wrap-reverse; +} + +.wrap, +.wrapReverse { + &.itemGutter { + &:not(:empty) { + margin-top: calc(-1 * var(--spacing-unit)); + } + & > * { + margin-top: var(--spacing-unit); + } + } + &.directionColumn.itemGutter { + &:not(:empty) { + margin-left: calc(-1 * var(--spacing-unit)); + } + &.itemGutter > * { + margin-left: var(--spacing-unit); + } + } + + &.halfItemGutter { + &:not(:empty) { + margin-top: calc(-0.5 * var(--spacing-unit)); + } + & > * { + margin-top: calc(0.5 * var(--spacing-unit)); + } + } + &.directionColumn.halfItemGutter { + &:not(:empty) { + margin-left: calc(-0.5 * var(--spacing-unit)); + } + &.halfItemGutter > * { + margin-left: calc(0.5 * var(--spacing-unit)); + } + } +} + .justifyFlexStart { justify-content: flex-start; } diff --git a/src/core/client/ui/components/Flex/Flex.spec.tsx b/src/core/client/ui/components/Flex/Flex.spec.tsx index b774d9961..5c68f0de0 100644 --- a/src/core/client/ui/components/Flex/Flex.spec.tsx +++ b/src/core/client/ui/components/Flex/Flex.spec.tsx @@ -1,5 +1,5 @@ -import { shallow } from "enzyme"; import React from "react"; +import TestRenderer from "react-test-renderer"; import { PropTypesOf } from "talk-ui/types"; @@ -11,10 +11,58 @@ it("renders correctly", () => { alignItems: "center", direction: "row", }; - const wrapper = shallow( + const renderer = TestRenderer.create(
Hello World
); - expect(wrapper).toMatchSnapshot(); + expect(renderer.toJSON()).toMatchSnapshot(); +}); + +it("renders with wrap", () => { + const props: PropTypesOf = { + wrap: true, + }; + const renderer = TestRenderer.create( + +
Hello World
+
+ ); + expect(renderer.toJSON()).toMatchSnapshot(); +}); + +it("renders with wrap reverse", () => { + const props: PropTypesOf = { + wrap: "reverse", + }; + const renderer = TestRenderer.create( + +
Hello World
+
+ ); + expect(renderer.toJSON()).toMatchSnapshot(); +}); + +it("renders with item gutter", () => { + const props: PropTypesOf = { + itemGutter: true, + }; + const renderer = TestRenderer.create( + +
Hello World
+
+ ); + expect(renderer.toJSON()).toMatchSnapshot(); +}); + +it("renders with halfe item gutter", () => { + const props: PropTypesOf = { + itemGutter: "half", + }; + const renderer = TestRenderer.create( + +
Hello World
+
+ ); + expect(renderer.toJSON()).toMatchSnapshot(); }); diff --git a/src/core/client/ui/components/Flex/Flex.tsx b/src/core/client/ui/components/Flex/Flex.tsx index bfba06d05..62d8ff66c 100644 --- a/src/core/client/ui/components/Flex/Flex.tsx +++ b/src/core/client/ui/components/Flex/Flex.tsx @@ -1,12 +1,18 @@ import cn from "classnames"; -import React from "react"; +import React, { Ref } from "react"; import { StatelessComponent } from "react"; import { pascalCase } from "talk-common/utils"; +import { withForwardRef, withStyles } from "talk-ui/hocs"; import * as styles from "./Flex.css"; interface InnerProps { + /** + * This prop can be used to add custom classnames. + * It is handled by the `withStyles `HOC. + */ + classes: typeof styles; id?: string; role?: string; justifyContent?: @@ -20,38 +26,52 @@ interface InnerProps { direction?: "row" | "column" | "row-reverse" | "column-reverse"; itemGutter?: boolean | "half"; className?: string; + wrap?: boolean | "reverse"; + + /** Ref to the root element */ + ref?: Ref; + + /** Internal: Forwarded Ref */ + forwardRef?: Ref; } const Flex: StatelessComponent = props => { const { + classes, className, justifyContent, alignItems, direction, itemGutter, + wrap, + forwardRef, ...rest } = props; const classObject: Record = { - [styles.itemGutter]: itemGutter === true, - [styles.halfItemGutter]: itemGutter === "half", + [classes.itemGutter]: itemGutter === true, + [classes.halfItemGutter]: itemGutter === "half", + [classes.wrap]: wrap === true, + [classes.wrapReverse]: wrap === "reverse", }; if (justifyContent) { - classObject[(styles as any)[`justify${pascalCase(justifyContent)}`]] = true; + classObject[ + (classes as any)[`justify${pascalCase(justifyContent)}`] + ] = true; } if (alignItems) { - classObject[(styles as any)[`align${pascalCase(alignItems)}`]] = true; + classObject[(classes as any)[`align${pascalCase(alignItems)}`]] = true; } if (direction) { - classObject[(styles as any)[`direction${pascalCase(direction)}`]] = true; + classObject[(classes as any)[`direction${pascalCase(direction)}`]] = true; } - const classNames: string = cn(styles.root, className, classObject); + const classNames: string = cn(classes.root, className, classObject); - return
; + return
; }; -export default Flex; +export default withForwardRef(withStyles(styles)(Flex)); diff --git a/src/core/client/ui/components/Flex/__snapshots__/Flex.spec.tsx.snap b/src/core/client/ui/components/Flex/__snapshots__/Flex.spec.tsx.snap index 90ef93c13..31f1f73c1 100644 --- a/src/core/client/ui/components/Flex/__snapshots__/Flex.spec.tsx.snap +++ b/src/core/client/ui/components/Flex/__snapshots__/Flex.spec.tsx.snap @@ -9,3 +9,43 @@ exports[`renders correctly 1`] = `
`; + +exports[`renders with halfe item gutter 1`] = ` +
+
+ Hello World +
+
+`; + +exports[`renders with item gutter 1`] = ` +
+
+ Hello World +
+
+`; + +exports[`renders with wrap 1`] = ` +
+
+ Hello World +
+
+`; + +exports[`renders with wrap reverse 1`] = ` +
+
+ Hello World +
+
+`; diff --git a/src/core/client/ui/components/Icon/Icon.css b/src/core/client/ui/components/Icon/Icon.css new file mode 100644 index 000000000..37fb390d5 --- /dev/null +++ b/src/core/client/ui/components/Icon/Icon.css @@ -0,0 +1,54 @@ +@font-face { + font-family: "Material Icons"; + font-style: normal; + font-weight: 400; + src: local("Material Icons"), local("MaterialIcons-Regular"), + url(material-design-icons/iconfont/MaterialIcons-Regular.woff2) + format("woff2"), + url(material-design-icons/iconfont/MaterialIcons-Regular.woff) + format("woff"), + url(material-design-icons/iconfont/MaterialIcons-Regular.ttf) + format("truetype"); +} + +.root { + font-family: "Material Icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + overflow: hidden; + vertical-align: middle; + display: inline-block; + letter-spacing: 0; + + /* Enable Ligatures */ + font-feature-settings: "liga"; + font-variant-ligatures: "discretionary-ligatures"; + + /* Support for Safari and Chrome. */ + text-rendering: optimizeLegibility; + + /* Better Font Rendering */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.sm { + font-size: 18px; + width: 18px; +} +.md { + font-size: 24px; + width: 24px; +} +.lg { + font-size: 36px; + width: 36px; +} +.xl { + font-size: 48px; + width: 48px; +} diff --git a/src/core/client/ui/components/Icon/Icon.mdx b/src/core/client/ui/components/Icon/Icon.mdx new file mode 100644 index 000000000..83428c28b --- /dev/null +++ b/src/core/client/ui/components/Icon/Icon.mdx @@ -0,0 +1,21 @@ +--- +name: Icon +menu: UI Kit +--- + +import { Playground, PropsTable } from 'docz' +import Icon from './Icon' + +# Icon + +Renders an icon. + +Checkout available icons https://material.io/tools/icons/ + +## Basic usage + + face + face + face + face + diff --git a/src/core/client/ui/components/Icon/Icon.spec.tsx b/src/core/client/ui/components/Icon/Icon.spec.tsx new file mode 100644 index 000000000..93cf68a66 --- /dev/null +++ b/src/core/client/ui/components/Icon/Icon.spec.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import TestRenderer from "react-test-renderer"; + +import { PropTypesOf } from "talk-framework/types"; + +import Icon from "./Icon"; + +it("renders correctly", () => { + const props: PropTypesOf = { + children: "face", + }; + const renderer = TestRenderer.create(); + expect(renderer.toJSON()).toMatchSnapshot(); +}); + +it("renders correctly with specified size", () => { + const props: PropTypesOf = { + size: "lg", + children: "bookmark", + }; + const renderer = TestRenderer.create(); + expect(renderer.toJSON()).toMatchSnapshot(); +}); diff --git a/src/core/client/ui/components/Icon/Icon.tsx b/src/core/client/ui/components/Icon/Icon.tsx new file mode 100644 index 000000000..754a46955 --- /dev/null +++ b/src/core/client/ui/components/Icon/Icon.tsx @@ -0,0 +1,40 @@ +import cn from "classnames"; +import React, { HTMLAttributes, Ref, StatelessComponent } from "react"; + +import { withForwardRef, withStyles } from "talk-ui/hocs"; +import { PropTypesOf } from "talk-ui/types"; + +import * as styles from "./Icon.css"; + +interface InnerProps extends HTMLAttributes { + /** + * This prop can be used to add custom classnames. + * It is handled by the `withStyles `HOC. + */ + classes: typeof styles; + + size?: "sm" | "md" | "lg" | "xl"; + + /** The name of the icon to render */ + children: string; + + /** ref to the HTMLIconElement */ + ref?: Ref; + + /** Internal: Forwarded Ref */ + forwardRef?: Ref; +} + +const Icon: StatelessComponent = props => { + const { classes, className, size, forwardRef, ...rest } = props; + const rootClassName = cn(classes.root, className, classes[size!]); + return ; +}; + +Icon.defaultProps = { + size: "sm", +}; + +const enhanced = withForwardRef(withStyles(styles)(Icon)); +export type IconProps = PropTypesOf; +export default enhanced; diff --git a/src/core/client/ui/components/Icon/__snapshots__/Icon.spec.tsx.snap b/src/core/client/ui/components/Icon/__snapshots__/Icon.spec.tsx.snap new file mode 100644 index 000000000..5e5a8c969 --- /dev/null +++ b/src/core/client/ui/components/Icon/__snapshots__/Icon.spec.tsx.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` + + face + +`; + +exports[`renders correctly with specified size 1`] = ` + + bookmark + +`; diff --git a/src/core/client/ui/components/Icon/index.ts b/src/core/client/ui/components/Icon/index.ts new file mode 100644 index 000000000..3e8146f8b --- /dev/null +++ b/src/core/client/ui/components/Icon/index.ts @@ -0,0 +1,2 @@ +export * from "./Icon"; +export { default } from "./Icon"; diff --git a/src/core/client/ui/components/RelativeTime/RelativeTime.tsx b/src/core/client/ui/components/RelativeTime/RelativeTime.tsx index 3a8022656..136bbca4f 100644 --- a/src/core/client/ui/components/RelativeTime/RelativeTime.tsx +++ b/src/core/client/ui/components/RelativeTime/RelativeTime.tsx @@ -1,9 +1,9 @@ import cn from "classnames"; -import React from "react"; +import React, { Ref } from "react"; import TimeAgo, { Formatter } from "react-timeago"; import { UIContext } from "talk-ui/components"; -import { withStyles } from "talk-ui/hocs"; +import { withForwardRef, withStyles } from "talk-ui/hocs"; import { PropTypesOf } from "talk-ui/types"; import * as styles from "./RelativeTime.css"; @@ -14,29 +14,33 @@ interface InnerProps { classes: typeof styles; className?: string; formatter?: Formatter; + + /** Ref to the root element */ + ref?: Ref; + + /** Internal: Forwarded Ref */ + forwardRef?: Ref; } const defaultFormatter: Formatter = (value, unit, suffix, timestamp: string) => new Date(timestamp).toISOString(); -class RelativeTime extends React.Component { - public render() { - const { date, classes, live, className, formatter } = this.props; - return ( - - {({ timeagoFormatter }) => ( - - )} - - ); - } -} +const RelativeTime: React.StatelessComponent = props => { + const { date, classes, live, className, formatter } = props; + return ( + + {({ timeagoFormatter }) => ( + + )} + + ); +}; -const enhanced = withStyles(styles)(RelativeTime); +const enhanced = withForwardRef(withStyles(styles)(RelativeTime)); export type RelativeTimeProps = PropTypesOf; export default enhanced; diff --git a/src/core/client/ui/components/ToggleShow/ToggleShow.spec.tsx b/src/core/client/ui/components/ToggleShow/ToggleShow.spec.tsx index 35bb6a01f..d81075019 100644 --- a/src/core/client/ui/components/ToggleShow/ToggleShow.spec.tsx +++ b/src/core/client/ui/components/ToggleShow/ToggleShow.spec.tsx @@ -18,13 +18,13 @@ it("renders correctly", () => { expect(tree).toMatchSnapshot(); }); -it("should work correctly", () => { +it("should hide the div", () => { const renderer = create( {({ toggleShow, show }) => (
{show &&
SHOW ME
} - +
)}
diff --git a/src/core/client/ui/components/ToggleShow/__snapshots__/ToggleShow.spec.tsx.snap b/src/core/client/ui/components/ToggleShow/__snapshots__/ToggleShow.spec.tsx.snap index 26bb513b1..323274282 100644 --- a/src/core/client/ui/components/ToggleShow/__snapshots__/ToggleShow.spec.tsx.snap +++ b/src/core/client/ui/components/ToggleShow/__snapshots__/ToggleShow.spec.tsx.snap @@ -13,12 +13,12 @@ exports[`renders correctly 1`] = ` `; -exports[`should work correctly 1`] = ` +exports[`should hide the div 1`] = `
`; diff --git a/src/core/client/ui/components/TrapFocus/TrapFocus.mdx b/src/core/client/ui/components/TrapFocus/TrapFocus.mdx new file mode 100644 index 000000000..d5c209aa9 --- /dev/null +++ b/src/core/client/ui/components/TrapFocus/TrapFocus.mdx @@ -0,0 +1,39 @@ +--- +name: TrapFocus +menu: UI Kit +--- + +import { Playground } from 'docz' +import TrapFocus from './TrapFocus' + +# TrapFocus + +Traps focus inside component when using keyboard navigation for accessibility purposes. + +## Basic usage + +```ts +import React from "react"; + +class Dialog extends React.Component { + private firstFocusable: HTMLElement; + private lastFocusable: HTMLElement; + + private setFirstFocusable = (ref: HTMLElement) => (this.firstFocusable = ref); + private setLastFocusable = (ref: HTMLElement) => (this.lastFocusable = ref); + + public render() { + return ( +
+ + + + +
+ ); + } +} +``` diff --git a/src/core/client/ui/components/TrapFocus/TrapFocus.spec.tsx b/src/core/client/ui/components/TrapFocus/TrapFocus.spec.tsx new file mode 100644 index 000000000..f4932775f --- /dev/null +++ b/src/core/client/ui/components/TrapFocus/TrapFocus.spec.tsx @@ -0,0 +1,71 @@ +import React from "react"; +import { create } from "react-test-renderer"; +import Sinon from "sinon"; + +import { PropTypesOf } from "talk-ui/types"; +import TrapFocus from "./TrapFocus"; + +it("renders correctly", () => { + const props: PropTypesOf = { + firstFocusable: null, + lastFocusable: null, + children: ( + <> + child1 + child2 + + ), + }; + const renderer = create( +
+ +
+ ); + expect(renderer.toJSON()).toMatchSnapshot(); +}); + +it("Change focus to `lastFocusable` when focus reaches beginning", () => { + const fakeHTMLElementBegin = { focus: Sinon.spy() }; + const fakeHTMLElementEnd = { focus: Sinon.spy() }; + const props: PropTypesOf = { + firstFocusable: fakeHTMLElementBegin as any, + lastFocusable: fakeHTMLElementEnd as any, + children: ( + <> + child1 + child2 + + ), + }; + const renderer = create( +
+ +
+ ); + renderer.root.findAllByProps({ tabIndex: 0 })[0].props.onFocus(); + expect(fakeHTMLElementBegin.focus.called).toBe(false); + expect(fakeHTMLElementEnd.focus.called).toBe(true); +}); + +it("Change focus to `firstFocusable` when focus reaches the end", () => { + const fakeHTMLElementBegin = { focus: Sinon.spy() }; + const fakeHTMLElementEnd = { focus: Sinon.spy() }; + const props: PropTypesOf = { + firstFocusable: fakeHTMLElementBegin as any, + lastFocusable: fakeHTMLElementEnd as any, + children: ( + <> + child1 + child2 + + ), + }; + const renderer = create( +
+ +
+ ); + renderer.root.findAllByProps({ tabIndex: 0 })[1].props.onFocus(); + expect(fakeHTMLElementBegin.focus.called).toBe(true); + expect(fakeHTMLElementEnd.focus.called).toBe(false); +}); diff --git a/src/core/client/ui/components/TrapFocus/TrapFocus.tsx b/src/core/client/ui/components/TrapFocus/TrapFocus.tsx new file mode 100644 index 000000000..a50b50d6c --- /dev/null +++ b/src/core/client/ui/components/TrapFocus/TrapFocus.tsx @@ -0,0 +1,27 @@ +import React from "react"; + +export interface Focusable { + focus: () => void; +} + +export interface TrapFocusProps { + firstFocusable: Focusable | null; + lastFocusable: Focusable | null; + children: React.ReactNode; +} + +export default class TrapFocus extends React.Component { + // Trap keyboard focus inside the dropdown until a value has been chosen. + public focusBegin = () => this.props.firstFocusable!.focus(); + public focusEnd = () => this.props.lastFocusable!.focus(); + + public render() { + return ( + <> +
+ {this.props.children} +
+ + ); + } +} diff --git a/src/core/client/ui/components/TrapFocus/__snapshots__/TrapFocus.spec.tsx.snap b/src/core/client/ui/components/TrapFocus/__snapshots__/TrapFocus.spec.tsx.snap new file mode 100644 index 000000000..b39ad808e --- /dev/null +++ b/src/core/client/ui/components/TrapFocus/__snapshots__/TrapFocus.spec.tsx.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +
+
+ + child1 + + + child2 + +
+
+`; diff --git a/src/core/client/ui/components/TrapFocus/index.ts b/src/core/client/ui/components/TrapFocus/index.ts new file mode 100644 index 000000000..965ecf53a --- /dev/null +++ b/src/core/client/ui/components/TrapFocus/index.ts @@ -0,0 +1,2 @@ +export * from "./TrapFocus"; +export { default } from "./TrapFocus"; diff --git a/src/core/client/ui/components/Typography/Typography.tsx b/src/core/client/ui/components/Typography/Typography.tsx index b743305e6..b78347944 100644 --- a/src/core/client/ui/components/Typography/Typography.tsx +++ b/src/core/client/ui/components/Typography/Typography.tsx @@ -1,8 +1,8 @@ import cn from "classnames"; -import React from "react"; +import React, { Ref } from "react"; import { HTMLAttributes, ReactNode, StatelessComponent } from "react"; -import { withStyles } from "talk-ui/hocs"; +import { withForwardRef, withStyles } from "talk-ui/hocs"; import { PropTypesOf } from "talk-ui/types"; import * as styles from "./Typography.css"; @@ -77,6 +77,12 @@ interface InnerProps extends HTMLAttributes { * Applies the theme typography styles. */ variant?: Variant; + + /** Ref to the root element */ + ref?: Ref; + + /** Internal: Forwarded Ref */ + forwardRef?: Ref; } const Typography: StatelessComponent = props => { @@ -91,6 +97,7 @@ const Typography: StatelessComponent = props => { noWrap, paragraph, variant, + forwardRef, ...rest } = props; @@ -116,7 +123,7 @@ const Typography: StatelessComponent = props => { const Component = component || (paragraph ? "p" : headlineMapping![variant!]) || "span"; - return ; + return ; }; Typography.defaultProps = { @@ -139,6 +146,6 @@ Typography.defaultProps = { variant: "body1", }; -const enhanced = withStyles(styles)(Typography); +const enhanced = withForwardRef(withStyles(styles)(Typography)); export type CenterProps = PropTypesOf; export default enhanced; diff --git a/src/core/client/ui/components/index.ts b/src/core/client/ui/components/index.ts index 3d81f1aba..91ddaeb68 100644 --- a/src/core/client/ui/components/index.ts +++ b/src/core/client/ui/components/index.ts @@ -5,3 +5,4 @@ export { default as RelativeTime } from "./RelativeTime"; export { default as UIContext, UIContextProps } from "./UIContext"; export { default as Flex } from "./Flex"; export { default as MatchMedia } from "./MatchMedia"; +export { default as TrapFocus } from "./TrapFocus"; diff --git a/src/core/client/ui/hocs/index.ts b/src/core/client/ui/hocs/index.ts index 1c49cdc24..6102cc5ce 100644 --- a/src/core/client/ui/hocs/index.ts +++ b/src/core/client/ui/hocs/index.ts @@ -1,2 +1,4 @@ export { default as withStyles } from "./withStyles"; export { default as withKeyboardFocus } from "./withKeyboardFocus"; +export { default as withMouseHover } from "./withMouseHover"; +export { default as withForwardRef } from "./withForwardRef"; diff --git a/src/core/client/ui/hocs/withForwardRef.tsx b/src/core/client/ui/hocs/withForwardRef.tsx new file mode 100644 index 000000000..883923634 --- /dev/null +++ b/src/core/client/ui/hocs/withForwardRef.tsx @@ -0,0 +1,36 @@ +import React, { Ref } from "react"; + +// TODO: ForwardRef API is not supported in enzymes shallow rendering +// https://github.com/airbnb/enzyme/issues/1553 +// As for now we do props passing instead until full React16 support lands in +// enzyme. + +/** + * withForwardRef provides a property called `forwardRef` using + * the `React.forwardRef` api. + */ +/* +function withForwardRef

; forwardRef?: Ref }>( + BaseComponent: React.ComponentType

+): React.ComponentType> { + const forwardRef: RefForwardingComponent = (props, ref) => ( + + ); + return React.forwardRef(forwardRef); +} + +// TODO: workaround, add bug link. +export default withForwardRef as < + P extends { ref?: Ref; forwardRef?: Ref } +>( + BaseComponent: React.ComponentType

+) => React.ComponentType

; + +*/ + +// Stub, currently doesn't do anything except adding types. +export default function withForwardRef

}>( + BaseComponent: React.ComponentType

+): React.ComponentType

{ + return BaseComponent; +} diff --git a/src/core/client/ui/hocs/withKeyboardFocus.tsx b/src/core/client/ui/hocs/withKeyboardFocus.tsx index eaae1529d..dff7d0e1e 100644 --- a/src/core/client/ui/hocs/withKeyboardFocus.tsx +++ b/src/core/client/ui/hocs/withKeyboardFocus.tsx @@ -3,10 +3,10 @@ import { FocusEvent, MouseEvent } from "react"; import { hoistStatics } from "recompose"; interface InjectedProps { - onFocus?: React.EventHandler>; - onBlur?: React.EventHandler>; - onMouseDown?: React.EventHandler>; - keyboardFocus?: boolean; + onFocus: React.EventHandler>; + onBlur: React.EventHandler>; + onMouseDown: React.EventHandler>; + keyboardFocus: boolean; } /** @@ -14,8 +14,8 @@ interface InjectedProps { * to indicate a focus on the element, that wasn't triggered by mouse * or touch. */ -export default hoistStatics( - (WrappedComponent: React.ComponentType) => { +const withKeyboardFocus = hoistStatics( + (BaseComponent: React.ComponentType) => { class WithKeyboardFocus extends React.Component { public state = { keyboardFocus: false, @@ -48,7 +48,7 @@ export default hoistStatics( public render() { return ( - ( return WithKeyboardFocus as React.ComponentType; } ); + +// TODO: workaround, add bug link. +export default withKeyboardFocus as

>( + BaseComponent: React.ComponentType

+) => React.ComponentType

; diff --git a/src/core/client/ui/hocs/withMouseHover.tsx b/src/core/client/ui/hocs/withMouseHover.tsx new file mode 100644 index 000000000..8172012f0 --- /dev/null +++ b/src/core/client/ui/hocs/withMouseHover.tsx @@ -0,0 +1,69 @@ +import * as React from "react"; +import { MouseEvent, TouchEvent } from "react"; +import { hoistStatics } from "recompose"; + +interface InjectedProps { + onMouseOver: React.EventHandler>; + onMouseOut: React.EventHandler>; + onTouchEnd: React.EventHandler>; + mouseHover: boolean; +} + +/** + * withMouseHover provides a property `MouseHover: boolean` + * to indicate a focus on the element, that wasn't triggered by mouse + * or touch. + */ +const withMouseHover = hoistStatics( + (BaseComponent: React.ComponentType) => { + class WithMouseHover extends React.Component { + public state = { + mouseHover: false, + lastTouchEndTime: 0, + }; + + private handleTouchEnd: React.EventHandler> = event => { + if (this.props.onTouchEnd) { + this.props.onTouchEnd(event); + } + this.setState({ lastTouchEndTime: new Date().getTime() }); + }; + + private handleMouseOver: React.EventHandler> = event => { + if (this.props.onMouseOver) { + this.props.onMouseOver(event); + } + const now = new Date().getTime(); + if (now - this.state.lastTouchEndTime > 750) { + this.setState({ mouseHover: true }); + } + }; + + private handleMouseOut: React.EventHandler> = event => { + if (this.props.onMouseOut) { + this.props.onMouseOut(event); + } + this.setState({ mouseHover: false }); + }; + + public render() { + return ( + + ); + } + } + + return WithMouseHover as React.ComponentType; + } +); + +// TODO: workaround, add bug link. +export default withMouseHover as

>( + BaseComponent: React.ComponentType

+) => React.ComponentType

; diff --git a/src/core/client/ui/hocs/withStyles.ts b/src/core/client/ui/hocs/withStyles.ts index 0ae53e860..d8ae4313b 100644 --- a/src/core/client/ui/hocs/withStyles.ts +++ b/src/core/client/ui/hocs/withStyles.ts @@ -13,17 +13,18 @@ function withStyles( ): DefaultingInferableComponentEnhancer<{ classes?: Partial }> { const classes = { ...(styles as any) }; return withPropsOnChange(["classes"], props => { + const resolvedClasses = { ...classes }; if (props.classes) { Object.keys(props.classes).forEach(k => { if (classes[k]) { - classes[k] += ` ${props.classes[k]}`; + resolvedClasses[k] += ` ${props.classes[k]}`; } else if (process.env.NODE_ENV !== "production") { // tslint:disable:next-line: no-console console.warn("Extending non existant className", k); } }); } - return { classes }; + return { classes: resolvedClasses }; }); } diff --git a/src/core/client/ui/shared/typography.css b/src/core/client/ui/shared/typography.css index 3cb483cd2..edc334b96 100644 --- a/src/core/client/ui/shared/typography.css +++ b/src/core/client/ui/shared/typography.css @@ -1,3 +1,61 @@ +@font-face { + font-family: "Source Sans Pro"; + font-style: normal; + font-display: swap; + font-weight: 300; + src: local("Source Sans Pro Light "), local("Source Sans Pro-Light"), + url("typeface-source-sans-pro/files/source-sans-pro-latin-300.woff2") + format("woff2"), + url("typeface-source-sans-pro/files/source-sans-pro-latin-300.woff") + format("woff"); +} + +@font-face { + font-family: "Source Sans Pro"; + font-style: normal; + font-display: swap; + font-weight: 400; + src: local("Source Sans Pro Regular italic"), + local("Source Sans Pro-Regularitalic"), + url("typeface-source-sans-pro/files/source-sans-pro-latin-400.woff2") + format("woff2"), + url("typeface-source-sans-pro/files/source-sans-pro-latin-400.woff") + format("woff"); +} + +@font-face { + font-family: "Source Sans Pro"; + font-style: normal; + font-display: swap; + font-weight: 600; + src: local("Source Sans Pro SemiBold italic"), + local("Source Sans Pro-SemiBolditalic"), + url("typeface-source-sans-pro/files/source-sans-pro-latin-600.woff2") + format("woff2"), + url("typeface-source-sans-pro/files/source-sans-pro-latin-600.woff") + format("woff"); +} + +@font-face { + font-family: "Manuale"; + font-style: normal; + font-display: swap; + font-weight: 400; + src: local("Manuale Regular "), local("Manuale-Regular"), + url("typeface-manuale/files/manuale-latin-400.woff2") format("woff2"), + url("typeface-manuale/files/manuale-latin-400.woff") format("woff"); +} + +@font-face { + font-family: "Manuale"; + font-style: normal; + font-display: swap; + font-weight: 600; + src: local("Manuale SemiBold "), local("Manuale-SemiBold"), + url("typeface-manuale/files/manuale-latin-600.woff2") format("woff2"), + url("typeface-manuale/files/manuale-latin-600.woff") format("woff"); +} + .heading1 { font-size: calc(24rem / var(--rem-base)); font-weight: var(--font-weight-medium); diff --git a/src/core/client/ui/theme/variables.ts b/src/core/client/ui/theme/variables.ts index d68b88a41..6e404f10f 100644 --- a/src/core/client/ui/theme/variables.ts +++ b/src/core/client/ui/theme/variables.ts @@ -19,7 +19,7 @@ const variables = { dark: "#65696B", main: "#787D80", light: "#9A9DA0", - lighter: "#636E72", + lighter: "#BBBEBF", }, /* Success colors */ success: { @@ -68,7 +68,7 @@ const variables = { fontSize: 16, fontWeightLight: 300, fontWeightRegular: 400, - fontWeightMedium: 550, + fontWeightMedium: 600, /* Breakpoints */ breakpoints: { xs: 320, diff --git a/src/core/client/ui/types.ts b/src/core/client/ui/types.ts index 576b881c3..55c7d638b 100644 --- a/src/core/client/ui/types.ts +++ b/src/core/client/ui/types.ts @@ -2,27 +2,19 @@ import React from "react"; // TODO: Extract useful common types into its own package. -/** - * Returns literals types that are in T but not in U. - * - * E.g. Diff<"a" | "b", "a"> = "b" - */ -export type Diff = ({ [P in T]: P } & - { [P in U]: never } & { [x: string]: never; [x: number]: never })[T]; - /** * Overwrite properties of `T`. * * E.g. Omit<{a: boolean, b: boolean}, "b"> = {a: boolean} */ -export type Omit = Pick>; +export type Omit = Pick>; /** * Overwrite properties of `T`. * * E.g. Overwrite<{a: boolean}, {a: string}> = {a: string} */ -export type Overwrite = Pick> & U; +export type Overwrite = Pick> & U; /** * Returns the PropTypes from a React Component.