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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -128,7 +128,7 @@ $base-spacing: $text-size;
|
||||
*/
|
||||
@include hover-or-touch {
|
||||
&::after{
|
||||
background-color: $globals-color-hover;
|
||||
background-color: $color-highlight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
73
src/presentation/components/Shared/FlatButton.vue
Normal file
73
src/presentation/components/Shared/FlatButton.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user