Skip to content

Commit

Permalink
InboxItems + InboxItem (#195)
Browse files Browse the repository at this point in the history
* feature: add skeleton for inboxitems with example story in storybook (WIP)

* feature: add InboxItems and InboxItem component

* feature: add stories for InboxItem(s) and resolve fonts

* remove: unused Button example no longer needed

* fixes: linting errors for story

* feature: add modifier isUnread for InboxItem

* add jsdoc for InboxItem and InboxItems

* fixes: label for sender should be in bold

* improvement: better example use of icons in tags

* fixes: move jsdoc def to top of component signature

---------

Co-authored-by: Sean Scully <seanscully@desktop-b76mhj2.guest.tul.entra.no>
Co-authored-by: Sean Scully <seanscully@Sean-sin-MacBook-Pro.local>
  • Loading branch information
3 people authored Feb 9, 2024
1 parent 7d1f05b commit d6db695
Show file tree
Hide file tree
Showing 27 changed files with 475 additions and 152 deletions.
2 changes: 2 additions & 0 deletions packages/frontend-design-poc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"dependencies": {
"@digdir/design-system-react": "^0.47.0",
"@digdir/design-system-tokens": "^0.12.0",
"@navikt/aksel-icons": "^5.18.0",
"classnames": "^2.5.1",
"i18next": "^23.8.2",
"i18next-icu": "^2.3.0",
"react": "^18.2.0",
Expand Down
7 changes: 5 additions & 2 deletions packages/frontend-design-poc/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@ import { Route, Routes } from "react-router-dom";
import { HelloWorld } from "./components/HelloWorld";
import { PageNotFound } from "./pages/PageNotFound";
import { PageLayout } from "./pages/PageLayout";
import { Inbox } from "./pages/Inbox";

import styles from "./app.module.css";

function App() {
return (
<div className={styles.app} role="main">
<main className={styles.app}>
<Routes>
<Route element={<PageLayout />}>
<Route path="/" element={<HelloWorld />} />
<Route path="/inbox" element={<Inbox />} />
<Route path="*" element={<PageNotFound />} />
</Route>
</Routes>
</div>
</main>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ export const HelloWorld = () => {
const { isLoading, data } = useQuery("user", getUser);
const { t } = useTranslation();
return (
<section className={styles.helloWorld}>
{isLoading ? (
<span>Loading ...</span>
) : (
<h1>{t("example.hello", { person: data?.name })}!</h1>
)}
</section>
<>
<section className={styles.helloWorld}>
{isLoading ? (
<span>Loading ...</span>
) : (
<h1>{t("example.hello", { person: data?.name })}!</h1>
)}
</section>
</>
);
};
120 changes: 120 additions & 0 deletions packages/frontend-design-poc/src/components/InboxItem/InboxItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { Checkbox } from "@digdir/design-system-react";
import classNames from "classnames";

import styles from "./inboxItem.module.css";

interface Participant {
label: string;
icon?: JSX.Element;
}

interface InboxItemTag {
label: string;
icon?: JSX.Element;
className?: string;
}

interface InboxItemProps {
checkboxValue: string;
title: string;
toLabel: string;
description: string;
sender: Participant;
receiver: Participant;
isChecked: boolean;
onCheckedChange: (value: boolean) => void;
tags?: InboxItemTag[];
isUnread?: boolean;
}

/**
* Represents an individual inbox item, displaying information such as the title,
* description, sender, and receiver, along with optional tags. It includes a checkbox
* to mark the item as checked/unchecked and can visually indicate if it is unread.
* Should only be used as child of InboxItems
*
* @component
* @param {Object} props - The properties passed to the component.
* @param {string} props.checkboxValue - The value attribute for the checkbox input.
* @param {string} props.title - The title of the inbox item.
* @param {string} props.toLabel - The label for "to" for full i18n support.
* @param {string} props.description - The description or content of the inbox item.
* @param {Participant} props.sender - The sender of the inbox item, including label and optional icon.
* @param {Participant} props.receiver - The receiver of the inbox item, including label and optional icon.
* @param {boolean} props.isChecked - Whether the inbox item is checked. This can support batch operations.
* @param {function(boolean): void} props.onCheckedChange - Callback function triggered when the checkbox value changes.
* @param {InboxItemTag[]} [props.tags=[]] - Optional array of tags associated with the inbox item, each with a label, optional icon, and optional className.
* @param {boolean} [props.isUnread=false] - Whether the inbox item should be styled to indicate it is unread.
* @returns {JSX.Element} The InboxItem component.
*
* @example
* <InboxItem
* checkboxValue="item1"
* title="Meeting Reminder"
* toLabel="to"
* description="Don't forget the meeting tomorrow at 10am."
* sender={{ label: "Alice", icon: <MailIcon /> }}
* receiver={{ label: "Bob", icon: <PersonIcon /> }}
* isChecked={false}
* onCheckedChange={(checked) => console.log(checked)}
* tags={[{ label: "Urgent", icon: <WarningIcon />, className: "urgent" }]}
* isUnread
* />
*/
export const InboxItem = ({
title,
description,
sender,
receiver,
toLabel,
tags = [],
isChecked,
onCheckedChange,
checkboxValue,
isUnread = false,
}: InboxItemProps) => {
return (
<div
className={classNames(styles.inboxItemWrapper, {
[styles.active]: isChecked,
[styles.isUnread]: isUnread,
})}
aria-selected={isChecked ? "true" : "false"}
tabIndex={0}
>
<section className={styles.inboxItem}>
<header className={styles.header}>
<h2 className={styles.title}>{title}</h2>
<Checkbox
checked={isChecked}
value={checkboxValue}
onChange={(e) => onCheckedChange(e.target.checked)}
size="small"
/>
</header>
<div className={styles.participants}>
<div className={styles.sender}>
{sender.icon && <div className={styles.icon}>{sender.icon}</div>}
<span>{sender.label}</span>
</div>
<span>{toLabel}</span>
<div className={styles.receiver}>
{receiver.icon && (
<div className={styles.icon}>{receiver.icon}</div>
)}
<span>{receiver.label}</span>
</div>
</div>
<p className={styles.description}>{description}</p>
<div className={styles.tags}>
{tags.map((tag) => (
<div key={tag.label} className={styles.tag}>
{tag.icon && <div className={styles.icon}>{tag.icon}</div>}
<span> {tag.label}</span>
</div>
))}
</div>
</section>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
.inboxItemWrapper {
display: flex;
flex-direction: column;
width: 100%;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.06), 0 1px 3px 0 rgba(0, 0, 0, 0.10);
}

.active {
outline: 2px solid #00315D;
background: #EFF5FD;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}

.inboxItem {
display: flex;
flex-direction: column;
padding: 0 1.0625em 1.0625em;
border-left: 4px solid rgba(0, 0, 0, 0.10);
}

.isUnread {
border-left: 4px solid #118849;
}

.header {
display: flex;
justify-content: space-between;
align-items: center;
}

.title {
color: #000;
font-size: 1.25rem;
font-weight: 500;
line-height: 1.25;
padding-bottom: 0.375em;
}

.participants {
display: flex;
align-items: center;
column-gap: 5px;
}

.receiver, .sender {
display: flex;
align-items: center;
}

.sender {
font-weight: 500;
}

.icon {
margin-right: 0.375em;
height: 18px;
width: 18px;
flex-shrink: 0;
}

.description {
color: #000;
font-size: 1rem;
font-weight: 400;
}


.tags {
display: flex;
flex-direction: row;
align-items: center;
}

.tag {
display: flex;
align-items: center;
margin-right: 1em;
font-weight: 400;
}


Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { InboxItem } from "./InboxItem.tsx";
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import styles from "./inboxItems.module.css";

export interface InboxItemsProps {
children: React.ReactNode;
}

/**
* A container component for displaying a list of inbox items. It serves as a wrapper
* for individual `InboxItem` components, ensuring they are styled and organized collectively.
* This component primarily handles the layout of its children inbox items.
*
* @component
* @param {Object} props - The properties passed to the component.
* @param {React.ReactNode} props.children - The child components to be rendered within the container.
* Typically, these are `InboxItem` components, but can include any React nodes.
* @returns {JSX.Element} A div element wrapping the children in a styled manner,
* according to the `inboxItems` CSS class defined in `inboxItems.module.css`.
*
* @example
* <InboxItems>
* <InboxItem {...inboxItemProps1} />
* <InboxItem {...inboxItemProps2} />
* <InboxItem {...inboxItemProps3} />
* </InboxItems>
*/

export const InboxItems = ({ children }: InboxItemsProps) => {
return <div className={styles.inboxItems}>{children}</div>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.inboxItems {
display: flex;
flex-direction: column;
row-gap: .5em;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { InboxItems } from "./InboxItems.tsx";
4 changes: 3 additions & 1 deletion packages/frontend-design-poc/src/i18n/resources/nb.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{
"example.hello": "Hei, {person}"
"example.hello": "Hei, {person}",
"example.your_inbox": "Din innboks",
"word.to": "til"
}
1 change: 0 additions & 1 deletion packages/frontend-design-poc/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { BrowserRouter } from "react-router-dom";
import "./i18n/";

import App from "./App.tsx";

async function enableMocking() {
if (import.meta.env.MODE === "development") {
const { worker } = await import("./mocks/browser");
Expand Down
51 changes: 51 additions & 0 deletions packages/frontend-design-poc/src/pages/Inbox/Inbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { PersonIcon, PersonSuitIcon, SealIcon, StarIcon } from '@navikt/aksel-icons';
import { InboxItems } from "../../components/InboxItems";
import { InboxItem } from "../../components/InboxItem";

export const Inbox = () => {
const { t } = useTranslation();
const [isChecked, setIsChecked] = useState<boolean>(false);
const [isChecked2, setIsChecked2] = useState<boolean>(false);

return (
<article>
<h1>{t('example.your_inbox')}</h1>
<InboxItems>
<InboxItem
checkboxValue="test"
title="Viktig melding"
toLabel={t("word.to")}
description="Du har mottatt en viktig melding!"
sender={{ label: "Viktig bedrift", icon: <PersonSuitIcon /> }}
receiver={{ label: "Bruker Brukerson", icon: <PersonIcon /> }}
isChecked={isChecked}
onCheckedChange={(checked) => {
setIsChecked(checked);
}}
tags={[
{ label: "hello", icon: <StarIcon /> },
{ label: "hallaz", icon: <SealIcon /> },
]}
/>
<InboxItem
checkboxValue="test2"
title="Har du glemt oss?"
toLabel={t("word.to")}
description="Det tror jeg du har!"
sender={{ label: "Viktig bedrift", icon: <PersonSuitIcon /> }}
receiver={{ label: "Bruker Brukerson", icon: <PersonIcon /> }}
isChecked={isChecked2}
onCheckedChange={(checked) => {
setIsChecked2(checked);
}}
tags={[
{ label: "hello", icon: <StarIcon /> },
{ label: "hallaz", icon: <SealIcon /> },
]}
/>
</InboxItems>
</article>
);
};
1 change: 1 addition & 0 deletions packages/frontend-design-poc/src/pages/Inbox/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Inbox } from './Inbox.tsx'
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Outlet } from "react-router-dom";

import styles from "./pageLayout.module.css";

export const PageLayout = () => {
return (
<>
<div className={styles.pageLayout}>
<nav aria-label="hovedmeny" />
<Outlet />
</>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.pageLayout {
max-width: 1280px;
}
Loading

0 comments on commit d6db695

Please sign in to comment.