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
Binary file modified e2e/screenshots.spec.ts-snapshots/data-page.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 39 additions & 0 deletions src/app/controls.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,45 @@
flex-wrap: wrap;
}

.selector .selectorButtons {
position: relative;
display: flex;
align-items: center;
flex-direction: row;
}

.selector.buttonList .selectorOptions {
display: flex;
flex-wrap: wrap;
gap: 0.25em;
}

.selector.filterList {
display: flex;
flex-direction: column;
width: 100%;
align-items: flex-start;
align-self: stretch;
}

.selector.filterList .selectorOptions {
display: flex;
flex-direction: column;
align-items: flex-start;
}

.selector.filterList .selectorButtons {
position: unset;
width: 100%;
align-items: flex-start;
flex-direction: column;
}

.selector.filterList .filterListMoreButton {
font-size: 0.9em;
padding: 0.25em 3.5em;
}

.selector.buttonList {
flex-direction: column;
align-items: start;
Expand Down
68 changes: 58 additions & 10 deletions src/features/params/ui/Selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type Props<T> = {
selectorStyle?: React.CSSProperties;
};

const FILTER_LIST_INITIAL_COUNT = 5;

function Selector<T extends React.Key>({
display,
getOptionDescription,
Expand All @@ -41,14 +43,25 @@ function Selector<T extends React.Key>({
}: Props<T>) {
const [expanded, setExpanded] = useState(false);
const optionsRef = useClickOutside(() => {
setTimeout(() => setExpanded(false), 100);
// Only auto-collapse for dropdown modes; FilterList is a persistent filter UI
if (display !== SelectorDisplay.FilterList) {
setTimeout(() => setExpanded(false), 100);
}
});

// For FilterList: collapse to first N items unless expanded
const isFilterList = display === SelectorDisplay.FilterList;
const filterListCollapsed =
isFilterList && !expanded && options.length > FILTER_LIST_INITIAL_COUNT;
const visibleOptions = filterListCollapsed
? options.slice(0, FILTER_LIST_INITIAL_COUNT)
: options;

return (
<SelectorContainer manualStyle={selectorStyle} manualDisplay={display}>
{selectorLabel && <SelectorLabel label={selectorLabel} description={selectorDescription} />}

<div style={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
<div className="selectorButtons">
{/* The dropdown menu or the button list */}
<OptionsContainer
isExpanded={expanded}
Expand All @@ -59,15 +72,22 @@ function Selector<T extends React.Key>({
getOptionDescription={getOptionDescription}
getOptionLabel={getOptionLabel}
onClick={(option) => {
setExpanded(false);
if (!isFilterList) setExpanded(false);
onChange(option);
}}
optionStyle={optionStyle}
options={options}
options={visibleOptions}
selected={selected}
/>
</OptionsContainer>

{/* "More / Less" toggle for FilterList when there are enough options */}
<FilterListMoreButton
totalCount={options.length}
isExpanded={expanded}
toggle={() => setExpanded((prev) => !prev)}
/>

{/* Standalone option to open/close the dropdown menu */}
<DropdownButton<T>
getOptionDescription={getOptionDescription}
Expand Down Expand Up @@ -120,13 +140,19 @@ const OptionsContainer: React.FC<React.PropsWithChildren<OptionsContainerProps>>
case SelectorDisplay.ButtonList:
return (
<div
className="selectorOptions"
ref={containerRef}
style={{ marginLeft: hasSelectorLabel ? '1em' : 'none' }}
>
{children}
</div>
);
case SelectorDisplay.FilterList:
return (
<div
className="selectorOptions"
ref={containerRef}
style={{
display: 'flex',
flexWrap: 'wrap',
gap: '0.25em',
marginLeft: hasSelectorLabel ? '1em' : 'none',
}}
style={{ marginLeft: hasSelectorLabel ? '1em' : 'none' }}
>
{children}
</div>
Expand Down Expand Up @@ -174,6 +200,28 @@ function Options<T extends React.Key>({
));
}

type FilterListMoreButtonProps = {
totalCount: number;
isExpanded: boolean;
toggle: () => void;
};

const FilterListMoreButton: React.FC<FilterListMoreButtonProps> = ({
totalCount,
isExpanded,
toggle,
}) => {
const { display } = useSelectorDisplay();
if (display !== SelectorDisplay.FilterList || totalCount <= FILTER_LIST_INITIAL_COUNT)
return null;

return (
<button className="filterListMoreButton" onClick={toggle}>
{isExpanded ? 'Collapse' : 'Expand All'}
</button>
);
};

type DropdownButtonProps<T> = {
getOptionDescription?: (value: T) => React.ReactNode;
getOptionLabel?: (value: T) => React.ReactNode;
Expand Down
1 change: 1 addition & 0 deletions src/features/params/ui/SelectorDisplayContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export enum SelectorDisplay {
InlineDropdown = 'inlineDropdown', // Used to be inline with text
ButtonGroup = 'buttonGroup', // Unsure if we want to keep this
ButtonList = 'buttonList',
FilterList = 'filterList',
}

export const SelectorDisplayContext = React.createContext<{
Expand Down
8 changes: 6 additions & 2 deletions src/features/params/ui/SelectorLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@ function getStyle(display: SelectorDisplay): React.CSSProperties {
const style: React.CSSProperties = {
display: 'flex',
gap: '0.25em',
lineHeight: '1em',
alignItems: 'center',
fontWeight: 'bold',
fontWeight: '800', // adjusted font weight for easier visibility
padding: '0.5em',
margin: 'auto 0', // Vertically center
whiteSpace: 'nowrap',
Expand All @@ -50,6 +49,11 @@ function getStyle(display: SelectorDisplay): React.CSSProperties {
case SelectorDisplay.InlineDropdown:
style.padding = '0 0.5em';
break;
case SelectorDisplay.FilterList:
style.padding = '0 0 0.5em 0.5em';
style.lineHeight = '2.25em'; // more spacing for visibility
style.marginBottom = '-0.5em'; // adjusted to have selector buttons closer to their label
break;
case SelectorDisplay.Dropdown:
// nothing special
break;
Expand Down
7 changes: 7 additions & 0 deletions src/features/params/ui/SelectorOption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,13 @@ export function getOptionStyle(
style.borderRadius = '1em';
if (!isSelected) style.border = '0.125em solid var(--color-button-secondary)';
break;
case SelectorDisplay.FilterList:
style.border = 'none';
style.borderRadius = '0.5em';
style.padding = '0.2em 0.4em';
style.lineHeight = '1.5em';
style.margin = '0.1em';
break;
case SelectorDisplay.InlineDropdown:
// The standalone option should match the regular page text
if (position === PositionInGroup.Standalone) {
Expand Down
10 changes: 5 additions & 5 deletions src/features/transforms/filtering/selectors/FilterSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,17 @@ const FilterSelector: React.FC<Props> = ({ field }) => {
case Field.WritingSystem:
return <WritingSystemFilterSelector display={SelectorDisplay.ButtonList} />;
case Field.Modality:
return <LanguageModalitySelector />;
return <LanguageModalitySelector display={SelectorDisplay.FilterList} />;
case Field.LanguageScope:
return <LanguageScopeSelector />;
return <LanguageScopeSelector display={SelectorDisplay.FilterList} />;
case Field.TerritoryScope:
return <TerritoryScopeSelector />;
return <TerritoryScopeSelector display={SelectorDisplay.FilterList} />;
case Field.ISOStatus:
return <LanguageISOStatusSelector />;
return <LanguageISOStatusSelector display={SelectorDisplay.FilterList} />;
case Field.Name:
return <SearchBar />; // Technically correct but not recommended usage
case Field.SourceForLanguage:
return <LanguageSourceSelector display={SelectorDisplay.ButtonList} />;
return <LanguageSourceSelector display={SelectorDisplay.FilterList} />;
case Field.Population:
return <PopulationFilterSelector />;
default:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import React from 'react';

import Selector from '@features/params/ui/Selector';
import { SelectorDisplay } from '@features/params/ui/SelectorDisplayContext';
import usePageParams from '@features/params/usePageParams';

import { LanguageModality } from '@entities/language/LanguageModality';
import { getModalityLabel } from '@entities/language/LanguageModalityDisplay';

const LanguageModalitySelector: React.FC = () => {
type Props = { display?: SelectorDisplay };

const LanguageModalitySelector: React.FC<Props> = ({ display }) => {
const { modalityFilter, updatePageParams } = usePageParams();

const selectorDescription =
Expand All @@ -29,6 +32,7 @@ const LanguageModalitySelector: React.FC = () => {
}
selected={modalityFilter}
getOptionLabel={getModalityLabel}
display={display}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import React from 'react';

import Selector from '@features/params/ui/Selector';
import { SelectorDisplay } from '@features/params/ui/SelectorDisplayContext';
import usePageParams from '@features/params/usePageParams';

import { LanguageScope } from '@entities/language/LanguageTypes';

import { getLanguageScopeDescription, getLanguageScopeLabel } from '@strings/LanguageScopeStrings';

const LanguageScopeSelector: React.FC = () => {
type Props = { display?: SelectorDisplay };

const LanguageScopeSelector: React.FC<Props> = ({ display }) => {
const { languageScopes, updatePageParams } = usePageParams();

const selectorDescription =
Expand All @@ -27,6 +30,7 @@ const LanguageScopeSelector: React.FC = () => {
selected={languageScopes}
getOptionLabel={getLanguageScopeLabel}
getOptionDescription={getLanguageScopeDescription}
display={display}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import React from 'react';

import Selector from '@features/params/ui/Selector';
import { SelectorDisplay } from '@features/params/ui/SelectorDisplayContext';
import usePageParams from '@features/params/usePageParams';

import { TerritoryScope } from '@entities/territory/TerritoryTypes';

import { getTerritoryScopeLabel } from '@strings/TerritoryScopeStrings';

const TerritoryScopeSelector: React.FC = () => {
type Props = { display?: SelectorDisplay };

const TerritoryScopeSelector: React.FC<Props> = ({ display }) => {
const { territoryScopes, updatePageParams } = usePageParams();

const selectorDescription =
Expand All @@ -28,6 +31,7 @@ const TerritoryScopeSelector: React.FC = () => {
}
selected={territoryScopes}
getOptionLabel={getTerritoryScopeLabel}
display={display}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';

import Selector from '@features/params/ui/Selector';
import { SelectorDisplay } from '@features/params/ui/SelectorDisplayContext';
import usePageParams from '@features/params/usePageParams';

import {
Expand All @@ -18,7 +19,9 @@ import {

const ETH_DISABLED = true;

export const LanguageISOStatusSelector: React.FC = () => {
type Props = { display?: SelectorDisplay };

export const LanguageISOStatusSelector: React.FC<Props> = ({ display }) => {
const { isoStatus, updatePageParams } = usePageParams();

return (
Expand All @@ -34,6 +37,7 @@ export const LanguageISOStatusSelector: React.FC = () => {
}
selected={isoStatus}
getOptionLabel={getLanguageISOStatusLabel}
display={display}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import {

vi.mock('@features/params/usePageParams', () => ({ default: vi.fn() }));
vi.mock('@features/params/ui/SelectorDisplayContext', () => ({
useSelectorDisplay: vi.fn().mockReturnValue({ display: 'buttonList' }),
SelectorDisplay: { ButtonList: 'buttonList', Dropdown: 'dropdown' },
useSelectorDisplay: vi.fn().mockReturnValue({ display: 'filterList' }),
SelectorDisplay: { ButtonList: 'buttonList', Dropdown: 'dropdown', FilterList: 'filterList' },
SelectorDisplayProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));
vi.mock('@features/layers/hovercard/useHoverCard', () => ({
Expand Down Expand Up @@ -59,10 +59,17 @@ describe('VitalitySelector', () => {
});

describe('LanguageISOStatusSelector', () => {
it('displays all ISO vitality options', () => {
it('displays all ISO vitality options', async () => {
const user = userEvent.setup();
render(<LanguageISOStatusSelector />);

const expected = Object.values(LanguageISOStatus).filter((v) => typeof v === 'number');

// If there are more than 4 options expand first so all are visible
if (expected.length > 4) {
await user.click(screen.getByText('Expand All'));
}

expected.forEach((status) => {
const label = getLanguageISOStatusLabel(status);
expect(screen.getByText(label)).toBeInTheDocument();
Expand Down
Loading