primer_view_components 0.0.68 → 0.0.69

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.
@@ -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