export * from './icons';
export * from './layouts';
export * from './popups';
+export * from './subnavigation';
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import styled from '@emotion/styled';
+import { ReactNode, useCallback, useState } from 'react';
+import tw from 'twin.macro';
+import { themeColor, themeContrast } from '../../helpers/theme';
+import { BareButton } from '../buttons';
+import { OpenCloseIndicator } from '../icons/OpenCloseIndicator';
+import { SubnavigationGroup } from './SubnavigationGroup';
+
+interface Props {
+ children: ReactNode;
+ className?: string;
+ header: ReactNode;
+ id: string;
+ initExpanded?: boolean;
+}
+
+export function SubnavigationAccordion(props: Props) {
+ const { children, className, header, id, initExpanded } = props;
+ const [expanded, setExpanded] = useState(initExpanded);
+ const toggleExpanded = useCallback(() => {
+ setExpanded((expanded) => !expanded);
+ }, [setExpanded]);
+
+ return (
+ <SubnavigationGroup
+ aria-labelledby={`${id}-subnavigation-accordion-button`}
+ className={className}
+ id={`${id}-subnavigation-accordion`}
+ role="region"
+ >
+ <SubnavigationAccordionItem
+ aria-controls={`${id}-subnavigation-accordion`}
+ aria-expanded={expanded}
+ id={`${id}-subnavigation-accordion-button`}
+ onClick={toggleExpanded}
+ >
+ {header}
+ <OpenCloseIndicator open={Boolean(expanded)} />
+ </SubnavigationAccordionItem>
+ {expanded && children}
+ </SubnavigationGroup>
+ );
+}
+
+const SubnavigationAccordionItem = styled(BareButton)`
+ ${tw`sw-flex sw-items-center sw-justify-between`}
+ ${tw`sw-box-border`}
+ ${tw`sw-body-sm-highlight`}
+ ${tw`sw-p-4`}
+ ${tw`sw-w-full`}
+ ${tw`sw-cursor-pointer`}
+
+ color: ${themeContrast('subnavigation')};
+ background-color: ${themeColor('subnavigation')};
+ transition: 0.2 ease;
+ transition-property: border-left, background-color, color;
+
+ &:hover,
+ &:focus {
+ background-color: ${themeColor('subnavigationHover')};
+ }
+`;
+SubnavigationAccordionItem.displayName = 'SubnavigationAccordionItem';
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import styled from '@emotion/styled';
+import { Children, Fragment, HtmlHTMLAttributes, ReactNode } from 'react';
+import tw from 'twin.macro';
+import { themeBorder, themeColor } from '../../helpers/theme';
+import { isDefined } from '../../helpers/types';
+
+interface Props extends HtmlHTMLAttributes<HTMLDivElement> {
+ children: ReactNode;
+ className?: string;
+}
+
+export function SubnavigationGroup({ className, children, ...htmlProps }: Props) {
+ const childrenArray = Children.toArray(children).filter(isDefined);
+ return (
+ <Group className={className} {...htmlProps}>
+ {childrenArray.map((child, index) => (
+ <Fragment key={index}>
+ {child}
+ {index < childrenArray.length - 1 && <Separator />}
+ </Fragment>
+ ))}
+ </Group>
+ );
+}
+
+const Group = styled.div`
+ ${tw`sw-relative`}
+ ${tw`sw-flex sw-flex-col`}
+ ${tw`sw-w-full`}
+
+ background-color: ${themeColor('subnavigation')};
+ border: ${themeBorder('default', 'subnavigationBorder')};
+`;
+
+const Separator = styled.div`
+ ${tw`sw-w-full`}
+
+ height: 1px;
+ background-color: ${themeColor('subnavigationSeparator')};
+`;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import styled from '@emotion/styled';
+import tw from 'twin.macro';
+import { themeColor, themeContrast } from '../../helpers/theme';
+
+export const SubnavigationHeading = styled.div`
+ ${tw`sw-flex sw-items-center sw-justify-between`}
+ ${tw`sw-box-border`}
+ ${tw`sw-body-sm`}
+ ${tw`sw-p-4`}
+ ${tw`sw-w-full`}
+
+ color: ${themeContrast('subnavigation')};
+ background-color: ${themeColor('subnavigation')};
+`;
+SubnavigationHeading.displayName = 'SubnavigationHeading';
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import styled from '@emotion/styled';
+import classNames from 'classnames';
+import { ReactNode, useCallback } from 'react';
+import tw, { theme as twTheme } from 'twin.macro';
+import { themeBorder, themeColor, themeContrast } from '../../helpers/theme';
+import { BareButton } from '../buttons';
+
+interface Props {
+ active?: boolean;
+ children: ReactNode;
+ className?: string;
+ innerRef?: (node: HTMLButtonElement) => void;
+ onClick: (value?: string) => void;
+ value?: string;
+}
+
+export function SubnavigationItem(props: Props) {
+ const { active, className, children, innerRef, onClick, value } = props;
+ const handleClick = useCallback(() => {
+ onClick(value);
+ }, [onClick, value]);
+ return (
+ <SubnavigationItemStyled
+ aria-current={active}
+ className={classNames('js-subnavigation-item', { active }, className)}
+ onClick={handleClick}
+ ref={innerRef}
+ >
+ {children}
+ </SubnavigationItemStyled>
+ );
+}
+
+const SubnavigationItemStyled = styled(BareButton)`
+ ${tw`sw-flex sw-items-center sw-justify-between`}
+ ${tw`sw-box-border`}
+ ${tw`sw-body-sm`}
+ ${tw`sw-py-4 sw-pr-4`}
+ ${tw`sw-w-full`}
+ ${tw`sw-cursor-pointer`}
+
+ padding-left: calc(${twTheme('spacing.4')} - 3px);
+ color: ${themeContrast('subnavigation')};
+ background-color: ${themeColor('subnavigation')};
+ border-left: ${themeBorder('active', 'transparent')};
+ transition: 0.2 ease;
+ transition-property: border-left, background-color, color;
+
+ &:hover,
+ &:focus,
+ &.active {
+ background-color: ${themeColor('subnavigationHover')};
+ }
+
+ &.active {
+ color: ${themeContrast('subnavigationHover')};
+ border-left: ${themeBorder('active')};
+ }
+`;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { render } from '../../../helpers/testUtils';
+import { FCProps } from '../../../types/misc';
+import { SubnavigationAccordion } from '../SubnavigationAccordion';
+
+it('should have correct style and html structure', () => {
+ setupWithProps();
+
+ expect(screen.getByRole('button', { expanded: false })).toBeVisible();
+ expect(screen.queryByText('Foo')).not.toBeInTheDocument();
+});
+
+it('should display expanded', () => {
+ setupWithProps({ initExpanded: true });
+
+ expect(screen.getByRole('button', { expanded: true })).toBeVisible();
+ expect(screen.getByText('Foo')).toBeVisible();
+});
+
+it('should toggle expand', async () => {
+ const user = userEvent.setup();
+ setupWithProps();
+
+ expect(screen.queryByText('Foo')).not.toBeInTheDocument();
+ await user.click(screen.getByRole('button'));
+ expect(screen.getByText('Foo')).toBeVisible();
+ await user.click(screen.getByRole('button'));
+ expect(screen.queryByText('Foo')).not.toBeInTheDocument();
+});
+
+function setupWithProps(props: Partial<FCProps<typeof SubnavigationAccordion>> = {}) {
+ return render(
+ <SubnavigationAccordion header="Header" id="test" initExpanded={false} {...props}>
+ <span>Foo</span>
+ </SubnavigationAccordion>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { screen } from '@testing-library/react';
+import { render } from '../../../helpers/testUtils';
+import { FCProps } from '../../../types/misc';
+import { SubnavigationItem } from '../SubnavigationItem';
+
+it('should render correctly', () => {
+ setupWithProps();
+
+ expect(screen.getByRole('button', { current: false })).toBeVisible();
+});
+
+it('should display selected', () => {
+ setupWithProps({ active: true });
+
+ expect(screen.getByRole('button', { current: true })).toBeVisible();
+});
+
+it('should call onClick with value when clicked', async () => {
+ const onClick = jest.fn();
+ const { user } = setupWithProps({ onClick });
+
+ await user.click(screen.getByRole('button'));
+ expect(onClick).toHaveBeenCalledWith('foo');
+});
+
+function setupWithProps(props: Partial<FCProps<typeof SubnavigationItem>> = {}) {
+ return render(
+ <SubnavigationItem active={false} onClick={jest.fn()} value="foo" {...props}>
+ Foo
+ </SubnavigationItem>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+export * from './SubnavigationAccordion';
+export * from './SubnavigationGroup';
+export * from './SubnavigationHeading';
+export * from './SubnavigationItem';