@charcoal-ui/react 3.0.0-beta.3 → 3.0.0-beta.4

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.
Files changed (86) hide show
  1. package/dist/_lib/compat.d.ts +18 -0
  2. package/dist/_lib/compat.d.ts.map +1 -1
  3. package/dist/_lib/index.d.ts +7 -0
  4. package/dist/_lib/index.d.ts.map +1 -1
  5. package/dist/components/Checkbox/index.d.ts +1 -0
  6. package/dist/components/Checkbox/index.d.ts.map +1 -1
  7. package/dist/components/Checkbox/index.story.d.ts +1 -0
  8. package/dist/components/Checkbox/index.story.d.ts.map +1 -1
  9. package/dist/components/DropdownSelector/index.d.ts.map +1 -1
  10. package/dist/components/LoadingSpinner/index.d.ts +8 -6
  11. package/dist/components/LoadingSpinner/index.d.ts.map +1 -1
  12. package/dist/components/LoadingSpinner/index.story.d.ts +1 -2
  13. package/dist/components/LoadingSpinner/index.story.d.ts.map +1 -1
  14. package/dist/components/Modal/index.d.ts +17 -26
  15. package/dist/components/Modal/index.d.ts.map +1 -1
  16. package/dist/components/Modal/index.story.d.ts +12 -2
  17. package/dist/components/Modal/index.story.d.ts.map +1 -1
  18. package/dist/components/MultiSelect/index.d.ts +14 -1
  19. package/dist/components/MultiSelect/index.d.ts.map +1 -1
  20. package/dist/components/MultiSelect/index.story.d.ts +14 -2
  21. package/dist/components/MultiSelect/index.story.d.ts.map +1 -1
  22. package/dist/components/Radio/index.d.ts +12 -5
  23. package/dist/components/Radio/index.d.ts.map +1 -1
  24. package/dist/components/Radio/index.story.d.ts +10 -6
  25. package/dist/components/Radio/index.story.d.ts.map +1 -1
  26. package/dist/components/SegmentedControl/index.d.ts +1 -0
  27. package/dist/components/SegmentedControl/index.d.ts.map +1 -1
  28. package/dist/components/Switch/index.d.ts +2 -1
  29. package/dist/components/Switch/index.d.ts.map +1 -1
  30. package/dist/components/Switch/index.story.d.ts +1 -2
  31. package/dist/components/Switch/index.story.d.ts.map +1 -1
  32. package/dist/components/TextArea/index.d.ts +3 -10
  33. package/dist/components/TextArea/index.d.ts.map +1 -1
  34. package/dist/components/TextField/TextField.story.d.ts +4 -5
  35. package/dist/components/TextField/TextField.story.d.ts.map +1 -1
  36. package/dist/components/TextField/index.d.ts +6 -29
  37. package/dist/components/TextField/index.d.ts.map +1 -1
  38. package/dist/components/TextField/index.story.d.ts +5 -4
  39. package/dist/components/TextField/index.story.d.ts.map +1 -1
  40. package/dist/index.cjs.js +636 -594
  41. package/dist/index.cjs.js.map +1 -1
  42. package/dist/index.d.ts +2 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.esm.js +604 -563
  45. package/dist/index.esm.js.map +1 -1
  46. package/package.json +6 -6
  47. package/src/_lib/compat.ts +19 -0
  48. package/src/_lib/index.ts +23 -0
  49. package/src/components/Checkbox/index.story.tsx +1 -0
  50. package/src/components/Checkbox/index.tsx +2 -1
  51. package/src/components/DropdownSelector/DropdownMenuItem.tsx +1 -1
  52. package/src/components/DropdownSelector/ListItem/index.story.tsx +1 -1
  53. package/src/components/DropdownSelector/ListItem/index.tsx +1 -1
  54. package/src/components/DropdownSelector/MenuItem/index.tsx +0 -1
  55. package/src/components/DropdownSelector/MenuItem/internals/useMenuItemHandleKeyDown.tsx +1 -1
  56. package/src/components/DropdownSelector/MenuItemGroup/index.tsx +0 -1
  57. package/src/components/DropdownSelector/MenuList/index.story.tsx +0 -1
  58. package/src/components/DropdownSelector/MenuList/index.tsx +1 -1
  59. package/src/components/DropdownSelector/MenuList/internals/getValuesRecursive.tsx +1 -1
  60. package/src/components/DropdownSelector/Popover/index.story.tsx +1 -1
  61. package/src/components/DropdownSelector/Popover/index.tsx +1 -1
  62. package/src/components/DropdownSelector/index.tsx +16 -14
  63. package/src/components/DropdownSelector/utils/findPreviewRecursive.tsx +2 -1
  64. package/src/components/LoadingSpinner/index.story.tsx +7 -1
  65. package/src/components/LoadingSpinner/index.tsx +27 -11
  66. package/src/components/Modal/index.tsx +18 -12
  67. package/src/components/MultiSelect/index.story.tsx +16 -4
  68. package/src/components/MultiSelect/index.tsx +70 -60
  69. package/src/components/Radio/index.story.tsx +7 -8
  70. package/src/components/Radio/index.test.tsx +3 -3
  71. package/src/components/Radio/index.tsx +23 -23
  72. package/src/components/SegmentedControl/index.tsx +6 -1
  73. package/src/components/Switch/index.tsx +37 -32
  74. package/src/components/TextArea/TextArea.story.tsx +61 -0
  75. package/src/components/TextArea/index.tsx +246 -0
  76. package/src/components/TextField/{index.story.tsx → TextField.story.tsx} +6 -28
  77. package/src/components/TextField/index.tsx +146 -371
  78. package/src/index.ts +1 -2
  79. package/dist/components/DropdownSelector/OptionItem.d.ts +0 -7
  80. package/dist/components/DropdownSelector/OptionItem.d.ts.map +0 -1
  81. package/dist/components/DropdownSelector/utils/focusIfHTMLLIElement.d.ts +0 -6
  82. package/dist/components/DropdownSelector/utils/focusIfHTMLLIElement.d.ts.map +0 -1
  83. package/dist/components/DropdownSelector/utils/handleFocusByKeyBoard.d.ts +0 -6
  84. package/dist/components/DropdownSelector/utils/handleFocusByKeyBoard.d.ts.map +0 -1
  85. package/dist/types/CustomJSXElement.d.ts +0 -3
  86. package/dist/types/CustomJSXElement.d.ts.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@charcoal-ui/react",
3
- "version": "3.0.0-beta.3",
3
+ "version": "3.0.0-beta.4",
4
4
  "license": "Apache-2.0",
5
5
  "main": "./dist/index.cjs.js",
6
6
  "module": "./dist/index.esm.js",
@@ -49,10 +49,10 @@
49
49
  "typescript": "^4.9.5"
50
50
  },
51
51
  "dependencies": {
52
- "@charcoal-ui/icons": "^3.0.0-beta.3",
53
- "@charcoal-ui/styled": "^3.0.0-beta.3",
54
- "@charcoal-ui/theme": "^3.0.0-beta.3",
55
- "@charcoal-ui/utils": "^3.0.0-beta.3",
52
+ "@charcoal-ui/icons": "^3.0.0-beta.4",
53
+ "@charcoal-ui/styled": "^3.0.0-beta.4",
54
+ "@charcoal-ui/theme": "^3.0.0-beta.4",
55
+ "@charcoal-ui/utils": "^3.0.0-beta.4",
56
56
  "@react-aria/button": "^3.7.0",
57
57
  "@react-aria/checkbox": "^3.8.0",
58
58
  "@react-aria/dialog": "^3.5.0",
@@ -88,5 +88,5 @@
88
88
  "url": "https://github.com/pixiv/charcoal.git",
89
89
  "directory": "packages/react"
90
90
  },
91
- "gitHead": "3d302a3e0becea0868d0f020c233ba68fa1d0974"
91
+ "gitHead": "47ec80abac2b87e78417a06db4943b3cb0408b78"
92
92
  }
@@ -10,3 +10,22 @@ import * as React from 'react'
10
10
  * `Type alias 'Interpolation' circularly references itself. ts(2456)`
11
11
  */
12
12
  export type Story<P> = React.ComponentType<P> & { args?: P }
13
+
14
+ /**
15
+ * react-ariaの`useTextField()`は、<textarea>をサポートするにも関わらず、
16
+ * `React.KeyboardEvent<HTMLInputElement>`しか想定していないイベントハンドラがいくつかある
17
+ * ↓ が直るまで、以下のイベントハンドラの型は信用しない(本当は`Element`ではなく`HTMLTextAreaElement`とかにしたい)
18
+ *
19
+ * @see https://github.com/adobe/react-spectrum/issues/4662
20
+ */
21
+ export interface ReactAreaUseTextFieldCompat<E = Element> {
22
+ readonly onCopy?: React.ClipboardEventHandler<E>
23
+ readonly onPaste?: React.ClipboardEventHandler<E>
24
+ readonly onCut?: React.ClipboardEventHandler<E>
25
+ readonly onCompositionStart?: React.CompositionEventHandler<E>
26
+ readonly onCompositionEnd?: React.CompositionEventHandler<E>
27
+ readonly onCompositionUpdate?: React.CompositionEventHandler<E>
28
+ readonly onSelect?: React.ReactEventHandler<E>
29
+ readonly onBeforeInput?: React.FormEventHandler<E>
30
+ readonly onInput?: React.FormEventHandler<E>
31
+ }
package/src/_lib/index.ts CHANGED
@@ -33,3 +33,26 @@ export function unreachable(value?: never): never {
33
33
  : `unreachable (${JSON.stringify(value)})`
34
34
  )
35
35
  }
36
+
37
+ /**
38
+ * 複数のrefをマージする。
39
+ *
40
+ * forwardRefで受け取ったrefと、コンポーネント内で定義したrefを同じ要素につけたいケースなどで使う
41
+ */
42
+ export function mergeRefs<T>(...refs: React.Ref<T>[]): React.RefCallback<T> {
43
+ return (value) => {
44
+ for (const ref of refs) {
45
+ if (typeof ref === 'function') {
46
+ ref(value)
47
+ } else if (ref !== null) {
48
+ ;(ref as React.MutableRefObject<T | null>).current = value
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ export function countCodePointsInString(string: string) {
55
+ // [...string] とするとproduction buildで動かなくなる
56
+ // cf. https://twitter.com/f_subal/status/1497214727511891972
57
+ return Array.from(string).length
58
+ }
@@ -12,6 +12,7 @@ type Props = {
12
12
  defaultChecked: boolean
13
13
  disabled: boolean
14
14
  readonly: boolean
15
+ className?: string
15
16
  }
16
17
 
17
18
  export const Labelled: Story<Props> = (props) => {
@@ -21,6 +21,7 @@ type CheckboxLabelProps =
21
21
  export type CheckboxProps = CheckboxLabelProps & {
22
22
  readonly id?: string
23
23
  readonly name?: string
24
+ readonly className?: string
24
25
 
25
26
  readonly checked?: boolean
26
27
  readonly defaultChecked?: boolean
@@ -53,7 +54,7 @@ const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
53
54
  const isDisabled = (props.disabled ?? false) || (props.readonly ?? false)
54
55
 
55
56
  return (
56
- <InputRoot aria-disabled={isDisabled}>
57
+ <InputRoot aria-disabled={isDisabled} className={props.className}>
57
58
  <CheckboxRoot>
58
59
  <CheckboxInput type="checkbox" {...inputProps} />
59
60
  <CheckboxInputOverlay aria-hidden={true} checked={inputProps.checked}>
@@ -1,7 +1,7 @@
1
1
  import styled from 'styled-components'
2
2
  import MenuItem, { MenuItemProps } from './MenuItem'
3
3
  import { MenuListContext } from './MenuList/MenuListContext'
4
- import React, { useContext } from 'react'
4
+ import { useContext } from 'react'
5
5
  import { theme } from '../../styled'
6
6
  import Icon from '../Icon'
7
7
 
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react'
1
+ import { useState } from 'react'
2
2
  import { Story } from '../../../_lib/compat'
3
3
  import Icon from '../../Icon'
4
4
  import Switch from '../../Switch'
@@ -1,4 +1,4 @@
1
- import React, { ReactNode } from 'react'
1
+ import { ReactNode } from 'react'
2
2
  import styled from 'styled-components'
3
3
  import { theme } from '../../../styled'
4
4
 
@@ -1,4 +1,3 @@
1
- import React from 'react'
2
1
  import ListItem, { CustomJSXElement, ListItemProps } from '../ListItem'
3
2
  import { useMenuItemHandleKeyDown } from './internals/useMenuItemHandleKeyDown'
4
3
 
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useContext } from 'react'
1
+ import { useCallback, useContext } from 'react'
2
2
  import { handleFocusByKeyBoard } from './handleFocusByKeyBoard'
3
3
  import { MenuListContext } from '../../MenuList/MenuListContext'
4
4
 
@@ -1,4 +1,3 @@
1
- import React from 'react'
2
1
  import styled from 'styled-components'
3
2
  import MenuItem from '../MenuItem'
4
3
  import { Divider } from '../Divider'
@@ -1,4 +1,3 @@
1
- import React from 'react'
2
1
  import { action } from '@storybook/addon-actions'
3
2
  import { Story } from '../../../_lib/compat'
4
3
  import MenuList, { MenuListProps } from '.'
@@ -1,4 +1,4 @@
1
- import React, { useRef } from 'react'
1
+ import { useRef } from 'react'
2
2
  import styled from 'styled-components'
3
3
  import { MenuListContext } from './MenuListContext'
4
4
  import { getValuesRecursive } from './internals/getValuesRecursive'
@@ -1,4 +1,4 @@
1
- import React from 'react'
1
+ import * as React from 'react'
2
2
  import MenuItem from '../../MenuItem'
3
3
  import { MenuListChildren } from '..'
4
4
  import MenuItemGroup from '../../MenuItemGroup'
@@ -1,4 +1,4 @@
1
- import React, { useRef, CSSProperties, useState } from 'react'
1
+ import { useRef, CSSProperties, useState } from 'react'
2
2
  import { Story } from '../../../_lib/compat'
3
3
  import Popover, { PopoverProps } from '.'
4
4
  import Button from '../../Button'
@@ -1,4 +1,4 @@
1
- import React, { RefObject, useRef } from 'react'
1
+ import { RefObject, useRef } from 'react'
2
2
  import { ReactNode } from 'react'
3
3
  import { DismissButton, Overlay, usePopover } from '@react-aria/overlays'
4
4
  import styled from 'styled-components'
@@ -56,22 +56,24 @@ export default function DropdownSelector(props: DropdownSelectorProps) {
56
56
  </DropdownButtonText>
57
57
  <DropdownButtonIcon name="16/Menu" />
58
58
  </DropdownButton>
59
- <DropdownPopover
60
- isOpen={isOpen}
61
- onClose={() => setIsOpen(false)}
62
- triggerRef={triggerRef}
63
- value={props.value}
64
- >
65
- <MenuList
59
+ {isOpen && (
60
+ <DropdownPopover
61
+ isOpen={isOpen}
62
+ onClose={() => setIsOpen(false)}
63
+ triggerRef={triggerRef}
66
64
  value={props.value}
67
- onChange={(v) => {
68
- props.onChange(v)
69
- setIsOpen(false)
70
- }}
71
65
  >
72
- {props.children}
73
- </MenuList>
74
- </DropdownPopover>
66
+ <MenuList
67
+ value={props.value}
68
+ onChange={(v) => {
69
+ props.onChange(v)
70
+ setIsOpen(false)
71
+ }}
72
+ >
73
+ {props.children}
74
+ </MenuList>
75
+ </DropdownPopover>
76
+ )}
75
77
  {props.assistiveText !== undefined && (
76
78
  <AssertiveText invalid={props.invalid}>
77
79
  {props.assistiveText}
@@ -1,4 +1,5 @@
1
- import React, { ReactNode } from 'react'
1
+ import { ReactNode } from 'react'
2
+ import * as React from 'react'
2
3
 
3
4
  /**
4
5
  * DropdownSelectorの選択中の要素をレンダリングするため、
@@ -21,9 +21,15 @@ export function Basic() {
21
21
  const size = number('size', 48)
22
22
  const padding = number('padding', 16)
23
23
  const transparent = boolean('transparent', false)
24
+ const className = text('className', 'basic')
24
25
 
25
26
  return (
26
- <LoadingSpinner size={size} padding={padding} transparent={transparent} />
27
+ <LoadingSpinner
28
+ size={size}
29
+ padding={padding}
30
+ transparent={transparent}
31
+ className={className}
32
+ />
27
33
  )
28
34
  }
29
35
 
@@ -1,19 +1,35 @@
1
- import { forwardRef, useImperativeHandle, useRef } from 'react'
1
+ import { forwardRef, useImperativeHandle, useRef, memo } from 'react'
2
2
  import styled, { keyframes } from 'styled-components'
3
3
  import { theme } from '../../styled'
4
4
 
5
- export default function LoadingSpinner({
6
- size = 48,
7
- padding = 16,
8
- transparent = false,
9
- }) {
10
- return (
11
- <LoadingSpinnerRoot size={size} padding={padding} transparent={transparent}>
12
- <LoadingSpinnerIcon />
13
- </LoadingSpinnerRoot>
14
- )
5
+ export type LoadingSpinnerProps = {
6
+ readonly size?: number
7
+ readonly padding?: number
8
+ readonly transparent?: boolean
9
+ readonly className?: string
15
10
  }
16
11
 
12
+ const LoadingSpinner = forwardRef<HTMLDivElement, LoadingSpinnerProps>(
13
+ function LoadingSpinnerInner(
14
+ { size = 48, padding = 16, transparent = false, className },
15
+ ref
16
+ ) {
17
+ return (
18
+ <LoadingSpinnerRoot
19
+ size={size}
20
+ padding={padding}
21
+ transparent={transparent}
22
+ className={className}
23
+ ref={ref}
24
+ >
25
+ <LoadingSpinnerIcon />
26
+ </LoadingSpinnerRoot>
27
+ )
28
+ }
29
+ )
30
+
31
+ export default memo(LoadingSpinner)
32
+
17
33
  const LoadingSpinnerRoot = styled.div.attrs({ role: 'progressbar' })<{
18
34
  size: number
19
35
  padding: number
@@ -1,4 +1,4 @@
1
- import { useContext, useRef } from 'react'
1
+ import { useContext, forwardRef, memo } from 'react'
2
2
  import * as React from 'react'
3
3
  import {
4
4
  AriaModalOverlayProps,
@@ -18,6 +18,7 @@ import { useMedia } from '@charcoal-ui/styled'
18
18
  import { animated, useTransition, easings } from 'react-spring'
19
19
  import Button, { ButtonProps } from '../Button'
20
20
  import IconButton from '../IconButton'
21
+ import { useObjectRef } from '@react-aria/utils'
21
22
 
22
23
  type BottomSheet = boolean | 'full'
23
24
  type Size = 'S' | 'M' | 'L'
@@ -31,6 +32,7 @@ export type ModalProps = AriaModalOverlayProps &
31
32
  bottomSheet?: BottomSheet
32
33
  isOpen: boolean
33
34
  onClose: () => void
35
+ className?: string
34
36
 
35
37
  /**
36
38
  * https://github.com/adobe/react-spectrum/issues/3787
@@ -56,31 +58,32 @@ const DEFAULT_Z_INDEX = 10
56
58
  *
57
59
  * <OverlayProvider>
58
60
  * <App>
59
- * <Modal isOpen={state.isOpen} onClose={() => state.close()} isDismissable>
61
+ * <Modal title="Title" isOpen={state.isOpen} onClose={() => state.close()} isDismissable>
60
62
  * <ModalHeader />
61
- * <ModalBody>...</ModalBody>
62
- * <ModalButtons>...</ModalButtons>
63
+ * <ModalBody>
64
+ * ...
65
+ * <ModalButtons>...</ModalButtons>
66
+ * </ModalBody>
63
67
  * </Modal>
64
68
  * </App>
65
69
  * </OverlayProvider>
66
70
  * ```
67
71
  */
68
- export default function Modal({
69
- children,
70
- zIndex = DEFAULT_Z_INDEX,
71
- portalContainer,
72
- ...props
73
- }: ModalProps) {
72
+ const Modal = forwardRef<HTMLDivElement, ModalProps>(function ModalInner(
73
+ { children, zIndex = DEFAULT_Z_INDEX, portalContainer, ...props },
74
+ external
75
+ ) {
74
76
  const {
75
77
  title,
76
78
  size = 'M',
77
79
  bottomSheet = false,
78
80
  isDismissable,
79
81
  onClose,
82
+ className,
80
83
  isOpen = false,
81
84
  } = props
82
85
 
83
- const ref = useRef<HTMLDivElement>(null)
86
+ const ref = useObjectRef<HTMLDivElement>(external)
84
87
  const { overlayProps, underlayProps } = useOverlay(props, ref)
85
88
 
86
89
  const { modalProps } = useModalOverlay(
@@ -146,6 +149,7 @@ export default function Modal({
146
149
  style={transitionEnabled ? { transform } : {}}
147
150
  size={size}
148
151
  bottomSheet={bottomSheet}
152
+ className={className}
149
153
  >
150
154
  <ModalContext.Provider
151
155
  value={{ titleProps, title, close: onClose, showDismiss }}
@@ -166,7 +170,9 @@ export default function Modal({
166
170
  </Overlay>
167
171
  )
168
172
  )
169
- }
173
+ })
174
+
175
+ export default memo(Modal)
170
176
 
171
177
  const ModalContext = React.createContext<{
172
178
  titleProps: React.HTMLAttributes<HTMLElement>
@@ -55,6 +55,7 @@ type Props = {
55
55
  readonly?: boolean
56
56
  invalid?: boolean
57
57
  variant?: 'default' | 'overlay'
58
+ className?: string
58
59
  }
59
60
 
60
61
  const StyledMultiSelectGroup = styled(MultiSelectGroup)`
@@ -72,6 +73,7 @@ const Template: Story<Props> = ({
72
73
  readonly,
73
74
  invalid,
74
75
  variant,
76
+ className,
75
77
  }) => {
76
78
  return (
77
79
  <StyledMultiSelectGroup
@@ -83,11 +85,15 @@ const Template: Story<Props> = ({
83
85
  readonly,
84
86
  invalid,
85
87
  }}
86
- className={''}
87
88
  selected={selected ? ['選択肢1', '選択肢3'] : []}
88
89
  >
89
90
  {[1, 2, 3, 4].map((idx) => (
90
- <MultiSelect value={`選択肢${idx}`} variant={variant} key={idx}>
91
+ <MultiSelect
92
+ value={`選択肢${idx}`}
93
+ variant={variant}
94
+ key={idx}
95
+ className={className}
96
+ >
91
97
  選択肢{idx}
92
98
  </MultiSelect>
93
99
  ))}
@@ -114,10 +120,11 @@ type PlaygroundProps = {
114
120
  disabled?: boolean
115
121
  readonly?: boolean
116
122
  invalid?: boolean
123
+ className?: string
117
124
  variant?: 'default' | 'overlay'
118
125
  }
119
126
 
120
- export const Playground: Story<PlaygroundProps> = (props) => {
127
+ export const Playground: Story<PlaygroundProps> = ({ className, ...props }) => {
121
128
  const [selected, setSelected] = useState<string[]>([])
122
129
 
123
130
  return (
@@ -127,7 +134,12 @@ export const Playground: Story<PlaygroundProps> = (props) => {
127
134
  onChange={setSelected}
128
135
  >
129
136
  {[1, 2, 3, 4].map((idx) => (
130
- <MultiSelect value={`選択肢${idx}`} variant={props.variant} key={idx}>
137
+ <MultiSelect
138
+ value={`選択肢${idx}`}
139
+ variant={props.variant}
140
+ key={idx}
141
+ className={className}
142
+ >
131
143
  選択肢{idx}
132
144
  </MultiSelect>
133
145
  ))}
@@ -1,4 +1,4 @@
1
- import { ChangeEvent, useCallback, useContext } from 'react'
1
+ import { ChangeEvent, useCallback, useContext, forwardRef, memo } from 'react'
2
2
  import * as React from 'react'
3
3
  import styled, { css } from 'styled-components'
4
4
  import warning from 'warning'
@@ -11,70 +11,80 @@ export type MultiSelectProps = React.PropsWithChildren<{
11
11
  value: string
12
12
  disabled?: boolean
13
13
  variant?: 'default' | 'overlay'
14
+ className?: string
14
15
  onChange?: (payload: { value: string; selected: boolean }) => void
15
16
  }>
16
17
 
17
- export default function MultiSelect({
18
- value,
19
- disabled = false,
20
- onChange,
21
- variant = 'default',
22
- children,
23
- }: MultiSelectProps) {
24
- const {
25
- name,
26
- selected,
27
- disabled: parentDisabled,
28
- readonly,
29
- invalid,
30
- onChange: parentOnChange,
31
- } = useContext(MultiSelectGroupContext)
32
-
33
- warning(
34
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
35
- name !== undefined,
36
- `"name" is not Provided for <MultiSelect>. Perhaps you forgot to wrap with <MultiSelectGroup> ?`
37
- )
38
-
39
- const isSelected = selected.includes(value)
40
- const isDisabled = disabled || parentDisabled || readonly
41
-
42
- const handleChange = useCallback(
43
- (event: ChangeEvent<HTMLInputElement>) => {
44
- if (!(event.currentTarget instanceof HTMLInputElement)) {
45
- return
46
- }
47
- if (onChange) onChange({ value, selected: event.currentTarget.checked })
48
- parentOnChange({ value, selected: event.currentTarget.checked })
18
+ const MultiSelect = forwardRef<HTMLInputElement, MultiSelectProps>(
19
+ function MultiSelectInner(
20
+ {
21
+ value,
22
+ disabled = false,
23
+ onChange,
24
+ variant = 'default',
25
+ className,
26
+ children,
49
27
  },
50
- [onChange, parentOnChange, value]
51
- )
28
+ ref
29
+ ) {
30
+ const {
31
+ name,
32
+ selected,
33
+ disabled: parentDisabled,
34
+ readonly,
35
+ invalid,
36
+ onChange: parentOnChange,
37
+ } = useContext(MultiSelectGroupContext)
38
+
39
+ warning(
40
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
41
+ name !== undefined,
42
+ `"name" is not Provided for <MultiSelect>. Perhaps you forgot to wrap with <MultiSelectGroup> ?`
43
+ )
44
+
45
+ const isSelected = selected.includes(value)
46
+ const isDisabled = disabled || parentDisabled || readonly
47
+
48
+ const handleChange = useCallback(
49
+ (event: ChangeEvent<HTMLInputElement>) => {
50
+ if (!(event.currentTarget instanceof HTMLInputElement)) {
51
+ return
52
+ }
53
+ if (onChange) onChange({ value, selected: event.currentTarget.checked })
54
+ parentOnChange({ value, selected: event.currentTarget.checked })
55
+ },
56
+ [onChange, parentOnChange, value]
57
+ )
58
+
59
+ return (
60
+ <MultiSelectRoot aria-disabled={isDisabled} className={className}>
61
+ <MultiSelectInput
62
+ {...{
63
+ name,
64
+ value,
65
+ invalid,
66
+ }}
67
+ checked={isSelected}
68
+ disabled={isDisabled}
69
+ onChange={handleChange}
70
+ overlay={variant === 'overlay'}
71
+ aria-invalid={invalid}
72
+ ref={ref}
73
+ />
74
+ <MultiSelectInputOverlay
75
+ overlay={variant === 'overlay'}
76
+ invalid={invalid}
77
+ aria-hidden={true}
78
+ >
79
+ <pixiv-icon name="24/Check" unsafe-non-guideline-scale={16 / 24} />
80
+ </MultiSelectInputOverlay>
81
+ {Boolean(children) && <MultiSelectLabel>{children}</MultiSelectLabel>}
82
+ </MultiSelectRoot>
83
+ )
84
+ }
85
+ )
52
86
 
53
- return (
54
- <MultiSelectRoot aria-disabled={isDisabled}>
55
- <MultiSelectInput
56
- {...{
57
- name,
58
- value,
59
- invalid,
60
- }}
61
- checked={isSelected}
62
- disabled={isDisabled}
63
- onChange={handleChange}
64
- overlay={variant === 'overlay'}
65
- aria-invalid={invalid}
66
- />
67
- <MultiSelectInputOverlay
68
- overlay={variant === 'overlay'}
69
- invalid={invalid}
70
- aria-hidden={true}
71
- >
72
- <pixiv-icon name="24/Check" unsafe-non-guideline-scale={16 / 24} />
73
- </MultiSelectInputOverlay>
74
- {Boolean(children) && <MultiSelectLabel>{children}</MultiSelectLabel>}
75
- </MultiSelectRoot>
76
- )
77
- }
87
+ export default memo(MultiSelect)
78
88
 
79
89
  const MultiSelectRoot = styled.label`
80
90
  display: grid;
@@ -16,30 +16,29 @@ export default {
16
16
  },
17
17
  },
18
18
  args: {
19
- hasError: false,
19
+ invalid: false,
20
20
  parentDisabled: false,
21
21
  childDisabled: false,
22
- forceChecked: false,
23
22
  readonly: false,
24
23
  },
25
24
  }
26
25
 
27
26
  interface Props {
28
27
  value?: string
29
- hasError: boolean
28
+ invalid: boolean
30
29
  parentDisabled: boolean
31
30
  childDisabled: boolean
32
- forceChecked: boolean
33
31
  readonly: boolean
32
+ className?: string
34
33
  }
35
34
 
36
35
  const Template: Story<Partial<Props>> = ({
37
36
  value,
38
- forceChecked,
39
- hasError,
37
+ invalid,
40
38
  parentDisabled,
41
39
  childDisabled,
42
40
  readonly,
41
+ className,
43
42
  }) => (
44
43
  <div
45
44
  css={css`
@@ -57,14 +56,14 @@ const Template: Story<Partial<Props>> = ({
57
56
  onChange={action('onChange')}
58
57
  disabled={parentDisabled}
59
58
  readonly={readonly}
60
- hasError={hasError}
59
+ invalid={invalid}
61
60
  >
62
61
  {options.map((option) => (
63
62
  <Radio
64
63
  key={option}
65
64
  value={option}
66
65
  disabled={childDisabled}
67
- forceChecked={forceChecked}
66
+ className={className}
68
67
  >
69
68
  {name}({option})を選ぶ
70
69
  </Radio>
@@ -111,7 +111,7 @@ function TestComponent({
111
111
  onChange = jest.fn(),
112
112
  radioGroupDisabled = false,
113
113
  readonly = false,
114
- hasError = false,
114
+ invalid = false,
115
115
  option1Disabled = false,
116
116
  option2Disabled = false,
117
117
  }: {
@@ -119,7 +119,7 @@ function TestComponent({
119
119
  onChange?: () => void
120
120
  radioGroupDisabled?: boolean
121
121
  readonly?: boolean
122
- hasError?: boolean
122
+ invalid?: boolean
123
123
  option1Disabled?: boolean
124
124
  option2Disabled?: boolean
125
125
  }) {
@@ -132,7 +132,7 @@ function TestComponent({
132
132
  onChange={onChange}
133
133
  disabled={radioGroupDisabled}
134
134
  readonly={readonly}
135
- hasError={hasError}
135
+ invalid={invalid}
136
136
  >
137
137
  <Radio value="option1" disabled={option1Disabled}>
138
138
  option1を選ぶ