Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

- Introduced SDD structure for Services Page
- Updated SDD docs to include new Service page issues
- Added "What We Offer" section to the Services page

### Fixed

Expand Down
18 changes: 18 additions & 0 deletions components/icons/CloseIcon/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export default function CloseIcon(props) {
const { className, fill = 'var(--color-black)' } = props;

return (
<svg
width='24'
height='24'
viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'
className={className}
>
<path
d='M2.343 2.343a1 1 0 0 1 1.414 0L12 10.586l8.243-8.243a1 1 0 1 1 1.414 1.414L13.414 12l8.243 8.243a1 1 0 0 1-1.414 1.414L12 13.414l-8.243 8.243a1 1 0 0 1-1.414-1.414L10.586 12 2.343 3.757a1 1 0 0 1 0-1.414Z'
fill={fill}
/>
</svg>
);
}
28 changes: 28 additions & 0 deletions components/overlays/Dialog/Dialog.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@use '@/styles/index' as *;

.backdrop {
position: fixed;
inset: 0;
background-color: var(--overlay-backdrop);
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
z-index: 100;
}

.panel {
border-radius: 1rem;
max-width: 40rem;
width: 100%;
min-width: 0;
max-height: 90vh;
overflow-y: auto;
padding: 2rem;
}

.containedPanel {
border-radius: 1rem;
min-width: 0;
padding: 2rem;
}
110 changes: 110 additions & 0 deletions components/overlays/Dialog/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import styles from './Dialog.module.scss';

const FOCUSABLE_SELECTOR =
'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';

export default function Dialog({
open,
onClose,
titleId,
className,
contained = false,
children,
}) {
const [mounted, setMounted] = useState(false);
const panelRef = useRef(null);
const previouslyFocusedRef = useRef(null);

useEffect(() => {
setMounted(true);
}, []);

useEffect(() => {
if (!open) return undefined;

previouslyFocusedRef.current = document.activeElement;
const panel = panelRef.current;
const focusable = panel
? Array.from(panel.querySelectorAll(FOCUSABLE_SELECTOR))
: [];
(focusable[0] || panel)?.focus();

const handleKeyDown = event => {
if (event.key === 'Escape') {
event.preventDefault();
onClose();
return;
}

if (event.key !== 'Tab' || focusable.length === 0) return;

const first = focusable[0];
const last = focusable[focusable.length - 1];

if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
};

const handleOutsideClick = event => {
if (panel && !panel.contains(event.target)) {
onClose();
}
};

document.addEventListener('keydown', handleKeyDown);
if (contained) {
document.addEventListener('mousedown', handleOutsideClick);
}

return () => {
document.removeEventListener('keydown', handleKeyDown);
if (contained) {
document.removeEventListener('mousedown', handleOutsideClick);
}
previouslyFocusedRef.current?.focus();
};
}, [open, onClose, contained]);

if (!open) return null;

if (contained) {
return (
<div
className={`${styles.containedPanel}${className ? ` ${className}` : ''}`}
role='dialog'
aria-modal='true'
aria-labelledby={titleId}
tabIndex={-1}
ref={panelRef}
>
{children}
</div>
);
}

if (!mounted) return null;

return createPortal(
<div className={styles.backdrop} onClick={onClose}>
<div
className={`${styles.panel}${className ? ` ${className}` : ''}`}
role='dialog'
aria-modal='true'
aria-labelledby={titleId}
tabIndex={-1}
ref={panelRef}
onClick={event => event.stopPropagation()}
>
{children}
</div>
</div>,
document.body,
);
}
29 changes: 29 additions & 0 deletions components/services/Pill/Pill.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@use '@/styles/index' as *;

.pill {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
border: 2px solid var(--color-primary-accent);
background-color: var(--color-transparent);
color: var(--color-black);
font-family: $font-family-primary;
font-weight: $font-weight-regular;
white-space: nowrap;
cursor: pointer;
padding: 0.5rem 1rem;
font-size: $font-size-2xs;
line-height: 1.5rem;
@include transition(all 0.3s ease);

@include desktop {
padding: 0.75rem 2rem;
font-size: $font-size-sm;
line-height: 2rem;
}

&.secondary {
border-color: var(--color-secondary-accent);
}
}
22 changes: 22 additions & 0 deletions components/services/Pill/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import styles from './Pill.module.scss';
import { combineClasses } from '@/utils/classnames';

export default function Pill({
label,
variant = 'primary',
onClick,
tabIndex,
}) {
const pillClass = combineClasses(styles.pill, variant, styles);

return (
<button
type='button'
className={pillClass}
onClick={onClick}
tabIndex={tabIndex}
>
{label}
</button>
);
}
105 changes: 105 additions & 0 deletions components/services/ServiceDialog/ServiceDialog.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
@use '@/styles/index' as *;

.panel {
position: absolute;
min-height: 100%;
z-index: 1;
display: flex;
flex-direction: column;
gap: 1.5rem;

@include mobile {
top: -0.5rem;
right: -2rem;
bottom: -4rem;
left: -2rem;
}

@include tablet {
top: -1rem;
right: -2rem;
left: -2rem;
}

@include desktop {
top: -1rem;
right: -2rem;
bottom: -2rem;
left: -2rem;
}

&.primary {
background-color: var(--color-primary-accent);
}

&.secondary {
background-color: var(--color-secondary-surface);
}
}

.header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
min-width: 0;
}

.echoPill {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
background-color: var(--color-black);
color: var(--color-white);
font-family: $font-family-primary;
font-weight: $font-weight-regular;
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
line-height: 1rem;
white-space: nowrap;
flex-shrink: 0;

@include desktop {
padding: 0.5rem 1.25rem;
font-size: 1rem;
line-height: 1.5rem;
}
}

.closeButton {
display: flex;
align-items: center;
justify-content: center;
width: 3.5rem;
height: 3.5rem;
border: none;
border-radius: 0.25rem;
background-color: var(--color-transparent);
cursor: pointer;
flex-shrink: 0;
@include transition(all 0.3s ease);

&:hover {
background-color: var(--color-box-shadow);
}
}

.closeIcon {
width: 2rem;
height: 2rem;
}

.description {
font-family: $font-family-primary;
font-size: $font-size-2xs;
line-height: 1.5rem;
color: var(--color-primary-content);
word-break: break-word;
margin: 0;

@include desktop {
font-size: $font-size-sm;
line-height: 2rem;
}
}
40 changes: 40 additions & 0 deletions components/services/ServiceDialog/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Dialog from '@/components/overlays/Dialog';
import CloseIcon from '@/components/icons/CloseIcon';
import { combineClasses } from '@/utils/classnames';
import styles from './ServiceDialog.module.scss';

export default function ServiceDialog({
open,
onClose,
titleId,
label,
description,
variant = 'primary',
}) {
const panelClass = combineClasses(styles.panel, variant, styles);

return (
<Dialog
open={open}
onClose={onClose}
titleId={titleId}
className={panelClass}
contained
>
<div className={styles.header}>
<span id={titleId} className={styles.echoPill}>
{label}
</span>
<button
type='button'
className={styles.closeButton}
onClick={onClose}
aria-label='Close'
>
<CloseIcon className={styles.closeIcon} />
</button>
</div>
<p className={styles.description}>{description}</p>
</Dialog>
);
}
Loading