Skip to content

Commit

Permalink
fix(device-page): fix search in header device selector
Browse files Browse the repository at this point in the history
  • Loading branch information
krzysztof-kwitt authored and nurikk committed Feb 3, 2024
1 parent d845d85 commit 919a3aa
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 51 deletions.
56 changes: 56 additions & 0 deletions src/components/device-page/header-device-selector.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// import '@testing-library/jest-dom';
import React from 'react';
import { createMockDevice, fireEvent, render, screen, waitFor } from '../../test-utils';
import { it, expect } from 'vitest';
import { HeaderDeviceSelector } from './header-device-selector';

it('all devices are visible in dropdown', async () => {
const ieeeAddress1 = 'FF:FF:FF:FF:FF:AA';
const ieeeAddress2 = 'AA:AA:AA:AA:AA:AA';
const currentDevice = createMockDevice({ieee_address: ieeeAddress1, friendly_name: "Button"})
const devices = {
[ieeeAddress1]: currentDevice,
[ieeeAddress2]: createMockDevice({ieee_address: ieeeAddress2, friendly_name: "Light"}),
}
render(
<HeaderDeviceSelector allDevices={devices} currentDevice={currentDevice}/>,
);
await waitFor(() => fireEvent.click(screen.getByLabelText(/Select a device/)));
expect(document.querySelectorAll('.dropdown-item')).toHaveLength(2)
});

it('device can be selected', async () => {
const ieeeAddress1 = 'FF:FF:FF:FF:FF:AA';
const ieeeAddress2 = 'AA:AA:AA:AA:AA:AA';
const currentDevice = createMockDevice({ieee_address: ieeeAddress1, friendly_name: "Button"})
const devices = {
[ieeeAddress1]: currentDevice,
[ieeeAddress2]: createMockDevice({ieee_address: ieeeAddress2, friendly_name: "Light"}),
}

render(
<HeaderDeviceSelector allDevices={devices} currentDevice={currentDevice}/>,
);

await waitFor(() => fireEvent.click(screen.getByLabelText(/Select a device/)));
await waitFor(() => fireEvent.click(screen.getByText(/Light/)));
expect(document.location.toString()).toContain(ieeeAddress2)
});

it('devices list can be filtered', async () => {
const ieeeAddress1 = 'FF:FF:FF:FF:FF:AA';
const ieeeAddress2 = 'AA:AA:AA:AA:AA:AA';
const currentDevice = createMockDevice({ieee_address: ieeeAddress1, friendly_name: "Button"})
const devices = {
[ieeeAddress1]: currentDevice,
[ieeeAddress2]: createMockDevice({ieee_address: ieeeAddress2, friendly_name: "Light"}),
}
render(
<HeaderDeviceSelector allDevices={devices} currentDevice={currentDevice}/>,
);

await waitFor(() => fireEvent.click(screen.getByLabelText(/Select a device/)));
await waitFor(() => fireEvent.change(screen.getByPlaceholderText(/Type to filter.../), {target: {value: 'Light'}}));

expect(document.querySelectorAll('.dropdown-item')).toHaveLength(1)
});
79 changes: 29 additions & 50 deletions src/components/device-page/header-device-selector.tsx
Original file line number Diff line number Diff line change
@@ -1,90 +1,69 @@
import React, { CSSProperties, PropsWithChildren, forwardRef, useState } from 'react';
import React, { useState } from 'react';
import Dropdown from 'react-bootstrap/Dropdown';
import Form from 'react-bootstrap/Form';
import { Devices } from '../../store';
import { TabName } from './types';
import { WithTranslation } from 'react-i18next';
import { TFunction } from 'i18next';
import { Device } from '../../types';
import Button from '../button';
import { withTranslation } from 'react-i18next';

interface SearchMenuProps {
style?: CSSProperties;
className?: string;
searchTerm?: string;
setSearchTerm: (value: string) => void;
t: TFunction;
}

interface HeaderDeviceSelectorProps {
devices: Devices;
dev: string;
allDevices: Devices;
currentDevice: Device;
tab?: TabName;
t: TFunction;
}

interface HeaderDeviceSelectorItemsProps {
devices: Devices;
dev: string;
devices: Device[];
currentDevice: Device;
tab?: TabName;
setSearchTerm: (value: string) => void;
}

const SearchMenu = forwardRef<HTMLDivElement, PropsWithChildren<SearchMenuProps>>(function SearchMenu(
{ children, style, className, searchTerm, setSearchTerm, t },
ref,
) {
return (
<div ref={ref} style={style} className={className}>
<Form.Control
autoFocus
className="mx-3 my-2 w-auto"
placeholder={t('type_to_filter')}
onChange={(e) => setSearchTerm(e.target.value)}
value={searchTerm}
/>
<ul className="list-unstyled">
{React.Children.toArray(children).filter(
(child) =>
!searchTerm ||
(React.isValidElement(child) &&
child.props.children.toLowerCase().includes(searchTerm.toLowerCase())),
)}
</ul>
</div>
);
});

export function HeaderDeviceSelector(props: HeaderDeviceSelectorProps): JSX.Element {
const { devices, dev, tab = 'info', t } = props;
export function HeaderDeviceSelectorRaw(props: HeaderDeviceSelectorProps): JSX.Element {
const { allDevices, currentDevice, tab = 'info', t } = props;
const [searchTerm, setSearchTerm] = useState<string>('');

const device = devices[dev];
const selectedDevices = Object.values(allDevices).filter(d => d.friendly_name.includes(searchTerm))

return (
<h1 className="h3">
<Dropdown>
<Dropdown.Toggle aria-label={t('select_a_device')} variant="" size="lg">
{device.friendly_name}{' '}
{currentDevice.friendly_name}{' '}
</Dropdown.Toggle>

<Dropdown.Menu as={SearchMenu} searchTerm={searchTerm} t={t} setSearchTerm={setSearchTerm}>
<HeaderDeviceSelectorItems devices={devices} dev={dev} tab={tab} setSearchTerm={setSearchTerm} />
<Dropdown.Menu>
<Form.Control
autoFocus
className="mx-3 my-2 w-auto"
placeholder={t('type_to_filter')}
onChange={(e) => setSearchTerm(e.target.value)}
value={searchTerm}
/>
<HeaderDeviceSelectorItems devices={selectedDevices} currentDevice={currentDevice} tab={tab} setSearchTerm={setSearchTerm} />
</Dropdown.Menu>
</Dropdown>
</h1>
);
}

function HeaderDeviceSelectorItems({ devices, dev, tab, setSearchTerm }: HeaderDeviceSelectorItemsProps): JSX.Element {
export const HeaderDeviceSelector = withTranslation('devicePage')(HeaderDeviceSelectorRaw);

function HeaderDeviceSelectorItems({ devices, currentDevice, tab, setSearchTerm }: HeaderDeviceSelectorItemsProps): JSX.Element {
return (
<>
{Object.entries(devices).map(([id, device]) => (
{Object.values(devices).map(listDevice => (
<Dropdown.Item
active={id === dev}
key={id}
href={`#/device/${id}/${tab}`}
active={currentDevice.ieee_address === listDevice.ieee_address}
key={listDevice.ieee_address}
href={`#/device/${listDevice.ieee_address}/${tab}`}
onClick={() => setSearchTerm('')}
>
{device.friendly_name}
{listDevice.friendly_name}
</Dropdown.Item>
))}
</>
Expand Down
2 changes: 1 addition & 1 deletion src/components/device-page/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export function DevicePage(props: DevicePageProps): JSX.Element {

return (
<>
<HeaderDeviceSelector devices={devices} dev={dev} tab={tab} t={t} />
<HeaderDeviceSelector allDevices={devices} currentDevice={device} tab={tab} />
<div className="tab">
<ul className="nav nav-tabs">
{links.map((link) => (
Expand Down

0 comments on commit 919a3aa

Please sign in to comment.