Skip to content

Commit

Permalink
#19 - feat: Implemented the Tabs component
Browse files Browse the repository at this point in the history
  • Loading branch information
Xaohs authored and Julian Roeland committed Feb 2, 2024
1 parent d1ddabd commit 7c580e9
Show file tree
Hide file tree
Showing 9 changed files with 257 additions and 5 deletions.
19 changes: 18 additions & 1 deletion src/components/button/button.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.mykn-button {
$self: &;
--mykn-button-color-background: var(--theme-color-primary-800);
--mykn-button-color-shadow: var(--theme-shade-1000);
--mykn-button-color-text: var(--theme-shade-0);
Expand Down Expand Up @@ -47,9 +48,14 @@
}

&--variant-primary {
&#{$self}--active, // TODO
&:focus,
&:hover {
--mykn-button-color-background: var(--theme-color-primary-700);
}

&:focus,
&:hover {
--mykn-button-offset: -2px;
}

Expand All @@ -66,9 +72,14 @@
--mykn-button-color-shadow: currentColor;
--mykn-button-color-text: var(--typography-color-body);

&#{$self}--active,
&:focus,
&:hover {
--mykn-button-color-background: var(--theme-color-primary-200);
}

&:focus,
&:hover {
--mykn-button-offset: -2px;
}

Expand All @@ -84,9 +95,15 @@
--mykn-button-color-shadow: currentColor;
--mykn-button-color-text: var(--theme-shade-700);

&#{$self}--active,
&:focus,
&:hover {
--mykn-button-color-background: var(--theme-color-primary-100);
--mykn-button-color-text: var(--theme-color-primary-800);
}

&:focus,
&:hover {
--mykn-button-color-background: var(--theme-color-primary-200);
--mykn-button-offset: -2px;
}

Expand Down
16 changes: 12 additions & 4 deletions src/components/button/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React, { LegacyRef } from "react";
import "./button.scss";

type BaseButtonProps = {
active?: boolean;
square?: boolean;
variant?: "primary" | "outline" | "transparent";
};
Expand All @@ -16,16 +17,19 @@ export type ButtonLinkProps = React.AnchorHTMLAttributes<HTMLAnchorElement> &

/**
* Button component
* @param active
* @param variant
* @param square
* @param props
* @constructor
*/
export const Button = React.forwardRef<HTMLAnchorElement, ButtonProps>(
({ square = false, variant = "primary", ...props }, ref) => {
({ active, square = false, variant = "primary", ...props }, ref) => {
return (
<button
ref={ref as LegacyRef<HTMLButtonElement>}
className={clsx("mykn-button", `mykn-button--variant-${variant}`, {
"mykn-button--active": active,
"mykn-button--square": square,
})}
{...props}
Expand All @@ -39,16 +43,20 @@ Button.displayName = "Button";

/**
* Button component
* @param active
* @param square
* @param variant
* @param props
* @constructor
*/
export const ButtonLink = React.forwardRef<HTMLAnchorElement, ButtonLinkProps>(
({ variant, ...props }, ref) => {
({ active, square, variant, ...props }, ref) => {
return (
<a
ref={ref as LegacyRef<HTMLAnchorElement>}
className={clsx("mykn-button", `mykn-button--variant-${variant}`)}
className={clsx("mykn-button", `mykn-button--variant-${variant}`, {
"mykn-button--active": active,
"mykn-button--square": square,
})}
{...props}
>
{props.children}
Expand Down
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ export * from "./logo";
export * from "./navbar";
export * from "./page";
export * from "./paginator";
export * from "./tabs";
export * from "./toolbar";
export * from "./typography";
1 change: 1 addition & 0 deletions src/components/tabs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./tabs";
35 changes: 35 additions & 0 deletions src/components/tabs/tabs.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@use "../../settings/constants";

.mykn-tabs {
&__tablist {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--spacing-v-l);
box-sizing: border-box;
border: 1px solid var(--theme-shade-100);
border-radius: var(--border-radus-s);
background-color: var(--theme-shade-50);
padding: var(--spacing-h-m) var(--spacing-v-xxxl);
width: 100%;
}

.mykn-p {
color: var(--theme-shade-200);
user-select: none;
}

&__content {
margin-top: var(--spacing-h-l);
}

@media screen and (max-width: constants.$breakpoint-desktop) {
&__tablist {
justify-content: center;
}

.mykn-p {
display: none;
}
}
}
78 changes: 78 additions & 0 deletions src/components/tabs/tabs.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Meta, StoryObj } from "@storybook/react";
import React from "react";

import { Card } from "../card";
import { Page } from "../page";
import { Tab, Tabs } from "./tabs";

const meta: Meta<typeof Tabs> = {
title: "Controls/Tabs",
component: Tabs,
decorators: [
(Story) => (
<Page>
<Story />
</Page>
),
],
};

export default meta;

export const TabsComponent: StoryObj<typeof meta> = {
render: (args) => (
<Tabs {...args}>
<Tab label="Structuur">Content for tab one</Tab>
<Tab label="Basis">Tab 2 content</Tab>
<Tab label="Statussen">Tab 3 content</Tab>
<Tab label="Documenten">Tab 4 content</Tab>
<Tab label="Rollen">Tab 5 content</Tab>
<Tab label="Eigenschappen">Tab 6 content</Tab>
</Tabs>
),
};

export const ExplicitId: StoryObj<typeof meta> = {
render: (args) => (
<Tabs {...args}>
<Tab id="structuur" label="Structuur">
Content for tab one
</Tab>
<Tab id="informatie" label="Basis informatie">
Tab 2 content
</Tab>
<Tab id="statussen" label="Statussen">
Tab 3 content
</Tab>
<Tab id="documenten" label="Documenten">
Tab 4 content
</Tab>
<Tab id="rollen" label="Rollen">
Tab 5 content
</Tab>
<Tab id="eigenschappen" label="Eigenschappen">
Tab 6 content
</Tab>
</Tabs>
),
};

export const TabsComponentInCard: StoryObj<typeof meta> = {
decorators: [
(Story) => (
<Card>
<Story />
</Card>
),
],
render: (args) => (
<Tabs {...args}>
<Tab label="Structuur">Content for tab one</Tab>
<Tab label="Basis">Tab 2 content</Tab>
<Tab label="Statussen">Tab 3 content</Tab>
<Tab label="Documenten">Tab 4 content</Tab>
<Tab label="Rollen">Tab 5 content</Tab>
<Tab label="Eigenschappen">Tab 6 content</Tab>
</Tabs>
),
};
88 changes: 88 additions & 0 deletions src/components/tabs/tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import clsx from "clsx";
import React, { Children, ReactElement, isValidElement, useState } from "react";

import { slugify } from "../../lib/format/string";
import { Button } from "../button";
import { P } from "../typography";
import "./tabs.scss";

export type TabsProps = React.PropsWithChildren<{
onTabChange?: (activeTabIndex: number) => void;
}>;

/**
* A tabs component meant to allow for cycling through different content sections. Allows for passing custom react nodes as content for each tab.
* @param tabs
* @param onTabChange
* @param props
* @constructor
*/
export const Tabs: React.FC<TabsProps> = ({
onTabChange,
children,
...props
}) => {
const [activeTab, setActiveTab] = useState(0);

const handleTabClick = (index: number) => {
setActiveTab(index);
if (onTabChange) {
onTabChange(index);
}
};

const tabs = Children.toArray(children).filter(
(child) => isValidElement(child) && child.type === Tab,
) as ReactElement[];

return (
<div className={clsx("mykn-tabs")} {...props}>
<nav className="mykn-tabs__tablist" role="tablist">
{tabs.map((tab, index) => (
<React.Fragment key={tab.props.id || slugify(tab.props.label)}>
<Button
active={activeTab === index}
id={`tab-${tab.props.id || slugify(tab.props.label)}`}
role="tab"
variant="transparent"
aria-controls={`tab-content-${tab.props.id || slugify(tab.props.label)}`}
aria-selected={activeTab === index ? "true" : "false"}
onClick={() => handleTabClick(index)}
>
{tab.props.label}
</Button>
{index < tabs.length - 1 && <P aria-hidden="true">|</P>}
</React.Fragment>
))}
</nav>
{tabs.map((tab, index) => (
<div
key={tab.props.id}
id={`tab-content-${tab.props.id || slugify(tab.props.label)}`}
role="tabpanel"
aria-labelledby={`tab-${tab.props.id || slugify(tab.props.label)}`}
hidden={activeTab !== index}
className="mykn-tabs__content"
>
{tab.props.children}
</div>
))}
</div>
);
};

export type TabProps = React.PropsWithChildren<{
label: string;
id?: string;
}>;

/**
* A tab component meant to be used as a child of the Tabs component. It is used to define the label and content of a tab.
* @param label - The label of the tab
* @param id - The id of the tab
* @param children - The content of the tab
*/
export const Tab: React.FC<TabProps> = ({ children }) => {
// Just a placeholder, as actual rendering does not happen here
return <>{children}</>;
};
12 changes: 12 additions & 0 deletions src/lib/format/string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Converts "Some object name" to "some-object-name".
* @param input
*/
export const slugify = (input: string): string => {
return input
.toLowerCase() // Convert to lowercase
.replace(/\s+/g, "-") // Replace spaces with hyphens
.replace(/[^\w-]+/g, "") // Remove non-word characters (excluding hyphens)
.replace(/--+/g, "-") // Replace multiple hyphens with a single hyphen
.replace(/^-+|-+$/g, ""); // Remove leading and trailing hyphens
};
12 changes: 12 additions & 0 deletions src/settings/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
--theme-color-primary-600: #8d75e6;
--theme-color-primary-400: #bdb0ed;
--theme-color-primary-200: #ecf1ff;
--theme-color-primary-100: #f2eeff;

--theme-color-tertiary-800: #0f172a;
--theme-color-tertiary-700: #1e293b;
Expand All @@ -19,9 +20,13 @@

--theme-shade-1000: #000;
--theme-shade-900: #272727;
--theme-shade-800: #4b4b4b;
--theme-shade-700: #545454;
--theme-shade-400: #b3b3b3;
--theme-shade-300: #eaeaea;
--theme-shade-200: #e6e6e6;
--theme-shade-100: #f1f1f1;
--theme-shade-50: #fcfcfc;
--theme-shade-0: #fff;

--theme-color-success-body: #0b4f00;
Expand All @@ -33,6 +38,7 @@
/* SPACING */
--border-radus-l: 12px;
--border-radus-m: 6px;
--border-radus-s: 4px;

--gutter-h-desktop: 12px;
--gutter-v-desktop: 24px;
Expand All @@ -46,12 +52,18 @@
--spacing-h-m: 8px;
--spacing-v-m: 8px;

--spacing-h-l: 16px;
--spacing-v-l: 16px;

--spacing-h-xl: 20px;
--spacing-v-xl: 50px;

--spacing-h-xxl: 32px;
--spacing-v-xxl: 32px;

--spacing-h-xxxl: 40px;
--spacing-v-xxxl: 40px;

/* TYPOGRAPHY */
--typography-color-background: var(--theme-shade-0);
--typography-color-h: var(--theme-shade-900);
Expand Down

0 comments on commit 7c580e9

Please sign in to comment.