@pyreweb/fabric 1.2.6
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 +119 -0
- package/dist/fabric.cjs.js +18109 -0
- package/dist/fabric.css +2180 -0
- package/dist/fabric.esm.js +18062 -0
- package/dist/fabric.min.js +18112 -0
- package/dist/types/components/atoms/FAvatar/FAvatar.test.d.ts +1 -0
- package/dist/types/components/atoms/FBadge/FBadge.test.d.ts +1 -0
- package/dist/types/components/atoms/FButton/FButton.test.d.ts +1 -0
- package/dist/types/components/atoms/FCheckbox/FCheckbox.test.d.ts +1 -0
- package/dist/types/components/atoms/FDivider/FDivider.test.d.ts +1 -0
- package/dist/types/components/atoms/FIcon/FIcon.test.d.ts +1 -0
- package/dist/types/components/atoms/FInput/FInput.test.d.ts +1 -0
- package/dist/types/components/atoms/FLoader/FLoader.test.d.ts +1 -0
- package/dist/types/components/atoms/FRadio/FRadio.test.d.ts +1 -0
- package/dist/types/components/atoms/FTextarea/FTextarea.test.d.ts +1 -0
- package/dist/types/components/atoms/FToggle/FToggle.test.d.ts +1 -0
- package/dist/types/components/atoms/FTypography/FTypography.test.d.ts +1 -0
- package/dist/types/components/atoms/index.d.ts +13 -0
- package/dist/types/components/molecules/FAccordionItem/FAccordionItem.test.d.ts +1 -0
- package/dist/types/components/molecules/FAlert/FAlert.test.d.ts +1 -0
- package/dist/types/components/molecules/FBreadcrumb/FBreadcrumb.test.d.ts +1 -0
- package/dist/types/components/molecules/FButtonGroup/FButtonGroup.test.d.ts +1 -0
- package/dist/types/components/molecules/FCard/FCard.test.d.ts +1 -0
- package/dist/types/components/molecules/FDatePicker/FDatePicker.test.d.ts +1 -0
- package/dist/types/components/molecules/FEmptyState/FEmptyState.test.d.ts +1 -0
- package/dist/types/components/molecules/FFilePreview/FFilePreview.test.d.ts +1 -0
- package/dist/types/components/molecules/FFormField/FFormField.test.d.ts +1 -0
- package/dist/types/components/molecules/FListItem/FListItem.test.d.ts +1 -0
- package/dist/types/components/molecules/FPagination/FPagination.test.d.ts +1 -0
- package/dist/types/components/molecules/FSearchBar/FSearchBar.test.d.ts +1 -0
- package/dist/types/components/molecules/FSelect/FSelect.test.d.ts +1 -0
- package/dist/types/components/molecules/FStatCard/FStatCard.test.d.ts +1 -0
- package/dist/types/components/molecules/FTabs/FTabs.test.d.ts +1 -0
- package/dist/types/components/molecules/FToast/FToast.test.d.ts +1 -0
- package/dist/types/components/molecules/index.d.ts +18 -0
- package/dist/types/components/organisms/FActivityFeed/FActivityFeed.test.d.ts +1 -0
- package/dist/types/components/organisms/FDataTable/FDataTable.test.d.ts +1 -0
- package/dist/types/components/organisms/FDrawer/FDrawer.test.d.ts +1 -0
- package/dist/types/components/organisms/FFileUpload/FFileUpload.test.d.ts +1 -0
- package/dist/types/components/organisms/FFilterSidebar/FFilterSidebar.test.d.ts +1 -0
- package/dist/types/components/organisms/FForm/FForm.test.d.ts +1 -0
- package/dist/types/components/organisms/FModal/FModal.test.d.ts +1 -0
- package/dist/types/components/organisms/FNavigationSidebar/FNavigationSidebar.test.d.ts +1 -0
- package/dist/types/components/organisms/FOnboardingStepper/FOnboardingStepper.test.d.ts +1 -0
- package/dist/types/components/organisms/FOnboardingStepper/FStepperProgress.test.d.ts +1 -0
- package/dist/types/components/organisms/FPageHeader/FPageHeader.test.d.ts +1 -0
- package/dist/types/components/organisms/FProfileSection/FProfileSection.test.d.ts +1 -0
- package/dist/types/components/organisms/FToastProvider/FToastProvider.test.d.ts +1 -0
- package/dist/types/components/organisms/FUserMenu/FUserMenu.test.d.ts +1 -0
- package/dist/types/components/organisms/index.d.ts +14 -0
- package/dist/types/components/utils/FThemeProvider.test.d.ts +1 -0
- package/dist/types/components/utils/index.d.ts +2 -0
- package/dist/types/components.d.ts +602 -0
- package/dist/types/composables/index.d.ts +12 -0
- package/dist/types/composables/useDataTableState.d.ts +106 -0
- package/dist/types/composables/useDataTableState.test.d.ts +1 -0
- package/dist/types/composables/useFormValidation.d.ts +49 -0
- package/dist/types/composables/useFormValidation.test.d.ts +1 -0
- package/dist/types/composables/useSidebarState.d.ts +65 -0
- package/dist/types/composables/useSidebarState.test.d.ts +1 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/types.d.ts +529 -0
- package/package.json +100 -0
- package/src/components/atoms/FAvatar/FAvatar.stories.js +100 -0
- package/src/components/atoms/FAvatar/FAvatar.test.ts +95 -0
- package/src/components/atoms/FAvatar/FAvatar.vue +190 -0
- package/src/components/atoms/FBadge/FBadge.stories.js +129 -0
- package/src/components/atoms/FBadge/FBadge.test.ts +93 -0
- package/src/components/atoms/FBadge/FBadge.vue +103 -0
- package/src/components/atoms/FButton/FButton.stories.js +122 -0
- package/src/components/atoms/FButton/FButton.test.ts +98 -0
- package/src/components/atoms/FButton/FButton.vue +147 -0
- package/src/components/atoms/FCheckbox/FCheckbox.stories.js +96 -0
- package/src/components/atoms/FCheckbox/FCheckbox.test.ts +64 -0
- package/src/components/atoms/FCheckbox/FCheckbox.vue +76 -0
- package/src/components/atoms/FDivider/FDivider.stories.js +104 -0
- package/src/components/atoms/FDivider/FDivider.test.ts +80 -0
- package/src/components/atoms/FDivider/FDivider.vue +117 -0
- package/src/components/atoms/FIcon/FIcon.stories.js +189 -0
- package/src/components/atoms/FIcon/FIcon.test.ts +99 -0
- package/src/components/atoms/FIcon/FIcon.vue +192 -0
- package/src/components/atoms/FInput/FInput.stories.js +119 -0
- package/src/components/atoms/FInput/FInput.test.ts +79 -0
- package/src/components/atoms/FInput/FInput.vue +88 -0
- package/src/components/atoms/FLoader/FLoader.stories.js +109 -0
- package/src/components/atoms/FLoader/FLoader.test.ts +66 -0
- package/src/components/atoms/FLoader/FLoader.vue +97 -0
- package/src/components/atoms/FRadio/FRadio.stories.js +105 -0
- package/src/components/atoms/FRadio/FRadio.test.ts +75 -0
- package/src/components/atoms/FRadio/FRadio.vue +119 -0
- package/src/components/atoms/FTextarea/FTextarea.stories.js +126 -0
- package/src/components/atoms/FTextarea/FTextarea.test.ts +94 -0
- package/src/components/atoms/FTextarea/FTextarea.vue +156 -0
- package/src/components/atoms/FToggle/FToggle.stories.js +108 -0
- package/src/components/atoms/FToggle/FToggle.test.ts +96 -0
- package/src/components/atoms/FToggle/FToggle.vue +123 -0
- package/src/components/atoms/FTypography/FTypography.stories.js +127 -0
- package/src/components/atoms/FTypography/FTypography.test.ts +93 -0
- package/src/components/atoms/FTypography/FTypography.vue +78 -0
- package/src/components/atoms/index.ts +27 -0
- package/src/components/molecules/FAccordionItem/FAccordionItem.stories.js +71 -0
- package/src/components/molecules/FAccordionItem/FAccordionItem.test.ts +61 -0
- package/src/components/molecules/FAccordionItem/FAccordionItem.vue +105 -0
- package/src/components/molecules/FAlert/FAlert.stories.js +87 -0
- package/src/components/molecules/FAlert/FAlert.test.ts +59 -0
- package/src/components/molecules/FAlert/FAlert.vue +108 -0
- package/src/components/molecules/FBreadcrumb/FBreadcrumb.stories.js +90 -0
- package/src/components/molecules/FBreadcrumb/FBreadcrumb.test.ts +76 -0
- package/src/components/molecules/FBreadcrumb/FBreadcrumb.vue +117 -0
- package/src/components/molecules/FButtonGroup/FButtonGroup.stories.js +82 -0
- package/src/components/molecules/FButtonGroup/FButtonGroup.test.ts +44 -0
- package/src/components/molecules/FButtonGroup/FButtonGroup.vue +31 -0
- package/src/components/molecules/FCard/FCard.stories.js +136 -0
- package/src/components/molecules/FCard/FCard.test.ts +87 -0
- package/src/components/molecules/FCard/FCard.vue +75 -0
- package/src/components/molecules/FDatePicker/FDatePicker.stories.js +305 -0
- package/src/components/molecules/FDatePicker/FDatePicker.test.ts +282 -0
- package/src/components/molecules/FDatePicker/FDatePicker.vue +750 -0
- package/src/components/molecules/FEmptyState/FEmptyState.stories.js +98 -0
- package/src/components/molecules/FEmptyState/FEmptyState.test.ts +82 -0
- package/src/components/molecules/FEmptyState/FEmptyState.vue +89 -0
- package/src/components/molecules/FFilePreview/FFilePreview.stories.js +130 -0
- package/src/components/molecules/FFilePreview/FFilePreview.test.ts +70 -0
- package/src/components/molecules/FFilePreview/FFilePreview.vue +125 -0
- package/src/components/molecules/FFormField/FFormField.stories.js +149 -0
- package/src/components/molecules/FFormField/FFormField.test.ts +85 -0
- package/src/components/molecules/FFormField/FFormField.vue +107 -0
- package/src/components/molecules/FListItem/FListItem.stories.js +158 -0
- package/src/components/molecules/FListItem/FListItem.test.ts +93 -0
- package/src/components/molecules/FListItem/FListItem.vue +113 -0
- package/src/components/molecules/FPagination/FPagination.stories.js +132 -0
- package/src/components/molecules/FPagination/FPagination.test.ts +79 -0
- package/src/components/molecules/FPagination/FPagination.vue +206 -0
- package/src/components/molecules/FSearchBar/FSearchBar.stories.js +129 -0
- package/src/components/molecules/FSearchBar/FSearchBar.test.ts +81 -0
- package/src/components/molecules/FSearchBar/FSearchBar.vue +180 -0
- package/src/components/molecules/FSelect/FSelect.stories.js +333 -0
- package/src/components/molecules/FSelect/FSelect.test.ts +478 -0
- package/src/components/molecules/FSelect/FSelect.vue +551 -0
- package/src/components/molecules/FStatCard/FStatCard.stories.js +144 -0
- package/src/components/molecules/FStatCard/FStatCard.test.ts +78 -0
- package/src/components/molecules/FStatCard/FStatCard.vue +106 -0
- package/src/components/molecules/FTabs/FTab.vue +63 -0
- package/src/components/molecules/FTabs/FTabs.stories.js +277 -0
- package/src/components/molecules/FTabs/FTabs.test.ts +264 -0
- package/src/components/molecules/FTabs/FTabs.vue +273 -0
- package/src/components/molecules/FToast/FToast.stories.js +150 -0
- package/src/components/molecules/FToast/FToast.test.ts +157 -0
- package/src/components/molecules/FToast/FToast.vue +283 -0
- package/src/components/molecules/index.ts +37 -0
- package/src/components/organisms/FActivityFeed/FActivityFeed.stories.js +217 -0
- package/src/components/organisms/FActivityFeed/FActivityFeed.test.ts +134 -0
- package/src/components/organisms/FActivityFeed/FActivityFeed.vue +589 -0
- package/src/components/organisms/FDataTable/FDataTable.stories.js +370 -0
- package/src/components/organisms/FDataTable/FDataTable.test.ts +248 -0
- package/src/components/organisms/FDataTable/FDataTable.vue +808 -0
- package/src/components/organisms/FDrawer/FDrawer.stories.js +296 -0
- package/src/components/organisms/FDrawer/FDrawer.test.ts +142 -0
- package/src/components/organisms/FDrawer/FDrawer.vue +303 -0
- package/src/components/organisms/FFileUpload/FFileUpload.stories.js +162 -0
- package/src/components/organisms/FFileUpload/FFileUpload.test.ts +103 -0
- package/src/components/organisms/FFileUpload/FFileUpload.vue +616 -0
- package/src/components/organisms/FFilterSidebar/FFilterSidebar.stories.js +161 -0
- package/src/components/organisms/FFilterSidebar/FFilterSidebar.test.ts +92 -0
- package/src/components/organisms/FFilterSidebar/FFilterSidebar.vue +458 -0
- package/src/components/organisms/FForm/FForm.stories.js +270 -0
- package/src/components/organisms/FForm/FForm.test.ts +63 -0
- package/src/components/organisms/FForm/FForm.vue +19 -0
- package/src/components/organisms/FModal/FModal.stories.js +227 -0
- package/src/components/organisms/FModal/FModal.test.ts +181 -0
- package/src/components/organisms/FModal/FModal.vue +319 -0
- package/src/components/organisms/FNavigationSidebar/FNavigationSidebar.stories.js +176 -0
- package/src/components/organisms/FNavigationSidebar/FNavigationSidebar.test.ts +95 -0
- package/src/components/organisms/FNavigationSidebar/FNavigationSidebar.vue +577 -0
- package/src/components/organisms/FOnboardingStepper/FOnboardingStepper.stories.js +197 -0
- package/src/components/organisms/FOnboardingStepper/FOnboardingStepper.test.ts +114 -0
- package/src/components/organisms/FOnboardingStepper/FOnboardingStepper.vue +212 -0
- package/src/components/organisms/FOnboardingStepper/FStepperProgress.stories.js +122 -0
- package/src/components/organisms/FOnboardingStepper/FStepperProgress.test.ts +130 -0
- package/src/components/organisms/FOnboardingStepper/FStepperProgress.vue +146 -0
- package/src/components/organisms/FPageHeader/FPageHeader.stories.js +142 -0
- package/src/components/organisms/FPageHeader/FPageHeader.test.ts +83 -0
- package/src/components/organisms/FPageHeader/FPageHeader.vue +241 -0
- package/src/components/organisms/FProfileSection/FProfileSection.stories.js +190 -0
- package/src/components/organisms/FProfileSection/FProfileSection.test.ts +85 -0
- package/src/components/organisms/FProfileSection/FProfileSection.vue +562 -0
- package/src/components/organisms/FToastProvider/FToastProvider.stories.js +290 -0
- package/src/components/organisms/FToastProvider/FToastProvider.test.ts +215 -0
- package/src/components/organisms/FToastProvider/FToastProvider.vue +214 -0
- package/src/components/organisms/FUserMenu/FUserMenu.stories.js +170 -0
- package/src/components/organisms/FUserMenu/FUserMenu.test.ts +102 -0
- package/src/components/organisms/FUserMenu/FUserMenu.vue +407 -0
- package/src/components/organisms/index.ts +29 -0
- package/src/components/utils/FThemeProvider.stories.js +236 -0
- package/src/components/utils/FThemeProvider.test.ts +244 -0
- package/src/components/utils/FThemeProvider.vue +191 -0
- package/src/components/utils/index.ts +3 -0
- package/src/components.d.ts +602 -0
- package/src/composables/README.md +233 -0
- package/src/composables/index.ts +25 -0
- package/src/composables/useDataTableState.test.ts +378 -0
- package/src/composables/useDataTableState.ts +361 -0
- package/src/composables/useFormValidation.test.ts +198 -0
- package/src/composables/useFormValidation.ts +178 -0
- package/src/composables/useSidebarState.test.ts +307 -0
- package/src/composables/useSidebarState.ts +201 -0
- package/src/env.d.ts +14 -0
- package/src/index.ts +167 -0
- package/src/styles/tailwind.css +173 -0
- package/src/types.ts +740 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import FModal from './FModal.vue';
|
|
4
|
+
|
|
5
|
+
describe('FModal', () => {
|
|
6
|
+
it('renders correctly when open', () => {
|
|
7
|
+
const wrapper = mount(FModal, {
|
|
8
|
+
propsData: { value: true, title: 'Test Modal' }
|
|
9
|
+
});
|
|
10
|
+
expect(wrapper.find('[role="dialog"]').exists()).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('does not render when closed', () => {
|
|
14
|
+
const wrapper = mount(FModal, {
|
|
15
|
+
propsData: { value: false }
|
|
16
|
+
});
|
|
17
|
+
expect(wrapper.find('[role="dialog"]').exists()).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('displays title', () => {
|
|
21
|
+
const wrapper = mount(FModal, {
|
|
22
|
+
propsData: { value: true, title: 'Modal Title' }
|
|
23
|
+
});
|
|
24
|
+
expect(wrapper.text()).toContain('Modal Title');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('displays subtitle', () => {
|
|
28
|
+
const wrapper = mount(FModal, {
|
|
29
|
+
propsData: { value: true, title: 'Title', subtitle: 'Subtitle' }
|
|
30
|
+
});
|
|
31
|
+
expect(wrapper.text()).toContain('Subtitle');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('renders body slot', () => {
|
|
35
|
+
const wrapper = mount(FModal, {
|
|
36
|
+
propsData: { value: true },
|
|
37
|
+
slots: { body: '<p>Body content</p>' }
|
|
38
|
+
});
|
|
39
|
+
expect(wrapper.html()).toContain('Body content');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('renders actions slot', () => {
|
|
43
|
+
const wrapper = mount(FModal, {
|
|
44
|
+
propsData: { value: true },
|
|
45
|
+
slots: { actions: '<button>Action</button>' }
|
|
46
|
+
});
|
|
47
|
+
expect(wrapper.html()).toContain('Action');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('shows close button by default', () => {
|
|
51
|
+
const wrapper = mount(FModal, {
|
|
52
|
+
propsData: { value: true }
|
|
53
|
+
});
|
|
54
|
+
expect(wrapper.findComponent({ name: 'FButton' }).exists()).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('hides close button when closable is false', () => {
|
|
58
|
+
const wrapper = mount(FModal, {
|
|
59
|
+
propsData: { value: true, closable: false }
|
|
60
|
+
});
|
|
61
|
+
expect(wrapper.findAllComponents({ name: 'FButton' }).length).toBe(0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('emits close when close button is clicked', async () => {
|
|
65
|
+
const wrapper = mount(FModal, {
|
|
66
|
+
propsData: { value: true }
|
|
67
|
+
});
|
|
68
|
+
await wrapper.findComponent({ name: 'FButton' }).trigger('click');
|
|
69
|
+
expect(wrapper.emitted('close')).toBeTruthy();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('emits input false when close button is clicked', async () => {
|
|
73
|
+
const wrapper = mount(FModal, {
|
|
74
|
+
propsData: { value: true }
|
|
75
|
+
});
|
|
76
|
+
await wrapper.findComponent({ name: 'FButton' }).trigger('click');
|
|
77
|
+
expect(wrapper.emitted('input')).toBeTruthy();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('closes on overlay click when closeOnOverlay is true', async () => {
|
|
81
|
+
const wrapper = mount(FModal, {
|
|
82
|
+
propsData: { value: true, closeOnOverlay: true }
|
|
83
|
+
});
|
|
84
|
+
await wrapper.find('.bg-black').trigger('click');
|
|
85
|
+
expect(wrapper.emitted('close')).toBeTruthy();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('does not close on overlay click when closeOnOverlay is false', async () => {
|
|
89
|
+
const wrapper = mount(FModal, {
|
|
90
|
+
propsData: { value: true, closeOnOverlay: false }
|
|
91
|
+
});
|
|
92
|
+
await wrapper.find('.bg-black').trigger('click');
|
|
93
|
+
expect(wrapper.emitted('close')).toBeFalsy();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('applies correct size classes', () => {
|
|
97
|
+
const sizes = ['small', 'medium', 'large', 'full'] as const;
|
|
98
|
+
sizes.forEach((size) => {
|
|
99
|
+
const wrapper = mount(FModal, {
|
|
100
|
+
propsData: { value: true, size }
|
|
101
|
+
});
|
|
102
|
+
expect(wrapper.find('[role="dialog"]').exists()).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('applies border when bordered is true', () => {
|
|
107
|
+
const wrapper = mount(FModal, {
|
|
108
|
+
propsData: { value: true, bordered: true }
|
|
109
|
+
});
|
|
110
|
+
expect(wrapper.find('[role="dialog"]').classes().join(' ')).toContain(
|
|
111
|
+
'border'
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('focuses first focusable element when opened', async () => {
|
|
116
|
+
const wrapper = mount(FModal, {
|
|
117
|
+
propsData: { value: false },
|
|
118
|
+
slots: {
|
|
119
|
+
body: '<button id="test-button">Test Button</button>'
|
|
120
|
+
},
|
|
121
|
+
attachTo: document.body
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Open the modal
|
|
125
|
+
await wrapper.setProps({ value: true });
|
|
126
|
+
await wrapper.vm.$nextTick();
|
|
127
|
+
|
|
128
|
+
// Check that a button receives focus (activeElement should be truthy)
|
|
129
|
+
expect(document.activeElement).toBeTruthy();
|
|
130
|
+
wrapper.destroy();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('traps Tab key within modal', async () => {
|
|
134
|
+
const wrapper = mount(FModal, {
|
|
135
|
+
propsData: { value: true },
|
|
136
|
+
slots: {
|
|
137
|
+
body: '<button id="first">First</button><button id="last">Last</button>'
|
|
138
|
+
},
|
|
139
|
+
attachTo: document.body
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await wrapper.vm.$nextTick();
|
|
143
|
+
const lastButton = document.getElementById('last');
|
|
144
|
+
|
|
145
|
+
if (lastButton) {
|
|
146
|
+
lastButton.focus();
|
|
147
|
+
await wrapper.trigger('keydown', { key: 'Tab' });
|
|
148
|
+
// After tabbing from last element, should focus first element
|
|
149
|
+
// Due to test limitations, we'll just verify the handler exists
|
|
150
|
+
expect(wrapper.vm.handleTabKey).toBeDefined();
|
|
151
|
+
}
|
|
152
|
+
wrapper.destroy();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('restores focus when modal closes', async () => {
|
|
156
|
+
const triggerButton = document.createElement('button');
|
|
157
|
+
triggerButton.id = 'trigger';
|
|
158
|
+
document.body.appendChild(triggerButton);
|
|
159
|
+
triggerButton.focus();
|
|
160
|
+
|
|
161
|
+
const wrapper = mount(FModal, {
|
|
162
|
+
propsData: { value: false },
|
|
163
|
+
slots: { body: '<button>Modal Button</button>' },
|
|
164
|
+
attachTo: document.body
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Open modal
|
|
168
|
+
await wrapper.setProps({ value: true });
|
|
169
|
+
await wrapper.vm.$nextTick();
|
|
170
|
+
|
|
171
|
+
// Close modal
|
|
172
|
+
await wrapper.setProps({ value: false });
|
|
173
|
+
await wrapper.vm.$nextTick();
|
|
174
|
+
|
|
175
|
+
// previousActiveElement should be stored
|
|
176
|
+
expect(wrapper.vm.previousActiveElement).toBeDefined();
|
|
177
|
+
|
|
178
|
+
wrapper.destroy();
|
|
179
|
+
document.body.removeChild(triggerButton);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="isOpen" class="fixed inset-0 z-50 overflow-y-auto">
|
|
3
|
+
<!-- Overlay -->
|
|
4
|
+
<div
|
|
5
|
+
class="fixed inset-0 bg-black opacity-50 transition-opacity duration-[var(--transition-duration-slow)] ease-[var(--transition-easing-standard)]"
|
|
6
|
+
@click="handleOverlayClick"
|
|
7
|
+
></div>
|
|
8
|
+
|
|
9
|
+
<!-- Modal Container -->
|
|
10
|
+
<div class="flex min-h-full items-center justify-center p-4">
|
|
11
|
+
<div
|
|
12
|
+
:class="modalClasses"
|
|
13
|
+
role="dialog"
|
|
14
|
+
aria-modal="true"
|
|
15
|
+
:aria-labelledby="titleId"
|
|
16
|
+
>
|
|
17
|
+
<!-- Header -->
|
|
18
|
+
<div
|
|
19
|
+
v-if="$slots.header || title"
|
|
20
|
+
class="flex items-center justify-between px-4 pt-4"
|
|
21
|
+
>
|
|
22
|
+
<div class="flex-1 min-w-0">
|
|
23
|
+
<slot name="header">
|
|
24
|
+
<f-typography :id="titleId" variant="h5">{{
|
|
25
|
+
title
|
|
26
|
+
}}</f-typography>
|
|
27
|
+
<f-typography
|
|
28
|
+
v-if="subtitle"
|
|
29
|
+
variant="caption"
|
|
30
|
+
class="text-neutral-500"
|
|
31
|
+
>
|
|
32
|
+
{{ subtitle }}
|
|
33
|
+
</f-typography>
|
|
34
|
+
</slot>
|
|
35
|
+
</div>
|
|
36
|
+
<f-button
|
|
37
|
+
v-if="closable"
|
|
38
|
+
variant="ghost"
|
|
39
|
+
size="small"
|
|
40
|
+
class="flex-shrink-0 -mr-2"
|
|
41
|
+
@click="handleClose"
|
|
42
|
+
>
|
|
43
|
+
<f-icon name="close" size="sm" />
|
|
44
|
+
<span class="sr-only">Fermer la modale</span>
|
|
45
|
+
</f-button>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<!-- Body -->
|
|
49
|
+
<div class="p-4">
|
|
50
|
+
<slot name="body">
|
|
51
|
+
<slot />
|
|
52
|
+
</slot>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<!-- Actions -->
|
|
56
|
+
<div v-if="$slots.actions" class="px-4 pb-4 flex gap-2 justify-end">
|
|
57
|
+
<slot name="actions" />
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</template>
|
|
63
|
+
|
|
64
|
+
<script>
|
|
65
|
+
import FTypography from '../../atoms/FTypography/FTypography.vue';
|
|
66
|
+
import FButton from '../../atoms/FButton/FButton.vue';
|
|
67
|
+
import FIcon from '../../atoms/FIcon/FIcon.vue';
|
|
68
|
+
|
|
69
|
+
let idCounter = 0;
|
|
70
|
+
|
|
71
|
+
export default {
|
|
72
|
+
name: 'FModal',
|
|
73
|
+
components: {
|
|
74
|
+
FTypography,
|
|
75
|
+
FButton,
|
|
76
|
+
FIcon
|
|
77
|
+
},
|
|
78
|
+
props: {
|
|
79
|
+
/**
|
|
80
|
+
* Controls the visibility of the modal.
|
|
81
|
+
* Use v-model for two-way binding.
|
|
82
|
+
*/
|
|
83
|
+
value: {
|
|
84
|
+
type: Boolean,
|
|
85
|
+
default: false
|
|
86
|
+
},
|
|
87
|
+
/**
|
|
88
|
+
* Modal title displayed in the header
|
|
89
|
+
*/
|
|
90
|
+
title: {
|
|
91
|
+
type: String,
|
|
92
|
+
default: ''
|
|
93
|
+
},
|
|
94
|
+
/**
|
|
95
|
+
* Optional subtitle displayed below the title
|
|
96
|
+
*/
|
|
97
|
+
subtitle: {
|
|
98
|
+
type: String,
|
|
99
|
+
default: ''
|
|
100
|
+
},
|
|
101
|
+
/**
|
|
102
|
+
* Show the close button in the header
|
|
103
|
+
*/
|
|
104
|
+
closable: {
|
|
105
|
+
type: Boolean,
|
|
106
|
+
default: true
|
|
107
|
+
},
|
|
108
|
+
/**
|
|
109
|
+
* Close the modal when clicking the overlay
|
|
110
|
+
*/
|
|
111
|
+
closeOnOverlay: {
|
|
112
|
+
type: Boolean,
|
|
113
|
+
default: true
|
|
114
|
+
},
|
|
115
|
+
/**
|
|
116
|
+
* Close the modal when pressing Escape key
|
|
117
|
+
*/
|
|
118
|
+
closeOnEscape: {
|
|
119
|
+
type: Boolean,
|
|
120
|
+
default: true
|
|
121
|
+
},
|
|
122
|
+
/**
|
|
123
|
+
* Modal size variant
|
|
124
|
+
*/
|
|
125
|
+
size: {
|
|
126
|
+
type: String,
|
|
127
|
+
default: 'medium',
|
|
128
|
+
validator: (value) => ['small', 'medium', 'large', 'full'].includes(value)
|
|
129
|
+
},
|
|
130
|
+
/**
|
|
131
|
+
* Whether the modal has a border
|
|
132
|
+
*/
|
|
133
|
+
bordered: {
|
|
134
|
+
type: Boolean,
|
|
135
|
+
default: true
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
data() {
|
|
139
|
+
return {
|
|
140
|
+
uid: idCounter++,
|
|
141
|
+
previousActiveElement: null,
|
|
142
|
+
focusableElementsSelector:
|
|
143
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
computed: {
|
|
147
|
+
/**
|
|
148
|
+
* Computed property for v-model support
|
|
149
|
+
*/
|
|
150
|
+
isOpen: {
|
|
151
|
+
get() {
|
|
152
|
+
return this.value;
|
|
153
|
+
},
|
|
154
|
+
set(val) {
|
|
155
|
+
this.$emit('input', val);
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
/**
|
|
159
|
+
* Unique ID for the modal title (accessibility)
|
|
160
|
+
*/
|
|
161
|
+
titleId() {
|
|
162
|
+
return `f-modal-title-${this.uid}`;
|
|
163
|
+
},
|
|
164
|
+
/**
|
|
165
|
+
* Modal container classes
|
|
166
|
+
*/
|
|
167
|
+
modalClasses() {
|
|
168
|
+
const baseClasses =
|
|
169
|
+
'relative bg-white rounded-lg overflow-hidden shadow-xl';
|
|
170
|
+
const transitionClasses =
|
|
171
|
+
'transition-all duration-[var(--transition-duration-slow)] ease-[var(--transition-easing-emphasized)]';
|
|
172
|
+
const borderedClasses = this.bordered ? 'border border-neutral-200' : '';
|
|
173
|
+
|
|
174
|
+
const sizeClasses = {
|
|
175
|
+
small: 'w-full max-w-sm',
|
|
176
|
+
medium: 'w-full max-w-lg',
|
|
177
|
+
large: 'w-full max-w-2xl',
|
|
178
|
+
full: 'w-full max-w-full m-4'
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
return [
|
|
182
|
+
baseClasses,
|
|
183
|
+
transitionClasses,
|
|
184
|
+
borderedClasses,
|
|
185
|
+
sizeClasses[this.size]
|
|
186
|
+
]
|
|
187
|
+
.filter(Boolean)
|
|
188
|
+
.join(' ');
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
watch: {
|
|
192
|
+
/**
|
|
193
|
+
* Watch for modal open/close to manage body scroll
|
|
194
|
+
*/
|
|
195
|
+
isOpen: {
|
|
196
|
+
immediate: true,
|
|
197
|
+
handler(newValue) {
|
|
198
|
+
if (newValue) {
|
|
199
|
+
this.lockBodyScroll();
|
|
200
|
+
this.$nextTick(() => {
|
|
201
|
+
this.setupFocusTrap();
|
|
202
|
+
if (this.closeOnEscape) {
|
|
203
|
+
document.addEventListener('keydown', this.handleKeydown);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
} else {
|
|
207
|
+
this.unlockBodyScroll();
|
|
208
|
+
this.removeFocusTrap();
|
|
209
|
+
document.removeEventListener('keydown', this.handleKeydown);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
beforeDestroy() {
|
|
215
|
+
this.unlockBodyScroll();
|
|
216
|
+
this.removeFocusTrap();
|
|
217
|
+
document.removeEventListener('keydown', this.handleKeydown);
|
|
218
|
+
},
|
|
219
|
+
methods: {
|
|
220
|
+
/**
|
|
221
|
+
* Handle overlay click
|
|
222
|
+
*/
|
|
223
|
+
handleOverlayClick() {
|
|
224
|
+
if (this.closeOnOverlay) {
|
|
225
|
+
this.handleClose();
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
/**
|
|
229
|
+
* Handle close action
|
|
230
|
+
*/
|
|
231
|
+
handleClose() {
|
|
232
|
+
this.isOpen = false;
|
|
233
|
+
this.$emit('close');
|
|
234
|
+
},
|
|
235
|
+
/**
|
|
236
|
+
* Handle keyboard events
|
|
237
|
+
*/
|
|
238
|
+
handleKeydown(event) {
|
|
239
|
+
if (event.key === 'Escape' && this.closeOnEscape) {
|
|
240
|
+
this.handleClose();
|
|
241
|
+
} else if (event.key === 'Tab') {
|
|
242
|
+
this.handleTabKey(event);
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
/**
|
|
246
|
+
* Handle Tab key for focus trap
|
|
247
|
+
*/
|
|
248
|
+
handleTabKey(event) {
|
|
249
|
+
const modalElement = this.$el.querySelector('[role="dialog"]');
|
|
250
|
+
if (!modalElement) return;
|
|
251
|
+
|
|
252
|
+
const focusableElements = modalElement.querySelectorAll(
|
|
253
|
+
this.focusableElementsSelector
|
|
254
|
+
);
|
|
255
|
+
const focusableArray = Array.from(focusableElements);
|
|
256
|
+
|
|
257
|
+
if (focusableArray.length === 0) return;
|
|
258
|
+
|
|
259
|
+
const firstElement = focusableArray[0];
|
|
260
|
+
const lastElement = focusableArray[focusableArray.length - 1];
|
|
261
|
+
|
|
262
|
+
if (event.shiftKey) {
|
|
263
|
+
// Shift + Tab: going backwards
|
|
264
|
+
if (document.activeElement === firstElement) {
|
|
265
|
+
event.preventDefault();
|
|
266
|
+
lastElement.focus();
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
// Tab: going forwards
|
|
270
|
+
if (document.activeElement === lastElement) {
|
|
271
|
+
event.preventDefault();
|
|
272
|
+
firstElement.focus();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
/**
|
|
277
|
+
* Setup focus trap and set initial focus
|
|
278
|
+
*/
|
|
279
|
+
setupFocusTrap() {
|
|
280
|
+
// Store the element that had focus before opening the modal
|
|
281
|
+
this.previousActiveElement = document.activeElement;
|
|
282
|
+
|
|
283
|
+
const modalElement = this.$el.querySelector('[role="dialog"]');
|
|
284
|
+
if (!modalElement) return;
|
|
285
|
+
|
|
286
|
+
// Find the first focusable element and focus it
|
|
287
|
+
const focusableElements = modalElement.querySelectorAll(
|
|
288
|
+
this.focusableElementsSelector
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
if (focusableElements.length > 0) {
|
|
292
|
+
focusableElements[0].focus();
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
/**
|
|
296
|
+
* Remove focus trap and restore focus
|
|
297
|
+
*/
|
|
298
|
+
removeFocusTrap() {
|
|
299
|
+
// Restore focus to the element that had it before the modal opened
|
|
300
|
+
if (this.previousActiveElement && this.previousActiveElement.focus) {
|
|
301
|
+
this.previousActiveElement.focus();
|
|
302
|
+
}
|
|
303
|
+
this.previousActiveElement = null;
|
|
304
|
+
},
|
|
305
|
+
/**
|
|
306
|
+
* Lock body scroll when modal is open
|
|
307
|
+
*/
|
|
308
|
+
lockBodyScroll() {
|
|
309
|
+
document.body.style.overflow = 'hidden';
|
|
310
|
+
},
|
|
311
|
+
/**
|
|
312
|
+
* Unlock body scroll when modal is closed
|
|
313
|
+
*/
|
|
314
|
+
unlockBodyScroll() {
|
|
315
|
+
document.body.style.overflow = '';
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
</script>
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import FNavigationSidebar from './FNavigationSidebar.vue';
|
|
2
|
+
import FAvatar from '../../atoms/FAvatar/FAvatar.vue';
|
|
3
|
+
import FButton from '../../atoms/FButton/FButton.vue';
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
title: 'Organisms/FNavigationSidebar',
|
|
7
|
+
component: FNavigationSidebar,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
items: {
|
|
11
|
+
control: 'object',
|
|
12
|
+
description: 'Éléments de navigation'
|
|
13
|
+
},
|
|
14
|
+
collapsed: {
|
|
15
|
+
control: 'boolean',
|
|
16
|
+
description: 'Sidebar réduite'
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const navItems = [
|
|
22
|
+
{ label: 'Tableau de bord', href: '/dashboard', icon: 'home', active: true },
|
|
23
|
+
{ label: 'Projets', href: '/projects', icon: 'folder' },
|
|
24
|
+
{ label: 'Équipe', href: '/team', icon: 'user' },
|
|
25
|
+
{ label: 'Messages', href: '/messages', icon: 'mail', badge: 5 },
|
|
26
|
+
{ label: 'Documents', href: '/documents', icon: 'document' },
|
|
27
|
+
{ label: 'Rapports', href: '/reports', icon: 'info' }
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const Template = (args, { argTypes }) => ({
|
|
31
|
+
components: { FNavigationSidebar },
|
|
32
|
+
props: Object.keys(argTypes),
|
|
33
|
+
template: '<FNavigationSidebar v-bind="$props" style="height: 400px" />'
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const Default = Template.bind({});
|
|
37
|
+
Default.args = {
|
|
38
|
+
items: navItems
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const Collapsed = Template.bind({});
|
|
42
|
+
Collapsed.args = {
|
|
43
|
+
items: navItems,
|
|
44
|
+
collapsed: true
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const WithHeader = () => ({
|
|
48
|
+
components: { FNavigationSidebar },
|
|
49
|
+
data() {
|
|
50
|
+
return { items: navItems };
|
|
51
|
+
},
|
|
52
|
+
template: `
|
|
53
|
+
<FNavigationSidebar :items="items" style="height: 400px">
|
|
54
|
+
<template #header>
|
|
55
|
+
<div class="px-4 py-3 border-b border-neutral-100">
|
|
56
|
+
<div class="flex items-center gap-2">
|
|
57
|
+
<div class="w-8 h-8 bg-primary-600 rounded-lg flex items-center justify-center">
|
|
58
|
+
<span class="text-white font-bold text-sm">F</span>
|
|
59
|
+
</div>
|
|
60
|
+
<span class="font-bold">Fabric</span>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</template>
|
|
64
|
+
</FNavigationSidebar>
|
|
65
|
+
`
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export const WithFooter = () => ({
|
|
69
|
+
components: { FNavigationSidebar, FAvatar, FButton },
|
|
70
|
+
data() {
|
|
71
|
+
return { items: navItems };
|
|
72
|
+
},
|
|
73
|
+
template: `
|
|
74
|
+
<FNavigationSidebar :items="items" style="height: 400px">
|
|
75
|
+
<template #footer>
|
|
76
|
+
<div class="px-4 py-3 border-t border-neutral-100">
|
|
77
|
+
<div class="flex items-center gap-3">
|
|
78
|
+
<FAvatar name="Jean Dupont" size="sm" />
|
|
79
|
+
<div class="flex-1 min-w-0">
|
|
80
|
+
<p class="text-sm font-medium truncate">Jean Dupont</p>
|
|
81
|
+
<p class="text-xs text-neutral-500 truncate">jean@example.com</p>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</template>
|
|
86
|
+
</FNavigationSidebar>
|
|
87
|
+
`
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export const WithSections = () => ({
|
|
91
|
+
components: { FNavigationSidebar },
|
|
92
|
+
data() {
|
|
93
|
+
return {
|
|
94
|
+
items: [
|
|
95
|
+
{ type: 'section', label: 'Principal' },
|
|
96
|
+
{
|
|
97
|
+
label: 'Tableau de bord',
|
|
98
|
+
href: '/dashboard',
|
|
99
|
+
icon: 'home',
|
|
100
|
+
active: true
|
|
101
|
+
},
|
|
102
|
+
{ label: 'Projets', href: '/projects', icon: 'folder' },
|
|
103
|
+
{ type: 'section', label: 'Équipe' },
|
|
104
|
+
{ label: 'Membres', href: '/members', icon: 'user' },
|
|
105
|
+
{ label: 'Messages', href: '/messages', icon: 'mail', badge: 3 },
|
|
106
|
+
{ type: 'section', label: 'Paramètres' },
|
|
107
|
+
{ label: 'Configuration', href: '/settings', icon: 'cog' },
|
|
108
|
+
{ label: 'Aide', href: '/help', icon: 'question' }
|
|
109
|
+
]
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
template: '<FNavigationSidebar :items="items" style="height: 500px" />'
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
export const WithNestedItems = () => ({
|
|
116
|
+
components: { FNavigationSidebar },
|
|
117
|
+
data() {
|
|
118
|
+
return {
|
|
119
|
+
items: [
|
|
120
|
+
{ label: 'Accueil', href: '/', icon: 'home' },
|
|
121
|
+
{
|
|
122
|
+
label: 'Produits',
|
|
123
|
+
icon: 'folder',
|
|
124
|
+
children: [
|
|
125
|
+
{ label: 'Catalogue', href: '/products/catalog' },
|
|
126
|
+
{ label: 'Inventaire', href: '/products/inventory' },
|
|
127
|
+
{ label: 'Commandes', href: '/products/orders' }
|
|
128
|
+
]
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
label: 'Utilisateurs',
|
|
132
|
+
icon: 'user',
|
|
133
|
+
children: [
|
|
134
|
+
{ label: 'Liste', href: '/users/list' },
|
|
135
|
+
{ label: 'Rôles', href: '/users/roles' }
|
|
136
|
+
]
|
|
137
|
+
},
|
|
138
|
+
{ label: 'Paramètres', href: '/settings', icon: 'cog' }
|
|
139
|
+
]
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
template: '<FNavigationSidebar :items="items" style="height: 400px" />'
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
export const Interactive = () => ({
|
|
146
|
+
components: { FNavigationSidebar, FButton },
|
|
147
|
+
data() {
|
|
148
|
+
return {
|
|
149
|
+
collapsed: false,
|
|
150
|
+
items: navItems
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
methods: {
|
|
154
|
+
handleNavigate(data) {
|
|
155
|
+
this.items = this.items.map((item) => ({
|
|
156
|
+
...item,
|
|
157
|
+
active: item.href === data.item.href
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
template: `
|
|
162
|
+
<div class="flex gap-4">
|
|
163
|
+
<FNavigationSidebar
|
|
164
|
+
:items="items"
|
|
165
|
+
:collapsed="collapsed"
|
|
166
|
+
style="height: 400px"
|
|
167
|
+
@navigate="handleNavigate"
|
|
168
|
+
/>
|
|
169
|
+
<div>
|
|
170
|
+
<FButton @click="collapsed = !collapsed">
|
|
171
|
+
{{ collapsed ? 'Étendre' : 'Réduire' }}
|
|
172
|
+
</FButton>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
`
|
|
176
|
+
});
|