@popsure/dirty-swan 0.65.1 → 0.66.1
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/dist/cjs/index.d.ts +2 -2
- package/dist/cjs/index.js +361 -209
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/lib/components/searchableDropdown/index.d.ts +22 -0
- package/dist/cjs/lib/components/searchableDropdown/index.stories.d.ts +108 -0
- package/dist/cjs/lib/components/searchableDropdown/index.test.d.ts +1 -0
- package/dist/cjs/lib/hooks/useDropdownAlignment.d.ts +5 -0
- package/dist/cjs/lib/index.d.ts +3 -2
- package/dist/esm/{Calendar-C7I0F5Gv.js → Calendar-8rhyapMz.js} +3 -19
- package/dist/esm/Calendar-8rhyapMz.js.map +1 -0
- package/dist/esm/components/dateSelector/components/Calendar.js +2 -1
- package/dist/esm/components/dateSelector/components/Calendar.js.map +1 -1
- package/dist/esm/components/dateSelector/index.js +2 -1
- package/dist/esm/components/dateSelector/index.js.map +1 -1
- package/dist/esm/components/dateSelector/index.stories.js +2 -1
- package/dist/esm/components/dateSelector/index.stories.js.map +1 -1
- package/dist/esm/components/dateSelector/index.test.js +2 -1
- package/dist/esm/components/dateSelector/index.test.js.map +1 -1
- package/dist/esm/components/searchableDropdown/index.js +13 -0
- package/dist/esm/components/searchableDropdown/index.js.map +1 -0
- package/dist/esm/components/searchableDropdown/index.stories.js +201 -0
- package/dist/esm/components/searchableDropdown/index.stories.js.map +1 -0
- package/dist/esm/components/searchableDropdown/index.test.js +607 -0
- package/dist/esm/components/searchableDropdown/index.test.js.map +1 -0
- package/dist/esm/components/toast/index.js +1 -1
- package/dist/esm/components/toast/index.stories.js +1 -1
- package/dist/esm/components/toast/index.test.js +1 -1
- package/dist/esm/{index-C4IAMlRE.js → index-CT0_LjIR.js} +2 -2
- package/dist/esm/{index-C4IAMlRE.js.map → index-CT0_LjIR.js.map} +1 -1
- package/dist/esm/index-QeP_xz9v.js +175 -0
- package/dist/esm/index-QeP_xz9v.js.map +1 -0
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +7 -17
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/components/searchableDropdown/index.d.ts +22 -0
- package/dist/esm/lib/components/searchableDropdown/index.stories.d.ts +108 -0
- package/dist/esm/lib/components/searchableDropdown/index.test.d.ts +1 -0
- package/dist/esm/lib/hooks/useDropdownAlignment.d.ts +5 -0
- package/dist/esm/lib/index.d.ts +3 -2
- package/dist/esm/useOnClickOutside-B5hujnpp.js +21 -0
- package/dist/esm/useOnClickOutside-B5hujnpp.js.map +1 -0
- package/dist/esm/util/images/index.stories.js +2 -1
- package/dist/esm/util/images/index.stories.js.map +1 -1
- package/package.json +1 -1
- package/src/index.tsx +3 -0
- package/src/lib/components/searchableDropdown/index.stories.tsx +286 -0
- package/src/lib/components/searchableDropdown/index.test.tsx +355 -0
- package/src/lib/components/searchableDropdown/index.tsx +267 -0
- package/src/lib/components/searchableDropdown/style.module.scss +137 -0
- package/src/lib/hooks/useDropdownAlignment.test.ts +210 -0
- package/src/lib/hooks/useDropdownAlignment.ts +34 -0
- package/src/lib/index.tsx +8 -0
- package/dist/esm/Calendar-C7I0F5Gv.js.map +0 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
@use '../../scss/public/colors' as *;
|
|
2
|
+
@use '../../scss/public/shadows' as *;
|
|
3
|
+
|
|
4
|
+
.container {
|
|
5
|
+
position: relative;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.selectTrigger {
|
|
9
|
+
padding: 10px 12px;
|
|
10
|
+
border: 1px solid $ds-neutral-300;
|
|
11
|
+
|
|
12
|
+
&:hover {
|
|
13
|
+
border-color: $ds-neutral-500;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
&:focus-visible {
|
|
17
|
+
outline: 1px solid $ds-neutral-900;
|
|
18
|
+
outline-offset: 1px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
&Open {
|
|
22
|
+
border-color: $ds-neutral-900;
|
|
23
|
+
box-shadow: $bs-xs;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.disabled {
|
|
28
|
+
opacity: 0.5;
|
|
29
|
+
cursor: not-allowed;
|
|
30
|
+
pointer-events: none;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.chevronOpen {
|
|
34
|
+
transform: rotate(180deg);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.condensed {
|
|
38
|
+
width: auto;
|
|
39
|
+
padding: 8px 10px;
|
|
40
|
+
gap: 8px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.bordered {
|
|
44
|
+
border: 1px solid $ds-neutral-300;
|
|
45
|
+
|
|
46
|
+
&:hover {
|
|
47
|
+
border-color: $ds-neutral-500;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.dropdown {
|
|
52
|
+
position: absolute;
|
|
53
|
+
top: calc(100% + 4px);
|
|
54
|
+
left: 0;
|
|
55
|
+
min-width: 280px;
|
|
56
|
+
border: 1px solid $ds-neutral-300;
|
|
57
|
+
box-shadow: $bs-md;
|
|
58
|
+
z-index: 10;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.dropdownRight {
|
|
62
|
+
left: auto;
|
|
63
|
+
right: 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.dropdownUp {
|
|
67
|
+
top: auto;
|
|
68
|
+
bottom: calc(100% + 4px);
|
|
69
|
+
|
|
70
|
+
.searchContainer {
|
|
71
|
+
order: 2;
|
|
72
|
+
padding-bottom: 0;
|
|
73
|
+
padding-top: 8px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.optionList {
|
|
77
|
+
order: 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.searchContainer {
|
|
82
|
+
position: sticky;
|
|
83
|
+
top: 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.optionList {
|
|
87
|
+
max-height: 308px;
|
|
88
|
+
overflow-y: auto;
|
|
89
|
+
scrollbar-width: none;
|
|
90
|
+
|
|
91
|
+
&::-webkit-scrollbar {
|
|
92
|
+
display: none;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.optionWrapper {
|
|
97
|
+
position: relative;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.optionRadio {
|
|
101
|
+
position: absolute;
|
|
102
|
+
opacity: 0;
|
|
103
|
+
width: 0;
|
|
104
|
+
height: 0;
|
|
105
|
+
|
|
106
|
+
&:focus-visible + label {
|
|
107
|
+
outline: 2px solid $ds-neutral-900;
|
|
108
|
+
outline-offset: -2px;
|
|
109
|
+
border-radius: 8px;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.option {
|
|
114
|
+
padding: 10px 12px;
|
|
115
|
+
border: none;
|
|
116
|
+
background-color: transparent;
|
|
117
|
+
|
|
118
|
+
&:hover {
|
|
119
|
+
background-color: $ds-neutral-100;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
&Selected {
|
|
123
|
+
background-color: $ds-purple-100;
|
|
124
|
+
|
|
125
|
+
&:hover {
|
|
126
|
+
background-color: $ds-purple-100;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.optionIcon {
|
|
132
|
+
flex-shrink: 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.noResults {
|
|
136
|
+
padding: 10px 12px;
|
|
137
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { renderHook, act } from '@testing-library/react-hooks';
|
|
2
|
+
import { RefObject } from 'react';
|
|
3
|
+
|
|
4
|
+
import { useDropdownAlignment } from './useDropdownAlignment';
|
|
5
|
+
|
|
6
|
+
const createRef = <T>(value: T | null = null): RefObject<T | null> => ({
|
|
7
|
+
current: value,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const mockContainerRect = (rect: Partial<DOMRect>) =>
|
|
11
|
+
({
|
|
12
|
+
top: 0,
|
|
13
|
+
bottom: 0,
|
|
14
|
+
left: 0,
|
|
15
|
+
right: 0,
|
|
16
|
+
width: 0,
|
|
17
|
+
height: 0,
|
|
18
|
+
x: 0,
|
|
19
|
+
y: 0,
|
|
20
|
+
toJSON: () => {},
|
|
21
|
+
...rect,
|
|
22
|
+
}) as DOMRect;
|
|
23
|
+
|
|
24
|
+
describe('useDropdownAlignment', () => {
|
|
25
|
+
let containerEl: HTMLElement;
|
|
26
|
+
let dropdownEl: HTMLElement;
|
|
27
|
+
let containerRef: RefObject<HTMLElement | null>;
|
|
28
|
+
let dropdownRef: RefObject<HTMLElement | null>;
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
jest.clearAllMocks();
|
|
32
|
+
|
|
33
|
+
window.innerWidth = 1024;
|
|
34
|
+
window.innerHeight = 768;
|
|
35
|
+
|
|
36
|
+
containerEl = document.createElement('div');
|
|
37
|
+
dropdownEl = document.createElement('div');
|
|
38
|
+
|
|
39
|
+
containerRef = createRef(containerEl);
|
|
40
|
+
dropdownRef = createRef(dropdownEl);
|
|
41
|
+
|
|
42
|
+
Object.defineProperty(dropdownEl, 'offsetWidth', {
|
|
43
|
+
value: 200,
|
|
44
|
+
configurable: true,
|
|
45
|
+
});
|
|
46
|
+
Object.defineProperty(dropdownEl, 'offsetHeight', {
|
|
47
|
+
value: 300,
|
|
48
|
+
configurable: true,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should return alignRight and alignUp as false by default', () => {
|
|
53
|
+
const { result } = renderHook(() =>
|
|
54
|
+
useDropdownAlignment(containerRef, dropdownRef, false)
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
expect(result.current.alignRight).toBe(false);
|
|
58
|
+
expect(result.current.alignUp).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should not compute alignment when isOpen is false', () => {
|
|
62
|
+
jest
|
|
63
|
+
.spyOn(containerEl, 'getBoundingClientRect')
|
|
64
|
+
.mockReturnValue(mockContainerRect({ left: 900, bottom: 600, top: 600 }));
|
|
65
|
+
|
|
66
|
+
renderHook(() => useDropdownAlignment(containerRef, dropdownRef, false));
|
|
67
|
+
|
|
68
|
+
expect(containerEl.getBoundingClientRect).not.toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should not compute alignment when refs are null', () => {
|
|
72
|
+
const nullContainerRef = createRef<HTMLElement>(null);
|
|
73
|
+
const nullDropdownRef = createRef<HTMLElement>(null);
|
|
74
|
+
|
|
75
|
+
const { result } = renderHook(() =>
|
|
76
|
+
useDropdownAlignment(nullContainerRef, nullDropdownRef, true)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
expect(result.current.alignRight).toBe(false);
|
|
80
|
+
expect(result.current.alignUp).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('horizontal alignment', () => {
|
|
84
|
+
it('should set alignRight to false when there is enough space on the right', () => {
|
|
85
|
+
jest.spyOn(containerEl, 'getBoundingClientRect').mockReturnValue(
|
|
86
|
+
mockContainerRect({ left: 100, bottom: 100, top: 100 })
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const { result } = renderHook(() =>
|
|
90
|
+
useDropdownAlignment(containerRef, dropdownRef, true)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// spaceOnRight = 1024 - 100 = 924, dropdownWidth = 200 → enough space
|
|
94
|
+
expect(result.current.alignRight).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should set alignRight to true when there is not enough space on the right', () => {
|
|
98
|
+
jest.spyOn(containerEl, 'getBoundingClientRect').mockReturnValue(
|
|
99
|
+
mockContainerRect({ left: 900, bottom: 100, top: 100 })
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const { result } = renderHook(() =>
|
|
103
|
+
useDropdownAlignment(containerRef, dropdownRef, true)
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// spaceOnRight = 1024 - 900 = 124, dropdownWidth = 200 → not enough space
|
|
107
|
+
expect(result.current.alignRight).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('vertical alignment', () => {
|
|
112
|
+
it('should set alignUp to false when there is enough space below', () => {
|
|
113
|
+
jest.spyOn(containerEl, 'getBoundingClientRect').mockReturnValue(
|
|
114
|
+
mockContainerRect({ left: 100, bottom: 100, top: 100 })
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const { result } = renderHook(() =>
|
|
118
|
+
useDropdownAlignment(containerRef, dropdownRef, true)
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// spaceBelow = 768 - 100 = 668, dropdownHeight = 300 → enough space
|
|
122
|
+
expect(result.current.alignUp).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should set alignUp to true when not enough space below and more space above', () => {
|
|
126
|
+
jest.spyOn(containerEl, 'getBoundingClientRect').mockReturnValue(
|
|
127
|
+
mockContainerRect({ left: 100, bottom: 600, top: 600 })
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const { result } = renderHook(() =>
|
|
131
|
+
useDropdownAlignment(containerRef, dropdownRef, true)
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// spaceBelow = 768 - 600 = 168, dropdownHeight = 300 → not enough below
|
|
135
|
+
// containerRect.top = 600 > spaceBelow = 168 → more space above
|
|
136
|
+
expect(result.current.alignUp).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should set alignUp to false when not enough space below but more space below than above', () => {
|
|
140
|
+
jest.spyOn(containerEl, 'getBoundingClientRect').mockReturnValue(
|
|
141
|
+
mockContainerRect({ left: 100, bottom: 500, top: 100 })
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const { result } = renderHook(() =>
|
|
145
|
+
useDropdownAlignment(containerRef, dropdownRef, true)
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// spaceBelow = 768 - 500 = 268, dropdownHeight = 300 → not enough below
|
|
149
|
+
// containerRect.top = 100 < spaceBelow = 268 → more space below
|
|
150
|
+
expect(result.current.alignUp).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('ResizeObserver', () => {
|
|
155
|
+
it('should observe document.documentElement when isOpen is true', () => {
|
|
156
|
+
jest
|
|
157
|
+
.spyOn(containerEl, 'getBoundingClientRect')
|
|
158
|
+
.mockReturnValue(mockContainerRect({ left: 100, bottom: 100, top: 100 }));
|
|
159
|
+
|
|
160
|
+
renderHook(() => useDropdownAlignment(containerRef, dropdownRef, true));
|
|
161
|
+
|
|
162
|
+
const observerInstance = (ResizeObserver as jest.Mock).mock.results[0].value;
|
|
163
|
+
expect(observerInstance.observe).toHaveBeenCalledWith(
|
|
164
|
+
document.documentElement
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should disconnect observer when isOpen changes to false', () => {
|
|
169
|
+
jest
|
|
170
|
+
.spyOn(containerEl, 'getBoundingClientRect')
|
|
171
|
+
.mockReturnValue(mockContainerRect({ left: 100, bottom: 100, top: 100 }));
|
|
172
|
+
|
|
173
|
+
const { rerender } = renderHook(
|
|
174
|
+
({ isOpen }) => useDropdownAlignment(containerRef, dropdownRef, isOpen),
|
|
175
|
+
{ initialProps: { isOpen: true } }
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const observerInstance = (ResizeObserver as jest.Mock).mock.results[0].value;
|
|
179
|
+
|
|
180
|
+
rerender({ isOpen: false });
|
|
181
|
+
|
|
182
|
+
expect(observerInstance.disconnect).toHaveBeenCalled();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should recalculate alignment when ResizeObserver fires', () => {
|
|
186
|
+
jest
|
|
187
|
+
.spyOn(containerEl, 'getBoundingClientRect')
|
|
188
|
+
.mockReturnValue(mockContainerRect({ left: 100, bottom: 100, top: 100 }));
|
|
189
|
+
|
|
190
|
+
const { result } = renderHook(() =>
|
|
191
|
+
useDropdownAlignment(containerRef, dropdownRef, true)
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
expect(result.current.alignRight).toBe(false);
|
|
195
|
+
|
|
196
|
+
// Simulate viewport change: now not enough space on the right
|
|
197
|
+
jest
|
|
198
|
+
.spyOn(containerEl, 'getBoundingClientRect')
|
|
199
|
+
.mockReturnValue(mockContainerRect({ left: 900, bottom: 100, top: 100 }));
|
|
200
|
+
|
|
201
|
+
// Get the callback passed to ResizeObserver and invoke it
|
|
202
|
+
const observerCallback = (ResizeObserver as jest.Mock).mock.calls[0][0];
|
|
203
|
+
act(() => {
|
|
204
|
+
observerCallback();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
expect(result.current.alignRight).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { RefObject, useCallback, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export const useDropdownAlignment = (
|
|
4
|
+
containerRef: RefObject<HTMLElement | null>,
|
|
5
|
+
dropdownRef: RefObject<HTMLElement | null>,
|
|
6
|
+
isOpen: boolean
|
|
7
|
+
) => {
|
|
8
|
+
const [alignRight, setAlignRight] = useState(false);
|
|
9
|
+
const [alignUp, setAlignUp] = useState(false);
|
|
10
|
+
|
|
11
|
+
const updateAlignment = useCallback(() => {
|
|
12
|
+
if (containerRef.current && dropdownRef.current) {
|
|
13
|
+
const containerRect = containerRef.current.getBoundingClientRect();
|
|
14
|
+
const dropdownWidth = dropdownRef.current.offsetWidth;
|
|
15
|
+
const dropdownHeight = dropdownRef.current.offsetHeight;
|
|
16
|
+
const spaceOnRight = window.innerWidth - containerRect.left;
|
|
17
|
+
const spaceBelow = window.innerHeight - containerRect.bottom;
|
|
18
|
+
setAlignRight(spaceOnRight < dropdownWidth);
|
|
19
|
+
setAlignUp(spaceBelow < dropdownHeight && containerRect.top > spaceBelow);
|
|
20
|
+
}
|
|
21
|
+
}, [containerRef, dropdownRef]);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!isOpen) return;
|
|
25
|
+
|
|
26
|
+
updateAlignment();
|
|
27
|
+
|
|
28
|
+
const observer = new ResizeObserver(updateAlignment);
|
|
29
|
+
observer.observe(document.documentElement);
|
|
30
|
+
return () => observer.disconnect();
|
|
31
|
+
}, [isOpen, updateAlignment]);
|
|
32
|
+
|
|
33
|
+
return { alignRight, alignUp };
|
|
34
|
+
};
|
package/src/lib/index.tsx
CHANGED
|
@@ -41,6 +41,11 @@ import {
|
|
|
41
41
|
TableHeader,
|
|
42
42
|
} from './components/comparisonTable';
|
|
43
43
|
import { SegmentedControl } from './components/segmentedControl';
|
|
44
|
+
import {
|
|
45
|
+
SearchableDropdown,
|
|
46
|
+
SearchableDropdownProps,
|
|
47
|
+
SearchableDropdownOption,
|
|
48
|
+
} from './components/searchableDropdown';
|
|
44
49
|
import { Link } from './components/link';
|
|
45
50
|
import { illustrations, images, IllustrationKeys } from './util/images';
|
|
46
51
|
import { Spinner } from './components/spinner';
|
|
@@ -91,6 +96,7 @@ export {
|
|
|
91
96
|
TableRowHeader,
|
|
92
97
|
TableButton,
|
|
93
98
|
TableInfoButton,
|
|
99
|
+
SearchableDropdown,
|
|
94
100
|
SegmentedControl,
|
|
95
101
|
Checkbox,
|
|
96
102
|
Radio,
|
|
@@ -120,6 +126,8 @@ export {
|
|
|
120
126
|
|
|
121
127
|
export type {
|
|
122
128
|
AccordionProps,
|
|
129
|
+
SearchableDropdownProps,
|
|
130
|
+
SearchableDropdownOption,
|
|
123
131
|
BadgeProps,
|
|
124
132
|
IllustrationKeys,
|
|
125
133
|
InformationBoxProps,
|