buttonBar.tsx 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. import React from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import classNames from 'classnames';
  5. import Button, {StyledButton} from 'app/components/button';
  6. import space, {ValidSize} from 'app/styles/space';
  7. type ButtonBarProps = {
  8. children: React.ReactNode;
  9. gap?: ValidSize;
  10. merged?: boolean;
  11. active?: React.ComponentProps<typeof Button>['barId'];
  12. className?: string;
  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. : React.Children.map(children, child => {
  27. if (!React.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 React.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. & > a {
  54. position: relative;
  55. /* First button is square on the right side */
  56. &:first-child:not(:last-child) {
  57. border-top-right-radius: 0;
  58. border-bottom-right-radius: 0;
  59. & > .dropdown-actor > ${StyledButton} {
  60. border-top-right-radius: 0;
  61. border-bottom-right-radius: 0;
  62. }
  63. }
  64. /* Middle buttons are square */
  65. &:not(:last-child):not(:first-child) {
  66. border-radius: 0;
  67. & > .dropdown-actor > ${StyledButton} {
  68. border-radius: 0;
  69. }
  70. }
  71. /* Middle buttons only need one border so we don't get a double line */
  72. &:first-child {
  73. & + .dropdown:not(:last-child),
  74. & + a:not(:last-child),
  75. & + button:not(:last-child) {
  76. margin-left: -1px;
  77. }
  78. }
  79. /* Middle buttons only need one border so we don't get a double line */
  80. /* stylelint-disable-next-line no-duplicate-selectors */
  81. &:not(:last-child):not(:first-child) {
  82. & + .dropdown,
  83. & + button,
  84. & + a {
  85. margin-left: -1px;
  86. }
  87. }
  88. /* Last button is square on the left side */
  89. &:last-child:not(:first-child) {
  90. border-top-left-radius: 0;
  91. border-bottom-left-radius: 0;
  92. margin-left: -1px;
  93. & > .dropdown-actor > ${StyledButton} {
  94. border-top-left-radius: 0;
  95. border-bottom-left-radius: 0;
  96. margin-left: -1px;
  97. }
  98. }
  99. }
  100. `;
  101. const ButtonGrid = styled('div')<{gap: ValidSize; merged: boolean}>`
  102. display: grid;
  103. grid-auto-flow: column;
  104. grid-column-gap: ${p => space(p.gap)};
  105. align-items: center;
  106. ${p => p.merged && MergedStyles}
  107. `;
  108. export default ButtonBar;