@nuskin/foundation-ui-components 1.30.2 → 1.31.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": "@nuskin/foundation-ui-components",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.31.0",
|
|
4
4
|
"description": "It is a foundation library written in react, that provides common components to all UI pages",
|
|
5
5
|
"main": "src/common/index.ts",
|
|
6
6
|
"repository": {
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"react-dom": "^18.2.0",
|
|
58
58
|
"react-material-ui-carousel": "^3.4.2",
|
|
59
59
|
"react-slick": "^0.29.0",
|
|
60
|
+
"react-window": "^1.8.10",
|
|
60
61
|
"slick-carousel": "^1.8.1",
|
|
61
62
|
"typescript": "^5.4.2"
|
|
62
63
|
},
|
|
@@ -86,6 +87,7 @@
|
|
|
86
87
|
"@types/ramda": "^0.29.7",
|
|
87
88
|
"@types/react-slick": "^0.23.11",
|
|
88
89
|
"@types/react-test-renderer": "^18.0.5",
|
|
90
|
+
"@types/react-window": "^1.8.8",
|
|
89
91
|
"babel-jest": "^29.7.0",
|
|
90
92
|
"babel-loader": "^9.1.3",
|
|
91
93
|
"babel-plugin-emotion": "^11.0.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { styled, ColorUtils } from '@nuskin/foundation-theme';
|
|
2
2
|
import { NsInputField } from '../input';
|
|
3
|
-
import { Popper } from '@mui/material';
|
|
3
|
+
import { Popper, autocompleteClasses } from '@mui/material';
|
|
4
4
|
const { getGenomeColor } = ColorUtils;
|
|
5
5
|
|
|
6
6
|
export const StyledTextField = styled(NsInputField)`
|
|
@@ -13,9 +13,12 @@ export const StyledTextField = styled(NsInputField)`
|
|
|
13
13
|
}
|
|
14
14
|
`;
|
|
15
15
|
|
|
16
|
-
export const StyledPopper = styled(Popper)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
export const StyledPopper = styled(Popper)({
|
|
17
|
+
[`& .${autocompleteClasses.listbox}`]: {
|
|
18
|
+
boxSizing: 'border-box',
|
|
19
|
+
'& ul': {
|
|
20
|
+
padding: 0,
|
|
21
|
+
margin: 0,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -1,36 +1,19 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
useEffect,
|
|
5
|
-
useRef,
|
|
6
|
-
useState,
|
|
7
|
-
} from 'react';
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
Autocomplete,
|
|
11
|
-
AutocompleteRenderInputParams,
|
|
12
|
-
Popper,
|
|
13
|
-
} from '@mui/material';
|
|
1
|
+
import React, { PropsWithChildren, ReactElement, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Autocomplete, AutocompleteRenderInputParams, ListSubheader, Popper, useMediaQuery, useTheme } from '@mui/material';
|
|
14
4
|
|
|
15
5
|
import { LineLoader } from './helper/LineLoader';
|
|
16
6
|
import { RecentClearAll } from './helper/RecentClearAll';
|
|
17
|
-
import {
|
|
18
|
-
renderGroupBy,
|
|
19
|
-
renderGroupName,
|
|
20
|
-
renderOptionLabel,
|
|
21
|
-
} from './helper/renderGroupName';
|
|
7
|
+
import { renderGroupBy, renderOptionLabel } from './helper/renderGroupName';
|
|
22
8
|
import { SeeMoreButton } from './helper/SeeMoreButton';
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
} from './NsAutocomplete.styled';
|
|
27
|
-
import {
|
|
28
|
-
ComponentBoxProps,
|
|
29
|
-
NsAutocompleteProps,
|
|
30
|
-
OptionsProps,
|
|
31
|
-
} from './types.d';
|
|
9
|
+
import { StyledPopper, StyledTextField } from './NsAutocomplete.styled';
|
|
10
|
+
import { ComponentBoxProps, NsAutocompleteProps, OptionsProps } from './types.d';
|
|
11
|
+
import { VariableSizeList, ListChildComponentProps } from 'react-window';
|
|
32
12
|
|
|
13
|
+
const LISTBOX_PADDING = 8;
|
|
33
14
|
const noop = () => {};
|
|
15
|
+
const OuterElementContext = React.createContext({});
|
|
16
|
+
|
|
34
17
|
const NsAutocomplete = ({
|
|
35
18
|
id = 'custom_autocomplete_dropdown',
|
|
36
19
|
options,
|
|
@@ -50,9 +33,10 @@ const NsAutocomplete = ({
|
|
|
50
33
|
value,
|
|
51
34
|
isFilterEnabled = true,
|
|
52
35
|
fullWidth,
|
|
53
|
-
|
|
36
|
+
onKeypress
|
|
37
|
+
}: PropsWithChildren<NsAutocompleteProps>): ReactElement => {
|
|
38
|
+
const listboxRef = useRef<any | null>(null);
|
|
54
39
|
const [scrollTopPosition, setScrollTopPosition] = useState(0);
|
|
55
|
-
const listboxRef = useRef<HTMLUListElement | null>(null);
|
|
56
40
|
|
|
57
41
|
useEffect(() => {
|
|
58
42
|
if (listboxRef.current && options.length > 0) {
|
|
@@ -60,6 +44,13 @@ const NsAutocomplete = ({
|
|
|
60
44
|
}
|
|
61
45
|
}, [options, scrollTopPosition]);
|
|
62
46
|
|
|
47
|
+
const OuterElementType = React.forwardRef((props: any, ref) => {
|
|
48
|
+
const outerProps = React.useContext(OuterElementContext);
|
|
49
|
+
return <ul ref={ref} {...props} {...outerProps} />;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
OuterElementType.displayName = 'OuterElementType';
|
|
53
|
+
|
|
63
54
|
const renderInput = (params: AutocompleteRenderInputParams) => (
|
|
64
55
|
<StyledTextField
|
|
65
56
|
{...(params as any)}
|
|
@@ -82,54 +73,132 @@ const NsAutocomplete = ({
|
|
|
82
73
|
</li>
|
|
83
74
|
);
|
|
84
75
|
|
|
85
|
-
const
|
|
76
|
+
const renderRow = (props: ListChildComponentProps) => {
|
|
77
|
+
const { data, index, style } = props;
|
|
78
|
+
const dataSet = data[index];
|
|
79
|
+
const option = dataSet?.[1];
|
|
80
|
+
const inlineStyle = {
|
|
81
|
+
top: (style.top as number) + LISTBOX_PADDING,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (Object.hasOwn(dataSet, 'group')) {
|
|
85
|
+
return (
|
|
86
|
+
<ListSubheader key={dataSet.key} component="div" style={inlineStyle}>
|
|
87
|
+
{dataSet.group}
|
|
88
|
+
</ListSubheader>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
86
92
|
return (
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
role={rest.role}
|
|
91
|
-
id={rest.id}
|
|
92
|
-
className={rest.className}
|
|
93
|
-
ref={listboxRef}
|
|
94
|
-
>
|
|
95
|
-
<RecentClearAll
|
|
96
|
-
show={clear?.show ?? false}
|
|
97
|
-
text={clear?.text ?? 'Clear All'}
|
|
98
|
-
onClick={clear?.onClear ?? noop}
|
|
99
|
-
testId="autocomplete-clear-id"
|
|
100
|
-
recentSearchText={clear?.recentSearchText ?? 'Your Recent Searches'}
|
|
101
|
-
/>
|
|
102
|
-
{children}
|
|
103
|
-
<SeeMoreButton
|
|
104
|
-
onSeeMore={seeMore?.onSeeMore}
|
|
105
|
-
show={seeMore?.show}
|
|
106
|
-
text={seeMore?.text}
|
|
107
|
-
options={options}
|
|
108
|
-
setScrollTopPosition={setScrollTopPosition}
|
|
109
|
-
listboxRef={listboxRef}
|
|
110
|
-
/>
|
|
111
|
-
</ul>
|
|
93
|
+
<div component="div" {...dataSet[0]} style={inlineStyle}>
|
|
94
|
+
{renderOption(props, option)}
|
|
95
|
+
</div>
|
|
112
96
|
);
|
|
113
97
|
};
|
|
98
|
+
|
|
99
|
+
const useResetCache = (data: any) => {
|
|
100
|
+
const ref = React.useRef<VariableSizeList>(null);
|
|
101
|
+
React.useEffect(() => {
|
|
102
|
+
if (ref.current != null) {
|
|
103
|
+
ref.current.resetAfterIndex(0, true);
|
|
104
|
+
}
|
|
105
|
+
}, [data]);
|
|
106
|
+
return ref;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const ListboxComponent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLElement>>(
|
|
110
|
+
function ListboxComponent(props, ref) {
|
|
111
|
+
const { children, ...other } = props;
|
|
112
|
+
const itemData: React.ReactElement[] = [];
|
|
113
|
+
(children as React.ReactElement[]).forEach(
|
|
114
|
+
(item: React.ReactElement & { children?: React.ReactElement[] }) => {
|
|
115
|
+
itemData.push(item);
|
|
116
|
+
itemData.push(...(item.children || []));
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const theme = useTheme();
|
|
121
|
+
const smUp = useMediaQuery(theme.breakpoints.up('sm'), {
|
|
122
|
+
noSsr: true,
|
|
123
|
+
});
|
|
124
|
+
const itemCount = itemData.length;
|
|
125
|
+
const itemSize = smUp ? 36 : 48;
|
|
126
|
+
|
|
127
|
+
const getChildSize = (child: React.ReactElement) => {
|
|
128
|
+
if (Object.hasOwn(child, 'group')) {
|
|
129
|
+
return 48;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return itemSize;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const getHeight = () => {
|
|
136
|
+
if (itemCount > 8) {
|
|
137
|
+
return 8 * itemSize;
|
|
138
|
+
}
|
|
139
|
+
return itemData.map(getChildSize).reduce((a, b) => a + b, 0);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const gridRef = useResetCache(itemCount);
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<>
|
|
146
|
+
<RecentClearAll
|
|
147
|
+
show={clear?.show ?? false}
|
|
148
|
+
text={clear?.text ?? 'Clear All'}
|
|
149
|
+
onClick={clear?.onClear ?? noop}
|
|
150
|
+
testId="autocomplete-clear-id"
|
|
151
|
+
recentSearchText={clear?.recentSearchText ?? 'Your Recent Searches'}
|
|
152
|
+
/>
|
|
153
|
+
<div ref={ref}>
|
|
154
|
+
<OuterElementContext.Provider value={other}>
|
|
155
|
+
<VariableSizeList
|
|
156
|
+
itemData={itemData}
|
|
157
|
+
height={getHeight() + 2 * LISTBOX_PADDING}
|
|
158
|
+
width="100%"
|
|
159
|
+
ref={gridRef}
|
|
160
|
+
outerElementType={OuterElementType}
|
|
161
|
+
innerElementType="ul"
|
|
162
|
+
itemSize={(index) => getChildSize(itemData[index])}
|
|
163
|
+
overscanCount={5}
|
|
164
|
+
itemCount={itemCount}
|
|
165
|
+
>
|
|
166
|
+
{renderRow}
|
|
167
|
+
</VariableSizeList>
|
|
168
|
+
</OuterElementContext.Provider>
|
|
169
|
+
</div>
|
|
170
|
+
<SeeMoreButton
|
|
171
|
+
onSeeMore={seeMore?.onSeeMore}
|
|
172
|
+
show={seeMore?.show}
|
|
173
|
+
text={seeMore?.text}
|
|
174
|
+
options={options}
|
|
175
|
+
setScrollTopPosition={setScrollTopPosition}
|
|
176
|
+
listboxRef={listboxRef}
|
|
177
|
+
/>
|
|
178
|
+
</>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
);
|
|
182
|
+
|
|
114
183
|
return (
|
|
115
184
|
<>
|
|
116
185
|
<LineLoader show={loading ?? false} />
|
|
117
186
|
<Autocomplete
|
|
118
|
-
|
|
187
|
+
open
|
|
119
188
|
id={id}
|
|
120
|
-
|
|
189
|
+
disableListWrap
|
|
121
190
|
className={className}
|
|
122
|
-
options={options}
|
|
123
191
|
loading={loading}
|
|
124
192
|
noOptionsText={noOptionsText}
|
|
193
|
+
PopperComponent={fullWidth ? StyledPopper : Popper}
|
|
194
|
+
ListboxComponent={ListboxComponent}
|
|
195
|
+
options={options}
|
|
125
196
|
groupBy={renderGroupBy}
|
|
126
197
|
getOptionLabel={renderOptionLabel}
|
|
127
|
-
renderOption={
|
|
128
|
-
|
|
129
|
-
PopperComponent={fullWidth ? StyledPopper : Popper} // TODO:https://code.tls.nuskin.io/ns-am/content-foundation/nuskin-app/-/jobs/1355817
|
|
130
|
-
ListboxComponent={renderListBoxComponent}
|
|
131
|
-
renderGroup={renderGroupName}
|
|
198
|
+
renderOption={(props, option, state) => [props, option, state.index] as React.ReactNode}
|
|
199
|
+
renderGroup={(params) => params as any}
|
|
132
200
|
onChange={onChange}
|
|
201
|
+
renderInput={renderInput}
|
|
133
202
|
onInputChange={onInputChange}
|
|
134
203
|
value={value}
|
|
135
204
|
onFocus={onFocus ?? noop}
|
|
@@ -140,6 +209,7 @@ const NsAutocomplete = ({
|
|
|
140
209
|
: (options) => options
|
|
141
210
|
}
|
|
142
211
|
freeSolo
|
|
212
|
+
onKeyDown={onKeypress}
|
|
143
213
|
/>
|
|
144
214
|
</>
|
|
145
215
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { ReactElement } from 'react';
|
|
1
|
+
import React, { ReactElement, SyntheticEvent } from 'react';
|
|
2
2
|
|
|
3
3
|
import { withCenterDecorator } from '@/stories/decorators/withCenterDecorator';
|
|
4
4
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
@@ -65,6 +65,7 @@ export default {
|
|
|
65
65
|
iconEnd: (<NsIcon name="roundedClose" size="small" colorOverride="#F5F5F5" />) as ReactElement,
|
|
66
66
|
placeholder: 'Search',
|
|
67
67
|
fullWidth: false,
|
|
68
|
+
onKeypress: (e: SyntheticEvent) => console.log(e),
|
|
68
69
|
},
|
|
69
70
|
} as MetaStory;
|
|
70
71
|
|
|
@@ -17,6 +17,7 @@ export type NsAutocompleteProps = {
|
|
|
17
17
|
reason Can be: "input" (user input), "reset" (programmatic change), "clear".
|
|
18
18
|
*/
|
|
19
19
|
onInputChange(event: SyntheticEvent, value: string, reason: string): void;
|
|
20
|
+
onKeypress?:(event:SyntheticEvent) => void;
|
|
20
21
|
/*
|
|
21
22
|
event The event source of the callback.
|
|
22
23
|
value The new value of the component.
|