detached 272f39f

refactor(ui): Improve tooltip and chat message interaction

- Enhance tooltip accessibility and interaction
- Add keyboard support and prevent multiple tooltip instances
- Modify chat message nickname styling for better usability
- Refactor tooltip component to use button for better semantics
commit
272f39f4d96e1c66cbbc4948355645fe7cba11cc
author
Kit · · 2025-02-18 15:53 UTC
parent
69a5042
signature
SSH UnverifiedSHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6Tk The key is unknown
2 files · +58 -12
M src/lib/components/chat-room.svelte +11 -5
@@ -535,7 +535,7 @@ function handleLoginSuccess() {
535535 {/if}
536536 {#if showRegistrationPrompt}
537537 <div class="registration-prompt">
538- <p>👋 <button class="link-button" onclick={openSignup}>Inscris-toi</button> pour lire le reste du chat !</p>
538+ <p>👋 <button class="link-button" onclick={openSignup}>Inscris-toi</button>pour lire le reste du chat !</p>
539539 </div>
540540 {/if}
541541 {#each visibleMessages as message (message.id)}
@@ -546,9 +546,10 @@ function handleLoginSuccess() {
546546 text: formatFrenchDateTime(new Date(message.timestamp)),
547547 direction: "left",
548548 closeDelay: 1000,
549- touchBehavior: "remove"
549+ touchBehavior: "remove",
550+ interactive: false
550551 }}>
551- <span class="nickname">{message.user.nickname}</span>
552+ <span class="nickname pointer-events-none">{message.user.nickname}</span>
552553 </Tooltip>
553554 {message.content}
554555 </span>
@@ -557,9 +558,10 @@ function handleLoginSuccess() {
557558 text: formatFrenchDateTime(new Date(message.timestamp)),
558559 direction: "left",
559560 closeDelay: 1000,
560- touchBehavior: "remove"
561+ touchBehavior: "remove",
562+ interactive: false
561563 }}>
562- <span class="nickname">{message.user.nickname}:</span>
564+ <span class="nickname pointer-events-none">{message.user.nickname}:</span>
563565 </Tooltip>
564566 <span class="content">{message.content}</span>
565567 {/if}
@@ -1020,4 +1022,8 @@ function handleLoginSuccess() {
10201022 gap: 2px;
10211023 margin-left: auto;
10221024 }
1025+
1026+ .pointer-events-none {
1027+ pointer-events: none;
1028+ }
10231029 </style>⏎̸
@@ -535,7 +535,7 @@ function handleLoginSuccess() {
535 {/if}535 {/if}
536 {#if showRegistrationPrompt}536 {#if showRegistrationPrompt}
537 <div class="registration-prompt">537 <div class="registration-prompt">
538- <p>👋 <button class="link-button" onclick={openSignup}>Inscris-toi</button> pour lire le reste du chat !</p>538+ <p>👋 <button class="link-button" onclick={openSignup}>Inscris-toi</button>pour lire le reste du chat !</p>
539 </div>539 </div>
540 {/if}540 {/if}
541 {#each visibleMessages as message (message.id)}541 {#each visibleMessages as message (message.id)}
@@ -546,9 +546,10 @@ function handleLoginSuccess() {
546 text: formatFrenchDateTime(new Date(message.timestamp)), 546 text: formatFrenchDateTime(new Date(message.timestamp)),
547 direction: "left",547 direction: "left",
548 closeDelay: 1000,548 closeDelay: 1000,
549- touchBehavior: "remove"549+ touchBehavior: "remove",
550+ interactive: false
550 }}>551 }}>
551- <span class="nickname">{message.user.nickname}</span>552+ <span class="nickname pointer-events-none">{message.user.nickname}</span>
552 </Tooltip>553 </Tooltip>
553 {message.content}554 {message.content}
554 </span>555 </span>
@@ -557,9 +558,10 @@ function handleLoginSuccess() {
557 text: formatFrenchDateTime(new Date(message.timestamp)), 558 text: formatFrenchDateTime(new Date(message.timestamp)),
558 direction: "left",559 direction: "left",
559 closeDelay: 1000,560 closeDelay: 1000,
560- touchBehavior: "remove"561+ touchBehavior: "remove",
562+ interactive: false
561 }}>563 }}>
562- <span class="nickname">{message.user.nickname}:</span>564+ <span class="nickname pointer-events-none">{message.user.nickname}:</span>
563 </Tooltip>565 </Tooltip>
564 <span class="content">{message.content}</span>566 <span class="content">{message.content}</span>
565 {/if}567 {/if}
@@ -1020,4 +1022,8 @@ function handleLoginSuccess() {
1020 gap: 2px;1022 gap: 2px;
1021 margin-left: auto;1023 margin-left: auto;
1022 }1024 }
1025+
1026+ .pointer-events-none {
1027+ pointer-events: none;
1028+ }
1023 </style>⏎̸1029 </style>⏎̸
M src/lib/components/ui/tooltip.svelte +47 -7
@@ -12,8 +12,8 @@
1212 let text = $derived(data.text);
1313 let direction = $derived(data.direction ?? 'top');
1414
15- let wrapper: HTMLDivElement;
16- let tooltipDiv: HTMLDivElement | null = null;
15+ let wrapper: HTMLButtonElement;
16+ let tooltipDiv = $state<HTMLDivElement | null>(null);
1717 let coords = { x: 0, y: 0 };
1818
1919 function updateCoords() {
@@ -33,11 +33,14 @@
3333
3434 function showTooltip() {
3535 updateCoords();
36+ if (tooltipDiv) return; // Prevent multiple tooltips
3637 tooltipDiv = document.createElement('div');
3738 tooltipDiv.className = `tooltip ${direction}`;
3839 tooltipDiv.style.position = 'fixed';
3940 tooltipDiv.style.zIndex = '1000000';
4041 tooltipDiv.textContent = text;
42+ tooltipDiv.setAttribute('role', 'tooltip');
43+ tooltipDiv.setAttribute('aria-hidden', 'false');
4144 document.body.appendChild(tooltipDiv);
4245
4346 const tooltipRect = tooltipDiv.getBoundingClientRect();
@@ -78,24 +81,61 @@
7881 tooltipDiv = null;
7982 }
8083 }
84+
85+ function handleKeyDown(e: KeyboardEvent) {
86+ if (e.key === 'Escape') {
87+ hideTooltip();
88+ }
89+ }
8190 </script>
8291
83-<div
92+<button
8493 class="tooltip-wrapper"
8594 bind:this={wrapper}
8695 onmouseover={showTooltip}
8796 onmouseout={hideTooltip}
8897 onfocus={showTooltip}
8998 onblur={hideTooltip}
90- role="tooltip"
91- tabindex="-1"
99+ onkeydown={handleKeyDown}
100+ type="button"
101+ aria-describedby={tooltipDiv?.id}
92102 >
93103 {@render children()}
94-</div>
104+</button>
95105
96106 <style>
97107 .tooltip-wrapper {
98- display: inline-block;
108+ display: inline;
109+ width: auto;
110+ min-width: 0;
111+ background: none !important;
112+ border: none !important;
113+ padding: 0 !important;
114+ margin: 0 !important;
115+ font: inherit !important;
116+ color: inherit !important;
117+ cursor: inherit !important;
118+ text-align: inherit !important;
119+ box-shadow: none !important;
120+ text-shadow: inherit !important;
121+ border-radius: 0 !important;
122+ -webkit-appearance: none !important;
123+ appearance: none !important;
124+ white-space: pre;
125+ }
126+ .tooltip-wrapper:hover,
127+ .tooltip-wrapper:active {
128+ background: none !important;
129+ border: none !important;
130+ box-shadow: none !important;
131+ }
132+ .tooltip-wrapper:focus {
133+ outline: none !important;
134+ box-shadow: none !important;
135+ }
136+ .tooltip-wrapper:focus-visible {
137+ outline: 2px solid #003C74 !important;
138+ outline-offset: 2px !important;
99139 }
100140 /* Use :global for styles that need to apply to elements outside the component */
101141 :global(.tooltip) {
@@ -12,8 +12,8 @@
12 let text = $derived(data.text);12 let text = $derived(data.text);
13 let direction = $derived(data.direction ?? 'top');13 let direction = $derived(data.direction ?? 'top');
14 14
15- let wrapper: HTMLDivElement;15+ let wrapper: HTMLButtonElement;
16- let tooltipDiv: HTMLDivElement | null = null;16+ let tooltipDiv = $state<HTMLDivElement | null>(null);
17 let coords = { x: 0, y: 0 };17 let coords = { x: 0, y: 0 };
18 18
19 function updateCoords() {19 function updateCoords() {
@@ -33,11 +33,14 @@
33 33
34 function showTooltip() {34 function showTooltip() {
35 updateCoords();35 updateCoords();
36+ if (tooltipDiv) return; // Prevent multiple tooltips
36 tooltipDiv = document.createElement('div');37 tooltipDiv = document.createElement('div');
37 tooltipDiv.className = `tooltip ${direction}`;38 tooltipDiv.className = `tooltip ${direction}`;
38 tooltipDiv.style.position = 'fixed';39 tooltipDiv.style.position = 'fixed';
39 tooltipDiv.style.zIndex = '1000000';40 tooltipDiv.style.zIndex = '1000000';
40 tooltipDiv.textContent = text;41 tooltipDiv.textContent = text;
42+ tooltipDiv.setAttribute('role', 'tooltip');
43+ tooltipDiv.setAttribute('aria-hidden', 'false');
41 document.body.appendChild(tooltipDiv);44 document.body.appendChild(tooltipDiv);
42 45
43 const tooltipRect = tooltipDiv.getBoundingClientRect();46 const tooltipRect = tooltipDiv.getBoundingClientRect();
@@ -78,24 +81,61 @@
78 tooltipDiv = null;81 tooltipDiv = null;
79 }82 }
80 }83 }
84+
85+ function handleKeyDown(e: KeyboardEvent) {
86+ if (e.key === 'Escape') {
87+ hideTooltip();
88+ }
89+ }
81 </script>90 </script>
82 91
83-<div 92+<button
84 class="tooltip-wrapper" 93 class="tooltip-wrapper"
85 bind:this={wrapper}94 bind:this={wrapper}
86 onmouseover={showTooltip}95 onmouseover={showTooltip}
87 onmouseout={hideTooltip}96 onmouseout={hideTooltip}
88 onfocus={showTooltip}97 onfocus={showTooltip}
89 onblur={hideTooltip}98 onblur={hideTooltip}
90- role="tooltip"99+ onkeydown={handleKeyDown}
91- tabindex="-1"100+ type="button"
101+ aria-describedby={tooltipDiv?.id}
92 >102 >
93 {@render children()}103 {@render children()}
94-</div>104+</button>
95 105
96 <style>106 <style>
97 .tooltip-wrapper {107 .tooltip-wrapper {
98- display: inline-block;108+ display: inline;
109+ width: auto;
110+ min-width: 0;
111+ background: none !important;
112+ border: none !important;
113+ padding: 0 !important;
114+ margin: 0 !important;
115+ font: inherit !important;
116+ color: inherit !important;
117+ cursor: inherit !important;
118+ text-align: inherit !important;
119+ box-shadow: none !important;
120+ text-shadow: inherit !important;
121+ border-radius: 0 !important;
122+ -webkit-appearance: none !important;
123+ appearance: none !important;
124+ white-space: pre;
125+ }
126+ .tooltip-wrapper:hover,
127+ .tooltip-wrapper:active {
128+ background: none !important;
129+ border: none !important;
130+ box-shadow: none !important;
131+ }
132+ .tooltip-wrapper:focus {
133+ outline: none !important;
134+ box-shadow: none !important;
135+ }
136+ .tooltip-wrapper:focus-visible {
137+ outline: 2px solid #003C74 !important;
138+ outline-offset: 2px !important;
99 }139 }
100 /* Use :global for styles that need to apply to elements outside the component */140 /* Use :global for styles that need to apply to elements outside the component */
101 :global(.tooltip) {141 :global(.tooltip) {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 8

↑↓ moveenter openesc close