@charcoal-ui/react 3.0.0-beta.2 → 3.0.0-beta.3
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/_lib/compat.d.ts +1 -1
- package/dist/_lib/compat.d.ts.map +1 -1
- package/dist/components/Button/index.d.ts +1 -2
- package/dist/components/Button/index.d.ts.map +1 -1
- package/dist/components/Button/index.story.d.ts +1 -2
- package/dist/components/Button/index.story.d.ts.map +1 -1
- package/dist/components/Button/index.test.d.ts +4 -0
- package/dist/components/Button/index.test.d.ts.map +1 -0
- package/dist/components/Checkbox/index.d.ts +1 -1
- package/dist/components/Checkbox/index.d.ts.map +1 -1
- package/dist/components/Checkbox/index.story.d.ts +1 -2
- package/dist/components/Checkbox/index.story.d.ts.map +1 -1
- package/dist/components/Clickable/index.d.ts +1 -1
- package/dist/components/Clickable/index.d.ts.map +1 -1
- package/dist/components/Clickable/index.story.d.ts +1 -2
- package/dist/components/Clickable/index.story.d.ts.map +1 -1
- package/dist/components/DropdownSelector/Divider.d.ts +3 -0
- package/dist/components/DropdownSelector/Divider.d.ts.map +1 -1
- package/dist/components/DropdownSelector/DropdownMenuItem.d.ts +7 -0
- package/dist/components/DropdownSelector/DropdownMenuItem.d.ts.map +1 -0
- package/dist/components/DropdownSelector/DropdownPopover.d.ts +8 -8
- package/dist/components/DropdownSelector/DropdownPopover.d.ts.map +1 -1
- package/dist/components/DropdownSelector/ListItem/index.d.ts +18 -0
- package/dist/components/DropdownSelector/ListItem/index.d.ts.map +1 -0
- package/dist/components/DropdownSelector/ListItem/index.story.d.ts +9 -0
- package/dist/components/DropdownSelector/ListItem/index.story.d.ts.map +1 -0
- package/dist/components/DropdownSelector/MenuItem/index.d.ts +11 -0
- package/dist/components/DropdownSelector/MenuItem/index.d.ts.map +1 -0
- package/dist/components/DropdownSelector/MenuItem/internals/handleFocusByKeyBoard.d.ts +9 -0
- package/dist/components/DropdownSelector/MenuItem/internals/handleFocusByKeyBoard.d.ts.map +1 -0
- package/dist/components/DropdownSelector/MenuItem/internals/useMenuItemHandleKeyDown.d.ts +10 -0
- package/dist/components/DropdownSelector/MenuItem/internals/useMenuItemHandleKeyDown.d.ts.map +1 -0
- package/dist/components/DropdownSelector/MenuItemGroup/index.d.ts +14 -0
- package/dist/components/DropdownSelector/MenuItemGroup/index.d.ts.map +1 -0
- package/dist/components/DropdownSelector/MenuList/MenuListContext.d.ts +10 -0
- package/dist/components/DropdownSelector/MenuList/MenuListContext.d.ts.map +1 -0
- package/dist/components/DropdownSelector/MenuList/index.d.ts +18 -0
- package/dist/components/DropdownSelector/MenuList/index.d.ts.map +1 -0
- package/dist/components/DropdownSelector/MenuList/index.story.d.ts +11 -0
- package/dist/components/DropdownSelector/MenuList/index.story.d.ts.map +1 -0
- package/dist/components/DropdownSelector/MenuList/internals/getValuesRecursive.d.ts +11 -0
- package/dist/components/DropdownSelector/MenuList/internals/getValuesRecursive.d.ts.map +1 -0
- package/dist/components/DropdownSelector/Popover/index.d.ts +17 -0
- package/dist/components/DropdownSelector/Popover/index.d.ts.map +1 -0
- package/dist/components/DropdownSelector/Popover/index.story.d.ts +9 -0
- package/dist/components/DropdownSelector/Popover/index.story.d.ts.map +1 -0
- package/dist/components/DropdownSelector/index.d.ts +3 -10
- package/dist/components/DropdownSelector/index.d.ts.map +1 -1
- package/dist/components/DropdownSelector/index.story.d.ts +4 -4
- package/dist/components/DropdownSelector/index.story.d.ts.map +1 -1
- package/dist/components/DropdownSelector/utils/findPreviewRecursive.d.ts +12 -0
- package/dist/components/DropdownSelector/utils/findPreviewRecursive.d.ts.map +1 -0
- package/dist/components/FieldLabel/index.d.ts +1 -1
- package/dist/components/FieldLabel/index.d.ts.map +1 -1
- package/dist/components/Icon/index.d.ts +1 -1
- package/dist/components/Icon/index.d.ts.map +1 -1
- package/dist/components/Icon/index.story.d.ts +2 -3
- package/dist/components/Icon/index.story.d.ts.map +1 -1
- package/dist/components/IconButton/index.d.ts +1 -2
- package/dist/components/IconButton/index.d.ts.map +1 -1
- package/dist/components/IconButton/index.story.d.ts +1 -2
- package/dist/components/IconButton/index.story.d.ts.map +1 -1
- package/dist/components/LoadingSpinner/index.d.ts +1 -2
- package/dist/components/LoadingSpinner/index.d.ts.map +1 -1
- package/dist/components/Modal/ModalPlumbing.d.ts.map +1 -1
- package/dist/components/Modal/index.d.ts +1 -1
- package/dist/components/Modal/index.d.ts.map +1 -1
- package/dist/components/Modal/index.story.d.ts.map +1 -1
- package/dist/components/MultiSelect/context.d.ts +1 -1
- package/dist/components/MultiSelect/context.d.ts.map +1 -1
- package/dist/components/MultiSelect/index.d.ts +5 -6
- package/dist/components/MultiSelect/index.d.ts.map +1 -1
- package/dist/components/MultiSelect/index.story.d.ts +7 -14
- package/dist/components/MultiSelect/index.story.d.ts.map +1 -1
- package/dist/components/Radio/index.d.ts +1 -1
- package/dist/components/Radio/index.d.ts.map +1 -1
- package/dist/components/Radio/index.story.d.ts +1 -2
- package/dist/components/Radio/index.story.d.ts.map +1 -1
- package/dist/components/SegmentedControl/RadioGroupContext.d.ts +1 -1
- package/dist/components/SegmentedControl/RadioGroupContext.d.ts.map +1 -1
- package/dist/components/SegmentedControl/index.d.ts +1 -1
- package/dist/components/SegmentedControl/index.d.ts.map +1 -1
- package/dist/components/SegmentedControl/index.story.d.ts +1 -2
- package/dist/components/SegmentedControl/index.story.d.ts.map +1 -1
- package/dist/components/Switch/index.d.ts +1 -1
- package/dist/components/Switch/index.d.ts.map +1 -1
- package/dist/components/TagItem/index.d.ts +3 -3
- package/dist/components/TagItem/index.d.ts.map +1 -1
- package/dist/components/TagItem/index.story.d.ts +2 -3
- package/dist/components/TagItem/index.story.d.ts.map +1 -1
- package/dist/components/TextArea/TextArea.story.d.ts +28 -0
- package/dist/components/TextArea/TextArea.story.d.ts.map +1 -0
- package/dist/components/TextArea/index.d.ts +28 -0
- package/dist/components/TextArea/index.d.ts.map +1 -0
- package/dist/components/TextField/TextField.story.d.ts +29 -0
- package/dist/components/TextField/TextField.story.d.ts.map +1 -0
- package/dist/components/TextField/index.d.ts +2 -1
- package/dist/components/TextField/index.d.ts.map +1 -1
- package/dist/components/TextField/index.story.d.ts +4 -5
- package/dist/components/TextField/index.story.d.ts.map +1 -1
- package/dist/core/CharcoalProvider.d.ts +1 -1
- package/dist/core/CharcoalProvider.d.ts.map +1 -1
- package/dist/core/ComponentAbstraction.d.ts +1 -1
- package/dist/core/ComponentAbstraction.d.ts.map +1 -1
- package/dist/index.cjs.js +744 -493
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +689 -452
- package/dist/index.esm.js.map +1 -1
- package/dist/styled.d.ts +13 -13
- package/dist/types/CustomJSXElement.d.ts +3 -0
- package/dist/types/CustomJSXElement.d.ts.map +1 -0
- package/package.json +7 -7
- package/src/_lib/compat.ts +1 -1
- package/src/components/Button/__snapshots__/index.test.tsx.snap +385 -0
- package/src/components/Button/index.story.tsx +1 -1
- package/src/components/Button/index.test.tsx +24 -0
- package/src/components/Button/index.tsx +2 -2
- package/src/components/Checkbox/index.story.tsx +0 -1
- package/src/components/Checkbox/index.tsx +2 -1
- package/src/components/Clickable/index.story.tsx +0 -1
- package/src/components/Clickable/index.tsx +1 -1
- package/src/components/DropdownSelector/Divider.tsx +3 -0
- package/src/components/DropdownSelector/DropdownMenuItem.tsx +40 -0
- package/src/components/DropdownSelector/DropdownPopover.tsx +21 -42
- package/src/components/DropdownSelector/ListItem/index.story.tsx +51 -0
- package/src/components/DropdownSelector/ListItem/index.tsx +58 -0
- package/src/components/DropdownSelector/MenuItem/index.tsx +32 -0
- package/src/components/DropdownSelector/MenuItem/internals/handleFocusByKeyBoard.tsx +43 -0
- package/src/components/DropdownSelector/MenuItem/internals/useMenuItemHandleKeyDown.tsx +55 -0
- package/src/components/DropdownSelector/MenuItemGroup/index.tsx +43 -0
- package/src/components/DropdownSelector/MenuList/MenuListContext.ts +17 -0
- package/src/components/DropdownSelector/MenuList/index.story.tsx +52 -0
- package/src/components/DropdownSelector/MenuList/index.tsx +51 -0
- package/src/components/DropdownSelector/MenuList/internals/getValuesRecursive.tsx +35 -0
- package/src/components/DropdownSelector/Popover/index.story.tsx +65 -0
- package/src/components/DropdownSelector/Popover/index.tsx +69 -0
- package/src/components/DropdownSelector/index.story.tsx +56 -21
- package/src/components/DropdownSelector/index.tsx +21 -64
- package/src/components/DropdownSelector/utils/findPreviewRecursive.tsx +38 -0
- package/src/components/FieldLabel/index.tsx +1 -1
- package/src/components/Icon/index.story.tsx +0 -1
- package/src/components/Icon/index.tsx +1 -1
- package/src/components/IconButton/index.story.tsx +0 -1
- package/src/components/IconButton/index.tsx +2 -2
- package/src/components/LoadingSpinner/index.story.tsx +1 -1
- package/src/components/LoadingSpinner/index.tsx +18 -19
- package/src/components/Modal/ModalPlumbing.tsx +0 -1
- package/src/components/Modal/index.story.tsx +0 -1
- package/src/components/Modal/index.tsx +2 -1
- package/src/components/MultiSelect/context.ts +2 -2
- package/src/components/MultiSelect/index.story.tsx +16 -29
- package/src/components/MultiSelect/index.test.tsx +5 -23
- package/src/components/MultiSelect/index.tsx +19 -24
- package/src/components/Radio/index.story.tsx +0 -1
- package/src/components/Radio/index.test.tsx +0 -1
- package/src/components/Radio/index.tsx +2 -1
- package/src/components/SegmentedControl/RadioGroupContext.tsx +2 -1
- package/src/components/SegmentedControl/index.story.tsx +0 -1
- package/src/components/SegmentedControl/index.tsx +10 -4
- package/src/components/Switch/index.story.tsx +1 -1
- package/src/components/Switch/index.tsx +2 -1
- package/src/components/TagItem/index.story.tsx +0 -1
- package/src/components/TagItem/index.tsx +1 -6
- package/src/components/TextField/index.story.tsx +0 -1
- package/src/components/TextField/index.tsx +2 -7
- package/src/components/a11y.test.tsx +0 -1
- package/src/core/CharcoalProvider.tsx +1 -1
- package/src/core/ComponentAbstraction.tsx +2 -1
- package/src/index.ts +7 -4
- package/src/components/DropdownSelector/OptionItem.tsx +0 -85
- package/src/components/DropdownSelector/utils/focusIfHTMLLIElement.tsx +0 -12
- package/src/components/DropdownSelector/utils/handleFocusByKeyBoard.tsx +0 -20
|
@@ -1,46 +1,25 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import { ReactNode } from 'react'
|
|
4
|
-
import {
|
|
5
|
-
AriaPopoverProps,
|
|
6
|
-
DismissButton,
|
|
7
|
-
Overlay,
|
|
8
|
-
usePopover,
|
|
9
|
-
} from '@react-aria/overlays'
|
|
10
|
-
import styled from 'styled-components'
|
|
11
|
-
import { theme } from '../../styled'
|
|
1
|
+
import { Key, useEffect, useRef } from 'react'
|
|
2
|
+
import Popover, { PopoverProps } from './Popover'
|
|
12
3
|
|
|
13
|
-
|
|
14
|
-
width: 100%;
|
|
15
|
-
${theme((o) => o.margin.top(4).bottom(4))}
|
|
16
|
-
`
|
|
17
|
-
|
|
18
|
-
type Props = Omit<AriaPopoverProps, 'popoverRef'> & {
|
|
19
|
-
state: OverlayTriggerState
|
|
20
|
-
} & {
|
|
21
|
-
children: ReactNode
|
|
4
|
+
type DropdownPopoverProps = PopoverProps & {
|
|
22
5
|
value?: Key
|
|
23
6
|
}
|
|
24
7
|
|
|
25
|
-
|
|
8
|
+
/**
|
|
9
|
+
* DropdownSelectorの選択肢をを表示するためのPopover
|
|
10
|
+
* triggerRefの要素と同じ幅になる
|
|
11
|
+
* 表示の際にvalueが等しいDropdownMenuItemを中央に表示する
|
|
12
|
+
*/
|
|
13
|
+
export function DropdownPopover({ children, ...props }: DropdownPopoverProps) {
|
|
26
14
|
const ref = useRef<HTMLDivElement>(null)
|
|
27
|
-
const { popoverProps, underlayProps } = usePopover(
|
|
28
|
-
{
|
|
29
|
-
...props,
|
|
30
|
-
popoverRef: ref,
|
|
31
|
-
containerPadding: 0,
|
|
32
|
-
},
|
|
33
|
-
state
|
|
34
|
-
)
|
|
35
|
-
|
|
36
15
|
useEffect(() => {
|
|
37
|
-
if (ref.current && props.triggerRef.current) {
|
|
16
|
+
if (props.isOpen && ref.current && props.triggerRef.current) {
|
|
38
17
|
ref.current.style.width = `${props.triggerRef.current.clientWidth}px`
|
|
39
18
|
}
|
|
40
|
-
}, [props.triggerRef])
|
|
19
|
+
}, [props.triggerRef, props.isOpen])
|
|
41
20
|
|
|
42
21
|
useEffect(() => {
|
|
43
|
-
if (
|
|
22
|
+
if (props.isOpen && props.value !== undefined) {
|
|
44
23
|
// windowのスクロールを維持したまま選択肢をPopoverの中心に表示する
|
|
45
24
|
const windowScrollY = window.scrollY
|
|
46
25
|
const windowScrollX = window.scrollX
|
|
@@ -51,16 +30,16 @@ export function DropdownPopover({ children, state, ...props }: Props) {
|
|
|
51
30
|
selectedElement?.focus()
|
|
52
31
|
window.scrollTo(windowScrollX, windowScrollY)
|
|
53
32
|
}
|
|
54
|
-
}, [props.value,
|
|
33
|
+
}, [props.value, props.isOpen])
|
|
55
34
|
|
|
56
35
|
return (
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
</
|
|
36
|
+
<Popover
|
|
37
|
+
isOpen={props.isOpen}
|
|
38
|
+
onClose={props.onClose}
|
|
39
|
+
popoverRef={ref}
|
|
40
|
+
triggerRef={props.triggerRef}
|
|
41
|
+
>
|
|
42
|
+
{children}
|
|
43
|
+
</Popover>
|
|
65
44
|
)
|
|
66
45
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import { Story } from '../../../_lib/compat'
|
|
3
|
+
import Icon from '../../Icon'
|
|
4
|
+
import Switch from '../../Switch'
|
|
5
|
+
import ListItem, { ListItemProps } from '.'
|
|
6
|
+
import styled from 'styled-components'
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
title: 'DropdownSelector/ListItem',
|
|
10
|
+
component: ListItem,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const CustomLink = styled.a`
|
|
14
|
+
color: red;
|
|
15
|
+
`
|
|
16
|
+
|
|
17
|
+
export const Basic: Story<ListItemProps> = () => {
|
|
18
|
+
const [checked, setChecked] = useState(false)
|
|
19
|
+
const handleCheck = () => {
|
|
20
|
+
setChecked((v) => !v)
|
|
21
|
+
}
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
<ListItem>Item</ListItem>
|
|
25
|
+
<ListItem>
|
|
26
|
+
<Icon name="16/Add" /> Add
|
|
27
|
+
</ListItem>
|
|
28
|
+
<ListItem as="a" href="#">
|
|
29
|
+
Normal Link
|
|
30
|
+
</ListItem>
|
|
31
|
+
<ListItem as={CustomLink} href="#">
|
|
32
|
+
Custom Link
|
|
33
|
+
</ListItem>
|
|
34
|
+
<ListItem onClick={handleCheck}>
|
|
35
|
+
Switch
|
|
36
|
+
<div
|
|
37
|
+
style={{
|
|
38
|
+
marginLeft: 'auto',
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<Switch
|
|
42
|
+
label="hello"
|
|
43
|
+
name="hello"
|
|
44
|
+
onChange={handleCheck}
|
|
45
|
+
checked={checked}
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
</ListItem>
|
|
49
|
+
</>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react'
|
|
2
|
+
import styled from 'styled-components'
|
|
3
|
+
import { theme } from '../../../styled'
|
|
4
|
+
|
|
5
|
+
export type CustomJSXElement =
|
|
6
|
+
| keyof JSX.IntrinsicElements
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
| React.JSXElementConstructor<any>
|
|
9
|
+
|
|
10
|
+
export type ListItemProps<T extends CustomJSXElement = 'div'> = {
|
|
11
|
+
children?: ReactNode
|
|
12
|
+
as?: T
|
|
13
|
+
} & Omit<React.ComponentProps<T>, 'children'>
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* リストのある要素を示すコンポーネント
|
|
17
|
+
*
|
|
18
|
+
* asを用いて拡張することができる
|
|
19
|
+
* @example
|
|
20
|
+
* ```
|
|
21
|
+
* <ListItem as="a" href="#">Link</ListItem>
|
|
22
|
+
* <ListItem as={NextLink} href="#">NextLink</ListItem>
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export default function ListItem<T extends CustomJSXElement = 'div'>(
|
|
26
|
+
props: ListItemProps<T>
|
|
27
|
+
) {
|
|
28
|
+
const { children, ...rest } = props
|
|
29
|
+
return (
|
|
30
|
+
<StyledLi role="option">
|
|
31
|
+
<ItemDiv {...rest}>{props.children}</ItemDiv>
|
|
32
|
+
</StyledLi>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const StyledLi = styled.li`
|
|
37
|
+
list-style: none;
|
|
38
|
+
`
|
|
39
|
+
|
|
40
|
+
const ItemDiv = styled.div`
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
min-height: 40px;
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
outline: none;
|
|
46
|
+
|
|
47
|
+
${theme((o) => [o.padding.horizontal(16), o.disabled])}
|
|
48
|
+
|
|
49
|
+
&[aria-disabled="true"] {
|
|
50
|
+
cursor: default;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
:hover,
|
|
54
|
+
:focus,
|
|
55
|
+
:focus-within {
|
|
56
|
+
${theme((o) => [o.bg.surface3])}
|
|
57
|
+
}
|
|
58
|
+
`
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import ListItem, { CustomJSXElement, ListItemProps } from '../ListItem'
|
|
3
|
+
import { useMenuItemHandleKeyDown } from './internals/useMenuItemHandleKeyDown'
|
|
4
|
+
|
|
5
|
+
export type MenuItemProps<T extends CustomJSXElement = never> = {
|
|
6
|
+
value?: string
|
|
7
|
+
disabled?: boolean
|
|
8
|
+
} & ListItemProps<T>
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 上下キーでフォーカス移動でき、エンターキーで選択できるリストの項目
|
|
12
|
+
* 基本的に`<MenuList>`, `<MenuGroup>`と合わせて使用する
|
|
13
|
+
*/
|
|
14
|
+
export default function MenuItem<T extends CustomJSXElement>(
|
|
15
|
+
props: MenuItemProps<T>
|
|
16
|
+
) {
|
|
17
|
+
const { children, as, ...rest } = props
|
|
18
|
+
const [handleKeyDown, setContextValue] = useMenuItemHandleKeyDown(props.value)
|
|
19
|
+
return (
|
|
20
|
+
<ListItem
|
|
21
|
+
{...rest}
|
|
22
|
+
as={as as CustomJSXElement}
|
|
23
|
+
data-key={props.value}
|
|
24
|
+
onKeyDown={handleKeyDown}
|
|
25
|
+
onClick={props.disabled === true ? undefined : setContextValue}
|
|
26
|
+
tabIndex={-1}
|
|
27
|
+
aria-disabled={props.disabled}
|
|
28
|
+
>
|
|
29
|
+
{props.children}
|
|
30
|
+
</ListItem>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* elementをparentのスクロールビューに入るようにスクロールする
|
|
3
|
+
* parentがスクロール可能でなければelementが見えるようにスクロールする
|
|
4
|
+
*
|
|
5
|
+
* @param element
|
|
6
|
+
* @param parent
|
|
7
|
+
*/
|
|
8
|
+
export function handleFocusByKeyBoard(element: Element, parent: HTMLElement) {
|
|
9
|
+
const isScrollable = parent.scrollHeight > parent.clientHeight
|
|
10
|
+
if (isScrollable) {
|
|
11
|
+
const rect = element.getBoundingClientRect()
|
|
12
|
+
const parentRect = parent.getBoundingClientRect()
|
|
13
|
+
if (rect.bottom > parentRect.bottom) {
|
|
14
|
+
parent.scrollTo({
|
|
15
|
+
top: parent.scrollTop + rect.bottom - parentRect.bottom,
|
|
16
|
+
})
|
|
17
|
+
} else if (rect.top < parentRect.top) {
|
|
18
|
+
parent.scrollTo({
|
|
19
|
+
top: parent.scrollTop - (parentRect.top - rect.top),
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
} else {
|
|
23
|
+
scrollIfNeeded(element)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 要素が画面外にあればスクロールする、画面内にあればスクロールしない
|
|
29
|
+
* @param element
|
|
30
|
+
*/
|
|
31
|
+
function scrollIfNeeded(element: Element) {
|
|
32
|
+
const elementRect = element.getBoundingClientRect()
|
|
33
|
+
const isVisible =
|
|
34
|
+
elementRect.top >= 0 &&
|
|
35
|
+
elementRect.bottom <=
|
|
36
|
+
(window.innerHeight || document.documentElement.clientHeight)
|
|
37
|
+
|
|
38
|
+
if (!isVisible) {
|
|
39
|
+
element.scrollIntoView({
|
|
40
|
+
block: 'nearest',
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React, { useCallback, useContext } from 'react'
|
|
2
|
+
import { handleFocusByKeyBoard } from './handleFocusByKeyBoard'
|
|
3
|
+
import { MenuListContext } from '../../MenuList/MenuListContext'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* MenuListContextに含まれるvalue間で上下キーでfocusを移動できる
|
|
7
|
+
* EnterキーでMenuListContextに値を設定する
|
|
8
|
+
* 上記2つの処理を含む処理(handleKeyDown)と、Enterキーを押下した処理(setContextValue)を配列で返す
|
|
9
|
+
* @param value
|
|
10
|
+
* @returns
|
|
11
|
+
*/
|
|
12
|
+
export function useMenuItemHandleKeyDown(
|
|
13
|
+
value?: string
|
|
14
|
+
): [(e: React.KeyboardEvent<HTMLDivElement>) => void, () => void] {
|
|
15
|
+
const { setValue, root, values } = useContext(MenuListContext)
|
|
16
|
+
const setContextValue = useCallback(() => {
|
|
17
|
+
if (value !== undefined) setValue(value)
|
|
18
|
+
}, [value, setValue])
|
|
19
|
+
|
|
20
|
+
const handleKeyDown = useCallback(
|
|
21
|
+
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
22
|
+
if (e.key === 'Enter') {
|
|
23
|
+
setContextValue()
|
|
24
|
+
} else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
|
25
|
+
// prevent scroll
|
|
26
|
+
e.preventDefault()
|
|
27
|
+
if (!values || value === undefined) return
|
|
28
|
+
const index = values.indexOf(value)
|
|
29
|
+
if (index === -1) return
|
|
30
|
+
|
|
31
|
+
const focusValue =
|
|
32
|
+
e.key === 'ArrowUp'
|
|
33
|
+
? // prev or last
|
|
34
|
+
index - 1 < 0
|
|
35
|
+
? values[values.length - 1]
|
|
36
|
+
: values[index - 1]
|
|
37
|
+
: // next or first
|
|
38
|
+
index + 1 >= values.length
|
|
39
|
+
? values[0]
|
|
40
|
+
: values[index + 1]
|
|
41
|
+
|
|
42
|
+
const next = root?.current?.querySelector(`[data-key='${focusValue}']`)
|
|
43
|
+
|
|
44
|
+
if (next instanceof HTMLElement) {
|
|
45
|
+
next.focus({ preventScroll: true })
|
|
46
|
+
if (root?.current?.parentElement) {
|
|
47
|
+
handleFocusByKeyBoard(next, root.current.parentElement)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
[setContextValue, value, root, values]
|
|
53
|
+
)
|
|
54
|
+
return [handleKeyDown, setContextValue]
|
|
55
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import styled from 'styled-components'
|
|
3
|
+
import MenuItem from '../MenuItem'
|
|
4
|
+
import { Divider } from '../Divider'
|
|
5
|
+
|
|
6
|
+
type MenuItemGroupChild = React.ReactElement<typeof MenuItem | typeof Divider>
|
|
7
|
+
|
|
8
|
+
export type MenuItemGroupProps = {
|
|
9
|
+
text: string
|
|
10
|
+
children: MenuItemGroupChild | MenuItemGroupChild[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 項目のリストを分類する見出しをつけるコンテナ要素
|
|
15
|
+
*/
|
|
16
|
+
export default function MenuItemGroup(props: MenuItemGroupProps) {
|
|
17
|
+
return (
|
|
18
|
+
<StyledLi role="presentation">
|
|
19
|
+
<TextSpan>{props.text}</TextSpan>
|
|
20
|
+
<StyledUl role="group">{props.children}</StyledUl>
|
|
21
|
+
</StyledLi>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const TextSpan = styled.span`
|
|
26
|
+
display: block;
|
|
27
|
+
color: ${({ theme }) => theme.color.text3};
|
|
28
|
+
font-size: 12px;
|
|
29
|
+
font-weight: bold;
|
|
30
|
+
padding: 12px 0 8px 16px;
|
|
31
|
+
`
|
|
32
|
+
|
|
33
|
+
const StyledUl = styled.ul`
|
|
34
|
+
padding-left: 0;
|
|
35
|
+
margin: 0;
|
|
36
|
+
box-sizing: border-box;
|
|
37
|
+
list-style: none;
|
|
38
|
+
overflow: hidden;
|
|
39
|
+
`
|
|
40
|
+
|
|
41
|
+
const StyledLi = styled.li`
|
|
42
|
+
display: block;
|
|
43
|
+
`
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { RefObject, createContext } from 'react'
|
|
2
|
+
|
|
3
|
+
type MenuListContextType = {
|
|
4
|
+
root?: RefObject<HTMLUListElement>
|
|
5
|
+
value?: string
|
|
6
|
+
values?: string[]
|
|
7
|
+
setValue: (v: string) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const MenuListContext = createContext<MenuListContextType>({
|
|
11
|
+
root: undefined,
|
|
12
|
+
value: '',
|
|
13
|
+
values: [],
|
|
14
|
+
setValue: (_v: string) => {
|
|
15
|
+
// empty
|
|
16
|
+
},
|
|
17
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { action } from '@storybook/addon-actions'
|
|
3
|
+
import { Story } from '../../../_lib/compat'
|
|
4
|
+
import MenuList, { MenuListProps } from '.'
|
|
5
|
+
import MenuItem from '../MenuItem'
|
|
6
|
+
import MenuItemGroup from '../MenuItemGroup'
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
title: 'DropdownSelector/MenuList',
|
|
10
|
+
component: MenuList,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function makeList(n: number, offset = 0) {
|
|
14
|
+
return [...(Array(n) as undefined[])].map((_, i) => {
|
|
15
|
+
const v = i + offset
|
|
16
|
+
return (
|
|
17
|
+
<MenuItem key={v} value={v.toString()}>
|
|
18
|
+
Menu {v}
|
|
19
|
+
</MenuItem>
|
|
20
|
+
)
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const Basic: Story<MenuListProps> = () => {
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
<MenuList onChange={action('onChange')}>{makeList(10)}</MenuList>
|
|
28
|
+
</>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const Disabled: Story<MenuListProps> = () => {
|
|
33
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
<MenuList onChange={action('onChange')}>
|
|
36
|
+
<MenuItem value="1">MenuItem</MenuItem>
|
|
37
|
+
<MenuItem value="2" disabled>
|
|
38
|
+
Disabled MenuItem
|
|
39
|
+
</MenuItem>
|
|
40
|
+
</MenuList>
|
|
41
|
+
</>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const Group: Story<MenuListProps> = () => {
|
|
46
|
+
return (
|
|
47
|
+
<MenuList onChange={action('onChange')} value="1">
|
|
48
|
+
<MenuItemGroup text="Section1">{makeList(5)}</MenuItemGroup>
|
|
49
|
+
<MenuItemGroup text="Section2">{makeList(5, 5)}</MenuItemGroup>
|
|
50
|
+
</MenuList>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React, { useRef } from 'react'
|
|
2
|
+
import styled from 'styled-components'
|
|
3
|
+
import { MenuListContext } from './MenuListContext'
|
|
4
|
+
import { getValuesRecursive } from './internals/getValuesRecursive'
|
|
5
|
+
import MenuItem from '../MenuItem'
|
|
6
|
+
import { Divider } from '../Divider'
|
|
7
|
+
import MenuItemGroup from '../MenuItemGroup'
|
|
8
|
+
|
|
9
|
+
type MenuListChild = React.ReactElement<
|
|
10
|
+
typeof MenuItem | typeof MenuItemGroup | typeof Divider
|
|
11
|
+
>
|
|
12
|
+
|
|
13
|
+
export type MenuListChildren = MenuListChild | MenuListChild[]
|
|
14
|
+
|
|
15
|
+
export type MenuListProps = {
|
|
16
|
+
children: MenuListChildren
|
|
17
|
+
value?: string
|
|
18
|
+
onChange?: (v: string) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 上下キーでフォーカス移動でき、エンターキーで選択できるリストの項目
|
|
23
|
+
* 基本的に`<MenuItem>`, `<MenuGroup>`と合わせて使用する
|
|
24
|
+
*/
|
|
25
|
+
export default function MenuList(props: MenuListProps) {
|
|
26
|
+
const root = useRef(null)
|
|
27
|
+
const values: string[] = []
|
|
28
|
+
getValuesRecursive(props.children, values)
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<StyledUl ref={root}>
|
|
32
|
+
<MenuListContext.Provider
|
|
33
|
+
value={{
|
|
34
|
+
value: props.value ?? '',
|
|
35
|
+
root,
|
|
36
|
+
values,
|
|
37
|
+
setValue: (v) => {
|
|
38
|
+
props.onChange?.(v)
|
|
39
|
+
},
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
{props.children}
|
|
43
|
+
</MenuListContext.Provider>
|
|
44
|
+
</StyledUl>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const StyledUl = styled.ul`
|
|
49
|
+
padding: 0;
|
|
50
|
+
margin: 0;
|
|
51
|
+
`
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import MenuItem from '../../MenuItem'
|
|
3
|
+
import { MenuListChildren } from '..'
|
|
4
|
+
import MenuItemGroup from '../../MenuItemGroup'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* valueというpropsを持つ子要素の値を再起的に探索して配列にする
|
|
8
|
+
*
|
|
9
|
+
* @param children
|
|
10
|
+
* @param value
|
|
11
|
+
* @param values
|
|
12
|
+
* @returns
|
|
13
|
+
*/
|
|
14
|
+
export function getValuesRecursive(
|
|
15
|
+
children: MenuListChildren,
|
|
16
|
+
values: string[] = []
|
|
17
|
+
) {
|
|
18
|
+
const childArray = React.Children.toArray(children)
|
|
19
|
+
for (let i = 0; i < childArray.length; i++) {
|
|
20
|
+
const child = childArray[i]
|
|
21
|
+
if (React.isValidElement(child)) {
|
|
22
|
+
const props = child.props as {
|
|
23
|
+
value?: never
|
|
24
|
+
children?: React.ReactElement<typeof MenuItem | typeof MenuItemGroup>[]
|
|
25
|
+
}
|
|
26
|
+
if ('value' in props && typeof props.value === 'string') {
|
|
27
|
+
const childValue = props.value
|
|
28
|
+
values.push(childValue)
|
|
29
|
+
}
|
|
30
|
+
if ('children' in props && props.children) {
|
|
31
|
+
getValuesRecursive(props.children, values)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import React, { useRef, CSSProperties, useState } from 'react'
|
|
2
|
+
import { Story } from '../../../_lib/compat'
|
|
3
|
+
import Popover, { PopoverProps } from '.'
|
|
4
|
+
import Button from '../../Button'
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
title: 'DropdownSelector/Popover',
|
|
8
|
+
component: Popover,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function Base(props: { style?: CSSProperties }) {
|
|
12
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
13
|
+
const triggerRef = useRef(null)
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
<Button
|
|
17
|
+
onClick={() => {
|
|
18
|
+
setIsOpen(true)
|
|
19
|
+
}}
|
|
20
|
+
style={props.style}
|
|
21
|
+
ref={triggerRef}
|
|
22
|
+
>
|
|
23
|
+
button
|
|
24
|
+
</Button>
|
|
25
|
+
<Popover
|
|
26
|
+
isOpen={isOpen}
|
|
27
|
+
onClose={() => setIsOpen(false)}
|
|
28
|
+
triggerRef={triggerRef}
|
|
29
|
+
>
|
|
30
|
+
<div style={{ margin: '8px 16px' }}>Hello</div>
|
|
31
|
+
</Popover>
|
|
32
|
+
</>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const Basic: Story<PopoverProps> = () => {
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
<Base
|
|
40
|
+
style={{
|
|
41
|
+
position: 'absolute',
|
|
42
|
+
}}
|
|
43
|
+
/>
|
|
44
|
+
<Base
|
|
45
|
+
style={{
|
|
46
|
+
position: 'absolute',
|
|
47
|
+
right: 8,
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
<Base
|
|
51
|
+
style={{
|
|
52
|
+
position: 'absolute',
|
|
53
|
+
bottom: 8,
|
|
54
|
+
}}
|
|
55
|
+
/>
|
|
56
|
+
<Base
|
|
57
|
+
style={{
|
|
58
|
+
position: 'absolute',
|
|
59
|
+
right: 8,
|
|
60
|
+
bottom: 8,
|
|
61
|
+
}}
|
|
62
|
+
/>
|
|
63
|
+
</>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React, { RefObject, useRef } from 'react'
|
|
2
|
+
import { ReactNode } from 'react'
|
|
3
|
+
import { DismissButton, Overlay, usePopover } from '@react-aria/overlays'
|
|
4
|
+
import styled from 'styled-components'
|
|
5
|
+
import { theme } from '../../../styled'
|
|
6
|
+
|
|
7
|
+
export type PopoverProps = {
|
|
8
|
+
isOpen: boolean
|
|
9
|
+
onClose: () => void
|
|
10
|
+
children: ReactNode
|
|
11
|
+
triggerRef: RefObject<Element>
|
|
12
|
+
popoverRef?: RefObject<HTMLDivElement>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const _empty = () => null
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 画面の全面に動的に開くことができるコンテナ要素
|
|
19
|
+
* 外の要素をクリックしたり、内部からフォーカスを移動した場合に自動的に閉じる
|
|
20
|
+
*
|
|
21
|
+
* triggerRefの付近に画面内に収まるように表示される
|
|
22
|
+
*/
|
|
23
|
+
export default function Popover(props: PopoverProps) {
|
|
24
|
+
const defaultPopoverRef = useRef<HTMLDivElement>(null)
|
|
25
|
+
const finalPopoverRef =
|
|
26
|
+
props.popoverRef === undefined ? defaultPopoverRef : props.popoverRef
|
|
27
|
+
const { popoverProps, underlayProps } = usePopover(
|
|
28
|
+
{
|
|
29
|
+
triggerRef: props.triggerRef,
|
|
30
|
+
popoverRef: finalPopoverRef,
|
|
31
|
+
containerPadding: 16,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
close: props.onClose,
|
|
35
|
+
isOpen: props.isOpen,
|
|
36
|
+
// never used
|
|
37
|
+
open: _empty,
|
|
38
|
+
setOpen: _empty,
|
|
39
|
+
toggle: _empty,
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if (!props.isOpen) return null
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Overlay portalContainer={document.body}>
|
|
47
|
+
<div {...underlayProps} style={{ position: 'fixed', inset: 0 }} />
|
|
48
|
+
<DropdownPopoverDiv {...popoverProps} ref={finalPopoverRef}>
|
|
49
|
+
<DismissButton onDismiss={() => props.onClose()} />
|
|
50
|
+
{props.children}
|
|
51
|
+
<DismissButton onDismiss={() => props.onClose()} />
|
|
52
|
+
</DropdownPopoverDiv>
|
|
53
|
+
</Overlay>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const DropdownPopoverDiv = styled.div`
|
|
58
|
+
margin: 4px 0;
|
|
59
|
+
list-style: none;
|
|
60
|
+
overflow: auto;
|
|
61
|
+
max-height: inherit;
|
|
62
|
+
|
|
63
|
+
${theme((o) => [
|
|
64
|
+
o.bg.background1,
|
|
65
|
+
o.border.default,
|
|
66
|
+
o.borderRadius(8),
|
|
67
|
+
o.padding.vertical(8),
|
|
68
|
+
])}
|
|
69
|
+
`
|