@oslokommune/punkt-elements 13.5.3 → 13.5.4

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 CHANGED
@@ -5,6 +5,23 @@ og skriver commits ca etter [Conventional Commits](https://conventionalcommits.o
5
5
 
6
6
  ---
7
7
 
8
+ ## [13.5.4](https://github.com/oslokommune/punkt/compare/13.5.3...13.5.4) (2025-09-09)
9
+
10
+ ### ⚠ BREAKING CHANGES
11
+ Ingen
12
+
13
+ ### Features
14
+ Ingen
15
+
16
+ ### Bug Fixes
17
+ Ingen
18
+
19
+ ### Chores
20
+ Ingen
21
+
22
+ ---
23
+
24
+
8
25
  ## [13.5.3](https://github.com/oslokommune/punkt/compare/13.5.2...13.5.3) (2025-09-08)
9
26
 
10
27
  ### ⚠ BREAKING CHANGES
@@ -1,4 +1,9 @@
1
- "use strict";const t=require("./element-6DBpyGQm.cjs"),d=require("./class-map-BBG2gMX4.cjs"),h=require("./state-DPobt-Yz.cjs"),p=require("./ref-iJtiv3o2.cjs"),u=require("./pkt-slot-controller-BzddBp7z.cjs");require("./icon-B_ryAy4Q.cjs");var c=Object.defineProperty,y=Object.getOwnPropertyDescriptor,s=(r,e,a,o)=>{for(var i=o>1?void 0:o?y(e,a):e,n=r.length-1,l;n>=0;n--)(l=r[n])&&(i=(o?l(e,a,i):l(i))||i);return o&&i&&c(e,a,i),i};window.pktAnimationPath=window.pktAnimationPath||"https://punkt-cdn.oslo.kommune.no/latest/animations/";exports.PktLoader=class extends t.PktElement{constructor(){super(),this.defaultSlot=p.e(),this.delay=0,this.inline=!1,this.isLoading=!0,this.message=null,this.size="medium",this.variant="shapes",this.loadingAnimationPath=window.pktAnimationPath,this._shouldDisplayLoader=!1,this.slotController=new u.PktSlotController(this,this.defaultSlot)}connectedCallback(){super.connectedCallback(),this._shouldDisplayLoader=this.delay===0,this.delay>0&&this.setupLoader()}updated(e){e.has("delay")&&this.setupLoader()}render(){const e=d.e({"pkt-loader":!0,[`pkt-loader--${this.inline?"inline":"box"}`]:!0,[`pkt-loader--${this.size}`]:!0}),a=d.e({"pkt-contents":!0,"pkt-hide":this.isLoading});return t.x`<div role="status" aria-live="polite" .aria-busy=${this.isLoading} class=${e}>
1
+ "use strict";const t=require("./element-6DBpyGQm.cjs"),d=require("./class-map-BBG2gMX4.cjs"),h=require("./state-DPobt-Yz.cjs"),p=require("./ref-iJtiv3o2.cjs"),u=require("./pkt-slot-controller-BzddBp7z.cjs");require("./icon-B_ryAy4Q.cjs");var c=Object.defineProperty,y=Object.getOwnPropertyDescriptor,s=(r,e,a,o)=>{for(var i=o>1?void 0:o?y(e,a):e,n=r.length-1,l;n>=0;n--)(l=r[n])&&(i=(o?l(e,a,i):l(i))||i);return o&&i&&c(e,a,i),i};window.pktAnimationPath=window.pktAnimationPath||"https://punkt-cdn.oslo.kommune.no/latest/animations/";exports.PktLoader=class extends t.PktElement{constructor(){super(),this.defaultSlot=p.e(),this.delay=0,this.inline=!1,this.isLoading=!0,this.message=null,this.size="medium",this.variant="shapes",this.loadingAnimationPath=window.pktAnimationPath,this._shouldDisplayLoader=!1,this.slotController=new u.PktSlotController(this,this.defaultSlot)}connectedCallback(){super.connectedCallback(),this._shouldDisplayLoader=this.delay===0,this.delay>0&&this.setupLoader()}updated(e){e.has("delay")&&this.setupLoader()}render(){const e=d.e({"pkt-loader":!0,[`pkt-loader--${this.inline?"inline":"box"}`]:!0,[`pkt-loader--${this.size}`]:!0}),a=d.e({"pkt-contents":!0,"pkt-hide":this.isLoading});return t.x`<div
2
+ role="status"
3
+ aria-live="polite"
4
+ aria-busy=${this.isLoading?"true":"false"}
5
+ class=${e}
6
+ >
2
7
  ${this.isLoading&&this._shouldDisplayLoader?t.x`<div class="pkt-loader__spinner">
3
8
  <pkt-icon
4
9
  name=${this.getVariant(this.variant)}
@@ -29,7 +29,12 @@ let t = class extends u {
29
29
  "pkt-contents": !0,
30
30
  "pkt-hide": this.isLoading
31
31
  });
32
- return d`<div role="status" aria-live="polite" .aria-busy=${this.isLoading} class=${s}>
32
+ return d`<div
33
+ role="status"
34
+ aria-live="polite"
35
+ aria-busy=${this.isLoading ? "true" : "false"}
36
+ class=${s}
37
+ >
33
38
  ${this.isLoading && this._shouldDisplayLoader ? d`<div class="pkt-loader__spinner">
34
39
  <pkt-icon
35
40
  name=${this.getVariant(this.variant)}
@@ -1,4 +1,4 @@
1
- import { P as b, E as u, x as n, n as p, a as m } from "./element-CgEWt74-.js";
1
+ import { P as b, E as d, x as n, n as a, a as m } from "./element-CgEWt74-.js";
2
2
  import { P as h } from "./pkt-slot-controller-BPGj-LC5.js";
3
3
  import { e as f, n as k } from "./ref-BBYSqgeW.js";
4
4
  import { e as _ } from "./class-map-BpTj9gtz.js";
@@ -15,17 +15,17 @@ const x = {
15
15
  closable: {
16
16
  default: !1
17
17
  }
18
- }, d = {
18
+ }, u = {
19
19
  props: x
20
20
  };
21
- var C = Object.defineProperty, y = Object.getOwnPropertyDescriptor, o = (t, l, a, i) => {
22
- for (var s = i > 1 ? void 0 : i ? y(l, a) : l, r = t.length - 1, c; r >= 0; r--)
23
- (c = t[r]) && (s = (i ? c(l, a, s) : c(s)) || s);
24
- return i && s && C(l, a, s), s;
21
+ var C = Object.defineProperty, y = Object.getOwnPropertyDescriptor, o = (t, l, p, i) => {
22
+ for (var s = i > 1 ? void 0 : i ? y(l, p) : l, r = t.length - 1, c; r >= 0; r--)
23
+ (c = t[r]) && (s = (i ? c(l, p, s) : c(s)) || s);
24
+ return i && s && C(l, p, s), s;
25
25
  };
26
26
  let e = class extends b {
27
27
  constructor() {
28
- super(), this.defaultSlot = f(), this.closable = d.props.closable.default, this.compact = d.props.compact.default, this.title = "", this.skin = d.props.skin.default, this._isClosed = !1, this.close = (t) => {
28
+ super(), this.defaultSlot = f(), this.closable = u.props.closable.default, this.compact = u.props.compact.default, this.title = "", this.skin = u.props.skin.default, this._isClosed = !1, this.close = (t) => {
29
29
  this._isClosed = !0, this.dispatchEvent(new CustomEvent("close", { detail: { origin: t }, bubbles: !0 })), this.dispatchEvent(new CustomEvent("on-close", { detail: { origin: t }, bubbles: !0 }));
30
30
  }, this.slotController = new h(this, this.defaultSlot), this._isClosed = !1;
31
31
  }
@@ -47,26 +47,27 @@ let e = class extends b {
47
47
  <button
48
48
  @click=${this.close}
49
49
  class="pkt-btn pkt-btn--tertiary pkt-btn--small pkt-btn--icon-only"
50
+ aria-label="Lukk"
50
51
  >
51
52
  <pkt-icon name="close" class="pkt-link__icon"></pkt-icon>
52
53
  </button>
53
- </div>` : u}
54
- ${this.title ? n`<div class="pkt-messagebox__title">${this.title}</div>` : u}
54
+ </div>` : d}
55
+ ${this.title ? n`<div class="pkt-messagebox__title">${this.title}</div>` : d}
55
56
  <div class="pkt-messagebox__text" ${k(this.defaultSlot)}></div>
56
57
  </div>`;
57
58
  }
58
59
  };
59
60
  o([
60
- p({ type: Boolean, reflect: !0 })
61
+ a({ type: Boolean, reflect: !0 })
61
62
  ], e.prototype, "closable", 2);
62
63
  o([
63
- p({ type: Boolean, reflect: !0 })
64
+ a({ type: Boolean, reflect: !0 })
64
65
  ], e.prototype, "compact", 2);
65
66
  o([
66
- p({ type: String, reflect: !0 })
67
+ a({ type: String, reflect: !0 })
67
68
  ], e.prototype, "title", 2);
68
69
  o([
69
- p({ type: String, reflect: !0 })
70
+ a({ type: String, reflect: !0 })
70
71
  ], e.prototype, "skin", 2);
71
72
  o([
72
73
  v()
@@ -3,6 +3,7 @@
3
3
  <button
4
4
  @click=${this.close}
5
5
  class="pkt-btn pkt-btn--tertiary pkt-btn--small pkt-btn--icon-only"
6
+ aria-label="Lukk"
6
7
  >
7
8
  <pkt-icon name="close" class="pkt-link__icon"></pkt-icon>
8
9
  </button>
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const P=require("./alert-7rUOhlNi.cjs"),l=require("./accordionitem-Csh7iSVG.cjs"),d=require("./backlink-JbBNi3qg.cjs"),b=require("./button-B8rdtaHB.cjs"),k=require("./calendar-32W9p9uc.cjs"),m=require("./card-DBlFf1ry.cjs"),g=require("./combobox-DjO0RMUB.cjs"),h=require("./consent-hYeFWNFr.cjs"),f=require("./checkbox-Gn7Wtk9h.cjs"),t=require("./element-6DBpyGQm.cjs"),y=require("./pkt-slot-controller-BzddBp7z.cjs"),s=require("./ref-iJtiv3o2.cjs"),O=require("./class-map-BBG2gMX4.cjs"),j=require("./datepicker-CmTrG5GE.cjs"),q=require("./helptext-CzQX6YVE.cjs"),x=require("./heading-CNycsyMj.cjs"),C=require("./icon-B_ryAy4Q.cjs"),v=require("./input-wrapper-CZ-a00V7.cjs"),S=require("./link-Cjl0xwSq.cjs"),$=require("./linkcard-DqIvb54H.cjs"),L=require("./loader-CHPxY9c6.cjs"),_=require("./messagebox-CqUBJs_D.cjs"),A=require("./modal-CRtxhCaP.cjs"),B=require("./progressbar-DhMBXkww.cjs"),p=require("./radiobutton-CdT6v1oq.cjs"),T=require("./tag-Bbs0U_Au.cjs"),I=require("./textarea-CPXsMFUq.cjs"),M=require("./textinput-aNI5kibM.cjs"),R=require("./select-Dkl0KhGW.cjs");var H=Object.defineProperty,w=Object.getOwnPropertyDescriptor,o=(a,e,r,i)=>{for(var n=i>1?void 0:i?w(e,r):e,u=a.length-1,c;u>=0;u--)(c=a[u])&&(n=(i?c(e,r,n):c(n))||n);return i&&n&&H(e,r,n),n};exports.PktComponent=class extends t.PktElement{constructor(){super(),this.string="",this.strings=[],this.darkmode=!1,this._list=[],this.defaultSlot=s.e(),this.namedSlot=s.e(),this.slotController=new y.PktSlotController(this,this.defaultSlot,this.namedSlot)}connectedCallback(){this.strings.length&&this.strings.forEach(e=>{this._list.push(e.toUpperCase())}),super.connectedCallback()}render(){const e={"pkt-component":!0,"pkt-component--has-list":this.strings.length>0,"pkt-darkmode":this.darkmode};return t.x`
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const P=require("./alert-7rUOhlNi.cjs"),l=require("./accordionitem-Csh7iSVG.cjs"),d=require("./backlink-JbBNi3qg.cjs"),b=require("./button-B8rdtaHB.cjs"),k=require("./calendar-32W9p9uc.cjs"),m=require("./card-DBlFf1ry.cjs"),g=require("./combobox-DjO0RMUB.cjs"),h=require("./consent-hYeFWNFr.cjs"),f=require("./checkbox-Gn7Wtk9h.cjs"),t=require("./element-6DBpyGQm.cjs"),y=require("./pkt-slot-controller-BzddBp7z.cjs"),s=require("./ref-iJtiv3o2.cjs"),O=require("./class-map-BBG2gMX4.cjs"),j=require("./datepicker-CmTrG5GE.cjs"),q=require("./helptext-CzQX6YVE.cjs"),x=require("./heading-CNycsyMj.cjs"),C=require("./icon-B_ryAy4Q.cjs"),v=require("./input-wrapper-CZ-a00V7.cjs"),S=require("./link-Cjl0xwSq.cjs"),$=require("./linkcard-DqIvb54H.cjs"),L=require("./loader-DNidjwH-.cjs"),_=require("./messagebox-CjPtPPrW.cjs"),A=require("./modal-CRtxhCaP.cjs"),B=require("./progressbar-DhMBXkww.cjs"),p=require("./radiobutton-CdT6v1oq.cjs"),T=require("./tag-Bbs0U_Au.cjs"),I=require("./textarea-CPXsMFUq.cjs"),M=require("./textinput-aNI5kibM.cjs"),R=require("./select-Dkl0KhGW.cjs");var H=Object.defineProperty,w=Object.getOwnPropertyDescriptor,o=(a,e,r,i)=>{for(var n=i>1?void 0:i?w(e,r):e,u=a.length-1,c;u>=0;u--)(c=a[u])&&(n=(i?c(e,r,n):c(n))||n);return i&&n&&H(e,r,n),n};exports.PktComponent=class extends t.PktElement{constructor(){super(),this.string="",this.strings=[],this.darkmode=!1,this._list=[],this.defaultSlot=s.e(),this.namedSlot=s.e(),this.slotController=new y.PktSlotController(this,this.defaultSlot,this.namedSlot)}connectedCallback(){this.strings.length&&this.strings.forEach(e=>{this._list.push(e.toUpperCase())}),super.connectedCallback()}render(){const e={"pkt-component":!0,"pkt-component--has-list":this.strings.length>0,"pkt-darkmode":this.darkmode};return t.x`
2
2
  <div class="${O.e(e)}">
3
3
  <h1 class="pkt-txt-28">${this.string}</h1>
4
4
 
package/dist/pkt-index.js CHANGED
@@ -19,8 +19,8 @@ import { P as Z } from "./icon-CC1js8eR.js";
19
19
  import { P as et } from "./input-wrapper-Dr__Sxql.js";
20
20
  import { P as ot } from "./link-AIyVfcyH.js";
21
21
  import { P as at } from "./linkcard-9CNlyT0S.js";
22
- import { P as it } from "./loader-Da4IOk_T.js";
23
- import { P as lt } from "./messagebox-DwGdcdm7.js";
22
+ import { P as it } from "./loader-h3d-3D7s.js";
23
+ import { P as lt } from "./messagebox-C8KQgCl_.js";
24
24
  import { P as mt } from "./modal-Zj8yRX3K.js";
25
25
  import { P as ft } from "./progressbar-Dj_mI_A6.js";
26
26
  import { P as ht, P as ct } from "./radiobutton-CWxiIVfA.js";
@@ -1 +1 @@
1
- "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("./loader-CHPxY9c6.cjs"),t=e.PktLoader;Object.defineProperty(exports,"PktLoader",{enumerable:!0,get:()=>e.PktLoader});exports.default=t;
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("./loader-DNidjwH-.cjs"),t=e.PktLoader;Object.defineProperty(exports,"PktLoader",{enumerable:!0,get:()=>e.PktLoader});exports.default=t;
@@ -1,4 +1,4 @@
1
- import { P as a } from "./loader-Da4IOk_T.js";
1
+ import { P as a } from "./loader-h3d-3D7s.js";
2
2
  const t = a;
3
3
  export {
4
4
  a as PktLoader,
@@ -1 +1 @@
1
- "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("./messagebox-CqUBJs_D.cjs"),t=e.PktMessagebox;Object.defineProperty(exports,"PktMessagebox",{enumerable:!0,get:()=>e.PktMessagebox});exports.default=t;
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("./messagebox-CjPtPPrW.cjs"),t=e.PktMessagebox;Object.defineProperty(exports,"PktMessagebox",{enumerable:!0,get:()=>e.PktMessagebox});exports.default=t;
@@ -1,4 +1,4 @@
1
- import { P as s } from "./messagebox-DwGdcdm7.js";
1
+ import { P as s } from "./messagebox-C8KQgCl_.js";
2
2
  const a = s;
3
3
  export {
4
4
  s as PktMessagebox,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oslokommune/punkt-elements",
3
- "version": "13.5.3",
3
+ "version": "13.5.4",
4
4
  "description": "Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo",
5
5
  "homepage": "https://punkt.oslo.kommune.no",
6
6
  "author": "Team Designsystem, Oslo Origo",
@@ -73,5 +73,5 @@
73
73
  "url": "https://github.com/oslokommune/punkt/issues"
74
74
  },
75
75
  "license": "MIT",
76
- "gitHead": "6b43945924c71b96b609f1abc7875e139e297233"
76
+ "gitHead": "aed226fa38fe77f28a9bc5d0380b194f0f90a64a"
77
77
  }
@@ -0,0 +1,225 @@
1
+ import '@testing-library/jest-dom'
2
+ import { axe, toHaveNoViolations } from 'jest-axe'
3
+ import { fireEvent } from '@testing-library/dom'
4
+ import { createElementTest, BaseTestConfig } from '../../tests/test-framework'
5
+ import { CustomElementFor } from '../../tests/component-registry'
6
+ import { type IPktListbox } from './listbox'
7
+ import './listbox'
8
+
9
+ export interface ListboxTestConfig extends Partial<IPktListbox>, BaseTestConfig {
10
+ label?: string
11
+ id?: string
12
+ }
13
+
14
+ // Use shared framework
15
+ export const createListboxTest = async (config: ListboxTestConfig = {}) => {
16
+ const { container, element } = await createElementTest<
17
+ CustomElementFor<'pkt-listbox'>,
18
+ ListboxTestConfig
19
+ >('pkt-listbox', { ...config, options: undefined })
20
+
21
+ // Set complex properties directly on the element after creation
22
+ if (config.options) {
23
+ element.options = config.options
24
+ await element.updateComplete
25
+ }
26
+
27
+ return {
28
+ container,
29
+ listbox: element,
30
+ }
31
+ }
32
+
33
+ expect.extend(toHaveNoViolations)
34
+
35
+ // Cleanup after each test
36
+ afterEach(() => {
37
+ document.body.innerHTML = ''
38
+ })
39
+
40
+ describe('PktListbox', () => {
41
+ describe('Rendering and basic functionality', () => {
42
+ test('renders without errors', async () => {
43
+ const { listbox } = await createListboxTest()
44
+
45
+ expect(listbox).toBeInTheDocument()
46
+ expect(listbox).toBeTruthy()
47
+ })
48
+
49
+ test('renders with options', async () => {
50
+ const options = [
51
+ { value: 'option1', label: 'Option 1' },
52
+ { value: 'option2', label: 'Option 2' },
53
+ ]
54
+ const { listbox } = await createListboxTest({
55
+ label: 'Test Listbox',
56
+ options,
57
+ })
58
+
59
+ const listboxElement = listbox.querySelector('.pkt-listbox')
60
+ expect(listboxElement).toBeInTheDocument()
61
+
62
+ const optionElements = listbox.querySelectorAll('.pkt-listbox__option')
63
+ expect(optionElements).toHaveLength(2)
64
+ })
65
+ })
66
+
67
+ describe('Properties and attributes', () => {
68
+ test('applies default properties correctly', async () => {
69
+ const { listbox } = await createListboxTest()
70
+
71
+ expect(listbox.isOpen).toBe(false)
72
+ expect(listbox.disabled).toBe(false)
73
+ expect(listbox.includeSearch).toBe(false)
74
+ expect(listbox.isMultiSelect).toBe(false)
75
+ expect(listbox.allowUserInput).toBe(false)
76
+ expect(listbox.maxLength).toBe(0)
77
+ })
78
+
79
+ test('sets properties correctly', async () => {
80
+ const { listbox } = await createListboxTest({
81
+ isOpen: true,
82
+ disabled: true,
83
+ includeSearch: true,
84
+ isMultiSelect: true,
85
+ })
86
+
87
+ expect(listbox.isOpen).toBe(true)
88
+ expect(listbox.disabled).toBe(true)
89
+ expect(listbox.includeSearch).toBe(true)
90
+ expect(listbox.isMultiSelect).toBe(true)
91
+ })
92
+ })
93
+
94
+ describe('Option handling', () => {
95
+ test('renders single select options correctly', async () => {
96
+ const options = [
97
+ { value: 'option1', label: 'Option 1', selected: true },
98
+ { value: 'option2', label: 'Option 2' },
99
+ ]
100
+ const { listbox } = await createListboxTest({ options })
101
+
102
+ const selectedOption = listbox.querySelector('.pkt-listbox__option--selected')
103
+ expect(selectedOption).toBeInTheDocument()
104
+
105
+ const checkIcon = listbox.querySelector('pkt-icon[name="check-big"]')
106
+ expect(checkIcon).toBeInTheDocument()
107
+ })
108
+
109
+ test('renders multi-select options with checkboxes', async () => {
110
+ const options = [
111
+ { value: 'option1', label: 'Option 1', selected: true },
112
+ { value: 'option2', label: 'Option 2' },
113
+ ]
114
+ const { listbox } = await createListboxTest({
115
+ isMultiSelect: true,
116
+ options,
117
+ })
118
+
119
+ const checkboxes = listbox.querySelectorAll('input[type="checkbox"]')
120
+ expect(checkboxes).toHaveLength(2)
121
+ expect(checkboxes[0]).toBeChecked()
122
+ expect(checkboxes[1]).not.toBeChecked()
123
+ })
124
+
125
+ test('handles option click', async () => {
126
+ const options = [
127
+ { value: 'option1', label: 'Option 1' },
128
+ { value: 'option2', label: 'Option 2' },
129
+ ]
130
+ const { listbox } = await createListboxTest({ options })
131
+
132
+ // Listen for the option-toggle event
133
+ let toggledValue: string | null = null
134
+ listbox.addEventListener('option-toggle', (e: any) => {
135
+ toggledValue = e.detail
136
+ })
137
+
138
+ const optionElement = listbox.querySelector('.pkt-listbox__option')
139
+ fireEvent.click(optionElement!)
140
+
141
+ await listbox.updateComplete
142
+ expect(toggledValue).toBe('option1')
143
+ })
144
+ })
145
+
146
+ describe('Search functionality', () => {
147
+ test('renders search when includeSearch is true', async () => {
148
+ const { listbox } = await createListboxTest({ includeSearch: true })
149
+
150
+ const searchInput = listbox.querySelector('[role="searchbox"]')
151
+ expect(searchInput).toBeInTheDocument()
152
+ })
153
+
154
+ test('filters options based on search', async () => {
155
+ const options = [
156
+ { value: 'apple', label: 'Apple' },
157
+ { value: 'banana', label: 'Banana' },
158
+ ]
159
+ const { listbox } = await createListboxTest({
160
+ includeSearch: true,
161
+ options,
162
+ })
163
+
164
+ // Set search value and trigger filtering
165
+ listbox.searchValue = 'app'
166
+ listbox.filterOptions()
167
+ await listbox.updateComplete
168
+
169
+ // Should filter to only show Apple - check filtered options
170
+ const visibleOptions = listbox.querySelectorAll('.pkt-listbox__option')
171
+ expect(visibleOptions).toHaveLength(1)
172
+ expect(visibleOptions[0].textContent?.trim()).toContain('Apple')
173
+ })
174
+ })
175
+
176
+ describe('User input functionality', () => {
177
+ test('renders new option banner when allowUserInput is true', async () => {
178
+ const { listbox } = await createListboxTest({
179
+ allowUserInput: true,
180
+ customUserInput: 'New',
181
+ })
182
+
183
+ const newOptionBanner = listbox.querySelector('.pkt-listbox__banner--new-option')
184
+ expect(newOptionBanner).toBeInTheDocument()
185
+ expect(newOptionBanner?.getAttribute('data-value')).toBe('New')
186
+ // Check that the text contains the basic structure
187
+ expect(newOptionBanner?.textContent).toMatch(/Legg til.*New/)
188
+ })
189
+ })
190
+
191
+ describe('Maximum selection', () => {
192
+ test('shows maximum reached banner', async () => {
193
+ const options = [
194
+ { value: 'option1', label: 'Option 1', selected: true },
195
+ { value: 'option2', label: 'Option 2', selected: true },
196
+ ]
197
+ const { listbox } = await createListboxTest({
198
+ isMultiSelect: true,
199
+ maxLength: 3,
200
+ options,
201
+ })
202
+
203
+ const banner = listbox.querySelector('.pkt-listbox__banner--maximum-reached')
204
+ expect(banner).toBeInTheDocument()
205
+ expect(banner?.textContent).toContain('2 av maks 3')
206
+ })
207
+ })
208
+
209
+ describe('Accessibility', () => {
210
+ test('basic listbox is accessible', async () => {
211
+ const options = [
212
+ { value: 'option1', label: 'Option 1' },
213
+ { value: 'option2', label: 'Option 2' },
214
+ ]
215
+ const { container } = await createListboxTest({
216
+ label: 'Accessible Listbox',
217
+ options,
218
+ })
219
+ await new Promise((resolve) => setTimeout(resolve, 100))
220
+
221
+ const results = await axe(container)
222
+ expect(results).toHaveNoViolations()
223
+ })
224
+ })
225
+ })
@@ -0,0 +1,257 @@
1
+ import '@testing-library/jest-dom'
2
+ import { axe, toHaveNoViolations } from 'jest-axe'
3
+ import { createElementTest, BaseTestConfig } from '../../tests/test-framework'
4
+ import { CustomElementFor } from '../../tests/component-registry'
5
+ import { type IPktLoader } from './loader'
6
+ import './loader'
7
+
8
+ export interface LoaderTestConfig extends Partial<IPktLoader>, BaseTestConfig {}
9
+
10
+ // Use shared framework
11
+ export const createLoaderTest = async (config: LoaderTestConfig = {}) => {
12
+ const { container, element } = await createElementTest<
13
+ CustomElementFor<'pkt-loader'>,
14
+ LoaderTestConfig
15
+ >('pkt-loader', { ...config, isLoading: undefined })
16
+
17
+ // Set boolean properties directly on the element after creation
18
+ if (config.isLoading !== undefined) {
19
+ element.isLoading = config.isLoading
20
+ await element.updateComplete
21
+ }
22
+ if (config.inline !== undefined) {
23
+ element.inline = config.inline
24
+ await element.updateComplete
25
+ }
26
+
27
+ return {
28
+ container,
29
+ loader: element,
30
+ }
31
+ }
32
+
33
+ expect.extend(toHaveNoViolations)
34
+
35
+ afterEach(() => {
36
+ document.body.innerHTML = ''
37
+ })
38
+
39
+ describe('PktLoader', () => {
40
+ describe('Rendering and basic functionality', () => {
41
+ test('renders without errors', async () => {
42
+ const { loader } = await createLoaderTest()
43
+
44
+ expect(loader).toBeInTheDocument()
45
+ expect(loader).toBeTruthy()
46
+ })
47
+
48
+ test('renders loading state by default', async () => {
49
+ const { loader } = await createLoaderTest()
50
+
51
+ expect(loader.isLoading).toBe(true)
52
+ const spinner = loader.querySelector('.pkt-loader__spinner')
53
+ expect(spinner).toBeInTheDocument()
54
+ })
55
+
56
+ test('renders content when not loading', async () => {
57
+ const { loader } = await createLoaderTest({
58
+ content: '<p>Content</p>',
59
+ isLoading: false,
60
+ })
61
+
62
+ expect(loader.isLoading).toBe(false)
63
+ const content = loader.querySelector('.pkt-contents')
64
+ expect(content).toBeInTheDocument()
65
+ expect(content?.classList.contains('pkt-hide')).toBe(false)
66
+ })
67
+
68
+ test('renders with slot content', async () => {
69
+ const { loader } = await createLoaderTest({
70
+ content: '<p>Test content</p>',
71
+ })
72
+
73
+ const content = loader.querySelector('p')
74
+ expect(content).toBeInTheDocument()
75
+ expect(content?.textContent).toBe('Test content')
76
+ })
77
+ })
78
+
79
+ describe('Properties and attributes', () => {
80
+ test('applies default properties correctly', async () => {
81
+ const { loader } = await createLoaderTest()
82
+
83
+ expect(loader.isLoading).toBe(true)
84
+ expect(loader.inline).toBe(false)
85
+ expect(loader.size).toBe('medium')
86
+ expect(loader.variant).toBe('shapes')
87
+ expect(loader.delay).toBe(0)
88
+ })
89
+
90
+ test('sets properties correctly', async () => {
91
+ const { loader } = await createLoaderTest({
92
+ size: 'small',
93
+ variant: 'blue',
94
+ delay: 100,
95
+ isLoading: false,
96
+ inline: true,
97
+ })
98
+
99
+ expect(loader.isLoading).toBe(false)
100
+ expect(loader.inline).toBe(true)
101
+ expect(loader.size).toBe('small')
102
+ expect(loader.variant).toBe('blue')
103
+ expect(loader.delay).toBe(100)
104
+ })
105
+ })
106
+
107
+ describe('Size variants', () => {
108
+ test('applies small size correctly', async () => {
109
+ const { loader } = await createLoaderTest({ size: 'small' })
110
+
111
+ const loaderDiv = loader.querySelector('.pkt-loader')
112
+ expect(loaderDiv?.classList.contains('pkt-loader--small')).toBe(true)
113
+ })
114
+
115
+ test('applies medium size correctly', async () => {
116
+ const { loader } = await createLoaderTest({ size: 'medium' })
117
+
118
+ const loaderDiv = loader.querySelector('.pkt-loader')
119
+ expect(loaderDiv?.classList.contains('pkt-loader--medium')).toBe(true)
120
+ })
121
+
122
+ test('applies large size correctly', async () => {
123
+ const { loader } = await createLoaderTest({ size: 'large' })
124
+
125
+ const loaderDiv = loader.querySelector('.pkt-loader')
126
+ expect(loaderDiv?.classList.contains('pkt-loader--large')).toBe(true)
127
+ })
128
+ })
129
+
130
+ describe('Loader variants', () => {
131
+ test('applies shapes variant correctly', async () => {
132
+ const { loader } = await createLoaderTest({ variant: 'shapes' })
133
+
134
+ const icon = loader.querySelector('pkt-icon')
135
+ expect(icon?.getAttribute('name')).toBe('loader')
136
+ })
137
+
138
+ test('applies blue variant correctly', async () => {
139
+ const { loader } = await createLoaderTest({ variant: 'blue' })
140
+
141
+ const icon = loader.querySelector('pkt-icon')
142
+ expect(icon?.getAttribute('name')).toBe('spinner-blue')
143
+ })
144
+
145
+ test('applies rainbow variant correctly', async () => {
146
+ const { loader } = await createLoaderTest({ variant: 'rainbow' })
147
+
148
+ const icon = loader.querySelector('pkt-icon')
149
+ expect(icon?.getAttribute('name')).toBe('spinner')
150
+ })
151
+ })
152
+
153
+ describe('Inline mode', () => {
154
+ test('applies box class when inline is false', async () => {
155
+ const { loader } = await createLoaderTest({ inline: false })
156
+
157
+ const loaderDiv = loader.querySelector('.pkt-loader')
158
+ expect(loaderDiv?.classList.contains('pkt-loader--box')).toBe(true)
159
+ })
160
+
161
+ test('applies inline class when inline is true', async () => {
162
+ const { loader } = await createLoaderTest({ inline: true })
163
+
164
+ const loaderDiv = loader.querySelector('.pkt-loader')
165
+ expect(loaderDiv?.classList.contains('pkt-loader--inline')).toBe(true)
166
+ })
167
+ })
168
+
169
+ describe('Message functionality', () => {
170
+ test('renders message when provided', async () => {
171
+ const { loader } = await createLoaderTest({ message: 'Loading data...' })
172
+
173
+ const message = loader.querySelector('p')
174
+ expect(message).toBeInTheDocument()
175
+ expect(message?.textContent).toBe('Loading data...')
176
+ })
177
+
178
+ test('does not render message when not provided', async () => {
179
+ const { loader } = await createLoaderTest()
180
+
181
+ const message = loader.querySelector('p')
182
+ expect(message).not.toBeInTheDocument()
183
+ })
184
+ })
185
+
186
+ describe('Delay functionality', () => {
187
+ test('handles delay correctly', async () => {
188
+ const { loader } = await createLoaderTest({ delay: 100 })
189
+
190
+ // Initially should not show spinner due to delay
191
+ let spinner = loader.querySelector('.pkt-loader__spinner')
192
+ expect(spinner).not.toBeInTheDocument()
193
+
194
+ // Wait for delay to pass
195
+ await new Promise((resolve) => setTimeout(resolve, 150))
196
+ await loader.updateComplete
197
+
198
+ // Now spinner should be visible
199
+ spinner = loader.querySelector('.pkt-loader__spinner')
200
+ expect(spinner).toBeInTheDocument()
201
+ }, 300)
202
+ })
203
+
204
+ describe('Content visibility', () => {
205
+ test('hides content when loading', async () => {
206
+ const { loader } = await createLoaderTest({
207
+ isLoading: true,
208
+ content: '<p>Content</p>',
209
+ })
210
+
211
+ const content = loader.querySelector('.pkt-contents')
212
+ expect(content?.classList.contains('pkt-hide')).toBe(true)
213
+ })
214
+
215
+ test('shows content when not loading', async () => {
216
+ const { loader } = await createLoaderTest({
217
+ isLoading: false,
218
+ content: '<p>Content</p>',
219
+ })
220
+
221
+ const content = loader.querySelector('.pkt-contents')
222
+ expect(content?.classList.contains('pkt-hide')).toBe(false)
223
+ })
224
+ })
225
+
226
+ describe('ARIA and accessibility', () => {
227
+ test('has correct ARIA attributes', async () => {
228
+ const { loader } = await createLoaderTest()
229
+
230
+ const loaderDiv = loader.querySelector('.pkt-loader')
231
+ expect(loaderDiv?.getAttribute('role')).toBe('status')
232
+ expect(loaderDiv?.getAttribute('aria-live')).toBe('polite')
233
+ // Check the aria-busy attribute
234
+ expect(loaderDiv?.getAttribute('aria-busy')).toBe('true')
235
+ })
236
+
237
+ test('updates aria-busy when loading state changes', async () => {
238
+ const { loader } = await createLoaderTest({ isLoading: false })
239
+
240
+ const loaderDiv = loader.querySelector('.pkt-loader')
241
+ // Check the aria-busy attribute
242
+ expect(loaderDiv?.getAttribute('aria-busy')).toBe('false')
243
+ })
244
+ })
245
+
246
+ describe('Accessibility', () => {
247
+ test('loader is accessible', async () => {
248
+ const { container } = await createLoaderTest({
249
+ message: 'Loading content',
250
+ content: '<p>Content</p>',
251
+ })
252
+
253
+ const results = await axe(container)
254
+ expect(results).toHaveNoViolations()
255
+ })
256
+ })
257
+ })
@@ -94,7 +94,12 @@ export class PktLoader extends PktElement implements IPktLoader {
94
94
  'pkt-hide': this.isLoading,
95
95
  })
96
96
 
97
- return html`<div role="status" aria-live="polite" .aria-busy=${this.isLoading} class=${classes}>
97
+ return html`<div
98
+ role="status"
99
+ aria-live="polite"
100
+ aria-busy=${this.isLoading ? 'true' : 'false'}
101
+ class=${classes}
102
+ >
98
103
  ${this.isLoading && this._shouldDisplayLoader
99
104
  ? html`<div class="pkt-loader__spinner">
100
105
  <pkt-icon
@@ -0,0 +1,241 @@
1
+ import '@testing-library/jest-dom'
2
+ import { axe, toHaveNoViolations } from 'jest-axe'
3
+ import { fireEvent } from '@testing-library/dom'
4
+ import { createElementTest, BaseTestConfig } from '../../tests/test-framework'
5
+ import { CustomElementFor } from '../../tests/component-registry'
6
+ import { type IPktMessagebox } from './messagebox'
7
+ import './messagebox'
8
+
9
+ export interface MessageboxTestConfig extends Partial<IPktMessagebox>, BaseTestConfig {}
10
+
11
+ // Use shared framework
12
+ export const createMessageboxTest = async (config: MessageboxTestConfig = {}) => {
13
+ const { container, element } = await createElementTest<
14
+ CustomElementFor<'pkt-messagebox'>,
15
+ MessageboxTestConfig
16
+ >('pkt-messagebox', config)
17
+
18
+ return {
19
+ container,
20
+ messagebox: element,
21
+ }
22
+ }
23
+
24
+ expect.extend(toHaveNoViolations)
25
+
26
+ // Cleanup after each test
27
+ afterEach(() => {
28
+ document.body.innerHTML = ''
29
+ })
30
+
31
+ describe('PktMessagebox', () => {
32
+ describe('Rendering and basic functionality', () => {
33
+ test('renders without errors', async () => {
34
+ const { messagebox } = await createMessageboxTest()
35
+
36
+ expect(messagebox).toBeInTheDocument()
37
+ expect(messagebox).toBeTruthy()
38
+ })
39
+
40
+ test('renders with basic structure', async () => {
41
+ const { messagebox } = await createMessageboxTest({
42
+ title: 'Test Title',
43
+ content: 'Test content',
44
+ })
45
+
46
+ const messageboxDiv = messagebox.querySelector('.pkt-messagebox')
47
+ expect(messageboxDiv).toBeInTheDocument()
48
+
49
+ const title = messagebox.querySelector('.pkt-messagebox__title')
50
+ expect(title).toBeInTheDocument()
51
+ expect(title?.textContent).toBe('Test Title')
52
+
53
+ const text = messagebox.querySelector('.pkt-messagebox__text')
54
+ expect(text).toBeInTheDocument()
55
+ })
56
+
57
+ test('renders content correctly', async () => {
58
+ const { messagebox } = await createMessageboxTest({
59
+ content: 'This is the message content',
60
+ })
61
+
62
+ const text = messagebox.querySelector('.pkt-messagebox__text')
63
+ expect(text?.textContent).toContain('This is the message content')
64
+ })
65
+ })
66
+
67
+ describe('Properties and attributes', () => {
68
+ test('applies default properties correctly', async () => {
69
+ const { messagebox } = await createMessageboxTest()
70
+
71
+ expect(messagebox.closable).toBe(false)
72
+ expect(messagebox.compact).toBe(false)
73
+ expect(messagebox.title).toBe('')
74
+ expect(messagebox.skin).toBe('beige')
75
+ })
76
+
77
+ test('sets properties correctly', async () => {
78
+ const { messagebox } = await createMessageboxTest({
79
+ closable: true,
80
+ compact: true,
81
+ title: 'Custom Title',
82
+ skin: 'blue',
83
+ })
84
+
85
+ expect(messagebox.closable).toBe(true)
86
+ expect(messagebox.compact).toBe(true)
87
+ expect(messagebox.title).toBe('Custom Title')
88
+ expect(messagebox.skin).toBe('blue')
89
+ })
90
+ })
91
+
92
+ describe('Skin variants', () => {
93
+ test('applies beige skin correctly', async () => {
94
+ const { messagebox } = await createMessageboxTest({ skin: 'beige' })
95
+
96
+ const messageboxDiv = messagebox.querySelector('.pkt-messagebox')
97
+ expect(messageboxDiv?.classList.contains('pkt-messagebox--beige')).toBe(true)
98
+ })
99
+
100
+ test('applies blue skin correctly', async () => {
101
+ const { messagebox } = await createMessageboxTest({ skin: 'blue' })
102
+
103
+ const messageboxDiv = messagebox.querySelector('.pkt-messagebox')
104
+ expect(messageboxDiv?.classList.contains('pkt-messagebox--blue')).toBe(true)
105
+ })
106
+
107
+ test('applies red skin correctly', async () => {
108
+ const { messagebox } = await createMessageboxTest({ skin: 'red' })
109
+
110
+ const messageboxDiv = messagebox.querySelector('.pkt-messagebox')
111
+ expect(messageboxDiv?.classList.contains('pkt-messagebox--red')).toBe(true)
112
+ })
113
+
114
+ test('applies green skin correctly', async () => {
115
+ const { messagebox } = await createMessageboxTest({ skin: 'green' })
116
+
117
+ const messageboxDiv = messagebox.querySelector('.pkt-messagebox')
118
+ expect(messageboxDiv?.classList.contains('pkt-messagebox--green')).toBe(true)
119
+ })
120
+ })
121
+
122
+ describe('Compact mode', () => {
123
+ test('applies compact class when compact is true', async () => {
124
+ const { messagebox } = await createMessageboxTest({ compact: true })
125
+
126
+ const messageboxDiv = messagebox.querySelector('.pkt-messagebox')
127
+ expect(messageboxDiv?.classList.contains('pkt-messagebox--compact')).toBe(true)
128
+ })
129
+
130
+ test('does not apply compact class when compact is false', async () => {
131
+ const { messagebox } = await createMessageboxTest({ compact: false })
132
+
133
+ const messageboxDiv = messagebox.querySelector('.pkt-messagebox')
134
+ expect(messageboxDiv?.classList.contains('pkt-messagebox--compact')).toBe(false)
135
+ })
136
+ })
137
+
138
+ describe('Closable functionality', () => {
139
+ test('renders close button when closable is true', async () => {
140
+ const { messagebox } = await createMessageboxTest({ closable: true })
141
+
142
+ const closeButton = messagebox.querySelector('.pkt-messagebox__close button')
143
+ expect(closeButton).toBeInTheDocument()
144
+
145
+ const closeIcon = messagebox.querySelector('pkt-icon[name="close"]')
146
+ expect(closeIcon).toBeInTheDocument()
147
+ })
148
+
149
+ test('does not render close button when closable is false', async () => {
150
+ const { messagebox } = await createMessageboxTest({ closable: false })
151
+
152
+ const closeButton = messagebox.querySelector('.pkt-messagebox__close')
153
+ expect(closeButton).not.toBeInTheDocument()
154
+ })
155
+
156
+ test('closes messagebox when close button is clicked', async () => {
157
+ const { messagebox } = await createMessageboxTest({ closable: true })
158
+
159
+ const closeButton = messagebox.querySelector('.pkt-messagebox__close button')
160
+ fireEvent.click(closeButton!)
161
+
162
+ await messagebox.updateComplete
163
+ expect(messagebox._isClosed).toBe(true)
164
+ expect(messagebox.classList.contains('pkt-hide')).toBe(true)
165
+ })
166
+
167
+ test('dispatches close events when closed', async () => {
168
+ const { messagebox } = await createMessageboxTest({ closable: true })
169
+
170
+ const closeHandler = jest.fn()
171
+ const onCloseHandler = jest.fn()
172
+ messagebox.addEventListener('close', closeHandler)
173
+ messagebox.addEventListener('on-close', onCloseHandler)
174
+
175
+ const closeButton = messagebox.querySelector('.pkt-messagebox__close button')
176
+ fireEvent.click(closeButton!)
177
+
178
+ expect(closeHandler).toHaveBeenCalled()
179
+ expect(onCloseHandler).toHaveBeenCalled()
180
+ })
181
+ })
182
+
183
+ describe('Title functionality', () => {
184
+ test('renders title when provided', async () => {
185
+ const { messagebox } = await createMessageboxTest({ title: 'Important Message' })
186
+
187
+ const title = messagebox.querySelector('.pkt-messagebox__title')
188
+ expect(title).toBeInTheDocument()
189
+ expect(title?.textContent).toBe('Important Message')
190
+ expect(title?.textContent).toBe('Important Message')
191
+ })
192
+
193
+ test('does not render title when empty', async () => {
194
+ const { messagebox } = await createMessageboxTest({ title: '' })
195
+
196
+ const title = messagebox.querySelector('.pkt-messagebox__title')
197
+ expect(title).not.toBeInTheDocument()
198
+ })
199
+ })
200
+
201
+ describe('CSS classes', () => {
202
+ test('applies base messagebox class', async () => {
203
+ const { messagebox } = await createMessageboxTest()
204
+
205
+ const messageboxDiv = messagebox.querySelector('.pkt-messagebox')
206
+ expect(messageboxDiv?.classList.contains('pkt-messagebox')).toBe(true)
207
+ })
208
+
209
+ test('applies closable class when closable', async () => {
210
+ const { messagebox } = await createMessageboxTest({ closable: true })
211
+
212
+ const messageboxDiv = messagebox.querySelector('.pkt-messagebox')
213
+ expect(messageboxDiv?.classList.contains('pkt-messagebox--closable')).toBe(true)
214
+ })
215
+ })
216
+
217
+ describe('Accessibility', () => {
218
+ test('basic messagebox is accessible', async () => {
219
+ const { container } = await createMessageboxTest({
220
+ title: 'Accessible Message',
221
+ content: 'This is an accessible message',
222
+ })
223
+ await new Promise((resolve) => setTimeout(resolve, 100))
224
+
225
+ const results = await axe(container)
226
+ expect(results).toHaveNoViolations()
227
+ })
228
+
229
+ test('closable messagebox is accessible', async () => {
230
+ const { container } = await createMessageboxTest({
231
+ closable: true,
232
+ title: 'Closable Message',
233
+ content: 'This message can be closed',
234
+ })
235
+ await new Promise((resolve) => setTimeout(resolve, 100))
236
+
237
+ const results = await axe(container)
238
+ expect(results).toHaveNoViolations()
239
+ })
240
+ })
241
+ })
@@ -65,6 +65,7 @@ export class PktMessagebox extends PktElement implements IPktMessagebox {
65
65
  <button
66
66
  @click=${this.close}
67
67
  class="pkt-btn pkt-btn--tertiary pkt-btn--small pkt-btn--icon-only"
68
+ aria-label="Lukk"
68
69
  >
69
70
  <pkt-icon name="close" class="pkt-link__icon"></pkt-icon>
70
71
  </button>