@purpurds/tabs 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ /// <reference types="react" />
2
+ import { TabContent } from "./tab-content";
3
+ export type TabsCmp<P> = React.FunctionComponent<P> & {
4
+ Content: typeof TabContent;
5
+ };
6
+ export declare const tabsVariants: readonly ["line", "line-negative", "contained", "contained-negative"];
7
+ export type TabsVariant = (typeof tabsVariants)[number];
8
+ export declare const createTabChangeDetailEvent: (value: string) => CustomEvent<TabChangeDetail>;
9
+ export type TabChangeDetail = {
10
+ value: string;
11
+ };
12
+ //# sourceMappingURL=tabs.utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tabs.utils.d.ts","sourceRoot":"","sources":["../src/tabs.utils.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC,CAAC,GAAG;IACpD,OAAO,EAAE,OAAO,UAAU,CAAC;CAC5B,CAAC;AAEF,eAAO,MAAM,YAAY,uEAAwE,CAAC;AAElG,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAExD,eAAO,MAAM,0BAA0B,UAAW,MAAM,iCACoB,CAAC;AAE7E,MAAM,MAAM,eAAe,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC"}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@purpurds/tabs",
3
+ "version": "3.0.0",
4
+ "license": "AGPL-3.0-only",
5
+ "main": "./dist/tabs.cjs.js",
6
+ "types": "./dist/tabs.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "require": "./dist/tabs.cjs.js",
10
+ "systemjs": "./dist/tabs.system.js",
11
+ "types": "./dist/tabs.d.ts",
12
+ "default": "./dist/tabs.es.js"
13
+ },
14
+ "./styles": "./dist/styles.css"
15
+ },
16
+ "source": "src/tabs.tsx",
17
+ "dependencies": {
18
+ "classnames": "~2.5.0",
19
+ "@radix-ui/react-tabs": "~1.0.4",
20
+ "@purpurds/paragraph": "3.0.0",
21
+ "@purpurds/icon": "3.0.0",
22
+ "@purpurds/tokens": "3.0.0"
23
+ },
24
+ "devDependencies": {
25
+ "@rushstack/eslint-patch": "~1.7.0",
26
+ "@storybook/blocks": "~7.6.0",
27
+ "@storybook/react": "~7.6.0",
28
+ "@telia/base-rig": "~8.2.0",
29
+ "@telia/react-rig": "~3.2.0",
30
+ "@testing-library/dom": "~9.3.3",
31
+ "@testing-library/jest-dom": "~6.3.0",
32
+ "@testing-library/react": "~14.1.2",
33
+ "@types/node": "18",
34
+ "@types/react-dom": "~18.2.17",
35
+ "@types/react": "~18.2.42",
36
+ "eslint-plugin-testing-library": "~6.2.0",
37
+ "eslint": "~8.56.0",
38
+ "jsdom": "~22.1.0",
39
+ "lint-staged": "~10.5.3",
40
+ "prettier": "~2.8.8",
41
+ "react-dom": "~18.2.0",
42
+ "react": "~18.2.0",
43
+ "typescript": "~5.2.2",
44
+ "vite": "~5.0.6",
45
+ "vitest": "~1.2.0",
46
+ "@purpurds/component-rig": "1.0.0"
47
+ },
48
+ "scripts": {
49
+ "build:dev": "vite",
50
+ "build:watch": "vite build --watch",
51
+ "build": "rm -rf dist && vite build && vite build --mode systemjs",
52
+ "ci:build": "rushx build",
53
+ "coverage": "vitest run --coverage",
54
+ "lint:fix": "eslint . --fix",
55
+ "lint": "lint-staged --no-stash 2>&1",
56
+ "sbdev": "rush sbdev",
57
+ "test:unit": "vitest run --passWithNoTests",
58
+ "test:watch": "vitest --watch",
59
+ "test": "rushx test:unit",
60
+ "typecheck": "tsc -p ./tsconfig.json"
61
+ }
62
+ }
package/readme.mdx ADDED
@@ -0,0 +1,68 @@
1
+ import { Meta, Stories, ArgTypes, Primary, Subtitle } from "@storybook/blocks";
2
+
3
+ import * as TabsStories from "./src/tabs.stories";
4
+ import * as TabContentStories from "./src/tab-content.stories";
5
+ import packageInfo from "./package.json";
6
+
7
+ <Meta name="Docs" title="Components/Tabs" of={TabsStories} />
8
+
9
+ # Tabs
10
+
11
+ <Subtitle>Version {packageInfo.version}</Subtitle>
12
+
13
+ ### Showcase
14
+
15
+ <Primary />
16
+
17
+ ### Properties
18
+
19
+ #### Tabs
20
+
21
+ <ArgTypes />
22
+
23
+ #### Tabs.Content
24
+
25
+ <ArgTypes of={TabContentStories} />
26
+
27
+ ### Installation
28
+
29
+ #### Via NPM
30
+
31
+ Add the dependency to your consumer app like `"@purpurds/tabs": "x.y.z"`
32
+
33
+ #### From outside the monorepo (build-time)
34
+
35
+ To install this package, you need to setup access to the artifactory. [Click here to go to the guide on how to do that](https://github.com/telia-company/jfrog-documentation/blob/main/doc/JFrog/JFrog_Onboarding.md#getting-access-to-artifactory-and-other-jfrog-applications).
36
+
37
+ ---
38
+
39
+ In MyApp.tsx
40
+
41
+ ```tsx
42
+ import "@purpurds/tokens/index.css";
43
+ ```
44
+
45
+ and
46
+
47
+ ```tsx
48
+ import "@purpurds/tabs/styles";
49
+ ```
50
+
51
+ In MyComponent.tsx
52
+
53
+ ```tsx
54
+ import { Tabs } from "@purpurds/tabs";
55
+
56
+ export const MyComponent = () => {
57
+ return (
58
+ <Tabs variant="contained">
59
+ <Tabs.Content tabId="tab-1" name="Tab name 1">
60
+ <div>Some content</div>
61
+ </Tabs.Content>
62
+ <Tabs.Content tabId="tab-2" name="Tab name 2">
63
+ <div>Some content</div>
64
+ </Tabs.Content>
65
+ </Tabs>
66
+ );
67
+ };
68
+ ```
@@ -0,0 +1,8 @@
1
+ import { vi } from "vitest";
2
+
3
+ const IntersectionObserverMock = vi.fn(() => ({
4
+ observe: vi.fn(),
5
+ unobserve: vi.fn(),
6
+ }));
7
+
8
+ vi.stubGlobal("IntersectionObserver", IntersectionObserverMock);
@@ -0,0 +1,4 @@
1
+ declare module "*.scss" {
2
+ const styles: { [className: string]: string };
3
+ export default styles;
4
+ }
@@ -0,0 +1,20 @@
1
+ .purpur-tab-content {
2
+ position: relative;
3
+
4
+ &:focus {
5
+ outline: 0;
6
+
7
+ &::before {
8
+ content: "";
9
+ position: absolute;
10
+ inset: calc(-1 * calc(var(--purpur-border-width-sm) * 2));
11
+ display: block;
12
+ border: var(--purpur-border-width-sm) solid var(--purpur-color-border-interactive-focus);
13
+ border-radius: var(--purpur-border-radius-sm);
14
+ }
15
+
16
+ &:not(:focus-visible)::before {
17
+ border: 0;
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,43 @@
1
+ import React from "react";
2
+ import { Root } from "@radix-ui/react-tabs";
3
+ import type { Meta, StoryObj } from "@storybook/react";
4
+
5
+ import { Tabs } from "./tabs";
6
+
7
+ const meta: Meta<typeof Tabs.Content> = {
8
+ title: "Components/Tabs",
9
+ component: Tabs.Content,
10
+ parameters: {
11
+ design: [
12
+ {
13
+ name: "Tabs",
14
+ type: "figma",
15
+ url: "https://www.figma.com/file/XEaIIFskrrxIBHMZDkIuIg/Purpur-DS---Component-library-%26-guidelines?type=design&node-id=2942-430&mode=design&t=zoKkl2HAtk3UuKoP-0",
16
+ },
17
+ ],
18
+ storyHead: {
19
+ title: "Tabs.Content",
20
+ },
21
+ },
22
+ argTypes: {
23
+ ["data-testid"]: { control: { type: "text" } },
24
+ className: { control: { type: "text" } },
25
+ },
26
+ };
27
+
28
+ export default meta;
29
+
30
+ type Story = StoryObj<typeof Tabs.Content>;
31
+
32
+ export const TabContent: Story = {
33
+ args: {
34
+ tabId: "tab-1",
35
+ name: "Tab name-1",
36
+ children: <div>Some content</div>,
37
+ },
38
+ render: ({ children, ...args }) => (
39
+ <Root defaultValue={args.tabId}>
40
+ <Tabs.Content {...args}>{children}</Tabs.Content>
41
+ </Root>
42
+ ),
43
+ };
@@ -0,0 +1,40 @@
1
+ import React from "react";
2
+ import * as matchers from "@testing-library/jest-dom/matchers";
3
+ import { cleanup, render, screen } from "@testing-library/react";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+
6
+ import { Tabs } from "./tabs";
7
+ import { TabContent } from "./tab-content";
8
+ import "./__mocks__/intersectionObserverMock";
9
+
10
+ const rootClassName = "purpur-tab-content";
11
+
12
+ expect.extend(matchers);
13
+
14
+ describe("TabContent", () => {
15
+ afterEach(cleanup);
16
+
17
+ it("should render plain", () => {
18
+ render(
19
+ <Tabs variant="line">
20
+ <TabContent tabId="tab-1" name="Tab name 1" data-testid="tab-content">
21
+ <div>Content</div>
22
+ </TabContent>
23
+ </Tabs>
24
+ );
25
+
26
+ const tabContent = screen.getByTestId("tab-content");
27
+ expect(tabContent).toBeInTheDocument();
28
+ expect(tabContent.classList.value).toContain(rootClassName);
29
+ });
30
+
31
+ it("should throw error if not used within Tab component", () => {
32
+ expect(() => {
33
+ render(
34
+ <TabContent tabId="tab-1" name="Tab name 1" data-testid="tab-content">
35
+ <div>Content</div>
36
+ </TabContent>
37
+ );
38
+ }).toThrow();
39
+ });
40
+ });
@@ -0,0 +1,67 @@
1
+ import React, {
2
+ ComponentPropsWithoutRef,
3
+ ForwardedRef,
4
+ forwardRef,
5
+ isValidElement,
6
+ ReactChild,
7
+ ReactElement,
8
+ ReactFragment,
9
+ ReactNode,
10
+ ReactPortal,
11
+ } from "react";
12
+ import { Content } from "@radix-ui/react-tabs";
13
+ import c from "classnames/bind";
14
+
15
+ import styles from "./tab-content.module.scss";
16
+
17
+ export type TabContentProps = {
18
+ /**
19
+ * The text that will be displayed in the tab.
20
+ * */
21
+ name: string;
22
+ /**
23
+ * A unique ID that associates the tab with a content.
24
+ * */
25
+ tabId: string;
26
+ className?: string;
27
+ "data-testid"?: string;
28
+ };
29
+
30
+ export type TabContentPropsWithChildren = ComponentPropsWithoutRef<"div"> &
31
+ TabContentProps & {
32
+ children: ReactNode;
33
+ };
34
+
35
+ const cx = c.bind(styles);
36
+ const rootClassName = "purpur-tab-content";
37
+
38
+ export const TabContent = forwardRef(
39
+ (
40
+ { children, tabId, "data-testid": dataTestId, className, ...rest }: TabContentPropsWithChildren,
41
+ ref: ForwardedRef<HTMLDivElement>
42
+ ) => (
43
+ <Content
44
+ ref={ref}
45
+ className={cx([rootClassName, className])}
46
+ data-testid={dataTestId}
47
+ value={tabId}
48
+ {...rest}
49
+ >
50
+ {children}
51
+ </Content>
52
+ )
53
+ );
54
+
55
+ export const isTabContent = (
56
+ child:
57
+ | ReactChild
58
+ | ReactElement
59
+ | ReactFragment
60
+ | ReactPortal
61
+ | string
62
+ | number
63
+ | boolean
64
+ | null
65
+ | undefined
66
+ ): child is ReactElement<TabContentProps> =>
67
+ !!child && isValidElement<TabContentProps>(child) && !!child.props.name && !!child.props.tabId;
@@ -0,0 +1,121 @@
1
+ @import "@purpurds/paragraph/src/paragraph.mixins.scss";
2
+
3
+ .purpur-tab-header {
4
+ $root: &;
5
+
6
+ position: relative;
7
+ width: 100%;
8
+ padding: calc(var(--purpur-spacing-100) + var(--purpur-spacing-25)) var(--purpur-spacing-200);
9
+ border: 0;
10
+ border-radius: var(--purpur-border-radius-sm) var(--purpur-border-radius-sm) 0 0;
11
+ background: transparent;
12
+ @include purpur-paragraph-100;
13
+ font-weight: 500;
14
+ white-space: nowrap;
15
+ cursor: pointer;
16
+ transition: all var(--purpur-motion-duration-150) var(--purpur-motion-easing-ease-in-out);
17
+
18
+ &[aria-selected="true"] {
19
+ cursor: auto;
20
+ }
21
+
22
+ &:focus {
23
+ outline: 0;
24
+
25
+ &:focus-visible::after {
26
+ content: "";
27
+ position: absolute;
28
+ inset: 0;
29
+ z-index: 20;
30
+ border: var(--purpur-border-width-sm) solid var(--purpur-color-border-interactive-focus);
31
+ border-radius: var(--purpur-border-radius-sm);
32
+ pointer-events: none;
33
+ }
34
+ }
35
+
36
+ &--contained,
37
+ &--contained-negative {
38
+ &:focus:focus-visible::after {
39
+ top: calc(-1 * var(--purpur-border-width-sm));
40
+ }
41
+ }
42
+
43
+ &--line {
44
+ color: var(--purpur-color-text-interactive-primary);
45
+ background: var(--purpur-color-functional-transparent);
46
+
47
+ &:not(#{$root}[aria-selected="true"]):hover {
48
+ color: var(--purpur-color-text-interactive-primary-hover);
49
+ background: var(--purpur-color-background-interactive-transparent-hover);
50
+ }
51
+
52
+ &:not(#{$root}[aria-selected="true"]):active {
53
+ color: var(--purpur-color-text-interactive-primary-active);
54
+ background: var(--purpur-color-background-interactive-transparent-active);
55
+ }
56
+ }
57
+
58
+ &--line-negative {
59
+ color: var(--purpur-color-text-interactive-primary-negative);
60
+ background: var(--purpur-color-functional-transparent);
61
+
62
+ &:not(#{$root}[aria-selected="true"]):hover {
63
+ color: var(--purpur-color-text-interactive-primary-negative-hover);
64
+ background: var(--purpur-color-background-interactive-transparent-negative-hover);
65
+ }
66
+
67
+ &:not(#{$root}[aria-selected="true"]):active {
68
+ color: var(--purpur-color-text-interactive-primary-negative-active);
69
+ background: var(--purpur-color-background-interactive-transparent-negative-active);
70
+ }
71
+ }
72
+
73
+ &--contained {
74
+ padding-top: calc(
75
+ (var(--purpur-spacing-100) + var(--purpur-spacing-25)) - var(--purpur-border-width-sm)
76
+ );
77
+ border-top: var(--purpur-border-width-sm) solid transparent;
78
+ color: var(--purpur-color-text-interactive-primary);
79
+ background: var(--purpur-color-background-interactive-inactive);
80
+
81
+ &#{$root}[aria-selected="true"] {
82
+ border-color: var(--purpur-color-border-interactive-primary);
83
+ background: var(--purpur-color-background-primary);
84
+ }
85
+
86
+ &:not(#{$root}[aria-selected="true"]):hover {
87
+ color: var(--purpur-color-text-interactive-primary-hover);
88
+ background: var(--purpur-color-background-interactive-transparent-hover);
89
+ }
90
+
91
+ &:not(#{$root}[aria-selected="true"]):active {
92
+ color: var(--purpur-color-text-interactive-primary-active);
93
+ background: var(--purpur-color-background-interactive-transparent-active);
94
+ }
95
+ }
96
+
97
+ &--contained-negative {
98
+ padding-top: calc(
99
+ (var(--purpur-spacing-100) + var(--purpur-spacing-25)) - var(--purpur-border-width-sm)
100
+ );
101
+ border-top: var(--purpur-border-width-sm) solid transparent;
102
+ color: var(--purpur-color-text-interactive-primary-negative);
103
+ background: var(--purpur-color-background-interactive-inactive-negative);
104
+
105
+ &#{$root}[aria-selected="true"] {
106
+ border-color: var(--purpur-color-border-interactive-primary-negative);
107
+ color: var(--purpur-color-text-interactive-primary);
108
+ background: var(--purpur-color-background-primary);
109
+ }
110
+
111
+ &:not(#{$root}[aria-selected="true"]):hover {
112
+ color: var(--purpur-color-text-interactive-primary-negative-hover);
113
+ background: var(--purpur-color-background-interactive-transparent-negative-hover);
114
+ }
115
+
116
+ &:not(#{$root}[aria-selected="true"]):active {
117
+ color: var(--purpur-color-text-interactive-primary-negative-active);
118
+ background: var(--purpur-color-background-interactive-transparent-negative-active);
119
+ }
120
+ }
121
+ }
@@ -0,0 +1,37 @@
1
+ import React, { FocusEventHandler, ForwardedRef, forwardRef, ReactNode } from "react";
2
+ import { Trigger } from "@radix-ui/react-tabs";
3
+ import c from "classnames/bind";
4
+
5
+ import styles from "./tab-header.module.scss";
6
+ import { TabsVariant } from "./tabs.utils";
7
+
8
+ type TabHeaderProps = {
9
+ "data-testid"?: string;
10
+ index: number;
11
+ tabId: string;
12
+ variant: TabsVariant;
13
+ onFocus: FocusEventHandler<HTMLButtonElement>;
14
+ children: ReactNode;
15
+ };
16
+
17
+ const cx = c.bind(styles);
18
+ const rootClassName = "purpur-tab-header";
19
+
20
+ export const TabHeader = forwardRef(
21
+ <T extends HTMLButtonElement>(
22
+ { index, tabId, variant, onFocus, "data-testid": dataTestId, children }: TabHeaderProps,
23
+ ref: ForwardedRef<T>
24
+ ) => (
25
+ <Trigger
26
+ id={`${tabId}-trigger`}
27
+ className={cx([rootClassName, `${rootClassName}--${variant}`])}
28
+ value={tabId}
29
+ data-testid={dataTestId}
30
+ data-index={index}
31
+ ref={ref}
32
+ onFocus={onFocus}
33
+ >
34
+ {children}
35
+ </Trigger>
36
+ )
37
+ );
@@ -0,0 +1,158 @@
1
+ .purpur-tabs {
2
+ $root: &;
3
+
4
+ &__wrapper {
5
+ position: relative;
6
+ -ms-overflow-style: none; /* For Internet Explorer and Edge */
7
+ scrollbar-width: none; /* Firefox */
8
+
9
+ ::-webkit-scrollbar {
10
+ display: none; /* For Chrome, Safari and Opera */
11
+ }
12
+
13
+ #{$root}__scroll-button {
14
+ display: none;
15
+ align-items: center;
16
+ position: absolute;
17
+ top: 0;
18
+ z-index: 10;
19
+ height: 100%;
20
+ padding: 0;
21
+ border: var(--purpur-border-width-xs) solid var(--purpur-color-border-interactive-subtle);
22
+ border-radius: var(--purpur-border-radius-xs);
23
+ color: var(--purpur-color-text-default);
24
+ background: var(--purpur-color-background-primary);
25
+ box-shadow: var(--purpur-shadow-md);
26
+ transition: all var(--purpur-motion-duration-150) var(--purpur-motion-easing-ease-in-out);
27
+ cursor: pointer;
28
+
29
+ &::after {
30
+ content: "";
31
+ position: absolute;
32
+ inset: calc(-1 * var(--purpur-border-width-xs));
33
+ border: var(--purpur-border-width-sm) solid var(--purpur-color-border-interactive-subtle-hover);
34
+ border-radius: var(--purpur-border-radius-xs);
35
+ box-sizing: border-box;
36
+ opacity: 0;
37
+ transition: all var(--purpur-motion-duration-150) var(--purpur-motion-easing-ease-in-out);
38
+ }
39
+
40
+ &:hover {
41
+ box-shadow: none;
42
+ border-color: transparent;
43
+ color: var(--purpur-color-text-interactive-primary-hover);
44
+
45
+ &::after {
46
+ opacity: 1;
47
+ }
48
+ }
49
+
50
+ &:active::after {
51
+ border-width: var(--purpur-border-width-xs);
52
+ }
53
+
54
+ &:focus {
55
+ outline: 0;
56
+ }
57
+
58
+ &:focus-visible {
59
+ outline: 0;
60
+ border-color: var(--purpur-color-border-interactive-subtle);
61
+ box-shadow: none;
62
+
63
+ &::after {
64
+ left: calc(-1 * (var(--purpur-border-width-sm) * 2));
65
+ top: calc(-1 * (var(--purpur-border-width-sm) * 2));
66
+ width: calc(100% + (var(--purpur-border-width-sm) * 4));
67
+ height: calc(100% + (var(--purpur-border-width-sm) * 4));
68
+ border-color: var(--purpur-color-border-interactive-focus);
69
+ opacity: 1;
70
+ pointer-events: none;
71
+ }
72
+ }
73
+ }
74
+
75
+ #{$root}__scroll-button--left {
76
+ left: 0;
77
+ }
78
+
79
+ #{$root}__scroll-button--right {
80
+ right: 0;
81
+ }
82
+
83
+ &--scroll-end {
84
+ #{$root}__scroll-button--left {
85
+ display: flex;
86
+ }
87
+ }
88
+
89
+ &--scroll-start {
90
+ #{$root}__scroll-button--right {
91
+ display: flex;
92
+ }
93
+ }
94
+ }
95
+
96
+ &--line,
97
+ &--line-negative {
98
+ #{$root}__wrapper::after {
99
+ content: "";
100
+ position: absolute;
101
+ bottom: 0;
102
+ left: 0;
103
+ width: 100%;
104
+ height: var(--purpur-border-width-sm);
105
+ }
106
+
107
+ #{$root}__selected-border {
108
+ position: absolute;
109
+ bottom: 0;
110
+ left: 0;
111
+ z-index: 10;
112
+ height: var(--purpur-border-width-sm);
113
+ transition: all var(--purpur-motion-duration-150) var(--purpur-motion-easing-ease-in-out);
114
+ }
115
+ }
116
+
117
+ &--line {
118
+ #{$root}__wrapper::after {
119
+ background: var(--purpur-color-border-weak);
120
+ }
121
+
122
+ #{$root}__selected-border {
123
+ background: var(--purpur-color-border-interactive-primary);
124
+ }
125
+ }
126
+
127
+ &--line-negative {
128
+ #{$root}__wrapper::after {
129
+ background: var(--purpur-color-border-weak-negative);
130
+ }
131
+
132
+ #{$root}__selected-border {
133
+ background: var(--purpur-color-border-interactive-primary-negative);
134
+ }
135
+ }
136
+
137
+ &--contained,
138
+ &--contained-negative {
139
+ #{$root}__list {
140
+ gap: var(--purpur-spacing-100);
141
+ }
142
+
143
+ #{$root}__content-container {
144
+ background: var(--purpur-color-background-primary);
145
+ }
146
+ }
147
+
148
+ &__list {
149
+ position: relative;
150
+ display: inline-flex;
151
+ max-width: 100%;
152
+ overflow: auto;
153
+ }
154
+
155
+ &--fullWidth #{$root}__list {
156
+ min-width: 100%;
157
+ }
158
+ }