Skip to content

Commit d4fbbd4

Browse files
committed
feat(NcPasswordField): allow to set obfuscation state
Signed-off-by: Ferdinand Thiessen <[email protected]>
1 parent c3bb9f2 commit d4fbbd4

9 files changed

+149
-28
lines changed

β€Žsrc/components/NcPasswordField/NcPasswordField.vueβ€Ž

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -93,40 +93,17 @@ export default {
9393
```
9494
</docs>
9595

96-
<template>
97-
<NcInputField v-bind="propsToForward"
98-
ref="inputField"
99-
v-model="modelValue"
100-
:error="error || isValid === false"
101-
:helper-text="helperText || internalHelpMessage"
102-
:input-class="[inputClass, { 'password-field__input--secure-text': isPasswordHidden && asText }]"
103-
:minlength="minlength ?? passwordPolicy?.minLength ?? 0"
104-
:success="success || isValid === true"
105-
:trailing-button-label="isPasswordHidden ? t('Show password') : t('Hide password')"
106-
:type="isPasswordHidden && !asText ? 'password' : 'text'"
107-
@trailing-button-click="isPasswordHidden = !isPasswordHidden">
108-
<template v-if="!!$slots.icon" #icon>
109-
<!-- @slot Leading icon -->
110-
<slot name="icon" />
111-
</template>
112-
<template #trailing-button-icon>
113-
<Eye v-if="isPasswordHidden" :size="18" />
114-
<EyeOff v-else :size="18" />
115-
</template>
116-
</NcInputField>
117-
</template>
118-
11996
<script setup lang="ts">
12097
import type { NcInputFieldProps } from '../NcInputField/NcInputField.vue'
12198
import type { Writable } from '../../utils/VueTypes.ts'
12299
123-
import debounce from 'debounce'
100+
import { mdiEye, mdiEyeOff } from '@mdi/js'
124101
import axios from '@nextcloud/axios'
125102
import { getCapabilities } from '@nextcloud/capabilities'
126103
import { generateOcsUrl } from '@nextcloud/router'
104+
import debounce from 'debounce'
127105
import { computed, ref, useTemplateRef, watch } from 'vue'
128-
import Eye from 'vue-material-design-icons/Eye.vue'
129-
import EyeOff from 'vue-material-design-icons/EyeOff.vue'
106+
import NcIconSvgWrapper from '../NcIconSvgWrapper/NcIconSvgWrapper.vue'
130107
import NcInputField from '../NcInputField/NcInputField.vue'
131108
import { t } from '../../l10n.js'
132109
import logger from '../../utils/logger.ts'
@@ -162,9 +139,14 @@ const props = withDefaults(defineProps<Omit<NcInputFieldProps, 'trailingButtonLa
162139
showTrailingButton: true,
163140
})
164141
165-
const modelValue = defineModel<string>('modelValue', { default: '' })
142+
const modelValue = defineModel<string>({ default: '' })
166143
watch(modelValue, debounce(checkPassword, 500))
167144
145+
/**
146+
* The obfuscating state of the password.
147+
*/
148+
const obfuscated = defineModel<boolean>('obfuscated', { default: true })
149+
168150
const emit = defineEmits<{
169151
valid: []
170152
invalid: []
@@ -224,7 +206,6 @@ const { password_policy: passwordPolicy } = getCapabilities() as { password_poli
224206
// internal state
225207
const inputField = useTemplateRef('inputField')
226208
227-
const isPasswordHidden = ref(true)
228209
const internalHelpMessage = ref('')
229210
const isValid = ref<boolean>()
230211
@@ -276,6 +257,13 @@ async function checkPassword() {
276257
}
277258
}
278259
260+
/**
261+
* Toggle the obfuscation of the password
262+
*/
263+
function toggleObfuscation() {
264+
obfuscated.value = !obfuscated.value
265+
}
266+
279267
/**
280268
* Focus the input element
281269
*
@@ -296,6 +284,28 @@ function select() {
296284
}
297285
</script>
298286

287+
<template>
288+
<NcInputField v-bind="propsToForward"
289+
ref="inputField"
290+
v-model="modelValue"
291+
:error="error || isValid === false"
292+
:helper-text="helperText || internalHelpMessage"
293+
:input-class="[inputClass, { 'password-field__input--secure-text': obfuscated && asText }]"
294+
:minlength="minlength ?? passwordPolicy?.minLength ?? 0"
295+
:success="success || isValid === true"
296+
:trailing-button-label="obfuscated ? t('Show password') : t('Hide password')"
297+
:type="obfuscated && !asText ? 'password' : 'text'"
298+
@trailing-button-click="toggleObfuscation">
299+
<template v-if="!!$slots.icon" #icon>
300+
<!-- @slot Leading icon -->
301+
<slot name="icon" />
302+
</template>
303+
<template #trailing-button-icon>
304+
<NcIconSvgWrapper :path="obfuscated ? mdiEye : mdiEyeOff" />
305+
</template>
306+
</NcInputField>
307+
</template>
308+
299309
<style lang="scss" scoped>
300310
:deep(.password-field__input--secure-text) {
301311
// Emulate password field look
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<template>
7+
<div class="story-wrapper">
8+
<NcPasswordField :as-text
9+
label="Password"
10+
model-value="password"
11+
:obfuscated />
12+
</div>
13+
</template>
14+
15+
<script setup lang="ts">
16+
import NcPasswordField from '../../../../src/components/NcPasswordField/NcPasswordField.vue'
17+
18+
withDefaults(defineProps<{
19+
asText?: boolean
20+
obfuscated?: boolean
21+
}>(), {
22+
asText: undefined,
23+
obfuscated: undefined,
24+
})
25+
</script>
26+
27+
<style scoped>
28+
.story-wrapper {
29+
margin-inline: auto;
30+
max-width: 300px;
31+
padding-top: 8px;
32+
}
33+
</style>
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { expect, test } from '@playwright/experimental-ct-vue'
7+
import NcPasswordField from './NcPassword.story.vue'
8+
9+
test.skip(({ browserName }) => browserName !== 'chromium')
10+
11+
test.describe('NcPasswordField', () => {
12+
test('password is hidden', { tag: '@visual' }, async ({ mount, page }) => {
13+
await mount(NcPasswordField)
14+
15+
await expect(page.getByRole('textbox', { name: 'Password' })).toBeVisible()
16+
await expect(page.locator('.story-wrapper')).toHaveScreenshot()
17+
})
18+
19+
test('password can be shown', { tag: '@visual' }, async ({ mount, page }) => {
20+
await mount(NcPasswordField, {
21+
props: {
22+
obfuscated: false,
23+
},
24+
})
25+
26+
await expect(page.getByRole('textbox', { name: 'Password' })).toBeVisible()
27+
await expect(page.locator('.story-wrapper')).toHaveScreenshot()
28+
})
29+
30+
test('password can be shown by trailing button click', { tag: '@visual' }, async ({ mount, page }) => {
31+
await mount(NcPasswordField)
32+
33+
const el = page.getByRole('textbox', { name: 'Password' })
34+
await expect(el).toBeVisible()
35+
await expect(el).toHaveAttribute('type', 'password')
36+
37+
await page.getByRole('button', { name: 'Show password' }).click()
38+
await expect(el).toHaveAttribute('type', 'text')
39+
await expect(page.locator('.story-wrapper')).toHaveScreenshot()
40+
})
41+
42+
test('password as-text is hidden', { tag: '@visual' }, async ({ mount, page }) => {
43+
await mount(NcPasswordField, {
44+
props: {
45+
asText: true,
46+
},
47+
})
48+
49+
await expect(page.getByRole('textbox', { name: 'Password' })).toBeVisible()
50+
await expect(page.locator('.story-wrapper')).toHaveScreenshot()
51+
})
52+
53+
test('password as-text can be shown', { tag: '@visual' }, async ({ mount, page }) => {
54+
await mount(NcPasswordField, {
55+
props: {
56+
asText: true,
57+
obfuscated: false,
58+
},
59+
})
60+
61+
await expect(page.getByRole('textbox', { name: 'Password' })).toBeVisible()
62+
await expect(page.locator('.story-wrapper')).toHaveScreenshot()
63+
})
64+
65+
test('password as-text can be shown by trailing button click', { tag: '@visual' }, async ({ mount, page }) => {
66+
await mount(NcPasswordField, {
67+
props: {
68+
asText: true,
69+
},
70+
})
71+
72+
const el = page.getByRole('textbox', { name: 'Password' })
73+
await expect(el).toBeVisible()
74+
75+
await page.getByRole('button', { name: 'Show password' }).click()
76+
await expect(page.locator('.story-wrapper')).toHaveScreenshot()
77+
})
78+
})
Loading
Loading
Loading
Loading

0 commit comments

Comments
Β (0)