@axinom/mosaic-ui 0.42.0-rc.4 → 0.42.0-rc.5

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 (37) hide show
  1. package/dist/components/Tabs/Tab/CustomTab.d.ts +3 -0
  2. package/dist/components/Tabs/Tab/CustomTab.d.ts.map +1 -0
  3. package/dist/components/Tabs/Tab/index.d.ts +2 -0
  4. package/dist/components/Tabs/Tab/index.d.ts.map +1 -0
  5. package/dist/components/Tabs/TabList/CustomTabList.d.ts +3 -0
  6. package/dist/components/Tabs/TabList/CustomTabList.d.ts.map +1 -0
  7. package/dist/components/Tabs/TabList/ScrollContainer/ScrollContainer.d.ts +3 -0
  8. package/dist/components/Tabs/TabList/ScrollContainer/ScrollContainer.d.ts.map +1 -0
  9. package/dist/components/Tabs/TabList/ScrollContainer/index.d.ts +2 -0
  10. package/dist/components/Tabs/TabList/ScrollContainer/index.d.ts.map +1 -0
  11. package/dist/components/Tabs/TabList/ScrollContainer/useScroll.d.ts +10 -0
  12. package/dist/components/Tabs/TabList/ScrollContainer/useScroll.d.ts.map +1 -0
  13. package/dist/components/Tabs/TabList/index.d.ts +2 -0
  14. package/dist/components/Tabs/TabList/index.d.ts.map +1 -0
  15. package/dist/components/Tabs/TabPanel/CustomTabPanel.d.ts +3 -0
  16. package/dist/components/Tabs/TabPanel/CustomTabPanel.d.ts.map +1 -0
  17. package/dist/components/Tabs/TabPanel/index.d.ts +2 -0
  18. package/dist/components/Tabs/TabPanel/index.d.ts.map +1 -0
  19. package/dist/components/Tabs/index.d.ts +5 -0
  20. package/dist/components/Tabs/index.d.ts.map +1 -0
  21. package/package.json +4 -3
  22. package/src/components/FormStation/FormStation.stories.tsx +172 -0
  23. package/src/components/Tabs/Tab/CustomTab.scss +42 -0
  24. package/src/components/Tabs/Tab/CustomTab.tsx +34 -0
  25. package/src/components/Tabs/Tab/index.ts +1 -0
  26. package/src/components/Tabs/TabList/CustomTabList.scss +7 -0
  27. package/src/components/Tabs/TabList/CustomTabList.tsx +15 -0
  28. package/src/components/Tabs/TabList/ScrollContainer/ScrollContainer.scss +34 -0
  29. package/src/components/Tabs/TabList/ScrollContainer/ScrollContainer.tsx +39 -0
  30. package/src/components/Tabs/TabList/ScrollContainer/index.ts +1 -0
  31. package/src/components/Tabs/TabList/ScrollContainer/useScroll.ts +114 -0
  32. package/src/components/Tabs/TabList/index.ts +1 -0
  33. package/src/components/Tabs/TabPanel/CustomTabPanel.scss +10 -0
  34. package/src/components/Tabs/TabPanel/CustomTabPanel.tsx +26 -0
  35. package/src/components/Tabs/TabPanel/index.ts +1 -0
  36. package/src/components/Tabs/Tabs.stories.tsx +108 -0
  37. package/src/components/Tabs/index.ts +4 -0
@@ -0,0 +1,3 @@
1
+ import { ReactTabsFunctionComponent, TabProps } from 'react-tabs';
2
+ export declare const CustomTab: ReactTabsFunctionComponent<TabProps>;
3
+ //# sourceMappingURL=CustomTab.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CustomTab.d.ts","sourceRoot":"","sources":["../../../../src/components/Tabs/Tab/CustomTab.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,0BAA0B,EAAO,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGvE,eAAO,MAAM,SAAS,EAAE,0BAA0B,CAAC,QAAQ,CA2B1D,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { CustomTab as Tab } from './CustomTab';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/Tabs/Tab/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,IAAI,GAAG,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { ReactTabsFunctionComponent, TabListProps } from 'react-tabs';
2
+ export declare const CustomTabList: ReactTabsFunctionComponent<TabListProps>;
3
+ //# sourceMappingURL=CustomTabList.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CustomTabList.d.ts","sourceRoot":"","sources":["../../../../src/components/Tabs/TabList/CustomTabList.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,0BAA0B,EAAW,YAAY,EAAE,MAAM,YAAY,CAAC;AAI/E,eAAO,MAAM,aAAa,EAAE,0BAA0B,CAAC,YAAY,CAOlE,CAAC"}
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ export declare const ScrollContainer: React.FC;
3
+ //# sourceMappingURL=ScrollContainer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScrollContainer.d.ts","sourceRoot":"","sources":["../../../../../src/components/Tabs/TabList/ScrollContainer/ScrollContainer.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAC;AAM1B,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EA+BnC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { ScrollContainer } from './ScrollContainer';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/Tabs/TabList/ScrollContainer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ export declare const useScroll: <T extends HTMLElement>() => {
3
+ scrollLeft: () => void;
4
+ scrollRight: () => void;
5
+ scrollRef: React.RefObject<T>;
6
+ showScroll: boolean;
7
+ enableScrollLeft: boolean;
8
+ enableScrollRight: boolean;
9
+ };
10
+ //# sourceMappingURL=useScroll.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useScroll.d.ts","sourceRoot":"","sources":["../../../../../src/components/Tabs/TabList/ScrollContainer/useScroll.ts"],"names":[],"mappings":"AACA,OAAO,KAA0D,MAAM,OAAO,CAAC;AAE/E,eAAO,MAAM,SAAS;gBACR,MAAM,IAAI;iBACT,MAAM,IAAI;;gBAEX,OAAO;sBACD,OAAO;uBACN,OAAO;CAwG3B,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { CustomTabList as TabList } from './CustomTabList';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/Tabs/TabList/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,IAAI,OAAO,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { ReactTabsFunctionComponent, TabPanelProps } from 'react-tabs';
2
+ export declare const CustomTabPanel: ReactTabsFunctionComponent<TabPanelProps>;
3
+ //# sourceMappingURL=CustomTabPanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CustomTabPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/Tabs/TabPanel/CustomTabPanel.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,0BAA0B,EAE1B,aAAa,EACd,MAAM,YAAY,CAAC;AAGpB,eAAO,MAAM,cAAc,EAAE,0BAA0B,CAAC,aAAa,CAcpE,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { CustomTabPanel as TabPanel } from './CustomTabPanel';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/Tabs/TabPanel/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,IAAI,QAAQ,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,5 @@
1
+ export { Tabs } from 'react-tabs';
2
+ export { Tab } from './Tab';
3
+ export { TabList } from './TabList';
4
+ export { TabPanel } from './TabPanel';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Tabs/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAClC,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAC5B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axinom/mosaic-ui",
3
- "version": "0.42.0-rc.4",
3
+ "version": "0.42.0-rc.5",
4
4
  "description": "UI components for building Axinom Mosaic applications",
5
5
  "author": "Axinom",
6
6
  "license": "PROPRIETARY",
@@ -32,7 +32,7 @@
32
32
  "build-storybook": "storybook build"
33
33
  },
34
34
  "dependencies": {
35
- "@axinom/mosaic-core": "^0.4.15-rc.4",
35
+ "@axinom/mosaic-core": "^0.4.15-rc.5",
36
36
  "@faker-js/faker": "^7.4.0",
37
37
  "@popperjs/core": "^2.11.8",
38
38
  "clsx": "^1.1.0",
@@ -43,6 +43,7 @@
43
43
  "react-content-loader": "^6.0.3",
44
44
  "react-imask": "^6.4.3",
45
45
  "react-popper": "^2.2.5",
46
+ "react-tabs": "4.3.0",
46
47
  "react-transition-group": "^4.3.0",
47
48
  "yup": "^0.32.11"
48
49
  },
@@ -104,5 +105,5 @@
104
105
  "publishConfig": {
105
106
  "access": "public"
106
107
  },
107
- "gitHead": "3cb3e16b29bd7f4c01eee49d551ce0a1e3a30d55"
108
+ "gitHead": "0a4a124c07abc12604ad50a1892c8ae6b4e49a15"
108
109
  }
@@ -24,6 +24,7 @@ import { CheckboxField } from '../FormElements/Checkbox/CheckboxField';
24
24
  import { DynamicDataListField } from '../FormElements/DynamicDataListControl/DynamicDataListField';
25
25
  import { MaskedSingleLineTextField } from '../FormElements/MaskedSingleLineText/MaskedSingleLineTextField';
26
26
  import { InfoPanel, Paragraph, Section } from '../InfoPanel';
27
+ import { Tab, TabList, TabPanel, Tabs } from '../Tabs';
27
28
  import { ErrorType } from '../models';
28
29
  import { Details, DetailsProps } from './Details/Details';
29
30
  import { ObjectSchemaDefinition } from './FormStation';
@@ -294,6 +295,177 @@ export const Extended: StoryObj<typeof Details> = (() => {
294
295
  };
295
296
  })();
296
297
 
298
+ export const TabbedContent: StoryObj<typeof Details> = (() => {
299
+ const listData = generateItemArray(4, (index) => ({
300
+ position: index + 1,
301
+ id: index,
302
+ locale: faker.random.locale(),
303
+ country: faker.address.country(),
304
+ }));
305
+ return {
306
+ args: {
307
+ titleProperty: 'title',
308
+ subtitle: 'Movies',
309
+ actions: generateActions(7),
310
+ validationSchema: Yup.object<ObjectSchemaDefinition<DetailsValues>>({
311
+ title: Yup.string().required().max(25).label('Title'),
312
+ genres: Yup.array<DetailsValues>()
313
+ .of(Yup.string())
314
+ .max(2)
315
+ .label('Genres'),
316
+ shortDescription: Yup.string().required().label('Short Description'),
317
+ cast: Yup.array<DetailsValues>().of(Yup.string()).min(1).label('Cast'),
318
+ }),
319
+ initialData: {
320
+ loading: false,
321
+ data: {
322
+ id: 12344567890,
323
+ title: 'My Movie',
324
+ publishState: 'PUBLISHED',
325
+ genres: [],
326
+ licenses: [],
327
+ ratings: 'PG-13',
328
+ shortDescription: 'Some short abstract...',
329
+ longDescription: '',
330
+ cast: ['Jane Doe', 'John Doe'],
331
+ released: '2020-04-03T00:00:00.000+00:00',
332
+ list: listData,
333
+ archived: false,
334
+ timestamp: '00:00:00.001',
335
+ },
336
+ },
337
+ children: (
338
+ <Tabs>
339
+ <TabList>
340
+ <Tab>Tab 1</Tab>
341
+ <Tab>Tab 2</Tab>
342
+ </TabList>
343
+ <TabPanel>
344
+ <div
345
+ style={{
346
+ display: 'grid',
347
+ gridAutoRows: 'max-content',
348
+ rowGap: '20px',
349
+ paddingTop: '20px',
350
+ paddingBottom: '20px',
351
+ }}
352
+ >
353
+ <Field name="id" label="Id" as={ReadOnlyField} />
354
+ <Field name="title" label="Title" as={SingleLineTextField} />
355
+ <Field
356
+ name="publishState"
357
+ label="Publish State"
358
+ as={ReadOnlyField}
359
+ />
360
+ <Field
361
+ name="genres"
362
+ label="Genre(s)"
363
+ tagsOptions={['Crime', 'Drama', 'Thriller']}
364
+ as={TagsField}
365
+ />
366
+
367
+ <Field
368
+ name="ratings"
369
+ label="Age Rating"
370
+ options={[
371
+ { value: 'PG', label: 'Parental Guidance Suggested (PG)' },
372
+ {
373
+ value: 'PG-13',
374
+ label: 'Parents Strongly Cautioned (PG-13)',
375
+ },
376
+ { value: 'R', label: 'Restricted (R)' },
377
+ ]}
378
+ as={SelectField}
379
+ />
380
+
381
+ <Field
382
+ name="shortDescription"
383
+ label="Short Description"
384
+ placeholder="Enter a short description..."
385
+ as={SingleLineTextField}
386
+ />
387
+ <Field
388
+ name="longDescription"
389
+ label="Long Description"
390
+ placeholder="Enter a description..."
391
+ as={SingleLineTextField}
392
+ />
393
+ <Field name="cast" label="Cast" as={CustomTagsField} />
394
+ <Field name="released" label="Released" as={DateTimeTextField} />
395
+ <Field
396
+ name="password"
397
+ label="Password"
398
+ as={SingleLineTextField}
399
+ type="password"
400
+ />
401
+
402
+ <Field
403
+ name="licenses"
404
+ label="License Countries"
405
+ tagsOptions={[
406
+ { value: 'DE', key: 'Germany' },
407
+ { value: 'EE', key: 'Estonia' },
408
+ { value: 'LK', key: 'Sri Lanka' },
409
+ ]}
410
+ as={TagsField}
411
+ displayKey="key"
412
+ valueKey="value"
413
+ />
414
+ <Field
415
+ name="list"
416
+ label="Subtitles"
417
+ columns={[
418
+ {
419
+ propertyName: 'id',
420
+ label: 'Id',
421
+ size: '50px',
422
+ },
423
+ {
424
+ propertyName: 'locale',
425
+ label: 'Locale',
426
+ dataEntryRender: createInputRenderer({
427
+ placeholder: 'Enter Locale',
428
+ }),
429
+ },
430
+ {
431
+ propertyName: 'country',
432
+ label: 'Country',
433
+ dataEntryRender: createSelectRenderer({
434
+ options: [
435
+ { value: 'Country 10', label: 'Country 10' },
436
+ { value: 'Country 11', label: 'Country 11' },
437
+ { value: 'Country 12', label: 'Country 12' },
438
+ ],
439
+ placeholder: 'Enter Country',
440
+ }),
441
+ },
442
+ ]}
443
+ data={listData}
444
+ positionPropertyName={'position'}
445
+ allowReordering={true}
446
+ allowRowDragging={true}
447
+ allowNewData={true}
448
+ as={DynamicDataListField}
449
+ />
450
+ <Field name="archived" label="Set Archived" as={CheckboxField} />
451
+ <Field
452
+ name="timestamp"
453
+ label="Timestamp"
454
+ mask="00:00:00.000"
455
+ as={MaskedSingleLineTextField}
456
+ />
457
+ </div>
458
+ </TabPanel>
459
+ <TabPanel>
460
+ <h2>Tab 2</h2>
461
+ <p>{faker.lorem.paragraph(20)}</p>
462
+ </TabPanel>
463
+ </Tabs>
464
+ ),
465
+ },
466
+ };
467
+ })();
468
+
297
469
  const errorGroups = createGroups({
298
470
  'Storybook (Loading Error)': [
299
471
  'loadingError',
@@ -0,0 +1,42 @@
1
+ @import '../../../styles/common.scss';
2
+
3
+ .tab {
4
+ @include boxSizing;
5
+
6
+ display: grid;
7
+ height: 49px;
8
+ min-width: 180px;
9
+ background-color: $blue;
10
+ color: white;
11
+ font-size: 16px;
12
+ border: 1px solid $blue;
13
+ border-bottom: none;
14
+
15
+ padding: 0 15px;
16
+
17
+ cursor: pointer;
18
+
19
+ &.selected {
20
+ background: white;
21
+ color: $dark-gray;
22
+ border: 1px solid $light-gray;
23
+ border-bottom: none;
24
+ }
25
+
26
+ &.disabled {
27
+ background-color: $dark-gray;
28
+ cursor: default;
29
+ }
30
+
31
+ &:focus {
32
+ outline: none;
33
+ }
34
+ }
35
+
36
+ .content {
37
+ display: grid;
38
+ align-items: center;
39
+ height: 100%;
40
+ width: 100%;
41
+ justify-content: center;
42
+ }
@@ -0,0 +1,34 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { ReactTabsFunctionComponent, Tab, TabProps } from 'react-tabs';
3
+ import classes from './CustomTab.scss';
4
+
5
+ export const CustomTab: ReactTabsFunctionComponent<TabProps> = ({
6
+ children,
7
+ ...otherProps
8
+ }) => {
9
+ const ref = useRef<HTMLDivElement>(null);
10
+
11
+ useEffect(() => {
12
+ if (ref.current && ref.current.parentElement && otherProps.selected) {
13
+ ref.current.parentElement.scrollIntoView({
14
+ behavior: 'smooth',
15
+ block: 'nearest',
16
+ });
17
+ }
18
+ }, [otherProps.selected]);
19
+
20
+ return (
21
+ <Tab
22
+ {...otherProps}
23
+ className={classes.tab}
24
+ selectedClassName={classes.selected}
25
+ data-test-id="tab"
26
+ >
27
+ <div className={classes.content} ref={ref}>
28
+ {children}
29
+ </div>
30
+ </Tab>
31
+ );
32
+ };
33
+
34
+ CustomTab.tabsRole = 'Tab';
@@ -0,0 +1 @@
1
+ export { CustomTab as Tab } from './CustomTab';
@@ -0,0 +1,7 @@
1
+ @import '../../../styles/common.scss';
2
+
3
+ .tablist {
4
+ border: none;
5
+ margin: 0;
6
+ padding: 0;
7
+ }
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ import { ReactTabsFunctionComponent, TabList, TabListProps } from 'react-tabs';
3
+ import classes from './CustomTabList.scss';
4
+ import { ScrollContainer } from './ScrollContainer';
5
+
6
+ export const CustomTabList: ReactTabsFunctionComponent<TabListProps> = ({
7
+ children,
8
+ ...otherProps
9
+ }) => (
10
+ <TabList {...otherProps} className={classes.tablist}>
11
+ <ScrollContainer>{children}</ScrollContainer>
12
+ </TabList>
13
+ );
14
+
15
+ CustomTabList.tabsRole = 'TabList';
@@ -0,0 +1,34 @@
1
+ @import '../../../../styles/common.scss';
2
+
3
+ .tablistWrapper {
4
+ overflow-x: auto;
5
+ overflow-y: hidden;
6
+ display: grid;
7
+ width: 100%;
8
+ white-space: nowrap;
9
+ grid-auto-flow: column;
10
+ grid-auto-columns: minmax(max-content, auto);
11
+ gap: 2px;
12
+
13
+ -ms-overflow-style: none; /* IE and Edge */
14
+ scrollbar-width: none; /* Firefox */
15
+ }
16
+
17
+ /* Hide scrollbar for Chrome, Safari and Opera */
18
+ .tablistWrapper::-webkit-scrollbar {
19
+ display: none;
20
+ }
21
+
22
+ .container {
23
+ display: grid;
24
+ // grid-template-columns: 50px auto 50px;
25
+
26
+ &.scroll {
27
+ grid-template-columns: 50px auto 50px;
28
+ gap: 2px;
29
+ }
30
+ }
31
+
32
+ .hide {
33
+ display: none;
34
+ }
@@ -0,0 +1,39 @@
1
+ import clsx from 'clsx';
2
+ import React from 'react';
3
+ import { Button } from '../../../Buttons';
4
+ import { IconName } from '../../../Icons';
5
+ import classes from './ScrollContainer.scss';
6
+ import { useScroll } from './useScroll';
7
+
8
+ export const ScrollContainer: React.FC = ({ children }) => {
9
+ const {
10
+ scrollRef,
11
+ showScroll,
12
+ scrollLeft,
13
+ scrollRight,
14
+ enableScrollLeft,
15
+ enableScrollRight,
16
+ } = useScroll<HTMLDivElement>();
17
+
18
+ return (
19
+ <div className={clsx(classes.container, { [classes.scroll]: showScroll })}>
20
+ <Button
21
+ icon={IconName.ChevronLeft}
22
+ onButtonClicked={scrollLeft}
23
+ className={clsx({ [classes.hide]: !showScroll })}
24
+ disabled={!enableScrollLeft}
25
+ />
26
+
27
+ <div className={classes.tablistWrapper} ref={scrollRef}>
28
+ {children}
29
+ </div>
30
+
31
+ <Button
32
+ icon={IconName.ChevronRight}
33
+ onButtonClicked={scrollRight}
34
+ className={clsx({ [classes.hide]: !showScroll })}
35
+ disabled={!enableScrollRight}
36
+ />
37
+ </div>
38
+ );
39
+ };
@@ -0,0 +1 @@
1
+ export { ScrollContainer } from './ScrollContainer';
@@ -0,0 +1,114 @@
1
+ import debounce from 'lodash/debounce';
2
+ import React, { useCallback, useEffect, useLayoutEffect, useRef } from 'react';
3
+
4
+ export const useScroll = <T extends HTMLElement>(): {
5
+ scrollLeft: () => void;
6
+ scrollRight: () => void;
7
+ scrollRef: React.RefObject<T>;
8
+ showScroll: boolean;
9
+ enableScrollLeft: boolean;
10
+ enableScrollRight: boolean;
11
+ } => {
12
+ const scrollRef = useRef<T>(null);
13
+
14
+ const scrollLeft = useCallback(() => {
15
+ if (scrollRef.current) {
16
+ scrollRef.current.scrollBy({
17
+ left: -220,
18
+ behavior: 'smooth',
19
+ });
20
+ }
21
+ }, []);
22
+
23
+ const scrollRight = useCallback(() => {
24
+ if (scrollRef.current) {
25
+ scrollRef.current.scrollBy({
26
+ left: 220,
27
+ behavior: 'smooth',
28
+ });
29
+ }
30
+ }, []);
31
+
32
+ const [showScroll, setShowScroll] = React.useState<boolean>(false);
33
+ const [enableScrollLeft, setEnableScrollLeft] =
34
+ React.useState<boolean>(false);
35
+ const [enableScrollRight, setEnableScrollRight] =
36
+ React.useState<boolean>(false);
37
+
38
+ const updateScroll = useCallback(() => {
39
+ if (scrollRef.current) {
40
+ const { scrollWidth, clientWidth } = scrollRef.current;
41
+
42
+ if (showScroll) {
43
+ // take into account the width of the scroll buttons
44
+ setShowScroll(scrollWidth > clientWidth + 100);
45
+ } else {
46
+ setShowScroll(scrollWidth > clientWidth);
47
+ }
48
+ }
49
+ }, [showScroll]);
50
+
51
+ const updateScrollButtons = useCallback(() => {
52
+ if (scrollRef.current) {
53
+ const { scrollLeft, scrollWidth, clientWidth } = scrollRef.current;
54
+
55
+ setEnableScrollLeft(scrollLeft > 0);
56
+ setEnableScrollRight(scrollLeft + clientWidth < scrollWidth);
57
+ }
58
+ }, []);
59
+
60
+ // Omitting the dependency array here since this effect needs to run at every render
61
+ // to handle the case where the components are dynamically added/removed
62
+ useEffect(updateScroll);
63
+ useEffect(updateScrollButtons);
64
+
65
+ useLayoutEffect(() => {
66
+ // debounce is needed to avoid re-rendering everything on every resize event
67
+ const debouncedUpdateScroll = debounce(updateScroll, 50);
68
+
69
+ // add window resize event listener on mount
70
+ window.addEventListener('resize', debouncedUpdateScroll);
71
+
72
+ debouncedUpdateScroll();
73
+
74
+ const scrollContainer = scrollRef.current;
75
+ const debouncedUpdateScrollButtons = debounce(updateScrollButtons, 50);
76
+
77
+ scrollContainer?.addEventListener('scroll', debouncedUpdateScrollButtons);
78
+
79
+ updateScrollButtons();
80
+
81
+ const transformScroll = (event: WheelEvent): void => {
82
+ if (event.deltaY !== 0 && scrollContainer) {
83
+ scrollContainer.scrollLeft += event.deltaY;
84
+ }
85
+ };
86
+
87
+ scrollContainer?.addEventListener('wheel', transformScroll, {
88
+ passive: false, // added for scrolling on Safari
89
+ });
90
+
91
+ // clear all event listeners on unmount
92
+ return () => {
93
+ debouncedUpdateScroll.cancel();
94
+ window.removeEventListener('resize', debouncedUpdateScroll);
95
+
96
+ debouncedUpdateScrollButtons.cancel();
97
+ scrollContainer?.removeEventListener(
98
+ 'scroll',
99
+ debouncedUpdateScrollButtons,
100
+ );
101
+
102
+ scrollContainer?.removeEventListener('wheel', transformScroll);
103
+ };
104
+ }, [showScroll, updateScroll, updateScrollButtons]);
105
+
106
+ return {
107
+ scrollLeft,
108
+ scrollRight,
109
+ scrollRef,
110
+ showScroll,
111
+ enableScrollLeft,
112
+ enableScrollRight,
113
+ };
114
+ };
@@ -0,0 +1 @@
1
+ export { CustomTabList as TabList } from './CustomTabList';
@@ -0,0 +1,10 @@
1
+ @import '../../../styles/common.scss';
2
+
3
+ .tabpanel {
4
+ border-bottom: 1px solid $light-gray;
5
+ display: none;
6
+
7
+ &.selected {
8
+ display: block;
9
+ }
10
+ }
@@ -0,0 +1,26 @@
1
+ import clsx from 'clsx';
2
+ import React from 'react';
3
+ import {
4
+ ReactTabsFunctionComponent,
5
+ TabPanel,
6
+ TabPanelProps,
7
+ } from 'react-tabs';
8
+ import classes from './CustomTabPanel.scss';
9
+
10
+ export const CustomTabPanel: ReactTabsFunctionComponent<TabPanelProps> = ({
11
+ children,
12
+ className,
13
+ selectedClassName,
14
+ ...otherProps
15
+ }) => (
16
+ <TabPanel
17
+ {...otherProps}
18
+ className={clsx(className, classes.tabpanel)}
19
+ selectedClassName={clsx(selectedClassName, classes.selected)}
20
+ data-test-id="tab"
21
+ >
22
+ {children}
23
+ </TabPanel>
24
+ );
25
+
26
+ CustomTabPanel.tabsRole = 'TabPanel';
@@ -0,0 +1 @@
1
+ export { CustomTabPanel as TabPanel } from './CustomTabPanel';
@@ -0,0 +1,108 @@
1
+ /* eslint-disable react-hooks/rules-of-hooks */
2
+ import { faker } from '@faker-js/faker';
3
+ import { useArgs } from '@storybook/preview-api';
4
+ import { Meta, StoryObj } from '@storybook/react';
5
+ import React, { useMemo } from 'react';
6
+ import { TabsProps } from 'react-tabs';
7
+ import { Tab, TabList, TabPanel, Tabs } from '.';
8
+ import { createGroups } from '../../helpers/storybook';
9
+
10
+ const groups = createGroups({
11
+ Storybook: ['amount'],
12
+ });
13
+
14
+ type TabsStoryComponentProps = React.FC<
15
+ typeof Tabs & {
16
+ amount: number;
17
+ selectedIndex?: number;
18
+ }
19
+ >;
20
+
21
+ const meta: Meta<TabsStoryComponentProps> = {
22
+ title: 'Other Components/Tabs',
23
+ component: Tabs,
24
+ argTypes: {
25
+ ...groups,
26
+ amount: {
27
+ ...groups.amount,
28
+ description: '<b>[Storybook only]</b> The amount of tabs to render.',
29
+ type: 'number',
30
+ control: { type: 'number', min: 0, max: 30 },
31
+ },
32
+ children: {
33
+ table: {
34
+ disable: true,
35
+ },
36
+ },
37
+ },
38
+ args: {
39
+ amount: 3,
40
+ },
41
+ };
42
+
43
+ const generateItems = (count: number): [JSX.Element[], JSX.Element[]] => {
44
+ const tabs = [];
45
+ const panels = [];
46
+
47
+ for (let i = 0; i < count; i++) {
48
+ const title = faker.random.words(faker.datatype.number({ min: 1, max: 3 }));
49
+ tabs.push(<Tab key={i}>{title}</Tab>);
50
+ panels.push(
51
+ <TabPanel key={i}>
52
+ <h2>{title}</h2>
53
+ <p>{faker.lorem.paragraph(20)}</p>
54
+ </TabPanel>,
55
+ );
56
+ }
57
+
58
+ return [tabs, panels];
59
+ };
60
+
61
+ export default meta;
62
+
63
+ export const Default: StoryObj<TabsStoryComponentProps> = {
64
+ render: (props) => {
65
+ const [tabs, panels] = generateItems(props.amount);
66
+
67
+ return (
68
+ <Tabs>
69
+ <TabList>{tabs}</TabList>
70
+ {panels}
71
+ </Tabs>
72
+ );
73
+ },
74
+ };
75
+
76
+ export const Controlled: StoryObj<TabsStoryComponentProps> = {
77
+ argTypes: {
78
+ ...meta.argTypes,
79
+ selectedIndex: {
80
+ ...groups.amount,
81
+ description: '<b>[Storybook only]</b> Index of the selected Tab.',
82
+ type: 'number',
83
+ control: { type: 'number', min: 0, max: 30 },
84
+ },
85
+ },
86
+ args: {
87
+ ...meta.args,
88
+ selectedIndex: 0,
89
+ },
90
+ render: (props) => {
91
+ const [tabs, panels] = useMemo(
92
+ () => generateItems(props.amount),
93
+ [props.amount],
94
+ );
95
+ const [{ selectedIndex }, updateArgs] = useArgs();
96
+
97
+ const onSelect: TabsProps['onSelect'] = (index) => {
98
+ updateArgs({ selectedIndex: index });
99
+ };
100
+
101
+ return (
102
+ <Tabs selectedIndex={selectedIndex} onSelect={onSelect}>
103
+ <TabList>{tabs}</TabList>
104
+ {panels}
105
+ </Tabs>
106
+ );
107
+ },
108
+ };
@@ -0,0 +1,4 @@
1
+ export { Tabs } from 'react-tabs';
2
+ export { Tab } from './Tab';
3
+ export { TabList } from './TabList';
4
+ export { TabPanel } from './TabPanel';