Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: TableRadioFilter 컴포넌트를 추가한다 #942

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { RangePickerField } from '../../RangePickerField';
import { useTableFilterGroup } from '../context';
import { TableFieldFilter } from '../TableFieldFilter';
import type { DateFilterOperator } from '../types';
import { isDateFilterValid, isValueRequiredOperator } from '../utils';
import { isDateFilterValid, isValuelessOperator } from '../utils';
import { withTableDateFilterVariation } from './TableDateFilterProps';

export const TableDateFilter = withTableDateFilterVariation(
Expand Down Expand Up @@ -35,6 +35,7 @@ export const TableDateFilter = withTableDateFilterVariation(
const prevOperatorRef = useRef<DateFilterOperator | undefined>();
const [fieldAutoFocus, setFieldAutoFocus] = useState(false);
const fieldRef = useRef<DatePickerFieldRefValue | null>(null);
const isMountedRef = useRef(false);

const {
translations: {
Expand All @@ -59,7 +60,11 @@ export const TableDateFilter = withTableDateFilterVariation(
);

useEffect(() => {
handleFilterChange();
if (isMountedRef.current) {
handleFilterChange();
}

isMountedRef.current = true;
}, [operator, value, handleFilterChange]);

useEffect(() => {
Expand Down Expand Up @@ -93,7 +98,7 @@ export const TableDateFilter = withTableDateFilterVariation(
minWidth={operator === 'between' ? 320 : 'auto'}
active={isDateFilterValid({ value, operator })}
onClose={() => {
if (isValueRequiredOperator(operator)) {
if (isValuelessOperator(operator)) {
setValue([]);
}

Expand All @@ -110,7 +115,7 @@ export const TableDateFilter = withTableDateFilterVariation(
}
}}
field={
!isValueRequiredOperator(operator) && (
!isValuelessOperator(operator) && (
<Box px={20}>
{operator === 'between' ? (
<RangePickerField
Expand All @@ -132,8 +137,6 @@ export const TableDateFilter = withTableDateFilterVariation(
autoFocus={fieldAutoFocus}
onValueChange={({ value: newValue }) => {
setValue(newValue ? [newValue] : []);

updateFilter();
}}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,20 @@ export const Basic: ComponentStory<typeof TableFilterGroup> = props => (
]}
/>
<TableFilterGroup.StringFilter dataKey="class" label="수강 중인 클래스" />
<TableFilterGroup.RadioFilter
dataKey="hidden"
label="숨김 여부"
operators={['equals']}
options={[
{
value: 'true',
label: '숨김',
},
{
value: 'false',
label: '숨기지 않음',
},
]}
/>
</TableFilterGroup>
);
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Children, Fragment, cloneElement, isValidElement, useRef, useState } from 'react';
import { Children, Fragment, cloneElement, isValidElement, useEffect, useRef, useState } from 'react';
import type { ComponentWithRef } from '@vibrant-ui/core';
import { ScrollBox, useConfig } from '@vibrant-ui/core';
import { Icon } from '@vibrant-ui/icons';
import { useCallbackRef } from '@vibrant-ui/utils';
import { Body } from '../Body';
import { Dropdown } from '../Dropdown';
import { GhostButton } from '../GhostButton';
Expand All @@ -15,6 +16,8 @@ import type { TableFilterGroupProps } from './TableFilterGroupProps';
import { withTableFilterGroupPropsVariation } from './TableFilterGroupProps';
import type { TableMultiSelectFilterProps } from './TableMultiSelectFilter';
import { TableMultiSelectFilter } from './TableMultiSelectFilter';
import type { TableRadioFilterProps } from './TableRadioFilter';
import { TableRadioFilter } from './TableRadioFilter';
import type { TableStringFilterProps } from './TableStringFilter';
import { TableStringFilter } from './TableStringFilter';
import type { TableFilterRefValue } from './types';
Expand All @@ -26,12 +29,10 @@ export const TableFilterGroup = withTableFilterGroupPropsVariation(
tableFilterGroup: { add, initialize },
},
} = useConfig();

const filterReferences = useRef<Record<string, TableFilterRefValue>>({});

const [currentFilterDataKeys, setCurrentFilterDataKeys] = useState<string[]>(initialFilterDataKeys);

const [isChanged, setIsChanged] = useState(false);
const isMountedRef = useRef(false);

const filterElements =
Children.toArray(children).filter(
Expand All @@ -41,52 +42,36 @@ export const TableFilterGroup = withTableFilterGroupPropsVariation(
const isAvailableFilterExist =
filterElements.filter(element => !currentFilterDataKeys.includes(element.props.dataKey)).length !== 0;

function checkInitialButtonState() {
if ([...initialFilterDataKeys].sort().join(',') !== [...currentFilterDataKeys].sort().join(',')) {
const checkInitialButtonState = useCallbackRef((filterDateKeys: string[]) => {
if ([...initialFilterDataKeys].sort().join(',') !== [...filterDateKeys].sort().join(',')) {
setIsChanged(true);

return;
}

setIsChanged(
!currentFilterDataKeys
.map(key => filterReferences.current[key].isDefaultState)
.every(isDefault => isDefault === true)
!filterDateKeys.map(key => filterReferences.current[key].isDefaultState).every(isDefault => isDefault === true)
);
}
});

const handleFilterChange = useCallbackRef(onFilterChange);

const addFilter = (filterDataKey: string) => {
setCurrentFilterDataKeys([...currentFilterDataKeys, filterDataKey]);

onFilterChange?.(
currentFilterDataKeys
.filter(key => filterReferences.current[key])
.map(key => filterReferences.current[key].value)
);

checkInitialButtonState();
};

const updateFilter = () => {
onFilterChange?.(
handleFilterChange(
currentFilterDataKeys
.filter(key => filterReferences.current[key])
.map(key => filterReferences.current[key].value)
);

checkInitialButtonState();
checkInitialButtonState(currentFilterDataKeys);
};

const deleteFilter = (filterDataKey: string) => {
setCurrentFilterDataKeys([...currentFilterDataKeys.filter(key => key !== filterDataKey)]);

onFilterChange?.(
currentFilterDataKeys
.filter(key => filterReferences.current[key])
.map(key => filterReferences.current[key].value)
);

checkInitialButtonState();
};

const onInitialize = () => {
Expand All @@ -95,11 +80,21 @@ export const TableFilterGroup = withTableFilterGroupPropsVariation(
});

setCurrentFilterDataKeys(initialFilterDataKeys);
};

onFilterChange?.(currentFilterDataKeys.map(key => filterReferences.current[key].value));
useEffect(() => {
if (isMountedRef.current) {
handleFilterChange(
currentFilterDataKeys
.filter(key => filterReferences.current[key])
.map(key => filterReferences.current[key].value)
);

checkInitialButtonState();
};
checkInitialButtonState(currentFilterDataKeys);
}

isMountedRef.current = true;
}, [checkInitialButtonState, currentFilterDataKeys, handleFilterChange]);

return (
<TableFilterGroupProvider
Expand Down Expand Up @@ -186,10 +181,13 @@ export const TableFilterGroup = withTableFilterGroupPropsVariation(
StringFilter: ComponentWithRef<TableStringFilterProps>;
DateFilter: ComponentWithRef<TableDateFilterProps>;
MultiSelectFilter: ComponentWithRef<TableMultiSelectFilterProps>;
RadioFilter: ComponentWithRef<TableRadioFilterProps>;
};

TableFilterGroup.StringFilter = TableStringFilter;

TableFilterGroup.DateFilter = TableDateFilter;

TableFilterGroup.MultiSelectFilter = TableMultiSelectFilter;

TableFilterGroup.RadioFilter = TableRadioFilter;
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ReactElement } from 'react';
import { withVariation } from '@vibrant-ui/core';
import type { TableDateFilterProps } from './TableDateFilter';
import type { TableMultiSelectFilterProps } from './TableMultiSelectFilter';
import type { TableRadioFilterProps } from './TableRadioFilter';
import type { TableStringFilterProps } from './TableStringFilter';
import type { Filter } from './types';

Expand All @@ -10,8 +11,10 @@ export type TableFilterGroupProps = {
initialFilterDataKeys?: string[];
testId?: string;
children:
| ReactElement<TableDateFilterProps | TableMultiSelectFilterProps | TableStringFilterProps>
| ReactElement<TableDateFilterProps | TableMultiSelectFilterProps | TableStringFilterProps>[];
| ReactElement<
TableDateFilterProps | TableMultiSelectFilterProps | TableRadioFilterProps | TableStringFilterProps
>[]
| ReactElement<TableDateFilterProps | TableMultiSelectFilterProps | TableRadioFilterProps | TableStringFilterProps>;
};

export const withTableFilterGroupPropsVariation = withVariation<TableFilterGroupProps>('TableFilterGroup')();
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useImperativeHandle, useState } from 'react';
import { useEffect, useImperativeHandle, useRef, useState } from 'react';
import { Box, useConfig } from '@vibrant-ui/core';
import { useCallbackRef } from '@vibrant-ui/utils';
import { CheckboxGroupField } from '../../CheckboxGroupField';
Expand All @@ -9,7 +9,7 @@ import { VStack } from '../../VStack';
import { useTableFilterGroup } from '../context';
import { TableFieldFilter } from '../TableFieldFilter';
import type { MultiSelectFilterOperator, Option } from '../types';
import { isMultiSelectFilterValid, isValueRequiredOperator } from '../utils';
import { isMultiSelectFilterValid, isValuelessOperator } from '../utils';
import { withTableMultiSelectFilterVariation } from './TableMultiSelectFilterProps';

export const TableMultiSelectFilter = withTableMultiSelectFilterVariation(
Expand All @@ -31,6 +31,7 @@ export const TableMultiSelectFilter = withTableMultiSelectFilterVariation(
const [operator, setOperator] = useState<MultiSelectFilterOperator>(defaultValue?.operator);
const { updateFilter } = useTableFilterGroup();
const handleFilterChange = useCallbackRef(updateFilter);
const isMountedRef = useRef(false);

const {
translations: {
Expand Down Expand Up @@ -61,7 +62,11 @@ export const TableMultiSelectFilter = withTableMultiSelectFilterVariation(
);

useEffect(() => {
handleFilterChange();
if (isMountedRef.current) {
handleFilterChange();
}

isMountedRef.current = true;
}, [selectedValues, operator, handleFilterChange]);

return (
Expand All @@ -81,7 +86,7 @@ export const TableMultiSelectFilter = withTableMultiSelectFilterVariation(
}
active={isMultiSelectFilterValid({ value: selectedValues, operator })}
onClose={() => {
if (isValueRequiredOperator(operator)) {
if (isValuelessOperator(operator)) {
setSelectedValues([]);
}
}}
Expand All @@ -91,7 +96,7 @@ export const TableMultiSelectFilter = withTableMultiSelectFilterVariation(
setOperator(operatorOption);
}}
field={
!isValueRequiredOperator(operator) && (
!isValuelessOperator(operator) && (
<VStack>
<Box px={20}>
<CheckboxGroupField
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { fireEvent, waitFor } from '@testing-library/react';
import { ConfigProvider, PortalRootProvider } from '@vibrant-ui/core';
import type { ReactRenderer } from '@vibrant-ui/utils/testing-web';
import { createReactRenderer } from '@vibrant-ui/utils/testing-web';
import { TableFilterGroup } from '../TableFilterGroup';
import { ko as TableFilterGroupTranslation } from '../translation';
import { TableRadioFilter } from './TableRadioFilter';

describe('<TableRadioFilter />', () => {
const { render } = createReactRenderer(children => (
<PortalRootProvider zIndex={1}>
<ConfigProvider
translations={{
tableFilterGroup: TableFilterGroupTranslation,
}}
>
<TableFilterGroup initialFilterDataKeys={['isHidden']}>{children}</TableFilterGroup>
</ConfigProvider>
</PortalRootProvider>
));
let renderer: ReactRenderer;

describe('when defaultValue not provided', () => {
beforeEach(() => {
renderer = render(
<TableRadioFilter
dataKey="isHidden"
label="숨김 여부"
options={[
{
value: 'true',
label: '숨김',
},
{
value: 'false',
label: '숨기지 않음',
},
]}
/>
);
});

it('should render button with label', () => {
expect(renderer.queryByRole('button', { name: '숨김 여부' })).toBeTruthy();
});
});

describe('when defaultValue provided with selected option value and operator that requires value', () => {
beforeEach(() => {
renderer = render(
<TableRadioFilter
dataKey="isHidden"
label="숨김 여부"
options={[
{
value: 'true',
label: '숨김',
},
{
value: 'false',
label: '숨기지 않음',
},
]}
defaultValue={{
value: 'true',
operator: 'equals',
}}
/>
);
});

it('should render button with label and selected option label', () => {
expect(renderer.queryByRole('button', { name: '숨김 여부: 숨김' })).toBeTruthy();
});

describe('when the button with label and selected option label', () => {
beforeEach(() => {
fireEvent.click(renderer.getByRole('button', { name: '숨김 여부: 숨김' }));
});

it('should render selected option with checked radio', async () => {
await waitFor(() => {
expect(renderer.queryByRole('radio', { name: '숨김', checked: true })).toBeTruthy();
});
});

it('should render options that are not selected with unchecked radio', async () => {
await waitFor(() => {
expect(renderer.queryByRole('radio', { name: '숨기지 않음', checked: false })).toBeTruthy();
});
});
});
});
});
Loading
Loading