@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.
@@ -0,0 +1,202 @@
1
+ import React, { useCallback, useContext, useState } from 'react'
2
+ import styled from 'styled-components'
3
+ import warning from 'warning'
4
+ import { theme } from '../../styled'
5
+ import { px } from '@charcoal-ui/utils'
6
+
7
+ export type RadioProps = React.PropsWithChildren<{
8
+ value: string
9
+ forceChecked?: boolean
10
+ disabled?: boolean
11
+ }>
12
+
13
+ export default function Radio({
14
+ value,
15
+ forceChecked = false,
16
+ disabled = false,
17
+ children,
18
+ }: RadioProps) {
19
+ const {
20
+ name,
21
+ selected,
22
+ disabled: isParentDisabled,
23
+ readonly,
24
+ hasError,
25
+ onChange,
26
+ } = useContext(RadioGroupContext)
27
+
28
+ warning(
29
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
30
+ name !== undefined,
31
+ `"name" is not Provided for <Radio>. Perhaps you forgot to wrap with <RadioGroup> ?`
32
+ )
33
+
34
+ const isSelected = value === selected
35
+ const isDisabled = disabled || isParentDisabled
36
+ const isReadonly = readonly && !isSelected
37
+
38
+ const handleChange = useCallback(
39
+ (e: React.ChangeEvent<HTMLInputElement>) => {
40
+ onChange(e.currentTarget.value)
41
+ },
42
+ [onChange]
43
+ )
44
+
45
+ return (
46
+ <RadioRoot aria-disabled={isDisabled || isReadonly}>
47
+ <RadioInput
48
+ name={name}
49
+ value={value}
50
+ checked={forceChecked || isSelected}
51
+ hasError={hasError}
52
+ onChange={handleChange}
53
+ disabled={isDisabled || isReadonly}
54
+ />
55
+ {children != null && <RadioLabel>{children}</RadioLabel>}
56
+ </RadioRoot>
57
+ )
58
+ }
59
+
60
+ const RadioRoot = styled.label`
61
+ display: grid;
62
+ grid-template-columns: auto 1fr;
63
+ grid-gap: ${({ theme }) => px(theme.spacing[4])};
64
+ align-items: center;
65
+ cursor: pointer;
66
+
67
+ ${theme((o) => [o.disabled])}
68
+ `
69
+
70
+ export const RadioInput = styled.input.attrs({ type: 'radio' })<{
71
+ hasError?: boolean
72
+ }>`
73
+ /** Make prior to browser default style */
74
+ &[type='radio'] {
75
+ appearance: none;
76
+ display: block;
77
+ box-sizing: border-box;
78
+
79
+ padding: 6px;
80
+
81
+ width: 20px;
82
+ height: 20px;
83
+
84
+ ${({ hasError = false }) =>
85
+ theme((o) => [
86
+ o.borderRadius('oval'),
87
+ o.bg.text5.hover.press,
88
+ hasError && o.outline.assertive,
89
+ ])};
90
+
91
+ &:not(:checked) {
92
+ border-width: 2px;
93
+ border-style: solid;
94
+ border-color: ${({ theme }) => theme.color.text4};
95
+ }
96
+
97
+ &:checked {
98
+ ${theme((o) => o.bg.brand.hover.press)}
99
+
100
+ &::after {
101
+ content: '';
102
+ display: block;
103
+ width: 8px;
104
+ height: 8px;
105
+ pointer-events: none;
106
+
107
+ ${theme((o) => [o.bg.text5.hover.press, o.borderRadius('oval')])}
108
+ }
109
+ }
110
+
111
+ ${theme((o) => o.outline.default.focus)}
112
+ }
113
+ `
114
+
115
+ const RadioLabel = styled.div`
116
+ ${theme((o) => [o.typography(14)])}
117
+ `
118
+
119
+ export type RadioGroupProps = React.PropsWithChildren<{
120
+ className?: string
121
+ defaultValue?: string
122
+ label: string
123
+ name: string
124
+ onChange(next: string): void
125
+ disabled?: boolean
126
+ readonly?: boolean
127
+ hasError?: boolean
128
+ }>
129
+
130
+ // TODO: use (or polyfill) flex gap
131
+ const StyledRadioGroup = styled.div`
132
+ display: grid;
133
+ grid-template-columns: 1fr;
134
+ grid-gap: ${({ theme }) => px(theme.spacing[8])};
135
+ `
136
+
137
+ interface RadioGroupContext {
138
+ name: string
139
+ selected?: string
140
+ disabled: boolean
141
+ readonly: boolean
142
+ hasError: boolean
143
+ onChange: (next: string) => void
144
+ }
145
+
146
+ const RadioGroupContext = React.createContext<RadioGroupContext>({
147
+ name: undefined as never,
148
+ selected: undefined,
149
+ disabled: false,
150
+ readonly: false,
151
+ hasError: false,
152
+ onChange() {
153
+ throw new Error(
154
+ 'Cannot find onChange() handler. Perhaps you forgot to wrap with <RadioGroup> ?'
155
+ )
156
+ },
157
+ })
158
+
159
+ export function RadioGroup({
160
+ className,
161
+ defaultValue,
162
+ label,
163
+ name,
164
+ onChange,
165
+ disabled,
166
+ readonly,
167
+ hasError,
168
+ children,
169
+ }: RadioGroupProps) {
170
+ const [selected, setSelected] = useState(defaultValue)
171
+
172
+ const handleChange = useCallback(
173
+ (next: string) => {
174
+ setSelected(next)
175
+ onChange(next)
176
+ },
177
+ [onChange]
178
+ )
179
+
180
+ return (
181
+ <RadioGroupContext.Provider
182
+ value={{
183
+ name,
184
+ selected,
185
+ disabled: disabled ?? false,
186
+ readonly: readonly ?? false,
187
+ hasError: hasError ?? false,
188
+ onChange: handleChange,
189
+ }}
190
+ >
191
+ <StyledRadioGroup
192
+ role="radiogroup"
193
+ aria-orientation="vertical"
194
+ aria-label={label}
195
+ aria-invalid={hasError}
196
+ className={className}
197
+ >
198
+ {children}
199
+ </StyledRadioGroup>
200
+ </RadioGroupContext.Provider>
201
+ )
202
+ }
@@ -0,0 +1,43 @@
1
+ import { action } from '@storybook/addon-actions'
2
+ import React from 'react'
3
+ import { Story } from '../../_lib/compat'
4
+ import Switch from '.'
5
+
6
+ export default {
7
+ title: 'Switch',
8
+ component: Switch,
9
+ }
10
+
11
+ interface Props {
12
+ checked: boolean
13
+ disabled: boolean
14
+ }
15
+
16
+ export const Labelled: Story<Props> = (props: Props) => (
17
+ <div>
18
+ <Switch {...props} name="name" onChange={action('onChange')}>
19
+ 選択する
20
+ </Switch>
21
+ </div>
22
+ )
23
+
24
+ Labelled.args = {
25
+ checked: false,
26
+ disabled: false,
27
+ }
28
+
29
+ export const Unlabelled: Story<Props> = (props: Props) => (
30
+ <div>
31
+ <Switch
32
+ {...props}
33
+ name="name"
34
+ label="label"
35
+ onChange={action('onChange')}
36
+ />
37
+ </div>
38
+ )
39
+
40
+ Unlabelled.args = {
41
+ checked: false,
42
+ disabled: false,
43
+ }
@@ -0,0 +1,116 @@
1
+ import { useSwitch } from '@react-aria/switch'
2
+ import type { AriaSwitchProps } from '@react-types/switch'
3
+ import React, { useRef, useMemo } from 'react'
4
+ import { useToggleState } from 'react-stately'
5
+ import styled from 'styled-components'
6
+ import { theme } from '../../styled'
7
+ import { disabledSelector, px } from '@charcoal-ui/utils'
8
+
9
+ export type SwitchProps = {
10
+ name: string
11
+ className?: string
12
+ value?: string
13
+ checked?: boolean
14
+ disabled?: boolean
15
+ onChange(checked: boolean): void
16
+ } & (
17
+ | // children か label は片方が必須
18
+ {
19
+ children: React.ReactNode
20
+ }
21
+ | {
22
+ label: string
23
+ }
24
+ )
25
+
26
+ export default function SwitchCheckbox(props: SwitchProps) {
27
+ const { disabled, className } = props
28
+
29
+ const ariaSwitchProps: AriaSwitchProps = useMemo(
30
+ () => ({
31
+ ...props,
32
+
33
+ // children がいない場合は aria-label をつけないといけない
34
+ 'aria-label': 'children' in props ? undefined : props.label,
35
+ isDisabled: props.disabled,
36
+ isSelected: props.checked,
37
+ }),
38
+ [props]
39
+ )
40
+
41
+ const state = useToggleState(ariaSwitchProps)
42
+ const ref = useRef<HTMLInputElement>(null)
43
+ const {
44
+ inputProps: { className: _className, type: _type, ...rest },
45
+ } = useSwitch(ariaSwitchProps, state, ref)
46
+
47
+ return (
48
+ <Label className={className} aria-disabled={disabled}>
49
+ <SwitchInput {...rest} ref={ref} />
50
+ {'children' in props ? (
51
+ // eslint-disable-next-line react/destructuring-assignment
52
+ <LabelInner>{props.children}</LabelInner>
53
+ ) : undefined}
54
+ </Label>
55
+ )
56
+ }
57
+
58
+ const Label = styled.label`
59
+ display: inline-grid;
60
+ grid-template-columns: auto 1fr;
61
+ gap: ${({ theme }) => px(theme.spacing[4])};
62
+ cursor: pointer;
63
+ outline: 0;
64
+
65
+ ${theme((o) => o.disabled)}
66
+
67
+ ${disabledSelector} {
68
+ cursor: default;
69
+ }
70
+ `
71
+
72
+ const LabelInner = styled.div`
73
+ ${theme((o) => o.typography(14))}
74
+ `
75
+
76
+ const SwitchInput = styled.input.attrs({
77
+ type: 'checkbox',
78
+ })`
79
+ &[type='checkbox'] {
80
+ appearance: none;
81
+ display: inline-flex;
82
+ position: relative;
83
+ box-sizing: border-box;
84
+ width: 28px;
85
+ border: 2px solid transparent;
86
+ transition: box-shadow 0.2s, background-color 0.2s;
87
+ cursor: inherit;
88
+ ${theme((o) => [
89
+ o.borderRadius(16),
90
+ o.height.px(16),
91
+ o.bg.text4.hover.press,
92
+ o.outline.default.focus,
93
+ ])}
94
+
95
+ &::after {
96
+ content: '';
97
+ position: absolute;
98
+ display: block;
99
+ top: 0;
100
+ left: 0;
101
+ width: 12px;
102
+ height: 12px;
103
+ transform: translateX(0);
104
+ transition: transform 0.2s;
105
+ ${theme((o) => [o.bg.text5.hover.press, o.borderRadius('oval')])}
106
+ }
107
+
108
+ &:checked {
109
+ ${theme((o) => o.bg.brand.hover.press)}
110
+
111
+ &::after {
112
+ transform: translateX(12px);
113
+ }
114
+ }
115
+ }
116
+ `
@@ -0,0 +1,83 @@
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 Clickable from '../Clickable'
6
+ import TextField, {
7
+ MultiLineTextFieldProps,
8
+ SingleLineTextFieldProps,
9
+ TextFieldProps,
10
+ } from '.'
11
+ import { px } from '@charcoal-ui/utils'
12
+
13
+ export default {
14
+ title: 'TextField',
15
+ component: TextField,
16
+ argTypes: {},
17
+ args: {
18
+ showLabel: false,
19
+ label: 'Label',
20
+ assistiveText: '',
21
+ disabled: false,
22
+ required: false,
23
+ invalid: false,
24
+ },
25
+ }
26
+
27
+ const Template: Story<Partial<TextFieldProps>> = (args) => (
28
+ <div
29
+ css={css`
30
+ display: grid;
31
+ gap: ${({ theme }) => px(theme.spacing[24])};
32
+ `}
33
+ >
34
+ <TextField
35
+ label="Label"
36
+ requiredText="*必須"
37
+ subLabel={
38
+ <Clickable to="#" onClick={action('click')}>
39
+ Text Link
40
+ </Clickable>
41
+ }
42
+ placeholder="Single Line"
43
+ onChange={action('change')}
44
+ {...(args as Partial<SingleLineTextFieldProps>)}
45
+ multiline={false}
46
+ />
47
+ <TextField
48
+ label="Label"
49
+ requiredText="*必須"
50
+ subLabel={
51
+ <Clickable to="#" onClick={action('click')}>
52
+ Text Link
53
+ </Clickable>
54
+ }
55
+ placeholder="Multi Line"
56
+ onChange={action('change')}
57
+ {...(args as Partial<MultiLineTextFieldProps>)}
58
+ multiline
59
+ />
60
+ </div>
61
+ )
62
+
63
+ export const Default = Template.bind({})
64
+
65
+ export const HasLabel = Template.bind({})
66
+ HasLabel.args = {
67
+ showLabel: true,
68
+ assistiveText: 'Assistive text',
69
+ required: true,
70
+ }
71
+
72
+ export const HasCount = Template.bind({})
73
+ HasCount.args = {
74
+ showCount: true,
75
+ maxLength: 100,
76
+ }
77
+
78
+ export const AutoHeight: Story<Partial<MultiLineTextFieldProps>> = (args) => (
79
+ <TextField label="Label" placeholder="Multi Line" {...args} multiline />
80
+ )
81
+ AutoHeight.args = {
82
+ autoHeight: true,
83
+ }