Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
11 KB
Referenced Files
None
Subscribers
None
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

Mime Type
text/x-diff
Expires
Fri, Oct 30, 4:50 AM (1 d, 1 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1799486
Default Alt Text
(11 KB)

Event Timeline