Skip to content

Commit

Permalink
Merge pull request #94 from element-hq/germain-gg/nav-bar
Browse files Browse the repository at this point in the history
Create Nav component
  • Loading branch information
MidhunSureshR authored Jul 2, 2024
2 parents f4166b8 + 9466205 commit 6142640
Show file tree
Hide file tree
Showing 21 changed files with 914 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
"stylelint-plugin-defensive-css": "^0.9.1",
"stylelint-use-logical": "^2.1.0",
"stylelint-value-no-unknown-custom-properties": "^4.0.0",
"ts-xor": "^1.3.0",
"typescript": "^5.2.2",
"vite": "^5.2.11",
"vite-plugin-dts": "^3.5.3",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
99 changes: 99 additions & 0 deletions src/components/Nav/Nav.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/* Copyright 2023 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

.nav-bar {
border-block-end: var(--cpd-border-width-1) solid var(--cpd-color-gray-400);
margin: var(--cpd-space-6x) 0;
padding: 0;
}

.nav-bar-items {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--cpd-space-3x);
list-style: none;
padding: 0;
margin: 0;
}

.nav-tab {
padding: var(--cpd-space-4x) 0;
position: relative;
}

/* Underline effect */
.nav-tab::before {
content: "";
position: absolute;
inset-block-end: 0;
inset-inline: 0;
block-size: 0;
border-radius: var(--cpd-radius-pill-effect) var(--cpd-radius-pill-effect) 0 0;
background-color: var(--cpd-color-bg-action-primary-rest);
transition: height 0.1s ease-in-out;
}

.nav-tab[data-current]::before {
/* This is not exactly right: designs says 3px, but there are no variables for that */
block-size: var(--cpd-border-width-4);
}

.nav-item {
padding-block: var(--cpd-space-1x);
padding-inline: var(--cpd-space-2x);
border-radius: var(--cpd-radius-pill-effect);
cursor: pointer;
appearance: none;
display: flex;
align-items: center;
justify-content: center;
gap: var(--cpd-space-2x);
box-sizing: border-box;
background: transparent;
border: 0;
font: var(--cpd-font-body-md-regular);
color: var(--cpd-color-text-secondary);
text-decoration: none;
}

@media (hover) {
.nav-item:not([disabled]):hover {
color: var(--cpd-color-text-primary);
background-color: var(--cpd-color-bg-subtle-secondary);
}
}

.nav-item:focus-visible {
outline: var(--cpd-color-border-focused) var(--cpd-border-width-2) solid;
}

.nav-item:not([disabled]):active {
color: var(--cpd-color-text-primary);
background-color: var(--cpd-color-bg-subtle-primary);
}

.nav-item[aria-current] {
color: var(--cpd-color-text-primary);
}

.nav-item[disabled] {
cursor: not-allowed;

/* Enable pointer events for svgs even with fill=none */
pointer-events: all !important;
color: var(--cpd-color-text-disabled);
}
105 changes: 105 additions & 0 deletions src/components/Nav/NavBar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright 2022 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import React, { useEffect, useState } from "react";
import { NavBar, NavItem } from ".";

export default {
title: "Nav",
component: NavBar,
tags: ["autodocs"],
parameters: {
controls: {
include: ["aria-label"],
},
design: {
type: "figma",
url: "https://www.figma.com/file/rTaQE2nIUSLav4Tg3nozq7/Compound-Web-Components?type=design&node-id=669-2723&mode=design&t=9Hy0h7BBDH0kJ2Ow-0",
},
},
args: {
"aria-label": "Main",
},
};

export const Default = {
args: {
children: (
<>
<NavItem onClick={() => {}}>Info</NavItem>
<NavItem onClick={() => {}} active>
People
</NavItem>
<NavItem onClick={() => {}} disabled>
Disabled
</NavItem>
<NavItem href="https://example.com">External</NavItem>
</>
),
},
};

export const TabRole = {
render: function Component() {
// An example tab implementation
const [activePanelId, setActivePanelId] = useState("panel-2");
const changeDisplay = (id: string, display: string) => {
const e = document.querySelector(`#${id}`) as HTMLDivElement;
if (e) e.style.display = display;
};
useEffect(() => {
["panel-1", "panel-2"].forEach((id) => {
changeDisplay(id, "none");
});
changeDisplay(activePanelId, "block");
}, [activePanelId]);

return (
<div>
<NavBar role="tablist" aria-label="main">
<NavItem
aria-controls="panel-1"
onClick={() => {
setActivePanelId("panel-1");
}}
active={activePanelId === "panel-1"}
>
Tab 1
</NavItem>
<NavItem
aria-controls="panel-2"
onClick={() => {
setActivePanelId("panel-2");
}}
active={activePanelId === "panel-2"}
>
Tab 2
</NavItem>
<NavItem aria-controls="panel-3" onClick={() => {}} disabled>
Disabled Tab
</NavItem>
</NavBar>
<div id="panel-1" style={{ display: "none" }}>
This is panel 1
</div>
<div id="panel-2" style={{ display: "none" }}>
This is panel 2
</div>
<div id="panel-3" style={{ display: "none" }}>
This is panel 3
</div>
</div>
);
},
};
36 changes: 36 additions & 0 deletions src/components/Nav/NavBar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { describe, it, expect } from "vitest";
import { render } from "@testing-library/react";
import React from "react";
import { composeStories } from "@storybook/react";

import * as stories from "./NavBar.stories";

const { Default, TabRole } = composeStories(stories);

describe("<NavBar />", () => {
it("render a default nav bar", () => {
const { container } = render(<Default />);
expect(container).toMatchSnapshot();
});

it("render a tabbed nav bar", () => {
const { container } = render(<TabRole />);
expect(container).toMatchSnapshot();
});
});
87 changes: 87 additions & 0 deletions src/components/Nav/NavBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import React from "react";
import classNames from "classnames";

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

type NavBarProps = {
/**
* The CSS class name
*/
className?: string;

/**
* Require a label for navigation landmarks
*/
"aria-label": string;

/**
* Accessibility role that defaults to navigation.
*
* If you pass tablist you must also pass `aria-controls` as prop to your NavItem and must
* ensure that the conditions stated in https://www.w3.org/WAI/ARIA/apg/patterns/tabs/#wai-ariaroles,states,andproperties
* are met.
*/
role?: "navigation" | "tablist";
};

/**
* A navigation bar component
*/
export const NavBar = ({
children,
className,
role,
"aria-label": ariaLabel,
...rest
}: React.PropsWithChildren<NavBarProps>) => {
const classes = classNames(className, styles["nav-bar"]);
/**
* We sometimes want to use this NavBar for tabs.
* This is done by passing `role=tablist` to this component.
* By default, this component is used as a navigation bar.
*
* Depending on this role, a different set of accessibility
* attributes need to be applied to the nav/ul element.
*/
const a11yAttributesForNav =
role !== "tablist"
? /**
* If role isn't tablist, default to navigation.
*/
{ role: "navigation", "aria-label": ariaLabel }
: /**
* If role is tablist, give nav presentation role to remove
* any semantic meaning.
*/
{ role: "presentation" };

/**
* When used as tabs, the tablist role must be applied to ul.
* When used as navigation, no special accessibility attribute
* is needed for the ul element.
*/
const a11yAttributesForUl =
role === "tablist" ? { role: "tablist", "aria-label": ariaLabel } : {};

return (
<nav {...a11yAttributesForNav} {...rest} className={classes}>
<ul {...a11yAttributesForUl} className={styles["nav-bar-items"]}>
{children}
</ul>
</nav>
);
};
Loading

0 comments on commit 6142640

Please sign in to comment.