@qssolutions/ssas-registration-form 2.0.0 → 2.0.2

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/dist/index.js CHANGED
@@ -23314,24 +23314,72 @@ const createFormTheme = (primaryColor = DEFAULT_PRIMARY_COLOR, fonts) => {
23314
23314
  * `primaryColor`/`fonts` props. The library is embedded in pages we don't control, so the provider
23315
23315
  * lives here rather than relying on a host one. `display: contents` keeps it out of the layout.
23316
23316
  */
23317
- const FormThemeProvider = ({ primaryColor, fonts, children }) => {
23317
+ const FormThemeProvider = ({ primaryColor, fonts, dir = "ltr", children }) => {
23318
23318
  const fontFamily = fonts?.family;
23319
23319
  const theme = React.useMemo(() => createFormTheme(primaryColor, { family: fontFamily }), [primaryColor, fontFamily]);
23320
- return (React.createElement(reactComponents.FluentProvider, { theme: theme, style: { display: "contents" } }, children));
23320
+ return (React.createElement(reactComponents.FluentProvider, { theme: theme, dir: dir, style: { display: "contents" } }, children));
23321
23321
  };
23322
23322
 
23323
+ /**
23324
+ * Deep merge utility
23325
+ */
23326
+ /**
23327
+ * Check if current language is RTL
23328
+ */
23329
+ function isRTLContent(languageCode) {
23330
+ const rtlLanguages = ['ar', 'he'];
23331
+ if (languageCode) {
23332
+ return rtlLanguages.includes(languageCode.toLowerCase());
23333
+ }
23334
+ // Try to detect from document or i18n
23335
+ return false;
23336
+ }
23337
+
23338
+ const INVALID = "[aria-invalid='true']";
23323
23339
  /**
23324
23340
  * v9 replacements for the v8 `textFieldStyles` / `dropdownStyles` / `comboboxStyles` objects
23325
23341
  * (`defaultStyles.ts`). The font family now comes from the `FluentProvider` theme
23326
23342
  * (`createFormTheme`), so only the box styling lives here.
23327
23343
  */
23328
23344
  const useFieldStyles = reactComponents.makeStyles({
23329
- // v8 `fieldGroup`: grey fill, 1px #DDD border, 5px radius, 50px high.
23345
+ // Mirrors SharedPortal's `inputRoot` (`form-screen.styles.ts`): a near-white fill with a #8A8A8A edge
23346
+ // (over 3:1, WCAG's minimum for a control's boundary), 5px radius, 50px high. The old #F5F5F5 fill
23347
+ // and #DDDDDD edge read as disabled, so that look is kept for real disabled fields only.
23330
23348
  input: {
23331
- backgroundColor: "#F5F5F5",
23332
- ...reactComponents.shorthands.border("1px", "solid", "#DDDDDD"),
23349
+ backgroundColor: "#FCFCFF",
23350
+ ...reactComponents.shorthands.border("1px", "solid", "#8A8A8A"),
23333
23351
  ...reactComponents.shorthands.borderRadius("5px"),
23334
23352
  height: "50px",
23353
+ transitionProperty: "border-color, background-color",
23354
+ transitionDuration: "0.15s",
23355
+ // Text and placeholder at 16px. The `<input>` and the Dropdown's button carry v9's own 14px
23356
+ // class, which a font size on the root does not reach.
23357
+ "& input, & .fui-Dropdown__button": {
23358
+ fontSize: "16px",
23359
+ },
23360
+ // The field being typed in turns white, so it stands out from the rest.
23361
+ ":focus-within": {
23362
+ backgroundColor: "#fff",
23363
+ },
23364
+ // Hover and focus are skipped on an invalid field, so its red edge always shows. `aria-invalid`
23365
+ // is what Field puts on the control when its `validationState` is "error": on the input, or
23366
+ // on the button for Dropdown. Hover also skips the focused field: Griffel emits `:hover` rules
23367
+ // after `:focus-within` ones, so the brand edge would otherwise turn grey under the pointer.
23368
+ [`:hover:not(:focus-within):not(${INVALID}):not(:has(${INVALID})):not(:has(:disabled))`]: {
23369
+ ...reactComponents.shorthands.borderColor("#424242"),
23370
+ },
23371
+ [`:focus-within:not(${INVALID}):not(:has(${INVALID}))`]: {
23372
+ ...reactComponents.shorthands.borderColor(reactComponents.tokens.colorBrandStroke1),
23373
+ },
23374
+ // v9 only draws its red edge while the field is not focused, and its own `:focus-within` grey
23375
+ // outranks a plain rule here; `!important` (as in SharedPortal) keeps the red on focus too.
23376
+ [`&${INVALID}, &:has(${INVALID})`]: {
23377
+ ...reactComponents.shorthands.borderColor(`${reactComponents.tokens.colorPaletteRedBorder2} !important`),
23378
+ },
23379
+ "&:has(:disabled)": {
23380
+ backgroundColor: "#F5F5F5",
23381
+ ...reactComponents.shorthands.borderColor("#DDDDDD"),
23382
+ },
23335
23383
  },
23336
23384
  // v9 Dropdown/Combobox roots are inline-block with a 250-400px width window; v8's filled its column.
23337
23385
  fill: {
@@ -23486,7 +23534,7 @@ const FormComponentList = (translation, performByPartner, model) => {
23486
23534
  const SSASTextInput = React.forwardRef(({ label, errorMessage, hint, fonts, required, className, ...props }, ref) => {
23487
23535
  const styles = useFieldStyles();
23488
23536
  return (React.createElement(reactComponents.Field, { label: label ? { children: label, style: fieldLabelStyle(fonts) } : undefined, hint: hint, required: !!required, validationState: errorMessage ? "error" : "none", validationMessage: errorMessage },
23489
- React.createElement(reactComponents.Input, { ref: ref, className: reactComponents.mergeClasses(styles.input, className), ...props })));
23537
+ React.createElement(reactComponents.Input, { ref: ref, className: reactComponents.mergeClasses(styles.input, styles.fill, className), ...props })));
23490
23538
  });
23491
23539
  SSASTextInput.displayName = "SSASTextInput";
23492
23540
 
@@ -23598,21 +23646,43 @@ const ControlledDropdown = ({ name, control, rules, defaultValue, options, ...pr
23598
23646
  };
23599
23647
 
23600
23648
  /**
23601
- * Ported from SharedPortal's `ControlledComboBox` (`Field` + `Combobox freeform`). v8's `allowFreeInput`
23602
- * had no per-call-site toggle, so `freeform` stays unconditional. The form value is `option.key` when it
23603
- * is a string, else `option.text`, as before; the country list has string keys.
23649
+ * Ported from SharedPortal's `ControlledComboBox` (`Field` + `Combobox freeform`). Typed text is kept
23650
+ * apart from the field (`typedText`) and resolved on blur: an exact (case-insensitive) option match
23651
+ * selects that option, anything else clears both, so only a listed option can ever reach the form and
23652
+ * the `required` rule still fails on unmatched text. The form value is `option.key` when it is a string,
23653
+ * else `option.text`; the country list has string keys.
23604
23654
  */
23605
- const ControlledCombobox = ({ name, control, rules, defaultValue, options, label, placeholder, required, disabled, autoComplete, style, className, fonts, }) => {
23655
+ const ComboboxField = ({ value, onChange, onBlur, errorMessage, options, label, placeholder, required, disabled, autoComplete, style, className, fonts, }) => {
23606
23656
  const styles = useFieldStyles();
23607
- return (React.createElement(reactHookForm.Controller, { name: name, control: control, rules: rules, defaultValue: defaultValue || "", render: ({ field: { onChange, onBlur, value }, fieldState: { error } }) => {
23608
- const matchedOption = options.find((o) => String(o.key) === String(value));
23609
- return (React.createElement(reactComponents.Field, { label: label ? { children: label, style: fieldLabelStyle(fonts) } : undefined, required: required, validationState: error ? "error" : "none", validationMessage: error?.message },
23610
- React.createElement(reactComponents.Combobox, { freeform: true, value: matchedOption ? matchedOption.text : value ?? "", selectedOptions: matchedOption ? [String(matchedOption.key)] : [], placeholder: placeholder, disabled: disabled, autoComplete: autoComplete, style: style, className: reactComponents.mergeClasses(styles.input, styles.fill, className), onOptionSelect: (_, data) => {
23611
- const option = options.find((o) => String(o.key) === data.optionValue);
23612
- if (option)
23613
- onChange(typeof option.key === "string" ? option.key : option.text);
23614
- }, onChange: (e) => onChange(e.target.value), onBlur: onBlur }, options.map((option) => (React.createElement(reactComponents.Option, { key: option.key, value: String(option.key), text: option.text }, option.text))))));
23615
- } }));
23657
+ const matchedOption = options.find((o) => String(o.key) === String(value));
23658
+ // `null` while not typing, so the input shows the selected option's text. A ref, because Tab both
23659
+ // selects the highlighted option and blurs in one go — the blur handler must see that selection.
23660
+ const typedTextRef = React.useRef(null);
23661
+ const [typedText, setTypedText] = React.useState(null);
23662
+ const updateTypedText = (text) => {
23663
+ typedTextRef.current = text;
23664
+ setTypedText(text);
23665
+ };
23666
+ const commit = (option) => {
23667
+ onChange(option ? (typeof option.key === "string" ? option.key : option.text) : "");
23668
+ updateTypedText(null);
23669
+ };
23670
+ return (React.createElement(reactComponents.Field, { label: label ? { children: label, style: fieldLabelStyle(fonts) } : undefined, required: required, validationState: errorMessage ? "error" : "none", validationMessage: errorMessage },
23671
+ React.createElement(reactComponents.Combobox, { freeform: true, value: typedText ?? matchedOption?.text ?? "", selectedOptions: matchedOption ? [String(matchedOption.key)] : [], placeholder: placeholder, disabled: disabled, autoComplete: autoComplete, style: style, className: reactComponents.mergeClasses(styles.input, styles.fill, className), onOptionSelect: (_, data) => {
23672
+ const option = options.find((o) => String(o.key) === data.optionValue);
23673
+ if (option)
23674
+ commit(option);
23675
+ }, onChange: (e) => updateTypedText(e.target.value), onBlur: () => {
23676
+ const text = typedTextRef.current;
23677
+ if (text !== null) {
23678
+ const search = text.trim().toLowerCase();
23679
+ commit(search ? options.find((o) => o.text.toLowerCase() === search) : undefined);
23680
+ }
23681
+ onBlur();
23682
+ } }, options.map((option) => (React.createElement(reactComponents.Option, { key: option.key, value: String(option.key), text: option.text }, option.text))))));
23683
+ };
23684
+ const ControlledCombobox = ({ name, control, rules, defaultValue, ...props }) => {
23685
+ return (React.createElement(reactHookForm.Controller, { name: name, control: control, rules: rules, defaultValue: defaultValue || "", render: ({ field: { onChange, onBlur, value }, fieldState: { error } }) => (React.createElement(ComboboxField, { ...props, value: value, onChange: onChange, onBlur: onBlur, errorMessage: error?.message })) }));
23616
23686
  };
23617
23687
 
23618
23688
  // eslint-disable-next-line @typescript-eslint/no-namespace
@@ -23756,21 +23826,6 @@ const FormComponentRenderer = ({ formModels, isRegistering, control, setValue, s
23756
23826
  return (React.createElement(React.Fragment, null, renderList.map((component) => renderByType(component, component.name))));
23757
23827
  };
23758
23828
 
23759
- /**
23760
- * Deep merge utility
23761
- */
23762
- /**
23763
- * Check if current language is RTL
23764
- */
23765
- function isRTLContent(languageCode) {
23766
- const rtlLanguages = ['ar', 'he'];
23767
- if (languageCode) {
23768
- return rtlLanguages.includes(languageCode.toLowerCase());
23769
- }
23770
- // Try to detect from document or i18n
23771
- return false;
23772
- }
23773
-
23774
23829
  // v8 ChoiceGroup root: 30px under the group.
23775
23830
  const useRadioStyles = reactComponents.makeStyles({
23776
23831
  root: {
@@ -23780,7 +23835,6 @@ const useRadioStyles = reactComponents.makeStyles({
23780
23835
  const UpperFormSection = ({ isRegistering, performByPartner, setPerformByPartner, setValue, setError, clearErrors, control, errors, trigger, watch, translation, config, apiService, styles, countryOptions, orgSizingOptions, orgIndustryOptions, hearAboutOptions, partnerData, primaryColor: primaryColorProp, }) => {
23781
23836
  const primaryColor = primaryColorProp || "#00BAE4";
23782
23837
  const radioStyles = useRadioStyles();
23783
- const isRTL = isRTLContent(partnerData?.languageCode);
23784
23838
  const choiceGroupOptions = [
23785
23839
  { key: "ByCustomer", text: translation.ByCustomer },
23786
23840
  { key: "ByPartner", text: translation.ByPartner },
@@ -23806,9 +23860,7 @@ const UpperFormSection = ({ isRegistering, performByPartner, setPerformByPartner
23806
23860
  isHearAbout ? exports.FormModel.HearAbout : false,
23807
23861
  ];
23808
23862
  return (React.createElement(React.Fragment, null,
23809
- React.createElement(reactComponents.RadioGroup, { defaultValue: "ByCustomer", className: radioStyles.root,
23810
- // v8's `flexDirection: row-reverse` pushed the options to the right edge for RTL
23811
- style: isRTL ? { alignItems: "flex-end" } : undefined, onChange: (_e, data) => {
23863
+ React.createElement(reactComponents.RadioGroup, { defaultValue: "ByCustomer", className: radioStyles.root, onChange: (_e, data) => {
23812
23864
  // Reset the partner fields
23813
23865
  setValue(exports.FormModel.CustomerPartnerMail, "");
23814
23866
  setValue(exports.FormModel.CustomerPartnerName, "");
@@ -23851,6 +23903,15 @@ const useStyles = reactComponents.makeStyles({
23851
23903
  ":active:where(:not(:disabled, [aria-disabled='true']))": {
23852
23904
  backgroundColor: `color-mix(in srgb, var(${BRAND_VAR}), #000 15%) !important`,
23853
23905
  },
23906
+ // v9 draws its keyboard focus ring with `border` and `box-shadow`, and the inline `border: none`
23907
+ // and drop shadow here overwrite both, so a focused button showed no ring at all. Redrawn as an
23908
+ // outline (which nothing inline sets) plus an inner white ring, readable on light and dark
23909
+ // backgrounds alike.
23910
+ ":focus-visible": {
23911
+ outline: "2px solid #000 !important",
23912
+ outlineOffset: "2px",
23913
+ boxShadow: "inset 0 0 0 2px #fff !important",
23914
+ },
23854
23915
  },
23855
23916
  });
23856
23917
  const baseStyle = (noShadow, disabled) => ({
@@ -23883,20 +23944,26 @@ const iconFor = (name, disabled) => {
23883
23944
  };
23884
23945
  const SSASPrimaryButton = (props) => {
23885
23946
  const classes = useStyles();
23947
+ // From `FormThemeProvider`, which sets it from the form's language.
23948
+ const { dir } = reactComponents.useFluent();
23886
23949
  const { styles, primaryColor, noShadow, iconProps, noIcon, iconName, loadingIcon, onRenderMenuIcon, text, className, style, disabled, type, onClick, children, } = props;
23887
23950
  const color = primaryColor || DEFAULT_PRIMARY_COLOR;
23888
23951
  const isDisabled = disabled || loadingIcon;
23952
+ // While loading the button is blocked but stays focusable (`aria-disabled`, not `disabled`): a truly
23953
+ // disabled button drops keyboard focus to the page body, so a visitor who pressed Enter on it
23954
+ // would be thrown back to the top of the tab order.
23955
+ const isBlockedWhileLoading = !!loadingIcon && !disabled;
23889
23956
  const brandStyle = {
23890
23957
  [BRAND_VAR]: color,
23891
23958
  ...(isDisabled ? {} : { backgroundColor: `var(${BRAND_VAR})`, color: "#fff" }),
23892
23959
  };
23893
23960
  const mergedStyle = { ...brandStyle, ...baseStyle(noShadow, isDisabled), ...styles?.root, ...style };
23894
23961
  // v8 rendered a trailing chevron by default, direction-aware, unless noIcon was set.
23895
- const defaultIconName = isRTLContent() ? "ChevronLeft" : "ChevronRight";
23962
+ const defaultIconName = dir === "rtl" ? "ChevronLeft" : "ChevronRight";
23896
23963
  const defaultIcon = iconFor(iconName ?? defaultIconName, isDisabled);
23897
23964
  // `onRenderMenuIcon` is checked first: the caller implements its own loading-spinner-vs-default
23898
23965
  // logic and calls `defaultRender()` on the non-loading branch.
23899
- const icon = onRenderMenuIcon ? (onRenderMenuIcon(props, () => defaultIcon ?? null) ?? undefined) : loadingIcon ? (React__namespace.createElement(reactComponents.Spinner, { size: "tiny" })) : noIcon ? undefined : (defaultIcon);
23966
+ const icon = onRenderMenuIcon ? (onRenderMenuIcon(props, () => defaultIcon ?? null) ?? undefined) : loadingIcon ? (React__namespace.createElement(reactComponents.Spinner, { "aria-label": "Loading", size: "tiny" })) : noIcon ? undefined : (defaultIcon);
23900
23967
  const iconPosition = iconProps ? "before" : "after";
23901
23968
  // A trailing icon makes the content lopsided; extra start padding rebalances it, and
23902
23969
  // `mergedStyle` goes last so a call site's own padding still wins.
@@ -23904,7 +23971,7 @@ const SSASPrimaryButton = (props) => {
23904
23971
  ...(icon && iconPosition === "after" ? { paddingInlineStart: 20 } : {}),
23905
23972
  ...mergedStyle,
23906
23973
  };
23907
- return (React__namespace.createElement(reactComponents.Button, { appearance: "primary", className: reactComponents.mergeClasses(classes.interactive, className), style: balancedStyle, icon: icon, iconPosition: iconPosition, disabled: isDisabled, type: type, onClick: onClick }, text ?? children));
23974
+ return (React__namespace.createElement(reactComponents.Button, { appearance: "primary", className: reactComponents.mergeClasses(classes.interactive, className), style: balancedStyle, icon: icon, iconPosition: iconPosition, disabled: disabled, disabledFocusable: isBlockedWhileLoading, type: type, onClick: onClick }, text ?? children));
23908
23975
  };
23909
23976
 
23910
23977
  const LowerFormSection = ({ setValue, isRegistering, errors, translation, submissionError, primaryColor: primaryColorProp, partnerData, }) => {
@@ -23949,7 +24016,7 @@ const LowerFormSection = ({ setValue, isRegistering, errors, translation, submis
23949
24016
  const renderLoadingIcon = (_props, defaultRender) => {
23950
24017
  // v8 `SpinnerSize.medium` (20px) maps to v9 "tiny"
23951
24018
  if (isRegistering)
23952
- return React.createElement(reactComponents.Spinner, { size: "tiny" });
24019
+ return React.createElement(reactComponents.Spinner, { "aria-label": "Submitting", size: "tiny" });
23953
24020
  else
23954
24021
  return defaultRender ? defaultRender() : null;
23955
24022
  };
@@ -23975,7 +24042,9 @@ const LowerFormSection = ({ setValue, isRegistering, errors, translation, submis
23975
24042
  React.createElement("span", { style: { fontSize: 16, fontWeight: 600 } }, "\u26A0"),
23976
24043
  React.createElement("span", null, submissionError))),
23977
24044
  React.createElement(SSASPrimaryButton, { text: translation.SubmitRequest, primaryColor: primaryColor, styles: {
23978
- root: { display: "block", width: "100%" },
24045
+ // `flex`, not `block`: v9 Button centres its icon and label as flex items, and
24046
+ // `block` dropped the icon onto the text baseline, a few px above the label.
24047
+ root: { display: "flex", width: "100%" },
23979
24048
  }, type: "submit", disabled: Object.keys(errors).length > 0 ||
23980
24049
  isRegistering ||
23981
24050
  !termAcceptance ||
@@ -24046,6 +24115,8 @@ const SSASRegistrationForm = ({ config, }) => {
24046
24115
  return (languageOptions.find((x) => x.data.code === currentLang)?.text ||
24047
24116
  "English");
24048
24117
  }, [mergedConfig.languageCode, partnerData?.languageCode, languageOptions]);
24118
+ // Same language the translations are loaded in (`getFormScreenLocalization` below).
24119
+ const formDir = isRTLContent(mergedConfig.languageCode || partnerData?.languageCode) ? "rtl" : "ltr";
24049
24120
  const { handleSubmit, setValue, setError, control, watch, clearErrors, trigger, formState: { errors }, } = reactHookForm.useForm({
24050
24121
  defaultValues: {
24051
24122
  orgSizingId: 1,
@@ -24284,7 +24355,7 @@ const SSASRegistrationForm = ({ config, }) => {
24284
24355
  // Show success screen if registration completed
24285
24356
  if (finalStep) {
24286
24357
  // Default success screen
24287
- return (React.createElement(FormThemeProvider, { primaryColor: mergedConfig.theme?.primaryColor || colorWidget?.column1, fonts: mergedConfig.theme?.fonts },
24358
+ return (React.createElement(FormThemeProvider, { primaryColor: mergedConfig.theme?.primaryColor || colorWidget?.column1, fonts: mergedConfig.theme?.fonts, dir: formDir },
24288
24359
  React.createElement("div", { className: styles.mainContainer, style: mainContainerStyle },
24289
24360
  React.createElement("div", { className: styles.thankyouTopWrapper },
24290
24361
  React.createElement("div", { className: styles.finalStepContainer },
@@ -24293,6 +24364,7 @@ const SSASRegistrationForm = ({ config, }) => {
24293
24364
  fontSize: mergedConfig.theme?.fonts?.sizes
24294
24365
  ?.heading,
24295
24366
  fontWeight: 700,
24367
+ lineHeight: 1.2,
24296
24368
  } }, translation.ThankYou),
24297
24369
  React.createElement("div", { style: {
24298
24370
  fontSize: mergedConfig.theme?.fonts?.sizes
@@ -24309,7 +24381,7 @@ const SSASRegistrationForm = ({ config, }) => {
24309
24381
  textUnderlinePosition: "under",
24310
24382
  } }, finalStep)))))));
24311
24383
  }
24312
- return (React.createElement(FormThemeProvider, { primaryColor: mergedConfig.theme?.primaryColor || colorWidget?.column1, fonts: mergedConfig.theme?.fonts },
24384
+ return (React.createElement(FormThemeProvider, { primaryColor: mergedConfig.theme?.primaryColor || colorWidget?.column1, fonts: mergedConfig.theme?.fonts, dir: formDir },
24313
24385
  React.createElement("div", { className: styles.mainContainer, style: mainContainerStyle },
24314
24386
  React.createElement("div", { className: styles.topWrapper, style: mergedConfig.customStyles?.topWrapper }, isLoadingData ? (React.createElement(reactComponents.Spinner, { size: "tiny", labelPosition: "below", label: "Loading form data...", style: { marginBottom: "20px" } })) : partnerLoadFailed ? (React.createElement("div", { style: {
24315
24387
  textAlign: "center",
@@ -24320,6 +24392,7 @@ const SSASRegistrationForm = ({ config, }) => {
24320
24392
  fontSize: mergedConfig.theme?.fonts?.sizes?.heading ||
24321
24393
  24,
24322
24394
  fontWeight: 700,
24395
+ lineHeight: 1.2,
24323
24396
  marginBottom: 16,
24324
24397
  } }, "Access Not Available"),
24325
24398
  React.createElement("div", { style: {
@@ -24335,6 +24408,7 @@ const SSASRegistrationForm = ({ config, }) => {
24335
24408
  fontSize: mergedConfig.theme?.fonts?.sizes?.heading ||
24336
24409
  24,
24337
24410
  fontWeight: 700,
24411
+ lineHeight: 1.2,
24338
24412
  marginBottom: 16,
24339
24413
  } }, "Domain Not Whitelisted"),
24340
24414
  React.createElement("div", { style: {
@@ -24347,6 +24421,7 @@ const SSASRegistrationForm = ({ config, }) => {
24347
24421
  fontSize: mergedConfig.theme?.fonts?.sizes
24348
24422
  ?.heading,
24349
24423
  fontWeight: 700,
24424
+ lineHeight: 1.2,
24350
24425
  } }, translation.AppForm),
24351
24426
  React.createElement("div", { style: {
24352
24427
  fontSize: mergedConfig.theme?.fonts?.sizes