diff --git a/.circleci/config.yml b/.circleci/config.yml index 4e39c6e87c..664ac3dd93 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,123 +1,141 @@ version: 2 -# this block contains anchors to reusable blocks of config. -references: - setup_env: &setup_env - docker: - - image: circleci/node:8.10.0 - save_cache: &save_cache - key: v9-dependency-cache-{{ checksum "yarn.lock" }} +aliases: + - &docker-node-lts + - image: circleci/node:lts + + - &docker-node-browsers + - image: circleci/node:lts-browsers + environment: + CHROME_BIN: "/usr/bin/google-chrome" + + - &restore-node-modules-cache + name: Restore node_modules cache + key: v2-yarn-deps-{{ checksum "yarn.lock" }} + + - &restore-yarn-cache + name: Restore yarnpkg cache + key: v2-yarn-cache + + - &save-node-modules-cache + name: Save node_modules cache paths: - node_modules - # explicitly list each package node_modules - - packages/core/node_modules - - packages/datetime/node_modules - - packages/docs-app/node_modules - - packages/docs-data/node_modules - - packages/docs-theme/node_modules - - packages/icons/node_modules - - packages/karma-build-scripts/node_modules - - packages/labs/node_modules - - packages/landing-app/node_modules - - packages/node-build-scripts/node_modules - - packages/select/node_modules - - packages/table/node_modules - - packages/table-dev-app/node_modules - - packages/test-commons/node_modules - - packages/test-react15/node_modules - - packages/timezone/node_modules - - packages/tslint-config/node_modules - - packages/webpack-build-scripts/node_modules - restore_cache: &restore_cache - keys: - - v9-dependency-cache-{{ checksum "yarn.lock" }} - - v9-dependency-cache- - persist_to_workspace: &persist_to_workspace - root: '.' + key: v2-yarn-deps-{{ checksum "yarn.lock" }} + + - &save-yarn-cache + name: Save yarnpkg cache paths: - # directories to persist to workspace - - packages/*/dist - - packages/*/lib - - packages/*/src/generated + - ~/.cache/yarn + key: v2-yarn-cache + +references: reports_path: &reports_path path: ./reports jobs: - install-dependencies: - <<: *setup_env + checkout-code: + docker: *docker-node-lts steps: - checkout - - attach_workspace: - at: '.' - - restore_cache: *restore_cache - - run: yarn --frozen-lockfile - - run: npm rebuild node-sass - - save_cache: *save_cache + - restore_cache: *restore-yarn-cache + - restore_cache: *restore-node-modules-cache + - run: yarn install --non-interactive --cache-folder ~/.cache/yarn + - run: + name: Check if yarn.lock changed during install + command: git diff --exit-code + - save_cache: *save-node-modules-cache + - save_cache: *save-yarn-cache - persist_to_workspace: root: '.' - paths: - - yarn.lock + paths: [packages/*/node_modules] + + clean-lockfile: + docker: *docker-node-lts + steps: + - checkout + - restore_cache: *restore-node-modules-cache + - run: ./scripts/verifyCleanLockfile.sh compile: - <<: *setup_env + docker: *docker-node-lts + resource_class: large steps: - checkout - - attach_workspace: - at: '.' - - restore_cache: *restore_cache + - restore_cache: *restore-node-modules-cache + - attach_workspace: { at: '.' } - run: yarn compile - - persist_to_workspace: *persist_to_workspace + - persist_to_workspace: + root: '.' + paths: [packages/*/lib, packages/*/src/generated] lint: - <<: *setup_env + docker: *docker-node-lts environment: JUNIT_REPORT_PATH: reports steps: - checkout - - attach_workspace: - at: '.' - - restore_cache: *restore_cache - - run: mkdir -p ./reports/tslint ./reports/stylelint - - run: yarn compile --scope "@blueprintjs/tslint-config" + - restore_cache: *restore-node-modules-cache + - attach_workspace: { at: '.' } + - run: mkdir -p ./reports/eslint ./reports/stylelint + # we need to compile the lint rules for blueprint + - run: yarn compile --scope "@blueprintjs/{eslint-plugin-blueprint,tslint-config}" - run: yarn lint - - store_test_results: *reports_path - - store_artifacts: *reports_path + - store_test_results: { path: ./reports } + - store_artifacts: { path: ./reports } dist: - <<: *setup_env + docker: *docker-node-lts + resource_class: large steps: - checkout - - attach_workspace: - at: '.' - - restore_cache: *restore_cache - - run: yarn dist:libs - - run: yarn dist:apps - - persist_to_workspace: *persist_to_workspace + - restore_cache: *restore-node-modules-cache + - attach_workspace: { at: '.' } + - run: yarn dist + - persist_to_workspace: + root: '.' + paths: [packages/*/lib, packages/*/dist] + + test-jest: + docker: *docker-node-lts + environment: + JUNIT_REPORT_PATH: reports + # JEST_JUNIT_OUTPUT_DIR: "reports/junit/js-test-results.xml" + steps: + - checkout + - restore_cache: *restore-node-modules-cache + - attach_workspace: { at: '.' } + - run: mkdir ./reports + - run: yarn lerna run test:jest-ci + - store_test_results: { path: ./reports } + - store_artifacts: { path: ./reports } test-react-16: &test-react - docker: - - image: circleci/node:8.10.0-browsers - environment: - CHROME_BIN: "/usr/bin/google-chrome" + docker: *docker-node-browsers environment: JUNIT_REPORT_PATH: reports - parallelism: 3 + parallelism: 6 steps: - checkout - - attach_workspace: - at: '.' - - restore_cache: *restore_cache + - restore_cache: *restore-node-modules-cache + - attach_workspace: { at: '.' } - run: mkdir ./reports - run: + # split karma tests into containers because they can take up a lot of memory + # running them in one container caused Karma to time out frequently + # see https://github.com/palantir/blueprint/issues/3616 command: | case $CIRCLE_NODE_INDEX in \ - 0) yarn lerna run --parallel test:pre ;; \ - 1) yarn lerna run --parallel test:iso ;; \ - 2) yarn lerna run --parallel test:karma ;; \ + 0) yarn lerna run --parallel test:typeCheck ;; \ + 1) yarn lerna run --scope "@blueprintjs/core" test:karma ;; \ + 2) yarn lerna run --scope "@blueprintjs/datetime" test:karma ;; \ + 3) yarn lerna run --scope "@blueprintjs/select" test:karma ;; \ + 4) yarn lerna run --scope "@blueprintjs/table" test:karma ;; \ + 5) yarn lerna run --scope "@blueprintjs/timezone" test:karma ;; \ esac when: always - - store_test_results: *reports_path - - store_artifacts: *reports_path + - store_test_results: { path: ./reports } + - store_artifacts: { path: ./reports } test-react-15: # copy test-react-16 and override environment @@ -126,52 +144,74 @@ jobs: JUNIT_REPORT_PATH: reports REACT: 15 # use React 15 for this job + test-iso-react-16: &test-iso + docker: *docker-node-lts + environment: + JUNIT_REPORT_PATH: reports + steps: + - checkout + - restore_cache: *restore-node-modules-cache + - attach_workspace: { at: '.' } + - run: mkdir ./reports + - run: yarn lerna run --parallel test:iso + - store_test_results: { path: ./reports } + - store_artifacts: { path: ./reports } + + test-iso-react-15: + # copy test-iso-react-16 and override environment + <<: *test-iso + environment: + JUNIT_REPORT_PATH: reports + REACT: 15 # use React 15 for this job + deploy-preview: - docker: - - image: circleci/node:8.10.0 + docker: *docker-node-lts steps: - checkout - - attach_workspace: - at: '.' - - restore_cache: *restore_cache - - store_artifacts: - path: packages/docs-app/dist - - store_artifacts: - path: packages/landing-app/dist - - store_artifacts: - path: packages/table-dev-app/dist + - restore_cache: *restore-node-modules-cache + - attach_workspace: { at: '.' } + - store_artifacts: { path: packages/docs-app/dist } + - store_artifacts: { path: packages/landing-app/dist } + - store_artifacts: { path: packages/table-dev-app/dist } - run: name: Submit Github comment with links to built artifacts command: node ./scripts/preview.js deploy-npm: - <<: *setup_env + docker: *docker-node-lts steps: - checkout - - attach_workspace: - at: '.' - - restore_cache: *restore_cache + - restore_cache: *restore-node-modules-cache + - attach_workspace: { at: '.' } - run: ./scripts/publish-npm-semver-tagged workflows: version: 2 compile_lint_test_dist_deploy: jobs: - - install-dependencies + - checkout-code + - clean-lockfile: + requires: [checkout-code] - compile: - requires: [install-dependencies] + requires: [checkout-code] - lint: - requires: [install-dependencies] + requires: [checkout-code] - dist: requires: [compile] + - test-jest: + requires: [compile] - test-react-15: requires: [compile] - test-react-16: requires: [compile] + - test-iso-react-15: + requires: [compile] + - test-iso-react-16: + requires: [compile] - deploy-preview: requires: [dist] - deploy-npm: - requires: [dist, lint, test-react-15, test-react-16] + requires: [dist, lint, test-react-15, test-react-16, test-iso-react-15, test-iso-react-16] filters: branches: only: diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000..34e26bba41 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,6 @@ +node_modules +dist +fixtures +coverage +__snapshots__ +src/generated diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000000..ac1bb60b72 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "root": true, + "extends": [ + "./packages/eslint-config" + ] +} diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md index f5b40d18a2..54c4e23328 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -10,7 +10,7 @@ about: Something not working as expected? - __Package version(s)__: - __Browser and OS versions__: -If possible, ink to a minimal repro (fork [this code sandbox](https://codesandbox.io/s/nko3k41y60)): +If possible, link to a minimal repro (fork [this code sandbox](https://codesandbox.io/s/blueprint-sandbox-et9xy)): #### Steps to reproduce diff --git a/.vscode/settings.json b/.vscode/settings.json index 8a9d619ea4..8c12e6a92c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,11 +7,13 @@ "docs": true }, "search.exclude": { - "**/node_modules": true, - "**/build": true, - "**/coverage": true, - "**/dist": true, - "docs": true + "**/build": true, + "**/coverage": true, + "**/dist": true, + "**/lib": true, + "**/node_modules": true, + "docs": true, + "site/docs": true }, "editor.insertSpaces": true, "editor.tabSize": 4, diff --git a/.yarnrc b/.yarnrc deleted file mode 100644 index c16e54f708..0000000000 --- a/.yarnrc +++ /dev/null @@ -1 +0,0 @@ -workspace-experimental true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3d17f74474..6dced7f956 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,7 +39,7 @@ A typical contributor workflow looks like this: - Linting is best handled by your editor for real-time feedback (see [Editor integration](https://github.com/palantir/blueprint/wiki/Editor-integration)). Run `yarn lint` to be 100% safe. - - TypeScript lint errors can often be automatically fixed by TSLint. Run lint fixes with `yarn lint-fix`. + - TypeScript lint errors can often be automatically fixed by ESLint. Run lint fixes with `yarn lint-fix`. 1. Submit a Pull Request on GitHub and fill out the template. - ⚠️ __DO NOT enable CircleCI for your fork of Blueprint.__ Our build will run on your fork when you open a PR. You can run NPM scripts locally diff --git a/README.md b/README.md index 70bba52824..5abd3d8519 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ It is optimized for building complex, data-dense web interfaces for _desktop app [**View the full documentation ▸**](http://blueprintjs.com/docs) -[**Try it out on CodeSandbox ▸**](https://codesandbox.io/s/nko3k41y60) +[**Try it out on CodeSandbox ▸**](https://codesandbox.io/s/blueprint-sandbox-et9xy) [**Read frequently asked questions (FAQ) on the wiki ▸**](https://github.com/palantir/blueprint/wiki/Frequently-Asked-Questions) @@ -39,7 +39,6 @@ These are the component libraries we publish to NPM. - [![npm](https://img.shields.io/npm/v/@blueprintjs/select.svg?label=@blueprintjs/select)](https://www.npmjs.com/package/@blueprintjs/select) – Components for selecting items from a list. - [![npm](https://img.shields.io/npm/v/@blueprintjs/table.svg?label=@blueprintjs/table)](https://www.npmjs.com/package/@blueprintjs/table) – Scalable interactive table component. - [![npm](https://img.shields.io/npm/v/@blueprintjs/timezone.svg?label=@blueprintjs/timezone)](https://www.npmjs.com/package/@blueprintjs/timezone) – Components for picking timezones. -- [![npm](https://img.shields.io/npm/v/@blueprintjs/labs.svg?label=@blueprintjs/labs)](https://www.npmjs.com/package/@blueprintjs/labs) – Incubator and staging area for new components still under initial development. ### Applications @@ -57,10 +56,12 @@ These are used as development playground environments: These packages define development dependencies and contain build configuration. They adhere to the standard NPM package layout, which allows us to keep clear API boundaries for build configuration and isolate groups of `devDependencies`. They are published to NPM in order to allow other Blueprint-related projects to use this infrastructure outside this monorepo. - [![npm](https://img.shields.io/npm/v/@blueprintjs/docs-theme.svg?label=@blueprintjs/docs-theme)](https://www.npmjs.com/package/@blueprintjs/docs-theme) – Documentation theme for [Documentalist](https://github.com/palantir/documentalist) data. +- [![npm](https://img.shields.io/npm/v/@blueprintjs/eslint-config.svg?label=@blueprintjs/eslint-config)](https://www.npmjs.com/package/@blueprintjs/eslint-config) – ESLint configuration used in this repo and recommended for Blueprint-related projects +- [![npm](https://img.shields.io/npm/v/@blueprintjs/eslint-plugin-blueprint.svg?label=@blueprintjs/eslint-plugin-blueprint)](https://www.npmjs.com/package/@blueprintjs/eslint-plugin-blueprint) – implementations for custom ESLint rules which enforce best practices for Blueprint usage - [![npm](https://img.shields.io/npm/v/@blueprintjs/karma-build-scripts.svg?label=@blueprintjs/karma-build-scripts)](https://www.npmjs.com/package/@blueprintjs/karma-build-scripts) -- [![npm](https://img.shields.io/npm/v/@blueprintjs/node-build-scripts.svg?label=@blueprintjs/node-build-scripts)](https://www.npmjs.com/package/@blueprintjs/node-build-scripts) -- [![npm](https://img.shields.io/npm/v/@blueprintjs/test-commons.svg?label=@blueprintjs/test-commons)](https://www.npmjs.com/package/@blueprintjs/test-commons) -- [![npm](https://img.shields.io/npm/v/@blueprintjs/tslint-config.svg?label=@blueprintjs/tslint-config)](https://www.npmjs.com/package/@blueprintjs/tslint-config) +- [![npm](https://img.shields.io/npm/v/@blueprintjs/node-build-scripts.svg?label=@blueprintjs/node-build-scripts)](https://www.npmjs.com/package/@blueprintjs/node-build-scripts) – various utility scripts for linting, working with CSS variables, and building icons +- [![npm](https://img.shields.io/npm/v/@blueprintjs/test-commons.svg?label=@blueprintjs/test-commons)](https://www.npmjs.com/package/@blueprintjs/test-commons) – various utility functions used in Blueprint test suites +- [![npm](https://img.shields.io/npm/v/@blueprintjs/tslint-config.svg?label=@blueprintjs/tslint-config)](https://www.npmjs.com/package/@blueprintjs/tslint-config) – TSLint configuration used in this repo and recommended for Blueprint-related projects (should be installed by `@blueprintjs/eslint-config`, not directly) - [![npm](https://img.shields.io/npm/v/@blueprintjs/webpack-build-scripts.svg?label=@blueprintjs/webpack-build-scripts)](https://www.npmjs.com/package/@blueprintjs/webpack-build-scripts) ## Contributing @@ -71,10 +72,10 @@ then [check out the "help wanted" label](https://github.com/palantir/blueprint/l ## Development -[Lerna](https://lernajs.io/) manages inter-package dependencies in this monorepo. +[Lerna](https://lerna.js.org/) manages inter-package dependencies in this monorepo. Builds are orchestrated via `lerna run` and NPM scripts. -__Prerequisites__: Node.js v8+, Yarn v1.10+ +__Prerequisites__: Node.js v10+, Yarn v1.18+ ### One-time setup diff --git a/config/tsconfig.base.json b/config/tsconfig.base.json index 14948c8d2f..f1e6d61a4c 100644 --- a/config/tsconfig.base.json +++ b/config/tsconfig.base.json @@ -1,5 +1,5 @@ { - "version": "2.8.4", + "version": "3.5.3", "compilerOptions": { "allowSyntheticDefaultImports": true, "declaration": true, diff --git a/package.json b/package.json index f4d388a179..bd4708ce47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blueprintjs-monorepo", - "version": "3.15.0", + "version": "3.22.3", "private": true, "description": "A React UI toolkit for the web.", "workspaces": [ @@ -17,12 +17,11 @@ "dev:core": "lerna run dev --parallel --scope \"@blueprintjs/{core,icons,docs-app}\"", "dev:docs": "lerna run dev --parallel --scope \"@blueprintjs/{core,docs-app,docs-theme}\"", "dev:datetime": "lerna run dev --parallel --scope \"@blueprintjs/{core,datetime,timezone,docs-app}\"", - "dev:labs": "lerna run dev --parallel --scope \"@blueprintjs/{core,labs,select,docs-app}\"", "dev:landing": "lerna run dev --parallel --scope \"@blueprintjs/{core,landing-app}\"", "dev:select": "lerna run dev --parallel --scope \"@blueprintjs/{core,select,docs-app}\"", "dev:table": "lerna run dev --parallel --scope \"@blueprintjs/table-dev-app\"", "dist": "run-s dist:libs dist:apps", - "dist:libs": "lerna run dist --parallel --scope \"@blueprintjs/{core,datetime,docs-theme,icons,labs,select,table,timezone}\"", + "dist:libs": "lerna run dist --parallel --scope \"@blueprintjs/{core,datetime,docs-theme,icons,select,table,timezone}\"", "dist:apps": "lerna run dist --parallel --scope \"@blueprintjs/{docs-app,landing-app,table-dev-app}\"", "docs-data": "lerna run compile --scope \"@blueprintjs/docs-data\"", "lint": "lerna run --parallel lint", @@ -34,36 +33,36 @@ "verify": "npm-run-all -s compile dist:libs dist:apps -p test lint" }, "dependencies": { - "@types/chai": "4.1.7", - "@types/classnames": "2.2.7", - "@types/enzyme": "3.1.15", - "@types/enzyme-adapter-react-16": "1.0.3", - "@types/mocha": "5.2.6", - "@types/prop-types": "15.7.0", - "@types/react": "16.4.18", - "@types/react-dom": "16.0.11", - "@types/react-transition-group": "2.0.14", - "@types/sinon": "7.0.6", - "@types/webpack": "4.4.24", + "@types/chai": "4.2.7", + "@types/classnames": "2.2.9", + "@types/enzyme": "3.10.4", + "@types/enzyme-adapter-react-16": "1.0.5", + "@types/mocha": "5.2.7", + "@types/prop-types": "15.7.1", + "@types/react": "16.9.17", + "@types/react-dom": "16.8.5", + "@types/react-lifecycles-compat": "^3.0.1", + "@types/react-transition-group": "4.2.0", + "@types/sinon": "7.5.1", + "@types/webpack": "4.41.2", "chai": "^4.2.0", "circle-github-bot": "^2.0.1", "cross-env": "^5.2.0", "gh-pages": "^2.0.1", "http-server": "^0.11.1", "lerna": "^2.11.0", - "npm-run-all": "^4.1.3", - "sinon": "^7.2.4", - "stylelint-config-palantir": "^3.1.1", - "stylelint-scss": "^3.3.1", - "typescript": "~2.8.3" + "npm-run-all": "^4.1.5", + "sinon": "^7.3.2", + "stylelint-config-palantir": "^4.0.0", + "stylelint-scss": "^3.9.2", + "typescript": "~3.7.5", + "yarn-deduplicate": "^1.1.1" }, "resolutions": { - "@types/enzyme": "3.1.15", - "@types/react": "16.4.18", - "node-gyp": "3.8.0" + "js-yaml": "3.13.1" }, "engines": { - "node": ">=6.1" + "node": ">=10" }, "repository": { "type": "git", diff --git a/packages/core/karma.conf.js b/packages/core/karma.conf.js index b74bf1c71f..abe10e52b8 100644 --- a/packages/core/karma.conf.js +++ b/packages/core/karma.conf.js @@ -13,6 +13,7 @@ module.exports = function (config) { // not worth full coverage "src/accessibility/*", "src/common/abstractComponent*", + "src/common/abstractPureComponent*", ], }); config.set(baseConfig); diff --git a/packages/core/package.json b/packages/core/package.json index f9fdd60066..ffd3c1aa8e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@blueprintjs/core", - "version": "3.15.0-graphext16", + "version": "3.24.0", "description": "Core styles & components", "main": "lib/cjs/index.js", "module": "lib/esm/index.js", @@ -9,7 +9,13 @@ "style": "lib/css/blueprint.css", "unpkg": "dist/core.bundle.js", "sideEffects": [ - "*.css" + "*.css", + "lib/esm/components/index.js", + "lib/esm/common/configureDom4.js", + "lib/esnext/components/index.js", + "lib/esnext/common/configureDom4.js", + "lib/cjs/components/index.js", + "lib/cjs/common/configureDom4.js" ], "bin": { "upgrade-blueprint-2.0.0-rename": "./scripts/upgrade-blueprint-2.0.0-rename.sh", @@ -21,55 +27,55 @@ "compile:esm": "tsc -p ./src", "compile:cjs": "tsc -p ./src -m commonjs --outDir lib/cjs", "compile:esnext": "tsc -p ./src -t esnext --outDir lib/esnext", - "compile:css": "sass-compile ./src --functions ./scripts/sass-inline-svg.js", + "compile:css": "sass-compile ./src --functions ./scripts/sass-custom-functions.js", "dev": "run-p \"compile:esm -- --watch\" \"compile:css -- --watch\"", "dist": "run-s \"dist:*\"", "dist:bundle": "cross-env NODE_ENV=production webpack", "dist:css": "css-dist lib/css/*.css", "dist:variables": "generate-css-variables common/_colors.scss common/_color-aliases.scss common/_variables.scss", "dist:verify": "assert-package-layout", - "generate-graphext-css": "yarn run compile:css && add-js-version", - "lint": "run-p lint:scss lint:ts", + "lint": "run-p lint:scss lint:es", "lint:scss": "sass-lint", - "lint:ts": "ts-lint", - "lint-fix": "ts-lint --fix", - "test": "run-s test:pre test:iso test:karma", - "test:pre": "tsc -p ./test", + "lint:es": "es-lint", + "lint-fix": "es-lint --fix", + "test": "run-s test:typeCheck test:iso test:karma", + "test:typeCheck": "tsc -p ./test", "test:iso": "mocha test/isotest.js", "test:karma": "karma start", "test:karma:debug": "karma start --single-run=false --reporters=mocha --debug", "verify": "npm-run-all compile -p dist test lint" }, "dependencies": { - "@blueprintjs/icons": "3.7.0-graphext16", + "@blueprintjs/icons": "^3.14.0", "@types/dom4": "^2.0.1", "classnames": "^2.2", - "dom4": "^2.0.1", - "normalize.css": "^8.0.0", - "popper.js": "^1.14.1", - "react-popper": "^1.0.0", - "react-transition-group": "^2.2.1", - "resize-observer-polyfill": "^1.5.0", - "tslib": "^1.9.0" + "dom4": "^2.1.5", + "normalize.css": "^8.0.1", + "popper.js": "^1.15.0", + "react-lifecycles-compat": "^3.0.4", + "react-popper": "^1.3.7", + "react-transition-group": "^2.9.0", + "resize-observer-polyfill": "^1.5.1", + "tslib": "~1.10.0" }, "peerDependencies": { "react": "^15.3.0 || 16", "react-dom": "^15.3.0 || 16" }, "devDependencies": { - "@blueprintjs/karma-build-scripts": "^0.10.0", - "@blueprintjs/node-build-scripts": "^0.9.0", - "@blueprintjs/test-commons": "^0.9.0", - "enzyme": "^3.3.0", - "karma": "^3.1.4", - "mocha": "^5.2.0", - "npm-run-all": "^4.1.2", - "react": "^16.2.0", - "react-dom": "^16.2.0", - "react-test-renderer": "^16.2.0", + "@blueprintjs/karma-build-scripts": "^0.12.0", + "@blueprintjs/node-build-scripts": "^1.0.0", + "@blueprintjs/test-commons": "^0.10.1", + "enzyme": "^3.10.0", + "karma": "^4.2.0", + "mocha": "^6.2.0", + "npm-run-all": "^4.1.5", + "react": "^16.8.6", + "react-dom": "^16.8.6", + "react-test-renderer": "^16.8.6", "sass-inline-svg": "^1.2.0", - "typescript": "~2.8.3", - "webpack-cli": "^3.1.2" + "typescript": "~3.7.5", + "webpack-cli": "^3.3.6" }, "repository": { "type": "git", diff --git a/packages/labs/webpack.config.js b/packages/core/scripts/sass-custom-functions.js similarity index 52% rename from packages/labs/webpack.config.js rename to packages/core/scripts/sass-custom-functions.js index 0708d456f4..b781efe1de 100644 --- a/packages/labs/webpack.config.js +++ b/packages/core/scripts/sass-custom-functions.js @@ -1,5 +1,6 @@ /* - * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * Copyright 2019 Palantir Technologies, Inc. All rights reserved. + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -13,22 +14,19 @@ * limitations under the License. */ -const { baseConfig, COMMON_EXTERNALS } = require("@blueprintjs/webpack-build-scripts"); -const path = require("path"); - -module.exports = Object.assign({}, baseConfig, { - entry: { - labs: [ - "./src/index.ts" - ], - }, - - externals: COMMON_EXTERNALS, +const inliner = require("sass-inline-svg"); - output: { - filename: "[name].bundle.js", - library: ["Blueprint", "Labs"], - libraryTarget: "umd", - path: path.resolve(__dirname, "./dist") - }, -}); +module.exports = { + /** + * Sass function to inline a UI icon svg and change its path color. + * + * Usage: + * svg-icon("16px/icon-name.svg", (path: (fill: $color)) ) + */ + "svg-icon": inliner("../../resources/icons", { + // run through SVGO first + optimize: true, + // minimal "uri" encoding is smaller than base64 + encodingFormat: "uri" + }), +}; diff --git a/packages/core/scripts/sass-inline-svg.js b/packages/core/scripts/sass-inline-svg.js deleted file mode 100644 index 790d83b47d..0000000000 --- a/packages/core/scripts/sass-inline-svg.js +++ /dev/null @@ -1,12 +0,0 @@ -const inliner = require('sass-inline-svg'); - -module.exports = { - // Sass function to inline a UI Icon svg and change its path color: - // svg-icon("16px/icon-name.svg", (path: (fill: $color)) ) - 'svg-icon': inliner('../../resources/icons', { - // run through SVGO first - optimize: true, - // minimal "uri" encoding is smaller than base64 - encodingFormat: "uri" - }) -}; diff --git a/packages/core/src/_typography.scss b/packages/core/src/_typography.scss index 100fcdb826..a7273b2062 100644 --- a/packages/core/src/_typography.scss +++ b/packages/core/src/_typography.scss @@ -109,18 +109,20 @@ Running text Markup:

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do - eiusmod tempor incididunt ut labore et dolore magna aliqua. + We build products that make people better at their most important + work — the kind of work you read about on the front page of the + newspaper, not just the technology section.

-

New section

+

Scale, Speed, Agility

- Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris - nisi ut aliquip ex ea commodo consequat. + A successful data transformation requires the whole organization — users, the IT shop, and + leadership — to operate in lockstep. With Foundry, the enterprise comes together to + transform the organization and turn data into a competitive advantage.

@@ -356,10 +358,10 @@ Block quotes Markup:
- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + Premium Aerotec is a key supplier for Airbus, producing 30 million parts per year, + which is huge complexity. Skywise helps us manage all the production steps. + It gives Airbus much better visibility into where the product is in the supply chain, + and it lets us immediately see our weak points so we can react on the spot.
Styleguide blockquote diff --git a/packages/core/src/_accessibility.scss b/packages/core/src/accessibility/_focus-states.scss similarity index 100% rename from packages/core/src/_accessibility.scss rename to packages/core/src/accessibility/_focus-states.scss diff --git a/packages/core/src/blueprint-hi-contrast.scss b/packages/core/src/blueprint-hi-contrast.scss new file mode 100644 index 0000000000..5b7e739ec6 --- /dev/null +++ b/packages/core/src/blueprint-hi-contrast.scss @@ -0,0 +1,22 @@ +/*! + +Copyright 2019-present Palantir Technologies, Inc. All rights reserved. +Licensed under the Apache License, Version 2.0. + +*/ + +// override some intent colors to pass contrast requirements +$pt-intent-primary: #106ba3 !default; // $blue2 +$pt-intent-success: #0d8050 !default; // $green2; +$pt-intent-warning: #a66321 !default; // $orange1; +$pt-intent-danger: #c23030 !default; // $red2; + +// Import files in the same order that they are documented in the docs +@import "common/variables"; +@import "common/colors"; + +@import "reset"; +@import "typography"; +@import "accessibility/focus-states"; + +@import "components/index"; diff --git a/packages/core/src/blueprint.scss b/packages/core/src/blueprint.scss index 53b781ebcc..0186d987c3 100644 --- a/packages/core/src/blueprint.scss +++ b/packages/core/src/blueprint.scss @@ -11,7 +11,7 @@ Licensed under the Apache License, Version 2.0. @import "reset"; @import "typography"; -@import "accessibility"; +@import "accessibility/focus-states"; @import "components/index"; @import "overrides"; diff --git a/packages/core/src/common/_color-aliases.scss b/packages/core/src/common/_color-aliases.scss index b801efd362..02957118cc 100644 --- a/packages/core/src/common/_color-aliases.scss +++ b/packages/core/src/common/_color-aliases.scss @@ -13,12 +13,12 @@ $pt-outline-color: rgba($blue3, 0.6); $pt-text-color: $dark-gray1 !default; $pt-text-color-muted: $gray1 !default; -$pt-text-color-disabled: rgba($pt-text-color-muted, 0.5) !default; +$pt-text-color-disabled: rgba($pt-text-color-muted, 0.6) !default; $pt-heading-color: $pt-text-color !default; $pt-link-color: $blue2 !default; $pt-dark-text-color: $light-gray5 !default; -$pt-dark-text-color-muted: $gray5 !default; -$pt-dark-text-color-disabled: rgba($pt-dark-text-color-muted, 0.5) !default; +$pt-dark-text-color-muted: $gray4 !default; +$pt-dark-text-color-disabled: rgba($pt-dark-text-color-muted, 0.6) !default; $pt-dark-heading-color: $pt-dark-text-color !default; $pt-dark-link-color: $blue5 !default; // Default text selection color using #7dbcff diff --git a/packages/core/src/common/abstractComponent.ts b/packages/core/src/common/abstractComponent.ts index fbca130b1a..9e8c1e38cf 100644 --- a/packages/core/src/common/abstractComponent.ts +++ b/packages/core/src/common/abstractComponent.ts @@ -20,6 +20,7 @@ import { isNodeEnv } from "./utils"; /** * An abstract component that Blueprint components can extend * in order to add some common functionality like runtime props validation. + * @deprecated componentWillReceiveProps is deprecated in React 16.9; use AbstractComponent2 instead */ export abstract class AbstractComponent extends React.Component { /** Component displayName should be `public static`. This property exists to prevent incorrect usage. */ diff --git a/packages/core/src/common/abstractComponent2.ts b/packages/core/src/common/abstractComponent2.ts new file mode 100644 index 0000000000..2911ab208d --- /dev/null +++ b/packages/core/src/common/abstractComponent2.ts @@ -0,0 +1,90 @@ +/* + * Copyright 2019 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as React from "react"; +import { isNodeEnv } from "./utils"; + +/** + * An abstract component that Blueprint components can extend + * in order to add some common functionality like runtime props validation. + */ +export abstract class AbstractComponent2 extends React.Component { + // unsafe lifecycle methods + public componentWillUpdate: never; + public componentWillReceiveProps: never; + public componentWillMount: never; + // this should be static, not an instance method + public getDerivedStateFromProps: never; + + /** Component displayName should be `public static`. This property exists to prevent incorrect usage. */ + protected displayName: never; + + // Not bothering to remove entries when their timeouts finish because clearing invalid ID is a no-op + private timeoutIds: number[] = []; + + constructor(props?: P, context?: any) { + super(props, context); + if (!isNodeEnv("production")) { + this.validateProps(this.props); + } + } + + public componentDidUpdate(_prevProps: P, _prevState: S, _snapshot?: SS) { + if (!isNodeEnv("production")) { + this.validateProps(this.props); + } + } + + public componentWillUnmount() { + this.clearTimeouts(); + } + + /** + * Set a timeout and remember its ID. + * All stored timeouts will be cleared when component unmounts. + * @returns a "cancel" function that will clear timeout when invoked. + */ + public setTimeout(callback: () => void, timeout?: number) { + const handle = window.setTimeout(callback, timeout); + this.timeoutIds.push(handle); + return () => window.clearTimeout(handle); + } + + /** + * Clear all known timeouts. + */ + public clearTimeouts = () => { + if (this.timeoutIds.length > 0) { + for (const timeoutId of this.timeoutIds) { + window.clearTimeout(timeoutId); + } + this.timeoutIds = []; + } + }; + + /** + * Ensures that the props specified for a component are valid. + * Implementations should check that props are valid and usually throw an Error if they are not. + * Implementations should not duplicate checks that the type system already guarantees. + * + * This method should be used instead of React's + * [propTypes](https://facebook.github.io/react/docs/reusable-components.html#prop-validation) feature. + * Like propTypes, these runtime checks run only in development mode. + */ + protected validateProps(_props: P) { + // implement in subclass + } +} diff --git a/packages/core/src/common/abstractPureComponent.ts b/packages/core/src/common/abstractPureComponent.ts index d8bb9031fa..88efc4b3e9 100644 --- a/packages/core/src/common/abstractPureComponent.ts +++ b/packages/core/src/common/abstractPureComponent.ts @@ -20,6 +20,7 @@ import { isNodeEnv } from "./utils"; /** * An abstract component that Blueprint components can extend * in order to add some common functionality like runtime props validation. + * @deprecated componentWillReceiveProps is deprecated in React 16.9; use AbstractPureComponent2 instead */ export abstract class AbstractPureComponent extends React.PureComponent { /** Component displayName should be `public static`. This property exists to prevent incorrect usage. */ @@ -77,7 +78,7 @@ export abstract class AbstractPureComponent extends React.PureCompone * [propTypes](https://facebook.github.io/react/docs/reusable-components.html#prop-validation) feature. * Like propTypes, these runtime checks run only in development mode. */ - protected validateProps(_: P & { children?: React.ReactNode }) { + protected validateProps(_props: P & { children?: React.ReactNode }) { // implement in subclass } } diff --git a/packages/core/src/common/abstractPureComponent2.ts b/packages/core/src/common/abstractPureComponent2.ts new file mode 100644 index 0000000000..514886154e --- /dev/null +++ b/packages/core/src/common/abstractPureComponent2.ts @@ -0,0 +1,90 @@ +/* + * Copyright 2019 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as React from "react"; +import { isNodeEnv } from "./utils"; + +/** + * An abstract component that Blueprint components can extend + * in order to add some common functionality like runtime props validation. + */ +export abstract class AbstractPureComponent2 extends React.PureComponent { + // unsafe lifecycle method + public componentWillUpdate: never; + public componentWillReceiveProps: never; + public componentWillMount: never; + // this should be static, not an instance method + public getDerivedStateFromProps: never; + + /** Component displayName should be `public static`. This property exists to prevent incorrect usage. */ + protected displayName: never; + + // Not bothering to remove entries when their timeouts finish because clearing invalid ID is a no-op + private timeoutIds: number[] = []; + + constructor(props?: P, context?: any) { + super(props, context); + if (!isNodeEnv("production")) { + this.validateProps(this.props); + } + } + + public componentDidUpdate(_prevProps: P, _prevState: S, _snapshot?: SS) { + if (!isNodeEnv("production")) { + this.validateProps(this.props); + } + } + + public componentWillUnmount() { + this.clearTimeouts(); + } + + /** + * Set a timeout and remember its ID. + * All stored timeouts will be cleared when component unmounts. + * @returns a "cancel" function that will clear timeout when invoked. + */ + public setTimeout(callback: () => void, timeout?: number) { + const handle = window.setTimeout(callback, timeout); + this.timeoutIds.push(handle); + return () => window.clearTimeout(handle); + } + + /** + * Clear all known timeouts. + */ + public clearTimeouts = () => { + if (this.timeoutIds.length > 0) { + for (const timeoutId of this.timeoutIds) { + window.clearTimeout(timeoutId); + } + this.timeoutIds = []; + } + }; + + /** + * Ensures that the props specified for a component are valid. + * Implementations should check that props are valid and usually throw an Error if they are not. + * Implementations should not duplicate checks that the type system already guarantees. + * + * This method should be used instead of React's + * [propTypes](https://facebook.github.io/react/docs/reusable-components.html#prop-validation) feature. + * Like propTypes, these runtime checks run only in development mode. + */ + protected validateProps(_props: P) { + // implement in subclass + } +} diff --git a/packages/core/src/common/classes.ts b/packages/core/src/common/classes.ts index ffe0eb7697..b46f668bf0 100644 --- a/packages/core/src/common/classes.ts +++ b/packages/core/src/common/classes.ts @@ -19,7 +19,7 @@ import { Elevation } from "./elevation"; import { Intent } from "./intent"; import { Position } from "./position"; -const NS = process.env.BLUEPRINT_NAMESPACE || "bp3"; +const NS = process.env.BLUEPRINT_NAMESPACE || process.env.REACT_APP_BLUEPRINT_NAMESPACE || "bp3"; // modifiers export const ACTIVE = `${NS}-active`; @@ -35,6 +35,7 @@ export const INTERACTIVE = `${NS}-interactive`; export const LARGE = `${NS}-large`; export const LOADING = `${NS}-loading`; export const MINIMAL = `${NS}-minimal`; +export const OUTLINED = `${NS}-outlined`; export const MULTILINE = `${NS}-multiline`; export const ROUND = `${NS}-round`; export const SMALL = `${NS}-small`; @@ -154,6 +155,7 @@ export const SWITCH_INNER_TEXT = `${SWITCH}-inner-text`; export const FILE_INPUT = `${NS}-file-input`; export const FILE_INPUT_HAS_SELECTION = `${NS}-file-input-has-selection`; export const FILE_UPLOAD_INPUT = `${NS}-file-upload-input`; +export const FILE_UPLOAD_INPUT_CUSTOM_TEXT = `${NS}-file-upload-input-custom-text`; export const KEY = `${NS}-key`; export const KEY_COMBO = `${KEY}-combo`; diff --git a/packages/core/src/common/configureDom4.ts b/packages/core/src/common/configureDom4.ts new file mode 100644 index 0000000000..f8f6651951 --- /dev/null +++ b/packages/core/src/common/configureDom4.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2019 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +if (typeof require !== "undefined" && typeof window !== "undefined" && typeof document !== "undefined") { + // we're in browser + // tslint:disable-next-line:no-var-requires + require("dom4"); // only import actual dom4 if we're in the browser (not server-compatible) + // we'll still need dom4 types for the TypeScript to compile, these are included in package.json +} diff --git a/packages/core/src/common/constructor.ts b/packages/core/src/common/constructor.ts index 373a6a9563..12d50c07ea 100644 --- a/packages/core/src/common/constructor.ts +++ b/packages/core/src/common/constructor.ts @@ -18,6 +18,4 @@ * Generic interface defining constructor types, such as classes. This is used to type the class * itself in meta-programming situations such as decorators. */ -export interface IConstructor { - new (...args: any[]): T; -} +export type IConstructor = new (...args: any[]) => T; diff --git a/packages/core/src/common/errors.ts b/packages/core/src/common/errors.ts index 8b1c0c2310..e3b81a7522 100644 --- a/packages/core/src/common/errors.ts +++ b/packages/core/src/common/errors.ts @@ -49,6 +49,10 @@ export const NUMERIC_INPUT_STEP_SIZE_NON_POSITIVE = ns + ` requires stepSize to be strictly greater than zero.`; export const NUMERIC_INPUT_STEP_SIZE_NULL = ns + ` requires stepSize to be defined.`; +export const PANEL_STACK_INITIAL_PANEL_STACK_MUTEX = + ns + ` requires exactly one of initialPanel and stack prop`; +export const PANEL_STACK_REQUIRES_PANEL = ns + ` requires at least one panel in the stack`; + export const OVERFLOW_LIST_OBSERVE_PARENTS_CHANGED = ns + ` does not support changing observeParents after mounting.`; @@ -94,3 +98,6 @@ export const DIALOG_WARN_NO_HEADER_CLOSE_BUTTON = export const DRAWER_VERTICAL_IS_IGNORED = ns + ` vertical is ignored if position is defined`; export const DRAWER_ANGLE_POSITIONS_ARE_CASTED = ns + ` all angle positions are casted into pure position (TOP, BOTTOM, LEFT or RIGHT)`; + +export const TOASTER_MAX_TOASTS_INVALID = + ns + ` maxToasts is set to an invalid number, must be greater than 0`; diff --git a/packages/core/src/common/index.ts b/packages/core/src/common/index.ts index a69d01e68c..202a7f76ce 100644 --- a/packages/core/src/common/index.ts +++ b/packages/core/src/common/index.ts @@ -15,7 +15,9 @@ */ export * from "./abstractComponent"; +export * from "./abstractComponent2"; export * from "./abstractPureComponent"; +export * from "./abstractPureComponent2"; export * from "./alignment"; export * from "./boundary"; export * from "./colors"; diff --git a/packages/core/src/common/position.ts b/packages/core/src/common/position.ts index 9b1271cde5..67ddb69185 100644 --- a/packages/core/src/common/position.ts +++ b/packages/core/src/common/position.ts @@ -55,11 +55,7 @@ export function isPositionVertical(position: Position) { } export function getPositionIgnoreAngles(position: Position) { - if ( - position === Position.TOP || - position === Position.TOP_LEFT || - position === Position.TOP_RIGHT - ) { + if (position === Position.TOP || position === Position.TOP_LEFT || position === Position.TOP_RIGHT) { return Position.TOP; } else if ( position === Position.BOTTOM || @@ -67,11 +63,7 @@ export function getPositionIgnoreAngles(position: Position) { position === Position.BOTTOM_RIGHT ) { return Position.BOTTOM; - } else if ( - position === Position.LEFT || - position === Position.LEFT_TOP || - position === Position.LEFT_BOTTOM - ) { + } else if (position === Position.LEFT || position === Position.LEFT_TOP || position === Position.LEFT_BOTTOM) { return Position.LEFT; } else { return Position.RIGHT; diff --git a/packages/core/src/common/props.ts b/packages/core/src/common/props.ts index 9a7f043932..8fed9af366 100644 --- a/packages/core/src/common/props.ts +++ b/packages/core/src/common/props.ts @@ -112,6 +112,7 @@ const INVALID_PROPS = [ "active", "alignText", "containerRef", + "current", "elementRef", "fill", "icon", @@ -122,8 +123,10 @@ const INVALID_PROPS = [ "loading", "leftIcon", "minimal", - "onChildrenMount", - "onRemove", + "onRemove", // ITagProps, ITagInputProps + "outlined", // IButtonProps + "panel", // ITabProps + "panelClassName", // ITabProps "popoverProps", "rightElement", "rightIcon", diff --git a/packages/core/src/common/utils.ts b/packages/core/src/common/utils.ts index f391d23a25..532f399f95 100644 --- a/packages/core/src/common/utils.ts +++ b/packages/core/src/common/utils.ts @@ -209,8 +209,8 @@ export function countDecimalPlaces(num: number) { if (!isFinite(num)) { return 0; } - let e = 1, - p = 0; + let e = 1; + let p = 0; while (Math.round(num * e) / e !== num) { e *= 10; p++; diff --git a/packages/core/src/common/utils/compareUtils.ts b/packages/core/src/common/utils/compareUtils.ts index e0aaeca8b8..e56b7f1d2a 100644 --- a/packages/core/src/common/utils/compareUtils.ts +++ b/packages/core/src/common/utils/compareUtils.ts @@ -41,6 +41,7 @@ export function arraysEqual(arrA: any[], arrB: any[], compare = (a: any, b: any) /** * Shallow comparison between objects. If `keys` is provided, just that subset * of keys will be compared; otherwise, all keys will be compared. + * @returns true if items are equal. */ export function shallowCompareKeys(objA: T, objB: T, keys?: IKeyBlacklist | IKeyWhitelist) { // treat `null` and `undefined` as the same @@ -65,8 +66,9 @@ export function shallowCompareKeys(objA: T, objB: T, keys?: IK /** * Deep comparison between objects. If `keys` is provided, just that subset of * keys will be compared; otherwise, all keys will be compared. + * @returns true if items are equal. */ -export function deepCompareKeys(objA: any, objB: any, keys?: string[]): boolean { +export function deepCompareKeys(objA: any, objB: any, keys?: Array): boolean { if (objA === objB) { return true; } else if (objA == null && objB == null) { @@ -95,26 +97,6 @@ export function deepCompareKeys(objA: any, objB: any, keys?: string[]): boolean } } -/** - * Returns a descriptive object for each key whose values are shallowly unequal - * between two provided objects. Useful for debugging shouldComponentUpdate. - */ -export function getShallowUnequalKeyValues( - objA: T, - objB: T, - keys?: IKeyBlacklist | IKeyWhitelist, -) { - // default param values let null values pass through, so we have to take - // this more thorough approach - const definedObjA = objA == null ? {} : objA; - const definedObjB = objB == null ? {} : objB; - - const filteredKeys = _filterKeys(definedObjA, definedObjB, keys == null ? { exclude: [] } : keys); - return _getUnequalKeyValues(definedObjA, definedObjB, filteredKeys, (a, b, key) => { - return shallowCompareKeys(a, b, { include: [key] }); - }); -} - /** * Returns a descriptive object for each key whose values are deeply unequal * between two provided objects. Useful for debugging shouldComponentUpdate. @@ -145,7 +127,7 @@ function _shallowCompareKeys(objA: T, objB: T, keys: IKeyBlacklist | IKeyW /** * Partial deep comparison between objects using the given list of keys. */ -function _deepCompareKeys(objA: any, objB: any, keys: string[]): boolean { +function _deepCompareKeys(objA: any, objB: any, keys: Array): boolean { return keys.every(key => { return objA.hasOwnProperty(key) === objB.hasOwnProperty(key) && deepCompareKeys(objA[key], objB[key]); }); @@ -158,7 +140,7 @@ function _isSimplePrimitiveType(value: any) { function _filterKeys(objA: T, objB: T, keys: IKeyBlacklist | IKeyWhitelist) { if (_isWhitelist(keys)) { return keys.include; - } else { + } else if (_isBlacklist(keys)) { const keysA = Object.keys(objA); const keysB = Object.keys(objB); @@ -171,12 +153,18 @@ function _filterKeys(objA: T, objB: T, keys: IKeyBlacklist | IKeyWhitelist // return the remaining keys as an array return Object.keys(keySet) as Array; } + + return []; } function _isWhitelist(keys: any): keys is IKeyWhitelist { return keys != null && (keys as IKeyWhitelist).include != null; } +function _isBlacklist(keys: any): keys is IKeyBlacklist { + return keys != null && (keys as IKeyBlacklist).exclude != null; +} + function _arrayToObject(arr: any[]) { return arr.reduce((obj: any, element: any) => { obj[element] = true; diff --git a/packages/core/src/common/utils/isDarkTheme.ts b/packages/core/src/common/utils/isDarkTheme.ts index c819c4fcd6..b4467c35ca 100644 --- a/packages/core/src/common/utils/isDarkTheme.ts +++ b/packages/core/src/common/utils/isDarkTheme.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import "../configureDom4"; + import { Classes } from "../"; export function isDarkTheme(element: Element | Text | null): boolean { diff --git a/packages/core/src/components/alert/alert.tsx b/packages/core/src/components/alert/alert.tsx index ee4de62e8d..5ee0e3fd47 100644 --- a/packages/core/src/components/alert/alert.tsx +++ b/packages/core/src/components/alert/alert.tsx @@ -16,8 +16,9 @@ import classNames from "classnames"; import * as React from "react"; +import { polyfill } from "react-lifecycles-compat"; -import { AbstractPureComponent, Classes, DISPLAYNAME_PREFIX, Intent, IProps, MaybeElement } from "../../common"; +import { AbstractPureComponent2, Classes, DISPLAYNAME_PREFIX, Intent, IProps, MaybeElement } from "../../common"; import { ALERT_WARN_CANCEL_ESCAPE_KEY, ALERT_WARN_CANCEL_OUTSIDE_CLICK, @@ -61,7 +62,7 @@ export interface IAlertProps extends IOverlayLifecycleProps, IProps { icon?: IconName | MaybeElement; /** - * The intent to be applied to the confirm (right-most) button. + * The intent to be applied to the confirm (right-most) button and the icon (if provided). */ intent?: Intent; @@ -117,7 +118,8 @@ export interface IAlertProps extends IOverlayLifecycleProps, IProps { onClose?(confirmed: boolean, evt?: React.SyntheticEvent): void; } -export class Alert extends AbstractPureComponent { +@polyfill +export class Alert extends AbstractPureComponent2 { public static defaultProps: IAlertProps = { canEscapeKeyCancel: false, canOutsideClickCancel: false, diff --git a/packages/core/src/components/breadcrumbs/_breadcrumbs.scss b/packages/core/src/components/breadcrumbs/_breadcrumbs.scss index fd172ecc00..6c1c07e586 100644 --- a/packages/core/src/components/breadcrumbs/_breadcrumbs.scss +++ b/packages/core/src/components/breadcrumbs/_breadcrumbs.scss @@ -53,7 +53,8 @@ Styleguide breadcrumbs .#{$ns}-breadcrumb, .#{$ns}-breadcrumb-current, .#{$ns}-breadcrumbs-collapsed { - display: inline-block; + display: inline-flex; + align-items: center; font-size: $pt-font-size-large; } @@ -71,6 +72,10 @@ Styleguide breadcrumbs cursor: not-allowed; color: $pt-text-color-disabled; } + + .#{$ns}-icon { + margin-right: $pt-grid-size / 2; + } } .#{$ns}-breadcrumb-current { diff --git a/packages/core/src/components/breadcrumbs/breadcrumb.tsx b/packages/core/src/components/breadcrumbs/breadcrumb.tsx index 1a788baea8..e89b9ddc06 100644 --- a/packages/core/src/components/breadcrumbs/breadcrumb.tsx +++ b/packages/core/src/components/breadcrumbs/breadcrumb.tsx @@ -19,6 +19,7 @@ import * as React from "react"; import * as Classes from "../../common/classes"; import { IActionProps, ILinkProps } from "../../common/props"; +import { Icon } from "../icon/icon"; export interface IBreadcrumbProps extends IActionProps, ILinkProps { /** Whether this breadcrumb is the current breadcrumb. */ @@ -34,9 +35,13 @@ export const Breadcrumb: React.SFC = breadcrumbProps => { }, breadcrumbProps.className, ); + + const icon = breadcrumbProps.icon != null ? : undefined; + if (breadcrumbProps.href == null && breadcrumbProps.onClick == null) { return ( + {icon} {breadcrumbProps.text} {breadcrumbProps.children} @@ -50,6 +55,7 @@ export const Breadcrumb: React.SFC = breadcrumbProps => { tabIndex={breadcrumbProps.disabled ? null : 0} target={breadcrumbProps.target} > + {icon} {breadcrumbProps.text} {breadcrumbProps.children} diff --git a/packages/core/src/components/breadcrumbs/breadcrumbs.tsx b/packages/core/src/components/breadcrumbs/breadcrumbs.tsx index 1c21173ef3..cc1641ddd3 100644 --- a/packages/core/src/components/breadcrumbs/breadcrumbs.tsx +++ b/packages/core/src/components/breadcrumbs/breadcrumbs.tsx @@ -16,11 +16,9 @@ import classNames from "classnames"; import * as React from "react"; +import { polyfill } from "react-lifecycles-compat"; -import { Boundary } from "../../common/boundary"; -import * as Classes from "../../common/classes"; -import { Position } from "../../common/position"; -import { IProps } from "../../common/props"; +import { AbstractPureComponent2, Boundary, Classes, IProps, Position, removeNonHTMLProps } from "../../common"; import { Menu } from "../menu/menu"; import { MenuItem } from "../menu/menuItem"; import { IOverflowListProps, OverflowList } from "../overflow-list/overflowList"; @@ -76,7 +74,8 @@ export interface IBreadcrumbsProps extends IProps { popoverProps?: IPopoverProps; } -export class Breadcrumbs extends React.PureComponent { +@polyfill +export class Breadcrumbs extends AbstractPureComponent2 { public static defaultProps: Partial = { collapseFrom: Boundary.START, }; @@ -120,7 +119,8 @@ export class Breadcrumbs extends React.PureComponent { private renderOverflowBreadcrumb = (props: IBreadcrumbProps, index: number) => { const isClickable = props.href != null || props.onClick != null; - return ; + const htmlProps = removeNonHTMLProps(props); + return ; }; private renderBreadcrumbWrapper = (props: IBreadcrumbProps, index: number) => { @@ -134,7 +134,8 @@ export class Breadcrumbs extends React.PureComponent { } else if (this.props.breadcrumbRenderer != null) { return this.props.breadcrumbRenderer(props); } else { - return ; + // allow user to override 'current' prop + return ; } } } diff --git a/packages/core/src/components/button/_button.scss b/packages/core/src/components/button/_button.scss index 0af068efae..4f706a6ceb 100644 --- a/packages/core/src/components/button/_button.scss +++ b/packages/core/src/components/button/_button.scss @@ -19,6 +19,7 @@ Markup: .#{$ns}-intent-warning - Warning intent .#{$ns}-intent-danger - Danger intent .#{$ns}-minimal - More subtle appearance +.#{$ns}-outlined - Subtle yet structured appearance .#{$ns}-large - Larger size .#{$ns}-small - Smaller size .#{$ns}-fill - Fill parent container @@ -46,12 +47,16 @@ Styleguide button width: 100%; } - // default is `text-align: left` so we only need `right` case. &.#{$ns}-align-right, .#{$ns}-align-right & { text-align: right; } + &.#{$ns}-align-left, + .#{$ns}-align-left & { + text-align: left; + } + // default styles &:not([class*="#{$ns}-intent-"]) { @include pt-button(); @@ -160,6 +165,12 @@ Styleguide button &.#{$ns}-minimal { @include pt-button-minimal(); } + + // outline is based on the styles of minimal + &.#{$ns}-outlined { + @include pt-button-minimal(); + @include pt-button-outlined(); + } } a.#{$ns}-button { diff --git a/packages/core/src/components/button/_common.scss b/packages/core/src/components/button/_common.scss index a6a9ff0e84..f88993b93e 100644 --- a/packages/core/src/components/button/_common.scss +++ b/packages/core/src/components/button/_common.scss @@ -85,6 +85,10 @@ $dark-minimal-button-background-color: none !default; $dark-minimal-button-background-color-hover: rgba($gray3, 0.15) !default; $dark-minimal-button-background-color-active: rgba($gray3, 0.3) !default; +$button-outlined-width: 1px !default; +$button-outlined-border-intent-opacity: 0.6 !default; +$button-outlined-border-disabled-intent-opacity: 0.2 !default; + // "intent": (default, hover, active colors) $button-intents: ( "primary": ($pt-intent-primary, $blue2, $blue1), @@ -448,3 +452,56 @@ $button-intents: ( } } +@mixin pt-button-outlined() { + border: $button-outlined-width solid rgba($pt-text-color, 0.2); + box-sizing: border-box; + + &:disabled, + &.#{$ns}-disabled, + &:disabled:hover, + &.#{$ns}-disabled:hover { + border-color: rgba($pt-text-color-disabled, 0.1); + } + + .#{$ns}-dark & { + @include pt-dark-button-outlined(); + } + + @each $intent, $colors in $button-intents { + &.#{$ns}-intent-#{$intent} { + @include pt-button-outlined-intent( + map-get($pt-intent-text-colors, $intent), + map-get($pt-dark-intent-text-colors, $intent) + ); + } + } +} + +@mixin pt-dark-button-outlined() { + border-color: rgba($white, 0.4); + + &:disabled, + &:disabled:hover, + &.#{$ns}-disabled, + &.#{$ns}-disabled:hover { + border-color: rgba($white, 0.2); + } +} + +@mixin pt-button-outlined-intent($text-color, $dark-text-color) { + border-color: rgba($text-color, $button-outlined-border-intent-opacity); + + &:disabled, + &.#{$ns}-disabled { + border-color: rgba($text-color, $button-outlined-border-disabled-intent-opacity); + } + + .#{$ns}-dark & { + border-color: rgba($dark-text-color, $button-outlined-border-intent-opacity); + + &:disabled, + &.#{$ns}-disabled { + border-color: rgba($dark-text-color, $button-outlined-border-disabled-intent-opacity); + } + } +} diff --git a/packages/core/src/components/button/abstractButton.tsx b/packages/core/src/components/button/abstractButton.tsx index d4f237e83b..0dc9e3c5d9 100644 --- a/packages/core/src/components/button/abstractButton.tsx +++ b/packages/core/src/components/button/abstractButton.tsx @@ -17,11 +17,7 @@ import classNames from "classnames"; import * as React from "react"; -import { Alignment } from "../../common/alignment"; -import * as Classes from "../../common/classes"; -import * as Keys from "../../common/keys"; -import { IActionProps, MaybeElement } from "../../common/props"; -import { isReactNodeEmpty, safeInvoke } from "../../common/utils"; +import { AbstractPureComponent2, Alignment, Classes, IActionProps, Keys, MaybeElement, Utils } from "../../common"; import { Icon, IconName } from "../icon/icon"; import { Spinner } from "../spinner/spinner"; @@ -61,6 +57,9 @@ export interface IButtonProps extends IActionProps { /** Whether this button should use minimal styles. */ minimal?: boolean; + /** Whether this button should use outlined styles. */ + outlined?: boolean; + /** Name of a Blueprint UI icon (or an icon element) to render after the text. */ rightIcon?: IconName | MaybeElement; @@ -68,18 +67,18 @@ export interface IButtonProps extends IActionProps { small?: boolean; /** - * HTML `type` attribute of button. Common values are `"button"` and `"submit"`. + * HTML `type` attribute of button. Accepted values are `"button"`, `"submit"`, and `"reset"`. * Note that this prop has no effect on `AnchorButton`; it only affects `Button`. * @default "button" */ - type?: string; + type?: "submit" | "reset" | "button"; } export interface IButtonState { isActive: boolean; } -export abstract class AbstractButton> extends React.PureComponent< +export abstract class AbstractButton> extends AbstractPureComponent2< IButtonProps & H, IButtonState > { @@ -91,7 +90,7 @@ export abstract class AbstractButton> extend protected refHandlers = { button: (ref: HTMLElement) => { this.buttonRef = ref; - safeInvoke(this.props.elementRef, ref); + Utils.safeInvoke(this.props.elementRef, ref); }, }; @@ -100,7 +99,7 @@ export abstract class AbstractButton> extend public abstract render(): JSX.Element; protected getCommonButtonProps() { - const { alignText, fill, large, loading, minimal, small, tabIndex } = this.props; + const { alignText, fill, large, loading, outlined, minimal, small, tabIndex } = this.props; const disabled = this.props.disabled || loading; const className = classNames( @@ -112,6 +111,7 @@ export abstract class AbstractButton> extend [Classes.LARGE]: large, [Classes.LOADING]: loading, [Classes.MINIMAL]: minimal, + [Classes.OUTLINED]: outlined, [Classes.SMALL]: small, }, Classes.alignmentClass(alignText), @@ -142,7 +142,7 @@ export abstract class AbstractButton> extend } } this.currentKeyDown = e.which; - safeInvoke(this.props.onKeyDown, e); + Utils.safeInvoke(this.props.onKeyDown, e); }; protected handleKeyUp = (e: React.KeyboardEvent) => { @@ -151,7 +151,7 @@ export abstract class AbstractButton> extend this.buttonRef.click(); } this.currentKeyDown = null; - safeInvoke(this.props.onKeyUp, e); + Utils.safeInvoke(this.props.onKeyUp, e); }; protected renderChildren(): React.ReactNode { @@ -159,7 +159,7 @@ export abstract class AbstractButton> extend return [ loading && , , - (!isReactNodeEmpty(text) || !isReactNodeEmpty(children)) && ( + (!Utils.isReactNodeEmpty(text) || !Utils.isReactNodeEmpty(children)) && ( {text} {children} diff --git a/packages/core/src/components/button/button.md b/packages/core/src/components/button/button.md index 484969917e..124fab0fc0 100644 --- a/packages/core/src/components/button/button.md +++ b/packages/core/src/components/button/button.md @@ -21,13 +21,17 @@ Buttons trigger actions when clicked. ``` diff --git a/packages/core/src/components/button/buttonGroup.tsx b/packages/core/src/components/button/buttonGroup.tsx index a83652bc12..b1fdabc152 100644 --- a/packages/core/src/components/button/buttonGroup.tsx +++ b/packages/core/src/components/button/buttonGroup.tsx @@ -16,8 +16,9 @@ import classNames from "classnames"; import * as React from "react"; -import { Alignment } from "../../common/alignment"; -import * as Classes from "../../common/classes"; +import { polyfill } from "react-lifecycles-compat"; + +import { AbstractPureComponent2, Alignment, Classes } from "../../common"; import { DISPLAYNAME_PREFIX, HTMLDivProps, IProps } from "../../common/props"; export interface IButtonGroupProps extends IProps, HTMLDivProps { @@ -56,7 +57,8 @@ export interface IButtonGroupProps extends IProps, HTMLDivProps { // this component is simple enough that tests would be purely tautological. /* istanbul ignore next */ -export class ButtonGroup extends React.PureComponent { +@polyfill +export class ButtonGroup extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.ButtonGroup`; public render() { diff --git a/packages/core/src/components/callout/_callout.scss b/packages/core/src/components/callout/_callout.scss index 1c0a1c99c6..dc82dda3bd 100644 --- a/packages/core/src/components/callout/_callout.scss +++ b/packages/core/src/components/callout/_callout.scss @@ -9,7 +9,7 @@ Callout Markup:

Callout Heading

- Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ex, delectus! + It's dangerous to go alone! Take this.
.#{$ns}-intent-primary - Primary intent diff --git a/packages/core/src/components/callout/callout.tsx b/packages/core/src/components/callout/callout.tsx index fdd4553d18..495e1065dd 100644 --- a/packages/core/src/components/callout/callout.tsx +++ b/packages/core/src/components/callout/callout.tsx @@ -16,11 +16,20 @@ import classNames from "classnames"; import * as React from "react"; +import { polyfill } from "react-lifecycles-compat"; -import { Classes, DISPLAYNAME_PREFIX, HTMLDivProps, IIntentProps, Intent, IProps, MaybeElement } from "../../common"; -import { Icon } from "../../index"; +import { + AbstractPureComponent2, + Classes, + DISPLAYNAME_PREFIX, + HTMLDivProps, + IIntentProps, + Intent, + IProps, + MaybeElement, +} from "../../common"; import { H4 } from "../html/html"; -import { IconName } from "../icon/icon"; +import { Icon, IconName } from "../icon/icon"; /** This component also supports the full range of HTML `
` props. */ export interface ICalloutProps extends IIntentProps, IProps, HTMLDivProps { @@ -50,7 +59,8 @@ export interface ICalloutProps extends IIntentProps, IProps, HTMLDivProps { title?: string; } -export class Callout extends React.PureComponent { +@polyfill +export class Callout extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.Callout`; public render() { diff --git a/packages/core/src/components/card/card.tsx b/packages/core/src/components/card/card.tsx index 2925b6cead..6abe9261c5 100644 --- a/packages/core/src/components/card/card.tsx +++ b/packages/core/src/components/card/card.tsx @@ -16,8 +16,8 @@ import classNames from "classnames"; import * as React from "react"; -import * as Classes from "../../common/classes"; -import { Elevation } from "../../common/elevation"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes, Elevation } from "../../common"; import { DISPLAYNAME_PREFIX, HTMLDivProps, IProps } from "../../common/props"; export interface ICardProps extends IProps, HTMLDivProps { @@ -48,7 +48,8 @@ export interface ICardProps extends IProps, HTMLDivProps { onClick?: (e: React.MouseEvent) => void; } -export class Card extends React.PureComponent { +@polyfill +export class Card extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.Card`; public static defaultProps: ICardProps = { elevation: Elevation.ZERO, diff --git a/packages/core/src/components/collapse/collapse.md b/packages/core/src/components/collapse/collapse.md index e346c819db..59968d9df8 100644 --- a/packages/core/src/components/collapse/collapse.md +++ b/packages/core/src/components/collapse/collapse.md @@ -32,9 +32,9 @@ export class CollapseExample extends React.Component<{}, ICollapseExampleState> {this.state.isOpen ? "Hide" : "Show"} build logs -
+                    
                         Dummy text.
-                    
+
); diff --git a/packages/core/src/components/collapse/collapse.tsx b/packages/core/src/components/collapse/collapse.tsx index 6292ebf9a4..e9969ce190 100644 --- a/packages/core/src/components/collapse/collapse.tsx +++ b/packages/core/src/components/collapse/collapse.tsx @@ -16,9 +16,8 @@ import classNames from "classnames"; import * as React from "react"; - -import { AbstractPureComponent } from "../../common/abstractPureComponent"; -import * as Classes from "../../common/classes"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes } from "../../common"; import { DISPLAYNAME_PREFIX, IProps } from "../../common/props"; export interface ICollapseProps extends IProps { @@ -53,11 +52,16 @@ export interface ICollapseProps extends IProps { } export interface ICollapseState { - /** The height that should be used for the content animations. This is a CSS value, not just a number. */ - height: string; - /** The state the element is currently in. */ animationState: AnimationStates; + + /** The height that should be used for the content animations. This is a CSS value, not just a number. */ + height: string | undefined; + + /** + * The most recent non-zero height (once a height has been measured upon first open; it is undefined until then) + */ + heightWhenOpen: number | undefined; } /** @@ -105,7 +109,8 @@ export enum AnimationStates { CLOSED, } -export class Collapse extends AbstractPureComponent { +@polyfill +export class Collapse extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.Collapse`; public static defaultProps: ICollapseProps = { @@ -115,31 +120,46 @@ export class Collapse extends AbstractPureComponent this.onDelayedStateChange(), transitionDuration); + } else if (animationState === AnimationStates.CLOSING_START) { this.setTimeout(() => this.setState({ animationState: AnimationStates.CLOSING, height: "0px", }), ); - this.setTimeout(() => this.onDelayedStateChange(), this.props.transitionDuration); - } - if (this.state.animationState === AnimationStates.OPEN_START) { - this.setState({ - animationState: AnimationStates.OPENING, - height: this.height + "px", - }); - this.setTimeout(() => this.onDelayedStateChange(), this.props.transitionDuration); + this.setTimeout(() => this.onDelayedStateChange(), transitionDuration); } } private contentsRefHandler = (el: HTMLElement) => { this.contents = el; - if (el != null) { - this.height = this.contents.clientHeight; + if (this.contents != null) { + const height = this.contents.clientHeight; this.setState({ animationState: this.props.isOpen ? AnimationStates.OPEN : AnimationStates.CLOSED, - height: `${this.height}px`, + height: height === 0 ? undefined : `${height}px`, + heightWhenOpen: height === 0 ? undefined : height, }); } }; diff --git a/packages/core/src/components/collapsible-list/collapsible-list.md b/packages/core/src/components/collapsible-list/collapsible-list.md index 86d13d1678..4c888bce64 100644 --- a/packages/core/src/components/collapsible-list/collapsible-list.md +++ b/packages/core/src/components/collapsible-list/collapsible-list.md @@ -7,18 +7,26 @@ customizing the appearance of visible items, using the props from the `MenuItem` children.
-

Deprecated: use [Overflow list](#core/components/overflow-list)

- This component is **deprecated since 3.0.0** with the introduction of - [`OverflowList`](#core/components/overflow-list) which provides a similar - experience with two distinct advantages: -
    -
  1. Items collapse automatically based on available space in the container.
  2. -
  3. - `OverflowList` accepts a generic array of items (instead of explicit - `` children) with custom renderers for both visible and overflowed - items, allowing for _any_ UI, not just a dropdown menu. -
  4. -
+

+ +Deprecated: use [Overflow list](#core/components/overflow-list) +

+ +This component is **deprecated since 3.0.0** with the introduction of +[`OverflowList`](#core/components/overflow-list) which provides a similar +experience with two distinct advantages: + +
    +
  1. Items collapse automatically based on available space in the container.
  2. +
  3. + +`OverflowList` accepts a generic array of items (instead of explicit +`` children) with custom renderers for both visible and overflowed +items, allowing for _any_ UI, not just a dropdown menu. + +
  4. +
+
@reactExample CollapsibleListExample diff --git a/packages/core/src/components/context-menu/contextMenu.tsx b/packages/core/src/components/context-menu/contextMenu.tsx index 45ba2b9732..47d466dc33 100644 --- a/packages/core/src/components/context-menu/contextMenu.tsx +++ b/packages/core/src/components/context-menu/contextMenu.tsx @@ -17,10 +17,8 @@ import classNames from "classnames"; import * as React from "react"; import * as ReactDOM from "react-dom"; - -import { AbstractPureComponent } from "../../common/abstractPureComponent"; -import * as Classes from "../../common/classes"; -import { Position } from "../../common/position"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes, Position } from "../../common"; import { safeInvoke } from "../../common/utils"; import { IOverlayLifecycleProps } from "../overlay/overlay"; import { Popover } from "../popover/popover"; @@ -47,7 +45,8 @@ const TRANSITION_DURATION = 100; type IContextMenuProps = IOverlayLifecycleProps; /* istanbul ignore next */ -class ContextMenu extends AbstractPureComponent { +@polyfill +class ContextMenu extends AbstractPureComponent2 { public state: IContextMenuState = { isDarkTheme: false, isOpen: false, diff --git a/packages/core/src/components/dialog/dialog.md b/packages/core/src/components/dialog/dialog.md index 05e5fb8837..919f6b9fa4 100644 --- a/packages/core/src/components/dialog/dialog.md +++ b/packages/core/src/components/dialog/dialog.md @@ -4,11 +4,13 @@ Dialogs present content overlaid over other parts of the UI.

Terminology note

- The term "modal" is sometimes used to mean "dialog," but this is a misnomer. - _Modal_ is an adjective that describes parts of a UI. - An element is considered modal if it - [blocks interaction with the rest of the application](https://en.wikipedia.org/wiki/Modal_window). - We use the term "dialog" to avoid confusion with the adjective. + +The term "modal" is sometimes used to mean "dialog," but this is a misnomer. +_Modal_ is an adjective that describes parts of a UI. +An element is considered modal if it +[blocks interaction with the rest of the application](https://en.wikipedia.org/wiki/Modal_window). +We use the term "dialog" to avoid confusion with the adjective. +
@reactExample DialogExample diff --git a/packages/core/src/components/dialog/dialog.tsx b/packages/core/src/components/dialog/dialog.tsx index 51fbbbf679..783ed55983 100644 --- a/packages/core/src/components/dialog/dialog.tsx +++ b/packages/core/src/components/dialog/dialog.tsx @@ -16,9 +16,8 @@ import classNames from "classnames"; import * as React from "react"; - -import { AbstractPureComponent } from "../../common/abstractPureComponent"; -import * as Classes from "../../common/classes"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes } from "../../common"; import * as Errors from "../../common/errors"; import { DISPLAYNAME_PREFIX, IProps, MaybeElement } from "../../common/props"; import { Button } from "../button/buttons"; @@ -72,7 +71,8 @@ export interface IDialogProps extends IOverlayableProps, IBackdropProps, IProps transitionName?: string; } -export class Dialog extends AbstractPureComponent { +@polyfill +export class Dialog extends AbstractPureComponent2 { public static defaultProps: IDialogProps = { canOutsideClickClose: true, isOpen: false, diff --git a/packages/core/src/components/divider/divider.tsx b/packages/core/src/components/divider/divider.tsx index 731fb36315..4230d5ddbf 100644 --- a/packages/core/src/components/divider/divider.tsx +++ b/packages/core/src/components/divider/divider.tsx @@ -16,7 +16,8 @@ import classNames from "classnames"; import * as React from "react"; - +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2 } from "../../common"; import { DIVIDER } from "../../common/classes"; import { DISPLAYNAME_PREFIX, IProps } from "../../common/props"; @@ -30,12 +31,16 @@ export interface IDividerProps extends IProps, React.HTMLAttributes // this component is simple enough that tests would be purely tautological. /* istanbul ignore next */ -export class Divider extends React.PureComponent { +@polyfill +export class Divider extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.Divider`; public render() { - const { className, tagName: TagName = "div", ...htmlProps } = this.props; + const { className, tagName = "div", ...htmlProps } = this.props; const classes = classNames(DIVIDER, className); - return ; + return React.createElement(tagName, { + ...htmlProps, + className: classes, + }); } } diff --git a/packages/core/src/components/drawer/_drawer.scss b/packages/core/src/components/drawer/_drawer.scss index d98142f502..500c435b32 100644 --- a/packages/core/src/components/drawer/_drawer.scss +++ b/packages/core/src/components/drawer/_drawer.scss @@ -11,12 +11,15 @@ $drawer-padding: $pt-grid-size * 2 !default; $drawer-default-size: 50%; +$drawer-background-color: $white !default; +$dark-drawer-background-color: $dark-gray4 !default; + .#{$ns}-drawer { display: flex; flex-direction: column; margin: 0; box-shadow: $pt-elevation-shadow-4; - background: $white; + background: $drawer-background-color; padding: 0; &:focus { @@ -167,7 +170,7 @@ $drawer-default-size: 50%; &.#{$ns}-dark, .#{$ns}-dark & { box-shadow: $pt-dark-dialog-box-shadow; - background: $dark-gray4; + background: $dark-drawer-background-color; color: $pt-dark-text-color; } } @@ -179,14 +182,14 @@ $drawer-default-size: 50%; position: relative; border-radius: 0; box-shadow: 0 1px 0 $pt-divider-black; - min-height: $pt-icon-size-large + $dialog-padding; - padding: $dialog-padding / 4; - padding-left: $dialog-padding; + min-height: $pt-icon-size-large + $drawer-padding; + padding: $drawer-padding / 4; + padding-left: $drawer-padding; .#{$ns}-icon-large, .#{$ns}-icon { flex: 0 0 auto; - margin-right: $dialog-padding / 2; + margin-right: $drawer-padding / 2; color: $pt-icon-color; } @@ -197,7 +200,7 @@ $drawer-default-size: 50%; line-height: inherit; &:last-child { - margin-right: $dialog-padding; + margin-right: $drawer-padding; } } @@ -221,7 +224,7 @@ $drawer-default-size: 50%; flex: 0 0 auto; position: relative; box-shadow: inset 0 1px 0 $pt-divider-black; - padding: $dialog-padding/2 $dialog-padding; + padding: $drawer-padding/2 $drawer-padding; .#{$ns}-dark & { box-shadow: inset 0 1px 0 $pt-dark-divider-black; diff --git a/packages/core/src/components/drawer/drawer.tsx b/packages/core/src/components/drawer/drawer.tsx index dbdebb43f3..421af8a3c4 100644 --- a/packages/core/src/components/drawer/drawer.tsx +++ b/packages/core/src/components/drawer/drawer.tsx @@ -16,9 +16,8 @@ import classNames from "classnames"; import * as React from "react"; - -import { AbstractPureComponent } from "../../common/abstractPureComponent"; -import * as Classes from "../../common/classes"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes } from "../../common"; import * as Errors from "../../common/errors"; import { getPositionIgnoreAngles, isPositionHorizontal, Position } from "../../common/position"; import { DISPLAYNAME_PREFIX, IProps, MaybeElement } from "../../common/props"; @@ -95,7 +94,8 @@ export interface IDrawerProps extends IOverlayableProps, IBackdropProps, IProps vertical?: boolean; } -export class Drawer extends AbstractPureComponent { +@polyfill +export class Drawer extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.Drawer`; public static defaultProps: IDrawerProps = { canOutsideClickClose: true, diff --git a/packages/core/src/components/editable-text/editable-text.md b/packages/core/src/components/editable-text/editable-text.md index 11624b59ee..99301fdeaf 100644 --- a/packages/core/src/components/editable-text/editable-text.md +++ b/packages/core/src/components/editable-text/editable-text.md @@ -15,9 +15,11 @@ You should not use `EditableText` when a static always-editable `` or

Centering the component

- **Do not center this component** using `text-align: center`, as it will cause an infinite loop - in the browser ([more details](https://github.com/JedWatson/react-select/issues/540)). Instead, - you should center the component via flexbox or with `position` and `transform: translateX(-50%)`. + +**Do not center this component** using `text-align: center`, as it will cause an infinite loop +in the browser ([more details](https://github.com/JedWatson/react-select/issues/540)). Instead, +you should center the component via flexbox or with `position` and `transform: translateX(-50%)`. +
diff --git a/packages/core/src/components/editable-text/editableText.tsx b/packages/core/src/components/editable-text/editableText.tsx index 7f02230f2c..c5db4e0876 100644 --- a/packages/core/src/components/editable-text/editableText.tsx +++ b/packages/core/src/components/editable-text/editableText.tsx @@ -16,15 +16,26 @@ import classNames from "classnames"; import * as React from "react"; - -import { AbstractPureComponent } from "../../common/abstractPureComponent"; -import * as Classes from "../../common/classes"; -import * as Keys from "../../common/keys"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes, Keys } from "../../common"; import { DISPLAYNAME_PREFIX, IIntentProps, IProps } from "../../common/props"; import { clamp, safeInvoke } from "../../common/utils"; import { Browser } from "../../compatibility"; export interface IEditableTextProps extends IIntentProps, IProps { + /** + * EXPERIMENTAL FEATURE. + * + * When true, this forces the component to _always_ render an editable input (or textarea) + * both when the component is focussed and unfocussed, instead of the component's default + * behavior of switching between a text span and a text input upon interaction. + * + * This behavior can help in certain applications where, for example, a custom right-click + * context menu is used to supply clipboard copy and paste functionality. + * @default false + */ + alwaysRenderInput?: boolean; + /** * If `true` and in multiline mode, the `enter` key will trigger onConfirm and `mod+enter` * will insert a newline. If `false`, the key bindings are inverted such that `enter` @@ -78,6 +89,7 @@ export interface IEditableTextProps extends IIntentProps, IProps { /** * Whether the entire text field should be selected on focus. * If `false`, the cursor is placed at the end of the text. + * This prop is ignored on inputs with type other then text, search, url, tel and password. See https://html.spec.whatwg.org/multipage/input.html#do-not-apply for details. * @default false */ selectAllOnFocus?: boolean; @@ -119,10 +131,12 @@ export interface IEditableTextState { const BUFFER_WIDTH_EDGE = 5; const BUFFER_WIDTH_IE = 30; -export class EditableText extends AbstractPureComponent { +@polyfill +export class EditableText extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.EditableText`; public static defaultProps: IEditableTextProps = { + alwaysRenderInput: false, confirmOnEnterKey: false, defaultValue: "", disabled: false, @@ -134,6 +148,7 @@ export class EditableText extends AbstractPureComponent { @@ -141,11 +156,22 @@ export class EditableText extends AbstractPureComponent { if (input != null) { - input.focus(); - const { length } = input.value; - input.setSelectionRange(this.props.selectAllOnFocus ? 0 : length, length); - if (!this.props.selectAllOnFocus) { - input.scrollLeft = input.scrollWidth; + this.inputElement = input; + + // temporary fix for #3882 + if (!this.props.alwaysRenderInput) { + this.inputElement.focus(); + } + + if (this.state != null && this.state.isEditing) { + const supportsSelection = inputSupportsSelection(input); + if (supportsSelection) { + const { length } = input.value; + input.setSelectionRange(this.props.selectAllOnFocus ? 0 : length, length); + } + if (!supportsSelection || !this.props.selectAllOnFocus) { + input.scrollLeft = input.scrollWidth; + } } } }, @@ -165,7 +191,7 @@ export class EditableText extends AbstractPureComponent - {this.maybeRenderInput(value)} - - {hasValue ? value : this.props.placeholder} - + {alwaysRenderInput || this.state.isEditing ? this.renderInput(value) : undefined} + {shouldHideContents ? ( + undefined + ) : ( + + {hasValue ? value : this.props.placeholder} + + )} ); } @@ -212,25 +248,24 @@ export class EditableText extends AbstractPureComponent { @@ -253,9 +288,16 @@ export class EditableText extends AbstractPureComponent { - if (!this.props.disabled) { + const { alwaysRenderInput, disabled, selectAllOnFocus } = this.props; + + if (!disabled) { this.setState({ isEditing: true }); } + + if (alwaysRenderInput && selectAllOnFocus && this.inputElement != null) { + const { length } = this.inputElement.value; + this.inputElement.setSelectionRange(0, length); + } }; private handleTextChange = (event: React.FormEvent) => { @@ -295,11 +337,8 @@ export class EditableText extends AbstractPureComponent = { className: Classes.EDITABLE_TEXT_INPUT, maxLength, @@ -307,13 +346,18 @@ export class EditableText extends AbstractPureComponent ) : ( @@ -392,3 +436,21 @@ function insertAtCaret(el: HTMLTextAreaElement, text: string) { el.selectionEnd = selectionStart + len; } } + +function inputSupportsSelection(input: HTMLInputElement | HTMLTextAreaElement) { + switch (input.type) { + // HTMLTextAreaElement + case "textarea": + return true; + // HTMLInputElement + // see https://html.spec.whatwg.org/multipage/input.html#do-not-apply + case "text": + case "search": + case "tel": + case "url": + case "password": + return true; + default: + return false; + } +} diff --git a/packages/core/src/components/forms/_control-group.scss b/packages/core/src/components/forms/_control-group.scss index c530ee8e56..2fffba6e90 100644 --- a/packages/core/src/components/forms/_control-group.scss +++ b/packages/core/src/components/forms/_control-group.scss @@ -100,13 +100,12 @@ Styleguide control-group .#{$ns}-button, .#{$ns}-html-select select, .#{$ns}-select select { + @include new-render-layer(); z-index: index($control-group-stack, "button-default"); // inherit radius since it's most likely to be zero border-radius: inherit; &:focus { - // establish new stacking context so focus state covers neighbors - position: relative; z-index: index($control-group-stack, "button-focus"); } diff --git a/packages/core/src/components/forms/_controls.scss b/packages/core/src/components/forms/_controls.scss index a55d69d969..af18a7eb6f 100644 --- a/packages/core/src/components/forms/_controls.scss +++ b/packages/core/src/components/forms/_controls.scss @@ -305,8 +305,8 @@ $control-indicator-spacing: $pt-grid-size !default; $dark-switch-background-color-active: rgba($black, 0.9) !default; $dark-switch-background-color-disabled: $dark-button-background-color-disabled !default; $dark-switch-checked-background-color: $control-checked-background-color !default; - $dark-switch-checked-background-color-hover: $blue4 !default; - $dark-switch-checked-background-color-active: $blue5 !default; + $dark-switch-checked-background-color-hover: $control-checked-background-color-hover !default; + $dark-switch-checked-background-color-active: $control-checked-background-color-active !default; $dark-switch-checked-background-color-disabled: rgba($blue1, 0.5) !default; $switch-indicator-background-color: $white !default; @@ -315,7 +315,14 @@ $control-indicator-spacing: $pt-grid-size !default; $dark-switch-indicator-background-color-disabled: rgba($black, 0.4) !default; &.#{$ns}-switch { - @mixin indicator-colors($selector, $color, $hover-color, $active-color, $disabled-color) { + @mixin indicator-colors( + $selector, + $color, + $hover-color, + $active-color, + $disabled-color, + $disabled-indicator-color + ) { input#{$selector} ~ .#{$ns}-control-indicator { background: $color; } @@ -330,6 +337,10 @@ $control-indicator-spacing: $pt-grid-size !default; input#{$selector}:disabled ~ .#{$ns}-control-indicator { background: $disabled-color; + + &::before { + background: $disabled-indicator-color; + } } } @@ -338,14 +349,16 @@ $control-indicator-spacing: $pt-grid-size !default; $switch-background-color, $switch-background-color-hover, $switch-background-color-active, - $switch-background-color-disabled + $switch-background-color-disabled, + $switch-indicator-background-color-disabled ); @include indicator-colors( ":checked", $switch-checked-background-color, $switch-checked-background-color-hover, $switch-checked-background-color-active, - $switch-checked-background-color-disabled + $switch-checked-background-color-disabled, + $switch-indicator-background-color-disabled ); // convert em variable to px value @include indicator-position($switch-width / 1em * $control-indicator-size); @@ -388,14 +401,16 @@ $control-indicator-spacing: $pt-grid-size !default; $dark-switch-background-color, $dark-switch-background-color-hover, $dark-switch-background-color-active, - $dark-switch-background-color-disabled + $dark-switch-background-color-disabled, + $dark-switch-indicator-background-color-disabled ); @include indicator-colors( ":checked", $dark-switch-checked-background-color, $dark-switch-checked-background-color-hover, $dark-switch-checked-background-color-active, - $dark-switch-checked-background-color-disabled + $dark-switch-checked-background-color-disabled, + $dark-switch-indicator-background-color-disabled ); .#{$ns}-control-indicator::before { diff --git a/packages/core/src/components/forms/_file-input.scss b/packages/core/src/components/forms/_file-input.scss index 69a677e1a6..32db71f651 100644 --- a/packages/core/src/components/forms/_file-input.scss +++ b/packages/core/src/components/forms/_file-input.scss @@ -3,6 +3,7 @@ @import "../../common/variables"; @import "../button/common"; +@import "../../common/mixins"; /* File input @@ -78,6 +79,10 @@ $file-input-button-padding-large: ($pt-input-height-large - $pt-button-height) / .#{$ns}-large & { height: $pt-input-height-large; } + + .#{$ns}-file-upload-input-custom-text::after { + content: attr(#{$ns}-button-text); + } } .#{$ns}-file-upload-input { @@ -94,6 +99,7 @@ $file-input-button-padding-large: ($pt-input-height-large - $pt-button-height) / &::after { @include pt-button(); @include pt-button-height($pt-button-height-small); + @include overflow-ellipsis(); position: absolute; top: 0; right: 0; diff --git a/packages/core/src/components/forms/_label.scss b/packages/core/src/components/forms/_label.scss index 4edf8da602..974ea1a4d6 100644 --- a/packages/core/src/components/forms/_label.scss +++ b/packages/core/src/components/forms/_label.scss @@ -53,6 +53,10 @@ label.#{$ns}-label { text-transform: none; } + .#{$ns}-button-group { + margin-top: $pt-grid-size / 2; + } + .#{$ns}-select select, .#{$ns}-html-select select { width: 100%; @@ -80,6 +84,10 @@ label.#{$ns}-label { vertical-align: top; } + .#{$ns}-button-group { + margin: 0 0 0 ($pt-grid-size / 2); + } + .#{$ns}-input-group .#{$ns}-input { margin-left: 0; } diff --git a/packages/core/src/components/forms/control-group.md b/packages/core/src/components/forms/control-group.md index bb4a86fdc4..9f22a54d37 100644 --- a/packages/core/src/components/forms/control-group.md +++ b/packages/core/src/components/forms/control-group.md @@ -6,13 +6,17 @@ groups, and HTML selects as direct children.

Control group vs. input group

-

Both components group multiple elements into a single unit, but their usage patterns are - quite different.

-

Think of `ControlGroup` as a parent with multiple children, with each one a separate - control.

-

Conversely, an `InputGroup` is a single control, and should function like so. A - button inside of an input group should only affect that input; if its reach is further, then it - should be promoted to live in a control group.

+ +Both components group multiple elements into a single unit, but their usage patterns are +quite different. + +Think of `ControlGroup` as a parent with multiple children, with each one a separate +control. + +Conversely, an `InputGroup` is a single control, and should function like so. A +button inside of an input group should only affect that input; if its reach is further, then it +should be promoted to live in a control group. +
@reactExample ControlGroupExample diff --git a/packages/core/src/components/forms/controlGroup.tsx b/packages/core/src/components/forms/controlGroup.tsx index 31cfa8300b..1fdec49994 100644 --- a/packages/core/src/components/forms/controlGroup.tsx +++ b/packages/core/src/components/forms/controlGroup.tsx @@ -16,7 +16,8 @@ import classNames from "classnames"; import * as React from "react"; -import * as Classes from "../../common/classes"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes } from "../../common"; import { DISPLAYNAME_PREFIX, HTMLDivProps, IProps } from "../../common/props"; export interface IControlGroupProps extends IProps, HTMLDivProps { @@ -35,7 +36,8 @@ export interface IControlGroupProps extends IProps, HTMLDivProps { // this component is simple enough that tests would be purely tautological. /* istanbul ignore next */ -export class ControlGroup extends React.PureComponent { +@polyfill +export class ControlGroup extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.ControlGroup`; public render() { diff --git a/packages/core/src/components/forms/controls.tsx b/packages/core/src/components/forms/controls.tsx index bf3878c617..184e4fa6d1 100644 --- a/packages/core/src/components/forms/controls.tsx +++ b/packages/core/src/components/forms/controls.tsx @@ -20,9 +20,9 @@ import classNames from "classnames"; import * as React from "react"; +import { polyfill } from "react-lifecycles-compat"; -import { Alignment } from "../../common/alignment"; -import * as Classes from "../../common/classes"; +import { AbstractPureComponent2, Alignment, Classes } from "../../common"; import { DISPLAYNAME_PREFIX, HTMLInputProps, IProps } from "../../common/props"; import { safeInvoke } from "../../common/utils"; @@ -112,7 +112,7 @@ const Control: React.SFC = ({ style, type, typeClassName, - tagName: TagName = "label", + tagName = "label", ...htmlProps }) => { const classes = classNames( @@ -126,14 +126,15 @@ const Control: React.SFC = ({ Classes.alignmentClass(alignIndicator), className, ); - return ( - - - {indicatorChildren} - {label} - {labelElement} - {children} - + + return React.createElement( + tagName, + { className: classes, style }, + , + {indicatorChildren}, + label, + labelElement, + children, ); }; @@ -156,7 +157,8 @@ export interface ISwitchProps extends IControlProps { innerLabel?: string; } -export class Switch extends React.PureComponent { +@polyfill +export class Switch extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.Switch`; public render() { @@ -191,7 +193,8 @@ export class Switch extends React.PureComponent { export interface IRadioProps extends IControlProps {} -export class Radio extends React.PureComponent { +@polyfill +export class Radio extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.Radio`; public render() { @@ -223,9 +226,19 @@ export interface ICheckboxState { indeterminate: boolean; } -export class Checkbox extends React.PureComponent { +@polyfill +export class Checkbox extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.Checkbox`; + public static getDerivedStateFromProps({ indeterminate }: ICheckboxProps): ICheckboxState | null { + // put props into state if controlled by props + if (indeterminate != null) { + return { indeterminate }; + } + + return null; + } + public state: ICheckboxState = { indeterminate: this.props.indeterminate || this.props.defaultIndeterminate || false, }; @@ -246,13 +259,6 @@ export class Checkbox extends React.PureComponent` element.

Static file name

- File name does not update on file selection. To get this behavior, - you must implement it separately in JS. + +File name does not update on file selection. To get this behavior, +you must implement it separately in JS. +
```tsx @@ -24,4 +25,7 @@ Use `inputProps` to apply props to the `` element. @## CSS +Use the standard `input type="file"` along with a `span` with class `@ns-file-upload-input`. +Wrap that all in a `label` with class `@ns-file-input`. + @css file-input diff --git a/packages/core/src/components/forms/fileInput.tsx b/packages/core/src/components/forms/fileInput.tsx index 041e0e3111..c9d6bf2e1e 100644 --- a/packages/core/src/components/forms/fileInput.tsx +++ b/packages/core/src/components/forms/fileInput.tsx @@ -16,8 +16,8 @@ import classNames from "classnames"; import * as React from "react"; -import { Utils } from "../../common"; -import * as Classes from "../../common/classes"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes, Utils } from "../../common"; import { DISPLAYNAME_PREFIX, IProps } from "../../common/props"; export interface IFileInputProps extends React.LabelHTMLAttributes, IProps { @@ -68,11 +68,18 @@ export interface IFileInputProps extends React.LabelHTMLAttributes { +@polyfill +export class FileInput extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.FileInput`; public static defaultProps: IFileInputProps = { @@ -83,6 +90,7 @@ export class FileInput extends React.PureComponent { public render() { const { + buttonText, className, disabled, fill, @@ -105,10 +113,19 @@ export class FileInput extends React.PureComponent { className, ); + const NS = Classes.getClassNamespace(); + + const uploadProps = { + [`${NS}-button-text`]: buttonText, + className: classNames(Classes.FILE_UPLOAD_INPUT, { + [Classes.FILE_UPLOAD_INPUT_CUSTOM_TEXT]: !!buttonText, + }), + }; + return ( ); } diff --git a/packages/core/src/components/forms/formGroup.tsx b/packages/core/src/components/forms/formGroup.tsx index 9f4eced105..6a7d7f36c3 100644 --- a/packages/core/src/components/forms/formGroup.tsx +++ b/packages/core/src/components/forms/formGroup.tsx @@ -16,7 +16,8 @@ import classNames from "classnames"; import * as React from "react"; -import * as Classes from "../../common/classes"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes } from "../../common"; import { DISPLAYNAME_PREFIX, IIntentProps, IProps } from "../../common/props"; export interface IFormGroupProps extends IIntentProps, IProps { @@ -60,7 +61,8 @@ export interface IFormGroupProps extends IIntentProps, IProps { style?: React.CSSProperties; } -export class FormGroup extends React.PureComponent { +@polyfill +export class FormGroup extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.FormGroup`; public render() { diff --git a/packages/core/src/components/forms/inputGroup.tsx b/packages/core/src/components/forms/inputGroup.tsx index 1d7672e546..6eb9f9a7fa 100644 --- a/packages/core/src/components/forms/inputGroup.tsx +++ b/packages/core/src/components/forms/inputGroup.tsx @@ -16,8 +16,8 @@ import classNames from "classnames"; import * as React from "react"; - -import * as Classes from "../../common/classes"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes } from "../../common"; import { DISPLAYNAME_PREFIX, HTMLInputProps, @@ -41,6 +41,11 @@ export interface IInputGroupProps extends IControlledProps, IIntentProps, IProps */ disabled?: boolean; + /** + * Whether the component should take up the full width of its container. + */ + fill?: boolean; + /** Ref handler that receives HTML `` element backing this component. */ inputRef?: (ref: HTMLInputElement | null) => any; @@ -79,7 +84,8 @@ export interface IInputGroupState { rightElementWidth: number; } -export class InputGroup extends React.PureComponent { +@polyfill +export class InputGroup extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.InputGroup`; public state: IInputGroupState = { @@ -92,12 +98,13 @@ export class InputGroup extends React.PureComponent`.

Prefer form groups over labels

- The [React `FormGroup` component](#core/components/form-group) provides - additional functionality such as helper text and modifier props as well as - full label support. `FormGroup` supports both simple and complex use cases, - therefore we recommend using it exclusively when constructing forms. + +The [React `FormGroup` component](#core/components/form-group) provides +additional functionality such as helper text and modifier props as well as +full label support. `FormGroup` supports both simple and complex use cases, +therefore we recommend using it exclusively when constructing forms. +
@## Props diff --git a/packages/core/src/components/forms/numeric-input.md b/packages/core/src/components/forms/numeric-input.md index 6eb123e486..680c935a67 100644 --- a/packages/core/src/components/forms/numeric-input.md +++ b/packages/core/src/components/forms/numeric-input.md @@ -43,9 +43,11 @@ custom `onKeyDown` callback) and when the field loses focus (via a custom trigged, the field will be cleared.
- This example contains non-core functionality that is meant to demonstrate - the extensibility of the `NumericInput` component. The correctness of the - custom evaluation code has not been tested robustly. + +This example contains non-core functionality that is meant to demonstrate +the extensibility of the `NumericInput` component. The correctness of the +custom evaluation code has not been tested robustly. +
@reactExample NumericInputExtendedExample @@ -106,7 +108,7 @@ import * as SomeLibrary from "some-library"; export class NumericInputExample extends React.Component<{}, { value?: number | string }> { - public state = { value: null }; + public state = { value: NumericInput.VALUE_EMPTY }; public render() { return ( diff --git a/packages/core/src/components/forms/numericInput.tsx b/packages/core/src/components/forms/numericInput.tsx index ddda8fd0e8..5f85893b3b 100644 --- a/packages/core/src/components/forms/numericInput.tsx +++ b/packages/core/src/components/forms/numericInput.tsx @@ -16,10 +16,11 @@ import classNames from "classnames"; import * as React from "react"; +import { polyfill } from "react-lifecycles-compat"; import { IconName } from "@blueprintjs/icons"; import { - AbstractPureComponent, + AbstractPureComponent2, Classes, DISPLAYNAME_PREFIX, HTMLInputProps, @@ -149,10 +150,13 @@ export interface INumericInputProps extends IIntentProps, IProps { onButtonClick?(valueAsNumber: number, valueAsString: string): void; /** The callback invoked when the value changes due to typing, arrow keys, or button clicks. */ - onValueChange?(valueAsNumber: number, valueAsString: string): void; + onValueChange?(valueAsNumber: number, valueAsString: string, inputElement: HTMLInputElement | null): void; } export interface INumericInputState { + prevMinProp?: number; + prevMaxProp?: number; + prevValueProp?: number | string; shouldSelectAfterUpdate: boolean; stepMaxPrecision: number; value: string; @@ -179,7 +183,8 @@ const NON_HTML_PROPS: Array = [ type ButtonEventHandlers = Required, "onKeyDown" | "onMouseDown">>; -export class NumericInput extends AbstractPureComponent { +@polyfill +export class NumericInput extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.NumericInput`; public static VALUE_EMPTY = ""; @@ -198,12 +203,56 @@ export class NumericInput extends AbstractPureComponent { const delta = this.updateDelta(direction, e); const nextValue = this.incrementValue(delta); - this.invokeValueCallback(nextValue, this.props.onButtonClick); + this.props.onButtonClick?.(+nextValue, nextValue); }; private startContinuousChange() { @@ -373,7 +404,7 @@ export class NumericInput extends AbstractPureComponent { const nextValue = this.incrementValue(this.delta); - this.invokeValueCallback(nextValue, this.props.onButtonClick); + this.props.onButtonClick?.(+nextValue, nextValue); }; // Callbacks - Input @@ -393,9 +424,6 @@ export class NumericInput extends AbstractPureComponent void) { - Utils.safeInvoke(callback, +value, value); - } - - // Value Helpers - // ============= - private incrementValue(delta: number) { // pretend we're incrementing from 0 if currValue is empty const currValue = this.state.value || NumericInput.VALUE_ZERO; const nextValue = this.getSanitizedValue(currValue, delta); this.setState({ shouldSelectAfterUpdate: this.props.selectAllOnIncrement, value: nextValue }); - this.invokeValueCallback(nextValue, this.props.onValueChange); return nextValue; } @@ -492,20 +511,14 @@ export class NumericInput extends AbstractPureComponent { +@polyfill +export class RadioGroup extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.RadioGroup`; // a unique name for this group, which can be overridden by `name` prop. diff --git a/packages/core/src/components/forms/text-inputs.md b/packages/core/src/components/forms/text-inputs.md index 79ce8a2f88..b24b76edb6 100644 --- a/packages/core/src/components/forms/text-inputs.md +++ b/packages/core/src/components/forms/text-inputs.md @@ -39,12 +39,13 @@ the parent input.

Icons only

-

You cannot use buttons with text in the CSS API for input groups. The padding for text inputs - in CSS cannot accommodate buttons whose width varies due to text content. You should use icons on - buttons instead.

- Conversely, the [`InputGroup`](#core/components/text-inputs.input-group) React - component _does_ support arbitrary content in its right element. +You cannot use buttons with text in the CSS API for input groups. The padding for text inputs +in CSS cannot accommodate buttons whose width varies due to text content. You should use icons on +buttons instead. + +Conversely, the [`InputGroup`](#core/components/text-inputs.input-group) React +component _does_ support arbitrary content in its right element.
diff --git a/packages/core/src/components/forms/textArea.tsx b/packages/core/src/components/forms/textArea.tsx index c7fcfda241..d4720eb314 100644 --- a/packages/core/src/components/forms/textArea.tsx +++ b/packages/core/src/components/forms/textArea.tsx @@ -16,7 +16,8 @@ import classNames from "classnames"; import * as React from "react"; -import * as Classes from "../../common/classes"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes } from "../../common"; import { DISPLAYNAME_PREFIX, IIntentProps, IProps } from "../../common/props"; export interface ITextAreaProps extends IIntentProps, IProps, React.TextareaHTMLAttributes { @@ -52,11 +53,19 @@ export interface ITextAreaState { // this component is simple enough that tests would be purely tautological. /* istanbul ignore next */ -export class TextArea extends React.PureComponent { +@polyfill +export class TextArea extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.TextArea`; - public state: ITextAreaState = {}; + private internalTextAreaRef: HTMLTextAreaElement; + public componentDidMount() { + if (this.props.growVertically) { + this.setState({ + height: this.internalTextAreaRef.scrollHeight, + }); + } + } public render() { const { className, fill, inputRef, intent, large, small, growVertically, ...htmlProps } = this.props; @@ -87,7 +96,7 @@ export class TextArea extends React.PureComponent ); @@ -104,4 +113,12 @@ export class TextArea extends React.PureComponent { + this.internalTextAreaRef = ref; + if (this.props.inputRef != null) { + this.props.inputRef(ref); + } + }; } diff --git a/packages/core/src/components/hotkeys/hotkey.tsx b/packages/core/src/components/hotkeys/hotkey.tsx index a68aedc1c2..ee6de4d5ee 100644 --- a/packages/core/src/components/hotkeys/hotkey.tsx +++ b/packages/core/src/components/hotkeys/hotkey.tsx @@ -16,8 +16,8 @@ import classNames from "classnames"; import * as React from "react"; - -import { AbstractPureComponent, Classes, DISPLAYNAME_PREFIX, IProps } from "../../common"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes, DISPLAYNAME_PREFIX, IProps } from "../../common"; import { KeyCombo } from "./keyCombo"; export interface IHotkeyProps extends IProps { @@ -83,7 +83,8 @@ export interface IHotkeyProps extends IProps { onKeyUp?(e: KeyboardEvent): any; } -export class Hotkey extends AbstractPureComponent { +@polyfill +export class Hotkey extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.Hotkey`; public static defaultProps = { diff --git a/packages/core/src/components/hotkeys/hotkeys.md b/packages/core/src/components/hotkeys/hotkeys.md index 71db766017..898019fd44 100644 --- a/packages/core/src/components/hotkeys/hotkeys.md +++ b/packages/core/src/components/hotkeys/hotkeys.md @@ -43,6 +43,14 @@ export class MyComponent extends React.Component<{}, {}> { } ``` +
+ +Your decorated component must return a single DOM element in its `render()` method, +not a custom React component. This constraint allows `HotkeysTarget` to inject +event handlers without creating an extra wrapper element. + +
+ @### Decorator The `@HotkeysTarget` decorator allows you to easily add global and local diff --git a/packages/core/src/components/hotkeys/hotkeys.tsx b/packages/core/src/components/hotkeys/hotkeys.tsx index 4f39bfb1e7..e04778b559 100644 --- a/packages/core/src/components/hotkeys/hotkeys.tsx +++ b/packages/core/src/components/hotkeys/hotkeys.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import * as React from "react"; - import classNames from "classnames"; -import { AbstractPureComponent, Classes, DISPLAYNAME_PREFIX, IProps } from "../../common"; +import * as React from "react"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes, DISPLAYNAME_PREFIX, IProps } from "../../common"; import { HOTKEYS_HOTKEY_CHILDREN } from "../../common/errors"; import { isElementOfType } from "../../common/utils"; import { H4 } from "../html/html"; @@ -41,7 +41,8 @@ export interface IHotkeysProps extends IProps { tabIndex?: number; } -export class Hotkeys extends AbstractPureComponent { +@polyfill +export class Hotkeys extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.Hotkeys`; public static defaultProps = { diff --git a/packages/core/src/components/hotkeys/hotkeysTarget.tsx b/packages/core/src/components/hotkeys/hotkeysTarget.tsx index f490d00bc8..f9af2e0bae 100644 --- a/packages/core/src/components/hotkeys/hotkeysTarget.tsx +++ b/packages/core/src/components/hotkeys/hotkeysTarget.tsx @@ -42,18 +42,10 @@ export function HotkeysTarget>(W public static displayName = `HotkeysTarget(${getDisplayName(WrappedComponent)})`; /** @internal */ - public globalHotkeysEvents?: HotkeysEvents; + public globalHotkeysEvents: HotkeysEvents = new HotkeysEvents(HotkeyScope.GLOBAL); /** @internal */ - public localHotkeysEvents?: HotkeysEvents; - - public componentWillMount() { - if (super.componentWillMount != null) { - super.componentWillMount(); - } - this.localHotkeysEvents = new HotkeysEvents(HotkeyScope.LOCAL); - this.globalHotkeysEvents = new HotkeysEvents(HotkeyScope.GLOBAL); - } + public localHotkeysEvents: HotkeysEvents = new HotkeysEvents(HotkeyScope.LOCAL); public componentDidMount() { if (super.componentDidMount != null) { @@ -91,23 +83,32 @@ export function HotkeysTarget>(W if (isFunction(this.renderHotkeys)) { const hotkeys = this.renderHotkeys(); - this.localHotkeysEvents.setHotkeys(hotkeys.props); - this.globalHotkeysEvents.setHotkeys(hotkeys.props); + if (this.localHotkeysEvents) { + this.localHotkeysEvents.setHotkeys(hotkeys.props); + } + if (this.globalHotkeysEvents) { + this.globalHotkeysEvents.setHotkeys(hotkeys.props); + } if (this.localHotkeysEvents.count() > 0) { const tabIndex = hotkeys.props.tabIndex === undefined ? 0 : hotkeys.props.tabIndex; - const { keyDown: existingKeyDown, keyUp: existingKeyUp } = element.props; - const onKeyDown = (e: React.KeyboardEvent) => { + const { onKeyDown: existingKeyDown, onKeyUp: existingKeyUp } = element.props; + + const handleKeyDownWrapper = (e: React.KeyboardEvent) => { this.localHotkeysEvents.handleKeyDown(e.nativeEvent as KeyboardEvent); safeInvoke(existingKeyDown, e); }; - const onKeyUp = (e: React.KeyboardEvent) => { + const handleKeyUpWrapper = (e: React.KeyboardEvent) => { this.localHotkeysEvents.handleKeyUp(e.nativeEvent as KeyboardEvent); safeInvoke(existingKeyUp, e); }; - return React.cloneElement(element, { tabIndex, onKeyDown, onKeyUp }); + return React.cloneElement(element, { + onKeyDown: handleKeyDownWrapper, + onKeyUp: handleKeyUpWrapper, + tabIndex, + }); } } return element; diff --git a/packages/core/src/components/hotkeys/keyCombo.tsx b/packages/core/src/components/hotkeys/keyCombo.tsx index 560f748014..924a491e31 100644 --- a/packages/core/src/components/hotkeys/keyCombo.tsx +++ b/packages/core/src/components/hotkeys/keyCombo.tsx @@ -16,7 +16,8 @@ import classNames from "classnames"; import * as React from "react"; -import { Classes, DISPLAYNAME_PREFIX, IProps } from "../../common"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes, DISPLAYNAME_PREFIX, IProps } from "../../common"; import { Icon, IconName } from "../icon/icon"; import { normalizeKeyCombo } from "./hotkeyParser"; @@ -47,7 +48,8 @@ export interface IKeyComboProps extends IProps { minimal?: boolean; } -export class KeyCombo extends React.Component { +@polyfill +export class KeyCombo extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.KeyCombo`; public render() { diff --git a/packages/core/src/components/html-select/_html-select.scss b/packages/core/src/components/html-select/_html-select.scss index 8177da0360..dfb3d60082 100644 --- a/packages/core/src/components/html-select/_html-select.scss +++ b/packages/core/src/components/html-select/_html-select.scss @@ -85,6 +85,10 @@ Styleguide select color: $pt-dark-text-color; } + option:disabled { + color: $pt-dark-text-color-disabled; + } + &::after { color: $pt-dark-icon-color; } diff --git a/packages/core/src/components/html-select/html-select.md b/packages/core/src/components/html-select/html-select.md index 7a79e8f796..287498e743 100644 --- a/packages/core/src/components/html-select/html-select.md +++ b/packages/core/src/components/html-select/html-select.md @@ -5,9 +5,11 @@ dropdown caret, so we provide an `HTMLSelect` component to streamline this process.
- The [`Select`](#select/multi-select) component in the [**@blueprintjs/select**](#select) - package provides a React alternative to the native HTML `` tag. Notably, it +supports custom filtering logic and item rendering. +
@## Props diff --git a/packages/core/src/components/html-select/htmlSelect.tsx b/packages/core/src/components/html-select/htmlSelect.tsx index cf15d2a4ca..da9e86b2a6 100644 --- a/packages/core/src/components/html-select/htmlSelect.tsx +++ b/packages/core/src/components/html-select/htmlSelect.tsx @@ -16,6 +16,8 @@ import classNames from "classnames"; import * as React from "react"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2 } from "../../common"; import { DISABLED, FILL, HTML_SELECT, LARGE, MINIMAL } from "../../common/classes"; import { IOptionProps } from "../../common/props"; import { IElementRefProps } from "../html/html"; @@ -58,7 +60,8 @@ export interface IHTMLSelectProps // this component is simple enough that tests would be purely tautological. /* istanbul ignore next */ -export class HTMLSelect extends React.PureComponent { +@polyfill +export class HTMLSelect extends AbstractPureComponent2 { public render() { const { className, diff --git a/packages/core/src/components/html-table/html-table.md b/packages/core/src/components/html-table/html-table.md index 59968128ce..34fad8dcb9 100644 --- a/packages/core/src/components/html-table/html-table.md +++ b/packages/core/src/components/html-table/html-table.md @@ -4,9 +4,11 @@ This component provides Blueprint styling to native HTML tables.

This is not @blueprintjs/table

- This table component is a simple CSS-only skin for HTML `` elements. - It is ideal for basic static tables. If you're looking for more complex - spreadsheet-like features, check out [**@blueprintjs/table**](#table). + +This table component is a simple CSS-only skin for HTML `
` elements. +It is ideal for basic static tables. If you're looking for more complex +spreadsheet-like features, check out [**@blueprintjs/table**](#table). + @## Props diff --git a/packages/core/src/components/html-table/htmlTable.tsx b/packages/core/src/components/html-table/htmlTable.tsx index f327a9e201..96a12eb1cf 100644 --- a/packages/core/src/components/html-table/htmlTable.tsx +++ b/packages/core/src/components/html-table/htmlTable.tsx @@ -16,14 +16,8 @@ import classNames from "classnames"; import * as React from "react"; -import { - HTML_TABLE, - HTML_TABLE_BORDERED, - HTML_TABLE_CONDENSED, - HTML_TABLE_STRIPED, - INTERACTIVE, - SMALL, -} from "../../common/classes"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes } from "../../common"; import { IElementRefProps } from "../html/html"; export interface IHTMLTableProps @@ -50,21 +44,22 @@ export interface IHTMLTableProps // this component is simple enough that tests would be purely tautological. /* istanbul ignore next */ -export class HTMLTable extends React.PureComponent { +@polyfill +export class HTMLTable extends AbstractPureComponent2 { public render() { const { bordered, className, condensed, elementRef, interactive, small, striped, ...htmlProps } = this.props; const classes = classNames( - HTML_TABLE, + Classes.HTML_TABLE, { - [HTML_TABLE_BORDERED]: bordered, - [HTML_TABLE_CONDENSED]: condensed, - [HTML_TABLE_STRIPED]: striped, - [INTERACTIVE]: interactive, - [SMALL]: small, + [Classes.HTML_TABLE_BORDERED]: bordered, + [Classes.HTML_TABLE_CONDENSED]: condensed, + [Classes.HTML_TABLE_STRIPED]: striped, + [Classes.INTERACTIVE]: interactive, + [Classes.SMALL]: small, }, className, ); - // tslint:disable-next-line:blueprint-html-components + // eslint-disable-next-line @blueprintjs/blueprint/html-components return
; } } diff --git a/packages/core/src/components/html/html.md b/packages/core/src/components/html/html.md index 3b4f758ec8..0247ae3765 100644 --- a/packages/core/src/components/html/html.md +++ b/packages/core/src/components/html/html.md @@ -40,9 +40,14 @@ See the [Running text](#core/typography.running-text) documentation for more inf @## Linting -The [**@blueprintjs/tslint-config**](https://www.npmjs.com/package/@blueprintjs/tslint-config) -NPM package provides advanced configuration for [TSLint](http://palantir.github.io/tslint/), -including a custom `blueprint-html-components` rule that will warn on usages of +The [**@blueprintjs/eslint-config**](https://www.npmjs.com/package/@blueprintjs/eslint-config) +NPM package provides advanced configuration for [ESLint](https://eslint.org/). Blueprint is +currently transitioning from [TSLint](https://palantir.github.io/tslint/) to ESLint, and as +such, rules are being migrated from TSLint to ESLint. In the meantime, some TSLint rules are +being run using ESLint. + +The [**@blueprintjs/eslint-plugin-blueprint**](https://www.npmjs.com/package/@blueprintjs/eslint-plugin-blueprint) +package includes a custom `blueprint-html-components` rule that will warn on usages of JSX intrinsic elements (`

`) that have a Blueprint alternative (`

`). See -the package's [README](https://www.npmjs.com/package/@blueprintjs/tslint-config) +the package's [README](https://www.npmjs.com/package/@blueprintjs/eslint-plugin-blueprint) for usage instructions. diff --git a/packages/core/src/components/icon/icon.md b/packages/core/src/components/icon/icon.md index 5048da2518..57cd85a2dc 100644 --- a/packages/core/src/components/icon/icon.md +++ b/packages/core/src/components/icon/icon.md @@ -1,15 +1,19 @@ @# Icon
- See the [**Icons package**](#icons) for a searchable list of all available UI icons. + +See the [**Icons package**](#icons) for a searchable list of all available UI icons. +

SVG icons in 2.0

- Blueprint 2.0 introduced SVG icon support and moved icon resources to a separate __@blueprintjs/icons__ package. - The `Icon` component renders SVG paths and the icon classes are no longer used by any Blueprint React component. - Icon font support has been preserved but should be considered a legacy feature that will be removed in a - future major version. + +Blueprint 2.0 introduced SVG icon support and moved icon resources to a separate __@blueprintjs/icons__ package. +The `Icon` component renders SVG paths and the icon classes are no longer used by any Blueprint React component. +Icon font support has been preserved but should be considered a legacy feature that will be removed in a +future major version. +
This section describes two ways of using the UI icon font: via React `Icon` @@ -66,9 +70,11 @@ import { IconNames } from "@blueprintjs/icons";

Icon fonts are legacy in 2.0

- Blueprint's icon fonts should be considered a legacy feature and will be removed in a future major version. - The SVGs rendered by the React component do not suffer from the blurriness of icon fonts, and browser - support is equivalent. + +Blueprint's icon fonts should be considered a legacy feature and will be removed in a future major version. +The SVGs rendered by the React component do not suffer from the blurriness of icon fonts, and browser +support is equivalent. +
The CSS-only icons API uses the __icon fonts__ from the __@blueprintjs/icons__ package. @@ -88,7 +94,9 @@ Icon classes also support the four `.@ns-intent-*` modifiers to color the image.

Non-standard sizes

- Generally, font icons should only be used at either 16px or 20px. However, if a non-standard size is - necessary, set a `font-size` that is whole multiple of 16 or 20 with the relevant size class. - You can instead use the class `@ns-icon` to make the icon inherit its size from surrounding text. + +Generally, font icons should only be used at either 16px or 20px. However, if a non-standard size is +necessary, set a `font-size` that is whole multiple of 16 or 20 with the relevant size class. +You can instead use the class `@ns-icon` to make the icon inherit its size from surrounding text. +
diff --git a/packages/core/src/components/icon/icon.tsx b/packages/core/src/components/icon/icon.tsx index aaa14c905b..4154c08ac3 100644 --- a/packages/core/src/components/icon/icon.tsx +++ b/packages/core/src/components/icon/icon.tsx @@ -16,9 +16,10 @@ import classNames from "classnames"; import * as React from "react"; +import { polyfill } from "react-lifecycles-compat"; import { IconName, IconSvgPaths16, IconSvgPaths20 } from "@blueprintjs/icons"; -import { Classes, DISPLAYNAME_PREFIX, IIntentProps, IProps, MaybeElement } from "../../common"; +import { AbstractPureComponent2, Classes, DISPLAYNAME_PREFIX, IIntentProps, IProps, MaybeElement } from "../../common"; export { IconName }; @@ -83,7 +84,8 @@ export interface IIconProps extends IIntentProps, IProps { title?: string | false | null; } -export class Icon extends React.PureComponent> { +@polyfill +export class Icon extends AbstractPureComponent2> { public static displayName = `${DISPLAYNAME_PREFIX}.Icon`; public static readonly SIZE_STANDARD = 16; @@ -104,7 +106,7 @@ export class Icon extends React.PureComponent - - {title && {title}} - {paths} - - + return React.createElement( + tagName, + { + ...htmlprops, + className: classes, + title: htmlTitle, + }, + + {title && {title}} + {paths} + , ); } diff --git a/packages/core/src/components/index.ts b/packages/core/src/components/index.ts index 1bbd4ff45a..d3b7d89207 100644 --- a/packages/core/src/components/index.ts +++ b/packages/core/src/components/index.ts @@ -14,13 +14,7 @@ * limitations under the License. */ -declare function require(moduleName: string): any; // declare node.js "require" so that we can conditionally import -if (typeof window !== "undefined" && typeof document !== "undefined") { - // we're in browser - // tslint:disable-next-line:no-var-requires - require("dom4"); // only import actual dom4 if we're in the browser (not server-compatible) - // we'll still need dom4 types for the TypeScript to compile, these are included in package.json -} +import "../common/configureDom4"; import * as contextMenu from "./context-menu/contextMenu"; export const ContextMenu = contextMenu; diff --git a/packages/core/src/components/menu/menu.md b/packages/core/src/components/menu/menu.md index 309e8b6225..3babed1e4d 100644 --- a/packages/core/src/components/menu/menu.md +++ b/packages/core/src/components/menu/menu.md @@ -43,8 +43,10 @@ there is not enough room to the right.

JavaScript only

- Submenus are only supported in the React components. They cannot be created with CSS alone because - they rely on the [`Popover`](#core/components/popover) component for positioning and transitions. + +Submenus are only supported in the React components. They cannot be created with CSS alone because +they rely on the [`Popover`](#core/components/popover) component for positioning and transitions. +
@## Props @@ -126,8 +128,10 @@ as they abstract away the tedious parts of implementing a menu. defined as part of `.@ns-menu-item`.
- Note that the following examples are `display: inline-block`; you may need to adjust - menu width in your own usage. + +Note that the following examples are `display: inline-block`; you may need to adjust +menu width in your own usage. +
@css menu diff --git a/packages/core/src/components/menu/menu.tsx b/packages/core/src/components/menu/menu.tsx index 18542795b7..f829046bbd 100644 --- a/packages/core/src/components/menu/menu.tsx +++ b/packages/core/src/components/menu/menu.tsx @@ -16,8 +16,8 @@ import classNames from "classnames"; import * as React from "react"; - -import * as Classes from "../../common/classes"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes } from "../../common"; import { DISPLAYNAME_PREFIX, IProps } from "../../common/props"; import { MenuDivider } from "./menuDivider"; import { MenuItem } from "./menuItem"; @@ -30,7 +30,8 @@ export interface IMenuProps extends IProps, React.HTMLAttributes any; } -export class Menu extends React.Component { +@polyfill +export class Menu extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.Menu`; public static Divider = MenuDivider; diff --git a/packages/core/src/components/menu/menuItem.tsx b/packages/core/src/components/menu/menuItem.tsx index 9f7e5d218a..282bf26484 100644 --- a/packages/core/src/components/menu/menuItem.tsx +++ b/packages/core/src/components/menu/menuItem.tsx @@ -18,8 +18,8 @@ import classNames from "classnames"; import * as React from "react"; import { Modifiers } from "popper.js"; -import * as Classes from "../../common/classes"; -import { Position } from "../../common/position"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes, Position } from "../../common"; import { DISPLAYNAME_PREFIX, IActionProps, ILinkProps } from "../../common/props"; import { Icon } from "../icon/icon"; import { IPopoverProps, Popover, PopoverInteractionKind } from "../popover/popover"; @@ -98,7 +98,8 @@ export interface IMenuItemProps extends IActionProps, ILinkProps { textClassName?: string; } -export class MenuItem extends React.PureComponent> { +@polyfill +export class MenuItem extends AbstractPureComponent2> { public static defaultProps: IMenuItemProps = { disabled: false, multiline: false, @@ -116,13 +117,14 @@ export class MenuItem extends React.PureComponent - - - {text} - - {this.maybeRenderLabel(labelElement)} - {hasSubmenu && } - + const target = React.createElement( + tagName, + { + ...htmlProps, + ...(disabled ? DISABLED_PROPS : {}), + className: anchorClasses, + }, + , + + {text} + , + this.maybeRenderLabel(labelElement), + hasSubmenu ? : undefined, ); const liClasses = classNames({ [Classes.MENU_SUBMENU]: hasSubmenu }); diff --git a/packages/core/src/components/navbar/navbar.md b/packages/core/src/components/navbar/navbar.md index 8ee0948131..b07e3c3048 100644 --- a/packages/core/src/components/navbar/navbar.md +++ b/packages/core/src/components/navbar/navbar.md @@ -14,8 +14,10 @@ This modifier is not illustrated here because it breaks the document flow.

Body padding required

- The fixed navbar will lie on top of your other content unless you add padding to the top of the - `` element equal to the height of the navbar. Use the `$pt-navbar-height` Sass variable. + +The fixed navbar will lie on top of your other content unless you add padding to the top of the +`` element equal to the height of the navbar. Use the `$pt-navbar-height` Sass variable. +
@### Fixed width diff --git a/packages/core/src/components/navbar/navbar.tsx b/packages/core/src/components/navbar/navbar.tsx index 8455dcbd25..d64b9dd4b4 100644 --- a/packages/core/src/components/navbar/navbar.tsx +++ b/packages/core/src/components/navbar/navbar.tsx @@ -16,7 +16,8 @@ import classNames from "classnames"; import * as React from "react"; -import * as Classes from "../../common/classes"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes } from "../../common"; import { DISPLAYNAME_PREFIX, HTMLDivProps, IProps } from "../../common/props"; import { NavbarDivider } from "./navbarDivider"; import { NavbarGroup } from "./navbarGroup"; @@ -34,7 +35,8 @@ export interface INavbarProps extends IProps, HTMLDivProps { // this component is simple enough that tests would be purely tautological. /* istanbul ignore next */ -export class Navbar extends React.PureComponent { +@polyfill +export class Navbar extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.Navbar`; public static Divider = NavbarDivider; diff --git a/packages/core/src/components/navbar/navbarDivider.tsx b/packages/core/src/components/navbar/navbarDivider.tsx index 8efb106e36..a3e64789fd 100644 --- a/packages/core/src/components/navbar/navbarDivider.tsx +++ b/packages/core/src/components/navbar/navbarDivider.tsx @@ -16,7 +16,8 @@ import classNames from "classnames"; import * as React from "react"; -import * as Classes from "../../common/classes"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes } from "../../common"; import { DISPLAYNAME_PREFIX, HTMLDivProps, IProps } from "../../common/props"; // allow the empty interface so we can label it clearly in the docs @@ -26,7 +27,8 @@ export interface INavbarDividerProps extends IProps, HTMLDivProps { // this component is simple enough that tests would be purely tautological. /* istanbul ignore next */ -export class NavbarDivider extends React.PureComponent { +@polyfill +export class NavbarDivider extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.NavbarDivider`; public render() { diff --git a/packages/core/src/components/navbar/navbarGroup.tsx b/packages/core/src/components/navbar/navbarGroup.tsx index 1bfd39e9a3..2689055c6f 100644 --- a/packages/core/src/components/navbar/navbarGroup.tsx +++ b/packages/core/src/components/navbar/navbarGroup.tsx @@ -16,8 +16,8 @@ import classNames from "classnames"; import * as React from "react"; -import { Alignment } from "../../common/alignment"; -import * as Classes from "../../common/classes"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Alignment, Classes } from "../../common"; import { DISPLAYNAME_PREFIX, HTMLDivProps, IProps } from "../../common/props"; export interface INavbarGroupProps extends IProps, HTMLDivProps { @@ -31,7 +31,8 @@ export interface INavbarGroupProps extends IProps, HTMLDivProps { // this component is simple enough that tests would be purely tautological. /* istanbul ignore next */ -export class NavbarGroup extends React.PureComponent { +@polyfill +export class NavbarGroup extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.NavbarGroup`; public static defaultProps: INavbarGroupProps = { diff --git a/packages/core/src/components/navbar/navbarHeading.tsx b/packages/core/src/components/navbar/navbarHeading.tsx index 417a795e01..80b8a2f979 100644 --- a/packages/core/src/components/navbar/navbarHeading.tsx +++ b/packages/core/src/components/navbar/navbarHeading.tsx @@ -16,7 +16,8 @@ import classNames from "classnames"; import * as React from "react"; -import * as Classes from "../../common/classes"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes } from "../../common"; import { DISPLAYNAME_PREFIX, HTMLDivProps, IProps } from "../../common/props"; // allow the empty interface so we can label it clearly in the docs @@ -26,7 +27,8 @@ export interface INavbarHeadingProps extends IProps, HTMLDivProps { // this component is simple enough that tests would be purely tautological. /* istanbul ignore next */ -export class NavbarHeading extends React.PureComponent { +@polyfill +export class NavbarHeading extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.NavbarHeading`; public render() { diff --git a/packages/core/src/components/non-ideal-state/nonIdealState.tsx b/packages/core/src/components/non-ideal-state/nonIdealState.tsx index 9216846c27..392905e78e 100644 --- a/packages/core/src/components/non-ideal-state/nonIdealState.tsx +++ b/packages/core/src/components/non-ideal-state/nonIdealState.tsx @@ -17,6 +17,8 @@ import classNames from "classnames"; import * as React from "react"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2 } from "../../common"; import * as Classes from "../../common/classes"; import { DISPLAYNAME_PREFIX, IProps, MaybeElement } from "../../common/props"; import { ensureElement } from "../../common/utils"; @@ -46,7 +48,8 @@ export interface INonIdealStateProps extends IProps { title?: React.ReactNode; } -export class NonIdealState extends React.PureComponent { +@polyfill +export class NonIdealState extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.NonIdealState`; public render() { diff --git a/packages/core/src/components/overflow-list/overflowList.tsx b/packages/core/src/components/overflow-list/overflowList.tsx index 7bbb553376..687a419457 100644 --- a/packages/core/src/components/overflow-list/overflowList.tsx +++ b/packages/core/src/components/overflow-list/overflowList.tsx @@ -137,45 +137,36 @@ export class OverflowList extends React.Component, IOve this.repartition(false); } - public componentWillReceiveProps(nextProps: IOverflowListProps) { - const { - collapseFrom, - items, - minVisibleItems, - observeParents, - overflowRenderer, - visibleItemRenderer, - } = this.props; - if (observeParents !== nextProps.observeParents) { + public shouldComponentUpdate(_nextProps: IOverflowListProps, nextState: IOverflowListState) { + // We want this component to always re-render, even when props haven't changed, so that + // changes in the renderers' behavior can be reflected. + // The following statement prevents re-rendering only in the case where the state changes + // identity (i.e. setState was called), but the state is still the same when + // shallow-compared to the previous state. + return !(this.state !== nextState && shallowCompareKeys(this.state, nextState)); + } + + public componentDidUpdate(prevProps: IOverflowListProps, prevState: IOverflowListState) { + if (prevProps.observeParents !== this.props.observeParents) { console.warn(OVERFLOW_LIST_OBSERVE_PARENTS_CHANGED); } + if ( - collapseFrom !== nextProps.collapseFrom || - items !== nextProps.items || - minVisibleItems !== nextProps.minVisibleItems || - overflowRenderer !== nextProps.overflowRenderer || - visibleItemRenderer !== nextProps.visibleItemRenderer + prevProps.collapseFrom !== this.props.collapseFrom || + prevProps.items !== this.props.items || + prevProps.minVisibleItems !== this.props.minVisibleItems || + prevProps.overflowRenderer !== this.props.overflowRenderer || + prevProps.visibleItemRenderer !== this.props.visibleItemRenderer ) { // reset visible state if the above props change. this.setState({ direction: OverflowDirection.GROW, lastOverflowCount: 0, overflow: [], - visible: nextProps.items, + visible: this.props.items, }); } - } - public shouldComponentUpdate(_nextProps: IOverflowListProps, nextState: IOverflowListState) { - // We want this component to always re-render, even when props haven't changed, so that - // changes in the renderers' behavior can be reflected. - // The following statement prevents re-rendering only in the case where the state changes - // identity (i.e. setState was called), but the state is still the same when - // shallow-compared to the previous state. - return !(this.state !== nextState && shallowCompareKeys(this.state, nextState)); - } - - public componentDidUpdate(_prevProps: IOverflowListProps, prevState: IOverflowListState) { if (!shallowCompareKeys(prevState, this.state)) { this.repartition(false); } @@ -191,23 +182,23 @@ export class OverflowList extends React.Component, IOve } public render() { - const { - className, - collapseFrom, - observeParents, - style, - tagName: TagName = "div", - visibleItemRenderer, - } = this.props; + const { className, collapseFrom, observeParents, style, tagName = "div", visibleItemRenderer } = this.props; const overflow = this.maybeRenderOverflow(); + const list = React.createElement( + tagName, + { + className: classNames(Classes.OVERFLOW_LIST, className), + style, + }, + collapseFrom === Boundary.START ? overflow : null, + this.state.visible.map(visibleItemRenderer), + collapseFrom === Boundary.END ? overflow : null, +
(this.spacer = ref)} />, + ); + return ( - - {collapseFrom === Boundary.START ? overflow : null} - {this.state.visible.map(visibleItemRenderer)} - {collapseFrom === Boundary.END ? overflow : null} -
(this.spacer = ref)} /> - + {list} ); } diff --git a/packages/core/src/components/overlay/overlay.md b/packages/core/src/components/overlay/overlay.md index 2a6ed8f682..fdd6a14ace 100644 --- a/packages/core/src/components/overlay/overlay.md +++ b/packages/core/src/components/overlay/overlay.md @@ -44,8 +44,9 @@ actually closes the overlay.

A note about overlay content positioning

- When rendered inline, content will automatically be set to `position: absolute` to respect - document flow. Otherwise, content will be set to `position: fixed` to cover the entire viewport. + +When rendered inline, content will automatically be set to `position: absolute` to respect +document flow. Otherwise, content will be set to `position: fixed` to cover the entire viewport.
```tsx diff --git a/packages/core/src/components/overlay/overlay.tsx b/packages/core/src/components/overlay/overlay.tsx index 85edc2ead3..e2e38dae7f 100644 --- a/packages/core/src/components/overlay/overlay.tsx +++ b/packages/core/src/components/overlay/overlay.tsx @@ -16,11 +16,11 @@ import classNames from "classnames"; import * as React from "react"; +import { polyfill } from "react-lifecycles-compat"; import { CSSTransition, TransitionGroup } from "react-transition-group"; import { findDOMNode } from "react-dom"; -import * as Classes from "../../common/classes"; -import * as Keys from "../../common/keys"; +import { AbstractPureComponent2, Classes, Keys } from "../../common"; import { DISPLAYNAME_PREFIX, IProps } from "../../common/props"; import { safeInvoke } from "../../common/utils"; import { Portal } from "../portal/portal"; @@ -171,7 +171,8 @@ export interface IOverlayState { hasEverOpened?: boolean; } -export class Overlay extends React.PureComponent { +@polyfill +export class Overlay extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.Overlay`; public static defaultProps: IOverlayProps = { @@ -188,6 +189,13 @@ export class Overlay extends React.PureComponent { usePortal: true, }; + public static getDerivedStateFromProps({ isOpen: hasEverOpened }: IOverlayProps) { + if (hasEverOpened) { + return { hasEverOpened }; + } + return null; + } + private static openStack: Overlay[] = []; private static getLastOpened = () => Overlay.openStack[Overlay.openStack.length - 1]; @@ -253,10 +261,6 @@ export class Overlay extends React.PureComponent { } } - public componentWillReceiveProps(nextProps: IOverlayProps) { - this.setState({ hasEverOpened: this.state.hasEverOpened || nextProps.isOpen }); - } - public componentDidUpdate(prevProps: IOverlayProps) { if (prevProps.isOpen && !this.props.isOpen) { this.overlayWillClose(); @@ -433,6 +437,7 @@ export class Overlay extends React.PureComponent { if ( this.props.enforceFocus && this.containerElement != null && + e.target instanceof Node && !this.containerElement.contains(e.target as HTMLElement) ) { // prevent default focus behavior (sometimes auto-scrolls the page) diff --git a/packages/core/src/components/panel-stack/_panel-stack.scss b/packages/core/src/components/panel-stack/_panel-stack.scss index e2162c7454..1a55f1a628 100644 --- a/packages/core/src/components/panel-stack/_panel-stack.scss +++ b/packages/core/src/components/panel-stack/_panel-stack.scss @@ -48,6 +48,7 @@ @include position-all(absolute, 0); display: flex; flex-direction: column; + z-index: 1; // border between panels, visible during transition margin-right: -1px; @@ -59,6 +60,10 @@ .#{$ns}-dark & { background-color: $dark-gray4; } + + &:nth-last-child(n + 4) { + display: none; + } } // PUSH transition: enter from right (100%), existing panel moves off left. diff --git a/packages/core/src/components/panel-stack/panel-stack.md b/packages/core/src/components/panel-stack/panel-stack.md index 5a3f61cbf0..42ca6e5f68 100644 --- a/packages/core/src/components/panel-stack/panel-stack.md +++ b/packages/core/src/components/panel-stack/panel-stack.md @@ -12,6 +12,11 @@ the stack. Panels use [`CSSTransition`](http://reactcommunity.org/react-transition-group/css-transition) for seamless transitions. +By default, only the currently active panel is rendered to the DOM. This means +that other panels are unmounted and can lose their component state as a user +transitions between the panels. You can notice this in the example below as +the numeric counter is reset. To render all panels to the DOM and keep their +React trees mounted, change the `renderActivePanelOnly` prop. @reactExample PanelStackExample diff --git a/packages/core/src/components/panel-stack/panelStack.tsx b/packages/core/src/components/panel-stack/panelStack.tsx index a8a7bb72da..13168010c4 100644 --- a/packages/core/src/components/panel-stack/panelStack.tsx +++ b/packages/core/src/components/panel-stack/panelStack.tsx @@ -16,9 +16,10 @@ import classNames from "classnames"; import * as React from "react"; +import { polyfill } from "react-lifecycles-compat"; import { CSSTransition, TransitionGroup } from "react-transition-group"; - -import * as Classes from "../../common/classes"; +import { AbstractPureComponent2, Classes } from "../../common"; +import * as Errors from "../../common/errors"; import { IProps } from "../../common/props"; import { safeInvoke } from "../../common/utils"; import { IPanel } from "./panelProps"; @@ -28,8 +29,10 @@ export interface IPanelStackProps extends IProps { /** * The initial panel to show on mount. This panel cannot be removed from the * stack and will appear when the stack is empty. + * This prop is only used in uncontrolled mode and is thus mutually + * exclusive with the `stack` prop. */ - initialPanel: IPanel; + initialPanel?: IPanel; /** * Callback invoked when the user presses the back button or a panel invokes @@ -42,6 +45,26 @@ export interface IPanelStackProps extends IProps { * prop method. */ onOpen?: (addedPanel: IPanel) => void; + + /** + * If false, PanelStack will render all panels in the stack to the DOM, allowing their + * React component trees to maintain state as a user navigates through the stack. + * Panels other than the currently active one will be invisible. + * @default true + */ + renderActivePanelOnly?: boolean; + + /** + * Whether to show the header with the "back" button in each panel. + * @default true + */ + showPanelHeader?: boolean; + + /** + * The full stack of panels in controlled mode. The last panel in the stack + * will be displayed. + */ + stack?: Array>; } export interface IPanelStackState { @@ -52,12 +75,29 @@ export interface IPanelStackState { stack: IPanel[]; } -export class PanelStack extends React.PureComponent { +@polyfill +export class PanelStack extends AbstractPureComponent2 { public state: IPanelStackState = { direction: "push", - stack: [this.props.initialPanel], + stack: this.props.stack != null ? this.props.stack.slice().reverse() : [this.props.initialPanel], }; + public componentDidUpdate(prevProps: IPanelStackProps, _prevState: IPanelStackState, _snapshot: {}) { + super.componentDidUpdate(prevProps, _prevState, _snapshot); + + // Always update local stack if stack prop changes + if (this.props.stack !== prevProps.stack && prevProps.stack != null) { + this.setState({ stack: this.props.stack.slice().reverse() }); + } + + // Only update animation direction if stack length changes + const stackLength = this.props.stack != null ? this.props.stack.length : 0; + const prevStackLength = prevProps.stack != null ? prevProps.stack.length : 0; + if (stackLength !== prevStackLength && prevProps.stack != null) { + this.setState({ direction: prevProps.stack.length - this.props.stack.length < 0 ? "push" : "pop" }); + } + } + public render() { const classes = classNames( Classes.PANEL_STACK, @@ -66,28 +106,59 @@ export class PanelStack extends React.PureComponent - {this.renderCurrentPanel()} + {this.renderPanels()} ); } - private renderCurrentPanel() { + protected validateProps(props: IPanelStackProps) { + if ( + (props.initialPanel == null && props.stack == null) || + (props.initialPanel != null && props.stack != null) + ) { + throw new Error(Errors.PANEL_STACK_INITIAL_PANEL_STACK_MUTEX); + } + if (props.stack != null && props.stack.length === 0) { + throw new Error(Errors.PANEL_STACK_REQUIRES_PANEL); + } + } + + private renderPanels() { + const { renderActivePanelOnly = true } = this.props; const { stack } = this.state; if (stack.length === 0) { return null; } - const [activePanel, previousPanel] = stack; + const panelsToRender = renderActivePanelOnly ? [stack[0]] : stack; + const panelViews = panelsToRender.map(this.renderPanel).reverse(); + return panelViews; + } + + private renderPanel = (panel: IPanel, index: number) => { + const { renderActivePanelOnly, showPanelHeader = true } = this.props; + const { stack } = this.state; + + // With renderActivePanelOnly={false} we would keep all the CSSTransitions rendered, + // therefore they would not trigger the "enter" transition event as they were entered. + // To force the enter event, we want to change the key, but stack.length is not enough + // and a single panel should not rerender as long as it's hidden. + // This key contains two parts: first one, stack.length - index is constant (and unique) for each panel, + // second one, active changes only when the panel becomes or stops being active. + const layer = stack.length - index; + const key = renderActivePanelOnly ? stack.length : layer; + return ( - + ); - } + }; private handlePanelClose = (panel: IPanel) => { const { stack } = this.state; @@ -96,17 +167,21 @@ export class PanelStack extends React.PureComponent ({ - direction: "pop", - stack: state.stack.filter(p => p !== panel), - })); + if (this.props.stack == null) { + this.setState(state => ({ + direction: "pop", + stack: state.stack.slice(1), + })); + } }; private handlePanelOpen = (panel: IPanel) => { safeInvoke(this.props.onOpen, panel); - this.setState(state => ({ - direction: "push", - stack: [panel, ...state.stack], - })); + if (this.props.stack == null) { + this.setState(state => ({ + direction: "push", + stack: [panel, ...state.stack], + })); + } }; } diff --git a/packages/core/src/components/panel-stack/panelView.tsx b/packages/core/src/components/panel-stack/panelView.tsx index a5aa2ea3f7..d566fad880 100644 --- a/packages/core/src/components/panel-stack/panelView.tsx +++ b/packages/core/src/components/panel-stack/panelView.tsx @@ -15,8 +15,8 @@ */ import * as React from "react"; - -import { Classes } from "../../common"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes } from "../../common"; import { Button } from "../button/buttons"; import { Text } from "../text/text"; import { IPanel } from "./panelProps"; @@ -39,27 +39,40 @@ export interface IPanelViewProps { /** The previous panel in the stack, for rendering the "back" button. */ previousPanel?: IPanel; + + /** Whether to show the header with the "back" button. */ + showHeader: boolean; } -export class PanelView extends React.PureComponent { +@polyfill +export class PanelView extends AbstractPureComponent2 { public render() { const { panel, onOpen } = this.props; // two tags in header ensure title is centered as long as // possible, due to `flex: 1` magic. return (
-
- {this.maybeRenderBack()} - - {panel.title} - - -
+ {this.maybeRenderHeader()}
); } + private maybeRenderHeader() { + if (!this.props.showHeader) { + return null; + } + return ( +
+ {this.maybeRenderBack()} + + {this.props.panel.title} + + +
+ ); + } + private maybeRenderBack() { if (this.props.previousPanel === undefined) { return null; diff --git a/packages/core/src/components/popover/_popover.scss b/packages/core/src/components/popover/_popover.scss index 9794755f1f..401842aeac 100644 --- a/packages/core/src/components/popover/_popover.scss +++ b/packages/core/src/components/popover/_popover.scss @@ -121,3 +121,7 @@ span.#{$ns}-popover-target { // this is important for span tag as default inline display height only includes text. // div tag can be used for display: block, which works fine. } + +.#{$ns}-popover-wrapper.#{$ns}-fill { + width: 100%; +} diff --git a/packages/core/src/components/popover/popover.md b/packages/core/src/components/popover/popover.md index b4050ef5fd..6757986be0 100644 --- a/packages/core/src/components/popover/popover.md +++ b/packages/core/src/components/popover/popover.md @@ -3,8 +3,8 @@ Popovers display floating content next to a target element. `Popover` is built on top of the [__Popper.js__](https://popper.js.org) library. -Popper.js is a small (~6kb) library that offers a powerful, customizable -positioning engine and operates at blazing speed (~60fps). +Popper.js is a small (`~6kb`) library that offers a powerful, customizable +positioning engine and operates at blazing speed (`~60fps`). @reactExample PopoverExample @@ -73,14 +73,16 @@ wrapped in a single element when rendering

Button targets

- Buttons make great popover targets, but the `disabled` attribute on a `
```tsx -const { Button, Intent, Popover, PopoverInteractionKind, Position } = "@blueprintjs/core"; +import { Button, Intent, Popover, PopoverInteractionKind, Position } from "@blueprintjs/core"; export class PopoverExample extends React.Component { public render() { @@ -107,7 +109,8 @@ export class PopoverExample extends React.Component { @### Position -The `position` prop controls the Popover's position relative to the target. The `Position` enumeration defines the full set of supported values. There are two attributes of positioning to consider: +The `position` prop controls the Popover's position relative to the target. +The `Position` enumeration defines the full set of supported values. There are two attributes of positioning to consider: - Which __side__ of the target the popover should render on. - The popover's __alignment__ relative to the target. @@ -118,25 +121,38 @@ These two attributes can be expressed with a single value having the following s [SIDE]_[ALIGNMENT] -The following example shows all supported `Position` values and how each behaves in practice. Note that if _[ALIGNMENT] is ommitted, the popover will align to the __center__ of the target. +The following example shows all supported `Position` values and how each behaves in practice. +Note that if _[ALIGNMENT] is ommitted, +the popover will align to the __center__ of the target. @reactExample PopoverPositionExample #### Automatic positioning -The Popover's `position` can also be chosen _automatically_ via `"auto"`, `"auto-start"`, or `"auto-end"`. All of these options choose and continually update the __side__ for you to avoid overflowing the boundary element (when scrolling within it, for instance). The options differ in how they handle __alignment__: +The Popover's `position` can also be chosen _automatically_ via `"auto"`, `"auto-start"`, or `"auto-end"`. +All of these options choose and continually update the __side__ +for you to avoid overflowing the boundary element (when scrolling within it, for instance). +The options differ in how they handle __alignment__: - In `"auto"` mode (the default for `position`), the Popover will align itself to the center of the target as it flips sides. - In `"auto-start"` mode, the Popover will align itself to the `start` of the target (i.e., the top edge when the popover is on the left or right, or the left edge when the popover is on the top or bottom). - In `"auto-end"` mode, the Popover will align itself to the `end` of the target (i.e., the bottom edge when the popover is on the left or right, or the right edge when the popover is on the top or bottom).
- You can also specify a specific initial position (e.g. `LEFT`, `TOP_RIGHT`) and still update the Popover's position automatically by enabling the modifiers `flip` and `preventOverflow`. [See below](#core/components/popover.modifiers) for information about modifiers. + +You can also specify a specific initial position (e.g. `LEFT`, `TOP_RIGHT`) and still update the Popover's position +automatically by enabling the modifiers `flip` and `preventOverflow`. +[See below](#core/components/popover.modifiers) for information about modifiers. +
@### Modifiers -Modifiers are the tools through which you customize Popper.js's behavior. Popper.js defines several of its own modifiers to handle things such as flipping, preventing overflow from a boundary element, and positioning the arrow. `Popover` defines a few additional modifiers to support itself. You can even define your own modifiers, and customize the Popper.js defaults, through the `modifiers` prop. (Note: it is not currently possible to configure `Popover`'s modifiers through the `modifiers` prop, nor can you define your own with the same name.) +Modifiers are the tools through which you customize Popper.js's behavior. Popper.js defines several of its own modifiers +to handle things such as flipping, preventing overflow from a boundary element, and positioning the arrow. +`Popover` defines a few additional modifiers to support itself. You can even define your own modifiers, and customize +the Popper.js defaults, through the `modifiers` prop. (Note: it is not currently possible to configure `Popover`'s modifiers +through the `modifiers` prop, nor can you define your own with the same name.) **Popper.js modifiers that can be customized via the `modifiers` prop:** @@ -157,8 +173,10 @@ Modifiers are the tools through which you customize Popper.js's behavior. Popper - `updatePopoverState` saves off some popper data to `Popover` React state for fancy things
- See [the Popper.js Modifiers documentation](https://popper.js.org/popper-documentation.html#modifiers) - for more details on all the available modifiers. + +See [the Popper.js Modifiers documentation](https://popper.js.org/popper-documentation.html#modifiers) +for more details on all the available modifiers. +
@### Controlled mode @@ -178,14 +196,16 @@ if the `nextOpenState` is not the same as the `Popover`'s current state).

Disabling controlled popovers

-

If `disabled={true}`, a controlled popover will remain closed even if `isOpen={true}`. - The popover will re-open when `disabled` is set to `false`.

+ +If `disabled={true}`, a controlled popover will remain closed even if `isOpen={true}`. +The popover will re-open when `disabled` is set to `false`. +
#### Example controlled usage ```tsx -const { Popover, Position } = "@blueprintjs/core"; +import { Popover, Position } from "@blueprintjs/core"; export class ControlledPopoverExample extends React.Component<{}, { isOpen: boolean }> { public state = { isOpen: false }; @@ -242,8 +262,10 @@ The following example demonstrates the various interaction kinds (note: these Po

Conditionally styling popover targets

- When a popover is open, `Classes.POPOVER_OPEN` is applied to the target. - You can use this to style the target differently when the popover is open. + +When a popover is open, `Classes.POPOVER_OPEN` is applied to the target. +You can use this to style the target differently when the popover is open. +
@### Closing on click @@ -254,8 +276,8 @@ and `Classes.POPOVER_DISMISS_OVERRIDE`, that can be attached to elements to describe whether click events should dismiss the enclosing popover. To mark an element (and its children) as "dismiss elements", simply add the -class `Classes.POPOVER_DISMISS`. For example, the **Dismiss** button in the -top-level [Popover example](#core/components/popover) has this class, and all +class `Classes.POPOVER_DISMISS`. For example, the **Cancel** and **Delete** buttons in the +top-level [Popover example](#core/components/popover) have this class, and all `MenuItem`s receive this class by default (see `shouldDismissPopover` prop). To enable this behavior on the entire popover body, pass `popoverClassName={Classes.POPOVER_DISMISS}`. @@ -284,10 +306,12 @@ for a menu tree. @reactExample PopoverDismissExample
- Dismiss elements won't have any effect in a popover with - `PopoverInteractionKind.HOVER_TARGET_ONLY`, because there is no way to - interact with the popover content itself: the popover is dismissed the - moment the user mouses away from the target. + +Dismiss elements won't have any effect in a popover with +`PopoverInteractionKind.HOVER_TARGET_ONLY`, because there is no way to +interact with the popover content itself: the popover is dismissed the +moment the user mouses away from the target. +
@### Backdrop @@ -318,9 +342,11 @@ The backdrop element has the same opacity-fade transition as the `Dialog` backdr

Dangerous edge case

- Rendering a `` outside the viewport bounds can easily break - your application by covering the UI with an invisible non-interactive backdrop. This edge case - must be handled by your application code or simply avoided if possible. + +Rendering a `` outside the viewport bounds can easily break +your application by covering the UI with an invisible non-interactive backdrop. This edge case +must be handled by your application code or simply avoided if possible. +
@### Portal rendering @@ -390,8 +416,10 @@ documentation. @## Testing
- Your best resource for strategies in popover testing is - [its own unit test suite.](https://github.com/palantir/blueprint/blob/develop/packages/core/test/popover/popoverTests.tsx) + +Your best resource for strategies in popover testing is +[its own unit test suite.](https://github.com/palantir/blueprint/blob/develop/packages/core/test/popover/popoverTests.tsx) +
#### Animation delays diff --git a/packages/core/src/components/popover/popover.tsx b/packages/core/src/components/popover/popover.tsx index ba1b1fbc67..94c50234c3 100644 --- a/packages/core/src/components/popover/popover.tsx +++ b/packages/core/src/components/popover/popover.tsx @@ -17,10 +17,10 @@ import classNames from "classnames"; import { ModifierFn } from "popper.js"; import * as React from "react"; +import { polyfill } from "react-lifecycles-compat"; import { Manager, Popper, PopperChildrenProps, Reference, ReferenceChildrenProps } from "react-popper"; -import { AbstractPureComponent } from "../../common/abstractPureComponent"; -import * as Classes from "../../common/classes"; +import { AbstractPureComponent2, Classes } from "../../common"; import * as Errors from "../../common/errors"; import { DISPLAYNAME_PREFIX, HTMLDivProps } from "../../common/props"; import * as Utils from "../../common/utils"; @@ -50,6 +50,13 @@ export interface IPopoverProps extends IPopoverSharedProps { */ content?: string | JSX.Element; + /** + * Whether the wrapper and target should take up the full width of their container. + * Note that supplying `true` for this prop will force `targetTagName="div"` and + * `wrapperTagName="div"`. + */ + fill?: boolean; + /** * The kind of interaction that triggers the display of the popover. * @default PopoverInteractionKind.CLICK @@ -92,7 +99,8 @@ export interface IPopoverState { hasDarkParent: boolean; } -export class Popover extends AbstractPureComponent { +@polyfill +export class Popover extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.Popover`; public static defaultProps: IPopoverProps = { @@ -100,6 +108,7 @@ export class Popover extends AbstractPureComponent captureDismiss: false, defaultIsOpen: false, disabled: false, + fill: false, hasBackdrop: false, hoverCloseDelay: 300, hoverOpenDelay: 150, @@ -156,8 +165,12 @@ export class Popover extends AbstractPureComponent // as JSX component instead of intrinsic element. but because of its // type, tsc actually recognizes that it is _any_ intrinsic element, so // it can typecheck the HTML props!! - const { className, disabled, wrapperTagName: WrapperTagName } = this.props; + const { className, disabled, fill } = this.props; const { isOpen } = this.state; + let { wrapperTagName } = this.props; + if (fill) { + wrapperTagName = "div"; + } const isContentEmpty = Utils.ensureElement(this.understandChildren().content) == null; // need to do this check in render(), because `isOpen` is derived from @@ -166,66 +179,77 @@ export class Popover extends AbstractPureComponent console.warn(Errors.POPOVER_WARN_EMPTY_CONTENT); } - return ( - - - {this.renderTarget} - - - {this.renderPopover} - - - - + const wrapperClasses = classNames(Classes.POPOVER_WRAPPER, className, { + [Classes.FILL]: fill, + }); + + const wrapper = React.createElement( + wrapperTagName, + { className: wrapperClasses }, + {this.renderTarget}, + + + {this.renderPopover} + + , ); + + return {wrapper}; } public componentDidMount() { this.updateDarkParent(); } - public componentWillReceiveProps(nextProps: IPopoverProps) { - super.componentWillReceiveProps(nextProps); + public componentDidUpdate(_: IPopoverProps, __: IPopoverState, snapshot: {}) { + super.componentDidUpdate(_, __, snapshot); + this.updateDarkParent(); - const nextIsOpen = this.getIsOpen(nextProps); + const nextIsOpen = this.getIsOpen(this.props); - if (nextProps.isOpen != null && nextIsOpen !== this.state.isOpen) { + if (this.props.isOpen != null && nextIsOpen !== this.state.isOpen) { this.setOpenState(nextIsOpen); // tricky: setOpenState calls setState only if this.props.isOpen is // not controlled, so we need to invoke setState manually here. this.setState({ isOpen: nextIsOpen }); - } else if (this.state.isOpen && nextProps.isOpen == null && nextProps.disabled) { + } else if (this.props.disabled && this.state.isOpen && this.props.isOpen == null) { // special case: close an uncontrolled popover when disabled is set to true this.setOpenState(false); } } - public componentDidUpdate() { - this.updateDarkParent(); - } + /** + * Instance method to instruct the `Popover` to recompute its position. + * + * This method should only be used if you are updating the target in a way + * that does not cause it to re-render, such as changing its _position_ + * without changing its _size_ (since `Popover` already repositions when it + * detects a resize). + */ + public reposition = () => Utils.safeInvoke(this.popperScheduleUpdate); /** * Instance method to instruct the `Popover` to recompute its position. @@ -316,9 +340,14 @@ export class Popover extends AbstractPureComponent }; private renderTarget = (referenceProps: ReferenceChildrenProps) => { - const { openOnTargetFocus, targetClassName, targetProps = {}, targetTagName: TagName } = this.props; + const { fill, openOnTargetFocus, targetClassName, targetProps = {} } = this.props; const { isOpen } = this.state; + const isControlled = this.isControlled(); const isHoverInteractionKind = this.isHoverInteractionKind(); + let { targetTagName } = this.props; + if (fill) { + targetTagName = "div"; + } const finalTargetProps: React.HTMLProps = isHoverInteractionKind ? { @@ -346,19 +375,24 @@ export class Popover extends AbstractPureComponent const tabIndex = rawTabIndex == null && openOnTargetFocus && isHoverInteractionKind ? 0 : rawTabIndex; const clonedTarget: JSX.Element = React.cloneElement(rawTarget, { className: classNames(rawTarget.props.className, { - [Classes.ACTIVE]: isOpen && !isHoverInteractionKind, + // this class is mainly useful for button targets; we should only apply it for uncontrolled popovers + // when they are opened by a user interaction + [Classes.ACTIVE]: isOpen && !isControlled && !isHoverInteractionKind, }), // force disable single Tooltip child when popover is open (BLUEPRINT-552) disabled: isOpen && Utils.isElementOfType(rawTarget, Tooltip) ? true : rawTarget.props.disabled, tabIndex, }); - return ( - - - {clonedTarget} - - + const target = React.createElement( + targetTagName, + { + ...targetProps, + ...finalTargetProps, + }, + clonedTarget, ); + + return {target}; }; // content and target can be specified as props or as children. this method @@ -373,6 +407,8 @@ export class Popover extends AbstractPureComponent }; } + private isControlled = () => this.props.isOpen !== undefined; + private getIsOpen(props: IPopoverProps) { // disabled popovers should never be allowed to open. if (props.disabled) { @@ -419,8 +455,11 @@ export class Popover extends AbstractPureComponent private handleTargetBlur = (e: React.FocusEvent) => { if (this.props.openOnTargetFocus && this.isHoverInteractionKind()) { // if the next element to receive focus is within the popover, we'll want to leave the - // popover open. - if (!this.isElementInPopover(e.relatedTarget as HTMLElement)) { + // popover open. e.relatedTarget ought to tell us the next element to receive focus, but if the user just + // clicked on an element which is not focusable (either by default or with a tabIndex attribute), + // it won't be set. So, we filter those out here and assume that a click handler somewhere else will + // close the popover if necessary. + if (e.relatedTarget != null && !this.isElementInPopover(e.relatedTarget as HTMLElement)) { this.handleMouseLeave(e); } } diff --git a/packages/core/src/components/popover/popoverSharedProps.ts b/packages/core/src/components/popover/popoverSharedProps.ts index b5bd7dbd9b..25a4ee8827 100644 --- a/packages/core/src/components/popover/popoverSharedProps.ts +++ b/packages/core/src/components/popover/popoverSharedProps.ts @@ -98,7 +98,7 @@ export interface IPopoverSharedProps extends IOverlayableProps, IProps { /** * Popper modifier options, passed directly to internal Popper instance. See - * https://popper.js.org/popper-documentation.html#modifiers for complete + * https://popper.js.org/docs/modifiers/ for complete * details. */ modifiers?: PopperModifiers; diff --git a/packages/core/src/components/portal/portal.md b/packages/core/src/components/portal/portal.md index b8d0a1536a..37deee390a 100644 --- a/packages/core/src/components/portal/portal.md +++ b/packages/core/src/components/portal/portal.md @@ -21,7 +21,9 @@ In a **React 16+** environment, the `Portal` component will use [`ReactDOM.creat To use them, supply a child context to a subtree that contains the Portals you want to customize.
- Blueprint uses the React 15-compatible `getChildContext()` API instead of the newer React 16.3 `React.createContext()` API. + +Blueprint uses the React 15-compatible `getChildContext()` API instead of the newer React 16.3 `React.createContext()` API. +
@interface IPortalContext @@ -37,9 +39,11 @@ application.

A note about responsive layouts

- For a single-page app, if the `` is styled with `width: 100%` and `height: 100%`, a `Portal` - may take up extra whitespace and cause the window to undesirably scroll. To fix this, instead - apply `position: absolute` to the `` tag. + +For a single-page app, if the `` is styled with `width: 100%` and `height: 100%`, a `Portal` +may take up extra whitespace and cause the window to undesirably scroll. To fix this, instead +apply `position: absolute` to the `` tag. +
@interface IPortalProps diff --git a/packages/core/src/components/progress-bar/progressBar.tsx b/packages/core/src/components/progress-bar/progressBar.tsx index ce2e95c3e9..fe1c2b020b 100644 --- a/packages/core/src/components/progress-bar/progressBar.tsx +++ b/packages/core/src/components/progress-bar/progressBar.tsx @@ -16,8 +16,8 @@ import classNames from "classnames"; import * as React from "react"; - -import * as Classes from "../../common/classes"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes } from "../../common"; import { DISPLAYNAME_PREFIX, IIntentProps, IProps } from "../../common/props"; import { clamp } from "../../common/utils"; @@ -42,7 +42,8 @@ export interface IProgressBarProps extends IProps, IIntentProps { value?: number; } -export class ProgressBar extends React.PureComponent { +@polyfill +export class ProgressBar extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.ProgressBar`; public render() { diff --git a/packages/core/src/components/resize-sensor/resize-sensor.md b/packages/core/src/components/resize-sensor/resize-sensor.md index 7274da7a1c..813b5a9d8a 100644 --- a/packages/core/src/components/resize-sensor/resize-sensor.md +++ b/packages/core/src/components/resize-sensor/resize-sensor.md @@ -26,10 +26,11 @@ function handleResize(entries: IResizeEntry[]) {

Asynchronous behavior

- The `onResize` callback is invoked asynchronously after a resize is detected - and typically happens at the end of a frame (after layout, before paint). - Therefore, testing behavior that relies on this component involves setting a - timeout for the next frame. + +The `onResize` callback is invoked asynchronously after a resize is detected +and typically happens at the end of a frame (after layout, before paint). +Therefore, testing behavior that relies on this component involves setting a +timeout for the next frame.
@interface IResizeSensorProps diff --git a/packages/core/src/components/resize-sensor/resizeSensor.tsx b/packages/core/src/components/resize-sensor/resizeSensor.tsx index c271d6e353..c39a3e397b 100644 --- a/packages/core/src/components/resize-sensor/resizeSensor.tsx +++ b/packages/core/src/components/resize-sensor/resizeSensor.tsx @@ -15,9 +15,10 @@ */ import * as React from "react"; -import ResizeObserver from "resize-observer-polyfill"; - import { findDOMNode } from "react-dom"; +import { polyfill } from "react-lifecycles-compat"; +import ResizeObserver from "resize-observer-polyfill"; +import { AbstractPureComponent2 } from "../../common"; import { DISPLAYNAME_PREFIX } from "../../common/props"; import { safeInvoke } from "../../common/utils"; @@ -57,7 +58,8 @@ export interface IResizeSensorProps { observeParents?: boolean; } -export class ResizeSensor extends React.PureComponent { +@polyfill +export class ResizeSensor extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.ResizeSensor`; private element: Element | null = null; diff --git a/packages/core/src/components/skeleton/skeleton.md b/packages/core/src/components/skeleton/skeleton.md index 6e8707e54d..adad54d29f 100644 --- a/packages/core/src/components/skeleton/skeleton.md +++ b/packages/core/src/components/skeleton/skeleton.md @@ -16,9 +16,11 @@ a loading animation.

Manually disable focusable elements

- When using the `.@ns-skeleton` class on focusable elements such as inputs - and buttons, be sure to disable the element, via either the `disabled` or - `tabindex="-1"` attributes. Failing to do so will allow these skeleton - elements to be focused when they shouldn't be. + +When using the `.@ns-skeleton` class on focusable elements such as inputs +and buttons, be sure to disable the element, via either the `disabled` or +`tabindex="-1"` attributes. Failing to do so will allow these skeleton +elements to be focused when they shouldn't be. +
diff --git a/packages/core/src/components/slider/handle.tsx b/packages/core/src/components/slider/handle.tsx index 79bbdc8c2a..3d78a64b23 100644 --- a/packages/core/src/components/slider/handle.tsx +++ b/packages/core/src/components/slider/handle.tsx @@ -16,10 +16,8 @@ import classNames from "classnames"; import * as React from "react"; - -import { AbstractPureComponent } from "../../common/abstractPureComponent"; -import * as Classes from "../../common/classes"; -import * as Keys from "../../common/keys"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes, Keys } from "../../common"; import { DISPLAYNAME_PREFIX } from "../../common/props"; import { clamp, safeInvoke } from "../../common/utils"; import { IHandleProps } from "./handleProps"; @@ -49,7 +47,8 @@ export interface IHandleState { const NUMBER_PROPS = ["max", "min", "stepSize", "tickSize", "value"]; /** Internal component for a Handle with click/drag/keyboard logic to determine a new value. */ -export class Handle extends AbstractPureComponent { +@polyfill +export class Handle extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.SliderHandle`; public state = { diff --git a/packages/core/src/components/slider/multiSlider.tsx b/packages/core/src/components/slider/multiSlider.tsx index ee7aab887c..e2b9c13b6e 100644 --- a/packages/core/src/components/slider/multiSlider.tsx +++ b/packages/core/src/components/slider/multiSlider.tsx @@ -16,11 +16,11 @@ import classNames from "classnames"; import * as React from "react"; +import { polyfill } from "react-lifecycles-compat"; -import { Classes, Intent } from "../../common"; -import { AbstractPureComponent } from "../../common/abstractPureComponent"; +import { AbstractPureComponent2, Classes, Intent } from "../../common"; import * as Errors from "../../common/errors"; -import { DISPLAYNAME_PREFIX, IProps } from "../../common/props"; +import { DISPLAYNAME_PREFIX, IIntentProps, IProps } from "../../common/props"; import * as Utils from "../../common/utils"; import { Handle } from "./handle"; import { HandleInteractionKind, HandleType, IHandleProps } from "./handleProps"; @@ -33,13 +33,7 @@ import { argMin, fillValues, formatPercentage } from "./sliderUtils"; const MultiSliderHandle: React.SFC = () => null; MultiSliderHandle.displayName = `${DISPLAYNAME_PREFIX}.MultiSliderHandle`; -// TODO: move this to props.ts in a follow up PR -/** A convenience type for React's optional children prop. */ -export interface IChildrenProps { - children?: React.ReactNode; -} - -export interface ISliderBaseProps extends IProps { +export interface ISliderBaseProps extends IProps, IIntentProps { /** * Whether the slider is non-interactive. * @default false @@ -119,7 +113,8 @@ export interface ISliderState { tickSizeRatio?: number; } -export class MultiSlider extends AbstractPureComponent { +@polyfill +export class MultiSlider extends AbstractPureComponent2 { public static defaultSliderProps: ISliderBaseProps = { disabled: false, labelStepSize: 1, @@ -139,6 +134,15 @@ export class MultiSlider extends AbstractPureComponent) { if (props.stepSize <= 0) { throw new Error(Errors.SLIDER_ZERO_STEP); } @@ -267,7 +266,9 @@ export class MultiSlider extends AbstractPureComponent left - right, + ); const startOffset = formatPercentage(startRatio); const endOffset = formatPercentage(1 - endRatio); const style: React.CSSProperties = this.props.vertical @@ -280,14 +281,20 @@ export class MultiSlider extends AbstractPureComponent ( + const handleProps = getSortedInteractiveHandleProps(this.props); + + if (handleProps.length === 0) { + return null; + } + + return handleProps.map(({ value, type }, index) => ( handle.value); + const handleProps = getSortedInteractiveHandleProps(this.props); + const oldValues = handleProps.map(handle => handle.value); const newValues = oldValues.slice(); newValues[oldIndex] = newValue; newValues.sort((left, right) => left - right); @@ -374,19 +382,23 @@ export class MultiSlider extends AbstractPureComponent { - const oldValues = this.handleProps.map(handle => handle.value); + const handleProps = getSortedInteractiveHandleProps(this.props); + const oldValues = handleProps.map(handle => handle.value); if (!Utils.arraysEqual(newValues, oldValues)) { Utils.safeInvoke(this.props.onChange, newValues); - this.handleProps.forEach((handle, index) => { + handleProps.forEach((handle, index) => { if (oldValues[index] !== newValues[index]) { Utils.safeInvoke(handle.onChange, newValues[index]); } @@ -395,17 +407,13 @@ export class MultiSlider extends AbstractPureComponent { + const handleProps = getSortedInteractiveHandleProps(this.props); Utils.safeInvoke(this.props.onRelease, newValues); - this.handleProps.forEach((handle, index) => { + handleProps.forEach((handle, index) => { Utils.safeInvoke(handle.onRelease, newValues[index]); }); }; - private getLabelPrecision({ labelPrecision, stepSize }: IMultiSliderProps) { - // infer default label precision from stepSize because that's how much the handle moves. - return labelPrecision == null ? Utils.countDecimalPlaces(stepSize) : labelPrecision; - } - private getOffsetRatio(value: number) { return Utils.clamp((value - this.props.min) * this.state.tickSizeRatio, 0, 1); } @@ -437,11 +445,14 @@ function getLabelPrecision({ labelPrecision, stepSize }: IMultiSliderProps) { return labelPrecision == null ? Utils.countDecimalPlaces(stepSize) : labelPrecision; } -function getSortedInteractiveHandleProps(props: IChildrenProps): IHandleProps[] { +function getSortedInteractiveHandleProps(props: React.PropsWithChildren): IHandleProps[] { return getSortedHandleProps(props, childProps => childProps.interactionKind !== HandleInteractionKind.NONE); } -function getSortedHandleProps({ children }: IChildrenProps, predicate: (props: IHandleProps) => boolean = () => true) { +function getSortedHandleProps( + { children }: React.PropsWithChildren, + predicate: (props: IHandleProps) => boolean = () => true, +) { const maybeHandles = React.Children.map(children, child => Utils.isElementOfType(child, MultiSlider.Handle) && predicate(child.props) ? child.props : null, ); diff --git a/packages/core/src/components/slider/rangeSlider.tsx b/packages/core/src/components/slider/rangeSlider.tsx index 85d65f3b98..558fc6405a 100644 --- a/packages/core/src/components/slider/rangeSlider.tsx +++ b/packages/core/src/components/slider/rangeSlider.tsx @@ -15,10 +15,9 @@ */ import * as React from "react"; - -import { AbstractPureComponent } from "../../common/abstractPureComponent"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Intent } from "../../common"; import * as Errors from "../../common/errors"; -import { Intent } from "../../common/intent"; import { DISPLAYNAME_PREFIX } from "../../common/props"; import { ISliderBaseProps, MultiSlider } from "./multiSlider"; @@ -43,9 +42,11 @@ export interface IRangeSliderProps extends ISliderBaseProps { onRelease?(value: NumberRange): void; } -export class RangeSlider extends AbstractPureComponent { +@polyfill +export class RangeSlider extends AbstractPureComponent2 { public static defaultProps: IRangeSliderProps = { ...MultiSlider.defaultSliderProps, + intent: Intent.PRIMARY, value: [0, 10], }; @@ -55,7 +56,7 @@ export class RangeSlider extends AbstractPureComponent { const { value, ...props } = this.props; return ( - + ); diff --git a/packages/core/src/components/slider/slider.tsx b/packages/core/src/components/slider/slider.tsx index b74364db05..be0ba0520c 100644 --- a/packages/core/src/components/slider/slider.tsx +++ b/packages/core/src/components/slider/slider.tsx @@ -15,9 +15,8 @@ */ import * as React from "react"; - -import { AbstractPureComponent } from "../../common/abstractPureComponent"; -import { Intent } from "../../common/intent"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Intent } from "../../common"; import { DISPLAYNAME_PREFIX } from "../../common/props"; import { ISliderBaseProps, MultiSlider } from "./multiSlider"; @@ -42,23 +41,25 @@ export interface ISliderProps extends ISliderBaseProps { onRelease?(value: number): void; } -export class Slider extends AbstractPureComponent { +@polyfill +export class Slider extends AbstractPureComponent2 { public static defaultProps: ISliderProps = { ...MultiSlider.defaultSliderProps, initialValue: 0, + intent: Intent.PRIMARY, value: 0, }; public static displayName = `${DISPLAYNAME_PREFIX}.Slider`; public render() { - const { initialValue, value, onChange, onRelease, ...props } = this.props; + const { initialValue, intent, value, onChange, onRelease, ...props } = this.props; return ( = initialValue ? Intent.PRIMARY : undefined} + intentAfter={value < initialValue ? intent : undefined} + intentBefore={value >= initialValue ? intent : undefined} onChange={onChange} onRelease={onRelease} /> diff --git a/packages/core/src/components/spinner/spinner.md b/packages/core/src/components/spinner/spinner.md index 712e403526..f4d756fe89 100644 --- a/packages/core/src/components/spinner/spinner.md +++ b/packages/core/src/components/spinner/spinner.md @@ -23,9 +23,11 @@ by including `Classes.SMALL` or `Classes.LARGE` in `className` instead of the

IE11 compatibility note

- IE11 [does not support CSS transitions on SVG elements][msdn-css-svg] so spinners with known - `value` will not smoothly transition as `value` changes. Indeterminate spinners still animate - correctly because they rely on CSS animations. + +IE11 [does not support CSS transitions on SVG elements][msdn-css-svg] so spinners with known +`value` will not smoothly transition as `value` changes. Indeterminate spinners still animate +correctly because they rely on CSS animations. +
[msdn-css-svg]: https://developer.microsoft.com/en-us/microsoft-edge/platform/status/csstransitionsforsvgelements/?q=svg diff --git a/packages/core/src/components/spinner/spinner.tsx b/packages/core/src/components/spinner/spinner.tsx index 27705fdd7d..a8ef40782c 100644 --- a/packages/core/src/components/spinner/spinner.tsx +++ b/packages/core/src/components/spinner/spinner.tsx @@ -16,9 +16,8 @@ import classNames from "classnames"; import * as React from "react"; - -import { AbstractPureComponent } from "../../common/abstractPureComponent"; -import * as Classes from "../../common/classes"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes } from "../../common"; import { SPINNER_WARN_CLASSES_SIZE } from "../../common/errors"; import { DISPLAYNAME_PREFIX, IIntentProps, IProps } from "../../common/props"; import { clamp } from "../../common/utils"; @@ -65,7 +64,8 @@ export interface ISpinnerProps extends IProps, IIntentProps { value?: number; } -export class Spinner extends AbstractPureComponent { +@polyfill +export class Spinner extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.Spinner`; public static readonly SIZE_SMALL = 20; @@ -80,7 +80,7 @@ export class Spinner extends AbstractPureComponent { } public render() { - const { className, intent, value, tagName: TagName = "div" } = this.props; + const { className, intent, value, tagName = "div" } = this.props; const size = this.getSize(); const classes = classNames( @@ -92,32 +92,33 @@ export class Spinner extends AbstractPureComponent { // keep spinner track width consistent at all sizes (down to about 10px). const strokeWidth = Math.min(MIN_STROKE_WIDTH, (STROKE_WIDTH * Spinner.SIZE_LARGE) / size); - const strokeOffset = PATH_LENGTH - PATH_LENGTH * (value == null ? 0.25 : clamp(value, 0, 1)); // multiple DOM elements around SVG are necessary to properly isolate animation: // - SVG elements in IE do not support anim/trans so they must be set on a parent HTML element. // - SPINNER_ANIMATION isolates svg from parent display and is always centered inside root element. - return ( - - - - - - - - + return React.createElement( + tagName, + { className: classes }, + React.createElement( + tagName, + { className: Classes.SPINNER_ANIMATION }, + + + + , + ), ); } diff --git a/packages/core/src/components/tabs/tab.tsx b/packages/core/src/components/tabs/tab.tsx index 5edfecd0d5..6a0d0baec1 100644 --- a/packages/core/src/components/tabs/tab.tsx +++ b/packages/core/src/components/tabs/tab.tsx @@ -16,13 +16,13 @@ import classNames from "classnames"; import * as React from "react"; - -import * as Classes from "../../common/classes"; -import { DISPLAYNAME_PREFIX, IProps } from "../../common/props"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes } from "../../common"; +import { DISPLAYNAME_PREFIX, HTMLDivProps, IProps } from "../../common/props"; export type TabId = string | number; -export interface ITabProps extends IProps { +export interface ITabProps extends IProps, Omit { /** * Content of tab title, rendered in a list above the active panel. * Can also be set via the `title` prop. @@ -59,7 +59,8 @@ export interface ITabProps extends IProps { title?: React.ReactNode; } -export class Tab extends React.PureComponent { +@polyfill +export class Tab extends AbstractPureComponent2 { public static defaultProps: ITabProps = { disabled: false, id: undefined, diff --git a/packages/core/src/components/tabs/tabTitle.tsx b/packages/core/src/components/tabs/tabTitle.tsx index 95cbc44862..05df9d1e64 100644 --- a/packages/core/src/components/tabs/tabTitle.tsx +++ b/packages/core/src/components/tabs/tabTitle.tsx @@ -16,9 +16,9 @@ import classNames from "classnames"; import * as React from "react"; - -import * as Classes from "../../common/classes"; -import { DISPLAYNAME_PREFIX } from "../../common/props"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes } from "../../common"; +import { DISPLAYNAME_PREFIX, removeNonHTMLProps } from "../../common/props"; import { ITabProps, TabId } from "./tab"; export interface ITabTitleProps extends ITabProps { @@ -32,26 +32,28 @@ export interface ITabTitleProps extends ITabProps { selected: boolean; } -export class TabTitle extends React.PureComponent { +@polyfill +export class TabTitle extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.TabTitle`; public render() { - const { disabled, id, parentId, selected } = this.props; + const { className, children, disabled, id, parentId, selected, title, ...htmlProps } = this.props; return ( ); } diff --git a/packages/core/src/components/tabs/tabs.tsx b/packages/core/src/components/tabs/tabs.tsx index 414d473b76..eff110160e 100644 --- a/packages/core/src/components/tabs/tabs.tsx +++ b/packages/core/src/components/tabs/tabs.tsx @@ -16,10 +16,9 @@ import classNames from "classnames"; import * as React from "react"; +import { polyfill } from "react-lifecycles-compat"; -import { AbstractPureComponent } from "../../common/abstractPureComponent"; -import * as Classes from "../../common/classes"; -import * as Keys from "../../common/keys"; +import { AbstractPureComponent2, Classes, Keys } from "../../common"; import { DISPLAYNAME_PREFIX, IProps } from "../../common/props"; import * as Utils from "../../common/utils"; @@ -92,7 +91,8 @@ export interface ITabsState { selectedTabId?: TabId; } -export class Tabs extends AbstractPureComponent { +@polyfill +export class Tabs extends AbstractPureComponent2 { /** Insert a `Tabs.Expander` between any two children to right-align all subsequent children. */ public static Expander = Expander; @@ -107,6 +107,14 @@ export class Tabs extends AbstractPureComponent { public static displayName = `${DISPLAYNAME_PREFIX}.Tabs`; + public static getDerivedStateFromProps({ selectedTabId }: ITabsProps) { + if (selectedTabId !== undefined) { + // keep state in sync with controlled prop, so state is canonical source of truth + return { selectedTabId }; + } + return null; + } + private tablistElement: HTMLDivElement; private refHandlers = { tablist: (tabElement: HTMLDivElement) => (this.tablistElement = tabElement), @@ -159,13 +167,6 @@ export class Tabs extends AbstractPureComponent { this.moveSelectionIndicator(); } - public componentWillReceiveProps({ selectedTabId }: ITabsProps) { - if (selectedTabId !== undefined) { - // keep state in sync with controlled prop, so state is canonical source of truth - this.setState({ selectedTabId }); - } - } - public componentDidUpdate(prevProps: ITabsProps, prevState: ITabsState) { if (this.state.selectedTabId !== prevState.selectedTabId) { this.moveSelectionIndicator(); diff --git a/packages/core/src/components/tag-input/_tag-input.scss b/packages/core/src/components/tag-input/_tag-input.scss index 0503e5cf1a..5f9e61c286 100644 --- a/packages/core/src/components/tag-input/_tag-input.scss +++ b/packages/core/src/components/tag-input/_tag-input.scss @@ -36,6 +36,8 @@ $tag-input-icon-padding-large: ($pt-input-height-large - $pt-icon-size-large) / align-self: stretch; margin-top: $tag-input-padding; margin-right: $tag-input-icon-padding; + // allow tags to ellipse and not overflow the container + min-width: 0; // use the larger, conventional input padding when there are no tags and no left icon present. // see: https://github.com/palantir/blueprint/issues/2872 @@ -111,6 +113,12 @@ $tag-input-icon-padding-large: ($pt-input-height-large - $pt-icon-size-large) / &.#{$ns}-active { box-shadow: input-transition-shadow($input-shadow-color-focus, true), $input-box-shadow-focus; background-color: $input-background-color; + + @each $intent, $color in $pt-intent-text-colors { + &.#{$ns}-intent-#{$intent} { + box-shadow: input-transition-shadow($color, true), $input-box-shadow-focus; + } + } } .#{$ns}-dark &, @@ -129,6 +137,12 @@ $tag-input-icon-padding-large: ($pt-input-height-large - $pt-icon-size-large) / box-shadow: dark-input-transition-shadow($dark-input-shadow-color-focus, true), $pt-dark-input-box-shadow; background-color: $dark-input-background-color; + + @each $intent, $color in $pt-intent-text-colors { + &.#{$ns}-intent-#{$intent} { + box-shadow: input-transition-shadow($color, true), $pt-dark-input-box-shadow; + } + } } } } diff --git a/packages/core/src/components/tag-input/tag-input.md b/packages/core/src/components/tag-input/tag-input.md index 5fba52ab93..aae86e4ceb 100644 --- a/packages/core/src/components/tag-input/tag-input.md +++ b/packages/core/src/components/tag-input/tag-input.md @@ -9,8 +9,10 @@ on the container will focus the text input for seamless interaction.

Looking for a dropdown menu?

- [`MultiSelect` in the **@blueprintjs/select** package](#select/multi-select) - composes this component with a dropdopwn menu of suggestions. + +[`MultiSelect` in the **@blueprintjs/select** package](#select/multi-select) +composes this component with a dropdown menu of suggestions. +
@## Props @@ -23,7 +25,8 @@ new items. A `separator` prop is supported to allow multiple items to be added at once; the default splits on commas and newlines. **Tags can be removed** by clicking their -buttons, or by pressing backspace repeatedly. +buttons, or by pressing either backspace or delete repeatedly. +Pressing delete mimics the behavior of deleting in a text editor, where trying to delete at the end of the line will do nothing. Arrow keys can also be used to focus on a particular tag before removing it. The cursor must be at the beginning of the text input for these interactions. @@ -44,16 +47,20 @@ be applied to the input via `inputProps`.

Handling long words

- Set an explicit `width` on the container element to cause long tags to wrap onto multiple lines. - Either supply a specific pixel value, or use `` - to fill its container's width (try this in the example above). + +Set an explicit `width` on the container element to cause long tags to wrap onto multiple lines. +Either supply a specific pixel value, or use `` +to fill its container's width (try this in the example above). +

Disabling a tag input

-

Disabling this component requires setting the `disabled` prop to `true` - and separately disabling the component's `rightElement` as appropriate - (because `TagInput` accepts any `JSX.Element` as its `rightElement`).

+ +Disabling this component requires setting the `disabled` prop to `true` +and separately disabling the component's `rightElement` as appropriate +(because `TagInput` accepts any `JSX.Element` as its `rightElement`). +
@interface ITagInputProps diff --git a/packages/core/src/components/tag-input/tagInput.tsx b/packages/core/src/components/tag-input/tagInput.tsx index 914179a21b..10f3ecd622 100644 --- a/packages/core/src/components/tag-input/tagInput.tsx +++ b/packages/core/src/components/tag-input/tagInput.tsx @@ -16,12 +16,10 @@ import classNames from "classnames"; import * as React from "react"; +import { polyfill } from "react-lifecycles-compat"; -import { AbstractPureComponent } from "../../common/abstractPureComponent"; -import * as Classes from "../../common/classes"; -import * as Keys from "../../common/keys"; +import { AbstractPureComponent2, Classes, Keys, Utils } from "../../common"; import { DISPLAYNAME_PREFIX, HTMLInputProps, IIntentProps, IProps, MaybeElement } from "../../common/props"; -import * as Utils from "../../common/utils"; import { Icon, IconName } from "../icon/icon"; import { ITagProps, Tag } from "../tag/tag"; @@ -192,12 +190,14 @@ export interface ITagInputState { activeIndex: number; inputValue: string; isInputFocused: boolean; + prevInputValueProp?: string; } /** special value for absence of active tag */ const NONE = -1; -export class TagInput extends AbstractPureComponent { +@polyfill +export class TagInput extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.TagInput`; public static defaultProps: Partial & object = { @@ -208,6 +208,19 @@ export class TagInput extends AbstractPureComponent, + state: Readonly, + ): Partial | null { + if (props.inputValue !== state.prevInputValueProp) { + return { + inputValue: props.inputValue, + prevInputValueProp: props.inputValue, + }; + } + return null; + } + public state: ITagInputState = { activeIndex: NONE, inputValue: this.props.inputValue || "", @@ -222,14 +235,6 @@ export class TagInput extends AbstractPureComponent) { + const { activeIndex } = this.state; + if (this.isValidIndex(activeIndex)) { + event.stopPropagation(); + this.removeIndexFromValues(activeIndex); + } + } + /** Remove the item at the given index by invoking `onRemove` and `onChange` accordingly. */ private removeIndexFromValues(index: number) { const { onChange, onRemove, values } = this.props; diff --git a/packages/core/src/components/tag/tag.tsx b/packages/core/src/components/tag/tag.tsx index 231e315ca8..60fe9f6133 100644 --- a/packages/core/src/components/tag/tag.tsx +++ b/packages/core/src/components/tag/tag.tsx @@ -16,8 +16,16 @@ import classNames from "classnames"; import * as React from "react"; - -import { Classes, DISPLAYNAME_PREFIX, IIntentProps, IProps, MaybeElement, Utils } from "../../common"; +import { polyfill } from "react-lifecycles-compat"; +import { + AbstractPureComponent2, + Classes, + DISPLAYNAME_PREFIX, + IIntentProps, + IProps, + MaybeElement, + Utils, +} from "../../common"; import { isReactNodeEmpty } from "../../common/utils"; import { Icon, IconName } from "../icon/icon"; import { Text } from "../text/text"; @@ -91,7 +99,8 @@ export interface ITagProps extends IProps, IIntentProps, React.HTMLAttributes { +@polyfill +export class Tag extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.Tag`; public render() { diff --git a/packages/core/src/components/text/text.tsx b/packages/core/src/components/text/text.tsx index d25d691684..00d5f6df8d 100644 --- a/packages/core/src/components/text/text.tsx +++ b/packages/core/src/components/text/text.tsx @@ -16,8 +16,8 @@ import classNames from "classnames"; import * as React from "react"; - -import * as Classes from "../../common/classes"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes } from "../../common"; import { DISPLAYNAME_PREFIX, IProps } from "../../common/props"; export interface ITextProps extends IProps { @@ -40,7 +40,8 @@ export interface ITextState { isContentOverflowing: boolean; } -export class Text extends React.PureComponent { +@polyfill +export class Text extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.Text`; public state: ITextState = { @@ -65,15 +66,16 @@ export class Text extends React.PureComponent { }, this.props.className, ); - const { tagName: TagName = "div" } = this.props; - return ( - (this.textRef = ref)} - title={this.state.isContentOverflowing ? this.state.textContent : undefined} - > - {this.props.children} - + const { children, tagName = "div" } = this.props; + + return React.createElement( + tagName, + { + className: classes, + ref: (ref: HTMLElement | null) => (this.textRef = ref), + title: this.state.isContentOverflowing ? this.state.textContent : undefined, + }, + children, ); } diff --git a/packages/core/src/components/toast/_toast.scss b/packages/core/src/components/toast/_toast.scss index 1e99273ea2..b188ad537b 100644 --- a/packages/core/src/components/toast/_toast.scss +++ b/packages/core/src/components/toast/_toast.scss @@ -144,6 +144,7 @@ $toast-margin: $pt-grid-size * 2 !default; .#{$ns}-toast-message { flex: 1 1 auto; padding: centered-text($toast-height); + word-break: break-word; } .#{$ns}-toast-container { @@ -172,8 +173,6 @@ $toast-margin: $pt-grid-size * 2 !default; &.#{$ns}-toast-container-top { top: 0; - // clear opposite side cuz .#{$ns}-overlay has all sides 0'ed - bottom: auto; } &.#{$ns}-toast-container-bottom { @@ -195,6 +194,9 @@ $toast-margin: $pt-grid-size * 2 !default; // minimal diff in react-transition styles so we can avoid calling those mixins again &.#{$ns}-toast-enter:not(.#{$ns}-toast-enter-active), &.#{$ns}-toast-enter:not(.#{$ns}-toast-enter-active) ~ .#{$ns}-toast, + &.#{$ns}-toast-appear:not(.#{$ns}-toast-appear-active), + &.#{$ns}-toast-appear:not(.#{$ns}-toast-appear-active) ~ .#{$ns}-toast, + &.#{$ns}-toast-exit-active ~ .#{$ns}-toast, &.#{$ns}-toast-leave-active ~ .#{$ns}-toast { transform: translateY($toast-margin + $toast-height); } diff --git a/packages/core/src/components/toast/toast.md b/packages/core/src/components/toast/toast.md index 75a1dccece..5ff883c6ba 100644 --- a/packages/core/src/components/toast/toast.md +++ b/packages/core/src/components/toast/toast.md @@ -36,16 +36,20 @@ There are three ways to use the `Toaster` component:

Working with multiple toasters

- You can have multiple toasters in a single application, but you must ensure that each has a unique - `position` to prevent overlap. + +You can have multiple toasters in a single application, but you must ensure that each has a unique +`position` to prevent overlap. +

Toaster focus

- `Toaster` always disables `Overlay`'s `enforceFocus` behavior (meaning that you're not blocked - from accessing other parts of the application while a toast is active), and by default also - disables `autoFocus` (meaning that focus will not switch to a toast when it appears). You can - enable `autoFocus` for an individual `Toaster` via a prop, if desired. + +`Toaster` always disables `Overlay`'s `enforceFocus` behavior (meaning that you're not blocked +from accessing other parts of the application while a toast is active), and by default also +disables `autoFocus` (meaning that focus will not switch to a toast when it appears). You can +enable `autoFocus` for an individual `Toaster` via a prop, if desired. +
@@ -70,8 +74,10 @@ because the `Toaster` should not be treated as a normal React component.

React 16 usage

- `Toaster.create()` will throw an error if invoked inside a component lifecycle method in React 16, as `ReactDOM.render()` will return - `null` resulting in an inaccessible toaster instance. See the second bullet point on the [React 16 release notes](https://reactjs.org/blog/2017/09/26/react-v16.0.html#breaking-changes) for more information. + +`Toaster.create()` will throw an error if invoked inside a component lifecycle method in React 16, as `ReactDOM.render()` will return +`null` resulting in an inaccessible toaster instance. See the second bullet point on the [React 16 release notes](https://reactjs.org/blog/2017/09/26/react-v16.0.html#breaking-changes) for more information. +
@interface IToaster @@ -140,7 +146,7 @@ class MyComponent extends React.PureComponent { {/* "Toasted!" will appear here after clicking button. */} {this.state.toasts.map(toast => )} -
) } diff --git a/packages/core/src/components/toast/toast.tsx b/packages/core/src/components/toast/toast.tsx index ea23683112..f1783ecc21 100644 --- a/packages/core/src/components/toast/toast.tsx +++ b/packages/core/src/components/toast/toast.tsx @@ -16,9 +16,8 @@ import classNames from "classnames"; import * as React from "react"; - -import { AbstractPureComponent } from "../../common/abstractPureComponent"; -import * as Classes from "../../common/classes"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes } from "../../common"; import { DISPLAYNAME_PREFIX, IActionProps, IIntentProps, ILinkProps, IProps, MaybeElement } from "../../common/props"; import { safeInvoke } from "../../common/utils"; import { ButtonGroup } from "../button/buttonGroup"; @@ -54,7 +53,8 @@ export interface IToastProps extends IProps, IIntentProps { timeout?: number; } -export class Toast extends AbstractPureComponent { +@polyfill +export class Toast extends AbstractPureComponent2 { public static defaultProps: IToastProps = { className: "", message: "", diff --git a/packages/core/src/components/toast/toaster.tsx b/packages/core/src/components/toast/toaster.tsx index 39a0257142..b63fec5a87 100644 --- a/packages/core/src/components/toast/toaster.tsx +++ b/packages/core/src/components/toast/toaster.tsx @@ -17,12 +17,10 @@ import classNames from "classnames"; import * as React from "react"; import * as ReactDOM from "react-dom"; - -import { AbstractPureComponent } from "../../common/abstractPureComponent"; -import * as Classes from "../../common/classes"; -import { TOASTER_CREATE_NULL, TOASTER_WARN_INLINE } from "../../common/errors"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes, Position } from "../../common"; +import { TOASTER_CREATE_NULL, TOASTER_MAX_TOASTS_INVALID, TOASTER_WARN_INLINE } from "../../common/errors"; import { ESCAPE } from "../../common/keys"; -import { Position } from "../../common/position"; import { DISPLAYNAME_PREFIX, IProps } from "../../common/props"; import { isNodeEnv, safeInvoke } from "../../common/utils"; import { Overlay } from "../overlay/overlay"; @@ -87,19 +85,25 @@ export interface IToasterProps extends IProps { /** * Position of `Toaster` within its container. - * - * Note that only `TOP` and `BOTTOM` are supported because Toaster only - * supports the top and bottom edge positioning. * @default Position.TOP */ position?: ToasterPosition; + + /** + * The maximum number of active toasts that can be displayed at once. + * + * When the limit is about to be exceeded, the oldest active toast is removed. + * @default undefined + */ + maxToasts?: number; } export interface IToasterState { toasts: IToastOptions[]; } -export class Toaster extends AbstractPureComponent implements IToaster { +@polyfill +export class Toaster extends AbstractPureComponent2 implements IToaster { public static displayName = `${DISPLAYNAME_PREFIX}.Toaster`; public static defaultProps: IToasterProps = { @@ -137,6 +141,10 @@ export class Toaster extends AbstractPureComponent private toastId = 0; public show(props: IToastProps, key?: string) { + if (this.props.maxToasts) { + // check if active number of toasts are at the maxToasts limit + this.dismissIfAtLimit(); + } const options = this.createToastOptions(props, key); if (key === undefined || this.isNewToastKey(key)) { this.setState(prevState => ({ @@ -194,10 +202,24 @@ export class Toaster extends AbstractPureComponent ); } + protected validateProps(props: IToasterProps) { + // maximum number of toasts should not be a number less than 1 + if (props.maxToasts < 1) { + throw new Error(TOASTER_MAX_TOASTS_INVALID); + } + } + private isNewToastKey(key: string) { return this.state.toasts.every(toast => toast.key !== key); } + private dismissIfAtLimit() { + if (this.state.toasts.length === this.props.maxToasts) { + // dismiss the oldest toast to stay within the maxToasts limit + this.dismiss(this.state.toasts[this.state.toasts.length - 1].key); + } + } + private renderToast(toast: IToastOptions) { return ; } diff --git a/packages/core/src/components/tooltip/tooltip.md b/packages/core/src/components/tooltip/tooltip.md index 2c2a1251a6..ce1120dcd0 100644 --- a/packages/core/src/components/tooltip/tooltip.md +++ b/packages/core/src/components/tooltip/tooltip.md @@ -1,6 +1,6 @@ @# Tooltip -A tooltip is a lightweight popover for showing additional infromation on hover. +A tooltip is a lightweight popover for showing additional information during hover interactions. @reactExample TooltipExample @@ -31,16 +31,20 @@ prop is not supported. When creating a tooltip, you must specify both: - its _content_ via the `content` prop, and -- its _target_ as a single child element or string. +- its _target_ as either: + - a single child element, or + - an instrinsic element string identifier (N.B. this doesn't work if you are using any of the target props, so use an element instead, i.e. `
...
` instead of `"div"`). The content will appear in a contrasting popover when the target is hovered.

Button targets

- Buttons make great tooltip targets, but the `disabled` attribute will prevent all - events so the enclosing `Tooltip` will not know when to respond. - Use [`AnchorButton`](#core/components/button.anchor-button) instead; - see the [callout here](#core/components/button.props) for more details. + +Buttons make great tooltip targets, but the `disabled` attribute will prevent all +events so the enclosing `Tooltip` will not know when to respond. +Use [`AnchorButton`](#core/components/button.anchor-button) instead; +see the [callout here](#core/components/button.props) for more details. +
@interface ITooltipProps diff --git a/packages/core/src/components/tooltip/tooltip.tsx b/packages/core/src/components/tooltip/tooltip.tsx index 2b7181924b..ca1494123b 100644 --- a/packages/core/src/components/tooltip/tooltip.tsx +++ b/packages/core/src/components/tooltip/tooltip.tsx @@ -16,8 +16,8 @@ import classNames from "classnames"; import * as React from "react"; - -import * as Classes from "../../common/classes"; +import { polyfill } from "react-lifecycles-compat"; +import { AbstractPureComponent2, Classes } from "../../common"; import { DISPLAYNAME_PREFIX, IIntentProps } from "../../common/props"; import { Popover, PopoverInteractionKind } from "../popover/popover"; import { IPopoverSharedProps } from "../popover/popoverSharedProps"; @@ -62,7 +62,8 @@ export interface ITooltipProps extends IPopoverSharedProps, IIntentProps { transitionDuration?: number; } -export class Tooltip extends React.PureComponent { +@polyfill +export class Tooltip extends AbstractPureComponent2 { public static displayName = `${DISPLAYNAME_PREFIX}.Tooltip`; public static defaultProps: Partial = { diff --git a/packages/core/src/components/tree/_tree.scss b/packages/core/src/components/tree/_tree.scss index 20d55767de..fcb8b7309a 100644 --- a/packages/core/src/components/tree/_tree.scss +++ b/packages/core/src/components/tree/_tree.scss @@ -179,6 +179,12 @@ $tree-icon-spacing: ($tree-row-height - $pt-icon-size-standard) / 2 !default; .#{$ns}-tree { #{$icon-classes} { color: $pt-dark-icon-color; + + @each $intent, $colors in $pt-intent-colors { + &.#{$ns}-intent-#{$intent} { + @include intent-color($intent); + } + } } } diff --git a/packages/core/src/components/tree/tree.md b/packages/core/src/components/tree/tree.md index 1d635fbf55..df2190a0ac 100644 --- a/packages/core/src/components/tree/tree.md +++ b/packages/core/src/components/tree/tree.md @@ -35,8 +35,10 @@ whether the node's children are shown. @## CSS
- Note that the following examples set a maximum width and background color for the tree; - you may want to do this as well in your own usage. + +Note that the following examples set a maximum width and background color for the tree; +you may want to do this as well in your own usage. +
@css tree diff --git a/packages/core/src/docs/classes.md b/packages/core/src/docs/classes.md index 4e24102cf7..3dab1c2f9f 100644 --- a/packages/core/src/docs/classes.md +++ b/packages/core/src/docs/classes.md @@ -96,4 +96,14 @@ requires several things: @## Linting -The [**@blueprintjs/tslint-config**](https://www.npmjs.com/package/@blueprintjs/tslint-config) NPM package provides advanced configuration for [TSLint](http://palantir.github.io/tslint/), including a custom `blueprint-classes-constants` rule that will detect and warn about hardcoded `pt-`prefixed strings. See the package's [README](https://www.npmjs.com/package/@blueprintjs/tslint-config) for usage instructions. +The [**@blueprintjs/eslint-config**](https://www.npmjs.com/package/@blueprintjs/eslint-config) +NPM package provides advanced configuration for [ESLint](https://eslint.org/). Blueprint is +currently transitioning from [TSLint](https://palantir.github.io/tslint/) to ESLint, and as +such, rules are being migrated from TSLint to ESLint. In the meantime, some TSLint rules are +being run using ESLint. + +The [**@blueprintjs/eslint-plugin-blueprint**](https://www.npmjs.com/package/@blueprintjs/eslint-plugin-blueprint) +NPM package includes a custom `blueprint-html-components` rule that will warn on usages of +JSX intrinsic elements (`

`) that have a Blueprint alternative (`

`). See +the package's [README](https://www.npmjs.com/package/@blueprintjs/eslint-plugin-blueprint) +for usage instructions. diff --git a/packages/core/test/breadcrumbs/breadcrumbTests.tsx b/packages/core/test/breadcrumbs/breadcrumbTests.tsx index c1b0da5ebc..92bcf753d7 100644 --- a/packages/core/test/breadcrumbs/breadcrumbTests.tsx +++ b/packages/core/test/breadcrumbs/breadcrumbTests.tsx @@ -19,7 +19,7 @@ import { shallow } from "enzyme"; import * as React from "react"; import { spy } from "sinon"; -import { Breadcrumb, Classes } from "../../src/index"; +import { Breadcrumb, Classes, Icon } from "../../src/index"; describe("Breadcrumb", () => { it("renders its contents", () => { @@ -50,4 +50,9 @@ describe("Breadcrumb", () => { assert.lengthOf(shallow().find("a"), 0); assert.lengthOf(shallow().find("span"), 1); }); + + it("renders an icon if one is provided", () => { + assert.lengthOf(shallow().find(Icon), 0); + assert.lengthOf(shallow().find(Icon), 1); + }); }); diff --git a/packages/core/test/common/utils/compareUtilsTests.ts b/packages/core/test/common/utils/compareUtilsTests.ts index 70d3ae0f8d..0546fa57bf 100644 --- a/packages/core/test/common/utils/compareUtilsTests.ts +++ b/packages/core/test/common/utils/compareUtilsTests.ts @@ -269,60 +269,6 @@ describe("CompareUtils", () => { }); }); - describe("getShallowUnequalKeyValues", () => { - describe("with `keys` defined as whitelist", () => { - describe("returns empty array if the specified values are shallowly equal", () => { - runTest([], { a: 1, b: [1, 2, 3], c: "3" }, { b: [1, 2, 3], a: 1, c: "3" }, wl(["a", "c"])); - }); - - describe("returns unequal key/values if any specified values are not shallowly equal", () => { - // identical objects, but different instances - runTest( - [{ key: "a", valueA: [1, "2", true], valueB: [1, "2", true] }], - { a: [1, "2", true] }, - { a: [1, "2", true] }, - wl(["a"]), - ); - // different primitive-type values - runTest([{ key: "a", valueA: 1, valueB: 2 }], { a: 1 }, { a: 2 }, wl(["a"])); - }); - }); - - describe("with `keys` defined as blacklist", () => { - describe("returns empty array if the specified values are shallowly equal", () => { - runTest([], { a: 1, b: [1, 2, 3], c: "3" }, { b: [1, 2, 3], a: 1, c: "3" }, bl(["b"])); - }); - - describe("returns unequal keys/values if any specified values are not shallowly equal", () => { - runTest( - [{ key: "a", valueA: [1, "2", true], valueB: [1, "2", true] }], - { a: [1, "2", true] }, - { a: [1, "2", true] }, - bl(["b", "c"]), - ); - runTest([{ key: "a", valueA: 1, valueB: 2 }], { a: 1 }, { a: 2 }, bl(["b"])); - }); - }); - - describe("with `keys` not defined", () => { - describe("returns empty array if values are shallowly equal", () => { - runTest([], { a: 1, b: "2", c: true }, { a: 1, b: "2", c: true }); - runTest([], undefined, undefined); - runTest([], null, undefined); - }); - - describe("returns unequal key/values if any specified values are not shallowly equal", () => { - runTest([{ key: "a", valueA: 1, valueB: 2 }], { a: 1 }, { a: 2 }); - }); - }); - - function runTest(expectedResult: any[], a: any, b: any, keys?: IKeyBlacklist | IKeyWhitelist) { - it(getCompareTestDescription(a, b, keys), () => { - expect(CompareUtils.getShallowUnequalKeyValues(a, b, keys)).to.deep.equal(expectedResult); - }); - } - }); - describe("getDeepUnequalKeyValues", () => { describe("with `keys` defined", () => { describe("returns empty array if only the specified values are deeply equal", () => { @@ -331,7 +277,10 @@ describe("CompareUtils", () => { describe("returns unequal key/values if any specified values are not deeply equal", () => { runTest( - [{ key: "a", valueA: 2, valueB: 1 }, { key: "b", valueA: [2, 3, 4], valueB: [1, 2, 3] }], + [ + { key: "a", valueA: 2, valueB: 1 }, + { key: "b", valueA: [2, 3, 4], valueB: [1, 2, 3] }, + ], { a: 2, b: [2, 3, 4], c: "3" }, { b: [1, 2, 3], a: 1, c: "3" }, ["a", "b"], diff --git a/packages/core/test/context-menu/contextMenuTests.tsx b/packages/core/test/context-menu/contextMenuTests.tsx index afb383dd49..d5dde1dde8 100644 --- a/packages/core/test/context-menu/contextMenuTests.tsx +++ b/packages/core/test/context-menu/contextMenuTests.tsx @@ -67,7 +67,7 @@ describe("ContextMenu", () => { setTimeout(() => { assert.isTrue(document.querySelector(`.${Classes.CONTEXT_MENU}`) == null); done(); - }, 110); + }, 200); }); it("does not invoke previous onClose callback", () => { diff --git a/packages/core/test/controls/numericInputTests.tsx b/packages/core/test/controls/numericInputTests.tsx index 08d5307a7e..d0ff191d3b 100644 --- a/packages/core/test/controls/numericInputTests.tsx +++ b/packages/core/test/controls/numericInputTests.tsx @@ -24,7 +24,7 @@ import { import * as React from "react"; import { spy } from "sinon"; -import { expectPropValidationError } from "@blueprintjs/test-commons"; +import { dispatchMouseEvent, expectPropValidationError } from "@blueprintjs/test-commons"; import * as Errors from "../../src/common/errors"; import { @@ -153,9 +153,9 @@ describe("", () => { it("provides numeric value to onValueChange as a number and a string", () => { const onValueChangeSpy = spy(); - const component = mount(); + const component = mount(); - component.find("input").simulate("change"); + component.setState({ value: "1" }); expect(onValueChangeSpy.calledOnce).to.be.true; expect(onValueChangeSpy.calledWith(1, "1")).to.be.true; @@ -163,9 +163,9 @@ describe("", () => { it("provides non-numeric value to onValueChange as NaN and a string", () => { const onValueChangeSpy = spy(); - const component = mount(); + const component = mount(); - component.find("input").simulate("change"); + component.setState({ value: "non-numeric-value" }); expect(onValueChangeSpy.calledOnce).to.be.true; expect(onValueChangeSpy.calledWith(NaN, "non-numeric-value")).to.be.true; @@ -193,15 +193,19 @@ describe("", () => { expect(component.state().value).to.equal("1 + 1"); }); - it("fires onValueChange with the number value and the string value when the value changes", () => { + it("fires onValueChange with the number value, string value, and input element when the value changes", () => { const onValueChangeSpy = spy(); const component = mount(); const incrementButton = component.find(Button).first(); incrementButton.simulate("mousedown"); + dispatchMouseEvent(document, "mouseup"); - expect(onValueChangeSpy.calledOnce).to.be.true; - expect(onValueChangeSpy.firstCall.args).to.deep.equal([1, "1"]); + const inputElement = component + .find("input") + .first() + .getDOMNode(); + expect(onValueChangeSpy.calledOnceWithExactly(1, "1", inputElement)).to.be.true; }); it("fires onButtonClick with the number value and the string value when either button is pressed", () => { @@ -213,6 +217,8 @@ describe("", () => { // incrementing from 0 incrementButton.simulate("mousedown"); + dispatchMouseEvent(document, "mouseup"); + expect(onButtonClickSpy.calledOnce).to.be.true; expect(onButtonClickSpy.firstCall.args).to.deep.equal([1, "1"]); onButtonClickSpy.resetHistory(); @@ -611,8 +617,12 @@ describe("", () => { const newValue = component.state().value; expect(newValue).to.equal("0"); - expect(onValueChangeSpy.calledOnce).to.be.true; - expect(onValueChangeSpy.firstCall.args).to.deep.equal([0, "0"]); + + const inputElement = component + .find("input") + .first() + .getDOMNode(); + expect(onValueChangeSpy.calledOnceWithExactly(0, "0", inputElement)).to.be.true; }); it("does not fire onValueChange if nextProps.min < value", () => { @@ -684,8 +694,12 @@ describe("", () => { const newValue = component.state().value; expect(newValue).to.equal("0"); - expect(onValueChangeSpy.calledOnce).to.be.true; - expect(onValueChangeSpy.firstCall.args).to.deep.equal([0, "0"]); + + const inputElement = component + .find("input") + .first() + .getDOMNode(); + expect(onValueChangeSpy.calledOnceWithExactly(0, "0", inputElement)).to.be.true; }); it("does not fire onValueChange if nextProps.max > value", () => { @@ -714,8 +728,12 @@ describe("", () => { .simulate("mousedown") .simulate("mousedown"); expect(component.state().value).to.equal("2"); - expect(onValueChangeSpy.callCount).to.equal(5); - expect(onValueChangeSpy.args[0]).to.deep.equal([2, "2"]); + + const inputElement = component + .find("input") + .first() + .getDOMNode(); + expect(onValueChangeSpy.calledOnceWithExactly(2, "2", inputElement)).to.be.true; }); }); @@ -839,6 +857,15 @@ describe("", () => { }); }); + describe("Controlled mode", () => { + it("value prop updates do not trigger onValueChange", () => { + const onValueChangeSpy = spy(); + const component = mount(); + component.setProps({ value: 1 }); + expect(onValueChangeSpy.notCalled).to.be.true; + }); + }); + describe("Other", () => { it("disables the input field and buttons when disabled is true", () => { const component = mount(); @@ -884,6 +911,16 @@ describe("", () => { expect(component.find(InputGroup).find(Button)).to.exist; }); + it("passed decimal value should be rounded by stepSize", () => { + const component = mount(); + expect(component.find("input").prop("value")).to.equal("9"); + }); + + it("passed decimal value should be rounded by minorStepSize", () => { + const component = mount(); + expect(component.find("input").prop("value")).to.equal("9.01"); + }); + it("changes max precision of displayed value to that of the smallest step size defined", () => { const component = mount(); const incrementButton = component.find(Button).first(); diff --git a/packages/core/test/editable-text/editableTextTests.tsx b/packages/core/test/editable-text/editableTextTests.tsx index 7eb16ad4b0..aa2f659283 100644 --- a/packages/core/test/editable-text/editableTextTests.tsx +++ b/packages/core/test/editable-text/editableTextTests.tsx @@ -40,6 +40,13 @@ describe("", () => { assert.isFalse(editable.state("isEditing")); }); + it("allows resetting controlled value to undefined or null", () => { + const editable = shallow(); + assert.strictEqual(editable.text(), "alphabet"); + editable.setProps({ value: null }); + assert.strictEqual(editable.text(), "placeholder"); + }); + describe("when editing", () => { it('renders when editing', () => { const input = shallow().find("input"); diff --git a/packages/core/test/forms/textAreaTests.tsx b/packages/core/test/forms/textAreaTests.tsx index 83084f619b..3a643533f3 100644 --- a/packages/core/test/forms/textAreaTests.tsx +++ b/packages/core/test/forms/textAreaTests.tsx @@ -51,4 +51,18 @@ describe("