@oslokommune/punkt-react 16.24.0 → 16.25.0

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.
@@ -59,7 +59,7 @@ var j = /* @__PURE__ */ O((/* @__PURE__ */ E(((e, t) => {
59
59
  return n;
60
60
  }) : window.classNames = n;
61
61
  })();
62
- })))(), 1), M = "https://punkt-cdn.oslo.kommune.no/16.24/icons/";
62
+ })))(), 1), M = "https://punkt-cdn.oslo.kommune.no/16.25/icons/";
63
63
  typeof window < "u" && (window.pktFetch = window.pktFetch === void 0 ? fetch : window.pktFetch, window.pktIconPath = window.pktIconPath || M);
64
64
  var te = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 32 32\"></svg>", ne = {}, re = 2, ie = 1500, ae = typeof Storage < "u" && typeof sessionStorage < "u", oe = typeof window < "u" ? f : l, N = (e, t) => !e || !ae ? "" : sessionStorage.getItem(t + e + ".svg") ?? "", se = (e) => window.pktFetch(e).then((t) => {
65
65
  if (!t.ok) throw Error("Missing icon: " + e);
@@ -174,7 +174,7 @@ var I = a(({ defaultOpen: e = !1, id: t, title: n, skin: r, compact: i = !1, isO
174
174
  });
175
175
  //#endregion
176
176
  //#region src/components/button/Button.tsx
177
- I.displayName = "PktAccordionItem", typeof window < "u" && (window.pktAnimationPath = window.pktAnimationPath || "https://punkt-cdn.oslo.kommune.no/16.24/animations/");
177
+ I.displayName = "PktAccordionItem", typeof window < "u" && (window.pktAnimationPath = window.pktAnimationPath || "https://punkt-cdn.oslo.kommune.no/16.25/animations/");
178
178
  var L = a(({ children: e, className: t, iconName: n = "user", secondIconName: r = "user", iconPath: i, secondIconPath: a, size: o = "medium", fullWidth: s = !1, fullWidthOnMobile: c = !1, skin: l = "primary", type: u = "button", variant: d = "label-only", state: f, color: p, isLoading: m = void 0, disabled: h = void 0, loadingAnimationPath: g = window.pktAnimationPath, ...y }, b) => {
179
179
  let x = [
180
180
  t,
@@ -2106,7 +2106,8 @@ function on(e) {
2106
2106
  t !== null && Y(e.nativeEvent, t);
2107
2107
  }, [z, Y]),
2108
2108
  handleFocusOut: s((e) => {
2109
- P.current && !P.current.contains(e.relatedTarget) && !e.target.classList.contains("pkt-hide") && z();
2109
+ let t = e.target, n = e.relatedTarget;
2110
+ P.current && P.current.contains(n) || t.classList.contains("pkt-hide") || t.closest(".pkt-cal-month-picker") || z();
2110
2111
  }, [z]),
2111
2112
  close: z,
2112
2113
  setFocusedDate: le
@@ -6097,23 +6098,30 @@ function Ui(e) {
6097
6098
  }
6098
6099
  //#endregion
6099
6100
  //#region src/components/link/Link.tsx
6100
- var Wi = ({ href: e, iconName: t, className: n, iconPosition: r, external: i, target: a, children: o, ...s }) => {
6101
- let c = {
6101
+ var Wi = ({ href: e, iconName: t, className: n, iconPosition: r, external: i, target: a, renderLink: o, children: s, ...c }) => {
6102
+ let l = {
6102
6103
  "pkt-link": !0,
6103
6104
  "pkt-link--icon-left": !!t && r === "left" || !!(t && !r),
6104
6105
  "pkt-link--icon-right": !!t && r === "right",
6105
6106
  "pkt-link--external": i
6106
- };
6107
- return /* @__PURE__ */ v("a", {
6108
- ...s,
6109
- className: (0, j.default)(c, n),
6107
+ }, u = /* @__PURE__ */ v(g, { children: [t && /* @__PURE__ */ _(F, {
6108
+ name: t,
6109
+ className: "pkt-link__icon"
6110
+ }), s] });
6111
+ return (o || (({ href: e, className: t, children: n, props: r }) => /* @__PURE__ */ _("a", {
6110
6112
  href: e,
6111
- target: a,
6112
- rel: i ? "noopener noreferrer" : void 0,
6113
- children: [t && /* @__PURE__ */ _(F, {
6114
- name: t,
6115
- className: "pkt-link__icon"
6116
- }), o]
6113
+ className: t,
6114
+ ...r,
6115
+ children: n
6116
+ })))({
6117
+ href: e ?? "",
6118
+ className: (0, j.default)(l, n),
6119
+ children: u,
6120
+ props: {
6121
+ ...c,
6122
+ target: a,
6123
+ rel: i ? "noopener noreferrer" : void 0
6124
+ }
6117
6125
  });
6118
6126
  };
6119
6127
  Wi.displayName = "PktLink";
@@ -6402,6 +6410,10 @@ var Xi = a(({ mobileBreakpoint: e = 768, tabletBreakpoint: t = 1280, children: n
6402
6410
  let r = t.current.getBoundingClientRect(), i = n.current.offsetWidth, a = r.left + i > window.innerWidth;
6403
6411
  e === "slot" ? V(a) : fe(a);
6404
6412
  }, [Z, X]), ye = s((e) => {
6413
+ if (!Z || P !== "slot") return;
6414
+ let t = e.target.closest("a[href], button, [role=\"link\"], [role=\"button\"], [role=\"menuitem\"], [role=\"menuitemradio\"], [role=\"menuitemcheckbox\"]");
6415
+ !t || !K.current?.contains(t) || t.closest("[data-pkt-header-keep-open]") || t.disabled || t.getAttribute("aria-disabled") === "true" || I("none");
6416
+ }, [Z, P]), be = s((e) => {
6405
6417
  P === "none" ? (q.current = document.activeElement, I(e)) : I("none");
6406
6418
  }, [P]);
6407
6419
  l(() => {
@@ -6409,7 +6421,7 @@ var Xi = a(({ mobileBreakpoint: e = 768, tabletBreakpoint: t = 1280, children: n
6409
6421
  ve(P);
6410
6422
  });
6411
6423
  }, [P, ve]);
6412
- let be = /* @__PURE__ */ v("header", {
6424
+ let xe = /* @__PURE__ */ v("header", {
6413
6425
  className: (0, j.default)("pkt-header-service", i && "pkt-header-service--compact", X && "pkt-header-service--mobile", Z && "pkt-header-service--tablet", se && "pkt-header-service--fixed", ce && "pkt-header-service--scroll-to-hide", ue && "pkt-header-service--hidden", r),
6414
6426
  "data-mode": a,
6415
6427
  ref: he,
@@ -6424,7 +6436,7 @@ var Xi = a(({ mobileBreakpoint: e = 768, tabletBreakpoint: t = 1280, children: n
6424
6436
  children: /* @__PURE__ */ _(F, {
6425
6437
  name: "oslologo",
6426
6438
  "aria-hidden": "true",
6427
- path: "https://punkt-cdn.oslo.kommune.no/16.24/logos/"
6439
+ path: "https://punkt-cdn.oslo.kommune.no/16.25/logos/"
6428
6440
  })
6429
6441
  }) : ne ? /* @__PURE__ */ _("button", {
6430
6442
  "aria-label": "Tilbake til forside",
@@ -6433,12 +6445,12 @@ var Xi = a(({ mobileBreakpoint: e = 768, tabletBreakpoint: t = 1280, children: n
6433
6445
  children: /* @__PURE__ */ _(F, {
6434
6446
  name: "oslologo",
6435
6447
  "aria-hidden": "true",
6436
- path: "https://punkt-cdn.oslo.kommune.no/16.24/logos/"
6448
+ path: "https://punkt-cdn.oslo.kommune.no/16.25/logos/"
6437
6449
  })
6438
6450
  }) : /* @__PURE__ */ _(F, {
6439
6451
  name: "oslologo",
6440
6452
  "aria-hidden": "true",
6441
- path: "https://punkt-cdn.oslo.kommune.no/16.24/logos/"
6453
+ path: "https://punkt-cdn.oslo.kommune.no/16.25/logos/"
6442
6454
  })
6443
6455
  }), T && /* @__PURE__ */ _("span", {
6444
6456
  className: "pkt-header-service__service-name",
@@ -6470,7 +6482,7 @@ var Xi = a(({ mobileBreakpoint: e = 768, tabletBreakpoint: t = 1280, children: n
6470
6482
  iconName: "menu",
6471
6483
  size: X ? "small" : "medium",
6472
6484
  state: P === "slot" ? "active" : "normal",
6473
- onClick: () => ye("slot"),
6485
+ onClick: () => be("slot"),
6474
6486
  "aria-expanded": P === "slot",
6475
6487
  "aria-controls": "mobile-slot-menu",
6476
6488
  "aria-label": "Åpne meny",
@@ -6481,6 +6493,7 @@ var Xi = a(({ mobileBreakpoint: e = 768, tabletBreakpoint: t = 1280, children: n
6481
6493
  role: Z ? "menu" : void 0,
6482
6494
  "aria-label": Z ? "Meny" : void 0,
6483
6495
  ref: K,
6496
+ onClick: ye,
6484
6497
  children: /* @__PURE__ */ _("div", {
6485
6498
  className: "pkt-contents",
6486
6499
  children: n
@@ -6496,7 +6509,7 @@ var Xi = a(({ mobileBreakpoint: e = 768, tabletBreakpoint: t = 1280, children: n
6496
6509
  variant: "icon-only",
6497
6510
  iconName: "magnifying-glass-big",
6498
6511
  size: X ? "small" : "medium",
6499
- onClick: () => ye("search"),
6512
+ onClick: () => be("search"),
6500
6513
  state: P === "search" ? "active" : "normal",
6501
6514
  "aria-expanded": P === "search",
6502
6515
  "aria-controls": "mobile-search-menu",
@@ -6558,7 +6571,7 @@ var Xi = a(({ mobileBreakpoint: e = 768, tabletBreakpoint: t = 1280, children: n
6558
6571
  variant: "icons-right-and-left",
6559
6572
  iconName: "user",
6560
6573
  secondIconName: P === "user" ? "chevron-thin-up" : "chevron-thin-down",
6561
- onClick: () => ye("user"),
6574
+ onClick: () => be("user"),
6562
6575
  children: [/* @__PURE__ */ _("span", {
6563
6576
  className: "pkt-sr-only",
6564
6577
  children: "Brukermeny: "
@@ -6591,8 +6604,8 @@ var Xi = a(({ mobileBreakpoint: e = 768, tabletBreakpoint: t = 1280, children: n
6591
6604
  });
6592
6605
  return se ? /* @__PURE__ */ v("div", {
6593
6606
  className: "pkt-header-service-wrapper",
6594
- children: [be, /* @__PURE__ */ _("div", { className: (0, j.default)("pkt-header-service-spacer", i && "pkt-header-service-spacer--compact", E && "pkt-header-service-spacer--has-user", X && "pkt-header-service-spacer--mobile", Z && "pkt-header-service-spacer--tablet") })]
6595
- }) : be;
6607
+ children: [xe, /* @__PURE__ */ _("div", { className: (0, j.default)("pkt-header-service-spacer", i && "pkt-header-service-spacer--compact", E && "pkt-header-service-spacer--has-user", X && "pkt-header-service-spacer--mobile", Z && "pkt-header-service-spacer--tablet") })]
6608
+ }) : xe;
6596
6609
  });
6597
6610
  Xi.displayName = "PktHeaderService";
6598
6611
  //#endregion
@@ -6938,7 +6951,7 @@ var ya = ({ children: e, skin: t, title: n, href: r, iconName: i, openInNewTab:
6938
6951
  };
6939
6952
  //#endregion
6940
6953
  //#region src/components/loader/Loader.tsx
6941
- ya.displayName = "PktLinkCard", typeof window < "u" && (window.pktAnimationPath = window.pktAnimationPath || "https://punkt-cdn.oslo.kommune.no/16.24/animations/");
6954
+ ya.displayName = "PktLinkCard", typeof window < "u" && (window.pktAnimationPath = window.pktAnimationPath || "https://punkt-cdn.oslo.kommune.no/16.25/animations/");
6942
6955
  var ba = (e) => {
6943
6956
  switch (e) {
6944
6957
  case "blue": return "spinner-blue";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oslokommune/punkt-react",
3
- "version": "16.24.0",
3
+ "version": "16.25.0",
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": "1da55a3f6c5825da99b119e03e090b01cc30565e"
103
+ "gitHead": "d01de2b51e9c71b709202eb0d2fb3336e2932a20"
104
104
  }
@@ -372,13 +372,24 @@ export function useCalendarState(props: IPktCalendar): ICalendarState {
372
372
 
373
373
  const handleFocusOut = useCallback(
374
374
  (e: React.FocusEvent) => {
375
- if (
376
- calendarRef.current &&
377
- !calendarRef.current.contains(e.relatedTarget as Node) &&
378
- !(e.target as Element).classList.contains('pkt-hide')
379
- ) {
380
- close()
375
+ const target = e.target as Element
376
+ const relatedTarget = e.relatedTarget as Node | null
377
+
378
+ if (calendarRef.current && calendarRef.current.contains(relatedTarget)) {
379
+ return
380
+ }
381
+
382
+ if (target.classList.contains('pkt-hide')) {
383
+ return
384
+ }
385
+
386
+ // Don't close if focus is coming from month/year picker controls
387
+ // Safari iOS triggers focusout with null relatedTarget when select changes
388
+ if (target.closest('.pkt-cal-month-picker')) {
389
+ return
381
390
  }
391
+
392
+ close()
382
393
  },
383
394
  [close],
384
395
  )
@@ -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,
@@ -273,6 +274,23 @@ export const PktHeaderService = forwardRef(
273
274
  [isTablet, isMobile],
274
275
  )
275
276
 
277
+ // Close the slot menu when the user makes an intentional click inside it.
278
+ const handleSlotContentClick = useCallback(
279
+ (event: ReactMouseEvent<HTMLDivElement>) => {
280
+ if (!isTablet || openMenu !== 'slot') return
281
+
282
+ const interactive = (event.target as HTMLElement).closest<HTMLElement>(
283
+ 'a[href], button, [role="link"], [role="button"], [role="menuitem"], [role="menuitemradio"], [role="menuitemcheckbox"]',
284
+ )
285
+ if (!interactive || !slotContentRef.current?.contains(interactive)) return
286
+ if (interactive.closest('[data-pkt-header-keep-open]')) return
287
+ if ((interactive as HTMLButtonElement).disabled || interactive.getAttribute('aria-disabled') === 'true') return
288
+
289
+ setOpenMenu('none')
290
+ },
291
+ [isTablet, openMenu],
292
+ )
293
+
276
294
  const handleMenuToggle = useCallback(
277
295
  (mode: THeaderMenu) => {
278
296
  if (openMenu !== 'none') {
@@ -393,6 +411,7 @@ export const PktHeaderService = forwardRef(
393
411
  role={isTablet ? 'menu' : undefined}
394
412
  aria-label={isTablet ? 'Meny' : undefined}
395
413
  ref={slotContentRef}
414
+ onClick={handleSlotContentClick}
396
415
  >
397
416
  <div className="pkt-contents">{children}</div>
398
417
  </div>
@@ -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'