Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement OnyxForm with disabled state injection #1902

Merged
merged 28 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8154312
implement onyxform with form-context
JoCa96 Sep 30, 2024
3f87481
rename onyxform.ts to oynxform.core.ts
JoCa96 Sep 30, 2024
0606262
add test
JoCa96 Sep 30, 2024
a352590
fix mistakes
JoCa96 Sep 30, 2024
ab81ab3
add storybook support
JoCa96 Sep 30, 2024
1587a7b
update description
JoCa96 Sep 30, 2024
3b26684
add unit test for form injection
JoCa96 Sep 30, 2024
6e3bfee
remove any's
JoCa96 Oct 1, 2024
7473f69
implement onyxform with form-context
JoCa96 Sep 30, 2024
9a62abd
rename onyxform.ts to oynxform.core.ts
JoCa96 Sep 30, 2024
c7079a0
add test
JoCa96 Sep 30, 2024
575335e
fix mistakes
JoCa96 Sep 30, 2024
e3213ee
add storybook support
JoCa96 Sep 30, 2024
c56271a
update description
JoCa96 Sep 30, 2024
d1c8364
add unit test for form injection
JoCa96 Sep 30, 2024
990b539
remove any's
JoCa96 Oct 1, 2024
a40a59d
Merge branch '757-onyx-form' of https://github.com/SchwarzIT/onyx int…
JoCa96 Oct 1, 2024
bcad4dd
Update packages/sit-onyx/src/components/OnyxForm/OnyxForm.vue
JoCa96 Oct 1, 2024
15dfb31
chore: implement review feedback
JoCa96 Oct 1, 2024
ed122ed
Merge branch '757-onyx-form' of https://github.com/SchwarzIT/onyx int…
JoCa96 Oct 1, 2024
ed5442d
impl createSymbolArgTypeEnhancer
JoCa96 Oct 1, 2024
0433fe0
add description and example
JoCa96 Oct 1, 2024
43d0e69
docs(changeset): New createSymbolArgTypeEnhancer which adds descripti…
JoCa96 Oct 1, 2024
9a01624
docs(changeset): Implement OnyxForm which allows setting of disabled …
JoCa96 Oct 1, 2024
ebd8a1b
review: remove getter
JoCa96 Oct 1, 2024
c0604f4
Merge branch 'main' of https://github.com/SchwarzIT/onyx into 757-ony…
JoCa96 Oct 1, 2024
8445b57
fix prettier
JoCa96 Oct 1, 2024
cee3b9a
Merge branch 'main' into 757-onyx-form
JoCa96 Oct 2, 2024
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
5 changes: 5 additions & 0 deletions .changeset/beige-swans-add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"sit-onyx": minor
---

Implement OnyxForm which allows setting of disabled state for all child form elements
5 changes: 5 additions & 0 deletions .changeset/sharp-news-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sit-onyx/storybook-utils": minor
---

New createSymbolArgTypeEnhancer which adds description text to symbols used as default props
7 changes: 7 additions & 0 deletions packages/sit-onyx/.storybook/formInjected.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createSymbolArgTypeEnhancer } from "@sit-onyx/storybook-utils";

export const enhanceFormInjectedSymbol = createSymbolArgTypeEnhancer(
"FORM_INJECTED_SYMBOL",
"If no value (or `undefined`) is provided, `FORM_INJECTED_SYMBOL` is the internal default value for this prop.\n" +
"In that case the props value will be derived from it's parent form (if it exists).\n",
);
60 changes: 9 additions & 51 deletions packages/sit-onyx/.storybook/managed.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,9 @@
import { walkTree } from "@sit-onyx/storybook-utils";
import type {
ArgTypesEnhancer,
InputType,
SBType,
StrictInputType,
} from "storybook/internal/types";

const SB_TYPE_CONTROL_MAP: Partial<Record<SBType["name"], InputType["control"]>> = {
boolean: { type: "boolean" },
string: { type: "text" },
number: { type: "number" },
};

const getManagedParent = (inputType?: StrictInputType) => {
if (!inputType?.type || inputType.table?.defaultValue?.summary !== "MANAGED_SYMBOL") {
return undefined;
}

return walkTree(inputType.type, (elem, parent) =>
elem.name === "symbol" || (elem.name === "other" && elem.value === "unique symbol")
? parent
: undefined,
);
};

export const enhanceManagedSymbol: ArgTypesEnhancer = (context) => {
Object.values(context.argTypes)
.map((argType) => {
const parent = getManagedParent(argType);
return { argType, parent };
})
.filter(({ parent }) => parent)
.forEach(({ argType, parent }) => {
const firstAvailableControl = walkTree(
parent || argType.type!,
(sb) => SB_TYPE_CONTROL_MAP[sb.name],
);

if (firstAvailableControl && argType.table?.defaultValue) {
argType.control = firstAvailableControl;
argType.table.defaultValue.detail =
"If no value (or `undefined`) is passed, `MANAGED_SYMBOL` is the internal default value for this prop.\n" +
"It signals the component that the prop is managed and it's state tracked internally.\n" +
"So in that case no prop binding or `v-model` is necessary.\n" +
"Updates for the prop will still be emitted.\n";
}
});

return context.argTypes;
};
import { createSymbolArgTypeEnhancer } from "@sit-onyx/storybook-utils";

export const enhanceManagedSymbol = createSymbolArgTypeEnhancer(
"MANAGED_SYMBOL",
"If no value (or `undefined`) is passed, `MANAGED_SYMBOL` is the internal default value for this prop.\n" +
"It signals the component that the prop is managed and it's state tracked internally.\n" +
"So in that case no prop binding or `v-model` is necessary.\n" +
"Updates for the prop will still be emitted.\n",
);
3 changes: 2 additions & 1 deletion packages/sit-onyx/.storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import "@fontsource-variable/source-sans-3";
import "@sit-onyx/storybook-utils/style.css";
import "../src/styles/index.scss";
import "./docs-template.scss";
import { enhanceFormInjectedSymbol } from "./formInjected";
import { enhanceManagedSymbol } from "./managed";
import { withOnyxVModelDecorator } from "./vModel";

const basePreview = createPreview({
argTypesEnhancers: [enhanceManagedSymbol],
argTypesEnhancers: [enhanceManagedSymbol, enhanceFormInjectedSymbol],
parameters: {
docs: {
page: docsTemplate,
Expand Down
6 changes: 4 additions & 2 deletions packages/sit-onyx/src/components/OnyxButton/OnyxButton.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
<script lang="ts" setup>
import { computed, ref, type ComponentInstance } from "vue";
import { useDensity } from "../../composables/density";
import { FORM_INJECTED_SYMBOL, useFormContext } from "../OnyxForm/OnyxForm.core";
import OnyxIcon from "../OnyxIcon/OnyxIcon.vue";
import OnyxLoadingIndicator from "../OnyxLoadingIndicator/OnyxLoadingIndicator.vue";
import OnyxRipple from "../OnyxRipple/OnyxRipple.vue";
import OnyxSkeleton from "../OnyxSkeleton/OnyxSkeleton.vue";
import type { OnyxButtonProps } from "./types";

const props = withDefaults(defineProps<OnyxButtonProps>(), {
disabled: false,
disabled: FORM_INJECTED_SYMBOL,
loading: false,
type: "button",
color: "primary",
Expand All @@ -17,6 +18,7 @@ const props = withDefaults(defineProps<OnyxButtonProps>(), {
});

const { densityClass } = useDensity(props);
const { disabled } = useFormContext(props);

const rippleRef = ref<ComponentInstance<typeof OnyxRipple>>();
const rippleEvents = computed(() => rippleRef.value?.events ?? {});
Expand All @@ -33,7 +35,7 @@ const rippleEvents = computed(() => rippleRef.value?.events ?? {});
{ 'onyx-button--loading': props.loading },
densityClass,
]"
:disabled="props.disabled || props.loading"
:disabled="disabled || props.loading"
:type="props.type"
:aria-label="props.loading ? props.label : undefined"
:autofocus="props.autofocus"
Expand Down
3 changes: 2 additions & 1 deletion packages/sit-onyx/src/components/OnyxButton/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { DensityProp } from "../../composables/density";
import type { AutofocusProp } from "../../types";
import type { FormInjected } from "../OnyxForm/OnyxForm.core";

export type OnyxButtonProps = DensityProp &
AutofocusProp & {
Expand All @@ -10,7 +11,7 @@ export type OnyxButtonProps = DensityProp &
/**
* If the button should be disabled or not.
*/
disabled?: boolean;
disabled?: FormInjected<boolean>;
/**
* Shows a loading indicator.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import { useRequired } from "../../composables/required";
import { useCustomValidity } from "../../composables/useCustomValidity";
import type { SelectOptionValue } from "../../types";
import OnyxErrorTooltip from "../OnyxErrorTooltip/OnyxErrorTooltip.vue";
import { FORM_INJECTED_SYMBOL, useFormContext } from "../OnyxForm/OnyxForm.core";
import OnyxLoadingIndicator from "../OnyxLoadingIndicator/OnyxLoadingIndicator.vue";
import OnyxSkeleton from "../OnyxSkeleton/OnyxSkeleton.vue";
import type { OnyxCheckboxProps } from "./types";

const props = withDefaults(defineProps<OnyxCheckboxProps<TValue>>(), {
modelValue: false,
indeterminate: false,
disabled: false,
disabled: FORM_INJECTED_SYMBOL,
loading: false,
required: false,
skeleton: false,
Expand All @@ -37,6 +38,7 @@ const { requiredMarkerClass, requiredTypeClass } = useRequired(props);
const { densityClass } = useDensity(props);

const { vCustomValidity, errorMessages } = useCustomValidity({ props, emit });
const { disabled } = useFormContext(props);

const title = computed(() => {
return props.hideLabel ? props.label : undefined;
Expand All @@ -49,7 +51,7 @@ const title = computed(() => {
<OnyxSkeleton v-if="!props.hideLabel" class="onyx-checkbox-skeleton__label" />
</div>

<OnyxErrorTooltip v-else :disabled="props.disabled" :error-messages="errorMessages">
<OnyxErrorTooltip v-else :disabled="disabled" :error-messages="errorMessages">
<label class="onyx-checkbox" :class="[requiredTypeClass, densityClass]" :title="title">
<div class="onyx-checkbox__container">
<OnyxLoadingIndicator v-if="props.loading" class="onyx-checkbox__loading" type="circle" />
Expand All @@ -61,7 +63,7 @@ const title = computed(() => {
class="onyx-checkbox__input"
type="checkbox"
:indeterminate="props.indeterminate"
:disabled="props.disabled"
:disabled="disabled"
:required="props.required"
:value="props.value"
:autofocus="props.autofocus"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import { useDensity } from "../../composables/density";
import { injectI18n } from "../../i18n";
import type { SelectOptionValue } from "../../types";
import OnyxCheckbox from "../OnyxCheckbox/OnyxCheckbox.vue";
import { FORM_INJECTED_SYMBOL, useFormContext } from "../OnyxForm/OnyxForm.core";
import OnyxHeadline from "../OnyxHeadline/OnyxHeadline.vue";
import type { OnyxCheckboxGroupProps } from "./types";

const props = withDefaults(defineProps<OnyxCheckboxGroupProps<TValue>>(), {
modelValue: () => [],
direction: "vertical",
withCheckAll: false,
disabled: false,
disabled: FORM_INJECTED_SYMBOL,
truncation: "ellipsis",
});

Expand All @@ -38,6 +39,8 @@ const enabledOptionValues = computed(() =>
props.options.filter((i) => !i.disabled && !i.skeleton).map(({ value }) => value),
);

const { disabled } = useFormContext(props);

const checkAll = useCheckAll(
enabledOptionValues,
computed(() => props.modelValue),
Expand All @@ -54,7 +57,7 @@ const checkAllLabel = computed(() => {
<template>
<fieldset
:class="['onyx-checkbox-group', densityClass]"
:disabled="props.disabled"
:disabled="disabled"
:aria-label="props.label"
>
<legend v-if="!props.hideLabel" class="onyx-checkbox-group__label">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { DensityProp } from "../../composables/density";
import type { RequiredMarkerProp } from "../../composables/required";
import type { CustomValidityProp } from "../../composables/useCustomValidity";
import type { AutofocusProp, BaseSelectOption, Direction, SelectOptionValue } from "../../types";
import type { FormInjected } from "../OnyxForm/OnyxForm.core";
import type { OnyxFormElementProps } from "../OnyxFormElement/types";

export type OnyxCheckboxGroupProps<TValue extends SelectOptionValue = SelectOptionValue> =
Expand Down Expand Up @@ -36,7 +37,7 @@ export type OnyxCheckboxGroupProps<TValue extends SelectOptionValue = SelectOpti
/**
* Whether all checkboxes should be disabled.
*/
disabled?: boolean;
disabled?: FormInjected<boolean>;
/**
* If set, the specified number of skeleton radio buttons will be shown.
*/
Expand Down
95 changes: 95 additions & 0 deletions packages/sit-onyx/src/components/OnyxForm/OnyxForm.core.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { expect, it, vi } from "vitest";
import { reactive, toValue } from "vue";
import {
FORM_INJECTED_SYMBOL,
provideFormContext,
useFormContext,
type FORM_INJECTED,
type FormInjected,
type FormInjectedProps,
} from "./OnyxForm.core";

let injected: (args: unknown[]) => void;

vi.mock("vue", async (importOriginal) => {
const mod = await importOriginal<typeof import("vue")>();
return {
...mod,
inject: () => injected,
provide: (_: symbol, ctx: (...args: unknown[]) => void) => (injected = ctx),
};
});

it.for([
{
formProps: { disabled: true },
localProps: { disabled: true },
expected: { disabled: true },
},
{
formProps: { disabled: false },
localProps: { disabled: true },
expected: { disabled: true },
},
{
formProps: { disabled: true },
localProps: { disabled: false },
expected: { disabled: false },
},
{
formProps: { disabled: false },
localProps: { disabled: false },
expected: { disabled: false },
},
{
formProps: { disabled: true },
localProps: { disabled: FORM_INJECTED_SYMBOL as FORM_INJECTED },
expected: { disabled: true },
},
{
formProps: { disabled: false },
localProps: { disabled: FORM_INJECTED_SYMBOL as FORM_INJECTED },
expected: { disabled: false },
},
{
formProps: undefined,
localProps: { disabled: FORM_INJECTED_SYMBOL as FORM_INJECTED },
expected: { disabled: false },
},
{
formProps: undefined,
localProps: { disabled: true },
expected: { disabled: true },
},
{
formProps: undefined,
localProps: { disabled: false },
expected: { disabled: false },
},
])("it should derive expected state when correctly", ({ formProps, localProps, expected }) => {
provideFormContext(formProps);
const result = useFormContext(localProps);
Object.entries(expected).forEach(([key, value]) => {
const resultValue = toValue(result[key as keyof FormInjectedProps]);

expect(
resultValue,
`Expected "${value}", got "${resultValue}" for formProps "${formProps}" and localProps "${localProps}"`,
).toBe(value);
});
});

it("should update when changed", async () => {
const formProps = reactive({ disabled: false });
provideFormContext(formProps);

const localProps = reactive({ disabled: FORM_INJECTED_SYMBOL as FormInjected<boolean> });
const { disabled } = useFormContext(localProps);
expect(disabled.value).toBe(false);

formProps.disabled = true;
localProps.disabled = true;
expect(disabled.value).toBe(true);
localProps.disabled = false;
expect(disabled.value).toBe(false);
});
Loading
Loading