primer_view_components 0.0.68 → 0.0.69

Sign up to get free protection for your applications and to get access to all the features.
@@ -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, privateMap, value) {
2
+ if (!privateMap.has(receiver)) {
3
+ throw new TypeError("attempted to set private field on non-instance");
4
+ }
5
+ privateMap.set(receiver, value);
6
+ return value;
7
+ };
8
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, privateMap) {
9
+ if (!privateMap.has(receiver)) {
10
+ throw new TypeError("attempted to get private field on non-instance");
11
+ }
12
+ return privateMap.get(receiver);
13
+ };
14
+ var _abortController, _align, _side, _allowUpdatePosition;
15
+ import { getAnchoredPosition } from '@primer/behaviors';
16
+ const TOOLTIP_OPEN_CLASS = 'tooltip-open';
17
+ const DIRECTION_CLASSES = [
18
+ 'tooltip-n',
19
+ 'tooltip-s',
20
+ 'tooltip-e',
21
+ 'tooltip-w',
22
+ 'tooltip-ne',
23
+ 'tooltip-se',
24
+ 'tooltip-nw',
25
+ 'tooltip-sw'
26
+ ];
27
+ class TooltipElement extends HTMLElement {
28
+ constructor() {
29
+ super();
30
+ _abortController.set(this, void 0);
31
+ _align.set(this, 'center');
32
+ _side.set(this, 'outside-bottom');
33
+ _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
+ }
65
+
66
+ :host:before{
67
+ position: absolute;
68
+ z-index: 1000001;
69
+ color: var(--color-neutral-emphasis-plus);
70
+ content: "";
71
+ border: 6px solid transparent;
72
+ opacity: 0
73
+ }
74
+
75
+ @keyframes tooltip-appear {
76
+ from {
77
+ opacity: 0
78
+ }
79
+ to {
80
+ opacity: 1
81
+ }
82
+ }
83
+
84
+ :host:after{
85
+ position: absolute;
86
+ display: block;
87
+ right: 0;
88
+ left: 0;
89
+ height: 12px;
90
+ content: ""
91
+ }
92
+
93
+ :host(.${TOOLTIP_OPEN_CLASS}),
94
+ :host(.${TOOLTIP_OPEN_CLASS}):before {
95
+ animation-name: tooltip-appear;
96
+ animation-duration: .1s;
97
+ animation-fill-mode: forwards;
98
+ animation-timing-function: ease-in;
99
+ animation-delay: .4s
100
+ }
101
+
102
+ :host(.tooltip-s):before,
103
+ :host(.tooltip-se):before,
104
+ :host(.tooltip-sw):before {
105
+ right: 50%;
106
+ bottom: 100%;
107
+ margin-right: -6px;
108
+ border-bottom-color: var(--color-neutral-emphasis-plus)
109
+ }
110
+
111
+ :host(.tooltip-s):after,
112
+ :host(.tooltip-se):after,
113
+ :host(.tooltip-sw):after {
114
+ bottom: 100%
115
+ }
116
+
117
+ :host(.tooltip-n):before,
118
+ :host(.tooltip-ne):before,
119
+ :host(.tooltip-nw):before {
120
+ top: 100%;
121
+ right: 50%;
122
+ margin-right: -6px;
123
+ border-top-color: var(--color-neutral-emphasis-plus)
124
+ }
125
+
126
+ :host(.tooltip-n):after,
127
+ :host(.tooltip-ne):after,
128
+ :host(.tooltip-nw):after {
129
+ top: 100%
130
+ }
131
+
132
+ :host(.tooltip-se):before,
133
+ :host(.tooltip-ne):before {
134
+ right: auto
135
+ }
136
+
137
+ :host(.tooltip-sw):before,
138
+ :host(.tooltip-nw):before {
139
+ right: 0;
140
+ margin-right: 6px
141
+ }
142
+
143
+ :host(.tooltip-w):before {
144
+ top: 50%;
145
+ bottom: 50%;
146
+ left: 100%;
147
+ margin-top: -6px;
148
+ border-left-color: var(--color-neutral-emphasis-plus)
149
+ }
150
+
151
+ :host(.tooltip-e):before {
152
+ top: 50%;
153
+ right: 100%;
154
+ bottom: 50%;
155
+ margin-top: -6px;
156
+ border-right-color: var(--color-neutral-emphasis-plus)
157
+ }
158
+ `;
159
+ }
160
+ get htmlFor() {
161
+ return this.getAttribute('for') || '';
162
+ }
163
+ set htmlFor(value) {
164
+ this.setAttribute('for', value);
165
+ }
166
+ get type() {
167
+ const type = this.getAttribute('data-type');
168
+ return type === 'label' ? 'label' : 'description';
169
+ }
170
+ set type(value) {
171
+ this.setAttribute('data-type', value);
172
+ }
173
+ get direction() {
174
+ return (this.getAttribute('data-direction') || 's');
175
+ }
176
+ set direction(value) {
177
+ this.setAttribute('data-direction', value);
178
+ }
179
+ get control() {
180
+ return this.ownerDocument.getElementById(this.htmlFor);
181
+ }
182
+ connectedCallback() {
183
+ var _a;
184
+ this.hidden = true;
185
+ __classPrivateFieldSet(this, _allowUpdatePosition, true);
186
+ if (!this.id) {
187
+ this.id = `tooltip-${Date.now()}-${(Math.random() * 10000).toFixed(0)}`;
188
+ }
189
+ if (!this.control)
190
+ return;
191
+ this.setAttribute('role', 'tooltip');
192
+ (_a = __classPrivateFieldGet(this, _abortController)) === null || _a === void 0 ? void 0 : _a.abort();
193
+ __classPrivateFieldSet(this, _abortController, new AbortController());
194
+ const { signal } = __classPrivateFieldGet(this, _abortController);
195
+ this.addEventListener('mouseleave', this, { signal });
196
+ this.control.addEventListener('mouseenter', this, { signal });
197
+ this.control.addEventListener('mouseleave', this, { signal });
198
+ this.control.addEventListener('focus', this, { signal });
199
+ this.control.addEventListener('blur', this, { signal });
200
+ this.ownerDocument.addEventListener('keydown', this, { signal });
201
+ }
202
+ disconnectedCallback() {
203
+ var _a;
204
+ (_a = __classPrivateFieldGet(this, _abortController)) === null || _a === void 0 ? void 0 : _a.abort();
205
+ }
206
+ handleEvent(event) {
207
+ if (!this.control)
208
+ return;
209
+ // Ensures that tooltip stays open when hovering between tooltip and element
210
+ // WCAG Success Criterion 1.4.13 Hoverable
211
+ if ((event.type === 'mouseenter' || event.type === 'focus') && this.hidden) {
212
+ this.hidden = false;
213
+ }
214
+ else if (event.type === 'blur') {
215
+ this.hidden = true;
216
+ }
217
+ else if (event.type === 'mouseleave' &&
218
+ event.relatedTarget !== this.control &&
219
+ event.relatedTarget !== this) {
220
+ this.hidden = true;
221
+ }
222
+ else if (event.type === 'keydown' && event.key === 'Escape' && !this.hidden) {
223
+ this.hidden = true;
224
+ }
225
+ }
226
+ attributeChangedCallback(name) {
227
+ if (name === 'id' || name === 'data-type') {
228
+ if (!this.id || !this.control)
229
+ return;
230
+ if (this.type === 'label') {
231
+ this.control.setAttribute('aria-labelledby', this.id);
232
+ }
233
+ else {
234
+ let describedBy = this.control.getAttribute('aria-describedby');
235
+ describedBy ? (describedBy = `${describedBy} ${this.id}`) : (describedBy = this.id);
236
+ this.control.setAttribute('aria-describedby', describedBy);
237
+ }
238
+ }
239
+ else if (name === 'hidden') {
240
+ if (this.hidden) {
241
+ this.classList.remove(TOOLTIP_OPEN_CLASS, ...DIRECTION_CLASSES);
242
+ }
243
+ else {
244
+ this.classList.add(TOOLTIP_OPEN_CLASS);
245
+ for (const tooltip of this.ownerDocument.querySelectorAll(this.tagName)) {
246
+ if (tooltip !== this)
247
+ tooltip.hidden = true;
248
+ }
249
+ this..call(this);
250
+ }
251
+ }
252
+ else if (name === 'data-direction') {
253
+ this.classList.remove(...DIRECTION_CLASSES);
254
+ const direction = this.direction;
255
+ if (direction === 'n') {
256
+ __classPrivateFieldSet(this, _align, 'center');
257
+ __classPrivateFieldSet(this, _side, 'outside-top');
258
+ }
259
+ else if (direction === 'ne') {
260
+ __classPrivateFieldSet(this, _align, 'start');
261
+ __classPrivateFieldSet(this, _side, 'outside-top');
262
+ }
263
+ else if (direction === 'e') {
264
+ __classPrivateFieldSet(this, _align, 'center');
265
+ __classPrivateFieldSet(this, _side, 'outside-right');
266
+ }
267
+ else if (direction === 'se') {
268
+ __classPrivateFieldSet(this, _align, 'start');
269
+ __classPrivateFieldSet(this, _side, 'outside-bottom');
270
+ }
271
+ else if (direction === 's') {
272
+ __classPrivateFieldSet(this, _align, 'center');
273
+ __classPrivateFieldSet(this, _side, 'outside-bottom');
274
+ }
275
+ else if (direction === 'sw') {
276
+ __classPrivateFieldSet(this, _align, 'end');
277
+ __classPrivateFieldSet(this, _side, 'outside-bottom');
278
+ }
279
+ else if (direction === 'w') {
280
+ __classPrivateFieldSet(this, _align, 'center');
281
+ __classPrivateFieldSet(this, _side, 'outside-left');
282
+ }
283
+ else if (direction === 'nw') {
284
+ __classPrivateFieldSet(this, _align, 'end');
285
+ __classPrivateFieldSet(this, _side, 'outside-top');
286
+ }
287
+ }
288
+ }
289
+ // `getAnchoredPosition` may calibrate `anchoredSide` but does not recalibrate `align`.
290
+ // Therefore, we need to determine which `align` is best based on the initial `getAnchoredPosition` calcluation.
291
+ // Related: https://github.com/primer/behaviors/issues/63
292
+ (anchorSide) {
293
+ if (!this.control)
294
+ return;
295
+ const tooltipPosition = this.getBoundingClientRect();
296
+ const targetPosition = this.control.getBoundingClientRect();
297
+ const tooltipWidth = tooltipPosition.width;
298
+ const tooltipCenter = tooltipPosition.left + tooltipWidth / 2;
299
+ const targetCenter = targetPosition.x + targetPosition.width / 2;
300
+ if (Math.abs(tooltipCenter - targetCenter) < 2 || anchorSide === 'outside-left' || anchorSide === 'outside-right') {
301
+ return 'center';
302
+ }
303
+ else if (tooltipPosition.left === targetPosition.left) {
304
+ return 'start';
305
+ }
306
+ else if (tooltipPosition.right === targetPosition.right) {
307
+ return 'end';
308
+ }
309
+ else if (tooltipCenter < targetCenter) {
310
+ if (tooltipPosition.left === 0)
311
+ return 'start';
312
+ return 'end';
313
+ }
314
+ else {
315
+ if (tooltipPosition.right === 0)
316
+ return 'end';
317
+ return 'start';
318
+ }
319
+ }
320
+ () {
321
+ if (!this.control)
322
+ return;
323
+ if (!__classPrivateFieldGet(this, _allowUpdatePosition) || this.hidden)
324
+ return;
325
+ const TOOLTIP_OFFSET = 10;
326
+ this.style.left = `0px`; // Ensures we have reliable tooltip width in `getAnchoredPosition`
327
+ let position = getAnchoredPosition(this, this.control, {
328
+ side: __classPrivateFieldGet(this, _side),
329
+ align: __classPrivateFieldGet(this, _align),
330
+ anchorOffset: TOOLTIP_OFFSET
331
+ });
332
+ let anchorSide = position.anchorSide;
333
+ // We need to set tooltip position in order to determine ideal align.
334
+ this.style.top = `${position.top}px`;
335
+ this.style.left = `${position.left}px`;
336
+ let direction = 's';
337
+ const align = this..call(this, anchorSide);
338
+ if (!align)
339
+ return;
340
+ this.style.left = `0px`; // Reset tooltip position again to ensure accurate width in `getAnchoredPosition`
341
+ position = getAnchoredPosition(this, this.control, { side: anchorSide, align, anchorOffset: TOOLTIP_OFFSET });
342
+ anchorSide = position.anchorSide;
343
+ this.style.top = `${position.top}px`;
344
+ this.style.left = `${position.left}px`;
345
+ if (anchorSide === 'outside-left') {
346
+ direction = 'w';
347
+ }
348
+ else if (anchorSide === 'outside-right') {
349
+ direction = 'e';
350
+ }
351
+ else if (anchorSide === 'outside-top') {
352
+ if (align === 'center') {
353
+ direction = 'n';
354
+ }
355
+ else if (align === 'start') {
356
+ direction = 'ne';
357
+ }
358
+ else {
359
+ direction = 'nw';
360
+ }
361
+ }
362
+ else {
363
+ if (align === 'center') {
364
+ direction = 's';
365
+ }
366
+ else if (align === 'start') {
367
+ direction = 'se';
368
+ }
369
+ else {
370
+ direction = 'sw';
371
+ }
372
+ }
373
+ this.classList.add(`tooltip-${direction}`);
374
+ }
375
+ }
376
+ _abortController = new WeakMap(), _align = new WeakMap(), _side = new WeakMap(), _allowUpdatePosition = new WeakMap();
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,95 @@
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
+ # @param for_id [String] The ID of the element that the tooltip should be attached to.
74
+ # @param type [Symbol] <%= one_of(Primer::Alpha::Tooltip::TYPE_OPTIONS) %>
75
+ # @param direction [Symbol] <%= one_of(Primer::Alpha::Tooltip::DIRECTION_OPTIONS) %>
76
+ # @param text [String] The text content of the tooltip. This should be brief and no longer than a sentence.
77
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
78
+ def initialize(type:, for_id:, text:, direction: DIRECTION_DEFAULT, **system_arguments)
79
+ raise TypeError, "tooltip text must be a string" unless text.is_a?(String)
80
+
81
+ @text = text
82
+ @system_arguments = system_arguments
83
+ @system_arguments[:hidden] = true
84
+ @system_arguments[:tag] = :"tool-tip"
85
+ @system_arguments[:for] = for_id
86
+ @system_arguments[:"data-direction"] = fetch_or_fallback(DIRECTION_OPTIONS, direction, DIRECTION_DEFAULT).to_s
87
+ @system_arguments[:"data-type"] = fetch_or_fallback(TYPE_OPTIONS, type, TYPE_FALLBACK).to_s
88
+ end
89
+
90
+ def call
91
+ render(Primer::BaseComponent.new(**@system_arguments)) { @text }
92
+ end
93
+ end
94
+ end
95
+ end