Merge branch '10.0-release' of github.com:cypress-io/cypress into md-10.0-merge

This commit is contained in:
Bill Glesias
2022-05-06 10:07:06 -04:00
160 changed files with 407 additions and 8722 deletions
-1
View File
@@ -13,7 +13,6 @@
/npm/angular @cypress-io/component-testing
/npm/create-cypress-tests @cypress-io/component-testing
/npm/cypress-schematic @cypress-io/component-testing
/npm/design-system @cypress-io/component-testing
/npm/eslint-plugin-dev @cypress-io/end-to-end @cypress-io/component-testing
/npm/mount-utils @cypress-io/component-testing
/npm/react @cypress-io/component-testing
-5
View File
@@ -75,11 +75,6 @@ system-tests/lib/fixtureDirs.ts
/npm/react/cypress/videos
/npm/react/.babel-cache
# from npm/design-system
/npm/design-system/bin/*
/npm/design-system/cypress/videos
/npm/design-system/.babel-cache
# from npm/webpack-dev-server
/npm/webpack-dev-server/cypress/videos
+1 -29
View File
@@ -1124,7 +1124,6 @@ jobs:
job-names: >
cli-visual-tests,
reporter-integration-tests,
npm-design-system,
run-app-component-tests-chrome,
run-app-integration-tests-chrome,
run-frontend-shared-component-tests-chrome,
@@ -1187,7 +1186,7 @@ jobs:
# run @cypress/design-system before other packages are built
- run: yarn lerna run build-prod --scope \"@cypress/design-system\"
# make sure packages with TypeScript can be transpiled to JS
- run: yarn lerna run build-prod --stream --ignore \"@cypress/design-system\"
- run: yarn lerna run build-prod --stream
# run unit tests from each individual package
- run: yarn test
# run type checking for each individual package
@@ -1651,28 +1650,6 @@ jobs:
path: npm/vue/test_results
- store-npm-logs
npm-design-system:
<<: *defaults
steps:
- restore_cached_workspace
- run:
name: Build
command: yarn workspace @cypress/design-system build
- run:
name: Run tests
# will use PERCY_TOKEN environment variable if available
command: |
CYPRESS_KONFIG_ENV=production \
PERCY_PARALLEL_NONCE=$CIRCLE_SHA1 \
PERCY_ENABLE=${PERCY_TOKEN:-0} \
PERCY_PARALLEL_TOTAL=-1 \
yarn percy exec --parallel -- -- \
yarn test --reporter mocha-multi-reporters --reporter-options configFile=../../mocha-reporter-config.json
working_directory: npm/design-system
- store_test_results:
path: npm/design-system/test_results
- store-npm-logs
npm-angular:
<<: *defaults
steps:
@@ -2366,10 +2343,6 @@ linux-workflow: &linux-workflow
- npm-webpack-batteries-included-preprocessor:
requires:
- build
- npm-design-system:
context: test-runner:percy
requires:
- build
- npm-vue:
requires:
- build
@@ -2434,7 +2407,6 @@ linux-workflow: &linux-workflow
- unit-tests-release
- cli-visual-tests
- reporter-integration-tests
- npm-design-system
- run-app-component-tests-chrome
- run-app-integration-tests-chrome
- run-frontend-shared-component-tests-chrome
+3 -1
View File
@@ -27,7 +27,8 @@
"glob": "^7.1.6",
"inquirer": "7.3.3",
"ora": "^5.1.0",
"recast": "0.20.4"
"recast": "0.20.4",
"semver": "7.3.7"
},
"devDependencies": {
"@types/babel__core": "^7.1.2",
@@ -35,6 +36,7 @@
"@types/mock-fs": "4.10.0",
"@types/node": "14.14.31",
"@types/ora": "^3.2.0",
"@types/semver": "7.3.9",
"copy": "0.3.2",
"mocha": "7.1.1",
"mock-fs": "5.1.1",
-144
View File
@@ -1,144 +0,0 @@
{
"plugins": [
"cypress",
"@cypress/dev"
],
"extends": [
"plugin:@cypress/dev/general",
"plugin:@cypress/dev/tests",
"plugin:@cypress/dev/react",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"../../packages/reporter/src/.eslintrc.json"
],
"parser": "@typescript-eslint/parser",
"env": {
"cypress/globals": true
},
"rules": {
"no-duplicate-imports": "off",
"no-else-return": [
"error",
{
"allowElseIf": true
}
],
"react/display-name": "off",
"react/function-component-definition": [
"error",
{
"namedComponents": "arrow-function",
"unnamedComponents": "arrow-function"
}
],
"react/jsx-boolean-value": [
"error",
"always"
],
"react/jsx-closing-bracket-location": [
"error",
"line-aligned"
],
"react/jsx-closing-tag-location": "error",
"react/jsx-curly-brace-presence": [
"error",
{
"props": "never",
"children": "never"
}
],
"react/jsx-curly-newline": "error",
"react/jsx-filename-extension": [
"warn",
{
"extensions": [
".js",
".jsx",
".tsx"
]
}
],
"react/jsx-first-prop-new-line": "error",
"react/jsx-max-props-per-line": [
"error",
{
"maximum": 1,
"when": "multiline"
}
],
"react/jsx-no-bind": [
"error",
{
"ignoreDOMComponents": true
}
],
"react/jsx-no-useless-fragment": "error",
"react/jsx-one-expression-per-line": [
"error",
{
"allow": "literal"
}
],
"react/jsx-sort-props": [
"error",
{
"callbacksLast": true,
"ignoreCase": true,
"noSortAlphabetically": true,
"reservedFirst": true
}
],
"react/jsx-tag-spacing": [
"error",
{
"closingSlash": "never",
"beforeSelfClosing": "always"
}
],
"react/jsx-wrap-multilines": [
"error",
{
"declaration": "parens-new-line",
"assignment": "parens-new-line",
"return": "parens-new-line",
"arrow": "parens-new-line",
"condition": "parens-new-line",
"logical": "parens-new-line",
"prop": "parens-new-line"
}
],
"react/no-array-index-key": "error",
"react/no-unescaped-entities": "off",
"react/prop-types": "off",
"quote-props": [
"error",
"as-needed"
]
},
"overrides": [
{
"files": [
"lib/*"
],
"rules": {
"no-console": 1
}
},
{
"files": [
"**/*.json"
],
"rules": {
"quotes": "off",
"comma-dangle": "off"
}
},
{
"files": "*.spec.tsx",
"rules": {
"no-unused-vars": "off",
"react/jsx-no-bind": "off"
}
}
]
}
-8
View File
@@ -1,8 +0,0 @@
{
"watch-ignore": [
"./test/_test-output",
"node_modules"
],
"require": "../../node_modules/@packages/web-config/node-register",
"exit": true
}
-1
View File
@@ -1 +0,0 @@
cypress
-63
View File
@@ -1,63 +0,0 @@
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
},
}
-5
View File
@@ -1,5 +0,0 @@
import '../src/global.scss'
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
}
-63
View File
@@ -1,63 +0,0 @@
# [@cypress/design-system-v1.4.0](https://github.com/cypress-io/cypress/compare/@cypress/design-system-v1.3.0...@cypress/design-system-v1.4.0) (2021-05-11)
### Bug Fixes
* **deps:** update dependency cypress-real-events to version 1.4.0 🌟 ([#16363](https://github.com/cypress-io/cypress/issues/16363)) ([38ab170](https://github.com/cypress-io/cypress/commit/38ab170fead2369bda0fa14a0cc89f505141e682))
### Features
* **design-system:** SpecList - FileTree - VirtualizedTree ([#16179](https://github.com/cypress-io/cypress/issues/16179)) ([a73adf2](https://github.com/cypress-io/cypress/commit/a73adf2b4959f72789fa7d0ff9d6b9f301296258))
# [@cypress/design-system-v1.3.0](https://github.com/cypress-io/cypress/compare/@cypress/design-system-v1.2.0...@cypress/design-system-v1.3.0) (2021-04-26)
### Bug Fixes
* **ds:** make design-system tests display icons ([#16114](https://github.com/cypress-io/cypress/issues/16114)) ([480b4b0](https://github.com/cypress-io/cypress/commit/480b4b008c998fb68a45d1f98296a6b8aa500feb))
### Features
* Design System Setup ([#15776](https://github.com/cypress-io/cypress/issues/15776)) ([8eaf4b4](https://github.com/cypress-io/cypress/commit/8eaf4b478b1a359c9284d055634fb2032afda536))
# [@cypress/design-system-v1.2.0](https://github.com/cypress-io/cypress/compare/@cypress/design-system-v1.1.0...@cypress/design-system-v1.2.0) (2021-04-05)
### Bug Fixes
* remove failing imports ([#15661](https://github.com/cypress-io/cypress/issues/15661)) ([357a296](https://github.com/cypress-io/cypress/commit/357a296fde0355fda6388345b1d01e4ffa11d69c))
* **runner-ct:** add highlighting to spec list fuzzy find ([#15604](https://github.com/cypress-io/cypress/issues/15604)) ([56234e5](https://github.com/cypress-io/cypress/commit/56234e52d6d1cbd292acdfd5f5d547f0c4706b51))
### Features
* simplify vite server ([#15416](https://github.com/cypress-io/cypress/issues/15416)) ([adc2fc8](https://github.com/cypress-io/cypress/commit/adc2fc893fbf32f1f6121d18ddb8a8096e5ebf39))
* **deps:** update dependency electron to version 12.x 🌟 ([#15292](https://github.com/cypress-io/cypress/issues/15292)) ([b52ac98](https://github.com/cypress-io/cypress/commit/b52ac98a6944bc831221ccb730f89c6cc92a4573))
# [@cypress/design-system-v1.1.0](https://github.com/cypress-io/cypress/compare/@cypress/design-system-v1.0.0...@cypress/design-system-v1.1.0) (2021-03-19)
### Bug Fixes
* addressing style regressions in the CT runner ([#15542](https://github.com/cypress-io/cypress/issues/15542)) ([ecccf54](https://github.com/cypress-io/cypress/commit/ecccf5444041eeeb1aa6bc911c4e06b520182ce3))
* specs overflow horizontally and z-index issues ([#15547](https://github.com/cypress-io/cypress/issues/15547)) ([e60a029](https://github.com/cypress-io/cypress/commit/e60a02912f9d92ae8bce9d03c2167f9a393482f6))
### Features
* **design-system:** FileExplorer ([#15513](https://github.com/cypress-io/cypress/issues/15513)) ([f2b880c](https://github.com/cypress-io/cypress/commit/f2b880c09de5c4490027689af86b6844706c8a6b))
# @cypress/design-system-v1.0.0 (2021-03-15)
### Bug Fixes
* **runner-ct:** open link in external browser ([#15420](https://github.com/cypress-io/cypress/issues/15420)) ([d291157](https://github.com/cypress-io/cypress/commit/d291157f07ffebe961527fdd85c7ec51056801e7))
### Features
* initial infrastructure for design-system ([#15211](https://github.com/cypress-io/cypress/issues/15211)) ([f52d6e4](https://github.com/cypress-io/cypress/commit/f52d6e4ee10c82b766fefadaf8015a3f8bbc8899))
* Runner-CT UI Improvements ([#15327](https://github.com/cypress-io/cypress/issues/15327)) ([e69b996](https://github.com/cypress-io/cypress/commit/e69b9968912471b9ece6298afd47fc6f14728813))
-81
View File
@@ -1,81 +0,0 @@
# @cypress/design-system
A design system for the surfaces of testing softwares. 🐛💅
## Values
### It is discreet
Let the surfaces fade away to allow work to happen.
### It is native
Let the surfaces be familiar to where work happens.
### It is accessible
Let the surfaces honor accepted standards so everyone can use them.
### It is specialized
Let the surfaces be appropriate for the job to be done. Favor consistency over novelty, but not at the cost of functionality.
## Usage
The components work with or without the global stylesheet import. The stylesheet import is used to setup global scss tokens, colors, utility classes, and typography.
Component Usage:
```jsx
import { Button } from '@cypress/design-system'
```
To setup global CSS tokens and mixins, you can import the library's `index.scss`. You can either do this with `@use` or `@import` (See CSS Trick's intro to Sass modules [here](https://css-tricks.com/introducing-sass-modules/#import-files-with-use))
SCSS usage:
```scss
// scoped within the *.scss file
@use '@cypress/design-system' as *;
// import variables and mixins throughout the whole project
// or @import('@cypress/design-system');
.my-component {
text-color: $accent-color-01;
}
```
## Development
We are currently using:
* CSS Modules for styling
* TSX for components
* SCSS with module support
* Rollup to bundle
* Cypress CT as a development environment
* Webpack is required for Cypress CT but will soon be replaced by a rollup dev server
#### Developing locally
`yarn cy:open`
#### Building the library
`yarn build`
#### Deploying
TODO: Add netlify site support and static app wrapper
#### Testing
`yarn cy:run`
## TODO
1. Deploy a static page demo-ing the design system
2. Import the first component inside of RunnerCT
3. Hook up tests to circle
4. Publish the package on npm (switch `package.json`'s `publishConfig` to 'public' instead of 'restricted' and then merge into master)
## Changelog
[Changelog](./CHANGELOG.md)
-8
View File
@@ -1,8 +0,0 @@
module.exports = {
plugins: ['@babel/plugin-proposal-optional-chaining'],
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript',
],
}
-4
View File
@@ -1,4 +0,0 @@
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))
-22
View File
@@ -1,22 +0,0 @@
const { defineConfig } = require('cypress')
module.exports = defineConfig({
viewportWidth: 1024,
viewportHeight: 800,
video: false,
projectId: 'z9dxah',
env: {
reactDevtools: true,
},
fixturesFolder: false,
component: {
excludeSpecPattern: [
'**/__snapshots__/*',
'**/__image_snapshots__/*',
],
devServer: {
framework: 'react',
bundler: 'vite',
},
},
})
@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Components App</title>
</head>
<body>
<div data-cy-root></div>
</body>
</html>
@@ -1,14 +0,0 @@
import 'regenerator-runtime/runtime'
import 'cypress-real-events/support'
import '@percy/cypress'
import './storybook'
import 'normalize.css/normalize.css'
// 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)
@@ -1,4 +0,0 @@
import { setGlobalConfig } from '@storybook/testing-react'
import * as sbPreview from '../../.storybook/preview'
setGlobalConfig(sbPreview)
-11
View File
@@ -1,11 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>AUT Frame</title>
</head>
<body>
</body>
</html>
-121
View File
@@ -1,121 +0,0 @@
{
"name": "@cypress/design-system",
"version": "0.0.0-development",
"description": "Styles, standards, and components used throughout Cypress",
"main": "dist/index.js",
"scripts": {
"build": "rimraf dist && 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 --component --project ${PWD}",
"cy:open:debug": "node --inspect-brk ../../scripts/start.js --component-testing --project ${PWD}",
"cy:run": "node ../../scripts/cypress.js run --component --project ${PWD}",
"cy:run:debug": "node --inspect-brk ../../scripts/start.js --component-testing --run-project ${PWD}",
"storybook": "start-storybook -p 6006",
"test": "yarn cy:run",
"test-unit": "mocha -r ../../node_modules/@packages/web-config/node-register src/**/*.specmocha.*",
"transpile": "tsc",
"watch": "yarn build --watch"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.0.0",
"@fortawesome/fontawesome-svg-core": "^1.3.0",
"@fortawesome/free-brands-svg-icons": "^6.0.0",
"@fortawesome/free-solid-svg-icons": "^6.0.0",
"@fortawesome/react-fontawesome": "^0.1.17",
"@iconify/icons-vscode-icons": "^1.1.4",
"@iconify/react": "2.0.0-rc.8",
"@iconify/types": "^1.0.6",
"classnames": "^2.3.1",
"debug": "^4.3.2",
"react-aria": "^3.5.0",
"react-virtualized-auto-sizer": "^1.0.5",
"react-vtree": "3.0.0-beta.1",
"react-window": "^1.8.6"
},
"devDependencies": {
"@babel/core": "7.4.5",
"@babel/plugin-proposal-optional-chaining": "^7.13.8",
"@babel/preset-env": "7.4.5",
"@babel/preset-react": "7.0.0",
"@babel/preset-typescript": "7.10.4",
"@packages/web-config": "0.0.0-development",
"@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",
"@storybook/testing-react": "^0.0.12",
"@types/node": "14.14.31",
"@types/react-virtualized-auto-sizer": "^1.0.0",
"@types/react-window": "^1.8.2",
"@types/semver": "7.3.4",
"babel-loader": "8.0.6",
"css-loader": "^5.1.3",
"cypress": "0.0.0-development",
"cypress-real-events": "1.4.0",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"mocha-junit-reporter": "^2.0.0",
"mocha-multi-reporters": "^1.5.1",
"postcss": "^8.2.8",
"react": "16.8.6",
"react-dom": "16.8.6",
"rollup": "^2.38.5",
"rollup-plugin-copy": "^3.4.0",
"rollup-plugin-peer-deps-external": "2.2.4",
"rollup-plugin-postcss": "^4.0.0",
"sass": "1.44.0",
"sass-loader": "10.1.1",
"style-loader": "^2.0.0",
"svg-url-loader": "3.0.3",
"ts-node": "^10.2.1",
"tsc-alias": "^1.2.9",
"tsconfig-paths-webpack-plugin": "^3.5.1",
"typed-scss-modules": "^4.1.1",
"typescript": "^4.2.3",
"vite": "2.9.0-beta.3",
"webpack": "^4.44.2"
},
"peerDependencies": {
"react": "^=16.x || ^=17.x",
"react-dom": "^=16.x || ^=17.x"
},
"files": [
"dist",
"src"
],
"types": "dist/index.d.ts",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/cypress-io/cypress.git"
},
"homepage": "https://github.com/cypress-io/cypress/blob/master/npm/design-system/#readme",
"keywords": [
"design-system",
"cypress",
"cypress-io"
],
"unpkg": "dist/cypress-design-system.browser.js",
"module": "dist/cypress-design-system.esm-bundler.js",
"style": "dist/index.scss",
"publishConfig": {
"access": "restricted"
},
"standard": {
"globals": [
"Cypress",
"cy",
"expect"
]
}
}
-95
View File
@@ -1,95 +0,0 @@
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'
import image from '@rollup/plugin-image'
import copy from 'rollup-plugin-copy'
import { replaceTscAliasPaths } from 'tsc-alias'
import pkg from './package.json'
const cssFolders = require('./css.folders')
const banner = `
/**
* ${pkg.name} v${pkg.version}
* (c) ${new Date().getFullYear()} Cypress.io
* Released under the MIT License
*/
`
function createEntry () {
const config = {
input: 'src/index.ts',
external: [
'react',
'react-dom',
],
plugins: [
peerDepsExternal(),
// Mirrors that in tsconfig
ts({
declaration: true,
sourceMap: true,
inlineSources: true,
exclude: ['**/*.spec.tsx', '**/*.stories.tsx'],
}),
resolve(),
json(),
commonjs(),
postcss({
modules: {
globalModulePaths: ['src/css/derived/export.scss'],
},
use: [
['sass', {
includePaths: cssFolders,
}],
],
}),
image(),
copy({
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',
},
],
}),
{ name: 'test', writeBundle: () => {
replaceTscAliasPaths()
} },
],
output: {
banner,
name: 'CypressDesignSystem',
dir: './dist',
// file: pkg.module,
format: 'es',
globals: {
react: 'React',
'react-dom': 'ReactDOM',
},
sourcemap: true,
},
}
return config
}
export default [
createEntry(),
]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

@@ -1,25 +0,0 @@
import React from 'react'
// @ts-ignore
import LogoPng from '../../assets/cypress-logo.png'
import styles from './CypressLogo.module.scss'
const sizes = {
small: '8rem',
medium: '12rem',
large: '16rem',
}
interface LogoProps {
size: 'small' | 'medium' | 'large'
}
export const CypressLogo: React.FC<LogoProps> = (props) => {
return (
<img
className={styles.logo}
style={{ width: sizes[props.size] }}
src={LogoPng}
/>
)
}
@@ -1,143 +0,0 @@
import * as React from 'react'
import { mount } from 'cypress/react'
import { library } from '@fortawesome/fontawesome-svg-core'
import { fab } from '@fortawesome/free-brands-svg-icons'
import { fas } from '@fortawesome/free-solid-svg-icons'
import { mountAndSnapshot } from 'util/testing'
import { NavItem } from './types'
import { LeftNav } from './LeftNav'
library.add(fas)
library.add(fab)
const homeItem: NavItem = {
id: 'foo',
title: 'Foo button',
icon: 'home',
interaction: {
type: 'anchor',
href: 'https://cypress.io',
},
}
const makeOnClickItem: (options?: Partial<NavItem>) => NavItem = (options?: Partial<NavItem>) => {
return {
id: 'bar',
title: 'Bar button',
icon: 'key',
interaction: {
type: 'js',
onClick: () => {
},
},
...options,
}
}
const items = [homeItem, makeOnClickItem()]
describe('LeftNav', () => {
it('renders', () => {
mount(<LeftNav items={[]} />)
cy.get('nav').should('exist')
})
it('renders a stack of items', () => {
mountAndSnapshot(<LeftNav items={items} />)
cy.get('nav').should('exist')
})
it('properly follows anchor links', () => {
mount(<LeftNav items={[
{
id: 'foo',
title: 'Foo button',
icon: 'home',
interaction: {
type: 'anchor',
href: '#foo',
},
},
]}
/>)
cy.get('a').first().eq(0).click().url().should('include', '#foo')
})
it('should properly display in page', () => {
const Comp = () => (
<div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr' }}>
<LeftNav items={items} />
<div style={{
height: 1000,
width: 1000,
}}
>
This is the main page content
</div>
</div>
)
mount(<Comp />)
})
it('properly follows JS onclicks', () => {
const clickSpy = cy.spy()
const Wrapper = () => {
const [activeIndex, setActiveIndex] = React.useState<number>()
const items = [
homeItem,
makeOnClickItem({
location: 'bottom',
id: 'bottom-item',
icon: 'ad',
interaction: {
type: 'js',
onClick ({ index }) {
if (index === activeIndex) {
return setActiveIndex(undefined)
}
setActiveIndex(2)
},
},
}),
makeOnClickItem({
itemClassesActive: 'second-item-button-active',
itemClasses: 'second-item-button',
interaction: {
type: 'js',
onClick ({ index }) {
if (index === activeIndex) {
return setActiveIndex(undefined)
}
setActiveIndex(1)
clickSpy()
},
},
}),
]
return (
<div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr' }}>
<LeftNav
activeIndex={activeIndex}
items={items}
/>
<div style={{ height: 300, width: 1000 }}>This is the main page content</div>
</div>
)
}
mount(<Wrapper />)
cy.get('a').eq(1).click().should(() => {
expect(clickSpy).to.be.called
})
})
})
@@ -1,134 +0,0 @@
import * as React from 'react'
import cs from 'classnames'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { library } from '@fortawesome/fontawesome-svg-core'
import { fas } from '@fortawesome/free-solid-svg-icons'
library.add(fas)
import styles from './LeftNav.module.scss'
import { LeftNavProps, NavButtonProps, NavLocation, NavItem } from './types'
interface NavItemDefinedLocation extends NavItem {
location: NavLocation
_index: number
}
interface GroupedItems {
top: NavItemDefinedLocation[]
bottom: NavItemDefinedLocation[]
}
export const LeftNav: React.FC<LeftNavProps> = ({ items, activeIndex, leftNavClasses, navButtonClasses }) => {
const mappedItems = items.reduce<GroupedItems>((acc, curr, index) => {
if (curr.location === 'bottom') {
return {
top: acc.top,
bottom: acc.bottom.concat({
...curr,
_index: curr._index || index,
location: 'bottom',
}),
}
}
return {
top: acc.top.concat({
...curr,
_index: curr._index || index,
location: 'top',
}),
bottom: acc.bottom,
}
}, { top: [], bottom: [] })
const navItem = (item: NavItemDefinedLocation) => (
<NavButtonCell
key={item.id}
item={item}
isActive={item._index === activeIndex}
navButtonClasses={navButtonClasses}
index={item._index}
/>
)
const topNav = (
<nav
key='nav-section-top'
className={styles.top}
>
{mappedItems.top.map((item) => navItem(item))}
</nav>
)
const bottomNav = (
<nav
key='nav-section-bottom'
className={styles.bottom}
>
{mappedItems.bottom.map((item) => navItem(item))}
</nav>
)
const nav = (
<nav className={cs(styles.leftNav, leftNavClasses || '')}>
{topNav}
{bottomNav}
</nav>
)
return nav
}
export const NavButtonCell: React.FC<NavButtonProps> = ({ item: { title, icon, interaction, itemClasses, itemClassesActive = '', itemClassesInactive = '', location }, isActive, navButtonClasses, index }) => {
const commonClasses = cs(styles.item, itemClasses, navButtonClasses, {
[styles.active]: isActive,
[styles.inactive]: !isActive,
[itemClassesActive]: isActive,
[itemClassesInactive]: !isActive,
})
const faIcon = <FontAwesomeIcon className={styles.icon} icon={icon} size="1x" />
if (interaction.type === 'anchor') {
const anchorProps = {
href: interaction.href,
className: cs(styles.itemAnchor, commonClasses),
onClick: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
if (!interaction.onClick) {
return
}
interaction.onClick({ event, index })
},
title,
}
if (interaction.targetBlank) {
return (
<a
{...anchorProps}
target='_blank'
>
{faIcon}
</a>
)
}
return (
<a {...anchorProps}>
{faIcon}
</a>
)
}
return (
<a
className={commonClasses}
title={title}
onClick={(event) => interaction.onClick({ event, index })}
>
{faIcon}
</a>
)
}
@@ -1,3 +0,0 @@
export * from './LeftNav'
export * from './types'
@@ -1,49 +0,0 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core'
export type NavLocation = 'top' | 'bottom'
export interface NavClick {
index: number
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
}
export interface NavItem {
id: string
_index?: number
/**
* Displayed on hover
*/
title: string
icon: IconProp
itemClasses?: string
itemClassesActive?: string
itemClassesInactive?: string
location?: NavLocation
interaction: {
type: 'anchor'
href: string
targetBlank?: boolean
onClick?: (payload: NavClick) => void
} | {
type: 'js'
onClick: (payload: NavClick) => void
}
}
export interface LeftNavProps {
items: NavItem[]
activeIndex?: number
leftNavClasses?: string
navButtonClasses?: string
}
export interface NavButtonProps {
index: number
item: NavItem
isActive: boolean
navButtonClasses?: string
}
@@ -1,24 +0,0 @@
import React from 'react'
import { CypressLogo } from './CypressLogo/CypressLogo'
import { mount } from 'cypress/react'
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)
describe('Playground', () => {
it('cypress logo', () => {
mount(
<>
<CypressLogo size="small" />
<br />
<CypressLogo size="medium" />
<br />
<CypressLogo size="large" />
</>,
)
})
})
@@ -1,12 +0,0 @@
import * as React from 'react'
import { mount } from 'cypress/react'
import { composeStories } from '@storybook/testing-react'
import * as stories from './CollapsibleGroup.stories'
const { CollapsibleGroup } = composeStories(stories)
describe('<CollapsibleGroup />', () => {
it('playground', () => {
mount(<CollapsibleGroup />)
})
})
@@ -1,44 +0,0 @@
import React from 'react'
import { createStory, createStorybookConfig } from '../../stories/util'
import { CollapsibleGroup as CollapsibleGroupComponent } from './CollapsibleGroup'
import { PaddedBox } from '../../core/surface/paddedBox/PaddedBox'
export default createStorybookConfig({
title: 'Components/CollapsibleGroup',
})
export const CollapsibleGroup = createStory(() => {
return (
<div>
<CollapsibleGroupComponent title="Expand me">
<PaddedBox>
Collapsible padded box with content
</PaddedBox>
</CollapsibleGroupComponent>
<CollapsibleGroupComponent title="Defaults to open" defaultExpanded={true}>
<PaddedBox>
Collapsible padded box with content
</PaddedBox>
</CollapsibleGroupComponent>
</div>
)
})
export const Icons = createStory(() => {
return (
<div>
<CollapsibleGroupComponent title="Expand me" icons={{ expanded: 'chevron-down', collapsed: 'chevron-right' }}>
<PaddedBox>
Collapsible padded box with content
</PaddedBox>
</CollapsibleGroupComponent>
<CollapsibleGroupComponent title="Defaults to open" defaultExpanded={true}>
<PaddedBox>
Collapsible padded box with content
</PaddedBox>
</CollapsibleGroupComponent>
</div>
)
})
@@ -1,62 +0,0 @@
import cs from 'classnames'
import React, { useLayoutEffect, useState } from 'react'
import { CollapsibleGroupHeader, CollapsibleGroupHeaderProps } from './CollapsibleGroupHeader'
import { TextSizableComponent } from 'core/shared'
import styles from './CollapsibleGroup.module.scss'
interface CollapsibleGroupProps extends Omit<CollapsibleGroupHeaderProps, 'expanded' | 'onClick'>, TextSizableComponent {
style?: React.CSSProperties
title: string | JSX.Element
expanded?: boolean
defaultExpanded?: boolean
onToggle?: (isExpanded: boolean) => void
disable?: boolean
}
export const CollapsibleGroup: React.FC<CollapsibleGroupProps> = ({
className,
style,
expanded: externalExpanded,
defaultExpanded = externalExpanded,
onToggle,
children,
...props
}) => {
const [isExpanded, setIsExpanded] = useState(!!defaultExpanded)
useLayoutEffect(() => {
if (externalExpanded !== undefined) {
setIsExpanded(externalExpanded)
}
}, [externalExpanded])
return (
<div
className={cs([
{ [styles.expanded]: isExpanded },
styles.group,
className,
])}
style={style}
>
<CollapsibleGroupHeader
{...props}
expanded={isExpanded}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => {
onToggle?.(!isExpanded)
setIsExpanded((expanded) => !expanded)
}}
/>
<div className={styles.content}>
{isExpanded && children}
</div>
</div>
)
}
@@ -1,63 +0,0 @@
import React, { CSSProperties } from 'react'
import cs from 'classnames'
import { IconName } from '@fortawesome/fontawesome-common-types'
import { TextSizableComponent } from 'core/shared'
import { StyledText } from 'core/text/styledText'
import { Icon, IconProps } from 'core/icon/Icon'
import styles from './CollapsibleGroup.module.scss'
export interface IconInfo {
expanded: IconName
collapsed: IconName
iconProps?: Omit<IconProps, 'icon'>
}
export interface CollapsibleGroupHeaderProps extends TextSizableComponent {
style?: CSSProperties
title: string | JSX.Element
tooltipTitle?: string
/**
* The icons to render for expanded and collapsed states. If not specified, no icons will be rendered
*/
icons?: IconInfo
expanded: boolean
disabled?: boolean
onClick?: () => void
}
export const CollapsibleGroupHeader: React.FC<CollapsibleGroupHeaderProps> = ({
className,
style,
title,
tooltipTitle,
expanded,
disabled,
size,
lineHeight,
icons,
onClick,
}) => (
<StyledText
className={cs([
styles.header,
{ [styles.expanded]: expanded, [styles.disabled]: disabled },
className,
])}
style={style}
title={tooltipTitle ? tooltipTitle : typeof title === 'string' ? title : undefined}
size={size}
lineHeight={lineHeight}
onClick={onClick}
>
{icons && <Icon {...icons.iconProps} icon={expanded ? icons.expanded : icons.collapsed} />}
<div className={styles.title}>
{title}
</div>
</StyledText>
)
@@ -1,132 +0,0 @@
import React from 'react'
import { mount } from 'cypress/react'
import { FileTree } from './FileTree'
import { mountAndSnapshot } from 'util/testing'
const files = [
{
path: 'foo/bar/foo.spec.js',
},
{
path: 'qux/dog.spec.tsx',
},
{
path: 'merp/cat.spec.ts',
},
]
const assertSelectedBorder = ($els: JQuery<HTMLElement>) => {
const win = $els[0].ownerDocument.defaultView
if (win) {
const after = win.getComputedStyle($els[0], 'after')
// Verify that we see at least some border, indicating it is highlighted
const leftStyle = after.getPropertyValue('border-left-style')
expect(leftStyle).to.eq('solid')
const leftWidth = after.getPropertyValue('border-left-width')
expect(leftWidth).to.eq('2px')
}
}
beforeEach(() => {
cy.viewport(500, 500)
})
describe('FileTree', () => {
it('should send onFilePress callback on space and enter', () => {
const filePressStub = cy.stub()
mountAndSnapshot(
<div style={{ height: 500, width: 500 }}>
<FileTree files={files} rootDirectory="/" emptyPlaceholder="No specs found" onFilePress={filePressStub} />
</div>,
)
// Click on the "foo" directory
cy.get('div').contains('foo').click()
// navigate to "qux"
cy.focused().type('{downarrow}')
cy.get('div').contains('dog.spec.tsx').should('exist')
// collapse "qux", hiding "dog.spec.tsx"
cy.focused().type('{enter}')
cy.get('div').contains('dog.spec.tsx').should('not.exist')
// uncollapse "qux", revealing "dog.spec.tsx"
cy.focused().type('{enter}')
cy.get('div').contains('dog.spec.tsx').should('exist')
// navigate to "dog.spec.tsx"
cy.focused().type('{downarrow}')
cy.focused().type(' ').then(() => {
expect(filePressStub).to.have.been.callCount(1)
})
cy.focused().type('{uparrow}')
cy.focused().type('{uparrow}')
cy.focused().should('contain', 'foo/bar')
})
describe('focus', () => {
it('should automatically focus the first row when focused', () => {
mount(
<div style={{ height: 500, width: 500 }}>
<FileTree files={files} rootDirectory="/" emptyPlaceholder="No specs found" />
</div>,
)
cy.get('[data-cy=virtualized-tree]').focus()
cy.contains('.treeChild', '/').then(assertSelectedBorder)
})
it('should preserve focus state', () => {
mount(
<div>
<div style={{ height: 500, width: 500 }}>
<FileTree files={files} rootDirectory="/" emptyPlaceholder="No specs found" />
</div>
<button>Test</button>
</div>,
)
cy.get('[data-cy=virtualized-tree]').focus().type('{downarrow}').type('{downarrow}')
cy.get('button').focus()
cy.get('[data-cy=virtualized-tree]').focus()
cy.contains('.treeChild', 'foo.spec.js').then(assertSelectedBorder)
})
it('should scroll to item on keyboard input', () => {
const files = []
for (let i = 0; i < 100; i++) {
files.push({ path: `File ${i}` })
}
mount(
<div style={{ height: 500, width: 500 }}>
<FileTree files={files} rootDirectory="/" emptyPlaceholder="No specs found" />
</div>,
)
cy.get('[data-cy=virtualized-tree]').focus().type('{downarrow}').type('{downarrow}')
cy.get('[data-cy=virtualized-tree] > div').scrollTo('bottom')
cy.contains('.treeChild', 'File 99').should('be.visible')
cy.get('[data-cy=virtualized-tree]').focus().type('{downarrow}').type('{downarrow}')
cy.contains('.treeChild', 'File 3').should('be.visible')
})
})
})
@@ -1,38 +0,0 @@
/* eslint-disable react/jsx-no-bind */
import React from 'react'
import { Button } from '../../core/button/Button'
import { createStory, createStorybookConfig } from '../../stories/util'
import { FileTree as FileTreeComponent } from './FileTree'
export default createStorybookConfig({
title: 'Components/FileTree',
})
const paths = [
'/cypress/integration/spec1.tsx',
'/cypress/integration/spec2.tsx',
'/cypress/integration/feature1/spec3.tsx',
'/cypress/integration/feature1/spec4.tsx',
'/cypress/integration/feature1/sub/spec6.tsx',
'/rootspec.tsx',
'/cypress/whyisthishere.spec.js',
'/src/core/foo.spec.js',
].map((path) => ({ path }))
export const FileTree = createStory(() => {
return (
<div>
<Button color="white" aria-label='Before focus'>Before focus</Button>
<div style={{ width: 800, height: 400 }}>
<FileTreeComponent<{path: string}>
files={paths}
rootDirectory="/"
emptyPlaceholder="Placeholder"
/>
</div>
<Button color="white" aria-label='After focus'>After focus</Button>
</div>
)
})
@@ -1,134 +0,0 @@
import React, { CSSProperties, useMemo } from 'react'
import { VirtualizedTree } from 'components/virtualizedTree/VirtualizedTree'
import { CollapsibleGroupHeader, IconInfo } from 'components/collapsibleGroup/CollapsibleGroupHeader'
import { LeafProps, OnNodeKeyDown, OnNodePress, ParentProps } from 'components/virtualizedTree/types'
import { Placeholder } from 'core/text/placeholder'
import { buildTree } from './buildTree'
import { FileBase, FilePressEvent, FileTreeProps, TreeFile, TreeFolder } from './types'
import { FileTreeFile, NameWithHighlighting } from './FileTreeFile'
import { FileTreeSelectedContext } from './state'
import styles from './FileTree.module.scss'
interface MutableFilePressEvent extends Omit<FilePressEvent, 'defaultPrevented'> {
defaultPrevented: boolean
}
const treeStyle: CSSProperties = { overflowX: 'hidden' }
export const FileTree = <T extends FileBase>({
innerRef,
files,
rootDirectory,
emptyPlaceholder,
selectedId,
leftOffset,
onRenderFolder,
onRenderFile,
onFolderPress,
onFilePress,
onFolderKeyDown,
onFileKeyDown,
}: FileTreeProps<T>) => {
const tree = useMemo(() => buildTree(files, rootDirectory), [files, rootDirectory])
const ParentComponent = useMemo(() => onRenderFolder ?? createDefaultFolderComponent(leftOffset), [leftOffset, onRenderFolder])
const LeafComponent = useMemo(() => onRenderFile ?? createDefaultFileComponent(leftOffset), [leftOffset, onRenderFile])
const onNodePress = useMemo<OnNodePress<TreeFile<T>, TreeFolder<T>> | undefined>(() => (node, event) => {
let customEvent: MutableFilePressEvent = {
...event,
defaultPrevented: false,
preventDefault: () => {},
}
customEvent.preventDefault = () => {
customEvent!.defaultPrevented = true
}
if (node.type === 'parent') {
onFolderPress?.(node.data, customEvent)
if (!customEvent?.defaultPrevented) {
node.setOpen(!node.isOpen)
}
} else {
onFilePress?.(node.data, customEvent)
}
}, [onFolderPress, onFilePress])
const onNodeKeyDown = useMemo<OnNodeKeyDown<TreeFile<T>, TreeFolder<T>> | undefined>(() => onFolderKeyDown || onFileKeyDown ? (node, event) => {
if (node.type === 'parent') {
onFolderKeyDown?.(node.data, event)
} else {
onFileKeyDown?.(node.data, event)
}
} : undefined, [onFolderKeyDown, onFileKeyDown])
return (
tree ? (
<FileTreeSelectedContext.Provider value={selectedId}>
<VirtualizedTree<TreeFile<T>, TreeFolder<T>>
innerRef={innerRef}
className={styles.tree}
// No x scrollbar. Unfortunately, react-vtree sets overflow using `style`, so we also have to
style={treeStyle}
tree={tree}
// TODO: This is hardcoded to spacing ml, but the API doesn't accept REM, only pixels
defaultItemSize={20}
showRoot={true}
onNodePress={onNodePress}
onNodeKeyDown={onNodeKeyDown}
onRenderParent={ParentComponent}
onRenderLeaf={LeafComponent}
/>
</FileTreeSelectedContext.Provider>
) : (
<div className={styles.placeholder}>
<Placeholder>
{emptyPlaceholder}
</Placeholder>
</div>
)
)
}
const icons: IconInfo = { expanded: 'chevron-down', collapsed: 'chevron-right', iconProps: { sizeWithoutCenter: true } }
const createDefaultFolderComponent = <T extends FileBase>(leftOffset = 0) => ({ parent: { id, name, indexes }, depth, isOpen }: ParentProps<TreeFolder<T>>) => (
<CollapsibleGroupHeader
className={styles.node}
style={depth > 0 || leftOffset !== 0 ? {
paddingLeft: `${depth + leftOffset}rem`,
backgroundSize: `${depth}rem 100%`,
// spacing(s) = 0.5rem is the default spacing as per styles.node
backgroundPositionX: leftOffset !== 0 ? `${0.5 + leftOffset}rem` : undefined,
} : undefined}
title={(
<NameWithHighlighting
name={name}
path={id}
indexes={indexes ?? []}
/>
)}
tooltipTitle={id}
expanded={isOpen}
icons={icons}
lineHeight="tight"
/>
)
const createDefaultFileComponent = <T extends FileBase>(leftOffset = 0) => (props: LeafProps<TreeFile<T>>) => (
<FileTreeFile
{...props}
style={props.depth > 0 || leftOffset !== 0 ? {
paddingLeft: `${props.depth + leftOffset}rem`,
backgroundSize: `${props.depth}rem 100%`,
// spacing(s) = 0.5rem is the default spacing as per styles.node
backgroundPositionX: leftOffset !== 0 ? `${0.5 + leftOffset}rem` : undefined,
} : undefined}
item={props.leaf}
indexes={props.leaf.file.indexes ?? []}
/>
)
@@ -1,104 +0,0 @@
import React, { CSSProperties } from 'react'
import cs from 'classnames'
import type { IconifyIcon } from '@iconify/types'
import { InlineIcon } from '@iconify/react'
import javascriptIcon from '@iconify/icons-vscode-icons/file-type-js-official'
import typescriptIcon from '@iconify/icons-vscode-icons/file-type-typescript-official'
import reactJs from '@iconify/icons-vscode-icons/file-type-reactjs'
import reactTs from '@iconify/icons-vscode-icons/file-type-reactts'
import { StyledText } from 'core/text/styledText'
import { FileBase, TreeFile } from './types'
import styles from './FileTree.module.scss'
import { useSelectedId } from './state'
export interface NodeComponentProps<T> {
item: T
depth: number
indexes: number[]
}
export interface FileComponentProps<T extends FileBase> extends NodeComponentProps<TreeFile<T>> {
style?: CSSProperties
depth: number
remeasure: () => void
}
export const icons: Record<string, { icon: IconifyIcon }> = {
js: { icon: javascriptIcon },
ts: { icon: typescriptIcon },
tsx: { icon: reactTs },
jsx: { icon: reactJs },
} as const
export const FileTreeFile = <T extends FileBase>({ item, indexes, style }: FileComponentProps<T>) => {
const selectedId = useSelectedId()
const isSelected = item.id === selectedId
const ext = getExt(item.name)
const inlineIconProps = ext ? icons[ext] : {
// If we don't have an icon for the extension, don't render an icon
icon: '',
}
return (
<div
className={cs(styles.node, styles.file, { [styles.active]: isSelected })}
style={style}
title={item.file.path}
data-cy={isSelected ? 'selected-spec' : ''}
>
<InlineIcon {...inlineIconProps} />
<NameWithHighlighting
name={item.name}
path={item.file.path}
indexes={indexes}
/>
</div>
)
}
export const NameWithHighlighting: React.FC<{
name: string
path: string
indexes: number[]
}> = ({ name, path, indexes }) => {
const lengthOffset = path.length - name.length
const indexSet = indexes.reduce((acc, current) => {
const newIndex = current - lengthOffset
if (newIndex >= 0) {
acc.add(newIndex)
}
return acc
}, new Set<number>())
// TODO: It would be nice if we didn't make `n` React nodes, and instead properly inserted only the spans when necessary
return (
<StyledText className={styles.highlight} size="ms" lineHeight="tight">
{[...name].map((char, index) => indexSet.has(index) ? (
// eslint-disable-next-line react/no-array-index-key
<span key={index}>
{char}
</span>
) : (
// eslint-disable-next-line react/no-array-index-key
<React.Fragment key={index}>
{char}
</React.Fragment>
))}
</StyledText>
)
}
const extensionRegex = /(?:\.([^.]+))?$/
const getExt = (path: string) => {
const extensionMatches = path.match(extensionRegex)
return extensionMatches ? extensionMatches[1] : ''
}
@@ -1,119 +0,0 @@
import { expect } from 'chai'
import { buildTree } from './buildTree'
import { FileBase, TreeFile, TreeFolder } from './types'
const getLeaves = (tree: TreeFolder<FileBase>): Array<TreeFile<FileBase>> => {
const folders: Array<TreeFolder<FileBase>> = []
const files: Array<TreeFile<FileBase>> = []
for (const item of tree.children) {
if ('children' in item) {
folders.push(item)
} else {
files.push(item)
}
}
return [...files, ...folders.flatMap(getLeaves)]
}
describe('tree contents', () => {
it('should contain all files', () => {
const files = [
'forOfStatement.js',
'foo/y/bar.js',
'foo/bar',
'a/b/c',
].map((path) => ({ path }))
const tree = buildTree(files, '/')
expect(tree).to.not.be.undefined
const leaves = getLeaves(tree!)
files.forEach(({ path }) => {
const pathSegments = path.split('/')
const fileName = pathSegments[pathSegments.length - 1]
expect(leaves).to.deep.include({ id: path, name: fileName, file: { path } })
})
})
it('should contain folders with files', () => {
const files = [
'folder1/file1.js',
'anotherFolder/file2.ts',
'folder3/file3.tsx',
'oneMore/file4.js',
].map((path) => ({ path }))
const tree = buildTree(files, '/')!
expect(tree).to.not.be.undefined
expect(tree.children.length).to.eq(4)
for (let i = 0; i < files.length; i++) {
const { path } = files[i]
const child = tree.children[i] as TreeFolder<FileBase>
const [folder, file] = path.split('/')
expect(child).to.have.property('id', folder)
expect(child.children.length).to.eq(1)
expect(child.children[0].id).to.eq(path)
expect(child.children[0].name).to.eq(file)
}
})
})
describe('tree collapsing', () => {
it('should collapse directories into a single node', () => {
const files = [
{ path: 'folder1/folder2/folder3/moreFolder/file1.js' },
]
const tree = buildTree(files, '/')!
expect(tree).to.not.be.undefined
expect(tree.children.length).to.eq(1)
expect(tree.id).to.eq('folder1/folder2/folder3/moreFolder')
expect(tree.name).to.eq('folder1/folder2/folder3/moreFolder')
expect(tree.children[0].id).to.eq(files[0].path)
})
it('should collapse directories with multiple children into a single shared node', () => {
const files = [
{ path: 'folder1/folder2/folder3/moreFolder/file1.js' },
{ path: 'folder1/folder2/diff/filesChanged/file2.js' },
]
const tree = buildTree(files, '/')!
expect(tree).to.not.be.undefined
expect(tree.children.length).to.eq(2)
const [file1Folder, file2Folder] = tree.children as [TreeFolder<FileBase>, TreeFolder<FileBase>]
expect(tree.id).to.eq('folder1/folder2')
expect(tree.name).to.eq('folder1/folder2')
expect(file1Folder.id).to.eq('folder1/folder2/folder3/moreFolder')
expect(file1Folder.name).to.eq('folder3/moreFolder')
expect(file2Folder.id).to.eq('folder1/folder2/diff/filesChanged')
expect(file2Folder.name).to.eq('diff/filesChanged')
expect(file1Folder.children.length).to.eq(1)
expect(file1Folder.children[0].id).to.eq('folder1/folder2/folder3/moreFolder/file1.js')
expect(file1Folder.children[0].name).to.eq('file1.js')
expect(file2Folder.children.length).to.eq(1)
expect(file2Folder.children[0].id).to.eq('folder1/folder2/diff/filesChanged/file2.js')
expect(file2Folder.children[0].name).to.eq('file2.js')
})
})
@@ -1,129 +0,0 @@
import { FileBase, TreeFolder } from './types'
interface BuildingFile<T extends FileBase> {
name: string
path: string
file: T
}
interface BuildingFolder<T extends FileBase> {
name: string
path: string
/**
* If undefined, this folder is root
*/
parent: BuildingFolder<T> | undefined
files: Array<BuildingFile<T>>
folders: Record<string, BuildingFolder<T>>
indexes?: number[]
}
const treeToFolders = <T extends FileBase>({ path, name, files, folders, indexes }: BuildingFolder<T>): TreeFolder<T> => {
return {
id: path,
name,
children: [...Object.values(folders).map(treeToFolders), ...files.map(({ path, name, file }) => {
return {
id: path,
name,
file,
}
})],
indexes,
}
}
const compressTree = <T extends FileBase>(folder: BuildingFolder<T>) => {
while (Object.keys(folder.folders).length === 1 && folder.files.length < 1) {
// Not root, has only one folder child and no file children
const child = folder.folders[Object.keys(folder.folders)[0]]
folder.folders = child.folders
folder.files = child.files
folder.path = child.path
folder.name = `${folder.name}/${child.name}`
folder.indexes = child.indexes
child.parent = folder.parent
}
for (const childKey of Object.keys(folder.folders)) {
compressTree(folder.folders[childKey])
}
}
export const buildTree = <T extends FileBase>(files: T[], rootDirectory: string) => {
if (files.length < 1) {
return undefined
}
const rootPathParts = rootDirectory.split('/')
const lastRootPart = rootPathParts[rootPathParts.length - 1]
const rootName = lastRootPart
? lastRootPart
: rootPathParts.length > 1
? rootPathParts[rootPathParts.length - 2]
// If no root path, use empty string as root
: ''
const rootFolder: BuildingFolder<T> = {
path: rootDirectory,
name: rootName,
files: [],
folders: {},
parent: undefined,
}
for (const file of files) {
let parentDirectory = rootFolder
// All paths should be POSIX compliant
const pathParts = file.path.split('/')
for (let i = 0; i < pathParts.length; i++) {
const part = pathParts[i]
if (i === pathParts.length - 1) {
// Last item, filename
parentDirectory.files.push({
path: file.path,
name: part,
file,
})
} else {
if (part in parentDirectory.folders) {
// Directory already exists, switch to new parent
parentDirectory = parentDirectory.folders[part]
} else {
// Directory hasn't been seen before
const newDirectory: BuildingFolder<T> = {
path: pathParts.slice(0, i + 1).join('/'),
name: part,
files: [],
folders: {},
parent: parentDirectory,
// First file. If we have highlight indexes, add to directory
// TODO: This is incorrect. This only has one entry for the directory, when individual files in that directory could have different indexes
indexes: file.indexes,
}
parentDirectory.folders[part] = newDirectory
parentDirectory = newDirectory
}
}
}
}
compressTree(rootFolder)
if (rootFolder.name !== '/' && rootFolder.name[0] === '/') {
// As long as root folder isn't the filesystem root, trim the beginning slash
rootFolder.name = rootFolder.name.slice(1)
} else if (rootFolder.name === '') {
rootFolder.name = '/'
}
return treeToFolders(rootFolder)
}
@@ -1,5 +0,0 @@
export * from './FileTree'
export * from './FileTreeFile'
export * from './types'
@@ -1,5 +0,0 @@
import { createContext, useContext } from 'react'
export const FileTreeSelectedContext = createContext<string | undefined>(undefined)
export const useSelectedId = () => useContext(FileTreeSelectedContext)
@@ -1,58 +0,0 @@
import type { PressEvent } from '@react-types/shared'
import type { MutableRefObject, ReactNode } from 'react'
import { LeafProps, ParentProps, SpecificTreeNode, VirtualizedTreeRef } from 'components/virtualizedTree/types'
export interface FileTreeProps<T extends FileBase> {
/**
* Use instead of `ref`. React/TS still doesn't have a good solution for `forwardRef` generics
*/
innerRef?: MutableRefObject<VirtualizedTreeRef>
files: T[]
rootDirectory: string
/**
* If specified, the node with this ID will be highlighted
*/
selectedId?: string
emptyPlaceholder: ReactNode
/**
* If specified, offset tree nodes by this amount in REM
*/
leftOffset?: number
onRenderFolder?: (folder: ParentProps<TreeFolder<T>>) => JSX.Element
onRenderFile?: (folder: LeafProps<TreeFile<T>>) => JSX.Element
onFolderPress?: (folder: SpecificTreeNode<TreeFolder<T>>, event: FilePressEvent) => void
onFilePress?: (file: SpecificTreeNode<TreeFile<T>>, event: FilePressEvent) => void
onFolderKeyDown?: (folder: SpecificTreeNode<TreeFolder<T>>, event: React.KeyboardEvent<HTMLDivElement>) => void
onFileKeyDown?: (file: SpecificTreeNode<TreeFile<T>>, event: React.KeyboardEvent<HTMLDivElement>) => void
}
export interface FilePressEvent extends PressEvent {
readonly defaultPrevented: boolean
preventDefault: () => void
}
export interface FileBase {
path: string
indexes?: number[]
}
export interface TreeFile<T extends FileBase> {
id: string
name: string
file: T
}
export interface TreeFolder<T extends FileBase> {
id: string
name: string
indexes?: number[]
children: Array<TreeFolder<T> | TreeFile<T>>
}
@@ -1,76 +0,0 @@
import * as React from 'react'
import { mount } from 'cypress/react'
import { SearchInput } from './SearchInput'
import { mountAndSnapshot } from 'util/testing'
const { useCallback, useState } = 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', () => {
function onInput () {}
mountAndSnapshot(<SearchInput placeholder="foo" value="" aria-label="Search" onInput={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')
cy.percySnapshot()
})
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', () => {
function onInput () {}
mount(<SearchInput placeholder="foo" value="a value" aria-label="Search" onInput={onInput} />)
cy.get('[aria-label="Clear search"]').click()
cy.get('input').should('be.focused')
})
})
})
@@ -1,20 +0,0 @@
import * as React from 'react'
import { createStory, createStorybookConfig } from 'stories/util'
import { SearchInput as SearchInputComponent } from './SearchInput'
const { useState } = 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>
)
})
@@ -1,81 +0,0 @@
import React, { KeyboardEvent, useMemo, 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
onEnter?: (input: string) => void
onVerticalArrowKey?: (key: 'up' | 'down') => void
['aria-label']: string
}
const prefixItem: IconSettings = {
icon: 'search',
}
export const SearchInput: React.FC<SearchInputProps> = ({ inputRef = null, onInput: externalOnInput, onEnter, onVerticalArrowKey, ...props }) => {
const ref = React.useRef<HTMLInputElement>(null)
useCombinedRefs(ref, inputRef)
const onInput = useCallback((e: FormEvent<HTMLInputElement>) => externalOnInput(e.currentTarget.value), [externalOnInput])
const onClear = useCallback(() => {
if (ref.current) {
ref.current.value = ''
}
externalOnInput('')
ref.current?.focus()
}, [externalOnInput])
const onKeyDown = useMemo(() => onEnter || onVerticalArrowKey ? (e: KeyboardEvent<HTMLInputElement>) => {
switch (e.key) {
case 'Enter':
onEnter?.(e.currentTarget.value)
break
case 'ArrowUp':
onVerticalArrowKey?.('up')
break
case 'ArrowDown':
onVerticalArrowKey?.('down')
break
default:
return
}
// If we get here, we matched a key
e.preventDefault()
} : undefined, [onEnter, onVerticalArrowKey])
const value = props.value ?? ref.current?.value
return (
<IconInput
{...props}
inputRef={ref}
label={{ type: 'aria', contents: props['aria-label'] }}
prefixIcon={prefixItem}
suffixIcon={value ? { icon: 'times', onPress: onClear, 'aria-label': 'Clear search' } : undefined}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onInput={onInput}
onKeyDown={onKeyDown}
/>
)
}
@@ -1,14 +0,0 @@
import * as React from 'react'
import { composeStories } from '@storybook/testing-react'
import { mountAndSnapshot } from 'util/testing'
import * as stories from './VirtualizedTree.stories'
const { VirtualizedTree } = composeStories(stories)
// TODO: Autogenerate from stories
describe('<VirtualizedTree />', () => {
it('VirtualizedTree', () => {
mountAndSnapshot(<VirtualizedTree />)
})
})
@@ -1,97 +0,0 @@
/* eslint-disable react/jsx-no-bind */
import React from 'react'
import { Button } from '../../core/button/Button'
import { createStory, createStorybookConfig } from '../../stories/util'
import { VirtualizedTree as VirtualizedTreeComponent } from './VirtualizedTree'
export default createStorybookConfig({
title: 'Components/VirtualizedTree',
})
type TreeLeaf = {
id: string
name: string
}
type TreeParent = {
id: string
name: string
children: Array<TreeLeaf | TreeParent>
}
const tree: TreeParent = {
id: 'root',
name: 'Root',
children: [
{
id: 'child1',
name: 'Child 1',
children: [
{
id: 'child11',
name: 'Child 11',
},
{
id: 'child12',
name: 'Child 12',
children: [
{
id: 'child121',
name: 'Child 121',
},
],
},
],
},
{
id: 'child2',
name: 'Child 2',
},
{
id: 'child3',
name: 'Child 3',
},
{
id: 'child4',
name: 'Child 4',
children: [
{
id: 'child41',
name: 'Child 41',
},
],
},
],
}
export const VirtualizedTree = createStory(() => {
return (
<div>
<Button color="white" aria-label='Before focus'>Before focus</Button>
<div style={{ width: 800, height: 400 }}>
<VirtualizedTreeComponent<TreeLeaf, TreeParent>
tree={tree}
defaultItemSize={20}
onNodePress={(node) => {
if (node.type === 'parent') {
node.setOpen(!node.isOpen)
}
}}
onRenderParent={({ parent, depth, isOpen, setOpen }) => (
<div style={{ marginLeft: 20 * depth, backgroundColor: 'red', cursor: 'pointer' }}>
{parent.name}
</div>
)}
onRenderLeaf={({ leaf, depth }) => (
<div style={{ marginLeft: 20 * depth }}>
{leaf.name}
</div>
)}
/>
</div>
<Button color="white" aria-label='After focus'>After focus</Button>
</div>
)
})
@@ -1,326 +0,0 @@
import React, {
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react'
import AutoSizer from 'react-virtualized-auto-sizer'
import { VariableSizeNodePublicState, VariableSizeTree } from 'react-vtree'
import type { NodeComponentProps } from 'react-vtree/dist/lib/Tree'
import { useCombinedRefs } from 'hooks/useCombinedRefs'
import { FocusStateContext, FocusStateHasFocusContext, useFocusDispatch } from './focusState'
import {
createPressEventNode,
isParent,
LeafTreeBase,
OnNodePress,
ParentTreeBase,
TreeNode,
TreeNodeData,
VirtualizedTreeProps,
} from './types'
import { TreeChild } from './VirtualizedTreeChild'
import styles from './VirtualizedTree.module.scss'
const VirtualizedTreeContents = <
TLeaf extends LeafTreeBase,
TParent extends ParentTreeBase<TLeaf>
>({
innerRef,
treeRef,
tree,
defaultItemSize,
overscanCount = 20,
indentSize,
showRoot,
shouldMeasure,
onNodePress: externalOnNodePress,
onNodeKeyDown,
onRenderLeaf,
onRenderParent,
...props
}: VirtualizedTreeProps<TLeaf, TParent>) => {
type TNodeData = TreeNodeData<TLeaf, TParent>
const wrapperRef = useRef<HTMLDivElement | null>(null)
const internalRef = useRef<VariableSizeTree<TNodeData> | null>(null)
useCombinedRefs(internalRef, treeRef ?? null)
const [focusIdRef, dispatch] = useFocusDispatch()
const [hasFocus, setHasFocus] = useState(false)
useImperativeHandle(innerRef, () => ({
focus: () => wrapperRef.current?.focus(),
}))
const treeWalker = useMemo(() => {
const buildNodeData = (node: TLeaf | TParent, nestingLevel: number, isFirst: boolean): TreeNode<TLeaf, TParent> => ({
data: {
id: node.id,
node,
nestingLevel,
isOpenByDefault: true,
defaultHeight: defaultItemSize,
isFirst,
},
})
function* walker (): Generator<TreeNode<TLeaf, TParent> | undefined, undefined, TreeNode<TLeaf, TParent>> {
if (showRoot) {
yield buildNodeData(tree, 0, true)
} else {
// Push all children of root as many psuedo roots
for (let i = 0; i < tree.children.length; i++) {
yield buildNodeData(tree.children[i] as TLeaf | TParent, 0, i === 0)
}
}
while (true) {
const parent = yield
if (!isParent(parent.data.node)) {
continue
}
for (const child of parent.data.node.children) {
yield buildNodeData(child as TLeaf | TParent, parent.data.nestingLevel + 1, false)
}
}
}
return walker
}, [tree, showRoot, defaultItemSize])
const currentSelectionIndex = useCallback(() => {
const order = internalRef.current?.state.order
if (order === undefined) {
return -1
}
if (order !== undefined) {
return focusIdRef.current ? order.indexOf(focusIdRef.current) ?? -1 : -1
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const onNodePress = useCallback<OnNodePress<TLeaf, TParent>>((node, event) => {
dispatch(node.data.id)
externalOnNodePress?.(node, event)
wrapperRef.current?.focus()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [externalOnNodePress])
const onKeyDown = useMemo(() => {
const currentNode = () => {
const order = internalRef.current?.state.order
const currentIndex = currentSelectionIndex()
if (order === undefined || currentIndex === undefined || currentIndex === -1) {
return undefined
}
return internalRef.current?.state.records.get(order[currentIndex])?.public
}
return (event: React.KeyboardEvent<HTMLDivElement>) => {
const order = internalRef.current?.state.order
if (!order) {
return
}
const node = currentNode()
if (node) {
const { data, isOpen, setOpen } = node
onNodeKeyDown?.(createPressEventNode(data as TreeNodeData<TLeaf, TParent>, isOpen, setOpen), event)
if (event.defaultPrevented) {
return
}
}
let id = node?.data.id
switch (event.key) {
case 'ArrowDown': {
const currentIndex = currentSelectionIndex()
if (currentIndex === undefined) {
break
}
const newSelectionIndex = currentIndex + 1
if (newSelectionIndex >= order.length) {
break
}
id = order[newSelectionIndex]
dispatch(id)
break
}
case 'ArrowUp': {
const currentIndex = currentSelectionIndex()
if (currentIndex === undefined) {
break
}
let newSelectionIndex = currentIndex - 1
if (newSelectionIndex < 0) {
newSelectionIndex = 0
}
id = order[newSelectionIndex]
dispatch(id)
break
}
case 'ArrowRight': {
if (!node) {
break
}
const { data, isOpen, setOpen } = node
if (isParent(data.node) && !isOpen) {
setOpen(true)
}
break
}
case 'ArrowLeft': {
if (!node) {
break
}
const { data, isOpen, setOpen } = node
if (isParent(data.node) && isOpen) {
setOpen(false)
}
break
}
case 'Enter':
case 'Spacebar':
case ' ': {
if (!node) {
break
}
const { data, isOpen, setOpen } = node
onNodePress?.(createPressEventNode(data as TreeNodeData<TLeaf, TParent>, isOpen, setOpen), {
type: 'press',
pointerType: 'keyboard',
target: event.currentTarget,
shiftKey: event.shiftKey,
metaKey: event.metaKey,
ctrlKey: event.ctrlKey,
})
break
}
default: {
return
}
}
if (id) {
// Make sure the node that was pressed is in view
internalRef.current?.scrollToItem(id)
}
event.preventDefault()
event.stopPropagation()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [onNodePress, onNodeKeyDown])
const onFocus = useCallback(() => {
setHasFocus(true)
const state = internalRef.current?.state
const currentIndex = currentSelectionIndex()
if (currentIndex === -1 && state && (state.order?.length ?? 0) > 0) {
// No selected row
dispatch(state.order![0])
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const onBlur = useCallback(() => setHasFocus(false), [])
useEffect(() => {
// Clear selected node
dispatch(undefined)
internalRef.current?.recomputeTree({
refreshNodes: true,
useDefaultHeight: true,
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tree])
const treeRow = useCallback((props: NodeComponentProps<TNodeData, VariableSizeNodePublicState<TNodeData>>) => {
return (
<TreeChild
{...props}
indentSize={indentSize}
showRoot={showRoot}
shouldMeasure={shouldMeasure}
onNodeKeyDown={onNodeKeyDown}
onNodePress={onNodePress}
onRenderLeaf={onRenderLeaf}
onRenderParent={onRenderParent}
/>
)
}, [showRoot, indentSize, shouldMeasure, onNodePress, onNodeKeyDown, onRenderLeaf, onRenderParent])
const sizer = useCallback(({ width, height }) => (
<div ref={wrapperRef} className={styles.focusWrapper} tabIndex={0} data-cy="virtualized-tree" onKeyDown={onKeyDown} onFocus={onFocus} onBlur={onBlur}>
<VariableSizeTree<TNodeData>
{...props}
ref={internalRef}
treeWalker={treeWalker}
width={width}
height={height}
overscanCount={overscanCount}
>
{treeRow}
</VariableSizeTree>
</div>
), [overscanCount, props, treeRow, treeWalker, onKeyDown, onFocus, onBlur])
// TODO: Figure out the proper accessibility wrappers
return (
<FocusStateHasFocusContext.Provider value={hasFocus}>
<AutoSizer>
{sizer}
</AutoSizer>
</FocusStateHasFocusContext.Provider>
)
}
export const VirtualizedTree = <
TLeaf extends LeafTreeBase,
TParent extends ParentTreeBase<TLeaf>
>(props: VirtualizedTreeProps<TLeaf, TParent>) => (
<FocusStateContext>
<VirtualizedTreeContents {...props} />
</FocusStateContext>
)
@@ -1,92 +0,0 @@
import React, { memo, useCallback, useMemo } from 'react'
import { usePress } from '@react-aria/interactions'
import { PressEvent } from '@react-types/shared'
import cs from 'classnames'
import { useMeasure } from 'hooks/useMeasure'
import { createPressEventNode, InternalChildProps, InternalOnRenderChildProps, isParent, LeafTreeBase, ParentTreeBase, treeChildClass } from './types'
import { useFocusState } from './focusState'
import styles from './VirtualizedTree.module.scss'
export const TreeChild = <
TLeaf extends LeafTreeBase,
TParent extends ParentTreeBase<TLeaf>
>({
data,
isOpen,
style,
height,
indentSize,
showRoot,
shouldMeasure,
onNodePress,
setOpen,
resize,
onRenderLeaf,
onRenderParent,
}: InternalChildProps<TLeaf, TParent>) => {
const globalFocusId = useFocusState()
const id = data.node.id
const isFocused = globalFocusId === id
const resizer = useCallback((height: number) => resize(height, true), [resize])
const { setRef, remeasure } = useMeasure(height, resizer, [data, style, isOpen], !shouldMeasure)
const onPress = useMemo(() => onNodePress ? {
onPress: (event: PressEvent) =>
onNodePress(createPressEventNode(data, isOpen, setOpen), event),
} : {}, [data, isOpen, setOpen, onNodePress])
const { pressProps } = usePress(onPress)
return id !== 'root' || showRoot ? (
// Wrapper is required for indent margin to work correctly with the tree's absolute positioning
<span style={style}>
<div
ref={setRef}
{...pressProps}
className={cs(treeChildClass, styles.child, { [styles.focus]: isFocused })}
style={indentSize ? { marginLeft: `${data.nestingLevel * indentSize}rem` } : undefined}
>
<MemoedOnRenderChild
data={data}
isOpen={isOpen}
setOpen={setOpen}
remeasure={remeasure}
onRenderLeaf={onRenderLeaf}
onRenderParent={onRenderParent}
/>
</div>
</span>
) : null
}
const OnRenderChild = <
TLeaf extends LeafTreeBase,
TParent extends ParentTreeBase<TLeaf>
>({
data: { node, nestingLevel },
isOpen,
setOpen,
remeasure,
onRenderLeaf,
onRenderParent,
}: InternalOnRenderChildProps<TLeaf, TParent>) =>
isParent(node)
? onRenderParent({
parent: node,
depth: nestingLevel,
isOpen,
setOpen,
remeasure,
})
: onRenderLeaf({
leaf: node,
depth: nestingLevel,
remeasure,
})
// Memo `onRender` callback results to double the speed of height calculation
const MemoedOnRenderChild = memo(OnRenderChild) as typeof OnRenderChild
@@ -1,36 +0,0 @@
import React, { createContext, Dispatch, RefObject, useContext, useMemo, useRef, useState } from 'react'
type FocusDispatch = [RefObject<string | undefined>, Dispatch<string | undefined>]
const FocusStateIdContext = createContext<string | undefined>(undefined)
const FocusStateDispatchContext = createContext<FocusDispatch>(undefined as any)
export const FocusStateHasFocusContext = createContext<boolean>(false)
export const FocusStateContext: React.FC = ({ children }) => {
const [focusState, setFocusState] = useState<string>()
const focusRef = useRef<string | undefined>()
const focusDispatch = useMemo((): FocusDispatch => ([focusRef, (value: string | undefined) => {
focusRef.current = value
setFocusState(value)
}]), [])
return (
<FocusStateDispatchContext.Provider value={focusDispatch}>
<FocusStateIdContext.Provider value={focusState}>
{children}
</FocusStateIdContext.Provider>
</FocusStateDispatchContext.Provider>
)
}
export const useFocusState = () => {
const focusedId = useContext(FocusStateIdContext)
const hasFocus = useContext(FocusStateHasFocusContext)
return hasFocus ? focusedId : undefined
}
export const useFocusDispatch = () => useContext(FocusStateDispatchContext)
@@ -1,5 +0,0 @@
export * from './VirtualizedTree'
export * from './VirtualizedTreeChild'
export * from './types'
@@ -1,175 +0,0 @@
import type { PressEvent } from '@react-types/shared'
import type { MutableRefObject } from 'react'
import type { NodeComponentProps } from 'react-vtree/dist/lib/Tree'
import type { VariableSizeNodePublicState } from 'react-vtree/dist/lib/VariableSizeTree'
import type { VariableSizeTree } from 'react-vtree'
import type { ListProps } from 'react-window'
// Props
export interface VirtualizedTreeRef {
focus: () => void
}
export interface VirtualizedTreeProps<
TLeaf extends LeafTreeBase,
TParent extends ParentTreeBase<TLeaf>
> extends RenderFunctions<TLeaf, TParent>, Omit<ListProps, 'children' | 'itemCount' | 'width' | 'height'> {
/**
* Use instead of `ref`. React/TS still doesn't have a good solution for `forwardRef` generics
*/
innerRef?: MutableRefObject<VirtualizedTreeRef>
treeRef?: MutableRefObject<VariableSizeTree<
TreeNodeData<TLeaf, TParent>
> | null>
tree: TParent
defaultItemSize: number
showRoot?: boolean
/**
* If true, calculate the size of each child node
*/
shouldMeasure?: boolean
/**
* See `react-window` `overscanCount`. Defaults to 20
*/
overscanCount?: number
/**
* If specified, automatically indent children elements by the specified size in REM units
*/
indentSize?: number
onNodePress?: OnNodePress<TLeaf, TParent>
onNodeKeyDown?: OnNodeKeyDown<TLeaf, TParent>
}
export interface LeafProps<T> {
leaf: T
depth: number
remeasure: () => void
}
export interface ParentProps<T> {
parent: T
depth: number
isOpen: boolean
setOpen: (isOpen: boolean) => void
remeasure: () => void
}
export interface RenderFunctions<TLeaf, TParent> {
onRenderLeaf: (props: LeafProps<TLeaf>) => JSX.Element
onRenderParent: (props: ParentProps<TParent>) => JSX.Element | null
}
export type ChildComponentProps<
TLeaf extends LeafTreeBase,
TParent extends ParentTreeBase<TLeaf>
> = NodeComponentProps<TreeNodeData<TLeaf, TParent>, VariableSizeNodePublicState<TreeNodeData<TLeaf, TParent>>> & {
onNodePress?: OnNodePress<TLeaf, TParent>
onNodeKeyDown?: OnNodeKeyDown<TLeaf, TParent>
}
export interface InternalChildProps<
TLeaf extends LeafTreeBase,
TParent extends ParentTreeBase<TLeaf>
> extends ChildComponentProps<TLeaf, TParent>, RenderFunctions<TLeaf, TParent> {
indentSize?: number
showRoot?: boolean
shouldMeasure?: boolean
}
export interface InternalOnRenderChildProps<
TLeaf extends LeafTreeBase,
TParent extends ParentTreeBase<TLeaf>
> extends Pick<ChildComponentProps<TLeaf, TParent>, 'data' | 'isOpen' | 'setOpen'>, RenderFunctions<TLeaf, TParent> {
remeasure: () => void
}
// Base
export interface NodeBase {
id: string
}
export interface ParentTreeBase<T extends LeafTreeBase> extends NodeBase {
children: Array<ParentTreeBase<T> | T>
}
export type LeafTreeBase = NodeBase
export interface TreeNode<
TLeaf extends LeafTreeBase,
TParent extends ParentTreeBase<TLeaf>
> {
data: TreeNodeData<TLeaf, TParent>
}
export type TreeNodeData<
TLeaf extends LeafTreeBase,
TParent extends ParentTreeBase<TLeaf>
> = SpecificTreeNode<TLeaf | TParent>
export interface SpecificTreeNode<T> {
id: string
nestingLevel: number
node: T
isOpenByDefault: boolean
defaultHeight: number
isFirst: boolean
}
type NodeCallbackData<
TLeaf extends LeafTreeBase,
TParent extends ParentTreeBase<TLeaf>
> = Pick<ChildComponentProps<TLeaf, TParent>, 'isOpen' | 'setOpen'> & (
{
type: 'leaf'
data: SpecificTreeNode<TLeaf>
} | {
type: 'parent'
data: SpecificTreeNode<TParent>
}
)
export type OnNodePress<
TLeaf extends LeafTreeBase,
TParent extends ParentTreeBase<TLeaf>
> = (node: NodeCallbackData<TLeaf, TParent>, event: PressEvent) => void
export type OnNodeKeyDown<
TLeaf extends LeafTreeBase,
TParent extends ParentTreeBase<TLeaf>
> = (node: NodeCallbackData<TLeaf, TParent>, event: React.KeyboardEvent<HTMLDivElement>) => void
export const isParent = <
TLeaf extends LeafTreeBase,
TParent extends ParentTreeBase<TLeaf>
>(
input: TLeaf | TParent,
): input is TParent => {
return 'children' in input
}
export const createPressEventNode = <
TLeaf extends LeafTreeBase,
TParent extends ParentTreeBase<TLeaf>
>(data: TreeNodeData<TLeaf, TParent>, isOpen: boolean, setOpen: (state: boolean) => Promise<void>) => {
return isParent(data.node) ? {
type: 'parent' as const,
data: data as SpecificTreeNode<TParent>,
isOpen,
setOpen,
} : {
type: 'leaf' as const,
data: data as SpecificTreeNode<TLeaf>,
isOpen,
setOpen,
}
}
export const treeChildClass = 'treeChild'
@@ -1,28 +0,0 @@
import * as React from 'react'
import { composeStories } from '@storybook/testing-react'
import { mountAndSnapshot } from 'util/testing'
import * as stories from './Button.stories'
const { Button, IconButton } = composeStories(stories)
// TODO: Autogenerate from stories
describe('<Button />', () => {
it('Button', () => {
mountAndSnapshot(<Button />)
})
it('ButtonSizes', () => {
const ButtonSizes = () => (
<div style={{ width: 500 }}>
{stories.buttonSizesWithSizes(['text-xs', 'text-s', 'text-ms', 'text-m', 'text-ml', 'text-l', 'text-xl', 'text-2xl', 'text-3xl', 'text-4xl'])}
</div>
)
mountAndSnapshot(<ButtonSizes />)
})
it('IconButton', () => {
mountAndSnapshot(<IconButton />)
})
})
@@ -1,84 +0,0 @@
import * as React from 'react'
// TODO: This is causing a "module not defined error"
// Find out why and fix it
// 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'
// stub it for now
const action = (action: string) => undefined
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 buttonSizesWithSizes = (sizes: string[]) => sizes.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>
)
})
export const ButtonSizes = createStory(() => (
<div>
<div style={{ width: 500 }}>
{buttonSizesWithSizes(Object.keys(typography))}
</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>
))
@@ -1,69 +0,0 @@
import * as React from 'react'
import type { 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 'core/text/styledText'
import styles from './Button.module.scss'
import { FocusRing } from '@react-aria/focus'
import { focusClass } from 'css/derived/util'
const { useRef } = React
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' />
@@ -1,22 +0,0 @@
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>
)
@@ -1,3 +0,0 @@
export * from './Button'
export * from './IconButton'
@@ -1,32 +0,0 @@
import * as React from 'react'
import { Icon } from './Icon'
import { mountAndSnapshot } from 'util/testing'
import styles from './Icon.stories.module.scss'
import { iconLines } from './Icon.stories'
// TODO: Autogenerate from stories
describe('<Icon />', () => {
it('Standard icons', () => {
const Icons = () => (
<div>
<Icon className={styles.icon} icon='check' size='xl' />
<Icon className={styles.icon} icon='exclamation' size='xl' />
<Icon className={styles.icon} icon='home' size='xl' />
<Icon className={styles.icon} icon='arrow-circle-up' size='xl' />
</div>
)
mountAndSnapshot(<Icons />)
})
it('Icon lines', () => {
const Icons = () => (
<>
{iconLines(['text-xs', 'text-s', 'text-ms', 'text-m', 'text-ml', 'text-l', 'text-xl', 'text-2xl', 'text-3xl', 'text-4xl'])}
</>
)
mountAndSnapshot(<Icons />)
})
})
@@ -1,72 +0,0 @@
import * as React from '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,
},
},
},
excludeStories: ['IconLines'],
})
export const iconLines = (sizes: string[]) => sizes.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>
)
})
export const Icon = createStory<{ 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 />
{iconLines(Object.keys(typography))}
</div>
), {
font: fontOptions[0],
})
-37
View File
@@ -1,37 +0,0 @@
import * as React from 'react'
import type { SVGAttributes } from 'react'
import cs from 'classnames'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { IconName } from '@fortawesome/fontawesome-svg-core'
import { styledTextSizeClassNames } from 'core/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
/**
* Render icon at 1em without centering
*/
ignoreTextCenter?: boolean
/**
* Render icon at text size without centering
*/
sizeWithoutCenter?: 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, sizeWithoutCenter, ...props }) => (
<FontAwesomeIcon
{...props}
className={cs(styledTextSizeClassNames(size, lineHeight), styles.icon, {
[styles.ignoreTextCenter]: ignoreTextCenter,
[styles.sizeWithoutCenter]: sizeWithoutCenter,
}, className)}
icon={icon}
/>
)
-9
View File
@@ -1,9 +0,0 @@
export * from './button'
export * from './icon/Icon'
export * from './input'
export * from './surface'
export * from './text'
@@ -1,95 +0,0 @@
import * as React from 'react'
import type { 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 'core/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']?: string | undefined
})
export type IconInputProps = InputProps<{
prefixIcon?: IconSettings
suffixIcon?: IconSettings
}>
& RefAttributes<HTMLInputElement>
export const IconInput: React.FC<IconInputProps> = (props) => <InputBase {...props} InputRenderer={IconInputComponent} />
const IconInputComponent: InputRenderer<IconInputProps> = ({ componentProps: { 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
{...props}
{...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>
)
}
@@ -1,31 +0,0 @@
import * as React from 'react'
import { composeStories } from '@storybook/testing-react'
import * as stories from './Input.stories'
import { mountAndSnapshot } from 'util/testing'
// import { iconSizesWithSizes } from './Input.stories'
const {
Input,
// Icon
} = composeStories(stories)
// TODO: Autogenerate from stories
describe('<Input />', () => {
it('Standard input', () => {
mountAndSnapshot(<Input />)
})
it('IconInput', () => {
// mountAndSnapshot(<Icon />)
})
// it('IconInput sizes', () => {
// const IconInput = () => (
// <>
// {iconSizesWithSizes(['xs', 's', 'ms', 'm', 'ml', 'l', 'xl', '2xl'])}
// </>
// )
// mountAndSnapshot(<IconInput />)
// })
})
@@ -1,146 +0,0 @@
import * as React from 'react'
// TODO: This is causing a "module not defined error"
// Find out why and fix it
// 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 { TextSize } from 'css'
// stub it for now
const action = (action: string) => undefined
export default createStorybookConfig({
title: 'Core/Input',
excludeStories: ['iconSizesWithSizes'],
})
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>
<InputComponent label={{ type: 'aria', contents: 'foo' }} />
</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 iconSizesWithSizes = (sizes: string[]) => sizes.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',
}}
/>
)
})
// export const IconSizes = createStory(() => (
// <div>
// <div style={{ width: 500 }}>
// {iconSizesWithSizes(Object.keys(typography).filter((key) => key !== 'type' && !key.startsWith('line-height') && !key.startsWith('text-mono') && key !== 'text-3xl' && key !== 'text-4xl'))}
// </div>
// </div>
// ))
@@ -1,30 +0,0 @@
import React, { InputHTMLAttributes } from 'react'
import { useFocusRing } from 'react-aria'
import cs from 'classnames'
import { focusClass } from 'css/derived/util'
import { BasicInput, BasicInputProps, InputBase, InputProps, InputRenderer } from './InputBase'
import styles from './InputBase.module.scss'
export const Input: React.FC<InputProps<{}>> = (props) => (
<InputBase
{...props}
InputRenderer={BasicInputRenderer}
/>
)
// TODO: The types here are not as elegant as I would like
const BasicInputRenderer: InputRenderer<Omit<BasicInputProps, 'inputRef'>> = ({ componentProps, inputProps, inputRef }) => {
const { isFocused, focusProps } = useFocusRing({ isTextInput: true })
return (
<span
className={cs(styles.wrapper, {
[focusClass]: isFocused,
}, componentProps.className)}
>
<BasicInput {...inputProps as Omit<InputHTMLAttributes<HTMLInputElement>, 'size'>} {...focusProps} {...componentProps} inputRef={inputRef} className={inputProps.className} />
</span>
)
}
@@ -1,106 +0,0 @@
import * as React from 'react'
import type { CSSProperties, InputHTMLAttributes, MutableRefObject, ReactNode, RefObject, TextareaHTMLAttributes } from 'react'
import { useTextField } from 'react-aria'
import cs from 'classnames'
import { ExtractFirstArg } from 'util/types'
import { LineHeight, TextSize } from 'css'
import { styledTextSizeClassNames } from 'core/text/styledText'
import { SizingProps } from 'core/shared'
import styles from './InputBase.module.scss'
import { useCombinedRefs } from 'hooks/useCombinedRefs'
const { useMemo, useRef } = React
export interface SharedInputBaseProps extends SizingProps, Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
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 interface InputRendererProps<T> {
componentProps: Omit<InputProps<T>, 'label'>
inputProps: InputHTMLAttributes<HTMLInputElement>
inputRef: RefObject<HTMLTextAreaElement | HTMLInputElement>
}
export type InputRenderer<T> = React.FC<InputRendererProps<T>>
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 = {
...props,
inputElementType: textArea ? 'textarea' : 'input',
} as ExtractFirstArg<typeof useTextField>
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 componentProps={props as Omit<InputProps<T>, 'label'>} inputProps={inputProps as InputHTMLAttributes<HTMLInputElement>} inputRef={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)} />
}
@@ -1,3 +0,0 @@
export * from './Input'
export * from './IconInput'
-19
View File
@@ -1,19 +0,0 @@
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
@@ -1,14 +0,0 @@
import * as React from 'react'
import { composeStories } from '@storybook/testing-react'
import { mountAndSnapshot } from 'util/testing'
import * as stories from './Elevation.stories'
const { Elevation } = composeStories(stories)
// TODO: Autogenerate from stories
describe('<Elevation />', () => {
it('Elevation', () => {
mountAndSnapshot(<Elevation />)
})
})
@@ -1,40 +0,0 @@
import * as React from '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-', '')),
},
},
},
})
export const Elevation = createStory<{
elevation: SurfaceElevation
}>(({ elevation }) => (
<div>
<ElevationComponent elevation={elevation}>
{lorem}
</ElevationComponent>
<br />
<StoryHighlightWrapper>
<ElevationComponent elevation={elevation}>
{lorem}
</ElevationComponent>
</StoryHighlightWrapper>
</div>
), {
elevation: 'bordered',
})
@@ -1,19 +0,0 @@
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>
)
@@ -1,3 +0,0 @@
export * from './elevation/Elevation'
export * from './paddedBox/PaddedBox'
@@ -1,14 +0,0 @@
import * as React from 'react'
import { composeStories } from '@storybook/testing-react'
import { mountAndSnapshot } from 'util/testing'
import * as stories from './PaddedBox.stories'
const { PaddedBox } = composeStories(stories)
// TODO: Autogenerate from stories
describe('<PaddedBox />', () => {
it('PaddedBox', () => {
mountAndSnapshot(<PaddedBox />)
})
})
@@ -1,39 +0,0 @@
import * as React from '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-', '')),
},
},
},
})
export const PaddedBox = createStory<{
padding: Spacing
}>(({ padding }) => (
<div>
<StoryHighlightWrapper>
<PaddedComponent padding={padding}>
{lorem}
</PaddedComponent>
</StoryHighlightWrapper>
</div>
), {
padding: 'm',
})
// Required to prevent Storybook from separating into two words and creating unnecessary nesting
PaddedBox.storyName = 'PaddedBox'
@@ -1,22 +0,0 @@
import * as React from 'react'
import type { 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>
)
-3
View File
@@ -1,3 +0,0 @@
export * from './placeholder/Placeholder'
export * from './styledText/StyledText'
@@ -1,14 +0,0 @@
import * as React from 'react'
import { composeStories } from '@storybook/testing-react'
import { mountAndSnapshot } from 'util/testing'
import * as stories from './Placeholder.stories'
const { Placeholder } = composeStories(stories)
// TODO: Autogenerate from stories
describe('<Placeholder />', () => {
it('Placeholder', () => {
mountAndSnapshot(<Placeholder />)
})
})
@@ -1,25 +0,0 @@
import * as React from 'react'
import { Placeholder as PlaceholderComponent } from './Placeholder'
import { createStory, createStorybookConfig } from 'stories/util'
import { lorem } from 'util/lorem'
export default createStorybookConfig({
title: 'Core/Placeholder',
})
export const Placeholder = createStory(() => (
<div>
<div>
<PlaceholderComponent>
This is placeholder text
</PlaceholderComponent>
</div>
<div>
<PlaceholderComponent>
{lorem}
</PlaceholderComponent>
</div>
</div>
))
@@ -1,13 +0,0 @@
import React from 'react'
import cs from 'classnames'
import { StyledText, StyledTextProps } from '../styledText/StyledText'
import styles from './Placeholder.module.scss'
/**
* StyledText designed to act as a placeholder state. Size defaults to ms
*/
export const Placeholder: React.FC<StyledTextProps> = (props) => (
<StyledText {...props} className={cs(styles.placeholder, props.className)} size={props.size ?? 'ms'} />
)
@@ -1 +0,0 @@
export * from './Placeholder'
@@ -1,18 +0,0 @@
import * as React from 'react'
import { mountAndSnapshot } from 'util/testing'
import { styledTextWithSizes } from './StyledText.stories'
// TODO: Autogenerate from stories
describe('<StyledText />', () => {
it('StyledText', () => {
const StyledText = () => (
<>
{styledTextWithSizes(['text-xs', 'text-s', 'text-ms', 'text-m', 'text-ml', 'text-l', 'text-xl', 'text-2xl', 'text-3xl', 'text-4xl'])}
</>
)
mountAndSnapshot(<StyledText />)
})
})
@@ -1,39 +0,0 @@
import * as React from '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',
excludeStories: ['styledTextWithSizes'],
})
export const styledTextWithSizes = (sizes: string[]) => sizes.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 />
</>
)
})
export const StyledText = createStory(() => (
<div>
{styledTextWithSizes(Object.keys(typography))}
</div>
))
StyledText.storyName = 'StyledText'
@@ -1,26 +0,0 @@
import * as React from 'react'
import type { CSSProperties } from 'react'
import cs from 'classnames'
import { LineHeight, TextSize } from 'css'
import { TextSizableComponent } from 'core/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}`
@@ -1 +0,0 @@
export * from './StyledText'
-61
View File
@@ -1,61 +0,0 @@
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;
-28
View File
@@ -1,28 +0,0 @@
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;
-26
View File
@@ -1,26 +0,0 @@
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;
@@ -1,23 +0,0 @@
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;
@@ -1,32 +0,0 @@
/* 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-'>
-37
View File
@@ -1,37 +0,0 @@
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'
-1
View File
@@ -1 +0,0 @@
export * from './derived/types'
-3
View File
@@ -1,3 +0,0 @@
// declaration.d.ts
declare module '*.scss';
declare module '*.module.scss';
@@ -1,22 +0,0 @@
import type { MutableRefObject, RefCallback } from 'react'
import { 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])
}
-16
View File
@@ -1,16 +0,0 @@
import { useLayoutEffect, useRef } from 'react'
/**
* Wraps the provided `value` in a Concurrent safe ref for consumption in `useEffect` and similar without forcing reruns on value changes
*
* **CAUTION:** `useCurrent` makes it easy to shoot yourself in the foot, particularly in scenarios involving callbacks. Use sparingly
*/
export const useCurrent = <T>(value: T) => {
const ref = useRef(value)
useLayoutEffect(() => {
ref.current = value
}, [value])
return ref
}
-52
View File
@@ -1,52 +0,0 @@
import { useCallback, useLayoutEffect, useState } from 'react'
import { useCurrent } from './useCurrent'
/**
* Calculates the height of a DOM element
*
* **NOTE:** This causes multiple renders of your component to measure
*/
export const useMeasure = (expectedHeight: number, resizer: (height: number) => void, deps: any[], disable = false) => {
const [ref, setRef] = useState<HTMLDivElement | null>(null)
const [measuring, setMeasuring] = useState(false)
const currentRef = useCurrent(ref)
const currentMeasuring = useCurrent(measuring)
const remeasure = useCallback(() => setMeasuring(true), [])
useLayoutEffect(() => {
if (disable) {
return
}
// On a new render (prop update), mark the component as ready to measure
if (currentMeasuring.current) {
return
}
setMeasuring(true)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps)
useLayoutEffect(() => {
// When we are measuring, measure the current DOM and update if necessary
if (!measuring || !currentRef.current) {
return
}
const measuredHeight = currentRef.current.getBoundingClientRect().height
if (measuredHeight !== expectedHeight) {
resizer(measuredHeight)
}
setMeasuring(false)
}, [measuring, expectedHeight, resizer, currentRef])
return {
ref: currentRef,
setRef,
remeasure,
}
}
-18
View File
@@ -1,18 +0,0 @@
// Add global CSS to the bundle
import './global.scss'
export * from './components/collapsibleGroup/CollapsibleGroup'
export * from './components/collapsibleGroup/CollapsibleGroupHeader'
export * from './components/CypressLogo/CypressLogo'
export * from './components/fileTree'
export * from './components/Nav'
export * from './components/searchInput/SearchInput'
export * from './components/virtualizedTree'
export * from './core'
@@ -1,14 +0,0 @@
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>
)
@@ -1,51 +0,0 @@
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)
@@ -1,51 +0,0 @@
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)
@@ -1,32 +0,0 @@
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)
@@ -1,35 +0,0 @@
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)
-17
View File
@@ -1,17 +0,0 @@
import type { 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
}

Some files were not shown because too many files have changed in this diff Show More