naria-ui 0.1.37 → 0.1.38

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.
@@ -1,54 +1,143 @@
1
- import {FC, useEffect, useState} from "react";
2
- import AngleDown from "../../../src/assets/icons/angle-down.svg?react";
3
- import Close from '../../../src/assets/icons/close.svg?react';
1
+ import {FC, useEffect, useRef, useState} from "react";
2
+ import AngleDown from "../../../assets/icons/angle-down.svg?react";
3
+ import Close from '../../../assets/icons/close.svg?react';
4
+ import Search from '../../../assets/icons/search.svg?react';
4
5
  import {useWidth} from "../../../hooks/use-width";
5
6
  import Loading from "../../../shared/loading/Loading";
6
7
  import './select.scss';
7
8
  import {addNavigation, onHashChanges, removeNavigation} from "../../utils/navigator";
9
+ import useClickOutside from "../../../hooks/click-outside";
10
+
11
+ interface Pagination {
12
+ page?: number;
13
+ pageLabel?: string;
14
+ size?: number;
15
+ sizeLabel?: string;
16
+ }
17
+
18
+ interface SDS extends Pagination {
19
+ isLoading?: boolean;
20
+ }
8
21
 
9
22
  export interface props {
10
- list?: any[];
23
+ options?: any[];
11
24
  label?: string;
12
25
  title: string;
13
26
  value?: string;
14
- api?: any;
15
- maxHeight?: string;
16
- theme?: "secondary";
27
+ api?: string;
17
28
  hasError?: string | null;
18
- size?: "md" | "sm";
19
- onSelectChange?: any;
20
29
  selected?: any;
30
+ placeholder?: string;
31
+ disabled?: boolean;
32
+ pagination?: Pagination;
33
+ optionFilterLabel?: string;
34
+ hasSearch?: boolean;
35
+ onSelectChange?: any;
21
36
  }
22
37
 
23
38
  export const Select: FC<props> = ({
24
- list, label, hasError,
25
- title, value, api,
26
- maxHeight = "max-h-64", theme = "secondary", size = "md",
27
- onSelectChange,
28
- selected
39
+ options,
40
+ label,
41
+ hasError,
42
+ title,
43
+ value,
44
+ api,
45
+ selected,
46
+ placeholder,
47
+ disabled = false,
48
+ pagination,
49
+ optionFilterLabel,
50
+ hasSearch = false,
51
+ onSelectChange
29
52
  }) => {
53
+ let isSubscribed = true;
30
54
  const getDeviceWidth = useWidth();
55
+
56
+ const isHashChanged = onHashChanges('#select');
31
57
  const [isShow, setIsShow] = useState(false);
32
58
  const [isLoading, setIsLoading] = useState(true);
33
- const [localSelected, setLocalSelected] = useState(null);
34
- const [localList, setLocalList] = useState(null);
59
+ const [localSelected, setLocalSelected] = useState<string | null>(null);
60
+ const [localOptions, setLocalOptions] = useState(null);
61
+ const [searchTerm, setSearchTerm] = useState("");
62
+ const [localPagination, setLocalPagination] = useState<SDS>({
63
+ page: 1,
64
+ pageLabel: 'page',
65
+ size: 20,
66
+ sizeLabel: 'size',
67
+ isLoading: false
68
+ });
69
+ const localApi = useRef<string | undefined>(undefined);
70
+ const wrapperRef = useRef(undefined);
71
+ const handlerRef = useRef(undefined);
72
+
73
+ const getData = async () => {
74
+ if (localApi?.current) {
75
+ try {
76
+ const response = await fetch(localApi?.current);
77
+ if (!response.ok) {
78
+ throw new Error(`Response status: ${response.status}`);
79
+ }
80
+ return await response.json();
81
+ } catch (error) {
82
+ console.error(error.message);
83
+ }
84
+ }
85
+
86
+ }
87
+
88
+ useEffect(() => {
89
+ return () => {
90
+ isSubscribed = false;
91
+ };
92
+ }, [])
35
93
  useEffect(() => {
36
- if (api) {
94
+ if (api?.length) {
95
+ localApi.current = api;
96
+ if (localApi.current.includes(pagination?.pageLabel || 'page')) {
97
+ const url = new URL(api);
98
+ url.searchParams.set(pagination?.pageLabel || 'page', (pagination?.page || localPagination.page).toString());
99
+ url.searchParams.set(pagination?.sizeLabel || 'size', (pagination?.size || localPagination.size).toString());
100
+ localApi.current = url.href;
101
+ }
37
102
  setIsLoading(true);
38
- api().then((res) => {
39
- setIsLoading(false);
40
- setLocalList(res?.data?.message ? [] : res.data);
103
+ getData().then((res) => {
104
+ if (isSubscribed) {
105
+ setIsLoading(false);
106
+ setLocalOptions(res);
107
+ }
41
108
  })
42
109
  }
43
- }, [api]);
44
- useEffect(() => {
45
- if (getDeviceWidth < 1024) {
110
+ }, [api, pagination]);
46
111
 
112
+ useEffect(() => {
113
+ if (localPagination.isLoading) {
114
+ if (localApi.current.includes(pagination?.pageLabel || 'page')) {
115
+ const url = new URL(localApi.current);
116
+ url.searchParams.set(pagination?.pageLabel || 'page', (localPagination.page).toString());
117
+ localApi.current = url.href;
118
+ }
119
+ getData().then((res) => {
120
+ if (isSubscribed) {
121
+ setIsLoading(false);
122
+ setLocalPagination({
123
+ ...localPagination,
124
+ isLoading: false
125
+ })
126
+ setLocalOptions([
127
+ ...localOptions,
128
+ ...res
129
+ ]);
130
+ }
131
+ })
132
+ }
133
+ }, [localPagination]);
134
+ useEffect(() => {
135
+ if (getDeviceWidth < 768) {
47
136
  if (isShow) {
48
137
  addNavigation('select');
49
138
  document.body.style.overflow = 'hidden';
139
+ handlerRef.current?.focus();
50
140
  } else {
51
-
52
141
  if (window.location.hash && !document.referrer.includes('#')) {
53
142
  removeNavigation();
54
143
  }
@@ -58,31 +147,70 @@ export const Select: FC<props> = ({
58
147
  }, [isShow]);
59
148
 
60
149
  useEffect(() => {
61
- if (label?.length) {
62
- setLocalSelected(localList?.find(item => item[label] === selected));
63
- } else {
64
- setLocalSelected(selected);
150
+ if (selected && options?.length) {
151
+ if (options?.find(item => item[label] === selected)) {
152
+ setLocalSelected(options?.find(item => item[label] === selected));
153
+ } else {
154
+ setLocalSelected(selected);
155
+ }
156
+ }
157
+ }, [selected]);
158
+
159
+ useEffect(() => {
160
+ if (localSelected) {
161
+ if (hasSearch) {
162
+ if (value?.length && localOptions?.find(item => item[label] === localSelected[label])) {
163
+ setSearchTerm(localOptions?.find(item => item[label] === localSelected[label])[value] || '');
164
+ } else {
165
+ setSearchTerm(localSelected);
166
+ }
167
+ }
65
168
  }
66
- }, [selected, localList]);
169
+ }, [localSelected]);
67
170
 
68
171
  useEffect(() => {
69
- if (list?.length) {
70
- setLocalList(list)
172
+ if (isHashChanged) {
173
+ setIsShow(false)
71
174
  }
72
- }, [list]);
175
+ }, [isHashChanged])
176
+
177
+
178
+ useEffect(() => {
179
+ if (options?.length) {
180
+ setLocalOptions(options)
181
+ }
182
+ }, [options]);
73
183
 
74
184
  const onToggle = () => {
185
+ if (hasSearch) {
186
+ setLocalOptions(options)
187
+ }
75
188
  setIsShow(prevState => !prevState);
76
189
  }
77
- const onToggleEvent = (e) => {
78
- if (e?.target?.className.toString()?.includes('backdrop-select')) {
79
- onToggle()
190
+ const onClose = () => {
191
+ if (hasSearch) {
192
+ setLocalOptions(options)
193
+ if (typeof localSelected === "string") {
194
+ setSearchTerm(localSelected);
195
+ } else {
196
+ setSearchTerm(localSelected ? localSelected[value] : "")
197
+ }
80
198
  }
199
+ setIsShow(false);
81
200
  }
82
201
  const onSelect = (item) => {
202
+ if (hasSearch) {
203
+ if (value?.length) {
204
+ setSearchTerm(item[value]);
205
+ } else {
206
+ setSearchTerm(item);
207
+ }
208
+ }
83
209
  setLocalSelected(item);
84
210
  onToggle();
85
- onSelectChange(item);
211
+ if (onSelectChange) {
212
+ onSelectChange(item);
213
+ }
86
214
  }
87
215
 
88
216
  const getActiveClass = (item) => {
@@ -96,68 +224,149 @@ export const Select: FC<props> = ({
96
224
  return "bg-grey-100"
97
225
  }
98
226
  }
227
+ const onScroll = (e) => {
228
+ if (localOptions?.length && !localPagination.isLoading) {
229
+ const bottom = e.target.offsetHeight + e.target.scrollTop >= e.target.scrollHeight - 100;
230
+
231
+ if (bottom) {
232
+ setLocalPagination({
233
+ page: localPagination.page + 1,
234
+ isLoading: true
235
+ })
236
+ }
237
+ }
238
+
239
+ }
240
+ const onSearch = (e) => {
241
+ const tempList = e?.target?.value?.length ? options.filter(val => typeof val === "object" ? val[value].includes(e?.target?.value) : val.includes(e?.target?.value)) : options
242
+ setLocalOptions(tempList)
243
+ setSearchTerm(e?.target?.value)
244
+ if (!isShow) {
245
+ setIsShow(true)
246
+ }
247
+ }
248
+ useClickOutside(wrapperRef, handlerRef, onClose);
99
249
  return (
100
- <div className="naria-select">
250
+ <div className={`nariaSelect ${disabled ? 'nariaSelect-disabled' : ''}`}>
101
251
  <label
102
252
  className={`cursor-pointer
103
253
  ${hasError && "!text-danger-100"}`}>
104
254
  <span className={``}>{title}</span>
105
- <button type="button"
106
- className={`relative z-20 flex items-center mt-1 text-base justify-between gap-2 w-full
107
- ${localSelected ? "text-dark-100" : "text-grey-300"}
108
- ${hasError && "!border-danger-100 focus:border-danger-100 outline-danger-100"}`}
109
- onClick={onToggle}>
110
- {
111
- localSelected ? (
112
- value?.length ? localSelected[value] : localSelected
113
- ) : "انتخاب"
114
- } <AngleDown
115
- className={`w-4 h-4 transition ease duration-200 ${!isShow ? "rotate-0" : "rotate-180"}`}/>
116
- </button>
255
+ {
256
+ hasSearch ? (
257
+ <div className="nariaSearchInput">
258
+ <input ref={handlerRef}
259
+ placeholder={placeholder?.length ? placeholder : "Select"}
260
+ className={`${localSelected ? "text-dark-100" : "text-grey-300"}
261
+ ${hasError && "!border-danger-100 focus:border-danger-100 outline-danger-100"}`}
262
+ value={searchTerm}
263
+ disabled={disabled} type="text" onClick={onToggle} onChange={onSearch}/>
264
+ {
265
+ isShow ? (
266
+ <Search
267
+ className="nariaSearchIcon"/>
268
+ ) : (
269
+ <AngleDown
270
+ className={`nariaArrowIcon ${isShow ? "nariaArrowIcon-rotate-180" : ""}`}/>
271
+ )
272
+ }
273
+
274
+
275
+ </div>
276
+ ) : (
277
+ <button type="button"
278
+ ref={handlerRef}
279
+ disabled={disabled}
280
+ className={`nariaHandler ${localSelected ? "text-dark-100" : "text-grey-300"}
281
+ ${hasError && "!border-danger-100 focus:border-danger-100 outline-danger-100"}`}
282
+ onClick={onToggle}>
283
+ {
284
+ localSelected ? (
285
+ value?.length ? localSelected[value] : localSelected
286
+ ) : (placeholder?.length ? placeholder : "Select")
287
+ } <AngleDown
288
+ className={`nariaArrowIcon ${isShow ? "nariaArrowIcon-rotate-180" : ""}`}/>
289
+ </button>
290
+ )
291
+ }
292
+
117
293
  </label>
118
294
 
119
295
  {
120
296
  isShow ? (
121
297
  <div
122
- className={`wrapper ${getDeviceWidth < 1024 ? "mobile" : ""}`}>
298
+ className={`nariaListWrapper ${getDeviceWidth < 768 ? "nariaListWrapper-mobile" : ""}`}
299
+ ref={wrapperRef}>
300
+ {
301
+ hasSearch && getDeviceWidth < 768 ? (
302
+ <div className="nariaSearchInput">
303
+ <input ref={handlerRef}
304
+ placeholder={placeholder?.length ? placeholder : "Select"}
305
+ className={`${localSelected ? "text-dark-100" : "text-grey-300"}
306
+ ${hasError && "!border-danger-100 focus:border-danger-100 outline-danger-100"}`}
307
+ value={searchTerm}
308
+ disabled={disabled} type="text" onChange={onSearch}/>
309
+ <Search
310
+ className="nariaSearchIcon"/>
311
+
312
+
313
+ </div>
314
+ ) : undefined
315
+ }
123
316
  <div
124
- className={`list ${getDeviceWidth < 1024 ? "mobile" : `desktop ${maxHeight}`}`}>
317
+ className={`nariaList ${getDeviceWidth < 768 ? "nariaList-mobile" : `nariaList-desktop`}`}
318
+ onScroll={onScroll}>
125
319
  {
126
320
  api && isLoading ? (
127
- <div className="py-10 flex justify-center">
128
- <Loading size="w-7 h-7"/>
321
+ <div className="nariaLoadingWrapper">
322
+ <Loading/>
129
323
  </div>
130
324
  ) : (
131
325
  <>
132
326
  {
133
- getDeviceWidth < 1024 ? (
327
+ getDeviceWidth < 768 ? (
134
328
  <div className="sticky top-0 text-left">
135
- <button className="p-3" onClick={onToggle}>
329
+ <button className="p-3" onClick={onClose} disabled={disabled}>
136
330
  <Close className="w-6"/>
137
331
  </button>
138
332
  </div>
139
333
  ) : undefined
140
334
  }
141
335
  {
142
- localList?.map((item, index) => {
143
- return (
144
- <button type="button" onClick={() => onSelect(item)}
145
- key={index.toString()}
146
- className={`text-right py-2.5 px-4 text-base hover:bg-grey-100 rounded-lg ${getActiveClass(item)}`}>
147
- {value?.length ? item[value] : item}
148
- </button>
149
- )
150
- })
336
+ localOptions?.length ? (
337
+ <>
338
+ {
339
+ localOptions?.map((item, index) => {
340
+ return (
341
+ <button type="button" onClick={() => onSelect(item)}
342
+ disabled={disabled}
343
+ key={index.toString()}
344
+ className={`text-right py-2.5 px-4 text-base hover:bg-grey-100 rounded-lg ${getActiveClass(item)}`}>
345
+ {value?.length ? item[value] : item}
346
+ </button>
347
+ )
348
+ })
349
+ }
350
+ </>
351
+ ) : (
352
+ <div>
353
+ No Data
354
+ </div>
355
+ )
356
+ }
357
+
358
+
359
+ {
360
+ localPagination.isLoading ? (
361
+ <div className="nariaLoadingMoreWrapper">
362
+ <Loading/>
363
+ </div>
364
+ ) : undefined
151
365
  }
152
366
  </>
153
367
  )
154
368
  }
155
369
  </div>
156
- <div
157
- className={`backdrop-select`}
158
- onClick={onToggleEvent}>
159
-
160
- </div>
161
370
  </div>
162
371
  ) : undefined
163
372
  }
@@ -1,8 +1,11 @@
1
- .naria-select {
1
+
2
+ .nariaSelect {
2
3
  position: relative;
3
- .wrapper {
4
+
5
+ .nariaListWrapper {
4
6
  overflow: hidden;
5
- &.mobile {
7
+
8
+ &.nariaListWrapper-mobile {
6
9
  position: fixed;
7
10
  top: 0;
8
11
  right: 0;
@@ -11,34 +14,111 @@
11
14
  z-index: 97;
12
15
  }
13
16
  }
14
- .list {
17
+
18
+ .nariaList {
15
19
  display: flex;
16
20
  flex-direction: column;
17
21
  width: 100%;
18
22
  z-index: 99;
19
- &.mobile {
23
+ position: relative;
24
+ overflow: auto;
25
+
26
+ &.nariaList-mobile {
20
27
  position: relative;
21
28
  height: 100%;
22
29
  }
23
- &.desktop {
24
- overflow: auto;
30
+
31
+ &.nariaList-desktop {
25
32
  position: absolute;
26
33
  animation: fadeInTranslateY 0.3s ease-out forwards;
34
+ max-height: 100px;
35
+ }
36
+
37
+ .nariaLoadingWrapper {
38
+ padding: 24px 0;
39
+ display: flex;
40
+ justify-content: center;
41
+
42
+ }
43
+
44
+ .nariaLoadingMoreWrapper {
45
+ position: sticky;
46
+ bottom: 0;
47
+ text-align: center;
48
+ width: 100%;
49
+
50
+ .nariaLoading {
51
+ width: 20px;
52
+ }
53
+ }
54
+ }
55
+
56
+ .nariaSearchInput {
57
+ position: relative;
58
+ display: flex;
59
+
60
+ input {
61
+ width: 100%;
62
+ padding-right: 20px;
27
63
  }
64
+
65
+ .nariaArrowIcon, .nariaSearchIcon {
66
+ position: absolute;
67
+ bottom: 0;
68
+ margin-top: auto;
69
+ margin-bottom: auto;
70
+ top: 0;
71
+ right: 6px;
72
+ width: 12px;
73
+ height: 12px;
74
+ }
75
+
28
76
  }
29
- .backdrop-select {
30
- position: fixed;
31
- top: 0;
32
- left: 0;
33
- bottom: 0;
77
+
78
+ .nariaHandler {
79
+ position: relative;
80
+ display: flex;
81
+ gap: 4px;
34
82
  width: 100%;
35
- height: 100%;
36
- z-index: 98;
37
- background-color: rgba(0, 0, 0, 0.2);
83
+ align-items: center;
84
+ justify-content: space-between;
85
+
86
+ .nariaArrowIcon {
87
+ width: 12px;
88
+ height: 12px;
89
+ transition: transform 0.4s;
90
+
91
+ -webkit-transform: rotate(0deg);
92
+ -moz-transform: rotate(0deg);
93
+ -o-transform: rotate(0deg);
94
+ -ms-transform: rotate(0deg);
95
+ transform: rotate(0deg);
96
+
97
+ &.nariaArrowIcon-rotate-180 {
98
+ -webkit-transform: rotate(180deg);
99
+ -moz-transform: rotate(180deg);
100
+ -o-transform: rotate(180deg);
101
+ -ms-transform: rotate(180deg);
102
+ transform: rotate(180deg);
103
+ }
104
+ }
38
105
  }
39
106
  }
40
107
 
41
-
108
+ html[dir="rtl"] {
109
+ .nariaSelect {
110
+ .nariaSearchInput {
111
+ input {
112
+ padding-left: 20px;
113
+ padding-right: 0;
114
+ }
115
+ .nariaArrowIcon, .nariaSearchIcon {
116
+ left: 6px;
117
+ right: auto;
118
+ }
119
+ }
120
+ }
121
+ }
42
122
 
43
123
  @keyframes fadeInTranslateY {
44
124
  from {
@@ -10,11 +10,11 @@ export const addNavigation = (state: string) => {
10
10
  }
11
11
 
12
12
 
13
- export const onHashChanges = () => {
13
+ export const onHashChanges = (state: string) => {
14
14
  const [isHashChanged, setIsHashChanged] = useState(false)
15
15
  useEffect(() => {
16
16
  const handleHashChange = (e) => {
17
- if(window.location.hash !== "#modal") {
17
+ if(window.location.hash !== state) {
18
18
  setIsHashChanged(true);
19
19
  } else {
20
20
  setIsHashChanged(false);
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "Mohammad Mehdi Mohammadi",
6
6
  "url": ""
7
7
  },
8
- "version": "0.1.37",
8
+ "version": "0.1.38",
9
9
  "type": "module",
10
10
  "main": "dist/naria-ui.cjs.js",
11
11
  "module": "dist/naria-ui.es.js",
@@ -19,7 +19,7 @@
19
19
  "./dist/*.css": "./dist/*.css",
20
20
  "./assets/*.css": "./assets/*.css"
21
21
  },
22
- "files": ["*.md", "dist", "lib", "es", "src"],
22
+ "files": ["*.md", "dist", "lib", "es"],
23
23
  "publishConfig": {
24
24
  "access": "public"
25
25
  },
package/src/App.css DELETED
File without changes
package/src/App.tsx DELETED
@@ -1,15 +0,0 @@
1
- import './App.css'
2
- import {Button, Input, Select} from "../lib";
3
-
4
- function App() {
5
-
6
- return (
7
- <>
8
- <Button value="sadasd"/>
9
- <Input placeholder="test" label="test"/>
10
- <Select label="id" value="name" list = {[{id: 1, name: "sad"}]} />
11
- </>
12
- )
13
- }
14
-
15
- export default App
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" fill="currentColor"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M241 337c-9.4 9.4-24.6 9.4-33.9 0L47 177c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l143 143L367 143c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9L241 337z"/></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Pro 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc. --><path d="M47 239c-9.4 9.4-9.4 24.6 0 33.9L207 433c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9L97.9 256 241 113c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0L47 239z"/></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Pro 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc. --><path d="M273 239c9.4 9.4 9.4 24.6 0 33.9L113 433c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l143-143L79 113c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0L273 239z"/></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" fill="currentColor"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M207 143c9.4-9.4 24.6-9.4 33.9 0L401 303c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-143-143L81 337c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9L207 143z"/></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor"><path d="M480-424 284-228q-11 11-28 11t-28-11q-11-11-11-28t11-28l196-196-196-196q-11-11-11-28t11-28q11-11 28-11t28 11l196 196 196-196q11-11 28-11t28 11q11 11 11 28t-11 28L536-480l196 196q11 11 11 28t-11 28q-11 11-28 11t-28-11L480-424Z"/></svg>
@@ -1 +0,0 @@
1
- @import "./select.scss";