Skip to content

Commit

Permalink
fix: allow using slotted elements in Select (#169)
Browse files Browse the repository at this point in the history
  • Loading branch information
web-padawan authored Dec 12, 2023
1 parent 54fb83c commit 04ece1e
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 4 deletions.
28 changes: 25 additions & 3 deletions src/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
type ComponentType,
type ForwardedRef,
forwardRef,
isValidElement,
type ReactElement,
type ReactNode,
useEffect,
Expand All @@ -16,15 +17,35 @@ export * from './generated/Select.js';

export type SelectReactRendererProps = ReactSimpleRendererProps<SelectElement>;

type SelectRenderer = ComponentType<SelectReactRendererProps>;

export type SelectProps = Partial<Omit<_SelectProps, 'children' | 'renderer'>> &
Readonly<{
children?: ReactNode | ComponentType<SelectReactRendererProps>;
renderer?: ComponentType<SelectReactRendererProps> | null;
children?: ReactNode | SelectRenderer | Array<ReactNode | SelectRenderer>;
renderer?: SelectRenderer | null;
}>;

function Select(props: SelectProps, ref: ForwardedRef<SelectElement>): ReactElement | null {
// React.Children.toArray() doesn't allow functions, so we convert manually.
const children = Array.isArray(props.children) ? props.children : [props.children];

// Components with slot attribute should stay in light DOM.
const slottedChildren = children.filter((child): child is ReactNode => {
return isValidElement(child) && child.props.slot;
});

// Component without slot attribute should go to the overlay.
const overlayChildren = children.filter((child): child is ReactNode => {
return isValidElement(child) && !slottedChildren.includes(child);
});

const renderFn = children.find((child) => typeof child === 'function');

const innerRef = useRef<SelectElement>(null);
const [portals, renderer] = useSimpleOrChildrenRenderer(props.renderer, props.children);
const [portals, renderer] = useSimpleOrChildrenRenderer(
props.renderer,
renderFn || (overlayChildren.length ? overlayChildren : undefined),
);
const finalRef = useMergedRefs(innerRef, ref);

useEffect(() => {
Expand All @@ -35,6 +56,7 @@ function Select(props: SelectProps, ref: ForwardedRef<SelectElement>): ReactElem

return (
<_Select {...props} ref={finalRef} renderer={renderer}>
{slottedChildren}
{portals}
</_Select>
);
Expand Down
47 changes: 47 additions & 0 deletions test/Select.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,51 @@ describe('Select', () => {
await expect(findByQuerySelector('vaadin-select-value-button')).to.eventually.have.text('Bar');
});
});

describe('slot', () => {
it('should render the element with slot if renderer prop is set', async () => {
render(
<Select renderer={Renderer}>
<div slot="prefix">Value:</div>
</Select>,
);

await expect(findByQuerySelector('div[slot="prefix"]')).to.eventually.have.text('Value:');
});

it('should render the element with slot if items prop is set', async () => {
render(
<Select items={items}>
<div slot="prefix">Value:</div>
</Select>,
);

await expect(findByQuerySelector('div[slot="prefix"]')).to.eventually.have.text('Value:');
});

it('should render the element with slot if children render function is set', async () => {
render(
<Select>
{Renderer}
<div slot="prefix">Value:</div>
</Select>,
);

await expect(findByQuerySelector('div[slot="prefix"]')).to.eventually.have.text('Value:');
});

it('should render the element with slot if children component is set', async () => {
render(
<Select>
<ListBox>
<Item value="foo">Foo</Item>
<Item value="bar">Bar</Item>
</ListBox>
<div slot="prefix">Value:</div>
</Select>,
);

await expect(findByQuerySelector('div[slot="prefix"]')).to.eventually.have.text('Value:');
});
});
});
7 changes: 6 additions & 1 deletion test/kitchen-sink/Row8.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { RadioButton } from '../../src/RadioButton.js';
import { RadioGroup } from '../../src/RadioGroup.js';
import { RichTextEditor } from '../../src/RichTextEditor.js';
import { Select } from '../../src/Select.js';
import { Tooltip } from '../../src/Tooltip.js';

function SelectListBox() {
return (
Expand All @@ -27,7 +28,11 @@ export default function Row8() {
</RadioButton>
</RadioGroup>
<RichTextEditor></RichTextEditor>
<Select label="Select" value="2" renderer={SelectListBox} />
<Select label="Select" value="2">
{SelectListBox}
<span slot="prefix">+</span>
<Tooltip slot="tooltip" text="Select tooltip"></Tooltip>
</Select>
</BoardRow>
);
}

0 comments on commit 04ece1e

Please sign in to comment.