@charcoal-ui/react 3.0.0-beta.0 → 3.0.0-beta.2

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 (41) hide show
  1. package/dist/components/Button/index.d.ts.map +1 -1
  2. package/dist/components/DropdownSelector/DropdownPopover.d.ts.map +1 -1
  3. package/dist/components/DropdownSelector/OptionItem.d.ts +7 -0
  4. package/dist/components/DropdownSelector/OptionItem.d.ts.map +1 -0
  5. package/dist/components/DropdownSelector/index.d.ts +22 -29
  6. package/dist/components/DropdownSelector/index.d.ts.map +1 -1
  7. package/dist/components/DropdownSelector/index.story.d.ts +5 -18
  8. package/dist/components/DropdownSelector/index.story.d.ts.map +1 -1
  9. package/dist/components/DropdownSelector/utils/focusIfHTMLLIElement.d.ts +6 -0
  10. package/dist/components/DropdownSelector/utils/focusIfHTMLLIElement.d.ts.map +1 -0
  11. package/dist/components/DropdownSelector/utils/handleFocusByKeyBoard.d.ts +6 -0
  12. package/dist/components/DropdownSelector/utils/handleFocusByKeyBoard.d.ts.map +1 -0
  13. package/dist/components/LoadingSpinner/index.d.ts.map +1 -1
  14. package/dist/components/Modal/index.d.ts +5 -2
  15. package/dist/components/Modal/index.d.ts.map +1 -1
  16. package/dist/components/Radio/index.d.ts.map +1 -1
  17. package/dist/components/SegmentedControl/index.d.ts.map +1 -1
  18. package/dist/components/SegmentedControl/index.story.d.ts.map +1 -1
  19. package/dist/index.cjs.js +313 -336
  20. package/dist/index.cjs.js.map +1 -1
  21. package/dist/index.d.ts +3 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.esm.js +296 -319
  24. package/dist/index.esm.js.map +1 -1
  25. package/package.json +6 -6
  26. package/src/components/DropdownSelector/DropdownPopover.tsx +9 -15
  27. package/src/components/DropdownSelector/OptionItem.tsx +85 -0
  28. package/src/components/DropdownSelector/index.story.tsx +69 -156
  29. package/src/components/DropdownSelector/index.tsx +110 -140
  30. package/src/components/DropdownSelector/utils/focusIfHTMLLIElement.tsx +12 -0
  31. package/src/components/DropdownSelector/utils/handleFocusByKeyBoard.tsx +20 -0
  32. package/src/components/LoadingSpinner/index.tsx +1 -0
  33. package/src/components/Modal/index.tsx +79 -61
  34. package/src/components/Radio/index.tsx +2 -0
  35. package/src/components/SegmentedControl/index.story.tsx +2 -0
  36. package/src/components/SegmentedControl/index.tsx +1 -0
  37. package/src/components/TextField/index.tsx +1 -0
  38. package/src/index.ts +7 -2
  39. package/src/components/DropdownSelector/ListBoxSection.tsx +0 -60
  40. package/src/components/DropdownSelector/Listbox.tsx +0 -67
  41. package/src/components/DropdownSelector/Option.tsx +0 -66
@@ -1,156 +1,130 @@
1
- import React, { Key, useMemo, useRef } from 'react'
1
+ import React, { ReactNode, createContext, useRef } from 'react'
2
2
  import styled from 'styled-components'
3
- import { Item, useSelectState } from 'react-stately'
3
+ import { useOverlayTriggerState } from 'react-stately'
4
4
  import { disabledSelector } from '@charcoal-ui/utils'
5
- import { useVisuallyHidden } from '@react-aria/visually-hidden'
6
- import { useSelect, HiddenSelect } from '@react-aria/select'
7
- import { useButton } from '@react-aria/button'
8
- import { SelectProps } from '@react-types/select'
9
- import Listbox, { ListboxProps } from './Listbox'
10
5
  import Icon from '../Icon'
11
6
  import FieldLabel from '../FieldLabel'
12
7
  import { theme } from '../../styled'
13
-
14
- import type { CollectionBase } from '@react-types/shared'
15
- import type { ReactNode } from 'react'
16
8
  import { DropdownPopover } from './DropdownPopover'
17
9
 
18
- type LabelProps = {
19
- readonly showLabel?: boolean
20
- readonly label: string
21
- readonly subLabel?: ReactNode
22
- readonly requiredText?: string
10
+ export const DropdownSelectorContext = createContext({
11
+ value: '',
12
+ setValue: (_v: string) => {
13
+ // empty
14
+ },
15
+ })
16
+
17
+ export type DropdownSelectorProps = {
18
+ label: string
19
+ value: string
20
+ disabled?: boolean
21
+ placeholder?: string
22
+ showLabel?: boolean
23
+ invalid?: boolean
24
+ assistiveText?: string
25
+ required?: boolean
26
+ requiredText?: string
27
+ subLabel?: ReactNode
28
+ children?: ReactNode
29
+ onChange: (value: string) => void
23
30
  }
24
31
 
25
- type Empty = Record<string, unknown>
26
- export type DropdownSelectorProps<T extends Empty = Empty> = LabelProps &
27
- Readonly<CollectionBase<T>> & {
28
- readonly id?: string
29
- readonly name?: string
30
- readonly autoComplete?: string
31
- readonly placeholder?: string
32
- readonly className?: string
33
- readonly disabled?: boolean
34
- readonly required?: boolean
35
- readonly invalid?: boolean
36
- readonly assertiveText?: string
37
- readonly value?: Key
38
- readonly defaultValue?: Key
39
- readonly open?: boolean
40
- readonly onOpenChange?: (isOpen?: boolean) => void
41
- readonly onChange?: (key: Key) => void
42
- readonly mode?: ListboxProps<T>['mode']
43
- }
44
-
45
- const DropdownSelector = <T extends Record<string, unknown>>({
46
- open,
47
- className,
48
- label = '',
49
- requiredText = '',
50
- subLabel,
51
- assertiveText,
52
- autoComplete,
53
- invalid = false,
54
- disabled = false,
55
- required = false,
56
- showLabel = false,
57
- mode = 'default',
58
- ...props
59
- }: DropdownSelectorProps<T>) => {
60
- const { visuallyHiddenProps } = useVisuallyHidden()
61
- const triggerRef = useRef<HTMLButtonElement>(null)
62
- const selectProps = useMemo<SelectProps<T>>(
63
- () => ({
64
- ...props,
65
- label,
66
- isOpen: open,
67
- isDisabled: disabled,
68
- isRequired: required,
69
- errorMessage: invalid && assertiveText,
70
- validationState: invalid ? 'invalid' : 'valid',
71
- onSelectionChange: props.onChange,
72
- selectedKey: props.value,
73
- defaultSelectedKey: props.defaultValue,
74
- }),
75
- [assertiveText, disabled, invalid, label, open, props, required]
76
- )
77
- const state = useSelectState<T>(selectProps)
78
-
79
- const {
80
- labelProps,
81
- triggerProps,
82
- valueProps,
83
- menuProps,
84
- errorMessageProps,
85
- descriptionProps,
86
- } = useSelect<T>(selectProps, state, triggerRef)
32
+ export type DropdownSelectorOption = {
33
+ label: string
34
+ id: string
35
+ }
87
36
 
88
- const { buttonProps } = useButton(triggerProps, triggerRef)
37
+ const defaultRequiredText = '*必須'
89
38
 
90
- const hasAssertiveText =
91
- assertiveText !== undefined && assertiveText.length > 0
39
+ export default function DropdownSelector(props: DropdownSelectorProps) {
40
+ const triggerRef = useRef<HTMLButtonElement>(null)
41
+ const state = useOverlayTriggerState({})
42
+
43
+ let preview: ReactNode | undefined
44
+ const childArray = React.Children.toArray(props.children)
45
+ for (let i = 0; i < childArray.length; i++) {
46
+ const child = childArray[i]
47
+ if (React.isValidElement(child) && 'value' in child.props) {
48
+ const find = (child.props as { value: string }).value === props.value
49
+ if (find && 'children' in child.props) {
50
+ preview = (child.props as { children: ReactNode }).children
51
+ break
52
+ }
53
+ }
54
+ }
92
55
 
93
56
  return (
94
- <DropdownSelectorRoot aria-disabled={disabled} className={className}>
95
- <DropdownFieldLabel
96
- label={label}
97
- required={required}
98
- requiredText={requiredText}
99
- subLabel={subLabel}
100
- {...labelProps}
101
- {...(!showLabel ? visuallyHiddenProps : {})}
102
- />
103
- <HiddenSelect
104
- state={state}
105
- triggerRef={triggerRef}
106
- label={label}
107
- name={props.name}
108
- isDisabled={disabled}
109
- autoComplete={autoComplete}
110
- />
111
- <DropdownButtonWrapper>
112
- <DropdownButton {...buttonProps} ref={triggerRef} invalid={invalid}>
113
- <DropdownButtonText {...valueProps}>
114
- {/*
115
- * react-stately の useSelectState から取得される selectedItem の型が常に
116
- * Node<T> であるが runtime では null が帰ってくることがある
117
- */}
118
- {/* eslint-disable-next-line @typescript-eslint/strict-boolean-expressions,@typescript-eslint/no-unnecessary-condition*/}
119
- {state.selectedItem
120
- ? state.selectedItem.rendered
121
- : props.placeholder}
122
- </DropdownButtonText>
123
-
124
- <DropdownButtonIcon name="16/Menu" />
125
- </DropdownButton>
126
- {state.isOpen && (
127
- <DropdownPopover
128
- state={state}
129
- triggerRef={triggerRef}
130
- value={props.value ?? props.defaultValue}
131
- >
132
- <Listbox {...menuProps} state={state} mode={mode} />
133
- </DropdownPopover>
134
- )}
135
- </DropdownButtonWrapper>
136
-
137
- {hasAssertiveText && (
138
- <AssertiveText
139
- invalid={invalid}
140
- {...(invalid ? errorMessageProps : descriptionProps)}
57
+ <DropdownSelectorRoot aria-disabled={props.disabled}>
58
+ {props.showLabel === true && (
59
+ <DropdownFieldLabel
60
+ label={props.label}
61
+ required={props.required}
62
+ requiredText={props.requiredText ?? defaultRequiredText}
63
+ subLabel={props.subLabel}
64
+ />
65
+ )}
66
+ <DropdownButton
67
+ invalid={props.invalid}
68
+ disabled={props.disabled}
69
+ onClick={() => {
70
+ if (props.disabled === true) return
71
+ state.open()
72
+ }}
73
+ ref={triggerRef}
74
+ >
75
+ <DropdownButtonText>
76
+ {props.placeholder !== undefined && preview === undefined
77
+ ? props.placeholder
78
+ : preview}
79
+ </DropdownButtonText>
80
+ <DropdownButtonIcon name="16/Menu" />
81
+ </DropdownButton>
82
+ {state.isOpen && (
83
+ <DropdownPopover
84
+ state={state}
85
+ triggerRef={triggerRef}
86
+ value={props.value}
141
87
  >
142
- {assertiveText}
88
+ <ListboxRoot>
89
+ <DropdownSelectorContext.Provider
90
+ value={{
91
+ value: props.value,
92
+ setValue: (v) => {
93
+ props.onChange(v)
94
+ state.close()
95
+ },
96
+ }}
97
+ >
98
+ {props.children}
99
+ </DropdownSelectorContext.Provider>
100
+ </ListboxRoot>
101
+ </DropdownPopover>
102
+ )}
103
+ {props.assistiveText !== undefined && (
104
+ <AssertiveText invalid={props.invalid}>
105
+ {props.assistiveText}
143
106
  </AssertiveText>
144
107
  )}
145
108
  </DropdownSelectorRoot>
146
109
  )
147
110
  }
148
111
 
149
- export default DropdownSelector
150
- export const DropdownSelectorItem = Item
151
-
112
+ const ListboxRoot = styled.ul`
113
+ padding-left: 0;
114
+ margin: 0;
115
+ box-sizing: border-box;
116
+ list-style: none;
117
+ overflow: auto;
118
+ max-height: inherit;
119
+
120
+ ${theme((o) => [
121
+ o.bg.background1,
122
+ o.border.default,
123
+ o.borderRadius(8),
124
+ o.padding.vertical(8),
125
+ ])}
126
+ `
152
127
  const DropdownSelectorRoot = styled.div`
153
- position: relative;
154
128
  display: inline-block;
155
129
  width: 100%;
156
130
 
@@ -166,11 +140,7 @@ const DropdownFieldLabel = styled(FieldLabel)`
166
140
  ${theme((o) => o.margin.bottom(8))}
167
141
  `
168
142
 
169
- const DropdownButtonWrapper = styled.div`
170
- position: relative;
171
- `
172
-
173
- const DropdownButton = styled.button<{ invalid: boolean }>`
143
+ const DropdownButton = styled.button<{ invalid?: boolean }>`
174
144
  display: flex;
175
145
  justify-content: space-between;
176
146
  align-items: center;
@@ -191,7 +161,7 @@ const DropdownButton = styled.button<{ invalid: boolean }>`
191
161
  o.outline.default.focus,
192
162
  o.bg.surface3,
193
163
  o.borderRadius(4),
194
- invalid && o.outline.assertive,
164
+ invalid === true && o.outline.assertive,
195
165
  ])}
196
166
  `
197
167
 
@@ -205,11 +175,11 @@ const DropdownButtonIcon = styled(Icon)`
205
175
  ${theme((o) => [o.font.text2])}
206
176
  `
207
177
 
208
- const AssertiveText = styled.div<{ invalid: boolean }>`
178
+ const AssertiveText = styled.div<{ invalid?: boolean }>`
209
179
  ${({ invalid }) =>
210
180
  theme((o) => [
211
181
  o.typography(14),
212
182
  o.margin.top(8),
213
- invalid ? o.font.assertive : o.font.text2,
183
+ invalid === true ? o.font.assertive : o.font.text2,
214
184
  ])}
215
185
  `
@@ -0,0 +1,12 @@
1
+ import { handleFocusByKeyBoard } from './handleFocusByKeyBoard'
2
+
3
+ /**
4
+ * li要素ならフォーカスしてスクロールスクロール領域に見えるように親要素をスクロールする
5
+ * @param element
6
+ */
7
+ export function focusIfHTMLLIElement(element: Node | null | undefined) {
8
+ if (element instanceof HTMLLIElement) {
9
+ element.focus({ preventScroll: true })
10
+ handleFocusByKeyBoard(element)
11
+ }
12
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * スクロールスクロール領域に見えるように親要素をスクロールする
3
+ * @param element
4
+ */
5
+
6
+ export function handleFocusByKeyBoard(element: HTMLElement) {
7
+ const parent = element.parentElement
8
+ if (!parent) return
9
+ const rect = element.getBoundingClientRect()
10
+ const parentRect = parent.getBoundingClientRect()
11
+ if (rect.bottom > parentRect.bottom) {
12
+ parent.scrollTo({
13
+ top: parent.scrollTop + rect.bottom - parentRect.bottom,
14
+ })
15
+ } else if (rect.top < parentRect.top) {
16
+ parent.scrollTo({
17
+ top: parent.scrollTop - (parentRect.top - rect.top),
18
+ })
19
+ }
20
+ }
@@ -19,6 +19,7 @@ const LoadingSpinnerRoot = styled.div.attrs({ role: 'progressbar' })<{
19
19
  padding: number
20
20
  transparent: boolean
21
21
  }>`
22
+ box-sizing: content-box;
22
23
  margin: auto;
23
24
  padding: ${(props) => props.padding}px;
24
25
  border-radius: 8px;
@@ -18,13 +18,16 @@ import { animated, useTransition, easings } from 'react-spring'
18
18
  import Button, { ButtonProps } from '../Button'
19
19
  import IconButton from '../IconButton'
20
20
 
21
+ type BottomSheet = boolean | 'full'
22
+ type Size = 'S' | 'M' | 'L'
23
+
21
24
  export type ModalProps = AriaModalOverlayProps &
22
25
  AriaDialogProps & {
23
26
  children: React.ReactNode
24
27
  zIndex?: number
25
28
  title: string
26
- size?: 'S' | 'M' | 'L'
27
- bottomSheet?: boolean | 'full'
29
+ size?: Size
30
+ bottomSheet?: BottomSheet
28
31
  isOpen: boolean
29
32
  onClose: () => void
30
33
 
@@ -133,28 +136,30 @@ export default function Modal({
133
136
  style={transitionEnabled ? { backgroundColor } : {}}
134
137
  >
135
138
  <FocusScope contain restoreFocus autoFocus>
136
- <ModalDialog
137
- ref={ref}
138
- {...overlayProps}
139
- {...modalProps}
140
- {...dialogProps}
141
- style={transitionEnabled ? { transform } : {}}
142
- size={size}
143
- bottomSheet={bottomSheet}
144
- >
145
- <ModalContext.Provider
146
- value={{ titleProps, title, close: onClose, showDismiss }}
139
+ <DialogContainer bottomSheet={bottomSheet} size={size}>
140
+ <ModalDialog
141
+ ref={ref}
142
+ {...overlayProps}
143
+ {...modalProps}
144
+ {...dialogProps}
145
+ style={transitionEnabled ? { transform } : {}}
146
+ size={size}
147
+ bottomSheet={bottomSheet}
147
148
  >
148
- {children}
149
- {isDismissable === true && (
150
- <ModalCrossButton
151
- size="S"
152
- icon="24/Close"
153
- onClick={onClose}
154
- />
155
- )}
156
- </ModalContext.Provider>
157
- </ModalDialog>
149
+ <ModalContext.Provider
150
+ value={{ titleProps, title, close: onClose, showDismiss }}
151
+ >
152
+ {children}
153
+ {isDismissable === true && (
154
+ <ModalCrossButton
155
+ size="S"
156
+ icon="24/Close"
157
+ onClick={onClose}
158
+ />
159
+ )}
160
+ </ModalContext.Provider>
161
+ </ModalDialog>
162
+ </DialogContainer>
158
163
  </FocusScope>
159
164
  </ModalBackground>
160
165
  </Overlay>
@@ -176,6 +181,8 @@ const ModalContext = React.createContext<{
176
181
 
177
182
  const ModalBackground = animated(styled.div<{ zIndex: number }>`
178
183
  z-index: ${({ zIndex }) => zIndex};
184
+ overflow: scroll;
185
+ display: flex;
179
186
  position: fixed;
180
187
  top: 0;
181
188
  left: 0;
@@ -185,50 +192,61 @@ const ModalBackground = animated(styled.div<{ zIndex: number }>`
185
192
  ${theme((o) => [o.bg.surface4])}
186
193
  `)
187
194
 
195
+ const DialogContainer = styled.div<{ bottomSheet: BottomSheet; size: Size }>`
196
+ position: relative;
197
+ margin: auto;
198
+ padding: 24px 0;
199
+ width: ${(p) => {
200
+ switch (p.size) {
201
+ case 'S': {
202
+ return columnSystem(3, COLUMN_UNIT, GUTTER_UNIT) + GUTTER_UNIT * 2
203
+ }
204
+ case 'M': {
205
+ return columnSystem(4, COLUMN_UNIT, GUTTER_UNIT) + GUTTER_UNIT * 2
206
+ }
207
+ case 'L': {
208
+ return columnSystem(6, COLUMN_UNIT, GUTTER_UNIT) + GUTTER_UNIT * 2
209
+ }
210
+ default: {
211
+ return unreachable(p.size)
212
+ }
213
+ }
214
+ }}px;
215
+
216
+ @media ${({ theme }) => maxWidth(theme.breakpoint.screen1)} {
217
+ width: calc(100% - 48px);
218
+ ${(p) =>
219
+ p.bottomSheet !== false &&
220
+ css`
221
+ margin: 0;
222
+ padding: 0;
223
+ bottom: 0;
224
+ position: absolute;
225
+ width: 100%;
226
+ ${p.bottomSheet === 'full' ? 'height: 100%' : ''};
227
+ `}
228
+ }
229
+ `
230
+
188
231
  const ModalDialog = animated(styled.div<{
189
- size: 'S' | 'M' | 'L'
190
- bottomSheet: boolean | 'full'
232
+ size: Size
233
+ bottomSheet: BottomSheet
191
234
  }>`
192
- position: absolute;
193
- top: 50%;
194
- left: 50%;
195
- transform: translate(-50%, -50%);
196
- width: ${(p) =>
197
- p.size === 'S'
198
- ? columnSystem(3, COLUMN_UNIT, GUTTER_UNIT) + GUTTER_UNIT * 2
199
- : p.size === 'M'
200
- ? columnSystem(4, COLUMN_UNIT, GUTTER_UNIT) + GUTTER_UNIT * 2
201
- : // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
202
- p.size === 'L'
203
- ? columnSystem(6, COLUMN_UNIT, GUTTER_UNIT) + GUTTER_UNIT * 2
204
- : unreachable(p.size)}px;
235
+ position: relative;
236
+ margin: auto;
237
+ padding: 24px 0;
205
238
 
206
239
  ${theme((o) => [o.bg.background1, o.borderRadius(24)])}
207
-
208
240
  @media ${({ theme }) => maxWidth(theme.breakpoint.screen1)} {
209
241
  ${(p) =>
210
- p.bottomSheet === 'full'
211
- ? css`
212
- top: auto;
213
- bottom: 0;
214
- left: 0;
215
- transform: none;
216
- border-radius: 0;
217
- width: 100%;
218
- height: 100%;
219
- `
220
- : p.bottomSheet
221
- ? css`
222
- top: auto;
223
- bottom: 0;
224
- left: 0;
225
- transform: none;
226
- border-radius: 0;
227
- width: 100%;
228
- `
229
- : css`
230
- width: calc(100% - 48px);
231
- `}
242
+ p.bottomSheet !== false &&
243
+ css`
244
+ border-radius: 0;
245
+ ${p.bottomSheet === 'full' &&
246
+ css`
247
+ height: 100%;
248
+ `}
249
+ `}
232
250
  }
233
251
  `)
234
252
 
@@ -82,6 +82,8 @@ export const RadioInput = styled.input.attrs({ type: 'radio' })<{
82
82
  width: 20px;
83
83
  height: 20px;
84
84
 
85
+ cursor: pointer;
86
+
85
87
  ${({ hasError = false }) =>
86
88
  theme((o) => [
87
89
  o.borderRadius('oval'),
@@ -13,6 +13,7 @@ export const StringSegments: Story<SegmentedControlProps> = (props) => {
13
13
  }
14
14
 
15
15
  StringSegments.args = {
16
+ name: 'test',
16
17
  data: ['option1', 'option2', 'option3'],
17
18
  disabled: false,
18
19
  readonly: false,
@@ -24,6 +25,7 @@ export const ObjectSegments: Story<SegmentedControlProps> = (props) => {
24
25
  }
25
26
 
26
27
  ObjectSegments.args = {
28
+ name: 'test',
27
29
  data: [
28
30
  { label: '選択肢1', value: 'option1' },
29
31
  { label: '選択肢2', value: 'option2' },
@@ -41,6 +41,7 @@ const SegmentedControl = forwardRef<HTMLDivElement, SegmentedControlProps>(
41
41
  isDisabled: props.disabled,
42
42
  isReadOnly: props.readonly,
43
43
  isRequired: props.required,
44
+ 'aria-label': props.name,
44
45
  }),
45
46
  [props]
46
47
  )
@@ -349,6 +349,7 @@ const PrefixContainer = styled.span`
349
349
  top: 50%;
350
350
  left: 8px;
351
351
  transform: translateY(-50%);
352
+ z-index: 1;
352
353
  `
353
354
 
354
355
  const SuffixContainer = styled.span`
package/src/index.ts CHANGED
@@ -52,11 +52,16 @@ export {
52
52
  } from './components/LoadingSpinner'
53
53
  export {
54
54
  default as DropdownSelector,
55
- DropdownSelectorItem,
55
+ type DropdownSelectorOption,
56
+ type DropdownSelectorProps,
56
57
  } from './components/DropdownSelector'
58
+ export {
59
+ OptionItem,
60
+ type OptionItemProps,
61
+ } from './components/DropdownSelector/OptionItem'
57
62
  export {
58
63
  default as SegmentedControl,
59
64
  type SegmentedControlProps,
60
65
  } from './components/SegmentedControl'
61
- export { default as Checkbox } from './components/Checkbox'
66
+ export { default as Checkbox, type CheckboxProps } from './components/Checkbox'
62
67
  export { default as TagItem, type TagItemProps } from './components/TagItem'
@@ -1,60 +0,0 @@
1
- import { useListBoxSection } from '@react-aria/listbox'
2
- import { useSeparator } from '@react-aria/separator'
3
- import { Node } from '@react-types/shared'
4
- import React from 'react'
5
- import { ListState } from 'react-stately'
6
- import styled from 'styled-components'
7
- import { Option } from './Option'
8
- import { Divider } from './Divider'
9
- import { theme } from '../../styled'
10
-
11
- export function ListBoxSection<T>(props: {
12
- section: Node<T>
13
- state: ListState<T>
14
- }) {
15
- const { state } = props
16
- const { itemProps, headingProps, groupProps } = useListBoxSection({
17
- heading: props.section.rendered,
18
- 'aria-label': props.section['aria-label'],
19
- })
20
-
21
- const { separatorProps } = useSeparator({
22
- elementType: 'li',
23
- })
24
-
25
- return (
26
- <>
27
- {props.section.key !== props.state.collection.getFirstKey() && (
28
- <Divider {...separatorProps} role="separator" />
29
- )}
30
- <StyledLi {...itemProps}>
31
- {props.section.rendered != null && (
32
- <SectionSpan {...headingProps}>{props.section.rendered}</SectionSpan>
33
- )}
34
- <StyledUl {...groupProps}>
35
- {[...props.section.childNodes].map((node) => (
36
- <Option key={node.key} item={node} state={state} />
37
- ))}
38
- </StyledUl>
39
- </StyledLi>
40
- </>
41
- )
42
- }
43
-
44
- const SectionSpan = styled.span`
45
- ${theme((o) => [
46
- o.font.text3,
47
- o.typography(12).bold,
48
- o.margin.bottom(8).left(16).top(16),
49
- ])}
50
- `
51
-
52
- const StyledUl = styled.ul`
53
- padding-left: 0;
54
- margin: 0;
55
- box-sizing: border-box;
56
- list-style: none;
57
- overflow: hidden;
58
- `
59
-
60
- const StyledLi = styled.li``