Browse Source

Add tests, refactor

Implement tests to increase code coverage

The option module was modified to split components and avoid
overloading.
master
TheoryOfNekomata 2 years ago
parent
commit
d2b8df726c
48 changed files with 2334 additions and 653 deletions
  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 View File

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

.idea/
.idea/
coverage/

+ 45
- 0
docs/philosophy.md View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

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

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

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

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

Badge.displayName = 'Badge';

src/modules/info/index.ts → src/modules/information/index.ts View File


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

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

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

+ 14
- 0
yarn.lock View File

@@ -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"


Loading…
Cancel
Save