From e52db7be1792fb3f83fd7934f25524cdaee2d100 Mon Sep 17 00:00:00 2001 From: axelEandrews Date: Tue, 6 Aug 2024 10:27:59 -0700 Subject: [PATCH] fix(ui-react): Whitespace in Tab IDs (#5378) * fix: whitespace in tabs, validate value & defaultValue types * chore: added changeset * fix: creates const idValue to avoid changing inputted values * fix: removed unused import, type-checked before .replace * fix: called functions in test * fix: updated ComposedTabsExample.tsx --- .changeset/tall-olives-beam.md | 5 ++ .../tabs/examples/ComposedTabsExample.tsx | 4 +- .../src/primitives/Tabs/TabsContainer.tsx | 5 +- .../react/src/primitives/Tabs/TabsContext.tsx | 2 + .../react/src/primitives/Tabs/TabsItem.tsx | 12 ++-- .../react/src/primitives/Tabs/TabsPanel.tsx | 12 +++- .../primitives/Tabs/__tests__/Tabs.test.tsx | 55 +++++++++++++++++++ .../react/src/primitives/Tabs/constants.ts | 2 + 8 files changed, 87 insertions(+), 10 deletions(-) create mode 100644 .changeset/tall-olives-beam.md create mode 100644 packages/react/src/primitives/Tabs/constants.ts diff --git a/.changeset/tall-olives-beam.md b/.changeset/tall-olives-beam.md new file mode 100644 index 00000000000..9fe7194f9bc --- /dev/null +++ b/.changeset/tall-olives-beam.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/ui-react': patch +--- + +fixes invalid tab IDs diff --git a/docs/src/pages/[platform]/components/tabs/examples/ComposedTabsExample.tsx b/docs/src/pages/[platform]/components/tabs/examples/ComposedTabsExample.tsx index 587ce1edf18..42742153050 100644 --- a/docs/src/pages/[platform]/components/tabs/examples/ComposedTabsExample.tsx +++ b/docs/src/pages/[platform]/components/tabs/examples/ComposedTabsExample.tsx @@ -11,9 +11,9 @@ export const ComposedTabsExample = () => { Tab 1 content - Tab 1 content + Tab 2 content - Tab 1 content + Tab 3 content ); diff --git a/packages/react/src/primitives/Tabs/TabsContainer.tsx b/packages/react/src/primitives/Tabs/TabsContainer.tsx index 163691c4ad2..c4f8c4e7105 100644 --- a/packages/react/src/primitives/Tabs/TabsContainer.tsx +++ b/packages/react/src/primitives/Tabs/TabsContainer.tsx @@ -7,6 +7,7 @@ import { primitiveWithForwardRef } from '../utils/primitiveWithForwardRef'; import { BaseTabsProps, TabsProps } from './types'; import { View } from '../View'; import { TabsContext } from './TabsContext'; +import { useStableId } from '../utils/useStableId'; const TabsContainerPrimitive: Primitive = ( { @@ -20,6 +21,7 @@ const TabsContainerPrimitive: Primitive = ( }: BaseTabsProps, ref ) => { + const groupId = useStableId(); // groupId is used to ensure uniqueness between Tab Groups in IDs const isControlled = controlledValue !== undefined; const [localValue, setLocalValue] = React.useState(() => isControlled ? controlledValue : defaultValue @@ -44,8 +46,9 @@ const TabsContainerPrimitive: Primitive = ( activeTab, isLazy, setActiveTab, + groupId, }; - }, [activeTab, setActiveTab, isLazy]); + }, [activeTab, setActiveTab, isLazy, groupId]); return ( diff --git a/packages/react/src/primitives/Tabs/TabsContext.tsx b/packages/react/src/primitives/Tabs/TabsContext.tsx index 103b80f0179..8b183e80ef9 100644 --- a/packages/react/src/primitives/Tabs/TabsContext.tsx +++ b/packages/react/src/primitives/Tabs/TabsContext.tsx @@ -3,10 +3,12 @@ import * as React from 'react'; export interface TabsContextInterface { activeTab: string; isLazy?: boolean; + groupId: string; setActiveTab: (tab: string) => void; } export const TabsContext = React.createContext({ + groupId: '', activeTab: '', setActiveTab: () => {}, }); diff --git a/packages/react/src/primitives/Tabs/TabsItem.tsx b/packages/react/src/primitives/Tabs/TabsItem.tsx index 47d45ff43bc..645fe0c987e 100644 --- a/packages/react/src/primitives/Tabs/TabsItem.tsx +++ b/packages/react/src/primitives/Tabs/TabsItem.tsx @@ -12,12 +12,17 @@ import { View } from '../View'; import { primitiveWithForwardRef } from '../utils/primitiveWithForwardRef'; import { BaseTabsItemProps, TabsItemProps } from './types'; import { TabsContext } from './TabsContext'; +import { WHITESPACE_VALUE } from './constants'; const TabsItemPrimitive: Primitive = ( { className, value, children, onClick, as = 'button', role = 'tab', ...rest }, ref ) => { - const { activeTab, setActiveTab } = React.useContext(TabsContext); + const { activeTab, setActiveTab, groupId } = React.useContext(TabsContext); + let idValue = value; + if (typeof idValue === 'string') { + idValue = idValue.replace(' ', WHITESPACE_VALUE); + } const isActive = activeTab === value; const handleOnClick = (e: React.MouseEvent) => { if (isTypedFunction(onClick)) { @@ -25,15 +30,14 @@ const TabsItemPrimitive: Primitive = ( } setActiveTab(value); }; - return ( = ( { className, value, children, role = 'tabpanel', ...rest }, ref ) => { - const { activeTab, isLazy } = React.useContext(TabsContext); + const { activeTab, isLazy, groupId } = React.useContext(TabsContext); if (isLazy && activeTab !== value) return null; + let idValue = value; + if (typeof idValue === 'string') { + idValue = idValue.replace(' ', WHITESPACE_VALUE); + } + return ( { expect(tabs[1]).toHaveAttribute('aria-selected', 'true'); }); + it('creates unique IDs for two tabs with same value in different tab groups"', async () => { + render( + + + Tab 1 + + + ); + render( + + + Tab 1 + + + ); + const tabs = await screen.findAllByRole('tab'); + expect(tabs[0].id === tabs[1].id).toBeFalsy(); + }); + + it('creates unique ids for each tab with a unique value', async () => { + render( + + + Tab 1 + Tab 2 + Tab 3 + + Tab 1 + Tab 2 + Tab 3 + + ); + const tabs = await screen.findAllByRole('tab'); + expect(tabs[0].id === tabs[1].id).toBeFalsy(); + expect(tabs[0].id === tabs[2].id).toBeFalsy(); + }); + + it('creates the same ids tabs with the same value in the same group', async () => { + render( + + + Tab 1 + Tab 2 + Tab 3 + + Tab 1 + Tab 2 + Tab 3 + + ); + const tabs = await screen.findAllByRole('tab'); + expect(tabs[0].id === tabs[1].id).toBeTruthy(); + expect(tabs[0].id === tabs[2].id).toBeTruthy(); + }); + describe('TabItem', () => { it('can render custom classnames', async () => { render( diff --git a/packages/react/src/primitives/Tabs/constants.ts b/packages/react/src/primitives/Tabs/constants.ts new file mode 100644 index 00000000000..550dc2dd01c --- /dev/null +++ b/packages/react/src/primitives/Tabs/constants.ts @@ -0,0 +1,2 @@ +/* WHITESPACE_VALUE is used to fill whitespace present in user-inputed `value` when creating id for TabsItem and TabsPanel */ +export const WHITESPACE_VALUE = '-';