@oslokommune/punkt-react 16.24.1 → 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,
@@ -6098,23 +6098,30 @@ function Ui(e) {
6098
6098
  }
6099
6099
  //#endregion
6100
6100
  //#region src/components/link/Link.tsx
6101
- var Wi = ({ href: e, iconName: t, className: n, iconPosition: r, external: i, target: a, children: o, ...s }) => {
6102
- 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 = {
6103
6103
  "pkt-link": !0,
6104
6104
  "pkt-link--icon-left": !!t && r === "left" || !!(t && !r),
6105
6105
  "pkt-link--icon-right": !!t && r === "right",
6106
6106
  "pkt-link--external": i
6107
- };
6108
- return /* @__PURE__ */ v("a", {
6109
- ...s,
6110
- 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", {
6111
6112
  href: e,
6112
- target: a,
6113
- rel: i ? "noopener noreferrer" : void 0,
6114
- children: [t && /* @__PURE__ */ _(F, {
6115
- name: t,
6116
- className: "pkt-link__icon"
6117
- }), 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
+ }
6118
6125
  });
6119
6126
  };
6120
6127
  Wi.displayName = "PktLink";
@@ -6403,6 +6410,10 @@ var Xi = a(({ mobileBreakpoint: e = 768, tabletBreakpoint: t = 1280, children: n
6403
6410
  let r = t.current.getBoundingClientRect(), i = n.current.offsetWidth, a = r.left + i > window.innerWidth;
6404
6411
  e === "slot" ? V(a) : fe(a);
6405
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) => {
6406
6417
  P === "none" ? (q.current = document.activeElement, I(e)) : I("none");
6407
6418
  }, [P]);
6408
6419
  l(() => {
@@ -6410,7 +6421,7 @@ var Xi = a(({ mobileBreakpoint: e = 768, tabletBreakpoint: t = 1280, children: n
6410
6421
  ve(P);
6411
6422
  });
6412
6423
  }, [P, ve]);
6413
- let be = /* @__PURE__ */ v("header", {
6424
+ let xe = /* @__PURE__ */ v("header", {
6414
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),
6415
6426
  "data-mode": a,
6416
6427
  ref: he,
@@ -6425,7 +6436,7 @@ var Xi = a(({ mobileBreakpoint: e = 768, tabletBreakpoint: t = 1280, children: n
6425
6436
  children: /* @__PURE__ */ _(F, {
6426
6437
  name: "oslologo",
6427
6438
  "aria-hidden": "true",
6428
- path: "https://punkt-cdn.oslo.kommune.no/16.24/logos/"
6439
+ path: "https://punkt-cdn.oslo.kommune.no/16.25/logos/"
6429
6440
  })
6430
6441
  }) : ne ? /* @__PURE__ */ _("button", {
6431
6442
  "aria-label": "Tilbake til forside",
@@ -6434,12 +6445,12 @@ var Xi = a(({ mobileBreakpoint: e = 768, tabletBreakpoint: t = 1280, children: n
6434
6445
  children: /* @__PURE__ */ _(F, {
6435
6446
  name: "oslologo",
6436
6447
  "aria-hidden": "true",
6437
- path: "https://punkt-cdn.oslo.kommune.no/16.24/logos/"
6448
+ path: "https://punkt-cdn.oslo.kommune.no/16.25/logos/"
6438
6449
  })
6439
6450
  }) : /* @__PURE__ */ _(F, {
6440
6451
  name: "oslologo",
6441
6452
  "aria-hidden": "true",
6442
- path: "https://punkt-cdn.oslo.kommune.no/16.24/logos/"
6453
+ path: "https://punkt-cdn.oslo.kommune.no/16.25/logos/"
6443
6454
  })
6444
6455
  }), T && /* @__PURE__ */ _("span", {
6445
6456
  className: "pkt-header-service__service-name",
@@ -6471,7 +6482,7 @@ var Xi = a(({ mobileBreakpoint: e = 768, tabletBreakpoint: t = 1280, children: n
6471
6482
  iconName: "menu",
6472
6483
  size: X ? "small" : "medium",
6473
6484
  state: P === "slot" ? "active" : "normal",
6474
- onClick: () => ye("slot"),
6485
+ onClick: () => be("slot"),
6475
6486
  "aria-expanded": P === "slot",
6476
6487
  "aria-controls": "mobile-slot-menu",
6477
6488
  "aria-label": "Åpne meny",
@@ -6482,6 +6493,7 @@ var Xi = a(({ mobileBreakpoint: e = 768, tabletBreakpoint: t = 1280, children: n
6482
6493
  role: Z ? "menu" : void 0,
6483
6494
  "aria-label": Z ? "Meny" : void 0,
6484
6495
  ref: K,
6496
+ onClick: ye,
6485
6497
  children: /* @__PURE__ */ _("div", {
6486
6498
  className: "pkt-contents",
6487
6499
  children: n
@@ -6497,7 +6509,7 @@ var Xi = a(({ mobileBreakpoint: e = 768, tabletBreakpoint: t = 1280, children: n
6497
6509
  variant: "icon-only",
6498
6510
  iconName: "magnifying-glass-big",
6499
6511
  size: X ? "small" : "medium",
6500
- onClick: () => ye("search"),
6512
+ onClick: () => be("search"),
6501
6513
  state: P === "search" ? "active" : "normal",
6502
6514
  "aria-expanded": P === "search",
6503
6515
  "aria-controls": "mobile-search-menu",
@@ -6559,7 +6571,7 @@ var Xi = a(({ mobileBreakpoint: e = 768, tabletBreakpoint: t = 1280, children: n
6559
6571
  variant: "icons-right-and-left",
6560
6572
  iconName: "user",
6561
6573
  secondIconName: P === "user" ? "chevron-thin-up" : "chevron-thin-down",
6562
- onClick: () => ye("user"),
6574
+ onClick: () => be("user"),
6563
6575
  children: [/* @__PURE__ */ _("span", {
6564
6576
  className: "pkt-sr-only",
6565
6577
  children: "Brukermeny: "
@@ -6592,8 +6604,8 @@ var Xi = a(({ mobileBreakpoint: e = 768, tabletBreakpoint: t = 1280, children: n
6592
6604
  });
6593
6605
  return se ? /* @__PURE__ */ v("div", {
6594
6606
  className: "pkt-header-service-wrapper",
6595
- 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") })]
6596
- }) : 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;
6597
6609
  });
6598
6610
  Xi.displayName = "PktHeaderService";
6599
6611
  //#endregion
@@ -6939,7 +6951,7 @@ var ya = ({ children: e, skin: t, title: n, href: r, iconName: i, openInNewTab:
6939
6951
  };
6940
6952
  //#endregion
6941
6953
  //#region src/components/loader/Loader.tsx
6942
- 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/");
6943
6955
  var ba = (e) => {
6944
6956
  switch (e) {
6945
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.1",
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": "f740327458813844b44a94fbb7a50b4036842883"
103
+ "gitHead": "d01de2b51e9c71b709202eb0d2fb3336e2932a20"
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,
@@ -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'