diff --git a/pages/tabs/cross-list.page.tsx b/pages/tabs/cross-list.page.tsx new file mode 100644 index 0000000000..b83f9796b1 --- /dev/null +++ b/pages/tabs/cross-list.page.tsx @@ -0,0 +1,164 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; + +import { Button } from '~components'; +import Box from '~components/box'; +import SpaceBetween from '~components/space-between'; +import Tabs, { TabsProps } from '~components/tabs'; + +const GROUP_ID = 'cross-list-demo-group'; +const LEFT_ID = 'cross-list-left'; +const RIGHT_ID = 'cross-list-right'; + +const crossListI18n: TabsProps.I18nStrings = { + scrollLeftAriaLabel: 'Scroll left', + scrollRightAriaLabel: 'Scroll right', + tabsWithActionsAriaRoleDescription: 'Reorderable tabs', + reorderDragHandleAriaLabel: 'Drag handle', + reorderDragHandleAriaDescription: + "Use Space or Enter to activate drag for a tab, then use the arrow keys to move the tab's position. Hold Ctrl (or Command) with the arrow keys at a list edge to move the tab into the neighboring list. To complete the move, use Space or Enter; to discard it, use Escape.", + liveAnnouncementReorderStarted: (position, total) => `Picked up tab at position ${position} of ${total}`, + liveAnnouncementReorderMoved: (from, to, total) => + from === to ? `Moving tab back to position ${to} of ${total}` : `Moving tab to position ${to} of ${total}`, + liveAnnouncementReorderCommitted: (from, to, total) => + from === to + ? `Tab returned to its original position ${to} of ${total}` + : `Tab moved from position ${from} to position ${to} of ${total}`, + liveAnnouncementReorderDiscarded: 'Reordering canceled', + liveAnnouncementTabMovedAcrossLists: (targetPosition, targetTotal) => + `Tab moved to position ${targetPosition} of ${targetTotal} in the destination tab list`, +}; + +function makeLeftTabs(): Array { + return [ + { id: 'l-overview', label: 'Overview', disableReorder: true, content:

Overview (pinned in list A)

}, + { id: 'l-alpha', label: 'Alpha', content:

Alpha content

}, + { + id: 'l-beta', + label: 'Beta', + dismissible: true, + dismissLabel: 'Dismiss Beta', + content:

Beta content (dismissible)

, + }, + { id: 'l-gamma', label: 'Gamma', content:

Gamma content

}, + ]; +} + +function makeRightTabs(): Array { + return [ + { id: 'r-summary', label: 'Summary', disableReorder: true, content:

Summary (pinned in list B)

}, + { id: 'r-delta', label: 'Delta', content:

Delta content

}, + { + id: 'r-epsilon', + label: 'Epsilon', + dismissible: true, + dismissLabel: 'Dismiss Epsilon', + content:

Epsilon content (dismissible)

, + }, + ]; +} + +export default function CrossListTabsPage() { + const [tabsByList, setTabsByList] = useState>>({ + [LEFT_ID]: makeLeftTabs(), + [RIGHT_ID]: makeRightTabs(), + }); + const [activeByList, setActiveByList] = useState>({ + [LEFT_ID]: 'l-alpha', + [RIGHT_ID]: 'r-delta', + }); + + const orderById = (source: Array, tabIds: string[]): Array => { + const byId = new Map(source.map(tab => [tab.id, tab])); + return tabIds.map(id => byId.get(id)!).filter(Boolean); + }; + + const handleReorder = + (listId: string) => + ({ detail }: { detail: TabsProps.ReorderDetail }) => + setTabsByList(prev => ({ ...prev, [listId]: orderById(prev[listId], detail.tabIds) })); + + // A single controlled handler for both lists: it rebuilds BOTH lists from the moved + // detail. `onTabMove` fires on the source and the target instance, but the rebuild is + // idempotent so applying it more than once is safe. + const handleTabMove = ({ detail }: { detail: TabsProps.TabMoveDetail }) => + setTabsByList(prev => { + const byId = new Map(); + Object.values(prev).forEach(list => list.forEach(tab => byId.set(tab.id, tab))); + const build = (ids: string[]) => ids.map(id => byId.get(id)).filter((tab): tab is TabsProps.Tab => !!tab); + return { + ...prev, + [detail.sourceGroupTabsId]: build(detail.sourceTabIds), + [detail.targetGroupTabsId]: build(detail.targetTabIds), + }; + }); + + const handleChange = + (listId: string) => + ({ detail }: { detail: TabsProps.ChangeDetail }) => + setActiveByList(prev => ({ ...prev, [listId]: detail.activeTabId })); + + const handleDismiss = (listId: string, tabId: string) => () => + setTabsByList(prev => ({ ...prev, [listId]: prev[listId].filter(tab => tab.id !== tabId) })); + + const decorate = (listId: string): Array => + tabsByList[listId].map(tab => (tab.dismissible ? { ...tab, onDismiss: handleDismiss(listId, tab.id) } : tab)); + + return ( + <> +

Cross-list reorderable tabs

+ + Two independent Tabs instances that share one reorderGroup value — no wrapper or + provider. Drag a tab (or lift it with the keyboard and use Ctrl/Command + arrow at a list edge) to move it + between the lists. Pinned tabs (Overview, Summary) stay put; dismissible tabs can still be removed. + + +
+

List A

+ + + Order: {tabsByList[LEFT_ID].map(tab => tab.id).join(', ') || '(empty)'} + +
+
+

List B

+ + + Order: {tabsByList[RIGHT_ID].map(tab => tab.id).join(', ') || '(empty)'} + +
+
+ + + + + ); +} diff --git a/pages/tabs/reorderable.page.tsx b/pages/tabs/reorderable.page.tsx new file mode 100644 index 0000000000..5476245ea0 --- /dev/null +++ b/pages/tabs/reorderable.page.tsx @@ -0,0 +1,132 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; + +import { Button } from '~components'; +import SpaceBetween from '~components/space-between'; +import Tabs, { TabsProps } from '~components/tabs'; + +const reorderI18n: TabsProps.I18nStrings = { + scrollLeftAriaLabel: 'Scroll left', + scrollRightAriaLabel: 'Scroll right', + tabsWithActionsAriaRoleDescription: 'Reorderable tabs', + reorderDragHandleAriaLabel: 'Drag handle', + reorderDragHandleAriaDescription: + "Use Space or Enter to activate drag for a tab, then use the arrow keys to move the tab's position. To complete the position move, use Space or Enter, or to discard the move, use Escape.", + liveAnnouncementReorderStarted: (position, total) => `Picked up tab at position ${position} of ${total}`, + liveAnnouncementReorderMoved: (from, to, total) => + from === to ? `Moving tab back to position ${to} of ${total}` : `Moving tab to position ${to} of ${total}`, + liveAnnouncementReorderCommitted: (from, to, total) => + from === to + ? `Tab returned to its original position ${to} of ${total}` + : `Tab moved from position ${from} to position ${to} of ${total}`, + liveAnnouncementReorderDiscarded: 'Reordering canceled', + addTabAriaLabel: 'Add new tab', +}; + +function makeInitialTabs(): Array { + return [ + { id: 'home', label: 'Home', disableReorder: true, content:

Home content (pinned)

}, + { id: 'first', label: 'First tab', content:

First content

}, + { + id: 'second', + label: 'Second tab', + dismissible: true, + dismissLabel: 'Dismiss second tab', + content:

Second content (dismissible)

, + }, + { + id: 'third', + label: 'Third tab', + action: