mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-12 00:15:44 +01:00
Implement chip input for multiple text entries
Co-authored-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com>
This commit is contained in:
2
gradle/changelog/chip_input_component.yaml
Normal file
2
gradle/changelog/chip_input_component.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
- type: added
|
||||||
|
description: New chip input component
|
||||||
@@ -43,7 +43,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@scm-manager/ui-buttons": "2.43.2-SNAPSHOT",
|
"@scm-manager/ui-buttons": "2.43.2-SNAPSHOT",
|
||||||
"@scm-manager/ui-overlays": "2.43.2-SNAPSHOT",
|
"@scm-manager/ui-overlays": "2.43.2-SNAPSHOT",
|
||||||
"@scm-manager/ui-api": "2.43.2-SNAPSHOT"
|
"@scm-manager/ui-api": "2.43.2-SNAPSHOT",
|
||||||
|
"@radix-ui/react-slot": "^1.0.1",
|
||||||
|
"@radix-ui/react-visually-hidden": "^1.0.3"
|
||||||
},
|
},
|
||||||
"prettier": "@scm-manager/prettier-config",
|
"prettier": "@scm-manager/prettier-config",
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import ControlledTable from "./table/ControlledTable";
|
|||||||
import AddListEntryForm from "./AddListEntryForm";
|
import AddListEntryForm from "./AddListEntryForm";
|
||||||
import { ScmFormListContextProvider } from "./ScmFormListContext";
|
import { ScmFormListContextProvider } from "./ScmFormListContext";
|
||||||
import { HalRepresentation } from "@scm-manager/ui-types";
|
import { HalRepresentation } from "@scm-manager/ui-types";
|
||||||
|
import ControlledChipInputField from "./chip-input/ControlledChipInputField";
|
||||||
|
|
||||||
export type SimpleWebHookConfiguration = {
|
export type SimpleWebHookConfiguration = {
|
||||||
urlPattern: string;
|
urlPattern: string;
|
||||||
@@ -239,6 +240,7 @@ storiesOf("Forms", module)
|
|||||||
disableA: false,
|
disableA: false,
|
||||||
disableB: false,
|
disableB: false,
|
||||||
disableC: true,
|
disableC: true,
|
||||||
|
labels: ["test"],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ControlledInputField name="url" />
|
<ControlledInputField name="url" />
|
||||||
@@ -247,6 +249,7 @@ storiesOf("Forms", module)
|
|||||||
<ControlledCheckboxField name="disableA" />
|
<ControlledCheckboxField name="disableA" />
|
||||||
<ControlledCheckboxField name="disableB" />
|
<ControlledCheckboxField name="disableB" />
|
||||||
<ControlledCheckboxField name="disableC" />
|
<ControlledCheckboxField name="disableC" />
|
||||||
|
<ControlledChipInputField name="labels" />
|
||||||
</Form>
|
</Form>
|
||||||
))
|
))
|
||||||
.add("ReadOnly", () => (
|
.add("ReadOnly", () => (
|
||||||
@@ -257,6 +260,7 @@ storiesOf("Forms", module)
|
|||||||
name: "trillian",
|
name: "trillian",
|
||||||
password: "secret",
|
password: "secret",
|
||||||
active: true,
|
active: true,
|
||||||
|
labels: ["test", "hero"],
|
||||||
}}
|
}}
|
||||||
readOnly
|
readOnly
|
||||||
>
|
>
|
||||||
@@ -269,6 +273,9 @@ storiesOf("Forms", module)
|
|||||||
<FormRow>
|
<FormRow>
|
||||||
<ControlledCheckboxField name="active" />
|
<ControlledCheckboxField name="active" />
|
||||||
</FormRow>
|
</FormRow>
|
||||||
|
<FormRow>
|
||||||
|
<ControlledChipInputField name="labels" aria-label="Test" placeholder="New label..." />
|
||||||
|
</FormRow>
|
||||||
</Form>
|
</Form>
|
||||||
))
|
))
|
||||||
.add("Nested", () => (
|
.add("Nested", () => (
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { Tooltip } from "@scm-manager/ui-overlays";
|
|||||||
type Props = { text?: string; className?: string };
|
type Props = { text?: string; className?: string };
|
||||||
const Help = ({ text, className }: Props) => (
|
const Help = ({ text, className }: Props) => (
|
||||||
<Tooltip className={className} message={text}>
|
<Tooltip className={className} message={text}>
|
||||||
<i className="fas fa-fw fa-question-circle has-text-blue-light" />
|
<i className="fas fa-fw fa-question-circle has-text-blue-light" aria-hidden />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
export default Help;
|
export default Help;
|
||||||
|
|||||||
40
scm-ui/ui-forms/src/chip-input/ChipInputField.stories.tsx
Normal file
40
scm-ui/ui-forms/src/chip-input/ChipInputField.stories.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* 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 { storiesOf } from "@storybook/react";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import ChipInputField from "./ChipInputField";
|
||||||
|
|
||||||
|
storiesOf("Chip Input Field", module).add("Default", () => {
|
||||||
|
const [value, setValue] = useState(["test"]);
|
||||||
|
return (
|
||||||
|
<ChipInputField
|
||||||
|
value={value}
|
||||||
|
onChange={setValue}
|
||||||
|
label="Test Chips"
|
||||||
|
placeholder="This is a placeholder..."
|
||||||
|
aria-label="My personal chip list"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
141
scm-ui/ui-forms/src/chip-input/ChipInputField.tsx
Normal file
141
scm-ui/ui-forms/src/chip-input/ChipInputField.tsx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
* 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, useCallback } from "react";
|
||||||
|
import { createAttributesForTesting, useGeneratedId } from "@scm-manager/ui-components";
|
||||||
|
import Field from "../base/Field";
|
||||||
|
import Label from "../base/label/Label";
|
||||||
|
import Help from "../base/help/Help";
|
||||||
|
import FieldMessage from "../base/field-message/FieldMessage";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { createVariantClass } from "../variants";
|
||||||
|
import ChipInput, { NewChipInput } from "../headless-chip-input/ChipInput";
|
||||||
|
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
const StyledChipInput = styled(ChipInput)`
|
||||||
|
min-height: 40px;
|
||||||
|
height: min-content;
|
||||||
|
gap: 0.5rem;
|
||||||
|
&:focus-within {
|
||||||
|
border: 1px solid var(--scm-info-color);
|
||||||
|
box-shadow: rgba(51, 178, 232, 0.25) 0px 0px 0px 0.125em;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledInput = styled(NewChipInput)`
|
||||||
|
color: var(--scm-secondary-more-color);
|
||||||
|
font-size: 1rem;
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type InputFieldProps = {
|
||||||
|
label: string;
|
||||||
|
createDeleteText?: (value: string) => string;
|
||||||
|
helpText?: string;
|
||||||
|
error?: string;
|
||||||
|
testId?: string;
|
||||||
|
id?: string;
|
||||||
|
} & Pick<ComponentProps<typeof NewChipInput>, "placeholder"> &
|
||||||
|
Pick<ComponentProps<typeof ChipInput>, "onChange" | "value" | "readOnly" | "disabled"> &
|
||||||
|
Pick<ComponentProps<typeof Field>, "className">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @beta
|
||||||
|
* @since 2.44.0
|
||||||
|
*/
|
||||||
|
const ChipInputField = React.forwardRef<HTMLInputElement, InputFieldProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
label,
|
||||||
|
helpText,
|
||||||
|
readOnly,
|
||||||
|
disabled,
|
||||||
|
error,
|
||||||
|
createDeleteText,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
value,
|
||||||
|
className,
|
||||||
|
testId,
|
||||||
|
id,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const [t] = useTranslation("commons", { keyPrefix: "form.chipList" });
|
||||||
|
const deleteTextCallback = useCallback(
|
||||||
|
(item) => (createDeleteText ? createDeleteText(item) : t("delete", { item })),
|
||||||
|
[createDeleteText, t]
|
||||||
|
);
|
||||||
|
const inputId = useGeneratedId(id ?? testId);
|
||||||
|
const labelId = useGeneratedId();
|
||||||
|
const inputDescriptionId = useGeneratedId();
|
||||||
|
const variant = error ? "danger" : undefined;
|
||||||
|
return (
|
||||||
|
<Field className={className} aria-owns={inputId}>
|
||||||
|
<Label id={labelId}>
|
||||||
|
{label}
|
||||||
|
{helpText ? <Help className="ml-1" text={helpText} /> : null}
|
||||||
|
</Label>
|
||||||
|
<StyledChipInput
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
className="is-flex is-flex-wrap-wrap input"
|
||||||
|
aria-labelledby={labelId}
|
||||||
|
disabled={readOnly || disabled}
|
||||||
|
>
|
||||||
|
{value?.map((val, index) => (
|
||||||
|
<ChipInput.Chip key={`${val}-${index}`} className="tag is-light">
|
||||||
|
{val}
|
||||||
|
<ChipInput.Chip.Delete aria-label={deleteTextCallback(val)} index={index} className="delete is-small" />
|
||||||
|
</ChipInput.Chip>
|
||||||
|
))}
|
||||||
|
<StyledInput
|
||||||
|
{...props}
|
||||||
|
className={classNames(
|
||||||
|
"is-borderless",
|
||||||
|
"is-flex-grow-1",
|
||||||
|
"has-background-transparent",
|
||||||
|
createVariantClass(variant)
|
||||||
|
)}
|
||||||
|
placeholder={!readOnly && !disabled ? placeholder : ""}
|
||||||
|
id={inputId}
|
||||||
|
ref={ref}
|
||||||
|
aria-describedby={inputDescriptionId}
|
||||||
|
{...createAttributesForTesting(testId)}
|
||||||
|
/>
|
||||||
|
</StyledChipInput>
|
||||||
|
<VisuallyHidden aria-hidden id={inputDescriptionId}>
|
||||||
|
{t("input.description")}
|
||||||
|
</VisuallyHidden>
|
||||||
|
{error ? <FieldMessage variant={variant}>{error}</FieldMessage> : null}
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
export default ChipInputField;
|
||||||
98
scm-ui/ui-forms/src/chip-input/ControlledChipInputField.tsx
Normal file
98
scm-ui/ui-forms/src/chip-input/ControlledChipInputField.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Controller, ControllerRenderProps, Path } from "react-hook-form";
|
||||||
|
import { useScmFormContext } from "../ScmFormContext";
|
||||||
|
import { useScmFormPathContext } from "../FormPathContext";
|
||||||
|
import { prefixWithoutIndices } from "../helpers";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import ChipInputField from "./ChipInputField";
|
||||||
|
|
||||||
|
type Props<T extends Record<string, unknown>> = Omit<
|
||||||
|
ComponentProps<typeof ChipInputField>,
|
||||||
|
"error" | "createDeleteText" | "label" | "defaultChecked" | "required" | keyof ControllerRenderProps
|
||||||
|
> & {
|
||||||
|
rules?: ComponentProps<typeof Controller>["rules"];
|
||||||
|
name: Path<T>;
|
||||||
|
label?: string;
|
||||||
|
defaultValue?: string[];
|
||||||
|
createDeleteText?: (value: string) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @beta
|
||||||
|
* @since 2.44.0
|
||||||
|
*/
|
||||||
|
function ControlledChipInputField<T extends Record<string, unknown>>({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
helpText,
|
||||||
|
rules,
|
||||||
|
testId,
|
||||||
|
defaultValue,
|
||||||
|
readOnly,
|
||||||
|
placeholder,
|
||||||
|
className,
|
||||||
|
createDeleteText,
|
||||||
|
...props
|
||||||
|
}: Props<T>) {
|
||||||
|
const { control, t, readOnly: formReadonly } = useScmFormContext();
|
||||||
|
const formPathPrefix = useScmFormPathContext();
|
||||||
|
|
||||||
|
const nameWithPrefix = formPathPrefix ? `${formPathPrefix}.${name}` : name;
|
||||||
|
const prefixedNameWithoutIndices = prefixWithoutIndices(nameWithPrefix);
|
||||||
|
const labelTranslation = label || t(`${prefixedNameWithoutIndices}.label`) || "";
|
||||||
|
const placeholderTranslation = placeholder || t(`${prefixedNameWithoutIndices}.placeholder`) || "";
|
||||||
|
const ariaLabelTranslation = t(`${prefixedNameWithoutIndices}.ariaLabel`);
|
||||||
|
const helpTextTranslation = helpText || t(`${prefixedNameWithoutIndices}.helpText`);
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={nameWithPrefix}
|
||||||
|
rules={rules}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<ChipInputField
|
||||||
|
label={labelTranslation}
|
||||||
|
helpText={helpTextTranslation}
|
||||||
|
placeholder={placeholderTranslation}
|
||||||
|
aria-label={ariaLabelTranslation}
|
||||||
|
{...props}
|
||||||
|
{...field}
|
||||||
|
readOnly={readOnly ?? formReadonly}
|
||||||
|
className={classNames("column", className)}
|
||||||
|
error={
|
||||||
|
fieldState.error
|
||||||
|
? fieldState.error.message || t(`${prefixedNameWithoutIndices}.error.${fieldState.error.type}`)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
testId={testId ?? `input-${nameWithPrefix}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ControlledChipInputField;
|
||||||
154
scm-ui/ui-forms/src/headless-chip-input/ChipInput.tsx
Normal file
154
scm-ui/ui-forms/src/headless-chip-input/ChipInput.tsx
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
/*
|
||||||
|
* 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, {
|
||||||
|
ButtonHTMLAttributes,
|
||||||
|
createContext,
|
||||||
|
HTMLAttributes,
|
||||||
|
InputHTMLAttributes,
|
||||||
|
KeyboardEventHandler,
|
||||||
|
LiHTMLAttributes,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useMemo,
|
||||||
|
} from "react";
|
||||||
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
|
|
||||||
|
type ChipInputContextType = {
|
||||||
|
add(newValue: string): void;
|
||||||
|
remove(index: number): void;
|
||||||
|
disabled?: boolean;
|
||||||
|
readOnly?: boolean;
|
||||||
|
value: string[];
|
||||||
|
};
|
||||||
|
const ChipInputContext = createContext<ChipInputContextType>(null as unknown as ChipInputContextType);
|
||||||
|
|
||||||
|
type ChipDeleteProps = {
|
||||||
|
asChild?: boolean;
|
||||||
|
index: number;
|
||||||
|
} & Omit<ButtonHTMLAttributes<HTMLButtonElement>, "type" | "onClick">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @beta
|
||||||
|
* @since 2.44.0
|
||||||
|
*/
|
||||||
|
export const ChipDelete = React.forwardRef<HTMLButtonElement, ChipDeleteProps>(({ asChild, index, ...props }, ref) => {
|
||||||
|
const { remove, disabled } = useContext(ChipInputContext);
|
||||||
|
const Comp = asChild ? Slot : "button";
|
||||||
|
|
||||||
|
if (disabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Comp {...props} ref={ref} type="button" onClick={() => remove(index)} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
type NewChipInputProps = {
|
||||||
|
asChild?: boolean;
|
||||||
|
} & Omit<InputHTMLAttributes<HTMLInputElement>, "onKeyDown" | "disabled" | "readOnly">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @beta
|
||||||
|
* @since 2.44.0
|
||||||
|
*/
|
||||||
|
export const NewChipInput = React.forwardRef<HTMLInputElement, NewChipInputProps>(({ asChild, ...props }, ref) => {
|
||||||
|
const { add, value, disabled, readOnly } = useContext(ChipInputContext);
|
||||||
|
const handleKeyDown = useCallback<KeyboardEventHandler<HTMLInputElement>>(
|
||||||
|
(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
const newValue = e.currentTarget.value.trim();
|
||||||
|
if (newValue && !value?.includes(newValue)) {
|
||||||
|
add(newValue);
|
||||||
|
e.currentTarget.value = "";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[add, value]
|
||||||
|
);
|
||||||
|
const Comp = asChild ? Slot : "input";
|
||||||
|
return <Comp {...props} onKeyDown={handleKeyDown} readOnly={readOnly} disabled={disabled} ref={ref} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
type ChipProps = { asChild?: boolean } & LiHTMLAttributes<HTMLLIElement>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @beta
|
||||||
|
* @since 2.44.0
|
||||||
|
*/
|
||||||
|
export const Chip = React.forwardRef<HTMLLIElement, ChipProps>(({ asChild, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "li";
|
||||||
|
|
||||||
|
return <Comp {...props} ref={ref} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value?: string[];
|
||||||
|
onChange?: (newValue: string[]) => void;
|
||||||
|
readOnly?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
} & Omit<HTMLAttributes<HTMLUListElement>, "onChange">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @beta
|
||||||
|
* @since 2.44.0
|
||||||
|
*/
|
||||||
|
const ChipInput = React.forwardRef<HTMLUListElement, Props>(
|
||||||
|
({ children, value = [], disabled, readOnly, onChange, ...props }, ref) => {
|
||||||
|
const isInactive = useMemo(() => disabled || readOnly, [disabled, readOnly]);
|
||||||
|
const add = useCallback(
|
||||||
|
(newValue: string) => !isInactive && onChange && onChange([...value, newValue]),
|
||||||
|
[isInactive, onChange, value]
|
||||||
|
);
|
||||||
|
const remove = useCallback(
|
||||||
|
(index: number) => !isInactive && onChange && onChange(value?.filter((_, itdx) => itdx !== index)),
|
||||||
|
[isInactive, onChange, value]
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<ChipInputContext.Provider
|
||||||
|
value={useMemo(
|
||||||
|
() => ({
|
||||||
|
value,
|
||||||
|
disabled,
|
||||||
|
readOnly,
|
||||||
|
add,
|
||||||
|
remove,
|
||||||
|
}),
|
||||||
|
[add, disabled, readOnly, remove, value]
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ul {...props} ref={ref}>
|
||||||
|
{children}
|
||||||
|
</ul>
|
||||||
|
</ChipInputContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Object.assign(ChipInput, {
|
||||||
|
Chip: Object.assign(Chip, {
|
||||||
|
Delete: ChipDelete,
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -28,6 +28,7 @@ import ControlledInputField from "./input/ControlledInputField";
|
|||||||
import ControlledCheckboxField from "./checkbox/ControlledCheckboxField";
|
import ControlledCheckboxField from "./checkbox/ControlledCheckboxField";
|
||||||
import ControlledSecretConfirmationField from "./input/ControlledSecretConfirmationField";
|
import ControlledSecretConfirmationField from "./input/ControlledSecretConfirmationField";
|
||||||
import ControlledSelectField from "./select/ControlledSelectField";
|
import ControlledSelectField from "./select/ControlledSelectField";
|
||||||
|
import ControlledChipInputField from "./chip-input/ControlledChipInputField";
|
||||||
import { ScmFormListContextProvider } from "./ScmFormListContext";
|
import { ScmFormListContextProvider } from "./ScmFormListContext";
|
||||||
import ControlledList from "./list/ControlledList";
|
import ControlledList from "./list/ControlledList";
|
||||||
import ControlledTable from "./table/ControlledTable";
|
import ControlledTable from "./table/ControlledTable";
|
||||||
@@ -37,6 +38,7 @@ import { ScmNestedFormPathContextProvider } from "./FormPathContext";
|
|||||||
|
|
||||||
export { default as ConfigurationForm } from "./ConfigurationForm";
|
export { default as ConfigurationForm } from "./ConfigurationForm";
|
||||||
export { default as SelectField } from "./select/SelectField";
|
export { default as SelectField } from "./select/SelectField";
|
||||||
|
export { default as ChipInputField } from "./chip-input/ChipInputField";
|
||||||
export { default as Select } from "./select/Select";
|
export { default as Select } from "./select/Select";
|
||||||
export * from "./resourceHooks";
|
export * from "./resourceHooks";
|
||||||
|
|
||||||
@@ -53,4 +55,5 @@ export const Form = Object.assign(FormCmp, {
|
|||||||
Table: Object.assign(ControlledTable, {
|
Table: Object.assign(ControlledTable, {
|
||||||
Column: ControlledColumn,
|
Column: ControlledColumn,
|
||||||
}),
|
}),
|
||||||
|
ChipInput: ControlledChipInputField,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,12 @@
|
|||||||
"reset": "Leeren",
|
"reset": "Leeren",
|
||||||
"discardChanges": "Änderungen verwerfen",
|
"discardChanges": "Änderungen verwerfen",
|
||||||
"submit-success-notification": "Einstellungen wurden erfolgreich geändert!",
|
"submit-success-notification": "Einstellungen wurden erfolgreich geändert!",
|
||||||
|
"chipList": {
|
||||||
|
"delete": "'{{item}}' löschen",
|
||||||
|
"input": {
|
||||||
|
"description": "Drücke 'Eingabetaste' um den Wert der Liste hinzuzufügen."
|
||||||
|
}
|
||||||
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"headers": {
|
"headers": {
|
||||||
"action": {
|
"action": {
|
||||||
|
|||||||
@@ -7,6 +7,12 @@
|
|||||||
"reset": "Clear",
|
"reset": "Clear",
|
||||||
"discardChanges": "Discard changes",
|
"discardChanges": "Discard changes",
|
||||||
"submit-success-notification": "Configuration changed successfully!",
|
"submit-success-notification": "Configuration changed successfully!",
|
||||||
|
"chipList": {
|
||||||
|
"delete": "Delete '{{item}}'",
|
||||||
|
"input": {
|
||||||
|
"description": "Press enter to add value to list."
|
||||||
|
}
|
||||||
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"headers": {
|
"headers": {
|
||||||
"action": {
|
"action": {
|
||||||
|
|||||||
31
yarn.lock
31
yarn.lock
@@ -2411,6 +2411,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
|
|
||||||
|
"@radix-ui/react-compose-refs@1.0.1":
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz#7ed868b66946aa6030e580b1ffca386dd4d21989"
|
||||||
|
integrity sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
|
||||||
"@radix-ui/react-context@1.0.0":
|
"@radix-ui/react-context@1.0.0":
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.0.tgz#f38e30c5859a9fb5e9aa9a9da452ee3ed9e0aee0"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.0.tgz#f38e30c5859a9fb5e9aa9a9da452ee3ed9e0aee0"
|
||||||
@@ -2587,6 +2594,14 @@
|
|||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
"@radix-ui/react-slot" "1.0.1"
|
"@radix-ui/react-slot" "1.0.1"
|
||||||
|
|
||||||
|
"@radix-ui/react-primitive@1.0.3":
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz#d49ea0f3f0b2fe3ab1cb5667eb03e8b843b914d0"
|
||||||
|
integrity sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@radix-ui/react-slot" "1.0.2"
|
||||||
|
|
||||||
"@radix-ui/react-roving-focus@1.0.3":
|
"@radix-ui/react-roving-focus@1.0.3":
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.3.tgz#0b4f4f9bd509f4510079e9e0734a734fd17cdce3"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.3.tgz#0b4f4f9bd509f4510079e9e0734a734fd17cdce3"
|
||||||
@@ -2611,6 +2626,14 @@
|
|||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
"@radix-ui/react-compose-refs" "1.0.0"
|
"@radix-ui/react-compose-refs" "1.0.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-slot@1.0.2", "@radix-ui/react-slot@^1.0.1":
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.2.tgz#a9ff4423eade67f501ffb32ec22064bc9d3099ab"
|
||||||
|
integrity sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@radix-ui/react-compose-refs" "1.0.1"
|
||||||
|
|
||||||
"@radix-ui/react-tooltip@1.0.2":
|
"@radix-ui/react-tooltip@1.0.2":
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.0.2.tgz#8e10b075767f785bf013146fdc954ac6885efda3"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.0.2.tgz#8e10b075767f785bf013146fdc954ac6885efda3"
|
||||||
@@ -2684,6 +2707,14 @@
|
|||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
"@radix-ui/react-primitive" "1.0.1"
|
"@radix-ui/react-primitive" "1.0.1"
|
||||||
|
|
||||||
|
"@radix-ui/react-visually-hidden@^1.0.3":
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz#51aed9dd0fe5abcad7dee2a234ad36106a6984ac"
|
||||||
|
integrity sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@radix-ui/react-primitive" "1.0.3"
|
||||||
|
|
||||||
"@radix-ui/rect@1.0.0":
|
"@radix-ui/rect@1.0.0":
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.0.0.tgz#0dc8e6a829ea2828d53cbc94b81793ba6383bf3c"
|
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.0.0.tgz#0dc8e6a829ea2828d53cbc94b81793ba6383bf3c"
|
||||||
|
|||||||
Reference in New Issue
Block a user