freenowtech / wave

Design System of FREE NOW
https://wave.free-now.com
Apache License 2.0
64 stars 23 forks source link

New ButtonGroup component #99

Open phllipo opened 3 years ago

phllipo commented 3 years ago

We recently needed a group of buttons to be able to select different weekdays.

Screenshot 2021-06-02 at 11 21 08 Screenshot 2021-06-02 at 11 21 00

So I went ahead and created this using the Button and Box component and styling them to our needs.

Styling code:

const ButtonGroup = styled(Box)`
    border-radius: 4px;
    overflow: hidden;
    border: ${props =>
        props.error ? `1px solid ${Colors.NEGATIVE_ORANGE_900}` : `1px solid ${Colors.AUTHENTIC_BLUE_200}`};
`;

const ButtonGroupItem = styled(Button)`
    background: ${props => (props.selected ? Colors.ACTION_BLUE_900 : Colors.WHITE)};
    border-top: none;
    border-bottom: none;
    border-left: none;
    border-right: ${props =>
          props.selected ? `1px solid ${Colors.ACTION_BLUE_900}` : `1px solid ${Colors.AUTHENTIC_BLUE_200}`};

    padding: 0 8px;
    min-width: 0;
    border-radius: 0;

    :nth-child(1) {
        border-left: ${props =>
           props.selected ? `1px solid ${Colors.ACTION_BLUE_900}` : `1px solid ${Colors.AUTHENTIC_BLUE_200}`};
    }

    &:hover {
        background: ${props => (props.selected ? Colors.ACTION_BLUE_1000 : Colors.AUTHENTIC_BLUE_50)};
    }
`;

JSX code:

<ButtonGroup mt={3} error={error && !rule.daysOfWeek.length} display="flex">
    {days.map((day, index) => (
        <ButtonGroupItem
            type="button"
            key={`time-restriction-day-${index}`}
            variant={rule.daysOfWeek.includes(day.value) ? 'primary' : 'secondary'}
            selected={rule.daysOfWeek.includes(day.value)}
            size="small"
            onClick={() => handleDayChange(day.value)}
        >
            {day.label}
        </ButtonGroupItem>
    ))}
</ButtonGroup>

It feels a little bit hacky overriding a bunch of the styles, but it seemed like the best approach to have the elements look like all other buttons. What do you think about this solution and do you see this as something worth adding to wave?

rafael-sepeda commented 3 years ago

Cool! We are using a hardcoded version of this UI-element already in GAME. And if I'm not mistaking @jonotrujillo implemented it. It is not looking 100% like in the design, but maybe he has somethings that could help you.

jonotrujillo commented 3 years ago

It's quite similar. The one I implemented is looking like this:

Screen Shot 2021-06-02 at 17 21 49

In my case, I implemented the options as checkboxes as I felt this was the closest native behaviour:

export interface WeekdaySelectorProp {
  error?: boolean;
}

export const WeekdaySelector = styled(Box)<WeekdaySelectorProp>`
  box-shadow: inset 0 0 0 0.0625rem
    ${({ error }) => (error ? Colors.NEGATIVE_ORANGE_900 : 'transparent')};
  border-radius: 4px;
  border: 1px solid
    ${({ error }) =>
      error ? Colors.NEGATIVE_ORANGE_900 : Colors.AUTHENTIC_BLUE_200};
  display: flex;
  margin: calc(${Spaces[1]} - 1px) 0;
  overflow: hidden;
`;

const OptionLabel = styled.label<{
  inverted?: boolean;
}>`
  background-color: ${({ inverted }) =>
    inverted ? Colors.ACTION_BLUE_900 : 'transparent'};
  border-right: 1px solid ${Colors.AUTHENTIC_BLUE_200};
  color: ${({ inverted }) =>
    inverted ? Colors.WHITE : Colors.AUTHENTIC_BLUE_1100};
  cursor: pointer;
  flex: 1;
  overflow: hidden;
  padding: 0 ${Spaces[1]};
  text-align: center;
  text-overflow: ellipsis;
  white-space: nowrap;

  &:last-child {
    border-right: 0;
  }
`;

const OptionText = styled(Text)`
  font-size: 14px;
  font-weight: 600;
  line-height: ${Spaces[4]};
`;

export interface WeekdaySelectorOptionProps {
  checked?: boolean;
  label?: string;
  onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
}

export const WeekdaySelectorOption: VFC<WeekdaySelectorOptionProps> = ({
  checked,
  label,
  onChange,
}) => (
  <OptionLabel inverted={checked}>
    <input type="checkbox" checked={checked} onChange={onChange} hidden></input>
    <OptionText inverted={checked}>{label}</OptionText>
  </OptionLabel>
);

And used like this:

<WeekdaySelector error={hasError}>
  <WeekdaySelectorOption
     checked={modayChecked}
     onChange={(event) => onChange(event.target.checked)}
     label="Tuesday"
  />
  <WeekdaySelectorOption
    checked={tuesdayChecked}
    onChange={(event) => onChange(event.target.checked)}
    label="Tuesday"
   />
  ...
</WekdaySelector>
jonotrujillo commented 3 years ago

@phllipo: what is the use case to have variant and selected separately in ButtonGroupItem?

phllipo commented 3 years ago

@jonotrujillo oh that's also an interesting approach, wouldn't have thought of using checkbox, but from an API perspective it makes a lot of sense.

Initially I used the variant to have the usual button behaviour (colors, hover etc.) and then the selected for my styled component, but I probably overwrote most of the variant styling, so I can probably get rid of the variant.

snapsnapturtle commented 3 years ago

Good point, I can also see something a little more compact when it comes to the selected button (checkboxes) here: https://baseweb.design/components/button-group/#checkbox-mode. Maybe we can make the checkbox-style "you can select multiple buttons" as one of more use cases. I can also imagine having two buttons in the table in the "actions" column for example, which both need to function as buttons.