Skip to content
Open
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
88 changes: 86 additions & 2 deletions patches/@gouvfr+dsfr+1.14.2.patch
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,94 @@ index e4d335c..6c53dce 100644
</svg>
+<!-- Slightly increasing size so it reaches the threshold that will prevent it from being inlined by Vite. -->
diff --git a/node_modules/@gouvfr/dsfr/dist/dsfr.module.js b/node_modules/@gouvfr/dsfr/dist/dsfr.module.js
index fcde523..7996e40 100644
index 0640386..8e30846 100644
--- a/node_modules/@gouvfr/dsfr/dist/dsfr.module.js
+++ b/node_modules/@gouvfr/dsfr/dist/dsfr.module.js
@@ -6461,9 +6461,11 @@ class HeaderLinks extends api.core.Instance {
@@ -400,6 +400,24 @@ const queryParentSelector = (element, selectors) => {
return queryParentSelector(parent, selectors);
};

+// Un sous-arbre marqué data-fr-hydrating est en cours d'hydratation par le
+// framework qui l'a rendu côté serveur : le moteur n'y touche pas tant qu'il
+// n'est pas libéré, soit par le retrait de l'attribut, soit par
+// dsfr.hydrated(id) quand l'attribut porte un identifiant. Dans ce second cas,
+// le moteur ne modifie pas le DOM : il retient l'élément libéré.
+const HYDRATING_ATTRIBUTE = ns.attr('hydrating');
+const HYDRATING_SELECTOR = ns.attr.selector('hydrating');
+const hydratedGates = new WeakSet();
+
+const isHydrating = (node) => {
+ let gate = node.closest(HYDRATING_SELECTOR);
+ while (gate) {
+ if (!hydratedGates.has(gate)) return true;
+ gate = gate.parentElement && gate.parentElement.closest(HYDRATING_SELECTOR);
+ }
+ return false;
+};
+
class Registration {
constructor (selector, InstanceClass, creator) {
this.selector = selector;
@@ -441,7 +459,9 @@ class Registration {
if (node.matches && node.matches(this.selector)) nodes.push(node);
// eslint-disable-next-line no-useless-call
if (!nonRecursive && node.querySelectorAll && node.querySelector(this.selector)) nodes.push.apply(nodes, querySelectorAllArray(node, this.selector));
- return nodes;
+ // Le retrait de l'attribut est une mutation d'attribut, à laquelle Stage
+ // répond déjà en analysant de nouveau tout le sous-arbre.
+ return nodes.filter(n => !isHydrating(n));
}

create (element) {
@@ -1149,6 +1169,15 @@ class Engine {
state.isActive = false;
rootDispatch(RootEvent.STOP);
}
+
+ // À appeler par le framework une fois hydraté le sous-arbre marqué
+ // data-fr-hydrating="<id>" : il est alors dans le document. Avant le
+ // démarrage, la libération est retenue et appliquée par start().
+ hydrated (id) {
+ const gates = querySelectorAllArray(document.documentElement, HYDRATING_SELECTOR).filter(gate => gate.getAttribute(HYDRATING_ATTRIBUTE) === id);
+ for (const gate of gates) hydratedGates.add(gate);
+ if (state.isActive) for (const gate of gates) state.getModule('stage').parse(gate);
+ }
}

const engine = new Engine();
@@ -1441,6 +1470,7 @@ api$1.version = config.version;

api$1.start = engine.start;
api$1.stop = engine.stop;
+api$1.hydrated = engine.hydrated;

api$1.inspector = inspector;
api$1.colors = colors;
@@ -2365,6 +2395,7 @@ class Disclosure extends Instance {

if (this.group) {
this.group.retrieve();
+ this.group.retrieveLate(this);
return;
}

@@ -2627,6 +2658,15 @@ class DisclosuresGroup extends Instance {
return this.getIndex();
}

+ // Un membre qui rejoint le groupe après _retrieve() (contenu streamé, ou
+ // libéré par dsfr.hydrated) n'y a pas été vu : ouvert par défaut, il est
+ // ouvert, sauf si un autre membre l'est déjà.
+ retrieveLate (member) {
+ if (!this._hasRetrieved || !member.isInitiallyDisclosed) return;
+ if (!this.isGrouped) member.disclose(true);
+ else if (this.current === null) this.current = member;
+ }
+
update () {
this.getMembers();
if (this._hasRetrieved) {
@@ -6461,9 +6501,11 @@ class HeaderLinks extends api.core.Instance {
case api.Modes.ANGULAR:
case api.Modes.REACT:
case api.Modes.VUE:
Expand Down
6 changes: 6 additions & 0 deletions src/Accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import React, {
forwardRef,
useId,
memo,
useState,
useEffect,
Expand All @@ -15,6 +16,7 @@ import { cx } from "./tools/cx";
import { symToStr } from "tsafe/symToStr";
import { useConstCallback } from "./tools/powerhooks/useConstCallback";
import { useAnalyticsId } from "./tools/useAnalyticsId";
import { HydrationRelease } from "./tools/HydrationRelease";

export type AccordionProps = AccordionProps.Controlled | AccordionProps.Uncontrolled;

Expand Down Expand Up @@ -72,6 +74,8 @@ export const Accordion = memo(
"explicitlyProvidedId": id_props
});

const hydrationId = useId();

const collapseElementId = `${id}-collapse`;

const [isExpanded, setIsExpanded] = useState(expanded_props ?? defaultExpanded);
Expand Down Expand Up @@ -100,6 +104,7 @@ export const Accordion = memo(
<section
className={cx(fr.cx("fr-accordion"), className)}
style={style}
data-fr-hydrating={hydrationId}
ref={ref}
{...rest}
>
Expand All @@ -118,6 +123,7 @@ export const Accordion = memo(
<div className={cx(fr.cx("fr-collapse"), classes.collapse)} id={collapseElementId}>
{children}
</div>
<HydrationRelease id={hydrationId} />
</section>
);
})
Expand Down
5 changes: 5 additions & 0 deletions src/Breadcrumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { createComponentI18nApi } from "./i18n";
import { fr } from "./fr";
import { cx } from "./tools/cx";
import { useAnalyticsId } from "./tools/useAnalyticsId";
import { HydrationRelease } from "./tools/HydrationRelease";

export type BreadcrumbProps = {
id?: string;
Expand Down Expand Up @@ -43,6 +44,8 @@ export const Breadcrumb = memo(
"explicitlyProvidedId": props_id
});

const hydrationId = useId();

const { t } = useTranslation();

const { Link } = getLink();
Expand All @@ -51,6 +54,7 @@ export const Breadcrumb = memo(
return (
<nav
id={id}
data-fr-hydrating={hydrationId}
ref={ref}
role="navigation"
className={cx(fr.cx("fr-breadcrumb"), classes.root, className)}
Expand Down Expand Up @@ -95,6 +99,7 @@ export const Breadcrumb = memo(
</>
</ol>
</div>
<HydrationRelease id={hydrationId} />
</nav>
);
})
Expand Down
15 changes: 13 additions & 2 deletions src/ButtonsGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { memo, forwardRef, type CSSProperties } from "react";
import React, { memo, forwardRef, useId, type CSSProperties } from "react";
import { Button } from "./Button";
import { ButtonProps } from "./Button";
import { symToStr } from "tsafe/symToStr";
Expand All @@ -7,6 +7,7 @@ import type { Equals } from "tsafe";
import { fr } from "./fr";
import { cx } from "./tools/cx";
import { useAnalyticsId } from "./tools/useAnalyticsId";
import { HydrationRelease } from "./tools/HydrationRelease";

export type ButtonsGroupProps = ButtonsGroupProps.AlwaysStacked | ButtonsGroupProps.Inline;

Expand Down Expand Up @@ -71,6 +72,8 @@ export const ButtonsGroup = memo(
"explicitlyProvidedId": props_id
});

const hydrationId = useId();

const buttonsGroupClassName = cx(
fr.cx(
"fr-btns-group",
Expand Down Expand Up @@ -105,12 +108,20 @@ export const ButtonsGroup = memo(
);

return (
<ul id={id} className={buttonsGroupClassName} style={style} ref={ref} {...rest}>
<ul
id={id}
className={buttonsGroupClassName}
style={style}
data-fr-hydrating={hydrationId}
ref={ref}
{...rest}
>
{buttons.map((buttonProps, i) => (
<li key={i}>
<Button {...buttonProps} />
</li>
))}
<HydrationRelease id={hydrationId} />
</ul>
);
})
Expand Down
6 changes: 6 additions & 0 deletions src/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, {
memo,
forwardRef,
useId,
type ReactNode,
type CSSProperties,
type DetailedHTMLProps,
Expand All @@ -14,6 +15,7 @@ import type { RegisteredLinkProps } from "./link";
import { getLink } from "./link";
import { cx } from "./tools/cx";
import { useAnalyticsId } from "./tools/useAnalyticsId";
import { HydrationRelease } from "./tools/HydrationRelease";
import { assert, type Equals } from "tsafe/assert";

//https://main--ds-gouv.netlify.app/example/component/card/
Expand Down Expand Up @@ -156,6 +158,8 @@ export const Card = memo(
"explicitlyProvidedId": props_id
});

const hydrationId = useId();

const { Link } = getLink();

return (
Expand Down Expand Up @@ -190,6 +194,7 @@ export const Card = memo(
className
)}
style={style}
data-fr-hydrating={hydrationId}
ref={ref}
{...rest}
>
Expand Down Expand Up @@ -265,6 +270,7 @@ export const Card = memo(
)}
</div>
)}
<HydrationRelease id={hydrationId} />
</div>
);
})
Expand Down
6 changes: 6 additions & 0 deletions src/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { JSX } from "../tools/JSX";
import React, {
memo,
forwardRef,
useId,
cloneElement,
type ReactNode,
type CSSProperties,
Expand All @@ -24,6 +25,7 @@ import { setBrandTopAndHomeLinkProps } from "../zz_internal/brandTopAndHomeLinkP
import { typeGuard } from "tsafe/typeGuard";
import { SearchButton } from "../SearchBar/SearchButton";
import { useTranslation as useSearchBarTranslation } from "../SearchBar/SearchBar";
import { HydrationRelease } from "../tools/HydrationRelease";

export type HeaderProps = {
className?: string;
Expand Down Expand Up @@ -158,6 +160,8 @@ export const Header = memo(
const { t } = useTranslation();
const { t: tSearchBar } = useSearchBarTranslation();

const hydrationId = useId();

const { Link } = getLink();

const getQuickAccessNode = (usecase: "mobile" | "desktop") => (
Expand Down Expand Up @@ -204,6 +208,7 @@ export const Header = memo(
role="banner"
id={id}
className={cx(fr.cx("fr-header"), classes.root, className)}
data-fr-hydrating={hydrationId}
ref={ref}
style={style}
{...rest}
Expand Down Expand Up @@ -471,6 +476,7 @@ export const Header = memo(
</div>
</div>
)}
<HydrationRelease id={hydrationId} />
</header>
</>
);
Expand Down
7 changes: 6 additions & 1 deletion src/MainNavigation/MainNavigation.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { memo, forwardRef, type ReactNode, type CSSProperties } from "react";
import React, { memo, forwardRef, useId, type ReactNode, type CSSProperties } from "react";
import { createComponentI18nApi } from "../i18n";
import { symToStr } from "tsafe/symToStr";
import { assert } from "tsafe/assert";
Expand All @@ -12,6 +12,7 @@ import { Menu } from "./Menu";
import type { MegaMenuProps } from "./MegaMenu";
import { MegaMenu } from "./MegaMenu";
import { useAnalyticsId } from "../tools/useAnalyticsId";
import { HydrationRelease } from "../tools/HydrationRelease";

export type MainNavigationProps = {
className?: string;
Expand Down Expand Up @@ -89,6 +90,8 @@ export const MainNavigation = memo(
"defaultIdPrefix": "main-navigation"
});

const hydrationId = useId();

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

return (
Expand All @@ -98,6 +101,7 @@ export const MainNavigation = memo(
style={style}
role="navigation"
aria-label={t("main menu")}
data-fr-hydrating={hydrationId}
ref={ref}
{...rest}
>
Expand Down Expand Up @@ -180,6 +184,7 @@ export const MainNavigation = memo(
)
)}
</ul>
<HydrationRelease id={hydrationId} />
</nav>
);
})
Expand Down
6 changes: 5 additions & 1 deletion src/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { JSX } from "../tools/JSX";
import React, { memo, forwardRef, type CSSProperties, type ReactNode } from "react";
import React, { memo, forwardRef, useId, type CSSProperties, type ReactNode } from "react";
import { fr } from "../fr";
import { cx } from "../tools/cx";
import { assert } from "tsafe/assert";
Expand All @@ -9,6 +9,7 @@ import { createComponentI18nApi } from "../i18n";
import type { FrIconClassName, RiIconClassName } from "../fr/generatedFromCss/classNames";
import Button, { ButtonProps } from "../Button";
import { overwriteReadonlyProp } from "tsafe/lab/overwriteReadonlyProp";
import { HydrationRelease } from "../tools/HydrationRelease";

export type ModalProps = {
className?: string;
Expand Down Expand Up @@ -66,13 +67,15 @@ const Modal = memo(
: [buttons_props];

const { t } = useTranslation();
const hydrationId = useId();
const titleId = `fr-modal-title-${id}`;
return (
<dialog
aria-labelledby={titleId}
id={id}
className={cx(fr.cx("fr-modal", topAnchor && "fr-modal--top"), className)}
style={style}
data-fr-hydrating={hydrationId}
ref={ref}
data-fr-concealing-backdrop={concealingBackdrop}
>
Expand Down Expand Up @@ -173,6 +176,7 @@ const Modal = memo(
</div>
</div>
</div>
<HydrationRelease id={hydrationId} />
</dialog>
);
})
Expand Down
Loading