Skip to content

Commit

Permalink
feat(field): programmatically associate inputs with field messages
Browse files Browse the repository at this point in the history
  • Loading branch information
mlmoravek committed Jul 12, 2024
1 parent eb856b4 commit 46ae176
Show file tree
Hide file tree
Showing 10 changed files with 122 additions and 33 deletions.
6 changes: 6 additions & 0 deletions packages/oruga/src/components/autocomplete/Autocomplete.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
} from "@/composables";
import type { ComponentClass, DynamicComponent, ClassBind } from "@/types";
import { injectField } from "../field/fieldInjection";
enum SpecialOption {
Header,
Expand Down Expand Up @@ -368,6 +369,9 @@ function setItemRef(
const { checkHtml5Validity, onInvalid, onFocus, onBlur, isFocused, setFocus } =
useInputHandler(inputRef, emits, props);
// inject parent field component if used inside one
const { parentField } = injectField();
const isActive = ref(false);
/** The selected option, use v-model to make it two-way binding */
Expand Down Expand Up @@ -747,7 +751,9 @@ function checkDropdownScroll(): void {
// --- Computed Component Classes ---
const attrs = useAttrs();
const inputBind = computed(() => ({
...parentField?.value?.inputAria,
...attrs,
...props.inputClasses,
}));
Expand Down
16 changes: 14 additions & 2 deletions packages/oruga/src/components/checkbox/Checkbox.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<script setup lang="ts" generic="T extends string | number | boolean | object">
import { computed, ref, type PropType } from "vue";
import { computed, ref, useAttrs, type PropType } from "vue";
import { getOption } from "@/utils/config";
import { defineClasses, useInputHandler } from "@/composables";
import { injectField } from "../field/fieldInjection";
import type { ComponentClass } from "@/types";
/**
Expand Down Expand Up @@ -180,6 +182,9 @@ const { onBlur, onFocus, onInvalid, setFocus } = useInputHandler(
props,
);
// inject parent field component if used inside one
const { parentField } = injectField();
const vmodel = defineModel<T | T[]>({ default: undefined });
const isIndeterminate = defineModel<boolean>("indeterminate", {
Expand All @@ -199,6 +204,13 @@ function onInput(event: Event): void {
// --- Computed Component Classes ---
const attrs = useAttrs();
const inputBind = computed(() => ({
...parentField?.value?.inputAria,
...attrs,
}));
const rootClasses = defineClasses(
["rootClass", "o-chk"],
["checkedClass", "o-chk--checked", null, isChecked],
Expand Down Expand Up @@ -246,7 +258,7 @@ defineExpose({ focus: setFocus, value: vmodel });
@click.stop="setFocus"
@keydown.prevent.enter="setFocus">
<input
v-bind="$attrs"
v-bind="inputBind"
ref="inputRef"
v-model="vmodel"
type="checkbox"
Expand Down
22 changes: 16 additions & 6 deletions packages/oruga/src/components/field/Field.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from "vue";
import { getOption } from "@/utils/config";
import { isVNodeEmpty } from "@/utils/helpers";
import { isVNodeEmpty, uuid } from "@/utils/helpers";
import { defineClasses, useMatchMedia } from "@/composables";
import { injectField, provideField } from "./fieldInjection";
Expand Down Expand Up @@ -180,14 +180,16 @@ watch(
() => fieldMessage.value,
(value) => {
if (parentField?.value?.hasInnerField) {
if (!parentField.value.fieldVariant)
if (!parentField.value.variant)
parentField.value.setVariant(fieldVariant.value);
if (!parentField.value.fieldMessage)
parentField.value.setMessage(value);
if (!parentField.value.message) parentField.value.setMessage(value);
}
},
);
/** a uniqe id for the message slot to associate an input to the field message */
const messageId = uuid();
/** this can be set from outside to update the focus state */
const isFocused = ref(false);
/** this can be set from outside to update the filled state */
Expand Down Expand Up @@ -248,14 +250,21 @@ function setInputId(value: string): void {
inputId.value = value;
}
const inputAria = computed(() =>
fieldVariant.value === "error"
? { "aria-errormessage": messageId }
: { "aria-describedby": messageId },
);
// Provided data is a computed ref to enjure reactivity.
const provideData = computed(() => ({
$el: rootRef.value,
props,
hasInnerField: hasInnerField.value,
hasMessage: hasMessage.value,
fieldVariant: fieldVariant.value,
fieldMessage: fieldMessage.value,
variant: fieldVariant.value,
message: fieldMessage.value,
inputAria: inputAria.value,
addInnerField,
setInputId,
setFocus,
Expand Down Expand Up @@ -403,6 +412,7 @@ const innerFieldClasses = defineClasses(
<component
:is="messageTag"
v-if="hasMessage && !horizontal"
:id="messageId"
:class="messageClasses">
<!--
@slot Override the message
Expand Down
30 changes: 18 additions & 12 deletions packages/oruga/src/components/field/fieldInjection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ type FieldData = {
$el: Element;
props: FieldProps;
hasInnerField: boolean;
variant: string;
hasMessage: boolean;
fieldVariant: string;
fieldMessage: string;
message: string;
inputAria: object;
addInnerField: () => void;
setInputId: (value: string) => void;
setFocus: (value: boolean) => void;
Expand All @@ -41,27 +42,32 @@ export function provideField(data: ProvidedField): void {
}

/** Inject parent field component if used inside one. **/
export function injectField() {
export function injectField(): {
parentField?: ComputedRef<FieldData> | undefined;
statusVariant: ComputedRef<string>;
statusVariantIcon: ComputedRef<string>;
statusMessage: ComputedRef<string>;
} {
const parentField = inject($FieldKey, undefined);

/**
* Get the type prop from parent if it's a Field.
*/
const statusVariant = computed(() => {
if (!parentField?.value?.fieldVariant) return undefined;
if (typeof parentField.value.fieldVariant === "string")
return parentField.value.fieldVariant;
if (Array.isArray(parentField.value.fieldVariant)) {
for (const key in parentField.value.fieldVariant as any) {
if (parentField.value.fieldVariant[key]) return key;
const statusVariant = computed<string>(() => {
if (!parentField?.value?.variant) return undefined;
if (typeof parentField.value.variant === "string")
return parentField.value.variant;
if (Array.isArray(parentField.value.variant)) {
for (const key in parentField.value.variant as any) {
if (parentField.value.variant[key]) return key;
}
}
return undefined;
});

/** Get the message prop from parent if it's a Field. */
const statusMessage = computed(() =>
parentField.value?.hasMessage ? parentField.value.fieldMessage : "",
const statusMessage = computed<string>(() =>
parentField.value?.hasMessage ? parentField.value.message : "",
);

/** Icon name based on the variant. */
Expand Down
12 changes: 10 additions & 2 deletions packages/oruga/src/components/input/Input.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
watch,
onMounted,
type StyleValue,
useAttrs,
} from "vue";
import OIcon from "../icon/Icon.vue";
Expand Down Expand Up @@ -266,6 +267,13 @@ function togglePasswordVisibility(): void {
// --- Computed Component Classes ---
const attrs = useAttrs();
const inputBind = computed(() => ({
...parentField?.value?.inputAria,
...attrs,
}));
const rootClasses = defineClasses(
["rootClass", "o-input__wrapper"],
[
Expand Down Expand Up @@ -337,7 +345,7 @@ defineExpose({ focus: setFocus, value: vmodel });
<div data-oruga="input" :class="rootClasses">
<input
v-if="type !== 'textarea'"
v-bind="$attrs"
v-bind="inputBind"
:id="id"
ref="inputRef"
v-model="vmodel"
Expand All @@ -355,7 +363,7 @@ defineExpose({ focus: setFocus, value: vmodel });

<textarea
v-else
v-bind="$attrs"
v-bind="inputBind"
:id="id"
ref="textareaRef"
v-model="vmodel"
Expand Down
16 changes: 14 additions & 2 deletions packages/oruga/src/components/radio/Radio.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<script setup lang="ts" generic="T extends string | number | boolean | object">
import { computed, ref, type PropType } from "vue";
import { computed, ref, useAttrs, type PropType } from "vue";
import { getOption } from "@/utils/config";
import { defineClasses, useInputHandler } from "@/composables";
import { injectField } from "../field/fieldInjection";
import type { ComponentClass } from "@/types";
/**
Expand Down Expand Up @@ -152,6 +154,9 @@ const { onBlur, onFocus, onInvalid, setFocus } = useInputHandler(
props,
);
// inject parent field component if used inside one
const { parentField } = injectField();
const vmodel = defineModel<T>({ default: undefined });
const isChecked = computed(() => vmodel.value === props.nativeValue);
Expand All @@ -162,6 +167,13 @@ function onInput(event: Event): void {
// --- Computed Component Classes ---
const attrs = useAttrs();
const inputBind = computed(() => ({
...parentField?.value?.inputAria,
...attrs,
}));
const rootClasses = defineClasses(
["rootClass", "o-radio"],
["checkedClass", "o-radio--checked", null, isChecked],
Expand Down Expand Up @@ -208,7 +220,7 @@ defineExpose({ focus: setFocus, value: vmodel });
@click.stop="setFocus"
@keydown.prevent.enter="setFocus">
<input
v-bind="$attrs"
v-bind="inputBind"
ref="inputRef"
v-model="vmodel"
type="radio"
Expand Down
11 changes: 9 additions & 2 deletions packages/oruga/src/components/select/Select.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
T extends string | number | object,
IsMultiple extends boolean = false
">
import { computed, watch, onMounted, ref, nextTick } from "vue";
import { computed, watch, onMounted, ref, nextTick, useAttrs } from "vue";
import OIcon from "../icon/Icon.vue";
Expand Down Expand Up @@ -174,6 +174,13 @@ function rightIconClick(event: Event): void {
// --- Computed Component Classes ---
const attrs = useAttrs();
const inputBind = computed(() => ({
...parentField?.value?.inputAria,
...attrs,
}));
const rootClasses = defineClasses(
["rootClass", "o-ctrl-sel"],
[
Expand Down Expand Up @@ -260,7 +267,7 @@ defineExpose({ focus: setFocus, value: vmodel });
@click="leftIconClick($event)" />
<select
v-bind="$attrs"
v-bind="inputBind"
:id="id"
ref="selectRef"
v-model="vmodel"
Expand Down
14 changes: 12 additions & 2 deletions packages/oruga/src/components/switch/Switch.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<script setup lang="ts" generic="T extends string | number | boolean | object">
import { computed, ref, type PropType } from "vue";
import { computed, ref, useAttrs, type PropType } from "vue";
import { getOption } from "@/utils/config";
import { defineClasses, useInputHandler, useVModel } from "@/composables";
import type { ComponentClass } from "@/types";
import { injectField } from "../field/fieldInjection";
/**
* Switch between two opposing states
Expand Down Expand Up @@ -204,6 +205,8 @@ const { onBlur, onFocus, onInvalid, setFocus } = useInputHandler(
emits,
props,
);
// inject parent field component if used inside one
const { parentField } = injectField();
// const vmodel = defineModel<T>({ default: undefined });
const vmodel = useVModel<T>();
Expand All @@ -221,6 +224,13 @@ function onInput(event: Event): void {
// --- Computed Component Classes ---
const attrs = useAttrs();
const inputBind = computed(() => ({
...parentField?.value?.inputAria,
...attrs,
}));
const rootClasses = defineClasses(
["rootClass", "o-switch"],
[
Expand Down Expand Up @@ -289,7 +299,7 @@ defineExpose({ focus: setFocus, value: vmodel });
@click="setFocus"
@keydown.prevent.enter="setFocus">
<input
v-bind="$attrs"
v-bind="inputBind"
ref="inputRef"
v-model="vmodel"
type="checkbox"
Expand Down
Loading

0 comments on commit 46ae176

Please sign in to comment.