@simplybusiness/mobius 6.3.2 → 6.3.3

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/esm/index.js CHANGED
@@ -4423,7 +4423,7 @@ var ExpandableText = forwardRef55((props, ref) => {
4423
4423
  setIsCollapsed(isOverflowing);
4424
4424
  }, [text, shouldCollapse, maxLines]);
4425
4425
  if (breakpoint && !shouldCollapse) {
4426
- return /* @__PURE__ */ jsx70("div", { ref, className, ...otherProps, children: /* @__PURE__ */ jsx70(Text, { ...textProps, children: text }) });
4426
+ return /* @__PURE__ */ jsx70("div", { ref, className, ...otherProps, children: /* @__PURE__ */ jsx70(Text, { ...textProps, children: /* @__PURE__ */ jsx70("span", { dangerouslySetInnerHTML: { __html: text } }) }) });
4427
4427
  }
4428
4428
  const handleAccordionChange = (expanded) => {
4429
4429
  setIsExpanded(expanded);
@@ -4452,7 +4452,7 @@ var ExpandableText = forwardRef55((props, ref) => {
4452
4452
  style: textContainerStyle,
4453
4453
  "data-testid": "expandable-text-content",
4454
4454
  "aria-describedby": isCollapsed ? expandButtonId : void 0,
4455
- children: /* @__PURE__ */ jsx70(Text, { elementType: "span", ...textProps, children: text })
4455
+ children: /* @__PURE__ */ jsx70(Text, { elementType: "span", ...textProps, children: /* @__PURE__ */ jsx70("span", { dangerouslySetInnerHTML: { __html: text } }) })
4456
4456
  }
4457
4457
  ),
4458
4458
  isCollapsed && /* @__PURE__ */ jsx70(
@@ -4476,7 +4476,7 @@ var ExpandableText = forwardRef55((props, ref) => {
4476
4476
  ExpandableText.displayName = "ExpandableText";
4477
4477
 
4478
4478
  // src/components/MaskedField/MaskedField.tsx
4479
- import { forwardRef as forwardRef56 } from "react";
4479
+ import { forwardRef as forwardRef56, useEffect as useEffect26 } from "react";
4480
4480
  import { useIMask } from "react-imask";
4481
4481
  import { jsx as jsx71 } from "react/jsx-runtime";
4482
4482
  var createSyntheticEvent = (options) => {
@@ -4511,10 +4511,17 @@ var MaskedField = forwardRef56((props, ref) => {
4511
4511
  const {
4512
4512
  ref: maskRef,
4513
4513
  value: maskedValue,
4514
- unmaskedValue
4514
+ unmaskedValue,
4515
+ setValue
4515
4516
  } = useIMask(mask, {
4516
- defaultValue
4517
+ defaultValue: value || defaultValue
4517
4518
  });
4519
+ useEffect26(() => {
4520
+ const valueToCompare = useMaskedValue ? maskedValue : unmaskedValue;
4521
+ if (value !== void 0 && value !== valueToCompare) {
4522
+ setValue(value);
4523
+ }
4524
+ }, [value, setValue]);
4518
4525
  const handleChange = (event) => {
4519
4526
  if (onChange) {
4520
4527
  onChange(
@@ -33,6 +33,9 @@ export type LoqateAddressDetailsItem = {
33
33
  Line2: string;
34
34
  PostalCode: string;
35
35
  Label: string;
36
+ SubBuilding?: string;
37
+ BuildingNumber?: string;
38
+ Street?: string;
36
39
  };
37
40
  export type LoqateAddressDetailsResponse = {
38
41
  Items: LoqateAddressDetailsItem[];
@@ -5,7 +5,7 @@ import type { AccordionProps } from "../Accordion/Accordion";
5
5
  export type ExpandableTextElementType = HTMLDivElement;
6
6
  export type ExpandableTextRef = React.Ref<ExpandableTextElementType>;
7
7
  export interface ExpandableTextProps extends DOMProps {
8
- /** The text content to display */
8
+ /** The text content to display (can be plain text or HTML) */
9
9
  text: string;
10
10
  /** Maximum number of lines to show when collapsed */
11
11
  maxLines?: number;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@simplybusiness/mobius",
3
3
  "license": "UNLICENSED",
4
- "version": "6.3.2",
4
+ "version": "6.3.3",
5
5
  "description": "Core library of Mobius react components",
6
6
  "repository": {
7
7
  "type": "git",
@@ -39,6 +39,10 @@ export type LoqateAddressDetailsItem = {
39
39
  Line2: string;
40
40
  PostalCode: string;
41
41
  Label: string;
42
+ // US only fields
43
+ SubBuilding?: string;
44
+ BuildingNumber?: string;
45
+ Street?: string;
42
46
  };
43
47
 
44
48
  export type LoqateAddressDetailsResponse = {
@@ -1,4 +1,4 @@
1
- import { render, screen } from "@testing-library/react";
1
+ import { render, screen, within } from "@testing-library/react";
2
2
  import userEvent from "@testing-library/user-event";
3
3
  import { ExpandableText } from "./ExpandableText";
4
4
 
@@ -184,9 +184,15 @@ describe("ExpandableText", () => {
184
184
  />,
185
185
  );
186
186
 
187
- const textElement = screen.getByText(longText);
188
- expect(textElement).toBeInTheDocument();
189
- expect(textElement).toHaveClass("custom-text-class");
187
+ // Verify the text is rendered within an element that has all the custom classes
188
+ const content = screen.getByTestId("expandable-text-content");
189
+
190
+ // The Text component with custom classes should be within the content
191
+ const textWithCustomClass = within(content).getByText(longText, {
192
+ selector: ".mobius-text.custom-text-class.--is-small *",
193
+ });
194
+
195
+ expect(textWithCustomClass).toBeInTheDocument();
190
196
  });
191
197
 
192
198
  it("passes accordionProps to Accordion component", async () => {
@@ -12,7 +12,7 @@ export type ExpandableTextElementType = HTMLDivElement;
12
12
  export type ExpandableTextRef = React.Ref<ExpandableTextElementType>;
13
13
 
14
14
  export interface ExpandableTextProps extends DOMProps {
15
- /** The text content to display */
15
+ /** The text content to display (can be plain text or HTML) */
16
16
  text: string;
17
17
  /** Maximum number of lines to show when collapsed */
18
18
  maxLines?: number;
@@ -77,7 +77,9 @@ export const ExpandableText: ForwardedRefComponent<
77
77
  if (breakpoint && !shouldCollapse) {
78
78
  return (
79
79
  <div ref={ref} className={className} {...otherProps}>
80
- <Text {...textProps}>{text}</Text>
80
+ <Text {...textProps}>
81
+ <span dangerouslySetInnerHTML={{ __html: text }} />
82
+ </Text>
81
83
  </div>
82
84
  );
83
85
  }
@@ -112,7 +114,7 @@ export const ExpandableText: ForwardedRefComponent<
112
114
  aria-describedby={isCollapsed ? expandButtonId : undefined}
113
115
  >
114
116
  <Text elementType="span" {...textProps}>
115
- {text}
117
+ <span dangerouslySetInnerHTML={{ __html: text }} />
116
118
  </Text>
117
119
  </div>
118
120
  {isCollapsed && (
@@ -2,6 +2,7 @@ import React from "react";
2
2
  import { render, screen } from "@testing-library/react";
3
3
  import userEvent from "@testing-library/user-event";
4
4
  import { MaskedField } from ".";
5
+ import type { ReactMaskOpts } from "react-imask";
5
6
 
6
7
  const commaSeparatedNumberMask = {
7
8
  mask: Number,
@@ -17,6 +18,30 @@ const usPhoneNumberMask = {
17
18
  const WRAPPER_CLASS_NAME = "mobius-text-field";
18
19
  const INPUT_CLASS_NAME = "mobius-text-field__input";
19
20
 
21
+ // Helper component for controlled component tests
22
+ const TestComponent = ({
23
+ value,
24
+ mask = usPhoneNumberMask,
25
+ useMaskedValue,
26
+ label = "Phone Number",
27
+ ...props
28
+ }: {
29
+ value: string;
30
+ mask?: ReactMaskOpts;
31
+ useMaskedValue?: boolean;
32
+ label?: string;
33
+ [key: string]: unknown;
34
+ }) => (
35
+ <MaskedField
36
+ mask={mask}
37
+ label={label}
38
+ data-testid="masked-field"
39
+ value={value}
40
+ useMaskedValue={useMaskedValue}
41
+ {...props}
42
+ />
43
+ );
44
+
20
45
  describe("MaskedField", () => {
21
46
  it("should render without errors", () => {
22
47
  render(
@@ -390,6 +415,126 @@ describe("MaskedField", () => {
390
415
  const input = screen.getByTestId("masked-field");
391
416
  expect(input).toHaveValue("(123) 456-7890");
392
417
  });
418
+
419
+ it("should update when value prop changes (controlled behavior)", () => {
420
+ const { rerender } = render(<TestComponent value="1234567890" />);
421
+
422
+ const input = screen.getByTestId("masked-field");
423
+ expect(input).toHaveValue("(123) 456-7890");
424
+
425
+ // Change the value prop to simulate server-side update
426
+ rerender(<TestComponent value="9876543210" />);
427
+
428
+ // The input should reflect the new value
429
+ expect(input).toHaveValue("(987) 654-3210");
430
+ });
431
+
432
+ it("should handle masked value input when useMaskedValue is true", () => {
433
+ const { rerender } = render(
434
+ <TestComponent value="(123) 456-7890" useMaskedValue={true} />,
435
+ );
436
+
437
+ const input = screen.getByTestId("masked-field");
438
+ expect(input).toHaveValue("(123) 456-7890");
439
+
440
+ // Change to a different masked value
441
+ rerender(<TestComponent value="(987) 654-3210" />);
442
+
443
+ // The input should reflect the new masked value
444
+ expect(input).toHaveValue("(987) 654-3210");
445
+ });
446
+
447
+ it("should handle unmasked value input when useMaskedValue is false", () => {
448
+ const { rerender } = render(
449
+ <TestComponent value="1234567890" useMaskedValue={false} />,
450
+ );
451
+
452
+ const input = screen.getByTestId("masked-field");
453
+ expect(input).toHaveValue("(123) 456-7890");
454
+
455
+ // Change to a different unmasked value
456
+ rerender(<TestComponent value="9876543210" />);
457
+
458
+ // The input should reflect the new value (displayed as masked)
459
+ expect(input).toHaveValue("(987) 654-3210");
460
+ });
461
+
462
+ it("should not update when value prop matches current internal state", () => {
463
+ const setValue = jest.fn();
464
+
465
+ // Mock useIMask to spy on setValue calls
466
+ const originalUseIMask = jest.requireActual("react-imask").useIMask;
467
+ const reactIMask = jest.requireActual("react-imask");
468
+ jest.spyOn(reactIMask, "useIMask").mockImplementation((mask, options) => {
469
+ const result = originalUseIMask(mask, options);
470
+ return {
471
+ ...result,
472
+ setValue: setValue,
473
+ };
474
+ });
475
+
476
+ const { rerender } = render(<TestComponent value="1234567890" />);
477
+
478
+ // Clear any initial setValue calls
479
+ setValue.mockClear();
480
+
481
+ // Re-render with the same value
482
+ rerender(<TestComponent value="1234567890" />);
483
+
484
+ // setValue should not be called since the value hasn't actually changed
485
+ expect(setValue).not.toHaveBeenCalled();
486
+
487
+ // Restore original implementation
488
+ jest.restoreAllMocks();
489
+ });
490
+
491
+ it("should handle partial phone number updates", () => {
492
+ const { rerender } = render(<TestComponent value="123" />);
493
+
494
+ const input = screen.getByTestId("masked-field");
495
+ expect(input).toHaveValue("(123");
496
+
497
+ // Update to a longer partial number
498
+ rerender(<TestComponent value="12345" />);
499
+
500
+ expect(input).toHaveValue("(123) 45");
501
+ });
502
+
503
+ it("should handle empty value updates", () => {
504
+ const { rerender } = render(<TestComponent value="1234567890" />);
505
+
506
+ const input = screen.getByTestId("masked-field");
507
+ expect(input).toHaveValue("(123) 456-7890");
508
+
509
+ // Clear the value
510
+ rerender(<TestComponent value="" />);
511
+
512
+ expect(input).toHaveValue("");
513
+ });
514
+
515
+ it("should work with number masks and controlled values", () => {
516
+ const { rerender } = render(
517
+ <TestComponent
518
+ value="1234567"
519
+ mask={commaSeparatedNumberMask}
520
+ label="Number"
521
+ />,
522
+ );
523
+
524
+ const input = screen.getByTestId("masked-field");
525
+ expect(input).toHaveValue("1,234,567");
526
+
527
+ // Update to different number
528
+ rerender(
529
+ <TestComponent
530
+ value="9876543"
531
+ mask={commaSeparatedNumberMask}
532
+ label="Number"
533
+ />,
534
+ );
535
+
536
+ expect(input).toHaveValue("9,876,543");
537
+ });
393
538
  });
394
539
 
395
540
  describe("accessibility", () => {
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import type { Ref, RefAttributes } from "react";
4
- import { forwardRef } from "react";
4
+ import { forwardRef, useEffect } from "react";
5
5
  import type { ReactMaskOpts } from "react-imask";
6
6
  import { useIMask } from "react-imask";
7
7
  import type { DOMProps } from "../../types/dom";
@@ -72,10 +72,19 @@ export const MaskedField: ForwardedRefComponent<
72
72
  ref: maskRef,
73
73
  value: maskedValue,
74
74
  unmaskedValue,
75
+ setValue,
75
76
  } = useIMask(mask, {
76
- defaultValue,
77
+ defaultValue: value || defaultValue,
77
78
  });
78
79
 
80
+ // Handle controlled behavior - sync external value changes with internal mask state
81
+ useEffect(() => {
82
+ const valueToCompare = useMaskedValue ? maskedValue : unmaskedValue;
83
+ if (value !== undefined && value !== valueToCompare) {
84
+ setValue(value);
85
+ }
86
+ }, [value, setValue]);
87
+
79
88
  // Enhanced onChange handler that provides the unmasked value
80
89
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
81
90
  if (onChange) {