Skip to content

Commit 5ff077e

Browse files
committed
[SharedUX][A11y] Fix tag selector missing error text (#241443)
Closes #237856 ## Summary - Added differentiated error messages for the `TagSelector` for the following use cases: - Tag doesn't exist: - If user has permissions, prompt to create it - If they don't, prompt to select one from the list - Tag exists, prompt to select it - Tag exists and it is already selected - This change should fix all occurrences of the missing error message for the `TagSelector`. - For the Settings flyout, instead of directly using the `TagSelector` we are now using the wrapper containing this validation logic, there should be no other changes. ### Testing Flyout Settings: https://github.com/user-attachments/assets/461772cc-40d5-43c2-8c7d-e0571d9f93f8 Dashboard Details: https://github.com/user-attachments/assets/3005cd63-9775-484a-804f-de9bcb2c381f Error announcement on VO: <img width="642" height="673" alt="Screenshot 2025-10-31 at 12 04 46" src="https://github.com/user-attachments/assets/7e387d5e-2df2-4968-8fd1-10daf03db6fd" /> Co-authored-by: Elastic Machine <[email protected]> (cherry picked from commit 9eb3ad3) # Conflicts: # src/platform/plugins/shared/dashboard/public/dashboard_renderer/settings/settings_flyout.tsx
1 parent 74ea618 commit 5ff077e

File tree

7 files changed

+61
-20
lines changed

7 files changed

+61
-20
lines changed

src/platform/plugins/shared/dashboard/public/dashboard_renderer/settings/settings_flyout.tsx

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -126,19 +126,10 @@ export const DashboardSettingsFlyout = ({ onClose, ariaLabelledBy }: DashboardSe
126126
if (!savedObjectsTaggingApi) return;
127127

128128
return (
129-
<EuiFormRow
130-
label={
131-
<FormattedMessage
132-
id="dashboard.embeddableApi.showSettings.flyout.form.tagsFormRowLabel"
133-
defaultMessage="Tags"
134-
/>
135-
}
136-
>
137-
<savedObjectsTaggingApi.ui.components.TagSelector
138-
selected={localSettings.tags}
139-
onTagsSelected={(selectedTags) => updateDashboardSetting({ tags: selectedTags })}
140-
/>
141-
</EuiFormRow>
129+
<savedObjectsTaggingApi.ui.components.SavedObjectSaveModalTagSelector
130+
initialSelection={localSettings.tags}
131+
onTagsSelected={(selectedTags) => updateDashboardSetting({ tags: selectedTags })}
132+
/>
142133
);
143134
};
144135

x-pack/platform/plugins/private/translations/translations/de-DE.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1474,7 +1474,6 @@
14741474
"dashboard.embeddableApi.showSettings.flyout.form.syncColorsBetweenPanelsSwitchLabel": "Farbpaletten zwischen den Bedienfeldern synchronisieren",
14751475
"dashboard.embeddableApi.showSettings.flyout.form.syncCursorBetweenPanelsSwitchLabel": "Cursor über alle Felder hinweg synchronisieren",
14761476
"dashboard.embeddableApi.showSettings.flyout.form.syncTooltipsBetweenPanelsSwitchLabel": "Synchrone Tooltips für alle Bedienfelder",
1477-
"dashboard.embeddableApi.showSettings.flyout.form.tagsFormRowLabel": "Tags",
14781477
"dashboard.embeddableApi.showSettings.flyout.form.useMarginsBetweenPanelsSwitchLabel": "Verwenden Sie Abstände zwischen den Panels",
14791478
"dashboard.embeddableApi.showSettings.flyout.formRow.syncAcrossPanelsLabel": "Über alle Felder hinweg synchronisieren",
14801479
"dashboard.embeddableApi.showSettings.flyout.title": "Dashboard-Einstellungen",

x-pack/platform/plugins/private/translations/translations/fr-FR.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1491,7 +1491,6 @@
14911491
"dashboard.embeddableApi.showSettings.flyout.form.syncColorsBetweenPanelsSwitchLabel": "Synchroniser les palettes de couleur de tous les panneaux",
14921492
"dashboard.embeddableApi.showSettings.flyout.form.syncCursorBetweenPanelsSwitchLabel": "Synchroniser le curseur de tous les panneaux",
14931493
"dashboard.embeddableApi.showSettings.flyout.form.syncTooltipsBetweenPanelsSwitchLabel": "Synchroniser les infobulles de tous les panneaux",
1494-
"dashboard.embeddableApi.showSettings.flyout.form.tagsFormRowLabel": "Balises",
14951494
"dashboard.embeddableApi.showSettings.flyout.form.useMarginsBetweenPanelsSwitchLabel": "Utiliser des marges entre les panneaux",
14961495
"dashboard.embeddableApi.showSettings.flyout.formRow.syncAcrossPanelsLabel": "Synchroniser sur tous les panneaux",
14971496
"dashboard.embeddableApi.showSettings.flyout.title": "Paramètres du tableau de bord",

x-pack/platform/plugins/private/translations/translations/ja-JP.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1494,7 +1494,6 @@
14941494
"dashboard.embeddableApi.showSettings.flyout.form.syncColorsBetweenPanelsSwitchLabel": "パネル全体でカラーパレットを同期",
14951495
"dashboard.embeddableApi.showSettings.flyout.form.syncCursorBetweenPanelsSwitchLabel": "パネル全体でカーソルを同期",
14961496
"dashboard.embeddableApi.showSettings.flyout.form.syncTooltipsBetweenPanelsSwitchLabel": "パネル間でツールチップを同期",
1497-
"dashboard.embeddableApi.showSettings.flyout.form.tagsFormRowLabel": "タグ",
14981497
"dashboard.embeddableApi.showSettings.flyout.form.useMarginsBetweenPanelsSwitchLabel": "パネルの間に余白を使用",
14991498
"dashboard.embeddableApi.showSettings.flyout.formRow.syncAcrossPanelsLabel": "パネル全体で同期",
15001499
"dashboard.embeddableApi.showSettings.flyout.title": "ダッシュボード設定",

x-pack/platform/plugins/private/translations/translations/zh-CN.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1488,7 +1488,6 @@
14881488
"dashboard.embeddableApi.showSettings.flyout.form.syncColorsBetweenPanelsSwitchLabel": "在面板之间同步调色板",
14891489
"dashboard.embeddableApi.showSettings.flyout.form.syncCursorBetweenPanelsSwitchLabel": "在面板之间同步光标",
14901490
"dashboard.embeddableApi.showSettings.flyout.form.syncTooltipsBetweenPanelsSwitchLabel": "在面板之间同步工具提示",
1491-
"dashboard.embeddableApi.showSettings.flyout.form.tagsFormRowLabel": "标签",
14921491
"dashboard.embeddableApi.showSettings.flyout.form.useMarginsBetweenPanelsSwitchLabel": "在面板间使用边距",
14931492
"dashboard.embeddableApi.showSettings.flyout.formRow.syncAcrossPanelsLabel": "跨面板同步",
14941493
"dashboard.embeddableApi.showSettings.flyout.title": "仪表板设置",

x-pack/platform/plugins/shared/saved_objects_tagging/public/components/base/tag_selector.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export const TagSelector: FC<TagSelectorProps> = ({
113113
allowCreate,
114114
openCreateModal,
115115
fullWidth = true,
116-
...otherProps
116+
...comboBoxProps
117117
}) => {
118118
const [currentSearch, setCurrentSearch] = useState('');
119119

@@ -175,16 +175,26 @@ export const TagSelector: FC<TagSelectorProps> = ({
175175
[selected, onTagsSelected, openCreateModal, currentSearch]
176176
);
177177

178+
const { onSearchChange: onSearchChangeProp, ...restProps } = comboBoxProps;
179+
180+
const handleOnSearchChange = useCallback(
181+
(searchValue: string) => {
182+
setCurrentSearch(searchValue);
183+
onSearchChangeProp?.(searchValue);
184+
},
185+
[onSearchChangeProp]
186+
);
187+
178188
return (
179189
<EuiComboBox<Tag | CreateOption>
180190
placeholder={''}
181191
options={options}
182192
selectedOptions={selectedOptions}
183-
onSearchChange={setCurrentSearch}
193+
onSearchChange={handleOnSearchChange}
184194
onChange={onChange}
185195
renderOption={renderOption}
186196
fullWidth={fullWidth}
187-
{...otherProps}
197+
{...restProps}
188198
/>
189199
);
190200
};

x-pack/platform/plugins/shared/saved_objects_tagging/public/components/connected/saved_object_save_modal_tag_selector.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import useObservable from 'react-use/lib/useObservable';
1111
import { EuiFormRow, EuiText } from '@elastic/eui';
1212
import { FormattedMessage } from '@kbn/i18n-react';
1313
import type { SavedObjectSaveModalTagSelectorComponentProps } from '@kbn/saved-objects-tagging-oss-plugin/public';
14+
import { i18n } from '@kbn/i18n';
1415
import type { TagsCapabilities } from '../../../common';
1516
import { TagSelector } from '../base';
1617
import type { ITagsCache } from '../../services';
@@ -35,6 +36,44 @@ export const getConnectedSavedObjectModalTagSelectorComponent = ({
3536
}: SavedObjectSaveModalTagSelectorComponentProps) => {
3637
const tags = useObservable(cache.getState$(), cache.getState());
3738
const [selected, setSelected] = useState<string[]>(initialSelection);
39+
const [searchValue, setSearchValue] = useState('');
40+
const [touched, setTouched] = useState(false);
41+
42+
const normalizedSearchValue = searchValue.trim().toLowerCase();
43+
44+
const exactTagMatch = !!normalizedSearchValue
45+
? tags.find((tag) => tag.name.toLowerCase() === normalizedSearchValue)
46+
: undefined;
47+
48+
const isTagAlreadyCreated = !!exactTagMatch;
49+
const isTagAlreadySelected = isTagAlreadyCreated && selected.includes(exactTagMatch.id);
50+
const noMatchingTag = !!normalizedSearchValue && !isTagAlreadyCreated;
51+
const isInvalid = touched && (noMatchingTag || isTagAlreadyCreated || isTagAlreadySelected);
52+
53+
const getErrorMessage = () => {
54+
if (isTagAlreadySelected) {
55+
return i18n.translate('xpack.savedObjectsTagging.uiApi.saveModal.alreadySelectedHint', {
56+
defaultMessage: 'Tag "{searchValue}" is already selected.',
57+
values: { searchValue },
58+
});
59+
}
60+
if (isTagAlreadyCreated) {
61+
return i18n.translate('xpack.savedObjectsTagging.uiApi.saveModal.exactTagMatchHint', {
62+
defaultMessage: 'Tag "{searchValue}" already exists. Select it from the existing tags.',
63+
values: { searchValue },
64+
});
65+
}
66+
return capabilities.create
67+
? i18n.translate('xpack.savedObjectsTagging.uiApi.saveModal.noMatchingTagCreateHint', {
68+
defaultMessage:
69+
'No tags match "{searchValue}". Select an existing tag or create a new one.',
70+
values: { searchValue },
71+
})
72+
: i18n.translate('xpack.savedObjectsTagging.uiApi.saveModal.noMatchingTagHint', {
73+
defaultMessage: 'No tags match "{searchValue}".',
74+
values: { searchValue },
75+
});
76+
};
3877

3978
const setSelectedInternal = useCallback(
4079
(newSelection: string[]) => {
@@ -63,6 +102,8 @@ export const getConnectedSavedObjectModalTagSelectorComponent = ({
63102
</EuiText>
64103
)
65104
}
105+
isInvalid={isInvalid}
106+
error={isInvalid ? getErrorMessage() : undefined}
66107
>
67108
<TagSelector
68109
selected={selected}
@@ -71,6 +112,9 @@ export const getConnectedSavedObjectModalTagSelectorComponent = ({
71112
data-test-subj="savedObjectTagSelector"
72113
allowCreate={capabilities.create}
73114
openCreateModal={openCreateModal}
115+
onSearchChange={setSearchValue}
116+
onBlur={() => setTouched(true)}
117+
isInvalid={isInvalid}
74118
{...rest}
75119
/>
76120
</EuiFormRow>

0 commit comments

Comments
 (0)