@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.30.2",
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
- width: 100% !important;
18
- #custom_autocomplete_dropdown-listbox{
19
- overflow-x:hidden;
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
- PropsWithChildren,
3
- ReactElement,
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
- StyledPopper,
25
- StyledTextField,
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
- }: PropsWithChildren<NsAutocompleteProps>):ReactElement => {
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 renderListBoxComponent = ({ children, ...rest }: ComponentBoxProps): React.JSX.Element => {
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
- <ul
88
- aria-labelledby={rest['aria-labelledby']}
89
- onMouseDown={rest.onMouseDown}
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
- openOnFocus
187
+ open
119
188
  id={id}
120
- disablePortal
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={renderOption as any}
128
- renderInput={renderInput}
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.