UNPKG

6.18 kBTypeScriptView Raw
1import {
2 type ComponentProps,
3 type CSSProperties,
4 forwardRef,
5 memo,
6 type ReactNode,
7 useId
8} from "react";
9import { assert, type Equals } from "tsafe";
10import { fr, type FrIconClassName, type RiIconClassName } from "./fr";
11import React from "react";
12import { type CxArg } from "tss-react";
13import { cx } from "./tools/cx";
14import { useAnalyticsId } from "./tools/useAnalyticsId";
15
16export 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 /** default: false */
28 small?: boolean;
29 /**
30 * Minimum 1, Maximum 5.
31 *
32 * All with icon or all without icon.
33 */
34 segments: SegmentedControlProps.Segments;
35} & (
36 | SegmentedControlProps.WithLegend
37 | SegmentedControlProps.WithInlineLegend
38 | SegmentedControlProps.WithHiddenLegend
39);
40
41//https://main--ds-gouv.netlify.app/example/component/segmented/
42export 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/** @see <https://components.react-dsfr.codegouv.studio/?path=/docs/components-segmented-control> */
90export 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