@charcoal-ui/react 2.7.0 → 3.0.0-beta.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/dist/components/Button/index.d.ts +1 -1
- package/dist/components/Button/index.d.ts.map +1 -1
- package/dist/components/DropdownSelector/DropdownPopover.d.ts.map +1 -1
- package/dist/components/DropdownSelector/OptionItem.d.ts +7 -0
- package/dist/components/DropdownSelector/OptionItem.d.ts.map +1 -0
- package/dist/components/DropdownSelector/OptionLi.d.ts +11 -0
- package/dist/components/DropdownSelector/OptionLi.d.ts.map +1 -0
- package/dist/components/DropdownSelector/index.d.ts +22 -29
- package/dist/components/DropdownSelector/index.d.ts.map +1 -1
- package/dist/components/DropdownSelector/index.story.d.ts +5 -18
- package/dist/components/DropdownSelector/index.story.d.ts.map +1 -1
- package/dist/components/DropdownSelector/utils/focusIfHTMLLIElement.d.ts +6 -0
- package/dist/components/DropdownSelector/utils/focusIfHTMLLIElement.d.ts.map +1 -0
- package/dist/components/DropdownSelector/utils/handleFocusByKeyBoard.d.ts +6 -0
- package/dist/components/DropdownSelector/utils/handleFocusByKeyBoard.d.ts.map +1 -0
- package/dist/index.cjs.js +265 -313
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +248 -296
- package/dist/index.esm.js.map +1 -1
- package/package.json +6 -6
- package/src/components/Button/index.story.tsx +6 -6
- package/src/components/Button/index.tsx +5 -5
- package/src/components/DropdownSelector/DropdownPopover.tsx +9 -15
- package/src/components/DropdownSelector/OptionItem.tsx +85 -0
- package/src/components/DropdownSelector/index.story.tsx +69 -156
- package/src/components/DropdownSelector/index.tsx +110 -140
- package/src/components/DropdownSelector/utils/focusIfHTMLLIElement.tsx +12 -0
- package/src/components/DropdownSelector/utils/handleFocusByKeyBoard.tsx +20 -0
- package/src/components/Modal/index.story.tsx +5 -5
- package/src/components/Modal/index.tsx +1 -1
- package/src/index.ts +6 -1
- package/src/components/DropdownSelector/ListBoxSection.tsx +0 -60
- package/src/components/DropdownSelector/Listbox.tsx +0 -67
- package/src/components/DropdownSelector/Option.tsx +0 -66
|
@@ -1,156 +1,130 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { ReactNode, createContext, useRef } from 'react'
|
|
2
2
|
import styled from 'styled-components'
|
|
3
|
-
import {
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
37
|
+
const defaultRequiredText = '*必須'
|
|
89
38
|
|
|
90
|
-
|
|
91
|
-
|
|
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}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
|
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
|
|
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
|
+
}
|
|
@@ -76,10 +76,10 @@ const DefaultStory = (args: ModalProps) => {
|
|
|
76
76
|
</ModalAlign>
|
|
77
77
|
</ModalVStack>
|
|
78
78
|
<ModalButtons>
|
|
79
|
-
<Button variant="Primary" onClick={() => state.close()}
|
|
79
|
+
<Button variant="Primary" onClick={() => state.close()} fullWidth>
|
|
80
80
|
Apply
|
|
81
81
|
</Button>
|
|
82
|
-
<Button onClick={() => state.close()}
|
|
82
|
+
<Button onClick={() => state.close()} fullWidth>
|
|
83
83
|
Cancel
|
|
84
84
|
</Button>
|
|
85
85
|
</ModalButtons>
|
|
@@ -140,10 +140,10 @@ const FullBottomSheetStory = (args: ModalProps) => {
|
|
|
140
140
|
</ModalAlign>
|
|
141
141
|
</ModalVStack>
|
|
142
142
|
<ModalButtons>
|
|
143
|
-
<Button variant="Primary" onClick={() => state.close()}
|
|
143
|
+
<Button variant="Primary" onClick={() => state.close()} fullWidth>
|
|
144
144
|
Apply
|
|
145
145
|
</Button>
|
|
146
|
-
<Button onClick={() => state.close()}
|
|
146
|
+
<Button onClick={() => state.close()} fullWidth>
|
|
147
147
|
Cancel
|
|
148
148
|
</Button>
|
|
149
149
|
</ModalButtons>
|
|
@@ -181,7 +181,7 @@ const BottomSheetStory = (args: ModalProps) => {
|
|
|
181
181
|
</StyledModalText>
|
|
182
182
|
</ModalVStack>
|
|
183
183
|
<ModalButtons>
|
|
184
|
-
<Button variant="Danger" onClick={() => state.close()}
|
|
184
|
+
<Button variant="Danger" onClick={() => state.close()} fullWidth>
|
|
185
185
|
削除する
|
|
186
186
|
</Button>
|
|
187
187
|
<ModalDismissButton>キャンセル</ModalDismissButton>
|
package/src/index.ts
CHANGED
|
@@ -52,8 +52,13 @@ export {
|
|
|
52
52
|
} from './components/LoadingSpinner'
|
|
53
53
|
export {
|
|
54
54
|
default as DropdownSelector,
|
|
55
|
-
|
|
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,
|
|
@@ -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``
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import React, { memo, useRef, Fragment, useMemo } from 'react'
|
|
2
|
-
import styled from 'styled-components'
|
|
3
|
-
import { ListProps, ListState } from 'react-stately'
|
|
4
|
-
import { useListBox } from '@react-aria/listbox'
|
|
5
|
-
import { theme } from '../../styled'
|
|
6
|
-
|
|
7
|
-
import { ListBoxSection } from './ListBoxSection'
|
|
8
|
-
import { Divider } from './Divider'
|
|
9
|
-
import { Option } from './Option'
|
|
10
|
-
|
|
11
|
-
export type ListMode = 'default' | 'separator'
|
|
12
|
-
export type ListboxProps<T> = Omit<ListProps<T>, 'children'> & {
|
|
13
|
-
state: ListState<T>
|
|
14
|
-
mode?: ListMode
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const Listbox = <T,>({
|
|
18
|
-
state,
|
|
19
|
-
mode = 'default',
|
|
20
|
-
...props
|
|
21
|
-
}: ListboxProps<T>) => {
|
|
22
|
-
const ref = useRef<HTMLUListElement>(null)
|
|
23
|
-
|
|
24
|
-
const { listBoxProps } = useListBox(props, state, ref)
|
|
25
|
-
const collection = useMemo(
|
|
26
|
-
() =>
|
|
27
|
-
[...state.collection].map((node, index, self) => ({
|
|
28
|
-
node,
|
|
29
|
-
first: index === 0,
|
|
30
|
-
last: index === self.length - 1,
|
|
31
|
-
})),
|
|
32
|
-
[state.collection]
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<ListboxRoot ref={ref} {...listBoxProps}>
|
|
37
|
-
{collection.map(({ node, last }) => (
|
|
38
|
-
<Fragment key={node.key}>
|
|
39
|
-
{node.type === 'section' ? (
|
|
40
|
-
<ListBoxSection section={node} state={state} />
|
|
41
|
-
) : (
|
|
42
|
-
<Option item={node} state={state} mode={mode} />
|
|
43
|
-
)}
|
|
44
|
-
{!last && mode === 'separator' && <Divider />}
|
|
45
|
-
</Fragment>
|
|
46
|
-
))}
|
|
47
|
-
</ListboxRoot>
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
export default memo(Listbox)
|
|
51
|
-
|
|
52
|
-
const ListboxRoot = styled.ul`
|
|
53
|
-
padding-left: 0;
|
|
54
|
-
margin: 0;
|
|
55
|
-
box-sizing: border-box;
|
|
56
|
-
list-style: none;
|
|
57
|
-
overflow: auto;
|
|
58
|
-
max-height: inherit;
|
|
59
|
-
|
|
60
|
-
${theme((o) => [
|
|
61
|
-
o.bg.background1,
|
|
62
|
-
o.border.default,
|
|
63
|
-
o.borderRadius(8),
|
|
64
|
-
o.padding.vertical(8),
|
|
65
|
-
o.outline.default.focus,
|
|
66
|
-
])}
|
|
67
|
-
`
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import React, { useRef } from 'react'
|
|
2
|
-
import styled, { css } from 'styled-components'
|
|
3
|
-
import { ListState } from 'react-stately'
|
|
4
|
-
import { useOption } from '@react-aria/listbox'
|
|
5
|
-
import { mergeProps } from '@react-aria/utils'
|
|
6
|
-
import { useFocusRing } from '@react-aria/focus'
|
|
7
|
-
import { px } from '@charcoal-ui/utils'
|
|
8
|
-
import Icon from '../Icon'
|
|
9
|
-
import { theme } from '../../styled'
|
|
10
|
-
import { Node } from '@react-types/shared'
|
|
11
|
-
import { ListMode } from './Listbox'
|
|
12
|
-
|
|
13
|
-
type OptionProps<T> = {
|
|
14
|
-
item: Node<T>
|
|
15
|
-
state: ListState<T>
|
|
16
|
-
mode?: ListMode
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function Option<T>({ item, state, mode }: OptionProps<T>) {
|
|
20
|
-
const ref = useRef<HTMLLIElement>(null)
|
|
21
|
-
|
|
22
|
-
const { optionProps, isSelected } = useOption(item, state, ref)
|
|
23
|
-
const { focusProps } = useFocusRing()
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<OptionRoot {...mergeProps(optionProps, focusProps)} ref={ref} mode={mode}>
|
|
27
|
-
<OptionCheckIcon name="16/Check" isSelected={isSelected} />
|
|
28
|
-
<OptionText>{item.rendered}</OptionText>
|
|
29
|
-
</OptionRoot>
|
|
30
|
-
)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const OptionRoot = styled.li<{ mode?: ListMode }>`
|
|
34
|
-
display: flex;
|
|
35
|
-
align-items: center;
|
|
36
|
-
gap: ${({ theme }) => px(theme.spacing[4])};
|
|
37
|
-
height: 40px;
|
|
38
|
-
cursor: pointer;
|
|
39
|
-
outline: none;
|
|
40
|
-
|
|
41
|
-
${({ mode }) =>
|
|
42
|
-
theme((o) => [
|
|
43
|
-
o.padding.horizontal(8),
|
|
44
|
-
mode === 'separator' && o.padding.vertical(4),
|
|
45
|
-
])}
|
|
46
|
-
|
|
47
|
-
&:focus {
|
|
48
|
-
${theme((o) => [o.bg.surface3])}
|
|
49
|
-
}
|
|
50
|
-
`
|
|
51
|
-
|
|
52
|
-
const OptionCheckIcon = styled(Icon)<{ isSelected: boolean }>`
|
|
53
|
-
visibility: hidden;
|
|
54
|
-
${theme((o) => [o.font.text2])}
|
|
55
|
-
|
|
56
|
-
${({ isSelected }) =>
|
|
57
|
-
isSelected &&
|
|
58
|
-
css`
|
|
59
|
-
visibility: visible;
|
|
60
|
-
`}
|
|
61
|
-
`
|
|
62
|
-
|
|
63
|
-
const OptionText = styled.span`
|
|
64
|
-
display: block;
|
|
65
|
-
${theme((o) => [o.typography(14), o.font.text2])}
|
|
66
|
-
`
|