@vtex/faststore-plugin-buyer-portal 1.0.25 → 1.0.26

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.
Files changed (126) hide show
  1. package/package.json +6 -3
  2. package/plugin.config.js +8 -4
  3. package/public/buyer-portal-icons.svg +24 -0
  4. package/src/clients/BuyerPortalClient.ts +128 -0
  5. package/src/clients/Client.ts +95 -0
  6. package/src/components/AddressLine/address-line.scss +5 -5
  7. package/src/components/AddressesCard/AddressesCard.tsx +4 -4
  8. package/src/components/AutocompleteDropdown/AutocompleteDropdown.tsx +175 -0
  9. package/src/components/AutocompleteDropdown/AutocompleteDropdownItem.tsx +39 -0
  10. package/src/components/AutocompleteDropdown/autocomplete-dropdown.scss +45 -0
  11. package/src/components/AutocompleteDropdown/useAutocompletePosition.ts +86 -0
  12. package/src/components/{HomeCard/HomeCard.tsx → BasicCard/BasicCard.tsx} +7 -5
  13. package/src/components/BasicDrawer/BasicDrawer.tsx +37 -0
  14. package/src/components/BasicDrawer/BasicDrawerBody.tsx +16 -0
  15. package/src/components/BasicDrawer/BasicDrawerButton.tsx +34 -0
  16. package/src/components/BasicDrawer/BasicDrawerFooter.tsx +16 -0
  17. package/src/components/BasicDrawer/BasicDrawerHeading.tsx +27 -0
  18. package/src/components/BasicDrawer/basic-drawer.scss +121 -0
  19. package/src/components/BasicDropdownMenu/BasicDropdownMenu.tsx +13 -0
  20. package/src/components/BasicDropdownMenu/basic-dropdown-menu.scss +19 -0
  21. package/src/components/Card/card.scss +59 -36
  22. package/src/components/ContractsCard/ContractsCard.tsx +19 -119
  23. package/src/components/CreateOrgUnitDrawer/CreateOrgUnitDrawer.tsx +126 -4
  24. package/src/components/CreateOrgUnitDrawer/create-org-unit-drawer.scss +6 -0
  25. package/src/components/CustomerSwitch/CustomerSwitchDrawer.tsx +0 -1
  26. package/src/components/CustomerSwitch/customer-switch.scss +9 -9
  27. package/src/components/DeleteOrgUnitDrawer/DeleteOrgUnitDrawer.tsx +145 -0
  28. package/src/components/DeleteOrgUnitDrawer/delete-org-unit-drawer.scss +81 -0
  29. package/src/components/DropdownFilter/dropdown-filter.scss +2 -2
  30. package/src/components/ErrorMessage/ErrorMessage.tsx +7 -0
  31. package/src/components/ErrorMessage/error-message.scss +7 -0
  32. package/src/components/HierarchyTree/HierarchyTree.tsx +3 -3
  33. package/src/components/InputText/InputText.tsx +82 -0
  34. package/src/components/InputText/input-text.scss +93 -0
  35. package/src/components/InternalSearch/internal-search.scss +2 -2
  36. package/src/components/InternalTopbar/InternalTopbar.tsx +1 -1
  37. package/src/components/InternalTopbar/internal-top-bar.scss +13 -24
  38. package/src/components/MainLinksDropdownMenu/MainLinksDropdownMenu.tsx +2 -2
  39. package/src/components/Navbar/navbar.scss +1 -1
  40. package/src/components/OrgUnitsDropdownMenu/OrgUnitsDropdownMenu.tsx +80 -0
  41. package/src/components/OrgUnitsDropdownMenu/org-units-dropdown-menu.scss +4 -0
  42. package/src/components/OrgUnitsHierarchyTree/OrgUnitsHierarchyTree.tsx +66 -25
  43. package/src/components/OrgUnitsHierarchyTree/org-units-hierarchy-tree.scss +56 -25
  44. package/src/components/OrganizationalUnitsCard/OrganizationalUnitsCard.tsx +44 -77
  45. package/src/components/ProfileCard/ProfileCard.tsx +3 -3
  46. package/src/components/ProfileSummary/profile-summary.scss +7 -7
  47. package/src/components/SelfManagementDrawer/self-management-drawer.scss +23 -21
  48. package/src/components/SortFilter/sort-filter.scss +2 -2
  49. package/src/components/Tab/Tab.tsx +40 -0
  50. package/src/components/Tab/TabBar.tsx +7 -0
  51. package/src/components/Tab/TabContent.tsx +14 -0
  52. package/src/components/Tab/TabOption.tsx +20 -0
  53. package/src/components/Tab/tab.scss +19 -0
  54. package/src/components/Toast/Toast.tsx +53 -0
  55. package/src/components/Toast/toast.scss +124 -0
  56. package/src/components/UpdateOrgUnitDrawer/UpdateOrgUnitDrawer.tsx +123 -0
  57. package/src/components/UpdateOrgUnitDrawer/update-org-unit-drawer.scss +6 -0
  58. package/src/components/UsersCard/UsersCard.tsx +15 -19
  59. package/src/hooks/useChildrenOrgUnits.ts +19 -0
  60. package/src/hooks/useCreateNewOrgUnit.ts +28 -0
  61. package/src/hooks/useDeleteOrgUnit.ts +24 -0
  62. package/src/hooks/useDrawerProps.ts +32 -0
  63. package/src/hooks/useMutation.ts +51 -0
  64. package/src/hooks/useOrgUnitStructure.ts +63 -0
  65. package/src/hooks/useQuery.ts +49 -0
  66. package/src/hooks/useRootOrgUnitByCustomer.ts +19 -0
  67. package/src/hooks/useUpdateOrgUnit.ts +24 -0
  68. package/src/hooks/useUsersByOrgUnit.ts +20 -0
  69. package/src/layouts/AddressDetailsLayout/address-details-layout.scss +9 -9
  70. package/src/layouts/AddressesLayout/addresses-layout.scss +3 -2
  71. package/src/layouts/ContractsLayout/ContractsLayout.tsx +1 -1
  72. package/src/layouts/ContractsLayout/contracts-layout.scss +1 -1
  73. package/src/layouts/GlobalLayout/GlobalLayout.tsx +4 -0
  74. package/src/layouts/GlobalLayout/global-layout.scss +1 -0
  75. package/src/layouts/HomeLayout/HomeLayout.tsx +22 -22
  76. package/src/layouts/HomeLayout/home-layout.scss +10 -8
  77. package/src/layouts/OrgUnitDetailsLayout/OrgUnitDetailsLayout.tsx +34 -0
  78. package/src/layouts/OrgUnitsLayout/OrgUnitsLayout.tsx +27 -9
  79. package/src/layouts/OrgUnitsLayout/org-units-layout.scss +9 -4
  80. package/src/layouts/ProfileLayout/profile-layout.scss +7 -7
  81. package/src/layouts/UserDetailsLayout/UserDetailsLayout.tsx +1 -1
  82. package/src/layouts/UserDetailsLayout/user-details-layout.scss +16 -15
  83. package/src/layouts/UsersLayout/UsersLayout.tsx +16 -12
  84. package/src/layouts/UsersLayout/users-layout.scss +20 -20
  85. package/src/mock/contracts-data.ts +3 -3
  86. package/src/mock/org-units-data.ts +31 -31
  87. package/src/mock/organization-data.ts +8 -1
  88. package/src/mock/users-data.ts +10 -4
  89. package/src/pages/address-details.tsx +18 -5
  90. package/src/pages/addresses.tsx +11 -3
  91. package/src/pages/contracts.tsx +24 -12
  92. package/src/pages/home.tsx +14 -3
  93. package/src/pages/org-unit-details.tsx +41 -0
  94. package/src/pages/org-units.tsx +23 -10
  95. package/src/pages/profile.tsx +14 -4
  96. package/src/pages/user-details.tsx +13 -7
  97. package/src/pages/users.tsx +23 -9
  98. package/src/services/create-new-org-unit.service.ts +15 -0
  99. package/src/services/delete-org-unit.service.ts +13 -0
  100. package/src/services/get-addresses.service.ts +33 -47
  101. package/src/services/get-children-org-units.service.ts +18 -0
  102. package/src/services/get-contracts.service.ts +22 -41
  103. package/src/services/get-org-unit-by-id.service.ts +25 -0
  104. package/src/services/get-org-unit-summary.service.ts +13 -0
  105. package/src/services/get-organization.service.ts +10 -7
  106. package/src/services/get-root-org-unit-by-customer-id.service.ts +10 -0
  107. package/src/services/get-users-by-org-unit-id.service.ts +9 -0
  108. package/src/services/get-users.service.ts +7 -19
  109. package/src/services/update-org-unit.service.ts +14 -0
  110. package/src/themes/index.scss +13 -12
  111. package/src/types/AwaitedType.d.ts +1 -0
  112. package/src/types/BreadcrumbData.ts +6 -0
  113. package/src/types/ContractData.ts +5 -2
  114. package/src/types/OrgUnitSummaryData.ts +10 -0
  115. package/src/types/OrgUnitsData.ts +3 -1
  116. package/src/types/OrganizationData.ts +3 -2
  117. package/src/types/UserData.ts +5 -2
  118. package/src/utils/api.ts +6 -4
  119. package/src/utils/compareItems.ts +19 -0
  120. package/src/utils/constants.ts +5 -4
  121. package/src/utils/cookie.ts +1 -1
  122. package/src/utils/getClientContext.ts +9 -1
  123. package/src/utils/getTreeDepth.ts +3 -6
  124. package/src/utils/search.tsx +2 -2
  125. package/src/utils/toQueryParams.ts +14 -0
  126. package/src/services/get-org-units-hierarchy.service.ts +0 -7
package/package.json CHANGED
@@ -1,14 +1,17 @@
1
1
  {
2
2
  "name": "@vtex/faststore-plugin-buyer-portal",
3
- "version": "1.0.25",
3
+ "version": "1.0.26",
4
4
  "description": "A plugin for faststore with buyer portal",
5
5
  "main": "index.js",
6
- "dependencies": {},
6
+ "dependencies": {
7
+ "@types/react-dom": "^19.0.3",
8
+ "react-dom": "^19.0.0"
9
+ },
7
10
  "devDependencies": {
8
11
  "@faststore/core": "^3.0.147",
9
12
  "@faststore/ui": "^3.0.147",
10
13
  "@types/react": "^18.2.42",
11
- "next": "13.5.6",
14
+ "next": "13.5.7",
12
15
  "typescript": "4.7.3"
13
16
  },
14
17
  "peerDependencies": {
package/plugin.config.js CHANGED
@@ -5,12 +5,16 @@ module.exports = {
5
5
  path: "/buyer-portal/[[...contractMode]]",
6
6
  appLayout: false,
7
7
  },
8
+ "org-unit-details": {
9
+ path: "/org-unit/[orgUnitId]",
10
+ appLayout: false,
11
+ },
8
12
  "org-units": {
9
- path: "/org-units",
13
+ path: "/org-units/[[...orgUnitId]]",
10
14
  appLayout: false,
11
15
  },
12
16
  contracts: {
13
- path: "/contracts",
17
+ path: "/contracts/[[...orgUnitId]]",
14
18
  appLayout: false,
15
19
  },
16
20
  addresses: {
@@ -18,7 +22,7 @@ module.exports = {
18
22
  appLayout: false,
19
23
  },
20
24
  "address-details": {
21
- path: "/address/[id]",
25
+ path: "/address/[addressId]",
22
26
  appLayout: false,
23
27
  },
24
28
  users: {
@@ -26,7 +30,7 @@ module.exports = {
26
30
  appLayout: false,
27
31
  },
28
32
  "user-details": {
29
- path: "/user/[id]",
33
+ path: "/user/[userId]",
30
34
  appLayout: false,
31
35
  },
32
36
  profile: {
@@ -62,6 +62,13 @@
62
62
  stroke-linejoin="round"></polyline>
63
63
  </symbol>
64
64
 
65
+ <symbol id="Check" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 10"
66
+ fill="currentColor">
67
+ <path
68
+ d="M4.10417 9.4375L0.0625 5.41667L1.125 4.33333L4.10417 7.3125L10.875 0.5625L11.9375 1.625L4.10417 9.4375Z"
69
+ fill="currentColor" />
70
+ </symbol>
71
+
65
72
 
66
73
  <symbol
67
74
  id="Delete" xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"
@@ -183,6 +190,16 @@
183
190
  d="M480-160q-33 0-56.5-23.5T400-240q0-33 23.5-56.5T480-320q33 0 56.5 23.5T560-240q0 33-23.5 56.5T480-160Zm0-240q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Zm0-240q-33 0-56.5-23.5T400-720q0-33 23.5-56.5T480-800q33 0 56.5 23.5T560-720q0 33-23.5 56.5T480-640Z" />
184
191
  </symbol>
185
192
 
193
+ <symbol
194
+ id="Close"
195
+ xmlns="http://www.w3.org/2000/svg"
196
+ viewBox="0 0 14 14"
197
+ fill="none">
198
+ <path
199
+ d="M14 1.41L12.59 0L7 5.59L1.41 0L0 1.41L5.59 7L0 12.59L1.41 14L7 8.41L12.59 14L14 12.59L8.41 7L14 1.41Z"
200
+ fill="currentColor" />
201
+ </symbol>
202
+
186
203
 
187
204
  <symbol
188
205
  id="OpenInNew"
@@ -216,4 +233,11 @@
216
233
  <path
217
234
  d="M154-128q-43.72 0-74.86-31.14Q48-190.27 48-234v-492q0-43.72 31.14-74.86T154-832h220l106 106h326q43.72 0 74.86 31.14T912-620v386q0 43.73-31.14 74.86Q849.72-128 806-128H154Zm339.67-136 104.16-79.68L702-264l-38-130 104-84H638l-40-126-40 126H428l104.17 84.23L493.67-264Z" />
218
235
  </symbol>
236
+
237
+ <symbol id="LoadingIndicator" xmlns="http://www.w3.org/2000/svg"
238
+ viewBox="0 0 18 18" fill="none">
239
+ <path
240
+ d="M17.4643 5.99996L15.5245 6.51972C16.2578 8.40957 16.1575 10.6033 15.0625 12.4999C13.1275 15.8514 8.85184 16.9971 5.50032 15.0621C2.1488 13.1271 1.00314 8.85143 2.93814 5.49992C4.03314 3.60332 5.88283 2.41957 7.88249 2.09606L7.36639 0.169961C4.8613 0.628903 2.57609 2.12701 1.20609 4.49992C-1.27891 8.80406 0.196175 14.3091 4.50032 16.7941C8.80447 19.2791 14.3096 17.8041 16.7946 13.4999C18.1646 11.127 18.3193 8.3989 17.4643 5.99996Z"
241
+ fill="currentColor" />
242
+ </symbol>
219
243
  </svg>
@@ -0,0 +1,128 @@
1
+ import type { AddressData } from "../types/AddressData";
2
+ import type { ContractData } from "../types/ContractData";
3
+ import type { OrgUnitData } from "../types/OrgUnitsData";
4
+ import type { OrgUnitSummaryData } from "../types/OrgUnitSummaryData";
5
+ import { getApiUrl } from "../utils/api";
6
+ import Client from "./Client";
7
+
8
+ class BuyerPortalClient extends Client {
9
+ constructor() {
10
+ super(getApiUrl());
11
+ }
12
+
13
+ // Contracts
14
+ getContractsByCustomerId(customerId: string, cookie: string) {
15
+ return this.get<{ data: { contracts: ContractData[]; total: number } }>(
16
+ `contracts/${customerId}`,
17
+ {
18
+ headers: {
19
+ Cookie: cookie,
20
+ },
21
+ }
22
+ );
23
+ }
24
+
25
+ getContracts(
26
+ cookie: string,
27
+ params?: {
28
+ salesRepresentative?: string;
29
+ isActive?: boolean;
30
+ page?: number;
31
+ sort?: "Asc" | "Desc";
32
+ }
33
+ ) {
34
+ return this.get<ContractData[]>("contracts/filter", {
35
+ headers: {
36
+ Cookie: cookie,
37
+ },
38
+ params,
39
+ });
40
+ }
41
+
42
+ //Addresses
43
+ getAddressesByCustomerId(customerId: string, cookie: string) {
44
+ return this.get<{ addresses: AddressData[] }>(`addresses/${customerId}`, {
45
+ headers: {
46
+ Cookie: cookie,
47
+ },
48
+ });
49
+ }
50
+
51
+ // Users
52
+ // getUsersByOrgUnitId(orgUnitId: string, cookie: string) {
53
+ // return this.get<ContractData[]>(`getUsers/${orgUnitId}`, {
54
+ // headers: {
55
+ // Cookie: cookie,
56
+ // },
57
+ // });
58
+ // }
59
+
60
+ //Org Units
61
+ getOrgUnitSummary(id: string, cookie: string) {
62
+ return this.get<OrgUnitSummaryData>(`units/summary/${id}`, {
63
+ headers: {
64
+ Cookie: cookie,
65
+ },
66
+ });
67
+ }
68
+
69
+ getRootOrgUnitByCustomerId(customerId: string, cookie: string) {
70
+ return this.get<{ organizationalUnits: OrgUnitData[]; total: number }>(
71
+ `units/root/${customerId}`,
72
+ {
73
+ headers: {
74
+ Cookie: cookie,
75
+ },
76
+ }
77
+ );
78
+ }
79
+
80
+ createOrgUnit(
81
+ {
82
+ parentId,
83
+ ...data
84
+ }: { name: string; parentId: string | null; customerId: string[] },
85
+ cookie: string
86
+ ) {
87
+ return this.post(
88
+ "units",
89
+ { ...data, ...(parentId && { parentId }) },
90
+ {
91
+ headers: {
92
+ Cookie: cookie,
93
+ },
94
+ }
95
+ );
96
+ }
97
+
98
+ updateOrgUnit(
99
+ { id, ...data }: { name?: string; id: string },
100
+ cookie: string
101
+ ) {
102
+ return this.patch(`units/${id}`, data, {
103
+ headers: {
104
+ Cookie: cookie,
105
+ },
106
+ });
107
+ }
108
+
109
+ deleteOrgUnit(orgUnitId: string, cookie: string) {
110
+ return this.delete(`units/${orgUnitId}`, {
111
+ headers: {
112
+ Cookie: cookie,
113
+ },
114
+ });
115
+ }
116
+
117
+ // getChildrenByOrgUnitId(orgUnitId: string, cookie: string) {
118
+ // return this.get<ContractData[]>(`getChildren/${orgUnitId}`, {
119
+ // headers: {
120
+ // Cookie: cookie,
121
+ // },
122
+ // });
123
+ // }
124
+ }
125
+
126
+ const buyerPortalClient = new BuyerPortalClient();
127
+
128
+ export { buyerPortalClient };
@@ -0,0 +1,95 @@
1
+ import { toQueryParams } from "../utils/toQueryParams";
2
+
3
+ interface RequestConfig<T> {
4
+ url: string;
5
+ method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
6
+ data?: T;
7
+ headers?: Record<string, string>;
8
+ params?: Record<string, string | number | boolean>;
9
+ }
10
+
11
+ class Client {
12
+ private baseURL: string;
13
+
14
+ constructor(baseURL = "") {
15
+ this.baseURL = baseURL;
16
+ }
17
+
18
+ async request<T>({
19
+ url,
20
+ method,
21
+ data,
22
+ headers = {},
23
+ params = {},
24
+ }: RequestConfig<T>): Promise<T> {
25
+ const config: RequestInit = {
26
+ method,
27
+ headers: {
28
+ "Content-Type": "application/json",
29
+ ...headers,
30
+ },
31
+ };
32
+
33
+ const paramsString = toQueryParams(params);
34
+
35
+ if (data) {
36
+ config.body = JSON.stringify(data);
37
+ }
38
+
39
+ try {
40
+ const response = await fetch(this.baseURL + url + paramsString, config);
41
+ let responseData: T | null = null;
42
+
43
+ const contentType = response.headers.get("Content-Type");
44
+ if (contentType?.includes("application/json")) {
45
+ responseData = await response.json();
46
+ }
47
+
48
+ if (!response.ok) {
49
+ throw new Error(
50
+ responseData
51
+ ? (responseData as unknown as { message: string }).message
52
+ : "Request failed"
53
+ );
54
+ }
55
+
56
+ return responseData as T;
57
+ } catch (error) {
58
+ return Promise.reject(error);
59
+ }
60
+ }
61
+
62
+ get<T>(url: string, config: Partial<RequestConfig<T>> = {}): Promise<T> {
63
+ return this.request<T>({ url, method: "GET", ...config });
64
+ }
65
+
66
+ post<T>(
67
+ url: string,
68
+ data: T,
69
+ config: Partial<RequestConfig<T>> = {}
70
+ ): Promise<T> {
71
+ return this.request<T>({ url, method: "POST", data, ...config });
72
+ }
73
+
74
+ put<T>(
75
+ url: string,
76
+ data: T,
77
+ config: Partial<RequestConfig<T>> = {}
78
+ ): Promise<T> {
79
+ return this.request<T>({ url, method: "PUT", data, ...config });
80
+ }
81
+
82
+ patch<T>(
83
+ url: string,
84
+ data: T,
85
+ config: Partial<RequestConfig<T>> = {}
86
+ ): Promise<T> {
87
+ return this.request<T>({ url, method: "PATCH", data, ...config });
88
+ }
89
+
90
+ delete<T>(url: string, config: Partial<RequestConfig<T>> = {}): Promise<T> {
91
+ return this.request<T>({ url, method: "DELETE", ...config });
92
+ }
93
+ }
94
+
95
+ export default Client;
@@ -27,8 +27,8 @@
27
27
 
28
28
  [data-fs-addresses-line-icon] {
29
29
  color: #0068d7;
30
- margin: 10px;
31
- margin-right: 20px;
30
+ margin: calc(var(--fs-spacing-2) - var(--fs-spacing-0));
31
+ margin-right: calc(var(--fs-spacing-3) + var(--fs-spacing-0));
32
32
  }
33
33
 
34
34
  [data-fs-addresses-line-name] {
@@ -52,9 +52,9 @@
52
52
 
53
53
  [data-fs-addresses-dropdown-trigger] {
54
54
  cursor: pointer;
55
- height: 40px;
56
- width: 40px;
57
- border-radius: 1000px;
55
+ height: var(--fs-spacing-6);
56
+ width: var(--fs-spacing-6);
57
+ border-radius: var(--fs-border-radius-pill);
58
58
  &:hover {
59
59
  background-color: #e0e0e0;
60
60
  }
@@ -8,7 +8,7 @@ import {
8
8
  import { Tag } from "../Tag/Tag";
9
9
  import { AddressData } from "../../types/AddressData";
10
10
  import { Icon } from "../Icon";
11
- import { HomeCard } from "../HomeCard/HomeCard";
11
+ import { BasicCard } from "../BasicCard/BasicCard";
12
12
 
13
13
  type AddressesCardProps = {
14
14
  addresses: AddressData[];
@@ -16,7 +16,7 @@ type AddressesCardProps = {
16
16
 
17
17
  export default function AddressesCard({ addresses }: AddressesCardProps) {
18
18
  return (
19
- <HomeCard
19
+ <BasicCard
20
20
  footerLink="/addresses"
21
21
  footerMessage="Manage addresses"
22
22
  title="Addresses"
@@ -74,7 +74,7 @@ export default function AddressesCard({ addresses }: AddressesCardProps) {
74
74
  />{" "}
75
75
  Delete
76
76
  </DropdownItem>
77
- <DropdownItem>
77
+ <DropdownItem dismissOnClick={false}>
78
78
  <div data-fs-dropdown-active-item>
79
79
  <div data-fs-dropdown-active-item-label>
80
80
  <Icon
@@ -99,6 +99,6 @@ export default function AddressesCard({ addresses }: AddressesCardProps) {
99
99
  </div>
100
100
  );
101
101
  })}
102
- </HomeCard>
102
+ </BasicCard>
103
103
  );
104
104
  }
@@ -0,0 +1,175 @@
1
+ import { InputText } from "../InputText/InputText";
2
+ import {
3
+ useRef,
4
+ useState,
5
+ KeyboardEvent,
6
+ ComponentProps,
7
+ useEffect,
8
+ ReactNode,
9
+ createContext,
10
+ useContext,
11
+ } from "react";
12
+ import { createPortal } from "react-dom";
13
+ import { useAutocompletePosition } from "./useAutocompletePosition";
14
+ import { Icon } from "../Icon";
15
+ import { AutocompleteDropdownItem } from "./AutocompleteDropdownItem";
16
+
17
+ export type AutocompleteDropdownContextProps = {
18
+ focusedItemIndex: number;
19
+ setFocusedItemIndex: (index: number) => void;
20
+ close: () => void;
21
+ };
22
+
23
+ const AutocompleteDropdownContext =
24
+ createContext<AutocompleteDropdownContextProps | null>(null);
25
+
26
+ export const useAutocompleteDropdownContext = () => {
27
+ const context = useContext(AutocompleteDropdownContext);
28
+
29
+ if (!context) {
30
+ throw new Error(
31
+ "AutocompleteDropdown compound components cannot be rendered outside the AutocompleteDropdown component"
32
+ );
33
+ }
34
+
35
+ return context;
36
+ };
37
+
38
+ export type AutocompleteDropdownProps<T> = ComponentProps<"input"> & {
39
+ label: string;
40
+ options?: T[];
41
+ renderOption?: (option: T, index: number) => ReactNode;
42
+ onConfirmKeyPress?: (option: T) => void;
43
+ hasError?: boolean;
44
+ };
45
+
46
+ export const AutocompleteDropdown = <T,>({
47
+ label,
48
+ options = [],
49
+ onChange,
50
+ renderOption,
51
+ onInput,
52
+ disabled,
53
+ onConfirmKeyPress,
54
+ hasError,
55
+ ...props
56
+ }: AutocompleteDropdownProps<T>) => {
57
+ const [isOpened, setIsOpened] = useState(false);
58
+
59
+ const [focusedItemIndex, setFocusedItemIndex] = useState(0);
60
+
61
+ const autocompleteMenuRef = useRef<HTMLDivElement>(null);
62
+ const wrapperAutocompleteRef = useRef<HTMLDivElement>(null);
63
+
64
+ const positionStyle = useAutocompletePosition(
65
+ "left",
66
+ isOpened,
67
+ wrapperAutocompleteRef
68
+ );
69
+
70
+ const handleClick = () => {
71
+ !disabled && setIsOpened((old) => !old);
72
+ };
73
+
74
+ const handleBackdropKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
75
+ if (event.defaultPrevented || event.key === " ") {
76
+ return;
77
+ }
78
+
79
+ event.preventDefault();
80
+
81
+ switch (event.key) {
82
+ case "Enter":
83
+ onConfirmKeyPress?.(options[focusedItemIndex]);
84
+ break;
85
+ case "ArrowDown":
86
+ focusedItemIndex < options.length - 1 &&
87
+ setFocusedItemIndex(focusedItemIndex + 1);
88
+ break;
89
+ case "ArrowUp":
90
+ focusedItemIndex > 0 && setFocusedItemIndex(focusedItemIndex - 1);
91
+ break;
92
+ case "Home":
93
+ setFocusedItemIndex(0);
94
+ break;
95
+ case "End":
96
+ setFocusedItemIndex(options.length - 1);
97
+ break;
98
+ default:
99
+ break;
100
+ }
101
+ };
102
+
103
+ const close = () => {
104
+ setIsOpened(false);
105
+ };
106
+
107
+ useEffect(() => {
108
+ const event = (e: MouseEvent) => {
109
+ const target = e.target as Node;
110
+ const wasSomeItemClicked =
111
+ autocompleteMenuRef.current === target ||
112
+ autocompleteMenuRef.current?.contains(target) ||
113
+ wrapperAutocompleteRef.current?.contains(target);
114
+ !wasSomeItemClicked && close();
115
+ };
116
+
117
+ const overlayRef = document.querySelector(
118
+ "[data-fs-overlay]"
119
+ ) as HTMLElement;
120
+
121
+ document.addEventListener("click", event);
122
+ overlayRef?.addEventListener("click", event);
123
+
124
+ return () => {
125
+ document.removeEventListener("click", event);
126
+ overlayRef.removeEventListener("click", event);
127
+ };
128
+ }, []);
129
+
130
+ return (
131
+ <AutocompleteDropdownContext.Provider
132
+ value={{
133
+ close,
134
+ focusedItemIndex,
135
+ setFocusedItemIndex: (index: number) => setFocusedItemIndex(index),
136
+ }}
137
+ >
138
+ <div
139
+ data-fs-bp-autocomplete-dropdown
140
+ data-fs-bp-autocomplete-dropdown-only-select={!onChange}
141
+ ref={wrapperAutocompleteRef}
142
+ onClick={handleClick}
143
+ >
144
+ <InputText
145
+ label={label}
146
+ onKeyDown={handleBackdropKeyDown}
147
+ icon={<Icon name="ArrowDropDown" width={20} height={20} />}
148
+ onChange={onChange}
149
+ disabled={disabled}
150
+ hasError={hasError}
151
+ {...props}
152
+ />
153
+
154
+ {isOpened
155
+ ? createPortal(
156
+ <div
157
+ data-fs-bp-autocomplete-dropdown-menu
158
+ style={positionStyle}
159
+ ref={autocompleteMenuRef}
160
+ >
161
+ <ul>
162
+ {options?.map((option, index) =>
163
+ renderOption?.(option, index)
164
+ )}
165
+ </ul>
166
+ </div>,
167
+ document.body
168
+ )
169
+ : null}
170
+ </div>
171
+ </AutocompleteDropdownContext.Provider>
172
+ );
173
+ };
174
+
175
+ AutocompleteDropdown.Item = AutocompleteDropdownItem;
@@ -0,0 +1,39 @@
1
+ import { ComponentProps, ReactNode } from "react";
2
+ import { useAutocompleteDropdownContext } from "./AutocompleteDropdown";
3
+
4
+ export type AutocompleteDropdownItemProps = ComponentProps<"li"> & {
5
+ isSelected: boolean;
6
+ children: ReactNode;
7
+ index: number;
8
+ closeOnClick?: boolean;
9
+ key: string | number;
10
+ };
11
+
12
+ export const AutocompleteDropdownItem = ({
13
+ isSelected,
14
+ children,
15
+ index,
16
+ onClick,
17
+ closeOnClick = false,
18
+ ...props
19
+ }: AutocompleteDropdownItemProps) => {
20
+ const { focusedItemIndex, setFocusedItemIndex, close } =
21
+ useAutocompleteDropdownContext();
22
+ return (
23
+ <li
24
+ data-fs-bp-autocomplete-dropdown-option
25
+ data-fs-bp-autocomplete-dropdown-option-focused={
26
+ focusedItemIndex === index
27
+ }
28
+ data-fs-bp-autocomplete-dropdown-option-selected={isSelected}
29
+ onMouseEnter={() => setFocusedItemIndex(index)}
30
+ onClick={(e) => {
31
+ closeOnClick && close();
32
+ onClick?.(e);
33
+ }}
34
+ {...props}
35
+ >
36
+ {children}
37
+ </li>
38
+ );
39
+ };
@@ -0,0 +1,45 @@
1
+ @import "@faststore/ui/src/components/molecules/Dropdown/styles.scss";
2
+
3
+ [data-fs-bp-autocomplete-dropdown] {
4
+ &[data-fs-bp-autocomplete-dropdown-only-select="true"] {
5
+ [data-fs-bp-input-text-input] {
6
+ caret-color: transparent;
7
+ background-color: #ffffff;
8
+ }
9
+ }
10
+ }
11
+
12
+ [data-fs-bp-autocomplete-dropdown-menu] {
13
+ padding: var(--fs-spacing-1) 0;
14
+ background-color: #fff;
15
+ box-shadow: 0rem 0.5rem 0.625rem 0rem #00000014;
16
+
17
+ border-radius: calc(var(--fs-border-radius) * 2);
18
+
19
+ [data-fs-bp-autocomplete-dropdown-option] {
20
+ display: flex;
21
+ align-items: center;
22
+ justify-content: space-between;
23
+
24
+ width: 100%;
25
+
26
+ font-size: var(--fs-text-size-1);
27
+ font-weight: 500;
28
+ line-height: calc(var(--fs-spacing-3) + var(--fs-spacing-0));
29
+
30
+ color: #3d3d3d;
31
+
32
+ padding: var(--fs-spacing-1) calc(var(--fs-spacing-3) + var(--fs-spacing-0));
33
+
34
+ cursor: pointer;
35
+
36
+ &[data-fs-bp-autocomplete-dropdown-option-focused="true"] {
37
+ background-color: #f5f5f5;
38
+ color: #3d3d3d;
39
+ }
40
+
41
+ &[data-fs-bp-autocomplete-dropdown-option-selected="true"] {
42
+ color: #0366dd;
43
+ }
44
+ }
45
+ }