diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.module.css b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.module.css index 50c2a6150c2..67c66e7afe7 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.module.css +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.module.css @@ -13,6 +13,7 @@ .leftNavWrapper { overflow-y: auto; + width: 250px; } .contentWrapper { diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx index c0bb28a0bc1..b52fc08b36d 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx @@ -1,33 +1,26 @@ import type { ReactElement } from 'react'; import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react'; import classes from './SettingsModal.module.css'; -import { - CogIcon, - InformationSquareIcon, - TimerStartIcon, - ShieldLockIcon, - SidebarBothIcon, -} from '@studio/icons'; -import { StudioModal } from '@studio/components'; -import type { LeftNavigationTab } from 'app-shared/types/LeftNavigationTab'; -import { LeftNavigationBar } from 'app-shared/components/LeftNavigationBar'; -import type { SettingsModalTab } from 'app-development/types/SettingsModalTab'; -import { createNavigationTab } from './utils'; +import { CogIcon } from '@studio/icons'; +import { StudioModal, StudioContentMenu } from '@studio/components'; +import type { SettingsModalTabId } from 'app-development/types/SettingsModalTabId'; import { useTranslation } from 'react-i18next'; import { PolicyTab } from './components/Tabs/PolicyTab'; import { AboutTab } from './components/Tabs/AboutTab'; import { AccessControlTab } from './components/Tabs/AccessControlTab'; import { SetupTab } from './components/Tabs/SetupTab'; import { type SettingsModalHandle } from 'app-development/types/SettingsModalHandle'; +import { useSettingsModalMenuTabConfigs } from './hooks/useSettingsModalMenuTabConfigs'; export const SettingsModal = forwardRef(({}, ref): ReactElement => { const { t } = useTranslation(); - const [currentTab, setCurrentTab] = useState('about'); + const [currentTab, setCurrentTab] = useState('about'); const dialogRef = useRef(); + const { getMenuTabConfigs } = useSettingsModalMenuTabConfigs(); const openSettings = useCallback( - (tab: SettingsModalTab = currentTab) => { + (tab: SettingsModalTabId = currentTab) => { setCurrentTab(tab); dialogRef.current?.showModal(); }, @@ -38,38 +31,6 @@ export const SettingsModal = forwardRef(({}, ref): Reac openSettings, ]); - const aboutTabId: SettingsModalTab = 'about'; - const setupTabId: SettingsModalTab = 'setup'; - const policyTabId: SettingsModalTab = 'policy'; - const accessControlTabId: SettingsModalTab = 'access_control'; - - const leftNavigationTabs: LeftNavigationTab[] = [ - createNavigationTab( - , - aboutTabId, - () => setCurrentTab(aboutTabId), - currentTab, - ), - createNavigationTab( - , - setupTabId, - () => setCurrentTab(setupTabId), - currentTab, - ), - createNavigationTab( - , - policyTabId, - () => setCurrentTab(policyTabId), - currentTab, - ), - createNavigationTab( - , - accessControlTabId, - () => setCurrentTab(accessControlTabId), - currentTab, - ), - ]; - const displayTabs = () => { switch (currentTab) { case 'about': { @@ -98,7 +59,11 @@ export const SettingsModal = forwardRef(({}, ref): Reac contentClassName={classes.modalContent} >
- + setCurrentTab(tabId)} + />
{displayTabs()}
diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx new file mode 100644 index 00000000000..b0e7e3b487b --- /dev/null +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import type { SettingsModalTabId } from '../../../../../../types/SettingsModalTabId'; +import { + InformationSquareIcon, + SidebarBothIcon, + ShieldLockIcon, + TimerStartIcon, +} from '@studio/icons'; +import { useTranslation } from 'react-i18next'; +import type { StudioMenuTabType } from '@studio/components'; + +const aboutTabId: SettingsModalTabId = 'about'; +const setupTabId: SettingsModalTabId = 'setup'; +const policyTabId: SettingsModalTabId = 'policy'; +const accessControlTabId: SettingsModalTabId = 'access_control'; + +export const useSettingsModalMenuTabConfigs = () => { + const { t } = useTranslation(); + + const getMenuTabConfigs = (): StudioMenuTabType[] => { + return [ + { + tabId: aboutTabId, + tabName: t(`settings_modal.left_nav_tab_${aboutTabId}`), + icon: , + }, + { + tabId: setupTabId, + tabName: t(`settings_modal.left_nav_tab_${setupTabId}`), + icon: , + }, + { + tabId: policyTabId, + tabName: t(`settings_modal.left_nav_tab_${policyTabId}`), + icon: , + }, + { + tabId: accessControlTabId, + tabName: t(`settings_modal.left_nav_tab_${accessControlTabId}`), + icon: , + }, + ]; + }; + + return { getMenuTabConfigs }; +}; diff --git a/frontend/app-development/types/SettingsModalHandle.ts b/frontend/app-development/types/SettingsModalHandle.ts index 5f2b30cc351..580159b04bb 100644 --- a/frontend/app-development/types/SettingsModalHandle.ts +++ b/frontend/app-development/types/SettingsModalHandle.ts @@ -1,5 +1,5 @@ -import type { SettingsModalTab } from './SettingsModalTab'; +import type { SettingsModalTabId } from './SettingsModalTabId'; export type SettingsModalHandle = { - openSettings: (tab?: SettingsModalTab) => void; + openSettings: (tab?: SettingsModalTabId) => void; }; diff --git a/frontend/app-development/types/SettingsModalTabId.ts b/frontend/app-development/types/SettingsModalTabId.ts new file mode 100644 index 00000000000..c596b1e0d5b --- /dev/null +++ b/frontend/app-development/types/SettingsModalTabId.ts @@ -0,0 +1 @@ +export type SettingsModalTabId = 'about' | 'setup' | 'policy' | 'access_control'; diff --git a/frontend/libs/studio-components/src/components/StudioContentMenu/StudioContentMenu.module.css b/frontend/libs/studio-components/src/components/StudioContentMenu/StudioContentMenu.module.css new file mode 100644 index 00000000000..81214c4d5dd --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioContentMenu/StudioContentMenu.module.css @@ -0,0 +1,4 @@ +.tabsContainer { + background-color: var(--fds-semantic-surface-action-second-subtle); + height: 100%; +} diff --git a/frontend/libs/studio-components/src/components/StudioContentMenu/StudioContentMenu.stories.tsx b/frontend/libs/studio-components/src/components/StudioContentMenu/StudioContentMenu.stories.tsx new file mode 100644 index 00000000000..84190daee83 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioContentMenu/StudioContentMenu.stories.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import type { Meta, StoryFn } from '@storybook/react'; +import { StudioContentMenu } from './StudioContentMenu'; +import { BookIcon, VideoIcon, QuestionmarkDiamondIcon, ExternalLinkIcon } from '@studio/icons'; +import { StudioContentMenuWrapper } from './StudioContentMenuWrapper'; + +type Story = StoryFn; + +const meta: Meta = { + title: 'Components/StudioContentMenu', + component: StudioContentMenu, + argTypes: { + contentTabs: { + control: 'object', + description: + 'Array of menu tabs with icons, names, and ids. Add prop `to` if tab should navigate to a different url', + table: { + type: { summary: 'StudioMenuTabType[]' }, + }, + }, + selectedTabId: { + table: { disable: true }, + }, + onChangeTab: { + table: { disable: true }, + }, + }, +}; + +export default meta; + +export const Preview: Story = (args) => ( + +); + +Preview.args = { + contentTabs: [ + { + tabId: 'booksTab', + tabName: 'Bøker', + icon: , + }, + { + tabId: 'videosTab', + tabName: 'Filmer', + icon: , + }, + { + tabId: 'tabWithVeryLongTabName', + tabName: 'LoremIpsumLoremIpsumLoremIpsum', + icon: , + }, + { + tabId: 'tabAsLink', + tabName: 'Gå til Designsystemet', + icon: , + to: 'https://next.storybook.designsystemet.no', + }, + ], +}; diff --git a/frontend/libs/studio-components/src/components/StudioContentMenu/StudioContentMenu.test.tsx b/frontend/libs/studio-components/src/components/StudioContentMenu/StudioContentMenu.test.tsx new file mode 100644 index 00000000000..0cfd86c9314 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioContentMenu/StudioContentMenu.test.tsx @@ -0,0 +1,172 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; +import type { StudioContentMenuProps } from './StudioContentMenu'; +import { StudioContentMenu } from './StudioContentMenu'; +import type { StudioMenuTabType } from './types/StudioMenuTabType'; + +type StudioMenuTabName = 'tab1' | 'tab2' | 'tab3'; + +const onChangeTabMock = jest.fn(); + +const tab1Name = 'My tab'; +const tab1Id: StudioMenuTabName = 'tab1'; +const tab1: StudioMenuTabType = { + tabName: tab1Name, + tabId: tab1Id, + icon: , +}; +const tab2Name = 'My second tab'; +const tab2Id: StudioMenuTabName = 'tab2'; +const tab2: StudioMenuTabType = { + tabName: tab2Name, + tabId: tab2Id, + icon: , +}; + +describe('StudioContentMenu', () => { + afterEach(jest.clearAllMocks); + + it('renders an empty contentMenu when there is no provided tabs', () => { + renderStudioContentMenu({ contentTabs: [] }); + const emptyMenu = screen.getByRole('tablist'); + expect(emptyMenu).toBeInTheDocument(); + }); + + it('renders the title and icon of a given menu tab', () => { + const iconTitle = 'My icon'; + renderStudioContentMenu({ + contentTabs: [ + { + ...tab1, + icon: , + }, + ], + }); + const menuTab = screen.getByRole('tab', { name: tab1Name }); + const menuIcon = screen.getByTestId(iconTitle); + expect(menuTab).toBeInTheDocument(); + expect(menuIcon).toBeInTheDocument(); + }); + + it('renders a tab with "to" prop as a link element', () => { + const link = 'url-link'; + renderStudioContentMenu({ + contentTabs: [ + { + ...tab1, + to: link, + }, + ], + }); + const menuTab = screen.getByRole('tab', { name: tab1Name }); + const linkTab = screen.getByRole('link', { name: tab1Name }); + expect(menuTab).toBeInTheDocument(); + expect(linkTab).toBeInTheDocument(); + expect(linkTab).toHaveAttribute('href', link); + }); + + it('allows changing focus to next tab using keyboard', async () => { + const user = userEvent.setup(); + renderStudioContentMenu({ + contentTabs: [tab1, tab2], + }); + const tab1Element = screen.getByRole('tab', { name: tab1Name }); + await user.click(tab1Element); + const tab2Element = screen.getByRole('tab', { name: tab2Name }); + expect(tab2Element).not.toHaveFocus(); + await user.keyboard('{ArrowDown}'); + expect(tab2Element).toHaveFocus(); + }); + + it('keeps focus on current tab if pressing keyDown when focus is on last tab in menu', async () => { + const user = userEvent.setup(); + renderStudioContentMenu({ + contentTabs: [tab1, tab2], + }); + const tab2Element = screen.getByRole('tab', { name: tab2Name }); + await user.click(tab2Element); + expect(tab2Element).toHaveFocus(); + await user.keyboard('{ArrowDown}'); + expect(tab2Element).toHaveFocus(); + }); + + it('allows changing focus to previous tab using keyboard', async () => { + const user = userEvent.setup(); + renderStudioContentMenu({ + contentTabs: [tab1, tab2], + }); + const tab2Element = screen.getByRole('tab', { name: tab2Name }); + await user.click(tab2Element); + const tab1Element = screen.getByRole('tab', { name: tab1Name }); + expect(tab1Element).not.toHaveFocus(); + await user.keyboard('{ArrowUp}'); + expect(tab1Element).toHaveFocus(); + }); + + it('keeps focus on current tab if pressing keyUp when focus is on first tab in menu', async () => { + const user = userEvent.setup(); + renderStudioContentMenu({ + contentTabs: [tab1, tab2], + }); + const tab1Element = screen.getByRole('tab', { name: tab1Name }); + await user.click(tab1Element); + expect(tab1Element).toHaveFocus(); + await user.keyboard('{ArrowUp}'); + expect(tab1Element).toHaveFocus(); + }); + + it('calls onChangeTab when clicking enter on a tab with focus', async () => { + const user = userEvent.setup(); + renderStudioContentMenu({ + contentTabs: [tab1, tab2], + }); + const tab1Element = screen.getByRole('tab', { name: tab1Name }); + await user.click(tab1Element); + await user.keyboard('{ArrowDown}'); + await user.keyboard('{Enter}'); + expect(onChangeTabMock).toHaveBeenCalledTimes(2); + expect(onChangeTabMock).toHaveBeenNthCalledWith(1, tab1Id); + expect(onChangeTabMock).toHaveBeenNthCalledWith(2, tab2Id); + }); + + it('calls onChangeTab when clicking on a menu tab', async () => { + const user = userEvent.setup(); + renderStudioContentMenu({ + contentTabs: [tab1], + }); + const menuTab = screen.getByRole('tab', { name: tab1Name }); + await user.click(menuTab); + expect(onChangeTabMock).toHaveBeenCalledTimes(1); + expect(onChangeTabMock).toHaveBeenCalledWith(tab1Id); + }); + + it('calls onChangeTab when clicking on a menu tab with link', async () => { + const link = 'url-link'; + const user = userEvent.setup(); + renderStudioContentMenu({ + contentTabs: [ + { + ...tab1, + to: link, + }, + ], + }); + const menuTab = screen.getByRole('tab', { name: tab1Name }); + await user.click(menuTab); + expect(onChangeTabMock).toHaveBeenCalledTimes(1); + expect(onChangeTabMock).toHaveBeenCalledWith(tab1Id); + }); +}); + +const renderStudioContentMenu = ({ + contentTabs, +}: Partial> = {}) => { + render( + , + ); +}; diff --git a/frontend/libs/studio-components/src/components/StudioContentMenu/StudioContentMenu.tsx b/frontend/libs/studio-components/src/components/StudioContentMenu/StudioContentMenu.tsx new file mode 100644 index 00000000000..ee029e588c8 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioContentMenu/StudioContentMenu.tsx @@ -0,0 +1,36 @@ +import React, { useState } from 'react'; +import type { ReactElement } from 'react'; +import classes from './StudioContentMenu.module.css'; +import { StudioMenuTabContainer } from './StudioMenuTab'; +import type { StudioMenuTabType } from './types/StudioMenuTabType'; + +export type StudioContentMenuProps = { + contentTabs: StudioMenuTabType[]; + selectedTabId: TabId; + onChangeTab: (tabId: TabId) => void; +}; + +export function StudioContentMenu({ + contentTabs, + selectedTabId, + onChangeTab, +}: StudioContentMenuProps): ReactElement { + const [selectedTab, setSelectedTab] = useState(selectedTabId ?? contentTabs[0]?.tabId); + const handleChangeTab = (tabId: TabId) => { + onChangeTab(tabId); + setSelectedTab(tabId); + }; + + return ( +
+ {contentTabs.map((contentTab) => ( + handleChangeTab(contentTab.tabId)} + /> + ))} +
+ ); +} diff --git a/frontend/libs/studio-components/src/components/StudioContentMenu/StudioContentMenuWrapper.module.css b/frontend/libs/studio-components/src/components/StudioContentMenu/StudioContentMenuWrapper.module.css new file mode 100644 index 00000000000..d6e23110149 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioContentMenu/StudioContentMenuWrapper.module.css @@ -0,0 +1,4 @@ +.contentMenuWrapper { + height: 300px; + width: 200px; +} diff --git a/frontend/libs/studio-components/src/components/StudioContentMenu/StudioContentMenuWrapper.tsx b/frontend/libs/studio-components/src/components/StudioContentMenu/StudioContentMenuWrapper.tsx new file mode 100644 index 00000000000..e2f1de537e1 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioContentMenu/StudioContentMenuWrapper.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import type { StudioContentMenuProps } from './StudioContentMenu'; +import { StudioContentMenu } from './StudioContentMenu'; +import classes from './StudioContentMenuWrapper.module.css'; + +export type StudioContentMenuWrapperProps = StudioContentMenuProps; + +export function StudioContentMenuWrapper({ + contentTabs, + selectedTabId, + onChangeTab, +}: StudioContentMenuWrapperProps) { + return ( +
+ +
+ ); +} diff --git a/frontend/libs/studio-components/src/components/StudioContentMenu/StudioMenuTab/StudioMenuTab.tsx b/frontend/libs/studio-components/src/components/StudioContentMenu/StudioMenuTab/StudioMenuTab.tsx new file mode 100644 index 00000000000..a4ea78ed2f7 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioContentMenu/StudioMenuTab/StudioMenuTab.tsx @@ -0,0 +1,19 @@ +import classes from './StudioMenuTabContainer.module.css'; +import { StudioParagraph } from '@studio/components'; +import React from 'react'; +import type { StudioMenuTabAsButtonType } from '../types/StudioMenuTabType'; + +type MenuTabContentProps = { + contentTab: StudioMenuTabAsButtonType; +}; + +export function StudioMenuTab({ contentTab }: MenuTabContentProps) { + return ( + <> +
{contentTab.icon}
+ + {contentTab.tabName} + + + ); +} diff --git a/frontend/libs/studio-components/src/components/StudioContentMenu/StudioMenuTab/StudioMenuTabAsLink.tsx b/frontend/libs/studio-components/src/components/StudioContentMenu/StudioMenuTab/StudioMenuTabAsLink.tsx new file mode 100644 index 00000000000..5107f256b02 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioContentMenu/StudioMenuTab/StudioMenuTabAsLink.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import classes from './StudioMenuTabContainer.module.css'; +import { StudioLink, StudioParagraph } from '@studio/components'; +import type { StudioMenuTabAsLinkType } from '../types/StudioMenuTabType'; + +export type StudioMenuTabAsLinkProps = { + contentTab: StudioMenuTabAsLinkType; +}; + +export function StudioMenuTabAsLink({ + contentTab, +}: StudioMenuTabAsLinkProps): React.ReactElement { + return ( + +
{contentTab.icon}
+ + {contentTab.tabName} + +
+ ); +} diff --git a/frontend/libs/studio-components/src/components/StudioContentMenu/StudioMenuTab/StudioMenuTabContainer.module.css b/frontend/libs/studio-components/src/components/StudioContentMenu/StudioMenuTab/StudioMenuTabContainer.module.css new file mode 100644 index 00000000000..9829660ca00 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioContentMenu/StudioMenuTab/StudioMenuTabContainer.module.css @@ -0,0 +1,39 @@ +:root { + --selected-border-marking: 3px; +} + +.tabIsSelected, +.tab { + display: flex; + align-items: center; + gap: var(--fds-spacing-2); + border: solid 1px var(--fds-semantic-border-action-second-subtle); + padding: var(--fds-spacing-3); + cursor: pointer; +} + +.linkIcon { + color: var(--fds-semantic-text-neutral-default); +} + +.linkIcon, +.icon { + display: flex; + align-items: center; + font-size: var(--fds-spacing-8); +} + +.tabTitle { + overflow: hidden; + text-overflow: ellipsis; +} + +.tabIsSelected { + border-left: var(--selected-border-marking) solid var(--semantic-surface-action-default); + background-color: white; +} + +.tab:hover, +.tabIsSelected:hover { + background-color: var(--fds-semantic-surface-action-no_fill-hover); +} diff --git a/frontend/libs/studio-components/src/components/StudioContentMenu/StudioMenuTab/StudioMenuTabContainer.tsx b/frontend/libs/studio-components/src/components/StudioContentMenu/StudioMenuTab/StudioMenuTabContainer.tsx new file mode 100644 index 00000000000..963d2b4cabe --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioContentMenu/StudioMenuTab/StudioMenuTabContainer.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import type { ReactElement } from 'react'; +import classes from './StudioMenuTabContainer.module.css'; +import { moveFocus } from '../utils/dom-utils'; +import type { StudioMenuTabAsLinkType, StudioMenuTabType } from '../types/StudioMenuTabType'; +import { StudioMenuTabAsLink } from './StudioMenuTabAsLink'; +import { StudioMenuTab } from './StudioMenuTab'; + +type StudioMenuTabProps = { + contentTab: StudioMenuTabType; + isTabSelected: boolean; + onClick: (tabId: TabId) => void; +}; + +export function StudioMenuTabContainer({ + contentTab, + isTabSelected, + onClick, +}: StudioMenuTabProps): ReactElement { + const handleKeyDown = (event: React.KeyboardEvent) => { + moveFocus(event); + if (event.key === 'Enter') { + event.preventDefault(); + onClick(contentTab.tabId); + } + }; + + return ( +
onClick(contentTab.tabId)} + role='tab' + tabIndex={-1} + onKeyDown={handleKeyDown} + title={contentTab.tabName} + > + {isStudioMenuTabAsLink(contentTab) ? ( + + ) : ( + + )} +
+ ); +} + +function isStudioMenuTabAsLink( + contentTab: StudioMenuTabType, +): contentTab is StudioMenuTabAsLinkType { + return 'to' in contentTab; +} diff --git a/frontend/libs/studio-components/src/components/StudioContentMenu/StudioMenuTab/index.ts b/frontend/libs/studio-components/src/components/StudioContentMenu/StudioMenuTab/index.ts new file mode 100644 index 00000000000..e3c7efa733f --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioContentMenu/StudioMenuTab/index.ts @@ -0,0 +1 @@ +export { StudioMenuTabContainer } from '../StudioMenuTab/StudioMenuTabContainer'; diff --git a/frontend/libs/studio-components/src/components/StudioContentMenu/index.ts b/frontend/libs/studio-components/src/components/StudioContentMenu/index.ts new file mode 100644 index 00000000000..ba41c941c37 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioContentMenu/index.ts @@ -0,0 +1,2 @@ +export { StudioContentMenu } from './StudioContentMenu'; +export type { StudioMenuTabType } from '../StudioContentMenu/types/StudioMenuTabType'; diff --git a/frontend/libs/studio-components/src/components/StudioContentMenu/types/StudioMenuTabType.ts b/frontend/libs/studio-components/src/components/StudioContentMenu/types/StudioMenuTabType.ts new file mode 100644 index 00000000000..aabedd778f7 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioContentMenu/types/StudioMenuTabType.ts @@ -0,0 +1,15 @@ +import type { ReactNode } from 'react'; + +export type StudioMenuTabAsButtonType = { + icon: ReactNode; + tabName: string; + tabId: TabId; +}; + +export type StudioMenuTabAsLinkType = StudioMenuTabAsButtonType & { + to: string; +}; + +export type StudioMenuTabType = + | StudioMenuTabAsButtonType + | StudioMenuTabAsLinkType; diff --git a/frontend/libs/studio-components/src/components/StudioContentMenu/utils/dom-utils.ts b/frontend/libs/studio-components/src/components/StudioContentMenu/utils/dom-utils.ts new file mode 100644 index 00000000000..bf7f8977aa3 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioContentMenu/utils/dom-utils.ts @@ -0,0 +1,48 @@ +import type React from 'react'; + +export function moveFocus(event: React.KeyboardEvent) { + const nextTab = getNextTab(event); + if (nextTab) { + event.preventDefault(); + nextTab.tabIndex = 0; + nextTab.focus(); + event.currentTarget.tabIndex = -1; + } +} + +function getNextTab({ key, currentTarget }: React.KeyboardEvent) { + const tablist = getParentTablist(currentTarget); + const tabs = getTabs(tablist); + switch (key) { + case 'ArrowUp': + return getTabElementAbove(tabs, currentTarget); + case 'ArrowDown': + return getTabElementBelow(tabs, currentTarget); + default: + return null; + } +} + +function getTabElementAbove(tabs: HTMLElement[], currentTab: HTMLElement) { + const currentIndex = tabs.indexOf(currentTab); + if (currentIndex > 0) { + return tabs[currentIndex - 1] as HTMLElement; + } + return null; +} + +function getTabElementBelow(tabs: HTMLElement[], currentTab: HTMLElement) { + const currentIndex = tabs.indexOf(currentTab); + if (currentIndex < tabs.length - 1) { + return tabs[currentIndex + 1] as HTMLElement; + } + return null; +} + +function getTabs(tablist: HTMLElement): HTMLElement[] { + return Array.from(tablist.querySelectorAll('[role="tab"]')) as HTMLElement[]; +} + +function getParentTablist(element: HTMLElement): HTMLElement | null { + return element.closest('[role="tablist"]'); +} diff --git a/frontend/libs/studio-components/src/components/index.ts b/frontend/libs/studio-components/src/components/index.ts index ecb388eda43..033e84e0424 100644 --- a/frontend/libs/studio-components/src/components/index.ts +++ b/frontend/libs/studio-components/src/components/index.ts @@ -12,6 +12,7 @@ export * from './StudioCheckbox'; export * from './StudioCodeFragment'; export * from './StudioCodelistEditor'; export * from './StudioCombobox'; +export * from './StudioContentMenu'; export * from './StudioDecimalInput'; export * from './StudioDeleteButton'; export * from './StudioDisplayTile';