@authhero/widget 0.37.0 → 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
@@ -523,6 +523,27 @@ const authheroWidgetCss = () => `:host{display:block;font-family:var(--ah-font-f
523
523
  * buttons. Used by long pick-one screens such as tenant selection.
524
524
  */
525
525
  const CHOICE_LIST_SEARCH_THRESHOLD = 5;
526
+ /**
527
+ * Component types the widget renders as something the user can actually fill
528
+ * in. Only these gate the primary action button: types that render nothing
529
+ * (CARDS, FILE, RECAPTCHA, …) can carry `required` in the schema too, and
530
+ * gating on them would leave Continue disabled with no way to satisfy it.
531
+ */
532
+ const FILLABLE_FIELD_TYPES = new Set([
533
+ "TEXT",
534
+ "EMAIL",
535
+ "CODE",
536
+ "PASSWORD",
537
+ "NUMBER",
538
+ "TEL",
539
+ "URL",
540
+ "DATE",
541
+ "BOOLEAN",
542
+ "LEGAL",
543
+ "COUNTRY",
544
+ "DROPDOWN",
545
+ "CHOICE",
546
+ ]);
526
547
  const AuthheroWidget = class {
527
548
  constructor(hostRef) {
528
549
  registerInstance(this, hostRef);
@@ -615,6 +636,13 @@ const AuthheroWidget = class {
615
636
  * @default false (same as autoSubmit when not specified)
616
637
  */
617
638
  autoNavigate;
639
+ /**
640
+ * BCP-47 locale for locale-dependent field layout, e.g. whether a DATE
641
+ * field reads DD/MM/YYYY, MM/DD/YYYY or YYYY-MM-DD. Resolve it server-side
642
+ * and pass it in so the server-rendered markup and the hydrated one agree;
643
+ * screen text itself is already localized by the server.
644
+ */
645
+ locale;
618
646
  /**
619
647
  * Internal parsed screen state.
620
648
  */
@@ -715,14 +743,19 @@ const AuthheroWidget = class {
715
743
  initFormDataFromDefaults(screen) {
716
744
  const defaults = {};
717
745
  for (const comp of screen.components || []) {
718
- if ("config" in comp &&
719
- comp.config &&
720
- "default_value" in comp.config &&
721
- comp.config.default_value) {
722
- const val = comp.config.default_value;
723
- if (typeof val === "string" && val !== "") {
724
- defaults[comp.id] = val;
725
- }
746
+ if (!("config" in comp) || !comp.config)
747
+ continue;
748
+ if (!("default_value" in comp.config))
749
+ continue;
750
+ const val = comp.config.default_value;
751
+ if (typeof val === "string" && val !== "") {
752
+ defaults[comp.id] = val;
753
+ }
754
+ else if (typeof val === "boolean") {
755
+ // A BOOLEAN's default decides whether the checkbox renders ticked, so
756
+ // seed the matching value — otherwise a field the user never touches
757
+ // submits nothing and the screen's state is lost.
758
+ defaults[comp.id] = val ? "true" : "false";
726
759
  }
727
760
  }
728
761
  if (Object.keys(defaults).length > 0) {
@@ -1723,6 +1756,11 @@ const AuthheroWidget = class {
1723
1756
  handleButtonClick = (detail) => {
1724
1757
  // If this is a submit button click, trigger form submission
1725
1758
  if (detail.type === "submit") {
1759
+ // Enter in a text field submits too, so the required-field gate has to
1760
+ // live here and not only on the button's disabled state.
1761
+ if (this._screen && this.hasUnfilledRequiredFields(this._screen)) {
1762
+ return;
1763
+ }
1726
1764
  // For GET screens (or missing method), navigate directly — no form submission needed
1727
1765
  if ((!this._screen?.method ||
1728
1766
  this._screen.method.toUpperCase() === "GET") &&
@@ -1911,6 +1949,32 @@ const AuthheroWidget = class {
1911
1949
  isDividerComponent(component) {
1912
1950
  return component.type === "DIVIDER";
1913
1951
  }
1952
+ /**
1953
+ * Whether a required component holds a value the user has supplied.
1954
+ */
1955
+ isRequiredFieldFilled(component) {
1956
+ const value = this.formData[component.id];
1957
+ // Checkboxes are only "filled" when ticked. A BOOLEAN's default_value is
1958
+ // seeded into formData, so it needs no separate handling here.
1959
+ if (component.type === "BOOLEAN" || component.type === "LEGAL") {
1960
+ return value === "true";
1961
+ }
1962
+ return typeof value === "string" && value.trim() !== "";
1963
+ }
1964
+ /**
1965
+ * Whether the screen still has required fields the user has not filled in.
1966
+ * Used to hold the primary action button disabled: the widget submits via
1967
+ * its own handler rather than a native form submit, so the browser's
1968
+ * constraint validation never runs and an empty required field would
1969
+ * otherwise only surface as a server error after a round trip.
1970
+ */
1971
+ hasUnfilledRequiredFields(screen) {
1972
+ return (screen.components ?? []).some((component) => component.visible !== false &&
1973
+ "required" in component &&
1974
+ component.required === true &&
1975
+ FILLABLE_FIELD_TYPES.has(component.type) &&
1976
+ !this.isRequiredFieldFilled(component));
1977
+ }
1914
1978
  /**
1915
1979
  * Visible label of a choice button, used to filter searchable choice lists.
1916
1980
  */
@@ -1946,6 +2010,9 @@ const AuthheroWidget = class {
1946
2010
  const choiceButtons = fieldComponents.filter((c) => c.type === "NEXT_BUTTON");
1947
2011
  const isSearchableChoiceList = choiceButtons.length === fieldComponents.length &&
1948
2012
  choiceButtons.length > CHOICE_LIST_SEARCH_THRESHOLD;
2013
+ // Hold the primary action button disabled until every required field has
2014
+ // a value.
2015
+ const requiredFieldsMissing = this.hasUnfilledRequiredFields(screen);
1949
2016
  const filterQuery = this.listFilter.trim().toLowerCase();
1950
2017
  const visibleChoiceButtons = isSearchableChoiceList && filterQuery
1951
2018
  ? choiceButtons.filter((c) => this.getChoiceButtonText(c).toLowerCase().includes(filterQuery))
@@ -1980,7 +2047,7 @@ const AuthheroWidget = class {
1980
2047
  };
1981
2048
  // Get logo URL from theme.widget (takes precedence) or branding
1982
2049
  const logoUrl = this._theme?.widget?.logo_url || this._branding?.logo_url;
1983
- return (h("div", { class: "widget-container", part: "container", "data-authstack-container": true }, h("header", { class: "widget-header", part: "header" }, logoUrl && (h("div", { class: "logo-wrapper", part: "logo-wrapper" }, h("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), screen.title && (h("h1", { class: "title", part: "title", innerHTML: sanitizeHtml(screen.title) })), screen.description && (h("p", { class: "description", part: "description", innerHTML: sanitizeHtml(screen.description) }))), h("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (h("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (h("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), h("form", { onSubmit: this.handleSubmit, action: screen.action, method: screen.method || "POST", part: "form" }, hiddenComponents.map((c) => (h("input", { type: "hidden", name: c.id, id: c.id, key: c.id, value: this.formData[c.id] || "" }))), h("div", { class: "form-content" }, socialComponents.length > 0 && (h("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (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 &&
2050
+ return (h("div", { class: "widget-container", part: "container", "data-authstack-container": true }, h("header", { class: "widget-header", part: "header" }, logoUrl && (h("div", { class: "logo-wrapper", part: "logo-wrapper" }, h("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), screen.title && (h("h1", { class: "title", part: "title", innerHTML: sanitizeHtml(screen.title) })), screen.description && (h("p", { class: "description", part: "description", innerHTML: sanitizeHtml(screen.description) }))), h("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (h("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (h("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), h("form", { onSubmit: this.handleSubmit, action: screen.action, method: screen.method || "POST", part: "form" }, hiddenComponents.map((c) => (h("input", { type: "hidden", name: c.id, id: c.id, key: c.id, value: this.formData[c.id] || "" }))), h("div", { class: "form-content" }, socialComponents.length > 0 && (h("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (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 &&
1984
2051
  fieldComponents.length > 0 &&
1985
2052
  hasDivider && (h("div", { class: "divider", part: "divider" }, h("span", { class: "divider-text" }, dividerText))), h("div", { class: "fields-section", part: "fields-section" }, isSearchableChoiceList && (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) => {
1986
2053
  // The search box lives inside the form; Enter would
@@ -1993,7 +2060,9 @@ const AuthheroWidget = class {
1993
2060
  ? "fields-list fields-list-scroll"
1994
2061
  : "fields-list" }, (isSearchableChoiceList
1995
2062
  ? visibleChoiceButtons
1996
- : fieldComponents).map((component) => (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 &&
2063
+ : fieldComponents).map((component) => (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 ||
2064
+ (component.type === "NEXT_BUTTON" &&
2065
+ requiredFieldsMissing) }))), isSearchableChoiceList &&
1997
2066
  visibleChoiceButtons.length === 0 && (h("div", { class: "choice-list-empty", part: "choice-list-empty" }, "No matches")))))), screen.links && screen.links.length > 0 && (h("div", { class: "links", part: "links" }, screen.links.map((link) => (h("span", { class: "link-wrapper", part: "link-wrapper", key: link.id ?? link.href }, link.linkText ? (h("span", null, link.text, " ", h("a", { href: link.href, class: "link", part: "link", onClick: (e) => this.handleLinkClick(e, {
1998
2067
  id: link.id,
1999
2068
  href: link.href,
@@ -17,5 +17,5 @@ var patchBrowser = () => {
17
17
 
18
18
  patchBrowser().then(async (options) => {
19
19
  await globalScripts();
20
- return bootstrapLazy([["authhero-node",[[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",[[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);
20
+ return bootstrapLazy([["authhero-node",[[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",[[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);
21
21
  });