@axinom/mosaic-ui 0.42.0 → 0.43.0-rc.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/dist/components/Tabs/Tab/CustomTab.d.ts +3 -0
- package/dist/components/Tabs/Tab/CustomTab.d.ts.map +1 -0
- package/dist/components/Tabs/Tab/index.d.ts +2 -0
- package/dist/components/Tabs/Tab/index.d.ts.map +1 -0
- package/dist/components/Tabs/TabList/CustomTabList.d.ts +3 -0
- package/dist/components/Tabs/TabList/CustomTabList.d.ts.map +1 -0
- package/dist/components/Tabs/TabList/ScrollContainer/ScrollContainer.d.ts +3 -0
- package/dist/components/Tabs/TabList/ScrollContainer/ScrollContainer.d.ts.map +1 -0
- package/dist/components/Tabs/TabList/ScrollContainer/index.d.ts +2 -0
- package/dist/components/Tabs/TabList/ScrollContainer/index.d.ts.map +1 -0
- package/dist/components/Tabs/TabList/ScrollContainer/useScroll.d.ts +10 -0
- package/dist/components/Tabs/TabList/ScrollContainer/useScroll.d.ts.map +1 -0
- package/dist/components/Tabs/TabList/index.d.ts +2 -0
- package/dist/components/Tabs/TabList/index.d.ts.map +1 -0
- package/dist/components/Tabs/TabPanel/CustomTabPanel.d.ts +3 -0
- package/dist/components/Tabs/TabPanel/CustomTabPanel.d.ts.map +1 -0
- package/dist/components/Tabs/TabPanel/index.d.ts +2 -0
- package/dist/components/Tabs/TabPanel/index.d.ts.map +1 -0
- package/dist/components/Tabs/index.d.ts +5 -0
- package/dist/components/Tabs/index.d.ts.map +1 -0
- package/dist/index.es.js +1 -1
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/components/FormStation/FormStation.stories.tsx +172 -0
- package/src/components/List/ListRow/Renderers/ExternalLinkRenderer/ExternalLinkRenderer.tsx +1 -1
- package/src/components/Tabs/Tab/CustomTab.scss +42 -0
- package/src/components/Tabs/Tab/CustomTab.tsx +34 -0
- package/src/components/Tabs/Tab/index.ts +1 -0
- package/src/components/Tabs/TabList/CustomTabList.scss +7 -0
- package/src/components/Tabs/TabList/CustomTabList.tsx +15 -0
- package/src/components/Tabs/TabList/ScrollContainer/ScrollContainer.scss +34 -0
- package/src/components/Tabs/TabList/ScrollContainer/ScrollContainer.tsx +39 -0
- package/src/components/Tabs/TabList/ScrollContainer/index.ts +1 -0
- package/src/components/Tabs/TabList/ScrollContainer/useScroll.ts +114 -0
- package/src/components/Tabs/TabList/index.ts +1 -0
- package/src/components/Tabs/TabPanel/CustomTabPanel.scss +10 -0
- package/src/components/Tabs/TabPanel/CustomTabPanel.tsx +26 -0
- package/src/components/Tabs/TabPanel/index.ts +1 -0
- package/src/components/Tabs/Tabs.stories.tsx +108 -0
- package/src/components/Tabs/index.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axinom/mosaic-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0-rc.0",
|
|
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.
|
|
35
|
+
"@axinom/mosaic-core": "^0.4.16-rc.0",
|
|
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": "
|
|
108
|
+
"gitHead": "b1b5a9a050664bc07d31401fec2b0f58b5c88f4b"
|
|
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',
|
|
@@ -55,10 +55,10 @@ export const createExternalLinkRenderer = <T extends Data>(
|
|
|
55
55
|
rel="noopener noreferrer"
|
|
56
56
|
onClick={handleClick}
|
|
57
57
|
>
|
|
58
|
+
<p title={value}>{value}</p>
|
|
58
59
|
{showNewTabIcon && (
|
|
59
60
|
<Icons icon={IconName.External} className={classes.externalIcon} />
|
|
60
61
|
)}
|
|
61
|
-
<p title={value}>{value}</p>
|
|
62
62
|
</a>
|
|
63
63
|
);
|
|
64
64
|
};
|
|
@@ -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,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,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
|
+
};
|