@papernote/ui 1.10.17 → 1.10.19
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.
- package/dist/components/BottomNavigation.d.ts +6 -0
- package/dist/components/BottomNavigation.d.ts.map +1 -1
- package/dist/components/DataTable.d.ts.map +1 -1
- package/dist/components/Menu.d.ts +6 -0
- package/dist/components/Menu.d.ts.map +1 -1
- package/dist/components/Sidebar.d.ts +6 -0
- package/dist/components/Sidebar.d.ts.map +1 -1
- package/dist/components/SkipLink.d.ts +48 -0
- package/dist/components/SkipLink.d.ts.map +1 -0
- package/dist/components/Tabs.d.ts +13 -1
- package/dist/components/Tabs.d.ts.map +1 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/index.d.ts +80 -3
- package/dist/index.esm.js +275 -14
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +275 -13
- package/dist/index.js.map +1 -1
- package/dist/styles.css +69 -0
- package/package.json +1 -1
- package/src/components/BottomNavigation.stories.tsx +57 -0
- package/src/components/BottomNavigation.tsx +10 -2
- package/src/components/DataTable.tsx +243 -4
- package/src/components/Menu.tsx +10 -0
- package/src/components/Sidebar.stories.tsx +83 -0
- package/src/components/Sidebar.tsx +18 -3
- package/src/components/SkipLink.stories.tsx +97 -0
- package/src/components/SkipLink.tsx +88 -0
- package/src/components/Tabs.stories.tsx +126 -0
- package/src/components/Tabs.tsx +17 -0
- package/src/components/index.ts +3 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import SkipLink from './SkipLink';
|
|
3
|
+
import Stack from './Stack';
|
|
4
|
+
import Text from './Text';
|
|
5
|
+
import Card from './Card';
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof SkipLink> = {
|
|
8
|
+
title: 'Accessibility/SkipLink',
|
|
9
|
+
component: SkipLink,
|
|
10
|
+
parameters: {
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component:
|
|
14
|
+
'A skip link allows keyboard users to bypass repetitive navigation and jump directly to the main content. The link is visually hidden until focused (press Tab to see it).',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
tags: ['autodocs'],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default meta;
|
|
22
|
+
type Story = StoryObj<typeof SkipLink>;
|
|
23
|
+
|
|
24
|
+
export const Default: Story = {
|
|
25
|
+
args: {
|
|
26
|
+
targetId: 'main-content',
|
|
27
|
+
},
|
|
28
|
+
render: (args) => (
|
|
29
|
+
<Stack gap="lg">
|
|
30
|
+
<Text size="sm" className="text-ink-500">
|
|
31
|
+
Press Tab to reveal the skip link
|
|
32
|
+
</Text>
|
|
33
|
+
<SkipLink {...args} />
|
|
34
|
+
<Card>
|
|
35
|
+
<Stack gap="md">
|
|
36
|
+
<Text weight="bold">Fake Navigation</Text>
|
|
37
|
+
<nav>
|
|
38
|
+
<Stack direction="horizontal" gap="md">
|
|
39
|
+
<a href="#" className="text-accent-600">Home</a>
|
|
40
|
+
<a href="#" className="text-accent-600">About</a>
|
|
41
|
+
<a href="#" className="text-accent-600">Contact</a>
|
|
42
|
+
</Stack>
|
|
43
|
+
</nav>
|
|
44
|
+
</Stack>
|
|
45
|
+
</Card>
|
|
46
|
+
<Card id="main-content" tabIndex={-1}>
|
|
47
|
+
<Stack gap="md">
|
|
48
|
+
<Text weight="bold">Main Content</Text>
|
|
49
|
+
<Text>This is the main content area. The skip link will focus here when activated.</Text>
|
|
50
|
+
</Stack>
|
|
51
|
+
</Card>
|
|
52
|
+
</Stack>
|
|
53
|
+
),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const CustomText: Story = {
|
|
57
|
+
args: {
|
|
58
|
+
targetId: 'content',
|
|
59
|
+
children: 'Skip navigation',
|
|
60
|
+
},
|
|
61
|
+
render: (args) => (
|
|
62
|
+
<Stack gap="lg">
|
|
63
|
+
<Text size="sm" className="text-ink-500">
|
|
64
|
+
Press Tab to reveal the skip link with custom text
|
|
65
|
+
</Text>
|
|
66
|
+
<SkipLink {...args} />
|
|
67
|
+
<Card id="content" tabIndex={-1}>
|
|
68
|
+
<Text>Target content area</Text>
|
|
69
|
+
</Card>
|
|
70
|
+
</Stack>
|
|
71
|
+
),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const MultipleSkipLinks: Story = {
|
|
75
|
+
render: () => (
|
|
76
|
+
<Stack gap="lg">
|
|
77
|
+
<Text size="sm" className="text-ink-500">
|
|
78
|
+
Press Tab multiple times to see multiple skip links
|
|
79
|
+
</Text>
|
|
80
|
+
<SkipLink targetId="main">Skip to main content</SkipLink>
|
|
81
|
+
<SkipLink targetId="search">Skip to search</SkipLink>
|
|
82
|
+
<SkipLink targetId="footer">Skip to footer</SkipLink>
|
|
83
|
+
<Card>
|
|
84
|
+
<Text weight="bold">Navigation</Text>
|
|
85
|
+
</Card>
|
|
86
|
+
<Card id="search" tabIndex={-1}>
|
|
87
|
+
<Text weight="bold">Search Section</Text>
|
|
88
|
+
</Card>
|
|
89
|
+
<Card id="main" tabIndex={-1}>
|
|
90
|
+
<Text weight="bold">Main Content</Text>
|
|
91
|
+
</Card>
|
|
92
|
+
<Card id="footer" tabIndex={-1}>
|
|
93
|
+
<Text weight="bold">Footer</Text>
|
|
94
|
+
</Card>
|
|
95
|
+
</Stack>
|
|
96
|
+
),
|
|
97
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SkipLink component props
|
|
5
|
+
*/
|
|
6
|
+
export interface SkipLinkProps {
|
|
7
|
+
/** The target element ID to skip to (without the # prefix) */
|
|
8
|
+
targetId: string;
|
|
9
|
+
/** Custom link text (default: "Skip to main content") */
|
|
10
|
+
children?: React.ReactNode;
|
|
11
|
+
/** Additional CSS classes */
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* SkipLink - Accessibility skip link for keyboard navigation
|
|
17
|
+
*
|
|
18
|
+
* A skip link allows keyboard users to bypass repetitive navigation
|
|
19
|
+
* and jump directly to the main content. The link is visually hidden
|
|
20
|
+
* until focused, making it invisible to mouse users while remaining
|
|
21
|
+
* accessible to keyboard and screen reader users.
|
|
22
|
+
*
|
|
23
|
+
* Place this component at the very beginning of your page layout,
|
|
24
|
+
* before any navigation elements.
|
|
25
|
+
*
|
|
26
|
+
* @example Basic usage
|
|
27
|
+
* ```tsx
|
|
28
|
+
* // In your layout component:
|
|
29
|
+
* <SkipLink targetId="main-content" />
|
|
30
|
+
* <Navigation />
|
|
31
|
+
* <main id="main-content">
|
|
32
|
+
* {children}
|
|
33
|
+
* </main>
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @example Custom text
|
|
37
|
+
* ```tsx
|
|
38
|
+
* <SkipLink targetId="content">Skip navigation</SkipLink>
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* @example Multiple skip links
|
|
42
|
+
* ```tsx
|
|
43
|
+
* <div>
|
|
44
|
+
* <SkipLink targetId="main-content">Skip to main content</SkipLink>
|
|
45
|
+
* <SkipLink targetId="search">Skip to search</SkipLink>
|
|
46
|
+
* </div>
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export default function SkipLink({
|
|
50
|
+
targetId,
|
|
51
|
+
children = 'Skip to main content',
|
|
52
|
+
className = '',
|
|
53
|
+
}: SkipLinkProps) {
|
|
54
|
+
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
const target = document.getElementById(targetId);
|
|
57
|
+
if (target) {
|
|
58
|
+
// Set tabindex to make the element focusable if it isn't already
|
|
59
|
+
if (!target.hasAttribute('tabindex')) {
|
|
60
|
+
target.setAttribute('tabindex', '-1');
|
|
61
|
+
}
|
|
62
|
+
target.focus();
|
|
63
|
+
// Scroll the element into view
|
|
64
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<a
|
|
70
|
+
href={`#${targetId}`}
|
|
71
|
+
onClick={handleClick}
|
|
72
|
+
className={`
|
|
73
|
+
sr-only focus:not-sr-only
|
|
74
|
+
focus:absolute focus:z-50
|
|
75
|
+
focus:top-4 focus:left-4
|
|
76
|
+
focus:px-4 focus:py-2
|
|
77
|
+
focus:bg-accent-600 focus:text-white
|
|
78
|
+
focus:rounded-lg focus:shadow-lg
|
|
79
|
+
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-400
|
|
80
|
+
font-medium text-sm
|
|
81
|
+
transition-none
|
|
82
|
+
${className}
|
|
83
|
+
`}
|
|
84
|
+
>
|
|
85
|
+
{children}
|
|
86
|
+
</a>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -923,3 +923,129 @@ This example places the tabs in a header row separate from the content area.
|
|
|
923
923
|
},
|
|
924
924
|
},
|
|
925
925
|
};
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Demonstrates custom data attributes for E2E testing and product tours.
|
|
929
|
+
* Each tab includes data-testid and data-tour attributes for reliable element targeting.
|
|
930
|
+
*
|
|
931
|
+
* Inspect the elements to see the data attributes:
|
|
932
|
+
* - `data-testid`: For E2E testing with Playwright/Cypress
|
|
933
|
+
* - `data-tour`: For product tours with Driver.js or similar libraries
|
|
934
|
+
*/
|
|
935
|
+
export const WithDataAttributes: Story = {
|
|
936
|
+
render: () => {
|
|
937
|
+
const [activeTab, setActiveTab] = useState('profile');
|
|
938
|
+
const tabs: Tab[] = [
|
|
939
|
+
{
|
|
940
|
+
id: 'profile',
|
|
941
|
+
label: 'Profile',
|
|
942
|
+
icon: <User className="h-4 w-4" />,
|
|
943
|
+
content: <div style={{ padding: '1rem' }}><h3>Profile</h3><p>Manage your profile information.</p></div>,
|
|
944
|
+
dataAttributes: {
|
|
945
|
+
'data-tour': 'tab-profile',
|
|
946
|
+
'data-testid': 'tab-profile',
|
|
947
|
+
},
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
id: 'settings',
|
|
951
|
+
label: 'Settings',
|
|
952
|
+
icon: <Settings className="h-4 w-4" />,
|
|
953
|
+
content: <div style={{ padding: '1rem' }}><h3>Settings</h3><p>Configure your preferences.</p></div>,
|
|
954
|
+
dataAttributes: {
|
|
955
|
+
'data-tour': 'tab-settings',
|
|
956
|
+
'data-testid': 'tab-settings',
|
|
957
|
+
},
|
|
958
|
+
},
|
|
959
|
+
{
|
|
960
|
+
id: 'notifications',
|
|
961
|
+
label: 'Notifications',
|
|
962
|
+
icon: <Bell className="h-4 w-4" />,
|
|
963
|
+
badge: 5,
|
|
964
|
+
content: <div style={{ padding: '1rem' }}><h3>Notifications</h3><p>View your notifications.</p></div>,
|
|
965
|
+
dataAttributes: {
|
|
966
|
+
'data-tour': 'tab-notifications',
|
|
967
|
+
'data-testid': 'tab-notifications',
|
|
968
|
+
},
|
|
969
|
+
},
|
|
970
|
+
];
|
|
971
|
+
|
|
972
|
+
return (
|
|
973
|
+
<div>
|
|
974
|
+
<p style={{ marginBottom: '1rem', color: '#64748b', fontSize: '0.875rem' }}>
|
|
975
|
+
Inspect the tab buttons to see the <code>data-tour</code> and <code>data-testid</code> attributes.
|
|
976
|
+
</p>
|
|
977
|
+
<Tabs tabs={tabs} activeTab={activeTab} onChange={setActiveTab} />
|
|
978
|
+
</div>
|
|
979
|
+
);
|
|
980
|
+
},
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Compound component pattern with custom data attributes for testing and product tours.
|
|
985
|
+
*/
|
|
986
|
+
export const CompoundWithDataAttributes: Story = {
|
|
987
|
+
render: () => (
|
|
988
|
+
<TabsRoot defaultValue="account">
|
|
989
|
+
<TabsList>
|
|
990
|
+
<TabsTrigger
|
|
991
|
+
value="account"
|
|
992
|
+
icon={<User className="h-4 w-4" />}
|
|
993
|
+
dataAttributes={{
|
|
994
|
+
'data-tour': 'compound-tab-account',
|
|
995
|
+
'data-testid': 'compound-tab-account',
|
|
996
|
+
}}
|
|
997
|
+
>
|
|
998
|
+
Account
|
|
999
|
+
</TabsTrigger>
|
|
1000
|
+
<TabsTrigger
|
|
1001
|
+
value="password"
|
|
1002
|
+
icon={<Lock className="h-4 w-4" />}
|
|
1003
|
+
dataAttributes={{
|
|
1004
|
+
'data-tour': 'compound-tab-password',
|
|
1005
|
+
'data-testid': 'compound-tab-password',
|
|
1006
|
+
}}
|
|
1007
|
+
>
|
|
1008
|
+
Password
|
|
1009
|
+
</TabsTrigger>
|
|
1010
|
+
<TabsTrigger
|
|
1011
|
+
value="settings"
|
|
1012
|
+
icon={<Settings className="h-4 w-4" />}
|
|
1013
|
+
dataAttributes={{
|
|
1014
|
+
'data-tour': 'compound-tab-settings',
|
|
1015
|
+
'data-testid': 'compound-tab-settings',
|
|
1016
|
+
}}
|
|
1017
|
+
>
|
|
1018
|
+
Settings
|
|
1019
|
+
</TabsTrigger>
|
|
1020
|
+
</TabsList>
|
|
1021
|
+
<TabsContent value="account">
|
|
1022
|
+
<div style={{ padding: '1rem' }}>
|
|
1023
|
+
<h3>Account Settings</h3>
|
|
1024
|
+
<p>Manage your account information and preferences.</p>
|
|
1025
|
+
</div>
|
|
1026
|
+
</TabsContent>
|
|
1027
|
+
<TabsContent value="password">
|
|
1028
|
+
<div style={{ padding: '1rem' }}>
|
|
1029
|
+
<h3>Password Settings</h3>
|
|
1030
|
+
<p>Change your password and security settings.</p>
|
|
1031
|
+
</div>
|
|
1032
|
+
</TabsContent>
|
|
1033
|
+
<TabsContent value="settings">
|
|
1034
|
+
<div style={{ padding: '1rem' }}>
|
|
1035
|
+
<h3>Application Settings</h3>
|
|
1036
|
+
<p>Configure application preferences.</p>
|
|
1037
|
+
</div>
|
|
1038
|
+
</TabsContent>
|
|
1039
|
+
</TabsRoot>
|
|
1040
|
+
),
|
|
1041
|
+
parameters: {
|
|
1042
|
+
docs: {
|
|
1043
|
+
description: {
|
|
1044
|
+
story: `
|
|
1045
|
+
The compound component pattern also supports \`dataAttributes\` on the \`TabsTrigger\` component.
|
|
1046
|
+
This allows targeting individual tabs with selectors like \`[data-tour="compound-tab-account"]\`.
|
|
1047
|
+
`,
|
|
1048
|
+
},
|
|
1049
|
+
},
|
|
1050
|
+
},
|
|
1051
|
+
};
|
package/src/components/Tabs.tsx
CHANGED
|
@@ -18,6 +18,12 @@ export interface Tab {
|
|
|
18
18
|
badgeVariant?: 'success' | 'warning' | 'error' | 'info' | 'neutral';
|
|
19
19
|
/** Whether this individual tab can be closed (overrides global closeable) */
|
|
20
20
|
closeable?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Custom data attributes to spread on the tab trigger element.
|
|
23
|
+
* Useful for product tours (e.g., Driver.js) and E2E testing (Playwright, Cypress).
|
|
24
|
+
* @example { 'data-tour': 'tab-settings', 'data-testid': 'settings-tab' }
|
|
25
|
+
*/
|
|
26
|
+
dataAttributes?: Record<string, string>;
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
export interface TabsProps {
|
|
@@ -274,6 +280,12 @@ export interface TabsTriggerProps {
|
|
|
274
280
|
badgeVariant?: 'success' | 'warning' | 'error' | 'info' | 'neutral';
|
|
275
281
|
/** Additional class name */
|
|
276
282
|
className?: string;
|
|
283
|
+
/**
|
|
284
|
+
* Custom data attributes to spread on the tab trigger element.
|
|
285
|
+
* Useful for product tours (e.g., Driver.js) and E2E testing (Playwright, Cypress).
|
|
286
|
+
* @example { 'data-tour': 'tab-settings', 'data-testid': 'settings-tab' }
|
|
287
|
+
*/
|
|
288
|
+
dataAttributes?: Record<string, string>;
|
|
277
289
|
}
|
|
278
290
|
|
|
279
291
|
/**
|
|
@@ -287,6 +299,7 @@ export function TabsTrigger({
|
|
|
287
299
|
badge,
|
|
288
300
|
badgeVariant = 'info',
|
|
289
301
|
className = '',
|
|
302
|
+
dataAttributes,
|
|
290
303
|
}: TabsTriggerProps) {
|
|
291
304
|
const { activeTab, setActiveTab, variant, orientation, size, registerTab, unregisterTab } = useTabsContext();
|
|
292
305
|
const isActive = activeTab === value;
|
|
@@ -332,6 +345,8 @@ export function TabsTrigger({
|
|
|
332
345
|
focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-500 focus-visible:ring-offset-1
|
|
333
346
|
${className}
|
|
334
347
|
`}
|
|
348
|
+
data-testid={dataAttributes?.['data-testid'] || `tab-${value}`}
|
|
349
|
+
{...dataAttributes}
|
|
335
350
|
>
|
|
336
351
|
{icon && <span className={`flex-shrink-0 ${sizeClasses[size].icon}`}>{icon}</span>}
|
|
337
352
|
<span>{children}</span>
|
|
@@ -660,6 +675,8 @@ export default function Tabs({
|
|
|
660
675
|
focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-500 focus-visible:ring-offset-1
|
|
661
676
|
group
|
|
662
677
|
`}
|
|
678
|
+
data-testid={tab.dataAttributes?.['data-testid'] || `tab-${tab.id}`}
|
|
679
|
+
{...tab.dataAttributes}
|
|
663
680
|
>
|
|
664
681
|
{tab.icon && <span className={`flex-shrink-0 ${sizeClasses[size].icon}`}>{tab.icon}</span>}
|
|
665
682
|
<span className={isTabCloseable ? 'mr-1' : ''}>{tab.label}</span>
|
package/src/components/index.ts
CHANGED
|
@@ -144,6 +144,9 @@ export type { TooltipProps } from './Tooltip';
|
|
|
144
144
|
export { default as HelpTooltip } from './HelpTooltip';
|
|
145
145
|
export type { HelpTooltipProps } from './HelpTooltip';
|
|
146
146
|
|
|
147
|
+
export { default as SkipLink } from './SkipLink';
|
|
148
|
+
export type { SkipLinkProps } from './SkipLink';
|
|
149
|
+
|
|
147
150
|
export { default as Popover } from './Popover';
|
|
148
151
|
export type { PopoverProps } from './Popover';
|
|
149
152
|
|