Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 36 additions & 0 deletions cypress/visual/NcPasswordField/NcPasswordField.story.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<template>
<div class="story-wrapper">
<NcPasswordField :as-text="asText"
label="Password"
model-value="password"
:visible="visible" />
</div>
</template>

<script setup>
import NcPasswordField from '../../../src/components/NcPasswordField/NcPasswordField.vue'

defineProps({
asText: {
type: Boolean,
default: undefined,
},
visible: {
type: Boolean,
default: undefined,
},
})
</script>

<style scoped>
.story-wrapper {
margin-inline: auto;
max-width: 300px;
padding-top: 8px;
}
</style>
87 changes: 87 additions & 0 deletions cypress/visual/NcPasswordField/NcPasswordField.visual.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import NcPasswordField from './NcPasswordField.story.vue'

describe('NcPasswordField', () => {
it('password is hidden', () => {
cy.mount(NcPasswordField)

cy.get('input[type="password"]').should('be.visible')
cy.get('.story-wrapper').compareSnapshot('NcPasswordField-password-hidden')
})

it('password can be shown', () => {
cy.mount(NcPasswordField, {
propsData: {
visible: true,
},
})

cy.get('input[type="text"]').should('be.visible')
cy.get('.story-wrapper').compareSnapshot('NcPasswordField-password-visible')
})

it('password can be shown by trailing button click', () => {
cy.mount(NcPasswordField)

cy.get('input[type="password"]')
.should('be.visible')

cy.get('[aria-label="Show password"]')
.should('be.visible')
.click()

cy.get('input[type="password"]')
.should('not.exist')
cy.get('input[type="text"]')
.should('be.visible')
cy.get('.story-wrapper').compareSnapshot('NcPasswordField-password-toggled-by-button-visible')
})

it('password as-text is hidden', () => {
cy.mount(NcPasswordField, {
propsData: {
asText: true,
},
})

cy.get('input[type="text"]').should('be.visible')
cy.get('.story-wrapper').compareSnapshot('NcPasswordField-password-as-text-hidden')
})

it('password as-text can be shown', () => {
cy.mount(NcPasswordField, {
propsData: {
asText: true,
visible: true,
},
})

cy.get('input[type="text"]').should('be.visible')
cy.get('.story-wrapper').compareSnapshot('NcPasswordField-password-as-text-visible')
})

it('password as-text can be shown by trailing button click', () => {
cy.mount(NcPasswordField, {
propsData: {
asText: true,
},
})

cy.get('input[type="text"]')
.should('be.visible')

cy.get('[aria-label="Show password"]')
.should('be.visible')
.click()

cy.get('input[type="text"]')
.should('be.visible')
cy.get('[aria-label="Hide password"]')
.should('be.visible')
cy.get('.story-wrapper').compareSnapshot('NcPasswordField-password-as-text-toggled-by-button-visible')
})
})
53 changes: 37 additions & 16 deletions src/components/NcPasswordField/NcPasswordField.vue
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,15 @@ export default {
<template>
<NcInputField v-bind="propsAndAttrsToForward"
ref="inputField"
:type="isPasswordHidden && !asText ? 'password' : 'text'"
:type="visibility || asText ? 'text' : 'password'"
:trailing-button-label="trailingButtonLabelPassword"
:helper-text="computedHelperText"
:error="computedError"
:success="computedSuccess"
:minlength="rules.minlength"
:input-class="{ 'password-field__input--secure-text': isPasswordHidden && asText }"
:input-class="{ 'password-field__input--secure-text': !visibility && asText }"
v-on="$listeners"
@trailing-button-click="togglePasswordVisibility"
@trailing-button-click="toggleVisibility"
@input="handleInput">
<template v-if="!!$scopedSlots.icon || !!$slots.default || !!$scopedSlots.default" #icon>
<!-- @slot Leading icon -->
Expand All @@ -105,8 +105,8 @@ export default {
</template>

<template #trailing-button-icon>
<Eye v-if="isPasswordHidden" :size="18" />
<EyeOff v-else :size="18" />
<IconEyeOff v-if="visibility" :size="18" />
<IconEye v-else :size="18" />
</template>
</NcInputField>
</template>
Expand All @@ -115,15 +115,16 @@ export default {
import { generateOcsUrl } from '@nextcloud/router'
import { loadState } from '@nextcloud/initial-state'
import axios from '@nextcloud/axios'
import { useVModel } from '@vueuse/core'
import debounce from 'debounce'

import Eye from 'vue-material-design-icons/Eye.vue'
import EyeOff from 'vue-material-design-icons/EyeOff.vue'
import IconEye from 'vue-material-design-icons/Eye.vue'
import IconEyeOff from 'vue-material-design-icons/EyeOff.vue'
import NcInputField from '../NcInputField/NcInputField.vue'

import { useModelMigration } from '../../composables/useModelMigration.ts'
import { logger } from '../../utils/logger.ts'
import { t } from '../../l10n.js'
import { useModelMigration } from '../../composables/useModelMigration.ts'
import NcInputField from '../NcInputField/NcInputField.vue'

/**
* @typedef PasswordPolicy
Expand All @@ -147,8 +148,8 @@ export default {

components: {
NcInputField,
Eye,
EyeOff,
IconEye,
IconEyeOff,
},

// Allow forwarding all attributes
Expand Down Expand Up @@ -227,6 +228,15 @@ export default {
type: Boolean,
default: false,
},

/**
* Visibility of the password.
* If this is set to `true` then the password will be shown to the user (input type will be set to `text`).
*/
visible: {
type: Boolean,
default: false,
},
},

emits: [
Expand All @@ -246,18 +256,27 @@ export default {
'update:modelValue',
/** Same as update:modelValue for Vue 2 compatibility */
'update:model-value',
/**
* Updated visibility of the password
* @property {boolean} visible the new visibility state
*/
'update:visible',
],

setup() {
setup(props, { emit }) {
const model = useModelMigration('value', 'update:value')
const visibility = useVModel(props, 'visible', emit, { passive: true })

return {
t,

model,
visibility,
}
},

data() {
return {
isPasswordHidden: true,
internalHelpMessage: '',
isValid: null,
}
Expand Down Expand Up @@ -285,7 +304,7 @@ export default {
},

trailingButtonLabelPassword() {
return this.isPasswordHidden ? t('Show password') : t('Hide password')
return this.visibility ? t('Hide password') : t('Show password')
},

propsAndAttrsToForward() {
Expand Down Expand Up @@ -333,9 +352,11 @@ export default {
handleInput(event) {
this.model = event.target.value
},
togglePasswordVisibility() {
this.isPasswordHidden = !this.isPasswordHidden

toggleVisibility() {
this.visibility = !this.visibility
},

checkPassword: debounce(async function(password) {
try {
const { data } = await axios.post(generateOcsUrl('apps/password_policy/api/v1/validate'), { password })
Expand Down
Loading