diff --git a/UNRELEASED.md b/UNRELEASED.md index fd34c8517f3..b238deedf8a 100644 --- a/UNRELEASED.md +++ b/UNRELEASED.md @@ -10,6 +10,8 @@ Use [the changelog guidelines](https://git.io/polaris-changelog-guidelines) to f ### New components +- `Filters`: Use to filter the items of a list or table ([#1718](https://github.com/Shopify/polaris-react/pull/1718)) + ### Enhancements ### Bug fixes diff --git a/src/components/Filters/Filters.scss b/src/components/Filters/Filters.scss new file mode 100644 index 00000000000..6322b2dc016 --- /dev/null +++ b/src/components/Filters/Filters.scss @@ -0,0 +1,204 @@ +$list-filters-header-height: rem(56px); +$list-filters-footer-height: rem(70px); + +.Filters { + position: relative; +} + +.FiltersContainer { + position: relative; + height: 100%; + width: 100%; + display: flex; + flex-direction: column; +} + +.FiltersContainerHeader { + top: 0; + width: 100%; + padding: rem(16px) rem(20px); + border-bottom: rem(1px) solid color('sky'); + height: rem($list-filters-header-height); + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: space-between; +} + +.FiltersDesktopContainerContent { + width: 100%; + height: calc( + 100% - #{rem($list-filters-footer-height)} - #{rem( + $list-filters-header-height + )} + ); +} + +.FiltersMobileContainerContent { + width: 100%; + height: calc(100% - #{rem($list-filters-header-height)}); +} + +.FiltersContainerFooter { + position: absolute; + bottom: 0; + width: 100%; + padding: rem(14px) rem(16px); + border-top: rem(1px) solid color('sky'); + height: rem($list-filters-footer-height); + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: space-between; +} + +.FiltersMobileContainerFooter { + width: 100%; + padding: rem(14px) rem(16px); + height: rem($list-filters-footer-height); + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: space-between; +} + +.EmptyFooterState { + border-top: border(); + padding-top: rem(14px); + width: 100%; + display: flex; + justify-content: center; +} + +.FilterTriggerContainer { + position: relative; +} + +.FilterTrigger { + width: 100%; + padding: rem(14px) spacing(loose); + border: none; + background: none; + color: color(ink); + + &:hover { + cursor: pointer; + @include state(hover); + } + + &:focus { + outline: none; + @include state(focused); + } +} + +.FilterTriggerTitle { + font-size: font-size(body); + @include text-emphasis-strong; +} + +.AppliedFilterBadgeContainer { + padding-top: spacing(extra-tight); + display: flex; + + .open & { + display: none; + } +} + +.FilterTriggerLabelContainer { + display: flex; + align-items: center; + justify-content: space-between; +} + +.open { + &::before, + &::after { + content: ''; + position: relative; + left: rem(16px); + width: calc(100% - #{rem(32px)}); + height: rem(1px); + background-color: color('sky'); + display: block; + } + + &::before { + top: 0; + } + + &::after { + bottom: 0; + } +} + +.open.first { + &::after { + content: ''; + bottom: 0; + position: relative; + left: rem(16px); + width: calc(100% - #{rem(32px)}); + height: rem(1px); + background-color: color('sky'); + display: block; + } + + &::before { + display: none; + } +} + +.open + .open { + &::before { + display: none; + } +} + +.open.last { + &::before { + content: ''; + top: 0; + position: relative; + left: rem(16px); + width: calc(100% - #{rem(32px)}); + height: rem(1px); + background-color: color('sky'); + display: block; + } + + &::after { + display: none; + } +} + +.FilterNodeContainer { + padding: rem(8px) rem(20px) rem(20px) rem(20px); +} + +.SearchIcon { + fill: currentColor; +} + +.Backdrop { + position: fixed; + z-index: z-index(backdrop, $fixed-element-stacking-order); + top: 0; + right: 0; + bottom: 0; + left: 0; + display: block; + opacity: 0; +} + +.TagsContainer { + display: flex; + padding-top: spacing(tight); + flex-wrap: wrap; + + > * { + margin-right: spacing(tight); + margin-bottom: spacing(tight); + } +} diff --git a/src/components/Filters/Filters.tsx b/src/components/Filters/Filters.tsx new file mode 100644 index 00000000000..371c89470e4 --- /dev/null +++ b/src/components/Filters/Filters.tsx @@ -0,0 +1,502 @@ +import React from 'react'; +import debounce from 'lodash/debounce'; +import compose from '@shopify/react-compose'; +import {classNames} from '@shopify/css-utilities'; +import {focusFirstFocusableNode} from '@shopify/javascript-utilities/focus'; +import { + SearchMinor, + ChevronUpMinor, + ChevronDownMinor, + CancelSmallMinor, +} from '@shopify/polaris-icons'; +import {withAppProvider, WithAppProviderProps} from '../AppProvider'; +import {Consumer, ResourceListContext} from '../ResourceList'; +import Button from '../Button'; +import DisplayText from '../DisplayText'; +import Collapsible from '../Collapsible'; +import Scrollable from '../Scrollable'; +import ScrollLock from '../ScrollLock'; +import Icon from '../Icon'; +import TextField from '../TextField'; +import Tag from '../Tag'; +import EventListener from '../EventListener'; +import TextStyle from '../TextStyle'; +import Badge from '../Badge'; +import Focus from '../Focus'; +import Sheet from '../Sheet'; +import Stack from '../Stack'; +import withContext from '../WithContext'; +import {Key, WithContextTypes} from '../../types'; + +import {navigationBarCollapsed} from '../../utilities/breakpoints'; +import KeypressListener from '../KeypressListener'; +import {ConnectedFilterControl, PopoverableAction} from './components'; + +import styles from './Filters.scss'; + +export interface AppliedFilter { + /** A unique key used to identify the applied filter */ + key: string; + /** A label for the applied filter */ + label: string; + /** Callback when the remove button is pressed */ + onRemove(key: string): void; +} + +export interface Filter { + /** A unique key used to identify the filter */ + key: string; + /** The label for the filter */ + label: string; + /** The markup for the given filter */ + filter: React.ReactNode; + /** Whether or not the filter should have a shortcut popover displayed */ + shortcut?: boolean; +} + +export interface Props { + /** Currently entered text in the query field */ + queryValue?: string; + /** Placeholder text for the query field */ + queryPlaceholder?: string; + /** Whether the query field is focused */ + focused?: boolean; + /** Available filters added to the sheet. Shortcut filters are exposed outside of the sheet. */ + filters: Filter[]; + /** Applied filters which are rendered as tags. The remove callback is called with the respective key */ + appliedFilters?: AppliedFilter[]; + /** Callback when the query field is changed */ + onQueryChange(queryValue: string): void; + /** Callback when the clear button is triggered */ + onQueryClear(): void; + /** Callback when the reset all button is pressed */ + onClearAll(): void; + /** Callback when the query field is blurred */ + onQueryBlur?(): void; + /** The content to display inline with the controls */ + children?: React.ReactNode; +} + +type ComposedProps = Props & + WithAppProviderProps & + WithContextTypes; + +interface State { + open: boolean; + mobile: boolean; + readyForFocus: boolean; + [key: string]: boolean; +} + +enum Suffix { + Filter = 'Filter', + Shortcut = 'Shortcut', +} + +class Filters extends React.Component { + state: State = { + open: false, + mobile: false, + readyForFocus: false, + }; + + private moreFiltersButtonContainer = React.createRef(); + + private get hasAppliedFilters(): boolean { + const {appliedFilters, queryValue} = this.props; + const filtersApplied = Boolean(appliedFilters && appliedFilters.length > 0); + const queryApplied = Boolean(queryValue && queryValue !== ''); + + return filtersApplied || queryApplied; + } + + private handleResize = debounce( + () => { + const {mobile} = this.state; + if (mobile !== isMobile()) { + this.setState({mobile: !mobile}); + } + }, + 40, + {leading: true, trailing: true, maxWait: 40}, + ); + + componentDidMount() { + this.handleResize(); + } + + render() { + const { + filters, + queryValue, + onQueryBlur, + onQueryChange, + focused, + onClearAll, + appliedFilters, + polaris: {intl}, + onQueryClear, + queryPlaceholder, + context: {resourceName}, + children, + } = this.props; + const {open, mobile, readyForFocus} = this.state; + + const backdropMarkup = open ? ( + + +
+ + ) : null; + + const filtersContentMarkup = filters.map((filter, index) => { + const filterIsOpen = this.state[`${filter.key}${Suffix.Filter}`] === true; + const icon = filterIsOpen ? ChevronUpMinor : ChevronDownMinor; + const className = classNames( + styles.FilterTriggerContainer, + filterIsOpen && styles.open, + index === 0 && styles.first, + index === filters.length - 1 && styles.last, + ); + + const appliedFilterContent = this.getAppliedFilterContent(filter.key); + const appliedFilterBadgeMarkup = appliedFilterContent ? ( +
+ + {appliedFilterContent} + +
+ ) : null; + + const collapsibleID = `${filter.key}Collapsible`; + + return ( +
+ + +
+ + {this.generateFilterMarkup(filter)} + +
+
+
+ ); + }); + + const rightActionMarkup = ( +
+ +
+ ); + + const filtersControlMarkup = ( + + + + + } + clearButton + onClearButtonClick={onQueryClear} + /> + + ); + + const filtersDesktopHeaderMarkup = ( +
+ + {intl.translate('Polaris.Filters.moreFilters')} + +
+ ); + + const filtersMobileHeaderMarkup = ( +
+ +
+ ); + + const filtersDesktopFooterMarkup = ( +
+ + +
+ ); + + const filtersMobileFooterMarkup = ( +
+ {this.hasAppliedFilters ? ( + + ) : ( +
+ +

{intl.translate('Polaris.Filters.noFiltersApplied')}

+
+
+ )} +
+ ); + + const tagsMarkup = + appliedFilters && appliedFilters.length ? ( +
+ {appliedFilters.map((filter) => { + return ( + { + filter.onRemove(filter.key); + }} + > + {filter.label} + + ); + })} +
+ ) : null; + + const filtersContainerMarkup = mobile ? ( + + {filtersMobileHeaderMarkup} + + {filtersContentMarkup} + {filtersMobileFooterMarkup} + + + ) : ( + +
+ {filtersDesktopHeaderMarkup} + + {filtersContentMarkup} + + {filtersDesktopFooterMarkup} +
+
+ ); + + return ( +
+ {filtersControlMarkup} + {filtersContainerMarkup} + {tagsMarkup} + {backdropMarkup} + + +
+ ); + } + + private getAppliedFilterContent(key: string): string | undefined { + const {appliedFilters} = this.props; + + if (!appliedFilters) { + return undefined; + } + + const filter = appliedFilters.find((filter) => filter.key === key); + + return filter == null ? undefined : filter.label; + } + + private getAppliedFilterRemoveHandler(key: string): Function | undefined { + const {appliedFilters} = this.props; + + if (!appliedFilters) { + return undefined; + } + + const filter = appliedFilters.find((filter) => filter.key === key); + + return filter == null ? undefined : filter.onRemove; + } + + private openFilters() { + this.setState({open: true}); + } + + private closeFilters = () => { + this.setState({open: false}, () => { + if (this.moreFiltersButtonContainer.current) { + focusFirstFocusableNode(this.moreFiltersButtonContainer.current, false); + } + }); + }; + + private toggleFilters = () => { + if (this.state.open === true) { + this.closeFilters(); + } else { + this.openFilters(); + } + }; + + private setReadyForFocus = (newState: boolean) => () => { + this.setState({readyForFocus: newState}); + }; + + private openFilter(key: string) { + this.setState({[`${key}${Suffix.Filter}`]: true}); + } + + private closeFilter(key: string) { + this.setState({[`${key}${Suffix.Filter}`]: false}); + } + + private toggleFilter(key: string) { + if (this.state[`${key}${Suffix.Filter}`] === true) { + this.closeFilter(key); + } else { + this.openFilter(key); + } + } + + private openFilterShortcut(key: string) { + this.setState({[`${key}${Suffix.Shortcut}`]: true}); + } + + private closeFilterShortcut(key: string) { + this.setState({[`${key}${Suffix.Shortcut}`]: false}); + } + + private toggleFilterShortcut(key: string) { + if (this.state[`${key}${Suffix.Shortcut}`] === true) { + this.closeFilterShortcut(key); + } else { + this.openFilterShortcut(key); + } + } + + private transformFilters(filters: Filter[]): PopoverableAction[] | null { + const transformedActions: PopoverableAction[] = []; + + getShortcutFilters(filters).forEach((filter) => { + const {key, label} = filter; + + transformedActions.push({ + popoverContent: this.generateFilterMarkup(filter), + popoverOpen: this.state[`${key}${Suffix.Shortcut}`], + key, + content: label, + onAction: () => this.toggleFilterShortcut(key), + }); + }); + return transformedActions; + } + + private generateFilterMarkup(filter: Filter) { + const intl = this.props.polaris.intl; + const removeCallback = this.getAppliedFilterRemoveHandler(filter.key); + const removeHandler = + removeCallback == null + ? undefined + : () => { + removeCallback(filter.key); + }; + return ( + + {filter.filter} + + + ); + } +} + +function isMobile(): boolean { + return navigationBarCollapsed().matches; +} + +function getShortcutFilters(filters: Filter[]) { + return filters.filter((filter) => filter.shortcut === true); +} + +export default compose( + withAppProvider(), + withContext(Consumer), +)(Filters); diff --git a/src/components/Filters/README.md b/src/components/Filters/README.md new file mode 100644 index 00000000000..19afca79605 --- /dev/null +++ b/src/components/Filters/README.md @@ -0,0 +1,482 @@ +--- +name: Filters +category: Lists and tables +keywords: + - filters + - filtering + - filter control + - resource list + - index + - list filter + - table +--- + +# Filters + +Filters is a composite component that filters the items of a list or table. + +Merchants use filters to: + +- view different subsets of items in a list or table +- filter by typing into a text field +- filter by selecting filters or promoted filters + +The way that merchants interact with filters depends on the components that you decide to incorporate. In its simplest form, filters includes a text field and a set of filters, which can be displayed in different ways. For example, you could show promoted filters and a More button that opens a [sheet](/components/overlays/sheet) containing more filters. What the filters are and how they’re exposed to merchants is flexible. + +--- + +## Accessibility + +The filters component relies on the accessibility features of multiple other components: + +- [Text field](https://polaris.shopify.com/components/forms/text-field) +- [Button](https://polaris.shopify.com/components/actions/button) +- [Popover](https://polaris.shopify.com/components/overlays/popover) +- [Sheet](https://polaris.shopify.com/components/overlays/sheet) +- [Collapsible](https://polaris.shopify.com/components/behavior/collapsible) + +### Maintain accessibility with custom features + +Since custom HTML can be passed to the component for additional actions, ensure that the filtering system you build is accessible as a whole. + +All merchants must: + +- be able to identify and understand labels for all controls +- be notified of state changes +- be able to complete all actions with the keyboard + +--- + +## Best practices + +The filters component should: + +- help reduce merchant effort by promoting the filtering categories that are most commonly used +- include no more than 2 or 3 promoted filters +- consider small screen sizes when designing the interface for each filter and the total number filters to include +- use children only for content that’s related or relevant to filtering + +--- + +## Content guidelines + +### Text field + +The text field should be clearly labeled so it’s obvious to merchants what they should enter into the field. + + + +#### Do + +- Filter orders + +#### Don’t + +- Enter text here + + + +### Filter badges + +Use the name of the filter if the purpose of the name is clear on its own. For example, when you see a filter badge that reads **Fulfilled**, it’s intuitive that it falls under the Fulfillment status category. + + + +#### Do + +- Fulfilled, Unfulfilled + +#### Don’t + +- Fulfillment: Fulfilled, Unfulfilled + + + +If the filter name is ambiguous on its own, add a descriptive word related to the status. For example, **Low** doesn’t make sense out of context. Add the word “risk” so that merchants know it’s from the Risk category. + + + +#### Do + +- High risk, Low risk + +#### Don’t + +- High, Low + + + +Group tags from the same category together. + + + +#### Do + +- (Unfulfilled, Fulfilled) + +#### Don’t + +- (Unfulfilled) (fulfilled) + + + +If all tag pills selected: truncate in the middle + + + +#### Do + +- Paid, par… unpaid + +#### Don’t + +- All payment status filters selected, Paid, unpa… + + + +--- + +## Examples + +### Filtering with a resource list + +```jsx +class FiltersExample extends React.Component { + state = { + accountStatus: null, + moneySpent: null, + taggedWith: null, + queryValue: null, + }; + + render() { + const {accountStatus, moneySpent, taggedWith, queryValue} = this.state; + + const filters = [ + { + key: 'accountStatus', + label: 'Account status', + filter: ( + + ), + shortcut: true, + }, + { + key: 'moneySpent', + label: 'Money spent', + filter: ( + + ), + shortcut: true, + }, + { + key: 'taggedWith', + label: 'Tagged with', + filter: ( + + ), + shortcut: true, + }, + ]; + + const appliedFilters = Object.keys(this.state) + .filter((key) => !isEmpty(this.state[key]) && key !== 'queryValue') + .map((key) => { + return { + key, + label: disambiguateLabel(key, this.state[key]), + onRemove: this.handleRemove, + }; + }); + + return ( +
+ + + } + items={[ + { + id: 341, + url: 'customers/341', + name: 'Mae Jemison', + location: 'Decatur, USA', + }, + { + id: 256, + url: 'customers/256', + name: 'Ellen Ochoa', + location: 'Los Angeles, USA', + }, + ]} + renderItem={(item) => { + const {id, url, name, location} = item; + const media = ; + + return ( + +

+ {name} +

+
{location}
+
+ ); + }} + /> +
+
+ ); + } + + handleChange = (key) => (value) => { + this.setState({[key]: value}); + }; + + handleRemove = (key) => { + this.setState({[key]: null}); + }; + + handleQueryClear = () => { + this.setState({queryValue: null}); + }; + + handleClearAll = () => { + this.setState({ + accountStatus: null, + moneySpent: null, + taggedWith: null, + queryValue: null, + }); + }; +} + +function disambiguateLabel(key, value) { + switch (key) { + case 'moneySpent': + return `Money spent is between $${value[0]} and $${value[1]}`; + case 'taggedWith': + return `Tagged with ${value}`; + case 'accountStatus': + return value.map((val) => `Customer ${val}`).join(', '); + default: + return value; + } +} + +function isEmpty(value) { + if (Array.isArray(value)) { + return value.length === 0; + } else { + return value === '' || value == null; + } +} +``` + +### Filtering with a data table + +```jsx +class FiltersExample extends React.Component { + state = { + availability: null, + productType: null, + taggedWith: null, + queryValue: null, + }; + + render() { + const {availability, productType, taggedWith, queryValue} = this.state; + + const filters = [ + { + key: 'availability', + label: 'Availability', + filter: ( + + ), + shortcut: true, + }, + { + key: 'productType', + label: 'Product type', + filter: ( + + ), + }, + { + key: 'taggedWith', + label: 'Tagged with', + filter: ( + + ), + }, + ]; + + const appliedFilters = Object.keys(this.state) + .filter((key) => !isEmpty(this.state[key]) && key !== 'queryValue') + .map((key) => { + return { + key, + label: disambiguateLabel(key, this.state[key]), + onRemove: this.handleRemove, + }; + }); + + return ( +
+ + + + + + + + +
+ ); + } + + handleChange = (key) => (value) => { + this.setState({[key]: value}); + }; + + handleRemove = (key) => { + this.setState({[key]: null}); + }; + + handleQueryClear = () => { + this.setState({queryValue: null}); + }; + + handleClearAll = () => { + this.setState({ + availability: null, + productType: null, + taggedWith: null, + queryValue: null, + }); + }; +} + +function disambiguateLabel(key, value) { + switch (key) { + case 'taggedWith': + return `Tagged with ${value}`; + case 'availability': + return value.map((val) => `Available on ${val}`).join(', '); + case 'productType': + return value.join(', '); + default: + return value; + } +} + +function isEmpty(value) { + if (Array.isArray(value)) { + return value.length === 0; + } else { + return value === '' || value == null; + } +} +``` diff --git a/src/components/Filters/components/ConnectedFilterControl/ConnectedFilterControl.scss b/src/components/Filters/components/ConnectedFilterControl/ConnectedFilterControl.scss new file mode 100644 index 00000000000..882004aec38 --- /dev/null +++ b/src/components/Filters/components/ConnectedFilterControl/ConnectedFilterControl.scss @@ -0,0 +1,82 @@ +// stylelint-disable declaration-no-important +// stylelint-disable selector-max-class +// stylelint-disable selector-max-combinators +// stylelint-disable selector-max-specificity +// stylelint-disable selector-max-compound-selectors +// 🐦🐀 +$stacking-order: ( + item: 10, + focused: 20, +); + +.Item { + position: relative; + z-index: z-index(item, $stacking-order); +} + +.Item-focused { + z-index: z-index(focused, $stacking-order); +} + +.ProxyButtonContainer { + position: absolute; + display: flex; + width: 100%; + height: 0; + visibility: hidden; + + > * { + flex-shrink: 0; + } +} + +.ConnectedFilterControl { + display: flex; + flex-grow: 1; + + .CenterContainer { + flex: 1 1 auto; + min-width: rem(100px); + } + + &.right { + .CenterContainer * { + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; + } + } +} + +.RightContainer { + display: flex; + + .Item > * > * { + margin-left: -1px; + border-radius: 0 !important; + } + + .Item { + flex-shrink: 0; + } +} + +.MoreFiltersButtonContainer * { + margin-left: -1px; + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; + flex-shrink: 0; + white-space: nowrap; +} + +.Wrapper { + display: flex; +} + +.AuxiliaryContainer { + flex-grow: 0; +} +// stylelint-enable selector-max-specificity +// stylelint-enable selector-max-combinators +// stylelint-enable selector-max-class +// stylelint-enable declaration-no-important +// stylelint-enable selector-max-compound-selectors diff --git a/src/components/Filters/components/ConnectedFilterControl/ConnectedFilterControl.tsx b/src/components/Filters/components/ConnectedFilterControl/ConnectedFilterControl.tsx new file mode 100644 index 00000000000..8113b735ade --- /dev/null +++ b/src/components/Filters/components/ConnectedFilterControl/ConnectedFilterControl.tsx @@ -0,0 +1,211 @@ +import React from 'react'; +import debounce from 'lodash/debounce'; +import {classNames} from '@shopify/css-utilities'; + +import {Action, BaseAction} from '../../../../types'; +import Popover from '../../../Popover'; +import Button from '../../../Button'; +import EventListener from '../../../EventListener'; +import {Item} from './components'; + +import styles from './ConnectedFilterControl.scss'; + +export interface PopoverableAction extends Action { + popoverOpen: boolean; + popoverContent: React.ReactNode; + key: string; + content: string; + onAction(): void; +} + +export interface Props { + children: React.ReactNode; + rightPopoverableActions?: PopoverableAction[] | null; + rightAction?: React.ReactNode; + auxiliary?: React.ReactNode; +} + +interface ComputedProperty { + [key: string]: number; +} + +interface State { + availableWidth: number; + proxyButtonsWidth: ComputedProperty; +} + +export const FILTER_FIELD_MIN_WIDTH = 150; +export const FILTER_FIELD_CUSTOM_PROPERTY = '--textfield-min-width'; + +export default class ConnectedFilterControl extends React.Component< + Props, + State +> { + state: State = { + availableWidth: 0, + proxyButtonsWidth: {}, + }; + + private container = React.createRef(); + private proxyButtonContainer = React.createRef(); + private moreFiltersButtonContainer = React.createRef(); + + private handleResize = debounce( + () => { + this.measureProxyButtons(); + this.measureAvailableWidth(); + }, + 40, + {leading: true, trailing: true, maxWait: 40}, + ); + + componentDidMount() { + this.handleResize(); + } + + render() { + const { + children, + rightPopoverableActions, + rightAction, + auxiliary, + } = this.props; + + const className = classNames( + styles.ConnectedFilterControl, + rightPopoverableActions && styles.right, + ); + + const rightMarkup = rightPopoverableActions ? ( +
+ {popoverFrom(this.getActionsToRender(rightPopoverableActions))} +
+ ) : null; + + const rightActionMarkup = rightAction ? ( +
+ {rightAction} +
+ ) : null; + + const proxyButtonMarkup = rightPopoverableActions ? ( +
+ {rightPopoverableActions.map((action) => ( +
+ {activatorButtonFrom(action)} +
+ ))} +
+ ) : null; + + const auxMarkup = auxiliary ? ( +
{auxiliary}
+ ) : null; + + return ( + + {proxyButtonMarkup} +
+
+
+ {children} +
+ {rightMarkup} + {rightActionMarkup} + +
+ {auxMarkup} +
+
+ ); + } + + private measureProxyButtons() { + if (this.proxyButtonContainer.current) { + const proxyButtonsWidth: ComputedProperty = {}; + // this number is magical, but tweaking it solved the problem of items overlapping + const tolerance = 52; + if (this.proxyButtonContainer.current) { + Array.from(this.proxyButtonContainer.current.children).forEach( + (element: Element) => { + const buttonWidth = + element.getBoundingClientRect().width + tolerance; + const buttonKey = (element as HTMLElement).dataset.key; + if (buttonKey) { + proxyButtonsWidth[buttonKey] = buttonWidth; + } + }, + ); + } + + this.setState({proxyButtonsWidth}); + } + } + + private measureAvailableWidth() { + if (this.container.current && this.moreFiltersButtonContainer.current) { + const containerWidth = this.container.current.getBoundingClientRect() + .width; + const moreFiltersButtonWidth = this.moreFiltersButtonContainer.current.getBoundingClientRect() + .width; + const filtersActionWidth = 0; + + const availableWidth = + containerWidth - + FILTER_FIELD_MIN_WIDTH - + moreFiltersButtonWidth - + filtersActionWidth; + + this.setState({availableWidth}); + } + } + + private getActionsToRender( + actions: PopoverableAction[], + ): PopoverableAction[] { + let remainingWidth = this.state.availableWidth; + const actionsToReturn: PopoverableAction[] = []; + for (let i = 0; remainingWidth > 0 && i < actions.length; i++) { + const action = actions[i]; + const actionWidth = this.state.proxyButtonsWidth[action.key]; + if (actionWidth <= remainingWidth) { + actionsToReturn.push(action); + remainingWidth -= actionWidth; + } + } + return actionsToReturn; + } +} + +function popoverFrom(actions: PopoverableAction[]): React.ReactElement[] { + return actions.map((action) => { + return ( + + + {action.popoverContent} + + + ); + }); +} + +function activatorButtonFrom(action: BaseAction): React.ReactElement { + return ( + + ); +} diff --git a/src/components/Filters/components/ConnectedFilterControl/components/Item/Item.tsx b/src/components/Filters/components/ConnectedFilterControl/components/Item/Item.tsx new file mode 100644 index 00000000000..1f541c406d4 --- /dev/null +++ b/src/components/Filters/components/ConnectedFilterControl/components/Item/Item.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import {classNames} from '@shopify/css-utilities'; +import styles from '../../ConnectedFilterControl.scss'; + +export interface Props { + children?: React.ReactNode; +} + +export interface State { + focused: boolean; +} + +export default class Item extends React.PureComponent { + state: State = {focused: false}; + + render() { + const {focused} = this.state; + const {children} = this.props; + const className = classNames( + styles.Item, + focused && styles['Item-focused'], + ); + + return ( +
+ {children} +
+ ); + } + + private handleBlur = () => { + this.setState({focused: false}); + }; + + private handleFocus = () => { + this.setState({focused: true}); + }; +} diff --git a/src/components/Filters/components/ConnectedFilterControl/components/Item/index.ts b/src/components/Filters/components/ConnectedFilterControl/components/Item/index.ts new file mode 100644 index 00000000000..01ed425e2fd --- /dev/null +++ b/src/components/Filters/components/ConnectedFilterControl/components/Item/index.ts @@ -0,0 +1,3 @@ +import Item from './Item'; + +export default Item; diff --git a/src/components/Filters/components/ConnectedFilterControl/components/Item/tests/Item.test.tsx b/src/components/Filters/components/ConnectedFilterControl/components/Item/tests/Item.test.tsx new file mode 100644 index 00000000000..28a9fc448fd --- /dev/null +++ b/src/components/Filters/components/ConnectedFilterControl/components/Item/tests/Item.test.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import {mountWithAppProvider, trigger} from 'test-utilities'; +import Item from '../Item'; + +describe('', () => { + it('handles focus', () => { + const item = mountWithAppProvider(); + + trigger(item.childAt(0), 'onFocus'); + expect(item.state().focused).toBe(true); + }); + + it('handles blur', () => { + const item = mountWithAppProvider(); + + trigger(item.childAt(0), 'onBlur'); + + expect(item.state().focused).toBe(false); + }); +}); diff --git a/src/components/Filters/components/ConnectedFilterControl/components/index.ts b/src/components/Filters/components/ConnectedFilterControl/components/index.ts new file mode 100644 index 00000000000..0b556f1ab34 --- /dev/null +++ b/src/components/Filters/components/ConnectedFilterControl/components/index.ts @@ -0,0 +1 @@ +export {default as Item} from './Item'; diff --git a/src/components/Filters/components/ConnectedFilterControl/index.ts b/src/components/Filters/components/ConnectedFilterControl/index.ts new file mode 100644 index 00000000000..52be243ded0 --- /dev/null +++ b/src/components/Filters/components/ConnectedFilterControl/index.ts @@ -0,0 +1,8 @@ +import ConnectedFilterControl from './ConnectedFilterControl'; + +export { + Props as ConnectedFilterControlProps, + PopoverableAction, +} from './ConnectedFilterControl'; + +export default ConnectedFilterControl; diff --git a/src/components/Filters/components/ConnectedFilterControl/tests/ConnectedFilterControl.test.tsx b/src/components/Filters/components/ConnectedFilterControl/tests/ConnectedFilterControl.test.tsx new file mode 100644 index 00000000000..89faedcab59 --- /dev/null +++ b/src/components/Filters/components/ConnectedFilterControl/tests/ConnectedFilterControl.test.tsx @@ -0,0 +1,112 @@ +import React from 'react'; + +import {Popover, Button} from 'components'; +import {mountWithAppProvider} from 'test-utilities'; + +import ConnectedFilterControl, { + PopoverableAction, +} from '../ConnectedFilterControl'; + +const MockChild = () =>
; +const MockFilter = () =>
; +const MockAux = () =>
; + +const mockRightOpenPopoverableAction: PopoverableAction = { + popoverOpen: true, + popoverContent: MockFilter, + key: 'openAction', + content: 'Open action', + onAction: noop, +}; + +const mockRightClosedPopoverableAction: PopoverableAction = { + popoverOpen: false, + popoverContent: MockFilter, + key: 'closedAction', + content: 'Closed action', + onAction: noop, +}; + +const mockRightAction = ; + +describe('', () => { + it('mounts', () => { + expect(() => { + mountWithAppProvider( + + + , + ); + }).not.toThrow(); + }); + + it('does not render buttons without right actions or right popoverable actions', () => { + const connectedFilterControl = mountWithAppProvider( + + + , + ); + + expect(connectedFilterControl.find(Button).exists()).toBe(false); + }); + + it('does not render popovers without right popoverable actions', () => { + const connectedFilterControl = mountWithAppProvider( + + + , + ); + + expect(connectedFilterControl.find(Popover).exists()).toBe(false); + }); + + it('does render a button with a right action', () => { + const connectedFilterControl = mountWithAppProvider( + + + , + ); + + expect(connectedFilterControl.find(Button).exists()).toBe(true); + }); + + it('does render a button with a popoverable action', () => { + const connectedFilterControl = mountWithAppProvider( + + + , + ); + + expect(connectedFilterControl.find(Button)).toHaveLength(1); + }); + + it('renders three buttons with two popoverable actions and a right action', () => { + const connectedFilterControl = mountWithAppProvider( + + + , + ); + + expect(connectedFilterControl.find(Button)).toHaveLength(3); + }); + + it('renders auxiliary content', () => { + const connectedFilterControl = mountWithAppProvider( + }> + + , + ); + + expect(connectedFilterControl.find(MockAux).exists()).toBe(true); + }); +}); + +function noop() {} diff --git a/src/components/Filters/components/index.ts b/src/components/Filters/components/index.ts new file mode 100644 index 00000000000..a81eb526575 --- /dev/null +++ b/src/components/Filters/components/index.ts @@ -0,0 +1,5 @@ +export { + default as ConnectedFilterControl, + ConnectedFilterControlProps, + PopoverableAction, +} from './ConnectedFilterControl'; diff --git a/src/components/Filters/index.ts b/src/components/Filters/index.ts new file mode 100644 index 00000000000..f62c845c138 --- /dev/null +++ b/src/components/Filters/index.ts @@ -0,0 +1,4 @@ +import Filters from './Filters'; + +export * from './Filters'; +export default Filters; diff --git a/src/components/Filters/tests/Filters.test.tsx b/src/components/Filters/tests/Filters.test.tsx new file mode 100644 index 00000000000..c3c7154e50d --- /dev/null +++ b/src/components/Filters/tests/Filters.test.tsx @@ -0,0 +1,288 @@ +import React from 'react'; +import {ReactWrapper} from 'enzyme'; +import {matchMedia} from '@shopify/jest-dom-mocks'; +import {Button, Popover, Sheet, Tag} from 'components'; + +import {mountWithAppProvider, trigger, findByTestID} from 'test-utilities'; + +import Filters, {Props} from '../Filters'; +import {ConnectedFilterControl} from '../components'; + +const MockFilter = (props: {id: string}) =>
; +const MockChild = () =>
; +const mockProps: Props = { + onQueryChange: noop, + onQueryClear: noop, + onClearAll: noop, + filters: [ + { + key: 'filterOne', + label: 'Filter One', + filter: , + }, + { + key: 'filterTwo', + label: 'Filter Two', + filter: , + }, + { + key: 'filterThree', + label: 'Filter Three', + filter: , + }, + ], +}; + +describe('', () => { + beforeAll(() => { + (window.scroll as any) = jest.fn(); + jest.useFakeTimers(); + }); + + beforeEach(() => { + matchMedia.mock(); + }); + + afterEach(() => { + matchMedia.restore(); + }); + + describe('toggleFilters()', () => { + it('opens the sheet on toggle button click', () => { + const resourceFilters = mountWithAppProvider(); + + trigger(findByTestID(resourceFilters, 'SheetToggleButton'), 'onClick'); + jest.runAllTimers(); + expect(resourceFilters.find(Sheet).props().open).toBe(true); + }); + + it('closes the sheet on second toggle button click', () => { + const resourceFilters = mountWithAppProvider(); + + trigger(findByTestID(resourceFilters, 'SheetToggleButton'), 'onClick'); + trigger(findByTestID(resourceFilters, 'SheetToggleButton'), 'onClick'); + + expect(resourceFilters.find(Sheet).props().open).toBe(false); + }); + + describe('isMobile()', () => { + it('renders a sheet on desktop size with right origin', () => { + const resourceFilters = mountWithAppProvider( + , + ); + + expect(resourceFilters.find(Sheet).exists()).toBe(true); + }); + + it('renders a sheet on mobile size with bottom origin', () => { + matchMedia.setMedia(() => ({matches: true})); + const resourceFilters = mountWithAppProvider( + , + ); + + expect(resourceFilters.find(Sheet).exists()).toBe(true); + }); + + it('opens the sheet at mobile size on toggle button click', () => { + matchMedia.setMedia(() => ({matches: true})); + const resourceFilters = mountWithAppProvider( + , + ); + + trigger(findByTestID(resourceFilters, 'SheetToggleButton'), 'onClick'); + expect(resourceFilters.find(Sheet).props().open).toBe(true); + }); + + it('closes the sheet at mobile size on second toggle button click', () => { + matchMedia.setMedia(() => ({matches: true})); + const resourceFilters = mountWithAppProvider( + , + ); + + trigger(findByTestID(resourceFilters, 'SheetToggleButton'), 'onClick'); + trigger(findByTestID(resourceFilters, 'SheetToggleButton'), 'onClick'); + + expect(resourceFilters.find(Sheet).props().open).toBe(false); + }); + }); + }); + + describe('toggleFilter()', () => { + it('opens the filter on toggle button click', () => { + const resourceFilters = mountWithAppProvider(); + + trigger(findByTestID(resourceFilters, 'SheetToggleButton'), 'onClick'); + trigger(findById(resourceFilters, 'filterOneToggleButton'), 'onClick'); + + expect( + findById(resourceFilters, 'filterOneCollapsible').props().open, + ).toBe(true); + }); + + it('closes the filter on second toggle button click', () => { + const resourceFilters = mountWithAppProvider(); + + trigger(findByTestID(resourceFilters, 'SheetToggleButton'), 'onClick'); + trigger(findById(resourceFilters, 'filterTwoToggleButton'), 'onClick'); + trigger(findById(resourceFilters, 'filterTwoToggleButton'), 'onClick'); + + expect( + findById(resourceFilters, 'filterTwoCollapsible').props().open, + ).toBe(false); + }); + + it('does not close other filters when a filter is toggled', () => { + const resourceFilters = mountWithAppProvider(); + + trigger(findByTestID(resourceFilters, 'SheetToggleButton'), 'onClick'); + trigger(findById(resourceFilters, 'filterOneToggleButton'), 'onClick'); + trigger(findById(resourceFilters, 'filterThreeToggleButton'), 'onClick'); + + expect( + findById(resourceFilters, 'filterOneCollapsible').props().open, + ).toBe(true); + expect( + findById(resourceFilters, 'filterThreeCollapsible').props().open, + ).toBe(true); + }); + }); + + describe('', () => { + const mockPropsWithShortcuts: Props = { + onQueryChange: noop, + onQueryClear: noop, + onClearAll: noop, + filters: [ + { + key: 'filterOne', + label: 'Filter One', + filter: , + shortcut: true, + }, + { + key: 'filterTwo', + label: 'Filter Two', + filter: , + }, + { + key: 'filterThree', + label: 'Filter Three', + filter: , + shortcut: true, + }, + ], + }; + + it('renders', () => { + const resourceFilters = mountWithAppProvider( + , + ); + + expect(resourceFilters.find(ConnectedFilterControl).exists()).toBe(true); + }); + + it('renders children', () => { + const resourceFilters = mountWithAppProvider( + + + , + ); + + expect(resourceFilters.find(MockChild).exists()).toBe(true); + }); + + it('receives the expected props when there are shortcut filters', () => { + const resourceFilters = mountWithAppProvider( + , + ); + + expect( + resourceFilters.find(ConnectedFilterControl).props() + .rightPopoverableActions, + ).toHaveLength(2); + }); + + it('toggles a shortcut filter', () => { + const resourceFilters = mountWithAppProvider( + , + ); + + const connected = resourceFilters.find(ConnectedFilterControl).first(); + connected.setState({availableWidth: 999}); + const shortcut = findByTestID(resourceFilters, 'FilterShortcutContainer') + .find(Button) + .first(); + + trigger(shortcut, 'onClick'); + expect( + resourceFilters + .find(Popover) + .first() + .props().active, + ).toBe(true); + trigger(shortcut, 'onClick'); + expect( + resourceFilters + .find(Popover) + .first() + .props().active, + ).toBe(false); + }); + + it('receives the expected props when there are no shortcut filters', () => { + const resourceFilters = mountWithAppProvider(); + + expect( + resourceFilters.find(ConnectedFilterControl).props() + .rightPopoverableActions, + ).toHaveLength(0); + }); + }); + + describe('appliedFilters', () => { + it('calls remove callback when tag is clicked', () => { + const spy = jest.fn(); + const appliedFilters = [{key: 'filterOne', label: 'foo', onRemove: spy}]; + + const resourceFilters = mountWithAppProvider( + , + ); + + const tag = resourceFilters.find(Tag).first(); + const removeButton = tag.find('button').first(); + + trigger(removeButton, 'onClick'); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith('filterOne'); + }); + + it('calls remove callback when clear button is clicked', () => { + const spy = jest.fn(); + const appliedFilters = [{key: 'filterOne', label: 'foo', onRemove: spy}]; + + const resourceFilters = mountWithAppProvider( + , + ); + + trigger(findByTestID(resourceFilters, 'SheetToggleButton'), 'onClick'); + trigger(findById(resourceFilters, 'filterOneToggleButton'), 'onClick'); + const collapsible = findById(resourceFilters, 'filterOneCollapsible'); + const clearButton = collapsible.find(Button).last(); + + trigger(clearButton, 'onClick'); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith('filterOne'); + }); + }); +}); + +function noop() {} + +function findById(wrapper: ReactWrapper, id: string) { + return wrapper.find(`#${id}`).first(); +} diff --git a/src/components/ResourceList/index.ts b/src/components/ResourceList/index.ts index 73adc2a3c4b..7d1480a8dc3 100644 --- a/src/components/ResourceList/index.ts +++ b/src/components/ResourceList/index.ts @@ -1,8 +1,9 @@ import ResourceList from './ResourceList'; +export {Consumer} from './components'; export * from './components/FilterControl/types'; export {Props as FilterControlProps} from './components/FilterControl'; export {Props} from './ResourceList'; -export {SelectedItems} from './types'; +export {SelectedItems, ResourceListContext} from './types'; export default ResourceList; diff --git a/src/components/Sheet/Sheet.tsx b/src/components/Sheet/Sheet.tsx index 2b29a65f04e..b3b1140c08a 100644 --- a/src/components/Sheet/Sheet.tsx +++ b/src/components/Sheet/Sheet.tsx @@ -7,7 +7,6 @@ import {classNames} from '@shopify/css-utilities'; import {navigationBarCollapsed} from '../../utilities/breakpoints'; import {Key} from '../../types'; import {layer, overlay, Duration} from '../shared'; -import {frameContextTypes, FrameContext} from '../Frame'; import {withAppProvider, WithAppProviderProps} from '../AppProvider'; import Backdrop from '../Backdrop'; @@ -39,6 +38,10 @@ export interface Props { children: React.ReactNode; /** Callback when the backdrop is clicked or `ESC` is pressed */ onClose(): void; + /** Callback when the sheet has completed entering */ + onEntered?(): void; + /** Callback when the sheet has started to exit */ + onExit?(): void; } type ComposedProps = Props & WithAppProviderProps; @@ -48,9 +51,6 @@ export interface State { } class Sheet extends React.Component { - static contextTypes = frameContextTypes; - context: FrameContext; - state: State = { mobile: false, }; @@ -71,37 +71,16 @@ class Sheet extends React.Component { ); componentDidMount() { - const { - state: {mobile}, - context: {frame}, - props: { - polaris: {intl}, - }, - handleToggleMobile, - } = this; - - if (frame == null) { - // eslint-disable-next-line no-console - console.warn(intl.translate('Polaris.Sheet.warningMessage')); - } - - if (mobile !== isMobile()) { - handleToggleMobile(); - } + this.handleResize(); } render() { const { - props: {children, open, onClose}, + props: {children, open, onClose, onEntered, onExit}, state: {mobile}, - context: {frame}, handleResize, } = this; - if (frame == null) { - return null; - } - return ( { in={open} mountOnEnter unmountOnExit + onEntered={onEntered} + onExit={onExit} > {children} diff --git a/src/components/Sheet/tests/Sheet.test.tsx b/src/components/Sheet/tests/Sheet.test.tsx index f223959a983..742a3d10411 100644 --- a/src/components/Sheet/tests/Sheet.test.tsx +++ b/src/components/Sheet/tests/Sheet.test.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import * as PropTypes from 'prop-types'; import {CSSTransition} from 'react-transition-group'; import {matchMedia} from '@shopify/jest-dom-mocks'; @@ -25,7 +24,9 @@ describe('', () => { it('renders its children', () => { const children =
Content
; - const {sheet} = mountWithContext({children}); + const sheet = mountWithAppProvider( + {children}, + ); expect(sheet.find(children)).not.toBeNull(); }); @@ -33,7 +34,9 @@ describe('', () => { it('renders a Backdrop when open', () => { const children =
Content
; - const {sheet} = mountWithContext({children}); + const sheet = mountWithAppProvider( + {children}, + ); const backdrop = sheet.find(Backdrop); expect(backdrop).not.toBeNull(); expect(backdrop.props().onClick).toBe(mockProps.onClose); @@ -42,7 +45,7 @@ describe('', () => { it('renders a css transition component with bottom class names at mobile sizes', () => { matchMedia.setMedia(() => ({matches: true})); - const {sheet} = mountWithContext( + const sheet = mountWithAppProvider(
Content
, @@ -54,7 +57,7 @@ describe('', () => { }); it('renders a css transition component with right class names at desktop sizes', () => { - const {sheet} = mountWithContext( + const sheet = mountWithAppProvider(
Content
, @@ -64,29 +67,6 @@ describe('', () => { RIGHT_CLASS_NAMES, ); }); - - it('warns when not mounted inside of the Frame component', () => { - const warnSpy = jest.spyOn(console, 'warn'); - mountWithAppProvider( - -
Content
-
, - ); - expect(warnSpy).toHaveBeenCalledTimes(1); - expect(warnSpy).toHaveBeenCalledWith( - 'The Sheet component must be used within the Frame component.', - ); - }); }); function noop() {} - -function mountWithContext(element: React.ReactElement) { - const frame = {}; - const sheet = mountWithAppProvider(element, { - context: {frame}, - childContextTypes: {frame: PropTypes.any}, - }); - - return {sheet, frame}; -} diff --git a/src/components/index.ts b/src/components/index.ts index 85886b73f57..67b5ff204a6 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -168,6 +168,8 @@ export {default as Link, Props as LinkProps} from './Link'; export {default as List, Props as ListProps} from './List'; +export {default as Filters, Props as FiltersProps} from './Filters'; + export {default as Loading, Props as LoadingProps} from './Loading'; export {default as Modal, Props as ModalProps} from './Modal'; diff --git a/src/locales/de.json b/src/locales/de.json index 862a8765d72..d2bb4464fa2 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -196,9 +196,6 @@ } } }, - "Sheet": { - "warningMessage": "Die Sheet-Komponente muss innerhalb der Frame-Komponente verwendet werden." - }, "SkeletonPage": { "loadingLabel": "Seite wird geladen" }, @@ -221,6 +218,16 @@ "clearButtonLabel": "Löschen", "search": "Suchen" } + }, + "Filters": { + "moreFilters": "Weitere Filter", + "filter": "{resourceName} filtern", + "noFiltersApplied": "Keine Filter angewendet", + "cancel": "Abbrechen", + "done": "Fertig", + "clearAllFilters": "Alle Filter löschen", + "clear": "Löschen", + "clearLabel": "{filterName} löschen" } } } diff --git a/src/locales/en.json b/src/locales/en.json index 71520b87069..341880020b7 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -122,6 +122,17 @@ } }, + "Filters": { + "moreFilters": "More filters", + "filter": "Filter {resourceName}", + "noFiltersApplied": "No filters applied", + "cancel": "Cancel", + "done": "Done", + "clearAllFilters": "Clear all filters", + "clear": "Clear", + "clearLabel": "Clear {filterName}" + }, + "Modal": { "iFrameTitle": "body markup", "modalWarning": "These required properties are missing from Modal: {missingProps}" @@ -222,10 +233,6 @@ } }, - "Sheet": { - "warningMessage": "The Sheet component must be used within the Frame component." - }, - "SkeletonPage": { "loadingLabel": "Page loading" }, diff --git a/src/locales/es.json b/src/locales/es.json index 28bf208e132..c9daf74cf9f 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -196,9 +196,6 @@ } } }, - "Sheet": { - "warningMessage": "El componente Hoja de cálculo debe usarse dentro del componente Frame." - }, "SkeletonPage": { "loadingLabel": "Cargando página" }, @@ -221,6 +218,16 @@ "clearButtonLabel": "Borrar", "search": "Búsqueda" } + }, + "Filters": { + "moreFilters": "Más filtros", + "filter": "Filtrar {resourceName}", + "noFiltersApplied": "No se aplicaron filtros", + "cancel": "Cancelar", + "done": "Listo", + "clearAllFilters": "Borrar todos los filtros", + "clear": "Borrar", + "clearLabel": "Borrar {filterName}" } } } diff --git a/src/locales/fr.json b/src/locales/fr.json index 59f58aac16a..0304db21ef1 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -196,9 +196,6 @@ } } }, - "Sheet": { - "warningMessage": "Le composant Feuille doit être utilisé dans le composant Cadre." - }, "SkeletonPage": { "loadingLabel": "Chargement de page" }, @@ -221,6 +218,16 @@ "clearButtonLabel": "Effacer", "search": "Recherche" } + }, + "Filters": { + "moreFilters": "Plus de filtres", + "filter": "Filtre {resourceName}", + "noFiltersApplied": "Aucun filtre appliqué", + "cancel": "Annuler", + "done": "Terminé", + "clearAllFilters": "Effacer tous les filtres", + "clear": "Effacer", + "clearLabel": "Effacer {filterName}" } } } diff --git a/src/locales/hi.json b/src/locales/hi.json index d525eb6d042..02c26cc2812 100644 --- a/src/locales/hi.json +++ b/src/locales/hi.json @@ -196,9 +196,6 @@ } } }, - "Sheet": { - "warningMessage": "शीट कंपोंनेंट का उपयोग फ़्रेम कंपोंनेंट के भीतर किया जाना चाहिए." - }, "SkeletonPage": { "loadingLabel": "पेज लोड हो रहा है" }, @@ -221,6 +218,16 @@ "clearButtonLabel": "मिटाएं", "search": "खोजें" } + }, + "Filters": { + "moreFilters": "अधिक फ़िल्टर", + "filter": "फ़िल्टर {resourceName}", + "noFiltersApplied": "कोई फ़िल्टर लागू नहीं किए गए", + "cancel": "रद्द करें", + "done": "पूरा हुआ", + "clearAllFilters": "सभी फ़िल्टर मिटाएं", + "clear": "मिटाएं", + "clearLabel": "{filterName} मिटाएं" } } } diff --git a/src/locales/it.json b/src/locales/it.json index e3cc878a9e4..a4b820db188 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -196,9 +196,6 @@ } } }, - "Sheet": { - "warningMessage": "Il componente Scheda deve essere utilizzato all'interno del componente Cornice." - }, "SkeletonPage": { "loadingLabel": "Caricamento della pagina" }, @@ -221,6 +218,16 @@ "clearButtonLabel": "Cancella", "search": "Cerca" } + }, + "Filters": { + "moreFilters": "Più filtri", + "filter": "Filtra {resourceName}", + "noFiltersApplied": "Nessun filtro applicato", + "cancel": "Annulla", + "done": "Fatto", + "clearAllFilters": "Cancella tutti i filtri", + "clear": "Cancella", + "clearLabel": "Cancella {filterName}" } } } diff --git a/src/locales/ja.json b/src/locales/ja.json index 873bdc738f2..628484635ca 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -196,9 +196,6 @@ } } }, - "Sheet": { - "warningMessage": "シートコンポーネントはフレームコンポーネント内で使用する必要があります。" - }, "SkeletonPage": { "loadingLabel": "ページの読み込み中" }, @@ -221,6 +218,16 @@ "clearButtonLabel": "クリア", "search": "検索" } + }, + "Filters": { + "moreFilters": "細かい絞り込み", + "filter": "フィルター{resourceName}", + "noFiltersApplied": "絞り込みが適用されていません", + "cancel": "キャンセルする", + "done": "完了", + "clearAllFilters": "すべての絞り込みをクリアする", + "clear": "クリア", + "clearLabel": "{filterName}をクリアする" } } } diff --git a/src/locales/ms.json b/src/locales/ms.json index df0a0e03b3e..009dfcdcc04 100644 --- a/src/locales/ms.json +++ b/src/locales/ms.json @@ -196,9 +196,6 @@ } } }, - "Sheet": { - "warningMessage": "Komponen Helaian mesti digunakan dalam komponen Rangka." - }, "SkeletonPage": { "loadingLabel": "Memuatkan halaman" }, @@ -221,6 +218,16 @@ "clearButtonLabel": "Kosongkan", "search": "Carian" } + }, + "Filters": { + "moreFilters": "Lebih banyak penapis", + "filter": "Penapis {resourceName}", + "noFiltersApplied": "Tiada penapis yang digunakan", + "cancel": "Batalkan", + "done": "Selesai", + "clearAllFilters": "Kosongkan semua penapis", + "clear": "Kosongkan", + "clearLabel": "Kosongkan {filterName}" } } } diff --git a/src/locales/nl.json b/src/locales/nl.json index a00b53b91d6..b47a9608936 100644 --- a/src/locales/nl.json +++ b/src/locales/nl.json @@ -196,9 +196,6 @@ } } }, - "Sheet": { - "warningMessage": "De Blad-component moet binnen de Frame-component worden gebruikt." - }, "SkeletonPage": { "loadingLabel": "Pagina is aan het laden" }, @@ -221,6 +218,16 @@ "clearButtonLabel": "Wissen", "search": "Zoeken" } + }, + "Filters": { + "moreFilters": "Meer filters", + "filter": "Filter {resourceName}", + "noFiltersApplied": "Geen filters toegepast", + "cancel": "Annuleren", + "done": "Gereed", + "clearAllFilters": "Alle filters wissen", + "clear": "Wissen", + "clearLabel": "{filterName} wissen" } } } diff --git a/src/locales/pt-BR.json b/src/locales/pt-BR.json index 764e9008b31..e3cb44b3a95 100644 --- a/src/locales/pt-BR.json +++ b/src/locales/pt-BR.json @@ -196,9 +196,6 @@ } } }, - "Sheet": { - "warningMessage": "O componente Planilha deve ser usado dentro do componente Quadro." - }, "SkeletonPage": { "loadingLabel": "Página carregando" }, @@ -221,6 +218,16 @@ "clearButtonLabel": "Limpar", "search": "Pesquisar" } + }, + "Filters": { + "moreFilters": "Mais filtros", + "filter": "Filtrar {resourceName}", + "noFiltersApplied": "Nenhum filtro aplicado", + "cancel": "Cancelar", + "done": "Pronto", + "clearAllFilters": "Limpar todos os filtros", + "clear": "Limpar", + "clearLabel": "Limpar {filterName}" } } } diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 391b57bb0b2..0e7a53cad12 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -196,9 +196,6 @@ } } }, - "Sheet": { - "warningMessage": "必须在 Frame 内使用 Sheet 组件。" - }, "SkeletonPage": { "loadingLabel": "页面加载" }, @@ -221,6 +218,16 @@ "clearButtonLabel": "清除", "search": "搜索" } + }, + "Filters": { + "moreFilters": "更多筛选器", + "filter": "筛选 {resourceName}", + "noFiltersApplied": "未应用筛选器", + "cancel": "取消", + "done": "已完成", + "clearAllFilters": "清除所有筛选条件", + "clear": "清除", + "clearLabel": "清除 {filterName}" } } } diff --git a/src/locales/zh-TW.json b/src/locales/zh-TW.json index 44057c4fba3..35ce5797e83 100644 --- a/src/locales/zh-TW.json +++ b/src/locales/zh-TW.json @@ -196,9 +196,6 @@ } } }, - "Sheet": { - "warningMessage": "Sheet組件必須在Frame組件中使用。" - }, "SkeletonPage": { "loadingLabel": "頁面載入中" }, @@ -221,6 +218,16 @@ "clearButtonLabel": "清除", "search": "搜尋" } + }, + "Filters": { + "moreFilters": "更多篩選條件", + "filter": "篩選 {resourceName}", + "noFiltersApplied": "未套用篩選條件", + "cancel": "取消", + "done": "完成", + "clearAllFilters": "清除所有篩選條件", + "clear": "清除", + "clearLabel": "清除 {filterName}" } } }