diff --git a/.storybook/main.js b/.storybook/main.js index c2b18054dd9..74cc60861ab 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -1,5 +1,7 @@ +const { highlightLog } = require('../scripts/utils'); const path = require('path'); const PATHS = require('../config/paths'); +const dedent = require('dedent'); require('dotenv').config({ path: path.join(PATHS.root, '.env') }); @@ -13,12 +15,7 @@ const DEPENDENCY_REGEX = BUILD_FOR_IE11 module.exports = { stories: ['../docs/**/*.stories.mdx', '../packages/**/*.stories.mdx', '../packages/**/*.stories.[tj]sx'], - addons: [ - '@storybook/addon-toolbars', - '@storybook/addon-docs', - '@storybook/addon-controls', - '@storybook/addon-actions' - ], + addons: ['@storybook/addon-knobs', '@storybook/addon-docs', '@storybook/addon-actions'], webpack: async (config, { configType }) => { // `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION' // You can change the configuration based on that. @@ -30,6 +27,35 @@ module.exports = { type: 'javascript/auto' }); + const tsLoader = { + test: /\.tsx?$/, + include: PATHS.packages, + use: [ + { + loader: require.resolve('babel-loader'), + options: { + envName: 'esm', + configFile: path.resolve(PATHS.root, 'babel.config.js') + } + } + ] + }; + + if (IS_RELEASE_BUILD) { + highlightLog('Warning: Prop Types Table Generation is active'); + tsLoader.use.push(require.resolve('react-docgen-typescript-loader')); + } else { + highlightLog('Info: Prop Types Table Generation is disabled'); + console.log(dedent` + The Prop Table Generation is very expensive during build-time and therefore disabled by default. + If you need Prop-Tables, you can activate it by adding a '.env' file to the root of the project with the following content: + + UI5_WEBCOMPONENTS_FOR_REACT_RELEASE_BUILD=true\n\n + `); + } + + config.module.rules.push(tsLoader); + if ((IS_RELEASE_BUILD && configType === 'PRODUCTION') || BUILD_FOR_IE11) { config.module.rules.push({ test: /\.(js|mjs)$/, @@ -76,6 +102,8 @@ module.exports = { } }); } + + config.resolve.extensions.push('.ts', '.tsx'); config.resolve.alias = { ...config.resolve.alias, '@shared': path.join(PATHS.root, 'shared'), diff --git a/.storybook/preview.js b/.storybook/preview.js index 8e9eed36757..9e5bf4ebe36 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,3 +1,4 @@ +import { select, withKnobs } from '@storybook/addon-knobs'; import { makeDecorator } from '@storybook/addons'; import { addDecorator, addParameters } from '@storybook/react'; import { setTheme } from '@ui5/webcomponents-base/dist/config/Theme'; @@ -22,11 +23,11 @@ addParameters({ : a[1].id.localeCompare(b[1].id, undefined, { numeric: true, caseFirst: 'upper' }); }, showRoots: true - }, - passArgsFirst: true, - actions: { argTypesRegex: '^on.*' } + } }); +addDecorator(withKnobs); + const ThemeContainer = ({ theme, contentDensity, children, direction }) => { useEffect(() => { if (contentDensity === ContentDensity.Compact) { @@ -36,9 +37,9 @@ const ThemeContainer = ({ theme, contentDensity, children, direction }) => { } }, [contentDensity]); - useEffect(() => { - document.querySelector('html').setAttribute('dir', direction); - }, [direction]); + // useEffect(() => { + // document.querySelector('html').setAttribute('dir', direction.toLowerCase()); + // }, [direction]); useEffect(() => { setTheme(theme); @@ -53,9 +54,9 @@ const withQuery = makeDecorator({ wrapper: (getStory, context) => { return ( {getStory(context)} @@ -64,74 +65,3 @@ const withQuery = makeDecorator({ }); addDecorator(withQuery); - -export const globalArgTypes = { - theme: { - name: 'Theme', - description: 'Fiori Theme', - defaultValue: Themes.sap_fiori_3, - toolbar: { - icon: 'paintbrush', - items: [ - { - value: Themes.sap_fiori_3, - title: Themes.sap_fiori_3 - }, - { - value: Themes.sap_fiori_3_dark, - title: Themes.sap_fiori_3_dark - }, - { - value: Themes.sap_belize, - title: Themes.sap_belize - }, - { - value: Themes.sap_belize_hcb, - title: Themes.sap_belize_hcb - }, - { - value: Themes.sap_belize_hcw, - title: Themes.sap_belize_hcw - } - ] - } - }, - contentDensity: { - name: 'Content Density', - description: 'Content Density', - defaultValue: ContentDensity.Cozy, - toolbar: { - icon: 'component', - items: [ - { - value: ContentDensity.Cozy, - title: ContentDensity.Cozy - }, - { - value: ContentDensity.Compact, - title: ContentDensity.Compact - } - ] - } - }, - direction: { - name: 'Direction', - description: 'Text Direction', - defaultValue: 'ltr', - toolbar: { - icon: 'transfer', - items: [ - { - value: 'ltr', - title: 'LTR', - icon: 'arrowrightalt' - }, - { - value: 'rtl', - title: 'RTL', - icon: 'arrowleftalt' - } - ] - } - } -}; diff --git a/CHANGELOG.md b/CHANGELOG.md index d2dbfa9ec51..c61b3d8c5be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,23 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.10.0-rc.1](https://github.com/SAP/ui5-webcomponents-react/compare/v0.10.0-rc.0...v0.10.0-rc.1) (2020-05-25) + + +### Bug Fixes + +* **AnalyticalTable:** remove padding and scrollbar from select-all header cell ([#536](https://github.com/SAP/ui5-webcomponents-react/issues/536)) ([97158a3](https://github.com/SAP/ui5-webcomponents-react/commit/97158a39663cf40c424829a86962df7070a0dacb)), closes [#532](https://github.com/SAP/ui5-webcomponents-react/issues/532) +* **DurationPicker:** use correct value for defaultProp maxValue ([#529](https://github.com/SAP/ui5-webcomponents-react/issues/529)) ([888d069](https://github.com/SAP/ui5-webcomponents-react/commit/888d069a86784c4833f9257abc67e569be3dd231)) + + +### Features + +* **StyleClassHelper:** add putIfPresent method ([#539](https://github.com/SAP/ui5-webcomponents-react/issues/539)) ([0ae0785](https://github.com/SAP/ui5-webcomponents-react/commit/0ae078554dd0e7e6a1424de6755ec02fa15bb12e)) + + + + + # [0.10.0-rc.0](https://github.com/SAP/ui5-webcomponents-react/compare/v0.9.5...v0.10.0-rc.0) (2020-05-19) diff --git a/config/jestsetup.ts b/config/jestsetup.ts index 12522411d5c..bb44aba718c 100644 --- a/config/jestsetup.ts +++ b/config/jestsetup.ts @@ -6,6 +6,7 @@ import { createSerializer } from 'enzyme-to-json'; import ResizeObserver from 'resize-observer-polyfill'; import 'intersection-observer'; import '@ui5/webcomponents/dist/generated/json-imports/i18n'; +import 'whatwg-fetch'; // React 16 Enzyme adapter Enzyme.configure({ adapter: new Adapter() }); diff --git a/lerna.json b/lerna.json index 3309cb0e6b1..efe573f0380 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "packages": [ "packages/*" ], - "version": "0.10.0-rc.0", + "version": "0.10.0-rc.1", "npmClient": "yarn", "useWorkspaces": true, "command": { diff --git a/package.json b/package.json index 6942c4b254c..079e6d09d69 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,8 @@ "targz": "^1.0.1", "tmp": "^0.1.0", "tslint": "^6.1.2", - "typescript": "^3.8.3" + "typescript": "^3.8.3", + "whatwg-fetch": "^3.0.0" }, "resolutions": { "@types/react": "16.9.34", diff --git a/packages/base/CHANGELOG.md b/packages/base/CHANGELOG.md index 29e2e81875f..304b5bdda76 100644 --- a/packages/base/CHANGELOG.md +++ b/packages/base/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.10.0-rc.1](https://github.com/SAP/ui5-webcomponents-react/compare/v0.10.0-rc.0...v0.10.0-rc.1) (2020-05-25) + + +### Features + +* **StyleClassHelper:** add putIfPresent method ([#539](https://github.com/SAP/ui5-webcomponents-react/issues/539)) ([0ae0785](https://github.com/SAP/ui5-webcomponents-react/commit/0ae078554dd0e7e6a1424de6755ec02fa15bb12e)) + + + + + # [0.10.0-rc.0](https://github.com/SAP/ui5-webcomponents-react/compare/v0.9.5...v0.10.0-rc.0) (2020-05-19) diff --git a/packages/base/package.json b/packages/base/package.json index ca42f6434d1..7cf939a9555 100644 --- a/packages/base/package.json +++ b/packages/base/package.json @@ -1,6 +1,6 @@ { "name": "@ui5/webcomponents-react-base", - "version": "0.10.0-rc.0", + "version": "0.10.0-rc.1", "description": "Base for ui5-webcomponents-react", "main": "index.cjs.js", "module": "index.esm.js", diff --git a/packages/base/src/hooks/useI18nBundle.ts b/packages/base/src/hooks/useI18nBundle.ts index 1a614a26004..ca0eed4f89c 100644 --- a/packages/base/src/hooks/useI18nBundle.ts +++ b/packages/base/src/hooks/useI18nBundle.ts @@ -1,20 +1,40 @@ import { fetchI18nBundle, getI18nBundle } from '@ui5/webcomponents-base/dist/i18nBundle'; import { useEffect, useState } from 'react'; +type TextWithDefault = { key: string; defaultText: string }; +type TextWithPlaceholders = [TextWithDefault, ...string[]]; + interface I18nBundle { - getText: (textObj: { key: string; defaultText: string }, ...args: any) => string; + getText: (textObj: TextWithDefault, ...args: any) => string; } -export const useI18nBundle = (bundleName): I18nBundle => { - const [bundle, setBundle] = useState(getI18nBundle(bundleName)); +const resolveTranslations = (bundle, texts) => { + return texts.map((text) => { + if (Array.isArray(text)) { + const [key, ...replacements] = text; + return bundle.getText(key, replacements); + } + return bundle.getText(text); + }); +}; + +export const useI18nText = (bundleName: string, ...texts: (TextWithDefault | TextWithPlaceholders)[]): string[] => { + const i18nBundle: I18nBundle = getI18nBundle(bundleName); + const [translations, setTranslations] = useState(resolveTranslations(i18nBundle, texts)); useEffect(() => { const fetchAndLoadBundle = async () => { await fetchI18nBundle(bundleName); - setBundle(getI18nBundle(bundleName)); + setTranslations((prev) => { + const next = resolveTranslations(i18nBundle, texts); + if (prev.length === next.length && prev.every((translation, index) => next[index] === translation)) { + return prev; + } + return next; + }); }; fetchAndLoadBundle(); - }, []); + }, [fetchI18nBundle, bundleName, texts]); - return bundle; + return translations; }; diff --git a/packages/base/src/lib/hooks.ts b/packages/base/src/lib/hooks.ts index 962bc294c5c..56cbe7a0aa8 100644 --- a/packages/base/src/lib/hooks.ts +++ b/packages/base/src/lib/hooks.ts @@ -1,6 +1,6 @@ import { useConsolidatedRef } from '../hooks/useConsolidatedRef'; -import { useI18nBundle } from '../hooks/useI18nBundle'; +import { useI18nText } from '../hooks/useI18nBundle'; import { usePassThroughHtmlProps } from '../hooks/usePassThroughHtmlProps'; import { useViewportRange } from '../hooks/useViewportRange'; -export { useConsolidatedRef, usePassThroughHtmlProps, useViewportRange, useI18nBundle }; +export { useConsolidatedRef, usePassThroughHtmlProps, useViewportRange, useI18nText }; diff --git a/packages/base/src/styling/CssSizeVariables.ts b/packages/base/src/styling/CssSizeVariables.ts index 6b34b12a4d8..6c4965f4d9f 100644 --- a/packages/base/src/styling/CssSizeVariables.ts +++ b/packages/base/src/styling/CssSizeVariables.ts @@ -14,6 +14,7 @@ export enum CssSizeVariablesNames { sapWcrAnalyticalTableTreePaddingLevel1 = 'sapWcrAnalyticalTableTreePaddingLevel1', sapWcrAnalyticalTableTreePaddingLevel2 = 'sapWcrAnalyticalTableTreePaddingLevel2', sapWcrAnalyticalTableTreePaddingLevel3 = 'sapWcrAnalyticalTableTreePaddingLevel3', + sapWcrCheckBoxWidthHeight = 'sapWcrCheckBoxWidthHeight', sapWcrAnalyticalTableSelectionColumnWidth = 'sapWcrAnalyticalTableSelectionColumnWidth' } @@ -42,6 +43,7 @@ export const cssVariablesStyles = ` --${CssSizeVariablesNames.sapWcrAnalyticalTableTreePaddingLevel1}:1.5rem; --${CssSizeVariablesNames.sapWcrAnalyticalTableTreePaddingLevel2}:2.25rem; --${CssSizeVariablesNames.sapWcrAnalyticalTableTreePaddingLevel3}:2.75rem; + --${CssSizeVariablesNames.sapWcrCheckBoxWidthHeight}:2.75rem; --${CssSizeVariablesNames.sapWcrAnalyticalTableSelectionColumnWidth}:55px; } @@ -63,6 +65,7 @@ export const cssVariablesStyles = ` --${CssSizeVariablesNames.sapWcrAnalyticalTableTreePaddingLevel1}:1rem; --${CssSizeVariablesNames.sapWcrAnalyticalTableTreePaddingLevel2}:1.5rem; --${CssSizeVariablesNames.sapWcrAnalyticalTableTreePaddingLevel3}:2rem; + --${CssSizeVariablesNames.sapWcrCheckBoxWidthHeight}:2rem; --${CssSizeVariablesNames.sapWcrAnalyticalTableSelectionColumnWidth}:40px; } `; diff --git a/packages/base/src/styling/StyleClassHelper.test.ts b/packages/base/src/styling/StyleClassHelper.test.ts new file mode 100644 index 00000000000..f1c00729f5e --- /dev/null +++ b/packages/base/src/styling/StyleClassHelper.test.ts @@ -0,0 +1,31 @@ +import { StyleClassHelper } from './StyleClassHelper'; + +describe('StyleClassHelper', () => { + test('add class names', () => { + const helper = StyleClassHelper.of('class1', 'class2'); + expect(helper.className).toEqual(`class1 class2`); + }); + + test('put new classes', () => { + const helper = StyleClassHelper.of('firstClass'); + helper.put('secondClass'); + expect(helper.className).toEqual(`firstClass secondClass`); + }); + + test('putIfPresent', () => { + const helper = StyleClassHelper.of('firstClass'); + helper.put('secondClass'); + helper.putIfPresent('ifPresent1'); + helper.putIfPresent('ifPresent2', 'ifPresent3'); + helper.putIfPresent(true === false && 'shouldNotBeThere1', 'a' === 'a' && 'ifPresent4'); + + expect(helper.className).toEqual('firstClass secondClass ifPresent1 ifPresent2 ifPresent3 ifPresent4'); + }); + + test('className, toString and valueOf return same value', () => { + const helper = StyleClassHelper.of('class1', 'class2'); + expect(helper.toString()).toEqual(`class1 class2`); + expect(helper.valueOf()).toEqual(`class1 class2`); + expect(helper.className).toEqual(`class1 class2`); + }); +}); diff --git a/packages/base/src/styling/StyleClassHelper.ts b/packages/base/src/styling/StyleClassHelper.ts index 199ef0c0500..eb71c7b4724 100644 --- a/packages/base/src/styling/StyleClassHelper.ts +++ b/packages/base/src/styling/StyleClassHelper.ts @@ -2,28 +2,40 @@ * Created by d059190 at 16.03.18 */ -class StyleClassHelper extends String { +class StyleClassHelper { private classes: string[] = []; constructor(...classes: string[]) { - super(); this.classes = [...classes]; } - put(...clazz: string[]) { + put(...clazz: string[]): StyleClassHelper { this.classes.push(...clazz); return this; } - valueOf() { - return this.classes.join(' '); + putIfPresent(...clazzes: (string | null | undefined)[]): StyleClassHelper { + for (const clazz of clazzes) { + if (clazz) { + this.classes.push(clazz); + } + } + return this; + } + + valueOf(): string { + return this.className; } - toString() { - return this.valueOf(); + toString(): string { + return this.className; + } + + get className(): string { + return this.classes.join(' '); } - static of(...classes: string[]) { + static of(...classes: string[]): StyleClassHelper { return new StyleClassHelper().put(...classes); } } diff --git a/packages/main/CHANGELOG.md b/packages/main/CHANGELOG.md index 635b5eba361..d434499c45b 100644 --- a/packages/main/CHANGELOG.md +++ b/packages/main/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.10.0-rc.1](https://github.com/SAP/ui5-webcomponents-react/compare/v0.10.0-rc.0...v0.10.0-rc.1) (2020-05-25) + + +### Bug Fixes + +* **AnalyticalTable:** remove padding and scrollbar from select-all header cell ([#536](https://github.com/SAP/ui5-webcomponents-react/issues/536)) ([97158a3](https://github.com/SAP/ui5-webcomponents-react/commit/97158a39663cf40c424829a86962df7070a0dacb)), closes [#532](https://github.com/SAP/ui5-webcomponents-react/issues/532) +* **DurationPicker:** use correct value for defaultProp maxValue ([#529](https://github.com/SAP/ui5-webcomponents-react/issues/529)) ([888d069](https://github.com/SAP/ui5-webcomponents-react/commit/888d069a86784c4833f9257abc67e569be3dd231)) + + + + + # [0.10.0-rc.0](https://github.com/SAP/ui5-webcomponents-react/compare/v0.9.5...v0.10.0-rc.0) (2020-05-19) diff --git a/packages/main/package.json b/packages/main/package.json index b76b1d55659..f7cb5e525d3 100644 --- a/packages/main/package.json +++ b/packages/main/package.json @@ -1,6 +1,6 @@ { "name": "@ui5/webcomponents-react", - "version": "0.10.0-rc.0", + "version": "0.10.0-rc.1", "description": "React Wrapper for UI5 Web Components and additional components", "main": "index.cjs.js", "module": "index.esm.js", @@ -32,7 +32,7 @@ }, "dependencies": { "@babel/runtime": "7.9.6", - "@ui5/webcomponents-react-base": "^0.10.0-rc.0", + "@ui5/webcomponents-react-base": "^0.10.0-rc.1", "lodash.debounce": "^4.0.8", "react-content-loader": "^5.0.4", "react-jss": "10.1.1", diff --git a/packages/main/src/components/AnalyticalCardHeader/index.tsx b/packages/main/src/components/AnalyticalCardHeader/index.tsx index 8acc1a22d47..aa8f3361d24 100644 --- a/packages/main/src/components/AnalyticalCardHeader/index.tsx +++ b/packages/main/src/components/AnalyticalCardHeader/index.tsx @@ -1,5 +1,5 @@ import { createComponentStyles } from '@ui5/webcomponents-react-base/lib/createComponentStyles'; -import { useI18nBundle, usePassThroughHtmlProps } from '@ui5/webcomponents-react-base/lib/hooks'; +import { useI18nText, usePassThroughHtmlProps } from '@ui5/webcomponents-react-base/lib/hooks'; import { StyleClassHelper } from '@ui5/webcomponents-react-base/lib/StyleClassHelper'; import { enrichEventWithDetails } from '@ui5/webcomponents-react-base/lib/Utils'; import { DEVIATION, TARGET } from '@ui5/webcomponents-react/dist/assets/i18n/i18n-defaults'; @@ -126,7 +126,7 @@ export const AnalyticalCardHeader: FC = forwardRe const passThroughProps = usePassThroughHtmlProps(props, ['onHeaderPress']); - const i18nBundle = useI18nBundle('@ui5/webcomponents-react'); + const [targetText, deviationText] = useI18nText('@ui5/webcomponents-react', TARGET, DEVIATION); return (
= forwardRe className={classes.targetAndDeviationColumn} wrap={FlexBoxWrap.NoWrap} > - {i18nBundle.getText(TARGET)} + {targetText} {target} )} @@ -182,7 +182,7 @@ export const AnalyticalCardHeader: FC = forwardRe className={classes.targetAndDeviationColumn} wrap={FlexBoxWrap.NoWrap} > - {i18nBundle.getText(DEVIATION)} + {deviationText} {deviation} )} diff --git a/packages/main/src/components/AnalyticalTable/AnayticalTable.jss.ts b/packages/main/src/components/AnalyticalTable/AnayticalTable.jss.ts index b1f36b339a8..8f44b6b68d4 100644 --- a/packages/main/src/components/AnalyticalTable/AnayticalTable.jss.ts +++ b/packages/main/src/components/AnalyticalTable/AnayticalTable.jss.ts @@ -22,7 +22,6 @@ const styles = { th: { backgroundColor: ThemingParameters.sapList_HeaderBackground, height: CssSizeVariables.sapWcrAnalyticalTableRowHeight, - fontWeight: 'normal', color: ThemingParameters.sapList_HeaderTextColor, borderTop: `1px solid ${ThemingParameters.sapList_BorderColor}`, borderBottom: `1px solid ${ThemingParameters.sapList_BorderColor}`, @@ -41,52 +40,39 @@ const styles = { }, tbody: { position: 'relative', - zIndex: 0, backgroundColor: ThemingParameters.sapList_Background, overflowX: 'hidden', overflowY: 'auto' }, alternateRowColor: { - backgroundColor: ThemingParameters.sapList_HeaderBackground + backgroundColor: ThemingParameters.sapList_AlternatingBackground }, - emptyRow: {}, tr: { position: 'absolute', top: 0, left: 0, width: '100%', - zIndex: 0, color: ThemingParameters.sapList_TextColor, borderBottom: `1px solid ${ThemingParameters.sapList_BorderColor}`, boxSizing: 'border-box', display: 'flex', height: CssSizeVariables.sapWcrAnalyticalTableRowHeight, + '&:hover': { + backgroundColor: ThemingParameters.sapList_Hover_Background + }, '&[data-is-selected]': { borderBottom: `1px solid ${ThemingParameters.sapList_SelectionBorderColor}`, - backgroundColor: `${ThemingParameters.sapList_SelectionBackgroundColor} !important` + backgroundColor: ThemingParameters.sapList_SelectionBackgroundColor }, '&[data-is-selected]:hover': { - backgroundColor: `${ThemingParameters.sapList_Hover_SelectionBackground} !important` - } - }, - tableGroupHeader: { - '&$tr': { - backgroundColor: `${ThemingParameters.sapList_TableGroupHeaderBackground} !important`, - border: `1px solid ${ThemingParameters.sapList_TableGroupHeaderBorderColor}`, - color: ThemingParameters.sapList_TextColor, - '& $tableCell': { - borderRight: 'none' - } + backgroundColor: ThemingParameters.sapList_Hover_SelectionBackground } }, - selectable: { - '& $tr:hover:not($emptyRow)': { - backgroundColor: ThemingParameters.sapList_Hover_Background, - '&:not($selectionModeRowSelector)': { - cursor: 'pointer' - } + trActive: { + '&:hover': { + cursor: 'pointer' }, - '& $tr:active:not([data-is-selected]):not($tableGroupHeader):not($emptyRow):not($selectionModeRowSelector)': { + '&:active': { backgroundColor: ThemingParameters.sapList_Active_Background, '& $tableCell': { borderRight: `1px solid ${ThemingParameters.sapList_Active_Background}`, @@ -95,7 +81,16 @@ const styles = { } } }, - selectionModeRowSelector: {}, + tableGroupHeader: { + '&$tr': { + backgroundColor: `${ThemingParameters.sapList_TableGroupHeaderBackground}`, + border: `1px solid ${ThemingParameters.sapList_TableGroupHeaderBorderColor}`, + color: ThemingParameters.sapList_TextColor, + '& $tableCell': { + borderRight: 'none' + } + } + }, tableCell: { height: CssSizeVariables.sapWcrAnalyticalTableRowHeight, boxSizing: 'border-box', diff --git a/packages/main/src/components/AnalyticalTable/ColumnHeader/ColumnHeaderModal.tsx b/packages/main/src/components/AnalyticalTable/ColumnHeader/ColumnHeaderModal.tsx index 65505729f91..80b3f6da9bc 100644 --- a/packages/main/src/components/AnalyticalTable/ColumnHeader/ColumnHeaderModal.tsx +++ b/packages/main/src/components/AnalyticalTable/ColumnHeader/ColumnHeaderModal.tsx @@ -1,5 +1,5 @@ import '@ui5/webcomponents-icons/dist/icons/decline'; -import { useI18nBundle } from '@ui5/webcomponents-react-base/lib/hooks'; +import { useI18nText } from '@ui5/webcomponents-react-base/lib/hooks'; import { ThemingParameters } from '@ui5/webcomponents-react-base/lib/ThemingParameters'; import { enrichEventWithDetails } from '@ui5/webcomponents-react-base/lib/Utils'; import { @@ -38,7 +38,14 @@ export const ColumnHeaderModal = forwardRef((props: ColumnHeaderModalProperties, const { Filter } = column; - const i18nBundle = useI18nBundle('@ui5/webcomponents-react'); + const [clearSortingText, sortAscendingText, sortDescendingText, groupText, ungroupText] = useI18nText( + '@ui5/webcomponents-react', + CLEAR_SORTING, + SORT_ASCENDING, + SORT_DESCENDING, + GROUP, + UNGROUP + ); const handleSort = useCallback( (e) => { @@ -112,22 +119,22 @@ export const ColumnHeaderModal = forwardRef((props: ColumnHeaderModalProperties, {isSortedAscending && ( - {i18nBundle.getText(CLEAR_SORTING)} + {clearSortingText} )} {showSort && !isSortedAscending && ( - {i18nBundle.getText(SORT_ASCENDING)} + {sortAscendingText} )} {showSort && !isSortedDescending && ( - {i18nBundle.getText(SORT_DESCENDING)} + {sortDescendingText} )} {isSortedDescending && ( - {i18nBundle.getText(CLEAR_SORTING)} + {clearSortingText} )} {showFilter && !column.isGrouped && ( @@ -145,7 +152,7 @@ export const ColumnHeaderModal = forwardRef((props: ColumnHeaderModalProperties, )} {showGroup && ( - {i18nBundle.getText(column.isGrouped ? UNGROUP : GROUP)} + {column.isGrouped ? ungroupText : groupText} )} diff --git a/packages/main/src/components/AnalyticalTable/__snapshots__/AnalyticalTable.test.tsx.snap b/packages/main/src/components/AnalyticalTable/__snapshots__/AnalyticalTable.test.tsx.snap index 887e191c699..f307850e4c5 100644 --- a/packages/main/src/components/AnalyticalTable/__snapshots__/AnalyticalTable.test.tsx.snap +++ b/packages/main/src/components/AnalyticalTable/__snapshots__/AnalyticalTable.test.tsx.snap @@ -398,7 +398,7 @@ exports[`AnalyticalTable Alternate Row Color 1`] = `
@@ -433,7 +433,7 @@ exports[`AnalyticalTable Alternate Row Color 1`] = `
@@ -468,7 +468,7 @@ exports[`AnalyticalTable Alternate Row Color 1`] = `
@@ -905,7 +905,7 @@ exports[`AnalyticalTable Loading - Loader 1`] = `
@@ -940,7 +940,7 @@ exports[`AnalyticalTable Loading - Loader 1`] = `
@@ -975,7 +975,7 @@ exports[`AnalyticalTable Loading - Loader 1`] = `
@@ -1848,7 +1848,7 @@ exports[`AnalyticalTable Tree Table 1`] = `
@@ -1949,7 +1949,7 @@ exports[`AnalyticalTable Tree Table 1`] = `
@@ -2033,7 +2033,7 @@ exports[`AnalyticalTable Tree Table 1`] = `
@@ -2075,7 +2075,7 @@ exports[`AnalyticalTable Tree Table 1`] = `
@@ -2117,7 +2117,7 @@ exports[`AnalyticalTable Tree Table 1`] = `
@@ -2561,7 +2561,7 @@ exports[`AnalyticalTable custom row height 1`] = `
@@ -2596,7 +2596,7 @@ exports[`AnalyticalTable custom row height 1`] = `
@@ -2631,7 +2631,7 @@ exports[`AnalyticalTable custom row height 1`] = `
@@ -3340,7 +3340,7 @@ exports[`AnalyticalTable test Asc desc 1`] = `
@@ -3375,7 +3375,7 @@ exports[`AnalyticalTable test Asc desc 1`] = `
@@ -3410,7 +3410,7 @@ exports[`AnalyticalTable test Asc desc 1`] = `
@@ -3847,7 +3847,7 @@ exports[`AnalyticalTable test Asc desc 2`] = `
@@ -3882,7 +3882,7 @@ exports[`AnalyticalTable test Asc desc 2`] = `
@@ -3917,7 +3917,7 @@ exports[`AnalyticalTable test Asc desc 2`] = `
@@ -4354,7 +4354,7 @@ exports[`AnalyticalTable test Asc desc 3`] = `
@@ -4389,7 +4389,7 @@ exports[`AnalyticalTable test Asc desc 3`] = `
@@ -4424,7 +4424,7 @@ exports[`AnalyticalTable test Asc desc 3`] = `
@@ -4861,7 +4861,7 @@ exports[`AnalyticalTable test drag and drop of a draggable column 1`] = `
@@ -4896,7 +4896,7 @@ exports[`AnalyticalTable test drag and drop of a draggable column 1`] = `
@@ -4931,7 +4931,7 @@ exports[`AnalyticalTable test drag and drop of a draggable column 1`] = `
@@ -5277,7 +5277,7 @@ exports[`AnalyticalTable with highlight row 1`] = `
@@ -5371,7 +5371,7 @@ exports[`AnalyticalTable with highlight row 1`] = `
@@ -5454,7 +5454,7 @@ exports[`AnalyticalTable with highlight row 1`] = `
@@ -5503,7 +5503,7 @@ exports[`AnalyticalTable with highlight row 1`] = `
@@ -5552,7 +5552,7 @@ exports[`AnalyticalTable with highlight row 1`] = `
@@ -5868,7 +5868,7 @@ exports[`AnalyticalTable without selection Column 1`] = `
@@ -5940,7 +5940,7 @@ exports[`AnalyticalTable without selection Column 1`] = `
@@ -6003,7 +6003,7 @@ exports[`AnalyticalTable without selection Column 1`] = `
@@ -6038,7 +6038,7 @@ exports[`AnalyticalTable without selection Column 1`] = `
@@ -6073,7 +6073,7 @@ exports[`AnalyticalTable without selection Column 1`] = `
diff --git a/packages/main/src/components/AnalyticalTable/hooks/useStyling.ts b/packages/main/src/components/AnalyticalTable/hooks/useStyling.ts index 9c4b1cbc3cb..ca3b771aea6 100644 --- a/packages/main/src/components/AnalyticalTable/hooks/useStyling.ts +++ b/packages/main/src/components/AnalyticalTable/hooks/useStyling.ts @@ -61,30 +61,27 @@ const getRowProps = (rowProps, { instance, row }) => { const { classes, selectionBehavior, selectionMode, alternateRowColor } = webComponentsReactProperties; const isEmptyRow = row.original?.emptyRow; let className = classes.tr; + const rowCanBeSelected = + [TableSelectionMode.SINGLE_SELECT, TableSelectionMode.MULTI_SELECT].includes(selectionMode) && !isEmptyRow; if (row.isGrouped) { className += ` ${classes.tableGroupHeader}`; } - if (isEmptyRow) { - className += ` ${classes.emptyRow}`; - } - if (alternateRowColor && row.index % 2 !== 0) { className += ` ${classes.alternateRowColor}`; } - if (TableSelectionBehavior.ROW_SELECTOR === selectionBehavior) { - className += ` ${classes.selectionModeRowSelector}`; - } - const newRowProps = { className, role: 'row', 'aria-rowindex': row.index }; - if ([TableSelectionMode.SINGLE_SELECT, TableSelectionMode.MULTI_SELECT].includes(selectionMode) && !isEmptyRow) { + if (rowCanBeSelected) { + if (TableSelectionBehavior.ROW_SELECTOR !== selectionBehavior) { + newRowProps.className += ` ${classes.trActive}`; + } if (row.isSelected) { newRowProps[ROW_SELECTION_ATTRIBUTE] = ''; } diff --git a/packages/main/src/components/AnalyticalTable/virtualization/VirtualTableBody.tsx b/packages/main/src/components/AnalyticalTable/virtualization/VirtualTableBody.tsx index 80a2bdb5d9e..ed04e476b52 100644 --- a/packages/main/src/components/AnalyticalTable/virtualization/VirtualTableBody.tsx +++ b/packages/main/src/components/AnalyticalTable/virtualization/VirtualTableBody.tsx @@ -64,9 +64,6 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => { }; const classNames = StyleClassHelper.of(classes.tbody, GlobalStyleClasses.sapScrollBar); - if (selectionMode === TableSelectionMode.SINGLE_SELECT || selectionMode === TableSelectionMode.MULTI_SELECT) { - classNames.put(classes.selectable); - } const lastScrollTop = useRef(0); diff --git a/packages/main/src/components/FilterBar/FilterBar.jss.ts b/packages/main/src/components/FilterBar/FilterBar.jss.ts index c696b1c210f..9606b8a4c19 100644 --- a/packages/main/src/components/FilterBar/FilterBar.jss.ts +++ b/packages/main/src/components/FilterBar/FilterBar.jss.ts @@ -44,29 +44,18 @@ const styles = { headerRowRight: { display: 'flex', justifyContent: 'flex-end', - flexGrow: 1 - }, - // is being applied to the span which represents the InfoLabel Text - label: { - fontSize: ThemingParameters.sapFontSmallSize, - fontFamily: ThemingParameters.sapFontFamily, - lineHeight: '1.125rem', - fontWeight: 600, - letterSpacing: '0.0125rem', - textTransform: 'uppercase', - textAlign: 'center', - verticalAlign: 'top', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - display: 'inline-block', - color: ThemingParameters.sapTextColor - }, - // specific padding needed for purely numeric input - numeric: {}, - // specific padding needed for text input - text: {}, - // displayOnly mode - displayOnly: {} + flexGrow: 1, + '& ui5-button': { + marginLeft: '0.5rem' + } + }, + showFiltersBtn: { minWidth: '108px' }, + loadingContainer: { + marginBottom: '0.5rem', + display: 'flex', + width: '100%', + justifyContent: 'center' + } }; export default styles; diff --git a/packages/main/src/components/FilterBar/FilterBar.test.tsx b/packages/main/src/components/FilterBar/FilterBar.test.tsx index a139d33cbff..0258f8c08b2 100644 --- a/packages/main/src/components/FilterBar/FilterBar.test.tsx +++ b/packages/main/src/components/FilterBar/FilterBar.test.tsx @@ -1,124 +1,287 @@ import { createPassThroughPropsTest } from '@shared/tests/utils'; -import { mount } from 'enzyme'; +import { Bar } from '@ui5/webcomponents-react/lib/Bar'; +import { Button } from '@ui5/webcomponents-react/lib/Button'; +import { CheckBox } from '@ui5/webcomponents-react/lib/CheckBox'; +import { DatePicker } from '@ui5/webcomponents-react/lib/DatePicker'; import { FilterBar } from '@ui5/webcomponents-react/lib/FilterBar'; -import { FilterItem } from '@ui5/webcomponents-react/lib/FilterItem'; -import { FilterType } from '@ui5/webcomponents-react/lib/FilterType'; +import { FilterGroupItem } from '@ui5/webcomponents-react/lib/FilterGroupItem'; import { Input } from '@ui5/webcomponents-react/lib/Input'; +import { MultiComboBox } from '@ui5/webcomponents-react/lib/MultiComboBox'; +import { Option } from '@ui5/webcomponents-react/lib/Option'; +import { Select } from '@ui5/webcomponents-react/lib/Select'; import { Switch } from '@ui5/webcomponents-react/lib/Switch'; import { VariantManagement } from '@ui5/webcomponents-react/lib/VariantManagement'; +import { MultiComboBoxItem } from '@ui5/webcomponents-react/lib/MultiComboBoxItem'; +import { mount } from 'enzyme'; import React from 'react'; +import { act } from 'react-dom/test-utils'; const variantItems = [ { label: 'Variant 1', key: '1' }, { label: 'Variant 2', key: '2' } ]; -const filterItems = [ - { text: 'Text 1', key: '1' }, - { text: 'Text 2', key: '2' } -]; const variants = ; const search = ; describe('FilterBar', () => { - it('Render without crashing', () => { + it('Render without crashing - default props', () => { const wrapper = mount( - - alert(e.getParameter('selectedItem').key)} - filterItems={filterItems} - label="Classification" - key="classification" - type={FilterType.Select} - /> - - - + + + + ); expect(wrapper.render()).toMatchSnapshot(); }); - it('Hide Filter Bar', () => { + it('Render without crashing - w/ filter dialog', () => { const wrapper = mount( - - alert(e.getParameter('selectedItem').key)} - filterItems={filterItems} - label="Classification" - key="classification" - type={FilterType.Select} - /> - + + + + + - - + + + + + + + + + + + + + + + + + + + + ); + expect(wrapper.render()).toMatchSnapshot(); + }); + + it('Hide FilterBar', () => { + const wrapper = mount( + + + + + + ); + const toggleBtn = wrapper.find('Button'); + const toggleBtnBefore = toggleBtn.render(); + act(() => { + toggleBtn.getElement().props.onClick(); + }); + expect(toggleBtn.render()).not.toBe(toggleBtnBefore); + expect(wrapper.render()).toMatchSnapshot(); + }); + + it('Toggle Filters Dialog', () => { + const wrapper = mount( + + + + + - + + + + + + + + + + + + + + + + + + ); - const component = wrapper.find('ui5-button').last().instance() as any; - component.fireEvent('click'); + const openFiltersDialogBtn = wrapper.find('.FilterBar-headerRowRight-0-2-7').childAt(3); + act(() => { + openFiltersDialogBtn.prop('onClick')(); + }); + wrapper.update(); + expect(wrapper.find('ui5-dialog').exists()).toBeTruthy(); + expect(wrapper.render()).toMatchSnapshot(); + const filtersDialogBtns = (index) => wrapper.find(Bar).find(Button).at(index); + act(() => { + //@ts-ignore + filtersDialogBtns(0).prop('onClick')(); + }); + wrapper.update(); + expect(wrapper.find('ui5-dialog').exists()).toBeFalsy(); expect(wrapper.render()).toMatchSnapshot(); + act(() => { + openFiltersDialogBtn.prop('onClick')(); + }); + wrapper.update(); + expect(wrapper.find('ui5-dialog').exists()).toBeTruthy(); + act(() => { + //@ts-ignore + filtersDialogBtns(3).prop('onClick')(); + }); + wrapper.update(); + expect(wrapper.find('ui5-dialog').exists()).toBeFalsy(); + act(() => { + openFiltersDialogBtn.prop('onClick')(); + }); + wrapper.update(); + expect(wrapper.find('ui5-dialog').exists()).toBeTruthy(); + act(() => { + //@ts-ignore + filtersDialogBtns(4).prop('onClick')(); + }); + wrapper.update(); + expect(wrapper.find('ui5-dialog').exists()).toBeFalsy(); }); - it('Select Filter Item', () => { + it('Group Filter Items mounted in Dialog', () => { const wrapper = mount( - - alert(e.getParameter('selectedItem').key)} - filterItems={filterItems} - label="Classification" - key="classification" - type={FilterType.Select} - /> - alert(e.getParameter('selectedItem').key)} - loading - filterItems={filterItems} - label="Classification" - key="classification2" - type={FilterType.Select} - /> - alert(e.getParameter('selectedItem').key)} - // filterItems={filterItems} - label="Classification" - key="classification3" - type={FilterType.Default} - /> - alert(e.getParameter('selectedItem').key)} - filterItems={filterItems} - label="Classification" - key="classification3" - type={FilterType.MultiSelect} - /> + + + + + + + + + + ); - wrapper.find('ui5-option').at(1).simulate('change'); + const filterItemsFB = wrapper.find(FilterGroupItem); + const openFiltersDialogBtn = wrapper.find('.FilterBar-headerRowRight-0-2-7').childAt(3); + act(() => { + openFiltersDialogBtn.prop('onClick')(); + }); + wrapper.update(); - expect(wrapper.render()).toMatchSnapshot(); + const filterItemsDialog = wrapper.find('ui5-dialog').find(FilterGroupItem); + + const filterItemsDialogString = filterItemsDialog.debug({ ignoreProps: true }); + const filterItemsFBString = filterItemsFB.debug({ ignoreProps: true }); + // @ts-ignore + expect(filterItemsDialogString).toEqual(filterItemsFBString); + const filterItemMandatory = filterItemsDialog.at(2); + expect(filterItemMandatory.prop('required')).toBeTruthy(); + const checkbox = filterItemMandatory.parents().find(CheckBox).at(0); + expect(checkbox.prop('disabled')).toBeTruthy(); }); createPassThroughPropsTest(FilterBar); diff --git a/packages/main/src/components/FilterBar/FilterBarDialog.jss.ts b/packages/main/src/components/FilterBar/FilterBarDialog.jss.ts new file mode 100644 index 00000000000..74093d4deab --- /dev/null +++ b/packages/main/src/components/FilterBar/FilterBarDialog.jss.ts @@ -0,0 +1,72 @@ +import { CssSizeVariables } from '@ui5/webcomponents-react-base/lib/CssSizeVariables'; +import { sapUiContentPadding } from '@ui5/webcomponents-react-base/lib/spacing'; + +const styles = { + dialog: { + ...sapUiContentPadding, + display: 'flex', + flexDirection: 'column', + maxWidth: '960px', + width: '80vw', + maxHeight: '70vh' + }, + header: { + padding: '0.25rem 1rem 0 1rem', + '& *': { + margin: '0.25rem 0 0.25rem 0' + }, + '& ui5-input': { + width: '100%' + } + }, + footer: { + '& :not(:last-child)': { + marginRight: '0.25rem' + } + }, + groupContainer: { + display: 'flex', + flexDirection: 'column' + }, + groupTitle: { + maxWidth: '85%', + marginRight: '0.5rem' + }, + filters: { + padding: '1rem 0 2rem 0' + }, + singleFilter: { + display: 'grid', + gridTemplateColumns: `auto minmax(${CssSizeVariables.sapWcrCheckBoxWidthHeight},7%)`, + gridTemplateRows: 'auto', + gridColumnGap: '0.5rem', + '@media(max-width:700px)': { + marginTop: '0.5rem' + }, + '& ui5-checkbox': { + placeSelf: 'center start', + '@media(max-width:700px)': { + marginTop: '0.8rem', + paddingLeft: 0, + placeSelf: 'end start' + } + } + }, + + fbSearch: { + '@media(min-width:700px)': { + display: 'grid', + gridTemplateColumns: '20% auto 7%', + gridTemplateRows: 'auto', + gridRowGap: '0.5rem', + gridColumnGap: '0.5rem' + }, + paddingBottom: '2rem', + width: '100%', + '& ui5-input': { + width: '100%' + } + } +}; + +export default styles; diff --git a/packages/main/src/components/FilterBar/FilterDialog.tsx b/packages/main/src/components/FilterBar/FilterDialog.tsx new file mode 100644 index 00000000000..491b999ef45 --- /dev/null +++ b/packages/main/src/components/FilterBar/FilterDialog.tsx @@ -0,0 +1,283 @@ +import '@ui5/webcomponents-icons/dist/icons/search'; +import { createComponentStyles } from '@ui5/webcomponents-react-base/lib/createComponentStyles'; +import { useI18nText } from '@ui5/webcomponents-react-base/lib/hooks'; +import { enrichEventWithDetails } from '@ui5/webcomponents-react-base/lib/Utils'; +import { + BASIC, + CANCEL, + CLEAR, + RESTORE, + SAVE, + SEARCH_FOR_FILTERS, + SHOW_ON_FILTER_BAR +} from '@ui5/webcomponents-react/dist/assets/i18n/i18n-defaults'; +import { Bar } from '@ui5/webcomponents-react/lib/Bar'; +import { BarDesign } from '@ui5/webcomponents-react/lib/BarDesign'; +import { Button } from '@ui5/webcomponents-react/lib/Button'; +import { ButtonDesign } from '@ui5/webcomponents-react/lib/ButtonDesign'; +import { CheckBox } from '@ui5/webcomponents-react/lib/CheckBox'; +import { Dialog } from '@ui5/webcomponents-react/lib/Dialog'; +import { FlexBox } from '@ui5/webcomponents-react/lib/FlexBox'; +import { FlexBoxAlignItems } from '@ui5/webcomponents-react/lib/FlexBoxAlignItems'; +import { FlexBoxDirection } from '@ui5/webcomponents-react/lib/FlexBoxDirection'; +import { FlexBoxJustifyContent } from '@ui5/webcomponents-react/lib/FlexBoxJustifyContent'; +import { Icon } from '@ui5/webcomponents-react/lib/Icon'; +import { Input } from '@ui5/webcomponents-react/lib/Input'; +import { Text } from '@ui5/webcomponents-react/lib/Text'; +import { Title } from '@ui5/webcomponents-react/lib/Title'; +import { TitleLevel } from '@ui5/webcomponents-react/lib/TitleLevel'; +import React, { Children, cloneElement, ReactElement, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import styles from './FilterBarDialog.jss'; +import { filterValue, renderSearchWithValue } from './utils'; + +const useStyles = createComponentStyles(styles, { name: 'FilterBarDialog' }); +export const FilterDialog = (props) => { + const { + filterBarRefs, + open, + handleDialogClose, + children, + showClearButton, + showRestoreButton, + showGoButton, + showSearch, + renderFBSearch, + handleClearFilters, + handleRestoreFilters, + handleDialogSave, + searchValue, + handleSearchValueChange, + onGo, + handleSelectionChange, + handleDialogSearch, + handleDialogCancel + } = props; + const classes = useStyles(); + const [searchString, setSearchString] = useState(''); + const searchRef = useRef(null); + const [toggledFilters, setToggledFilters] = useState({}); + const dialogRefs = useRef({}); + const dialogRef = useRef(); + + const [ + basicText, + cancelText, + clearText, + restoreText, + saveText, + searchForFiltersText, + showOnFilterBarText + ] = useI18nText( + '@ui5/webcomponents-react', + BASIC, + CANCEL, + CLEAR, + RESTORE, + SAVE, + SEARCH_FOR_FILTERS, + SHOW_ON_FILTER_BAR + ); + + useEffect(() => { + if (open) { + dialogRef.current.open(); + } + }, [open]); + + const handleSearch = useCallback( + (e) => { + if (handleDialogSearch) { + handleDialogSearch(enrichEventWithDetails(e, { value: e.target.value })); + } + setSearchString(e.target.value); + }, + [setSearchString, handleDialogSearch] + ); + const handleSave = useCallback( + (e) => { + if (renderFBSearch) { + handleSearchValueChange(searchRef.current?.children[1].value); + } + handleDialogSave(e, dialogRefs.current, toggledFilters); + }, + [renderFBSearch, handleSearchValueChange, searchRef, handleDialogSave, toggledFilters, dialogRefs] + ); + + const handleClose = useCallback( + (e) => { + if (!showGoButton) { + handleSave(e); + return; + } + handleDialogClose(e); + }, + [showGoButton, handleSave, handleDialogClose] + ); + + const handleDialogGo = useCallback( + (e) => { + if (onGo) { + onGo(enrichEventWithDetails(e)); + } + handleDialogClose(e); + }, + [onGo, handleDialogClose] + ); + + const handleRestore = useCallback( + (e) => { + handleRestoreFilters(e, 'dialog'); + }, + [handleRestoreFilters] + ); + + const handleCancel = useCallback( + (e) => { + if (handleDialogCancel) { + handleDialogCancel(enrichEventWithDetails(e)); + } + handleDialogClose(e); + }, + [handleDialogCancel] + ); + + const footerContentRight = useMemo( + () => ( + + {showGoButton && ( + + )} + {showClearButton && } + {showRestoreButton && } + + + + ), + [ + showGoButton, + classes.footer, + handleDialogGo, + showClearButton, + handleClearFilters, + showRestoreButton, + handleRestore, + handleSave, + handleCancel + ] + ); + + const renderFooter = useCallback(() => { + return ; + }, [footerContentRight]); + + const renderHeader = useCallback( + () => ( + + Filters + {showSearch && ( + } /> + )} + + ), + [classes.header, showSearch, handleSearch] + ); + + const renderChildren = useCallback(() => { + return children + .filter((item) => { + if (item.type.displayName !== 'FilterGroupItem') return true; //needed for deprecated FilterItem or custom elements + return ( + !!item?.props && + item.props?.visible && + (item.props?.label?.toLowerCase().includes(searchString.toLowerCase()) || searchString.length === 0) + ); + }) + .map((child) => { + if (child.type.displayName !== 'FilterGroupItem') return child; //needed for deprecated FilterItem or custom elements + const filterBarItemRef = filterBarRefs.current[child.key]; + let filterItemProps = {}; + if (filterBarItemRef) { + filterItemProps = filterValue(filterBarItemRef, child); + } + if (!child.props.children) return child; + return cloneElement(child as ReactElement, { + children: { + ...child.props.children, + props: { + ...child.props.children.props, + ...filterItemProps + }, + ref: (node) => { + dialogRefs.current[child.key] = node; + } + } + }); + }); + }, [children, searchString, filterBarRefs]); + + const handleCheckBoxChange = useCallback( + (element, toggledFilters) => (e) => { + if (handleSelectionChange) { + handleSelectionChange(enrichEventWithDetails(e, { element, checked: e.target.checked })); + } + setToggledFilters({ ...toggledFilters, [element.key]: e.target.checked }); + }, + [setToggledFilters, handleSelectionChange] + ); + const renderGroups = useCallback(() => { + let groups = {}; + Children.forEach(renderChildren(), (child) => { + const childGroups = child.props.groupName ?? 'default'; + if (groups[childGroups]) { + groups[childGroups].push(child); + } else { + groups[childGroups] = [child]; + } + }); + return Object.keys(groups) + .sort((x, y) => (x === 'default' ? -1 : y === 'role' ? 1 : 0)) + .map((item, index) => { + const filters = groups[item].map((el) => { + return ( +
+ {el} + +
+ ); + }); + return ( +
+ + + {item === 'default' ? basicText : item} + + {index === 0 && {showOnFilterBarText}} + +
{filters}
+
+ ); + }); + }, [renderChildren, toggledFilters, handleCheckBoxChange]); + + return ( + +
+ {renderFBSearch && ( +
+ + {renderSearchWithValue(renderFBSearch, searchValue)} +
+ )} + {renderGroups()} +
+
+ ); +}; diff --git a/packages/main/src/components/FilterBar/__snapshots__/FilterBar.test.tsx.snap b/packages/main/src/components/FilterBar/__snapshots__/FilterBar.test.tsx.snap index a0c775f1c03..9c136e6c121 100644 --- a/packages/main/src/components/FilterBar/__snapshots__/FilterBar.test.tsx.snap +++ b/packages/main/src/components/FilterBar/__snapshots__/FilterBar.test.tsx.snap @@ -1,66 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`FilterBar Hide Filter Bar 1`] = ` +exports[`FilterBar Hide FilterBar 1`] = `
-
- - Variant 1 - - - - - Cancel - - - - Variant 1 - - - Variant 2 - - - -
Show Filter Bar @@ -71,72 +22,107 @@ exports[`FilterBar Hide Filter Bar 1`] = ` class="FilterBar-filterArea-0 FilterBar-filterAreaClosed-0" >
- +
+ + Classification - + +
- - Text 1 + + Option 1 - Text 2 + Option 2 + + + Option 3 + + + Option 4
+
+
+`; + +exports[`FilterBar Render without crashing - default props 1`] = ` +
+
-
- - Custom Filter 1 - -
- -
-
+ + Hide Filter Bar +
+
+
- - Custom Filter 1 -
- + + + Classification +
+ + + Option 1 + + + Option 2 + + + Option 3 + + + Option 4 + +
`; -exports[`FilterBar Render without crashing 1`] = ` +exports[`FilterBar Render without crashing - w/ filter dialog 1`] = `
@@ -196,74 +182,921 @@ exports[`FilterBar Render without crashing 1`] = `
- -
+ Clear + + + Restore + + Hide Filter Bar + + Filters (1337) + + + Go +
- - Classification - +
+ + + Input + +
+ +
+
+
+
+
+ + + Switch + +
+ +
+
+
+
+
+ + + SELECT w/ initial selected + +
+ + Option 1 + + Option 2 + + + Option 3 + + + Option 4 + + +
+
+
+
+
+ + + SELECT w/o initial selected + +
+ + - Text 1 + Test 1 + Test 2 + + - Text 2 + Test 3 + + + Test 4 + + + Test 5
- - Custom Filter 1 - -
- +
+ + Group 2: + MultBox + +
+ + + + + + +
+
+
+
+
+ + Group 2: + Date Picker +
+
`; -exports[`FilterBar Select Filter Item 1`] = ` +exports[`FilterBar Toggle Filters Dialog 1`] = ` +Array [ + +
+ + Filters + + + + +
+
- - Classification - - - - Text 1 - - - Text 2 - - + Group 2: + Date Picker + +
+
diff --git a/packages/main/src/components/FilterBar/demo.stories.tsx b/packages/main/src/components/FilterBar/demo.stories.tsx index 304767b878d..cb348e05d44 100644 --- a/packages/main/src/components/FilterBar/demo.stories.tsx +++ b/packages/main/src/components/FilterBar/demo.stories.tsx @@ -1,12 +1,14 @@ import { action } from '@storybook/addon-actions'; -import { boolean, select } from '@storybook/addon-knobs'; +import { boolean, number, text } from '@storybook/addon-knobs'; +import { DatePicker } from '@ui5/webcomponents-react/lib/DatePicker'; import { FilterBar } from '@ui5/webcomponents-react/lib/FilterBar'; -import { FilterItem } from '@ui5/webcomponents-react/lib/FilterItem'; -import { FilterType } from '@ui5/webcomponents-react/lib/FilterType'; +import { FilterGroupItem } from '@ui5/webcomponents-react/lib/FilterGroupItem'; import { Input } from '@ui5/webcomponents-react/lib/Input'; -import { PlacementType } from '@ui5/webcomponents-react/lib/PlacementType'; +import { MultiComboBox } from '@ui5/webcomponents-react/lib/MultiComboBox'; +import { MultiComboBoxItem } from '@ui5/webcomponents-react/lib/MultiComboBoxItem'; +import { Option } from '@ui5/webcomponents-react/lib/Option'; +import { Select } from '@ui5/webcomponents-react/lib/Select'; import { Switch } from '@ui5/webcomponents-react/lib/Switch'; -import { TitleLevel } from '@ui5/webcomponents-react/lib/TitleLevel'; import { VariantManagement } from '@ui5/webcomponents-react/lib/VariantManagement'; import React from 'react'; @@ -14,61 +16,176 @@ const variantItems = [ { label: 'Variant 1', key: '1' }, { label: 'Variant 2', key: '2' } ]; -const filterItems = [ - { text: 'Text 1', key: '1' }, - { text: 'Text 2', key: '2' } -]; -export const renderStory = () => { +export const renderDefaultStory = () => { + return ( + } + variants={} + useToolbar={boolean('useToolbar', true)} + filterBarExpanded={boolean('filterBarExpanded', true)} + loading={boolean('loading', false)} + considerGroupName={boolean('considerGroupName', false)} + filterContainerWidth={text('auto', undefined)} + activeFiltersCount={number('activeFiltersCount', undefined)} + showClearOnFB={boolean('showClearOnFB', false)} + showRestoreOnFB={boolean('showRestoreOnFB', false)} + showGo={boolean('showGo', false)} + showGoOnFB={boolean('showGoOnFB', false)} + showFilterConfiguration={boolean('showFilterConfiguration', false)} + showSearchOnFiltersDialog={boolean('showSearchOnFiltersDialog', false)} + showClearButton={boolean('showClearButton', false)} + showRestoreButton={boolean('showRestoreButton', false)} + onToggleFilters={action('onToggleFilters')} + onFiltersDialogOpen={action('onFiltersDialogOpen')} + onFiltersDialogClose={action('onFiltersDialogClose')} + onFiltersDialogSave={action('onFiltersDialogSave')} + onFiltersDialogClear={action('onFiltersDialogClear')} + onClear={action('onClear')} + onFiltersDialogSelectionChange={action('onFiltersDialogSelectionChange')} + onFiltersDialogSearch={action('onFiltersDialogSearch')} + onGo={action('onGo')} + onRestore={action('onRestore')} + > + + + + + + + + + + + + + + + + ); +}; +renderDefaultStory.story = { + name: 'Default' +}; + +export const renderStoryWithFiltersDialog = () => { return ( } - variants={ - - } + variants={} + useToolbar={boolean('useToolbar', true)} + filterBarExpanded={boolean('filterBarExpanded', true)} + loading={boolean('loading', false)} + considerGroupName={boolean('considerGroupName', true)} + filterContainerWidth={text('filterContainerWidth', '13rem')} + activeFiltersCount={number('activeFiltersCount', 0)} + showClearOnFB={boolean('showClearOnFB', true)} + showRestoreOnFB={boolean('showRestoreOnFB', true)} + showGo={boolean('showGo', true)} + showGoOnFB={boolean('showGoOnFB', true)} + showFilterConfiguration={boolean('showFilterConfiguration', true)} + showSearchOnFiltersDialog={boolean('showSearchOnFiltersDialog', true)} + showClearButton={boolean('showClearButton', true)} + showRestoreButton={boolean('showRestoreButton', true)} + onToggleFilters={action('onToggleFilters')} + onFiltersDialogOpen={action('onFiltersDialogOpen')} + onFiltersDialogClose={action('onFiltersDialogClose')} + onFiltersDialogSave={action('onFiltersDialogSave')} + onFiltersDialogClear={action('onFiltersDialogClear')} + onClear={action('onClear')} + onFiltersDialogSelectionChange={action('onFiltersDialogSelectionChange')} + onFiltersDialogSearch={action('onFiltersDialogSearch')} + onGo={action('onGo')} + onRestore={action('onRestore')} > - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + ); }; -renderStory.storyName = 'Default'; +renderStoryWithFiltersDialog.story = { + name: 'With Filters Dialog' +}; export default { title: 'Components / FilterBar', component: FilterBar, parameters: { - subcomponents: { FilterItem } + subcomponents: { FilterGroupItem } } }; diff --git a/packages/main/src/components/FilterBar/index.tsx b/packages/main/src/components/FilterBar/index.tsx index 4e560de74c9..ca9232dec49 100644 --- a/packages/main/src/components/FilterBar/index.tsx +++ b/packages/main/src/components/FilterBar/index.tsx @@ -1,34 +1,152 @@ +import { createComponentStyles } from '@ui5/webcomponents-react-base/lib/createComponentStyles'; +import { useI18nText } from '@ui5/webcomponents-react-base/lib/hooks'; import { StyleClassHelper } from '@ui5/webcomponents-react-base/lib/StyleClassHelper'; import { usePassThroughHtmlProps } from '@ui5/webcomponents-react-base/lib/usePassThroughHtmlProps'; +import { enrichEventWithDetails } from '@ui5/webcomponents-react-base/lib/Utils'; +import { + CLEAR, + FILTERS, + GO, + HIDE_FILTER_BAR, + RESTORE, + SHOW_FILTER_BAR +} from '@ui5/webcomponents-react/dist/assets/i18n/i18n-defaults'; +import { BusyIndicator } from '@ui5/webcomponents-react/lib/BusyIndicator'; +import { BusyIndicatorSize } from '@ui5/webcomponents-react/lib/BusyIndicatorSize'; import { Button } from '@ui5/webcomponents-react/lib/Button'; import { ButtonDesign } from '@ui5/webcomponents-react/lib/ButtonDesign'; -import React, { FC, forwardRef, ReactNode, ReactNodeArray, RefObject, useCallback, useState } from 'react'; -import { createComponentStyles } from '@ui5/webcomponents-react-base/lib/createComponentStyles'; +import React, { + Children, + cloneElement, + CSSProperties, + FC, + forwardRef, + ReactElement, + ReactNode, + ReactNodeArray, + RefObject, + useCallback, + useEffect, + useRef, + useState +} from 'react'; import { CommonProps } from '../../interfaces/CommonProps'; import styles from './FilterBar.jss'; +import { FilterDialog } from './FilterDialog'; +import { filterValue, renderSearchWithValue } from './utils'; export interface FilterBarPropTypes extends CommonProps { - variants?: ReactNode; - search?: ReactNode; children: ReactNode | ReactNodeArray; + search?: ReactNode; + variants?: ReactNode; + useToolbar?: boolean; + filterBarExpanded?: boolean; + filterContainerWidth?: CSSProperties['width']; + considerGroupName?: boolean; + showClearOnFB?: boolean; + showGoOnFB?: boolean; + showFilterConfiguration?: boolean; + showClearButton?: boolean; + showRestoreButton?: boolean; + showGo?: boolean; + activeFiltersCount?: number | string; + loading?: boolean; + showSearchOnFiltersDialog?: boolean; + showRestoreOnFB?: boolean; + onToggleFilters?: (event: CustomEvent<{ visible?: boolean }>) => void; + onFiltersDialogSave?: (event: CustomEvent<{ elements?: unknown; toggledElements?: unknown }>) => void; + onFiltersDialogClear?: (event: CustomEvent) => void; + onFiltersDialogCancel?: (event: CustomEvent) => void; + onFiltersDialogOpen?: (event: CustomEvent) => void; + onFiltersDialogClose?: (event: CustomEvent) => void; + onFiltersDialogSelectionChange?: (event: CustomEvent<{ element?: unknown; checked?: unknown }>) => void; + onFiltersDialogSearch?: (event: CustomEvent<{ value?: unknown }>) => void; + onClear?: (event: CustomEvent) => void; + onGo?: (event: CustomEvent) => void; + onRestore?: (event: CustomEvent<{ source?: unknown }>) => void; } -interface FilterBarInternalProps extends FilterBarPropTypes {} - const useStyles = createComponentStyles(styles, { name: 'FilterBar' }); /** * import { FilterBar } from '@ui5/webcomponents-react/lib/FilterBar'; */ const FilterBar: FC = forwardRef((props: FilterBarPropTypes, ref: RefObject) => { - const { children, search, variants, className, style, tooltip, slot } = props as FilterBarInternalProps; - const [showFilters, setShowFilters] = useState(true); + const { + children, + useToolbar, + loading, + filterBarExpanded, + considerGroupName, + filterContainerWidth, + activeFiltersCount, + showClearOnFB, + showGoOnFB, + showGo, + showFilterConfiguration, + showRestoreOnFB, + showClearButton, + showRestoreButton, + showSearchOnFiltersDialog, + style, + className, + tooltip, + slot, + search, + variants, - const classes = useStyles(); + onToggleFilters, + onFiltersDialogOpen, + onFiltersDialogCancel, + onFiltersDialogClose, + onFiltersDialogSave, + onFiltersDialogClear, + onClear, + onFiltersDialogSelectionChange, + onFiltersDialogSearch, + onGo, + onRestore + } = props; + const [showFilters, setShowFilters] = useState(useToolbar ? filterBarExpanded : true); + const [mountFilters, setMountFilters] = useState(true); + const [dialogOpen, setDialogOpen] = useState(false); + const [searchValue, setSearchValue] = useState(''); + const searchRef = useRef(null); + const filterRefs = useRef({}); + const [dialogRefs, setDialogRefs] = useState({}); + const [toggledFilters, setToggledFilters] = useState({}); + const prevVisibleInFilterBarProps = useRef({}); - const handleHideFilterBar = useCallback(() => { - setShowFilters(!showFilters); - }, [showFilters]); + const [clearText, restoreText, showFilterBarText, hideFilterBarText, goText, filtersText] = useI18nText( + '@ui5/webcomponents-react', + CLEAR, + RESTORE, + SHOW_FILTER_BAR, + HIDE_FILTER_BAR, + GO, + FILTERS + ); + + useEffect(() => { + if (showFilterConfiguration) { + Children.toArray(children).forEach((item: ReactElement) => { + if ( + prevVisibleInFilterBarProps.current?.[item.key] !== undefined && + prevVisibleInFilterBarProps.current?.[item.key] !== item.props.visibleInFilterBar + ) { + const updatedToggledFilters = toggledFilters; + delete updatedToggledFilters[item.key]; + setToggledFilters(updatedToggledFilters); + } + }); + } + }, [children, prevVisibleInFilterBarProps, setToggledFilters, toggledFilters, showFilterConfiguration]); + + useEffect(() => { + setShowFilters(useToolbar ? filterBarExpanded : true); + }, [setShowFilters, useToolbar, filterBarExpanded]); + + const classes = useStyles(); const filterAreaClasses = StyleClassHelper.of(classes.filterArea); if (showFilters) { @@ -37,35 +155,257 @@ const FilterBar: FC = forwardRef((props: FilterBarPropTypes, filterAreaClasses.put(classes.filterAreaClosed); } - const passThroughProps = usePassThroughHtmlProps(props); + const handleToggle = useCallback( + (e) => { + if (onToggleFilters) { + onToggleFilters(enrichEventWithDetails(e, { visible: !showFilters })); + } + setShowFilters(!showFilters); + }, + [showFilters, onToggleFilters, setShowFilters] + ); + + const handleDialogSave = useCallback( + (e, dialogRefs, updatedToggledFilters) => { + setDialogRefs(dialogRefs); + setToggledFilters({ ...toggledFilters, ...updatedToggledFilters }); + if (onFiltersDialogSave) { + onFiltersDialogSave(enrichEventWithDetails(e)); + } + handleDialogClose(e); + }, + [setDialogOpen, setDialogRefs, setToggledFilters, onFiltersDialogSave, toggledFilters] + ); - const filterBarClasses = StyleClassHelper.of(classes.outerContainer); + const handleDialogOpen = useCallback( + (e) => { + setDialogOpen(true); + if (onFiltersDialogOpen) { + onFiltersDialogOpen(enrichEventWithDetails(e)); + } + }, + [setDialogOpen, onFiltersDialogOpen] + ); + + const handleDialogClose = useCallback( + (e) => { + if (onFiltersDialogClose) { + onFiltersDialogClose(enrichEventWithDetails(e)); + } + setDialogOpen(false); + }, + [setDialogOpen, onFiltersDialogClose] + ); + + const passThroughProps = usePassThroughHtmlProps(props, [ + 'onToggleFilters', + 'onFiltersDialogOpen', + 'onFiltersDialogClose', + 'onFiltersDialogSave', + 'onFiltersDialogClear', + 'onClear', + 'onFiltersDialogSelectionChange', + 'onFiltersDialogSearch', + 'onGo', + 'onRestore', + 'onFiltersDialogCancel' + ]); + + const safeChildren = useCallback(() => { + if (showFilterConfiguration && Object.keys(toggledFilters).length > 0) { + return Children.toArray(children).map((child: ReactElement) => { + if (toggledFilters?.[child.key] !== undefined) { + return cloneElement(child, { + visibleInFilterBar: toggledFilters[child.key] + }); + } + return child; + }); + } + return Children.toArray(children) as any; + }, [toggledFilters, children]); + + const renderChildren = useCallback(() => { + let childProps = { considerGroupName: considerGroupName, inFB: true } as any; + return safeChildren() + .filter((item) => { + if (item.type.displayName !== 'FilterGroupItem') return true; //needed for deprecated FilterItem or custom elements + + return !!item?.props && item?.props.visible && item.props?.visibleInFilterBar; + }) + .map((child) => { + if (child.type.displayName !== 'FilterGroupItem') return child; //needed for deprecated FilterItem or custom elements + if (filterContainerWidth) { + childProps.style = { width: filterContainerWidth, ...child.props.style }; + } + if (!showFilterConfiguration) { + return cloneElement(child as ReactElement, { ...childProps }); + } + prevVisibleInFilterBarProps.current[child.key] = child.props.visibleInFilterBar; + let filterItemProps = {}; + if (Object.keys(dialogRefs).length > 0) { + const dialogItemRef = dialogRefs[child.key]; + if (dialogItemRef) { + filterItemProps = filterValue(dialogItemRef, child); + } + } + if (!child.props.children) + return cloneElement(child as ReactElement, { + ...childProps + }); + return cloneElement(child as ReactElement, { + ...childProps, + children: { + ...child.props.children, + props: { + ...child.props.children.props, + ...filterItemProps + }, + ref: (node) => { + filterRefs.current[child.key] = node; + } + } + }); + }); + }, [filterContainerWidth, considerGroupName, dialogRefs, safeChildren, showFilterConfiguration]); + + const handleSearchValueChange = useCallback( + (newVal) => { + setSearchValue(newVal); + }, + [setSearchValue] + ); + + const handleRestoreFilters = useCallback( + (e, source) => { + if (source === 'dialog' && showGo) { + setDialogOpen(false); + setDialogOpen(true); + } else if (source === 'filterBar' && showGoOnFB) { + setMountFilters(false); + setMountFilters(true); + } + if (onRestore) { + onRestore(enrichEventWithDetails(e, { source })); + } + }, + [setDialogOpen, showGo, showGoOnFB, onRestore] + ); + + const handleFBRestore = useCallback( + (e) => { + handleRestoreFilters(e, 'filterBar'); + }, + [handleRestoreFilters] + ); + + const cssClasses = StyleClassHelper.of(classes.outerContainer); if (className) { - filterBarClasses.put(className); + cssClasses.put(className); } return ( -
-
- {variants} - {search &&
{search}
} -
- -
+ <> + {dialogOpen && showFilterConfiguration && ( + + {safeChildren()} + + )} +
+ {loading ? ( + + ) : ( + <> +
+ {variants} + {search && ( +
+ {renderSearchWithValue(search, searchValue)} +
+ )} + {useToolbar && ( +
+ {showClearOnFB && ( + + )} + {showRestoreOnFB && ( + + )} + + {showFilterConfiguration && ( + + )} + {showGoOnFB && ( + + )} +
+ )} +
+ {mountFilters &&
{renderChildren()}
} + + )}
-
{children}
-
+ ); }); +FilterBar.defaultProps = { + useToolbar: true, + filterBarExpanded: true, + showClearOnFB: false, + showGo: false, + showRestoreOnFB: false, + showGoOnFB: false, + showFilterConfiguration: false, + showClearButton: false, + showRestoreButton: false, + showSearchOnFiltersDialog: false, + considerGroupName: false, + loading: false, + onToggleFilters: null, + onFiltersDialogOpen: null, + onFiltersDialogCancel: null, + onFiltersDialogClose: null, + onFiltersDialogSave: null, + onFiltersDialogClear: null, + onClear: null, + onFiltersDialogSelectionChange: null, + onFiltersDialogSearch: null, + onGo: null, + onRestore: null +}; + FilterBar.displayName = 'FilterBar'; export { FilterBar }; diff --git a/packages/main/src/components/FilterBar/utils.tsx b/packages/main/src/components/FilterBar/utils.tsx new file mode 100644 index 00000000000..c8e444574e4 --- /dev/null +++ b/packages/main/src/components/FilterBar/utils.tsx @@ -0,0 +1,38 @@ +import { cloneElement } from 'react'; + +export const filterValue = (ref, child) => { + const tagName = ref.tagName; + let filterItemProps = {}; + if ( + tagName === 'UI5-INPUT' || + tagName === 'UI5-DATEPICKER' || + tagName === 'UI5-DATETIME-PICKER' || + tagName === 'UI5-DURATION-PICKER' + ) { + filterItemProps = { value: ref.value }; + } + if (tagName === 'UI5-COMBOBOX') { + filterItemProps = { value: ref.value, filterValue: ref.filterValue }; + } + if (tagName === 'UI5-SELECT' || tagName === 'UI5-MULTI-COMBOBOX') { + const selectedIndices = Array.from(ref.children as HTMLCollectionOf) + .map((item, index) => (item.selected ? index : false)) + .filter((el) => el !== false); + const selectedIndicesSet = new Set(selectedIndices); + const options = child.props.children.props.children?.map((item, index) => { + if (selectedIndicesSet.has(index)) { + return cloneElement(item, { selected: true }); + } + return cloneElement(item, { selected: false }); + }); + filterItemProps = { children: options }; + } + if (tagName === 'UI5-SWITCH' || tagName === 'UI5-CHECKBOX') { + filterItemProps = { checked: ref.checked }; + } + return filterItemProps; +}; + +export const renderSearchWithValue = (renderSearchElement, searchValue) => { + return cloneElement(renderSearchElement, { value: searchValue }); +}; diff --git a/packages/main/src/components/FilterGroupItem/FilterGroupItem.jss.ts b/packages/main/src/components/FilterGroupItem/FilterGroupItem.jss.ts new file mode 100644 index 00000000000..7a3688639ab --- /dev/null +++ b/packages/main/src/components/FilterGroupItem/FilterGroupItem.jss.ts @@ -0,0 +1,45 @@ +const styles = { + filterItem: { + width: '13rem', + marginRight: '1rem', + marginBottom: '1rem' + }, + filterItemDialog: { + flexGrow: 1, + overflow: 'hidden' + }, + innerFilterItemContainer: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'start' + }, + innerFilterItemContainerDialog: { + display: 'grid', + gridTemplateColumns: '20% calc(80% - 1rem)', + '@media(max-width:700px)': { + gridTemplateColumns: '100%' + }, + gridTemplateRows: 'auto', + gridRowGap: '0px', + gridColumnGap: '1rem', + '& :first-child': { + maxWidth: '100%', + placeSelf: 'center end', + '@media(max-width:700px)': { + placeSelf: 'center start' + } + }, + '& :last-child': { + placeSelf: 'center auto', + width: '100%' + } + }, + loadingContainer: { + display: 'flex', + width: '100%', + height: '1.625rem', + justifyContent: 'center' + } +}; + +export default styles; diff --git a/packages/main/src/components/FilterGroupItem/index.tsx b/packages/main/src/components/FilterGroupItem/index.tsx new file mode 100644 index 00000000000..0e0f6e47c7f --- /dev/null +++ b/packages/main/src/components/FilterGroupItem/index.tsx @@ -0,0 +1,91 @@ +import { createComponentStyles } from '@ui5/webcomponents-react-base/lib/createComponentStyles'; +import { StyleClassHelper } from '@ui5/webcomponents-react-base/lib/StyleClassHelper'; +import { usePassThroughHtmlProps } from '@ui5/webcomponents-react-base/lib/usePassThroughHtmlProps'; +import { BusyIndicator } from '@ui5/webcomponents-react/lib/BusyIndicator'; +import { BusyIndicatorSize } from '@ui5/webcomponents-react/lib/BusyIndicatorSize'; +import { FlexBox } from '@ui5/webcomponents-react/lib/FlexBox'; +import { Label } from '@ui5/webcomponents-react/lib/Label'; +import React, { FC, forwardRef, ReactElement, RefObject } from 'react'; +import { CommonProps } from '../../interfaces/CommonProps'; +import styles from './FilterGroupItem.jss'; + +const useStyles = createComponentStyles(styles, { name: 'FilterGroupItem' }); + +const emptyObject = {}; + +export interface FilterGroupItemPropTypes extends CommonProps { + children: ReactElement; + label?: string; + groupName?: string; + labelTooltip?: string; + loading?: boolean; + required?: boolean; + visible?: boolean; + visibleInFilterBar?: boolean; + considerGroupName?: boolean; +} + +export const FilterGroupItem: FC = forwardRef( + (props: FilterGroupItemPropTypes, ref: RefObject) => { + const classes = useStyles(); + const { + groupName, + considerGroupName, + label, + labelTooltip, + required, + visible, + visibleInFilterBar, + children, + style, + loading, + className, + tooltip, + slot, + // @ts-ignore + inFB + } = props; + + const passThroughProps = usePassThroughHtmlProps(props); + + const styleClasses = StyleClassHelper.of(inFB ? classes.filterItem : classes.filterItemDialog); + if (className) { + styleClasses.put(className); + } + + if (!required && (!visible || (inFB && !visibleInFilterBar))) return null; + return ( +
+
+ + + + {loading ? ( + + ) : ( + children + )} +
+
+ ); + } +); + +FilterGroupItem.displayName = 'FilterGroupItem'; + +FilterGroupItem.defaultProps = { + groupName: 'default', + visible: true, + visibleInFilterBar: true, + required: false +}; diff --git a/packages/main/src/components/FilterItem/index.tsx b/packages/main/src/components/FilterItem/index.tsx index d79132c184a..a78bad6eb34 100644 --- a/packages/main/src/components/FilterItem/index.tsx +++ b/packages/main/src/components/FilterItem/index.tsx @@ -1,4 +1,6 @@ +import { deprecationNotice } from '@ui5/webcomponents-react-base/lib/Utils'; import { enrichEventWithDetails } from '@ui5/webcomponents-react-base/lib/Utils'; +import { createComponentStyles } from '@ui5/webcomponents-react-base/lib/createComponentStyles'; import { StyleClassHelper } from '@ui5/webcomponents-react-base/lib/StyleClassHelper'; import { usePassThroughHtmlProps } from '@ui5/webcomponents-react-base/lib/usePassThroughHtmlProps'; import { BusyIndicator } from '@ui5/webcomponents-react/lib/BusyIndicator'; @@ -10,8 +12,7 @@ import { MultiComboBox } from '@ui5/webcomponents-react/lib/MultiComboBox'; import { Option } from '@ui5/webcomponents-react/lib/Option'; import { Select } from '@ui5/webcomponents-react/lib/Select'; import { StandardListItem } from '@ui5/webcomponents-react/lib/StandardListItem'; -import React, { FC, forwardRef, ReactNode, RefObject, useMemo } from 'react'; -import { createComponentStyles } from '@ui5/webcomponents-react-base/lib/createComponentStyles'; +import React, { FC, forwardRef, ReactNode, RefObject, useEffect, useMemo } from 'react'; import { CommonProps } from '../../interfaces/CommonProps'; import styles from './FilterItem.jss'; @@ -48,6 +49,13 @@ const FilterItem: FC = forwardRef((props: FilterItemPropTyp } = props as FilterItemPropTypes; const classes = useStyles(); + useEffect(() => { + deprecationNotice( + 'FilterItem', + "'@ui5/webcomponents-react/lib/FilterItem' is deprecated and will be removed in the next major release.\nPlease use '@ui5/webcomponents-react/lib/FilterGroupItem' instead." + ); + }, []); + function getItemByKey(key) { return filterItems.filter((item) => item.key === key)[0]; } diff --git a/packages/main/src/components/Loader/index.tsx b/packages/main/src/components/Loader/index.tsx index 137b31055cb..698b69f2744 100644 --- a/packages/main/src/components/Loader/index.tsx +++ b/packages/main/src/components/Loader/index.tsx @@ -1,7 +1,7 @@ import { createComponentStyles } from '@ui5/webcomponents-react-base/lib/createComponentStyles'; -import { useI18nBundle } from '@ui5/webcomponents-react-base/lib/hooks'; import { StyleClassHelper } from '@ui5/webcomponents-react-base/lib/StyleClassHelper'; import { usePassThroughHtmlProps } from '@ui5/webcomponents-react-base/lib/usePassThroughHtmlProps'; +import { useI18nText } from '@ui5/webcomponents-react-base/lib/hooks'; import { PLEASE_WAIT } from '@ui5/webcomponents-react/dist/assets/i18n/i18n-defaults'; import { LoaderType } from '@ui5/webcomponents-react/lib/LoaderType'; import React, { CSSProperties, FC, forwardRef, RefObject, useEffect, useMemo, useState } from 'react'; @@ -55,7 +55,7 @@ const Loader: FC = forwardRef((props: LoaderProps, ref: RefObject = forwardRef((props: LoaderProps, ref: RefObject*:not(:last-child)': { - marginRight: '0.5rem' + '& > *': { + margin: '0 0.25rem' }, '& > ui5-button': { display: 'flex' diff --git a/packages/main/src/components/MessageBox/__snapshots__/MessageBox.test.tsx.snap b/packages/main/src/components/MessageBox/__snapshots__/MessageBox.test.tsx.snap index ee625d26bba..9b3a7e6a30a 100644 --- a/packages/main/src/components/MessageBox/__snapshots__/MessageBox.test.tsx.snap +++ b/packages/main/src/components/MessageBox/__snapshots__/MessageBox.test.tsx.snap @@ -9,13 +9,9 @@ exports[`MessageBox Confirm - Cancel 1`] = ` data-type="Confirm" slot="header" > -
- -
+ @@ -58,13 +54,9 @@ exports[`MessageBox Confirm - OK 1`] = ` data-type="Confirm" slot="header" > -
- -
+ @@ -107,13 +99,9 @@ exports[`MessageBox Error 1`] = ` data-type="Error" slot="header" > -
- -
+ @@ -149,13 +137,9 @@ exports[`MessageBox Information 1`] = ` data-type="Information" slot="header" > -
- -
+ @@ -191,13 +175,9 @@ exports[`MessageBox No Title 1`] = ` data-type="Confirm" slot="header" > -
- -
+ @@ -238,13 +218,9 @@ exports[`MessageBox Not open 1`] = ` data-type="Success" slot="header" > -
- -
+ @@ -280,13 +256,9 @@ exports[`MessageBox Show 1`] = ` data-type="Confirm" slot="header" > -
- -
+ @@ -329,13 +301,9 @@ exports[`MessageBox Success 1`] = ` data-type="Success" slot="header" > -
- -
+ @@ -371,13 +339,9 @@ exports[`MessageBox Success w/ custom title 1`] = ` data-type="Success" slot="header" > -
- -
+ @@ -413,13 +377,9 @@ exports[`MessageBox Warning 1`] = ` data-type="Warning" slot="header" > -
- -
+ diff --git a/packages/main/src/components/MessageBox/index.tsx b/packages/main/src/components/MessageBox/index.tsx index 048f2de10f7..77112646bff 100644 --- a/packages/main/src/components/MessageBox/index.tsx +++ b/packages/main/src/components/MessageBox/index.tsx @@ -5,7 +5,7 @@ import '@ui5/webcomponents-icons/dist/icons/message-success'; import '@ui5/webcomponents-icons/dist/icons/message-warning'; import '@ui5/webcomponents-icons/dist/icons/question-mark'; import { createComponentStyles } from '@ui5/webcomponents-react-base/lib/createComponentStyles'; -import { useConsolidatedRef, useI18nBundle, usePassThroughHtmlProps } from '@ui5/webcomponents-react-base/lib/hooks'; +import { useConsolidatedRef, useI18nText, usePassThroughHtmlProps } from '@ui5/webcomponents-react-base/lib/hooks'; import { enrichEventWithDetails } from '@ui5/webcomponents-react-base/lib/Utils'; import { @@ -40,15 +40,15 @@ import { Ui5DialogDomRef } from '../../interfaces/Ui5DialogDomRef'; import styles from './MessageBox.jss'; const actionTextMap = new Map(); -actionTextMap.set(MessageBoxActions.ABORT, ABORT); -actionTextMap.set(MessageBoxActions.CANCEL, CANCEL); -actionTextMap.set(MessageBoxActions.CLOSE, CLOSE); -actionTextMap.set(MessageBoxActions.DELETE, DELETE); -actionTextMap.set(MessageBoxActions.IGNORE, IGNORE); -actionTextMap.set(MessageBoxActions.NO, NO); -actionTextMap.set(MessageBoxActions.OK, OK); -actionTextMap.set(MessageBoxActions.RETRY, RETRY); -actionTextMap.set(MessageBoxActions.YES, YES); +actionTextMap.set(MessageBoxActions.ABORT, 0); +actionTextMap.set(MessageBoxActions.CANCEL, 1); +actionTextMap.set(MessageBoxActions.CLOSE, 2); +actionTextMap.set(MessageBoxActions.DELETE, 3); +actionTextMap.set(MessageBoxActions.IGNORE, 4); +actionTextMap.set(MessageBoxActions.NO, 5); +actionTextMap.set(MessageBoxActions.OK, 6); +actionTextMap.set(MessageBoxActions.RETRY, 7); +actionTextMap.set(MessageBoxActions.YES, 8); export interface MessageBoxPropTypes extends CommonProps { open?: boolean; @@ -68,8 +68,6 @@ const useStyles = createComponentStyles(styles, { name: 'MessageBox' }); const MessageBox: FC = forwardRef((props: MessageBoxPropTypes, ref: Ref) => { const { open, type, children, className, style, tooltip, slot, title, icon, actions, onClose } = props; - const i18nBundle = useI18nBundle('@ui5/webcomponents-react'); - const classes = useStyles(); const iconToRender = useMemo(() => { @@ -92,26 +90,53 @@ const MessageBox: FC = forwardRef((props: MessageBoxPropTyp return null; }, [icon, type]); - const titleToRender = useMemo(() => { + const [ + titleConfirmation, + titleError, + titleInformation, + titleSuccess, + titleWarning, + titleHighlight, + ...actionTranslations + ] = useI18nText( + '@ui5/webcomponents-react', + CONFIRMATION, + ERROR, + INFORMATION, + SUCCESS, + WARNING, + HIGHLIGHT, + ABORT, + CANCEL, + CLOSE, + DELETE, + IGNORE, + NO, + OK, + RETRY, + YES + ); + + const titleToRender = () => { if (title) { return title; } switch (type) { case MessageBoxTypes.CONFIRM: - return i18nBundle.getText(CONFIRMATION); + return titleConfirmation; case MessageBoxTypes.ERROR: - return i18nBundle.getText(ERROR); + return titleError; case MessageBoxTypes.INFORMATION: - return i18nBundle.getText(INFORMATION); + return titleInformation; case MessageBoxTypes.SUCCESS: - return i18nBundle.getText(SUCCESS); + return titleSuccess; case MessageBoxTypes.WARNING: - return i18nBundle.getText(WARNING); + return titleWarning; case MessageBoxTypes.HIGHLIGHT: - return i18nBundle.getText(HIGHLIGHT); + return titleHighlight; } return null; - }, [title, type, i18nBundle]); + }; const actionsToRender = useMemo(() => { if (actions && actions.length > 0) { @@ -158,14 +183,13 @@ const MessageBox: FC = forwardRef((props: MessageBoxPropTyp onAfterClose={open ? handleOnClose : null} header={
- {!!iconToRender &&
{iconToRender}
} - {titleToRender} + {iconToRender} + {titleToRender()}
} footer={