Skip to content

Commit

Permalink
Merge branch 'main' into fix/accessability-help-text-settings-modal
Browse files Browse the repository at this point in the history
  • Loading branch information
Jondyr authored Oct 30, 2024
2 parents bc79290 + 06fb2ef commit 349425d
Show file tree
Hide file tree
Showing 24 changed files with 729 additions and 8 deletions.
8 changes: 4 additions & 4 deletions frontend/dashboard/components/Resources/Resources.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ const resources: Resource[] = [
),
},
{
label: 'dashboard.resource_slack_label',
description: 'dashboard.resource_slack_description',
url: 'https://altinn.slack.com',
label: 'dashboard.resource_contact_label',
description: 'dashboard.resource_contact_description',
url: 'https://altinn.studio/contact',
icon: (
<svg
width='60'
Expand Down Expand Up @@ -174,7 +174,7 @@ const resources: Resource[] = [
{
label: 'dashboard.resource_status_label',
description: 'dashboard.resource_status_description',
url: 'https://www.altinn.no/om-altinn/driftsmeldinger/',
url: 'https://status.digdir.no/',
icon: (
<svg
width='60'
Expand Down
4 changes: 2 additions & 2 deletions frontend/language/src/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@
"dashboard.org_resources": "{{orgName}} resources",
"dashboard.repository": "Repository",
"dashboard.repository_in_list": "Repository for {{appName}}",
"dashboard.resource_contact_description": "In our Slack-chanel you can talk directly with the people working with Altinn Studio and other app developers.",
"dashboard.resource_contact_label": "Contact / Slack",
"dashboard.resource_design_description": "Get access to Figma to prototype and user test your application before starting development. The prototype kit includes everything you need.",
"dashboard.resource_design_label": "Design / usertest",
"dashboard.resource_docs_description": "Documentation for Altinn Studio. Here you will find resources on how to develop your application.",
Expand All @@ -111,8 +113,6 @@
"dashboard.resource_organisations_label": "All organisations",
"dashboard.resource_roadmap_description": "Altinn is under continuous development. Find out whats being worked on and what we are planning for in the future.",
"dashboard.resource_roadmap_label": "Roadmap",
"dashboard.resource_slack_description": "In our Slack-chanel you can talk directly with the people working with Altinn Studio and other app developers.",
"dashboard.resource_slack_label": "Contact / Slack",
"dashboard.resource_status_description": "We keep you updated on new releases and service announcements that might be relevant for you when developing apps.",
"dashboard.resource_status_label": "Service announcements",
"dashboard.resource_table_header_createdby": "Created by",
Expand Down
4 changes: 2 additions & 2 deletions frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@
"dashboard.org_apps": "{{orgName}}s apper",
"dashboard.org_data_models": "{{orgName}}s datamodeller",
"dashboard.org_resources": "{{orgName}}s ressurser",
"dashboard.resource_contact_description": "Se oversikt over hvordan du kommer i kontakt med oss i Altinn Studio.",
"dashboard.resource_contact_label": "Kontakt oss",
"dashboard.resource_design_description": "Få tilgang til Figma for å prototype og brukerteste tjenesten din før du utvikler den. Med Prototype-pakken i Figma har du alt du trenger for å lage trådskisser.",
"dashboard.resource_design_label": "Design/brukertest",
"dashboard.resource_docs_description": "Dokumentasjonen til Altinn Studio. Her får du hjelp til å utvikle dine egne apper.",
Expand All @@ -156,8 +158,6 @@
"dashboard.resource_organisations_label": "Alle organisasjoner",
"dashboard.resource_roadmap_description": "Altinn utvikles hele tiden. Veikartet forteller mer om hva vi har gjort og hva vi planlegger å gjøre.",
"dashboard.resource_roadmap_label": "Veikart",
"dashboard.resource_slack_description": "I vår Slack-kanal kan du snakke direkte med de som jobber med Altinn Studio og andre som utvikler apper.",
"dashboard.resource_slack_label": "Kontakt/Slack",
"dashboard.resource_status_description": "Vi oppdaterer deg om nye utgaver av Studio og gir deg driftsmeldinger som kan ha betydning for deg når du utvikler tjenester.",
"dashboard.resource_status_label": "Driftsmeldinger",
"dashboard.resource_table_header_createdby": "Opprettet av",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.menuContainer {
height: 100%;
background-color: var(--fds-semantic-surface-action-second-subtle);
}

.tabsContainer {
display: flex;
flex-direction: column;
gap: var(--fds-border_width-default);
background-color: var(--fds-semantic-border-action-second-subtle);
padding: var(--fds-border_width-default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react';
import type { Meta, StoryFn } from '@storybook/react';
import { BookIcon, VideoIcon, QuestionmarkDiamondIcon, ExternalLinkIcon } from '@studio/icons';
import type { StudioContentMenuStoryExampleProps } from './StudioContentMenuStoryExample';
import { StudioContentMenuStoryExample } from './StudioContentMenuStoryExample';

type Story = StoryFn<StudioContentMenuStoryExampleProps<StudioMenuTabName>>;

const meta: Meta<StudioContentMenuStoryExampleProps<StudioMenuTabName>> = {
title: 'Components/StudioContentMenu',
component: StudioContentMenuStoryExample,
argTypes: {
buttonTabs: {
control: 'object',
description: 'Array of button menu tabs with icons, names, and ids.',
table: {
type: { summary: 'StudioContentMenuButtonTabProps<TabId>[]' },
},
},
linkTabs: {
control: 'object',
description:
'Array of link menu tabs with icons, names, and ids. Provide an optional link-element to return `props` in renderTab.',
table: {
type: { summary: 'StudioContentMenuLinkTabProps<TabId>[]' },
},
},
selectedTabId: {
table: { disable: true },
},
onChangeTab: {
table: { disable: true },
},
},
};

export default meta;

type StudioMenuTabName = 'booksTab' | 'videosTab' | 'tabWithVeryLongTabName' | 'tabAsLink';

export const Preview: Story = (args: StudioContentMenuStoryExampleProps<StudioMenuTabName>) => (
<StudioContentMenuStoryExample {...args} />
);

Preview.args = {
buttonTabs: [
{
tabId: 'booksTab',
tabName: 'Bøker',
icon: <BookIcon />,
},
{
tabId: 'videosTab',
tabName: 'Filmer',
icon: <VideoIcon />,
},
{
tabId: 'tabWithVeryLongTabName',
tabName: 'LoremIpsumLoremIpsumLoremIpsum',
icon: <QuestionmarkDiamondIcon />,
},
],
linkTabs: [
{
tabId: 'tabAsLink',
tabName: 'Gå til Designsystemet',
icon: <ExternalLinkIcon />,
renderTab: (props) => <a href={'https://next.storybook.designsystemet.no'} {...props} />,
},
],
onChangeTab: () => {},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { StudioContentMenu, type StudioContentMenuLinkTabProps } from './';
import type { StudioContentMenuButtonTabProps } from './StudioContentMenuButtonTab';

type StudioMenuTabName = 'tab1' | 'tab2' | 'tab3';

const onChangeTabMock = jest.fn();

const tab1Name = 'My tab';
const tab1Id: StudioMenuTabName = 'tab1';
const tab1: StudioContentMenuButtonTabProps<StudioMenuTabName> = {
tabName: tab1Name,
tabId: tab1Id,
icon: <svg />,
};
const tab2Name = 'My second tab';
const tab2Id: StudioMenuTabName = 'tab2';
const tab2: StudioContentMenuButtonTabProps<StudioMenuTabName> = {
tabName: tab2Name,
tabId: tab2Id,
icon: <svg />,
};

describe('StudioContentMenu', () => {
afterEach(jest.clearAllMocks);

it('renders first tab as selected if selectedTab is not provided', () => {
renderStudioContentMenu({
buttonTabs: [tab1, tab2],
});
const firstTab = screen.getByRole('tab', { name: tab1Name });
expect(firstTab).toHaveClass('selected');
});

it('renders a not selected tab without selected style', () => {
renderStudioContentMenu({
buttonTabs: [tab1, tab2],
});
const notSelectedTab = screen.getByRole('tab', { name: tab2Name });
expect(notSelectedTab).not.toHaveClass('selected');
});

it('renders a selected tab with tabIndex 0', () => {
renderStudioContentMenu({
buttonTabs: [tab1],
});
const menuTab = screen.getByRole('tab', { name: tab1Name });
expect(menuTab).toHaveAttribute('tabIndex', '0');
});

it('renders a not selected tab with tabIndex -1', () => {
renderStudioContentMenu({
buttonTabs: [tab1, tab2],
});
const notSelectedTab = screen.getByRole('tab', { name: tab2Name });
expect(notSelectedTab).toHaveAttribute('tabIndex', '-1');
});

it('renders an empty contentMenu when there is no provided tabs', () => {
renderStudioContentMenu({ buttonTabs: [] });
const emptyMenu = screen.getByRole('tablist');
expect(emptyMenu).toBeInTheDocument();
});

it('renders the title and icon of a given menu tab', () => {
const iconTitle = 'My icon';
renderStudioContentMenu({
buttonTabs: [
{
...tab1,
icon: <svg data-testid={iconTitle}></svg>,
},
],
});
const menuTab = screen.getByRole('tab', { name: tab1Name });
const menuTabTitle = screen.getByTitle(tab1Name);
const menuIcon = screen.getByTestId(iconTitle);
expect(menuTab).toBeInTheDocument();
expect(menuTabTitle).toBeInTheDocument();
expect(menuIcon).toBeInTheDocument();
});

it('renders a linkTab as a link element', () => {
const link = 'url-link';
renderStudioContentMenu({
linkTabs: [
{
...tab1,
renderTab: (props) => <a href={link} {...props} />,
},
],
});
const linkTab = screen.getByRole('tab', { name: tab1Name });
expect(linkTab).toBeInTheDocument();
expect(linkTab).toHaveAttribute('href', link);
});

it('allows changing focus to next tab using keyboard', async () => {
const user = userEvent.setup();
renderStudioContentMenu({
buttonTabs: [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({
buttonTabs: [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({
buttonTabs: [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({
buttonTabs: [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({
buttonTabs: [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({
buttonTabs: [tab1],
});
const menuTab = screen.getByRole('tab', { name: tab1Name });
await user.click(menuTab);
expect(onChangeTabMock).toHaveBeenCalledTimes(1);
expect(onChangeTabMock).toHaveBeenCalledWith(tab1Id);
});
});

type RenderStudioContentMenuProps<TabId extends string> = {
buttonTabs: StudioContentMenuButtonTabProps<TabId>[];
linkTabs: StudioContentMenuLinkTabProps<TabId>[];
};

const renderStudioContentMenu = ({
buttonTabs = [],
linkTabs = [],
}: Partial<RenderStudioContentMenuProps<StudioMenuTabName>> = {}) => {
render(
<StudioContentMenu selectedTabId={undefined} onChangeTab={onChangeTabMock}>
{buttonTabs.map((buttonTab) => (
<StudioContentMenu.ButtonTab
key={buttonTab.tabId}
icon={buttonTab.icon}
tabId={buttonTab.tabId}
tabName={buttonTab.tabName}
/>
))}
{linkTabs.map((linkTab) => (
<StudioContentMenu.LinkTab
key={linkTab.tabId}
icon={linkTab.icon}
tabId={linkTab.tabId}
tabName={linkTab.tabName}
renderTab={linkTab.renderTab}
/>
))}
</StudioContentMenu>,
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { Children, forwardRef, useState } from 'react';
import type { ReactElement, ReactNode } from 'react';
import classes from './StudioContentMenu.module.css';
import { StudioContentMenuContextProvider } from './context/StudioContentMenuContext';

export type StudioContentMenuProps<TabId extends string> = {
children: ReactNode;
selectedTabId: TabId;
onChangeTab: (tabId: TabId) => void;
};

function StudioContentMenuForwarded<TabId extends string>(
{ children, selectedTabId, onChangeTab }: StudioContentMenuProps<TabId>,
ref: React.Ref<HTMLDivElement>,
): ReactElement {
const firstTabId = getFirstTabId(children);
const [selectedTab, setSelectedTab] = useState<TabId>(selectedTabId ?? firstTabId);

const handleChangeTab = (tabId: TabId) => {
onChangeTab(tabId);
setSelectedTab(tabId);
};

const isTabSelected = (tabId: TabId) => selectedTab === tabId;

return (
<div ref={ref} className={classes.menuContainer}>
<div ref={ref} className={classes.tabsContainer} role='tablist'>
<StudioContentMenuContextProvider
isTabSelected={isTabSelected}
onChangeTab={handleChangeTab}
>
{children}
</StudioContentMenuContextProvider>
</div>
</div>
);
}

export const StudioContentMenu = forwardRef<HTMLDivElement, StudioContentMenuProps<string>>(
StudioContentMenuForwarded,
);

const getFirstTabId = (children: ReactNode) => {
return Children.toArray(children).filter((child): child is ReactElement =>
React.isValidElement(child),
)[0]?.props.tabId;
};
Loading

0 comments on commit 349425d

Please sign in to comment.