@charcoal-ui/react 1.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +18 -0
- package/package.json +75 -0
- package/src/_lib/compat.ts +15 -0
- package/src/_lib/index.ts +35 -0
- package/src/components/Button/index.story.tsx +187 -0
- package/src/components/Button/index.tsx +122 -0
- package/src/components/Clickable/index.story.tsx +18 -0
- package/src/components/Clickable/index.tsx +108 -0
- package/src/components/FieldLabel/index.tsx +73 -0
- package/src/components/IconButton/index.story.tsx +51 -0
- package/src/components/IconButton/index.tsx +118 -0
- package/src/components/Radio/index.story.tsx +79 -0
- package/src/components/Radio/index.test.tsx +149 -0
- package/src/components/Radio/index.tsx +202 -0
- package/src/components/Switch/index.story.tsx +43 -0
- package/src/components/Switch/index.tsx +116 -0
- package/src/components/TextField/index.story.tsx +83 -0
- package/src/components/TextField/index.tsx +386 -0
- package/src/components/a11y.test.tsx +82 -0
- package/src/core/ComponentAbstraction.tsx +47 -0
- package/src/index.ts +21 -0
- package/src/styled.ts +3 -0
- package/src/type.d.ts +16 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import styled, { css } from 'styled-components'
|
|
3
|
+
import {
|
|
4
|
+
LinkProps,
|
|
5
|
+
useComponentAbstraction,
|
|
6
|
+
} from '../../core/ComponentAbstraction'
|
|
7
|
+
import { disabledSelector } from '@charcoal-ui/utils'
|
|
8
|
+
|
|
9
|
+
interface BaseProps {
|
|
10
|
+
/**
|
|
11
|
+
* クリックの無効化
|
|
12
|
+
*/
|
|
13
|
+
disabled?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface LinkBaseProps {
|
|
17
|
+
/**
|
|
18
|
+
* リンクのURL。指定するとbuttonタグではなくaタグとして描画される
|
|
19
|
+
*/
|
|
20
|
+
to: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ClickableProps =
|
|
24
|
+
| (BaseProps & Omit<React.ComponentPropsWithoutRef<'button'>, 'disabled'>)
|
|
25
|
+
| (BaseProps & LinkBaseProps & Omit<LinkProps, 'to'>)
|
|
26
|
+
export type ClickableElement = HTMLButtonElement & HTMLAnchorElement
|
|
27
|
+
|
|
28
|
+
const Clickable = React.forwardRef<ClickableElement, ClickableProps>(
|
|
29
|
+
function Clickable(props, ref) {
|
|
30
|
+
const { Link } = useComponentAbstraction()
|
|
31
|
+
if ('to' in props) {
|
|
32
|
+
const { onClick, disabled = false, ...rest } = props
|
|
33
|
+
return (
|
|
34
|
+
<A<typeof Link>
|
|
35
|
+
{...rest}
|
|
36
|
+
as={disabled ? undefined : Link}
|
|
37
|
+
onClick={disabled ? undefined : onClick}
|
|
38
|
+
aria-disabled={disabled}
|
|
39
|
+
ref={ref}
|
|
40
|
+
/>
|
|
41
|
+
)
|
|
42
|
+
} else {
|
|
43
|
+
return <Button {...props} ref={ref} />
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
export default Clickable
|
|
48
|
+
|
|
49
|
+
const clickableCss = css`
|
|
50
|
+
/* Clickable style */
|
|
51
|
+
cursor: pointer;
|
|
52
|
+
|
|
53
|
+
${disabledSelector} {
|
|
54
|
+
cursor: default;
|
|
55
|
+
}
|
|
56
|
+
`
|
|
57
|
+
|
|
58
|
+
const Button = styled.button`
|
|
59
|
+
/* Reset button appearance */
|
|
60
|
+
appearance: none;
|
|
61
|
+
background: transparent;
|
|
62
|
+
padding: 0;
|
|
63
|
+
border-style: none;
|
|
64
|
+
outline: none;
|
|
65
|
+
color: inherit;
|
|
66
|
+
text-rendering: inherit;
|
|
67
|
+
letter-spacing: inherit;
|
|
68
|
+
word-spacing: inherit;
|
|
69
|
+
|
|
70
|
+
&:focus {
|
|
71
|
+
outline: none;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Change the font styles in all browsers. */
|
|
75
|
+
font: inherit;
|
|
76
|
+
|
|
77
|
+
/* Remove the margin in Firefox and Safari. */
|
|
78
|
+
margin: 0;
|
|
79
|
+
|
|
80
|
+
/* Show the overflow in Edge. */
|
|
81
|
+
overflow: visible;
|
|
82
|
+
|
|
83
|
+
/* Remove the inheritance of text transform in Firefox. */
|
|
84
|
+
text-transform: none;
|
|
85
|
+
|
|
86
|
+
/* Remove the inner border and padding in Firefox. */
|
|
87
|
+
&::-moz-focus-inner {
|
|
88
|
+
border-style: none;
|
|
89
|
+
padding: 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
${clickableCss}
|
|
93
|
+
`
|
|
94
|
+
|
|
95
|
+
const A = styled.span`
|
|
96
|
+
/* Reset a-tag appearance */
|
|
97
|
+
color: inherit;
|
|
98
|
+
|
|
99
|
+
&:focus {
|
|
100
|
+
outline: none;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.text {
|
|
104
|
+
top: calc(1em + 2em);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
${clickableCss}
|
|
108
|
+
`
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import styled from 'styled-components'
|
|
3
|
+
import createTheme from '@charcoal-ui/styled'
|
|
4
|
+
|
|
5
|
+
export interface FieldLabelProps
|
|
6
|
+
extends React.LabelHTMLAttributes<HTMLLabelElement> {
|
|
7
|
+
readonly className?: string
|
|
8
|
+
readonly label: string
|
|
9
|
+
readonly subLabel?: React.ReactNode
|
|
10
|
+
readonly required?: boolean
|
|
11
|
+
// TODO: 翻訳用のContextで注入する
|
|
12
|
+
readonly requiredText?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const FieldLabel = React.forwardRef<HTMLLabelElement, FieldLabelProps>(
|
|
16
|
+
function FieldLabel(
|
|
17
|
+
{
|
|
18
|
+
style,
|
|
19
|
+
className,
|
|
20
|
+
label,
|
|
21
|
+
required = false,
|
|
22
|
+
requiredText,
|
|
23
|
+
subLabel,
|
|
24
|
+
...labelProps
|
|
25
|
+
},
|
|
26
|
+
ref
|
|
27
|
+
) {
|
|
28
|
+
return (
|
|
29
|
+
<FieldLabelWrapper style={style} className={className}>
|
|
30
|
+
<Label ref={ref} {...labelProps}>
|
|
31
|
+
{label}
|
|
32
|
+
</Label>
|
|
33
|
+
{required && <RequiredText>{requiredText}</RequiredText>}
|
|
34
|
+
<SubLabelClickable>
|
|
35
|
+
<span>{subLabel}</span>
|
|
36
|
+
</SubLabelClickable>
|
|
37
|
+
</FieldLabelWrapper>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
export default FieldLabel
|
|
43
|
+
|
|
44
|
+
const theme = createTheme(styled)
|
|
45
|
+
|
|
46
|
+
const Label = styled.label`
|
|
47
|
+
${theme((o) => [o.typography(14).bold, o.font.text1])}
|
|
48
|
+
`
|
|
49
|
+
|
|
50
|
+
const RequiredText = styled.span`
|
|
51
|
+
${theme((o) => [o.typography(14), o.font.text3])}
|
|
52
|
+
`
|
|
53
|
+
|
|
54
|
+
const SubLabelClickable = styled.div`
|
|
55
|
+
${theme((o) => [
|
|
56
|
+
o.typography(14),
|
|
57
|
+
o.font.text3.hover.press,
|
|
58
|
+
o.outline.default.focus,
|
|
59
|
+
])}
|
|
60
|
+
`
|
|
61
|
+
|
|
62
|
+
const FieldLabelWrapper = styled.div`
|
|
63
|
+
display: inline-flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
|
|
66
|
+
> ${RequiredText} {
|
|
67
|
+
${theme((o) => o.margin.left(4))}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
> ${SubLabelClickable} {
|
|
71
|
+
${theme((o) => o.margin.left('auto'))}
|
|
72
|
+
}
|
|
73
|
+
`
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import type { Story } from '../../_lib/compat'
|
|
3
|
+
import '@charcoal-ui/icons'
|
|
4
|
+
import IconButton from '.'
|
|
5
|
+
import type { KnownIconType } from '@charcoal-ui/icons'
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
title: 'IconButton',
|
|
9
|
+
component: IconButton,
|
|
10
|
+
argTypes: {
|
|
11
|
+
variant: {
|
|
12
|
+
control: {
|
|
13
|
+
type: 'inline-radio',
|
|
14
|
+
options: ['Default', 'Overlay'],
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
size: {
|
|
18
|
+
control: {
|
|
19
|
+
type: 'inline-radio',
|
|
20
|
+
options: ['M', 'S', 'XS'],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type Props = {
|
|
27
|
+
variant?: 'Default' | 'Overlay'
|
|
28
|
+
size?: 'M' | 'S' | 'XS'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const Template: Story<Props> = (props) => {
|
|
32
|
+
const { size } = props
|
|
33
|
+
const icon: keyof KnownIconType = {
|
|
34
|
+
XS: '16/Remove' as const,
|
|
35
|
+
S: '24/Close' as const,
|
|
36
|
+
M: '24/Close' as const,
|
|
37
|
+
}[size ?? 'M']
|
|
38
|
+
return <IconButton title="close" {...props} icon={icon} />
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const DefaultM: Story<Props> = Template.bind({})
|
|
42
|
+
DefaultM.args = {
|
|
43
|
+
variant: 'Default',
|
|
44
|
+
size: 'M',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const OverlayM: Story<Props> = Template.bind({})
|
|
48
|
+
OverlayM.args = {
|
|
49
|
+
variant: 'Overlay',
|
|
50
|
+
size: 'M',
|
|
51
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import styled from 'styled-components'
|
|
3
|
+
import { theme } from '../../styled'
|
|
4
|
+
import Clickable, { ClickableElement, ClickableProps } from '../Clickable'
|
|
5
|
+
import type { KnownIconType } from '@charcoal-ui/icons'
|
|
6
|
+
|
|
7
|
+
type Variant = 'Default' | 'Overlay'
|
|
8
|
+
type Size = 'XS' | 'S' | 'M'
|
|
9
|
+
|
|
10
|
+
interface StyledProps {
|
|
11
|
+
readonly variant?: Variant
|
|
12
|
+
readonly size?: Size
|
|
13
|
+
readonly icon: keyof KnownIconType
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type IconButtonProps = StyledProps & ClickableProps
|
|
17
|
+
|
|
18
|
+
const IconButton = React.forwardRef<ClickableElement, IconButtonProps>(
|
|
19
|
+
function IconButtonInner(
|
|
20
|
+
{ variant = 'Default', size = 'M', icon, ...rest }: IconButtonProps,
|
|
21
|
+
ref
|
|
22
|
+
) {
|
|
23
|
+
validateIconSize(size, icon)
|
|
24
|
+
return (
|
|
25
|
+
<StyledIconButton {...rest} ref={ref} variant={variant} size={size}>
|
|
26
|
+
<pixiv-icon name={icon} />
|
|
27
|
+
</StyledIconButton>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
export default IconButton
|
|
33
|
+
|
|
34
|
+
const StyledIconButton = styled(Clickable).attrs<
|
|
35
|
+
Required<StyledProps>,
|
|
36
|
+
ReturnType<typeof styledProps>
|
|
37
|
+
>(styledProps)`
|
|
38
|
+
user-select: none;
|
|
39
|
+
|
|
40
|
+
width: ${(p) => p.width}px;
|
|
41
|
+
height: ${(p) => p.height}px;
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
justify-content: center;
|
|
45
|
+
|
|
46
|
+
${({ font, background }) =>
|
|
47
|
+
theme((o) => [
|
|
48
|
+
o.font[font],
|
|
49
|
+
o.bg[background].hover.press,
|
|
50
|
+
o.disabled,
|
|
51
|
+
o.borderRadius('oval'),
|
|
52
|
+
o.outline.default.focus,
|
|
53
|
+
])}
|
|
54
|
+
`
|
|
55
|
+
|
|
56
|
+
function styledProps(props: Required<StyledProps>) {
|
|
57
|
+
return {
|
|
58
|
+
...props,
|
|
59
|
+
...variantToProps(props.variant),
|
|
60
|
+
...sizeToProps(props.size),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function variantToProps(variant: Variant) {
|
|
65
|
+
switch (variant) {
|
|
66
|
+
case 'Default':
|
|
67
|
+
return { font: 'text3', background: 'transparent' } as const
|
|
68
|
+
case 'Overlay':
|
|
69
|
+
return { font: 'text5', background: 'surface4' } as const
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function sizeToProps(size: Size) {
|
|
74
|
+
switch (size) {
|
|
75
|
+
case 'XS':
|
|
76
|
+
return {
|
|
77
|
+
width: 20,
|
|
78
|
+
height: 20,
|
|
79
|
+
}
|
|
80
|
+
case 'S':
|
|
81
|
+
return {
|
|
82
|
+
width: 32,
|
|
83
|
+
height: 32,
|
|
84
|
+
}
|
|
85
|
+
case 'M':
|
|
86
|
+
return {
|
|
87
|
+
width: 40,
|
|
88
|
+
height: 40,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* validates matches of size and icon
|
|
95
|
+
*/
|
|
96
|
+
function validateIconSize(size: Size, icon: keyof KnownIconType) {
|
|
97
|
+
let requiredIconSize: string
|
|
98
|
+
switch (size) {
|
|
99
|
+
case 'XS':
|
|
100
|
+
requiredIconSize = '16'
|
|
101
|
+
break
|
|
102
|
+
case 'S':
|
|
103
|
+
case 'M':
|
|
104
|
+
requiredIconSize = '24'
|
|
105
|
+
break
|
|
106
|
+
}
|
|
107
|
+
// アイコン名は サイズ/名前
|
|
108
|
+
const result = /^\d*/u.exec(icon)
|
|
109
|
+
if (result == null) {
|
|
110
|
+
throw new Error('Invalid icon name')
|
|
111
|
+
}
|
|
112
|
+
const [iconSize] = result
|
|
113
|
+
if (iconSize !== requiredIconSize) {
|
|
114
|
+
console.warn(
|
|
115
|
+
`IconButton with size "${size}" expect icon size "${requiredIconSize}, but got "${iconSize}"`
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { action } from '@storybook/addon-actions'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { css } from 'styled-components'
|
|
4
|
+
import { Story } from '../../_lib/compat'
|
|
5
|
+
import Radio, { RadioGroup } from '.'
|
|
6
|
+
import { px } from '@charcoal-ui/utils'
|
|
7
|
+
|
|
8
|
+
const options = ['value1', 'value2']
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
title: 'Radio',
|
|
12
|
+
component: Radio,
|
|
13
|
+
argTypes: {
|
|
14
|
+
defaultValue: {
|
|
15
|
+
control: { type: 'select', options },
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
defaultValue: string
|
|
22
|
+
hasError: boolean
|
|
23
|
+
parentDisabled: boolean
|
|
24
|
+
childDisabled: boolean
|
|
25
|
+
forceChecked: boolean
|
|
26
|
+
readonly: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const DefaultStory = ({
|
|
30
|
+
defaultValue,
|
|
31
|
+
forceChecked,
|
|
32
|
+
hasError,
|
|
33
|
+
parentDisabled,
|
|
34
|
+
childDisabled,
|
|
35
|
+
readonly,
|
|
36
|
+
}: Props) => (
|
|
37
|
+
<div
|
|
38
|
+
css={css`
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: column;
|
|
41
|
+
gap: ${({ theme }) => px(theme.spacing[24])};
|
|
42
|
+
`}
|
|
43
|
+
>
|
|
44
|
+
{['name1', 'name2', 'name3'].map((name) => (
|
|
45
|
+
<RadioGroup
|
|
46
|
+
key={name}
|
|
47
|
+
label={`選択肢-${name}`}
|
|
48
|
+
name={name}
|
|
49
|
+
defaultValue={defaultValue}
|
|
50
|
+
onChange={action('onChange')}
|
|
51
|
+
disabled={parentDisabled}
|
|
52
|
+
readonly={readonly}
|
|
53
|
+
hasError={hasError}
|
|
54
|
+
>
|
|
55
|
+
{options.map((option) => (
|
|
56
|
+
<Radio
|
|
57
|
+
key={option}
|
|
58
|
+
value={option}
|
|
59
|
+
disabled={childDisabled}
|
|
60
|
+
forceChecked={forceChecked}
|
|
61
|
+
>
|
|
62
|
+
{name}({option})を選ぶ
|
|
63
|
+
</Radio>
|
|
64
|
+
))}
|
|
65
|
+
</RadioGroup>
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
export const Normal: Story<Props> = DefaultStory.bind({})
|
|
71
|
+
|
|
72
|
+
Normal.args = {
|
|
73
|
+
defaultValue: options[0],
|
|
74
|
+
hasError: false,
|
|
75
|
+
parentDisabled: false,
|
|
76
|
+
childDisabled: false,
|
|
77
|
+
forceChecked: false,
|
|
78
|
+
readonly: false,
|
|
79
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { ThemeProvider } from 'styled-components'
|
|
4
|
+
import Radio, { RadioGroup } from '.'
|
|
5
|
+
import createTheme from '@charcoal-ui/styled'
|
|
6
|
+
const { light } = createTheme
|
|
7
|
+
|
|
8
|
+
describe('Radio', () => {
|
|
9
|
+
describe('__DEV__ mode', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
12
|
+
// @ts-expect-error
|
|
13
|
+
global.__DEV__ = {}
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('<Radio> is not surrounded by <RadioGroup>', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
console.error = jest.fn()
|
|
19
|
+
|
|
20
|
+
render(
|
|
21
|
+
<ThemeProvider theme={light}>
|
|
22
|
+
<Radio value="option1" />
|
|
23
|
+
</ThemeProvider>
|
|
24
|
+
)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('console.error()', () => {
|
|
28
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
29
|
+
expect.stringMatching(/Perhaps you forgot to wrap with <RadioGroup>/u)
|
|
30
|
+
)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('defaultValue is the first option', () => {
|
|
36
|
+
let option1: HTMLInputElement
|
|
37
|
+
let option2: HTMLInputElement
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
render(<TestComponent defaultValue="option1" />)
|
|
41
|
+
|
|
42
|
+
option1 = screen.getByDisplayValue('option1')
|
|
43
|
+
option2 = screen.getByDisplayValue('option2')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('the first <Radio> is checked', () => {
|
|
47
|
+
expect(option1.checked).toBeTruthy()
|
|
48
|
+
expect(option2.checked).toBeFalsy()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('clicking the second', () => {
|
|
52
|
+
it('the second <Radio> is checked', () => {
|
|
53
|
+
fireEvent.click(option2)
|
|
54
|
+
|
|
55
|
+
expect(option1.checked).toBeFalsy()
|
|
56
|
+
expect(option2.checked).toBeTruthy()
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('<RadioGroup> is disabled', () => {
|
|
62
|
+
it('all <Radio>s are disabled', () => {
|
|
63
|
+
render(<TestComponent defaultValue="option1" radioGroupDisabled />)
|
|
64
|
+
screen.getAllByRole<HTMLInputElement>('radio').forEach((element) => {
|
|
65
|
+
expect(element.disabled).toBeTruthy()
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('the first <Radio> is disabled', () => {
|
|
71
|
+
let option1: HTMLInputElement
|
|
72
|
+
let option2: HTMLInputElement
|
|
73
|
+
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
render(<TestComponent defaultValue="option1" option1Disabled />)
|
|
76
|
+
|
|
77
|
+
option1 = screen.getByDisplayValue('option1')
|
|
78
|
+
option2 = screen.getByDisplayValue('option2')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('<input> in the first <Radio> is disabled', () => {
|
|
82
|
+
expect(option1.checked).toBeTruthy()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('No other <input> is disabled', () => {
|
|
86
|
+
expect(option2.checked).toBeFalsy()
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe('has readonly in <RadioGroup>', () => {
|
|
91
|
+
let option1: HTMLInputElement
|
|
92
|
+
let option2: HTMLInputElement
|
|
93
|
+
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
render(<TestComponent defaultValue="option1" readonly />)
|
|
96
|
+
|
|
97
|
+
option1 = screen.getByDisplayValue('option1')
|
|
98
|
+
option2 = screen.getByDisplayValue('option2')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('the first <Radio> is checked', () => {
|
|
102
|
+
expect(option1.checked).toBeTruthy()
|
|
103
|
+
expect(option1.disabled).toBeFalsy()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('Non-first <Radio>s are disabled', () => {
|
|
107
|
+
expect(option2.disabled).toBeTruthy()
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
function TestComponent({
|
|
113
|
+
defaultValue,
|
|
114
|
+
onChange = jest.fn(),
|
|
115
|
+
radioGroupDisabled = false,
|
|
116
|
+
readonly = false,
|
|
117
|
+
hasError = false,
|
|
118
|
+
option1Disabled = false,
|
|
119
|
+
option2Disabled = false,
|
|
120
|
+
}: {
|
|
121
|
+
defaultValue: string
|
|
122
|
+
onChange?: () => void
|
|
123
|
+
radioGroupDisabled?: boolean
|
|
124
|
+
readonly?: boolean
|
|
125
|
+
hasError?: boolean
|
|
126
|
+
option1Disabled?: boolean
|
|
127
|
+
option2Disabled?: boolean
|
|
128
|
+
}) {
|
|
129
|
+
return (
|
|
130
|
+
<ThemeProvider theme={light}>
|
|
131
|
+
<RadioGroup
|
|
132
|
+
label="テスト項目"
|
|
133
|
+
name="test"
|
|
134
|
+
defaultValue={defaultValue}
|
|
135
|
+
onChange={onChange}
|
|
136
|
+
disabled={radioGroupDisabled}
|
|
137
|
+
readonly={readonly}
|
|
138
|
+
hasError={hasError}
|
|
139
|
+
>
|
|
140
|
+
<Radio value="option1" disabled={option1Disabled}>
|
|
141
|
+
option1を選ぶ
|
|
142
|
+
</Radio>
|
|
143
|
+
<Radio value="option2" disabled={option2Disabled}>
|
|
144
|
+
option2を選ぶ
|
|
145
|
+
</Radio>
|
|
146
|
+
</RadioGroup>
|
|
147
|
+
</ThemeProvider>
|
|
148
|
+
)
|
|
149
|
+
}
|