You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Tabs.tsx 4.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import styled from '@emotion/styled';
  21. import { PropsWithChildren } from 'react';
  22. import tw from 'twin.macro';
  23. import { OPACITY_20_PERCENT, themeBorder, themeColor } from '../helpers';
  24. import { BareButton } from '../sonar-aligned/components/buttons';
  25. import { getTabId, getTabPanelId } from '../sonar-aligned/helpers/tabs';
  26. import { Badge } from './Badge';
  27. type TabValueType = string | number | boolean;
  28. export interface TabOption<T extends TabValueType> {
  29. counter?: number;
  30. disabled?: boolean;
  31. label: string | React.ReactNode;
  32. value: T;
  33. }
  34. export interface TabsProps<T extends TabValueType> {
  35. className?: string;
  36. disabled?: boolean;
  37. label?: string;
  38. onChange: (value: T) => void;
  39. options: ReadonlyArray<TabOption<T>>;
  40. value?: T;
  41. }
  42. export function Tabs<T extends TabValueType>(props: PropsWithChildren<TabsProps<T>>) {
  43. const { disabled = false, label, options, value, className, children } = props;
  44. return (
  45. <TabsContainer className={className}>
  46. <TabList aria-label={label} role="tablist">
  47. {options.map((option) => (
  48. <TabButton
  49. aria-controls={getTabPanelId(String(option.value))}
  50. aria-current={option.value === value}
  51. aria-selected={option.value === value}
  52. data-value={option.value}
  53. disabled={disabled || option.disabled}
  54. id={getTabId(String(option.value))}
  55. key={option.value.toString()}
  56. onClick={() => {
  57. if (option.value !== value) {
  58. props.onChange(option.value);
  59. }
  60. }}
  61. role="tab"
  62. selected={option.value === value}
  63. >
  64. {option.label}
  65. {option.counter ? (
  66. <Badge className="sw-ml-2" variant="counterFailed">
  67. {option.counter}
  68. </Badge>
  69. ) : null}
  70. </TabButton>
  71. ))}
  72. </TabList>
  73. <RightSection>{children}</RightSection>
  74. </TabsContainer>
  75. );
  76. }
  77. const TabsContainer = styled.div`
  78. ${tw`sw-w-full`};
  79. ${tw`sw-pl-4`};
  80. ${tw`sw-flex sw-justify-between`};
  81. border-bottom: ${themeBorder('default')};
  82. `;
  83. const TabList = styled.div`
  84. ${tw`sw-inline-flex`};
  85. `;
  86. const TabButton = styled(BareButton)<{ selected: boolean }>`
  87. ${tw`sw-relative`};
  88. ${tw`sw-px-3 sw-py-1 sw-mb-[-1px]`};
  89. ${tw`sw-flex sw-items-center`};
  90. ${tw`sw-body-sm`};
  91. ${tw`sw-font-semibold`};
  92. ${tw`sw-rounded-t-1`};
  93. height: 34px;
  94. background: ${(props) => (props.selected ? themeColor('backgroundSecondary') : 'none')};
  95. color: ${(props) => (props.selected ? themeColor('tabSelected') : themeColor('tab'))};
  96. border: ${(props) =>
  97. props.selected ? themeBorder('default') : themeBorder('default', 'transparent')};
  98. border-bottom: ${(props) =>
  99. themeBorder('default', props.selected ? 'backgroundSecondary' : undefined)};
  100. &:hover {
  101. background: ${themeColor('tabHover')};
  102. }
  103. &:active {
  104. outline: ${themeBorder('xsActive', 'tabSelected', OPACITY_20_PERCENT)};
  105. z-index: 1;
  106. }
  107. // Active line
  108. &::after {
  109. content: '';
  110. ${tw`sw-absolute`};
  111. ${tw`sw-rounded-t-1`};
  112. top: 0;
  113. left: 0;
  114. width: calc(100%);
  115. height: 2px;
  116. background: ${(props) => (props.selected ? themeColor('tabSelected') : 'none')};
  117. }
  118. `;
  119. const RightSection = styled.div`
  120. max-height: 43px;
  121. ${tw`sw-flex sw-items-center`};
  122. `;