@oslokommune/punkt-react 16.20.0 → 16.21.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oslokommune/punkt-react",
3
- "version": "16.20.0",
3
+ "version": "16.21.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",
@@ -39,7 +39,7 @@
39
39
  "dependencies": {
40
40
  "@lit-labs/ssr-dom-shim": "^1.2.1",
41
41
  "@lit/react": "^1.0.7",
42
- "@oslokommune/punkt-elements": "^16.19.0",
42
+ "@oslokommune/punkt-elements": "^16.21.0",
43
43
  "classnames": "^2.5.1",
44
44
  "prettier": "^3.3.3",
45
45
  "react-hook-form": "^7.53.0"
@@ -109,5 +109,5 @@
109
109
  "url": "https://github.com/oslokommune/punkt/issues"
110
110
  },
111
111
  "license": "MIT",
112
- "gitHead": "dee5033c347f7c10530f57d5dc05c5411b75033f"
112
+ "gitHead": "f7f1779ccee5d510e6d011e7ec3edba89b41c255"
113
113
  }
@@ -1,38 +1,75 @@
1
1
  import '@testing-library/jest-dom'
2
2
 
3
- import { render } from '@testing-library/react'
3
+ import { act, render } from '@testing-library/react'
4
4
  import { axe, toHaveNoViolations } from 'jest-axe'
5
+ import { vi } from 'vitest'
5
6
 
6
7
  import { PktLoader } from './Loader'
7
8
  expect.extend(toHaveNoViolations)
8
9
 
9
10
  describe('PktLoader', () => {
10
11
  describe('basic rendering', () => {
11
- it('renders correctly without any props', async () => {
12
+ it('renders correctly without any props', () => {
12
13
  const { container } = render(<PktLoader id="loader1" />)
13
- await window.customElements.whenDefined('pkt-loader')
14
14
 
15
- const element = container.querySelector('pkt-loader')
16
- expect(element).toBeInTheDocument()
15
+ const root = container.querySelector('.pkt-loader')
16
+ expect(root).toBeInTheDocument()
17
+ expect(root).toHaveAttribute('id', 'loader1')
18
+ expect(root).toHaveAttribute('role', 'status')
19
+ expect(root).toHaveAttribute('aria-busy', 'true')
17
20
  })
18
21
 
19
- it('renders with size and variant', async () => {
20
- const { container } = render(<PktLoader id="loader1" size="small" variant="rainbow" message="Loading.." />)
21
- await window.customElements.whenDefined('pkt-loader')
22
+ it('renders with size and variant', () => {
23
+ const { container } = render(
24
+ <PktLoader id="loader1" size="small" variant="rainbow" message="Loading.." />,
25
+ )
22
26
 
23
- const loaderBox = container.querySelector('pkt-loader')
24
- const loaderSize = loaderBox?.querySelector('.pkt-loader--small')
25
-
26
- expect(loaderSize).toBeInTheDocument()
27
- expect(loaderBox).toHaveTextContent('Loading..')
27
+ const root = container.querySelector('.pkt-loader')
28
+ expect(root).toHaveClass('pkt-loader--small')
29
+ expect(root?.querySelector('.pkt-loader__spinner')).toBeInTheDocument()
30
+ expect(root).toHaveTextContent('Loading..')
28
31
  })
29
32
 
30
- it('renders inline', async () => {
33
+ it('renders inline', () => {
31
34
  const { container } = render(<PktLoader size="medium" message="Loading.." inline />)
32
- await window.customElements.whenDefined('pkt-loader')
33
- const elementInline = container.querySelector('.pkt-loader--inline')
35
+ expect(container.querySelector('.pkt-loader--inline')).toBeInTheDocument()
36
+ })
37
+
38
+ it('hides children while loading and shows them when not loading', () => {
39
+ const { container, rerender } = render(
40
+ <PktLoader isLoading>
41
+ <span data-testid="child">Innhold</span>
42
+ </PktLoader>,
43
+ )
44
+
45
+ const slot = container.querySelector('.pkt-contents')
46
+ expect(slot).toHaveClass('pkt-hide')
47
+ expect(slot?.querySelector('[data-testid="child"]')).toBeInTheDocument()
48
+
49
+ rerender(
50
+ <PktLoader isLoading={false}>
51
+ <span data-testid="child">Innhold</span>
52
+ </PktLoader>,
53
+ )
54
+ expect(container.querySelector('.pkt-contents')).not.toHaveClass('pkt-hide')
55
+ expect(container.querySelector('.pkt-loader__spinner')).not.toBeInTheDocument()
56
+ })
57
+ })
58
+
59
+ describe('delay', () => {
60
+ it('defers the spinner until after the delay has elapsed', () => {
61
+ vi.useFakeTimers()
62
+ try {
63
+ const { container } = render(<PktLoader delay={500} />)
64
+ expect(container.querySelector('.pkt-loader__spinner')).not.toBeInTheDocument()
34
65
 
35
- expect(elementInline).toBeInTheDocument()
66
+ act(() => {
67
+ vi.advanceTimersByTime(500)
68
+ })
69
+ expect(container.querySelector('.pkt-loader__spinner')).toBeInTheDocument()
70
+ } finally {
71
+ vi.useRealTimers()
72
+ }
36
73
  })
37
74
  })
38
75
 
@@ -1,33 +1,123 @@
1
1
  'use client'
2
2
 
3
- import { createComponent } from '@lit/react'
4
- import { type IPktLoader as IPktElLoader, PktLoader as PktElLoader } from '@oslokommune/punkt-elements'
5
- // eslint-disable-next-line no-restricted-syntax -- React is required for createComponent
6
- import React, { FC, ForwardedRef, forwardRef, type ReactElement } from 'react'
3
+ import classNames from 'classnames'
4
+ import { forwardRef, HTMLAttributes, ReactNode, useEffect, useState } from 'react'
7
5
 
8
- import type { PktElConstructor, PktElType } from '@/interfaces/IPktElements'
6
+ import { PktIcon } from '../icon/Icon'
9
7
 
10
- type ExtendedLoader = IPktElLoader & PktElType
8
+ declare global {
9
+ interface Window {
10
+ pktAnimationPath?: string
11
+ }
12
+ }
11
13
 
12
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type
13
- export interface IPktLoader extends ExtendedLoader {}
14
+ if (typeof window !== 'undefined') {
15
+ window.pktAnimationPath =
16
+ window.pktAnimationPath || 'https://punkt-cdn.oslo.kommune.no/latest/animations/'
17
+ }
14
18
 
15
- const LitComponent: FC<IPktLoader> = createComponent({
16
- tagName: 'pkt-loader',
17
- elementClass: PktElLoader as PktElConstructor<HTMLElement>,
18
- react: React,
19
- displayName: 'PktLoader',
20
- events: {},
21
- })
19
+ export type TPktLoaderVariant = 'blue' | 'rainbow' | 'shapes'
20
+ export type TPktLoaderSize = 'small' | 'medium' | 'large'
21
+
22
+ export interface IPktLoader extends HTMLAttributes<HTMLDivElement> {
23
+ /**
24
+ * Tid i millisekunder før loader vises. Nyttig når lastetiden kan være
25
+ * så kort at loader ikke trengs.
26
+ */
27
+ delay?: number
28
+ /**
29
+ * Vises loader inline eller som blokk.
30
+ */
31
+ inline?: boolean
32
+ /**
33
+ * Styrer om loader eller barnenoder vises. Når `false` vises barnenoder.
34
+ */
35
+ isLoading?: boolean
36
+ /**
37
+ * Meldingstekst som vises sammen med loader.
38
+ */
39
+ message?: string | null
40
+ /**
41
+ * Størrelse på loader. Standard er "medium".
42
+ */
43
+ size?: TPktLoaderSize
44
+ /**
45
+ * Loader-variant. Standard er "shapes" (Oslo-bølger). Andre er "blue" og "rainbow" (spinner-varianter).
46
+ */
47
+ variant?: TPktLoaderVariant
48
+ /**
49
+ * Overstyrer stien til loader-animasjoner.
50
+ */
51
+ loadingAnimationPath?: string
52
+ children?: ReactNode
53
+ }
22
54
 
23
- export const PktLoader: FC<IPktLoader> = forwardRef(
24
- ({ children, ...props }: IPktLoader, ref: ForwardedRef<HTMLElement>): ReactElement => {
25
- return (
26
- <LitComponent {...props} ref={ref}>
27
- <div className="pkt-contents">{children}</div>
28
- </LitComponent>
29
- )
55
+ const variantIcon = (variant: TPktLoaderVariant): string => {
56
+ switch (variant) {
57
+ case 'blue':
58
+ return 'spinner-blue'
59
+ case 'rainbow':
60
+ return 'spinner'
61
+ default:
62
+ return 'loader'
63
+ }
64
+ }
65
+
66
+ export const PktLoader = forwardRef<HTMLDivElement, IPktLoader>(function PktLoader(
67
+ {
68
+ delay = 0,
69
+ inline = false,
70
+ isLoading = true,
71
+ message = null,
72
+ size = 'medium',
73
+ variant = 'shapes',
74
+ loadingAnimationPath = typeof window !== 'undefined' ? window.pktAnimationPath : undefined,
75
+ className,
76
+ children,
77
+ ...rest
30
78
  },
31
- )
79
+ ref,
80
+ ) {
81
+ const [shouldDisplayLoader, setShouldDisplayLoader] = useState(delay === 0)
82
+
83
+ useEffect(() => {
84
+ if (delay > 0) {
85
+ setShouldDisplayLoader(false)
86
+ const handle = setTimeout(() => setShouldDisplayLoader(true), delay)
87
+ return () => clearTimeout(handle)
88
+ }
89
+ setShouldDisplayLoader(true)
90
+ }, [delay])
91
+
92
+ const classes = classNames(
93
+ 'pkt-loader',
94
+ inline ? 'pkt-loader--inline' : 'pkt-loader--box',
95
+ `pkt-loader--${size}`,
96
+ className,
97
+ )
98
+
99
+ return (
100
+ <div
101
+ {...rest}
102
+ ref={ref}
103
+ role="status"
104
+ aria-live="polite"
105
+ aria-busy={isLoading ? 'true' : 'false'}
106
+ className={classes}
107
+ >
108
+ {isLoading && shouldDisplayLoader && (
109
+ <div className="pkt-loader__spinner">
110
+ <PktIcon
111
+ name={variantIcon(variant)}
112
+ path={loadingAnimationPath}
113
+ className={`pkt-loader__svg pkt-loader__${variant}`}
114
+ />
115
+ {message && <p>{message}</p>}
116
+ </div>
117
+ )}
118
+ <div className={classNames('pkt-contents', isLoading && 'pkt-hide')}>{children}</div>
119
+ </div>
120
+ )
121
+ })
32
122
 
33
123
  PktLoader.displayName = 'PktLoader'