@object-ui/components 0.5.0 → 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.
- package/.turbo/turbo-build.log +12 -25
- package/CHANGELOG.md +32 -0
- package/dist/index.css +1 -1
- package/dist/index.js +23987 -22576
- package/dist/index.umd.cjs +30 -30
- package/dist/src/custom/action-param-dialog.d.ts +21 -0
- package/dist/src/custom/index.d.ts +4 -0
- package/dist/src/custom/navigation-overlay.d.ts +50 -0
- package/dist/src/custom/view-skeleton.d.ts +37 -0
- package/dist/src/custom/view-states.d.ts +33 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/renderers/action/action-button.d.ts +11 -0
- package/dist/src/renderers/action/action-group.d.ts +25 -0
- package/dist/src/renderers/action/action-icon.d.ts +10 -0
- package/dist/src/renderers/action/action-menu.d.ts +19 -0
- package/dist/src/renderers/action/index.d.ts +0 -0
- package/dist/src/renderers/action/resolve-icon.d.ts +6 -0
- package/package.json +20 -19
- package/src/__tests__/PageRendererRegions.test.tsx +664 -55
- package/src/__tests__/__snapshots__/snapshot-critical.test.tsx.snap +811 -0
- package/src/__tests__/__snapshots__/snapshot.test.tsx.snap +327 -0
- package/src/__tests__/accessibility.test.tsx +137 -0
- package/src/__tests__/api-consistency.test.tsx +596 -0
- package/src/__tests__/color-contrast.test.tsx +212 -0
- package/src/__tests__/compliance.test.tsx +72 -0
- package/src/__tests__/edge-cases.test.tsx +285 -0
- package/src/__tests__/navigation-overlay.test.tsx +273 -0
- package/src/__tests__/snapshot-critical.test.tsx +317 -0
- package/src/__tests__/snapshot.test.tsx +205 -0
- package/src/__tests__/view-compliance.test.tsx +153 -0
- package/src/__tests__/wcag-audit.test.tsx +493 -0
- package/src/custom/action-param-dialog.tsx +264 -0
- package/src/custom/index.ts +4 -0
- package/src/custom/navigation-overlay.tsx +296 -0
- package/src/custom/view-skeleton.tsx +243 -0
- package/src/custom/view-states.tsx +153 -0
- package/src/index.ts +1 -0
- package/src/renderers/action/action-button.tsx +147 -0
- package/src/renderers/action/action-group.tsx +270 -0
- package/src/renderers/action/action-icon.tsx +150 -0
- package/src/renderers/action/action-menu.tsx +203 -0
- package/src/renderers/action/index.ts +18 -0
- package/src/renderers/action/resolve-icon.ts +35 -0
- package/src/renderers/complex/__tests__/data-table-batch-editing.test.tsx +275 -0
- package/src/renderers/complex/__tests__/data-table-cell-renderer.test.tsx +120 -0
- package/src/renderers/complex/__tests__/data-table-editing.test.tsx +221 -0
- package/src/renderers/complex/data-table.tsx +269 -33
- package/src/renderers/complex/resizable.tsx +20 -17
- package/src/renderers/data-display/list.tsx +1 -1
- package/src/renderers/data-display/table.tsx +1 -1
- package/src/renderers/data-display/tree-view.tsx +2 -1
- package/src/renderers/form/form.tsx +33 -10
- package/src/renderers/index.ts +1 -0
- package/src/renderers/layout/aspect-ratio.tsx +1 -1
- package/src/renderers/layout/page.tsx +416 -52
- package/src/renderers/navigation/sidebar.tsx +6 -0
- package/src/renderers/placeholders.tsx +2 -2
- package/src/stories/MockedData.stories.tsx +87 -37
- package/src/stories-json/Accessibility.mdx +297 -0
- package/src/stories-json/EdgeCases.stories.tsx +160 -0
- package/src/stories-json/GettingStarted.mdx +89 -0
- package/src/stories-json/Introduction.mdx +127 -0
- package/src/stories-json/accordion.stories.tsx +1 -1
- package/src/stories-json/aggrid.stories.tsx +1 -1
- package/src/stories-json/alert.stories.tsx +1 -1
- package/src/stories-json/aspect-ratio.stories.tsx +1 -1
- package/src/stories-json/avatar.stories.tsx +1 -1
- package/src/stories-json/badge.stories.tsx +1 -1
- package/src/stories-json/breadcrumb.stories.tsx +1 -1
- package/src/stories-json/button-group.stories.tsx +1 -1
- package/src/stories-json/button.stories.tsx +1 -1
- package/src/stories-json/calendar.stories.tsx +1 -1
- package/src/stories-json/card.stories.tsx +1 -1
- package/src/stories-json/carousel.stories.tsx +1 -1
- package/src/stories-json/charts.stories.tsx +1 -1
- package/src/stories-json/chatbot.stories.tsx +1 -1
- package/src/stories-json/code-editor.stories.tsx +1 -1
- package/src/stories-json/collapsible.stories.tsx +1 -1
- package/src/stories-json/controls.stories.tsx +1 -1
- package/src/stories-json/crm-live-data.stories.tsx +154 -0
- package/src/stories-json/data-table.stories.tsx +80 -4
- package/src/stories-json/data_display_extras.stories.tsx +1 -1
- package/src/stories-json/date-picker.stories.tsx +1 -1
- package/src/stories-json/detail-view.stories.tsx +1 -1
- package/src/stories-json/dialog.stories.tsx +1 -1
- package/src/stories-json/feedback_extras.stories.tsx +1 -1
- package/src/stories-json/feedback_others.stories.tsx +1 -1
- package/src/stories-json/form-variants.stories.tsx +210 -0
- package/src/stories-json/form_advanced.stories.tsx +1 -1
- package/src/stories-json/form_extras.stories.tsx +1 -1
- package/src/stories-json/grid.stories.tsx +1 -1
- package/src/stories-json/icon.stories.tsx +1 -1
- package/src/stories-json/input.stories.tsx +1 -1
- package/src/stories-json/kanban.stories.tsx +1 -1
- package/src/stories-json/layout_extended.stories.tsx +1 -1
- package/src/stories-json/layout_flex.stories.tsx +1 -1
- package/src/stories-json/list-view.stories.tsx +1 -1
- package/src/stories-json/markdown.stories.tsx +1 -1
- package/src/stories-json/menus.stories.tsx +1 -1
- package/src/stories-json/metric-card.stories.tsx +1 -1
- package/src/stories-json/navigation-menu.stories.tsx +1 -1
- package/src/stories-json/object-aggrid-advanced.stories.tsx +389 -0
- package/src/stories-json/object-aggrid.stories.tsx +1 -1
- package/src/stories-json/object-form.stories.tsx +1 -1
- package/src/stories-json/object-gantt.stories.tsx +1 -1
- package/src/stories-json/object-grid.stories.tsx +159 -1
- package/src/stories-json/object-map.stories.tsx +1 -1
- package/src/stories-json/object-view.stories.tsx +1 -1
- package/src/stories-json/overlay_extras.stories.tsx +1 -1
- package/src/stories-json/overlay_others.stories.tsx +1 -1
- package/src/stories-json/resizable.stories.tsx +1 -1
- package/src/stories-json/select.stories.tsx +1 -1
- package/src/stories-json/separator.stories.tsx +1 -1
- package/src/stories-json/statistic.stories.tsx +1 -1
- package/src/stories-json/tabs.stories.tsx +1 -1
- package/src/stories-json/timeline.stories.tsx +1 -1
- package/src/stories-json/typography.stories.tsx +1 -1
- package/src/ui/slider.tsx +6 -2
- package/src/stories/Introduction.mdx +0 -34
|
@@ -1,71 +1,121 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
-
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import React, { useEffect, useState, useCallback } from 'react';
|
|
3
3
|
import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react';
|
|
4
|
-
import {
|
|
4
|
+
import type { BaseSchema } from '@object-ui/types';
|
|
5
|
+
import { createStorybookDataSource } from '@storybook-config/datasource';
|
|
5
6
|
|
|
6
|
-
//
|
|
7
|
-
|
|
7
|
+
// ==========================================
|
|
8
|
+
// Story 1: Static Data Binding
|
|
9
|
+
// ==========================================
|
|
10
|
+
// Demonstrates how JSON schemas can reference data via expressions like ${data.field}
|
|
11
|
+
|
|
12
|
+
const staticDataSchema: BaseSchema = {
|
|
8
13
|
type: "page",
|
|
9
|
-
props: { title: "
|
|
14
|
+
props: { title: "Static Data Binding" },
|
|
10
15
|
children: [
|
|
11
16
|
{
|
|
12
17
|
type: "page:header",
|
|
13
18
|
props: {
|
|
14
|
-
title: "
|
|
15
|
-
description: "
|
|
19
|
+
title: "Sarah Connors",
|
|
20
|
+
description: "Lead Engineer at Cyberdyne Systems"
|
|
16
21
|
}
|
|
17
22
|
},
|
|
18
23
|
{
|
|
19
|
-
type: "
|
|
20
|
-
props: {
|
|
24
|
+
type: "grid",
|
|
25
|
+
props: { cols: 2, gap: 4, className: "mt-4" },
|
|
21
26
|
children: [
|
|
22
|
-
{ type: "
|
|
23
|
-
{ type: "
|
|
24
|
-
{ type: "
|
|
27
|
+
{ type: "card", title: "Full Name", children: [{ type: "text", content: "Sarah Connors" }] },
|
|
28
|
+
{ type: "card", title: "Email", children: [{ type: "text", content: "sarah@cyberdyne.com" }] },
|
|
29
|
+
{ type: "card", title: "Status", children: [{ type: "badge", props: { variant: "default", children: "Active" } }] },
|
|
30
|
+
{ type: "card", title: "Department", children: [{ type: "text", content: "Engineering" }] },
|
|
25
31
|
]
|
|
26
32
|
}
|
|
27
33
|
]
|
|
28
34
|
};
|
|
29
35
|
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
// ==========================================
|
|
37
|
+
// Story 2: MSW-Backed Data (Live API)
|
|
38
|
+
// ==========================================
|
|
39
|
+
// The ObjectStack kernel seeds CRM data via MSW. This story fetches contacts from /api/v1.
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
// Simulate a fetch call locally since MSW might not be configured in this environment
|
|
36
|
-
const mockData = {
|
|
37
|
-
name: "Sarah Connors",
|
|
38
|
-
title: "Lead Engineer",
|
|
39
|
-
company: "Cyberdyne Systems",
|
|
40
|
-
email: "sarah@cyberdyne.com",
|
|
41
|
-
status: "Active"
|
|
42
|
-
};
|
|
41
|
+
const dataSource = createStorybookDataSource();
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
const ContactListFromAPI = () => {
|
|
44
|
+
const [contacts, setContacts] = useState<any[]>([]);
|
|
45
|
+
const [loading, setLoading] = useState(true);
|
|
46
|
+
const [error, setError] = useState<string | null>(null);
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
const fetchContacts = useCallback(async () => {
|
|
49
|
+
setLoading(true);
|
|
50
|
+
setError(null);
|
|
51
|
+
try {
|
|
52
|
+
const result = await dataSource.find('contact', { $top: 10 });
|
|
53
|
+
setContacts(result.data as any[]);
|
|
54
|
+
} catch (err: any) {
|
|
55
|
+
console.error('[Data Binding] Failed to fetch contacts:', err);
|
|
56
|
+
setError(err?.message || 'Failed to fetch data from MSW API');
|
|
57
|
+
} finally {
|
|
58
|
+
setLoading(false);
|
|
59
|
+
}
|
|
49
60
|
}, []);
|
|
50
61
|
|
|
51
|
-
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
fetchContacts();
|
|
64
|
+
}, [fetchContacts]);
|
|
65
|
+
|
|
66
|
+
if (loading) return <div className="p-8 text-muted-foreground">Loading contacts from MSW API...</div>;
|
|
67
|
+
if (error) return (
|
|
68
|
+
<div className="p-8 space-y-2">
|
|
69
|
+
<div className="text-destructive font-medium">API Error: {error}</div>
|
|
70
|
+
<div className="text-sm text-muted-foreground">
|
|
71
|
+
This story requires the ObjectStack MSW runtime to be running.
|
|
72
|
+
Check the browser console for [Storybook MSW] logs.
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const gridSchema: BaseSchema = {
|
|
78
|
+
type: 'object-grid',
|
|
79
|
+
objectName: 'contact',
|
|
80
|
+
columns: [
|
|
81
|
+
{ field: 'name', header: 'Name', sortable: true },
|
|
82
|
+
{ field: 'email', header: 'Email', sortable: true },
|
|
83
|
+
{ field: 'title', header: 'Title' },
|
|
84
|
+
{ field: 'status', header: 'Status' },
|
|
85
|
+
],
|
|
86
|
+
data: contacts,
|
|
87
|
+
className: 'w-full'
|
|
88
|
+
} as any;
|
|
52
89
|
|
|
53
90
|
return (
|
|
54
|
-
<SchemaRendererProvider dataSource={
|
|
55
|
-
<
|
|
91
|
+
<SchemaRendererProvider dataSource={dataSource}>
|
|
92
|
+
<div className="space-y-4">
|
|
93
|
+
<div className="flex items-center justify-between">
|
|
94
|
+
<h2 className="text-lg font-semibold">Contacts from MSW ({contacts.length} records)</h2>
|
|
95
|
+
</div>
|
|
96
|
+
<SchemaRenderer schema={gridSchema} />
|
|
97
|
+
</div>
|
|
56
98
|
</SchemaRendererProvider>
|
|
57
99
|
);
|
|
58
100
|
};
|
|
59
101
|
|
|
102
|
+
// ==========================================
|
|
103
|
+
// Meta
|
|
104
|
+
// ==========================================
|
|
105
|
+
|
|
60
106
|
const meta: Meta = {
|
|
61
|
-
title: '
|
|
62
|
-
component: DataFetcher,
|
|
107
|
+
title: 'Getting Started/Data Binding',
|
|
63
108
|
parameters: {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
109
|
+
layout: 'padded',
|
|
110
|
+
},
|
|
67
111
|
};
|
|
68
112
|
|
|
69
113
|
export default meta;
|
|
70
114
|
|
|
71
|
-
export const
|
|
115
|
+
export const StaticData: StoryObj = {
|
|
116
|
+
render: () => <SchemaRenderer schema={staticDataSchema} />,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const LiveAPI: StoryObj = {
|
|
120
|
+
render: () => <ContactListFromAPI />,
|
|
121
|
+
};
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { Meta } from '@storybook/blocks';
|
|
2
|
+
|
|
3
|
+
<Meta title="Getting Started/Accessibility" />
|
|
4
|
+
|
|
5
|
+
# Accessibility Reference
|
|
6
|
+
|
|
7
|
+
ObjectUI targets **WCAG 2.1 Level AA** conformance. Every component inherits accessible
|
|
8
|
+
primitives from [Radix UI](https://www.radix-ui.com/) and is styled with Tailwind CSS
|
|
9
|
+
utility classes that respect user preferences for reduced motion and color contrast.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Compliance Baseline
|
|
14
|
+
|
|
15
|
+
| Standard | Level | Status |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| WCAG 2.1 | AA | ✅ Target conformance for all components |
|
|
18
|
+
| WAI-ARIA 1.2 | — | ✅ Roles, states, and properties via Radix |
|
|
19
|
+
| Section 508 | — | ✅ Covered by WCAG AA compliance |
|
|
20
|
+
|
|
21
|
+
ObjectUI relies on **Radix UI** for its accessible primitive layer. Radix components
|
|
22
|
+
ship with correct ARIA roles, keyboard interactions, and focus management built in.
|
|
23
|
+
Shadcn UI wraps these primitives with Tailwind styling while preserving all accessibility
|
|
24
|
+
behaviour.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Keyboard Navigation Patterns
|
|
29
|
+
|
|
30
|
+
All interactive components support keyboard navigation following WAI-ARIA authoring
|
|
31
|
+
practices. Below is a reference of the patterns used across component types.
|
|
32
|
+
|
|
33
|
+
### General Controls
|
|
34
|
+
|
|
35
|
+
| Component | Key | Action |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| **Button** | `Enter` / `Space` | Activate the button |
|
|
38
|
+
| **Link** | `Enter` | Follow the link |
|
|
39
|
+
| **Toggle** | `Enter` / `Space` | Toggle the state |
|
|
40
|
+
| All focusable | `Tab` | Move focus to next element |
|
|
41
|
+
| All focusable | `Shift + Tab` | Move focus to previous element |
|
|
42
|
+
|
|
43
|
+
### Composite Widgets
|
|
44
|
+
|
|
45
|
+
| Component | Key | Action |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| **Tabs** | `Arrow Left` / `Arrow Right` | Move between tab triggers |
|
|
48
|
+
| **Tabs** | `Enter` / `Space` | Activate focused tab |
|
|
49
|
+
| **Tabs** | `Home` / `End` | Jump to first / last tab |
|
|
50
|
+
| **Accordion** | `Arrow Up` / `Arrow Down` | Move between headers |
|
|
51
|
+
| **Accordion** | `Enter` / `Space` | Expand / collapse section |
|
|
52
|
+
| **Accordion** | `Home` / `End` | Jump to first / last header |
|
|
53
|
+
| **Menu / DropdownMenu** | `Arrow Up` / `Arrow Down` | Navigate menu items |
|
|
54
|
+
| **Menu / DropdownMenu** | `Enter` / `Space` | Activate item |
|
|
55
|
+
| **Menu / DropdownMenu** | `Escape` | Close menu and return focus |
|
|
56
|
+
| **NavigationMenu** | `Arrow Left` / `Arrow Right` | Move between top-level items |
|
|
57
|
+
| **NavigationMenu** | `Arrow Down` | Open submenu |
|
|
58
|
+
| **NavigationMenu** | `Escape` | Close submenu |
|
|
59
|
+
|
|
60
|
+
### Form Fields
|
|
61
|
+
|
|
62
|
+
| Component | Key | Action |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| **Input / Textarea** | Standard typing | Enter text |
|
|
65
|
+
| **Select** | `Arrow Up` / `Arrow Down` | Navigate options |
|
|
66
|
+
| **Select** | `Enter` | Select option |
|
|
67
|
+
| **Select** | `Escape` | Close listbox |
|
|
68
|
+
| **Checkbox** | `Space` | Toggle checked state |
|
|
69
|
+
| **Radio Group** | `Arrow Up` / `Arrow Down` | Move selection |
|
|
70
|
+
| **Switch** | `Space` | Toggle on/off |
|
|
71
|
+
| **Slider** | `Arrow Left` / `Arrow Right` | Decrease / increase value |
|
|
72
|
+
| **Slider** | `Home` / `End` | Set to minimum / maximum |
|
|
73
|
+
| **DatePicker** | `Arrow Keys` | Navigate calendar grid |
|
|
74
|
+
| **DatePicker** | `Enter` | Select date |
|
|
75
|
+
| **DatePicker** | `Escape` | Close calendar popover |
|
|
76
|
+
|
|
77
|
+
### Overlay Components
|
|
78
|
+
|
|
79
|
+
| Component | Key | Action |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| **Dialog** | `Escape` | Close the dialog |
|
|
82
|
+
| **Dialog** | `Tab` | Cycle focus within dialog (focus trap) |
|
|
83
|
+
| **AlertDialog** | `Escape` | Close (if not critical) |
|
|
84
|
+
| **AlertDialog** | `Tab` | Cycle focus within alert |
|
|
85
|
+
| **Popover** | `Escape` | Close popover |
|
|
86
|
+
| **Tooltip** | `Escape` | Dismiss tooltip |
|
|
87
|
+
| **Sheet** | `Escape` | Close sheet overlay |
|
|
88
|
+
| **Sheet** | `Tab` | Cycle focus within sheet |
|
|
89
|
+
|
|
90
|
+
### Data Views (Plugins)
|
|
91
|
+
|
|
92
|
+
| Component | Key | Action |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| **DataTable / Grid** | `Arrow Keys` | Navigate cells |
|
|
95
|
+
| **DataTable / Grid** | `Enter` | Activate cell / begin editing |
|
|
96
|
+
| **DataTable / Grid** | `Escape` | Cancel editing |
|
|
97
|
+
| **DataTable / Grid** | `Tab` | Move to next focusable cell |
|
|
98
|
+
| **Kanban** | `Tab` | Move between cards |
|
|
99
|
+
| **Kanban** | `Enter` / `Space` | Open card detail |
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## ARIA Roles and Attributes
|
|
104
|
+
|
|
105
|
+
ObjectUI components emit the correct ARIA roles and attributes automatically through
|
|
106
|
+
Radix primitives. Below is a reference of the key mappings.
|
|
107
|
+
|
|
108
|
+
### Primitive Components
|
|
109
|
+
|
|
110
|
+
| Component | ARIA Role | Key Attributes |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| **Button** | `button` | `aria-disabled`, `aria-pressed` (toggle) |
|
|
113
|
+
| **Badge** | `status` | `aria-label` (when used as status indicator) |
|
|
114
|
+
| **Card** | `region` | `aria-labelledby` (card title) |
|
|
115
|
+
| **Separator** | `separator` | `aria-orientation` |
|
|
116
|
+
| **Avatar** | `img` | `alt` text on image, fallback initials |
|
|
117
|
+
| **Progress** | `progressbar` | `aria-valuenow`, `aria-valuemin`, `aria-valuemax` |
|
|
118
|
+
| **Skeleton** | `status` | `aria-busy="true"`, `aria-label="Loading"` |
|
|
119
|
+
|
|
120
|
+
### Composite Components
|
|
121
|
+
|
|
122
|
+
| Component | ARIA Role | Key Attributes |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| **Tabs** | `tablist`, `tab`, `tabpanel` | `aria-selected`, `aria-controls`, `aria-labelledby` |
|
|
125
|
+
| **Accordion** | `region` | `aria-expanded`, `aria-controls` on triggers |
|
|
126
|
+
| **Breadcrumb** | `navigation` | `aria-label="Breadcrumb"`, `aria-current="page"` on active |
|
|
127
|
+
| **NavigationMenu** | `navigation` | `aria-label`, `aria-expanded` on submenus |
|
|
128
|
+
| **Carousel** | `region` | `aria-roledescription="carousel"`, `aria-label` on slides |
|
|
129
|
+
|
|
130
|
+
### Form Components
|
|
131
|
+
|
|
132
|
+
| Component | ARIA Role | Key Attributes |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| **Input** | `textbox` | `aria-required`, `aria-invalid`, `aria-describedby` (error/hint) |
|
|
135
|
+
| **Textarea** | `textbox` | `aria-required`, `aria-invalid`, `aria-describedby` |
|
|
136
|
+
| **Select** | `combobox` / `listbox` | `aria-expanded`, `aria-activedescendant`, `aria-required` |
|
|
137
|
+
| **Checkbox** | `checkbox` | `aria-checked`, `aria-required` |
|
|
138
|
+
| **Radio Group** | `radiogroup`, `radio` | `aria-checked`, `aria-required` |
|
|
139
|
+
| **Switch** | `switch` | `aria-checked`, `aria-label` |
|
|
140
|
+
| **Slider** | `slider` | `aria-valuenow`, `aria-valuemin`, `aria-valuemax`, `aria-label` |
|
|
141
|
+
| **Label** | — | `for` / `htmlFor` association with input |
|
|
142
|
+
|
|
143
|
+
### Overlay Components
|
|
144
|
+
|
|
145
|
+
| Component | ARIA Role | Key Attributes |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| **Dialog** | `dialog` | `aria-modal="true"`, `aria-labelledby`, `aria-describedby` |
|
|
148
|
+
| **AlertDialog** | `alertdialog` | `aria-modal="true"`, `aria-labelledby`, `aria-describedby` |
|
|
149
|
+
| **Popover** | `dialog` | `aria-expanded` on trigger, auto `aria-labelledby` |
|
|
150
|
+
| **Tooltip** | `tooltip` | `aria-describedby` on trigger element |
|
|
151
|
+
| **Sheet** | `dialog` | `aria-modal="true"`, `aria-labelledby` |
|
|
152
|
+
| **DropdownMenu** | `menu`, `menuitem` | `aria-expanded`, `aria-haspopup` |
|
|
153
|
+
| **ContextMenu** | `menu`, `menuitem` | `aria-expanded`, `aria-haspopup` |
|
|
154
|
+
|
|
155
|
+
### Data View Components
|
|
156
|
+
|
|
157
|
+
| Component | ARIA Role | Key Attributes |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| **DataTable** | `table`, `row`, `cell` | `aria-sort` on sortable columns, `aria-rowcount` |
|
|
160
|
+
| **Grid (AG Grid)** | `grid`, `row`, `gridcell` | `aria-colcount`, `aria-rowindex`, `aria-selected` |
|
|
161
|
+
| **Kanban** | `list`, `listitem` | `aria-label` per column, `aria-grabbed` on draggable cards |
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Screen Reader Behaviour
|
|
166
|
+
|
|
167
|
+
ObjectUI components are designed to work seamlessly with screen readers
|
|
168
|
+
(NVDA, JAWS, VoiceOver, TalkBack). Key behaviours include:
|
|
169
|
+
|
|
170
|
+
### Focus Management
|
|
171
|
+
- **Dialogs and Sheets** trap focus within the overlay while open. On close, focus
|
|
172
|
+
returns to the trigger element.
|
|
173
|
+
- **Menus and Popovers** return focus to their trigger on dismiss.
|
|
174
|
+
- **Toast notifications** use `role="status"` with `aria-live="polite"` so screen
|
|
175
|
+
readers announce them without interrupting the user.
|
|
176
|
+
|
|
177
|
+
### Live Regions
|
|
178
|
+
- **Form validation errors** are linked via `aria-describedby`, so errors are announced
|
|
179
|
+
when the field receives focus.
|
|
180
|
+
- **Loading states** use `aria-busy="true"` on the loading container and `aria-live`
|
|
181
|
+
regions to announce completion.
|
|
182
|
+
- **Toast / Alert** components use `aria-live="polite"` or `aria-live="assertive"`
|
|
183
|
+
depending on urgency.
|
|
184
|
+
|
|
185
|
+
### Landmark Regions
|
|
186
|
+
- **AppShell** emits `<header>`, `<nav>`, `<main>`, and `<aside>` landmarks so screen
|
|
187
|
+
reader users can jump between sections.
|
|
188
|
+
- **Breadcrumb** uses `<nav aria-label="Breadcrumb">` for quick landmark navigation.
|
|
189
|
+
- **Sidebar** uses `<aside>` with a descriptive `aria-label`.
|
|
190
|
+
|
|
191
|
+
### Announcements
|
|
192
|
+
- **Sortable table columns** announce sort direction changes via `aria-sort`.
|
|
193
|
+
- **Accordion** sections announce expanded/collapsed state.
|
|
194
|
+
- **Tabs** announce the active tab label on selection.
|
|
195
|
+
- **Carousel** announces slide transitions with `aria-roledescription`.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Color Contrast Requirements
|
|
200
|
+
|
|
201
|
+
ObjectUI uses Tailwind CSS design tokens (CSS custom properties) for all color
|
|
202
|
+
values. The default theme meets WCAG AA contrast ratios:
|
|
203
|
+
|
|
204
|
+
| Element | Minimum Ratio | Standard |
|
|
205
|
+
|---|---|---|
|
|
206
|
+
| Body text (`foreground` on `background`) | **4.5 : 1** | WCAG AA — Normal text |
|
|
207
|
+
| Large text (≥ 18pt or ≥ 14pt bold) | **3 : 1** | WCAG AA — Large text |
|
|
208
|
+
| UI components and graphical objects | **3 : 1** | WCAG 2.1 SC 1.4.11 |
|
|
209
|
+
| Focus indicators | **3 : 1** | WCAG 2.1 SC 1.4.11 |
|
|
210
|
+
|
|
211
|
+
### Theme Tokens
|
|
212
|
+
|
|
213
|
+
All colors are defined as CSS variables in HSL format:
|
|
214
|
+
|
|
215
|
+
```css
|
|
216
|
+
:root {
|
|
217
|
+
--background: 0 0% 100%;
|
|
218
|
+
--foreground: 222 84% 5%;
|
|
219
|
+
--primary: 222 47% 11%;
|
|
220
|
+
--primary-foreground: 210 40% 98%;
|
|
221
|
+
--destructive: 0 84% 60%;
|
|
222
|
+
--destructive-foreground: 0 0% 98%;
|
|
223
|
+
--muted: 210 40% 96%;
|
|
224
|
+
--muted-foreground: 215 16% 47%;
|
|
225
|
+
/* ...additional tokens */
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
When customising tokens, verify contrast ratios with a tool such as the
|
|
230
|
+
[WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) or the
|
|
231
|
+
built-in Storybook accessibility panel.
|
|
232
|
+
|
|
233
|
+
### Dark Mode
|
|
234
|
+
|
|
235
|
+
ObjectUI supports a `.dark` class on the root element. The dark theme ships with
|
|
236
|
+
its own set of tokens that maintain the same contrast ratios:
|
|
237
|
+
|
|
238
|
+
```css
|
|
239
|
+
.dark {
|
|
240
|
+
--background: 222 84% 5%;
|
|
241
|
+
--foreground: 210 40% 98%;
|
|
242
|
+
/* ...dark tokens */
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Focus Indicators
|
|
247
|
+
|
|
248
|
+
All interactive elements display a visible focus ring when navigated via keyboard.
|
|
249
|
+
The default ring uses the `--ring` token and applies a `2px` outline with a `2px`
|
|
250
|
+
offset, ensuring it meets the **3 : 1** non-text contrast requirement.
|
|
251
|
+
|
|
252
|
+
```css
|
|
253
|
+
:root {
|
|
254
|
+
--ring: 222 47% 11%;
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
> **Tip:** Never remove focus outlines (`outline: none`) without providing an
|
|
259
|
+
> equivalent visible indicator. ObjectUI's `focus-visible` styles handle this
|
|
260
|
+
> automatically.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Testing Accessibility
|
|
265
|
+
|
|
266
|
+
### Storybook A11y Addon
|
|
267
|
+
|
|
268
|
+
Every story in this Storybook can be audited via the **Accessibility** panel in the
|
|
269
|
+
addons bar. It runs [axe-core](https://github.com/dequelabs/axe-core) checks and
|
|
270
|
+
reports violations, passes, and incomplete audits.
|
|
271
|
+
|
|
272
|
+
### Automated Testing
|
|
273
|
+
|
|
274
|
+
Use `vitest` with `@testing-library/jest-dom` matchers for assertions like
|
|
275
|
+
`toBeVisible()`, `toHaveAccessibleName()`, and `toHaveAttribute('aria-expanded')`.
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
import { render, screen } from '@testing-library/react';
|
|
279
|
+
import { axe, toHaveNoViolations } from 'jest-axe';
|
|
280
|
+
|
|
281
|
+
expect.extend(toHaveNoViolations);
|
|
282
|
+
|
|
283
|
+
it('Button has no accessibility violations', async () => {
|
|
284
|
+
const { container } = render(<Button>Click me</Button>);
|
|
285
|
+
const results = await axe(container);
|
|
286
|
+
expect(results).toHaveNoViolations();
|
|
287
|
+
});
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Manual Testing Checklist
|
|
291
|
+
|
|
292
|
+
- [ ] Navigate all interactive elements using only the keyboard (`Tab`, `Enter`, `Space`, `Escape`, `Arrow` keys).
|
|
293
|
+
- [ ] Verify visible focus indicators on every focusable element.
|
|
294
|
+
- [ ] Test with a screen reader (VoiceOver on macOS, NVDA on Windows).
|
|
295
|
+
- [ ] Check colour contrast using the browser DevTools or WebAIM checker.
|
|
296
|
+
- [ ] Confirm the page is usable at 200% browser zoom.
|
|
297
|
+
- [ ] Verify `prefers-reduced-motion` is respected (animations are disabled or reduced).
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { SchemaRenderer } from '../SchemaRenderer';
|
|
3
|
+
import type { BaseSchema } from '@object-ui/types';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Components / Edge Cases',
|
|
7
|
+
component: SchemaRenderer,
|
|
8
|
+
parameters: { layout: 'padded' },
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
argTypes: {
|
|
11
|
+
schema: { table: { disable: true } },
|
|
12
|
+
},
|
|
13
|
+
} satisfies Meta<any>;
|
|
14
|
+
|
|
15
|
+
export default meta;
|
|
16
|
+
type Story = StoryObj<typeof meta>;
|
|
17
|
+
|
|
18
|
+
const renderStory = (args: any) => <SchemaRenderer schema={args as unknown as BaseSchema} />;
|
|
19
|
+
|
|
20
|
+
// ── Empty States ──────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export const EmptyStateDefault: Story = {
|
|
23
|
+
name: 'Empty State – Default',
|
|
24
|
+
render: renderStory,
|
|
25
|
+
args: {
|
|
26
|
+
type: 'empty',
|
|
27
|
+
description: 'No items to display',
|
|
28
|
+
} as any,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const EmptyStateWithAction: Story = {
|
|
32
|
+
name: 'Empty State – With Action',
|
|
33
|
+
render: renderStory,
|
|
34
|
+
args: {
|
|
35
|
+
type: 'empty',
|
|
36
|
+
description: 'No results found. Try a different search or create a new item.',
|
|
37
|
+
children: [
|
|
38
|
+
{ type: 'button', content: 'Create New', variant: 'default', size: 'sm' },
|
|
39
|
+
],
|
|
40
|
+
} as any,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// ── Loading / Spinner ─────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
export const SpinnerSmall: Story = {
|
|
46
|
+
name: 'Spinner – Small',
|
|
47
|
+
render: renderStory,
|
|
48
|
+
args: {
|
|
49
|
+
type: 'spinner',
|
|
50
|
+
size: 'sm',
|
|
51
|
+
className: 'text-primary',
|
|
52
|
+
} as any,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const SpinnerLarge: Story = {
|
|
56
|
+
name: 'Spinner – Large',
|
|
57
|
+
render: renderStory,
|
|
58
|
+
args: {
|
|
59
|
+
type: 'spinner',
|
|
60
|
+
size: 'lg',
|
|
61
|
+
className: 'text-primary',
|
|
62
|
+
} as any,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const LoadingWithText: Story = {
|
|
66
|
+
name: 'Loading – With Text',
|
|
67
|
+
render: renderStory,
|
|
68
|
+
args: {
|
|
69
|
+
type: 'loading',
|
|
70
|
+
text: 'Please wait while we fetch your data…',
|
|
71
|
+
className: 'h-[200px]',
|
|
72
|
+
} as any,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// ── Overflow / Long Text ──────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
const LONG_TEXT =
|
|
78
|
+
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit.';
|
|
79
|
+
|
|
80
|
+
export const CardWithLongText: Story = {
|
|
81
|
+
name: 'Card – Very Long Text',
|
|
82
|
+
render: renderStory,
|
|
83
|
+
args: {
|
|
84
|
+
type: 'card',
|
|
85
|
+
className: 'w-[350px]',
|
|
86
|
+
title: 'This is an extremely long card title that should test how the component handles overflow and text wrapping gracefully',
|
|
87
|
+
description: LONG_TEXT,
|
|
88
|
+
children: [
|
|
89
|
+
{
|
|
90
|
+
type: 'text',
|
|
91
|
+
content: LONG_TEXT + ' ' + LONG_TEXT,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
} as any,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const BadgeWithLongText: Story = {
|
|
98
|
+
name: 'Badge – Very Long Text',
|
|
99
|
+
render: renderStory,
|
|
100
|
+
args: {
|
|
101
|
+
type: 'badge',
|
|
102
|
+
label: 'This is an unusually long badge label that tests truncation and overflow behaviour in tight layouts',
|
|
103
|
+
} as any,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const ButtonWithIconAndLongText: Story = {
|
|
107
|
+
name: 'Button – Icon + Long Text',
|
|
108
|
+
render: (args: any) => <SchemaRenderer schema={args as unknown as BaseSchema} />,
|
|
109
|
+
args: {
|
|
110
|
+
type: 'button',
|
|
111
|
+
props: { variant: 'default' },
|
|
112
|
+
children: [
|
|
113
|
+
{ type: 'icon', name: 'download', className: 'mr-2 h-4 w-4' },
|
|
114
|
+
{
|
|
115
|
+
type: 'text',
|
|
116
|
+
content: 'Download All Reports For The Current Financial Year Including Amendments',
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
} as any,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// ── RTL Layout ────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
export const RTLCard: Story = {
|
|
125
|
+
name: 'RTL – Card Layout',
|
|
126
|
+
render: (args: any) => (
|
|
127
|
+
<div dir="rtl">
|
|
128
|
+
<SchemaRenderer schema={args as unknown as BaseSchema} />
|
|
129
|
+
</div>
|
|
130
|
+
),
|
|
131
|
+
args: {
|
|
132
|
+
type: 'card',
|
|
133
|
+
className: 'w-[350px]',
|
|
134
|
+
title: 'عنوان البطاقة',
|
|
135
|
+
description: 'هذا النص باللغة العربية لاختبار تخطيط الاتجاه من اليمين إلى اليسار.',
|
|
136
|
+
children: [
|
|
137
|
+
{ type: 'text', content: 'محتوى البطاقة الرئيسي يظهر هنا باللغة العربية.' },
|
|
138
|
+
],
|
|
139
|
+
footer: [
|
|
140
|
+
{ type: 'button', props: { variant: 'outline' }, children: [{ type: 'text', content: 'إلغاء' }] },
|
|
141
|
+
{ type: 'button', children: [{ type: 'text', content: 'حفظ' }] },
|
|
142
|
+
],
|
|
143
|
+
} as any,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const RTLAlert: Story = {
|
|
147
|
+
name: 'RTL – Alert',
|
|
148
|
+
render: (args: any) => (
|
|
149
|
+
<div dir="rtl">
|
|
150
|
+
<SchemaRenderer schema={args as unknown as BaseSchema} />
|
|
151
|
+
</div>
|
|
152
|
+
),
|
|
153
|
+
args: {
|
|
154
|
+
type: 'alert',
|
|
155
|
+
variant: 'destructive',
|
|
156
|
+
title: 'خطأ',
|
|
157
|
+
description: 'انتهت صلاحية الجلسة. الرجاء تسجيل الدخول مرة أخرى.',
|
|
158
|
+
className: 'w-[400px]',
|
|
159
|
+
} as any,
|
|
160
|
+
};
|