@lilydesignsystem/nunjucks-text-size-picker 0.1.1 → 0.2.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/dist/index.d.ts +135 -0
- package/dist/index.js +102 -0
- package/dist/text-size-picker.njk +1 -0
- package/index.md +4 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -70,6 +70,138 @@ function safeStorageSet(key, value) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// ---------------------------------------------------------------------
|
|
74
|
+
// Tooltip (purely visual; the same text is already the button's
|
|
75
|
+
// aria-label, so it is NOT linked with aria-describedby). Visible iff the
|
|
76
|
+
// popup is closed, it has not been dismissed with Escape, and the pointer
|
|
77
|
+
// is over the button or the tooltip (hoverable, WCAG 1.4.13) or the
|
|
78
|
+
// button has KEYBOARD focus (:focus-visible; a mouse click's focus does
|
|
79
|
+
// not count). Never interferes with the picker's own keys or closing.
|
|
80
|
+
// Idempotent: wiring the same tooltip twice replaces the first wiring.
|
|
81
|
+
// ---------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
const tooltipWirings = new WeakMap();
|
|
84
|
+
|
|
85
|
+
function wireTooltip(root, button, selector, popup) {
|
|
86
|
+
const tooltip = root.querySelector(selector);
|
|
87
|
+
if (!tooltip || !button || !popup) return () => {};
|
|
88
|
+
const previous = tooltipWirings.get(tooltip);
|
|
89
|
+
if (previous) previous();
|
|
90
|
+
|
|
91
|
+
let hoverButton = false;
|
|
92
|
+
let hoverTooltip = false;
|
|
93
|
+
let focusButton = false;
|
|
94
|
+
let dismissed = false;
|
|
95
|
+
|
|
96
|
+
// WCAG 1.4.13 "dismissable": Escape must work wherever focus is while
|
|
97
|
+
// the tooltip is visible (pointer hover alone leaves focus elsewhere).
|
|
98
|
+
// The document listener exists only while visible; never preventDefault
|
|
99
|
+
// or stopPropagation, never move focus.
|
|
100
|
+
let docListening = false;
|
|
101
|
+
const onDocumentKeydown = (event) => {
|
|
102
|
+
if (event.key === "Escape") {
|
|
103
|
+
dismissed = true;
|
|
104
|
+
update();
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
function syncDocumentListener() {
|
|
108
|
+
const want = !tooltip.hidden;
|
|
109
|
+
if (want && !docListening) {
|
|
110
|
+
document.addEventListener("keydown", onDocumentKeydown);
|
|
111
|
+
docListening = true;
|
|
112
|
+
} else if (!want && docListening) {
|
|
113
|
+
document.removeEventListener("keydown", onDocumentKeydown);
|
|
114
|
+
docListening = false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function update() {
|
|
119
|
+
tooltip.hidden = !(
|
|
120
|
+
popup.hidden &&
|
|
121
|
+
!dismissed &&
|
|
122
|
+
(hoverButton || hoverTooltip || focusButton)
|
|
123
|
+
);
|
|
124
|
+
syncDocumentListener();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const onButtonEnter = () => {
|
|
128
|
+
hoverButton = true;
|
|
129
|
+
dismissed = false;
|
|
130
|
+
update();
|
|
131
|
+
};
|
|
132
|
+
const onButtonLeave = () => {
|
|
133
|
+
hoverButton = false;
|
|
134
|
+
update();
|
|
135
|
+
};
|
|
136
|
+
const onTooltipEnter = () => {
|
|
137
|
+
hoverTooltip = true;
|
|
138
|
+
update();
|
|
139
|
+
};
|
|
140
|
+
const onTooltipLeave = () => {
|
|
141
|
+
hoverTooltip = false;
|
|
142
|
+
update();
|
|
143
|
+
};
|
|
144
|
+
const onButtonFocus = () => {
|
|
145
|
+
try {
|
|
146
|
+
focusButton = button.matches(":focus-visible");
|
|
147
|
+
} catch {
|
|
148
|
+
focusButton = true; // engine without :focus-visible
|
|
149
|
+
}
|
|
150
|
+
update();
|
|
151
|
+
};
|
|
152
|
+
const onButtonBlur = () => {
|
|
153
|
+
focusButton = false;
|
|
154
|
+
dismissed = false;
|
|
155
|
+
update();
|
|
156
|
+
};
|
|
157
|
+
const onButtonKeydown = (event) => {
|
|
158
|
+
if (event.key === "Escape" && !tooltip.hidden) dismissed = true;
|
|
159
|
+
update();
|
|
160
|
+
};
|
|
161
|
+
const onButtonClick = () => {
|
|
162
|
+
hoverButton = false;
|
|
163
|
+
update();
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
button.addEventListener("mouseenter", onButtonEnter);
|
|
167
|
+
button.addEventListener("mouseleave", onButtonLeave);
|
|
168
|
+
button.addEventListener("focus", onButtonFocus);
|
|
169
|
+
button.addEventListener("blur", onButtonBlur);
|
|
170
|
+
button.addEventListener("keydown", onButtonKeydown);
|
|
171
|
+
button.addEventListener("click", onButtonClick);
|
|
172
|
+
tooltip.addEventListener("mouseenter", onTooltipEnter);
|
|
173
|
+
tooltip.addEventListener("mouseleave", onTooltipLeave);
|
|
174
|
+
|
|
175
|
+
// The popup's own open/close (including the programmatic API) flips its
|
|
176
|
+
// `hidden` attribute; follow it so the tooltip never shows over it.
|
|
177
|
+
const observer =
|
|
178
|
+
typeof MutationObserver === "function"
|
|
179
|
+
? new MutationObserver(update)
|
|
180
|
+
: null;
|
|
181
|
+
if (observer) observer.observe(popup, { attributes: true, attributeFilter: ["hidden"] });
|
|
182
|
+
|
|
183
|
+
update();
|
|
184
|
+
|
|
185
|
+
const destroy = () => {
|
|
186
|
+
button.removeEventListener("mouseenter", onButtonEnter);
|
|
187
|
+
button.removeEventListener("mouseleave", onButtonLeave);
|
|
188
|
+
button.removeEventListener("focus", onButtonFocus);
|
|
189
|
+
button.removeEventListener("blur", onButtonBlur);
|
|
190
|
+
button.removeEventListener("keydown", onButtonKeydown);
|
|
191
|
+
button.removeEventListener("click", onButtonClick);
|
|
192
|
+
tooltip.removeEventListener("mouseenter", onTooltipEnter);
|
|
193
|
+
tooltip.removeEventListener("mouseleave", onTooltipLeave);
|
|
194
|
+
if (observer) observer.disconnect();
|
|
195
|
+
if (docListening) {
|
|
196
|
+
document.removeEventListener("keydown", onDocumentKeydown);
|
|
197
|
+
docListening = false;
|
|
198
|
+
}
|
|
199
|
+
if (tooltipWirings.get(tooltip) === destroy) tooltipWirings.delete(tooltip);
|
|
200
|
+
};
|
|
201
|
+
tooltipWirings.set(tooltip, destroy);
|
|
202
|
+
return destroy;
|
|
203
|
+
}
|
|
204
|
+
|
|
73
205
|
/**
|
|
74
206
|
* Wire one rendered TextSizePicker root.
|
|
75
207
|
*
|
|
@@ -276,9 +408,12 @@ function initTextSizePicker(root, opts = {}) {
|
|
|
276
408
|
|
|
277
409
|
if (initial) applySize(initial);
|
|
278
410
|
|
|
411
|
+
const tooltipDestroy = wireTooltip(root, button, ".text-size-picker-tooltip", list);
|
|
412
|
+
|
|
279
413
|
return {
|
|
280
414
|
setSize: applySize,
|
|
281
415
|
destroy: () => {
|
|
416
|
+
tooltipDestroy();
|
|
282
417
|
keyboard.destroy();
|
|
283
418
|
button.removeEventListener("click", onButtonClick);
|
|
284
419
|
button.removeEventListener("keydown", onButtonKeydown);
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,106 @@ function safeStorageSet(key, value) {
|
|
|
16
16
|
} catch (_e) {
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
var tooltipWirings = /* @__PURE__ */ new WeakMap();
|
|
20
|
+
function wireTooltip(root, button, selector, popup) {
|
|
21
|
+
const tooltip = root.querySelector(selector);
|
|
22
|
+
if (!tooltip || !button || !popup) return () => {
|
|
23
|
+
};
|
|
24
|
+
const previous = tooltipWirings.get(tooltip);
|
|
25
|
+
if (previous) previous();
|
|
26
|
+
let hoverButton = false;
|
|
27
|
+
let hoverTooltip = false;
|
|
28
|
+
let focusButton = false;
|
|
29
|
+
let dismissed = false;
|
|
30
|
+
let docListening = false;
|
|
31
|
+
const onDocumentKeydown = (event) => {
|
|
32
|
+
if (event.key === "Escape") {
|
|
33
|
+
dismissed = true;
|
|
34
|
+
update();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
function syncDocumentListener() {
|
|
38
|
+
const want = !tooltip.hidden;
|
|
39
|
+
if (want && !docListening) {
|
|
40
|
+
document.addEventListener("keydown", onDocumentKeydown);
|
|
41
|
+
docListening = true;
|
|
42
|
+
} else if (!want && docListening) {
|
|
43
|
+
document.removeEventListener("keydown", onDocumentKeydown);
|
|
44
|
+
docListening = false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function update() {
|
|
48
|
+
tooltip.hidden = !(popup.hidden && !dismissed && (hoverButton || hoverTooltip || focusButton));
|
|
49
|
+
syncDocumentListener();
|
|
50
|
+
}
|
|
51
|
+
const onButtonEnter = () => {
|
|
52
|
+
hoverButton = true;
|
|
53
|
+
dismissed = false;
|
|
54
|
+
update();
|
|
55
|
+
};
|
|
56
|
+
const onButtonLeave = () => {
|
|
57
|
+
hoverButton = false;
|
|
58
|
+
update();
|
|
59
|
+
};
|
|
60
|
+
const onTooltipEnter = () => {
|
|
61
|
+
hoverTooltip = true;
|
|
62
|
+
update();
|
|
63
|
+
};
|
|
64
|
+
const onTooltipLeave = () => {
|
|
65
|
+
hoverTooltip = false;
|
|
66
|
+
update();
|
|
67
|
+
};
|
|
68
|
+
const onButtonFocus = () => {
|
|
69
|
+
try {
|
|
70
|
+
focusButton = button.matches(":focus-visible");
|
|
71
|
+
} catch {
|
|
72
|
+
focusButton = true;
|
|
73
|
+
}
|
|
74
|
+
update();
|
|
75
|
+
};
|
|
76
|
+
const onButtonBlur = () => {
|
|
77
|
+
focusButton = false;
|
|
78
|
+
dismissed = false;
|
|
79
|
+
update();
|
|
80
|
+
};
|
|
81
|
+
const onButtonKeydown = (event) => {
|
|
82
|
+
if (event.key === "Escape" && !tooltip.hidden) dismissed = true;
|
|
83
|
+
update();
|
|
84
|
+
};
|
|
85
|
+
const onButtonClick = () => {
|
|
86
|
+
hoverButton = false;
|
|
87
|
+
update();
|
|
88
|
+
};
|
|
89
|
+
button.addEventListener("mouseenter", onButtonEnter);
|
|
90
|
+
button.addEventListener("mouseleave", onButtonLeave);
|
|
91
|
+
button.addEventListener("focus", onButtonFocus);
|
|
92
|
+
button.addEventListener("blur", onButtonBlur);
|
|
93
|
+
button.addEventListener("keydown", onButtonKeydown);
|
|
94
|
+
button.addEventListener("click", onButtonClick);
|
|
95
|
+
tooltip.addEventListener("mouseenter", onTooltipEnter);
|
|
96
|
+
tooltip.addEventListener("mouseleave", onTooltipLeave);
|
|
97
|
+
const observer = typeof MutationObserver === "function" ? new MutationObserver(update) : null;
|
|
98
|
+
if (observer) observer.observe(popup, { attributes: true, attributeFilter: ["hidden"] });
|
|
99
|
+
update();
|
|
100
|
+
const destroy = () => {
|
|
101
|
+
button.removeEventListener("mouseenter", onButtonEnter);
|
|
102
|
+
button.removeEventListener("mouseleave", onButtonLeave);
|
|
103
|
+
button.removeEventListener("focus", onButtonFocus);
|
|
104
|
+
button.removeEventListener("blur", onButtonBlur);
|
|
105
|
+
button.removeEventListener("keydown", onButtonKeydown);
|
|
106
|
+
button.removeEventListener("click", onButtonClick);
|
|
107
|
+
tooltip.removeEventListener("mouseenter", onTooltipEnter);
|
|
108
|
+
tooltip.removeEventListener("mouseleave", onTooltipLeave);
|
|
109
|
+
if (observer) observer.disconnect();
|
|
110
|
+
if (docListening) {
|
|
111
|
+
document.removeEventListener("keydown", onDocumentKeydown);
|
|
112
|
+
docListening = false;
|
|
113
|
+
}
|
|
114
|
+
if (tooltipWirings.get(tooltip) === destroy) tooltipWirings.delete(tooltip);
|
|
115
|
+
};
|
|
116
|
+
tooltipWirings.set(tooltip, destroy);
|
|
117
|
+
return destroy;
|
|
118
|
+
}
|
|
19
119
|
function initTextSizePicker(root, opts = {}) {
|
|
20
120
|
const noop = { setSize: () => {
|
|
21
121
|
}, destroy: () => {
|
|
@@ -129,9 +229,11 @@ function initTextSizePicker(root, opts = {}) {
|
|
|
129
229
|
if (!initial && values.includes("medium")) initial = "medium";
|
|
130
230
|
if (!initial && values.length > 0) initial = values[0];
|
|
131
231
|
if (initial) applySize(initial);
|
|
232
|
+
const tooltipDestroy = wireTooltip(root, button, ".text-size-picker-tooltip", list);
|
|
132
233
|
return {
|
|
133
234
|
setSize: applySize,
|
|
134
235
|
destroy: () => {
|
|
236
|
+
tooltipDestroy();
|
|
135
237
|
keyboard.destroy();
|
|
136
238
|
button.removeEventListener("click", onButtonClick);
|
|
137
239
|
button.removeEventListener("keydown", onButtonKeydown);
|
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
<button type="button" class="text-size-picker-button" aria-label="{{ opts.label }}" aria-haspopup="listbox" aria-expanded="false" aria-controls="{{ listId }}" data-lily-text-size-picker-button>
|
|
87
87
|
{%- if caller %}{{ caller() }}{% else %}<svg class="text-size-picker-icon" viewBox="0 0 16 16" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" width="1.05rem" height="1.05rem"><path d="M4 13 7.2 3h1.6L12 13M5.4 9.5h5.2"/></svg>{% endif -%}
|
|
88
88
|
</button>
|
|
89
|
+
<div class="text-size-picker-tooltip" role="tooltip" id="{{ id }}-tooltip" hidden>{{ opts.label }}</div>
|
|
89
90
|
<ul class="text-size-picker-list" id="{{ listId }}" role="listbox" aria-label="{{ opts.label }}" tabindex="-1" hidden data-lily-text-size-picker-list>
|
|
90
91
|
{%- for slug in opts.sizes -%}
|
|
91
92
|
<li class="text-size-picker-option" id="{{ id }}-option-{{ loop.index0 }}" role="option" aria-selected="{{ 'true' if slug == selected else 'false' }}" data-value="{{ slug }}">
|
package/index.md
CHANGED
|
@@ -295,6 +295,10 @@ acceptance criterion in [spec/index.md §7](./spec/index.md#7-testing-acceptance
|
|
|
295
295
|
MIT or Apache-2.0 or GPL-2.0 or GPL-3.0 or BSD-3-Clause. Contact
|
|
296
296
|
joel@joelparkerhenderson.com for other terms.
|
|
297
297
|
|
|
298
|
+
## Tooltip
|
|
299
|
+
|
|
300
|
+
`.text-size-picker-tooltip` (`<div role="tooltip" id="{id}-tooltip" hidden>`) is rendered right after the icon button, holding the button's `label`. `text-size-picker.client.js` shows it on pointer hover (button or tooltip) or keyboard focus (`:focus-visible`), hides it on `Escape` (focus stays) and never shows it while the popup is open. Not linked with `aria-describedby`. See `spec/index.md` (Tooltip).
|
|
301
|
+
|
|
298
302
|
---
|
|
299
303
|
|
|
300
304
|
Lily™ and Lily Design System™ are trademarks.
|