@authhero/widget 0.32.42 → 0.34.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.
@@ -855,6 +855,94 @@ const AuthheroWidget = class {
855
855
  }
856
856
  });
857
857
  }
858
+ /**
859
+ * Pending resolvers awaiting the next render, flushed by
860
+ * `componentDidRender`. Lets `swapScreen` wait until the new screen is
861
+ * actually in the DOM before measuring its height.
862
+ */
863
+ pendingRenderResolvers = [];
864
+ componentDidRender() {
865
+ if (this.pendingRenderResolvers.length === 0)
866
+ return;
867
+ const resolvers = this.pendingRenderResolvers;
868
+ this.pendingRenderResolvers = [];
869
+ resolvers.forEach((resolve) => resolve());
870
+ }
871
+ /**
872
+ * Resolves after Stencil flushes the next render, so `swapScreen` waits for
873
+ * the new screen to be in the DOM before measuring its height.
874
+ */
875
+ nextRender() {
876
+ return new Promise((resolve) => {
877
+ let settled = false;
878
+ const done = () => {
879
+ if (settled)
880
+ return;
881
+ settled = true;
882
+ resolve();
883
+ };
884
+ this.pendingRenderResolvers.push(done);
885
+ // Safety net: a screen swap always triggers a render here, but never
886
+ // hang the swap if one somehow doesn't fire — resolve after a couple of
887
+ // frames at the latest.
888
+ requestAnimationFrame(() => requestAnimationFrame(done));
889
+ });
890
+ }
891
+ /**
892
+ * Apply a screen swap, animating the widget card's height to the next
893
+ * screen's size (Stripe-style): lock the current height, swap the content,
894
+ * measure the new natural height, then animate between them. The content
895
+ * itself doesn't fade — the card just resizes.
896
+ *
897
+ * The card (.widget-container) is animated rather than the host because it
898
+ * carries the background, so its resize is visible; clipping its content
899
+ * (not the host) also keeps the card's drop-shadow intact.
900
+ *
901
+ * Degrades to an immediate swap when the Web Animations API is unavailable
902
+ * or the user prefers reduced motion.
903
+ */
904
+ async swapScreen(apply) {
905
+ const card = this.el.shadowRoot?.querySelector(".widget-container");
906
+ const prefersReducedMotion = typeof window.matchMedia === "function" &&
907
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches;
908
+ if (!card || typeof card.animate !== "function" || prefersReducedMotion) {
909
+ apply();
910
+ return;
911
+ }
912
+ // Lock the current height and clip, so swapping the content doesn't jump
913
+ // before the animation runs.
914
+ const startHeight = card.getBoundingClientRect().height;
915
+ card.style.height = `${startHeight}px`;
916
+ card.style.overflow = "hidden";
917
+ apply();
918
+ // Wait for the new screen to render before measuring its height.
919
+ await this.nextRender();
920
+ const cleanup = () => {
921
+ card.style.height = "";
922
+ card.style.overflow = "";
923
+ };
924
+ // Measure the new content's natural height without painting: toggle to
925
+ // auto, read, restore — all synchronous, so there's no visible jump.
926
+ card.style.height = "auto";
927
+ const endHeight = card.getBoundingClientRect().height;
928
+ card.style.height = `${startHeight}px`;
929
+ card.getBoundingClientRect(); // force reflow from startHeight
930
+ if (Math.abs(endHeight - startHeight) < 1) {
931
+ cleanup();
932
+ return;
933
+ }
934
+ const animation = card.animate([{ height: `${startHeight}px` }, { height: `${endHeight}px` }], { duration: 520, easing: "cubic-bezier(0.22, 1, 0.36, 1)" });
935
+ try {
936
+ await animation.finished;
937
+ }
938
+ catch {
939
+ // Animation cancelled (e.g. superseded by a newer swap); the final
940
+ // DOM state is already correct, so just fall through to cleanup.
941
+ }
942
+ finally {
943
+ cleanup();
944
+ }
945
+ }
858
946
  /**
859
947
  * Get the effective autoNavigate value (defaults to autoSubmit if not set)
860
948
  */
@@ -1157,45 +1245,55 @@ const AuthheroWidget = class {
1157
1245
  }
1158
1246
  else if (!response.ok && result.screen) {
1159
1247
  // Handle validation errors (400 response) — preserve user input
1160
- this._screen = result.screen;
1161
- this.initFormDataFromDefaults(result.screen);
1162
- this.screenChange.emit(result.screen);
1163
- this.updateDataScreenAttribute();
1164
- this.focusFirstInput();
1248
+ await this.swapScreen(() => {
1249
+ // Clear loading first so the captured "after" snapshot doesn't
1250
+ // freeze a disabled button into the cross-fade.
1251
+ this.loading = false;
1252
+ this._screen = result.screen;
1253
+ this.initFormDataFromDefaults(result.screen);
1254
+ this.screenChange.emit(result.screen);
1255
+ this.updateDataScreenAttribute();
1256
+ this.focusFirstInput();
1257
+ });
1165
1258
  }
1166
1259
  else if (result.screen) {
1167
1260
  // Next screen (success)
1168
- this._screen = result.screen;
1169
- this.formData = {};
1170
- this.initFormDataFromDefaults(result.screen);
1171
- this.screenChange.emit(result.screen);
1172
- this.updateDataScreenAttribute();
1173
- // Update screenId if returned in response
1174
- if (result.screenId) {
1175
- this.screenId = result.screenId;
1176
- }
1177
- // Persist state (especially for session storage mode)
1178
- this.persistState();
1179
- // Update URL path if navigateUrl is provided (client-side navigation)
1180
- if (result.navigateUrl && this.shouldAutoNavigate) {
1181
- window.history.pushState({ screen: result.screenId, state: this.state }, "", result.navigateUrl);
1182
- }
1183
- // Apply branding if included
1184
- if (result.branding) {
1185
- this._branding = result.branding;
1186
- this.applyThemeStyles();
1187
- }
1188
- // Update state if returned
1189
- if (result.state) {
1190
- this.state = result.state;
1261
+ await this.swapScreen(() => {
1262
+ // Clear loading first so the captured "after" snapshot doesn't
1263
+ // freeze a disabled button into the cross-fade.
1264
+ this.loading = false;
1265
+ this._screen = result.screen;
1266
+ this.formData = {};
1267
+ this.initFormDataFromDefaults(result.screen);
1268
+ this.screenChange.emit(result.screen);
1269
+ this.updateDataScreenAttribute();
1270
+ // Update screenId if returned in response
1271
+ if (result.screenId) {
1272
+ this.screenId = result.screenId;
1273
+ }
1274
+ // Persist state (especially for session storage mode)
1191
1275
  this.persistState();
1192
- }
1193
- // Perform WebAuthn ceremony if present (structured data, not script)
1194
- if (result.ceremony) {
1195
- this.performWebAuthnCeremony(result.ceremony);
1196
- }
1197
- // Focus first input on new screen
1198
- this.focusFirstInput();
1276
+ // Update URL path if navigateUrl is provided (client-side navigation)
1277
+ if (result.navigateUrl && this.shouldAutoNavigate) {
1278
+ window.history.pushState({ screen: result.screenId, state: this.state }, "", result.navigateUrl);
1279
+ }
1280
+ // Apply branding if included
1281
+ if (result.branding) {
1282
+ this._branding = result.branding;
1283
+ this.applyThemeStyles();
1284
+ }
1285
+ // Update state if returned
1286
+ if (result.state) {
1287
+ this.state = result.state;
1288
+ this.persistState();
1289
+ }
1290
+ // Perform WebAuthn ceremony if present (structured data, not script)
1291
+ if (result.ceremony) {
1292
+ this.performWebAuthnCeremony(result.ceremony);
1293
+ }
1294
+ // Focus first input on new screen
1295
+ this.focusFirstInput();
1296
+ });
1199
1297
  }
1200
1298
  else if (result.complete) {
1201
1299
  // Flow complete without redirect