feature: multiselect menu

This commit is contained in:
Timothy Z. 2024-07-24 16:11:41 +03:00
parent 8fb85f9c67
commit 6a5dcb9fae
12 changed files with 278 additions and 7 deletions

View file

@ -0,0 +1,30 @@
// Copyright (C) 2017-2024 Smart code 203358507
.dropdown {
background: var(--modal-background-color);
display: none;
position: absolute;
width: 100%;
top: 100%;
left: 0;
z-index: 10;
box-shadow: var(--outer-glow);
border-radius: var(--border-radius);
overflow: hidden;
&.open {
display: block;
}
.back-button {
display: flex;
align-items: center;
gap: 0 0.5rem;
padding: 0.75rem;
color: var(--primary-foreground-color);
.back-button-icon {
width: 1.5rem;
}
}
}

View file

@ -0,0 +1,53 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React from 'react';
import Button from 'stremio/common/Button';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import Option from './Option';
import Icon from '@stremio/stremio-icons/react';
import styles from './Dropdown.less';
type Props = {
options: MultiselectMenuOption[];
selectedOption?: MultiselectMenuOption | null;
menuOpen: boolean | (() => void);
level: number;
setLevel: (level: number) => void;
onSelect: (option: MultiselectMenuOption) => void;
};
const Dropdown = ({ level, setLevel, options, onSelect, selectedOption, menuOpen }: Props) => {
const { t } = useTranslation();
const onBackButtonClick = () => {
setLevel(level - 1);
};
return (
<div className={classNames(styles['dropdown'], { [styles['open']]: menuOpen })} role='listbox'>
{
level > 0 ?
<Button className={styles['back-button']} onClick={onBackButtonClick}>
<Icon name={'chevron-left'} className={styles['back-button-icon']} />
{t('BACK')}
</Button>
: null
}
{
options
.filter((option: MultiselectMenuOption) => !option.hidden)
.map((option: MultiselectMenuOption) => (
<Option
option={option}
onSelect={onSelect}
selectedOption={selectedOption}
/>
))
}
</div>
);
};
export default Dropdown;

View file

@ -0,0 +1,30 @@
// Copyright (C) 2017-2024 Smart code 203358507
.option {
font-size: var(--font-size-normal);
color: var(--primary-foreground-color);
align-items: center;
display: flex;
flex-direction: row;
padding: 1rem;
.label {
flex: 1;
color: var(--primary-foreground-color);
}
.icon {
flex: none;
// display: none;
width: 0.5rem;
height: 0.5rem;
border-radius: 100%;
margin-left: 1rem;
background-color: var(--secondary-accent-color);
opacity: 1;
}
&:hover {
background-color: rgba(255, 255, 255, 0.15);
}
}

View file

@ -0,0 +1,42 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React, { useMemo } from 'react';
import classNames from 'classnames';
import Button from 'stremio/common/Button';
import styles from './Option.less';
import Icon from '@stremio/stremio-icons/react';
type Props = {
option: MultiselectMenuOption;
selectedOption?: MultiselectMenuOption | null;
onSelect: (option: MultiselectMenuOption) => void;
};
const Option = ({ option, selectedOption, onSelect }: Props) => {
// consider using option.id === selectedOption?.id instead
const selected = useMemo(() => option?.value === selectedOption?.value, [option, selectedOption]);
return (
<Button
className={classNames(styles['option'], { [styles['selected']]: selected })}
key={option.id}
onClick={(event: any) => onSelect(event)}
aria-selected={selected}
>
<div className={styles['label']}>{ option.label }</div>
{
selected && !option.level ?
<div className={styles['icon']} />
: null
}
{
option.level ?
<Icon name={'chevron-right'} className={styles['option-chevron']} />
: null
}
</Button>
);
};
export default Option;

View file

@ -0,0 +1,5 @@
// Copyright (C) 2017-2024 Smart code 203358507
import Option from './Option';
export default Option;

View file

@ -0,0 +1,5 @@
// Copyright (C) 2017-2024 Smart code 203358507
import Dropdown from './Dropdown';
export default Dropdown;

View file

@ -0,0 +1,34 @@
@border-radius: 2.75rem;
.multiselect-menu {
background-color: var(--overlay-color);
position: relative;
min-width: 150px;
overflow: visible;
border-radius: @border-radius;
&.disabled {
pointer-events: none;
opacity: 0.3;
}
.multiselect-button {
color: var(--primary-foreground-color);
background-color: rgba(255, 255, 255, 0.05);
padding: 0.75rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
gap: 0 0.5rem;
border-radius: @border-radius;
.icon {
width: 1.5rem;
color: var(--primary-foreground-color);
&.open {
transform: rotate(180deg);
}
}
}
}

View file

@ -0,0 +1,56 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React from 'react';
import Button from 'stremio/common/Button';
import useBinaryState from 'stremio/common/useBinaryState';
import Dropdown from './Dropdown';
import classNames from 'classnames';
import Icon from '@stremio/stremio-icons/react';
import styles from './MultiselectMenu.less';
type Props = {
className?: string,
title?: string;
options: MultiselectMenuOption[];
selectedOption?: MultiselectMenuOption;
onSelect: () => void;
};
const MultiselectMenu = ({ className, title, options, selectedOption, onSelect }: Props) => {
const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false);
const multiselectMenuRef = React.useRef<HTMLDivElement>(null);
const [level, setLevel] = React.useState<number>(0);
const onOptionSelect = (option: MultiselectMenuOption) => {
option.level ? setLevel(level + 1) : onSelect(), closeMenu();
};
return (
<div className={classNames(styles['multiselect-menu'], className)} ref={multiselectMenuRef}>
<Button
className={classNames(styles['multiselect-button'], { [styles['open']]: menuOpen })}
onClick={toggleMenu}
tabIndex={0}
aria-haspopup='listbox'
aria-expanded={menuOpen}
>
{title}
<Icon name={'chevron-down'} className={classNames(styles['icon'], { [styles['open']]: menuOpen })} />
</Button>
{
menuOpen ?
<Dropdown
level={level}
setLevel={setLevel}
options={options}
onSelect={onOptionSelect}
menuOpen={menuOpen}
selectedOption={selectedOption}
/>
: null
}
</div>
);
};
export default MultiselectMenu;

View file

@ -0,0 +1,5 @@
// Copyright (C) 2017-2024 Smart code 203358507
import MultiselectMenu from './MultiselectMenu';
export default MultiselectMenu;

9
src/common/MultiselectMenu/types.d.ts vendored Normal file
View file

@ -0,0 +1,9 @@
type MultiselectMenuOption = {
id?: number;
label: string;
value: string;
destination?: string;
default?: boolean;
hidden?: boolean;
level?: MultiselectMenuOption[];
};

View file

@ -15,6 +15,7 @@ const MetaPreview = require('./MetaPreview');
const MetaRow = require('./MetaRow');
const ModalDialog = require('./ModalDialog');
const Multiselect = require('./Multiselect');
const { default: MultiselectMenu } = require('./MultiselectMenu');
const { HorizontalNavBar, VerticalNavBar } = require('./NavBar');
const PaginationInput = require('./PaginationInput');
const PlayIconCircleCentered = require('./PlayIconCircleCentered');
@ -63,6 +64,7 @@ module.exports = {
MetaRow,
ModalDialog,
Multiselect,
MultiselectMenu,
HorizontalNavBar,
VerticalNavBar,
PaginationInput,

View file

@ -5,9 +5,10 @@ const PropTypes = require('prop-types');
const classnames = require('classnames');
const { t } = require('i18next');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { Button, Multiselect } = require('stremio/common');
const { Button } = require('stremio/common');
const SeasonsBarPlaceholder = require('./SeasonsBarPlaceholder');
const styles = require('./styles');
const { MultiselectMenu } = require('stremio/common');
const SeasonsBar = ({ className, seasons, season, onSelect }) => {
const options = React.useMemo(() => {
@ -16,8 +17,8 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => {
label: season > 0 ? `${t('SEASON')} ${season}` : t('SPECIAL')
}));
}, [seasons]);
const selected = React.useMemo(() => {
return [String(season)];
const selectedSeason = React.useMemo(() => {
return { label: String(season), value: String(season) };
}, [season]);
const prevNextButtonOnClick = React.useCallback((event) => {
if (typeof onSelect === 'function') {
@ -61,12 +62,11 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => {
<Icon className={styles['icon']} name={'chevron-back'} />
<div className={styles['label']}>Prev</div>
</Button>
<Multiselect
<MultiselectMenu
className={styles['seasons-popup-label-container']}
title={season > 0 ? `${t('SEASON')} ${season}` : t('SPECIAL')}
direction={'bottom-left'}
options={options}
selected={selected}
title={season > 0 ? `${t('SEASON')} ${season}` : t('SPECIAL')}
selectedOption={selectedSeason}
onSelect={seasonOnSelect}
/>
<Button className={classnames(styles['next-season-button'], { 'disabled': nextDisabled })} title={'Next season'} data-action={'next'} onClick={prevNextButtonOnClick}>