Skip to content

Commit

Permalink
enhancement dev-9440 loading states on dropdowns
Browse files Browse the repository at this point in the history
  • Loading branch information
joshlacey committed Nov 20, 2024
1 parent 56147ba commit b085961
Show file tree
Hide file tree
Showing 8 changed files with 70 additions and 20 deletions.
15 changes: 10 additions & 5 deletions packages/core/components/Loader/Loader.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import React, { useEffect, useRef } from 'react'
import './loader.styles.css'

// these coorespond to bootstrap classes
// https://getbootstrap.com/docs/4.2/components/spinners/
type SpinnerType = 'text-primary' | 'text-secondary'

type LoaderProps = {
fullScreen?: boolean
spinnerType?: SpinnerType
}

const Spinner = () => (
<div className='spinner-border text-primary' role='status'>
const Spinner = ({ spinnerType }: { spinnerType: SpinnerType }) => (
<div className={`spinner-border ${spinnerType}`} role='status'>
<span className='sr-only'>Loading...</span>
</div>
)

const Loader: React.FC<LoaderProps> = ({ fullScreen = false }) => {
const Loader: React.FC<LoaderProps> = ({ fullScreen = false, spinnerType }) => {
const backgroundRef = useRef(null)

useEffect(() => {
Expand All @@ -23,10 +28,10 @@ const Loader: React.FC<LoaderProps> = ({ fullScreen = false }) => {

return fullScreen ? (
<div ref={backgroundRef} className='cove-loader fullscreen'>
<Spinner />
<Spinner spinnerType={spinnerType || 'text-primary'} />
</div>
) : (
<Spinner />
<Spinner spinnerType={spinnerType || 'text-primary'} />
)
}

Expand Down
12 changes: 9 additions & 3 deletions packages/core/components/MultiSelect/MultiSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Icon from '../ui/Icon'

import './multiselect.styles.css'
import { UpdateFieldFunc } from '../../types/UpdateFieldFunc'
import Loader from '../Loader'

interface Option {
value: string | number
Expand All @@ -20,6 +21,7 @@ interface MultiSelectProps {
selected?: (string | number)[]
limit?: number
tooltip?: React.ReactNode
loading?: boolean
}

const MultiSelect: React.FC<MultiSelectProps> = ({
Expand All @@ -31,7 +33,8 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
updateField,
selected = [],
limit,
tooltip
tooltip,
loading
}) => {
const preselectedItems = options.filter(opt => selected.includes(opt.value)).slice(0, limit)
const [selectedItems, setSelectedItems] = useState<Option[]>(preselectedItems)
Expand Down Expand Up @@ -90,11 +93,12 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
<div
id={multiID}
onClick={() => {
if (!selectedItems.length) {
if (!selectedItems.length && !loading) {
setExpanded(true)
}
}}
className='selected'
aria-disabled={loading}
>
{selectedItems.length ? (
selectedItems.map(item => (
Expand All @@ -115,7 +119,7 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
</div>
))
) : (
<span className='pl-1 pt-1'>- Select -</span>
<span className='pl-1 pt-1'>{loading ? 'Loading...' : '- Select -'}</span>
)}
<button
aria-label={expanded ? 'Collapse' : 'Expand'}
Expand All @@ -129,6 +133,7 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
<Icon display={'caretDown'} style={{ cursor: 'pointer' }} />
</button>
</div>
{loading && <Loader spinnerType={'text-secondary'} />}
{!!limit && (
<Tooltip style={{ textTransform: 'none' }}>
<Tooltip.Target>
Expand All @@ -140,6 +145,7 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
</Tooltip>
)}
</div>

<ul className={'dropdown' + (expanded ? '' : ' d-none')}>
{options
.filter(option => !selectedItems.find(item => item.value === option.value))
Expand Down
6 changes: 6 additions & 0 deletions packages/core/components/MultiSelect/multiselect.styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
display: inline-flex;
align-items: center;
.selected {
&[aria-disabled='true'] {
background: var(--lightestGray);
}
border: 1px solid var(--lightGray);
padding: 7px;
min-width: 200px;
Expand Down Expand Up @@ -41,6 +44,9 @@
margin-bottom: 0;
}
}
.spinner-border {
right: 20% !important;
}
}
.dropdown {
background: white;
Expand Down
19 changes: 13 additions & 6 deletions packages/core/components/NestedDropdown/NestedDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState, useEffect, useRef, useMemo, useId } from 'react'
import './nesteddropdown.styles.css'
import Icon from '@cdc/core/components/ui/Icon'
import { filterSearchTerm, NestedOptions, ValueTextPair } from './nestedDropdownHelpers'
import Loader from '../Loader'

const Options: React.FC<{
subOptions: ValueTextPair[]
Expand Down Expand Up @@ -106,12 +107,10 @@ type NestedDropdownProps = {
activeGroup: string
activeSubGroup?: string
filterIndex: number
isEditor?: boolean
isUrlFilter?: boolean
listLabel: string
handleSelectedItems: ([group, subgroup]: [string, string]) => void
options: NestedOptions
subGroupingActive?: string
loading?: boolean
}

const NestedDropdown: React.FC<NestedDropdownProps> = ({
Expand All @@ -120,7 +119,8 @@ const NestedDropdown: React.FC<NestedDropdownProps> = ({
activeSubGroup,
filterIndex,
listLabel,
handleSelectedItems
handleSelectedItems,
loading
}) => {
const dropdownId = useId()
const groupFilterActive = activeGroup
Expand Down Expand Up @@ -255,7 +255,12 @@ const NestedDropdown: React.FC<NestedDropdownProps> = ({
className={`nested-dropdown nested-dropdown-${filterIndex} ${isListOpened ? 'open-filter' : ''}`}
onKeyUp={handleKeyUp}
>
<div className='nested-dropdown-input-container' aria-label='searchInput' role='textbox'>
<div
className={`nested-dropdown-input-container${loading ? ' disabled' : ''}`}
aria-label='searchInput'
aria-disabled={loading}
role='textbox'
>
<input
id={`nested-dropdown-${filterIndex}`}
className='search-input'
Expand All @@ -266,7 +271,8 @@ const NestedDropdown: React.FC<NestedDropdownProps> = ({
tabIndex={0}
value={inputValue}
onChange={handleSearchTermChange}
placeholder={'- Select -'}
placeholder={loading ? 'Loading...' : '- Select -'}
disabled={loading}
onClick={() => {
if (inputHasFocus) setIsListOpened(!isListOpened)
}}
Expand All @@ -277,6 +283,7 @@ const NestedDropdown: React.FC<NestedDropdownProps> = ({
<Icon display='caretDown' />
</span>
</div>
{loading && <Loader spinnerType={'text-secondary'} />}
<ul
role='tree'
key={listLabel}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
display: inline-block;
width: 100%;
padding: 0;
&::placeholder {
color: var(--darkGray);
}
}

.main-nested-dropdown-container,
Expand Down Expand Up @@ -56,6 +59,10 @@
right: 1px;
float: right;
}

&.disabled {
background-color: var(--lightestGray);
}
}

& .main-nested-dropdown-container {
Expand Down
1 change: 0 additions & 1 deletion packages/core/styles/_reset.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
}
}
.cdc-open-viz-module {
span,
applet,
object,
iframe,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { FILTER_STYLE } from '../../types/FilterStyles'
import { NestedOptions, ValueTextPair } from '@cdc/core/components/NestedDropdown/nestedDropdownHelpers'
import NestedDropdown from '@cdc/core/components/NestedDropdown'
import { MouseEventHandler } from 'react'
import Loader from '@cdc/core/components/Loader'

type DashboardFilterProps = {
show: number[]
Expand Down Expand Up @@ -64,6 +65,7 @@ const DashboardFilters: React.FC<DashboardFilterProps> = ({
}

const _key = filter.apiFilter?.apiEndpoint
const loading = !apiFilterDropdowns[_key]

const multiValues: { value; label }[] = []

Expand Down Expand Up @@ -92,50 +94,55 @@ const DashboardFilters: React.FC<DashboardFilterProps> = ({
})
}

const formGroupClass = `form-group mr-3 mb-1${loading ? ' loading-filter' : ''}`
return filter.filterStyle === FILTER_STYLE.multiSelect ? (
<div className='form-group mr-3 mb-1' key={`${filter.key}-filtersection-${filterIndex}`}>
<div className={formGroupClass} key={`${filter.key}-filtersection-${filterIndex}`}>
<MultiSelect
label={filter.key}
options={multiValues}
fieldName={filterIndex}
updateField={updateField}
selected={filter.active as string[]}
limit={filter.selectLimit || 5}
loading={loading}
/>
</div>
) : filter.filterStyle === FILTER_STYLE.nestedDropdown ? (
<div className='form-group mr-3 mb-1' key={`${filter.key}-filtersection-${filterIndex}`}>
<div className={formGroupClass} key={`${filter.key}-filtersection-${filterIndex}`}>
<NestedDropdown
activeGroup={filter.active as string}
activeSubGroup={filter.subGrouping?.active}
filterIndex={filterIndex}
options={getNestedDropdownOptions(apiFilterDropdowns[_key])}
listLabel={filter.key}
handleSelectedItems={value => updateField(null, null, filterIndex, value)}
loading={loading}
/>
</div>
) : (
<div className='form-group mr-3 mb-1' key={`${filter.key}-filtersection-${filterIndex}`}>
<div className={formGroupClass} key={`${filter.key}-filtersection-${filterIndex}`}>
<label className='text-capitalize font-weight-bold' htmlFor={`filter-${filterIndex}`}>
{filter.key}
</label>
<select
id={`filter-${filterIndex}`}
className='cove-form-select'
data-index='0'
value={filter.queuedActive || filter.active}
value={loading ? 'Loading...' : filter.queuedActive || filter.active}
onChange={val => {
handleOnChange(filterIndex, val.target.value)
}}
disabled={values.length === 1 && !nullVal(filter)}
disabled={loading ? true : values.length === 1 && !nullVal(filter)}
>
{loading && <option value='Loading...'>Loading...</option>}
{nullVal(filter) && (
<option key={`select`} value=''>
{filter.resetLabel || '- Select -'}
</option>
)}
{values}
</select>
{loading && <Loader spinnerType={'text-secondary'} />}
</div>
)
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,17 @@
height: calc(1.5em + 0.75rem + 2px);
align-self: flex-end;
}
.form-control:disabled {
background-color: var(--lightestGray);
}
.loading-filter {
position: relative;
.spinner-border {
position: absolute;
top: 55%;
right: 10%;
width: 1.5rem;
height: 1.5rem;
}
}
}

0 comments on commit b085961

Please sign in to comment.