Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion COMPONENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
- [x] Button
- [x] Buttons group
- [ ] France Connect button
- [ ] Radio button
- [x] Radio button
- [ ] Radio rich
- [ ] Checkbox
- [x] Cards
Expand Down
23 changes: 21 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,29 @@ Thank You Very Much ❤️

PS: If you want to contribute to the Doc website. You can edit [the source Markdown](https://github.com/codegouvfr/react-dsfr/tree/v1_docs) or ask me for access to our GitBook. (We'll migrate to Docusaurus once we have the DSFR theme for it ready.)

## Linking a working version of `@gouvfr/dsfr`
## Linking your local copy of `@codegouvfr/react-dsfr` in your project

This will enable you to see your react-dsfr changes in your main project.

```bash
cd ~/github
git clone https://github.com/ORG/YOUR-PROJECT-USING-REACT-DSFR
cd YOUR-PROJECT-USING-REACT-DSFR
yarn # or npm install or pnpm install depending of what you are using...

cd ~/github
git clone https://github.com/codegouvfr/react-dsfr
cd react-dsfr
yarn
yarn build
yarn link-external YOUR-PROJECT-USING-REACT-DSFR
npx tsc -w -p src # Leave this running if you want hot reload.
```

## Linking a working version of `@gouvfr/dsfr` (For the SIG)

```bash
cd ~/github/
cd ~/github
git clone http://github.com/gouvernementfr/dsfr
cd dsfr
# git checkout my-working-branch
Expand Down
2 changes: 1 addition & 1 deletion README.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ DSFR en pur JavaScript/CSS.
- [x] la plupart des composants peuvent être rendus directement sur le serveur (voir [RSC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)). Les autres sont étiquetées `"use client";`.
- [x] [Intégration clef en main pour les différents frameworks de développement: vite, Next.js, y compris la version beta de Next 13 (configuration AppDir) et Create React App](https://react-dsfr.etalab.studio/) si votre
framework n'est pas supporter, il suffit de demander notre, nous avons pour but de couvrir tous les cas d'usage effectifs.
- [ ] tout [les composants de référence implémenter](https://www.systeme-de-design.gouv.fr/elements-d-interface). À ce jour 18/41, [see details](COMPONENTS.md)
- [ ] tout [les composants de référence implémenter](https://www.systeme-de-design.gouv.fr/elements-d-interface). À ce jour 19/41, [see details](COMPONENTS.md)
- [x] seulement le code des composants que vous utilisez effectivement sera inclus dans votre projet final.
- [x] Intégration facultative avec [MUI](https://mui.com/). Si vous utilisez des composants MUI ils seront automatiquement adaptés pour ressembler à des composants DSFR.
Voir là [documentation](https://react-dsfr.etalab.studio/mui-integration).
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ This module is a wrapper/compatibility layer for [@gouvfr/dsfr](https://github.c
- [x] No [white flash when reloading in SSR setup](https://github.com/codegouvfr/@codegouvfr/react-dsfr/issues/2#issuecomment-1257263480).
- [x] Most components are server component ready. The others are labeled with `"use client";`
- [x] [Perfect integration with all major React framework: Next.js (PagesDir and AppDir), Create React App, Vue](https://react-dsfr.etalab.studio/).
- [ ] All [the components](https://www.systeme-de-design.gouv.fr/elements-d-interface) are implemented (18/41, [see details](COMPONENTS.md))
- [ ] All [the components](https://www.systeme-de-design.gouv.fr/elements-d-interface) are implemented (19/41, [see details](COMPONENTS.md))
- [x] Three shakable distribution, cherry pick the components you import. (It's not all in a big .js bundle)
- [x] Optional integration with [MUI](https://mui.com/). If you use MUI components they will
be automatically adapted to look like [DSFR components](https://www.systeme-de-design.gouv.fr/elements-d-interface). See [documentation](https://react-dsfr.etalab.studio/mui-integration).
Expand Down
14 changes: 8 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
{
"name": "@codegouvfr/react-dsfr",
"version": "0.30.1",
"version": "0.31.0",
"description": "French State Design System React integration library",
"repository": {
"type": "git",
"url": "git://github.com/codegouvfr/react-dsfr.git"
},
"scripts": {
"build": "ts-node -T src/scripts/build",
"yarn-link": "ts-node -T src/scripts/yarn-link.ts",
"start-cra": "yarn build && yarn yarn-link && ((tsc -w -p src) & (cd test/integration/cra && yarn start))",
"start-vite": "yarn build && yarn yarn-link && ((tsc -w -p src) & (cd test/integration/vite && yarn dev))",
"start-next-pagesdir": "yarn build && yarn yarn-link && ((tsc -w -p src) & (cd test/integration/next-pagesdir && yarn dev))",
"start-next-appdir": "yarn build && yarn yarn-link && ((tsc -w -p src) & (cd test/integration/next-appdir && yarn dev))",
"_link": "ts-node -T src/scripts/link-in-integration-apps.ts",
"link-external": "ts-node -T src/scripts/link-in-external-project.ts",
"start-cra": "yarn build && yarn _link && ((tsc -w -p src) & (cd test/integration/cra && yarn start))",
"start-vite": "yarn build && yarn _link && ((tsc -w -p src) & (cd test/integration/vite && yarn dev))",
"start-next-pagesdir": "yarn build && yarn _link && ((tsc -w -p src) & (cd test/integration/next-pagesdir && yarn dev))",
"start-next-appdir": "yarn build && yarn _link && ((tsc -w -p src) & (cd test/integration/next-appdir && yarn dev))",
"test": "vitest",
"lint:check": "eslint . --ext .ts,.tsx",
"lint": "yarn lint:check --fix",
Expand Down Expand Up @@ -137,6 +138,7 @@
"./SkipLinks": "./dist/SkipLinks.js",
"./Select": "./dist/Select.js",
"./SearchBar": "./dist/SearchBar.js",
"./RadioButtons": "./dist/RadioButtons.js",
"./Quote": "./dist/Quote.js",
"./Pagination": "./dist/Pagination.js",
"./Notice": "./dist/Notice.js",
Expand Down
180 changes: 180 additions & 0 deletions src/RadioButtons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import React, {
useId,
memo,
forwardRef,
type ReactNode,
type CSSProperties,
type InputHTMLAttributes,
type DetailedHTMLProps
} from "react";
import { symToStr } from "tsafe/symToStr";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
import { cx } from "./tools/cx";
import { fr } from "./fr";

export type RadioButtonsProps = {
className?: string;
classes?: Partial<Record<"root" | "legend" | "content", string>>;
style?: CSSProperties;
legend: ReactNode;
hintText?: ReactNode;
name?: string;
options: {
label: ReactNode;
hintText?: ReactNode;
nativeInputProps: DetailedHTMLProps<
InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
>;
}[];
/** Default: "vertical" */
orientation?: "vertical" | "horizontal";
/** Default: "default" */
state?: "success" | "error" | "default";
/**
* The message won't be displayed if state is "default".
* If the state is "error" providing a message is mandatory
**/
stateRelatedMessage?: ReactNode;
/** Default: false */
disabled?: boolean;
};

/** @see <https://react-dsfr-components.etalab.studio/?path=/docs/components-radiobutton> */
export const RadioButtons = memo(
forwardRef<HTMLFieldSetElement, RadioButtonsProps>((props, ref) => {
const {
className,
classes = {},
style,
name: name_props,
legend,
hintText,
options,
orientation = "vertical",
state = "default",
stateRelatedMessage,
disabled = false,
...rest
} = props;

assert<Equals<keyof typeof rest, never>>();

const { getRadioId, legendId, errorDescId, successDescId } = (function useClosure() {
const id = `radio${name_props === undefined ? "" : `-${name_props}`}-${useId()}`;

const getRadioId = (i: number) => `${id}-${i}`;

const legendId = `${id}-legend`;

const errorDescId = `${id}-desc-error`;
const successDescId = `${id}-desc-valid`;

return { getRadioId, legendId, errorDescId, successDescId };
})();

const name = (function useClosure() {
const id = useId();

return name_props ?? `radio-name-${id}`;
})();

return (
<fieldset
className={cx(
fr.cx(
"fr-fieldset",
orientation === "horizontal" && "fr-fieldset--inline",
(() => {
switch (state) {
case "default":
return undefined;
case "error":
return "fr-fieldset--error";
case "success":
return "fr-fieldset--valid";
}
})()
),
classes.root,
className
)}
disabled={disabled}
style={style}
aria-labelledby={cx(
legendId,
(() => {
switch (state) {
case "default":
return undefined;
case "error":
return errorDescId;
case "success":
return successDescId;
}
})()
)}
role={state === "default" ? undefined : "group"}
{...rest}
ref={ref}
>
<legend
id={legendId}
className={cx(fr.cx("fr-fieldset__legend", "fr-text--regular"), classes.legend)}
>
{legend}
{hintText !== undefined && (
<span className={fr.cx("fr-hint-text")}>{hintText}</span>
)}
</legend>
<div className={cx(fr.cx("fr-fieldset__content"), classes.content)}>
{options.map(({ label, hintText, nativeInputProps }, i) => (
<div className={fr.cx("fr-radio-group")} key={i}>
<input
type="radio"
id={getRadioId(i)}
name={name}
{...nativeInputProps}
/>
<label className={fr.cx("fr-label")} htmlFor={getRadioId(i)}>
{label}
{hintText !== undefined && (
<span className={fr.cx("fr-hint-text")}>{hintText}</span>
)}
</label>
</div>
))}
</div>
{state !== "default" && (
<p
id={(() => {
switch (state) {
case "error":
return errorDescId;
case "success":
return successDescId;
}
})()}
className={fr.cx(
(() => {
switch (state) {
case "error":
return "fr-error-text";
case "success":
return "fr-valid-text";
}
})()
)}
>
{stateRelatedMessage ?? ""}
</p>
)}
</fieldset>
);
})
);

RadioButtons.displayName = symToStr({ RadioButtons });

export default RadioButtons;
119 changes: 119 additions & 0 deletions src/scripts/link-in-external-project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { execSync } from "child_process";
import { join as pathJoin, relative as pathRelative } from "path";
import { exclude } from "tsafe/exclude";
import * as fs from "fs";

const rootDirPath = pathJoin(__dirname, "..", "..");

const commonThirdPartyDeps = (() => {
const namespaceModuleNames = ["@emotion"];
const standaloneModuleNames = ["react", "@types/react"];

return [
...namespaceModuleNames
.map(namespaceModuleName =>
fs
.readdirSync(pathJoin(rootDirPath, "node_modules", namespaceModuleName))
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`)
)
.reduce((prev, curr) => [...prev, ...curr], []),
...standaloneModuleNames
];
})();

const yarnHomeDirPath = pathJoin(rootDirPath, ".yarn_home");

fs.rmSync(yarnHomeDirPath, { "recursive": true, "force": true });
fs.mkdirSync(yarnHomeDirPath);

const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
const { targetModuleName, cwd } = params;

const cmd = [
"yarn",
"link",
...(targetModuleName !== undefined ? [targetModuleName] : [])
].join(" ");

console.log(`$ cd ${pathRelative(rootDirPath, cwd) || "."} && ${cmd}`);

execSync(cmd, {
cwd,
"env": {
...process.env,
"HOME": yarnHomeDirPath
}
});
};

const testAppPaths = (() => {
const [, , ...testAppNames] = process.argv;

return testAppNames
.map(testAppName => {
const testAppPath = pathJoin(rootDirPath, "..", testAppName);

if (fs.existsSync(testAppPath)) {
return testAppPath;
}

console.warn(`Skipping ${testAppName} since it cant be found here: ${testAppPath}`);

return undefined;
})
.filter(exclude(undefined));
})();

if (testAppPaths.length === 0) {
console.error("No test app to link into!");
process.exit(-1);
}

testAppPaths.forEach(testAppPath => execSync("yarn install", { "cwd": testAppPath }));

console.log("=== Linking common dependencies ===");

const total = commonThirdPartyDeps.length;
let current = 0;

commonThirdPartyDeps.forEach(commonThirdPartyDep => {
current++;

console.log(`${current}/${total} ${commonThirdPartyDep}`);

const localInstallPath = pathJoin(
...[
rootDirPath,
"node_modules",
...(commonThirdPartyDep.startsWith("@")
? commonThirdPartyDep.split("/")
: [commonThirdPartyDep])
]
);

execYarnLink({ "cwd": localInstallPath });
});

commonThirdPartyDeps.forEach(commonThirdPartyDep =>
testAppPaths.forEach(testAppPath =>
execYarnLink({
"cwd": testAppPath,
"targetModuleName": commonThirdPartyDep
})
)
);

console.log("=== Linking in house dependencies ===");

execYarnLink({ "cwd": pathJoin(rootDirPath, "dist") });

testAppPaths.forEach(testAppPath =>
execYarnLink({
"cwd": testAppPath,
"targetModuleName": JSON.parse(
fs.readFileSync(pathJoin(rootDirPath, "package.json")).toString("utf8")
)["name"]
})
);

export {};
File renamed without changes.
Loading