primer_view_components 0.0.68 → 0.0.71

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +96 -1
  3. data/app/assets/javascripts/primer_view_components.js +1 -1
  4. data/app/assets/javascripts/primer_view_components.js.map +1 -1
  5. data/app/components/primer/alpha/navigation_list_element.d.ts +11 -0
  6. data/app/components/primer/alpha/navigation_list_element.js +42 -0
  7. data/app/components/primer/alpha/tooltip.d.ts +24 -0
  8. data/app/components/primer/alpha/tooltip.js +381 -0
  9. data/app/components/primer/alpha/tooltip.rb +103 -0
  10. data/app/components/primer/alpha/tooltip.ts +383 -0
  11. data/app/components/primer/base_component.rb +2 -2
  12. data/app/components/primer/beta/auto_complete/auto_complete.html.erb +16 -6
  13. data/app/components/primer/beta/auto_complete.rb +82 -20
  14. data/app/components/primer/beta/truncate.rb +1 -0
  15. data/app/components/primer/button_component.html.erb +1 -0
  16. data/app/components/primer/button_component.rb +29 -0
  17. data/app/components/primer/component.rb +9 -2
  18. data/app/components/primer/details_component.rb +1 -1
  19. data/app/components/primer/icon_button.rb +1 -1
  20. data/app/components/primer/link_component.erb +4 -0
  21. data/app/components/primer/link_component.rb +29 -4
  22. data/app/components/primer/markdown.rb +1 -1
  23. data/app/components/primer/primer.d.ts +1 -0
  24. data/app/components/primer/primer.js +1 -0
  25. data/app/components/primer/primer.ts +1 -0
  26. data/app/components/primer/subhead_component.html.erb +1 -1
  27. data/app/components/primer/subhead_component.rb +1 -1
  28. data/lib/primer/classify/utilities.yml +40 -0
  29. data/lib/primer/view_components/linters/button_component_migration_counter.rb +1 -1
  30. data/lib/primer/view_components/version.rb +1 -1
  31. data/lib/rubocop/cop/primer/primer_octicon.rb +1 -1
  32. data/lib/tasks/custom_utilities.yml +12 -0
  33. data/lib/tasks/docs.rake +9 -5
  34. data/lib/tasks/utilities.rake +1 -1
  35. data/static/arguments.yml +43 -1
  36. data/static/audited_at.json +1 -0
  37. data/static/classes.yml +5 -0
  38. data/static/constants.json +18 -0
  39. data/static/statuses.json +1 -0
  40. metadata +18 -11
@@ -0,0 +1,11 @@
1
+ declare class NavigationListElement extends HTMLElement {
2
+ #private;
3
+ connectedCallback(): void;
4
+ handleEvent(event: Event): void;
5
+ }
6
+ export {};
7
+ declare global {
8
+ interface Window {
9
+ NavigationListElement: typeof NavigationListElement;
10
+ }
11
+ }
@@ -0,0 +1,42 @@
1
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
2
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
3
+ 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");
4
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5
+ };
6
+ var _NavigationListElement_instances, _NavigationListElement_handleClick;
7
+ class NavigationListElement extends HTMLElement {
8
+ constructor() {
9
+ super(...arguments);
10
+ _NavigationListElement_instances.add(this);
11
+ }
12
+ connectedCallback() {
13
+ this.addEventListener('click', this);
14
+ }
15
+ handleEvent(event) {
16
+ var _a;
17
+ if (!(event.target instanceof HTMLElement))
18
+ return;
19
+ const item = (_a = event.target) === null || _a === void 0 ? void 0 : _a.closest('button');
20
+ if ((item === null || item === void 0 ? void 0 : item.closest(this.tagName)) !== this)
21
+ return;
22
+ if (event.type === 'click') {
23
+ __classPrivateFieldGet(this, _NavigationListElement_instances, "m", _NavigationListElement_handleClick).call(this, item, event);
24
+ }
25
+ }
26
+ }
27
+ _NavigationListElement_instances = new WeakSet(), _NavigationListElement_handleClick = function _NavigationListElement_handleClick(item, e) {
28
+ if (item.getAttribute('aria-expanded') !== null) {
29
+ if (item.getAttribute('aria-expanded') === 'true') {
30
+ item.setAttribute('aria-expanded', 'false');
31
+ }
32
+ else {
33
+ item.setAttribute('aria-expanded', 'true');
34
+ }
35
+ }
36
+ e.stopPropagation();
37
+ };
38
+ if (!window.customElements.get('navigation-list')) {
39
+ window.NavigationListElement = NavigationListElement;
40
+ window.customElements.define('navigation-list', NavigationListElement);
41
+ }
42
+ export {};
@@ -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