浏览代码

Add tests, refactor

Implement tests to increase code coverage

The option module was modified to split components and avoid
overloading.
master
父节点
当前提交
d2b8df726c
共有 48 个文件被更改,包括 2334 次插入653 次删除
  1. +2
    -1
      .gitignore
  2. +45
    -0
      docs/philosophy.md
  3. +11
    -0
      jest.config.js
  4. +3
    -1
      package.json
  5. +201
    -0
      src/modules/action/components/ActionButton/ActionButton.test.tsx
  6. +15
    -9
      src/modules/action/components/ActionButton/index.tsx
  7. +9
    -0
      src/modules/action/web-action-react.test.ts
  8. +42
    -0
      src/modules/base-badge/index.ts
  9. +0
    -6
      src/modules/base-button/index.ts
  10. +8
    -8
      src/modules/base-checkcontrol/index.ts
  11. +13
    -1
      src/modules/base-textcontrol/index.ts
  12. +22
    -0
      src/modules/component-prop-utils/index.ts
  13. +41
    -28
      src/modules/css-utils/index.test.ts
  14. +183
    -3
      src/modules/freeform/components/MaskedTextInput/MaskedTextInput.test.tsx
  15. +10
    -2
      src/modules/freeform/components/MaskedTextInput/index.tsx
  16. +179
    -1
      src/modules/freeform/components/MultilineTextInput/MultilineTextInput.test.tsx
  17. +11
    -2
      src/modules/freeform/components/MultilineTextInput/index.tsx
  18. +196
    -2
      src/modules/freeform/components/TextInput/TextInput.test.tsx
  19. +11
    -2
      src/modules/freeform/components/TextInput/index.tsx
  20. +11
    -0
      src/modules/freeform/web-freeform-react.test.ts
  21. +0
    -66
      src/modules/info/components/Badge/index.tsx
  22. +31
    -0
      src/modules/information/components/Badge/Badge.test.tsx
  23. +36
    -0
      src/modules/information/components/Badge/index.tsx
  24. +0
    -0
      src/modules/information/index.ts
  25. +9
    -0
      src/modules/information/web-information-react.test.ts
  26. +189
    -0
      src/modules/navigation/components/LinkButton/LinkButton.test.tsx
  27. +5
    -11
      src/modules/navigation/components/LinkButton/index.tsx
  28. +9
    -0
      src/modules/navigation/web-navigation-react.test.ts
  29. +0
    -277
      src/modules/option/components/Checkbox/index.tsx
  30. +265
    -0
      src/modules/option/components/DropdownSelect/DropdownSelect.test.tsx
  31. +12
    -26
      src/modules/option/components/DropdownSelect/index.tsx
  32. +22
    -0
      src/modules/option/components/RadioButton/RadioButton.test.tsx
  33. +49
    -151
      src/modules/option/components/RadioButton/index.tsx
  34. +20
    -0
      src/modules/option/components/RadioTickBox/RadioTickBox.test.tsx
  35. +89
    -0
      src/modules/option/components/RadioTickBox/index.tsx
  36. +32
    -0
      src/modules/option/components/ToggleButton/ToggleButton.test.tsx
  37. +181
    -0
      src/modules/option/components/ToggleButton/index.tsx
  38. +20
    -0
      src/modules/option/components/ToggleSwitch/ToggleSwitch.test.tsx
  39. +109
    -0
      src/modules/option/components/ToggleSwitch/index.tsx
  40. +30
    -0
      src/modules/option/components/ToggleTickBox/ToggleTickBox.test.tsx
  41. +122
    -0
      src/modules/option/components/ToggleTickBox/index.tsx
  42. +4
    -1
      src/modules/option/index.ts
  43. +14
    -0
      src/modules/option/web-option-react.test.ts
  44. +1
    -1
      src/pages/categories/navigation/index.tsx
  45. +52
    -52
      src/pages/categories/option/index.tsx
  46. +2
    -1
      tsconfig.json
  47. +4
    -1
      tsconfig.test.json
  48. +14
    -0
      yarn.lock

+ 2
- 1
.gitignore 查看文件

@@ -34,4 +34,5 @@ yarn-error.log*
# vercel
.vercel

.idea/
.idea/
coverage/

+ 45
- 0
docs/philosophy.md 查看文件

@@ -0,0 +1,45 @@
# Tesseract Web

## Rationale

- Every component library makes their own conventions of component organization
- Graceful degradation through backwards compatibility with HTML controls is not the main focus
- In return, aspects such as accessibility may suffer since HTML controls are generally compliant to accessibility
considerations suggested by approved standards
- Emerging Web applications are at risk of deviating through said standards which can fragment the Web platform
implementation
- Hopefully we can inspire component development through the component organization we are proposing;
everyone can have their own implementations
- cf. Authoring (publishing interfaces) vs Dependency (coercing consumers to adapt to concrete implementations)

## Ground Rules

- Each component is an enhanced version of an HTML control (see [HTML enhancement](#html-enhancement))
- Each component should only do one thing and one thing only in terms of purpose and appearance (contrast to HTML's
`<input>` where its function is overloaded depending on its `type` attribute)
- Each component has decoupled styling, wherein each styling has their own API
- This styling API is then made cross-framework compatible
- This styling API allows use of dynamic styles (which then requires CSS-in-JS way of application)

## HTML enhancement

| HTML element | Tesseract counterpart | Remarks |
|----------------------------------------------------------------------------|-------------------------------|-----------------------------------------------------------------------|
| `<button>` | `action/ActionButton` | |
| `<input type="button">` | `action/ActionButton` | |
| `<input type="reset">` | `action/ActionButton` | |
| `<input type="submit">` | `action/ActionButton` | |
| `<textarea>` | `freeform/MultilineTextInput` | |
| `<input type="text">` | `freeform/TextInput` | |
| `<input type="search">` | `freeform/TextInput` | |
| `<input type="password">` | `freeform/MaskedTextInput` | |
| `<a>` with button appearance | `navigation/LinkButton` | |
| `<select>` without `multiple` attribute | `option/DropdownSelect` | |
| `<input type="checkbox">` with `indeterminate` state | `option/ToggleTickBox` | |
| `<input type="checkbox">` with `indeterminate` state and button appearance | `option/ToggleButton` | |
| `<input type="checkbox">` without `indeterminate` state | `option/ToggleSwitch` | Prefer using this component when indeterminate state is not expected. |
| `<input type="radio">` | `option/RadioTickBox` | |
| `<input type="radio">` with button appearance | `option/RadioButton` | |
| `<input type="number">` | `number/Spinner` | Use this component for discrete values. |
| `<input type="range">` | `number/Slider` | Use this component for continuous values. |


+ 11
- 0
jest.config.js 查看文件

@@ -1,3 +1,5 @@
const tsconfig = require('./tsconfig.json');

/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
@@ -7,4 +9,13 @@ module.exports = {
tsconfig: 'tsconfig.test.json',
},
},
moduleNameMapper: Object.fromEntries(
Object
.entries(tsconfig.compilerOptions.paths)
.map(([alias, paths]) => [alias, paths[0].replace('.', '<rootDir>')])
),
collectCoverageFrom: [
'src/modules/**/*.{ts,tsx}',
'!src/modules/base-*/**/*.*',
],
};

+ 3
- 1
package.json 查看文件

@@ -6,7 +6,8 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"test": "jest"
},
"dependencies": {
"goober": "^2.0.41",
@@ -16,6 +17,7 @@
"tailwindcss": "^2.2.16"
},
"devDependencies": {
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.0.3",
"@types/react": "17.0.27",
"eslint": "^7.32.0",


+ 201
- 0
src/modules/action/components/ActionButton/ActionButton.test.tsx 查看文件

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

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

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

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

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

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

it('should handle click events', () => {
const onClick = jest.fn();
render(
<ActionButton
onClick={onClick}
/>
);
const button: HTMLButtonElement = screen.getByRole('button');
userEvent.click(button);
expect(onClick).toBeCalled();
});

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

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

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

describe.each([
ButtonBase.ButtonSize.SMALL,
ButtonBase.ButtonSize.MEDIUM,
ButtonBase.ButtonSize.LARGE,
])('on %s size', (size) => {
it('should render button styles', () => {
render(
<ActionButton
size={size}
/>
);

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

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

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

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

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

it.each([
ButtonBase.ButtonVariant.OUTLINE,
ButtonBase.ButtonVariant.FILLED,
])('should render a button with variant %s', (variant) => {
render(
<ActionButton
variant={variant}
/>
);

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

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

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

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

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

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

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

it.each([
ActionButtonType.BUTTON,
ActionButtonType.RESET,
ActionButtonType.SUBMIT,
])('should render a button with type %s', (buttonType) => {
render(
<ActionButton
type={buttonType}
/>
);
const button: HTMLButtonElement = screen.getByRole('button');
expect(button).toHaveProperty('type', buttonType);
});

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

+ 15
- 9
src/modules/action/components/ActionButton/index.tsx 查看文件

@@ -1,12 +1,18 @@
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 type ActionButtonProps = Omit<React.HTMLProps<HTMLButtonElement>, 'size' | 'type' | 'style'> & {
/**
* Size of the component.
@@ -28,10 +34,6 @@ export type ActionButtonProps = Omit<React.HTMLProps<HTMLButtonElement>, 'size'
* Type of the component.
*/
type?: ActionButtonType,
/**
* Style of the component.
*/
style?: ButtonBase.ButtonStyle,
/**
* Does the component need to conserve space?
*/
@@ -64,7 +66,6 @@ export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProp
children,
type = ActionButtonType.BUTTON,
block = false,
style = ButtonBase.ButtonStyle.DEFAULT,
disabled = false,
compact = false,
subtext,
@@ -81,11 +82,10 @@ export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProp
block,
variant,
border,
style,
compact,
menuItem,
disabled,
}), [size, block, variant, border, style, compact, menuItem]);
}), [size, block, variant, border, compact, menuItem, disabled]);

return (
<button
@@ -93,7 +93,7 @@ export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProp
disabled={disabled}
className={ButtonBase.Button(styleProps)}
ref={ref}
type={type ?? ActionButtonType.BUTTON}
type={type}
>
<span
className={ButtonBase.Border(styleProps)}
@@ -103,6 +103,7 @@ export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProp
>
<span
className={ButtonBase.MainText()}
data-testid="children"
>
<span
className={ButtonBase.OverflowText()}
@@ -115,7 +116,10 @@ export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProp
&& (
<>
{' '}
<span className={ButtonBase.Subtext()}>
<span
className={ButtonBase.Subtext()}
data-testid="subtext"
>
<span
className={ButtonBase.OverflowText()}
>
@@ -133,6 +137,7 @@ export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProp
{' '}
<span
className={ButtonBase.BadgeContainer(styleProps)}
data-testid="badge"
>
{badge}
</span>
@@ -146,6 +151,7 @@ export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProp
{' '}
<span
className={ButtonBase.IndicatorWrapper(styleProps)}
data-testid="menuItemIndicator"
>
<svg
className={ButtonBase.Indicator()}


+ 9
- 0
src/modules/action/web-action-react.test.ts 查看文件

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

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

+ 42
- 0
src/modules/base-badge/index.ts 查看文件

@@ -0,0 +1,42 @@
import { css } from '@tesseract-design/css-utils';

export type BadgeBaseArgs = {
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
- 6
src/modules/base-button/index.ts 查看文件

@@ -11,17 +11,12 @@ export enum ButtonVariant {
FILLED = 'filled',
}

export enum ButtonStyle {
DEFAULT = 'default',
}

export type ButtonBaseArgs = {
size: ButtonSize,
block: boolean,
variant: ButtonVariant,
border: boolean,
disabled: boolean,
style: ButtonStyle,
compact: boolean,
menuItem: boolean,
}
@@ -186,7 +181,6 @@ export const Border = ({
),
);


export const Label = ({
compact,
menuItem,


+ 8
- 8
src/modules/base-checkcontrol/index.ts 查看文件

@@ -1,7 +1,7 @@
import { css } from '@tesseract-design/css-utils'

export enum CheckControlAppearance {
DEFAULT = 'default',
TICK_BOX = 'tick-box',
BUTTON = 'button',
SWITCH = 'switch',
}
@@ -62,7 +62,7 @@ export const CheckStateContainer = ({
`,
css.nest('&:checked + * > :first-child + * > *') (
css.if (
appearance === CheckControlAppearance.DEFAULT
appearance === CheckControlAppearance.TICK_BOX
|| appearance === CheckControlAppearance.BUTTON
) (
css.if (type === 'checkbox') (
@@ -106,7 +106,7 @@ export const CheckStateContainer = ({
css.nest('&:indeterminate[type="checkbox"] + * > :first-child + * > *') (
css.if (
appearance === CheckControlAppearance.BUTTON
|| appearance === CheckControlAppearance.DEFAULT
|| appearance === CheckControlAppearance.TICK_BOX
) (
css`
width: 1.5em;
@@ -125,7 +125,7 @@ export const CheckStateContainer = ({
css.nest('&:indeterminate[type="checkbox"] + * > :first-child + * > * > :first-child + *') (
css.if (
appearance === CheckControlAppearance.BUTTON
|| appearance === CheckControlAppearance.DEFAULT
|| appearance === CheckControlAppearance.TICK_BOX
) (
css`
display: block;
@@ -135,7 +135,7 @@ export const CheckStateContainer = ({
css.nest('&:checked + * > :first-child + * > * > :first-child') (
css.if (
appearance === CheckControlAppearance.BUTTON
|| appearance === CheckControlAppearance.DEFAULT
|| appearance === CheckControlAppearance.TICK_BOX
) (
css`
display: block;
@@ -177,7 +177,7 @@ export const CheckIndicatorArea = ({
box-sizing: border-box;
}
`,
css.if (appearance === CheckControlAppearance.DEFAULT) (
css.if (appearance === CheckControlAppearance.TICK_BOX) (
css`
width: 1.5em;
height: 1.5em;
@@ -240,7 +240,7 @@ export const CheckIndicatorWrapper = ({
border-radius: inherit;
`,
css.if(
appearance === CheckControlAppearance.DEFAULT
appearance === CheckControlAppearance.TICK_BOX
|| appearance === CheckControlAppearance.BUTTON
) (
css`
@@ -289,7 +289,7 @@ export const ClickAreaWrapper = ({
css.dynamic({
display: block ? 'block' : 'inline-block',
}),
css.if (appearance === CheckControlAppearance.DEFAULT) (
css.if (appearance === CheckControlAppearance.TICK_BOX) (
css`
padding-left: 2.25rem;
text-indent: -2.25rem;


+ 13
- 1
src/modules/base-textcontrol/index.ts 查看文件

@@ -57,7 +57,7 @@ export type TextControlBaseArgs = {
predefinedValues: boolean,
}

export const ComponentBase = ({
export const Root = ({
block,
}: TextControlBaseArgs): string => css.cx(
css`
@@ -365,3 +365,15 @@ export const IndicatorWrapper = ({
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;
`,
);

+ 22
- 0
src/modules/component-prop-utils/index.ts 查看文件

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

// TODO check if a utility library like this is needed!

export type ButtonBaseProps<Node> = {
/**
* Size of the component.
*/
size?: ButtonBase.ButtonSize,
/**
* Variant of the component.
*/
variant?: ButtonBase.ButtonVariant,
/**
* Should the component display a border?
*/
border?: boolean,
/**
* Short complementary content displayed at the edge of the component.
*/
badge?: Node,
}

+ 41
- 28
src/modules/css-utils/index.test.ts 查看文件

@@ -1,5 +1,11 @@
import { css, CssIfStringImpl, CssStringImpl } from '.';

jest.mock('goober', () => {
return {
css: () => 'gooberClass',
};
});

describe('css-utils', () => {
describe('css', () => {
it('should return CssString', () => {
@@ -9,7 +15,7 @@ describe('css-utils', () => {
`

expect(c).toBeInstanceOf(CssStringImpl);
expect(c.toString()).toBe('background-color:white; color:black;');
expect(c.toString()).toBe('background-color:white;color:black;');
})
})

@@ -35,7 +41,7 @@ describe('css-utils', () => {
expect(c.toString()).toBe('')
})

it('should return CssString with .else when the condition is true', () => {
it('should return CssString with .else when the if condition is false', () => {
const c = css.if(false)(
css`
background-color: white;
@@ -46,42 +52,21 @@ describe('css-utils', () => {
`
)

expect(c).toBeInstanceOf(CssStringImpl)
expect(c.toString()).toBe('background-color:black;')
})

it('should return CssString with .else.if when the condition is true', () => {
const c = css.if(false)(
css`
background-color: white;
`
).else.if(true)(
css`
background-color: black;
`
)

expect(c).toBeInstanceOf(CssIfStringImpl)
expect(c.toString()).toBe('background-color:black;')
})

it('should return CssString with .else.if.else when the condition is false', () => {
const c = css.if('a'.toUpperCase() === 'C')(
it('should return CssString with .else when the if condition is true', () => {
const c = css.if(true)(
css`
background-color: white;
`
).else.if('b'.toUpperCase() === 'C')(
css`
background-color: black;
`
).else(
css`
background-color: gray;
background-color: black;
`
)

expect(c).toBeInstanceOf(CssStringImpl)
expect(c.toString()).toBe('background-color:gray;')
expect(c.toString()).toBe('background-color:white;')
})
})

@@ -118,7 +103,35 @@ describe('css-utils', () => {
`
)

expect(c.toString()).toBe('@media only screen and (min-width: 720px){color:black; background-color:white;}')
expect(c.toString()).toBe('@media only screen and (min-width: 720px){color:black;background-color:white;}')
})
})

describe('css.cx', () => {
it('should accept strings as classnames', () => {
expect(css.cx('class1', 'class2')).toBe('class1 class2');
})

it('should accept CSS strings for classname generation', () => {
expect(
css.cx(
css`
color: white;
`
)
).toBe('gooberClass');
})

it('should accept mixed values', () => {
expect(
css.cx(
'class1',
'class2',
css`
color: white;
`
)
).toBe('class1 class2 gooberClass');
})
})
})

+ 183
- 3
src/modules/freeform/components/MaskedTextInput/MaskedTextInput.test.tsx 查看文件

@@ -1,18 +1,198 @@
import * as React from 'react';
import {
render,
screen
screen,
} from '@testing-library/react';
import '@testing-library/jest-dom';
import userEvent from '@testing-library/user-event';
import * as TextControlBase from '@tesseract-design/web-base-textcontrol';
import {
MaskedTextInput
} from '.';

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

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

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

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

it('should render 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('should render a hint', () => {
render(
<MaskedTextInput
hint="foo"
/>
);
const hint = screen.getByTestId('hint');
expect(hint).toBeInTheDocument();
});

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

describe.each([
TextControlBase.TextControlSize.SMALL,
TextControlBase.TextControlSize.MEDIUM,
TextControlBase.TextControlSize.LARGE,
])('on %s size', (size) => {
it('should render input styles', () => {
render(
<MaskedTextInput
size={size}
/>
);

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

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

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

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

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

it('should render a block textbox', () => {
render(
<MaskedTextInput
block
/>
);

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

describe.each([
TextControlBase.TextControlStyle.DEFAULT,
TextControlBase.TextControlStyle.ALTERNATE,
])('on %s style', (style) => {
it('should render input styles', () => {
render(
<MaskedTextInput
style={style}
/>
);

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

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

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

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

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

it('should handle change events', () => {
const onChange = jest.fn();
render(
<MaskedTextInput
onChange={onChange}
/>
);
const textbox: HTMLInputElement = screen.getByTestId('input');
userEvent.type(textbox, 'foobar');
expect(onChange).toBeCalled();
});
});

+ 10
- 2
src/modules/freeform/components/MaskedTextInput/index.tsx 查看文件

@@ -71,7 +71,7 @@ export const MaskedTextInput = React.forwardRef<HTMLInputElement, MaskedTextInpu

return (
<div
className={TextControlBase.ComponentBase(textInputBaseArgs)}
className={TextControlBase.Root(textInputBaseArgs)}
>
<input
{...etcProps}
@@ -81,10 +81,17 @@ export const MaskedTextInput = React.forwardRef<HTMLInputElement, MaskedTextInpu
type="password"
data-testid="input"
/>
{border && <span />}
{
border && (
<span
data-testid="border"
/>
)
}
{
label && !hiddenLabel && (
<div
data-testid="label"
className={TextControlBase.LabelWrapper(textInputBaseArgs)}
>
{label}
@@ -94,6 +101,7 @@ export const MaskedTextInput = React.forwardRef<HTMLInputElement, MaskedTextInpu
{hint && (
<div
className={TextControlBase.HintWrapper(textInputBaseArgs)}
data-testid="hint"
>
<div
className={TextControlBase.Hint()}


+ 179
- 1
src/modules/freeform/components/MultilineTextInput/MultilineTextInput.test.tsx 查看文件

@@ -4,14 +4,192 @@ import {
screen
} from '@testing-library/react';
import '@testing-library/jest-dom';
import userEvent from '@testing-library/user-event';
import * as TextControlBase from '@tesseract-design/web-base-textcontrol';
import {
MultilineTextInput
} from '.';

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

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

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

it('should render 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('should render a hint', () => {
render(
<MultilineTextInput
hint="foo"
/>
);
const hint = screen.getByTestId('hint');
expect(hint).toBeInTheDocument();
});

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

describe.each([
TextControlBase.TextControlSize.SMALL,
TextControlBase.TextControlSize.MEDIUM,
TextControlBase.TextControlSize.LARGE,
])('on %s size', (size) => {
it('should render input styles', () => {
render(
<MultilineTextInput
size={size}
/>
);

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

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

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

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

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

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

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

describe.each([
TextControlBase.TextControlStyle.DEFAULT,
TextControlBase.TextControlStyle.ALTERNATE,
])('on %s style', (style) => {
it('should render input styles', () => {
render(
<MultilineTextInput
style={style}
/>
);

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

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

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

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

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

it('should handle change events', () => {
const onChange = jest.fn();
render(
<MultilineTextInput
onChange={onChange}
/>
);
const textbox: HTMLTextAreaElement = screen.getByRole('textbox');
userEvent.type(textbox, 'foobar');
expect(onChange).toBeCalled();
});
});

+ 11
- 2
src/modules/freeform/components/MultilineTextInput/index.tsx 查看文件

@@ -71,7 +71,7 @@ export const MultilineTextInput = React.forwardRef<HTMLTextAreaElement, Multilin

return (
<div
className={TextControlBase.ComponentBase(textInputBaseArgs)}
className={TextControlBase.Root(textInputBaseArgs)}
>
<textarea
{...etcProps}
@@ -81,11 +81,19 @@ export const MultilineTextInput = React.forwardRef<HTMLTextAreaElement, Multilin
style={{
height: TextControlBase.MIN_HEIGHTS[size],
}}
data-testid="input"
/>
{border && <span />}
{
border && (
<span
data-testid="border"
/>
)
}
{
label && !hiddenLabel && (
<div
data-testid="label"
className={TextControlBase.LabelWrapper(textInputBaseArgs)}
>
{label}
@@ -95,6 +103,7 @@ export const MultilineTextInput = React.forwardRef<HTMLTextAreaElement, Multilin
{hint && (
<div
className={TextControlBase.HintWrapper(textInputBaseArgs)}
data-testid="hint"
>
<div
className={TextControlBase.Hint()}


+ 196
- 2
src/modules/freeform/components/TextInput/TextInput.test.tsx 查看文件

@@ -4,14 +4,208 @@ import {
screen
} from '@testing-library/react';
import '@testing-library/jest-dom';
import userEvent from '@testing-library/user-event';
import * as TextControlBase from '@tesseract-design/web-base-textcontrol';
import {
TextInput
TextInput, TextInputType,
} from '.';

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

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

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

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

it('should render 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('should render a hint', () => {
render(
<TextInput
hint="foo"
/>
);
const hint = screen.getByTestId('hint');
expect(hint).toBeInTheDocument();
});

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

describe.each([
TextControlBase.TextControlSize.SMALL,
TextControlBase.TextControlSize.MEDIUM,
TextControlBase.TextControlSize.LARGE,
])('on %s size', (size) => {
it('should render input styles', () => {
render(
<TextInput
size={size}
/>
);

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

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

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

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

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

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

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

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

describe.each([
TextControlBase.TextControlStyle.DEFAULT,
TextControlBase.TextControlStyle.ALTERNATE,
])('on %s style', (style) => {
it('should render input styles', () => {
render(
<TextInput
style={style}
/>
);

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

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

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

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

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

it('should handle change events', () => {
const onChange = jest.fn();
render(
<TextInput
onChange={onChange}
/>
);
const textbox: HTMLInputElement = screen.getByRole('textbox');
userEvent.type(textbox, 'foobar');
expect(onChange).toBeCalled();
});
});

+ 11
- 2
src/modules/freeform/components/TextInput/index.tsx 查看文件

@@ -81,7 +81,7 @@ export const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>(

return (
<div
className={TextControlBase.ComponentBase(textInputBaseArgs)}
className={TextControlBase.Root(textInputBaseArgs)}
>
<input
{...etcProps}
@@ -89,11 +89,19 @@ export const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>(
ref={ref}
aria-label={label}
type={type}
data-testid="input"
/>
{border && <span />}
{
border && (
<span
data-testid="border"
/>
)
}
{
label && !hiddenLabel && (
<div
data-testid="label"
className={TextControlBase.LabelWrapper(textInputBaseArgs)}
>
{label}
@@ -103,6 +111,7 @@ export const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>(
{hint && (
<div
className={TextControlBase.HintWrapper(textInputBaseArgs)}
data-testid="hint"
>
<div
className={TextControlBase.Hint()}


+ 11
- 0
src/modules/freeform/web-freeform-react.test.ts 查看文件

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

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

+ 0
- 66
src/modules/info/components/Badge/index.tsx 查看文件

@@ -1,66 +0,0 @@
import * as React from 'react';
import {
css,
} from 'goober';

const BadgeBase = ({ rounded }: { rounded: boolean }) => css`
position: relative;
height: 1.5em;
min-width: 1.5em;
display: inline-grid;
vertical-align: middle;
place-content: center;
border-radius: ${rounded ? '0.75em' : '0.25rem'};
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: '';
}
`;

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

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

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

return (
<strong
ref={ref}
className={BadgeBase(badgeStyleProps)}
>
<span
className={Content()}
>
{children}
</span>
</strong>
)
}
)

Badge.displayName = 'Badge';

+ 31
- 0
src/modules/information/components/Badge/Badge.test.tsx 查看文件

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

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

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

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

+ 36
- 0
src/modules/information/components/Badge/index.tsx 查看文件

@@ -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';

src/modules/info/index.ts → src/modules/information/index.ts 查看文件


+ 9
- 0
src/modules/information/web-information-react.test.ts 查看文件

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

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

+ 189
- 0
src/modules/navigation/components/LinkButton/LinkButton.test.tsx 查看文件

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

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

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

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

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

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

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

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

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

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

describe.each([
ButtonBase.ButtonSize.SMALL,
ButtonBase.ButtonSize.MEDIUM,
ButtonBase.ButtonSize.LARGE,
])('on %s size', (size) => {
it('should render button styles', () => {
render(
<LinkButton
size={size}
/>
);

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

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

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

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

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

it.each([
ButtonBase.ButtonVariant.OUTLINE,
ButtonBase.ButtonVariant.FILLED,
])('should render a button with variant %s', (variant) => {
render(
<LinkButton
variant={variant}
/>
);

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

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

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

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

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

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

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

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

+ 5
- 11
src/modules/navigation/components/LinkButton/index.tsx 查看文件

@@ -20,14 +20,6 @@ export type LinkButtonProps = Omit<React.HTMLProps<LinkButtonElement>, 'size' |
* Should the component occupy the whole width of its parent?
*/
block?: boolean,
/**
* Can the component be activated?
*/
disabled?: boolean,
/**
* Style of the component.
*/
style?: ButtonBase.ButtonStyle,
/**
* Does the component need to conserve space?
*/
@@ -60,7 +52,6 @@ export const LinkButton = React.forwardRef<LinkButtonElement, LinkButtonProps>(
children,
block = false,
disabled = false,
style = ButtonBase.ButtonStyle.DEFAULT,
onClick,
href,
target,
@@ -81,10 +72,9 @@ export const LinkButton = React.forwardRef<LinkButtonElement, LinkButtonProps>(
variant,
border,
disabled,
style,
compact,
menuItem,
}), [size, block, variant, border, disabled, style, compact, menuItem]);
}), [size, block, variant, border, disabled, compact, menuItem]);

const commonChildren = (
<>
@@ -96,6 +86,7 @@ export const LinkButton = React.forwardRef<LinkButtonElement, LinkButtonProps>(
>
<span
className={ButtonBase.MainText()}
data-testid="children"
>
<span
className={ButtonBase.OverflowText()}
@@ -110,6 +101,7 @@ export const LinkButton = React.forwardRef<LinkButtonElement, LinkButtonProps>(
{' '}
<span
className={ButtonBase.Subtext()}
data-testid="subtext"
>
<span
className={ButtonBase.OverflowText()}
@@ -128,6 +120,7 @@ export const LinkButton = React.forwardRef<LinkButtonElement, LinkButtonProps>(
{' '}
<span
className={ButtonBase.BadgeContainer(styleProps)}
data-testid="badge"
>
{badge}
</span>
@@ -141,6 +134,7 @@ export const LinkButton = React.forwardRef<LinkButtonElement, LinkButtonProps>(
{' '}
<span
className={ButtonBase.IndicatorWrapper(styleProps)}
data-testid="menuItemIndicator"
>
<svg
className={ButtonBase.Indicator()}


+ 9
- 0
src/modules/navigation/web-navigation-react.test.ts 查看文件

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

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

+ 0
- 277
src/modules/option/components/Checkbox/index.tsx 查看文件

@@ -1,277 +0,0 @@
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 CheckboxProps = 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,
/**
* Style of the component.
*/
style?: ButtonBase.ButtonStyle,
/**
* 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,
/**
* Appearance of the component.
*/
appearance?: CheckControlBase.CheckControlAppearance,
/**
* 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 Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
(
{
size = ButtonBase.ButtonSize.MEDIUM,
variant = ButtonBase.ButtonVariant.OUTLINE,
border = false,
children,
block = false,
style = ButtonBase.ButtonStyle.DEFAULT,
disabled = false,
compact = false,
subtext,
badge,
appearance = CheckControlBase.CheckControlAppearance.DEFAULT,
indeterminate = false,
className: _className,
as: _as,
...etcProps
}: CheckboxProps,
ref,
) => {
const styleProps = React.useMemo<ButtonBase.ButtonBaseArgs & CheckControlBase.CheckControlBaseArgs>(() => ({
size,
block,
variant,
border,
style,
compact,
menuItem: false,
disabled,
appearance,
type: 'checkbox',
}), [size, block, variant, border, style, compact, disabled, appearance]);
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]);

const checkIndicatorCommon = (
<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>
)

return (
<div
className={CheckControlBase.ClickAreaWrapper(styleProps)}
>
<label
className={CheckControlBase.ClickArea()}
>
<input
{...etcProps}
disabled={disabled}
type="checkbox"
ref={theRef}
className={CheckControlBase.CheckStateContainer(styleProps)}
/>
{
appearance === CheckControlBase.CheckControlAppearance.DEFAULT
&& (
<span>
<span />
{checkIndicatorCommon}
<span>
{children}
{
subtext
&& (
<>
<br />
<span
className={CheckControlBase.Subtext()}
>
{subtext}
</span>
</>
)
}
</span>
{
badge
&& (
<>
{' '}
<span
className={ButtonBase.BadgeContainer(styleProps)}
>
{badge}
</span>
</>
)
}
</span>
)
}
{
appearance === CheckControlBase.CheckControlAppearance.BUTTON
&& (
<span
className={ButtonBase.Button(styleProps)}
>
<span
className={ButtonBase.Border(styleProps)}
/>
{checkIndicatorCommon}
<span
className={ButtonBase.Label(styleProps)}
>
<span
className={ButtonBase.MainText()}
>
<span
className={ButtonBase.OverflowText()}
>
{children}
</span>
</span>
{
subtext
&& (
<>
{' '}
<span
className={ButtonBase.Subtext()}
>
<span
className={ButtonBase.OverflowText()}
>
{subtext}
</span>
</span>
</>
)
}
</span>
{
badge
&& (
<>
{' '}
<span
className={ButtonBase.BadgeContainer(styleProps)}
>
{badge}
</span>
</>
)
}
</span>
)
}
{
appearance === CheckControlBase.CheckControlAppearance.SWITCH
&& (
<span>
<span />
{checkIndicatorCommon}
<span>
{children}
{
subtext
&& (
<>
<br />
<span
className={CheckControlBase.Subtext()}
>
{subtext}
</span>
</>
)
}
</span>
{
badge
&& (
<>
{' '}
<span
className={ButtonBase.BadgeContainer(styleProps)}
>
{badge}
</span>
</>
)
}
</span>
)
}
</label>
</div>
);
},
);

Checkbox.displayName = 'ActionButton';

+ 265
- 0
src/modules/option/components/DropdownSelect/DropdownSelect.test.tsx 查看文件

@@ -4,14 +4,279 @@ import {
screen
} from '@testing-library/react';
import '@testing-library/jest-dom';
import * as TextControlBase from '@tesseract-design/web-base-textcontrol';
import {
DropdownSelect
} from '.';

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

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

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

it('should render 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('should render a hint', () => {
render(
<DropdownSelect
hint="foo"
/>
);
const hint = screen.getByTestId('hint');
expect(hint).toBeInTheDocument();
});

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

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

it('should render 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('should render 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([
TextControlBase.TextControlSize.SMALL,
TextControlBase.TextControlSize.MEDIUM,
TextControlBase.TextControlSize.LARGE,
])('on %s size', (size) => {
it('should render input styles', () => {
render(
<DropdownSelect
size={size}
/>
);

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

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

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

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

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

it('should render a block textbox', () => {
render(
<DropdownSelect
block
/>
);

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

describe.each([
TextControlBase.TextControlStyle.DEFAULT,
TextControlBase.TextControlStyle.ALTERNATE,
])('on %s style', (style) => {
it('should render input styles', () => {
render(
<DropdownSelect
style={style}
/>
);

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

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

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

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

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

+ 12
- 26
src/modules/option/components/DropdownSelect/index.tsx 查看文件

@@ -1,19 +1,6 @@
import * as React from 'react';
import {
css
} from 'goober';
import * as TextControlBase from '@tesseract-design/web-base-textcontrol';

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

export interface SelectOption {
label: string,
value?: string | number | readonly string[]
@@ -22,12 +9,8 @@ export interface SelectOption {

type RenderOptionsProps = {
options: SelectOption[],
// FIXME bug in eslint does not play well with React.VFC
// eslint-disable-next-line react/require-default-props
optionComponent?: React.ElementType,
// eslint-disable-next-line react/require-default-props
optgroupComponent?: React.ElementType,
// eslint-disable-next-line react/require-default-props
level?: number,
}

@@ -62,6 +45,7 @@ const RenderOptions: React.VFC<RenderOptionsProps> = ({
options={o.children}
optionComponent={Option}
optgroupComponent={Optgroup}
level={level + 1}
/>
</Optgroup>
);
@@ -79,6 +63,7 @@ const RenderOptions: React.VFC<RenderOptionsProps> = ({
options={o.children}
optionComponent={Option}
optgroupComponent={Optgroup}
level={level + 1}
/>
</React.Fragment>
);
@@ -123,10 +108,6 @@ export type DropdownSelectProps = Omit<React.HTMLProps<HTMLSelectElement>, 'size
* Options available for the component's values.
*/
options?: SelectOption[],
/**
* Component for rendering options.
*/
renderOptions?: React.ElementType,
}

/**
@@ -149,7 +130,6 @@ export const DropdownSelect = React.forwardRef<HTMLSelectElement, DropdownSelect
placeholder: _placeholder,
as: _as,
options = [],
renderOptions: Render = RenderOptions,
...etcProps
}: DropdownSelectProps,
ref,
@@ -166,7 +146,7 @@ export const DropdownSelect = React.forwardRef<HTMLSelectElement, DropdownSelect

return (
<div
className={TextControlBase.ComponentBase(styleArgs)}
className={TextControlBase.Root(styleArgs)}
>
<select
{...etcProps}
@@ -174,14 +154,19 @@ export const DropdownSelect = React.forwardRef<HTMLSelectElement, DropdownSelect
ref={ref}
aria-label={label}
>
<Render
<RenderOptions
options={options}
/>
</select>
{border && <span />}
{border && (
<span
data-testid="border"
/>
)}
{label && !hiddenLabel && (
<div
className={TextControlBase.LabelWrapper(styleArgs)}
data-testid="label"
>
{label}
</div>
@@ -189,6 +174,7 @@ export const DropdownSelect = React.forwardRef<HTMLSelectElement, DropdownSelect
{hint && (
<div
className={TextControlBase.HintWrapper(styleArgs)}
data-testid="hint"
>
<div
className={TextControlBase.Hint()}
@@ -201,7 +187,7 @@ export const DropdownSelect = React.forwardRef<HTMLSelectElement, DropdownSelect
className={TextControlBase.IndicatorWrapper(styleArgs)}
>
<svg
className={Indicator()}
className={TextControlBase.Indicator()}
viewBox="0 0 24 24"
role="presentation"
>


+ 22
- 0
src/modules/option/components/RadioButton/RadioButton.test.tsx 查看文件

@@ -0,0 +1,22 @@
import * as React from 'react';
import {
render,
screen
} from '@testing-library/react';
import '@testing-library/jest-dom';
import * as ButtonBase from '@tesseract-design/web-base-button';
import * as CheckControlBase from '@tesseract-design/web-base-checkcontrol';
import { RadioButton } from '.';

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

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

+ 49
- 151
src/modules/option/components/RadioButton/index.tsx 查看文件

@@ -19,10 +19,6 @@ export type RadioButtonProps = Omit<React.HTMLProps<HTMLInputElement>, 'size' |
* Should the component occupy the whole width of its parent?
*/
block?: boolean,
/**
* Style of the component.
*/
style?: ButtonBase.ButtonStyle,
/**
* Does the component need to conserve space?
*/
@@ -35,10 +31,6 @@ export type RadioButtonProps = Omit<React.HTMLProps<HTMLInputElement>, 'size' |
* Short complementary content displayed at the edge of the component.
*/
badge?: React.ReactNode,
/**
* Appearance of the component.
*/
appearance?: CheckControlBase.CheckControlAppearance,
}

/**
@@ -54,12 +46,10 @@ export const RadioButton = React.forwardRef<HTMLInputElement, RadioButtonProps>(
border = false,
children,
block = false,
style = ButtonBase.ButtonStyle.DEFAULT,
disabled = false,
compact = false,
subtext,
badge,
appearance = CheckControlBase.CheckControlAppearance.DEFAULT,
className: _className,
as: _as,
...etcProps
@@ -71,23 +61,12 @@ export const RadioButton = React.forwardRef<HTMLInputElement, RadioButtonProps>(
block,
variant,
border,
style,
compact,
menuItem: false,
disabled,
appearance,
appearance: CheckControlBase.CheckControlAppearance.BUTTON,
type: 'radio',
}), [size, block, variant, border, style, compact, disabled, appearance]);

const checkIndicatorCommon = (
<span
className={CheckControlBase.CheckIndicatorArea(styleProps)}
>
<span
className={CheckControlBase.CheckIndicatorWrapper(styleProps)}
/>
</span>
)
}), [size, block, variant, border, compact, disabled]);

return (
<div
@@ -103,144 +82,63 @@ export const RadioButton = React.forwardRef<HTMLInputElement, RadioButtonProps>(
ref={ref}
className={CheckControlBase.CheckStateContainer(styleProps)}
/>
{
appearance === CheckControlBase.CheckControlAppearance.DEFAULT
&& (
<span>
<span />
{checkIndicatorCommon}
<span>
{children}
{
subtext
&& (
<>
<br />
<span
className={CheckControlBase.Subtext()}
>
{subtext}
</span>
</>
)
}
</span>
{
badge
&& (
<>
{' '}
<span
className={ButtonBase.BadgeContainer(styleProps)}
>
{badge}
</span>
</>
)
}
</span>
)
}
{
appearance === CheckControlBase.CheckControlAppearance.BUTTON
&& (
<span
className={ButtonBase.Button(styleProps)}
>
<span
className={ButtonBase.Border(styleProps)}
/>
<span
className={CheckControlBase.CheckIndicatorArea(styleProps)}
>
<span
className={ButtonBase.Button(styleProps)}
className={CheckControlBase.CheckIndicatorWrapper(styleProps)}
/>
</span>
<span
className={ButtonBase.Label(styleProps)}
>
<span
className={ButtonBase.MainText()}
>
<span
className={ButtonBase.Border(styleProps)}
/>
{checkIndicatorCommon}
<span
className={ButtonBase.Label(styleProps)}
className={ButtonBase.OverflowText()}
>
<span
className={ButtonBase.MainText()}
>
{children}
</span>
</span>
{
subtext
&& (
<>
{' '}
<span
className={ButtonBase.OverflowText()}
className={ButtonBase.Subtext()}
>
{children}
</span>
</span>
{
subtext
&& (
<>
{' '}
<span
className={ButtonBase.Subtext()}
>
<span
className={ButtonBase.OverflowText()}
>
{subtext}
</span>
</span>
</>
)
}
</span>
{
badge
&& (
<>
{' '}
<span
className={ButtonBase.BadgeContainer(styleProps)}
className={ButtonBase.OverflowText()}
>
{badge}
{subtext}
</span>
</>
)
}
</span>
)
}
{
appearance === CheckControlBase.CheckControlAppearance.SWITCH
&& (
<span>
<span />
<span
className={CheckControlBase.CheckIndicatorArea(styleProps)}
>
</span>
</>
)
}
</span>
{
badge
&& (
<>
{' '}
<span
className={CheckControlBase.CheckIndicatorWrapper(styleProps)}
/>
</span>
<span>
{children}
{
subtext
&& (
<>
<br />
<span
className={CheckControlBase.Subtext()}
>
{subtext}
</span>
</>
)
}
</span>
{
badge
&& (
<>
{' '}
<span
className={ButtonBase.BadgeContainer(styleProps)}
>
{badge}
</span>
</>
)
}
</span>
)
}
className={ButtonBase.BadgeContainer(styleProps)}
>
{badge}
</span>
</>
)
}
</span>
</label>
</div>
);


+ 20
- 0
src/modules/option/components/RadioTickBox/RadioTickBox.test.tsx 查看文件

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

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

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

+ 89
- 0
src/modules/option/components/RadioTickBox/index.tsx 查看文件

@@ -0,0 +1,89 @@
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: 'radio',
}), [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()}
>
{subtext}
</span>
</>
)
}
</span>
</span>
</label>
</div>
);
},
);

RadioTickBox.displayName = 'ActionButton';

+ 32
- 0
src/modules/option/components/ToggleButton/ToggleButton.test.tsx 查看文件

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

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

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

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

+ 181
- 0
src/modules/option/components/ToggleButton/index.tsx 查看文件

@@ -0,0 +1,181 @@
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: 'checkbox',
}), [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()}
>
<span
className={ButtonBase.OverflowText()}
>
{children}
</span>
</span>
{
subtext
&& (
<>
{' '}
<span
className={ButtonBase.Subtext()}
>
<span
className={ButtonBase.OverflowText()}
>
{subtext}
</span>
</span>
</>
)
}
</span>
{
badge
&& (
<>
{' '}
<span
className={ButtonBase.BadgeContainer(styleProps)}
>
{badge}
</span>
</>
)
}
</span>
</label>
</div>
);
},
);

ToggleButton.displayName = 'ActionButton';

+ 20
- 0
src/modules/option/components/ToggleSwitch/ToggleSwitch.test.tsx 查看文件

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

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

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

+ 109
- 0
src/modules/option/components/ToggleSwitch/index.tsx 查看文件

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

export type ToggleSwitchProps = 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 ToggleSwitch = React.forwardRef<HTMLInputElement, ToggleSwitchProps>(
(
{
children,
block = false,
compact = false,
subtext,
className: _className,
as: _as,
...etcProps
}: ToggleSwitchProps,
ref,
) => {
const styleProps = React.useMemo<CheckControlBase.CheckControlBaseArgs>(() => ({
block,
compact,
menuItem: false,
appearance: CheckControlBase.CheckControlAppearance.SWITCH,
type: 'checkbox',
}), [block, compact]);

return (
<div
className={CheckControlBase.ClickAreaWrapper(styleProps)}
>
<label
className={CheckControlBase.ClickArea()}
>
<input
{...etcProps}
type="checkbox"
ref={ref}
className={CheckControlBase.CheckStateContainer(styleProps)}
/>
<span>
<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>
{children}
{
subtext
&& (
<>
<br />
<span
className={CheckControlBase.Subtext()}
>
{subtext}
</span>
</>
)
}
</span>
</span>
</label>
</div>
);
},
);

ToggleSwitch.displayName = 'ActionButton';

+ 30
- 0
src/modules/option/components/ToggleTickBox/ToggleTickBox.test.tsx 查看文件

@@ -0,0 +1,30 @@
import * as React from 'react';
import {
render,
screen
} from '@testing-library/react';
import '@testing-library/jest-dom';
import * as CheckControlBase from '@tesseract-design/web-base-checkcontrol';
import { ToggleTickBox } from '.';

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

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

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

+ 122
- 0
src/modules/option/components/ToggleTickBox/index.tsx 查看文件

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

export type ToggleTickBoxProps = Omit<React.HTMLProps<HTMLInputElement>, '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,
/**
* 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 ToggleTickBox = React.forwardRef<HTMLInputElement, ToggleTickBoxProps>(
(
{
children,
block = false,
compact = false,
subtext,
indeterminate = false,
className: _className,
as: _as,
...etcProps
}: ToggleTickBoxProps,
ref,
) => {
const styleProps = React.useMemo<CheckControlBase.CheckControlBaseArgs>(() => ({
block,
compact,
appearance: CheckControlBase.CheckControlAppearance.TICK_BOX,
type: 'checkbox',
}), [block, compact]);
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}
type="checkbox"
ref={theRef}
className={CheckControlBase.CheckStateContainer(styleProps)}
/>
<span>
<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>
{children}
{
subtext
&& (
<>
<br />
<span
className={CheckControlBase.Subtext()}
>
{subtext}
</span>
</>
)
}
</span>
</span>
</label>
</div>
);
},
);

ToggleTickBox.displayName = 'ActionButton';

+ 4
- 1
src/modules/option/index.ts 查看文件

@@ -1,3 +1,6 @@
export * from './components/Checkbox';
export * from './components/DropdownSelect';
export * from './components/RadioButton';
export * from './components/RadioTickBox';
export * from './components/ToggleButton';
export * from './components/ToggleSwitch';
export * from './components/ToggleTickBox';

+ 14
- 0
src/modules/option/web-option-react.test.ts 查看文件

@@ -0,0 +1,14 @@
import * as WebOptionReact from '.';

describe('web-option-react', () => {
it.each([
'DropdownSelect',
'RadioButton',
'RadioTickBox',
'ToggleButton',
'ToggleSwitch',
'ToggleTickBox',
])('should export %s', (namedExport) => {
expect(WebOptionReact).toHaveProperty(namedExport);
});
});

+ 1
- 1
src/pages/categories/navigation/index.tsx 查看文件

@@ -1,7 +1,7 @@
import { NextPage } from 'next';
import { ButtonSize, ButtonVariant } from '@tesseract-design/web-base-button';
import * as Navigation from '@tesseract-design/web-navigation-react';
import * as Info from '@tesseract-design/web-info-react';
import * as Info from '@tesseract-design/web-information-react';

const ActionPage: NextPage = () => {
return (


+ 52
- 52
src/pages/categories/option/index.tsx 查看文件

@@ -614,21 +614,21 @@ const OptionPage: NextPage<Props> = ({
<div>
<div className="grid md:grid-cols-2 gap-4 my-4">
<div>
<Option.Checkbox
<Option.ToggleTickBox
block
subtext="Subtext"
>
Checkbox
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
<div>
<Option.Checkbox
<Option.ToggleTickBox
block
indeterminate
subtext="Subtext"
>
Checkbox
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
</div>
</div>
@@ -638,23 +638,23 @@ const OptionPage: NextPage<Props> = ({
<div>
<div className="grid md:grid-cols-2 gap-4 my-4">
<div>
<Option.Checkbox
<Option.ToggleTickBox
block
subtext="Subtext"
appearance={CheckControlAppearance.SWITCH}
>
Checkbox
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
<div>
<Option.Checkbox
<Option.ToggleTickBox
block
indeterminate
subtext="Subtext"
appearance={CheckControlAppearance.SWITCH}
>
Checkbox
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
</div>
</div>
@@ -664,72 +664,72 @@ const OptionPage: NextPage<Props> = ({
<div>
<div className="grid md:grid-cols-2 gap-4 my-4">
<div>
<Option.Checkbox
<Option.ToggleTickBox
block
appearance={CheckControlAppearance.BUTTON}
>
Button
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
<div>
<Option.Checkbox
<Option.ToggleTickBox
variant={ButtonVariant.FILLED}
block
appearance={CheckControlAppearance.BUTTON}
>
Button
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
<div>
<Option.Checkbox
<Option.ToggleTickBox
border
block
appearance={CheckControlAppearance.BUTTON}
>
Button
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
<div>
<Option.Checkbox
<Option.ToggleTickBox
border
variant={ButtonVariant.FILLED}
block
appearance={CheckControlAppearance.BUTTON}
>
Button
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
<div>
<Option.Checkbox
<Option.ToggleTickBox
block
disabled
appearance={CheckControlAppearance.BUTTON}
>
Button
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
<div>
<Option.Checkbox
<Option.ToggleTickBox
variant={ButtonVariant.FILLED}
block
disabled
appearance={CheckControlAppearance.BUTTON}
>
Button
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
<div>
<Option.Checkbox
<Option.ToggleTickBox
border
block
disabled
appearance={CheckControlAppearance.BUTTON}
>
Button
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
<div>
<Option.Checkbox
<Option.ToggleTickBox
border
variant={ButtonVariant.FILLED}
block
@@ -737,20 +737,20 @@ const OptionPage: NextPage<Props> = ({
appearance={CheckControlAppearance.BUTTON}
>
Button
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
<div>
<Option.Checkbox
<Option.ToggleTickBox
border
block
appearance={CheckControlAppearance.BUTTON}
indeterminate
>
Button
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
<div>
<Option.Checkbox
<Option.ToggleTickBox
border
variant={ButtonVariant.FILLED}
block
@@ -758,7 +758,7 @@ const OptionPage: NextPage<Props> = ({
indeterminate
>
Button
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
</div>
</div>
@@ -768,17 +768,17 @@ const OptionPage: NextPage<Props> = ({
<div>
<div className="grid md:grid-cols-2 gap-4 my-4">
<div>
<Option.Checkbox
<Option.ToggleTickBox
block
border
size={ButtonSize.SMALL}
appearance={CheckControlAppearance.BUTTON}
>
Button
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
<div>
<Option.Checkbox
<Option.ToggleTickBox
block
border
variant={ButtonVariant.FILLED}
@@ -786,20 +786,20 @@ const OptionPage: NextPage<Props> = ({
appearance={CheckControlAppearance.BUTTON}
>
Button
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
<div>
<Option.Checkbox
<Option.ToggleTickBox
block
border
size={ButtonSize.MEDIUM}
appearance={CheckControlAppearance.BUTTON}
>
Button
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
<div>
<Option.Checkbox
<Option.ToggleTickBox
block
border
variant={ButtonVariant.FILLED}
@@ -807,20 +807,20 @@ const OptionPage: NextPage<Props> = ({
appearance={CheckControlAppearance.BUTTON}
>
Button
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
<div>
<Option.Checkbox
<Option.ToggleTickBox
block
border
size={ButtonSize.LARGE}
appearance={CheckControlAppearance.BUTTON}
>
Button
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
<div>
<Option.Checkbox
<Option.ToggleTickBox
block
border
variant={ButtonVariant.FILLED}
@@ -828,7 +828,7 @@ const OptionPage: NextPage<Props> = ({
appearance={CheckControlAppearance.BUTTON}
>
Button
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
</div>
</div>
@@ -838,17 +838,17 @@ const OptionPage: NextPage<Props> = ({
<div>
<div className="grid md:grid-cols-2 gap-4 my-4">
<div>
<Option.Checkbox
<Option.ToggleTickBox
block
compact
border
appearance={CheckControlAppearance.BUTTON}
>
Button
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
<div>
<Option.Checkbox
<Option.ToggleTickBox
block
compact
border
@@ -856,10 +856,10 @@ const OptionPage: NextPage<Props> = ({
appearance={CheckControlAppearance.BUTTON}
>
Button
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
<div>
<Option.Checkbox
<Option.ToggleTickBox
block
compact
border
@@ -871,10 +871,10 @@ const OptionPage: NextPage<Props> = ({
}
>
Button
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
<div>
<Option.Checkbox
<Option.ToggleTickBox
block
compact
border
@@ -887,10 +887,10 @@ const OptionPage: NextPage<Props> = ({
}
>
Button
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
<div>
<Option.Checkbox
<Option.ToggleTickBox
block
compact
border
@@ -903,10 +903,10 @@ const OptionPage: NextPage<Props> = ({
}
>
Button
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
<div>
<Option.Checkbox
<Option.ToggleTickBox
block
compact
border
@@ -920,7 +920,7 @@ const OptionPage: NextPage<Props> = ({
}
>
Button
</Option.Checkbox>
</Option.ToggleTickBox>
</div>
</div>
</div>


+ 2
- 1
tsconfig.json 查看文件

@@ -17,9 +17,10 @@
"paths": {
"@tesseract-design/web-action-react": ["./src/modules/action"],
"@tesseract-design/web-freeform-react": ["./src/modules/freeform"],
"@tesseract-design/web-info-react": ["./src/modules/info"],
"@tesseract-design/web-information-react": ["./src/modules/information"],
"@tesseract-design/web-navigation-react": ["./src/modules/navigation"],
"@tesseract-design/web-option-react": ["./src/modules/option"],
"@tesseract-design/web-base-badge": ["./src/modules/base-badge"],
"@tesseract-design/web-base-button": ["./src/modules/base-button"],
"@tesseract-design/web-base-checkcontrol": ["./src/modules/base-checkcontrol"],
"@tesseract-design/web-base-textcontrol": ["./src/modules/base-textcontrol"],


+ 4
- 1
tsconfig.test.json 查看文件

@@ -1,3 +1,6 @@
{
"extends": "./tsconfig.json"
"extends": "./tsconfig.json",
"compilerOptions": {
"jsx": "react"
}
}

+ 14
- 0
yarn.lock 查看文件

@@ -317,6 +317,13 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.12.5":
version "7.16.3"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5"
integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.16.0", "@babel/template@^7.3.3":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.0.tgz#d16a35ebf4cd74e202083356fab21dd89363ddd6"
@@ -707,6 +714,13 @@
dependencies:
"@sinonjs/commons" "^1.7.0"
"@testing-library/user-event@^13.5.0":
version "13.5.0"
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.5.0.tgz#69d77007f1e124d55314a2b73fd204b333b13295"
integrity sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==
dependencies:
"@babel/runtime" "^7.12.5"
"@tootallnate/once@1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"


正在加载...
取消
保存