@cerebruminc/cerebellum 20.0.3 → 20.0.4
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/CHANGELOG.md +8 -0
- package/package.json +2 -2
- package/src/components/AddressInput/AddressInput.mdx +1 -0
- package/src/components/AddressInput/AddressInput.test.tsx +60 -3
- package/src/components/AddressInput/AddressInput.tsx +11 -0
- package/src/components/AddressInput/types.ts +1 -0
- package/src/mantine/mantineTheme.ts +35 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# react-component-lib-boilerplate
|
|
2
2
|
|
|
3
|
+
## [20.0.4](https://github.com/cerebruminc/cerebellum/compare/v20.0.3...v20.0.4) (2026-08-12)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* expose Google secondary address ([2ce454c](https://github.com/cerebruminc/cerebellum/commit/2ce454cc9a2f55b5eea285e2c158d0c4fa310378))
|
|
9
|
+
* upgrade next to 16.2.11 ([89700e3](https://github.com/cerebruminc/cerebellum/commit/89700e37e09c1f680001cff682c4f7646351070a))
|
|
10
|
+
|
|
3
11
|
## [20.0.3](https://github.com/cerebruminc/cerebellum/compare/v20.0.2...v20.0.3) (2026-08-06)
|
|
4
12
|
|
|
5
13
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cerebruminc/cerebellum",
|
|
3
|
-
"version": "20.0.
|
|
3
|
+
"version": "20.0.4",
|
|
4
4
|
"description": "Cerebrum's React Component Library",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"lint-staged": "^15.0.0",
|
|
95
95
|
"lorem-ipsum": "^2.0.4",
|
|
96
96
|
"msw": "^2.14.6",
|
|
97
|
-
"next": "^16.2.
|
|
97
|
+
"next": "^16.2.11",
|
|
98
98
|
"react": "^18.3.1",
|
|
99
99
|
"react-dom": "^18.3.1",
|
|
100
100
|
"remark-gfm": "^4.0.0",
|
|
@@ -64,6 +64,7 @@ import { AddressInput } from "@cerebruminc/cerebellum";
|
|
|
64
64
|
| stateAbbreviation | string | Two letter state abbreviation |
|
|
65
65
|
| street | string | Street address, including number. Does not include preceeding text, like "Historic District" |
|
|
66
66
|
| postalCode | string | zip code |
|
|
67
|
+
| secondaryAddress | optional string | Apartment, unit, or suite from Google's `subpremise` component, when available |
|
|
67
68
|
| rawAddressObject | AddressObjectProps | The location object returned by Google maps |
|
|
68
69
|
|
|
69
70
|
<br />
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { fireEvent, render, screen } from "@testing-library/react";
|
|
1
|
+
import { act, fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { geocodeByPlaceId } from "react-places-autocomplete";
|
|
2
3
|
import { withTheme } from "../../hocs/withTheme";
|
|
3
|
-
import { AddressInput } from "./AddressInput";
|
|
4
|
+
import { AddressInput, getSecondaryAddress } from "./AddressInput";
|
|
4
5
|
|
|
5
6
|
jest.mock("@react-google-maps/api", () => {
|
|
6
7
|
return {
|
|
@@ -17,9 +18,11 @@ jest.mock("@react-google-maps/api", () => {
|
|
|
17
18
|
});
|
|
18
19
|
|
|
19
20
|
let mockShouldThrow = false;
|
|
21
|
+
let mockOnSelect: ((address: string, placeId: string, addressObject: any) => Promise<void>) | undefined;
|
|
20
22
|
|
|
21
23
|
interface Props {
|
|
22
24
|
children: () => void;
|
|
25
|
+
onSelect: (address: string, placeId: string, addressObject: any) => Promise<void>;
|
|
23
26
|
}
|
|
24
27
|
jest.mock("react-places-autocomplete", () => {
|
|
25
28
|
const React = require("react");
|
|
@@ -44,18 +47,57 @@ jest.mock("react-places-autocomplete", () => {
|
|
|
44
47
|
if (mockShouldThrow) {
|
|
45
48
|
throw new Error("Google Maps JavaScript API library must be loaded");
|
|
46
49
|
}
|
|
50
|
+
mockOnSelect = this.props.onSelect;
|
|
47
51
|
return <>{this.props.children(this.renderProps)}</>;
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
return
|
|
55
|
+
return {
|
|
56
|
+
__esModule: true,
|
|
57
|
+
default: PlacesAutocomplete,
|
|
58
|
+
geocodeByPlaceId: jest.fn(),
|
|
59
|
+
};
|
|
52
60
|
});
|
|
53
61
|
|
|
54
62
|
const ThemedAddressInput = withTheme(AddressInput);
|
|
63
|
+
const mockGeocodeByPlaceId = geocodeByPlaceId as jest.Mock;
|
|
55
64
|
|
|
56
65
|
describe("AddressInput", () => {
|
|
57
66
|
afterEach(() => {
|
|
58
67
|
mockShouldThrow = false;
|
|
68
|
+
mockGeocodeByPlaceId.mockReset();
|
|
69
|
+
mockOnSelect = undefined;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("passes Google's secondary address to selection callbacks", async () => {
|
|
73
|
+
const onChange = jest.fn();
|
|
74
|
+
const addressObject = {
|
|
75
|
+
formattedSuggestion: { mainText: "415 East Irving Park Road" },
|
|
76
|
+
terms: [],
|
|
77
|
+
};
|
|
78
|
+
mockGeocodeByPlaceId.mockResolvedValue([
|
|
79
|
+
{
|
|
80
|
+
address_components: [
|
|
81
|
+
{ long_name: "60613", types: ["postal_code"] },
|
|
82
|
+
{ long_name: "APT 2N", types: ["subpremise"] },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
render(<ThemedAddressInput inputLabel="Address" value="" onChange={onChange} />);
|
|
88
|
+
|
|
89
|
+
await act(async () => {
|
|
90
|
+
await mockOnSelect?.("415 East Irving Park Road, Chicago, IL, USA", "place-id", addressObject);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(onChange).toHaveBeenLastCalledWith(
|
|
94
|
+
{ target: { value: "415 East Irving Park Road, Chicago, IL, USA", name: undefined } },
|
|
95
|
+
expect.objectContaining({
|
|
96
|
+
postalCode: "60613",
|
|
97
|
+
rawAddressObject: addressObject,
|
|
98
|
+
secondaryAddress: "APT 2N",
|
|
99
|
+
})
|
|
100
|
+
);
|
|
59
101
|
});
|
|
60
102
|
|
|
61
103
|
test("renders AddressInput with a label", async () => {
|
|
@@ -88,3 +130,18 @@ describe("AddressInput", () => {
|
|
|
88
130
|
});
|
|
89
131
|
});
|
|
90
132
|
});
|
|
133
|
+
|
|
134
|
+
describe("getSecondaryAddress", () => {
|
|
135
|
+
test("returns Google's subpremise address component", () => {
|
|
136
|
+
expect(
|
|
137
|
+
getSecondaryAddress([
|
|
138
|
+
{ long_name: "415", types: ["street_number"] },
|
|
139
|
+
{ long_name: "APT 2N", types: ["subpremise"] },
|
|
140
|
+
])
|
|
141
|
+
).toBe("APT 2N");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("returns undefined when there is no subpremise", () => {
|
|
145
|
+
expect(getSecondaryAddress([{ long_name: "East Irving Park Road", types: ["route"] }])).toBeUndefined();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -8,6 +8,15 @@ import { AddressInputErrorBoundary } from "./AddressInputErrorBoundary";
|
|
|
8
8
|
import { AddressInputContainer, PlaceOption, PlacesList } from "./AddressInputStyles";
|
|
9
9
|
import { AddressInputType, AddressObjectProps } from "./types";
|
|
10
10
|
|
|
11
|
+
type GoogleAddressComponent = {
|
|
12
|
+
long_name: string;
|
|
13
|
+
types: string[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const getSecondaryAddress = (addressComponents: GoogleAddressComponent[]): string | undefined => {
|
|
17
|
+
return addressComponents.find((component) => component.types.includes("subpremise"))?.long_name;
|
|
18
|
+
};
|
|
19
|
+
|
|
11
20
|
export const AddressInput: FC<AddressInputType> = (props: AddressInputType) => {
|
|
12
21
|
const {
|
|
13
22
|
disabled,
|
|
@@ -43,6 +52,7 @@ export const AddressInput: FC<AddressInputType> = (props: AddressInputType) => {
|
|
|
43
52
|
// Get zip code
|
|
44
53
|
const [place] = await geocodeByPlaceId(placeId);
|
|
45
54
|
const { long_name: postalCode = "" } = place.address_components.find((c) => c.types.includes("postal_code")) || {};
|
|
55
|
+
const secondaryAddress = getSecondaryAddress(place.address_components);
|
|
46
56
|
const stateAbbreviation = addressObject?.terms?.[addressObject?.terms?.length - 2]?.value;
|
|
47
57
|
|
|
48
58
|
const details = {
|
|
@@ -52,6 +62,7 @@ export const AddressInput: FC<AddressInputType> = (props: AddressInputType) => {
|
|
|
52
62
|
stateAbbreviation: addressObject?.terms?.[addressObject?.terms?.length - 2]?.value,
|
|
53
63
|
street: addressObject?.formattedSuggestion?.mainText,
|
|
54
64
|
postalCode,
|
|
65
|
+
secondaryAddress,
|
|
55
66
|
rawAddressObject: addressObject,
|
|
56
67
|
};
|
|
57
68
|
|
|
@@ -46,6 +46,24 @@ const getLabelStyles = (theme: MantineTheme) => {
|
|
|
46
46
|
};
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Whether a Mantine `style` prop pins an explicit width. The prop accepts a
|
|
51
|
+
* single object, a theme callback, or arbitrarily nested arrays of either, so
|
|
52
|
+
* this walks the whole structure. Callbacks are treated as width-free because
|
|
53
|
+
* they can only be resolved once a theme is applied.
|
|
54
|
+
*/
|
|
55
|
+
const styleSetsWidth = (style: ButtonProps["style"]): boolean => {
|
|
56
|
+
if (!style || typeof style === "function") {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (Array.isArray(style)) {
|
|
61
|
+
return style.some(styleSetsWidth);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return style.width !== undefined;
|
|
65
|
+
};
|
|
66
|
+
|
|
49
67
|
/**
|
|
50
68
|
* Common variables for form input sizing.
|
|
51
69
|
*/
|
|
@@ -459,16 +477,27 @@ export const createCustomMantineTheme = (optionsOrBrandColor?: CreateCustomManti
|
|
|
459
477
|
|
|
460
478
|
// Normalize sizes: sm and xs render at sm dimensions; md and all
|
|
461
479
|
// other unsupported sizes (lg, xl, etc.) use md dimensions.
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
480
|
+
const isSmall = props.size === "xs" || props.size === "sm";
|
|
481
|
+
root["--button-height"] = isSmall ? "32px" : "46px";
|
|
482
|
+
|
|
483
|
+
// An explicit width means the caller owns the button's footprint, so
|
|
484
|
+
// the generous default padding below is skipped and Mantine's
|
|
485
|
+
// per-size padding applies — otherwise the wider padding would eat
|
|
486
|
+
// into that fixed width and clip the label. Mirrors Cerebellum's
|
|
487
|
+
// `$buttonWidth ? 0` rule.
|
|
488
|
+
const hasExplicitWidth = props.w !== undefined || props.miw !== undefined || styleSetsWidth(props.style);
|
|
468
489
|
|
|
469
490
|
if (props.boxed || props.variant === "subtle") {
|
|
470
491
|
root["--button-radius"] = "8px";
|
|
471
492
|
root["--button-padding-x"] = "22px";
|
|
493
|
+
} else if (!hasExplicitWidth) {
|
|
494
|
+
// Match Cerebellum's pill buttons, which used 45px of horizontal
|
|
495
|
+
// padding (30px when an icon was present). Mantine derives the
|
|
496
|
+
// icon-side padding as `--button-padding-x / 1.5`, so a single
|
|
497
|
+
// value reproduces both. Still overridable per call site via the
|
|
498
|
+
// `px`/`p` style props or `styles={{ root: { paddingInline } }}`,
|
|
499
|
+
// which render as inline styles and therefore win over this var.
|
|
500
|
+
root["--button-padding-x"] = isSmall ? "30px" : "45px";
|
|
472
501
|
}
|
|
473
502
|
|
|
474
503
|
if (props.variant === "light") {
|