@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.
- package/CHANGELOG.md +4 -2
- package/dist/browser.esm.js +717 -718
- package/dist/browser.esm.js.map +1 -1
- package/dist/browser.umd.js +320 -321
- package/dist/browser.umd.js.map +1 -1
- package/docs/content/Autocomplete.mdx +627 -0
- package/docs/content/TextInputTokens.mdx +89 -0
- package/docs/src/@primer/gatsby-theme-doctocat/nav.yml +2 -0
- package/lib/AnchoredOverlay/AnchoredOverlay.d.ts +2 -1
- package/lib/AnchoredOverlay/AnchoredOverlay.js +11 -3
- package/lib/Autocomplete/Autocomplete.d.ts +304 -0
- package/lib/Autocomplete/Autocomplete.js +145 -0
- package/lib/Autocomplete/AutocompleteContext.d.ts +17 -0
- package/lib/Autocomplete/AutocompleteContext.js +11 -0
- package/lib/Autocomplete/AutocompleteInput.d.ts +292 -0
- package/lib/Autocomplete/AutocompleteInput.js +157 -0
- package/lib/Autocomplete/AutocompleteMenu.d.ts +72 -0
- package/lib/Autocomplete/AutocompleteMenu.js +224 -0
- package/lib/Autocomplete/AutocompleteOverlay.d.ts +20 -0
- package/lib/Autocomplete/AutocompleteOverlay.js +80 -0
- package/lib/Autocomplete/index.d.ts +2 -0
- package/lib/Autocomplete/index.js +15 -0
- package/lib/FilteredActionList/FilteredActionList.js +5 -31
- package/lib/Overlay.d.ts +1 -0
- package/lib/Overlay.js +3 -1
- package/lib/__tests__/Autocomplete.test.d.ts +1 -0
- package/lib/__tests__/Autocomplete.test.js +528 -0
- package/lib/__tests__/behaviors/scrollIntoViewingArea.test.d.ts +1 -0
- package/lib/__tests__/behaviors/scrollIntoViewingArea.test.js +226 -0
- package/lib/behaviors/scrollIntoViewingArea.d.ts +1 -0
- package/lib/behaviors/scrollIntoViewingArea.js +39 -0
- package/lib/hooks/useOpenAndCloseFocus.d.ts +2 -1
- package/lib/hooks/useOpenAndCloseFocus.js +7 -2
- package/lib/hooks/useOverlay.d.ts +2 -1
- package/lib/hooks/useOverlay.js +4 -2
- package/lib/index.d.ts +2 -0
- package/lib/index.js +8 -0
- package/lib/stories/Autocomplete.stories.js +608 -0
- package/lib/utils/types/MandateProps.d.ts +3 -0
- package/lib/utils/types/MandateProps.js +1 -0
- package/lib/utils/types/index.d.ts +1 -0
- package/lib/utils/types/index.js +13 -0
- package/lib-esm/AnchoredOverlay/AnchoredOverlay.d.ts +2 -1
- package/lib-esm/AnchoredOverlay/AnchoredOverlay.js +11 -3
- package/lib-esm/Autocomplete/Autocomplete.d.ts +304 -0
- package/lib-esm/Autocomplete/Autocomplete.js +123 -0
- package/lib-esm/Autocomplete/AutocompleteContext.d.ts +17 -0
- package/lib-esm/Autocomplete/AutocompleteContext.js +2 -0
- package/lib-esm/Autocomplete/AutocompleteInput.d.ts +292 -0
- package/lib-esm/Autocomplete/AutocompleteInput.js +138 -0
- package/lib-esm/Autocomplete/AutocompleteMenu.d.ts +72 -0
- package/lib-esm/Autocomplete/AutocompleteMenu.js +205 -0
- package/lib-esm/Autocomplete/AutocompleteOverlay.d.ts +20 -0
- package/lib-esm/Autocomplete/AutocompleteOverlay.js +62 -0
- package/lib-esm/Autocomplete/index.d.ts +2 -0
- package/lib-esm/Autocomplete/index.js +1 -0
- package/lib-esm/FilteredActionList/FilteredActionList.js +3 -31
- package/lib-esm/Overlay.d.ts +1 -0
- package/lib-esm/Overlay.js +3 -1
- package/lib-esm/__tests__/Autocomplete.test.d.ts +1 -0
- package/lib-esm/__tests__/Autocomplete.test.js +494 -0
- package/lib-esm/__tests__/behaviors/scrollIntoViewingArea.test.d.ts +1 -0
- package/lib-esm/__tests__/behaviors/scrollIntoViewingArea.test.js +224 -0
- package/lib-esm/behaviors/scrollIntoViewingArea.d.ts +1 -0
- package/lib-esm/behaviors/scrollIntoViewingArea.js +30 -0
- package/lib-esm/hooks/useOpenAndCloseFocus.d.ts +2 -1
- package/lib-esm/hooks/useOpenAndCloseFocus.js +7 -2
- package/lib-esm/hooks/useOverlay.d.ts +2 -1
- package/lib-esm/hooks/useOverlay.js +4 -2
- package/lib-esm/index.d.ts +2 -0
- package/lib-esm/index.js +1 -0
- package/lib-esm/stories/Autocomplete.stories.js +549 -0
- package/lib-esm/utils/types/MandateProps.d.ts +3 -0
- package/lib-esm/utils/types/MandateProps.js +1 -0
- package/lib-esm/utils/types/index.d.ts +1 -0
- package/lib-esm/utils/types/index.js +2 -1
- package/package.json +1 -1
- package/src/AnchoredOverlay/AnchoredOverlay.tsx +14 -3
- package/src/Autocomplete/Autocomplete.tsx +103 -0
- package/src/Autocomplete/AutocompleteContext.tsx +19 -0
- package/src/Autocomplete/AutocompleteInput.tsx +179 -0
- package/src/Autocomplete/AutocompleteMenu.tsx +341 -0
- package/src/Autocomplete/AutocompleteOverlay.tsx +68 -0
- package/src/Autocomplete/index.ts +2 -0
- package/src/FilteredActionList/FilteredActionList.tsx +10 -25
- package/src/Overlay.tsx +4 -1
- package/src/__tests__/Autocomplete.test.tsx +444 -0
- package/src/__tests__/__snapshots__/Autocomplete.test.tsx.snap +3414 -0
- package/src/__tests__/behaviors/scrollIntoViewingArea.test.ts +195 -0
- package/src/behaviors/scrollIntoViewingArea.ts +27 -0
- package/src/hooks/useOpenAndCloseFocus.ts +7 -2
- package/src/hooks/useOverlay.tsx +4 -2
- package/src/index.ts +2 -0
- package/src/stories/Autocomplete.stories.tsx +572 -0
- package/src/utils/types/MandateProps.ts +19 -0
- package/src/utils/types/index.ts +1 -0
- 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
|
}
|
package/src/hooks/useOverlay.tsx
CHANGED
@@ -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'
|