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..79fd89db07e 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
@@ -11,6 +11,7 @@ showTabs: true
| `to` | _(optional)_ use this prop only if you are using a router Link component as the `element` that uses the `to` property to declare the navigation url. |
| `target` | _(optional)_ defines the opening method. Use `_blank` to open a new browser window/tab. |
| `targetBlankTitle` | _(optional)_ the title shown as a tooltip when target is set to `_blank`. |
+| `scrollToHash` | _(optional)_ When set to true, a click will attempt to use `window.scroll` to support the missing Chromium support. |
| `tooltip` | _(optional)_ Provide a string or a React Element to be shown as the tooltip content. |
| `skeleton` | _(optional)_ if set to `true`, an overlaying skeleton with animation will be shown. |
| [Space](/uilib/components/space/properties) | _(optional)_ spacing properties like `top` or `bottom` are supported. |
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..9d8a4c4bc4e 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,8 @@
*
*/
-import React, { MouseEvent } from 'react'
+import React from 'react'
import { Link } from '@dnb/eufemia/src'
-import { getOffsetTop } from '@dnb/eufemia/src/shared/helpers'
const Anchor = ({ children, href, ...rest }) => {
if (/^http/.test(href) || href[0] === '!') {
@@ -17,34 +16,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..9f46aeed8ca 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'
@@ -26,6 +27,13 @@ export type AnchorProps = {
omitClass?: boolean
innerRef?: React.RefObject
+ /**
+ * When set to true,
+ * and there is a hash with an existing and matching id,
+ * it uses window.scroll to support the missing scroll feature in Chromium browsers.
+ */
+ scrollToHash?: boolean
+
/** @deprecated use innerRef instead */
inner_ref?: React.RefObject
}
@@ -65,6 +73,7 @@ export function AnchorInstance(localProps: AnchorAllProps) {
omitClass,
innerRef,
targetBlankTitle,
+ scrollToHash,
...rest
} = allProps
@@ -94,6 +103,13 @@ export function AnchorInstance(localProps: AnchorAllProps) {
'dnb-anchor--no-icon'
)}
{...attributes}
+ onClick={(e) => {
+ attributes?.['onClick']?.(e)
+
+ if (scrollToHash) {
+ scrollToHashHandler(e)
+ }
+ }}
innerRef={innerRef}
>
{children}
@@ -111,6 +127,50 @@ export function AnchorInstance(localProps: AnchorAllProps) {
)}
>
)
+
+ function scrollToHashHandler(
+ e: React.MouseEvent
+ ) {
+ const element = e.currentTarget as HTMLAnchorElement
+ const href = element.getAttribute('href')
+ if (!href.includes('#') || typeof document === 'undefined') {
+ 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 samePath =
+ href.startsWith('#') ||
+ window.location.href.includes(removeEndingSlash(element.pathname))
+
+ // Only continue, when we are sure we are on the same page,
+ // because, the same ID may exists occasionally on the current page.
+ if (samePath) {
+ 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 })
+ }
+ }
+ }
+}
+
+const removeEndingSlash = (url: string) => {
+ return url.replace(/\/$/, '')
}
const Anchor = React.forwardRef(
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..00a806261c7
--- /dev/null
+++ b/packages/dnb-eufemia/src/components/anchor/__tests__/AnchorScroll.test.tsx
@@ -0,0 +1,136 @@
+/**
+ * Element Test
+ *
+ */
+
+import React from 'react'
+import { fireEvent, render } from '@testing-library/react'
+import Anchor from '../Anchor'
+
+describe('Anchor with scrollToHash', () => {
+ let location: Location
+
+ beforeEach(() => {
+ location = window.location
+ // jest.spyOn(window, 'location', 'get').mockRestore()
+ })
+
+ 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)
+ })
+})