Skip to content

Commit

Permalink
(react-switch) - Adding dragging functionality (#19864)
Browse files Browse the repository at this point in the history
* Adding dragging

* Updating state and styles

* Cleaning up code

* Cleaing up custom stories

* Cleaning up code

* Updating API

* Adding RTL support and updating snapshots

* Change files

* Updating a doc comment

* Fixing a visual bug in controlled scenarios related to the renderedPosition.

* Fixing a bug in mobile screen readers due to stacked change events.

* Switching internalState interface to a type

* Updating tests
  • Loading branch information
czearing authored Sep 22, 2021
1 parent f4080a9 commit 047f5f2
Show file tree
Hide file tree
Showing 10 changed files with 444 additions and 117 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Adding dragging to the Switch component.",
"packageName": "@fluentui/react-switch",
"email": "czearing@outlook.com",
"dependentChangeType": "patch"
}
3 changes: 2 additions & 1 deletion packages/react-switch/etc/react-switch.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface SwitchCommons {
checked?: boolean;
defaultChecked?: boolean;
disabled?: boolean;
onChange?: (ev: React_2.ChangeEvent<HTMLInputElement>, data: {
onChange?: (ev: React_2.PointerEvent<HTMLDivElement> | React_2.KeyboardEvent<HTMLDivElement>, data: {
checked: boolean;
}) => void;
}
Expand All @@ -39,6 +39,7 @@ export type SwitchSlots = {
thumbWrapper: IntrinsicShorthandProps<'div'>;
thumb: IntrinsicShorthandProps<'div'>;
input: IntrinsicShorthandProps<'input'>;
activeRail: IntrinsicShorthandProps<'div'>;
};

// @public (undocumented)
Expand Down
98 changes: 67 additions & 31 deletions packages/react-switch/src/Switch.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,35 @@ const useIosStyles = makeStyles({
height: '30px',

':hover .ms-Switch-thumb': {
background: 'white',
':before': {
background: 'white',
},
},

':active .ms-Switch-thumb': {
background: 'white',
':before': {
background: 'white',
},
},

':hover .ms-Switch-track': {
borderColor: 'none',
},
// Unchecked
':before': {
borderColor: 'none',
},

'&.checked': {
':hover .ms-Switch-track': {
// Checked
':after': {
background: '#4cd964',
},
},

'& .ms-Switch-track': {
':active .ms-Switch-track': {
':before': {
borderColor: 'none',
},
':after': {
background: '#4cd964',
border: 'none',
},
},
},
Expand All @@ -51,16 +61,29 @@ const useIosStyles = makeStyles({
thumb: {
width: '27px',
height: '27px',
background: 'white',
boxShadow: `
0px 3px 8px 0px rgba(0, 0, 0, 0.15),
0px 3px 1px 0px rgba(0, 0, 0, 0.06)
`,
':before': {
opacity: 1,
background: 'white',
},
':after': {
opacity: 1,
background: 'white',
},
},

track: {
background: 'white',
border: '1px solid #e0e0e0',
':after': {
background: '#4cd964',
border: 'none',
},
':before': {
background: 'white',
border: '1px solid #e0e0e0',
},
},
});

Expand All @@ -70,28 +93,29 @@ const useMaterialStyles = makeStyles({
height: '14px',

':hover .ms-Switch-thumb': {
background: 'white',
},

':hover .ms-Switch-track': {
background: '#9f9f9f',
},

'&.checked': {
'& .ms-Switch-thumb': {
background: '#f50057',
':before': {
background: 'white',
},

':hover .ms-Switch-thumb': {
':after': {
background: '#f50057',
},
},

'& .ms-Switch-track': {
':hover .ms-Switch-track': {
':before': {
background: '#9f9f9f',
},
':after': {
background: '#fa80ab',
border: 'none',
},
},

':hover .ms-Switch-track': {
':active .ms-Switch-track': {
':before': {
background: '#9f9f9f',
},
':after': {
background: '#fa80ab',
border: 'none',
},
Expand All @@ -107,23 +131,35 @@ const useMaterialStyles = makeStyles({
thumb: {
width: '20px',
height: '20px',
background: 'white',
boxShadow: '0px 2px 1px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 1px 3px 0px rgb(0 0 0 / 12%)',
':before': {
background: 'white',
},
':after': {
background: '#f50057',
},
},

track: {
background: '#9f9f9f',
border: 'none',
':before': {
background: '#9f9f9f',
border: 'none',
},
':after': {
background: '#fa80ab',
border: 'none',
},
},
});

export const BasicSwitchExample = (props: SwitchProps) => {
const styles = useStyles();
const [switchValue, setSwitchValue] = React.useState(false);

const switchOnChange = (ev: React.ChangeEvent<HTMLInputElement>, data: { checked: boolean }) => {
setSwitchValue(data.checked);
};
const switchOnChange = (
ev: React.PointerEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>,
data: { checked: boolean },
) => setSwitchValue(data.checked);

return (
<div className={styles.root}>
Expand Down
53 changes: 39 additions & 14 deletions packages/react-switch/src/components/Switch/Switch.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,11 @@ describe('Switch', () => {
const inputRef = React.createRef<HTMLInputElement>();
const eventHandler = jest.fn();

render(<Switch checked={false} onChange={eventHandler} input={{ ref: inputRef }} />);
const inputElement = screen.getByRole('switch');
render(<Switch data-testid="root-element" checked={false} onChange={eventHandler} input={{ ref: inputRef }} />);
const rootElement = screen.getByTestId('root-element');

fireEvent.click(inputElement);
fireEvent.pointerDown(rootElement);
fireEvent.pointerUp(rootElement);

expect(eventHandler).toBeCalledTimes(1);
expect(eventHandler.mock.calls[0][1]).toEqual({ checked: true });
Expand All @@ -72,14 +73,17 @@ describe('Switch', () => {
it('calls onChange with the correct value', () => {
const eventHandler = jest.fn();

render(<Switch onChange={eventHandler} />);
render(<Switch data-testid="root-element" onChange={eventHandler} />);

const input = screen.getByRole('switch');
const rootElement = screen.getByTestId('root-element');
expect(eventHandler).toBeCalledTimes(0);

fireEvent.click(input);
fireEvent.click(input);
fireEvent.click(input);
fireEvent.pointerDown(rootElement);
fireEvent.pointerUp(rootElement);
fireEvent.pointerDown(rootElement);
fireEvent.pointerUp(rootElement);
fireEvent.pointerDown(rootElement);
fireEvent.pointerUp(rootElement);

expect(eventHandler).toBeCalledTimes(3);
expect(eventHandler.mock.calls[2][1]).toEqual({ checked: true });
Expand All @@ -98,25 +102,46 @@ describe('Switch', () => {
expect(inputRef?.current?.checked).toEqual(true);
});

it('handles keydown events', () => {
const inputRef = React.createRef<HTMLInputElement>();
const onChange = jest.fn();

render(<Switch onChange={onChange} data-testid="test" input={{ ref: inputRef }} />);
const rootElement = screen.getByTestId('test');

expect(onChange).toBeCalledTimes(0);

fireEvent.keyDown(rootElement, { key: ' ' });
expect(onChange).toBeCalledTimes(1);
expect(onChange.mock.calls[0][1]).toEqual({ checked: true });
expect(inputRef?.current?.checked).toEqual(true);

fireEvent.keyDown(rootElement, { key: ' ' });
expect(onChange).toBeCalledTimes(2);
expect(onChange.mock.calls[1][1]).toEqual({ checked: false });
expect(inputRef?.current?.checked).toEqual(false);
});

it('handles onKeyDown callback', () => {
const eventHandler = jest.fn();

render(<Switch onKeyDown={eventHandler} data-testid="test" />);
const switchRoot = screen.getByTestId('test');
const rootElement = screen.getByTestId('test');

expect(eventHandler).toBeCalledTimes(0);
fireEvent.keyDown(switchRoot, { key: 'ArrowUp' });

fireEvent.keyDown(rootElement, { key: ' ' });
expect(eventHandler).toBeCalledTimes(1);
});

it('handles onClick callback', () => {
it('handles onPointerDown callback', () => {
const eventHandler = jest.fn();

render(<Switch onClick={eventHandler} data-testid="test" />);
const switchRoot = screen.getByTestId('test');
render(<Switch data-testid="root-element" onPointerDown={eventHandler} />);
const rootElement = screen.getByTestId('root-element');

expect(eventHandler).toBeCalledTimes(0);
fireEvent.click(switchRoot);
fireEvent.pointerDown(rootElement);
expect(eventHandler).toBeCalledTimes(1);
});

Expand Down
7 changes: 6 additions & 1 deletion packages/react-switch/src/components/Switch/Switch.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export type SwitchSlots = {
* The hidden input that handles the Switch's internal functionality.
*/
input: IntrinsicShorthandProps<'input'>;

/**
* The area in which the Switch's rail allows for the thumb to be dragged.
*/
activeRail: IntrinsicShorthandProps<'div'>;
};

export interface SwitchCommons {
Expand Down Expand Up @@ -54,7 +59,7 @@ export interface SwitchCommons {
* Callback to be called when the `checked` value changes.
*/
onChange?: (
ev: React.ChangeEvent<HTMLInputElement>,
ev: React.PointerEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>,
data: {
checked: boolean;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ exports[`Switch Snapshot Tests renders a basic Switch checked 1`] = `
/>
<div
class=""
style="transform: translate(100%);"
style="transform: translate(100%); transition: transform .1s cubic-bezier(0.33, 0.0, 0.67, 1), opacity .1s cubic-bezier(0.33, 0.0, 0.67, 1);"
>
<div
class="ms-Switch-thumb"
Expand All @@ -20,9 +20,13 @@ exports[`Switch Snapshot Tests renders a basic Switch checked 1`] = `
<input
checked=""
class=""
readonly=""
role="switch"
type="checkbox"
/>
<div
class=""
/>
</div>
</div>
`;
Expand All @@ -38,17 +42,21 @@ exports[`Switch Snapshot Tests renders a basic Switch unchecked 1`] = `
/>
<div
class=""
style="transform: translate(0%);"
style="transform: translate(0%); transition: transform .1s cubic-bezier(0.33, 0.0, 0.67, 1), opacity .1s cubic-bezier(0.33, 0.0, 0.67, 1);"
>
<div
class="ms-Switch-thumb"
/>
</div>
<input
class=""
readonly=""
role="switch"
type="checkbox"
/>
<div
class=""
/>
</div>
</div>
`;
Expand All @@ -64,7 +72,7 @@ exports[`Switch Snapshot Tests renders a disabled Switch checked 1`] = `
/>
<div
class=""
style="transform: translate(100%);"
style="transform: translate(100%); transition: transform .1s cubic-bezier(0.33, 0.0, 0.67, 1), opacity .1s cubic-bezier(0.33, 0.0, 0.67, 1);"
>
<div
class="ms-Switch-thumb"
Expand All @@ -73,9 +81,13 @@ exports[`Switch Snapshot Tests renders a disabled Switch checked 1`] = `
<input
checked=""
class=""
readonly=""
role="switch"
type="checkbox"
/>
<div
class=""
/>
</div>
</div>
`;
Expand All @@ -91,17 +103,21 @@ exports[`Switch Snapshot Tests renders a disabled Switch unchecked 1`] = `
/>
<div
class=""
style="transform: translate(0%);"
style="transform: translate(0%); transition: transform .1s cubic-bezier(0.33, 0.0, 0.67, 1), opacity .1s cubic-bezier(0.33, 0.0, 0.67, 1);"
>
<div
class="ms-Switch-thumb"
/>
</div>
<input
class=""
readonly=""
role="switch"
type="checkbox"
/>
<div
class=""
/>
</div>
</div>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const renderSwitch = (state: SwitchState) => {
<slots.thumb {...slotProps.thumb} />
</slots.thumbWrapper>
<slots.input {...slotProps.input} />
<slots.activeRail {...slotProps.activeRail} />
</slots.root>
);
};
Loading

0 comments on commit 047f5f2

Please sign in to comment.