@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.
Files changed (174) hide show
  1. package/dist/_lib/compat.d.ts +1 -1
  2. package/dist/_lib/compat.d.ts.map +1 -1
  3. package/dist/components/Button/index.d.ts +1 -2
  4. package/dist/components/Button/index.d.ts.map +1 -1
  5. package/dist/components/Button/index.story.d.ts +1 -2
  6. package/dist/components/Button/index.story.d.ts.map +1 -1
  7. package/dist/components/Button/index.test.d.ts +4 -0
  8. package/dist/components/Button/index.test.d.ts.map +1 -0
  9. package/dist/components/Checkbox/index.d.ts +1 -1
  10. package/dist/components/Checkbox/index.d.ts.map +1 -1
  11. package/dist/components/Checkbox/index.story.d.ts +1 -2
  12. package/dist/components/Checkbox/index.story.d.ts.map +1 -1
  13. package/dist/components/Clickable/index.d.ts +1 -1
  14. package/dist/components/Clickable/index.d.ts.map +1 -1
  15. package/dist/components/Clickable/index.story.d.ts +1 -2
  16. package/dist/components/Clickable/index.story.d.ts.map +1 -1
  17. package/dist/components/DropdownSelector/Divider.d.ts +3 -0
  18. package/dist/components/DropdownSelector/Divider.d.ts.map +1 -1
  19. package/dist/components/DropdownSelector/DropdownMenuItem.d.ts +7 -0
  20. package/dist/components/DropdownSelector/DropdownMenuItem.d.ts.map +1 -0
  21. package/dist/components/DropdownSelector/DropdownPopover.d.ts +8 -8
  22. package/dist/components/DropdownSelector/DropdownPopover.d.ts.map +1 -1
  23. package/dist/components/DropdownSelector/ListItem/index.d.ts +18 -0
  24. package/dist/components/DropdownSelector/ListItem/index.d.ts.map +1 -0
  25. package/dist/components/DropdownSelector/ListItem/index.story.d.ts +9 -0
  26. package/dist/components/DropdownSelector/ListItem/index.story.d.ts.map +1 -0
  27. package/dist/components/DropdownSelector/MenuItem/index.d.ts +11 -0
  28. package/dist/components/DropdownSelector/MenuItem/index.d.ts.map +1 -0
  29. package/dist/components/DropdownSelector/MenuItem/internals/handleFocusByKeyBoard.d.ts +9 -0
  30. package/dist/components/DropdownSelector/MenuItem/internals/handleFocusByKeyBoard.d.ts.map +1 -0
  31. package/dist/components/DropdownSelector/MenuItem/internals/useMenuItemHandleKeyDown.d.ts +10 -0
  32. package/dist/components/DropdownSelector/MenuItem/internals/useMenuItemHandleKeyDown.d.ts.map +1 -0
  33. package/dist/components/DropdownSelector/MenuItemGroup/index.d.ts +14 -0
  34. package/dist/components/DropdownSelector/MenuItemGroup/index.d.ts.map +1 -0
  35. package/dist/components/DropdownSelector/MenuList/MenuListContext.d.ts +10 -0
  36. package/dist/components/DropdownSelector/MenuList/MenuListContext.d.ts.map +1 -0
  37. package/dist/components/DropdownSelector/MenuList/index.d.ts +18 -0
  38. package/dist/components/DropdownSelector/MenuList/index.d.ts.map +1 -0
  39. package/dist/components/DropdownSelector/MenuList/index.story.d.ts +11 -0
  40. package/dist/components/DropdownSelector/MenuList/index.story.d.ts.map +1 -0
  41. package/dist/components/DropdownSelector/MenuList/internals/getValuesRecursive.d.ts +11 -0
  42. package/dist/components/DropdownSelector/MenuList/internals/getValuesRecursive.d.ts.map +1 -0
  43. package/dist/components/DropdownSelector/Popover/index.d.ts +17 -0
  44. package/dist/components/DropdownSelector/Popover/index.d.ts.map +1 -0
  45. package/dist/components/DropdownSelector/Popover/index.story.d.ts +9 -0
  46. package/dist/components/DropdownSelector/Popover/index.story.d.ts.map +1 -0
  47. package/dist/components/DropdownSelector/index.d.ts +3 -10
  48. package/dist/components/DropdownSelector/index.d.ts.map +1 -1
  49. package/dist/components/DropdownSelector/index.story.d.ts +4 -4
  50. package/dist/components/DropdownSelector/index.story.d.ts.map +1 -1
  51. package/dist/components/DropdownSelector/utils/findPreviewRecursive.d.ts +12 -0
  52. package/dist/components/DropdownSelector/utils/findPreviewRecursive.d.ts.map +1 -0
  53. package/dist/components/FieldLabel/index.d.ts +1 -1
  54. package/dist/components/FieldLabel/index.d.ts.map +1 -1
  55. package/dist/components/Icon/index.d.ts +1 -1
  56. package/dist/components/Icon/index.d.ts.map +1 -1
  57. package/dist/components/Icon/index.story.d.ts +2 -3
  58. package/dist/components/Icon/index.story.d.ts.map +1 -1
  59. package/dist/components/IconButton/index.d.ts +1 -2
  60. package/dist/components/IconButton/index.d.ts.map +1 -1
  61. package/dist/components/IconButton/index.story.d.ts +1 -2
  62. package/dist/components/IconButton/index.story.d.ts.map +1 -1
  63. package/dist/components/LoadingSpinner/index.d.ts +1 -2
  64. package/dist/components/LoadingSpinner/index.d.ts.map +1 -1
  65. package/dist/components/Modal/ModalPlumbing.d.ts.map +1 -1
  66. package/dist/components/Modal/index.d.ts +1 -1
  67. package/dist/components/Modal/index.d.ts.map +1 -1
  68. package/dist/components/Modal/index.story.d.ts.map +1 -1
  69. package/dist/components/MultiSelect/context.d.ts +1 -1
  70. package/dist/components/MultiSelect/context.d.ts.map +1 -1
  71. package/dist/components/MultiSelect/index.d.ts +5 -6
  72. package/dist/components/MultiSelect/index.d.ts.map +1 -1
  73. package/dist/components/MultiSelect/index.story.d.ts +7 -14
  74. package/dist/components/MultiSelect/index.story.d.ts.map +1 -1
  75. package/dist/components/Radio/index.d.ts +1 -1
  76. package/dist/components/Radio/index.d.ts.map +1 -1
  77. package/dist/components/Radio/index.story.d.ts +1 -2
  78. package/dist/components/Radio/index.story.d.ts.map +1 -1
  79. package/dist/components/SegmentedControl/RadioGroupContext.d.ts +1 -1
  80. package/dist/components/SegmentedControl/RadioGroupContext.d.ts.map +1 -1
  81. package/dist/components/SegmentedControl/index.d.ts +1 -1
  82. package/dist/components/SegmentedControl/index.d.ts.map +1 -1
  83. package/dist/components/SegmentedControl/index.story.d.ts +1 -2
  84. package/dist/components/SegmentedControl/index.story.d.ts.map +1 -1
  85. package/dist/components/Switch/index.d.ts +1 -1
  86. package/dist/components/Switch/index.d.ts.map +1 -1
  87. package/dist/components/TagItem/index.d.ts +3 -3
  88. package/dist/components/TagItem/index.d.ts.map +1 -1
  89. package/dist/components/TagItem/index.story.d.ts +2 -3
  90. package/dist/components/TagItem/index.story.d.ts.map +1 -1
  91. package/dist/components/TextArea/TextArea.story.d.ts +28 -0
  92. package/dist/components/TextArea/TextArea.story.d.ts.map +1 -0
  93. package/dist/components/TextArea/index.d.ts +28 -0
  94. package/dist/components/TextArea/index.d.ts.map +1 -0
  95. package/dist/components/TextField/TextField.story.d.ts +29 -0
  96. package/dist/components/TextField/TextField.story.d.ts.map +1 -0
  97. package/dist/components/TextField/index.d.ts +2 -1
  98. package/dist/components/TextField/index.d.ts.map +1 -1
  99. package/dist/components/TextField/index.story.d.ts +4 -5
  100. package/dist/components/TextField/index.story.d.ts.map +1 -1
  101. package/dist/core/CharcoalProvider.d.ts +1 -1
  102. package/dist/core/CharcoalProvider.d.ts.map +1 -1
  103. package/dist/core/ComponentAbstraction.d.ts +1 -1
  104. package/dist/core/ComponentAbstraction.d.ts.map +1 -1
  105. package/dist/index.cjs.js +744 -493
  106. package/dist/index.cjs.js.map +1 -1
  107. package/dist/index.d.ts +3 -2
  108. package/dist/index.d.ts.map +1 -1
  109. package/dist/index.esm.js +689 -452
  110. package/dist/index.esm.js.map +1 -1
  111. package/dist/styled.d.ts +13 -13
  112. package/dist/types/CustomJSXElement.d.ts +3 -0
  113. package/dist/types/CustomJSXElement.d.ts.map +1 -0
  114. package/package.json +7 -7
  115. package/src/_lib/compat.ts +1 -1
  116. package/src/components/Button/__snapshots__/index.test.tsx.snap +385 -0
  117. package/src/components/Button/index.story.tsx +1 -1
  118. package/src/components/Button/index.test.tsx +24 -0
  119. package/src/components/Button/index.tsx +2 -2
  120. package/src/components/Checkbox/index.story.tsx +0 -1
  121. package/src/components/Checkbox/index.tsx +2 -1
  122. package/src/components/Clickable/index.story.tsx +0 -1
  123. package/src/components/Clickable/index.tsx +1 -1
  124. package/src/components/DropdownSelector/Divider.tsx +3 -0
  125. package/src/components/DropdownSelector/DropdownMenuItem.tsx +40 -0
  126. package/src/components/DropdownSelector/DropdownPopover.tsx +21 -42
  127. package/src/components/DropdownSelector/ListItem/index.story.tsx +51 -0
  128. package/src/components/DropdownSelector/ListItem/index.tsx +58 -0
  129. package/src/components/DropdownSelector/MenuItem/index.tsx +32 -0
  130. package/src/components/DropdownSelector/MenuItem/internals/handleFocusByKeyBoard.tsx +43 -0
  131. package/src/components/DropdownSelector/MenuItem/internals/useMenuItemHandleKeyDown.tsx +55 -0
  132. package/src/components/DropdownSelector/MenuItemGroup/index.tsx +43 -0
  133. package/src/components/DropdownSelector/MenuList/MenuListContext.ts +17 -0
  134. package/src/components/DropdownSelector/MenuList/index.story.tsx +52 -0
  135. package/src/components/DropdownSelector/MenuList/index.tsx +51 -0
  136. package/src/components/DropdownSelector/MenuList/internals/getValuesRecursive.tsx +35 -0
  137. package/src/components/DropdownSelector/Popover/index.story.tsx +65 -0
  138. package/src/components/DropdownSelector/Popover/index.tsx +69 -0
  139. package/src/components/DropdownSelector/index.story.tsx +56 -21
  140. package/src/components/DropdownSelector/index.tsx +21 -64
  141. package/src/components/DropdownSelector/utils/findPreviewRecursive.tsx +38 -0
  142. package/src/components/FieldLabel/index.tsx +1 -1
  143. package/src/components/Icon/index.story.tsx +0 -1
  144. package/src/components/Icon/index.tsx +1 -1
  145. package/src/components/IconButton/index.story.tsx +0 -1
  146. package/src/components/IconButton/index.tsx +2 -2
  147. package/src/components/LoadingSpinner/index.story.tsx +1 -1
  148. package/src/components/LoadingSpinner/index.tsx +18 -19
  149. package/src/components/Modal/ModalPlumbing.tsx +0 -1
  150. package/src/components/Modal/index.story.tsx +0 -1
  151. package/src/components/Modal/index.tsx +2 -1
  152. package/src/components/MultiSelect/context.ts +2 -2
  153. package/src/components/MultiSelect/index.story.tsx +16 -29
  154. package/src/components/MultiSelect/index.test.tsx +5 -23
  155. package/src/components/MultiSelect/index.tsx +19 -24
  156. package/src/components/Radio/index.story.tsx +0 -1
  157. package/src/components/Radio/index.test.tsx +0 -1
  158. package/src/components/Radio/index.tsx +2 -1
  159. package/src/components/SegmentedControl/RadioGroupContext.tsx +2 -1
  160. package/src/components/SegmentedControl/index.story.tsx +0 -1
  161. package/src/components/SegmentedControl/index.tsx +10 -4
  162. package/src/components/Switch/index.story.tsx +1 -1
  163. package/src/components/Switch/index.tsx +2 -1
  164. package/src/components/TagItem/index.story.tsx +0 -1
  165. package/src/components/TagItem/index.tsx +1 -6
  166. package/src/components/TextField/index.story.tsx +0 -1
  167. package/src/components/TextField/index.tsx +2 -7
  168. package/src/components/a11y.test.tsx +0 -1
  169. package/src/core/CharcoalProvider.tsx +1 -1
  170. package/src/core/ComponentAbstraction.tsx +2 -1
  171. package/src/index.ts +7 -4
  172. package/src/components/DropdownSelector/OptionItem.tsx +0 -85
  173. package/src/components/DropdownSelector/utils/focusIfHTMLLIElement.tsx +0 -12
  174. package/src/components/DropdownSelector/utils/handleFocusByKeyBoard.tsx +0 -20
@@ -1,46 +1,25 @@
1
- import React, { Key, useEffect, useRef } from 'react'
2
- import { OverlayTriggerState } from 'react-stately'
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
- const DropdownPopoverDiv = styled.div`
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
- export function DropdownPopover({ children, state, ...props }: Props) {
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 (state.isOpen && props.value !== undefined) {
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, state.isOpen])
33
+ }, [props.value, props.isOpen])
55
34
 
56
35
  return (
57
- <Overlay portalContainer={document.body}>
58
- <div {...underlayProps} style={{ position: 'fixed', inset: 0 }} />
59
- <DropdownPopoverDiv {...popoverProps} ref={ref}>
60
- <DismissButton onDismiss={() => state.close()} />
61
- {children}
62
- <DismissButton onDismiss={() => state.close()} />
63
- </DropdownPopoverDiv>
64
- </Overlay>
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
+ `