Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 2x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 2x 2x 2x 1x 2x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 1045x 2x 2x 2x 2x 2x 1x 1x 1x 1x 1x | <script setup lang="ts">
/**
* FzInput Component
*
* Flexible input component with icon support, validation states, and multiple variants.
* Supports left/right icons (static or clickable buttons), floating label variant,
* error/valid states, and full accessibility features.
*
* @component
* @example
* <FzInput label="Email" type="email" v-model="email" />
* <FzInput label="Password" type="password" rightIcon="eye" @fzinput:right-icon-click="toggleVisibility" />
*/
import { computed, toRefs, Ref, ref } from "vue";
import { FzInputProps } from "./types";
import { FzIcon } from "@fiscozen/icons";
import { FzIconButton } from "@fiscozen/button";
import useInputStyle from "./useInputStyle";
const props = withDefaults(defineProps<FzInputProps>(), {
size: "md",
error: false,
type: "text",
rightIconButtonVariant: "invisible",
variant: "normal",
});
const model = defineModel<string>();
const containerRef: Ref<HTMLElement | null> = ref(null);
const inputRef: Ref<HTMLInputElement | null> = ref(null);
const uniqueId = `fz-input-${Math.random().toString(36).slice(2, 9)}`;
const {
staticContainerClass,
computedContainerClass,
computedLabelClass,
staticInputClass,
computedInputClass,
computedHelpClass,
computedErrorClass,
containerWidth,
showNormalPlaceholder,
} = useInputStyle(toRefs(props), containerRef, model);
/**
* Maps input size to icon button size
*
* Icon buttons use a smaller size scale than inputs to maintain visual balance.
*/
const sizeMap: Record<"sm" | "md" | "lg", "xs" | "sm" | "md" | "lg"> = {
sm: "xs",
md: "sm",
lg: "md",
};
const mappedSize = computed<"xs" | "sm" | "md" | "lg">(() => {
return sizeMap[props.size];
});
const slots = defineSlots<{
label?: () => unknown;
"left-icon"?: () => unknown;
"right-icon"?: () => unknown;
errorMessage?: () => unknown;
helpText?: () => unknown;
}>();
/**
* Computes aria-describedby value linking input to help text or error message
*
* Creates space-separated list of IDs for screen readers to announce contextual information.
*/
const ariaDescribedBy = computed(() => {
const ids: string[] = [];
if (props.error && slots.errorMessage) {
ids.push(`${uniqueId}-error`);
}
if (!props.error && slots.helpText) {
ids.push(`${uniqueId}-help`);
}
return ids.length > 0 ? ids.join(" ") : undefined;
});
const emit = defineEmits([
"input",
"focus",
"paste",
"blur",
"fzinput:left-icon-click",
"fzinput:right-icon-click",
]);
defineExpose({
inputRef,
containerRef,
});
</script>
<template>
<div class="fz-input w-full flex flex-col gap-8">
<slot name="label">
<label
v-if="label"
:id="`${uniqueId}-label`"
:class="['text-sm', computedLabelClass]"
:for="uniqueId"
>
{{ label }}{{ required ? " *" : "" }}
</label>
</slot>
<div
:class="[staticContainerClass, computedContainerClass]"
ref="containerRef"
@click="inputRef?.focus()"
>
<slot name="left-icon">
<FzIcon
v-if="leftIcon"
:name="leftIcon"
:size="size"
:variant="leftIconVariant"
@click.stop="emit('fzinput:left-icon-click')"
:class="leftIconClass"
/>
</slot>
<div class="flex flex-col space-around min-w-0 grow">
<span
v-if="!showNormalPlaceholder"
class="text-xs text-gray-300 grow-0 overflow-hidden text-ellipsis whitespace-nowrap"
>{{ placeholder }}</span
>
<input
:type="type"
:required="required ? required : false"
:disabled="disabled"
:readonly="readonly"
:placeholder="showNormalPlaceholder ? placeholder : ''"
v-model="model"
:id="uniqueId"
ref="inputRef"
:class="[staticInputClass, computedInputClass]"
:pattern="pattern"
:name
:maxlength
:aria-required="required ? 'true' : 'false'"
:aria-invalid="error ? 'true' : 'false'"
:aria-disabled="disabled ? 'true' : 'false'"
:aria-labelledby="label ? `${uniqueId}-label` : undefined"
:aria-describedby="ariaDescribedBy"
@blur="(e) => $emit('blur', e)"
@focus="(e) => $emit('focus', e)"
@paste="(e) => $emit('paste', e)"
/>
</div>
<slot name="right-icon">
<FzIcon
v-if="valid"
name="check"
:size="size"
class="text-semantic-success"
/>
<FzIcon
v-if="rightIcon && !rightIconButton"
:name="rightIcon"
:size="size"
:variant="rightIconVariant"
@click.stop="emit('fzinput:right-icon-click')"
:class="rightIconClass"
/>
<FzIconButton
v-if="rightIcon && rightIconButton"
:iconName="rightIcon"
:size="mappedSize"
:iconVariant="rightIconVariant"
:variant="disabled ? 'invisible' : rightIconButtonVariant"
@click.stop="emit('fzinput:right-icon-click')"
:class="[{ 'bg-grey-100 !text-gray-300': disabled }, rightIconClass]"
/>
</slot>
</div>
<div
v-if="error && $slots.errorMessage"
:id="`${uniqueId}-error`"
role="alert"
class="flex gap-4"
:style="{ width: containerWidth }"
>
<FzIcon
name="triangle-exclamation"
class="text-semantic-error"
:size="size"
aria-hidden="true"
/>
<div :class="['mt-1', computedErrorClass]">
<slot name="errorMessage"></slot>
</div>
</div>
<span
v-else-if="$slots.helpText"
:id="`${uniqueId}-help`"
:class="[computedHelpClass]"
:style="{ width: containerWidth }"
>
<slot name="helpText"></slot>
</span>
</div>
</template>
<style scoped></style>
|