mirror of
https://github.com/cypress-io/cypress.git
synced 2026-05-07 15:31:30 -05:00
Merge branch 'develop' into b45fd0e8a-master-into-develop
This commit is contained in:
+4
-4
@@ -290,10 +290,7 @@ temp/
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
# Thumbnails
|
||||
#Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
@@ -340,3 +337,6 @@ $RECYCLE.BIN/
|
||||
|
||||
/npm/react/bin/*
|
||||
# End of https://www.gitignore.io/api/osx,git,node,windows,intellij,linux
|
||||
|
||||
# Circle cache artifacts
|
||||
globbed_node_modules
|
||||
|
||||
@@ -126,7 +126,10 @@ In the following instructions, "X.Y.Z" is used to denote the version of Cypress
|
||||
```
|
||||
- Test the new version of Cypress against the Cypress dashboard repo.
|
||||
|
||||
7. Deploy the release-specific documentation and changelog in [cypress-documentation](https://github.com/cypress-io/cypress-documentation).
|
||||
|
||||
7. Confirm that every issue labeled [stage: pending release](https://github.com/cypress-io/cypress/issues?q=label%3A%22stage%3A+pending+release%22+is%3Aclosed) has a ZenHub release set. **Tip:** there is a command in [`release-automations`](https://github.com/cypress-io/release-automations)'s `issues-in-release` tool to list and check such issues. Without a ZenHub release issues will not be included in the right changelog.
|
||||
|
||||
8. Deploy the release-specific documentation and changelog in [cypress-documentation](https://github.com/cypress-io/cypress-documentation).
|
||||
- If there is not already a release-specific PR open, create one. You can use [`release-automations`](https://github.com/cypress-io/release-automations)'s `issues-in-release` tool to generate a starting point for the changelog, based off of ZenHub:
|
||||
```
|
||||
cd packages/issues-in-release
|
||||
@@ -136,26 +139,26 @@ In the following instructions, "X.Y.Z" is used to denote the version of Cypress
|
||||
- Merge any release-specific documentation changes into the main release PR.
|
||||
- Merging this PR into `develop` will deploy to `docs-staging` and then a PR will be automatically created against `master`. It will be automatically merged after it passes and will deploy to production.
|
||||
|
||||
8. Make the new npm version the "latest" version by updating the dist-tag `latest` to point to the new version:
|
||||
9. Make the new npm version the "latest" version by updating the dist-tag `latest` to point to the new version:
|
||||
```shell
|
||||
npm dist-tag add cypress@X.Y.Z
|
||||
```
|
||||
|
||||
9. Run `binary-release` to update the [download server's manifest](https://download.cypress.io/desktop.json):
|
||||
10. Run `binary-release` to update the [download server's manifest](https://download.cypress.io/desktop.json):
|
||||
```shell
|
||||
yarn run binary-release --version X.Y.Z
|
||||
```
|
||||
|
||||
10. If needed, push out any updated changes to the links manifest to [`on.cypress.io`](https://github.com/cypress-io/cypress-services/tree/develop/packages/on).
|
||||
11. If needed, push out any updated changes to the links manifest to [`on.cypress.io`](https://github.com/cypress-io/cypress-services/tree/develop/packages/on).
|
||||
|
||||
11. If needed, deploy the updated [`cypress-example-kitchensink`][cypress-example-kitchensink] to `example.cypress.io` by following [these instructions under "Deployment"](./packages/example/README.md).
|
||||
12. If needed, deploy the updated [`cypress-example-kitchensink`][cypress-example-kitchensink] to `example.cypress.io` by following [these instructions under "Deployment"](./packages/example/README.md).
|
||||
|
||||
12. Update the releases in [ZenHub](https://app.zenhub.com/workspaces/test-runner-5c3ea3baeb1e75374f7b0708/reports/release):
|
||||
13. Update the releases in [ZenHub](https://app.zenhub.com/workspaces/test-runner-5c3ea3baeb1e75374f7b0708/reports/release):
|
||||
- Close the current release in ZenHub.
|
||||
- Create a new patch release (and a new minor release, if this is a minor release) in ZenHub, and schedule them both to be completed 2 weeks from the current date.
|
||||
- Move all issues that are still open from the current release to the appropriate future release.
|
||||
|
||||
13. Bump `version` in [`package.json`](package.json), commit it to `develop`, tag it with the version, and push the tag up:
|
||||
14. Bump `version` in [`package.json`](package.json), commit it to `develop`, tag it with the version, and push the tag up:
|
||||
```shell
|
||||
git commit -am "release X.Y.Z [skip ci]"
|
||||
git log --pretty=oneline
|
||||
@@ -163,7 +166,7 @@ In the following instructions, "X.Y.Z" is used to denote the version of Cypress
|
||||
git tag -a vX.Y.Z <sha>
|
||||
git push origin vX.Y.Z
|
||||
```
|
||||
14. Merge `develop` into `master` and push both branches up. Note: pushing to `master` will automatically publish any independent npm packages that have not yet been published.
|
||||
15. Merge `develop` into `master` and push both branches up. Note: pushing to `master` will automatically publish any independent npm packages that have not yet been published.
|
||||
```shell
|
||||
git push origin develop
|
||||
git checkout master
|
||||
@@ -171,7 +174,7 @@ In the following instructions, "X.Y.Z" is used to denote the version of Cypress
|
||||
git push origin master
|
||||
```
|
||||
|
||||
15. Inside of [cypress-io/release-automations][release-automations]:
|
||||
16. Inside of [cypress-io/release-automations][release-automations]:
|
||||
- Publish GitHub release to [cypress-io/cypress/releases](https://github.com/cypress-io/cypress/releases) using package `set-releases`:
|
||||
```shell
|
||||
cd packages/set-releases && npm run release-log -- --version X.Y.Z
|
||||
@@ -180,10 +183,11 @@ In the following instructions, "X.Y.Z" is used to denote the version of Cypress
|
||||
```shell
|
||||
cd packages/issues-in-release && npm run do:comment -- --release X.Y.Z
|
||||
```
|
||||
- Confirm there are no issues with the label [stage: pending release](https://github.com/cypress-io/cypress/issues?q=label%3A%22stage%3A+pending+release%22+is%3Aclosed) left
|
||||
|
||||
16. Publish a new docker image in [`cypress-docker-images`](https://github.com/cypress-io/cypress-docker-images) under `included` for the new cypress version.
|
||||
17. Publish a new docker image in [`cypress-docker-images`](https://github.com/cypress-io/cypress-docker-images) under `included` for the new cypress version. Note: we use the base image with the Node version matching the bundled Node version.
|
||||
|
||||
17. Update example projects to the new version. For most projects, you can go to the Renovate dependency issue and check the box next to `Update dependency cypress to X.Y.Z`. It will automatically create a PR. Once it passes, you can merge it. Try updating at least the following projects:
|
||||
18. Update example projects to the new version. For most projects, you can go to the Renovate dependency issue and check the box next to `Update dependency cypress to X.Y.Z`. It will automatically create a PR. Once it passes, you can merge it. Try updating at least the following projects:
|
||||
- [cypress-example-todomvc](https://github.com/cypress-io/cypress-example-todomvc/issues/99)
|
||||
- [cypress-example-todomvc-redux](https://github.com/cypress-io/cypress-example-todomvc-redux/issues/1)
|
||||
- [cypress-example-realworld](https://github.com/cypress-io/cypress-example-realworld/issues/2)
|
||||
@@ -195,7 +199,7 @@ In the following instructions, "X.Y.Z" is used to denote the version of Cypress
|
||||
- [cypress-documentation](https://github.com/cypress-io/cypress-documentation/issues/1313)
|
||||
- [cypress-example-docker-compose](https://github.com/cypress-io/cypress-example-docker-compose) - Doesn't have a Renovate issue, but will auto-create and auto-merge non-major Cypress updates as long as the tests pass.
|
||||
|
||||
18. Check if any test or example repositories have a branch for testing the features or fixes from the newly published version `x.y.z`. The branch should also be named `x.y.z`. Check all `cypress-test-*` and `cypress-example-*` repositories, and if there is a branch named `x.y.z`, merge it into `master`.
|
||||
19. Check if any test or example repositories have a branch for testing the features or fixes from the newly published version `x.y.z`. The branch should also be named `x.y.z`. Check all `cypress-test-*` and `cypress-example-*` repositories, and if there is a branch named `x.y.z`, merge it into `master`.
|
||||
|
||||
**Test Repos**
|
||||
|
||||
|
||||
+290
-213
@@ -18,6 +18,13 @@ defaults: &defaults
|
||||
executor:
|
||||
type: executor
|
||||
default: cy-doc
|
||||
is-mac:
|
||||
type: boolean
|
||||
default: false
|
||||
arch:
|
||||
type: enum
|
||||
default: 'linux'
|
||||
enum: ['linux', 'darwin']
|
||||
executor: <<parameters.executor>>
|
||||
environment:
|
||||
## set specific timezone
|
||||
@@ -31,7 +38,7 @@ defaults: &defaults
|
||||
LINES: 24
|
||||
|
||||
# filters and requires for testing binary with Firefox
|
||||
testBinaryFirefox: &testBinaryFirefox
|
||||
onlyMainBranches: &onlyMainBranches
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
@@ -74,6 +81,187 @@ executors:
|
||||
PLATFORM: mac
|
||||
|
||||
commands:
|
||||
restore_workspace_binaries:
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
# make sure we have cypress.zip received
|
||||
- run: ls -l
|
||||
- run: ls -l cypress.zip cypress.tgz
|
||||
- run: node --version
|
||||
- run: npm --version
|
||||
|
||||
restore_cached_workspace:
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- unpack-dependencies
|
||||
|
||||
prepare-modules-cache:
|
||||
parameters:
|
||||
dont-move:
|
||||
type: boolean
|
||||
default: false
|
||||
steps:
|
||||
- run: node scripts/circle-cache.js --action prepare
|
||||
- unless:
|
||||
condition: << parameters.dont-move >>
|
||||
steps:
|
||||
- run:
|
||||
name: Move to /tmp dir for consistent caching across root/non-root users
|
||||
command: |
|
||||
mkdir -p /tmp/node_modules_cache
|
||||
mv ~/cypress/node_modules /tmp/node_modules_cache/root_node_modules
|
||||
mv ~/cypress/cli/node_modules /tmp/node_modules_cache/cli_node_modules
|
||||
mv ~/cypress/globbed_node_modules /tmp/node_modules_cache/globbed_node_modules
|
||||
|
||||
build-and-persist:
|
||||
description: Save entire folder as artifact for other jobs to run without reinstalling
|
||||
steps:
|
||||
- run:
|
||||
name: Build packages
|
||||
command: yarn build
|
||||
- prepare-modules-cache # So we don't throw these in the workspace cache
|
||||
- persist_to_workspace:
|
||||
root: ~/
|
||||
paths:
|
||||
- cypress
|
||||
- .ssh
|
||||
- node_modules # contains the npm i -g modules
|
||||
|
||||
unpack-dependencies:
|
||||
description: 'Unpacks dependencies associated with the current workflow'
|
||||
steps:
|
||||
- run:
|
||||
name: Generate Circle Cache Key
|
||||
command: node scripts/circle-cache.js --action cacheKey > circle_cache_key
|
||||
- restore_cache:
|
||||
key: v{{ .Environment.CACHE_VERSION }}-{{ arch }}-test2-node-modules-cache-{{ checksum "circle_cache_key" }}
|
||||
- run:
|
||||
name: Move node_modules back from /tmp
|
||||
command: |
|
||||
if [[ -d "/tmp/node_modules_cache" ]]; then
|
||||
mv /tmp/node_modules_cache/root_node_modules ~/cypress/node_modules
|
||||
mv /tmp/node_modules_cache/cli_node_modules ~/cypress/cli/node_modules
|
||||
mv /tmp/node_modules_cache/globbed_node_modules ~/cypress/globbed_node_modules
|
||||
rm -rf /tmp/node_modules_cache
|
||||
fi
|
||||
- run:
|
||||
name: Restore all node_modules to proper workspace folders
|
||||
command: node scripts/circle-cache.js --action unpack
|
||||
|
||||
caching-dependency-installer:
|
||||
description: 'Installs & caches the dependencies based on yarn lock & package json dependencies'
|
||||
parameters:
|
||||
is-mac:
|
||||
type: boolean
|
||||
default: false
|
||||
arch:
|
||||
type: enum
|
||||
enum: ['linux', 'darwin']
|
||||
steps:
|
||||
- run:
|
||||
name: Generate Circle Cache Key
|
||||
command: node scripts/circle-cache.js --action cacheKey > circle_cache_key
|
||||
- restore_cache:
|
||||
name: Restore cache state, to check for known modules cache existence
|
||||
key: v{{ .Environment.CACHE_VERSION }}-{{ arch }}-test2-node-modules-cache-state-{{ checksum "circle_cache_key" }}
|
||||
- run:
|
||||
name: Bail if cache exists
|
||||
command: |
|
||||
if [[ -f "node_modules_installed" ]]; then
|
||||
echo "Node modules already cached for dependencies, exiting"
|
||||
circleci-agent step halt
|
||||
fi
|
||||
- run: date +%Y-%U > cache_date
|
||||
- restore_cache:
|
||||
name: Restore weekly yarn cache
|
||||
keys:
|
||||
- v{{ .Environment.CACHE_VERSION }}-{{ arch }}-test2-deps-root-weekly-{{ checksum "cache_date" }}
|
||||
- run:
|
||||
name: Install Node Modules
|
||||
command: |
|
||||
. ./scripts/load-nvm.sh
|
||||
yarn --prefer-offline --frozen-lockfile --cache-folder ~/.yarn-<< parameters.arch >>
|
||||
no_output_timeout: 20m
|
||||
- prepare-modules-cache:
|
||||
dont-move: <<parameters.is-mac>> # we don't move, so we don't hit any issues unpacking symlinks
|
||||
- when:
|
||||
condition: <<parameters.is-mac>> # on mac, we don't move to /tmp since we don't need to worry about different users
|
||||
steps:
|
||||
- save_cache:
|
||||
name: Saving node modules for root, cli, and all globbed workspace packages
|
||||
key: v{{ .Environment.CACHE_VERSION }}-{{ arch }}-test2-node-modules-cache-{{ checksum "circle_cache_key" }}
|
||||
paths:
|
||||
- node_modules
|
||||
- cli/node_modules
|
||||
- globbed_node_modules
|
||||
- unless:
|
||||
condition: <<parameters.is-mac>>
|
||||
steps:
|
||||
- save_cache:
|
||||
name: Saving node modules for root, cli, and all globbed workspace packages
|
||||
key: v{{ .Environment.CACHE_VERSION }}-{{ arch }}-test2-node-modules-cache-{{ checksum "circle_cache_key" }}
|
||||
paths:
|
||||
- /tmp/node_modules_cache
|
||||
- run: touch node_modules_installed
|
||||
- save_cache:
|
||||
name: Saving node-modules cache state key
|
||||
key: v{{ .Environment.CACHE_VERSION }}-{{ arch }}-test2-node-modules-cache-state-{{ checksum "circle_cache_key" }}
|
||||
paths:
|
||||
- node_modules_installed
|
||||
- save_cache:
|
||||
name: Save weekly yarn cache
|
||||
key: v{{ .Environment.CACHE_VERSION }}-{{ arch }}-test2-deps-root-weekly-{{ checksum "cache_date" }}
|
||||
paths:
|
||||
- ~/.yarn-<< parameters.arch >>
|
||||
|
||||
install-build-setup:
|
||||
description: Common commands run when setting up for build or yarn install
|
||||
steps:
|
||||
- run:
|
||||
name: Print working folder
|
||||
command: echo $PWD
|
||||
- run:
|
||||
name: print global yarn cache path
|
||||
command: echo $(yarn global bin)
|
||||
- run:
|
||||
name: print Node version
|
||||
command: |
|
||||
. ./scripts/load-nvm.sh
|
||||
echo "nvm use default"
|
||||
nvm use default
|
||||
node -v
|
||||
- run:
|
||||
name: print yarn version
|
||||
command: yarn -v
|
||||
- run:
|
||||
name: check Node version
|
||||
command: |
|
||||
. ./scripts/load-nvm.sh
|
||||
yarn check-node-version
|
||||
|
||||
## make sure the TERM is set to 'xterm' in node (Linux only)
|
||||
## else colors (and tests) will fail
|
||||
## See the following information
|
||||
## * http://andykdocs.de/development/Docker/Fixing+the+Docker+TERM+variable+issue
|
||||
## * https://unix.stackexchange.com/questions/43945/whats-the-difference-between-various-term-variables
|
||||
- run:
|
||||
name: Check terminal
|
||||
command: |
|
||||
. ./scripts/load-nvm.sh
|
||||
yarn check-terminal
|
||||
- run:
|
||||
name: Stop .only
|
||||
command: |
|
||||
. ./scripts/load-nvm.sh
|
||||
yarn stop-only-all
|
||||
- run:
|
||||
# Deps needed by circle-cache.js, before we "yarn" or unpack cached node_modules
|
||||
name: Cache Helper Deps
|
||||
working_directory: ~/
|
||||
command: npm i globby fs-extra minimist fast-json-stable-stringify
|
||||
|
||||
install-required-node:
|
||||
# https://discuss.circleci.com/t/switch-nodejs-version-on-machine-executor-solved/26675/2
|
||||
description: Install Node version matching .node-version
|
||||
@@ -126,8 +314,7 @@ commands:
|
||||
description: browser shortname to target
|
||||
type: string
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run:
|
||||
environment:
|
||||
CYPRESS_KONFIG_ENV: production
|
||||
@@ -167,8 +354,7 @@ commands:
|
||||
type: boolean
|
||||
default: false
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run:
|
||||
command: |
|
||||
cmd=$([[ <<parameters.percy>> == 'true' ]] && echo 'yarn percy exec --') || true
|
||||
@@ -194,8 +380,7 @@ commands:
|
||||
type: boolean
|
||||
default: false
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run:
|
||||
command: |
|
||||
cmd=$([[ <<parameters.percy>> == 'true' ]] && echo 'yarn percy exec --') || true
|
||||
@@ -223,8 +408,7 @@ commands:
|
||||
description: browser shortname to target
|
||||
type: string
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run:
|
||||
command: yarn workspace @packages/server test ./test/e2e/$(( $CIRCLE_NODE_INDEX ))_*spec* --browser <<parameters.browser>>
|
||||
- verify-mocha-results
|
||||
@@ -273,8 +457,7 @@ commands:
|
||||
description: "Name of the github repo to clone like: cypress-example-kitchensink"
|
||||
type: string
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run:
|
||||
name: "Cloning test project: <<parameters.repo>>"
|
||||
command: |
|
||||
@@ -321,11 +504,6 @@ commands:
|
||||
type: string
|
||||
default: "CI=true yarn start"
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
# make sure the binary and NPM package files are present
|
||||
- run: ls -l
|
||||
- run: ls -l cypress.zip cypress.tgz
|
||||
- clone-repo-and-checkout-release-branch:
|
||||
repo: <<parameters.repo>>
|
||||
- when:
|
||||
@@ -456,11 +634,6 @@ commands:
|
||||
type: string
|
||||
default: "npm start --if-present"
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
# make sure the binary and NPM package files are present
|
||||
- run: ls -l
|
||||
- run: ls -l cypress.zip cypress.tgz
|
||||
- clone-repo-and-checkout-release-branch:
|
||||
repo: <<parameters.repo>>
|
||||
- when:
|
||||
@@ -474,16 +647,16 @@ commands:
|
||||
git checkout pr-<<parameters.pull_request_id>>
|
||||
git log -n 2
|
||||
- run:
|
||||
# Install with yarn if yarn.lock present
|
||||
# Install deps + Cypress binary with yarn if yarn.lock present
|
||||
command: |
|
||||
[[ -f yarn.lock ]] && yarn || npm install
|
||||
if [[ -f yarn.lock ]]; then
|
||||
yarn --frozen-lockfile
|
||||
CYPRESS_INSTALL_BINARY=~/cypress/cypress.zip yarn add -D ~/cypress/cypress.tgz
|
||||
else
|
||||
npm install
|
||||
CYPRESS_INSTALL_BINARY=~/cypress/cypress.zip npm install ~/cypress/cypress.tgz
|
||||
fi
|
||||
working_directory: /tmp/<<parameters.repo>>
|
||||
- run:
|
||||
name: Install Cypress
|
||||
working_directory: /tmp/<<parameters.repo>>
|
||||
# force installing the freshly built binary
|
||||
command: |
|
||||
CYPRESS_INSTALL_BINARY=~/cypress/cypress.zip npm i ~/cypress/cypress.tgz
|
||||
- run:
|
||||
name: Print Cypress version
|
||||
working_directory: /tmp/<<parameters.repo>>
|
||||
@@ -685,90 +858,37 @@ commands:
|
||||
- cypress/npm-package-url.json
|
||||
|
||||
jobs:
|
||||
## code checkout and yarn installs
|
||||
## Checks if we already have a valid cache for the node_modules_install and if it has,
|
||||
## skips ahead to the build step, otherwise installs and caches the node_modules
|
||||
node_modules_install:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
- install-required-node
|
||||
- install-build-setup
|
||||
- caching-dependency-installer:
|
||||
arch: << parameters.arch >>
|
||||
is-mac: <<parameters.is-mac>>
|
||||
- store-npm-logs
|
||||
|
||||
## restores node_modules from previous step & builds if first step skipped
|
||||
build:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
- install-required-node
|
||||
- run:
|
||||
name: Print working folder
|
||||
command: echo $PWD
|
||||
- run:
|
||||
name: print global yarn cache path
|
||||
command: echo $(yarn global bin)
|
||||
- run:
|
||||
name: print Node version
|
||||
command: |
|
||||
. ./scripts/load-nvm.sh
|
||||
echo "nvm use default"
|
||||
nvm use default
|
||||
node -v
|
||||
- run:
|
||||
name: print yarn version
|
||||
command: yarn -v
|
||||
- run:
|
||||
name: check Node version
|
||||
command: |
|
||||
. ./scripts/load-nvm.sh
|
||||
yarn check-node-version
|
||||
|
||||
## make sure the TERM is set to 'xterm' in node (Linux only)
|
||||
## else colors (and tests) will fail
|
||||
## See the following information
|
||||
## * http://andykdocs.de/development/Docker/Fixing+the+Docker+TERM+variable+issue
|
||||
## * https://unix.stackexchange.com/questions/43945/whats-the-difference-between-various-term-variables
|
||||
- run:
|
||||
name: Check terminal
|
||||
command: |
|
||||
. ./scripts/load-nvm.sh
|
||||
yarn check-terminal
|
||||
|
||||
- run:
|
||||
name: Stop .only
|
||||
command: |
|
||||
. ./scripts/load-nvm.sh
|
||||
yarn stop-only-all
|
||||
|
||||
- restore_cache:
|
||||
name: Restore yarn cache
|
||||
key: v{{ .Environment.CACHE_VERSION }}-{{ arch }}-deps-root-{{ checksum "yarn.lock" }}
|
||||
|
||||
# show what is already cached globally
|
||||
- run: ls $(yarn global bin)
|
||||
- run: ls $(yarn global bin)/../lib/node_modules
|
||||
|
||||
# try several times, because flaky NPM installs ...
|
||||
- run:
|
||||
name: install and build
|
||||
command: |
|
||||
. ./scripts/load-nvm.sh
|
||||
yarn --frozen-lockfile || yarn --frozen-lockfile
|
||||
yarn build-prod
|
||||
- install-build-setup
|
||||
- unpack-dependencies
|
||||
- run:
|
||||
name: Top level packages
|
||||
command: yarn list --depth=0 || true
|
||||
|
||||
- build-and-persist
|
||||
- store-npm-logs
|
||||
|
||||
- save_cache:
|
||||
name: Save yarn cache
|
||||
key: v{{ .Environment.CACHE_VERSION }}-{{ arch }}-deps-root-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- ~/.cache
|
||||
|
||||
## save entire folder as artifact for other jobs to run without reinstalling
|
||||
- persist_to_workspace:
|
||||
root: ~/
|
||||
paths:
|
||||
- cypress
|
||||
- .ssh
|
||||
|
||||
lint:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- install-required-node
|
||||
## this will catch ".only"s in js/coffee as well
|
||||
- run:
|
||||
@@ -791,8 +911,7 @@ jobs:
|
||||
required_env_var:
|
||||
type: env_var_name
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run:
|
||||
# if this is an external pull request, the environment variables
|
||||
# are NOT set for security reasons, thus no need to poll -
|
||||
@@ -819,8 +938,7 @@ jobs:
|
||||
<<: *defaults
|
||||
parallelism: 8
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run: mkdir -p cli/visual-snapshots
|
||||
- run:
|
||||
command: node cli/bin/cypress info --dev | yarn --silent term-to-html | node scripts/sanitize --type cli-info > cli/visual-snapshots/cypress-info.html
|
||||
@@ -843,8 +961,7 @@ jobs:
|
||||
<<: *defaults
|
||||
parallelism: 1
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
# make sure mocha runs
|
||||
- run: yarn test-mocha
|
||||
# test binary build code
|
||||
@@ -870,8 +987,7 @@ jobs:
|
||||
<<: *defaults
|
||||
parallelism: 1
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run:
|
||||
command: ls -la types
|
||||
working_directory: cli
|
||||
@@ -890,8 +1006,7 @@ jobs:
|
||||
<<: *defaults
|
||||
parallelism: 1
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run: yarn test-unit --scope @packages/server
|
||||
- verify-mocha-results:
|
||||
expectedResultCount: 1
|
||||
@@ -903,8 +1018,7 @@ jobs:
|
||||
<<: *defaults
|
||||
parallelism: 1
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run: yarn test-unit --scope @packages/server-ct
|
||||
- verify-mocha-results:
|
||||
expectedResultCount: 1
|
||||
@@ -916,8 +1030,7 @@ jobs:
|
||||
<<: *defaults
|
||||
parallelism: 1
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run: yarn test-integration --scope @packages/server
|
||||
- verify-mocha-results:
|
||||
expectedResultCount: 1
|
||||
@@ -928,8 +1041,7 @@ jobs:
|
||||
server-performance-tests:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run:
|
||||
command: yarn workspace @packages/server test-performance
|
||||
- verify-mocha-results:
|
||||
@@ -964,8 +1076,7 @@ jobs:
|
||||
server-e2e-tests-non-root:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run:
|
||||
command: yarn workspace @packages/server test ./test/e2e/non_root*spec* --browser electron
|
||||
- verify-mocha-results
|
||||
@@ -1016,8 +1127,7 @@ jobs:
|
||||
<<: *defaults
|
||||
parallelism: 2
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run:
|
||||
command: yarn build-prod
|
||||
working_directory: packages/desktop-gui
|
||||
@@ -1042,8 +1152,7 @@ jobs:
|
||||
<<: *defaults
|
||||
parallelism: 1
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run:
|
||||
# will use PERCY_TOKEN environment variable if available
|
||||
command: |
|
||||
@@ -1060,8 +1169,7 @@ jobs:
|
||||
reporter-integration-tests:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run:
|
||||
command: yarn build-for-tests
|
||||
working_directory: packages/reporter
|
||||
@@ -1084,8 +1192,7 @@ jobs:
|
||||
ui-components-integration-tests:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run:
|
||||
command: yarn build-for-tests
|
||||
working_directory: packages/ui-components
|
||||
@@ -1105,8 +1212,7 @@ jobs:
|
||||
npm-webpack-preprocessor:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run:
|
||||
name: Build
|
||||
command: yarn workspace @cypress/webpack-preprocessor build
|
||||
@@ -1143,16 +1249,14 @@ jobs:
|
||||
npm-webpack-dev-server:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run:
|
||||
name: Run tests
|
||||
command: yarn workspace @cypress/webpack-dev-server test
|
||||
npm-vite-dev-server:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run:
|
||||
name: Run tests
|
||||
command: yarn test --reporter cypress-circleci-reporter --reporter-options resultsDir=./test_results
|
||||
@@ -1166,8 +1270,7 @@ jobs:
|
||||
npm-rollup-dev-server:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run:
|
||||
name: Run tests
|
||||
command: yarn workspace @cypress/rollup-dev-server test
|
||||
@@ -1175,8 +1278,7 @@ jobs:
|
||||
npm-webpack-batteries-included-preprocessor:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run:
|
||||
name: Run tests
|
||||
command: yarn workspace @cypress/webpack-batteries-included-preprocessor test
|
||||
@@ -1185,8 +1287,7 @@ jobs:
|
||||
<<: *defaults
|
||||
parallelism: 3
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run:
|
||||
name: Build
|
||||
command: yarn workspace @cypress/vue build
|
||||
@@ -1203,15 +1304,14 @@ jobs:
|
||||
npm-design-system:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run:
|
||||
name: Build
|
||||
command: yarn workspace @cypress/design-system build
|
||||
- run:
|
||||
name: Run tests
|
||||
command: yarn test
|
||||
working_directory: npm/design-system
|
||||
# - run:
|
||||
# name: Run tests
|
||||
# command: yarn test
|
||||
# working_directory: npm/design-system
|
||||
- store-npm-logs
|
||||
|
||||
|
||||
@@ -1219,8 +1319,7 @@ jobs:
|
||||
<<: *defaults
|
||||
parallelism: 8
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- restore_cache:
|
||||
name: Restore yarn cache
|
||||
key: v{{ .Environment.CACHE_VERSION }}-{{ arch }}-npm-react-babel-cache
|
||||
@@ -1240,8 +1339,7 @@ jobs:
|
||||
npm-mount-utils:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run:
|
||||
name: Build
|
||||
command: yarn workspace @cypress/mount-utils build
|
||||
@@ -1250,8 +1348,7 @@ jobs:
|
||||
npm-create-cypress-tests:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run: yarn workspace create-cypress-tests build
|
||||
- run:
|
||||
name: Run unit test
|
||||
@@ -1260,8 +1357,7 @@ jobs:
|
||||
npm-eslint-plugin-dev:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run:
|
||||
name: Run tests
|
||||
command: yarn workspace @cypress/eslint-plugin-dev test
|
||||
@@ -1269,8 +1365,7 @@ jobs:
|
||||
npm-release:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run:
|
||||
name: Release packages after all jobs pass
|
||||
command: yarn npm-release
|
||||
@@ -1279,8 +1374,7 @@ jobs:
|
||||
<<: *defaults
|
||||
shell: /bin/bash --login
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- build-binary
|
||||
- build-npm-package
|
||||
- run:
|
||||
@@ -1362,8 +1456,7 @@ jobs:
|
||||
<<: *defaults
|
||||
steps:
|
||||
# needs uploaded NPM and test binary
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
- run: ls -la
|
||||
# make sure JSON files with uploaded urls are present
|
||||
- run: ls -la binary-url.json npm-package-url.json
|
||||
@@ -1398,8 +1491,7 @@ jobs:
|
||||
"test-npm-module-and-verify-binary":
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_cached_workspace
|
||||
# make sure we have cypress.zip received
|
||||
- run: ls -l
|
||||
- run: ls -l cypress.zip cypress.tgz
|
||||
@@ -1437,14 +1529,8 @@ jobs:
|
||||
docker:
|
||||
- image: cypress/base:12.0.0-libgbm
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
# make sure we have cypress.zip received
|
||||
- run: ls -l
|
||||
- run: ls -l cypress.zip cypress.tgz
|
||||
- restore_workspace_binaries
|
||||
- run: mkdir test-binary
|
||||
- run: node --version
|
||||
- run: npm --version
|
||||
- run:
|
||||
name: Create new NPM package
|
||||
working_directory: test-binary
|
||||
@@ -1478,14 +1564,8 @@ jobs:
|
||||
default: /root/test-cypress-and-jest
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
# make sure we have cypress.zip received
|
||||
- run: ls -l
|
||||
- run: ls -l cypress.zip cypress.tgz
|
||||
- restore_workspace_binaries
|
||||
- run: mkdir <<parameters.wd>>
|
||||
- run: node --version
|
||||
- run: npm --version
|
||||
- run:
|
||||
name: Create new NPM package ⚗️
|
||||
working_directory: <<parameters.wd>>
|
||||
@@ -1521,14 +1601,8 @@ jobs:
|
||||
default: /root/test-scaffold
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
# make sure we have cypress.zip received
|
||||
- run: ls -l
|
||||
- run: ls -l cypress.zip cypress.tgz
|
||||
- restore_workspace_binaries
|
||||
- run: mkdir <<parameters.wd>>
|
||||
- run: node --version
|
||||
- run: npm --version
|
||||
- run:
|
||||
name: Create new NPM package ⚗️
|
||||
working_directory: <<parameters.wd>>
|
||||
@@ -1564,14 +1638,8 @@ jobs:
|
||||
default: /root/test-full-typescript
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
# make sure we have cypress.zip received
|
||||
- run: ls -l
|
||||
- run: ls -l cypress.zip cypress.tgz
|
||||
- restore_workspace_binaries
|
||||
- run: mkdir <<parameters.wd>>
|
||||
- run: node --version
|
||||
- run: npm --version
|
||||
- run:
|
||||
name: Create new NPM package ⚗️
|
||||
working_directory: <<parameters.wd>>
|
||||
@@ -1596,11 +1664,7 @@ jobs:
|
||||
"test-binary-against-staging":
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- run: ls -l
|
||||
# make sure we have the binary and NPM package
|
||||
- run: ls -l cypress.zip cypress.tgz
|
||||
- restore_workspace_binaries
|
||||
- clone-repo-and-checkout-release-branch:
|
||||
repo: cypress-test-tiny
|
||||
- run:
|
||||
@@ -1687,15 +1751,16 @@ jobs:
|
||||
- test-binary-against-repo:
|
||||
repo: cypress-documentation
|
||||
browser: firefox
|
||||
command: "npm run cypress:run"
|
||||
wait-on: http://localhost:2222
|
||||
command: "yarn cypress run"
|
||||
wait-on: http://localhost:3000
|
||||
server-start-command: yarn serve:dist
|
||||
|
||||
"test-binary-against-realworld-firefox":
|
||||
"test-binary-against-conduit-chrome":
|
||||
<<: *defaults
|
||||
steps:
|
||||
- test-binary-against-repo:
|
||||
repo: cypress-example-realworld
|
||||
browser: firefox
|
||||
repo: cypress-example-conduit-app
|
||||
browser: chrome
|
||||
command: "npm run cypress:run"
|
||||
|
||||
"test-binary-against-api-testing-firefox":
|
||||
@@ -1726,8 +1791,7 @@ jobs:
|
||||
test-binary-as-specific-user:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- restore_workspace_binaries
|
||||
# the user should be "node"
|
||||
- run: whoami
|
||||
- run: pwd
|
||||
@@ -1773,7 +1837,11 @@ jobs:
|
||||
|
||||
linux-workflow: &linux-workflow
|
||||
jobs:
|
||||
- build
|
||||
- node_modules_install:
|
||||
arch: 'linux'
|
||||
- build:
|
||||
requires:
|
||||
- node_modules_install
|
||||
- lint:
|
||||
name: Linux lint
|
||||
requires:
|
||||
@@ -1887,7 +1955,7 @@ linux-workflow: &linux-workflow
|
||||
# Any attempts to automate this are welcome
|
||||
# If CircleCI provided an "after all" hook, then this wouldn't be necessary
|
||||
- npm-release:
|
||||
requires:
|
||||
requires:
|
||||
- build
|
||||
- npm-eslint-plugin-dev
|
||||
- npm-create-cypress-tests
|
||||
@@ -2011,22 +2079,22 @@ linux-workflow: &linux-workflow
|
||||
requires:
|
||||
- create-build-artifacts
|
||||
|
||||
- test-binary-against-recipes-firefox:
|
||||
<<: *testBinaryFirefox
|
||||
- test-binary-against-kitchensink-firefox:
|
||||
<<: *testBinaryFirefox
|
||||
- test-binary-against-kitchensink-chrome:
|
||||
<<: *testBinaryFirefox
|
||||
<<: *onlyMainBranches
|
||||
- test-binary-against-conduit-chrome:
|
||||
<<: *onlyMainBranches
|
||||
- test-binary-against-recipes-firefox:
|
||||
<<: *onlyMainBranches
|
||||
- test-binary-against-kitchensink-firefox:
|
||||
<<: *onlyMainBranches
|
||||
- test-binary-against-todomvc-firefox:
|
||||
<<: *testBinaryFirefox
|
||||
<<: *onlyMainBranches
|
||||
- test-binary-against-documentation-firefox:
|
||||
<<: *testBinaryFirefox
|
||||
<<: *onlyMainBranches
|
||||
- test-binary-against-api-testing-firefox:
|
||||
<<: *testBinaryFirefox
|
||||
- test-binary-against-realworld-firefox:
|
||||
<<: *testBinaryFirefox
|
||||
<<: *onlyMainBranches
|
||||
- test-binary-against-piechopper-firefox:
|
||||
<<: *testBinaryFirefox
|
||||
<<: *onlyMainBranches
|
||||
- test-binary-against-cypress-realworld-app:
|
||||
executor: cy-doc-plus
|
||||
filters:
|
||||
@@ -2049,9 +2117,18 @@ linux-workflow: &linux-workflow
|
||||
|
||||
mac-workflow: &mac-workflow
|
||||
jobs:
|
||||
- node_modules_install:
|
||||
name: darwin-node-modules-install
|
||||
executor: mac
|
||||
arch: darwin
|
||||
is-mac: true
|
||||
<<: *macBuildFilters
|
||||
|
||||
- build:
|
||||
name: darwin-build
|
||||
executor: mac
|
||||
requires:
|
||||
- darwin-node-modules-install
|
||||
<<: *macBuildFilters
|
||||
|
||||
- lint:
|
||||
|
||||
Vendored
+16
-1
@@ -392,7 +392,7 @@ declare namespace Cypress {
|
||||
Cookies: {
|
||||
debug(enabled: boolean, options?: Partial<DebugOptions>): void
|
||||
preserveOnce(...names: string[]): void
|
||||
defaults(options: Partial<CookieDefaults>): void
|
||||
defaults(options: Partial<CookieDefaults>): CookieDefaults
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -498,6 +498,14 @@ declare namespace Cypress {
|
||||
defaults(options: Partial<ScreenshotDefaultsOptions>): void
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://on.cypress.io/selector-playground-api
|
||||
*/
|
||||
SelectorPlayground: {
|
||||
defaults(options: Partial<SelectorPlaygroundDefaultsOptions>): void
|
||||
getSelector($el: JQuery): JQuery.Selector
|
||||
}
|
||||
|
||||
/**
|
||||
* These events come from Cypress as it issues commands and reacts to their state. These are all useful to listen to for debugging purposes.
|
||||
* @see https://on.cypress.io/catalog-of-events#App-Events
|
||||
@@ -2882,6 +2890,11 @@ declare namespace Cypress {
|
||||
screenshotOnRunFailure: boolean
|
||||
}
|
||||
|
||||
interface SelectorPlaygroundDefaultsOptions {
|
||||
selectorPriority: string[]
|
||||
onElement: ($el: JQuery) => string | null | undefined
|
||||
}
|
||||
|
||||
interface ScrollToOptions extends Loggable, Timeoutable {
|
||||
/**
|
||||
* Scrolls over the duration (in ms)
|
||||
@@ -5458,6 +5471,8 @@ declare namespace Cypress {
|
||||
interface LogConfig extends Timeoutable {
|
||||
/** The JQuery element for the command. This will highlight the command in the main window when debugging */
|
||||
$el: JQuery
|
||||
/** The scope of the log entry. If child, will appear nested below parents, prefixed with '-' */
|
||||
type: 'parent' | 'child'
|
||||
/** Allows the name of the command to be overwritten */
|
||||
name: string
|
||||
/** Override *name* for display purposes only */
|
||||
|
||||
@@ -6,7 +6,7 @@ Installs and injects all the required configuration to run cypress tests.
|
||||
|
||||
```
|
||||
cd my-app
|
||||
npx create-cypress-test
|
||||
npx create-cypress-tests
|
||||
npx cypress open
|
||||
```
|
||||
|
||||
|
||||
@@ -16,6 +16,13 @@
|
||||
"cypress/globals": true
|
||||
},
|
||||
"rules": {
|
||||
"no-duplicate-imports": "off",
|
||||
"no-else-return": [
|
||||
"error",
|
||||
{
|
||||
"allowElseIf": true
|
||||
}
|
||||
],
|
||||
"react/display-name": "off",
|
||||
"react/function-component-definition": [
|
||||
"error",
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
const cssFolders = require('../css.folders')
|
||||
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin')
|
||||
|
||||
module.exports = {
|
||||
stories: [
|
||||
'../src/**/*.stories.mdx',
|
||||
'../src/**/*.stories.@(js|jsx|ts|tsx)',
|
||||
],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
],
|
||||
webpackFinal: async (config) => {
|
||||
const sassLoader = {
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
sassOptions: {
|
||||
includePaths: cssFolders,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
config.module.rules.push(...[
|
||||
{
|
||||
test: /\.s[ca]ss$/,
|
||||
exclude: /\.module\.s[ca]ss$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
modules: {
|
||||
compileType: 'icss',
|
||||
},
|
||||
},
|
||||
},
|
||||
sassLoader,
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.module\.s[ca]ss$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
modules: {
|
||||
compileType: 'module',
|
||||
localIdentName: '[name]__[local]--[hash:base64:5]',
|
||||
exportLocalsConvention: 'camelCaseOnly',
|
||||
},
|
||||
},
|
||||
},
|
||||
sassLoader,
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
config.resolve.plugins.push(new TsconfigPathsPlugin())
|
||||
|
||||
return config
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import '../src/global.scss'
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
}
|
||||
@@ -35,10 +35,10 @@ SCSS usage:
|
||||
|
||||
```scss
|
||||
// scoped within the *.scss file
|
||||
@use '@cypress/design-system/src/index.scss' as *;
|
||||
@use '@cypress/design-system' as *;
|
||||
|
||||
// import variables and mixins throughout the whole project
|
||||
// or @import('@cypress/design-system/src/index.scss');
|
||||
// or @import('@cypress/design-system');
|
||||
|
||||
.my-component {
|
||||
text-color: $accent-color-01;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
const path = require('path')
|
||||
|
||||
// Resolve css dir and both local and monorepo node_modules
|
||||
module.exports = ['src/css', 'node_modules', '../../node_modules'].map((p) => path.resolve(p))
|
||||
@@ -1,2 +1,11 @@
|
||||
import 'regenerator-runtime/runtime'
|
||||
import 'cypress-real-events/support'
|
||||
|
||||
// Need to register these once per app. Depending which components are consumed
|
||||
// from @cypress/design-system, different icons are required.
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { fab } from '@fortawesome/free-brands-svg-icons'
|
||||
import { fas } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(fas)
|
||||
library.add(fab)
|
||||
|
||||
@@ -6,10 +6,13 @@
|
||||
"scripts": {
|
||||
"build": "rimraf dist && yarn rollup -c rollup.config.js",
|
||||
"build-prod": "yarn build",
|
||||
"build-storybook": "build-storybook",
|
||||
"build-style-types": "tsm \"src/css/derived/*.scss\" --nameFormat none --exportType default",
|
||||
"cy:open": "node ../../scripts/cypress.js open-ct --project ${PWD}",
|
||||
"cy:open:debug": "node --inspect-brk ../../scripts/start.js --component-testing --project ${PWD}",
|
||||
"cy:run": "node ../../scripts/cypress.js run-ct --project ${PWD}",
|
||||
"cy:run:debug": "node --inspect-brk ../../scripts/start.js --component-testing --run-project ${PWD}",
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"pretest": "yarn transpile",
|
||||
"test": "yarn cy:run",
|
||||
"transpile": "tsc",
|
||||
@@ -22,7 +25,8 @@
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.2",
|
||||
"@fortawesome/react-fontawesome": "0.1.14",
|
||||
"classnames": "2.2.6",
|
||||
"debug": "4.3.2"
|
||||
"debug": "4.3.2",
|
||||
"react-aria": "^3.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.4.5",
|
||||
@@ -33,29 +37,39 @@
|
||||
"@cypress/react": "0.0.0-development",
|
||||
"@cypress/vite-dev-server": "0.0.0-development",
|
||||
"@percy/cypress": "2.3.2",
|
||||
"@react-types/button": "^3.3.1",
|
||||
"@react-types/shared": "^3.5.0",
|
||||
"@rollup/plugin-commonjs": "^17.1.0",
|
||||
"@rollup/plugin-image": "2.0.6",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-node-resolve": "^11.1.1",
|
||||
"@rollup/plugin-typescript": "^8.2.1",
|
||||
"@storybook/addon-actions": "^6.1.21",
|
||||
"@storybook/addon-essentials": "^6.1.21",
|
||||
"@storybook/addon-links": "^6.1.21",
|
||||
"@storybook/preset-typescript": "^3.0.0",
|
||||
"@storybook/react": "^6.1.21",
|
||||
"@types/node": "14.14.31",
|
||||
"@types/semver": "7.3.4",
|
||||
"babel-loader": "8.0.6",
|
||||
"css-loader": "2.1.1",
|
||||
"css-loader": "^5.1.3",
|
||||
"cypress": "0.0.0-development",
|
||||
"cypress-real-events": "1.1.0",
|
||||
"eslint-plugin-react": "^7.22.0",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"postcss": "^8.2.8",
|
||||
"react": "16.8.6",
|
||||
"react-dom": "16.8.6",
|
||||
"rollup": "^2.38.5",
|
||||
"rollup-plugin-copy-assets": "2.0.3",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"rollup-plugin-peer-deps-external": "2.2.4",
|
||||
"rollup-plugin-postcss-modules": "2.0.2",
|
||||
"rollup-plugin-typescript2": "^0.29.0",
|
||||
"rollup-plugin-postcss": "^4.0.0",
|
||||
"sass": "1.32.8",
|
||||
"sass-loader": "10.1.1",
|
||||
"style-loader": "0.23.1",
|
||||
"style-loader": "^2.0.0",
|
||||
"svg-url-loader": "3.0.3",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.1",
|
||||
"typed-scss-modules": "^4.1.1",
|
||||
"typescript": "^4.2.3",
|
||||
"vite": "2.1.3",
|
||||
"webpack": "4.44.1"
|
||||
@@ -74,7 +88,7 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/cypress-io/cypress.git"
|
||||
},
|
||||
"author": "Jessica Sachs <jess@jessicasachs.io>",
|
||||
"author": "Cypress.io",
|
||||
"keywords": [
|
||||
"design-system",
|
||||
"cypress",
|
||||
@@ -82,6 +96,7 @@
|
||||
],
|
||||
"unpkg": "dist/cypress-design-system.browser.js",
|
||||
"module": "dist/cypress-design-system.esm-bundler.js",
|
||||
"style": "dist/index.scss",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
@@ -92,4 +107,4 @@
|
||||
"expect"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import ts from 'rollup-plugin-typescript2'
|
||||
import ts from '@rollup/plugin-typescript'
|
||||
import resolve from '@rollup/plugin-node-resolve'
|
||||
import commonjs from '@rollup/plugin-commonjs'
|
||||
import json from '@rollup/plugin-json'
|
||||
import peerDepsExternal from 'rollup-plugin-peer-deps-external'
|
||||
import postcss from 'rollup-plugin-postcss-modules'
|
||||
import postcss from 'rollup-plugin-postcss'
|
||||
import pkg from './package.json'
|
||||
import image from '@rollup/plugin-image'
|
||||
import copy from 'rollup-plugin-copy-assets'
|
||||
import copy from 'rollup-plugin-copy'
|
||||
|
||||
const cssFolders = require('./css.folders')
|
||||
|
||||
const banner = `
|
||||
/**
|
||||
@@ -31,14 +33,42 @@ function createEntry (options) {
|
||||
],
|
||||
plugins: [
|
||||
peerDepsExternal(),
|
||||
ts({
|
||||
// check: format === 'es' && isBrowser,
|
||||
declaration: format === 'es',
|
||||
target: 'es5', // not sure what this should be?
|
||||
module: format === 'cjs' ? 'es2015' : 'esnext',
|
||||
}),
|
||||
resolve(),
|
||||
json(),
|
||||
commonjs(),
|
||||
postcss({ writeDefinitions: false }),
|
||||
postcss({
|
||||
modules: {
|
||||
globalModulePaths: ['src/css/derived/export.scss'],
|
||||
},
|
||||
use: [
|
||||
['sass', {
|
||||
includePaths: cssFolders,
|
||||
}],
|
||||
],
|
||||
}),
|
||||
image(),
|
||||
copy({
|
||||
assets: [
|
||||
'./index.scss',
|
||||
targets: [
|
||||
// Purposefully ignore global.scss to prevent direct imports
|
||||
{
|
||||
src: './src/index.scss',
|
||||
dest: './dist',
|
||||
},
|
||||
// Purposefully ignore `derived` directory SASS
|
||||
{
|
||||
src: './src/css/*.scss',
|
||||
dest: './dist/css',
|
||||
},
|
||||
{
|
||||
src: './src/css/derived/*.d.ts',
|
||||
dest: './dist/css/derived',
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
@@ -68,19 +98,6 @@ function createEntry (options) {
|
||||
/* eslint-disable no-console */
|
||||
console.log(`Building ${format}: ${config.output.file}`)
|
||||
|
||||
config.plugins.push(
|
||||
ts({
|
||||
check: format === 'es' && isBrowser,
|
||||
tsconfigOverride: {
|
||||
compilerOptions: {
|
||||
declaration: format === 'es',
|
||||
target: 'es5', // not sure what this should be?
|
||||
module: format === 'cjs' ? 'es2015' : 'esnext',
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
@use '../../index.scss' as *;
|
||||
|
||||
.button {
|
||||
background: $accent-01;
|
||||
|
||||
&:hover {
|
||||
@extend .text-black;
|
||||
background: $papaya-05;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from 'react'
|
||||
import { mount } from '@cypress/react'
|
||||
import { Button } from './Button'
|
||||
|
||||
describe('Button', () => {
|
||||
it('renders', () => {
|
||||
mount(<Button />)
|
||||
cy.get('button').should('exist')
|
||||
})
|
||||
})
|
||||
@@ -1,6 +0,0 @@
|
||||
import React from 'react'
|
||||
import { button } from './Button.module.scss'
|
||||
|
||||
export const Button = () => (
|
||||
<button className={button}>Hello World</button>
|
||||
)
|
||||
@@ -1 +0,0 @@
|
||||
export * from './Button'
|
||||
@@ -1,4 +1,6 @@
|
||||
@use '../../index.scss' as *;
|
||||
@use 'baseColors' as *;
|
||||
@use 'semanticColors' as *;
|
||||
@use 'typography' as *;
|
||||
|
||||
$left-nav-width: 48px;
|
||||
$icon-color: $metal-20;
|
||||
@@ -33,7 +35,7 @@ $active-color: $brand-01;
|
||||
|
||||
.item {
|
||||
height: 100%;
|
||||
font-size: $text-ml;
|
||||
font-size: text(ml);
|
||||
cursor: pointer;
|
||||
color: $icon-color;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react'
|
||||
import { CypressLogo } from './CypressLogo/CypressLogo'
|
||||
import { SearchInput } from './SearchInput/SearchInput'
|
||||
import { mount } from '@cypress/react'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
@@ -22,52 +21,4 @@ describe('Playground', () => {
|
||||
</>,
|
||||
)
|
||||
})
|
||||
|
||||
it('search input', () => {
|
||||
const Wrapper = (props) => {
|
||||
const [value, setValue] = React.useState(props.value || '')
|
||||
const inputRef = React.useRef<HTMLInputElement>(null)
|
||||
|
||||
return (
|
||||
<SearchInput
|
||||
prefixIcon={props.prefixIcon}
|
||||
placeholder={props.placeholder}
|
||||
inputRef={inputRef}
|
||||
value={value}
|
||||
onSuffixClicked={() => {
|
||||
setValue('')
|
||||
inputRef.current.focus()
|
||||
}}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
>
|
||||
</SearchInput>
|
||||
)
|
||||
}
|
||||
|
||||
mount(
|
||||
<>
|
||||
<Wrapper placeholder="Find components..." prefixIcon="search" />
|
||||
<br />
|
||||
{/* <Wrapper placeholder="Find components..." prefixIcon="coffee"/> */}
|
||||
<br />
|
||||
{/* <Wrapper placeholder="Find components..." prefixIcon="search" suffixIcon="times"/> */}
|
||||
</>,
|
||||
)
|
||||
|
||||
cy.get('input').should('exist')
|
||||
cy.get('input').should('exist').first()
|
||||
// .its('placeholder').should('be', placeholderText)
|
||||
// .click()
|
||||
|
||||
cy.get('input').first().type('Hello World!').clear().type('WHATS UP ⚡️')
|
||||
cy.get('input').first().should('contain.value', '⚡️')
|
||||
|
||||
cy.get('input')
|
||||
.click()
|
||||
.type('hello')
|
||||
.get('[data-testid=close]')
|
||||
.click()
|
||||
.get('input')
|
||||
.should('be.focused')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
@use '../../index.scss' as *;
|
||||
|
||||
$active-color: $metal-50;
|
||||
$inactive-color: $metal-20;
|
||||
$input-color: $metal-50;
|
||||
|
||||
@mixin quick-transition($param) {
|
||||
transition: $param 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
@include text-style-light-xs;
|
||||
@include quick-transition(border);
|
||||
color: $input-color;
|
||||
padding: 0 $text-m 0 $text-xxs;
|
||||
width: 100%;
|
||||
border: unset;
|
||||
outline: unset;
|
||||
border-bottom: 1px solid $inactive-color;
|
||||
|
||||
&.hasPrefix {
|
||||
padding: 0.2rem $text-m;
|
||||
}
|
||||
}
|
||||
|
||||
// prefix and suffix math
|
||||
|
||||
$icon-size: $text-xs;
|
||||
$icon-negative-margin: calc(#{$icon-size} * (-4 / 3));
|
||||
$icon-margin: calc(#{$icon-size} / 3);
|
||||
|
||||
.prefix, .suffix {
|
||||
@include quick-transition(color);
|
||||
position: absolute;
|
||||
font-size: $icon-size;
|
||||
color: $inactive-color;
|
||||
}
|
||||
|
||||
.prefix {
|
||||
margin: $icon-margin;
|
||||
}
|
||||
|
||||
.suffix {
|
||||
margin: $icon-margin $icon-negative-margin;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.inputButton {
|
||||
position: relative;
|
||||
display: flex;
|
||||
margin: 1rem;
|
||||
align-items: center;
|
||||
|
||||
&:active, &:focus-within {
|
||||
.searchInput {
|
||||
border-bottom: 1px solid $active-color;
|
||||
}
|
||||
.prefix {
|
||||
color: $active-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import React from 'react'
|
||||
import { FontAwesomeIcon, FontAwesomeIconProps } from '@fortawesome/react-fontawesome'
|
||||
import cs from 'classnames'
|
||||
import styles from './SearchInput.module.scss'
|
||||
|
||||
interface SearchInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
prefixIcon?: FontAwesomeIconProps['icon']
|
||||
placeholder: string
|
||||
onSuffixClicked?: () => void
|
||||
onPrefixClicked?: () => void
|
||||
inputRef: React.RefObject<HTMLInputElement>
|
||||
}
|
||||
|
||||
export const SearchInput: React.FC<SearchInputProps> = (props) => {
|
||||
const { onSuffixClicked } = props
|
||||
|
||||
const prefixIcon = props.prefixIcon && (
|
||||
<FontAwesomeIcon
|
||||
className={styles.prefix}
|
||||
icon={props.prefixIcon}
|
||||
/>
|
||||
)
|
||||
|
||||
const onKeyPress = React.useCallback((e: React.KeyboardEvent<SVGSVGElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
onSuffixClicked?.()
|
||||
}
|
||||
}, [onSuffixClicked])
|
||||
|
||||
return (
|
||||
<span className={styles.inputButton}>
|
||||
{prefixIcon}
|
||||
<input
|
||||
ref={props.inputRef}
|
||||
type="text"
|
||||
className={cs([styles.searchInput, props.prefixIcon ? styles.hasPrefix : ''])}
|
||||
placeholder={props.placeholder}
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
{
|
||||
props.value && (
|
||||
<FontAwesomeIcon
|
||||
data-testid="close"
|
||||
className={styles.suffix}
|
||||
tabIndex={0}
|
||||
icon="times"
|
||||
onClick={props.onSuffixClicked}
|
||||
onKeyPress={onKeyPress}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import * as React from 'react'
|
||||
import { mount } from '@cypress/react'
|
||||
|
||||
import { SearchInput } from './SearchInput'
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
describe('SearchInput', () => {
|
||||
const StatefulWrapper: React.FC<{onInput?: (input: string) => void}> = ({ onInput }) => {
|
||||
const [value, setValue] = useState('')
|
||||
|
||||
const memoedOnInput = useCallback((input: string) => {
|
||||
setValue(input)
|
||||
onInput?.(input)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return <SearchInput placeholder="foo" value={value} aria-label="Search" onInput={memoedOnInput} />
|
||||
}
|
||||
|
||||
it('should render', () => {
|
||||
mount(<SearchInput placeholder="foo" value="" aria-label="Search" onInput={() => {}} />)
|
||||
cy.get('input').should('exist')
|
||||
})
|
||||
|
||||
it('should pass input to onInput', () => {
|
||||
const onInput = cy.stub()
|
||||
|
||||
mount(<StatefulWrapper onInput={onInput} />)
|
||||
|
||||
const string = 'Testing input!'
|
||||
|
||||
cy.get('input').type(string).then(() => {
|
||||
expect(onInput).to.be.callCount(string.length)
|
||||
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
expect(onInput.getCall(i)).to.be.calledWithExactly(string.slice(0, i + 1))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Clear button', () => {
|
||||
it('should only show when text is present', () => {
|
||||
mount(<StatefulWrapper />)
|
||||
|
||||
cy.get('[aria-label="Clear search"]').should('not.exist')
|
||||
|
||||
cy.get('input').type('some input')
|
||||
|
||||
cy.get('[aria-label="Clear search"]').should('exist')
|
||||
})
|
||||
|
||||
it('should clear input on click', () => {
|
||||
const onInput = cy.stub()
|
||||
|
||||
mount(<SearchInput placeholder="foo" value="a value" aria-label="Search" onInput={onInput} />)
|
||||
|
||||
cy.get('input').should('have.value', 'a value')
|
||||
|
||||
cy.get('[aria-label="Clear search"]').click().then(() => expect(onInput).to.be.calledOnceWith(''))
|
||||
})
|
||||
|
||||
it('should focus input on click', () => {
|
||||
mount(<SearchInput placeholder="foo" value="a value" aria-label="Search" onInput={() => {}} />)
|
||||
|
||||
cy.get('[aria-label="Clear search"]').click()
|
||||
|
||||
cy.get('input').should('be.focused')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { createStory, createStorybookConfig } from 'stories/util'
|
||||
|
||||
import { SearchInput as SearchInputComponent } from './SearchInput'
|
||||
import { useState } from 'react'
|
||||
|
||||
export default createStorybookConfig({
|
||||
title: 'Components/SearchInput',
|
||||
})
|
||||
|
||||
export const SearchInput = createStory(() => {
|
||||
const [value, setValue] = useState('')
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SearchInputComponent value={value} placeholder='Search specs' aria-label="Search" onInput={setValue} />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,50 @@
|
||||
import * as React from 'react'
|
||||
import { FormEvent, MutableRefObject, useCallback } from 'react'
|
||||
import { IconInput, IconSettings } from 'core/input/IconInput'
|
||||
import { CoreComponent } from 'core/shared'
|
||||
import { TextSize } from 'css'
|
||||
import { useCombinedRefs } from '../../hooks/useCombinedRefs'
|
||||
|
||||
export interface SearchInputProps extends CoreComponent {
|
||||
inputRef?: MutableRefObject<HTMLInputElement> | null
|
||||
|
||||
value: string
|
||||
placeholder: string
|
||||
|
||||
/**
|
||||
* Defaults to 'm'
|
||||
*/
|
||||
size?: TextSize
|
||||
|
||||
onInput: (input: string) => void
|
||||
|
||||
['aria-label']: string
|
||||
}
|
||||
|
||||
const prefixItem: IconSettings = {
|
||||
icon: 'search',
|
||||
}
|
||||
|
||||
export const SearchInput: React.FC<SearchInputProps> = ({ inputRef = null, onInput: externalOnInput, ...props }) => {
|
||||
const ref = React.useRef<HTMLInputElement>(null)
|
||||
|
||||
useCombinedRefs(ref, inputRef)
|
||||
|
||||
const onInput = useCallback((e: FormEvent<HTMLInputElement>) => externalOnInput(e.currentTarget.value), [externalOnInput])
|
||||
const onClear = useCallback(() => {
|
||||
externalOnInput('')
|
||||
|
||||
ref.current?.focus()
|
||||
}, [externalOnInput])
|
||||
|
||||
return (
|
||||
<IconInput
|
||||
{...props}
|
||||
inputRef={ref}
|
||||
label={{ type: 'aria', contents: props['aria-label'] }}
|
||||
prefixIcon={prefixItem}
|
||||
suffixIcon={props.value.length > 0 ? { icon: 'times', onPress: onClear, 'aria-label': 'Clear search' } : undefined}
|
||||
onInput={onInput}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
@use 'semanticColors' as *;
|
||||
@use 'typography' as *;
|
||||
@use 'spacing' as *;
|
||||
@use 'surfaces' as *;
|
||||
@use 'func' as *;
|
||||
@use 'util';
|
||||
|
||||
$button-vertical-padding: change-rem-unit-to-em(spacing(xs));
|
||||
$button-horizontal-padding: change-rem-unit-to-em(spacing(s));
|
||||
|
||||
.button {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
// Reset
|
||||
@include util.no-selection;
|
||||
border: 0;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
|
||||
// Style
|
||||
padding: $button-vertical-padding $button-horizontal-padding;
|
||||
border-radius: $button-radius;
|
||||
|
||||
color: $control-text-color-white;
|
||||
background-color: $button-blue-color;
|
||||
|
||||
&:hover {
|
||||
background-color: $button-blue-hover-color;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $button-blue-push-color;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&.white {
|
||||
color: $control-text-color-black;
|
||||
background-color: $button-white-color;
|
||||
|
||||
border: 1px solid $control-border-color;
|
||||
|
||||
&:hover {
|
||||
background-color: $button-white-hover-color;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $button-white-push-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.disableBorder {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
:local(.button.white:not(.disableBorder)) {
|
||||
&.focused::after {
|
||||
// White buttons have a border that needs to be compensated for
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import * as React from 'react'
|
||||
import { action } from '@storybook/addon-actions'
|
||||
|
||||
import { createStory, createStorybookConfig } from 'stories/util'
|
||||
|
||||
import { Button as ButtonComponent, LinkButton } from './Button'
|
||||
import { IconButton as IconButtonComponent } from './IconButton'
|
||||
|
||||
import typography from 'css/derived/jsTypography.scss'
|
||||
import { TextSize } from 'css'
|
||||
import { PaddedBox } from '../surface/paddedBox/PaddedBox'
|
||||
import { Icon } from '../icon/Icon'
|
||||
|
||||
export default createStorybookConfig({
|
||||
title: 'Core/Button',
|
||||
})
|
||||
|
||||
export const Button = createStory(() => (
|
||||
<div>
|
||||
<PaddedBox>
|
||||
<ButtonComponent aria-label="buttonPress" onPress={action('buttonPress')}>Simple button</ButtonComponent>
|
||||
<LinkButton aria-label="anchorButtonPress" onPress={action('anchorButtonPress')}>Anchor button</LinkButton>
|
||||
</PaddedBox>
|
||||
<PaddedBox style={{ backgroundColor: 'var(--brand-00)' }}>
|
||||
<ButtonComponent aria-label="buttonPress" color='white' onPress={action('buttonPress')}>Simple button</ButtonComponent>
|
||||
<LinkButton aria-label="anchorButtonPress" color='white' onPress={action('anchorButtonPress')}>Anchor button</LinkButton>
|
||||
</PaddedBox>
|
||||
<PaddedBox>
|
||||
<ButtonComponent aria-label="buttonPress" color='white' onPress={action('buttonPress')}>Simple button</ButtonComponent>
|
||||
<LinkButton aria-label="anchorButtonPress" color='white' onPress={action('anchorButtonPress')}>Anchor button</LinkButton>
|
||||
</PaddedBox>
|
||||
</div>
|
||||
))
|
||||
|
||||
export const ButtonSizes = createStory(() => (
|
||||
<div>
|
||||
<div style={{ width: 500 }}>
|
||||
{Object.keys(typography).filter((key) => key !== 'type' && !key.startsWith('line-height') && !key.startsWith('text-mono')).map((key) => {
|
||||
const size = key.replace('text-', '')
|
||||
|
||||
return (
|
||||
<ButtonComponent
|
||||
key={key}
|
||||
size={size as TextSize}
|
||||
aria-label="buttonPress"
|
||||
>
|
||||
{`Button ${size}`}
|
||||
</ButtonComponent>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
|
||||
export const IconButton = createStory(() => (
|
||||
<div>
|
||||
<div style={{ width: 500 }}>
|
||||
<IconButtonComponent aria-label="iconButton" elementType='button' icon='horse' />
|
||||
</div>
|
||||
<PaddedBox>
|
||||
<IconButtonComponent aria-label="iconButton" elementType='button' icon='hotdog' />
|
||||
<ButtonComponent aria-label="normalButton">Text button</ButtonComponent>
|
||||
<LinkButton aria-label="linkButton">
|
||||
<Icon icon='jedi' />
|
||||
{' Inline Icon with text'}
|
||||
</LinkButton>
|
||||
</PaddedBox>
|
||||
<PaddedBox style={{ backgroundColor: 'var(--brand-00)' }}>
|
||||
<IconButtonComponent aria-label="iconButton" elementType='button' icon='hotdog' color='white' />
|
||||
<ButtonComponent aria-label="normalButton" color='white'>Text button</ButtonComponent>
|
||||
<LinkButton aria-label="linkButton" color='white'>
|
||||
<Icon icon='jedi' />
|
||||
{' Inline Icon with text'}
|
||||
</LinkButton>
|
||||
</PaddedBox>
|
||||
</div>
|
||||
))
|
||||
@@ -0,0 +1,67 @@
|
||||
import * as React from 'react'
|
||||
import { useRef, RefObject } from 'react'
|
||||
import cs from 'classnames'
|
||||
|
||||
import { useButton } from '@react-aria/button'
|
||||
import { AriaButtonProps } from '@react-types/button'
|
||||
import { TextSizableComponent } from '../shared'
|
||||
import { styledTextSizeClassNames } from '../text/StyledText'
|
||||
|
||||
import styles from './Button.module.scss'
|
||||
import { FocusRing } from '@react-aria/focus'
|
||||
import { focusClass } from 'css/derived/util'
|
||||
|
||||
interface SharedButtonProps extends TextSizableComponent {
|
||||
/**
|
||||
* Defaults to 'blue'
|
||||
*/
|
||||
color?: 'blue' | 'white'
|
||||
noBorder?: boolean
|
||||
|
||||
['aria-label']: string
|
||||
}
|
||||
|
||||
export type BaseButtonProps = SharedButtonProps & (({
|
||||
elementType: 'button'
|
||||
} & AriaButtonProps<'button'>) | ({
|
||||
elementType: 'a'
|
||||
} & AriaButtonProps<'a'>))
|
||||
|
||||
export type ButtonProps = SharedButtonProps & Omit<AriaButtonProps<'button'>, 'elementType'>
|
||||
|
||||
export type LinkButtonProps = ButtonProps & Omit<AriaButtonProps<'a'>, 'elementType'>
|
||||
|
||||
export const BaseButton: React.FC<BaseButtonProps> = ({ size, color, noBorder, children, ...props }) => {
|
||||
const buttonRef = useRef<HTMLAnchorElement | HTMLButtonElement>(null)
|
||||
|
||||
const { buttonProps } = useButton(props, buttonRef)
|
||||
|
||||
const textClass = styledTextSizeClassNames(size)
|
||||
|
||||
const classNames = cs(textClass, styles.button, {
|
||||
[styles.white]: color === 'white',
|
||||
[styles.disableBorder]: noBorder,
|
||||
}, buttonProps.className, props.className)
|
||||
|
||||
return (
|
||||
<FocusRing focusRingClass={focusClass}>
|
||||
{props.elementType === 'button' ? (
|
||||
<button
|
||||
{...buttonProps}
|
||||
ref={buttonRef as RefObject<HTMLButtonElement>}
|
||||
className={classNames}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
) : (
|
||||
<a {...buttonProps} ref={buttonRef as RefObject<HTMLAnchorElement>} className={classNames}>
|
||||
{children}
|
||||
</a>
|
||||
)}
|
||||
</FocusRing>
|
||||
)
|
||||
}
|
||||
|
||||
export const Button: React.FC<ButtonProps & Omit<AriaButtonProps<'button'>, 'elementType'>> = (props) => <BaseButton {...props} elementType='button' />
|
||||
|
||||
export const LinkButton: React.FC<ButtonProps & Omit<AriaButtonProps<'a'>, 'elementType'>> = (props) => <BaseButton {...props} elementType='a' />
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as React from 'react'
|
||||
import { Icon, IconProps } from '../icon/Icon'
|
||||
import { BaseButton, BaseButtonProps } from './Button'
|
||||
|
||||
export type IconButtonProps = {
|
||||
iconClassName?: string
|
||||
} & BaseButtonProps & IconProps;
|
||||
|
||||
// We don't actually need to spread several of these props, but FontAwesome complains if it receives extra props
|
||||
export const IconButton: React.FC<IconButtonProps> = ({ className, iconClassName, color, elementType, noBorder, onPress, ...props }) => (
|
||||
// Cast to button just to prevent TS error
|
||||
<BaseButton
|
||||
{...props}
|
||||
elementType={elementType as 'button'}
|
||||
className={className}
|
||||
color={color}
|
||||
noBorder={noBorder}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Icon ignoreTextCenter={true} {...props} aria-label={undefined} className={iconClassName} />
|
||||
</BaseButton>
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
$icon-margin: 0.15em;
|
||||
$icon-bottom-offset: 0.125em;
|
||||
|
||||
$icon-size: 1em - $icon-margin * 2;
|
||||
|
||||
:global(.svg-inline--fa) {
|
||||
// TODO: Is there ever a need for the icon to not take a square space as long as it's properly centered?
|
||||
&.icon {
|
||||
position: relative;
|
||||
|
||||
width: $icon-size;
|
||||
height: $icon-size;
|
||||
|
||||
bottom: $icon-bottom-offset;
|
||||
}
|
||||
|
||||
&.ignoreTextCenter {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
|
||||
bottom: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.icon {
|
||||
background-color: orange;
|
||||
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.textIcon {
|
||||
border: 1px solid black;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import * as React from 'react'
|
||||
import { Story } from '@storybook/react'
|
||||
|
||||
import { createStory, createStorybookConfig } from 'stories/util'
|
||||
|
||||
import { Icon as IconComponent } from './Icon'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { fab } from '@fortawesome/free-brands-svg-icons'
|
||||
import { fas } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import typography from 'css/derived/jsTypography.scss'
|
||||
import styles from './Icon.stories.module.scss'
|
||||
import { TextSize } from 'css'
|
||||
import { Baseline } from '../../measure/baseline/Baseline'
|
||||
|
||||
library.add(fas)
|
||||
library.add(fab)
|
||||
|
||||
const fontOptions = ['-apple-system, BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica']
|
||||
|
||||
export default createStorybookConfig({
|
||||
title: 'Core/Icon',
|
||||
argTypes: {
|
||||
font: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: fontOptions,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const Template: Story<{
|
||||
font: string
|
||||
}> = ({ font }) => (
|
||||
<div style={{
|
||||
'--font-stack-sans': font,
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<IconComponent className={styles.icon} icon='check' size='xl' />
|
||||
<IconComponent className={styles.icon} icon='exclamation' size='xl' />
|
||||
<IconComponent className={styles.icon} icon='home' size='xl' />
|
||||
<IconComponent className={styles.icon} icon='arrow-circle-up' size='xl' />
|
||||
<br />
|
||||
{Object.keys(typography).filter((key) => key !== 'type').map((key) => {
|
||||
const size = key.replace('text-', '')
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
style={{
|
||||
marginBottom: '2em',
|
||||
}}
|
||||
>
|
||||
<div className="text-mono-m">
|
||||
{size}
|
||||
</div>
|
||||
<Baseline className={key}>
|
||||
<IconComponent className={styles.textIcon} icon='square' size={size as TextSize} />
|
||||
<IconComponent className={styles.textIcon} icon='exclamation' size={size as TextSize} />
|
||||
The five boxing wizards jump quickly
|
||||
<IconComponent icon='exclamation' size={size as TextSize} />
|
||||
<IconComponent icon='bell' size={size as TextSize} />
|
||||
</Baseline>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
|
||||
export const Icon = createStory(Template, {
|
||||
font: fontOptions[0],
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
import * as React from 'react'
|
||||
import { SVGAttributes } from 'react'
|
||||
import cs from 'classnames'
|
||||
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { IconName } from '@fortawesome/fontawesome-svg-core'
|
||||
import { styledTextSizeClassNames } from '../text/StyledText'
|
||||
|
||||
import styles from './Icon.module.scss'
|
||||
import { TextSizableComponent } from '../shared'
|
||||
|
||||
export interface IconProps extends TextSizableComponent, Omit<SVGAttributes<SVGSVGElement>, 'mask'> {
|
||||
// TODO: Limit literals to only those available in the iconset
|
||||
icon: IconName
|
||||
|
||||
ignoreTextCenter?: boolean
|
||||
}
|
||||
|
||||
// Currently only a passthrough for FontAwesome. This provides a single place to swap out the icon library
|
||||
export const Icon: React.FC<IconProps> = ({ className, size, lineHeight, icon, ignoreTextCenter, ...props }) => (
|
||||
<FontAwesomeIcon
|
||||
{...props}
|
||||
className={cs(styledTextSizeClassNames(size, lineHeight), styles.icon, {
|
||||
[styles.ignoreTextCenter]: ignoreTextCenter,
|
||||
}, className)}
|
||||
icon={icon}
|
||||
/>
|
||||
)
|
||||
@@ -0,0 +1,98 @@
|
||||
@use 'spacing' as *;
|
||||
@use 'semanticColors' as *;
|
||||
@use '../icon/Icon.module.scss' as *;
|
||||
@use 'surfaces' as *;
|
||||
|
||||
$icon-overall-size: 1.5em;
|
||||
$input-icon-margin: 0.25em;
|
||||
|
||||
.iconInput {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
// No actual border drawn. This provides the mask for the rounded corners to clip child elements
|
||||
border-radius: $button-radius;
|
||||
overflow: hidden;
|
||||
|
||||
z-index: 0;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
border: 1px solid $control-border-color;
|
||||
border-radius: $button-radius;
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
||||
&:first-child {
|
||||
// If first child, apply border padding
|
||||
padding-left: spacing(ms);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
// If last child, apply border padding
|
||||
padding-right: spacing(ms);
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
flex-grow: 1;
|
||||
// Required to allow input to shrink in certain size scenarios
|
||||
width: 0;
|
||||
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
// Icon
|
||||
margin: 0 $input-icon-margin;
|
||||
bottom: 0;
|
||||
|
||||
color: $control-text-color-black;
|
||||
}
|
||||
|
||||
.iconButton {
|
||||
// See global expression below
|
||||
// Make sure button fills entire height, to enable the button focus ring to cover the IconInput border
|
||||
align-self: stretch;
|
||||
|
||||
padding: 0 $input-icon-margin;
|
||||
line-height: 1;
|
||||
|
||||
border-radius: 0;
|
||||
|
||||
&::after {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
:local(.iconButton) {
|
||||
&.focused {
|
||||
// Set overflow and border radius to clip corners when focused
|
||||
overflow: hidden;
|
||||
|
||||
border-radius: $button-radius;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import * as React from 'react'
|
||||
import { InputHTMLAttributes, RefAttributes } from 'react'
|
||||
import cs from 'classnames'
|
||||
import { useFocusRing } from '@react-aria/focus'
|
||||
import { PressEvent } from '@react-types/shared'
|
||||
import { Icon, IconProps } from '../icon/Icon'
|
||||
import { BasicInput, InputBase, InputProps, InputRenderer } from './InputBase'
|
||||
|
||||
import { focusClass, modifySize } from 'css/derived/util'
|
||||
import { textSizeToClassName } from '../text/StyledText'
|
||||
import { IconButton, IconButtonProps } from '../button/IconButton'
|
||||
|
||||
import styles from './IconInput.module.scss'
|
||||
|
||||
export type IconSettings = {
|
||||
className?: string
|
||||
icon: IconProps['icon']
|
||||
hideOnFocus?: boolean
|
||||
hidden?: boolean
|
||||
} & ({
|
||||
// If click is specified, it _must_ have an aria label
|
||||
onPress: (event: PressEvent) => void
|
||||
['aria-label']: string
|
||||
} | {
|
||||
onPress?: undefined
|
||||
['aria-label']?: undefined
|
||||
})
|
||||
|
||||
export type IconInputProps = InputProps<{
|
||||
prefixIcon?: IconSettings
|
||||
suffixIcon?: IconSettings
|
||||
} & Omit<InputHTMLAttributes<HTMLInputElement>, 'size'>>
|
||||
& RefAttributes<HTMLInputElement>
|
||||
|
||||
export const IconInput: React.FC<IconInputProps> = (props) => <InputBase {...props} inputRenderer={IconInputComponent} />
|
||||
|
||||
const IconInputComponent: InputRenderer<IconInputProps> = ({ size = 'm', prefixIcon, suffixIcon, className, ...props }, inputProps, inputRef) => {
|
||||
const iconSize = modifySize(size, 2)
|
||||
const { isFocused, focusProps } = useFocusRing({ isTextInput: true })
|
||||
|
||||
const prefixIconProps = prefixIcon ? {
|
||||
className: cs(prefixIcon.onPress ? styles.iconButton : styles.icon, prefixIcon.className),
|
||||
size: iconSize,
|
||||
['aria-label']: prefixIcon['aria-label'],
|
||||
} : {}
|
||||
|
||||
const suffixIconProps = suffixIcon ? {
|
||||
className: cs(suffixIcon.onPress ? styles.iconButton : styles.icon, suffixIcon.className),
|
||||
size: iconSize,
|
||||
['aria-label']: suffixIcon['aria-label'],
|
||||
} : {}
|
||||
|
||||
return (
|
||||
<span className={cs(styles.iconInput, { [focusClass]: isFocused }, className)}>
|
||||
{prefixIcon && (
|
||||
prefixIcon.onPress ? (
|
||||
<IconButton
|
||||
{...prefixIconProps as IconButtonProps}
|
||||
elementType='button'
|
||||
color='white'
|
||||
noBorder={true}
|
||||
ignoreTextCenter={false}
|
||||
icon={prefixIcon.icon}
|
||||
onPress={prefixIcon.onPress}
|
||||
/>
|
||||
) : <Icon {...prefixIconProps} icon={prefixIcon.icon} />
|
||||
)}
|
||||
{/* Apply iconSize to input wrapper, so we have the same em measure */}
|
||||
<div className={cs(textSizeToClassName(iconSize), styles.wrapper)}>
|
||||
<BasicInput
|
||||
{...inputProps}
|
||||
{...focusProps}
|
||||
inputRef={inputRef}
|
||||
textArea={false}
|
||||
className={cs(styles.input)}
|
||||
size={size}
|
||||
/>
|
||||
</div>
|
||||
{suffixIcon && (
|
||||
suffixIcon.onPress ? (
|
||||
<IconButton
|
||||
{...suffixIconProps as IconButtonProps}
|
||||
elementType='button'
|
||||
color='white'
|
||||
noBorder={true}
|
||||
ignoreTextCenter={false}
|
||||
icon={suffixIcon.icon}
|
||||
onPress={suffixIcon.onPress}
|
||||
/>
|
||||
) : <Icon {...suffixIconProps} icon={suffixIcon.icon} />
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import * as React from 'react'
|
||||
import { action } from '@storybook/addon-actions'
|
||||
|
||||
import { createStory, createStorybookConfig } from 'stories/util'
|
||||
|
||||
import { Input as InputComponent } from './Input'
|
||||
import { IconInput as IconInputComponent } from './IconInput'
|
||||
|
||||
import typography from 'css/derived/jsTypography.scss'
|
||||
import { TextSize } from 'css'
|
||||
|
||||
export default createStorybookConfig({
|
||||
title: 'Core/Input',
|
||||
})
|
||||
|
||||
export const Input = createStory(() => (
|
||||
<div>
|
||||
<InputComponent label={{ type: 'aria', contents: 'aria labeled input' }} />
|
||||
<InputComponent label={{
|
||||
type: 'tag',
|
||||
contents: 'Labeled input',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
|
||||
export const Icon = createStory(() => (
|
||||
<div>
|
||||
<div>
|
||||
<input />
|
||||
</div>
|
||||
<div>
|
||||
<IconInputComponent
|
||||
label={{ type: 'aria', contents: 'full width input' }}
|
||||
prefixIcon={{
|
||||
icon: 'home',
|
||||
onPress: action('onPrefixClick'),
|
||||
'aria-label': 'onPrefixClick',
|
||||
}}
|
||||
suffixIcon={{
|
||||
icon: 'times',
|
||||
onPress: action('onSuffixClick'),
|
||||
'aria-label': 'onSuffixClick',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ width: 500 }}>
|
||||
<IconInputComponent
|
||||
label={{ type: 'aria', contents: '500px width input' }}
|
||||
suffixIcon={{
|
||||
icon: 'times',
|
||||
onPress: action('onSuffixClick'),
|
||||
'aria-label': 'onSuffixClick',
|
||||
}}
|
||||
value="This is a very long string in an IconInput. This displays the padding on the input section"
|
||||
/>
|
||||
<IconInputComponent
|
||||
label={{ type: 'aria', contents: '500px width input' }}
|
||||
prefixIcon={{
|
||||
icon: 'home',
|
||||
onPress: action('onPrefixClick'),
|
||||
'aria-label': 'onPrefixClick',
|
||||
}}
|
||||
value="This is a very long string in an IconInput. This displays the padding on the input section"
|
||||
/>
|
||||
<IconInputComponent
|
||||
label={{
|
||||
type: 'tag',
|
||||
contents: 'Labeled IconInput',
|
||||
}}
|
||||
prefixIcon={{
|
||||
icon: 'home',
|
||||
onPress: action('onPrefixClick'),
|
||||
'aria-label': 'onPrefixClick',
|
||||
}}
|
||||
suffixIcon={{
|
||||
icon: 'times',
|
||||
onPress: action('onSuffixClick'),
|
||||
'aria-label': 'onSuffixClick',
|
||||
}}
|
||||
/>
|
||||
<IconInputComponent
|
||||
label={{ type: 'aria', contents: 'trailing button only' }}
|
||||
prefixIcon={{
|
||||
icon: 'home',
|
||||
}}
|
||||
suffixIcon={{
|
||||
icon: 'times',
|
||||
onPress: action('onSuffixClick'),
|
||||
'aria-label': 'onSuffixClick',
|
||||
}}
|
||||
placeholder="The leading icon isn't a button"
|
||||
/>
|
||||
<IconInputComponent
|
||||
label={{ type: 'aria', contents: 'leading button only' }}
|
||||
prefixIcon={{
|
||||
icon: 'home',
|
||||
onPress: action('onPrefixClick'),
|
||||
'aria-label': 'onPrefixClick',
|
||||
}}
|
||||
suffixIcon={{
|
||||
icon: 'times',
|
||||
}}
|
||||
placeholder="The trailing icon isn't a button"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
|
||||
export const IconSizes = createStory(() => (
|
||||
<div>
|
||||
<div style={{ width: 500 }}>
|
||||
{Object.keys(typography).filter((key) => key !== 'type' && !key.startsWith('line-height') && !key.startsWith('text-mono') && key !== 'text-3xl' && key !== 'text-4xl').map((key) => {
|
||||
const size = key.replace('text-', '')
|
||||
|
||||
return (
|
||||
<IconInputComponent
|
||||
key={key}
|
||||
label={{ type: 'aria', contents: `input size ${size}` }}
|
||||
size={size as TextSize}
|
||||
prefixIcon={{
|
||||
icon: 'home',
|
||||
onPress: action('onPrefixClick'),
|
||||
'aria-label': 'onPrefixClick',
|
||||
}}
|
||||
suffixIcon={{
|
||||
icon: 'times',
|
||||
onPress: action('onSuffixClick'),
|
||||
'aria-label': 'onSuffixClick',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
@@ -0,0 +1,4 @@
|
||||
import * as React from 'react'
|
||||
import { basicInputRenderer, InputBase, InputProps } from './InputBase'
|
||||
|
||||
export const Input: React.FC<InputProps<{}>> = (props) => <InputBase {...props} inputRenderer={basicInputRenderer} />
|
||||
@@ -0,0 +1,9 @@
|
||||
@use 'semanticColors' as *;
|
||||
@use 'surfaces' as *;
|
||||
|
||||
input.input {
|
||||
font-weight: normal;
|
||||
|
||||
border: 1px solid $control-border-color;
|
||||
border-radius: $button-radius;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import * as React from 'react'
|
||||
import { CSSProperties, InputHTMLAttributes, MutableRefObject, ReactNode, RefObject, TextareaHTMLAttributes, useMemo, useRef } from 'react'
|
||||
import { useTextField } from 'react-aria'
|
||||
import cs from 'classnames'
|
||||
|
||||
import { ExtractFirstArg } from 'util/types'
|
||||
import { LineHeight, TextSize } from 'css'
|
||||
import { styledTextSizeClassNames } from '../text/StyledText'
|
||||
import { SizingProps } from 'core/shared'
|
||||
|
||||
import styles from './InputBase.module.scss'
|
||||
import { useCombinedRefs } from 'hooks/useCombinedRefs'
|
||||
|
||||
export interface SharedInputBaseProps extends SizingProps {
|
||||
inputRef?: MutableRefObject<HTMLTextAreaElement | HTMLInputElement | null> | null
|
||||
|
||||
label: {
|
||||
type: 'tag'
|
||||
contents: ReactNode
|
||||
labelClassName?: string
|
||||
size?: TextSize
|
||||
lineHeight?: LineHeight
|
||||
} | {
|
||||
type: 'aria'
|
||||
contents: string
|
||||
}
|
||||
|
||||
/**
|
||||
* If true, render as a textarea (multiline) instead of an input. Defaults to false
|
||||
*/
|
||||
textArea?: boolean
|
||||
}
|
||||
|
||||
export type InputProps<T> = SharedInputBaseProps & {
|
||||
className?: string
|
||||
style?: CSSProperties
|
||||
} & T
|
||||
|
||||
export type InputRenderer<T> = (componentProps: Omit<InputProps<T>, 'label'>, inputProps: InputHTMLAttributes<HTMLInputElement>, inputRef: RefObject<HTMLTextAreaElement | HTMLInputElement>) => ReactNode
|
||||
|
||||
export type InputBaseProps<T> = SharedInputBaseProps & {
|
||||
inputRenderer: InputRenderer<T>
|
||||
} & T
|
||||
|
||||
export const InputBase = <T, >({ inputRenderer, label, textArea, inputRef: externalInputRef = null, ...props }: InputBaseProps<T>) => {
|
||||
const inputRef = useRef<HTMLTextAreaElement | HTMLInputElement>(null)
|
||||
|
||||
useCombinedRefs(inputRef, externalInputRef)
|
||||
|
||||
const textFieldProps = useMemo((): ExtractFirstArg<typeof useTextField> => {
|
||||
const newProps: ExtractFirstArg<typeof useTextField> = {
|
||||
...props,
|
||||
inputElementType: textArea ? 'textarea' : 'input',
|
||||
}
|
||||
|
||||
if (label.type === 'aria') {
|
||||
newProps['aria-label'] = label.contents
|
||||
} else if (label.type === 'tag') {
|
||||
newProps.label = label.contents
|
||||
}
|
||||
|
||||
return newProps
|
||||
}, [label, textArea, props])
|
||||
|
||||
const { inputProps, labelProps } = useTextField(textFieldProps, inputRef)
|
||||
|
||||
return (
|
||||
<>
|
||||
{label.type === 'tag' && (
|
||||
<label {...labelProps} className={cs(styledTextSizeClassNames(label.size, label.lineHeight), labelProps.className)}>
|
||||
{label.contents}
|
||||
</label>
|
||||
)}
|
||||
{/* TODO: This cast is incorrect. It can be textarea */}
|
||||
{inputRenderer(props as Omit<InputProps<T>, 'label'>, inputProps as InputHTMLAttributes<HTMLInputElement>, inputRef)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export type BasicInputProps = SizingProps & Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> & {
|
||||
inputRef: RefObject<HTMLTextAreaElement | HTMLInputElement>
|
||||
|
||||
/**
|
||||
* If true, render as a textarea (multiline) instead of an input. Defaults to false
|
||||
*/
|
||||
textArea?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* **Note:** Should not be directly rendered in app code. This should only be provided in an `inputRenderer` function
|
||||
*/
|
||||
export const BasicInput: React.FC<BasicInputProps> = ({ inputRef, className, size, lineHeight, textArea, ...props }) => {
|
||||
const textClass = styledTextSizeClassNames(size, lineHeight)
|
||||
|
||||
return textArea
|
||||
? <textarea {...props as TextareaHTMLAttributes<HTMLTextAreaElement>} ref={inputRef as RefObject<HTMLTextAreaElement>} className={cs(textClass, styles.input, className)} />
|
||||
: <input {...props as InputHTMLAttributes<HTMLInputElement>} ref={inputRef as RefObject<HTMLInputElement>} className={cs(textClass, styles.input, className)} />
|
||||
}
|
||||
|
||||
// TODO: The types here are not as elegant as I would like
|
||||
export const basicInputRenderer: InputRenderer<Omit<BasicInputProps, 'inputRef'>> = (props, inputProps, inputRef) =>
|
||||
<BasicInput {...inputProps as Omit<InputHTMLAttributes<HTMLInputElement>, 'size'>} {...props} inputRef={inputRef} className={cs(inputProps.className, props.className)} />
|
||||
@@ -0,0 +1,19 @@
|
||||
import { LineHeight, TextSize } from 'css'
|
||||
|
||||
export interface CoreComponent {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export interface SizingProps {
|
||||
/**
|
||||
* Defaults to 'm'
|
||||
*/
|
||||
size?: TextSize
|
||||
|
||||
/**
|
||||
* Defaults to 'normal'
|
||||
*/
|
||||
lineHeight?: LineHeight
|
||||
}
|
||||
|
||||
export type TextSizableComponent = CoreComponent & SizingProps
|
||||
@@ -0,0 +1,43 @@
|
||||
import * as React from 'react'
|
||||
import { Story } from '@storybook/react'
|
||||
|
||||
import { createStory, createStorybookConfig } from 'stories/util'
|
||||
|
||||
import { Elevation as ElevationComponent } from './Elevation'
|
||||
import { lorem } from 'util/lorem'
|
||||
import { StoryHighlightWrapper } from 'util/storybook/storyHighlightWrapper/StoryHighlightWrapper'
|
||||
import { SurfaceElevation } from 'css'
|
||||
|
||||
import surfaces from 'css/derived/jsSurfaces.scss'
|
||||
|
||||
export default createStorybookConfig({
|
||||
title: 'Core/Surfaces/Elevation',
|
||||
argTypes: {
|
||||
elevation: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: Object.keys(surfaces).map((key) => key.replace('shadow-', '')),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const Template: Story<{
|
||||
elevation: SurfaceElevation
|
||||
}> = ({ elevation }) => (
|
||||
<div>
|
||||
<ElevationComponent elevation={elevation}>
|
||||
{lorem}
|
||||
</ElevationComponent>
|
||||
<br />
|
||||
<StoryHighlightWrapper>
|
||||
<ElevationComponent elevation={elevation}>
|
||||
{lorem}
|
||||
</ElevationComponent>
|
||||
</StoryHighlightWrapper>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const Elevation = createStory(Template, {
|
||||
elevation: 'bordered',
|
||||
})
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as React from 'react'
|
||||
import cs from 'classnames'
|
||||
|
||||
import { SurfaceElevation } from 'css'
|
||||
|
||||
export interface ElevationProps extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
|
||||
className?: string
|
||||
|
||||
/**
|
||||
* Defaults to 'flat'
|
||||
*/
|
||||
elevation?: SurfaceElevation
|
||||
}
|
||||
|
||||
export const Elevation: React.FC<ElevationProps> = ({ className, elevation, children, ...props }) => (
|
||||
<div {...props} className={cs(`depth-${elevation ?? 'flat'}`, className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as React from 'react'
|
||||
import { Story } from '@storybook/react'
|
||||
|
||||
import { createStory, createStorybookConfig } from 'stories/util'
|
||||
|
||||
import { PaddedBox as PaddedComponent } from './PaddedBox'
|
||||
import { lorem } from 'util/lorem'
|
||||
import { StoryHighlightWrapper } from 'util/storybook/storyHighlightWrapper/StoryHighlightWrapper'
|
||||
import { Spacing } from 'css'
|
||||
|
||||
import spacing from 'css/derived/jsSpacing.scss'
|
||||
|
||||
export default createStorybookConfig({
|
||||
title: 'Core/Surfaces/PaddedBox',
|
||||
argTypes: {
|
||||
padding: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: Object.keys(spacing).map((key) => key.replace('space-', '')),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const Template: Story<{
|
||||
padding: Spacing
|
||||
}> = ({ padding }) => (
|
||||
<div>
|
||||
<StoryHighlightWrapper>
|
||||
<PaddedComponent padding={padding}>
|
||||
{lorem}
|
||||
</PaddedComponent>
|
||||
</StoryHighlightWrapper>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const PaddedBox = createStory(Template, {
|
||||
padding: 'm',
|
||||
})
|
||||
|
||||
// Required to prevent Storybook from separating into two words and creating unnecessary nesting
|
||||
PaddedBox.storyName = 'PaddedBox'
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as React from 'react'
|
||||
import { CSSProperties } from 'react'
|
||||
import cs from 'classnames'
|
||||
|
||||
import { Spacing } from 'css'
|
||||
import { CoreComponent } from 'core/shared'
|
||||
import { paddingClass } from 'css/derived/util'
|
||||
|
||||
export interface PaddedBoxProps extends CoreComponent, React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
|
||||
style?: CSSProperties
|
||||
|
||||
/**
|
||||
* Defaults to 'm'
|
||||
*/
|
||||
padding?: Spacing
|
||||
}
|
||||
|
||||
export const PaddedBox: React.FC<PaddedBoxProps> = ({ className, style, padding, children, ...props }) => (
|
||||
<div {...props} className={cs(paddingClass(padding ?? 'm'), className)} style={style}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
@@ -0,0 +1,39 @@
|
||||
import * as React from 'react'
|
||||
import { Story } from '@storybook/react'
|
||||
|
||||
import { StyledText as TextComponent } from './StyledText'
|
||||
import { createStory, createStorybookConfig } from 'stories/util'
|
||||
|
||||
import typography from 'css/derived/jsTypography.scss'
|
||||
import { TextSize } from 'css'
|
||||
import { lorem } from 'util/lorem'
|
||||
|
||||
export default createStorybookConfig({
|
||||
title: 'Core/StyledText',
|
||||
})
|
||||
|
||||
const Template: Story = () => (
|
||||
<div>
|
||||
{Object.keys(typography).filter((key) => !key.startsWith('line-height')).map((key) => {
|
||||
return (
|
||||
<>
|
||||
<h3>
|
||||
<TextComponent size='mono-m'>
|
||||
{key}
|
||||
</TextComponent>
|
||||
</h3>
|
||||
<p key={key}>
|
||||
<TextComponent size={key.replace('text-', '') as TextSize}>
|
||||
{lorem}
|
||||
</TextComponent>
|
||||
</p>
|
||||
<hr />
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
|
||||
export const StyledText = createStory(Template)
|
||||
|
||||
StyledText.storyName = 'StyledText'
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as React from 'react'
|
||||
import { CSSProperties } from 'react'
|
||||
import cs from 'classnames'
|
||||
|
||||
import { LineHeight, TextSize } from 'css'
|
||||
import { TextSizableComponent } from '../shared'
|
||||
|
||||
export type StyledTextProps = {
|
||||
style?: CSSProperties
|
||||
} & TextSizableComponent & React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>
|
||||
|
||||
// Named "StyledText" instead of "Text" to avoid collision with top level React type
|
||||
export const StyledText: React.FC<StyledTextProps> = ({ className, size, lineHeight, children, ...props }) => {
|
||||
return (
|
||||
<span {...props} className={cs(styledTextSizeClassNames(size, lineHeight), className)}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export const styledTextSizeClassNames = (size: TextSize = 'm', lineHeight: LineHeight = 'normal'): string =>
|
||||
`${textSizeToClassName(size)} ${lineHeightToClassName(lineHeight)}`
|
||||
|
||||
export const textSizeToClassName = (size: TextSize): string => `text-${size}`
|
||||
|
||||
const lineHeightToClassName = (lineHeight: LineHeight): string => `line-height-${lineHeight}`
|
||||
@@ -0,0 +1,139 @@
|
||||
@import './func.scss';
|
||||
|
||||
$colors: (
|
||||
metal-100: rgba(0, 0, 0, 1), // #000000;
|
||||
metal-90: rgba(33, 36, 38, 1), // #212426;
|
||||
metal-80: rgba(45, 49, 52, 1), // #2d3134;
|
||||
metal-70: rgba(62, 67, 71, 1), // #3e4347;
|
||||
metal-60: rgba(83, 90, 95, 1), // #535a5f;
|
||||
metal-50: rgba(107, 116, 123, 1), // #6b747b;
|
||||
metal-40: rgba(135, 144, 151, 1), // #879097;
|
||||
metal-30: rgba(160, 167, 172, 1), // #a0a7ac;
|
||||
metal-20: rgba(190, 194, 198, 1), // #bec2c6;
|
||||
metal-10: rgba(230, 232, 234, 1), // #e6e8ea;
|
||||
metal-05: rgba(244, 245, 246, 1), // #f4f5f6;
|
||||
metal-00: rgba(255, 255, 255, 1), // #ffffff;
|
||||
red-70: rgba(153, 11, 18, 1), // #990b12;
|
||||
red-60: rgba(191, 13, 22, 1), // #bf0d16;
|
||||
red-50: rgba(224, 16, 26, 1), // #e0101a;
|
||||
red-40: rgba(241, 55, 64, 1), // #f13740;
|
||||
chill-90: rgba(8, 33, 68, 1), // #082144;
|
||||
chill-80: rgba(12, 49, 100, 1), // #0c3164;
|
||||
chill-70: rgba(16, 66, 137, 1), // #104289;
|
||||
chill-60: rgba(21, 86, 178, 1), // #1556b2;
|
||||
chill-50: rgba(27, 111, 228, 1), // #1b6fe4;
|
||||
chill-40: rgba(73, 142, 238, 1), // #498eee;
|
||||
chill-30: rgba(118, 168, 239, 1), // #76a8ef;
|
||||
chill-20: rgba(164, 197, 244, 1), // #a4c5f4;
|
||||
chill-10: rgba(222, 235, 252, 1), // #deebfc;
|
||||
chill-05: rgba(237, 243, 253, 1), // #edf3fd;
|
||||
olive-60: rgba(93, 100, 12, 1), // #5d640c;
|
||||
olive-50: rgba(114, 123, 15, 1), // #727b0f;
|
||||
olive-40: rgba(143, 154, 25, 1), // #8f9a19;
|
||||
olive-30: rgba(211, 228, 27, 1), // #d3e41b;
|
||||
olive-20: rgba(225, 237, 100, 1), // #e1ed64;
|
||||
olive-10: rgba(238, 245, 168, 1), // #eef5a8;
|
||||
olive-05: rgba(246, 250, 209, 1), // #f6fad1;
|
||||
papaya-60: rgba(143, 68, 10, 1), // #8f440a;
|
||||
papaya-50: rgba(190, 90, 14, 1), // #be5a0e;
|
||||
papaya-40: rgba(235, 107, 10, 1), // #eb6b0a;
|
||||
papaya-30: rgba(242, 141, 64, 1), // #f28d40;
|
||||
papaya-20: rgba(246, 175, 121, 1), // #f6af79;
|
||||
papaya-10: rgba(252, 229, 212, 1), // #fce5d4;
|
||||
papaya-05: rgba(253, 241, 231, 1), // #fdf1e7;
|
||||
green-60: rgba(41, 102, 10, 1), // #29660a;
|
||||
green-50: rgba(54, 133, 15, 1), // #36850f;
|
||||
green-40: rgba(68, 164, 20, 1), // #44a414;
|
||||
green-30: rgba(79, 191, 23, 1), // #4fbf17;
|
||||
green-20: rgba(107, 219, 51, 1), // #6bdb33;
|
||||
green-10: rgba(204, 244, 185, 1), // #ccf4b9;
|
||||
green-05: rgba(236, 251, 228, 1), // #ecfbe4;
|
||||
cran-50: rgba(228, 28, 95, 1), // #e41c5f;
|
||||
brand-00: rgba(28, 228, 150, 1), // #1ce496;
|
||||
// TODO: Rewrite these names
|
||||
brand-01: rgba(8, 41, 63, 1), // #08293f;
|
||||
accent-00: rgba(177, 99, 255, 1), // #b163ff;
|
||||
accent-01: rgba(54, 197, 255, 1), // #36c5ff;
|
||||
accent-02: rgba(230, 255, 30, 1), // #e6ff1e;
|
||||
);
|
||||
|
||||
// TODO: Is this needed?
|
||||
// :root {
|
||||
// @each $name, $color in $colors {
|
||||
// .text-#{"" + $name} {
|
||||
// color: $color;
|
||||
// }
|
||||
// .bg-#{"" + $name} {
|
||||
// background-color: $color;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// --- Color Variables
|
||||
|
||||
@function color($name: string) {
|
||||
@return var(--#{"" + $name});
|
||||
}
|
||||
|
||||
// Must be manually written out as SASS does not support dynamic variable creation
|
||||
// Exposes SASS variables as references to CSS variables
|
||||
$metal-100: color('metal-100');
|
||||
$metal-90: color('metal-90');
|
||||
$metal-80: color('metal-80');
|
||||
$metal-70: color('metal-70');
|
||||
$metal-60: color('metal-60');
|
||||
$metal-50: color('metal-50');
|
||||
$metal-40: color('metal-40');
|
||||
$metal-30: color('metal-30');
|
||||
$metal-20: color('metal-20');
|
||||
$metal-10: color('metal-10');
|
||||
$metal-05: color('metal-05');
|
||||
$metal-00: color('metal-00');
|
||||
|
||||
$red-70: color('red-70');
|
||||
$red-60: color('red-60');
|
||||
$red-50: color('red-50');
|
||||
$red-40: color('red-40');
|
||||
|
||||
$chill-90: color('chill-90');
|
||||
$chill-80: color('chill-80');
|
||||
$chill-70: color('chill-70');
|
||||
$chill-60: color('chill-60');
|
||||
$chill-50: color('chill-50');
|
||||
$chill-40: color('chill-40');
|
||||
$chill-30: color('chill-30');
|
||||
$chill-20: color('chill-20');
|
||||
$chill-10: color('chill-10');
|
||||
$chill-05: color('chill-05');
|
||||
|
||||
$olive-60: color('olive-60');
|
||||
$olive-50: color('olive-50');
|
||||
$olive-40: color('olive-40');
|
||||
$olive-30: color('olive-30');
|
||||
$olive-20: color('olive-20');
|
||||
$olive-10: color('olive-10');
|
||||
$olive-05: color('olive-05');
|
||||
|
||||
$papaya-60: color('papaya-60');
|
||||
$papaya-50: color('papaya-50');
|
||||
$papaya-40: color('papaya-40');
|
||||
$papaya-30: color('papaya-30');
|
||||
$papaya-20: color('papaya-20');
|
||||
$papaya-10: color('papaya-10');
|
||||
$papaya-05: color('papaya-05');
|
||||
|
||||
$green-60: color('green-60');
|
||||
$green-50: color('green-50');
|
||||
$green-40: color('green-40');
|
||||
$green-30: color('green-30');
|
||||
$green-20: color('green-20');
|
||||
$green-10: color('green-10');
|
||||
$green-05: color('green-05');
|
||||
|
||||
$cran-50: color('cran-50');
|
||||
|
||||
$brand-00: color('brand-00');
|
||||
$brand-01: color('brand-01');
|
||||
$accent-00: color('accent-00');
|
||||
$accent-01: color('accent-01');
|
||||
$accent-02: color('accent-02');
|
||||
@@ -0,0 +1,78 @@
|
||||
@use 'baseColors' as *;
|
||||
@use 'semanticColors' as *;
|
||||
@use 'spacing' as *;
|
||||
@use 'surfaces' as *;
|
||||
@use 'spacing' as *;
|
||||
@use 'typography' as *;
|
||||
|
||||
// BaseColors
|
||||
:root {
|
||||
@each $name, $color in $colors {
|
||||
--#{"" + $name}: #{$color};
|
||||
}
|
||||
}
|
||||
|
||||
// SemanticColors
|
||||
// Write semantic CSS color variables to root
|
||||
// **NOTE**: Most variables do not need to be exposed as a CSS variable
|
||||
:root {
|
||||
// See def in semanticColors.scss
|
||||
--black-rgb-color: #{extract-rgb('metal-100')};
|
||||
}
|
||||
|
||||
// Spacing
|
||||
@each $name, $text-def in $spacing {
|
||||
$suffix: str-replace('' + $name, 'space-', '');
|
||||
|
||||
.#{'padding-' + $suffix} {
|
||||
@include padding($suffix)
|
||||
}
|
||||
}
|
||||
|
||||
// Surfaces
|
||||
@each $name, $def in $shadow {
|
||||
$suffix: str-replace('' + $name, 'shadow-', '');
|
||||
|
||||
.#{'depth-' + $suffix} {
|
||||
@include depth($suffix)
|
||||
}
|
||||
}
|
||||
|
||||
.focused {
|
||||
@include focused;
|
||||
}
|
||||
|
||||
// Typography
|
||||
// See typography.scss
|
||||
:root {
|
||||
--font-stack-sans: #{$internal-font-stack-sans};
|
||||
--font-stack-mono: #{$internal-font-stack-mono};
|
||||
}
|
||||
|
||||
// :root {
|
||||
@each $name, $text-def in $text {
|
||||
.#{$name} {
|
||||
@include text(str-replace($name, 'text-', ''))
|
||||
}
|
||||
}
|
||||
|
||||
.text-mono-m {
|
||||
@include text-mono-m;
|
||||
}
|
||||
|
||||
.text-mono-s {
|
||||
@include text-mono-s;
|
||||
}
|
||||
|
||||
.line-height-normal {
|
||||
@include line-height-normal;
|
||||
}
|
||||
|
||||
.line-height-condensed {
|
||||
@include line-height-condensed;
|
||||
}
|
||||
|
||||
.line-height-tight {
|
||||
@include line-height-tight;
|
||||
}
|
||||
// }
|
||||
@@ -0,0 +1,8 @@
|
||||
@use '../baseColors' as *;
|
||||
|
||||
// Exposes $color, stripped of hyphens, as a JS variable when directly imported
|
||||
:export {
|
||||
@each $name, $color in $colors {
|
||||
#{str-replace("" + $name, '-', '')}: #{$color};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
export type Styles = {
|
||||
accent00: string;
|
||||
accent01: string;
|
||||
accent02: string;
|
||||
brand00: string;
|
||||
brand01: string;
|
||||
chill05: string;
|
||||
chill10: string;
|
||||
chill20: string;
|
||||
chill30: string;
|
||||
chill40: string;
|
||||
chill50: string;
|
||||
chill60: string;
|
||||
chill70: string;
|
||||
chill80: string;
|
||||
chill90: string;
|
||||
cran50: string;
|
||||
green05: string;
|
||||
green10: string;
|
||||
green20: string;
|
||||
green30: string;
|
||||
green40: string;
|
||||
green50: string;
|
||||
green60: string;
|
||||
metal00: string;
|
||||
metal05: string;
|
||||
metal10: string;
|
||||
metal100: string;
|
||||
metal20: string;
|
||||
metal30: string;
|
||||
metal40: string;
|
||||
metal50: string;
|
||||
metal60: string;
|
||||
metal70: string;
|
||||
metal80: string;
|
||||
metal90: string;
|
||||
olive05: string;
|
||||
olive10: string;
|
||||
olive20: string;
|
||||
olive30: string;
|
||||
olive40: string;
|
||||
olive50: string;
|
||||
olive60: string;
|
||||
papaya05: string;
|
||||
papaya10: string;
|
||||
papaya20: string;
|
||||
papaya30: string;
|
||||
papaya40: string;
|
||||
papaya50: string;
|
||||
papaya60: string;
|
||||
red40: string;
|
||||
red50: string;
|
||||
red60: string;
|
||||
red70: string;
|
||||
};
|
||||
|
||||
export type ClassNames = keyof Styles;
|
||||
|
||||
declare const styles: Styles;
|
||||
|
||||
export default styles;
|
||||
@@ -0,0 +1,8 @@
|
||||
@use '../spacing.scss' as *;
|
||||
|
||||
// Exposes $spacing, as a JS variable when directly imported
|
||||
:export {
|
||||
@each $name, $size in $spacing {
|
||||
#{$name}: #{$size};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export type Styles = {
|
||||
"padding-2xl": string;
|
||||
"padding-3xl": string;
|
||||
"padding-4xl": string;
|
||||
"padding-l": string;
|
||||
"padding-m": string;
|
||||
"padding-ml": string;
|
||||
"padding-ms": string;
|
||||
"padding-s": string;
|
||||
"padding-xl": string;
|
||||
"padding-xs": string;
|
||||
"space-2xl": string;
|
||||
"space-3xl": string;
|
||||
"space-4xl": string;
|
||||
"space-l": string;
|
||||
"space-m": string;
|
||||
"space-ml": string;
|
||||
"space-ms": string;
|
||||
"space-s": string;
|
||||
"space-xl": string;
|
||||
"space-xs": string;
|
||||
};
|
||||
|
||||
export type ClassNames = keyof Styles;
|
||||
|
||||
declare const styles: Styles;
|
||||
|
||||
export default styles;
|
||||
@@ -0,0 +1,8 @@
|
||||
@use '../surfaces.scss' as *;
|
||||
|
||||
// Exposes $shadow as a JS variable when directly imported
|
||||
:export {
|
||||
@each $name, $shadow in $shadow {
|
||||
#{$name}: #{$shadow};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
export type Styles = {
|
||||
"depth-3": string;
|
||||
"depth-4": string;
|
||||
"depth-6": string;
|
||||
"depth-bordered": string;
|
||||
"depth-flat": string;
|
||||
"depth-float": string;
|
||||
"depth-inset-slight": string;
|
||||
"depth-inset-well": string;
|
||||
"depth-slight": string;
|
||||
"shadow-3": string;
|
||||
"shadow-4": string;
|
||||
"shadow-6": string;
|
||||
"shadow-bordered": string;
|
||||
"shadow-flat": string;
|
||||
"shadow-float": string;
|
||||
"shadow-inset-slight": string;
|
||||
"shadow-inset-well": string;
|
||||
"shadow-slight": string;
|
||||
};
|
||||
|
||||
export type ClassNames = keyof Styles;
|
||||
|
||||
declare const styles: Styles;
|
||||
|
||||
export default styles;
|
||||
@@ -0,0 +1,18 @@
|
||||
@use '../typography.scss' as *;
|
||||
@use '../func.scss' as *;
|
||||
|
||||
// Exposes $text, as a JS variable when directly imported
|
||||
// This is the only way for JS to directly receive the exposed `.text-*` classes
|
||||
:export {
|
||||
@each $name, $text-def in $text {
|
||||
#{$name}: text(#{str-replace("" + $name, 'text-', '')})
|
||||
}
|
||||
|
||||
// TODO: Can this be improved?
|
||||
text-mono-m: text-mono-m;
|
||||
text-mono-s: text-mono-s;
|
||||
|
||||
line-height-normal: line-height-normal;
|
||||
line-height-condensed: line-height-condensed;
|
||||
line-height-tight: line-height-tight;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export type Styles = {
|
||||
"line-height-condensed": string;
|
||||
"line-height-normal": string;
|
||||
"line-height-tight": string;
|
||||
"text-2xl": string;
|
||||
"text-3xl": string;
|
||||
"text-4xl": string;
|
||||
"text-l": string;
|
||||
"text-m": string;
|
||||
"text-ml": string;
|
||||
"text-mono-m": string;
|
||||
"text-mono-s": string;
|
||||
"text-ms": string;
|
||||
"text-s": string;
|
||||
"text-xl": string;
|
||||
"text-xs": string;
|
||||
};
|
||||
|
||||
export type ClassNames = keyof Styles;
|
||||
|
||||
declare const styles: Styles;
|
||||
|
||||
export default styles;
|
||||
@@ -0,0 +1,32 @@
|
||||
/* eslint-disable */
|
||||
import type colors from './jsColors.scss'
|
||||
import type { ClassNames as JsSpacing } from './jsSpacing.scss'
|
||||
import type { ClassNames as JsTypography } from './jsTypography.scss'
|
||||
import type {ClassNames as JsSurface } from './jsSurfaces.scss'
|
||||
|
||||
type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
|
||||
type TwoDigit = `${Digit}${Digit}`
|
||||
|
||||
/**
|
||||
* Retrieve all names in `TString` that are followed by a suffix `TSuffix`
|
||||
*/
|
||||
type ExtractStringBeforeSuffix<TString extends string, TSuffix extends string> = TString extends `${infer S}${TSuffix}` ? S : never
|
||||
|
||||
/**
|
||||
* Retrieve all substrings following the prefix `TPrefix` in `TString`
|
||||
*/
|
||||
type ExtractStringAfterPrefix<TString extends string, TPrefix extends string> = TString extends `${TPrefix}${infer S}` ? S : never
|
||||
|
||||
/**
|
||||
* Convert numbered names resulting from collapsing CSS class names into their original, hyphenated form
|
||||
*/
|
||||
type HyphenateNumberedName<T extends string> = {
|
||||
[Key in ExtractStringBeforeSuffix<T, TwoDigit>]: `${Key}-${ExtractStringAfterPrefix<T, Key>}`
|
||||
}[ExtractStringBeforeSuffix<T, TwoDigit>]
|
||||
|
||||
export type Color = HyphenateNumberedName<keyof typeof colors>
|
||||
export type Spacing = ExtractStringAfterPrefix<JsSpacing, 'space-'>
|
||||
export type SurfaceElevation = ExtractStringAfterPrefix<JsSurface, 'shadow-'>
|
||||
|
||||
export type TextSize = ExtractStringAfterPrefix<JsTypography, 'text-'>
|
||||
export type LineHeight = ExtractStringAfterPrefix<JsTypography, 'line-height-'>
|
||||
@@ -0,0 +1,37 @@
|
||||
import { TextSize } from './types'
|
||||
|
||||
import jsTypography from './jsTypography.scss'
|
||||
|
||||
const sizes: TextSize[] = ['xs', 's', 'ms', 'm', 'ml', 'l', 'xl', '2xl', '3xl', '4xl']
|
||||
const monoSizes: TextSize[] = ['mono-s', 'mono-m']
|
||||
|
||||
/**
|
||||
* Converts a t-shirt typography size into the corresponding REM float
|
||||
*/
|
||||
export const typographySizeFromSize = (size: TextSize): number => {
|
||||
const key = `text-${size}` as const
|
||||
|
||||
return parseFloat(jsTypography[key])
|
||||
}
|
||||
|
||||
export const modifySize = (size: TextSize, numberOfSizes: number): TextSize => {
|
||||
const sizeArray = !size.startsWith('mono') ? sizes : monoSizes
|
||||
|
||||
const index = sizeArray.indexOf(size)
|
||||
|
||||
if (index === -1) {
|
||||
throw new Error(`Could not find size ${size}`)
|
||||
}
|
||||
|
||||
if (numberOfSizes > 0 && index + numberOfSizes < sizeArray.length) {
|
||||
return sizeArray[index + numberOfSizes]
|
||||
} else if (numberOfSizes < 0 && index + numberOfSizes > -1) {
|
||||
return sizeArray[index + numberOfSizes]
|
||||
}
|
||||
|
||||
throw new Error(`Cannot add ${numberOfSizes} to size ${size}`)
|
||||
}
|
||||
|
||||
export const paddingClass = (padding: TextSize): string => `padding-${padding}`
|
||||
|
||||
export const focusClass = 'focused'
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Replace `$search` with `$replace` in `$string`
|
||||
* @author Hugo Giraudel
|
||||
* @param {String} $string - Initial string
|
||||
* @param {String} $search - Substring to replace
|
||||
* @param {String} $replace ('') - New value
|
||||
* @return {String} - Updated string
|
||||
*/
|
||||
// Taken from https://gist.github.com/PuddingNL/51866d4b9f1151963fbd973bf1d66116
|
||||
@function str-replace($string, $search, $replace: '') {
|
||||
$index: str-index($string, $search);
|
||||
|
||||
@if $index {
|
||||
@return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);
|
||||
}
|
||||
|
||||
@return $string;
|
||||
}
|
||||
|
||||
@function change-rem-unit-to-em($number) {
|
||||
@return ($number / 1rem) + 0em;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@forward 'baseColors';
|
||||
@forward 'semanticColors';
|
||||
@forward 'surfaces';
|
||||
@forward 'typography';
|
||||
@forward 'spacing';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './derived/types'
|
||||
@@ -0,0 +1,33 @@
|
||||
@use 'baseColors' as *;
|
||||
@use 'func';
|
||||
|
||||
/**
|
||||
* Extracts RGB color channels from a named color definition
|
||||
*/
|
||||
@function extract-rgb($color-name: string) {
|
||||
$color: map-get($colors, $color-name);
|
||||
|
||||
@if $color {
|
||||
@return red($color), green($color), blue($color);
|
||||
}
|
||||
|
||||
@return null;
|
||||
}
|
||||
|
||||
// Provides raw black as a RGB number tuple for substitution into alpha modifying situations (shadows)
|
||||
// See exposed variable in export.scss
|
||||
$black-rgb-color: var(--black-rgb-color);
|
||||
|
||||
$control-border-color: $metal-30;
|
||||
$control-focus-color: $chill-40;
|
||||
|
||||
$button-blue-color: $chill-50;
|
||||
$button-blue-hover-color: $chill-60;
|
||||
$button-blue-push-color: $chill-70;
|
||||
|
||||
$button-white-color: $metal-00;
|
||||
$button-white-hover-color: $metal-05;
|
||||
$button-white-push-color: $metal-10;
|
||||
|
||||
$control-text-color-white: $metal-05;
|
||||
$control-text-color-black: $metal-90;
|
||||
@@ -0,0 +1,26 @@
|
||||
@use 'func' as *;
|
||||
|
||||
$_base-space: 1rem;
|
||||
|
||||
$spacing: (
|
||||
space-xs: 0.25 * $_base-space, // 4px
|
||||
space-s: 0.5 * $_base-space, // 8px
|
||||
|
||||
space-ms: 0.75 * $_base-space, // 12px
|
||||
space-m: 1 * $_base-space, // 16px
|
||||
space-ml: 1.25 * $_base-space, // 20px
|
||||
|
||||
space-l: 1.5 * $_base-space, // 24px
|
||||
space-xl: 2 * $_base-space, // 32px
|
||||
space-2xl: 2.5 * $_base-space, // 40px
|
||||
space-3xl: 3 * $_base-space, // 48px
|
||||
space-4xl: 4 * $_base-space, // 64px
|
||||
);
|
||||
|
||||
@function spacing($name: string) {
|
||||
@return map-get($spacing, "space-" + $name);
|
||||
}
|
||||
|
||||
@mixin padding($name: string) {
|
||||
padding: spacing($name);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
@use 'semanticColors' as *;
|
||||
@use 'func' as *;
|
||||
|
||||
// --- Shadows
|
||||
|
||||
// TODO: Need better semantic names
|
||||
$shadow: (
|
||||
shadow-flat: none,
|
||||
shadow-slight: 0 1px 2px 0 rgba($black-rgb-color, 0.05),
|
||||
shadow-bordered: (0 1px 3px 0 rgba($black-rgb-color, 0.1), 0 1px 2px 0 rgba($black-rgb-color, 0.06)),
|
||||
shadow-3: (0 4px 6px -1px rgba($black-rgb-color, 0.1), 0 2px 4px -1px rgba($black-rgb-color, 0.06)),
|
||||
shadow-4: (0 10px 15px -3px rgba($black-rgb-color, 0.1), 0 4px 6px -2px rgba($black-rgb-color, 0.05)),
|
||||
shadow-popup: (0 20px 25px -5px rgba($black-rgb-color, 0.1), 0 10px 10px -5px rgba($black-rgb-color, 0.04)),
|
||||
shadow-6: (0 25px 50px -12px rgba($black-rgb-color, 0.25)),
|
||||
shadow-inset-slight: inset 0 2px 4px 0 rgba($black-rgb-color, 0.06),
|
||||
shadow-inset-well: inset 0 3px 5px 0 rgba($black-rgb-color, 0.1),
|
||||
);
|
||||
|
||||
// $shadow-outline: 0 0 0 3px rgba(66, 153, 225, 0.5);
|
||||
|
||||
$button-radius: 0.5rem; // 8px
|
||||
|
||||
@function shadow($name: string) {
|
||||
@return map-get($shadow, "shadow-" + $name);
|
||||
}
|
||||
|
||||
@mixin depth($name: string) {
|
||||
box-shadow: shadow($name);
|
||||
}
|
||||
|
||||
@mixin focused {
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
border: 2px solid $control-focus-color;
|
||||
border-radius: $button-radius;
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
@use 'func' as *;
|
||||
|
||||
// --- Font Families
|
||||
|
||||
/**
|
||||
* Do not directly reference. Use the $font-stack-sans and $font-stack-mono variables instead
|
||||
*/
|
||||
$internal-font-stack-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
$internal-font-stack-mono: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
|
||||
// See export.scss
|
||||
$font-stack-sans: var(--font-stack-sans);
|
||||
$font-stack-mono: var(--font-stack-mono);
|
||||
|
||||
// --- Text Sizes/Scale
|
||||
// Chosen to produce integer sizes from a base font size of 16px
|
||||
|
||||
$text: (
|
||||
text-xs: (
|
||||
size: 0.5rem, // 8px
|
||||
),
|
||||
text-s: (
|
||||
size: 0.75rem, // 12px
|
||||
),
|
||||
text-ms: (
|
||||
size: 0.875rem, // 14px
|
||||
),
|
||||
text-m: (
|
||||
size: 1rem, // 16px
|
||||
weight: bolder,
|
||||
),
|
||||
text-ml: (
|
||||
size: 1.25rem, // 18px
|
||||
weight: bolder,
|
||||
),
|
||||
text-l: (
|
||||
size: 1.5rem, // 24px
|
||||
weight: bolder,
|
||||
),
|
||||
text-xl: (
|
||||
size: 2rem, // 32px
|
||||
weight: bolder,
|
||||
),
|
||||
text-2xl: (
|
||||
size: 2.5rem, // 40px
|
||||
weight: bolder,
|
||||
),
|
||||
text-3xl: (
|
||||
size: 3rem, // 48px
|
||||
weight: bolder,
|
||||
),
|
||||
text-4xl: (
|
||||
size: 4rem, // 64px
|
||||
weight: bolder,
|
||||
)
|
||||
);
|
||||
|
||||
@function text($name: string) {
|
||||
@return map-get(map-get($text, "text-" + #{$name}), size);
|
||||
}
|
||||
|
||||
// --- Line heights
|
||||
|
||||
$lh-tight: 1;
|
||||
$lh-condensed: 1.25;
|
||||
$lh-normal: 1.5;
|
||||
|
||||
// --- Mixins
|
||||
|
||||
@mixin _text-base {
|
||||
font-family: $font-stack-sans;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
// General text
|
||||
|
||||
@mixin text($name: string) {
|
||||
$text-def: map-get($text, "text-" + $name);
|
||||
$size: map-get($text-def, size);
|
||||
$weight: map-get($text-def, weight);
|
||||
|
||||
@include _text-base;
|
||||
@if $size {
|
||||
font-size: $size;
|
||||
}
|
||||
@if $weight {
|
||||
font-weight: $weight;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin text-mono-m {
|
||||
font-size: text(m);
|
||||
font-family: $font-stack-mono;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@mixin text-mono-s {
|
||||
font-size: text(s);
|
||||
font-family: $font-stack-mono;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
// Line heights
|
||||
|
||||
@mixin line-height-normal {
|
||||
line-height: $lh-normal;
|
||||
}
|
||||
|
||||
@mixin line-height-condensed {
|
||||
line-height: $lh-condensed;
|
||||
}
|
||||
|
||||
@mixin line-height-tight {
|
||||
line-height: $lh-tight;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
@mixin no-selection {
|
||||
-webkit-touch-callout: none; /* iOS Safari */
|
||||
-webkit-user-select: none; /* Safari */
|
||||
-khtml-user-select: none; /* Konqueror HTML */
|
||||
-moz-user-select: none; /* Old versions of Firefox */
|
||||
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||
user-select: none; /* Non-prefixed version, currently
|
||||
supported by Chrome, Edge, Opera and Firefox */
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* Global CSS values to wrap into the bundle. Should not be imported by dependants
|
||||
*/
|
||||
@use 'derived/export.scss';
|
||||
@use 'typography';
|
||||
|
||||
@use 'normalize.css/normalize.css';
|
||||
|
||||
// probably should leave this for the consumer to set?
|
||||
body, html {
|
||||
font-size: typography.text(m);
|
||||
font-family: typography.$font-stack-sans;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { MutableRefObject, RefCallback, useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* Joins the `externalRef` to receive the same boxed value as `localRef`
|
||||
*
|
||||
* **NOTE:** This will not update values after initial render. This is only sufficient for element refs
|
||||
*/
|
||||
export const useCombinedRefs = <T, >(localRef: MutableRefObject<T>, externalRef: MutableRefObject<T> | RefCallback<T> | null) => {
|
||||
useEffect(() => {
|
||||
if (!externalRef) {
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof externalRef === 'function') {
|
||||
externalRef(localRef.current)
|
||||
} else {
|
||||
externalRef.current = localRef.current
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [externalRef])
|
||||
}
|
||||
@@ -1,260 +1,6 @@
|
||||
// Color map
|
||||
$colors: (
|
||||
black: rgba(0, 0, 0, 1),
|
||||
metal-90: rgba(33, 36, 38, 1),
|
||||
metal-80: rgba(45, 49, 52, 1),
|
||||
metal-70: rgba(62, 67, 71, 1),
|
||||
metal-60: rgba(83, 90, 95, 1),
|
||||
metal-50: rgba(107, 116, 123, 1),
|
||||
metal-40: rgba(135, 144, 151, 1),
|
||||
metal-30: rgba(160, 167, 172, 1),
|
||||
metal-20: rgba(190, 194, 198, 1),
|
||||
metal-10: rgba(230, 232, 234, 1),
|
||||
metal-05: rgba(244, 245, 246, 1),
|
||||
white: rgba(255, 255, 255, 1),
|
||||
red-70: rgba(153, 11, 18, 1),
|
||||
red-60: rgba(191, 13, 22, 1),
|
||||
red-50: rgba(224, 16, 26, 1),
|
||||
red-40: rgba(241, 55, 64, 1),
|
||||
chill-90: rgba(8, 33, 68, 1),
|
||||
chill-80: rgba(12, 49, 100, 1),
|
||||
chill-70: rgba(16, 66, 137, 1),
|
||||
chill-60: rgba(21, 86, 178, 1),
|
||||
chill-50: rgba(27, 111, 228, 1),
|
||||
chill-40: rgba(73, 142, 238, 1),
|
||||
chill-30: rgba(118, 168, 239, 1),
|
||||
chill-20: rgba(164, 197, 244, 1),
|
||||
chill-10: rgba(222, 235, 252, 1),
|
||||
chill-05: rgba(237, 243, 253, 1),
|
||||
olive-60: rgba(93, 100, 12, 1),
|
||||
olive-50: rgba(114, 123, 15, 1),
|
||||
olive-40: rgba(143, 154, 25, 1),
|
||||
olive-30: rgba(211, 228, 27, 1),
|
||||
olive-20: rgba(225, 237, 100, 1),
|
||||
olive-10: rgba(238, 245, 168, 1),
|
||||
olive-05: rgba(246, 250, 209, 1),
|
||||
papaya-60: rgba(143, 68, 10, 1),
|
||||
papaya-50: rgba(190, 90, 14, 1),
|
||||
papaya-40: rgba(235, 107, 10, 1),
|
||||
papaya-30: rgba(242, 141, 64, 1),
|
||||
papaya-20: rgba(246, 175, 121, 1),
|
||||
papaya-10: rgba(252, 229, 212, 1),
|
||||
papaya-05: rgba(253, 241, 231, 1),
|
||||
green-60: rgba(41, 102, 10, 1),
|
||||
green-50: rgba(54, 133, 15, 1),
|
||||
green-40: rgba(68, 164, 20, 1),
|
||||
green-30: rgba(79, 191, 23, 1),
|
||||
green-20: rgba(107, 219, 51, 1),
|
||||
green-10: rgba(204, 244, 185, 1),
|
||||
green-05: rgba(236, 251, 228, 1),
|
||||
cran-50: rgba(228, 28, 95, 1),
|
||||
brand-00: rgba(28, 228, 150, 1),
|
||||
brand-01: rgba(8, 41, 63, 1),
|
||||
accent-00: rgba(177, 99, 255, 1),
|
||||
accent-01: rgba(54, 197, 255, 1),
|
||||
accent-02: rgba(230, 255, 30, 1),
|
||||
);
|
||||
/*
|
||||
* Base SCSS file for external SASS imports (all dependants should import from this file to use the SASS vars)
|
||||
*/
|
||||
|
||||
// --- Generate color classes
|
||||
// .text- color utilities
|
||||
// .bg- color utilities
|
||||
|
||||
@each $name, $color in $colors {
|
||||
.text-#{"" + $name} {
|
||||
color: $color;
|
||||
}
|
||||
.bg-#{"" + $name} {
|
||||
background-color: $color;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Color Variables
|
||||
|
||||
$black: #000000;
|
||||
$metal-90: #212426;
|
||||
$metal-80: #2d3134;
|
||||
$metal-70: #3e4347;
|
||||
$metal-60: #535a5f;
|
||||
$metal-50: #6b747b;
|
||||
$metal-40: #879097;
|
||||
$metal-30: #a0a7ac;
|
||||
$metal-20: #bec2c6;
|
||||
$metal-10: #e6e8ea;
|
||||
$metal-05: #f4f5f6;
|
||||
$white: #ffffff;
|
||||
$red-70: #990b12;
|
||||
$red-60: #bf0d16;
|
||||
$red-50: #e0101a;
|
||||
$red-40: #f13740;
|
||||
$chill-90: #082144;
|
||||
$chill-80: #0c3164;
|
||||
$chill-70: #104289;
|
||||
$chill-60: #1556b2;
|
||||
$chill-50: #1b6fe4;
|
||||
$chill-40: #498eee;
|
||||
$chill-30: #76a8ef;
|
||||
$chill-20: #a4c5f4;
|
||||
$chill-10: #deebfc;
|
||||
$chill-05: #edf3fd;
|
||||
$olve-60: #5d640c;
|
||||
$olive-50: #727b0f;
|
||||
$olive-40: #8f9a19;
|
||||
$olive-30: #d3e41b;
|
||||
$olive-20: #e1ed64;
|
||||
$olive-10: #eef5a8;
|
||||
$olive-05: #f6fad1;
|
||||
$papaya-60: #8f440a;
|
||||
$papaya-50: #be5a0e;
|
||||
$papaya-40: #eb6b0a;
|
||||
$papaya-30: #f28d40;
|
||||
$papaya-20: #f6af79;
|
||||
$papaya-10: #fce5d4;
|
||||
$papaya-05: #fdf1e7;
|
||||
$green-60: #29660a;
|
||||
$green-50: #36850f;
|
||||
$green-40: #44a414;
|
||||
$green-30: #4fbf17;
|
||||
$green-20: #6bdb33;
|
||||
$green-10: #ccf4b9;
|
||||
$green-05: #ecfbe4;
|
||||
$cran-50: #e41c5f;
|
||||
$brand-00: #1ce496;
|
||||
$brand-01: #08293f;
|
||||
$accent-00: #b163ff;
|
||||
$accent-01: #36c5ff;
|
||||
$accent-02: #e6ff1e;
|
||||
|
||||
// ---
|
||||
|
||||
// Typography
|
||||
|
||||
// --- Font Families
|
||||
|
||||
$font-stack-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
$font-stack-mono: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
|
||||
// --- Text Sizes/Scale
|
||||
|
||||
$text-xxs: 0.5rem;
|
||||
$text-xs: 0.75rem;
|
||||
$text-s: 0.875rem;
|
||||
$text-base: 1rem;
|
||||
$text-m: 1.25rem;
|
||||
$text-ml: 1.5rem;
|
||||
$text-l: 2rem;
|
||||
$text-xl: 2.5rem;
|
||||
$text-xxl: 3rem;
|
||||
$text-xxxl: 4rem;
|
||||
$text-xxxxl: 4.75rem;
|
||||
|
||||
// Line heights
|
||||
|
||||
$lh-tight: 1;
|
||||
$lh-condensed: 1.25;
|
||||
$lh-default: 1.5;
|
||||
|
||||
// --- Shadows
|
||||
|
||||
$shadow-xs: 0 0 0 1px rgba(0, 0, 0, 0.05);
|
||||
$shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
$shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
$shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
$shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
$shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
$shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
$shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
|
||||
$shadow-outline: 0 0 0 3px rgba(66, 153, 225, 0.5);
|
||||
$shadow-none: none;
|
||||
|
||||
// --- Mixins
|
||||
|
||||
@mixin text-style-text-l {
|
||||
font-size: $text-l;
|
||||
font-family: $font-stack-sans;
|
||||
font-weight: bolder;
|
||||
font-style: normal;
|
||||
}
|
||||
@mixin text-style-text-m {
|
||||
font-size: $text-m;
|
||||
font-family: $font-stack-sans;
|
||||
font-weight: bolder;
|
||||
font-style: normal;
|
||||
}
|
||||
@mixin text-style-text-s {
|
||||
font-size: $text-s;
|
||||
font-family: $font-stack-sans;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@mixin text-style-text-xs {
|
||||
font-size: $text-xs;
|
||||
font-family: $font-stack-sans;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@mixin text-style-text-base {
|
||||
font-size: $text-base;
|
||||
font-family: $font-stack-sans;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@mixin text-style-text-xl {
|
||||
font-size: $text-xl;
|
||||
font-family: $font-stack-sans;
|
||||
font-weight: bolder;
|
||||
font-style: normal;
|
||||
}
|
||||
@mixin text-style-text-xxl {
|
||||
font-size: $text-xxl;
|
||||
font-family: $font-stack-sans;
|
||||
font-weight: bolder;
|
||||
font-style: normal;
|
||||
}
|
||||
@mixin text-style-text-xxxl {
|
||||
font-size: $text-xxxl;
|
||||
font-family: $font-stack-sans;
|
||||
font-weight: bolder;
|
||||
font-style: normal;
|
||||
}
|
||||
@mixin text-style-text-xxxxl {
|
||||
font-size: $text-xxxxl;
|
||||
font-family: $font-stack-sans;
|
||||
font-weight: bolder;
|
||||
font-style: normal;
|
||||
}
|
||||
@mixin text-style-text-s-caps {
|
||||
font-size: $text-s;
|
||||
font-family: $font-stack-sans;
|
||||
text-transform: uppercase;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@mixin text-style-text-mono-base {
|
||||
font-size: $text-base;
|
||||
font-family: $font-stack-mono;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
@mixin text-style-text-mono-s {
|
||||
font-size: $text-s;
|
||||
font-family: $font-stack-mono;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
// Lighter text for accent, etc
|
||||
@mixin text-style-light-xs {
|
||||
color: $metal-30;
|
||||
font-size: $text-xs;
|
||||
font-family: $font-stack-sans;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
// probably should leave this for the consumer to set?
|
||||
body, html {
|
||||
font-size: $text-m;
|
||||
font-family: $font-stack-sans;
|
||||
}
|
||||
// css prefix for clarity
|
||||
@forward 'css/index';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
export * from './components/Button'
|
||||
// Add global CSS to the bundle
|
||||
import './global.scss'
|
||||
|
||||
export * from './components/CypressLogo/CypressLogo'
|
||||
|
||||
export * from './components/SearchInput/SearchInput'
|
||||
export * from './components/searchInput/SearchInput'
|
||||
|
||||
export * from './components/Nav'
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Draws the baseline and toplines for a DOM element
|
||||
*/
|
||||
.baseline {
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
|
||||
top: 0.2em;
|
||||
background-color: rgba(red, 0.2);
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
|
||||
bottom: 0.2em;
|
||||
left: 0;
|
||||
background-color: rgba(red, 0.2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as React from 'react'
|
||||
import cs from 'classnames'
|
||||
|
||||
import styles from './Baseline.module.scss'
|
||||
|
||||
export interface BaselineProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const Baseline: React.FC<BaselineProps> = ({ className, children }) => (
|
||||
<div className={cs(styles.baseline, className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
@@ -0,0 +1,51 @@
|
||||
import * as React from 'react'
|
||||
import { Story } from '@storybook/react'
|
||||
|
||||
import { createStory, createStorybookConfig } from './util'
|
||||
|
||||
import styles from './colors.module.scss'
|
||||
import colors from 'css/derived/jsColors.scss'
|
||||
import '../index.scss'
|
||||
|
||||
export default createStorybookConfig({
|
||||
title: 'System/Colors',
|
||||
})
|
||||
|
||||
const Color: React.FC<{
|
||||
name: string
|
||||
}> = ({ name }) => {
|
||||
const match = name.match(/([A-Z]+)([0-9]+)/i)
|
||||
|
||||
if (!match) {
|
||||
return (
|
||||
<div>
|
||||
{`Could not parse color ${name}`}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Group colors based on type
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [, type, number] = match
|
||||
|
||||
let textColor = parseInt(number, 10) < 50 ? '--metal-90' : '--metal-10'
|
||||
|
||||
if (name === 'brand01') {
|
||||
// Override secondary brand
|
||||
textColor = '--metal-10'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.colorBlock} style={{ backgroundColor: `var(--${type}-${number})`, color: `var(${textColor})` }}>
|
||||
{name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Template: Story = () => (
|
||||
<div>
|
||||
{Object.keys(colors).map((name: string) => <Color key={name} name={name} />)}
|
||||
</div>
|
||||
)
|
||||
|
||||
export const Colors = createStory(Template)
|
||||
@@ -0,0 +1,51 @@
|
||||
import * as React from 'react'
|
||||
import { Story } from '@storybook/react'
|
||||
|
||||
import { createStory, createStorybookConfig } from './util'
|
||||
|
||||
import styles from './spacing.module.scss'
|
||||
import spacing from 'css/derived/jsSpacing.scss'
|
||||
import '../index.scss'
|
||||
|
||||
export default createStorybookConfig({
|
||||
title: 'System/Spacing',
|
||||
})
|
||||
|
||||
const currentFontSize = () => parseFloat(getComputedStyle(document.documentElement).fontSize)
|
||||
|
||||
const pixelsFromRem = (rem: string) => {
|
||||
const remFloat = parseFloat(rem.replace('rem', ''))
|
||||
|
||||
return remFloat * currentFontSize()
|
||||
}
|
||||
|
||||
const Cube: React.FC<{
|
||||
name: string
|
||||
}> = ({ name }) => {
|
||||
const size: string = spacing[name as keyof typeof spacing]
|
||||
const pixelSize = pixelsFromRem(size)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
{`${name}: ${size} (${pixelSize}px @ ${currentFontSize()})`}
|
||||
</div>
|
||||
<div
|
||||
className={styles.cube}
|
||||
style={{
|
||||
height: size,
|
||||
width: size,
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Template: Story = () => (
|
||||
<div>
|
||||
{Object.keys(spacing).map((name) => <Cube key={name} name={name} />)}
|
||||
</div>
|
||||
)
|
||||
|
||||
export const Spacing = createStory(Template)
|
||||
@@ -0,0 +1,32 @@
|
||||
import * as React from 'react'
|
||||
import { Story } from '@storybook/react'
|
||||
|
||||
import { createStory, createStorybookConfig } from './util'
|
||||
|
||||
import styles from './surfaces.module.scss'
|
||||
import surfaces from 'css/derived/jsSurfaces.scss'
|
||||
import '../index.scss'
|
||||
|
||||
export default createStorybookConfig({
|
||||
title: 'System/Surfaces',
|
||||
})
|
||||
|
||||
const Surface: React.FC<{
|
||||
className: string
|
||||
}> = ({ className }) => {
|
||||
const level = className.replace('shadow-', '')
|
||||
|
||||
return (
|
||||
<div className={`${styles.surface} ${`depth-${level}`}`}>
|
||||
{`Level ${level}`}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Template: Story = () => (
|
||||
<div>
|
||||
{Object.keys(surfaces).map((className) => <Surface key={className} className={className} />)}
|
||||
</div>
|
||||
)
|
||||
|
||||
export const Surfaces = createStory(Template)
|
||||
@@ -0,0 +1,35 @@
|
||||
import * as React from 'react'
|
||||
import { Story } from '@storybook/react'
|
||||
|
||||
import { createStory, createStorybookConfig } from './util'
|
||||
|
||||
import typography from 'css/derived/jsTypography.scss'
|
||||
import '../index.scss'
|
||||
|
||||
export default createStorybookConfig({
|
||||
title: 'System/Typography',
|
||||
})
|
||||
|
||||
const Template: Story = () => (
|
||||
<div>
|
||||
{Object.keys(typography).filter((key) => key !== 'type').map((key) => {
|
||||
const size = key.replace('text-', '')
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
style={{
|
||||
marginBottom: '2em',
|
||||
}}
|
||||
>
|
||||
<div className="text-mono-m">
|
||||
{size}
|
||||
</div>
|
||||
<div className={key}>The five boxing wizards jump quickly</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
|
||||
export const Typography = createStory(Template)
|
||||
@@ -0,0 +1,9 @@
|
||||
.colorBlock {
|
||||
display: inline-block;
|
||||
height: 7rem;
|
||||
width: 7rem;
|
||||
|
||||
padding: 1.5rem;
|
||||
margin-right: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
@use 'baseColors' as *;
|
||||
|
||||
.cube {
|
||||
margin: 1rem;
|
||||
background-color: $brand-00;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.surface {
|
||||
display: inline-block;
|
||||
height: 12rem;
|
||||
width: 12rem;
|
||||
|
||||
padding: 1.5rem;
|
||||
margin-right: 4rem;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Story, Meta } from '@storybook/react'
|
||||
|
||||
/**
|
||||
* Passthrough config creator for typing without casting
|
||||
*/
|
||||
export const createStorybookConfig = (config: Meta): Meta => config
|
||||
|
||||
/**
|
||||
* Compact way of declaring a new story
|
||||
*/
|
||||
export const createStory = <T>(template: Story<T>, args?: Partial<T>) => {
|
||||
const story = template.bind({})
|
||||
|
||||
story.args = args
|
||||
|
||||
return story
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export const lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut tempus dapibus mi. Sed convallis interdum aliquet.' +
|
||||
' Donec pellentesque felis non diam finibus, ac interdum orci tincidunt. Nam a nunc non mi auctor congue. Mauris id tempus urna.'
|
||||
@@ -0,0 +1,9 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import styles from './storyHighlightWrapper.module.scss'
|
||||
|
||||
export const StoryHighlightWrapper: React.FC = ({ children }) => (
|
||||
<div className={styles.wrapper}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
@use 'baseColors' as *;
|
||||
|
||||
.wrapper {
|
||||
background-color: $brand-00;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export type ExtractFirstArg<T extends (...args: any[]) => unknown> = T extends (arg0: infer S, ...otherArgs: any[]) => unknown ? S : never
|
||||
@@ -20,8 +20,18 @@
|
||||
"cypress"
|
||||
] /* Type declaration files to be included in compilation. */,
|
||||
"allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
|
||||
"esModuleInterop": true
|
||||
"esModuleInterop": true,
|
||||
"baseUrl": "src",
|
||||
"paths": {
|
||||
"core/*": ["core/*"],
|
||||
"css/*": ["css/*"],
|
||||
"components/*": ["components/*"],
|
||||
"hooks/*": ["hooks/*"],
|
||||
"shared/*": ["shared/*"],
|
||||
"stories/*": ["stories/*"],
|
||||
"util/*": ["util/*"],
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.spec.ts", "src/**/*.spec.tsx"]
|
||||
"exclude": ["src/**/*.spec.ts", "src/**/*.spec.tsx"],
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
/**
|
||||
* Additional styles to inject into the document.
|
||||
* A component might need 3rd party libraries from CDN,
|
||||
|
||||
@@ -8,7 +8,7 @@ it('should select null after timing out', () => {
|
||||
const onSelect = cy.stub().as('selected')
|
||||
|
||||
mount(<Card onSelect={onSelect} />)
|
||||
cy.get('@selected', { timeout: 5100 }).should('have.been.calledWith', null)
|
||||
cy.get('@selected', { timeout: 6000 }).should('have.been.calledWith', null)
|
||||
})
|
||||
|
||||
it('should accept selections', () => {
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# re-render
|
||||
|
||||
If you want to see how a component re-renders in reaction to changed props, you have to make a mini-web app around it. See [spec.js](spec.js) but in essence you never keep the component, but create a new one.
|
||||
|
||||

|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 591 KiB |
@@ -1,46 +0,0 @@
|
||||
import React from 'react'
|
||||
import { mount } from '@cypress/react'
|
||||
|
||||
const MyInput = ({ inputVal, onInputChanged }) => {
|
||||
console.log('MyInput "%s"', inputVal)
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
value={inputVal}
|
||||
onChange={(e) => onInputChanged(e.target.value)}
|
||||
/>
|
||||
<p>You entered {inputVal} </p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
describe('My Input', () => {
|
||||
it('updates when props change', () => {
|
||||
// can we shorten this example
|
||||
// that checks if the MyInput is re-rendering?
|
||||
const App = () => {
|
||||
const [message, setMessage] = React.useState('')
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyInput
|
||||
inputVal={message}
|
||||
onInputChanged={(newValue) => {
|
||||
setMessage(newValue)
|
||||
|
||||
return null
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
mount(<App />)
|
||||
|
||||
/* Update props */
|
||||
cy.get('input').type('hello there!')
|
||||
cy.contains('You entered hello there!')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
/// <reference types="cypress" />
|
||||
import React from 'react'
|
||||
import { mount } from '@cypress/react'
|
||||
|
||||
it('should properly handle swapping components', () => {
|
||||
const Component1 = ({ input }) => {
|
||||
return <div>{input}</div>
|
||||
}
|
||||
|
||||
const Component2 = ({ differentProp }) => {
|
||||
return <div style={{ backgroundColor: 'blue' }}>{differentProp}</div>
|
||||
}
|
||||
|
||||
mount(<Component1 input="0" />).then(({ rerender }) => {
|
||||
rerender(<Component2 differentProp="1" />).get('body').should('contain', '1').should('not.contain', '0')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,67 @@
|
||||
/// <reference types="cypress" />
|
||||
import React, { useLayoutEffect, useEffect } from 'react'
|
||||
import { mount } from '@cypress/react'
|
||||
|
||||
it('should not run unmount effect cleanup when rerendering', () => {
|
||||
const layoutEffectCleanup = cy.stub()
|
||||
const effectCleanup = cy.stub()
|
||||
|
||||
const Component = ({ input }) => {
|
||||
useLayoutEffect(() => {
|
||||
return layoutEffectCleanup
|
||||
}, [input])
|
||||
|
||||
useEffect(() => {
|
||||
return effectCleanup
|
||||
}, [])
|
||||
|
||||
return <div>{input}</div>
|
||||
}
|
||||
|
||||
mount(<Component input="0" />).then(({ rerender }) => {
|
||||
expect(layoutEffectCleanup).to.have.been.callCount(0)
|
||||
expect(effectCleanup).to.have.been.callCount(0)
|
||||
|
||||
rerender(<Component input="0" />).then(() => {
|
||||
expect(layoutEffectCleanup).to.have.been.callCount(0)
|
||||
expect(effectCleanup).to.have.been.callCount(0)
|
||||
})
|
||||
|
||||
rerender(<Component input="1" />).then(() => {
|
||||
expect(layoutEffectCleanup).to.have.been.callCount(1)
|
||||
expect(effectCleanup).to.have.been.callCount(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should run unmount effect cleanup when unmounting', () => {
|
||||
const layoutEffectCleanup = cy.stub()
|
||||
const effectCleanup = cy.stub()
|
||||
|
||||
const Component = ({ input }) => {
|
||||
useLayoutEffect(() => {
|
||||
return layoutEffectCleanup
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
return effectCleanup
|
||||
}, [])
|
||||
|
||||
return <div>{input}</div>
|
||||
}
|
||||
|
||||
mount(<Component input="0" />).then(({ rerender, unmount }) => {
|
||||
expect(layoutEffectCleanup).to.have.been.callCount(0)
|
||||
expect(effectCleanup).to.have.been.callCount(0)
|
||||
|
||||
rerender(<Component input="1" />).then(() => {
|
||||
expect(layoutEffectCleanup).to.have.been.callCount(0)
|
||||
expect(effectCleanup).to.have.been.callCount(0)
|
||||
})
|
||||
|
||||
unmount().then(() => {
|
||||
expect(layoutEffectCleanup).to.have.been.callCount(1)
|
||||
expect(effectCleanup).to.have.been.callCount(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
export const InputAccumulator = ({ input }) => {
|
||||
const [store, setStore] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
setStore((prev) => [...prev, input])
|
||||
}, [input])
|
||||
|
||||
return (<ul>
|
||||
{store.map((v) => <li key={v}>{v}</li>)}
|
||||
</ul>)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/// <reference types="cypress" />
|
||||
import React from 'react'
|
||||
import { mount } from '@cypress/react'
|
||||
import { InputAccumulator } from './input-accumulator'
|
||||
|
||||
it('should rerender preserving input values', () => {
|
||||
mount(<InputAccumulator input="initial" />).then(({ rerender }) => {
|
||||
cy.get('li').eq(0).contains('initial')
|
||||
|
||||
rerender(<InputAccumulator input="Rerendered value" />)
|
||||
cy.get('li:nth-child(1)').should('contain', 'initial')
|
||||
cy.get('li:nth-child(2)').should('contain', 'Rerendered value')
|
||||
|
||||
rerender(<InputAccumulator input="Second rerendered value" />)
|
||||
|
||||
cy.get('li:nth-child(1)').should('contain', 'initial')
|
||||
cy.get('li:nth-child(2)').should('contain', 'Rerendered value')
|
||||
cy.get('li:nth-child(3)').should('contain', 'Second rerendered value')
|
||||
})
|
||||
})
|
||||
@@ -34,7 +34,7 @@ module.exports = (on, config) => {
|
||||
}
|
||||
```
|
||||
|
||||
See example repo [bahmutov/try-cra-with-unit-test](https://github.com/bahmutov/try-cra-with-unit-test) or included example in the folder [examples/react-scripts](../examples/react-scripts).
|
||||
See example repo [bahmutov/try-cra-with-unit-test](https://github.com/bahmutov/try-cra-with-unit-test) or included example in the folder [examples/react-scripts](https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/react-scripts).
|
||||
|
||||
**Tip:** `plugins/react-scripts` is just loading `plugins/cra-v3`.
|
||||
|
||||
@@ -50,7 +50,7 @@ module.exports = (on, config) => {
|
||||
}
|
||||
```
|
||||
|
||||
See example in the folder [examples/nextjs](../examples/nextjs).
|
||||
See example in the folder [examples/nextjs](https://github.com/cypress-io/cypress/tree/develop/npm/react/https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/nextjs).
|
||||
|
||||
### Your Webpack config
|
||||
|
||||
@@ -68,7 +68,7 @@ module.exports = (on, config) => {
|
||||
}
|
||||
```
|
||||
|
||||
See example in [bahmutov/Jscrambler-Webpack-React](https://github.com/bahmutov/Jscrambler-Webpack-React) or included example in the folder [examples/webpack-file](../examples/webpack-file).
|
||||
See example in [bahmutov/Jscrambler-Webpack-React](https://github.com/bahmutov/Jscrambler-Webpack-React) or included example in the folder [examples/webpack-file](https://github.com/cypress-io/cypress/tree/develop/npm/react/https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/webpack-file).
|
||||
|
||||
### Your `.babelrc` file
|
||||
|
||||
@@ -85,7 +85,7 @@ module.exports = (on, config) => {
|
||||
}
|
||||
```
|
||||
|
||||
See example in the folder [examples/using-babel](../examples/using-babel) and [examples/using-babel-typescript](../examples/using-babel-typescript).
|
||||
See example in the folder [examples/using-babel](https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/using-babel) and [examples/using-babel-typescript](https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/using-babel-typescript).
|
||||
|
||||
#### Add Babel plugins
|
||||
|
||||
@@ -129,7 +129,7 @@ When loading your `.babelrc` settings, `@cypress/react` sets `BABEL_ENV` and `NO
|
||||
}
|
||||
```
|
||||
|
||||
See [examples/using-babel](../examples/using-babel) folder for full example.
|
||||
See [examples/using-babel](https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/using-babel) folder for full example.
|
||||
|
||||
#### Using rollup config
|
||||
|
||||
@@ -163,7 +163,7 @@ commonjs(),
|
||||
replace({ 'process.env.NODE_ENV': JSON.stringify('development') }),
|
||||
```
|
||||
|
||||
See [examples/rollup](../examples/rollup) folder for full example.
|
||||
See [react/examples/rollup](https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/rollup) folder for full example.
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
+68
-16
@@ -30,7 +30,7 @@ const injectStyles = (options: MountOptions) => {
|
||||
* @example
|
||||
```
|
||||
import Hello from './hello.jsx'
|
||||
import {mount} from '@cypress/react'
|
||||
import { mount } from '@cypress/react'
|
||||
it('works', () => {
|
||||
mount(<Hello onClick={cy.stub()} />)
|
||||
// use Cypress commands
|
||||
@@ -38,14 +38,24 @@ const injectStyles = (options: MountOptions) => {
|
||||
})
|
||||
```
|
||||
**/
|
||||
export const mount = (jsx: React.ReactNode, options: MountOptions = {}) => {
|
||||
export const mount = (jsx: React.ReactNode, options: MountOptions = {}) => _mount('mount', jsx, options)
|
||||
|
||||
/**
|
||||
* @see `mount`
|
||||
* @param type The type of mount executed
|
||||
* @param rerenderKey If specified, use the provided key rather than generating a new one
|
||||
*/
|
||||
const _mount = (type: 'mount' | 'rerender', jsx: React.ReactNode, options: MountOptions = {}, rerenderKey?: string): globalThis.Cypress.Chainable<MountReturn> => {
|
||||
// Get the display name property via the component constructor
|
||||
// @ts-ignore FIXME
|
||||
const componentName = getDisplayName(jsx.type, options.alias)
|
||||
const displayName = options.alias || componentName
|
||||
|
||||
const jsxComponentName = `<${componentName} ... />`
|
||||
|
||||
const message = options.alias
|
||||
? `<${componentName} ... /> as "${options.alias}"`
|
||||
: `<${componentName} ... />`
|
||||
? `${jsxComponentName} as "${options.alias}"`
|
||||
: jsxComponentName
|
||||
|
||||
return cy
|
||||
.then(injectStyles(options))
|
||||
@@ -62,9 +72,9 @@ export const mount = (jsx: React.ReactNode, options: MountOptions = {}) => {
|
||||
)
|
||||
}
|
||||
|
||||
const key =
|
||||
const key = rerenderKey ??
|
||||
// @ts-ignore provide unique key to the the wrapped component to make sure we are rerendering between tests
|
||||
(Cypress?.mocha?.getRunner()?.test?.title || '') + Math.random()
|
||||
(Cypress?.mocha?.getRunner()?.test?.title as string || '') + Math.random()
|
||||
const props = {
|
||||
key,
|
||||
}
|
||||
@@ -76,21 +86,24 @@ export const mount = (jsx: React.ReactNode, options: MountOptions = {}) => {
|
||||
)
|
||||
// since we always surround the component with a fragment
|
||||
// let's get back the original component
|
||||
// @ts-ignore
|
||||
const userComponent = reactComponent.props.children
|
||||
const userComponent = (reactComponent.props as {
|
||||
key: string
|
||||
children: React.ReactNode
|
||||
}).children
|
||||
|
||||
reactDomToUse.render(reactComponent, el)
|
||||
|
||||
if (options.log !== false) {
|
||||
Cypress.log({
|
||||
name: 'mount',
|
||||
name: type,
|
||||
type: 'parent',
|
||||
message: [message],
|
||||
$el: (el.children.item(0) as unknown) as JQuery<HTMLElement>,
|
||||
consoleProps: () => {
|
||||
return {
|
||||
// @ts-ignore protect the use of jsx functional components use ReactNode
|
||||
props: jsx.props,
|
||||
description: 'Mounts React component',
|
||||
description: type === 'mount' ? 'Mounts React component' : 'Rerenders mounted React component',
|
||||
home: 'https://github.com/cypress-io/cypress',
|
||||
}
|
||||
},
|
||||
@@ -98,15 +111,23 @@ export const mount = (jsx: React.ReactNode, options: MountOptions = {}) => {
|
||||
}
|
||||
|
||||
return (
|
||||
cy
|
||||
.wrap(userComponent, { log: false })
|
||||
// Separate alias and returned value. Alias returns the component only, and the thenable returns the additional functions
|
||||
cy.wrap<React.ReactNode>(userComponent)
|
||||
.as(displayName)
|
||||
.then(() => {
|
||||
return cy.wrap<MountReturn>({
|
||||
component: userComponent,
|
||||
rerender: (newComponent) => _mount('rerender', newComponent, options, key),
|
||||
unmount: () => _unmount({ boundComponentMessage: jsxComponentName, log: true }),
|
||||
}, { log: false })
|
||||
})
|
||||
// by waiting, we delaying test execution for the next tick of event loop
|
||||
// and letting hooks and component lifecycle methods to execute mount
|
||||
// https://github.com/bahmutov/cypress-react-unit-test/issues/200
|
||||
.wait(0, { log: false })
|
||||
)
|
||||
})
|
||||
// Bluebird types are terrible. I don't think the return type can be carried without this cast
|
||||
}) as unknown as globalThis.Cypress.Chainable<MountReturn>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,7 +146,9 @@ export const mount = (jsx: React.ReactNode, options: MountOptions = {}) => {
|
||||
})
|
||||
```
|
||||
*/
|
||||
export const unmount = (options = { log: true }) => {
|
||||
export const unmount = (options = { log: true }): globalThis.Cypress.Chainable<JQuery<HTMLElement>> => _unmount(options)
|
||||
|
||||
const _unmount = (options: { boundComponentMessage?: string, log: boolean }) => {
|
||||
return cy.then(() => {
|
||||
const selector = `#${ROOT_ID}`
|
||||
|
||||
@@ -133,7 +156,18 @@ export const unmount = (options = { log: true }) => {
|
||||
const wasUnmounted = ReactDOM.unmountComponentAtNode($el[0])
|
||||
|
||||
if (wasUnmounted && options.log) {
|
||||
cy.log('Unmounted component at', $el)
|
||||
Cypress.log({
|
||||
name: 'unmount',
|
||||
type: 'parent',
|
||||
message: [options.boundComponentMessage ?? 'Unmounted component'],
|
||||
consoleProps: () => {
|
||||
return {
|
||||
description: 'Unmounts React component',
|
||||
parent: $el[0],
|
||||
home: 'https://github.com/cypress-io/cypress',
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -179,12 +213,13 @@ export const createMount = (defaultOptions: MountOptions) => {
|
||||
}
|
||||
|
||||
/** @deprecated Should be removed in the next major version */
|
||||
// TODO: Remove
|
||||
export default mount
|
||||
|
||||
// I hope to get types and docs from functions imported from ./index one day
|
||||
// but for now have to document methods in both places
|
||||
// like this: import {mount} from './index'
|
||||
|
||||
// TODO: Clean up types
|
||||
export interface ReactModule {
|
||||
name: string
|
||||
type: string
|
||||
@@ -209,6 +244,23 @@ export interface MountReactComponentOptions {
|
||||
|
||||
export type MountOptions = Partial<StyleOptions & MountReactComponentOptions>
|
||||
|
||||
export interface MountReturn {
|
||||
/**
|
||||
* The component that was rendered.
|
||||
*/
|
||||
component: React.ReactNode
|
||||
/**
|
||||
* Rerenders the specified component with new props. This allows testing of components that store state (`setState`)
|
||||
* or have asynchronous updates (`useEffect`, `useLayoutEffect`).
|
||||
*/
|
||||
rerender: (component: React.ReactNode) => globalThis.Cypress.Chainable<MountReturn>
|
||||
/**
|
||||
* Removes the mounted component.
|
||||
* @see `unmount`
|
||||
*/
|
||||
unmount: () => globalThis.Cypress.Chainable<JQuery<HTMLElement>>
|
||||
}
|
||||
|
||||
/**
|
||||
* The `type` property from the transpiled JSX object.
|
||||
* @example
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { startDevServer } from '@cypress/vite-dev-server'
|
||||
import viteConfig from '../vite.config'
|
||||
const path = require('path')
|
||||
const { startDevServer } = require('../dist')
|
||||
|
||||
module.exports = (on, config) => {
|
||||
on('dev-server:start', async (options) => {
|
||||
return startDevServer({
|
||||
options,
|
||||
viteConfig,
|
||||
viteConfig: {
|
||||
configFile: path.resolve(__dirname, '..', 'vite.config.ts'),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -14,10 +14,17 @@ interface Options {
|
||||
}
|
||||
|
||||
export interface StartDevServer {
|
||||
/* this is the Cypress options object */
|
||||
/**
|
||||
* the Cypress options object
|
||||
*/
|
||||
options: Options
|
||||
/* support passing a path to the user's webpack config */
|
||||
viteConfig?: UserConfig // TODO: implement taking in the user's vite configuration. Right now we don't
|
||||
/**
|
||||
* By default, vite will use your vite.config file to
|
||||
* Start the server. If you need additional plugins or
|
||||
* to override some options, you can do so using this.
|
||||
* @optional
|
||||
*/
|
||||
viteConfig?: UserConfig
|
||||
}
|
||||
|
||||
const resolveServerConfig = async ({ viteConfig, options }: StartDevServer): Promise<InlineConfig> => {
|
||||
|
||||
+1
-12
@@ -2,7 +2,7 @@
|
||||
|
||||
If you component imports its own style, the style should be applied during the Cypress test. But sometimes you need more power.
|
||||
|
||||
You can 3 options to load additional styles:
|
||||
You can 2 options to load additional styles:
|
||||
|
||||
```js
|
||||
const myComponent = {
|
||||
@@ -11,7 +11,6 @@ const myComponent = {
|
||||
|
||||
mount(myComponent, {
|
||||
style: string, // load inline style CSS
|
||||
cssFiles: string | string[], // load a single or a list of local CSS files
|
||||
stylesheets: string | string[] // load external stylesheets
|
||||
})
|
||||
```
|
||||
@@ -47,16 +46,6 @@ it('can be passed as an option', () => {
|
||||
})
|
||||
```
|
||||
|
||||
## Load local CSS file
|
||||
|
||||
```js
|
||||
const cssFiles = 'cypress/integration/Button.css'
|
||||
const myComponent = {
|
||||
template: '<button class="orange"><slot/></button>'
|
||||
}
|
||||
mount(myComponent, { cssFiles })
|
||||
```
|
||||
|
||||
## Load external stylesheets
|
||||
|
||||
```js
|
||||
|
||||
@@ -7,9 +7,21 @@ const webpackConfig = require('@vue/cli-service/webpack.config')
|
||||
*/
|
||||
module.exports = (on, config) => {
|
||||
on('dev-server:start', (options) => {
|
||||
// HtmlPwaPlugin is coupled to a hook in HtmlWebpackPlugin
|
||||
// that was deprecated after 3.x. We currently only support
|
||||
// HtmlWebpackPlugin 4.x and 5.x.
|
||||
// TODO: Figure out how to deal with 2 major versions old HtmlWebpackPlugin
|
||||
// which is still in widespread usage.
|
||||
const modifiedWebpackConfig = {
|
||||
...webpackConfig,
|
||||
plugins: (webpackConfig.plugins || []).filter((x) => {
|
||||
return x.constructor.name !== 'HtmlPwaPlugin'
|
||||
}),
|
||||
}
|
||||
|
||||
return startDevServer({
|
||||
options,
|
||||
webpackConfig,
|
||||
webpackConfig: modifiedWebpackConfig,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user