1 | import {
|
2 | type ComponentProps,
|
3 | type CSSProperties,
|
4 | forwardRef,
|
5 | memo,
|
6 | type ReactNode,
|
7 | useId
|
8 | } from "react";
|
9 | import { assert, type Equals } from "tsafe";
|
10 | import { fr, type FrIconClassName, type RiIconClassName } from "./fr";
|
11 | import React from "react";
|
12 | import { type CxArg } from "tss-react";
|
13 | import { cx } from "./tools/cx";
|
14 | import { useAnalyticsId } from "./tools/useAnalyticsId";
|
15 |
|
16 | export type SegmentedControlProps = {
|
17 | id?: string;
|
18 | className?: string;
|
19 | name?: string;
|
20 | classes?: Partial<
|
21 | Record<
|
22 | "root" | "legend" | "hintText" | "elements" | "element-each" | "element-each__label",
|
23 | CxArg
|
24 | >
|
25 | >;
|
26 | style?: CSSProperties;
|
27 |
|
28 | small?: boolean;
|
29 | |
30 |
|
31 |
|
32 |
|
33 |
|
34 | segments: SegmentedControlProps.Segments;
|
35 | } & (
|
36 | | SegmentedControlProps.WithLegend
|
37 | | SegmentedControlProps.WithInlineLegend
|
38 | | SegmentedControlProps.WithHiddenLegend
|
39 | );
|
40 |
|
41 |
|
42 | export namespace SegmentedControlProps {
|
43 | export type WithLegend = {
|
44 | inlineLegend?: boolean;
|
45 | legend: ReactNode;
|
46 | hintText?: ReactNode;
|
47 | hideLegend?: boolean;
|
48 | };
|
49 |
|
50 | export type WithInlineLegend = {
|
51 | inlineLegend: true;
|
52 | legend: ReactNode;
|
53 | hintText?: ReactNode;
|
54 | hideLegend?: never;
|
55 | };
|
56 |
|
57 | export type WithHiddenLegend = {
|
58 | inlineLegend?: never;
|
59 | legend?: ReactNode;
|
60 | hintText?: never;
|
61 | hideLegend: true;
|
62 | };
|
63 |
|
64 | export type Segment = {
|
65 | label: ReactNode;
|
66 | nativeInputProps?: ComponentProps<"input">;
|
67 | iconId?: FrIconClassName | RiIconClassName;
|
68 | };
|
69 |
|
70 | export type SegmentWithIcon = Segment & {
|
71 | iconId: FrIconClassName | RiIconClassName;
|
72 | };
|
73 |
|
74 | export type SegmentWithoutIcon = Segment & {
|
75 | iconId?: never;
|
76 | };
|
77 |
|
78 | export type Segments =
|
79 | | [SegmentWithIcon, SegmentWithIcon?, SegmentWithIcon?, SegmentWithIcon?, SegmentWithIcon?]
|
80 | | [
|
81 | SegmentWithoutIcon,
|
82 | SegmentWithoutIcon?,
|
83 | SegmentWithoutIcon?,
|
84 | SegmentWithoutIcon?,
|
85 | SegmentWithoutIcon?
|
86 | ];
|
87 | }
|
88 |
|
89 |
|
90 | export const SegmentedControl = memo(
|
91 | forwardRef<HTMLFieldSetElement, SegmentedControlProps>((props, ref) => {
|
92 | const {
|
93 | id: props_id,
|
94 | name: props_name,
|
95 | className,
|
96 | classes = {},
|
97 | style,
|
98 | small = false,
|
99 | segments,
|
100 | hideLegend,
|
101 | inlineLegend,
|
102 | legend,
|
103 | hintText,
|
104 | ...rest
|
105 | } = props;
|
106 |
|
107 | assert<Equals<keyof typeof rest, never>>();
|
108 |
|
109 | const id = useAnalyticsId({
|
110 | "defaultIdPrefix": `fr-segmented${props_name === undefined ? "" : `-${props_name}`}`,
|
111 | "explicitlyProvidedId": props_id
|
112 | });
|
113 |
|
114 | const getInputId = (i: number) => `${id}-${i}`;
|
115 |
|
116 | const segmentedName = (function useClosure() {
|
117 | const id = useId();
|
118 |
|
119 | return props_name ?? `segmented-name-${id}`;
|
120 | })();
|
121 |
|
122 | return (
|
123 | <fieldset
|
124 | id={id}
|
125 | className={cx(
|
126 | fr.cx(
|
127 | "fr-segmented",
|
128 | small && "fr-segmented--sm",
|
129 | hideLegend && "fr-segmented--no-legend"
|
130 | ),
|
131 | classes.root,
|
132 | className
|
133 | )}
|
134 | ref={ref}
|
135 | style={style}
|
136 | {...rest}
|
137 | >
|
138 | {legend !== undefined && (
|
139 | <legend
|
140 | className={cx(
|
141 | fr.cx(
|
142 | "fr-segmented__legend",
|
143 | inlineLegend && "fr-segmented__legend--inline"
|
144 | ),
|
145 | classes.legend
|
146 | )}
|
147 | >
|
148 | {legend}
|
149 | {hintText !== undefined && (
|
150 | <span className={cx(fr.cx("fr-hint-text"), classes.hintText)}>
|
151 | {hintText}
|
152 | </span>
|
153 | )}
|
154 | </legend>
|
155 | )}
|
156 | <div className={cx(fr.cx("fr-segmented__elements"), classes.elements)}>
|
157 | {segments.map((segment, index) => {
|
158 | if (!segment) return null;
|
159 |
|
160 | const segmentId = getInputId(index);
|
161 | return (
|
162 | <div
|
163 | className={cx(
|
164 | fr.cx("fr-segmented__element"),
|
165 | classes["element-each"]
|
166 | )}
|
167 | key={index}
|
168 | >
|
169 | <input
|
170 | {...segment.nativeInputProps}
|
171 | id={segmentId}
|
172 | name={segmentedName}
|
173 | type="radio"
|
174 | />
|
175 | <label
|
176 | className={cx(
|
177 | fr.cx(
|
178 | segment.iconId !== undefined && segment.iconId,
|
179 | "fr-label"
|
180 | ),
|
181 | classes["element-each__label"]
|
182 | )}
|
183 | htmlFor={segmentId}
|
184 | >
|
185 | {segment.label}
|
186 | </label>
|
187 | </div>
|
188 | );
|
189 | })}
|
190 | </div>
|
191 | </fieldset>
|
192 | );
|
193 | })
|
194 | );
|
195 |
|
\ | No newline at end of file |