wally-ui 1.14.0 → 1.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/playground/showcase/src/app/components/selection-popover/selection-popover.html +8 -2
- package/playground/showcase/src/app/components/selection-popover/selection-popover.ts +76 -12
- package/playground/showcase/src/app/pages/documentation/components/selection-popover-docs/selection-popover-docs.examples.ts +4 -0
- package/playground/showcase/src/app/pages/documentation/components/selection-popover-docs/selection-popover-docs.html +49 -0
- package/playground/showcase/src/app/pages/documentation/components/selection-popover-docs/selection-popover-docs.ts +1 -0
package/package.json
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
<!-- Floating popover -->
|
|
6
6
|
@if (isVisible()) {
|
|
7
7
|
<div #popover
|
|
8
|
-
class="fixed z-50 bg-white dark:bg-neutral-900 shadow-lg rounded-xl border border-neutral-300 dark:border-neutral-700
|
|
8
|
+
class="fixed z-50 bg-white dark:bg-neutral-900 shadow-lg rounded-xl border border-neutral-300 dark:border-neutral-700 transition-opacity duration-150"
|
|
9
|
+
[class.p-1]="!isMobile()"
|
|
10
|
+
[class.p-2]="isMobile()"
|
|
9
11
|
[class.opacity-0]="!isPositioned()"
|
|
10
12
|
[class.opacity-100]="isPositioned()"
|
|
11
13
|
[style.top.px]="adjustedPosition().top" [style.left.px]="adjustedPosition().left" role="dialog"
|
|
@@ -19,7 +21,11 @@
|
|
|
19
21
|
|
|
20
22
|
<!-- Fallback: default button (hidden if custom actions exist) -->
|
|
21
23
|
<button [class.hidden]="hasCustomActionsSignal()"
|
|
22
|
-
class="
|
|
24
|
+
class="text-[#0a0a0a] text-sm font-mono hover:bg-neutral-100 dark:text-white dark:hover:bg-neutral-800 rounded transition-colors cursor-pointer"
|
|
25
|
+
[class.px-3]="!isMobile()"
|
|
26
|
+
[class.py-2]="!isMobile()"
|
|
27
|
+
[class.px-4]="isMobile()"
|
|
28
|
+
[class.py-3]="isMobile()">
|
|
23
29
|
Default Action
|
|
24
30
|
</button>
|
|
25
31
|
</div>
|
|
@@ -48,44 +48,77 @@ export class SelectionPopover implements AfterViewInit {
|
|
|
48
48
|
/** Currently selected text */
|
|
49
49
|
selectedText: WritableSignal<string> = signal<string>('');
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Detects if user is on mobile device
|
|
53
|
+
*/
|
|
54
|
+
isMobile = computed(() => {
|
|
55
|
+
if (typeof window === 'undefined') return false;
|
|
56
|
+
|
|
57
|
+
return 'ontouchstart' in window ||
|
|
58
|
+
navigator.maxTouchPoints > 0 ||
|
|
59
|
+
window.innerWidth < 768;
|
|
60
|
+
});
|
|
61
|
+
|
|
51
62
|
/**
|
|
52
63
|
* Computes adjusted position with viewport constraints
|
|
53
64
|
* Prevents popover from overflowing screen edges
|
|
65
|
+
* Mobile-aware: accounts for virtual keyboard and smaller screens
|
|
54
66
|
*/
|
|
55
67
|
adjustedPosition = computed(() => {
|
|
56
68
|
const position = this.popoverPosition();
|
|
57
69
|
const viewportWidth = window.innerWidth;
|
|
70
|
+
const viewportHeight = window.innerHeight;
|
|
58
71
|
|
|
59
|
-
// Get real popover
|
|
72
|
+
// Get real popover dimensions if available, otherwise estimate
|
|
60
73
|
const popoverWidth = this.popoverElement?.nativeElement?.offsetWidth || 200;
|
|
74
|
+
const popoverHeight = this.popoverElement?.nativeElement?.offsetHeight || 50;
|
|
61
75
|
|
|
62
76
|
let left = position.left;
|
|
77
|
+
let top = position.top;
|
|
63
78
|
|
|
64
|
-
//
|
|
79
|
+
// Horizontal adjustment
|
|
65
80
|
if (left + popoverWidth > viewportWidth) {
|
|
66
81
|
left = viewportWidth - popoverWidth - 10;
|
|
67
82
|
}
|
|
68
|
-
|
|
69
|
-
// Prevent overflow on the left edge
|
|
70
83
|
if (left < 10) {
|
|
71
84
|
left = 10;
|
|
72
85
|
}
|
|
73
86
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
87
|
+
// Vertical adjustment for mobile (avoid keyboard and screen edges)
|
|
88
|
+
if (this.isMobile()) {
|
|
89
|
+
// If too close to top, position below selection instead
|
|
90
|
+
if (top < 80) {
|
|
91
|
+
const selection = window.getSelection();
|
|
92
|
+
if (selection && selection.rangeCount > 0) {
|
|
93
|
+
const range = selection.getRangeAt(0);
|
|
94
|
+
const rect = range.getBoundingClientRect();
|
|
95
|
+
top = rect.bottom + 10; // 10px below selection
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// If too close to bottom (virtual keyboard area), keep visible
|
|
100
|
+
if (top + popoverHeight > viewportHeight - 100) {
|
|
101
|
+
top = viewportHeight - popoverHeight - 100;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { top, left };
|
|
78
106
|
});
|
|
79
107
|
|
|
80
108
|
/**
|
|
81
|
-
* Handles
|
|
82
|
-
*
|
|
109
|
+
* Handles both mouse and touch selection events
|
|
110
|
+
* Mobile: touchend after long-press selection
|
|
111
|
+
* Desktop: mouseup after click-drag selection
|
|
83
112
|
*/
|
|
84
113
|
@HostListener('mouseup', ['$event'])
|
|
85
|
-
|
|
114
|
+
@HostListener('touchend', ['$event'])
|
|
115
|
+
onMouseUp(event: MouseEvent | TouchEvent): void {
|
|
116
|
+
const isMobile = 'ontouchstart' in window;
|
|
117
|
+
const delay = isMobile ? 100 : 10;
|
|
118
|
+
|
|
86
119
|
setTimeout(() => {
|
|
87
120
|
this.handleTextSelection();
|
|
88
|
-
},
|
|
121
|
+
}, delay);
|
|
89
122
|
}
|
|
90
123
|
|
|
91
124
|
/**
|
|
@@ -111,6 +144,37 @@ export class SelectionPopover implements AfterViewInit {
|
|
|
111
144
|
this.hide();
|
|
112
145
|
}
|
|
113
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Prevents native mobile selection menu from appearing
|
|
149
|
+
* This ensures only our custom popover is shown
|
|
150
|
+
*/
|
|
151
|
+
@HostListener('selectionchange')
|
|
152
|
+
onNativeSelectionChange() {
|
|
153
|
+
const selection = window.getSelection();
|
|
154
|
+
|
|
155
|
+
if (selection && selection.toString().trim().length >= 3) {
|
|
156
|
+
// Prevent native menu only if valid selection exists
|
|
157
|
+
// and our popover will appear
|
|
158
|
+
event?.preventDefault?.();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Prevents scroll when touching the popover on mobile
|
|
164
|
+
* Ensures users can interact with actions without accidentally scrolling
|
|
165
|
+
*/
|
|
166
|
+
@HostListener('touchmove', ['$event'])
|
|
167
|
+
onTouchMove(event: TouchEvent): void {
|
|
168
|
+
if (this.isVisible() && this.popoverElement) {
|
|
169
|
+
const target = event.target as Node;
|
|
170
|
+
const isPopoverTouch = this.popoverElement.nativeElement.contains(target);
|
|
171
|
+
|
|
172
|
+
if (isPopoverTouch) {
|
|
173
|
+
event.preventDefault();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
114
178
|
/**
|
|
115
179
|
* Handles text selection and shows popover
|
|
116
180
|
*
|
|
@@ -168,6 +168,10 @@ export const SelectionPopoverCodeExamples = {
|
|
|
168
168
|
</div>
|
|
169
169
|
</wally-selection-popover>`,
|
|
170
170
|
|
|
171
|
+
// === MOBILE SUPPORT ===
|
|
172
|
+
|
|
173
|
+
mobileSupport: `<!-- Automatic mobile support -->\n<wally-selection-popover (textSelected)="onSelect($event)">\n <!-- Works seamlessly on mobile devices:\n - Touch event support (touchstart, touchend)\n - Native selection menu prevention\n - Viewport-aware positioning (avoids virtual keyboard)\n - Larger touch targets on mobile (44x44px minimum)\n - Touch scroll prevention on popover\n - Adaptive delays for better touch detection\n -->\n\n <article>\n <p>Try selecting text on mobile - the popover automatically adapts!</p>\n </article>\n</wally-selection-popover>`,
|
|
174
|
+
|
|
171
175
|
// === ADVANCED ===
|
|
172
176
|
|
|
173
177
|
minSelectionLength: `<!-- Custom minimum selection length -->
|
|
@@ -332,6 +332,48 @@
|
|
|
332
332
|
</div>
|
|
333
333
|
</div>
|
|
334
334
|
</article>
|
|
335
|
+
|
|
336
|
+
<!-- Mobile Support -->
|
|
337
|
+
<article class="mb-8" aria-labelledby="mobile-support-heading">
|
|
338
|
+
<h3 id="mobile-support-heading" class="text-sm sm:text-base font-bold text-[#0a0a0a] dark:text-white mb-3 uppercase">
|
|
339
|
+
<span aria-hidden="true">> </span>Mobile Support
|
|
340
|
+
</h3>
|
|
341
|
+
<p class="text-xs sm:text-sm text-neutral-600 dark:text-neutral-400 mb-3 leading-relaxed">
|
|
342
|
+
The component works seamlessly on mobile devices with automatic optimizations for touch interactions. All features are enabled by default with zero configuration.
|
|
343
|
+
</p>
|
|
344
|
+
<div class="bg-neutral-100 dark:bg-[#121212] border border-neutral-300 dark:border-neutral-700 p-3 mb-4">
|
|
345
|
+
<pre><code [innerHTML]="mobileSupportCode" class="text-xs sm:text-sm text-[#0a0a0a] dark:text-white"></code></pre>
|
|
346
|
+
</div>
|
|
347
|
+
<div class="bg-white dark:bg-[#0f0f0f] border-2 border-neutral-300 dark:border-neutral-800 p-4">
|
|
348
|
+
<h4 class="text-xs font-bold text-[#0a0a0a] dark:text-white mb-3 uppercase">Automatic Mobile Features:</h4>
|
|
349
|
+
<ul class="space-y-2 text-xs sm:text-sm text-neutral-600 dark:text-neutral-400">
|
|
350
|
+
<li class="flex items-start gap-2">
|
|
351
|
+
<span class="text-green-600 dark:text-green-400 mt-0.5">✓</span>
|
|
352
|
+
<span>Touch event support (touchstart, touchend) with adaptive delays for better detection</span>
|
|
353
|
+
</li>
|
|
354
|
+
<li class="flex items-start gap-2">
|
|
355
|
+
<span class="text-green-600 dark:text-green-400 mt-0.5">✓</span>
|
|
356
|
+
<span>Native selection menu prevention - only your custom popover appears</span>
|
|
357
|
+
</li>
|
|
358
|
+
<li class="flex items-start gap-2">
|
|
359
|
+
<span class="text-green-600 dark:text-green-400 mt-0.5">✓</span>
|
|
360
|
+
<span>Viewport-aware positioning that avoids virtual keyboard overlap</span>
|
|
361
|
+
</li>
|
|
362
|
+
<li class="flex items-start gap-2">
|
|
363
|
+
<span class="text-green-600 dark:text-green-400 mt-0.5">✓</span>
|
|
364
|
+
<span>Larger touch targets (44x44px minimum) for better tap accuracy</span>
|
|
365
|
+
</li>
|
|
366
|
+
<li class="flex items-start gap-2">
|
|
367
|
+
<span class="text-green-600 dark:text-green-400 mt-0.5">✓</span>
|
|
368
|
+
<span>Touch scroll prevention on popover to avoid accidental dismissal</span>
|
|
369
|
+
</li>
|
|
370
|
+
<li class="flex items-start gap-2">
|
|
371
|
+
<span class="text-green-600 dark:text-green-400 mt-0.5">✓</span>
|
|
372
|
+
<span>Smart device detection using multiple methods (touch points, window width, touch API)</span>
|
|
373
|
+
</li>
|
|
374
|
+
</ul>
|
|
375
|
+
</div>
|
|
376
|
+
</article>
|
|
335
377
|
</section>
|
|
336
378
|
|
|
337
379
|
<!-- API Reference -->
|
|
@@ -462,6 +504,13 @@
|
|
|
462
504
|
Full accessibility with role="dialog" and aria-label="Selection actions"
|
|
463
505
|
</td>
|
|
464
506
|
</tr>
|
|
507
|
+
<tr>
|
|
508
|
+
<td class="p-4 font-mono text-blue-600 dark:text-blue-400">Mobile Support</td>
|
|
509
|
+
<td class="p-4 text-gray-700 dark:text-gray-300">
|
|
510
|
+
Automatic touch event support, native menu prevention, larger touch targets, scroll prevention, and
|
|
511
|
+
viewport-aware positioning that avoids virtual keyboard overlap
|
|
512
|
+
</td>
|
|
513
|
+
</tr>
|
|
465
514
|
</tbody>
|
|
466
515
|
</table>
|
|
467
516
|
</div>
|
|
@@ -51,6 +51,7 @@ export class SelectionPopoverDocs {
|
|
|
51
51
|
keyboardAccessibilityCode = getFormattedCode(SelectionPopoverCodeExamples.keyboardAccessibility, 'html');
|
|
52
52
|
eventHandlingCode = getFormattedCode(SelectionPopoverCodeExamples.eventHandling, 'html');
|
|
53
53
|
eventHandlingTsCode = getFormattedCode(SelectionPopoverCodeExamples.eventHandlingTs, 'typescript');
|
|
54
|
+
mobileSupportCode = getFormattedCode(SelectionPopoverCodeExamples.mobileSupport, 'html');
|
|
54
55
|
|
|
55
56
|
// Full Example
|
|
56
57
|
fullExampleCode = getFormattedCode(SelectionPopoverCodeExamples.fullExample, 'html');
|