@tpzdsp/next-toolkit 3.0.0 → 3.1.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/ErrorModal/ErrorModal.css +4 -2
- package/src/components/ErrorModal/ErrorModal.stories.tsx +3 -4
- package/src/components/ErrorModal/ErrorModal.tsx +57 -54
- package/src/components/Footer/Copyright.tsx +0 -1
- package/src/components/Footer/Footer.stories.tsx +3 -0
- package/src/components/Footer/Footer.tsx +7 -2
- package/src/components/Footer/MetaLinks.tsx +28 -22
- package/src/components/Footer/Small/SmallFooter.stories.tsx +3 -0
- package/src/components/Footer/Small/SmallFooter.tsx +11 -0
- package/src/components/Header/Header.tsx +10 -9
- package/src/components/Layout/PageShell/PageShell.stories.tsx +2 -2
- package/src/components/Modal/Modal.css +7 -10
- package/src/components/Modal/Modal.stories.tsx +76 -18
- package/src/components/Modal/Modal.tsx +9 -0
- package/src/components/Select/Select.css +8 -0
- package/src/components/Select/Select.tsx +6 -1
- package/src/components/index.ts +1 -0
- package/src/http/constants.ts +1 -0
package/package.json
CHANGED
|
@@ -17,9 +17,11 @@
|
|
|
17
17
|
/* min-w-0 lets this column shrink below its content's natural width
|
|
18
18
|
within the flex row, same reasoning as elsewhere in this project:
|
|
19
19
|
without it, long unbroken content (e.g. a digest UUID) could force the
|
|
20
|
-
row wider than the modal itself instead of wrapping/scrolling.
|
|
20
|
+
row wider than the modal itself instead of wrapping/scrolling. flex and
|
|
21
|
+
flex-col themselves come from Stack (this is applied to one via
|
|
22
|
+
ErrorModal.tsx's className), not repeated here. */
|
|
21
23
|
.cn-error-modal-body {
|
|
22
|
-
@apply
|
|
24
|
+
@apply min-w-0;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/* Scoped to the summary list only (not the copy button/hint below it), an
|
|
@@ -5,7 +5,6 @@ import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
|
5
5
|
import { ErrorModal } from './ErrorModal';
|
|
6
6
|
import { ApiError } from '../../errors/ApiError';
|
|
7
7
|
import { Button } from '../Button/Button';
|
|
8
|
-
import { Paragraph } from '../Paragraph/Paragraph';
|
|
9
8
|
|
|
10
9
|
const meta: Meta<typeof ErrorModal> = {
|
|
11
10
|
title: 'Components/ErrorModal',
|
|
@@ -41,7 +40,7 @@ export const Default: Story = {
|
|
|
41
40
|
onOpenChange={setOpen}
|
|
42
41
|
error={new Error('Failed to fetch sampling points')}
|
|
43
42
|
>
|
|
44
|
-
|
|
43
|
+
An error occurred while fetching sampling points.
|
|
45
44
|
</ErrorModal>
|
|
46
45
|
</>
|
|
47
46
|
);
|
|
@@ -69,7 +68,7 @@ export const WithApiError: Story = {
|
|
|
69
68
|
onOpenChange={setOpen}
|
|
70
69
|
error={ApiError.internalServerError('The upstream sampling points service timed out')}
|
|
71
70
|
>
|
|
72
|
-
|
|
71
|
+
An error occurred while fetching sampling points.
|
|
73
72
|
</ErrorModal>
|
|
74
73
|
</>
|
|
75
74
|
);
|
|
@@ -93,7 +92,7 @@ export const WithoutError: Story = {
|
|
|
93
92
|
<Button onClick={() => setOpen(true)}>Trigger error</Button>
|
|
94
93
|
|
|
95
94
|
<ErrorModal open={open} onOpenChange={setOpen}>
|
|
96
|
-
|
|
95
|
+
Something went wrong. Try again in a few minutes.
|
|
97
96
|
</ErrorModal>
|
|
98
97
|
</>
|
|
99
98
|
);
|
|
@@ -52,68 +52,71 @@ export const ErrorModal = ({
|
|
|
52
52
|
<LuCircleAlert aria-hidden className="size-8" />
|
|
53
53
|
</div>
|
|
54
54
|
|
|
55
|
-
<
|
|
55
|
+
<Stack gap="sm" className="cn-error-modal-body">
|
|
56
56
|
<ModalHeader>
|
|
57
57
|
<ModalTitle>{title}</ModalTitle>
|
|
58
58
|
</ModalHeader>
|
|
59
59
|
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
60
|
+
{/* ModalDescription renders a <p> (it's the target of the
|
|
61
|
+
dialog's aria-describedby), which can only contain phrasing
|
|
62
|
+
content. children is assumed to be plain descriptive text for
|
|
63
|
+
that reason, Details below is a sibling instead, not nested
|
|
64
|
+
inside it, both to stay valid HTML (a browser silently closes
|
|
65
|
+
a <p> early upon hitting a nested block element, which desyncs
|
|
66
|
+
SSR/client markup into a hydration error, exactly like nesting
|
|
67
|
+
a <dl> in one here did) and because interactive elements like
|
|
68
|
+
the copy button below don't make sense as part of an
|
|
69
|
+
aria-describedby'd description in the first place. */}
|
|
70
|
+
<ModalDescription>{children}</ModalDescription>
|
|
71
|
+
|
|
72
|
+
{info ? (
|
|
73
|
+
<Details>
|
|
74
|
+
<DetailsTrigger>Technical information</DetailsTrigger>
|
|
75
|
+
|
|
76
|
+
<DetailsContent>
|
|
77
|
+
<Stack gap="sm">
|
|
78
|
+
<div className="cn-error-modal-details">
|
|
79
|
+
<SummaryList>
|
|
80
|
+
<SummaryListRow>
|
|
81
|
+
<SummaryListKey>Reason</SummaryListKey>
|
|
82
|
+
|
|
83
|
+
<SummaryListValue>{info.message}</SummaryListValue>
|
|
84
|
+
</SummaryListRow>
|
|
85
|
+
|
|
86
|
+
{info.details ? (
|
|
87
|
+
<SummaryListRow>
|
|
88
|
+
<SummaryListKey>Details</SummaryListKey>
|
|
89
|
+
|
|
90
|
+
<SummaryListValue>{info.details}</SummaryListValue>
|
|
91
|
+
</SummaryListRow>
|
|
92
|
+
) : null}
|
|
93
|
+
|
|
94
|
+
{info.digest ? (
|
|
95
|
+
<SummaryListRow>
|
|
96
|
+
<SummaryListKey>Digest</SummaryListKey>
|
|
97
|
+
|
|
98
|
+
<SummaryListValue>{info.digest}</SummaryListValue>
|
|
99
|
+
</SummaryListRow>
|
|
100
|
+
) : null}
|
|
101
|
+
</SummaryList>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div className="flex flex-col gap-gds-1">
|
|
105
|
+
<CopyButton appearance="secondary" content={formatErrorForClipboard(info)} />
|
|
106
|
+
|
|
107
|
+
<HintText className="text-sm">
|
|
108
|
+
Copy to share error details with the app administrator.
|
|
109
|
+
</HintText>
|
|
110
|
+
</div>
|
|
111
|
+
</Stack>
|
|
112
|
+
</DetailsContent>
|
|
113
|
+
</Details>
|
|
114
|
+
) : null}
|
|
112
115
|
|
|
113
116
|
<ModalFooter className="sm:justify-start">
|
|
114
117
|
<Button onClick={() => onOpenChange?.(false)}>{closeLabel}</Button>
|
|
115
118
|
</ModalFooter>
|
|
116
|
-
</
|
|
119
|
+
</Stack>
|
|
117
120
|
</ModalContent>
|
|
118
121
|
</Modal>
|
|
119
122
|
);
|
|
@@ -7,7 +7,6 @@ export const Copyright = () => {
|
|
|
7
7
|
<ExternalLink
|
|
8
8
|
className="text-balance sm:text-nowrap text-center sm:text-left justify-start"
|
|
9
9
|
appearance="contrast"
|
|
10
|
-
variant="plain"
|
|
11
10
|
href="https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/uk-government-licensing-framework/crown-copyright/"
|
|
12
11
|
>
|
|
13
12
|
© Crown Copyright {currentYear}
|
|
@@ -4,7 +4,12 @@ import { MetaLinks } from './MetaLinks';
|
|
|
4
4
|
import { DefraLogo } from '../../assets/images/DefraLogo';
|
|
5
5
|
import { WidthContainer } from '../Layout/WidthContainer/WidthContainer';
|
|
6
6
|
|
|
7
|
-
export
|
|
7
|
+
export type FooterProps = {
|
|
8
|
+
/** Passed straight through to MetaLinks, see its own accessibilityStatementHref doc. */
|
|
9
|
+
accessibilityStatementHref?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const Footer = ({ accessibilityStatementHref }: FooterProps) => (
|
|
8
13
|
<footer className="bg-wash">
|
|
9
14
|
<WidthContainer className="py-gds-3">
|
|
10
15
|
<div
|
|
@@ -15,7 +20,7 @@ export const Footer = () => (
|
|
|
15
20
|
<nav className="row-start-1 col-start-1 w-full pb-gds-2 self-end">
|
|
16
21
|
<h2 className="sr-only">Support Links</h2>
|
|
17
22
|
|
|
18
|
-
<MetaLinks />
|
|
23
|
+
<MetaLinks accessibilityStatementHref={accessibilityStatementHref} />
|
|
19
24
|
</nav>
|
|
20
25
|
|
|
21
26
|
<div className="row-start-2 col-start-1 self-end pt-gds-1">
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Link } from '../Link/Link';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
{
|
|
5
|
-
label: 'Accessibility statement',
|
|
6
|
-
url: '/support/faqs/275810340/275810354',
|
|
7
|
-
},
|
|
3
|
+
const STATIC_LINKS = [
|
|
8
4
|
{
|
|
9
5
|
label: 'Cookie Policy',
|
|
10
6
|
url: '/help/cookies',
|
|
@@ -23,19 +19,29 @@ const LINKS = [
|
|
|
23
19
|
},
|
|
24
20
|
];
|
|
25
21
|
|
|
26
|
-
export
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
22
|
+
export type MetaLinksProps = {
|
|
23
|
+
/** No single accessibility statement applies across every app this footer
|
|
24
|
+
* ships in, so unlike the other, shared support links above, this one has
|
|
25
|
+
* to be supplied by the consuming app rather than hardcoded here. Optional
|
|
26
|
+
* for now, apps that don't have one yet just don't get the link, rather
|
|
27
|
+
* than one pointing nowhere. */
|
|
28
|
+
accessibilityStatementHref?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const MetaLinks = ({ accessibilityStatementHref }: MetaLinksProps) => {
|
|
32
|
+
const links = accessibilityStatementHref
|
|
33
|
+
? [{ label: 'Accessibility statement', url: accessibilityStatementHref }, ...STATIC_LINKS]
|
|
34
|
+
: STATIC_LINKS;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<ul className="flex w-full flex-wrap gap-x-gds-4 gap-y-gds-2 md:gap-gds-5 md:justify-start">
|
|
38
|
+
{links.map((link) => (
|
|
39
|
+
<li className="flex" key={link.label}>
|
|
40
|
+
<Link className="md:text-left" appearance="contrast" href={link.url}>
|
|
41
|
+
{link.label}
|
|
42
|
+
</Link>
|
|
43
|
+
</li>
|
|
44
|
+
))}
|
|
45
|
+
</ul>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
@@ -3,6 +3,7 @@ import { EaLogo } from '../../../assets/images/EaLogo';
|
|
|
3
3
|
import { OglLogo } from '../../../assets/images/OglLogo';
|
|
4
4
|
import { ExternalLink } from '../../ExternalLink/ExternalLink';
|
|
5
5
|
import { WidthContainer } from '../../Layout/WidthContainer/WidthContainer';
|
|
6
|
+
import { Link } from '../../Link/Link';
|
|
6
7
|
|
|
7
8
|
const CROWN_COPYRIGHT_URL =
|
|
8
9
|
'https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/uk-government-licensing-framework/crown-copyright/';
|
|
@@ -13,11 +14,15 @@ const LICENSING_DETAILS_URL =
|
|
|
13
14
|
export type SmallFooterProps = {
|
|
14
15
|
eaLogoPrimaryFill?: string;
|
|
15
16
|
eaLogoHighlightFill?: string;
|
|
17
|
+
/** Same as Footer's own accessibilityStatementHref: no universal default,
|
|
18
|
+
* omit it to hide the link entirely rather than pointing at nothing. */
|
|
19
|
+
accessibilityStatementHref?: string;
|
|
16
20
|
};
|
|
17
21
|
|
|
18
22
|
export const SmallFooter = ({
|
|
19
23
|
eaLogoPrimaryFill = '#F3F2F1',
|
|
20
24
|
eaLogoHighlightFill = '#79A12F',
|
|
25
|
+
accessibilityStatementHref,
|
|
21
26
|
}: SmallFooterProps) => (
|
|
22
27
|
<footer aria-label="Site footer" className="flex justify-center">
|
|
23
28
|
<WidthContainer className="bg-wash py-gds-2">
|
|
@@ -32,6 +37,12 @@ export const SmallFooter = ({
|
|
|
32
37
|
</div>
|
|
33
38
|
|
|
34
39
|
<div className="flex items-center gap-gds-6 xs:gap-gds-2">
|
|
40
|
+
{accessibilityStatementHref ? (
|
|
41
|
+
<Link appearance="contrast" href={accessibilityStatementHref}>
|
|
42
|
+
Accessibility statement
|
|
43
|
+
</Link>
|
|
44
|
+
) : null}
|
|
45
|
+
|
|
35
46
|
<ExternalLink
|
|
36
47
|
appearance="contrast"
|
|
37
48
|
variant="plain"
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
Separator,
|
|
18
18
|
WidthContainer,
|
|
19
19
|
Popover,
|
|
20
|
-
Paragraph,
|
|
21
20
|
ParagraphGroup,
|
|
22
21
|
} from '@tpzdsp/next-toolkit/components';
|
|
23
22
|
import { cn } from '@tpzdsp/next-toolkit/utils';
|
|
@@ -144,7 +143,7 @@ export const Header = ({ appContent, credentials }: HeaderProps) => {
|
|
|
144
143
|
|
|
145
144
|
<abbr
|
|
146
145
|
title="Department for Environment, Food, and Rural Affairs"
|
|
147
|
-
className="sr-only xs:not-sr-only"
|
|
146
|
+
className="sr-only xs:not-sr-only no-underline"
|
|
148
147
|
>
|
|
149
148
|
DEFRA
|
|
150
149
|
</abbr>
|
|
@@ -156,7 +155,9 @@ export const Header = ({ appContent, credentials }: HeaderProps) => {
|
|
|
156
155
|
<span className="hidden xs:inline">Data Services Platform</span>
|
|
157
156
|
|
|
158
157
|
<span className="inline xs:hidden">
|
|
159
|
-
<abbr title="Data Services Platform">
|
|
158
|
+
<abbr title="Data Services Platform" className="no-underline">
|
|
159
|
+
DSP
|
|
160
|
+
</abbr>
|
|
160
161
|
</span>
|
|
161
162
|
</Link>
|
|
162
163
|
</div>
|
|
@@ -165,9 +166,9 @@ export const Header = ({ appContent, credentials }: HeaderProps) => {
|
|
|
165
166
|
<ul className="flex items-center gap-gds-4">
|
|
166
167
|
{DSP_NAV_LINKS.map((item) => (
|
|
167
168
|
<li className={cn('flex items-center nav-item', item.mainNav)} key={item.href}>
|
|
168
|
-
<
|
|
169
|
+
<Link href={item.href} appearance="inverse">
|
|
169
170
|
{item.label}
|
|
170
|
-
</
|
|
171
|
+
</Link>
|
|
171
172
|
</li>
|
|
172
173
|
))}
|
|
173
174
|
<li ref={triggerRef} className="flex items-center more-btn shrink-0">
|
|
@@ -195,11 +196,11 @@ export const Header = ({ appContent, credentials }: HeaderProps) => {
|
|
|
195
196
|
|
|
196
197
|
<PopoverContent sideOffset={0}>
|
|
197
198
|
<ParagraphGroup>
|
|
198
|
-
<
|
|
199
|
+
<div>
|
|
199
200
|
<div className="font-bold">Signed in as:</div>
|
|
200
201
|
|
|
201
202
|
<div>{credentials.user.email}</div>
|
|
202
|
-
</
|
|
203
|
+
</div>
|
|
203
204
|
|
|
204
205
|
<Link href={`${LOGOUT_URL}?redirect_uri=${redirectUri}`}>Sign-out</Link>
|
|
205
206
|
</ParagraphGroup>
|
|
@@ -273,9 +274,9 @@ export const Header = ({ appContent, credentials }: HeaderProps) => {
|
|
|
273
274
|
<ul className="flex flex-wrap w-full gap-x-gds-2 gap-y-gds-2">
|
|
274
275
|
{DSP_NAV_LINKS.map((item) => (
|
|
275
276
|
<li className={cn('flex items-center nav-item', item.drawerNav)} key={item.href}>
|
|
276
|
-
<
|
|
277
|
+
<Link href={item.href} appearance="inverse">
|
|
277
278
|
{item.label}
|
|
278
|
-
</
|
|
279
|
+
</Link>
|
|
279
280
|
</li>
|
|
280
281
|
))}
|
|
281
282
|
</ul>
|
|
@@ -80,7 +80,7 @@ export const Static: Story = {
|
|
|
80
80
|
<LongContent />
|
|
81
81
|
</MainWrapper>
|
|
82
82
|
|
|
83
|
-
<Footer />
|
|
83
|
+
<Footer accessibilityStatementHref="/accessibility-statement" />
|
|
84
84
|
</PageShell>
|
|
85
85
|
),
|
|
86
86
|
};
|
|
@@ -102,7 +102,7 @@ export const ScrollLocked: Story = {
|
|
|
102
102
|
<LongContent />
|
|
103
103
|
</MainWrapper>
|
|
104
104
|
|
|
105
|
-
<Footer />
|
|
105
|
+
<Footer accessibilityStatementHref="/accessibility-statement" />
|
|
106
106
|
</PageShell>
|
|
107
107
|
),
|
|
108
108
|
};
|
|
@@ -91,16 +91,13 @@
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/* MARK: Vertical rhythm between sections
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
+ :where(.cn-modal-header, .cn-modal-description, .cn-modal-footer) {
|
|
102
|
-
margin-top: var(--spacing-gds-4);
|
|
103
|
-
}
|
|
94
|
+
No layout opinion of its own on cn-modal-content, it needs to stay a
|
|
95
|
+
plain block so consumers can turn it into a flex row instead (e.g.
|
|
96
|
+
ErrorModal, an icon beside the content) without fighting a hardcoded
|
|
97
|
+
flex-direction. Wrap whatever sits inside (ModalHeader, ModalDescription,
|
|
98
|
+
ModalFooter, and anything else a specific modal needs, a form, a
|
|
99
|
+
Details section, ...) in a Stack instead, same as any other arbitrary
|
|
100
|
+
vertical content elsewhere in the library. */
|
|
104
101
|
|
|
105
102
|
/* MARK: Modal title and description */
|
|
106
103
|
|
|
@@ -13,6 +13,11 @@ import {
|
|
|
13
13
|
ModalTrigger,
|
|
14
14
|
} from './Modal';
|
|
15
15
|
import { Button } from '../Button/Button';
|
|
16
|
+
import { FormGroup } from '../Form/FormGroup';
|
|
17
|
+
import { HintText } from '../HintText/HintText';
|
|
18
|
+
import { Input } from '../Input/Input';
|
|
19
|
+
import { Label } from '../Label/Label';
|
|
20
|
+
import { Stack } from '../Layout/Stack/Stack';
|
|
16
21
|
|
|
17
22
|
const meta: Meta<typeof Modal> = {
|
|
18
23
|
title: 'Components/Modal',
|
|
@@ -21,7 +26,8 @@ const meta: Meta<typeof Modal> = {
|
|
|
21
26
|
parameters: {
|
|
22
27
|
docs: {
|
|
23
28
|
description: {
|
|
24
|
-
component:
|
|
29
|
+
component:
|
|
30
|
+
"Use the Modal primitives to display dialog content in an overlay. ModalContent has no layout opinion of its own (so consumers can turn it into something other than a column, e.g. ErrorModal's icon-beside-content row), wrap ModalHeader/ModalDescription/ModalFooter and anything else in a Stack for spacing between them, as below.",
|
|
25
31
|
},
|
|
26
32
|
},
|
|
27
33
|
},
|
|
@@ -45,23 +51,75 @@ export const Default: Story = {
|
|
|
45
51
|
</ModalTrigger>
|
|
46
52
|
|
|
47
53
|
<ModalContent>
|
|
48
|
-
<
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
54
|
+
<Stack gap="sm">
|
|
55
|
+
<ModalHeader>
|
|
56
|
+
<ModalTitle>Confirm action</ModalTitle>
|
|
57
|
+
</ModalHeader>
|
|
58
|
+
|
|
59
|
+
<ModalDescription>
|
|
60
|
+
Are you sure you want to continue? This action cannot be undone.
|
|
61
|
+
</ModalDescription>
|
|
62
|
+
|
|
63
|
+
<ModalFooter>
|
|
64
|
+
<Button appearance="secondary" onClick={() => setOpen(false)}>
|
|
65
|
+
Cancel
|
|
66
|
+
</Button>
|
|
67
|
+
|
|
68
|
+
<ModalClose asChild>
|
|
69
|
+
<Button>Confirm</Button>
|
|
70
|
+
</ModalClose>
|
|
71
|
+
</ModalFooter>
|
|
72
|
+
</Stack>
|
|
73
|
+
</ModalContent>
|
|
74
|
+
</Modal>
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const WithFormContent: Story = {
|
|
80
|
+
parameters: {
|
|
81
|
+
docs: {
|
|
82
|
+
description: {
|
|
83
|
+
story:
|
|
84
|
+
"Arbitrary content (a FormGroup here, could be anything) sits in the same Stack as ModalHeader/ModalDescription/ModalFooter, getting the same gap on both sides automatically, no extra spacing rules needed regardless of what's between them.",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
render: () => {
|
|
89
|
+
const [open, setOpen] = useState(false);
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<Modal open={open} onOpenChange={setOpen}>
|
|
93
|
+
<ModalTrigger asChild>
|
|
94
|
+
<Button>Open modal</Button>
|
|
95
|
+
</ModalTrigger>
|
|
96
|
+
|
|
97
|
+
<ModalContent>
|
|
98
|
+
<Stack gap="sm">
|
|
99
|
+
<ModalHeader>
|
|
100
|
+
<ModalTitle>Rename item</ModalTitle>
|
|
101
|
+
</ModalHeader>
|
|
102
|
+
|
|
103
|
+
<ModalDescription>Choose a new name for this item.</ModalDescription>
|
|
104
|
+
|
|
105
|
+
<FormGroup>
|
|
106
|
+
<Label htmlFor="rename-item">Name</Label>
|
|
107
|
+
|
|
108
|
+
<HintText>As it should appear in the list</HintText>
|
|
109
|
+
|
|
110
|
+
<Input id="rename-item" aria-describedby="rename-item-hint" defaultValue="Untitled" />
|
|
111
|
+
</FormGroup>
|
|
112
|
+
|
|
113
|
+
<ModalFooter>
|
|
114
|
+
<Button appearance="secondary" onClick={() => setOpen(false)}>
|
|
115
|
+
Cancel
|
|
116
|
+
</Button>
|
|
117
|
+
|
|
118
|
+
<ModalClose asChild>
|
|
119
|
+
<Button>Save</Button>
|
|
120
|
+
</ModalClose>
|
|
121
|
+
</ModalFooter>
|
|
122
|
+
</Stack>
|
|
65
123
|
</ModalContent>
|
|
66
124
|
</Modal>
|
|
67
125
|
);
|
|
@@ -131,6 +131,15 @@ export const ModalTitle = ({ className, ...props }: ModalTitleProps) => (
|
|
|
131
131
|
|
|
132
132
|
export type ModalDescriptionProps = ComponentProps<typeof DialogPrimitive.Description>;
|
|
133
133
|
|
|
134
|
+
// Renders a <p> (Description's own default), which can only contain
|
|
135
|
+
// phrasing content, children here is assumed to be plain descriptive text
|
|
136
|
+
// for that reason. Anything else (a Details/SummaryList section, a copy
|
|
137
|
+
// button, ...) belongs as a sibling of ModalDescription instead, not
|
|
138
|
+
// nested inside it, both because a <p> can't legally contain block
|
|
139
|
+
// content (a browser silently closes it early on hitting one, which
|
|
140
|
+
// desyncs SSR/client markup into a hydration error) and because
|
|
141
|
+
// aria-describedby content should read as plain description text, not
|
|
142
|
+
// fold in interactive elements. See ErrorModal for the sibling pattern.
|
|
134
143
|
export const ModalDescription = ({ className, ...props }: ModalDescriptionProps) => (
|
|
135
144
|
<DialogPrimitive.Description
|
|
136
145
|
data-slot="modal-description"
|
|
@@ -100,6 +100,14 @@
|
|
|
100
100
|
@apply panel absolute w-full mt-1 overflow-hidden cn-z-overlay;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/* react-select flips the menu above the control when there isn't room
|
|
104
|
+
below. mt-1 pushes it further down rather than opening a gap in that
|
|
105
|
+
direction, mirror it as mb-1 so there's still a visible gap between
|
|
106
|
+
control and menu regardless of which side it opens on. */
|
|
107
|
+
.cn-select-menu-top {
|
|
108
|
+
@apply mt-0 mb-1;
|
|
109
|
+
}
|
|
110
|
+
|
|
103
111
|
.cn-select-menu-list {
|
|
104
112
|
@apply flex flex-col overflow-y-auto max-h-60;
|
|
105
113
|
}
|
|
@@ -81,7 +81,12 @@ const buildClassNames = <Option, IsMulti extends boolean, Group extends GroupBas
|
|
|
81
81
|
cn('cn-select-clear-indicator', userClassNames?.clearIndicator?.(props)),
|
|
82
82
|
loadingIndicator: (props) =>
|
|
83
83
|
cn('cn-select-loading-indicator', userClassNames?.loadingIndicator?.(props)),
|
|
84
|
-
menu: (props) =>
|
|
84
|
+
menu: (props) =>
|
|
85
|
+
cn(
|
|
86
|
+
'cn-select-menu',
|
|
87
|
+
props.placement === 'top' && 'cn-select-menu-top',
|
|
88
|
+
userClassNames?.menu?.(props),
|
|
89
|
+
),
|
|
85
90
|
menuList: (props) => cn('cn-select-menu-list', userClassNames?.menuList?.(props)),
|
|
86
91
|
option: (props) =>
|
|
87
92
|
cn(
|
package/src/components/index.ts
CHANGED
|
@@ -330,6 +330,7 @@ export { Header } from './Header/Header';
|
|
|
330
330
|
export { type HeaderProps } from './Header/Header';
|
|
331
331
|
export { HeaderAppContent } from './Header/HeaderAppContent';
|
|
332
332
|
export { Footer } from './Footer/Footer';
|
|
333
|
+
export { type FooterProps } from './Footer/Footer';
|
|
333
334
|
export { SmallFooter } from './Footer/Small/SmallFooter';
|
|
334
335
|
export { Unauthorized } from './Layout/401/401';
|
|
335
336
|
export { Forbidden } from './Layout/403/403';
|