@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oslokommune/punkt-react",
3
- "version": "16.24.1",
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": "f740327458813844b44a94fbb7a50b4036842883"
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
- return (
35
- <a
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
- </a>
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'