@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.
Files changed (53) hide show
  1. package/dist/cjs/index.d.ts +2 -2
  2. package/dist/cjs/index.js +361 -209
  3. package/dist/cjs/index.js.map +1 -1
  4. package/dist/cjs/lib/components/searchableDropdown/index.d.ts +22 -0
  5. package/dist/cjs/lib/components/searchableDropdown/index.stories.d.ts +108 -0
  6. package/dist/cjs/lib/components/searchableDropdown/index.test.d.ts +1 -0
  7. package/dist/cjs/lib/hooks/useDropdownAlignment.d.ts +5 -0
  8. package/dist/cjs/lib/index.d.ts +3 -2
  9. package/dist/esm/{Calendar-C7I0F5Gv.js → Calendar-8rhyapMz.js} +3 -19
  10. package/dist/esm/Calendar-8rhyapMz.js.map +1 -0
  11. package/dist/esm/components/dateSelector/components/Calendar.js +2 -1
  12. package/dist/esm/components/dateSelector/components/Calendar.js.map +1 -1
  13. package/dist/esm/components/dateSelector/index.js +2 -1
  14. package/dist/esm/components/dateSelector/index.js.map +1 -1
  15. package/dist/esm/components/dateSelector/index.stories.js +2 -1
  16. package/dist/esm/components/dateSelector/index.stories.js.map +1 -1
  17. package/dist/esm/components/dateSelector/index.test.js +2 -1
  18. package/dist/esm/components/dateSelector/index.test.js.map +1 -1
  19. package/dist/esm/components/searchableDropdown/index.js +13 -0
  20. package/dist/esm/components/searchableDropdown/index.js.map +1 -0
  21. package/dist/esm/components/searchableDropdown/index.stories.js +201 -0
  22. package/dist/esm/components/searchableDropdown/index.stories.js.map +1 -0
  23. package/dist/esm/components/searchableDropdown/index.test.js +607 -0
  24. package/dist/esm/components/searchableDropdown/index.test.js.map +1 -0
  25. package/dist/esm/components/toast/index.js +1 -1
  26. package/dist/esm/components/toast/index.stories.js +1 -1
  27. package/dist/esm/components/toast/index.test.js +1 -1
  28. package/dist/esm/{index-C4IAMlRE.js → index-CT0_LjIR.js} +2 -2
  29. package/dist/esm/{index-C4IAMlRE.js.map → index-CT0_LjIR.js.map} +1 -1
  30. package/dist/esm/index-QeP_xz9v.js +175 -0
  31. package/dist/esm/index-QeP_xz9v.js.map +1 -0
  32. package/dist/esm/index.d.ts +2 -2
  33. package/dist/esm/index.js +7 -17
  34. package/dist/esm/index.js.map +1 -1
  35. package/dist/esm/lib/components/searchableDropdown/index.d.ts +22 -0
  36. package/dist/esm/lib/components/searchableDropdown/index.stories.d.ts +108 -0
  37. package/dist/esm/lib/components/searchableDropdown/index.test.d.ts +1 -0
  38. package/dist/esm/lib/hooks/useDropdownAlignment.d.ts +5 -0
  39. package/dist/esm/lib/index.d.ts +3 -2
  40. package/dist/esm/useOnClickOutside-B5hujnpp.js +21 -0
  41. package/dist/esm/useOnClickOutside-B5hujnpp.js.map +1 -0
  42. package/dist/esm/util/images/index.stories.js +2 -1
  43. package/dist/esm/util/images/index.stories.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/index.tsx +3 -0
  46. package/src/lib/components/searchableDropdown/index.stories.tsx +286 -0
  47. package/src/lib/components/searchableDropdown/index.test.tsx +355 -0
  48. package/src/lib/components/searchableDropdown/index.tsx +267 -0
  49. package/src/lib/components/searchableDropdown/style.module.scss +137 -0
  50. package/src/lib/hooks/useDropdownAlignment.test.ts +210 -0
  51. package/src/lib/hooks/useDropdownAlignment.ts +34 -0
  52. package/src/lib/index.tsx +8 -0
  53. 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,