@oslokommune/punkt-react 16.24.1 → 16.25.1
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.
- package/CHANGELOG.md +36 -0
- package/dist/index.d.ts +8 -0
- package/dist/punkt-react.cjs +1 -1
- package/dist/punkt-react.js +467 -453
- package/package.json +2 -2
- package/src/components/header/HeaderService.test.tsx +82 -0
- package/src/components/header/HeaderService.tsx +35 -15
- package/src/components/header/types.ts +2 -0
- package/src/components/link/Link.test.tsx +22 -0
- package/src/components/link/Link.tsx +30 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oslokommune/punkt-react",
|
|
3
|
-
"version": "16.
|
|
3
|
+
"version": "16.25.1",
|
|
4
4
|
"description": "React komponentbibliotek til Punkt, et designsystem laget av Oslo Origo",
|
|
5
5
|
"homepage": "https://punkt.oslo.kommune.no",
|
|
6
6
|
"author": "Team Designsystem, Oslo Origo",
|
|
@@ -100,5 +100,5 @@
|
|
|
100
100
|
"url": "https://github.com/oslokommune/punkt/issues"
|
|
101
101
|
},
|
|
102
102
|
"license": "MIT",
|
|
103
|
-
"gitHead": "
|
|
103
|
+
"gitHead": "5fe880d7964ac407f7271a8d80c178f72ca028a1"
|
|
104
104
|
}
|
|
@@ -512,4 +512,86 @@ describe('PktHeaderService', () => {
|
|
|
512
512
|
expect(logoutButtons.length).toBe(0)
|
|
513
513
|
})
|
|
514
514
|
})
|
|
515
|
+
|
|
516
|
+
// Closing the slot menu on intentional clicks inside the dropdown (mobile/tablet)
|
|
517
|
+
describe('slot menu closes on intentional click', () => {
|
|
518
|
+
const openSlotMenu = () => {
|
|
519
|
+
fireEvent.click(screen.getByRole('button', { name: 'Åpne meny' }))
|
|
520
|
+
expect(screen.getByRole('button', { name: 'Åpne meny' })).toHaveAttribute('aria-expanded', 'true')
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
beforeEach(() => {
|
|
524
|
+
mockedWidth = 500
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
it('closes when clicking a link inside the menu', () => {
|
|
528
|
+
render(
|
|
529
|
+
<PktHeaderService serviceName="Svc">
|
|
530
|
+
<a href="#">Gå hit</a>
|
|
531
|
+
</PktHeaderService>,
|
|
532
|
+
)
|
|
533
|
+
openSlotMenu()
|
|
534
|
+
fireEvent.click(screen.getByText('Gå hit'))
|
|
535
|
+
expect(screen.getByRole('button', { name: 'Åpne meny' })).toHaveAttribute('aria-expanded', 'false')
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
it('closes when clicking a button inside the menu', () => {
|
|
539
|
+
render(
|
|
540
|
+
<PktHeaderService serviceName="Svc">
|
|
541
|
+
<button type="button">Gjør noe</button>
|
|
542
|
+
</PktHeaderService>,
|
|
543
|
+
)
|
|
544
|
+
openSlotMenu()
|
|
545
|
+
fireEvent.click(screen.getByText('Gjør noe'))
|
|
546
|
+
expect(screen.getByRole('button', { name: 'Åpne meny' })).toHaveAttribute('aria-expanded', 'false')
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
it('stays open when clicking a form input inside the menu', () => {
|
|
550
|
+
render(
|
|
551
|
+
<PktHeaderService serviceName="Svc">
|
|
552
|
+
<input type="text" aria-label="Filtrer" />
|
|
553
|
+
</PktHeaderService>,
|
|
554
|
+
)
|
|
555
|
+
openSlotMenu()
|
|
556
|
+
fireEvent.click(screen.getByLabelText('Filtrer'))
|
|
557
|
+
expect(screen.getByRole('button', { name: 'Åpne meny' })).toHaveAttribute('aria-expanded', 'true')
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
it('stays open when clicking non-interactive content inside the menu', () => {
|
|
561
|
+
render(
|
|
562
|
+
<PktHeaderService serviceName="Svc">
|
|
563
|
+
<span>Bare tekst</span>
|
|
564
|
+
</PktHeaderService>,
|
|
565
|
+
)
|
|
566
|
+
openSlotMenu()
|
|
567
|
+
fireEvent.click(screen.getByText('Bare tekst'))
|
|
568
|
+
expect(screen.getByRole('button', { name: 'Åpne meny' })).toHaveAttribute('aria-expanded', 'true')
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
it('stays open when clicking an element marked data-pkt-header-keep-open', () => {
|
|
572
|
+
render(
|
|
573
|
+
<PktHeaderService serviceName="Svc">
|
|
574
|
+
<button type="button" data-pkt-header-keep-open>
|
|
575
|
+
Vis mer
|
|
576
|
+
</button>
|
|
577
|
+
</PktHeaderService>,
|
|
578
|
+
)
|
|
579
|
+
openSlotMenu()
|
|
580
|
+
fireEvent.click(screen.getByText('Vis mer'))
|
|
581
|
+
expect(screen.getByRole('button', { name: 'Åpne meny' })).toHaveAttribute('aria-expanded', 'true')
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
it('stays open when clicking a disabled button inside the menu', () => {
|
|
585
|
+
render(
|
|
586
|
+
<PktHeaderService serviceName="Svc">
|
|
587
|
+
<button type="button" disabled>
|
|
588
|
+
Utilgjengelig
|
|
589
|
+
</button>
|
|
590
|
+
</PktHeaderService>,
|
|
591
|
+
)
|
|
592
|
+
openSlotMenu()
|
|
593
|
+
fireEvent.click(screen.getByText('Utilgjengelig'))
|
|
594
|
+
expect(screen.getByRole('button', { name: 'Åpne meny' })).toHaveAttribute('aria-expanded', 'true')
|
|
595
|
+
})
|
|
596
|
+
})
|
|
515
597
|
})
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
ForwardedRef,
|
|
8
8
|
forwardRef,
|
|
9
9
|
type KeyboardEvent,
|
|
10
|
+
type MouseEvent as ReactMouseEvent,
|
|
10
11
|
type MutableRefObject,
|
|
11
12
|
type RefObject,
|
|
12
13
|
useCallback,
|
|
@@ -28,6 +29,18 @@ import { IPktHeader, Representing, THeaderMenu, User, UserMenuFooterItem, UserMe
|
|
|
28
29
|
export type { Representing, THeaderMenu, User, UserMenuFooterItem, UserMenuItem }
|
|
29
30
|
export type IPktHeaderService = IPktHeader
|
|
30
31
|
|
|
32
|
+
declare global {
|
|
33
|
+
interface Window {
|
|
34
|
+
pktLogoPath?: string
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const defaultLogoPath = 'https://punkt-cdn.oslo.kommune.no/latest/logos/'
|
|
39
|
+
|
|
40
|
+
if (typeof window !== 'undefined') {
|
|
41
|
+
window.pktLogoPath = window.pktLogoPath || defaultLogoPath
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
export const PktHeaderService = forwardRef(
|
|
32
45
|
(
|
|
33
46
|
{
|
|
@@ -58,6 +71,7 @@ export const PktHeaderService = forwardRef(
|
|
|
58
71
|
hideLogo = false,
|
|
59
72
|
logoLink,
|
|
60
73
|
logoClick,
|
|
74
|
+
logoPath = typeof window !== 'undefined' ? window.pktLogoPath : defaultLogoPath,
|
|
61
75
|
position = 'fixed',
|
|
62
76
|
scrollBehavior = 'hide',
|
|
63
77
|
slotMenuVariant = 'icon-only',
|
|
@@ -273,6 +287,23 @@ export const PktHeaderService = forwardRef(
|
|
|
273
287
|
[isTablet, isMobile],
|
|
274
288
|
)
|
|
275
289
|
|
|
290
|
+
// Close the slot menu when the user makes an intentional click inside it.
|
|
291
|
+
const handleSlotContentClick = useCallback(
|
|
292
|
+
(event: ReactMouseEvent<HTMLDivElement>) => {
|
|
293
|
+
if (!isTablet || openMenu !== 'slot') return
|
|
294
|
+
|
|
295
|
+
const interactive = (event.target as HTMLElement).closest<HTMLElement>(
|
|
296
|
+
'a[href], button, [role="link"], [role="button"], [role="menuitem"], [role="menuitemradio"], [role="menuitemcheckbox"]',
|
|
297
|
+
)
|
|
298
|
+
if (!interactive || !slotContentRef.current?.contains(interactive)) return
|
|
299
|
+
if (interactive.closest('[data-pkt-header-keep-open]')) return
|
|
300
|
+
if ((interactive as HTMLButtonElement).disabled || interactive.getAttribute('aria-disabled') === 'true') return
|
|
301
|
+
|
|
302
|
+
setOpenMenu('none')
|
|
303
|
+
},
|
|
304
|
+
[isTablet, openMenu],
|
|
305
|
+
)
|
|
306
|
+
|
|
276
307
|
const handleMenuToggle = useCallback(
|
|
277
308
|
(mode: THeaderMenu) => {
|
|
278
309
|
if (openMenu !== 'none') {
|
|
@@ -319,11 +350,7 @@ export const PktHeaderService = forwardRef(
|
|
|
319
350
|
<span className="pkt-header-service__logo">
|
|
320
351
|
{logoLink ? (
|
|
321
352
|
<a href={logoLink} aria-label="Tilbake til forside">
|
|
322
|
-
<PktIcon
|
|
323
|
-
name="oslologo"
|
|
324
|
-
aria-hidden="true"
|
|
325
|
-
path="https://punkt-cdn.oslo.kommune.no/latest/logos/"
|
|
326
|
-
></PktIcon>
|
|
353
|
+
<PktIcon name="oslologo" aria-hidden="true" path={logoPath}></PktIcon>
|
|
327
354
|
</a>
|
|
328
355
|
) : logoClick ? (
|
|
329
356
|
<button
|
|
@@ -331,18 +358,10 @@ export const PktHeaderService = forwardRef(
|
|
|
331
358
|
className="pkt-link-button pkt-link pkt-header-service__logo-link"
|
|
332
359
|
onClick={logoClick}
|
|
333
360
|
>
|
|
334
|
-
<PktIcon
|
|
335
|
-
name="oslologo"
|
|
336
|
-
aria-hidden="true"
|
|
337
|
-
path="https://punkt-cdn.oslo.kommune.no/latest/logos/"
|
|
338
|
-
></PktIcon>
|
|
361
|
+
<PktIcon name="oslologo" aria-hidden="true" path={logoPath}></PktIcon>
|
|
339
362
|
</button>
|
|
340
363
|
) : (
|
|
341
|
-
<PktIcon
|
|
342
|
-
name="oslologo"
|
|
343
|
-
aria-hidden="true"
|
|
344
|
-
path="https://punkt-cdn.oslo.kommune.no/latest/logos/"
|
|
345
|
-
></PktIcon>
|
|
364
|
+
<PktIcon name="oslologo" aria-hidden="true" path={logoPath}></PktIcon>
|
|
346
365
|
)}
|
|
347
366
|
</span>
|
|
348
367
|
)}
|
|
@@ -393,6 +412,7 @@ export const PktHeaderService = forwardRef(
|
|
|
393
412
|
role={isTablet ? 'menu' : undefined}
|
|
394
413
|
aria-label={isTablet ? 'Meny' : undefined}
|
|
395
414
|
ref={slotContentRef}
|
|
415
|
+
onClick={handleSlotContentClick}
|
|
396
416
|
>
|
|
397
417
|
<div className="pkt-contents">{children}</div>
|
|
398
418
|
</div>
|
|
@@ -54,6 +54,8 @@ export interface IPktHeader {
|
|
|
54
54
|
hideLogo?: boolean
|
|
55
55
|
/** Link URL for the logo */
|
|
56
56
|
logoLink?: string
|
|
57
|
+
/** Override CDN path for the logo */
|
|
58
|
+
logoPath?: string
|
|
57
59
|
/** Callback when logo is clicked */
|
|
58
60
|
logoClick?: (event: MouseEvent) => void
|
|
59
61
|
/** Name of the service displayed in the header */
|
|
@@ -109,4 +109,26 @@ describe('PktLink', () => {
|
|
|
109
109
|
const link = screen.getByTestId('custom-link')
|
|
110
110
|
expect(link).toHaveAttribute('aria-label', 'Custom label')
|
|
111
111
|
})
|
|
112
|
+
|
|
113
|
+
it('should use renderLink to render a custom link element', () => {
|
|
114
|
+
render(
|
|
115
|
+
<PktLink
|
|
116
|
+
href="/test"
|
|
117
|
+
iconName="arrow"
|
|
118
|
+
renderLink={({ href, className, children, props }) => (
|
|
119
|
+
<a href={href} className={className} data-custom="true" {...props}>
|
|
120
|
+
{children}
|
|
121
|
+
</a>
|
|
122
|
+
)}
|
|
123
|
+
>
|
|
124
|
+
Test Link
|
|
125
|
+
</PktLink>,
|
|
126
|
+
)
|
|
127
|
+
const link = screen.getByText('Test Link').closest('a')
|
|
128
|
+
expect(link).toHaveAttribute('href', '/test')
|
|
129
|
+
expect(link).toHaveAttribute('data-custom', 'true')
|
|
130
|
+
expect(link).toHaveClass('pkt-link')
|
|
131
|
+
expect(link).toHaveClass('pkt-link--icon-left')
|
|
132
|
+
expect(document.querySelector('.pkt-link__icon')).toBeInTheDocument()
|
|
133
|
+
})
|
|
112
134
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import classNames from 'classnames'
|
|
4
|
-
import { FC, LinkHTMLAttributes } from 'react'
|
|
4
|
+
import { AnchorHTMLAttributes, FC, LinkHTMLAttributes, ReactNode } from 'react'
|
|
5
5
|
|
|
6
6
|
import { PktIcon } from '../icon/Icon'
|
|
7
7
|
|
|
@@ -12,6 +12,12 @@ interface IPktLink extends LinkHTMLAttributes<HTMLAnchorElement> {
|
|
|
12
12
|
iconPosition?: string | undefined
|
|
13
13
|
external?: boolean
|
|
14
14
|
target?: string | undefined
|
|
15
|
+
renderLink?: (args: {
|
|
16
|
+
href: string
|
|
17
|
+
className: string
|
|
18
|
+
children: ReactNode
|
|
19
|
+
props: AnchorHTMLAttributes<HTMLAnchorElement>
|
|
20
|
+
}) => ReactNode
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
export const PktLink: FC<IPktLink> = ({
|
|
@@ -21,6 +27,7 @@ export const PktLink: FC<IPktLink> = ({
|
|
|
21
27
|
iconPosition,
|
|
22
28
|
external,
|
|
23
29
|
target,
|
|
30
|
+
renderLink,
|
|
24
31
|
children,
|
|
25
32
|
...props
|
|
26
33
|
}: IPktLink) => {
|
|
@@ -31,18 +38,31 @@ export const PktLink: FC<IPktLink> = ({
|
|
|
31
38
|
'pkt-link--external': external,
|
|
32
39
|
}
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
{...props}
|
|
37
|
-
className={classNames(classes, className)}
|
|
38
|
-
href={href}
|
|
39
|
-
target={target}
|
|
40
|
-
rel={external ? 'noopener noreferrer' : undefined}
|
|
41
|
-
>
|
|
41
|
+
const linkChildren = (
|
|
42
|
+
<>
|
|
42
43
|
{iconName && <PktIcon name={iconName} className="pkt-link__icon" />}
|
|
43
44
|
{children}
|
|
44
|
-
|
|
45
|
+
</>
|
|
45
46
|
)
|
|
47
|
+
|
|
48
|
+
const linkRenderer =
|
|
49
|
+
renderLink ||
|
|
50
|
+
(({ href, className, children, props }) => (
|
|
51
|
+
<a href={href} className={className} {...props}>
|
|
52
|
+
{children}
|
|
53
|
+
</a>
|
|
54
|
+
))
|
|
55
|
+
|
|
56
|
+
return linkRenderer({
|
|
57
|
+
href: href ?? '',
|
|
58
|
+
className: classNames(classes, className),
|
|
59
|
+
children: linkChildren,
|
|
60
|
+
props: {
|
|
61
|
+
...props,
|
|
62
|
+
target,
|
|
63
|
+
rel: external ? 'noopener noreferrer' : undefined,
|
|
64
|
+
},
|
|
65
|
+
})
|
|
46
66
|
}
|
|
47
67
|
|
|
48
68
|
PktLink.displayName = 'PktLink'
|