From 402ce89dc27c934f3c1c8a53641f5e79e828cfbd Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 7 Jul 2026 11:02:25 -0400 Subject: [PATCH 1/6] feat(ui): Add clear button to API keys search input Adds a clear (X) button anchored to the trailing edge of the API keys search input. The button stays mounted and toggles via opacity + inert to avoid layout shift, refocuses the input on keyboard clear, and shares trailing-button chrome with the password toggle via a new common.inputTrailingButton helper. Themeable via apiKeysSearchClearButton and labeled via apiKeys.action__clearSearch. --- .changeset/api-keys-search-clear-button.md | 7 +++ packages/localizations/src/en-US.ts | 1 + packages/shared/src/types/localization.ts | 1 + packages/ui/src/baseTheme.ts | 22 +++++---- .../ui/src/components/APIKeys/APIKeys.tsx | 47 +++++++++++++++++-- .../src/customizables/elementDescriptors.ts | 1 + packages/ui/src/elements/PasswordInput.tsx | 18 +------ packages/ui/src/internal/appearance.ts | 1 + packages/ui/src/styledSystem/common.ts | 21 +++++++++ 9 files changed, 90 insertions(+), 29 deletions(-) create mode 100644 .changeset/api-keys-search-clear-button.md diff --git a/.changeset/api-keys-search-clear-button.md b/.changeset/api-keys-search-clear-button.md new file mode 100644 index 00000000000..4168a106b8d --- /dev/null +++ b/.changeset/api-keys-search-clear-button.md @@ -0,0 +1,7 @@ +--- +'@clerk/localizations': patch +'@clerk/shared': patch +'@clerk/ui': patch +--- + +Add a clear button to the `` search input for quickly resetting the current query. The button is themeable via the new `apiKeysSearchClearButton` appearance element and its label can be customized with the new `apiKeys.action__clearSearch` localization key. diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 6f4c008a9ae..e1c2ad0e809 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -4,6 +4,7 @@ export const enUS: LocalizationResource = { locale: 'en-US', apiKeys: { action__add: 'Add new key', + action__clearSearch: 'Clear search', action__search: 'Search keys', copySecret: { formButtonPrimary__copyAndClose: 'Copy & Close', diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 8c8f12fb467..130f81355da 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1880,6 +1880,7 @@ export type __internal_LocalizationResource = { formButtonPrimary__add: LocalizationValue; menuAction__revoke: LocalizationValue; action__search: LocalizationValue; + action__clearSearch: LocalizationValue; action__add: LocalizationValue; tableHeader__name: LocalizationValue; tableHeader__lastUsed: LocalizationValue; diff --git a/packages/ui/src/baseTheme.ts b/packages/ui/src/baseTheme.ts index fef86197013..2bd73403ea5 100644 --- a/packages/ui/src/baseTheme.ts +++ b/packages/ui/src/baseTheme.ts @@ -75,6 +75,18 @@ const checkboxBaseStyles = (theme: InternalTheme) => ({ }, }); +// Clerk theme tightens the trailing-button geometry (see `common.inputTrailingButton` for the +// simple-theme base) so it sits flush inside the denser default-theme input. Apply to every +// button anchored to the trailing edge of an input. +const inputTrailingButtonStyles = (theme: InternalTheme) => ({ + insetInlineEnd: theme.space.$0x75, + insetBlock: theme.space.$0x75, + borderRadius: `calc(${theme.radii.$md} - ${theme.space.$0x75})`, + '&::before': { + inset: `calc(${theme.space.$0x75} * -1)`, + }, +}); + const inputStyles = (theme: InternalTheme) => ({ borderWidth: 0, ...inputShadowStyles(theme, { @@ -305,14 +317,8 @@ const clerkTheme: Appearance = { borderWidth: 0, boxShadow: `0px 0px 2px 0px rgba(0, 0, 0, 0.08), 0px 1px 2px 0px rgba(25, 28, 33, 0.12), 0px 0px 0px 1px ${theme.colors.$borderAlpha100}`, }, - formFieldInputShowPasswordButton: { - insetInlineEnd: theme.space.$0x75, - insetBlock: theme.space.$0x75, - borderRadius: `calc(${theme.radii.$md} - ${theme.space.$0x75})`, - '&::before': { - inset: `calc(${theme.space.$0x75} * -1)`, - }, - }, + formFieldInputShowPasswordButton: inputTrailingButtonStyles(theme), + apiKeysSearchClearButton: inputTrailingButtonStyles(theme), }; }, } satisfies Appearance; diff --git a/packages/ui/src/components/APIKeys/APIKeys.tsx b/packages/ui/src/components/APIKeys/APIKeys.tsx index bf018663e22..c77bcf72f6b 100644 --- a/packages/ui/src/components/APIKeys/APIKeys.tsx +++ b/packages/ui/src/components/APIKeys/APIKeys.tsx @@ -1,8 +1,9 @@ import { isClerkAPIResponseError } from '@clerk/shared/error'; +import { inertProps } from '@clerk/shared/inert'; import { isOrganizationId } from '@clerk/shared/internal/clerk-js/organization'; -import { useAPIKeys, __internal_useOrganizationBase, useClerk, useUser } from '@clerk/shared/react'; +import { __internal_useOrganizationBase, useAPIKeys, useClerk, useUser } from '@clerk/shared/react'; import type { APIKeyResource } from '@clerk/shared/types'; -import { lazy, useState } from 'react'; +import { lazy, useRef, useState } from 'react'; import { useProtect } from '@/ui/common'; import { useAPIKeysContext, withCoreUserGuard } from '@/ui/contexts'; @@ -19,12 +20,13 @@ import { } from '@/ui/customizables'; import { Action } from '@/ui/elements/Action'; import { useCardState, withCardStateProvider } from '@/ui/elements/contexts'; +import { IconButton } from '@/ui/elements/IconButton'; import { InputWithIcon } from '@/ui/elements/InputWithIcon'; import { Pagination } from '@/ui/elements/Pagination'; import { useDebounce } from '@/ui/hooks'; +import { Close, MagnifyingGlass } from '@/ui/icons'; +import { common, mqu } from '@/ui/styledSystem'; import { handleError } from '@/ui/utils/errorHandler'; -import { MagnifyingGlass } from '@/ui/icons'; -import { mqu } from '@/ui/styledSystem'; import { APIKeysTable } from './ApiKeysTable'; import type { OnCreateParams } from './CreateAPIKeyForm'; @@ -58,6 +60,17 @@ export const APIKeysPage = ({ subject, perPage, revokeModalRoot }: APIKeysPagePr const canManageAPIKeys = useProtect({ permission: 'org:sys_api_keys:manage' }); const [searchValue, setSearchValue] = useState(''); + const searchInputRef = useRef(null); + + const clearSearch = (event: React.MouseEvent) => { + setSearchValue(''); + // Keyboard activation produces a synthetic click with detail === 0. In that case the button + // is about to go inert and drop out of the tab order, so return focus to the input. A real + // pointer click (detail >= 1) leaves focus alone. + if (event.detail === 0) { + searchInputRef.current?.focus(); + } + }; const debouncedSearchValue = useDebounce(searchValue, apiKeysSearchDebounceMs); const query = debouncedSearchValue.trim(); @@ -160,7 +173,10 @@ export const APIKeysPage = ({ subject, perPage, revokeModalRoot }: APIKeysPagePr }} elementDescriptor={descriptors.apiKeysHeader} > - + setSearchValue(e.target.value)} + ref={searchInputRef} elementDescriptor={descriptors.apiKeysSearchInput} + sx={t => ({ + paddingInlineEnd: t.space.$10, + '::-webkit-search-cancel-button': { + display: 'none', + }, + })} + /> + ({ + ...common.inputTrailingButton(theme), + opacity: searchValue ? 1 : 0, + transition: `opacity ${theme.transitionDuration.$fast} ${theme.transitionTiming.$common}`, + })} /> {((isOrg && canManageAPIKeys) || !isOrg) && ( diff --git a/packages/ui/src/customizables/elementDescriptors.ts b/packages/ui/src/customizables/elementDescriptors.ts index 69eca6184fe..7233fa74f89 100644 --- a/packages/ui/src/customizables/elementDescriptors.ts +++ b/packages/ui/src/customizables/elementDescriptors.ts @@ -513,6 +513,7 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([ 'apiKeysHeader', 'apiKeysSearchBox', 'apiKeysSearchInput', + 'apiKeysSearchClearButton', 'apiKeysAddButton', 'apiKeysTable', 'apiKeysCopyButton', diff --git a/packages/ui/src/elements/PasswordInput.tsx b/packages/ui/src/elements/PasswordInput.tsx index 719f0897563..93c1c2be5a1 100644 --- a/packages/ui/src/elements/PasswordInput.tsx +++ b/packages/ui/src/elements/PasswordInput.tsx @@ -7,7 +7,7 @@ import { useEnvironment } from '../contexts'; import { descriptors, Flex, Input, localizationKeys, useLocalizations } from '../customizables'; import { usePassword } from '../hooks/usePassword'; import { Eye, EyeSlash } from '../icons'; -import type { PropsOfComponent } from '../styledSystem'; +import { common, type PropsOfComponent } from '../styledSystem'; import { mergeRefs } from '../utils/mergeRefs'; import { IconButton } from './IconButton'; @@ -112,21 +112,7 @@ export const PasswordInput = forwardRef((p size='xs' tabIndex={tabIndex} onClick={() => setHidden(s => !s)} - sx={theme => ({ - position: 'absolute', - insetInlineEnd: theme.space.$1, - insetBlock: theme.space.$1, - borderRadius: `calc(${theme.radii.$md} - ${theme.space.$1})`, - color: theme.colors.$neutralAlpha400, - paddingInline: theme.space.$2, - '&::before': { - content: '""', - position: 'absolute', - inset: `calc(${theme.space.$1} * -1)`, - display: 'block', - borderRadius: theme.radii.$md, - }, - })} + sx={theme => common.inputTrailingButton(theme)} icon={hidden ? Eye : EyeSlash} /> diff --git a/packages/ui/src/internal/appearance.ts b/packages/ui/src/internal/appearance.ts index 71d20488744..00c95416900 100644 --- a/packages/ui/src/internal/appearance.ts +++ b/packages/ui/src/internal/appearance.ts @@ -649,6 +649,7 @@ export type ElementsConfig = { apiKeysHeader: WithOptions; apiKeysSearchBox: WithOptions; apiKeysSearchInput: WithOptions; + apiKeysSearchClearButton: WithOptions; apiKeysAddButton: WithOptions; apiKeysTable: WithOptions; apiKeysCopyButton: WithOptions; diff --git a/packages/ui/src/styledSystem/common.ts b/packages/ui/src/styledSystem/common.ts index 9abc7b7173c..6ffa85d2f2a 100644 --- a/packages/ui/src/styledSystem/common.ts +++ b/packages/ui/src/styledSystem/common.ts @@ -188,6 +188,26 @@ const disabled = (t: InternalTheme) => { } as const; }; +// Shared chrome for a button anchored to the trailing edge of an input (e.g. password reveal, +// search clear). The clerk theme tightens the geometry in `baseTheme.ts`; the simple theme uses +// these values as-is. Callers layer on their own icon/visibility styles. +const inputTrailingButton = (t: InternalTheme) => + ({ + position: 'absolute', + insetInlineEnd: t.space.$1, + insetBlock: t.space.$1, + borderRadius: `calc(${t.radii.$md} - ${t.space.$1})`, + color: t.colors.$neutralAlpha400, + paddingInline: t.space.$2, + '&::before': { + content: '""', + position: 'absolute', + inset: `calc(${t.space.$1} * -1)`, + display: 'block', + borderRadius: t.radii.$md, + }, + }) as const; + const centeredFlex = (display: 'flex' | 'inline-flex' = 'flex') => ({ display: display, justifyContent: 'center', @@ -264,6 +284,7 @@ export const common = { focusRing, disabled, borderColor, + inputTrailingButton, centeredFlex, maxHeightScroller, mutedBackground, From cc37a9dce5d92f6e816678490a4b77f0618f0a31 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 7 Jul 2026 11:11:22 -0400 Subject: [PATCH 2/6] refactor(ui): Move clear button into InputWithIcon and add to members search Generalizes the API keys search clear button into an opt-in onClear prop on InputWithIcon (clear button, opacity + inert toggle, keyboard refocus, native cancel hidden) and applies it to the OrganizationProfile members search. Adds the organizationProfileMembersSearchClearButton appearance element and organizationProfile.membersPage.action__clearSearch localization key. --- .changeset/api-keys-search-clear-button.md | 4 +- packages/localizations/src/en-US.ts | 1 + packages/shared/src/types/localization.ts | 1 + packages/ui/src/baseTheme.ts | 1 + .../ui/src/components/APIKeys/APIKeys.tsx | 48 ++------------ .../OrganizationProfile/MembersSearch.tsx | 12 ++++ .../src/customizables/elementDescriptors.ts | 1 + packages/ui/src/elements/InputWithIcon.tsx | 64 +++++++++++++++++-- packages/ui/src/internal/appearance.ts | 1 + 9 files changed, 87 insertions(+), 46 deletions(-) diff --git a/.changeset/api-keys-search-clear-button.md b/.changeset/api-keys-search-clear-button.md index 4168a106b8d..e5b7c31f534 100644 --- a/.changeset/api-keys-search-clear-button.md +++ b/.changeset/api-keys-search-clear-button.md @@ -4,4 +4,6 @@ '@clerk/ui': patch --- -Add a clear button to the `` search input for quickly resetting the current query. The button is themeable via the new `apiKeysSearchClearButton` appearance element and its label can be customized with the new `apiKeys.action__clearSearch` localization key. +Add a clear button to search inputs for quickly resetting the current query. It appears in the `` search and the `` members search. + +The button is themeable via the new `apiKeysSearchClearButton` and `organizationProfileMembersSearchClearButton` appearance elements, and its label can be customized with the new `apiKeys.action__clearSearch` and `organizationProfile.membersPage.action__clearSearch` localization keys. diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index e1c2ad0e809..32f47c0b352 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -996,6 +996,7 @@ export const enUS: LocalizationResource = { membersPage: { action__invite: 'Invite', action__search: 'Search', + action__clearSearch: 'Clear search', activeMembersTab: { menuAction__remove: 'Remove member', tableHeader__actions: 'Actions', diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 130f81355da..5922c9938c3 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1188,6 +1188,7 @@ export type __internal_LocalizationResource = { detailsTitle__emptyRow: LocalizationValue; action__invite: LocalizationValue; action__search: LocalizationValue; + action__clearSearch: LocalizationValue; start: { headerTitle__members: LocalizationValue; headerTitle__invitations: LocalizationValue; diff --git a/packages/ui/src/baseTheme.ts b/packages/ui/src/baseTheme.ts index 2bd73403ea5..953887b324d 100644 --- a/packages/ui/src/baseTheme.ts +++ b/packages/ui/src/baseTheme.ts @@ -319,6 +319,7 @@ const clerkTheme: Appearance = { }, formFieldInputShowPasswordButton: inputTrailingButtonStyles(theme), apiKeysSearchClearButton: inputTrailingButtonStyles(theme), + organizationProfileMembersSearchClearButton: inputTrailingButtonStyles(theme), }; }, } satisfies Appearance; diff --git a/packages/ui/src/components/APIKeys/APIKeys.tsx b/packages/ui/src/components/APIKeys/APIKeys.tsx index c77bcf72f6b..ef8cac1bd89 100644 --- a/packages/ui/src/components/APIKeys/APIKeys.tsx +++ b/packages/ui/src/components/APIKeys/APIKeys.tsx @@ -1,9 +1,8 @@ import { isClerkAPIResponseError } from '@clerk/shared/error'; -import { inertProps } from '@clerk/shared/inert'; import { isOrganizationId } from '@clerk/shared/internal/clerk-js/organization'; import { __internal_useOrganizationBase, useAPIKeys, useClerk, useUser } from '@clerk/shared/react'; import type { APIKeyResource } from '@clerk/shared/types'; -import { lazy, useRef, useState } from 'react'; +import { lazy, useState } from 'react'; import { useProtect } from '@/ui/common'; import { useAPIKeysContext, withCoreUserGuard } from '@/ui/contexts'; @@ -20,12 +19,11 @@ import { } from '@/ui/customizables'; import { Action } from '@/ui/elements/Action'; import { useCardState, withCardStateProvider } from '@/ui/elements/contexts'; -import { IconButton } from '@/ui/elements/IconButton'; import { InputWithIcon } from '@/ui/elements/InputWithIcon'; import { Pagination } from '@/ui/elements/Pagination'; import { useDebounce } from '@/ui/hooks'; -import { Close, MagnifyingGlass } from '@/ui/icons'; -import { common, mqu } from '@/ui/styledSystem'; +import { MagnifyingGlass } from '@/ui/icons'; +import { mqu } from '@/ui/styledSystem'; import { handleError } from '@/ui/utils/errorHandler'; import { APIKeysTable } from './ApiKeysTable'; @@ -60,17 +58,6 @@ export const APIKeysPage = ({ subject, perPage, revokeModalRoot }: APIKeysPagePr const canManageAPIKeys = useProtect({ permission: 'org:sys_api_keys:manage' }); const [searchValue, setSearchValue] = useState(''); - const searchInputRef = useRef(null); - - const clearSearch = (event: React.MouseEvent) => { - setSearchValue(''); - // Keyboard activation produces a synthetic click with detail === 0. In that case the button - // is about to go inert and drop out of the tab order, so return focus to the input. A real - // pointer click (detail >= 1) leaves focus alone. - if (event.detail === 0) { - searchInputRef.current?.focus(); - } - }; const debouncedSearchValue = useDebounce(searchValue, apiKeysSearchDebounceMs); const query = debouncedSearchValue.trim(); @@ -173,10 +160,7 @@ export const APIKeysPage = ({ subject, perPage, revokeModalRoot }: APIKeysPagePr }} elementDescriptor={descriptors.apiKeysHeader} > - + setSearchValue(e.target.value)} - ref={searchInputRef} + onClear={() => setSearchValue('')} + clearButtonLabel={t(localizationKeys('apiKeys.action__clearSearch'))} + clearButtonElementDescriptor={descriptors.apiKeysSearchClearButton} elementDescriptor={descriptors.apiKeysSearchInput} - sx={t => ({ - paddingInlineEnd: t.space.$10, - '::-webkit-search-cancel-button': { - display: 'none', - }, - })} - /> - ({ - ...common.inputTrailingButton(theme), - opacity: searchValue ? 1 : 0, - transition: `opacity ${theme.transitionDuration.$fast} ${theme.transitionTiming.$common}`, - })} /> {((isOrg && canManageAPIKeys) || !isOrg) && ( diff --git a/packages/ui/src/components/OrganizationProfile/MembersSearch.tsx b/packages/ui/src/components/OrganizationProfile/MembersSearch.tsx index a882e3982c2..f6262de2552 100644 --- a/packages/ui/src/components/OrganizationProfile/MembersSearch.tsx +++ b/packages/ui/src/components/OrganizationProfile/MembersSearch.tsx @@ -63,6 +63,15 @@ export const MembersSearch = ({ query, value, memberships, onSearchChange, onQue }, membersSearchDebounceMs); } + const handleClear = () => { + // Cancel any pending debounce so it can't re-trigger the query with the stale value. + if (debounceTimer.current) { + clearTimeout(debounceTimer.current); + } + onSearchChange(''); + onQueryTrigger(''); + }; + // If search is not performed on a initial page, resets pagination offset // based on the response count useEffect(() => { @@ -108,6 +117,9 @@ export const MembersSearch = ({ query, value, memberships, onSearchChange, onQue } onKeyUp={handleKeyUp} onChange={handleChange} + onClear={handleClear} + clearButtonLabel={t(localizationKeys('organizationProfile.membersPage.action__clearSearch'))} + clearButtonElementDescriptor={descriptors.organizationProfileMembersSearchClearButton} elementDescriptor={descriptors.organizationProfileMembersSearchInput} /> diff --git a/packages/ui/src/customizables/elementDescriptors.ts b/packages/ui/src/customizables/elementDescriptors.ts index 7233fa74f89..081d074ac9a 100644 --- a/packages/ui/src/customizables/elementDescriptors.ts +++ b/packages/ui/src/customizables/elementDescriptors.ts @@ -222,6 +222,7 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([ 'organizationProfileMembersSearchInputIcon', 'organizationProfileMembersSearchInput', + 'organizationProfileMembersSearchClearButton', 'organizationProfileSecuritySsoBadge', 'organizationProfileSecuritySsoDescription', diff --git a/packages/ui/src/elements/InputWithIcon.tsx b/packages/ui/src/elements/InputWithIcon.tsx index 09e7d5329f1..99c33777ab9 100644 --- a/packages/ui/src/elements/InputWithIcon.tsx +++ b/packages/ui/src/elements/InputWithIcon.tsx @@ -1,12 +1,43 @@ +import { inertProps } from '@clerk/shared/inert'; import React from 'react'; import { Box, Flex, Input } from '../customizables'; -import type { PropsOfComponent } from '../styledSystem'; +import type { ElementDescriptor } from '../customizables/elementDescriptors'; +import { Close } from '../icons'; +import { common, type PropsOfComponent } from '../styledSystem'; +import { mergeRefs } from '../utils/mergeRefs'; +import { IconButton } from './IconButton'; -type InputWithIcon = PropsOfComponent & { leftIcon?: React.ReactElement }; +type InputWithIcon = PropsOfComponent & { + leftIcon?: React.ReactElement; + /** + * When provided, renders a clear button anchored to the trailing edge of the input. The button + * stays mounted (fading via opacity + `inert`) to avoid layout shift, and calls `onClear` on + * activation — the parent is responsible for resetting `value`. Keyboard activation returns focus + * to the input. + */ + onClear?: () => void; + clearButtonLabel?: string; + clearButtonElementDescriptor?: ElementDescriptor; +}; export const InputWithIcon = React.forwardRef((props, ref) => { - const { leftIcon, sx, ...rest } = props; + const { leftIcon, sx, onClear, clearButtonLabel, clearButtonElementDescriptor, ...rest } = props; + const internalRef = React.useRef(null); + + const hasClearButton = Boolean(onClear); + const hasValue = rest.value != null && rest.value !== ''; + + const handleClear = (event: React.MouseEvent) => { + onClear?.(); + // Keyboard activation produces a synthetic click with detail === 0. The button is about to go + // inert and drop out of the tab order, so return focus to the input. A pointer click leaves + // focus alone. + if (event.detail === 0) { + internalRef.current?.focus(); + } + }; + return ( (( width: '100%', paddingInlineStart: theme.space.$10, }), + hasClearButton + ? theme => ({ + paddingInlineEnd: theme.space.$10, + '::-webkit-search-cancel-button': { + display: 'none', + }, + }) + : undefined, sx, ]} - ref={ref} + // @ts-expect-error Type mismatch between ForwardRef and RefObject due to null + ref={hasClearButton ? mergeRefs(ref, internalRef) : ref} /> + {hasClearButton ? ( + ({ + ...common.inputTrailingButton(theme), + opacity: hasValue ? 1 : 0, + transition: `opacity ${theme.transitionDuration.$fast} ${theme.transitionTiming.$common}`, + })} + /> + ) : null} ); }); diff --git a/packages/ui/src/internal/appearance.ts b/packages/ui/src/internal/appearance.ts index 00c95416900..9f8185b0642 100644 --- a/packages/ui/src/internal/appearance.ts +++ b/packages/ui/src/internal/appearance.ts @@ -362,6 +362,7 @@ export type ElementsConfig = { organizationProfileMembersSearchInputIcon: WithOptions; organizationProfileMembersSearchInput: WithOptions; + organizationProfileMembersSearchClearButton: WithOptions; organizationProfileSecuritySsoBadge: WithOptions; organizationProfileSecuritySsoDescription: WithOptions; From 3369b2bede5fd348a7c024a85227d9c70f8f3e1f Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 7 Jul 2026 11:21:25 -0400 Subject: [PATCH 3/6] refactor(ui): Introduce shared SearchInput with a single clear-button element Replaces the per-consumer clear-button descriptors (apiKeysSearchClearButton, organizationProfileMembersSearchClearButton) and localization keys with a single SearchInput element that bakes in the search icon/spinner and a trailing clear button. The clear button uses one shared searchInputClearButton appearance element and the shared searchInput.action__clear localization key, so new search inputs need no additional theming or localization surface. --- .changeset/api-keys-search-clear-button.md | 2 +- packages/localizations/src/en-US.ts | 5 +- packages/shared/src/types/localization.ts | 6 +- packages/ui/src/baseTheme.ts | 3 +- .../ui/src/components/APIKeys/APIKeys.tsx | 29 +-------- .../OrganizationProfile/MembersSearch.tsx | 26 ++------ .../src/customizables/elementDescriptors.ts | 3 +- packages/ui/src/elements/SearchInput.tsx | 62 +++++++++++++++++++ packages/ui/src/internal/appearance.ts | 3 +- 9 files changed, 81 insertions(+), 58 deletions(-) create mode 100644 packages/ui/src/elements/SearchInput.tsx diff --git a/.changeset/api-keys-search-clear-button.md b/.changeset/api-keys-search-clear-button.md index e5b7c31f534..384fc029989 100644 --- a/.changeset/api-keys-search-clear-button.md +++ b/.changeset/api-keys-search-clear-button.md @@ -6,4 +6,4 @@ Add a clear button to search inputs for quickly resetting the current query. It appears in the `` search and the `` members search. -The button is themeable via the new `apiKeysSearchClearButton` and `organizationProfileMembersSearchClearButton` appearance elements, and its label can be customized with the new `apiKeys.action__clearSearch` and `organizationProfile.membersPage.action__clearSearch` localization keys. +The button is themeable via the new shared `searchInputClearButton` appearance element, and its label can be customized with the new shared `searchInput.action__clear` localization key. diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 32f47c0b352..ffd52f8e7e0 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -4,7 +4,6 @@ export const enUS: LocalizationResource = { locale: 'en-US', apiKeys: { action__add: 'Add new key', - action__clearSearch: 'Clear search', action__search: 'Search keys', copySecret: { formButtonPrimary__copyAndClose: 'Copy & Close', @@ -861,6 +860,9 @@ export const enUS: LocalizationResource = { title: 'Signed in as {{identifier}}', }, lastAuthenticationStrategy: 'Last used', + searchInput: { + action__clear: 'Clear search', + }, maintenanceMode: "We are currently undergoing maintenance, but don't worry, it shouldn't take more than a few minutes.", membershipRole__admin: 'Admin', @@ -996,7 +998,6 @@ export const enUS: LocalizationResource = { membersPage: { action__invite: 'Invite', action__search: 'Search', - action__clearSearch: 'Clear search', activeMembersTab: { menuAction__remove: 'Remove member', tableHeader__actions: 'Actions', diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 5922c9938c3..9ca7ecee35e 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -87,6 +87,10 @@ export type __internal_LocalizationResource = { /** Label for the “Last used” badge on authentication strategies. */ lastAuthenticationStrategy: LocalizationValue; dividerText: LocalizationValue; + /** Shared strings for search inputs across the components. */ + searchInput: { + action__clear: LocalizationValue; + }; formFieldLabel__emailAddress: LocalizationValue; formFieldLabel__emailAddresses: LocalizationValue; formFieldLabel__phoneNumber: LocalizationValue; @@ -1188,7 +1192,6 @@ export type __internal_LocalizationResource = { detailsTitle__emptyRow: LocalizationValue; action__invite: LocalizationValue; action__search: LocalizationValue; - action__clearSearch: LocalizationValue; start: { headerTitle__members: LocalizationValue; headerTitle__invitations: LocalizationValue; @@ -1881,7 +1884,6 @@ export type __internal_LocalizationResource = { formButtonPrimary__add: LocalizationValue; menuAction__revoke: LocalizationValue; action__search: LocalizationValue; - action__clearSearch: LocalizationValue; action__add: LocalizationValue; tableHeader__name: LocalizationValue; tableHeader__lastUsed: LocalizationValue; diff --git a/packages/ui/src/baseTheme.ts b/packages/ui/src/baseTheme.ts index 953887b324d..4cb07b6fc57 100644 --- a/packages/ui/src/baseTheme.ts +++ b/packages/ui/src/baseTheme.ts @@ -318,8 +318,7 @@ const clerkTheme: Appearance = { boxShadow: `0px 0px 2px 0px rgba(0, 0, 0, 0.08), 0px 1px 2px 0px rgba(25, 28, 33, 0.12), 0px 0px 0px 1px ${theme.colors.$borderAlpha100}`, }, formFieldInputShowPasswordButton: inputTrailingButtonStyles(theme), - apiKeysSearchClearButton: inputTrailingButtonStyles(theme), - organizationProfileMembersSearchClearButton: inputTrailingButtonStyles(theme), + searchInputClearButton: inputTrailingButtonStyles(theme), }; }, } satisfies Appearance; diff --git a/packages/ui/src/components/APIKeys/APIKeys.tsx b/packages/ui/src/components/APIKeys/APIKeys.tsx index ef8cac1bd89..64a16abc1a3 100644 --- a/packages/ui/src/components/APIKeys/APIKeys.tsx +++ b/packages/ui/src/components/APIKeys/APIKeys.tsx @@ -6,23 +6,12 @@ import { lazy, useState } from 'react'; import { useProtect } from '@/ui/common'; import { useAPIKeysContext, withCoreUserGuard } from '@/ui/contexts'; -import { - Box, - Button, - Col, - descriptors, - Flex, - Flow, - Icon, - localizationKeys, - useLocalizations, -} from '@/ui/customizables'; +import { Box, Button, Col, descriptors, Flex, Flow, localizationKeys, useLocalizations } from '@/ui/customizables'; import { Action } from '@/ui/elements/Action'; import { useCardState, withCardStateProvider } from '@/ui/elements/contexts'; -import { InputWithIcon } from '@/ui/elements/InputWithIcon'; import { Pagination } from '@/ui/elements/Pagination'; +import { SearchInput } from '@/ui/elements/SearchInput'; import { useDebounce } from '@/ui/hooks'; -import { MagnifyingGlass } from '@/ui/icons'; import { mqu } from '@/ui/styledSystem'; import { handleError } from '@/ui/utils/errorHandler'; @@ -161,25 +150,13 @@ export const APIKeysPage = ({ subject, perPage, revokeModalRoot }: APIKeysPagePr elementDescriptor={descriptors.apiKeysHeader} > - ({ color: t.colors.$colorMutedForeground })} - /> - } value={searchValue} - type='search' - autoComplete='off' - autoCapitalize='none' - spellCheck={false} onChange={e => setSearchValue(e.target.value)} onClear={() => setSearchValue('')} - clearButtonLabel={t(localizationKeys('apiKeys.action__clearSearch'))} - clearButtonElementDescriptor={descriptors.apiKeysSearchClearButton} elementDescriptor={descriptors.apiKeysSearchInput} /> diff --git a/packages/ui/src/components/OrganizationProfile/MembersSearch.tsx b/packages/ui/src/components/OrganizationProfile/MembersSearch.tsx index f6262de2552..bcb7195e189 100644 --- a/packages/ui/src/components/OrganizationProfile/MembersSearch.tsx +++ b/packages/ui/src/components/OrganizationProfile/MembersSearch.tsx @@ -2,12 +2,10 @@ import type { useOrganization } from '@clerk/shared/react'; import type { GetMembersParams } from '@clerk/shared/types'; import { useEffect, useRef } from 'react'; -import { descriptors, Flex, Icon, localizationKeys, useLocalizations } from '@/customizables'; -import { MagnifyingGlass } from '@/icons'; -import { Spinner } from '@/primitives'; +import { descriptors, Flex, localizationKeys, useLocalizations } from '@/customizables'; import { mqu } from '@/styledSystem'; import { Animated } from '@/ui/elements/Animated'; -import { InputWithIcon } from '@/ui/elements/InputWithIcon'; +import { SearchInput } from '@/ui/elements/SearchInput'; import { ACTIVE_MEMBERS_PAGE_SIZE } from './OrganizationMembers'; @@ -97,29 +95,15 @@ export const MembersSearch = ({ query, value, memberships, onSearchChange, onQue }, }} > - - ) : ( - ({ color: t.colors.$colorMutedForeground })} - /> - ) - } + leftIconElementDescriptor={descriptors.organizationProfileMembersSearchInputIcon} onKeyUp={handleKeyUp} onChange={handleChange} onClear={handleClear} - clearButtonLabel={t(localizationKeys('organizationProfile.membersPage.action__clearSearch'))} - clearButtonElementDescriptor={descriptors.organizationProfileMembersSearchClearButton} elementDescriptor={descriptors.organizationProfileMembersSearchInput} /> diff --git a/packages/ui/src/customizables/elementDescriptors.ts b/packages/ui/src/customizables/elementDescriptors.ts index 081d074ac9a..cf21a8d62fa 100644 --- a/packages/ui/src/customizables/elementDescriptors.ts +++ b/packages/ui/src/customizables/elementDescriptors.ts @@ -177,6 +177,7 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([ 'formFieldInputShowPasswordIcon', 'formFieldInputCopyToClipboardButton', 'formFieldInputCopyToClipboardIcon', + 'searchInputClearButton', 'phoneInputBox', 'formInputGroup', @@ -222,7 +223,6 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([ 'organizationProfileMembersSearchInputIcon', 'organizationProfileMembersSearchInput', - 'organizationProfileMembersSearchClearButton', 'organizationProfileSecuritySsoBadge', 'organizationProfileSecuritySsoDescription', @@ -514,7 +514,6 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([ 'apiKeysHeader', 'apiKeysSearchBox', 'apiKeysSearchInput', - 'apiKeysSearchClearButton', 'apiKeysAddButton', 'apiKeysTable', 'apiKeysCopyButton', diff --git a/packages/ui/src/elements/SearchInput.tsx b/packages/ui/src/elements/SearchInput.tsx new file mode 100644 index 00000000000..9fbe26735be --- /dev/null +++ b/packages/ui/src/elements/SearchInput.tsx @@ -0,0 +1,62 @@ +import React from 'react'; + +import { descriptors, Icon, localizationKeys, useLocalizations } from '../customizables'; +import type { ElementDescriptor } from '../customizables/elementDescriptors'; +import { MagnifyingGlass } from '../icons'; +import { Spinner } from '../primitives'; +import type { PropsOfComponent } from '../styledSystem'; +import { InputWithIcon } from './InputWithIcon'; + +type SearchInputProps = Omit< + PropsOfComponent, + 'leftIcon' | 'type' | 'clearButtonElementDescriptor' +> & { + /** + * Called when the user clears the input. The parent is responsible for resetting `value`. + */ + onClear: () => void; + /** + * Renders a spinner in place of the search icon, e.g. while results are loading. + */ + isLoading?: boolean; + /** + * Descriptor for the leading search icon. + */ + leftIconElementDescriptor?: ElementDescriptor; +}; + +/** + * A search input preset: a magnifying-glass icon (or spinner while loading) and a trailing clear + * button that all search inputs share. The clear button uses the single `searchInputClearButton` + * appearance element and a shared clear label, so new search inputs don't introduce their own. + */ +export const SearchInput = React.forwardRef(function SearchInput( + { isLoading, leftIconElementDescriptor, clearButtonLabel, ...rest }, + ref, +) { + const { t } = useLocalizations(); + + return ( + + ) : ( + ({ color: theme.colors.$colorMutedForeground })} + /> + ) + } + clearButtonLabel={clearButtonLabel ?? t(localizationKeys('searchInput.action__clear'))} + clearButtonElementDescriptor={descriptors.searchInputClearButton} + {...rest} + /> + ); +}); diff --git a/packages/ui/src/internal/appearance.ts b/packages/ui/src/internal/appearance.ts index 9f8185b0642..23ef99ca9a7 100644 --- a/packages/ui/src/internal/appearance.ts +++ b/packages/ui/src/internal/appearance.ts @@ -306,6 +306,7 @@ export type ElementsConfig = { formFieldInputShowPasswordIcon: WithOptions; formFieldInputCopyToClipboardButton: WithOptions; formFieldInputCopyToClipboardIcon: WithOptions; + searchInputClearButton: WithOptions; phoneInputBox: WithOptions; formInputGroup: WithOptions; @@ -362,7 +363,6 @@ export type ElementsConfig = { organizationProfileMembersSearchInputIcon: WithOptions; organizationProfileMembersSearchInput: WithOptions; - organizationProfileMembersSearchClearButton: WithOptions; organizationProfileSecuritySsoBadge: WithOptions; organizationProfileSecuritySsoDescription: WithOptions; @@ -650,7 +650,6 @@ export type ElementsConfig = { apiKeysHeader: WithOptions; apiKeysSearchBox: WithOptions; apiKeysSearchInput: WithOptions; - apiKeysSearchClearButton: WithOptions; apiKeysAddButton: WithOptions; apiKeysTable: WithOptions; apiKeysCopyButton: WithOptions; From f43b49c67162815e7886c245a2ff077ff264807a Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 7 Jul 2026 11:25:56 -0400 Subject: [PATCH 4/6] feat(ui): Add shared searchInput appearance element SearchInput layers a shared searchInput descriptor onto the underlying input alongside any consumer-specific element (e.g. apiKeysSearchInput), so all search inputs can be themed together via cl-searchInput. --- .changeset/api-keys-search-clear-button.md | 2 +- packages/ui/src/customizables/elementDescriptors.ts | 1 + packages/ui/src/elements/SearchInput.tsx | 10 +++++++++- packages/ui/src/internal/appearance.ts | 1 + 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.changeset/api-keys-search-clear-button.md b/.changeset/api-keys-search-clear-button.md index 384fc029989..f60a7bab5e8 100644 --- a/.changeset/api-keys-search-clear-button.md +++ b/.changeset/api-keys-search-clear-button.md @@ -6,4 +6,4 @@ Add a clear button to search inputs for quickly resetting the current query. It appears in the `` search and the `` members search. -The button is themeable via the new shared `searchInputClearButton` appearance element, and its label can be customized with the new shared `searchInput.action__clear` localization key. +Search inputs now expose a shared `searchInput` appearance element (layered alongside any existing component-specific element), and the clear button is themeable via the new shared `searchInputClearButton` element. The clear button's label can be customized with the new shared `searchInput.action__clear` localization key. diff --git a/packages/ui/src/customizables/elementDescriptors.ts b/packages/ui/src/customizables/elementDescriptors.ts index cf21a8d62fa..0e8f3dc9d92 100644 --- a/packages/ui/src/customizables/elementDescriptors.ts +++ b/packages/ui/src/customizables/elementDescriptors.ts @@ -177,6 +177,7 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([ 'formFieldInputShowPasswordIcon', 'formFieldInputCopyToClipboardButton', 'formFieldInputCopyToClipboardIcon', + 'searchInput', 'searchInputClearButton', 'phoneInputBox', diff --git a/packages/ui/src/elements/SearchInput.tsx b/packages/ui/src/elements/SearchInput.tsx index 9fbe26735be..40ecdd8a8f9 100644 --- a/packages/ui/src/elements/SearchInput.tsx +++ b/packages/ui/src/elements/SearchInput.tsx @@ -31,11 +31,18 @@ type SearchInputProps = Omit< * appearance element and a shared clear label, so new search inputs don't introduce their own. */ export const SearchInput = React.forwardRef(function SearchInput( - { isLoading, leftIconElementDescriptor, clearButtonLabel, ...rest }, + { isLoading, leftIconElementDescriptor, clearButtonLabel, elementDescriptor, ...rest }, ref, ) { const { t } = useLocalizations(); + // Every search input shares the `searchInput` element, layered with any consumer-specific + // descriptor so both `cl-searchInput` and e.g. `cl-apiKeysSearchInput` are themeable. + const inputDescriptors = [ + descriptors.searchInput, + ...(Array.isArray(elementDescriptor) ? elementDescriptor : [elementDescriptor]), + ]; + return ( ( autoComplete='off' autoCapitalize='none' spellCheck={false} + elementDescriptor={inputDescriptors} leftIcon={ isLoading ? ( diff --git a/packages/ui/src/internal/appearance.ts b/packages/ui/src/internal/appearance.ts index 23ef99ca9a7..bb64ee12425 100644 --- a/packages/ui/src/internal/appearance.ts +++ b/packages/ui/src/internal/appearance.ts @@ -306,6 +306,7 @@ export type ElementsConfig = { formFieldInputShowPasswordIcon: WithOptions; formFieldInputCopyToClipboardButton: WithOptions; formFieldInputCopyToClipboardIcon: WithOptions; + searchInput: WithOptions; searchInputClearButton: WithOptions; phoneInputBox: WithOptions; From 5bd310812b783fe719a12f21e5279ad6101c29fc Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 7 Jul 2026 14:55:29 -0400 Subject: [PATCH 5/6] chore(localizations): Align locales with searchInput key --- packages/localizations/src/ar-SA.ts | 3 +++ packages/localizations/src/be-BY.ts | 3 +++ packages/localizations/src/bg-BG.ts | 3 +++ packages/localizations/src/bn-IN.ts | 3 +++ packages/localizations/src/ca-ES.ts | 3 +++ packages/localizations/src/cs-CZ.ts | 3 +++ packages/localizations/src/da-DK.ts | 3 +++ packages/localizations/src/de-DE.ts | 3 +++ packages/localizations/src/el-GR.ts | 3 +++ packages/localizations/src/en-GB.ts | 3 +++ packages/localizations/src/en-US.ts | 6 +++--- packages/localizations/src/es-CR.ts | 3 +++ packages/localizations/src/es-ES.ts | 3 +++ packages/localizations/src/es-MX.ts | 3 +++ packages/localizations/src/es-UY.ts | 3 +++ packages/localizations/src/fa-IR.ts | 3 +++ packages/localizations/src/fi-FI.ts | 3 +++ packages/localizations/src/fr-FR.ts | 3 +++ packages/localizations/src/he-IL.ts | 3 +++ packages/localizations/src/hi-IN.ts | 3 +++ packages/localizations/src/hr-HR.ts | 3 +++ packages/localizations/src/hu-HU.ts | 3 +++ packages/localizations/src/id-ID.ts | 3 +++ packages/localizations/src/is-IS.ts | 3 +++ packages/localizations/src/it-IT.ts | 3 +++ packages/localizations/src/ja-JP.ts | 3 +++ packages/localizations/src/kk-KZ.ts | 3 +++ packages/localizations/src/ko-KR.ts | 3 +++ packages/localizations/src/mn-MN.ts | 3 +++ packages/localizations/src/ms-MY.ts | 3 +++ packages/localizations/src/nb-NO.ts | 3 +++ packages/localizations/src/nl-BE.ts | 3 +++ packages/localizations/src/nl-NL.ts | 3 +++ packages/localizations/src/pl-PL.ts | 3 +++ packages/localizations/src/pt-BR.ts | 3 +++ packages/localizations/src/pt-PT.ts | 3 +++ packages/localizations/src/ro-RO.ts | 3 +++ packages/localizations/src/ru-RU.ts | 3 +++ packages/localizations/src/sk-SK.ts | 3 +++ packages/localizations/src/sr-RS.ts | 3 +++ packages/localizations/src/sv-SE.ts | 3 +++ packages/localizations/src/ta-IN.ts | 3 +++ packages/localizations/src/te-IN.ts | 3 +++ packages/localizations/src/th-TH.ts | 3 +++ packages/localizations/src/tr-TR.ts | 3 +++ packages/localizations/src/uk-UA.ts | 3 +++ packages/localizations/src/vi-VN.ts | 3 +++ packages/localizations/src/zh-CN.ts | 3 +++ packages/localizations/src/zh-TW.ts | 3 +++ 49 files changed, 147 insertions(+), 3 deletions(-) diff --git a/packages/localizations/src/ar-SA.ts b/packages/localizations/src/ar-SA.ts index de94ee932ae..9b70f8d0a2d 100644 --- a/packages/localizations/src/ar-SA.ts +++ b/packages/localizations/src/ar-SA.ts @@ -1216,6 +1216,9 @@ export const arSA: LocalizationResource = { title: undefined, }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'إضافة حساب', diff --git a/packages/localizations/src/be-BY.ts b/packages/localizations/src/be-BY.ts index 2203157ce23..3fb72ae5d54 100644 --- a/packages/localizations/src/be-BY.ts +++ b/packages/localizations/src/be-BY.ts @@ -1223,6 +1223,9 @@ export const beBY: LocalizationResource = { title: 'Код TOTP', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Дадаць уліковы запіс', diff --git a/packages/localizations/src/bg-BG.ts b/packages/localizations/src/bg-BG.ts index 8b6d0093b09..4e14def42ab 100644 --- a/packages/localizations/src/bg-BG.ts +++ b/packages/localizations/src/bg-BG.ts @@ -1220,6 +1220,9 @@ export const bgBG: LocalizationResource = { title: 'Authenticator App MFA', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Add account', diff --git a/packages/localizations/src/bn-IN.ts b/packages/localizations/src/bn-IN.ts index bf5acdc03a2..d8a33bdfa92 100644 --- a/packages/localizations/src/bn-IN.ts +++ b/packages/localizations/src/bn-IN.ts @@ -1228,6 +1228,9 @@ export const bnIN: LocalizationResource = { title: 'যাচাইকরণ প্রয়োজন', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'অ্যাকাউন্ট যোগ করুন', diff --git a/packages/localizations/src/ca-ES.ts b/packages/localizations/src/ca-ES.ts index a768b80edd1..fe24667d6a5 100644 --- a/packages/localizations/src/ca-ES.ts +++ b/packages/localizations/src/ca-ES.ts @@ -1228,6 +1228,9 @@ export const caES: LocalizationResource = { title: 'Verificació per TOTP (2FA)', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Afegeix compte', diff --git a/packages/localizations/src/cs-CZ.ts b/packages/localizations/src/cs-CZ.ts index e5566d4bce0..40ca97e4f47 100644 --- a/packages/localizations/src/cs-CZ.ts +++ b/packages/localizations/src/cs-CZ.ts @@ -1226,6 +1226,9 @@ export const csCZ: LocalizationResource = { title: 'Vyžadováno ověření', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Přidat účet', diff --git a/packages/localizations/src/da-DK.ts b/packages/localizations/src/da-DK.ts index d6a111c43a7..8f6cd90e1ce 100644 --- a/packages/localizations/src/da-DK.ts +++ b/packages/localizations/src/da-DK.ts @@ -1218,6 +1218,9 @@ export const daDK: LocalizationResource = { title: undefined, }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Tilføj konto', diff --git a/packages/localizations/src/de-DE.ts b/packages/localizations/src/de-DE.ts index 970558a0f4f..f7c181cf898 100644 --- a/packages/localizations/src/de-DE.ts +++ b/packages/localizations/src/de-DE.ts @@ -1235,6 +1235,9 @@ export const deDE: LocalizationResource = { title: 'TOTP-Verifizierung (MFA)', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Konto hinzufügen', diff --git a/packages/localizations/src/el-GR.ts b/packages/localizations/src/el-GR.ts index 156e7e56130..fbc71c2473a 100644 --- a/packages/localizations/src/el-GR.ts +++ b/packages/localizations/src/el-GR.ts @@ -1226,6 +1226,9 @@ export const elGR: LocalizationResource = { title: 'Επαλήθευση δύο παραγόντων', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Προσθήκη λογαριασμού', diff --git a/packages/localizations/src/en-GB.ts b/packages/localizations/src/en-GB.ts index 27e0239b6ea..74fa7ea9e73 100644 --- a/packages/localizations/src/en-GB.ts +++ b/packages/localizations/src/en-GB.ts @@ -1219,6 +1219,9 @@ export const enGB: LocalizationResource = { title: 'Verification required', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Add account', diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index ffd52f8e7e0..d766d4ce383 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -860,9 +860,6 @@ export const enUS: LocalizationResource = { title: 'Signed in as {{identifier}}', }, lastAuthenticationStrategy: 'Last used', - searchInput: { - action__clear: 'Clear search', - }, maintenanceMode: "We are currently undergoing maintenance, but don't worry, it shouldn't take more than a few minutes.", membershipRole__admin: 'Admin', @@ -1250,6 +1247,9 @@ export const enUS: LocalizationResource = { title: 'Verification required', }, }, + searchInput: { + action__clear: 'Clear search', + }, signIn: { accountSwitcher: { action__addAccount: 'Add account', diff --git a/packages/localizations/src/es-CR.ts b/packages/localizations/src/es-CR.ts index f48e0d8dab4..6c465f680a0 100644 --- a/packages/localizations/src/es-CR.ts +++ b/packages/localizations/src/es-CR.ts @@ -1224,6 +1224,9 @@ export const esCR: LocalizationResource = { title: 'Verificación requerida', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Agregar cuenta', diff --git a/packages/localizations/src/es-ES.ts b/packages/localizations/src/es-ES.ts index 7d08afe08cd..2d6895e5822 100644 --- a/packages/localizations/src/es-ES.ts +++ b/packages/localizations/src/es-ES.ts @@ -1229,6 +1229,9 @@ export const esES: LocalizationResource = { title: 'Verificación por TOTP (2FA)', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Añadir cuenta', diff --git a/packages/localizations/src/es-MX.ts b/packages/localizations/src/es-MX.ts index 18d06e8dc88..64b90aa3da2 100644 --- a/packages/localizations/src/es-MX.ts +++ b/packages/localizations/src/es-MX.ts @@ -1225,6 +1225,9 @@ export const esMX: LocalizationResource = { title: 'Verificación requerida', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Agregar cuenta', diff --git a/packages/localizations/src/es-UY.ts b/packages/localizations/src/es-UY.ts index 45a1012d1a7..b027e07069f 100644 --- a/packages/localizations/src/es-UY.ts +++ b/packages/localizations/src/es-UY.ts @@ -1223,6 +1223,9 @@ export const esUY: LocalizationResource = { title: 'Verificación requerida', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Agregar cuenta', diff --git a/packages/localizations/src/fa-IR.ts b/packages/localizations/src/fa-IR.ts index 2a28fa818b2..1cf0c875abf 100644 --- a/packages/localizations/src/fa-IR.ts +++ b/packages/localizations/src/fa-IR.ts @@ -1228,6 +1228,9 @@ export const faIR: LocalizationResource = { title: 'تایید هویت الزامی است', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'افزودن حساب', diff --git a/packages/localizations/src/fi-FI.ts b/packages/localizations/src/fi-FI.ts index 8f9779263f9..d02b810a53a 100644 --- a/packages/localizations/src/fi-FI.ts +++ b/packages/localizations/src/fi-FI.ts @@ -1230,6 +1230,9 @@ export const fiFI: LocalizationResource = { title: 'Vahvistus vaaditaan', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Lisää tili', diff --git a/packages/localizations/src/fr-FR.ts b/packages/localizations/src/fr-FR.ts index 0c8fc0af507..3ef7465f900 100644 --- a/packages/localizations/src/fr-FR.ts +++ b/packages/localizations/src/fr-FR.ts @@ -1236,6 +1236,9 @@ export const frFR: LocalizationResource = { title: 'Vérification par application d’authentification', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Ajouter un compte', diff --git a/packages/localizations/src/he-IL.ts b/packages/localizations/src/he-IL.ts index 991eba72e8e..b1763ec3337 100644 --- a/packages/localizations/src/he-IL.ts +++ b/packages/localizations/src/he-IL.ts @@ -1213,6 +1213,9 @@ export const heIL: LocalizationResource = { title: 'אימות דו-שלבי', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'הוסף חשבון', diff --git a/packages/localizations/src/hi-IN.ts b/packages/localizations/src/hi-IN.ts index 10ad49c33fd..98812de0410 100644 --- a/packages/localizations/src/hi-IN.ts +++ b/packages/localizations/src/hi-IN.ts @@ -1228,6 +1228,9 @@ export const hiIN: LocalizationResource = { title: 'सत्यापन आवश्यक है', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'खाता जोड़ें', diff --git a/packages/localizations/src/hr-HR.ts b/packages/localizations/src/hr-HR.ts index 9911ad376c8..db93f008960 100644 --- a/packages/localizations/src/hr-HR.ts +++ b/packages/localizations/src/hr-HR.ts @@ -1229,6 +1229,9 @@ export const hrHR: LocalizationResource = { title: 'Potrebna verifikacija', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Dodaj račun', diff --git a/packages/localizations/src/hu-HU.ts b/packages/localizations/src/hu-HU.ts index f0d727933c6..3a3aff4cfe8 100644 --- a/packages/localizations/src/hu-HU.ts +++ b/packages/localizations/src/hu-HU.ts @@ -1231,6 +1231,9 @@ export const huHU: LocalizationResource = { title: 'Ellenőrzés szükséges', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Fiók hozzáadása', diff --git a/packages/localizations/src/id-ID.ts b/packages/localizations/src/id-ID.ts index 0e8ad38dd4a..df03c826a49 100644 --- a/packages/localizations/src/id-ID.ts +++ b/packages/localizations/src/id-ID.ts @@ -1222,6 +1222,9 @@ export const idID: LocalizationResource = { title: 'Verifikasi diperlukan', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Tambah akun', diff --git a/packages/localizations/src/is-IS.ts b/packages/localizations/src/is-IS.ts index 0ca57d76698..a2f5087a28e 100644 --- a/packages/localizations/src/is-IS.ts +++ b/packages/localizations/src/is-IS.ts @@ -1230,6 +1230,9 @@ export const isIS: LocalizationResource = { title: 'Staðfesting nauðsynleg', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Bæta við reikningi', diff --git a/packages/localizations/src/it-IT.ts b/packages/localizations/src/it-IT.ts index 96a6a24fba0..305b7cc39d0 100644 --- a/packages/localizations/src/it-IT.ts +++ b/packages/localizations/src/it-IT.ts @@ -1228,6 +1228,9 @@ export const itIT: LocalizationResource = { title: 'Verifica TOTP (MFA)', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Aggiungi account', diff --git a/packages/localizations/src/ja-JP.ts b/packages/localizations/src/ja-JP.ts index 8eb8b07dfcf..ad9f53c787f 100644 --- a/packages/localizations/src/ja-JP.ts +++ b/packages/localizations/src/ja-JP.ts @@ -1229,6 +1229,9 @@ export const jaJP: LocalizationResource = { title: '確認が必要です', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'アカウントを追加', diff --git a/packages/localizations/src/kk-KZ.ts b/packages/localizations/src/kk-KZ.ts index 4978a338c80..f2ecf7e21fe 100644 --- a/packages/localizations/src/kk-KZ.ts +++ b/packages/localizations/src/kk-KZ.ts @@ -1212,6 +1212,9 @@ export const kkKZ: LocalizationResource = { title: 'Растау қажет', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Есептік жазба қосу', diff --git a/packages/localizations/src/ko-KR.ts b/packages/localizations/src/ko-KR.ts index 12aed128ad1..0bb35f7cee4 100644 --- a/packages/localizations/src/ko-KR.ts +++ b/packages/localizations/src/ko-KR.ts @@ -1217,6 +1217,9 @@ export const koKR: LocalizationResource = { title: '인증이 필요해요', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: '계정 추가', diff --git a/packages/localizations/src/mn-MN.ts b/packages/localizations/src/mn-MN.ts index 1c05a448f19..0d25f3cea12 100644 --- a/packages/localizations/src/mn-MN.ts +++ b/packages/localizations/src/mn-MN.ts @@ -1221,6 +1221,9 @@ export const mnMN: LocalizationResource = { title: undefined, }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Бүртгэл нэмэх', diff --git a/packages/localizations/src/ms-MY.ts b/packages/localizations/src/ms-MY.ts index 9e015ceee8e..72e32d5764c 100644 --- a/packages/localizations/src/ms-MY.ts +++ b/packages/localizations/src/ms-MY.ts @@ -1232,6 +1232,9 @@ export const msMY: LocalizationResource = { title: 'Pengesahan diperlukan', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Tambah akaun', diff --git a/packages/localizations/src/nb-NO.ts b/packages/localizations/src/nb-NO.ts index 55692054367..edd764c32d5 100644 --- a/packages/localizations/src/nb-NO.ts +++ b/packages/localizations/src/nb-NO.ts @@ -1231,6 +1231,9 @@ export const nbNO: LocalizationResource = { title: 'Verifisering påkrevd', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Legg til konto', diff --git a/packages/localizations/src/nl-BE.ts b/packages/localizations/src/nl-BE.ts index 74cde0f3c28..4107509c7d1 100644 --- a/packages/localizations/src/nl-BE.ts +++ b/packages/localizations/src/nl-BE.ts @@ -1222,6 +1222,9 @@ export const nlBE: LocalizationResource = { title: 'Tweestapsverificatie', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Account toevoegen', diff --git a/packages/localizations/src/nl-NL.ts b/packages/localizations/src/nl-NL.ts index cb89363c510..b54ddc59780 100644 --- a/packages/localizations/src/nl-NL.ts +++ b/packages/localizations/src/nl-NL.ts @@ -1222,6 +1222,9 @@ export const nlNL: LocalizationResource = { title: 'Tweestapsverificatie', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Account toevoegen', diff --git a/packages/localizations/src/pl-PL.ts b/packages/localizations/src/pl-PL.ts index 6fede4db9eb..92adfd113a1 100644 --- a/packages/localizations/src/pl-PL.ts +++ b/packages/localizations/src/pl-PL.ts @@ -1220,6 +1220,9 @@ export const plPL: LocalizationResource = { title: 'Wymagana weryfikacja', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Dodaj konto', diff --git a/packages/localizations/src/pt-BR.ts b/packages/localizations/src/pt-BR.ts index ae61bfe88c4..0b9478c97dd 100644 --- a/packages/localizations/src/pt-BR.ts +++ b/packages/localizations/src/pt-BR.ts @@ -1230,6 +1230,9 @@ export const ptBR: LocalizationResource = { title: 'Autenticação TOTP', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Adicionar conta', diff --git a/packages/localizations/src/pt-PT.ts b/packages/localizations/src/pt-PT.ts index 7754da6a7fb..f378392dcb4 100644 --- a/packages/localizations/src/pt-PT.ts +++ b/packages/localizations/src/pt-PT.ts @@ -1231,6 +1231,9 @@ export const ptPT: LocalizationResource = { title: 'Verificação necessária', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Adicionar conta', diff --git a/packages/localizations/src/ro-RO.ts b/packages/localizations/src/ro-RO.ts index dec56853bd8..6a91ac6bbd0 100644 --- a/packages/localizations/src/ro-RO.ts +++ b/packages/localizations/src/ro-RO.ts @@ -1231,6 +1231,9 @@ export const roRO: LocalizationResource = { title: 'Verificare în doi pași', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Adaugă cont', diff --git a/packages/localizations/src/ru-RU.ts b/packages/localizations/src/ru-RU.ts index 201b23e3775..545061ad0e5 100644 --- a/packages/localizations/src/ru-RU.ts +++ b/packages/localizations/src/ru-RU.ts @@ -1227,6 +1227,9 @@ export const ruRU: LocalizationResource = { title: 'Двухфакторная верификация', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Добавить учетную запись', diff --git a/packages/localizations/src/sk-SK.ts b/packages/localizations/src/sk-SK.ts index c81c0238ae1..0c34697e47f 100644 --- a/packages/localizations/src/sk-SK.ts +++ b/packages/localizations/src/sk-SK.ts @@ -1221,6 +1221,9 @@ export const skSK: LocalizationResource = { title: 'Vyžaduje sa overenie', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Pridať účet', diff --git a/packages/localizations/src/sr-RS.ts b/packages/localizations/src/sr-RS.ts index 1544a7ae617..92aee810ba2 100644 --- a/packages/localizations/src/sr-RS.ts +++ b/packages/localizations/src/sr-RS.ts @@ -1218,6 +1218,9 @@ export const srRS: LocalizationResource = { title: undefined, }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Dodaj nalog', diff --git a/packages/localizations/src/sv-SE.ts b/packages/localizations/src/sv-SE.ts index 8240c4fae21..56d3da38257 100644 --- a/packages/localizations/src/sv-SE.ts +++ b/packages/localizations/src/sv-SE.ts @@ -1220,6 +1220,9 @@ export const svSE: LocalizationResource = { title: 'Tvåstegsverifiering', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Lägg till konto', diff --git a/packages/localizations/src/ta-IN.ts b/packages/localizations/src/ta-IN.ts index a209297f22c..587a71d59e5 100644 --- a/packages/localizations/src/ta-IN.ts +++ b/packages/localizations/src/ta-IN.ts @@ -1234,6 +1234,9 @@ export const taIN: LocalizationResource = { title: 'சரிபார்ப்பு தேவை', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'கணக்கைச் சேர்', diff --git a/packages/localizations/src/te-IN.ts b/packages/localizations/src/te-IN.ts index d209049d175..de16deaba50 100644 --- a/packages/localizations/src/te-IN.ts +++ b/packages/localizations/src/te-IN.ts @@ -1231,6 +1231,9 @@ export const teIN: LocalizationResource = { title: 'ధృవీకరణ అవసరం', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'ఖాతాను జోడించండి', diff --git a/packages/localizations/src/th-TH.ts b/packages/localizations/src/th-TH.ts index 3db92e6126c..8c8b34e16ba 100644 --- a/packages/localizations/src/th-TH.ts +++ b/packages/localizations/src/th-TH.ts @@ -1220,6 +1220,9 @@ export const thTH: LocalizationResource = { title: 'ต้องการการยืนยัน', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'เพิ่มบัญชี', diff --git a/packages/localizations/src/tr-TR.ts b/packages/localizations/src/tr-TR.ts index 85ee6d51781..98e08e93e4a 100644 --- a/packages/localizations/src/tr-TR.ts +++ b/packages/localizations/src/tr-TR.ts @@ -1219,6 +1219,9 @@ export const trTR: LocalizationResource = { title: 'TOTP Kodunu Girin', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Hesap ekle', diff --git a/packages/localizations/src/uk-UA.ts b/packages/localizations/src/uk-UA.ts index cf4b56558fb..dc6f645e1c5 100644 --- a/packages/localizations/src/uk-UA.ts +++ b/packages/localizations/src/uk-UA.ts @@ -1218,6 +1218,9 @@ export const ukUA: LocalizationResource = { title: undefined, }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Add account', diff --git a/packages/localizations/src/vi-VN.ts b/packages/localizations/src/vi-VN.ts index 2496a6447e9..4d61f937845 100644 --- a/packages/localizations/src/vi-VN.ts +++ b/packages/localizations/src/vi-VN.ts @@ -1228,6 +1228,9 @@ export const viVN: LocalizationResource = { title: 'Xác minh yêu cầu', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: 'Thêm tài khoản', diff --git a/packages/localizations/src/zh-CN.ts b/packages/localizations/src/zh-CN.ts index 4caee7c1c85..50fd9254b16 100644 --- a/packages/localizations/src/zh-CN.ts +++ b/packages/localizations/src/zh-CN.ts @@ -1209,6 +1209,9 @@ export const zhCN: LocalizationResource = { title: undefined, }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: '添加账户', diff --git a/packages/localizations/src/zh-TW.ts b/packages/localizations/src/zh-TW.ts index eb79aa38554..b79689fb355 100644 --- a/packages/localizations/src/zh-TW.ts +++ b/packages/localizations/src/zh-TW.ts @@ -1212,6 +1212,9 @@ export const zhTW: LocalizationResource = { title: '輸入驗證碼', }, }, + searchInput: { + action__clear: undefined, + }, signIn: { accountSwitcher: { action__addAccount: '新增帳戶', From da8cbdfb71306b5d98de4bb8c2b6f01ed8c0e51b Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 8 Jul 2026 21:58:30 -0400 Subject: [PATCH 6/6] fix(ui): use isVirtualClick to reliably detect keyboard-activated input clear --- packages/ui/src/elements/InputWithIcon.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/elements/InputWithIcon.tsx b/packages/ui/src/elements/InputWithIcon.tsx index 99c33777ab9..3d4c1f20199 100644 --- a/packages/ui/src/elements/InputWithIcon.tsx +++ b/packages/ui/src/elements/InputWithIcon.tsx @@ -1,4 +1,5 @@ import { inertProps } from '@clerk/shared/inert'; +import { isVirtualClick } from '@floating-ui/react/utils'; import React from 'react'; import { Box, Flex, Input } from '../customizables'; @@ -30,11 +31,12 @@ export const InputWithIcon = React.forwardRef(( const handleClear = (event: React.MouseEvent) => { onClear?.(); - // Keyboard activation produces a synthetic click with detail === 0. The button is about to go - // inert and drop out of the tab order, so return focus to the input. A pointer click leaves - // focus alone. - if (event.detail === 0) { - internalRef.current?.focus(); + // The button is about to go inert and drop out of the tab order. On keyboard or assistive-tech + // activation, return focus to the input; a real pointer or touch click leaves focus alone so the + // on-screen keyboard is not forced open. `isVirtualClick` guards `detail === 0` with a pointer + // check plus Firefox/Android quirks, which a bare `detail === 0` check misses on touch devices. + if (isVirtualClick(event.nativeEvent)) { + internalRef.current?.focus({ preventScroll: true }); } };