Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enh(a11y): Make privacy markup accessible #992

Merged
merged 12 commits into from
Nov 27, 2023
Prev Previous commit
Next Next commit
enh(a11y): Improve access and location accessibility
- Migrate to new @nextcloud/vue API

Signed-off-by: Christopher Ng <chrng8@gmail.com>
  • Loading branch information
Pytal authored and susnux committed Nov 27, 2023
commit b411e6f5c1cd3b66b4cafbd9bf227e8ac3bd49b7
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"@nextcloud/router": "^2.1.2",
"@nextcloud/vue": "^8.2.0",
"vue": "^2.7.14",
"vue-click-outside": "^1.1.0"
"vue-material-design-icons": "^5.2.0"
},
"browserslist": [
"extends @nextcloud/browserslist-config"
Expand Down
272 changes: 272 additions & 0 deletions src/components/AdminAccess.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
<!--
- @copyright 2023 Christopher Ng <chrng8@gmail.com>
-
- @author Christopher Ng <chrng8@gmail.com>
-
- @license AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->

<template>
<div class="admin">
<h3>{{ t('privacy', 'Administrators') }}</h3>

<NcLoadingIcon v-if="isLoading"
:name="t('privacy', 'Loading administrators …')"
:size="40" />

<div v-else class="admin__controls">
<ul class="admin__list">
<li v-for="admin in admins"
:key="admin.id"
class="admin__entry">
<span class="admin__user"
:class="{
'admin__user--external': !admin.internal,
}">
<NcAvatar :user="admin.internal ? admin.id : null"
:display-name="admin.displayname"
:size="44"
:is-no-user="!admin.internal"
:show-user-status="false" />

<span class="admin__displayname">{{ admin.displayname }}</span>
</span>

<NcButton v-if="!admin.internal"
type="tertiary"
:aria-label="t('privacy', 'Remove external {propertyName} admin', { propertyName: admin.displayname })"
:title="t('privacy', 'Remove external {propertyName} admin', { propertyName: admin.displayname })"
@click="deleteAdditionalAdmin(admin)">
<template #icon>
<Close :size="20" />
</template>
</NcButton>
</li>
</ul>

<div class="admin__add">
<NcButton v-if="isAdmin"
:aria-label="!isAdding ? t('privacy', 'Add external admin') : t('privacy', 'Cancel')"
@click="toggleAdd">
<template #icon>
<Plus v-if="!isAdding" :size="20" />
<Close v-else :size="20" />
</template>
{{ !isAdding ? t('privacy', 'Add') : t('privacy', 'Cancel') }}
</NcButton>

<form v-if="isAdding"
class="admin__form"
@submit.prevent="addAdditionalAdmin">
<NcTextField ref="addInput"
:value.sync="newAdmin"
:label="t('privacy', 'Name of external admin')"
maxlength="64"
autocorrect="off"
autocapitalize="off"
spellcheck="false" />
</form>
</div>
</div>
</div>
</template>

<script>
import Vue from 'vue'

import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import { showError } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'

import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'

import Close from 'vue-material-design-icons/Close.vue'
import Plus from 'vue-material-design-icons/Plus.vue'

export default {
name: 'AdminAccess',

components: {
Close,
NcAvatar,
NcButton,
NcLoadingIcon,
NcTextField,
Plus,
},

inject: [
't',
'isAdmin',
],

data() {
return {
admins: [],
newAdmin: '',
isLoading: true,
isAdding: false,
isSavingChanges: false,
}
},

/**
* This function is called on mount of the Vue component
* It sets the isAdmin property and
* loads additional admins from the server
*/
async mounted() {
const url = generateUrl('/apps/privacy/api/admins')
try {
const resp = await axios.get(url)
Vue.set(this, 'admins', resp.data)
} catch (error) {
console.error(error)
showError(t('privacy', 'Error loading additional administrator.'))
} finally {
this.isLoading = false
}
},

methods: {
toggleAdd() {
if (!this.isAdding) {
this.openNewAdmin()
return
}
this.closeNewAdmin()
},

/**
* Opens the new Admin dialog
*/
async openNewAdmin() {
this.isAdding = true
await this.$nextTick()
this.$refs.addInput?.focus()
},

/**
* Closes the new Admin dialog and resets the input field
*/
closeNewAdmin() {
this.isAdding = false
this.newAdmin = ''
},

/**
* Creates an additional (virtual) admin on the server
*
* @return {Promise<void>}
*/
async addAdditionalAdmin() {
const url = generateUrl('/apps/privacy/api/admins')
this.isSavingChanges = true

try {
const response = await axios.post(url, { name: this.newAdmin })
this.admins.push(response.data)
} catch (error) {
console.error(error)
showError(t('privacy', 'Error adding new administrator.'))
} finally {
this.isSavingChanges = false
this.isAdding = false
this.newAdmin = ''
}
},

/**
* Deletes an additional (virtual) admin from the server
*
* @param {object} admin The admin object from this.admins
* @return {Promise<void>}
*/
async deleteAdditionalAdmin(admin) {
const url = generateUrl('/apps/privacy/api/admins/{id}', { id: admin.id })

try {
await axios.delete(url)

const index = this.admins.indexOf(admin)
if (index !== -1) {
this.admins.splice(index, 1)
}
} catch (error) {
console.error(error)
showError(t('privacy', 'Error deleting new administrator.'))
}
},
},
}
</script>

<style lang="scss" scoped>
.admin {
&__controls {
display: flex;
flex-direction: column;
gap: 10px 0;
}

&__list {
display: grid;
grid-auto-flow: row;
grid-template-columns: repeat(auto-fit, 260px);
gap: 8px;
}

&__entry {
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px 0;
width: 260px;
}

&__user {
display: flex;
align-items: center;
gap: 0 10px;
width: 100%;

&--external {
width: calc(260px - 44px); // Entry - button
}
}

&__displayname {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

&__add {
display: flex;
flex-direction: column;
gap: 4px 0;
}

&__form {
max-width: 400px;
}
}
</style>
Loading