@primer/components 31.0.2-rc.c7dafefb → 31.2.0-rc.25d7c83f

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 (230) hide show
  1. package/.storybook/main.js +7 -9
  2. package/.storybook/preview.js +5 -1
  3. package/CHANGELOG.md +23 -1
  4. package/dist/browser.esm.js +623 -620
  5. package/dist/browser.esm.js.map +1 -1
  6. package/dist/browser.umd.js +164 -161
  7. package/dist/browser.umd.js.map +1 -1
  8. package/docs/content/ActionList2.mdx +354 -0
  9. package/docs/content/FilterList.md +2 -2
  10. package/docs/content/TextInputWithTokens.mdx +114 -0
  11. package/docs/content/getting-started.md +1 -1
  12. package/docs/content/theming.md +23 -0
  13. package/docs/src/@primer/gatsby-theme-doctocat/components/hero.js +1 -3
  14. package/docs/src/@primer/gatsby-theme-doctocat/components/live-preview-wrapper.js +1 -1
  15. package/docs/src/@primer/gatsby-theme-doctocat/live-code-scope.js +17 -0
  16. package/lib/ActionList/Header.js +1 -1
  17. package/lib/ActionList2/Description.d.ts +12 -0
  18. package/lib/ActionList2/Description.js +53 -0
  19. package/lib/ActionList2/Divider.d.ts +5 -0
  20. package/lib/ActionList2/Divider.js +35 -0
  21. package/lib/ActionList2/Group.d.ts +11 -0
  22. package/lib/ActionList2/Group.js +57 -0
  23. package/lib/ActionList2/Header.d.ts +26 -0
  24. package/lib/ActionList2/Header.js +55 -0
  25. package/lib/ActionList2/Item.d.ts +63 -0
  26. package/lib/ActionList2/Item.js +234 -0
  27. package/lib/ActionList2/LinkItem.d.ts +17 -0
  28. package/lib/ActionList2/LinkItem.js +57 -0
  29. package/lib/ActionList2/List.d.ts +26 -0
  30. package/lib/ActionList2/List.js +59 -0
  31. package/lib/ActionList2/Selection.d.ts +5 -0
  32. package/lib/ActionList2/Selection.js +70 -0
  33. package/lib/ActionList2/Visuals.d.ts +9 -0
  34. package/lib/ActionList2/Visuals.js +90 -0
  35. package/lib/ActionList2/index.d.ts +36 -0
  36. package/lib/ActionList2/index.js +47 -0
  37. package/lib/Autocomplete/Autocomplete.d.ts +4 -4
  38. package/lib/Autocomplete/AutocompleteInput.d.ts +4 -4
  39. package/lib/Button/Button.d.ts +5 -5
  40. package/lib/Button/ButtonBase.d.ts +1 -1
  41. package/lib/Button/ButtonClose.d.ts +3 -3
  42. package/lib/Button/ButtonDanger.d.ts +5 -5
  43. package/lib/Button/ButtonInvisible.d.ts +5 -5
  44. package/lib/Button/ButtonOutline.d.ts +5 -5
  45. package/lib/Button/ButtonPrimary.d.ts +5 -5
  46. package/lib/CircleBadge.d.ts +2 -2
  47. package/lib/CircleOcticon.d.ts +4 -4
  48. package/lib/Dialog.d.ts +4 -4
  49. package/lib/Dropdown.d.ts +16 -16
  50. package/lib/DropdownMenu/DropdownButton.d.ts +6 -6
  51. package/lib/FilterList.d.ts +3 -3
  52. package/lib/Flash.d.ts +1 -1
  53. package/lib/Label.d.ts +1 -1
  54. package/lib/Overlay.js +3 -1
  55. package/lib/Portal/Portal.js +3 -2
  56. package/lib/Position.d.ts +4 -4
  57. package/lib/ProgressBar.d.ts +1 -1
  58. package/lib/SelectMenu/SelectMenu.d.ts +24 -24
  59. package/lib/SelectMenu/SelectMenuItem.d.ts +1 -1
  60. package/lib/TextInputWithTokens.d.ts +8 -4
  61. package/lib/TextInputWithTokens.js +61 -8
  62. package/lib/Timeline.d.ts +4 -4
  63. package/lib/Token/AvatarToken.d.ts +1 -1
  64. package/lib/Token/IssueLabelToken.d.ts +1 -1
  65. package/lib/Token/Token.d.ts +1 -1
  66. package/lib/_TextInputWrapper.d.ts +1 -1
  67. package/lib/_TextInputWrapper.js +2 -2
  68. package/lib/__tests__/ActionList2.test.d.ts +1 -0
  69. package/lib/__tests__/ActionList2.test.js +53 -0
  70. package/lib/__tests__/AnchoredOverlay.test.js +4 -2
  71. package/lib/__tests__/KeyPaths.types.test.d.ts +11 -0
  72. package/lib/__tests__/KeyPaths.types.test.js +10 -0
  73. package/lib/__tests__/TextInputWithTokens.test.js +138 -7
  74. package/lib/__tests__/utils/createSlots.test.d.ts +1 -0
  75. package/lib/__tests__/utils/createSlots.test.js +75 -0
  76. package/lib/hooks/useAnchoredPosition.js +3 -2
  77. package/lib/hooks/useCombinedRefs.d.ts +2 -2
  78. package/lib/hooks/useCombinedRefs.js +4 -6
  79. package/lib/hooks/useResizeObserver.js +2 -2
  80. package/lib/stories/ActionList2.stories.js +907 -0
  81. package/lib/stories/TextInput.stories.js +144 -0
  82. package/lib/stories/TextInputWithTokens.stories.js +18 -1
  83. package/lib/stories/Token.stories.js +19 -2
  84. package/lib/sx.d.ts +10 -2
  85. package/lib/sx.js +8 -0
  86. package/lib/theme-preval.js +81 -2
  87. package/lib/theme.d.ts +78 -0
  88. package/lib/theme.js +3 -1
  89. package/lib/unreleased.d.ts +7 -0
  90. package/lib/unreleased.js +18 -0
  91. package/lib/utils/create-slots.d.ts +17 -0
  92. package/lib/utils/create-slots.js +105 -0
  93. package/lib/utils/testing.d.ts +14 -1
  94. package/lib/utils/types/KeyPaths.d.ts +3 -0
  95. package/lib/utils/types/KeyPaths.js +1 -0
  96. package/lib/utils/use-force-update.d.ts +1 -0
  97. package/lib/utils/use-force-update.js +19 -0
  98. package/lib/utils/useIsomorphicLayoutEffect.d.ts +3 -0
  99. package/lib/utils/useIsomorphicLayoutEffect.js +12 -0
  100. package/lib-esm/ActionList/Header.js +1 -1
  101. package/lib-esm/ActionList2/Description.d.ts +12 -0
  102. package/lib-esm/ActionList2/Description.js +37 -0
  103. package/lib-esm/ActionList2/Divider.d.ts +5 -0
  104. package/lib-esm/ActionList2/Divider.js +23 -0
  105. package/lib-esm/ActionList2/Group.d.ts +11 -0
  106. package/lib-esm/ActionList2/Group.js +40 -0
  107. package/lib-esm/ActionList2/Header.d.ts +26 -0
  108. package/lib-esm/ActionList2/Header.js +44 -0
  109. package/lib-esm/ActionList2/Item.d.ts +63 -0
  110. package/lib-esm/ActionList2/Item.js +201 -0
  111. package/lib-esm/ActionList2/LinkItem.d.ts +17 -0
  112. package/lib-esm/ActionList2/LinkItem.js +43 -0
  113. package/lib-esm/ActionList2/List.d.ts +26 -0
  114. package/lib-esm/ActionList2/List.js +37 -0
  115. package/lib-esm/ActionList2/Selection.d.ts +5 -0
  116. package/lib-esm/ActionList2/Selection.js +52 -0
  117. package/lib-esm/ActionList2/Visuals.d.ts +9 -0
  118. package/lib-esm/ActionList2/Visuals.js +68 -0
  119. package/lib-esm/ActionList2/index.d.ts +36 -0
  120. package/lib-esm/ActionList2/index.js +33 -0
  121. package/lib-esm/Autocomplete/Autocomplete.d.ts +4 -4
  122. package/lib-esm/Autocomplete/AutocompleteInput.d.ts +4 -4
  123. package/lib-esm/Button/Button.d.ts +5 -5
  124. package/lib-esm/Button/ButtonBase.d.ts +1 -1
  125. package/lib-esm/Button/ButtonClose.d.ts +3 -3
  126. package/lib-esm/Button/ButtonDanger.d.ts +5 -5
  127. package/lib-esm/Button/ButtonInvisible.d.ts +5 -5
  128. package/lib-esm/Button/ButtonOutline.d.ts +5 -5
  129. package/lib-esm/Button/ButtonPrimary.d.ts +5 -5
  130. package/lib-esm/CircleBadge.d.ts +2 -2
  131. package/lib-esm/CircleOcticon.d.ts +4 -4
  132. package/lib-esm/Dialog.d.ts +4 -4
  133. package/lib-esm/Dropdown.d.ts +16 -16
  134. package/lib-esm/DropdownMenu/DropdownButton.d.ts +6 -6
  135. package/lib-esm/FilterList.d.ts +3 -3
  136. package/lib-esm/Flash.d.ts +1 -1
  137. package/lib-esm/Label.d.ts +1 -1
  138. package/lib-esm/Overlay.js +2 -1
  139. package/lib-esm/Portal/Portal.js +2 -1
  140. package/lib-esm/Position.d.ts +4 -4
  141. package/lib-esm/ProgressBar.d.ts +1 -1
  142. package/lib-esm/SelectMenu/SelectMenu.d.ts +24 -24
  143. package/lib-esm/SelectMenu/SelectMenuItem.d.ts +1 -1
  144. package/lib-esm/TextInputWithTokens.d.ts +8 -4
  145. package/lib-esm/TextInputWithTokens.js +60 -8
  146. package/lib-esm/Timeline.d.ts +4 -4
  147. package/lib-esm/Token/AvatarToken.d.ts +1 -1
  148. package/lib-esm/Token/IssueLabelToken.d.ts +1 -1
  149. package/lib-esm/Token/Token.d.ts +1 -1
  150. package/lib-esm/_TextInputWrapper.d.ts +1 -1
  151. package/lib-esm/_TextInputWrapper.js +2 -2
  152. package/lib-esm/__tests__/ActionList2.test.d.ts +1 -0
  153. package/lib-esm/__tests__/ActionList2.test.js +41 -0
  154. package/lib-esm/__tests__/AnchoredOverlay.test.js +4 -2
  155. package/lib-esm/__tests__/KeyPaths.types.test.d.ts +11 -0
  156. package/lib-esm/__tests__/KeyPaths.types.test.js +3 -0
  157. package/lib-esm/__tests__/TextInputWithTokens.test.js +132 -8
  158. package/lib-esm/__tests__/utils/createSlots.test.d.ts +1 -0
  159. package/lib-esm/__tests__/utils/createSlots.test.js +67 -0
  160. package/lib-esm/hooks/useAnchoredPosition.js +2 -1
  161. package/lib-esm/hooks/useCombinedRefs.d.ts +2 -2
  162. package/lib-esm/hooks/useCombinedRefs.js +3 -2
  163. package/lib-esm/hooks/useResizeObserver.js +2 -2
  164. package/lib-esm/stories/ActionList2.stories.js +796 -0
  165. package/lib-esm/stories/TextInput.stories.js +117 -0
  166. package/lib-esm/stories/TextInputWithTokens.stories.js +14 -0
  167. package/lib-esm/stories/Token.stories.js +14 -1
  168. package/lib-esm/sx.d.ts +10 -2
  169. package/lib-esm/sx.js +3 -1
  170. package/lib-esm/theme-preval.js +81 -2
  171. package/lib-esm/theme.d.ts +78 -0
  172. package/lib-esm/theme.js +2 -1
  173. package/lib-esm/unreleased.d.ts +7 -0
  174. package/lib-esm/unreleased.js +8 -0
  175. package/lib-esm/utils/create-slots.d.ts +17 -0
  176. package/lib-esm/utils/create-slots.js +84 -0
  177. package/lib-esm/utils/testing.d.ts +14 -1
  178. package/lib-esm/utils/types/KeyPaths.d.ts +3 -0
  179. package/lib-esm/utils/types/KeyPaths.js +1 -0
  180. package/lib-esm/utils/use-force-update.d.ts +1 -0
  181. package/lib-esm/utils/use-force-update.js +6 -0
  182. package/lib-esm/utils/useIsomorphicLayoutEffect.d.ts +3 -0
  183. package/lib-esm/utils/useIsomorphicLayoutEffect.js +3 -0
  184. package/migrating.md +1 -1
  185. package/package-lock.json +38098 -45
  186. package/package.json +7 -3
  187. package/script/build +2 -0
  188. package/src/ActionList/Header.tsx +1 -1
  189. package/src/ActionList2/Description.tsx +49 -0
  190. package/src/ActionList2/Divider.tsx +24 -0
  191. package/src/ActionList2/Group.tsx +34 -0
  192. package/src/ActionList2/Header.tsx +58 -0
  193. package/src/ActionList2/Item.tsx +223 -0
  194. package/src/ActionList2/LinkItem.tsx +49 -0
  195. package/src/ActionList2/List.tsx +55 -0
  196. package/src/ActionList2/Selection.tsx +40 -0
  197. package/src/ActionList2/Visuals.tsx +76 -0
  198. package/src/ActionList2/index.ts +39 -0
  199. package/src/Overlay.tsx +2 -1
  200. package/src/Portal/Portal.tsx +2 -1
  201. package/src/TextInputWithTokens.tsx +64 -8
  202. package/src/_TextInputWrapper.tsx +8 -0
  203. package/src/__tests__/ActionList2.test.tsx +47 -0
  204. package/src/__tests__/AnchoredOverlay.test.tsx +2 -2
  205. package/src/__tests__/KeyPaths.types.test.ts +14 -0
  206. package/src/__tests__/TextInputWithTokens.test.tsx +123 -1
  207. package/src/__tests__/__snapshots__/ActionList2.test.tsx.snap +14 -0
  208. package/src/__tests__/__snapshots__/AnchoredOverlay.test.tsx.snap +35 -135
  209. package/src/__tests__/__snapshots__/Autocomplete.test.tsx.snap +7 -0
  210. package/src/__tests__/__snapshots__/TextInput.test.tsx.snap +6 -0
  211. package/src/__tests__/__snapshots__/TextInputWithTokens.test.tsx.snap +463 -0
  212. package/src/__tests__/utils/__snapshots__/createSlots.test.tsx.snap +55 -0
  213. package/src/__tests__/utils/createSlots.test.tsx +74 -0
  214. package/src/hooks/useAnchoredPosition.ts +2 -1
  215. package/src/hooks/useCombinedRefs.ts +3 -3
  216. package/src/hooks/useResizeObserver.ts +2 -2
  217. package/src/stories/ActionList2.stories.tsx +1290 -0
  218. package/src/stories/Button.stories.tsx +1 -1
  219. package/src/stories/TextInput.stories.tsx +113 -0
  220. package/src/stories/TextInputWithTokens.stories.tsx +9 -0
  221. package/src/stories/Token.stories.tsx +12 -1
  222. package/src/sx.ts +17 -2
  223. package/src/theme-preval.js +1 -0
  224. package/src/theme.ts +86 -0
  225. package/src/unreleased.ts +9 -0
  226. package/src/utils/create-slots.tsx +96 -0
  227. package/src/utils/types/KeyPaths.ts +10 -0
  228. package/src/utils/use-force-update.ts +7 -0
  229. package/src/utils/useIsomorphicLayoutEffect.ts +10 -0
  230. package/stats.html +1 -1
@@ -0,0 +1,39 @@
1
+ import {List} from './List'
2
+ import {Group} from './Group'
3
+ import {Item} from './Item'
4
+ import {LinkItem} from './LinkItem'
5
+ import {Divider} from './Divider'
6
+ import {Description} from './Description'
7
+ import {LeadingVisual, TrailingVisual} from './Visuals'
8
+
9
+ export type {ListProps as ActionListProps} from './List'
10
+ export type {GroupProps} from './Group'
11
+ export type {ItemProps} from './Item'
12
+ export type {DescriptionProps} from './Description'
13
+ export type {LeadingVisualProps, TrailingVisualProps} from './Visuals'
14
+
15
+ /**
16
+ * Collection of list-related components.
17
+ */
18
+ export const ActionList = Object.assign(List, {
19
+ /** Collects related `Items` in an `ActionList`. */
20
+ Group,
21
+
22
+ /** An actionable or selectable `Item` */
23
+ Item,
24
+
25
+ /** A `Item` that renders a full-size anchor inside ListItem */
26
+ LinkItem,
27
+
28
+ /** Visually separates `Item`s or `Group`s in an `ActionList`. */
29
+ Divider,
30
+
31
+ /** Secondary text which provides additional information about an `Item`. */
32
+ Description,
33
+
34
+ /** Icon (or similar) positioned before `Item` text. */
35
+ LeadingVisual,
36
+
37
+ /** Icon (or similar) positioned after `Item` text. */
38
+ TrailingVisual
39
+ })
package/src/Overlay.tsx CHANGED
@@ -1,7 +1,8 @@
1
1
  import styled from 'styled-components'
2
- import React, {ReactElement, useEffect, useLayoutEffect, useRef} from 'react'
2
+ import React, {ReactElement, useEffect, useRef} from 'react'
3
3
  import {get, COMMON, SystemPositionProps, SystemCommonProps} from './constants'
4
4
  import {ComponentProps} from './utils/types'
5
+ import useLayoutEffect from './utils/useIsomorphicLayoutEffect'
5
6
  import {useOverlay, TouchOrMouseEvent} from './hooks'
6
7
  import Portal from './Portal'
7
8
  import sx, {SxProp} from './sx'
@@ -1,5 +1,6 @@
1
1
  import React from 'react'
2
2
  import {createPortal} from 'react-dom'
3
+ import useLayoutEffect from '../utils/useIsomorphicLayoutEffect'
3
4
 
4
5
  const PRIMER_PORTAL_ROOT_ID = '__primerPortalRoot__'
5
6
  const DEFAULT_PORTAL_CONTAINER_NAME = '__default__'
@@ -69,7 +70,7 @@ export const Portal: React.FC<PortalProps> = ({children, onMount, containerName:
69
70
  hostElement.style.zIndex = '1'
70
71
  const elementRef = React.useRef(hostElement)
71
72
 
72
- React.useLayoutEffect(() => {
73
+ useLayoutEffect(() => {
73
74
  let containerName = _containerName
74
75
  if (containerName === undefined) {
75
76
  containerName = DEFAULT_PORTAL_CONTAINER_NAME
@@ -1,4 +1,4 @@
1
- import React, {FocusEventHandler, KeyboardEventHandler, RefObject, useRef, useState} from 'react'
1
+ import React, {FocusEventHandler, KeyboardEventHandler, MouseEventHandler, RefObject, useRef, useState} from 'react'
2
2
  import {omit} from '@styled-system/props'
3
3
  import {FocusKeys} from './behaviors/focusZone'
4
4
  import {useCombinedRefs} from './hooks/useCombinedRefs'
@@ -11,6 +11,7 @@ import {useProvidedRefOrCreate} from './hooks'
11
11
  import UnstyledTextInput from './_UnstyledTextInput'
12
12
  import TextInputWrapper from './_TextInputWrapper'
13
13
  import Box from './Box'
14
+ import Text from './Text'
14
15
  import {isFocusable} from './utils/iterateFocusableElements'
15
16
 
16
17
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -48,8 +49,19 @@ type TextInputWithTokensInternalProps<TokenComponentType extends AnyReactCompone
48
49
  * Whether the remove buttons should be rendered in the tokens
49
50
  */
50
51
  hideTokenRemoveButtons?: boolean
52
+ /**
53
+ * The number of tokens to display before truncating
54
+ */
55
+ visibleTokenCount?: number
51
56
  } & TextInputProps
52
57
 
58
+ const overflowCountFontSizeMap: Record<TokenSizeKeys, number> = {
59
+ small: 0,
60
+ medium: 1,
61
+ large: 1,
62
+ extralarge: 2
63
+ }
64
+
53
65
  // using forwardRef is important so that other components (ex. Autocomplete) can use the ref
54
66
  function TextInputWithTokensInnerComponent<TokenComponentType extends AnyReactComponent>(
55
67
  {
@@ -71,6 +83,7 @@ function TextInputWithTokensInnerComponent<TokenComponentType extends AnyReactCo
71
83
  minWidth: minWidthProp,
72
84
  maxWidth: maxWidthProp,
73
85
  variant: variantProp,
86
+ visibleTokenCount,
74
87
  ...rest
75
88
  }: TextInputWithTokensInternalProps<TokenComponentType> & {
76
89
  selectedTokenIndex: number | undefined
@@ -78,11 +91,12 @@ function TextInputWithTokensInnerComponent<TokenComponentType extends AnyReactCo
78
91
  },
79
92
  externalRef: React.ForwardedRef<HTMLInputElement>
80
93
  ) {
81
- const {onFocus, onKeyDown, ...inputPropsRest} = omit(rest)
94
+ const {onBlur, onFocus, onKeyDown, ...inputPropsRest} = omit(rest)
82
95
  const ref = useProvidedRefOrCreate<HTMLInputElement>(externalRef as React.RefObject<HTMLInputElement>)
83
96
  const localInputRef = useRef<HTMLInputElement>(null)
84
97
  const combinedInputRef = useCombinedRefs(localInputRef, ref)
85
98
  const [selectedTokenIndex, setSelectedTokenIndex] = useState<number | undefined>()
99
+ const [tokensAreTruncated, setTokensAreTruncated] = useState<boolean>(Boolean(visibleTokenCount))
86
100
  const {containerRef} = useFocusZone(
87
101
  {
88
102
  focusOutBehavior: 'wrap',
@@ -144,18 +158,42 @@ function TextInputWithTokensInnerComponent<TokenComponentType extends AnyReactCo
144
158
 
145
159
  const handleTokenBlur: FocusEventHandler = () => {
146
160
  setSelectedTokenIndex(undefined)
161
+
162
+ // HACK: wait a tick and check the focused element before hiding truncated tokens
163
+ // this prevents the tokens from hiding when the user is moving focus between tokens,
164
+ // but still hides the tokens when the user blurs the token by tabbing out or clicking somewhere else on the page
165
+ setTimeout(() => {
166
+ if (!containerRef.current?.contains(document.activeElement) && visibleTokenCount) {
167
+ setTokensAreTruncated(true)
168
+ }
169
+ }, 0)
147
170
  }
148
171
 
149
- const handleTokenKeyUp: KeyboardEventHandler = e => {
150
- if (e.key === 'Escape') {
172
+ const handleTokenKeyUp: KeyboardEventHandler = event => {
173
+ if (event.key === 'Escape') {
151
174
  ref.current?.focus()
152
175
  }
153
176
  }
154
177
 
155
- const handleInputFocus: FocusEventHandler = e => {
156
- onFocus && onFocus(e)
178
+ const handleInputFocus: FocusEventHandler = event => {
179
+ onFocus && onFocus(event)
157
180
  setSelectedTokenIndex(undefined)
181
+ visibleTokenCount && setTokensAreTruncated(false)
182
+ }
183
+
184
+ const handleInputBlur: FocusEventHandler = event => {
185
+ onBlur && onBlur(event)
186
+
187
+ // HACK: wait a tick and check the focused element before hiding truncated tokens
188
+ // this prevents the tokens from hiding when the user is moving focus from the input to a token,
189
+ // but still hides the tokens when the user blurs the input by tabbing out or clicking somewhere else on the page
190
+ setTimeout(() => {
191
+ if (!containerRef.current?.contains(document.activeElement) && visibleTokenCount) {
192
+ setTokensAreTruncated(true)
193
+ }
194
+ }, 0)
158
195
  }
196
+
159
197
  const handleInputKeyDown: KeyboardEventHandler = e => {
160
198
  if (onKeyDown) {
161
199
  onKeyDown(e)
@@ -187,6 +225,16 @@ function TextInputWithTokensInnerComponent<TokenComponentType extends AnyReactCo
187
225
  }
188
226
  }
189
227
 
228
+ const focusInput: MouseEventHandler = () => {
229
+ combinedInputRef.current?.focus()
230
+ }
231
+
232
+ const preventTokenClickPropagation: MouseEventHandler = event => {
233
+ event.stopPropagation()
234
+ }
235
+
236
+ const visibleTokens = tokensAreTruncated ? tokens.slice(0, visibleTokenCount) : tokens
237
+
190
238
  return (
191
239
  <TextInputWrapper
192
240
  block={block}
@@ -199,6 +247,7 @@ function TextInputWithTokensInnerComponent<TokenComponentType extends AnyReactCo
199
247
  minWidth={minWidthProp}
200
248
  maxWidth={maxWidthProp}
201
249
  variant={variantProp}
250
+ onClick={focusInput}
202
251
  sx={{
203
252
  ...(block
204
253
  ? {
@@ -251,19 +300,21 @@ function TextInputWithTokensInnerComponent<TokenComponentType extends AnyReactCo
251
300
  ref={combinedInputRef}
252
301
  disabled={disabled}
253
302
  onFocus={handleInputFocus}
303
+ onBlur={handleInputBlur}
254
304
  onKeyDown={handleInputKeyDown}
255
305
  type="text"
256
306
  sx={{height: '100%'}}
257
307
  {...inputPropsRest}
258
308
  />
259
309
  </Box>
260
- {tokens.length && TokenComponent
261
- ? tokens.map(({id, ...tokenRest}, i) => (
310
+ {TokenComponent
311
+ ? visibleTokens.map(({id, ...tokenRest}, i) => (
262
312
  <TokenComponent
263
313
  key={id}
264
314
  onFocus={handleTokenFocus(i)}
265
315
  onBlur={handleTokenBlur}
266
316
  onKeyUp={handleTokenKeyUp}
317
+ onClick={preventTokenClickPropagation}
267
318
  isSelected={selectedTokenIndex === i}
268
319
  onRemove={() => {
269
320
  handleTokenRemove(id)
@@ -275,6 +326,11 @@ function TextInputWithTokensInnerComponent<TokenComponentType extends AnyReactCo
275
326
  />
276
327
  ))
277
328
  : null}
329
+ {tokensAreTruncated ? (
330
+ <Text color="fg.muted" fontSize={size && overflowCountFontSizeMap[size]}>
331
+ +{tokens.length - visibleTokens.length}
332
+ </Text>
333
+ ) : null}
278
334
  </Box>
279
335
  </TextInputWrapper>
280
336
  )
@@ -45,6 +45,7 @@ const TextInputWrapper = styled.span<StyledWrapperProps>`
45
45
  border-radius: ${get('radii.2')};
46
46
  outline: none;
47
47
  box-shadow: ${get('shadows.primer.shadow.inset')};
48
+ cursor: text;
48
49
 
49
50
  ${props => {
50
51
  if (props.hasIcon) {
@@ -90,6 +91,13 @@ const TextInputWrapper = styled.span<StyledWrapperProps>`
90
91
  display: block;
91
92
  width: 100%;
92
93
  `}
94
+
95
+ ${props =>
96
+ props.block &&
97
+ props.hasIcon &&
98
+ css`
99
+ display: flex;
100
+ `}
93
101
 
94
102
  // Ensures inputs don't zoom on mobile but are body-font size on desktop
95
103
  @media (min-width: ${get('breakpoints.1')}) {
@@ -0,0 +1,47 @@
1
+ import {cleanup, render as HTMLRender} from '@testing-library/react'
2
+ import 'babel-polyfill'
3
+ import {axe, toHaveNoViolations} from 'jest-axe'
4
+ import React from 'react'
5
+ import theme from '../theme'
6
+ import {ActionList} from '../ActionList2'
7
+ import {behavesAsComponent, checkExports} from '../utils/testing'
8
+ import {BaseStyles, ThemeProvider, SSRProvider} from '..'
9
+ expect.extend(toHaveNoViolations)
10
+
11
+ function SimpleActionList(): JSX.Element {
12
+ return (
13
+ <ThemeProvider theme={theme}>
14
+ <SSRProvider>
15
+ <BaseStyles>
16
+ <ActionList>
17
+ <ActionList.Item>New file</ActionList.Item>
18
+ <ActionList.Divider />
19
+ <ActionList.Item>Copy link</ActionList.Item>
20
+ <ActionList.Item>Edit file</ActionList.Item>
21
+ <ActionList.Item variant="danger">Delete file</ActionList.Item>
22
+ </ActionList>
23
+ </BaseStyles>
24
+ </SSRProvider>
25
+ </ThemeProvider>
26
+ )
27
+ }
28
+
29
+ describe('ActionList', () => {
30
+ behavesAsComponent({
31
+ Component: ActionList,
32
+ options: {skipAs: true, skipSx: true},
33
+ toRender: () => <ActionList />
34
+ })
35
+
36
+ checkExports('ActionList2', {
37
+ default: undefined,
38
+ ActionList
39
+ })
40
+
41
+ it('should have no axe violations', async () => {
42
+ const {container} = HTMLRender(<SimpleActionList />)
43
+ const results = await axe(container)
44
+ expect(results).toHaveNoViolations()
45
+ cleanup()
46
+ })
47
+ })
@@ -144,7 +144,7 @@ describe('AnchoredOverlay', () => {
144
144
  })
145
145
 
146
146
  it('should render consistently when open', () => {
147
- const anchoredOverlay = HTMLRender(<AnchoredOverlayTestComponent initiallyOpen={true} />)
148
- expect(anchoredOverlay).toMatchSnapshot()
147
+ const {container} = HTMLRender(<AnchoredOverlayTestComponent initiallyOpen={true} />)
148
+ expect(container).toMatchSnapshot()
149
149
  })
150
150
  })
@@ -0,0 +1,14 @@
1
+ import {Union} from 'ts-toolbelt'
2
+ import {KeyPaths} from '../utils/types/KeyPaths'
3
+
4
+ type NestedObject = {
5
+ a: string
6
+ b: {
7
+ b1: string
8
+ b2: string
9
+ }
10
+ }
11
+
12
+ export function generatesKeyPathsFromObject(x: Union.Diff<KeyPaths<NestedObject>, 'a' | 'b.b1' | 'b.b2'>): never {
13
+ return x
14
+ }
@@ -1,6 +1,6 @@
1
1
  import React from 'react'
2
2
  import {render} from '../utils/testing'
3
- import {render as HTMLRender, cleanup, fireEvent} from '@testing-library/react'
3
+ import {render as HTMLRender, fireEvent, act, cleanup} from '@testing-library/react'
4
4
  import {axe, toHaveNoViolations} from 'jest-axe'
5
5
  import 'babel-polyfill'
6
6
  import {TokenSizeKeys, tokenSizes} from '../Token/TokenBase'
@@ -29,6 +29,8 @@ const LabelledTextInputWithTokens: React.FC<TextInputWithTokensProps> = ({onToke
29
29
  </>
30
30
  )
31
31
 
32
+ jest.useFakeTimers()
33
+
32
34
  describe('TextInputWithTokens', () => {
33
35
  it('renders without tokens', () => {
34
36
  const onRemoveMock = jest.fn()
@@ -85,6 +87,13 @@ describe('TextInputWithTokens', () => {
85
87
  ).toMatchSnapshot()
86
88
  })
87
89
 
90
+ it('renders a truncated set of tokens', () => {
91
+ const onRemoveMock = jest.fn()
92
+ expect(
93
+ render(<TextInputWithTokens tokens={mockTokens} onTokenRemove={onRemoveMock} visibleTokenCount={2} />)
94
+ ).toMatchSnapshot()
95
+ })
96
+
88
97
  it('focuses the previous token when keying ArrowLeft', () => {
89
98
  const onRemoveMock = jest.fn()
90
99
  const {getByLabelText, getByText} = HTMLRender(
@@ -165,6 +174,119 @@ describe('TextInputWithTokens', () => {
165
174
  expect(document.activeElement?.id).toEqual(inputNode.id)
166
175
  })
167
176
 
177
+ it('does not focus the input when clicking a token', () => {
178
+ const onRemoveMock = jest.fn()
179
+ const {getByLabelText, getByText} = HTMLRender(
180
+ <LabelledTextInputWithTokens tokens={mockTokens} onTokenRemove={onRemoveMock} visibleTokenCount={2} />
181
+ )
182
+ const inputNode = getByLabelText('Tokens')
183
+ const tokenNode = getByText(mockTokens[0].text)
184
+
185
+ expect(document.activeElement).not.toEqual(inputNode.id)
186
+ fireEvent.click(tokenNode)
187
+ expect(document.activeElement?.id).not.toEqual(inputNode.id)
188
+ })
189
+
190
+ it('focuses the input when clicking somewhere in the component besides the tokens', () => {
191
+ const onRemoveMock = jest.fn()
192
+ const {getByLabelText, getByText} = HTMLRender(
193
+ <LabelledTextInputWithTokens tokens={mockTokens} onTokenRemove={onRemoveMock} visibleTokenCount={2} />
194
+ )
195
+ const inputNode = getByLabelText('Tokens')
196
+ const truncatedTokenCount = getByText('+6')
197
+
198
+ expect(document.activeElement).not.toEqual(inputNode.id)
199
+ fireEvent.click(truncatedTokenCount)
200
+ expect(document.activeElement?.id).toEqual(inputNode.id)
201
+ })
202
+
203
+ it('shows all tokens when the input is focused and hides them when it is blurred (when visibleTokenCount is set)', () => {
204
+ const onRemoveMock = jest.fn()
205
+ const visibleTokenCount = 2
206
+ const {getByLabelText, getByText} = HTMLRender(
207
+ <>
208
+ <LabelledTextInputWithTokens
209
+ tokens={mockTokens}
210
+ onTokenRemove={onRemoveMock}
211
+ visibleTokenCount={visibleTokenCount}
212
+ />
213
+ <button id="focusableOutsideComponent">Focus me</button>
214
+ </>
215
+ )
216
+ const inputNode = getByLabelText('Tokens')
217
+ const focusableOutsideComponentNode = getByText('Focus me')
218
+ const allTokenLabels = mockTokens.map(token => token.text)
219
+ const truncatedTokenCountNode = getByText('+6')
220
+
221
+ act(() => {
222
+ jest.runAllTimers()
223
+ fireEvent.focus(inputNode)
224
+ })
225
+
226
+ setTimeout(() => {
227
+ for (const tokenLabel of allTokenLabels) {
228
+ const tokenNode = getByText(tokenLabel)
229
+ expect(tokenNode).toBeDefined()
230
+ }
231
+ }, 0)
232
+
233
+ act(() => {
234
+ jest.runAllTimers()
235
+ // onBlur isn't called on input unless we specifically fire the "blur" event
236
+ // eslint-disable-next-line github/no-blur
237
+ fireEvent.blur(inputNode)
238
+ fireEvent.focus(focusableOutsideComponentNode)
239
+ })
240
+
241
+ setTimeout(() => {
242
+ expect(truncatedTokenCountNode).toBeDefined()
243
+
244
+ for (const tokenLabel of allTokenLabels) {
245
+ const tokenNode = getByText(tokenLabel)
246
+ if (allTokenLabels.indexOf(tokenLabel) > visibleTokenCount) {
247
+ // eslint-disable-next-line jest/no-conditional-expect
248
+ expect(tokenNode).toBeDefined()
249
+ } else {
250
+ // eslint-disable-next-line jest/no-conditional-expect
251
+ expect(tokenNode).not.toBeDefined()
252
+ }
253
+ }
254
+ }, 0)
255
+
256
+ jest.useRealTimers()
257
+ })
258
+
259
+ it('does not hide tokens when blurring the input to focus within the component (when visibleTokenCount is set)', () => {
260
+ const onRemoveMock = jest.fn()
261
+ const visibleTokenCount = 2
262
+ const {getByLabelText, getByText} = HTMLRender(
263
+ <>
264
+ <LabelledTextInputWithTokens
265
+ tokens={mockTokens}
266
+ onTokenRemove={onRemoveMock}
267
+ visibleTokenCount={visibleTokenCount}
268
+ />
269
+ <button id="focusableOutsideComponent">Focus me</button>
270
+ </>
271
+ )
272
+ const inputNode = getByLabelText('Tokens')
273
+ const firstTokenNode = getByText(mockTokens[visibleTokenCount - 1].text)
274
+ const allTokenLabels = mockTokens.map(token => token.text)
275
+ const truncatedTokenCountNode = getByText('+6')
276
+
277
+ act(() => {
278
+ fireEvent.focus(inputNode)
279
+ fireEvent.focus(firstTokenNode)
280
+ })
281
+
282
+ expect(truncatedTokenCountNode).toBeDefined()
283
+
284
+ for (const tokenLabel of allTokenLabels) {
285
+ const tokenNode = getByText(tokenLabel)
286
+ expect(tokenNode).toBeDefined()
287
+ }
288
+ })
289
+
168
290
  it('focuses the first token when keying ArrowRight in the input', () => {
169
291
  const onRemoveMock = jest.fn()
170
292
  const {getByLabelText, getByText} = HTMLRender(
@@ -0,0 +1,14 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`ActionList renders consistently 1`] = `
4
+ .c0 {
5
+ margin: 0;
6
+ padding-inline-start: 0;
7
+ padding-top: 8px;
8
+ padding-bottom: 8px;
9
+ }
10
+
11
+ <ul
12
+ className="c0"
13
+ />
14
+ `;
@@ -94,9 +94,7 @@ exports[`AnchoredOverlay renders consistently 1`] = `
94
94
  `;
95
95
 
96
96
  exports[`AnchoredOverlay should render consistently when open 1`] = `
97
- Object {
98
- "asFragment": [Function],
99
- "baseElement": .c0 {
97
+ .c0 {
100
98
  font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";
101
99
  line-height: 1.5;
102
100
  color: #24292f;
@@ -187,146 +185,48 @@ Object {
187
185
  outline: none;
188
186
  }
189
187
 
190
- <body>
191
- <div>
192
- <div
193
- class="c0"
194
- color="fg.default"
195
- data-portal-root="true"
196
- font-family="normal"
197
- >
198
- <button
199
- aria-haspopup="true"
200
- aria-labelledby="react-aria-1"
201
- class="c1"
202
- id="react-aria-1"
203
- tabindex="0"
204
- >
205
- Anchor Button
206
- </button>
207
- <div
208
- id="__primerPortalRoot__"
209
- style="position: absolute; top: 0px; left: 0px;"
210
- >
211
- <div
212
- style="position: relative; z-index: 1;"
213
- >
214
- <div
215
- class="c2"
216
- data-focus-trap="active"
217
- height="auto"
218
- role="none"
219
- style="top: 4px; left: 0px; --styled-overlay-visibility: visible;"
220
- width="auto"
221
- >
222
- <button
223
- class="focus-visible"
224
- data-focus-visible-added=""
225
- tabindex="0"
226
- type="button"
227
- >
228
- Focusable Child
229
- </button>
230
- </div>
231
- </div>
232
- </div>
233
- </div>
234
- </div>
235
- </body>,
236
- "container": <div>
188
+ <div>
189
+ <div
190
+ class="c0"
191
+ color="fg.default"
192
+ data-portal-root="true"
193
+ font-family="normal"
194
+ >
195
+ <button
196
+ aria-haspopup="true"
197
+ aria-labelledby="react-aria-1"
198
+ class="c1"
199
+ id="react-aria-1"
200
+ tabindex="0"
201
+ >
202
+ Anchor Button
203
+ </button>
237
204
  <div
238
- class="BaseStyles__Base-qvuaww-0 ihFkAM"
239
- color="fg.default"
240
- data-portal-root="true"
241
- font-family="normal"
205
+ id="__primerPortalRoot__"
206
+ style="position: absolute; top: 0px; left: 0px;"
242
207
  >
243
- <button
244
- aria-haspopup="true"
245
- aria-labelledby="react-aria-1"
246
- class="ButtonBase-sc-181ps9o-0 Button-xjtz72-0 iRqJHc"
247
- id="react-aria-1"
248
- tabindex="0"
249
- >
250
- Anchor Button
251
- </button>
252
208
  <div
253
- id="__primerPortalRoot__"
254
- style="position: absolute; top: 0px; left: 0px;"
209
+ style="position: relative; z-index: 1;"
255
210
  >
256
211
  <div
257
- style="position: relative; z-index: 1;"
212
+ class="c2"
213
+ data-focus-trap="active"
214
+ height="auto"
215
+ role="none"
216
+ style="top: 4px; left: 0px; --styled-overlay-visibility: visible;"
217
+ width="auto"
258
218
  >
259
- <div
260
- class="Overlay__StyledOverlay-jhwkzw-0 jFTKwM"
261
- data-focus-trap="active"
262
- height="auto"
263
- role="none"
264
- style="top: 4px; left: 0px; --styled-overlay-visibility: visible;"
265
- width="auto"
219
+ <button
220
+ class="focus-visible"
221
+ data-focus-visible-added=""
222
+ tabindex="0"
223
+ type="button"
266
224
  >
267
- <button
268
- class="focus-visible"
269
- data-focus-visible-added=""
270
- tabindex="0"
271
- type="button"
272
- >
273
- Focusable Child
274
- </button>
275
- </div>
225
+ Focusable Child
226
+ </button>
276
227
  </div>
277
228
  </div>
278
229
  </div>
279
- </div>,
280
- "debug": [Function],
281
- "findAllByAltText": [Function],
282
- "findAllByDisplayValue": [Function],
283
- "findAllByLabelText": [Function],
284
- "findAllByPlaceholderText": [Function],
285
- "findAllByRole": [Function],
286
- "findAllByTestId": [Function],
287
- "findAllByText": [Function],
288
- "findAllByTitle": [Function],
289
- "findByAltText": [Function],
290
- "findByDisplayValue": [Function],
291
- "findByLabelText": [Function],
292
- "findByPlaceholderText": [Function],
293
- "findByRole": [Function],
294
- "findByTestId": [Function],
295
- "findByText": [Function],
296
- "findByTitle": [Function],
297
- "getAllByAltText": [Function],
298
- "getAllByDisplayValue": [Function],
299
- "getAllByLabelText": [Function],
300
- "getAllByPlaceholderText": [Function],
301
- "getAllByRole": [Function],
302
- "getAllByTestId": [Function],
303
- "getAllByText": [Function],
304
- "getAllByTitle": [Function],
305
- "getByAltText": [Function],
306
- "getByDisplayValue": [Function],
307
- "getByLabelText": [Function],
308
- "getByPlaceholderText": [Function],
309
- "getByRole": [Function],
310
- "getByTestId": [Function],
311
- "getByText": [Function],
312
- "getByTitle": [Function],
313
- "queryAllByAltText": [Function],
314
- "queryAllByDisplayValue": [Function],
315
- "queryAllByLabelText": [Function],
316
- "queryAllByPlaceholderText": [Function],
317
- "queryAllByRole": [Function],
318
- "queryAllByTestId": [Function],
319
- "queryAllByText": [Function],
320
- "queryAllByTitle": [Function],
321
- "queryByAltText": [Function],
322
- "queryByDisplayValue": [Function],
323
- "queryByLabelText": [Function],
324
- "queryByPlaceholderText": [Function],
325
- "queryByRole": [Function],
326
- "queryByTestId": [Function],
327
- "queryByText": [Function],
328
- "queryByTitle": [Function],
329
- "rerender": [Function],
330
- "unmount": [Function],
331
- }
230
+ </div>
231
+ </div>
332
232
  `;