Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src-ts/lib/form/form-groups/form-input/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './form-input-autcomplete-option.enum'
export * from './input-rating'
export * from './input-select'
export * from './input-text'
export * from './input-textarea'
export { inputOptional } from './input-wrapper'
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
@import '../../../../styles/includes';

.selected {
display: flex;
align-items: center;
margin-top: $space-xs;
cursor: pointer;
color: $black-100;

&-icon {
margin-left: auto;
padding: $border-xs 0;
color: $turq-160;
> svg {
@include icon-size(14);
}
}
}

.menu-wrap {
position: absolute;
top: calc(100% - 2px);
left: 0;
width: 100%;
&:not(:empty) {
z-index: 9;
}
}

.select-menu {
position: absolute;
top: 100%;
left: -1px;
right: -1px;
background: $tc-white;
border: $border-xs solid $black-40;
border-radius: 0 0 $space-xs $space-xs;
padding: $space-sm 0;
max-height: 230px;
overflow: auto;
}

.select-option {
font-weight: normal;
color: $black-100;
padding: $space-sm $space-lg;

&:hover:global(:not(.selected)) {
background: $turq-160;
color: $tc-white;
cursor: pointer;
}

&:global(.selected) {
font-weight: bold;
}
}
104 changes: 104 additions & 0 deletions src-ts/lib/form/form-groups/form-input/input-select/InputSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import classNames from 'classnames'
import {
ChangeEvent,
Dispatch,
FC,
MutableRefObject,
ReactNode,
SetStateAction,
useRef,
useState,
} from 'react'

import { useClickOutside } from '../../../../hooks'
import { IconOutline } from '../../../../svgs'
import { InputWrapper } from '../input-wrapper'

import styles from './InputSelect.module.scss'

export interface InputSelectOption {
label?: ReactNode
value: string
}

interface InputSelectProps {
readonly dirty?: boolean
readonly disabled?: boolean
readonly error?: string
readonly hideInlineErrors?: boolean
readonly hint?: string
readonly label?: string
readonly name: string
readonly onChange: (event: ChangeEvent<HTMLInputElement>) => void
readonly options: Array<InputSelectOption>
readonly tabIndex?: number
readonly value?: string
}

const InputSelect: FC<InputSelectProps> = (props: InputSelectProps) => {
const triggerRef: MutableRefObject<any> = useRef(undefined)
const [menuIsVisible, setMenuIsVisible]: [boolean, Dispatch<SetStateAction<boolean>>] = useState(false)

const selectedOption: InputSelectOption | undefined = props.options.find(option => option.value === props.value)

const label: (option: InputSelectOption) => ReactNode = (option?: InputSelectOption) => (
option ? option.label ?? option.value : ''
)

const toggleMenu: () => void = () => setMenuIsVisible((wasVisible) => !wasVisible)

const select: (option: InputSelectOption) => () => void = (option: InputSelectOption) => () => {
props.onChange({
target: {value: option.value} ,
} as unknown as ChangeEvent<HTMLInputElement>)
toggleMenu()
}

useClickOutside(triggerRef.current, () => setMenuIsVisible(false))

return (
<InputWrapper
{...props}
dirty={!!props.dirty}
disabled={!!props.disabled}
hint={props.hint ?? ''}
label={props.label ?? ''}
type='text'
className={styles['select-input-wrapper']}
hideInlineErrors={props.hideInlineErrors}
ref={triggerRef}
>
<div className={styles['selected']} onClick={toggleMenu}>
<span className='body-small'>{selectedOption ? label(selectedOption) : ''}</span>
<span className={styles['selected-icon']}>
<IconOutline.ChevronDownIcon />
</span>
</div>

<div className={styles['menu-wrap']}>
{menuIsVisible && (
<div className={styles['select-menu']}>
{props.options.map((option) => (
<div
className={
classNames(
styles['select-option'],
'body-main',
selectedOption === option && 'selected',
)
}
onClick={select(option)}
key={option.value}
>
{label(option)}
</div>
))}
</div>
)}
</div>

</InputWrapper>
)
}

export default InputSelect
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as InputSelect } from './InputSelect'
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ $error-line-height: 14px;
box-sizing: border-box;
border-radius: $space-xs;
margin-bottom: calc($error-line-height + $space-xs);
position: relative;

&.rating {
border-color: transparent;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import classNames from 'classnames'
import { Dispatch, FC, ReactNode, SetStateAction, useState } from 'react'
import { Dispatch, forwardRef, ForwardRefExoticComponent, ReactNode, SetStateAction, useState } from 'react'

import { IconSolid } from '../../../../svgs'

Expand All @@ -16,18 +16,19 @@ interface InputWrapperProps {
readonly hideInlineErrors?: boolean
readonly hint?: string
readonly label: string | JSX.Element
readonly tabIndex: number
readonly tabIndex?: number
readonly type: 'checkbox' | 'password' | 'rating' | 'text' | 'textarea'
}

const InputWrapper: FC<InputWrapperProps> = (props: InputWrapperProps) => {
const InputWrapper: ForwardRefExoticComponent<InputWrapperProps> = forwardRef<HTMLDivElement, InputWrapperProps>((props: InputWrapperProps, ref) => {

const [focusStyle, setFocusStyle]: [string | undefined, Dispatch<SetStateAction<string | undefined>>] = useState<string | undefined>()

const isShowError: () => boolean = () => !!props.error && !!props.dirty && !props.hideInlineErrors
const showError: boolean = isShowError()
const formFieldClasses: string = classNames(
styles.input,
'input-el',
styles[props.type],
props.disabled ? styles.disabled : undefined,
focusStyle,
Expand All @@ -43,8 +44,9 @@ const InputWrapper: FC<InputWrapperProps> = (props: InputWrapperProps) => {

return (
<div
className={classNames(styles['input-wrapper'], styles[props.type])}
tabIndex={props.type === 'rating' ? props.tabIndex : -1}
className={classNames(styles['input-wrapper'], 'input-wrapper', styles[props.type])}
tabIndex={props.type === 'rating' ? (props.tabIndex ?? -1) : -1}
ref={ref}
>

<div
Expand Down Expand Up @@ -85,6 +87,6 @@ const InputWrapper: FC<InputWrapperProps> = (props: InputWrapperProps) => {
)}
</div>
)
}
})

export default InputWrapper
2 changes: 1 addition & 1 deletion src-ts/lib/form/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ export {
} from './form-functions'
export * from './form-input.model'
export * from './form-group.model'
export { inputOptional, FormInputAutocompleteOption } from './form-groups'
export * from './form-groups/form-input'
export * from './validator-functions'
1 change: 1 addition & 0 deletions src-ts/lib/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './use-check-is-mobile.hook'
export * from './use-click-outside.hook'
export * from './use-on-hover-element.hook'
export * from './use-storage.hook'
export * from './use-window-size.hook'
69 changes: 69 additions & 0 deletions src-ts/lib/hooks/use-storage.hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Dispatch, SetStateAction, useCallback, useState } from 'react'

type StorageTypes = 'localStorage' | 'sessionStorage'

export function useStorage<T>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love this. Nice work!!

storageType: StorageTypes,
storageKey: string,
initialValue?: T
): [T, Dispatch<SetStateAction<T>>] {
const storage: Storage = window[storageType]

const readStoredValue: () => T = useCallback(() => {
try {
// Get from local storage by key
const item: string | null = storage.getItem(storageKey)
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue
} catch (error) {
// If error also return value
return initialValue
}
}, [storage, storageKey, initialValue])

// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue]: [T, Dispatch<SetStateAction<T>>] = useState(readStoredValue())

// Return a wrapped version of useState's setter function that
// persists the new value to local or session storage.
const setValue: Dispatch<SetStateAction<T>> = useCallback((value: T) => {
try {
// Allow value to be a function so we have same API as useState
setStoredValue((storedv: T) => {
const valueToStore: T = value instanceof Function ? value(storedv) : value

if (valueToStore === undefined) {
storage.removeItem(storageKey)
} else {
// Save to local storage
storage.setItem(storageKey, JSON.stringify(valueToStore))
}

return valueToStore
})
} catch (error) {
// A more advanced implementation would handle the error case
// tslint:disable-next-line:no-console
console.error(error)
}
}, [storage, storageKey]) as Dispatch<SetStateAction<T>>

return [storedValue, setValue]
}

export const useLocalStorage: <T, >(
key: string,
initialValue?: T
) => [T, Dispatch<SetStateAction<T>>] = <T>(
key: string,
initialValue?: T
) => useStorage<T>('localStorage', key, initialValue)

export const useSessionStorage: <T, >(
key: string,
initialValue?: T
) => [T, Dispatch<SetStateAction<T>>] = <T>(
key: string,
initialValue?: T
) => useStorage<T>('sessionStorage', key, initialValue)
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { LearnCertification } from './all-certifications-functions'

export interface AllCertificationsProviderData {
allCertifications: Array<LearnCertification>
certification?: LearnCertification
certifications: Array<LearnCertification>
certificationsCount: number
loading: boolean
ready: boolean
}
Loading