@zag-js/avatar
Version:
Core logic for the avatar widget implemented as a state machine
166 lines (160 loc) • 4.43 kB
JavaScript
;
var anatomy$1 = require('@zag-js/anatomy');
var domQuery = require('@zag-js/dom-query');
var core = require('@zag-js/core');
var types = require('@zag-js/types');
var utils = require('@zag-js/utils');
// src/avatar.anatomy.ts
var anatomy = anatomy$1.createAnatomy("avatar").parts("root", "image", "fallback");
var parts = anatomy.build();
// src/avatar.dom.ts
var getRootId = (ctx) => ctx.ids?.root ?? `avatar:${ctx.id}`;
var getImageId = (ctx) => ctx.ids?.image ?? `avatar:${ctx.id}:image`;
var getFallbackId = (ctx) => ctx.ids?.fallback ?? `avatar:${ctx.id}:fallback`;
var getRootEl = (ctx) => ctx.getById(getRootId(ctx));
var getImageEl = (ctx) => ctx.getById(getImageId(ctx));
// src/avatar.connect.ts
function connect(service, normalize) {
const { state, send, prop, scope } = service;
const loaded = state.matches("loaded");
return {
loaded,
setSrc(src) {
const img = getImageEl(scope);
img?.setAttribute("src", src);
},
setLoaded() {
send({ type: "img.loaded", src: "api" });
},
setError() {
send({ type: "img.error", src: "api" });
},
getRootProps() {
return normalize.element({
...parts.root.attrs,
dir: prop("dir"),
id: getRootId(scope)
});
},
getImageProps() {
return normalize.img({
...parts.image.attrs,
hidden: !loaded,
dir: prop("dir"),
id: getImageId(scope),
"data-state": loaded ? "visible" : "hidden",
onLoad() {
send({ type: "img.loaded", src: "element" });
},
onError() {
send({ type: "img.error", src: "element" });
}
});
},
getFallbackProps() {
return normalize.element({
...parts.fallback.attrs,
dir: prop("dir"),
id: getFallbackId(scope),
hidden: loaded,
"data-state": loaded ? "hidden" : "visible"
});
}
};
}
var machine = core.createMachine({
initialState() {
return "loading";
},
effects: ["trackImageRemoval", "trackSrcChange"],
on: {
"src.change": {
target: "loading"
},
"img.unmount": {
target: "error"
}
},
states: {
loading: {
entry: ["checkImageStatus"],
on: {
"img.loaded": {
target: "loaded",
actions: ["invokeOnLoad"]
},
"img.error": {
target: "error",
actions: ["invokeOnError"]
}
}
},
error: {
on: {
"img.loaded": {
target: "loaded",
actions: ["invokeOnLoad"]
}
}
},
loaded: {
on: {
"img.error": {
target: "error",
actions: ["invokeOnError"]
}
}
}
},
implementations: {
actions: {
invokeOnLoad({ prop }) {
prop("onStatusChange")?.({ status: "loaded" });
},
invokeOnError({ prop }) {
prop("onStatusChange")?.({ status: "error" });
},
checkImageStatus({ send, scope }) {
const imageEl = getImageEl(scope);
if (!imageEl?.complete) return;
const type = hasLoaded(imageEl) ? "img.loaded" : "img.error";
send({ type, src: "ssr" });
}
},
effects: {
trackImageRemoval({ send, scope }) {
const rootEl = getRootEl(scope);
return domQuery.observeChildren(rootEl, {
callback(records) {
const removedNodes = Array.from(records[0].removedNodes);
const removed = removedNodes.find(
(node) => node.nodeType === Node.ELEMENT_NODE && node.matches("[data-scope=avatar][data-part=image]")
);
if (removed) {
send({ type: "img.unmount" });
}
}
});
},
trackSrcChange({ send, scope }) {
const imageEl = getImageEl(scope);
return domQuery.observeAttributes(imageEl, {
attributes: ["src", "srcset"],
callback() {
send({ type: "src.change" });
}
});
}
}
}
});
function hasLoaded(image) {
return image.complete && image.naturalWidth !== 0 && image.naturalHeight !== 0;
}
var props = types.createProps()(["dir", "id", "ids", "onStatusChange", "getRootNode"]);
var splitProps = utils.createSplitProps(props);
exports.anatomy = anatomy;
exports.connect = connect;
exports.machine = machine;
exports.props = props;
exports.splitProps = splitProps;