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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/dist/_lib/compat.d.ts +19 -1
  2. package/dist/_lib/compat.d.ts.map +1 -1
  3. package/dist/_lib/index.d.ts +7 -0
  4. package/dist/_lib/index.d.ts.map +1 -1
  5. package/dist/components/Button/index.d.ts +1 -2
  6. package/dist/components/Button/index.d.ts.map +1 -1
  7. package/dist/components/Button/index.story.d.ts +1 -2
  8. package/dist/components/Button/index.story.d.ts.map +1 -1
  9. package/dist/components/Button/index.test.d.ts +4 -0
  10. package/dist/components/Button/index.test.d.ts.map +1 -0
  11. package/dist/components/Checkbox/index.d.ts +2 -1
  12. package/dist/components/Checkbox/index.d.ts.map +1 -1
  13. package/dist/components/Checkbox/index.story.d.ts +2 -2
  14. package/dist/components/Checkbox/index.story.d.ts.map +1 -1
  15. package/dist/components/Clickable/index.d.ts +1 -1
  16. package/dist/components/Clickable/index.d.ts.map +1 -1
  17. package/dist/components/Clickable/index.story.d.ts +1 -2
  18. package/dist/components/Clickable/index.story.d.ts.map +1 -1
  19. package/dist/components/DropdownSelector/Divider.d.ts +3 -0
  20. package/dist/components/DropdownSelector/Divider.d.ts.map +1 -1
  21. package/dist/components/DropdownSelector/DropdownMenuItem.d.ts +7 -0
  22. package/dist/components/DropdownSelector/DropdownMenuItem.d.ts.map +1 -0
  23. package/dist/components/DropdownSelector/DropdownPopover.d.ts +8 -8
  24. package/dist/components/DropdownSelector/DropdownPopover.d.ts.map +1 -1
  25. package/dist/components/DropdownSelector/ListItem/index.d.ts +18 -0
  26. package/dist/components/DropdownSelector/ListItem/index.d.ts.map +1 -0
  27. package/dist/components/DropdownSelector/ListItem/index.story.d.ts +9 -0
  28. package/dist/components/DropdownSelector/ListItem/index.story.d.ts.map +1 -0
  29. package/dist/components/DropdownSelector/MenuItem/index.d.ts +11 -0
  30. package/dist/components/DropdownSelector/MenuItem/index.d.ts.map +1 -0
  31. package/dist/components/DropdownSelector/MenuItem/internals/handleFocusByKeyBoard.d.ts +9 -0
  32. package/dist/components/DropdownSelector/MenuItem/internals/handleFocusByKeyBoard.d.ts.map +1 -0
  33. package/dist/components/DropdownSelector/MenuItem/internals/useMenuItemHandleKeyDown.d.ts +10 -0
  34. package/dist/components/DropdownSelector/MenuItem/internals/useMenuItemHandleKeyDown.d.ts.map +1 -0
  35. package/dist/components/DropdownSelector/MenuItemGroup/index.d.ts +14 -0
  36. package/dist/components/DropdownSelector/MenuItemGroup/index.d.ts.map +1 -0
  37. package/dist/components/DropdownSelector/MenuList/MenuListContext.d.ts +10 -0
  38. package/dist/components/DropdownSelector/MenuList/MenuListContext.d.ts.map +1 -0
  39. package/dist/components/DropdownSelector/MenuList/index.d.ts +18 -0
  40. package/dist/components/DropdownSelector/MenuList/index.d.ts.map +1 -0
  41. package/dist/components/DropdownSelector/MenuList/index.story.d.ts +11 -0
  42. package/dist/components/DropdownSelector/MenuList/index.story.d.ts.map +1 -0
  43. package/dist/components/DropdownSelector/MenuList/internals/getValuesRecursive.d.ts +11 -0
  44. package/dist/components/DropdownSelector/MenuList/internals/getValuesRecursive.d.ts.map +1 -0
  45. package/dist/components/DropdownSelector/Popover/index.d.ts +17 -0
  46. package/dist/components/DropdownSelector/Popover/index.d.ts.map +1 -0
  47. package/dist/components/DropdownSelector/Popover/index.story.d.ts +9 -0
  48. package/dist/components/DropdownSelector/Popover/index.story.d.ts.map +1 -0
  49. package/dist/components/DropdownSelector/index.d.ts +3 -10
  50. package/dist/components/DropdownSelector/index.d.ts.map +1 -1
  51. package/dist/components/DropdownSelector/index.story.d.ts +4 -4
  52. package/dist/components/DropdownSelector/index.story.d.ts.map +1 -1
  53. package/dist/components/DropdownSelector/utils/findPreviewRecursive.d.ts +12 -0
  54. package/dist/components/DropdownSelector/utils/findPreviewRecursive.d.ts.map +1 -0
  55. package/dist/components/FieldLabel/index.d.ts +1 -1
  56. package/dist/components/FieldLabel/index.d.ts.map +1 -1
  57. package/dist/components/Icon/index.d.ts +1 -1
  58. package/dist/components/Icon/index.d.ts.map +1 -1
  59. package/dist/components/Icon/index.story.d.ts +2 -3
  60. package/dist/components/Icon/index.story.d.ts.map +1 -1
  61. package/dist/components/IconButton/index.d.ts +1 -2
  62. package/dist/components/IconButton/index.d.ts.map +1 -1
  63. package/dist/components/IconButton/index.story.d.ts +1 -2
  64. package/dist/components/IconButton/index.story.d.ts.map +1 -1
  65. package/dist/components/LoadingSpinner/index.d.ts +9 -8
  66. package/dist/components/LoadingSpinner/index.d.ts.map +1 -1
  67. package/dist/components/LoadingSpinner/index.story.d.ts +1 -2
  68. package/dist/components/LoadingSpinner/index.story.d.ts.map +1 -1
  69. package/dist/components/Modal/ModalPlumbing.d.ts.map +1 -1
  70. package/dist/components/Modal/index.d.ts +18 -27
  71. package/dist/components/Modal/index.d.ts.map +1 -1
  72. package/dist/components/Modal/index.story.d.ts +12 -2
  73. package/dist/components/Modal/index.story.d.ts.map +1 -1
  74. package/dist/components/MultiSelect/context.d.ts +1 -1
  75. package/dist/components/MultiSelect/context.d.ts.map +1 -1
  76. package/dist/components/MultiSelect/index.d.ts +18 -6
  77. package/dist/components/MultiSelect/index.d.ts.map +1 -1
  78. package/dist/components/MultiSelect/index.story.d.ts +21 -16
  79. package/dist/components/MultiSelect/index.story.d.ts.map +1 -1
  80. package/dist/components/Radio/index.d.ts +13 -6
  81. package/dist/components/Radio/index.d.ts.map +1 -1
  82. package/dist/components/Radio/index.story.d.ts +11 -8
  83. package/dist/components/Radio/index.story.d.ts.map +1 -1
  84. package/dist/components/SegmentedControl/RadioGroupContext.d.ts +1 -1
  85. package/dist/components/SegmentedControl/RadioGroupContext.d.ts.map +1 -1
  86. package/dist/components/SegmentedControl/index.d.ts +2 -1
  87. package/dist/components/SegmentedControl/index.d.ts.map +1 -1
  88. package/dist/components/SegmentedControl/index.story.d.ts +1 -2
  89. package/dist/components/SegmentedControl/index.story.d.ts.map +1 -1
  90. package/dist/components/Switch/index.d.ts +3 -2
  91. package/dist/components/Switch/index.d.ts.map +1 -1
  92. package/dist/components/Switch/index.story.d.ts +1 -2
  93. package/dist/components/Switch/index.story.d.ts.map +1 -1
  94. package/dist/components/TagItem/index.d.ts +3 -3
  95. package/dist/components/TagItem/index.d.ts.map +1 -1
  96. package/dist/components/TagItem/index.story.d.ts +2 -3
  97. package/dist/components/TagItem/index.story.d.ts.map +1 -1
  98. package/dist/components/TextArea/TextArea.story.d.ts +28 -0
  99. package/dist/components/TextArea/TextArea.story.d.ts.map +1 -0
  100. package/dist/components/TextArea/index.d.ts +21 -0
  101. package/dist/components/TextArea/index.d.ts.map +1 -0
  102. package/dist/components/TextField/TextField.story.d.ts +28 -0
  103. package/dist/components/TextField/TextField.story.d.ts.map +1 -0
  104. package/dist/components/TextField/index.d.ts +8 -30
  105. package/dist/components/TextField/index.d.ts.map +1 -1
  106. package/dist/core/CharcoalProvider.d.ts +1 -1
  107. package/dist/core/CharcoalProvider.d.ts.map +1 -1
  108. package/dist/core/ComponentAbstraction.d.ts +1 -1
  109. package/dist/core/ComponentAbstraction.d.ts.map +1 -1
  110. package/dist/index.cjs.js +1064 -771
  111. package/dist/index.cjs.js.map +1 -1
  112. package/dist/index.d.ts +5 -3
  113. package/dist/index.d.ts.map +1 -1
  114. package/dist/index.esm.js +1028 -750
  115. package/dist/index.esm.js.map +1 -1
  116. package/dist/styled.d.ts +13 -13
  117. package/package.json +7 -7
  118. package/src/_lib/compat.ts +20 -1
  119. package/src/_lib/index.ts +23 -0
  120. package/src/components/Button/__snapshots__/index.test.tsx.snap +385 -0
  121. package/src/components/Button/index.story.tsx +1 -1
  122. package/src/components/Button/index.test.tsx +24 -0
  123. package/src/components/Button/index.tsx +2 -2
  124. package/src/components/Checkbox/index.story.tsx +1 -1
  125. package/src/components/Checkbox/index.tsx +4 -2
  126. package/src/components/Clickable/index.story.tsx +0 -1
  127. package/src/components/Clickable/index.tsx +1 -1
  128. package/src/components/DropdownSelector/Divider.tsx +3 -0
  129. package/src/components/DropdownSelector/DropdownMenuItem.tsx +40 -0
  130. package/src/components/DropdownSelector/DropdownPopover.tsx +21 -42
  131. package/src/components/DropdownSelector/ListItem/index.story.tsx +51 -0
  132. package/src/components/DropdownSelector/ListItem/index.tsx +58 -0
  133. package/src/components/DropdownSelector/MenuItem/index.tsx +31 -0
  134. package/src/components/DropdownSelector/MenuItem/internals/handleFocusByKeyBoard.tsx +43 -0
  135. package/src/components/DropdownSelector/MenuItem/internals/useMenuItemHandleKeyDown.tsx +55 -0
  136. package/src/components/DropdownSelector/MenuItemGroup/index.tsx +42 -0
  137. package/src/components/DropdownSelector/MenuList/MenuListContext.ts +17 -0
  138. package/src/components/DropdownSelector/MenuList/index.story.tsx +51 -0
  139. package/src/components/DropdownSelector/MenuList/index.tsx +51 -0
  140. package/src/components/DropdownSelector/MenuList/internals/getValuesRecursive.tsx +35 -0
  141. package/src/components/DropdownSelector/Popover/index.story.tsx +65 -0
  142. package/src/components/DropdownSelector/Popover/index.tsx +69 -0
  143. package/src/components/DropdownSelector/index.story.tsx +56 -21
  144. package/src/components/DropdownSelector/index.tsx +19 -60
  145. package/src/components/DropdownSelector/utils/findPreviewRecursive.tsx +39 -0
  146. package/src/components/FieldLabel/index.tsx +1 -1
  147. package/src/components/Icon/index.story.tsx +0 -1
  148. package/src/components/Icon/index.tsx +1 -1
  149. package/src/components/IconButton/index.story.tsx +0 -1
  150. package/src/components/IconButton/index.tsx +2 -2
  151. package/src/components/LoadingSpinner/index.story.tsx +8 -2
  152. package/src/components/LoadingSpinner/index.tsx +44 -29
  153. package/src/components/Modal/ModalPlumbing.tsx +0 -1
  154. package/src/components/Modal/index.story.tsx +0 -1
  155. package/src/components/Modal/index.tsx +19 -12
  156. package/src/components/MultiSelect/context.ts +2 -2
  157. package/src/components/MultiSelect/index.story.tsx +26 -27
  158. package/src/components/MultiSelect/index.test.tsx +5 -23
  159. package/src/components/MultiSelect/index.tsx +83 -78
  160. package/src/components/Radio/index.story.tsx +7 -9
  161. package/src/components/Radio/index.test.tsx +3 -4
  162. package/src/components/Radio/index.tsx +24 -23
  163. package/src/components/SegmentedControl/RadioGroupContext.tsx +2 -1
  164. package/src/components/SegmentedControl/index.story.tsx +0 -1
  165. package/src/components/SegmentedControl/index.tsx +16 -5
  166. package/src/components/Switch/index.story.tsx +1 -1
  167. package/src/components/Switch/index.tsx +38 -32
  168. package/src/components/TagItem/index.story.tsx +0 -1
  169. package/src/components/TagItem/index.tsx +1 -6
  170. package/src/components/TextArea/TextArea.story.tsx +61 -0
  171. package/src/components/TextArea/index.tsx +246 -0
  172. package/src/components/TextField/{index.story.tsx → TextField.story.tsx} +6 -29
  173. package/src/components/TextField/index.tsx +148 -378
  174. package/src/components/a11y.test.tsx +0 -1
  175. package/src/core/CharcoalProvider.tsx +1 -1
  176. package/src/core/ComponentAbstraction.tsx +2 -1
  177. package/src/index.ts +8 -6
  178. package/dist/components/DropdownSelector/OptionItem.d.ts +0 -7
  179. package/dist/components/DropdownSelector/OptionItem.d.ts.map +0 -1
  180. package/dist/components/DropdownSelector/utils/focusIfHTMLLIElement.d.ts +0 -6
  181. package/dist/components/DropdownSelector/utils/focusIfHTMLLIElement.d.ts.map +0 -1
  182. package/dist/components/DropdownSelector/utils/handleFocusByKeyBoard.d.ts +0 -6
  183. package/dist/components/DropdownSelector/utils/handleFocusByKeyBoard.d.ts.map +0 -1
  184. package/src/components/DropdownSelector/OptionItem.tsx +0 -85
  185. package/src/components/DropdownSelector/utils/focusIfHTMLLIElement.tsx +0 -12
  186. package/src/components/DropdownSelector/utils/handleFocusByKeyBoard.tsx +0 -20
@@ -0,0 +1,40 @@
1
+ import styled from 'styled-components'
2
+ import MenuItem, { MenuItemProps } from './MenuItem'
3
+ import { MenuListContext } from './MenuList/MenuListContext'
4
+ import { useContext } from 'react'
5
+ import { theme } from '../../styled'
6
+ import Icon from '../Icon'
7
+
8
+ export type DropdownMenuItemProps = Omit<MenuItemProps<'div'>, 'as'>
9
+
10
+ /**
11
+ * DropdownSelectorの選択肢として使うMenuItem
12
+ */
13
+ export default function DropdownMenuItem(props: DropdownMenuItemProps) {
14
+ const { value: ctxValue } = useContext(MenuListContext)
15
+ const isSelected = props.value === ctxValue
16
+ const { children, ...rest } = props
17
+
18
+ return (
19
+ <MenuItem {...rest}>
20
+ {isSelected && <Text2ColorIcon name="16/Check" />}
21
+ <StyledSpan isSelected={isSelected}>{props.children}</StyledSpan>
22
+ </MenuItem>
23
+ )
24
+ }
25
+
26
+ /**
27
+ * アイコンがない時を考慮して20px(16pxのwidthと4pxのgap)の余白をとる
28
+ */
29
+ const StyledSpan = styled.span<{ isSelected?: boolean }>`
30
+ ${theme((o) => [o.typography(14), o.font.text2])};
31
+ display: flex;
32
+ align-items: center;
33
+ width: 100%;
34
+ margin-left: ${({ isSelected }) => (isSelected === true ? 0 : 20)}px;
35
+ `
36
+
37
+ const Text2ColorIcon = styled(Icon)`
38
+ ${theme((o) => [o.font.text2])}
39
+ padding-right: 4px;
40
+ `
@@ -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 { 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 { 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,31 @@
1
+ import ListItem, { CustomJSXElement, ListItemProps } from '../ListItem'
2
+ import { useMenuItemHandleKeyDown } from './internals/useMenuItemHandleKeyDown'
3
+
4
+ export type MenuItemProps<T extends CustomJSXElement = never> = {
5
+ value?: string
6
+ disabled?: boolean
7
+ } & ListItemProps<T>
8
+
9
+ /**
10
+ * 上下キーでフォーカス移動でき、エンターキーで選択できるリストの項目
11
+ * 基本的に`<MenuList>`, `<MenuGroup>`と合わせて使用する
12
+ */
13
+ export default function MenuItem<T extends CustomJSXElement>(
14
+ props: MenuItemProps<T>
15
+ ) {
16
+ const { children, as, ...rest } = props
17
+ const [handleKeyDown, setContextValue] = useMenuItemHandleKeyDown(props.value)
18
+ return (
19
+ <ListItem
20
+ {...rest}
21
+ as={as as CustomJSXElement}
22
+ data-key={props.value}
23
+ onKeyDown={handleKeyDown}
24
+ onClick={props.disabled === true ? undefined : setContextValue}
25
+ tabIndex={-1}
26
+ aria-disabled={props.disabled}
27
+ >
28
+ {props.children}
29
+ </ListItem>
30
+ )
31
+ }
@@ -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 { 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,42 @@
1
+ import styled from 'styled-components'
2
+ import MenuItem from '../MenuItem'
3
+ import { Divider } from '../Divider'
4
+
5
+ type MenuItemGroupChild = React.ReactElement<typeof MenuItem | typeof Divider>
6
+
7
+ export type MenuItemGroupProps = {
8
+ text: string
9
+ children: MenuItemGroupChild | MenuItemGroupChild[]
10
+ }
11
+
12
+ /**
13
+ * 項目のリストを分類する見出しをつけるコンテナ要素
14
+ */
15
+ export default function MenuItemGroup(props: MenuItemGroupProps) {
16
+ return (
17
+ <StyledLi role="presentation">
18
+ <TextSpan>{props.text}</TextSpan>
19
+ <StyledUl role="group">{props.children}</StyledUl>
20
+ </StyledLi>
21
+ )
22
+ }
23
+
24
+ const TextSpan = styled.span`
25
+ display: block;
26
+ color: ${({ theme }) => theme.color.text3};
27
+ font-size: 12px;
28
+ font-weight: bold;
29
+ padding: 12px 0 8px 16px;
30
+ `
31
+
32
+ const StyledUl = styled.ul`
33
+ padding-left: 0;
34
+ margin: 0;
35
+ box-sizing: border-box;
36
+ list-style: none;
37
+ overflow: hidden;
38
+ `
39
+
40
+ const StyledLi = styled.li`
41
+ display: block;
42
+ `
@@ -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,51 @@
1
+ import { action } from '@storybook/addon-actions'
2
+ import { Story } from '../../../_lib/compat'
3
+ import MenuList, { MenuListProps } from '.'
4
+ import MenuItem from '../MenuItem'
5
+ import MenuItemGroup from '../MenuItemGroup'
6
+
7
+ export default {
8
+ title: 'DropdownSelector/MenuList',
9
+ component: MenuList,
10
+ }
11
+
12
+ function makeList(n: number, offset = 0) {
13
+ return [...(Array(n) as undefined[])].map((_, i) => {
14
+ const v = i + offset
15
+ return (
16
+ <MenuItem key={v} value={v.toString()}>
17
+ Menu {v}
18
+ </MenuItem>
19
+ )
20
+ })
21
+ }
22
+
23
+ export const Basic: Story<MenuListProps> = () => {
24
+ return (
25
+ <>
26
+ <MenuList onChange={action('onChange')}>{makeList(10)}</MenuList>
27
+ </>
28
+ )
29
+ }
30
+
31
+ export const Disabled: Story<MenuListProps> = () => {
32
+ return (
33
+ <>
34
+ <MenuList onChange={action('onChange')}>
35
+ <MenuItem value="1">MenuItem</MenuItem>
36
+ <MenuItem value="2" disabled>
37
+ Disabled MenuItem
38
+ </MenuItem>
39
+ </MenuList>
40
+ </>
41
+ )
42
+ }
43
+
44
+ export const Group: Story<MenuListProps> = () => {
45
+ return (
46
+ <MenuList onChange={action('onChange')} value="1">
47
+ <MenuItemGroup text="Section1">{makeList(5)}</MenuItemGroup>
48
+ <MenuItemGroup text="Section2">{makeList(5, 5)}</MenuItemGroup>
49
+ </MenuList>
50
+ )
51
+ }
@@ -0,0 +1,51 @@
1
+ import { 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 * as 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 { 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
+ }