Skip to content

Commit

Permalink
feat(Option): enable passing children flexibly without disturbance fr…
Browse files Browse the repository at this point in the history
…om icon prop
  • Loading branch information
filiptammergard committed Apr 27, 2023
1 parent 33ccb66 commit dba167f
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 42 deletions.
5 changes: 5 additions & 0 deletions .changeset/gold-garlics-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@einride/ui": minor
---

Option: Enable passing children flexibly without disturbance from icon prop.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from "react"
import { useScrollIntoView } from "../../../../hooks/useScrollIntoView"
import { zIndex } from "../../../../lib/zIndex"
import { Icon } from "../../../content/Icon/Icon"
import { Box, BoxProps } from "../../../layout/Box/Box"
import { Option } from "../../../menus/Option/Option"

Expand Down Expand Up @@ -261,7 +262,7 @@ export const MultiSelect = <Option extends BaseOption>({
{filteredOptions?.map((option, index) => (
<Option
key={option.value}
focused={index === highlightedDropdownIndex}
data-focused={index === highlightedDropdownIndex}
onClick={(e) => {
e.stopPropagation()
handleOptionSelect(option)
Expand All @@ -272,13 +273,13 @@ export const MultiSelect = <Option extends BaseOption>({
ref={(node: HTMLDivElement) => {
optionRefs.current[option.value] = node
}}
selected={!!selectedOptions?.includes(option)}
aria-selected={!!selectedOptions?.includes(option)}
role="option"
variant="secondary"
{...optionProps}
>
{option.label}
{!!selectedOptions?.includes(option) && <Icon name="checkmark" />}
</Option>
))}
</OptionsWrapper>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ export const SearchSelect = <Option extends BaseOption>({
<OptionsWrapper role="listbox" aria-labelledby={id} {...dropdownProps} ref={scrollableRef}>
{filteredOptions?.map((option, index) => (
<Option
focused={index === selectedIndex}
data-focused={index === selectedIndex}
key={option.key ?? option.value}
variant="secondary"
onClick={(e) => {
Expand Down
24 changes: 24 additions & 0 deletions packages/einride-ui/src/components/menus/Option/Option.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,27 @@ import { Option } from "@einride/ui"
<Canvas of={Option.Basic} />

<Controls of={Option.Basic} />

## Variant

<Description of={Option.Secondary} />

<Canvas of={Option.Secondary} />

<Controls of={Option.Secondary} include={["variant"]} />

## End text

<Description of={Option.WithEndText} />

<Canvas of={Option.WithEndText} />

<Controls of={Option.WithEndText} include={["children"]} />

## As button

<Description of={Option.AsButton} />

<Canvas of={Option.AsButton} />

<Controls of={Option.AsButton} include={["as"]} />
92 changes: 82 additions & 10 deletions packages/einride-ui/src/components/menus/Option/Option.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,117 @@
import { expect } from "@storybook/jest"
import { Meta, StoryObj } from "@storybook/react"
import { within } from "@storybook/testing-library"
import { Icon } from "../../content/Icon/Icon"
import { Text } from "../../typography/Text/Text"
import { Option } from "./Option"

const meta = {
component: Option,
argTypes: {
as: {
control: "text",
},
children: {
control: false,
},
},
} satisfies Meta<typeof Option>

export default meta
type Story = StoryObj<typeof meta>

export const Basic = {
args: {
children: "Option",
icon: <Icon name="arrowRight" />,
children: "Label",
},
} satisfies Story
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement)

export const Selected = {
args: {
...Basic.args,
selected: true,
await step("Expect children to show", async () => {
expect(canvas.getByText(Basic.args.children)).toBeInTheDocument()
})
},
} satisfies Story

/** Change the variant with the `variant` prop. */
export const Secondary = {
args: {
...Basic.args,
variant: "secondary",
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement)

await step("Expect children to show", async () => {
expect(canvas.getByText(Secondary.args.children)).toBeInTheDocument()
})
},
} satisfies Story

export const SecondarySelected = {
export const WithEndIcon = {
args: {
...Secondary.args,
selected: true,
children: (
<>
<Text as="span">Label</Text>
<Icon name="arrowRight" />
</>
),
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement)

await step("Expect children to show", async () => {
expect(canvas.getByText("Label")).toBeInTheDocument()
})
},
} satisfies Story

/** Use `children` to render content on the other side of the option. */
export const WithEndText = {
args: {
children: (
<>
<Text as="span">Label</Text>
<Text as="span" color="secondary">
Label
</Text>
</>
),
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement)

await step("Expect children to show", async () => {
expect(canvas.getAllByText("Label").length).toBe(2)
})
},
} satisfies Story

export const SecondaryWithEndLabel = {
args: {
...WithEndText.args,
variant: "secondary",
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement)

await step("Expect children to show", async () => {
expect(canvas.getAllByText("Label").length).toBe(2)
})
},
} satisfies Story

/** Use the `as` prop to change the rendered HTML element. */
export const AsButton = {
args: {
...Basic.args,
as: "button",
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement)

await step("Expect children to show", async () => {
expect(canvas.getByRole("button", { name: AsButton.args.children })).toBeInTheDocument()
})
},
} satisfies Story
38 changes: 9 additions & 29 deletions packages/einride-ui/src/components/menus/Option/Option.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import styled from "@emotion/styled"
import { ComponentPropsWithoutRef, ElementType, forwardRef, ReactNode } from "react"
import { Icon } from "../../content/Icon/Icon"

export interface OptionProps extends ComponentPropsWithoutRef<"div"> {
/** Rendered element. */
Expand All @@ -9,32 +8,15 @@ export interface OptionProps extends ComponentPropsWithoutRef<"div"> {
/** Option content. */
children: ReactNode

/** Icon shown at the end of the option row. */
icon?: ReactNode

/** Whether the option is selected or not. */
selected?: boolean

/** Whether the option is focused (e.g. via arrow keys) or not. */
focused?: boolean

/** Variant of the option. Default is `primary`. */
variant?: Variant
}

export const Option = forwardRef<HTMLDivElement, OptionProps>(
({ children, icon, selected, focused, variant = "primary", ...props }, forwardedRef) => {
({ children, variant = "primary", ...props }, forwardedRef) => {
return (
<Wrapper
aria-selected={selected}
data-focused={focused}
variant={variant}
tabIndex={0}
{...props}
ref={forwardedRef}
>
<Wrapper variant={variant} tabIndex={0} {...props} ref={forwardedRef}>
{children}
{selected ? <Icon name="checkmark" /> : icon}
</Wrapper>
)
},
Expand All @@ -52,7 +34,7 @@ const Wrapper = styled.div<WrapperProps>`
flex-direction: row;
justify-content: space-between;
align-items: center;
padding-block: ${({ theme }) => 1.25 * theme.spacer}px;
block-size: ${({ theme }) => 6 * theme.spacingBase}rem;
padding-inline: ${({ theme }) => 1 * theme.spacer}px;
color: ${({ theme }) => theme.colors.content.primary};
border-radius: ${({ theme }) => theme.borderRadii.sm};
Expand All @@ -61,24 +43,22 @@ const Wrapper = styled.div<WrapperProps>`
min-block-size: ${({ theme }) => 6 * theme.spacingBase}rem;
column-gap: ${({ theme }) => 2 * theme.spacingBase}rem;
inline-size: 100%;
background: ${({ variant, theme }) =>
variant === "secondary"
? theme.colors.background.secondaryElevated
: theme.colors.background.primary};
&:focus-visible {
outline: none;
background: ${({ theme }) => theme.colors.background.tertiary};
box-shadow: inset 0 0 0 1px ${({ theme }) => theme.colors.border.selected};
}
&:hover,
&:focus-visible,
&[data-focused="true"] {
background: ${({ variant, theme }) =>
variant === "secondary"
? theme.colors.background.tertiary
: theme.colors.background.secondaryElevated};
box-shadow: none;
}
&:focus-visible {
outline: none;
box-shadow: inset 0 0 0 1px ${({ theme }) => theme.colors.border.selected};
}
`

0 comments on commit dba167f

Please sign in to comment.