Skip to content

Commit cd8c85a

Browse files
committed
- Update examples
- Work on a11y - Use new external scripts functionality
1 parent 9235abd commit cd8c85a

File tree

8 files changed

+160
-108
lines changed

8 files changed

+160
-108
lines changed

examples/Demo/FluentUI.Demo.Client/Documentation/Components/SortableList/Examples/SortableListDisabledSorting.razor

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
<FluentGrid Justify="JustifyContent.FlexStart" Spacing="3">
22
<FluentGridItem xs="12" sm="6">
33
<h5>List 1</h5>
4-
<FluentSortableList Id="disabledOne" Group="disabledSorting" Clone="true" Put="false" Sort="false" Items="items1" Context="item" OnRemove="ListOneRemove">
4+
<FluentSortableList Id="disabledOne" Group="disabledSorting" Drop="false" Sort="false" Items="items1" Context="item" OnRemove="ListOneRemove">
55
<ItemTemplate>@item.Name</ItemTemplate>
66
</FluentSortableList>
77
</FluentGridItem>
88
<FluentGridItem xs="12" sm="6">
99
<h5>List 2</h5>
10-
<FluentSortableList Id="disabledTwo" Group="disabledSorting" Clone="true" Items="items2" Context="item" OnUpdate="SortList">
10+
<FluentSortableList Id="disabledTwo" Group="disabledSorting" Items="items2" Context="item" OnUpdate="SortList">
1111
<ItemTemplate>@item.Name</ItemTemplate>
1212
</FluentSortableList>
1313
</FluentGridItem>

examples/Demo/FluentUI.Demo.Client/Documentation/Components/SortableList/Examples/SortableListMoveBetweenLists.razor

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
// remove the item from the old index in list 1
5050
items1.Remove(items1[args.OldIndex]);
5151

52-
Console.WriteLine($"Moved item from list {args.FromListId} to list {args.ToListId}");
52+
Console.WriteLine($"Moved item '{item.Name}' from list '{args.FromListId}' to list '{args.ToListId}'");
5353
}
5454

5555
private void ListTwoRemove(FluentSortableListEventArgs args)
@@ -67,8 +67,9 @@
6767
// remove the item from the old index in list 2
6868
items2.Remove(items2[args.OldIndex]);
6969

70-
Console.WriteLine($"Moved item from list {args.FromListId} to list {args.ToListId}");
70+
Console.WriteLine($"Moved item '{item.Name}' from list '{args.FromListId}' to list '{args.ToListId}'");
7171
}
72+
7273
private void SortListOne(FluentSortableListEventArgs args)
7374
{
7475
if (args is null || args.OldIndex == args.NewIndex)

src/Core.Scripts/package-lock.json

Lines changed: 0 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Core.Scripts/src/Components/SortableList/FluentSortableList.ts

Lines changed: 147 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,10 @@
11
import type Sortable from 'sortablejs';
2-
import { SortableJSUrl } from '../../ExternalLibs';
2+
import { ExternalLibraryLoader, SortableCdn } from '../../ExternalLibs';
33

4-
declare global {
5-
interface Window {
6-
Sortable?: typeof Sortable;
7-
}
8-
}
9-
10-
let loadPromise: Promise<typeof Sortable> | null = null;
11-
12-
/**
13-
* Dynamically loads SortableJS from CDN if not already loaded
14-
*/
15-
async function ensureSortableJSLoaded(): Promise<typeof Sortable> {
16-
// Check if SortableJS is already available
17-
if (window.Sortable) {
18-
return window.Sortable;
19-
}
20-
21-
if (loadPromise) {
22-
return loadPromise;
23-
}
24-
25-
// Load SortableJS from CDN
26-
loadPromise = new Promise((resolve, reject) => {
27-
const script = document.createElement('script');
28-
script.src = SortableJSUrl;
29-
script.onload = () => {
30-
if (window.Sortable) {
31-
resolve(window.Sortable);
32-
} else {
33-
reject(new Error('SortableJS library failed to load'));
34-
}
35-
};
36-
script.onerror = () => {
37-
loadPromise = null;
38-
reject(new Error('Failed to load SortableJS from CDN'));
39-
};
40-
document.head.appendChild(script);
41-
});
42-
43-
return loadPromise;
44-
}
4+
const sortableLoader = new ExternalLibraryLoader<typeof Sortable>(SortableCdn.name, SortableCdn.url);
455

466
export namespace Microsoft.FluentUI.Blazor.Components.SortableList {
47-
export async function init(
7+
export async function Initialize(
488
list: HTMLElement,
499
group: string,
5010
pull: any,
@@ -56,11 +16,10 @@ export namespace Microsoft.FluentUI.Blazor.Components.SortableList {
5616
component: any
5717
): Promise<any> {
5818

59-
await ensureSortableJSLoaded();
19+
const Sortable = await sortableLoader.load();
6020

6121
const controller = new AbortController();
6222
const { signal } = controller;
63-
let grabMode: boolean = false;
6423

6524
if (group) {
6625
list.setAttribute('data-sortable-group', group);
@@ -106,44 +65,110 @@ export namespace Microsoft.FluentUI.Blazor.Components.SortableList {
10665
});
10766

10867
list.addEventListener('keydown', (event: KeyboardEvent) => {
109-
const item = document.activeElement;
110-
if (item == null) {
68+
const item = document.activeElement as HTMLElement;
69+
if (item == null || !item.classList.contains('sortable-item')) {
11170
return;
11271
}
11372

114-
if (!item.classList.contains('sortable-item')) return;
73+
const isGrabbed = item.getAttribute('aria-grabbed') === 'true';
11574

11675
switch (event.key) {
11776
case 'Enter':
11877
case ' ':
119-
grabMode = !grabMode;
120-
item.setAttribute('aria-grabbed', grabMode.toString());
78+
item.setAttribute('aria-grabbed', (!isGrabbed).toString());
12179
event.preventDefault();
12280
break;
12381

12482
case 'ArrowUp':
125-
if (grabMode && item.previousElementSibling) {
126-
item.parentNode!.insertBefore(item, item.previousElementSibling);
127-
(item as HTMLElement).focus();
128-
} else if (item.previousElementSibling) {
129-
(item.previousElementSibling as HTMLElement).focus();
83+
if (item.previousElementSibling) {
84+
if (isGrabbed) {
85+
if (!sortable.options.sort) {
86+
item.setAttribute('aria-grabbed', 'false');
87+
event.preventDefault();
88+
break;
89+
}
90+
const oldIndex = Array.from(item.parentNode!.children).indexOf(item);
91+
const newIndex = oldIndex - 1;
92+
93+
item.parentNode!.insertBefore(item, item.previousElementSibling);
94+
95+
const updateEvent = new CustomEvent('update') as any;
96+
updateEvent.item = item;
97+
updateEvent.from = list;
98+
updateEvent.to = list;
99+
updateEvent.oldIndex = oldIndex;
100+
updateEvent.newIndex = newIndex;
101+
updateEvent.oldDraggableIndex = oldIndex;
102+
updateEvent.newDraggableIndex = newIndex;
103+
104+
sortable.options.onUpdate?.(updateEvent);
105+
106+
setTimeout(() => {
107+
const refreshedList = document.getElementById(list.id);
108+
const movedItem = refreshedList?.children[newIndex] as HTMLElement;
109+
if (movedItem) {
110+
movedItem.focus();
111+
movedItem.setAttribute('aria-grabbed', 'true');
112+
}
113+
const oldPositionItem = refreshedList?.children[oldIndex] as HTMLElement;
114+
if (oldPositionItem) {
115+
oldPositionItem.setAttribute('aria-grabbed', 'false');
116+
}
117+
}, 50);
118+
} else {
119+
(item.previousElementSibling as HTMLElement).focus();
120+
}
130121
}
131122
event.preventDefault();
132123
break;
133124

134125
case 'ArrowDown':
135-
if (grabMode && item.nextElementSibling) {
136-
item.parentNode!.insertBefore(item.nextElementSibling, item);
137-
(item as HTMLElement).focus();
138-
} else if (item.nextElementSibling) {
139-
(item.nextElementSibling as HTMLElement).focus();
126+
if (item.nextElementSibling) {
127+
if (isGrabbed) {
128+
if (!sortable.options.sort) {
129+
item.setAttribute('aria-grabbed', 'false');
130+
event.preventDefault();
131+
break;
132+
}
133+
const oldIndex = Array.from(item.parentNode!.children).indexOf(item);
134+
const newIndex = oldIndex + 1;
135+
136+
item.parentNode!.insertBefore(item.nextElementSibling, item);
137+
138+
const updateEvent = new CustomEvent('update') as any;
139+
updateEvent.item = item;
140+
updateEvent.from = list;
141+
updateEvent.to = list;
142+
updateEvent.oldIndex = oldIndex;
143+
updateEvent.newIndex = newIndex;
144+
updateEvent.oldDraggableIndex = oldIndex;
145+
updateEvent.newDraggableIndex = newIndex;
146+
147+
sortable.options.onUpdate?.(updateEvent);
148+
149+
setTimeout(() => {
150+
const refreshedList = document.getElementById(list.id);
151+
const movedItem = refreshedList?.children[newIndex] as HTMLElement;
152+
if (movedItem) {
153+
movedItem.focus();
154+
movedItem.setAttribute('aria-grabbed', 'true');
155+
}
156+
const oldPositionItem = refreshedList?.children[oldIndex] as HTMLElement;
157+
if (oldPositionItem) {
158+
oldPositionItem.setAttribute('aria-grabbed', 'false');
159+
}
160+
}, 50);
161+
} else {
162+
(item.nextElementSibling as HTMLElement).focus();
163+
}
140164
}
141165
event.preventDefault();
142166
break;
143167

144168
case 'ArrowLeft':
145169
case 'ArrowRight':
146-
if (grabMode && group) {
170+
if (group) {
171+
147172
const allLists = Array.from(document.querySelectorAll(`[data-sortable-group="${group}"]`));
148173
const currentIndex = allLists.indexOf(list);
149174
let nextIndex = currentIndex;
@@ -155,39 +180,78 @@ export namespace Microsoft.FluentUI.Blazor.Components.SortableList {
155180
}
156181

157182
if (nextIndex !== currentIndex) {
158-
const targetList = allLists[nextIndex] as HTMLElement;
159-
const targetListId = targetList.id;
160-
const oldIndex = Array.from(item.parentNode!.children).indexOf(item);
161-
const newIndex = 0;
183+
if (isGrabbed) {
162184

163-
sortable.options.onRemove!({
164-
item: item,
165-
from: list,
166-
to: targetList,
167-
oldIndex: oldIndex,
168-
newIndex: newIndex,
169-
oldDraggableIndex: oldIndex,
170-
newDraggableIndex: newIndex
171-
} as any);
185+
const targetList = allLists[nextIndex] as HTMLElement;
186+
const targetListId = targetList.id;
187+
const oldIndex = Array.from(item.parentNode!.children).indexOf(item);
188+
const newIndex = 0;
172189

173-
item.setAttribute('aria-grabbed', 'false');
174-
grabMode = false;
190+
const pullMode = pull || true
191+
//const putMode = put ? true : null;
175192

176-
setTimeout(() => {
177-
const newList = document.getElementById(targetListId);
178-
const movedItem = newList?.children[newIndex] as HTMLElement;
179-
movedItem?.focus();
180-
}, 50);
193+
if (pullMode === false) {
194+
break;
195+
}
196+
197+
// Move in DOM immediately for feedback
198+
targetList.insertBefore(item, targetList.firstChild);
199+
200+
// Notify via sortable.onRemove
201+
const removeEvent = new CustomEvent('remove') as any;
202+
removeEvent.item = item;
203+
removeEvent.from = list;
204+
removeEvent.to = targetList;
205+
removeEvent.oldIndex = oldIndex;
206+
removeEvent.newIndex = newIndex;
207+
removeEvent.oldDraggableIndex = oldIndex;
208+
removeEvent.newDraggableIndex = newIndex;
209+
removeEvent.pullMode = pullMode;
210+
if (pullMode === 'clone') {
211+
removeEvent.clone = item.cloneNode(true);
212+
}
213+
214+
sortable.options.onRemove?.(removeEvent);
215+
216+
setTimeout(() => {
217+
const refreshedTargetList = document.getElementById(targetListId);
218+
const movedItem = refreshedTargetList?.children[newIndex] as HTMLElement;
219+
if (movedItem) {
220+
movedItem.focus();
221+
movedItem.setAttribute('aria-grabbed', 'false');
222+
}
223+
const refreshedSourceList = document.getElementById(list.id);
224+
const oldPositionItem = refreshedSourceList?.children[oldIndex] as HTMLElement;
225+
if (oldPositionItem) {
226+
oldPositionItem.setAttribute('aria-grabbed', 'false');
227+
}
228+
}, 50);
229+
}
230+
else {
231+
const targetList = allLists[nextIndex] as HTMLElement;
232+
const itemIndex = Array.from(item.parentNode!.children).indexOf(item);
233+
let targetItem = targetList.children[itemIndex] as HTMLElement;
234+
235+
if (!targetItem && targetList.children.length > 0) {
236+
const firstDist = Math.abs(itemIndex - 0);
237+
const lastDist = Math.abs(itemIndex - (targetList.children.length - 1));
238+
targetItem = (firstDist < lastDist ? targetList.firstElementChild : targetList.lastElementChild) as HTMLElement;
239+
}
240+
241+
if (targetItem) {
242+
targetItem.focus();
243+
}
244+
}
181245
}
182246
}
183247
event.preventDefault();
184248
break;
185249

186250
case 'Tab':
187-
if (item.getAttribute('aria-grabbed') === 'true') {
251+
if (isGrabbed) {
188252
item.setAttribute('aria-grabbed', 'false');
189-
grabMode = false;
190253
}
254+
break;
191255
}
192256
}, { signal });
193257

src/Core.Scripts/src/ExternalLibs.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ export const IMaskCdn = {
88
name: 'IMask',
99
url: 'https://unpkg.com/imask@7.6.1/dist/imask.min.js'
1010
};
11-
export const IMaskUrl = 'https://unpkg.com/imask@7.6.1/dist/imask.min.js';
12-
export const SortableJSUrl = 'https://unpkg.com/sortablejs@1.15.6/Sortable.min.js';
11+
export const SortableCdn = {
12+
name: 'Sortable',
13+
url: 'https://unpkg.com/sortablejs@1.15.6/Sortable.min.js'
14+
};
1315

1416
/*
1517
---------------------------------------------------------------------------------------

0 commit comments

Comments
 (0)