@navikt/ds-react 7.29.1 → 7.30.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.
Files changed (85) hide show
  1. package/cjs/date/datepicker/parts/DatePicker.WeekNumber.js +1 -1
  2. package/cjs/date/datepicker/parts/DatePicker.WeekNumber.js.map +1 -1
  3. package/cjs/form/form-summary/FormSummary.d.ts +11 -17
  4. package/cjs/form/form-summary/FormSummary.js +5 -1
  5. package/cjs/form/form-summary/FormSummary.js.map +1 -1
  6. package/cjs/form/form-summary/FormSummaryEditLink.js +6 -1
  7. package/cjs/form/form-summary/FormSummaryEditLink.js.map +1 -1
  8. package/cjs/form/form-summary/FormSummaryFooter.d.ts +12 -0
  9. package/cjs/form/form-summary/FormSummaryFooter.js +56 -0
  10. package/cjs/form/form-summary/FormSummaryFooter.js.map +1 -0
  11. package/cjs/form/form-summary/FormSummaryHeader.d.ts +1 -1
  12. package/cjs/form/form-summary/FormSummaryHeader.js +3 -1
  13. package/cjs/form/form-summary/FormSummaryHeader.js.map +1 -1
  14. package/cjs/form/form-summary/index.d.ts +1 -0
  15. package/cjs/form/form-summary/index.js +3 -1
  16. package/cjs/form/form-summary/index.js.map +1 -1
  17. package/cjs/timeline/period/index.d.ts +1 -1
  18. package/cjs/timeline/period/index.js.map +1 -1
  19. package/cjs/timeline/utils/timeline.d.ts +7 -6
  20. package/cjs/timeline/utils/timeline.js +39 -62
  21. package/cjs/timeline/utils/timeline.js.map +1 -1
  22. package/cjs/timeline/utils/types.external.d.ts +2 -3
  23. package/cjs/timeline/utils/types.internal.d.ts +1 -1
  24. package/cjs/util/composition-warning/CompositionWarning.d.ts +37 -0
  25. package/cjs/util/composition-warning/CompositionWarning.js +71 -0
  26. package/cjs/util/composition-warning/CompositionWarning.js.map +1 -0
  27. package/cjs/util/composition-warning/index.d.ts +1 -0
  28. package/cjs/util/composition-warning/index.js +38 -0
  29. package/cjs/util/composition-warning/index.js.map +1 -0
  30. package/cjs/util/getChildRef.d.ts +1 -0
  31. package/cjs/util/getChildRef.js +8 -0
  32. package/cjs/util/getChildRef.js.map +1 -0
  33. package/cjs/util/renderStoriesForChromatic.d.ts +2 -10
  34. package/cjs/util/renderStoriesForChromatic.js +2 -2
  35. package/cjs/util/renderStoriesForChromatic.js.map +1 -1
  36. package/esm/date/datepicker/parts/DatePicker.WeekNumber.js +1 -1
  37. package/esm/date/datepicker/parts/DatePicker.WeekNumber.js.map +1 -1
  38. package/esm/form/form-summary/FormSummary.d.ts +11 -17
  39. package/esm/form/form-summary/FormSummary.js +5 -1
  40. package/esm/form/form-summary/FormSummary.js.map +1 -1
  41. package/esm/form/form-summary/FormSummaryEditLink.js +6 -1
  42. package/esm/form/form-summary/FormSummaryEditLink.js.map +1 -1
  43. package/esm/form/form-summary/FormSummaryFooter.d.ts +12 -0
  44. package/esm/form/form-summary/FormSummaryFooter.js +20 -0
  45. package/esm/form/form-summary/FormSummaryFooter.js.map +1 -0
  46. package/esm/form/form-summary/FormSummaryHeader.d.ts +1 -1
  47. package/esm/form/form-summary/FormSummaryHeader.js +3 -1
  48. package/esm/form/form-summary/FormSummaryHeader.js.map +1 -1
  49. package/esm/form/form-summary/index.d.ts +1 -0
  50. package/esm/form/form-summary/index.js +1 -0
  51. package/esm/form/form-summary/index.js.map +1 -1
  52. package/esm/timeline/period/index.d.ts +1 -1
  53. package/esm/timeline/period/index.js.map +1 -1
  54. package/esm/timeline/utils/timeline.d.ts +7 -6
  55. package/esm/timeline/utils/timeline.js +39 -62
  56. package/esm/timeline/utils/timeline.js.map +1 -1
  57. package/esm/timeline/utils/types.external.d.ts +2 -3
  58. package/esm/timeline/utils/types.internal.d.ts +1 -1
  59. package/esm/util/composition-warning/CompositionWarning.d.ts +37 -0
  60. package/esm/util/composition-warning/CompositionWarning.js +34 -0
  61. package/esm/util/composition-warning/CompositionWarning.js.map +1 -0
  62. package/esm/util/composition-warning/index.d.ts +1 -0
  63. package/esm/util/composition-warning/index.js +2 -0
  64. package/esm/util/composition-warning/index.js.map +1 -0
  65. package/esm/util/getChildRef.d.ts +1 -0
  66. package/esm/util/getChildRef.js +4 -0
  67. package/esm/util/getChildRef.js.map +1 -0
  68. package/esm/util/renderStoriesForChromatic.d.ts +2 -10
  69. package/esm/util/renderStoriesForChromatic.js +2 -2
  70. package/esm/util/renderStoriesForChromatic.js.map +1 -1
  71. package/package.json +3 -3
  72. package/src/date/datepicker/parts/DatePicker.WeekNumber.tsx +3 -1
  73. package/src/form/form-summary/FormSummary.tsx +12 -17
  74. package/src/form/form-summary/FormSummaryEditLink.tsx +16 -7
  75. package/src/form/form-summary/FormSummaryFooter.tsx +33 -0
  76. package/src/form/form-summary/FormSummaryHeader.tsx +12 -9
  77. package/src/form/form-summary/index.ts +4 -0
  78. package/src/timeline/period/index.tsx +3 -1
  79. package/src/timeline/utils/timeline.ts +56 -67
  80. package/src/timeline/utils/types.external.ts +4 -3
  81. package/src/timeline/utils/types.internal.ts +3 -1
  82. package/src/util/composition-warning/CompositionWarning.tsx +67 -0
  83. package/src/util/composition-warning/index.ts +1 -0
  84. package/src/util/getChildRef.ts +6 -0
  85. package/src/util/renderStoriesForChromatic.tsx +12 -9
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Give warnings based on component composition (which slot/parent a child is rendered in).
3
+ *
4
+ * Used when child components need to know which slot/parent they are rendered in
5
+ * (e.g. `FormSummary.Header` vs `FormSummary.Footer`) and should warn or error in development
6
+ * if placed in a discouraged or forbidden slot.
7
+ *
8
+ * Usage:
9
+ * - Wrap slot components with <CompositionWarning.Root name="FormSummary.Header">...</CompositionWarning.Root>
10
+ * - In child: `<CompositionWarning.Forbidden name="FormSummary.Header" />` to forbid slot.
11
+ *
12
+ * This is guidance only: warnings are logged to the console in development, never enforced at runtime.
13
+ */
14
+ import React, { useEffect, useRef } from "react";
15
+ import { Slot } from "../../slot/Slot.js";
16
+ import { createContext } from "../create-context.js";
17
+ const isDev = process.env.NODE_ENV !== "production";
18
+ const [CompositionWarning, useCompositionWarning] = createContext({
19
+ errorMessage: "useCompositionWarning() must be used within <CompositionWarning />",
20
+ });
21
+ function CompositionWarningForbidden({ children, name, message, }) {
22
+ var _a;
23
+ const compositionName = (_a = useCompositionWarning(false)) === null || _a === void 0 ? void 0 : _a.name;
24
+ const elementRef = useRef(null);
25
+ useEffect(() => {
26
+ if (!isDev || !compositionName || name !== compositionName) {
27
+ return;
28
+ }
29
+ console.warn(`[Aksel] ${message}\nElement: `, elementRef.current);
30
+ }, [compositionName, name, message]);
31
+ return React.createElement(Slot, { ref: elementRef }, children);
32
+ }
33
+ export { CompositionWarningForbidden as Forbidden, CompositionWarning as Root };
34
+ //# sourceMappingURL=CompositionWarning.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CompositionWarning.js","sourceRoot":"","sources":["../../../src/util/composition-warning/CompositionWarning.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AACjD,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAIlD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,CAAC;AASpD,MAAM,CAAC,kBAAkB,EAAE,qBAAqB,CAAC,GAC/C,aAAa,CAAgC;IAC3C,YAAY,EACV,oEAAoE;CACvE,CAAC,CAAC;AAcL,SAAS,2BAA2B,CAAC,EACnC,QAAQ,EACR,IAAI,EACJ,OAAO,GAC0B;;IACjC,MAAM,eAAe,GAAG,MAAA,qBAAqB,CAAC,KAAK,CAAC,0CAAE,IAAI,CAAC;IAE3D,MAAM,UAAU,GAAG,MAAM,CAAc,IAAI,CAAC,CAAC;IAE7C,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,KAAK,IAAI,CAAC,eAAe,IAAI,IAAI,KAAK,eAAe,EAAE,CAAC;YAC3D,OAAO;QACT,CAAC;QAED,OAAO,CAAC,IAAI,CAAC,WAAW,OAAO,aAAa,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC;IACpE,CAAC,EAAE,CAAC,eAAe,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IAErC,OAAO,oBAAC,IAAI,IAAC,GAAG,EAAE,UAAU,IAAG,QAAQ,CAAQ,CAAC;AAClD,CAAC;AAED,OAAO,EAAE,2BAA2B,IAAI,SAAS,EAAE,kBAAkB,IAAI,IAAI,EAAE,CAAC"}
@@ -0,0 +1 @@
1
+ export * as CompositionWarning from "./CompositionWarning.js";
@@ -0,0 +1,2 @@
1
+ export * as CompositionWarning from "./CompositionWarning.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/util/composition-warning/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,kBAAkB,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1 @@
1
+ export declare const getChildRef: <T>(children: React.ReactElement<React.RefAttributes<T>>) => React.Ref<T> | undefined;
@@ -0,0 +1,4 @@
1
+ export const getChildRef = (children) => Object.prototype.propertyIsEnumerable.call(children.props, "ref")
2
+ ? children.props.ref // React 19 (children.ref still works, but gives a warning)
3
+ : children.ref; // React <19
4
+ //# sourceMappingURL=getChildRef.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getChildRef.js","sourceRoot":"","sources":["../../src/util/getChildRef.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,QAAoD,EAC1B,EAAE,CAC5B,MAAM,CAAC,SAAS,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IAC/D,CAAC,CAAE,QAAQ,CAAC,KAAa,CAAC,GAAG,CAAC,2DAA2D;IACzF,CAAC,CAAE,QAAgB,CAAC,GAAG,CAAC,CAAC,YAAY"}
@@ -1,13 +1,5 @@
1
- import { Args } from "@storybook/react";
1
+ import { StoryObj } from "@storybook/react";
2
2
  import React from "react";
3
- import { Renderer, StoryContext } from "storybook/internal/types";
4
3
  export declare function renderStoriesForChromatic(stories: Record<string, {
5
4
  render?: (...args: any[]) => React.ReactNode;
6
- } | React.FunctionComponent<void>>): {
7
- render: (args_0: Args, args_1: StoryContext<Renderer, Args>) => React.JSX.Element[];
8
- parameters: {
9
- chromatic: {
10
- disable: boolean;
11
- };
12
- };
13
- };
5
+ } | React.FunctionComponent<void>>): StoryObj;
@@ -1,12 +1,12 @@
1
1
  import React from "react";
2
2
  export function renderStoriesForChromatic(stories) {
3
3
  return {
4
- render: (...args) => Object.entries(stories).map(([storyName, story]) => {
4
+ render: (...args) => (React.createElement(React.Fragment, null, Object.entries(stories).map(([storyName, story]) => {
5
5
  var _a;
6
6
  return (React.createElement("div", { key: storyName },
7
7
  React.createElement("h2", { className: "storyheading" }, storyName),
8
8
  typeof story === "function" ? story() : (_a = story.render) === null || _a === void 0 ? void 0 : _a.call(story, ...args)));
9
- }),
9
+ }))),
10
10
  parameters: {
11
11
  chromatic: { disable: false },
12
12
  },
@@ -1 +1 @@
1
- {"version":3,"file":"renderStoriesForChromatic.js","sourceRoot":"","sources":["../../src/util/renderStoriesForChromatic.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,MAAM,UAAU,yBAAyB,CACvC,OAIC;IAED,OAAO;QACL,MAAM,EAAE,CAAC,GAAG,IAA0C,EAAE,EAAE,CACxD,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,KAAK,CAAC,EAAE,EAAE;;YAAC,OAAA,CAClD,6BAAK,GAAG,EAAE,SAAS;gBACjB,4BAAI,SAAS,EAAC,cAAc,IAAE,SAAS,CAAM;gBAC5C,OAAO,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,MAAA,KAAK,CAAC,MAAM,sDAAG,GAAG,IAAI,CAAC,CAC5D,CACP,CAAA;SAAA,CAAC;QACJ,UAAU,EAAE;YACV,SAAS,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE;SAC9B;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"renderStoriesForChromatic.js","sourceRoot":"","sources":["../../src/util/renderStoriesForChromatic.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,MAAM,UAAU,yBAAyB,CACvC,OAIC;IAED,OAAO;QACL,MAAM,EAAE,CAAC,GAAG,IAA0C,EAAE,EAAE,CAAC,CACzD,0CACG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,KAAK,CAAC,EAAE,EAAE;;YAAC,OAAA,CACnD,6BAAK,GAAG,EAAE,SAAS;gBACjB,4BAAI,SAAS,EAAC,cAAc,IAAE,SAAS,CAAM;gBAC5C,OAAO,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,MAAA,KAAK,CAAC,MAAM,sDAAG,GAAG,IAAI,CAAC,CAC5D,CACP,CAAA;SAAA,CAAC,CACD,CACJ;QACD,UAAU,EAAE;YACV,SAAS,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE;SAC9B;KACF,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@navikt/ds-react",
3
- "version": "7.29.1",
3
+ "version": "7.30.0",
4
4
  "description": "React components from the Norwegian Labour and Welfare Administration.",
5
5
  "author": "Aksel, a team part of the Norwegian Labour and Welfare Administration.",
6
6
  "license": "MIT",
@@ -650,8 +650,8 @@
650
650
  "dependencies": {
651
651
  "@floating-ui/react": "0.27.8",
652
652
  "@floating-ui/react-dom": "^2.0.9",
653
- "@navikt/aksel-icons": "^7.29.1",
654
- "@navikt/ds-tokens": "^7.29.1",
653
+ "@navikt/aksel-icons": "^7.30.0",
654
+ "@navikt/ds-tokens": "^7.30.0",
655
655
  "clsx": "^2.1.0",
656
656
  "date-fns": "^4.0.0",
657
657
  "react-day-picker": "9.7.0"
@@ -76,7 +76,9 @@ const DatePickerWeekNumber = ({
76
76
  );
77
77
  }}
78
78
  icon={
79
- <span className="navds-date__weeknumber-number">{weekNumber}</span>
79
+ <span className={cn("navds-date__weeknumber-number")}>
80
+ {weekNumber}
81
+ </span>
80
82
  }
81
83
  />
82
84
  </td>
@@ -3,6 +3,7 @@ import { useRenameCSS } from "../../theme/Theme";
3
3
  import FormSummaryAnswer from "./FormSummaryAnswer";
4
4
  import FormSummaryAnswers from "./FormSummaryAnswers";
5
5
  import FormSummaryEditLink from "./FormSummaryEditLink";
6
+ import FormSummaryFooter from "./FormSummaryFooter";
6
7
  import FormSummaryHeader from "./FormSummaryHeader";
7
8
  import FormSummaryHeading from "./FormSummaryHeading";
8
9
  import FormSummaryLabel from "./FormSummaryLabel";
@@ -13,7 +14,7 @@ interface FormSummaryComponent
13
14
  FormSummaryProps & React.RefAttributes<HTMLDivElement>
14
15
  > {
15
16
  /**
16
- * Must include `<FormSummary.Heading>` and optionally `<FormSummary.EditLink>`.
17
+ * Must include `<FormSummary.Heading>`.
17
18
  */
18
19
  Header: typeof FormSummaryHeader;
19
20
  /**
@@ -21,7 +22,7 @@ interface FormSummaryComponent
21
22
  */
22
23
  Heading: typeof FormSummaryHeading;
23
24
  /**
24
- * Link to edit the answers to use in the `FormSummary.Header` component. Should link to the relevant part of the form.
25
+ * Link to edit the answers to use in the `FormSummary.Footer` component. Should link to the relevant part of the form.
25
26
  */
26
27
  EditLink: typeof FormSummaryEditLink;
27
28
  /**
@@ -40,6 +41,10 @@ interface FormSummaryComponent
40
41
  * Corresponds to the answer in the form. To be used in the `FormSummary.Answer` component.
41
42
  */
42
43
  Value: typeof FormSummaryValue;
44
+ /**
45
+ * Footer component for the form summary, if applicable this is a good place for `<FormSummary.EditLink>`.
46
+ */
47
+ Footer: typeof FormSummaryFooter;
43
48
  }
44
49
 
45
50
  export interface FormSummaryProps extends HTMLAttributes<HTMLDivElement> {
@@ -48,20 +53,7 @@ export interface FormSummaryProps extends HTMLAttributes<HTMLDivElement> {
48
53
  *
49
54
  * - `<FormSummary.Header>`
50
55
  * - `<FormSummary.Answers>`
51
- *
52
- * @example
53
- * <FormSummary>
54
- * <FormSummary.Header>
55
- * <FormSummary.Heading level="2">HeadingTekst</FormSummary.Heading>
56
- * <FormSummary.EditLink href="#" />
57
- * </FormSummary.Header>
58
- * <FormSummary.Answers>
59
- * <FormSummary.Answer>
60
- * <FormSummary.Label>Navn</FormSummary.Label>
61
- * <FormSummary.Value>Ola Nordmann</FormSummary.Value>
62
- * </FormSummary.Answer>
63
- * </FormSummary.Answers>
64
- * </FormSummary>
56
+ * - `<FormSummary.Footer>` (optional)
65
57
  */
66
58
  children: React.ReactNode;
67
59
  }
@@ -75,7 +67,6 @@ export interface FormSummaryProps extends HTMLAttributes<HTMLDivElement> {
75
67
  * <FormSummary>
76
68
  * <FormSummary.Header>
77
69
  * <FormSummary.Heading level="2">HeadingTekst</FormSummary.Heading>
78
- * <FormSummary.EditLink href="#" />
79
70
  * </FormSummary.Header>
80
71
  * <FormSummary.Answers>
81
72
  * <FormSummary.Answer>
@@ -83,6 +74,9 @@ export interface FormSummaryProps extends HTMLAttributes<HTMLDivElement> {
83
74
  * <FormSummary.Value>Ola Nordmann</FormSummary.Value>
84
75
  * </FormSummary.Answer>
85
76
  * </FormSummary.Answers>
77
+ * <FormSummary.Footer>
78
+ * <FormSummary.EditLink href="#" />
79
+ * </FormSummary.Footer>
86
80
  * </FormSummary>
87
81
  */
88
82
  export const FormSummary = forwardRef<HTMLDivElement, FormSummaryProps>(
@@ -104,5 +98,6 @@ FormSummary.Answers = FormSummaryAnswers;
104
98
  FormSummary.Answer = FormSummaryAnswer;
105
99
  FormSummary.Label = FormSummaryLabel;
106
100
  FormSummary.Value = FormSummaryValue;
101
+ FormSummary.Footer = FormSummaryFooter;
107
102
 
108
103
  export default FormSummary;
@@ -1,6 +1,8 @@
1
1
  import React, { forwardRef } from "react";
2
+ import { PencilIcon } from "@navikt/aksel-icons";
2
3
  import { Link } from "../../link";
3
4
  import { useRenameCSS } from "../../theme/Theme";
5
+ import { CompositionWarning } from "../../util/composition-warning";
4
6
  import { useI18n } from "../../util/i18n/i18n.hooks";
5
7
  import { OverridableComponent } from "../../util/types";
6
8
 
@@ -24,17 +26,24 @@ export const FormSummaryEditLink: OverridableComponent<
24
26
  HTMLAnchorElement
25
27
  > = forwardRef(({ children, className, as = "a", ...rest }, ref) => {
26
28
  const { cn } = useRenameCSS();
29
+
27
30
  const translate = useI18n("FormSummary");
28
31
 
29
32
  return (
30
- <Link
31
- ref={ref}
32
- as={as}
33
- {...rest}
34
- className={cn("navds-form-summary__edit", className)}
33
+ <CompositionWarning.Forbidden
34
+ name="FormSummary.Header"
35
+ message="<FormSummary.EditLink> should not be placed in <FormSummary.Header> anymore. See https://aksel.nav.no/komponenter/core/formsummary"
35
36
  >
36
- {children || translate("editAnswer")}
37
- </Link>
37
+ <Link
38
+ ref={ref}
39
+ as={as}
40
+ {...rest}
41
+ className={cn("navds-form-summary__edit", className)}
42
+ >
43
+ <PencilIcon aria-hidden fontSize="1.5rem" />
44
+ {children || translate("editAnswer")}
45
+ </Link>
46
+ </CompositionWarning.Forbidden>
38
47
  );
39
48
  });
40
49
 
@@ -0,0 +1,33 @@
1
+ import React, { forwardRef } from "react";
2
+ import { useRenameCSS } from "../../theme/Theme";
3
+
4
+ /**
5
+ * Footer slot for actions in `FormSummary`.
6
+ */
7
+ export interface FormSummaryFooterProps
8
+ extends React.HTMLAttributes<HTMLDivElement> {
9
+ /**
10
+ * Should include `<FormSummary.EditLink>`.
11
+ */
12
+ children: React.ReactNode;
13
+ }
14
+
15
+ export const FormSummaryFooter = forwardRef<
16
+ HTMLDivElement,
17
+ FormSummaryFooterProps
18
+ >(({ children, className, ...rest }, ref) => {
19
+ const { cn } = useRenameCSS();
20
+
21
+ return (
22
+ <div
23
+ ref={ref}
24
+ data-color="info"
25
+ {...rest}
26
+ className={cn("navds-form-summary__footer", className)}
27
+ >
28
+ {children}
29
+ </div>
30
+ );
31
+ });
32
+
33
+ export default FormSummaryFooter;
@@ -1,10 +1,11 @@
1
1
  import React, { forwardRef } from "react";
2
2
  import { useRenameCSS } from "../../theme/Theme";
3
+ import { CompositionWarning } from "../../util/composition-warning";
3
4
 
4
5
  export interface FormSummaryHeaderProps
5
6
  extends React.HTMLAttributes<HTMLDivElement> {
6
7
  /**
7
- * Must include `<FormSummary.Heading>` and optionally `<FormSummary.EditLink>`.
8
+ * Must include `<FormSummary.Heading>`.
8
9
  */
9
10
  children: React.ReactNode;
10
11
  }
@@ -12,17 +13,19 @@ export interface FormSummaryHeaderProps
12
13
  export const FormSummaryHeader = forwardRef<
13
14
  HTMLDivElement,
14
15
  FormSummaryHeaderProps
15
- >(({ children, className, ...rest }, ref) => {
16
+ >(({ children, className, ...rest }: FormSummaryHeaderProps, ref) => {
16
17
  const { cn } = useRenameCSS();
17
18
 
18
19
  return (
19
- <header
20
- ref={ref}
21
- {...rest}
22
- className={cn("navds-form-summary__header", className)}
23
- >
24
- {children}
25
- </header>
20
+ <CompositionWarning.Root name="FormSummary.Header">
21
+ <div
22
+ ref={ref}
23
+ {...rest}
24
+ className={cn("navds-form-summary__header", className)}
25
+ >
26
+ {children}
27
+ </div>
28
+ </CompositionWarning.Root>
26
29
  );
27
30
  });
28
31
 
@@ -12,6 +12,10 @@ export {
12
12
  default as FormSummaryEditLink,
13
13
  type FormSummaryEditProps,
14
14
  } from "./FormSummaryEditLink";
15
+ export {
16
+ default as FormSummaryFooter,
17
+ type FormSummaryFooterProps,
18
+ } from "./FormSummaryFooter";
15
19
  export {
16
20
  default as FormSummaryHeader,
17
21
  type FormSummaryHeaderProps,
@@ -32,7 +32,9 @@ export interface TimelinePeriodProps
32
32
  /**
33
33
  * Callback when selecting a period.
34
34
  */
35
- onSelectPeriod?: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
35
+ onSelectPeriod?: (
36
+ event: React.MouseEvent<Element> | React.KeyboardEvent<Element>,
37
+ ) => void;
36
38
  /**
37
39
  * Content displayed in Popover on click.
38
40
  */
@@ -1,82 +1,71 @@
1
1
  import React, { ReactNode } from "react";
2
+ import type { TimelinePeriod, TimelineRow, TimelineRowProps } from "..";
2
3
  import { omit } from "../../util";
4
+ import { getChildRef } from "../../util/getChildRef";
3
5
  import { Period } from "./types.external";
4
6
 
5
- type ParsedChild = {
6
- label?: string;
7
+ type TimelineRowPropsWithRef = React.ComponentProps<typeof TimelineRow>;
8
+ type TimelinePeriodPropsWithRef = React.ComponentProps<typeof TimelinePeriod>;
9
+
10
+ type ParsedRow = {
11
+ label: string;
7
12
  icon?: React.ReactNode;
8
- headingTag: string;
13
+ headingTag?: string;
9
14
  periods: Omit<Period, "id" | "endInclusive">[];
10
- restProps: any;
11
- ref: any;
15
+ restProps: Omit<TimelineRowProps, "label" | "icon" | "headingTag">;
16
+ ref?: React.Ref<HTMLOListElement>;
12
17
  };
13
18
 
14
19
  export const parseRows = (rowChildren: ReactNode[]) => {
15
- const parsedChildren: ParsedChild[] = [];
16
- rowChildren?.forEach((r: React.ReactNode) => {
17
- const periods: ParsedChild["periods"] = [];
18
- if (React.isValidElement(r) && r?.props?.children) {
19
- if (Array.isArray(r.props.children)) {
20
- for (let i = 0; i < r.props.children.length; i++) {
21
- const p = r.props.children[i];
20
+ const parsedChildren: ParsedRow[] = [];
21
+
22
+ rowChildren?.forEach((row: React.ReactNode) => {
23
+ const periods: ParsedRow["periods"] = [];
24
+
25
+ if (
26
+ !React.isValidElement<TimelineRowPropsWithRef>(row) ||
27
+ !row.props.children
28
+ ) {
29
+ return;
30
+ }
22
31
 
23
- periods.push({
24
- start: p?.props?.start,
25
- end: p?.props?.end,
26
- status: p?.props?.status || "neutral",
27
- onSelectPeriod: p.props?.onSelectPeriod,
28
- label: r.props.label,
29
- icon: p.props.icon,
30
- children: p.props.children,
31
- isActive: p.props.isActive,
32
- statusLabel: p.props.statusLabel,
33
- restProps: omit(p.props, [
34
- "start",
35
- "end",
36
- "status",
37
- "onSelectPeriod",
38
- "label",
39
- "icon",
40
- "children",
41
- "isActive",
42
- "statusLabel",
43
- ]),
44
- ref: p?.ref,
45
- });
46
- }
47
- } else {
48
- periods.push({
49
- start: r.props.children.props.start,
50
- end: r.props.children.props.end,
51
- status: r.props.children.props?.status || "neutral",
52
- onSelectPeriod: r.props.children.props?.onSelectPeriod,
53
- label: r.props.label,
54
- icon: r.props.children.props?.icon,
55
- children: r.props.children.props?.children,
56
- statusLabel: r.props.children.props?.statusLabel,
57
- restProps: omit(r.props.children.props, [
58
- "start",
59
- "end",
60
- "status",
61
- "onSelectPeriod",
62
- "label",
63
- "icon",
64
- "children",
65
- "isActive",
66
- "statusLabel",
67
- ]),
68
- ref: r.props?.children?.ref,
69
- });
32
+ React.Children.toArray(row.props.children).forEach((period) => {
33
+ if (!React.isValidElement<TimelinePeriodPropsWithRef>(period)) {
34
+ return;
70
35
  }
71
- parsedChildren.push({
72
- label: r.props.label,
73
- icon: r.props.icon,
74
- headingTag: r.props.headingTag,
75
- periods,
76
- restProps: omit(r.props, ["label", "icon", "headingTag"]),
77
- ref: (r as any)?.ref,
36
+
37
+ periods.push({
38
+ start: period.props.start,
39
+ end: period.props.end,
40
+ status: period.props.status || "neutral",
41
+ onSelectPeriod: period.props.onSelectPeriod,
42
+ icon: period.props.icon,
43
+ children: period.props.children,
44
+ isActive: period.props.isActive,
45
+ statusLabel: period.props.statusLabel,
46
+ restProps: omit(period.props, [
47
+ "start",
48
+ "end",
49
+ "status",
50
+ "onSelectPeriod",
51
+ "icon",
52
+ "children",
53
+ "isActive",
54
+ "statusLabel",
55
+ "placement",
56
+ ]),
57
+ ref: getChildRef(period),
78
58
  });
79
- }
59
+ });
60
+
61
+ parsedChildren.push({
62
+ label: row.props.label,
63
+ icon: row.props.icon,
64
+ headingTag: row.props.headingTag,
65
+ periods,
66
+ restProps: omit(row.props, ["label", "icon", "headingTag"]),
67
+ ref: getChildRef(row),
68
+ });
80
69
  });
81
70
 
82
71
  return parsedChildren;
@@ -16,18 +16,19 @@ export interface Positioned {
16
16
 
17
17
  export interface Period {
18
18
  id: string;
19
- label?: string;
20
19
  start: Date;
21
20
  endInclusive: Date;
22
21
  status?: PeriodStatus;
23
- onSelectPeriod?: () => void;
22
+ onSelectPeriod?: (
23
+ e: React.MouseEvent<Element, MouseEvent> | React.KeyboardEvent<Element>,
24
+ ) => void;
24
25
  icon?: React.ReactNode;
25
26
  children?: React.ReactNode;
26
27
  end: Date;
27
28
  isActive?: boolean;
28
29
  statusLabel?: string;
29
30
  restProps?: any;
30
- ref?: Element;
31
+ ref?: React.Ref<HTMLDivElement | HTMLButtonElement>;
31
32
  }
32
33
 
33
34
  export interface PositionedPeriod extends Period, Positioned {
@@ -21,7 +21,9 @@ export interface Period {
21
21
  start: Date;
22
22
  endInclusive: Date;
23
23
  status?: PeriodStatus;
24
- onSelectPeriod?: () => void;
24
+ onSelectPeriod?: (
25
+ e: React.MouseEvent<Element, MouseEvent> | React.KeyboardEvent<Element>,
26
+ ) => void;
25
27
  icon?: React.ReactNode;
26
28
  children?: React.ReactNode;
27
29
  isActive?: boolean;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Give warnings based on component composition (which slot/parent a child is rendered in).
3
+ *
4
+ * Used when child components need to know which slot/parent they are rendered in
5
+ * (e.g. `FormSummary.Header` vs `FormSummary.Footer`) and should warn or error in development
6
+ * if placed in a discouraged or forbidden slot.
7
+ *
8
+ * Usage:
9
+ * - Wrap slot components with <CompositionWarning.Root name="FormSummary.Header">...</CompositionWarning.Root>
10
+ * - In child: `<CompositionWarning.Forbidden name="FormSummary.Header" />` to forbid slot.
11
+ *
12
+ * This is guidance only: warnings are logged to the console in development, never enforced at runtime.
13
+ */
14
+ import React, { useEffect, useRef } from "react";
15
+ import { Slot } from "../../slot/Slot";
16
+ import { createContext } from "../create-context";
17
+
18
+ type CompositionName = string;
19
+
20
+ const isDev = process.env.NODE_ENV !== "production";
21
+
22
+ type CompositionWarningContextType = {
23
+ /**
24
+ * Name of the slot component we want to check.
25
+ */
26
+ name: CompositionName;
27
+ };
28
+
29
+ const [CompositionWarning, useCompositionWarning] =
30
+ createContext<CompositionWarningContextType>({
31
+ errorMessage:
32
+ "useCompositionWarning() must be used within <CompositionWarning />",
33
+ });
34
+
35
+ type CompositionWarningForbiddenProps = {
36
+ children?: React.ReactElement;
37
+ /**
38
+ * Name of the parent slot component where the child is not allowed.
39
+ */
40
+ name: CompositionName;
41
+ /**
42
+ * Warning message to display if the child is found.
43
+ */
44
+ message: string;
45
+ };
46
+
47
+ function CompositionWarningForbidden({
48
+ children,
49
+ name,
50
+ message,
51
+ }: CompositionWarningForbiddenProps) {
52
+ const compositionName = useCompositionWarning(false)?.name;
53
+
54
+ const elementRef = useRef<HTMLElement>(null);
55
+
56
+ useEffect(() => {
57
+ if (!isDev || !compositionName || name !== compositionName) {
58
+ return;
59
+ }
60
+
61
+ console.warn(`[Aksel] ${message}\nElement: `, elementRef.current);
62
+ }, [compositionName, name, message]);
63
+
64
+ return <Slot ref={elementRef}>{children}</Slot>;
65
+ }
66
+
67
+ export { CompositionWarningForbidden as Forbidden, CompositionWarning as Root };
@@ -0,0 +1 @@
1
+ export * as CompositionWarning from "./CompositionWarning";
@@ -0,0 +1,6 @@
1
+ export const getChildRef = <T>(
2
+ children: React.ReactElement<React.RefAttributes<T>>,
3
+ ): React.Ref<T> | undefined =>
4
+ Object.prototype.propertyIsEnumerable.call(children.props, "ref")
5
+ ? (children.props as any).ref // React 19 (children.ref still works, but gives a warning)
6
+ : (children as any).ref; // React <19
@@ -1,4 +1,4 @@
1
- import { Args } from "@storybook/react";
1
+ import { Args, StoryObj } from "@storybook/react";
2
2
  import React from "react";
3
3
  import { Renderer, StoryContext } from "storybook/internal/types";
4
4
 
@@ -8,15 +8,18 @@ export function renderStoriesForChromatic(
8
8
  | { render?: (...args: any[]) => React.ReactNode }
9
9
  | React.FunctionComponent<void>
10
10
  >,
11
- ) {
11
+ ): StoryObj {
12
12
  return {
13
- render: (...args: [Args, StoryContext<Renderer, Args>]) =>
14
- Object.entries(stories).map(([storyName, story]) => (
15
- <div key={storyName}>
16
- <h2 className="storyheading">{storyName}</h2>
17
- {typeof story === "function" ? story() : story.render?.(...args)}
18
- </div>
19
- )),
13
+ render: (...args: [Args, StoryContext<Renderer, Args>]) => (
14
+ <>
15
+ {Object.entries(stories).map(([storyName, story]) => (
16
+ <div key={storyName}>
17
+ <h2 className="storyheading">{storyName}</h2>
18
+ {typeof story === "function" ? story() : story.render?.(...args)}
19
+ </div>
20
+ ))}
21
+ </>
22
+ ),
20
23
  parameters: {
21
24
  chromatic: { disable: false },
22
25
  },