@popsure/dirty-swan 0.66.19 → 0.66.21

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 (63) hide show
  1. package/dist/cjs/index.js +352 -306
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/cjs/lib/components/table/Table.d.ts +2 -1
  4. package/dist/cjs/lib/components/table/Table.stories.d.ts +9 -1
  5. package/dist/cjs/lib/components/table/components/TableContents/TableContents.d.ts +2 -1
  6. package/dist/cjs/lib/components/tabs/Tabs.d.ts +12 -0
  7. package/dist/cjs/lib/components/tabs/Tabs.stories.d.ts +45 -0
  8. package/dist/cjs/lib/index.d.ts +3 -2
  9. package/dist/esm/components/icon/icons/CircleSelected.js +2 -2
  10. package/dist/esm/components/icon/icons/CircleSelected.js.map +1 -1
  11. package/dist/esm/components/icon/icons/CircleUnselected.js +2 -2
  12. package/dist/esm/components/icon/icons/CircleUnselected.js.map +1 -1
  13. package/dist/esm/components/icon/icons.stories.js +1 -1
  14. package/dist/esm/components/icon/index.stories.js +1 -1
  15. package/dist/esm/components/table/Table.js +27 -10
  16. package/dist/esm/components/table/Table.js.map +1 -1
  17. package/dist/esm/components/table/Table.stories.js +86 -1
  18. package/dist/esm/components/table/Table.stories.js.map +1 -1
  19. package/dist/esm/components/table/Table.test.js +3 -0
  20. package/dist/esm/components/table/Table.test.js.map +1 -1
  21. package/dist/esm/components/table/components/TableCell/ButtonCell/ButtonCell.js +9 -8
  22. package/dist/esm/components/table/components/TableCell/ButtonCell/ButtonCell.js.map +1 -1
  23. package/dist/esm/components/table/components/TableCell/ButtonCell/ButtonCell.stories.js +4 -2
  24. package/dist/esm/components/table/components/TableCell/ButtonCell/ButtonCell.stories.js.map +1 -1
  25. package/dist/esm/components/table/components/TableCell/TableCell.js +2 -0
  26. package/dist/esm/components/table/components/TableCell/TableCell.js.map +1 -1
  27. package/dist/esm/components/table/components/TableCell/TableCell.test.js +2 -0
  28. package/dist/esm/components/table/components/TableCell/TableCell.test.js.map +1 -1
  29. package/dist/esm/components/table/components/TableContents/TableContents.js +5 -3
  30. package/dist/esm/components/table/components/TableContents/TableContents.js.map +1 -1
  31. package/dist/esm/components/table/components/TableContents/TableContents.test.js +15 -0
  32. package/dist/esm/components/table/components/TableContents/TableContents.test.js.map +1 -1
  33. package/dist/esm/components/table/components/TableSection/TableSection.js +2 -0
  34. package/dist/esm/components/table/components/TableSection/TableSection.js.map +1 -1
  35. package/dist/esm/components/table/components/TableSection/TableSection.test.js +2 -0
  36. package/dist/esm/components/table/components/TableSection/TableSection.test.js.map +1 -1
  37. package/dist/esm/components/tabs/Tabs.js +40 -0
  38. package/dist/esm/components/tabs/Tabs.js.map +1 -0
  39. package/dist/esm/components/tabs/Tabs.stories.js +77 -0
  40. package/dist/esm/components/tabs/Tabs.stories.js.map +1 -0
  41. package/dist/esm/{index-D41W71Hh.js → index-D7G3EuxU.js} +5 -5
  42. package/dist/esm/{index-D41W71Hh.js.map → index-D7G3EuxU.js.map} +1 -1
  43. package/dist/esm/index.js +1 -0
  44. package/dist/esm/index.js.map +1 -1
  45. package/dist/esm/lib/components/table/Table.d.ts +2 -1
  46. package/dist/esm/lib/components/table/Table.stories.d.ts +9 -1
  47. package/dist/esm/lib/components/table/components/TableContents/TableContents.d.ts +2 -1
  48. package/dist/esm/lib/components/tabs/Tabs.d.ts +12 -0
  49. package/dist/esm/lib/components/tabs/Tabs.stories.d.ts +45 -0
  50. package/dist/esm/lib/index.d.ts +3 -2
  51. package/dist/esm/util/images/index.stories.js +2 -0
  52. package/dist/esm/util/images/index.stories.js.map +1 -1
  53. package/package.json +1 -1
  54. package/src/lib/components/table/Table.stories.tsx +116 -0
  55. package/src/lib/components/table/Table.tsx +61 -24
  56. package/src/lib/components/table/components/TableCell/ButtonCell/ButtonCell.module.scss +27 -21
  57. package/src/lib/components/table/components/TableCell/ButtonCell/ButtonCell.tsx +19 -16
  58. package/src/lib/components/table/components/TableContents/TableContents.test.tsx +21 -0
  59. package/src/lib/components/table/components/TableContents/TableContents.tsx +3 -1
  60. package/src/lib/components/tabs/Tabs.module.scss +26 -0
  61. package/src/lib/components/tabs/Tabs.stories.tsx +94 -0
  62. package/src/lib/components/tabs/Tabs.tsx +65 -0
  63. package/src/lib/index.tsx +4 -0
@@ -20,6 +20,7 @@ import {
20
20
  TableCellData,
21
21
  TableData,
22
22
  } from './types';
23
+ import { Tabs, TabItem } from '../tabs/Tabs';
23
24
 
24
25
  export type { TableData } from './types';
25
26
 
@@ -49,6 +50,7 @@ export interface TableProps {
49
50
  hideTableNavigation?: boolean;
50
51
  hideStickyHeader?: boolean;
51
52
  showSelectedColumn?: boolean;
53
+ mobileNavigationMode?: 'arrows' | 'tabs';
52
54
  }
53
55
 
54
56
  const defaultTextOverrides = {
@@ -77,6 +79,7 @@ const Table = ({
77
79
  hideTableNavigation = false,
78
80
  hideStickyHeader = false,
79
81
  showSelectedColumn = false,
82
+ mobileNavigationMode = 'arrows',
80
83
  }: TableProps) => {
81
84
  const textOverrides = { ...defaultTextOverrides, ...definedTextOverrides };
82
85
  const isMobile = useMediaQuery('BELOW_MOBILE');
@@ -89,6 +92,8 @@ const Table = ({
89
92
 
90
93
  useScrollSync(headerRef, containerRef, !isMobile);
91
94
 
95
+ const useTabs = isMobile && mobileNavigationMode === 'tabs';
96
+
92
97
  const { activeSection, navigateTable, setActiveSection } = useTableNavigation(
93
98
  {
94
99
  enabled: isMobile,
@@ -97,11 +102,25 @@ const Table = ({
97
102
  }
98
103
  );
99
104
 
105
+ const headerRow = tableData?.[0]?.rows?.[0];
100
106
  const titleCell = {
101
107
  text: '',
102
- ...(tableData?.[0]?.rows?.[0]?.[0] || {}),
108
+ ...(headerRow?.[0] || {}),
103
109
  };
104
- const currentActiveSection = tableData?.[0]?.rows?.[0]?.[activeSection];
110
+ const currentActiveSection = headerRow?.[activeSection];
111
+
112
+ const mobileTabs: TabItem[] = useTabs
113
+ ? headerRow
114
+ ?.slice(1)
115
+ .map((cell, index) => {
116
+ let label = '';
117
+ if (!cell.type) label = String(cell.text || '');
118
+ else if (cell.type === 'CTA') label = String(cell.title || '');
119
+ else if (cell.type === 'BUTTON') label = String(cell.buttonCaption || '');
120
+ return { id: String(index), label };
121
+ }) ?? []
122
+ : [];
123
+
105
124
  const currentActiveSectionReplacements =
106
125
  (currentActiveSection?.cellId &&
107
126
  cellReplacements?.[currentActiveSection.cellId]) ||
@@ -127,7 +146,7 @@ const Table = ({
127
146
  <div className={classNames(styles.wrapper, 'bg-white')}>
128
147
  {isMobile ? (
129
148
  <>
130
- {titleCell?.text && (
149
+ {!useTabs && titleCell?.text && (
131
150
  <TableCell
132
151
  {...titleCell}
133
152
  align="left"
@@ -138,27 +157,44 @@ const Table = ({
138
157
  )}
139
158
 
140
159
  {!hideTableNavigation && currentActiveSection && (
141
- <TableControls
142
- activeSection={activeSection}
143
- columnsLength={columnsLength}
144
- navigateTable={navigateTable}
145
- stickyHeaderTopOffset={stickyHeaderTopOffset}
146
- >
147
- <TableCell
148
- {...(isBaseCell(currentActiveSection)
149
- ? {
150
- openModal: (body: ReactNode) =>
151
- handleOpenModal({
152
- body,
153
- title: currentActiveSection?.text,
154
- }),
155
- }
156
- : {})}
157
- {...activeCellProps}
158
- imageComponent={imageComponent}
159
- isNavigation
160
- />
161
- </TableControls>
160
+ useTabs ? (
161
+ <div
162
+ className={classNames(
163
+ 'bg-white',
164
+ styles.stickyHeader
165
+ )}
166
+ style={{ top: `${stickyHeaderTopOffset}px` }}
167
+ >
168
+ <Tabs
169
+ tabs={mobileTabs}
170
+ activeIndex={activeSection - 1}
171
+ onTabChange={(index) => setActiveSection(index)}
172
+ className="mb16"
173
+ />
174
+ </div>
175
+ ) : (
176
+ <TableControls
177
+ activeSection={activeSection}
178
+ columnsLength={columnsLength}
179
+ navigateTable={navigateTable}
180
+ stickyHeaderTopOffset={stickyHeaderTopOffset}
181
+ >
182
+ <TableCell
183
+ {...(isBaseCell(currentActiveSection)
184
+ ? {
185
+ openModal: (body: ReactNode) =>
186
+ handleOpenModal({
187
+ body,
188
+ title: currentActiveSection?.text,
189
+ }),
190
+ }
191
+ : {})}
192
+ {...activeCellProps}
193
+ imageComponent={imageComponent}
194
+ isNavigation
195
+ />
196
+ </TableControls>
197
+ )
162
198
  )}
163
199
  </>
164
200
  ) : (
@@ -203,6 +239,7 @@ const Table = ({
203
239
  cellReplacements={cellReplacements}
204
240
  imageComponent={imageComponent}
205
241
  selectedColumn={showSelectedColumn ? activeSection : undefined}
242
+ showHeader={useTabs}
206
243
  />
207
244
  </div>
208
245
 
@@ -2,21 +2,28 @@
2
2
  @use "../../../../../scss/public/colors" as *;
3
3
 
4
4
  .buttonCell {
5
+ display: flex;
6
+ align-items: center;
7
+ justify-content: space-between;
8
+ width: calc(100% + 40px);
9
+ margin-left: -20px;
10
+ margin-right: -20px;
11
+ padding: 16px 24px;
12
+ border-radius: 12px;
13
+ border: 1px solid $ds-neutral-300;
14
+ background-color: transparent;
5
15
  color: $ds-neutral-900;
6
- border: 1px solid $ds-neutral-400;
16
+ cursor: pointer;
17
+ transition: border-color 0.3s ease-in-out, background-color 0.3s ease-in-out;
18
+ text-align: left;
7
19
 
8
- transition: all 0.3s ease-in-out;
9
-
10
- &:disabled {
11
- border: none;
12
- background-color: $ds-neutral-100;
13
- color: $ds-neutral-900;
14
- opacity: 1;
15
- cursor: default;
20
+ &:hover:not(:disabled):not(.selected) {
21
+ background-color: $ds-orange-50;
22
+ }
16
23
 
17
- &:hover {
18
- background-color: $ds-neutral-100;
19
- }
24
+ &:focus-visible {
25
+ outline: 1px solid $ds-neutral-900;
26
+ outline-offset: 0;
20
27
  }
21
28
 
22
29
  @include p-size-mobile {
@@ -24,15 +31,14 @@
24
31
  }
25
32
  }
26
33
 
27
- .withoutPrice {
28
- height: 64px;
29
- }
30
-
31
- .withPrice {
32
- height: 75px;
34
+ .selected {
35
+ border: 1px solid $ds-neutral-900;
36
+ background-color: $ds-orange-50;
33
37
  }
34
38
 
35
- .selected {
36
- transition: all 0.3 ease-in-out;
37
- border: 2px solid $ds-neutral-900;
39
+ .disabled {
40
+ background-color: $ds-neutral-100;
41
+ cursor: default;
42
+ opacity: 1;
43
+ pointer-events: none;
38
44
  }
@@ -1,7 +1,7 @@
1
1
  import classNames from 'classnames';
2
2
 
3
3
  import styles from './ButtonCell.module.scss';
4
- import { Button } from '../../../../button';
4
+ import { CircleSelectedIcon, CircleUnselectedIcon } from '../../../../icon';
5
5
  import { ReactNode } from 'react';
6
6
 
7
7
  export type ButtonCellProps = {
@@ -27,29 +27,32 @@ export const ButtonCell = ({
27
27
  }: ButtonCellProps) => {
28
28
  return (
29
29
  <div
30
- className={
31
- classNames(
32
- "w100 d-flex fd-column ai-start jc-center gap8",
33
- className,
34
- )
35
- }
30
+ className={classNames(
31
+ "w100 d-flex fd-column ai-start jc-center gap8",
32
+ className,
33
+ )}
36
34
  data-cy={dataCy}
37
35
  data-testid={dataTestId}
38
36
  >
39
- <Button
40
- className={classNames('w100 wmx5 d-flex fd-column', styles.buttonCell, {
37
+ <button
38
+ className={classNames('w100 wmx5', styles.buttonCell, {
41
39
  [styles.selected]: isSelected,
42
- [styles.withoutPrice]: !price,
43
- [styles.withPrice]: !!price,
40
+ [styles.disabled]: disabled,
44
41
  })}
45
- variant="filledWhite"
46
- type="submit"
42
+ type="button"
47
43
  onClick={onClick}
48
44
  disabled={disabled}
49
45
  >
50
- {buttonCaption}
51
- {price && <span className="p-p">{price}</span>}
52
- </Button>
46
+ <div className="d-flex fd-column ai-start">
47
+ <span className="p-p fw-bold">{buttonCaption}</span>
48
+ {price && <span className="p-p">{price}</span>}
49
+ </div>
50
+ {!disabled && (isSelected ? (
51
+ <CircleSelectedIcon size={16} />
52
+ ) : (
53
+ <CircleUnselectedIcon size={16} />
54
+ ))}
55
+ </button>
53
56
  </div>
54
57
  );
55
58
  };
@@ -73,4 +73,25 @@ describe('TableContents', () => {
73
73
  expect(screen.queryByText('Section 2')).not.toBeInTheDocument();
74
74
  expect(screen.queryByText('Item 2.1.1')).not.toBeInTheDocument();
75
75
  });
76
+
77
+ it('shows the header row only in the first section when showHeader is set', () => {
78
+ const { container } = render(
79
+ <TableContents showHeader tableData={mockData} title="Table" />
80
+ );
81
+
82
+ const tableHeaders = container.querySelectorAll('thead');
83
+ expect(tableHeaders).toHaveLength(2);
84
+ expect(tableHeaders[0]).not.toHaveClass('sr-only');
85
+ expect(tableHeaders[1]).toHaveClass('sr-only');
86
+ });
87
+
88
+ it('keeps every header row hidden when showHeader is not set', () => {
89
+ const { container } = render(
90
+ <TableContents tableData={mockData} title="Table" />
91
+ );
92
+
93
+ container.querySelectorAll('thead').forEach((tableHeader) => {
94
+ expect(tableHeader).toHaveClass('sr-only');
95
+ });
96
+ });
76
97
  });
@@ -25,6 +25,7 @@ export interface TableContentsProps {
25
25
  cellReplacements?: CellReplacements;
26
26
  imageComponent?: (args: any) => JSX.Element;
27
27
  selectedColumn?: number;
28
+ showHeader?: boolean;
28
29
  }
29
30
 
30
31
  const TableContents = ({
@@ -44,6 +45,7 @@ const TableContents = ({
44
45
  cellReplacements,
45
46
  imageComponent,
46
47
  selectedColumn,
48
+ showHeader = false,
47
49
  }: TableContentsProps) => {
48
50
  const [isSectionOpen, setOpenSection] = useState<number | null>(null);
49
51
  const lastToggledSection = useRef<number | null>(null);
@@ -147,7 +149,7 @@ const TableContents = ({
147
149
  }
148
150
  hideColumns={hideColumns}
149
151
  hideRows={sectionHideRows}
150
- hideHeader
152
+ hideHeader={!showHeader || !isFirstSection}
151
153
  openModal={openModal}
152
154
  title={`${title}${
153
155
  section?.title ? ` - ${section.title}` : ''
@@ -0,0 +1,26 @@
1
+ @use '../../scss/public/colors' as *;
2
+ @use '../../scss/public/font-weight' as *;
3
+
4
+ .tabs {
5
+ display: flex;
6
+ column-gap: 24px;
7
+ }
8
+
9
+ .tab {
10
+ border: none;
11
+ border-bottom: 2px solid transparent;
12
+ font-weight: $p-font-weight-bold;
13
+ padding: 0 0 4px;
14
+ color: $ds-neutral-500;
15
+
16
+ &:hover, &Active {
17
+ color: $ds-neutral-900;
18
+ border-bottom-color: $ds-neutral-900;
19
+ }
20
+
21
+ &:focus-visible {
22
+ outline: 1px solid $ds-neutral-900;
23
+ outline-offset: 2px;
24
+ border-radius: 2px;
25
+ }
26
+ }
@@ -0,0 +1,94 @@
1
+ import { useState } from 'react';
2
+ import { Tabs, TabsProps } from './Tabs';
3
+
4
+ const story = {
5
+ title: 'JSX/Tabs',
6
+ component: Tabs,
7
+ argTypes: {
8
+ tabs: {
9
+ description: 'Array of tab items with id and label',
10
+ },
11
+ activeIndex: {
12
+ description: 'The currently active tab index',
13
+ },
14
+ onTabChange: {
15
+ action: true,
16
+ table: {
17
+ category: 'Callbacks',
18
+ },
19
+ },
20
+ className: {
21
+ description: 'Optional CSS class to apply to the tabs container',
22
+ },
23
+ },
24
+ args: {
25
+ tabs: [
26
+ { id: 'basic', label: 'Basic' },
27
+ { id: 'standard', label: 'Standard' },
28
+ { id: 'premium', label: 'Premium' },
29
+ ],
30
+ activeIndex: 0,
31
+ },
32
+ parameters: {
33
+ componentSubtitle:
34
+ 'A horizontal tab bar for switching between content sections.',
35
+ },
36
+ };
37
+
38
+ export const Default = {
39
+ render: ({ tabs, activeIndex, onTabChange }: TabsProps) => {
40
+ const [index, setIndex] = useState(activeIndex);
41
+
42
+ const handleChange = (newIndex: number) => {
43
+ onTabChange?.(newIndex);
44
+ setIndex(newIndex);
45
+ };
46
+
47
+ return <Tabs tabs={tabs} activeIndex={index} onTabChange={handleChange} />;
48
+ },
49
+
50
+ name: 'Default',
51
+ };
52
+
53
+ export const TwoTabs = {
54
+ render: () => {
55
+ const [index, setIndex] = useState(0);
56
+
57
+ return (
58
+ <Tabs
59
+ tabs={[
60
+ { id: 'monthly', label: 'Monthly' },
61
+ { id: 'yearly', label: 'Yearly' },
62
+ ]}
63
+ activeIndex={index}
64
+ onTabChange={setIndex}
65
+ />
66
+ );
67
+ },
68
+
69
+ name: 'Two Tabs',
70
+ };
71
+
72
+ export const ManyTabs = {
73
+ render: () => {
74
+ const [index, setIndex] = useState(0);
75
+
76
+ return (
77
+ <Tabs
78
+ tabs={[
79
+ { id: 'overview', label: 'Overview' },
80
+ { id: 'coverage', label: 'Coverage' },
81
+ { id: 'pricing', label: 'Pricing' },
82
+ { id: 'reviews', label: 'Reviews' },
83
+ { id: 'faq', label: 'FAQ' },
84
+ ]}
85
+ activeIndex={index}
86
+ onTabChange={setIndex}
87
+ />
88
+ );
89
+ },
90
+
91
+ name: 'Many Tabs',
92
+ };
93
+
94
+ export default story;
@@ -0,0 +1,65 @@
1
+ import classNames from 'classnames';
2
+ import { KeyboardEvent, useCallback, useRef } from 'react';
3
+
4
+ import styles from './Tabs.module.scss';
5
+
6
+ export interface TabItem {
7
+ id: string;
8
+ label: string;
9
+ }
10
+
11
+ export interface TabsProps {
12
+ tabs: TabItem[];
13
+ activeIndex: number;
14
+ onTabChange: (index: number) => void;
15
+ className?: string;
16
+ }
17
+
18
+ const Tabs = ({ tabs, activeIndex, onTabChange, className }: TabsProps) => {
19
+ const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);
20
+
21
+ const setTabRef = useCallback(
22
+ (index: number) => (el: HTMLButtonElement | null) => {
23
+ tabRefs.current[index] = el;
24
+ },
25
+ []
26
+ );
27
+
28
+ const handleKeyDown = (e: KeyboardEvent, index: number) => {
29
+ const count = tabs.length;
30
+ let nextIndex: number | null = null;
31
+
32
+ if (e.key === 'ArrowRight') nextIndex = (index + 1) % count;
33
+ if (e.key === 'ArrowLeft') nextIndex = (index - 1 + count) % count;
34
+
35
+ if (nextIndex !== null) {
36
+ e.preventDefault();
37
+ onTabChange(nextIndex);
38
+ tabRefs.current[nextIndex]?.focus({ preventScroll: true });
39
+ }
40
+ };
41
+
42
+ return (
43
+ <div className={classNames(styles.tabs, className)} role="tablist">
44
+ {tabs.map((tab, index) => (
45
+ <button
46
+ ref={setTabRef(index)}
47
+ key={tab.id}
48
+ type="button"
49
+ role="tab"
50
+ aria-selected={activeIndex === index}
51
+ tabIndex={activeIndex === index ? 0 : -1}
52
+ className={classNames('p-p bg-transparent c-pointer', styles.tab, {
53
+ [styles.tabActive]: activeIndex === index,
54
+ })}
55
+ onClick={() => onTabChange(index)}
56
+ onKeyDown={(e) => handleKeyDown(e, index)}
57
+ >
58
+ {tab.label}
59
+ </button>
60
+ ))}
61
+ </div>
62
+ );
63
+ };
64
+
65
+ export { Tabs };
package/src/lib/index.tsx CHANGED
@@ -54,6 +54,7 @@ import { Toaster, toast } from './components/toast';
54
54
  import { IconWrapperProps } from './components/icon/IconWrapper';
55
55
  import { Accordion, AccordionProps } from './components/accordion';
56
56
  import { Table, TableData, TableProps } from './components/table/Table';
57
+ import { Tabs, TabsProps, TabItem } from './components/tabs/Tabs';
57
58
  import { useEscapeKey } from './hooks/useEscapeKey';
58
59
  import { useFocusWithin } from './hooks/useFocusWithin';
59
60
  import { useMediaQuery } from './hooks/useMediaQuery';
@@ -107,6 +108,7 @@ export {
107
108
  illustrations,
108
109
  Spinner,
109
110
  Table,
111
+ Tabs,
110
112
  Toggle,
111
113
  Toaster,
112
114
  toast,
@@ -139,6 +141,8 @@ export type {
139
141
  UploadStatus,
140
142
  CardProps,
141
143
  IconWrapperProps,
144
+ TabItem,
145
+ TabsProps,
142
146
  TableData,
143
147
  TableProps,
144
148
  };