Skip to content

Commit

Permalink
feat: add selection state to Detail View (#5868)
Browse files Browse the repository at this point in the history
---------
Co-authored-by: Caleb Pollman <[email protected]>
  • Loading branch information
reesscot authored Oct 10, 2024
1 parent 36976bb commit f229fde
Show file tree
Hide file tree
Showing 11 changed files with 551 additions and 128 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react';

import { CLASS_BASE } from '../views/constants';
import {
ViewElement,
LabelElement,
InputElement,
} from '../context/elements/definitions';

const BLOCK_NAME = `${CLASS_BASE}__checkbox`;
export const INPUT_CLASSNAME = `${BLOCK_NAME}-input`;
export const LABEL_CLASSNAME = `${BLOCK_NAME}-label`;

interface CheckboxControlProps {
checked?: boolean;
disabled?: boolean;
id?: string;
labelHidden?: boolean;
labelText?: string;
onSelect?: () => void;
}

export const CheckboxControl = ({
disabled,
checked,
onSelect,
labelText,
labelHidden = false,
id,
}: CheckboxControlProps): React.JSX.Element => (
<ViewElement className={BLOCK_NAME}>
<InputElement
checked={checked}
className={INPUT_CLASSNAME}
disabled={disabled}
id={id}
onChange={onSelect}
type="checkbox"
/>
<LabelElement
className={[
LABEL_CLASSNAME,
labelHidden ? `${CLASS_BASE}-visually-hidden` : undefined,
].join(' ')}
htmlFor={id}
>
{labelText}
</LabelElement>
</ViewElement>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { CheckboxControl, INPUT_CLASSNAME, LABEL_CLASSNAME } from '../Checkbox';

const myLabelText = 'My Checkbox';
const handleSelect = jest.fn();

describe('CheckboxControl', () => {
it('renders the CheckboxControl', () => {
render(
<CheckboxControl
id="checkbox-id"
checked={false}
labelText={myLabelText}
onSelect={handleSelect}
/>
);
const input = screen.getByRole('checkbox');
const label = screen.getByText(myLabelText);
expect(input).toBeInTheDocument();
expect(input).not.toHaveAttribute('checked');
expect(input).toHaveClass(INPUT_CLASSNAME);
expect(label).toHaveClass(LABEL_CLASSNAME);
expect(label).toBeInTheDocument();
});

it('renders the CheckboxControl checked', () => {
render(
<CheckboxControl
id="checkbox-id"
checked
labelText={myLabelText}
onSelect={handleSelect}
/>
);
const input = screen.getByRole('checkbox');
expect(input).toHaveAttribute('checked');
});

it('accepts onSelect prop', () => {
render(
<CheckboxControl
id="checkbox-id"
checked
labelText={myLabelText}
onSelect={handleSelect}
/>
);
const input = screen.getByRole('checkbox');
fireEvent.click(input);
expect(handleSelect).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ export const InputElement = defineBaseElement<
| 'onBlur'
| 'onChange'
| 'onFocus'
| 'checked'
| 'defaultChecked'
>({
type: 'input',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,96 @@ describe('locationActionsReducer', () => {
};
expect(newState).toEqual(expectedState);
});
it.todo('handles a SET_FILES as expected');
it.todo('handles a SELECT_LOCATION_ITEM as expected');
it.todo('handles a DESELECT_LOCATION_ITEM as expected');
it.todo('handles a DESELECT_ALL_LOCATION_ITEMS as expected');

it('handles a TOGGLE_SELECT_ITEM as expected', () => {
const initialState: LocationActionsState = {
actions: {},
selected: {
type: undefined,
items: [],
},
};
const item = {
key: 'key',
lastModified: new Date(),
size: 1000,
type: 'FILE' as const,
};
const action: LocationActionsAction = {
type: 'TOGGLE_SELECTED_ITEM',
item,
};
const newState = locationActionsReducer(initialState, action);

const expectedState = {
actions: {},
selected: {
type: undefined,
items: [item],
},
};
expect(newState).toEqual(expectedState);

const unToggleAction: LocationActionsAction = {
type: 'TOGGLE_SELECTED_ITEM',
item,
};
const unToggledState = locationActionsReducer(newState, unToggleAction);

const unToggledExpectedState = {
actions: {},
selected: {
type: undefined,
items: [],
},
};
expect(unToggledState).toEqual(unToggledExpectedState);
});

it('handles a TOGGLE_SELECT_ITEMS as expected', () => {
const initialState: LocationActionsState = {
actions: {},
selected: {
type: undefined,
items: [],
},
};
const item = {
key: 'key',
lastModified: new Date(),
size: 1000,
type: 'FILE' as const,
};
const action: LocationActionsAction = {
type: 'TOGGLE_SELECTED_ITEMS',
items: [item, item, item],
};
const newState = locationActionsReducer(initialState, action);

const expectedState = {
actions: {},
selected: {
type: undefined,
items: [item, item, item],
},
};
expect(newState).toEqual(expectedState);

const unselectAllAction: LocationActionsAction = {
type: 'TOGGLE_SELECTED_ITEMS',
};
const unselectAllState = locationActionsReducer(
initialState,
unselectAllAction
);

const unselectAllExpectedState = {
actions: {},
selected: {
type: undefined,
items: [],
},
};
expect(unselectAllState).toEqual(unselectAllExpectedState);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,29 @@ export function locationActionsReducer(
const selected = { ...state.selected, type: action.payload };
return { ...state, selected };
}
case 'TOGGLE_SELECTED_ITEM': {
const hasItem = !!state.selected.items?.some(
(item) => item.key === action.item.key
);
const selectedItems = hasItem
? {
items: state.selected.items?.filter(
(item) => item.key !== action.item.key
),
}
: {
items: [
...(state.selected.items ? state.selected.items : []),
action.item,
],
};
const selected = { ...state.selected, ...selectedItems };
return { ...state, selected };
}
case 'TOGGLE_SELECTED_ITEMS': {
const selected = { ...state.selected, items: [...(action.items ?? [])] };
return { ...state, selected };
}
case 'CLEAR': {
// reset state
return getInitialState(state.actions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ export interface LocationActions<T = Permission> {
export type LocationActionsAction<T = string> =
| { type: 'CLEAR' }
| { type: 'SET_ACTION'; payload: T }
| { type: 'SET_LOCATION_ITEM'; item: LocationItem }
| { type: 'UNSET_LOCATION_ITEM'; key: string };
| { type: 'TOGGLE_SELECTED_ITEM'; item: LocationItem }
| { type: 'TOGGLE_SELECTED_ITEMS'; items?: LocationItem[] };

export interface LocationActionsState<T = string> {
actions: LocationActions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ function NavigateContainer({
export function NavigateControl(): React.JSX.Element {
const [{ history, location }, handleUpdateState] = useControl('NAVIGATE');
const [{ isLoading }, handleUpdateList] = useAction('LIST_LOCATION_ITEMS');
const [, handleLocationActionsState] = useControl('LOCATION_ACTIONS');

const { bucket } = location
? parseLocationAccess(location)
Expand All @@ -99,6 +100,7 @@ export function NavigateControl(): React.JSX.Element {
onClick={() => {
handleUpdateState({ type: 'EXIT' });
handleUpdateList({ prefix: '', options: { reset: true } });
handleLocationActionsState({ type: 'CLEAR' });
}}
>
{HOME_NAVIGATE_ITEM}
Expand All @@ -123,6 +125,7 @@ export function NavigateControl(): React.JSX.Element {
key={`${prefix}/${position}`}
onClick={() => {
handleUpdateState({ type: 'NAVIGATE', entry });
handleLocationActionsState({ type: 'CLEAR' });
}}
isCurrent={isCurrent}
>
Expand Down
Loading

0 comments on commit f229fde

Please sign in to comment.