@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.
@@ -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
+ };
@@ -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>
@@ -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