@authhero/widget 0.37.1 → 0.38.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.
Files changed (32) hide show
  1. package/dist/authhero-widget/authhero-widget.esm.js +1 -1
  2. package/dist/authhero-widget/index.esm.js +1 -1
  3. package/dist/authhero-widget/p-b5f14fa6.entry.js +1 -0
  4. package/dist/authhero-widget/p-f94037cd.entry.js +1 -0
  5. package/dist/cjs/authhero-node.cjs.entry.js +458 -13
  6. package/dist/cjs/authhero-widget.cjs.entry.js +79 -10
  7. package/dist/cjs/authhero-widget.cjs.js +1 -1
  8. package/dist/cjs/index.cjs.js +1 -1
  9. package/dist/cjs/loader.cjs.js +1 -1
  10. package/dist/collection/components/authhero-node/authhero-node.css +94 -0
  11. package/dist/collection/components/authhero-node/authhero-node.js +302 -13
  12. package/dist/collection/components/authhero-widget/authhero-widget.js +98 -10
  13. package/dist/collection/utils/date-format.js +176 -0
  14. package/dist/components/authhero-node.js +1 -1
  15. package/dist/components/authhero-widget.js +1 -1
  16. package/dist/components/index.js +1 -1
  17. package/dist/components/p-C9ZfIQiS.js +1 -0
  18. package/dist/esm/authhero-node.entry.js +458 -13
  19. package/dist/esm/authhero-widget.entry.js +79 -10
  20. package/dist/esm/authhero-widget.js +1 -1
  21. package/dist/esm/index.js +1 -1
  22. package/dist/esm/loader.js +1 -1
  23. package/dist/types/components/authhero-node/authhero-node.d.ts +83 -0
  24. package/dist/types/components/authhero-widget/authhero-widget.d.ts +19 -0
  25. package/dist/types/components.d.ts +18 -0
  26. package/dist/types/utils/date-format.d.ts +52 -0
  27. package/hydrate/index.js +541 -24
  28. package/hydrate/index.mjs +541 -24
  29. package/package.json +2 -2
  30. package/dist/authhero-widget/p-cdfc4555.entry.js +0 -1
  31. package/dist/authhero-widget/p-e95c436f.entry.js +0 -1
  32. package/dist/components/p-BP46GHTc.js +0 -1
@@ -525,6 +525,27 @@ const authheroWidgetCss = () => `:host{display:block;font-family:var(--ah-font-f
525
525
  * buttons. Used by long pick-one screens such as tenant selection.
526
526
  */
527
527
  const CHOICE_LIST_SEARCH_THRESHOLD = 5;
528
+ /**
529
+ * Component types the widget renders as something the user can actually fill
530
+ * in. Only these gate the primary action button: types that render nothing
531
+ * (CARDS, FILE, RECAPTCHA, …) can carry `required` in the schema too, and
532
+ * gating on them would leave Continue disabled with no way to satisfy it.
533
+ */
534
+ const FILLABLE_FIELD_TYPES = new Set([
535
+ "TEXT",
536
+ "EMAIL",
537
+ "CODE",
538
+ "PASSWORD",
539
+ "NUMBER",
540
+ "TEL",
541
+ "URL",
542
+ "DATE",
543
+ "BOOLEAN",
544
+ "LEGAL",
545
+ "COUNTRY",
546
+ "DROPDOWN",
547
+ "CHOICE",
548
+ ]);
528
549
  const AuthheroWidget = class {
529
550
  constructor(hostRef) {
530
551
  index.registerInstance(this, hostRef);
@@ -617,6 +638,13 @@ const AuthheroWidget = class {
617
638
  * @default false (same as autoSubmit when not specified)
618
639
  */
619
640
  autoNavigate;
641
+ /**
642
+ * BCP-47 locale for locale-dependent field layout, e.g. whether a DATE
643
+ * field reads DD/MM/YYYY, MM/DD/YYYY or YYYY-MM-DD. Resolve it server-side
644
+ * and pass it in so the server-rendered markup and the hydrated one agree;
645
+ * screen text itself is already localized by the server.
646
+ */
647
+ locale;
620
648
  /**
621
649
  * Internal parsed screen state.
622
650
  */
@@ -717,14 +745,19 @@ const AuthheroWidget = class {
717
745
  initFormDataFromDefaults(screen) {
718
746
  const defaults = {};
719
747
  for (const comp of screen.components || []) {
720
- if ("config" in comp &&
721
- comp.config &&
722
- "default_value" in comp.config &&
723
- comp.config.default_value) {
724
- const val = comp.config.default_value;
725
- if (typeof val === "string" && val !== "") {
726
- defaults[comp.id] = val;
727
- }
748
+ if (!("config" in comp) || !comp.config)
749
+ continue;
750
+ if (!("default_value" in comp.config))
751
+ continue;
752
+ const val = comp.config.default_value;
753
+ if (typeof val === "string" && val !== "") {
754
+ defaults[comp.id] = val;
755
+ }
756
+ else if (typeof val === "boolean") {
757
+ // A BOOLEAN's default decides whether the checkbox renders ticked, so
758
+ // seed the matching value — otherwise a field the user never touches
759
+ // submits nothing and the screen's state is lost.
760
+ defaults[comp.id] = val ? "true" : "false";
728
761
  }
729
762
  }
730
763
  if (Object.keys(defaults).length > 0) {
@@ -1725,6 +1758,11 @@ const AuthheroWidget = class {
1725
1758
  handleButtonClick = (detail) => {
1726
1759
  // If this is a submit button click, trigger form submission
1727
1760
  if (detail.type === "submit") {
1761
+ // Enter in a text field submits too, so the required-field gate has to
1762
+ // live here and not only on the button's disabled state.
1763
+ if (this._screen && this.hasUnfilledRequiredFields(this._screen)) {
1764
+ return;
1765
+ }
1728
1766
  // For GET screens (or missing method), navigate directly — no form submission needed
1729
1767
  if ((!this._screen?.method ||
1730
1768
  this._screen.method.toUpperCase() === "GET") &&
@@ -1913,6 +1951,32 @@ const AuthheroWidget = class {
1913
1951
  isDividerComponent(component) {
1914
1952
  return component.type === "DIVIDER";
1915
1953
  }
1954
+ /**
1955
+ * Whether a required component holds a value the user has supplied.
1956
+ */
1957
+ isRequiredFieldFilled(component) {
1958
+ const value = this.formData[component.id];
1959
+ // Checkboxes are only "filled" when ticked. A BOOLEAN's default_value is
1960
+ // seeded into formData, so it needs no separate handling here.
1961
+ if (component.type === "BOOLEAN" || component.type === "LEGAL") {
1962
+ return value === "true";
1963
+ }
1964
+ return typeof value === "string" && value.trim() !== "";
1965
+ }
1966
+ /**
1967
+ * Whether the screen still has required fields the user has not filled in.
1968
+ * Used to hold the primary action button disabled: the widget submits via
1969
+ * its own handler rather than a native form submit, so the browser's
1970
+ * constraint validation never runs and an empty required field would
1971
+ * otherwise only surface as a server error after a round trip.
1972
+ */
1973
+ hasUnfilledRequiredFields(screen) {
1974
+ return (screen.components ?? []).some((component) => component.visible !== false &&
1975
+ "required" in component &&
1976
+ component.required === true &&
1977
+ FILLABLE_FIELD_TYPES.has(component.type) &&
1978
+ !this.isRequiredFieldFilled(component));
1979
+ }
1916
1980
  /**
1917
1981
  * Visible label of a choice button, used to filter searchable choice lists.
1918
1982
  */
@@ -1948,6 +2012,9 @@ const AuthheroWidget = class {
1948
2012
  const choiceButtons = fieldComponents.filter((c) => c.type === "NEXT_BUTTON");
1949
2013
  const isSearchableChoiceList = choiceButtons.length === fieldComponents.length &&
1950
2014
  choiceButtons.length > CHOICE_LIST_SEARCH_THRESHOLD;
2015
+ // Hold the primary action button disabled until every required field has
2016
+ // a value.
2017
+ const requiredFieldsMissing = this.hasUnfilledRequiredFields(screen);
1951
2018
  const filterQuery = this.listFilter.trim().toLowerCase();
1952
2019
  const visibleChoiceButtons = isSearchableChoiceList && filterQuery
1953
2020
  ? choiceButtons.filter((c) => this.getChoiceButtonText(c).toLowerCase().includes(filterQuery))
@@ -1982,7 +2049,7 @@ const AuthheroWidget = class {
1982
2049
  };
1983
2050
  // Get logo URL from theme.widget (takes precedence) or branding
1984
2051
  const logoUrl = this._theme?.widget?.logo_url || this._branding?.logo_url;
1985
- return (index.h("div", { class: "widget-container", part: "container", "data-authstack-container": true }, index.h("header", { class: "widget-header", part: "header" }, logoUrl && (index.h("div", { class: "logo-wrapper", part: "logo-wrapper" }, index.h("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), screen.title && (index.h("h1", { class: "title", part: "title", innerHTML: sanitizeHtml(screen.title) })), screen.description && (index.h("p", { class: "description", part: "description", innerHTML: sanitizeHtml(screen.description) }))), index.h("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (index.h("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (index.h("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), index.h("form", { onSubmit: this.handleSubmit, action: screen.action, method: screen.method || "POST", part: "form" }, hiddenComponents.map((c) => (index.h("input", { type: "hidden", name: c.id, id: c.id, key: c.id, value: this.formData[c.id] || "" }))), index.h("div", { class: "form-content" }, socialComponents.length > 0 && (index.h("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (index.h("authhero-node", { key: component.id, component: component, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading, exportparts: getExportParts(component) }))))), socialComponents.length > 0 &&
2052
+ return (index.h("div", { class: "widget-container", part: "container", "data-authstack-container": true }, index.h("header", { class: "widget-header", part: "header" }, logoUrl && (index.h("div", { class: "logo-wrapper", part: "logo-wrapper" }, index.h("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), screen.title && (index.h("h1", { class: "title", part: "title", innerHTML: sanitizeHtml(screen.title) })), screen.description && (index.h("p", { class: "description", part: "description", innerHTML: sanitizeHtml(screen.description) }))), index.h("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (index.h("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (index.h("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), index.h("form", { onSubmit: this.handleSubmit, action: screen.action, method: screen.method || "POST", part: "form" }, hiddenComponents.map((c) => (index.h("input", { type: "hidden", name: c.id, id: c.id, key: c.id, value: this.formData[c.id] || "" }))), index.h("div", { class: "form-content" }, socialComponents.length > 0 && (index.h("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (index.h("authhero-node", { key: component.id, component: component, locale: this.locale, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading, exportparts: getExportParts(component) }))))), socialComponents.length > 0 &&
1986
2053
  fieldComponents.length > 0 &&
1987
2054
  hasDivider && (index.h("div", { class: "divider", part: "divider" }, index.h("span", { class: "divider-text" }, dividerText))), index.h("div", { class: "fields-section", part: "fields-section" }, isSearchableChoiceList && (index.h("input", { type: "text", class: "choice-list-search", part: "choice-list-search", placeholder: "Search", "aria-label": "Search", autocomplete: "off", value: this.listFilter, onInput: (e) => (this.listFilter = e.target.value), onKeyDown: (e) => {
1988
2055
  // The search box lives inside the form; Enter would
@@ -1995,7 +2062,9 @@ const AuthheroWidget = class {
1995
2062
  ? "fields-list fields-list-scroll"
1996
2063
  : "fields-list" }, (isSearchableChoiceList
1997
2064
  ? visibleChoiceButtons
1998
- : fieldComponents).map((component) => (index.h("authhero-node", { key: component.id, component: component, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading }))), isSearchableChoiceList &&
2065
+ : fieldComponents).map((component) => (index.h("authhero-node", { key: component.id, component: component, locale: this.locale, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading ||
2066
+ (component.type === "NEXT_BUTTON" &&
2067
+ requiredFieldsMissing) }))), isSearchableChoiceList &&
1999
2068
  visibleChoiceButtons.length === 0 && (index.h("div", { class: "choice-list-empty", part: "choice-list-empty" }, "No matches")))))), screen.links && screen.links.length > 0 && (index.h("div", { class: "links", part: "links" }, screen.links.map((link) => (index.h("span", { class: "link-wrapper", part: "link-wrapper", key: link.id ?? link.href }, link.linkText ? (index.h("span", null, link.text, " ", index.h("a", { href: link.href, class: "link", part: "link", onClick: (e) => this.handleLinkClick(e, {
2000
2069
  id: link.id,
2001
2070
  href: link.href,
@@ -19,7 +19,7 @@ var patchBrowser = () => {
19
19
 
20
20
  patchBrowser().then(async (options) => {
21
21
  await appGlobals.globalScripts();
22
- return index.bootstrapLazy([["authhero-node.cjs",[[513,"authhero-node",{"component":[16],"value":[1],"disabled":[4],"passwordVisible":[32],"selectedCountry":[32],"localPhoneNumber":[32],"countryDropdownOpen":[32],"telEmailMode":[32]},null,{"component":[{"componentChanged":0}],"value":[{"valueChanged":0}]}]]],["authhero-widget.cjs",[[513,"authhero-widget",{"screen":[1],"apiUrl":[1,"api-url"],"baseUrl":[1,"base-url"],"state":[1025],"screenId":[1025,"screen-id"],"authParams":[1,"auth-params"],"statePersistence":[1,"state-persistence"],"storageKey":[1,"storage-key"],"branding":[1],"theme":[1],"loading":[1028],"autoSubmit":[4,"auto-submit"],"autoNavigate":[4,"auto-navigate"],"_screen":[32],"_authParams":[32],"_branding":[32],"_theme":[32],"formData":[32],"listFilter":[32]},null,{"screenId":[{"watchScreenId":0}],"screen":[{"watchScreen":0}],"branding":[{"watchBranding":0}],"theme":[{"watchTheme":0}],"authParams":[{"watchAuthParams":0}]}]]]], options);
22
+ return index.bootstrapLazy([["authhero-node.cjs",[[513,"authhero-node",{"component":[16],"value":[1],"disabled":[4],"locale":[1],"passwordVisible":[32],"selectedCountry":[32],"localPhoneNumber":[32],"countryDropdownOpen":[32],"telEmailMode":[32],"dateSegments":[32]},null,{"component":[{"componentChanged":0}],"value":[{"valueChanged":0}]}]]],["authhero-widget.cjs",[[513,"authhero-widget",{"screen":[1],"apiUrl":[1,"api-url"],"baseUrl":[1,"base-url"],"state":[1025],"screenId":[1025,"screen-id"],"authParams":[1,"auth-params"],"statePersistence":[1,"state-persistence"],"storageKey":[1,"storage-key"],"branding":[1],"theme":[1],"loading":[1028],"autoSubmit":[4,"auto-submit"],"autoNavigate":[4,"auto-navigate"],"locale":[1],"_screen":[32],"_authParams":[32],"_branding":[32],"_theme":[32],"formData":[32],"listFilter":[32]},null,{"screenId":[{"watchScreenId":0}],"screen":[{"watchScreen":0}],"branding":[{"watchBranding":0}],"theme":[{"watchTheme":0}],"authParams":[{"watchAuthParams":0}]}]]]], options);
23
23
  });
24
24
 
25
25
  exports.setNonce = index.setNonce;