Browse Source

Initial commit

Add files from restructured repository.
pull/1/head
TheoryOfNekomata 1 year ago
commit
22c815d58c
100 changed files with 45295 additions and 0 deletions
  1. +108
    -0
      .gitignore
  2. +7
    -0
      LICENSE
  3. +71
    -0
      packages/web/base/badge/package.json
  4. +3
    -0
      packages/web/base/badge/pridepack.json
  5. +45
    -0
      packages/web/base/badge/src/index.ts
  6. +9
    -0
      packages/web/base/badge/tsconfig.json
  7. +3413
    -0
      packages/web/base/badge/yarn.lock
  8. +71
    -0
      packages/web/base/button/package.json
  9. +3
    -0
      packages/web/base/button/pridepack.json
  10. +318
    -0
      packages/web/base/button/src/index.ts
  11. +9
    -0
      packages/web/base/button/tsconfig.json
  12. +3413
    -0
      packages/web/base/button/yarn.lock
  13. +71
    -0
      packages/web/base/checkcontrol/package.json
  14. +3
    -0
      packages/web/base/checkcontrol/pridepack.json
  15. +346
    -0
      packages/web/base/checkcontrol/src/index.ts
  16. +9
    -0
      packages/web/base/checkcontrol/tsconfig.json
  17. +3413
    -0
      packages/web/base/checkcontrol/yarn.lock
  18. +67
    -0
      packages/web/base/selectcontrol/package.json
  19. +3
    -0
      packages/web/base/selectcontrol/pridepack.json
  20. +5
    -0
      packages/web/base/selectcontrol/src/index.ts
  21. +9
    -0
      packages/web/base/selectcontrol/tsconfig.json
  22. +3380
    -0
      packages/web/base/selectcontrol/yarn.lock
  23. +71
    -0
      packages/web/base/textcontrol/package.json
  24. +3
    -0
      packages/web/base/textcontrol/pridepack.json
  25. +400
    -0
      packages/web/base/textcontrol/src/index.ts
  26. +9
    -0
      packages/web/base/textcontrol/tsconfig.json
  27. +3413
    -0
      packages/web/base/textcontrol/yarn.lock
  28. +82
    -0
      packages/web/categories/action/react/package.json
  29. +3
    -0
      packages/web/categories/action/react/pridepack.json
  30. +4
    -0
      packages/web/categories/action/react/setupTests.ts
  31. +190
    -0
      packages/web/categories/action/react/src/components/ActionButton/ActionButton.test.tsx
  32. +172
    -0
      packages/web/categories/action/react/src/components/ActionButton/index.tsx
  33. +1
    -0
      packages/web/categories/action/react/src/index.ts
  34. +9
    -0
      packages/web/categories/action/react/src/web-action-react.test.ts
  35. +11
    -0
      packages/web/categories/action/react/tsconfig.json
  36. +9
    -0
      packages/web/categories/action/react/vite.config.ts
  37. +4209
    -0
      packages/web/categories/action/react/yarn.lock
  38. +82
    -0
      packages/web/categories/formatted/react/package.json
  39. +3
    -0
      packages/web/categories/formatted/react/pridepack.json
  40. +4
    -0
      packages/web/categories/formatted/react/setupTests.ts
  41. +203
    -0
      packages/web/categories/formatted/react/src/components/EmailAddressInput/EmailAddressInput.test.tsx
  42. +124
    -0
      packages/web/categories/formatted/react/src/components/EmailAddressInput/index.tsx
  43. +203
    -0
      packages/web/categories/formatted/react/src/components/PhoneNumberInput/PhoneNumberInput.test.tsx
  44. +124
    -0
      packages/web/categories/formatted/react/src/components/PhoneNumberInput/index.tsx
  45. +203
    -0
      packages/web/categories/formatted/react/src/components/UrlInput/UrlInput.test.tsx
  46. +124
    -0
      packages/web/categories/formatted/react/src/components/UrlInput/index.tsx
  47. +3
    -0
      packages/web/categories/formatted/react/src/index.ts
  48. +11
    -0
      packages/web/categories/formatted/react/src/web-freeform-react.test.ts
  49. +10
    -0
      packages/web/categories/formatted/react/tsconfig.json
  50. +9
    -0
      packages/web/categories/formatted/react/vite.config.ts
  51. +4209
    -0
      packages/web/categories/formatted/react/yarn.lock
  52. +82
    -0
      packages/web/categories/freeform/react/package.json
  53. +3
    -0
      packages/web/categories/freeform/react/pridepack.json
  54. +4
    -0
      packages/web/categories/freeform/react/setupTests.ts
  55. +203
    -0
      packages/web/categories/freeform/react/src/components/MaskedTextInput/MaskedTextInput.test.tsx
  56. +125
    -0
      packages/web/categories/freeform/react/src/components/MaskedTextInput/index.tsx
  57. +200
    -0
      packages/web/categories/freeform/react/src/components/MultilineTextInput/MultilineTextInput.test.tsx
  58. +127
    -0
      packages/web/categories/freeform/react/src/components/MultilineTextInput/index.tsx
  59. +213
    -0
      packages/web/categories/freeform/react/src/components/TextInput/TextInput.test.tsx
  60. +135
    -0
      packages/web/categories/freeform/react/src/components/TextInput/index.tsx
  61. +3
    -0
      packages/web/categories/freeform/react/src/index.ts
  62. +11
    -0
      packages/web/categories/freeform/react/src/web-freeform-react.test.ts
  63. +10
    -0
      packages/web/categories/freeform/react/tsconfig.json
  64. +9
    -0
      packages/web/categories/freeform/react/vite.config.ts
  65. +4209
    -0
      packages/web/categories/freeform/react/yarn.lock
  66. +81
    -0
      packages/web/categories/information/react/package.json
  67. +3
    -0
      packages/web/categories/information/react/pridepack.json
  68. +4
    -0
      packages/web/categories/information/react/setupTests.ts
  69. +31
    -0
      packages/web/categories/information/react/src/components/Badge/Badge.test.tsx
  70. +36
    -0
      packages/web/categories/information/react/src/components/Badge/index.tsx
  71. +1
    -0
      packages/web/categories/information/react/src/index.ts
  72. +9
    -0
      packages/web/categories/information/react/src/web-information-react.test.ts
  73. +10
    -0
      packages/web/categories/information/react/tsconfig.json
  74. +9
    -0
      packages/web/categories/information/react/vite.config.ts
  75. +4202
    -0
      packages/web/categories/information/react/yarn.lock
  76. +82
    -0
      packages/web/categories/navigation/react/package.json
  77. +3
    -0
      packages/web/categories/navigation/react/pridepack.json
  78. +4
    -0
      packages/web/categories/navigation/react/setupTests.ts
  79. +182
    -0
      packages/web/categories/navigation/react/src/components/LinkButton/LinkButton.test.tsx
  80. +183
    -0
      packages/web/categories/navigation/react/src/components/LinkButton/index.tsx
  81. +1
    -0
      packages/web/categories/navigation/react/src/index.ts
  82. +9
    -0
      packages/web/categories/navigation/react/src/web-navigation-react.test.ts
  83. +10
    -0
      packages/web/categories/navigation/react/tsconfig.json
  84. +9
    -0
      packages/web/categories/navigation/react/vite.config.ts
  85. +4209
    -0
      packages/web/categories/navigation/react/yarn.lock
  86. +86
    -0
      packages/web/categories/option/react/package.json
  87. +3
    -0
      packages/web/categories/option/react/pridepack.json
  88. +4
    -0
      packages/web/categories/option/react/setupTests.ts
  89. +299
    -0
      packages/web/categories/option/react/src/components/DropdownSelect/DropdownSelect.test.tsx
  90. +199
    -0
      packages/web/categories/option/react/src/components/DropdownSelect/index.tsx
  91. +196
    -0
      packages/web/categories/option/react/src/components/MenuSelect/index.tsx
  92. +175
    -0
      packages/web/categories/option/react/src/components/RadioButton/RadioButton.test.tsx
  93. +152
    -0
      packages/web/categories/option/react/src/components/RadioButton/index.tsx
  94. +80
    -0
      packages/web/categories/option/react/src/components/RadioTickBox/RadioTickBox.test.tsx
  95. +91
    -0
      packages/web/categories/option/react/src/components/RadioTickBox/index.tsx
  96. +327
    -0
      packages/web/categories/option/react/src/components/TagInput/index.tsx
  97. +209
    -0
      packages/web/categories/option/react/src/components/ToggleButton/ToggleButton.test.tsx
  98. +185
    -0
      packages/web/categories/option/react/src/components/ToggleButton/index.tsx
  99. +80
    -0
      packages/web/categories/option/react/src/components/ToggleSwitch/ToggleSwitch.test.tsx
  100. +123
    -0
      packages/web/categories/option/react/src/components/ToggleSwitch/index.tsx

+ 108
- 0
.gitignore View File

@@ -0,0 +1,108 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
.env.production
.env.development

# parcel-bundler cache (https://parceljs.org/)
.cache

# Next.js build output
.next

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

.npmrc
.idea/

+ 7
- 0
LICENSE View File

@@ -0,0 +1,7 @@
MIT License Copyright (c) 2022 TheoryOfNekomata <allan.crisostomo@outlook.com>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 71
- 0
packages/web/base/badge/package.json View File

@@ -0,0 +1,71 @@
{
"name": "@tesseract-design/web-base-badge",
"version": "0.0.0",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=10"
},
"license": "MIT",
"keywords": [
"pridepack"
],
"devDependencies": {
"@types/node": "^18.0.0",
"@types/react": "^18.2.6",
"csstype": "^3.1.2",
"eslint": "^8.20.0",
"eslint-config-lxsmnsyc": "^0.4.7",
"pridepack": "2.4.4",
"tslib": "^2.4.0",
"typescript": "^4.7.4",
"vitest": "^0.19.1"
},
"dependencies": {
"@tesseract-design/goofy-goober": "link:../../../../../goofy-goober"
},
"scripts": {
"prepublishOnly": "pridepack clean && pridepack build",
"build": "pridepack build",
"type-check": "pridepack check",
"lint": "pridepack lint",
"clean": "pridepack clean",
"watch": "pridepack watch",
"start": "pridepack start",
"dev": "pridepack dev",
"test": "vitest"
},
"private": true,
"description": "Base badge styles for Tesseract.",
"repository": {
"url": "",
"type": "git"
},
"homepage": "",
"bugs": {
"url": ""
},
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>",
"publishConfig": {
"access": "restricted"
},
"types": "./dist/types/index.d.ts",
"exports": {
".": {
"development": {
"require": "./dist/cjs/development/index.js",
"import": "./dist/esm/development/index.js"
},
"require": "./dist/cjs/production/index.js",
"import": "./dist/esm/production/index.js",
"types": "./dist/types/index.d.ts"
}
},
"typesVersions": {
"*": {}
},
"main": "./dist/cjs/production/index.js",
"module": "./dist/esm/production/index.js"
}

+ 3
- 0
packages/web/base/badge/pridepack.json View File

@@ -0,0 +1,3 @@
{
"target": "es2017"
}

+ 45
- 0
packages/web/base/badge/src/index.ts View File

@@ -0,0 +1,45 @@
import { css } from '@tesseract-design/goofy-goober';

export type BadgeBaseArgs = {
/**
* Will the component be displayed with circular sides?
*/
rounded: boolean,
}

export const Root = ({
rounded,
}: BadgeBaseArgs) => css.cx(
css`
position: relative;
height: 1.5em;
min-width: 1.5em;
display: inline-grid;
vertical-align: middle;
place-content: center;
overflow: hidden;
font-stretch: var(--font-stretch-base, normal);
padding: 0 0.25rem;
box-sizing: border-box;
&::before {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: currentColor;
opacity: 0.25;
content: '';
}
`,
css.dynamic({
'border-radius': rounded ? '0.75em' : '0.25rem',
}),
);

export const Content = () => css.cx(
css`
position: relative;
font-size: 0.75em;
`
);

+ 9
- 0
packages/web/base/badge/tsconfig.json View File

@@ -0,0 +1,9 @@
{
"exclude": ["node_modules"],
"extends": "../../../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"emitDeclarationOnly": true,
"declaration": true
}
}

+ 3413
- 0
packages/web/base/badge/yarn.lock
File diff suppressed because it is too large
View File


+ 71
- 0
packages/web/base/button/package.json View File

@@ -0,0 +1,71 @@
{
"name": "@tesseract-design/web-base-button",
"version": "0.0.0",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=10"
},
"license": "MIT",
"keywords": [
"pridepack"
],
"devDependencies": {
"@types/node": "^18.0.0",
"@types/react": "^18.2.6",
"csstype": "^3.1.2",
"eslint": "^8.20.0",
"eslint-config-lxsmnsyc": "^0.4.7",
"pridepack": "2.4.4",
"tslib": "^2.4.0",
"typescript": "^4.7.4",
"vitest": "^0.19.1"
},
"dependencies": {
"@tesseract-design/goofy-goober": "link:../../../../../goofy-goober"
},
"scripts": {
"prepublishOnly": "pridepack clean && pridepack build",
"build": "pridepack build",
"type-check": "pridepack check",
"lint": "pridepack lint",
"clean": "pridepack clean",
"watch": "pridepack watch",
"start": "pridepack start",
"dev": "pridepack dev",
"test": "vitest"
},
"private": true,
"description": "Base button styles for Tesseract.",
"repository": {
"url": "",
"type": "git"
},
"homepage": "",
"bugs": {
"url": ""
},
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>",
"publishConfig": {
"access": "restricted"
},
"types": "./dist/types/index.d.ts",
"exports": {
".": {
"development": {
"require": "./dist/cjs/development/index.js",
"import": "./dist/esm/development/index.js"
},
"require": "./dist/cjs/production/index.js",
"import": "./dist/esm/production/index.js",
"types": "./dist/types/index.d.ts"
}
},
"typesVersions": {
"*": {}
},
"main": "./dist/cjs/production/index.js",
"module": "./dist/esm/production/index.js"
}

+ 3
- 0
packages/web/base/button/pridepack.json View File

@@ -0,0 +1,3 @@
{
"target": "es2017"
}

+ 318
- 0
packages/web/base/button/src/index.ts View File

@@ -0,0 +1,318 @@
import { css } from '@tesseract-design/goofy-goober';

export enum ButtonSize {
SMALL = 'small',
MEDIUM = 'medium',
LARGE = 'large',
}

export enum ButtonVariant {
OUTLINE = 'outline',
FILLED = 'filled',
}

export type ButtonBaseArgs = {
/**
* Size of the component.
*/
size: ButtonSize,
/**
* Will the component occupy the whole width of its container?
*/
block: boolean,
/**
* Stylistic variant of the component.
*/
variant: ButtonVariant,
/**
* Will the component display a surrounding border?
*/
border: boolean,
/**
* Will the component reject any activation?
*/
disabled: boolean,
/**
* Will the component conserve visual space?
*/
compact: boolean,
/**
* Is the component an item inside a menu?
*/
menuItem: boolean,
}

const MIN_HEIGHTS: Record<ButtonSize, string> = {
[ButtonSize.SMALL]: '2.5rem',
[ButtonSize.MEDIUM]: '3rem',
[ButtonSize.LARGE]: '4rem',
};

export const Button = ({
size,
block,
variant,
disabled,
compact,
}: ButtonBaseArgs): string => css.cx(
css`
box-sizing: border-box;
vertical-align: middle;
appearance: none;
font: inherit;
font-family: var(--font-family-base, sans-serif);
text-transform: uppercase;
font-weight: bolder;
border-radius: 0.25rem;
justify-content: center;
align-items: center;
position: relative;
border: 0;
user-select: none;
text-decoration: none;
white-space: nowrap;
line-height: 1;

& > :first-child::before {
box-shadow: 0 0 0 0 var(--color-accent, blue);
transition-property: box-shadow;
transition-duration: 150ms;
transition-timing-function: linear;
}

&:disabled {
opacity: 0.5;
cursor: not-allowed;
}

&::-moz-focus-inner {
outline: 0;
border: 0;
}

&:focus > :first-child::before {
box-shadow: 0 0 0 0.375rem var(--color-accent, blue);
}

&:disabled > :first-child::before {
box-shadow: 0 0 0 0 var(--color-accent, blue) !important;
}
`,

css.dynamic({
'min-height': MIN_HEIGHTS[size],
}),

css.if (disabled) (
css`
--color-accent: var(--color-primary, blue);
opacity: 0.5;
cursor: not-allowed;
`
).else (
css`
cursor: pointer;
&:hover {
--color-accent: var(--color-hover, blue);
outline: 0;
}
&:focus {
--color-accent: var(--color-hover, blue);
outline: 0;
}
&:active {
--color-accent: var(--color-active, red);
outline: 0;
}
&:hover > :first-child::before {
box-shadow: 0 0 0 0.375rem var(--color-accent, blue);
}
`
),

css.if (block) (
css`
width: 100%;
display: flex;
`
).else (
css`
display: inline-flex;
`
),

css.if (compact) (
css`
font-stretch: condensed;
padding: 0 0.5rem;
`
).else(
css`
padding: 0 1rem;
`
),

css.if (variant === ButtonVariant.FILLED) (
css`
background-color: var(--color-accent, blue);
color: var(--color-bg, white) !important;
`
),

css.if (variant === ButtonVariant.OUTLINE) (
css`
background-color: var(--color-bg, white);
color: var(--color-accent, blue);
`
),
);

export const Border = ({
border
}: ButtonBaseArgs): string => css.cx(
css.if (border) (
css`
border-color: var(--color-accent, blue);
box-sizing: border-box;
display: inline-block;
border-width: 0.125rem;
border-style: solid;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: inherit;
pointer-events: none;
&::before {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
content: '';
border-radius: 0.125rem;
opacity: 0.5;
pointer-events: none;
}
`
),
);

export const Label = ({
compact,
menuItem,
}: ButtonBaseArgs): string => css.cx(
css`
display: block;
flex-grow: 1;
flex-basis: 0;
min-width: 0;
`,

css.if (compact || menuItem) (
css`
text-align: left;
`
).else (
css`
text-align: center;
`
),

css.if (compact) (
css`
& ~ :last-child {
margin-right: -0.5rem;
}
`
).else (
css`
& ~ :last-child {
margin-right: -1rem;
}
`
),
);

export const BadgeContainer = ({
size,
}: ButtonBaseArgs): string => css.cx(
css`
width: 2rem;
text-align: center;
flex-shrink: 0;
& + * {
margin-left: -0.5rem;
}
`,
css.nest('&:last-child')(
css.dynamic({
width: MIN_HEIGHTS[size],
})
),
);

export const OverflowText = (): string => css.cx(
css`
width: 100%;
display: block;
overflow: hidden;
text-overflow: ellipsis;
height: 1.1em;
line-height: 1;
`,
);

export const IndicatorWrapper = ({
size
}: ButtonBaseArgs): string => css.cx(
css`
flex-shrink: 0;
box-sizing: border-box;
display: grid;
place-content: center;
padding: 0 1rem;
z-index: 1;
pointer-events: none;
line-height: 1;
user-select: none;
`,
css.dynamic({
width: `calc(${MIN_HEIGHTS[size]} * 0.75)`,
height: MIN_HEIGHTS[size],
}),
);

export const Indicator = () => css.cx(
css`
width: 1.5em;
height: 1.5em;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
`,
);

export const MainText = () => css.cx(
css`
width: 100%;
`,
);

export const Subtext = () => css.cx(
css`
display: block;
height: 1.1em;
line-height: 1.1;
width: 100%;
font-size: 0.875em;
text-transform: none;
font-weight: var(--font-weight-base, normal);
`,
);

+ 9
- 0
packages/web/base/button/tsconfig.json View File

@@ -0,0 +1,9 @@
{
"exclude": ["node_modules"],
"extends": "../../../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"emitDeclarationOnly": true,
"declaration": true
}
}

+ 3413
- 0
packages/web/base/button/yarn.lock
File diff suppressed because it is too large
View File


+ 71
- 0
packages/web/base/checkcontrol/package.json View File

@@ -0,0 +1,71 @@
{
"name": "@tesseract-design/web-base-checkcontrol",
"version": "0.0.0",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=10"
},
"license": "MIT",
"keywords": [
"pridepack"
],
"devDependencies": {
"@types/node": "^18.0.0",
"@types/react": "^18.2.6",
"csstype": "^3.1.2",
"eslint": "^8.20.0",
"eslint-config-lxsmnsyc": "^0.4.7",
"pridepack": "2.4.4",
"tslib": "^2.4.0",
"typescript": "^4.7.4",
"vitest": "^0.19.1"
},
"dependencies": {
"@tesseract-design/goofy-goober": "link:../../../../../goofy-goober"
},
"scripts": {
"prepublishOnly": "pridepack clean && pridepack build",
"build": "pridepack build",
"type-check": "pridepack check",
"lint": "pridepack lint",
"clean": "pridepack clean",
"watch": "pridepack watch",
"start": "pridepack start",
"dev": "pridepack dev",
"test": "vitest"
},
"private": true,
"description": "Base check control styles for Tesseract.",
"repository": {
"url": "",
"type": "git"
},
"homepage": "",
"bugs": {
"url": ""
},
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>",
"publishConfig": {
"access": "restricted"
},
"types": "./dist/types/index.d.ts",
"exports": {
".": {
"development": {
"require": "./dist/cjs/development/index.js",
"import": "./dist/esm/development/index.js"
},
"require": "./dist/cjs/production/index.js",
"import": "./dist/esm/production/index.js",
"types": "./dist/types/index.d.ts"
}
},
"typesVersions": {
"*": {}
},
"main": "./dist/cjs/production/index.js",
"module": "./dist/esm/production/index.js"
}

+ 3
- 0
packages/web/base/checkcontrol/pridepack.json View File

@@ -0,0 +1,3 @@
{
"target": "es2017"
}

+ 346
- 0
packages/web/base/checkcontrol/src/index.ts View File

@@ -0,0 +1,346 @@
import { css } from '@tesseract-design/goofy-goober'

export enum CheckControlAppearance {
TICK_BOX = 'tick-box',
BUTTON = 'button',
SWITCH = 'switch',
}

export enum CheckControlType {
/**
* One or more of this component within its group can be selected.
*/
CHECKBOX = 'checkbox',
/**
* At most one of this component within its group can be selected.
*/
RADIO = 'radio',
}

export type CheckControlBaseArgs = {
/**
* Will the component conserve visual space?
*/
compact: boolean,
/**
* Appearance of the component.
*/
appearance: CheckControlAppearance,
/**
* Will the component occupy the whole width of its container?
*/
block: boolean,
/**
* Type of the component defining its behavior.
*/
type: CheckControlType,
/**
* Label to display signifying the component's unselected state.
*/
uncheckedLabel: boolean,
}

export const CheckStateContainer = ({
appearance,
type,
}: CheckControlBaseArgs): string => css.cx(
css`
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
&:disabled + * {
cursor: not-allowed;
}
&:first-child + * > :first-child + * + * {
align-items: flex-start;
text-align: left;
}
&:checked + * {
--color-accent: var(--color-active, Highlight);
outline: 0;
}
&:indeterminate[type="checkbox"] + * {
--color-accent: var(--color-active, Highlight);
outline: 0;
}
&:indeterminate[type="checkbox"] + * > :first-child + * > * > :first-child {
display: none;
}
&:checked + * > :first-child + * > * > :first-child + * {
display: none;
}
&:focus + * {
--color-accent: var(--color-hover, red);
outline: 0;
}
&:focus[type="checkbox"] + * {
--color-accent: var(--color-hover, red);
outline: 0;
}
&:focus + * > :first-child::before {
box-shadow: 0 0 0 0.375rem var(--color-accent, blue);
}
`,
css.nest('&:checked + * > :first-child + * > *') (
css.if (
appearance === CheckControlAppearance.TICK_BOX
|| appearance === CheckControlAppearance.BUTTON
) (
css.if (type === 'checkbox') (
css`
width: 1.5em;
height: 1.5em;
`
),
css.if (type === 'radio') (
css`
width: 1em;
height: 1em;
`
),
),
css.if (appearance === CheckControlAppearance.SWITCH) (
css`
width: 1em;
height: 1em;
margin-right: 0;
margin-left: 1em;
`
),
),
css.nest('&:first-child + *') (
css`
cursor: pointer;
`,
css.if (appearance === CheckControlAppearance.BUTTON) (
css`
display: flex;
`,
),
css.if (appearance === CheckControlAppearance.SWITCH) (
css`
width: 1em;
height: 1em;
`,
),
),
css.nest('&:indeterminate[type="checkbox"] + * > :first-child + * > *') (
css.if (
appearance === CheckControlAppearance.BUTTON
|| appearance === CheckControlAppearance.TICK_BOX
) (
css`
width: 1.5em;
height: 1.5em;
`
),
css.if (appearance === CheckControlAppearance.SWITCH) (
css`
width: 1em;
height: 1em;
margin-right: 0.5em;
margin-left: 0.5em;
`
)
),
css.nest('&:indeterminate[type="checkbox"] + * > :first-child + * > * > :first-child + *') (
css.if (
appearance === CheckControlAppearance.BUTTON
|| appearance === CheckControlAppearance.TICK_BOX
) (
css`
display: block;
`
),
),
css.nest('&:checked + * > :first-child + * > * > :first-child') (
css.if (
appearance === CheckControlAppearance.BUTTON
|| appearance === CheckControlAppearance.TICK_BOX
) (
css`
display: block;
`
),
),
);

export const ClickArea = (): string => css.cx(
css`
display: contents;
`
);

export const CheckIndicatorArea = ({
compact,
appearance,
type,
uncheckedLabel,
}: CheckControlBaseArgs): string => css.cx(
css`
display: inline-grid;
vertical-align: middle;
place-content: center;
position: relative;
background-color: var(--color-bg, white);
box-shadow: 0 0 0 0.125rem var(--color-bg, white);
color: var(--color-accent, blue);
overflow: hidden;
&::before {
content: '';
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
border-radius: inherit;
border-width: 0.125rem;
border-style: solid;
box-sizing: border-box;
}
`,
css.if (appearance === CheckControlAppearance.TICK_BOX) (
css`
width: 1.5em;
height: 1.5em;
`,
css.if (type === CheckControlType.CHECKBOX) (
css`
border-radius: 0.25rem;
`
),
css.if (type === CheckControlType.RADIO) (
css`
border-radius: 50%;
`
),
),
css.if (appearance === CheckControlAppearance.BUTTON) (
css`
width: 1.5em;
height: 1.5em;
`,
css.if (!compact) (
css`
margin-left: -0.25rem;
`
),
css.if (type === CheckControlType.CHECKBOX) (
css`
border-radius: 0.25rem;
`
),
css.if (type === CheckControlType.RADIO) (
css`
border-radius: 50%;
`
),
),
css.if (appearance === CheckControlAppearance.SWITCH) (
css`
width: 2.5em;
height: 1.5em;
border-radius: 0.75em;
`,
css.if(uncheckedLabel) (
css.dynamic({
'margin-left': compact ? '0.375rem' : '0.75rem',
})
),
),
css.nest('& + *') (
css.dynamic({
'margin-left': compact ? '0.375rem' : '0.75rem',
})
)
);

export const CheckIndicatorWrapper = ({
appearance,
}: CheckControlBaseArgs): string => css.cx(
css`
flex-shrink: 0;
display: grid;
position: relative;
background-color: var(--color-accent, blue);
overflow: hidden;
border-radius: inherit;
`,
css.if(
appearance === CheckControlAppearance.TICK_BOX
|| appearance === CheckControlAppearance.BUTTON
) (
css`
width: 0;
height: 0;
`
),
css.if(appearance === CheckControlAppearance.SWITCH) (
css`
width: 1em;
height: 1em;
margin-right: 1em;
transition-property: margin-left, margin-right;
transition-duration: 150ms;
transition-timing-function: ease-out;
`,
),
);

export const CheckIndicator = ({
appearance,
}: CheckControlBaseArgs) => css.cx(
css`
fill: none;
stroke: var(--color-bg, white);
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
width: 1.5em;
height: 1.5em;
`,
css.if(appearance === CheckControlAppearance.SWITCH) (
css`
display: none;
`
)
);

export const ClickAreaWrapper = ({
block,
appearance,
uncheckedLabel,
}: CheckControlBaseArgs) => css.cx(
css`
vertical-align: middle;
`,
css.dynamic({
display: block ? 'block' : 'inline-block',
}),
css.if (appearance === CheckControlAppearance.TICK_BOX) (
css`
padding-left: 2.25rem;
text-indent: -2.25rem;
`
),
css.if (appearance === CheckControlAppearance.SWITCH) (
css.if (!uncheckedLabel) (
css`
padding-left: 3.25rem;
text-indent: -3.25rem;
`
),
),
);

export const Subtext = () => css.cx(
css`
font-size: 0.875em;
`
);

+ 9
- 0
packages/web/base/checkcontrol/tsconfig.json View File

@@ -0,0 +1,9 @@
{
"exclude": ["node_modules"],
"extends": "../../../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"emitDeclarationOnly": true,
"declaration": true
}
}

+ 3413
- 0
packages/web/base/checkcontrol/yarn.lock
File diff suppressed because it is too large
View File


+ 67
- 0
packages/web/base/selectcontrol/package.json View File

@@ -0,0 +1,67 @@
{
"name": "@tesseract-design/web-base-selectcontrol",
"version": "0.0.0",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=10"
},
"license": "MIT",
"keywords": [
"pridepack"
],
"devDependencies": {
"@types/node": "^18.0.0",
"eslint": "^8.20.0",
"eslint-config-lxsmnsyc": "^0.4.7",
"pridepack": "2.4.4",
"tslib": "^2.4.0",
"typescript": "^4.7.4",
"vitest": "^0.19.1"
},
"dependencies": {},
"scripts": {
"prepublishOnly": "pridepack clean && pridepack build",
"build": "pridepack build",
"type-check": "pridepack check",
"lint": "pridepack lint",
"clean": "pridepack clean",
"watch": "pridepack watch",
"start": "pridepack start",
"dev": "pridepack dev",
"test": "vitest"
},
"private": true,
"description": "Base select control styles for Tesseract.",
"repository": {
"url": "",
"type": "git"
},
"homepage": "",
"bugs": {
"url": ""
},
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>",
"publishConfig": {
"access": "restricted"
},
"types": "./dist/types/index.d.ts",
"exports": {
".": {
"development": {
"require": "./dist/cjs/development/index.js",
"import": "./dist/esm/development/index.js"
},
"require": "./dist/cjs/production/index.js",
"import": "./dist/esm/production/index.js",
"types": "./dist/types/index.d.ts"
}
},
"typesVersions": {
"*": {}
},
"main": "./dist/cjs/production/index.js",
"module": "./dist/esm/production/index.js"
}

+ 3
- 0
packages/web/base/selectcontrol/pridepack.json View File

@@ -0,0 +1,3 @@
{
"target": "es2017"
}

+ 5
- 0
packages/web/base/selectcontrol/src/index.ts View File

@@ -0,0 +1,5 @@
export interface SelectOption {
label: string,
value?: string | number | readonly string[]
children?: SelectOption[]
}

+ 9
- 0
packages/web/base/selectcontrol/tsconfig.json View File

@@ -0,0 +1,9 @@
{
"exclude": ["node_modules"],
"extends": "../../../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"emitDeclarationOnly": true,
"declaration": true
}
}

+ 3380
- 0
packages/web/base/selectcontrol/yarn.lock
File diff suppressed because it is too large
View File


+ 71
- 0
packages/web/base/textcontrol/package.json View File

@@ -0,0 +1,71 @@
{
"name": "@tesseract-design/web-base-textcontrol",
"version": "0.0.0",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=10"
},
"license": "MIT",
"keywords": [
"pridepack"
],
"devDependencies": {
"@types/node": "^18.0.0",
"@types/react": "^18.2.6",
"csstype": "^3.1.2",
"eslint": "^8.20.0",
"eslint-config-lxsmnsyc": "^0.4.7",
"pridepack": "2.4.4",
"tslib": "^2.4.0",
"typescript": "^4.7.4",
"vitest": "^0.19.1"
},
"dependencies": {
"@tesseract-design/goofy-goober": "link:../../../../../goofy-goober"
},
"scripts": {
"prepublishOnly": "pridepack clean && pridepack build",
"build": "pridepack build",
"type-check": "pridepack check",
"lint": "pridepack lint",
"clean": "pridepack clean",
"watch": "pridepack watch",
"start": "pridepack start",
"dev": "pridepack dev",
"test": "vitest"
},
"private": true,
"description": "Base text control styles for Tesseract.",
"repository": {
"url": "",
"type": "git"
},
"homepage": "",
"bugs": {
"url": ""
},
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>",
"publishConfig": {
"access": "restricted"
},
"types": "./dist/types/index.d.ts",
"exports": {
".": {
"development": {
"require": "./dist/cjs/development/index.js",
"import": "./dist/esm/development/index.js"
},
"require": "./dist/cjs/production/index.js",
"import": "./dist/esm/production/index.js",
"types": "./dist/types/index.d.ts"
}
},
"typesVersions": {
"*": {}
},
"main": "./dist/cjs/production/index.js",
"module": "./dist/esm/production/index.js"
}

+ 3
- 0
packages/web/base/textcontrol/pridepack.json View File

@@ -0,0 +1,3 @@
{
"target": "es2017"
}

+ 400
- 0
packages/web/base/textcontrol/src/index.ts View File

@@ -0,0 +1,400 @@
import { css } from '@tesseract-design/goofy-goober';

export enum TextControlSize {
SMALL = 'small',
MEDIUM = 'medium',
LARGE = 'large',
}

export enum TextControlStyle {
DEFAULT = 'default',
ALTERNATE = 'alternate',
}

export const MIN_HEIGHTS: Record<TextControlSize, string> = {
[TextControlSize.SMALL]: '2.5rem',
[TextControlSize.MEDIUM]: '3rem',
[TextControlSize.LARGE]: '4rem',
};

const LABEL_VERTICAL_PADDING_SIZES: Record<TextControlSize, string> = {
[TextControlSize.SMALL]: '0.125rem',
[TextControlSize.MEDIUM]: '0.25rem',
[TextControlSize.LARGE]: '0.375rem',
};

const INPUT_FONT_SIZES: Record<TextControlSize, string> = {
[TextControlSize.SMALL]: '0.75em',
[TextControlSize.MEDIUM]: '0.85em',
[TextControlSize.LARGE]: '1em',
};

const SECONDARY_TEXT_SIZES: Record<TextControlSize, string> = {
[TextControlSize.SMALL]: '0.6em',
[TextControlSize.MEDIUM]: '0.725em',
[TextControlSize.LARGE]: '0.85em',
};

const MULTILINE_VERTICAL_PADDING_FACTORS: Record<TextControlSize, string> = {
[TextControlSize.SMALL]: '1.25',
[TextControlSize.MEDIUM]: '1.2',
[TextControlSize.LARGE]: '1.45',
};

const ALTERNATE_VERTICAL_PADDING_FACTORS: Record<TextControlSize, string> = {
[TextControlSize.SMALL]: '1.75',
[TextControlSize.MEDIUM]: '1.35',
[TextControlSize.LARGE]: '1.25',
};

export type TextControlBaseArgs = {
/**
* Will the component occupy the whole width of its container?
*/
block: boolean,
/**
* Stylistic variant of the component.
*/
style: TextControlStyle,
/**
* Will the component display a surrounding border?
*/
border: boolean,
/**
* Does the component include an additional indicator for labels?
*/
indicator: boolean,
/**
* Size of the component.
*/
size: TextControlSize,
/**
* Can the size of the component be changed?
*/
resizable: boolean,
/**
* Does this component have predefined values?
*/
predefinedValues: boolean,
}

export const Root = ({
block,
}: TextControlBaseArgs): string => css.cx(
css`
vertical-align: middle;
position: relative;
border-radius: 0.25rem;
font-family: var(--font-family-base, sans-serif);
max-width: 100%;
&:focus-within {
--color-accent: var(--color-hover, red);
}
& > span {
border-color: var(--color-accent, blue);
box-sizing: border-box;
display: inline-block;
border-width: 0.125rem;
border-style: solid;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: inherit;
z-index: 2;
pointer-events: none;
transition-property: border-color;
}
& > span::before {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
content: '';
border-radius: 0.125rem;
opacity: 0.5;
pointer-events: none;
box-shadow: 0 0 0 0 var(--color-accent, blue);
transition-property: box-shadow;
transition-duration: 150ms;
transition-timing-function: linear;
}
&:focus-within > span::before {
box-shadow: 0 0 0 0.375rem var(--color-accent, blue);
}
`,
css.dynamic({
display: block ? 'block' : 'inline-block',
}),
);

export const LabelWrapper = ({
style,
border,
indicator,
size,
}: TextControlBaseArgs): string => css.cx(
css`
color: var(--color-accent, blue);
box-sizing: border-box;
position: absolute;
top: 0;
left: 0;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: bolder;
z-index: 1;
pointer-events: none;
transition-property: color;
line-height: 0.65;
user-select: none;
`,
css.dynamic({
'padding-bottom': LABEL_VERTICAL_PADDING_SIZES[size],
'font-size': SECONDARY_TEXT_SIZES[size],
}),
css.if (border) (
css`
background-color: var(--color-bg, white);
`
),
css.if (style === TextControlStyle.ALTERNATE) (
css.dynamic({
'padding-top': `calc(${LABEL_VERTICAL_PADDING_SIZES[size]} * 0.5)`,
}),
css.if (border) (
css`
padding-left: 0.5rem;
`,
css.dynamic({
'padding-right': indicator ? MIN_HEIGHTS[size] : '0.5rem',
}),
),
css.if (!border && indicator) (
css.dynamic({
'padding-right': MIN_HEIGHTS[size],
}),
),
),
css.if (style === TextControlStyle.DEFAULT) (
css`
padding-left: 0.5rem;
`,
css.dynamic({
'padding-top': LABEL_VERTICAL_PADDING_SIZES[size],
'padding-right': !indicator ? '0.5rem' : MIN_HEIGHTS[size],
}),
),
)

export const Input = ({
style,
size,
indicator,
border,
resizable,
predefinedValues,
}: TextControlBaseArgs): string => css.cx(
css`
appearance: none;
display: block;
box-sizing: border-box;
position: relative;
border: 0;
border-radius: inherit;
margin: 0;
font-family: inherit;
min-width: 8rem;
max-width: 100%;
width: 100%;
z-index: 1;
transition-property: background-color, color;
&:focus {
outline: 0;
color: var(--color-fg, black);
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
&:disabled ~ * {
opacity: 0.5;
}
`,
css.media('only screen') (
css`
background-color: var(--color-bg, white);
color: var(--color-fg, black);
`
),
css.dynamic({
'min-height': MIN_HEIGHTS[size],
'font-size': INPUT_FONT_SIZES[size],
}),
css.if (resizable) (
css`
resize: vertical;
`
),
css.if (predefinedValues) (
css`
cursor: pointer;
`
),
css.if (border) (
css`
background-color: var(--color-bg, white);
`
),
css.if (style === TextControlStyle.ALTERNATE) (
css`
padding-bottom: 0;
`,
css.dynamic({
'padding-top': resizable
? `calc(${SECONDARY_TEXT_SIZES[size]} * 2.5)`
: `calc(${SECONDARY_TEXT_SIZES[size]} * 2)`,
'line-height': `calc(${MULTILINE_VERTICAL_PADDING_FACTORS[size]} * 1.1)`,
}),
css.if (border) (
css`
padding-left: 0.5rem;
`,
css.dynamic({
'padding-right': indicator ? MIN_HEIGHTS[size] : '0.5rem',
}),
),
css.if (!border && indicator) (
css.dynamic({
'padding-right': MIN_HEIGHTS[size],
}),
)
),
css.if (style === TextControlStyle.DEFAULT) (
css`
padding-left: 1rem;
`,
css.dynamic({
'padding-right': !indicator ? '1rem' : MIN_HEIGHTS[size],
'line-height': `calc(${MULTILINE_VERTICAL_PADDING_FACTORS[size]} * 1.1)`,
}),
css.if (resizable) (
css.dynamic({
'padding-top': `calc(${SECONDARY_TEXT_SIZES[size]} * ${MULTILINE_VERTICAL_PADDING_FACTORS[size]})`,
'padding-bottom': `calc(${SECONDARY_TEXT_SIZES[size]} * ${MULTILINE_VERTICAL_PADDING_FACTORS[size]})`,
})
),
css.if (!resizable) (
css.dynamic({
'padding-bottom': `calc(${SECONDARY_TEXT_SIZES[size]} * ${MULTILINE_VERTICAL_PADDING_FACTORS[size]} * 0.5)`,
})
)
),
)

export const HintWrapper = ({
style,
size,
border,
}: TextControlBaseArgs): string => css.cx(
css`
box-sizing: border-box;
position: absolute;
left: 0;
font-size: 0.85em;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
z-index: 1;
pointer-events: none;
user-select: none;
line-height: 0;
`,
css.if (border) (
css`
background-color: var(--color-bg, white);
`
),
css.if (style === TextControlStyle.ALTERNATE) (
css`
line-height: 1.25;
`,
css.dynamic({
top: `calc(${SECONDARY_TEXT_SIZES[size]} * ${ALTERNATE_VERTICAL_PADDING_FACTORS[size]})`,
'font-size': SECONDARY_TEXT_SIZES[size],
}),
css.if (border) (
css`
padding-left: 0.5rem;
&:last-child {
padding-right: 0.5rem;
}
`,
css.dynamic({
'padding-right': MIN_HEIGHTS[size],
})
)
),
css.if (style === TextControlStyle.DEFAULT) (
css`
bottom: 0;
padding-left: 1rem;
line-height: 1.25;
&:last-child {
padding-right: 1rem;
}
`,
css.dynamic({
'padding-bottom': `calc(${LABEL_VERTICAL_PADDING_SIZES[size]} * 0.9)`,
'padding-right': MIN_HEIGHTS[size],
'font-size': SECONDARY_TEXT_SIZES[size],
})
)
)

export const Hint = (): string => css.cx(
css`
opacity: 0.5;
`
);

export const IndicatorWrapper = ({
size
}: TextControlBaseArgs): string => css.cx(
css`
color: var(--color-accent, blue);
box-sizing: border-box;
position: absolute;
bottom: 0;
right: 0;
display: grid;
place-content: center;
padding: 0 1rem;
z-index: 2;
pointer-events: none;
transition-property: color;
line-height: 1;
user-select: none;
`,
css.dynamic({
width: MIN_HEIGHTS[size],
height: MIN_HEIGHTS[size],
}),
);

export const Indicator = (): string => css.cx(
css`
width: 1.5em;
height: 1.5em;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
`,
);

+ 9
- 0
packages/web/base/textcontrol/tsconfig.json View File

@@ -0,0 +1,9 @@
{
"exclude": ["node_modules"],
"extends": "../../../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"emitDeclarationOnly": true,
"declaration": true
}
}

+ 3413
- 0
packages/web/base/textcontrol/yarn.lock
File diff suppressed because it is too large
View File


+ 82
- 0
packages/web/categories/action/react/package.json View File

@@ -0,0 +1,82 @@
{
"name": "@tesseract-design/web-action-react",
"version": "0.0.0",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=10"
},
"license": "MIT",
"keywords": [
"pridepack"
],
"dependencies": {
"@tesseract-design/web-base-button": "link:../../../base/button"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^13.5.0",
"@types/node": "^18.0.0",
"@types/react": "^18.0.14",
"eslint": "^8.20.0",
"eslint-config-lxsmnsyc": "^0.4.7",
"jsdom": "^20.0.0",
"pridepack": "2.4.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-test-renderer": "^18.2.0",
"tslib": "^2.4.0",
"typescript": "^4.7.4",
"vitest": "^0.31.0"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"scripts": {
"prepublishOnly": "pridepack clean && pridepack build",
"build": "pridepack build",
"type-check": "pridepack check",
"lint": "pridepack lint",
"clean": "pridepack clean",
"watch": "pridepack watch",
"start": "pridepack start",
"dev": "pridepack dev",
"test": "vitest"
},
"private": true,
"description": "Action components for Tesseract for use in React.",
"repository": {
"url": "",
"type": "git"
},
"homepage": "",
"bugs": {
"url": ""
},
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>",
"publishConfig": {
"access": "restricted"
},
"types": "./dist/types/index.d.ts",
"exports": {
".": {
"development": {
"require": "./dist/cjs/development/index.js",
"import": "./dist/esm/development/index.js"
},
"require": "./dist/cjs/production/index.js",
"import": "./dist/esm/production/index.js",
"types": "./dist/types/index.d.ts"
}
},
"typesVersions": {
"*": {}
},
"main": "./dist/cjs/production/index.js",
"module": "./dist/esm/production/index.js"
}

+ 3
- 0
packages/web/categories/action/react/pridepack.json View File

@@ -0,0 +1,3 @@
{
"target": "es2017"
}

+ 4
- 0
packages/web/categories/action/react/setupTests.ts View File

@@ -0,0 +1,4 @@
import matchers from '@testing-library/jest-dom/matchers';
import '@testing-library/jest-dom';

expect.extend(matchers);

+ 190
- 0
packages/web/categories/action/react/src/components/ActionButton/ActionButton.test.tsx View File

@@ -0,0 +1,190 @@
import * as React from 'react';
import {
render,
screen,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as ButtonBase from '@tesseract-design/web-base-button';
import { vi } from 'vitest';
import {
ActionButton,
ActionButtonType,
} from '.';

vi.mock('@tesseract-design/web-base-button');

describe('ActionButton', () => {
it('renders a button', () => {
render(
<ActionButton />
);
const button: HTMLButtonElement = screen.getByRole('button');
expect(button).toBeInTheDocument();
expect(button).toHaveProperty('type', 'button');
});

it('renders a subtext', () => {
render(
<ActionButton
subtext="subtext"
/>
);
const subtext: HTMLElement = screen.getByTestId('subtext');
expect(subtext).toBeInTheDocument();
});

it('renders a badge', () => {
render(
<ActionButton
badge="badge"
/>
);
const badge: HTMLElement = screen.getByTestId('badge');
expect(badge).toBeInTheDocument();
});

it('renders as a menu item', () => {
render(
<ActionButton
menuItem
/>
);
const menuItemIndicator: HTMLElement = screen.getByTestId('menuItemIndicator');
expect(menuItemIndicator).toBeInTheDocument();
});

it('handles click events', () => {
const onClick = vi.fn().mockImplementationOnce((e) => { e.preventDefault() });
render(
<ActionButton
onClick={onClick}
/>
);
const button: HTMLButtonElement = screen.getByRole('button');
userEvent.click(button);
expect(onClick).toBeCalled();
});

it('renders a compact button', () => {
render(
<ActionButton
compact
/>
);

expect(ButtonBase.Button).toBeCalledWith(expect.objectContaining({
compact: true,
}));

expect(ButtonBase.Label).toBeCalledWith(expect.objectContaining({
compact: true,
}));
});

describe.each(Object.values(ButtonBase.ButtonSize))('on %s size', (size) => {
it('renders button styles', () => {
render(
<ActionButton
size={size}
/>
);

expect(ButtonBase.Button).toBeCalledWith(expect.objectContaining({
size,
}));
});

it('renders badge styles', () => {
render(
<ActionButton
size={size}
badge="badge"
/>
);

expect(ButtonBase.BadgeContainer).toBeCalledWith(expect.objectContaining({
size,
}));
});

it('renders indicator styles', () => {
render(
<ActionButton
size={size}
menuItem
/>
);

expect(ButtonBase.IndicatorWrapper).toBeCalledWith(expect.objectContaining({
size,
}));
});
});

it.each(Object.values(ButtonBase.ButtonVariant))('renders a button with variant %s', (variant) => {
render(
<ActionButton
variant={variant}
/>
);

expect(ButtonBase.Button).toBeCalledWith(expect.objectContaining({
variant,
}));
});

it('renders a bordered button', () => {
render(
<ActionButton
border
/>
);

expect(ButtonBase.Border).toBeCalledWith(expect.objectContaining({
border: true,
}));
});

it('renders a block button', () => {
render(
<ActionButton
block
/>
);

expect(ButtonBase.Border).toBeCalledWith(expect.objectContaining({
block: true,
}));
});

it('renders children', () => {
render(
<ActionButton>
Foo
</ActionButton>
);

const children: HTMLElement = screen.getByTestId('children');
expect(children).toHaveTextContent('Foo');
});

it.each(Object.values(ActionButtonType))('renders a button with type %s', (buttonType) => {
render(
<ActionButton
type={buttonType}
/>
);
const button: HTMLButtonElement = screen.getByRole('button');
expect(button).toHaveProperty('type', buttonType);
});

it('renders a disabled button', () => {
render(
<ActionButton
disabled
/>
);
const button: HTMLButtonElement = screen.getByRole('button');
expect(button).toBeDisabled();
});
});

+ 172
- 0
packages/web/categories/action/react/src/components/ActionButton/index.tsx View File

@@ -0,0 +1,172 @@
import * as React from 'react';
import * as ButtonBase from '@tesseract-design/web-base-button';

/**
* Available ActionButton type values.
*/
export enum ActionButtonType {
SUBMIT = 'submit',
RESET = 'reset',
BUTTON = 'button',
}

/**
* Props for the component.
*/
export interface ActionButtonProps extends Omit<React.HTMLProps<HTMLButtonElement>, 'size' | 'type' | 'style'> {
/**
* Size of the component.
*/
size?: ButtonBase.ButtonSize,
/**
* Variant of the component.
*/
variant?: ButtonBase.ButtonVariant,
/**
* Should the component display a border?
*/
border?: boolean,
/**
* Should the component occupy the whole width of its parent?
*/
block?: boolean,
/**
* Type of the component.
*/
type?: ActionButtonType,
/**
* Does the component need to conserve space?
*/
compact?: boolean,
/**
* Complementary content of the component.
*/
subtext?: React.ReactNode,
/**
* Short complementary content displayed at the edge of the component.
*/
badge?: React.ReactNode,
/**
* Is this component part of a menu?
*/
menuItem?: boolean,
}

/**
* Component for performing an action upon activation (e.g. when clicked).
*
* This component functions as a regular button.
*/
export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProps>(
(
{
size = ButtonBase.ButtonSize.MEDIUM,
variant = ButtonBase.ButtonVariant.OUTLINE,
border = false,
children,
type = ActionButtonType.BUTTON,
block = false,
disabled = false,
compact = false,
subtext,
badge,
menuItem = false,
className: _className,
as: _as,
...etcProps
}: ActionButtonProps,
ref,
) => {
const styleProps = React.useMemo<ButtonBase.ButtonBaseArgs>(() => ({
size,
block,
variant,
border,
compact,
menuItem,
disabled,
}), [size, block, variant, border, compact, menuItem, disabled]);

return (
<button
{...etcProps}
disabled={disabled}
className={ButtonBase.Button(styleProps)}
ref={ref}
type={type}
>
<span
className={ButtonBase.Border(styleProps)}
/>
<span
className={ButtonBase.Label(styleProps)}
>
<span
className={ButtonBase.MainText()}
data-testid="children"
>
<span
className={ButtonBase.OverflowText()}
>
{children}
</span>
</span>
{
subtext
&& (
<>
{' '}
<span
className={ButtonBase.Subtext()}
data-testid="subtext"
>
<span
className={ButtonBase.OverflowText()}
>
{subtext}
</span>
</span>
</>
)
}
</span>
{
badge
&& (
<>
{' '}
<span
className={ButtonBase.BadgeContainer(styleProps)}
data-testid="badge"
>
{badge}
</span>
</>
)
}
{
menuItem
&& (
<>
{' '}
<span
className={ButtonBase.IndicatorWrapper(styleProps)}
data-testid="menuItemIndicator"
>
<svg
className={ButtonBase.Indicator()}
viewBox="0 0 24 24"
role="presentation"
>
<polyline points="9 18 15 12 9 6"/>
</svg>
</span>
</>
)
}
</button>
);
},
);

ActionButton.displayName = 'ActionButton';

+ 1
- 0
packages/web/categories/action/react/src/index.ts View File

@@ -0,0 +1 @@
export * from './components/ActionButton';

+ 9
- 0
packages/web/categories/action/react/src/web-action-react.test.ts View File

@@ -0,0 +1,9 @@
import * as WebActionReact from '.';

describe('web-action-react', () => {
it.each([
'ActionButton',
])('exports %s', (namedExport) => {
expect(WebActionReact).toHaveProperty(namedExport);
});
});

+ 11
- 0
packages/web/categories/action/react/tsconfig.json View File

@@ -0,0 +1,11 @@
{
"exclude": ["node_modules"],
"extends": "../../../../../tsconfig.json",
"compilerOptions": {
"lib": ["dom"],
"rootDir": "src",
"jsx": "react",
"emitDeclarationOnly": true,
"declaration": true
}
}

+ 9
- 0
packages/web/categories/action/react/vite.config.ts View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['setupTests.ts'],
},
})

+ 4209
- 0
packages/web/categories/action/react/yarn.lock
File diff suppressed because it is too large
View File


+ 82
- 0
packages/web/categories/formatted/react/package.json View File

@@ -0,0 +1,82 @@
{
"name": "@tesseract-design/web-formatted-react",
"version": "0.0.0",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=10"
},
"license": "MIT",
"keywords": [
"pridepack"
],
"dependencies": {
"@tesseract-design/web-base-textcontrol": "link:../../../base/textcontrol"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^13.5.0",
"@types/node": "^18.0.0",
"@types/react": "^18.0.14",
"eslint": "^8.20.0",
"eslint-config-lxsmnsyc": "^0.4.7",
"jsdom": "^20.0.0",
"pridepack": "2.4.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-test-renderer": "^18.2.0",
"tslib": "^2.4.0",
"typescript": "^4.7.4",
"vitest": "^0.31.0"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"scripts": {
"prepublishOnly": "pridepack clean && pridepack build",
"build": "pridepack build",
"type-check": "pridepack check",
"lint": "pridepack lint",
"clean": "pridepack clean",
"watch": "pridepack watch",
"start": "pridepack start",
"dev": "pridepack dev",
"test": "vitest"
},
"private": true,
"description": "Formatted components for Tesseract for use in React.",
"repository": {
"url": "",
"type": "git"
},
"homepage": "",
"bugs": {
"url": ""
},
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>",
"publishConfig": {
"access": "restricted"
},
"types": "./dist/types/index.d.ts",
"exports": {
".": {
"development": {
"require": "./dist/cjs/development/index.js",
"import": "./dist/esm/development/index.js"
},
"require": "./dist/cjs/production/index.js",
"import": "./dist/esm/production/index.js",
"types": "./dist/types/index.d.ts"
}
},
"typesVersions": {
"*": {}
},
"main": "./dist/cjs/production/index.js",
"module": "./dist/esm/production/index.js"
}

+ 3
- 0
packages/web/categories/formatted/react/pridepack.json View File

@@ -0,0 +1,3 @@
{
"target": "es2017"
}

+ 4
- 0
packages/web/categories/formatted/react/setupTests.ts View File

@@ -0,0 +1,4 @@
import matchers from '@testing-library/jest-dom/matchers';
import '@testing-library/jest-dom';

expect.extend(matchers);

+ 203
- 0
packages/web/categories/formatted/react/src/components/EmailAddressInput/EmailAddressInput.test.tsx View File

@@ -0,0 +1,203 @@
import * as React from 'react';
import {
render,
screen
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as TextControlBase from '@tesseract-design/web-base-textcontrol';
import { vi } from 'vitest';
import {
EmailAddressInput,
} from '.';

vi.mock('@tesseract-design/web-base-textcontrol');

describe('EmailAddressInput', () => {
it('renders a textbox', () => {
render(
<EmailAddressInput />
);
const textbox = screen.getByRole('textbox');
expect(textbox).toBeInTheDocument();
expect(textbox).toHaveProperty('type', 'email');
});

it('renders a border', () => {
render(
<EmailAddressInput
border
/>
);
const border = screen.getByTestId('border');
expect(border).toBeInTheDocument();
});

it('renders a label', () => {
render(
<EmailAddressInput
label="foo"
/>
);
const textbox = screen.getByLabelText('foo');
expect(textbox).toBeInTheDocument();
const label = screen.getByTestId('label');
expect(label).toHaveTextContent('foo');
});

it('renders a hidden label', () => {
render(
<EmailAddressInput
label="foo"
hiddenLabel
/>
);
const textbox = screen.getByLabelText('foo');
expect(textbox).toBeInTheDocument();
const label = screen.queryByTestId('label');
expect(label).toBeNull();
});

it('renders a hint', () => {
render(
<EmailAddressInput
hint="foo"
/>
);
const hint = screen.getByTestId('hint');
expect(hint).toBeInTheDocument();
});

it('renders an indicator', () => {
render(
<EmailAddressInput
indicator={
<div data-testid="indicator" />
}
/>
);
const indicator = screen.getByTestId('indicator');
expect(indicator).toBeInTheDocument();
});

describe.each(Object.values(TextControlBase.TextControlSize))('on %s size', (size) => {
it('renders input styles', () => {
render(
<EmailAddressInput
size={size}
/>
);

expect(TextControlBase.Input).toBeCalledWith(expect.objectContaining({
size,
}));
});

it('renders hint styles', () => {
render(
<EmailAddressInput
size={size}
hint="hint"
/>
);

expect(TextControlBase.HintWrapper).toBeCalledWith(expect.objectContaining({
size,
}));
});

it('renders indicator styles', () => {
render(
<EmailAddressInput
size={size}
indicator={
<div data-testid="indicator" />
}
/>
);

expect(TextControlBase.IndicatorWrapper).toBeCalledWith(expect.objectContaining({
size,
}));
});
});

it('renders a block textbox', () => {
render(
<EmailAddressInput
block
/>
);

expect(TextControlBase.Root).toBeCalledWith(expect.objectContaining({
block: true,
}));
});

describe.each(Object.values(TextControlBase.TextControlStyle))('on %s style', (style) => {
it('renders input styles', () => {
render(
<EmailAddressInput
style={style}
/>
);

expect(TextControlBase.Input).toBeCalledWith(expect.objectContaining({
style,
}));
});

it('renders hint styles', () => {
render(
<EmailAddressInput
style={style}
hint="hint"
/>
);

expect(TextControlBase.HintWrapper).toBeCalledWith(expect.objectContaining({
style,
}));
});

it('renders indicator styles', () => {
render(
<EmailAddressInput
style={style}
indicator={
<div
data-testid="indicator"
/>
}
/>
);

expect(TextControlBase.IndicatorWrapper).toBeCalledWith(expect.objectContaining({
style,
}));
});
});

it('handles change events', () => {
const onChange = vi.fn().mockImplementationOnce((e) => { e.preventDefault() });
render(
<EmailAddressInput
onChange={onChange}
/>
);
const textbox: HTMLInputElement = screen.getByRole('textbox');
userEvent.type(textbox, 'foobar');
expect(onChange).toBeCalled();
});

it('handles input events', () => {
const onInput = vi.fn().mockImplementationOnce((e) => { e.preventDefault() });
render(
<EmailAddressInput
onInput={onInput}
/>
);
const textbox: HTMLInputElement = screen.getByTestId('input');
userEvent.type(textbox, 'foobar');
expect(onInput).toBeCalled();
});
});

+ 124
- 0
packages/web/categories/formatted/react/src/components/EmailAddressInput/index.tsx View File

@@ -0,0 +1,124 @@
import * as React from 'react';
import * as TextControlBase from '@tesseract-design/web-base-textcontrol';

export type EmailAddressInputProps = Omit<React.HTMLProps<HTMLInputElement>, 'size' | 'style'> & {
/**
* Short textual description indicating the nature of the component's value.
*/
label?: React.ReactNode,
/**
* Short textual description as guidelines for valid input values.
*/
hint?: React.ReactNode,
/**
* Size of the component.
*/
size?: TextControlBase.TextControlSize,
/**
* Additional description, usually graphical, indicating the nature of the component's value.
*/
indicator?: React.ReactNode,
/**
* Should the component display a border?
*/
border?: boolean,
/**
* Should the component occupy the whole width of its parent?
*/
block?: boolean,
/**
* Style of the component.
*/
style?: TextControlBase.TextControlStyle,
/**
* Is the label hidden?
*/
hiddenLabel?: boolean,
}

/**
* Component for inputting email address values.
*/
export const EmailAddressInput = React.forwardRef<HTMLInputElement, EmailAddressInputProps>(
(
{
label = '',
hint = '',
indicator = null,
size = TextControlBase.TextControlSize.MEDIUM,
border = false,
block = false,
style = TextControlBase.TextControlStyle.DEFAULT,
hiddenLabel = false,
type: _type,
className: _className,
placeholder: _placeholder,
as: _as,
...etcProps
}: EmailAddressInputProps,
ref,
) => {
const styleArgs = React.useMemo<TextControlBase.TextControlBaseArgs>(() => ({
block,
border,
size,
indicator: Boolean(indicator),
style,
resizable: false,
predefinedValues: false,
}), [block, border, size, indicator, style]);

return (
<div
className={TextControlBase.Root(styleArgs)}
>
<input
{...etcProps}
className={TextControlBase.Input(styleArgs)}
ref={ref}
aria-label={label}
type="email"
data-testid="input"
/>
{
border && (
<span
data-testid="border"
/>
)
}
{
label && !hiddenLabel && (
<div
data-testid="label"
className={TextControlBase.LabelWrapper(styleArgs)}
>
{label}
</div>
)
}
{hint && (
<div
className={TextControlBase.HintWrapper(styleArgs)}
data-testid="hint"
>
<div
className={TextControlBase.Hint()}
>
{hint}
</div>
</div>
)}
{indicator && (
<div
className={TextControlBase.IndicatorWrapper(styleArgs)}
>
{indicator}
</div>
)}
</div>
);
}
);

EmailAddressInput.displayName = 'EmailAddressInput';

+ 203
- 0
packages/web/categories/formatted/react/src/components/PhoneNumberInput/PhoneNumberInput.test.tsx View File

@@ -0,0 +1,203 @@
import * as React from 'react';
import {
render,
screen
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as TextControlBase from '@tesseract-design/web-base-textcontrol';
import { vi } from 'vitest';
import {
PhoneNumberInput,
} from '.';

vi.mock('@tesseract-design/web-base-textcontrol');

describe('PhoneNumberInput', () => {
it('renders a textbox', () => {
render(
<PhoneNumberInput />
);
const textbox = screen.getByRole('textbox');
expect(textbox).toBeInTheDocument();
expect(textbox).toHaveProperty('type', 'tel');
});

it('renders a border', () => {
render(
<PhoneNumberInput
border
/>
);
const border = screen.getByTestId('border');
expect(border).toBeInTheDocument();
});

it('renders a label', () => {
render(
<PhoneNumberInput
label="foo"
/>
);
const textbox = screen.getByLabelText('foo');
expect(textbox).toBeInTheDocument();
const label = screen.getByTestId('label');
expect(label).toHaveTextContent('foo');
});

it('renders a hidden label', () => {
render(
<PhoneNumberInput
label="foo"
hiddenLabel
/>
);
const textbox = screen.getByLabelText('foo');
expect(textbox).toBeInTheDocument();
const label = screen.queryByTestId('label');
expect(label).toBeNull();
});

it('renders a hint', () => {
render(
<PhoneNumberInput
hint="foo"
/>
);
const hint = screen.getByTestId('hint');
expect(hint).toBeInTheDocument();
});

it('renders an indicator', () => {
render(
<PhoneNumberInput
indicator={
<div data-testid="indicator" />
}
/>
);
const indicator = screen.getByTestId('indicator');
expect(indicator).toBeInTheDocument();
});

describe.each(Object.values(TextControlBase.TextControlSize))('on %s size', (size) => {
it('renders input styles', () => {
render(
<PhoneNumberInput
size={size}
/>
);

expect(TextControlBase.Input).toBeCalledWith(expect.objectContaining({
size,
}));
});

it('renders hint styles', () => {
render(
<PhoneNumberInput
size={size}
hint="hint"
/>
);

expect(TextControlBase.HintWrapper).toBeCalledWith(expect.objectContaining({
size,
}));
});

it('renders indicator styles', () => {
render(
<PhoneNumberInput
size={size}
indicator={
<div data-testid="indicator" />
}
/>
);

expect(TextControlBase.IndicatorWrapper).toBeCalledWith(expect.objectContaining({
size,
}));
});
});

it('renders a block textbox', () => {
render(
<PhoneNumberInput
block
/>
);

expect(TextControlBase.Root).toBeCalledWith(expect.objectContaining({
block: true,
}));
});

describe.each(Object.values(TextControlBase.TextControlStyle))('on %s style', (style) => {
it('renders input styles', () => {
render(
<PhoneNumberInput
style={style}
/>
);

expect(TextControlBase.Input).toBeCalledWith(expect.objectContaining({
style,
}));
});

it('renders hint styles', () => {
render(
<PhoneNumberInput
style={style}
hint="hint"
/>
);

expect(TextControlBase.HintWrapper).toBeCalledWith(expect.objectContaining({
style,
}));
});

it('renders indicator styles', () => {
render(
<PhoneNumberInput
style={style}
indicator={
<div
data-testid="indicator"
/>
}
/>
);

expect(TextControlBase.IndicatorWrapper).toBeCalledWith(expect.objectContaining({
style,
}));
});
});

it('handles change events', () => {
const onChange = vi.fn().mockImplementationOnce((e) => { e.preventDefault() });
render(
<PhoneNumberInput
onChange={onChange}
/>
);
const textbox: HTMLInputElement = screen.getByRole('textbox');
userEvent.type(textbox, 'foobar');
expect(onChange).toBeCalled();
});

it('handles input events', () => {
const onInput = vi.fn().mockImplementationOnce((e) => { e.preventDefault() });
render(
<PhoneNumberInput
onInput={onInput}
/>
);
const textbox: HTMLInputElement = screen.getByTestId('input');
userEvent.type(textbox, 'foobar');
expect(onInput).toBeCalled();
});
});

+ 124
- 0
packages/web/categories/formatted/react/src/components/PhoneNumberInput/index.tsx View File

@@ -0,0 +1,124 @@
import * as React from 'react';
import * as TextControlBase from '@tesseract-design/web-base-textcontrol';

export type PhoneNumberInputProps = Omit<React.HTMLProps<HTMLInputElement>, 'size' | 'style'> & {
/**
* Short textual description indicating the nature of the component's value.
*/
label?: React.ReactNode,
/**
* Short textual description as guidelines for valid input values.
*/
hint?: React.ReactNode,
/**
* Size of the component.
*/
size?: TextControlBase.TextControlSize,
/**
* Additional description, usually graphical, indicating the nature of the component's value.
*/
indicator?: React.ReactNode,
/**
* Should the component display a border?
*/
border?: boolean,
/**
* Should the component occupy the whole width of its parent?
*/
block?: boolean,
/**
* Style of the component.
*/
style?: TextControlBase.TextControlStyle,
/**
* Is the label hidden?
*/
hiddenLabel?: boolean,
}

/**
* Component for inputting phone number values.
*/
export const PhoneNumberInput = React.forwardRef<HTMLInputElement, PhoneNumberInputProps>(
(
{
label = '',
hint = '',
indicator = null,
size = TextControlBase.TextControlSize.MEDIUM,
border = false,
block = false,
style = TextControlBase.TextControlStyle.DEFAULT,
hiddenLabel = false,
type: _type,
className: _className,
placeholder: _placeholder,
as: _as,
...etcProps
}: PhoneNumberInputProps,
ref,
) => {
const styleArgs = React.useMemo<TextControlBase.TextControlBaseArgs>(() => ({
block,
border,
size,
indicator: Boolean(indicator),
style,
resizable: false,
predefinedValues: false,
}), [block, border, size, indicator, style]);

return (
<div
className={TextControlBase.Root(styleArgs)}
>
<input
{...etcProps}
className={TextControlBase.Input(styleArgs)}
ref={ref}
aria-label={label}
type="tel"
data-testid="input"
/>
{
border && (
<span
data-testid="border"
/>
)
}
{
label && !hiddenLabel && (
<div
data-testid="label"
className={TextControlBase.LabelWrapper(styleArgs)}
>
{label}
</div>
)
}
{hint && (
<div
className={TextControlBase.HintWrapper(styleArgs)}
data-testid="hint"
>
<div
className={TextControlBase.Hint()}
>
{hint}
</div>
</div>
)}
{indicator && (
<div
className={TextControlBase.IndicatorWrapper(styleArgs)}
>
{indicator}
</div>
)}
</div>
);
}
);

PhoneNumberInput.displayName = 'PhoneNumberInput';

+ 203
- 0
packages/web/categories/formatted/react/src/components/UrlInput/UrlInput.test.tsx View File

@@ -0,0 +1,203 @@
import * as React from 'react';
import {
render,
screen
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as TextControlBase from '@tesseract-design/web-base-textcontrol';
import { vi } from 'vitest';
import {
UrlInput,
} from '.';

vi.mock('@tesseract-design/web-base-textcontrol');

describe('UrlInput', () => {
it('renders a textbox', () => {
render(
<UrlInput />
);
const textbox = screen.getByRole('textbox');
expect(textbox).toBeInTheDocument();
expect(textbox).toHaveProperty('type', 'url');
});

it('renders a border', () => {
render(
<UrlInput
border
/>
);
const border = screen.getByTestId('border');
expect(border).toBeInTheDocument();
});

it('renders a label', () => {
render(
<UrlInput
label="foo"
/>
);
const textbox = screen.getByLabelText('foo');
expect(textbox).toBeInTheDocument();
const label = screen.getByTestId('label');
expect(label).toHaveTextContent('foo');
});

it('renders a hidden label', () => {
render(
<UrlInput
label="foo"
hiddenLabel
/>
);
const textbox = screen.getByLabelText('foo');
expect(textbox).toBeInTheDocument();
const label = screen.queryByTestId('label');
expect(label).toBeNull();
});

it('renders a hint', () => {
render(
<UrlInput
hint="foo"
/>
);
const hint = screen.getByTestId('hint');
expect(hint).toBeInTheDocument();
});

it('renders an indicator', () => {
render(
<UrlInput
indicator={
<div data-testid="indicator" />
}
/>
);
const indicator = screen.getByTestId('indicator');
expect(indicator).toBeInTheDocument();
});

describe.each(Object.values(TextControlBase.TextControlSize))('on %s size', (size) => {
it('renders input styles', () => {
render(
<UrlInput
size={size}
/>
);

expect(TextControlBase.Input).toBeCalledWith(expect.objectContaining({
size,
}));
});

it('renders hint styles', () => {
render(
<UrlInput
size={size}
hint="hint"
/>
);

expect(TextControlBase.HintWrapper).toBeCalledWith(expect.objectContaining({
size,
}));
});

it('renders indicator styles', () => {
render(
<UrlInput
size={size}
indicator={
<div data-testid="indicator" />
}
/>
);

expect(TextControlBase.IndicatorWrapper).toBeCalledWith(expect.objectContaining({
size,
}));
});
});

it('renders a block textbox', () => {
render(
<UrlInput
block
/>
);

expect(TextControlBase.Root).toBeCalledWith(expect.objectContaining({
block: true,
}));
});

describe.each(Object.values(TextControlBase.TextControlStyle))('on %s style', (style) => {
it('renders input styles', () => {
render(
<UrlInput
style={style}
/>
);

expect(TextControlBase.Input).toBeCalledWith(expect.objectContaining({
style,
}));
});

it('renders hint styles', () => {
render(
<UrlInput
style={style}
hint="hint"
/>
);

expect(TextControlBase.HintWrapper).toBeCalledWith(expect.objectContaining({
style,
}));
});

it('renders indicator styles', () => {
render(
<UrlInput
style={style}
indicator={
<div
data-testid="indicator"
/>
}
/>
);

expect(TextControlBase.IndicatorWrapper).toBeCalledWith(expect.objectContaining({
style,
}));
});
});

it('handles change events', () => {
const onChange = vi.fn().mockImplementationOnce((e) => { e.preventDefault() });
render(
<UrlInput
onChange={onChange}
/>
);
const textbox: HTMLInputElement = screen.getByRole('textbox');
userEvent.type(textbox, 'foobar');
expect(onChange).toBeCalled();
});

it('handles input events', () => {
const onInput = vi.fn().mockImplementationOnce((e) => { e.preventDefault() });
render(
<UrlInput
onInput={onInput}
/>
);
const textbox: HTMLInputElement = screen.getByTestId('input');
userEvent.type(textbox, 'foobar');
expect(onInput).toBeCalled();
});
});

+ 124
- 0
packages/web/categories/formatted/react/src/components/UrlInput/index.tsx View File

@@ -0,0 +1,124 @@
import * as React from 'react';
import * as TextControlBase from '@tesseract-design/web-base-textcontrol';

export type UrlInputProps = Omit<React.HTMLProps<HTMLInputElement>, 'size' | 'style'> & {
/**
* Short textual description indicating the nature of the component's value.
*/
label?: React.ReactNode,
/**
* Short textual description as guidelines for valid input values.
*/
hint?: React.ReactNode,
/**
* Size of the component.
*/
size?: TextControlBase.TextControlSize,
/**
* Additional description, usually graphical, indicating the nature of the component's value.
*/
indicator?: React.ReactNode,
/**
* Should the component display a border?
*/
border?: boolean,
/**
* Should the component occupy the whole width of its parent?
*/
block?: boolean,
/**
* Style of the component.
*/
style?: TextControlBase.TextControlStyle,
/**
* Is the label hidden?
*/
hiddenLabel?: boolean,
}

/**
* Component for inputting URL values.
*/
export const UrlInput = React.forwardRef<HTMLInputElement, UrlInputProps>(
(
{
label = '',
hint = '',
indicator = null,
size = TextControlBase.TextControlSize.MEDIUM,
border = false,
block = false,
style = TextControlBase.TextControlStyle.DEFAULT,
hiddenLabel = false,
type: _type,
className: _className,
placeholder: _placeholder,
as: _as,
...etcProps
}: UrlInputProps,
ref,
) => {
const styleArgs = React.useMemo<TextControlBase.TextControlBaseArgs>(() => ({
block,
border,
size,
indicator: Boolean(indicator),
style,
resizable: false,
predefinedValues: false,
}), [block, border, size, indicator, style]);

return (
<div
className={TextControlBase.Root(styleArgs)}
>
<input
{...etcProps}
className={TextControlBase.Input(styleArgs)}
ref={ref}
aria-label={label}
type="url"
data-testid="input"
/>
{
border && (
<span
data-testid="border"
/>
)
}
{
label && !hiddenLabel && (
<div
data-testid="label"
className={TextControlBase.LabelWrapper(styleArgs)}
>
{label}
</div>
)
}
{hint && (
<div
className={TextControlBase.HintWrapper(styleArgs)}
data-testid="hint"
>
<div
className={TextControlBase.Hint()}
>
{hint}
</div>
</div>
)}
{indicator && (
<div
className={TextControlBase.IndicatorWrapper(styleArgs)}
>
{indicator}
</div>
)}
</div>
);
}
);

UrlInput.displayName = 'UrlInput';

+ 3
- 0
packages/web/categories/formatted/react/src/index.ts View File

@@ -0,0 +1,3 @@
export * from './components/EmailAddressInput';
export * from './components/PhoneNumberInput';
export * from './components/UrlInput';

+ 11
- 0
packages/web/categories/formatted/react/src/web-freeform-react.test.ts View File

@@ -0,0 +1,11 @@
import * as WebFormattedReact from '.';

describe('web-formatted-react', () => {
it.each([
'EmailAddressInput',
'PhoneNumberInput',
'UrlInput',
])('exports %s', (namedExport) => {
expect(WebFormattedReact).toHaveProperty(namedExport);
});
});

+ 10
- 0
packages/web/categories/formatted/react/tsconfig.json View File

@@ -0,0 +1,10 @@
{
"exclude": ["node_modules"],
"extends": "../../../../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"jsx": "react",
"emitDeclarationOnly": true,
"declaration": true
}
}

+ 9
- 0
packages/web/categories/formatted/react/vite.config.ts View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['setupTests.ts'],
},
})

+ 4209
- 0
packages/web/categories/formatted/react/yarn.lock
File diff suppressed because it is too large
View File


+ 82
- 0
packages/web/categories/freeform/react/package.json View File

@@ -0,0 +1,82 @@
{
"name": "@tesseract-design/web-freeform-react",
"version": "0.0.0",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=10"
},
"license": "MIT",
"keywords": [
"pridepack"
],
"dependencies": {
"@tesseract-design/web-base-textcontrol": "link:../../../base/textcontrol"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^13.5.0",
"@types/node": "^18.0.0",
"@types/react": "^18.0.14",
"eslint": "^8.20.0",
"eslint-config-lxsmnsyc": "^0.4.7",
"jsdom": "^20.0.0",
"pridepack": "2.4.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-test-renderer": "^18.2.0",
"tslib": "^2.4.0",
"typescript": "^4.7.4",
"vitest": "^0.31.0"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"scripts": {
"prepublishOnly": "pridepack clean && pridepack build",
"build": "pridepack build",
"type-check": "pridepack check",
"lint": "pridepack lint",
"clean": "pridepack clean",
"watch": "pridepack watch",
"start": "pridepack start",
"dev": "pridepack dev",
"test": "vitest"
},
"private": true,
"description": "Freeform components for Tesseract for use in React.",
"repository": {
"url": "",
"type": "git"
},
"homepage": "",
"bugs": {
"url": ""
},
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>",
"publishConfig": {
"access": "restricted"
},
"types": "./dist/types/index.d.ts",
"exports": {
".": {
"development": {
"require": "./dist/cjs/development/index.js",
"import": "./dist/esm/development/index.js"
},
"require": "./dist/cjs/production/index.js",
"import": "./dist/esm/production/index.js",
"types": "./dist/types/index.d.ts"
}
},
"typesVersions": {
"*": {}
},
"main": "./dist/cjs/production/index.js",
"module": "./dist/esm/production/index.js"
}

+ 3
- 0
packages/web/categories/freeform/react/pridepack.json View File

@@ -0,0 +1,3 @@
{
"target": "es2017"
}

+ 4
- 0
packages/web/categories/freeform/react/setupTests.ts View File

@@ -0,0 +1,4 @@
import matchers from '@testing-library/jest-dom/matchers';
import '@testing-library/jest-dom';

expect.extend(matchers);

+ 203
- 0
packages/web/categories/freeform/react/src/components/MaskedTextInput/MaskedTextInput.test.tsx View File

@@ -0,0 +1,203 @@
import * as React from 'react';
import {
render,
screen,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as TextControlBase from '@tesseract-design/web-base-textcontrol';
import { vi } from 'vitest';
import {
MaskedTextInput
} from '.';

vi.mock('@tesseract-design/web-base-textcontrol');

describe('MaskedTextInput', () => {
it('renders an input', () => {
render(
<MaskedTextInput />
);
const textbox: HTMLInputElement = screen.getByTestId('input');
expect(textbox).toBeInTheDocument();
expect(textbox).toHaveProperty('type', 'password');
});

it('renders a border', () => {
render(
<MaskedTextInput
border
/>
);
const border = screen.getByTestId('border');
expect(border).toBeInTheDocument();
});

it('renders a label', () => {
render(
<MaskedTextInput
label="foo"
/>
);
const textbox = screen.getByLabelText('foo');
expect(textbox).toBeInTheDocument();
const label = screen.getByTestId('label');
expect(label).toHaveTextContent('foo');
});

it('renders a hidden label', () => {
render(
<MaskedTextInput
label="foo"
hiddenLabel
/>
);
const textbox = screen.getByLabelText('foo');
expect(textbox).toBeInTheDocument();
const label = screen.queryByTestId('label');
expect(label).toBeNull();
});

it('renders a hint', () => {
render(
<MaskedTextInput
hint="foo"
/>
);
const hint = screen.getByTestId('hint');
expect(hint).toBeInTheDocument();
});

it('renders an indicator', () => {
render(
<MaskedTextInput
indicator={
<div data-testid="indicator" />
}
/>
);
const indicator = screen.getByTestId('indicator');
expect(indicator).toBeInTheDocument();
});

describe.each(Object.values(TextControlBase.TextControlSize))('on %s size', (size) => {
it('renders input styles', () => {
render(
<MaskedTextInput
size={size}
/>
);

expect(TextControlBase.Input).toBeCalledWith(expect.objectContaining({
size,
}));
});

it('renders hint styles', () => {
render(
<MaskedTextInput
size={size}
hint="hint"
/>
);

expect(TextControlBase.HintWrapper).toBeCalledWith(expect.objectContaining({
size,
}));
});

it('renders indicator styles', () => {
render(
<MaskedTextInput
size={size}
indicator={
<div data-testid="indicator" />
}
/>
);

expect(TextControlBase.IndicatorWrapper).toBeCalledWith(expect.objectContaining({
size,
}));
});
});

it('renders a block input', () => {
render(
<MaskedTextInput
block
/>
);

expect(TextControlBase.Root).toBeCalledWith(expect.objectContaining({
block: true,
}));
});

describe.each(Object.values(TextControlBase.TextControlStyle))('on %s style', (style) => {
it('renders input styles', () => {
render(
<MaskedTextInput
style={style}
/>
);

expect(TextControlBase.Input).toBeCalledWith(expect.objectContaining({
style,
}));
});

it('renders hint styles', () => {
render(
<MaskedTextInput
style={style}
hint="hint"
/>
);

expect(TextControlBase.HintWrapper).toBeCalledWith(expect.objectContaining({
style,
}));
});

it('renders indicator styles', () => {
render(
<MaskedTextInput
style={style}
indicator={
<div
data-testid="indicator"
/>
}
/>
);

expect(TextControlBase.IndicatorWrapper).toBeCalledWith(expect.objectContaining({
style,
}));
});
});

it('handles change events', () => {
const onChange = vi.fn().mockImplementationOnce((e) => { e.preventDefault() });
render(
<MaskedTextInput
onChange={onChange}
/>
);
const textbox: HTMLInputElement = screen.getByTestId('input');
userEvent.type(textbox, 'foobar');
expect(onChange).toBeCalled();
});

it('handles input events', () => {
const onInput = vi.fn().mockImplementationOnce((e) => { e.preventDefault() });
render(
<MaskedTextInput
onInput={onInput}
/>
);
const textbox: HTMLInputElement = screen.getByTestId('input');
userEvent.type(textbox, 'foobar');
expect(onInput).toBeCalled();
});
});

+ 125
- 0
packages/web/categories/freeform/react/src/components/MaskedTextInput/index.tsx View File

@@ -0,0 +1,125 @@
import * as React from 'react';
import * as TextControlBase from '@tesseract-design/web-base-textcontrol';

export type MaskedTextInputProps = Omit<React.HTMLProps<HTMLInputElement>, 'size' | 'type' | 'style'> & {
/**
* Short textual description indicating the nature of the component's value.
*/
label?: React.ReactNode,
/**
* Short textual description as guidelines for valid input values.
*/
hint?: React.ReactNode,
/**
* Size of the component.
*/
size?: TextControlBase.TextControlSize,
/**
* Additional description, usually graphical, indicating the nature of the component's value.
*/
indicator?: React.ReactNode,
/**
* Should the component display a border?
*/
border?: boolean,
/**
* Should the component occupy the whole width of its parent?
*/
block?: boolean,
/**
* Style of the component.
*/
style?: TextControlBase.TextControlStyle,
/**
* Is the label hidden?
*/
hiddenLabel?: boolean,
}

/**
* Component for inputting textual values.
*
* This component supports multiline input and adjusts its layout accordingly.
*/
export const MaskedTextInput = React.forwardRef<HTMLInputElement, MaskedTextInputProps>(
(
{
label = '',
hint = '',
indicator = null,
size = TextControlBase.TextControlSize.MEDIUM,
border = false,
block = false,
style = TextControlBase.TextControlStyle.DEFAULT,
hiddenLabel = false,
className: _className,
placeholder: _placeholder,
as: _as,
...etcProps
}: MaskedTextInputProps,
ref,
) => {
const textInputBaseArgs = React.useMemo<TextControlBase.TextControlBaseArgs>(() => ({
block,
border,
size,
indicator: Boolean(indicator),
style,
resizable: false,
predefinedValues: false,
}), [block, border, size, indicator, style]);

return (
<div
className={TextControlBase.Root(textInputBaseArgs)}
>
<input
{...etcProps}
className={TextControlBase.Input(textInputBaseArgs)}
ref={ref}
aria-label={label}
type="password"
data-testid="input"
/>
{
border && (
<span
data-testid="border"
/>
)
}
{
label && !hiddenLabel && (
<div
data-testid="label"
className={TextControlBase.LabelWrapper(textInputBaseArgs)}
>
{label}
</div>
)
}
{hint && (
<div
className={TextControlBase.HintWrapper(textInputBaseArgs)}
data-testid="hint"
>
<div
className={TextControlBase.Hint()}
>
{hint}
</div>
</div>
)}
{indicator && (
<div
className={TextControlBase.IndicatorWrapper(textInputBaseArgs)}
>
{indicator}
</div>
)}
</div>
);
},
);

MaskedTextInput.displayName = 'MaskedTextInput';

+ 200
- 0
packages/web/categories/freeform/react/src/components/MultilineTextInput/MultilineTextInput.test.tsx View File

@@ -0,0 +1,200 @@
import * as React from 'react';
import {
render,
screen
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as TextControlBase from '@tesseract-design/web-base-textcontrol';
import { vi } from 'vitest';
import {
MultilineTextInput
} from '.';

vi.mock('@tesseract-design/web-base-textcontrol');

describe('MultilineTextInput', () => {
it('renders a textbox', () => {
render(<MultilineTextInput />);
const textbox: HTMLTextAreaElement = screen.getByRole('textbox');
expect(textbox).toBeInTheDocument();
});

it('renders a border', () => {
render(
<MultilineTextInput
border
/>
);
const border = screen.getByTestId('border');
expect(border).toBeInTheDocument();
});

it('renders a label', () => {
render(
<MultilineTextInput
label="foo"
/>
);
const textbox = screen.getByLabelText('foo');
expect(textbox).toBeInTheDocument();
const label = screen.getByTestId('label');
expect(label).toHaveTextContent('foo');
});

it('renders a hidden label', () => {
render(
<MultilineTextInput
label="foo"
hiddenLabel
/>
);
const textbox = screen.getByLabelText('foo');
expect(textbox).toBeInTheDocument();
const label = screen.queryByTestId('label');
expect(label).toBeNull();
});

it('renders a hint', () => {
render(
<MultilineTextInput
hint="foo"
/>
);
const hint = screen.getByTestId('hint');
expect(hint).toBeInTheDocument();
});

it('renders an indicator', () => {
render(
<MultilineTextInput
indicator={
<div data-testid="indicator" />
}
/>
);
const indicator = screen.getByTestId('indicator');
expect(indicator).toBeInTheDocument();
});

describe.each(Object.values(TextControlBase.TextControlSize))('on %s size', (size) => {
it('renders input styles', () => {
render(
<MultilineTextInput
size={size}
/>
);

expect(TextControlBase.Input).toBeCalledWith(expect.objectContaining({
size,
}));
});

it('renders hint styles', () => {
render(
<MultilineTextInput
size={size}
hint="hint"
/>
);

expect(TextControlBase.HintWrapper).toBeCalledWith(expect.objectContaining({
size,
}));
});

it('renders indicator styles', () => {
render(
<MultilineTextInput
size={size}
indicator={
<div data-testid="indicator" />
}
/>
);

expect(TextControlBase.IndicatorWrapper).toBeCalledWith(expect.objectContaining({
size,
}));
});
});

it('renders a block textbox', () => {
render(
<MultilineTextInput
block
/>
);

expect(TextControlBase.Root).toBeCalledWith(expect.objectContaining({
block: true,
}));
});

describe.each(Object.values(TextControlBase.TextControlStyle))('on %s style', (style) => {
it('renders input styles', () => {
render(
<MultilineTextInput
style={style}
/>
);

expect(TextControlBase.Input).toBeCalledWith(expect.objectContaining({
style,
}));
});

it('renders hint styles', () => {
render(
<MultilineTextInput
style={style}
hint="hint"
/>
);

expect(TextControlBase.HintWrapper).toBeCalledWith(expect.objectContaining({
style,
}));
});

it('renders indicator styles', () => {
render(
<MultilineTextInput
style={style}
indicator={
<div
data-testid="indicator"
/>
}
/>
);

expect(TextControlBase.IndicatorWrapper).toBeCalledWith(expect.objectContaining({
style,
}));
});
});

it('handles change events', () => {
const onChange = vi.fn().mockImplementationOnce((e) => { e.preventDefault() });
render(
<MultilineTextInput
onChange={onChange}
/>
);
const textbox: HTMLTextAreaElement = screen.getByRole('textbox');
userEvent.type(textbox, 'foobar');
expect(onChange).toBeCalled();
});

it('handles input events', () => {
const onInput = vi.fn().mockImplementationOnce((e) => { e.preventDefault() });
render(
<MultilineTextInput
onInput={onInput}
/>
);
const textbox: HTMLInputElement = screen.getByTestId('input');
userEvent.type(textbox, 'foobar');
expect(onInput).toBeCalled();
});
});

+ 127
- 0
packages/web/categories/freeform/react/src/components/MultilineTextInput/index.tsx View File

@@ -0,0 +1,127 @@
import * as React from 'react';
import * as TextControlBase from '@tesseract-design/web-base-textcontrol';

export type MultilineTextInputProps = Omit<React.HTMLProps<HTMLTextAreaElement>, 'size' | 'style'> & {
/**
* Short textual description indicating the nature of the component's value.
*/
label?: React.ReactNode,
/**
* Short textual description as guidelines for valid input values.
*/
hint?: React.ReactNode,
/**
* Size of the component.
*/
size?: TextControlBase.TextControlSize,
/**
* Additional description, usually graphical, indicating the nature of the component's value.
*/
indicator?: React.ReactNode,
/**
* Should the component display a border?
*/
border?: boolean,
/**
* Should the component occupy the whole width of its parent?
*/
block?: boolean,
/**
* Style of the component.
*/
style?: TextControlBase.TextControlStyle,
/**
* Is the label hidden?
*/
hiddenLabel?: boolean,
}

/**
* Component for inputting textual values.
*
* This component supports multiline input and adjusts its layout accordingly.
*/
export const MultilineTextInput = React.forwardRef<HTMLTextAreaElement, MultilineTextInputProps>(
(
{
label = '',
hint = '',
indicator = null,
size = TextControlBase.TextControlSize.MEDIUM,
border = false,
block = false,
style = TextControlBase.TextControlStyle.DEFAULT,
hiddenLabel = false,
className: _className,
placeholder: _placeholder,
as: _as,
...etcProps
}: MultilineTextInputProps,
ref,
) => {
const textInputBaseArgs = React.useMemo<TextControlBase.TextControlBaseArgs>(() => ({
block,
border,
size,
indicator: Boolean(indicator),
style,
resizable: true,
predefinedValues: false,
}), [block, border, size, indicator, style]);

return (
<div
className={TextControlBase.Root(textInputBaseArgs)}
>
<textarea
{...etcProps}
className={TextControlBase.Input(textInputBaseArgs)}
ref={ref}
aria-label={label}
style={{
height: TextControlBase.MIN_HEIGHTS[size],
}}
data-testid="input"
/>
{
border && (
<span
data-testid="border"
/>
)
}
{
label && !hiddenLabel && (
<div
data-testid="label"
className={TextControlBase.LabelWrapper(textInputBaseArgs)}
>
{label}
</div>
)
}
{hint && (
<div
className={TextControlBase.HintWrapper(textInputBaseArgs)}
data-testid="hint"
>
<div
className={TextControlBase.Hint()}
>
{hint}
</div>
</div>
)}
{indicator && (
<div
className={TextControlBase.IndicatorWrapper(textInputBaseArgs)}
>
{indicator}
</div>
)}
</div>
);
}
);

MultilineTextInput.displayName = 'MultilineTextInput';

+ 213
- 0
packages/web/categories/freeform/react/src/components/TextInput/TextInput.test.tsx View File

@@ -0,0 +1,213 @@
import * as React from 'react';
import {
render,
screen
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as TextControlBase from '@tesseract-design/web-base-textcontrol';
import { vi } from 'vitest';
import {
TextInput, TextInputType,
} from '.';

vi.mock('@tesseract-design/web-base-textcontrol');

describe('TextInput', () => {
it('renders a textbox', () => {
render(
<TextInput />
);
const textbox = screen.getByRole('textbox');
expect(textbox).toBeInTheDocument();
expect(textbox).toHaveProperty('type', 'text');
});

it('renders a border', () => {
render(
<TextInput
border
/>
);
const border = screen.getByTestId('border');
expect(border).toBeInTheDocument();
});

it('renders a label', () => {
render(
<TextInput
label="foo"
/>
);
const textbox = screen.getByLabelText('foo');
expect(textbox).toBeInTheDocument();
const label = screen.getByTestId('label');
expect(label).toHaveTextContent('foo');
});

it('renders a hidden label', () => {
render(
<TextInput
label="foo"
hiddenLabel
/>
);
const textbox = screen.getByLabelText('foo');
expect(textbox).toBeInTheDocument();
const label = screen.queryByTestId('label');
expect(label).toBeNull();
});

it('renders a hint', () => {
render(
<TextInput
hint="foo"
/>
);
const hint = screen.getByTestId('hint');
expect(hint).toBeInTheDocument();
});

it('renders an indicator', () => {
render(
<TextInput
indicator={
<div data-testid="indicator" />
}
/>
);
const indicator = screen.getByTestId('indicator');
expect(indicator).toBeInTheDocument();
});

describe.each(Object.values(TextControlBase.TextControlSize))('on %s size', (size) => {
it('renders input styles', () => {
render(
<TextInput
size={size}
/>
);

expect(TextControlBase.Input).toBeCalledWith(expect.objectContaining({
size,
}));
});

it('renders hint styles', () => {
render(
<TextInput
size={size}
hint="hint"
/>
);

expect(TextControlBase.HintWrapper).toBeCalledWith(expect.objectContaining({
size,
}));
});

it('renders indicator styles', () => {
render(
<TextInput
size={size}
indicator={
<div data-testid="indicator" />
}
/>
);

expect(TextControlBase.IndicatorWrapper).toBeCalledWith(expect.objectContaining({
size,
}));
});
});

it('renders a block textbox', () => {
render(
<TextInput
block
/>
);

expect(TextControlBase.Root).toBeCalledWith(expect.objectContaining({
block: true,
}));
});

it.each(Object.values(TextInputType))('renders a textbox with type %s', (buttonType) => {
render(
<TextInput
type={buttonType}
/>
);
const textbox: HTMLButtonElement = screen.getByTestId('input');
expect(textbox).toHaveProperty('type', buttonType);
});

describe.each(Object.values(TextControlBase.TextControlStyle))('on %s style', (style) => {
it('renders input styles', () => {
render(
<TextInput
style={style}
/>
);

expect(TextControlBase.Input).toBeCalledWith(expect.objectContaining({
style,
}));
});

it('renders hint styles', () => {
render(
<TextInput
style={style}
hint="hint"
/>
);

expect(TextControlBase.HintWrapper).toBeCalledWith(expect.objectContaining({
style,
}));
});

it('renders indicator styles', () => {
render(
<TextInput
style={style}
indicator={
<div
data-testid="indicator"
/>
}
/>
);

expect(TextControlBase.IndicatorWrapper).toBeCalledWith(expect.objectContaining({
style,
}));
});
});

it('handles change events', () => {
const onChange = vi.fn().mockImplementationOnce((e) => { e.preventDefault() });
render(
<TextInput
onChange={onChange}
/>
);
const textbox: HTMLInputElement = screen.getByRole('textbox');
userEvent.type(textbox, 'foobar');
expect(onChange).toBeCalled();
});

it('handles input events', () => {
const onInput = vi.fn().mockImplementationOnce((e) => { e.preventDefault() });
render(
<TextInput
onInput={onInput}
/>
);
const textbox: HTMLInputElement = screen.getByTestId('input');
userEvent.type(textbox, 'foobar');
expect(onInput).toBeCalled();
});
});

+ 135
- 0
packages/web/categories/freeform/react/src/components/TextInput/index.tsx View File

@@ -0,0 +1,135 @@
import * as React from 'react';
import * as TextControlBase from '@tesseract-design/web-base-textcontrol';

export enum TextInputType {
TEXT = 'text',
SEARCH = 'search',
}

export type TextInputProps = Omit<React.HTMLProps<HTMLInputElement>, 'size' | 'type' | 'style'> & {
/**
* Short textual description indicating the nature of the component's value.
*/
label?: React.ReactNode,
/**
* Short textual description as guidelines for valid input values.
*/
hint?: React.ReactNode,
/**
* Size of the component.
*/
size?: TextControlBase.TextControlSize,
/**
* Additional description, usually graphical, indicating the nature of the component's value.
*/
indicator?: React.ReactNode,
/**
* Should the component display a border?
*/
border?: boolean,
/**
* Should the component occupy the whole width of its parent?
*/
block?: boolean,
/**
* Type of the component value.
*/
type?: TextInputType,
/**
* Style of the component.
*/
style?: TextControlBase.TextControlStyle,
/**
* Is the label hidden?
*/
hiddenLabel?: boolean,
}

/**
* Component for inputting textual values.
*
* This component supports multiline input and adjusts its layout accordingly.
*/
export const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>(
(
{
label = '',
hint = '',
indicator = null,
size = TextControlBase.TextControlSize.MEDIUM,
border = false,
block = false,
type = TextInputType.TEXT,
style = TextControlBase.TextControlStyle.DEFAULT,
hiddenLabel = false,
className: _className,
placeholder: _placeholder,
as: _as,
...etcProps
}: TextInputProps,
ref,
) => {
const textInputBaseArgs = React.useMemo<TextControlBase.TextControlBaseArgs>(() => ({
block,
border,
size,
indicator: Boolean(indicator),
style,
resizable: false,
predefinedValues: false,
}), [block, border, size, indicator, style]);

return (
<div
className={TextControlBase.Root(textInputBaseArgs)}
>
<input
{...etcProps}
className={TextControlBase.Input(textInputBaseArgs)}
ref={ref}
aria-label={label}
type={type}
data-testid="input"
/>
{
border && (
<span
data-testid="border"
/>
)
}
{
label && !hiddenLabel && (
<div
data-testid="label"
className={TextControlBase.LabelWrapper(textInputBaseArgs)}
>
{label}
</div>
)
}
{hint && (
<div
className={TextControlBase.HintWrapper(textInputBaseArgs)}
data-testid="hint"
>
<div
className={TextControlBase.Hint()}
>
{hint}
</div>
</div>
)}
{indicator && (
<div
className={TextControlBase.IndicatorWrapper(textInputBaseArgs)}
>
{indicator}
</div>
)}
</div>
);
}
);

TextInput.displayName = 'TextInput';

+ 3
- 0
packages/web/categories/freeform/react/src/index.ts View File

@@ -0,0 +1,3 @@
export * from './components/MultilineTextInput';
export * from './components/MaskedTextInput';
export * from './components/TextInput';

+ 11
- 0
packages/web/categories/freeform/react/src/web-freeform-react.test.ts View File

@@ -0,0 +1,11 @@
import * as WebFreeformReact from '.';

describe('web-freeform-react', () => {
it.each([
'MaskedTextInput',
'MultilineTextInput',
'TextInput',
])('exports %s', (namedExport) => {
expect(WebFreeformReact).toHaveProperty(namedExport);
});
});

+ 10
- 0
packages/web/categories/freeform/react/tsconfig.json View File

@@ -0,0 +1,10 @@
{
"exclude": ["node_modules"],
"extends": "../../../../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"jsx": "react",
"emitDeclarationOnly": true,
"declaration": true
}
}

+ 9
- 0
packages/web/categories/freeform/react/vite.config.ts View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['setupTests.ts'],
},
})

+ 4209
- 0
packages/web/categories/freeform/react/yarn.lock
File diff suppressed because it is too large
View File


+ 81
- 0
packages/web/categories/information/react/package.json View File

@@ -0,0 +1,81 @@
{
"name": "@tesseract-design/web-information-react",
"version": "0.0.0",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=10"
},
"license": "MIT",
"keywords": [
"pridepack"
],
"dependencies": {
"@tesseract-design/web-base-badge": "link:../../../base/badge"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/react-hooks": "^8.0.1",
"@types/node": "^18.0.0",
"@types/react": "^18.0.14",
"eslint": "^8.20.0",
"eslint-config-lxsmnsyc": "^0.4.7",
"jsdom": "^20.0.0",
"pridepack": "2.4.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-test-renderer": "^18.2.0",
"tslib": "^2.4.0",
"typescript": "^4.7.4",
"vitest": "^0.31.0"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"scripts": {
"prepublishOnly": "pridepack clean && pridepack build",
"build": "pridepack build",
"type-check": "pridepack check",
"lint": "pridepack lint",
"clean": "pridepack clean",
"watch": "pridepack watch",
"start": "pridepack start",
"dev": "pridepack dev",
"test": "vitest"
},
"private": true,
"description": "Information components for Tesseract for use in React.",
"repository": {
"url": "",
"type": "git"
},
"homepage": "",
"bugs": {
"url": ""
},
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>",
"publishConfig": {
"access": "restricted"
},
"types": "./dist/types/index.d.ts",
"exports": {
".": {
"development": {
"require": "./dist/cjs/development/index.js",
"import": "./dist/esm/development/index.js"
},
"require": "./dist/cjs/production/index.js",
"import": "./dist/esm/production/index.js",
"types": "./dist/types/index.d.ts"
}
},
"typesVersions": {
"*": {}
},
"main": "./dist/cjs/production/index.js",
"module": "./dist/esm/production/index.js"
}

+ 3
- 0
packages/web/categories/information/react/pridepack.json View File

@@ -0,0 +1,3 @@
{
"target": "es2017"
}

+ 4
- 0
packages/web/categories/information/react/setupTests.ts View File

@@ -0,0 +1,4 @@
import matchers from '@testing-library/jest-dom/matchers';
import '@testing-library/jest-dom';

expect.extend(matchers);

+ 31
- 0
packages/web/categories/information/react/src/components/Badge/Badge.test.tsx View File

@@ -0,0 +1,31 @@
import * as React from 'react';
import {
render,
screen,
} from '@testing-library/react';
import { vi } from 'vitest';
import {
Badge,
} from '.';

vi.mock('@tesseract-design/web-base-badge');

describe('Badge', () => {
it('renders a badge', () => {
render(
<Badge />
);
const badge: HTMLButtonElement = screen.getByTestId('badge');
expect(badge).toBeInTheDocument();
});

it('renders a rounded badge', () => {
render(
<Badge
rounded
/>
);
const badge: HTMLButtonElement = screen.getByTestId('badge');
expect(badge).toBeInTheDocument();
});
});

+ 36
- 0
packages/web/categories/information/react/src/components/Badge/index.tsx View File

@@ -0,0 +1,36 @@
import * as React from 'react';
import * as BadgeBase from '@tesseract-design/web-base-badge';

export type BadgeProps = React.HTMLProps<HTMLSpanElement> & {
rounded?: boolean,
};

export const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
(
{
children,
rounded = false,
},
ref,
) => {
const badgeStyleProps = React.useMemo<BadgeBase.BadgeBaseArgs>(() => ({
rounded,
}), [rounded]);

return (
<strong
ref={ref}
className={BadgeBase.Root(badgeStyleProps)}
data-testid="badge"
>
<span
className={BadgeBase.Content()}
>
{children}
</span>
</strong>
)
}
)

Badge.displayName = 'Badge';

+ 1
- 0
packages/web/categories/information/react/src/index.ts View File

@@ -0,0 +1 @@
export * from './components/Badge';

+ 9
- 0
packages/web/categories/information/react/src/web-information-react.test.ts View File

@@ -0,0 +1,9 @@
import * as WebInformationReact from '.';

describe('web-information-react', () => {
it.each([
'Badge',
])('exports %s', (namedExport) => {
expect(WebInformationReact).toHaveProperty(namedExport);
});
});

+ 10
- 0
packages/web/categories/information/react/tsconfig.json View File

@@ -0,0 +1,10 @@
{
"exclude": ["node_modules"],
"extends": "../../../../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"jsx": "react",
"emitDeclarationOnly": true,
"declaration": true
}
}

+ 9
- 0
packages/web/categories/information/react/vite.config.ts View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['setupTests.ts'],
},
})

+ 4202
- 0
packages/web/categories/information/react/yarn.lock
File diff suppressed because it is too large
View File


+ 82
- 0
packages/web/categories/navigation/react/package.json View File

@@ -0,0 +1,82 @@
{
"name": "@tesseract-design/web-navigation-react",
"version": "0.0.0",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=10"
},
"license": "MIT",
"keywords": [
"pridepack"
],
"dependencies": {
"@tesseract-design/web-base-button": "link:../../../base/button"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^13.5.0",
"@types/node": "^18.0.0",
"@types/react": "^18.0.14",
"eslint": "^8.20.0",
"eslint-config-lxsmnsyc": "^0.4.7",
"jsdom": "^20.0.0",
"pridepack": "2.4.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-test-renderer": "^18.2.0",
"tslib": "^2.4.0",
"typescript": "^4.7.4",
"vitest": "^0.31.0"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"scripts": {
"prepublishOnly": "pridepack clean && pridepack build",
"build": "pridepack build",
"type-check": "pridepack check",
"lint": "pridepack lint",
"clean": "pridepack clean",
"watch": "pridepack watch",
"start": "pridepack start",
"dev": "pridepack dev",
"test": "vitest"
},
"private": true,
"description": "Navigation components for Tesseract for use in React.",
"repository": {
"url": "",
"type": "git"
},
"homepage": "",
"bugs": {
"url": ""
},
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>",
"publishConfig": {
"access": "restricted"
},
"types": "./dist/types/index.d.ts",
"exports": {
".": {
"development": {
"require": "./dist/cjs/development/index.js",
"import": "./dist/esm/development/index.js"
},
"require": "./dist/cjs/production/index.js",
"import": "./dist/esm/production/index.js",
"types": "./dist/types/index.d.ts"
}
},
"typesVersions": {
"*": {}
},
"main": "./dist/cjs/production/index.js",
"module": "./dist/esm/production/index.js"
}

+ 3
- 0
packages/web/categories/navigation/react/pridepack.json View File

@@ -0,0 +1,3 @@
{
"target": "es2017"
}

+ 4
- 0
packages/web/categories/navigation/react/setupTests.ts View File

@@ -0,0 +1,4 @@
import matchers from '@testing-library/jest-dom/matchers';
import '@testing-library/jest-dom';

expect.extend(matchers);

+ 182
- 0
packages/web/categories/navigation/react/src/components/LinkButton/LinkButton.test.tsx View File

@@ -0,0 +1,182 @@
import * as React from 'react';
import {
render,
screen,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as ButtonBase from '@tesseract-design/web-base-button';
import { vi } from 'vitest';
import {
LinkButton,
} from '.';

vi.mock('@tesseract-design/web-base-button');

describe('LinkButton', () => {
it('renders a link', () => {
render(
<LinkButton
href="http://example.com"
/>
);
const button: HTMLButtonElement = screen.getByRole('link');
expect(button).toBeInTheDocument();
});

it('renders a subtext', () => {
render(
<LinkButton
subtext="subtext"
/>
);
const subtext: HTMLElement = screen.getByTestId('subtext');
expect(subtext).toBeInTheDocument();
});

it('renders a badge', () => {
render(
<LinkButton
badge="badge"
/>
);
const badge: HTMLElement = screen.getByTestId('badge');
expect(badge).toBeInTheDocument();
});

it('renders as a menu item', () => {
render(
<LinkButton
menuItem
/>
);
const menuItemIndicator: HTMLElement = screen.getByTestId('menuItemIndicator');
expect(menuItemIndicator).toBeInTheDocument();
});

it('handles click events', () => {
const onClick = vi.fn().mockImplementationOnce((e) => { e.preventDefault() });
render(
<LinkButton
href="http://example.com"
onClick={onClick}
/>
);
const button: HTMLButtonElement = screen.getByRole('link');
userEvent.click(button);
expect(onClick).toBeCalled();
});

it('renders a compact button', () => {
render(
<LinkButton
compact
/>
);

expect(ButtonBase.Button).toBeCalledWith(expect.objectContaining({
compact: true,
}));

expect(ButtonBase.Label).toBeCalledWith(expect.objectContaining({
compact: true,
}));
});

describe.each(Object.values(ButtonBase.ButtonSize))('on %s size', (size) => {
it('renders button styles', () => {
render(
<LinkButton
size={size}
/>
);

expect(ButtonBase.Button).toBeCalledWith(expect.objectContaining({
size,
}));
});

it('renders badge styles', () => {
render(
<LinkButton
size={size}
badge="badge"
/>
);

expect(ButtonBase.BadgeContainer).toBeCalledWith(expect.objectContaining({
size,
}));
});

it('renders indicator styles', () => {
render(
<LinkButton
size={size}
menuItem
/>
);

expect(ButtonBase.IndicatorWrapper).toBeCalledWith(expect.objectContaining({
size,
}));
});
});

it.each(Object.values(ButtonBase.ButtonVariant))('renders a button with variant %s', (variant) => {
render(
<LinkButton
variant={variant}
/>
);

expect(ButtonBase.Button).toBeCalledWith(expect.objectContaining({
variant,
}));
});

it('renders a bordered button', () => {
render(
<LinkButton
border
/>
);

expect(ButtonBase.Border).toBeCalledWith(expect.objectContaining({
border: true,
}));
});

it('renders a block button', () => {
render(
<LinkButton
block
/>
);

expect(ButtonBase.Border).toBeCalledWith(expect.objectContaining({
block: true,
}));
});

it('renders children', () => {
render(
<LinkButton>
Foo
</LinkButton>
);

const children: HTMLElement = screen.getByTestId('children');
expect(children).toHaveTextContent('Foo');
});

it('renders a disabled link', () => {
render(
<LinkButton
href="http://example.com"
disabled
/>
);
const button = screen.queryByRole('link');
expect(button).toBeNull();
});
});

+ 183
- 0
packages/web/categories/navigation/react/src/components/LinkButton/index.tsx View File

@@ -0,0 +1,183 @@
import * as React from 'react';
import * as ButtonBase from '@tesseract-design/web-base-button';

type LinkButtonElement = HTMLAnchorElement | HTMLSpanElement;

export type LinkButtonProps = Omit<React.HTMLProps<LinkButtonElement>, 'size' | 'style'> & {
/**
* Size of the component.
*/
size?: ButtonBase.ButtonSize,
/**
* Variant of the component.
*/
variant?: ButtonBase.ButtonVariant,
/**
* Should the component display a border?
*/
border?: boolean,
/**
* Should the component occupy the whole width of its parent?
*/
block?: boolean,
/**
* Does the component need to conserve space?
*/
compact?: boolean,
/**
* Complementary content of the component.
*/
subtext?: React.ReactNode,
/**
* Short complementary content displayed at the edge of the component.
*/
badge?: React.ReactNode,
/**
* Is this component part of a menu?
*/
menuItem?: boolean,
};

/**
* Component for performing an action upon activation (e.g. when clicked).
*
* This component functions as a hyperlink.
*/
export const LinkButton = React.forwardRef<LinkButtonElement, LinkButtonProps>(
(
{
size = ButtonBase.ButtonSize.MEDIUM,
variant = ButtonBase.ButtonVariant.OUTLINE,
border = false,
children,
block = false,
disabled = false,
onClick,
href,
target,
rel,
compact = false,
subtext,
badge,
menuItem = false,
className: _className,
as: _as,
...etcProps
}: LinkButtonProps,
ref,
) => {
const styleProps = React.useMemo<ButtonBase.ButtonBaseArgs>(() => ({
size,
block,
variant,
border,
disabled,
compact,
menuItem,
}), [size, block, variant, border, disabled, compact, menuItem]);

const commonChildren = (
<>
<span
className={ButtonBase.Border(styleProps)}
/>
<span
className={ButtonBase.Label(styleProps)}
>
<span
className={ButtonBase.MainText()}
data-testid="children"
>
<span
className={ButtonBase.OverflowText()}
>
{children}
</span>
</span>
{
subtext
&& (
<>
{' '}
<span
className={ButtonBase.Subtext()}
data-testid="subtext"
>
<span
className={ButtonBase.OverflowText()}
>
{subtext}
</span>
</span>
</>
)
}
</span>
{
badge
&& (
<>
{' '}
<span
className={ButtonBase.BadgeContainer(styleProps)}
data-testid="badge"
>
{badge}
</span>
</>
)
}
{
menuItem
&& (
<>
{' '}
<span
className={ButtonBase.IndicatorWrapper(styleProps)}
data-testid="menuItemIndicator"
>
<svg
className={ButtonBase.Indicator()}
viewBox="0 0 24 24"
role="presentation"
>
<polyline points="9 18 15 12 9 6"/>
</svg>
</span>
</>
)
}
</>
);

if (disabled) {
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
<span
{...etcProps}
className={ButtonBase.Button(styleProps)}
ref={ref as React.ForwardedRef<HTMLSpanElement>}
onClick={undefined}
>
{commonChildren}
</span>
);
}

return (
<a
{...etcProps}
className={ButtonBase.Button(styleProps)}
ref={ref as React.ForwardedRef<HTMLAnchorElement>}
href={href}
target={target}
rel={rel}
onClick={onClick}
>
{commonChildren}
</a>
);
},
);

LinkButton.displayName = 'LinkButton';

+ 1
- 0
packages/web/categories/navigation/react/src/index.ts View File

@@ -0,0 +1 @@
export * from './components/LinkButton';

+ 9
- 0
packages/web/categories/navigation/react/src/web-navigation-react.test.ts View File

@@ -0,0 +1,9 @@
import * as WebNavigationReact from '.';

describe('web-navigation-react', () => {
it.each([
'LinkButton',
])('exports %s', (namedExport) => {
expect(WebNavigationReact).toHaveProperty(namedExport);
});
});

+ 10
- 0
packages/web/categories/navigation/react/tsconfig.json View File

@@ -0,0 +1,10 @@
{
"exclude": ["node_modules"],
"extends": "../../../../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"jsx": "react",
"emitDeclarationOnly": true,
"declaration": true
}
}

+ 9
- 0
packages/web/categories/navigation/react/vite.config.ts View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['setupTests.ts'],
},
})

+ 4209
- 0
packages/web/categories/navigation/react/yarn.lock
File diff suppressed because it is too large
View File


+ 86
- 0
packages/web/categories/option/react/package.json View File

@@ -0,0 +1,86 @@
{
"name": "@tesseract-design/web-option-react",
"version": "0.0.0",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=10"
},
"license": "MIT",
"keywords": [
"pridepack"
],
"dependencies": {
"@tesseract-design/web-base-badge": "link:../../../base/badge",
"@tesseract-design/web-base-button": "link:../../../base/button",
"@tesseract-design/web-base-checkcontrol": "link:../../../base/checkcontrol",
"@tesseract-design/web-base-selectcontrol": "link:../../../base/selectcontrol",
"@tesseract-design/web-base-textcontrol": "link:../../../base/textcontrol"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^13.5.0",
"@types/node": "^18.0.0",
"@types/react": "^18.0.14",
"eslint": "^8.20.0",
"eslint-config-lxsmnsyc": "^0.4.7",
"jsdom": "^20.0.0",
"pridepack": "2.4.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-test-renderer": "^18.2.0",
"tslib": "^2.4.0",
"typescript": "^4.7.4",
"vitest": "^0.31.0"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"scripts": {
"prepublishOnly": "pridepack clean && pridepack build",
"build": "pridepack build",
"type-check": "pridepack check",
"lint": "pridepack lint",
"clean": "pridepack clean",
"watch": "pridepack watch",
"start": "pridepack start",
"dev": "pridepack dev",
"test": "vitest"
},
"private": true,
"description": "Option components for Tesseract for use in React.",
"repository": {
"url": "",
"type": "git"
},
"homepage": "",
"bugs": {
"url": ""
},
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>",
"publishConfig": {
"access": "restricted"
},
"types": "./dist/types/index.d.ts",
"exports": {
".": {
"development": {
"require": "./dist/cjs/development/index.js",
"import": "./dist/esm/development/index.js"
},
"require": "./dist/cjs/production/index.js",
"import": "./dist/esm/production/index.js",
"types": "./dist/types/index.d.ts"
}
},
"typesVersions": {
"*": {}
},
"main": "./dist/cjs/production/index.js",
"module": "./dist/esm/production/index.js"
}

+ 3
- 0
packages/web/categories/option/react/pridepack.json View File

@@ -0,0 +1,3 @@
{
"target": "es2017"
}

+ 4
- 0
packages/web/categories/option/react/setupTests.ts View File

@@ -0,0 +1,4 @@
import matchers from '@testing-library/jest-dom/matchers';
import '@testing-library/jest-dom';

expect.extend(matchers);

+ 299
- 0
packages/web/categories/option/react/src/components/DropdownSelect/DropdownSelect.test.tsx View File

@@ -0,0 +1,299 @@
import * as React from 'react';
import {
render,
screen
} from '@testing-library/react';
import * as TextControlBase from '@tesseract-design/web-base-textcontrol';
import userEvent from '@testing-library/user-event';
import { vi } from 'vitest';
import {
DropdownSelect
} from '.';

vi.mock('@tesseract-design/web-base-textcontrol');

describe('DropdownSelect', () => {
it('renders a combobox', () => {
render(<DropdownSelect />);
const combobox = screen.getByRole('combobox');
expect(combobox).toBeInTheDocument();
});


it('renders a border', () => {
render(
<DropdownSelect
border
/>
);
const border = screen.getByTestId('border');
expect(border).toBeInTheDocument();
});

it('renders a label', () => {
render(
<DropdownSelect
label="foo"
/>
);
const combobox = screen.getByLabelText('foo');
expect(combobox).toBeInTheDocument();
const label = screen.getByTestId('label');
expect(label).toHaveTextContent('foo');
});

it('renders a hidden label', () => {
render(
<DropdownSelect
label="foo"
hiddenLabel
/>
);
const combobox = screen.getByLabelText('foo');
expect(combobox).toBeInTheDocument();
const label = screen.queryByTestId('label');
expect(label).toBeNull();
});

it('renders a hint', () => {
render(
<DropdownSelect
hint="foo"
/>
);
const hint = screen.getByTestId('hint');
expect(hint).toBeInTheDocument();
});

it('does not render invalid options', () => {
render(
<DropdownSelect
options={[
{
label: 'foo',
},
{
label: 'bar',
}
]}
/>
);
const combobox = screen.getByRole('combobox');
expect(combobox.children).toHaveLength(0);
});

it('renders valid options', () => {
render(
<DropdownSelect
options={[
{
label: 'foo',
value: 'foo',
},
{
label: 'bar',
value: 'bar',
}
]}
/>
);
const combobox = screen.getByRole('combobox');
expect(combobox.children).toHaveLength(2);
});

it('renders shallow option groups', () => {
render(
<DropdownSelect
options={[
{
label: 'foo',
children: [
{
label: 'baz',
value: 'baz',
},
],
},
{
label: 'bar',
children: [
{
label: 'quux',
value: 'quux',
},
{
label: 'quuux',
value: 'quuux',
},
],
}
]}
/>
);
const combobox = screen.getByRole('combobox');
expect(combobox.children).toHaveLength(2);
expect(combobox.children[0].children).toHaveLength(1);
expect(combobox.children[1].children).toHaveLength(2);
});

it('renders deep option groups', () => {
render(
<DropdownSelect
options={[
{
label: 'foo',
children: [
{
label: 'baz',
children: [
{
label: 'quuuux',
value: 'quuuux',
},
{
label: 'quuuuux',
value: 'quuuuux',
},
{
label: 'quuuuuux',
value: 'quuuuuux',
},
],
},
],
},
{
label: 'bar',
children: [
{
label: 'quux',
value: 'quux',
},
{
label: 'quuux',
value: 'quuux',
},
],
}
]}
/>
);
const combobox = screen.getByRole('combobox');
expect(combobox.children).toHaveLength(2);
expect(combobox.children[0].children).toHaveLength(4);
expect(combobox.children[1].children).toHaveLength(2);
});

describe.each(Object.values(TextControlBase.TextControlSize))('on %s size', (size) => {
it('renders input styles', () => {
render(
<DropdownSelect
size={size}
/>
);

expect(TextControlBase.Input).toBeCalledWith(expect.objectContaining({
size,
}));
});

it('renders hint styles', () => {
render(
<DropdownSelect
size={size}
hint="hint"
/>
);

expect(TextControlBase.HintWrapper).toBeCalledWith(expect.objectContaining({
size,
}));
});

it('renders indicator styles', () => {
render(
<DropdownSelect
size={size}
/>
);

expect(TextControlBase.IndicatorWrapper).toBeCalledWith(expect.objectContaining({
size,
}));
});
});

it('renders a block input', () => {
render(
<DropdownSelect
block
/>
);

expect(TextControlBase.Root).toBeCalledWith(expect.objectContaining({
block: true,
}));
});

describe.each(Object.values(TextControlBase.TextControlStyle))('on %s style', (style) => {
it('renders input styles', () => {
render(
<DropdownSelect
style={style}
/>
);

expect(TextControlBase.Input).toBeCalledWith(expect.objectContaining({
style,
}));
});

it('renders hint styles', () => {
render(
<DropdownSelect
style={style}
hint="hint"
/>
);

expect(TextControlBase.HintWrapper).toBeCalledWith(expect.objectContaining({
style,
}));
});

it('renders indicator styles', () => {
render(
<DropdownSelect
style={style}
/>
);

expect(TextControlBase.IndicatorWrapper).toBeCalledWith(expect.objectContaining({
style,
}));
});
});

it('handles change events', () => {
const onChange = vi.fn().mockImplementationOnce((e) => { e.preventDefault(); })
render(
<DropdownSelect
onChange={onChange}
options={[
{
label: 'foo',
value: 'foo',
},
{
label: 'bar',
value: 'bar',
}
]}
/>
);
const combobox: HTMLSelectElement = screen.getByRole('combobox');
const [, secondOption] = screen.getAllByRole('option');
userEvent.selectOptions(combobox, secondOption);
expect(onChange).toBeCalled();
});
});

+ 199
- 0
packages/web/categories/option/react/src/components/DropdownSelect/index.tsx View File

@@ -0,0 +1,199 @@
import * as React from 'react';
import * as TextControlBase from '@tesseract-design/web-base-textcontrol';
import * as SelectControlBase from '@tesseract-design/web-base-selectcontrol';

type RenderOptionsProps = {
options: SelectControlBase.SelectOption[],
optionComponent?: React.ElementType,
optgroupComponent?: React.ElementType,
level?: number,
}

const RenderOptions: React.VFC<RenderOptionsProps> = ({
options,
optionComponent: Option = 'option',
optgroupComponent: Optgroup = 'optgroup',
level = 0,
}: RenderOptionsProps) => (
<>
{
options.map((o) => {
if (typeof o.value !== 'undefined') {
return (
<Option
key={`${o.label}:${o.value.toString()}`}
value={o.value}
>
{o.label}
</Option>
);
}

if (typeof o.children !== 'undefined') {
if (level === 0) {
return (
<Optgroup
key={o.label}
label={o.label}
>
<RenderOptions
options={o.children}
optionComponent={Option}
optgroupComponent={Optgroup}
level={level + 1}
/>
</Optgroup>
);
}
return (
<React.Fragment
key={o.label}
>
<Option
disabled
>
{o.label}
</Option>
<RenderOptions
options={o.children}
optionComponent={Option}
optgroupComponent={Optgroup}
level={level + 1}
/>
</React.Fragment>
);
}

return null;
})
}
</>
);

export type DropdownSelectProps = Omit<React.HTMLProps<HTMLSelectElement>, 'size' | 'style' | 'children'> & {
/**
* Short textual description indicating the nature of the component's value.
*/
label?: React.ReactNode,
/**
* Short textual description as guidelines for valid input values.
*/
hint?: React.ReactNode,
/**
* Size of the component.
*/
size?: TextControlBase.TextControlSize,
/**
* Should the component display a border?
*/
border?: boolean,
/**
* Should the component occupy the whole width of its parent?
*/
block?: boolean,
/**
* Style of the component.
*/
style?: TextControlBase.TextControlStyle,
/**
* Is the label hidden?
*/
hiddenLabel?: boolean,
/**
* Options available for the component's values.
*/
options?: SelectControlBase.SelectOption[],
}

/**
* Component for inputting textual values.
*
* This component supports multiline input and adjusts its layout accordingly.
*/
export const DropdownSelect = React.forwardRef<HTMLSelectElement, DropdownSelectProps>(
(
{
label = '',
hint = '',
size = TextControlBase.TextControlSize.MEDIUM,
border = false,
block = false,
style = TextControlBase.TextControlStyle.DEFAULT,
hiddenLabel = false,
multiple: _multiple,
className: _className,
placeholder: _placeholder,
as: _as,
options = [],
...etcProps
}: DropdownSelectProps,
ref,
) => {
const styleArgs = React.useMemo<TextControlBase.TextControlBaseArgs>(() => ({
block,
border,
size,
indicator: true,
style,
resizable: true,
predefinedValues: true,
}), [block, border, size, style]);

return (
<div
className={TextControlBase.Root(styleArgs)}
>
<select
{...etcProps}
className={TextControlBase.Input(styleArgs)}
ref={ref}
aria-label={label}
>
<RenderOptions
options={options}
/>
</select>
{border && (
<span
data-testid="border"
/>
)}
{label && !hiddenLabel && (
<div
className={TextControlBase.LabelWrapper(styleArgs)}
data-testid="label"
>
{label}
</div>
)}
{hint && (
<div
className={TextControlBase.HintWrapper(styleArgs)}
data-testid="hint"
>
<div
className={TextControlBase.Hint()}
>
{hint}
</div>
</div>
)}
<div
className={TextControlBase.IndicatorWrapper(styleArgs)}
>
<svg
className={TextControlBase.Indicator()}
viewBox="0 0 24 24"
role="presentation"
>
<polyline
points="6 9 12 15 18 9"
/>
</svg>
</div>
</div>
);
}
);

DropdownSelect.displayName = 'DropdownSelect';

+ 196
- 0
packages/web/categories/option/react/src/components/MenuSelect/index.tsx View File

@@ -0,0 +1,196 @@
import * as React from 'react';
import * as TextControlBase from '@tesseract-design/web-base-textcontrol';
import * as SelectControlBase from '@tesseract-design/web-base-selectcontrol';

type RenderOptionsProps = {
options: SelectControlBase.SelectOption[],
optionComponent?: React.ElementType,
optgroupComponent?: React.ElementType,
level?: number,
}

const RenderOptions: React.VFC<RenderOptionsProps> = ({
options,
optionComponent: Option = 'option',
optgroupComponent: Optgroup = 'optgroup',
level = 0,
}: RenderOptionsProps) => (
<>
{
options.map((o) => {
if (typeof o.value !== 'undefined') {
return (
<Option
key={`${o.label}:${o.value.toString()}`}
value={o.value}
>
{o.label}
</Option>
);
}

if (typeof o.children !== 'undefined') {
if (level === 0) {
return (
<Optgroup
key={o.label}
label={o.label}
>
<RenderOptions
options={o.children}
optionComponent={Option}
optgroupComponent={Optgroup}
level={level + 1}
/>
</Optgroup>
);
}
return (
<React.Fragment
key={o.label}
>
<Option
disabled
>
{o.label}
</Option>
<RenderOptions
options={o.children}
optionComponent={Option}
optgroupComponent={Optgroup}
level={level + 1}
/>
</React.Fragment>
);
}

return null;
})
}
</>
);

export type MenuSelectProps = Omit<React.HTMLProps<HTMLSelectElement>, 'size' | 'style'> & {
/**
* Short textual description indicating the nature of the component's value.
*/
label?: React.ReactNode,
/**
* Short textual description as guidelines for valid input values.
*/
hint?: React.ReactNode,
/**
* Size of the component.
*/
size?: TextControlBase.TextControlSize,
/**
* Additional description, usually graphical, indicating the nature of the component's value.
*/
indicator?: React.ReactNode,
/**
* Should the component display a border?
*/
border?: boolean,
/**
* Should the component occupy the whole width of its parent?
*/
block?: boolean,
/**
* Style of the component.
*/
style?: TextControlBase.TextControlStyle,
/**
* Is the label hidden?
*/
hiddenLabel?: boolean,
/**
* Options available for the component's values.
*/
options?: SelectControlBase.SelectOption[],
}

export const MenuSelect = React.forwardRef<HTMLSelectElement, MenuSelectProps>(({
label = '',
hint = '',
indicator = null,
size = TextControlBase.TextControlSize.MEDIUM,
border = false,
block = false,
style = TextControlBase.TextControlStyle.DEFAULT,
hiddenLabel = false,
options = [],
className: _className,
placeholder: _placeholder,
as: _as,
...etcProps
}: MenuSelectProps, ref) => {
const styleArgs = React.useMemo<TextControlBase.TextControlBaseArgs>(() => ({
block,
border,
size,
indicator: true,
style,
resizable: true,
predefinedValues: true,
}), [block, border, size, style]);

return (
<div
className={TextControlBase.Root(styleArgs)}
>
<select
{...etcProps}
className={TextControlBase.Input(styleArgs)}
ref={ref}
aria-label={label}
style={{
height: TextControlBase.MIN_HEIGHTS[size],
}}
size={2}
data-testid="input"
>
<RenderOptions
options={options}
/>
</select>
{
border && (
<span
data-testid="border"
/>
)
}
{
label && !hiddenLabel && (
<div
data-testid="label"
className={TextControlBase.LabelWrapper(styleArgs)}
>
{label}
</div>
)
}
{hint && (
<div
className={TextControlBase.HintWrapper(styleArgs)}
data-testid="hint"
>
<div
className={TextControlBase.Hint()}
>
{hint}
</div>
</div>
)}
{indicator && (
<div
className={TextControlBase.IndicatorWrapper(styleArgs)}
>
{indicator}
</div>
)}
</div>
);
});

MenuSelect.displayName = 'MenuSelect';

+ 175
- 0
packages/web/categories/option/react/src/components/RadioButton/RadioButton.test.tsx View File

@@ -0,0 +1,175 @@
import * as React from 'react';
import {
render,
screen
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as ButtonBase from '@tesseract-design/web-base-button';
import * as CheckControlBase from '@tesseract-design/web-base-checkcontrol';
import { vi } from 'vitest';
import { RadioButton } from '.';

vi.mock('@tesseract-design/web-base-button');
vi.mock('@tesseract-design/web-base-checkcontrol');

describe('RadioButton', () => {
it('renders a radio button', () => {
render(
<RadioButton />
);
const checkbox = screen.getByRole('radio');
expect(checkbox).toBeInTheDocument();
});

it('renders a subtext', () => {
render(
<RadioButton
subtext="subtext"
/>
);
const subtext: HTMLElement = screen.getByTestId('subtext');
expect(subtext).toBeInTheDocument();
});

it('renders a badge', () => {
render(
<RadioButton
badge="badge"
/>
);
const badge: HTMLElement = screen.getByTestId('badge');
expect(badge).toBeInTheDocument();
});

it('handles click events', () => {
const onClick = vi.fn().mockImplementationOnce((e) => { e.preventDefault(); });
render(
<RadioButton
onClick={onClick}
/>
);
const button: HTMLInputElement = screen.getByRole('radio');
userEvent.click(button);
expect(onClick).toBeCalled();
});

it('renders a compact button', () => {
render(
<RadioButton
compact
/>
);

expect(ButtonBase.Button).toBeCalledWith(expect.objectContaining({
compact: true,
}));

expect(ButtonBase.Label).toBeCalledWith(expect.objectContaining({
compact: true,
}));
});

describe.each(Object.values(ButtonBase.ButtonSize))('on %s size', (size) => {
it('renders button styles', () => {
render(
<RadioButton
size={size}
/>
);

expect(ButtonBase.Button).toBeCalledWith(expect.objectContaining({
size,
}));
});

it('renders badge styles', () => {
render(
<RadioButton
size={size}
badge="badge"
/>
);

expect(ButtonBase.BadgeContainer).toBeCalledWith(expect.objectContaining({
size,
}));
});
});

it.each(Object.values(ButtonBase.ButtonVariant))('renders a button with variant %s', (variant) => {
render(
<RadioButton
variant={variant}
/>
);

expect(ButtonBase.Button).toBeCalledWith(expect.objectContaining({
variant,
}));
});

it('renders a bordered button', () => {
render(
<RadioButton
border
/>
);

expect(ButtonBase.Border).toBeCalledWith(expect.objectContaining({
border: true,
}));
});

it('renders a block button', () => {
render(
<RadioButton
block
/>
);

expect(ButtonBase.Button).toBeCalledWith(expect.objectContaining({
block: true,
}));

expect(ButtonBase.Border).toBeCalledWith(expect.objectContaining({
block: true,
}));

expect(CheckControlBase.ClickAreaWrapper).toBeCalledWith(expect.objectContaining({
block: true,
}));
});

it('renders children', () => {
render(
<RadioButton>
Foo
</RadioButton>
);

const children: HTMLElement = screen.getByTestId('children');
expect(children).toHaveTextContent('Foo');
});

it('renders a disabled button', () => {
render(
<RadioButton
disabled
/>
);
const radio: HTMLButtonElement = screen.getByRole('radio');
expect(radio).toBeDisabled();
});

it('handles change events', () => {
const onChange = vi.fn().mockImplementationOnce((e) => { e.preventDefault(); })
render(
<RadioButton
onChange={onChange}
/>
);
const radio: HTMLInputElement = screen.getByRole('radio');
userEvent.click(radio);
expect(onChange).toBeCalled();
});
});

+ 152
- 0
packages/web/categories/option/react/src/components/RadioButton/index.tsx View File

@@ -0,0 +1,152 @@
import * as React from 'react';
import * as ButtonBase from '@tesseract-design/web-base-button';
import * as CheckControlBase from '@tesseract-design/web-base-checkcontrol';

export type RadioButtonProps = Omit<React.HTMLProps<HTMLInputElement>, 'size' | 'type' | 'style'> & {
/**
* Size of the component.
*/
size?: ButtonBase.ButtonSize,
/**
* Variant of the component.
*/
variant?: ButtonBase.ButtonVariant,
/**
* Should the component display a border?
*/
border?: boolean,
/**
* Should the component occupy the whole width of its parent?
*/
block?: boolean,
/**
* Does the component need to conserve space?
*/
compact?: boolean,
/**
* Complementary content of the component.
*/
subtext?: React.ReactNode,
/**
* Short complementary content displayed at the edge of the component.
*/
badge?: React.ReactNode,
}

/**
* Component for performing an action upon activation (e.g. when clicked).
*
* This component functions as a regular button.
*/
export const RadioButton = React.forwardRef<HTMLInputElement, RadioButtonProps>(
(
{
size = ButtonBase.ButtonSize.MEDIUM,
variant = ButtonBase.ButtonVariant.OUTLINE,
border = false,
children,
block = false,
disabled = false,
compact = false,
subtext,
badge,
className: _className,
as: _as,
...etcProps
}: RadioButtonProps,
ref,
) => {
const styleProps = React.useMemo<ButtonBase.ButtonBaseArgs & CheckControlBase.CheckControlBaseArgs>(() => ({
size,
block,
variant,
border,
compact,
menuItem: false,
disabled,
appearance: CheckControlBase.CheckControlAppearance.BUTTON,
type: CheckControlBase.CheckControlType.RADIO,
uncheckedLabel: false,
}), [size, block, variant, border, compact, disabled]);

return (
<div
className={CheckControlBase.ClickAreaWrapper(styleProps)}
>
<label
className={CheckControlBase.ClickArea()}
>
<input
{...etcProps}
disabled={disabled}
type="radio"
ref={ref}
className={CheckControlBase.CheckStateContainer(styleProps)}
/>
<span
className={ButtonBase.Button(styleProps)}
>
<span
className={ButtonBase.Border(styleProps)}
/>
<span
className={CheckControlBase.CheckIndicatorArea(styleProps)}
>
<span
className={CheckControlBase.CheckIndicatorWrapper(styleProps)}
/>
</span>
<span
className={ButtonBase.Label(styleProps)}
>
<span
className={ButtonBase.MainText()}
data-testid="children"
>
<span
className={ButtonBase.OverflowText()}
>
{children}
</span>
</span>
{
subtext
&& (
<>
{' '}
<span
className={ButtonBase.Subtext()}
data-testid="subtext"
>
<span
className={ButtonBase.OverflowText()}
>
{subtext}
</span>
</span>
</>
)
}
</span>
{
badge
&& (
<>
{' '}
<span
className={ButtonBase.BadgeContainer(styleProps)}
data-testid="badge"
>
{badge}
</span>
</>
)
}
</span>
</label>
</div>
);
},
);

RadioButton.displayName = 'ActionButton';

+ 80
- 0
packages/web/categories/option/react/src/components/RadioTickBox/RadioTickBox.test.tsx View File

@@ -0,0 +1,80 @@
import * as React from 'react';
import {
render,
screen
} from '@testing-library/react';
import '@testing-library/jest-dom';
import userEvent from '@testing-library/user-event';
import * as CheckControlBase from '@tesseract-design/web-base-checkcontrol';
import { vi } from 'vitest';
import { RadioTickBox } from '.';

vi.mock('@tesseract-design/web-base-checkcontrol');

describe('RadioTickBox', () => {
it('renders a radio button', () => {
render(
<RadioTickBox />
);
const checkbox = screen.getByRole('radio');
expect(checkbox).toBeInTheDocument();
});

it('renders a compact tick box', () => {
render(
<RadioTickBox
compact
/>
);

expect(CheckControlBase.CheckIndicatorArea).toBeCalledWith(expect.objectContaining({
compact: true,
}));
});

it('renders a block tick box', () => {
render(
<RadioTickBox
block
/>
);

expect(CheckControlBase.ClickAreaWrapper).toBeCalledWith(expect.objectContaining({
block: true,
}));
});

it('renders a subtext', () => {
render(
<RadioTickBox
subtext="subtext"
/>
);
const subtext: HTMLElement = screen.getByTestId('subtext');
expect(subtext).toBeInTheDocument();
});

it('handles click events', () => {
const onClick = vi.fn().mockImplementationOnce((e) => { e.preventDefault(); })
render(
<RadioTickBox
onClick={onClick}
/>
);
const radio: HTMLInputElement = screen.getByRole('radio');
userEvent.click(radio);
expect(onClick).toBeCalled();
});

it('handles change events', () => {
const onChange = vi.fn().mockImplementationOnce((e) => { e.preventDefault(); })
render(
<RadioTickBox
onChange={onChange}
/>
);
const radio: HTMLInputElement = screen.getByRole('radio');
userEvent.click(radio);
expect(onChange).toBeCalled();
});
});

+ 91
- 0
packages/web/categories/option/react/src/components/RadioTickBox/index.tsx View File

@@ -0,0 +1,91 @@
import * as React from 'react';
import * as CheckControlBase from '@tesseract-design/web-base-checkcontrol';

export type RadioTickBoxProps = Omit<React.HTMLProps<HTMLInputElement>, 'size' | 'type' | 'style'> & {
/**
* Should the component occupy the whole width of its parent?
*/
block?: boolean,
/**
* Does the component need to conserve space?
*/
compact?: boolean,
/**
* Complementary content of the component.
*/
subtext?: React.ReactNode,
}

/**
* Component for performing an action upon activation (e.g. when clicked).
*
* This component functions as a regular button.
*/
export const RadioTickBox = React.forwardRef<HTMLInputElement, RadioTickBoxProps>(
(
{
children,
block = false,
compact = false,
subtext,
className: _className,
as: _as,
...etcProps
}: RadioTickBoxProps,
ref,
) => {
const styleProps = React.useMemo<CheckControlBase.CheckControlBaseArgs>(() => ({
block,
compact,
appearance: CheckControlBase.CheckControlAppearance.TICK_BOX,
type: CheckControlBase.CheckControlType.RADIO,
uncheckedLabel: false,
}), [block, compact]);

return (
<div
className={CheckControlBase.ClickAreaWrapper(styleProps)}
>
<label
className={CheckControlBase.ClickArea()}
>
<input
{...etcProps}
type="radio"
ref={ref}
className={CheckControlBase.CheckStateContainer(styleProps)}
/>
<span>
<span />
<span
className={CheckControlBase.CheckIndicatorArea(styleProps)}
>
<span
className={CheckControlBase.CheckIndicatorWrapper(styleProps)}
/>
</span>
<span>
{children}
{
subtext
&& (
<>
<br />
<span
className={CheckControlBase.Subtext()}
data-testid="subtext"
>
{subtext}
</span>
</>
)
}
</span>
</span>
</label>
</div>
);
},
);

RadioTickBox.displayName = 'ActionButton';

+ 327
- 0
packages/web/categories/option/react/src/components/TagInput/index.tsx View File

@@ -0,0 +1,327 @@
import * as React from 'react';
import * as TextControlBase from '@tesseract-design/web-base-textcontrol';
import * as BadgeBase from '@tesseract-design/web-base-badge';

export type TagInputProps = Omit<React.HTMLProps<HTMLInputElement>, 'size' | 'style'> & {
/**
* Short textual description indicating the nature of the component's value.
*/
label?: React.ReactNode,
/**
* Short textual description as guidelines for valid input values.
*/
hint?: React.ReactNode,
/**
* Size of the component.
*/
size?: TextControlBase.TextControlSize,
/**
* Additional description, usually graphical, indicating the nature of the component's value.
*/
indicator?: React.ReactNode,
/**
* Should the component display a border?
*/
border?: boolean,
/**
* Should the component occupy the whole width of its parent?
*/
block?: boolean,
/**
* Style of the component.
*/
style?: TextControlBase.TextControlStyle,
/**
* Is the label hidden?
*/
hiddenLabel?: boolean,
enhanced?: boolean,
separator?: string,
}

export const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(((
{
label = '',
hint = '',
indicator = null,
size = TextControlBase.TextControlSize.MEDIUM,
border = false,
block = false,
style = TextControlBase.TextControlStyle.DEFAULT,
hiddenLabel = false,
onInput,
enhanced = false,
defaultValue = '',
separator = ',',
onFocus,
onBlur,
onSelect,
className: _className,
placeholder: _placeholder,
as: _as,
...etcProps
}: TagInputProps,
forwardedRef,
) => {
const [hydrated, setHydrated] = React.useState(false);
const [focused, setFocused] = React.useState(false);
const [selectionStart, setSelectionStart] = React.useState(0);
const [selectionEnd, setSelectionEnd] = React.useState(0);
const [viewValue, setViewValue] = React.useState<string[]>(() => {
const theDefaultValue = !Array.isArray(defaultValue) ? [defaultValue.toString(), ''] : [...defaultValue, ''];
return theDefaultValue.filter(v => v.length > 0);
});
const defaultRef = React.useRef<HTMLInputElement>(null);
const effectiveRef = forwardedRef ?? defaultRef;

const styleArgs = React.useMemo<TextControlBase.TextControlBaseArgs>(() => ({
block,
border,
size,
indicator: Boolean(indicator),
style,
resizable: true,
predefinedValues: false,
}), [block, border, size, indicator, style]);

const renderEnhanced = React.useMemo(() => enhanced && hydrated, [enhanced, hydrated]);

const handleInput: React.FormEventHandler<HTMLInputElement> = (e) => {
const target = e.target as HTMLInputElement;
setViewValue(target.value.split(separator))

if (onInput) {
onInput(e)
}
}

const handleFocus: React.FocusEventHandler<HTMLInputElement> = (e) => {
setFocused(true);
if (onFocus) {
onFocus(e);
}
}

const handleBlur: React.FocusEventHandler<HTMLInputElement> = (e) => {
setFocused(false);
if (onBlur) {
onBlur(e);
}
}

const handleSelect: React.ReactEventHandler<HTMLInputElement> = (e) => {
const target = e.target as HTMLInputElement;
const newSelectionStart = target.selectionStart ?? 0;
const newSelectionEnd = target.selectionEnd ?? 0;
const newDirection = Math.sign(newSelectionStart - selectionStart) > 0 ? 'forward' : 'backward'
setSelectionStart(newSelectionStart);
setSelectionEnd(newSelectionEnd);

console.log(newDirection);

const lastSeparatorIndex = target.value.lastIndexOf(separator);
const separatorStartRaw = newSelectionStart > lastSeparatorIndex ? newSelectionStart : target.value.slice(0, newSelectionEnd).lastIndexOf(separator) + separator?.length;
const separatorEndRaw = newSelectionEnd > lastSeparatorIndex ? newSelectionEnd : target.value.slice(0, newSelectionEnd).length + target.value.slice(newSelectionEnd).indexOf(separator);

let separatorStart = 0;
let separatorEnd;
if (lastSeparatorIndex > -1) {
separatorStart = separatorStartRaw > -1 ? separatorStartRaw : 0;
}
separatorEnd = separatorEndRaw;
if (newSelectionStart <= target.value.lastIndexOf(separator)) {
if (newSelectionStart === newSelectionEnd && newSelectionStart === separatorStart && newDirection === 'backward') {
target.selectionStart = newSelectionStart - separator?.length;
target.selectionEnd = newSelectionStart - separator?.length;
target.selectionDirection = newDirection;
} else if (newSelectionStart === newSelectionEnd && newSelectionEnd === separatorEnd && newDirection === 'forward') {
target.selectionStart = newSelectionEnd + separator?.length;
target.selectionEnd = newSelectionEnd + separator?.length;
target.selectionDirection = newDirection;
} else {
target.selectionStart = separatorStart;
target.selectionEnd = separatorEnd;
target.selectionDirection = 'backward';
}
}
if (onSelect) {
onSelect(e);
}
}

const focusOnInput: React.MouseEventHandler<HTMLDivElement> = (e) => {
e.preventDefault();
if (typeof effectiveRef === 'function') {
return;
}
if (effectiveRef !== null && effectiveRef.current) {
effectiveRef.current.focus();
}
}

const tags = React.useMemo(() => viewValue.slice(0, -1), [viewValue])
const inputText = React.useMemo(() => viewValue.slice(-1)[0] ?? '', [viewValue])

React.useEffect(() => {
setHydrated(true)
}, []);

return (
<div
className={TextControlBase.Root(styleArgs)}
>
<input
{...etcProps}
className={TextControlBase.Input(styleArgs)}
ref={effectiveRef}
aria-label={label}
style={{
height: TextControlBase.MIN_HEIGHTS[size],
// position: renderEnhanced ? 'absolute' : undefined,
// left: renderEnhanced ? -999999 : undefined,
}}
data-testid="input"
onInput={handleInput}
onFocus={handleFocus}
onBlur={handleBlur}
onSelect={handleSelect}
/>
<div
className={TextControlBase.Input(styleArgs)}
onClick={focusOnInput}
style={{
cursor: 'text',
}}
>
<div
style={{
margin: '-0.125rem',
}}
>
{tags.map(v => (
<div
style={{
padding: '0.125rem',
display: 'inline-block',
}}
key={v}
>
<button
className={BadgeBase.Root({ rounded: false })}
style={{
border: 0,
font: 'inherit',
lineHeight: 0,
paddingTop: 0,
paddingBottom: 0,
color: 'inherit',
backgroundColor: 'transparent',
}}
>
<div
className={BadgeBase.Content()}
>
{v}
{' '}
&times;
</div>
</button>
</div>
))}
{
inputText.lastIndexOf(separator) < 0
&& (
<>
{
inputText.slice(0, selectionStart - tags.join(separator).length - separator?.length)
}
<div
style={{
display: 'inline-block',
verticalAlign: 'middle',
backgroundColor: focused ? 'Highlight' : undefined,
color: focused ? 'HighlightText' : undefined,
height: '1.25em',
minWidth: 1,
}}
>
{
inputText.slice(selectionStart - tags.join(separator).length - separator?.length, selectionEnd - tags.join(separator).length - separator?.length)
}
</div>
{
inputText.slice(selectionEnd - tags.join(separator).length - separator?.length)
}
</>
)
}
{
inputText.lastIndexOf(separator) >= 0
&& (
<>
{
inputText.slice(0, selectionStart - tags.join(separator).length)
}
<div
style={{
display: 'inline-block',
verticalAlign: 'middle',
backgroundColor: focused ? 'Highlight' : undefined,
color: focused ? 'HighlightText' : undefined,
height: '1.25em',
minWidth: 1,
}}
>
{
inputText.slice(selectionStart - tags.join(separator).length - 1, selectionEnd - tags.join(separator).length - 1)
}
</div>
{
inputText.slice(selectionEnd - tags.join(separator).length - 1)
}
</>
)
}
</div>
</div>
{
border && (
<span
data-testid="border"
/>
)
}
{
label && !hiddenLabel && (
<div
data-testid="label"
className={TextControlBase.LabelWrapper(styleArgs)}
>
{label}
</div>
)
}
{hint && (
<div
className={TextControlBase.HintWrapper(styleArgs)}
data-testid="hint"
>
<div
className={TextControlBase.Hint()}
>
{hint}
</div>
</div>
)}
{indicator && (
<div
className={TextControlBase.IndicatorWrapper(styleArgs)}
>
{indicator}
</div>
)}
</div>
);
}));

TagInput.displayName = 'TagInput';

+ 209
- 0
packages/web/categories/option/react/src/components/ToggleButton/ToggleButton.test.tsx View File

@@ -0,0 +1,209 @@
import * as React from 'react';
import {
render,
screen
} from '@testing-library/react';
import '@testing-library/jest-dom';
import userEvent from '@testing-library/user-event';
import * as ButtonBase from '@tesseract-design/web-base-button';
import * as CheckControlBase from '@tesseract-design/web-base-checkcontrol';
import { vi } from 'vitest';
import { ToggleButton } from '.';

vi.mock('@tesseract-design/web-base-button');
vi.mock('@tesseract-design/web-base-checkcontrol');

describe('ToggleButton', () => {
it('renders a checkbox', () => {
render(
<ToggleButton />
);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toBeInTheDocument();
});

it('renders an indeterminate checkbox', () => {
render(
<ToggleButton
indeterminate
/>
);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toHaveProperty('indeterminate', true);
});

it('renders a subtext', () => {
render(
<ToggleButton
subtext="subtext"
/>
);
const subtext: HTMLElement = screen.getByTestId('subtext');
expect(subtext).toBeInTheDocument();
});

it('renders a badge', () => {
render(
<ToggleButton
badge="badge"
/>
);
const badge: HTMLElement = screen.getByTestId('badge');
expect(badge).toBeInTheDocument();
});

describe('on indeterminate', () => {
it('renders an indeterminate checkbox', () => {
render(
<ToggleButton
indeterminate
/>
);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toHaveProperty('indeterminate', true);
});

it('acknowledges passed ref', () => {
const ref = React.createRef<HTMLInputElement>()
render(
<ToggleButton
indeterminate
ref={ref}
/>
);
expect(ref.current).toHaveProperty('indeterminate', true);
});
});

it('handles click events', () => {
const onClick = vi.fn().mockImplementationOnce((e) => { e.preventDefault(); })
render(
<ToggleButton
onClick={onClick}
/>
);
const checkbox: HTMLInputElement = screen.getByRole('checkbox');
userEvent.click(checkbox);
expect(onClick).toBeCalled();
});

it('renders a compact button', () => {
render(
<ToggleButton
compact
/>
);

expect(ButtonBase.Button).toBeCalledWith(expect.objectContaining({
compact: true,
}));

expect(ButtonBase.Label).toBeCalledWith(expect.objectContaining({
compact: true,
}));
});

describe.each(Object.values(ButtonBase.ButtonSize))('on %s size', (size) => {
it('renders button styles', () => {
render(
<ToggleButton
size={size}
/>
);

expect(ButtonBase.Button).toBeCalledWith(expect.objectContaining({
size,
}));
});

it('renders badge styles', () => {
render(
<ToggleButton
size={size}
badge="badge"
/>
);

expect(ButtonBase.BadgeContainer).toBeCalledWith(expect.objectContaining({
size,
}));
});
});

it.each(Object.values(ButtonBase.ButtonVariant))('renders a button with variant %s', (variant) => {
render(
<ToggleButton
variant={variant}
/>
);

expect(ButtonBase.Button).toBeCalledWith(expect.objectContaining({
variant,
}));
});

it('renders a bordered button', () => {
render(
<ToggleButton
border
/>
);

expect(ButtonBase.Border).toBeCalledWith(expect.objectContaining({
border: true,
}));
});

it('renders a block button', () => {
render(
<ToggleButton
block
/>
);

expect(ButtonBase.Button).toBeCalledWith(expect.objectContaining({
block: true,
}));

expect(ButtonBase.Border).toBeCalledWith(expect.objectContaining({
block: true,
}));

expect(CheckControlBase.ClickAreaWrapper).toBeCalledWith(expect.objectContaining({
block: true,
}));
});

it('renders children', () => {
render(
<ToggleButton>
Foo
</ToggleButton>
);

const children: HTMLElement = screen.getByTestId('children');
expect(children).toHaveTextContent('Foo');
});

it('renders a disabled button', () => {
render(
<ToggleButton
disabled
/>
);
const checkbox: HTMLButtonElement = screen.getByRole('checkbox');
expect(checkbox).toBeDisabled();
});

it('handles change events', () => {
const onChange = vi.fn().mockImplementationOnce((e) => { e.preventDefault(); })
render(
<ToggleButton
onChange={onChange}
/>
);
const checkbox: HTMLInputElement = screen.getByRole('checkbox');
userEvent.click(checkbox);
expect(onChange).toBeCalled();
});
});

+ 185
- 0
packages/web/categories/option/react/src/components/ToggleButton/index.tsx View File

@@ -0,0 +1,185 @@
import * as React from 'react';
import * as ButtonBase from '@tesseract-design/web-base-button';
import * as CheckControlBase from '@tesseract-design/web-base-checkcontrol';

export type ToggleButtonProps = Omit<React.HTMLProps<HTMLInputElement>, 'size' | 'type' | 'style'> & {
/**
* Size of the component.
*/
size?: ButtonBase.ButtonSize,
/**
* Variant of the component.
*/
variant?: ButtonBase.ButtonVariant,
/**
* Should the component display a border?
*/
border?: boolean,
/**
* Should the component occupy the whole width of its parent?
*/
block?: boolean,
/**
* Does the component need to conserve space?
*/
compact?: boolean,
/**
* Complementary content of the component.
*/
subtext?: React.ReactNode,
/**
* Short complementary content displayed at the edge of the component.
*/
badge?: React.ReactNode,
/**
* Does the component have indeterminate check state?
*/
indeterminate?: boolean,
}

/**
* Component for performing an action upon activation (e.g. when clicked).
*
* This component functions as a regular button.
*/
export const ToggleButton = React.forwardRef<HTMLInputElement, ToggleButtonProps>(
(
{
size = ButtonBase.ButtonSize.MEDIUM,
variant = ButtonBase.ButtonVariant.OUTLINE,
border = false,
children,
block = false,
disabled = false,
compact = false,
subtext,
badge,
indeterminate = false,
className: _className,
as: _as,
...etcProps
}: ToggleButtonProps,
ref,
) => {
const styleProps = React.useMemo<ButtonBase.ButtonBaseArgs & CheckControlBase.CheckControlBaseArgs>(() => ({
size,
block,
variant,
border,
compact,
menuItem: false,
disabled,
appearance: CheckControlBase.CheckControlAppearance.BUTTON,
type: CheckControlBase.CheckControlType.CHECKBOX,
uncheckedLabel: false,
}), [size, block, variant, border, compact, disabled]);
const defaultRef = React.useRef<HTMLInputElement>(null);
const theRef = (ref ?? defaultRef) as React.MutableRefObject<HTMLInputElement>;

React.useEffect(() => {
if (!(indeterminate && theRef.current)) {
return;
}
theRef.current.indeterminate = indeterminate;
}, [theRef, indeterminate]);

return (
<div
className={CheckControlBase.ClickAreaWrapper(styleProps)}
>
<label
className={CheckControlBase.ClickArea()}
>
<input
{...etcProps}
disabled={disabled}
type="checkbox"
ref={theRef}
className={CheckControlBase.CheckStateContainer(styleProps)}
/>
<span
className={ButtonBase.Button(styleProps)}
>
<span
className={ButtonBase.Border(styleProps)}
/>
<span
className={CheckControlBase.CheckIndicatorArea(styleProps)}
>
<span
className={CheckControlBase.CheckIndicatorWrapper(styleProps)}
>
<svg
className={CheckControlBase.CheckIndicator(styleProps)}
viewBox="0 0 24 24"
role="presentation"
>
<polyline
points="20 6 9 17 4 12"
/>
</svg>
<svg
className={CheckControlBase.CheckIndicator(styleProps)}
viewBox="0 0 24 24"
role="presentation"
>
<polyline
points="20 12 4 12"
/>
</svg>
</span>
</span>
<span
className={ButtonBase.Label(styleProps)}
>
<span
className={ButtonBase.MainText()}
data-testid="children"
>
<span
className={ButtonBase.OverflowText()}
>
{children}
</span>
</span>
{
subtext
&& (
<>
{' '}
<span
className={ButtonBase.Subtext()}
data-testid="subtext"
>
<span
className={ButtonBase.OverflowText()}
>
{subtext}
</span>
</span>
</>
)
}
</span>
{
badge
&& (
<>
{' '}
<span
className={ButtonBase.BadgeContainer(styleProps)}
data-testid="badge"
>
{badge}
</span>
</>
)
}
</span>
</label>
</div>
);
},
);

ToggleButton.displayName = 'ActionButton';

+ 80
- 0
packages/web/categories/option/react/src/components/ToggleSwitch/ToggleSwitch.test.tsx View File

@@ -0,0 +1,80 @@
import * as React from 'react';
import {
render,
screen
} from '@testing-library/react';
import '@testing-library/jest-dom';
import userEvent from '@testing-library/user-event';
import * as CheckControlBase from '@tesseract-design/web-base-checkcontrol';
import { vi } from 'vitest';
import { ToggleSwitch } from '.';

vi.mock('@tesseract-design/web-base-checkcontrol');

describe('ToggleSwitch', () => {
it('renders a checkbox', () => {
render(
<ToggleSwitch />
);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toBeInTheDocument();
});

it('renders a compact switch', () => {
render(
<ToggleSwitch
compact
/>
);

expect(CheckControlBase.CheckIndicatorArea).toBeCalledWith(expect.objectContaining({
compact: true,
}));
});

it('renders a block switch', () => {
render(
<ToggleSwitch
block
/>
);

expect(CheckControlBase.ClickAreaWrapper).toBeCalledWith(expect.objectContaining({
block: true,
}));
});

it('renders a subtext', () => {
render(
<ToggleSwitch
subtext="subtext"
/>
);
const subtext: HTMLElement = screen.getByTestId('subtext');
expect(subtext).toBeInTheDocument();
});

it('handles click events', () => {
const onClick = vi.fn().mockImplementationOnce((e) => { e.preventDefault(); })
render(
<ToggleSwitch
onClick={onClick}
/>
);
const checkbox: HTMLInputElement = screen.getByRole('checkbox');
userEvent.click(checkbox);
expect(onClick).toBeCalled();
});

it('handles change events', () => {
const onChange = vi.fn().mockImplementationOnce((e) => { e.preventDefault(); })
render(
<ToggleSwitch
onChange={onChange}
/>
);
const checkbox: HTMLInputElement = screen.getByRole('checkbox');
userEvent.click(checkbox);
expect(onChange).toBeCalled();
});
});

+ 123
- 0
packages/web/categories/option/react/src/components/ToggleSwitch/index.tsx View File

@@ -0,0 +1,123 @@
import * as React from 'react';
import * as CheckControlBase from '@tesseract-design/web-base-checkcontrol';

export type ToggleSwitchProps = Omit<React.HTMLProps<HTMLInputElement>, 'size' | 'type' | 'style'> & {
/**
* Label of the component when in the unchecked state.
*/
uncheckedLabel?: React.ReactNode,
/**
* Label of the component when in the checked state.
*/
checkedLabel?: React.ReactNode,
/**
* Should the component occupy the whole width of its parent?
*/
block?: boolean,
/**
* Does the component need to conserve space?
*/
compact?: boolean,
/**
* Complementary content of the component.
*/
subtext?: React.ReactNode,
}

/**
* Component for performing an action upon activation (e.g. when clicked).
*
* This component functions as a regular button.
*/
export const ToggleSwitch = React.forwardRef<HTMLInputElement, ToggleSwitchProps>(
(
{
checkedLabel,
uncheckedLabel,
block = false,
compact = false,
subtext,
className: _className,
as: _as,
children: _children,
...etcProps
}: ToggleSwitchProps,
ref,
) => {
const styleProps = React.useMemo<CheckControlBase.CheckControlBaseArgs>(() => ({
block,
compact,
menuItem: false,
appearance: CheckControlBase.CheckControlAppearance.SWITCH,
type: CheckControlBase.CheckControlType.CHECKBOX,
uncheckedLabel: Boolean(uncheckedLabel),
}), [block, compact, uncheckedLabel]);

return (
<div
className={CheckControlBase.ClickAreaWrapper(styleProps)}
>
<label
className={CheckControlBase.ClickArea()}
>
<input
{...etcProps}
type="checkbox"
ref={ref}
className={CheckControlBase.CheckStateContainer(styleProps)}
/>
<span>
<span>
{uncheckedLabel}
</span>
<span
className={CheckControlBase.CheckIndicatorArea(styleProps)}
>
<span
className={CheckControlBase.CheckIndicatorWrapper(styleProps)}
>
<svg
className={CheckControlBase.CheckIndicator(styleProps)}
viewBox="0 0 24 24"
role="presentation"
>
<polyline
points="20 6 9 17 4 12"
/>
</svg>
<svg
className={CheckControlBase.CheckIndicator(styleProps)}
viewBox="0 0 24 24"
role="presentation"
>
<polyline
points="20 12 4 12"
/>
</svg>
</span>
</span>
<span>
{checkedLabel}
{
subtext
&& (
<>
<br />
<span
className={CheckControlBase.Subtext()}
data-testid="subtext"
>
{subtext}
</span>
</>
)
}
</span>
</span>
</label>
</div>
);
},
);

ToggleSwitch.displayName = 'ActionButton';

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

Loading…
Cancel
Save