@primer/components 30.3.0-rc.2010c7d4 → 30.3.0-rc.9dbc85a9

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 (97) hide show
  1. package/CHANGELOG.md +4 -2
  2. package/dist/browser.esm.js +717 -718
  3. package/dist/browser.esm.js.map +1 -1
  4. package/dist/browser.umd.js +320 -321
  5. package/dist/browser.umd.js.map +1 -1
  6. package/docs/content/Autocomplete.mdx +627 -0
  7. package/docs/content/TextInputTokens.mdx +89 -0
  8. package/docs/src/@primer/gatsby-theme-doctocat/nav.yml +2 -0
  9. package/lib/AnchoredOverlay/AnchoredOverlay.d.ts +2 -1
  10. package/lib/AnchoredOverlay/AnchoredOverlay.js +11 -3
  11. package/lib/Autocomplete/Autocomplete.d.ts +304 -0
  12. package/lib/Autocomplete/Autocomplete.js +145 -0
  13. package/lib/Autocomplete/AutocompleteContext.d.ts +17 -0
  14. package/lib/Autocomplete/AutocompleteContext.js +11 -0
  15. package/lib/Autocomplete/AutocompleteInput.d.ts +292 -0
  16. package/lib/Autocomplete/AutocompleteInput.js +157 -0
  17. package/lib/Autocomplete/AutocompleteMenu.d.ts +72 -0
  18. package/lib/Autocomplete/AutocompleteMenu.js +224 -0
  19. package/lib/Autocomplete/AutocompleteOverlay.d.ts +20 -0
  20. package/lib/Autocomplete/AutocompleteOverlay.js +80 -0
  21. package/lib/Autocomplete/index.d.ts +2 -0
  22. package/lib/Autocomplete/index.js +15 -0
  23. package/lib/FilteredActionList/FilteredActionList.js +5 -31
  24. package/lib/Overlay.d.ts +1 -0
  25. package/lib/Overlay.js +3 -1
  26. package/lib/__tests__/Autocomplete.test.d.ts +1 -0
  27. package/lib/__tests__/Autocomplete.test.js +528 -0
  28. package/lib/__tests__/behaviors/scrollIntoViewingArea.test.d.ts +1 -0
  29. package/lib/__tests__/behaviors/scrollIntoViewingArea.test.js +226 -0
  30. package/lib/behaviors/scrollIntoViewingArea.d.ts +1 -0
  31. package/lib/behaviors/scrollIntoViewingArea.js +39 -0
  32. package/lib/hooks/useOpenAndCloseFocus.d.ts +2 -1
  33. package/lib/hooks/useOpenAndCloseFocus.js +7 -2
  34. package/lib/hooks/useOverlay.d.ts +2 -1
  35. package/lib/hooks/useOverlay.js +4 -2
  36. package/lib/index.d.ts +2 -0
  37. package/lib/index.js +8 -0
  38. package/lib/stories/Autocomplete.stories.js +608 -0
  39. package/lib/utils/types/MandateProps.d.ts +3 -0
  40. package/lib/utils/types/MandateProps.js +1 -0
  41. package/lib/utils/types/index.d.ts +1 -0
  42. package/lib/utils/types/index.js +13 -0
  43. package/lib-esm/AnchoredOverlay/AnchoredOverlay.d.ts +2 -1
  44. package/lib-esm/AnchoredOverlay/AnchoredOverlay.js +11 -3
  45. package/lib-esm/Autocomplete/Autocomplete.d.ts +304 -0
  46. package/lib-esm/Autocomplete/Autocomplete.js +123 -0
  47. package/lib-esm/Autocomplete/AutocompleteContext.d.ts +17 -0
  48. package/lib-esm/Autocomplete/AutocompleteContext.js +2 -0
  49. package/lib-esm/Autocomplete/AutocompleteInput.d.ts +292 -0
  50. package/lib-esm/Autocomplete/AutocompleteInput.js +138 -0
  51. package/lib-esm/Autocomplete/AutocompleteMenu.d.ts +72 -0
  52. package/lib-esm/Autocomplete/AutocompleteMenu.js +205 -0
  53. package/lib-esm/Autocomplete/AutocompleteOverlay.d.ts +20 -0
  54. package/lib-esm/Autocomplete/AutocompleteOverlay.js +62 -0
  55. package/lib-esm/Autocomplete/index.d.ts +2 -0
  56. package/lib-esm/Autocomplete/index.js +1 -0
  57. package/lib-esm/FilteredActionList/FilteredActionList.js +3 -31
  58. package/lib-esm/Overlay.d.ts +1 -0
  59. package/lib-esm/Overlay.js +3 -1
  60. package/lib-esm/__tests__/Autocomplete.test.d.ts +1 -0
  61. package/lib-esm/__tests__/Autocomplete.test.js +494 -0
  62. package/lib-esm/__tests__/behaviors/scrollIntoViewingArea.test.d.ts +1 -0
  63. package/lib-esm/__tests__/behaviors/scrollIntoViewingArea.test.js +224 -0
  64. package/lib-esm/behaviors/scrollIntoViewingArea.d.ts +1 -0
  65. package/lib-esm/behaviors/scrollIntoViewingArea.js +30 -0
  66. package/lib-esm/hooks/useOpenAndCloseFocus.d.ts +2 -1
  67. package/lib-esm/hooks/useOpenAndCloseFocus.js +7 -2
  68. package/lib-esm/hooks/useOverlay.d.ts +2 -1
  69. package/lib-esm/hooks/useOverlay.js +4 -2
  70. package/lib-esm/index.d.ts +2 -0
  71. package/lib-esm/index.js +1 -0
  72. package/lib-esm/stories/Autocomplete.stories.js +549 -0
  73. package/lib-esm/utils/types/MandateProps.d.ts +3 -0
  74. package/lib-esm/utils/types/MandateProps.js +1 -0
  75. package/lib-esm/utils/types/index.d.ts +1 -0
  76. package/lib-esm/utils/types/index.js +2 -1
  77. package/package.json +1 -1
  78. package/src/AnchoredOverlay/AnchoredOverlay.tsx +14 -3
  79. package/src/Autocomplete/Autocomplete.tsx +103 -0
  80. package/src/Autocomplete/AutocompleteContext.tsx +19 -0
  81. package/src/Autocomplete/AutocompleteInput.tsx +179 -0
  82. package/src/Autocomplete/AutocompleteMenu.tsx +341 -0
  83. package/src/Autocomplete/AutocompleteOverlay.tsx +68 -0
  84. package/src/Autocomplete/index.ts +2 -0
  85. package/src/FilteredActionList/FilteredActionList.tsx +10 -25
  86. package/src/Overlay.tsx +4 -1
  87. package/src/__tests__/Autocomplete.test.tsx +444 -0
  88. package/src/__tests__/__snapshots__/Autocomplete.test.tsx.snap +3414 -0
  89. package/src/__tests__/behaviors/scrollIntoViewingArea.test.ts +195 -0
  90. package/src/behaviors/scrollIntoViewingArea.ts +27 -0
  91. package/src/hooks/useOpenAndCloseFocus.ts +7 -2
  92. package/src/hooks/useOverlay.tsx +4 -2
  93. package/src/index.ts +2 -0
  94. package/src/stories/Autocomplete.stories.tsx +572 -0
  95. package/src/utils/types/MandateProps.ts +19 -0
  96. package/src/utils/types/index.ts +1 -0
  97. package/stats.html +1 -1
@@ -0,0 +1,195 @@
1
+ import {scrollIntoViewingArea} from '../../behaviors/scrollIntoViewingArea'
2
+
3
+ function scrollPositionFormula(
4
+ positionData: {viewingAreaEdgePosition: number; childEdgePosition: number; margin: number},
5
+ isChildAboveViewingArea: boolean
6
+ ) {
7
+ const {viewingAreaEdgePosition, childEdgePosition, margin} = positionData
8
+ const marginOffset = margin * (isChildAboveViewingArea ? -1 : 1)
9
+
10
+ return childEdgePosition - viewingAreaEdgePosition + marginOffset
11
+ }
12
+
13
+ // The DOMRect constructor isn't available in JSDOM, so we improvise here.
14
+ function makeDOMRect(x: number, y: number, width: number, height: number): DOMRect {
15
+ return {
16
+ x,
17
+ y,
18
+ width,
19
+ height,
20
+ top: y,
21
+ left: x,
22
+ right: x + width,
23
+ bottom: y + height,
24
+ toJSON() {
25
+ return this
26
+ }
27
+ }
28
+ }
29
+
30
+ // Since Jest/JSDOM doesn't support layout, we can stub out getBoundingClientRect if we know the
31
+ // correct dimensions. JSDOM will handle the rest of the DOM API used by getAnchoredPosition.
32
+ function createVirtualDOM(viewingAreaRect: DOMRect, childRect: DOMRect) {
33
+ const viewingArea = document.createElement('div')
34
+ viewingArea.style.overflow = 'auto'
35
+ viewingArea.id = 'viewingArea'
36
+ // eslint-disable-next-line github/unescaped-html-literal
37
+ viewingArea.innerHTML = '<div id="child"></div>'
38
+ const child = viewingArea.querySelector('#child')!
39
+ child.getBoundingClientRect = () => childRect
40
+ viewingArea.getBoundingClientRect = () => viewingAreaRect
41
+ return {viewingArea, child}
42
+ }
43
+
44
+ describe('scrollIntoViewingArea', () => {
45
+ it('scrolls the expected amount when only the viewingArea element and child element are passed to the function', () => {
46
+ const scrollToMock = jest.fn()
47
+ Object.defineProperty(window.Element.prototype, 'scrollTo', {
48
+ writable: true,
49
+ value: scrollToMock
50
+ })
51
+ const childHeight = 50
52
+ const viewAreaHeight = 100
53
+ const childStart = viewAreaHeight + 10
54
+ const expectedScrollPosition = scrollPositionFormula(
55
+ {viewingAreaEdgePosition: viewAreaHeight, childEdgePosition: childStart + childHeight, margin: 8},
56
+ false
57
+ )
58
+
59
+ const viewingAreaRect = makeDOMRect(0, 0, 100, viewAreaHeight)
60
+ const childRect = makeDOMRect(0, childStart, 100, childHeight)
61
+ const {viewingArea, child} = createVirtualDOM(viewingAreaRect, childRect)
62
+
63
+ viewingArea.getBoundingClientRect = () => viewingAreaRect
64
+ viewingArea.scrollTop = 0
65
+ child.getBoundingClientRect = () => childRect
66
+
67
+ scrollIntoViewingArea(child as HTMLDivElement, viewingArea)
68
+ expect(scrollToMock).toHaveBeenCalledWith({
69
+ behavior: 'smooth',
70
+ top: expectedScrollPosition
71
+ })
72
+ })
73
+
74
+ describe('y-axis', () => {
75
+ it('scrolls the child into the viewing area when it is AFTER the overflow cutoff point', () => {
76
+ const scrollToMock = jest.fn()
77
+ Object.defineProperty(window.Element.prototype, 'scrollTo', {
78
+ writable: true,
79
+ value: scrollToMock
80
+ })
81
+ const childHeight = 50
82
+ const viewAreaHeight = 100
83
+ const childStart = viewAreaHeight + 10
84
+ const scrollMargin = 10
85
+ const expectedScrollPosition = scrollPositionFormula(
86
+ {viewingAreaEdgePosition: viewAreaHeight, childEdgePosition: childStart + childHeight, margin: scrollMargin},
87
+ false
88
+ )
89
+
90
+ const viewingAreaRect = makeDOMRect(0, 0, 100, viewAreaHeight)
91
+ const childRect = makeDOMRect(0, childStart, 100, childHeight)
92
+ const {viewingArea, child} = createVirtualDOM(viewingAreaRect, childRect)
93
+
94
+ viewingArea.getBoundingClientRect = () => viewingAreaRect
95
+ viewingArea.scrollTop = 0
96
+ child.getBoundingClientRect = () => childRect
97
+
98
+ scrollIntoViewingArea(child as HTMLDivElement, viewingArea, 'vertical', scrollMargin, scrollMargin, 'auto')
99
+ expect(scrollToMock).toHaveBeenCalledWith({
100
+ behavior: 'auto',
101
+ top: expectedScrollPosition
102
+ })
103
+ })
104
+
105
+ it('scrolls the child into the viewing area when it is BEFORE the overflow cutoff point', () => {
106
+ const scrollToMock = jest.fn()
107
+ Object.defineProperty(window.Element.prototype, 'scrollTo', {
108
+ writable: true,
109
+ value: scrollToMock
110
+ })
111
+ const childHeight = 50
112
+ const childStart = childHeight * -1 - 10
113
+ const scrollMargin = 10
114
+ const expectedScrollPosition = scrollPositionFormula(
115
+ {viewingAreaEdgePosition: 0, childEdgePosition: childStart, margin: scrollMargin},
116
+ true
117
+ )
118
+
119
+ const viewingAreaRect = makeDOMRect(0, 0, 100, 100)
120
+ const childRect = makeDOMRect(0, childStart, 100, childHeight)
121
+ const {viewingArea, child} = createVirtualDOM(viewingAreaRect, childRect)
122
+
123
+ viewingArea.getBoundingClientRect = () => viewingAreaRect
124
+ viewingArea.scrollTop = 0
125
+ child.getBoundingClientRect = () => childRect
126
+
127
+ scrollIntoViewingArea(child as HTMLDivElement, viewingArea, 'vertical', scrollMargin, scrollMargin, 'auto')
128
+ expect(scrollToMock).toHaveBeenCalledWith({
129
+ behavior: 'auto',
130
+ top: expectedScrollPosition
131
+ })
132
+ })
133
+ })
134
+
135
+ describe('x-axis', () => {
136
+ it('scrolls the child into the viewing area when it is AFTER the overflow cutoff point', () => {
137
+ const scrollToMock = jest.fn()
138
+ Object.defineProperty(window.Element.prototype, 'scrollTo', {
139
+ writable: true,
140
+ value: scrollToMock
141
+ })
142
+ const childWidth = 50
143
+ const viewAreaWidth = 100
144
+ const childStart = viewAreaWidth + 10
145
+ const scrollMargin = 10
146
+ const expectedScrollPosition = scrollPositionFormula(
147
+ {viewingAreaEdgePosition: viewAreaWidth, childEdgePosition: childStart + childWidth, margin: scrollMargin},
148
+ false
149
+ )
150
+
151
+ const viewingAreaRect = makeDOMRect(0, 0, 100, viewAreaWidth)
152
+ const childRect = makeDOMRect(childStart, 0, childWidth, 100)
153
+ const {viewingArea, child} = createVirtualDOM(viewingAreaRect, childRect)
154
+
155
+ viewingArea.getBoundingClientRect = () => viewingAreaRect
156
+ viewingArea.scrollLeft = 0
157
+ child.getBoundingClientRect = () => childRect
158
+
159
+ scrollIntoViewingArea(child as HTMLDivElement, viewingArea, 'horizontal', scrollMargin, scrollMargin, 'auto')
160
+ expect(scrollToMock).toHaveBeenCalledWith({
161
+ behavior: 'auto',
162
+ left: expectedScrollPosition
163
+ })
164
+ })
165
+
166
+ it('scrolls the child into the viewing area when it is BEFORE the overflow cutoff point', () => {
167
+ const scrollToMock = jest.fn()
168
+ Object.defineProperty(window.Element.prototype, 'scrollTo', {
169
+ writable: true,
170
+ value: scrollToMock
171
+ })
172
+ const childWidth = 50
173
+ const childStart = childWidth * -1 - 10
174
+ const scrollMargin = 10
175
+ const expectedScrollPosition = scrollPositionFormula(
176
+ {viewingAreaEdgePosition: 0, childEdgePosition: childStart, margin: scrollMargin},
177
+ true
178
+ )
179
+
180
+ const viewingAreaRect = makeDOMRect(0, 0, 100, 100)
181
+ const childRect = makeDOMRect(childStart, 0, childWidth, 100)
182
+ const {viewingArea, child} = createVirtualDOM(viewingAreaRect, childRect)
183
+
184
+ viewingArea.getBoundingClientRect = () => viewingAreaRect
185
+ viewingArea.scrollTop = 0
186
+ child.getBoundingClientRect = () => childRect
187
+
188
+ scrollIntoViewingArea(child as HTMLDivElement, viewingArea, 'horizontal', scrollMargin, scrollMargin, 'auto')
189
+ expect(scrollToMock).toHaveBeenCalledWith({
190
+ behavior: 'auto',
191
+ left: expectedScrollPosition
192
+ })
193
+ })
194
+ })
195
+ })
@@ -0,0 +1,27 @@
1
+ export const scrollIntoViewingArea = (
2
+ child: HTMLElement,
3
+ viewingArea: HTMLElement,
4
+ direction: 'horizontal' | 'vertical' = 'vertical',
5
+ startMargin = 8,
6
+ endMargin = 0,
7
+ behavior: ScrollBehavior = 'smooth'
8
+ ) => {
9
+ const startSide = direction === 'vertical' ? 'top' : 'left'
10
+ const endSide = direction === 'vertical' ? 'bottom' : 'right'
11
+ const scrollSide = direction === 'vertical' ? 'scrollTop' : 'scrollLeft'
12
+ const {[startSide]: childStart, [endSide]: childEnd} = child.getBoundingClientRect()
13
+ const {[startSide]: viewingAreaStart, [endSide]: viewingAreaEnd} = viewingArea.getBoundingClientRect()
14
+
15
+ const isChildStartAboveViewingArea = childStart < viewingAreaStart + endMargin
16
+ const isChildBottomBelowViewingArea = childEnd > viewingAreaEnd - startMargin
17
+
18
+ if (isChildStartAboveViewingArea) {
19
+ const scrollHeightToChildStart = childStart - viewingAreaStart + viewingArea[scrollSide]
20
+ viewingArea.scrollTo({behavior, [startSide]: scrollHeightToChildStart - endMargin})
21
+ } else if (isChildBottomBelowViewingArea) {
22
+ const scrollHeightToChildBottom = childEnd - viewingAreaEnd + viewingArea[scrollSide]
23
+ viewingArea.scrollTo({behavior, [startSide]: scrollHeightToChildBottom + startMargin})
24
+ }
25
+
26
+ // either completely in view or outside viewing area on both ends, don't scroll
27
+ }
@@ -5,14 +5,19 @@ export type UseOpenAndCloseFocusSettings = {
5
5
  initialFocusRef?: React.RefObject<HTMLElement>
6
6
  containerRef: React.RefObject<HTMLElement>
7
7
  returnFocusRef: React.RefObject<HTMLElement>
8
+ preventFocusOnOpen?: boolean
8
9
  }
9
10
 
10
11
  export function useOpenAndCloseFocus({
11
12
  initialFocusRef,
12
13
  returnFocusRef,
13
- containerRef
14
+ containerRef,
15
+ preventFocusOnOpen
14
16
  }: UseOpenAndCloseFocusSettings): void {
15
17
  useEffect(() => {
18
+ if (preventFocusOnOpen) {
19
+ return
20
+ }
16
21
  const returnRef = returnFocusRef.current
17
22
  if (initialFocusRef && initialFocusRef.current) {
18
23
  initialFocusRef.current.focus()
@@ -23,5 +28,5 @@ export function useOpenAndCloseFocus({
23
28
  return function () {
24
29
  returnRef?.focus()
25
30
  }
26
- }, [initialFocusRef, returnFocusRef, containerRef])
31
+ }, [initialFocusRef, returnFocusRef, containerRef, preventFocusOnOpen])
27
32
  }
@@ -10,6 +10,7 @@ export type UseOverlaySettings = {
10
10
  onEscape: (e: KeyboardEvent) => void
11
11
  onClickOutside: (e: TouchOrMouseEvent) => void
12
12
  overlayRef?: React.RefObject<HTMLDivElement>
13
+ preventFocusOnOpen?: boolean
13
14
  }
14
15
 
15
16
  export type OverlayReturnProps = {
@@ -22,10 +23,11 @@ export const useOverlay = ({
22
23
  initialFocusRef,
23
24
  onEscape,
24
25
  ignoreClickRefs,
25
- onClickOutside
26
+ onClickOutside,
27
+ preventFocusOnOpen
26
28
  }: UseOverlaySettings): OverlayReturnProps => {
27
29
  const overlayRef = useProvidedRefOrCreate<HTMLDivElement>(_overlayRef)
28
- useOpenAndCloseFocus({containerRef: overlayRef, returnFocusRef, initialFocusRef})
30
+ useOpenAndCloseFocus({containerRef: overlayRef, returnFocusRef, initialFocusRef, preventFocusOnOpen})
29
31
  useOnOutsideClick({containerRef: overlayRef, ignoreClickRefs, onClickOutside})
30
32
  useOnEscapePress(onEscape)
31
33
  return {ref: overlayRef}
package/src/index.ts CHANGED
@@ -30,6 +30,8 @@ export {useConfirm} from './Dialog/ConfirmationDialog'
30
30
  export {ActionList} from './ActionList'
31
31
  export {ActionMenu} from './ActionMenu'
32
32
  export type {ActionMenuProps} from './ActionMenu'
33
+ export {default as Autocomplete} from './Autocomplete'
34
+ export type {AutocompleteMenuProps, AutocompleteInputProps, AutocompleteOverlayProps} from './Autocomplete'
33
35
  export {default as Avatar} from './Avatar'
34
36
  export type {AvatarProps} from './Avatar'
35
37
  export {default as AvatarPair} from './AvatarPair'