Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85826048
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
11 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/components/dialog_modal/dialog_modal.js b/src/components/dialog_modal/dialog_modal.js
index 083f7a21da..e513a04104 100644
--- a/src/components/dialog_modal/dialog_modal.js
+++ b/src/components/dialog_modal/dialog_modal.js
@@ -1,23 +1,30 @@
+import { useId } from 'vue'
+
import { useMergedConfigStore } from 'src/stores/merged_config.js'
const DialogModal = {
props: {
darkOverlay: {
default: true,
type: Boolean,
},
onCancel: {
default: () => {
/* no-op */
},
type: Function,
},
},
+ setup() {
+ const uniqueId = useId()
+
+ return { uniqueId }
+ },
computed: {
mobileCenter() {
return useMergedConfigStore().mergedConfig.modalMobileCenter
},
},
}
export default DialogModal
diff --git a/src/components/dialog_modal/dialog_modal.vue b/src/components/dialog_modal/dialog_modal.vue
index b23af60afa..e00bb74e3d 100644
--- a/src/components/dialog_modal/dialog_modal.vue
+++ b/src/components/dialog_modal/dialog_modal.vue
@@ -1,119 +1,125 @@
<template>
<span
class="dialog-container"
:class="{ 'dark-overlay': darkOverlay, '-center-mobile': mobileCenter }"
@click.self.stop="onCancel()"
>
<div
class="dialog-modal panel panel-default"
+ role="dialog"
+ aria-modal="true"
+ :aria-labelledby="uniqueId + 'header'"
@click.stop=""
>
<div class="panel-heading dialog-modal-heading">
- <h1 class="title">
+ <h1
+ :id="uniqueId + 'header'"
+ class="title"
+ >
<slot name="header" />
</h1>
</div>
<div class="panel-body dialog-modal-content">
<slot name="default" />
</div>
<div class="dialog-modal-footer user-interactions panel-footer">
<slot name="footer" />
</div>
</div>
</span>
</template>
<script src="./dialog_modal.js"></script>
<style lang="scss">
// TODO: unify with other modals.
.dark-overlay {
&::before {
inset: 0;
content: " ";
display: block;
cursor: default;
position: fixed;
background: rgb(27 31 35 / 50%);
z-index: 2000;
}
}
.dialog-container {
display: grid;
position: fixed;
inset: 0;
justify-content: center;
place-items: center center;
overflow: auto;
}
.dialog-modal.panel {
max-width: 90vw;
z-index: 2001;
cursor: default;
display: block;
.dialog-modal-heading {
.title {
text-align: center;
}
}
.dialog-modal-content {
margin: 0;
padding: 1rem;
white-space: normal;
text-align: center;
}
.dialog-modal-footer {
margin: 0;
padding: 0.5em;
border-top: 1px solid var(--border);
display: grid;
justify-content: end;
gap: 0.5em;
grid-template-columns: min-content;
grid-auto-columns: min-content;
grid-auto-flow: column dense;
height: auto;
button {
width: auto;
white-space: nowrap;
padding-left: 2em;
padding-right: 2em;
}
}
}
#modal.-mobile {
.dialog-container {
justify-content: stretch;
place-items: end stretch;
&.-center-mobile {
align-items: center;
}
}
.dialog-modal.panel {
min-width: 100vw;
}
.dialog-modal-footer {
flex-direction: column;
justify-content: flex-end;
grid-template-columns: 1fr;
grid-auto-rows: auto;
grid-auto-flow: row dense;
button {
grid-column: 1;
height: 3em;
}
}
}
</style>
diff --git a/src/components/modal/modal.vue b/src/components/modal/modal.vue
index 96642142ca..42fc184d07 100644
--- a/src/components/modal/modal.vue
+++ b/src/components/modal/modal.vue
@@ -1,79 +1,80 @@
<template>
<div
role="dialog"
+ aria-modal="true"
v-show="isOpen"
v-body-scroll-lock="isOpen && !noBackground"
class="modal-view"
:class="classes"
@click.self="$emit('backdropClicked')"
v-bind="$attrs"
>
<slot />
</div>
</template>
<script>
export default {
provide: {
popoversZLayer: 'modals',
},
props: {
isOpen: {
type: Boolean,
default: true,
},
noBackground: {
type: Boolean,
default: false,
},
},
emits: ['backdropClicked'],
computed: {
classes() {
return {
'modal-background': !this.noBackground,
open: this.isOpen,
}
},
},
}
</script>
<style lang="scss">
.modal-view {
z-index: var(--ZI_modals);
position: fixed;
inset: 0;
display: flex;
justify-content: center;
align-items: center;
overflow: auto;
pointer-events: none;
animation-duration: 0.2s;
animation-name: modal-background-fadein;
opacity: 0;
> * {
pointer-events: initial;
}
&.modal-background {
pointer-events: initial;
background-color: rgb(0 0 0 / 50%);
}
&.open {
opacity: 1;
}
}
@keyframes modal-background-fadein {
from {
background-color: rgb(0 0 0 / 0%);
}
to {
background-color: rgb(0 0 0 / 50%);
}
}
</style>
diff --git a/src/components/settings_modal/helpers/vertical_tab_switcher.jsx b/src/components/settings_modal/helpers/vertical_tab_switcher.jsx
index abfd333434..a2599eef38 100644
--- a/src/components/settings_modal/helpers/vertical_tab_switcher.jsx
+++ b/src/components/settings_modal/helpers/vertical_tab_switcher.jsx
@@ -1,237 +1,237 @@
import { mapState } from 'pinia'
import { Fragment, useId } from 'vue'
import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome'
import './vertical_tab_switcher.scss'
import { useInterfaceStore } from 'src/stores/interface.js'
const findFirstUsable = (slots) => slots.findIndex((_) => _.props)
export default {
name: 'VerticalTabSwitcher',
props: {
renderOnlyFocused: {
required: false,
type: Boolean,
default: false,
},
onSwitch: {
required: false,
type: Function,
default: undefined,
},
activeTab: {
required: false,
type: String,
default: undefined,
},
bodyScrollLock: {
required: false,
type: Boolean,
default: false,
},
},
setup() {
const uniqueId = useId()
return {
uniqueId,
}
},
data() {
return {
active: findFirstUsable(this.slots()),
resizeHandler: null,
navSide: 'tabs',
}
},
computed: {
activeIndex() {
// In case of controlled component
if (this.activeTab) {
return this.slots().findIndex(
(slot) => slot?.props && this.activeTab === slot.props.key,
)
} else {
return this.active
}
},
idPrefix() {
return `tab-switcher-${this.uniqueId}-`
},
isActive() {
return (tabName) => {
const isWanted = (slot) =>
slot.props && slot.props['data-tab-name'] === tabName
return this.$slots.default().findIndex(isWanted) === this.activeIndex
}
},
...mapState(useInterfaceStore, {
mobileLayout: (store) => store.layoutType === 'mobile',
}),
},
beforeUpdate() {
const currentSlot = this.slots()[this.active]
if (!currentSlot.props) {
this.active = findFirstUsable(this.slots())
}
},
methods: {
clickTab(index) {
return (e) => {
e.preventDefault()
this.setTab(index)
}
},
setTab(index) {
if (typeof this.onSwitch === 'function') {
this.onSwitch.call(null, this.slots()[index].key)
}
this.active = index
this.changeNavSide('content')
},
changeNavSide(side) {
if (this.navSide !== side) {
this.navSide = side
}
},
// DO NOT put it to computed, it doesn't work (caching?)
slots() {
if (this.$slots.default()[0].type === Fragment) {
return this.$slots.default()[0].children
}
return this.$slots.default()
},
},
render() {
const tabs = this.slots().map((slot, index) => {
const props = slot.props
if (!props) return
const classesTab = ['vertical-tab', 'menu-item']
if (
this.activeIndex === index &&
useInterfaceStore().layoutType !== 'mobile'
) {
classesTab.push('-active')
}
return (
<button
disabled={props.disabled}
onClick={this.clickTab(index)}
class={classesTab.join(' ')}
type="button"
role="tab"
id={`${this.idPrefix}tab-${index}`}
aria-selected={this.activeIndex === index}
aria-controls={`${this.idPrefix}tabpanel-${index}`}
title={props.label}
>
{!props.icon ? (
''
) : (
<FAIcon class="tab-icon" size="1x" fixed-width icon={props.icon} />
)}
<span class="text">{props.label}</span>
</button>
)
})
const contents = this.slots().map((slot, index) => {
const props = slot.props
if (!props) return
const active = this.activeIndex === index
let delayRender = slot.props['delay-render']
if (delayRender && active) {
slot.props['delay-render'] = false
delayRender = false
}
const renderSlot =
!delayRender && (!this.renderOnlyFocused || active) ? slot : ''
const headerClasses = ['tab-content-label']
const header = (
<h2 class={headerClasses} aria-label={props.label}>
<button
type="button"
onClick={() => this.changeNavSide('tabs')}
title={this.$t('nav.back')}
class="button-unstyled"
>
<FAIcon size="lg" class="back-button-icon" icon="chevron-left" />
</button>
{props.label}
</h2>
)
const wrapperClasses = [
'tab-content-wrapper',
active ? '-active' : '-hidden',
]
const slotWrapperClasses = [
'tab-slot-wrapper',
active ? '-active' : '-hidden',
]
const contentClasses = ['tab-content']
if (props['full-width'] || props['full-width'] != null) {
contentClasses.push('-full-width')
wrapperClasses.push('-full-width')
slotWrapperClasses.push('-full-width')
}
if (props['full-height'] || props['full-width'] != null) {
contentClasses.push('-full-height')
wrapperClasses.push('-full-height')
slotWrapperClasses.push('-full-height')
}
return (
<div
class={wrapperClasses}
- role="presentation"
id={`${this.idPrefix}tabpanel-${index}`}
+ aria-label={props.label}
+ role="tabpanel"
>
<div class="tab-mobile-header" role="presentation">
{header}
</div>
<div
class={slotWrapperClasses}
- role="tabpanel"
- aria-label={props.label}
+ role="presentation"
>
<div class={contentClasses} role="presentation">
{renderSlot}
</div>
</div>
</div>
)
})
const rootClasses = ['vertical-tab-switcher']
if (useInterfaceStore().layoutType === 'mobile') {
rootClasses.push('-mobile')
}
if (this.navSide === 'tabs') {
rootClasses.push('-nav-tabs')
} else {
rootClasses.push('-nav-contents')
}
return (
<div ref="root" class={rootClasses.join(' ')}>
<div class="tabs" role="tablist" ref="nav">
{tabs}
</div>
<div
role="presentation"
class="contents"
v-body-scroll-lock={this.bodyScrollLock}
ref="contents"
>
{contents}
</div>
</div>
)
},
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Oct 30, 4:50 AM (1 d, 3 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1799486
Default Alt Text
(11 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline