Skip to content

Commit

Permalink
feat: add NeTextInput component (#39)
Browse files Browse the repository at this point in the history
  • Loading branch information
andre8244 authored Apr 12, 2024
1 parent 7ac3d63 commit e2afb02
Show file tree
Hide file tree
Showing 3 changed files with 343 additions and 0 deletions.
210 changes: 210 additions & 0 deletions src/components/NeTextInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
<!--
Copyright (C) 2024 Nethesis S.r.l.
SPDX-License-Identifier: GPL-3.0-or-later
-->

<script setup lang="ts">
import { computed, ref, useAttrs } from 'vue'
import { v4 as uuidv4 } from 'uuid'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleExclamation as fasCircleExclamation } from '@fortawesome/free-solid-svg-icons'
import { faEye as fasEye } from '@fortawesome/free-solid-svg-icons'
import { faEyeSlash as fasEyeSlash } from '@fortawesome/free-solid-svg-icons'

const props = defineProps({
label: {
type: String,
default: ''
},
modelValue: {
type: String,
default: ''
},
id: {
type: String,
default: ''
},
placeholder: {
type: String,
default: ''
},
helperText: {
type: String,
default: ''
},
invalidMessage: {
type: String,
default: ''
},
optional: {
type: Boolean
},
isPassword: {
type: Boolean,
default: false
},
showPasswordLabel: {
type: String,
default: 'Show password'
},
hidePasswordLabel: {
type: String,
default: 'Hide password'
},
optionalLabel: {
type: String,
default: 'Optional'
},
disabled: {
type: Boolean,
default: false
}
})

const emit = defineEmits(['update:modelValue'])

// expose focus function
defineExpose({
focus
})

defineOptions({
inheritAttrs: false
})

// add fontawesome icons
library.add(fasCircleExclamation)
library.add(fasEye)
library.add(fasEyeSlash)

const inputBaseStyle =
'block w-full rounded-md border-0 py-1.5 ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 mt-0 disabled:cursor-not-allowed disabled:opacity-50 text-gray-900 bg-white placeholder:text-gray-400 transition-colors duration-200 dark:text-gray-50 dark:bg-gray-950 dark:placeholder:text-gray-500'
const inputValidStyle =
'ring-gray-300 focus:ring-primary-500 dark:ring-gray-600 dark:focus:ring-primary-300'
const inputInvalidStyle =
'pr-10 ring-rose-700 focus:ring-rose-500 dark:ring-rose-500 dark:focus:ring-rose-500'

const descriptionBaseStyle = 'mt-2 text-sm'

let isPasswordVisible = ref(false)

const componentId = computed(() => (props.id ? props.id : uuidv4()))

const inputStyles = computed(() =>
[inputBaseStyle, props.invalidMessage ? inputInvalidStyle : inputValidStyle].join(' ')
)

const inputRef = ref()

const attrs = useAttrs()

const rootAttrs = computed(() => {
const allowed = ['class']
return Object.keys(attrs)
.filter((key) => allowed.includes(key))
.reduce((obj: any, key) => {
obj[key] = attrs[key]
return obj
}, {})
})

const inputAttrs = computed(() => {
const notAllowed = ['class']
return Object.keys(attrs)
.filter((key) => !notAllowed.includes(key))
.reduce((obj: any, key) => {
obj[key] = attrs[key]
return obj
}, {})
})

function emitModelValue(ev: any) {
emit('update:modelValue', ev.target.value)
}

function togglePasswordVisible() {
isPasswordVisible.value = !isPasswordVisible.value
}

function focus() {
inputRef.value.focus()
}
</script>

<template>
<div v-bind="rootAttrs">
<label
v-if="label"
:for="componentId"
class="mb-2 flex items-end justify-between text-sm font-medium leading-6 text-gray-700 dark:text-gray-200"
>
<span>
{{ label }}
<span v-if="$slots.tooltip" class="ml-1">
<slot name="tooltip"></slot>
</span>
</span>
<span v-if="optional" class="ml-2 font-normal">{{ optionalLabel }}</span>
</label>
<div class="relative rounded-md shadow-sm">
<input
:id="componentId"
ref="inputRef"
:type="isPassword && !isPasswordVisible ? 'password' : 'text'"
:value="modelValue"
:class="inputStyles"
:placeholder="placeholder"
:aria-describedby="componentId + '-description'"
v-bind="inputAttrs"
:disabled="disabled"
@input="($event) => emitModelValue($event)"
/>
<!-- show/hide password toggle -->
<div v-if="isPassword" class="absolute inset-y-0 right-0 flex items-center">
<button
type="button"
:disabled="disabled"
:class="['px-3', disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer']"
@click="togglePasswordVisible"
>
<span class="sr-only">{{
isPasswordVisible ? hidePasswordLabel : showPasswordLabel
}}</span>
<font-awesome-icon
:icon="['fas', isPasswordVisible ? 'eye-slash' : 'eye']"
class="h-4 w-4 text-gray-500 dark:text-gray-400"
aria-hidden="true"
/>
</button>
</div>
<!-- invalid icon -->
<div
v-else-if="invalidMessage"
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3"
>
<font-awesome-icon
:icon="['fas', 'circle-exclamation']"
class="h-4 w-4 text-rose-700 dark:text-rose-400"
aria-hidden="true"
/>
</div>
</div>
<!-- invalid message -->
<p
v-if="invalidMessage"
:id="componentId + '-description'"
:class="[descriptionBaseStyle, 'text-rose-700 dark:text-rose-400']"
>
{{ invalidMessage }}
</p>
<!-- helper text -->
<p
v-else-if="helperText"
:id="componentId + '-description'"
:class="[descriptionBaseStyle, 'text-gray-500 dark:text-gray-400']"
>
{{ helperText }}
</p>
</div>
</template>
1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export { default as NePaginator } from '@/components/NePaginator.vue'
export { default as NeEmptyState } from '@/components/NeEmptyState.vue'
export { default as NeTabs } from '@/components/NeTabs.vue'
export { default as NeTextArea } from '@/components/NeTextArea.vue'
export { default as NeTextInput } from '@/components/NeTextInput.vue'

// types export
export type { NeComboboxOption } from '@/components/NeCombobox.vue'
Expand Down
132 changes: 132 additions & 0 deletions stories/NeTextInput.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright (C) 2024 Nethesis S.r.l.
// SPDX-License-Identifier: GPL-3.0-or-later

import type { Meta, StoryObj } from '@storybook/vue3'

import { NeTextInput, NeTooltip } from '../src/main'

const meta = {
title: 'Control/NeTextInput',
component: NeTextInput,
// default values
args: {
label: 'Label',
modelValue: '',
placeholder: 'Placeholder',
helperText: '',
invalidMessage: '',
optional: false,
disabled: false,
id: '',
isPassword: false,
showPasswordLabel: 'Show password',
hidePasswordLabel: 'Hide password',
optionalLabel: 'Optional'
}
} satisfies Meta<typeof NeTextInput>

export default meta
type Story = StoryObj<typeof meta>

const template = '<NeTextInput v-bind="args" class="max-w-md" />'

export const Default: Story = {
render: (args) => ({
components: { NeTextInput },
setup() {
return { args }
},
template: template
}),
args: {}
}

export const HelperText: Story = {
render: (args) => ({
components: { NeTextInput },
setup() {
return { args }
},
template: template
}),
args: { helperText: 'Helper text' }
}

export const Invalid: Story = {
render: (args) => ({
components: { NeTextInput },
setup() {
return { args }
},
template: template
}),
args: {
invalidMessage: 'Invalid input value'
}
}

export const Optional: Story = {
render: (args) => ({
components: { NeTextInput },
setup() {
return { args }
},
template: template
}),
args: { optional: true }
}

export const Disabled: Story = {
render: (args) => ({
components: { NeTextInput },
setup() {
return { args }
},
template: template
}),
args: { disabled: true }
}

export const Password: Story = {
render: (args) => ({
components: { NeTextInput },
setup() {
return { args }
},
template: template
}),
args: { isPassword: true, label: 'Enter password', placeholder: '' }
}

const typeNumberTemplate = '<NeTextInput v-bind="args" type="number" class="max-w-md" />'

export const TypeNumber: Story = {
render: (args) => ({
components: { NeTextInput },
setup() {
return { args }
},
template: typeNumberTemplate
}),
args: {}
}

const templateWithTooltip =
'<NeTextInput v-bind="args" class="max-w-md">\
<template #tooltip>\
<NeTooltip>\
<template #content>Tooltip</template>\
</NeTooltip>\
</template>\
</NeTextInput>'

export const WithTooltip: Story = {
render: (args) => ({
components: { NeTextInput, NeTooltip },
setup() {
return { args }
},
template: templateWithTooltip
}),
args: {}
}

0 comments on commit e2afb02

Please sign in to comment.