buttonBar.tsx 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. import {Children, cloneElement, isValidElement} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import classNames from 'classnames';
  5. import {ButtonProps, StyledButton} from 'sentry/components/button';
  6. import space, {ValidSize} from 'sentry/styles/space';
  7. type ButtonBarProps = {
  8. children: React.ReactNode;
  9. active?: ButtonProps['barId'];
  10. className?: string;
  11. gap?: ValidSize;
  12. merged?: boolean;
  13. };
  14. function ButtonBar({
  15. children,
  16. className,
  17. active,
  18. merged = false,
  19. gap = 0,
  20. }: ButtonBarProps) {
  21. const shouldCheckActive = typeof active !== 'undefined';
  22. return (
  23. <ButtonGrid merged={merged} gap={gap} className={className}>
  24. {!shouldCheckActive
  25. ? children
  26. : Children.map(children, child => {
  27. if (!isValidElement(child)) {
  28. return child;
  29. }
  30. const {props: childProps, ...childWithoutProps} = child;
  31. // We do not want to pass `barId` to <Button>`
  32. const {barId, ...props} = childProps;
  33. const isActive = active === barId;
  34. // This ["primary"] could be customizable with a prop,
  35. // but let's just enforce one "active" type for now
  36. const priority = isActive ? 'primary' : childProps.priority || 'default';
  37. return cloneElement(childWithoutProps as React.ReactElement, {
  38. ...props,
  39. className: classNames(className, {active: isActive}),
  40. priority,
  41. });
  42. })}
  43. </ButtonGrid>
  44. );
  45. }
  46. const MergedStyles = () => css`
  47. /* Raised buttons show borders on both sides. Useful to create pill bars */
  48. & > .active {
  49. z-index: 2;
  50. }
  51. & > .dropdown,
  52. & > button,
  53. & > input,
  54. & > a {
  55. position: relative;
  56. /* First button is square on the right side */
  57. &:first-child:not(:last-child) {
  58. border-top-right-radius: 0;
  59. border-bottom-right-radius: 0;
  60. & > .dropdown-actor > ${StyledButton} {
  61. border-top-right-radius: 0;
  62. border-bottom-right-radius: 0;
  63. }
  64. }
  65. /* Middle buttons are square */
  66. &:not(:last-child):not(:first-child) {
  67. border-radius: 0;
  68. & > .dropdown-actor > ${StyledButton} {
  69. border-radius: 0;
  70. }
  71. }
  72. /* Middle buttons only need one border so we don't get a double line */
  73. &:first-child {
  74. & + .dropdown:not(:last-child),
  75. & + a:not(:last-child),
  76. & + input:not(:last-child),
  77. & + button:not(:last-child) {
  78. margin-left: -1px;
  79. }
  80. }
  81. /* Middle buttons only need one border so we don't get a double line */
  82. /* stylelint-disable-next-line no-duplicate-selectors */
  83. &:not(:last-child):not(:first-child) {
  84. & + .dropdown,
  85. & + button,
  86. & + input,
  87. & + a {
  88. margin-left: -1px;
  89. }
  90. }
  91. /* Last button is square on the left side */
  92. &:last-child:not(:first-child) {
  93. border-top-left-radius: 0;
  94. border-bottom-left-radius: 0;
  95. margin-left: -1px;
  96. & > .dropdown-actor > ${StyledButton} {
  97. border-top-left-radius: 0;
  98. border-bottom-left-radius: 0;
  99. margin-left: -1px;
  100. }
  101. }
  102. }
  103. `;
  104. const ButtonGrid = styled('div')<{gap: ValidSize; merged: boolean}>`
  105. display: grid;
  106. grid-auto-flow: column;
  107. grid-column-gap: ${p => space(p.gap)};
  108. align-items: center;
  109. ${p => p.merged && MergedStyles}
  110. `;
  111. export {ButtonGrid, MergedStyles};
  112. export default ButtonBar;