Skip to content

Commit 1628caf

Browse files
committed
Overhaul the Font Awesome picker
The previous implementation relied on the legacy implementation of dialogs which causes a stacking issue when opened from a new dialog. Another flaw was the abysmal accessibility because it was impossible to reach the icons with a keyboard navigation. In addition, the new implementation no longer relies on an initialization method which was used in a prior iteration.
1 parent 1f73fde commit 1628caf

File tree

4 files changed

+171
-184
lines changed

4 files changed

+171
-184
lines changed
Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
<script>
2-
require(['Language', 'WoltLabSuite/Core/Ui/Style/FontAwesome'], (Language, UiStyleFontAwesome) => {
3-
Language.addObject({
4-
'wcf.global.filter.button.clear': '{jslang}wcf.global.filter.button.clear{/jslang}',
5-
'wcf.global.filter.error.noMatches': '{jslang}wcf.global.filter.error.noMatches{/jslang}',
6-
'wcf.global.filter.placeholder': '{jslang}wcf.global.filter.placeholder{/jslang}',
7-
'wcf.global.fontAwesome.selectIcon': '{jslang}wcf.global.fontAwesome.selectIcon{/jslang}'
8-
});
9-
10-
UiStyleFontAwesome.setup();
11-
});
2+
{jsphrase name='wcf.global.filter.button.clear'}
3+
{jsphrase name='wcf.global.filter.error.noMatches'}
4+
{jsphrase name='wcf.global.filter.placeholder'}
5+
{jsphrase name='wcf.global.fontAwesome.selectIcon'}
126
</script>

ts/WoltLabSuite/Core/Ui/Style/FontAwesome.ts

Lines changed: 82 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -7,94 +7,86 @@
77
* @woltlabExcludeBundle tiny
88
*/
99

10-
import { DialogCallbackObject, DialogCallbackSetup } from "../Dialog/Data";
11-
import * as Language from "../../Language";
12-
import UiDialog from "../Dialog";
10+
import { getPhrase } from "WoltLabSuite/Core/Language";
1311
import UiItemListFilter from "../ItemList/Filter";
12+
import { dialogFactory } from "WoltLabSuite/Core/Component/Dialog";
1413

1514
type CallbackSelect = (icon: string, forceSolid: boolean) => void;
15+
type IconData = { icon: string; forceSolid: boolean };
16+
17+
function createIconList(): HTMLElement {
18+
const ul = document.createElement("ul");
19+
ul.classList.add("fontAwesome__icons");
20+
ul.id = "fontAwesomeIcons";
21+
22+
const icons: string[] = [];
23+
window.getFontAwesome7Metadata().forEach(([, hasRegular], name) => {
24+
if (hasRegular) {
25+
icons.push(
26+
`<li><button type="button" class="fontAwesome__icon"><fa-icon size="48" name="${name}" solid></fa-icon><small class="fontAwesome__icon__name">${name}</small></button></li>`,
27+
);
28+
}
29+
30+
icons.push(
31+
`<li><button type="button" class="fontAwesome__icon"><fa-icon size="48" name="${name}"></fa-icon><small class="fontAwesome__icon__name">${name}</small></button></li>`,
32+
);
33+
});
1634

17-
class UiStyleFontAwesome implements DialogCallbackObject {
18-
private callback?: CallbackSelect = undefined;
19-
private iconList?: HTMLElement = undefined;
20-
private itemListFilter?: UiItemListFilter = undefined;
21-
22-
open(callback: CallbackSelect): void {
23-
this.callback = callback;
24-
25-
UiDialog.open(this);
26-
}
27-
28-
/**
29-
* Selects an icon, notifies the callback and closes the dialog.
30-
*/
31-
protected click(event: MouseEvent): void {
32-
event.preventDefault();
35+
ul.innerHTML = icons.join("");
3336

34-
const target = event.target as HTMLElement;
35-
const item = target.closest("li") as HTMLLIElement;
36-
const icon = item.querySelector("fa-icon")!;
37+
return ul;
38+
}
3739

38-
UiDialog.close(this);
40+
let content: HTMLElement | undefined = undefined;
41+
function getContent(): HTMLElement {
42+
if (content === undefined) {
43+
const iconList = createIconList();
44+
iconList.addEventListener("click", (event) => {
45+
event.preventDefault();
46+
47+
const { target } = event;
48+
if (!(target instanceof HTMLButtonElement)) {
49+
return;
50+
}
51+
52+
const icon = target.querySelector("fa-icon")!;
53+
const selectedEvent = new CustomEvent<IconData>("font-awesome:selected", {
54+
bubbles: true,
55+
detail: {
56+
icon: icon.name,
57+
forceSolid: icon.solid,
58+
},
59+
});
60+
iconList.dispatchEvent(selectedEvent);
61+
});
3962

40-
this.callback!(icon.name, icon.solid);
63+
content = document.createElement("div");
64+
content.id = "fontAwesomeSelection";
65+
content.append(iconList);
4166
}
4267

43-
_dialogSetup(): ReturnType<DialogCallbackSetup> {
44-
return {
45-
id: "fontAwesomeSelection",
46-
options: {
47-
onSetup: () => {
48-
this.iconList = document.getElementById("fontAwesomeIcons") as HTMLElement;
49-
50-
const icons: string[] = [];
51-
window.getFontAwesome7Metadata().forEach(([, hasRegular], name) => {
52-
if (hasRegular) {
53-
icons.push(`<li><fa-icon size="48" name="${name}" solid></fa-icon><small>${name}</small></li>`);
54-
}
55-
56-
icons.push(`<li><fa-icon size="48" name="${name}"></fa-icon><small>${name}</small></li>`);
57-
});
58-
59-
// build icons
60-
this.iconList.innerHTML = icons.join("");
61-
62-
this.iconList.addEventListener("click", (ev) => this.click(ev));
63-
64-
this.itemListFilter = new UiItemListFilter("fontAwesomeIcons", {
65-
callbackPrepareItem: (item) => {
66-
const small = item.querySelector("small") as HTMLElement;
67-
const text = small.textContent.trim();
68-
69-
return {
70-
item,
71-
span: small,
72-
text,
73-
};
74-
},
75-
enableVisibilityFilter: false,
76-
filterPosition: "top",
77-
});
78-
},
79-
onShow: () => {
80-
this.itemListFilter!.reset();
81-
},
82-
title: Language.get("wcf.global.fontAwesome.selectIcon"),
83-
},
84-
source: '<ul class="fontAwesomeIcons" id="fontAwesomeIcons"></ul>',
85-
};
86-
}
68+
return content;
8769
}
8870

89-
let uiStyleFontAwesome: UiStyleFontAwesome;
90-
91-
/**
92-
* Sets the list of available icons, must be invoked prior to any call
93-
* to the `open()` method.
94-
*/
95-
export function setup(): void {
96-
if (!uiStyleFontAwesome) {
97-
uiStyleFontAwesome = new UiStyleFontAwesome();
71+
let itemListFilter: UiItemListFilter | undefined = undefined;
72+
function setupListeners(): void {
73+
if (itemListFilter === undefined) {
74+
itemListFilter = new UiItemListFilter("fontAwesomeIcons", {
75+
callbackPrepareItem: (item) => {
76+
const small = item.querySelector("small") as HTMLElement;
77+
const text = small.textContent.trim();
78+
79+
return {
80+
item,
81+
span: small,
82+
text,
83+
};
84+
},
85+
enableVisibilityFilter: false,
86+
filterPosition: "top",
87+
});
88+
} else {
89+
itemListFilter.reset();
9890
}
9991
}
10092

@@ -103,11 +95,18 @@ export function setup(): void {
10395
* invoked with the selection icon's name as the only argument.
10496
*/
10597
export function open(callback: CallbackSelect): void {
106-
if (!uiStyleFontAwesome) {
107-
throw new Error(
108-
"Missing icon data, please include the template before calling this method using `{include file='shared_fontAwesomeJavaScript'}`.",
109-
);
110-
}
98+
const dialog = dialogFactory().fromElement(getContent()).asConfirmation();
99+
dialog.addEventListener(
100+
"font-awesome:selected",
101+
(event: CustomEvent<IconData>) => {
102+
dialog.close();
103+
104+
callback(event.detail.icon, event.detail.forceSolid);
105+
},
106+
{ once: true },
107+
);
108+
109+
dialog.show(getPhrase("wcf.global.fontAwesome.selectIcon"));
111110

112-
uiStyleFontAwesome.open(callback);
111+
setupListeners();
113112
}

wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Style/FontAwesome.js

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

0 commit comments

Comments
 (0)