Fix button inconsistencies and macOS layout shifts

This commit fixes layout shifts experienced in macOS Safari when
hovering over top menu items. Instead of making text bold — which was
causing layout shifts — the hover effect now changes the text color.
This ensures a consistent UI across different browsers and platforms.

Additionally, this commit fixes the styling of the privacy button
located in the bottom right corner. Previously styled as an `<a>`
element, it is now correctly represented as a `<button>`.

Furthermore, the commit enhances HTML conformity and accessibility by
correctly using `<button>` and `<a>` tags instead of relying on click
interactions on `<span>` elements.

This commit introduces `FlatButton` Vue component and a new
`flat-button` mixin. These centralize button usage and link styles,
aligning the hover/touch reactions of buttons across the application,
thereby creating a more consistent user interface.
This commit is contained in:
undergroundwires
2023-12-29 17:26:40 +01:00
parent 2f06043559
commit 86fde6d7dc
19 changed files with 363 additions and 106 deletions

View File

@@ -30,7 +30,6 @@ $color-on-surface : #4d5156;
// Background | Appears behind scrollable content.
$color-background : #e6ecf4;
/*
Application-specific colors:
These are tailored to the specific needs of the application and derived from the above theme colors.
@@ -38,3 +37,4 @@ $color-background : #e6ecf4;
This approach maintains a cohesive look and feel and simplifies theme adjustments.
*/
$color-scripts-bg: $color-primary-darker;
$color-highlight: $color-primary;

View File

@@ -11,14 +11,10 @@
box-sizing: border-box;
}
$globals-color-hover: $color-primary;
a {
color:inherit;
text-decoration: underline;
color: inherit;
cursor: pointer;
@include hover-or-touch {
color: $globals-color-hover;
}
@include flat-button($disabled: false);
}
body {

View File

@@ -1,3 +1,5 @@
@use "@/presentation/assets/styles/colors" as *;
@mixin hover-or-touch($selector-suffix: '', $selector-prefix: '&') {
@media (hover: hover) {
/*
@@ -65,3 +67,47 @@
padding: 0;
list-style: none;
}
@mixin reset-button {
margin: 0;
padding-block: 0;
padding-inline: 0;
font: unset;
border: unset;
background: unset;
align-items: unset;
text-align: unset;
text-shadow: unset;
text-rendering: unset;
color: inherit;
writing-mode: unset;
letter-spacing: unset;
word-spacing: unset;
line-height: unset;
text-transform: unset;
text-indent: unset;
appearance: unset;
cursor: unset;
}
@mixin flat-button($disabled: false) {
@include reset-button;
@if $disabled {
color: $color-primary-light;
} @else {
color: inherit;
@include clickable;
@include hover-or-touch {
text-decoration: underline;
color: $color-highlight;
/*
Using color change and underlining and as hover cues instead of bold text,
due to inconsistent bold rendering in macOS browsers:
- Safari: Renders bold, causes layout shift.
- Firefox: Renders bold correctly, no layout shift.
- Chromium-based browsers (including Electron app): Do not render bold, no layout shift.
*/
}
}
}

View File

@@ -64,6 +64,8 @@ export default defineComponent({
}
.button {
@include reset-button;
display: flex;
align-items: center;
justify-content: center;
@@ -72,13 +74,13 @@ export default defineComponent({
color: $color-on-secondary;
border: none;
padding:20px;
padding: 20px;
transition-duration: 0.4s;
overflow: hidden;
box-shadow: 0 3px 9px $color-primary-darkest;
border-radius: 4px;
&__icon {
.button__icon {
font-size: 2em;
}

View File

@@ -4,11 +4,7 @@
<code ref="codeElement"><slot /></code>
<div class="copy-action-container">
<TooltipWrapper>
<AppIcon
icon="copy"
class="copy-button"
@click="copyCode"
/>
<FlatButton icon="copy" @click="copyCode" />
<template #tooltip>
Copy
</template>
@@ -20,13 +16,13 @@
<script lang="ts">
import { defineComponent, shallowRef } from 'vue';
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
import { injectKey } from '@/presentation/injectionSymbols';
import FlatButton from '@/presentation/components/Shared/FlatButton.vue';
export default defineComponent({
components: {
TooltipWrapper,
AppIcon,
FlatButton,
},
setup() {
const { copyText } = injectKey((keys) => keys.useClipboard);
@@ -73,12 +69,6 @@ export default defineComponent({
.copy-action-container {
margin-left: 1rem;
}
.copy-button {
@include clickable;
@include hover-or-touch {
color: $color-primary;
}
}
code {
font-size: 1rem;
}

View File

@@ -5,9 +5,7 @@
<div class="title">
Tools
</div>
<button type="button" class="close-button" @click="close">
<AppIcon icon="xmark" />
</button>
<FlatButton icon="xmark" class="close-button" @click="close" />
</div>
<hr />
<div class="action-buttons">
@@ -28,12 +26,12 @@
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { injectKey } from '@/presentation/injectionSymbols';
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
import FlatButton from '@/presentation/components/Shared/FlatButton.vue';
import { dumpNames } from './DumpNames';
export default defineComponent({
components: {
AppIcon,
FlatButton,
},
setup() {
const { log } = injectKey((keys) => keys.useLogger);
@@ -118,19 +116,19 @@ interface DevAction {
display: flex;
flex-direction: column;
gap: 10px;
}
button {
display: block;
padding: 5px 10px;
background-color: $color-primary;
color: $color-on-primary;
border: none;
cursor: pointer;
button {
display: block;
padding: 5px 10px;
background-color: $color-primary;
color: $color-on-primary;
border: none;
cursor: pointer;
@include hover-or-touch {
background-color: $color-secondary;
color: $color-on-secondary;
@include hover-or-touch {
background-color: $color-secondary;
color: $color-on-secondary;
}
}
}
}

View File

@@ -4,23 +4,23 @@
Parent wrapper allows `MenuOptionList` to safely add content inside
such as adding content in `::before` block without making it clickable.
-->
<span
v-non-collapsing
:class="{
disabled: !enabled,
enabled: enabled,
}"
<FlatButton
:disabled="!enabled"
:label="label"
flat
@click="onClicked()"
>{{ label }}</span>
/>
</span>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { NonCollapsing } from '@/presentation/components/Scripts/View/Cards/NonCollapsingDirective';
import FlatButton from '@/presentation/components/Shared/FlatButton.vue';
export default defineComponent({
directives: { NonCollapsing },
components: { FlatButton },
props: {
enabled: {
type: Boolean,
@@ -48,18 +48,3 @@ export default defineComponent({
},
});
</script>
<style scoped lang="scss">
@use "@/presentation/assets/styles/main" as *;
.enabled {
@include clickable;
@include hover-or-touch {
font-weight:bold;
text-decoration:underline;
}
}
.disabled {
color: $color-primary-light;
}
</style>

View File

@@ -31,7 +31,7 @@
</div>
<div class="card__expander" @click.stop>
<div class="card__expander__close-button">
<AppIcon
<FlatButton
icon="xmark"
@click="collapse()"
/>
@@ -51,6 +51,7 @@ import {
defineComponent, computed, shallowRef,
} from 'vue';
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
import FlatButton from '@/presentation/components/Shared/FlatButton.vue';
import { injectKey } from '@/presentation/injectionSymbols';
import ScriptsTree from '@/presentation/components/Scripts/View/Tree/ScriptsTree.vue';
import { sleep } from '@/infrastructure/Threading/AsyncSleep';
@@ -61,6 +62,7 @@ export default defineComponent({
ScriptsTree,
AppIcon,
CardSelectionIndicator,
FlatButton,
},
props: {
categoryId: {

View File

@@ -17,7 +17,7 @@
class="search__query__close-button"
@click="clearSearchQuery()"
>
<AppIcon icon="xmark" />
<FlatButton icon="xmark" />
</div>
</div>
<div v-if="!searchHasMatches" class="search-no-matches">
@@ -39,19 +39,19 @@
import {
defineComponent, PropType, ref, computed,
} from 'vue';
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
import { injectKey } from '@/presentation/injectionSymbols';
import ScriptsTree from '@/presentation/components/Scripts/View/Tree/ScriptsTree.vue';
import CardList from '@/presentation/components/Scripts/View/Cards/CardList.vue';
import { ViewType } from '@/presentation/components/Scripts/Menu/View/ViewType';
import { IReadOnlyUserFilter } from '@/application/Context/State/Filter/IUserFilter';
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
import FlatButton from '@/presentation/components/Shared/FlatButton.vue';
export default defineComponent({
components: {
ScriptsTree,
CardList,
AppIcon,
FlatButton,
},
props: {
currentView: {
@@ -149,14 +149,10 @@ $margin-inner: 4px;
flex-direction: row;
align-items: center;
margin-top: 1em;
color: $color-primary;
color: $color-primary-light;
.search__query__close-button {
@include clickable;
font-size: 1.25em;
margin-left: 0.25rem;
@include hover-or-touch {
color: $color-primary-dark;
}
}
}
.search-no-matches {

View File

@@ -128,7 +128,7 @@ $base-spacing: $text-size;
*/
@include hover-or-touch {
&::after{
background-color: $globals-color-hover;
background-color: $color-highlight;
}
}
}

View File

@@ -1,22 +1,23 @@
<template>
<a
class="button"
target="_blank"
:class="{ 'button-on': isOn }"
<div
class="documentation-button"
:class="{ expanded: isOn }"
@click.stop
@click="toggle()"
>
<AppIcon icon="circle-info" />
</a>
<FlatButton
icon="circle-info"
@click="toggle()"
/>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
import FlatButton from '@/presentation/components/Shared/FlatButton.vue';
export default defineComponent({
components: {
AppIcon,
FlatButton,
},
emits: [
'show',
@@ -45,14 +46,15 @@ export default defineComponent({
<style scoped lang="scss">
@use "@/presentation/assets/styles/main" as *;
.button {
@include clickable;
.documentation-button {
vertical-align: middle;
color: $color-primary;
@include hover-or-touch {
color: $color-primary-darker;
:deep() { // This override leads to inconsistent highlight color, it should be re-styled.
@include hover-or-touch {
color: $color-primary-darker;
}
}
&-on {
&.expanded {
color: $color-primary-light;
}
}

View File

@@ -0,0 +1,73 @@
<template>
<!-- Use `button` instead of DIV as it is semantically correct and accessibility best-practice -->
<button
v-non-collapsing
type="button"
class="flat-button"
:class="{
disabled: disabled,
}"
@click="onClicked"
>
<AppIcon v-if="icon" :icon="icon" />
<span v-if="label">{{ label }}</span>
</button>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { NonCollapsing } from '@/presentation/components/Scripts/View/Cards/NonCollapsingDirective';
import { IconName } from '@/presentation/components/Shared/Icon/IconName';
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
export default defineComponent({
components: { AppIcon },
directives: { NonCollapsing },
props: {
label: {
type: String,
default: undefined,
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
icon: {
type: String as PropType<IconName | undefined>,
default: undefined,
required: false,
},
},
emits: [
'click',
],
setup(props, { emit }) {
function onClicked() {
if (props.disabled) {
return;
}
emit('click');
}
return { onClicked };
},
});
</script>
<style lang="scss" scoped>
@use "@/presentation/assets/styles/main" as *;
.flat-button {
display: inline-flex;
gap: 0.5em;
font-family: $font-normal;
&.disabled {
@include flat-button($disabled: true);
}
&:not(.disabled) {
@include flat-button($disabled: false);
}
}
</style>

View File

@@ -6,25 +6,24 @@
<div class="dialog__content">
<slot />
</div>
<div
<FlatButton
icon="xmark"
class="dialog__close-button"
@click="hide"
>
<AppIcon icon="xmark" />
</div>
/>
</div>
</ModalContainer>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue';
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
import FlatButton from '@/presentation/components/Shared/FlatButton.vue';
import ModalContainer from './ModalContainer.vue';
export default defineComponent({
components: {
ModalContainer,
AppIcon,
FlatButton,
},
props: {
modelValue: {
@@ -72,16 +71,12 @@ export default defineComponent({
margin: 5%;
}
&__close-button {
.dialog__close-button {
color: $color-primary-dark;
width: auto;
font-size: 1.5em;
margin-right: 0.25em;
align-self: flex-start;
@include clickable;
@include hover-or-touch {
color: $color-primary;
}
}
}
</style>

View File

@@ -64,7 +64,6 @@ function hasDesktopVersion(os: OperatingSystem): boolean {
<style scoped lang="scss">
@use "@/presentation/assets/styles/main" as *;
.url {
@include clickable;
&__active {
font-size: 1em;
}

View File

@@ -32,8 +32,12 @@
</a>
</div>
<div class="footer__section__item">
<AppIcon class="icon" icon="user-secret" />
<a @click="showPrivacyDialog()">Privacy</a>
<FlatButton
label="Privacy"
icon="user-secret"
flat
@click="showPrivacyDialog()"
/>
</div>
</div>
</div>
@@ -50,6 +54,7 @@ import {
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
import { injectKey } from '@/presentation/injectionSymbols';
import FlatButton from '@/presentation/components/Shared/FlatButton.vue';
import DownloadUrlList from './DownloadUrlList.vue';
import PrivacyPolicy from './PrivacyPolicy.vue';
@@ -59,6 +64,7 @@ export default defineComponent({
PrivacyPolicy,
DownloadUrlList,
AppIcon,
FlatButton,
},
setup() {
const { info } = injectKey((keys) => keys.useApplication);
@@ -99,7 +105,6 @@ export default defineComponent({
.icon {
margin-right: 0.5em;
text-decoration: none;
}
.footer {