@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
JavaScript
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