From 3a944765e0256de7d3f7060280edc1f525705230 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kristoffer=20Nordstr=C3=B6m?= <totte.nordstrom@gmail.com>
Date: Mon, 2 Dec 2024 11:35:02 +0100
Subject: [PATCH 1/3] feat(hooks): add package

---
 apps/storybook/stories/Hooks.mdx              |  9 ----
 package-lock.json                             | 15 +++++++
 packages/hooks/CHANGELOG.md                   |  0
 packages/hooks/README.md                      | 19 ++++++++
 packages/hooks/index.ts                       |  4 ++
 packages/hooks/package.json                   | 43 +++++++++++++++++++
 .../useWindowResize}/useWindowResize.test.tsx |  2 +-
 .../src/useWindowResize}/useWindowResize.tsx  |  9 ++--
 packages/hooks/stories/Changelog.mdx          |  6 +++
 packages/hooks/stories/Readme.mdx             |  6 +++
 packages/modal/src/Modal.tsx                  |  2 +-
 11 files changed, 100 insertions(+), 15 deletions(-)
 delete mode 100644 apps/storybook/stories/Hooks.mdx
 create mode 100644 packages/hooks/CHANGELOG.md
 create mode 100644 packages/hooks/README.md
 create mode 100644 packages/hooks/index.ts
 create mode 100644 packages/hooks/package.json
 rename packages/{modal/src => hooks/src/useWindowResize}/useWindowResize.test.tsx (97%)
 rename packages/{modal/src => hooks/src/useWindowResize}/useWindowResize.tsx (82%)
 create mode 100644 packages/hooks/stories/Changelog.mdx
 create mode 100644 packages/hooks/stories/Readme.mdx

diff --git a/apps/storybook/stories/Hooks.mdx b/apps/storybook/stories/Hooks.mdx
deleted file mode 100644
index f25405738..000000000
--- a/apps/storybook/stories/Hooks.mdx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { Meta } from "@storybook/blocks";
-
-<Meta title="Utils/Hooks" />
-
-# Utils
-
-## React Hooks
-
-We use the external library [usehooks-ts](https://usehooks-ts.com/) for Hooks.
diff --git a/package-lock.json b/package-lock.json
index 242adaf9e..c07dee677 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11318,6 +11318,10 @@
       "resolved": "packages/header",
       "link": true
     },
+    "node_modules/@sikt/sds-hooks": {
+      "resolved": "packages/hooks",
+      "link": true
+    },
     "node_modules/@sikt/sds-icons": {
       "resolved": "packages/icons",
       "link": true
@@ -49602,6 +49606,17 @@
         "react-dom": "^18.0.0 || ^19.0.0"
       }
     },
+    "packages/hooks": {
+      "name": "@sikt/sds-hooks",
+      "version": "0.1.0",
+      "license": "UNLICENSED",
+      "peerDependencies": {
+        "@types/react": "^18.0.0 || ^19.0.0",
+        "@types/react-dom": "^18.0.0 || ^19.0.0",
+        "react": "^18.0.0 || ^19.0.0",
+        "react-dom": "^18.0.0 || ^19.0.0"
+      }
+    },
     "packages/icons": {
       "name": "@sikt/sds-icons",
       "version": "3.0.0",
diff --git a/packages/hooks/CHANGELOG.md b/packages/hooks/CHANGELOG.md
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/hooks/README.md b/packages/hooks/README.md
new file mode 100644
index 000000000..fac5af94e
--- /dev/null
+++ b/packages/hooks/README.md
@@ -0,0 +1,19 @@
+# `@sikt/sds-hooks`
+
+## Consume
+
+```sh
+npm i -s @sikt/sds-hooks
+```
+
+### React
+
+```js
+import { use<Hook> } from "@sikt/sds-hooks";
+
+use<Hook>();
+```
+
+### Custom hooks
+
+We use [usehooks-ts](https://usehooks-ts.com/) for external hooks.
diff --git a/packages/hooks/index.ts b/packages/hooks/index.ts
new file mode 100644
index 000000000..f65bcd4b7
--- /dev/null
+++ b/packages/hooks/index.ts
@@ -0,0 +1,4 @@
+export {
+  useWindowResize,
+  type useWindowResizeOptions,
+} from "./src/useWindowResize/useWindowResize";
diff --git a/packages/hooks/package.json b/packages/hooks/package.json
new file mode 100644
index 000000000..b6d2af153
--- /dev/null
+++ b/packages/hooks/package.json
@@ -0,0 +1,43 @@
+{
+  "name": "@sikt/sds-hooks",
+  "version": "0.1.0",
+  "license": "UNLICENSED",
+  "description": "React Hooks, Sikt component library",
+  "homepage": "https://designsystem.sikt.no/",
+  "repository": {
+    "type": "git",
+    "url": "git+https://gitlab.sikt.no/designsystem/sds-komponentbibliotek.git"
+  },
+  "type": "commonjs",
+  "main": "dist/index.js",
+  "module": "dist/index.mjs",
+  "exports": {
+    ".": {
+      "import": {
+        "types": "./dist/index.d.mts",
+        "default": "./dist/index.mjs"
+      },
+      "require": {
+        "types": "./dist/index.d.ts",
+        "default": "./dist/index.js"
+      }
+    },
+    "./dist/*": "./dist/*"
+  },
+  "types": "dist/index.d.ts",
+  "files": [
+    "CHANGELOG.md",
+    "dist",
+    "README.md"
+  ],
+  "scripts": {
+    "build": "tsup"
+  },
+  "dependencies": {},
+  "peerDependencies": {
+    "@types/react": "^18.0.0 || ^19.0.0",
+    "@types/react-dom": "^18.0.0 || ^19.0.0",
+    "react": "^18.0.0 || ^19.0.0",
+    "react-dom": "^18.0.0 || ^19.0.0"
+  }
+}
diff --git a/packages/modal/src/useWindowResize.test.tsx b/packages/hooks/src/useWindowResize/useWindowResize.test.tsx
similarity index 97%
rename from packages/modal/src/useWindowResize.test.tsx
rename to packages/hooks/src/useWindowResize/useWindowResize.test.tsx
index aa93e663b..26130ecbc 100644
--- a/packages/modal/src/useWindowResize.test.tsx
+++ b/packages/hooks/src/useWindowResize/useWindowResize.test.tsx
@@ -1,5 +1,5 @@
 import { act, renderHook } from "@testing-library/react";
-import useWindowResize from "./useWindowResize";
+import { useWindowResize } from "./useWindowResize";
 
 describe("useWindowResize", () => {
   beforeEach(() => {
diff --git a/packages/modal/src/useWindowResize.tsx b/packages/hooks/src/useWindowResize/useWindowResize.tsx
similarity index 82%
rename from packages/modal/src/useWindowResize.tsx
rename to packages/hooks/src/useWindowResize/useWindowResize.tsx
index cf36c2083..3358980f2 100644
--- a/packages/modal/src/useWindowResize.tsx
+++ b/packages/hooks/src/useWindowResize/useWindowResize.tsx
@@ -1,10 +1,13 @@
 import { useEffect } from "react";
 
-interface Options {
+export interface useWindowResizeOptions {
   throttleTime?: number;
 }
 
-const useWindowResize = (callback: () => void, options?: Options) => {
+export const useWindowResize = (
+  callback: () => void,
+  options?: useWindowResizeOptions,
+) => {
   const { throttleTime = 200 } = options ?? {};
 
   useEffect(() => {
@@ -29,5 +32,3 @@ const useWindowResize = (callback: () => void, options?: Options) => {
     };
   }, [callback, throttleTime]);
 };
-
-export default useWindowResize;
diff --git a/packages/hooks/stories/Changelog.mdx b/packages/hooks/stories/Changelog.mdx
new file mode 100644
index 000000000..1e4bb1f31
--- /dev/null
+++ b/packages/hooks/stories/Changelog.mdx
@@ -0,0 +1,6 @@
+import { Markdown, Meta } from "@storybook/blocks";
+import Changelog from "../CHANGELOG.md?raw";
+
+<Meta title="Utils/Hooks/Changelog" />
+
+<Markdown>{Changelog}</Markdown>
diff --git a/packages/hooks/stories/Readme.mdx b/packages/hooks/stories/Readme.mdx
new file mode 100644
index 000000000..d6b2f5da6
--- /dev/null
+++ b/packages/hooks/stories/Readme.mdx
@@ -0,0 +1,6 @@
+import { Markdown, Meta } from "@storybook/blocks";
+import Readme from "../README.md?raw";
+
+<Meta title="Utils/Hooks/Readme" />
+
+<Markdown>{Readme}</Markdown>
diff --git a/packages/modal/src/Modal.tsx b/packages/modal/src/Modal.tsx
index a9432bbb5..74b6522b5 100644
--- a/packages/modal/src/Modal.tsx
+++ b/packages/modal/src/Modal.tsx
@@ -4,8 +4,8 @@ import { XIcon } from "@sikt/sds-icons";
 import { clsx } from "clsx/lite";
 import { ReactNode, useEffect, useId, useRef, useState } from "react";
 import ReactModal from "react-modal";
-import useWindowResize from "./useWindowResize";
 import "./modal.pcss";
+import useWindowResize from "./useWindowResize";
 
 export interface ModalProps {
   /**
-- 
GitLab


From 4424d1308de09f4646685f0aa7ba0c9440db4c30 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kristoffer=20Nordstr=C3=B6m?= <totte.nordstrom@gmail.com>
Date: Tue, 3 Dec 2024 14:49:41 +0100
Subject: [PATCH 2/3] refactor(modal): use hooks package

---
 package-lock.json            | 1 +
 packages/modal/package.json  | 1 +
 packages/modal/src/Modal.tsx | 2 +-
 3 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/package-lock.json b/package-lock.json
index c07dee677..09fcf4a74 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -49834,6 +49834,7 @@
       "dependencies": {
         "@sikt/sds-button": "^4.0.1",
         "@sikt/sds-core": "^4.1.1",
+        "@sikt/sds-hooks": "^0.1.0",
         "@sikt/sds-icons": "^2.0.2",
         "react-modal": "^3.16.1"
       },
diff --git a/packages/modal/package.json b/packages/modal/package.json
index 2dec7d59c..bf54fbe7b 100644
--- a/packages/modal/package.json
+++ b/packages/modal/package.json
@@ -37,6 +37,7 @@
   "dependencies": {
     "@sikt/sds-button": "^4.0.1",
     "@sikt/sds-core": "^4.1.1",
+    "@sikt/sds-hooks": "^0.1.0",
     "@sikt/sds-icons": "^2.0.2",
     "react-modal": "^3.16.1"
   },
diff --git a/packages/modal/src/Modal.tsx b/packages/modal/src/Modal.tsx
index 74b6522b5..026ae04ff 100644
--- a/packages/modal/src/Modal.tsx
+++ b/packages/modal/src/Modal.tsx
@@ -1,11 +1,11 @@
 import { Button } from "@sikt/sds-button";
 import { Heading1, Paragraph } from "@sikt/sds-core";
+import { useWindowResize } from "@sikt/sds-hooks";
 import { XIcon } from "@sikt/sds-icons";
 import { clsx } from "clsx/lite";
 import { ReactNode, useEffect, useId, useRef, useState } from "react";
 import ReactModal from "react-modal";
 import "./modal.pcss";
-import useWindowResize from "./useWindowResize";
 
 export interface ModalProps {
   /**
-- 
GitLab


From 11f8ad96422c94f8bafd3b32be336a2f6ec50188 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kristoffer=20Nordstr=C3=B6m?= <totte.nordstrom@gmail.com>
Date: Sun, 1 Dec 2024 13:29:11 +0100
Subject: [PATCH 3/3] fix(popover): anchored target follows trigger on layout
 shift or window resize

---
 package-lock.json                |  3 ++-
 packages/popover/package.json    |  3 ++-
 packages/popover/src/Popover.tsx | 27 ++++++++++++++++++++-------
 3 files changed, 24 insertions(+), 9 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 09fcf4a74..8c24b4bb8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -49910,7 +49910,8 @@
       "version": "1.0.1",
       "license": "UNLICENSED",
       "dependencies": {
-        "@sikt/sds-core": "^4.1.1"
+        "@sikt/sds-core": "^4.1.1",
+        "@sikt/sds-hooks": "^0.1.0"
       },
       "peerDependencies": {
         "@types/react": "^18.0.0 || ^19.0.0",
diff --git a/packages/popover/package.json b/packages/popover/package.json
index 6a95829df..a427d647d 100644
--- a/packages/popover/package.json
+++ b/packages/popover/package.json
@@ -35,7 +35,8 @@
     "build": "tsup"
   },
   "dependencies": {
-    "@sikt/sds-core": "^4.1.1"
+    "@sikt/sds-core": "^4.1.1",
+    "@sikt/sds-hooks": "^0.1.0"
   },
   "peerDependencies": {
     "@types/react": "^18.0.0 || ^19.0.0",
diff --git a/packages/popover/src/Popover.tsx b/packages/popover/src/Popover.tsx
index 4c1eb6639..a99095337 100644
--- a/packages/popover/src/Popover.tsx
+++ b/packages/popover/src/Popover.tsx
@@ -1,5 +1,13 @@
+import { useWindowResize } from "@sikt/sds-hooks";
 import { clsx } from "clsx/lite";
-import { HTMLAttributes, MouseEvent, ReactNode, useId, useState } from "react";
+import {
+  HTMLAttributes,
+  MouseEvent,
+  ReactNode,
+  useId,
+  useRef,
+  useState,
+} from "react";
 import "./popover.pcss";
 
 export interface PopoverProps extends HTMLAttributes<HTMLButtonElement> {
@@ -21,28 +29,33 @@ export const Popover = ({
   ...rest
 }: PopoverProps) => {
   const id = useId();
+  const buttonRef = useRef<HTMLButtonElement | null>(null);
   const [top, setTop] = useState(0);
   const [left, setLeft] = useState(0);
   const popovertargetAttr = { popovertarget: id };
   const popoverAttr = { popover };
 
   // TODO: Replace with https://developer.mozilla.org/en-US/docs/Web/CSS/position-anchor when good browser support
-  const setPopoverStylePosition = (event: MouseEvent<HTMLButtonElement>) => {
-    const target = event.target as HTMLButtonElement;
-    const bounding = target.getBoundingClientRect();
-    setTop(bounding.top + bounding.height + window.scrollY);
-    setLeft(bounding.left);
+  const setPopoverStylePosition = () => {
+    if (buttonRef.current) {
+      const bounding = buttonRef.current.getBoundingClientRect();
+      setTop(bounding.top + bounding.height + window.scrollY);
+      setLeft(bounding.left);
+    }
   };
 
+  anchor && useWindowResize(setPopoverStylePosition, { throttleTime: 10 });
+
   return (
     <>
       <button
+        ref={buttonRef}
         className={clsx("sds-popover", className)}
         // INFO: This is a hack to solve that React/TypeScript does not support this native attribute
         {...popovertargetAttr}
         {...rest}
         onClick={(event) => {
-          anchor && setPopoverStylePosition(event);
+          anchor && setPopoverStylePosition();
           if (onClick) {
             onClick(event);
           }
-- 
GitLab