@buildcanada/components 0.1.0 → 0.2.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/feedback/Dialog/Dialog.scss +158 -0
- package/src/feedback/Dialog/Dialog.tsx +120 -0
- package/src/feedback/Dialog/index.ts +1 -0
- package/src/feedback/PopupForm/PopupForm.scss +34 -0
- package/src/feedback/PopupForm/PopupForm.tsx +90 -0
- package/src/feedback/PopupForm/index.ts +1 -0
- package/src/index.ts +4 -0
- package/src/styles/main.scss +2 -0
package/package.json
CHANGED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
@use "../../styles/tokens" as *;
|
|
2
|
+
|
|
3
|
+
/*******************************************************************************
|
|
4
|
+
* Dialog Component
|
|
5
|
+
*
|
|
6
|
+
* A non-modal floating panel that doesn't block interaction with the UI.
|
|
7
|
+
* Sizes to its contents and can be positioned in corners.
|
|
8
|
+
******************************************************************************/
|
|
9
|
+
|
|
10
|
+
.bc-dialog {
|
|
11
|
+
position: fixed;
|
|
12
|
+
z-index: $z-popover;
|
|
13
|
+
background: $white;
|
|
14
|
+
border: 1px solid $border-default;
|
|
15
|
+
box-shadow: $shadow-lg;
|
|
16
|
+
max-width: calc(100vw - var(--bc-dialog-offset, 16px) * 2);
|
|
17
|
+
max-height: calc(100dvh - var(--bc-dialog-offset, 16px) * 2);
|
|
18
|
+
|
|
19
|
+
// Animation
|
|
20
|
+
animation: bc-dialog-enter $transition-base;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@keyframes bc-dialog-enter {
|
|
24
|
+
from {
|
|
25
|
+
opacity: 0;
|
|
26
|
+
transform: scale(0.95);
|
|
27
|
+
}
|
|
28
|
+
to {
|
|
29
|
+
opacity: 1;
|
|
30
|
+
transform: scale(1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/*******************************************************************************
|
|
35
|
+
* Dialog Positions
|
|
36
|
+
******************************************************************************/
|
|
37
|
+
|
|
38
|
+
.bc-dialog--top-left {
|
|
39
|
+
top: var(--bc-dialog-offset, 16px);
|
|
40
|
+
left: var(--bc-dialog-offset, 16px);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.bc-dialog--top-right {
|
|
44
|
+
top: var(--bc-dialog-offset, 16px);
|
|
45
|
+
right: var(--bc-dialog-offset, 16px);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.bc-dialog--bottom-left {
|
|
49
|
+
bottom: var(--bc-dialog-offset, 16px);
|
|
50
|
+
left: var(--bc-dialog-offset, 16px);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.bc-dialog--bottom-right {
|
|
54
|
+
bottom: var(--bc-dialog-offset, 16px);
|
|
55
|
+
right: var(--bc-dialog-offset, 16px);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.bc-dialog--center {
|
|
59
|
+
top: 50%;
|
|
60
|
+
left: 50%;
|
|
61
|
+
transform: translate(-50%, -50%);
|
|
62
|
+
animation: bc-dialog-enter-center $transition-base;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@keyframes bc-dialog-enter-center {
|
|
66
|
+
from {
|
|
67
|
+
opacity: 0;
|
|
68
|
+
transform: translate(-50%, -50%) scale(0.95);
|
|
69
|
+
}
|
|
70
|
+
to {
|
|
71
|
+
opacity: 1;
|
|
72
|
+
transform: translate(-50%, -50%) scale(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/*******************************************************************************
|
|
77
|
+
* Dialog Container
|
|
78
|
+
******************************************************************************/
|
|
79
|
+
|
|
80
|
+
.bc-dialog__container {
|
|
81
|
+
display: flex;
|
|
82
|
+
flex-direction: column;
|
|
83
|
+
overflow: hidden;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/*******************************************************************************
|
|
87
|
+
* Dialog Header
|
|
88
|
+
******************************************************************************/
|
|
89
|
+
|
|
90
|
+
.bc-dialog__header {
|
|
91
|
+
display: flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
justify-content: space-between;
|
|
94
|
+
gap: $space-2;
|
|
95
|
+
padding: $space-2 $space-2 0 $space-3;
|
|
96
|
+
flex-shrink: 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.bc-dialog__title {
|
|
100
|
+
font-family: "Soehne Kraftig", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
101
|
+
font-size: 1.125rem;
|
|
102
|
+
font-weight: 500;
|
|
103
|
+
line-height: 1.4;
|
|
104
|
+
color: $charcoal;
|
|
105
|
+
margin: 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.bc-dialog__close {
|
|
109
|
+
display: flex;
|
|
110
|
+
align-items: center;
|
|
111
|
+
justify-content: center;
|
|
112
|
+
width: 32px;
|
|
113
|
+
height: 32px;
|
|
114
|
+
padding: 0;
|
|
115
|
+
border: none;
|
|
116
|
+
background: transparent;
|
|
117
|
+
color: $gray-600;
|
|
118
|
+
cursor: pointer;
|
|
119
|
+
transition: background-color $transition-fast, color $transition-fast;
|
|
120
|
+
flex-shrink: 0;
|
|
121
|
+
|
|
122
|
+
&:hover {
|
|
123
|
+
background-color: $gray-100;
|
|
124
|
+
color: $charcoal;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
&:focus-visible {
|
|
128
|
+
outline: 2px solid $auburn;
|
|
129
|
+
outline-offset: 2px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
svg {
|
|
133
|
+
width: 16px;
|
|
134
|
+
height: 16px;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/*******************************************************************************
|
|
139
|
+
* Dialog Description
|
|
140
|
+
******************************************************************************/
|
|
141
|
+
|
|
142
|
+
.bc-dialog__description {
|
|
143
|
+
font-family: "Financier Text Regular", Georgia, "Times New Roman", serif;
|
|
144
|
+
font-size: 0.875rem;
|
|
145
|
+
line-height: 1.5;
|
|
146
|
+
color: $gray-600;
|
|
147
|
+
margin: 0;
|
|
148
|
+
padding: $space-1 $space-3 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/*******************************************************************************
|
|
152
|
+
* Dialog Content
|
|
153
|
+
******************************************************************************/
|
|
154
|
+
|
|
155
|
+
.bc-dialog__content {
|
|
156
|
+
overflow-y: auto;
|
|
157
|
+
padding: $space-3;
|
|
158
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { useEffect, useRef, useCallback } from "react"
|
|
2
|
+
import cx from "classnames"
|
|
3
|
+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
|
4
|
+
import { faXmark } from "@fortawesome/free-solid-svg-icons"
|
|
5
|
+
|
|
6
|
+
export type DialogPosition =
|
|
7
|
+
| "top-left"
|
|
8
|
+
| "top-right"
|
|
9
|
+
| "bottom-left"
|
|
10
|
+
| "bottom-right"
|
|
11
|
+
| "center"
|
|
12
|
+
|
|
13
|
+
export interface DialogProps {
|
|
14
|
+
open: boolean
|
|
15
|
+
onClose: () => void
|
|
16
|
+
children: React.ReactNode
|
|
17
|
+
title?: string
|
|
18
|
+
description?: string
|
|
19
|
+
position?: DialogPosition
|
|
20
|
+
className?: string
|
|
21
|
+
closeOnEscape?: boolean
|
|
22
|
+
showCloseButton?: boolean
|
|
23
|
+
ariaLabelledBy?: string
|
|
24
|
+
ariaDescribedBy?: string
|
|
25
|
+
offset?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function Dialog({
|
|
29
|
+
open,
|
|
30
|
+
onClose,
|
|
31
|
+
children,
|
|
32
|
+
title,
|
|
33
|
+
description,
|
|
34
|
+
position = "bottom-right",
|
|
35
|
+
className,
|
|
36
|
+
closeOnEscape = true,
|
|
37
|
+
showCloseButton = true,
|
|
38
|
+
ariaLabelledBy,
|
|
39
|
+
ariaDescribedBy,
|
|
40
|
+
offset = 16,
|
|
41
|
+
}: DialogProps) {
|
|
42
|
+
const dialogRef = useRef<HTMLDivElement>(null)
|
|
43
|
+
|
|
44
|
+
const handleClose = useCallback(() => {
|
|
45
|
+
onClose()
|
|
46
|
+
}, [onClose])
|
|
47
|
+
|
|
48
|
+
// Handle escape key
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!closeOnEscape || !open) return
|
|
51
|
+
|
|
52
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
53
|
+
if (e.key === "Escape") {
|
|
54
|
+
handleClose()
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
document.addEventListener("keydown", handleKeyDown)
|
|
59
|
+
return () => document.removeEventListener("keydown", handleKeyDown)
|
|
60
|
+
}, [closeOnEscape, open, handleClose])
|
|
61
|
+
|
|
62
|
+
if (!open) return null
|
|
63
|
+
|
|
64
|
+
const classes = cx(
|
|
65
|
+
"bc-dialog",
|
|
66
|
+
`bc-dialog--${position}`,
|
|
67
|
+
className
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const titleId = ariaLabelledBy || (title ? "bc-dialog-title" : undefined)
|
|
71
|
+
const descriptionId = ariaDescribedBy || (description ? "bc-dialog-description" : undefined)
|
|
72
|
+
|
|
73
|
+
const style = {
|
|
74
|
+
"--bc-dialog-offset": `${offset}px`,
|
|
75
|
+
} as React.CSSProperties
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
ref={dialogRef}
|
|
80
|
+
className={classes}
|
|
81
|
+
style={style}
|
|
82
|
+
role="dialog"
|
|
83
|
+
aria-modal="false"
|
|
84
|
+
aria-labelledby={titleId}
|
|
85
|
+
aria-describedby={descriptionId}
|
|
86
|
+
>
|
|
87
|
+
<div className="bc-dialog__container">
|
|
88
|
+
{(title || showCloseButton) && (
|
|
89
|
+
<header className="bc-dialog__header">
|
|
90
|
+
{title && (
|
|
91
|
+
<h2 id={titleId} className="bc-dialog__title">
|
|
92
|
+
{title}
|
|
93
|
+
</h2>
|
|
94
|
+
)}
|
|
95
|
+
{showCloseButton && (
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
className="bc-dialog__close"
|
|
99
|
+
onClick={handleClose}
|
|
100
|
+
aria-label="Close dialog"
|
|
101
|
+
>
|
|
102
|
+
<FontAwesomeIcon icon={faXmark} />
|
|
103
|
+
</button>
|
|
104
|
+
)}
|
|
105
|
+
</header>
|
|
106
|
+
)}
|
|
107
|
+
{description && (
|
|
108
|
+
<p id={descriptionId} className="bc-dialog__description">
|
|
109
|
+
{description}
|
|
110
|
+
</p>
|
|
111
|
+
)}
|
|
112
|
+
<div className="bc-dialog__content">
|
|
113
|
+
{children}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default Dialog
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Dialog, type DialogProps, type DialogPosition } from "./Dialog"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
@use "../../styles/tokens" as *;
|
|
2
|
+
|
|
3
|
+
/*******************************************************************************
|
|
4
|
+
* PopupForm Component
|
|
5
|
+
******************************************************************************/
|
|
6
|
+
|
|
7
|
+
.bc-popup-form {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
gap: $space-3;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/*******************************************************************************
|
|
14
|
+
* PopupForm Fields
|
|
15
|
+
******************************************************************************/
|
|
16
|
+
|
|
17
|
+
.bc-popup-form__fields {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
gap: $space-3;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/*******************************************************************************
|
|
24
|
+
* PopupForm Actions
|
|
25
|
+
******************************************************************************/
|
|
26
|
+
|
|
27
|
+
.bc-popup-form__actions {
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-direction: row;
|
|
30
|
+
justify-content: flex-end;
|
|
31
|
+
gap: $space-2;
|
|
32
|
+
padding-top: $space-2;
|
|
33
|
+
border-top: 1px solid $border-muted;
|
|
34
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import cx from "classnames"
|
|
2
|
+
|
|
3
|
+
import { Dialog, type DialogPosition } from "../Dialog"
|
|
4
|
+
import { Button, type ButtonVariant } from "../../primitives/Button"
|
|
5
|
+
|
|
6
|
+
export interface PopupFormProps {
|
|
7
|
+
open: boolean
|
|
8
|
+
onClose: () => void
|
|
9
|
+
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void
|
|
10
|
+
children: React.ReactNode
|
|
11
|
+
title?: string
|
|
12
|
+
description?: string
|
|
13
|
+
position?: DialogPosition
|
|
14
|
+
offset?: number
|
|
15
|
+
className?: string
|
|
16
|
+
submitText?: string
|
|
17
|
+
submitVariant?: ButtonVariant
|
|
18
|
+
cancelText?: string
|
|
19
|
+
cancelVariant?: ButtonVariant
|
|
20
|
+
showCancel?: boolean
|
|
21
|
+
isSubmitting?: boolean
|
|
22
|
+
submitDisabled?: boolean
|
|
23
|
+
closeOnEscape?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function PopupForm({
|
|
27
|
+
open,
|
|
28
|
+
onClose,
|
|
29
|
+
onSubmit,
|
|
30
|
+
children,
|
|
31
|
+
title,
|
|
32
|
+
description,
|
|
33
|
+
position = "bottom-right",
|
|
34
|
+
offset,
|
|
35
|
+
className,
|
|
36
|
+
submitText = "Submit",
|
|
37
|
+
submitVariant = "solid-auburn",
|
|
38
|
+
cancelText = "Cancel",
|
|
39
|
+
cancelVariant = "outline-charcoal",
|
|
40
|
+
showCancel = true,
|
|
41
|
+
isSubmitting = false,
|
|
42
|
+
submitDisabled = false,
|
|
43
|
+
closeOnEscape = true,
|
|
44
|
+
}: PopupFormProps) {
|
|
45
|
+
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
46
|
+
e.preventDefault()
|
|
47
|
+
onSubmit(e)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const classes = cx("bc-popup-form", className)
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Dialog
|
|
54
|
+
open={open}
|
|
55
|
+
onClose={onClose}
|
|
56
|
+
title={title}
|
|
57
|
+
description={description}
|
|
58
|
+
position={position}
|
|
59
|
+
offset={offset}
|
|
60
|
+
closeOnEscape={closeOnEscape}
|
|
61
|
+
>
|
|
62
|
+
<form onSubmit={handleSubmit} className={classes}>
|
|
63
|
+
<div className="bc-popup-form__fields">
|
|
64
|
+
{children}
|
|
65
|
+
</div>
|
|
66
|
+
<div className="bc-popup-form__actions">
|
|
67
|
+
{showCancel && (
|
|
68
|
+
<Button
|
|
69
|
+
type="button"
|
|
70
|
+
text={cancelText}
|
|
71
|
+
variant={cancelVariant}
|
|
72
|
+
onClick={onClose}
|
|
73
|
+
disabled={isSubmitting}
|
|
74
|
+
icon={null}
|
|
75
|
+
/>
|
|
76
|
+
)}
|
|
77
|
+
<Button
|
|
78
|
+
type="submit"
|
|
79
|
+
text={isSubmitting ? "Submitting..." : submitText}
|
|
80
|
+
variant={submitVariant}
|
|
81
|
+
disabled={submitDisabled || isSubmitting}
|
|
82
|
+
icon={null}
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
</form>
|
|
86
|
+
</Dialog>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default PopupForm
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PopupForm, type PopupFormProps } from "./PopupForm"
|
package/src/index.ts
CHANGED
|
@@ -55,3 +55,7 @@ export { StatBlock, type StatBlockProps, type StatBlockSize, type StatBlockTrend
|
|
|
55
55
|
// Navigation
|
|
56
56
|
export { Header, type HeaderProps, type NavItem } from "./navigation/Header"
|
|
57
57
|
export { Footer, type FooterProps, type FooterLink, type SocialLink } from "./navigation/Footer"
|
|
58
|
+
|
|
59
|
+
// Feedback
|
|
60
|
+
export { Dialog, type DialogProps, type DialogPosition } from "./feedback/Dialog"
|
|
61
|
+
export { PopupForm, type PopupFormProps } from "./feedback/PopupForm"
|
package/src/styles/main.scss
CHANGED
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
@use "../content/StatBlock/StatBlock.scss";
|
|
23
23
|
@use "../navigation/Header/Header.scss";
|
|
24
24
|
@use "../navigation/Footer/Footer.scss";
|
|
25
|
+
@use "../feedback/Dialog/Dialog.scss";
|
|
26
|
+
@use "../feedback/PopupForm/PopupForm.scss";
|
|
25
27
|
|
|
26
28
|
/*******************************************************************************
|
|
27
29
|
* Base Styles
|