mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-12 00:15:44 +01:00
Changes for the Signature Check Plugin
This PR contains new Radiobutton components that are needed for the signature-check-plugin. Additionally the ref passing for ControlledChipInputs was also fixed. The current styling of the Radio Buttons need to be discussed with Philipp again. The Signature Check Plugin provides an example use of it. Co-authored-by: Konstantin Schaper<konstantin.schaper@cloudogu.com>
This commit is contained in:
2
gradle/changelog/added_radion_button.yaml
Normal file
2
gradle/changelog/added_radion_button.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: Radiobutton components are now available in the ui-forms module
|
||||
2
gradle/changelog/fixed_chip_input_ref.yaml
Normal file
2
gradle/changelog/fixed_chip_input_ref.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: fixed
|
||||
description: Some chip inputs not correctly passing the ref
|
||||
@@ -18,6 +18,7 @@
|
||||
"@scm-manager/tsconfig": "^2.13.0",
|
||||
"@scm-manager/ui-styles": "2.47.1-SNAPSHOT",
|
||||
"@storybook/addon-actions": "^6.5.10",
|
||||
"@storybook/addon-docs": "^6.5.14",
|
||||
"@storybook/addon-essentials": "^6.5.10",
|
||||
"@storybook/addon-interactions": "^6.5.10",
|
||||
"@storybook/addon-links": "^6.5.10",
|
||||
@@ -25,7 +26,6 @@
|
||||
"@storybook/manager-webpack5": "^6.5.10",
|
||||
"@storybook/react": "^6.5.10",
|
||||
"@storybook/testing-library": "^0.0.13",
|
||||
"@storybook/addon-docs": "^6.5.14",
|
||||
"babel-loader": "^8.2.5",
|
||||
"storybook-addon-mock": "^3.2.0",
|
||||
"storybook-addon-themes": "^6.1.0",
|
||||
@@ -41,12 +41,13 @@
|
||||
"styled-components": "5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scm-manager/ui-buttons": "2.47.1-SNAPSHOT",
|
||||
"@scm-manager/ui-overlays": "2.47.1-SNAPSHOT",
|
||||
"@scm-manager/ui-api": "2.47.1-SNAPSHOT",
|
||||
"@headlessui/react": "^1.7.15",
|
||||
"@radix-ui/react-radio-group": "^1.1.3",
|
||||
"@radix-ui/react-slot": "^1.0.1",
|
||||
"@radix-ui/react-visually-hidden": "^1.0.3"
|
||||
"@radix-ui/react-visually-hidden": "^1.0.3",
|
||||
"@scm-manager/ui-api": "2.47.1-SNAPSHOT",
|
||||
"@scm-manager/ui-buttons": "2.47.1-SNAPSHOT",
|
||||
"@scm-manager/ui-overlays": "2.47.1-SNAPSHOT"
|
||||
},
|
||||
"prettier": "@scm-manager/prettier-config",
|
||||
"eslintConfig": {
|
||||
|
||||
@@ -40,6 +40,7 @@ import ControlledChipInputField from "./chip-input/ControlledChipInputField";
|
||||
import Combobox from "./combobox/Combobox";
|
||||
import ControlledComboboxField from "./combobox/ControlledComboboxField";
|
||||
import { defaultOptionFactory } from "./helpers";
|
||||
import ChipInput from "./headless-chip-input/ChipInput";
|
||||
|
||||
export type SimpleWebHookConfiguration = {
|
||||
urlPattern: string;
|
||||
@@ -430,4 +431,23 @@ storiesOf("Forms", module)
|
||||
</ControlledList>
|
||||
</ScmFormListContextProvider>
|
||||
</Form>
|
||||
));
|
||||
))
|
||||
.add("Controlled Chip Input with add", () => {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
return (
|
||||
<Form
|
||||
onSubmit={console.log}
|
||||
translationPath={["sample", "form"]}
|
||||
defaultValues={{
|
||||
branches: ["main", "develop"],
|
||||
}}
|
||||
>
|
||||
<FormRow>
|
||||
<ControlledChipInputField name="branches" ref={ref} />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ChipInput.AddButton inputRef={ref}>Add</ChipInput.AddButton>
|
||||
</FormRow>
|
||||
</Form>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { FC, HTMLProps } from "react";
|
||||
import React, { HTMLProps } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
const Control: FC<HTMLProps<HTMLDivElement>> = ({ className, children, ...rest }) => (
|
||||
<div className={classNames("control", className)} {...rest}>
|
||||
const Control = React.forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(({ className, children, ...rest }, ref) => (
|
||||
<div className={classNames("control", className)} {...rest} ref={ref}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
));
|
||||
|
||||
export default Control;
|
||||
|
||||
@@ -25,9 +25,11 @@
|
||||
import React, { FC, HTMLProps } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
const Field: FC<HTMLProps<HTMLDivElement>> = ({ className, children, ...rest }) => (
|
||||
<div className={classNames("field", className)} {...rest}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
const Field: FC<HTMLProps<HTMLDivElement> | ({ as: keyof JSX.IntrinsicElements } & HTMLProps<HTMLElement>)> = ({
|
||||
as = "div",
|
||||
className,
|
||||
children,
|
||||
...rest
|
||||
}) => React.createElement(as, { className: classNames("field", className), ...rest }, children);
|
||||
|
||||
export default Field;
|
||||
|
||||
@@ -25,9 +25,11 @@
|
||||
import React, { FC, HTMLProps } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
const Label: FC<HTMLProps<HTMLLabelElement>> = ({ className, children, ...rest }) => (
|
||||
<label className={classNames("label", className)} {...rest}>
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
const Label: FC<HTMLProps<HTMLLabelElement> | ({ as: keyof JSX.IntrinsicElements } & HTMLProps<HTMLElement>)> = ({
|
||||
as = "label",
|
||||
className,
|
||||
children,
|
||||
...rest
|
||||
}) => React.createElement(as, { className: classNames("label", className), ...rest }, children);
|
||||
|
||||
export default Label;
|
||||
|
||||
@@ -26,7 +26,7 @@ import React, { ComponentProps } from "react";
|
||||
import { Controller, ControllerRenderProps, Path } from "react-hook-form";
|
||||
import { useScmFormContext } from "../ScmFormContext";
|
||||
import { useScmFormPathContext } from "../FormPathContext";
|
||||
import { defaultOptionFactory, prefixWithoutIndices } from "../helpers";
|
||||
import { defaultOptionFactory, prefixWithoutIndices, withForwardRef } from "../helpers";
|
||||
import classNames from "classnames";
|
||||
import ChipInputField from "./ChipInputField";
|
||||
import { Option } from "@scm-manager/ui-types";
|
||||
@@ -41,27 +41,31 @@ type Props<T extends Record<string, unknown>> = Omit<
|
||||
defaultValue?: string[];
|
||||
createDeleteText?: (value: string) => string;
|
||||
optionFactory?: (val: any) => Option<unknown>;
|
||||
ref?: React.ForwardedRef<HTMLInputElement>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.44.0
|
||||
*/
|
||||
function ControlledChipInputField<T extends Record<string, unknown>>({
|
||||
name,
|
||||
label,
|
||||
helpText,
|
||||
rules,
|
||||
testId,
|
||||
defaultValue,
|
||||
readOnly,
|
||||
placeholder,
|
||||
className,
|
||||
createDeleteText,
|
||||
children,
|
||||
optionFactory = defaultOptionFactory,
|
||||
...props
|
||||
}: Props<T>) {
|
||||
function ControlledChipInputField<T extends Record<string, unknown>>(
|
||||
{
|
||||
name,
|
||||
label,
|
||||
helpText,
|
||||
rules,
|
||||
testId,
|
||||
defaultValue,
|
||||
readOnly,
|
||||
placeholder,
|
||||
className,
|
||||
createDeleteText,
|
||||
children,
|
||||
optionFactory = defaultOptionFactory,
|
||||
...props
|
||||
}: Props<T>,
|
||||
ref: React.ForwardedRef<HTMLInputElement>
|
||||
) {
|
||||
const { control, t, readOnly: formReadonly } = useScmFormContext();
|
||||
const formPathPrefix = useScmFormPathContext();
|
||||
|
||||
@@ -95,6 +99,7 @@ function ControlledChipInputField<T extends Record<string, unknown>>({
|
||||
: undefined
|
||||
}
|
||||
testId={testId ?? `input-${nameWithPrefix}`}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</ChipInputField>
|
||||
@@ -103,4 +108,4 @@ function ControlledChipInputField<T extends Record<string, unknown>>({
|
||||
);
|
||||
}
|
||||
|
||||
export default ControlledChipInputField;
|
||||
export default withForwardRef(ControlledChipInputField);
|
||||
|
||||
@@ -38,6 +38,10 @@ import { ScmNestedFormPathContextProvider } from "./FormPathContext";
|
||||
import ControlledComboboxField from "./combobox/ControlledComboboxField";
|
||||
import ChipInputFieldComponent from "./chip-input/ChipInputField";
|
||||
import ChipInput from "./headless-chip-input/ChipInput";
|
||||
import ControlledRadioGroupField from "./radio-button/ControlledRadioGroupField";
|
||||
import RadioGroupComponent from "./radio-button/RadioGroup";
|
||||
import RadioButton from "./radio-button/RadioButton";
|
||||
import RadioGroupFieldComponent from "./radio-button/RadioGroupField";
|
||||
|
||||
export { default as Field } from "./base/Field";
|
||||
export { default as Checkbox } from "./checkbox/Checkbox";
|
||||
@@ -51,6 +55,13 @@ export { default as Select } from "./select/Select";
|
||||
export * from "./resourceHooks";
|
||||
export { default as Label } from "./base/label/Label";
|
||||
|
||||
const RadioGroupExport = {
|
||||
Option: RadioButton,
|
||||
};
|
||||
|
||||
export const RadioGroup = Object.assign(RadioGroupComponent, RadioGroupExport);
|
||||
export const RadioGroupField = Object.assign(RadioGroupFieldComponent, RadioGroupExport);
|
||||
|
||||
export const ChipInputField = Object.assign(ChipInputFieldComponent, {
|
||||
AddButton: ChipInput.AddButton,
|
||||
});
|
||||
@@ -70,4 +81,5 @@ export const Form = Object.assign(FormCmp, {
|
||||
}),
|
||||
ChipInput: ControlledChipInputField,
|
||||
Combobox: ControlledComboboxField,
|
||||
RadioGroup: Object.assign(ControlledRadioGroupField, RadioGroupExport),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { ComponentProps } from "react";
|
||||
import RadioGroupField from "./RadioGroupField";
|
||||
import { Controller, ControllerRenderProps, Path } from "react-hook-form";
|
||||
import { useScmFormContext } from "../ScmFormContext";
|
||||
import { useScmFormPathContext } from "../FormPathContext";
|
||||
import { prefixWithoutIndices } from "../helpers";
|
||||
import classNames from "classnames";
|
||||
import { RadioButtonContextProvider } from "./RadioButtonContext";
|
||||
|
||||
type Props<T extends Record<string, unknown>> = Omit<
|
||||
ComponentProps<typeof RadioGroupField>,
|
||||
"label" | keyof ControllerRenderProps
|
||||
> & {
|
||||
name: Path<T>;
|
||||
rules?: ComponentProps<typeof Controller>["rules"];
|
||||
label?: string;
|
||||
readOnly?: boolean;
|
||||
};
|
||||
|
||||
function ControlledRadioGroupField<T extends Record<string, unknown>>({
|
||||
name,
|
||||
label,
|
||||
helpText,
|
||||
rules,
|
||||
defaultValue,
|
||||
children,
|
||||
fieldClassName,
|
||||
readOnly,
|
||||
...props
|
||||
}: Props<T>) {
|
||||
const { control, t, readOnly: formReadOnly, formId } = useScmFormContext();
|
||||
const formPathPrefix = useScmFormPathContext();
|
||||
const nameWithPrefix = formPathPrefix ? `${formPathPrefix}.${name}` : name;
|
||||
const prefixedNameWithoutIndices = prefixWithoutIndices(nameWithPrefix);
|
||||
const labelTranslation = label ?? t(`${prefixedNameWithoutIndices}.label`) ?? "";
|
||||
const helpTextTranslation = helpText ?? t(`${prefixedNameWithoutIndices}.helpText`);
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={nameWithPrefix}
|
||||
rules={rules}
|
||||
defaultValue={defaultValue}
|
||||
render={({ field }) => (
|
||||
<RadioButtonContextProvider t={t} prefix={prefixedNameWithoutIndices} formId={formId}>
|
||||
<RadioGroupField
|
||||
defaultValue={defaultValue}
|
||||
disabled={readOnly ?? formReadOnly}
|
||||
required={rules?.required as boolean}
|
||||
label={labelTranslation}
|
||||
helpText={helpTextTranslation}
|
||||
fieldClassName={classNames("column", fieldClassName)}
|
||||
onValueChange={field.onChange}
|
||||
{...props}
|
||||
{...field}
|
||||
name={nameWithPrefix}
|
||||
>
|
||||
{children}
|
||||
</RadioGroupField>
|
||||
</RadioButtonContextProvider>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ControlledRadioGroupField;
|
||||
226
scm-ui/ui-forms/src/radio-button/RadioButton.stories.tsx
Normal file
226
scm-ui/ui-forms/src/radio-button/RadioButton.stories.tsx
Normal file
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import RadioButton from "./RadioButton";
|
||||
import RadioGroup from "./RadioGroup";
|
||||
import RadioGroupField from "./RadioGroupField";
|
||||
import Form from "../Form";
|
||||
import ControlledRadioGroupField from "./ControlledRadioGroupField";
|
||||
import ControlledInputField from "../input/ControlledInputField";
|
||||
import FormRow from "../FormRow";
|
||||
import { ScmFormListContextProvider } from "../ScmFormListContext";
|
||||
import ControlledList from "../list/ControlledList";
|
||||
import ControlledTable from "../table/ControlledTable";
|
||||
import ControlledColumn from "../table/ControlledColumn";
|
||||
import AddListEntryForm from "../AddListEntryForm";
|
||||
|
||||
storiesOf("Radio Group", module)
|
||||
.add("Uncontrolled State", () => {
|
||||
return (
|
||||
<RadioGroup name="starter_pokemon">
|
||||
<RadioButton value="CHARMANDER" label="Charmander" />
|
||||
<RadioButton value="SQUIRTLE" label="Squirtle" />
|
||||
<RadioButton value="BULBASAUR" label="Bulbasaur" helpText="Dont pick this one" />
|
||||
</RadioGroup>
|
||||
);
|
||||
})
|
||||
.add("Controlled State", () => {
|
||||
const [value, setValue] = useState<string | undefined>(undefined);
|
||||
return (
|
||||
<RadioGroup name="starter_pokemon" value={value} onValueChange={setValue}>
|
||||
<RadioButton value="CHARMANDER" label="Charmander" />
|
||||
<RadioButton value="SQUIRTLE" label="Squirtle" />
|
||||
<RadioButton value="BULBASAUR" label="Bulbasaur" helpText="Dont pick this one" />
|
||||
</RadioGroup>
|
||||
);
|
||||
})
|
||||
.add("Radio Group Field", () => {
|
||||
const [value, setValue] = useState<string | undefined>(undefined);
|
||||
return (
|
||||
<RadioGroupField
|
||||
label="Choose your starter"
|
||||
helpText="Choose wisely"
|
||||
name="starter_pokemon"
|
||||
value={value}
|
||||
onValueChange={setValue}
|
||||
>
|
||||
<RadioButton value="CHARMANDER" label="Charmander" />
|
||||
<RadioButton value="SQUIRTLE" label="Squirtle" />
|
||||
<RadioButton value="BULBASAUR" label="Bulbasaur" helpText="Dont pick this one" />
|
||||
</RadioGroupField>
|
||||
);
|
||||
})
|
||||
.add("Controlled Radio Group Field", () => (
|
||||
<Form
|
||||
// eslint-disable-next-line no-console
|
||||
onSubmit={console.log}
|
||||
translationPath={["sample", "form"]}
|
||||
defaultValues={{ name: "", starter_pokemon: null }}
|
||||
>
|
||||
<FormRow>
|
||||
<ControlledInputField name="name" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledRadioGroupField
|
||||
name="starter_pokemon"
|
||||
label="Starter Pokemon"
|
||||
helpText="Choose wisely"
|
||||
rules={{ required: true }}
|
||||
>
|
||||
<RadioButton value="CHARMANDER" label="Charmander" labelClassName="is-block" />
|
||||
<RadioButton value="SQUIRTLE" label="Squirtle" />
|
||||
<RadioButton value="BULBASAUR" label="Bulbasaur" helpText="Dont pick this one" />
|
||||
</ControlledRadioGroupField>
|
||||
</FormRow>
|
||||
</Form>
|
||||
))
|
||||
.add("Readonly Controlled Radio Group Field", () => (
|
||||
<Form
|
||||
// eslint-disable-next-line no-console
|
||||
onSubmit={console.log}
|
||||
translationPath={["sample", "form"]}
|
||||
defaultValues={{ name: "Red", starter_pokemon: "SQUIRTLE" }}
|
||||
>
|
||||
<FormRow>
|
||||
<ControlledInputField name="name" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledRadioGroupField
|
||||
name="starter_pokemon"
|
||||
label="Starter Pokemon"
|
||||
helpText="Choose wisely"
|
||||
readOnly={true}
|
||||
>
|
||||
<RadioButton value="CHARMANDER" label="Charmander" labelClassName="is-block" />
|
||||
<RadioButton value="SQUIRTLE" label="Squirtle" />
|
||||
<RadioButton value="BULBASAUR" label="Bulbasaur" helpText="Dont pick this one" />
|
||||
</ControlledRadioGroupField>
|
||||
</FormRow>
|
||||
</Form>
|
||||
))
|
||||
.add("With options", () => (
|
||||
<Form
|
||||
// eslint-disable-next-line no-console
|
||||
onSubmit={console.log}
|
||||
translationPath={["sample", "form"]}
|
||||
defaultValues={{ starter_pokemon: "CHARMANDER" }}
|
||||
>
|
||||
<FormRow>
|
||||
<ControlledRadioGroupField
|
||||
name="starter_pokemon"
|
||||
label="Starter Pokemon"
|
||||
helpText="Choose wisely"
|
||||
rules={{ required: true }}
|
||||
options={[
|
||||
{ value: "CHARMANDER", label: "Charmander" },
|
||||
{ value: "SQUIRTLE", label: "Squirtle" },
|
||||
{ value: "BULBASAUR", label: "Bulbasaur", helpText: "Dont pick this one" },
|
||||
]}
|
||||
/>
|
||||
</FormRow>
|
||||
</Form>
|
||||
))
|
||||
.add("Without label prop", () => (
|
||||
<Form
|
||||
// eslint-disable-next-line no-console
|
||||
onSubmit={console.log}
|
||||
translationPath={["sample", "form"]}
|
||||
defaultValues={{ starter_pokemon: "CHARMANDER" }}
|
||||
>
|
||||
<FormRow>
|
||||
<ControlledRadioGroupField
|
||||
name="starter_pokemon"
|
||||
label="Starter Pokemon"
|
||||
helpText="Choose wisely"
|
||||
rules={{ required: true }}
|
||||
options={[
|
||||
{ value: "CHARMANDER" },
|
||||
{ value: "SQUIRTLE" },
|
||||
{ value: "BULBASAUR", helpText: "Dont pick this one" },
|
||||
]}
|
||||
/>
|
||||
</FormRow>
|
||||
</Form>
|
||||
))
|
||||
.add("Nested", () => (
|
||||
<Form
|
||||
translationPath={["sample", "form"]}
|
||||
// eslint-disable-next-line no-console
|
||||
onSubmit={console.log}
|
||||
defaultValues={{
|
||||
trainers: [
|
||||
{
|
||||
name: "Red",
|
||||
team: [{ nickname: "Charlie", pokemon: "CHARMANDER" }],
|
||||
},
|
||||
{
|
||||
name: "Blue",
|
||||
team: [{ nickname: "Squirtle", pokemon: "SQUIRTLE" }],
|
||||
},
|
||||
{
|
||||
name: "Green",
|
||||
team: [{ nickname: "Plant", pokemon: "SQUIRTLE" }],
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<ScmFormListContextProvider name={"trainers"}>
|
||||
<ControlledList withDelete>
|
||||
{({ value: trainers }) => (
|
||||
<>
|
||||
<FormRow>
|
||||
<ControlledInputField name={"name"} />
|
||||
</FormRow>
|
||||
<details className="has-background-dark-25 mb-2 p-2">
|
||||
<summary className="is-clickable">Team</summary>
|
||||
<div>
|
||||
<ScmFormListContextProvider name={"team"}>
|
||||
<ControlledTable withDelete>
|
||||
<ControlledColumn name="nickname" />
|
||||
<ControlledColumn name="pokemon" />
|
||||
</ControlledTable>
|
||||
<AddListEntryForm defaultValues={{ nickname: "", pokemon: null }}>
|
||||
<FormRow>
|
||||
<ControlledInputField name="nickname" />
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<ControlledRadioGroupField
|
||||
name="pokemon"
|
||||
label="Starter Pokemon"
|
||||
rules={{ required: true }}
|
||||
options={[{ value: "CHARMANDER" }, { value: "SQUIRTLE" }, { value: "BULBASAUR" }]}
|
||||
/>
|
||||
</FormRow>
|
||||
</AddListEntryForm>
|
||||
</ScmFormListContextProvider>
|
||||
</div>
|
||||
</details>
|
||||
</>
|
||||
)}
|
||||
</ControlledList>
|
||||
</ScmFormListContextProvider>
|
||||
</Form>
|
||||
));
|
||||
112
scm-ui/ui-forms/src/radio-button/RadioButton.tsx
Normal file
112
scm-ui/ui-forms/src/radio-button/RadioButton.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { ComponentProps } from "react";
|
||||
import classNames from "classnames";
|
||||
import Help from "../base/help/Help";
|
||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||
import { createAttributesForTesting, useGeneratedId } from "@scm-manager/ui-components";
|
||||
import styled from "styled-components";
|
||||
import { useRadioButtonContext } from "./RadioButtonContext";
|
||||
|
||||
const StyledRadioButton = styled(RadioGroup.Item)`
|
||||
all: unset;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: var(--scm-border);
|
||||
border-radius: 100%;
|
||||
|
||||
:hover {
|
||||
border-color: var(--scm-hover-color);
|
||||
}
|
||||
|
||||
:hover *::after {
|
||||
background-color: var(--scm-info-hover-color);
|
||||
}
|
||||
|
||||
:disabled {
|
||||
background-color: var(--scm-dark-color-25);
|
||||
border-color: var(--scm-hover-color);
|
||||
}
|
||||
|
||||
:disabled *::after {
|
||||
background-color: var(--scm-info-color);
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledIndicator = styled(RadioGroup.Indicator)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
background-color: var(--scm-info-color);
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
id?: string;
|
||||
testId?: string;
|
||||
indicatorClassName?: string;
|
||||
label?: string;
|
||||
labelClassName?: string;
|
||||
helpText?: string;
|
||||
} & ComponentProps<typeof RadioGroup.Item>;
|
||||
|
||||
const RadioButton = React.forwardRef<HTMLButtonElement, Props>(
|
||||
({ id, testId, indicatorClassName, label, labelClassName, className, helpText, value, ...props }, ref) => {
|
||||
const context = useRadioButtonContext();
|
||||
const inputId = useGeneratedId(id);
|
||||
const labelKey = `${context?.prefix}.radio.${value}`;
|
||||
|
||||
return (
|
||||
<label className={classNames("radio is-flex is-align-items-center", labelClassName)} htmlFor={inputId}>
|
||||
<StyledRadioButton
|
||||
form={context?.formId}
|
||||
id={inputId}
|
||||
value={value}
|
||||
ref={ref}
|
||||
className={classNames("mr-3 mt-3 mb-3", className)}
|
||||
{...props}
|
||||
{...createAttributesForTesting(testId)}
|
||||
>
|
||||
<StyledIndicator className={indicatorClassName} />
|
||||
</StyledRadioButton>
|
||||
{label ?? context?.t(labelKey) ?? value}
|
||||
{helpText ? <Help className="ml-3" text={helpText} /> : null}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default RadioButton;
|
||||
42
scm-ui/ui-forms/src/radio-button/RadioButtonContext.tsx
Normal file
42
scm-ui/ui-forms/src/radio-button/RadioButtonContext.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { createContext, FC, useContext } from "react";
|
||||
import { TFunction } from "i18next";
|
||||
|
||||
type ContextType = {
|
||||
t: TFunction;
|
||||
prefix: string;
|
||||
formId?: string;
|
||||
};
|
||||
|
||||
const RadioButtonContext = createContext<ContextType | null>(null);
|
||||
|
||||
export function useRadioButtonContext() {
|
||||
return useContext(RadioButtonContext);
|
||||
}
|
||||
|
||||
export const RadioButtonContextProvider: FC<ContextType> = ({ children, ...props }) => (
|
||||
<RadioButtonContext.Provider value={props}>{children}</RadioButtonContext.Provider>
|
||||
);
|
||||
45
scm-ui/ui-forms/src/radio-button/RadioGroup.tsx
Normal file
45
scm-ui/ui-forms/src/radio-button/RadioGroup.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { ComponentProps } from "react";
|
||||
import Control from "../base/Control";
|
||||
import * as RadixRadio from "@radix-ui/react-radio-group";
|
||||
import RadioButton from "./RadioButton";
|
||||
|
||||
type Props = {
|
||||
options?: { value: string; label?: string; helpText?: string }[];
|
||||
} & ComponentProps<typeof RadixRadio.Root>;
|
||||
|
||||
const RadioGroup = React.forwardRef<HTMLDivElement, Props>(({ options, children, className, ...props }, ref) => (
|
||||
<RadixRadio.Root {...props} asChild>
|
||||
<Control ref={ref} className={className}>
|
||||
{children ??
|
||||
options?.map((option) => (
|
||||
<RadioButton key={option.value} value={option.value} label={option.label} helpText={option.helpText} />
|
||||
))}
|
||||
</Control>
|
||||
</RadixRadio.Root>
|
||||
));
|
||||
|
||||
export default RadioGroup;
|
||||
54
scm-ui/ui-forms/src/radio-button/RadioGroupField.tsx
Normal file
54
scm-ui/ui-forms/src/radio-button/RadioGroupField.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { ComponentProps } from "react";
|
||||
import Field from "../base/Field";
|
||||
import Label from "../base/label/Label";
|
||||
import Help from "../base/help/Help";
|
||||
import RadioGroup from "./RadioGroup";
|
||||
|
||||
type Props = {
|
||||
fieldClassName?: string;
|
||||
labelClassName?: string;
|
||||
label: string;
|
||||
helpText?: string;
|
||||
} & ComponentProps<typeof RadioGroup>;
|
||||
|
||||
const RadioGroupField = React.forwardRef<HTMLDivElement, Props>(
|
||||
({ fieldClassName, labelClassName, label, helpText, children, ...props }, ref) => {
|
||||
return (
|
||||
<Field className={fieldClassName} as="fieldset">
|
||||
<Label className={labelClassName} as="legend">
|
||||
{label}
|
||||
{helpText ? <Help className="ml-1" text={helpText} /> : null}
|
||||
</Label>
|
||||
<RadioGroup ref={ref} {...props}>
|
||||
{children}
|
||||
</RadioGroup>
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default RadioGroupField;
|
||||
@@ -26,9 +26,11 @@
|
||||
--scm-white-color: #{$white};
|
||||
--scm-light-color: #{$light};
|
||||
--scm-dark-color: #{$dark};
|
||||
--scm-dark-color-25: #{$dark-25};
|
||||
--scm-primary-color: #{$primary};
|
||||
--scm-link-color: #{$link};
|
||||
--scm-info-color: #{$info};
|
||||
--scm-info-hover-color: #{$info-75};
|
||||
--scm-success-color: #{$success};
|
||||
--scm-warning-color: #{$warning};
|
||||
--scm-danger-color: #{$danger};
|
||||
|
||||
24
yarn.lock
24
yarn.lock
@@ -2944,6 +2944,23 @@
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-slot" "1.0.2"
|
||||
|
||||
"@radix-ui/react-radio-group@^1.1.3":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz#3197f5dcce143bcbf961471bf89320735c0212d3"
|
||||
integrity sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.1"
|
||||
"@radix-ui/react-compose-refs" "1.0.1"
|
||||
"@radix-ui/react-context" "1.0.1"
|
||||
"@radix-ui/react-direction" "1.0.1"
|
||||
"@radix-ui/react-presence" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
"@radix-ui/react-roving-focus" "1.0.4"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||
"@radix-ui/react-use-previous" "1.0.1"
|
||||
"@radix-ui/react-use-size" "1.0.1"
|
||||
|
||||
"@radix-ui/react-roving-focus@1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz#e90c4a6a5f6ac09d3b8c1f5b5e81aab2f0db1974"
|
||||
@@ -3070,6 +3087,13 @@
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-use-previous@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz#b595c087b07317a4f143696c6a01de43b0d0ec66"
|
||||
integrity sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-use-rect@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.0.tgz"
|
||||
|
||||
Reference in New Issue
Block a user