@@ -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/ |
@@ -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. |
@@ -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" | |||
} |
@@ -0,0 +1,3 @@ | |||
{ | |||
"target": "es2017" | |||
} |
@@ -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; | |||
` | |||
); |
@@ -0,0 +1,9 @@ | |||
{ | |||
"exclude": ["node_modules"], | |||
"extends": "../../../../tsconfig.json", | |||
"compilerOptions": { | |||
"rootDir": "src", | |||
"emitDeclarationOnly": true, | |||
"declaration": true | |||
} | |||
} |
@@ -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" | |||
} |
@@ -0,0 +1,3 @@ | |||
{ | |||
"target": "es2017" | |||
} |
@@ -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); | |||
`, | |||
); |
@@ -0,0 +1,9 @@ | |||
{ | |||
"exclude": ["node_modules"], | |||
"extends": "../../../../tsconfig.json", | |||
"compilerOptions": { | |||
"rootDir": "src", | |||
"emitDeclarationOnly": true, | |||
"declaration": true | |||
} | |||
} |
@@ -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" | |||
} |
@@ -0,0 +1,3 @@ | |||
{ | |||
"target": "es2017" | |||
} |
@@ -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; | |||
` | |||
); |
@@ -0,0 +1,9 @@ | |||
{ | |||
"exclude": ["node_modules"], | |||
"extends": "../../../../tsconfig.json", | |||
"compilerOptions": { | |||
"rootDir": "src", | |||
"emitDeclarationOnly": true, | |||
"declaration": true | |||
} | |||
} |
@@ -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" | |||
} |
@@ -0,0 +1,3 @@ | |||
{ | |||
"target": "es2017" | |||
} |
@@ -0,0 +1,5 @@ | |||
export interface SelectOption { | |||
label: string, | |||
value?: string | number | readonly string[] | |||
children?: SelectOption[] | |||
} |
@@ -0,0 +1,9 @@ | |||
{ | |||
"exclude": ["node_modules"], | |||
"extends": "../../../../tsconfig.json", | |||
"compilerOptions": { | |||
"rootDir": "src", | |||
"emitDeclarationOnly": true, | |||
"declaration": true | |||
} | |||
} |
@@ -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" | |||
} |
@@ -0,0 +1,3 @@ | |||
{ | |||
"target": "es2017" | |||
} |
@@ -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; | |||
`, | |||
); |
@@ -0,0 +1,9 @@ | |||
{ | |||
"exclude": ["node_modules"], | |||
"extends": "../../../../tsconfig.json", | |||
"compilerOptions": { | |||
"rootDir": "src", | |||
"emitDeclarationOnly": true, | |||
"declaration": true | |||
} | |||
} |
@@ -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" | |||
} |
@@ -0,0 +1,3 @@ | |||
{ | |||
"target": "es2017" | |||
} |
@@ -0,0 +1,4 @@ | |||
import matchers from '@testing-library/jest-dom/matchers'; | |||
import '@testing-library/jest-dom'; | |||
expect.extend(matchers); |
@@ -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(); | |||
}); | |||
}); |
@@ -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'; |
@@ -0,0 +1 @@ | |||
export * from './components/ActionButton'; |
@@ -0,0 +1,9 @@ | |||
import * as WebActionReact from '.'; | |||
describe('web-action-react', () => { | |||
it.each([ | |||
'ActionButton', | |||
])('exports %s', (namedExport) => { | |||
expect(WebActionReact).toHaveProperty(namedExport); | |||
}); | |||
}); |
@@ -0,0 +1,11 @@ | |||
{ | |||
"exclude": ["node_modules"], | |||
"extends": "../../../../../tsconfig.json", | |||
"compilerOptions": { | |||
"lib": ["dom"], | |||
"rootDir": "src", | |||
"jsx": "react", | |||
"emitDeclarationOnly": true, | |||
"declaration": true | |||
} | |||
} |
@@ -0,0 +1,9 @@ | |||
import { defineConfig } from 'vitest/config' | |||
export default defineConfig({ | |||
test: { | |||
globals: true, | |||
environment: 'jsdom', | |||
setupFiles: ['setupTests.ts'], | |||
}, | |||
}) |
@@ -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" | |||
} |
@@ -0,0 +1,3 @@ | |||
{ | |||
"target": "es2017" | |||
} |
@@ -0,0 +1,4 @@ | |||
import matchers from '@testing-library/jest-dom/matchers'; | |||
import '@testing-library/jest-dom'; | |||
expect.extend(matchers); |
@@ -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(); | |||
}); | |||
}); |
@@ -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'; |
@@ -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(); | |||
}); | |||
}); |
@@ -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'; |
@@ -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(); | |||
}); | |||
}); |
@@ -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'; |
@@ -0,0 +1,3 @@ | |||
export * from './components/EmailAddressInput'; | |||
export * from './components/PhoneNumberInput'; | |||
export * from './components/UrlInput'; |
@@ -0,0 +1,11 @@ | |||
import * as WebFormattedReact from '.'; | |||
describe('web-formatted-react', () => { | |||
it.each([ | |||
'EmailAddressInput', | |||
'PhoneNumberInput', | |||
'UrlInput', | |||
])('exports %s', (namedExport) => { | |||
expect(WebFormattedReact).toHaveProperty(namedExport); | |||
}); | |||
}); |
@@ -0,0 +1,10 @@ | |||
{ | |||
"exclude": ["node_modules"], | |||
"extends": "../../../../../tsconfig.json", | |||
"compilerOptions": { | |||
"rootDir": "src", | |||
"jsx": "react", | |||
"emitDeclarationOnly": true, | |||
"declaration": true | |||
} | |||
} |
@@ -0,0 +1,9 @@ | |||
import { defineConfig } from 'vitest/config' | |||
export default defineConfig({ | |||
test: { | |||
globals: true, | |||
environment: 'jsdom', | |||
setupFiles: ['setupTests.ts'], | |||
}, | |||
}) |
@@ -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" | |||
} |
@@ -0,0 +1,3 @@ | |||
{ | |||
"target": "es2017" | |||
} |
@@ -0,0 +1,4 @@ | |||
import matchers from '@testing-library/jest-dom/matchers'; | |||
import '@testing-library/jest-dom'; | |||
expect.extend(matchers); |
@@ -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(); | |||
}); | |||
}); |
@@ -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'; |
@@ -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(); | |||
}); | |||
}); |
@@ -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'; |
@@ -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(); | |||
}); | |||
}); |
@@ -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'; |
@@ -0,0 +1,3 @@ | |||
export * from './components/MultilineTextInput'; | |||
export * from './components/MaskedTextInput'; | |||
export * from './components/TextInput'; |
@@ -0,0 +1,11 @@ | |||
import * as WebFreeformReact from '.'; | |||
describe('web-freeform-react', () => { | |||
it.each([ | |||
'MaskedTextInput', | |||
'MultilineTextInput', | |||
'TextInput', | |||
])('exports %s', (namedExport) => { | |||
expect(WebFreeformReact).toHaveProperty(namedExport); | |||
}); | |||
}); |
@@ -0,0 +1,10 @@ | |||
{ | |||
"exclude": ["node_modules"], | |||
"extends": "../../../../../tsconfig.json", | |||
"compilerOptions": { | |||
"rootDir": "src", | |||
"jsx": "react", | |||
"emitDeclarationOnly": true, | |||
"declaration": true | |||
} | |||
} |
@@ -0,0 +1,9 @@ | |||
import { defineConfig } from 'vitest/config' | |||
export default defineConfig({ | |||
test: { | |||
globals: true, | |||
environment: 'jsdom', | |||
setupFiles: ['setupTests.ts'], | |||
}, | |||
}) |
@@ -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" | |||
} |
@@ -0,0 +1,3 @@ | |||
{ | |||
"target": "es2017" | |||
} |
@@ -0,0 +1,4 @@ | |||
import matchers from '@testing-library/jest-dom/matchers'; | |||
import '@testing-library/jest-dom'; | |||
expect.extend(matchers); |
@@ -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(); | |||
}); | |||
}); |
@@ -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'; |
@@ -0,0 +1 @@ | |||
export * from './components/Badge'; |
@@ -0,0 +1,9 @@ | |||
import * as WebInformationReact from '.'; | |||
describe('web-information-react', () => { | |||
it.each([ | |||
'Badge', | |||
])('exports %s', (namedExport) => { | |||
expect(WebInformationReact).toHaveProperty(namedExport); | |||
}); | |||
}); |
@@ -0,0 +1,10 @@ | |||
{ | |||
"exclude": ["node_modules"], | |||
"extends": "../../../../../tsconfig.json", | |||
"compilerOptions": { | |||
"rootDir": "src", | |||
"jsx": "react", | |||
"emitDeclarationOnly": true, | |||
"declaration": true | |||
} | |||
} |
@@ -0,0 +1,9 @@ | |||
import { defineConfig } from 'vitest/config' | |||
export default defineConfig({ | |||
test: { | |||
globals: true, | |||
environment: 'jsdom', | |||
setupFiles: ['setupTests.ts'], | |||
}, | |||
}) |
@@ -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" | |||
} |
@@ -0,0 +1,3 @@ | |||
{ | |||
"target": "es2017" | |||
} |
@@ -0,0 +1,4 @@ | |||
import matchers from '@testing-library/jest-dom/matchers'; | |||
import '@testing-library/jest-dom'; | |||
expect.extend(matchers); |
@@ -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(); | |||
}); | |||
}); |
@@ -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'; |
@@ -0,0 +1 @@ | |||
export * from './components/LinkButton'; |
@@ -0,0 +1,9 @@ | |||
import * as WebNavigationReact from '.'; | |||
describe('web-navigation-react', () => { | |||
it.each([ | |||
'LinkButton', | |||
])('exports %s', (namedExport) => { | |||
expect(WebNavigationReact).toHaveProperty(namedExport); | |||
}); | |||
}); |
@@ -0,0 +1,10 @@ | |||
{ | |||
"exclude": ["node_modules"], | |||
"extends": "../../../../../tsconfig.json", | |||
"compilerOptions": { | |||
"rootDir": "src", | |||
"jsx": "react", | |||
"emitDeclarationOnly": true, | |||
"declaration": true | |||
} | |||
} |
@@ -0,0 +1,9 @@ | |||
import { defineConfig } from 'vitest/config' | |||
export default defineConfig({ | |||
test: { | |||
globals: true, | |||
environment: 'jsdom', | |||
setupFiles: ['setupTests.ts'], | |||
}, | |||
}) |
@@ -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" | |||
} |
@@ -0,0 +1,3 @@ | |||
{ | |||
"target": "es2017" | |||
} |
@@ -0,0 +1,4 @@ | |||
import matchers from '@testing-library/jest-dom/matchers'; | |||
import '@testing-library/jest-dom'; | |||
expect.extend(matchers); |
@@ -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(); | |||
}); | |||
}); |
@@ -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'; |
@@ -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'; |
@@ -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(); | |||
}); | |||
}); |
@@ -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'; |
@@ -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(); | |||
}); | |||
}); |
@@ -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'; |
@@ -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} | |||
{' '} | |||
× | |||
</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'; |
@@ -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(); | |||
}); | |||
}); |
@@ -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'; |
@@ -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(); | |||
}); | |||
}); |
@@ -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'; |