@simplybusiness/mobius 6.6.0 → 6.7.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.
- package/CHANGELOG.md +6 -0
- package/dist/cjs/index.js +58 -7
- package/dist/esm/index.js +79 -19
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/types/src/components/Radio/Radio.d.ts +8 -10
- package/dist/types/src/components/Radio/Radio.stories.d.ts +1 -0
- package/dist/types/src/components/Radio/RadioGroup.d.ts +5 -0
- package/package.json +1 -1
- package/src/components/Radio/Radio.css +1 -0
- package/src/components/Radio/Radio.stories.tsx +47 -2
- package/src/components/Radio/Radio.test.tsx +289 -0
- package/src/components/Radio/Radio.tsx +85 -13
- package/src/components/Radio/RadioGroup.tsx +46 -4
|
@@ -3,6 +3,10 @@ import type { ForwardedRefComponent } from "../../types/components";
|
|
|
3
3
|
import type { DOMProps } from "../../types/dom";
|
|
4
4
|
import type { HTMLElementEvent } from "../../types/events";
|
|
5
5
|
export type RadioElementType = HTMLInputElement;
|
|
6
|
+
export type RadioOverflowInfo = {
|
|
7
|
+
vertical: boolean;
|
|
8
|
+
horizontal: boolean;
|
|
9
|
+
};
|
|
6
10
|
export type AriaRadioProps = {
|
|
7
11
|
/**
|
|
8
12
|
* Defines a string value that labels the current element.
|
|
@@ -31,20 +35,14 @@ export interface RadioProps extends DOMProps, AriaRadioProps, RefAttributes<Radi
|
|
|
31
35
|
onChange?: (event: HTMLElementEvent<RadioElementType>) => void;
|
|
32
36
|
defaultChecked?: boolean;
|
|
33
37
|
/**
|
|
34
|
-
*
|
|
38
|
+
* Callback fired when label overflow state changes.
|
|
39
|
+
* Only invoked when the Radio is in horizontal orientation to prevent infinite loops with autoStack.
|
|
40
|
+
* Provides information about vertical and horizontal overflow.
|
|
35
41
|
*/
|
|
42
|
+
onOverflow?: (overflow: RadioOverflowInfo) => void;
|
|
36
43
|
groupDisabled?: boolean;
|
|
37
|
-
/**
|
|
38
|
-
* **Internal:** Do not use
|
|
39
|
-
*/
|
|
40
44
|
name?: string;
|
|
41
|
-
/**
|
|
42
|
-
* **Internal:** Do not use
|
|
43
|
-
*/
|
|
44
45
|
selected?: string;
|
|
45
|
-
/**
|
|
46
|
-
* **Internal:** Do not use
|
|
47
|
-
*/
|
|
48
46
|
setSelected?: React.Dispatch<React.SetStateAction<string>>;
|
|
49
47
|
isRequired?: boolean;
|
|
50
48
|
}
|
|
@@ -10,4 +10,5 @@ export declare const Invalid: StoryType;
|
|
|
10
10
|
export declare const WithIconViaChildren: StoryType;
|
|
11
11
|
export declare const ComplexLabel: StoryType;
|
|
12
12
|
export declare const HorizontalLayout: StoryType;
|
|
13
|
+
export declare const AutoStack: StoryType;
|
|
13
14
|
export default meta;
|
|
@@ -10,6 +10,11 @@ export interface RadioGroupProps extends DOMProps, Validation, RefAttributes<Rad
|
|
|
10
10
|
orientation?: "horizontal" | "vertical";
|
|
11
11
|
errorMessage?: string;
|
|
12
12
|
onChange?: (event: HTMLElementEvent<HTMLInputElement>) => void;
|
|
13
|
+
/**
|
|
14
|
+
* Automatically change orientation from horizontal to vertical when any Radio label overflows.
|
|
15
|
+
* Only applies when orientation is set to "horizontal".
|
|
16
|
+
*/
|
|
17
|
+
autoStack?: boolean;
|
|
13
18
|
"aria-label"?: string;
|
|
14
19
|
"aria-labelledby"?: string;
|
|
15
20
|
"aria-errormessage"?: string;
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
+
import { rocketLaunch, screwdriverWrench, truck } from "@simplybusiness/icons";
|
|
1
2
|
import type { Meta, StoryObj } from "@storybook/react-webpack5";
|
|
2
3
|
import { excludeControls } from "../../utils";
|
|
3
4
|
import { StoryContainer } from "../../utils/StoryContainer";
|
|
4
5
|
import { Divider } from "../Divider";
|
|
5
6
|
import { Flex } from "../Flex";
|
|
7
|
+
import { Icon } from "../Icon";
|
|
6
8
|
import { Radio } from "./Radio";
|
|
7
9
|
import type { RadioGroupProps } from "./RadioGroup";
|
|
8
10
|
import { RadioGroup } from "./RadioGroup";
|
|
9
|
-
import { rocketLaunch, screwdriverWrench, truck } from "@simplybusiness/icons";
|
|
10
|
-
import { Icon } from "../Icon";
|
|
11
11
|
|
|
12
12
|
type StoryType = StoryObj<typeof RadioGroup>;
|
|
13
13
|
|
|
@@ -222,4 +222,49 @@ export const HorizontalLayout: StoryType = {
|
|
|
222
222
|
},
|
|
223
223
|
};
|
|
224
224
|
|
|
225
|
+
export const AutoStack: StoryType = {
|
|
226
|
+
render: () => (
|
|
227
|
+
<div>
|
|
228
|
+
<p style={{ marginBottom: "20px" }}>
|
|
229
|
+
With <code>autoStack</code> enabled, RadioGroup automatically changes
|
|
230
|
+
from horizontal to vertical orientation when any radio label overflows.
|
|
231
|
+
Resize the container to see it in action.
|
|
232
|
+
</p>
|
|
233
|
+
<div
|
|
234
|
+
style={{
|
|
235
|
+
maxWidth: "400px",
|
|
236
|
+
border: "1px dashed #ccc",
|
|
237
|
+
padding: "10px",
|
|
238
|
+
}}
|
|
239
|
+
>
|
|
240
|
+
<RadioGroup
|
|
241
|
+
label="Payment Options (with autoStack)"
|
|
242
|
+
orientation="horizontal"
|
|
243
|
+
autoStack
|
|
244
|
+
>
|
|
245
|
+
<Radio value="credit" label="Credit Card" />
|
|
246
|
+
<Radio value="debit" label="Debit Card" />
|
|
247
|
+
</RadioGroup>
|
|
248
|
+
</div>
|
|
249
|
+
<div
|
|
250
|
+
style={{
|
|
251
|
+
marginTop: "30px",
|
|
252
|
+
maxWidth: "200px",
|
|
253
|
+
border: "1px dashed #ccc",
|
|
254
|
+
padding: "10px",
|
|
255
|
+
}}
|
|
256
|
+
>
|
|
257
|
+
<RadioGroup
|
|
258
|
+
label="Same options, narrower container"
|
|
259
|
+
orientation="horizontal"
|
|
260
|
+
autoStack
|
|
261
|
+
>
|
|
262
|
+
<Radio value="credit" label="Credit Card" />
|
|
263
|
+
<Radio value="debit" label="Debit Card" />
|
|
264
|
+
</RadioGroup>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
),
|
|
268
|
+
};
|
|
269
|
+
|
|
225
270
|
export default meta;
|
|
@@ -10,6 +10,41 @@ const LABEL_CLASS = "mobius-label";
|
|
|
10
10
|
const RADIO_WRAPPER_CLASS = "mobius-radio__wrapper";
|
|
11
11
|
const RADIO_INPUT_CLASS = "mobius-radio__input";
|
|
12
12
|
|
|
13
|
+
const mockElementDimensions = (
|
|
14
|
+
element: Element,
|
|
15
|
+
dimensions: {
|
|
16
|
+
scrollHeight?: number;
|
|
17
|
+
clientHeight?: number;
|
|
18
|
+
scrollWidth?: number;
|
|
19
|
+
clientWidth?: number;
|
|
20
|
+
},
|
|
21
|
+
) => {
|
|
22
|
+
Object.entries(dimensions).forEach(([key, value]) => {
|
|
23
|
+
Object.defineProperty(element, key, {
|
|
24
|
+
configurable: true,
|
|
25
|
+
value,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const mockOverflow = (element: Element) => {
|
|
31
|
+
mockElementDimensions(element, {
|
|
32
|
+
scrollHeight: 100,
|
|
33
|
+
clientHeight: 50,
|
|
34
|
+
scrollWidth: 100,
|
|
35
|
+
clientWidth: 100,
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const mockNoOverflow = (element: Element) => {
|
|
40
|
+
mockElementDimensions(element, {
|
|
41
|
+
scrollHeight: 50,
|
|
42
|
+
clientHeight: 50,
|
|
43
|
+
scrollWidth: 100,
|
|
44
|
+
clientWidth: 100,
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
13
48
|
describe("Radio", () => {
|
|
14
49
|
it("should render without error", () => {
|
|
15
50
|
const view = render(
|
|
@@ -837,3 +872,257 @@ describe.skip("focus behavior", () => {
|
|
|
837
872
|
expect(selectedOption).toHaveFocus();
|
|
838
873
|
});
|
|
839
874
|
});
|
|
875
|
+
|
|
876
|
+
describe("overflow detection", () => {
|
|
877
|
+
describe("Radio onOverflow callback", () => {
|
|
878
|
+
it("should call onOverflow with vertical overflow when content height exceeds container", () => {
|
|
879
|
+
const onOverflow = jest.fn();
|
|
880
|
+
|
|
881
|
+
const { container, rerender } = render(
|
|
882
|
+
<Radio value="test" label="Test Label" onOverflow={onOverflow} />,
|
|
883
|
+
);
|
|
884
|
+
|
|
885
|
+
const contentElement = container.querySelector(
|
|
886
|
+
".mobius-radio__content",
|
|
887
|
+
) as HTMLElement;
|
|
888
|
+
|
|
889
|
+
// Mock overflow: scrollHeight > clientHeight
|
|
890
|
+
mockOverflow(contentElement);
|
|
891
|
+
|
|
892
|
+
// Trigger useLayoutEffect by re-rendering
|
|
893
|
+
rerender(
|
|
894
|
+
<Radio
|
|
895
|
+
value="test"
|
|
896
|
+
label="Test Label Updated"
|
|
897
|
+
onOverflow={onOverflow}
|
|
898
|
+
/>,
|
|
899
|
+
);
|
|
900
|
+
|
|
901
|
+
expect(onOverflow).toHaveBeenCalledWith({
|
|
902
|
+
vertical: true,
|
|
903
|
+
horizontal: false,
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
it("should call onOverflow with horizontal overflow when content width exceeds container", () => {
|
|
908
|
+
const onOverflow = jest.fn();
|
|
909
|
+
|
|
910
|
+
const { container, rerender } = render(
|
|
911
|
+
<Radio value="test" label="Test Label" onOverflow={onOverflow} />,
|
|
912
|
+
);
|
|
913
|
+
|
|
914
|
+
const contentElement = container.querySelector(
|
|
915
|
+
".mobius-radio__content",
|
|
916
|
+
) as HTMLElement;
|
|
917
|
+
|
|
918
|
+
// Mock overflow: scrollWidth > clientWidth
|
|
919
|
+
mockElementDimensions(contentElement, {
|
|
920
|
+
scrollHeight: 50,
|
|
921
|
+
clientHeight: 50,
|
|
922
|
+
scrollWidth: 200,
|
|
923
|
+
clientWidth: 100,
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
// Trigger useLayoutEffect by re-rendering
|
|
927
|
+
rerender(
|
|
928
|
+
<Radio
|
|
929
|
+
value="test"
|
|
930
|
+
label="Test Label Updated"
|
|
931
|
+
onOverflow={onOverflow}
|
|
932
|
+
/>,
|
|
933
|
+
);
|
|
934
|
+
|
|
935
|
+
expect(onOverflow).toHaveBeenCalledWith({
|
|
936
|
+
vertical: false,
|
|
937
|
+
horizontal: true,
|
|
938
|
+
});
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
it("should call onOverflow with both overflow types when content exceeds in both dimensions", () => {
|
|
942
|
+
const onOverflow = jest.fn();
|
|
943
|
+
|
|
944
|
+
const { container, rerender } = render(
|
|
945
|
+
<Radio value="test" label="Test Label" onOverflow={onOverflow} />,
|
|
946
|
+
);
|
|
947
|
+
|
|
948
|
+
const contentElement = container.querySelector(
|
|
949
|
+
".mobius-radio__content",
|
|
950
|
+
) as HTMLElement;
|
|
951
|
+
|
|
952
|
+
// Mock overflow in both dimensions
|
|
953
|
+
mockElementDimensions(contentElement, {
|
|
954
|
+
scrollHeight: 100,
|
|
955
|
+
clientHeight: 50,
|
|
956
|
+
scrollWidth: 200,
|
|
957
|
+
clientWidth: 100,
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
// Trigger useLayoutEffect by re-rendering
|
|
961
|
+
rerender(
|
|
962
|
+
<Radio
|
|
963
|
+
value="test"
|
|
964
|
+
label="Test Label Updated"
|
|
965
|
+
onOverflow={onOverflow}
|
|
966
|
+
/>,
|
|
967
|
+
);
|
|
968
|
+
|
|
969
|
+
expect(onOverflow).toHaveBeenCalledWith({
|
|
970
|
+
vertical: true,
|
|
971
|
+
horizontal: true,
|
|
972
|
+
});
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
it("should not call onOverflow when content fits within container and state has not changed", () => {
|
|
976
|
+
const onOverflow = jest.fn();
|
|
977
|
+
|
|
978
|
+
const { container, rerender } = render(
|
|
979
|
+
<Radio value="test" label="Test Label" onOverflow={onOverflow} />,
|
|
980
|
+
);
|
|
981
|
+
|
|
982
|
+
const contentElement = container.querySelector(
|
|
983
|
+
".mobius-radio__content",
|
|
984
|
+
) as HTMLElement;
|
|
985
|
+
|
|
986
|
+
mockNoOverflow(contentElement);
|
|
987
|
+
|
|
988
|
+
// Trigger useLayoutEffect by re-rendering
|
|
989
|
+
rerender(
|
|
990
|
+
<Radio
|
|
991
|
+
value="test"
|
|
992
|
+
label="Test Label Updated"
|
|
993
|
+
onOverflow={onOverflow}
|
|
994
|
+
/>,
|
|
995
|
+
);
|
|
996
|
+
|
|
997
|
+
// Should not be called because overflow state didn't change (was false, still false)
|
|
998
|
+
expect(onOverflow).not.toHaveBeenCalled();
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
it("should not call onOverflow when callback is not provided", () => {
|
|
1002
|
+
const { container } = render(<Radio value="test" label="Test Label" />);
|
|
1003
|
+
|
|
1004
|
+
const contentElement = container.querySelector(
|
|
1005
|
+
".mobius-radio__content",
|
|
1006
|
+
) as HTMLElement;
|
|
1007
|
+
|
|
1008
|
+
// Mock overflow
|
|
1009
|
+
Object.defineProperty(contentElement, "scrollHeight", {
|
|
1010
|
+
configurable: true,
|
|
1011
|
+
value: 100,
|
|
1012
|
+
});
|
|
1013
|
+
Object.defineProperty(contentElement, "clientHeight", {
|
|
1014
|
+
configurable: true,
|
|
1015
|
+
value: 50,
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
// Should not throw
|
|
1019
|
+
expect(() => {
|
|
1020
|
+
render(<Radio value="test" label="Test Label Updated" />);
|
|
1021
|
+
}).not.toThrow();
|
|
1022
|
+
});
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
describe("RadioGroup autoStack", () => {
|
|
1026
|
+
it("should change orientation from horizontal to vertical when overflow detected and autoStack is true", () => {
|
|
1027
|
+
const { container, rerender } = render(
|
|
1028
|
+
<RadioGroup label="Color" orientation="horizontal" autoStack>
|
|
1029
|
+
<Radio value="red">Red</Radio>
|
|
1030
|
+
<Radio value="blue">Blue</Radio>
|
|
1031
|
+
</RadioGroup>,
|
|
1032
|
+
);
|
|
1033
|
+
|
|
1034
|
+
// Initially should be horizontal
|
|
1035
|
+
expect(container.firstChild).toHaveClass("--is-horizontal");
|
|
1036
|
+
expect(container.firstChild).toHaveAttribute(
|
|
1037
|
+
"aria-orientation",
|
|
1038
|
+
"horizontal",
|
|
1039
|
+
);
|
|
1040
|
+
|
|
1041
|
+
const radioContents = container.querySelectorAll(
|
|
1042
|
+
".mobius-radio__content",
|
|
1043
|
+
);
|
|
1044
|
+
|
|
1045
|
+
mockOverflow(radioContents[0]);
|
|
1046
|
+
mockNoOverflow(radioContents[1]);
|
|
1047
|
+
|
|
1048
|
+
// Trigger overflow detection by re-rendering
|
|
1049
|
+
rerender(
|
|
1050
|
+
<RadioGroup label="Color Updated" orientation="horizontal" autoStack>
|
|
1051
|
+
<Radio value="red">Red</Radio>
|
|
1052
|
+
<Radio value="blue">Blue</Radio>
|
|
1053
|
+
</RadioGroup>,
|
|
1054
|
+
);
|
|
1055
|
+
|
|
1056
|
+
// Should now be vertical
|
|
1057
|
+
expect(container.firstChild).toHaveClass("--is-vertical");
|
|
1058
|
+
expect(container.firstChild).toHaveAttribute(
|
|
1059
|
+
"aria-orientation",
|
|
1060
|
+
"vertical",
|
|
1061
|
+
);
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
it("should not change orientation when autoStack is false", () => {
|
|
1065
|
+
const { container, rerender } = render(
|
|
1066
|
+
<RadioGroup label="Color" orientation="horizontal" autoStack={false}>
|
|
1067
|
+
<Radio value="red">Red</Radio>
|
|
1068
|
+
<Radio value="blue">Blue</Radio>
|
|
1069
|
+
</RadioGroup>,
|
|
1070
|
+
);
|
|
1071
|
+
|
|
1072
|
+
const radioContents = container.querySelectorAll(
|
|
1073
|
+
".mobius-radio__content",
|
|
1074
|
+
);
|
|
1075
|
+
|
|
1076
|
+
mockOverflow(radioContents[0]);
|
|
1077
|
+
|
|
1078
|
+
// Trigger overflow detection
|
|
1079
|
+
rerender(
|
|
1080
|
+
<RadioGroup
|
|
1081
|
+
label="Color Updated"
|
|
1082
|
+
orientation="horizontal"
|
|
1083
|
+
autoStack={false}
|
|
1084
|
+
>
|
|
1085
|
+
<Radio value="red">Red</Radio>
|
|
1086
|
+
<Radio value="blue">Blue</Radio>
|
|
1087
|
+
</RadioGroup>,
|
|
1088
|
+
);
|
|
1089
|
+
|
|
1090
|
+
// Should still be horizontal
|
|
1091
|
+
expect(container.firstChild).toHaveClass("--is-horizontal");
|
|
1092
|
+
expect(container.firstChild).toHaveAttribute(
|
|
1093
|
+
"aria-orientation",
|
|
1094
|
+
"horizontal",
|
|
1095
|
+
);
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
it("should not change orientation when initial orientation is vertical", () => {
|
|
1099
|
+
const { container, rerender } = render(
|
|
1100
|
+
<RadioGroup label="Color" orientation="vertical" autoStack>
|
|
1101
|
+
<Radio value="red">Red</Radio>
|
|
1102
|
+
<Radio value="blue">Blue</Radio>
|
|
1103
|
+
</RadioGroup>,
|
|
1104
|
+
);
|
|
1105
|
+
|
|
1106
|
+
const radioContents = container.querySelectorAll(
|
|
1107
|
+
".mobius-radio__content",
|
|
1108
|
+
);
|
|
1109
|
+
|
|
1110
|
+
mockOverflow(radioContents[0]);
|
|
1111
|
+
|
|
1112
|
+
// Trigger overflow detection
|
|
1113
|
+
rerender(
|
|
1114
|
+
<RadioGroup label="Color Updated" orientation="vertical" autoStack>
|
|
1115
|
+
<Radio value="red">Red</Radio>
|
|
1116
|
+
<Radio value="blue">Blue</Radio>
|
|
1117
|
+
</RadioGroup>,
|
|
1118
|
+
);
|
|
1119
|
+
|
|
1120
|
+
// Should remain vertical (no change)
|
|
1121
|
+
expect(container.firstChild).toHaveClass("--is-vertical");
|
|
1122
|
+
expect(container.firstChild).toHaveAttribute(
|
|
1123
|
+
"aria-orientation",
|
|
1124
|
+
"vertical",
|
|
1125
|
+
);
|
|
1126
|
+
});
|
|
1127
|
+
});
|
|
1128
|
+
});
|
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import classNames from "classnames/dedupe";
|
|
4
4
|
import type { ReactNode, Ref, RefAttributes } from "react";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
Children,
|
|
7
|
+
forwardRef,
|
|
8
|
+
isValidElement,
|
|
9
|
+
useLayoutEffect,
|
|
10
|
+
useMemo,
|
|
11
|
+
useRef,
|
|
12
|
+
} from "react";
|
|
6
13
|
import type { ForwardedRefComponent } from "../../types/components";
|
|
7
14
|
import type { DOMProps } from "../../types/dom";
|
|
8
15
|
import type { HTMLElementEvent } from "../../types/events";
|
|
@@ -11,6 +18,11 @@ import { Label } from "../Label";
|
|
|
11
18
|
|
|
12
19
|
export type RadioElementType = HTMLInputElement;
|
|
13
20
|
|
|
21
|
+
export type RadioOverflowInfo = {
|
|
22
|
+
vertical: boolean;
|
|
23
|
+
horizontal: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
14
26
|
export type AriaRadioProps = {
|
|
15
27
|
/**
|
|
16
28
|
* Defines a string value that labels the current element.
|
|
@@ -43,20 +55,18 @@ export interface RadioProps
|
|
|
43
55
|
onChange?: (event: HTMLElementEvent<RadioElementType>) => void;
|
|
44
56
|
defaultChecked?: boolean;
|
|
45
57
|
/**
|
|
46
|
-
*
|
|
58
|
+
* Callback fired when label overflow state changes.
|
|
59
|
+
* Only invoked when the Radio is in horizontal orientation to prevent infinite loops with autoStack.
|
|
60
|
+
* Provides information about vertical and horizontal overflow.
|
|
47
61
|
*/
|
|
62
|
+
onOverflow?: (overflow: RadioOverflowInfo) => void;
|
|
63
|
+
// Internal:** Do not use
|
|
48
64
|
groupDisabled?: boolean;
|
|
49
|
-
|
|
50
|
-
* **Internal:** Do not use
|
|
51
|
-
*/
|
|
65
|
+
// Internal:** Do not use
|
|
52
66
|
name?: string;
|
|
53
|
-
|
|
54
|
-
* **Internal:** Do not use
|
|
55
|
-
*/
|
|
67
|
+
// Internal:** Do not use
|
|
56
68
|
selected?: string;
|
|
57
|
-
|
|
58
|
-
* **Internal:** Do not use
|
|
59
|
-
*/
|
|
69
|
+
// Internal:** Do not use
|
|
60
70
|
setSelected?: React.Dispatch<React.SetStateAction<string>>;
|
|
61
71
|
isRequired?: boolean;
|
|
62
72
|
}
|
|
@@ -79,6 +89,7 @@ const Radio: ForwardedRefComponent<RadioProps, RadioElementType> = forwardRef(
|
|
|
79
89
|
selected,
|
|
80
90
|
setSelected,
|
|
81
91
|
isRequired,
|
|
92
|
+
onOverflow,
|
|
82
93
|
...otherProps
|
|
83
94
|
} = props;
|
|
84
95
|
const realDisabled = groupDisabled || isDisabled;
|
|
@@ -86,6 +97,19 @@ const Radio: ForwardedRefComponent<RadioProps, RadioElementType> = forwardRef(
|
|
|
86
97
|
const isControlled = selected !== undefined;
|
|
87
98
|
const isChecked = isControlled ? selected === value : defaultChecked;
|
|
88
99
|
|
|
100
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
101
|
+
const prevOverflowRef = useRef<RadioOverflowInfo>({
|
|
102
|
+
vertical: false,
|
|
103
|
+
horizontal: false,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Extract orientation from otherProps for overflow detection
|
|
107
|
+
// @ts-expect-error - orientation is passed via cloneElement
|
|
108
|
+
const currentOrientation = otherProps.orientation as
|
|
109
|
+
| "horizontal"
|
|
110
|
+
| "vertical"
|
|
111
|
+
| undefined;
|
|
112
|
+
|
|
89
113
|
const hasIconFirst = useMemo(() => {
|
|
90
114
|
if (!children || Children.count(children) === 0) return false;
|
|
91
115
|
|
|
@@ -96,6 +120,52 @@ const Radio: ForwardedRefComponent<RadioProps, RadioElementType> = forwardRef(
|
|
|
96
120
|
return "icon" in props && props.icon !== undefined;
|
|
97
121
|
}, [children]);
|
|
98
122
|
|
|
123
|
+
// Detect overflow and call callback
|
|
124
|
+
useLayoutEffect(() => {
|
|
125
|
+
if (!contentRef.current || !onOverflow) return;
|
|
126
|
+
|
|
127
|
+
// Only detect overflow when in horizontal orientation
|
|
128
|
+
// This prevents infinite loops when autoStack switches to vertical
|
|
129
|
+
if (currentOrientation === "vertical") {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const element = contentRef.current;
|
|
134
|
+
|
|
135
|
+
// Check for content being cut off (true overflow)
|
|
136
|
+
const scrollOverflowVertical =
|
|
137
|
+
element.scrollHeight > element.clientHeight;
|
|
138
|
+
const scrollOverflowHorizontal =
|
|
139
|
+
element.scrollWidth > element.clientWidth;
|
|
140
|
+
|
|
141
|
+
// Check for multi-line text wrapping by comparing height to single line height
|
|
142
|
+
const styles = window.getComputedStyle(element);
|
|
143
|
+
const lineHeight = parseFloat(styles.lineHeight);
|
|
144
|
+
const fontSize = parseFloat(styles.fontSize);
|
|
145
|
+
const singleLineHeight = isNaN(lineHeight) ? fontSize * 1.2 : lineHeight;
|
|
146
|
+
|
|
147
|
+
// Tolerance multiplier to account for sub-pixel rendering and line-height variations.
|
|
148
|
+
// If element height is greater than single line, text has wrapped.
|
|
149
|
+
const WRAP_DETECTION_TOLERANCE = 1.1;
|
|
150
|
+
const hasWrapped =
|
|
151
|
+
element.clientHeight > singleLineHeight * WRAP_DETECTION_TOLERANCE;
|
|
152
|
+
|
|
153
|
+
const vertical = scrollOverflowVertical || hasWrapped;
|
|
154
|
+
const horizontal = scrollOverflowHorizontal;
|
|
155
|
+
|
|
156
|
+
const newOverflowState = { vertical, horizontal };
|
|
157
|
+
const prevOverflow = prevOverflowRef.current;
|
|
158
|
+
|
|
159
|
+
// Only call callback if state actually changed
|
|
160
|
+
if (
|
|
161
|
+
newOverflowState.vertical !== prevOverflow.vertical ||
|
|
162
|
+
newOverflowState.horizontal !== prevOverflow.horizontal
|
|
163
|
+
) {
|
|
164
|
+
prevOverflowRef.current = newOverflowState;
|
|
165
|
+
onOverflow(newOverflowState);
|
|
166
|
+
}
|
|
167
|
+
}, [label, children, onOverflow, currentOrientation]);
|
|
168
|
+
|
|
99
169
|
const radioClasses = {
|
|
100
170
|
"--is-disabled": realDisabled,
|
|
101
171
|
"--is-selected": selected === value,
|
|
@@ -157,12 +227,14 @@ const Radio: ForwardedRefComponent<RadioProps, RadioElementType> = forwardRef(
|
|
|
157
227
|
{...rest}
|
|
158
228
|
/>
|
|
159
229
|
{isMultiline ? (
|
|
160
|
-
<div className="mobius-radio__content--multiline">
|
|
230
|
+
<div ref={contentRef} className="mobius-radio__content--multiline">
|
|
161
231
|
<div className="mobius-radio__content-first-line">{label}</div>
|
|
162
232
|
<div className="mobius-radio__extra-content">{children}</div>
|
|
163
233
|
</div>
|
|
164
234
|
) : (
|
|
165
|
-
<div className="mobius-radio__content">
|
|
235
|
+
<div ref={contentRef} className="mobius-radio__content">
|
|
236
|
+
{label || children}
|
|
237
|
+
</div>
|
|
166
238
|
)}
|
|
167
239
|
</Label>
|
|
168
240
|
{errorMessage && <ErrorMessage errorMessage={errorMessage} />}
|