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
135 changes: 135 additions & 0 deletions src/components/SegmentedControl/SegmentedControl.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import React from "react";

import { Meta, StoryObj } from "@storybook/react";
import SegmentedControl from "./SegmentedControl";

const meta: Meta<typeof SegmentedControl> = {
component: SegmentedControl,
tags: ["autodocs"],
};

export default meta;

type Story = StoryObj<typeof SegmentedControl>;

export const Default: Story = {
name: "Default",

args: {
segments: [
{
label: "OLM",
segmentContent: (
<p>
A system to help you move from configuration management to
application management across your hybrid cloud estate - through
sharable, reusable, tiny applications called Charmed Operators.
</p>
),
},
{
label: "SDK",
segmentContent: (
<p>
A set of tools to help you write Charmed Operators and to package
them as Charms.
</p>
),
},
{
label: "Charmhub",
segmentContent: (
<p>
A repository for charms - from Observability to Data to Identity and
more.
</p>
),
},
],
},
};

/**
* The buttons take up a more compact apperance by adding the modifier is-dense
*/

export const Dense: Story = {
name: "Dense",

args: {
className: "is-dense",
segments: [
{
label: "OLM",
segmentContent: (
<p>
A system to help you move from configuration management to
application management across your hybrid cloud estate - through
sharable, reusable, tiny applications called Charmed Operators.
</p>
),
},
{
label: "SDK",
segmentContent: (
<p>
A set of tools to help you write Charmed Operators and to package
them as Charms.
</p>
),
},
{
label: "Charmhub",
segmentContent: (
<p>
A repository for charms - from Observability to Data to Identity and
more.
</p>
),
},
],
},
};
/**
* The pattern also supports the use of icons within each button.
*
* Icons : If any icons are used, all buttons within the pattern should have an icon, to avoid any potential confusion that could arise from a mix of buttons with and without an icon.
*/
export const WithIcon: Story = {
name: "With Icon",
args: {
segments: [
{
label: "OLM",
segmentContent: (
<p>
A system to help you move from configuration management to
application management across your hybrid cloud estate - through
sharable, reusable, tiny applications called Charmed Operators.
</p>
),
segmentIcon: <i className="p-icon--information"></i>,
},
{
label: "SDK",
segmentContent: (
<p>
A set of tools to help you write Charmed Operators and to package
them as Charms.
</p>
),
segmentIcon: <i className="p-icon--information"></i>,
},
{
label: "Charmhub",
segmentContent: (
<p>
A repository for charms - from Observability to Data to Identity and
more.
</p>
),
segmentIcon: <i className="p-icon--information"></i>,
},
],
},
};
60 changes: 60 additions & 0 deletions src/components/SegmentedControl/SegmentedControl.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import SegmentedControl from "./SegmentedControl";

describe("SegmentedControl", () => {
it("renders", () => {
const { container } = render(
<SegmentedControl
segments={[
{
label: "label1",
segmentContent: <p>content1</p>,
},
]}
/>,
);
expect(container).toMatchSnapshot();
});

it("can set className correctly", () => {
render(
<SegmentedControl
className="is-dense"
segments={[
{
label: "label1",
segmentContent: <p>content1</p>,
},
]}
/>,
);
expect(screen.getByTestId("segmented-control-div")).toHaveClass("is-dense");
});

it("can set active segment on segment click", async () => {
render(
<SegmentedControl
segments={[
{
label: "label1",
segmentContent: <p>content1</p>,
},
{
label: "label2",
segmentContent: <p>content2</p>,
},
]}
/>,
);
const segment = screen.getByRole("tab", { name: "label2" });
await userEvent.click(segment);
expect(screen.getByRole("tab", { name: "label2" })).toHaveAttribute(
"aria-selected",
"true",
);
expect(screen.getByRole("tabpanel")).toHaveTextContent("content2");
});
});
74 changes: 74 additions & 0 deletions src/components/SegmentedControl/SegmentedControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import classNames from "classnames";
import React, { ReactNode, HTMLProps, useState } from "react";
import type { ClassName } from "types";

export type Segments<P = null> = {
/**
* Label to be displayed inside the segment.
*/
label: ReactNode;
/**
* Content to be displayed inside the segment.
*/
segmentContent: ReactNode;
/**
* Icon to be displayed alongside the label of the segment.
*/
segmentIcon?: ReactNode;
} & (HTMLProps<HTMLElement> | P);

export type Props<P = null> = {
/**
* Optional classes applied to the parent element.
*/
className?: ClassName;
/**
* List of segments present in the element.
*/
segments: Segments<P>[];
};
/**
* This is the [React](https://reactjs.org/) component for Vanilla [SegmentedControl](https://vanillaframework.io/docs/patterns/segmented-control).
SegmentedControl organises and allows navigation between groups of content that are related and at the same level
of hierarchy.
*/
const SegmentedControl = ({
className,
segments,
}: Props): React.JSX.Element => {
const [activeIndex, setActiveIndex] = useState<number>(0);
return (
<div
className={classNames("p-segmented-control", className)}
data-testid="segmented-control-div"
>
<div className={classNames("p-segmented-control__list")} role="tablist">
{segments.map((segment, i) => {
return (
<button
aria-selected={activeIndex === i}
className={classNames("p-segmented-control__button")}
role="tab"
key={i}
onClick={() => setActiveIndex(i)}
>
{segment.segmentIcon ? (
<>
{segment.segmentIcon}
<span>{segment.label}</span>
</>
) : (
segment.label
)}
</button>
);
})}
</div>
<div tabIndex={activeIndex} role="tabpanel" data-testid="text-content">
{segments[activeIndex].segmentContent}
</div>
</div>
);
};

export default SegmentedControl;
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`SegmentedControl renders 1`] = `
<div>
<div
class="p-segmented-control"
data-testid="segmented-control-div"
>
<div
class="p-segmented-control__list"
role="tablist"
>
<button
aria-selected="true"
class="p-segmented-control__button"
role="tab"
>
label1
</button>
</div>
<div
data-testid="text-content"
role="tabpanel"
tabindex="0"
>
<p>
content1
</p>
</div>
</div>
</div>
`;
2 changes: 2 additions & 0 deletions src/components/SegmentedControl/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from "./SegmentedControl";
export type { Props as SegmentedControlProps } from "./SegmentedControl";
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export { default as ScrollableContainer } from "./components/ScrollableContainer
export { default as ScrollableTable } from "./components/ScrollableTable";
export { default as SearchAndFilter } from "./components/SearchAndFilter";
export { default as SearchBox } from "./components/SearchBox";
export { default as SegmentedControl } from "./components/SegmentedControl";
export { default as Select } from "./components/Select";
export { default as SideNavigation } from "./components/SideNavigation";
export { default as SideNavigationItem } from "./components/SideNavigation/SideNavigationItem";
Expand Down Expand Up @@ -194,6 +195,7 @@ export type { ScrollableTableProps } from "./components/ScrollableTable";
export type { ScrollableContainerProps } from "./components/ScrollableContainer";
export type { SearchAndFilterProps } from "./components/SearchAndFilter";
export type { SearchBoxProps } from "./components/SearchBox";
export type { SegmentedControlProps } from "./components/SegmentedControl";
export type { SelectProps } from "./components/Select";
export type { SideNavigationProps } from "./components/SideNavigation";
export type { SideNavigationItemProps } from "./components/SideNavigation/SideNavigationItem";
Expand Down