@axinom/mosaic-ui 0.42.0-rc.3 → 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.
- 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 +4 -4
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/components/FormElements/CustomTags/CustomTags.scss +12 -11
- package/src/components/FormStation/FormStation.stories.tsx +172 -0
- 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/src/styles/variables.scss +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axinom/mosaic-ui",
|
|
3
|
-
"version": "0.42.0-rc.
|
|
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.
|
|
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": "
|
|
108
|
+
"gitHead": "0a4a124c07abc12604ad50a1892c8ae6b4e49a15"
|
|
108
109
|
}
|
|
@@ -85,14 +85,13 @@
|
|
|
85
85
|
|
|
86
86
|
input,
|
|
87
87
|
.autoComplete > li {
|
|
88
|
-
padding:
|
|
88
|
+
padding: 15px;
|
|
89
89
|
color: var(--input-color, $input-color);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
.autoComplete {
|
|
93
93
|
display: grid;
|
|
94
94
|
grid-template-columns: 1fr;
|
|
95
|
-
grid-auto-rows: 25px;
|
|
96
95
|
align-items: center;
|
|
97
96
|
|
|
98
97
|
box-sizing: border-box;
|
|
@@ -101,25 +100,27 @@
|
|
|
101
100
|
position: absolute;
|
|
102
101
|
top: 36px;
|
|
103
102
|
left: 0;
|
|
104
|
-
right:
|
|
103
|
+
right: 0;
|
|
105
104
|
|
|
106
105
|
list-style-type: none;
|
|
107
106
|
z-index: 10;
|
|
108
107
|
|
|
109
|
-
border: 1px solid
|
|
108
|
+
border: 1px solid
|
|
109
|
+
var(--live-suggest-border-color, $live-suggest-border-color);
|
|
110
110
|
background-color: #ffffff;
|
|
111
111
|
|
|
112
|
+
li {
|
|
113
|
+
color: var(--live-suggest-text-color, $live-suggest-text-color);
|
|
114
|
+
}
|
|
115
|
+
|
|
112
116
|
li:hover {
|
|
113
|
-
color:
|
|
114
|
-
|
|
117
|
+
background-color: var(
|
|
118
|
+
--live-suggest-background-selected-color,
|
|
119
|
+
$live-suggest-background-selected-color
|
|
120
|
+
);
|
|
115
121
|
|
|
116
122
|
cursor: pointer;
|
|
117
123
|
}
|
|
118
|
-
|
|
119
|
-
li.selected {
|
|
120
|
-
color: #ffffff;
|
|
121
|
-
background-color: #1e90ff;
|
|
122
|
-
}
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
.plusButton {
|
|
@@ -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,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';
|