diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/anchor/properties.mdx b/packages/dnb-design-system-portal/src/docs/uilib/components/anchor/properties.mdx
index c38e4e599e1..18585c0ebd5 100644
--- a/packages/dnb-design-system-portal/src/docs/uilib/components/anchor/properties.mdx
+++ b/packages/dnb-design-system-portal/src/docs/uilib/components/anchor/properties.mdx
@@ -28,3 +28,23 @@ render(
)
```
+
+### Anchor hash
+
+Some browser like Chrome (behind a flag) does still not support animated anchor hash clicks when CSS `scroll-behavior: smooth;` is set. To make it work, you can provide the `scrollToHashHandler` helper function to the Anchor:
+
+```jsx
+import Anchor, {
+ scrollToHashHandler,
+} from '@dnb/eufemia/components/Anchor'
+
+render(
+ <>
+
+ {children}
+
+
+
element to scroll to
+ >
+)
+```
diff --git a/packages/dnb-design-system-portal/src/shared/tags/Anchor.tsx b/packages/dnb-design-system-portal/src/shared/tags/Anchor.tsx
index a237c3988eb..9fd4188267f 100644
--- a/packages/dnb-design-system-portal/src/shared/tags/Anchor.tsx
+++ b/packages/dnb-design-system-portal/src/shared/tags/Anchor.tsx
@@ -3,9 +3,9 @@
*
*/
-import React, { MouseEvent } from 'react'
+import React from 'react'
import { Link } from '@dnb/eufemia/src'
-import { getOffsetTop } from '@dnb/eufemia/src/shared/helpers'
+import { scrollToHashHandler } from '@dnb/eufemia/src/components/Anchor'
const Anchor = ({ children, href, ...rest }) => {
if (/^http/.test(href) || href[0] === '!') {
@@ -17,34 +17,10 @@ const Anchor = ({ children, href, ...rest }) => {
}
return (
-
+
{children}
)
-
- function clickHandler(e: MouseEvent) {
- /**
- * What happens here?
- * When `scroll-behavior: smooth;` in CSS is set,
- * Blink/Chromium wants the user to click two times in order to actually scroll to the anchor hash.
- * The first click, sets the hash, the second one, srollts to it.
- * We want Chromium browsers to scorll to the element on the first click.
- */
- const id = e.currentTarget
- .getAttribute('href')
- .split(/#/g)
- .reverse()[0]
- const anchorElem = document.getElementById(id)
- if (anchorElem instanceof HTMLElement) {
- e.preventDefault()
-
- const scrollPadding = parseFloat(
- window.getComputedStyle(document.documentElement).scrollPaddingTop
- )
- const top = getOffsetTop(anchorElem) - scrollPadding
- window.scroll({ top })
- }
- }
}
export default Anchor
diff --git a/packages/dnb-eufemia/src/components/anchor/Anchor.tsx b/packages/dnb-eufemia/src/components/anchor/Anchor.tsx
index a20ad0b60ec..5a516493957 100644
--- a/packages/dnb-eufemia/src/components/anchor/Anchor.tsx
+++ b/packages/dnb-eufemia/src/components/anchor/Anchor.tsx
@@ -11,6 +11,7 @@ import {
makeUniqueId,
extendPropsWithContext,
} from '../../shared/component-helper'
+import { getOffsetTop } from '../../shared/helpers'
import Tooltip from '../tooltip/Tooltip'
import type { SkeletonShow } from '../skeleton/Skeleton'
import type { SpacingProps } from '../../shared/types'
@@ -120,3 +121,43 @@ const Anchor = React.forwardRef(
)
export default Anchor
+
+export function scrollToHashHandler(
+ e: React.MouseEvent
+) {
+ const element = e.currentTarget as HTMLAnchorElement
+ const href = element.getAttribute('href')
+
+ if (typeof document === 'undefined' || !href.includes('#')) {
+ return // stop here
+ }
+
+ /**
+ * What happens here?
+ * When `scroll-behavior: smooth;` in CSS is set,
+ * Blink/Chromium wants the user to click two times in order to actually scroll to the anchor hash.
+ * The first click, sets the hash, the second one, srollts to it.
+ * We want Chromium browsers to scorll to the element on the first click.
+ */
+ const isSamePath =
+ href.startsWith('#') ||
+ window.location.href.includes(element.pathname?.replace(/\/$/, ''))
+
+ // Only continue, when we are sure we are on the same page,
+ // because, the same ID may exists occasionally on the current page.
+ if (isSamePath) {
+ const id = href.split(/#/g).reverse()[0]
+ const anchorElem = document.getElementById(id)
+
+ if (anchorElem instanceof HTMLElement) {
+ e.preventDefault()
+
+ const scrollPadding = parseFloat(
+ window.getComputedStyle(document.documentElement).scrollPaddingTop
+ )
+ const top = getOffsetTop(anchorElem) - scrollPadding || 0
+
+ window.scroll({ top })
+ }
+ }
+}
diff --git a/packages/dnb-eufemia/src/components/anchor/__tests__/AnchorScroll.test.tsx b/packages/dnb-eufemia/src/components/anchor/__tests__/AnchorScroll.test.tsx
new file mode 100644
index 00000000000..5216a2c18ed
--- /dev/null
+++ b/packages/dnb-eufemia/src/components/anchor/__tests__/AnchorScroll.test.tsx
@@ -0,0 +1,138 @@
+/**
+ * Element Test
+ *
+ */
+
+import React from 'react'
+import { fireEvent, render } from '@testing-library/react'
+import Anchor, { scrollToHashHandler } from '../Anchor'
+
+describe('Anchor with scrollToHashHandler', () => {
+ let location: Location
+
+ beforeEach(() => {
+ location = window.location
+ })
+
+ it('should call window.scroll', () => {
+ const onScoll = jest.fn()
+
+ jest.spyOn(window, 'scroll').mockImplementationOnce(onScoll)
+ jest.spyOn(window, 'location', 'get').mockReturnValueOnce({
+ ...location,
+ href: 'http://localhost/path',
+ })
+
+ render(
+ <>
+
+ text
+
+
+ >
+ )
+
+ const element = document.querySelector('a')
+ fireEvent.click(element)
+
+ expect(onScoll).toHaveBeenCalledTimes(1)
+ expect(onScoll).toHaveBeenCalledWith({ top: 0 })
+ })
+
+ it('should use last hash', () => {
+ const onScoll = jest.fn()
+
+ jest.spyOn(window, 'scroll').mockImplementationOnce(onScoll)
+ jest.spyOn(window, 'location', 'get').mockReturnValueOnce({
+ ...location,
+ href: 'http://localhost/path',
+ })
+
+ render(
+ <>
+
+ text
+
+
+ >
+ )
+
+ const element = document.querySelector('a')
+ fireEvent.click(element)
+
+ expect(onScoll).toHaveBeenCalledTimes(1)
+ expect(onScoll).toHaveBeenCalledWith({ top: 0 })
+ })
+
+ it('should not call window.scroll when no hash-id found', () => {
+ const onScoll = jest.fn()
+
+ jest.spyOn(window, 'scroll').mockImplementationOnce(onScoll)
+ jest.spyOn(window, 'location', 'get').mockReturnValueOnce({
+ ...location,
+ href: 'http://localhost/path',
+ })
+
+ render(
+ <>
+
+ text
+
+
+ >
+ )
+
+ const element = document.querySelector('a')
+ fireEvent.click(element)
+
+ expect(onScoll).toHaveBeenCalledTimes(0)
+ })
+
+ it('will skip when no # exists in href', () => {
+ const onScoll = jest.fn()
+
+ jest.spyOn(window, 'scroll').mockImplementationOnce(onScoll)
+ jest.spyOn(window, 'location', 'get').mockReturnValueOnce({
+ ...location,
+ href: 'http://localhost/path',
+ })
+
+ render(
+
+ text
+
+ )
+
+ const element = document.querySelector('a')
+ fireEvent.click(element)
+
+ expect(onScoll).toHaveBeenCalledTimes(0)
+ })
+
+ it('should not call window.scroll when not on same page', () => {
+ const onScoll = jest.fn()
+
+ jest.spyOn(window, 'scroll').mockImplementationOnce(onScoll)
+ jest.spyOn(window, 'location', 'get').mockReturnValueOnce({
+ ...location,
+ href: 'http://localhost/path',
+ })
+
+ render(
+ <>
+
+ text
+
+
+ >
+ )
+
+ const element = document.querySelector('a')
+ fireEvent.click(element)
+
+ expect(onScoll).toHaveBeenCalledTimes(0)
+ })
+})