mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-25 14:13:40 +09:00
fix(ui): misc ui fixes (#39336)
1. Give menu items an inset pill highlight, text position and menu width are unchanged in most menus 1. Add arrow key, Enter, Space and Escape handling to all tippy menus 1. Mark the keyboard cursor of fomantic and tippy menus with the focus ring instead of the hover background 1. Mark the current dropdown item with the active color so it stands out from the hovered one 1. Stop navbar dropdown links from taking the navbar item hover color 1. Replace the actions job log options dropdown with the shared tippy menu, its hover highlight was invisible 1. Stop changing font weight on active and selected menu items, it resized rows while arrowing 1. Stretch the "All extensions" button in the diff file extension filter to the full menu width 1. Fix the actions run summary block covering the panel's rounded corners and indenting wrapped stats
This commit is contained in:
+12
-19
@@ -445,11 +445,6 @@ a.label,
|
||||
color: var(--color-text) !important;
|
||||
}
|
||||
|
||||
.user-menu > .item {
|
||||
width: 100%;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.color-preview {
|
||||
display: inline-block;
|
||||
margin-left: 0.4em;
|
||||
@@ -544,24 +539,22 @@ overflow-menu .overflow-menu-popup::after {
|
||||
border-bottom-color: var(--color-menu);
|
||||
}
|
||||
|
||||
overflow-menu .overflow-menu-popup > .item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 9px 18px !important;
|
||||
color: var(--color-text) !important;
|
||||
background: transparent !important;
|
||||
text-decoration: none;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
overflow-menu.ui.menu .overflow-menu-popup > .item.item { /* doubled class beats the host menu's item styles */
|
||||
margin: 0 4px;
|
||||
padding-inline: 8px !important; /* plus the margin, matches the host's tab padding */
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--color-text);
|
||||
width: calc(100% - 8px);
|
||||
}
|
||||
|
||||
overflow-menu .overflow-menu-popup > .item:hover,
|
||||
overflow-menu .overflow-menu-popup > .item:focus {
|
||||
background: var(--color-hover) !important;
|
||||
overflow-menu.ui.menu .overflow-menu-popup > .item.item:hover,
|
||||
overflow-menu.ui.menu .overflow-menu-popup > .item.item:focus {
|
||||
background: var(--color-hover);
|
||||
}
|
||||
|
||||
overflow-menu .overflow-menu-popup > .item.active {
|
||||
background: var(--color-active) !important;
|
||||
overflow-menu.ui.menu .overflow-menu-popup > .item.item.active {
|
||||
background: var(--color-active);
|
||||
}
|
||||
|
||||
overflow-menu .overflow-menu-items {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
top: 100%;
|
||||
min-width: max-content;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding: 4px 0;
|
||||
background: var(--color-menu);
|
||||
font-size: 1em;
|
||||
text-align: left;
|
||||
@@ -69,7 +69,10 @@
|
||||
line-height: var(--line-height-default);
|
||||
font-size: 1rem;
|
||||
color: var(--color-text);
|
||||
padding: 0.78571429rem 1.14285714rem !important;
|
||||
width: calc(100% - 8px); /* button items do not stretch on their own */
|
||||
margin: 0 4px;
|
||||
padding: 0.78571429rem 0.85714286rem !important; /* 12px, the margin adds the remaining 4px */
|
||||
border-radius: var(--border-radius);
|
||||
text-transform: none;
|
||||
font-weight: var(--font-weight-normal);
|
||||
box-shadow: none;
|
||||
@@ -211,6 +214,7 @@ select.ui.dropdown {
|
||||
border-top-width: 0 !important;
|
||||
outline: none;
|
||||
margin: 0 -1px;
|
||||
padding: 0;
|
||||
min-width: calc(100% + 2px);
|
||||
width: calc(100% + 2px);
|
||||
border-radius: 0 0 0.28571429rem 0.28571429rem;
|
||||
@@ -252,15 +256,14 @@ select.ui.dropdown {
|
||||
|
||||
.ui.selection.dropdown .menu > .item {
|
||||
border-top: 1px solid var(--color-secondary);
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0.78571429rem 1.14285714rem !important;
|
||||
border-radius: 0;
|
||||
white-space: normal;
|
||||
overflow-wrap: normal;
|
||||
}
|
||||
|
||||
.ui.selection.dropdown .menu .item:first-of-type {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.ui.selection.dropdown .menu > .hidden.addition.item {
|
||||
display: none;
|
||||
}
|
||||
@@ -505,7 +508,11 @@ select.ui.dropdown {
|
||||
color: var(--color-text);
|
||||
box-shadow: none;
|
||||
z-index: 12;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.ui.dropdown.keyboard-nav .menu .selected.item {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.ui.dropdown .menu > .item:hover {
|
||||
@@ -579,7 +586,7 @@ select.ui.dropdown {
|
||||
|
||||
.ui.dropdown .menu .selected.item {
|
||||
color: var(--color-text);
|
||||
background: var(--color-hover);
|
||||
background: var(--color-active);
|
||||
}
|
||||
|
||||
.ui.dropdown > .filtered.text {
|
||||
@@ -821,18 +828,6 @@ select.ui.dropdown {
|
||||
margin-right: 0.78571429rem;
|
||||
}
|
||||
|
||||
.ui.dropdown .menu .item {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.ui.dropdown .menu .item:first-of-type {
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
}
|
||||
|
||||
.ui.dropdown .menu .item:last-of-type {
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
}
|
||||
|
||||
/* Gitea uses SVG images instead of Fomantic builtin "<i>" font icons, so we need to reset the icon styles */
|
||||
.ui.ui.dropdown > .icon.icon {
|
||||
position: initial;
|
||||
@@ -897,10 +892,6 @@ select.ui.dropdown {
|
||||
max-width: 80vw;
|
||||
}
|
||||
|
||||
.scrolling.menu .item.selected {
|
||||
font-weight: var(--font-weight-semibold) !important;
|
||||
}
|
||||
|
||||
/* to override Fomantic's default display: block for ".menu .item", and use a slightly larger gap for menu item content
|
||||
the "!important" is necessary to override Fomantic UI menu item styles, meanwhile we should keep the "hidden" items still hidden */
|
||||
.ui.dropdown .menu.flex-items-menu > .item:not(.hidden, .filtered, .tw-hidden) {
|
||||
|
||||
@@ -96,33 +96,6 @@
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.ui.menu .ui.dropdown .menu > .item {
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
font-size: 1em !important;
|
||||
padding: 0.78571429em 1.14285714em !important;
|
||||
background: transparent !important;
|
||||
color: var(--color-text) !important;
|
||||
font-weight: var(--font-weight-normal) !important;
|
||||
}
|
||||
.ui.menu .ui.dropdown .menu > .item:hover {
|
||||
color: var(--color-text) !important;
|
||||
background: var(--color-hover) !important;
|
||||
}
|
||||
.ui.menu .ui.dropdown .menu > .selected.item {
|
||||
color: var(--color-text) !important;
|
||||
background: var(--color-hover) !important;
|
||||
}
|
||||
.ui.menu .ui.dropdown .menu > .active.item {
|
||||
color: var(--color-text) !important;
|
||||
background: var(--color-active) !important;
|
||||
font-weight: var(--font-weight-medium) !important;
|
||||
}
|
||||
|
||||
.ui.menu .ui.dropdown.item .menu .item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ui.menu .ui.dropdown.item .menu .item:not(.filtered) {
|
||||
display: block;
|
||||
}
|
||||
@@ -442,7 +415,8 @@
|
||||
.ui.stackable.menu {
|
||||
flex-direction: column;
|
||||
}
|
||||
.ui.stackable.menu .item {
|
||||
.ui.stackable.menu > .item,
|
||||
.ui.stackable.menu > .menu > .item {
|
||||
width: 100% !important;
|
||||
}
|
||||
.ui.stackable.menu .left.menu {
|
||||
|
||||
@@ -34,8 +34,7 @@
|
||||
background: var(--color-active);
|
||||
}
|
||||
|
||||
#navbar a.item:hover,
|
||||
#navbar button.item:hover {
|
||||
#navbar :is(a, button).item:not(.dropdown *):hover {
|
||||
background: var(--color-nav-hover-bg);
|
||||
}
|
||||
|
||||
|
||||
@@ -91,22 +91,20 @@
|
||||
.tippy-box[data-theme="menu"] .item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 9px 18px;
|
||||
margin: 0 4px;
|
||||
padding: 9px 14px;
|
||||
border-radius: var(--border-radius);
|
||||
color: inherit;
|
||||
background: inherit;
|
||||
text-decoration: none;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
width: calc(100% - 8px); /* button items do not stretch on their own */
|
||||
}
|
||||
|
||||
.tippy-box[data-theme="menu"] .item:hover {
|
||||
background: var(--color-hover);
|
||||
}
|
||||
|
||||
.tippy-box[data-theme="menu"] .item:focus-visible {
|
||||
background: var(--color-hover);
|
||||
}
|
||||
|
||||
.tippy-box[data-theme="menu"] .item.active {
|
||||
background: var(--color-active);
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
.ui.dropdown.select-reaction .menu > .item {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
border-radius: var(--border-radius);
|
||||
display: flex;
|
||||
|
||||
@@ -66,11 +66,8 @@ gitea-theme-meta-info {
|
||||
--color-console-fg: #f8f8f8;
|
||||
--color-console-fg-subtle: #c1c3c8;
|
||||
--color-console-bg: #191a1c;
|
||||
--color-console-border: #313338;
|
||||
--color-console-hover-bg: #2a2c30;
|
||||
--color-console-active-bg: #313338;
|
||||
--color-console-menu-bg: #292b2e;
|
||||
--color-console-menu-border: #46494f;
|
||||
--color-console-link: #969aa1;
|
||||
/* named colors */
|
||||
--color-red: #cc4848;
|
||||
|
||||
@@ -66,11 +66,8 @@ gitea-theme-meta-info {
|
||||
--color-console-fg: #0d1117;
|
||||
--color-console-fg-subtle: #40474d;
|
||||
--color-console-bg: #ffffff;
|
||||
--color-console-border: #d0d7de;
|
||||
--color-console-hover-bg: #f1f3f5;
|
||||
--color-console-active-bg: #d0d7de;
|
||||
--color-console-menu-bg: #f8f9fb;
|
||||
--color-console-menu-border: #d0d7de;
|
||||
--color-console-link: #5c656d;
|
||||
/* named colors */
|
||||
--color-red: #db2828;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, nextTick, onBeforeUnmount, onMounted, ref, toRefs, watch} from 'vue';
|
||||
import {computed, nextTick, onBeforeUnmount, onMounted, ref, toRefs, useTemplateRef, watch} from 'vue';
|
||||
import SvgIcon from './SvgIcon.vue';
|
||||
import ActionStatusIcon from './ActionStatusIcon.vue';
|
||||
import {addDelegatedEventListener, createElementFromAttrs} from '../utils/dom.ts';
|
||||
import {formatDatetime, formatDatetimeISO} from '../utils/time.ts';
|
||||
import {POST} from '../modules/fetch.ts';
|
||||
import {createTippy} from '../modules/tippy.ts';
|
||||
import {copyToClipboardWithFeedback} from '../modules/clipboard.ts';
|
||||
import type {Instance} from 'tippy.js';
|
||||
import type {IntervalId} from '../types.ts';
|
||||
import {toggleFullScreen} from '../utils.ts';
|
||||
import {localUserSettings} from '../modules/user-settings.ts';
|
||||
@@ -115,7 +117,6 @@ let loadingAbortController: AbortController | null = null;
|
||||
let intervalID: IntervalId | null = null;
|
||||
|
||||
const currentJobStepsStates = ref<Array<JobStepState>>([]);
|
||||
const menuVisible = ref(false);
|
||||
const isFullScreen = ref(false);
|
||||
const timeVisible = ref<Record<string, boolean>>({
|
||||
'log-time-stamp': actionsLogShowTimestamps,
|
||||
@@ -130,6 +131,9 @@ const currentJob = ref<CurrentJob>({
|
||||
});
|
||||
const stepsContainer = ref<HTMLElement | null>(null);
|
||||
const jobStepLogs = ref<Array<StepContainerElement | undefined>>([]);
|
||||
const menuTriggerEl = useTemplateRef<HTMLButtonElement>('menuTriggerEl');
|
||||
const menuPanelEl = useTemplateRef<HTMLDivElement>('menuPanelEl');
|
||||
let menuTippy: Instance;
|
||||
|
||||
// Reusable workflow caller view: the right pane shows just the header (name + uses path +
|
||||
// status). Callers don't run on a runner, and the dependency graph for their children lives
|
||||
@@ -146,6 +150,16 @@ watch(optionAlwaysExpandRunning, () => {
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
menuTippy = createTippy(menuTriggerEl.value!, {
|
||||
content: menuPanelEl.value!,
|
||||
trigger: 'click',
|
||||
interactive: true,
|
||||
hideOnClick: true,
|
||||
placement: 'bottom-end',
|
||||
theme: 'menu',
|
||||
arrow: false,
|
||||
});
|
||||
|
||||
// load job data and then auto-reload periodically
|
||||
// need to await first loadJob so this.currentJobStepsStates is initialized and can be used in hashChangeListener
|
||||
await loadJob();
|
||||
@@ -163,13 +177,12 @@ onMounted(async () => {
|
||||
});
|
||||
|
||||
intervalID = setInterval(() => void loadJob(), 1000);
|
||||
document.body.addEventListener('click', closeDropdown);
|
||||
void hashChangeListener();
|
||||
window.addEventListener('hashchange', hashChangeListener);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.body.removeEventListener('click', closeDropdown);
|
||||
menuTippy.destroy();
|
||||
window.removeEventListener('hashchange', hashChangeListener);
|
||||
// clear the interval timer when the component is unmounted
|
||||
// even our page is rendered once, not spa style
|
||||
@@ -396,10 +409,6 @@ function isExpandable(status: ActionsStatus) {
|
||||
return ['success', 'running', 'failure', 'cancelled'].includes(status);
|
||||
}
|
||||
|
||||
function closeDropdown() {
|
||||
if (menuVisible.value) menuVisible.value = false;
|
||||
}
|
||||
|
||||
function elStepsContainer(): HTMLElement {
|
||||
return stepsContainer.value as HTMLElement;
|
||||
}
|
||||
@@ -449,38 +458,36 @@ async function hashChangeListener() {
|
||||
</p>
|
||||
</div>
|
||||
<div class="job-info-header-right">
|
||||
<div class="ui top right pointing dropdown custom jump item" @click.stop="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible">
|
||||
<button class="btn interact-bg tw-p-2">
|
||||
<SvgIcon name="octicon-gear" :size="18"/>
|
||||
</button>
|
||||
<div class="menu transition action-job-menu" :class="{visible: menuVisible}" v-if="menuVisible" v-cloak>
|
||||
<a class="item" @click="toggleTimeDisplay('seconds')">
|
||||
<i class="icon"><SvgIcon :name="timeVisible['log-time-seconds'] ? 'octicon-check' : 'gitea-empty-checkbox'"/></i>
|
||||
{{ locale.showLogSeconds }}
|
||||
</a>
|
||||
<a class="item" @click="toggleTimeDisplay('stamp')">
|
||||
<i class="icon"><SvgIcon :name="timeVisible['log-time-stamp'] ? 'octicon-check' : 'gitea-empty-checkbox'"/></i>
|
||||
{{ locale.showTimeStamps }}
|
||||
</a>
|
||||
<a class="item" @click="toggleFullScreenMode()">
|
||||
<i class="icon"><SvgIcon :name="isFullScreen ? 'octicon-check' : 'gitea-empty-checkbox'"/></i>
|
||||
{{ locale.showFullScreen }}
|
||||
</a>
|
||||
<div class="divider"/>
|
||||
<a class="item" @click="optionAlwaysAutoScroll = !optionAlwaysAutoScroll">
|
||||
<i class="icon"><SvgIcon :name="optionAlwaysAutoScroll ? 'octicon-check' : 'gitea-empty-checkbox'"/></i>
|
||||
{{ locale.logsAlwaysAutoScroll }}
|
||||
</a>
|
||||
<a class="item" @click="optionAlwaysExpandRunning = !optionAlwaysExpandRunning">
|
||||
<i class="icon"><SvgIcon :name="optionAlwaysExpandRunning ? 'octicon-check' : 'gitea-empty-checkbox'"/></i>
|
||||
{{ locale.logsAlwaysExpandRunning }}
|
||||
</a>
|
||||
<div class="divider"/>
|
||||
<a :class="['item', !currentJob.steps.length ? 'disabled' : '']" :href="run.link + '/jobs/' + jobId + '/logs'" download>
|
||||
<i class="icon"><SvgIcon name="octicon-download"/></i>
|
||||
{{ locale.downloadLogs }}
|
||||
</a>
|
||||
</div>
|
||||
<button ref="menuTriggerEl" type="button" class="btn interact-bg tw-p-2">
|
||||
<SvgIcon name="octicon-gear" :size="18"/>
|
||||
</button>
|
||||
<div ref="menuPanelEl" class="tippy-target" @click="menuTippy.hide()">
|
||||
<a class="item" role="menuitemcheckbox" :aria-checked="timeVisible['log-time-seconds']" @click="toggleTimeDisplay('seconds')">
|
||||
<SvgIcon :name="timeVisible['log-time-seconds'] ? 'octicon-check' : 'gitea-empty-checkbox'"/>
|
||||
{{ locale.showLogSeconds }}
|
||||
</a>
|
||||
<a class="item" role="menuitemcheckbox" :aria-checked="timeVisible['log-time-stamp']" @click="toggleTimeDisplay('stamp')">
|
||||
<SvgIcon :name="timeVisible['log-time-stamp'] ? 'octicon-check' : 'gitea-empty-checkbox'"/>
|
||||
{{ locale.showTimeStamps }}
|
||||
</a>
|
||||
<a class="item" role="menuitemcheckbox" :aria-checked="isFullScreen" @click="toggleFullScreenMode()">
|
||||
<SvgIcon :name="isFullScreen ? 'octicon-check' : 'gitea-empty-checkbox'"/>
|
||||
{{ locale.showFullScreen }}
|
||||
</a>
|
||||
<div class="divider"/>
|
||||
<a class="item" role="menuitemcheckbox" :aria-checked="optionAlwaysAutoScroll" @click="optionAlwaysAutoScroll = !optionAlwaysAutoScroll">
|
||||
<SvgIcon :name="optionAlwaysAutoScroll ? 'octicon-check' : 'gitea-empty-checkbox'"/>
|
||||
{{ locale.logsAlwaysAutoScroll }}
|
||||
</a>
|
||||
<a class="item" role="menuitemcheckbox" :aria-checked="optionAlwaysExpandRunning" @click="optionAlwaysExpandRunning = !optionAlwaysExpandRunning">
|
||||
<SvgIcon :name="optionAlwaysExpandRunning ? 'octicon-check' : 'gitea-empty-checkbox'"/>
|
||||
{{ locale.logsAlwaysExpandRunning }}
|
||||
</a>
|
||||
<div class="divider"/>
|
||||
<a class="item" role="menuitem" :class="{disabled: !currentJob.steps.length}" :href="run.link + '/jobs/' + jobId + '/logs'" download>
|
||||
<SvgIcon name="octicon-download"/>
|
||||
{{ locale.downloadLogs }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -534,38 +541,6 @@ async function hashChangeListener() {
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
/* begin fomantic dropdown menu overrides */
|
||||
|
||||
.action-view-right .ui.dropdown .menu {
|
||||
background: var(--color-console-menu-bg);
|
||||
border-color: var(--color-console-menu-border);
|
||||
}
|
||||
|
||||
.action-view-right .ui.dropdown .menu > .item {
|
||||
color: var(--color-console-fg);
|
||||
}
|
||||
|
||||
.action-view-right .ui.dropdown .menu > .item:hover {
|
||||
color: var(--color-console-fg);
|
||||
background: var(--color-console-hover-bg);
|
||||
}
|
||||
|
||||
.action-view-right .ui.dropdown .menu > .item:active {
|
||||
color: var(--color-console-fg);
|
||||
background: var(--color-console-active-bg);
|
||||
}
|
||||
|
||||
.action-view-right .ui.dropdown .menu > .divider {
|
||||
border-top-color: var(--color-console-menu-border);
|
||||
}
|
||||
|
||||
.action-view-right .ui.pointing.dropdown > .menu:not(.hidden)::after {
|
||||
background: var(--color-console-menu-bg);
|
||||
box-shadow: -1px -1px 0 0 var(--color-console-menu-border);
|
||||
}
|
||||
|
||||
/* end fomantic dropdown menu overrides */
|
||||
|
||||
.job-info-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -581,7 +556,7 @@ async function hashChangeListener() {
|
||||
|
||||
.job-info-header:has(+ .job-step-container) {
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
border-bottom: 1px solid var(--color-console-border);
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
}
|
||||
|
||||
.job-info-header .job-info-header-title {
|
||||
|
||||
@@ -119,7 +119,7 @@ onBeforeUnmount(() => {
|
||||
<span class="action-run-summary-stat-value">{{ run.duration || '–' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="action-run-summary-stat action-run-summary-stat-last">
|
||||
<div class="action-run-summary-stat">
|
||||
<span class="action-run-summary-label">{{ locale.artifactsTitle }}</span>
|
||||
<span class="action-run-summary-stat-value">{{ artifactsDisplay }}</span>
|
||||
</div>
|
||||
@@ -148,9 +148,9 @@ onBeforeUnmount(() => {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: stretch; /* equal-height columns so labels align at top and values at bottom */
|
||||
gap: 8px 48px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
background: var(--color-console-bg);
|
||||
}
|
||||
|
||||
.action-run-summary-trigger {
|
||||
@@ -159,7 +159,6 @@ onBeforeUnmount(() => {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.action-run-summary-label {
|
||||
@@ -222,18 +221,11 @@ onBeforeUnmount(() => {
|
||||
flex-direction: column;
|
||||
flex: 0 0 auto;
|
||||
min-width: 72px;
|
||||
margin-left: 24px;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.action-run-summary-stat-last {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.action-run-summary-stat-divider {
|
||||
display: none;
|
||||
flex: 0 0 100%;
|
||||
margin: 8px 0;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
}
|
||||
|
||||
@@ -247,14 +239,12 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.action-run-summary-trigger {
|
||||
flex: 0 0 100%;
|
||||
margin-right: 0;
|
||||
.action-run-summary-block {
|
||||
column-gap: 24px;
|
||||
}
|
||||
|
||||
.action-run-summary-stat {
|
||||
margin-left: 0;
|
||||
margin-right: 24px;
|
||||
.action-run-summary-trigger {
|
||||
flex: 0 0 100%;
|
||||
}
|
||||
|
||||
.action-run-summary-stat-divider {
|
||||
|
||||
@@ -290,6 +290,7 @@ function commitClickedShift(commit: Commit) {
|
||||
<style scoped>
|
||||
.ui.dropdown.diff-commit-selector .menu {
|
||||
margin-top: 0.25em;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
max-height: 450px;
|
||||
}
|
||||
@@ -305,7 +306,10 @@ function commitClickedShift(commit: Commit) {
|
||||
flex-direction: row;
|
||||
line-height: 1.4;
|
||||
gap: 0.25em;
|
||||
width: auto;
|
||||
margin: 0;
|
||||
padding: 7px 14px !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.ui.dropdown.diff-commit-selector .menu > .item:not(:first-child),
|
||||
|
||||
@@ -40,10 +40,6 @@ function toggleAll() {
|
||||
store.activeExtensions = store.activeExtensions === 'all' ? [] : 'all';
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') tippyInstance.hide();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
tippyInstance = createTippy(triggerEl.value!, {
|
||||
content: panelEl.value!,
|
||||
@@ -54,8 +50,6 @@ onMounted(() => {
|
||||
theme: 'menu',
|
||||
arrow: false,
|
||||
limitSizeToViewport: {vertical: true},
|
||||
onShow: () => document.addEventListener('keydown', onKeyDown),
|
||||
onHide: () => document.removeEventListener('keydown', onKeyDown),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -77,19 +71,17 @@ onUnmounted(() => {
|
||||
<div ref="panelEl" class="tippy-target">
|
||||
<div class="diff-ext-filter-menu" role="menu" :aria-label="props.locale.fileExtensions">
|
||||
<div class="diff-ext-filter-header">{{ props.locale.fileExtensions }}</div>
|
||||
<div class="diff-ext-filter-list">
|
||||
<button
|
||||
v-for="ext in allExtensions" :key="ext.ext"
|
||||
type="button" class="item" role="menuitemcheckbox"
|
||||
:aria-checked="isChecked(ext.ext)" @click="toggleExt(ext.ext)"
|
||||
>
|
||||
<span class="diff-ext-filter-check">
|
||||
<SvgIcon v-if="isChecked(ext.ext)" name="octicon-check"/>
|
||||
</span>
|
||||
<span class="gt-ellipsis">{{ extLabel(ext.ext) }}</span>
|
||||
<span class="diff-ext-filter-count">{{ ext.count }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
v-for="ext in allExtensions" :key="ext.ext"
|
||||
type="button" class="item" role="menuitemcheckbox"
|
||||
:aria-checked="isChecked(ext.ext)" @click="toggleExt(ext.ext)"
|
||||
>
|
||||
<span class="diff-ext-filter-check">
|
||||
<SvgIcon v-if="isChecked(ext.ext)" name="octicon-check"/>
|
||||
</span>
|
||||
<span class="gt-ellipsis">{{ extLabel(ext.ext) }}</span>
|
||||
<span class="diff-ext-filter-count">{{ ext.count }}</span>
|
||||
</button>
|
||||
<div class="divider"/>
|
||||
<button
|
||||
type="button" class="item" role="menuitemcheckbox"
|
||||
@@ -111,28 +103,12 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.diff-ext-filter-header {
|
||||
padding: 6px 16px;
|
||||
padding: 6px 18px;
|
||||
color: var(--color-text-light-2);
|
||||
font-size: 12px;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.diff-ext-filter-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.diff-ext-filter-menu .item {
|
||||
width: auto; /* buttons are shrink-to-fit, the flex column parent stretches them */
|
||||
margin: 0 4px; /* matches the menu's vertical padding so the inset is even on all sides */
|
||||
padding: 6px 12px;
|
||||
gap: 8px;
|
||||
border: none;
|
||||
border-radius: var(--border-radius-medium);
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.diff-ext-filter-check {
|
||||
display: flex;
|
||||
flex: 0 0 16px;
|
||||
|
||||
@@ -542,7 +542,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
.action-view-right-panel {
|
||||
flex: 1; /* fill the right column so the summary graph stretches even without a job-summary section */
|
||||
border: 1px solid var(--color-console-border);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--color-console-bg);
|
||||
display: flex;
|
||||
@@ -589,7 +589,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
.job-summary-section-header {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--color-console-border);
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
background: var(--color-console-bg);
|
||||
color: var(--color-console-fg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
@@ -606,7 +606,7 @@ onBeforeUnmount(() => {
|
||||
padding: 12px;
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--color-console-hover-bg);
|
||||
border: 1px solid var(--color-console-border);
|
||||
border: 1px solid var(--color-secondary);
|
||||
}
|
||||
|
||||
.job-summary-header {
|
||||
|
||||
@@ -443,7 +443,6 @@ function onNodeClick(job: GraphNode | ActionsJob, event: MouseEvent) {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 16px 8px;
|
||||
background: var(--color-console-bg);
|
||||
gap: var(--gap-block);
|
||||
flex-wrap: wrap;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
|
||||
@@ -241,10 +241,14 @@ function attachDomEvents(dropdown: AriaDropdownElement, focusable: HTMLElement,
|
||||
}
|
||||
};
|
||||
|
||||
dropdown.addEventListener('mousedown', () => dropdown.classList.remove('keyboard-nav')); // stands in for ":focus-visible", menu items never receive focus
|
||||
|
||||
dropdown.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if (e.isComposing) return;
|
||||
// here it must use keydown event before dropdown's keyup handler, otherwise there is no Enter event in our keyup handler
|
||||
if (e.key === 'Enter') {
|
||||
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
||||
dropdown.classList.add('keyboard-nav');
|
||||
} else if (e.key === 'Enter') {
|
||||
// here it must use keydown event before dropdown's keyup handler, otherwise there is no Enter event in our keyup handler
|
||||
const elItem = menu.querySelector<HTMLElement>(':scope > .item.selected, .menu > .item.selected');
|
||||
// if the selected item is clickable, then trigger the click event.
|
||||
// we can not click any item without check, because Fomantic code might also handle the Enter event. that would result in double click.
|
||||
@@ -279,6 +283,7 @@ function attachDomEvents(dropdown: AriaDropdownElement, focusable: HTMLElement,
|
||||
}, {capture: true});
|
||||
dropdown.addEventListener('blur', () => {
|
||||
ignoreClickPreVisible = ignoreClickPreEvents = 0;
|
||||
dropdown.classList.remove('keyboard-nav');
|
||||
deferredRefreshAriaActiveItem(100);
|
||||
}, {capture: true});
|
||||
dropdown.addEventListener('mouseup', () => {
|
||||
|
||||
@@ -1,4 +1,38 @@
|
||||
import {availableSizeForPlacement} from './tippy.ts';
|
||||
import {userEvent} from 'vitest/browser';
|
||||
import {createElementFromHTML} from '../utils/dom.ts';
|
||||
import {availableSizeForPlacement, createTippy} from './tippy.ts';
|
||||
|
||||
test('createTippy handles keys in menus only', async () => {
|
||||
const menuButton = createElementFromHTML('<button>menu</button>');
|
||||
const panelButton = createElementFromHTML('<button>panel</button>');
|
||||
document.body.append(menuButton, panelButton);
|
||||
|
||||
const clicked: Array<string> = [];
|
||||
const menuContent = createElementFromHTML('<div><button class="item">a</button><button class="item">b</button></div>');
|
||||
for (const item of menuContent.querySelectorAll('.item')) item.addEventListener('click', () => { clicked.push(item.textContent) });
|
||||
const menu = createTippy(menuButton, {content: menuContent, theme: 'menu', trigger: 'manual', interactive: true});
|
||||
menu.show();
|
||||
await userEvent.keyboard('{ArrowDown}{ArrowDown} {Enter}');
|
||||
expect(clicked).toEqual(['b', 'b']);
|
||||
await userEvent.keyboard('{Escape}');
|
||||
expect(menu.state.isVisible).toBe(false);
|
||||
expect(document.activeElement).toBe(menuButton);
|
||||
|
||||
menu.show();
|
||||
const panel = createTippy(panelButton, {content: createElementFromHTML('<div><textarea></textarea></div>'), trigger: 'manual', interactive: true});
|
||||
panel.show();
|
||||
const textarea = panel.popper.querySelector('textarea')!;
|
||||
textarea.focus();
|
||||
await userEvent.keyboard('a b{Enter}{ArrowUp}c{Escape}');
|
||||
expect(textarea.value).toEqual('ca b\n');
|
||||
expect(menu.state.isVisible).toBe(true);
|
||||
expect(panel.state.isVisible).toBe(true);
|
||||
|
||||
menu.destroy();
|
||||
panel.destroy();
|
||||
menuButton.remove();
|
||||
panelButton.remove();
|
||||
});
|
||||
|
||||
test('availableSizeForPlacement', () => {
|
||||
const rect = (values: Partial<DOMRect>) => values as DOMRect;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import tippy, {followCursor} from 'tippy.js';
|
||||
import {isDocumentFragmentOrElementNode} from '../utils/dom.ts';
|
||||
import {isDocumentFragmentOrElementNode, isElemVisible} from '../utils/dom.ts';
|
||||
import type {Content, Instance, Placement, Props} from 'tippy.js';
|
||||
import {html, htmlEscape} from '../utils/html.ts';
|
||||
import {stripTags} from '../utils.ts';
|
||||
@@ -13,6 +13,7 @@ type TippyOpts = {
|
||||
type PopperModifier = NonNullable<NonNullable<Props['popperOptions']>['modifiers']>[number];
|
||||
|
||||
const visibleInstances = new Set<Instance>();
|
||||
|
||||
const arrowSvg = html`<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`;
|
||||
|
||||
// shrink tippy's default 3px arrow padding so the arrow can point at the center of
|
||||
@@ -59,6 +60,37 @@ function sizeModifier(limit: {horizontal?: boolean, vertical?: boolean}): Popper
|
||||
};
|
||||
}
|
||||
|
||||
function isMenu(instance: Instance): boolean {
|
||||
return instance.props.role === 'menu' && instance.props.theme === 'menu'; // role defaults to "menu" for every non-tooltip popup
|
||||
}
|
||||
|
||||
function focusMenuItem(instance: Instance, delta: number) {
|
||||
const items = [...instance.popper.querySelectorAll<HTMLElement>('.item:not(.disabled)')].filter(isElemVisible);
|
||||
if (!items.length) return;
|
||||
const current = items.indexOf(document.activeElement as HTMLElement);
|
||||
const next = current === -1 ? (delta > 0 ? 0 : items.length - 1) : (current + delta + items.length) % items.length;
|
||||
items[next].focus();
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if (e.isComposing) return;
|
||||
const menuInstance = [...visibleInstances].findLast(isMenu);
|
||||
if (!menuInstance) return;
|
||||
const focused = document.activeElement as HTMLElement;
|
||||
const inMenu = menuInstance.popper.contains(focused);
|
||||
if (!inMenu && focused !== menuInstance.reference && focused !== document.body) return; // body: macOS Safari and Firefox do not focus clicked buttons
|
||||
if (e.key === 'Escape') {
|
||||
menuInstance.hide();
|
||||
} else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||
focusMenuItem(menuInstance, e.key === 'ArrowDown' ? 1 : -1);
|
||||
} else if ((e.key === 'Enter' || e.key === ' ') && inMenu) {
|
||||
focused.click();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
export function createTippy(target: Element, opts: TippyOpts = {}): Instance {
|
||||
// the callback functions should be destructured from opts,
|
||||
// because we should use our own wrapper functions to handle them, do not let the user override them
|
||||
@@ -77,6 +109,7 @@ export function createTippy(target: Element, opts: TippyOpts = {}): Instance {
|
||||
maxWidth: 500, // increase over default 350px
|
||||
onHide: (instance: Instance) => {
|
||||
visibleInstances.delete(instance);
|
||||
if (isMenu(instance) && instance.popper.contains(document.activeElement)) (instance.reference as HTMLElement).focus();
|
||||
return onHide?.(instance);
|
||||
},
|
||||
onDestroy: (instance: Instance) => {
|
||||
@@ -92,6 +125,9 @@ export function createTippy(target: Element, opts: TippyOpts = {}): Instance {
|
||||
}
|
||||
visibleInstances.add(instance);
|
||||
target.setAttribute('aria-controls', instance.popper.id);
|
||||
if (isMenu(instance)) { // focusable by the arrow keys, out of the Tab order
|
||||
for (const item of instance.popper.querySelectorAll<HTMLElement>('.item')) item.tabIndex = -1;
|
||||
}
|
||||
return onShow?.(instance);
|
||||
},
|
||||
arrow: resolvedArrow,
|
||||
|
||||
Reference in New Issue
Block a user