@availity/mui-favorites 0.1.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/CHANGELOG.md +15 -0
- package/README.md +59 -0
- package/dist/index.d.mts +36 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +356 -0
- package/dist/index.mjs +320 -0
- package/introduction.stories.mdx +7 -0
- package/jest.config.js +7 -0
- package/package.json +59 -0
- package/project.json +41 -0
- package/src/index.ts +2 -0
- package/src/lib/FavoriteHeart.tsx +133 -0
- package/src/lib/Favorites.stories.tsx +43 -0
- package/src/lib/Favorites.test.tsx +273 -0
- package/src/lib/Favorites.tsx +168 -0
- package/src/lib/constants.tsx +9 -0
- package/src/lib/utils.ts +61 -0
- package/tsconfig.json +5 -0
- package/tsconfig.spec.json +10 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
3
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
4
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
5
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
6
|
+
var __spreadValues = (a, b) => {
|
|
7
|
+
for (var prop in b || (b = {}))
|
|
8
|
+
if (__hasOwnProp.call(b, prop))
|
|
9
|
+
__defNormalProp(a, prop, b[prop]);
|
|
10
|
+
if (__getOwnPropSymbols)
|
|
11
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
12
|
+
if (__propIsEnum.call(b, prop))
|
|
13
|
+
__defNormalProp(a, prop, b[prop]);
|
|
14
|
+
}
|
|
15
|
+
return a;
|
|
16
|
+
};
|
|
17
|
+
var __objRest = (source, exclude) => {
|
|
18
|
+
var target = {};
|
|
19
|
+
for (var prop in source)
|
|
20
|
+
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
|
|
21
|
+
target[prop] = source[prop];
|
|
22
|
+
if (source != null && __getOwnPropSymbols)
|
|
23
|
+
for (var prop of __getOwnPropSymbols(source)) {
|
|
24
|
+
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
|
|
25
|
+
target[prop] = source[prop];
|
|
26
|
+
}
|
|
27
|
+
return target;
|
|
28
|
+
};
|
|
29
|
+
var __async = (__this, __arguments, generator) => {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
var fulfilled = (value) => {
|
|
32
|
+
try {
|
|
33
|
+
step(generator.next(value));
|
|
34
|
+
} catch (e) {
|
|
35
|
+
reject(e);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
var rejected = (value) => {
|
|
39
|
+
try {
|
|
40
|
+
step(generator.throw(value));
|
|
41
|
+
} catch (e) {
|
|
42
|
+
reject(e);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
|
46
|
+
step((generator = generator.apply(__this, __arguments)).next());
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// src/lib/Favorites.tsx
|
|
51
|
+
import { createContext, useContext, useEffect, useState, useMemo } from "react";
|
|
52
|
+
import { useQueryClient as useQueryClient2 } from "@tanstack/react-query";
|
|
53
|
+
import avMessages2 from "@availity/message-core";
|
|
54
|
+
|
|
55
|
+
// src/lib/utils.ts
|
|
56
|
+
import avMessages from "@availity/message-core";
|
|
57
|
+
import { avSettingsApi } from "@availity/api-axios";
|
|
58
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
59
|
+
|
|
60
|
+
// src/lib/constants.tsx
|
|
61
|
+
var MAX_FAVORITES = 60;
|
|
62
|
+
var NAV_APP_ID = "Gateway-AvNavigation";
|
|
63
|
+
var AV_INTERNAL_GLOBALS = {
|
|
64
|
+
FAVORITES_UPDATE: "av:favorites:update",
|
|
65
|
+
FAVORITES_CHANGED: "av:favorites:changed",
|
|
66
|
+
MAX_FAVORITES: "av:favorites:maxed",
|
|
67
|
+
MY_TOP_APPS_UPDATED: "av:topApps:updated"
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// src/lib/utils.ts
|
|
71
|
+
var isFavorite = (arg) => Boolean(typeof (arg == null ? void 0 : arg.id) === "string" && typeof (arg == null ? void 0 : arg.pos) === "number");
|
|
72
|
+
var validateFavorites = (unvalidatedFavorites) => {
|
|
73
|
+
const validatedFavorites = Array.isArray(unvalidatedFavorites) ? unvalidatedFavorites == null ? void 0 : unvalidatedFavorites.filter(isFavorite) : [];
|
|
74
|
+
return validatedFavorites;
|
|
75
|
+
};
|
|
76
|
+
var submit = (_0) => __async(void 0, [_0], function* ({ favorites, targetFavoriteId }) {
|
|
77
|
+
const response = yield avSettingsApi.setApplication(NAV_APP_ID, { favorites });
|
|
78
|
+
return { favorites: response.data.favorites, targetFavoriteId };
|
|
79
|
+
});
|
|
80
|
+
var getFavorites = () => __async(void 0, null, function* () {
|
|
81
|
+
var _a, _b, _c;
|
|
82
|
+
const result = yield avSettingsApi.getApplication(NAV_APP_ID);
|
|
83
|
+
const unvalidatedFavorites = (_c = (_b = (_a = result == null ? void 0 : result.data) == null ? void 0 : _a.settings) == null ? void 0 : _b[0]) == null ? void 0 : _c.favorites;
|
|
84
|
+
const validatedFavorites = validateFavorites(unvalidatedFavorites);
|
|
85
|
+
return validatedFavorites;
|
|
86
|
+
});
|
|
87
|
+
var useFavoritesQuery = () => useQuery(["favorites"], getFavorites);
|
|
88
|
+
var useSubmitFavorites = ({ onMutationStart }) => {
|
|
89
|
+
const queryClient = useQueryClient();
|
|
90
|
+
const _a = useMutation(submit, {
|
|
91
|
+
onMutate(variables) {
|
|
92
|
+
onMutationStart == null ? void 0 : onMutationStart(variables.targetFavoriteId);
|
|
93
|
+
},
|
|
94
|
+
onSuccess(data) {
|
|
95
|
+
queryClient.setQueryData(["favorites"], data.favorites);
|
|
96
|
+
}
|
|
97
|
+
}), { mutateAsync: submitFavorites } = _a, rest = __objRest(_a, ["mutateAsync"]);
|
|
98
|
+
return __spreadValues({ submitFavorites }, rest);
|
|
99
|
+
};
|
|
100
|
+
var sendUpdateMessage = (favorites) => {
|
|
101
|
+
avMessages.send({ favorites, event: AV_INTERNAL_GLOBALS.FAVORITES_UPDATE });
|
|
102
|
+
};
|
|
103
|
+
var openMaxModal = () => avMessages.send(AV_INTERNAL_GLOBALS.MAX_FAVORITES);
|
|
104
|
+
|
|
105
|
+
// src/lib/Favorites.tsx
|
|
106
|
+
import { jsx } from "react/jsx-runtime";
|
|
107
|
+
var FavoritesContext = createContext(null);
|
|
108
|
+
var FavoritesProvider = ({
|
|
109
|
+
children,
|
|
110
|
+
onFavoritesChange
|
|
111
|
+
}) => {
|
|
112
|
+
const [lastClickedFavoriteId, setLastClickedFavoriteId] = useState("");
|
|
113
|
+
const queryClient = useQueryClient2();
|
|
114
|
+
const { data: favorites, status: queryStatus } = useFavoritesQuery();
|
|
115
|
+
const { submitFavorites, status: mutationStatus } = useSubmitFavorites({
|
|
116
|
+
onMutationStart(targetFavoriteId) {
|
|
117
|
+
setLastClickedFavoriteId(targetFavoriteId);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
const unsubscribeFavoritesChanged = avMessages2.subscribe(
|
|
122
|
+
AV_INTERNAL_GLOBALS.FAVORITES_CHANGED,
|
|
123
|
+
(data) => {
|
|
124
|
+
if (data == null ? void 0 : data.favorites) {
|
|
125
|
+
queryClient.setQueryData(["favorites"], data == null ? void 0 : data.favorites);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
{ ignoreSameWindow: false }
|
|
129
|
+
);
|
|
130
|
+
const unsubscribeFavoritesUpdate = avMessages2.subscribe(
|
|
131
|
+
AV_INTERNAL_GLOBALS.FAVORITES_UPDATE,
|
|
132
|
+
(data) => {
|
|
133
|
+
if (data == null ? void 0 : data.favorites) {
|
|
134
|
+
queryClient.setQueryData(["favorites"], data == null ? void 0 : data.favorites);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
{ ignoreSameWindow: false }
|
|
138
|
+
);
|
|
139
|
+
return () => {
|
|
140
|
+
unsubscribeFavoritesChanged();
|
|
141
|
+
unsubscribeFavoritesUpdate();
|
|
142
|
+
};
|
|
143
|
+
}, [queryClient]);
|
|
144
|
+
const deleteFavorite = (id) => __async(void 0, null, function* () {
|
|
145
|
+
if (favorites) {
|
|
146
|
+
const response = yield submitFavorites({
|
|
147
|
+
favorites: favorites.filter((favorite) => favorite.id !== id),
|
|
148
|
+
targetFavoriteId: id
|
|
149
|
+
});
|
|
150
|
+
sendUpdateMessage(response.favorites);
|
|
151
|
+
onFavoritesChange == null ? void 0 : onFavoritesChange(response.favorites);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
const addFavorite = (id) => __async(void 0, null, function* () {
|
|
155
|
+
if (!favorites)
|
|
156
|
+
return false;
|
|
157
|
+
if (favorites.length >= MAX_FAVORITES) {
|
|
158
|
+
openMaxModal();
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
const maxFavorite = favorites.reduce((accum, fave) => {
|
|
162
|
+
if (!accum || fave.pos > accum.pos) {
|
|
163
|
+
accum = fave;
|
|
164
|
+
}
|
|
165
|
+
return accum;
|
|
166
|
+
}, null);
|
|
167
|
+
const newFavPos = maxFavorite ? maxFavorite.pos + 1 : 0;
|
|
168
|
+
const response = yield submitFavorites({
|
|
169
|
+
favorites: [...favorites, { id, pos: newFavPos }],
|
|
170
|
+
targetFavoriteId: id
|
|
171
|
+
});
|
|
172
|
+
sendUpdateMessage(response.favorites);
|
|
173
|
+
onFavoritesChange == null ? void 0 : onFavoritesChange(response.favorites);
|
|
174
|
+
const isFavorited = response.favorites.find((f) => f.id === id);
|
|
175
|
+
return !!isFavorited;
|
|
176
|
+
});
|
|
177
|
+
return /* @__PURE__ */ jsx(
|
|
178
|
+
FavoritesContext.Provider,
|
|
179
|
+
{
|
|
180
|
+
value: {
|
|
181
|
+
favorites,
|
|
182
|
+
queryStatus,
|
|
183
|
+
mutationStatus,
|
|
184
|
+
lastClickedFavoriteId,
|
|
185
|
+
deleteFavorite,
|
|
186
|
+
addFavorite
|
|
187
|
+
},
|
|
188
|
+
children
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
};
|
|
192
|
+
var noOp = () => {
|
|
193
|
+
};
|
|
194
|
+
var useFavorites = (id) => {
|
|
195
|
+
const context = useContext(FavoritesContext);
|
|
196
|
+
if (context === null) {
|
|
197
|
+
throw new Error("useFavorites must be used within a FavoritesProvider");
|
|
198
|
+
}
|
|
199
|
+
const { favorites, queryStatus, mutationStatus, lastClickedFavoriteId, deleteFavorite, addFavorite } = context;
|
|
200
|
+
const isLastClickedFavorite = lastClickedFavoriteId === id;
|
|
201
|
+
const isFavorited = useMemo(() => {
|
|
202
|
+
const fav = favorites == null ? void 0 : favorites.find((f) => f.id === id);
|
|
203
|
+
return !!fav;
|
|
204
|
+
}, [favorites, id]);
|
|
205
|
+
const toggleFavorite = () => isFavorited ? deleteFavorite(id) : addFavorite(id);
|
|
206
|
+
const isDisabled = queryStatus === "loading" || queryStatus === "idle" || mutationStatus === "loading";
|
|
207
|
+
let status = "initLoading";
|
|
208
|
+
if (queryStatus === "loading")
|
|
209
|
+
status = "initLoading";
|
|
210
|
+
if (mutationStatus === "loading")
|
|
211
|
+
status = "reloading";
|
|
212
|
+
if (queryStatus === "error" || mutationStatus === "error")
|
|
213
|
+
status = "error";
|
|
214
|
+
if (queryStatus === "success" && (mutationStatus === "success" || mutationStatus === "idle"))
|
|
215
|
+
status = "success";
|
|
216
|
+
return {
|
|
217
|
+
isFavorited,
|
|
218
|
+
status,
|
|
219
|
+
isLastClickedFavorite,
|
|
220
|
+
toggleFavorite: isDisabled ? noOp : toggleFavorite
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// src/lib/FavoriteHeart.tsx
|
|
225
|
+
import { Tooltip } from "@availity/mui-tooltip";
|
|
226
|
+
import { HeartIcon, HeartEmptyIcon } from "@availity/mui-icon";
|
|
227
|
+
import { CircularProgress } from "@availity/mui-progress";
|
|
228
|
+
import { styled } from "@mui/material/styles";
|
|
229
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
230
|
+
var icons = {
|
|
231
|
+
spinner: /* @__PURE__ */ jsx2(CircularProgress, { "aria-hidden": true, size: "small", loadingCaption: false }),
|
|
232
|
+
unknownDisabledHeart: /* @__PURE__ */ jsx2(HeartIcon, { "aria-hidden": true, color: "disabled" }),
|
|
233
|
+
favoritedDisabledHeart: /* @__PURE__ */ jsx2(HeartIcon, { "aria-hidden": true, color: "error", opacity: "0.6" }),
|
|
234
|
+
unfavoritedDisabledHeart: /* @__PURE__ */ jsx2(HeartEmptyIcon, { "aria-hidden": true, color: "disabled", opacity: "0.6" }),
|
|
235
|
+
favoritedHeart: /* @__PURE__ */ jsx2(HeartIcon, { "aria-hidden": true, color: "error" }),
|
|
236
|
+
unfavoritedHeart: /* @__PURE__ */ jsx2(HeartEmptyIcon, { "aria-hidden": true, color: "secondary" })
|
|
237
|
+
};
|
|
238
|
+
var FavoriteHeartContainer = styled("div", { name: "AvFavoriteHeart", slot: "root" })({});
|
|
239
|
+
var FavoriteInput = styled("input", {
|
|
240
|
+
name: "AvFavoriteHeart",
|
|
241
|
+
slot: "input"
|
|
242
|
+
})({});
|
|
243
|
+
var FavoriteIcon = styled("div", {
|
|
244
|
+
name: "AvFavoriteHeart",
|
|
245
|
+
slot: "icon"
|
|
246
|
+
})({});
|
|
247
|
+
var FavoriteHeart = ({
|
|
248
|
+
id,
|
|
249
|
+
name,
|
|
250
|
+
onChange,
|
|
251
|
+
onMouseDown,
|
|
252
|
+
disabled = false
|
|
253
|
+
}) => {
|
|
254
|
+
const { isFavorited, isLastClickedFavorite, status, toggleFavorite } = useFavorites(id);
|
|
255
|
+
const handleChange = (event) => {
|
|
256
|
+
onChange == null ? void 0 : onChange(isFavorited, event);
|
|
257
|
+
toggleFavorite();
|
|
258
|
+
};
|
|
259
|
+
const handleKeyPress = (event) => {
|
|
260
|
+
if (event.code === "Enter" || event.key === "Enter") {
|
|
261
|
+
onChange == null ? void 0 : onChange(isFavorited, event);
|
|
262
|
+
toggleFavorite();
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
const iconKey = (() => {
|
|
266
|
+
if (status === "initLoading")
|
|
267
|
+
return "unknownDisabledHeart";
|
|
268
|
+
if (status === "reloading") {
|
|
269
|
+
if (isLastClickedFavorite)
|
|
270
|
+
return "spinner";
|
|
271
|
+
return isFavorited ? "favoritedDisabledHeart" : "unfavoritedDisabledHeart";
|
|
272
|
+
}
|
|
273
|
+
if (disabled) {
|
|
274
|
+
return isFavorited ? "favoritedDisabledHeart" : "unfavoritedDisabledHeart";
|
|
275
|
+
}
|
|
276
|
+
if (isFavorited)
|
|
277
|
+
return "favoritedHeart";
|
|
278
|
+
return "unfavoritedHeart";
|
|
279
|
+
})();
|
|
280
|
+
const cursor = disabled || !isLastClickedFavorite && (status === "initLoading" || status === "reloading") ? "not-allowed" : void 0;
|
|
281
|
+
const tooltipContent = `${isFavorited ? "Remove from" : "Add to"} My Favorites`;
|
|
282
|
+
const favoriteInputProps = {
|
|
283
|
+
onKeyDown: handleKeyPress,
|
|
284
|
+
type: "checkbox",
|
|
285
|
+
"aria-label": `Favorite ${name}`,
|
|
286
|
+
id: `av-favorite-heart-${id}`,
|
|
287
|
+
disabled,
|
|
288
|
+
checked: isFavorited,
|
|
289
|
+
onChange: handleChange,
|
|
290
|
+
onMouseDown,
|
|
291
|
+
style: { cursor }
|
|
292
|
+
};
|
|
293
|
+
return /* @__PURE__ */ jsxs(FavoriteHeartContainer, { children: [
|
|
294
|
+
/* @__PURE__ */ jsx2(FavoriteIcon, { children: icons[iconKey] }),
|
|
295
|
+
/* @__PURE__ */ jsx2(
|
|
296
|
+
"span",
|
|
297
|
+
{
|
|
298
|
+
style: {
|
|
299
|
+
position: "absolute",
|
|
300
|
+
width: "1px",
|
|
301
|
+
height: "1px",
|
|
302
|
+
padding: 0,
|
|
303
|
+
margin: "-1px",
|
|
304
|
+
overflow: "hidden",
|
|
305
|
+
clip: "rect(0,0,0,0)",
|
|
306
|
+
whiteSpace: "nowrap",
|
|
307
|
+
border: 0
|
|
308
|
+
},
|
|
309
|
+
"aria-live": isLastClickedFavorite && (status === "reloading" || status === "error") ? "polite" : "off",
|
|
310
|
+
children: isLastClickedFavorite && status === "reloading" ? "Loading..." : isLastClickedFavorite && status === "error" ? "An error has occurred. Please try again." : ""
|
|
311
|
+
}
|
|
312
|
+
),
|
|
313
|
+
disabled ? /* @__PURE__ */ jsx2(FavoriteInput, __spreadValues({}, favoriteInputProps)) : /* @__PURE__ */ jsx2(Tooltip, { title: tooltipContent, placement: "top", children: /* @__PURE__ */ jsx2(FavoriteInput, __spreadValues({}, favoriteInputProps)) })
|
|
314
|
+
] });
|
|
315
|
+
};
|
|
316
|
+
export {
|
|
317
|
+
FavoriteHeart,
|
|
318
|
+
FavoritesProvider,
|
|
319
|
+
useFavorites
|
|
320
|
+
};
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@availity/mui-favorites",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Availity MUI Favorites Component - part of the @availity/element design system",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"typescript",
|
|
8
|
+
"availity",
|
|
9
|
+
"mui"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://availity.github.io/element/?path=/docs/components-favorites-introduction--docs",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Availity/element/issues"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/Availity/element.git",
|
|
18
|
+
"directory": "packages/favorites"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "Availity Developers <AVOSS@availity.com>",
|
|
22
|
+
"browser": "./dist/index.js",
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"module": "./dist/index.mjs",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
28
|
+
"dev": "tsup src/index.ts --format esm,cjs --watch --dts",
|
|
29
|
+
"clean": "rm -rf dist",
|
|
30
|
+
"clean:nm": "rm -rf node_modules",
|
|
31
|
+
"publish": "yarn npm publish --tolerate-republish --access public",
|
|
32
|
+
"publish:canary": "yarn npm publish --access public --tag canary"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@availity/api-axios": "^8.0.8",
|
|
36
|
+
"@availity/message-core": "^6.1.3",
|
|
37
|
+
"@tanstack/react-query": "^4.36.1"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@availity/mui-icon": "^0.8.2",
|
|
41
|
+
"@availity/mui-progress": "^0.1.8",
|
|
42
|
+
"@availity/mui-tooltip": "^0.5.8",
|
|
43
|
+
"@mui/material": "^5.15.15",
|
|
44
|
+
"react": "18.2.0",
|
|
45
|
+
"react-dom": "18.2.0",
|
|
46
|
+
"tsup": "^8.0.2",
|
|
47
|
+
"typescript": "^5.4.5"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"@availity/mui-icon": "^0.8.2",
|
|
51
|
+
"@availity/mui-progress": "^0.1.8",
|
|
52
|
+
"@availity/mui-tooltip": "^0.5.8",
|
|
53
|
+
"@mui/material": "^5.11.9",
|
|
54
|
+
"react": ">=16.3.0"
|
|
55
|
+
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/project.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mui-favorites",
|
|
3
|
+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "packages/favorites/src",
|
|
5
|
+
"projectType": "library",
|
|
6
|
+
"tags": [],
|
|
7
|
+
"targets": {
|
|
8
|
+
"lint": {
|
|
9
|
+
"executor": "@nx/eslint:lint",
|
|
10
|
+
"options": {
|
|
11
|
+
"eslintConfig": ".eslintrc.json",
|
|
12
|
+
"silent": false,
|
|
13
|
+
"fix": false,
|
|
14
|
+
"cache": true,
|
|
15
|
+
"cacheLocation": "./node_modules/.cache/favorites/.eslintcache",
|
|
16
|
+
"maxWarnings": -1,
|
|
17
|
+
"quiet": false,
|
|
18
|
+
"noEslintrc": false,
|
|
19
|
+
"hasTypeAwareRules": true,
|
|
20
|
+
"cacheStrategy": "metadata"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"test": {
|
|
24
|
+
"executor": "@nx/jest:jest",
|
|
25
|
+
"outputs": ["{workspaceRoot}/coverage/favorites"],
|
|
26
|
+
"options": {
|
|
27
|
+
"jestConfig": "packages/favorites/jest.config.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"version": {
|
|
31
|
+
"executor": "@jscutlery/semver:version",
|
|
32
|
+
"options": {
|
|
33
|
+
"preset": "conventional",
|
|
34
|
+
"commitMessageFormat": "chore({projectName}): release version ${version} [skip ci]",
|
|
35
|
+
"tagPrefix": "@availity/{projectName}@",
|
|
36
|
+
"trackDeps": true,
|
|
37
|
+
"skipCommitTypes": ["docs"]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Tooltip } from '@availity/mui-tooltip';
|
|
3
|
+
import { HeartIcon, HeartEmptyIcon } from '@availity/mui-icon';
|
|
4
|
+
import { CircularProgress } from '@availity/mui-progress';
|
|
5
|
+
import { styled } from '@mui/material/styles';
|
|
6
|
+
import { useFavorites } from './Favorites';
|
|
7
|
+
|
|
8
|
+
const icons = {
|
|
9
|
+
spinner: <CircularProgress aria-hidden size="small" loadingCaption={false} />,
|
|
10
|
+
unknownDisabledHeart: <HeartIcon aria-hidden color="disabled" />,
|
|
11
|
+
favoritedDisabledHeart: <HeartIcon aria-hidden color="error" opacity="0.6" />,
|
|
12
|
+
unfavoritedDisabledHeart: <HeartEmptyIcon aria-hidden color="disabled" opacity="0.6" />,
|
|
13
|
+
favoritedHeart: <HeartIcon aria-hidden color="error" />,
|
|
14
|
+
unfavoritedHeart: <HeartEmptyIcon aria-hidden color="secondary" />,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const FavoriteHeartContainer = styled('div', { name: 'AvFavoriteHeart', slot: 'root' })({});
|
|
18
|
+
const FavoriteInput = styled('input', {
|
|
19
|
+
name: 'AvFavoriteHeart',
|
|
20
|
+
slot: 'input',
|
|
21
|
+
})({});
|
|
22
|
+
|
|
23
|
+
const FavoriteIcon = styled('div', {
|
|
24
|
+
name: 'AvFavoriteHeart',
|
|
25
|
+
slot: 'icon',
|
|
26
|
+
})({});
|
|
27
|
+
|
|
28
|
+
type FavoriteHeartProps = {
|
|
29
|
+
/** The configuration's id */
|
|
30
|
+
id: string;
|
|
31
|
+
/** The name of the configuration */
|
|
32
|
+
name: string;
|
|
33
|
+
/** What to do on Favorite Toggle */
|
|
34
|
+
onChange?: (
|
|
35
|
+
isFavorited: boolean,
|
|
36
|
+
event: React.ChangeEvent<HTMLInputElement> | React.KeyboardEvent<HTMLInputElement>
|
|
37
|
+
) => void;
|
|
38
|
+
/** What to do on click */
|
|
39
|
+
onMouseDown?: (event: React.MouseEvent<HTMLInputElement, MouseEvent>) => void;
|
|
40
|
+
/** Whether or not the Favorite is disabled
|
|
41
|
+
* @default false
|
|
42
|
+
*/
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const FavoriteHeart = ({
|
|
47
|
+
id,
|
|
48
|
+
name,
|
|
49
|
+
onChange,
|
|
50
|
+
onMouseDown,
|
|
51
|
+
disabled = false,
|
|
52
|
+
}: FavoriteHeartProps): JSX.Element => {
|
|
53
|
+
const { isFavorited, isLastClickedFavorite, status, toggleFavorite } = useFavorites(id);
|
|
54
|
+
|
|
55
|
+
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
56
|
+
onChange?.(isFavorited, event);
|
|
57
|
+
toggleFavorite();
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
61
|
+
if (event.code === 'Enter' || event.key === 'Enter') {
|
|
62
|
+
onChange?.(isFavorited, event);
|
|
63
|
+
toggleFavorite();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const iconKey = (() => {
|
|
68
|
+
if (status === 'initLoading') return 'unknownDisabledHeart';
|
|
69
|
+
|
|
70
|
+
if (status === 'reloading') {
|
|
71
|
+
if (isLastClickedFavorite) return 'spinner';
|
|
72
|
+
return isFavorited ? 'favoritedDisabledHeart' : 'unfavoritedDisabledHeart';
|
|
73
|
+
}
|
|
74
|
+
if (disabled) {
|
|
75
|
+
return isFavorited ? 'favoritedDisabledHeart' : 'unfavoritedDisabledHeart';
|
|
76
|
+
}
|
|
77
|
+
if (isFavorited) return 'favoritedHeart';
|
|
78
|
+
return 'unfavoritedHeart';
|
|
79
|
+
})();
|
|
80
|
+
|
|
81
|
+
const cursor =
|
|
82
|
+
disabled || (!isLastClickedFavorite && (status === 'initLoading' || status === 'reloading'))
|
|
83
|
+
? 'not-allowed'
|
|
84
|
+
: undefined;
|
|
85
|
+
|
|
86
|
+
const tooltipContent = `${isFavorited ? 'Remove from' : 'Add to'} My Favorites`;
|
|
87
|
+
|
|
88
|
+
const favoriteInputProps = {
|
|
89
|
+
onKeyDown: handleKeyPress,
|
|
90
|
+
type: 'checkbox',
|
|
91
|
+
'aria-label': `Favorite ${name}`,
|
|
92
|
+
id: `av-favorite-heart-${id}`,
|
|
93
|
+
disabled,
|
|
94
|
+
checked: isFavorited,
|
|
95
|
+
onChange: handleChange,
|
|
96
|
+
onMouseDown,
|
|
97
|
+
style: { cursor },
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<FavoriteHeartContainer>
|
|
102
|
+
<FavoriteIcon>{icons[iconKey]}</FavoriteIcon>
|
|
103
|
+
<span
|
|
104
|
+
style={{
|
|
105
|
+
position: 'absolute',
|
|
106
|
+
width: '1px',
|
|
107
|
+
height: '1px',
|
|
108
|
+
padding: 0,
|
|
109
|
+
margin: '-1px',
|
|
110
|
+
overflow: 'hidden',
|
|
111
|
+
clip: 'rect(0,0,0,0)',
|
|
112
|
+
whiteSpace: 'nowrap',
|
|
113
|
+
border: 0,
|
|
114
|
+
}}
|
|
115
|
+
aria-live={isLastClickedFavorite && (status === 'reloading' || status === 'error') ? 'polite' : 'off'}
|
|
116
|
+
>
|
|
117
|
+
{isLastClickedFavorite && status === 'reloading'
|
|
118
|
+
? 'Loading...'
|
|
119
|
+
: isLastClickedFavorite && status === 'error'
|
|
120
|
+
? 'An error has occurred. Please try again.'
|
|
121
|
+
: ''}
|
|
122
|
+
</span>
|
|
123
|
+
|
|
124
|
+
{disabled ? (
|
|
125
|
+
<FavoriteInput {...favoriteInputProps} />
|
|
126
|
+
) : (
|
|
127
|
+
<Tooltip title={tooltipContent} placement="top">
|
|
128
|
+
<FavoriteInput {...favoriteInputProps} />
|
|
129
|
+
</Tooltip>
|
|
130
|
+
)}
|
|
131
|
+
</FavoriteHeartContainer>
|
|
132
|
+
);
|
|
133
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { StoryObj } from '@storybook/react';
|
|
2
|
+
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
|
|
3
|
+
|
|
4
|
+
import { FavoritesProvider } from './Favorites';
|
|
5
|
+
import { FavoriteHeart } from './FavoriteHeart';
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
title: 'Components/Favorites',
|
|
9
|
+
component: FavoriteHeart,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const storyFavorites = [
|
|
13
|
+
{ id: '123', pos: 0, name: 'App #1' },
|
|
14
|
+
{ id: '456', pos: 0, name: 'A retired app' },
|
|
15
|
+
{ id: '789', pos: 0, name: 'Another retired app' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export const _FavoriteHeart: StoryObj = {
|
|
19
|
+
render: () => {
|
|
20
|
+
return (
|
|
21
|
+
<QueryClientProvider
|
|
22
|
+
client={
|
|
23
|
+
new QueryClient({
|
|
24
|
+
defaultOptions: {
|
|
25
|
+
queries: {
|
|
26
|
+
refetchOnWindowFocus: false,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
>
|
|
32
|
+
<FavoritesProvider>
|
|
33
|
+
{storyFavorites.map((fav) => (
|
|
34
|
+
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }} key={fav.id}>
|
|
35
|
+
<FavoriteHeart id={fav.id} name={fav.name} disabled={fav.name.includes('retired')} />
|
|
36
|
+
<div>{fav.name}</div>
|
|
37
|
+
</div>
|
|
38
|
+
))}
|
|
39
|
+
</FavoritesProvider>
|
|
40
|
+
</QueryClientProvider>
|
|
41
|
+
);
|
|
42
|
+
},
|
|
43
|
+
};
|