primer_view_components 0.0.67 → 0.0.70
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +133 -2
- data/README.md +1 -1
- data/app/assets/javascripts/primer_view_components.js +1 -1
- data/app/assets/javascripts/primer_view_components.js.map +1 -1
- data/app/components/primer/alpha/tooltip.d.ts +24 -0
- data/app/components/primer/alpha/tooltip.js +381 -0
- data/app/components/primer/alpha/tooltip.rb +103 -0
- data/app/components/primer/alpha/tooltip.ts +383 -0
- data/app/components/primer/base_component.rb +2 -2
- data/app/components/primer/beta/auto_complete/auto_complete.html.erb +22 -4
- data/app/components/primer/beta/auto_complete.rb +86 -99
- data/app/components/primer/beta/blankslate.html.erb +6 -2
- data/app/components/primer/beta/blankslate.rb +4 -9
- data/app/components/primer/beta/truncate.rb +1 -0
- data/app/components/primer/button_component.html.erb +1 -0
- data/app/components/primer/button_component.rb +29 -0
- data/app/components/primer/component.rb +9 -2
- data/app/components/primer/details_component.rb +1 -1
- data/app/components/primer/icon_button.rb +1 -1
- data/app/components/primer/link_component.erb +4 -0
- data/app/components/primer/link_component.rb +29 -4
- data/app/components/primer/markdown.rb +1 -1
- data/app/components/primer/popover_component.rb +5 -9
- data/app/components/primer/primer.d.ts +1 -0
- data/app/components/primer/primer.js +1 -0
- data/app/components/primer/primer.ts +1 -0
- data/app/components/primer/subhead_component.html.erb +1 -1
- data/app/components/primer/subhead_component.rb +1 -1
- data/app/components/primer/tooltip.rb +1 -1
- data/app/lib/primer/test_selector_helper.rb +1 -1
- data/lib/primer/classify/utilities.yml +28 -0
- data/lib/primer/view_components/linters/button_component_migration_counter.rb +1 -1
- data/lib/primer/view_components/version.rb +1 -1
- data/lib/rubocop/cop/primer/component_name_migration.rb +35 -0
- data/lib/rubocop/cop/primer/primer_octicon.rb +1 -1
- data/lib/tasks/docs.rake +12 -7
- data/lib/tasks/utilities.rake +1 -1
- data/static/arguments.yml +52 -1
- data/static/audited_at.json +1 -1
- data/static/classes.yml +9 -4
- data/static/constants.json +18 -8
- data/static/statuses.json +2 -2
- metadata +13 -9
- data/app/components/primer/auto_complete/auto_complete.d.ts +0 -1
- data/app/components/primer/auto_complete/auto_complete.js +0 -1
@@ -0,0 +1,24 @@
|
|
1
|
+
declare type Direction = 'n' | 's' | 'e' | 'w' | 'ne' | 'se' | 'nw' | 'sw';
|
2
|
+
declare class TooltipElement extends HTMLElement {
|
3
|
+
#private;
|
4
|
+
styles(): string;
|
5
|
+
get htmlFor(): string;
|
6
|
+
set htmlFor(value: string);
|
7
|
+
get type(): 'description' | 'label';
|
8
|
+
set type(value: 'description' | 'label');
|
9
|
+
get direction(): Direction;
|
10
|
+
set direction(value: Direction);
|
11
|
+
get control(): HTMLElement | null;
|
12
|
+
constructor();
|
13
|
+
connectedCallback(): void;
|
14
|
+
disconnectedCallback(): void;
|
15
|
+
handleEvent(event: Event): void;
|
16
|
+
static observedAttributes: string[];
|
17
|
+
attributeChangedCallback(name: string): void;
|
18
|
+
}
|
19
|
+
declare global {
|
20
|
+
interface Window {
|
21
|
+
TooltipElement: typeof TooltipElement;
|
22
|
+
}
|
23
|
+
}
|
24
|
+
export {};
|
@@ -0,0 +1,381 @@
|
|
1
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
2
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
4
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
5
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
6
|
+
};
|
7
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
10
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
11
|
+
};
|
12
|
+
var _TooltipElement_instances, _TooltipElement_abortController, _TooltipElement_align, _TooltipElement_side, _TooltipElement_allowUpdatePosition, _TooltipElement_adjustedAnchorAlignment, _TooltipElement_updatePosition;
|
13
|
+
import { getAnchoredPosition } from '@primer/behaviors';
|
14
|
+
const TOOLTIP_OPEN_CLASS = 'tooltip-open';
|
15
|
+
const TOOLTIP_ARROW_EDGE_OFFSET = 10;
|
16
|
+
const DIRECTION_CLASSES = [
|
17
|
+
'tooltip-n',
|
18
|
+
'tooltip-s',
|
19
|
+
'tooltip-e',
|
20
|
+
'tooltip-w',
|
21
|
+
'tooltip-ne',
|
22
|
+
'tooltip-se',
|
23
|
+
'tooltip-nw',
|
24
|
+
'tooltip-sw'
|
25
|
+
];
|
26
|
+
class TooltipElement extends HTMLElement {
|
27
|
+
constructor() {
|
28
|
+
super();
|
29
|
+
_TooltipElement_instances.add(this);
|
30
|
+
_TooltipElement_abortController.set(this, void 0);
|
31
|
+
_TooltipElement_align.set(this, 'center');
|
32
|
+
_TooltipElement_side.set(this, 'outside-bottom');
|
33
|
+
_TooltipElement_allowUpdatePosition.set(this, false);
|
34
|
+
const shadow = this.attachShadow({ mode: 'open' });
|
35
|
+
shadow.innerHTML = `
|
36
|
+
<style>
|
37
|
+
${this.styles()}
|
38
|
+
</style>
|
39
|
+
<slot></slot>
|
40
|
+
`;
|
41
|
+
}
|
42
|
+
styles() {
|
43
|
+
return `
|
44
|
+
:host {
|
45
|
+
position: absolute;
|
46
|
+
z-index: 1000000;
|
47
|
+
padding: .5em .75em;
|
48
|
+
font: normal normal 11px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
49
|
+
-webkit-font-smoothing: subpixel-antialiased;
|
50
|
+
color: var(--color-fg-on-emphasis);
|
51
|
+
text-align: center;
|
52
|
+
text-decoration: none;
|
53
|
+
text-shadow: none;
|
54
|
+
text-transform: none;
|
55
|
+
letter-spacing: normal;
|
56
|
+
word-wrap: break-word;
|
57
|
+
white-space: pre;
|
58
|
+
background: var(--color-neutral-emphasis-plus);
|
59
|
+
border-radius: 6px;
|
60
|
+
opacity: 0;
|
61
|
+
max-width: 250px;
|
62
|
+
word-wrap: break-word;
|
63
|
+
white-space: normal;
|
64
|
+
width: max-content;
|
65
|
+
}
|
66
|
+
|
67
|
+
:host:before{
|
68
|
+
position: absolute;
|
69
|
+
z-index: 1000001;
|
70
|
+
color: var(--color-neutral-emphasis-plus);
|
71
|
+
content: "";
|
72
|
+
border: 6px solid transparent;
|
73
|
+
opacity: 0
|
74
|
+
}
|
75
|
+
|
76
|
+
@keyframes tooltip-appear {
|
77
|
+
from {
|
78
|
+
opacity: 0
|
79
|
+
}
|
80
|
+
to {
|
81
|
+
opacity: 1
|
82
|
+
}
|
83
|
+
}
|
84
|
+
|
85
|
+
:host:after{
|
86
|
+
position: absolute;
|
87
|
+
display: block;
|
88
|
+
right: 0;
|
89
|
+
left: 0;
|
90
|
+
height: 12px;
|
91
|
+
content: ""
|
92
|
+
}
|
93
|
+
|
94
|
+
:host(.${TOOLTIP_OPEN_CLASS}),
|
95
|
+
:host(.${TOOLTIP_OPEN_CLASS}):before {
|
96
|
+
animation-name: tooltip-appear;
|
97
|
+
animation-duration: .1s;
|
98
|
+
animation-fill-mode: forwards;
|
99
|
+
animation-timing-function: ease-in;
|
100
|
+
animation-delay: .4s
|
101
|
+
}
|
102
|
+
|
103
|
+
:host(.tooltip-s):before,
|
104
|
+
:host(.tooltip-n):before {
|
105
|
+
right: 50%;
|
106
|
+
}
|
107
|
+
|
108
|
+
:host(.tooltip-s):before,
|
109
|
+
:host(.tooltip-se):before,
|
110
|
+
:host(.tooltip-sw):before {
|
111
|
+
bottom: 100%;
|
112
|
+
margin-right: -${TOOLTIP_ARROW_EDGE_OFFSET}px;
|
113
|
+
border-bottom-color: var(--color-neutral-emphasis-plus)
|
114
|
+
}
|
115
|
+
|
116
|
+
:host(.tooltip-s):after,
|
117
|
+
:host(.tooltip-se):after,
|
118
|
+
:host(.tooltip-sw):after {
|
119
|
+
bottom: 100%
|
120
|
+
}
|
121
|
+
|
122
|
+
:host(.tooltip-n):before,
|
123
|
+
:host(.tooltip-ne):before,
|
124
|
+
:host(.tooltip-nw):before {
|
125
|
+
top: 100%;
|
126
|
+
margin-right: -${TOOLTIP_ARROW_EDGE_OFFSET}px;
|
127
|
+
border-top-color: var(--color-neutral-emphasis-plus)
|
128
|
+
}
|
129
|
+
|
130
|
+
:host(.tooltip-n):after,
|
131
|
+
:host(.tooltip-ne):after,
|
132
|
+
:host(.tooltip-nw):after {
|
133
|
+
top: 100%
|
134
|
+
}
|
135
|
+
|
136
|
+
:host(.tooltip-se):before,
|
137
|
+
:host(.tooltip-ne):before {
|
138
|
+
left: 0;
|
139
|
+
margin-left: ${TOOLTIP_ARROW_EDGE_OFFSET}px;
|
140
|
+
}
|
141
|
+
|
142
|
+
:host(.tooltip-sw):before,
|
143
|
+
:host(.tooltip-nw):before {
|
144
|
+
right: 0;
|
145
|
+
margin-right: ${TOOLTIP_ARROW_EDGE_OFFSET}px;
|
146
|
+
}
|
147
|
+
|
148
|
+
:host(.tooltip-w):before {
|
149
|
+
top: 50%;
|
150
|
+
bottom: 50%;
|
151
|
+
left: 100%;
|
152
|
+
margin-top: -6px;
|
153
|
+
border-left-color: var(--color-neutral-emphasis-plus)
|
154
|
+
}
|
155
|
+
|
156
|
+
:host(.tooltip-e):before {
|
157
|
+
top: 50%;
|
158
|
+
right: 100%;
|
159
|
+
bottom: 50%;
|
160
|
+
margin-top: -6px;
|
161
|
+
border-right-color: var(--color-neutral-emphasis-plus)
|
162
|
+
}
|
163
|
+
`;
|
164
|
+
}
|
165
|
+
get htmlFor() {
|
166
|
+
return this.getAttribute('for') || '';
|
167
|
+
}
|
168
|
+
set htmlFor(value) {
|
169
|
+
this.setAttribute('for', value);
|
170
|
+
}
|
171
|
+
get type() {
|
172
|
+
const type = this.getAttribute('data-type');
|
173
|
+
return type === 'label' ? 'label' : 'description';
|
174
|
+
}
|
175
|
+
set type(value) {
|
176
|
+
this.setAttribute('data-type', value);
|
177
|
+
}
|
178
|
+
get direction() {
|
179
|
+
return (this.getAttribute('data-direction') || 's');
|
180
|
+
}
|
181
|
+
set direction(value) {
|
182
|
+
this.setAttribute('data-direction', value);
|
183
|
+
}
|
184
|
+
get control() {
|
185
|
+
return this.ownerDocument.getElementById(this.htmlFor);
|
186
|
+
}
|
187
|
+
connectedCallback() {
|
188
|
+
var _a;
|
189
|
+
this.hidden = true;
|
190
|
+
__classPrivateFieldSet(this, _TooltipElement_allowUpdatePosition, true, "f");
|
191
|
+
if (!this.id) {
|
192
|
+
this.id = `tooltip-${Date.now()}-${(Math.random() * 10000).toFixed(0)}`;
|
193
|
+
}
|
194
|
+
if (!this.control)
|
195
|
+
return;
|
196
|
+
this.setAttribute('role', 'tooltip');
|
197
|
+
(_a = __classPrivateFieldGet(this, _TooltipElement_abortController, "f")) === null || _a === void 0 ? void 0 : _a.abort();
|
198
|
+
__classPrivateFieldSet(this, _TooltipElement_abortController, new AbortController(), "f");
|
199
|
+
const { signal } = __classPrivateFieldGet(this, _TooltipElement_abortController, "f");
|
200
|
+
this.addEventListener('mouseleave', this, { signal });
|
201
|
+
this.control.addEventListener('mouseenter', this, { signal });
|
202
|
+
this.control.addEventListener('mouseleave', this, { signal });
|
203
|
+
this.control.addEventListener('focus', this, { signal });
|
204
|
+
this.control.addEventListener('blur', this, { signal });
|
205
|
+
this.ownerDocument.addEventListener('keydown', this, { signal });
|
206
|
+
}
|
207
|
+
disconnectedCallback() {
|
208
|
+
var _a;
|
209
|
+
(_a = __classPrivateFieldGet(this, _TooltipElement_abortController, "f")) === null || _a === void 0 ? void 0 : _a.abort();
|
210
|
+
}
|
211
|
+
handleEvent(event) {
|
212
|
+
if (!this.control)
|
213
|
+
return;
|
214
|
+
// Ensures that tooltip stays open when hovering between tooltip and element
|
215
|
+
// WCAG Success Criterion 1.4.13 Hoverable
|
216
|
+
if ((event.type === 'mouseenter' || event.type === 'focus') && this.hidden) {
|
217
|
+
this.hidden = false;
|
218
|
+
}
|
219
|
+
else if (event.type === 'blur') {
|
220
|
+
this.hidden = true;
|
221
|
+
}
|
222
|
+
else if (event.type === 'mouseleave' &&
|
223
|
+
event.relatedTarget !== this.control &&
|
224
|
+
event.relatedTarget !== this) {
|
225
|
+
this.hidden = true;
|
226
|
+
}
|
227
|
+
else if (event.type === 'keydown' && event.key === 'Escape' && !this.hidden) {
|
228
|
+
this.hidden = true;
|
229
|
+
}
|
230
|
+
}
|
231
|
+
attributeChangedCallback(name) {
|
232
|
+
if (name === 'id' || name === 'data-type') {
|
233
|
+
if (!this.id || !this.control)
|
234
|
+
return;
|
235
|
+
if (this.type === 'label') {
|
236
|
+
this.control.setAttribute('aria-labelledby', this.id);
|
237
|
+
}
|
238
|
+
else {
|
239
|
+
let describedBy = this.control.getAttribute('aria-describedby');
|
240
|
+
describedBy ? (describedBy = `${describedBy} ${this.id}`) : (describedBy = this.id);
|
241
|
+
this.control.setAttribute('aria-describedby', describedBy);
|
242
|
+
}
|
243
|
+
}
|
244
|
+
else if (name === 'hidden') {
|
245
|
+
if (this.hidden) {
|
246
|
+
this.classList.remove(TOOLTIP_OPEN_CLASS, ...DIRECTION_CLASSES);
|
247
|
+
}
|
248
|
+
else {
|
249
|
+
this.classList.add(TOOLTIP_OPEN_CLASS);
|
250
|
+
for (const tooltip of this.ownerDocument.querySelectorAll(this.tagName)) {
|
251
|
+
if (tooltip !== this)
|
252
|
+
tooltip.hidden = true;
|
253
|
+
}
|
254
|
+
__classPrivateFieldGet(this, _TooltipElement_instances, "m", _TooltipElement_updatePosition).call(this);
|
255
|
+
}
|
256
|
+
}
|
257
|
+
else if (name === 'data-direction') {
|
258
|
+
this.classList.remove(...DIRECTION_CLASSES);
|
259
|
+
const direction = this.direction;
|
260
|
+
if (direction === 'n') {
|
261
|
+
__classPrivateFieldSet(this, _TooltipElement_align, 'center', "f");
|
262
|
+
__classPrivateFieldSet(this, _TooltipElement_side, 'outside-top', "f");
|
263
|
+
}
|
264
|
+
else if (direction === 'ne') {
|
265
|
+
__classPrivateFieldSet(this, _TooltipElement_align, 'start', "f");
|
266
|
+
__classPrivateFieldSet(this, _TooltipElement_side, 'outside-top', "f");
|
267
|
+
}
|
268
|
+
else if (direction === 'e') {
|
269
|
+
__classPrivateFieldSet(this, _TooltipElement_align, 'center', "f");
|
270
|
+
__classPrivateFieldSet(this, _TooltipElement_side, 'outside-right', "f");
|
271
|
+
}
|
272
|
+
else if (direction === 'se') {
|
273
|
+
__classPrivateFieldSet(this, _TooltipElement_align, 'start', "f");
|
274
|
+
__classPrivateFieldSet(this, _TooltipElement_side, 'outside-bottom', "f");
|
275
|
+
}
|
276
|
+
else if (direction === 's') {
|
277
|
+
__classPrivateFieldSet(this, _TooltipElement_align, 'center', "f");
|
278
|
+
__classPrivateFieldSet(this, _TooltipElement_side, 'outside-bottom', "f");
|
279
|
+
}
|
280
|
+
else if (direction === 'sw') {
|
281
|
+
__classPrivateFieldSet(this, _TooltipElement_align, 'end', "f");
|
282
|
+
__classPrivateFieldSet(this, _TooltipElement_side, 'outside-bottom', "f");
|
283
|
+
}
|
284
|
+
else if (direction === 'w') {
|
285
|
+
__classPrivateFieldSet(this, _TooltipElement_align, 'center', "f");
|
286
|
+
__classPrivateFieldSet(this, _TooltipElement_side, 'outside-left', "f");
|
287
|
+
}
|
288
|
+
else if (direction === 'nw') {
|
289
|
+
__classPrivateFieldSet(this, _TooltipElement_align, 'end', "f");
|
290
|
+
__classPrivateFieldSet(this, _TooltipElement_side, 'outside-top', "f");
|
291
|
+
}
|
292
|
+
}
|
293
|
+
}
|
294
|
+
}
|
295
|
+
_TooltipElement_abortController = new WeakMap(), _TooltipElement_align = new WeakMap(), _TooltipElement_side = new WeakMap(), _TooltipElement_allowUpdatePosition = new WeakMap(), _TooltipElement_instances = new WeakSet(), _TooltipElement_adjustedAnchorAlignment = function _TooltipElement_adjustedAnchorAlignment(anchorSide) {
|
296
|
+
if (!this.control)
|
297
|
+
return;
|
298
|
+
const tooltipPosition = this.getBoundingClientRect();
|
299
|
+
const targetPosition = this.control.getBoundingClientRect();
|
300
|
+
const tooltipWidth = tooltipPosition.width;
|
301
|
+
const tooltipCenter = tooltipPosition.left + tooltipWidth / 2;
|
302
|
+
const targetCenter = targetPosition.x + targetPosition.width / 2;
|
303
|
+
if (Math.abs(tooltipCenter - targetCenter) < 2 || anchorSide === 'outside-left' || anchorSide === 'outside-right') {
|
304
|
+
return 'center';
|
305
|
+
}
|
306
|
+
else if (tooltipPosition.left === targetPosition.left) {
|
307
|
+
return 'start';
|
308
|
+
}
|
309
|
+
else if (tooltipPosition.right === targetPosition.right) {
|
310
|
+
return 'end';
|
311
|
+
}
|
312
|
+
else if (tooltipCenter < targetCenter) {
|
313
|
+
if (tooltipPosition.left === 0)
|
314
|
+
return 'start';
|
315
|
+
return 'end';
|
316
|
+
}
|
317
|
+
else {
|
318
|
+
if (tooltipPosition.right === 0)
|
319
|
+
return 'end';
|
320
|
+
return 'start';
|
321
|
+
}
|
322
|
+
}, _TooltipElement_updatePosition = function _TooltipElement_updatePosition() {
|
323
|
+
if (!this.control)
|
324
|
+
return;
|
325
|
+
if (!__classPrivateFieldGet(this, _TooltipElement_allowUpdatePosition, "f") || this.hidden)
|
326
|
+
return;
|
327
|
+
const TOOLTIP_OFFSET = 10;
|
328
|
+
this.style.left = `0px`; // Ensures we have reliable tooltip width in `getAnchoredPosition`
|
329
|
+
let position = getAnchoredPosition(this, this.control, {
|
330
|
+
side: __classPrivateFieldGet(this, _TooltipElement_side, "f"),
|
331
|
+
align: __classPrivateFieldGet(this, _TooltipElement_align, "f"),
|
332
|
+
anchorOffset: TOOLTIP_OFFSET
|
333
|
+
});
|
334
|
+
let anchorSide = position.anchorSide;
|
335
|
+
// We need to set tooltip position in order to determine ideal align.
|
336
|
+
this.style.top = `${position.top}px`;
|
337
|
+
this.style.left = `${position.left}px`;
|
338
|
+
let direction = 's';
|
339
|
+
const align = __classPrivateFieldGet(this, _TooltipElement_instances, "m", _TooltipElement_adjustedAnchorAlignment).call(this, anchorSide);
|
340
|
+
if (!align)
|
341
|
+
return;
|
342
|
+
this.style.left = `0px`; // Reset tooltip position again to ensure accurate width in `getAnchoredPosition`
|
343
|
+
position = getAnchoredPosition(this, this.control, { side: anchorSide, align, anchorOffset: TOOLTIP_OFFSET });
|
344
|
+
anchorSide = position.anchorSide;
|
345
|
+
this.style.top = `${position.top}px`;
|
346
|
+
this.style.left = `${position.left}px`;
|
347
|
+
if (anchorSide === 'outside-left') {
|
348
|
+
direction = 'w';
|
349
|
+
}
|
350
|
+
else if (anchorSide === 'outside-right') {
|
351
|
+
direction = 'e';
|
352
|
+
}
|
353
|
+
else if (anchorSide === 'outside-top') {
|
354
|
+
if (align === 'center') {
|
355
|
+
direction = 'n';
|
356
|
+
}
|
357
|
+
else if (align === 'start') {
|
358
|
+
direction = 'ne';
|
359
|
+
}
|
360
|
+
else {
|
361
|
+
direction = 'nw';
|
362
|
+
}
|
363
|
+
}
|
364
|
+
else {
|
365
|
+
if (align === 'center') {
|
366
|
+
direction = 's';
|
367
|
+
}
|
368
|
+
else if (align === 'start') {
|
369
|
+
direction = 'se';
|
370
|
+
}
|
371
|
+
else {
|
372
|
+
direction = 'sw';
|
373
|
+
}
|
374
|
+
}
|
375
|
+
this.classList.add(`tooltip-${direction}`);
|
376
|
+
};
|
377
|
+
TooltipElement.observedAttributes = ['data-type', 'data-direction', 'id', 'hidden'];
|
378
|
+
if (!window.customElements.get('tool-tip')) {
|
379
|
+
window.TooltipElement = TooltipElement;
|
380
|
+
window.customElements.define('tool-tip', TooltipElement);
|
381
|
+
}
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Primer
|
4
|
+
module Alpha
|
5
|
+
# `Tooltip` only appears on mouse hover or keyboard focus and contain a label or description text.
|
6
|
+
# Use tooltips sparingly and as a last resort.
|
7
|
+
#
|
8
|
+
# When using a tooltip, follow the provided guidelines to avoid accessibility issues.
|
9
|
+
#
|
10
|
+
# - Tooltip text should be brief and to the point. The tooltip content must be a string.
|
11
|
+
# - Tooltips should contain only **non-essential text**. Tooltips can easily be missed and are not accessible on touch devices so never
|
12
|
+
# use tooltips to convey critical information.
|
13
|
+
#
|
14
|
+
# @accessibility
|
15
|
+
# - **Never set tooltips on static elements.** Tooltips should only be used on interactive elements like buttons or links to avoid excluding keyboard-only users
|
16
|
+
# and screen reader users.
|
17
|
+
# - Place `Tooltip` adjacent after its trigger element in the DOM. This allows screen reader users to navigate to and copy the tooltip
|
18
|
+
# content.
|
19
|
+
# ### Which `type` should I set?
|
20
|
+
# Setting `:description` establishes an `aria-describedby` relationship, while `:label` establishes an `aria-labelledby` relationship between the trigger element and the tooltip,
|
21
|
+
#
|
22
|
+
# The `type` drastically changes semantics and screen reader behavior so follow these guidelines carefully:
|
23
|
+
# - When there is already a visible label text on the trigger element, the tooltip is likely intended to supplement the existing text, so set `type: :description`.
|
24
|
+
# The majority of tooltips will fall under this category.
|
25
|
+
# - When there is no visible text on the trigger element and the tooltip content is appropriate as a label for the element, set `type: :label`.
|
26
|
+
# This type is usually only appropriate for an icon-only control.
|
27
|
+
class Tooltip < Primer::Component
|
28
|
+
DIRECTION_DEFAULT = :s
|
29
|
+
DIRECTION_OPTIONS = [DIRECTION_DEFAULT, :n, :e, :w, :ne, :nw, :se, :sw].freeze
|
30
|
+
|
31
|
+
TYPE_FALLBACK = :description
|
32
|
+
TYPE_OPTIONS = [:label, :description].freeze
|
33
|
+
# @example As a description for an icon-only button
|
34
|
+
# @description
|
35
|
+
# If the tooltip content provides supplementary description, set `type: :description` to establish an `aria-describedby` relationship.
|
36
|
+
# The trigger element should also have a _concise_ accessible label via `aria-label`.
|
37
|
+
# @code
|
38
|
+
# <%= render(Primer::IconButton.new(id: "bold-button-0", icon: :bold, "aria-label": "Bold")) %>
|
39
|
+
# <%= render(Primer::Alpha::Tooltip.new(for_id: "bold-button-0", type: :description, text: "Add bold text", direction: :ne)) %>
|
40
|
+
# @example As a label for an icon-only button
|
41
|
+
# @description
|
42
|
+
# If the tooltip labels the icon-only button, set `type: :label`. This tooltip content becomes the accessible name for the button.
|
43
|
+
# @code
|
44
|
+
# <%= render(Primer::ButtonComponent.new(id: "like-button")) { "👍" } %>
|
45
|
+
# <%= render(Primer::Alpha::Tooltip.new(for_id: "like-button", type: :label, text: "Like", direction: :n)) %>
|
46
|
+
#
|
47
|
+
# @example As a description for a button with visible label
|
48
|
+
# @description
|
49
|
+
# If the button already has visible label text, the tooltip content is likely supplementary so set `type: :description`.
|
50
|
+
# @code
|
51
|
+
# <%= render(Primer::ButtonComponent.new(id: "save-button", scheme: :primary)) { "Save" } %>
|
52
|
+
# <%= render(Primer::Alpha::Tooltip.new(for_id: "save-button", type: :description, text: "This will immediately impact all organization members", direction: :ne)) %>
|
53
|
+
# @example With direction
|
54
|
+
# @description
|
55
|
+
# Set direction of tooltip with `direction`. The tooltip is responsive and will automatically adjust direction to avoid cutting off.
|
56
|
+
# @code
|
57
|
+
# <%= render(Primer::ButtonComponent.new(id: "North", m: 2)) { "North" } %>
|
58
|
+
# <%= render(Primer::Alpha::Tooltip.new(for_id: "North", type: :description, text: "This is a North-facing tooltip, and is responsive.", direction: :n)) %>
|
59
|
+
# <%= render(Primer::ButtonComponent.new(id: "South", m: 2)) { "South" } %>
|
60
|
+
# <%= render(Primer::Alpha::Tooltip.new(for_id: "South", type: :description, text: "This is a South-facing tooltip and is responsive.", direction: :s)) %>
|
61
|
+
# <%= render(Primer::ButtonComponent.new(id: "East", m: 2)) { "East" } %>
|
62
|
+
# <%= render(Primer::Alpha::Tooltip.new(for_id: "East", type: :description, text: "This is a East-facing tooltip and is responsive.", direction: :e)) %>
|
63
|
+
# <%= render(Primer::ButtonComponent.new(id: "West", m: 2)) { "West" } %>
|
64
|
+
# <%= render(Primer::Alpha::Tooltip.new(for_id: "West", type: :description, text: "This is a West-facing tooltip and is responsive.", direction: :w)) %>
|
65
|
+
# <%= render(Primer::ButtonComponent.new(id: "Northeast", m: 2)) { "Northeast" } %>
|
66
|
+
# <%= render(Primer::Alpha::Tooltip.new(for_id: "Northeast", type: :description, text: "This is a Northeast-facing tooltip and is responsive.", direction: :ne)) %>
|
67
|
+
# <%= render(Primer::ButtonComponent.new(id: "Southeast", m: 2)) { "Southeast" } %>
|
68
|
+
# <%= render(Primer::Alpha::Tooltip.new(for_id: "Southeast", type: :description, text: "This is a Southeast-facing tooltip and is responsive.", direction: :se)) %>
|
69
|
+
# <%= render(Primer::ButtonComponent.new(id: "Northwest", m: 2)) { "Northwest" } %>
|
70
|
+
# <%= render(Primer::Alpha::Tooltip.new(for_id: "Northwest", type: :description, text: "This is a Northwest-facing tooltip and is responsive.", direction: :nw)) %>
|
71
|
+
# <%= render(Primer::ButtonComponent.new(id: "Southwest", m: 2)) { "Southwest" } %>
|
72
|
+
# <%= render(Primer::Alpha::Tooltip.new(for_id: "Southwest", type: :description, text: "This is a Southwest-facing tooltip and is responsive.", direction: :sw)) %>
|
73
|
+
# @example With relative parent
|
74
|
+
# @description
|
75
|
+
# When the tooltip and trigger element have a parent container with `relative: position`, it should not affect width of the tooltip.
|
76
|
+
# @code
|
77
|
+
# <span style="position: relative;">
|
78
|
+
# <%= render(Primer::ButtonComponent.new(id: "test-button", scheme: :primary)) { "Test" } %>
|
79
|
+
# <%= render(Primer::Alpha::Tooltip.new(for_id: "test-button", type: :description, text: "This tooltip should take up the full width", direction: :ne)) %>
|
80
|
+
# </span>
|
81
|
+
# @param for_id [String] The ID of the element that the tooltip should be attached to.
|
82
|
+
# @param type [Symbol] <%= one_of(Primer::Alpha::Tooltip::TYPE_OPTIONS) %>
|
83
|
+
# @param direction [Symbol] <%= one_of(Primer::Alpha::Tooltip::DIRECTION_OPTIONS) %>
|
84
|
+
# @param text [String] The text content of the tooltip. This should be brief and no longer than a sentence.
|
85
|
+
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
86
|
+
def initialize(type:, for_id:, text:, direction: DIRECTION_DEFAULT, **system_arguments)
|
87
|
+
raise TypeError, "tooltip text must be a string" unless text.is_a?(String)
|
88
|
+
|
89
|
+
@text = text
|
90
|
+
@system_arguments = system_arguments
|
91
|
+
@system_arguments[:hidden] = true
|
92
|
+
@system_arguments[:tag] = :"tool-tip"
|
93
|
+
@system_arguments[:for] = for_id
|
94
|
+
@system_arguments[:"data-direction"] = fetch_or_fallback(DIRECTION_OPTIONS, direction, DIRECTION_DEFAULT).to_s
|
95
|
+
@system_arguments[:"data-type"] = fetch_or_fallback(TYPE_OPTIONS, type, TYPE_FALLBACK).to_s
|
96
|
+
end
|
97
|
+
|
98
|
+
def call
|
99
|
+
render(Primer::BaseComponent.new(**@system_arguments)) { @text }
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|