@a11ypros/a11y-ui-components 1.0.1 → 1.0.2
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/README.md +182 -157
- package/dist/components/Button/Button.d.ts +37 -0
- package/dist/components/Button/Button.d.ts.map +1 -0
- package/dist/components/Button/Button.js +52 -0
- package/dist/components/Button/index.d.ts +3 -0
- package/dist/components/Button/index.d.ts.map +1 -0
- package/dist/components/Button/index.js +1 -0
- package/dist/components/DataTable/DataTable.d.ts +71 -0
- package/dist/components/DataTable/DataTable.d.ts.map +1 -0
- package/dist/components/DataTable/DataTable.js +122 -0
- package/dist/components/DataTable/index.d.ts +3 -0
- package/dist/components/DataTable/index.d.ts.map +1 -0
- package/dist/components/DataTable/index.js +1 -0
- package/dist/components/Form/Checkbox.d.ts +36 -0
- package/dist/components/Form/Checkbox.d.ts.map +1 -0
- package/dist/components/Form/Checkbox.js +39 -0
- package/dist/components/Form/Fieldset.d.ts +33 -0
- package/dist/components/Form/Fieldset.d.ts.map +1 -0
- package/dist/components/Form/Fieldset.js +34 -0
- package/dist/components/Form/Input.d.ts +37 -0
- package/dist/components/Form/Input.d.ts.map +1 -0
- package/dist/components/Form/Input.js +41 -0
- package/dist/components/Form/Label.d.ts +30 -0
- package/dist/components/Form/Label.d.ts.map +1 -0
- package/dist/components/Form/Label.js +30 -0
- package/dist/components/Form/Radio.d.ts +53 -0
- package/dist/components/Form/Radio.d.ts.map +1 -0
- package/dist/components/Form/Radio.js +39 -0
- package/dist/components/Form/Select.d.ts +51 -0
- package/dist/components/Form/Select.d.ts.map +1 -0
- package/dist/components/Form/Select.js +49 -0
- package/dist/components/Form/Textarea.d.ts +44 -0
- package/dist/components/Form/Textarea.d.ts.map +1 -0
- package/dist/components/Form/Textarea.js +43 -0
- package/dist/components/Form/index.d.ts +8 -0
- package/dist/components/Form/index.d.ts.map +1 -0
- package/dist/components/Form/index.js +7 -0
- package/dist/components/Link/Link.d.ts +34 -0
- package/dist/components/Link/Link.d.ts.map +1 -0
- package/dist/components/Link/Link.js +48 -0
- package/dist/components/Link/index.d.ts +3 -0
- package/dist/components/Link/index.d.ts.map +1 -0
- package/dist/components/Link/index.js +1 -0
- package/dist/components/Modal/Modal.d.ts +64 -0
- package/dist/components/Modal/Modal.d.ts.map +1 -0
- package/dist/components/Modal/Modal.js +108 -0
- package/dist/components/Modal/index.d.ts +3 -0
- package/dist/components/Modal/index.d.ts.map +1 -0
- package/dist/components/Modal/index.js +1 -0
- package/dist/components/Tabs/Tabs.d.ts +63 -0
- package/dist/components/Tabs/Tabs.d.ts.map +1 -0
- package/dist/components/Tabs/Tabs.js +134 -0
- package/dist/components/Tabs/index.d.ts +3 -0
- package/dist/components/Tabs/index.d.ts.map +1 -0
- package/dist/components/Tabs/index.js +1 -0
- package/dist/components/Toast/Toast.d.ts +59 -0
- package/dist/components/Toast/Toast.d.ts.map +1 -0
- package/dist/components/Toast/Toast.js +91 -0
- package/dist/components/Toast/ToastProvider.d.ts +22 -0
- package/dist/components/Toast/ToastProvider.d.ts.map +1 -0
- package/dist/components/Toast/ToastProvider.js +33 -0
- package/dist/components/Toast/index.d.ts +5 -0
- package/dist/components/Toast/index.d.ts.map +1 -0
- package/dist/components/Toast/index.js +2 -0
- package/dist/hooks/useAriaLive.d.ts +9 -0
- package/dist/hooks/useAriaLive.d.ts.map +1 -0
- package/dist/hooks/useAriaLive.js +39 -0
- package/dist/hooks/useFocusReturn.d.ts +9 -0
- package/dist/hooks/useFocusReturn.d.ts.map +1 -0
- package/dist/hooks/useFocusReturn.js +33 -0
- package/dist/hooks/useFocusTrap.d.ts +9 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -0
- package/dist/hooks/useFocusTrap.js +68 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/{packages/design-system/src/index.ts → dist/index.js} +0 -4
- package/dist/styles/index.d.ts +3 -0
- package/dist/styles/index.d.ts.map +1 -0
- package/dist/styles/index.js +1 -0
- package/dist/tokens/breakpoints.d.ts +25 -0
- package/dist/tokens/breakpoints.d.ts.map +1 -0
- package/dist/tokens/breakpoints.js +23 -0
- package/dist/tokens/colors.d.ts +81 -0
- package/dist/tokens/colors.d.ts.map +1 -0
- package/dist/tokens/colors.js +86 -0
- package/dist/tokens/index.d.ts +6 -0
- package/dist/tokens/index.d.ts.map +1 -0
- package/dist/tokens/index.js +5 -0
- package/dist/tokens/motion.d.ts +30 -0
- package/dist/tokens/motion.d.ts.map +1 -0
- package/dist/tokens/motion.js +34 -0
- package/dist/tokens/spacing.d.ts +22 -0
- package/dist/tokens/spacing.d.ts.map +1 -0
- package/dist/tokens/spacing.js +20 -0
- package/dist/tokens/theme.d.ts +159 -0
- package/dist/tokens/theme.d.ts.map +1 -0
- package/dist/tokens/theme.js +15 -0
- package/dist/tokens/typography.d.ts +45 -0
- package/dist/tokens/typography.d.ts.map +1 -0
- package/dist/tokens/typography.js +56 -0
- package/dist/utils/aria.d.ts +60 -0
- package/dist/utils/aria.d.ts.map +1 -0
- package/dist/utils/aria.js +86 -0
- package/dist/utils/focus.d.ts +30 -0
- package/dist/utils/focus.d.ts.map +1 -0
- package/dist/utils/focus.js +80 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/keyboard.d.ts +38 -0
- package/dist/utils/keyboard.d.ts.map +1 -0
- package/dist/utils/keyboard.js +59 -0
- package/package.json +58 -31
- package/.storybook/custom.css +0 -69
- package/.storybook/main.ts +0 -46
- package/.storybook/manager.ts +0 -26
- package/.storybook/package.json +0 -6
- package/.storybook/preview.tsx +0 -31
- package/.storybook/public/logo.png +0 -0
- package/.storybook/vite.config.ts +0 -24
- package/.storybook/welcome.mdx +0 -97
- package/DEPLOYMENT.md +0 -154
- package/apps/web/app/(docs)/audit/audit.css +0 -269
- package/apps/web/app/(docs)/audit/page.tsx +0 -271
- package/apps/web/app/(docs)/components/button/page.tsx +0 -49
- package/apps/web/app/(docs)/components/form/page.tsx +0 -92
- package/apps/web/app/(docs)/components/link/page.tsx +0 -31
- package/apps/web/app/(docs)/components/modal/page.tsx +0 -41
- package/apps/web/app/(docs)/components/page.tsx +0 -37
- package/apps/web/app/(docs)/components/table/page.tsx +0 -54
- package/apps/web/app/(docs)/components/tabs/page.tsx +0 -61
- package/apps/web/app/(docs)/components/toast/page.tsx +0 -51
- package/apps/web/app/api/audit/route.ts +0 -128
- package/apps/web/app/favicon.ico +0 -0
- package/apps/web/app/layout.tsx +0 -20
- package/apps/web/app/page.tsx +0 -17
- package/apps/web/app/styles/globals.css +0 -5
- package/apps/web/next-env.d.ts +0 -5
- package/apps/web/next.config.js +0 -21
- package/apps/web/package.json +0 -28
- package/apps/web/public/_headers +0 -17
- package/apps/web/public/_redirects +0 -31
- package/apps/web/public/logo.png +0 -0
- package/apps/web/tsconfig.json +0 -29
- package/netlify/functions/audit.ts +0 -163
- package/netlify.toml +0 -37
- package/packages/design-system/README.md +0 -252
- package/packages/design-system/package.json +0 -68
- package/packages/design-system/scripts/copy-css.js +0 -63
- package/packages/design-system/src/components/Button/Button.stories.tsx +0 -228
- package/packages/design-system/src/components/Button/Button.tsx +0 -137
- package/packages/design-system/src/components/Button/index.ts +0 -3
- package/packages/design-system/src/components/DataTable/DataTable.stories.tsx +0 -211
- package/packages/design-system/src/components/DataTable/DataTable.tsx +0 -293
- package/packages/design-system/src/components/DataTable/index.ts +0 -3
- package/packages/design-system/src/components/Form/Checkbox.stories.tsx +0 -252
- package/packages/design-system/src/components/Form/Checkbox.tsx +0 -114
- package/packages/design-system/src/components/Form/Fieldset.stories.tsx +0 -210
- package/packages/design-system/src/components/Form/Fieldset.tsx +0 -71
- package/packages/design-system/src/components/Form/Input.stories.tsx +0 -164
- package/packages/design-system/src/components/Form/Input.tsx +0 -113
- package/packages/design-system/src/components/Form/Label.tsx +0 -56
- package/packages/design-system/src/components/Form/Radio.stories.tsx +0 -265
- package/packages/design-system/src/components/Form/Radio.tsx +0 -147
- package/packages/design-system/src/components/Form/Select.stories.tsx +0 -295
- package/packages/design-system/src/components/Form/Select.tsx +0 -160
- package/packages/design-system/src/components/Form/Textarea.stories.tsx +0 -253
- package/packages/design-system/src/components/Form/Textarea.tsx +0 -145
- package/packages/design-system/src/components/Form/index.ts +0 -8
- package/packages/design-system/src/components/Link/Link.stories.tsx +0 -128
- package/packages/design-system/src/components/Link/Link.tsx +0 -117
- package/packages/design-system/src/components/Link/index.ts +0 -3
- package/packages/design-system/src/components/Modal/Modal.stories.tsx +0 -165
- package/packages/design-system/src/components/Modal/Modal.tsx +0 -202
- package/packages/design-system/src/components/Modal/index.ts +0 -3
- package/packages/design-system/src/components/Tabs/Tabs.stories.tsx +0 -213
- package/packages/design-system/src/components/Tabs/Tabs.tsx +0 -248
- package/packages/design-system/src/components/Tabs/index.ts +0 -3
- package/packages/design-system/src/components/Toast/Toast.stories.tsx +0 -153
- package/packages/design-system/src/components/Toast/Toast.tsx +0 -175
- package/packages/design-system/src/components/Toast/ToastProvider.tsx +0 -73
- package/packages/design-system/src/components/Toast/index.ts +0 -5
- package/packages/design-system/src/hooks/useAriaLive.ts +0 -51
- package/packages/design-system/src/hooks/useFocusReturn.ts +0 -40
- package/packages/design-system/src/hooks/useFocusTrap.ts +0 -82
- package/packages/design-system/src/styles/index.ts +0 -3
- package/packages/design-system/src/tokens/breakpoints.ts +0 -28
- package/packages/design-system/src/tokens/colors.ts +0 -98
- package/packages/design-system/src/tokens/index.ts +0 -6
- package/packages/design-system/src/tokens/motion.ts +0 -41
- package/packages/design-system/src/tokens/spacing.ts +0 -24
- package/packages/design-system/src/tokens/theme.ts +0 -19
- package/packages/design-system/src/tokens/typography.ts +0 -64
- package/packages/design-system/src/utils/aria.ts +0 -108
- package/packages/design-system/src/utils/focus.ts +0 -87
- package/packages/design-system/src/utils/index.ts +0 -4
- package/packages/design-system/src/utils/keyboard.ts +0 -77
- package/packages/design-system/tsconfig.json +0 -17
- package/public/logo.png +0 -0
- package/scripts/fix-storybook-paths.js +0 -53
- package/tsconfig.json +0 -20
- /package/{packages/design-system/src → dist}/components/Button/Button.css +0 -0
- /package/{packages/design-system/src → dist}/components/DataTable/DataTable.css +0 -0
- /package/{packages/design-system/src → dist}/components/Form/Checkbox.css +0 -0
- /package/{packages/design-system/src → dist}/components/Form/Fieldset.css +0 -0
- /package/{packages/design-system/src → dist}/components/Form/Input.css +0 -0
- /package/{packages/design-system/src → dist}/components/Form/Label.css +0 -0
- /package/{packages/design-system/src → dist}/components/Form/Radio.css +0 -0
- /package/{packages/design-system/src → dist}/components/Form/Select.css +0 -0
- /package/{packages/design-system/src → dist}/components/Form/Textarea.css +0 -0
- /package/{packages/design-system/src → dist}/components/Link/Link.css +0 -0
- /package/{packages/design-system/src → dist}/components/Modal/Modal.css +0 -0
- /package/{packages/design-system/src → dist}/components/Tabs/Tabs.css +0 -0
- /package/{packages/design-system/src → dist}/components/Toast/Toast.css +0 -0
- /package/{packages/design-system/src → dist}/components/Toast/ToastProvider.css +0 -0
- /package/{packages/design-system/src → dist}/styles/components.css +0 -0
- /package/{packages/design-system/src → dist}/styles/global.css +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import './Textarea.css';
|
|
3
|
+
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
4
|
+
/**
|
|
5
|
+
* Error message to display
|
|
6
|
+
*/
|
|
7
|
+
error?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Helper text to display below the textarea
|
|
10
|
+
*/
|
|
11
|
+
helperText?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Label for the textarea
|
|
14
|
+
*/
|
|
15
|
+
label?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Maximum character count (shows counter)
|
|
18
|
+
*/
|
|
19
|
+
maxLength?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Whether to show character count
|
|
22
|
+
*/
|
|
23
|
+
showCount?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Accessible Textarea component
|
|
27
|
+
*
|
|
28
|
+
* WCAG Compliance:
|
|
29
|
+
* - 1.3.1 Info and Relationships: Proper label-textarea association
|
|
30
|
+
* - 4.1.2 Name, Role, Value: Proper ARIA attributes
|
|
31
|
+
* - 4.1.3 Status Messages: Error messages announced
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```tsx
|
|
35
|
+
* <Textarea
|
|
36
|
+
* id="message"
|
|
37
|
+
* label="Message"
|
|
38
|
+
* maxLength={500}
|
|
39
|
+
* showCount
|
|
40
|
+
* />
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare const Textarea: React.ForwardRefExoticComponent<TextareaProps & React.RefAttributes<HTMLTextAreaElement>>;
|
|
44
|
+
//# sourceMappingURL=Textarea.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Textarea.d.ts","sourceRoot":"","sources":["../../../src/components/Form/Textarea.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,gBAAgB,CAAA;AAEvB,MAAM,WAAW,aAAc,SAAQ,KAAK,CAAC,sBAAsB,CAAC,mBAAmB,CAAC;IACtF;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IAEd;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IAEd;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;OAEG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,QAAQ,2FA0FpB,CAAA"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { combineAriaDescribedBy } from '../../utils/aria';
|
|
5
|
+
import './Textarea.css';
|
|
6
|
+
/**
|
|
7
|
+
* Accessible Textarea component
|
|
8
|
+
*
|
|
9
|
+
* WCAG Compliance:
|
|
10
|
+
* - 1.3.1 Info and Relationships: Proper label-textarea association
|
|
11
|
+
* - 4.1.2 Name, Role, Value: Proper ARIA attributes
|
|
12
|
+
* - 4.1.3 Status Messages: Error messages announced
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <Textarea
|
|
17
|
+
* id="message"
|
|
18
|
+
* label="Message"
|
|
19
|
+
* maxLength={500}
|
|
20
|
+
* showCount
|
|
21
|
+
* />
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export const Textarea = React.forwardRef(({ id, error, helperText, label, maxLength, showCount = false, className = '', value, 'aria-describedby': ariaDescribedBy, ...props }, ref) => {
|
|
25
|
+
const textareaId = React.useId();
|
|
26
|
+
const finalId = id || `textarea-${textareaId}`;
|
|
27
|
+
const errorId = error ? `${finalId}-error` : undefined;
|
|
28
|
+
const helperId = helperText ? `${finalId}-helper` : undefined;
|
|
29
|
+
const countId = showCount ? `${finalId}-count` : undefined;
|
|
30
|
+
const describedBy = combineAriaDescribedBy(ariaDescribedBy, errorId, helperId, countId);
|
|
31
|
+
const currentLength = typeof value === 'string' ? value.length : 0;
|
|
32
|
+
const remainingChars = maxLength ? maxLength - currentLength : undefined;
|
|
33
|
+
const classes = [
|
|
34
|
+
'form-textarea',
|
|
35
|
+
error && 'form-textarea--error',
|
|
36
|
+
props.disabled && 'form-textarea--disabled',
|
|
37
|
+
className,
|
|
38
|
+
]
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.join(' ');
|
|
41
|
+
return (_jsxs("div", { className: "form-textarea-wrapper", children: [label && (_jsxs("label", { htmlFor: finalId, className: "form-label", children: [label, props.required && (_jsxs("span", { className: "form-label__required", "aria-hidden": "true", children: [' ', "*"] }))] })), _jsx("textarea", { ref: ref, id: finalId, className: classes, maxLength: maxLength, value: value, "aria-invalid": error ? true : undefined, "aria-describedby": describedBy, required: props.required ? true : undefined, ...props }), (showCount || helperText) && (_jsxs("div", { className: "form-textarea-footer", children: [helperText && !error && (_jsx("span", { id: helperId, className: "form-helper-text", children: helperText })), error && (_jsx("span", { id: errorId, className: "form-error-text", role: "alert", children: error })), showCount && maxLength && (_jsxs("span", { id: countId, className: "form-character-count", "aria-live": "polite", children: [currentLength, " / ", maxLength] }))] }))] }));
|
|
42
|
+
});
|
|
43
|
+
Textarea.displayName = 'Textarea';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Form/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAA;AACvB,cAAc,YAAY,CAAA;AAC1B,cAAc,UAAU,CAAA;AACxB,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA;AACvB,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import './Link.css';
|
|
3
|
+
export interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
4
|
+
/**
|
|
5
|
+
* Whether this is an external link
|
|
6
|
+
* Automatically adds rel="noopener noreferrer" for security
|
|
7
|
+
*/
|
|
8
|
+
external?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Whether this is a skip link (for keyboard navigation)
|
|
11
|
+
*/
|
|
12
|
+
skip?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* ARIA label for the link (required if no visible text)
|
|
15
|
+
*/
|
|
16
|
+
'aria-label'?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Accessible Link component
|
|
20
|
+
*
|
|
21
|
+
* WCAG Compliance:
|
|
22
|
+
* - 2.4.4 Link Purpose: Clear link text or aria-label
|
|
23
|
+
* - 2.4.7 Focus Visible: Clear focus indicators
|
|
24
|
+
* - 4.1.2 Name, Role, Value: Proper semantic HTML
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* <Link href="/about" external>
|
|
29
|
+
* Learn more
|
|
30
|
+
* </Link>
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare const Link: React.ForwardRefExoticComponent<LinkProps & React.RefAttributes<HTMLAnchorElement>>;
|
|
34
|
+
//# sourceMappingURL=Link.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Link.d.ts","sourceRoot":"","sources":["../../../src/components/Link/Link.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,YAAY,CAAA;AAEnB,MAAM,WAAW,SAAU,SAAQ,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC;IAC9E;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAElB;;OAEG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;IAEd;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,IAAI,qFA2EhB,CAAA"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import './Link.css';
|
|
5
|
+
/**
|
|
6
|
+
* Accessible Link component
|
|
7
|
+
*
|
|
8
|
+
* WCAG Compliance:
|
|
9
|
+
* - 2.4.4 Link Purpose: Clear link text or aria-label
|
|
10
|
+
* - 2.4.7 Focus Visible: Clear focus indicators
|
|
11
|
+
* - 4.1.2 Name, Role, Value: Proper semantic HTML
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* <Link href="/about" external>
|
|
16
|
+
* Learn more
|
|
17
|
+
* </Link>
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export const Link = React.forwardRef(({ external = false, skip = false, href, rel, target, className = '', children, 'aria-label': ariaLabel, ...props }, ref) => {
|
|
21
|
+
// Determine if link is external based on href or explicit prop
|
|
22
|
+
const isExternal = external ||
|
|
23
|
+
(href && (href.startsWith('http') || href.startsWith('//')));
|
|
24
|
+
// Build rel attribute
|
|
25
|
+
const relAttributes = React.useMemo(() => {
|
|
26
|
+
const attrs = new Set(rel?.split(' ') || []);
|
|
27
|
+
if (isExternal) {
|
|
28
|
+
attrs.add('noopener');
|
|
29
|
+
attrs.add('noreferrer');
|
|
30
|
+
}
|
|
31
|
+
return Array.from(attrs).join(' ');
|
|
32
|
+
}, [isExternal, rel]);
|
|
33
|
+
// Set target for external links
|
|
34
|
+
const linkTarget = isExternal && !target ? '_blank' : target;
|
|
35
|
+
const classes = [
|
|
36
|
+
'link',
|
|
37
|
+
skip && 'link--skip',
|
|
38
|
+
className,
|
|
39
|
+
]
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
.join(' ');
|
|
42
|
+
// Skip links should use button semantics if no href
|
|
43
|
+
if (skip && !href) {
|
|
44
|
+
return (_jsx("button", { ref: ref, className: classes, "aria-label": ariaLabel, ...props, children: children }));
|
|
45
|
+
}
|
|
46
|
+
return (_jsxs("a", { ref: ref, href: href, rel: relAttributes || undefined, target: linkTarget, className: classes, "aria-label": ariaLabel, ...props, children: [children, isExternal && (_jsxs("span", { className: "link__external-icon", "aria-hidden": "true", children: [' ', _jsx("span", { "aria-label": "(opens in new tab)", children: "\u2197" })] }))] }));
|
|
47
|
+
});
|
|
48
|
+
Link.displayName = 'Link';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Link/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAC7B,YAAY,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Link } from './Link';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import './Modal.css';
|
|
3
|
+
export interface ModalProps {
|
|
4
|
+
/**
|
|
5
|
+
* Whether the modal is open
|
|
6
|
+
*/
|
|
7
|
+
isOpen: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Callback when modal should close
|
|
10
|
+
*/
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
/**
|
|
13
|
+
* Title of the modal (required for accessibility)
|
|
14
|
+
*/
|
|
15
|
+
title: string;
|
|
16
|
+
/**
|
|
17
|
+
* Content of the modal
|
|
18
|
+
*/
|
|
19
|
+
children: React.ReactNode;
|
|
20
|
+
/**
|
|
21
|
+
* Whether to close on backdrop click
|
|
22
|
+
*/
|
|
23
|
+
closeOnBackdropClick?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Whether to close on ESC key press
|
|
26
|
+
*/
|
|
27
|
+
closeOnEscape?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Size of the modal
|
|
30
|
+
*/
|
|
31
|
+
size?: 'sm' | 'md' | 'lg' | 'full';
|
|
32
|
+
/**
|
|
33
|
+
* Element to return focus to when modal closes
|
|
34
|
+
*/
|
|
35
|
+
returnFocusTo?: HTMLElement | null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Accessible Modal component using HTML5 dialog element
|
|
39
|
+
*
|
|
40
|
+
* Uses the native `<dialog>` element which provides:
|
|
41
|
+
* - Built-in focus management and focus trapping
|
|
42
|
+
* - Automatic body scroll prevention
|
|
43
|
+
* - Native backdrop overlay
|
|
44
|
+
* - ESC key handling (configurable)
|
|
45
|
+
*
|
|
46
|
+
* WCAG Compliance:
|
|
47
|
+
* - 2.1.1 Keyboard: ESC key support, built-in focus trap
|
|
48
|
+
* - 2.1.2 No Keyboard Trap: Focus returns to trigger
|
|
49
|
+
* - 2.4.3 Focus Order: Focus trapped within modal (native behavior)
|
|
50
|
+
* - 4.1.2 Name, Role, Value: ARIA modal pattern
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* <Modal
|
|
55
|
+
* isOpen={isOpen}
|
|
56
|
+
* onClose={() => setIsOpen(false)}
|
|
57
|
+
* title="Confirm Action"
|
|
58
|
+
* >
|
|
59
|
+
* <p>Are you sure?</p>
|
|
60
|
+
* </Modal>
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare const Modal: React.FC<ModalProps>;
|
|
64
|
+
//# sourceMappingURL=Modal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Modal.d.ts","sourceRoot":"","sources":["../../../src/components/Modal/Modal.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA4B,MAAM,OAAO,CAAA;AAGhD,OAAO,aAAa,CAAA;AAEpB,MAAM,WAAW,UAAU;IACzB;;OAEG;IACH,MAAM,EAAE,OAAO,CAAA;IAEf;;OAEG;IACH,OAAO,EAAE,MAAM,IAAI,CAAA;IAEnB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAA;IAEb;;OAEG;IACH,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IAEzB;;OAEG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAE9B;;OAEG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IAEvB;;OAEG;IACH,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,MAAM,CAAA;IAElC;;OAEG;IACH,aAAa,CAAC,EAAE,WAAW,GAAG,IAAI,CAAA;CACnC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,UAAU,CA2HtC,CAAA"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import React, { useEffect, useRef } from 'react';
|
|
4
|
+
import { useFocusReturn } from '../../hooks/useFocusReturn';
|
|
5
|
+
import { Button } from '../Button/Button';
|
|
6
|
+
import './Modal.css';
|
|
7
|
+
/**
|
|
8
|
+
* Accessible Modal component using HTML5 dialog element
|
|
9
|
+
*
|
|
10
|
+
* Uses the native `<dialog>` element which provides:
|
|
11
|
+
* - Built-in focus management and focus trapping
|
|
12
|
+
* - Automatic body scroll prevention
|
|
13
|
+
* - Native backdrop overlay
|
|
14
|
+
* - ESC key handling (configurable)
|
|
15
|
+
*
|
|
16
|
+
* WCAG Compliance:
|
|
17
|
+
* - 2.1.1 Keyboard: ESC key support, built-in focus trap
|
|
18
|
+
* - 2.1.2 No Keyboard Trap: Focus returns to trigger
|
|
19
|
+
* - 2.4.3 Focus Order: Focus trapped within modal (native behavior)
|
|
20
|
+
* - 4.1.2 Name, Role, Value: ARIA modal pattern
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* <Modal
|
|
25
|
+
* isOpen={isOpen}
|
|
26
|
+
* onClose={() => setIsOpen(false)}
|
|
27
|
+
* title="Confirm Action"
|
|
28
|
+
* >
|
|
29
|
+
* <p>Are you sure?</p>
|
|
30
|
+
* </Modal>
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export const Modal = ({ isOpen, onClose, title, children, closeOnBackdropClick = true, closeOnEscape = true, size = 'md', returnFocusTo, }) => {
|
|
34
|
+
const dialogRef = useRef(null);
|
|
35
|
+
const contentRef = useRef(null);
|
|
36
|
+
const titleId = React.useId();
|
|
37
|
+
const descriptionId = React.useId();
|
|
38
|
+
// Return focus on close
|
|
39
|
+
useFocusReturn(isOpen, returnFocusTo);
|
|
40
|
+
// Handle dialog open/close
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const dialog = dialogRef.current;
|
|
43
|
+
if (!dialog)
|
|
44
|
+
return;
|
|
45
|
+
if (isOpen) {
|
|
46
|
+
// Show modal dialog
|
|
47
|
+
dialog.showModal();
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// Close dialog
|
|
51
|
+
dialog.close();
|
|
52
|
+
}
|
|
53
|
+
return () => {
|
|
54
|
+
// Cleanup: ensure dialog is closed when component unmounts
|
|
55
|
+
if (dialog.open) {
|
|
56
|
+
dialog.close();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}, [isOpen]);
|
|
60
|
+
// Handle backdrop clicks
|
|
61
|
+
// The ::backdrop pseudo-element doesn't bubble events to the dialog element,
|
|
62
|
+
// so we need to listen for clicks on the document and check if they're outside
|
|
63
|
+
// the dialog content area.
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!isOpen || !closeOnBackdropClick)
|
|
66
|
+
return;
|
|
67
|
+
const handleDocumentClick = (event) => {
|
|
68
|
+
const dialog = dialogRef.current;
|
|
69
|
+
const content = contentRef.current;
|
|
70
|
+
if (!dialog || !content)
|
|
71
|
+
return;
|
|
72
|
+
// Check if click target is outside the dialog content area
|
|
73
|
+
const target = event.target;
|
|
74
|
+
// If the click is not inside the content wrapper, it's a backdrop click
|
|
75
|
+
if (!content.contains(target)) {
|
|
76
|
+
// Verify click coordinates are outside content bounds for extra safety
|
|
77
|
+
const rect = content.getBoundingClientRect();
|
|
78
|
+
const clickX = event.clientX;
|
|
79
|
+
const clickY = event.clientY;
|
|
80
|
+
const isOutsideContent = clickX < rect.left ||
|
|
81
|
+
clickX > rect.right ||
|
|
82
|
+
clickY < rect.top ||
|
|
83
|
+
clickY > rect.bottom;
|
|
84
|
+
if (isOutsideContent) {
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
event.stopPropagation();
|
|
87
|
+
onClose();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
// Use capture phase to catch events before they bubble
|
|
92
|
+
document.addEventListener('mousedown', handleDocumentClick, true);
|
|
93
|
+
return () => {
|
|
94
|
+
document.removeEventListener('mousedown', handleDocumentClick, true);
|
|
95
|
+
};
|
|
96
|
+
}, [isOpen, closeOnBackdropClick, onClose]);
|
|
97
|
+
// Handle cancel event (fires when ESC key is pressed)
|
|
98
|
+
const handleCancel = (event) => {
|
|
99
|
+
// Prevent default close behavior
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
// Only close if closeOnEscape is enabled
|
|
102
|
+
if (closeOnEscape) {
|
|
103
|
+
onClose();
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
return (_jsx("dialog", { ref: dialogRef, className: `modal modal--${size} ${isOpen ? 'modal--open' : ''}`, "aria-labelledby": titleId, "aria-describedby": descriptionId, onCancel: handleCancel, children: _jsxs("div", { ref: contentRef, className: "modal-content-wrapper", children: [_jsxs("div", { className: "modal-header", children: [_jsx("h2", { id: titleId, className: "modal-title", children: title }), _jsx(Button, { variant: "ghost", size: "sm", onClick: onClose, "aria-label": "Close modal", className: "modal-close", children: "\u00D7" })] }), _jsx("div", { id: descriptionId, className: "modal-content", children: children })] }) }));
|
|
107
|
+
};
|
|
108
|
+
Modal.displayName = 'Modal';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Modal/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/B,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Modal } from './Modal';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import './Tabs.css';
|
|
3
|
+
export interface TabItem {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
content: React.ReactNode;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface TabsProps {
|
|
10
|
+
/**
|
|
11
|
+
* Tab items
|
|
12
|
+
*/
|
|
13
|
+
items: TabItem[];
|
|
14
|
+
/**
|
|
15
|
+
* Default selected tab ID
|
|
16
|
+
*/
|
|
17
|
+
defaultSelectedId?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Controlled selected tab ID
|
|
20
|
+
*/
|
|
21
|
+
selectedId?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Callback when tab selection changes
|
|
24
|
+
*/
|
|
25
|
+
onSelectionChange?: (id: string) => void;
|
|
26
|
+
/**
|
|
27
|
+
* Orientation of tabs
|
|
28
|
+
*/
|
|
29
|
+
orientation?: 'horizontal' | 'vertical';
|
|
30
|
+
/**
|
|
31
|
+
* Activation mode for tabs
|
|
32
|
+
* - 'automatic': Arrow keys both move focus and activate tabs immediately
|
|
33
|
+
* - 'manual': Arrow keys move focus only, Enter/Space activates the focused tab
|
|
34
|
+
* @default 'automatic'
|
|
35
|
+
*/
|
|
36
|
+
activationMode?: 'automatic' | 'manual';
|
|
37
|
+
/**
|
|
38
|
+
* Label for the tab list (required for accessibility)
|
|
39
|
+
*/
|
|
40
|
+
'aria-label'?: string;
|
|
41
|
+
'aria-labelledby'?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Accessible Tabs component
|
|
45
|
+
*
|
|
46
|
+
* WCAG Compliance:
|
|
47
|
+
* - 2.1.1 Keyboard: Arrow key navigation, Home/End support
|
|
48
|
+
* - 4.1.2 Name, Role, Value: ARIA tabs pattern
|
|
49
|
+
* - 2.4.3 Focus Order: Proper focus management
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```tsx
|
|
53
|
+
* <Tabs
|
|
54
|
+
* items={[
|
|
55
|
+
* { id: 'tab1', label: 'Tab 1', content: <div>Content 1</div> },
|
|
56
|
+
* { id: 'tab2', label: 'Tab 2', content: <div>Content 2</div> },
|
|
57
|
+
* ]}
|
|
58
|
+
* aria-label="Settings tabs"
|
|
59
|
+
* />
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export declare const Tabs: React.FC<TabsProps>;
|
|
63
|
+
//# sourceMappingURL=Tabs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Tabs.d.ts","sourceRoot":"","sources":["../../../src/components/Tabs/Tabs.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAwC,MAAM,OAAO,CAAA;AAG5D,OAAO,YAAY,CAAA;AAEnB,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,KAAK,CAAC,SAAS,CAAA;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,SAAS;IACxB;;OAEG;IACH,KAAK,EAAE,OAAO,EAAE,CAAA;IAEhB;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAE1B;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB;;OAEG;IACH,iBAAiB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IAExC;;OAEG;IACH,WAAW,CAAC,EAAE,YAAY,GAAG,UAAU,CAAA;IAEvC;;;;;OAKG;IACH,cAAc,CAAC,EAAE,WAAW,GAAG,QAAQ,CAAA;IAEvC;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,SAAS,CA0KpC,CAAA"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useCallback, useRef } from 'react';
|
|
4
|
+
import { isNavigationKey, isArrowKey } from '../../utils/keyboard';
|
|
5
|
+
import { getCurrentAttributes } from '../../utils/aria';
|
|
6
|
+
import './Tabs.css';
|
|
7
|
+
/**
|
|
8
|
+
* Accessible Tabs component
|
|
9
|
+
*
|
|
10
|
+
* WCAG Compliance:
|
|
11
|
+
* - 2.1.1 Keyboard: Arrow key navigation, Home/End support
|
|
12
|
+
* - 4.1.2 Name, Role, Value: ARIA tabs pattern
|
|
13
|
+
* - 2.4.3 Focus Order: Proper focus management
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* <Tabs
|
|
18
|
+
* items={[
|
|
19
|
+
* { id: 'tab1', label: 'Tab 1', content: <div>Content 1</div> },
|
|
20
|
+
* { id: 'tab2', label: 'Tab 2', content: <div>Content 2</div> },
|
|
21
|
+
* ]}
|
|
22
|
+
* aria-label="Settings tabs"
|
|
23
|
+
* />
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export const Tabs = ({ items, defaultSelectedId, selectedId: controlledSelectedId, onSelectionChange, orientation = 'horizontal', activationMode = 'automatic', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, }) => {
|
|
27
|
+
const initialSelectedId = defaultSelectedId || items[0]?.id;
|
|
28
|
+
const [internalSelectedId, setInternalSelectedId] = useState(initialSelectedId);
|
|
29
|
+
const [focusedId, setFocusedId] = useState(initialSelectedId);
|
|
30
|
+
const tabRefs = useRef(new Map());
|
|
31
|
+
const selectedId = controlledSelectedId ?? internalSelectedId;
|
|
32
|
+
const selectedIndex = items.findIndex((item) => item.id === selectedId);
|
|
33
|
+
// In automatic mode, focused tab is always the selected tab
|
|
34
|
+
// In manual mode, focused tab can be different from selected tab
|
|
35
|
+
const effectiveFocusedId = activationMode === 'automatic' ? selectedId : (focusedId || selectedId);
|
|
36
|
+
const handleSelect = useCallback((id) => {
|
|
37
|
+
if (onSelectionChange) {
|
|
38
|
+
onSelectionChange(id);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
setInternalSelectedId(id);
|
|
42
|
+
}
|
|
43
|
+
// In manual mode, update focused tab when selecting
|
|
44
|
+
if (activationMode === 'manual') {
|
|
45
|
+
setFocusedId(id);
|
|
46
|
+
}
|
|
47
|
+
}, [onSelectionChange, activationMode]);
|
|
48
|
+
const handleKeyDown = useCallback((event, currentIndex) => {
|
|
49
|
+
const isHorizontal = orientation === 'horizontal';
|
|
50
|
+
let newIndex = currentIndex;
|
|
51
|
+
// Handle Enter/Space for manual activation
|
|
52
|
+
if (activationMode === 'manual' && (event.key === 'Enter' || event.key === ' ')) {
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
const currentTab = items[currentIndex];
|
|
55
|
+
if (currentTab && !currentTab.disabled) {
|
|
56
|
+
handleSelect(currentTab.id);
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// Handle arrow keys and Home/End
|
|
61
|
+
if (isNavigationKey(event.key) || isArrowKey(event.key)) {
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
switch (event.key) {
|
|
64
|
+
case 'Home':
|
|
65
|
+
newIndex = 0;
|
|
66
|
+
break;
|
|
67
|
+
case 'End':
|
|
68
|
+
newIndex = items.length - 1;
|
|
69
|
+
break;
|
|
70
|
+
case 'ArrowRight':
|
|
71
|
+
if (isHorizontal) {
|
|
72
|
+
newIndex = (currentIndex + 1) % items.length;
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
case 'ArrowLeft':
|
|
76
|
+
if (isHorizontal) {
|
|
77
|
+
newIndex = (currentIndex - 1 + items.length) % items.length;
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
case 'ArrowDown':
|
|
81
|
+
if (!isHorizontal) {
|
|
82
|
+
newIndex = (currentIndex + 1) % items.length;
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
case 'ArrowUp':
|
|
86
|
+
if (!isHorizontal) {
|
|
87
|
+
newIndex = (currentIndex - 1 + items.length) % items.length;
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
// Skip disabled tabs
|
|
92
|
+
while (items[newIndex]?.disabled && newIndex !== currentIndex) {
|
|
93
|
+
if (event.key === 'Home' || event.key === 'ArrowRight' || event.key === 'ArrowDown') {
|
|
94
|
+
newIndex = (newIndex + 1) % items.length;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
newIndex = (newIndex - 1 + items.length) % items.length;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const newTab = items[newIndex];
|
|
101
|
+
if (newTab && !newTab.disabled) {
|
|
102
|
+
if (activationMode === 'automatic') {
|
|
103
|
+
// Automatic: move focus and activate
|
|
104
|
+
handleSelect(newTab.id);
|
|
105
|
+
tabRefs.current.get(newTab.id)?.focus();
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// Manual: move focus only
|
|
109
|
+
setFocusedId(newTab.id);
|
|
110
|
+
tabRefs.current.get(newTab.id)?.focus();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}, [items, orientation, activationMode, handleSelect]);
|
|
115
|
+
const selectedTab = items.find((item) => item.id === selectedId);
|
|
116
|
+
return (_jsxs("div", { className: `tabs tabs--${orientation}`, children: [_jsx("div", { className: "tabs-list", role: "tablist", "aria-orientation": orientation, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, children: items.map((item, index) => {
|
|
117
|
+
const isSelected = item.id === selectedId;
|
|
118
|
+
const isFocused = item.id === effectiveFocusedId;
|
|
119
|
+
// In manual mode, focused tab should be focusable even if not selected
|
|
120
|
+
// In automatic mode, only selected tab is focusable
|
|
121
|
+
const tabIndex = activationMode === 'manual'
|
|
122
|
+
? (isFocused ? 0 : -1)
|
|
123
|
+
: (isSelected ? 0 : -1);
|
|
124
|
+
return (_jsx("button", { ref: (el) => {
|
|
125
|
+
if (el) {
|
|
126
|
+
tabRefs.current.set(item.id, el);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
tabRefs.current.delete(item.id);
|
|
130
|
+
}
|
|
131
|
+
}, id: `tab-${item.id}`, role: "tab", "aria-controls": `tabpanel-${item.id}`, "aria-selected": isSelected, tabIndex: tabIndex, disabled: item.disabled, className: `tabs-tab ${isSelected ? 'tabs-tab--selected' : ''} ${item.disabled ? 'tabs-tab--disabled' : ''}`, onClick: () => !item.disabled && handleSelect(item.id), onKeyDown: (e) => handleKeyDown(e, index), onFocus: () => setFocusedId(item.id), ...getCurrentAttributes(isSelected ? 'page' : undefined), children: item.label }, item.id));
|
|
132
|
+
}) }), selectedTab && (_jsx("div", { id: `tabpanel-${selectedTab.id}`, role: "tabpanel", "aria-labelledby": `tab-${selectedTab.id}`, className: "tabs-panel", children: selectedTab.content }))] }));
|
|
133
|
+
};
|
|
134
|
+
Tabs.displayName = 'Tabs';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Tabs/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAC7B,YAAY,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Tabs } from './Tabs';
|