UNPKG

@awsui/components-react

Version:

On July 19th, 2022, we launched [Cloudscape Design System](https://cloudscape.design). Cloudscape is an evolution of AWS-UI. It consists of user interface guidelines, front-end components, design resources, and development tools for building intuitive, en

177 lines • 14.7 kB
import { __rest } from "tslib"; // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import React, { useCallback, useImperativeHandle, useLayoutEffect, useMemo, useRef } from 'react'; import clsx from 'clsx'; import { useStableCallback } from '@awsui/component-toolkit/internal'; import InternalAttributeEditor from '../attribute-editor/internal'; import InternalBox from '../box/internal'; import { FormFieldError } from '../form-field/internal'; import { useInternalI18n } from '../i18n/context'; import { getBaseProps } from '../internal/base-component'; import { fireNonCancelableEvent } from '../internal/events'; import useBaseComponent from '../internal/hooks/use-base-component'; import { applyDisplayName } from '../internal/utils/apply-display-name'; import InternalLiveRegion from '../live-region/internal'; import InternalStatusIndicator from '../status-indicator/internal'; import { TagControl, UndoButton } from './internal'; import { findIndex, useMemoizedArray } from './utils'; import { getTagsDiff } from './utils'; import { validate } from './validation'; import styles from './styles.css.js'; export { getTagsDiff }; const isItemRemovable = ({ tag }) => !tag.markedForRemoval; const TagEditor = React.forwardRef((_a, ref) => { var _b, _c, _d, _e, _f, _g; var { tags = [], i18nStrings, loading = false, tagLimit = 50, allowedCharacterPattern, keysRequest, valuesRequest, onChange } = _a, restProps = __rest(_a, ["tags", "i18nStrings", "loading", "tagLimit", "allowedCharacterPattern", "keysRequest", "valuesRequest", "onChange"]); const baseComponentProps = useBaseComponent('TagEditor', { props: { tagLimit, allowedCharacterPattern }, }); const i18n = useInternalI18n('tag-editor'); const remainingTags = tagLimit - tags.filter(tag => !tag.markedForRemoval).length; const attributeEditorRef = useRef(null); const keyInputRefs = useRef([]); const valueInputRefs = useRef([]); const undoButtonRefs = useRef([]); const initialKeyOptionsRef = useRef([]); const keyDirtyStateRef = useRef([]); const focusEventRef = useRef(); useLayoutEffect(() => { var _a; (_a = focusEventRef.current) === null || _a === void 0 ? void 0 : _a.apply(undefined); focusEventRef.current = undefined; }); const errors = validate(tags, keyDirtyStateRef.current, i18n, i18nStrings, allowedCharacterPattern ? new RegExp(allowedCharacterPattern) : undefined); const internalTags = useMemoizedArray(tags.map((tag, i) => ({ tag, error: errors[i] })), (prev, next) => { var _a, _b, _c, _d; return prev.tag === next.tag && ((_a = prev.error) === null || _a === void 0 ? void 0 : _a.key) === ((_b = next.error) === null || _b === void 0 ? void 0 : _b.key) && ((_c = prev.error) === null || _c === void 0 ? void 0 : _c.value) === ((_d = next.error) === null || _d === void 0 ? void 0 : _d.value); }); useImperativeHandle(ref, () => ({ focus() { var _a, _b; const errorIndex = findIndex(internalTags, ({ error }) => (error === null || error === void 0 ? void 0 : error.key) || (error === null || error === void 0 ? void 0 : error.value)); if (errorIndex !== -1) { const refArray = ((_a = internalTags[errorIndex].error) === null || _a === void 0 ? void 0 : _a.key) ? keyInputRefs : valueInputRefs; (_b = refArray.current[errorIndex]) === null || _b === void 0 ? void 0 : _b.focus(); } }, }), [internalTags]); const validateAndFire = useCallback((newTags) => { fireNonCancelableEvent(onChange, { tags: newTags, valid: !validate(newTags, keyDirtyStateRef.current, i18n, i18nStrings, allowedCharacterPattern ? new RegExp(allowedCharacterPattern) : undefined).some(error => error), }); }, [onChange, i18n, i18nStrings, allowedCharacterPattern]); const onAddButtonClick = () => { validateAndFire([...tags, { key: '', value: '', existing: false }]); focusEventRef.current = () => { var _a; (_a = keyInputRefs.current[tags.length]) === null || _a === void 0 ? void 0 : _a.focus(); }; }; const onRemoveButtonClick = useStableCallback(({ detail }) => { var _a, _b, _c, _d, _e; const existing = tags[detail.itemIndex].existing; validateAndFire([ ...tags.slice(0, detail.itemIndex), ...(existing ? [Object.assign(Object.assign({}, tags[detail.itemIndex]), { markedForRemoval: true })] : []), ...tags.slice(detail.itemIndex + 1), ]); if (existing) { focusEventRef.current = () => { var _a; (_a = undoButtonRefs.current[detail.itemIndex]) === null || _a === void 0 ? void 0 : _a.focus(); }; } else { keyDirtyStateRef.current.splice(detail.itemIndex, 1); const nextKey = keyInputRefs.current[detail.itemIndex + 1]; if (nextKey) { // if next key is present, focus _current_ key which will be replaced by next after state update (_a = keyInputRefs.current[detail.itemIndex]) === null || _a === void 0 ? void 0 : _a.focus(); } else if (detail.itemIndex > 0) { // otherwise focus previous key/value/undo button const previousIsExisting = tags[detail.itemIndex - 1].existing; if (previousIsExisting) { if (tags[detail.itemIndex - 1].markedForRemoval) { (_b = undoButtonRefs.current[detail.itemIndex - 1]) === null || _b === void 0 ? void 0 : _b.focus(); } else { (_c = valueInputRefs.current[detail.itemIndex - 1]) === null || _c === void 0 ? void 0 : _c.focus(); } } else { (_d = keyInputRefs.current[detail.itemIndex - 1]) === null || _d === void 0 ? void 0 : _d.focus(); } } else { // or the 'add' button (_e = attributeEditorRef.current) === null || _e === void 0 ? void 0 : _e.focusAddButton(); } } }); const onKeyChange = useStableCallback((value, row) => { keyDirtyStateRef.current[row] = true; validateAndFire([...tags.slice(0, row), Object.assign(Object.assign({}, tags[row]), { key: value }), ...tags.slice(row + 1)]); }); const onKeyBlur = useStableCallback((row) => { keyDirtyStateRef.current[row] = true; // Force re-render by providing a new array reference validateAndFire([...tags]); }); const onValueChange = useStableCallback((value, row) => { validateAndFire([...tags.slice(0, row), Object.assign(Object.assign({}, tags[row]), { value }), ...tags.slice(row + 1)]); }); const onUndoRemoval = useStableCallback((row) => { validateAndFire([...tags.slice(0, row), Object.assign(Object.assign({}, tags[row]), { markedForRemoval: false }), ...tags.slice(row + 1)]); focusEventRef.current = () => { var _a; (_a = attributeEditorRef.current) === null || _a === void 0 ? void 0 : _a.focusRemoveButton(row); }; }); const definition = useMemo(() => [ { label: i18n('i18nStrings.keyHeader', i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.keyHeader), control: ({ tag }, row) => (React.createElement(TagControl, { row: row, value: tag.key, readOnly: tag.existing, limit: 200, defaultOptions: [], placeholder: i18n('i18nStrings.keyPlaceholder', i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.keyPlaceholder), errorText: i18n('i18nStrings.keysSuggestionError', i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.keysSuggestionError), loadingText: i18n('i18nStrings.keysSuggestionLoading', i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.keysSuggestionLoading), suggestionText: i18n('i18nStrings.keySuggestion', i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.keySuggestion), tooManySuggestionText: i18n('i18nStrings.tooManyKeysSuggestion', i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.tooManyKeysSuggestion), enteredTextLabel: i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.enteredKeyLabel, clearAriaLabel: i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.clearAriaLabel, onRequest: keysRequest, onChange: onKeyChange, onBlur: onKeyBlur, initialOptionsRef: initialKeyOptionsRef, ref: ref => { keyInputRefs.current[row] = ref; } })), errorText: ({ error }) => error === null || error === void 0 ? void 0 : error.key, }, { label: (React.createElement("span", null, i18n('i18nStrings.valueHeader', i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.valueHeader), " -", ' ', React.createElement("i", null, i18n('i18nStrings.optional', i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.optional)))), control: ({ tag }, row) => { var _a; return tag.markedForRemoval ? (React.createElement("div", { role: "alert" }, React.createElement(InternalBox, { margin: { top: 'xxs' } }, i18n('i18nStrings.undoPrompt', i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.undoPrompt), ' ', React.createElement(UndoButton, { onClick: () => onUndoRemoval(row), ref: elem => { undoButtonRefs.current[row] = elem; } }, i18n('i18nStrings.undoButton', i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.undoButton))))) : (React.createElement(TagControl, { row: row, value: tag.value, readOnly: false, limit: 200, defaultOptions: (_a = tag.valueSuggestionOptions) !== null && _a !== void 0 ? _a : [], placeholder: i18n('i18nStrings.valuePlaceholder', i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.valuePlaceholder), errorText: i18n('i18nStrings.valuesSuggestionError', i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.valuesSuggestionError), loadingText: i18n('i18nStrings.valuesSuggestionLoading', i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.valuesSuggestionLoading), suggestionText: i18n('i18nStrings.valueSuggestion', i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.valueSuggestion), tooManySuggestionText: i18n('i18nStrings.tooManyValuesSuggestion', i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.tooManyValuesSuggestion), enteredTextLabel: i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.enteredValueLabel, clearAriaLabel: i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.clearAriaLabel, filteringKey: tag.key, onRequest: valuesRequest && (value => valuesRequest(tag.key, value)), onChange: onValueChange, ref: ref => { valueInputRefs.current[row] = ref; } })); }, errorText: ({ error }) => error === null || error === void 0 ? void 0 : error.value, }, ], [i18n, i18nStrings, keysRequest, onKeyChange, onKeyBlur, valuesRequest, onValueChange, onUndoRemoval]); const forwardedI18nStrings = useMemo(() => ({ errorIconAriaLabel: i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.errorIconAriaLabel, itemRemovedAriaLive: i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.itemRemovedAriaLive, removeButtonAriaLabel: i18n('i18nStrings.removeButtonAriaLabel', (i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.removeButtonAriaLabel) && (({ tag }) => i18nStrings.removeButtonAriaLabel(tag)), format => ({ tag }) => format({ tag__key: tag.key })), }), [i18nStrings, i18n]); if (loading) { return (React.createElement("div", { className: styles.root, ref: baseComponentProps.__internalRootRef }, React.createElement(InternalStatusIndicator, { className: styles.loading, type: "loading" }, React.createElement(InternalLiveRegion, { tagName: "span" }, i18n('i18nStrings.loading', i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.loading))))); } const baseProps = getBaseProps(restProps); return (React.createElement(InternalAttributeEditor, Object.assign({}, baseProps, baseComponentProps, { ref: attributeEditorRef, className: clsx(styles.root, baseProps.className), items: internalTags, isItemRemovable: isItemRemovable, onAddButtonClick: onAddButtonClick, onRemoveButtonClick: onRemoveButtonClick, addButtonText: (_b = i18n('i18nStrings.addButton', i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.addButton)) !== null && _b !== void 0 ? _b : '', removeButtonText: i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.removeButton, disableAddButton: remainingTags <= 0, empty: i18n('i18nStrings.emptyTags', i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.emptyTags), additionalInfo: remainingTags < 0 ? (React.createElement(FormFieldError, { errorIconAriaLabel: i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.errorIconAriaLabel }, (_d = i18n('i18nStrings.tagLimitExceeded', (_c = i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.tagLimitExceeded) === null || _c === void 0 ? void 0 : _c.call(i18nStrings, tagLimit), format => format({ tagLimit }))) !== null && _d !== void 0 ? _d : '')) : remainingTags === 0 ? ((_f = i18n('i18nStrings.tagLimitReached', (_e = i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.tagLimitReached) === null || _e === void 0 ? void 0 : _e.call(i18nStrings, tagLimit), format => format({ tagLimit }))) !== null && _f !== void 0 ? _f : '') : (i18n('i18nStrings.tagLimit', (_g = i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.tagLimit) === null || _g === void 0 ? void 0 : _g.call(i18nStrings, remainingTags, tagLimit), format => format({ tagLimitAvailable: `${remainingTags === tagLimit}`, availableTags: remainingTags, tagLimit }))), definition: definition, i18nStrings: forwardedI18nStrings }))); }); applyDisplayName(TagEditor, 'TagEditor'); export default TagEditor; //# sourceMappingURL=index.js.map