@popsure/dirty-swan 0.62.5 → 0.63.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@popsure/dirty-swan",
3
- "version": "v0.62.5",
3
+ "version": "0.63.0",
4
4
  "author": "Vincent Audoire <vincent@getpopsure.com>",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -1,7 +1,7 @@
1
1
  import AnimateHeight from 'react-animate-height';
2
2
 
3
3
  import styles from './AccordionItem.module.scss';
4
- import { useState } from 'react';
4
+ import { useRef, useState } from 'react';
5
5
 
6
6
  const ChevronSVG = ({ className }: { className?: string }) => (
7
7
  <svg
@@ -27,19 +27,52 @@ export const AccordionItem = ({
27
27
  className = '',
28
28
  headerClassName = '',
29
29
  label,
30
+ isOpen: controlledIsOpen,
31
+ onToggle,
32
+ scrollOnOpen,
33
+ scrollTopOffset = 0,
30
34
  }: {
31
35
  children: React.ReactNode | string;
32
36
  className?: string;
33
37
  headerClassName?: string;
34
38
  label: React.ReactNode;
39
+ isOpen?: boolean;
40
+ onToggle?: () => void;
41
+ scrollOnOpen?: boolean;
42
+ scrollTopOffset?: number;
35
43
  }) => {
36
- const [isOpen, setIsOpen] = useState(false);
44
+ const [internalIsOpen, setInternalIsOpen] = useState(false);
45
+ const sectionRef = useRef<HTMLElement>(null);
46
+ const userToggled = useRef(false);
47
+
48
+ const isControlled = controlledIsOpen !== undefined && onToggle !== undefined;
49
+ const isOpen = isControlled ? controlledIsOpen : internalIsOpen;
50
+
37
51
  const handleClick = () => {
38
- setIsOpen(!isOpen);
52
+ userToggled.current = true;
53
+ if (isControlled) {
54
+ onToggle();
55
+ } else {
56
+ setInternalIsOpen(!internalIsOpen);
57
+ }
58
+ };
59
+
60
+ const handleAnimationEnd = () => {
61
+ if (userToggled.current && isOpen && scrollOnOpen && sectionRef.current) {
62
+ userToggled.current = false;
63
+ const top =
64
+ sectionRef.current.getBoundingClientRect().top +
65
+ window.scrollY -
66
+ scrollTopOffset;
67
+ window.scrollTo({ top, behavior: 'smooth' });
68
+ }
39
69
  };
40
70
 
41
71
  return (
42
- <section className={`d-flex fd-column ${styles.container} ${className}`}>
72
+ <section
73
+ ref={sectionRef}
74
+ className={`d-flex fd-column ${styles.container} ${className}`}
75
+ >
43
76
  <button
44
77
  className={`d-flex ai-center jc-between ${styles.headerButton} ${headerClassName}`}
45
78
  onClick={handleClick}
@@ -58,7 +91,11 @@ export const AccordionItem = ({
58
91
  </button>
59
92
  {/* Min height is 0.1 so that the scroll position is correctly synced across accordion items but is not actually shown.
60
93
  If set to 0, react-animate-height will set display to "none" which means scrolling is not synced. */}
61
- <AnimateHeight duration={300} height={isOpen ? 'auto' : 0.1}>
94
+ <AnimateHeight
95
+ duration={300}
96
+ height={isOpen ? 'auto' : 0.1}
97
+ onHeightAnimationEnd={handleAnimationEnd}
98
+ >
62
99
  {children}
63
100
  </AnimateHeight>
64
101
  </section>
@@ -11,7 +11,6 @@ export const useComparisonTable = ({
11
11
  const [showMore, setShowMore] = useState<boolean>(false);
12
12
  const [headerWidth, setHeaderWidth] = useState(1400);
13
13
  const [selectedTabIndex, setSelectedTabIndex] = useState(0);
14
- const [selectedSection, setSelectedSection] = useState('');
15
14
  const [headerId, setHeaderId] = useState('');
16
15
  const headerRef = useRef<HTMLDivElement | null>(null);
17
16
  const contentContainerRef = useRef<HTMLDivElement | null>(null);
@@ -140,8 +139,6 @@ export const useComparisonTable = ({
140
139
  headerWidth,
141
140
  headerId,
142
141
  contentContainerRef,
143
- selectedSection,
144
- setSelectedSection,
145
142
  selectedTabIndex,
146
143
  setSelectedTabIndex,
147
144
  headerRefCallbackRef,
@@ -254,6 +254,14 @@ const story = {
254
254
  collapsibleSections: {
255
255
  description: 'Make table groups with a label collapsible',
256
256
  },
257
+ scrollOnOpen: {
258
+ description:
259
+ 'When enabled, the page scrolls to the top of a newly expanded section.',
260
+ },
261
+ scrollTopOffset: {
262
+ description:
263
+ 'Offset in pixels from the top of the viewport when scrolling to an expanded section.',
264
+ },
257
265
  cellWidth: {
258
266
  description: 'Width of a table content cell',
259
267
  },
@@ -287,7 +295,9 @@ const story = {
287
295
  showDetailsCaption: 'Show details',
288
296
  hideScrollBars: false,
289
297
  hideScrollBarsMobile: true,
290
- collapsibleSections: false,
298
+ collapsibleSections: true,
299
+ scrollOnOpen: true,
300
+ scrollTopOffset: 0,
291
301
  cellWidth: undefined,
292
302
  firstColumnWidth: undefined,
293
303
  stickyHeaderTopOffset: 0,
@@ -325,6 +335,8 @@ export const ComparisonTableStory = {
325
335
  data,
326
336
  headers,
327
337
  collapsibleSections,
338
+ scrollOnOpen,
339
+ scrollTopOffset,
328
340
  hideDetails,
329
341
  classNameOverrides,
330
342
  hideDetailsCaption,
@@ -341,6 +353,8 @@ export const ComparisonTableStory = {
341
353
  data={data}
342
354
  headers={headers}
343
355
  collapsibleSections={collapsibleSections}
356
+ scrollOnOpen={scrollOnOpen}
357
+ scrollTopOffset={scrollTopOffset}
344
358
  hideDetails={hideDetails}
345
359
  classNameOverrides={classNameOverrides}
346
360
  hideDetailsCaption={hideDetailsCaption}
@@ -1,5 +1,5 @@
1
1
  import classNames from 'classnames';
2
- import { Fragment } from 'react';
2
+ import { Fragment, useRef, useState } from 'react';
3
3
  import { ScrollSync, ScrollSyncPane } from 'react-scroll-sync';
4
4
 
5
5
  import { AccordionItem } from './components/AccordionItem';
@@ -52,6 +52,8 @@ export interface ComparisonTableProps<T> {
52
52
  hideScrollBars?: boolean;
53
53
  hideScrollBarsMobile?: boolean;
54
54
  collapsibleSections?: boolean;
55
+ scrollOnOpen?: boolean;
56
+ scrollTopOffset?: number;
55
57
  cellWidth?: number;
56
58
  firstColumnWidth?: number;
57
59
  stickyHeaderTopOffset?: number;
@@ -83,6 +85,8 @@ const ComparisonTable = <T extends { id: number }>(
83
85
  hideScrollBars,
84
86
  hideScrollBarsMobile = true,
85
87
  collapsibleSections,
88
+ scrollOnOpen,
89
+ scrollTopOffset = 0,
86
90
  cellWidth,
87
91
  firstColumnWidth,
88
92
  stickyHeaderTopOffset,
@@ -94,8 +98,6 @@ const ComparisonTable = <T extends { id: number }>(
94
98
  headerWidth,
95
99
  headerId,
96
100
  contentContainerRef,
97
- selectedSection,
98
- setSelectedSection,
99
101
  selectedTabIndex,
100
102
  headerRefCallbackRef,
101
103
  handleArrowsClick,
@@ -103,6 +105,9 @@ const ComparisonTable = <T extends { id: number }>(
103
105
  showMore,
104
106
  } = useComparisonTable({ onSelectionChanged });
105
107
 
108
+ const [openSectionId, setOpenSectionId] = useState<number | null>(null);
109
+ const stickyHeaderRef = useRef<HTMLDivElement>(null);
110
+
106
111
  const cssVariablesStyle = {
107
112
  '--tableWidth': `${headerWidth}px`,
108
113
  ...(cellWidth ? { '--cellWidth': `${cellWidth}px` } : {}),
@@ -125,6 +130,7 @@ const ComparisonTable = <T extends { id: number }>(
125
130
  })}
126
131
  >
127
132
  <div
133
+ ref={stickyHeaderRef}
128
134
  id={headerId}
129
135
  className={classNames(baseStyles.header, classNameOverrides?.header)}
130
136
  >
@@ -190,6 +196,18 @@ const ComparisonTable = <T extends { id: number }>(
190
196
  )}
191
197
  label={headerGroup.label}
192
198
  headerClassName="p24 br8"
199
+ isOpen={openSectionId === headerGroup.id}
200
+ onToggle={() =>
201
+ setOpenSectionId((prev) =>
202
+ prev === headerGroup.id ? null : headerGroup.id
203
+ )
204
+ }
205
+ scrollOnOpen={scrollOnOpen}
206
+ scrollTopOffset={
207
+ scrollTopOffset +
208
+ (stickyHeaderRef.current?.getBoundingClientRect()
209
+ .height ?? 0)
210
+ }
193
211
  >
194
212
  <ScrollSyncPane>
195
213
  <div
@@ -44,7 +44,6 @@
44
44
  border: 1px solid $ds-neutral-300;
45
45
  cursor: pointer;
46
46
  font-weight: 700;
47
- font-size: 1.15em;
48
47
  color: $ds-neutral-900;
49
48
  padding: 8px 12px;
50
49
  border-radius: 8px;
@@ -244,7 +244,6 @@
244
244
  top: -112px;
245
245
  z-index: 100;
246
246
  background-color: white;
247
- border: 1px solid #d2d2d8;
248
247
  box-shadow: 0px 0px 32px rgba(210, 210, 216, 0.25);
249
248
  border-radius: 16px;
250
249
  display: inline-block;
@@ -143,6 +143,48 @@ const initialData: TableData = [
143
143
  { rating: { type: 'zap', value: 3 } },
144
144
  { rating: { type: 'star', value: 3 } },
145
145
  ],
146
+ [
147
+ { text: 'Rating', modalContent: 'info' },
148
+ { rating: { type: 'zap', value: 1 }, modalContent: 'Maybe' },
149
+ { rating: { type: 'zap', value: 3 } },
150
+ { rating: { type: 'star', value: 3 } },
151
+ ],
152
+ [
153
+ { text: 'Rating', modalContent: 'info' },
154
+ { rating: { type: 'zap', value: 1 }, modalContent: 'Maybe' },
155
+ { rating: { type: 'zap', value: 3 } },
156
+ { rating: { type: 'star', value: 3 } },
157
+ ],
158
+ [
159
+ { text: 'Rating', modalContent: 'info' },
160
+ { rating: { type: 'zap', value: 1 }, modalContent: 'Maybe' },
161
+ { rating: { type: 'zap', value: 3 } },
162
+ { rating: { type: 'star', value: 3 } },
163
+ ],
164
+ [
165
+ { text: 'Rating', modalContent: 'info' },
166
+ { rating: { type: 'zap', value: 1 }, modalContent: 'Maybe' },
167
+ { rating: { type: 'zap', value: 3 } },
168
+ { rating: { type: 'star', value: 3 } },
169
+ ],
170
+ [
171
+ { text: 'Rating', modalContent: 'info' },
172
+ { rating: { type: 'zap', value: 1 }, modalContent: 'Maybe' },
173
+ { rating: { type: 'zap', value: 3 } },
174
+ { rating: { type: 'star', value: 3 } },
175
+ ],
176
+ [
177
+ { text: 'Rating', modalContent: 'info' },
178
+ { rating: { type: 'zap', value: 1 }, modalContent: 'Maybe' },
179
+ { rating: { type: 'zap', value: 3 } },
180
+ { rating: { type: 'star', value: 3 } },
181
+ ],
182
+ [
183
+ { text: 'Rating', modalContent: 'info' },
184
+ { rating: { type: 'zap', value: 1 }, modalContent: 'Maybe' },
185
+ { rating: { type: 'zap', value: 3 } },
186
+ { rating: { type: 'star', value: 3 } },
187
+ ],
146
188
  [
147
189
  {
148
190
  type: 'CARD',
@@ -203,6 +245,14 @@ const story = {
203
245
  collapsibleSections: {
204
246
  subContent: 'This property allows to collapse the sections of the table.',
205
247
  },
248
+ scrollOnOpen: {
249
+ subContent:
250
+ 'When enabled, the page scrolls to the top of a newly expanded section.',
251
+ },
252
+ scrollTopOffset: {
253
+ subContent:
254
+ 'Offset in pixels from the top of the viewport when scrolling to an expanded section.',
255
+ },
206
256
  hideDetails: {
207
257
  subContent: 'This property allows to hide the details of the table.',
208
258
  },
@@ -244,7 +294,9 @@ const story = {
244
294
  },
245
295
  args: {
246
296
  tableData: initialData,
247
- collapsibleSections: false,
297
+ collapsibleSections: true,
298
+ scrollOnOpen: true,
299
+ scrollTopOffset: 0,
248
300
  hideDetails: false,
249
301
  stickyHeaderTopOffset: 0,
250
302
  title: 'Title of the table',
@@ -262,6 +314,8 @@ const story = {
262
314
  export const TableStory = {
263
315
  render: ({
264
316
  collapsibleSections,
317
+ scrollOnOpen,
318
+ scrollTopOffset,
265
319
  tableData,
266
320
  hideColumns,
267
321
  hideDetails,
@@ -303,6 +357,8 @@ export const TableStory = {
303
357
  },
304
358
  }}
305
359
  collapsibleSections={collapsibleSections}
360
+ scrollOnOpen={scrollOnOpen}
361
+ scrollTopOffset={scrollTopOffset}
306
362
  tableData={tableData}
307
363
  hideColumns={hideColumns}
308
364
  hideDetails={hideDetails}
@@ -39,6 +39,8 @@ export interface TableProps {
39
39
  modalContentRenderer?: (content: ReactNode) => ReactNode;
40
40
  onModalOpen?: ModalFunction;
41
41
  onSelectionChanged?: (index: number) => void;
42
+ scrollOnOpen?: boolean;
43
+ scrollTopOffset?: number;
42
44
  stickyHeaderTopOffset?: number;
43
45
  tableData: TableData;
44
46
  textOverrides?: TextOverrides;
@@ -63,6 +65,8 @@ const Table = ({
63
65
  modalContentRenderer,
64
66
  onModalOpen,
65
67
  onSelectionChanged,
68
+ scrollOnOpen,
69
+ scrollTopOffset = 0,
66
70
  stickyHeaderTopOffset = 0,
67
71
  tableData,
68
72
  textOverrides: definedTextOverrides,
@@ -76,6 +80,7 @@ const Table = ({
76
80
  const [shouldHideDetails, setShouldHideDetails] = useState(true);
77
81
  const containerRef = useRef<HTMLDivElement | null>(null);
78
82
  const headerRef = useRef<HTMLDivElement | null>(null);
83
+ const stickyHeaderRef = useRef<HTMLDivElement | null>(null);
79
84
  const columnsLength = tableData[0].rows[0].length;
80
85
 
81
86
  useScrollSync(headerRef, containerRef, !isMobile);
@@ -154,6 +159,7 @@ const Table = ({
154
159
  </>
155
160
  ) : (
156
161
  <div
162
+ ref={stickyHeaderRef}
157
163
  aria-hidden
158
164
  className={styles.stickyHeader}
159
165
  style={{ top: `${stickyHeaderTopOffset}px` }}
@@ -179,6 +185,9 @@ const Table = ({
179
185
  title={title}
180
186
  className={className}
181
187
  collapsibleSections={collapsibleSections}
188
+ scrollOnOpen={scrollOnOpen}
189
+ scrollTopOffset={scrollTopOffset}
190
+ stickyHeaderRef={stickyHeaderRef}
182
191
  hideColumns={hideColumns}
183
192
  hideDetails={hideDetails}
184
193
  hideRows={hideRows}
@@ -4,9 +4,10 @@ import classNames from 'classnames';
4
4
  interface CollapsibleProps {
5
5
  children: ReactNode;
6
6
  isExpanded?: boolean;
7
+ onTransitionEnd?: () => void;
7
8
  }
8
9
 
9
- export const Collapsible = ({ children, isExpanded }: CollapsibleProps) => {
10
+ export const Collapsible = ({ children, isExpanded, onTransitionEnd }: CollapsibleProps) => {
10
11
  const [height, setHeight] = useState<number | undefined>();
11
12
 
12
13
  const observerRef = useRef<ResizeObserver | null>(null);
@@ -45,6 +46,7 @@ export const Collapsible = ({ children, isExpanded }: CollapsibleProps) => {
45
46
  style={{
46
47
  maxHeight: isExpanded ? height : '0px',
47
48
  }}
49
+ onTransitionEnd={isExpanded ? onTransitionEnd : undefined}
48
50
  >
49
51
  {children}
50
52
  </div>
@@ -1,4 +1,4 @@
1
- import { useState } from 'react';
1
+ import { MutableRefObject, useCallback, useRef, useState } from 'react';
2
2
  import { TableSection } from '../TableSection/TableSection';
3
3
  import { ChevronDownIcon, ChevronUpIcon } from '../../../icon';
4
4
  import { Card } from '../../../cards/card';
@@ -12,6 +12,9 @@ import { IconRenderer } from '../IconRenderer/IconRenderer';
12
12
  export interface TableContentsProps {
13
13
  className?: string;
14
14
  collapsibleSections?: boolean;
15
+ scrollOnOpen?: boolean;
16
+ scrollTopOffset?: number;
17
+ stickyHeaderRef?: MutableRefObject<HTMLDivElement | null>;
15
18
  tableData: TableData;
16
19
  hideColumns?: number[];
17
20
  hideDetails?: boolean;
@@ -27,6 +30,9 @@ export interface TableContentsProps {
27
30
  const TableContents = ({
28
31
  className,
29
32
  collapsibleSections,
33
+ scrollOnOpen,
34
+ scrollTopOffset = 0,
35
+ stickyHeaderRef,
30
36
  tableData,
31
37
  hideColumns = [],
32
38
  hideDetails,
@@ -39,14 +45,33 @@ const TableContents = ({
39
45
  imageComponent,
40
46
  }: TableContentsProps) => {
41
47
  const [isSectionOpen, setOpenSection] = useState<number | null>(null);
48
+ const lastToggledSection = useRef<number | null>(null);
49
+ const sectionRefs = useRef<Record<number, HTMLDivElement | null>>({});
42
50
  const firstHeadRow = tableData?.[0]?.rows?.[0];
43
51
  const tableWidth = isMobile ? `${firstHeadRow?.length * 50}%` : '';
44
52
  const handleToggleSection = (index: number) => {
53
+ lastToggledSection.current = isSectionOpen === index ? null : index;
45
54
  setOpenSection((currentSection) =>
46
55
  currentSection === index ? null : index
47
56
  );
48
57
  };
49
58
 
59
+ const handleScrollToSection = useCallback(
60
+ (index: number) => {
61
+ if (scrollOnOpen && lastToggledSection.current === index && sectionRefs.current[index]) {
62
+ const headerHeight =
63
+ stickyHeaderRef?.current?.getBoundingClientRect().height ?? 0;
64
+ const top =
65
+ sectionRefs.current[index]!.getBoundingClientRect().top +
66
+ window.scrollY -
67
+ scrollTopOffset -
68
+ headerHeight;
69
+ window.scrollTo({ top, behavior: 'smooth' });
70
+ }
71
+ },
72
+ [scrollOnOpen, scrollTopOffset, stickyHeaderRef]
73
+ );
74
+
50
75
  // Calculate global row offset for each section
51
76
  let globalRowOffset = 0;
52
77
 
@@ -69,7 +94,7 @@ const TableContents = ({
69
94
  .filter(localRowIndex => localRowIndex >= 0 && localRowIndex < rows.length);
70
95
 
71
96
  const result = (isFirstSection || isVisible) && (
72
- <div key={index}>
97
+ <div key={index} ref={(el) => { sectionRefs.current[index] = el; }}>
73
98
  {section?.title && (
74
99
  <div className={styles.cardWrapper}>
75
100
  <div className={classNames(styles.card, 'p0')}>
@@ -103,7 +128,10 @@ const TableContents = ({
103
128
  </div>
104
129
  )}
105
130
 
106
- <Collapsible isExpanded={isExpanded}>
131
+ <Collapsible
132
+ isExpanded={isExpanded}
133
+ onTransitionEnd={() => handleScrollToSection(index)}
134
+ >
107
135
  <TableSection
108
136
  className={classNames(className, 'mb24')}
109
137
  tableCellRows={