Skip to content

Commit eb28dbf

Browse files
authored
feat: support custom icon and clearIcon props (#297)
* test: add and configure jest-dom * feat(datepicker): allow custom icon and clear icon as components * test(datepicker): assert that custom icons work as expected * chore(stories): add selects for custom icon and clearIcon props * chore: improve type-checking and add story for custom icons * docs: add icon and clearIcon props to README
1 parent 33cbf6e commit eb28dbf

File tree

11 files changed

+280
-42
lines changed

11 files changed

+280
-42
lines changed

README.md

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -73,31 +73,32 @@ More examples [here](https://react-semantic-ui-datepickers.now.sh).
7373

7474
### Own Props
7575

76-
| property | type | required | default | description |
77-
| -------------------- | ------------ | -------- | ------------ | --------------------------------------------------------------------------------------------------------------- |
78-
| allowOnlyNumbers | boolean | no | true | Allows the user enter only numbers |
79-
| autoComplete | string | no | -- | Specifies if the input should have autocomplete enabled |
80-
| clearOnSameDateClick | boolean | no | true | Controls whether the datepicker's state resets if the same date is selected in succession. |
81-
| clearable | boolean | no | true | Allows the user to clear the value |
82-
| filterDate | function | no | () => true | Function that receives each date and returns a boolean to indicate whether it is enabled or not |
83-
| format | string | no | 'YYYY-MM-DD' | Specifies how the date will be formatted using the [date-fns' format](https://date-fns.org/v1.29.0/docs/format) |
84-
| keepOpenOnClear | boolean | no | false | Keeps the datepicker open (or opens it if it's closed) when the clear icon is clicked |
85-
| keepOpenOnSelect | boolean | no | false | Keeps the datepicker open when a date is selected |
86-
| inline | boolean | no | false | Uses an inline variant, without the input and the features related to it, e.g. clearable datepicker |
87-
| locale | string | no | 'en-US' | Filename of the locale to be used. PS: Feel free to submit PR's with more locales! |
88-
| onBlur | function | no | () => {} | Callback fired when the input loses focus |
89-
| onChange | function | no | () => {} | Callback fired when the value changes |
90-
| pointing | string | no | 'left' | Location to render the component around input. Available options: 'left', 'right', 'top left', 'top right' |
91-
| type | string | no | basic | Type of input to render. Available options: 'basic' and 'range' |
92-
| datePickerOnly | boolean | no | false | Allows the date to be selected only via the date picker and disables the text input |
93-
| value | Date\|Date[] | no | -- | The value of the datepicker |
76+
| property | type | required | default | description |
77+
| -------------------- | ----------------------------------- | -------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
78+
| allowOnlyNumbers | boolean | no | true | Allows the user enter only numbers |
79+
| autoComplete | string | no | -- | Specifies if the input should have autocomplete enabled |
80+
| clearIcon | SemanticICONS \| React.ReactElement | no | 'close' | An [icon from semantic-ui-react](https://react.semantic-ui.com/elements/icon/) or a custom component. The custom component will get two props: `data-testid` and `onClick`. |
81+
| clearOnSameDateClick | boolean | no | true | Controls whether the datepicker's state resets if the same date is selected in succession. |
82+
| clearable | boolean | no | true | Allows the user to clear the value |
83+
| datePickerOnly | boolean | no | false | Allows the date to be selected only via the date picker and disables the text input |
84+
| filterDate | function | no | () => true | Function that receives each date and returns a boolean to indicate whether it is enabled or not |
85+
| format | string | no | 'YYYY-MM-DD' | Specifies how the date will be formatted using the [date-fns' format](https://date-fns.org/v1.29.0/docs/format) |
86+
| icon | SemanticICONS \| React.ReactElement | no | 'calendar' | An [icon from semantic-ui-react](https://react.semantic-ui.com/elements/icon/) or a custom component. The custom component will get two props: `data-testid` and `onClick`. |
87+
| inline | boolean | no | false | Uses an inline variant, without the input and the features related to it, e.g. clearable datepicker |
88+
| keepOpenOnClear | boolean | no | false | Keeps the datepicker open (or opens it if it's closed) when the clear icon is clicked |
89+
| keepOpenOnSelect | boolean | no | false | Keeps the datepicker open when a date is selected |
90+
| locale | string | no | 'en-US' | Filename of the locale to be used. PS: Feel free to submit PR's with more locales! |
91+
| onBlur | function | no | () => {} | Callback fired when the input loses focus |
92+
| onChange | function | no | () => {} | Callback fired when the value changes |
93+
| pointing | string | no | 'left' | Location to render the component around input. Available options: 'left', 'right', 'top left', 'top right' |
94+
| type | string | no | basic | Type of input to render. Available options: 'basic' and 'range' |
95+
| value | Date\|Date[] | no | -- | The value of the datepicker |
9496

9597
### Form.Input Props
9698

9799
- autoComplete
98100
- disabled
99101
- error
100-
- icon
101102
- iconPosition
102103
- id
103104
- label

jest.setup.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import '@testing-library/jest-dom';

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"@storybook/addon-links": "5.3.19",
5151
"@storybook/addons": "5.3.19",
5252
"@storybook/react": "5.3.19",
53+
"@testing-library/jest-dom": "5.10.1",
5354
"@testing-library/react": "10.2.1",
5455
"@types/jest": "26.0.0",
5556
"@types/storybook__react": "5.2.1",
@@ -84,6 +85,7 @@
8485
"tsConfig": "tsconfig.test.json"
8586
}
8687
},
88+
"setupFilesAfterEnv": ["./jest.setup.ts"],
8789
"transform": {
8890
".+\\.css$": "jest-transform-css",
8991
".(js|ts)x?": "ts-jest"

src/__tests__/datepicker.test.tsx

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,12 @@ import DatePicker from '../';
99
const setup = (props: Partial<SemanticDatepickerProps> = {}) => {
1010
const options = render(<DatePicker onChange={jest.fn()} {...props} />);
1111
const getQuery = props.inline ? options.queryByTestId : options.getByTestId;
12+
const getIcon = () => options.getByTestId('datepicker-icon');
1213

1314
return {
1415
...options,
15-
openDatePicker: () => {
16-
const icon = options.getByTestId('datepicker-icon');
17-
18-
fireEvent.click(icon);
19-
},
16+
getIcon,
17+
openDatePicker: () => fireEvent.click(getIcon()),
2018
rerender: (newProps?: Partial<SemanticDatepickerProps>) =>
2119
options.rerender(
2220
<DatePicker onChange={jest.fn()} {...props} {...newProps} />
@@ -488,4 +486,71 @@ describe('Inline datepicker', () => {
488486
);
489487
});
490488
});
489+
490+
describe('Custom icons', () => {
491+
it('should allow for custom Semantic UI icons', () => {
492+
const icon = 'search';
493+
const { getByText, getIcon, openDatePicker } = setup({ icon });
494+
495+
// Assert custom icon
496+
expect(getIcon()).toHaveClass(icon, 'icon');
497+
// Select current date
498+
openDatePicker();
499+
fireEvent.click(getByText('Today'));
500+
// Assert datepicker is clearable
501+
expect(getIcon()).toHaveClass('close', 'icon');
502+
fireEvent.click(getIcon());
503+
// Assert datepicker was cleared
504+
expect(getIcon()).toHaveClass(icon, 'icon');
505+
});
506+
507+
it('should allow for custom icon components', () => {
508+
const { getByText, getIcon, openDatePicker } = setup({
509+
icon: <span>Custom icon</span>,
510+
});
511+
512+
// Assert custom icon
513+
expect(getIcon().textContent).toBe('Custom icon');
514+
// Select current date
515+
openDatePicker();
516+
fireEvent.click(getByText('Today'));
517+
// Assert datepicker is clearable
518+
expect(getIcon()).toHaveClass('close', 'icon');
519+
fireEvent.click(getIcon());
520+
// Assert datepicker was cleared
521+
expect(getIcon().textContent).toBe('Custom icon');
522+
});
523+
524+
it('should allow for custom clear Semantic UI icons', () => {
525+
const clearIcon = 'ban';
526+
const { getByText, getIcon, openDatePicker } = setup({ clearIcon });
527+
528+
// Select current date
529+
openDatePicker();
530+
fireEvent.click(getByText('Today'));
531+
// Assert custom icon
532+
expect(getIcon()).toHaveClass(clearIcon, 'icon');
533+
// Assert datepicker is clearable
534+
fireEvent.click(getIcon());
535+
// Assert datepicker was cleared
536+
expect(getIcon()).toHaveClass('calendar', 'icon');
537+
});
538+
539+
it('should allow for custom clear icon components', () => {
540+
const customClearIcon = <span>Custom icon</span>;
541+
const { getByText, getIcon, openDatePicker } = setup({
542+
clearIcon: customClearIcon,
543+
});
544+
545+
// Select current date
546+
openDatePicker();
547+
fireEvent.click(getByText('Today'));
548+
// Assert custom icon
549+
expect(getIcon().textContent).toBe('Custom icon');
550+
// Assert datepicker is clearable
551+
fireEvent.click(getIcon());
552+
// Assert datepicker was cleared
553+
expect(getIcon()).toHaveClass('calendar', 'icon');
554+
});
555+
});
491556
});

src/components/icon.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import React from 'react';
2+
import { Icon as SUIIcon } from 'semantic-ui-react';
3+
import { SemanticDatepickerProps } from '../types';
4+
5+
type CustomIconProps = {
6+
clearIcon: SemanticDatepickerProps['clearIcon'];
7+
icon: SemanticDatepickerProps['icon'];
8+
isClearIconVisible: boolean;
9+
onClear: () => void;
10+
onClick: () => void;
11+
};
12+
13+
const CustomIcon = ({
14+
clearIcon,
15+
icon,
16+
isClearIconVisible,
17+
onClear,
18+
onClick,
19+
}: CustomIconProps) => {
20+
if (isClearIconVisible && clearIcon && React.isValidElement(clearIcon)) {
21+
return React.cloneElement(clearIcon, {
22+
'data-testid': 'datepicker-icon',
23+
onClick: onClear,
24+
});
25+
}
26+
27+
if (isClearIconVisible && clearIcon && !React.isValidElement(clearIcon)) {
28+
return (
29+
<SUIIcon
30+
data-testid="datepicker-icon"
31+
link
32+
name={clearIcon}
33+
onClick={onClear}
34+
/>
35+
);
36+
}
37+
38+
if (icon && React.isValidElement(icon)) {
39+
return React.cloneElement(icon, {
40+
'data-testid': 'datepicker-icon',
41+
onClick,
42+
});
43+
}
44+
45+
return (
46+
<SUIIcon data-testid="datepicker-icon" link name={icon} onClick={onClick} />
47+
);
48+
};
49+
50+
CustomIcon.defaultProps = {
51+
clearIcon: 'close',
52+
icon: 'calendar',
53+
};
54+
55+
export default CustomIcon;

src/components/input.tsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import React from 'react';
2-
import { Form, Icon, Input, FormInputProps } from 'semantic-ui-react';
2+
import { Form, Input, FormInputProps } from 'semantic-ui-react';
3+
import { SemanticDatepickerProps } from '../types';
4+
import CustomIcon from './icon';
35

46
type InputProps = FormInputProps & {
7+
clearIcon: SemanticDatepickerProps['clearIcon'];
8+
icon: SemanticDatepickerProps['icon'];
59
isClearIconVisible: boolean;
610
};
711

812
const CustomInput = React.forwardRef<Input, InputProps>((props, ref) => {
913
const {
14+
clearIcon,
1015
icon,
1116
isClearIconVisible,
1217
label,
@@ -24,11 +29,12 @@ const CustomInput = React.forwardRef<Input, InputProps>((props, ref) => {
2429
{...rest}
2530
ref={ref}
2631
icon={
27-
<Icon
28-
data-testid="datepicker-icon"
29-
link
30-
name={isClearIconVisible ? 'close' : icon}
31-
onClick={isClearIconVisible ? onClear : onClick}
32+
<CustomIcon
33+
clearIcon={clearIcon}
34+
icon={icon}
35+
isClearIconVisible={isClearIconVisible}
36+
onClear={onClear}
37+
onClick={onClick}
3238
/>
3339
}
3440
onClick={onClick}
@@ -38,8 +44,4 @@ const CustomInput = React.forwardRef<Input, InputProps>((props, ref) => {
3844
);
3945
});
4046

41-
CustomInput.defaultProps = {
42-
icon: 'calendar',
43-
};
44-
4547
export default CustomInput;

src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const style: React.CSSProperties = {
2323
};
2424
const semanticInputProps = [
2525
'autoComplete',
26+
'clearIcon',
2627
'disabled',
2728
'error',
2829
'icon',

src/types/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FormInputProps } from 'semantic-ui-react';
1+
import { FormInputProps, SemanticICONS } from 'semantic-ui-react';
22

33
export type Object = { [key: string]: any };
44

@@ -41,7 +41,6 @@ export type PickedFormInputProps = Pick<
4141
FormInputProps,
4242
| 'disabled'
4343
| 'error'
44-
| 'icon'
4544
| 'iconPosition'
4645
| 'id'
4746
| 'label'
@@ -59,10 +58,12 @@ export type SemanticDatepickerProps = PickedDayzedProps &
5958
autoComplete?: string;
6059
clearOnSameDateClick: boolean;
6160
clearable: boolean;
61+
clearIcon?: SemanticICONS | React.ReactElement;
6262
filterDate: (date: Date) => boolean;
6363
format: string;
6464
keepOpenOnClear: boolean;
6565
keepOpenOnSelect: boolean;
66+
icon?: SemanticICONS | React.ReactElement;
6667
inline: boolean;
6768
locale: LocaleOptions;
6869
onBlur: (event?: React.SyntheticEvent) => void;

stories/data.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { ALL_ICONS_IN_ALL_CONTEXTS } from 'semantic-ui-react/src/lib/SUI';
2+
import { SemanticICONS } from 'semantic-ui-react';
3+
14
const types = <const>['basic', 'range'];
25
const pointing = <const>['left', 'right', 'top left', 'top right'];
36
const locale = <const>[
@@ -35,3 +38,7 @@ export const typeMap = arrayToMap(types);
3538
export const pointingMap = arrayToMap(pointing);
3639

3740
export const localeMap = arrayToMap([...locale].sort());
41+
42+
export const iconMap = arrayToMap<SemanticICONS>(
43+
ALL_ICONS_IN_ALL_CONTEXTS.sort()
44+
);

stories/index.stories.tsx

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ import {
88
text,
99
} from '@storybook/addon-knobs';
1010
import { storiesOf } from '@storybook/react';
11-
import { Form } from 'semantic-ui-react';
11+
import { Form, SemanticICONS } from 'semantic-ui-react';
1212
import 'semantic-ui-css/semantic.min.css';
1313
import SemanticDatepicker from '../src';
1414
import {
1515
Content,
16+
iconMap,
1617
isWeekday,
1718
localeMap,
1819
onChange,
@@ -37,14 +38,17 @@ stories.add('Basic usage', () => {
3738
const inline = boolean('Inline (without input)', false);
3839
const allowOnlyNumbers = boolean('Allow only numbers', false);
3940
const clearOnSameDateClick = boolean('Clear on same date click', true);
41+
const clearable = boolean('Clearable', true);
42+
const icon = select('Icon (without value)', iconMap, iconMap.calendar);
43+
const clearIcon = select('Clear icon (with value)', iconMap, iconMap.close);
44+
const iconOnLeft = boolean('Icon on the left', false);
4045
const datePickerOnly = boolean('Datepicker only', false);
4146
const firstDayOfWeek = number('First day of week', 0, { max: 6, min: 0 });
4247
const format = text('Format', 'YYYY-MM-DD');
4348
const keepOpenOnClear = boolean('Keep open on clear', false);
4449
const keepOpenOnSelect = boolean('Keep open on select', false);
4550
const locale = select('Locale', localeMap, localeMap['en-US']);
4651
const pointing = select('Pointing', pointingMap, pointingMap.left);
47-
const clearable = boolean('Clearable', true);
4852
const readOnly = boolean('Read-only', false);
4953
const showOutsideDays = boolean('Show outside days', false);
5054
const minDate = new Date(date('Min date', new Date('2018-01-01')));
@@ -64,15 +68,18 @@ stories.add('Basic usage', () => {
6468
<SemanticDatepicker
6569
key={key}
6670
allowOnlyNumbers={allowOnlyNumbers}
71+
clearIcon={clearIcon}
6772
clearOnSameDateClick={clearOnSameDateClick}
6873
clearable={clearable}
6974
datePickerOnly={datePickerOnly}
7075
filterDate={filterDate}
7176
firstDayOfWeek={firstDayOfWeek}
7277
format={format}
78+
icon={icon}
79+
iconPosition={iconOnLeft ? 'left' : undefined}
80+
inline={inline}
7381
keepOpenOnClear={keepOpenOnClear}
7482
keepOpenOnSelect={keepOpenOnSelect}
75-
inline={inline}
7683
locale={locale}
7784
maxDate={maxDate}
7885
minDate={minDate}
@@ -87,6 +94,26 @@ stories.add('Basic usage', () => {
8794
);
8895
});
8996

97+
stories.add('With custom icons', () => {
98+
const icon = select('Icon (without value)', iconMap, iconMap.calendar);
99+
const clearIcon = select('Clear icon (with value)', iconMap, iconMap.close);
100+
const useCustomIcon = boolean('Custom icon', false);
101+
const useCustomClearIcon = boolean('Custom clear icon', false);
102+
const CustomIcon = (props: any) => <button {...props}>Select</button>;
103+
const CustomClearIcon = (props: any) => <button {...props}>Reset</button>;
104+
const x = useCustomIcon ? ((<CustomIcon />) as unknown) : icon;
105+
const y = useCustomClearIcon ? ((<CustomClearIcon />) as unknown) : clearIcon;
106+
107+
return (
108+
<Content>
109+
<SemanticDatepicker
110+
clearIcon={y as SemanticICONS | React.ReactElement}
111+
icon={x as SemanticICONS | React.ReactElement}
112+
/>
113+
</Content>
114+
);
115+
});
116+
90117
stories.add('Usage with Form', () => {
91118
return (
92119
<Content>

0 commit comments

Comments
 (0)