@cobdfamily/clf-core 7.0.1
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 +250 -0
- package/dist/_variables.scss +973 -0
- package/dist/components/cobd-embed.d.ts +4 -0
- package/dist/components/cobd-embed.js +163 -0
- package/dist/components/cobd-nav.d.ts +1 -0
- package/dist/components/cobd-nav.js +383 -0
- package/dist/components/cobd-support.d.ts +26 -0
- package/dist/components/cobd-support.js +296 -0
- package/dist/components/font-scale-toggle.d.ts +9 -0
- package/dist/components/font-scale-toggle.js +159 -0
- package/dist/components/forms/checkbox.d.ts +1 -0
- package/dist/components/forms/checkbox.js +118 -0
- package/dist/components/forms/common.d.ts +17 -0
- package/dist/components/forms/common.js +137 -0
- package/dist/components/forms/index.d.ts +5 -0
- package/dist/components/forms/index.js +13 -0
- package/dist/components/forms/select.d.ts +1 -0
- package/dist/components/forms/select.js +132 -0
- package/dist/components/forms/submit.d.ts +1 -0
- package/dist/components/forms/submit.js +78 -0
- package/dist/components/forms/textarea.d.ts +1 -0
- package/dist/components/forms/textarea.js +95 -0
- package/dist/components/forms/textfield.d.ts +1 -0
- package/dist/components/forms/textfield.js +125 -0
- package/dist/components/index.d.ts +7 -0
- package/dist/components/index.js +33 -0
- package/dist/components/theme-toggle.d.ts +10 -0
- package/dist/components/theme-toggle.js +130 -0
- package/dist/i18n/chrome.json +94 -0
- package/dist/navs/cobd.ca.json +83 -0
- package/dist/navs/more-cobd.json +60 -0
- package/dist/navs.d.ts +27 -0
- package/dist/navs.js +193 -0
- package/dist/theming/font-scale-paint.d.ts +0 -0
- package/dist/theming/font-scale-paint.js +32 -0
- package/dist/theming/init.d.ts +1 -0
- package/dist/theming/init.js +19 -0
- package/dist/theming/runtime.d.ts +22 -0
- package/dist/theming/runtime.js +333 -0
- package/dist/tokens.css +972 -0
- package/dist/tokens.json +97 -0
- package/dist/tokens.scss +95 -0
- package/package.json +123 -0
- package/src/navs/schema.json +64 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// <cobd-support [label="..."] [color="..."]
|
|
3
|
+
// [donate-url="..."]
|
|
4
|
+
// [volunteer-url="..."]
|
|
5
|
+
// [subscribe-url="..."]
|
|
6
|
+
// [share-text="..."]>
|
|
7
|
+
// <!-- optional: child <a> elements override
|
|
8
|
+
// the default action list, like cobd-nav's
|
|
9
|
+
// path="local" mode -->
|
|
10
|
+
// </cobd-support>
|
|
11
|
+
//
|
|
12
|
+
// Drop-in "Support COBD" button. On click, opens
|
|
13
|
+
// an action sheet (`<ion-action-sheet>` if Ionic
|
|
14
|
+
// is on the page; native `popover` element if
|
|
15
|
+
// not) with four default actions:
|
|
16
|
+
//
|
|
17
|
+
// - Donate -> donate-url (cobd.ca/donate)
|
|
18
|
+
// - Volunteer -> volunteer-url (cobd.ca/volunteer)
|
|
19
|
+
// - Spread the word-> Web Share API (clipboard
|
|
20
|
+
// fallback on desktop)
|
|
21
|
+
// - Stay in touch -> subscribe-url (cobd.ca/newsletter)
|
|
22
|
+
//
|
|
23
|
+
// Light DOM. The "Spread the word" handler tries
|
|
24
|
+
// `navigator.share()` first (which on mobile opens
|
|
25
|
+
// the system share sheet); when that's missing or
|
|
26
|
+
// throws (desktop / user-cancelled), it falls back
|
|
27
|
+
// to copying the share text to the clipboard.
|
|
28
|
+
function ionicPresent() {
|
|
29
|
+
return typeof document !== "undefined"
|
|
30
|
+
&& document.querySelector("ion-app") !== null;
|
|
31
|
+
}
|
|
32
|
+
const BASE_STYLE_ID = "cobd-support-base-style";
|
|
33
|
+
const BASE_STYLE = `
|
|
34
|
+
cobd-support { display: inline-block; }
|
|
35
|
+
.cobd-support-menu {
|
|
36
|
+
border: 1px solid var(--cobd-color-medium, #92949c);
|
|
37
|
+
border-radius: var(--cobd-radius-md, 8px);
|
|
38
|
+
padding: var(--cobd-spacing-xs, 4px);
|
|
39
|
+
background: var(--cobd-color-background, #fff);
|
|
40
|
+
color: var(--cobd-color-foreground, #1a1a1a);
|
|
41
|
+
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.15);
|
|
42
|
+
min-width: 14rem;
|
|
43
|
+
margin: 0;
|
|
44
|
+
}
|
|
45
|
+
.cobd-support-menu-item {
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
gap: var(--cobd-spacing-sm, 8px);
|
|
49
|
+
padding: var(--cobd-spacing-sm, 8px) var(--cobd-spacing-md, 16px);
|
|
50
|
+
color: inherit;
|
|
51
|
+
background: none;
|
|
52
|
+
border: 0;
|
|
53
|
+
border-radius: var(--cobd-radius-sm, 4px);
|
|
54
|
+
text-decoration: none;
|
|
55
|
+
font: inherit;
|
|
56
|
+
text-align: left;
|
|
57
|
+
width: 100%;
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
}
|
|
60
|
+
.cobd-support-menu-item:hover,
|
|
61
|
+
.cobd-support-menu-item:focus-visible {
|
|
62
|
+
background: var(--cobd-color-light, #f4f5f8);
|
|
63
|
+
outline: 2px solid var(--cobd-color-primary, #72cadb);
|
|
64
|
+
outline-offset: -2px;
|
|
65
|
+
}
|
|
66
|
+
.cobd-support-button {
|
|
67
|
+
font-family: var(--cobd-typography-family-sans,
|
|
68
|
+
system-ui, sans-serif);
|
|
69
|
+
font-size: var(--cobd-typography-size-md, 1rem);
|
|
70
|
+
font-weight: var(--cobd-typography-weight-medium, 500);
|
|
71
|
+
padding: var(--cobd-spacing-sm, 8px) var(--cobd-spacing-md, 16px);
|
|
72
|
+
border-radius: var(--cobd-radius-sm, 4px);
|
|
73
|
+
border: 0;
|
|
74
|
+
background: var(--cobd-color-primary, #72cadb);
|
|
75
|
+
color: var(--cobd-color-primary-contrast, #000);
|
|
76
|
+
cursor: pointer;
|
|
77
|
+
}
|
|
78
|
+
.cobd-support-button:hover {
|
|
79
|
+
background: var(--cobd-color-primary-strong,
|
|
80
|
+
var(--cobd-color-primary, #72cadb));
|
|
81
|
+
}
|
|
82
|
+
`;
|
|
83
|
+
function ensureBaseStyle() {
|
|
84
|
+
if (typeof document === "undefined")
|
|
85
|
+
return;
|
|
86
|
+
if (document.getElementById(BASE_STYLE_ID))
|
|
87
|
+
return;
|
|
88
|
+
const style = document.createElement("style");
|
|
89
|
+
style.id = BASE_STYLE_ID;
|
|
90
|
+
style.textContent = BASE_STYLE;
|
|
91
|
+
document.head.appendChild(style);
|
|
92
|
+
}
|
|
93
|
+
function readCustomActions(host) {
|
|
94
|
+
const links = Array.from(host.querySelectorAll(":scope > a"));
|
|
95
|
+
if (links.length === 0)
|
|
96
|
+
return null;
|
|
97
|
+
return links.map(a => ({
|
|
98
|
+
label: a.textContent?.trim() ?? "",
|
|
99
|
+
icon: a.getAttribute("data-icon") ?? undefined,
|
|
100
|
+
href: a.getAttribute("href") ?? undefined,
|
|
101
|
+
target: a.getAttribute("target") ?? undefined,
|
|
102
|
+
})).filter(a => a.label);
|
|
103
|
+
}
|
|
104
|
+
class CobdSupport extends HTMLElement {
|
|
105
|
+
constructor() {
|
|
106
|
+
super(...arguments);
|
|
107
|
+
this.rendered = false;
|
|
108
|
+
this.customActions = null;
|
|
109
|
+
}
|
|
110
|
+
static get observedAttributes() {
|
|
111
|
+
return [
|
|
112
|
+
"label", "color",
|
|
113
|
+
"donate-url", "volunteer-url",
|
|
114
|
+
"subscribe-url", "share-text",
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
connectedCallback() {
|
|
118
|
+
ensureBaseStyle();
|
|
119
|
+
if (!this.rendered) {
|
|
120
|
+
// Snapshot the child <a> tags once -- the
|
|
121
|
+
// render path replaceChildren()s the host,
|
|
122
|
+
// which would otherwise wipe them.
|
|
123
|
+
this.customActions = readCustomActions(this);
|
|
124
|
+
this.render();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
attributeChangedCallback() {
|
|
128
|
+
if (this.isConnected && this.rendered)
|
|
129
|
+
this.render();
|
|
130
|
+
}
|
|
131
|
+
render() {
|
|
132
|
+
const label = this.getAttribute("label") ?? "Support COBD";
|
|
133
|
+
const actions = this.customActions ?? this.defaultActions();
|
|
134
|
+
if (ionicPresent()) {
|
|
135
|
+
this.renderIonic(label, actions);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
this.renderPlain(label, actions);
|
|
139
|
+
}
|
|
140
|
+
this.rendered = true;
|
|
141
|
+
}
|
|
142
|
+
defaultActions() {
|
|
143
|
+
return [
|
|
144
|
+
{
|
|
145
|
+
label: "Donate",
|
|
146
|
+
icon: "heart-outline",
|
|
147
|
+
href: this.getAttribute("donate-url")
|
|
148
|
+
?? "https://cobd.ca/donate",
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
label: "Volunteer",
|
|
152
|
+
icon: "hand-right-outline",
|
|
153
|
+
href: this.getAttribute("volunteer-url")
|
|
154
|
+
?? "https://cobd.ca/volunteer",
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
label: "Spread the word",
|
|
158
|
+
icon: "share-social-outline",
|
|
159
|
+
share: true,
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
label: "Stay in touch",
|
|
163
|
+
icon: "mail-outline",
|
|
164
|
+
href: this.getAttribute("subscribe-url")
|
|
165
|
+
?? "https://cobd.ca/newsletter",
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
renderIonic(label, actions) {
|
|
170
|
+
const color = this.getAttribute("color") ?? "primary";
|
|
171
|
+
const btn = document.createElement("ion-button");
|
|
172
|
+
btn.setAttribute("color", color);
|
|
173
|
+
btn.textContent = label;
|
|
174
|
+
btn.addEventListener("click", () => this.openIonicSheet(actions));
|
|
175
|
+
this.replaceChildren(btn);
|
|
176
|
+
}
|
|
177
|
+
async openIonicSheet(actions) {
|
|
178
|
+
const sheet = document.createElement("ion-action-sheet");
|
|
179
|
+
sheet.header = "Support COBD";
|
|
180
|
+
sheet.buttons = [
|
|
181
|
+
...actions.map(a => ({
|
|
182
|
+
text: a.label,
|
|
183
|
+
icon: a.icon,
|
|
184
|
+
handler: () => { this.handleAction(a); },
|
|
185
|
+
})),
|
|
186
|
+
{ text: "Cancel", role: "cancel" },
|
|
187
|
+
];
|
|
188
|
+
document.body.appendChild(sheet);
|
|
189
|
+
sheet.addEventListener("ionActionSheetDidDismiss", () => sheet.remove());
|
|
190
|
+
if (typeof sheet.present === "function") {
|
|
191
|
+
await sheet.present();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
renderPlain(label, actions) {
|
|
195
|
+
const id = "cobd-support-pop-"
|
|
196
|
+
+ Math.random().toString(36).slice(2, 10);
|
|
197
|
+
const btn = document.createElement("button");
|
|
198
|
+
btn.type = "button";
|
|
199
|
+
btn.className = "cobd-support-button";
|
|
200
|
+
btn.textContent = label;
|
|
201
|
+
btn.setAttribute("popovertarget", id);
|
|
202
|
+
btn.setAttribute("aria-haspopup", "menu");
|
|
203
|
+
const popover = document.createElement("div");
|
|
204
|
+
popover.id = id;
|
|
205
|
+
popover.setAttribute("popover", "auto");
|
|
206
|
+
popover.className = "cobd-support-menu";
|
|
207
|
+
popover.setAttribute("role", "menu");
|
|
208
|
+
popover.setAttribute("aria-label", "Support COBD");
|
|
209
|
+
for (const action of actions) {
|
|
210
|
+
popover.appendChild(this.buildPlainItem(action, popover));
|
|
211
|
+
}
|
|
212
|
+
this.replaceChildren(btn, popover);
|
|
213
|
+
}
|
|
214
|
+
buildPlainItem(action, popover) {
|
|
215
|
+
const item = action.href && !action.share
|
|
216
|
+
? document.createElement("a")
|
|
217
|
+
: document.createElement("button");
|
|
218
|
+
item.className = "cobd-support-menu-item";
|
|
219
|
+
item.setAttribute("role", "menuitem");
|
|
220
|
+
if (item instanceof HTMLAnchorElement && action.href) {
|
|
221
|
+
item.href = action.href;
|
|
222
|
+
if (action.target)
|
|
223
|
+
item.target = action.target;
|
|
224
|
+
}
|
|
225
|
+
else if (item instanceof HTMLButtonElement) {
|
|
226
|
+
item.type = "button";
|
|
227
|
+
}
|
|
228
|
+
item.addEventListener("click", async (e) => {
|
|
229
|
+
const handled = await this.handleAction(action);
|
|
230
|
+
if (handled) {
|
|
231
|
+
// Close the popover after a handled action.
|
|
232
|
+
// ".hidePopover()" is part of the native
|
|
233
|
+
// popover API; types may not have it yet
|
|
234
|
+
// in older lib.dom.
|
|
235
|
+
popover.hidePopover?.();
|
|
236
|
+
if (!action.href)
|
|
237
|
+
e.preventDefault();
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
if (action.icon) {
|
|
241
|
+
const ic = document.createElement("ion-icon");
|
|
242
|
+
ic.setAttribute("name", action.icon);
|
|
243
|
+
ic.setAttribute("aria-hidden", "true");
|
|
244
|
+
item.appendChild(ic);
|
|
245
|
+
}
|
|
246
|
+
item.appendChild(document.createTextNode(action.label));
|
|
247
|
+
return item;
|
|
248
|
+
}
|
|
249
|
+
async handleAction(action) {
|
|
250
|
+
if (action.share) {
|
|
251
|
+
const text = this.getAttribute("share-text")
|
|
252
|
+
?? "The Canadian Organization of the Blind "
|
|
253
|
+
+ "and DeafBlind: https://cobd.ca/";
|
|
254
|
+
const url = "https://cobd.ca/";
|
|
255
|
+
if (typeof navigator !== "undefined"
|
|
256
|
+
&& typeof navigator.share === "function") {
|
|
257
|
+
try {
|
|
258
|
+
await navigator.share({
|
|
259
|
+
title: "COBD",
|
|
260
|
+
text,
|
|
261
|
+
url,
|
|
262
|
+
});
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// User cancelled or share API rejected;
|
|
267
|
+
// fall through to clipboard.
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (typeof navigator !== "undefined"
|
|
271
|
+
&& navigator.clipboard) {
|
|
272
|
+
try {
|
|
273
|
+
await navigator.clipboard.writeText(text);
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
if (action.href) {
|
|
283
|
+
if (action.target === "_blank") {
|
|
284
|
+
window.open(action.href, "_blank", "noopener,noreferrer");
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
window.location.href = action.href;
|
|
288
|
+
}
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (!customElements.get("cobd-support")) {
|
|
295
|
+
customElements.define("cobd-support", CobdSupport);
|
|
296
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// <cobd-font-scale-toggle> -- vanilla custom element
|
|
2
|
+
// that lets the user pick a text-size scale
|
|
3
|
+
// (Small / Medium / Large). Persists via the runtime's
|
|
4
|
+
// font-scale storage (matching the theme's choice of
|
|
5
|
+
// localStorage vs cookie); applies the scale to <html>
|
|
6
|
+
// as --cobd-font-scale, which the consumer's stylesheet
|
|
7
|
+
// multiplies into the root font-size:
|
|
8
|
+
//
|
|
9
|
+
// html {
|
|
10
|
+
// font-size: calc(100% * var(--cobd-font-scale, 1));
|
|
11
|
+
// }
|
|
12
|
+
//
|
|
13
|
+
// ARIA: a `role="radiogroup"` with three button-style
|
|
14
|
+
// radios. Arrow keys move focus + selection; Space /
|
|
15
|
+
// Enter activate.
|
|
16
|
+
import { getFontScalePreference, setFontScale, onFontScaleChange, } from "../theming/runtime.js";
|
|
17
|
+
const ORDER = ["sm", "md", "lg"];
|
|
18
|
+
// Visible label per scale + an aria-label that names
|
|
19
|
+
// the scale (the visible label is just "A" rendered at
|
|
20
|
+
// the right size, so screen readers need the explicit
|
|
21
|
+
// name).
|
|
22
|
+
const LABEL = {
|
|
23
|
+
sm: "Small",
|
|
24
|
+
md: "Medium",
|
|
25
|
+
lg: "Large",
|
|
26
|
+
};
|
|
27
|
+
const SIZE = {
|
|
28
|
+
sm: "0.85em",
|
|
29
|
+
md: "1em",
|
|
30
|
+
lg: "1.2em",
|
|
31
|
+
};
|
|
32
|
+
const STYLE = `
|
|
33
|
+
:host {
|
|
34
|
+
display: inline-flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
gap: 0.25rem;
|
|
37
|
+
--_border: var(--cobd-color-medium, currentColor);
|
|
38
|
+
--_active-bg:
|
|
39
|
+
var(--cobd-color-primary-tint,
|
|
40
|
+
var(--ion-color-primary-tint, #80cfdf));
|
|
41
|
+
--_active-fg:
|
|
42
|
+
var(--cobd-color-primary-contrast, #000);
|
|
43
|
+
}
|
|
44
|
+
[role="radiogroup"] {
|
|
45
|
+
display: inline-flex;
|
|
46
|
+
border: 1px solid var(--_border);
|
|
47
|
+
border-radius: var(--cobd-radius-md, 8px);
|
|
48
|
+
overflow: hidden;
|
|
49
|
+
}
|
|
50
|
+
button {
|
|
51
|
+
border: 0;
|
|
52
|
+
background: transparent;
|
|
53
|
+
color: inherit;
|
|
54
|
+
padding: 0.4rem 0.75rem;
|
|
55
|
+
font: inherit;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
line-height: 1;
|
|
58
|
+
border-right: 1px solid var(--_border);
|
|
59
|
+
}
|
|
60
|
+
button:last-of-type { border-right: 0; }
|
|
61
|
+
button:focus-visible {
|
|
62
|
+
outline: 2px solid var(--cobd-color-primary, currentColor);
|
|
63
|
+
outline-offset: 2px;
|
|
64
|
+
}
|
|
65
|
+
button[aria-checked="true"] {
|
|
66
|
+
background: var(--_active-bg);
|
|
67
|
+
color: var(--_active-fg);
|
|
68
|
+
font-weight: 600;
|
|
69
|
+
}
|
|
70
|
+
.glyph {
|
|
71
|
+
display: inline-block;
|
|
72
|
+
line-height: 1;
|
|
73
|
+
}
|
|
74
|
+
`;
|
|
75
|
+
export class CobdFontScaleToggle extends HTMLElement {
|
|
76
|
+
constructor() {
|
|
77
|
+
super(...arguments);
|
|
78
|
+
this.buttons = {};
|
|
79
|
+
this.cleanup = null;
|
|
80
|
+
}
|
|
81
|
+
connectedCallback() {
|
|
82
|
+
if (!this.shadowRoot) {
|
|
83
|
+
const root = this.attachShadow({ mode: "open" });
|
|
84
|
+
const style = document.createElement("style");
|
|
85
|
+
style.textContent = STYLE;
|
|
86
|
+
this.group = document.createElement("div");
|
|
87
|
+
this.group.setAttribute("role", "radiogroup");
|
|
88
|
+
this.group.setAttribute("aria-label", this.getAttribute("aria-label") || "Text size");
|
|
89
|
+
for (const pref of ORDER) {
|
|
90
|
+
const btn = document.createElement("button");
|
|
91
|
+
btn.type = "button";
|
|
92
|
+
btn.setAttribute("role", "radio");
|
|
93
|
+
btn.setAttribute("aria-checked", "false");
|
|
94
|
+
btn.setAttribute("aria-label", LABEL[pref]);
|
|
95
|
+
btn.dataset.cobdScale = pref;
|
|
96
|
+
const glyph = document.createElement("span");
|
|
97
|
+
glyph.className = "glyph";
|
|
98
|
+
glyph.setAttribute("aria-hidden", "true");
|
|
99
|
+
glyph.style.fontSize = SIZE[pref];
|
|
100
|
+
glyph.textContent = "A";
|
|
101
|
+
btn.appendChild(glyph);
|
|
102
|
+
btn.addEventListener("click", () => {
|
|
103
|
+
setFontScale(pref);
|
|
104
|
+
});
|
|
105
|
+
btn.addEventListener("keydown", e => this.onKeyDown(e, pref));
|
|
106
|
+
this.buttons[pref] = btn;
|
|
107
|
+
this.group.appendChild(btn);
|
|
108
|
+
}
|
|
109
|
+
root.appendChild(style);
|
|
110
|
+
root.appendChild(this.group);
|
|
111
|
+
}
|
|
112
|
+
this.render();
|
|
113
|
+
this.cleanup = onFontScaleChange(() => this.render());
|
|
114
|
+
}
|
|
115
|
+
disconnectedCallback() {
|
|
116
|
+
this.cleanup?.();
|
|
117
|
+
this.cleanup = null;
|
|
118
|
+
}
|
|
119
|
+
onKeyDown(e, current) {
|
|
120
|
+
let target = null;
|
|
121
|
+
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
|
|
122
|
+
const i = ORDER.indexOf(current);
|
|
123
|
+
target = ORDER[(i + 1) % ORDER.length];
|
|
124
|
+
}
|
|
125
|
+
else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
|
|
126
|
+
const i = ORDER.indexOf(current);
|
|
127
|
+
target = ORDER[(i - 1 + ORDER.length) % ORDER.length];
|
|
128
|
+
}
|
|
129
|
+
else if (e.key === "Home") {
|
|
130
|
+
target = ORDER[0];
|
|
131
|
+
}
|
|
132
|
+
else if (e.key === "End") {
|
|
133
|
+
target = ORDER[ORDER.length - 1];
|
|
134
|
+
}
|
|
135
|
+
if (target) {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
setFontScale(target);
|
|
138
|
+
this.buttons[target]?.focus();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
render() {
|
|
142
|
+
const pref = getFontScalePreference();
|
|
143
|
+
for (const k of ORDER) {
|
|
144
|
+
const btn = this.buttons[k];
|
|
145
|
+
if (!btn)
|
|
146
|
+
continue;
|
|
147
|
+
const active = k === pref;
|
|
148
|
+
btn.setAttribute("aria-checked", active ? "true" : "false");
|
|
149
|
+
// Roving tabindex: only the active radio is in
|
|
150
|
+
// the tab order; arrow keys move within the
|
|
151
|
+
// group.
|
|
152
|
+
btn.tabIndex = active ? 0 : -1;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (typeof customElements !== "undefined"
|
|
157
|
+
&& !customElements.get("cobd-font-scale-toggle")) {
|
|
158
|
+
customElements.define("cobd-font-scale-toggle", CobdFontScaleToggle);
|
|
159
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// <cobd-checkbox name="..." label="..." [value="..."]
|
|
2
|
+
// [checked] [help="..."] [error="..."]
|
|
3
|
+
// [required]>
|
|
4
|
+
//
|
|
5
|
+
// Single checkbox. Form-associated -- when checked,
|
|
6
|
+
// the form sees `name=value` on POST (where value
|
|
7
|
+
// defaults to "on", matching native <input
|
|
8
|
+
// type="checkbox">). When unchecked, the form sees
|
|
9
|
+
// no field for `name`, also matching native.
|
|
10
|
+
//
|
|
11
|
+
// Renders <ion-checkbox> + label when Ionic is on,
|
|
12
|
+
// plain <input type="checkbox"> + label otherwise.
|
|
13
|
+
import { ionicPresent, ensureBaseStyle, readAttr, readBoolAttr, readFieldDef, buildHelp, buildError, describedByValue, } from "./common.js";
|
|
14
|
+
class CobdCheckbox extends HTMLElement {
|
|
15
|
+
static get observedAttributes() {
|
|
16
|
+
return [
|
|
17
|
+
"name", "label", "value", "checked",
|
|
18
|
+
"help", "error", "required", "id",
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
constructor() {
|
|
22
|
+
super();
|
|
23
|
+
this.rendered = false;
|
|
24
|
+
this.internals = this.attachInternals();
|
|
25
|
+
}
|
|
26
|
+
connectedCallback() {
|
|
27
|
+
ensureBaseStyle();
|
|
28
|
+
if (!this.rendered)
|
|
29
|
+
this.render();
|
|
30
|
+
}
|
|
31
|
+
attributeChangedCallback() {
|
|
32
|
+
if (this.isConnected && this.rendered)
|
|
33
|
+
this.render();
|
|
34
|
+
}
|
|
35
|
+
render() {
|
|
36
|
+
const def = readFieldDef(this, "cobd-checkbox");
|
|
37
|
+
const value = readAttr(this, "value") ?? "on";
|
|
38
|
+
const checked = readBoolAttr(this, "checked");
|
|
39
|
+
const row = document.createElement("div");
|
|
40
|
+
row.className = "cobd-field-row";
|
|
41
|
+
// Layout: checkbox sits next to the label
|
|
42
|
+
// text, not on its own line. Use a flex
|
|
43
|
+
// wrapper so the click target spans both.
|
|
44
|
+
const wrap = document.createElement("label");
|
|
45
|
+
wrap.setAttribute("for", def.id);
|
|
46
|
+
wrap.style.display = "flex";
|
|
47
|
+
wrap.style.alignItems = "center";
|
|
48
|
+
wrap.style.gap = "var(--cobd-spacing-sm, 8px)";
|
|
49
|
+
const control = ionicPresent()
|
|
50
|
+
? this.buildIonCheckbox(def, value, checked)
|
|
51
|
+
: this.buildPlainCheckbox(def, value, checked);
|
|
52
|
+
wrap.appendChild(control);
|
|
53
|
+
const labelText = document.createElement("span");
|
|
54
|
+
labelText.textContent = def.label;
|
|
55
|
+
if (def.required) {
|
|
56
|
+
const star = document.createElement("span");
|
|
57
|
+
star.className = "cobd-required-asterisk";
|
|
58
|
+
star.setAttribute("aria-hidden", "true");
|
|
59
|
+
star.textContent = "*";
|
|
60
|
+
labelText.appendChild(star);
|
|
61
|
+
const sr = document.createElement("span");
|
|
62
|
+
sr.className = "sr-only";
|
|
63
|
+
sr.textContent = " (required)";
|
|
64
|
+
labelText.appendChild(sr);
|
|
65
|
+
}
|
|
66
|
+
wrap.appendChild(labelText);
|
|
67
|
+
row.appendChild(wrap);
|
|
68
|
+
const help = buildHelp(def);
|
|
69
|
+
if (help)
|
|
70
|
+
row.appendChild(help);
|
|
71
|
+
row.appendChild(buildError(def));
|
|
72
|
+
this.replaceChildren(row);
|
|
73
|
+
this.syncForm(checked, value);
|
|
74
|
+
this.rendered = true;
|
|
75
|
+
}
|
|
76
|
+
buildIonCheckbox(def, value, checked) {
|
|
77
|
+
const it = document.createElement("ion-checkbox");
|
|
78
|
+
it.setAttribute("id", def.id);
|
|
79
|
+
if (def.name)
|
|
80
|
+
it.setAttribute("name", def.name);
|
|
81
|
+
it.setAttribute("value", value);
|
|
82
|
+
if (checked)
|
|
83
|
+
it.setAttribute("checked", "");
|
|
84
|
+
if (def.required)
|
|
85
|
+
it.setAttribute("required", "");
|
|
86
|
+
it.setAttribute("aria-describedby", describedByValue(def));
|
|
87
|
+
it.addEventListener("ionChange", e => {
|
|
88
|
+
const isChecked = e.detail?.checked
|
|
89
|
+
?? it.checked;
|
|
90
|
+
this.syncForm(isChecked, value);
|
|
91
|
+
});
|
|
92
|
+
return it;
|
|
93
|
+
}
|
|
94
|
+
buildPlainCheckbox(def, value, checked) {
|
|
95
|
+
const it = document.createElement("input");
|
|
96
|
+
it.type = "checkbox";
|
|
97
|
+
it.id = def.id;
|
|
98
|
+
if (def.name)
|
|
99
|
+
it.name = def.name;
|
|
100
|
+
it.value = value;
|
|
101
|
+
it.checked = checked;
|
|
102
|
+
if (def.required)
|
|
103
|
+
it.required = true;
|
|
104
|
+
it.setAttribute("aria-describedby", describedByValue(def));
|
|
105
|
+
it.addEventListener("change", () => this.syncForm(it.checked, value));
|
|
106
|
+
return it;
|
|
107
|
+
}
|
|
108
|
+
syncForm(checked, value) {
|
|
109
|
+
// Native checkbox semantics: unchecked = field
|
|
110
|
+
// not sent. setFormValue(null) achieves the
|
|
111
|
+
// same on the form-associated element side.
|
|
112
|
+
this.internals.setFormValue(checked ? value : null);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
CobdCheckbox.formAssociated = true;
|
|
116
|
+
if (!customElements.get("cobd-checkbox")) {
|
|
117
|
+
customElements.define("cobd-checkbox", CobdCheckbox);
|
|
118
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare function ionicPresent(): boolean;
|
|
2
|
+
export declare function ensureBaseStyle(): void;
|
|
3
|
+
export declare function readAttr(el: HTMLElement, name: string): string | null;
|
|
4
|
+
export declare function readBoolAttr(el: HTMLElement, name: string): boolean;
|
|
5
|
+
export interface FieldDef {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
label: string;
|
|
9
|
+
help: string | null;
|
|
10
|
+
error: string | null;
|
|
11
|
+
required: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function readFieldDef(el: HTMLElement, fallbackIdPrefix: string): FieldDef;
|
|
14
|
+
export declare function buildLabel(def: FieldDef): HTMLLabelElement;
|
|
15
|
+
export declare function buildHelp(def: FieldDef): HTMLElement | null;
|
|
16
|
+
export declare function buildError(def: FieldDef): HTMLElement;
|
|
17
|
+
export declare function describedByValue(def: FieldDef): string;
|