Skip to content

Commit

Permalink
v4.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
based-ghost committed Nov 23, 2022
1 parent 7725280 commit 78192bb
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 65 deletions.
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,17 @@ $ yarn add react-window styled-components react-functional-select

```jsx
import { Select } from 'react-functional-select';
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, type ComponentProps } from 'react';
import { Card, CardHeader, CardBody, Container, SelectContainer } from '../shared/components';

type SelectProps = ComponentProps<typeof Select>;

type Option = Readonly<{
id: number;
city: string;
state: string;
}>;

type SingleSelectProps = Readonly<{
isDisabled: boolean;
}>;

const CITY_OPTIONS: Option[] = [
{ id: 1, city: 'Austin', state: 'TX' },
{ id: 2, city: 'Denver', state: 'CO' },
Expand All @@ -67,16 +65,18 @@ const CITY_OPTIONS: Option[] = [
{ id: 5, city: 'Houston', state: 'TX' }
];

const SingleSelect: React.FC<SingleSelectProps> = ({ isDisabled }) => {
const SingleSelect: React.FC<SelectProps> = ({ isDisabled }) => {
const [isInvalid, setIsInvalid] = useState<boolean>(false);
const [selectedOption, setSelectedOption] = useState<Option | null>(null);

const getOptionValue = useCallback((option: Option): number => option.id, []);
const onOptionChange = useCallback((option: Option | null): void => setSelectedOption(option), []);
const getOptionLabel = useCallback((option: Option): string => `${option.city}, ${option.state}`, []);
const getOptionValue = useCallback((opt: Option): number => opt.id, []);
const onOptionChange = useCallback((opt: Option | null): void => setSelectedOption(opt), []);
const getOptionLabel = useCallback((opt: Option): string => `${opt.city}, ${opt.state}`, []);

useEffect(() => {
isDisabled && setIsInvalid(false);
if (isDisabled) {
setIsInvalid(false);
}
}, [isDisabled]);

return (
Expand Down
1 change: 1 addition & 0 deletions __stories__/helpers/components/CodeMarkup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter';
const dark = require('react-syntax-highlighter/dist/esm/styles/prism/dark').default;
const markup = require('react-syntax-highlighter/dist/esm/languages/prism/markup').default;
const javascript = require('react-syntax-highlighter/dist/esm/languages/prism/javascript').default;

SyntaxHighlighter.registerLanguage('markup', markup);
SyntaxHighlighter.registerLanguage('javascript', javascript);

Expand Down
16 changes: 10 additions & 6 deletions __tests__/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ export const getOptionSingle = (index: number = 0): Option => ({ ...OPTIONS[inde
export const getSelectedOptionSingle = (): SelectedOption[] => {
const data = getOptionSingle();
const { value, label } = data;
const option: SelectedOption = { data, value, label };

return [option];
return [{ data, value, label }];
};

// ============================================
Expand Down Expand Up @@ -60,9 +58,15 @@ export const MENU_OPTIONS: MenuOption[] = [MENU_OPTION_SELECTED, MENU_OPTION_DIS
// ============================================

export const stringifyCSSProperties = (obj: CSSProperties = {}): string => {
const cssProps = Object.keys(obj).map((key) => `${key}: ${obj[key]};`);
return cssProps.join(' ');
return Object.keys(obj)
.map((key) => `${key}: ${obj[key]};`)
.join(' ');
};

export const RENDER_OPTION_LABEL_MOCK = jest.fn(({ label }: OptionData): ReactNode => label);
export const RENDER_MULTI_OPTIONS_MOCK = jest.fn(({ selected, renderOptionLabel }: MultiParams): ReactNode => selected.map((option) => renderOptionLabel(option.data)).join(', '));

export const RENDER_MULTI_OPTIONS_MOCK = jest.fn(
({selected, renderOptionLabel}: MultiParams): ReactNode => {
return selected.map((option) => renderOptionLabel(option.data)).join(', ');
}
);
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
"jest-environment-jsdom": "^29.3.1",
"jest-enzyme": "^7.1.2",
"npm-run-all": "^4.1.5",
"prettier": "^2.7.1",
"prettier": "^2.8.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-syntax-highlighter": "^15.5.0",
Expand Down
20 changes: 9 additions & 11 deletions src/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,19 @@ import { OPTION_CLS, EMPTY_ARRAY, OPTION_FOCUSED_CLS, OPTION_SELECTED_CLS, OPTIO
const DIACRITICS_REG_EXP = /[\u0300-\u036f]/g;

/**
* @private
*
* Strips all diacritics from a string.
* May not be supported by all legacy browsers (IE11 >=).
*/
const stripDiacritics = (val: string): string => val.normalize('NFD').replace(DIACRITICS_REG_EXP, '');
const stripDiacritics = (val: string): string => {
return val.normalize('NFD').replace(DIACRITICS_REG_EXP, '');
};

// Simple helper functions
export const isBoolean = (val: unknown): val is boolean => typeof val === 'boolean';
export const isFunction = (val: unknown): val is CallbackFn => typeof val === 'function';
export const isArrayWithLength = (val: unknown): boolean => Array.isArray(val) && !!val.length;
export const isPlainObject = (val: unknown): boolean => val !== null && typeof val === 'object' && !Array.isArray(val);

/**
* Prevent default behavior and propagation of an event.
* Prevent default behavior and propagation of an event
*/
export const suppressEvent = (e: SyntheticEvent<Element>): void => {
e.preventDefault();
Expand All @@ -28,7 +26,7 @@ export const suppressEvent = (e: SyntheticEvent<Element>): void => {

/**
* Apply regex to string, and if the value is NOT case sensitive,
* call .toLowerCase() and return result.
* call .toLowerCase() and return result
*/
export const trimAndFormatFilterStr = (
value: string,
Expand All @@ -43,7 +41,7 @@ export const trimAndFormatFilterStr = (
};

/**
* Builds the className property in Option.tsx component.
* Builds the className property in Option.tsx component
*/
export const buildOptionClsName = (
isDisabled: boolean,
Expand All @@ -63,7 +61,7 @@ export const buildOptionClsName = (
};

/**
* Parses an object or an array of objects into output of SelectedOption[].
* Parses an object or an array of objects into output of SelectedOption[]
*/
export const normalizeValue = (
value: unknown,
Expand All @@ -88,9 +86,9 @@ export const normalizeValue = (
};

/**
* Immutable implementation of mergeDeep for two objects. Will return the merged result.
* Immutable implementation of mergeDeep for two objects. Will return the merged result
* In first condition of if/else block - check that property is no 'animation',
* since we never want to merge that complex styled-component object.
* since we never want to merge that complex styled-component object
*/
export const mergeDeep = <T>(target: any, source: any): T => {
const output = { ...target };
Expand Down
62 changes: 25 additions & 37 deletions src/utils/menu.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,33 @@
import type { CallbackFn } from '../types';

/**
* @private
*
* @param t: time (elapsed)
* @param b: initial value
* @param c: amount of change
* @param d: duration
*/
function easeOutCubic(t: number, b: number, c: number, d: number): number {
const easeOutCubic = (
t: number,
b: number,
c: number,
d: number
): number => {
return c * ((t = t / d - 1) * t * t + 1) + b;
}
};

/**
* @private
*/
function getScrollTop(el: HTMLElement): number {
const getScrollTop = (el: HTMLElement): number => {
return isDocumentElement(el) ? window.pageYOffset : el.scrollTop;
}
};

/**
* @private
*/
function scrollTo(el: HTMLElement, top: number): void {
const scrollTo = (el: HTMLElement, top: number): void => {
isDocumentElement(el) ? window.scrollTo(0, top) : (el.scrollTop = top);
}
};

/**
* @private
*/
function isDocumentElement(el: HTMLElement | typeof window): boolean {
const isDocumentElement = (el: HTMLElement | typeof window): boolean => {
return el === document.body || el === document.documentElement || el === window;
}
};

/**
* @private
*/
function getScrollParent(el: HTMLElement): HTMLElement {
const getScrollParent = (el: HTMLElement): HTMLElement => {
let style = getComputedStyle(el);

if (style.position === 'fixed') {
Expand All @@ -57,34 +48,31 @@ function getScrollParent(el: HTMLElement): HTMLElement {
}

return document.documentElement;
}
};

/**
* @private
*/
function smoothScrollTo(
const smoothScrollTo = (
el: HTMLElement,
to: number,
duration: number = 300,
callback?: CallbackFn
): void {
): void => {
let currentTime = 0;
const start = getScrollTop(el);
const change = to - start;

function scrollFn() {
const scrollFn = () => {
currentTime += 5;
const calcScrollTop = easeOutCubic(currentTime, start, change, duration);
scrollTo(el, calcScrollTop);
(currentTime < duration) ? requestAnimationFrame(scrollFn) : callback?.();
}
};

requestAnimationFrame(scrollFn);
}
};

/**
* Calculates the top property value for the MenuWrapper <div />.
* This property is only generated when the position of the menu is above the control.
* Calculates the top property value for the MenuWrapper element
* This property is only generated when the position of the menu is above the control
*/
export const calculateMenuTop = (
menuHeight: number,
Expand All @@ -110,7 +98,7 @@ export const menuFitsBelowControl = (el: HTMLElement | null): boolean => {

/**
* Calculate space around the control and menu to determine if an animated
* scroll can performed to show the menu in full view. Also, execute a callback if defined.
* scroll can performed to show the menu in full view. Also, execute a callback if defined
*/
export const scrollMenuIntoViewOnOpen = (
menuEl: HTMLElement | null,
Expand Down Expand Up @@ -138,8 +126,8 @@ export const scrollMenuIntoViewOnOpen = (
const spaceBelow = scrollParent.getBoundingClientRect().height - scrollTop - top;
const notEnoughSpaceBelow = spaceBelow < height;

// Sufficient space does not exist to scroll menu fully into view.
// Calculate available space and use that as the the new menuHeight (use scrollSpaceBelow for now).
// Sufficient space does not exist to scroll menu fully into view
// Calculate available space and use that as the the new menuHeight (use scrollSpaceBelow for now)
// OR scrollMenuIntoView = false
if (notEnoughSpaceBelow || !scrollMenuIntoView) {
const condensedMenuHeight = notEnoughSpaceBelow ? spaceBelow : undefined;
Expand Down

0 comments on commit 78192bb

Please sign in to comment.