@tpzdsp/next-toolkit 1.11.3 → 1.12.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 +1 -1
- package/src/components/Button/Button.tsx +2 -3
- package/src/components/Card/Card.tsx +8 -5
- package/src/components/ErrorBoundary/ErrorFallback.tsx +15 -2
- package/src/components/ErrorText/ErrorText.tsx +2 -3
- package/src/components/Heading/Heading.tsx +8 -9
- package/src/components/Hint/Hint.tsx +2 -3
- package/src/components/Modal/Modal.tsx +13 -4
- package/src/components/NotificationBanner/NotificationBanner.stories.tsx +45 -0
- package/src/components/NotificationBanner/NotificationBanner.test.tsx +60 -0
- package/src/components/NotificationBanner/NotificationBanner.tsx +45 -0
- package/src/components/Paragraph/Paragraph.tsx +2 -3
- package/src/components/SlidingPanel/SlidingPanel.tsx +15 -2
- package/src/components/accordion/Accordion.tsx +19 -6
- package/src/components/backToTop/BackToTop.tsx +16 -15
- package/src/components/chip/Chip.tsx +8 -5
- package/src/components/container/Container.tsx +6 -2
- package/src/components/divider/RuleDivider.tsx +8 -3
- package/src/components/dropdown/DropdownMenu.tsx +9 -7
- package/src/components/form/Input.tsx +2 -3
- package/src/components/form/TextArea.tsx +2 -3
- package/src/components/index.ts +2 -0
- package/src/components/link/ExternalLink.tsx +2 -3
- package/src/components/link/Link.tsx +2 -2
- package/src/components/select/Select.tsx +14 -20
- package/src/components/select/SelectSkeleton.tsx +10 -7
- package/src/components/skipLink/SkipLink.tsx +13 -4
package/package.json
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { twMerge } from 'tailwind-merge';
|
|
2
|
-
|
|
3
1
|
import type { ExtendProps } from '../../types/utils';
|
|
2
|
+
import { cn } from '../../utils';
|
|
4
3
|
|
|
5
4
|
type Props = {
|
|
6
5
|
type?: 'submit' | 'reset' | 'button';
|
|
@@ -27,7 +26,7 @@ export const Button = ({
|
|
|
27
26
|
return (
|
|
28
27
|
<button
|
|
29
28
|
type={type}
|
|
30
|
-
className={
|
|
29
|
+
className={cn(
|
|
31
30
|
`sm:w-auto text-lg relative flex w-full outline-none items-center border-transparent
|
|
32
31
|
justify-center border-2 text-center active-enabled:translate-y-[2px] focus:shadow-focus
|
|
33
32
|
focus:border-focus focus:shadow-[inset_0_0_0_1px] focus-idle:border-focus
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ExtendProps } from '../../types';
|
|
2
|
+
import { cn } from '../../utils';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
className?: string;
|
|
4
|
+
type Props = {
|
|
5
5
|
children: React.ReactNode;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
-
export
|
|
8
|
+
export type CardProps = ExtendProps<'article', Props>;
|
|
9
|
+
|
|
10
|
+
export const Card = ({ className, children, ...props }: CardProps) => {
|
|
9
11
|
return (
|
|
10
12
|
<article
|
|
11
|
-
className={
|
|
13
|
+
className={cn(
|
|
12
14
|
'h-full flex flex-col pt-[12px] px-[4px] mx-3 border-t border-slate-500',
|
|
13
15
|
className,
|
|
14
16
|
)}
|
|
17
|
+
{...props}
|
|
15
18
|
>
|
|
16
19
|
{children}
|
|
17
20
|
</article>
|
|
@@ -5,12 +5,21 @@ import { useId } from 'react';
|
|
|
5
5
|
import { type FallbackProps } from 'react-error-boundary';
|
|
6
6
|
|
|
7
7
|
import { ApiError } from '../../errors/ApiError';
|
|
8
|
+
import type { ExtendProps } from '../../types';
|
|
9
|
+
import { cn } from '../../utils';
|
|
8
10
|
import { Accordion } from '../accordion/Accordion';
|
|
9
11
|
import { Button } from '../Button/Button';
|
|
10
12
|
import { Heading } from '../Heading/Heading';
|
|
11
13
|
import { Paragraph } from '../Paragraph/Paragraph';
|
|
12
14
|
|
|
13
|
-
export
|
|
15
|
+
export type ErrorFallbackProps = ExtendProps<'div', FallbackProps>;
|
|
16
|
+
|
|
17
|
+
export const ErrorFallback = ({
|
|
18
|
+
resetErrorBoundary,
|
|
19
|
+
error,
|
|
20
|
+
className,
|
|
21
|
+
...props
|
|
22
|
+
}: ErrorFallbackProps) => {
|
|
14
23
|
const id = useId();
|
|
15
24
|
|
|
16
25
|
let message;
|
|
@@ -39,8 +48,12 @@ export const ErrorFallback = ({ resetErrorBoundary, error }: FallbackProps) => {
|
|
|
39
48
|
<div
|
|
40
49
|
role="alert"
|
|
41
50
|
aria-labelledby={id}
|
|
42
|
-
className=
|
|
51
|
+
className={cn(
|
|
52
|
+
'grid gap-2 border-form border-transparent border-l-error max-w-full pl-2',
|
|
53
|
+
className,
|
|
54
|
+
)}
|
|
43
55
|
style={{ gridTemplateColumns: '1fr auto' }}
|
|
56
|
+
{...props}
|
|
44
57
|
>
|
|
45
58
|
<div className="flex flex-col gap-0.5">
|
|
46
59
|
<Heading id={id} type="h3" className="text-error text-base font-semibold py-0">
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { twMerge } from 'tailwind-merge';
|
|
2
|
-
|
|
3
1
|
import type { ExtendProps } from '../../types/utils';
|
|
2
|
+
import { cn } from '../../utils';
|
|
4
3
|
|
|
5
4
|
export type ErrorTextProps = ExtendProps<'p'>;
|
|
6
5
|
|
|
@@ -10,7 +9,7 @@ export const ErrorText = ({ className, children, ...props }: ErrorTextProps) =>
|
|
|
10
9
|
return (
|
|
11
10
|
<Component
|
|
12
11
|
role="alert"
|
|
13
|
-
className={
|
|
12
|
+
className={cn('mb-3 text-base text-error font-bold', className)}
|
|
14
13
|
{...props}
|
|
15
14
|
>
|
|
16
15
|
{children}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { twMerge } from 'tailwind-merge';
|
|
2
|
-
|
|
3
1
|
import type { ExtendProps } from '../../types';
|
|
2
|
+
import { cn } from '../../utils';
|
|
4
3
|
|
|
5
4
|
type Props = {
|
|
6
5
|
type: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
@@ -9,41 +8,41 @@ type Props = {
|
|
|
9
8
|
// h1 through h6 have identical props
|
|
10
9
|
export type HeadingProps = ExtendProps<'h1', Props>;
|
|
11
10
|
|
|
12
|
-
export const Heading = ({ type, className, children }: HeadingProps) => {
|
|
11
|
+
export const Heading = ({ type, className, children, ...props }: HeadingProps) => {
|
|
13
12
|
switch (type) {
|
|
14
13
|
case 'h1':
|
|
15
14
|
return (
|
|
16
|
-
<h1 className={
|
|
15
|
+
<h1 className={cn('py-4 text-4xl font-bold text-text-primary', className)} {...props}>
|
|
17
16
|
{children}
|
|
18
17
|
</h1>
|
|
19
18
|
);
|
|
20
19
|
case 'h2':
|
|
21
20
|
return (
|
|
22
|
-
<h2 className={
|
|
21
|
+
<h2 className={cn('py-3 text-3xl font-bold text-text-primary', className)} {...props}>
|
|
23
22
|
{children}
|
|
24
23
|
</h2>
|
|
25
24
|
);
|
|
26
25
|
case 'h3':
|
|
27
26
|
return (
|
|
28
|
-
<h3 className={
|
|
27
|
+
<h3 className={cn('py-3 text-xl font-bold text-text-primary', className)} {...props}>
|
|
29
28
|
{children}
|
|
30
29
|
</h3>
|
|
31
30
|
);
|
|
32
31
|
case 'h4':
|
|
33
32
|
return (
|
|
34
|
-
<h4 className={
|
|
33
|
+
<h4 className={cn('py-3 text-lg font-bold text-text-primary', className)} {...props}>
|
|
35
34
|
{children}
|
|
36
35
|
</h4>
|
|
37
36
|
);
|
|
38
37
|
case 'h5':
|
|
39
38
|
return (
|
|
40
|
-
<h5 className={
|
|
39
|
+
<h5 className={cn('py-2 text-base font-bold text-text-primary', className)} {...props}>
|
|
41
40
|
{children}
|
|
42
41
|
</h5>
|
|
43
42
|
);
|
|
44
43
|
case 'h6':
|
|
45
44
|
return (
|
|
46
|
-
<h6 className={
|
|
45
|
+
<h6 className={cn('py-2 text-sm font-bold text-text-primary', className)} {...props}>
|
|
47
46
|
{children}
|
|
48
47
|
</h6>
|
|
49
48
|
);
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { twMerge } from 'tailwind-merge';
|
|
2
|
-
|
|
3
1
|
import type { ExtendProps } from '../../types/utils';
|
|
2
|
+
import { cn } from '../../utils';
|
|
4
3
|
|
|
5
4
|
export type HintProps = ExtendProps<'div'>;
|
|
6
5
|
|
|
7
6
|
export const Hint = ({ className, children, ...props }: HintProps) => {
|
|
8
7
|
return (
|
|
9
|
-
<div className={
|
|
8
|
+
<div className={cn('mb-2 text-lg text-text-secondary', className)} {...props}>
|
|
10
9
|
{children}
|
|
11
10
|
</div>
|
|
12
11
|
);
|
|
@@ -5,13 +5,18 @@ import { useCallback, useEffect, useRef } from 'react';
|
|
|
5
5
|
import { createPortal } from 'react-dom';
|
|
6
6
|
import { IoMdCloseCircle } from 'react-icons/io';
|
|
7
7
|
|
|
8
|
-
type
|
|
8
|
+
import type { ExtendProps } from '../../types';
|
|
9
|
+
import { cn } from '../../utils';
|
|
10
|
+
|
|
11
|
+
type Props = {
|
|
9
12
|
isOpen: boolean;
|
|
10
13
|
onClose: () => void;
|
|
11
14
|
children: React.ReactNode;
|
|
12
15
|
};
|
|
13
16
|
|
|
14
|
-
export
|
|
17
|
+
export type ModalProps = ExtendProps<'dialog', Props>;
|
|
18
|
+
|
|
19
|
+
export const Modal = ({ isOpen, onClose, children, className, ...props }: ModalProps) => {
|
|
15
20
|
const modalRef = useRef<HTMLDialogElement>(null);
|
|
16
21
|
|
|
17
22
|
const handleClose = useCallback(() => {
|
|
@@ -36,8 +41,11 @@ export const Modal = ({ isOpen, onClose, children }: ModalProps) => {
|
|
|
36
41
|
// dialog elements do have a key handler as you can close them with `Escape`, so this can be ignored
|
|
37
42
|
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
|
|
38
43
|
<dialog
|
|
39
|
-
className=
|
|
40
|
-
|
|
44
|
+
className={cn(
|
|
45
|
+
`fixed inset-0 flex items-center justify-center w-full h-full m-0 bg-transparent
|
|
46
|
+
backdrop:bg-black/50`,
|
|
47
|
+
className,
|
|
48
|
+
)}
|
|
41
49
|
ref={modalRef}
|
|
42
50
|
onCancel={(event) => {
|
|
43
51
|
event.preventDefault();
|
|
@@ -49,6 +57,7 @@ export const Modal = ({ isOpen, onClose, children }: ModalProps) => {
|
|
|
49
57
|
// close the modal if the user clicks outside the main content (i.e. on the backdrop)
|
|
50
58
|
handleClose();
|
|
51
59
|
}}
|
|
60
|
+
{...props}
|
|
52
61
|
>
|
|
53
62
|
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
|
54
63
|
<div
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
|
|
3
|
+
import { NotificationBanner } from './NotificationBanner';
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
title: 'Components/NotificationBanner',
|
|
7
|
+
component: NotificationBanner,
|
|
8
|
+
parameters: {
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component:
|
|
12
|
+
'A notification banner for displaying important messages. Follows the GOV.UK Design System notification banner pattern. Commonly used wrapped in a <noscript> tag to inform users when JavaScript is required.',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
} as Meta<typeof NotificationBanner>;
|
|
17
|
+
|
|
18
|
+
export const Default: StoryObj<typeof NotificationBanner> = {
|
|
19
|
+
args: {
|
|
20
|
+
message: 'This is an important notification message for users.',
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const CustomTitle: StoryObj<typeof NotificationBanner> = {
|
|
25
|
+
args: {
|
|
26
|
+
title: 'Service Update',
|
|
27
|
+
message:
|
|
28
|
+
'This service will be undergoing maintenance on Saturday 15th January from 9am to 5pm.',
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const JavaScriptRequired: StoryObj<typeof NotificationBanner> = {
|
|
33
|
+
args: {
|
|
34
|
+
title: 'Important: JavaScript is required',
|
|
35
|
+
message:
|
|
36
|
+
'This application requires JavaScript to display the interactive map and data. Please enable JavaScript in your browser settings to use this service.',
|
|
37
|
+
},
|
|
38
|
+
parameters: {
|
|
39
|
+
docs: {
|
|
40
|
+
description: {
|
|
41
|
+
story: 'Example usage for a noscript fallback message.',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { NotificationBanner } from './NotificationBanner';
|
|
4
|
+
import { render, screen } from '../../test/renderers';
|
|
5
|
+
|
|
6
|
+
describe('NotificationBanner Component', () => {
|
|
7
|
+
it('renders with default title and provided message', () => {
|
|
8
|
+
render(<NotificationBanner message="Test message content" />);
|
|
9
|
+
|
|
10
|
+
const heading = screen.getByRole('heading', { name: 'Important' });
|
|
11
|
+
const message = screen.getByText('Test message content');
|
|
12
|
+
|
|
13
|
+
expect(heading).toHaveAttribute('id', 'notification-banner-title');
|
|
14
|
+
expect(heading).toBeInTheDocument();
|
|
15
|
+
expect(message).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('renders with custom title and message', () => {
|
|
19
|
+
render(<NotificationBanner title="Custom Title" message="Custom message content" />);
|
|
20
|
+
|
|
21
|
+
const heading = screen.getByRole('heading', { name: 'Custom Title' });
|
|
22
|
+
const message = screen.getByText('Custom message content');
|
|
23
|
+
|
|
24
|
+
expect(heading).toBeInTheDocument();
|
|
25
|
+
expect(message).toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('renders as a section with aria-labelledby', () => {
|
|
29
|
+
render(<NotificationBanner message="Test message" />);
|
|
30
|
+
|
|
31
|
+
const section = screen.getByRole('region', { name: 'Important' });
|
|
32
|
+
|
|
33
|
+
expect(section).toBeInTheDocument();
|
|
34
|
+
expect(section).toHaveAttribute('aria-labelledby', 'notification-banner-title');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('applies default styling classes', () => {
|
|
38
|
+
render(<NotificationBanner message="Test message" />);
|
|
39
|
+
|
|
40
|
+
const section = screen.getByRole('region');
|
|
41
|
+
|
|
42
|
+
expect(section).toHaveClass('bg-brand', 'text-white', 'font-gds');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('allows custom className to be merged', () => {
|
|
46
|
+
render(<NotificationBanner message="Test message" className="custom-class" />);
|
|
47
|
+
|
|
48
|
+
const section = screen.getByRole('region');
|
|
49
|
+
|
|
50
|
+
expect(section).toHaveClass('bg-brand', 'text-white', 'font-gds', 'custom-class');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('spreads additional props to section element', () => {
|
|
54
|
+
render(<NotificationBanner message="Test message" data-testid="notification-banner" />);
|
|
55
|
+
|
|
56
|
+
const section = screen.getByTestId('notification-banner');
|
|
57
|
+
|
|
58
|
+
expect(section).toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ExtendProps } from '../../types';
|
|
2
|
+
import { cn } from '../../utils';
|
|
3
|
+
import { Heading } from '../Heading/Heading';
|
|
4
|
+
import { Paragraph } from '../Paragraph/Paragraph';
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
title?: string;
|
|
8
|
+
message: string;
|
|
9
|
+
/** Explicitly disallow children as we use title and message props instead */
|
|
10
|
+
children?: never;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type NotificationBannerProps = ExtendProps<'section', Props>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A notification banner component for displaying important messages.
|
|
17
|
+
* Follows the GOV.UK Design System notification banner pattern.
|
|
18
|
+
*
|
|
19
|
+
* Common use cases:
|
|
20
|
+
* - Wrapped in <noscript> to inform users that JavaScript is required
|
|
21
|
+
* - Service announcements or maintenance notices
|
|
22
|
+
* - Important information that needs prominent display
|
|
23
|
+
*/
|
|
24
|
+
export const NotificationBanner = ({
|
|
25
|
+
title = 'Important',
|
|
26
|
+
message,
|
|
27
|
+
className,
|
|
28
|
+
...props
|
|
29
|
+
}: NotificationBannerProps) => (
|
|
30
|
+
<section
|
|
31
|
+
aria-labelledby="notification-banner-title"
|
|
32
|
+
className={cn('bg-brand text-white font-gds', className)}
|
|
33
|
+
{...props}
|
|
34
|
+
>
|
|
35
|
+
<div className="px-4 py-2 border-b border-white/30">
|
|
36
|
+
<Heading type="h2" id="notification-banner-title" className="text-lg text-white py-2">
|
|
37
|
+
{title}
|
|
38
|
+
</Heading>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div className="p-4">
|
|
42
|
+
<Paragraph className="text-lg text-white pb-0">{message}</Paragraph>
|
|
43
|
+
</div>
|
|
44
|
+
</section>
|
|
45
|
+
);
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { twMerge } from 'tailwind-merge';
|
|
2
|
-
|
|
3
1
|
import type { ExtendProps } from '../../types/utils';
|
|
2
|
+
import { cn } from '../../utils';
|
|
4
3
|
|
|
5
4
|
export type ParagraphProps = ExtendProps<'p'>;
|
|
6
5
|
|
|
7
6
|
export const Paragraph = ({ className, children, ...props }: ParagraphProps) => {
|
|
8
7
|
return (
|
|
9
|
-
<p className={
|
|
8
|
+
<p className={cn('pb-4 text-sm text-text-primary', className)} {...props}>
|
|
10
9
|
{children}
|
|
11
10
|
</p>
|
|
12
11
|
);
|
|
@@ -4,20 +4,27 @@ import { useState, useEffect, type ReactNode, useRef, useMemo, useId } from 'rea
|
|
|
4
4
|
|
|
5
5
|
import { FocusTrap } from 'focus-trap-react';
|
|
6
6
|
|
|
7
|
+
import type { ExtendProps } from '../../types';
|
|
8
|
+
import { cn } from '../../utils';
|
|
9
|
+
|
|
7
10
|
type Position = 'center-left' | 'center-right' | 'center-top' | 'center-bottom';
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
type Props = {
|
|
10
13
|
children: ReactNode;
|
|
11
14
|
position?: Position;
|
|
12
15
|
tabLabel?: string;
|
|
13
16
|
defaultOpen?: boolean;
|
|
14
17
|
};
|
|
15
18
|
|
|
19
|
+
export type SlidingPanelProps = ExtendProps<'div', Props>;
|
|
20
|
+
|
|
16
21
|
export const SlidingPanel = ({
|
|
17
22
|
children,
|
|
18
23
|
tabLabel = 'Open',
|
|
19
24
|
position = 'center-left',
|
|
20
25
|
defaultOpen = false,
|
|
26
|
+
className,
|
|
27
|
+
...props
|
|
21
28
|
}: SlidingPanelProps) => {
|
|
22
29
|
const id = useId();
|
|
23
30
|
|
|
@@ -145,7 +152,13 @@ export const SlidingPanel = ({
|
|
|
145
152
|
};
|
|
146
153
|
|
|
147
154
|
return (
|
|
148
|
-
<div
|
|
155
|
+
<div
|
|
156
|
+
className={cn(
|
|
157
|
+
'absolute inset-0 z-30 overflow-hidden pointer-events-none sliding-panel',
|
|
158
|
+
className,
|
|
159
|
+
)}
|
|
160
|
+
{...props}
|
|
161
|
+
>
|
|
149
162
|
<button
|
|
150
163
|
className={`pointer-events-auto ${buttonPosition} bg-gray-700 text-white z-40 focus-yellow`}
|
|
151
164
|
style={getButtonStyle}
|
|
@@ -3,22 +3,35 @@
|
|
|
3
3
|
import { type ReactNode, useId, useState } from 'react';
|
|
4
4
|
|
|
5
5
|
import { LuChevronDown } from 'react-icons/lu';
|
|
6
|
-
import { twMerge } from 'tailwind-merge';
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
import type { ExtendProps } from '../../types';
|
|
8
|
+
import { cn } from '../../utils';
|
|
9
|
+
|
|
10
|
+
type Props = {
|
|
9
11
|
title: string;
|
|
10
12
|
children: ReactNode;
|
|
11
13
|
defaultOpen?: boolean;
|
|
12
14
|
};
|
|
13
15
|
|
|
14
|
-
export
|
|
16
|
+
export type AccordionProps = ExtendProps<'div', Props>;
|
|
17
|
+
|
|
18
|
+
export const Accordion = ({
|
|
19
|
+
title,
|
|
20
|
+
children,
|
|
21
|
+
defaultOpen = false,
|
|
22
|
+
className,
|
|
23
|
+
...props
|
|
24
|
+
}: AccordionProps) => {
|
|
15
25
|
const contentId = useId();
|
|
16
26
|
const buttonId = useId();
|
|
17
27
|
|
|
18
28
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
19
29
|
|
|
20
30
|
return (
|
|
21
|
-
<div
|
|
31
|
+
<div
|
|
32
|
+
className={cn('flex flex-col gap-2 border-l-2 border-neutral-100 rounded-md', className)}
|
|
33
|
+
{...props}
|
|
34
|
+
>
|
|
22
35
|
<button
|
|
23
36
|
aria-expanded={isOpen}
|
|
24
37
|
aria-controls={contentId}
|
|
@@ -31,7 +44,7 @@ export const Accordion = ({ title, children, defaultOpen = false }: AccordionPro
|
|
|
31
44
|
<span>{title}</span>
|
|
32
45
|
|
|
33
46
|
<span aria-hidden="true">
|
|
34
|
-
<LuChevronDown className={
|
|
47
|
+
<LuChevronDown className={cn('w-4 h-4', isOpen ? 'rotate-180' : '')} />
|
|
35
48
|
</span>
|
|
36
49
|
</button>
|
|
37
50
|
|
|
@@ -39,7 +52,7 @@ export const Accordion = ({ title, children, defaultOpen = false }: AccordionPro
|
|
|
39
52
|
id={contentId}
|
|
40
53
|
aria-labelledby={buttonId}
|
|
41
54
|
aria-hidden={!isOpen}
|
|
42
|
-
className={
|
|
55
|
+
className={cn('px-2 pb-1', isOpen ? 'block' : 'hidden')}
|
|
43
56
|
>
|
|
44
57
|
{children}
|
|
45
58
|
</section>
|
|
@@ -4,24 +4,28 @@ import { type KeyboardEvent, useCallback, useEffect, useState } from 'react';
|
|
|
4
4
|
|
|
5
5
|
import { LuArrowUp } from 'react-icons/lu';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import type { ExtendProps } from '../../types';
|
|
8
|
+
import { cn, KeyboardKeys } from '../../utils';
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
type Props = {
|
|
10
11
|
/** Scroll threshold in pixels before button appears */
|
|
11
12
|
threshold?: number;
|
|
12
13
|
/** Position from bottom in pixels */
|
|
13
14
|
bottom?: number;
|
|
14
15
|
/** Position from left in pixels */
|
|
15
16
|
left?: number;
|
|
16
|
-
/**
|
|
17
|
-
|
|
17
|
+
/** Explicitly disallow children as we hard-code the button content */
|
|
18
|
+
children?: never;
|
|
18
19
|
};
|
|
19
20
|
|
|
21
|
+
export type BackToTopProps = ExtendProps<'button', Props>;
|
|
22
|
+
|
|
20
23
|
export const BackToTop = ({
|
|
21
24
|
threshold = 600,
|
|
22
25
|
bottom = 16,
|
|
23
26
|
left = 8,
|
|
24
|
-
className
|
|
27
|
+
className,
|
|
28
|
+
...props
|
|
25
29
|
}: BackToTopProps) => {
|
|
26
30
|
const [isVisible, setIsVisible] = useState(false);
|
|
27
31
|
|
|
@@ -104,16 +108,12 @@ export const BackToTop = ({
|
|
|
104
108
|
return (
|
|
105
109
|
<button
|
|
106
110
|
type="button"
|
|
107
|
-
className={
|
|
108
|
-
fixed z-50 inline-flex items-center gap-1
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
${className}
|
|
114
|
-
`.trim()}
|
|
115
|
-
// className={`fixed bottom-4 left-2 text-link gap-1 inline-flex items-center bg-white
|
|
116
|
-
// ${className}`.trim()}
|
|
111
|
+
className={cn(
|
|
112
|
+
`fixed z-50 inline-flex items-center gap-1 bg-white text-link border border-gray-300
|
|
113
|
+
rounded-md px-3 py-2 shadow-lg hover:bg-gray-50 focus:outline-none focus:ring-2
|
|
114
|
+
focus:ring-blue-500 focus:ring-offset-2 transition-all duration-200 ease-in-out`,
|
|
115
|
+
className,
|
|
116
|
+
)}
|
|
117
117
|
style={{
|
|
118
118
|
bottom: `${bottom}px`,
|
|
119
119
|
left: `${left}px`,
|
|
@@ -122,6 +122,7 @@ export const BackToTop = ({
|
|
|
122
122
|
onKeyDown={handleKeyDown}
|
|
123
123
|
aria-label="Scroll back to top of page"
|
|
124
124
|
title="Back to top"
|
|
125
|
+
{...props}
|
|
125
126
|
>
|
|
126
127
|
<LuArrowUp size={20} aria-hidden="true" />
|
|
127
128
|
|
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ExtendProps } from '../../types';
|
|
2
|
+
import { cn } from '../../utils';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
type Props = {
|
|
4
5
|
children: React.ReactNode;
|
|
5
|
-
className?: string;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
-
export
|
|
8
|
+
export type ChipProps = ExtendProps<'span', Props>;
|
|
9
|
+
|
|
10
|
+
export const Chip = ({ className, children, ...props }: ChipProps) => {
|
|
9
11
|
return (
|
|
10
12
|
<span
|
|
11
|
-
className={
|
|
13
|
+
className={cn(
|
|
12
14
|
`inline-flex items-center rounded-lg bg-gray-200 px-3 py-1 text-sm font-medium
|
|
13
15
|
text-gray-800`,
|
|
14
16
|
className,
|
|
15
17
|
)}
|
|
18
|
+
{...props}
|
|
16
19
|
>
|
|
17
20
|
{children}
|
|
18
21
|
</span>
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
|
|
3
|
+
import type { ExtendProps } from '../../types';
|
|
3
4
|
import { cn } from '../../utils';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
type Props = {
|
|
6
7
|
children: ReactNode;
|
|
7
|
-
className?: string;
|
|
8
8
|
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
|
9
9
|
centerContent?: boolean;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
export type ContainerProps = ExtendProps<'div', Props>;
|
|
13
|
+
|
|
12
14
|
const containerSizes = {
|
|
13
15
|
sm: 'max-w-3xl',
|
|
14
16
|
md: 'max-w-5xl',
|
|
@@ -22,6 +24,7 @@ export const Container = ({
|
|
|
22
24
|
className,
|
|
23
25
|
size = 'lg',
|
|
24
26
|
centerContent = false,
|
|
27
|
+
...props
|
|
25
28
|
}: ContainerProps) => {
|
|
26
29
|
return (
|
|
27
30
|
<div
|
|
@@ -31,6 +34,7 @@ export const Container = ({
|
|
|
31
34
|
centerContent && 'flex items-center justify-center min-h-screen',
|
|
32
35
|
className,
|
|
33
36
|
)}
|
|
37
|
+
{...props}
|
|
34
38
|
>
|
|
35
39
|
{children}
|
|
36
40
|
</div>
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
type
|
|
1
|
+
import type { ExtendProps } from '../../types';
|
|
2
|
+
import { cn } from '../../utils';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
2
5
|
children?: React.ReactNode;
|
|
3
6
|
};
|
|
4
7
|
|
|
5
|
-
export
|
|
8
|
+
export type RuleDividerProps = ExtendProps<'div', Props>;
|
|
9
|
+
|
|
10
|
+
export const RuleDivider = ({ children, className, ...props }: RuleDividerProps) => {
|
|
6
11
|
return (
|
|
7
|
-
<div className=
|
|
12
|
+
<div className={cn('flex items-center', className)} {...props}>
|
|
8
13
|
<hr className="flex-grow" />
|
|
9
14
|
{children ? (
|
|
10
15
|
<>
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import type { ComponentType } from 'react';
|
|
3
|
+
import type { ComponentType, HTMLAttributes } from 'react';
|
|
4
4
|
|
|
5
5
|
import { LuChevronDown } from 'react-icons/lu';
|
|
6
|
-
import { twMerge } from 'tailwind-merge';
|
|
7
6
|
|
|
8
7
|
import {
|
|
9
8
|
type ButtonRendererProps,
|
|
10
9
|
type ItemRendererProps,
|
|
11
10
|
useDropdownMenu,
|
|
12
11
|
} from './useDropdownMenu';
|
|
12
|
+
import { cn } from '../../utils';
|
|
13
13
|
|
|
14
14
|
export type DropdownMenuItem<ItemProps extends object> = ItemRendererProps & {
|
|
15
15
|
label: string;
|
|
@@ -17,7 +17,7 @@ export type DropdownMenuItem<ItemProps extends object> = ItemRendererProps & {
|
|
|
17
17
|
|
|
18
18
|
export type DrowndownMenuButton = ButtonRendererProps;
|
|
19
19
|
|
|
20
|
-
type DropdownMenuProps<ItemProps extends object> = {
|
|
20
|
+
type DropdownMenuProps<ItemProps extends object> = HTMLAttributes<HTMLDivElement> & {
|
|
21
21
|
items: DropdownMenuItem<ItemProps>[];
|
|
22
22
|
containerClassName?: string;
|
|
23
23
|
menuContainerClassName?: string;
|
|
@@ -32,7 +32,7 @@ const DefaultButton = ({ state: { isOpen }, ...props }: ButtonRendererProps) =>
|
|
|
32
32
|
<button
|
|
33
33
|
{...props}
|
|
34
34
|
aria-label="Open Menu"
|
|
35
|
-
className={
|
|
35
|
+
className={cn(
|
|
36
36
|
`text-black flex gap-2 items-center justify-center border rounded-md border-gray-300
|
|
37
37
|
bg-white px-2 py-1 text-sm shadow-sm focus:border-brand focus:outline-none focus:ring-1
|
|
38
38
|
focus:ring-brand`,
|
|
@@ -51,7 +51,7 @@ const DefaultItem = <Item extends object>({ label, ...props }: DropdownMenuItem<
|
|
|
51
51
|
<div
|
|
52
52
|
{...props}
|
|
53
53
|
aria-label={label}
|
|
54
|
-
className={
|
|
54
|
+
className={cn(
|
|
55
55
|
`text-black cursor-pointer hover:bg-slate-200 focus:bg-slate-200 active:bg-slate-300 px-2
|
|
56
56
|
py-1`,
|
|
57
57
|
props.className,
|
|
@@ -70,6 +70,8 @@ export const DropdownMenu = <Item extends object>({
|
|
|
70
70
|
buttonClassName,
|
|
71
71
|
itemRenderer = DefaultItem,
|
|
72
72
|
itemClassName,
|
|
73
|
+
className,
|
|
74
|
+
...props
|
|
73
75
|
}: DropdownMenuProps<Item>) => {
|
|
74
76
|
// rebind to avoid linting errors
|
|
75
77
|
const ButtonRenderer = buttonRenderer;
|
|
@@ -78,13 +80,13 @@ export const DropdownMenu = <Item extends object>({
|
|
|
78
80
|
const { isOpen, buttonProps, itemProps } = useDropdownMenu(items.length);
|
|
79
81
|
|
|
80
82
|
return (
|
|
81
|
-
<div className={
|
|
83
|
+
<div className={cn('relative', containerClassName, className)} {...props}>
|
|
82
84
|
<ButtonRenderer {...buttonProps} className={buttonClassName} />
|
|
83
85
|
|
|
84
86
|
<div
|
|
85
87
|
style={{ display: isOpen ? 'flex' : 'none' }}
|
|
86
88
|
aria-hidden={!isOpen}
|
|
87
|
-
className={
|
|
89
|
+
className={cn(
|
|
88
90
|
`absolute right-0 mt-1 bg-white border shadow-md rounded-md min-w-full flex-col gap-0
|
|
89
91
|
z-[999] divide-y divide-slate-400`,
|
|
90
92
|
menuContainerClassName,
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { twMerge } from 'tailwind-merge';
|
|
4
|
-
|
|
5
3
|
import type { ExtendProps } from '../../types/utils';
|
|
4
|
+
import { cn } from '../../utils';
|
|
6
5
|
|
|
7
6
|
type Props = {
|
|
8
7
|
hasError?: boolean;
|
|
@@ -14,7 +13,7 @@ export const Input = ({ hasError, className, ...props }: InputProps) => {
|
|
|
14
13
|
return (
|
|
15
14
|
<input
|
|
16
15
|
{...props}
|
|
17
|
-
className={
|
|
16
|
+
className={cn(
|
|
18
17
|
'rounded-md border p-1 disabled:opacity-60 disabled:bg-gray-100',
|
|
19
18
|
hasError ? 'border-error' : '',
|
|
20
19
|
className,
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { twMerge } from 'tailwind-merge';
|
|
4
|
-
|
|
5
3
|
import type { ExtendProps } from '../../types/utils';
|
|
4
|
+
import { cn } from '../../utils';
|
|
6
5
|
|
|
7
6
|
type Props = {
|
|
8
7
|
hasError?: boolean;
|
|
@@ -14,7 +13,7 @@ export const TextArea = ({ hasError, className, ...props }: TextAreaProps) => {
|
|
|
14
13
|
return (
|
|
15
14
|
<textarea
|
|
16
15
|
{...props}
|
|
17
|
-
className={
|
|
16
|
+
className={cn(
|
|
18
17
|
'rounded-md border p-1 disabled:opacity-60 disabled:bg-gray-100',
|
|
19
18
|
hasError ? 'border-error' : '',
|
|
20
19
|
className,
|
package/src/components/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export { GlobalVars } from './googleAnalytics/GlobalVars';
|
|
|
10
10
|
export { GoogleAnalytics } from './googleAnalytics/GoogleAnalytics';
|
|
11
11
|
export { Heading } from './Heading/Heading';
|
|
12
12
|
export { Hint } from './Hint/Hint';
|
|
13
|
+
export { NotificationBanner } from './NotificationBanner/NotificationBanner';
|
|
13
14
|
export { DefraLogo } from './images/DefraLogo';
|
|
14
15
|
export { EaLogo } from './images/EaLogo';
|
|
15
16
|
export { OglLogo } from './images/OglLogo';
|
|
@@ -30,6 +31,7 @@ export type { ContainerProps } from './container/Container';
|
|
|
30
31
|
export type { ErrorTextProps } from './ErrorText/ErrorText';
|
|
31
32
|
export type { HeadingProps } from './Heading/Heading';
|
|
32
33
|
export type { HintProps } from './Hint/Hint';
|
|
34
|
+
export type { NotificationBannerProps } from './NotificationBanner/NotificationBanner';
|
|
33
35
|
export type { ExternalLinkProps } from './link/ExternalLink';
|
|
34
36
|
export type { LinkProps } from './link/Link';
|
|
35
37
|
export type { ParagraphProps } from './Paragraph/Paragraph';
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
|
|
3
|
-
import { twMerge } from 'tailwind-merge';
|
|
4
|
-
|
|
5
3
|
import type { ExtendProps } from '../../types/utils';
|
|
4
|
+
import { cn } from '../../utils';
|
|
6
5
|
|
|
7
6
|
type Props = {
|
|
8
7
|
children: ReactNode;
|
|
@@ -13,7 +12,7 @@ export type ExternalLinkProps = ExtendProps<'a', Props>;
|
|
|
13
12
|
export const ExternalLink = ({ href, className, children, ...props }: ExternalLinkProps) => (
|
|
14
13
|
<a
|
|
15
14
|
{...props}
|
|
16
|
-
className={
|
|
15
|
+
className={cn(
|
|
17
16
|
`cursor-pointer text-link hover:decoration-[max(3px,_.1875rem,_.12em)] hover:text-link-hover
|
|
18
17
|
visited:text-link-visited focus:decoration-[max(3px,_.1875rem,_.12em)]
|
|
19
18
|
decoration-[max(1px,_.0625rem)] underline-offset-[0.1578em] underline outline-none
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import NextLink from 'next/link';
|
|
2
|
-
import { twMerge } from 'tailwind-merge';
|
|
3
2
|
|
|
4
3
|
import type { ExtendProps } from '../../types/utils';
|
|
4
|
+
import { cn } from '../../utils';
|
|
5
5
|
|
|
6
6
|
type Props = {
|
|
7
7
|
href: string | object;
|
|
@@ -12,7 +12,7 @@ export type LinkProps = ExtendProps<typeof NextLink, Props>;
|
|
|
12
12
|
export const Link = ({ href, className, children, ...props }: LinkProps) => (
|
|
13
13
|
<NextLink
|
|
14
14
|
{...props}
|
|
15
|
-
className={
|
|
15
|
+
className={cn(
|
|
16
16
|
`cursor-pointer text-link hover:decoration-[max(3px,_.1875rem,_.12em)] hover:text-link-hover
|
|
17
17
|
visited:text-link-visited active:text-black focus:decoration-[max(3px,_.1875rem,_.12em)]
|
|
18
18
|
decoration-[max(1px,_.0625rem)] underline-offset-[0.1578em] underline outline-none
|
|
@@ -12,9 +12,9 @@ import type {
|
|
|
12
12
|
Props as ReactSelectProps,
|
|
13
13
|
} from 'react-select';
|
|
14
14
|
import { components, default as ReactSelect } from 'react-select';
|
|
15
|
-
import { twMerge } from 'tailwind-merge';
|
|
16
15
|
|
|
17
16
|
import { SELECT_CONTAINER_CLASSES, SELECT_CONTROL_CLASSES, SELECT_MIN_HEIGHT } from './common';
|
|
17
|
+
import { cn } from '../../utils';
|
|
18
18
|
|
|
19
19
|
// extends the react-select props with some of our own
|
|
20
20
|
export type SelectProps<Option, IsMulti extends boolean, Group extends GroupBase<Option>> = Omit<
|
|
@@ -26,9 +26,9 @@ const getClassNames = <Option, IsMulti extends boolean, Group extends GroupBase<
|
|
|
26
26
|
userClassNames?: ClassNamesConfig<Option, IsMulti, Group>,
|
|
27
27
|
): ClassNamesConfig<Option, IsMulti, Group> => {
|
|
28
28
|
return {
|
|
29
|
-
container: (props) =>
|
|
29
|
+
container: (props) => cn(SELECT_CONTAINER_CLASSES, userClassNames?.container?.(props)),
|
|
30
30
|
control: (props) =>
|
|
31
|
-
|
|
31
|
+
cn(
|
|
32
32
|
SELECT_CONTROL_CLASSES,
|
|
33
33
|
SELECT_MIN_HEIGHT,
|
|
34
34
|
props.isDisabled ? '!cursor-not-allowed bg-gray-100' : 'bg-white',
|
|
@@ -37,43 +37,37 @@ const getClassNames = <Option, IsMulti extends boolean, Group extends GroupBase<
|
|
|
37
37
|
: '',
|
|
38
38
|
userClassNames?.control?.(props),
|
|
39
39
|
),
|
|
40
|
-
dropdownIndicator: (props) =>
|
|
41
|
-
placeholder: (props) =>
|
|
40
|
+
dropdownIndicator: (props) => cn('w-4 h-4', userClassNames?.dropdownIndicator?.(props)),
|
|
41
|
+
placeholder: (props) => cn('text-text-secondary', userClassNames?.placeholder?.(props)),
|
|
42
42
|
menu: (props) =>
|
|
43
|
-
|
|
43
|
+
cn(
|
|
44
44
|
'bg-white rounded-md border mt-1 overflow-hidden shadow-sm shadow-[0px_0px_6px_0px_#00000044]',
|
|
45
45
|
userClassNames?.menu?.(props),
|
|
46
46
|
),
|
|
47
|
-
menuList: (props) =>
|
|
47
|
+
menuList: (props) => cn('flex flex-col', userClassNames?.menuList?.(props)),
|
|
48
48
|
option: (props) =>
|
|
49
|
-
|
|
49
|
+
cn(
|
|
50
50
|
'overflow-x-hidden text-ellipsis px-4 py-1 shrink-0',
|
|
51
51
|
!props.isSelected && props.isFocused ? 'bg-slate-100' : '',
|
|
52
52
|
props.isSelected ? 'bg-brand text-white' : '',
|
|
53
53
|
userClassNames?.option?.(props),
|
|
54
54
|
),
|
|
55
|
-
noOptionsMessage: (props) =>
|
|
55
|
+
noOptionsMessage: (props) => cn('px-4 py-1', userClassNames?.noOptionsMessage?.(props)),
|
|
56
56
|
clearIndicator: (props) =>
|
|
57
|
-
|
|
58
|
-
'cursor-pointer pointer-events-auto w-4 h-4',
|
|
59
|
-
userClassNames?.clearIndicator?.(props),
|
|
60
|
-
),
|
|
57
|
+
cn('cursor-pointer pointer-events-auto w-4 h-4', userClassNames?.clearIndicator?.(props)),
|
|
61
58
|
indicatorsContainer: (props) =>
|
|
62
|
-
|
|
63
|
-
'flex gap-1 items-center justify-center',
|
|
64
|
-
userClassNames?.indicatorsContainer?.(props),
|
|
65
|
-
),
|
|
59
|
+
cn('flex gap-1 items-center justify-center', userClassNames?.indicatorsContainer?.(props)),
|
|
66
60
|
indicatorSeparator: (props) =>
|
|
67
|
-
|
|
61
|
+
cn(
|
|
68
62
|
props.isMulti && props.hasValue ? 'bg-border-input/30' : '',
|
|
69
63
|
userClassNames?.indicatorSeparator?.(props),
|
|
70
64
|
),
|
|
71
65
|
multiValue: (props) =>
|
|
72
|
-
|
|
66
|
+
cn(
|
|
73
67
|
'flex gap-2 items-center justify-center px-2 bg-brand text-white rounded-md m-[2px]',
|
|
74
68
|
userClassNames?.multiValue?.(props),
|
|
75
69
|
),
|
|
76
|
-
multiValueRemove: (props) =>
|
|
70
|
+
multiValueRemove: (props) => cn('w-3 h-3', userClassNames?.multiValueRemove?.(props)),
|
|
77
71
|
};
|
|
78
72
|
};
|
|
79
73
|
|
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import { twMerge } from 'tailwind-merge';
|
|
2
|
-
|
|
3
1
|
import { SELECT_CONTAINER_CLASSES, SELECT_CONTROL_CLASSES, SELECT_MIN_HEIGHT } from './common';
|
|
2
|
+
import type { ExtendProps } from '../../types';
|
|
3
|
+
import { cn } from '../../utils';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
type Props = {
|
|
6
|
+
/** Explicitly disallow children as we hard-code the skeleton content */
|
|
7
|
+
children?: never;
|
|
7
8
|
};
|
|
8
9
|
|
|
9
|
-
export
|
|
10
|
+
export type SelectSkeletonProps = ExtendProps<'div', Props>;
|
|
11
|
+
|
|
12
|
+
export const SelectSkeleton = ({ className, ...props }: SelectSkeletonProps = {}) => {
|
|
10
13
|
return (
|
|
11
|
-
<div className={
|
|
12
|
-
<div className={
|
|
14
|
+
<div className={cn(SELECT_CONTAINER_CLASSES, className)} {...props}>
|
|
15
|
+
<div className={cn(SELECT_CONTROL_CLASSES, SELECT_MIN_HEIGHT)}>
|
|
13
16
|
<div
|
|
14
17
|
className="w-full h-full bg-gray-100 animate-pulse rounded-md col-span-2"
|
|
15
18
|
aria-label="Loading options"
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import type { ExtendProps } from '../../types';
|
|
4
|
+
import { cn, KeyboardKeys } from '../../utils';
|
|
4
5
|
import { Link } from '../link/Link';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
type Props = {
|
|
7
8
|
mainContentId?: string;
|
|
9
|
+
/** Explicitly disallow children as we hard-code the link content */
|
|
10
|
+
children?: never;
|
|
8
11
|
};
|
|
9
12
|
|
|
10
|
-
export
|
|
13
|
+
export type SkipLinkProps = ExtendProps<'nav', Props>;
|
|
14
|
+
|
|
15
|
+
export const SkipLink = ({
|
|
16
|
+
mainContentId = 'main-content',
|
|
17
|
+
className,
|
|
18
|
+
...props
|
|
19
|
+
}: SkipLinkProps) => {
|
|
11
20
|
const handleActivate = () => {
|
|
12
21
|
// Let the browser scroll first, then move focus
|
|
13
22
|
setTimeout(() => {
|
|
@@ -20,7 +29,7 @@ export const SkipLink = ({ mainContentId = 'main-content' }: SkipLinkProps) => {
|
|
|
20
29
|
};
|
|
21
30
|
|
|
22
31
|
return (
|
|
23
|
-
<nav aria-label="Skip navigation">
|
|
32
|
+
<nav aria-label="Skip navigation" className={cn(className)} {...props}>
|
|
24
33
|
<Link
|
|
25
34
|
className="absolute w-full p-3 text-black bg-focus focus:relative focus:top-0 -top-full
|
|
26
35
|
visited:text-black hover:text-black skip-link"
|