diff --git a/.github/workflows/qunit_tests.yml b/.github/workflows/qunit_tests.yml index d913917650b1..9a9dfcea6e33 100644 --- a/.github/workflows/qunit_tests.yml +++ b/.github/workflows/qunit_tests.yml @@ -127,7 +127,7 @@ jobs: qunit-tests: needs: build - runs-on: devextreme-shr2 + runs-on: ${{ github.event_name == 'merge_group' && 'devextreme-shr2-ondemand' || 'devextreme-shr2' }} name: Constel ${{ matrix.CONSTEL }}${{ matrix.TIMEZONE != '' && format('-{0}', matrix.TIMEZONE) || '' }}${{ matrix.CSP == 'false' && '-no-csp' || '' }} timeout-minutes: 25 strategy: diff --git a/.github/workflows/repository-check.yml b/.github/workflows/repository-check.yml index 095674fa2d4e..2eae3e61ae00 100644 --- a/.github/workflows/repository-check.yml +++ b/.github/workflows/repository-check.yml @@ -8,9 +8,95 @@ on: branches: - main +permissions: {} + jobs: + collect-flaky-reports: + name: Collect flaky reports + runs-on: ubuntu-slim + permissions: + contents: read # checkout + actions: read # list workflow runs and download their artifacts + # Flaky collection must never break the repository check it feeds. + continue-on-error: true + outputs: + details: ${{ steps.collect.outputs.details }} + + steps: + - name: Get sources + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + + - name: Use Node.js + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 + with: + node-version-file: '.node-version' + + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + with: + run_install: false + + - name: Install dependencies + run: pnpm install --frozen-lockfile --filter flaky-test-reporter... + + - name: Collect flaky test candidates + id: collect + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + WINDOW_HOURS: 72 + run: | + # tee opens the log before the tool runs, so the directory must already exist. + mkdir -p "$GITHUB_WORKSPACE/artifacts" + report="$GITHUB_WORKSPACE/artifacts/flaky-tests-report.json" + markdown="$GITHUB_WORKSPACE/artifacts/flaky-tests-report.md" + log="$GITHUB_WORKSPACE/artifacts/collect.log" + + # --silent keeps pnpm's own banner out of the summary; 2>&1 keeps the tool's warnings in. + # tee stores the plain text for the summary, then sed turns the tool's warnings into + # GitHub annotations so they surface on the run page. + pnpm --silent --filter flaky-test-reporter run collect -- \ + --repo "${{ github.repository }}" \ + --workflow testcafe_tests.yml \ + --artifact flaky-candidates \ + --branch "${{ github.ref_name }}" \ + --window-hours "$WINDOW_HOURS" \ + --out-json "$report" \ + --out-markdown "$markdown" 2>&1 \ + | tee "$log" \ + | sed -e 's/^warning: /::warning::/' -e 's/^Collection failed: /::error::/' + + { + echo "## Flaky test candidates" + echo "" + echo '```' + cat "$log" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + # Multiline output form, so the markdown crosses the job boundary as-is. The delimiter + # must not occur in the content, which markdown headings and tables never produce. + { + echo 'details<> "$GITHUB_OUTPUT" + + - name: Upload flaky test report + if: ${{ !cancelled() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: flaky-tests-report + path: | + ${{ github.workspace }}/artifacts/flaky-tests-report.json + ${{ github.workspace }}/artifacts/collect.log + retention-days: 7 + if-no-files-found: ignore + invoke: name: Invoke + needs: [collect-flaky-reports] + permissions: + contents: read # the reusable workflow checks this repository out + if: ${{ !cancelled() }} uses: DevExpress/github-actions/.github/workflows/repository-check.yml@repository-check with: config: | @@ -20,3 +106,6 @@ jobs: 'GHSA-848j-6mx2-7j84', // elliptic ] } + # Markdown appended to this repo's monitor issue body. Empty when the collect job + # produced nothing, which makes the shared workflow skip publishing it. + extraDetails: ${{ needs.collect-flaky-reports.outputs.details }} diff --git a/.github/workflows/testcafe_tests.yml b/.github/workflows/testcafe_tests.yml index 1c7167fab625..432134804233 100644 --- a/.github/workflows/testcafe_tests.yml +++ b/.github/workflows/testcafe_tests.yml @@ -470,8 +470,15 @@ jobs: quarantined_failed=0 result=./artifacts/quarantine-run-result.json new_flakes_summary=./artifacts/new-flakes-summary.txt + new_flakes=./artifacts/flaky-candidates.jsonl + report_json=./artifacts/flaky-candidates.json + # base_ref is empty for push and merge_group, which carry the target elsewhere. + base_branch="${{ github.base_ref || github.event.merge_group.base_ref || github.ref_name }}" + base_branch="${base_branch#refs/heads/}" mkdir -p ./artifacts : > "$new_flakes_summary" + : > "$new_flakes" + rm -f "$report_json" # Every test gets up to 3 attempts (initial run + 2 retries) and passes # if at least one attempt succeeds. A reported failure means all attempts failed. @@ -549,12 +556,23 @@ jobs: while IFS= read -r name; do if jq -e --arg n "$name" '.tests | index($n)' "$result" > /dev/null; then echo "FAILED | $name | $job_name" >> "$new_flakes_summary" + verdict=failed else echo "passed | $name | $job_name" >> "$new_flakes_summary" + verdict=passed fi + jq -cn --arg test "$name" --arg sourceJob "$job_name" --arg theme "$theme" \ + --arg componentFolder "$componentFolder" --arg timezone "$timezone" \ + --arg platform "$platform" --arg verdict "$verdict" --arg baseBranch "$base_branch" \ + '{ test: $test, sourceJob: $sourceJob, theme: $theme, componentFolder: $componentFolder, timezone: $timezone, platform: $platform, verdict: $verdict, baseBranch: $baseBranch }' \ + >> "$new_flakes" done < <(jq -r '.tests[]' "$report") done + if [ -s "$new_flakes" ]; then + jq -s '{ schemaVersion: 1, candidates: . }' "$new_flakes" > "$report_json" + fi + sudo ln -sf /usr/share/zoneinfo/GMT /etc/localtime echo "" @@ -631,6 +649,15 @@ jobs: path: ${{ github.workspace }}/e2e/testcafe-devextreme/artifacts/failedtests/**/* if-no-files-found: ignore + - name: Upload flaky candidates report + if: ${{ !cancelled() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: flaky-candidates + path: ${{ github.workspace }}/e2e/testcafe-devextreme/artifacts/flaky-candidates.json + retention-days: 7 + if-no-files-found: ignore + merge-artifacts: runs-on: ubuntu-latest needs: [testcafe, testcafe-quarantine] diff --git a/.github/workflows/visual-tests-demos.yml b/.github/workflows/visual-tests-demos.yml index 8a5f3107baf4..6bf84c43dd7c 100644 --- a/.github/workflows/visual-tests-demos.yml +++ b/.github/workflows/visual-tests-demos.yml @@ -26,130 +26,13 @@ jobs: id: check run: echo "should-run=${{ env.RUN_TESTS }}" >> $GITHUB_OUTPUT - get-changes: - runs-on: ubuntu-latest - needs: check-should-run - if: github.event_name == 'pull_request' && needs.check-should-run.outputs.should-run == 'true' - name: Get changed demos - timeout-minutes: 5 - outputs: - has-changed-demos: ${{ steps.check-changes.outputs.has-changed-demos }} - - steps: - - name: Get sources - uses: actions/checkout@v6 - - - name: Get changed files - uses: DevExpress/github-actions/get-changed-files@get-changed-files - with: - gh-token: ${{ secrets.GITHUB_TOKEN }} - paths: 'apps/demos/Demos/**/*' - output: apps/demos/changed-files.json - - - name: Display changed files - id: check-changes - run: | - HAS_CHANGED="false" - if [ -f "apps/demos/changed-files.json" ]; then - DEMO_COUNT=$(jq length apps/demos/changed-files.json) - echo "Found changed-files.json" - echo "Content of changed-files.json:" - cat apps/demos/changed-files.json - echo "Number of changed files: $DEMO_COUNT" - if [ "$DEMO_COUNT" -gt 0 ]; then - HAS_CHANGED="true" - fi - else - echo "changed-files.json not found" - fi - echo "has-changed-demos=${HAS_CHANGED}" >> $GITHUB_OUTPUT - - - name: Upload artifacts - uses: actions/upload-artifact@v7 - with: - name: changed-demos - path: apps/demos/changed-files.json - retention-days: 1 - - determine-framework-tests-scope: - runs-on: ubuntu-slim - name: Determine scope for framework tests - needs: [check-should-run, get-changes] - if: | - !cancelled() && - needs.check-should-run.outputs.should-run == 'true' && - (needs.get-changes.result == 'success' || needs.get-changes.result == 'skipped') - outputs: - framework-tests-scope: ${{ steps.determine.outputs.framework-tests-scope }} - - steps: - - name: Determine framework tests scope - id: determine - run: | - if [ "${{ github.event_name }}" != "pull_request" ] || [ "${{ contains(github.event.pull_request.labels.*.name, 'force all tests') }}" = "true" ]; then - echo "Framework tests scope: all demos" - echo "framework-tests-scope=all" >> $GITHUB_OUTPUT - elif [ "${{ needs.get-changes.outputs.has-changed-demos }}" = "true" ]; then - echo "Framework tests scope: changed demos" - echo "framework-tests-scope=changed" >> $GITHUB_OUTPUT - else - echo "Framework tests NOT needed" - echo "framework-tests-scope=none" >> $GITHUB_OUTPUT - fi - - determine-jquery-test-matrix: - runs-on: ubuntu-slim - name: Determine jQuery test matrix - needs: [check-should-run, determine-framework-tests-scope] - if: | - !cancelled() && - needs.check-should-run.outputs.should-run == 'true' && - needs.determine-framework-tests-scope.result == 'success' - outputs: - matrix: ${{ steps.matrix.outputs.matrix }} - - steps: - - name: Build matrix - id: matrix - run: | - base_matrix=$(cat <<'JSON' - [ - { "STRATEGY": "screenshots", "THEME": "material.blue.light", "CONSTEL": "jquery(1/3)" }, - { "STRATEGY": "screenshots", "THEME": "material.blue.light", "CONSTEL": "jquery(2/3)" }, - { "STRATEGY": "screenshots", "THEME": "material.blue.light", "CONSTEL": "jquery(3/3)" }, - { "STRATEGY": "screenshots", "THEME": "fluent.blue.light", "CONSTEL": "jquery(1/3)" }, - { "STRATEGY": "screenshots", "THEME": "fluent.blue.light", "CONSTEL": "jquery(2/3)" }, - { "STRATEGY": "screenshots", "THEME": "fluent.blue.light", "CONSTEL": "jquery(3/3)" }, - { "STRATEGY": "accessibility", "THEME": "material.blue.light", "CONSTEL": "jquery" }, - { "STRATEGY": "accessibility", "THEME": "fluent.blue.light", "CONSTEL": "jquery" } - ] - JSON - ) - - dark_accessibility_matrix=$(cat <<'JSON' - [ - { "STRATEGY": "accessibility", "THEME": "material.blue.dark", "CONSTEL": "jquery" }, - { "STRATEGY": "accessibility", "THEME": "fluent.blue.dark", "CONSTEL": "jquery" } - ] - JSON - ) - - matrix=$(jq -c -n \ - --argjson base "$base_matrix" \ - --argjson darkAccessibility "$dark_accessibility_matrix" \ - --arg frameworkTestsScope "${{ needs.determine-framework-tests-scope.outputs.framework-tests-scope }}" \ - '{ include: ($base + (if $frameworkTestsScope != "none" then $darkAccessibility else [] end)) }') - - echo "matrix=$matrix" >> $GITHUB_OUTPUT - build-devextreme: runs-on: ${{ github.event_name == 'merge_group' && 'devextreme-shr2-ondemand' || 'devextreme-shr2' }} name: Build DevExtreme - needs: [check-should-run, determine-framework-tests-scope] + needs: [check-should-run] if: | !cancelled() && - needs.check-should-run.outputs.should-run == 'true' && - needs.determine-framework-tests-scope.result == 'success' + needs.check-should-run.outputs.should-run == 'true' env: NODE_OPTIONS: --max-old-space-size=8192 timeout-minutes: 30 @@ -228,15 +111,7 @@ jobs: ${{ runner.os }}-nx-v2-${{ github.workflow }}-${{ github.job }}- ${{ runner.os }}-nx-v2- - - name: DevExtreme - Build - if: needs.determine-framework-tests-scope.outputs.framework-tests-scope == 'none' - shell: bash - run: | - pnpm exec nx build devextreme-scss - pnpm exec nx build devextreme -c testing - - name: DevExtreme - Build-all - if: needs.determine-framework-tests-scope.outputs.framework-tests-scope != 'none' run: pnpm nx all:build-testing workflows - name: Zip artifacts (for jQuery tests) @@ -252,7 +127,6 @@ jobs: retention-days: 1 - name: Move packages - if: needs.determine-framework-tests-scope.outputs.framework-tests-scope != 'none' run: | mv ./packages/devextreme/artifacts/npm/devextreme/*.tgz ./devextreme-installer.tgz mv ./packages/devextreme/artifacts/npm/devextreme-dist/*.tgz ./devextreme-dist-installer.tgz @@ -261,7 +135,6 @@ jobs: mv ./packages/devextreme-vue/npm/*.tgz ./devextreme-vue-installer.tgz - name: Copy build artifacts - if: needs.determine-framework-tests-scope.outputs.framework-tests-scope != 'none' uses: actions/upload-artifact@v7 with: name: devextreme-sources @@ -276,12 +149,10 @@ jobs: name: Build vendor bundles (React, Vue) runs-on: ${{ github.event_name == 'merge_group' && 'devextreme-shr2-ondemand' || 'devextreme-shr2' }} timeout-minutes: 15 - needs: [check-should-run, determine-framework-tests-scope, build-devextreme] + needs: [check-should-run, build-devextreme] if: | !cancelled() && needs.check-should-run.outputs.should-run == 'true' && - needs.determine-framework-tests-scope.result == 'success' && - needs.determine-framework-tests-scope.outputs.framework-tests-scope != 'none' && needs.build-devextreme.result == 'success' env: NODE_OPTIONS: --max-old-space-size=8192 @@ -337,12 +208,10 @@ jobs: name: ${{ matrix.SHARD_TOTAL == 1 && format('Build demos ({0})', matrix.FRAMEWORK) || format('Build demos ({0} {1}/{2})', matrix.FRAMEWORK, matrix.SHARD_INDEX, matrix.SHARD_TOTAL) }} runs-on: ${{ github.event_name == 'merge_group' && 'devextreme-shr2-ondemand' || 'devextreme-shr2' }} timeout-minutes: 30 - needs: [check-should-run, determine-framework-tests-scope, build-devextreme, build-vendor-bundles] + needs: [check-should-run, build-devextreme, build-vendor-bundles] if: | !cancelled() && needs.check-should-run.outputs.should-run == 'true' && - needs.determine-framework-tests-scope.result == 'success' && - needs.determine-framework-tests-scope.outputs.framework-tests-scope != 'none' && needs.build-devextreme.result == 'success' && needs.build-vendor-bundles.result == 'success' env: @@ -437,12 +306,10 @@ jobs: lint: name: Check TS and lint code base - needs: [check-should-run, determine-framework-tests-scope, build-devextreme] + needs: [check-should-run, build-devextreme] if: | !cancelled() && needs.check-should-run.outputs.should-run == 'true' && - needs.determine-framework-tests-scope.result == 'success' && - needs.determine-framework-tests-scope.outputs.framework-tests-scope != 'none' && needs.build-devextreme.result == 'success' runs-on: ubuntu-latest @@ -452,14 +319,6 @@ jobs: - name: Get sources uses: actions/checkout@v6 - - name: Download artifacts - if: github.event_name == 'pull_request' - uses: actions/download-artifact@v8 - with: - name: changed-demos - path: apps/demos - continue-on-error: true - - name: Download devextreme sources uses: actions/download-artifact@v8 with: @@ -512,7 +371,6 @@ jobs: - name: Run lint on all demos id: lint_all - if: needs.determine-framework-tests-scope.outputs.framework-tests-scope == 'all' continue-on-error: true working-directory: apps/demos env: @@ -520,51 +378,19 @@ jobs: NODE_OPTIONS: --max-old-space-size=8192 run: pnpm exec nx lint - - name: Run lint on changed demos - id: lint_changed - if: needs.determine-framework-tests-scope.outputs.framework-tests-scope == 'changed' - continue-on-error: true - working-directory: apps/demos - env: - DEBUG: 'eslint:cli-engine,stylelint:standalone' - run: | - pnpm exec nx lint-non-demos - - if [ -f "changed-files.json" ]; then - echo "Running lint-demos on changed files" - CHANGED_DEMOS=$(jq -r '.[].filename' changed-files.json \ - | grep '^apps/demos/Demos/' \ - | sed 's|^apps/demos/||' \ - | while read f; do - [ -f "$f" ] && echo "$f" - done \ - | tr '\n' ' ') - if [ ! -z "$CHANGED_DEMOS" ]; then - echo "Changed demo files: $CHANGED_DEMOS" - pnpm run eslint $CHANGED_DEMOS - else - echo "No demo files changed, skipping lint-demos" - fi - else - echo "changed-files.json not found" - pnpm run lint-demos - fi - - name: Check TS and lint results if: always() env: TS_VUE_OUTCOME: ${{ steps.ts_vue.outcome }} TS_REACT_OUTCOME: ${{ steps.ts_react.outcome }} LINT_ALL_OUTCOME: ${{ steps.lint_all.outcome }} - LINT_CHANGED_OUTCOME: ${{ steps.lint_changed.outcome }} run: | failed=0 for check in \ "Demos - Check Vue TS:$TS_VUE_OUTCOME" \ "Demos - Check React TS:$TS_REACT_OUTCOME" \ - "Run lint on all demos:$LINT_ALL_OUTCOME" \ - "Run lint on changed demos:$LINT_CHANGED_OUTCOME"; do + "Run lint on all demos:$LINT_ALL_OUTCOME"; do name="${check%%:*}" outcome="${check#*:}" @@ -576,124 +402,14 @@ jobs: exit "$failed" - check-generated-demos-changed: - name: Check generated demos (changed only) - runs-on: devextreme-shr2 - timeout-minutes: 20 - needs: [check-should-run, get-changes, build-devextreme, determine-framework-tests-scope] - if: | - !cancelled() && - needs.check-should-run.outputs.should-run == 'true' && - needs.determine-framework-tests-scope.result == 'success' && - needs.determine-framework-tests-scope.outputs.framework-tests-scope == 'changed' && - needs.build-devextreme.result == 'success' - - steps: - - name: Get sources - uses: actions/checkout@v6 - - - name: Download changed demos - uses: actions/download-artifact@v8 - with: - name: changed-demos - path: apps/demos - continue-on-error: true - - - name: Detect changed React TS demos - id: changed-react-demos - working-directory: apps/demos - run: | - if [ ! -f "changed-files.json" ]; then - echo "changed-files.json not found, skipping generated JS demos check" - echo "has-react-demos=false" >> $GITHUB_OUTPUT - exit 0 - fi - - jq -r '.[].filename' changed-files.json \ - | grep '/React/' \ - | grep -E '\.tsx?$' \ - | sed 's|^apps/demos/||' \ - | sed -E 's|/[^/]*\.tsx?$||' \ - | sort \ - | uniq > changed-react-demos.txt || true - - if [ -s changed-react-demos.txt ]; then - echo "Changed React demos:" - cat changed-react-demos.txt - echo "has-react-demos=true" >> $GITHUB_OUTPUT - else - echo "No React demos found in changed files, skipping conversion" - echo "has-react-demos=false" >> $GITHUB_OUTPUT - fi - - - uses: pnpm/action-setup@v6 - if: steps.changed-react-demos.outputs.has-react-demos == 'true' - with: - run_install: false - - - name: Use Node.js - if: steps.changed-react-demos.outputs.has-react-demos == 'true' - uses: actions/setup-node@v6 - with: - node-version-file: '.node-version' - - - name: Download devextreme sources - if: steps.changed-react-demos.outputs.has-react-demos == 'true' - uses: actions/download-artifact@v8 - with: - name: devextreme-sources - - - name: Get pnpm store directory - if: steps.changed-react-demos.outputs.has-react-demos == 'true' - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - uses: actions/cache/restore@v5 - if: steps.changed-react-demos.outputs.has-react-demos == 'true' - name: Restore pnpm cache - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-cache-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-cache - - - name: Install dependencies - if: steps.changed-react-demos.outputs.has-react-demos == 'true' - run: pnpm install --frozen-lockfile - - - name: Install tgz - if: steps.changed-react-demos.outputs.has-react-demos == 'true' - working-directory: apps/demos - run: pnpm add --ignore-workspace --allow-build=core-js --allow-build=inferno devextreme-aspnet-data@5.1.0 devextreme-aspnet-data-nojquery@5.1.0 ../../devextreme-installer.tgz ../../devextreme-dist-installer.tgz ../../devextreme-react-installer.tgz ../../devextreme-vue-installer.tgz ../../devextreme-angular-installer.tgz && rm -f pnpm-workspace.yaml pnpm-lock.yaml - - - name: Check generated JS demos - if: steps.changed-react-demos.outputs.has-react-demos == 'true' - working-directory: apps/demos - run: | - echo "Running convert-to-js on changed files only" - xargs -r pnpm run convert-to-js < changed-react-demos.txt - - git add ./Demos -N - - if git diff --exit-code . ':!package.json' ; then - echo "Generated JS demos are up-to-date" - else - echo "Generated JS demos are outdated. Execute 'pnpm run convert-to-js split' and commit changes." - echo "If you see another diff, ensure that extra listed files have LF endings." - exit 1 - fi - check-generated-demos-all: name: Check generated demos runs-on: ${{ github.event_name == 'merge_group' && 'devextreme-shr2-ondemand' || 'devextreme-shr2' }} timeout-minutes: 20 - needs: [check-should-run, build-devextreme, determine-framework-tests-scope] + needs: [check-should-run, build-devextreme] if: | !cancelled() && needs.check-should-run.outputs.should-run == 'true' && - needs.determine-framework-tests-scope.result == 'success' && - needs.determine-framework-tests-scope.outputs.framework-tests-scope == 'all' && needs.build-devextreme.result == 'success' steps: @@ -751,16 +467,25 @@ jobs: fi testcafe-jquery: - needs: [check-should-run, build-devextreme, determine-framework-tests-scope, determine-jquery-test-matrix] + needs: [check-should-run, build-devextreme] if: | !cancelled() && needs.check-should-run.outputs.should-run == 'true' && - needs.determine-framework-tests-scope.result == 'success' && - needs.determine-jquery-test-matrix.result == 'success' && needs.build-devextreme.result == 'success' strategy: fail-fast: false - matrix: ${{ fromJson(needs.determine-jquery-test-matrix.outputs.matrix) }} + matrix: + include: + - { STRATEGY: screenshots, THEME: material.blue.light, CONSTEL: jquery(1/3) } + - { STRATEGY: screenshots, THEME: material.blue.light, CONSTEL: jquery(2/3) } + - { STRATEGY: screenshots, THEME: material.blue.light, CONSTEL: jquery(3/3) } + - { STRATEGY: screenshots, THEME: fluent.blue.light, CONSTEL: jquery(1/3) } + - { STRATEGY: screenshots, THEME: fluent.blue.light, CONSTEL: jquery(2/3) } + - { STRATEGY: screenshots, THEME: fluent.blue.light, CONSTEL: jquery(3/3) } + - { STRATEGY: accessibility, THEME: material.blue.light, CONSTEL: jquery } + - { STRATEGY: accessibility, THEME: fluent.blue.light, CONSTEL: jquery } + - { STRATEGY: accessibility, THEME: material.blue.dark, CONSTEL: jquery } + - { STRATEGY: accessibility, THEME: fluent.blue.dark, CONSTEL: jquery } env: ACCESSIBILITY_TESTCAFE_REPORT_PATH: "accessibility_testcafe_report" @@ -909,12 +634,10 @@ jobs: if-no-files-found: ignore testcafe-frameworks-all: - needs: [check-should-run, determine-framework-tests-scope, build-demos, build-vendor-bundles] + needs: [check-should-run, build-demos, build-vendor-bundles] if: | !cancelled() && needs.check-should-run.outputs.should-run == 'true' && - needs.determine-framework-tests-scope.result == 'success' && - needs.determine-framework-tests-scope.outputs.framework-tests-scope == 'all' && needs.build-demos.result == 'success' strategy: fail-fast: false @@ -1096,176 +819,10 @@ jobs: path: ${{ github.workspace }}/apps/demos/testing/artifacts/compared-screenshots/**/* if-no-files-found: ignore - testcafe-frameworks-changed: - needs: [check-should-run, determine-framework-tests-scope, build-demos, build-vendor-bundles] - if: | - !cancelled() && - needs.check-should-run.outputs.should-run == 'true' && - needs.determine-framework-tests-scope.result == 'success' && - needs.determine-framework-tests-scope.outputs.framework-tests-scope == 'changed' && - needs.build-demos.result == 'success' - strategy: - fail-fast: false - matrix: - CONSTEL: [react, vue, angular] - THEME: ['fluent.blue.light'] - - runs-on: ${{ github.event_name == 'merge_group' && 'devextreme-shr2-ondemand' || 'devextreme-shr2' }} - name: ${{ matrix.CONSTEL }}-screenshots-${{ matrix.THEME }} - timeout-minutes: 60 - - steps: - - name: Get sources - uses: actions/checkout@v6 - - - name: Setup Chrome - uses: ./.github/actions/setup-chrome - with: - chrome-version: '150.0.7871.181' - - - name: Use Node.js - uses: actions/setup-node@v6 - with: - node-version-file: '.node-version' - - - name: Download devextreme sources - uses: actions/download-artifact@v8 - with: - name: devextreme-sources - - - name: Download changed demos - uses: actions/download-artifact@v8 - with: - name: changed-demos - path: apps/demos - continue-on-error: true - - - uses: pnpm/action-setup@v6 - with: - run_install: false - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - uses: actions/cache/restore@v5 - name: Restore pnpm cache - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-cache-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-cache - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Install tgz - working-directory: apps/demos - run: pnpm add --ignore-workspace --allow-build=core-js --allow-build=inferno devextreme-aspnet-data@5.1.0 devextreme-aspnet-data-nojquery@5.1.0 ../../devextreme-installer.tgz ../../devextreme-dist-installer.tgz ../../devextreme-react-installer.tgz ../../devextreme-vue-installer.tgz ../../devextreme-angular-installer.tgz && rm -f pnpm-workspace.yaml pnpm-lock.yaml - - - name: Download demo bundles (React) - if: matrix.CONSTEL == 'react' - uses: actions/download-artifact@v8 - with: - name: devextreme-bundles-React - path: apps/demos/.demo-bundles/react - - - name: Download demo bundles (Vue) - if: matrix.CONSTEL == 'vue' - uses: actions/download-artifact@v8 - with: - name: devextreme-bundles-Vue - path: apps/demos/.demo-bundles/vue - - # build-demos shards Angular 3 ways regardless of how it's tested here. - - name: Download demo bundles (Angular shard 1) - if: matrix.CONSTEL == 'angular' - uses: actions/download-artifact@v8 - with: - name: devextreme-bundles-Angular-shard1 - path: apps/demos/.demo-bundles/angular-shard1 - - - name: Download demo bundles (Angular shard 2) - if: matrix.CONSTEL == 'angular' - uses: actions/download-artifact@v8 - with: - name: devextreme-bundles-Angular-shard2 - path: apps/demos/.demo-bundles/angular-shard2 - - - name: Download demo bundles (Angular shard 3) - if: matrix.CONSTEL == 'angular' - uses: actions/download-artifact@v8 - with: - name: devextreme-bundles-Angular-shard3 - path: apps/demos/.demo-bundles/angular-shard3 - - - name: Download changes artifacts - uses: actions/download-artifact@v8 - with: - name: changed-demos - path: apps/demos - - # No-op for angular demos (no manifest — see build-vendor-bundles.js). - - name: Download vendor bundles - uses: actions/download-artifact@v8 - with: - name: devextreme-vendor-bundles - path: apps/demos/bundles/vendor - - - name: Unpack demo bundles - run: | - shopt -s nullglob - archives=(apps/demos/.demo-bundles/*/demo-bundles.tgz) - if [ ${#archives[@]} -eq 0 ]; then - echo "No demo bundle archives downloaded for constellation '${{ matrix.CONSTEL }}'" >&2 - exit 1 - fi - for archive in "${archives[@]}"; do - tar -xzf "$archive" -C apps/demos - done - rm -rf apps/demos/.demo-bundles - echo "Unpacked $(find apps/demos/Demos -name 'bundle.js' | wc -l) demo bundle(s)" - - - name: Run Web Server - run: | - python -m http.server 8080 & - python -m http.server 8081 & - python -m http.server 8082 & - python -m http.server 8083 & - - - name: Run TestCafe tests - shell: bash - working-directory: apps/demos - env: - CHANGEDFILEINFOSPATH: changed-files.json - BROWSERS: chrome:headless --window-size=1200,800 --disable-gpu --no-sandbox --disable-dev-shm-usage --disable-partial-raster --disable-skia-runtime-opts --run-all-compositor-stages-before-draw --disable-new-content-rendering-timeout --disable-threaded-animation --disable-threaded-scrolling --disable-checker-imaging --disable-image-animation-resync --use-gl=swiftshader --disable-features=PaintHolding --js-flags=--random-seed=2147483647 --font-render-hinting=none --disable-font-subpixel-positioning - # DEBUG: hammerhead:*,testcafe:* - CONCURRENCY: 1 - CONSTEL: ${{ matrix.CONSTEL }} - THEME: ${{ matrix.THEME }} - # DISABLE_DEMO_TEST_SETTINGS: all # Uncomment to ignore all the visualtestrc.json settings - # DISABLE_DEMO_TEST_SETTINGS: ignore # Uncomment to ignore the `ignore` field - # DISABLE_DEMO_TEST_SETTINGS: comparison-options # Uncomment to ignore the `comparison-options` field - CI_ENV: true # The `ignore` field in the visualtestrc.json should be disabled when running test locally - run: pnpm exec nx test-testcafe - - - name: Sanitize job name - if: ${{ failure() }} - run: echo "JOB_NAME=$(echo "${{ matrix.CONSTEL }}-${{ matrix.THEME }}" | tr '/' '-')" >> $GITHUB_ENV - - - name: Copy screenshots artifacts - if: ${{ failure() }} - uses: actions/upload-artifact@v7 - with: - name: screenshots-${{ env.JOB_NAME }} - path: ${{ github.workspace }}/apps/demos/testing/artifacts/compared-screenshots/**/* - if-no-files-found: ignore - merge-artifacts: runs-on: ubuntu-latest - needs: [testcafe-jquery, testcafe-frameworks-all, testcafe-frameworks-changed] - if: ${{ !cancelled() && (needs.testcafe-jquery.result == 'failure' || needs.testcafe-frameworks-all.result == 'failure' || needs.testcafe-frameworks-changed.result == 'failure') }} + needs: [testcafe-jquery, testcafe-frameworks-all] + if: ${{ !cancelled() && (needs.testcafe-jquery.result == 'failure' || needs.testcafe-frameworks-all.result == 'failure') }} steps: - name: Merge jQuery screenshot artifacts @@ -1278,7 +835,7 @@ jobs: delete-merged: true - name: Merge React screenshot artifacts - if: needs.testcafe-frameworks-all.result == 'failure' || needs.testcafe-frameworks-changed.result == 'failure' + if: needs.testcafe-frameworks-all.result == 'failure' uses: actions/upload-artifact/merge@v7 continue-on-error: true with: @@ -1287,7 +844,7 @@ jobs: delete-merged: true - name: Merge Vue screenshot artifacts - if: needs.testcafe-frameworks-all.result == 'failure' || needs.testcafe-frameworks-changed.result == 'failure' + if: needs.testcafe-frameworks-all.result == 'failure' uses: actions/upload-artifact/merge@v7 continue-on-error: true with: @@ -1296,7 +853,7 @@ jobs: delete-merged: true - name: Merge Angular screenshot artifacts - if: needs.testcafe-frameworks-all.result == 'failure' || needs.testcafe-frameworks-changed.result == 'failure' + if: needs.testcafe-frameworks-all.result == 'failure' uses: actions/upload-artifact/merge@v7 continue-on-error: true with: @@ -1386,12 +943,10 @@ jobs: csp-check-frameworks: name: ${{ matrix.SHARD_TOTAL == 1 && format('CSP check ({0})', matrix.FRAMEWORK) || format('CSP check ({0} {1}/{2})', matrix.FRAMEWORK, matrix.SHARD_INDEX, matrix.SHARD_TOTAL) }} - needs: [check-should-run, determine-framework-tests-scope, build-devextreme] + needs: [check-should-run, build-devextreme] if: | !cancelled() && needs.check-should-run.outputs.should-run == 'true' && - needs.determine-framework-tests-scope.result == 'success' && - needs.determine-framework-tests-scope.outputs.framework-tests-scope != 'none' && needs.build-devextreme.result == 'success' strategy: fail-fast: false diff --git a/apps/demos/Demos/HtmlEditor/AITextEditing/Angular/app/app.service.ts b/apps/demos/Demos/HtmlEditor/AITextEditing/Angular/app/app.service.ts index f04866495dcc..7c99ac1335d9 100644 --- a/apps/demos/Demos/HtmlEditor/AITextEditing/Angular/app/app.service.ts +++ b/apps/demos/Demos/HtmlEditor/AITextEditing/Angular/app/app.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { type DxHtmlEditorTypes } from 'devextreme-angular/ui/html_editor'; +import { type DxHtmlEditorTypes } from 'devextreme-angular/ui/html-editor'; const extractKeywordsPrompt: DxHtmlEditorTypes.AICustomCommand['prompt'] = () => 'Extract a list of keywords from the text and return it as a comma-separated string'; diff --git a/apps/demos/Demos/PivotGrid/StandaloneFieldChooser/Angular/app/app.component.ts b/apps/demos/Demos/PivotGrid/StandaloneFieldChooser/Angular/app/app.component.ts index 9b0bcfb6a7c5..1079a7780bf7 100644 --- a/apps/demos/Demos/PivotGrid/StandaloneFieldChooser/Angular/app/app.component.ts +++ b/apps/demos/Demos/PivotGrid/StandaloneFieldChooser/Angular/app/app.component.ts @@ -1,20 +1,111 @@ import { bootstrapApplication } from '@angular/platform-browser'; -import { Component } from '@angular/core'; - -// TODO: restore the original demo (see git history for this file, app.component.html, -// app.component.css, app.service.ts). Disabled because combining DxPivotGridModule and -// DxPivotGridFieldChooserModule here reproducibly triggers, in CI only (not locally, likely a -// devextreme-angular artifacts staleness difference between environments): -// NG8023: Multiple components match node with tagname dxo-pivot-grid-field-chooser-texts: -// 'DxoPivotGridFieldChooserTextsComponent', 'DxoPivotGridFieldChooserTextsComponent'. -// Both modules generate their own nested-options component for that tag name, and combining -// them in one standalone component's imports puts both in scope at once. Needs a real fix in -// devextreme-angular's generated wrappers (or dropping one of the two imports if one turns out -// to be redundant here) rather than a demo-side workaround. +import { Component, enableProdMode, provideZoneChangeDetection } from '@angular/core'; +import { + DxRadioGroupComponent, + DxButtonComponent, + DxSelectBoxComponent, +} from 'devextreme-angular'; +import PivotGridDataSource from 'devextreme/ui/pivot_grid/data_source'; +import { + DxPivotGridComponent, + DxoPivotGridFieldChooserComponent, + DxPivotGridTypes, +} from 'devextreme-angular/ui/pivot-grid'; +import { + DxPivotGridFieldChooserComponent, + DxoPivotGridFieldChooserTextsComponent, +} from 'devextreme-angular/ui/pivot-grid-field-chooser'; +import { Service, Layout, Sale } from './app.service'; + +if (!/localhost/.test(document.location.host)) { + enableProdMode(); +} + @Component({ selector: 'demo-app', - template: '

This demo is temporarily disabled. See the TODO in app.component.ts.

', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'], + providers: [Service], + preserveWhitespaces: true, + imports: [ + DxPivotGridComponent, + DxoPivotGridFieldChooserComponent, + DxRadioGroupComponent, + DxPivotGridFieldChooserComponent, + DxoPivotGridFieldChooserTextsComponent, + DxSelectBoxComponent, + DxButtonComponent, + ], }) -export class AppComponent {} +export class AppComponent { + pivotGridDataSource: PivotGridDataSource; + + layouts: Layout[]; + + layout = 0; + + applyChangesModes: DxPivotGridTypes.ApplyChangesMode[] = ['instantly', 'onDemand']; + + applyChangesMode = this.applyChangesModes[0]; + + state: Record; + + constructor(service: Service) { + this.pivotGridDataSource = new PivotGridDataSource({ + fields: [{ + caption: 'Region', + width: 120, + dataField: 'region', + area: 'row', + headerFilter: { + search: { + enabled: true, + }, + }, + }, { + caption: 'City', + dataField: 'city', + width: 150, + area: 'row', + headerFilter: { + search: { + enabled: true, + }, + }, + selector(data: Sale) { + return `${data.city} (${data.country})`; + }, + }, { + dataField: 'date', + dataType: 'date', + area: 'column', + }, { + caption: 'Sales', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + format: 'currency', + area: 'data', + }], + store: service.getSales(), + }); + + this.state = this.pivotGridDataSource.state(); + + this.layouts = service.getLayouts(); + } + + applyClick() { + this.pivotGridDataSource.state(this.state); + } + + cancelClick() { + this.state = this.pivotGridDataSource.state(); + } +} -bootstrapApplication(AppComponent); +bootstrapApplication(AppComponent, { + providers: [ + provideZoneChangeDetection({ eventCoalescing: true, runCoalescing: true }), + ], +}); diff --git a/apps/demos/Demos/Stepper/StepTemplate/Angular/app/app.component.html b/apps/demos/Demos/Stepper/StepTemplate/Angular/app/app.component.html index 0128c7d521dd..e1c08fa86bc7 100644 --- a/apps/demos/Demos/Stepper/StepTemplate/Angular/app/app.component.html +++ b/apps/demos/Demos/Stepper/StepTemplate/Angular/app/app.component.html @@ -41,6 +41,11 @@ [elementAttr]="{ 'aria-labelledby': 'label-iconOnly' }" >
- +
diff --git a/apps/demos/Demos/Stepper/StepTemplate/React/IconOnly.tsx b/apps/demos/Demos/Stepper/StepTemplate/React/IconOnly.tsx index d84e95907c85..bb0e60634491 100644 --- a/apps/demos/Demos/Stepper/StepTemplate/React/IconOnly.tsx +++ b/apps/demos/Demos/Stepper/StepTemplate/React/IconOnly.tsx @@ -2,10 +2,11 @@ import React from 'react'; interface IconOnlyProps { icon: string; + label: string; } -export default function IconOnly({ icon }: IconOnlyProps) { +export default function IconOnly({ icon, label }: IconOnlyProps) { return ( - + ); } diff --git a/apps/demos/Demos/Stepper/StepTemplate/ReactJs/IconOnly.js b/apps/demos/Demos/Stepper/StepTemplate/ReactJs/IconOnly.js index 1344ad909c1d..466eafa507af 100644 --- a/apps/demos/Demos/Stepper/StepTemplate/ReactJs/IconOnly.js +++ b/apps/demos/Demos/Stepper/StepTemplate/ReactJs/IconOnly.js @@ -1,5 +1,11 @@ import React from 'react'; -export default function IconOnly({ icon }) { - return ; +export default function IconOnly({ icon, label }) { + return ( + + ); } diff --git a/apps/demos/Demos/Stepper/StepTemplate/Vue/App.vue b/apps/demos/Demos/Stepper/StepTemplate/Vue/App.vue index a0a8ea0375a2..2fd89b68b2c9 100644 --- a/apps/demos/Demos/Stepper/StepTemplate/Vue/App.vue +++ b/apps/demos/Demos/Stepper/StepTemplate/Vue/App.vue @@ -57,7 +57,11 @@ > diff --git a/apps/demos/Demos/Stepper/StepTemplate/jQuery/index.js b/apps/demos/Demos/Stepper/StepTemplate/jQuery/index.js index fd5945874d17..a80ea8666056 100644 --- a/apps/demos/Demos/Stepper/StepTemplate/jQuery/index.js +++ b/apps/demos/Demos/Stepper/StepTemplate/jQuery/index.js @@ -33,7 +33,9 @@ $(() => { linear: false, elementAttr: { 'aria-labelledby': 'label-iconOnly' }, itemTemplate(data) { - return ``; + return $('') + .addClass(`dx-icon dx-icon-${data.icon}`) + .attr({ role: 'img', 'aria-label': data.label }); }, }); }); diff --git a/apps/demos/Demos/TabPanel/Overview/Angular/app/app.component.css b/apps/demos/Demos/TabPanel/Overview/Angular/app/app.component.css index 046af40d3171..320a5f6dc23e 100644 --- a/apps/demos/Demos/TabPanel/Overview/Angular/app/app.component.css +++ b/apps/demos/Demos/TabPanel/Overview/Angular/app/app.component.css @@ -104,7 +104,7 @@ ::ng-deep .task-item-info { font-size: 12px; - opacity: 0.6; + opacity: 0.65; } ::ng-deep .task-item-pseudo-button { diff --git a/apps/demos/Demos/TabPanel/Overview/React/styles.css b/apps/demos/Demos/TabPanel/Overview/React/styles.css index 7d2c7f569c05..71ad3ea17b02 100644 --- a/apps/demos/Demos/TabPanel/Overview/React/styles.css +++ b/apps/demos/Demos/TabPanel/Overview/React/styles.css @@ -104,7 +104,7 @@ .task-item-info { font-size: 12px; - opacity: 0.6; + opacity: 0.65; } .task-item-pseudo-button { diff --git a/apps/demos/Demos/TabPanel/Overview/ReactJs/styles.css b/apps/demos/Demos/TabPanel/Overview/ReactJs/styles.css index 7d2c7f569c05..71ad3ea17b02 100644 --- a/apps/demos/Demos/TabPanel/Overview/ReactJs/styles.css +++ b/apps/demos/Demos/TabPanel/Overview/ReactJs/styles.css @@ -104,7 +104,7 @@ .task-item-info { font-size: 12px; - opacity: 0.6; + opacity: 0.65; } .task-item-pseudo-button { diff --git a/apps/demos/Demos/TabPanel/Overview/Vue/TaskItem.vue b/apps/demos/Demos/TabPanel/Overview/Vue/TaskItem.vue index bfe6973f9762..76699ee28f0d 100644 --- a/apps/demos/Demos/TabPanel/Overview/Vue/TaskItem.vue +++ b/apps/demos/Demos/TabPanel/Overview/Vue/TaskItem.vue @@ -66,7 +66,7 @@ withDefaults(defineProps<{ .task-item-info { font-size: 12px; - opacity: 0.6; + opacity: 0.65; } .task-item-pseudo-button { diff --git a/apps/demos/Demos/TabPanel/Overview/jQuery/styles.css b/apps/demos/Demos/TabPanel/Overview/jQuery/styles.css index 7d2c7f569c05..71ad3ea17b02 100644 --- a/apps/demos/Demos/TabPanel/Overview/jQuery/styles.css +++ b/apps/demos/Demos/TabPanel/Overview/jQuery/styles.css @@ -104,7 +104,7 @@ .task-item-info { font-size: 12px; - opacity: 0.6; + opacity: 0.65; } .task-item-pseudo-button { diff --git a/apps/demos/README.md b/apps/demos/README.md index 0fb102135541..07b8c7706398 100644 --- a/apps/demos/README.md +++ b/apps/demos/README.md @@ -28,17 +28,6 @@ You can pass a port as an extra argument. That is useful when you need to switch Angular, React, and Vue demos are bundled on demand when you open a page. jQuery demos load `dx.all.js` from `devextreme-dist`. -### Demo render signal - -Angular, React, and Vue demos are not bundled from their own entry point directly. `utils/server/demo-render-signal.js` generates a shim that becomes the bundle's entry point; the shim waits for `themes.initialized()` from `devextreme/ui/themes`, then imports the demo's entry (`index.tsx` / `index.ts` / `app/app.component.ts`), so nothing mounts before the theme CSS is applied. - -Once the demo has rendered, the shim posts one message to the embedding page: - -```js -window.parent.postMessage({ type: 'demo-rendered' }, '*'); -``` - - ### Before Commiting Changes Auto-fix lint errors: diff --git a/apps/demos/testing/common.test.ts b/apps/demos/testing/common.test.ts index d318a8b6cde1..192deec511ad 100644 --- a/apps/demos/testing/common.test.ts +++ b/apps/demos/testing/common.test.ts @@ -119,6 +119,15 @@ const getIgnoredRules = (testName) => { ]; }; +// The appointment title and date do not reach the AA contrast ratio: the date line is dimmed +// to 70% opacity and the appointments are painted with the resource colors. +const getAxeContext = (testName) => (testName.startsWith('Scheduler-') + ? { + include: ['.demo-container'], + exclude: ['.dx-scheduler-appointment-title', '.dx-scheduler-appointment-content-date'], + } + : '.demo-container'); + const getClientScripts = (approach: string) => { const scripts = [ { module: 'mockdate' }, @@ -250,7 +259,7 @@ Object.values(FRAMEWORKS).forEach((approach) => { options.rules[ruleName] = { enabled: false }; }); - const axeResult = await axeCheck(t, '.demo-container', options); + const axeResult = await axeCheck(t, getAxeContext(testName), options); const { error, results } = axeResult; if (results.violations.length > 0) { diff --git a/apps/demos/testing/skipped-tests.js b/apps/demos/testing/skipped-tests.js index 52bd9f86578d..cdd9e5ce7dcf 100644 --- a/apps/demos/testing/skipped-tests.js +++ b/apps/demos/testing/skipped-tests.js @@ -6,8 +6,6 @@ export const skippedTests = { Angular: { // WebGL initialization error at the provider. There is no point in adding a mask Map: ['ProvidersAndTypes', 'Markers', 'Routes'], - // Demo is stubbed out (see the TODO in its app.component.ts) pending a devextreme-angular fix - PivotGrid: ['StandaloneFieldChooser'], }, React: { // WebGL initialization error at the provider. There is no point in adding a mask diff --git a/apps/demos/testing/widgets/scheduler/etalons/scheduler_ResolveTimeConflicts_appointment-popup_recurrence-group_conflict-informer (fluent.blue.light)_mask.png b/apps/demos/testing/widgets/scheduler/etalons/scheduler_ResolveTimeConflicts_appointment-popup_recurrence-group_conflict-informer (fluent.blue.light)_mask.png new file mode 100644 index 000000000000..e4f261f4139e Binary files /dev/null and b/apps/demos/testing/widgets/scheduler/etalons/scheduler_ResolveTimeConflicts_appointment-popup_recurrence-group_conflict-informer (fluent.blue.light)_mask.png differ diff --git a/apps/demos/testing/widgets/scheduler/etalons/scheduler_ResolveTimeConflicts_appointment-popup_recurrence-group_conflict-informer (material.blue.light).png b/apps/demos/testing/widgets/scheduler/etalons/scheduler_ResolveTimeConflicts_appointment-popup_recurrence-group_conflict-informer (material.blue.light).png index ef926159c9c3..410371c30851 100644 Binary files a/apps/demos/testing/widgets/scheduler/etalons/scheduler_ResolveTimeConflicts_appointment-popup_recurrence-group_conflict-informer (material.blue.light).png and b/apps/demos/testing/widgets/scheduler/etalons/scheduler_ResolveTimeConflicts_appointment-popup_recurrence-group_conflict-informer (material.blue.light).png differ diff --git a/apps/demos/testing/widgets/scheduler/etalons/scheduler_ResolveTimeConflicts_appointment-popup_recurrence-group_conflict-informer (material.blue.light)_mask.png b/apps/demos/testing/widgets/scheduler/etalons/scheduler_ResolveTimeConflicts_appointment-popup_recurrence-group_conflict-informer (material.blue.light)_mask.png new file mode 100644 index 000000000000..bf66555a7025 Binary files /dev/null and b/apps/demos/testing/widgets/scheduler/etalons/scheduler_ResolveTimeConflicts_appointment-popup_recurrence-group_conflict-informer (material.blue.light)_mask.png differ diff --git a/apps/demos/utils/server/csp-bundle-angular.js b/apps/demos/utils/server/csp-bundle-angular.js index b6104c09749d..e28b2b36ad53 100644 --- a/apps/demos/utils/server/csp-bundle-angular.js +++ b/apps/demos/utils/server/csp-bundle-angular.js @@ -179,11 +179,6 @@ function findAngularEntry(srcDir) { return null; } -// TODO: remove once PivotGrid/StandaloneFieldChooser/Angular renders reliably (times out in csp-check.js) -const SKIPPED_DEMOS = new Set([ - 'PivotGrid/StandaloneFieldChooser/Angular', -]); - function findDemos() { const out = []; if (!fs.existsSync(SRC_DEMOS_DIR)) return out; @@ -194,7 +189,6 @@ function findDemos() { const demos = fs.readdirSync(widgetDir, { withFileTypes: true }).filter((d) => d.isDirectory()); for (const demo of demos) { const key = `${widget.name}/${demo.name}`; - if (SKIPPED_DEMOS.has(`${key}/${FRAMEWORK}`)) continue; // eslint-disable-line no-continue const fwDir = path.join(widgetDir, demo.name, FRAMEWORK); const matchesFilter = !FILTER || key.includes(FILTER); if (matchesFilter && fs.existsSync(path.join(fwDir, 'index.html'))) { diff --git a/apps/demos/utils/server/csp-check.js b/apps/demos/utils/server/csp-check.js index b3a8afdfac4a..0e19d27275fa 100644 --- a/apps/demos/utils/server/csp-check.js +++ b/apps/demos/utils/server/csp-check.js @@ -176,11 +176,6 @@ function applyShard(demos) { return applyShardGeneric(demos, (d) => `${d.widget}/${d.demo}`, { SHARD_TOTAL, SHARD_INDEX }); } -// TODO: remove once PivotGrid/StandaloneFieldChooser/Angular renders reliably (times out here) -const SKIPPED_DEMOS = new Set([ - 'PivotGrid/StandaloneFieldChooser/Angular', -]); - function findDemos() { const demosDirName = 'Demos'; const demosDir = join(DEMO_ROOT, demosDirName); @@ -202,7 +197,6 @@ function findDemos() { .filter((d) => d.isDirectory()); for (const demo of demos) { - if (SKIPPED_DEMOS.has(`${widget.name}/${demo.name}/${FRAMEWORK}`)) continue; // eslint-disable-line no-continue const fwDir = join(widgetDir, demo.name, FRAMEWORK); if (existsSync(join(fwDir, 'index.html'))) { result.push({ diff --git a/apps/demos/utils/server/demo-render-signal.js b/apps/demos/utils/server/demo-render-signal.js index 0e2f05cbc79d..8466c9631057 100644 --- a/apps/demos/utils/server/demo-render-signal.js +++ b/apps/demos/utils/server/demo-render-signal.js @@ -6,17 +6,29 @@ const RUNTIME_PATH = path.join(__dirname, 'demo-render-signal.runtime.js'); const VIRTUAL_ENTRY_PREFIX = 'demo-render-signal:'; const VIRTUAL_ENTRY_NAMESPACE = 'demo-render-signal'; +function allowedParentOrigins() { + const fromEnv = String(process.env.DEMO_PARENT_ORIGINS || '') + .split(',') + .map((origin) => origin.trim()) + .filter(Boolean); + return fromEnv.length > 0 ? fromEnv : null; +} + function toImportPath(filePath) { return JSON.stringify(filePath.split(path.sep).join('/')); } function createEntryShimSource(entryPath, { tsNoCheck = false } = {}) { + const origins = allowedParentOrigins(); + const imported = ['signal', 'signalWhenRendered']; + if (origins) imported.unshift('setAllowedOrigins'); + return ` ${tsNoCheck ? '// @ts-nocheck\n' : ''} // Generated by utils/server/demo-render-signal.js. import themes from ${JSON.stringify(THEMES_SPECIFIER)}; - import { signal, signalWhenRendered } from ${toImportPath(RUNTIME_PATH)}; - + import { ${imported.join(', ')} } from ${toImportPath(RUNTIME_PATH)}; +${origins ? `\n setAllowedOrigins(${JSON.stringify(origins)});\n` : ''} themes.initialized(function () { import(${toImportPath(entryPath)}).then(signalWhenRendered, function (error) { console.error('demo failed to load', error); diff --git a/apps/demos/utils/server/demo-render-signal.runtime.js b/apps/demos/utils/server/demo-render-signal.runtime.js index a82fb1f54b68..4fc043e37a52 100644 --- a/apps/demos/utils/server/demo-render-signal.runtime.js +++ b/apps/demos/utils/server/demo-render-signal.runtime.js @@ -5,13 +5,85 @@ export const ROOT_SELECTOR = '#app, demo-app'; export const RENDER_TIMEOUT_MS = 10000; +export const DEFAULT_ALLOWED_ORIGINS = [ + 'localhost', + 'https://js.devexpress.com', +]; + +const PATTERN = /^(?:([a-z][a-z0-9+.-]*):\/\/)?([^/:]+)(?::(\d+))?$/i; + +let allowedOrigins = DEFAULT_ALLOWED_ORIGINS; let posted = false; +export function setAllowedOrigins(origins) { + if (Array.isArray(origins) && origins.length > 0) { + allowedOrigins = origins; + } +} + +function hostMatches(hostname, patternHost) { + if (!patternHost.startsWith('*.')) return hostname === patternHost; + + const suffix = patternHost.slice(1); + return hostname.length > suffix.length && hostname.endsWith(suffix); +} + +function matchesPattern(url, pattern) { + const parts = PATTERN.exec(pattern); + if (!parts) return false; + + const [, scheme, host, port] = parts; + if (scheme && `${scheme.toLowerCase()}:` !== url.protocol) return false; + if (port && port !== url.port) return false; + + return hostMatches(url.hostname, host.toLowerCase()); +} + +function isAllowed(origin) { + let url; + try { + url = new URL(origin); + } catch { + return false; + } + return allowedOrigins.some((pattern) => matchesPattern(url, pattern)); +} + +function parentOrigin() { + const { ancestorOrigins } = window.location; + if (ancestorOrigins && ancestorOrigins.length > 0) return ancestorOrigins[0]; + + if (!document.referrer) return null; + try { + return new URL(document.referrer).origin; + } catch { + return null; + } +} + +export function resolveTargetOrigin() { + const ownOrigin = window.location.origin; + if (window.parent === window) return ownOrigin; + + const origin = parentOrigin(); + if (!origin) return null; + if (origin === ownOrigin) return origin; + + return isAllowed(origin) ? origin : null; +} + export function signal() { if (posted) return; posted = true; + + const targetOrigin = resolveTargetOrigin(); + if (!targetOrigin) { + console.warn(`Demo render signal was not sent: the embedding origin is unknown or not in [${allowedOrigins.join(', ')}].`); + return; + } + try { - window.parent.postMessage({ type: MESSAGE_TYPE }, '*'); + window.parent.postMessage({ type: MESSAGE_TYPE }, targetOrigin); } catch { // A parent that rejects the message is the parent's problem. } diff --git a/apps/demos/utils/tests/server/demo-render-signal.runtime.test.js b/apps/demos/utils/tests/server/demo-render-signal.runtime.test.js index 27af89bbffbd..e2f94564b2eb 100644 --- a/apps/demos/utils/tests/server/demo-render-signal.runtime.test.js +++ b/apps/demos/utils/tests/server/demo-render-signal.runtime.test.js @@ -1,7 +1,12 @@ +const OWN_ORIGIN = 'https://demos.test'; +const ALLOWED_PARENT_HOST = 'js.devexpress.com'; +const ALLOWED_PARENT = `https://${ALLOWED_PARENT_HOST}`; + let messages; let root; let rafCallbacks; let observers; +let warnings; function loadRuntime() { let runtime; @@ -22,6 +27,10 @@ function flushFrames() { } } +function embedIn(origin) { + global.window.location.ancestorOrigins = origin === null ? [] : [origin]; +} + beforeEach(() => { jest.useFakeTimers(); @@ -29,13 +38,15 @@ beforeEach(() => { root = null; rafCallbacks = []; observers = []; + warnings = []; global.window = { parent: { postMessage: (data, targetOrigin) => messages.push({ data, targetOrigin }), }, + location: { origin: OWN_ORIGIN, ancestorOrigins: [ALLOWED_PARENT] }, }; - global.document = { querySelector: () => root }; + global.document = { querySelector: () => root, referrer: '' }; global.requestAnimationFrame = (callback) => rafCallbacks.push(callback); global.MutationObserver = class { constructor(callback) { @@ -50,10 +61,13 @@ beforeEach(() => { this.disconnected = true; } }; + + jest.spyOn(console, 'warn').mockImplementation((message) => warnings.push(message)); }); afterEach(() => { jest.useRealTimers(); + jest.restoreAllMocks(); delete global.window; delete global.document; delete global.requestAnimationFrame; @@ -61,12 +75,12 @@ afterEach(() => { }); describe('signal', () => { - test('posts the contract message to the parent', () => { + test('posts the contract message to the embedding origin', () => { const { signal, MESSAGE_TYPE } = loadRuntime(); signal(); - expect(messages).toEqual([{ data: { type: MESSAGE_TYPE }, targetOrigin: '*' }]); + expect(messages).toEqual([{ data: { type: MESSAGE_TYPE }, targetOrigin: ALLOWED_PARENT }]); expect(MESSAGE_TYPE).toBe('demo-rendered'); }); @@ -85,6 +99,192 @@ describe('signal', () => { expect(() => signal()).not.toThrow(); }); + + test('warns instead of posting to an origin that is not allowed', () => { + embedIn('https://evil.example'); + const { signal } = loadRuntime(); + + signal(); + + expect(messages).toHaveLength(0); + expect(warnings).toHaveLength(1); + expect(warnings[0]).toContain(ALLOWED_PARENT_HOST); + }); + + test('gives up for good after a dropped message', () => { + embedIn('https://evil.example'); + const { signal } = loadRuntime(); + + signal(); + embedIn(ALLOWED_PARENT); + signal(); + + expect(messages).toHaveLength(0); + }); +}); + +describe('resolveTargetOrigin', () => { + test('takes the embedding origin from ancestorOrigins', () => { + const { resolveTargetOrigin } = loadRuntime(); + + expect(resolveTargetOrigin()).toBe(ALLOWED_PARENT); + }); + + test.each([ + 'https://js.devexpress.com', + 'http://localhost:8080', + 'http://localhost:3000', + ])('allows the %s sandbox', (origin) => { + embedIn(origin); + const { resolveTargetOrigin } = loadRuntime(); + + expect(resolveTargetOrigin()).toBe(origin); + }); + + test('allows any scheme for an entry that omits one', () => { + embedIn('http://js.devexpress.com'); + const { setAllowedOrigins, resolveTargetOrigin } = loadRuntime(); + + setAllowedOrigins(['js.devexpress.com']); + + expect(resolveTargetOrigin()).toBe('http://js.devexpress.com'); + }); + + test.each([ + ['the apex of an allowed host', 'https://devexpress.com'], + ['a host that merely ends with an allowed one', 'https://evil-js.devexpress.com'], + ['a host that merely starts with an allowed one', 'https://localhost.evil.example'], + ['an opaque origin', 'null'], + ])('rejects %s', (_, origin) => { + embedIn(origin); + const { resolveTargetOrigin } = loadRuntime(); + + expect(resolveTargetOrigin()).toBeNull(); + }); + + test('pins the scheme for an entry that specifies one', () => { + embedIn('http://js.devexpress.com'); + const { setAllowedOrigins, resolveTargetOrigin } = loadRuntime(); + + setAllowedOrigins(['https://js.devexpress.com']); + + expect(resolveTargetOrigin()).toBeNull(); + }); + + test('pins the port for an entry that specifies one', () => { + embedIn('http://localhost:9999'); + const { setAllowedOrigins, resolveTargetOrigin } = loadRuntime(); + + setAllowedOrigins(['localhost:8080']); + + expect(resolveTargetOrigin()).toBeNull(); + }); + + test('matches a pinned port', () => { + embedIn('http://localhost:8080'); + const { setAllowedOrigins, resolveTargetOrigin } = loadRuntime(); + + setAllowedOrigins(['http://localhost:8080']); + + expect(resolveTargetOrigin()).toBe('http://localhost:8080'); + }); + + test('allows a subdomain of a wildcard entry', () => { + embedIn('https://demos.example.com'); + const { setAllowedOrigins, resolveTargetOrigin } = loadRuntime(); + + setAllowedOrigins(['*.example.com']); + + expect(resolveTargetOrigin()).toBe('https://demos.example.com'); + }); + + test.each([ + ['the apex domain', 'https://example.com'], + ['a host that only ends with the wildcard suffix', 'https://evil-example.com'], + ['a host shorter than the wildcard suffix', 'https://ex.com'], + ])('rejects %s for a wildcard entry', (_, origin) => { + embedIn(origin); + const { setAllowedOrigins, resolveTargetOrigin } = loadRuntime(); + + setAllowedOrigins(['*.example.com']); + + expect(resolveTargetOrigin()).toBeNull(); + }); + + test('ignores an entry that is not a bare origin', () => { + embedIn('https://js.devexpress.com'); + const { setAllowedOrigins, resolveTargetOrigin } = loadRuntime(); + + setAllowedOrigins(['https://js.devexpress.com/Demos/']); + + expect(resolveTargetOrigin()).toBeNull(); + }); + + test('allows an embedder on the demo own origin, allowlisted or not', () => { + embedIn(OWN_ORIGIN); + const { resolveTargetOrigin } = loadRuntime(); + + expect(resolveTargetOrigin()).toBe(OWN_ORIGIN); + }); + + test('targets the own origin when the demo is not framed', () => { + global.window.parent = global.window; + embedIn(null); + const { resolveTargetOrigin } = loadRuntime(); + + expect(resolveTargetOrigin()).toBe(OWN_ORIGIN); + }); + + test('falls back to the referrer origin without ancestorOrigins', () => { + delete global.window.location.ancestorOrigins; + global.document.referrer = `${ALLOWED_PARENT}/Demos/DataGrid/Overview/React/Light/`; + const { resolveTargetOrigin } = loadRuntime(); + + expect(resolveTargetOrigin()).toBe(ALLOWED_PARENT); + }); + + test('falls back to the referrer origin with empty ancestorOrigins', () => { + embedIn(null); + global.document.referrer = `${ALLOWED_PARENT}/`; + const { resolveTargetOrigin } = loadRuntime(); + + expect(resolveTargetOrigin()).toBe(ALLOWED_PARENT); + }); + + test('resolves nothing without ancestorOrigins or a referrer', () => { + embedIn(null); + const { resolveTargetOrigin } = loadRuntime(); + + expect(resolveTargetOrigin()).toBeNull(); + }); + + test('resolves nothing from an unparsable referrer', () => { + embedIn(null); + global.document.referrer = 'not-a-url'; + const { resolveTargetOrigin } = loadRuntime(); + + expect(resolveTargetOrigin()).toBeNull(); + }); +}); + +describe('setAllowedOrigins', () => { + test('replaces the default allowlist', () => { + embedIn('https://sandbox.example'); + const { setAllowedOrigins, resolveTargetOrigin } = loadRuntime(); + + setAllowedOrigins(['https://sandbox.example']); + + expect(resolveTargetOrigin()).toBe('https://sandbox.example'); + }); + + test('keeps the default allowlist when the build injects nothing', () => { + const { setAllowedOrigins, resolveTargetOrigin } = loadRuntime(); + + setAllowedOrigins(null); + setAllowedOrigins([]); + + expect(resolveTargetOrigin()).toBe(ALLOWED_PARENT); + }); }); describe('signalWhenRendered', () => { diff --git a/e2e/testcafe-devextreme/runner.ts b/e2e/testcafe-devextreme/runner.ts index 0be6eeeafc80..ab47c5657686 100644 --- a/e2e/testcafe-devextreme/runner.ts +++ b/e2e/testcafe-devextreme/runner.ts @@ -16,6 +16,7 @@ import { getCurrentTheme } from './helpers/themeUtils'; const LAUNCH_RETRY_ATTEMPTS = 3; const LAUNCH_RETRY_TIMEOUT = 10000; const FAILED_TESTS_RETRY_ATTEMPTS = 0; +const TEST_EXECUTION_TIMEOUT = 3 * 60 * 1000; const wait = async ( timeout: number, @@ -328,6 +329,7 @@ async function main() { const runOptions: RunOptions = { quarantineMode: false, + testExecutionTimeout: TEST_EXECUTION_TIMEOUT, // @ts-expect-error ts-error hooks: { test: { diff --git a/e2e/testcafe-devextreme/tests/accessibility/scheduler/appointment.ts b/e2e/testcafe-devextreme/tests/accessibility/scheduler/appointment.ts index 6a7727e2871a..f9251fd8e70b 100644 --- a/e2e/testcafe-devextreme/tests/accessibility/scheduler/appointment.ts +++ b/e2e/testcafe-devextreme/tests/accessibility/scheduler/appointment.ts @@ -2,7 +2,7 @@ import Scheduler from 'devextreme-testcafe-models/scheduler'; import { a11yCheck } from '../../../helpers/accessibility/utils'; import { createWidget } from '../../../helpers/createWidget'; import url from '../../../helpers/getPageUrl'; -import { a11yCheckConfig } from './axe_options'; +import { a11yCheckConfig, a11yContext } from './axe_options'; fixture.disablePageReloads`Scheduler - Appointment` .page(url(__dirname, '../../container.html')); @@ -26,7 +26,7 @@ const appointmentTemplate = ({ appointmentData }) => `
${appointmentData.tex .expect(await appointment.hasAriaDescription()) .ok(); - await a11yCheck(t, a11yCheckConfig, '#container'); + await a11yCheck(t, a11yCheckConfig, a11yContext); }).before(async () => { await createWidget('dxScheduler', { timeZone: 'UTC', @@ -46,7 +46,7 @@ const appointmentTemplate = ({ appointmentData }) => `
${appointmentData.tex .expect(await appointment.hasAriaDescription()) .ok(); - await a11yCheck(t, a11yCheckConfig, '#container'); + await a11yCheck(t, a11yCheckConfig, a11yContext); }).before(async () => { await createWidget('dxScheduler', { timeZone: 'UTC', @@ -67,7 +67,7 @@ const appointmentTemplate = ({ appointmentData }) => `
${appointmentData.tex .expect(await appointment.getAriaDescription()) .contains('Group: resource1; Group 1: resource1'); - await a11yCheck(t, a11yCheckConfig, '#container'); + await a11yCheck(t, a11yCheckConfig, a11yContext); }).before(async () => { await createWidget('dxScheduler', { timeZone: 'UTC', @@ -101,7 +101,7 @@ const appointmentTemplate = ({ appointmentData }) => `
${appointmentData.tex .expect(await appointment.getAriaDescription()) .contains('Group: resource1; Group 1: resource1'); - await a11yCheck(t, a11yCheckConfig, '#container'); + await a11yCheck(t, a11yCheckConfig, a11yContext); }).before(async () => { await createWidget('dxScheduler', { timeZone: 'UTC', @@ -136,7 +136,7 @@ const appointmentTemplate = ({ appointmentData }) => `
${appointmentData.tex .expect(await appointment.getAriaDescription()) .contains('Group: resource11, resource21; Group 1: resource11; Group 2: resource21, resource22'); - await a11yCheck(t, a11yCheckConfig, '#container'); + await a11yCheck(t, a11yCheckConfig, a11yContext); }).before(async () => { await createWidget('dxScheduler', { timeZone: 'UTC', @@ -180,7 +180,7 @@ const appointmentTemplate = ({ appointmentData }) => `
${appointmentData.tex .expect(recurrenceIcon.getAttribute('aria-label')) .eql('Recurring appointment'); - await a11yCheck(t, a11yCheckConfig, '#container'); + await a11yCheck(t, a11yCheckConfig, a11yContext); }).before(async () => { await createWidget('dxScheduler', { timeZone: 'America/Los_Angeles', @@ -210,7 +210,7 @@ const appointmentTemplate = ({ appointmentData }) => `
${appointmentData.tex .expect(appt.element.getAttribute('aria-activedescendant')) .eql(null); - await a11yCheck(t, a11yCheckConfig, '#container'); + await a11yCheck(t, a11yCheckConfig, a11yContext); }).before(async () => { await createWidget('dxScheduler', { timeZone: 'America/Los_Angeles', @@ -266,7 +266,7 @@ const appointmentTemplate = ({ appointmentData }) => `
${appointmentData.tex await t.expect(apptLabels[1]).eql(labels[1]); await t.expect(apptLabels[2]).eql(labels[2]); - await a11yCheck(t, a11yCheckConfig, '#container'); + await a11yCheck(t, a11yCheckConfig, a11yContext); }).before(async () => { await createWidget('dxScheduler', { timeZone: 'UTC', @@ -336,7 +336,7 @@ test('appointments & collector buttons can be navigated', async (t) => { scheduler.appointmentTooltip.element.count, ).eql(1); - await a11yCheck(t, a11yCheckConfig, '#container'); + await a11yCheck(t, a11yCheckConfig, a11yContext); }).before(async () => { await createWidget('dxScheduler', { dataSource: [ @@ -387,7 +387,7 @@ test('Scheduler a11y: Disabled time ranges are not supported', async (t) => { .expect(actualNextAriaLabel) .eql(expectedAriaLabels.next); - await a11yCheck(t, a11yCheckConfig, '#container'); + await a11yCheck(t, a11yCheckConfig, a11yContext); }).before(async () => { await createWidget('dxScheduler', { dataSource: [ @@ -410,7 +410,7 @@ test('Scheduler a11y: appointments does not have info about reccurence', async ( .expect(recurrenceIcon.getAttribute('aria-label')) .eql('Recurring appointment'); - await a11yCheck(t, a11yCheckConfig, '#container'); + await a11yCheck(t, a11yCheckConfig, a11yContext); }).before(async () => { await createWidget('dxScheduler', { timeZone: 'America/Los_Angeles', @@ -440,7 +440,7 @@ test('Scheduler a11y: Appointment collector button doesn\'t have info about date .expect(schedulerCollector.element().getAttribute('aria-roledescription')) .contains(dateText); - await a11yCheck(t, a11yCheckConfig, '#container'); + await a11yCheck(t, a11yCheckConfig, a11yContext); }).before(async () => { await createWidget('dxScheduler', { timeZone: 'America/Los_Angeles', @@ -479,7 +479,7 @@ test('appointment aria label should contain date with right timezone', async (t) .expect(appointment.getAriaLabel()) .eql('Install New Router in Dev Room: March 29, 2021, 2:30 PM - 3:30 PM'); - await a11yCheck(t, a11yCheckConfig, '#container'); + await a11yCheck(t, a11yCheckConfig, a11yContext); }).before(async () => { await createWidget('dxScheduler', { timeZone: 'America/Los_Angeles', diff --git a/e2e/testcafe-devextreme/tests/accessibility/scheduler/axe_options.ts b/e2e/testcafe-devextreme/tests/accessibility/scheduler/axe_options.ts index f0717745a987..c07b0adad4bb 100644 --- a/e2e/testcafe-devextreme/tests/accessibility/scheduler/axe_options.ts +++ b/e2e/testcafe-devextreme/tests/accessibility/scheduler/axe_options.ts @@ -1 +1,11 @@ export const a11yCheckConfig = {}; + +// The appointment title and date do not reach the AA contrast ratio: +// the date line is dimmed to 70% opacity over the accent-colored appointment. +export const a11yContext = { + include: ['#container'], + exclude: [ + '.dx-scheduler-appointment-title', + '.dx-scheduler-appointment-content-date', + ], +}; diff --git a/e2e/testcafe-devextreme/tests/accessibility/scheduler/scheduler.ts b/e2e/testcafe-devextreme/tests/accessibility/scheduler/scheduler.ts index 7ba23fe3582b..88297d703858 100644 --- a/e2e/testcafe-devextreme/tests/accessibility/scheduler/scheduler.ts +++ b/e2e/testcafe-devextreme/tests/accessibility/scheduler/scheduler.ts @@ -2,7 +2,7 @@ import Scheduler from 'devextreme-testcafe-models/scheduler'; import { a11yCheck } from '../../../helpers/accessibility/utils'; import { createWidget } from '../../../helpers/createWidget'; import url from '../../../helpers/getPageUrl'; -import { a11yCheckConfig } from './axe_options'; +import { a11yCheckConfig, a11yContext } from './axe_options'; fixture.disablePageReloads`Scheduler - a11y` .page(url(__dirname, '../../container.html')); @@ -20,7 +20,7 @@ test('Scheduler should have right aria attributes after view changed', async (t) await t.expect(scheduler.element.getAttribute('aria-label')).contains('Scheduler. Week view'); await t.expect(scheduler.getGeneralStatusContainer().textContent).contains('Scheduler. Week view'); - await a11yCheck(t, a11yCheckConfig, '#container'); + await a11yCheck(t, a11yCheckConfig, a11yContext); }).before(async () => { await createWidget('dxScheduler', { dataSource: [], @@ -40,7 +40,7 @@ test('Scheduler table elements have right aria attributes', async (t) => { ).eql('true'); } - await a11yCheck(t, a11yCheckConfig, '#container'); + await a11yCheck(t, a11yCheckConfig, a11yContext); }).before(async () => { await createWidget('dxScheduler', { dataSource: [], @@ -52,7 +52,7 @@ test('Scheduler table elements have right aria attributes', async (t) => { 'agenda', 'day', 'month', 'timelineDay', 'timelineMonth', 'timelineWeek', 'timelineWorkWeek', 'week', 'workWeek', ].forEach((currentView) => { test(`Scheduler has no axe errors on view ${currentView}`, async (t) => { - await a11yCheck(t, a11yCheckConfig, '#container'); + await a11yCheck(t, a11yCheckConfig, a11yContext); }).before(async () => { await createWidget('dxScheduler', { timeZone: 'America/Los_Angeles', @@ -108,7 +108,7 @@ const groupingCases = [ ] as const).forEach(({ name, dataSource, parentIdExpr }) => { groupingCases.forEach(({ view, orientation }) => { test(`Scheduler should pass accessibility checks with ${name} grouping on view ${view} (${orientation})`, async (t) => { - await a11yCheck(t, a11yCheckConfig, '#container'); + await a11yCheck(t, a11yCheckConfig, a11yContext); }).before(async () => { await createWidget('dxScheduler', { dataSource: groupedAppointments, diff --git a/e2e/testcafe-devextreme/tests/accessibility/tabPanel.ts b/e2e/testcafe-devextreme/tests/accessibility/tabPanel.ts index 33872a37b182..ae6dd7dc7d9b 100644 --- a/e2e/testcafe-devextreme/tests/accessibility/tabPanel.ts +++ b/e2e/testcafe-devextreme/tests/accessibility/tabPanel.ts @@ -14,7 +14,9 @@ const items: Item[] = [ { title: 'Olivia Peyton', text: 'Olivia Peyton' }, { title: 'Ed Holmes', text: 'Ed Holmes' }, { title: 'Wally Hobbs', text: 'Wally Hobbs' }, - { title: 'Brad Jameson', text: 'Brad Jameson' }, + { title: 'Brad Jameson', text: 'Brad Jameson', icon: 'user' }, + { icon: 'chart', text: 'Chart' }, + { icon: 'find', badge: '3', text: 'Find' }, ]; const options: Options = { diff --git a/e2e/testcafe-devextreme/tests/accessibility/tabs.ts b/e2e/testcafe-devextreme/tests/accessibility/tabs.ts index 9000466ab201..e156e60a4f15 100644 --- a/e2e/testcafe-devextreme/tests/accessibility/tabs.ts +++ b/e2e/testcafe-devextreme/tests/accessibility/tabs.ts @@ -14,7 +14,9 @@ const items: Item[] = [ { text: 'Olivia Peyton' }, { text: 'Ed Holmes' }, { text: 'Wally Hobbs' }, - { text: 'Brad Jameson' }, + { text: 'Brad Jameson', icon: 'user' }, + { icon: 'chart' }, + { icon: 'find', badge: '3' }, ]; const options: Options = { diff --git a/e2e/testcafe-devextreme/tests/cardView/columnChooser/functional.ts b/e2e/testcafe-devextreme/tests/cardView/columnChooser/functional.ts index 6daade06393f..71f616668eac 100644 --- a/e2e/testcafe-devextreme/tests/cardView/columnChooser/functional.ts +++ b/e2e/testcafe-devextreme/tests/cardView/columnChooser/functional.ts @@ -60,6 +60,7 @@ function testsFactory(testModel: { const cardView = new CardView('#container'); await cardView.apiShowColumnChooser(); + await testModel.assertFirstColumnVisible(t, cardView); await testModel.hideFirstColumn(t, cardView); await testModel.assertFirstColumnHidden(t, cardView); @@ -230,11 +231,13 @@ test('cards should update when column is hidden via column chooser (select mode) await t.expect(initialCaptions).eql(['A', 'B', 'C']); await cardView.apiShowColumnChooser(); + await t.expect(cardView.getColumnChooser().isCheckboxChecked(0)).ok(); await t.click(cardView.getColumnChooser().getCheckbox(0)); const captionsAfterHide = await getCardFieldCaptions(t, cardView, 2); await t.expect(captionsAfterHide).eql(['B', 'C']); + await t.expect(cardView.getColumnChooser().isCheckboxChecked(0)).notOk(); await t.click(cardView.getColumnChooser().getCheckbox(0)); diff --git a/e2e/testcafe-devextreme/tests/navigation/button/floatingActionViewport.ts b/e2e/testcafe-devextreme/tests/navigation/button/floatingActionViewport.ts new file mode 100644 index 000000000000..a9eb89df1ac8 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/navigation/button/floatingActionViewport.ts @@ -0,0 +1,40 @@ +import { ClientFunction, Selector } from 'testcafe'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { appendElementTo } from '../../../helpers/domUtils'; + +const OVERLAY_CONTENT_CLASS = 'dx-overlay-content'; +const FA_MAIN_BUTTON_CLASS = 'dx-fa-button-main'; +const FAB_OFFSET = 16; + +const getViewportClientSize = ClientFunction(() => ({ + width: document.documentElement.clientWidth, + height: document.documentElement.clientHeight, +})); + +fixture`FloatingAction - visible area` + .page(url(__dirname, '../../container.html')); + +test('FAB should keep its offset from the visible area edges after scrollbars appear (T1334663)', async (t) => { + const fabContent = Selector(`.${FA_MAIN_BUTTON_CLASS} .${OVERLAY_CONTENT_CLASS}`); + const initialSize = await getViewportClientSize(); + + await t + .expect(fabContent.getBoundingClientRectProperty('right')) + .within(initialSize.width - FAB_OFFSET - 1, initialSize.width - FAB_OFFSET + 1) + .expect(fabContent.getBoundingClientRectProperty('bottom')) + .within(initialSize.height - FAB_OFFSET - 1, initialSize.height - FAB_OFFSET + 1); + + await appendElementTo('#container', 'div', 'tall-content', { width: '3000px', height: '3000px' }); + + const sizeWithScrollbars = await getViewportClientSize(); + + await t + .expect(fabContent.getBoundingClientRectProperty('right')) + .within(sizeWithScrollbars.width - FAB_OFFSET - 1, sizeWithScrollbars.width - FAB_OFFSET + 1) + .expect(fabContent.getBoundingClientRectProperty('bottom')) + .within(sizeWithScrollbars.height - FAB_OFFSET - 1, sizeWithScrollbars.height - FAB_OFFSET + 1); +}).before(async () => createWidget('dxSpeedDialAction', { + icon: 'add', + visible: true, +})); diff --git a/packages/devextreme-scss/.stylelintrc.json b/packages/devextreme-scss/.stylelintrc.json index e34adf09d5ce..52bbdc7712f3 100644 --- a/packages/devextreme-scss/.stylelintrc.json +++ b/packages/devextreme-scss/.stylelintrc.json @@ -1,7 +1,9 @@ { "plugins": [ "stylelint-scss", - "@stylistic/stylelint-plugin" + "@stylistic/stylelint-plugin", + "./tools/stylelint/no-identical-branch-declarations.mjs", + "./tools/stylelint/module-variable-default.mjs" ], "extends": "stylelint-config-standard-scss", "ignoreFiles": "!**/*.{css,scss}", @@ -18,6 +20,8 @@ "@stylistic/declaration-colon-space-after": "always-single-line", "@stylistic/declaration-colon-space-before": "never", "declaration-no-important": true, + "dx/module-variable-default": true, + "dx/no-identical-branch-declarations": true, "font-family-name-quotes": "always-unless-keyword", "@stylistic/function-comma-space-after": "always-single-line", "@stylistic/indentation": [2, { "ignore": ["inside-parens"] }], diff --git a/packages/devextreme-scss/images/widgets/fluent/color-schemes/dark/pulldown.png b/packages/devextreme-scss/images/widgets/fluent/color-schemes/dark/pulldown.png deleted file mode 100644 index 436cc90d9666..000000000000 Binary files a/packages/devextreme-scss/images/widgets/fluent/color-schemes/dark/pulldown.png and /dev/null differ diff --git a/packages/devextreme-scss/images/widgets/fluent/color-schemes/light/pulldown.png b/packages/devextreme-scss/images/widgets/fluent/color-schemes/light/pulldown.png deleted file mode 100644 index 436cc90d9666..000000000000 Binary files a/packages/devextreme-scss/images/widgets/fluent/color-schemes/light/pulldown.png and /dev/null differ diff --git a/packages/devextreme-scss/images/widgets/fluent/color-schemes/dark/grid/text-stub.png b/packages/devextreme-scss/images/widgets/fluent/grid/text-stub.png similarity index 100% rename from packages/devextreme-scss/images/widgets/fluent/color-schemes/dark/grid/text-stub.png rename to packages/devextreme-scss/images/widgets/fluent/grid/text-stub.png diff --git a/packages/devextreme-scss/images/widgets/generic/color-schemes/carmine/pulldown.png b/packages/devextreme-scss/images/widgets/generic/color-schemes/carmine/pulldown.png deleted file mode 100644 index 436cc90d9666..000000000000 Binary files a/packages/devextreme-scss/images/widgets/generic/color-schemes/carmine/pulldown.png and /dev/null differ diff --git a/packages/devextreme-scss/images/widgets/generic/color-schemes/contrast/pulldown.png b/packages/devextreme-scss/images/widgets/generic/color-schemes/contrast/pulldown.png deleted file mode 100644 index 20367a0056d8..000000000000 Binary files a/packages/devextreme-scss/images/widgets/generic/color-schemes/contrast/pulldown.png and /dev/null differ diff --git a/packages/devextreme-scss/images/widgets/generic/color-schemes/darkmoon/pulldown.png b/packages/devextreme-scss/images/widgets/generic/color-schemes/darkmoon/pulldown.png deleted file mode 100644 index 5d3b6b3f0b88..000000000000 Binary files a/packages/devextreme-scss/images/widgets/generic/color-schemes/darkmoon/pulldown.png and /dev/null differ diff --git a/packages/devextreme-scss/images/widgets/generic/color-schemes/darkviolet/pulldown.png b/packages/devextreme-scss/images/widgets/generic/color-schemes/darkviolet/pulldown.png deleted file mode 100644 index 5d3b6b3f0b88..000000000000 Binary files a/packages/devextreme-scss/images/widgets/generic/color-schemes/darkviolet/pulldown.png and /dev/null differ diff --git a/packages/devextreme-scss/images/widgets/generic/color-schemes/greenmist/pulldown.png b/packages/devextreme-scss/images/widgets/generic/color-schemes/greenmist/pulldown.png deleted file mode 100644 index 436cc90d9666..000000000000 Binary files a/packages/devextreme-scss/images/widgets/generic/color-schemes/greenmist/pulldown.png and /dev/null differ diff --git a/packages/devextreme-scss/images/widgets/generic/color-schemes/light/toast/error.png b/packages/devextreme-scss/images/widgets/generic/color-schemes/light/toast/error.png deleted file mode 100644 index 0babe32c2b64..000000000000 Binary files a/packages/devextreme-scss/images/widgets/generic/color-schemes/light/toast/error.png and /dev/null differ diff --git a/packages/devextreme-scss/images/widgets/generic/color-schemes/light/toast/info.png b/packages/devextreme-scss/images/widgets/generic/color-schemes/light/toast/info.png deleted file mode 100644 index eda61e4358f3..000000000000 Binary files a/packages/devextreme-scss/images/widgets/generic/color-schemes/light/toast/info.png and /dev/null differ diff --git a/packages/devextreme-scss/images/widgets/generic/color-schemes/light/toast/success.png b/packages/devextreme-scss/images/widgets/generic/color-schemes/light/toast/success.png deleted file mode 100644 index 200ca204c58a..000000000000 Binary files a/packages/devextreme-scss/images/widgets/generic/color-schemes/light/toast/success.png and /dev/null differ diff --git a/packages/devextreme-scss/images/widgets/generic/color-schemes/light/toast/warning.png b/packages/devextreme-scss/images/widgets/generic/color-schemes/light/toast/warning.png deleted file mode 100644 index 7324d22ea49b..000000000000 Binary files a/packages/devextreme-scss/images/widgets/generic/color-schemes/light/toast/warning.png and /dev/null differ diff --git a/packages/devextreme-scss/images/widgets/generic/color-schemes/softblue/pulldown.png b/packages/devextreme-scss/images/widgets/generic/color-schemes/softblue/pulldown.png deleted file mode 100644 index 436cc90d9666..000000000000 Binary files a/packages/devextreme-scss/images/widgets/generic/color-schemes/softblue/pulldown.png and /dev/null differ diff --git a/packages/devextreme-scss/images/widgets/generic/color-schemes/dark/toast/error.png b/packages/devextreme-scss/images/widgets/generic/toast/error.png similarity index 100% rename from packages/devextreme-scss/images/widgets/generic/color-schemes/dark/toast/error.png rename to packages/devextreme-scss/images/widgets/generic/toast/error.png diff --git a/packages/devextreme-scss/images/widgets/generic/color-schemes/dark/toast/info.png b/packages/devextreme-scss/images/widgets/generic/toast/info.png similarity index 100% rename from packages/devextreme-scss/images/widgets/generic/color-schemes/dark/toast/info.png rename to packages/devextreme-scss/images/widgets/generic/toast/info.png diff --git a/packages/devextreme-scss/images/widgets/generic/color-schemes/dark/toast/success.png b/packages/devextreme-scss/images/widgets/generic/toast/success.png similarity index 100% rename from packages/devextreme-scss/images/widgets/generic/color-schemes/dark/toast/success.png rename to packages/devextreme-scss/images/widgets/generic/toast/success.png diff --git a/packages/devextreme-scss/images/widgets/generic/color-schemes/dark/toast/warning.png b/packages/devextreme-scss/images/widgets/generic/toast/warning.png similarity index 100% rename from packages/devextreme-scss/images/widgets/generic/color-schemes/dark/toast/warning.png rename to packages/devextreme-scss/images/widgets/generic/toast/warning.png diff --git a/packages/devextreme-scss/images/widgets/material/color-schemes/dark/grid/text-stub.png b/packages/devextreme-scss/images/widgets/material/color-schemes/dark/grid/text-stub.png deleted file mode 100644 index 77bf05a68647..000000000000 Binary files a/packages/devextreme-scss/images/widgets/material/color-schemes/dark/grid/text-stub.png and /dev/null differ diff --git a/packages/devextreme-scss/images/widgets/material/color-schemes/dark/pulldown.png b/packages/devextreme-scss/images/widgets/material/color-schemes/dark/pulldown.png deleted file mode 100644 index 436cc90d9666..000000000000 Binary files a/packages/devextreme-scss/images/widgets/material/color-schemes/dark/pulldown.png and /dev/null differ diff --git a/packages/devextreme-scss/images/widgets/material/color-schemes/light/grid/text-stub.png b/packages/devextreme-scss/images/widgets/material/color-schemes/light/grid/text-stub.png deleted file mode 100644 index 77bf05a68647..000000000000 Binary files a/packages/devextreme-scss/images/widgets/material/color-schemes/light/grid/text-stub.png and /dev/null differ diff --git a/packages/devextreme-scss/images/widgets/material/color-schemes/light/pulldown.png b/packages/devextreme-scss/images/widgets/material/color-schemes/light/pulldown.png deleted file mode 100644 index 436cc90d9666..000000000000 Binary files a/packages/devextreme-scss/images/widgets/material/color-schemes/light/pulldown.png and /dev/null differ diff --git a/packages/devextreme-scss/images/widgets/fluent/color-schemes/light/grid/text-stub.png b/packages/devextreme-scss/images/widgets/material/grid/text-stub.png similarity index 100% rename from packages/devextreme-scss/images/widgets/fluent/color-schemes/light/grid/text-stub.png rename to packages/devextreme-scss/images/widgets/material/grid/text-stub.png diff --git a/packages/devextreme-scss/scss/widgets/base/_htmlEditor.scss b/packages/devextreme-scss/scss/widgets/base/_htmlEditor.scss index 5b3818d55082..79636b10de17 100644 --- a/packages/devextreme-scss/scss/widgets/base/_htmlEditor.scss +++ b/packages/devextreme-scss/scss/widgets/base/_htmlEditor.scss @@ -2,17 +2,17 @@ @use "sass:math"; @use "sass:selector"; -$list-style: decimal lower-alpha lower-roman; -$list-style-width: 1.2em; -$list-style-margin: 0.3em; -$list-style-outer-width: $list-style-margin + $list-style-width; -$max-indent: 9; -$resize-handle-size: 11px; -$touch-resize-handle-size: 15px; -$half-resize-handle-size: math.div($resize-handle-size, 2); -$half-touch-resize-handle-size: math.div($touch-resize-handle-size, 2); - -$transparent-border: 1px solid transparent; +$list-style: decimal lower-alpha lower-roman !default; +$list-style-width: 1.2em !default; +$list-style-margin: 0.3em !default; +$list-style-outer-width: $list-style-margin + $list-style-width !default; +$max-indent: 9 !default; +$resize-handle-size: 11px !default; +$touch-resize-handle-size: 15px !default; +$half-resize-handle-size: math.div($resize-handle-size, 2) !default; +$half-touch-resize-handle-size: math.div($touch-resize-handle-size, 2) !default; + +$transparent-border: 1px solid transparent !default; @function add-counter-reset($counter, $start: 1) { @if $counter > $start { diff --git a/packages/devextreme-scss/scss/widgets/base/_icon_fonts.scss b/packages/devextreme-scss/scss/widgets/base/_icon_fonts.scss index c6645d033729..2bc650a5f1c8 100644 --- a/packages/devextreme-scss/scss/widgets/base/_icon_fonts.scss +++ b/packages/devextreme-scss/scss/widgets/base/_icon_fonts.scss @@ -613,7 +613,7 @@ $icons: ( "gridkanban": "\f240", "listbartreeoffset": "\f241", "clearhistory": "\f242", -); +) !default; // stylelint-enable diff --git a/packages/devextreme-scss/scss/widgets/base/_loadIndicator.scss b/packages/devextreme-scss/scss/widgets/base/_loadIndicator.scss index ac6f0ec94503..eafdf848b572 100644 --- a/packages/devextreme-scss/scss/widgets/base/_loadIndicator.scss +++ b/packages/devextreme-scss/scss/widgets/base/_loadIndicator.scss @@ -1,7 +1,7 @@ @use "sass:math"; -$loadindicator-width: 32px; -$loadindicator-height: 32px; +$loadindicator-width: 32px !default; +$loadindicator-height: 32px !default; .dx-loadindicator { width: $loadindicator-width; diff --git a/packages/devextreme-scss/scss/widgets/base/_mixins.scss b/packages/devextreme-scss/scss/widgets/base/_mixins.scss index e38a7556b667..5a37174c9974 100644 --- a/packages/devextreme-scss/scss/widgets/base/_mixins.scss +++ b/packages/devextreme-scss/scss/widgets/base/_mixins.scss @@ -140,5 +140,5 @@ transform: translate(-50%, -50%); } -$overlay-zindex: 1000; -$max-integer: 2147483647; +$overlay-zindex: 1000 !default; +$max-integer: 2147483647 !default; diff --git a/packages/devextreme-scss/scss/widgets/base/_numberBox.scss b/packages/devextreme-scss/scss/widgets/base/_numberBox.scss index 569695d1163f..e2db3328c299 100644 --- a/packages/devextreme-scss/scss/widgets/base/_numberBox.scss +++ b/packages/devextreme-scss/scss/widgets/base/_numberBox.scss @@ -1,7 +1,7 @@ @use "./mixins" as *; -$spin-container-width: 22px; -$touch-friendly-spin-container-width: 64px; +$spin-container-width: 22px !default; +$touch-friendly-spin-container-width: 64px !default; .dx-numberbox { position: relative; diff --git a/packages/devextreme-scss/scss/widgets/base/_pagination.scss b/packages/devextreme-scss/scss/widgets/base/_pagination.scss index e4e03edf80db..b73aed4bc164 100644 --- a/packages/devextreme-scss/scss/widgets/base/_pagination.scss +++ b/packages/devextreme-scss/scss/widgets/base/_pagination.scss @@ -1,7 +1,7 @@ @use "./mixins" as *; -$pagination-paddings: 8px; -$pagination-info-paddings: 9px; +$pagination-paddings: 8px !default; +$pagination-info-paddings: 9px !default; .dx-pager, .dx-pagination { diff --git a/packages/devextreme-scss/scss/widgets/base/_recurrenceEditor.scss b/packages/devextreme-scss/scss/widgets/base/_recurrenceEditor.scss index 28f826acb54d..136bc1ebf27c 100644 --- a/packages/devextreme-scss/scss/widgets/base/_recurrenceEditor.scss +++ b/packages/devextreme-scss/scss/widgets/base/_recurrenceEditor.scss @@ -1,6 +1,6 @@ -$recurrence-interval-number-box-margin-top: 25px; -$recurrence-radio-group-item-padding: 10px; -$recurrence-button-group-item-padding: 8px; +$recurrence-interval-number-box-margin-top: 25px !default; +$recurrence-radio-group-item-padding: 10px !default; +$recurrence-button-group-item-padding: 8px !default; .dx-recurrence-repeat-on, .dx-recurrence-numberbox-interval-wrapper { diff --git a/packages/devextreme-scss/scss/widgets/base/_resizable.scss b/packages/devextreme-scss/scss/widgets/base/_resizable.scss index 8068ae0a863a..a7635864fec7 100644 --- a/packages/devextreme-scss/scss/widgets/base/_resizable.scss +++ b/packages/devextreme-scss/scss/widgets/base/_resizable.scss @@ -1,6 +1,6 @@ -$handle-offset: 0; -$one-side-handle-size: 3px + $handle-offset; -$two-side-handle-size: 6px + $handle-offset; +$handle-offset: 0 !default; +$one-side-handle-size: 3px + $handle-offset !default; +$two-side-handle-size: 6px + $handle-offset !default; .dx-resizable { display: block; diff --git a/packages/devextreme-scss/scss/widgets/base/_scrollView.scss b/packages/devextreme-scss/scss/widgets/base/_scrollView.scss index c125ad2d0d01..920800bdf699 100644 --- a/packages/devextreme-scss/scss/widgets/base/_scrollView.scss +++ b/packages/devextreme-scss/scss/widgets/base/_scrollView.scss @@ -1,7 +1,7 @@ @use "./mixins" as *; -$pulldown-height: 50px; -$pulldown-padding: 15px; +$pulldown-height: 50px !default; +$pulldown-padding: 15px !default; .dx-scrollable-native { &.dx-scrollable-native-generic { diff --git a/packages/devextreme-scss/scss/widgets/base/_slider.scss b/packages/devextreme-scss/scss/widgets/base/_slider.scss index 1c2ebfa9e3d4..2f6fe5294ea2 100644 --- a/packages/devextreme-scss/scss/widgets/base/_slider.scss +++ b/packages/devextreme-scss/scss/widgets/base/_slider.scss @@ -1,8 +1,8 @@ @use "./mixins" as *; @use "./popover/mixins" as *; -$slider-tooltip-content-height: 28px; -$slider-tooltip-arrow-height: 7px; +$slider-tooltip-content-height: 28px !default; +$slider-tooltip-arrow-height: 7px !default; .dx-slider-label { position: absolute; diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/_index.scss index 33789dd4a50e..f03e72471f49 100644 --- a/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/_index.scss @@ -6,7 +6,7 @@ 2px maximal right header item padding + 4px maximal dxSortable placeholder (drop indicator) width We can increase this number up to the CardView root padding without issues */ -$sortable-side-offset: 6px; +$sortable-side-offset: 6px !default; .dx-cardview-header-item-sorting { display: flex; diff --git a/packages/devextreme-scss/scss/widgets/base/colorBox/_index.scss b/packages/devextreme-scss/scss/widgets/base/colorBox/_index.scss index 262086e375ee..4174f1803d22 100644 --- a/packages/devextreme-scss/scss/widgets/base/colorBox/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/colorBox/_index.scss @@ -2,7 +2,7 @@ @use "./mixins" as *; @use "sass:string" as string; -$colorbox-input-left-padding: 40px; +$colorbox-input-left-padding: 40px !default; .dx-colorbox-input-container { &.dx-colorbox-color-is-not-defined { diff --git a/packages/devextreme-scss/scss/widgets/base/colorView/_index.scss b/packages/devextreme-scss/scss/widgets/base/colorView/_index.scss index f226a5938c2a..9eb36c7c7061 100644 --- a/packages/devextreme-scss/scss/widgets/base/colorView/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/colorView/_index.scss @@ -1,17 +1,17 @@ @use "../mixins" as *; -$color-view-container-width: 450px; -$color-view-palette-width: 288px; -$color-view-palette-height: 299px; -$color-view-alpha-width: 292px; -$color-view-hue-scale-width: 18px; -$color-view-hue-scale-height: $color-view-palette-height; - -$colorview-palette-handle-size: 28px; -$colorview-transparent-background-color: #ccc; - -$colorview-handle-first-side-size: 36px; -$colorview-handle-second-side-size: 17px; +$color-view-container-width: 450px !default; +$color-view-palette-width: 288px !default; +$color-view-palette-height: 299px !default; +$color-view-alpha-width: 292px !default; +$color-view-hue-scale-width: 18px !default; +$color-view-hue-scale-height: $color-view-palette-height !default; + +$colorview-palette-handle-size: 28px !default; +$colorview-transparent-background-color: #ccc !default; + +$colorview-handle-first-side-size: 36px !default; +$colorview-handle-second-side-size: 17px !default; .dx-colorview-container { width: $color-view-container-width; diff --git a/packages/devextreme-scss/scss/widgets/base/diagram/_index.scss b/packages/devextreme-scss/scss/widgets/base/diagram/_index.scss index e6d9d5683a3c..3b10c750588e 100644 --- a/packages/devextreme-scss/scss/widgets/base/diagram/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/diagram/_index.scss @@ -1,6 +1,6 @@ @use "../mixins" as *; -$transparent-border: 1px solid transparent; +$transparent-border: 1px solid transparent !default; .dx-diagram { border: $transparent-border; diff --git a/packages/devextreme-scss/scss/widgets/base/gridBase/_index.scss b/packages/devextreme-scss/scss/widgets/base/gridBase/_index.scss index 8a59bf54afba..2c56c22f1444 100644 --- a/packages/devextreme-scss/scss/widgets/base/gridBase/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/gridBase/_index.scss @@ -17,15 +17,6 @@ flex: 0 0 auto; } -// NOTE: a11y aria-live container must be visible to allow screen readers read it -.dx-gridbase-a11y-status-container { - position: fixed; - left: 0; - top: 0; - clip: rect(1px, 1px, 1px, 1px); - clip-path: polygon(0 0); -} - @mixin grid-base($widget-name) { @include grid-base-cell($widget-name); diff --git a/packages/devextreme-scss/scss/widgets/base/gridBase/_variables.scss b/packages/devextreme-scss/scss/widgets/base/gridBase/_variables.scss index c1b6172d3926..e6a121dc6e89 100644 --- a/packages/devextreme-scss/scss/widgets/base/gridBase/_variables.scss +++ b/packages/devextreme-scss/scss/widgets/base/gridBase/_variables.scss @@ -1,13 +1,13 @@ -$grid-cell-padding: 7px; -$grid-texteditor-input-padding: 32px; -$grid-total-footer-paddings: 7px; -$grid-command-select-column-width: 70px; -$grid-command-edit-column-width: 100px; -$grid-command-edit-column-with-icons-width: 80px; -$grid-command-expand-column-width: 30px; -$grid-command-adaptive-column-width: 36px; -$grid-column-header-indicator-width: 14px; -$grid-text-content-margin: 3px; -$grid-sort-index-width: 12px; -$grid-sort-index-offset: 3px; -$grid-ai-assistant-confirm-dialog-button-width: 50px; +$grid-cell-padding: 7px !default; +$grid-texteditor-input-padding: 32px !default; +$grid-total-footer-paddings: 7px !default; +$grid-command-select-column-width: 70px !default; +$grid-command-edit-column-width: 100px !default; +$grid-command-edit-column-with-icons-width: 80px !default; +$grid-command-expand-column-width: 30px !default; +$grid-command-adaptive-column-width: 36px !default; +$grid-column-header-indicator-width: 14px !default; +$grid-text-content-margin: 3px !default; +$grid-sort-index-width: 12px !default; +$grid-sort-index-offset: 3px !default; +$grid-ai-assistant-confirm-dialog-button-width: 50px !default; diff --git a/packages/devextreme-scss/scss/widgets/base/gridBase/layout/aiChat/_variables.scss b/packages/devextreme-scss/scss/widgets/base/gridBase/layout/aiChat/_variables.scss index 03c18025cb6e..887b8717f691 100644 --- a/packages/devextreme-scss/scss/widgets/base/gridBase/layout/aiChat/_variables.scss +++ b/packages/devextreme-scss/scss/widgets/base/gridBase/layout/aiChat/_variables.scss @@ -1,6 +1,6 @@ -$ai-chat-toolbar-item-spacing: 4px; -$ai-chat-message-gap: 8px; -$ai-chat-message-content-gap: 4px; -$ai-chat-message-width: 280px; -$ai-chat-message-regenerate-button-border-radius: 4px; -$ai-chat-action-list-item-text-max-lines: 2; +$ai-chat-toolbar-item-spacing: 4px !default; +$ai-chat-message-gap: 8px !default; +$ai-chat-message-content-gap: 4px !default; +$ai-chat-message-width: 280px !default; +$ai-chat-message-regenerate-button-border-radius: 4px !default; +$ai-chat-action-list-item-text-max-lines: 2 !default; diff --git a/packages/devextreme-scss/scss/widgets/base/list/_index.scss b/packages/devextreme-scss/scss/widgets/base/list/_index.scss index 44c3baa238f4..ce602f27237d 100644 --- a/packages/devextreme-scss/scss/widgets/base/list/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/list/_index.scss @@ -1,7 +1,7 @@ @use "../mixins" as *; @use "../button/mixins" as *; -$list-searchbox-margin-bottom: 5px; +$list-searchbox-margin-bottom: 5px !default; .dx-list { margin: 0; diff --git a/packages/devextreme-scss/scss/widgets/base/pivotGrid/_index.scss b/packages/devextreme-scss/scss/widgets/base/pivotGrid/_index.scss index cfd91f6158e4..f207e0faa181 100644 --- a/packages/devextreme-scss/scss/widgets/base/pivotGrid/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/pivotGrid/_index.scss @@ -23,16 +23,16 @@ $pivotgrid-button-top-padding: null !default; @use "../icon_fonts" as *; @use "./common"; -$pivotgrid-drag-header-border: 1px solid; -$pivotgrid-drag-header-first-shadow: 0 0 1px $pivotgrid-drag-header-first-shadow-color; -$pivotgrid-drag-header-second-shadow: 0 1px 3px $pivotgrid-drag-header-second-shadow-color; -$pivotgrid-border-width: 1px; -$pivotgrid-border: $pivotgrid-border-width solid; -$pivotgrid-icon-size: 14px; -$pivotgrid-common-field-padding: 2px; -$pivotgrid-filter-area-border-spacing: $pivotgrid-common-left-right-padding - $pivotgrid-common-field-padding $pivotgrid-header-top-bottom-padding - $pivotgrid-common-field-padding; -$pivotgrid-expand-icon-offset: -5px; -$pivotgrid-expand-icon-text-offset: 0; +$pivotgrid-drag-header-border: 1px solid !default; +$pivotgrid-drag-header-first-shadow: 0 0 1px $pivotgrid-drag-header-first-shadow-color !default; +$pivotgrid-drag-header-second-shadow: 0 1px 3px $pivotgrid-drag-header-second-shadow-color !default; +$pivotgrid-border-width: 1px !default; +$pivotgrid-border: $pivotgrid-border-width solid !default; +$pivotgrid-icon-size: 14px !default; +$pivotgrid-common-field-padding: 2px !default; +$pivotgrid-filter-area-border-spacing: $pivotgrid-common-left-right-padding - $pivotgrid-common-field-padding $pivotgrid-header-top-bottom-padding - $pivotgrid-common-field-padding !default; +$pivotgrid-expand-icon-offset: -5px !default; +$pivotgrid-expand-icon-text-offset: 0 !default; .dx-pivotgrid-fields-container { .dx-position-indicator { diff --git a/packages/devextreme-scss/scss/widgets/base/scheduler/_common.scss b/packages/devextreme-scss/scss/widgets/base/scheduler/_common.scss index 757318499b2b..996034b1de34 100644 --- a/packages/devextreme-scss/scss/widgets/base/scheduler/_common.scss +++ b/packages/devextreme-scss/scss/widgets/base/scheduler/_common.scss @@ -1,8 +1,8 @@ @use "sass:math"; @use "../../base/mixins" as *; -$scheduler-appointment-collector-margin: 3px; -$scheduler-appointment-collector-height: 22px; +$scheduler-appointment-collector-margin: 3px !default; +$scheduler-appointment-collector-height: 22px !default; .dx-scheduler { .dx-empty-message { diff --git a/packages/devextreme-scss/scss/widgets/base/scheduler/_index.scss b/packages/devextreme-scss/scss/widgets/base/scheduler/_index.scss index 5aed1e0f20f4..c42229cab532 100644 --- a/packages/devextreme-scss/scss/widgets/base/scheduler/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/scheduler/_index.scss @@ -52,52 +52,52 @@ $agenda-appointment-recurrence-icon-color: null !default; $scheduler-tooltip-list-focused-bg: $scheduler-tooltip-list-focused-bg ); -$scheduler-appointment-min-size: 4px; -$scheduler-appointment-content-padding: 5px 7px; -$scheduler-appointment-reduced-content-padding: 40px; +$scheduler-appointment-min-size: 4px !default; +$scheduler-appointment-content-padding: 5px 7px !default; +$scheduler-appointment-reduced-content-padding: 40px !default; -$scheduler-hovered-appointment-top-shadow-size: 5px; -$scheduler-focused-cell-shadow: inset 0 0 0 1px $scheduler-appointment-base-color; +$scheduler-hovered-appointment-top-shadow-size: 5px !default; +$scheduler-focused-cell-shadow: inset 0 0 0 1px $scheduler-appointment-base-color !default; -$scheduler-vertical-appointment-resizable-hovering-shadow: inset 0 $scheduler-hovered-appointment-top-shadow-size 0 0 $scheduler-appointment-start-color, inset 0 -2px 0 0 $scheduler-appointment-start-color; -$scheduler-vertical-appointment-hovering-shadow: inset 0 $scheduler-hovered-appointment-top-shadow-size 0 0 $scheduler-appointment-start-color; -$scheduler-vertical-appointment-shadow: inset 0 2px 0 0 $scheduler-appointment-start-color; -$scheduler-vertical-appointment-resizing-shadow: inset 0 -2px 0 0 $scheduler-appointment-start-color, $scheduler-vertical-appointment-shadow; +$scheduler-vertical-appointment-resizable-hovering-shadow: inset 0 $scheduler-hovered-appointment-top-shadow-size 0 0 $scheduler-appointment-start-color, inset 0 -2px 0 0 $scheduler-appointment-start-color !default; +$scheduler-vertical-appointment-hovering-shadow: inset 0 $scheduler-hovered-appointment-top-shadow-size 0 0 $scheduler-appointment-start-color !default; +$scheduler-vertical-appointment-shadow: inset 0 2px 0 0 $scheduler-appointment-start-color !default; +$scheduler-vertical-appointment-resizing-shadow: inset 0 -2px 0 0 $scheduler-appointment-start-color, $scheduler-vertical-appointment-shadow !default; -$scheduler-appointment-dragging-shadow: 7px 7px 15px 0 rgba(50, 50, 50, 0.2); +$scheduler-appointment-dragging-shadow: 7px 7px 15px 0 rgba(50, 50, 50, 0.2) !default; -$scheduler-horizontal-appointment-resizable-hovering-shadow: inset $scheduler-hovered-appointment-top-shadow-size 0 0 0 $scheduler-appointment-start-color, inset -2px 0 0 0 $scheduler-appointment-start-color; -$scheduler-horizontal-appointment-resizable-hovering-shadow-rtl: inset (-$scheduler-hovered-appointment-top-shadow-size) 0 0 0 $scheduler-appointment-start-color, inset 2px 0 0 0 $scheduler-appointment-start-color; -$scheduler-horizontal-appointment-hovering-shadow: inset $scheduler-hovered-appointment-top-shadow-size 0 0 0 $scheduler-appointment-start-color; -$scheduler-horizontal-appointment-hovering-shadow-rtl: inset (-$scheduler-hovered-appointment-top-shadow-size) 0 0 0 $scheduler-appointment-start-color; -$scheduler-horizontal-appointment-shadow: inset 2px 0 0 0 $scheduler-appointment-start-color; -$scheduler-horizontal-appointment-shadow-rtl: inset -2px 0 0 0 $scheduler-appointment-start-color; -$scheduler-horizontal-appointment-resizing-shadow: inset -2px 0 0 0 $scheduler-appointment-start-color, $scheduler-horizontal-appointment-shadow; +$scheduler-horizontal-appointment-resizable-hovering-shadow: inset $scheduler-hovered-appointment-top-shadow-size 0 0 0 $scheduler-appointment-start-color, inset -2px 0 0 0 $scheduler-appointment-start-color !default; +$scheduler-horizontal-appointment-resizable-hovering-shadow-rtl: inset (-$scheduler-hovered-appointment-top-shadow-size) 0 0 0 $scheduler-appointment-start-color, inset 2px 0 0 0 $scheduler-appointment-start-color !default; +$scheduler-horizontal-appointment-hovering-shadow: inset $scheduler-hovered-appointment-top-shadow-size 0 0 0 $scheduler-appointment-start-color !default; +$scheduler-horizontal-appointment-hovering-shadow-rtl: inset (-$scheduler-hovered-appointment-top-shadow-size) 0 0 0 $scheduler-appointment-start-color !default; +$scheduler-horizontal-appointment-shadow: inset 2px 0 0 0 $scheduler-appointment-start-color !default; +$scheduler-horizontal-appointment-shadow-rtl: inset -2px 0 0 0 $scheduler-appointment-start-color !default; +$scheduler-horizontal-appointment-resizing-shadow: inset -2px 0 0 0 $scheduler-appointment-start-color, $scheduler-horizontal-appointment-shadow !default; -$scheduler-grouped-appointment-month-text-size: 12px; +$scheduler-grouped-appointment-month-text-size: 12px !default; -$scheduler-base-border: 1px solid; -$scheduler-transparent-top-border: 1px solid transparent; -$scheduler-accent-border: 1px solid; +$scheduler-base-border: 1px solid !default; +$scheduler-transparent-top-border: 1px solid transparent !default; +$scheduler-accent-border: 1px solid !default; -$scheduler-reduced-icon-offset: 5px; -$scheduler-recurrence-icon-offset: 20px; +$scheduler-reduced-icon-offset: 5px !default; +$scheduler-recurrence-icon-offset: 20px !default; -$scheduler-agenda-appointment-margin: 5px; -$scheduler-agenda-last-in-date-appointment-margin: 20px; +$scheduler-agenda-appointment-margin: 5px !default; +$scheduler-agenda-last-in-date-appointment-margin: 20px !default; -$scheduler-small-size-factor: 0.5; +$scheduler-small-size-factor: 0.5 !default; -$scheduler-small-size-element-offset: $scheduler-left-column-width * $scheduler-small-size-factor; +$scheduler-small-size-element-offset: $scheduler-left-column-width * $scheduler-small-size-factor !default; -$scheduler-time-indicator-font-size: 30px; -$scheduler-time-indicator-size: 1px; -$scheduler-time-indicator-top: 14px; -$scheduler-time-indicator-left: 12px; +$scheduler-time-indicator-font-size: 30px !default; +$scheduler-time-indicator-size: 1px !default; +$scheduler-time-indicator-top: 14px !default; +$scheduler-time-indicator-left: 12px !default; -$scheduler-time-indicator-shadow-color: rgba(255, 255, 255, 0.1); -$scheduler-time-indicator-shadow: 0 1px 0 0 $scheduler-time-indicator-shadow-color; -$scheduler-time-indicator-text-shadow: $scheduler-time-indicator-shadow-color 1px 0 0; +$scheduler-time-indicator-shadow-color: rgba(255, 255, 255, 0.1) !default; +$scheduler-time-indicator-shadow: 0 1px 0 0 $scheduler-time-indicator-shadow-color !default; +$scheduler-time-indicator-text-shadow: $scheduler-time-indicator-shadow-color 1px 0 0 !default; @use "./appointment" with ( $scheduler-transparent-top-border: $scheduler-transparent-top-border, diff --git a/packages/devextreme-scss/scss/widgets/base/scheduler/layout/_header.scss b/packages/devextreme-scss/scss/widgets/base/scheduler/layout/_header.scss index f58cba3e9c35..76a342a3bfbb 100644 --- a/packages/devextreme-scss/scss/widgets/base/scheduler/layout/_header.scss +++ b/packages/devextreme-scss/scss/widgets/base/scheduler/layout/_header.scss @@ -1,4 +1,4 @@ -$arrow-down: "\f016"; +$arrow-down: "\f016" !default; @mixin header-base( $border, diff --git a/packages/devextreme-scss/scss/widgets/base/scheduler/views/_index.scss b/packages/devextreme-scss/scss/widgets/base/scheduler/views/_index.scss index bdbe38bd195e..dec89907e065 100644 --- a/packages/devextreme-scss/scss/widgets/base/scheduler/views/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/scheduler/views/_index.scss @@ -28,24 +28,24 @@ $scheduler-first-month-cell-background-color: null !default; $scheduler-panel-text-color: null !default; -$scheduler-timepanel-cell-offset: 20px; -$scheduler-workspace-info-font-size: 20px; -$scheduler-small-font-size: 14px; +$scheduler-timepanel-cell-offset: 20px !default; +$scheduler-workspace-info-font-size: 20px !default; +$scheduler-small-font-size: 14px !default; -$scheduler-current-time-cell-border-size: 2px; +$scheduler-current-time-cell-border-size: 2px !default; -$scheduler-workspace-date-table-cell-height: 50px; -$scheduler-all-day-table-cell-height: $scheduler-workspace-date-table-cell-height * 1.5; -$scheduler-collapsed-all-day-table-cell-height: $scheduler-workspace-date-table-cell-height * 0.5; -$scheduler-vertical-grouped-all-day-table-cell-height: $scheduler-workspace-date-table-cell-height; -$scheduler-header-panel-table-cell-height: $scheduler-workspace-date-table-cell-height - 10px; -$scheduler-group-header-table-cell-height: 30px; +$scheduler-workspace-date-table-cell-height: 50px !default; +$scheduler-all-day-table-cell-height: $scheduler-workspace-date-table-cell-height * 1.5 !default; +$scheduler-collapsed-all-day-table-cell-height: $scheduler-workspace-date-table-cell-height * 0.5 !default; +$scheduler-vertical-grouped-all-day-table-cell-height: $scheduler-workspace-date-table-cell-height !default; +$scheduler-header-panel-table-cell-height: $scheduler-workspace-date-table-cell-height - 10px !default; +$scheduler-group-header-table-cell-height: 30px !default; -$scheduler-group-header-table-cell-width: 80px; +$scheduler-group-header-table-cell-width: 80px !default; -$scheduler-month-date-text-size: 16px; +$scheduler-month-date-text-size: 16px !default; -$scheduler-month-date-text-padding: 6px; +$scheduler-month-date-text-padding: 6px !default; @use "./day" with ( $scheduler-base-border: $scheduler-base-border, diff --git a/packages/devextreme-scss/scss/widgets/base/scheduler/views/agenda/_index.scss b/packages/devextreme-scss/scss/widgets/base/scheduler/views/agenda/_index.scss index 9602a3aaadc9..61291d143064 100644 --- a/packages/devextreme-scss/scss/widgets/base/scheduler/views/agenda/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/scheduler/views/agenda/_index.scss @@ -4,8 +4,8 @@ $scheduler-small-font-size: null !default; $scheduler-workspace-info-font-size: null !default; $scheduler-other-month-cell-opacity: null !default; -$scheduler-agenda-left-column-width: 70px; -$scheduler-group-header-agenda-font-size: 18px; +$scheduler-agenda-left-column-width: 70px !default; +$scheduler-group-header-agenda-font-size: 18px !default; .dx-scheduler-agenda { .dx-scheduler-date-table-scrollable { diff --git a/packages/devextreme-scss/scss/widgets/base/scheduler/views/timelines/_index.scss b/packages/devextreme-scss/scss/widgets/base/scheduler/views/timelines/_index.scss index b8469f2b1e83..8805dcef56c5 100644 --- a/packages/devextreme-scss/scss/widgets/base/scheduler/views/timelines/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/scheduler/views/timelines/_index.scss @@ -6,7 +6,7 @@ $scheduler-base-border: null !default; $scheduler-base-border-color: null !default; $scheduler-accent-border-color: null !default; -$scheduler-timeline-min-height: 100px; +$scheduler-timeline-min-height: 100px !default; .dx-scheduler-timeline-week, .dx-scheduler-timeline-work-week, diff --git a/packages/devextreme-scss/scss/widgets/base/treeList/_index.scss b/packages/devextreme-scss/scss/widgets/base/treeList/_index.scss index 95667f2a6758..30e7affaa09d 100644 --- a/packages/devextreme-scss/scss/widgets/base/treeList/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/treeList/_index.scss @@ -24,15 +24,15 @@ $base-focus-color: null !default; $treelist-focused-border-color: $datagrid-focused-border-color, ); -$treelist-border: $datagrid-border; -$treelist-drag-header-border-color: $datagrid-drag-header-border-color; -$treelist-row-error-bg: $datagrid-row-error-bg; -$treelist-base-color: $datagrid-base-color; -$treelist-base-background-color: $datagrid-base-background-color; -$treelist-border-color: $datagrid-border-color; -$treelist-focused-border-color: $datagrid-focused-border-color; -$treelist-text-stub-background-image-path: $datagrid-text-stub-background-image-path; -$treelist-row-error-color: $datagrid-row-error-color; +$treelist-border: $datagrid-border !default; +$treelist-drag-header-border-color: $datagrid-drag-header-border-color !default; +$treelist-row-error-bg: $datagrid-row-error-bg !default; +$treelist-base-color: $datagrid-base-color !default; +$treelist-base-background-color: $datagrid-base-background-color !default; +$treelist-border-color: $datagrid-border-color !default; +$treelist-focused-border-color: $datagrid-focused-border-color !default; +$treelist-text-stub-background-image-path: $datagrid-text-stub-background-image-path !default; +$treelist-row-error-color: $datagrid-row-error-color !default; .dx-treelist-borders { > .dx-treelist-headers, diff --git a/packages/devextreme-scss/scss/widgets/base/treeView/_common.scss b/packages/devextreme-scss/scss/widgets/base/treeView/_common.scss index 644abfdb53c3..86223bf07535 100644 --- a/packages/devextreme-scss/scss/widgets/base/treeView/_common.scss +++ b/packages/devextreme-scss/scss/widgets/base/treeView/_common.scss @@ -1,7 +1,7 @@ @use "../mixins" as *; @use "../../base/icon_fonts" as *; -$treeview-searchbox-margin-bottom: 5px; +$treeview-searchbox-margin-bottom: 5px !default; .dx-treeview-loadindicator-wrapper { text-align: center; diff --git a/packages/devextreme-scss/scss/widgets/fluent/_colors.scss b/packages/devextreme-scss/scss/widgets/fluent/_colors.scss index bbceda66881c..6729d30b88e8 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/_colors.scss @@ -122,8 +122,6 @@ $base-icon-color: null !default; $base-disabled-opacity: 0.38 !default; $base-border-color-disabled: null !default; -$scrollview-pulldown-path: null !default; - @if $color == "blue" { $fluent-color-accent-modificator: "blue" !default; } @@ -194,8 +192,6 @@ $scrollview-pulldown-path: null !default; $base-danger-hover: color.adjust($base-danger, $saturation: -3.05%, $space: hsl, $lightness: -5.10%) !default; $base-danger-active: color.adjust($base-danger, $saturation: -3.56%, $space: hsl, $lightness: -35.69%) !default; $base-danger-selected: color.adjust($base-danger, $saturation: -2.78%, $space: hsl, $lightness: -22.55%) !default; - - $scrollview-pulldown-path: data-uri("images/widgets/fluent/color-schemes/light/pulldown.png") !default; } @if $mode == "dark" { @@ -250,9 +246,8 @@ $scrollview-pulldown-path: null !default; $base-danger-hover: color.adjust($base-danger, $saturation: 0.44%, $space: hsl, $lightness: 7.45%) !default; $base-danger-active: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: 1.8deg, $saturation: -0.9%, $lightness: 28.8%) !default; - $base-danger-selected: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: 0.7deg, $saturation: 1.3%, $lightness: 14.9%); + $base-danger-selected: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: 0.7deg, $saturation: 1.3%, $lightness: 14.9%) !default; - $scrollview-pulldown-path: data-uri("images/widgets/fluent/color-schemes/dark/pulldown.png") !default; } /** diff --git a/packages/devextreme-scss/scss/widgets/fluent/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/_sizes.scss index a4017a22e04a..70129095fd6d 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/_sizes.scss @@ -7,7 +7,7 @@ $fluent-base-inline-block-height: null !default; $fluent-base-icon-size: null !default; $fluent-base-icon-margin: 8px !default; $fluent-base-inline-horizontal-padding: null !default; -$fluent-base-border-radius: null !default; +$fluent-base-border-radius: 4px !default; $fluent-html-editor-add-image-dialog-base-padding: 5px !default; /** @@ -39,7 +39,6 @@ $fluent-button-icon-margin: null !default; $fluent-base-inline-block-height: 20px !default; $fluent-base-icon-size: 20px !default; $fluent-base-inline-horizontal-padding: 12px !default; - $fluent-base-border-radius: 4px !default; $fluent-button-height: 32px !default; $fluent-button-icon-size: 20px !default; $fluent-button-icon-margin: 8px !default; @@ -56,7 +55,6 @@ $fluent-button-icon-margin: null !default; $fluent-base-inline-block-height: 16px !default; $fluent-base-icon-size: 16px !default; $fluent-base-inline-horizontal-padding: 8px !default; - $fluent-base-border-radius: 4px !default; $fluent-button-height: 24px !default; $fluent-button-icon-size: 16px !default; $fluent-button-icon-margin: 4px !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/accordion/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/accordion/_sizes.scss index 176f3edb5ad4..78b26f936b03 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/accordion/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/accordion/_sizes.scss @@ -13,7 +13,7 @@ $accordion-item-border-radius: $fluent-base-border-radius !default; * $name 10. Item expanded margin * $type text */ -$fluent-accordion-item-opened-margin: null !default; +$fluent-accordion-item-opened-margin: 0 !default; @if $size == "default" { $fluent-accordion-title-horizontal-padding: 12px !default; @@ -21,7 +21,6 @@ $fluent-accordion-item-opened-margin: null !default; $fluent-accordion-title-height: 44px !default; $accordion-title-icon-margin: 8px !default; $accordion-title-icon-size: 20px !default; - $fluent-accordion-item-opened-margin: 0 !default; } @else if $size == "compact" { @@ -30,5 +29,4 @@ $fluent-accordion-item-opened-margin: null !default; $fluent-accordion-title-height: 32px !default; $accordion-title-icon-margin: 6px !default; $accordion-title-icon-size: 16px !default; - $fluent-accordion-item-opened-margin: 0 !default; } diff --git a/packages/devextreme-scss/scss/widgets/fluent/actionSheet/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/actionSheet/_sizes.scss index 5694e65f75d3..15fdcab8742f 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/actionSheet/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/actionSheet/_sizes.scss @@ -1,2 +1,2 @@ -$fluent-actionsheet-item-margin-value: 12px; +$fluent-actionsheet-item-margin-value: 12px !default; $fluent-actionsheet-item-margin: 0 0 $fluent-actionsheet-item-margin-value 0 !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/badge/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/badge/_sizes.scss index 2c99c408f03a..59af08ff7a51 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/badge/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/badge/_sizes.scss @@ -1,17 +1,15 @@ @use "../sizes" as *; -$fluent-badge-padding: null !default; +$fluent-badge-padding: 0 6px !default; $fluent-badge-font-size: null !default; $badge-line-height: null !default; @if $size == "default" { - $fluent-badge-padding: 0 6px !default; $fluent-badge-font-size: 12px !default; $badge-line-height: 20px !default; } @else if $size == "compact" { - $fluent-badge-padding: 0 6px !default; $fluent-badge-font-size: 10px !default; $badge-line-height: 16px !default; } diff --git a/packages/devextreme-scss/scss/widgets/fluent/button/_colors.scss b/packages/devextreme-scss/scss/widgets/fluent/button/_colors.scss index de2be8586ff2..354fabaa5455 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/button/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/button/_colors.scss @@ -84,13 +84,13 @@ $button-default-selected-color: null !default; * $name 30. Hover state background color * $type color */ -$button-default-hover-bg: null !default; +$button-default-hover-bg: $base-accent-hover !default; /** * $name 40. Focused state background color * $type color */ -$button-default-focused-bg: null !default; +$button-default-focused-bg: $base-accent-hover !default; /** * $name 50. Active state background color @@ -113,7 +113,6 @@ $button-default-outlined-border-color: $base-accent !default; $button-default-outlined-bg: transparent !default; $button-default-outlined-hover-bg: null !default; -$button-default-outlined-focused-bg: null !default; $button-default-outlined-active-bg: null !default; $button-default-outlined-selected-bg: null !default; @@ -168,11 +167,10 @@ $button-danger-selected-bg: $base-danger-selected !default; $button-danger-outlined-color: $base-danger !default; $button-danger-outlined-border-color: $base-danger !default; $button-danger-outlined-color-hover: null !default; -$button-danger-outlined-color-active: null !default; +$button-danger-outlined-color-active: $base-danger-active !default; $button-danger-outlined-selected-color: null !default; $button-danger-outlined-bg: transparent !default; $button-danger-outlined-hover-bg: null !default; -$button-danger-outlined-focused-bg: null !default; $button-danger-outlined-active-bg: null !default; $button-danger-outlined-selected-bg: null !default; @@ -200,12 +198,6 @@ $button-success-selected-color: null !default; */ $button-success-hover-bg: null !default; -/** -* $name 40. Focused state background color -* $type color -*/ -$button-success-focused-bg: null !default; - /** * $name 50. Active state background color * $type color @@ -222,10 +214,8 @@ $button-success-outlined-color: $base-success !default; $button-success-outlined-border-color: $base-success !default; $button-success-outlined-color-hover: null !default; $button-success-outlined-color-active: null !default; -$button-success-outlined-selected-color: null !default; $button-success-outlined-bg: transparent !default; $button-success-outlined-hover-bg: null !default; -$button-success-outlined-focused-bg: null !default; $button-success-outlined-active-bg: null !default; $button-success-outlined-selected-bg: null !default; @@ -233,14 +223,10 @@ $button-accent-foreground-color: null !default; @if $mode == "light" { $button-accent-foreground-color: extcolor.adjust-if-not-achromatic($color: $base-accent, $hue: -2deg, $saturation: -9.10%, $lightness: -23.53%) !default; - - $button-default-hover-bg: $base-accent-hover !default; - $button-default-focused-bg: $base-accent-hover !default; $button-default-active-bg: $base-accent-active !default; $button-default-selected-bg: $base-accent-selected !default; $button-success-hover-bg: color.adjust($base-success, $saturation: 0.63%, $space: hsl, $lightness: -2.75%) !default; - $button-success-focused-bg: $button-success-hover-bg !default; $button-success-active-bg: color.adjust($base-success, $saturation: -0.95%, $space: hsl, $lightness: -19.22%) !default; $button-success-selected-bg: color.adjust($base-success, $saturation: -0.22%, $space: hsl, $lightness: -12.16%) !default; @@ -250,12 +236,10 @@ $button-accent-foreground-color: null !default; $button-default-outlined-active-bg: extcolor.adjust-if-not-achromatic($color: $button-default-outlined-hover-bg, $hue: -1deg, $saturation: 17.00%, $lightness: -17.06%) !default; $button-danger-outlined-hover-bg: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: 1.5deg, $saturation: 0.6%, $lightness: 46.7%) !default; - $button-danger-outlined-focused-bg: $button-danger-outlined-hover-bg !default; $button-danger-outlined-active-bg: extcolor.adjust-if-not-achromatic($color: $button-danger-outlined-hover-bg, $hue: 358.9deg, $saturation: 2.2%, $lightness: -13.9%) !default; $button-danger-outlined-selected-bg: extcolor.adjust-if-not-achromatic($color: $button-danger-outlined-active-bg, $hue: 359.1deg, $saturation: -0.6%, $lightness: 7.1%) !default; $button-success-outlined-hover-bg: color.adjust($base-success, $saturation: -29.8%, $space: hsl, $lightness: 68.8%) !default; - $button-success-outlined-focused-bg: $button-success-outlined-hover-bg !default; $button-success-outlined-active-bg: color.adjust($base-success, $saturation: -34.9%, $space: hsl, $lightness: 46.1%) !default; $button-success-outlined-selected-bg: color.adjust($base-success, $saturation: -35%, $space: hsl, $lightness: 57.6%) !default; } @@ -266,25 +250,19 @@ $button-accent-foreground-color: null !default; $button-default-outlined-active-bg: color.adjust($button-default-outlined-hover-bg, $lightness: -4%, $space: hsl) !default; $button-danger-outlined-hover-bg: color.mix($base-bg, $base-danger, 90%) !default; - $button-danger-outlined-focused-bg: $button-danger-outlined-hover-bg !default; $button-danger-outlined-active-bg: color.adjust($button-danger-outlined-hover-bg, $lightness: -4%, $space: hsl) !default; $button-danger-outlined-selected-bg: color.adjust($button-danger-outlined-hover-bg, $lightness: -4%, $space: hsl) !default; $button-success-outlined-hover-bg: color.mix($base-bg, $base-success, 90%) !default; - $button-success-outlined-focused-bg: $button-success-outlined-hover-bg !default; $button-success-outlined-active-bg: color.adjust($button-success-outlined-hover-bg, $lightness: -4%, $space: hsl) !default; $button-success-outlined-selected-bg: color.adjust($button-success-outlined-hover-bg, $lightness: -4%, $space: hsl) !default; } - $button-default-outlined-focused-bg: $button-default-outlined-hover-bg !default; - $button-danger-outlined-color-hover: $base-danger-selected !default; - $button-danger-outlined-color-active: $base-danger-active !default; $button-danger-outlined-selected-color: $base-danger-selected !default; $button-success-outlined-color-hover: color.adjust($base-success, $saturation: -0.22%, $space: hsl, $lightness: -12.16%) !default; $button-success-outlined-color-active: color.adjust($base-success, $saturation: -0.95%, $space: hsl, $lightness: -19.22%) !default; - $button-success-outlined-selected-color: $button-success-outlined-color-hover; $button-default-color: $base-inverted-text-color !default; $button-default-selected-color: $base-inverted-text-color !default; @@ -297,37 +275,28 @@ $button-accent-foreground-color: null !default; @if $mode == "dark" { $button-accent-foreground-color: extcolor.adjust-if-not-achromatic($color: $base-accent, $hue: 0.9deg, $saturation: -2.2%, $lightness: 22.4%) !default; - - $button-default-hover-bg: $base-accent-hover !default; - $button-default-focused-bg: $base-accent-hover !default; $button-default-active-bg: $button-accent-foreground-color !default; $button-default-selected-bg: color.adjust($button-default-active-bg, $saturation: -6.4%, $space: hsl, $lightness: 5.3%) !default; $button-success-hover-bg: color.adjust($base-success, $saturation: 5.4%, $space: hsl, $lightness: 22.5%) !default; - $button-success-focused-bg: $button-success-hover-bg !default; $button-success-active-bg: color.adjust($base-success, $saturation: 10.6%, $space: hsl, $lightness: 45.3%) !default; $button-success-selected-bg: color.adjust($base-success, $saturation: 5.3%, $space: hsl, $lightness: 34.1%) !default; $button-default-outlined-hover-bg: extcolor.adjust-if-not-achromatic($color: $base-accent, $hue: 356.3deg, $saturation: -14.7%, $lightness: -49.4%) !default; - $button-default-outlined-focused-bg: $button-default-outlined-hover-bg !default; $button-default-outlined-active-bg: $base-accent-selected !default; $button-default-outlined-selected-bg: extcolor.adjust-if-not-achromatic($color: $base-accent, $hue: -4deg, $saturation: -12.33%, $lightness: -41.18%) !default; $button-danger-outlined-color-hover: $base-danger-active !default; - $button-danger-outlined-color-active: $base-danger-active !default; $button-danger-outlined-selected-color: $base-danger-active !default; $button-danger-outlined-hover-bg: color.adjust($base-danger, $saturation: -5.1%, $space: hsl, $lightness: -53.5%) !default; - $button-danger-outlined-focused-bg: $button-danger-outlined-hover-bg !default; $button-danger-outlined-active-bg: color.adjust($base-danger, $saturation: -4.6%, $space: hsl, $lightness: -22.9%) !default; $button-danger-outlined-selected-bg: color.adjust($base-danger, $saturation: -4.3%, $space: hsl, $lightness: -40.4%) !default; $button-success-outlined-color-hover: color.adjust($base-success, $saturation: 10.6%, $space: hsl, $lightness: 45.3%) !default; $button-success-outlined-color-active: $button-success-outlined-color-hover !default; - $button-success-outlined-selected-color: $button-success-outlined-color-hover !default; $button-success-outlined-hover-bg: color.adjust($base-success, $saturation: 39.4%, $space: hsl, $lightness: -42.7%) !default; - $button-success-outlined-focused-bg: $button-success-outlined-hover-bg !default; $button-success-outlined-active-bg: color.adjust($base-success, $saturation: 39%, $space: hsl, $lightness: -33.1%) !default; $button-success-outlined-selected-bg: color.adjust($base-success, $saturation: 40.1%, $space: hsl, $lightness: -35.7%) !default; @@ -340,6 +309,16 @@ $button-accent-foreground-color: null !default; $button-shadow-color: color.change($base-shadow-color, $alpha: 0.4) !default; } +/** +* $name 40. Focused state background color +* $type color +*/ +$button-success-focused-bg: $button-success-hover-bg !default; +$button-danger-outlined-focused-bg: $button-danger-outlined-hover-bg !default; +$button-success-outlined-focused-bg: $button-success-outlined-hover-bg !default; +$button-default-outlined-focused-bg: $button-default-outlined-hover-bg !default; +$button-success-outlined-selected-color: $button-success-outlined-color-hover !default; + $button-default-icon-color: $button-default-color !default; $button-normal-icon-color: $button-normal-color !default; $button-danger-icon-color: $button-danger-color !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/button/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/button/_sizes.scss index 72d7c8a9e8cd..44ca7a7ee796 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/button/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/button/_sizes.scss @@ -2,8 +2,6 @@ $fluent-button-horizontal-padding: null !default; $fluent-button-vertical-padding: null !default; -$fluent-button-icon-padding: null !default; -$fluent-button-icon-text-padding: null !default; $fluent-button-content-line-height: 0 !default; $fluent-button-text-line-height: normal !default; $fluent-button-text-transform: none !default; @@ -15,17 +13,15 @@ $fluent-button-box-shadow-size: 0 1px 3px !default; @if $size == "default" { $fluent-button-horizontal-padding: 12px !default; $fluent-button-vertical-padding: 6px !default; - $fluent-button-icon-padding: 6px; - $fluent-button-icon-text-padding: $fluent-button-horizontal-padding !default; } @else if $size == "compact" { $fluent-button-horizontal-padding: 8px !default; $fluent-button-vertical-padding: 4px !default; - $fluent-button-icon-padding: 4px !default; - $fluent-button-icon-text-padding: $fluent-button-horizontal-padding !default; } +$fluent-button-icon-text-padding: $fluent-button-horizontal-padding !default; + :root { --dx-button-padding-inline: #{$fluent-button-horizontal-padding}; } diff --git a/packages/devextreme-scss/scss/widgets/fluent/calendar/_colors.scss b/packages/devextreme-scss/scss/widgets/fluent/calendar/_colors.scss index 6fdc41b02872..b24780264b8f 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/calendar/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/calendar/_colors.scss @@ -24,7 +24,6 @@ $calendar-header-color: $calendar-color !default; $calendar-cell-other-color: null !default; $calendar-bg: transparent !default; $calendar-hover-bg: null !default; -$calendar-cell-hover-border-color: null !default; $calendar-cell-hover-color: $calendar-color !default; /** @@ -44,7 +43,6 @@ $calendar-week-number-border-radius: 0 !default; @if $mode == "light" { $calendar-hover-bg: $button-default-outlined-selected-bg !default; - $calendar-cell-hover-border-color: $calendar-hover-bg !default; $calendar-cell-active-bg: $button-default-outlined-active-bg !default; $calendar-cell-other-color: color.adjust($calendar-color, $lightness: 23.92%, $space: hsl) !default; $calendar-week-number-cell-color: color.adjust($calendar-color, $lightness: 23.92%, $space: hsl) !default; @@ -54,7 +52,6 @@ $calendar-week-number-border-radius: 0 !default; @if $mode == "dark" { $calendar-hover-bg: extcolor.adjust-if-not-achromatic($color: $base-accent, $hue: 358.4deg, $saturation: -8.6%, $lightness: -26.7%) !default; - $calendar-cell-hover-border-color: $calendar-hover-bg !default; $calendar-cell-active-bg: $base-accent-selected !default; $calendar-cell-other-color: color.adjust($calendar-color, $lightness: -32.16%, $space: hsl) !default; $calendar-week-number-cell-color: color.adjust($calendar-color, $lightness: -32.16%, $space: hsl) !default; @@ -62,4 +59,6 @@ $calendar-week-number-border-radius: 0 !default; $calendar-week-number-cell-bg: $base-hover-bg !default; } +$calendar-cell-hover-border-color: $calendar-hover-bg !default; + $calendar-cell-other-hover-color: $calendar-cell-other-color !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/cardView/_colors.scss b/packages/devextreme-scss/scss/widgets/fluent/cardView/_colors.scss index 7c1d3a4c5b92..7d0f1ab8613e 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/cardView/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/cardView/_colors.scss @@ -19,19 +19,19 @@ * $name 100. Background color * $type color */ -$cardview-fluent-common-bg-color: null !default; +$cardview-fluent-common-bg-color: $base-typography-bg !default; /** * $name 102. Link color * $type color */ -$cardview-fluent-common-link-color: null !default; +$cardview-fluent-common-link-color: $base-link-color !default; /** * $name 103. Text color * $type color */ -$cardview-fluent-common-text-color: null !default; +$cardview-fluent-common-text-color: $base-text-color !default; // #endregion @@ -41,31 +41,31 @@ $cardview-fluent-common-text-color: null !default; * $name 200. Background color * $type color */ -$cardview-fluent-card-bg-color: null !default; +$cardview-fluent-card-bg-color: $base-bg !default; /** * $name 201. Border color * $type color */ -$cardview-fluent-card-border-color: null !default; +$cardview-fluent-card-border-color: $base-border-color !default; /** * $name 203. Divider color * $type color */ -$cardview-fluent-card-divider-color: null !default; +$cardview-fluent-card-divider-color: $cardview-fluent-card-border-color !default; /** * $name 204. Text color * $type color */ -$cardview-fluent-card-text-color: null !default; +$cardview-fluent-card-text-color: $cardview-fluent-common-text-color !default; /** * $name 206. Cover background color * $type color */ -$cardview-fluent-card__cover-bg-color: null !default; +$cardview-fluent-card__cover-bg-color: #FFF !default; /** * $name 207. "Empty cover" background color @@ -79,7 +79,7 @@ $cardview-fluent-card__cover--noimage-bg-color: null !default; */ $cardview-fluent-card__cover--noimage__icon-color: null !default; -$cardview-fluent-card--focused-outline-color: null !default; +$cardview-fluent-card--focused-outline-color: $base-accent !default; // #endregion @@ -97,35 +97,17 @@ $cardview-fluent-card--selected-bg-color: null !default; */ $cardview-fluent-card--selected-border-color: null !default; -/** -* $name 302. Divider color -* $type color -*/ -$cardview-fluent-card--selected-divider-color: null !default; - /** * $name 303. Text color * $type color */ -$cardview-fluent-card--selected-text-color: null !default; +$cardview-fluent-card--selected-text-color: $cardview-fluent-card-text-color !default; /** * $name 305. Cover background color * $type color */ -$cardview-fluent-card--selected__cover-bg-color: null !default; - -/** -* $name 306. "Empty cover" background color -* $type color -*/ -$cardview-fluent-card--selected__cover--noimage-bg-color: null !default; - -/** -* $name 307. "Empty cover" icon color -* $type color -*/ -$cardview-fluent-card--selected__cover--noimage__icon-color: null !default; +$cardview-fluent-card--selected__cover-bg-color: $cardview-fluent-card__cover-bg-color !default; // #endregion @@ -141,33 +123,33 @@ $cardview-fluent-header-panel__item-bg-color: null !default; * $name 401. Border color * $type color */ -$cardview-fluent-header-panel__item-border-color: null !default; +$cardview-fluent-header-panel__item-border-color: $base-border-color !default; /** * $name 403. Text color * $type color */ -$cardview-fluent-header-panel__item-text-color: null !default; +$cardview-fluent-header-panel__item-text-color: $cardview-fluent-common-text-color !default; /** * $name 405. Icon color * $type color */ -$cardview-fluent-header-panel__item__icon-color: null !default; +$cardview-fluent-header-panel__item__icon-color: $cardview-fluent-common-text-color !default; /** * $name 406. Active "Filter" icon color * $type color */ -$cardview-fluent-header-panel__item__header-filter-icon--selected-color: null !default; +$cardview-fluent-header-panel__item__header-filter-icon--selected-color: $base-accent !default; /** * $name 407. Sort index text color * $type color */ -$cardview-fluent-header-panel__item__sort-index-text-color: null !default; +$cardview-fluent-header-panel__item__sort-index-text-color: $cardview-fluent-common-text-color !default; -$cardview-fluent-header-panel__item--focused-outline-color: null !default; +$cardview-fluent-header-panel__item--focused-outline-color: $base-accent !default; // #endregion @@ -213,17 +195,11 @@ $cardview-fluent-header-panel__item--hovered__sort-index-text-color: null !defau // #region HEADER PANEL ITEM (DISABLED) -/** -* $name 600. Background color -* $type color -*/ -$cardview-fluent-header-panel__item--disabled-bg-color: null !default; - /** * $name 601. Border color * $type color */ -$cardview-fluent-header-panel__item--disabled-border-color: null !default; +$cardview-fluent-header-panel__item--disabled-border-color: $cardview-fluent-header-panel__item-border-color !default; /** * $name 602. Text color @@ -253,29 +229,23 @@ $cardview-fluent-header-panel__item--disabled__sort-index-text-color: null !defa // #region HEADER PANEL ITEM (ON DRAG) -/** -* $name 700. Background color -* $type color -*/ -$cardview-fluent-header-panel__item--dragged-bg-color: null !default; - /** * $name 701. Border color * $type color */ -$cardview-fluent-header-panel__item--dragged-border-color: null !default; +$cardview-fluent-header-panel__item--dragged-border-color: $cardview-fluent-header-panel__item-border-color !default; /** * $name 702. Text color * $type color */ -$cardview-fluent-header-panel__item--dragged-text-color: null !default; +$cardview-fluent-header-panel__item--dragged-text-color: $cardview-fluent-header-panel__item-text-color !default; /** * $name 704. "Drag allowed" icon color * $type color */ -$cardview-fluent-header-panel__item--dragged__allowance-icon-color: null !default; +$cardview-fluent-header-panel__item--dragged__allowance-icon-color: $cardview-fluent-common-text-color !default; /** * $name 705. "Drag blocked" icon color @@ -287,7 +257,7 @@ $cardview-fluent-header-panel__item--dragged__prohibition-icon-color: null !defa * $name 706. Box shadow color * $type color */ -$cardview-fluent-header-panel__item--dragged-box-shadow-color: null !default; +$cardview-fluent-header-panel__item--dragged-box-shadow-color: rgba(0, 0, 0, 0.2) !default; // #endregion @@ -303,19 +273,19 @@ $cardview-fluent-header-panel__dropzone-bg-color: null !default; * $name 802. Border color * $type color */ -$cardview-fluent-header-panel__dropzone-border-color: null !default; +$cardview-fluent-header-panel__dropzone-border-color: $base-accent !default; /** * $name 803. Text color * $type color */ -$cardview-fluent-header-panel__dropzone-text-color: null !default; +$cardview-fluent-header-panel__dropzone-text-color: $base-accent !default; /** * $name 804. Icon color * $type color */ -$cardview-fluent-header-panel__dropzone__icon-color: null !default; +$cardview-fluent-header-panel__dropzone__icon-color: $base-accent !default; // #endregion @@ -325,25 +295,25 @@ $cardview-fluent-header-panel__dropzone__icon-color: null !default; * $name 900. Background color * $type color */ -$cardview-fluent-filter-panel-bg-color: null !default; +$cardview-fluent-filter-panel-bg-color: $cardview-fluent-common-bg-color !default; /** * $name 902. Border color * $type color */ -$cardview-fluent-filter-panel-border-color: null !default; +$cardview-fluent-filter-panel-border-color: $base-border-color !default; /** * $name 903. Text color * $type color */ -$cardview-fluent-filter-panel-text-color: null !default; +$cardview-fluent-filter-panel-text-color: $base-accent !default; /** * $name 904. Icon color * $type color */ -$cardview-fluent-filter-panel__icon-color: null !default; +$cardview-fluent-filter-panel__icon-color: $base-accent !default; // #endregion @@ -353,7 +323,7 @@ $cardview-fluent-filter-panel__icon-color: null !default; * $name 1000. Background color * $type color */ -$cardview-fluent-nodata-view-bg-color: null !default; +$cardview-fluent-nodata-view-bg-color: $cardview-fluent-common-bg-color !default; /** * $name 1001. Text color @@ -381,7 +351,7 @@ $cardview-fluent-nodata-view__icon-bg-color: null !default; * $name 1100. Background color * $type color */ -$cardview-fluent-card__content__field-value--highlighted-bg-color: null !default; +$cardview-fluent-card__content__field-value--highlighted-bg-color: $base-warning !default; /** * $name 1102. Text color @@ -393,52 +363,28 @@ $cardview-fluent-card__content__field-value--highlighted-text-color: null !defau * $name 1104. Box shadow color * $type color */ -$cardview-fluent-card__content__field-value--highlighted-box-shadow-color: null !default; +$cardview-fluent-card__content__field-value--highlighted-box-shadow-color: rgba(0, 0, 0, 0.25) !default; // #endregion // #region COLUMN CHOOSER -$cardview-fluent-columnchooser--empty__message-color: null !default; +$cardview-fluent-columnchooser--empty__message-color: color.adjust($cardview-fluent-common-text-color, $lightness: 26%, $space: hsl) !default; // #endregion @if $mode == "light" { - // Common - $cardview-fluent-common-bg-color: $base-typography-bg !default; - $cardview-fluent-common-link-color: $base-link-color !default; - $cardview-fluent-common-text-color: $base-text-color !default; - // Card - $cardview-fluent-card-bg-color: $base-bg !default; - $cardview-fluent-card-border-color: $base-border-color !default; - $cardview-fluent-card-divider-color: $cardview-fluent-card-border-color !default; - $cardview-fluent-card-text-color: $cardview-fluent-common-text-color !default; - $cardview-fluent-card__cover-bg-color: #FFF !default; $cardview-fluent-card__cover--noimage-bg-color: color.adjust($base-bg, $lightness: -12.16%, $space: hsl) !default; $cardview-fluent-card__cover--noimage__icon-color: color.adjust($cardview-fluent-common-text-color, $lightness: 23.92%, $space: hsl) !default; - $cardview-fluent-card--focused-outline-color: $base-accent !default; - // Card - selected $cardview-fluent-card--selected-bg-color: extcolor.adjust-if-not-achromatic($color: $base-accent, $hue: 4deg, $saturation: -11.38%, $lightness: 55.49%) !default; $cardview-fluent-card--selected-border-color: extcolor.adjust-if-not-achromatic($color: $base-accent, $hue: 3deg, $saturation: 2.21%, $lightness: 44.31%) !default; - $cardview-fluent-card--selected-divider-color: $cardview-fluent-card--selected-border-color !default; - $cardview-fluent-card--selected-text-color: $cardview-fluent-card-text-color !default; - $cardview-fluent-card--selected__cover-bg-color: $cardview-fluent-card__cover-bg-color !default; - $cardview-fluent-card--selected__cover--noimage-bg-color: $cardview-fluent-card__cover--noimage-bg-color !default; - $cardview-fluent-card--selected__cover--noimage__icon-color: $cardview-fluent-card__cover--noimage__icon-color !default; // Header panel item - default state $cardview-fluent-header-panel__item-bg-color: color.adjust($base-bg, $lightness: -5.88%, $space: hsl) !default; - $cardview-fluent-header-panel__item-border-color: $base-border-color !default; - $cardview-fluent-header-panel__item-text-color: $cardview-fluent-common-text-color !default; - $cardview-fluent-header-panel__item__icon-color: $cardview-fluent-common-text-color !default; - $cardview-fluent-header-panel__item__header-filter-icon--selected-color: $base-accent !default; - $cardview-fluent-header-panel__item__sort-index-text-color: $cardview-fluent-common-text-color !default; - - $cardview-fluent-header-panel__item--focused-outline-color: $base-accent !default; // Header panel item - hovered $cardview-fluent-header-panel__item--hovered-bg-color: color.adjust($cardview-fluent-header-panel__item-bg-color, $lightness: -1.96%, $space: hsl) !default; @@ -449,84 +395,38 @@ $cardview-fluent-columnchooser--empty__message-color: null !default; $cardview-fluent-header-panel__item--hovered__sort-index-text-color: $cardview-fluent-header-panel__item__sort-index-text-color !default; // Header panel item - disabled - $cardview-fluent-header-panel__item--disabled-bg-color: $cardview-fluent-header-panel__item-bg-color !default; - $cardview-fluent-header-panel__item--disabled-border-color: $cardview-fluent-header-panel__item-border-color !default; $cardview-fluent-header-panel__item--disabled-text-color: color.adjust($cardview-fluent-header-panel__item-text-color, $lightness: 60.00%, $space: hsl) !default; $cardview-fluent-header-panel__item--disabled__icon-color: color.adjust($cardview-fluent-header-panel__item__icon-color, $lightness: 60.00%, $space: hsl) !default; $cardview-fluent-header-panel__item--disabled__header-filter-icon--selected-color: color.adjust($cardview-fluent-header-panel__item__header-filter-icon--selected-color, $saturation: -34.10%, $space: hsl, $lightness: 27.06%) !default; $cardview-fluent-header-panel__item--disabled__sort-index-text-color: color.adjust($cardview-fluent-header-panel__item__sort-index-text-color, $lightness: 60.00%, $space: hsl) !default; // Header panel item - dragged - $cardview-fluent-header-panel__item--dragged-bg-color: $cardview-fluent-header-panel__item-bg-color !default; - $cardview-fluent-header-panel__item--dragged-border-color: $cardview-fluent-header-panel__item-border-color !default; - $cardview-fluent-header-panel__item--dragged-text-color: $cardview-fluent-header-panel__item-text-color !default; - $cardview-fluent-header-panel__item--dragged__allowance-icon-color: $cardview-fluent-common-text-color !default; $cardview-fluent-header-panel__item--dragged__prohibition-icon-color: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: -4deg, $saturation: 22.80%, $lightness: -9.61%) !default; - $cardview-fluent-header-panel__item--dragged-box-shadow-color: rgba(0, 0, 0, 0.2) !default; // Header panel Drop-zone $cardview-fluent-header-panel__dropzone-bg-color: extcolor.adjust-if-not-achromatic($color: $base-accent, $hue: 4deg, $saturation: -11.38%, $lightness: 55.49%) !default; - $cardview-fluent-header-panel__dropzone-border-color: $base-accent !default; - $cardview-fluent-header-panel__dropzone-text-color: $base-accent !default; - $cardview-fluent-header-panel__dropzone__icon-color: $base-accent !default; - - // Filter panel - $cardview-fluent-filter-panel-bg-color: $cardview-fluent-common-bg-color !default; - $cardview-fluent-filter-panel-border-color: $base-border-color !default; - $cardview-fluent-filter-panel-text-color: $base-accent !default; - $cardview-fluent-filter-panel__icon-color: $base-accent !default; // Empty view - $cardview-fluent-nodata-view-bg-color: $cardview-fluent-common-bg-color !default; $cardview-fluent-nodata-view-text-color: color.adjust($cardview-fluent-common-text-color, $lightness: 23.92%, $space: hsl) !default; $cardview-fluent-nodata-view__icon-color: $cardview-fluent-nodata-view-text-color !default; $cardview-fluent-nodata-view__icon-bg-color: color.adjust($base-bg, $lightness: -9.80%, $space: hsl) !default; // Search highlight - $cardview-fluent-card__content__field-value--highlighted-bg-color: $base-warning !default; $cardview-fluent-card__content__field-value--highlighted-text-color: $cardview-fluent-common-text-color !default; - $cardview-fluent-card__content__field-value--highlighted-box-shadow-color: rgba(0, 0, 0, 0.25) !default; - - // Column chooser - $cardview-fluent-columnchooser--empty__message-color: color.adjust($cardview-fluent-common-text-color, $lightness: 26%, $space: hsl) !default; } @if $mode == "dark" { - // Common - $cardview-fluent-common-bg-color: $base-typography-bg !default; - $cardview-fluent-common-link-color: $base-link-color !default; - $cardview-fluent-common-text-color: $base-text-color !default; - // Card - $cardview-fluent-card-bg-color: $base-bg !default; - $cardview-fluent-card-border-color: $base-border-color !default; - $cardview-fluent-card-divider-color: $cardview-fluent-card-border-color !default; - $cardview-fluent-card-text-color: $cardview-fluent-common-text-color !default; - $cardview-fluent-card__cover-bg-color: #FFF !default; $cardview-fluent-card__cover--noimage-bg-color: color.adjust($base-bg, $lightness: -5.88%, $space: hsl) !default; $cardview-fluent-card__cover--noimage__icon-color: color.adjust($base-text-color, $lightness: -32.16%, $space: hsl) !default; - $cardview-fluent-card--focused-outline-color: $base-accent !default; - // Card - selected $cardview-fluent-card--selected-bg-color: extcolor.adjust-if-not-achromatic($color: $base-accent, $hue: -4deg, $saturation: -14.69%, $lightness: -49.41%) !default; $cardview-fluent-card--selected-border-color: extcolor.adjust-if-not-achromatic($color: $base-accent, $hue: -3deg, $saturation: -11.06%, $lightness: -36.27%) !default; - $cardview-fluent-card--selected-divider-color: $cardview-fluent-card--selected-border-color !default; - $cardview-fluent-card--selected-text-color: $cardview-fluent-card-text-color !default; - $cardview-fluent-card--selected__cover-bg-color: $cardview-fluent-card__cover-bg-color !default; - $cardview-fluent-card--selected__cover--noimage-bg-color: $cardview-fluent-card__cover--noimage-bg-color !default; - $cardview-fluent-card--selected__cover--noimage__icon-color: $cardview-fluent-card__cover--noimage__icon-color !default; // Header panel item - default state $cardview-fluent-header-panel__item-bg-color: color.adjust($base-bg, $lightness: -12.16%, $space: hsl) !default; - $cardview-fluent-header-panel__item-border-color: $base-border-color !default; - $cardview-fluent-header-panel__item-text-color: $cardview-fluent-common-text-color !default; - $cardview-fluent-header-panel__item__icon-color: $cardview-fluent-common-text-color !default; - $cardview-fluent-header-panel__item__header-filter-icon--selected-color: $base-accent !default; - $cardview-fluent-header-panel__item__sort-index-text-color: $cardview-fluent-common-text-color !default; - - $cardview-fluent-header-panel__item--focused-outline-color: $base-accent !default; // Header panel item - hovered $cardview-fluent-header-panel__item--hovered-bg-color: color.adjust($cardview-fluent-header-panel__item-bg-color, $lightness: 18.04%, $space: hsl) !default; @@ -537,48 +437,56 @@ $cardview-fluent-columnchooser--empty__message-color: null !default; $cardview-fluent-header-panel__item--hovered__sort-index-text-color: $cardview-fluent-common-text-color !default; // Header panel item - disabled - $cardview-fluent-header-panel__item--disabled-bg-color: $cardview-fluent-header-panel__item-bg-color !default; - $cardview-fluent-header-panel__item--disabled-border-color: $cardview-fluent-header-panel__item-border-color !default; $cardview-fluent-header-panel__item--disabled-text-color: color.adjust($cardview-fluent-header-panel__item-text-color, $lightness: -56.08%, $space: hsl) !default; $cardview-fluent-header-panel__item--disabled__icon-color: color.adjust($cardview-fluent-header-panel__item__icon-color, $lightness: -56.08%, $space: hsl) !default; $cardview-fluent-header-panel__item--disabled__header-filter-icon--selected-color: color.adjust($cardview-fluent-header-panel__item__header-filter-icon--selected-color, $saturation: -6.72%, $space: hsl, $lightness: -18.04%) !default; $cardview-fluent-header-panel__item--disabled__sort-index-text-color: color.adjust($cardview-fluent-header-panel__item__sort-index-text-color, $lightness: -56.08%, $space: hsl) !default; // Header panel item - dragged - $cardview-fluent-header-panel__item--dragged-bg-color: $cardview-fluent-header-panel__item-bg-color !default; - $cardview-fluent-header-panel__item--dragged-border-color: $cardview-fluent-header-panel__item-border-color !default; - $cardview-fluent-header-panel__item--dragged-text-color: $cardview-fluent-header-panel__item-text-color !default; - $cardview-fluent-header-panel__item--dragged__allowance-icon-color: $cardview-fluent-common-text-color !default; $cardview-fluent-header-panel__item--dragged__prohibition-icon-color: color.adjust($base-danger, $saturation: -0.27%, $space: hsl, $lightness: -7.45%) !default; - $cardview-fluent-header-panel__item--dragged-box-shadow-color: rgba(0, 0, 0, 0.2) !default; // Header panel Drop-zone $cardview-fluent-header-panel__dropzone-bg-color: extcolor.adjust-if-not-achromatic($color: $base-accent, $hue: -4deg, $saturation: -14.69%, $lightness: -49.41%) !default; - $cardview-fluent-header-panel__dropzone-border-color: $base-accent !default; - $cardview-fluent-header-panel__dropzone-text-color: $base-accent !default; - $cardview-fluent-header-panel__dropzone__icon-color: $base-accent !default; - - // Filter panel - $cardview-fluent-filter-panel-bg-color: $cardview-fluent-common-bg-color !default; - $cardview-fluent-filter-panel-border-color: $base-border-color !default; - $cardview-fluent-filter-panel-text-color: $base-accent !default; - $cardview-fluent-filter-panel__icon-color: $base-accent !default; // Empty view - $cardview-fluent-nodata-view-bg-color: $cardview-fluent-common-bg-color !default; $cardview-fluent-nodata-view-text-color: color.adjust($cardview-fluent-common-text-color, $lightness: -32.16%, $space: hsl) !default; $cardview-fluent-nodata-view__icon-color: color.adjust($cardview-fluent-common-text-color, $lightness: -32.16%, $space: hsl) !default; $cardview-fluent-nodata-view__icon-bg-color: color.adjust($base-bg, $lightness: 3.92%, $space: hsl) !default; // Search highlight - $cardview-fluent-card__content__field-value--highlighted-bg-color: $base-warning !default; $cardview-fluent-card__content__field-value--highlighted-text-color: $base-inverted-text-color !default; - $cardview-fluent-card__content__field-value--highlighted-box-shadow-color: rgba(0, 0, 0, 0.25) !default; - - // Column chooser - $cardview-fluent-columnchooser--empty__message-color: color.adjust($cardview-fluent-common-text-color, $lightness: 26%, $space: hsl) !default; } +/** +* $name 302. Divider color +* $type color +*/ +$cardview-fluent-card--selected-divider-color: $cardview-fluent-card--selected-border-color !default; + +/** +* $name 306. "Empty cover" background color +* $type color +*/ +$cardview-fluent-card--selected__cover--noimage-bg-color: $cardview-fluent-card__cover--noimage-bg-color !default; + +/** +* $name 307. "Empty cover" icon color +* $type color +*/ +$cardview-fluent-card--selected__cover--noimage__icon-color: $cardview-fluent-card__cover--noimage__icon-color !default; + +/** +* $name 600. Background color +* $type color +*/ +$cardview-fluent-header-panel__item--disabled-bg-color: $cardview-fluent-header-panel__item-bg-color !default; + +/** +* $name 700. Background color +* $type color +*/ +$cardview-fluent-header-panel__item--dragged-bg-color: $cardview-fluent-header-panel__item-bg-color !default; + $cardview-bg-color: $cardview-fluent-common-bg-color !default; $cardview-link-color: $cardview-fluent-common-link-color !default; $cardview-text-color: $cardview-fluent-common-text-color !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/cardView/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/cardView/_sizes.scss index a47f1f1d3df2..92390ea11dec 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/cardView/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/cardView/_sizes.scss @@ -17,14 +17,14 @@ * $name 101. Border radius * $type text */ -$cardview-fluent-common-border-radius: null !default; +$cardview-fluent-common-border-radius: 16px !default; /** * $name 104. Text weight * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-fluent-common-text-weight: null !default; +$cardview-fluent-common-text-weight: 400 !default; /** * $name 105. Vertical gap between cards @@ -46,9 +46,9 @@ $cardview-fluent-common-padding: null !default; // #region CARD -$cardview-fluent-card-min-width: null !default; +$cardview-fluent-card-min-width: 250px !default; -$cardview-fluent-card-border-width: null !default; +$cardview-fluent-card-border-width: 1px !default; /** * $name 202. Border radius @@ -67,7 +67,7 @@ $cardview-fluent-card__header__selection-checkbox-padding-right: null !default; * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-fluent-card__field-caption-weight: null !default; +$cardview-fluent-card__field-caption-weight: 600 !default; $cardview-fluent-card__cover-max-height: null !default; @@ -90,7 +90,7 @@ $cardview-fluent-card__content__cell-padding-horizontal: null !default; * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-fluent-card--selected__field-caption-weight: null !default; +$cardview-fluent-card--selected__field-caption-weight: $cardview-fluent-card__field-caption-weight !default; // #endregion @@ -110,13 +110,13 @@ $cardview-fluent-header-panel--empty-line-height: null !default; // #region HEADER PANEL ITEM -$cardview-fluent-header-panel__item-border-width: null !default; +$cardview-fluent-header-panel__item-border-width: 1px !default; /** * $name 402. Border radius * $type text */ -$cardview-fluent-header-panel__item-border-radius: null !default; +$cardview-fluent-header-panel__item-border-radius: 6px !default; $cardview-fluent-header-panel__item-padding-vertical: null !default; @@ -127,16 +127,16 @@ $cardview-fluent-header-panel__item-padding-horizontal: null !default; * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-fluent-header-panel__item-text-weight: null !default; +$cardview-fluent-header-panel__item-text-weight: $cardview-fluent-common-text-weight !default; /** * $name 408. Sort index text weight * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-fluent-header-panel__item__sort-index-text-weight: null !default; +$cardview-fluent-header-panel__item__sort-index-text-weight: 400 !default; -$cardview-fluent-header-panel__item__icon-size: null !default; +$cardview-fluent-header-panel__item__icon-size: $fluent-base-icon-size !default; $cardview-fluent-header-item__left-padding: null !default; $cardview-fluent-header-item__right-padding: null !default; @@ -150,14 +150,14 @@ $cardview-fluent-header-item__right-padding: null !default; * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-fluent-header-panel__item--hovered-text-weight: null !default; +$cardview-fluent-header-panel__item--hovered-text-weight: $cardview-fluent-header-panel__item-text-weight !default; /** * $name 507. Sort index text weight * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-fluent-header-panel__item--hovered__sort-index-text-weight: null !default; +$cardview-fluent-header-panel__item--hovered__sort-index-text-weight: $cardview-fluent-header-panel__item__sort-index-text-weight !default; // #endregion @@ -168,14 +168,14 @@ $cardview-fluent-header-panel__item--hovered__sort-index-text-weight: null !defa * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-fluent-header-panel__item--disabled-text-weight: null !default; +$cardview-fluent-header-panel__item--disabled-text-weight: $cardview-fluent-header-panel__item-text-weight !default; /** * $name 607. Sort index text weight * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-fluent-header-panel__item--disabled__sort-index-text-weight: null !default; +$cardview-fluent-header-panel__item--disabled__sort-index-text-weight: $cardview-fluent-header-panel__item__sort-index-text-weight !default; // #endregion @@ -186,10 +186,10 @@ $cardview-fluent-header-panel__item--disabled__sort-index-text-weight: null !def * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-fluent-header-panel__item--dragged-text-weight: null !default; +$cardview-fluent-header-panel__item--dragged-text-weight: $cardview-fluent-header-panel__item-text-weight !default; -$cardview-fluent-header-item__sort-indicator__border-size: null; -$cardview-fluent-header-item__sort-indicator__border-radius: null; +$cardview-fluent-header-item__sort-indicator__border-size: null !default; +$cardview-fluent-header-item__sort-indicator__border-radius: null !default; // #endregion @@ -199,7 +199,7 @@ $cardview-fluent-header-panel__dropzone-height: null !default; $cardview-fluent-header-panel__dropzone-top: null !default; -$cardview-fluent-header-panel__dropzone-border-width: null !default; +$cardview-fluent-header-panel__dropzone-border-width: 1.5px !default; /** * $name 801. Border radius @@ -215,7 +215,7 @@ $cardview-fluent-header-panel__dropzone-border-radius: null !default; * $name 901. Border radius * $type text */ -$cardview-fluent-filter-panel-border-radius: null !default; +$cardview-fluent-filter-panel-border-radius: 0 !default; // #endregion @@ -235,16 +235,16 @@ $cardview-fluent-nodata-view__icon-size: null !default; * $name 1101. Border radius * $type text */ -$cardview-fluent-card__content__field-value--highlighted-border-radius: null !default; +$cardview-fluent-card__content__field-value--highlighted-border-radius: 5px !default; -$cardview-fluent-card__content__field-value--highlighted-border-padding: null !default; +$cardview-fluent-card__content__field-value--highlighted-border-padding: 0.125em 0.2em !default; /** * $name 1103. Text weight * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-fluent-card__content__field-value--highlighted-text-weight: null !default; +$cardview-fluent-card__content__field-value--highlighted-text-weight: 500 !default; // #endregion @@ -259,21 +259,16 @@ $cardview-fluent-column-chooser__select-all-item-horizontal-padding: null !defau @if $size == "default" { // Common - $cardview-fluent-common-border-radius: 16px !default; - $cardview-fluent-common-text-weight: 400 !default; $cardview-fluent-common-cards-row-gap: 16px !default; $cardview-fluent-common-cards-column-gap: 16px !default; $cardview-fluent-common-gap: 24px !default; $cardview-fluent-common-padding: 24px !default; // Card - $cardview-fluent-card-min-width: 250px !default; - $cardview-fluent-card-border-width: 1px !default; $cardview-fluent-card-border-radius: 8px !default; $cardview-fluent-card__header-height: 48px !default; $cardview-fluent-card__header-text-size: 16px !default; $cardview-fluent-card__header__selection-checkbox-padding-right: 12px !default; - $cardview-fluent-card__field-caption-weight: 600 !default; $cardview-fluent-card__cover-max-height: 180px !default; $cardview-fluent-card__cover--noimage__icon-size: 48px !default; $cardview-fluent-card__content-padding: 12px !default; @@ -281,9 +276,6 @@ $cardview-fluent-column-chooser__select-all-item-horizontal-padding: null !defau $cardview-fluent-card__content__cell-padding-vertical: 6px !default; $cardview-fluent-card__content__cell-padding-horizontal: 12px !default; - // Card - selected - $cardview-fluent-card--selected__field-caption-weight: $cardview-fluent-card__field-caption-weight !default; - // Header $cardview-fluent-header-gap: 0 !default; @@ -292,48 +284,25 @@ $cardview-fluent-column-chooser__select-all-item-horizontal-padding: null !defau $cardview-fluent-header-panel--empty-line-height: 32px !default; // Header panel item - default - $cardview-fluent-header-panel__item-border-width: 1px !default; - $cardview-fluent-header-panel__item-border-radius: 6px !default; $cardview-fluent-header-panel__item-padding-horizontal: 8px !default; $cardview-fluent-header-panel__item-padding-vertical: 5px !default; $cardview-fluent-header-item__left-padding: 6px !default; $cardview-fluent-header-item__right-padding: 2px !default; - $cardview-fluent-header-panel__item-text-weight: $cardview-fluent-common-text-weight !default; - $cardview-fluent-header-panel__item__sort-index-text-weight: 400 !default; - $cardview-fluent-header-panel__item__icon-size: $fluent-base-icon-size !default; - - // Header panel item - hovered - $cardview-fluent-header-panel__item--hovered-text-weight: $cardview-fluent-header-panel__item-text-weight !default; - $cardview-fluent-header-panel__item--hovered__sort-index-text-weight: $cardview-fluent-header-panel__item__sort-index-text-weight !default; - - // Header panel item - disabled - $cardview-fluent-header-panel__item--disabled-text-weight: $cardview-fluent-header-panel__item-text-weight !default; - $cardview-fluent-header-panel__item--disabled__sort-index-text-weight: $cardview-fluent-header-panel__item__sort-index-text-weight !default; // Header panel item - dragged - $cardview-fluent-header-panel__item--dragged-text-weight: $cardview-fluent-header-panel__item-text-weight !default; $cardview-fluent-header-item__sort-indicator__border-size: 2px !default; $cardview-fluent-header-item__sort-indicator__border-radius: 8px !default; // Header panel Drop-zone $cardview-fluent-header-panel__dropzone-height: 44px !default; $cardview-fluent-header-panel__dropzone-top: -6px !default; - $cardview-fluent-header-panel__dropzone-border-width: 1.5px !default; $cardview-fluent-header-panel__dropzone-border-radius: 8px !default; - // Filter panel - $cardview-fluent-filter-panel-border-radius: 0 !default; - // Empty view $cardview-fluent-nodata-view__icon-container-size: 64px !default; $cardview-fluent-nodata-view__icon-container-margin: 12px !default; $cardview-fluent-nodata-view__icon-size: 40px !default; - // Search highlight - $cardview-fluent-card__content__field-value--highlighted-border-radius: 5px !default; - $cardview-fluent-card__content__field-value--highlighted-border-padding: 0.125em 0.2em !default; - $cardview-fluent-card__content__field-value--highlighted-text-weight: 500 !default; - // Column chooser $cardview-fluent-column-chooser--empty__message-horizontal-padding: 24px !default; $cardview-fluent-column-chooser__select-all-item-horizontal-padding: 14px !default; @@ -341,21 +310,16 @@ $cardview-fluent-column-chooser__select-all-item-horizontal-padding: null !defau @else if $size == "compact" { // Common - $cardview-fluent-common-border-radius: 16px !default; - $cardview-fluent-common-text-weight: 400 !default; $cardview-fluent-common-cards-row-gap: 12px !default; $cardview-fluent-common-cards-column-gap: 12px !default; $cardview-fluent-common-gap: 16px !default; $cardview-fluent-common-padding: 16px !default; // Card - $cardview-fluent-card-min-width: 250px !default; - $cardview-fluent-card-border-width: 1px !default; $cardview-fluent-card-border-radius: 6px !default; $cardview-fluent-card__header-height: 36px !default; $cardview-fluent-card__header-text-size: 14px !default; $cardview-fluent-card__header__selection-checkbox-padding-right: 8px !default; - $cardview-fluent-card__field-caption-weight: 600 !default; $cardview-fluent-card__cover-max-height: 140px !default; $cardview-fluent-card__cover--noimage__icon-size: 32px !default; $cardview-fluent-card__content-padding: 8px !default; @@ -363,9 +327,6 @@ $cardview-fluent-column-chooser__select-all-item-horizontal-padding: null !defau $cardview-fluent-card__content__cell-padding-vertical: 4px !default; $cardview-fluent-card__content__cell-padding-horizontal: 8px !default; - // Card - selected - $cardview-fluent-card--selected__field-caption-weight: $cardview-fluent-card__field-caption-weight !default; - // Header $cardview-fluent-header-gap: 4px !default; @@ -374,48 +335,25 @@ $cardview-fluent-column-chooser__select-all-item-horizontal-padding: null !defau $cardview-fluent-header-panel--empty-line-height: 24px !default; // Header panel item - default - $cardview-fluent-header-panel__item-border-width: 1px !default; - $cardview-fluent-header-panel__item-border-radius: 6px !default; $cardview-fluent-header-panel__item-padding-horizontal: 6px !default; $cardview-fluent-header-panel__item-padding-vertical: 3px !default; $cardview-fluent-header-item__left-padding: 3px !default; $cardview-fluent-header-item__right-padding: 1px !default; - $cardview-fluent-header-panel__item-text-weight: $cardview-fluent-common-text-weight !default; - $cardview-fluent-header-panel__item__sort-index-text-weight: 400 !default; - $cardview-fluent-header-panel__item__icon-size: $fluent-base-icon-size !default; - - // Header panel item - hovered - $cardview-fluent-header-panel__item--hovered-text-weight: $cardview-fluent-header-panel__item-text-weight !default; - $cardview-fluent-header-panel__item--hovered__sort-index-text-weight: $cardview-fluent-header-panel__item__sort-index-text-weight !default; - - // Header panel item - disabled - $cardview-fluent-header-panel__item--disabled-text-weight: $cardview-fluent-header-panel__item-text-weight !default; - $cardview-fluent-header-panel__item--disabled__sort-index-text-weight: $cardview-fluent-header-panel__item__sort-index-text-weight !default; // Header panel item - dragged - $cardview-fluent-header-panel__item--dragged-text-weight: $cardview-fluent-header-panel__item-text-weight !default; $cardview-fluent-header-item__sort-indicator__border-size: 1px !default; $cardview-fluent-header-item__sort-indicator__border-radius: 4px !default; // Header panel Drop-zone $cardview-fluent-header-panel__dropzone-height: 32px !default; $cardview-fluent-header-panel__dropzone-top: -4px !default; - $cardview-fluent-header-panel__dropzone-border-width: 1.5px !default; $cardview-fluent-header-panel__dropzone-border-radius: 6px !default; - // Filter panel - $cardview-fluent-filter-panel-border-radius: 0 !default; - // Empty view $cardview-fluent-nodata-view__icon-container-size: 48px !default; $cardview-fluent-nodata-view__icon-container-margin: 8px !default; $cardview-fluent-nodata-view__icon-size: 32px !default; - // Search highlight - $cardview-fluent-card__content__field-value--highlighted-border-radius: 5px !default; - $cardview-fluent-card__content__field-value--highlighted-border-padding: 0.125em 0.2em !default; - $cardview-fluent-card__content__field-value--highlighted-text-weight: 500 !default; - // Column chooser $cardview-fluent-column-chooser--empty__message-horizontal-padding: 16px !default; $cardview-fluent-column-chooser__select-all-item-horizontal-padding: 12px !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/chat/_colors.scss b/packages/devextreme-scss/scss/widgets/fluent/chat/_colors.scss index 2d7ee1140d81..7f53705ade4e 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/chat/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/chat/_colors.scss @@ -125,7 +125,7 @@ $chat-typingindicator-circle-bg-color: null !default; $chat-file-container-background-color: $base-bg !default; $chat-file-container-box-shadow: null !default; -$chat-file-secondary-color: null !default; +$chat-file-secondary-color: $base-icon-color !default; $chat-suggestions-box-shadow: null !default; @@ -144,7 +144,6 @@ $chat-suggestions-box-shadow: null !default; $chat-typingindicator-circle-bg-color-center: color.adjust($base-bg, $lightness: -56.08%, $space: hsl) !default; $chat-file-container-box-shadow: 0 0 2px 0 color.change($base-shadow-color, $alpha: 0.12), 0 1px 2px 0 color.change($base-shadow-color, $alpha: 0.14) !default; - $chat-file-secondary-color: $base-icon-color !default; $chat-suggestions-box-shadow: 0 4px 12px 0 color.change($base-shadow-color, $alpha: 0.04), 0 4px 24px 0 color.change($base-shadow-color, $alpha: 0.02) !default; @@ -176,7 +175,6 @@ $chat-suggestions-box-shadow: null !default; $chat-typingindicator-circle-bg-color-center: color.adjust($base-bg, $lightness: 43.92%, $space: hsl) !default; $chat-file-container-box-shadow: 0 0 2px 0 color.change($base-shadow-color, $alpha: 0.24), 0 1px 2px 0 color.change($base-shadow-color, $alpha: 0.28) !default; - $chat-file-secondary-color: $base-icon-color !default; $chat-suggestions-box-shadow: 0 4px 24px 0 color.change($base-shadow-color, $alpha: 0.02), 0 4px 12px 0 color.change($base-shadow-color, $alpha: 0.04) !default; } diff --git a/packages/devextreme-scss/scss/widgets/fluent/chat/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/chat/_sizes.scss index f8069845cec3..cad0368631dd 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/chat/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/chat/_sizes.scss @@ -14,7 +14,7 @@ $chat-bubble-delete-gap: null !default; $chat-bubble-delete-icon-size: null !default; $chat-messagebox-padding: null !default; $chat-messagebox-textarea-padding: null !default; -$chat-messagebox-textarea-border-radius: null !default; +$chat-messagebox-textarea-border-radius: 8px !default; $chat-messagebox-toolbar-min-height: null !default; $chat-messagebox-gap: 8px !default; $chat-messagebox-files-container-padding: 3px !default; @@ -32,7 +32,7 @@ $chat-information-edited-gap: 2px !default; $chat-alerts-padding-block: null !default; $chat-alerts-padding-inline: 8px !default; -$chat-alerts-row-gap: null !default; +$chat-alerts-row-gap: 2px !default; $chat-alert-padding-block: null !default; $chat-suggestions-padding: null !default; @@ -57,12 +57,10 @@ $chat-messagelist-day-header-first-padding-top: 20px !default; $chat-messagelist-day-header-font-size: 12px !default; $chat-typingindicator-template: $chat-messagegroup-alignment-start-template !default; -$chat-typingindicator-padding: null !default; $chat-typingindicator-row-gap: $chat-messagegroup-gap !default; $chat-typingindicator-font-size: $chat-information-font-size !default; $chat-typingindicator-bubble-padding-block: null !default; $chat-typingindicator-bubble-padding-inline: null !default; -$chat-typingindicator-bubble-border-radius: null !default; $chat-typingindicator-bubble-column-gap: 4px !default; $chat-typingindicator-circle-size: null !default; $chat-typingindicator-circle-size-center: null !default; @@ -90,14 +88,12 @@ $chat-bubble-gap: 8px !default; $chat-bubble-delete-gap: 4px !default; $chat-bubble-delete-icon-size: 20px !default; $chat-messagebox-padding: 20px !default; - $chat-messagebox-textarea-border-radius: 8px !default; $chat-messagebox-textarea-padding: 12px !default; $chat-messagebox-toolbar-min-height: 32px !default; $chat-messagelist-padding: 20px !default; $chat-messagegroup-padding: 24px !default; $chat-alerts-padding-block: 12px !default; - $chat-alerts-row-gap: 2px !default; $chat-alert-padding-block: 6px !default; $chat-suggestions-padding: 20px !default; @@ -116,8 +112,6 @@ $chat-bubble-gap: 8px !default; $chat-typingindicator-circle-size-center: math.div(12, 14) * 1em !default; $chat-typingindicator-bubble-padding-block: 12px !default; $chat-typingindicator-bubble-padding-inline: 20px !default; - $chat-typingindicator-padding: $chat-messagegroup-padding !default; - $chat-typingindicator-bubble-border-radius: $chat-bubble-border-radius !default; $chat-confirmation-popup-toolbar-gap: 8px !default; @@ -130,14 +124,12 @@ $chat-bubble-gap: 8px !default; $chat-bubble-delete-gap: 2px !default; $chat-bubble-delete-icon-size: 16px !default; $chat-messagebox-padding: 16px !default; - $chat-messagebox-textarea-border-radius: 8px !default; $chat-messagebox-textarea-padding: 8px !default; $chat-messagebox-toolbar-min-height: 24px !default; $chat-messagelist-padding: 16px !default; $chat-messagegroup-padding: 12px !default; $chat-alerts-padding-block: 8px !default; - $chat-alerts-row-gap: 2px !default; $chat-alert-padding-block: 5px !default; $chat-suggestions-padding: 16px !default; @@ -156,10 +148,11 @@ $chat-bubble-gap: 8px !default; $chat-typingindicator-circle-size-center: math.div(12, 12) * 1em !default; $chat-typingindicator-bubble-padding-block: 8px !default; $chat-typingindicator-bubble-padding-inline: 12px !default; - $chat-typingindicator-padding: $chat-messagegroup-padding !default; - $chat-typingindicator-bubble-border-radius: $chat-bubble-border-radius !default; $chat-confirmation-popup-toolbar-gap: 4px !default; $chat-file-icon-font-size: 16px !default; } + +$chat-typingindicator-padding: $chat-messagegroup-padding !default; +$chat-typingindicator-bubble-border-radius: $chat-bubble-border-radius !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/colorView/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/colorView/_index.scss index bef18943d2af..4259a2430fb2 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/colorView/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/colorView/_index.scss @@ -4,8 +4,8 @@ @use "../../base/colorView"; @include dx-base-colorview-styles($colorview-handle-color, $colorview-handle-border-color); -$fluent-colorview-alpha-width: $fluent-colorview-palette-width + 4; -$fluent-colorview-hue-scale-height: $fluent-colorview-palette-height; +$fluent-colorview-alpha-width: $fluent-colorview-palette-width + 4 !default; +$fluent-colorview-hue-scale-height: $fluent-colorview-palette-height !default; .dx-colorview-container { width: $fluent-colorview-container-width; diff --git a/packages/devextreme-scss/scss/widgets/fluent/common/_mixins.scss b/packages/devextreme-scss/scss/widgets/fluent/common/_mixins.scss index 07d36367e858..83f2458bf9b9 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/common/_mixins.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/common/_mixins.scss @@ -7,7 +7,7 @@ @use "../../base/validation" as *; @use "../list/sizes" as *; -$badge-invalid-bg: $base-invalid-color; +$badge-invalid-bg: $base-invalid-color !default; @mixin dx-base-typography() { @include dx-base-typography-mixin( diff --git a/packages/devextreme-scss/scss/widgets/fluent/dateRangeBox/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/dateRangeBox/_index.scss index c9c93f884a25..7540a81f6463 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/dateRangeBox/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/dateRangeBox/_index.scss @@ -9,7 +9,7 @@ @use "../../base/icon_fonts" as *; @use "../textEditor" as *; -$fluent-editor-custom-button-margin: 6px; +$fluent-editor-custom-button-margin: 6px !default; .dx-daterangebox { @include dx-dropdowneditor-button-icon-styling(daterangepicker); diff --git a/packages/devextreme-scss/scss/widgets/fluent/dateView/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/dateView/_index.scss index 276fbf644f98..67bb91377d8d 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/dateView/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/dateView/_index.scss @@ -6,7 +6,7 @@ @use "../../base/dateView"; @use "../scrollable"; -$fluent-dateview-roller-padding: math.div($fluent-dateview-roller-height - $fluent-dateview-roller-item-height, 2); +$fluent-dateview-roller-padding: math.div($fluent-dateview-roller-height - $fluent-dateview-roller-item-height, 2) !default; .dx-dateview-rollers { width: auto; diff --git a/packages/devextreme-scss/scss/widgets/fluent/diagram/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/diagram/_index.scss index 9b8af08066b7..d7d9b61bed19 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/diagram/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/diagram/_index.scss @@ -15,10 +15,10 @@ @use "../fieldset/sizes" as *; @use "../menu/colors" as *; -$fluent-diagram-toolbar-icon-size: $fluent-base-icon-size; -$diagram-toolbar-colorbutton-border-width: 3px; +$fluent-diagram-toolbar-icon-size: $fluent-base-icon-size !default; +$diagram-toolbar-colorbutton-border-width: 3px !default; -$diagram-toolbox-title-size: 18px; +$diagram-toolbox-title-size: 18px !default; .dx-diagram { border-color: $diagram-border-color; diff --git a/packages/devextreme-scss/scss/widgets/fluent/dropDownButton/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/dropDownButton/_sizes.scss index b6f005a34d5d..b970e8c4cacd 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/dropDownButton/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/dropDownButton/_sizes.scss @@ -6,12 +6,12 @@ $fluent-dropdownbutton-icon-spindown-right-size: null !default; @if $size == "default" { $fluent-dropdownbutton-action-horizontal-padding: 12px !default; - $fluent-dropdownbutton-icon-spindown-size: 24px; + $fluent-dropdownbutton-icon-spindown-size: 24px !default; $fluent-dropdownbutton-icon-spindown-right-size: 16px !default; } @else if $size == "compact" { $fluent-dropdownbutton-action-horizontal-padding: 8px !default; - $fluent-dropdownbutton-icon-spindown-size: 18px; + $fluent-dropdownbutton-icon-spindown-size: 18px !default; $fluent-dropdownbutton-icon-spindown-right-size: 12px !default; } diff --git a/packages/devextreme-scss/scss/widgets/fluent/dropDownEditor/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/dropDownEditor/_index.scss index c21aed8508d0..1a767ca16beb 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/dropDownEditor/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/dropDownEditor/_index.scss @@ -8,17 +8,17 @@ @use "../../base/dropDownEditor"; @use "../../base/dropDownEditor/mixins" as *; -$fluent-dropdowneditor-container-button-size: $fluent-base-icon-size; +$fluent-dropdowneditor-container-button-size: $fluent-base-icon-size !default; $fluent-dropdowneditor-invalid-badge-offset: $fluent-dropdowneditor-container-button-size + - $fluent-invalid-badge-halfhorizontal-padding; + $fluent-invalid-badge-halfhorizontal-padding !default; $fluent-dropdowneditor-invalid-badge-with-clear-offset: $fluent-dropdowneditor-container-button-size * 2 + - $fluent-invalid-badge-halfhorizontal-padding; + $fluent-invalid-badge-halfhorizontal-padding !default; -$fluent-dropdowneditor-invalid-badge-size: $fluent-invalid-badge-size + $fluent-invalid-badge-horizontal-padding; +$fluent-dropdowneditor-invalid-badge-size: $fluent-invalid-badge-size + $fluent-invalid-badge-horizontal-padding !default; @mixin dx-dropdowneditor-button-icon() { width: $fluent-dropdowneditor-container-button-size; diff --git a/packages/devextreme-scss/scss/widgets/fluent/fileManager/_colors.scss b/packages/devextreme-scss/scss/widgets/fluent/fileManager/_colors.scss index 9c5aed4a9f7a..f0a6358c9177 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/fileManager/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/fileManager/_colors.scss @@ -16,14 +16,11 @@ $filemanager-file-item-hover-bg: $base-hover-bg !default; $filemanager-file-item-focused-border: $base-accent !default; $filemanager-item-focused-color: $base-inverted-text-color !default; $filemanager-dirs-tree-item-focus-bg: null !default; -$filemanager-progressbox-shadow-color: null !default; @if $mode == "light" { $filemanager-dirs-tree-item-focus-bg: color.adjust($base-bg, $lightness: -8%, $space: hsl) !default; - $filemanager-progressbox-shadow-color: color.change($base-shadow-color, $alpha: 0.175) !default; } @if $mode == "dark" { $filemanager-dirs-tree-item-focus-bg: color.adjust($base-bg, $lightness: 8%, $space: hsl) !default; - $filemanager-progressbox-shadow-color: color.change($base-shadow-color, $alpha: 0.3) !default; } diff --git a/packages/devextreme-scss/scss/widgets/fluent/fileManager/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/fileManager/_sizes.scss index c20681b38015..6d827e99f941 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/fileManager/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/fileManager/_sizes.scss @@ -15,7 +15,7 @@ $filemanager-large-icon-size: null !default; $filemanager-progress-panel-title-padding: null !default; $filemanager-progress-box-button-close-margin-top: null !default; -$filemanager-toolbar-height: $fluent-toolbar-height; +$filemanager-toolbar-height: $fluent-toolbar-height !default; @if $size == "default" { $filemanager-toolbar-separator-height: 36px !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/fileUploader/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/fileUploader/_index.scss index 985d8a670669..feaf4bcd5aa4 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/fileUploader/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/fileUploader/_index.scss @@ -7,8 +7,8 @@ @use "../../base/mixins" as *; @use "../../base/fileUploader"; -$fluent-fileuploader-file-wrapper-border-size: 3px; -$fluent-fileuploader-file-container-boxshadow: 0 1px 3px 0 color.change($base-shadow-color, $alpha: 0.1); +$fluent-fileuploader-file-wrapper-border-size: 3px !default; +$fluent-fileuploader-file-container-boxshadow: 0 1px 3px 0 color.change($base-shadow-color, $alpha: 0.1) !default; .dx-fileuploader-wrapper { padding: $fluent-fileuploader-vertical-padding; diff --git a/packages/devextreme-scss/scss/widgets/fluent/fileUploader/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/fileUploader/_sizes.scss index fa118fa0c463..c41cfcdc2c1b 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/fileUploader/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/fileUploader/_sizes.scss @@ -7,7 +7,7 @@ $fluent-fileuploader-file-cancel-button-margin-left: 4px !default; $fluent-fileuploader-file-container-button-size: null !default; $fluent-fileuploader-vertical-padding: 7px !default; -$fluent-fileuploader-files-container-padding: null !default; +$fluent-fileuploader-files-container-padding: 12px 4px 0 !default; $fluent-fileuploader-file-size-margin: null !default; $fluent-fileuploader-file-icon-font-size: null !default; $fluent-fileuploader-files-container-rounding: 4px !default; @@ -17,7 +17,6 @@ $fluent-fileuploader-files-container-rounding: 4px !default; $fluent-fileuploader-file-container-text-margin: 15px !default; $fluent-fileuploader-file-container-button-margin: 8px !default; $fluent-fileuploader-file-container-button-size: 32px !default; - $fluent-fileuploader-files-container-padding: 12px 4px 0 !default; $fluent-fileuploader-file-size-margin: 0 0 0 4px !default; $fluent-fileuploader-file-icon-font-size: 20px !default; } @@ -27,7 +26,6 @@ $fluent-fileuploader-files-container-rounding: 4px !default; $fluent-fileuploader-file-container-text-margin: 6px !default; $fluent-fileuploader-file-container-button-margin: 2px !default; $fluent-fileuploader-file-container-button-size: 28px !default; - $fluent-fileuploader-files-container-padding: 12px 4px 0 !default; $fluent-fileuploader-file-size-margin: 0 0 4px !default; $fluent-fileuploader-file-icon-font-size: 16px !default; } diff --git a/packages/devextreme-scss/scss/widgets/fluent/form/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/form/_sizes.scss index 384113d46730..465839839624 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/form/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/form/_sizes.scss @@ -1,6 +1,6 @@ @use "../sizes" as *; -$fluent-form-group-caption-bottom-margin: null !default; +$fluent-form-group-caption-bottom-margin: 4px !default; $fluent-form-group-caption-top-padding: null !default; $fluent-form-group-content-bottom-padding: null !default; $fluent-form-group-caption-font-size: null !default; @@ -17,7 +17,6 @@ $fluent-form-label-icon-size: $fluent-base-icon-size !default; $fluent-form-group-custom-caption-icon-margin: null !default; @if $size == "default" { - $fluent-form-group-caption-bottom-margin: 4px !default; $fluent-form-group-caption-top-padding: 16px !default; $fluent-form-group-content-bottom-padding: 16px !default; $fluent-form-group-caption-font-size: 20px !default; @@ -30,7 +29,6 @@ $fluent-form-group-custom-caption-icon-margin: null !default; } @else if $size == "compact" { - $fluent-form-group-caption-bottom-margin: 4px !default; $fluent-form-group-caption-top-padding: 12px !default; $fluent-form-group-content-bottom-padding: 12px !default; $fluent-form-group-caption-font-size: 16px !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/gantt/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/gantt/_index.scss index c6e20ce847e9..06d100805263 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/gantt/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/gantt/_index.scss @@ -9,7 +9,7 @@ @use "../form/sizes" as *; @use "../splitterBar"; -$gantt-successor-background-color: white; +$gantt-successor-background-color: white !default; .dx-gantt { border: 1px solid; diff --git a/packages/devextreme-scss/scss/widgets/fluent/gridBase/_colors.scss b/packages/devextreme-scss/scss/widgets/fluent/gridBase/_colors.scss index c154f911500c..1ece29b5e750 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/gridBase/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/gridBase/_colors.scss @@ -34,7 +34,7 @@ $datagrid-drag-header-border-color: color.change($base-accent, $alpha: 0.5) !def $datagrid-drag-header-shadow-color: null !default; $datagrid-drag-header-second-shadow-color: null !default; $datagrid-header-drag-bg: $datagrid-base-background-color !default; -$header-filter-color: null !default; +$header-filter-color: $datagrid-columnchooser-item-color !default; /** * $name 51. Selected row background color @@ -75,7 +75,7 @@ $datagrid-row-hovered-color: $base-hover-color !default; $datagrid-cell-modified-border-color: $button-success-text-hover-bg !default; $datagrid-cell-removed-border-color: $datagrid-border-color !default; $datagrid-row-removed-bg: $datagrid-hover-bg !default; -$datagrid-cell-removed-text-color: null !default; +$datagrid-cell-removed-text-color: $datagrid-columnchooser-item-color !default; /** * $name 45. Invalid data faded color @@ -88,7 +88,7 @@ $datagrid-nodata-color: $base-label-color !default; * $name 90. Group row text color * $type color */ -$datagrid-group-row-color: null !default; +$datagrid-group-row-color: $datagrid-columnchooser-item-color !default; /** * $name 95. Group row background color @@ -155,12 +155,13 @@ $datagrid-summary-bg: $datagrid-base-background-color !default; $datagrid-column-separator-bg: $base-accent !default; $datagrid-filter-row-background-color: null !default; $datagrid-active-header-filter-icon-color: $base-accent !default; -$datagrid-text-stub-background-image-path: null !default; + +$datagrid-text-stub-background-image-path: data-uri("images/widgets/fluent/grid/text-stub.png") !default; $datagrid-filter-panel-color: $base-accent !default; $datagrid-draggable-column-text-color: null !default; -$datagrid-text-link-disabled-opacity: $base-disabled-opacity; -$datagrid-icon-link-disabled-opacity: $button-disabled-icon-opacity; +$datagrid-text-link-disabled-opacity: $base-disabled-opacity !default; +$datagrid-icon-link-disabled-opacity: $button-disabled-icon-opacity !default; @if $mode == "light" { @if $color == "blue" { @@ -172,31 +173,22 @@ $datagrid-icon-link-disabled-opacity: $button-disabled-icon-opacity; $datagrid-row-focused-bg: extcolor.adjust-if-not-achromatic($color: $base-accent, $hue: -1deg, $saturation: 0%, $lightness: 26.47%) !default; $datagrid-filter-row-background-color: $base-typography-bg !default; } - - $datagrid-group-row-color: $datagrid-columnchooser-item-color !default; $datagrid-block-separator-bg: color.adjust($datagrid-base-background-color, $lightness: -12%, $space: hsl) !default; $datagrid-drag-header-shadow-color: color.change($base-shadow-color, $alpha: 0.1) !default; $datagrid-drag-header-second-shadow-color: color.change($base-shadow-color, $alpha: 0.2) !default; - $header-filter-color: $datagrid-columnchooser-item-color !default; $datagrid-columnchooser-item-bg: $datagrid-columnchooser-bg !default; $datagrid-row-alternation-bg: color.adjust($datagrid-base-background-color, $lightness: -4%, $space: hsl) !default; - $datagrid-text-stub-background-image-path: data-uri("images/widgets/fluent/color-schemes/light/grid/text-stub.png") !default; - $datagrid-cell-removed-text-color: $datagrid-columnchooser-item-color !default; $datagrid-draggable-column-text-color: color.adjust($base-text-color, $lightness: 60%, $space: hsl) !default; } @if $mode == "dark" { $datagrid-row-focused-bg: #0c3b5e !default; $datagrid-filter-row-background-color: color.adjust($datagrid-base-background-color, $lightness: 2%, $space: hsl) !default; - $datagrid-group-row-color: $datagrid-columnchooser-item-color !default; $datagrid-block-separator-bg: color.adjust($datagrid-base-background-color, $lightness: 12%, $space: hsl) !default; $datagrid-drag-header-shadow-color: color.change($base-shadow-color, $alpha: 0.3) !default; $datagrid-drag-header-second-shadow-color: color.change($base-shadow-color, $alpha: 0.4) !default; - $header-filter-color: $datagrid-columnchooser-item-color !default; $datagrid-columnchooser-item-bg: color.adjust($datagrid-columnchooser-bg, $lightness: 3%, $space: hsl) !default; $datagrid-row-alternation-bg: color.adjust($datagrid-base-background-color, $lightness: 4%, $space: hsl) !default; - $datagrid-text-stub-background-image-path: data-uri("images/widgets/fluent/color-schemes/dark/grid/text-stub.png") !default; - $datagrid-cell-removed-text-color: $datagrid-columnchooser-item-color !default; $datagrid-draggable-column-text-color: color.adjust($base-text-color, $lightness: -63.9%, $space: hsl) !default; } diff --git a/packages/devextreme-scss/scss/widgets/fluent/gridBase/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/gridBase/_index.scss index 26ed9c35829f..27842a42af13 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/gridBase/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/gridBase/_index.scss @@ -28,11 +28,11 @@ @use "layout/cell" as *; @use "layout/aiChat" as *; -$fluent-grid-base-border-hidden: 1px solid transparent; -$fluent-grid-base-row-border: 1px solid transparent; -$fluent-grid-base-drag-header-first-shadow: 0 0 1px $datagrid-drag-header-shadow-color; -$fluent-grid-base-drag-header-second-shadow: 0 1px 3px $datagrid-drag-header-second-shadow-color; -$fluent-grid-base-group-panel-message-line-height: $fluent-button-text-line-height; +$fluent-grid-base-border-hidden: 1px solid transparent !default; +$fluent-grid-base-row-border: 1px solid transparent !default; +$fluent-grid-base-drag-header-first-shadow: 0 0 1px $datagrid-drag-header-shadow-color !default; +$fluent-grid-base-drag-header-second-shadow: 0 1px 3px $datagrid-drag-header-second-shadow-color !default; +$fluent-grid-base-group-panel-message-line-height: $fluent-button-text-line-height !default; @mixin grid-base($widget-name) { @include fluent-grid-base-cell($widget-name); diff --git a/packages/devextreme-scss/scss/widgets/fluent/gridBase/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/gridBase/_sizes.scss index caaa91c8ec2f..bc836c3c34a0 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/gridBase/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/gridBase/_sizes.scss @@ -17,14 +17,13 @@ $fluent-grid-base-menu-item-padding: null !default; $fluent-grid-base-column-chooser-horizontal-padding: null !default; $fluent-grid-base-column-chooser-paddings: null !default; $fluent-grid-base-header-panel-padding: null !default; -$fluent-grid-base-grouppanel-item-margin: null !default; +$fluent-grid-base-grouppanel-item-margin: 6px !default; $fluent-grid-base-command-edit-column-width: null !default; $fluent-grid-base-command-edit-column-with-icons-width: null !default; $fluent-grid-base-adaptive-column-height: null !default; -$fluent-grid-base-treeview-search-margin: null !default; +$fluent-grid-base-treeview-search-margin: 10px !default; $fluent-grid-base-nodata-font-size: null !default; $fluent-filter-row-between-editor-padding: null !default; -$fluent-command-column-additional-right-margin: null !default; $fluent-grid-base-treeview-select-all-item-offset: null !default; $fluent-grid-base-header-line-height: 18px !default; @@ -57,16 +56,13 @@ $fluent-grid-base-ai-prompt-editor-progressbar-height: 4px !default; $fluent-grid-base-column-chooser-horizontal-padding: 24px !default; $fluent-grid-base-column-chooser-paddings: 16px $fluent-grid-base-column-chooser-horizontal-padding 9px !default; $fluent-grid-base-header-panel-padding: 8px 0 !default; - $fluent-grid-base-grouppanel-item-margin: 6px !default; $fluent-grid-base-command-edit-column-width: 115px !default; $fluent-grid-base-command-edit-column-with-icons-width: 93px !default; $fluent-grid-base-adaptive-column-height: 40px !default; - $fluent-grid-base-treeview-search-margin: 10px !default; $fluent-grid-base-nodata-font-size: 17px !default; $fluent-filter-row-between-editor-padding: 12px !default; - $fluent-command-column-additional-right-margin: 0 !default; $fluent-grid-base-treeview-select-all-item-offset: 34px !default; @@ -92,16 +88,13 @@ $fluent-grid-base-ai-prompt-editor-progressbar-height: 4px !default; $fluent-grid-base-column-chooser-horizontal-padding: 8px !default; $fluent-grid-base-column-chooser-paddings: 8px $fluent-grid-base-column-chooser-horizontal-padding 8px !default; $fluent-grid-base-header-panel-padding: 6px 0 !default; - $fluent-grid-base-grouppanel-item-margin: 6px !default; $fluent-grid-base-command-edit-column-width: 105px !default; $fluent-grid-base-command-edit-column-with-icons-width: 80px !default; $fluent-grid-base-adaptive-column-height: 26px !default; - $fluent-grid-base-treeview-search-margin: 10px !default; $fluent-grid-base-nodata-font-size: 14px !default; $fluent-filter-row-between-editor-padding: 8px !default; - $fluent-command-column-additional-right-margin: 2px !default; $fluent-grid-base-treeview-select-all-item-offset: 28px !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/gridBase/_variables.scss b/packages/devextreme-scss/scss/widgets/fluent/gridBase/_variables.scss index 1ec5dd5b0875..2c57c3b85b93 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/gridBase/_variables.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/gridBase/_variables.scss @@ -2,9 +2,9 @@ @use "colors" as *; @use "sizes" as *; -$fluent-grid-base-border-color: $datagrid-border-color; -$fluent-grid-base-cell-vertical-padding: math.round(math.div($fluent-grid-base-cell-height - $fluent-grid-base-cell-line-height, 2)); -$fluent-grid-base-cell-padding: $fluent-grid-base-cell-vertical-padding $fluent-grid-base-cell-horizontal-padding; -$fluent-grid-base-header-cell-vertical-padding: math.round(math.div($fluent-grid-base-header-height - $fluent-grid-base-header-line-height, 2)); -$fluent-grid-base-command-select-column-width: $fluent-grid-base-checkbox-size + 2 * $fluent-grid-base-cell-horizontal-padding; -$grid-masterdetail-padding: 16px; +$fluent-grid-base-border-color: $datagrid-border-color !default; +$fluent-grid-base-cell-vertical-padding: math.round(math.div($fluent-grid-base-cell-height - $fluent-grid-base-cell-line-height, 2)) !default; +$fluent-grid-base-cell-padding: $fluent-grid-base-cell-vertical-padding $fluent-grid-base-cell-horizontal-padding !default; +$fluent-grid-base-header-cell-vertical-padding: math.round(math.div($fluent-grid-base-header-height - $fluent-grid-base-header-line-height, 2)) !default; +$fluent-grid-base-command-select-column-width: $fluent-grid-base-checkbox-size + 2 * $fluent-grid-base-cell-horizontal-padding !default; +$grid-masterdetail-padding: 16px !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/gridBase/layout/aiChat/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/gridBase/layout/aiChat/_sizes.scss index 23463e560ea2..bf760a009072 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/gridBase/layout/aiChat/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/gridBase/layout/aiChat/_sizes.scss @@ -1,3 +1,3 @@ @use "../../../sizes" as *; -$ai-chat-message-icon-size: $fluent-base-icon-size; +$ai-chat-message-icon-size: $fluent-base-icon-size !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/icons/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/icons/_index.scss index 4bc6a54ebb1b..c3f928382586 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/icons/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/icons/_index.scss @@ -20,12 +20,6 @@ } } -.dx-scrollview-pulldown { - background-image: $scrollview-pulldown-path; - background-position: 0 0; - background-repeat: no-repeat; -} - .dx-loadindicator-image { background-position: center center; background-repeat: no-repeat; diff --git a/packages/devextreme-scss/scss/widgets/fluent/informer/_colors.scss b/packages/devextreme-scss/scss/widgets/fluent/informer/_colors.scss index 8fc917a37748..256a14dca7d3 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/informer/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/informer/_colors.scss @@ -8,11 +8,11 @@ $informer-info-color: $base-text-color !default; $informer-info-background-color: null !default; @if $mode == "light" { - $informer-error-background-color: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: -358deg, $saturation: 0.58%, $lightness: 46.67%); - $informer-info-background-color: color.adjust($base-text-color, $lightness: 74.90%, $space: hsl); + $informer-error-background-color: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: -358deg, $saturation: 0.58%, $lightness: 46.67%) !default; + $informer-info-background-color: color.adjust($base-text-color, $lightness: 74.90%, $space: hsl) !default; } @if $mode == "dark" { - $informer-error-background-color: color.adjust($base-danger, $saturation: -5.06%, $space: hsl, $lightness: -53.53%); - $informer-info-background-color: color.adjust($base-text-color, $lightness: -76.08%, $space: hsl); + $informer-error-background-color: color.adjust($base-danger, $saturation: -5.06%, $space: hsl, $lightness: -53.53%) !default; + $informer-info-background-color: color.adjust($base-text-color, $lightness: -76.08%, $space: hsl) !default; } diff --git a/packages/devextreme-scss/scss/widgets/fluent/list/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/list/_index.scss index 12d1d175891c..21aefb71d99d 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/list/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/list/_index.scss @@ -13,10 +13,10 @@ @use "../radioGroup/colors" as *; @use "../badge"; -$fluent-list-item-border-width: 1px; -$fluent-list-border-width: 1px; -$fluent-list-border: $fluent-list-border-width solid; -$fluent-list-item-border: $fluent-list-item-border-width solid; +$fluent-list-item-border-width: 1px !default; +$fluent-list-border-width: 1px !default; +$fluent-list-border: $fluent-list-border-width solid !default; +$fluent-list-item-border: $fluent-list-item-border-width solid !default; .dx-state-disabled { &.dx-list { diff --git a/packages/devextreme-scss/scss/widgets/fluent/menuBase/_colors.scss b/packages/devextreme-scss/scss/widgets/fluent/menuBase/_colors.scss index f55789e57034..1f1279d587f3 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/menuBase/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/menuBase/_colors.scss @@ -1,3 +1,3 @@ @use "../colors" as *; -$menu-item-color-disabled: $base-foreground-disabled; +$menu-item-color-disabled: $base-foreground-disabled !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/menuBase/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/menuBase/_sizes.scss index 63d09d1d7dbf..dd2ef1a21d2f 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/menuBase/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/menuBase/_sizes.scss @@ -3,7 +3,7 @@ $fluent-menu-item-line-height: null !default; $fluent-menu-item-with-icon-horizontal-padding: null !default; $fluent-menu-horizontal-padding: null !default; -$fluent-menu-icon-to-text-offset: 4px; +$fluent-menu-icon-to-text-offset: 4px !default; @if $size == "default" { $fluent-menu-item-line-height: 20px !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/numberBox/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/numberBox/_sizes.scss index 09bdf6b8492b..a166d36fbe1f 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/numberBox/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/numberBox/_sizes.scss @@ -1,12 +1,4 @@ @use "../sizes" as *; $fluent-numberbox-spin-container-width: 24px !default; -$fluent-numberbox-touch-friendly-spin-container-width: null !default; - -@if $size == "default" { - $fluent-numberbox-touch-friendly-spin-container-width: 48px !default; -} - -@else if $size == "compact" { - $fluent-numberbox-touch-friendly-spin-container-width: 48px !default; -} +$fluent-numberbox-touch-friendly-spin-container-width: 48px !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/pivotGrid/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/pivotGrid/_index.scss index 3871987a599d..0afa3b10b188 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/pivotGrid/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/pivotGrid/_index.scss @@ -32,12 +32,12 @@ $pivotgrid-button-top-padding: $pivotgrid-button-top-padding, ); -$fluent-pivotgrid-headers-font-size: 14px; -$fluent-pivotgrid-area-font-size: 13px; -$fluent-pivotgrid-chevron-icon-size: 20px; +$fluent-pivotgrid-headers-font-size: 14px !default; +$fluent-pivotgrid-area-font-size: 13px !default; +$fluent-pivotgrid-chevron-icon-size: 20px !default; -$fluent-pivotgrid-drag-header-shadow: 0 3px 4px color.change($base-shadow-color, $alpha: 0.15); -$fluent-pivotgridfieldchooser-field-min-height: 36px; +$fluent-pivotgrid-drag-header-shadow: 0 3px 4px color.change($base-shadow-color, $alpha: 0.15) !default; +$fluent-pivotgridfieldchooser-field-min-height: 36px !default; .dx-pivotgrid-fields-container { &.dx-drag { diff --git a/packages/devextreme-scss/scss/widgets/fluent/popup/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/popup/_index.scss index 75423b3fb670..3f1362182fa1 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/popup/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/popup/_index.scss @@ -8,7 +8,7 @@ @use "../../base/popup"; @use "../overlay"; -$fluent-popup-toolbar-label-font-size: $fluent-m-font-size; +$fluent-popup-toolbar-label-font-size: $fluent-m-font-size !default; .dx-popup-wrapper { & > .dx-overlay-content { diff --git a/packages/devextreme-scss/scss/widgets/fluent/popup/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/popup/_sizes.scss index d2d88fcd38d3..5553608c8b33 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/popup/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/popup/_sizes.scss @@ -12,8 +12,8 @@ $fluent-popup-toolbar-height: null !default; $fluent-popup-button-margin: 0 8px !default; $fluent-popup-toolbar-item-spacing: 8px !default; -$fluent-popup-content-shadow: null !default; -$fluent-popup-focused-content-shadow: null !default; +$fluent-popup-content-shadow: 0 0 8px 0 $popup-content-shadow-color1, 0 32px 64px 0 $popup-content-shadow-color2 !default; +$fluent-popup-focused-content-shadow: 0 0 12px 0 $popup-focused-content-shadow-color, 0 32px 64px 0 $popup-focused-content-shadow-color !default; @if $size == "default" { $fluent-popup-content-padding: 24px !default; @@ -24,9 +24,6 @@ $fluent-popup-focused-content-shadow: null !default; $fluent-popup-toolbar-horizontal-padding: 24px !default; $fluent-popup-bottom-toolbar-height: 48px !default; $fluent-popup-toolbar-height: 48px !default; - - $fluent-popup-content-shadow: 0 0 8px 0 $popup-content-shadow-color1, 0 32px 64px 0 $popup-content-shadow-color2 !default; - $fluent-popup-focused-content-shadow: 0 0 12px 0 $popup-focused-content-shadow-color, 0 32px 64px 0 $popup-focused-content-shadow-color !default; } @else if $size == "compact" { @@ -38,9 +35,6 @@ $fluent-popup-focused-content-shadow: null !default; $fluent-popup-toolbar-horizontal-padding: 16px !default; $fluent-popup-bottom-toolbar-height: 36px !default; $fluent-popup-toolbar-height: 36px !default; - - $fluent-popup-content-shadow: 0 0 8px 0 $popup-content-shadow-color1, 0 32px 64px 0 $popup-content-shadow-color2 !default; - $fluent-popup-focused-content-shadow: 0 0 12px 0 $popup-focused-content-shadow-color, 0 32px 64px 0 $popup-focused-content-shadow-color !default; } :root { diff --git a/packages/devextreme-scss/scss/widgets/fluent/progressBar/_colors.scss b/packages/devextreme-scss/scss/widgets/fluent/progressBar/_colors.scss index 85165ce798f0..a81d38c91d00 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/progressBar/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/progressBar/_colors.scss @@ -18,9 +18,9 @@ $progressbar-range-disabled-bg: $base-foreground-disabled !default; $progressbar-range-indeterminate-color: $base-accent !default; $progressbar-range-invalid-color: $base-invalid-color !default; -$progressbar-interdeterminate-state-gradient: -90deg, transparent 5%, $progressbar-range-indeterminate-color, transparent 95%; -$progressbar-disabled-interdeterminate-state-gradient: -90deg, transparent 5%, $progressbar-range-disabled-bg, transparent 95%; -$progressbar-invalid-interdeterminate-state-gradient: -90deg, transparent 5%, $progressbar-range-invalid-color, transparent 95%; +$progressbar-interdeterminate-state-gradient: -90deg, transparent 5%, $progressbar-range-indeterminate-color, transparent 95% !default; +$progressbar-disabled-interdeterminate-state-gradient: -90deg, transparent 5%, $progressbar-range-disabled-bg, transparent 95% !default; +$progressbar-invalid-interdeterminate-state-gradient: -90deg, transparent 5%, $progressbar-range-invalid-color, transparent 95% !default; /** * $name 30. Label color diff --git a/packages/devextreme-scss/scss/widgets/fluent/progressBar/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/progressBar/_sizes.scss index 91677287e082..d093147ac5d1 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/progressBar/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/progressBar/_sizes.scss @@ -3,7 +3,7 @@ $progressbar-height: null !default; $progressbar-border-radius: $base-border-radius !default; $progressbar-status-margin-top: null !default; -$progressbar-indeterminate-background-width: 90%; +$progressbar-indeterminate-background-width: 90% !default; @if $size == "default" { $progressbar-height: 4px !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/radioGroup/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/radioGroup/_sizes.scss index 47f7cd1cd4ed..ada30c55a57e 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/radioGroup/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/radioGroup/_sizes.scss @@ -1,14 +1,11 @@ @use "../sizes" as *; $fluent-radio-button-vertical-margin: null !default; -$fluent-radio-group-horizontal-gap: null !default; @if $size == "default" { - $fluent-radio-group-horizontal-gap: 16px !default; $fluent-radio-button-vertical-margin: 12px !default; } @else if $size == "compact" { - $fluent-radio-group-horizontal-gap: 12px !default; $fluent-radio-button-vertical-margin: 6px !default; } diff --git a/packages/devextreme-scss/scss/widgets/fluent/recurrenceEditor/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/recurrenceEditor/_index.scss index a3bad1a3542a..cded1ffcecac 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/recurrenceEditor/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/recurrenceEditor/_index.scss @@ -4,9 +4,9 @@ @use "../scheduler/sizes" as *; @use "../../base/recurrenceEditor" as *; -$fluent-recurrence-number-box-margin: 11px; -$fluent-recurrence-button-group-padding: 6px; -$fluent-recurrence-radio-group-margin: 15px; +$fluent-recurrence-number-box-margin: 11px !default; +$fluent-recurrence-button-group-padding: 6px !default; +$fluent-recurrence-radio-group-margin: 15px !default; .dx-recurrence-numberbox-interval { margin-right: $fluent-recurrence-number-box-margin; diff --git a/packages/devextreme-scss/scss/widgets/fluent/scheduler/_colors.scss b/packages/devextreme-scss/scss/widgets/fluent/scheduler/_colors.scss index c307c71d772d..edaae60d331c 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/scheduler/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/scheduler/_colors.scss @@ -23,7 +23,6 @@ $scheduler-accent-border-color: $base-border-color !default; * $type color */ $scheduler-appointment-text-color: null !default; -$scheduler-appointment-focus-color: null !default; /** * $name 40. Appointment background color @@ -36,7 +35,7 @@ $scheduler-appointment-base-color: null !default; * $type color */ $scheduler-appointment-start-color: color.change($base-shadow-color, $alpha: 0.3) !default; -$scheduler-appointment-active-color: $button-default-outlined-active-bg; +$scheduler-appointment-active-color: $button-default-outlined-active-bg !default; /** * $name 60. Focused cell color @@ -87,7 +86,7 @@ $header-panel-cell-date: null !default; @if $mode == "light" { $scheduler-appointment-text-color: #000 !default; - $header-panel-cell-date: #424242; + $header-panel-cell-date: #424242 !default; @if $color == "blue" { $scheduler-appointment-base-color: color.adjust($base-accent, $lightness: 48%, $space: hsl) !default; @@ -107,7 +106,7 @@ $header-panel-cell-date: null !default; @if $mode == "dark" { $scheduler-appointment-base-color: color.adjust($base-accent, $lightness: -48%, $space: hsl) !default; $scheduler-appointment-text-color: #fff !default; - $header-panel-cell-date: #adadad; + $header-panel-cell-date: #adadad !default; $scheduler-cell-hover-bg: $base-hover-bg !default; @if $color == "blue" { @@ -122,4 +121,4 @@ $header-panel-cell-date: null !default; } $scheduler-first-month-cell-background-color: color.adjust($scheduler-appointment-base-color, $alpha: -0.85) !default; -$scheduler-appointment-focus-color: $scheduler-appointment-text-color; +$scheduler-appointment-focus-color: $scheduler-appointment-text-color !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/scheduler/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/scheduler/_index.scss index b7136eab49d5..28f18745f2d9 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/scheduler/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/scheduler/_index.scss @@ -55,13 +55,13 @@ @use "../recurrenceEditor" as *; @use "../scrollable"; -$fluent-scheduler-all-day-table-cell-height: $fluent-scheduler-workspace-date-table-cell-height * 1.5; +$fluent-scheduler-all-day-table-cell-height: $fluent-scheduler-workspace-date-table-cell-height * 1.5 !default; -$fluent-scheduler-month-date-text-size: 14px; +$fluent-scheduler-month-date-text-size: 14px !default; -$scheduler-first-of-month-border: 1px solid; +$scheduler-first-of-month-border: 1px solid !default; -$fluent-scheduler-agenda-time-panel-cell-padding: 8px; +$fluent-scheduler-agenda-time-panel-cell-padding: 8px !default; @include baseScheduler.scheduler-mixin(); diff --git a/packages/devextreme-scss/scss/widgets/fluent/scheduler/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/scheduler/_sizes.scss index 6fb2abec8776..1db3d084feb0 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/scheduler/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/scheduler/_sizes.scss @@ -4,8 +4,7 @@ @use "../toolbar/sizes" as *; $fluent-scheduler-workspace-date-table-cell-height: null !default; -$fluent-scheduler-toolbar-item-offset: null !default; -$fluent-scheduler-navigator-height: null !default; +$fluent-scheduler-toolbar-item-offset: 4px !default; $fluent-scheduler-header-panel-day-font-size: null !default; $fluent-scheduler-appointment-tooltip-title-font-size: null !default; $fluent-scheduler-appointment-tooltip-date-font-size: null !default; @@ -18,8 +17,8 @@ $fluent-scheduler-dropdown-button-height: null !default; $fluent-scheduler-dropdown-button-padding: null !default; $fluent-scheduler-dropdown-button-margin: null !default; $fluent-scheduler-dropdown-button-vertical-margin: null !default; -$fluent-scheduler-dropdown-button-adaptive-margin: null !default; -$fluent-scheduler-dropdown-button-adaptive-vertical-margin: null !default; +$fluent-scheduler-dropdown-button-adaptive-margin: 0 2px !default; +$fluent-scheduler-dropdown-button-adaptive-vertical-margin: 0 2px 0 1px !default; $fluent-scheduler-appointment-tooltip-content-margin-left: null !default; $fluent-tooltip-marker-dimension: null !default; $fluent-scheduler-appointment-tooltip-title-margin-top: null !default; @@ -34,19 +33,19 @@ $fluent-scheduler-appointment-popup-all-day-item-height: null !default; $fluent-scheduler-appointment-popup-days-of-week-buttons-gap: null !default; $fluent-scheduler-appointment-popup-repeat-end-item-height: null !default; -$fluent-scheduler-appointment-short-content-padding: null !default; -$fluent-scheduler-appointment-15min-height: null !default; -$fluent-scheduler-appointment-20min-height: null !default; -$fluent-scheduler-appointment-25min-height: null !default; -$fluent-scheduler-appointment-10min-recurrence-padding-right: null !default; -$fluent-scheduler-appointment-10min-title-font-size: null !default; -$fluent-scheduler-appointment-10min-title-line-height: null !default; -$fluent-scheduler-appointment-10min-icon-font-size: null !default; -$fluent-scheduler-appointment-10min-icon-right: null !default; +$fluent-scheduler-appointment-short-content-padding: 0 8px 0 12px !default; +$fluent-scheduler-appointment-15min-height: 17px !default; +$fluent-scheduler-appointment-20min-height: 24px !default; +$fluent-scheduler-appointment-25min-height: 32px !default; +$fluent-scheduler-appointment-10min-recurrence-padding-right: 14px !default; +$fluent-scheduler-appointment-10min-title-font-size: 8px !default; +$fluent-scheduler-appointment-10min-title-line-height: 12px !default; +$fluent-scheduler-appointment-10min-icon-font-size: 12px !default; +$fluent-scheduler-appointment-10min-icon-right: 2px !default; $fluent-scheduler-appointment-15min-recurrence-padding-right: null !default; -$fluent-scheduler-appointment-15min-title-font-size: null !default; +$fluent-scheduler-appointment-15min-title-font-size: 12px !default; $fluent-scheduler-appointment-15min-title-line-height: null !default; -$fluent-scheduler-appointment-15min-icon-font-size: null !default; +$fluent-scheduler-appointment-15min-icon-font-size: 12px !default; $fluent-scheduler-appointment-15min-icon-right: null !default; $fluent-scheduler-appointment-20min-recurrence-padding-right: null !default; $fluent-scheduler-appointment-20min-icon-right: null !default; @@ -60,7 +59,7 @@ $fluent-scheduler-timeline-date-table-cell-height: 50px !default; */ $scheduler-border-radius: 0 !default; -$fluent-scheduler-navigator-icon-font-size: $fluent-base-icon-size; +$fluent-scheduler-navigator-icon-font-size: $fluent-base-icon-size !default; $fluent-scheduler-appointment-month-text-size: 12px !default; $fluent-scheduler-workspace-info-text-size: 16px !default; $fluent-scheduler-all-day-title-font-size: 12px !default; @@ -90,14 +89,12 @@ $header-panel-time-cell-padding: null !default; $fluent-scheduler-collapsed-all-day-table-cell-height: 32px !default; $header-panel-time-cell-padding: 12px !default; - $header-day-element-height: 26px; + $header-day-element-height: 26px !default; $fluent-scheduler-workspace-date-table-cell-height: 38px !default; - $fluent-scheduler-navigator-height: 36px !default; $fluent-scheduler-header-panel-day-font-size: 16px !default; $fluent-scheduler-header-height: 46px !default; $fluent-scheduler-header-offset: 0 12px !default; - $fluent-scheduler-toolbar-item-offset: 4px !default; $fluent-scheduler-appointment-tooltip-title-font-size: 16px !default; $fluent-scheduler-appointment-tooltip-date-font-size: 14px !default; @@ -111,8 +108,6 @@ $header-panel-time-cell-padding: null !default; $fluent-scheduler-dropdown-button-padding: 4px !default; $fluent-scheduler-dropdown-button-margin: 5px 1px 3px 3px !default; $fluent-scheduler-dropdown-button-vertical-margin: 5px 3px 3px 3px !default; - $fluent-scheduler-dropdown-button-adaptive-margin: 0 2px !default; - $fluent-scheduler-dropdown-button-adaptive-vertical-margin: 0 2px 0 1px !default; $fluent-scheduler-appointment-tooltip-content-margin-left: 6px !default; $fluent-tooltip-marker-dimension: 12px !default; @@ -128,23 +123,10 @@ $header-panel-time-cell-padding: null !default; $fluent-scheduler-appointment-popup-days-of-week-buttons-gap: 12px !default; $fluent-scheduler-appointment-popup-repeat-end-item-height: 32px !default; - $agenda-appointment-title-font-size: 14px; - - $fluent-scheduler-appointment-short-content-padding: 0 8px 0 12px !default; - $fluent-scheduler-appointment-15min-height: 17px !default; - $fluent-scheduler-appointment-20min-height: 24px !default; - $fluent-scheduler-appointment-25min-height: 32px !default; - - $fluent-scheduler-appointment-10min-recurrence-padding-right: 14px !default; - $fluent-scheduler-appointment-10min-title-font-size: 8px !default; - $fluent-scheduler-appointment-10min-title-line-height: 12px !default; - $fluent-scheduler-appointment-10min-icon-font-size: 12px !default; - $fluent-scheduler-appointment-10min-icon-right: 2px !default; + $agenda-appointment-title-font-size: 14px !default; $fluent-scheduler-appointment-15min-recurrence-padding-right: 14px !default; - $fluent-scheduler-appointment-15min-title-font-size: 12px !default; $fluent-scheduler-appointment-15min-title-line-height: 18px !default; - $fluent-scheduler-appointment-15min-icon-font-size: 12px !default; $fluent-scheduler-appointment-15min-icon-right: 2px !default; $fluent-scheduler-appointment-20min-icon-right: 2px !default; @@ -155,14 +137,12 @@ $header-panel-time-cell-padding: null !default; $fluent-scheduler-collapsed-all-day-table-cell-height: 24px !default; $header-panel-time-cell-padding: 8px !default; - $header-day-element-height: 24px; + $header-day-element-height: 24px !default; $fluent-scheduler-workspace-date-table-cell-height: 28px !default; - $fluent-scheduler-navigator-height: 28px !default; $fluent-scheduler-header-panel-day-font-size: 14px !default; $fluent-scheduler-header-height: 34px !default; $fluent-scheduler-header-offset: 2px 8px !default; - $fluent-scheduler-toolbar-item-offset: 4px !default; $fluent-scheduler-appointment-tooltip-title-font-size: 14px !default; $fluent-scheduler-appointment-tooltip-date-font-size: 11px !default; @@ -176,8 +156,6 @@ $header-panel-time-cell-padding: null !default; $fluent-scheduler-dropdown-button-padding: 2px !default; $fluent-scheduler-dropdown-button-margin: 2px 1px 2px 3px !default; $fluent-scheduler-dropdown-button-vertical-margin: 2px 1px 2px 1px !default; - $fluent-scheduler-dropdown-button-adaptive-margin: 0 2px !default; - $fluent-scheduler-dropdown-button-adaptive-vertical-margin: 0 2px 0 1px !default; $fluent-scheduler-appointment-tooltip-content-margin-left: 8px !default; $fluent-tooltip-marker-dimension: 18px !default; @@ -193,23 +171,10 @@ $header-panel-time-cell-padding: null !default; $fluent-scheduler-appointment-popup-days-of-week-buttons-gap: 8px !default; $fluent-scheduler-appointment-popup-repeat-end-item-height: 24px !default; - $agenda-appointment-title-font-size: 13px; - - $fluent-scheduler-appointment-short-content-padding: 0 8px 0 12px !default; - $fluent-scheduler-appointment-15min-height: 17px !default; - $fluent-scheduler-appointment-20min-height: 24px !default; - $fluent-scheduler-appointment-25min-height: 32px !default; - - $fluent-scheduler-appointment-10min-recurrence-padding-right: 14px !default; - $fluent-scheduler-appointment-10min-title-font-size: 8px !default; - $fluent-scheduler-appointment-10min-title-line-height: 12px !default; - $fluent-scheduler-appointment-10min-icon-font-size: 12px !default; - $fluent-scheduler-appointment-10min-icon-right: 2px !default; + $agenda-appointment-title-font-size: 13px !default; $fluent-scheduler-appointment-15min-recurrence-padding-right: 16px !default; - $fluent-scheduler-appointment-15min-title-font-size: 12px !default; $fluent-scheduler-appointment-15min-title-line-height: 12px !default; - $fluent-scheduler-appointment-15min-icon-font-size: 12px !default; $fluent-scheduler-appointment-15min-icon-right: 5px !default; $fluent-scheduler-appointment-20min-recurrence-padding-right: 20px !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/scrollView/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/scrollView/_index.scss index 12a4b458fc80..fa971352b542 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/scrollView/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/scrollView/_index.scss @@ -6,7 +6,7 @@ @use "../scrollable"; @use "../../base/scrollView"; -$scrollview-loading-indicator-size: 42px; +$scrollview-loading-indicator-size: 42px !default; .dx-scrollview-scrollbottom-text, .dx-scrollview-pull-down-text { diff --git a/packages/devextreme-scss/scss/widgets/fluent/scrollable/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/scrollable/_sizes.scss index 993a6b25a95a..0cfcdad10d93 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/scrollable/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/scrollable/_sizes.scss @@ -1,2 +1,2 @@ -$fluent-scrollbar-size-thin: 6px; -$fluent-scrollbar-size: 13px; +$fluent-scrollbar-size-thin: 6px !default; +$fluent-scrollbar-size: 13px !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/slider/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/slider/_sizes.scss index 788adabaef81..b9af6d044e39 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/slider/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/slider/_sizes.scss @@ -3,31 +3,22 @@ $fluent-slider-handle-size: null !default; $fluent-slider-wrapper-height: null !default; $fluent-slider-tooltip-width: null !default; -$fluent-slider-top-tooltip-margin: null !default; $fluent-slider-height: 28px !default; // for fieldset height $fluent-slider-radius: 50% !default; $fluent-slider-track-height: null !default; $fluent-slider-track-border-radius: 2px !default; -$fluent-slider-handle-height: null !default; -$fluent-slider-handle-inner-size: null !default; @if $size == "default" { $fluent-slider-track-height: 4px !default; - $fluent-slider-handle-height: 18px !default; $fluent-slider-handle-size: 20px !default; - $fluent-slider-handle-inner-size: 18px !default; $fluent-slider-wrapper-height: 32px !default; $fluent-slider-tooltip-width: 34px !default; - $fluent-slider-top-tooltip-margin: -5px !default; } @else if $size == "compact" { $fluent-slider-track-height: 2px !default; - $fluent-slider-handle-height: 14px !default; $fluent-slider-handle-size: 16px !default; - $fluent-slider-handle-inner-size: 14px !default; $fluent-slider-wrapper-height: 26px !default; $fluent-slider-tooltip-width: 28px !default; - $fluent-slider-top-tooltip-margin: -2px !default; } diff --git a/packages/devextreme-scss/scss/widgets/fluent/speedDialAction/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/speedDialAction/_index.scss index 4ddec0093191..d1143ea3fcea 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/speedDialAction/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/speedDialAction/_index.scss @@ -5,8 +5,8 @@ @use "../button/colors" as *; @use "../overlay"; -$fluent-fa-button-label-offset: 16px; -$fluent-fa-button-label-padding: 4px 8px; +$fluent-fa-button-label-offset: 16px !default; +$fluent-fa-button-label-padding: 4px 8px !default; @include dx-fa-button-mixin( $fluent-main-fa-button-size, diff --git a/packages/devextreme-scss/scss/widgets/fluent/speedDialAction/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/speedDialAction/_sizes.scss index 8862d4505a19..bfd200a0685b 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/speedDialAction/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/speedDialAction/_sizes.scss @@ -8,13 +8,13 @@ $fluent-fa-button-size: null !default; @if $size == "default" { $fluent-main-fa-button-size: 44px !default; $fluent-fa-button-size: 44px !default; - $fluent-main-fa-button-horizontal-padding: 16px; - $fluent-main-fa-button-label-padding: 0 $fluent-main-fa-button-horizontal-padding 0 8px; + $fluent-main-fa-button-horizontal-padding: 16px !default; + $fluent-main-fa-button-label-padding: 0 $fluent-main-fa-button-horizontal-padding 0 8px !default; } @else if $size == "compact" { $fluent-main-fa-button-size: 32px !default; $fluent-fa-button-size: 32px !default; - $fluent-main-fa-button-horizontal-padding: 12px; - $fluent-main-fa-button-label-padding: 0 $fluent-main-fa-button-horizontal-padding 0 4px; + $fluent-main-fa-button-horizontal-padding: 12px !default; + $fluent-main-fa-button-label-padding: 0 $fluent-main-fa-button-horizontal-padding 0 4px !default; } diff --git a/packages/devextreme-scss/scss/widgets/fluent/splitter/_colors.scss b/packages/devextreme-scss/scss/widgets/fluent/splitter/_colors.scss index 44c8a9441194..31e5ce8db750 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/splitter/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/splitter/_colors.scss @@ -10,19 +10,19 @@ $splitter-resize-handle-color: null !default; * $name 30. Focused state icon color * $type color */ -$splitter-resize-handle-color-focused: null !default; +$splitter-resize-handle-color-focused: $base-inverted-text-color !default; /** * $name 40. Handle background color * $type color */ -$splitter-resize-handle-bg: null !default; +$splitter-resize-handle-bg: $base-border-color !default; /** * $name 50. Handle hover state background color * $type color */ -$splitter-resize-handle-bg-hover: null !default; +$splitter-resize-handle-bg-hover: $base-border-color-hover !default; /** * $name 60. Handle focused state background color @@ -32,18 +32,10 @@ $splitter-resize-handle-bg-focused: $base-accent !default; @if $mode == "light" { $splitter-resize-handle-color: $base-icon-color !default; - $splitter-resize-handle-color-focused: $base-inverted-text-color !default; - - $splitter-resize-handle-bg: $base-border-color !default; - $splitter-resize-handle-bg-hover: $base-border-color-hover !default; } @if $mode == "dark" { $splitter-resize-handle-color: $base-text-color !default; - $splitter-resize-handle-color-focused: $base-inverted-text-color !default; - - $splitter-resize-handle-bg: $base-border-color !default; - $splitter-resize-handle-bg-hover: $base-border-color-hover !default; } /** diff --git a/packages/devextreme-scss/scss/widgets/fluent/splitter/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/splitter/_sizes.scss index a0e0b973b604..94d7ce69c89a 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/splitter/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/splitter/_sizes.scss @@ -1 +1 @@ -$splitter-resize-handle-icon-size: 20px; +$splitter-resize-handle-icon-size: 20px !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/switch/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/switch/_index.scss index f2dcd883f771..55c05689deca 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/switch/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/switch/_index.scss @@ -5,9 +5,9 @@ @use "../../base/switch"; @use "../../base/switch/mixins" as *; -$fluent-switch-transition: all 0.2s cubic-bezier(0.33, 0, 0.67, 1); -$fluent-switch-handle-on-shadow: 0 0 0 $fluent-switch-shadow-blur-radius color.change($switch-handle-on-bg, $alpha: 0.1); -$fluent-switch-shadow: none; +$fluent-switch-transition: all 0.2s cubic-bezier(0.33, 0, 0.67, 1) !default; +$fluent-switch-handle-on-shadow: 0 0 0 $fluent-switch-shadow-blur-radius color.change($switch-handle-on-bg, $alpha: 0.1) !default; +$fluent-switch-shadow: none !default; @include dx-switch( $fluent-switch-width, diff --git a/packages/devextreme-scss/scss/widgets/fluent/switch/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/switch/_sizes.scss index c2817e8f9714..8906ee1bef54 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/switch/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/switch/_sizes.scss @@ -3,11 +3,9 @@ $fluent-switch-width: null !default; $switch-handle-margin: null !default; $switch-padding: 8px !default; -$switch-container-vertical-margin: null !default; -$switch-container-horizontal-padding: null !default; +$switch-container-vertical-margin: 0 !default; $fluent-switch-height: null !default; $switch-handle-height: null !default; -$switch-container-height: null !default; $fluent-switch-shadow-blur-radius: 0 !default; $fluent-switch-container-adjustment: 6px !default; @@ -16,18 +14,15 @@ $fluent-switch-container-adjustment: 6px !default; $fluent-switch-width: 40px !default; $switch-handle-height: 14px !default; $switch-handle-margin: 3px !default; - $switch-container-vertical-margin: 0 !default; - $switch-container-horizontal-padding: $switch-handle-margin !default; $fluent-switch-height: 20px !default; - $switch-container-height: $fluent-switch-height !default; } @else if $size == "compact" { $fluent-switch-width: 32px !default; $switch-handle-height: 12px !default; $switch-handle-margin: 2px !default; - $switch-container-vertical-margin: 0 !default; - $switch-container-horizontal-padding: $switch-handle-margin !default; $fluent-switch-height: 16px !default; - $switch-container-height: $fluent-switch-height !default; } + +$switch-container-horizontal-padding: $switch-handle-margin !default; +$switch-container-height: $fluent-switch-height !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/tabs/layout/tab/_base.scss b/packages/devextreme-scss/scss/widgets/fluent/tabs/layout/tab/_base.scss index 43392b992139..a55f2877d88d 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/tabs/layout/tab/_base.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/tabs/layout/tab/_base.scss @@ -31,21 +31,13 @@ } .dx-tab-text .dx-tab-text-span { - visibility: visible; - - .dx-tab-text-span-pseudo { - visibility: hidden; - } + font-weight: $fluent-tab-font-weight-accent; } } &.dx-state-disabled { color: $tabs-tab-disabled-color; - .dx-tab-text .dx-tab-text-span-pseudo { - color: $tabs-tab-disabled-color; - } - .dx-icon { color: $tabs-tab-icon-disabled-color; } @@ -61,22 +53,16 @@ line-height: $fluent-line-height; .dx-tab-text-span { - position: relative; display: block; - font-weight: $fluent-tab-font-weight-accent; - visibility: hidden; + font-weight: $fluent-tab-font-weight-basic; } .dx-tab-text-span-pseudo { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; + display: block; + height: 0; pointer-events: none; - color: $tabs-tab-color; - font-weight: $fluent-tab-font-weight-basic; - visibility: visible; + font-weight: $fluent-tab-font-weight-accent; + visibility: hidden; } } diff --git a/packages/devextreme-scss/scss/widgets/fluent/tabs/variables/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/tabs/variables/_sizes.scss index 1793c8ce0a4a..0c73f52e0640 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/tabs/variables/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/tabs/variables/_sizes.scss @@ -1,22 +1,21 @@ @use "../../sizes" as *; $fluent-tabs-item-gap: 4px !default; -$fluent-tabs-item-padding: null !default; $fluent-tabs-item-padding-top-bottom: null !default; $fluent-tabs-item-padding-right-left: null !default; -$fluent-tab-height: null !default; +$fluent-tab-height: auto !default; $fluent-tab-font-weight-basic: 400 !default; $fluent-tab-font-weight-accent: 500 !default; -$fluent-base-icon-margin: null !default; +$fluent-base-icon-margin: 8px !default; $fluent-tabs-icon-size: $fluent-base-icon-size !default; $fluent-tab-nav-button-icon-width: null !default; $fluent-tab-nav-button-icon-height: null !default; $fluent-tab-nav-button-icon-font-size: null !default; -$fluent-tab-nav-button-content-padding: null !default; +$fluent-tab-nav-button-content-padding: 0 4px !default; -$fluent-tab-vertical-nav-button-content-padding: null !default; +$fluent-tab-vertical-nav-button-content-padding: 4px 0 !default; $fluent-indicator-thickness-primary: 2px !default; $fluent-indicator-thickness-secondary: 3px !default; @@ -32,27 +31,19 @@ $fluent-badge-min-size: null !default; @if $size == "default" { $fluent-tabs-item-padding-top-bottom: 12px !default; $fluent-tabs-item-padding-right-left: 10px !default; - $fluent-tabs-item-padding: $fluent-tabs-item-padding-top-bottom $fluent-tabs-item-padding-right-left !default; - $fluent-tab-height: auto !default; - $fluent-base-icon-margin: 8px !default; $fluent-tab-nav-button-icon-width: 20px !default; $fluent-tab-nav-button-icon-height: 20px !default; $fluent-tab-nav-button-icon-font-size: 20px !default; - $fluent-tab-nav-button-content-padding: 0 4px !default; - $fluent-tab-vertical-nav-button-content-padding: 4px 0 !default; $fluent-badge-min-size: 20px !default; } @else if $size == "compact" { $fluent-tabs-item-padding-top-bottom: 8px !default; $fluent-tabs-item-padding-right-left: 8px !default; - $fluent-tabs-item-padding: $fluent-tabs-item-padding-top-bottom $fluent-tabs-item-padding-right-left !default; - $fluent-tab-height: auto !default; - $fluent-base-icon-margin: 8px !default; $fluent-tab-nav-button-icon-width: 16px !default; $fluent-tab-nav-button-icon-height: 16px !default; $fluent-tab-nav-button-icon-font-size: 16px !default; - $fluent-tab-nav-button-content-padding: 0 4px !default; - $fluent-tab-vertical-nav-button-content-padding: 4px 0 !default; $fluent-badge-min-size: 16px !default; } + +$fluent-tabs-item-padding: $fluent-tabs-item-padding-top-bottom $fluent-tabs-item-padding-right-left !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/tagBox/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/tagBox/_sizes.scss index 214f072e13de..92052e567b9e 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/tagBox/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/tagBox/_sizes.scss @@ -1,7 +1,7 @@ @use "../sizes" as *; $fluent-tagbox-tag-content-padding-left: null !default; -$fluent-tagbox-tag-content-padding-right: null !default; +$fluent-tagbox-tag-content-padding-right: 20px !default; $fluent-tagbox-tag-content-vertical-padding: null !default; $fluent-tagbox-tag-content-font-size: null !default; $fluent-tagbox-tag-container-padding-left: null !default; @@ -17,7 +17,6 @@ $fluent-tagbox-tag-container-gap: null !default; @if $size == "default" { $fluent-tagbox-tag-content-padding-left: 6px !default; - $fluent-tagbox-tag-content-padding-right: 20px !default; $fluent-tagbox-tag-content-vertical-padding: 2px !default; $fluent-tagbox-tag-content-font-size: 12px !default; $fluent-tagbox-tag-container-padding-left: 12px !default; @@ -31,7 +30,6 @@ $fluent-tagbox-tag-container-gap: null !default; @else if $size == "compact" { $fluent-tagbox-tag-content-padding-left: 4px !default; - $fluent-tagbox-tag-content-padding-right: 20px !default; $fluent-tagbox-tag-content-vertical-padding: 0 !default; $fluent-tagbox-tag-content-font-size: 10px !default; $fluent-tagbox-tag-container-padding-left: 8px !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/textEditor/_colors.scss b/packages/devextreme-scss/scss/widgets/fluent/textEditor/_colors.scss index b711bc2cce9d..40f6e1def3da 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/textEditor/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/textEditor/_colors.scss @@ -87,20 +87,20 @@ $texteditor-input-border-radius: $base-border-radius !default; $texteditor-label-transition: font-size 0.2s cubic-bezier(0, 0, 0.2, 1) 0ms, transform 0.2s cubic-bezier(0, 0, 0.2, 1) 0ms, - top 0.2s cubic-bezier(0, 0, 0.2, 1) 0ms; + top 0.2s cubic-bezier(0, 0, 0.2, 1) 0ms !default; @if $mode == "light" { + $texteditor-outlined-bg: $base-element-bg !default; + @if $color == "blue" { $texteditor-color: $base-text-color !default; $texteditor-placeholder-color: color.adjust($base-text-color, $lightness: 29.80%, $space: hsl) !default; - $texteditor-outlined-bg: $base-element-bg !default; $texteditor-filled-bg: color.adjust($base-element-bg, $lightness: -3.9%, $space: hsl) !default; } @if $color == "saas" { $texteditor-color: rgba(66, 81, 118, 1) !default; $texteditor-placeholder-color: rgba(144, 158, 192, 1) !default; - $texteditor-outlined-bg: $base-element-bg !default; $texteditor-filled-bg: rgba(243, 245, 248, 1) !default; } @@ -141,8 +141,8 @@ $texteditor-filled-bg-hover: $texteditor-filled-bg !default; */ $texteditor-filled-bg-focused: $texteditor-filled-bg !default; -$texteditor-label-color: $texteditor-placeholder-color; -$texteditor-label-color-focused: $base-accent; +$texteditor-label-color: $texteditor-placeholder-color !default; +$texteditor-label-color-focused: $base-accent !default; :root { --dx-texteditor-color-text: #{$texteditor-color}; diff --git a/packages/devextreme-scss/scss/widgets/fluent/textEditor/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/textEditor/_index.scss index 6ef8c31da61a..463672eeeafe 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/textEditor/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/textEditor/_index.scss @@ -12,10 +12,10 @@ @use "../../base/textEditor/mixins" as *; @use "../colorBox/sizes" as *; -$fluent-base-standard-texteditor-input-horizontal-padding: 0; -$fluent-texteditor-invalid-badge-size: $fluent-invalid-badge-size + 2 * $fluent-invalid-badge-horizontal-padding; -$fluent-editor-custom-button-margin: 6px; -$fluent-editor-border-width: 1px; +$fluent-base-standard-texteditor-input-horizontal-padding: 0 !default; +$fluent-texteditor-invalid-badge-size: $fluent-invalid-badge-size + 2 * $fluent-invalid-badge-horizontal-padding !default; +$fluent-editor-custom-button-margin: 6px !default; +$fluent-editor-border-width: 1px !default; @mixin texteditor-outlined-styling() { background-color: $texteditor-outlined-bg; diff --git a/packages/devextreme-scss/scss/widgets/fluent/textEditor/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/textEditor/_sizes.scss index eabf7d1cf17a..d49693d85091 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/textEditor/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/textEditor/_sizes.scss @@ -4,13 +4,13 @@ $fluent-texteditor-line-height: null !default; $fluent-texteditor-font-size: null !default; $fluent-texteditor-input-height: null !default; -$fluent-texteditor-label-top-offset: null !default; +$fluent-texteditor-label-top-offset: -1px !default; $fluent-texteditor-label-outside-top-offset: null !default; $fluent-filled-texteditor-input-horizontal-padding: null !default; $fluent-filled-texteditor-input-top-padding: null !default; $fluent-filled-texteditor-input-bottom-padding: null !default; $fluent-standard-texteditor-input-top-padding: null !default; -$fluent-underlined-editor-button-height: null !default; +$fluent-underlined-editor-button-height: 22px !default; $fluent-texteditor-label-font-size: null !default; $fluent-texteditor-label-outside-height: null !default; $fluent-standard-texteditor-input-bottom-padding: null !default; @@ -27,7 +27,6 @@ $texteditor-icon-container-size: $fluent-base-icon-size !default; $fluent-texteditor-label-outside-height: 20px !default; $fluent-texteditor-label-outside-top-offset: 24px !default; $fluent-texteditor-label-font-size: 12px !default; - $fluent-texteditor-label-top-offset: -1px !default; $fluent-filled-texteditor-input-horizontal-padding: 12px !default; $fluent-filled-texteditor-input-top-padding: 5px !default; $fluent-filled-texteditor-input-with-label-top-padding: 8px !default; @@ -37,7 +36,6 @@ $texteditor-icon-container-size: $fluent-base-icon-size !default; $fluent-standard-texteditor-input-bottom-padding: 5px !default; $fluent-standard-texteditor-with-label-input-top-padding: 9px !default; $fluent-standard-texteditor-with-label-input-bottom-padding: 3px !default; - $fluent-underlined-editor-button-height: 22px !default; } @else if $size == "compact" { @@ -47,7 +45,6 @@ $texteditor-icon-container-size: $fluent-base-icon-size !default; $fluent-texteditor-label-outside-height: 16px !default; $fluent-texteditor-label-outside-top-offset: 18px !default; $fluent-texteditor-label-font-size: 10px !default; - $fluent-texteditor-label-top-offset: -1px !default; $fluent-filled-texteditor-input-horizontal-padding: 8px !default; $fluent-filled-texteditor-input-top-padding: 3px !default; $fluent-filled-texteditor-input-with-label-top-padding: 6px !default; @@ -57,7 +54,6 @@ $texteditor-icon-container-size: $fluent-base-icon-size !default; $fluent-standard-texteditor-input-bottom-padding: 3px !default; $fluent-standard-texteditor-with-label-input-top-padding: 6px !default; $fluent-standard-texteditor-with-label-input-bottom-padding: 2px !default; - $fluent-underlined-editor-button-height: 22px !default; } $fluent-filled-texteditor-input-padding: diff --git a/packages/devextreme-scss/scss/widgets/fluent/tooltip/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/tooltip/_sizes.scss index 62ab9b9c048a..80bebbfef54f 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/tooltip/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/tooltip/_sizes.scss @@ -2,16 +2,14 @@ @use "../sizes" as *; $fluent-tooltip-padding: null !default; -$fluent-tooltip-font-size: null !default; +$fluent-tooltip-font-size: 12px !default; $fluent-tooltip-arrow-border-radius: 2px !default; $fluent-tooltip-shadow: 0 0 2px 0 $base-dropdown-shadow-color1 !default; @if $size == "default" { $fluent-tooltip-padding: 7px 11px !default; - $fluent-tooltip-font-size: 12px; } @else if $size == "compact" { $fluent-tooltip-padding: 3px 5px !default; - $fluent-tooltip-font-size: 12px; } diff --git a/packages/devextreme-scss/scss/widgets/fluent/treeView/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/treeView/_sizes.scss index 8f2518ae8958..8cb50f05e334 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/treeView/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/treeView/_sizes.scss @@ -1,18 +1,17 @@ @use "../sizes" as *; $fluent-treeview-toggle-item-width: null !default; -$fluent-treeview-item-height: null !default; $fluent-checkbox-container-size: null !default; -$fluent-treeview-container-with-border-offset: 8px; +$fluent-treeview-container-with-border-offset: 8px !default; $fluent-treeview-item-element-gap: 8px !default; -$fluent-treeview-item-margin: 1px 0; -$fluent-treeview-item-border-radius: 4px; -$fluent-treeview-border-padding: 1px; +$fluent-treeview-item-margin: 1px 0 !default; +$fluent-treeview-item-border-radius: 4px !default; +$fluent-treeview-border-padding: 1px !default; $fluent-treeview-container-padding: $fluent-treeview-border-padding $fluent-treeview-border-padding $fluent-treeview-border-padding - $fluent-treeview-container-with-border-offset; + $fluent-treeview-container-with-border-offset !default; $fluent-treeview-checkbox-offset: null !default; $fluent-select-all-item-padding: null !default; @@ -24,7 +23,6 @@ $fluent-treeview-item-side-padding: 12px !default; $fluent-select-all-item-border-visible-padding: 26px !default; @if $size == "default" { - $fluent-treeview-item-height: 44px !default; $fluent-treeview-toggle-item-width: 20px !default; $fluent-checkbox-container-size: 20px !default; $fluent-treeview-checkbox-offset: 42px !default; @@ -32,11 +30,10 @@ $fluent-select-all-item-border-visible-padding: 26px !default; $fluent-treeview-item-padding: 12px $fluent-treeview-item-side-padding !default; $fluent-treeview-load-indicator-size: 20px !default; $fluent-treeview-searchbox-margin-bottom: 8px !default; - $fluent-treeview-search-editor-height: 48px; + $fluent-treeview-search-editor-height: 48px !default; } @else if $size == "compact" { - $fluent-treeview-item-height: 32px !default; $fluent-treeview-toggle-item-width: 16px !default; $fluent-treeview-checkbox-offset: 36px !default; $fluent-checkbox-container-size: 16px !default; @@ -44,5 +41,5 @@ $fluent-select-all-item-border-visible-padding: 26px !default; $fluent-treeview-item-padding: 10px $fluent-treeview-item-side-padding !default; $fluent-treeview-load-indicator-size: 16px !default; $fluent-treeview-searchbox-margin-bottom: 4px !default; - $fluent-treeview-search-editor-height: 32px; + $fluent-treeview-search-editor-height: 32px !default; } diff --git a/packages/devextreme-scss/scss/widgets/fluent/typography/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/typography/_sizes.scss index 554959097d56..7a73a5756f5e 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/typography/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/typography/_sizes.scss @@ -4,7 +4,7 @@ $fluent-xl-font-size: null !default; $fluent-l-font-size: null !default; $fluent-m-font-size: null !default; $fluent-s-font-size: null !default; -$fluent-xs-font-size: null !default; +$fluent-xs-font-size: 12px !default; $fluent-h1-font-size: null !default; $fluent-h2-font-size: null !default; $fluent-h3-font-size: null !default; @@ -17,7 +17,6 @@ $fluent-h6-font-size: null !default; $fluent-l-font-size: 28px !default; $fluent-m-font-size: 20px !default; $fluent-s-font-size: 18px !default; - $fluent-xs-font-size: 12px !default; $fluent-h1-font-size: 40px !default; $fluent-h2-font-size: 36px !default; @@ -32,7 +31,6 @@ $fluent-h6-font-size: null !default; $fluent-l-font-size: 20px !default; $fluent-m-font-size: 16px !default; $fluent-s-font-size: 14px !default; - $fluent-xs-font-size: 12px !default; $fluent-h1-font-size: 32px !default; $fluent-h2-font-size: 28px !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/validation/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/validation/_sizes.scss index 408e81922fdd..f46cec6854f2 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/validation/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/validation/_sizes.scss @@ -1,6 +1,5 @@ @use "../sizes" as *; -$fluent-validation-message-padding: null !default; $fluent-validationsummary-item-margin: null !default; $fluent-validation-message-line-height: null !default; $fluent-validation-message-horizontal-padding: null !default; @@ -10,7 +9,6 @@ $fluent-validation-message-vertical-padding: 4px !default; $fluent-validation-message-font-size: 12px !default; @if $size == "default" { - $fluent-validation-message-padding: 6px !default; $fluent-validationsummary-item-margin: 6px !default; $fluent-validation-message-horizontal-padding: 8px !default; @@ -18,7 +16,6 @@ $fluent-validation-message-font-size: 12px !default; } @else if $size == "compact" { - $fluent-validation-message-padding: 4px !default; $fluent-validationsummary-item-margin: 4px !default; $fluent-validation-message-horizontal-padding: 6px !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/_colors.scss b/packages/devextreme-scss/scss/widgets/generic/_colors.scss index c0a20d72d71e..85a4533e8ef8 100644 --- a/packages/devextreme-scss/scss/widgets/generic/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/generic/_colors.scss @@ -124,8 +124,6 @@ $base-invalid-color: null !default; */ $base-invalid-faded-border-color: null !default; $base-header-color: null !default; - -$scrollview-pulldown-path: null !default; $base-default: null !default; $base-info: null !default; $screen-text-color: null !default; @@ -166,8 +164,6 @@ $base-grid-selectedrow-border-color: null !default; $base-invalid-color: $base-danger !default; $base-invalid-faded-border-color: color.change($base-invalid-color, $alpha: 0.4) !default; $base-header-color: #627789 !default; - - $scrollview-pulldown-path: data-uri("images/widgets/generic/color-schemes/carmine/pulldown.png") !default; } @if $color == "contrast" { @@ -194,8 +190,6 @@ $base-grid-selectedrow-border-color: null !default; $base-invalid-faded-border-color: $base-invalid-color !default; $base-label-color: $base-text-color !default; $base-dropdown-shadow-color: $base-shadow-color !default; - - $scrollview-pulldown-path: data-uri("images/widgets/generic/color-schemes/contrast/pulldown.png") !default; } @if $color == "dark" { @@ -226,8 +220,6 @@ $base-grid-selectedrow-border-color: null !default; $base-dropdown-shadow-color: color.change($base-shadow-color, $alpha: 0.175) !default; $base-invalid-color: $base-danger !default; $base-invalid-faded-border-color: color.change($base-invalid-color, $alpha: 0.4) !default; - - $scrollview-pulldown-path: data-uri("images/widgets/generic/color-schemes/dark/pulldown.png") !default; } @if $color == "darkmoon" { @@ -262,8 +254,6 @@ $base-grid-selectedrow-border-color: null !default; $base-invalid-faded-border-color: color.change($base-invalid-color, $alpha: 0.4) !default; $screen-text-color: extcolor.screen($base-bg, #686868) !default; $base-grid-selected-border-color: #45c7de !default; - - $scrollview-pulldown-path: data-uri("images/widgets/generic/color-schemes/darkmoon/pulldown.png") !default; } @if $color == "darkviolet" { @@ -298,8 +288,6 @@ $base-grid-selectedrow-border-color: null !default; $base-dropdown-shadow-color: color.change($base-shadow-color, $alpha: 0.175) !default; $base-invalid-color: $base-danger !default; $base-invalid-faded-border-color: color.change($base-invalid-color, $alpha: 0.4) !default; - - $scrollview-pulldown-path: data-uri("images/widgets/generic/color-schemes/darkviolet/pulldown.png") !default; } @if $color == "greenmist" { @@ -330,8 +318,6 @@ $base-grid-selectedrow-border-color: null !default; $base-dropdown-shadow-color: color.change($base-shadow-color, $alpha: 0.175) !default; $base-invalid-color: $base-danger !default; $base-invalid-faded-border-color: color.change($base-invalid-color, $alpha: 0.4) !default; - - $scrollview-pulldown-path: data-uri("images/widgets/generic/color-schemes/greenmist/pulldown.png") !default; } @if $color == "light" { @@ -362,8 +348,6 @@ $base-grid-selectedrow-border-color: null !default; $base-dropdown-shadow-color: color.change($base-shadow-color, $alpha: 0.175) !default; $base-invalid-color: $base-danger !default; $base-invalid-faded-border-color: color.change($base-invalid-color, $alpha: 0.4) !default; - - $scrollview-pulldown-path: data-uri("images/widgets/generic/color-schemes/light/pulldown.png") !default; } @if $color == "softblue" { @@ -399,8 +383,6 @@ $base-grid-selectedrow-border-color: null !default; $base-dropdown-shadow-color: color.change($base-shadow-color, $alpha: 0.175) !default; $base-invalid-color: $base-danger !default; $base-invalid-faded-border-color: color.change($base-invalid-color, $alpha: 0.4) !default; - - $scrollview-pulldown-path: data-uri("images/widgets/generic/color-schemes/softblue/pulldown.png") !default; } $typography-bg: $base-bg !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/_sizes.scss index bfcfe2484d0a..3ad8c9f221f2 100644 --- a/packages/devextreme-scss/scss/widgets/generic/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/generic/_sizes.scss @@ -69,7 +69,7 @@ $generic-base-inline-borderedwidget-bottom-padding: $generic-base-inline-widget-bottom-padding - $generic-base-border-width !default; -$generic-treeview-search-editor-height: math.round($generic-base-line-height * $generic-base-font-size) + $generic-base-inline-borderedwidget-top-padding + $generic-base-inline-borderedwidget-bottom-padding + 2 * $generic-base-border-width; +$generic-treeview-search-editor-height: math.round($generic-base-line-height * $generic-base-font-size) + $generic-base-inline-borderedwidget-top-padding + $generic-base-inline-borderedwidget-bottom-padding + 2 * $generic-base-border-width !default; :root { --dx-component-height: #{$generic-base-inline-widget-height}; diff --git a/packages/devextreme-scss/scss/widgets/generic/button/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/button/_sizes.scss index 8c648c6e3c96..d8302f0516c3 100644 --- a/packages/devextreme-scss/scss/widgets/generic/button/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/generic/button/_sizes.scss @@ -4,7 +4,6 @@ $generic-button-text-line-height: null !default; $generic-button-vertical-padding: null !default; $generic-button-horizontal-padding: null !default; $generic-button-icon-horizontal-padding: null !default; -$generic-button-box-shadow-size: null !default; $generic-button-border-weight: $generic-base-border-width !default; $generic-button-content-line-height: $generic-base-line-height !default; @@ -14,7 +13,6 @@ $generic-button-content-line-height: $generic-base-line-height !default; $generic-button-vertical-padding: 8px !default; $generic-button-horizontal-padding: 12px !default; $generic-button-icon-horizontal-padding: 8px !default; - $generic-button-box-shadow-size: 0 1px 3px !default; } @else if $size == "compact" { @@ -22,7 +20,6 @@ $generic-button-content-line-height: $generic-base-line-height !default; $generic-button-vertical-padding: 4px !default; $generic-button-horizontal-padding: 10px !default; $generic-button-icon-horizontal-padding: 5px !default; - $generic-button-box-shadow-size: 0 1px 2px !default; } :root { diff --git a/packages/devextreme-scss/scss/widgets/generic/cardView/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/cardView/_sizes.scss index e1f0ed77f527..561619679484 100644 --- a/packages/devextreme-scss/scss/widgets/generic/cardView/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/generic/cardView/_sizes.scss @@ -17,14 +17,14 @@ * $name 101. Border radius * $type text */ -$cardview-generic-common-border-radius: null !default; +$cardview-generic-common-border-radius: 16px !default; /** * $name 104. Text weight * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-generic-common-text-weight: null !default; +$cardview-generic-common-text-weight: 400 !default; /** * $name 105. Vertical gap between cards @@ -46,9 +46,9 @@ $cardview-generic-common-padding: null !default; // #region CARD -$cardview-generic-card-min-width: null !default; +$cardview-generic-card-min-width: 250px !default; -$cardview-generic-card-border-width: null !default; +$cardview-generic-card-border-width: 1px !default; /** * $name 202. Border radius @@ -67,7 +67,7 @@ $cardview-generic-card__header__selection-checkbox-padding-right: null !default; * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-generic-card__field-caption-weight: null !default; +$cardview-generic-card__field-caption-weight: 600 !default; $cardview-generic-card__cover-max-height: null !default; @@ -90,13 +90,13 @@ $cardview-generic-card__content__cell-padding-horizontal: null !default; * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-generic-card--selected__field-caption-weight: null !default; +$cardview-generic-card--selected__field-caption-weight: $cardview-generic-card__field-caption-weight !default; // #endregion // #region HEADER -$cardview-generic-header-gap: null !default; +$cardview-generic-header-gap: 12px !default; // #endregion @@ -110,13 +110,13 @@ $cardview-generic-header-panel--empty-line-height: null !default; // #region HEADER PANEL ITEM -$cardview-generic-header-panel__item-border-width: null !default; +$cardview-generic-header-panel__item-border-width: 1px !default; /** * $name 402. Border radius * $type text */ -$cardview-generic-header-panel__item-border-radius: null !default; +$cardview-generic-header-panel__item-border-radius: 4px !default; $cardview-generic-header-panel__item-padding-vertical: null !default; @@ -129,16 +129,16 @@ $cardview-generic-header-panel__item-line-height: null !default; * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-generic-header-panel__item-text-weight: null !default; +$cardview-generic-header-panel__item-text-weight: $cardview-generic-common-text-weight !default; /** * $name 408. Sort index text weight * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-generic-header-panel__item__sort-index-text-weight: null !default; +$cardview-generic-header-panel__item__sort-index-text-weight: 400 !default; -$cardview-generic-header-panel__item__icon-size: null !default; +$cardview-generic-header-panel__item__icon-size: $generic-base-icon-size !default; $cardview-generic-header-item__left-padding: null !default; $cardview-generic-header-item__right-padding: null !default; @@ -152,14 +152,14 @@ $cardview-generic-header-item__right-padding: null !default; * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-generic-header-panel__item--hovered-text-weight: null !default; +$cardview-generic-header-panel__item--hovered-text-weight: $cardview-generic-header-panel__item-text-weight !default; /** * $name 507. Sort index text weight * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-generic-header-panel__item--hovered__sort-index-text-weight: null !default; +$cardview-generic-header-panel__item--hovered__sort-index-text-weight: $cardview-generic-header-panel__item__sort-index-text-weight !default; // #endregion @@ -170,14 +170,14 @@ $cardview-generic-header-panel__item--hovered__sort-index-text-weight: null !def * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-generic-header-panel__item--disabled-text-weight: null !default; +$cardview-generic-header-panel__item--disabled-text-weight: $cardview-generic-header-panel__item-text-weight !default; /** * $name 607. Sort index text weight * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-generic-header-panel__item--disabled__sort-index-text-weight: null !default; +$cardview-generic-header-panel__item--disabled__sort-index-text-weight: $cardview-generic-header-panel__item__sort-index-text-weight !default; // #endregion @@ -188,10 +188,10 @@ $cardview-generic-header-panel__item--disabled__sort-index-text-weight: null !de * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-generic-header-panel__item--dragged-text-weight: null !default; +$cardview-generic-header-panel__item--dragged-text-weight: $cardview-generic-header-panel__item-text-weight !default; -$cardview-generic-header-item__sort-indicator__border-size: null; -$cardview-generic-header-item__sort-indicator__border-radius: null; +$cardview-generic-header-item__sort-indicator__border-size: null !default; +$cardview-generic-header-item__sort-indicator__border-radius: null !default; // #endregion @@ -199,9 +199,9 @@ $cardview-generic-header-item__sort-indicator__border-radius: null; $cardview-generic-header-panel__dropzone-height: null !default; -$cardview-generic-header-panel__dropzone-top: null !default; +$cardview-generic-header-panel__dropzone-top: 0 !default; -$cardview-generic-header-panel__dropzone-border-width: null !default; +$cardview-generic-header-panel__dropzone-border-width: 1.5px !default; /** * $name 801. Border radius @@ -217,7 +217,7 @@ $cardview-generic-header-panel__dropzone-border-radius: null !default; * $name 901. Border radius * $type text */ -$cardview-generic-filter-panel-border-radius: null !default; +$cardview-generic-filter-panel-border-radius: 0 !default; // #endregion @@ -237,16 +237,16 @@ $cardview-generic-nodata-view__icon-size: null !default; * $name 1101. Border radius * $type text */ -$cardview-generic-card__content__field-value--highlighted-border-radius: null !default; +$cardview-generic-card__content__field-value--highlighted-border-radius: 5px !default; -$cardview-generic-card__content__field-value--highlighted-border-padding: null !default; +$cardview-generic-card__content__field-value--highlighted-border-padding: 0.125em 0.2em !default; /** * $name 1103. Text weight * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-generic-card__content__field-value--highlighted-text-weight: null !default; +$cardview-generic-card__content__field-value--highlighted-text-weight: 500 !default; // #endregion @@ -261,21 +261,16 @@ $cardview-generic-column-chooser__select-all-item-horizontal-padding: null !defa @if $size == "default" { // Common - $cardview-generic-common-border-radius: 16px !default; - $cardview-generic-common-text-weight: 400 !default; $cardview-generic-common-cards-row-gap: 16px !default; $cardview-generic-common-cards-column-gap: 16px !default; $cardview-generic-common-gap: 20px !default; $cardview-generic-common-padding: 24px !default; // Card - $cardview-generic-card-min-width: 250px !default; - $cardview-generic-card-border-width: 1px !default; $cardview-generic-card-border-radius: 8px !default; $cardview-generic-card__header-height: 52px !default; $cardview-generic-card__header-text-size: 16px !default; $cardview-generic-card__header__selection-checkbox-padding-right: 12px !default; - $cardview-generic-card__field-caption-weight: 600 !default; $cardview-generic-card__cover-max-height: 180px !default; $cardview-generic-card__cover--noimage__icon-size: 48px !default; $cardview-generic-card__content-padding: 12px !default; @@ -283,60 +278,30 @@ $cardview-generic-column-chooser__select-all-item-horizontal-padding: null !defa $cardview-generic-card__content__cell-padding-vertical: 6px !default; $cardview-generic-card__content__cell-padding-horizontal: 12px !default; - // Card - selected - $cardview-generic-card--selected__field-caption-weight: $cardview-generic-card__field-caption-weight !default; - - // Header - $cardview-generic-header-gap: 12px !default; - // Header panel $cardview-generic-header-panel-vertical-margin: 4px !default; $cardview-generic-header-panel--empty-line-height: 36px !default; // Header panel item - default - $cardview-generic-header-panel__item-border-width: 1px !default; - $cardview-generic-header-panel__item-border-radius: 4px !default; $cardview-generic-header-panel__item-padding-horizontal: 12px !default; $cardview-generic-header-panel__item-padding-vertical: 8px !default; $cardview-generic-header-item__left-padding: 6px !default; $cardview-generic-header-item__right-padding: 2px !default; $cardview-generic-header-panel__item-line-height: 18px !default; - $cardview-generic-header-panel__item-text-weight: $cardview-generic-common-text-weight !default; - $cardview-generic-header-panel__item__sort-index-text-weight: 400 !default; - $cardview-generic-header-panel__item__icon-size: $generic-base-icon-size !default; - - // Header panel item - hovered - $cardview-generic-header-panel__item--hovered-text-weight: $cardview-generic-header-panel__item-text-weight !default; - $cardview-generic-header-panel__item--hovered__sort-index-text-weight: $cardview-generic-header-panel__item__sort-index-text-weight !default; - - // Header panel item - disabled - $cardview-generic-header-panel__item--disabled-text-weight: $cardview-generic-header-panel__item-text-weight !default; - $cardview-generic-header-panel__item--disabled__sort-index-text-weight: $cardview-generic-header-panel__item__sort-index-text-weight !default; // Header panel item - dragged - $cardview-generic-header-panel__item--dragged-text-weight: $cardview-generic-header-panel__item-text-weight !default; $cardview-generic-header-item__sort-indicator__border-size: 2px !default; $cardview-generic-header-item__sort-indicator__border-radius: 8px !default; // Header panel Drop-zone $cardview-generic-header-panel__dropzone-height: 36px !default; - $cardview-generic-header-panel__dropzone-top: 0 !default; - $cardview-generic-header-panel__dropzone-border-width: 1.5px !default; $cardview-generic-header-panel__dropzone-border-radius: 8px !default; - // Filter panel - $cardview-generic-filter-panel-border-radius: 0 !default; - // Empty view $cardview-generic-nodata-view__icon-container-size: 60px !default; $cardview-generic-nodata-view__icon-container-margin: 12px !default; $cardview-generic-nodata-view__icon-size: 36px !default; - // Search highlight - $cardview-generic-card__content__field-value--highlighted-border-radius: 5px !default; - $cardview-generic-card__content__field-value--highlighted-border-padding: 0.125em 0.2em !default; - $cardview-generic-card__content__field-value--highlighted-text-weight: 500 !default; - // Column chooser $cardview-generic-column-chooser--empty__message-horizontal-padding: 24px !default; $cardview-generic-column-chooser__select-all-item-horizontal-padding: 10px !default; @@ -344,21 +309,16 @@ $cardview-generic-column-chooser__select-all-item-horizontal-padding: null !defa @else if $size == "compact" { // Common - $cardview-generic-common-border-radius: 16px !default; - $cardview-generic-common-text-weight: 400 !default; $cardview-generic-common-cards-row-gap: 12px !default; $cardview-generic-common-cards-column-gap: 12px !default; $cardview-generic-common-gap: 16px !default; $cardview-generic-common-padding: 16px !default; // Card - $cardview-generic-card-min-width: 250px !default; - $cardview-generic-card-border-width: 1px !default; $cardview-generic-card-border-radius: 6px !default; $cardview-generic-card__header-height: 48px !default; $cardview-generic-card__header-text-size: 14px !default; $cardview-generic-card__header__selection-checkbox-padding-right: 8px !default; - $cardview-generic-card__field-caption-weight: 600 !default; $cardview-generic-card__cover-max-height: 140px !default; $cardview-generic-card__cover--noimage__icon-size: 32px !default; $cardview-generic-card__content-padding: 8px !default; @@ -366,60 +326,30 @@ $cardview-generic-column-chooser__select-all-item-horizontal-padding: null !defa $cardview-generic-card__content__cell-padding-vertical: 4px !default; $cardview-generic-card__content__cell-padding-horizontal: 8px !default; - // Card - selected - $cardview-generic-card--selected__field-caption-weight: $cardview-generic-card__field-caption-weight !default; - - // Header - $cardview-generic-header-gap: 12px !default; - // Header panel $cardview-generic-header-panel-vertical-margin: 0 !default; $cardview-generic-header-panel--empty-line-height: 24px !default; // Header panel item - default - $cardview-generic-header-panel__item-border-width: 1px !default; - $cardview-generic-header-panel__item-border-radius: 4px !default; $cardview-generic-header-panel__item-padding-horizontal: 8px !default; $cardview-generic-header-panel__item-padding-vertical: 5px !default; $cardview-generic-header-item__left-padding: 3px !default; $cardview-generic-header-item__right-padding: 1px !default; $cardview-generic-header-panel__item-line-height: 14px !default; - $cardview-generic-header-panel__item-text-weight: $cardview-generic-common-text-weight !default; - $cardview-generic-header-panel__item__sort-index-text-weight: 400 !default; - $cardview-generic-header-panel__item__icon-size: $generic-base-icon-size !default; - - // Header panel item - hovered - $cardview-generic-header-panel__item--hovered-text-weight: $cardview-generic-header-panel__item-text-weight !default; - $cardview-generic-header-panel__item--hovered__sort-index-text-weight: $cardview-generic-header-panel__item__sort-index-text-weight !default; - - // Header panel item - disabled - $cardview-generic-header-panel__item--disabled-text-weight: $cardview-generic-header-panel__item-text-weight !default; - $cardview-generic-header-panel__item--disabled__sort-index-text-weight: $cardview-generic-header-panel__item__sort-index-text-weight !default; // Header panel item - dragged - $cardview-generic-header-panel__item--dragged-text-weight: $cardview-generic-header-panel__item-text-weight !default; $cardview-generic-header-item__sort-indicator__border-size: 1px !default; $cardview-generic-header-item__sort-indicator__border-radius: 4px !default; // Header panel Drop-zone $cardview-generic-header-panel__dropzone-height: 24px !default; - $cardview-generic-header-panel__dropzone-top: 0 !default; - $cardview-generic-header-panel__dropzone-border-width: 1.5px !default; $cardview-generic-header-panel__dropzone-border-radius: 6px !default; - // Filter panel - $cardview-generic-filter-panel-border-radius: 0 !default; - // Empty view $cardview-generic-nodata-view__icon-container-size: 40px !default; $cardview-generic-nodata-view__icon-container-margin: 8px !default; $cardview-generic-nodata-view__icon-size: 24px !default; - // Search highlight - $cardview-generic-card__content__field-value--highlighted-border-radius: 5px !default; - $cardview-generic-card__content__field-value--highlighted-border-padding: 0.125em 0.2em !default; - $cardview-generic-card__content__field-value--highlighted-text-weight: 500 !default; - // Column chooser $cardview-generic-column-chooser--empty__message-horizontal-padding: 16px !default; $cardview-generic-column-chooser__select-all-item-horizontal-padding: 6px !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/chat/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/chat/_sizes.scss index d7d7f7342931..5c6de4412cf1 100644 --- a/packages/devextreme-scss/scss/widgets/generic/chat/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/generic/chat/_sizes.scss @@ -59,7 +59,6 @@ $chat-messagelist-day-header-first-padding-top: 20px !default; $chat-messagelist-day-header-font-size: 12px !default; $chat-typingindicator-template: $chat-messagegroup-alignment-start-template !default; -$chat-typingindicator-padding: null !default; $chat-typingindicator-row-gap: $chat-messagegroup-gap !default; $chat-typingindicator-font-size: $chat-information-font-size !default; $chat-typingindicator-bubble-padding-block: null !default; @@ -119,7 +118,6 @@ $chat-bubble-gap: 8px !default; $chat-typingindicator-circle-size-center: math.div(12, 14) * 1em !default; $chat-typingindicator-bubble-padding-block: 12px !default; $chat-typingindicator-bubble-padding-inline: 20px !default; - $chat-typingindicator-padding: $chat-messagegroup-padding !default; $chat-confirmation-popup-content-padding: 0 !default; $chat-confirmation-popup-toolbar-gap: 8px !default; @@ -160,10 +158,11 @@ $chat-bubble-gap: 8px !default; $chat-typingindicator-circle-size-center: math.div(12, 12) * 1em !default; $chat-typingindicator-bubble-padding-block: 8px !default; $chat-typingindicator-bubble-padding-inline: 12px !default; - $chat-typingindicator-padding: $chat-messagegroup-padding !default; $chat-confirmation-popup-content-padding: $generic-popup-content-padding !default; $chat-confirmation-popup-toolbar-gap: 4px !default; $chat-file-icon-font-size: 14 !default; } + +$chat-typingindicator-padding: $chat-messagegroup-padding !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/checkBox/_index.scss b/packages/devextreme-scss/scss/widgets/generic/checkBox/_index.scss index 740205c5095b..9a808cc9f92a 100644 --- a/packages/devextreme-scss/scss/widgets/generic/checkBox/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/checkBox/_index.scss @@ -4,7 +4,7 @@ @use "../../base/checkBox"; @use "../../base/checkBox/mixins" as *; -$generic-checkbox-disable-opacity: 0.4; +$generic-checkbox-disable-opacity: 0.4 !default; .dx-checkbox { line-height: 0; diff --git a/packages/devextreme-scss/scss/widgets/generic/checkBox/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/checkBox/_sizes.scss index 4d759dc73135..bda37bf216e9 100644 --- a/packages/devextreme-scss/scss/widgets/generic/checkBox/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/generic/checkBox/_sizes.scss @@ -5,7 +5,7 @@ $generic-checkbox-icon-font-size: null !default; $generic-checkbox-border-width: 1px !default; -$generic-checkbox-border-radius: null !default; +$generic-checkbox-border-radius: if($base-border-radius-exsmall, min($base-border-radius-exsmall, 2px), null) !default; $generic-checkbox-arrow-icon-size: 1em !default; $generic-checkbox-arrow-font-size: null !default; @@ -16,12 +16,10 @@ $generic-checkbox-indeterminate-icon-border-radius: 2px !default; $generic-checkbox-icon-font-size: 22px !default; $generic-checkbox-arrow-font-size: 0.727273em !default; $generic-checkbox-indeterminate-icon-size: 0.545455em !default; - $generic-checkbox-border-radius: if($base-border-radius-exsmall, min($base-border-radius-exsmall, 2px), null) !default; } @else if $size == "compact" { $generic-checkbox-icon-font-size: 16px !default; $generic-checkbox-arrow-font-size: 0.625em !default; $generic-checkbox-indeterminate-icon-size: 0.5em !default; - $generic-checkbox-border-radius: if($base-border-radius-exsmall, min($base-border-radius-exsmall, 2px), null) !default; } diff --git a/packages/devextreme-scss/scss/widgets/generic/colorView/_index.scss b/packages/devextreme-scss/scss/widgets/generic/colorView/_index.scss index 05284c86677b..d304e8f7c8d0 100644 --- a/packages/devextreme-scss/scss/widgets/generic/colorView/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/colorView/_index.scss @@ -4,8 +4,8 @@ @use "../../base/colorView"; @include dx-base-colorview-styles($colorview-handle-color, $colorview-handle-border-color); -$generic-colorview-alpha-width: $generic-colorview-palette-width + 4; -$generic-colorview-hue-scale-height: $generic-colorview-palette-height; +$generic-colorview-alpha-width: $generic-colorview-palette-width + 4 !default; +$generic-colorview-hue-scale-height: $generic-colorview-palette-height !default; .dx-colorview-container { width: $generic-colorview-container-width; diff --git a/packages/devextreme-scss/scss/widgets/generic/common/_mixins.scss b/packages/devextreme-scss/scss/widgets/generic/common/_mixins.scss index c4e2389d750a..004e85bb20cb 100644 --- a/packages/devextreme-scss/scss/widgets/generic/common/_mixins.scss +++ b/packages/devextreme-scss/scss/widgets/generic/common/_mixins.scss @@ -6,10 +6,10 @@ @use "../../base/validation" as *; @use "../list/sizes" as *; -$badge-invalid-bg: $base-invalid-color; +$badge-invalid-bg: $base-invalid-color !default; $generic-dropdownlist-min-height: $generic-base-font-size * $generic-base-line-height + - 2 * $generic-list-item-vertical-padding; + 2 * $generic-list-item-vertical-padding !default; @mixin dx-base-typography() { @include dx-base-typography-mixin( diff --git a/packages/devextreme-scss/scss/widgets/generic/contextMenu/_index.scss b/packages/devextreme-scss/scss/widgets/generic/contextMenu/_index.scss index 4fe335a1b57b..5dddf82d1e49 100644 --- a/packages/devextreme-scss/scss/widgets/generic/contextMenu/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/contextMenu/_index.scss @@ -8,7 +8,7 @@ @use "../overlay"; @use "../menuBase" as *; // need as private -$generic-menu-popup-border: 1px solid; +$generic-menu-popup-border: 1px solid !default; .dx-context-menu { color: $menu-color; diff --git a/packages/devextreme-scss/scss/widgets/generic/dateView/_index.scss b/packages/devextreme-scss/scss/widgets/generic/dateView/_index.scss index f769f5703721..9a546976aa44 100644 --- a/packages/devextreme-scss/scss/widgets/generic/dateView/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/dateView/_index.scss @@ -5,7 +5,7 @@ @use "../../base/dateView"; @use "../scrollable"; -$generic-dateview-roller-padding: math.div($generic-dateview-roller-height - $generic-dateview-roller-item-height, 2); +$generic-dateview-roller-padding: math.div($generic-dateview-roller-height - $generic-dateview-roller-item-height, 2) !default; .dx-dateview-rollers { width: auto; diff --git a/packages/devextreme-scss/scss/widgets/generic/diagram/_index.scss b/packages/devextreme-scss/scss/widgets/generic/diagram/_index.scss index db120bcabaf2..f226c1eabec4 100644 --- a/packages/devextreme-scss/scss/widgets/generic/diagram/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/diagram/_index.scss @@ -14,11 +14,11 @@ @use "../fieldset/sizes" as *; @use "../menu/colors" as *; -$generic-diagram-toolbar-icon-size: $generic-base-icon-size; -$diagram-toolbar-colorbutton-border-width: 3px; -$diagram-touchbar-item-padding: 5px; +$generic-diagram-toolbar-icon-size: $generic-base-icon-size !default; +$diagram-toolbar-colorbutton-border-width: 3px !default; +$diagram-touchbar-item-padding: 5px !default; -$diagram-toolbox-title-size: 12px; +$diagram-toolbox-title-size: 12px !default; .dx-diagram { border-color: $diagram-border-color; diff --git a/packages/devextreme-scss/scss/widgets/generic/dropDownEditor/_index.scss b/packages/devextreme-scss/scss/widgets/generic/dropDownEditor/_index.scss index e9ba41d0d338..6656681ab031 100644 --- a/packages/devextreme-scss/scss/widgets/generic/dropDownEditor/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/dropDownEditor/_index.scss @@ -8,20 +8,20 @@ @use "../../base/icon_fonts" as *; @use "../../base/dropDownEditor"; -$generic-dropdowneditor-container-button-size: $generic-base-inline-borderedwidget-inner-size; -$generic-dropdowneditor-button-offset: $generic-base-border-width; +$generic-dropdowneditor-container-button-size: $generic-base-inline-borderedwidget-inner-size !default; +$generic-dropdowneditor-button-offset: $generic-base-border-width !default; $generic-dropdowneditor-button-size: $generic-dropdowneditor-container-button-size - - $generic-dropdowneditor-button-offset * 2; + $generic-dropdowneditor-button-offset * 2 !default; $generic-dropdowneditor-invalid-badge-offset: $generic-dropdowneditor-container-button-size + - $generic-invalid-badge-halfhorizontal-padding; + $generic-invalid-badge-halfhorizontal-padding !default; -$generic-dropdowneditor-invalid-badge-with-clear-offset: $generic-dropdowneditor-container-button-size * 2; +$generic-dropdowneditor-invalid-badge-with-clear-offset: $generic-dropdowneditor-container-button-size * 2 !default; -$generic-dropdowneditor-invalid-badge-size: $generic-invalid-badge-size + $generic-invalid-badge-horizontal-padding; +$generic-dropdowneditor-invalid-badge-size: $generic-invalid-badge-size + $generic-invalid-badge-horizontal-padding !default; @mixin dx-dropdowneditor-button-icon() { width: $generic-dropdowneditor-button-size; diff --git a/packages/devextreme-scss/scss/widgets/generic/fieldset/_index.scss b/packages/devextreme-scss/scss/widgets/generic/fieldset/_index.scss index 0ea3fc18ff59..a6c5d42e5a62 100644 --- a/packages/devextreme-scss/scss/widgets/generic/fieldset/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/fieldset/_index.scss @@ -11,8 +11,8 @@ @use "../slider/sizes" as *; @use "../../base/fieldset"; -$generic-field-min-height: $generic-base-inline-widget-height; -$generic-fieldset-attention-icon-offset: $generic-base-inline-horizontal-padding + $generic-invalid-badge-size; +$generic-field-min-height: $generic-base-inline-widget-height !default; +$generic-fieldset-attention-icon-offset: $generic-base-inline-horizontal-padding + $generic-invalid-badge-size !default; @mixin dx-fieldset-styling() { .dx-field { diff --git a/packages/devextreme-scss/scss/widgets/generic/fileManager/_colors.scss b/packages/devextreme-scss/scss/widgets/generic/fileManager/_colors.scss index e7a8071ba5b7..28c2fc9c26b0 100644 --- a/packages/devextreme-scss/scss/widgets/generic/fileManager/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/generic/fileManager/_colors.scss @@ -14,19 +14,15 @@ $filemanager-file-item-select-bg: color.change($base-accent, $alpha: 0.8) !defau $filemanager-file-item-select-focus-color: $base-bg !default; $filemanager-file-item-select-focus-bg: color.change($base-accent, $alpha: 0.7) !default; $filemanager-file-item-hover-bg: null !default; -$filemanager-file-item-focused-bg: null !default; $filemanager-file-item-focused-border: $base-accent !default; $filemanager-item-focused-color: null !default; -$filemanager-progressbox-shadow-color: null !default; @if $color == "carmine" { $filemanager-file-toolbar-bg: color.adjust($base-bg, $lightness: -3%, $space: hsl) !default; $filemanager-dirs-tree-item-focus-bg: color.adjust($base-bg, $lightness: -8%, $space: hsl) !default; $filemanager-dirs-tree-item-focus-color: $base-text-color !default; $filemanager-file-item-hover-bg: $base-hover-bg !default; - $filemanager-file-item-focused-bg: $base-accent !default; $filemanager-item-focused-color: $base-inverted-text-color !default; - $filemanager-progressbox-shadow-color: color.change($base-shadow-color, $alpha: 0.175) !default; } @if $color == "contrast" { @@ -34,9 +30,7 @@ $filemanager-progressbox-shadow-color: null !default; $filemanager-dirs-tree-item-focus-bg: $base-text-color !default; $filemanager-dirs-tree-item-focus-color: $base-bg !default; $filemanager-file-item-hover-bg: $base-hover-color !default; - $filemanager-file-item-focused-bg: $base-accent !default; $filemanager-item-focused-color: $base-text-color !default; - $filemanager-progressbox-shadow-color: $base-shadow-color !default; } @if $color == "dark" { @@ -44,9 +38,7 @@ $filemanager-progressbox-shadow-color: null !default; $filemanager-dirs-tree-item-focus-bg: color.adjust($base-bg, $lightness: 8%, $space: hsl) !default; $filemanager-dirs-tree-item-focus-color: $base-text-color !default; $filemanager-file-item-hover-bg: $base-hover-bg !default; - $filemanager-file-item-focused-bg: $base-focus-bg !default; $filemanager-item-focused-color: $base-inverted-text-color !default; - $filemanager-progressbox-shadow-color: color.change($base-shadow-color, $alpha: 0.3) !default; } @if $color == "darkmoon" { @@ -54,9 +46,7 @@ $filemanager-progressbox-shadow-color: null !default; $filemanager-dirs-tree-item-focus-bg: color.adjust($base-bg, $lightness: 8%, $space: hsl) !default; $filemanager-dirs-tree-item-focus-color: $base-text-color !default; $filemanager-file-item-hover-bg: $base-hover-bg !default; - $filemanager-file-item-focused-bg: $base-accent !default; $filemanager-item-focused-color: $base-inverted-text-color !default; - $filemanager-progressbox-shadow-color: color.change(color.adjust($base-shadow-color, $lightness: -100%, $space: hsl), $alpha: 0.3) !default; } @if $color == "darkviolet" { @@ -64,9 +54,7 @@ $filemanager-progressbox-shadow-color: null !default; $filemanager-dirs-tree-item-focus-bg: color.adjust($base-bg, $lightness: 8%, $space: hsl) !default; $filemanager-dirs-tree-item-focus-color: $base-text-color !default; $filemanager-file-item-hover-bg: $base-hover-bg !default; - $filemanager-file-item-focused-bg: $base-focus-bg !default; $filemanager-item-focused-color: $base-inverted-text-color !default; - $filemanager-progressbox-shadow-color: color.change($base-shadow-color, $alpha: 0.3) !default; } @if $color == "greenmist" { @@ -74,9 +62,7 @@ $filemanager-progressbox-shadow-color: null !default; $filemanager-dirs-tree-item-focus-bg: color.adjust($base-bg, $lightness: -8%, $space: hsl) !default; $filemanager-dirs-tree-item-focus-color: $base-text-color !default; $filemanager-file-item-hover-bg: $base-hover-bg !default; - $filemanager-file-item-focused-bg: $base-accent !default; $filemanager-item-focused-color: $base-inverted-text-color !default; - $filemanager-progressbox-shadow-color: color.change($base-shadow-color, $alpha: 0.175) !default; } @if $color == "light" { @@ -84,9 +70,7 @@ $filemanager-progressbox-shadow-color: null !default; $filemanager-dirs-tree-item-focus-bg: color.adjust($base-bg, $lightness: -8%, $space: hsl) !default; $filemanager-dirs-tree-item-focus-color: $base-text-color !default; $filemanager-file-item-hover-bg: $base-hover-bg !default; - $filemanager-file-item-focused-bg: $base-focus-bg !default; $filemanager-item-focused-color: $base-inverted-text-color !default; - $filemanager-progressbox-shadow-color: color.change($base-shadow-color, $alpha: 0.175) !default; } @if $color == "softblue" { @@ -94,7 +78,5 @@ $filemanager-progressbox-shadow-color: null !default; $filemanager-dirs-tree-item-focus-bg: color.adjust($base-bg, $lightness: -8%, $space: hsl) !default; $filemanager-dirs-tree-item-focus-color: $base-text-color !default; $filemanager-file-item-hover-bg: $base-hover-bg !default; - $filemanager-file-item-focused-bg: $base-focus-bg !default; $filemanager-item-focused-color: $base-inverted-text-color !default; - $filemanager-progressbox-shadow-color: color.change($base-shadow-color, $alpha: 0.175) !default; } diff --git a/packages/devextreme-scss/scss/widgets/generic/fileManager/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/fileManager/_sizes.scss index 50a1d5bc883e..ff43e8aea566 100644 --- a/packages/devextreme-scss/scss/widgets/generic/fileManager/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/generic/fileManager/_sizes.scss @@ -4,7 +4,7 @@ $filemanager-toolbar-separator-height: null !default; $filemanager-toolbar-viewmode-width: null !default; $filemanager-toolbar-viewmode-padding: null !default; -$filemanager-toolbar-height: null !default; +$filemanager-toolbar-height: $generic-toolbar-height + 12px !default; $filemanager-breadcrumbs-item-padding-top: null !default; $filemanager-breadcrumbs-item-padding-bottom: null !default; $filemanager-details-view-checkbox-column-width: null !default; @@ -21,7 +21,6 @@ $filemanager-progress-panel-padding-top: 6px !default; $filemanager-toolbar-separator-height: 36px !default; $filemanager-toolbar-viewmode-width: 150px !default; $filemanager-toolbar-viewmode-padding: 8px !default; - $filemanager-toolbar-height: $generic-toolbar-height + 12px; $filemanager-breadcrumbs-item-padding-top: 5px !default; $filemanager-breadcrumbs-item-padding-bottom: 8px !default; $filemanager-details-view-checkbox-column-width: 43px !default; @@ -37,7 +36,6 @@ $filemanager-progress-panel-padding-top: 6px !default; $filemanager-toolbar-separator-height: 26px !default; $filemanager-toolbar-viewmode-width: 130px !default; $filemanager-toolbar-viewmode-padding: 5px !default; - $filemanager-toolbar-height: $generic-toolbar-height + 12px; $filemanager-breadcrumbs-item-padding-top: 3px !default; $filemanager-breadcrumbs-item-padding-bottom: 7px !default; $filemanager-details-view-checkbox-column-width: 39px !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/fileUploader/_index.scss b/packages/devextreme-scss/scss/widgets/generic/fileUploader/_index.scss index db8ac6f2827f..d9c1d0c3c4b5 100644 --- a/packages/devextreme-scss/scss/widgets/generic/fileUploader/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/fileUploader/_index.scss @@ -6,7 +6,7 @@ @use "../../base/mixins" as *; @use "../../base/fileUploader"; -$generic-fileuploader-file-wrapper-border-size: 3px; +$generic-fileuploader-file-wrapper-border-size: 3px !default; .dx-fileuploader-wrapper { padding: $generic-fileuploader-vertical-padding; diff --git a/packages/devextreme-scss/scss/widgets/generic/filterBuilder/_colors.scss b/packages/devextreme-scss/scss/widgets/generic/filterBuilder/_colors.scss index 151208112147..238ce96d88f5 100644 --- a/packages/devextreme-scss/scss/widgets/generic/filterBuilder/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/generic/filterBuilder/_colors.scss @@ -67,7 +67,7 @@ $filterbuilder-item-value-focus-color: null !default; * $name 10. Menu icon color * $type color */ -$filterbuilder-menu-icon-color: null !default; +$filterbuilder-menu-icon-color: $base-text-color !default; @if $color == "carmine" { $filterbuilder-text-color: $base-text-color !default; @@ -90,7 +90,6 @@ $filterbuilder-menu-icon-color: null !default; $filterbuilder-item-value-color: color.change($base-border-color, $alpha: 0.5) !default; $filterbuilder-item-value-hover-color: $filterbuilder-item-value-color !default; $filterbuilder-item-value-focus-color: color.adjust($filterbuilder-item-value-color, $alpha: 0.7) !default; - $filterbuilder-menu-icon-color: $base-text-color !default; } @if $color == "contrast" { @@ -114,7 +113,6 @@ $filterbuilder-menu-icon-color: null !default; $filterbuilder-item-value-focus-color: $filterbuilder-item-value-color !default; $filterbuilder-text-color: $base-text-color !default; $filterbuilder-text-focus-color: $base-text-color !default; - $filterbuilder-menu-icon-color: $base-text-color !default; } @if $color == "dark" { @@ -138,13 +136,11 @@ $filterbuilder-menu-icon-color: null !default; $filterbuilder-item-value-color: rgba(204, 204, 204, 0.8) !default; $filterbuilder-item-value-hover-color: color.adjust($filterbuilder-item-value-color, $alpha: -0.1) !default; $filterbuilder-item-value-focus-color: color.adjust($filterbuilder-item-value-color, $alpha: 0.2) !default; - $filterbuilder-menu-icon-color: $base-text-color !default; } @if $color == "darkmoon" { $filterbuilder-text-color: #000 !default; $filterbuilder-text-focus-color: $filterbuilder-text-color !default; - $filterbuilder-menu-icon-color: $base-text-color !default; $filterbuilder-plus-icon-color: color.change($base-success, $alpha: 0.8) !default; $filterbuilder-plus-icon-hover-color: color.adjust($filterbuilder-plus-icon-color, $alpha: -0.1) !default; $filterbuilder-plus-icon-focus-color: color.adjust($filterbuilder-plus-icon-color, $alpha: 0.2) !default; @@ -186,7 +182,6 @@ $filterbuilder-menu-icon-color: null !default; $filterbuilder-item-value-color: rgba(204, 204, 204, 0.8) !default; $filterbuilder-item-value-hover-color: color.adjust($filterbuilder-item-value-color, $alpha: -0.1) !default; $filterbuilder-item-value-focus-color: color.adjust($filterbuilder-item-value-color, $alpha: 0.2) !default; - $filterbuilder-menu-icon-color: $base-text-color !default; } @if $color == "greenmist" { @@ -210,7 +205,6 @@ $filterbuilder-menu-icon-color: null !default; $filterbuilder-item-value-color: color.change($base-border-color, $alpha: 0.5) !default; $filterbuilder-item-value-hover-color: $filterbuilder-item-value-color !default; $filterbuilder-item-value-focus-color: color.adjust($filterbuilder-item-value-color, $alpha: 0.7) !default; - $filterbuilder-menu-icon-color: $base-text-color !default; } @if $color == "light" { @@ -234,7 +228,6 @@ $filterbuilder-menu-icon-color: null !default; $filterbuilder-item-value-color: color.change($base-border-color, $alpha: 0.5) !default; $filterbuilder-item-value-hover-color: $filterbuilder-item-value-color !default; $filterbuilder-item-value-focus-color: color.adjust($filterbuilder-item-value-color, $alpha: 0.7) !default; - $filterbuilder-menu-icon-color: $base-text-color !default; } @if $color == "softblue" { @@ -258,5 +251,4 @@ $filterbuilder-menu-icon-color: null !default; $filterbuilder-item-value-color: color.change($base-border-color, $alpha: 0.5) !default; $filterbuilder-item-value-hover-color: $filterbuilder-item-value-color !default; $filterbuilder-item-value-focus-color: color.adjust($filterbuilder-item-value-color, $alpha: 0.7) !default; - $filterbuilder-menu-icon-color: $base-text-color !default; } diff --git a/packages/devextreme-scss/scss/widgets/generic/gantt/_index.scss b/packages/devextreme-scss/scss/widgets/generic/gantt/_index.scss index aab84e869c3f..5ee45c681393 100644 --- a/packages/devextreme-scss/scss/widgets/generic/gantt/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/gantt/_index.scss @@ -8,7 +8,7 @@ @use "../toolbar/sizes" as *; @use "../splitterBar"; -$gantt-successor-background-color: white; +$gantt-successor-background-color: white !default; .dx-gantt { border: 1px solid; diff --git a/packages/devextreme-scss/scss/widgets/generic/gridBase/_index.scss b/packages/devextreme-scss/scss/widgets/generic/gridBase/_index.scss index ea37ef5d4fc7..3a3ba9008d53 100644 --- a/packages/devextreme-scss/scss/widgets/generic/gridBase/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/gridBase/_index.scss @@ -26,15 +26,15 @@ @use "../overlay"; @use "../pagination"; -$generic-grid-base-border-hidden: 1px solid transparent; -$generic-grid-base-row-border: 1px solid transparent; -$generic-grid-base-drag-header-border: 1px solid; -$generic-grid-base-drag-header-first-shadow: 0 0 1px $datagrid-drag-header-shadow-color; -$generic-grid-base-drag-header-second-shadow: 0 1px 3px $datagrid-drag-header-second-shadow-color; -$generic-grid-base-checkbox-icon-size: 16px; -$generic-grid-base-checkbox-indeterminate-icon-size: 6px; -$generic-grid-base-group-panel-message-line-height: $generic-button-text-line-height; -$generic-grid-base-cell-input-height: math.round($generic-base-line-height * $generic-base-font-size) + 2 * $generic-grid-base-cell-padding; +$generic-grid-base-border-hidden: 1px solid transparent !default; +$generic-grid-base-row-border: 1px solid transparent !default; +$generic-grid-base-drag-header-border: 1px solid !default; +$generic-grid-base-drag-header-first-shadow: 0 0 1px $datagrid-drag-header-shadow-color !default; +$generic-grid-base-drag-header-second-shadow: 0 1px 3px $datagrid-drag-header-second-shadow-color !default; +$generic-grid-base-checkbox-icon-size: 16px !default; +$generic-grid-base-checkbox-indeterminate-icon-size: 6px !default; +$generic-grid-base-group-panel-message-line-height: $generic-button-text-line-height !default; +$generic-grid-base-cell-input-height: math.round($generic-base-line-height * $generic-base-font-size) + 2 * $generic-grid-base-cell-padding !default; @mixin dx-set-checkbox-border-color-as-background() { @if $generic-color-postfix == ".contrast" { diff --git a/packages/devextreme-scss/scss/widgets/generic/gridBase/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/gridBase/_sizes.scss index 3d9e0643fe47..9ab006263cd5 100644 --- a/packages/devextreme-scss/scss/widgets/generic/gridBase/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/generic/gridBase/_sizes.scss @@ -16,8 +16,8 @@ $generic-grid-base-checkbox-fontsize: null !default; $generic-grid-base-adaptive-text-padding: null !default; $generic-grid-base-treeview-search-margin: null !default; $generic-command-edit-icon-size: null !default; -$generic-command-edit-icon-margin: null !default; -$generic-command-edit-text-margin: null !default; +$generic-command-edit-icon-margin: 5px !default; +$generic-command-edit-text-margin: 2px !default; $generic-grid-base-nodata-font-size: null !default; $generic-grid-base-checkbox-padding-bottom: null !default; $generic-grid-base-treeview-select-all-item-offset: null !default; @@ -26,8 +26,8 @@ $generic-grid-base-command-ai-text-content-padding: null !default; $generic-grid-base-command-ai-header-button-padding: null !default; $generic-grid-base-command-ai-header-button-size: null !default; -$datagrid-text-link-disabled-opacity: $base-disabled-opacity; -$datagrid-icon-link-disabled-opacity: 0.6; +$datagrid-text-link-disabled-opacity: $base-disabled-opacity !default; +$datagrid-icon-link-disabled-opacity: 0.6 !default; $generic-grid-base-ai-prompt-editor-progressbar-height: 4px !default; @if $size == "default" { @@ -46,11 +46,9 @@ $generic-grid-base-ai-prompt-editor-progressbar-height: 4px !default; $generic-grid-base-adaptive-text-padding: 8px !default; $generic-grid-base-treeview-search-margin: 10px !default; $generic-command-edit-icon-size: 18px !default; - $generic-command-edit-icon-margin: 5px !default; - $generic-command-edit-text-margin: 2px !default; $generic-grid-base-nodata-font-size: 17px !default; - $generic-grid-base-checkbox-padding-bottom: 2px; - $generic-grid-base-treeview-select-all-item-offset: 34px; + $generic-grid-base-checkbox-padding-bottom: 2px !default; + $generic-grid-base-treeview-select-all-item-offset: 34px !default; $generic-grid-base-command-ai-chatsparkleoutline-icon-size: 18px !default; $generic-grid-base-command-ai-text-content-padding: $generic-grid-base-command-ai-chatsparkleoutline-icon-size + 8px !default; $generic-grid-base-command-ai-header-button-padding: 6px !default; @@ -73,11 +71,9 @@ $generic-grid-base-ai-prompt-editor-progressbar-height: 4px !default; $generic-grid-base-adaptive-text-padding: 6px !default; $generic-grid-base-treeview-search-margin: 5px !default; $generic-command-edit-icon-size: 14px !default; - $generic-command-edit-icon-margin: 5px !default; - $generic-command-edit-text-margin: 2px !default; $generic-grid-base-nodata-font-size: 14px !default; - $generic-grid-base-checkbox-padding-bottom: 0; - $generic-grid-base-treeview-select-all-item-offset: 28px; + $generic-grid-base-checkbox-padding-bottom: 0 !default; + $generic-grid-base-treeview-select-all-item-offset: 28px !default; $generic-grid-base-command-ai-chatsparkleoutline-icon-size: 14px !default; $generic-grid-base-command-ai-text-content-padding: $generic-grid-base-command-ai-chatsparkleoutline-icon-size + 6px !default; $generic-grid-base-command-ai-header-button-padding: 5px !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/gridBase/_variables.scss b/packages/devextreme-scss/scss/widgets/generic/gridBase/_variables.scss index 4d9254181e0a..58d431d63c19 100644 --- a/packages/devextreme-scss/scss/widgets/generic/gridBase/_variables.scss +++ b/packages/devextreme-scss/scss/widgets/generic/gridBase/_variables.scss @@ -1 +1 @@ -$grid-masterdetail-padding: 20px; +$grid-masterdetail-padding: 20px !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/gridBase/layout/aiChat/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/gridBase/layout/aiChat/_sizes.scss index fa9ad84197d8..0b43b77902d3 100644 --- a/packages/devextreme-scss/scss/widgets/generic/gridBase/layout/aiChat/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/generic/gridBase/layout/aiChat/_sizes.scss @@ -1,3 +1,3 @@ @use "../../../sizes" as *; -$ai-chat-message-icon-size: $generic-base-icon-size; +$ai-chat-message-icon-size: $generic-base-icon-size !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/htmlEditor/_colors.scss b/packages/devextreme-scss/scss/widgets/generic/htmlEditor/_colors.scss index 797aaf643d03..90e0b2e4232c 100644 --- a/packages/devextreme-scss/scss/widgets/generic/htmlEditor/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/generic/htmlEditor/_colors.scss @@ -9,10 +9,6 @@ $htmleditor-filled-border-color: transparent !default; $htmleditor-invalid-border-color: $base-invalid-color !default; $htmleditor-invalid-faded-border-color: $base-invalid-faded-border-color !default; $htmleditor-toolbar-border-color: $base-border-color !default; -$htmleditor-normal-format-active-bg: null !default; -$htmleditor-default-format-active-bg: null !default; -$htmleditor-danger-format-active-bg: null !default; -$htmleditor-success-format-active-bg: null !default; $htmleditor-variable-bg: color.change($base-accent, $alpha: 0.15) !default; $htmleditor-resize-frame-border-color: $base-accent !default; $htmleditor-resize-handle-bg: $base-accent !default; @@ -22,73 +18,41 @@ $htmleditor-table-border-color: $base-border-color !default; $htmleditor-cells-separator-bg: color.change($base-accent, $alpha: 0.5) !default; @if $color == "carmine" { - $htmleditor-normal-format-active-bg: color.adjust($base-element-bg, $lightness: -10%, $space: hsl) !default; - $htmleditor-default-format-active-bg: color.adjust($base-accent, $lightness: -10%, $space: hsl) !default; - $htmleditor-danger-format-active-bg: color.adjust($base-danger, $lightness: -10%, $space: hsl) !default; - $htmleditor-success-format-active-bg: color.adjust($base-success, $lightness: -10%, $space: hsl) !default; $htmleditor-mention-bg: color.adjust($base-bg, $lightness: -9%, $space: hsl) !default; $htmleditor-mention-color: $base-text-color !default; } @if $color == "contrast" { - $htmleditor-normal-format-active-bg: $base-bg !default; - $htmleditor-default-format-active-bg: $base-default !default; - $htmleditor-danger-format-active-bg: $base-danger !default; - $htmleditor-success-format-active-bg: $base-success !default; $htmleditor-mention-bg: extcolor.difference(#fff, $base-bg) !default; $htmleditor-mention-color: $base-inverted-text-color !default; } @if $color == "dark" { - $htmleditor-normal-format-active-bg: color.adjust($base-element-bg, $lightness: 10%, $space: hsl) !default; - $htmleditor-default-format-active-bg: color.adjust($base-accent, $lightness: -10%, $space: hsl) !default; - $htmleditor-danger-format-active-bg: color.adjust($base-danger, $lightness: -10%, $space: hsl) !default; - $htmleditor-success-format-active-bg: color.adjust($base-success, $lightness: -10%, $space: hsl) !default; $htmleditor-mention-bg: color.adjust($base-bg, $lightness: 13.5%, $space: hsl) !default; $htmleditor-mention-color: $base-text-color !default; } @if $color == "darkmoon" { - $htmleditor-normal-format-active-bg: color.adjust($base-element-bg, $lightness: 10%, $space: hsl) !default; - $htmleditor-default-format-active-bg: color.adjust($base-accent, $lightness: 10%, $space: hsl) !default; - $htmleditor-danger-format-active-bg: color.adjust($base-danger, $lightness: 10%, $space: hsl) !default; - $htmleditor-success-format-active-bg: color.adjust($base-success, $lightness: 10%, $space: hsl) !default; $htmleditor-mention-bg: color.adjust($base-bg, $lightness: 13.5%, $space: hsl) !default; $htmleditor-mention-color: $base-text-color !default; } @if $color == "darkviolet" { - $htmleditor-normal-format-active-bg: color.adjust($base-element-bg, $lightness: 10%, $space: hsl) !default; - $htmleditor-default-format-active-bg: color.adjust($base-accent, $lightness: 10%, $space: hsl) !default; - $htmleditor-danger-format-active-bg: color.adjust($base-danger, $lightness: 10%, $space: hsl) !default; - $htmleditor-success-format-active-bg: color.adjust($base-success, $lightness: 10%, $space: hsl) !default; $htmleditor-mention-bg: $base-select-bg !default; $htmleditor-mention-color: $base-text-color !default; } @if $color == "greenmist" { - $htmleditor-normal-format-active-bg: color.adjust($base-element-bg, $lightness: 10%, $space: hsl) !default; - $htmleditor-default-format-active-bg: color.adjust($base-accent, $lightness: 10%, $space: hsl) !default; - $htmleditor-danger-format-active-bg: color.adjust($base-danger, $lightness: 10%, $space: hsl) !default; - $htmleditor-success-format-active-bg: color.adjust($base-success, $lightness: 10%, $space: hsl) !default; $htmleditor-mention-bg: $base-accent !default; $htmleditor-mention-color: color.adjust($base-text-color, $lightness: 100%, $space: hsl) !default; } @if $color == "light" { - $htmleditor-normal-format-active-bg: color.adjust($base-element-bg, $lightness: -10%, $space: hsl) !default; - $htmleditor-default-format-active-bg: color.adjust($base-accent, $lightness: -10%, $space: hsl) !default; - $htmleditor-danger-format-active-bg: color.adjust($base-danger, $lightness: -10%, $space: hsl) !default; - $htmleditor-success-format-active-bg: color.adjust($base-success, $lightness: -10%, $space: hsl) !default; $htmleditor-mention-bg: color.adjust($base-bg, $lightness: -13.5%, $space: hsl) !default; $htmleditor-mention-color: $base-text-color !default; } @if $color == "softblue" { - $htmleditor-normal-format-active-bg: color.adjust($base-element-bg, $lightness: -10%, $space: hsl) !default; - $htmleditor-default-format-active-bg: color.adjust($base-accent, $lightness: -10%, $space: hsl) !default; - $htmleditor-danger-format-active-bg: color.adjust($base-danger, $lightness: -10%, $space: hsl) !default; - $htmleditor-success-format-active-bg: color.adjust($base-success, $lightness: -10%, $space: hsl) !default; $htmleditor-mention-bg: color.adjust($base-bg, $lightness: -13.5%, $space: hsl) !default; $htmleditor-mention-color: $base-text-color !default; } diff --git a/packages/devextreme-scss/scss/widgets/generic/htmlEditor/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/htmlEditor/_sizes.scss index b19b96465f4e..85381b1e0bfc 100644 --- a/packages/devextreme-scss/scss/widgets/generic/htmlEditor/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/generic/htmlEditor/_sizes.scss @@ -10,15 +10,15 @@ $generic-aidialog-content-padding: null !default; @if $size == "default" { $generic-toolbar-size-editor-width: 105px !default; $generic-html-editor-add-image-dialog-fileuploader-padding: 60px 20px !default; - $generic-aidialog-selects-gap: 10px; - $generic-aidialog-content-gap: 20px; - $generic-aidialog-content-padding: 20px; + $generic-aidialog-selects-gap: 10px !default; + $generic-aidialog-content-gap: 20px !default; + $generic-aidialog-content-padding: 20px !default; } @else if $size == "compact" { $generic-toolbar-size-editor-width: 80px !default; $generic-html-editor-add-image-dialog-fileuploader-padding: 40px 20px !default; - $generic-aidialog-selects-gap: 5px; - $generic-aidialog-content-gap: 10px; - $generic-aidialog-content-padding: 10px; + $generic-aidialog-selects-gap: 5px !default; + $generic-aidialog-content-gap: 10px !default; + $generic-aidialog-content-padding: 10px !default; } diff --git a/packages/devextreme-scss/scss/widgets/generic/icons/_index.scss b/packages/devextreme-scss/scss/widgets/generic/icons/_index.scss index cce54f733c88..5d955957b919 100644 --- a/packages/devextreme-scss/scss/widgets/generic/icons/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/icons/_index.scss @@ -12,12 +12,6 @@ } } -.dx-scrollview-pulldown { - background-image: $scrollview-pulldown-path; - background-position: 0 0; - background-repeat: no-repeat; -} - .dx-loadindicator-image { background-position: center center; background-repeat: no-repeat; diff --git a/packages/devextreme-scss/scss/widgets/generic/informer/_colors.scss b/packages/devextreme-scss/scss/widgets/generic/informer/_colors.scss index 573e09684108..20a972f1246b 100644 --- a/packages/devextreme-scss/scss/widgets/generic/informer/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/generic/informer/_colors.scss @@ -8,57 +8,57 @@ $informer-info-color: null !default; $informer-info-background-color: null !default; @if $color == "carmine" { - $informer-error-color: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: -351deg, $saturation: -26.91%, $lightness: -5.49%); - $informer-error-background-color: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: -347deg, $saturation: -26.69%, $lightness: 33.14%); - $informer-info-color: $base-text-color; - $informer-info-background-color: color.adjust($base-text-color, $lightness: 72.16%, $space: hsl); + $informer-error-color: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: -351deg, $saturation: -26.91%, $lightness: -5.49%) !default; + $informer-error-background-color: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: -347deg, $saturation: -26.69%, $lightness: 33.14%) !default; + $informer-info-color: $base-text-color !default; + $informer-info-background-color: color.adjust($base-text-color, $lightness: 72.16%, $space: hsl) !default; } @if $color == "contrast" { - $informer-error-color: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: 2deg, $saturation: -16.93%, $lightness: -6.08%); - $informer-error-background-color: rgba(212, 63, 58, 0.1); - $informer-info-color: color.adjust($base-text-color, $lightness: -12.94%, $space: hsl); - $informer-info-background-color: color.adjust($base-text-color, $lightness: -68.63%, $space: hsl); + $informer-error-color: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: 2deg, $saturation: -16.93%, $lightness: -6.08%) !default; + $informer-error-background-color: rgba(212, 63, 58, 0.1) !default; + $informer-info-color: color.adjust($base-text-color, $lightness: -12.94%, $space: hsl) !default; + $informer-info-background-color: color.adjust($base-text-color, $lightness: -68.63%, $space: hsl) !default; } @if $color == "dark" { - $informer-error-color: $base-danger; - $informer-error-background-color: rgba(212, 63, 58, 0.1); - $informer-info-color: $base-text-color; - $informer-info-background-color: color.adjust($base-text-color, $lightness: -55.69%, $space: hsl); + $informer-error-color: $base-danger !default; + $informer-error-background-color: rgba(212, 63, 58, 0.1) !default; + $informer-info-color: $base-text-color !default; + $informer-info-background-color: color.adjust($base-text-color, $lightness: -55.69%, $space: hsl) !default; } @if $color == "darkmoon" { - $informer-error-color: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: -342deg, $saturation: -28.85%, $lightness: -6.67%); - $informer-error-background-color: rgba(212, 63, 58, 0.1); - $informer-info-color: color.adjust($base-text-color, $lightness: -12.94%, $space: hsl); - $informer-info-background-color: color.adjust($base-text-color, $lightness: -68.63%, $space: hsl); + $informer-error-color: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: -342deg, $saturation: -28.85%, $lightness: -6.67%) !default; + $informer-error-background-color: rgba(212, 63, 58, 0.1) !default; + $informer-info-color: color.adjust($base-text-color, $lightness: -12.94%, $space: hsl) !default; + $informer-info-background-color: color.adjust($base-text-color, $lightness: -68.63%, $space: hsl) !default; } @if $color == "darkviolet" { - $informer-error-color: $base-danger; - $informer-error-background-color: rgba(212, 63, 58, 0.1); - $informer-info-color: extcolor.adjust-if-not-achromatic($color: $base-text-color, $hue: -210deg, $saturation: -11.11%, $lightness: -9.41%); - $informer-info-background-color: extcolor.adjust-if-not-achromatic($color: $base-text-color, $hue: -210deg, $saturation: -11.11%, $lightness: -65.10%); + $informer-error-color: $base-danger !default; + $informer-error-background-color: rgba(212, 63, 58, 0.1) !default; + $informer-info-color: extcolor.adjust-if-not-achromatic($color: $base-text-color, $hue: -210deg, $saturation: -11.11%, $lightness: -9.41%) !default; + $informer-info-background-color: extcolor.adjust-if-not-achromatic($color: $base-text-color, $hue: -210deg, $saturation: -11.11%, $lightness: -65.10%) !default; } @if $color == "greenmist" { - $informer-error-color: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: -351deg, $saturation: -27.05%, $lightness: -4.90%); - $informer-error-background-color: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: -348deg, $saturation: -26.83%, $lightness: 33.73%); - $informer-info-color: extcolor.adjust-if-not-achromatic($color: $base-text-color, $hue: -191deg, $saturation: -32.77%, $lightness: -3.33%); - $informer-info-background-color: extcolor.adjust-if-not-achromatic($color: $base-text-color, $hue: -191deg, $saturation: -32.77%, $lightness: 68.82%); + $informer-error-color: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: -351deg, $saturation: -27.05%, $lightness: -4.90%) !default; + $informer-error-background-color: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: -348deg, $saturation: -26.83%, $lightness: 33.73%) !default; + $informer-info-color: extcolor.adjust-if-not-achromatic($color: $base-text-color, $hue: -191deg, $saturation: -32.77%, $lightness: -3.33%) !default; + $informer-info-background-color: extcolor.adjust-if-not-achromatic($color: $base-text-color, $hue: -191deg, $saturation: -32.77%, $lightness: 68.82%) !default; } @if $color == "light" { - $informer-error-color: $base-danger; - $informer-error-background-color: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: 4deg, $saturation: 0.22%, $lightness: 38.63%); - $informer-info-color: $base-text-color; - $informer-info-background-color: color.adjust($base-text-color, $lightness: 72.16%, $space: hsl); + $informer-error-color: $base-danger !default; + $informer-error-background-color: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: 4deg, $saturation: 0.22%, $lightness: 38.63%) !default; + $informer-info-color: $base-text-color !default; + $informer-info-background-color: color.adjust($base-text-color, $lightness: 72.16%, $space: hsl) !default; } @if $color == "softblue" { - $informer-error-color: $base-danger; - $informer-error-background-color: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: 4deg, $saturation: 0.22%, $lightness: 38.63%); - $informer-info-color: $base-text-color; - $informer-info-background-color: color.adjust($base-text-color, $lightness: 72.16%, $space: hsl); + $informer-error-color: $base-danger !default; + $informer-error-background-color: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: 4deg, $saturation: 0.22%, $lightness: 38.63%) !default; + $informer-info-color: $base-text-color !default; + $informer-info-background-color: color.adjust($base-text-color, $lightness: 72.16%, $space: hsl) !default; } diff --git a/packages/devextreme-scss/scss/widgets/generic/list/_index.scss b/packages/devextreme-scss/scss/widgets/generic/list/_index.scss index c1dcca0e4a38..ed6b8aed869c 100644 --- a/packages/devextreme-scss/scss/widgets/generic/list/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/list/_index.scss @@ -13,9 +13,9 @@ @use "../../base/list/mixins" as *; @use "../badge"; -$generic-list-border: $generic-list-border-width solid; -$generic-list-item-border: $generic-list-item-border-width solid; -$generic-list-search-editor-height: math.round($generic-base-line-height * $generic-base-font-size) + $generic-base-inline-borderedwidget-top-padding + $generic-base-inline-borderedwidget-bottom-padding + 2 * $generic-base-border-width; +$generic-list-border: $generic-list-border-width solid !default; +$generic-list-item-border: $generic-list-item-border-width solid !default; +$generic-list-search-editor-height: math.round($generic-base-line-height * $generic-base-font-size) + $generic-base-inline-borderedwidget-top-padding + $generic-base-inline-borderedwidget-bottom-padding + 2 * $generic-base-border-width !default; .dx-list-item-chevron { @include dx-icon(chevronnext); diff --git a/packages/devextreme-scss/scss/widgets/generic/loadIndicator/_colors.scss b/packages/devextreme-scss/scss/widgets/generic/loadIndicator/_colors.scss index a5829b425501..21159495c936 100644 --- a/packages/devextreme-scss/scss/widgets/generic/loadIndicator/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/generic/loadIndicator/_colors.scss @@ -8,14 +8,13 @@ */ $loadindicator-bg: null !default; $loadindicator-segment-shadow-color: null !default; -$load-indicator-animation-sparkle-bg-color1: null !default; +$load-indicator-animation-sparkle-bg-color1: $base-accent !default; $load-indicator-animation-sparkle-bg-color2: null !default; $load-indicator-animation-sparkle-bg-color3: null !default; @if $color == "carmine" { $loadindicator-bg: $base-accent !default; $loadindicator-segment-shadow-color: color.change($base-shadow-color, $alpha: 0.2) !default; - $load-indicator-animation-sparkle-bg-color1: $base-accent !default; $load-indicator-animation-sparkle-bg-color2: color.adjust($base-accent, $saturation: -8.58%, $space: hsl, $lightness: 27.06%) !default; $load-indicator-animation-sparkle-bg-color3: extcolor.adjust-if-not-achromatic($color: $base-accent, $hue: 2deg, $saturation: -6.88%, $lightness: 33.14%) !default; } @@ -23,7 +22,6 @@ $load-indicator-animation-sparkle-bg-color3: null !default; @if $color == "contrast" { $loadindicator-bg: $base-inverted-bg !default; $loadindicator-segment-shadow-color: $base-shadow-color !default; - $load-indicator-animation-sparkle-bg-color1: $base-accent !default; $load-indicator-animation-sparkle-bg-color2: color.adjust($base-accent, $saturation: -25.83%, $space: hsl, $lightness: -19.61%) !default; $load-indicator-animation-sparkle-bg-color3: extcolor.adjust-if-not-achromatic($color: $base-accent, $hue: 1deg, $saturation: -44.18%, $lightness: -11.76%) !default; } @@ -31,7 +29,6 @@ $load-indicator-animation-sparkle-bg-color3: null !default; @if $color == "dark" { $loadindicator-bg: color.adjust($base-text-color, $lightness: -17.5%, $space: hsl) !default; $loadindicator-segment-shadow-color: color.change($base-shadow-color, $alpha: 0.2) !default; - $load-indicator-animation-sparkle-bg-color1: $base-accent !default; $load-indicator-animation-sparkle-bg-color2: color.adjust($base-accent, $saturation: -25.83%, $space: hsl, $lightness: -19.61%) !default; $load-indicator-animation-sparkle-bg-color3: extcolor.adjust-if-not-achromatic($color: $base-accent, $hue: 1deg, $saturation: -44.18%, $lightness: -11.76%) !default; } @@ -39,7 +36,6 @@ $load-indicator-animation-sparkle-bg-color3: null !default; @if $color == "darkmoon" { $loadindicator-bg: $base-accent !default; $loadindicator-segment-shadow-color: color.change($base-shadow-color, $alpha: 0.2) !default; - $load-indicator-animation-sparkle-bg-color1: $base-accent !default; $load-indicator-animation-sparkle-bg-color2: color.adjust($base-accent, $saturation: -25.83%, $space: hsl, $lightness: -19.61%) !default; $load-indicator-animation-sparkle-bg-color3: extcolor.adjust-if-not-achromatic($color: $base-accent, $hue: 1deg, $saturation: -44.18%, $lightness: -11.76%) !default; } @@ -47,7 +43,6 @@ $load-indicator-animation-sparkle-bg-color3: null !default; @if $color == "darkviolet" { $loadindicator-bg: $base-accent-highlight-color !default; $loadindicator-segment-shadow-color: color.change($base-shadow-color, $alpha: 0.2) !default; - $load-indicator-animation-sparkle-bg-color1: $base-accent !default; $load-indicator-animation-sparkle-bg-color2: color.adjust($base-accent, $saturation: -25.83%, $space: hsl, $lightness: -19.61%) !default; $load-indicator-animation-sparkle-bg-color3: extcolor.adjust-if-not-achromatic($color: $base-accent, $hue: 1deg, $saturation: -44.18%, $lightness: -11.76%) !default; } @@ -55,7 +50,6 @@ $load-indicator-animation-sparkle-bg-color3: null !default; @if $color == "greenmist" { $loadindicator-bg: color.adjust($base-text-color, $lightness: 17.5%, $space: hsl) !default; $loadindicator-segment-shadow-color: color.change($base-shadow-color, $alpha: 0.2) !default; - $load-indicator-animation-sparkle-bg-color1: $base-accent !default; $load-indicator-animation-sparkle-bg-color2: color.adjust($base-accent, $saturation: -8.58%, $space: hsl, $lightness: 27.06%) !default; $load-indicator-animation-sparkle-bg-color3: extcolor.adjust-if-not-achromatic($color: $base-accent, $hue: 2deg, $saturation: -6.88%, $lightness: 33.14%) !default; } @@ -63,7 +57,6 @@ $load-indicator-animation-sparkle-bg-color3: null !default; @if $color == "light" { $loadindicator-bg: color.adjust($base-text-color, $lightness: 17.5%, $space: hsl) !default; $loadindicator-segment-shadow-color: color.change($base-shadow-color, $alpha: 0.2) !default; - $load-indicator-animation-sparkle-bg-color1: $base-accent !default; $load-indicator-animation-sparkle-bg-color2: color.adjust($base-accent, $saturation: -8.58%, $space: hsl, $lightness: 27.06%) !default; $load-indicator-animation-sparkle-bg-color3: extcolor.adjust-if-not-achromatic($color: $base-accent, $hue: 2deg, $saturation: -6.88%, $lightness: 33.14%) !default; } @@ -71,7 +64,6 @@ $load-indicator-animation-sparkle-bg-color3: null !default; @if $color == "softblue" { $loadindicator-bg: $base-accent !default; $loadindicator-segment-shadow-color: color.change($base-shadow-color, $alpha: 0.2) !default; - $load-indicator-animation-sparkle-bg-color1: $base-accent !default; $load-indicator-animation-sparkle-bg-color2: color.adjust($base-accent, $saturation: -8.58%, $space: hsl, $lightness: 27.06%) !default; $load-indicator-animation-sparkle-bg-color3: extcolor.adjust-if-not-achromatic($color: $base-accent, $hue: 2deg, $saturation: -6.88%, $lightness: 33.14%) !default; } diff --git a/packages/devextreme-scss/scss/widgets/generic/menuBase/_index.scss b/packages/devextreme-scss/scss/widgets/generic/menuBase/_index.scss index c000cb70ef14..fbfba161c754 100644 --- a/packages/devextreme-scss/scss/widgets/generic/menuBase/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/menuBase/_index.scss @@ -8,11 +8,11 @@ @use "../../base/mixins" as *; @use "../../base/menuBase"; -$generic-menu-item-padding-increased: $generic-menu-item-padding-base + 2px; -$generic-menu-item-padding-reduced: $generic-menu-item-padding-base - 2px; -$generic-menu-arrow-image-size: 7px; +$generic-menu-item-padding-increased: $generic-menu-item-padding-base + 2px !default; +$generic-menu-item-padding-reduced: $generic-menu-item-padding-base - 2px !default; +$generic-menu-arrow-image-size: 7px !default; -$generic-menu-popup-border: 1px solid; +$generic-menu-popup-border: 1px solid !default; .dx-menu-item { color: $menu-color; diff --git a/packages/devextreme-scss/scss/widgets/generic/pivotGrid/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/pivotGrid/_sizes.scss index a6c667cfc55b..f1c91857cdf8 100644 --- a/packages/devextreme-scss/scss/widgets/generic/pivotGrid/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/generic/pivotGrid/_sizes.scss @@ -16,7 +16,7 @@ $pivotgrid-empty-area-text-padding: null !default; $pivotgrid-common-top-bottom-padding: 7px !default; $pivotgrid-field-indicators-offset: 6px !default; $pivotgrid-indicators-line-height: 19px !default; - $pivotgrid-button-top-padding: 12px; + $pivotgrid-button-top-padding: 12px !default; $pivotgrid-empty-area-text-padding: 7px !default; } @@ -27,6 +27,6 @@ $pivotgrid-empty-area-text-padding: null !default; $pivotgrid-common-top-bottom-padding: 5px !default; $pivotgrid-field-indicators-offset: 4px !default; $pivotgrid-indicators-line-height: 16px !default; - $pivotgrid-button-top-padding: 8px; + $pivotgrid-button-top-padding: 8px !default; $pivotgrid-empty-area-text-padding: 5px !default; } diff --git a/packages/devextreme-scss/scss/widgets/generic/popup/_index.scss b/packages/devextreme-scss/scss/widgets/generic/popup/_index.scss index 584ad3d8053d..f7cfb912957a 100644 --- a/packages/devextreme-scss/scss/widgets/generic/popup/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/popup/_index.scss @@ -11,8 +11,8 @@ @use "../../base/popup"; @use "../overlay"; -$generic-popup-toolbar-height: $generic-base-inline-widget-height; -$generic-popup-toolbar-label-font-size: $generic-m-font-size; +$generic-popup-toolbar-height: $generic-base-inline-widget-height !default; +$generic-popup-toolbar-label-font-size: $generic-m-font-size !default; .dx-popup-wrapper { & > .dx-overlay-content { diff --git a/packages/devextreme-scss/scss/widgets/generic/progressBar/_index.scss b/packages/devextreme-scss/scss/widgets/generic/progressBar/_index.scss index ed0a16ca0296..0d42ade93bae 100644 --- a/packages/devextreme-scss/scss/widgets/generic/progressBar/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/progressBar/_index.scss @@ -4,11 +4,11 @@ @use "../../base/mixins" as *; @use "../../base/progressBar"; -$generic-bar-height: 6px; -$generic-background-width: 90%; -$generic-progressbar-border-radius: 2px; -$generic-progressbar-interdeterminate-state-gradient: -90deg, transparent 5%, $progressbar-range-indeterminate-color, transparent 95%; -$generic-progress-bar-border: 1px solid; +$generic-bar-height: 6px !default; +$generic-background-width: 90% !default; +$generic-progressbar-border-radius: 2px !default; +$generic-progressbar-interdeterminate-state-gradient: -90deg, transparent 5%, $progressbar-range-indeterminate-color, transparent 95% !default; +$generic-progress-bar-border: 1px solid !default; .dx-progressbar-container { height: $generic-bar-height; diff --git a/packages/devextreme-scss/scss/widgets/generic/rangeSlider/_index.scss b/packages/devextreme-scss/scss/widgets/generic/rangeSlider/_index.scss index d437e5903f72..5e412c3672ef 100644 --- a/packages/devextreme-scss/scss/widgets/generic/rangeSlider/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/rangeSlider/_index.scss @@ -2,7 +2,7 @@ @use "sizes" as *; @use "../../base/rangeSlider"; -$generic-slider-handle-margin: -7px; +$generic-slider-handle-margin: -7px !default; .dx-rangeslider-start-handle { margin-inline-start: $generic-slider-handle-margin; diff --git a/packages/devextreme-scss/scss/widgets/generic/recurrenceEditor/_index.scss b/packages/devextreme-scss/scss/widgets/generic/recurrenceEditor/_index.scss index 8ae754ace0b8..58c2cc65dbce 100644 --- a/packages/devextreme-scss/scss/widgets/generic/recurrenceEditor/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/recurrenceEditor/_index.scss @@ -3,7 +3,7 @@ @use "../scheduler/sizes" as *; @use "../../base/recurrenceEditor" as *; -$generic-recurrence-number-box-margin-right: 11px; +$generic-recurrence-number-box-margin-right: 11px !default; .dx-recurrence-editor-container { .dx-recurrence-numberbox-interval { diff --git a/packages/devextreme-scss/scss/widgets/generic/scheduler/_colors.scss b/packages/devextreme-scss/scss/widgets/generic/scheduler/_colors.scss index 998c9d6176d5..71c6890c436a 100644 --- a/packages/devextreme-scss/scss/widgets/generic/scheduler/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/generic/scheduler/_colors.scss @@ -67,32 +67,27 @@ $scheduler-dd-appointment-hover-text-color: null !default; $scheduler-form-icon-color: $base-icon-color !default; $scheduler-workspace-month-text-color: null !default; $scheduler-dropdown-appointment-date-color: null !default; -$scheduler-workspace-active-cell-color: null !default; $scheduler-workspace-hovered-cell-color: null !default; $scheduler-group-header-color: null !default; $scheduler-header-bg: null !default; $scheduler-alldaypanel-bg: null !default; -$scheduler-tab-selected-bg: null !default; -$scheduler-tab-selected-before-bg: null !default; -$scheduler-tab-focused-border-color: null !default; $scheduler-other-month-cell-opacity: null !default; $scheduler-fill-focused-appointment: null !default; $scheduler-is-shadow-color-for-focused-state: true !default; $scheduler-time-indicator-background-color: null !default; $scheduler-first-month-cell-background-color: null !default; -$scheduler-time-panel-background-color: null !default; $scheduler-tooltip-appointment-text-color: null !default; $scheduler-appointment-overlay-bg: null !default; -$month-text-color-light: #757575; -$month-text-color-dark: #9c9c9c; +$month-text-color-light: #757575 !default; +$month-text-color-dark: #9c9c9c !default; // Agenda $agenda-appointment-recurrence-icon-color: null !default; $agenda-appointment-hover-bg: $base-hover-bg !default; $agenda-appointment-active-bg: null !default; -$agenda-appointment-text-color: null !default; +$agenda-appointment-text-color: $base-text-color !default; @if $color == "carmine" { $scheduler-base-border-color: color.change($base-border-color, $alpha: 0.6) !default; @@ -112,14 +107,10 @@ $agenda-appointment-text-color: null !default; $scheduler-dd-appointment-hover-text-color: $base-inverted-text-color !default; $scheduler-workspace-month-text-color: $month-text-color-light !default; $scheduler-dropdown-appointment-date-color: color.adjust($base-text-color, $lightness: 38.5%, $space: hsl) !default; - $scheduler-workspace-active-cell-color: $scheduler-accent-border-color !default; $scheduler-workspace-hovered-cell-color: $scheduler-workspace-month-text-color !default; $scheduler-group-header-color: $scheduler-panel-text-color !default; $scheduler-header-bg: color.adjust($base-bg, $lightness: -4%, $space: hsl) !default; $scheduler-alldaypanel-bg: $base-element-bg !default; - $scheduler-tab-selected-bg: $base-bg !default; - $scheduler-tab-selected-before-bg: $scheduler-tab-selected-bg !default; - $scheduler-tab-focused-border-color: $base-accent !default; $scheduler-other-month-cell-opacity: 0.5 !default; $scheduler-fill-focused-appointment: false !default; $scheduler-time-indicator-background-color: rgba(51, 51, 51, 0.04) !default; @@ -129,7 +120,6 @@ $agenda-appointment-text-color: null !default; $scheduler-appointment-overlay-bg: $base-bg !default; $agenda-appointment-recurrence-icon-color: $base-inverted-text-color !default; - $agenda-appointment-text-color: $base-text-color !default; $agenda-appointment-active-bg: color.change($base-accent, $alpha: 0.15) !default; } @@ -149,14 +139,10 @@ $agenda-appointment-text-color: null !default; $scheduler-workspace-focused-cell-color: color.adjust($base-accent, $lightness: 5%, $space: hsl) !default; $scheduler-panel-text-color: $base-text-color !default; $scheduler-dropdown-appointment-date-color: $base-text-color !default; - $scheduler-workspace-active-cell-color: $scheduler-accent-border-color !default; $scheduler-workspace-hovered-cell-color: extcolor.difference(#fff, $base-text-color) !default; $scheduler-group-header-color: $base-text-color !default; $scheduler-header-bg: $base-bg !default; $scheduler-alldaypanel-bg: $base-bg !default; - $scheduler-tab-selected-bg: $base-inverted-bg !default; - $scheduler-tab-selected-before-bg: transparent !default; - $scheduler-tab-focused-border-color: $base-border-color !default; $scheduler-other-month-cell-opacity: 1 !default; $scheduler-fill-focused-appointment: true !default; $scheduler-time-indicator-background-color: color.change(#3806ff, $alpha: 0.6) !default; @@ -167,7 +153,6 @@ $agenda-appointment-text-color: null !default; $scheduler-appointment-overlay-bg: $base-inverted-bg !default; $agenda-appointment-recurrence-icon-color: $base-text-color !default; - $agenda-appointment-text-color: $base-text-color !default; $agenda-appointment-active-bg: $base-accent !default; } @@ -189,14 +174,10 @@ $agenda-appointment-text-color: null !default; $scheduler-dd-appointment-hover-text-color: $base-inverted-text-color !default; $scheduler-workspace-month-text-color: $month-text-color-dark !default; $scheduler-dropdown-appointment-date-color: color.adjust($base-text-color, $lightness: 38.5%, $space: hsl) !default; - $scheduler-workspace-active-cell-color: $scheduler-accent-border-color !default; $scheduler-workspace-hovered-cell-color: $scheduler-workspace-month-text-color !default; $scheduler-group-header-color: $scheduler-panel-text-color !default; $scheduler-header-bg: $base-hover-bg !default; $scheduler-alldaypanel-bg: $base-element-bg !default; - $scheduler-tab-selected-bg: $scheduler-workspace-background-color !default; - $scheduler-tab-selected-before-bg: $scheduler-tab-selected-bg !default; - $scheduler-tab-focused-border-color: $base-focus-bg !default; $scheduler-other-month-cell-opacity: 0.5 !default; $scheduler-fill-focused-appointment: false !default; $scheduler-time-indicator-background-color: rgba(222, 222, 222, 0.07) !default; @@ -206,7 +187,6 @@ $agenda-appointment-text-color: null !default; $scheduler-appointment-overlay-bg: $base-bg !default; $agenda-appointment-recurrence-icon-color: $base-text-color !default; - $agenda-appointment-text-color: $base-text-color !default; $agenda-appointment-active-bg: color.change(#fff, $alpha: 0.15) !default; } @@ -230,14 +210,10 @@ $agenda-appointment-text-color: null !default; $scheduler-dd-appointment-hover-text-color: #fff !default; $scheduler-workspace-month-text-color: $month-text-color-dark !default; $scheduler-dropdown-appointment-date-color: color.adjust($base-text-color, $lightness: 38.5%, $space: hsl) !default; - $scheduler-workspace-active-cell-color: $scheduler-accent-border-color !default; $scheduler-workspace-hovered-cell-color: $scheduler-workspace-month-text-color !default; $scheduler-group-header-color: $scheduler-panel-text-color !default; $scheduler-header-bg: $base-hover-bg !default; $scheduler-alldaypanel-bg: $base-element-bg !default; - $scheduler-tab-selected-bg: $scheduler-workspace-background-color !default; - $scheduler-tab-selected-before-bg: $scheduler-tab-selected-bg !default; - $scheduler-tab-focused-border-color: $base-accent !default; $scheduler-other-month-cell-opacity: 0.5 !default; $scheduler-fill-focused-appointment: false !default; $scheduler-time-indicator-background-color: rgba(222, 222, 222, 0.07) !default; @@ -247,7 +223,6 @@ $agenda-appointment-text-color: null !default; $scheduler-appointment-overlay-bg: $base-bg !default; $agenda-appointment-recurrence-icon-color: $base-text-color !default; - $agenda-appointment-text-color: $base-text-color !default; $agenda-appointment-active-bg: color.change(#000, $alpha: 0.15) !default; } @@ -269,14 +244,10 @@ $agenda-appointment-text-color: null !default; $scheduler-dd-appointment-hover-text-color: $base-text-color !default; $scheduler-workspace-month-text-color: $month-text-color-dark !default; $scheduler-dropdown-appointment-date-color: color.adjust($base-text-color, $lightness: 38.5%, $space: hsl) !default; - $scheduler-workspace-active-cell-color: $scheduler-accent-border-color !default; $scheduler-workspace-hovered-cell-color: $scheduler-workspace-month-text-color !default; $scheduler-group-header-color: $scheduler-panel-text-color !default; $scheduler-header-bg: $base-hover-bg !default; $scheduler-alldaypanel-bg: $base-element-bg !default; - $scheduler-tab-selected-bg: $scheduler-workspace-background-color !default; - $scheduler-tab-selected-before-bg: $scheduler-tab-selected-bg !default; - $scheduler-tab-focused-border-color: $base-accent !default; $scheduler-other-month-cell-opacity: 0.5 !default; $scheduler-fill-focused-appointment: false !default; $scheduler-time-indicator-background-color: rgba(222, 222, 222, 0.07) !default; @@ -286,7 +257,6 @@ $agenda-appointment-text-color: null !default; $scheduler-appointment-overlay-bg: $base-bg !default; $agenda-appointment-recurrence-icon-color: $base-text-color !default; - $agenda-appointment-text-color: $base-text-color !default; $agenda-appointment-active-bg: color.change(#9e9ec5, $alpha: 0.35) !default; } @@ -308,15 +278,10 @@ $agenda-appointment-text-color: null !default; $scheduler-dd-appointment-hover-text-color: $base-inverted-text-color !default; $scheduler-workspace-month-text-color: color.adjust($base-text-color, $lightness: 38.5%, $space: hsl) !default; $scheduler-dropdown-appointment-date-color: color.adjust($base-text-color, $lightness: 38.5%, $space: hsl) !default; - $scheduler-time-panel-background-color: $scheduler-workspace-background-color !default; - $scheduler-workspace-active-cell-color: $scheduler-accent-border-color !default; $scheduler-workspace-hovered-cell-color: $scheduler-workspace-month-text-color !default; $scheduler-group-header-color: $scheduler-panel-text-color !default; $scheduler-header-bg: $base-hover-bg !default; $scheduler-alldaypanel-bg: $base-element-bg !default; - $scheduler-tab-selected-bg: $scheduler-time-panel-background-color !default; - $scheduler-tab-selected-before-bg: $scheduler-tab-selected-bg !default; - $scheduler-tab-focused-border-color: $base-focus-bg !default; $scheduler-other-month-cell-opacity: 0.5 !default; $scheduler-fill-focused-appointment: false !default; $scheduler-time-indicator-background-color: rgba(51, 51, 51, 0.04) !default; @@ -326,7 +291,6 @@ $agenda-appointment-text-color: null !default; $scheduler-appointment-overlay-bg: $base-bg !default; $agenda-appointment-recurrence-icon-color: $base-inverted-text-color !default; - $agenda-appointment-text-color: $base-text-color !default; $agenda-appointment-active-bg: color.change(#000, $alpha: 0.15) !default; } @@ -349,13 +313,9 @@ $agenda-appointment-text-color: null !default; $scheduler-dd-appointment-hover-text-color: $base-inverted-text-color !default; $scheduler-workspace-month-text-color: $month-text-color-light !default; $scheduler-dropdown-appointment-date-color: color.adjust($base-text-color, $lightness: 38.5%, $space: hsl) !default; - $scheduler-workspace-active-cell-color: $scheduler-accent-border-color !default; $scheduler-workspace-hovered-cell-color: $scheduler-workspace-month-text-color !default; $scheduler-group-header-color: $scheduler-panel-text-color !default; $scheduler-alldaypanel-bg: $base-element-bg !default; - $scheduler-tab-selected-bg: $scheduler-workspace-background-color !default; - $scheduler-tab-selected-before-bg: $scheduler-tab-selected-bg !default; - $scheduler-tab-focused-border-color: $base-focus-bg !default; $scheduler-other-month-cell-opacity: 0.5 !default; $scheduler-fill-focused-appointment: false !default; $scheduler-time-indicator-background-color: rgba(51, 51, 51, 0.04) !default; @@ -365,7 +325,6 @@ $agenda-appointment-text-color: null !default; $scheduler-appointment-overlay-bg: $base-bg !default; $agenda-appointment-recurrence-icon-color: $base-inverted-text-color !default; - $agenda-appointment-text-color: $base-text-color !default; $agenda-appointment-active-bg: color.change(#000, $alpha: 0.08) !default; } @@ -387,14 +346,10 @@ $agenda-appointment-text-color: null !default; $scheduler-dd-appointment-hover-text-color: $base-inverted-text-color !default; $scheduler-workspace-month-text-color: $month-text-color-light !default; $scheduler-dropdown-appointment-date-color: color.adjust($base-text-color, $lightness: 38.5%, $space: hsl) !default; - $scheduler-workspace-active-cell-color: $scheduler-accent-border-color !default; $scheduler-workspace-hovered-cell-color: $scheduler-workspace-month-text-color !default; $scheduler-group-header-color: $scheduler-panel-text-color !default; $scheduler-header-bg: $base-hover-bg !default; $scheduler-alldaypanel-bg: $base-element-bg !default; - $scheduler-tab-selected-bg: $scheduler-workspace-background-color !default; - $scheduler-tab-selected-before-bg: $scheduler-tab-selected-bg !default; - $scheduler-tab-focused-border-color: $base-accent !default; $scheduler-other-month-cell-opacity: 0.5 !default; $scheduler-fill-focused-appointment: false !default; $scheduler-time-indicator-background-color: rgba(51, 51, 51, 0.04) !default; @@ -404,8 +359,9 @@ $agenda-appointment-text-color: null !default; $scheduler-appointment-overlay-bg: $base-bg !default; $agenda-appointment-recurrence-icon-color: $base-inverted-text-color !default; - $agenda-appointment-text-color: $base-text-color !default; $agenda-appointment-active-bg: color.change(#000, $alpha: 0.08) !default; } -$scheduler-appointment-active-color: $scheduler-appointment-start-color; +$scheduler-workspace-active-cell-color: $scheduler-accent-border-color !default; + +$scheduler-appointment-active-color: $scheduler-appointment-start-color !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/scheduler/_index.scss b/packages/devextreme-scss/scss/widgets/generic/scheduler/_index.scss index 89156a91ae9a..1d8501d7f8bd 100644 --- a/packages/devextreme-scss/scss/widgets/generic/scheduler/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/scheduler/_index.scss @@ -52,21 +52,21 @@ @use "../recurrenceEditor" as *; @use "../scrollable"; -$generic-scheduler-appointment-reduced-content-padding: 40px; -$generic-scheduler-appointment-empty-transparent-border: 1px solid transparent; +$generic-scheduler-appointment-reduced-content-padding: 40px !default; +$generic-scheduler-appointment-empty-transparent-border: 1px solid transparent !default; -$generic-scheduler-all-day-table-cell-height: $generic-scheduler-workspace-date-table-cell-height * 1.5; -$generic-scheduler-collapsed-all-day-table-cell-height: $generic-scheduler-workspace-date-table-cell-height * 0.5; -$generic-scheduler-header-panel-table-cell-height: $generic-scheduler-workspace-date-table-cell-height; +$generic-scheduler-all-day-table-cell-height: $generic-scheduler-workspace-date-table-cell-height * 1.5 !default; +$generic-scheduler-collapsed-all-day-table-cell-height: $generic-scheduler-workspace-date-table-cell-height * 0.5 !default; +$generic-scheduler-header-panel-table-cell-height: $generic-scheduler-workspace-date-table-cell-height !default; -$generic-scheduler-current-time-cell-border-size: 2px; +$generic-scheduler-current-time-cell-border-size: 2px !default; -$generic-scheduler-month-date-text-size: $generic-h5-font-size; +$generic-scheduler-month-date-text-size: $generic-h5-font-size !default; -$scheduler-first-of-month-border: 1px solid; +$scheduler-first-of-month-border: 1px solid !default; -$generic-scheduler-agenda-time-cell-padding: 10px; -$generic-scheduler-agenda-group-header-padding: $generic-scheduler-agenda-time-cell-padding 0 0 10px; +$generic-scheduler-agenda-time-cell-padding: 10px !default; +$generic-scheduler-agenda-group-header-padding: $generic-scheduler-agenda-time-cell-padding 0 0 10px !default; @include baseScheduler.scheduler-mixin(); diff --git a/packages/devextreme-scss/scss/widgets/generic/scheduler/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/scheduler/_sizes.scss index 46750f3037f9..cab02338233c 100644 --- a/packages/devextreme-scss/scss/widgets/generic/scheduler/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/generic/scheduler/_sizes.scss @@ -7,32 +7,29 @@ $generic-scheduler-timeline-date-table-cell-height: null !default; $generic-scheduler-header-height: null !default; $generic-scheduler-header-offset: null !default; $generic-scheduler-toolbar-item-offset: null !default; -$generic-scheduler-navigator-height: null !default; -$generic-scheduler-navigation-buttons-padding: null !default; $generic-scheduler-appointment-month-text-size: null !default; $generic-scheduler-workspace-info-text-size: null !default; $generic-scheduler-all-day-title-font-size: null !default; $generic-scheduler-appointment-content-padding: null !default; $generic-scheduler-appointment-recurrence-content-padding: null !default; $generic-scheduler-appointment-recurrence-content-padding-rtl: null !default; -$generic-scheduler-agenda-appointment-font-size: null !default; $generic-scheduler-group-header-table-cell-width: null !default; $generic-scheduler-appointment-transparent-left-border: null !default; $generic-scheduler-appointment-transparent-right-border: null !default; $generic-scheduler-dropdown-button-height: null !default; $generic-scheduler-dropdown-button-margin: null !default; $generic-scheduler-dropdown-button-vertical-margin: null !default; -$generic-scheduler-dropdown-button-adaptive-margin: null !default; -$generic-scheduler-dropdown-button-adaptive-vertical-margin: null !default; +$generic-scheduler-dropdown-button-adaptive-margin: 0 2px !default; +$generic-scheduler-dropdown-button-adaptive-vertical-margin: 0 2px 0 0 !default; $generic-scheduler-appointment-popup-toolbar-height: null !default; $generic-scheduler-appointment-popup-toolbar-label-size: null !default; $generic-scheduler-appointment-popup-icon-size: null !default; $generic-scheduler-appointment-popup-icon-container-height: null !default; -$generic-scheduler-appointment-popup-icon-padding-right: null !default; +$generic-scheduler-appointment-popup-icon-padding-right: 5px !default; $generic-scheduler-appointment-popup-icon-margin-top: null !default; $generic-scheduler-appointment-popup-icon-inner-label-margin-top: null !default; -$generic-scheduler-appointment-popup-item-padding-horizontal: null !default; +$generic-scheduler-appointment-popup-item-padding-horizontal: 5px !default; $generic-scheduler-appointment-popup-item-padding-top: null !default; $generic-scheduler-appointment-popup-all-day-item-height: null !default; $generic-scheduler-appointment-popup-days-of-week-button-size: null !default; @@ -42,22 +39,22 @@ $generic-scheduler-appointment-popup-repeat-end-item-height: null !default; $generic-scheduler-appointment-short-content-padding: null !default; $generic-scheduler-appointment-10min-height: null !default; -$generic-scheduler-appointment-15min-height: null !default; -$generic-scheduler-appointment-20min-height: null !default; -$generic-scheduler-appointment-25min-height: null !default; -$generic-scheduler-appointment-10min-recurrence-padding-right: null !default; -$generic-scheduler-appointment-10min-title-font-size: null !default; -$generic-scheduler-appointment-10min-title-line-height: null !default; -$generic-scheduler-appointment-10min-icon-font-size: null !default; -$generic-scheduler-appointment-10min-icon-right: null !default; +$generic-scheduler-appointment-15min-height: 23px !default; +$generic-scheduler-appointment-20min-height: 32px !default; +$generic-scheduler-appointment-25min-height: 41px !default; +$generic-scheduler-appointment-10min-recurrence-padding-right: 16px !default; +$generic-scheduler-appointment-10min-title-font-size: 8px !default; +$generic-scheduler-appointment-10min-title-line-height: 16px !default; +$generic-scheduler-appointment-10min-icon-font-size: 14px !default; +$generic-scheduler-appointment-10min-icon-right: 2px !default; $generic-scheduler-appointment-15min-recurrence-padding-right: null !default; -$generic-scheduler-appointment-15min-title-font-size: null !default; +$generic-scheduler-appointment-15min-title-font-size: 12px !default; $generic-scheduler-appointment-15min-title-line-height: null !default; $generic-scheduler-appointment-15min-icon-font-size: null !default; -$generic-scheduler-appointment-15min-icon-right: null !default; +$generic-scheduler-appointment-15min-icon-right: 2px !default; $generic-scheduler-appointment-20min-recurrence-padding-right: null !default; -$agenda-appointment-title-font-size: null !default; +$agenda-appointment-title-font-size: 16px !default; $scheduler-timeline-cell-height: null !default; $scheduler-left-column-width: 100px !default; @@ -70,8 +67,6 @@ $generic-scheduler-group-header-agenda-font-size: 14px !default; $generic-scheduler-header-height: 36px !default; $generic-scheduler-header-offset: 9px 10px !default; $generic-scheduler-toolbar-item-offset: 2px !default; - $generic-scheduler-navigator-height: 36px !default; - $generic-scheduler-navigation-buttons-padding: 6px !default; $generic-scheduler-appointment-month-text-size: 13px !default; $generic-scheduler-workspace-info-text-size: 14px !default; @@ -81,7 +76,6 @@ $generic-scheduler-group-header-agenda-font-size: 14px !default; $generic-scheduler-appointment-recurrence-content-padding: 5px 25px 5px 7px !default; $generic-scheduler-appointment-recurrence-content-padding-rtl: 5px 7px 5px 25px !default; - $generic-scheduler-agenda-appointment-font-size: 16px !default; $generic-scheduler-group-header-table-cell-width: 80px !default; $generic-scheduler-appointment-transparent-left-border: 3px solid transparent !default; $generic-scheduler-appointment-transparent-right-border: 2px solid transparent !default; @@ -89,17 +83,13 @@ $generic-scheduler-group-header-agenda-font-size: 14px !default; $generic-scheduler-dropdown-button-height: 20px !default; $generic-scheduler-dropdown-button-margin: 3px 2px 3px 3px !default; $generic-scheduler-dropdown-button-vertical-margin: 3px 2px 3px 0 !default; - $generic-scheduler-dropdown-button-adaptive-margin: 0 2px !default; - $generic-scheduler-dropdown-button-adaptive-vertical-margin: 0 2px 0 0 !default; $generic-scheduler-appointment-popup-toolbar-height: 43px !default; $generic-scheduler-appointment-popup-toolbar-label-size: 20px !default; $generic-scheduler-appointment-popup-icon-size: 18px !default; $generic-scheduler-appointment-popup-icon-container-height: 36px !default; - $generic-scheduler-appointment-popup-icon-padding-right: 5px !default; $generic-scheduler-appointment-popup-icon-margin-top: 22px !default; $generic-scheduler-appointment-popup-icon-inner-label-margin-top: 7px !default; - $generic-scheduler-appointment-popup-item-padding-horizontal: 5px !default; $generic-scheduler-appointment-popup-item-padding-top: 20px !default; $generic-scheduler-appointment-popup-all-day-item-height: 36px !default; $generic-scheduler-appointment-popup-repeat-end-radio-margin-top: 10px !default; @@ -107,27 +97,14 @@ $generic-scheduler-group-header-agenda-font-size: 14px !default; $generic-scheduler-appointment-popup-days-of-week-buttons-gap: 10px !default; $generic-scheduler-appointment-popup-repeat-end-item-height: 36px !default; - $agenda-appointment-title-font-size: 16px; - $scheduler-timeline-cell-height: 78px !default; $generic-scheduler-appointment-short-content-padding: 0 7px !default; $generic-scheduler-appointment-10min-height: 23px !default; - $generic-scheduler-appointment-15min-height: 23px !default; - $generic-scheduler-appointment-20min-height: 32px !default; - $generic-scheduler-appointment-25min-height: 41px !default; - - $generic-scheduler-appointment-10min-recurrence-padding-right: 16px !default; - $generic-scheduler-appointment-10min-title-font-size: 8px !default; - $generic-scheduler-appointment-10min-title-line-height: 16px !default; - $generic-scheduler-appointment-10min-icon-font-size: 14px !default; - $generic-scheduler-appointment-10min-icon-right: 2px !default; $generic-scheduler-appointment-15min-recurrence-padding-right: 20px !default; - $generic-scheduler-appointment-15min-title-font-size: 12px !default; $generic-scheduler-appointment-15min-title-line-height: 24px !default; $generic-scheduler-appointment-15min-icon-font-size: 18px !default; - $generic-scheduler-appointment-15min-icon-right: 2px !default; $generic-scheduler-appointment-20min-recurrence-padding-right: 24px !default; } @@ -139,8 +116,6 @@ $generic-scheduler-group-header-agenda-font-size: 14px !default; $generic-scheduler-header-height: 26px !default; $generic-scheduler-header-offset: 6px 7px !default; $generic-scheduler-toolbar-item-offset: 0 !default; - $generic-scheduler-navigator-height: 25px !default; - $generic-scheduler-navigation-buttons-padding: 4px !default; $generic-scheduler-appointment-month-text-size: 11px !default; $generic-scheduler-workspace-info-text-size: 12px !default; @@ -150,7 +125,6 @@ $generic-scheduler-group-header-agenda-font-size: 14px !default; $generic-scheduler-appointment-recurrence-content-padding: 2px 25px 2px 5px !default; $generic-scheduler-appointment-recurrence-content-padding-rtl: 2px 5px 2px 25px !default; - $generic-scheduler-agenda-appointment-font-size: 14px !default; $generic-scheduler-group-header-table-cell-width: 60px !default; $generic-scheduler-appointment-transparent-left-border: 2px solid transparent !default; $generic-scheduler-appointment-transparent-right-border: 1px solid transparent !default; @@ -158,17 +132,13 @@ $generic-scheduler-group-header-agenda-font-size: 14px !default; $generic-scheduler-dropdown-button-height: 17px !default; $generic-scheduler-dropdown-button-margin: 3px 1px 3px 3px !default; $generic-scheduler-dropdown-button-vertical-margin: 3px 1px 3px 0 !default; - $generic-scheduler-dropdown-button-adaptive-margin: 0 2px !default; - $generic-scheduler-dropdown-button-adaptive-vertical-margin: 0 2px 0 0 !default; $generic-scheduler-appointment-popup-toolbar-height: 39px !default; $generic-scheduler-appointment-popup-toolbar-label-size: 18px !default; $generic-scheduler-appointment-popup-icon-size: 14px !default; $generic-scheduler-appointment-popup-icon-container-height: 26px !default; - $generic-scheduler-appointment-popup-icon-padding-right: 5px !default; $generic-scheduler-appointment-popup-icon-margin-top: 18px !default; $generic-scheduler-appointment-popup-icon-inner-label-margin-top: 6px !default; - $generic-scheduler-appointment-popup-item-padding-horizontal: 5px !default; $generic-scheduler-appointment-popup-item-padding-top: 10px !default; $generic-scheduler-appointment-popup-all-day-item-height: 24px !default; $generic-scheduler-appointment-popup-repeat-end-radio-margin-top: 6px !default; @@ -176,27 +146,14 @@ $generic-scheduler-group-header-agenda-font-size: 14px !default; $generic-scheduler-appointment-popup-days-of-week-buttons-gap: 6px !default; $generic-scheduler-appointment-popup-repeat-end-item-height: 26px !default; - $agenda-appointment-title-font-size: 16px; - $scheduler-timeline-cell-height: 50px !default; $generic-scheduler-appointment-short-content-padding: 0 5px !default; $generic-scheduler-appointment-10min-height: 14px !default; - $generic-scheduler-appointment-15min-height: 23px !default; - $generic-scheduler-appointment-20min-height: 32px !default; - $generic-scheduler-appointment-25min-height: 41px !default; - - $generic-scheduler-appointment-10min-title-font-size: 8px !default; - $generic-scheduler-appointment-10min-title-line-height: 16px !default; - $generic-scheduler-appointment-10min-recurrence-padding-right: 16px !default; - $generic-scheduler-appointment-10min-icon-font-size: 14px !default; - $generic-scheduler-appointment-10min-icon-right: 2px !default; $generic-scheduler-appointment-15min-recurrence-padding-right: 16px !default; - $generic-scheduler-appointment-15min-title-font-size: 12px !default; $generic-scheduler-appointment-15min-title-line-height: 16px !default; $generic-scheduler-appointment-15min-icon-font-size: 14px !default; - $generic-scheduler-appointment-15min-icon-right: 2px !default; $generic-scheduler-appointment-20min-recurrence-padding-right: 20px !default; } diff --git a/packages/devextreme-scss/scss/widgets/generic/scrollable/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/scrollable/_sizes.scss index 721bcb419a19..7e5c45388a59 100644 --- a/packages/devextreme-scss/scss/widgets/generic/scrollable/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/generic/scrollable/_sizes.scss @@ -1,2 +1,2 @@ -$generic-scrollbar-size-thin: 8px; -$generic-scrollbar-size: 15px; +$generic-scrollbar-size-thin: 8px !default; +$generic-scrollbar-size: 15px !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/slider/_colors.scss b/packages/devextreme-scss/scss/widgets/generic/slider/_colors.scss index 6e7b828ad034..4ffcbc4e0e48 100644 --- a/packages/devextreme-scss/scss/widgets/generic/slider/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/generic/slider/_colors.scss @@ -18,7 +18,6 @@ $slider-handle-bg: null !default; $slider-handle-border-color: null !default; $slider-handle-active-bg: null !default; $slider-handle-hover-bg: null !default; -$slider-handle-focused-bg: null !default; $slider-invalid-handle-accent-bg: null !default; $slider-handle-border-radius: $base-border-radius - 1px !default; @@ -27,7 +26,6 @@ $slider-handle-border-radius: $base-border-radius - 1px !default; $slider-handle-border-color: $base-bg !default; $slider-handle-active-bg: color.adjust($slider-handle-bg, $lightness: -17%, $space: hsl) !default; $slider-handle-hover-bg: color.adjust($slider-handle-bg, $lightness: -10%, $space: hsl) !default; - $slider-handle-focused-bg: $slider-handle-hover-bg !default; $slider-invalid-handle-accent-bg: color.adjust($base-danger, $lightness: -10%, $space: hsl) !default; } @@ -36,7 +34,6 @@ $slider-handle-border-radius: $base-border-radius - 1px !default; $slider-handle-border-color: extcolor.difference(#fff, $base-border-color) !default; $slider-handle-active-bg: $base-inverted-bg !default; $slider-handle-hover-bg: $slider-range-bg !default; - $slider-handle-focused-bg: $slider-handle-hover-bg !default; $slider-invalid-handle-accent-bg: color.adjust($base-danger, $lightness: 10%, $space: hsl) !default; } @@ -45,7 +42,6 @@ $slider-handle-border-radius: $base-border-radius - 1px !default; $slider-handle-border-color: $base-bg !default; $slider-handle-active-bg: color.adjust($slider-handle-bg, $lightness: 17%, $space: hsl) !default; $slider-handle-hover-bg: color.adjust($slider-handle-bg, $lightness: 10%, $space: hsl) !default; - $slider-handle-focused-bg: $slider-handle-hover-bg !default; $slider-invalid-handle-accent-bg: color.adjust($base-danger, $lightness: 10%, $space: hsl) !default; } @@ -54,7 +50,6 @@ $slider-handle-border-radius: $base-border-radius - 1px !default; $slider-handle-border-color: $base-bg !default; $slider-handle-active-bg: color.adjust($slider-handle-bg, $lightness: 17%, $space: hsl) !default; $slider-handle-hover-bg: color.adjust($slider-handle-bg, $lightness: 10%, $space: hsl) !default; - $slider-handle-focused-bg: $slider-handle-hover-bg !default; $slider-invalid-handle-accent-bg: color.adjust($base-danger, $lightness: 10%, $space: hsl) !default; } @@ -63,7 +58,6 @@ $slider-handle-border-radius: $base-border-radius - 1px !default; $slider-handle-border-color: $base-bg !default; $slider-handle-active-bg: color.adjust($slider-handle-bg, $lightness: 17%, $space: hsl) !default; $slider-handle-hover-bg: color.adjust($slider-handle-bg, $lightness: 10%, $space: hsl) !default; - $slider-handle-focused-bg: $slider-handle-hover-bg !default; $slider-invalid-handle-accent-bg: color.adjust($base-danger, $lightness: 10%, $space: hsl) !default; } @@ -72,7 +66,6 @@ $slider-handle-border-radius: $base-border-radius - 1px !default; $slider-handle-border-color: $base-bg !default; $slider-handle-active-bg: color.adjust($slider-handle-bg, $lightness: -17%, $space: hsl) !default; $slider-handle-hover-bg: color.adjust($slider-handle-bg, $lightness: -10%, $space: hsl) !default; - $slider-handle-focused-bg: $slider-handle-hover-bg !default; $slider-invalid-handle-accent-bg: color.adjust($base-danger, $lightness: -10%, $space: hsl) !default; } @@ -81,7 +74,6 @@ $slider-handle-border-radius: $base-border-radius - 1px !default; $slider-handle-border-color: $base-bg !default; $slider-handle-active-bg: color.adjust($slider-handle-bg, $lightness: -17%, $space: hsl) !default; $slider-handle-hover-bg: color.adjust($slider-handle-bg, $lightness: -10%, $space: hsl) !default; - $slider-handle-focused-bg: $slider-handle-hover-bg !default; $slider-invalid-handle-accent-bg: color.adjust($base-danger, $lightness: -10%, $space: hsl) !default; } @@ -90,6 +82,7 @@ $slider-handle-border-radius: $base-border-radius - 1px !default; $slider-handle-border-color: $base-bg !default; $slider-handle-active-bg: color.adjust($slider-handle-bg, $lightness: -17%, $space: hsl) !default; $slider-handle-hover-bg: color.adjust($slider-handle-bg, $lightness: -10%, $space: hsl) !default; - $slider-handle-focused-bg: $slider-handle-hover-bg !default; $slider-invalid-handle-accent-bg: color.adjust($base-danger, $lightness: -10%, $space: hsl) !default; } + +$slider-handle-focused-bg: $slider-handle-hover-bg !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/slider/_index.scss b/packages/devextreme-scss/scss/widgets/generic/slider/_index.scss index b9459bab9b75..c036962f113b 100644 --- a/packages/devextreme-scss/scss/widgets/generic/slider/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/slider/_index.scss @@ -5,7 +5,7 @@ @use "sizes" as *; @use "../../base/slider"; -$generic-slider-radius: 2px; +$generic-slider-radius: 2px !default; .dx-slider { .dx-tooltip-wrapper { diff --git a/packages/devextreme-scss/scss/widgets/generic/speedDialAction/_index.scss b/packages/devextreme-scss/scss/widgets/generic/speedDialAction/_index.scss index 396fe10c61a2..1089f51ba2a1 100644 --- a/packages/devextreme-scss/scss/widgets/generic/speedDialAction/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/speedDialAction/_index.scss @@ -5,9 +5,9 @@ @use "../button/colors" as *; @use "../overlay"; -$generic-fa-button-size: 30px; -$generic-fa-button-label-padding: 5px 10px; -$generic-fa-button-label-offset: 10px; +$generic-fa-button-size: 30px !default; +$generic-fa-button-label-padding: 5px 10px !default; +$generic-fa-button-label-offset: 10px !default; @include dx-fa-button-mixin( $generic-main-fa-button-size, diff --git a/packages/devextreme-scss/scss/widgets/generic/speedDialAction/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/speedDialAction/_sizes.scss index 650d9654e9bd..5e8f7e17bc8e 100644 --- a/packages/devextreme-scss/scss/widgets/generic/speedDialAction/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/generic/speedDialAction/_sizes.scss @@ -7,10 +7,10 @@ $generic-fa-button-label-font-size: 12px !default; @if $size == "default" { $generic-main-fa-button-size: 48px !default; - $generic-main-fa-button-label-padding: 0 25px 0 10px; + $generic-main-fa-button-label-padding: 0 25px 0 10px !default; } @else if $size == "compact" { $generic-main-fa-button-size: 34px !default; - $generic-main-fa-button-label-padding: 0 15px 0 5px; + $generic-main-fa-button-label-padding: 0 15px 0 5px !default; } diff --git a/packages/devextreme-scss/scss/widgets/generic/splitter/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/splitter/_sizes.scss index 839b901f0580..b4d9ec2a53d7 100644 --- a/packages/devextreme-scss/scss/widgets/generic/splitter/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/generic/splitter/_sizes.scss @@ -1 +1 @@ -$splitter-resize-handle-icon-size: 18px; +$splitter-resize-handle-icon-size: 18px !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/switch/_colors.scss b/packages/devextreme-scss/scss/widgets/generic/switch/_colors.scss index a4e5208391da..85340cd00c40 100644 --- a/packages/devextreme-scss/scss/widgets/generic/switch/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/generic/switch/_colors.scss @@ -35,7 +35,6 @@ $switch-active-border-color: null !default; $switch-off-color: null !default; $switch-handle-off-bg: null !default; $switch-readonly-border-color: null !default; -$switch-container-readonly-bg: null !default; $switch-border-radius: null !default; @if $color == "carmine" { @@ -51,7 +50,6 @@ $switch-border-radius: null !default; $switch-off-color: color.adjust($base-text-color, $lightness: 40%, $space: hsl) !default; $switch-handle-off-bg: color.adjust($switch-handle-bg, $lightness: 15%, $space: hsl) !default; $switch-readonly-border-color: color.adjust($switch-border-color, $lightness: 9%, $space: hsl) !default; - $switch-container-readonly-bg: $switch-bg !default; $switch-border-radius: $base-border-radius-exsmall !default; } @@ -67,7 +65,6 @@ $switch-border-radius: null !default; $switch-off-color: $base-text-color !default; $switch-handle-off-bg: $base-inverted-bg !default; $switch-readonly-border-color: color.adjust($switch-border-color, $lightness: -50%, $space: hsl) !default; - $switch-container-readonly-bg: $switch-bg !default; $switch-border-radius: $base-border-radius - 2px !default; } @@ -83,7 +80,6 @@ $switch-border-radius: null !default; $switch-off-color: color.adjust($base-text-color, $lightness: -40%, $space: hsl) !default; $switch-handle-off-bg: color.adjust($switch-handle-bg, $lightness: -15%, $space: hsl) !default; $switch-readonly-border-color: color.adjust($switch-border-color, $lightness: -9%, $space: hsl) !default; - $switch-container-readonly-bg: $switch-bg !default; $switch-border-radius: $base-border-radius-exsmall !default; } @@ -99,7 +95,6 @@ $switch-border-radius: null !default; $switch-off-color: color.adjust($base-text-color, $lightness: -40%, $space: hsl) !default; $switch-handle-off-bg: color.adjust($switch-handle-bg, $lightness: -15%, $space: hsl) !default; $switch-readonly-border-color: color.adjust($switch-border-color, $lightness: -9%, $space: hsl) !default; - $switch-container-readonly-bg: $switch-bg !default; $switch-border-radius: $base-border-radius-exsmall !default; } @@ -115,7 +110,6 @@ $switch-border-radius: null !default; $switch-off-color: color.adjust($base-text-color, $lightness: -40%, $space: hsl) !default; $switch-handle-off-bg: color.adjust($switch-handle-bg, $lightness: -15%, $space: hsl) !default; $switch-readonly-border-color: color.adjust($switch-border-color, $lightness: -9%, $space: hsl) !default; - $switch-container-readonly-bg: $switch-bg !default; $switch-border-radius: $base-border-radius-exsmall !default; } @@ -131,7 +125,6 @@ $switch-border-radius: null !default; $switch-off-color: color.adjust($base-text-color, $lightness: 40%, $space: hsl) !default; $switch-handle-off-bg: color.adjust($switch-handle-bg, $lightness: 15%, $space: hsl) !default; $switch-readonly-border-color: color.adjust($switch-border-color, $lightness: 9%, $space: hsl) !default; - $switch-container-readonly-bg: $switch-bg !default; $switch-border-radius: $base-border-radius-exsmall !default; } @@ -147,7 +140,6 @@ $switch-border-radius: null !default; $switch-off-color: color.adjust($base-text-color, $lightness: 40%, $space: hsl) !default; $switch-handle-off-bg: color.adjust($switch-handle-bg, $lightness: 15%, $space: hsl) !default; $switch-readonly-border-color: color.adjust($switch-border-color, $lightness: 9%, $space: hsl) !default; - $switch-container-readonly-bg: $switch-bg !default; $switch-border-radius: $base-border-radius-exsmall !default; } @@ -163,6 +155,7 @@ $switch-border-radius: null !default; $switch-off-color: color.adjust($base-text-color, $lightness: 40%, $space: hsl) !default; $switch-handle-off-bg: color.adjust($switch-handle-bg, $lightness: 15%, $space: hsl) !default; $switch-readonly-border-color: color.adjust($switch-border-color, $lightness: 9%, $space: hsl) !default; - $switch-container-readonly-bg: $switch-bg !default; $switch-border-radius: $base-border-radius-exsmall !default; } + +$switch-container-readonly-bg: $switch-bg !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/switch/_index.scss b/packages/devextreme-scss/scss/widgets/generic/switch/_index.scss index 7b3bf528f688..de3aee4096fd 100644 --- a/packages/devextreme-scss/scss/widgets/generic/switch/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/switch/_index.scss @@ -4,8 +4,8 @@ @use "../../base/mixins" as *; @use "../../base/switch"; -$generic-switch-border-weight: 1px; -$generic-switch-inner-size: $generic-switch-height - $generic-switch-border-weight * 2; +$generic-switch-border-weight: 1px !default; +$generic-switch-inner-size: $generic-switch-height - $generic-switch-border-weight * 2 !default; .dx-switch { width: $generic-switch-width; diff --git a/packages/devextreme-scss/scss/widgets/generic/tabPanel/variables/_borders.scss b/packages/devextreme-scss/scss/widgets/generic/tabPanel/variables/_borders.scss index 7065d72a2709..89b71d9318f2 100644 --- a/packages/devextreme-scss/scss/widgets/generic/tabPanel/variables/_borders.scss +++ b/packages/devextreme-scss/scss/widgets/generic/tabPanel/variables/_borders.scss @@ -1,93 +1,4 @@ -@use "../../colors" as *; -@use "../variables/sizes" as *; -@use "../../button/colors" as *; @use "../../tabs/variables/sizes" as *; -@use "../../tabs/variables/colors" as *; -$generic-tabpanel-tab-border-transparent: null !default; -$generic-tabpanel-tab-border-selected-bottom: null !default; -$generic-tabpanel-tab-border-active-bottom: null !default; - -$generic-tabpanel-tab-border-focused-bottom: null !default; -$generic-tabpanel-tab-border-focused-left: null !default; -$generic-tabpanel-tab-border-focused-right: null !default; - -@if $color == "carmine" { - $generic-tabpanel-tab-border-transparent: $generic-tabs-border-width solid transparent !default; - $generic-tabpanel-tab-border-selected-bottom: $generic-tabs-border-width solid !default; - $generic-tabpanel-tab-border-active-bottom: $generic-tabs-border-width solid $tabs-focused-border-color !default; - - $generic-tabpanel-tab-border-focused-bottom: $generic-tabs-border-width solid $tabs-tab-selected-bg !default; - $generic-tabpanel-tab-border-focused-left: $generic-tabs-border-width solid $tabs-focused-border-color !default; - $generic-tabpanel-tab-border-focused-right: $generic-tabs-border-width solid $tabs-focused-border-color !default; -} - -@if $color == "contrast" { - $generic-tabpanel-tab-border-transparent: $generic-tabs-border-width solid transparent !default; - $generic-tabpanel-tab-border-selected-bottom: $generic-tabs-border-width solid !default; - $generic-tabpanel-tab-border-active-bottom: $generic-tabs-border-width solid $tabs-focused-border-color !default; - - $generic-tabpanel-tab-border-focused-bottom: $generic-tabs-border-width solid !default; - $generic-tabpanel-tab-border-focused-left: $generic-tabs-border-width solid $tabs-focused-border-color !default; - $generic-tabpanel-tab-border-focused-right: $generic-tabs-border-width solid $tabs-focused-border-color !default; -} - -@if $color == "dark" { - $generic-tabpanel-tab-border-transparent: $generic-tabs-border-width solid transparent !default; - $generic-tabpanel-tab-border-selected-bottom: $generic-tabs-border-width solid !default; - $generic-tabpanel-tab-border-active-bottom: $generic-tabs-border-width solid $tabs-focused-border-color !default; - - $generic-tabpanel-tab-border-focused-bottom: $generic-tabs-border-width solid $tabs-tab-selected-bg !default; - $generic-tabpanel-tab-border-focused-left: $generic-tabs-border-width solid $tabs-focused-border-color !default; - $generic-tabpanel-tab-border-focused-right: $generic-tabs-border-width solid $tabs-focused-border-color !default; -} - -@if $color == "darkmoon" { - $generic-tabpanel-tab-border-transparent: $generic-tabs-border-width solid transparent !default; - $generic-tabpanel-tab-border-selected-bottom: $generic-tabs-border-width solid !default; - $generic-tabpanel-tab-border-active-bottom: $generic-tabs-border-width solid $tabs-focused-border-color !default; - - $generic-tabpanel-tab-border-focused-bottom: $generic-tabs-border-width solid $tabs-tab-selected-bg !default; - $generic-tabpanel-tab-border-focused-left: $generic-tabs-border-width solid $tabs-focused-border-color !default; - $generic-tabpanel-tab-border-focused-right: $generic-tabs-border-width solid $tabs-focused-border-color !default; -} - -@if $color == "darkviolet" { - $generic-tabpanel-tab-border-transparent: $generic-tabs-border-width solid transparent !default; - $generic-tabpanel-tab-border-selected-bottom: $generic-tabs-border-width solid !default; - $generic-tabpanel-tab-border-active-bottom: $generic-tabs-border-width solid $tabs-focused-border-color !default; - - $generic-tabpanel-tab-border-focused-bottom: $generic-tabs-border-width solid $tabs-tab-selected-bg !default; - $generic-tabpanel-tab-border-focused-left: $generic-tabs-border-width solid $tabs-focused-border-color !default; - $generic-tabpanel-tab-border-focused-right: $generic-tabs-border-width solid $tabs-focused-border-color !default; -} - -@if $color == "greenmist" { - $generic-tabpanel-tab-border-transparent: $generic-tabs-border-width solid transparent !default; - $generic-tabpanel-tab-border-selected-bottom: $generic-tabs-border-width solid !default; - $generic-tabpanel-tab-border-active-bottom: $generic-tabs-border-width solid $tabs-focused-border-color !default; - - $generic-tabpanel-tab-border-focused-bottom: $generic-tabs-border-width solid $tabs-tab-selected-bg !default; - $generic-tabpanel-tab-border-focused-left: $generic-tabs-border-width solid $tabs-focused-border-color !default; - $generic-tabpanel-tab-border-focused-right: $generic-tabs-border-width solid $tabs-focused-border-color !default; -} - -@if $color == "light" { - $generic-tabpanel-tab-border-transparent: $generic-tabs-border-width solid transparent !default; - $generic-tabpanel-tab-border-selected-bottom: $generic-tabs-border-width solid !default; - $generic-tabpanel-tab-border-active-bottom: $generic-tabs-border-width solid $tabs-focused-border-color !default; - - $generic-tabpanel-tab-border-focused-bottom: $generic-tabs-border-width solid $tabs-tab-selected-bg !default; - $generic-tabpanel-tab-border-focused-left: $generic-tabs-border-width solid $tabs-focused-border-color !default; - $generic-tabpanel-tab-border-focused-right: $generic-tabs-border-width solid $tabs-focused-border-color !default; -} - -@if $color == "softblue" { - $generic-tabpanel-tab-border-transparent: $generic-tabs-border-width solid transparent !default; - $generic-tabpanel-tab-border-selected-bottom: $generic-tabs-border-width solid !default; - $generic-tabpanel-tab-border-active-bottom: $generic-tabs-border-width solid $tabs-focused-border-color !default; - - $generic-tabpanel-tab-border-focused-bottom: $generic-tabs-border-width solid $tabs-tab-selected-bg !default; - $generic-tabpanel-tab-border-focused-left: $generic-tabs-border-width solid $tabs-focused-border-color !default; - $generic-tabpanel-tab-border-focused-right: $generic-tabs-border-width solid $tabs-focused-border-color !default; -} +$generic-tabpanel-tab-border-transparent: $generic-tabs-border-width solid transparent !default; +$generic-tabpanel-tab-border-selected-bottom: $generic-tabs-border-width solid !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/tabs/variables/_colors.scss b/packages/devextreme-scss/scss/widgets/generic/tabs/variables/_colors.scss index 63fc74fbffb6..99b07836830a 100644 --- a/packages/devextreme-scss/scss/widgets/generic/tabs/variables/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/generic/tabs/variables/_colors.scss @@ -50,8 +50,7 @@ $tabs-focused-disabled-border-color: null !default; $tabs-tab-hover-bg: null !default; $tabs-tab-active-bg: null !default; $tabs-tab-active-border-color: null !default; -$tabs-tab-hover-border-color-horizontal: null !default; -$tabs-tab-hover-border-color-vertical: null !default; +$tabs-tab-hover-border-color-horizontal: $base-border-color !default; @if $color == "carmine" { $tabs-tab-selected-color: $base-text-color !default; @@ -64,8 +63,6 @@ $tabs-tab-hover-border-color-vertical: null !default; $tabs-tab-hover-bg: $base-hover-bg !default; $tabs-tab-active-bg: color.change(color.adjust($tabs-tab-bg, $lightness: -62.5%, $space: hsl), $alpha: 0.2) !default; $tabs-tab-active-border-color: color.change(color.adjust($base-border-color, $lightness: -2.55%, $space: hsl)) !default; - $tabs-tab-hover-border-color-horizontal: $base-border-color !default; - $tabs-tab-hover-border-color-vertical: color.change(color.adjust($base-accent, $lightness: 33.92%, $space: hsl)) !default; } @if $color == "contrast" { @@ -79,8 +76,6 @@ $tabs-tab-hover-border-color-vertical: null !default; $tabs-focused-disabled-border-color: color.adjust($base-border-color, $lightness: -20%, $space: hsl) !default; $tabs-tab-active-bg: $base-inverted-bg !default; $tabs-tab-active-border-color: $base-border-color !default; - $tabs-tab-hover-border-color-horizontal: $base-border-color !default; - $tabs-tab-hover-border-color-vertical: $tabs-tab-hover-bg !default; } @if $color == "dark" { @@ -94,8 +89,6 @@ $tabs-tab-hover-border-color-vertical: null !default; $tabs-tab-hover-bg: $base-element-bg !default; $tabs-tab-active-bg: color.change(color.adjust($tabs-tab-bg, $lightness: 62.5%, $space: hsl), $alpha: 0.2) !default; $tabs-tab-active-border-color: color.change(color.adjust($tabs-tab-bg, $lightness: 12.55%, $space: hsl)) !default; - $tabs-tab-hover-border-color-horizontal: $base-border-color !default; - $tabs-tab-hover-border-color-vertical: $tabs-tab-hover-bg !default; } @if $color == "darkmoon" { @@ -109,8 +102,6 @@ $tabs-tab-hover-border-color-vertical: null !default; $tabs-tab-hover-bg: $base-element-bg !default; $tabs-tab-active-bg: color.change(color.adjust($tabs-tab-bg, $lightness: 62.5%, $space: hsl), $alpha: 0.2) !default; $tabs-tab-active-border-color: transparent !default; - $tabs-tab-hover-border-color-horizontal: $base-border-color !default; - $tabs-tab-hover-border-color-vertical: $tabs-tab-hover-bg !default; } @if $color == "darkviolet" { @@ -124,8 +115,6 @@ $tabs-tab-hover-border-color-vertical: null !default; $tabs-tab-hover-bg: $base-hover-bg !default; $tabs-tab-active-bg: color.change(color.adjust($tabs-tab-bg, $lightness: 62.5%, $space: hsl), $alpha: 0.2) !default; $tabs-tab-active-border-color: color.change(color.adjust($base-bg, $lightness: 15.49%, $space: hsl)) !default; - $tabs-tab-hover-border-color-horizontal: $base-border-color !default; - $tabs-tab-hover-border-color-vertical: color.change(color.adjust($tabs-tab-bg, $lightness: 16.86%, $space: hsl)) !default; } @if $color == "greenmist" { @@ -139,8 +128,6 @@ $tabs-tab-hover-border-color-vertical: null !default; $tabs-tab-hover-bg: $base-element-bg !default; $tabs-tab-active-bg: color.change(color.adjust($tabs-tab-bg, $lightness: -62.5%, $space: hsl), $alpha: 0.2) !default; $tabs-tab-active-border-color: color.adjust($tabs-tab-bg, $lightness: -12.16%, $space: hsl) !default; - $tabs-tab-hover-border-color-horizontal: $base-border-color !default; - $tabs-tab-hover-border-color-vertical: $tabs-tab-hover-bg !default; } @if $color == "light" { @@ -154,8 +141,6 @@ $tabs-tab-hover-border-color-vertical: null !default; $tabs-tab-hover-bg: $base-element-bg !default; $tabs-tab-active-bg: color.change(color.adjust($tabs-tab-bg, $lightness: -62.5%, $space: hsl), $alpha: 0.2) !default; $tabs-tab-active-border-color: color.adjust($tabs-tab-bg, $lightness: -12.16%, $space: hsl) !default; - $tabs-tab-hover-border-color-horizontal: $base-border-color !default; - $tabs-tab-hover-border-color-vertical: $tabs-tab-hover-bg !default; } @if $color == "softblue" { @@ -169,6 +154,4 @@ $tabs-tab-hover-border-color-vertical: null !default; $tabs-tab-hover-bg: $base-element-bg !default; $tabs-tab-active-bg: color.change(color.adjust($tabs-tab-bg, $lightness: -62.5%, $space: hsl), $alpha: 0.2) !default; $tabs-tab-active-border-color: color.adjust($tabs-tab-bg, $lightness: -12.16%, $space: hsl) !default; - $tabs-tab-hover-border-color-horizontal: $base-border-color !default; - $tabs-tab-hover-border-color-vertical: $tabs-tab-hover-bg !default; } diff --git a/packages/devextreme-scss/scss/widgets/generic/tabs/variables/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/tabs/variables/_sizes.scss index 9aa3bb5dc194..5183306a42b6 100644 --- a/packages/devextreme-scss/scss/widgets/generic/tabs/variables/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/generic/tabs/variables/_sizes.scss @@ -3,11 +3,10 @@ $generic-tabs-item-gap: null !default; $generic-tabs-item-padding: null !default; -$generic-tabs-item-padding-disabled-focused: null !default; $generic-tabs-border-width: $generic-base-border-width !default; -$generic-tabs-nav-button-height: null !default; -$generic-tabs-nav-button-width: null !default; +$generic-tabs-nav-button-height: 38px !default; +$generic-tabs-nav-button-width: 25px !default; $generic-tab-text-line-height: null !default; $generic-tabs-icon-margin: null !default; $generic-tabs-icon-size: $generic-base-icon-size !default; @@ -16,10 +15,7 @@ $generic-badge-min-size: null !default; @if $size == "default" { $generic-tabs-item-gap: 4px !default; $generic-tabs-item-padding: 10px 9px !default; - $generic-tabs-item-padding-disabled-focused: 10px !default; - $generic-tabs-nav-button-width: 25px !default; $generic-tab-text-line-height: 1.285715 !default; - $generic-tabs-nav-button-height: 38px !default; $generic-tabs-icon-margin: 8px !default; $generic-badge-min-size: 18px !default; } @@ -27,10 +23,7 @@ $generic-badge-min-size: null !default; @else if $size == "compact" { $generic-tabs-item-gap: 2px !default; $generic-tabs-item-padding: 5px 4px !default; - $generic-tabs-item-padding-disabled-focused: 5px !default; - $generic-tabs-nav-button-width: 25px !default; $generic-tab-text-line-height: 1.75 !default; - $generic-tabs-nav-button-height: 38px !default; $generic-tabs-icon-margin: 7px !default; $generic-badge-min-size: 21px !default; } diff --git a/packages/devextreme-scss/scss/widgets/generic/tabs/variables/borders/_borders-horizontal.scss b/packages/devextreme-scss/scss/widgets/generic/tabs/variables/borders/_borders-horizontal.scss index eed206407654..42077dcc6b05 100644 --- a/packages/devextreme-scss/scss/widgets/generic/tabs/variables/borders/_borders-horizontal.scss +++ b/packages/devextreme-scss/scss/widgets/generic/tabs/variables/borders/_borders-horizontal.scss @@ -8,243 +8,27 @@ $generic-tabs-border: $generic-tabs-border-width solid !default; $generic-tabs-border-transparent: $generic-tabs-border-width solid transparent !default; -$generic-tabs-border-hover-top: null !default; -$generic-tabs-border-hover-bottom: null !default; -$generic-tabs-border-hover-left: null !default; -$generic-tabs-border-hover-right: null !default; - -$generic-tabs-border-selected-top: null !default; -$generic-tabs-border-selected-bottom: null !default; -$generic-tabs-border-selected-left: null !default; -$generic-tabs-border-selected-right: null !default; - -$generic-tabs-border-focused-top: null !default; -$generic-tabs-border-focused-bottom: null !default; -$generic-tabs-border-focused-left: null !default; -$generic-tabs-border-focused-right: null !default; - -$generic-tabs-border-focused-disabled-top: null !default; -$generic-tabs-border-focused-disabled-bottom: null !default; -$generic-tabs-border-focused-disabled-left: null !default; -$generic-tabs-border-focused-disabled-right: null !default; - -$generic-tabs-border-active-top: null !default; -$generic-tabs-border-active-bottom: null !default; -$generic-tabs-border-active-left: null !default; -$generic-tabs-border-active-right: null !default; - -@if $color == "carmine" { - $generic-tabs-border-hover-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-hover-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-hover-left: none !default; - $generic-tabs-border-hover-right: none !default; - - $generic-tabs-border-selected-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-selected-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-selected-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-selected-right: $generic-tabs-border-width solid !default; - - $generic-tabs-border-focused-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-right: $generic-tabs-border-width solid !default; - - $generic-tabs-border-focused-disabled-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-disabled-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-disabled-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-disabled-right: $generic-tabs-border-width solid !default; - - $generic-tabs-border-active-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-active-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-active-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-active-right: $generic-tabs-border-width solid !default; -} - -@if $color == "contrast" { - $generic-tabs-border-hover-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-hover-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-hover-left: none !default; - $generic-tabs-border-hover-right: none !default; - - $generic-tabs-border-selected-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-selected-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-selected-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-selected-right: $generic-tabs-border-width solid !default; - - $generic-tabs-border-focused-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-right: $generic-tabs-border-width solid !default; - - $generic-tabs-border-focused-disabled-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-disabled-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-disabled-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-disabled-right: $generic-tabs-border-width solid !default; - - $generic-tabs-border-active-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-active-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-active-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-active-right: $generic-tabs-border-width solid !default; -} - -@if $color == "dark" { - $generic-tabs-border-hover-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-hover-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-hover-left: none !default; - $generic-tabs-border-hover-right: none !default; - - $generic-tabs-border-selected-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-selected-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-selected-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-selected-right: $generic-tabs-border-width solid !default; - - $generic-tabs-border-focused-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-right: $generic-tabs-border-width solid !default; - - $generic-tabs-border-focused-disabled-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-disabled-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-disabled-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-disabled-right: $generic-tabs-border-width solid !default; - - $generic-tabs-border-active-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-active-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-active-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-active-right: $generic-tabs-border-width solid !default; -} - -@if $color == "darkmoon" { - $generic-tabs-border-hover-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-hover-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-hover-left: none !default; - $generic-tabs-border-hover-right: none !default; - - $generic-tabs-border-selected-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-selected-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-selected-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-selected-right: $generic-tabs-border-width solid !default; - - $generic-tabs-border-focused-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-right: $generic-tabs-border-width solid !default; - - $generic-tabs-border-focused-disabled-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-disabled-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-disabled-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-disabled-right: $generic-tabs-border-width solid !default; - - $generic-tabs-border-active-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-active-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-active-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-active-right: $generic-tabs-border-width solid !default; -} - -@if $color == "darkviolet" { - $generic-tabs-border-hover-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-hover-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-hover-left: none !default; - $generic-tabs-border-hover-right: none !default; - - $generic-tabs-border-selected-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-selected-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-selected-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-selected-right: $generic-tabs-border-width solid !default; - - $generic-tabs-border-focused-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-right: $generic-tabs-border-width solid !default; - - $generic-tabs-border-focused-disabled-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-disabled-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-disabled-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-disabled-right: $generic-tabs-border-width solid !default; - - $generic-tabs-border-active-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-active-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-active-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-active-right: $generic-tabs-border-width solid !default; -} - -@if $color == "greenmist" { - $generic-tabs-border-hover-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-hover-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-hover-left: none !default; - $generic-tabs-border-hover-right: none !default; - - $generic-tabs-border-selected-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-selected-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-selected-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-selected-right: $generic-tabs-border-width solid !default; - - $generic-tabs-border-focused-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-right: $generic-tabs-border-width solid !default; - - $generic-tabs-border-focused-disabled-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-disabled-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-disabled-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-disabled-right: $generic-tabs-border-width solid !default; - - $generic-tabs-border-active-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-active-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-active-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-active-right: $generic-tabs-border-width solid !default; -} - -@if $color == "light" { - $generic-tabs-border-hover-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-hover-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-hover-left: none !default; - $generic-tabs-border-hover-right: none !default; - - $generic-tabs-border-selected-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-selected-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-selected-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-selected-right: $generic-tabs-border-width solid !default; - - $generic-tabs-border-focused-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-right: $generic-tabs-border-width solid !default; - - $generic-tabs-border-focused-disabled-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-disabled-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-disabled-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-disabled-right: $generic-tabs-border-width solid !default; - - $generic-tabs-border-active-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-active-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-active-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-active-right: $generic-tabs-border-width solid !default; -} - -@if $color == "softblue" { - $generic-tabs-border-hover-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-hover-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-hover-left: none !default; - $generic-tabs-border-hover-right: none !default; - - $generic-tabs-border-selected-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-selected-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-selected-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-selected-right: $generic-tabs-border-width solid !default; - - $generic-tabs-border-focused-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-right: $generic-tabs-border-width solid !default; - - $generic-tabs-border-focused-disabled-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-disabled-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-disabled-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-focused-disabled-right: $generic-tabs-border-width solid !default; - - $generic-tabs-border-active-top: $generic-tabs-border-width solid !default; - $generic-tabs-border-active-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-border-active-left: $generic-tabs-border-width solid !default; - $generic-tabs-border-active-right: $generic-tabs-border-width solid !default; -} +$generic-tabs-border-hover-top: $generic-tabs-border-width solid !default; +$generic-tabs-border-hover-bottom: $generic-tabs-border-width solid !default; +$generic-tabs-border-hover-left: none !default; +$generic-tabs-border-hover-right: none !default; + +$generic-tabs-border-selected-top: $generic-tabs-border-width solid !default; +$generic-tabs-border-selected-bottom: $generic-tabs-border-width solid !default; +$generic-tabs-border-selected-left: $generic-tabs-border-width solid !default; +$generic-tabs-border-selected-right: $generic-tabs-border-width solid !default; + +$generic-tabs-border-focused-top: $generic-tabs-border-width solid !default; +$generic-tabs-border-focused-bottom: $generic-tabs-border-width solid !default; +$generic-tabs-border-focused-left: $generic-tabs-border-width solid !default; +$generic-tabs-border-focused-right: $generic-tabs-border-width solid !default; + +$generic-tabs-border-focused-disabled-top: $generic-tabs-border-width solid !default; +$generic-tabs-border-focused-disabled-bottom: $generic-tabs-border-width solid !default; +$generic-tabs-border-focused-disabled-left: $generic-tabs-border-width solid !default; +$generic-tabs-border-focused-disabled-right: $generic-tabs-border-width solid !default; + +$generic-tabs-border-active-top: $generic-tabs-border-width solid !default; +$generic-tabs-border-active-bottom: $generic-tabs-border-width solid !default; +$generic-tabs-border-active-left: $generic-tabs-border-width solid !default; +$generic-tabs-border-active-right: $generic-tabs-border-width solid !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/tabs/variables/borders/_borders-vertical.scss b/packages/devextreme-scss/scss/widgets/generic/tabs/variables/borders/_borders-vertical.scss index f9d01c04e193..04cc910391e2 100644 --- a/packages/devextreme-scss/scss/widgets/generic/tabs/variables/borders/_borders-vertical.scss +++ b/packages/devextreme-scss/scss/widgets/generic/tabs/variables/borders/_borders-vertical.scss @@ -6,243 +6,27 @@ @use "../../variables/sizes" as *; @use "../../variables/borders/borders-horizontal"; -$generic-tabs-vertical-border-hover-top: null !default; -$generic-tabs-vertical-border-hover-bottom: null !default; -$generic-tabs-vertical-border-hover-left: null !default; -$generic-tabs-vertical-border-hover-right: null !default; - -$generic-tabs-vertical-border-selected-top: null !default; -$generic-tabs-vertical-border-selected-bottom: null !default; -$generic-tabs-vertical-border-selected-left: null !default; -$generic-tabs-vertical-border-selected-right: null !default; - -$generic-tabs-vertical-border-focused-top: null !default; -$generic-tabs-vertical-border-focused-bottom: null !default; -$generic-tabs-vertical-border-focused-left: null !default; -$generic-tabs-vertical-border-focused-right: null !default; - -$generic-tabs-vertical-border-focused-disabled-top: null !default; -$generic-tabs-vertical-border-focused-disabled-bottom: null !default; -$generic-tabs-vertical-border-focused-disabled-left: null !default; -$generic-tabs-vertical-border-focused-disabled-right: null !default; - -$generic-tabs-vertical-border-active-top: null !default; -$generic-tabs-vertical-border-active-bottom: null !default; -$generic-tabs-vertical-border-active-left: null !default; -$generic-tabs-vertical-border-active-right: null !default; - -@if $color == "carmine" { - $generic-tabs-vertical-border-hover-top: none !default; - $generic-tabs-vertical-border-hover-bottom: none !default; - $generic-tabs-vertical-border-hover-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-hover-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-selected-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-selected-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-selected-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-selected-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-focused-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-focused-disabled-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-disabled-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-disabled-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-disabled-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-active-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-active-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-active-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-active-right: $generic-tabs-border-width solid !default; -} - -@if $color == "contrast" { - $generic-tabs-vertical-border-hover-top: none !default; - $generic-tabs-vertical-border-hover-bottom: none !default; - $generic-tabs-vertical-border-hover-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-hover-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-selected-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-selected-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-selected-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-selected-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-focused-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-focused-disabled-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-disabled-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-disabled-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-disabled-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-active-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-active-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-active-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-active-right: $generic-tabs-border-width solid !default; -} - -@if $color == "dark" { - $generic-tabs-vertical-border-hover-top: none !default; - $generic-tabs-vertical-border-hover-bottom: none !default; - $generic-tabs-vertical-border-hover-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-hover-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-selected-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-selected-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-selected-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-selected-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-focused-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-focused-disabled-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-disabled-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-disabled-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-disabled-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-active-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-active-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-active-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-active-right: $generic-tabs-border-width solid !default; -} - -@if $color == "darkmoon" { - $generic-tabs-vertical-border-hover-top: none !default; - $generic-tabs-vertical-border-hover-bottom: none !default; - $generic-tabs-vertical-border-hover-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-hover-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-selected-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-selected-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-selected-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-selected-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-focused-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-focused-disabled-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-disabled-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-disabled-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-disabled-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-active-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-active-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-active-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-active-right: $generic-tabs-border-width solid !default; -} - -@if $color == "darkviolet" { - $generic-tabs-vertical-border-hover-top: none !default; - $generic-tabs-vertical-border-hover-bottom: none !default; - $generic-tabs-vertical-border-hover-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-hover-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-selected-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-selected-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-selected-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-selected-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-focused-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-focused-disabled-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-disabled-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-disabled-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-disabled-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-active-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-active-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-active-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-active-right: $generic-tabs-border-width solid !default; -} - -@if $color == "greenmist" { - $generic-tabs-vertical-border-hover-top: none !default; - $generic-tabs-vertical-border-hover-bottom: none !default; - $generic-tabs-vertical-border-hover-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-hover-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-selected-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-selected-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-selected-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-selected-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-focused-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-focused-disabled-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-disabled-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-disabled-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-disabled-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-active-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-active-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-active-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-active-right: $generic-tabs-border-width solid !default; -} - -@if $color == "light" { - $generic-tabs-vertical-border-hover-top: none !default; - $generic-tabs-vertical-border-hover-bottom: none !default; - $generic-tabs-vertical-border-hover-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-hover-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-selected-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-selected-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-selected-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-selected-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-focused-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-focused-disabled-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-disabled-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-disabled-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-disabled-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-active-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-active-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-active-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-active-right: $generic-tabs-border-width solid !default; -} - -@if $color == "softblue" { - $generic-tabs-vertical-border-hover-top: none !default; - $generic-tabs-vertical-border-hover-bottom: none !default; - $generic-tabs-vertical-border-hover-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-hover-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-selected-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-selected-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-selected-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-selected-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-focused-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-focused-disabled-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-disabled-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-disabled-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-focused-disabled-right: $generic-tabs-border-width solid !default; - - $generic-tabs-vertical-border-active-top: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-active-bottom: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-active-left: $generic-tabs-border-width solid !default; - $generic-tabs-vertical-border-active-right: $generic-tabs-border-width solid !default; -} +$generic-tabs-vertical-border-hover-top: none !default; +$generic-tabs-vertical-border-hover-bottom: none !default; +$generic-tabs-vertical-border-hover-left: $generic-tabs-border-width solid !default; +$generic-tabs-vertical-border-hover-right: $generic-tabs-border-width solid !default; + +$generic-tabs-vertical-border-selected-top: $generic-tabs-border-width solid !default; +$generic-tabs-vertical-border-selected-bottom: $generic-tabs-border-width solid !default; +$generic-tabs-vertical-border-selected-left: $generic-tabs-border-width solid !default; +$generic-tabs-vertical-border-selected-right: $generic-tabs-border-width solid !default; + +$generic-tabs-vertical-border-focused-top: $generic-tabs-border-width solid !default; +$generic-tabs-vertical-border-focused-bottom: $generic-tabs-border-width solid !default; +$generic-tabs-vertical-border-focused-left: $generic-tabs-border-width solid !default; +$generic-tabs-vertical-border-focused-right: $generic-tabs-border-width solid !default; + +$generic-tabs-vertical-border-focused-disabled-top: $generic-tabs-border-width solid !default; +$generic-tabs-vertical-border-focused-disabled-bottom: $generic-tabs-border-width solid !default; +$generic-tabs-vertical-border-focused-disabled-left: $generic-tabs-border-width solid !default; +$generic-tabs-vertical-border-focused-disabled-right: $generic-tabs-border-width solid !default; + +$generic-tabs-vertical-border-active-top: $generic-tabs-border-width solid !default; +$generic-tabs-vertical-border-active-bottom: $generic-tabs-border-width solid !default; +$generic-tabs-vertical-border-active-left: $generic-tabs-border-width solid !default; +$generic-tabs-vertical-border-active-right: $generic-tabs-border-width solid !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/tagBox/_colors.scss b/packages/devextreme-scss/scss/widgets/generic/tagBox/_colors.scss index a342938c89a6..294bb2ffc202 100644 --- a/packages/devextreme-scss/scss/widgets/generic/tagBox/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/generic/tagBox/_colors.scss @@ -23,7 +23,6 @@ $tagbox-tag-button-remove-bg: null !default; $tagbox-tag-button-remove-active-bg: null !default; $tagbox-tag-focused-bg: null !default; $tagbox-tag-focused-color: null !default; -$tagbox-tag-focused-button-remove-bg: null !default; $tagbox-select-all-border-color: $base-border-color !default; @if $color == "carmine" { @@ -33,7 +32,6 @@ $tagbox-select-all-border-color: $base-border-color !default; $tagbox-tag-button-remove-active-bg: color.adjust($tagbox-tag-button-remove-bg, $lightness: 20%, $space: hsl) !default; $tagbox-tag-focused-bg: color.adjust($tagbox-tag-bg, $lightness: -7%, $space: hsl) !default; $tagbox-tag-focused-color: $tagbox-tag-color !default; - $tagbox-tag-focused-button-remove-bg: $tagbox-tag-button-remove-bg !default; } @if $color == "contrast" { @@ -43,7 +41,6 @@ $tagbox-select-all-border-color: $base-border-color !default; $tagbox-tag-button-remove-active-bg: color.adjust($tagbox-tag-button-remove-bg, $lightness: 20%, $space: hsl) !default; $tagbox-tag-focused-bg: $base-focus-color !default; $tagbox-tag-focused-color: $base-text-color !default; - $tagbox-tag-focused-button-remove-bg: $tagbox-tag-button-remove-bg !default; } @if $color == "dark" { @@ -53,7 +50,6 @@ $tagbox-select-all-border-color: $base-border-color !default; $tagbox-tag-button-remove-active-bg: color.adjust($tagbox-tag-button-remove-bg, $lightness: -20%, $space: hsl) !default; $tagbox-tag-focused-bg: color.adjust($tagbox-tag-bg, $lightness: -7.3%, $space: hsl) !default; $tagbox-tag-focused-color: $tagbox-tag-color !default; - $tagbox-tag-focused-button-remove-bg: $tagbox-tag-button-remove-bg !default; } @if $color == "darkmoon" { @@ -63,7 +59,6 @@ $tagbox-select-all-border-color: $base-border-color !default; $tagbox-tag-button-remove-active-bg: color.adjust($tagbox-tag-button-remove-bg, $lightness: -20%, $space: hsl) !default; $tagbox-tag-focused-bg: color.adjust($tagbox-tag-bg, $lightness: -7.3%, $space: hsl) !default; $tagbox-tag-focused-color: $tagbox-tag-color !default; - $tagbox-tag-focused-button-remove-bg: $tagbox-tag-button-remove-bg !default; } @if $color == "darkviolet" { @@ -73,7 +68,6 @@ $tagbox-select-all-border-color: $base-border-color !default; $tagbox-tag-button-remove-active-bg: color.adjust($tagbox-tag-button-remove-bg, $lightness: -20%, $space: hsl) !default; $tagbox-tag-focused-bg: color.adjust($tagbox-tag-bg, $lightness: -7.3%, $space: hsl) !default; $tagbox-tag-focused-color: $tagbox-tag-color !default; - $tagbox-tag-focused-button-remove-bg: $tagbox-tag-button-remove-bg !default; } @if $color == "greenmist" { @@ -83,7 +77,6 @@ $tagbox-select-all-border-color: $base-border-color !default; $tagbox-tag-button-remove-active-bg: color.adjust($tagbox-tag-button-remove-bg, $lightness: 20%, $space: hsl) !default; $tagbox-tag-focused-bg: color.adjust($tagbox-tag-bg, $lightness: -7%, $space: hsl) !default; $tagbox-tag-focused-color: $tagbox-tag-color !default; - $tagbox-tag-focused-button-remove-bg: $tagbox-tag-button-remove-bg !default; } @if $color == "light" { @@ -93,7 +86,6 @@ $tagbox-select-all-border-color: $base-border-color !default; $tagbox-tag-button-remove-active-bg: color.adjust($tagbox-tag-button-remove-bg, $lightness: 20%, $space: hsl) !default; $tagbox-tag-focused-bg: color.adjust($tagbox-tag-bg, $lightness: -7%, $space: hsl) !default; $tagbox-tag-focused-color: $tagbox-tag-color !default; - $tagbox-tag-focused-button-remove-bg: $tagbox-tag-button-remove-bg !default; } @if $color == "softblue" { @@ -103,5 +95,6 @@ $tagbox-select-all-border-color: $base-border-color !default; $tagbox-tag-button-remove-active-bg: color.adjust($tagbox-tag-button-remove-bg, $lightness: 20%, $space: hsl) !default; $tagbox-tag-focused-bg: color.adjust($tagbox-tag-bg, $lightness: -7%, $space: hsl) !default; $tagbox-tag-focused-color: $tagbox-tag-color !default; - $tagbox-tag-focused-button-remove-bg: $tagbox-tag-button-remove-bg !default; } + +$tagbox-tag-focused-button-remove-bg: $tagbox-tag-button-remove-bg !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/textEditor/_colors.scss b/packages/devextreme-scss/scss/widgets/generic/textEditor/_colors.scss index f6c080b13de3..79a778f4d217 100644 --- a/packages/devextreme-scss/scss/widgets/generic/textEditor/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/generic/textEditor/_colors.scss @@ -54,7 +54,7 @@ $texteditor-readonly-border-color: $base-border-color !default; $texteditor-label-transition: font-size 0.2s cubic-bezier(0, 0, 0.2, 1) 0ms, transform 0.2s cubic-bezier(0, 0, 0.2, 1) 0ms, - top 0.2s cubic-bezier(0, 0, 0.2, 1) 0ms; + top 0.2s cubic-bezier(0, 0, 0.2, 1) 0ms !default; @if $color == "carmine" { $texteditor-placeholder-color: $base-label-color !default; @@ -128,7 +128,7 @@ $texteditor-label-transition: $texteditor-filled-invalid-background: color.change($texteditor-invalid-faded-border-color, $alpha: 0.1) !default; } -$texteditor-label-color: $texteditor-placeholder-color; +$texteditor-label-color: $texteditor-placeholder-color !default; :root { --dx-texteditor-color-text: #{$texteditor-color}; diff --git a/packages/devextreme-scss/scss/widgets/generic/textEditor/_index.scss b/packages/devextreme-scss/scss/widgets/generic/textEditor/_index.scss index 67a56708f324..d3b4dc45bebc 100644 --- a/packages/devextreme-scss/scss/widgets/generic/textEditor/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/textEditor/_index.scss @@ -13,21 +13,21 @@ @use "../../base/textEditor/mixins" as *; @use "../colorBox/sizes" as *; -$generic-texteditor-border-width: $generic-base-border-width; +$generic-texteditor-border-width: $generic-base-border-width !default; $generic-texteditor-input-padding: $generic-base-inline-borderedwidget-top-padding $generic-base-inline-borderedwidget-horizontal-padding - $generic-base-inline-borderedwidget-bottom-padding; + $generic-base-inline-borderedwidget-bottom-padding !default; -$generic-texteditor-input-height: math.round($generic-base-line-height * $generic-base-font-size) + $generic-base-inline-borderedwidget-top-padding + $generic-base-inline-borderedwidget-bottom-padding; +$generic-texteditor-input-height: math.round($generic-base-line-height * $generic-base-font-size) + $generic-base-inline-borderedwidget-top-padding + $generic-base-inline-borderedwidget-bottom-padding !default; -$generic-texteditor-icon-container-size: $generic-base-inline-borderedwidget-inner-size; -$generic-texteditor-icon-size: $generic-base-icon-size; +$generic-texteditor-icon-container-size: $generic-base-inline-borderedwidget-inner-size !default; +$generic-texteditor-icon-size: $generic-base-icon-size !default; -$generic-texteditor-clear-icon-size: $generic-texteditor-icon-size + $generic-texteditor-clear-icon-oversize; +$generic-texteditor-clear-icon-size: $generic-texteditor-icon-size + $generic-texteditor-clear-icon-oversize !default; -$generic-texteditor-custom-button-margin: 1px; -$generic-texteditor-invalid-badge-size: $generic-invalid-badge-size + 2 * $generic-invalid-badge-horizontal-padding; +$generic-texteditor-custom-button-margin: 1px !default; +$generic-texteditor-invalid-badge-size: $generic-invalid-badge-size + 2 * $generic-invalid-badge-horizontal-padding !default; @mixin dx-texteditor-icon() { position: absolute; diff --git a/packages/devextreme-scss/scss/widgets/generic/textEditor/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/textEditor/_sizes.scss index ebbe32e60ef1..6520341bb9fe 100644 --- a/packages/devextreme-scss/scss/widgets/generic/textEditor/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/generic/textEditor/_sizes.scss @@ -7,7 +7,7 @@ $generic-texteditor-label-outside-font-size: null !default; $generic-texteditor-label-outside-height: null !default; $generic-texteditor-label-outside-top-offset: null !default; $generic-texteditor-button-icononly-bottom-padding: null !default; -$generic-texteditor-button-icononly-horizontal-padding: null !default; +$generic-texteditor-button-icononly-horizontal-padding: $generic-button-icon-horizontal-padding - 2px !default; $generic-texteditor-button-top-padding: $generic-base-inline-widget-top-padding - $generic-button-border-weight - 2px !default; $generic-texteditor-button-bottom-padding: $generic-base-inline-widget-bottom-padding - $generic-button-border-weight - 2px !default; @@ -26,7 +26,6 @@ $generic-texteditor-underlined-input-with-label-bottom-padding: $generic-base-in $generic-texteditor-label-outside-height: 20px !default; $generic-texteditor-label-outside-top-offset: 24px !default; $generic-texteditor-button-icononly-bottom-padding: $generic-texteditor-button-bottom-padding !default; - $generic-texteditor-button-icononly-horizontal-padding: $generic-button-icon-horizontal-padding - 2px !default; } @else if $size == "compact" { @@ -36,5 +35,4 @@ $generic-texteditor-underlined-input-with-label-bottom-padding: $generic-base-in $generic-texteditor-label-outside-height: 16px !default; $generic-texteditor-label-outside-top-offset: 18px !default; $generic-texteditor-button-icononly-bottom-padding: $generic-texteditor-button-bottom-padding + 1px !default; - $generic-texteditor-button-icononly-horizontal-padding: $generic-button-icon-horizontal-padding - 2px !default; } diff --git a/packages/devextreme-scss/scss/widgets/generic/toast/_colors.scss b/packages/devextreme-scss/scss/widgets/generic/toast/_colors.scss index 4c1601dec53a..9ffbc3ea7ffa 100644 --- a/packages/devextreme-scss/scss/widgets/generic/toast/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/generic/toast/_colors.scss @@ -32,79 +32,48 @@ $toast-error-bg: $base-danger !default; * $type color */ $toast-success-bg: $base-success !default; -$toast-info-icon-image-bg: null !default; -$toast-warning-icon-image-bg: null !default; -$toast-error-icon-image-bg: null !default; -$toast-success-icon-image-bg: null !default; + +$toast-info-icon-image-bg: data-uri("images/widgets/generic/toast/info.png") !default; +$toast-warning-icon-image-bg: data-uri("images/widgets/generic/toast/warning.png") !default; +$toast-error-icon-image-bg: data-uri("images/widgets/generic/toast/error.png") !default; +$toast-success-icon-image-bg: data-uri("images/widgets/generic/toast/success.png") !default; @if $color == "carmine" { $toast-shadow-color: transparent !default; $toast-info-bg: $base-accent !default; - $toast-info-icon-image-bg: data-uri("images/widgets/generic/color-schemes/light/toast/info.png") !default; - $toast-warning-icon-image-bg: data-uri("images/widgets/generic/color-schemes/light/toast/warning.png") !default; - $toast-error-icon-image-bg: data-uri("images/widgets/generic/color-schemes/light/toast/error.png") !default; - $toast-success-icon-image-bg: data-uri("images/widgets/generic/color-schemes/light/toast/success.png") !default; } @if $color == "contrast" { $toast-shadow-color: $base-shadow-color !default; $toast-info-bg: $base-info !default; - $toast-info-icon-image-bg: data-uri("images/widgets/generic/color-schemes/light/toast/info.png") !default; - $toast-warning-icon-image-bg: data-uri("images/widgets/generic/color-schemes/light/toast/warning.png") !default; - $toast-error-icon-image-bg: data-uri("images/widgets/generic/color-schemes/light/toast/error.png") !default; - $toast-success-icon-image-bg: data-uri("images/widgets/generic/color-schemes/light/toast/success.png") !default; } @if $color == "dark" { $toast-shadow-color: transparent !default; $toast-info-bg: $base-accent !default; - $toast-info-icon-image-bg: data-uri("images/widgets/generic/color-schemes/dark/toast/info.png") !default; - $toast-warning-icon-image-bg: data-uri("images/widgets/generic/color-schemes/dark/toast/warning.png") !default; - $toast-error-icon-image-bg: data-uri("images/widgets/generic/color-schemes/dark/toast/error.png") !default; - $toast-success-icon-image-bg: data-uri("images/widgets/generic/color-schemes/dark/toast/success.png") !default; } @if $color == "darkmoon" { $toast-shadow-color: transparent !default; $toast-info-bg: $base-accent !default; - $toast-info-icon-image-bg: data-uri("images/widgets/generic/color-schemes/dark/toast/info.png") !default; - $toast-warning-icon-image-bg: data-uri("images/widgets/generic/color-schemes/dark/toast/warning.png") !default; - $toast-error-icon-image-bg: data-uri("images/widgets/generic/color-schemes/dark/toast/error.png") !default; - $toast-success-icon-image-bg: data-uri("images/widgets/generic/color-schemes/dark/toast/success.png") !default; } @if $color == "darkviolet" { $toast-shadow-color: transparent !default; $toast-info-bg: $base-accent !default; - $toast-info-icon-image-bg: data-uri("images/widgets/generic/color-schemes/dark/toast/info.png") !default; - $toast-warning-icon-image-bg: data-uri("images/widgets/generic/color-schemes/dark/toast/warning.png") !default; - $toast-error-icon-image-bg: data-uri("images/widgets/generic/color-schemes/dark/toast/error.png") !default; - $toast-success-icon-image-bg: data-uri("images/widgets/generic/color-schemes/dark/toast/success.png") !default; } @if $color == "greenmist" { $toast-shadow-color: transparent !default; $toast-info-bg: $base-accent !default; - $toast-info-icon-image-bg: data-uri("images/widgets/generic/color-schemes/light/toast/info.png") !default; - $toast-warning-icon-image-bg: data-uri("images/widgets/generic/color-schemes/light/toast/warning.png") !default; - $toast-error-icon-image-bg: data-uri("images/widgets/generic/color-schemes/light/toast/error.png") !default; - $toast-success-icon-image-bg: data-uri("images/widgets/generic/color-schemes/light/toast/success.png") !default; } @if $color == "light" { $toast-shadow-color: transparent !default; $toast-info-bg: $base-accent !default; - $toast-info-icon-image-bg: data-uri("images/widgets/generic/color-schemes/light/toast/info.png") !default; - $toast-warning-icon-image-bg: data-uri("images/widgets/generic/color-schemes/light/toast/warning.png") !default; - $toast-error-icon-image-bg: data-uri("images/widgets/generic/color-schemes/light/toast/error.png") !default; - $toast-success-icon-image-bg: data-uri("images/widgets/generic/color-schemes/light/toast/success.png") !default; } @if $color == "softblue" { $toast-shadow-color: transparent !default; $toast-info-bg: $base-accent !default; - $toast-info-icon-image-bg: data-uri("images/widgets/generic/color-schemes/light/toast/info.png") !default; - $toast-warning-icon-image-bg: data-uri("images/widgets/generic/color-schemes/light/toast/warning.png") !default; - $toast-error-icon-image-bg: data-uri("images/widgets/generic/color-schemes/light/toast/error.png") !default; - $toast-success-icon-image-bg: data-uri("images/widgets/generic/color-schemes/light/toast/success.png") !default; } diff --git a/packages/devextreme-scss/scss/widgets/generic/toast/_index.scss b/packages/devextreme-scss/scss/widgets/generic/toast/_index.scss index 3be2e70bcbaf..8ae3fbf94cd5 100644 --- a/packages/devextreme-scss/scss/widgets/generic/toast/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/toast/_index.scss @@ -4,7 +4,7 @@ @use "../../base/toast"; @use "../overlay"; -$generic-toast-shadow: 0 2px 3px 0 $toast-shadow-color; +$generic-toast-shadow: 0 2px 3px 0 $toast-shadow-color !default; .dx-toast-content { color: $toast-color; diff --git a/packages/devextreme-scss/scss/widgets/generic/treeList/_colors.scss b/packages/devextreme-scss/scss/widgets/generic/treeList/_colors.scss index 86c52ae07a9f..b46d475d6eeb 100644 --- a/packages/devextreme-scss/scss/widgets/generic/treeList/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/generic/treeList/_colors.scss @@ -2,45 +2,37 @@ @use "../colors" as *; @use "../gridBase/colors" as *; -$treelist-spin-icon-color: null !default; +$treelist-spin-icon-color: $datagrid-spin-icon-color !default; $treelist-selection-spin-icon-color: null !default; @if $color == "carmine" { - $treelist-spin-icon-color: $datagrid-spin-icon-color !default; $treelist-selection-spin-icon-color: color.adjust($datagrid-spin-icon-color, $lightness: 100%, $space: hsl) !default; } @if $color == "contrast" { - $treelist-spin-icon-color: $datagrid-spin-icon-color !default; $treelist-selection-spin-icon-color: $base-inverted-text-color !default; } @if $color == "dark" { - $treelist-spin-icon-color: $datagrid-spin-icon-color !default; $treelist-selection-spin-icon-color: $treelist-spin-icon-color !default; } @if $color == "darkmoon" { - $treelist-spin-icon-color: $datagrid-spin-icon-color !default; $treelist-selection-spin-icon-color: $base-bg !default; } @if $color == "darkviolet" { - $treelist-spin-icon-color: $datagrid-spin-icon-color !default; $treelist-selection-spin-icon-color: $base-icon-color !default; } @if $color == "greenmist" { - $treelist-spin-icon-color: $datagrid-spin-icon-color !default; $treelist-selection-spin-icon-color: color.adjust($treelist-spin-icon-color, $lightness: 100%, $space: hsl) !default; } @if $color == "light" { - $treelist-spin-icon-color: $datagrid-spin-icon-color !default; $treelist-selection-spin-icon-color: $treelist-spin-icon-color !default; } @if $color == "softblue" { - $treelist-spin-icon-color: $datagrid-spin-icon-color !default; $treelist-selection-spin-icon-color: $base-icon-color !default; } diff --git a/packages/devextreme-scss/scss/widgets/generic/treeView/_colors.scss b/packages/devextreme-scss/scss/widgets/generic/treeView/_colors.scss index 1a618a1088fe..b69ae56ce488 100644 --- a/packages/devextreme-scss/scss/widgets/generic/treeView/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/generic/treeView/_colors.scss @@ -29,7 +29,6 @@ $treeview-border-color: $base-border-color !default; */ $treeview-item-selected-color: null !default; $treeview-item-selected-bg: null !default; -$treeview-item-selected-hover-bg: null !default; $treeview-item-content-disabled: null !default; /** @@ -38,7 +37,7 @@ $treeview-item-content-disabled: null !default; */ $treeview-spin-icon-color: null !default; -$generic-treeview-focused-row-load-indicator-color: #fff; +$generic-treeview-focused-row-load-indicator-color: #fff !default; @if $color == "carmine" { $treeview-color: $base-text-color !default; @@ -48,7 +47,6 @@ $generic-treeview-focused-row-load-indicator-color: #fff; $treeview-item-hover-color: $base-hover-color !default; $treeview-item-selected-color: $treeview-color !default; $treeview-item-selected-bg: color.change($base-accent, $alpha: 0.48) !default; - $treeview-item-selected-hover-bg: color.change($base-accent, $alpha: 0.3) !default; $treeview-spin-icon-color: $base-header-color !default; $treeview-item-content-disabled: color.adjust($base-text-color, $lightness: 40%, $space: hsl) !default; } @@ -60,7 +58,6 @@ $generic-treeview-focused-row-load-indicator-color: #fff; $treeview-item-hover-color: $base-text-color !default; $treeview-item-selected-color: $base-inverted-text-color !default; $treeview-item-selected-bg: $base-inverted-bg !default; - $treeview-item-selected-hover-bg: $base-inverted-bg !default; $treeview-spin-icon-color: $base-icon-color !default; $treeview-item-content-disabled: color.adjust($base-text-color, $lightness: -48.24%, $space: hsl) !default; } @@ -73,7 +70,6 @@ $generic-treeview-focused-row-load-indicator-color: #fff; $treeview-item-hover-color: $base-hover-color !default; $treeview-item-selected-color: $treeview-color !default; $treeview-item-selected-bg: color.change(#fff, $alpha: 0.12) !default; - $treeview-item-selected-hover-bg: color.change(#fff, $alpha: 0.09) !default; $treeview-spin-icon-color: $base-icon-color !default; $treeview-item-content-disabled: color.change($base-text-color, $lightness: 51.8%, $space: hsl) !default; } @@ -86,7 +82,6 @@ $generic-treeview-focused-row-load-indicator-color: #fff; $treeview-item-hover-color: $treeview-color !default; $treeview-item-selected-color: $treeview-color !default; $treeview-item-selected-bg: color.change(#748fbc, $alpha: 0.42) !default; - $treeview-item-selected-hover-bg: color.change(#748fbc, $alpha: 0.3) !default; $treeview-spin-icon-color: $base-icon-color !default; $treeview-item-content-disabled: color.adjust($base-text-color, $lightness: -48.24%, $space: hsl) !default; } @@ -99,7 +94,6 @@ $generic-treeview-focused-row-load-indicator-color: #fff; $treeview-item-hover-color: $base-hover-color !default; $treeview-item-selected-color: $treeview-color !default; $treeview-item-selected-bg: color.change($base-accent, $alpha: 0.46) !default; - $treeview-item-selected-hover-bg: color.change($base-accent, $alpha: 0.6) !default; $treeview-spin-icon-color: $base-header-color !default; $treeview-item-content-disabled: color.change($base-text-color, $saturation: 0%, $lightness: 51.76%, $space: hsl) !default; } @@ -112,7 +106,6 @@ $generic-treeview-focused-row-load-indicator-color: #fff; $treeview-item-hover-color: $base-hover-color !default; $treeview-item-selected-color: $treeview-color !default; $treeview-item-selected-bg: color.change(#000, $alpha: 0.1) !default; - $treeview-item-selected-hover-bg: color.change(#000, $alpha: 0.2) !default; $treeview-spin-icon-color: $base-icon-color !default; $treeview-item-content-disabled: color.change($base-text-color, $saturation: 0%, $lightness: 60%, $space: hsl) !default; } @@ -125,7 +118,6 @@ $generic-treeview-focused-row-load-indicator-color: #fff; $treeview-item-hover-color: $treeview-color !default; $treeview-item-selected-color: $treeview-color !default; $treeview-item-selected-bg: color.change(#000, $alpha: 0.1) !default; - $treeview-item-selected-hover-bg: color.change(#000, $alpha: 0.07) !default; $treeview-spin-icon-color: $base-icon-color !default; $treeview-item-content-disabled: color.adjust($base-text-color, $lightness: 40%, $space: hsl) !default; } @@ -138,7 +130,6 @@ $generic-treeview-focused-row-load-indicator-color: #fff; $treeview-item-hover-color: $treeview-color !default; $treeview-item-selected-color: $treeview-color !default; $treeview-item-selected-bg: color.change(#000, $alpha: 0.1) !default; - $treeview-item-selected-hover-bg: color.change(#000, $alpha: 0.15) !default; $treeview-spin-icon-color: $base-icon-color !default; $treeview-item-content-disabled: color.adjust($base-text-color, $lightness: 40%, $space: hsl) !default; } diff --git a/packages/devextreme-scss/scss/widgets/generic/treeView/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/treeView/_sizes.scss index e24765ccb446..aa86eaa7255c 100644 --- a/packages/devextreme-scss/scss/widgets/generic/treeView/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/generic/treeView/_sizes.scss @@ -3,10 +3,8 @@ $generic-treeview-container-with-border-offset: null !default; $generic-treeview-base-container-size: null !default; $generic-treeview-toggle-item-width: null !default; -$generic-treeview-item-height: null !default; $generic-select-all-item-padding: null !default; $generic-treeview-item-padding: null !default; -$generic-treeview-load-indicator-size: null !default; $generic-checkbox-container-size: null !default; $generic-treeview-searchbox-margin-bottom: null !default; $generic-treeview-border-padding: 1px !default; @@ -14,29 +12,27 @@ $generic-treeview-item-element-gap: 8px !default; $generic-select-all-item-border-visible-padding: 20px !default; @if $size == "default" { - $generic-treeview-item-height: 32px; $generic-treeview-base-container-size: 22px !default; $generic-treeview-container-with-border-offset: 7px !default; $generic-checkbox-container-size: 22px !default; $generic-treeview-toggle-item-width: 22px !default; $generic-select-all-item-padding: 9px 40px 11px 40px !default; $generic-treeview-item-padding: 5px 10px !default; - $generic-treeview-load-indicator-size: $generic-treeview-toggle-item-width !default; $generic-treeview-searchbox-margin-bottom: 5px !default; } @else if $size == "compact" { - $generic-treeview-item-height: 28px; $generic-treeview-container-with-border-offset: 6px !default; $generic-treeview-base-container-size: 16px !default; $generic-checkbox-container-size: 16px !default; $generic-treeview-toggle-item-width: 16px !default; $generic-select-all-item-padding: 6px 30px 8px 30px !default; $generic-treeview-item-padding: 4px 6px !default; - $generic-treeview-load-indicator-size: $generic-treeview-toggle-item-width !default; $generic-treeview-searchbox-margin-bottom: 3px !default; } +$generic-treeview-load-indicator-size: $generic-treeview-toggle-item-width !default; + $generic-treeview-container-padding: $generic-treeview-border-padding $generic-treeview-border-padding diff --git a/packages/devextreme-scss/scss/widgets/material/_colors.scss b/packages/devextreme-scss/scss/widgets/material/_colors.scss index 6b8686ed2905..1a27a389588a 100644 --- a/packages/devextreme-scss/scss/widgets/material/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/material/_colors.scss @@ -31,18 +31,6 @@ $base-text-color: null !default; */ $base-label-color: null !default; -/** -* $name 40. Icon color -* $type color -*/ -$base-icon-color: null !default; - -/** -* $name 50. Arrow icon color -* $type color -*/ -$base-spin-icon-color: null !default; - /** * $name 30. Background color * $type color @@ -55,7 +43,6 @@ $base-bg: null !default; */ $base-border-color: null !default; $base-inverted-bg: null !default; -$base-element-bg: null !default; $base-shadow-color: null !default; /** @@ -76,12 +63,6 @@ $base-warning: null !default; */ $base-danger: null !default; -/** -* $name 80. Hover state text color -* $type color -*/ -$base-hover-color: null !default; - /** * $name 90. Hover state background color * $type color @@ -89,22 +70,14 @@ $base-hover-color: null !default; $base-hover-bg: null !default; $base-inverted-text-color: null !default; -/** -* $name 100. Focused state text color -* $type color -*/ -$base-focus-color: null !default; - /** * $name 110. Focused state background color * $type color */ $base-focus-bg: null !default; -$base-inverted-icon-color: null !default; $base-disabled-opacity: 0.38 !default; $base-disabled-color: null !default; $base-dropdown-shadow-color: null !default; -$scrollview-pulldown-path: null !default; @if $color == "blue" { $material-color-accent-modificator: "blue" !default; @@ -155,22 +128,14 @@ $scrollview-pulldown-path: null !default; $base-text-color: rgba(0, 0, 0, 0.87) !default; $base-label-color: rgba(0, 0, 0, 0.6) !default; $base-disabled-color: rgba(0, 0, 0, $base-disabled-opacity) !default; - $base-icon-color: color.change($base-text-color, $alpha: 0.6) !default; - $base-spin-icon-color: color.change($base-text-color, $alpha: 0.6) !default; $base-bg: #fff !default; $base-border-color: #e0e0e0 !default; $base-inverted-bg: color.adjust($base-bg, $lightness: -100%, $space: hsl) !default; - $base-element-bg: $base-bg !default; $base-shadow-color: $base-inverted-bg !default; - $base-hover-color: $base-text-color !default; $base-hover-bg: color.change(#000, $alpha: 0.04) !default; $base-inverted-text-color: #fff !default; - $base-focus-color: $base-inverted-text-color !default; $base-focus-bg: color.change(#000, $alpha: 0.04) !default; - $base-inverted-icon-color: $base-inverted-text-color !default; $base-dropdown-shadow-color: color.change($base-shadow-color, $alpha: 0.2) !default; - - $scrollview-pulldown-path: data-uri("images/widgets/material/color-schemes/light/pulldown.png") !default; } @if $mode == "dark" { @@ -185,24 +150,42 @@ $scrollview-pulldown-path: null !default; $base-text-color: #fff !default; $base-label-color: rgba(255, 255, 255, 0.6) !default; $base-disabled-color: rgba(255, 255, 255, $base-disabled-opacity) !default; - $base-icon-color: color.change($base-text-color, $alpha: 0.6) !default; - $base-spin-icon-color: color.change($base-text-color, $alpha: 0.6) !default; $base-bg: #292929 !default; $base-border-color: #515159 !default; $base-inverted-bg: color.adjust($base-bg, $lightness: 100%, $space: hsl) !default; - $base-element-bg: $base-bg !default; $base-shadow-color: #000 !default; - $base-hover-color: $base-text-color !default; $base-hover-bg: color.change(#fff, $alpha: 0.04) !default; $base-inverted-text-color: rgba(0, 0, 0, 0.87) !default; - $base-focus-color: $base-inverted-text-color !default; $base-focus-bg: color.change(#fff, $alpha: 0.04) !default; - $base-inverted-icon-color: $base-inverted-text-color !default; $base-dropdown-shadow-color: color.change($base-shadow-color, $alpha: 0.4) !default; - - $scrollview-pulldown-path: data-uri("images/widgets/material/color-schemes/dark/pulldown.png") !default; } +/** +* $name 40. Icon color +* $type color +*/ +$base-icon-color: color.change($base-text-color, $alpha: 0.6) !default; + +/** +* $name 50. Arrow icon color +* $type color +*/ +$base-spin-icon-color: color.change($base-text-color, $alpha: 0.6) !default; +$base-element-bg: $base-bg !default; + +/** +* $name 80. Hover state text color +* $type color +*/ +$base-hover-color: $base-text-color !default; + +/** +* $name 100. Focused state text color +* $type color +*/ +$base-focus-color: $base-inverted-text-color !default; +$base-inverted-icon-color: $base-inverted-text-color !default; + /** * $name 30. Link color * $type color diff --git a/packages/devextreme-scss/scss/widgets/material/actionSheet/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/actionSheet/_sizes.scss index 01d93773d6c3..14a898935fca 100644 --- a/packages/devextreme-scss/scss/widgets/material/actionSheet/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/actionSheet/_sizes.scss @@ -1,2 +1,2 @@ -$material-actionsheet-item-margin-value: 20px; +$material-actionsheet-item-margin-value: 20px !default; $material-actionsheet-item-margin: 0 0 $material-actionsheet-item-margin-value 0 !default; diff --git a/packages/devextreme-scss/scss/widgets/material/button/_colors.scss b/packages/devextreme-scss/scss/widgets/material/button/_colors.scss index d14a977b8059..8e2218b4706a 100644 --- a/packages/devextreme-scss/scss/widgets/material/button/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/material/button/_colors.scss @@ -13,7 +13,7 @@ $button-shadow-color: null !default; * $type color */ $button-normal-color: $base-text-color !default; -$button-normal-border-color: color.change($button-normal-color, $alpha: 0.24); +$button-normal-border-color: color.change($button-normal-color, $alpha: 0.24) !default; /** * $name 20. Selected state text color diff --git a/packages/devextreme-scss/scss/widgets/material/button/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/button/_sizes.scss index 7de9889ee26f..88e97b8ff4a4 100644 --- a/packages/devextreme-scss/scss/widgets/material/button/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/button/_sizes.scss @@ -2,7 +2,6 @@ $material-button-horizontal-padding: null !default; $material-button-vertical-padding: null !default; -$material-button-icon-padding: null !default; $material-button-icon-text-padding: null !default; $material-button-min-width: null !default; @@ -19,16 +18,14 @@ $material-button-active-box-shadow-size: 0 4px 6px !default; @if $size == "default" { $material-button-horizontal-padding: 16px !default; $material-button-vertical-padding: 6px !default; - $material-button-icon-padding: 8px !default; - $material-button-icon-text-padding: 12px; + $material-button-icon-text-padding: 12px !default; $material-button-min-width: 64px !default; } @else if $size == "compact" { $material-button-horizontal-padding: 12px !default; $material-button-vertical-padding: 5px !default; - $material-button-icon-padding: 5px !default; - $material-button-icon-text-padding: 8px; + $material-button-icon-text-padding: 8px !default; $material-button-min-width: 48px !default; } diff --git a/packages/devextreme-scss/scss/widgets/material/cardView/_colors.scss b/packages/devextreme-scss/scss/widgets/material/cardView/_colors.scss index f5b547710de0..3f3c19fa0907 100644 --- a/packages/devextreme-scss/scss/widgets/material/cardView/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/material/cardView/_colors.scss @@ -19,19 +19,19 @@ * $name 100. Background color * $type color */ -$cardview-material-common-bg-color: null !default; +$cardview-material-common-bg-color: $base-bg !default; /** * $name 102. Link color * $type color */ -$cardview-material-common-link-color: null !default; +$cardview-material-common-link-color: $base-link-color !default; /** * $name 103. Text color * $type color */ -$cardview-material-common-text-color: null !default; +$cardview-material-common-text-color: $base-text-color !default; // #endregion @@ -41,31 +41,31 @@ $cardview-material-common-text-color: null !default; * $name 200. Background color * $type color */ -$cardview-material-card-bg-color: null !default; +$cardview-material-card-bg-color: $base-bg !default; /** * $name 201. Border color * $type color */ -$cardview-material-card-border-color: null !default; +$cardview-material-card-border-color: $base-border-color !default; /** * $name 203. Divider color * $type color */ -$cardview-material-card-divider-color: null !default; +$cardview-material-card-divider-color: $cardview-material-card-border-color !default; /** * $name 204. Text color * $type color */ -$cardview-material-card-text-color: null !default; +$cardview-material-card-text-color: $cardview-material-common-text-color !default; /** * $name 206. Cover background color * $type color */ -$cardview-material-card__cover-bg-color: null !default; +$cardview-material-card__cover-bg-color: #FFF !default; /** * $name 207. "Empty cover" background color @@ -77,9 +77,9 @@ $cardview-material-card__cover--noimage-bg-color: null !default; * $name 208. "Empty cover" icon color * $type color */ -$cardview-material-card__cover--noimage__icon-color: null !default; +$cardview-material-card__cover--noimage__icon-color: color.change($cardview-material-common-text-color, $alpha: 0.38) !default; -$cardview-material-card--focused-outline-color: null !default; +$cardview-material-card--focused-outline-color: $base-accent !default; // #endregion @@ -97,35 +97,23 @@ $cardview-material-card--selected-bg-color: null !default; */ $cardview-material-card--selected-border-color: null !default; -/** -* $name 302. Divider color -* $type color -*/ -$cardview-material-card--selected-divider-color: null !default; - /** * $name 303. Text color * $type color */ -$cardview-material-card--selected-text-color: null !default; +$cardview-material-card--selected-text-color: $cardview-material-card-text-color !default; /** * $name 305. Cover background color * $type color */ -$cardview-material-card--selected__cover-bg-color: null !default; - -/** -* $name 306. "Empty cover" background color -* $type color -*/ -$cardview-material-card--selected__cover--noimage-bg-color: null !default; +$cardview-material-card--selected__cover-bg-color: $cardview-material-card__cover-bg-color !default; /** * $name 307. "Empty cover" icon color * $type color */ -$cardview-material-card--selected__cover--noimage__icon-color: null !default; +$cardview-material-card--selected__cover--noimage__icon-color: $cardview-material-card__cover--noimage__icon-color !default; // #endregion @@ -137,37 +125,31 @@ $cardview-material-card--selected__cover--noimage__icon-color: null !default; */ $cardview-material-header-panel__item-bg-color: null !default; -/** -* $name 401. Border color -* $type color -*/ -$cardview-material-header-panel__item-border-color: null !default; - /** * $name 403. Text color * $type color */ -$cardview-material-header-panel__item-text-color: null !default; +$cardview-material-header-panel__item-text-color: $cardview-material-common-text-color !default; /** * $name 405. Icon color * $type color */ -$cardview-material-header-panel__item__icon-color: null !default; +$cardview-material-header-panel__item__icon-color: $cardview-material-common-text-color !default; /** * $name 406. Active "Filter" icon color * $type color */ -$cardview-material-header-panel__item__header-filter-icon--selected-color: null !default; +$cardview-material-header-panel__item__header-filter-icon--selected-color: $base-accent !default; /** * $name 407. Sort index text color * $type color */ -$cardview-material-header-panel__item__sort-index-text-color: null !default; +$cardview-material-header-panel__item__sort-index-text-color: $cardview-material-common-text-color !default; -$cardview-material-header-panel__item--focused-outline-color: null !default; +$cardview-material-header-panel__item--focused-outline-color: $base-accent !default; // #endregion @@ -179,103 +161,73 @@ $cardview-material-header-panel__item--focused-outline-color: null !default; */ $cardview-material-header-panel__item--hovered-bg-color: null !default; -/** -* $name 501. Border color -* $type color -*/ -$cardview-material-header-panel__item--hovered-border-color: null !default; - /** * $name 502. Text color * $type color */ -$cardview-material-header-panel__item--hovered-text-color: null !default; +$cardview-material-header-panel__item--hovered-text-color: $cardview-material-header-panel__item-text-color !default; /** * $name 504. Icon color * $type color */ -$cardview-material-header-panel__item--hovered__icon-color: null !default; +$cardview-material-header-panel__item--hovered__icon-color: $cardview-material-header-panel__item__icon-color !default; /** * $name 505. Active "Filter" icon color * $type color */ -$cardview-material-header-panel__item--hovered__header-filter-icon--selected-color: null !default; +$cardview-material-header-panel__item--hovered__header-filter-icon--selected-color: $cardview-material-header-panel__item__header-filter-icon--selected-color !default; /** * $name 506. Sort index text color * $type color */ -$cardview-material-header-panel__item--hovered__sort-index-text-color: null !default; +$cardview-material-header-panel__item--hovered__sort-index-text-color: $cardview-material-header-panel__item__sort-index-text-color !default; // #endregion // #region HEADER PANEL ITEM (DISABLED) -/** -* $name 600. Background color -* $type color -*/ -$cardview-material-header-panel__item--disabled-bg-color: null !default; - -/** -* $name 601. Border color -* $type color -*/ -$cardview-material-header-panel__item--disabled-border-color: null !default; - /** * $name 602. Text color * $type color */ -$cardview-material-header-panel__item--disabled-text-color: null !default; +$cardview-material-header-panel__item--disabled-text-color: color.change($cardview-material-header-panel__item-text-color, $alpha: 0.38) !default; /** * $name 604. Icon color * $type color */ -$cardview-material-header-panel__item--disabled__icon-color: null !default; +$cardview-material-header-panel__item--disabled__icon-color: color.change($cardview-material-header-panel__item__icon-color, $alpha: 0.38) !default; /** * $name 605. Active "Filter" icon color * $type color */ -$cardview-material-header-panel__item--disabled__header-filter-icon--selected-color: null !default; +$cardview-material-header-panel__item--disabled__header-filter-icon--selected-color: color.change($cardview-material-header-panel__item__header-filter-icon--selected-color, $alpha: 0.38) !default; /** * $name 606. Sort index text color * $type color */ -$cardview-material-header-panel__item--disabled__sort-index-text-color: null !default; +$cardview-material-header-panel__item--disabled__sort-index-text-color: color.change($cardview-material-header-panel__item__sort-index-text-color, $alpha: 0.38) !default; // #endregion // #region HEADER PANEL ITEM (ON DRAG) -/** -* $name 700. Background color -* $type color -*/ -$cardview-material-header-panel__item--dragged-bg-color: null !default; - -/** -* $name 701. Border color -* $type color -*/ -$cardview-material-header-panel__item--dragged-border-color: null !default; - /** * $name 702. Text color * $type color */ -$cardview-material-header-panel__item--dragged-text-color: null !default; +$cardview-material-header-panel__item--dragged-text-color: $cardview-material-header-panel__item-text-color !default; /** * $name 704. "Drag allowed" icon color * $type color */ -$cardview-material-header-panel__item--dragged__allowance-icon-color: null !default; +$cardview-material-header-panel__item--dragged__allowance-icon-color: $cardview-material-common-text-color !default; /** * $name 705. "Drag blocked" icon color @@ -287,7 +239,7 @@ $cardview-material-header-panel__item--dragged__prohibition-icon-color: null !de * $name 706. Box shadow color * $type color */ -$cardview-material-header-panel__item--dragged-box-shadow-color: null !default; +$cardview-material-header-panel__item--dragged-box-shadow-color: rgba(0, 0, 0, 0.2) !default; // #endregion @@ -303,19 +255,19 @@ $cardview-material-header-panel__dropzone-bg-color: null !default; * $name 802. Border color * $type color */ -$cardview-material-header-panel__dropzone-border-color: null !default; +$cardview-material-header-panel__dropzone-border-color: $base-accent !default; /** * $name 803. Text color * $type color */ -$cardview-material-header-panel__dropzone-text-color: null !default; +$cardview-material-header-panel__dropzone-text-color: $base-accent !default; /** * $name 804. Icon color * $type color */ -$cardview-material-header-panel__dropzone__icon-color: null !default; +$cardview-material-header-panel__dropzone__icon-color: $base-accent !default; // #endregion @@ -325,25 +277,25 @@ $cardview-material-header-panel__dropzone__icon-color: null !default; * $name 900. Background color * $type color */ -$cardview-material-filter-panel-bg-color: null !default; +$cardview-material-filter-panel-bg-color: $cardview-material-common-bg-color !default; /** * $name 902. Border color * $type color */ -$cardview-material-filter-panel-border-color: null !default; +$cardview-material-filter-panel-border-color: $base-border-color !default; /** * $name 903. Text color * $type color */ -$cardview-material-filter-panel-text-color: null !default; +$cardview-material-filter-panel-text-color: $base-accent !default; /** * $name 904. Icon color * $type color */ -$cardview-material-filter-panel__icon-color: null !default; +$cardview-material-filter-panel__icon-color: $base-accent !default; // #endregion @@ -353,19 +305,19 @@ $cardview-material-filter-panel__icon-color: null !default; * $name 1000. Background color * $type color */ -$cardview-material-nodata-view-bg-color: null !default; +$cardview-material-nodata-view-bg-color: $cardview-material-common-bg-color !default; /** * $name 1001. Text color * $type color */ -$cardview-material-nodata-view-text-color: null !default; +$cardview-material-nodata-view-text-color: $base-label-color !default; /** * $name 1002. Icon color * $type color */ -$cardview-material-nodata-view__icon-color: null !default; +$cardview-material-nodata-view__icon-color: $cardview-material-nodata-view-text-color !default; /** * $name 1003. Icon background color @@ -393,190 +345,120 @@ $cardview-material-card__content__field-value--highlighted-text-color: null !def * $name 1104. Box shadow color * $type color */ -$cardview-material-card__content__field-value--highlighted-box-shadow-color: null !default; +$cardview-material-card__content__field-value--highlighted-box-shadow-color: rgba(0, 0, 0, 0.25) !default; // #endregion // #region COLUMN CHOOSER -$cardview-material-columnchooser--empty__message-color: null !default; +$cardview-material-columnchooser--empty__message-color: color.adjust($cardview-material-common-text-color, $lightness: 26%, $space: hsl) !default; // #endregion @if $mode == "light" { - // Common - $cardview-material-common-bg-color: $base-bg !default; - $cardview-material-common-link-color: $base-link-color !default; - $cardview-material-common-text-color: $base-text-color !default; - // Card - $cardview-material-card-bg-color: $base-bg !default; - $cardview-material-card-border-color: $base-border-color !default; - $cardview-material-card-divider-color: $cardview-material-card-border-color !default; - $cardview-material-card-text-color: $cardview-material-common-text-color !default; - $cardview-material-card__cover-bg-color: #FFF !default; $cardview-material-card__cover--noimage-bg-color: color.adjust($base-bg, $lightness: -7.84%, $space: hsl) !default; - $cardview-material-card__cover--noimage__icon-color: color.change($cardview-material-common-text-color, $alpha: 0.38) !default; - - $cardview-material-card--focused-outline-color: $base-accent !default; // Card - selected $cardview-material-card--selected-bg-color: color.change($base-accent, $alpha: 0.08) !default; $cardview-material-card--selected-border-color: color.change($base-accent, $alpha: 0.32) !default; - $cardview-material-card--selected-divider-color: $cardview-material-card--selected-border-color !default; - $cardview-material-card--selected-text-color: $cardview-material-card-text-color !default; - $cardview-material-card--selected__cover-bg-color: $cardview-material-card__cover-bg-color !default; - $cardview-material-card--selected__cover--noimage-bg-color: $cardview-material-card__cover--noimage-bg-color !default; - $cardview-material-card--selected__cover--noimage__icon-color: $cardview-material-card__cover--noimage__icon-color !default; // Header panel item - default state $cardview-material-header-panel__item-bg-color: color.adjust($base-bg, $lightness: -7.84%, $space: hsl) !default; - $cardview-material-header-panel__item-border-color: $cardview-material-header-panel__item-bg-color !default; - $cardview-material-header-panel__item-text-color: $cardview-material-common-text-color !default; - $cardview-material-header-panel__item__icon-color: $cardview-material-common-text-color !default; - $cardview-material-header-panel__item__header-filter-icon--selected-color: $base-accent !default; - $cardview-material-header-panel__item__sort-index-text-color: $cardview-material-common-text-color !default; - - $cardview-material-header-panel__item--focused-outline-color: $base-accent !default; // Header panel item - hovered $cardview-material-header-panel__item--hovered-bg-color: color.adjust($cardview-material-header-panel__item-bg-color, $lightness: -4.31%, $space: hsl) !default; - $cardview-material-header-panel__item--hovered-border-color: $cardview-material-header-panel__item--hovered-bg-color !default; - $cardview-material-header-panel__item--hovered-text-color: $cardview-material-header-panel__item-text-color !default; - $cardview-material-header-panel__item--hovered__icon-color: $cardview-material-header-panel__item__icon-color !default; - $cardview-material-header-panel__item--hovered__header-filter-icon--selected-color: $cardview-material-header-panel__item__header-filter-icon--selected-color !default; - $cardview-material-header-panel__item--hovered__sort-index-text-color: $cardview-material-header-panel__item__sort-index-text-color !default; - - // Header panel item - disabled - $cardview-material-header-panel__item--disabled-bg-color: $cardview-material-header-panel__item-bg-color !default; - $cardview-material-header-panel__item--disabled-border-color: $cardview-material-header-panel__item--disabled-bg-color !default; - $cardview-material-header-panel__item--disabled-text-color: color.change($cardview-material-header-panel__item-text-color, $alpha: 0.38) !default; - $cardview-material-header-panel__item--disabled__icon-color: color.change($cardview-material-header-panel__item__icon-color, $alpha: 0.38) !default; - $cardview-material-header-panel__item--disabled__header-filter-icon--selected-color: color.change($cardview-material-header-panel__item__header-filter-icon--selected-color, $alpha: 0.38) !default; - $cardview-material-header-panel__item--disabled__sort-index-text-color: color.change($cardview-material-header-panel__item__sort-index-text-color, $alpha: 0.38) !default; // Header panel item - dragged - $cardview-material-header-panel__item--dragged-bg-color: $cardview-material-header-panel__item-bg-color !default; - $cardview-material-header-panel__item--dragged-border-color: $cardview-material-header-panel__item--dragged-bg-color !default; - $cardview-material-header-panel__item--dragged-text-color: $cardview-material-header-panel__item-text-color !default; - $cardview-material-header-panel__item--dragged__allowance-icon-color: $cardview-material-common-text-color !default; $cardview-material-header-panel__item--dragged__prohibition-icon-color: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: -4deg, $saturation: 22.8%, $lightness: -9.61%) !default; - $cardview-material-header-panel__item--dragged-box-shadow-color: rgba(0, 0, 0, 0.2) !default; // Header panel Drop-zone $cardview-material-header-panel__dropzone-bg-color: color.mix($base-accent, $base-bg, 8%) !default; - $cardview-material-header-panel__dropzone-border-color: $base-accent !default; - $cardview-material-header-panel__dropzone-text-color: $base-accent !default; - $cardview-material-header-panel__dropzone__icon-color: $base-accent !default; - - // Filter panel - $cardview-material-filter-panel-bg-color: $cardview-material-common-bg-color !default; - $cardview-material-filter-panel-border-color: $base-border-color !default; - $cardview-material-filter-panel-text-color: $base-accent !default; - $cardview-material-filter-panel__icon-color: $base-accent !default; // Empty view - $cardview-material-nodata-view-bg-color: $cardview-material-common-bg-color !default; - $cardview-material-nodata-view-text-color: $base-label-color !default; - $cardview-material-nodata-view__icon-color: $cardview-material-nodata-view-text-color !default; $cardview-material-nodata-view__icon-bg-color: color.adjust($base-bg, $lightness: -7.84%, $space: hsl) !default; // Search highlight $cardview-material-card__content__field-value--highlighted-text-color: rgba(0, 0, 0, 1) !default; - $cardview-material-card__content__field-value--highlighted-box-shadow-color: rgba(0, 0, 0, 0.25) !default; - - // Column chooser - $cardview-material-columnchooser--empty__message-color: color.adjust($cardview-material-common-text-color, $lightness: 26%, $space: hsl) !default; } @if $mode == "dark" { - // Common - $cardview-material-common-bg-color: $base-bg !default; - $cardview-material-common-link-color: $base-link-color !default; - $cardview-material-common-text-color: $base-text-color !default; - // Card - $cardview-material-card-bg-color: $base-bg !default; - $cardview-material-card-border-color: $base-border-color !default; - $cardview-material-card-divider-color: $cardview-material-card-border-color !default; - $cardview-material-card-text-color: $cardview-material-common-text-color !default; - $cardview-material-card__cover-bg-color: #FFF !default; $cardview-material-card__cover--noimage-bg-color: color.adjust($base-bg, $saturation: -0.3%, $space: hsl, $lightness: 8.04%) !default; - $cardview-material-card__cover--noimage__icon-color: color.change($cardview-material-common-text-color, $alpha: 0.38) !default; - - $cardview-material-card--focused-outline-color: $base-accent !default; // Card - selected $cardview-material-card--selected-bg-color: color.change(color.adjust($base-accent, $lightness: 19.22%, $space: hsl), $alpha: 0.08) !default; $cardview-material-card--selected-border-color: color.change(color.adjust($base-accent, $lightness: 19.22%, $space: hsl), $alpha: 0.32) !default; - $cardview-material-card--selected-divider-color: $cardview-material-card--selected-border-color !default; - $cardview-material-card--selected-text-color: $cardview-material-card-text-color !default; - $cardview-material-card--selected__cover-bg-color: $cardview-material-card__cover-bg-color !default; - $cardview-material-card--selected__cover--noimage-bg-color: $cardview-material-card__cover--noimage-bg-color !default; - $cardview-material-card--selected__cover--noimage__icon-color: $cardview-material-card__cover--noimage__icon-color !default; // Header panel item - default $cardview-material-header-panel__item-bg-color: color.adjust($base-bg, $saturation: -0.3%, $space: hsl, $lightness: 8.04%) !default; - $cardview-material-header-panel__item-border-color: $cardview-material-header-panel__item-bg-color !default; - $cardview-material-header-panel__item-text-color: $cardview-material-common-text-color !default; - $cardview-material-header-panel__item__icon-color: $cardview-material-common-text-color !default; - $cardview-material-header-panel__item__header-filter-icon--selected-color: $base-accent !default; - $cardview-material-header-panel__item__sort-index-text-color: $cardview-material-common-text-color !default; - - $cardview-material-header-panel__item--focused-outline-color: $base-accent !default; // Header panel item - hovered $cardview-material-header-panel__item--hovered-bg-color: color.adjust($cardview-material-header-panel__item-bg-color, $saturation: -3.47%, $space: hsl, $lightness: 2.16%) !default; - $cardview-material-header-panel__item--hovered-border-color: $cardview-material-header-panel__item--hovered-bg-color !default; - $cardview-material-header-panel__item--hovered-text-color: $cardview-material-header-panel__item-text-color !default; - $cardview-material-header-panel__item--hovered__icon-color: $cardview-material-header-panel__item__icon-color !default; - $cardview-material-header-panel__item--hovered__header-filter-icon--selected-color: $cardview-material-header-panel__item__header-filter-icon--selected-color !default; - $cardview-material-header-panel__item--hovered__sort-index-text-color: $cardview-material-header-panel__item__sort-index-text-color !default; - - // Header panel item - disabled - $cardview-material-header-panel__item--disabled-bg-color: $cardview-material-header-panel__item-bg-color !default; - $cardview-material-header-panel__item--disabled-border-color: $cardview-material-header-panel__item--disabled-bg-color !default; - $cardview-material-header-panel__item--disabled-text-color: color.change($cardview-material-header-panel__item-text-color, $alpha: 0.38) !default; - $cardview-material-header-panel__item--disabled__icon-color: color.change($cardview-material-header-panel__item__icon-color, $alpha: 0.38) !default; - $cardview-material-header-panel__item--disabled__header-filter-icon--selected-color: color.change($cardview-material-header-panel__item__header-filter-icon--selected-color, $alpha: 0.38) !default; - $cardview-material-header-panel__item--disabled__sort-index-text-color: color.change($cardview-material-header-panel__item__sort-index-text-color, $alpha: 0.38) !default; // Header panel item - dragged - $cardview-material-header-panel__item--dragged-bg-color: $cardview-material-header-panel__item-bg-color !default; - $cardview-material-header-panel__item--dragged-border-color: $cardview-material-header-panel__item--dragged-bg-color !default; - $cardview-material-header-panel__item--dragged-text-color: $cardview-material-header-panel__item-text-color !default; - $cardview-material-header-panel__item--dragged__allowance-icon-color: $cardview-material-common-text-color !default; $cardview-material-header-panel__item--dragged__prohibition-icon-color: color.adjust($base-danger, $saturation: -0.27%, $space: hsl, $lightness: -7.45%) !default; - $cardview-material-header-panel__item--dragged-box-shadow-color: rgba(0, 0, 0, 0.2) !default; // Header panel Drop-zone $cardview-material-header-panel__dropzone-bg-color: color.mix(color.adjust($base-accent, $lightness: 19.22%, $space: hsl), $base-bg, 8%) !default; - $cardview-material-header-panel__dropzone-border-color: $base-accent !default; - $cardview-material-header-panel__dropzone-text-color: $base-accent !default; - $cardview-material-header-panel__dropzone__icon-color: $base-accent !default; - - // Filter panel - $cardview-material-filter-panel-bg-color: $cardview-material-common-bg-color !default; - $cardview-material-filter-panel-border-color: $base-border-color !default; - $cardview-material-filter-panel-text-color: $base-accent !default; - $cardview-material-filter-panel__icon-color: $base-accent !default; // Empty view - $cardview-material-nodata-view-bg-color: $cardview-material-common-bg-color !default; - $cardview-material-nodata-view-text-color: $base-label-color !default; - $cardview-material-nodata-view__icon-color: $cardview-material-nodata-view-text-color !default; $cardview-material-nodata-view__icon-bg-color: color.adjust($base-bg, $saturation: -0.3%, $space: hsl, $lightness: 8.04%) !default; // Search highlight $cardview-material-card__content__field-value--highlighted-text-color: $base-inverted-text-color !default; - $cardview-material-card__content__field-value--highlighted-box-shadow-color: rgba(0, 0, 0, 0.25) !default; - - // Column chooser - $cardview-material-columnchooser--empty__message-color: color.adjust($cardview-material-common-text-color, $lightness: 26%, $space: hsl) !default; } +/** +* $name 302. Divider color +* $type color +*/ +$cardview-material-card--selected-divider-color: $cardview-material-card--selected-border-color !default; + +/** +* $name 306. "Empty cover" background color +* $type color +*/ +$cardview-material-card--selected__cover--noimage-bg-color: $cardview-material-card__cover--noimage-bg-color !default; + +/** +* $name 401. Border color +* $type color +*/ +$cardview-material-header-panel__item-border-color: $cardview-material-header-panel__item-bg-color !default; + +/** +* $name 501. Border color +* $type color +*/ +$cardview-material-header-panel__item--hovered-border-color: $cardview-material-header-panel__item--hovered-bg-color !default; + +/** +* $name 600. Background color +* $type color +*/ +$cardview-material-header-panel__item--disabled-bg-color: $cardview-material-header-panel__item-bg-color !default; + +/** +* $name 601. Border color +* $type color +*/ +$cardview-material-header-panel__item--disabled-border-color: $cardview-material-header-panel__item--disabled-bg-color !default; + +/** +* $name 700. Background color +* $type color +*/ +$cardview-material-header-panel__item--dragged-bg-color: $cardview-material-header-panel__item-bg-color !default; + +/** +* $name 701. Border color +* $type color +*/ +$cardview-material-header-panel__item--dragged-border-color: $cardview-material-header-panel__item--dragged-bg-color !default; + $cardview-bg-color: $cardview-material-common-bg-color !default; $cardview-link-color: $cardview-material-common-link-color !default; $cardview-text-color: $cardview-material-common-text-color !default; diff --git a/packages/devextreme-scss/scss/widgets/material/cardView/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/cardView/_sizes.scss index c7ef0ddf524f..e67175a11df0 100644 --- a/packages/devextreme-scss/scss/widgets/material/cardView/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/cardView/_sizes.scss @@ -17,14 +17,14 @@ * $name 101. Border radius * $type text */ -$cardview-material-common-border-radius: null !default; +$cardview-material-common-border-radius: 16px !default; /** * $name 104. Text weight * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-material-common-text-weight: null !default; +$cardview-material-common-text-weight: 400 !default; /** * $name 105. Vertical gap between cards @@ -46,9 +46,9 @@ $cardview-material-common-padding: null !default; // #region CARD -$cardview-material-card-min-width: null !default; +$cardview-material-card-min-width: 250px !default; -$cardview-material-card-border-width: null !default; +$cardview-material-card-border-width: 1px !default; /** * $name 202. Border radius @@ -73,7 +73,7 @@ $cardview-material-card__content-line-height: null !default; * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-material-card__field-caption-weight: null !default; +$cardview-material-card__field-caption-weight: 500 !default; $cardview-material-card__cover-max-height: null !default; @@ -85,7 +85,7 @@ $cardview-material-card__content-row-gap: null !default; $cardview-material-card__content__cell-padding-vertical: null !default; -$cardview-material-card__content__cell-padding-horizontal: null !default; +$cardview-material-card__content__cell-padding-horizontal: 12px !default; // #endregion @@ -96,19 +96,19 @@ $cardview-material-card__content__cell-padding-horizontal: null !default; * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-material-card--selected__field-caption-weight: null !default; +$cardview-material-card--selected__field-caption-weight: $cardview-material-card__field-caption-weight !default; // #endregion // #region HEADER -$cardview-material-header-gap: null !default; +$cardview-material-header-gap: 4px !default; // #endregion // #region HEADER PANEL -$cardview-material-header-panel-vertical-margin: null !default; +$cardview-material-header-panel-vertical-margin: 4px !default; $cardview-material-header-panel--empty-line-height: null !default; @@ -116,13 +116,13 @@ $cardview-material-header-panel--empty-line-height: null !default; // #region HEADER PANEL ITEM -$cardview-material-header-panel__item-border-width: null !default; +$cardview-material-header-panel__item-border-width: 1px !default; /** * $name 402. Border radius * $type text */ -$cardview-material-header-panel__item-border-radius: null !default; +$cardview-material-header-panel__item-border-radius: 99px !default; $cardview-material-header-panel__item-padding-vertical: null !default; @@ -135,14 +135,14 @@ $cardview-material-header-panel__item-line-height: null !default; * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-material-header-panel__item-text-weight: null !default; +$cardview-material-header-panel__item-text-weight: $cardview-material-common-text-weight !default; /** * $name 408. Sort index text weight * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-material-header-panel__item__sort-index-text-weight: null !default; +$cardview-material-header-panel__item__sort-index-text-weight: 400 !default; $cardview-material-header-panel__item__icon-size: null !default; @@ -158,14 +158,14 @@ $cardview-material-header-item__right-padding: null !default; * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-material-header-panel__item--hovered-text-weight: null !default; +$cardview-material-header-panel__item--hovered-text-weight: $cardview-material-header-panel__item-text-weight !default; /** * $name 507. Sort index text weight * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-material-header-panel__item--hovered__sort-index-text-weight: null !default; +$cardview-material-header-panel__item--hovered__sort-index-text-weight: $cardview-material-header-panel__item__sort-index-text-weight !default; // #endregion @@ -176,14 +176,14 @@ $cardview-material-header-panel__item--hovered__sort-index-text-weight: null !de * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-material-header-panel__item--disabled-text-weight: null !default; +$cardview-material-header-panel__item--disabled-text-weight: $cardview-material-header-panel__item-text-weight !default; /** * $name 607. Sort index text weight * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-material-header-panel__item--disabled__sort-index-text-weight: null !default; +$cardview-material-header-panel__item--disabled__sort-index-text-weight: $cardview-material-header-panel__item__sort-index-text-weight !default; // #endregion @@ -194,10 +194,10 @@ $cardview-material-header-panel__item--disabled__sort-index-text-weight: null !d * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-material-header-panel__item--dragged-text-weight: null !default; +$cardview-material-header-panel__item--dragged-text-weight: $cardview-material-header-panel__item-text-weight !default; -$cardview-material-header-item__sort-indicator__border-size: null; -$cardview-material-header-item__sort-indicator__border-radius: null; +$cardview-material-header-item__sort-indicator__border-size: null !default; +$cardview-material-header-item__sort-indicator__border-radius: null !default; // #endregion @@ -207,7 +207,7 @@ $cardview-material-header-panel__dropzone-height: null !default; $cardview-material-header-panel__dropzone-top: null !default; -$cardview-material-header-panel__dropzone-border-width: null !default; +$cardview-material-header-panel__dropzone-border-width: 1.5px !default; /** * $name 801. Border radius @@ -223,7 +223,7 @@ $cardview-material-header-panel__dropzone-border-radius: null !default; * $name 901. Border radius * $type text */ -$cardview-material-filter-panel-border-radius: null !default; +$cardview-material-filter-panel-border-radius: 0 !default; // #endregion @@ -243,16 +243,16 @@ $cardview-material-nodata-view__icon-size: null !default; * $name 1101. Border radius * $type text */ -$cardview-material-card__content__field-value--highlighted-border-radius: null !default; +$cardview-material-card__content__field-value--highlighted-border-radius: 5px !default; -$cardview-material-card__content__field-value--highlighted-border-padding: null !default; +$cardview-material-card__content__field-value--highlighted-border-padding: 0.125em 0.2em !default; /** * $name 1103. Text weight * $type select * $type-values 100|200|300|400|500|600|700|800|900 */ -$cardview-material-card__content__field-value--highlighted-text-weight: null !default; +$cardview-material-card__content__field-value--highlighted-text-weight: 500 !default; // #endregion @@ -267,84 +267,48 @@ $cardview-material-column-chooser__select-all-item-horizontal-padding: null !def @if $size == "default" { // Common - $cardview-material-common-border-radius: 16px !default; - $cardview-material-common-text-weight: 400 !default; $cardview-material-common-cards-row-gap: 16px !default; $cardview-material-common-cards-column-gap: 16px !default; $cardview-material-common-gap: 24px !default; $cardview-material-common-padding: 24px !default; // Card - $cardview-material-card-min-width: 250px !default; - $cardview-material-card-border-width: 1px !default; $cardview-material-card-border-radius: 8px !default; $cardview-material-card__header-height: 52px !default; $cardview-material-card__header-text-size: 16px !default; $cardview-material-card__header__toolbar-override-height: 52px !default; $cardview-material-card__header__selection-checkbox-padding-right: 12px !default; - $cardview-material-card__header-toolbar-override-height: 52px !default; - $cardview-material-card__field-caption-weight: 500 !default; $cardview-material-card__cover-max-height: 180px !default; $cardview-material-card__cover--noimage__icon-size: 48px !default; $cardview-material-card__content-padding: 12px !default; $cardview-material-card__content-row-gap: 4px !default; $cardview-material-card__content__cell-padding-vertical: 6px !default; - $cardview-material-card__content__cell-padding-horizontal: 12px !default; - - // Card - selected - $cardview-material-card--selected__field-caption-weight: $cardview-material-card__field-caption-weight !default; - - // Header - $cardview-material-header-gap: 4px !default; // Header panel - $cardview-material-header-panel-vertical-margin: 4px !default; $cardview-material-header-panel--empty-line-height: 36px !default; // Header panel item - default - $cardview-material-header-panel__item-border-width: 1px !default; - $cardview-material-header-panel__item-border-radius: 99px !default; $cardview-material-header-panel__item-padding-horizontal: 16px !default; $cardview-material-header-panel__item-padding-vertical: 7px !default; $cardview-material-header-item__left-padding: 6px !default; $cardview-material-header-item__right-padding: 2px !default; $cardview-material-header-panel__item-line-height: 20px !default; - $cardview-material-header-panel__item-text-weight: $cardview-material-common-text-weight !default; - $cardview-material-header-panel__item__sort-index-text-weight: 400 !default; $cardview-material-header-panel__item__icon-size: 20px !default; - // Header panel item - hovered - $cardview-material-header-panel__item--hovered-text-weight: $cardview-material-header-panel__item-text-weight !default; - $cardview-material-header-panel__item--hovered__sort-index-text-weight: $cardview-material-header-panel__item__sort-index-text-weight !default; - - // Header panel item - disabled - $cardview-material-header-panel__item--disabled-text-weight: $cardview-material-header-panel__item-text-weight !default; - $cardview-material-header-panel__item--disabled__sort-index-text-weight: $cardview-material-header-panel__item__sort-index-text-weight !default; - // Header panel item - dragged - $cardview-material-header-panel__item--dragged-text-weight: $cardview-material-header-panel__item-text-weight !default; $cardview-material-header-item__sort-indicator__border-size: 2px !default; $cardview-material-header-item__sort-indicator__border-radius: 8px !default; // Header panel Drop-zone $cardview-material-header-panel__dropzone-height: 44px !default; $cardview-material-header-panel__dropzone-top: -4px !default; - $cardview-material-header-panel__dropzone-border-width: 1.5px !default; $cardview-material-header-panel__dropzone-border-radius: 8px !default; - // Filter panel - $cardview-material-filter-panel-border-radius: 0 !default; - // Empty view $cardview-material-nodata-view__icon-container-size: 64px !default; $cardview-material-nodata-view__icon-container-margin: 12px !default; $cardview-material-nodata-view__icon-size: 40px !default; - // Search highlight - $cardview-material-card__content__field-value--highlighted-border-radius: 5px !default; - $cardview-material-card__content__field-value--highlighted-border-padding: 0.125em 0.2em !default; - $cardview-material-card__content__field-value--highlighted-text-weight: 500 !default; - // Column chooser $cardview-material-column-chooser--empty__message-horizontal-padding: 24px !default; $cardview-material-column-chooser__select-all-item-horizontal-padding: 19px !default; @@ -353,22 +317,16 @@ $cardview-material-column-chooser__select-all-item-horizontal-padding: null !def @else if $size == "compact" { // Common - $cardview-material-common-border-radius: 16px !default; - $cardview-material-common-text-weight: 400 !default; $cardview-material-common-cards-row-gap: 12px !default; $cardview-material-common-cards-column-gap: 12px !default; $cardview-material-common-gap: 16px !default; $cardview-material-common-padding: 16px !default; // Card - $cardview-material-card-min-width: 250px !default; - $cardview-material-card-border-width: 1px !default; $cardview-material-card-border-radius: 6px !default; $cardview-material-card__header-height: 44px !default; $cardview-material-card__header-text-size: 14px !default; $cardview-material-card__header__selection-checkbox-padding-right: 8px !default; - $cardview-material-card__header-toolbar-override-height: 44px !default; - $cardview-material-card__field-caption-weight: 500 !default; $cardview-material-card__cover-max-height: 140px !default; $cardview-material-card__cover--noimage__icon-size: 32px !default; $cardview-material-card__content-padding: 8px !default; @@ -376,62 +334,32 @@ $cardview-material-column-chooser__select-all-item-horizontal-padding: null !def $cardview-material-card__content-font-size: 12px !default; $cardview-material-card__content-line-height: 16px !default; $cardview-material-card__content__cell-padding-vertical: 4px !default; - $cardview-material-card__content__cell-padding-horizontal: 12px !default; - - // Card - selected - $cardview-material-card--selected__field-caption-weight: $cardview-material-card__field-caption-weight !default; - - // Header - $cardview-material-header-gap: 4px !default; // Header panel - $cardview-material-header-panel-vertical-margin: 4px !default; $cardview-material-header-panel--empty-line-height: 32px !default; // Header panel item - default - $cardview-material-header-panel__item-border-width: 1px !default; - $cardview-material-header-panel__item-border-radius: 99px !default; $cardview-material-header-panel__item-padding-horizontal: 12px !default; $cardview-material-header-panel__item-padding-vertical: 5px !default; $cardview-material-header-item__left-padding: 3px !default; $cardview-material-header-item__right-padding: 1px !default; $cardview-material-header-panel__item-line-height: 16px !default; - $cardview-material-header-panel__item-text-weight: $cardview-material-common-text-weight !default; - $cardview-material-header-panel__item__sort-index-text-weight: 400 !default; $cardview-material-header-panel__item__icon-size: 16px !default; - // Header panel item - hovered - $cardview-material-header-panel__item--hovered-text-weight: $cardview-material-header-panel__item-text-weight !default; - $cardview-material-header-panel__item--hovered__sort-index-text-weight: $cardview-material-header-panel__item__sort-index-text-weight !default; - - // Header panel item - disabled - $cardview-material-header-panel__item--disabled-text-weight: $cardview-material-header-panel__item-text-weight !default; - $cardview-material-header-panel__item--disabled__sort-index-text-weight: $cardview-material-header-panel__item__sort-index-text-weight !default; - // Header panel item - dragged - $cardview-material-header-panel__item--dragged-text-weight: $cardview-material-header-panel__item-text-weight !default; $cardview-material-header-item__sort-indicator__border-size: 1px !default; $cardview-material-header-item__sort-indicator__border-radius: 4px !default; // Header panel Drop-zone $cardview-material-header-panel__dropzone-height: 38px !default; $cardview-material-header-panel__dropzone-top: -3px !default; - $cardview-material-header-panel__dropzone-border-width: 1.5px !default; $cardview-material-header-panel__dropzone-border-radius: 6px !default; - // Filter panel - $cardview-material-filter-panel-border-radius: 0 !default; - // Empty view $cardview-material-nodata-view__icon-container-size: 48px !default; $cardview-material-nodata-view__icon-container-margin: 8px !default; $cardview-material-nodata-view__icon-size: 32px !default; - // Search highlight - $cardview-material-card__content__field-value--highlighted-border-radius: 5px !default; - $cardview-material-card__content__field-value--highlighted-border-padding: 0.125em 0.2em !default; - $cardview-material-card__content__field-value--highlighted-text-weight: 500 !default; - // Column chooser $cardview-material-column-chooser--empty__message-horizontal-padding: 16px !default; $cardview-material-column-chooser__select-all-item-horizontal-padding: 17px !default; diff --git a/packages/devextreme-scss/scss/widgets/material/chat/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/chat/_sizes.scss index eee7e7d54d74..8bb57067f07c 100644 --- a/packages/devextreme-scss/scss/widgets/material/chat/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/chat/_sizes.scss @@ -56,7 +56,6 @@ $chat-messagelist-day-header-first-padding-top: 20px !default; $chat-messagelist-day-header-font-size: 12px !default; $chat-typingindicator-template: $chat-messagegroup-alignment-start-template !default; -$chat-typingindicator-padding: null !default; $chat-typingindicator-row-gap: $chat-messagegroup-gap !default; $chat-typingindicator-font-size: $chat-information-font-size !default; $chat-typingindicator-bubble-padding-block: null !default; @@ -115,7 +114,6 @@ $chat-bubble-gap: 8px !default; $chat-typingindicator-circle-size-center: math.div(12, 14) * 1em !default; $chat-typingindicator-bubble-padding-block: 11px !default; $chat-typingindicator-bubble-padding-inline: 20px !default; - $chat-typingindicator-padding: $chat-messagegroup-padding !default; $chat-confirmation-popup-content-padding: 16px !default; $chat-confirmation-popup-toolbar-padding-block: 0 16px !default; @@ -156,7 +154,6 @@ $chat-bubble-gap: 8px !default; $chat-typingindicator-circle-size-center: math.div(12, 13) * 1em !default; $chat-typingindicator-bubble-padding-block: 8px !default; $chat-typingindicator-bubble-padding-inline: 12px !default; - $chat-typingindicator-padding: $chat-messagegroup-padding !default; $chat-confirmation-popup-content-padding: 8px !default; $chat-confirmation-popup-toolbar-padding-block: 0 8px !default; @@ -165,3 +162,5 @@ $chat-bubble-gap: 8px !default; $chat-file-icon-font-size: 16px !default; } + +$chat-typingindicator-padding: $chat-messagegroup-padding !default; diff --git a/packages/devextreme-scss/scss/widgets/material/checkBox/_index.scss b/packages/devextreme-scss/scss/widgets/material/checkBox/_index.scss index 119dfc9588ae..cb1034a3b0dd 100644 --- a/packages/devextreme-scss/scss/widgets/material/checkBox/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/checkBox/_index.scss @@ -7,7 +7,7 @@ @use "../../base/checkBox"; @use "../../base/checkBox/mixins" as *; -$material-checkbox-ripple-transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1); +$material-checkbox-ripple-transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1) !default; .dx-checkbox { .dx-checkbox-container { diff --git a/packages/devextreme-scss/scss/widgets/material/colorView/_index.scss b/packages/devextreme-scss/scss/widgets/material/colorView/_index.scss index 556fe5fe74c5..deb1bfcbfced 100644 --- a/packages/devextreme-scss/scss/widgets/material/colorView/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/colorView/_index.scss @@ -4,8 +4,8 @@ @use "../../base/colorView"; @include dx-base-colorview-styles($colorview-handle-color, $colorview-handle-border-color); -$material-colorview-alpha-width: $material-colorview-palette-width + 4; -$material-colorview-hue-scale-height: $material-colorview-palette-height; +$material-colorview-alpha-width: $material-colorview-palette-width + 4 !default; +$material-colorview-hue-scale-height: $material-colorview-palette-height !default; .dx-colorview-container { width: $material-colorview-container-width; diff --git a/packages/devextreme-scss/scss/widgets/material/common/_mixins.scss b/packages/devextreme-scss/scss/widgets/material/common/_mixins.scss index 9d81f4600169..a4c30cea256a 100644 --- a/packages/devextreme-scss/scss/widgets/material/common/_mixins.scss +++ b/packages/devextreme-scss/scss/widgets/material/common/_mixins.scss @@ -8,7 +8,7 @@ @use "../../base/validation" as *; @use "../list/sizes" as *; -$badge-invalid-bg: $base-invalid-color; +$badge-invalid-bg: $base-invalid-color !default; @mixin dx-base-typography() { @include dx-base-typography-mixin( diff --git a/packages/devextreme-scss/scss/widgets/material/dateRangeBox/_index.scss b/packages/devextreme-scss/scss/widgets/material/dateRangeBox/_index.scss index 38c27e283940..902b0493def2 100644 --- a/packages/devextreme-scss/scss/widgets/material/dateRangeBox/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/dateRangeBox/_index.scss @@ -9,7 +9,7 @@ @use "../../base/icon_fonts" as *; @use "../textEditor" as *; -$material-editor-custom-button-margin: 5px; +$material-editor-custom-button-margin: 5px !default; .dx-daterangebox { @include dx-dropdowneditor-button-icon-styling(daterangepicker); diff --git a/packages/devextreme-scss/scss/widgets/material/dateView/_index.scss b/packages/devextreme-scss/scss/widgets/material/dateView/_index.scss index 936652fe63ca..003ec8b587fe 100644 --- a/packages/devextreme-scss/scss/widgets/material/dateView/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/dateView/_index.scss @@ -6,7 +6,7 @@ @use "../../base/dateView"; @use "../scrollable"; -$material-dateview-roller-padding: math.div($material-dateview-roller-height - $material-dateview-roller-item-height, 2); +$material-dateview-roller-padding: math.div($material-dateview-roller-height - $material-dateview-roller-item-height, 2) !default; .dx-dateview-rollers { width: auto; diff --git a/packages/devextreme-scss/scss/widgets/material/diagram/_index.scss b/packages/devextreme-scss/scss/widgets/material/diagram/_index.scss index eb57e7b9e913..c436fcd6eebe 100644 --- a/packages/devextreme-scss/scss/widgets/material/diagram/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/diagram/_index.scss @@ -15,10 +15,10 @@ @use "../fieldset/sizes" as *; @use "../menu/colors" as *; -$material-diagram-toolbar-icon-size: $material-base-icon-size; -$diagram-toolbar-colorbutton-border-width: 3px; +$material-diagram-toolbar-icon-size: $material-base-icon-size !default; +$diagram-toolbar-colorbutton-border-width: 3px !default; -$diagram-toolbox-title-size: 18px; +$diagram-toolbox-title-size: 18px !default; .dx-diagram { border-color: $diagram-border-color; diff --git a/packages/devextreme-scss/scss/widgets/material/dropDownButton/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/dropDownButton/_sizes.scss index 57c4be674692..60ff376fd31c 100644 --- a/packages/devextreme-scss/scss/widgets/material/dropDownButton/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/dropDownButton/_sizes.scss @@ -5,10 +5,10 @@ $material-dropdownbutton-icon-spindown-size: null !default; @if $size == "default" { $material-dropdownbutton-action-horizontal-padding: 12px !default; - $material-dropdownbutton-icon-spindown-size: 24px; + $material-dropdownbutton-icon-spindown-size: 24px !default; } @else if $size == "compact" { $material-dropdownbutton-action-horizontal-padding: 8px !default; - $material-dropdownbutton-icon-spindown-size: 18px; + $material-dropdownbutton-icon-spindown-size: 18px !default; } diff --git a/packages/devextreme-scss/scss/widgets/material/dropDownEditor/_index.scss b/packages/devextreme-scss/scss/widgets/material/dropDownEditor/_index.scss index aecf0ce9c787..e71b1065dae8 100644 --- a/packages/devextreme-scss/scss/widgets/material/dropDownEditor/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/dropDownEditor/_index.scss @@ -8,17 +8,17 @@ @use "../../base/dropDownEditor"; @use "../../base/dropDownEditor/mixins" as *; -$material-dropdowneditor-container-button-size: $material-base-icon-size; +$material-dropdowneditor-container-button-size: $material-base-icon-size !default; $material-dropdowneditor-invalid-badge-offset: $material-dropdowneditor-container-button-size + - $material-invalid-badge-halfhorizontal-padding; + $material-invalid-badge-halfhorizontal-padding !default; $material-dropdowneditor-invalid-badge-with-clear-offset: $material-dropdowneditor-container-button-size * 2 + - $material-invalid-badge-halfhorizontal-padding; + $material-invalid-badge-halfhorizontal-padding !default; -$material-dropdowneditor-invalid-badge-size: $material-invalid-badge-size + $material-invalid-badge-horizontal-padding; +$material-dropdowneditor-invalid-badge-size: $material-invalid-badge-size + $material-invalid-badge-horizontal-padding !default; @mixin dx-dropdowneditor-button-icon() { width: $material-dropdowneditor-container-button-size; diff --git a/packages/devextreme-scss/scss/widgets/material/fileManager/_colors.scss b/packages/devextreme-scss/scss/widgets/material/fileManager/_colors.scss index ed7f90e27e38..f0a6358c9177 100644 --- a/packages/devextreme-scss/scss/widgets/material/fileManager/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/material/fileManager/_colors.scss @@ -16,14 +16,11 @@ $filemanager-file-item-hover-bg: $base-hover-bg !default; $filemanager-file-item-focused-border: $base-accent !default; $filemanager-item-focused-color: $base-inverted-text-color !default; $filemanager-dirs-tree-item-focus-bg: null !default; -$filemanager-progressbox-shadow-color: null !default; @if $mode == "light" { $filemanager-dirs-tree-item-focus-bg: color.adjust($base-bg, $lightness: -8%, $space: hsl) !default; - $filemanager-progressbox-shadow-color: color.change($base-shadow-color, $alpha: 0.175) !default; } @if $mode == "dark" { $filemanager-dirs-tree-item-focus-bg: color.adjust($base-bg, $lightness: 8%, $space: hsl) !default; - $filemanager-progressbox-shadow-color: color.change(color.adjust($base-shadow-color, $lightness: -100%, $space: hsl), $alpha: 0.3) !default; } diff --git a/packages/devextreme-scss/scss/widgets/material/fileManager/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/fileManager/_sizes.scss index 8a8635282724..710c1b09d13f 100644 --- a/packages/devextreme-scss/scss/widgets/material/fileManager/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/fileManager/_sizes.scss @@ -15,7 +15,7 @@ $filemanager-large-icon-size: null !default; $filemanager-progress-panel-title-padding: null !default; $filemanager-progress-box-button-close-margin-top: null !default; -$filemanager-toolbar-height: $material-toolbar-height; +$filemanager-toolbar-height: $material-toolbar-height !default; @if $size == "default" { $filemanager-toolbar-separator-height: 36px !default; diff --git a/packages/devextreme-scss/scss/widgets/material/fileUploader/_index.scss b/packages/devextreme-scss/scss/widgets/material/fileUploader/_index.scss index bb9fcb08ec53..1d2218ae1772 100644 --- a/packages/devextreme-scss/scss/widgets/material/fileUploader/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/fileUploader/_index.scss @@ -6,7 +6,7 @@ @use "../../base/mixins" as *; @use "../../base/fileUploader"; -$material-fileuploader-file-wrapper-border-size: 3px; +$material-fileuploader-file-wrapper-border-size: 3px !default; .dx-fileuploader-wrapper { padding: $material-fileuploader-vertical-padding; diff --git a/packages/devextreme-scss/scss/widgets/material/fileUploader/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/fileUploader/_sizes.scss index 7956eae8fdf4..a0d2d94e429e 100644 --- a/packages/devextreme-scss/scss/widgets/material/fileUploader/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/fileUploader/_sizes.scss @@ -3,7 +3,7 @@ $material-fileuploader-file-container-padding: null !default; $material-fileuploader-file-container-text-margin: null !default; $material-fileuploader-file-container-button-margin: null !default; -$material-fileuploader-file-cancel-button-margin-left: 4px; +$material-fileuploader-file-cancel-button-margin-left: 4px !default; $material-fileuploader-file-container-button-size: null !default; $material-fileuploader-file-icon-font-size: null !default; diff --git a/packages/devextreme-scss/scss/widgets/material/gantt/_colors.scss b/packages/devextreme-scss/scss/widgets/material/gantt/_colors.scss index de5874276173..fe96fa4a1ac0 100644 --- a/packages/devextreme-scss/scss/widgets/material/gantt/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/material/gantt/_colors.scss @@ -12,8 +12,8 @@ $gantt-task-background-color: null !default; @if $color == "lime" { $gantt-parent-task-background-color: #f44336 !default; $gantt-task-progress-background-color: rgba(0, 0, 0, 0.2) !default; - $gantt-task-color: #fff; - $gantt-task-background-color: #999; + $gantt-task-color: #fff !default; + $gantt-task-background-color: #999 !default; } @if $color == "blue" { @@ -34,8 +34,8 @@ $gantt-task-background-color: null !default; @if $color == "orange" or $color == "purple" or $color == "teal" { $gantt-parent-task-background-color: #8bc34a !default; $gantt-task-progress-background-color: rgba(0, 0, 0, 0.2) !default; - $gantt-task-color: #fff; - $gantt-task-background-color: #999; + $gantt-task-color: #fff !default; + $gantt-task-background-color: #999 !default; } @if $mode == "light" { diff --git a/packages/devextreme-scss/scss/widgets/material/gantt/_index.scss b/packages/devextreme-scss/scss/widgets/material/gantt/_index.scss index fcdcfef389c2..4a5b51c944b4 100644 --- a/packages/devextreme-scss/scss/widgets/material/gantt/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/gantt/_index.scss @@ -9,7 +9,7 @@ @use "../form/sizes" as *; @use "../splitterBar"; -$gantt-successor-background-color: white; +$gantt-successor-background-color: white !default; .dx-gantt { border: 1px solid; diff --git a/packages/devextreme-scss/scss/widgets/material/gridBase/_colors.scss b/packages/devextreme-scss/scss/widgets/material/gridBase/_colors.scss index 3c3662d4e159..31b9b07734c4 100644 --- a/packages/devextreme-scss/scss/widgets/material/gridBase/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/material/gridBase/_colors.scss @@ -35,7 +35,6 @@ $datagrid-drag-header-border-color: color.change($base-accent, $alpha: 0.5) !def $datagrid-drag-header-shadow-color: null !default; $datagrid-drag-header-second-shadow-color: null !default; $datagrid-header-drag-bg: $datagrid-base-background-color !default; -$header-filter-color: null !default; /** * $name 51. Selected row background color @@ -82,7 +81,6 @@ $datagrid-menu-icon-color: inherit !default; $datagrid-cell-modified-border-color: null !default; $datagrid-cell-removed-border-color: $datagrid-border-color !default; $datagrid-row-removed-bg: $datagrid-border-color !default; -$datagrid-cell-removed-text-color: null !default; /** * $name 45. Invalid data faded color @@ -162,12 +160,13 @@ $datagrid-summary-bg: $datagrid-base-background-color !default; $datagrid-column-separator-bg: $base-accent !default; $datagrid-filter-row-background-color: color.adjust($datagrid-base-background-color, $lightness: -2%, $space: hsl) !default; $datagrid-active-header-filter-icon-color: $base-accent !default; -$datagrid-text-stub-background-image-path: null !default; + +$datagrid-text-stub-background-image-path: data-uri("images/widgets/material/grid/text-stub.png") !default; $datagrid-filter-panel-color: $base-accent !default; $datagrid-draggable-column-text-color: color.change($base-text-color, $alpha: 0.38) !default; -$datagrid-text-link-disabled-opacity: $base-disabled-opacity; -$datagrid-icon-link-disabled-opacity: $button-disabled-icon-opacity; +$datagrid-text-link-disabled-opacity: $base-disabled-opacity !default; +$datagrid-icon-link-disabled-opacity: $button-disabled-icon-opacity !default; @if $mode == "light" { $datagrid-cell-modified-border-color: color.adjust($base-success, $lightness: 51%, $space: hsl) !default; @@ -175,12 +174,9 @@ $datagrid-icon-link-disabled-opacity: $button-disabled-icon-opacity; $datagrid-block-separator-bg: color.adjust($datagrid-base-background-color, $lightness: -12%, $space: hsl) !default; $datagrid-drag-header-shadow-color: color.change($base-shadow-color, $alpha: 0.1) !default; $datagrid-drag-header-second-shadow-color: color.change($base-shadow-color, $alpha: 0.2) !default; - $header-filter-color: $datagrid-columnchooser-item-color !default; $datagrid-columnchooser-item-bg: $datagrid-columnchooser-bg !default; $datagrid-row-alternation-bg: color.adjust($datagrid-base-background-color, $lightness: -4%, $space: hsl) !default; $datagrid-hover-bg: color.adjust($datagrid-base-background-color, $lightness: -4%, $space: hsl) !default; - $datagrid-text-stub-background-image-path: data-uri("images/widgets/material/color-schemes/light/grid/text-stub.png") !default; - $datagrid-cell-removed-text-color: $datagrid-columnchooser-item-color !default; $datagrid-row-invalid-faded-border-color: color.adjust($base-danger, $lightness: 44%, $space: hsl) !default; } @@ -190,15 +186,15 @@ $datagrid-icon-link-disabled-opacity: $button-disabled-icon-opacity; $datagrid-block-separator-bg: color.adjust($datagrid-base-background-color, $lightness: 12%, $space: hsl) !default; $datagrid-drag-header-shadow-color: color.change($base-shadow-color, $alpha: 0.3) !default; $datagrid-drag-header-second-shadow-color: color.change($base-shadow-color, $alpha: 0.4) !default; - $header-filter-color: $datagrid-columnchooser-item-color !default; $datagrid-columnchooser-item-bg: color.adjust($datagrid-columnchooser-bg, $lightness: 3%, $space: hsl) !default; $datagrid-row-alternation-bg: color.adjust($datagrid-base-background-color, $lightness: 4%, $space: hsl) !default; $datagrid-hover-bg: color.adjust($datagrid-base-background-color, $lightness: 4%, $space: hsl) !default; - $datagrid-text-stub-background-image-path: data-uri("images/widgets/material/color-schemes/dark/grid/text-stub.png") !default; - $datagrid-cell-removed-text-color: $datagrid-columnchooser-item-color !default; $datagrid-row-invalid-faded-border-color: color.adjust($base-danger, $lightness: -31%, $space: hsl) !default; } +$header-filter-color: $datagrid-columnchooser-item-color !default; +$datagrid-cell-removed-text-color: $datagrid-columnchooser-item-color !default; + $header-filter-color-empty: $header-filter-color !default; :root { diff --git a/packages/devextreme-scss/scss/widgets/material/gridBase/_index.scss b/packages/devextreme-scss/scss/widgets/material/gridBase/_index.scss index 1afc72cc3cfb..7ea5a0c74fb6 100644 --- a/packages/devextreme-scss/scss/widgets/material/gridBase/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/gridBase/_index.scss @@ -28,12 +28,12 @@ @use "../overlay"; @use "../pagination"; -$material-grid-base-border-hidden: 1px solid transparent; -$material-grid-base-row-border: 1px solid transparent; -$material-grid-base-drag-header-first-shadow: 0 0 1px $datagrid-drag-header-shadow-color; -$material-grid-base-drag-header-second-shadow: 0 1px 3px $datagrid-drag-header-second-shadow-color; +$material-grid-base-border-hidden: 1px solid transparent !default; +$material-grid-base-row-border: 1px solid transparent !default; +$material-grid-base-drag-header-first-shadow: 0 0 1px $datagrid-drag-header-shadow-color !default; +$material-grid-base-drag-header-second-shadow: 0 1px 3px $datagrid-drag-header-second-shadow-color !default; -$material-grid-base-group-panel-message-line-height: $material-button-text-line-height; +$material-grid-base-group-panel-message-line-height: $material-button-text-line-height !default; @mixin grid-base($widget-name) { @include material-grid-base-cell($widget-name); diff --git a/packages/devextreme-scss/scss/widgets/material/gridBase/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/gridBase/_sizes.scss index 0ea533d4e9aa..928d14b042ab 100644 --- a/packages/devextreme-scss/scss/widgets/material/gridBase/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/gridBase/_sizes.scss @@ -15,11 +15,10 @@ $material-grid-base-grouppanel-item-margin: null !default; $material-grid-base-command-edit-column-width: null !default; $material-grid-base-command-edit-column-with-icons-width: null !default; $material-grid-base-adaptive-column-height: null !default; -$material-grid-base-treeview-search-margin: null !default; +$material-grid-base-treeview-search-margin: 10px !default; $material-grid-base-nodata-font-size: null !default; $material-command-edit-icon-size: null !default; $material-filter-row-between-editor-padding: null !default; -$material-command-column-additional-right-margin: null !default; $material-grid-base-treeview-select-all-item-offset: null !default; $material-grid-base-command-ai-chatsparkleoutline-icon-size: null !default; $material-grid-base-command-ai-text-content-padding: null !default; @@ -51,12 +50,10 @@ $material-grid-base-ai-prompt-editor-progressbar-height: 4px !default; $material-grid-base-command-edit-column-width: 115px !default; $material-grid-base-command-edit-column-with-icons-width: 93px !default; $material-grid-base-adaptive-column-height: 40px !default; - $material-grid-base-treeview-search-margin: 10px !default; $material-grid-base-nodata-font-size: 17px !default; $material-command-edit-icon-size: 18px !default; $material-filter-row-between-editor-padding: 12px !default; - $material-command-column-additional-right-margin: 0 !default; $material-grid-base-treeview-select-all-item-offset: 43px !default; $material-grid-base-command-ai-chatsparkleoutline-icon-size: 24px !default; @@ -83,12 +80,10 @@ $material-grid-base-ai-prompt-editor-progressbar-height: 4px !default; $material-grid-base-command-edit-column-width: 105px !default; $material-grid-base-command-edit-column-with-icons-width: 80px !default; $material-grid-base-adaptive-column-height: 26px !default; - $material-grid-base-treeview-search-margin: 10px !default; $material-grid-base-nodata-font-size: 14px !default; $material-command-edit-icon-size: 16px !default; $material-filter-row-between-editor-padding: 8px !default; - $material-command-column-additional-right-margin: 2px !default; $material-grid-base-treeview-select-all-item-offset: 35px !default; $material-grid-base-command-ai-chatsparkleoutline-icon-size: 18px !default; diff --git a/packages/devextreme-scss/scss/widgets/material/gridBase/_variables.scss b/packages/devextreme-scss/scss/widgets/material/gridBase/_variables.scss index 9ad1220cd023..4014da737971 100644 --- a/packages/devextreme-scss/scss/widgets/material/gridBase/_variables.scss +++ b/packages/devextreme-scss/scss/widgets/material/gridBase/_variables.scss @@ -2,9 +2,9 @@ @use "colors" as *; @use "sizes" as *; -$grid-masterdetail-padding: 16px; -$material-grid-base-border-color: $datagrid-border-color; -$material-grid-base-header-cell-vertical-padding: math.round(math.div($material-grid-base-header-height - $material-grid-base-header-line-height, 2)); -$material-grid-base-command-select-column-width: $material-grid-base-checkbox-size + 2 * $material-grid-base-cell-horizontal-padding; -$material-grid-base-cell-vertical-padding: math.round(math.div($material-grid-base-cell-height - $material-grid-base-cell-line-height, 2)); -$material-grid-base-cell-padding: $material-grid-base-cell-vertical-padding $material-grid-base-cell-horizontal-padding; +$grid-masterdetail-padding: 16px !default; +$material-grid-base-border-color: $datagrid-border-color !default; +$material-grid-base-header-cell-vertical-padding: math.round(math.div($material-grid-base-header-height - $material-grid-base-header-line-height, 2)) !default; +$material-grid-base-command-select-column-width: $material-grid-base-checkbox-size + 2 * $material-grid-base-cell-horizontal-padding !default; +$material-grid-base-cell-vertical-padding: math.round(math.div($material-grid-base-cell-height - $material-grid-base-cell-line-height, 2)) !default; +$material-grid-base-cell-padding: $material-grid-base-cell-vertical-padding $material-grid-base-cell-horizontal-padding !default; diff --git a/packages/devextreme-scss/scss/widgets/material/gridBase/layout/aiChat/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/gridBase/layout/aiChat/_sizes.scss index a5cd8a835bab..c8f045a350f9 100644 --- a/packages/devextreme-scss/scss/widgets/material/gridBase/layout/aiChat/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/gridBase/layout/aiChat/_sizes.scss @@ -1,3 +1,3 @@ @use "../../../sizes" as *; -$ai-chat-message-icon-size: $material-base-icon-size - 4px; +$ai-chat-message-icon-size: $material-base-icon-size - 4px !default; diff --git a/packages/devextreme-scss/scss/widgets/material/htmlEditor/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/htmlEditor/_sizes.scss index 03c12ee590fa..adbf5ec48cd4 100644 --- a/packages/devextreme-scss/scss/widgets/material/htmlEditor/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/htmlEditor/_sizes.scss @@ -4,7 +4,7 @@ $material-toolbar-size-editor-width: null !default; $material-htmleditor-toolbar-padding: null !default; $material-html-editor-add-image-dialog-fileuploader-padding: null !default; -$material-aidialog-selects-gap: null !default; +$material-aidialog-selects-gap: 8px !default; $material-aidialog-content-gap: null !default; $material-aidialog-content-padding: null !default; $material-aidialog-toolbar-padding: null !default; @@ -14,7 +14,6 @@ $material-aidialog-border-radius: $base-border-radius * 2 !default; $material-toolbar-size-editor-width: 120px !default; $material-htmleditor-toolbar-padding: 16px !default; $material-html-editor-add-image-dialog-fileuploader-padding: 50px 40px !default; - $material-aidialog-selects-gap: 8px !default; $material-aidialog-content-gap: 24px !default; $material-aidialog-content-padding: 24px !default; $material-aidialog-toolbar-padding: 0 24px 16px !default; @@ -24,7 +23,6 @@ $material-aidialog-border-radius: $base-border-radius * 2 !default; $material-toolbar-size-editor-width: 90px !default; $material-htmleditor-toolbar-padding: 11px !default; $material-html-editor-add-image-dialog-fileuploader-padding: 40px 30px !default; - $material-aidialog-selects-gap: 8px !default; $material-aidialog-content-gap: 16px !default; $material-aidialog-content-padding: 16px !default; $material-aidialog-toolbar-padding: 0 16px 8px !default; diff --git a/packages/devextreme-scss/scss/widgets/material/icons/_index.scss b/packages/devextreme-scss/scss/widgets/material/icons/_index.scss index 4e194c685efb..1c9b359a0ede 100644 --- a/packages/devextreme-scss/scss/widgets/material/icons/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/icons/_index.scss @@ -19,12 +19,6 @@ } } -.dx-scrollview-pulldown { - background-image: $scrollview-pulldown-path; - background-position: 0 0; - background-repeat: no-repeat; -} - .dx-loadindicator-image { background-position: center center; background-repeat: no-repeat; diff --git a/packages/devextreme-scss/scss/widgets/material/informer/_colors.scss b/packages/devextreme-scss/scss/widgets/material/informer/_colors.scss index d8059cab008d..fd8e7ba5a2e7 100644 --- a/packages/devextreme-scss/scss/widgets/material/informer/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/material/informer/_colors.scss @@ -7,11 +7,11 @@ $informer-info-color: $base-text-color !default; $informer-info-background-color: null !default; @if $mode == "light" { - $informer-error-background-color: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: 1deg, $saturation: -24.92%, $lightness: 38.24%); - $informer-info-background-color: extcolor.adjust-if-not-achromatic($color: $base-text-color, $hue: -240deg, $saturation: -100.00%, $lightness: 48.63%); + $informer-error-background-color: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: 1deg, $saturation: -24.92%, $lightness: 38.24%) !default; + $informer-info-background-color: extcolor.adjust-if-not-achromatic($color: $base-text-color, $hue: -240deg, $saturation: -100.00%, $lightness: 48.63%) !default; } @if $mode == "dark" { - $informer-error-background-color: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: 1deg, $saturation: -41.80%, $lightness: -53.92%); - $informer-info-background-color: extcolor.adjust-if-not-achromatic($color: $base-text-color, $hue: 240deg, $saturation: 6.04%, $lightness: -70.78%); + $informer-error-background-color: extcolor.adjust-if-not-achromatic($color: $base-danger, $hue: 1deg, $saturation: -41.80%, $lightness: -53.92%) !default; + $informer-info-background-color: extcolor.adjust-if-not-achromatic($color: $base-text-color, $hue: 240deg, $saturation: 6.04%, $lightness: -70.78%) !default; } diff --git a/packages/devextreme-scss/scss/widgets/material/list/_index.scss b/packages/devextreme-scss/scss/widgets/material/list/_index.scss index 0394033ee1eb..1f6f61db64c5 100644 --- a/packages/devextreme-scss/scss/widgets/material/list/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/list/_index.scss @@ -13,11 +13,11 @@ @use "../radioGroup/colors" as *; @use "../badge"; -$material-list-item-border-width: 1px; -$material-list-border-width: 1px; -$material-list-border: $material-list-border-width solid; -$material-list-item-border: $material-list-item-border-width solid; -$material-list-searchbox-padding-top: $material-list-searchbox-vertical-padding - 2px; +$material-list-item-border-width: 1px !default; +$material-list-border-width: 1px !default; +$material-list-border: $material-list-border-width solid !default; +$material-list-item-border: $material-list-item-border-width solid !default; +$material-list-searchbox-padding-top: $material-list-searchbox-vertical-padding - 2px !default; .dx-list-item-chevron { @include dx-icon(chevronnext); diff --git a/packages/devextreme-scss/scss/widgets/material/menu/_index.scss b/packages/devextreme-scss/scss/widgets/material/menu/_index.scss index 86d8eec7b186..74feb67ec42e 100644 --- a/packages/devextreme-scss/scss/widgets/material/menu/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/menu/_index.scss @@ -9,7 +9,7 @@ @use "../menuBase" as *; @use "../../base/menu"; -$material-menu-font-size: $material-base-font-size; +$material-menu-font-size: $material-base-font-size !default; .dx-context-menu-container-border { background-color: transparent; diff --git a/packages/devextreme-scss/scss/widgets/material/menuBase/_index.scss b/packages/devextreme-scss/scss/widgets/material/menuBase/_index.scss index c58fb28cc838..e9cff178ebc9 100644 --- a/packages/devextreme-scss/scss/widgets/material/menuBase/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/menuBase/_index.scss @@ -8,7 +8,7 @@ @use "../../base/mixins" as *; @use "../../base/menuBase"; -$material-menu-arrow-image-size: 7px; +$material-menu-arrow-image-size: 7px !default; .dx-menu-item { color: $menu-color; diff --git a/packages/devextreme-scss/scss/widgets/material/menuBase/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/menuBase/_sizes.scss index cbcdc4c94b66..697bd594e2e1 100644 --- a/packages/devextreme-scss/scss/widgets/material/menuBase/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/menuBase/_sizes.scss @@ -3,7 +3,7 @@ $material-menu-item-line-height: null !default; $material-menu-item-with-icon-horizontal-padding: null !default; $material-menu-horizontal-padding: null !default; -$material-menu-icon-to-text-offset: 8px; +$material-menu-icon-to-text-offset: 8px !default; @if $size == "default" { $material-menu-item-line-height: 18px !default; diff --git a/packages/devextreme-scss/scss/widgets/material/pivotGrid/_index.scss b/packages/devextreme-scss/scss/widgets/material/pivotGrid/_index.scss index e8f0a912e74c..fedacbe636cf 100644 --- a/packages/devextreme-scss/scss/widgets/material/pivotGrid/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/pivotGrid/_index.scss @@ -32,12 +32,12 @@ $pivotgrid-button-top-padding: $pivotgrid-button-top-padding, ); -$material-pivotgrid-headers-font-size: 12px; -$material-pivotgrid-area-font-size: 13px; -$material-pivotgrid-chevron-icon-size: 20px; +$material-pivotgrid-headers-font-size: 12px !default; +$material-pivotgrid-area-font-size: 13px !default; +$material-pivotgrid-chevron-icon-size: 20px !default; -$material-pivotgrid-drag-header-shadow: 0 3px 4px color.change($base-shadow-color, $alpha: 0.15); -$material-pivotgridfieldchooser-field-min-height: 36px; +$material-pivotgrid-drag-header-shadow: 0 3px 4px color.change($base-shadow-color, $alpha: 0.15) !default; +$material-pivotgridfieldchooser-field-min-height: 36px !default; .dx-pivotgrid-fields-container { &.dx-drag { diff --git a/packages/devextreme-scss/scss/widgets/material/popup/_index.scss b/packages/devextreme-scss/scss/widgets/material/popup/_index.scss index 0082098d5238..bfbb2902eed0 100644 --- a/packages/devextreme-scss/scss/widgets/material/popup/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/popup/_index.scss @@ -8,7 +8,7 @@ @use "../../base/popup"; @use "../overlay"; -$material-popup-toolbar-label-font-size: $material-m-font-size; +$material-popup-toolbar-label-font-size: $material-m-font-size !default; .dx-popup-wrapper { & > .dx-overlay-content { diff --git a/packages/devextreme-scss/scss/widgets/material/progressBar/_index.scss b/packages/devextreme-scss/scss/widgets/material/progressBar/_index.scss index 58b93ad7bfc8..66c0e933c0fa 100644 --- a/packages/devextreme-scss/scss/widgets/material/progressBar/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/progressBar/_index.scss @@ -4,9 +4,9 @@ @use "../../base/mixins" as *; @use "../../base/progressBar"; -$material-bar-height: 4px; -$material-background-width: 90%; -$material-progressbar-interdeterminate-state-gradient: -90deg, transparent 5%, $progressbar-range-indeterminate-color, transparent 95%; +$material-bar-height: 4px !default; +$material-background-width: 90% !default; +$material-progressbar-interdeterminate-state-gradient: -90deg, transparent 5%, $progressbar-range-indeterminate-color, transparent 95% !default; .dx-progressbar-container { height: $material-bar-height; diff --git a/packages/devextreme-scss/scss/widgets/material/radioButton/_index.scss b/packages/devextreme-scss/scss/widgets/material/radioButton/_index.scss index 960a46991c2a..f42753264aab 100644 --- a/packages/devextreme-scss/scss/widgets/material/radioButton/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/radioButton/_index.scss @@ -6,7 +6,7 @@ @use "../radioGroup/colors" as *; @use "../../base/radioButton"; -$material-radiobutton-ripple-transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1); +$material-radiobutton-ripple-transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1) !default; @include dx-radiobutton-icon-mixin( $material-radiobutton-size, diff --git a/packages/devextreme-scss/scss/widgets/material/rangeSlider/_index.scss b/packages/devextreme-scss/scss/widgets/material/rangeSlider/_index.scss index 583170ede72f..d73e48a91ceb 100644 --- a/packages/devextreme-scss/scss/widgets/material/rangeSlider/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/rangeSlider/_index.scss @@ -2,7 +2,7 @@ @use "sizes" as *; @use "../../base/rangeSlider"; -$material-slider-handle-margin: -14px; +$material-slider-handle-margin: -14px !default; .dx-rangeslider-start-handle { margin-inline-start: $material-slider-handle-margin; diff --git a/packages/devextreme-scss/scss/widgets/material/recurrenceEditor/_index.scss b/packages/devextreme-scss/scss/widgets/material/recurrenceEditor/_index.scss index 2ca39d753187..48e26a618efb 100644 --- a/packages/devextreme-scss/scss/widgets/material/recurrenceEditor/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/recurrenceEditor/_index.scss @@ -4,9 +4,9 @@ @use "../scheduler/sizes" as *; @use "../../base/recurrenceEditor" as *; -$material-recurrence-number-box-margin: 11px; -$material-recurrence-button-group-padding: 6px; -$material-recurrence-radio-group-margin: 15px; +$material-recurrence-number-box-margin: 11px !default; +$material-recurrence-button-group-padding: 6px !default; +$material-recurrence-radio-group-margin: 15px !default; .dx-recurrence-numberbox-interval { margin-right: $material-recurrence-number-box-margin; diff --git a/packages/devextreme-scss/scss/widgets/material/scheduler/_colors.scss b/packages/devextreme-scss/scss/widgets/material/scheduler/_colors.scss index 1d3349f02d9d..1468a9d68cfb 100644 --- a/packages/devextreme-scss/scss/widgets/material/scheduler/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/material/scheduler/_colors.scss @@ -35,7 +35,7 @@ $scheduler-appointment-base-color: $base-accent !default; * $type color */ $scheduler-appointment-start-color: color.change($base-shadow-color, $alpha: 0.3) !default; -$scheduler-appointment-active-color: $scheduler-appointment-start-color; +$scheduler-appointment-active-color: $scheduler-appointment-start-color !default; /** * $name 60. Focused cell color @@ -74,8 +74,8 @@ $scheduler-fill-focused-appointment: false !default; $scheduler-is-shadow-color-for-focused-state: true !default; $scheduler-time-indicator-background-color: rgba(0, 0, 0, 0.03) !default; $scheduler-first-month-cell-background-color: color.adjust($scheduler-appointment-base-color, $alpha: -0.85) !default; -$material-scheduler-toolbar-color: $base-accent; -$material-scheduler-toolbar-active-color: color.change($base-accent, $alpha: 0.08); +$material-scheduler-toolbar-color: $base-accent !default; +$material-scheduler-toolbar-active-color: color.change($base-accent, $alpha: 0.08) !default; $scheduler-tooltip-appointment-text-color: $base-text-color !default; $scheduler-appointment-overlay-bg: $base-bg !default; diff --git a/packages/devextreme-scss/scss/widgets/material/scheduler/_index.scss b/packages/devextreme-scss/scss/widgets/material/scheduler/_index.scss index e655da5b8b04..7b42c6fb901a 100644 --- a/packages/devextreme-scss/scss/widgets/material/scheduler/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/scheduler/_index.scss @@ -53,17 +53,17 @@ @use "../recurrenceEditor" as *; @use "../scrollable"; -$scheduler-appointment-recurrence-content-padding: 5px 25px 5px 7px; -$scheduler-appointment-recurrence-content-padding-rtl: 5px 7px 5px 25px; +$scheduler-appointment-recurrence-content-padding: 5px 25px 5px 7px !default; +$scheduler-appointment-recurrence-content-padding-rtl: 5px 7px 5px 25px !default; -$material-scheduler-all-day-table-cell-height: $material-scheduler-workspace-date-table-cell-height * 1.5; -$material-scheduler-collapsed-all-day-table-cell-height: $material-scheduler-workspace-date-table-cell-height * 0.5; +$material-scheduler-all-day-table-cell-height: $material-scheduler-workspace-date-table-cell-height * 1.5 !default; +$material-scheduler-collapsed-all-day-table-cell-height: $material-scheduler-workspace-date-table-cell-height * 0.5 !default; -$material-scheduler-month-date-text-size: 14px; +$material-scheduler-month-date-text-size: 14px !default; -$scheduler-first-of-month-border: 1px solid; +$scheduler-first-of-month-border: 1px solid !default; -$material-scheduler-agenda-time-panel-cell-padding: 8px; +$material-scheduler-agenda-time-panel-cell-padding: 8px !default; @include baseScheduler.scheduler-mixin(); diff --git a/packages/devextreme-scss/scss/widgets/material/scheduler/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/scheduler/_sizes.scss index 26815ff6d53b..d8178c42da4d 100644 --- a/packages/devextreme-scss/scss/widgets/material/scheduler/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/scheduler/_sizes.scss @@ -4,45 +4,43 @@ $material-scheduler-workspace-date-table-cell-height: null !default; $material-scheduler-toolbar-item-offset: null !default; -$material-scheduler-navigator-height: null !default; $material-scheduler-header-panel-day-font-size: null !default; $material-scheduler-appointment-tooltip-title-font-size: null !default; $material-scheduler-appointment-tooltip-date-font-size: null !default; $material-scheduler-workspace-month-cell-left-offset: null !default; $material-scheduler-header-panel-table-cell-height: null !default; $material-scheduler-appointment-content-padding: null !default; -$material-scheduler-dropdown-appointment-margin-top: null !default; $material-scheduler-dropdown-button-height: null !default; $material-scheduler-dropdown-button-padding: null !default; $material-scheduler-dropdown-button-margin: null !default; $material-scheduler-dropdown-button-vertical-margin: null !default; -$material-scheduler-dropdown-button-adaptive-margin: null !default; -$material-scheduler-dropdown-button-adaptive-vertical-margin: null !default; +$material-scheduler-dropdown-button-adaptive-margin: 0 2px !default; +$material-scheduler-dropdown-button-adaptive-vertical-margin: 0 2px 0 1px !default; $material-scheduler-appointment-tooltip-content-margin-left: null !default; $material-tooltip-marker-dimension: null !default; $material-scheduler-appointment-tooltip-title-margin-top: null !default; $material-scheduler-appointment-tooltip-remove-padding-top: null !default; -$material-scheduler-appointment-short-content-padding: null !default; -$material-scheduler-appointment-15min-height: null !default; -$material-scheduler-appointment-20min-height: null !default; -$material-scheduler-appointment-25min-height: null !default; -$material-scheduler-appointment-10min-recurrence-padding-right: null !default; -$material-scheduler-appointment-10min-title-font-size: null !default; -$material-scheduler-appointment-10min-title-line-height: null !default; -$material-scheduler-appointment-10min-icon-font-size: null !default; -$material-scheduler-appointment-10min-icon-right: null !default; +$material-scheduler-appointment-short-content-padding: 0 7px !default; +$material-scheduler-appointment-15min-height: 17px !default; +$material-scheduler-appointment-20min-height: 24px !default; +$material-scheduler-appointment-25min-height: 32px !default; +$material-scheduler-appointment-10min-recurrence-padding-right: 14px !default; +$material-scheduler-appointment-10min-title-font-size: 8px !default; +$material-scheduler-appointment-10min-title-line-height: 12px !default; +$material-scheduler-appointment-10min-icon-font-size: 12px !default; +$material-scheduler-appointment-10min-icon-right: 2px !default; $material-scheduler-appointment-15min-recurrence-padding-right: null !default; -$material-scheduler-appointment-15min-title-font-size: null !default; +$material-scheduler-appointment-15min-title-font-size: 12px !default; $material-scheduler-appointment-15min-title-line-height: null !default; $material-scheduler-appointment-15min-icon-font-size: null !default; -$material-scheduler-appointment-15min-icon-right: null !default; -$material-scheduler-appointment-20min-recurrence-padding-right: null !default; -$material-scheduler-appointment-20min-icon-right: null !default; +$material-scheduler-appointment-15min-icon-right: 2px !default; +$material-scheduler-appointment-20min-recurrence-padding-right: 25px !default; +$material-scheduler-appointment-20min-icon-right: 4px !default; $scheduler-left-column-width: 65px !default; $material-scheduler-timeline-date-table-cell-height: 50px !default; -$material-scheduler-navigator-icon-font-size: $material-base-icon-size; +$material-scheduler-navigator-icon-font-size: $material-base-icon-size !default; $material-scheduler-appointment-month-text-size: 12px !default; $material-scheduler-workspace-info-text-size: 16px !default; $material-scheduler-all-day-title-font-size: 12px !default; @@ -80,7 +78,6 @@ $scheduler-timeline-cell-height: 50px !default; @if $size == "default" { $material-scheduler-workspace-date-table-cell-height: 38px !default; - $material-scheduler-navigator-height: 36px !default; $material-scheduler-header-panel-day-font-size: 30px !default; $material-scheduler-header-height: 54px !default; $material-scheduler-header-offset: 2px 16px !default; @@ -92,13 +89,10 @@ $scheduler-timeline-cell-height: 50px !default; $material-scheduler-header-panel-table-cell-height: 56px !default; $material-scheduler-appointment-content-padding: 5px 7px !default; - $material-scheduler-dropdown-appointment-margin-top: 7px !default; $material-scheduler-dropdown-button-height: 22px !default; $material-scheduler-dropdown-button-padding: 4px !default; $material-scheduler-dropdown-button-margin: 5px 1px 3px 3px !default; $material-scheduler-dropdown-button-vertical-margin: 5px 2px 3px 1px !default; - $material-scheduler-dropdown-button-adaptive-margin: 0 2px !default; - $material-scheduler-dropdown-button-adaptive-vertical-margin: 0 2px 0 1px !default; $material-scheduler-appointment-tooltip-content-margin-left: 10px !default; $material-tooltip-marker-dimension: 24px !default; @@ -120,33 +114,16 @@ $scheduler-timeline-cell-height: 50px !default; $material-scheduler-appointment-popup-days-of-week-buttons-gap: 8px !default; $material-scheduler-appointment-popup-repeat-end-item-height: 48px !default; - $agenda-appointment-title-font-size: 14px; - - $material-scheduler-appointment-short-content-padding: 0 7px !default; - $material-scheduler-appointment-15min-height: 17px !default; - $material-scheduler-appointment-20min-height: 24px !default; - $material-scheduler-appointment-25min-height: 32px !default; - - $material-scheduler-appointment-10min-recurrence-padding-right: 14px !default; - $material-scheduler-appointment-10min-title-font-size: 8px !default; - $material-scheduler-appointment-10min-title-line-height: 12px !default; - $material-scheduler-appointment-10min-icon-font-size: 12px !default; - $material-scheduler-appointment-10min-icon-right: 2px !default; + $agenda-appointment-title-font-size: 14px !default; $material-scheduler-appointment-15min-recurrence-padding-right: 18px !default; - $material-scheduler-appointment-15min-title-font-size: 12px !default; $material-scheduler-appointment-15min-title-line-height: 18px !default; $material-scheduler-appointment-15min-icon-font-size: 16px !default; - $material-scheduler-appointment-15min-icon-right: 2px !default; - - $material-scheduler-appointment-20min-recurrence-padding-right: 25px !default; - $material-scheduler-appointment-20min-icon-right: 4px !default; } @else if $size == "compact" { $material-scheduler-workspace-date-table-cell-height: 28px !default; - $material-scheduler-navigator-height: 28px !default; $material-scheduler-header-panel-day-font-size: 20px !default; $material-scheduler-header-height: 38px !default; $material-scheduler-header-offset: 0 8px !default; @@ -158,13 +135,10 @@ $scheduler-timeline-cell-height: 50px !default; $material-scheduler-header-panel-table-cell-height: 40px !default; $material-scheduler-appointment-content-padding: 2px 4px !default; - $material-scheduler-dropdown-appointment-margin-top: 3px !default; $material-scheduler-dropdown-button-height: 18px !default; $material-scheduler-dropdown-button-padding: 2px !default; $material-scheduler-dropdown-button-margin: 2px 1px 2px 2px !default; $material-scheduler-dropdown-button-vertical-margin: 2px 1px 2px 1px !default; - $material-scheduler-dropdown-button-adaptive-margin: 0 2px !default; - $material-scheduler-dropdown-button-adaptive-vertical-margin: 0 2px 0 1px !default; $material-scheduler-appointment-tooltip-content-margin-left: 8px !default; $material-tooltip-marker-dimension: 18px !default; @@ -186,25 +160,9 @@ $scheduler-timeline-cell-height: 50px !default; $material-scheduler-appointment-popup-days-of-week-buttons-gap: 4px !default; $material-scheduler-appointment-popup-repeat-end-item-height: 32px !default; - $agenda-appointment-title-font-size: 13px; - - $material-scheduler-appointment-short-content-padding: 0 7px !default; - $material-scheduler-appointment-15min-height: 17px !default; - $material-scheduler-appointment-20min-height: 24px !default; - $material-scheduler-appointment-25min-height: 32px !default; - - $material-scheduler-appointment-10min-recurrence-padding-right: 14px !default; - $material-scheduler-appointment-10min-title-font-size: 8px !default; - $material-scheduler-appointment-10min-title-line-height: 12px !default; - $material-scheduler-appointment-10min-icon-font-size: 12px !default; - $material-scheduler-appointment-10min-icon-right: 2px !default; + $agenda-appointment-title-font-size: 13px !default; $material-scheduler-appointment-15min-recurrence-padding-right: 14px !default; - $material-scheduler-appointment-15min-title-font-size: 12px !default; $material-scheduler-appointment-15min-title-line-height: 12px !default; $material-scheduler-appointment-15min-icon-font-size: 12px !default; - $material-scheduler-appointment-15min-icon-right: 2px !default; - - $material-scheduler-appointment-20min-recurrence-padding-right: 25px !default; - $material-scheduler-appointment-20min-icon-right: 4px !default; } diff --git a/packages/devextreme-scss/scss/widgets/material/scrollView/_index.scss b/packages/devextreme-scss/scss/widgets/material/scrollView/_index.scss index b91875142119..ea4f3b80cd11 100644 --- a/packages/devextreme-scss/scss/widgets/material/scrollView/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/scrollView/_index.scss @@ -6,7 +6,7 @@ @use "../scrollable"; @use "../../base/scrollView"; -$scrollview-loading-indicator-size: 42px; +$scrollview-loading-indicator-size: 42px !default; .dx-scrollview-scrollbottom-text, .dx-scrollview-pull-down-text { diff --git a/packages/devextreme-scss/scss/widgets/material/scrollable/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/scrollable/_sizes.scss index 28567ffc4f8a..08d5cbb4b37b 100644 --- a/packages/devextreme-scss/scss/widgets/material/scrollable/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/scrollable/_sizes.scss @@ -1,2 +1,2 @@ -$material-scrollbar-size-thin: 6px; -$material-scrollbar-size: 13px; +$material-scrollbar-size-thin: 6px !default; +$material-scrollbar-size: 13px !default; diff --git a/packages/devextreme-scss/scss/widgets/material/slider/_index.scss b/packages/devextreme-scss/scss/widgets/material/slider/_index.scss index 36bf203145da..9c819f8ec0b2 100644 --- a/packages/devextreme-scss/scss/widgets/material/slider/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/slider/_index.scss @@ -6,11 +6,11 @@ @use "../../material/tooltip/colors" as *; @use "../../base/slider"; -$material-slider-radius: 50%; -$material-slider-handle-active-border-width: 2px; -$material-slider-handle-inner-size: 12px; -$material-slider-handle-inner-border-width: 1px; -$material-slider-handle-inner-disabled-size: 2px; +$material-slider-radius: 50% !default; +$material-slider-handle-active-border-width: 2px !default; +$material-slider-handle-inner-size: 12px !default; +$material-slider-handle-inner-border-width: 1px !default; +$material-slider-handle-inner-disabled-size: 2px !default; .dx-slider-wrapper { height: $material-slider-wrapper-height; diff --git a/packages/devextreme-scss/scss/widgets/material/slider/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/slider/_sizes.scss index 6a7fa38a0fdd..9b1763a9f246 100644 --- a/packages/devextreme-scss/scss/widgets/material/slider/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/slider/_sizes.scss @@ -3,7 +3,6 @@ $material-slider-handle-size: null !default; $material-slider-wrapper-height: null !default; $material-slider-tooltip-width: null !default; -$material-slider-top-tooltip-margin: null !default; $material-slider-tooltip-popup-content-padding: null !default; $material-slider-track-height: 2px !default; @@ -13,7 +12,6 @@ $material-slider-height: 28px !default; $material-slider-handle-size: 32px !default; $material-slider-wrapper-height: 32px !default; $material-slider-tooltip-width: 34px !default; - $material-slider-top-tooltip-margin: -5px !default; $material-slider-tooltip-popup-content-padding: 5px 7px !default; } @@ -21,6 +19,5 @@ $material-slider-height: 28px !default; $material-slider-handle-size: 26px !default; $material-slider-wrapper-height: 26px !default; $material-slider-tooltip-width: 28px !default; - $material-slider-top-tooltip-margin: -2px !default; $material-slider-tooltip-popup-content-padding: 3px 5px !default; } diff --git a/packages/devextreme-scss/scss/widgets/material/speedDialAction/_index.scss b/packages/devextreme-scss/scss/widgets/material/speedDialAction/_index.scss index ac08160d72be..615c8df0cf78 100644 --- a/packages/devextreme-scss/scss/widgets/material/speedDialAction/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/speedDialAction/_index.scss @@ -5,9 +5,9 @@ @use "../button/colors" as *; @use "../overlay"; -$material-fa-button-size: 40px; -$material-fa-button-label-offset: 16px; -$material-fa-button-label-padding: 4px 8px; +$material-fa-button-size: 40px !default; +$material-fa-button-label-offset: 16px !default; +$material-fa-button-label-padding: 4px 8px !default; @include dx-fa-button-mixin( $material-main-fa-button-size, diff --git a/packages/devextreme-scss/scss/widgets/material/speedDialAction/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/speedDialAction/_sizes.scss index bc68610c12b7..ffdd306ceace 100644 --- a/packages/devextreme-scss/scss/widgets/material/speedDialAction/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/speedDialAction/_sizes.scss @@ -3,16 +3,12 @@ $material-main-fa-button-label-padding: null !default; $material-main-fa-button-size: null !default; -$material-fa-button-label-font-size: null !default; - @if $size == "default" { $material-main-fa-button-size: 56px !default; - $material-main-fa-button-label-padding: 0 24px 0 8px; - $material-fa-button-label-font-size: 14px; + $material-main-fa-button-label-padding: 0 24px 0 8px !default; } @else if $size == "compact" { $material-main-fa-button-size: 42px !default; - $material-main-fa-button-label-padding: 0 20px 0 8px; - $material-fa-button-label-font-size: 12px; + $material-main-fa-button-label-padding: 0 20px 0 8px !default; } diff --git a/packages/devextreme-scss/scss/widgets/material/splitter/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/splitter/_sizes.scss index 3af001d1096d..a97016b18725 100644 --- a/packages/devextreme-scss/scss/widgets/material/splitter/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/splitter/_sizes.scss @@ -1 +1 @@ -$splitter-resize-handle-icon-size: 24px; +$splitter-resize-handle-icon-size: 24px !default; diff --git a/packages/devextreme-scss/scss/widgets/material/switch/_index.scss b/packages/devextreme-scss/scss/widgets/material/switch/_index.scss index 9b7f631c40ef..59f2347d69ed 100644 --- a/packages/devextreme-scss/scss/widgets/material/switch/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/switch/_index.scss @@ -6,10 +6,10 @@ @use "../../base/switch"; @use "../../base/switch/mixins" as *; -$material-switch-transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1); -$material-switch-handle-on-shadow: 0 0 0 $material-switch-shadow-blur-radius color.change($switch-handle-on-bg, $alpha: 0.1); -$material-switch-shadow: 0 0 0 $material-switch-shadow-blur-radius color.change($base-text-color, $alpha: 0.1); -$material-invalid-switch-shadow: 0 0 0 $material-switch-shadow-blur-radius color.change($switch-invalid-handle-bg, $alpha: 0.1); +$material-switch-transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1) !default; +$material-switch-handle-on-shadow: 0 0 0 $material-switch-shadow-blur-radius color.change($switch-handle-on-bg, $alpha: 0.1) !default; +$material-switch-shadow: 0 0 0 $material-switch-shadow-blur-radius color.change($base-text-color, $alpha: 0.1) !default; +$material-invalid-switch-shadow: 0 0 0 $material-switch-shadow-blur-radius color.change($switch-invalid-handle-bg, $alpha: 0.1) !default; @include dx-switch( $material-switch-width, diff --git a/packages/devextreme-scss/scss/widgets/material/switch/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/switch/_sizes.scss index edc230fb821e..5688f2501bf0 100644 --- a/packages/devextreme-scss/scss/widgets/material/switch/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/switch/_sizes.scss @@ -3,28 +3,25 @@ $material-switch-width: null !default; $switch-padding: 10px !default; $switch-container-vertical-margin: null !default; -$switch-container-horizontal-padding: null !default; $material-switch-height: null !default; -$switch-handle-height: null !default; $switch-container-height: 14px !default; $switch-handle-margin: 0 !default; $material-switch-shadow-blur-radius: 10px !default; $material-switch-handle-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.2) !default; $material-switch-container-adjustment: 6px !default; +$switch-container-horizontal-padding: $switch-handle-margin !default; @if $size == "default" { $material-switch-width: 36px !default; $switch-container-vertical-margin: 3px !default; - $switch-container-horizontal-padding: $switch-handle-margin !default; $material-switch-height: 20px !default; - $switch-handle-height: $material-switch-height !default; } @else if $size == "compact" { $material-switch-width: 32px !default; $switch-container-vertical-margin: 2px !default; - $switch-container-horizontal-padding: $switch-handle-margin !default; $material-switch-height: 18px !default; - $switch-handle-height: $material-switch-height !default; } + +$switch-handle-height: $material-switch-height !default; diff --git a/packages/devextreme-scss/scss/widgets/material/tabs/variables/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/tabs/variables/_sizes.scss index 97c92422d3a9..c50cef3909cf 100644 --- a/packages/devextreme-scss/scss/widgets/material/tabs/variables/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/tabs/variables/_sizes.scss @@ -1,21 +1,20 @@ @use "../../sizes" as *; $material-tabs-item-gap: 4px !default; -$material-tabs-item-padding: null !default; $material-tabs-item-padding-horizontal: null !default; $material-tabs-item-padding-vertical: null !default; -$material-tab-height: null !default; +$material-tab-height: auto !default; $material-tab-min-width: null !default; $material-tab-text-line-height: 1.714286 !default; $material-base-icon-margin: null !default; -$material-tab-nav-button-icon-width: null !default; -$material-tab-nav-button-icon-height: null !default; -$material-tab-nav-button-icon-font-size: null !default; -$material-tab-nav-button-content-padding: null !default; +$material-tab-nav-button-icon-width: 24px !default; +$material-tab-nav-button-icon-height: 24px !default; +$material-tab-nav-button-icon-font-size: 24px !default; +$material-tab-nav-button-content-padding: 0 4px !default; -$material-tab-vertical-nav-button-content-padding: null !default; +$material-tab-vertical-nav-button-content-padding: 4px 0 !default; $material-indicator-thickness-primary: 2px !default; $material-indicator-thickness-secondary: 3px !default; @@ -30,27 +29,15 @@ $material-tabs-icon-size: $material-base-icon-size !default; @if $size == "default" { $material-tabs-item-padding-horizontal: 12px !default; $material-tabs-item-padding-vertical: 16px !default; - $material-tabs-item-padding: $material-tabs-item-padding-horizontal $material-tabs-item-padding-vertical !default; - $material-tab-height: auto !default; $material-tab-min-width: 90px !default; $material-base-icon-margin: 8px !default; - $material-tab-nav-button-icon-width: 24px !default; - $material-tab-nav-button-icon-height: 24px !default; - $material-tab-nav-button-icon-font-size: 24px !default; - $material-tab-nav-button-content-padding: 0 4px !default; - $material-tab-vertical-nav-button-content-padding: 4px 0 !default; } @else if $size == "compact" { $material-tabs-item-padding-horizontal: 6px !default; $material-tabs-item-padding-vertical: 12px !default; - $material-tabs-item-padding: $material-tabs-item-padding-horizontal $material-tabs-item-padding-vertical !default; - $material-tab-height: auto !default; $material-tab-min-width: 82px !default; $material-base-icon-margin: 7px !default; - $material-tab-nav-button-icon-width: 24px !default; - $material-tab-nav-button-icon-height: 24px !default; - $material-tab-nav-button-icon-font-size: 24px !default; - $material-tab-nav-button-content-padding: 0 4px !default; - $material-tab-vertical-nav-button-content-padding: 4px 0 !default; } + +$material-tabs-item-padding: $material-tabs-item-padding-horizontal $material-tabs-item-padding-vertical !default; diff --git a/packages/devextreme-scss/scss/widgets/material/tagBox/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/tagBox/_sizes.scss index af20f502f158..0a55098aa46e 100644 --- a/packages/devextreme-scss/scss/widgets/material/tagBox/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/tagBox/_sizes.scss @@ -4,7 +4,6 @@ $material-tagbox-tag-content-padding-left: null !default; $material-tagbox-tag-content-padding-right: null !default; $material-tagbox-tag-content-vertical-padding: null !default; $material-tagbox-tag-content-font-size: null !default; -$material-tagbox-tag-container-padding: null !default; $material-tagbox-tag-container-padding-left: null !default; $material-tagbox-tag-container-vertical-padding: null !default; $material-tagbox-tag-height: null !default; @@ -15,21 +14,19 @@ $material-tagbox-select-all-padding-bottom: 12px !default; $material-tagbox-select-all-margin-bottom: 3px !default; $tagbox-tag-border-radius: 2em !default; $material-tagbox-filled-with-label-top-padding: null !default; -$material-tagbox-filled-with-label-bottom-padding: null !default; +$material-tagbox-filled-with-label-bottom-padding: 4px !default; @if $size == "default" { $material-tagbox-tag-content-padding-left: 8px !default; $material-tagbox-tag-content-padding-right: 30px !default; $material-tagbox-tag-content-vertical-padding: 4px !default; $material-tagbox-tag-content-font-size: 14px !default; - $material-tagbox-tag-container-padding: 0 12px 11px 16px !default; $material-tagbox-tag-container-padding-left: 16px !default; $material-tagbox-tag-container-vertical-padding: 11px !default; $material-tagbox-tag-height: 26px !default; $material-tagbox-remove-button-width: 26px !default; $material-tagbox-remove-button-icon-size: 16px !default; $material-tagbox-filled-with-label-top-padding: 18px !default; - $material-tagbox-filled-with-label-bottom-padding: 4px !default; } @else if $size == "compact" { @@ -37,12 +34,10 @@ $material-tagbox-filled-with-label-bottom-padding: null !default; $material-tagbox-tag-content-padding-right: 20px !default; $material-tagbox-tag-content-vertical-padding: 2px !default; $material-tagbox-tag-content-font-size: 12px !default; - $material-tagbox-tag-container-padding: 0 8px 3px 11px !default; $material-tagbox-tag-container-padding-left: 12px !default; $material-tagbox-tag-container-vertical-padding: 7px !default; $material-tagbox-tag-height: 18px !default; $material-tagbox-remove-button-width: 18px !default; $material-tagbox-remove-button-icon-size: 12px !default; $material-tagbox-filled-with-label-top-padding: 10px !default; - $material-tagbox-filled-with-label-bottom-padding: 4px !default; } diff --git a/packages/devextreme-scss/scss/widgets/material/textBox/_index.scss b/packages/devextreme-scss/scss/widgets/material/textBox/_index.scss index dacb0d90e04e..da0905c94c67 100644 --- a/packages/devextreme-scss/scss/widgets/material/textBox/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/textBox/_index.scss @@ -8,15 +8,15 @@ @use "../../base/textBox"; @use "../textEditor" as *; -$textbox-search-icon-right-padding: math.div($material-filled-texteditor-input-horizontal-padding, 2); +$textbox-search-icon-right-padding: math.div($material-filled-texteditor-input-horizontal-padding, 2) !default; $textbox-standard-search-icon-size: $material-texteditor-icon-container-size + $material-base-standard-texteditor-input-horizontal-padding + - $textbox-search-icon-right-padding; + $textbox-search-icon-right-padding !default; $textbox-filled-search-icon-size: $material-texteditor-icon-container-size + $material-filled-texteditor-input-horizontal-padding + - $textbox-search-icon-right-padding; + $textbox-search-icon-right-padding !default; @mixin dx-texteditor-search-icon($texteditor-icon-size, $texteditor-padding) { position: absolute; diff --git a/packages/devextreme-scss/scss/widgets/material/textEditor/_colors.scss b/packages/devextreme-scss/scss/widgets/material/textEditor/_colors.scss index e05f1ab8f71e..e7495f66bedd 100644 --- a/packages/devextreme-scss/scss/widgets/material/textEditor/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/material/textEditor/_colors.scss @@ -13,7 +13,7 @@ $texteditor-color: $base-text-color !default; * $type color */ $texteditor-placeholder-color: $base-label-color !default; -$texteditor-label-color: $texteditor-placeholder-color; +$texteditor-label-color: $texteditor-placeholder-color !default; /** * $name 30. Background color @@ -59,7 +59,7 @@ $texteditor-input-border-radius: $base-border-radius !default; $texteditor-label-transition: font-size 0.2s cubic-bezier(0, 0, 0.2, 1) 0ms, transform 0.2s cubic-bezier(0, 0, 0.2, 1) 0ms, - top 0.2s cubic-bezier(0, 0, 0.2, 1) 0ms; + top 0.2s cubic-bezier(0, 0, 0.2, 1) 0ms !default; @if $mode == "light" { $texteditor-disabled-color: rgba(0, 0, 0, 0.6) !default; diff --git a/packages/devextreme-scss/scss/widgets/material/textEditor/_index.scss b/packages/devextreme-scss/scss/widgets/material/textEditor/_index.scss index ae152575c305..c9e40f7baa5b 100644 --- a/packages/devextreme-scss/scss/widgets/material/textEditor/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/textEditor/_index.scss @@ -13,10 +13,10 @@ @use "../../base/textEditor/mixins" as *; @use "../colorBox/sizes" as *; -$material-base-standard-texteditor-input-horizontal-padding: 0; -$material-texteditor-icon-container-size: $material-base-icon-size - 2px; -$material-texteditor-invalid-badge-size: $material-invalid-badge-size + 2 * $material-invalid-badge-horizontal-padding; -$material-editor-custom-button-margin: 5px; +$material-base-standard-texteditor-input-horizontal-padding: 0 !default; +$material-texteditor-icon-container-size: $material-base-icon-size - 2px !default; +$material-texteditor-invalid-badge-size: $material-invalid-badge-size + 2 * $material-invalid-badge-horizontal-padding !default; +$material-editor-custom-button-margin: 5px !default; @mixin dx-texteditor-icon() { position: absolute; diff --git a/packages/devextreme-scss/scss/widgets/material/textEditor/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/textEditor/_sizes.scss index 8082b872260d..1e206df56fc9 100644 --- a/packages/devextreme-scss/scss/widgets/material/textEditor/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/textEditor/_sizes.scss @@ -28,9 +28,9 @@ $material-filled-texteditor-input-with-label-bottom-padding: null !default; $material-texteditor-label-outside-top-offset: 24px !default; $material-filled-texteditor-input-horizontal-padding: 16px !default; $material-filled-texteditor-input-top-padding: 15px !default; - $material-filled-texteditor-input-with-label-top-padding: 21px; + $material-filled-texteditor-input-with-label-top-padding: 21px !default; $material-filled-texteditor-input-bottom-padding: 14px !default; - $material-filled-texteditor-input-with-label-bottom-padding: 8px; + $material-filled-texteditor-input-with-label-bottom-padding: 8px !default; $material-standard-texteditor-input-top-padding: 15px !default; $material-standard-texteditor-input-bottom-padding: 14px !default; $material-standard-texteditor-with-label-input-top-padding: 21px !default; @@ -47,10 +47,10 @@ $material-filled-texteditor-input-with-label-bottom-padding: null !default; $material-texteditor-label-outside-height: 16px !default; $material-texteditor-label-outside-top-offset: 18px !default; $material-filled-texteditor-input-horizontal-padding: 12px !default; - $material-filled-texteditor-input-top-padding: 9px; - $material-filled-texteditor-input-with-label-top-padding: 15px; - $material-filled-texteditor-input-bottom-padding: 9px; - $material-filled-texteditor-input-with-label-bottom-padding: 3px; + $material-filled-texteditor-input-top-padding: 9px !default; + $material-filled-texteditor-input-with-label-top-padding: 15px !default; + $material-filled-texteditor-input-bottom-padding: 9px !default; + $material-filled-texteditor-input-with-label-bottom-padding: 3px !default; $material-standard-texteditor-input-top-padding: 9px !default; $material-standard-texteditor-input-bottom-padding: 9px !default; $material-standard-texteditor-with-label-input-top-padding: 14px !default; @@ -68,5 +68,5 @@ $material-standard-texteditor-input-padding: $material-base-standard-texteditor-input-horizontal-padding $material-standard-texteditor-input-bottom-padding !default; -$material-filled-texteditor-input-button-horizontal-padding: $material-filled-texteditor-input-horizontal-padding - 4px; -$material-filled-texteditor-spin-button-horizontal-padding: $material-filled-texteditor-input-horizontal-padding - 8px; +$material-filled-texteditor-input-button-horizontal-padding: $material-filled-texteditor-input-horizontal-padding - 4px !default; +$material-filled-texteditor-spin-button-horizontal-padding: $material-filled-texteditor-input-horizontal-padding - 8px !default; diff --git a/packages/devextreme-scss/scss/widgets/material/treeView/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/treeView/_sizes.scss index a9a01c5e3df1..b1bdbb525bb6 100644 --- a/packages/devextreme-scss/scss/widgets/material/treeView/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/treeView/_sizes.scss @@ -4,11 +4,10 @@ $material-treeview-toggle-item-width: null !default; $material-treeview-checkbox-offset: null !default; $material-treeview-item-padding: null !default; $material-select-all-item-padding: null !default; -$material-treeview-item-height: null !default; $material-checkbox-container-size: null !default; $material-treeview-load-indicator-size: null !default; -$material-treeview-item-element-gap: null !default; +$material-treeview-item-element-gap: 12px !default; $material-treeview-searchbox-margin-bottom: null !default; $material-treeview-search-editor-height: null !default; @@ -20,30 +19,26 @@ $material-treeview-container-padding: $material-treeview-border-padding $material-treeview-border-padding $material-treeview-border-padding - $material-treeview-container-with-border-offset; + $material-treeview-container-with-border-offset !default; @if $size == "default" { - $material-treeview-item-element-gap: 12px !default; $material-treeview-toggle-item-width: 24px !default; $material-treeview-checkbox-offset: 55px !default; $material-select-all-item-padding: 15px $material-treeview-checkbox-offset !default; $material-checkbox-container-size: 24px !default; $material-treeview-item-padding: 12px $material-treeview-item-side-padding !default; - $material-treeview-item-height: 48px !default; $material-treeview-load-indicator-size: 24px !default; $material-treeview-searchbox-margin-bottom: 8px !default; - $material-treeview-search-editor-height: 48px; + $material-treeview-search-editor-height: 48px !default; } @else if $size == "compact" { - $material-treeview-item-element-gap: 12px !default; $material-treeview-toggle-item-width: 18px !default; $material-treeview-checkbox-offset: 47px !default; $material-select-all-item-padding: 10px $material-treeview-checkbox-offset !default; $material-treeview-item-padding: 9px $material-treeview-item-side-padding !default; - $material-treeview-item-height: 36px !default; $material-treeview-load-indicator-size: 18px !default; - $material-checkbox-container-size: 18px; + $material-checkbox-container-size: 18px !default; $material-treeview-searchbox-margin-bottom: 4px !default; - $material-treeview-search-editor-height: 32px; + $material-treeview-search-editor-height: 32px !default; } diff --git a/packages/devextreme-scss/scss/widgets/material/typography/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/typography/_sizes.scss index c9ab6f8f1224..8cf4574b658d 100644 --- a/packages/devextreme-scss/scss/widgets/material/typography/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/typography/_sizes.scss @@ -4,7 +4,7 @@ $material-xl-font-size: null !default; $material-l-font-size: null !default; $material-m-font-size: null !default; $material-s-font-size: null !default; -$material-xs-font-size: null !default; +$material-xs-font-size: 12px !default; $material-h1-font-size: null !default; $material-h2-font-size: null !default; $material-h3-font-size: null !default; @@ -17,7 +17,6 @@ $material-h6-font-size: null !default; $material-l-font-size: 28px !default; $material-m-font-size: 20px !default; $material-s-font-size: 18px !default; - $material-xs-font-size: 12px !default; $material-h1-font-size: 96px !default; $material-h2-font-size: 60px !default; @@ -32,7 +31,6 @@ $material-h6-font-size: null !default; $material-l-font-size: 20px !default; $material-m-font-size: 16px !default; $material-s-font-size: 14px !default; - $material-xs-font-size: 12px !default; $material-h1-font-size: 64px !default; $material-h2-font-size: 42px !default; diff --git a/packages/devextreme-scss/scss/widgets/material/validation/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/validation/_sizes.scss index ba97716f3337..5fec857c6d92 100644 --- a/packages/devextreme-scss/scss/widgets/material/validation/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/validation/_sizes.scss @@ -1,6 +1,5 @@ @use "../sizes" as *; -$material-validation-message-padding: null !default; $material-validationsummary-item-margin: null !default; $material-validation-message-line-height: null !default; @@ -10,7 +9,6 @@ $material-validation-overlay-border-radius: 4px !default; $material-validation-message-font-size: 12px !default; @if $size == "default" { - $material-validation-message-padding: 6px !default; $material-validationsummary-item-margin: 10px !default; $material-validation-message-horizontal-padding: 8px !default; @@ -18,7 +16,6 @@ $material-validation-message-font-size: 12px !default; } @else if $size == "compact" { - $material-validation-message-padding: 4px !default; $material-validationsummary-item-margin: 6px !default; $material-validation-message-horizontal-padding: 6px !default; diff --git a/packages/devextreme-scss/tests/module-variable-default.test.ts b/packages/devextreme-scss/tests/module-variable-default.test.ts new file mode 100644 index 000000000000..92edb88505f2 --- /dev/null +++ b/packages/devextreme-scss/tests/module-variable-default.test.ts @@ -0,0 +1,90 @@ +import { createRunner, scss } from './stylelint-rule'; + +const ruleName = 'dx/module-variable-default'; +const { lint, fix } = createRunner('module-variable-default.mjs', ruleName); + +test('a module-level variable without the flag is reported, at the top level and inside @if branches', () => { + const { warnings } = lint('missing.scss', scss( + '$top: 7px;', + '$flagged: 8px !default;', + '$global: 9px !global;', + '', + '@if $size == "default" {', + ' $branch: 1px;', + '}', + '', + '@else {', + ' $branch: 2px;', + '}', + )); + + expect(warnings).toEqual([ + { line: 1, rule: ruleName, text: `Expected the !default flag on "$top": without it a value passed through @use … with() is ignored (${ruleName})` }, + { line: 6, rule: ruleName, text: `Expected the !default flag on "$branch": without it a value passed through @use … with() is ignored (${ruleName})` }, + { line: 10, rule: ruleName, text: `Expected the !default flag on "$branch": without it a value passed through @use … with() is ignored (${ruleName})` }, + ]); +}); + +test('variables local to a mixin, function or rule are not module variables', () => { + const { warnings } = lint('local.scss', scss( + '@mixin size($base) {', + ' $double: $base * 2;', + ' width: $double;', + '}', + '', + '@function half($value) {', + ' $result: $value / 2;', + ' @return $result;', + '}', + '', + '.dx-widget {', + ' $gap: 4px;', + ' margin: $gap;', + '', + ' @if $size == "compact" {', + ' $gap: 2px;', + ' }', + '}', + )); + + expect(warnings).toEqual([]); +}); + +test('reassigning a variable that already holds a value is not a default', () => { + const { warnings } = lint('reassign.scss', scss( + '$offset: 4px !default;', + '$offset: $offset + 1px;', + '', + '$late: null !default;', + '$late: 2px;', + )); + + expect(warnings.map((warning) => warning.line)).toEqual([5]); +}); + +test('--fix appends the flag, also to a multi-line value', () => { + const { warnings, output } = fix('fix.scss', scss( + '$top: 7px;', + '$map: (', + ' "a": 1,', + ' "b": 2,', + ');', + '', + '@if $size == "default" {', + ' $branch: 1px;', + '}', + )); + + expect(warnings).toEqual([]); + expect(output).toBe(scss( + '$top: 7px !default;', + '$map: (', + ' "a": 1,', + ' "b": 2,', + ') !default;', + '', + '@if $size == "default" {', + ' $branch: 1px !default;', + '}', + )); +}); diff --git a/packages/devextreme-scss/tests/no-identical-branch-declarations.test.ts b/packages/devextreme-scss/tests/no-identical-branch-declarations.test.ts new file mode 100644 index 000000000000..1c70a11fe2f9 --- /dev/null +++ b/packages/devextreme-scss/tests/no-identical-branch-declarations.test.ts @@ -0,0 +1,392 @@ +import { createRunner, scss } from './stylelint-rule'; + +const ruleName = 'dx/no-identical-branch-declarations'; +const { lint, fix } = createRunner('no-identical-branch-declarations.mjs', ruleName); + +test('a variable set to one value in every @else branch is reported once, on the first branch', () => { + const { warnings } = lint('else-chain.scss', scss( + '$same: null !default;', + '$differs: null !default;', + '', + '@if $size == "default" {', + ' $same: 8px !default;', + ' $differs: 8px !default;', + '}', + '', + '@else if $size == "compact" {', + ' $same: 8px !default;', + ' $differs: 4px !default;', + '}', + )); + + expect(warnings).toEqual([{ + line: 5, + rule: ruleName, + text: `"$same" has the same value in all 2 branches of the @if chain; declare it once outside the chain (${ruleName})`, + }]); +}); + +test('consecutive @if blocks testing one variable against different constants form a chain', () => { + const { warnings } = lint('if-if.scss', scss( + '@if $size == "default" {', + ' $same: 8px !default;', + '}', + '', + '@if $size == "compact" {', + ' $same: 8px !default;', + '}', + '', + '@if $mode == "dark" {', + ' $same: 8px !default;', + '}', + )); + + expect(warnings.map((warning) => warning.line)).toEqual([2]); +}); + +test('nothing is reported for a value that differs, a variable missing from a branch, or a repeated constant', () => { + const { warnings } = lint('clean.scss', scss( + '@if $size == "default" {', + ' $a: 8px !default;', + ' $only-here: 1px !default;', + '}', + '', + '@else if $size == "compact" {', + ' $a: 4px !default;', + '}', + '', + '@if $size == "default" {', + ' $b: 8px !default;', + '}', + '', + '@if $size == "default" {', + ' $b: 8px !default;', + '}', + )); + + expect(warnings).toEqual([]); +}); + +test('--fix replaces the null pre-declaration with a literal and removes the emptied chain', () => { + const { warnings, output } = fix('fix-literal.scss', scss( + '$x: null !default;', + '$keep: 1px !default;', + '', + '@if $size == "default" {', + ' $x: ds.$spacing-480 !default;', + '}', + '', + '@else if $size == "compact" {', + ' $x: ds.$spacing-480 !default;', + '}', + )); + + expect(warnings).toEqual([]); + expect(output).toBe(scss( + '$x: ds.$spacing-480 !default;', + '$keep: 1px !default;', + )); +}); + +test('--fix puts a value that reads a branch-bound variable after the chain', () => { + const { output } = fix('fix-after.scss', scss( + '$height: null !default;', + '$handle: null !default;', + '', + '@if $size == "default" {', + ' $height: 20px !default;', + ' $handle: $height !default;', + '}', + '', + '@else if $size == "compact" {', + ' $height: 18px !default;', + ' $handle: $height !default;', + '}', + )); + + expect(output).toBe(scss( + '$height: null !default;', + '', + '@if $size == "default" {', + ' $height: 20px !default;', + '}', + '', + '@else if $size == "compact" {', + ' $height: 18px !default;', + '}', + '', + '$handle: $height !default;', + )); +}); + +test('--fix leaves no blank first line when the removed declaration opened its group', () => { + const { output } = fix('fix-first-line.scss', scss( + '$same: null !default;', + '', + '@if $size == "default" {', + ' $same: 1px !default;', + '', + ' $width: 36px !default;', + '}', + '', + '@else if $size == "compact" {', + ' $same: 1px !default;', + '', + ' $width: 32px !default;', + '}', + )); + + expect(output).toBe(scss( + '$same: 1px !default;', + '', + '@if $size == "default" {', + ' $width: 36px !default;', + '}', + '', + '@else if $size == "compact" {', + ' $width: 32px !default;', + '}', + )); +}); + +test('--fix moves a value that reads a variable declared below its pre-declaration to just before the chain', () => { + const { output } = fix('fix-before.scss', scss( + '$padding: null !default;', + '$margin: 0 !default;', + '', + '@if $size == "default" {', + ' $padding: $margin !default;', + ' $width: 36px !default;', + '}', + '', + '@if $size == "compact" {', + ' $padding: $margin !default;', + ' $width: 32px !default;', + '}', + )); + + expect(output).toBe(scss( + '$margin: 0 !default;', + '$padding: $margin !default;', + '', + '@if $size == "default" {', + ' $width: 36px !default;', + '}', + '', + '@if $size == "compact" {', + ' $width: 32px !default;', + '}', + )); +}); + +test('--fix only removes branch declarations that a non-null default above the chain already overrides', () => { + const { output } = fix('fix-dead.scss', scss( + '$x: 10px !default;', + '', + '@if $size == "default" {', + ' $x: 12px !default;', + '}', + '', + '@else if $size == "compact" {', + ' $x: 12px !default;', + '}', + )); + + expect(output).toBe(scss('$x: 10px !default;')); +}); + +test('--fix settles a duplicate that only appears in the outer chain after the inner one is hoisted', () => { + const { warnings, output } = fix('fix-nested.scss', scss( + '$focused: null !default;', + '', + '@if $mode == "light" {', + ' @if $color == "blue" {', + ' $hover: #00f !default;', + ' $focused: $hover !default;', + ' }', + '', + ' @if $color == "saas" {', + ' $hover: #0ff !default;', + ' $focused: $hover !default;', + ' }', + '}', + '', + '@if $mode == "dark" {', + ' @if $color == "blue" {', + ' $hover: #008 !default;', + ' $focused: $hover !default;', + ' }', + '', + ' @if $color == "saas" {', + ' $hover: #088 !default;', + ' $focused: $hover !default;', + ' }', + '}', + )); + + expect(warnings).toEqual([]); + expect(output).toBe(scss( + '@if $mode == "light" {', + ' @if $color == "blue" {', + ' $hover: #00f !default;', + ' }', + '', + ' @if $color == "saas" {', + ' $hover: #0ff !default;', + ' }', + '}', + '', + '@if $mode == "dark" {', + ' @if $color == "blue" {', + ' $hover: #008 !default;', + ' }', + '', + ' @if $color == "saas" {', + ' $hover: #088 !default;', + ' }', + '}', + '', + '$focused: $hover !default;', + )); +}); + +test('--fix keeps a declaration behind the chain ahead of the declarations that read it', () => { + const { warnings, output } = fix('fix-order.scss', scss( + '$item-bg: null !default;', + '$disabled-bg: null !default;', + '$disabled-border: null !default;', + '', + '@if $mode == "light" {', + ' $item-bg: #eee !default;', + ' $disabled-bg: $item-bg !default;', + ' $disabled-border: $disabled-bg !default;', + '}', + '', + '@if $mode == "dark" {', + ' $item-bg: #333 !default;', + ' $disabled-bg: $item-bg !default;', + ' $disabled-border: $disabled-bg !default;', + '}', + )); + + expect(warnings).toEqual([]); + expect(output).toBe(scss( + '$item-bg: null !default;', + '', + '@if $mode == "light" {', + ' $item-bg: #eee !default;', + '}', + '', + '@if $mode == "dark" {', + ' $item-bg: #333 !default;', + '}', + '', + '$disabled-bg: $item-bg !default;', + '$disabled-border: $disabled-bg !default;', + )); +}); + +test('a duplicate that a branch-bound declaration still reads is reported but not moved', () => { + const source = scss( + '$item-bg: null !default;', + '$disabled-bg: null !default;', + '$hovered-bg: null !default;', + '', + '@if $mode == "light" {', + ' $item-bg: #eee !default;', + ' $disabled-bg: $item-bg !default;', + ' $hovered-bg: color.adjust($disabled-bg, $lightness: -4%) !default;', + '}', + '', + '@if $mode == "dark" {', + ' $item-bg: #333 !default;', + ' $disabled-bg: $item-bg !default;', + ' $hovered-bg: color.adjust($disabled-bg, $lightness: 4%) !default;', + '}', + ); + + const { warnings, output } = fix('fix-blocked.scss', source); + + expect(warnings.map((warning) => warning.line)).toEqual([7]); + expect(output).toBe(source); +}); + +test('--fix keeps a declaration under its doc comment when what it reads is not reassigned in between', () => { + const { output } = fix('fix-doc-in-place.scss', scss( + '@use "../colors" as *;', + '', + '/**', + '* $name 100. Background color', + '* $type color', + '*/', + '$common-bg: null !default;', + '', + '@if $mode == "light" {', + ' $common-bg: $base-bg !default;', + ' $accent: #00f !default;', + '}', + '', + '@if $mode == "dark" {', + ' $common-bg: $base-bg !default;', + ' $accent: #0ff !default;', + '}', + )); + + expect(output).toBe(scss( + '@use "../colors" as *;', + '', + '/**', + '* $name 100. Background color', + '* $type color', + '*/', + '$common-bg: $base-bg !default;', + '', + '@if $mode == "light" {', + ' $accent: #00f !default;', + '}', + '', + '@if $mode == "dark" {', + ' $accent: #0ff !default;', + '}', + )); +}); + +test('--fix takes the doc comment along when the declaration has to move', () => { + const { output } = fix('fix-doc-moves.scss', scss( + '/**', + '* $name 302. Divider color', + '* $type color', + '*/', + '$divider: null !default;', + '', + '$border: null !default;', + '', + '@if $mode == "light" {', + ' $border: #ccc !default;', + ' $divider: $border !default;', + '}', + '', + '@if $mode == "dark" {', + ' $border: #444 !default;', + ' $divider: $border !default;', + '}', + )); + + expect(output).toBe(scss( + '$border: null !default;', + '', + '@if $mode == "light" {', + ' $border: #ccc !default;', + '}', + '', + '@if $mode == "dark" {', + ' $border: #444 !default;', + '}', + '', + '/**', + '* $name 302. Divider color', + '* $type color', + '*/', + '$divider: $border !default;', + )); +}); diff --git a/packages/devextreme-scss/tests/stylelint-rule.ts b/packages/devextreme-scss/tests/stylelint-rule.ts new file mode 100644 index 000000000000..c8e8d5bafbf7 --- /dev/null +++ b/packages/devextreme-scss/tests/stylelint-rule.ts @@ -0,0 +1,49 @@ +import { execFileSync } from 'child_process'; +import { mkdtempSync, readFileSync, writeFileSync } from 'fs'; +import { tmpdir } from 'os'; +import { basename, join } from 'path'; + +export type Warning = { line: number; rule: string; text: string }; +export type Result = { warnings: Warning[]; output: string }; +export type Runner = { + lint: (name: string, source: string) => Result; + fix: (name: string, source: string) => Result; +}; + +const packageRoot = process.cwd(); +const stylelintBin = join(packageRoot, 'node_modules', '.bin', 'stylelint'); + +export const scss = (...rows: string[]): string => `${rows.join('\n')}\n`; + +export const createRunner = (pluginFile: string, ruleName: string): Runner => { + const fixture = mkdtempSync(join(tmpdir(), `${basename(pluginFile, '.mjs')}-`)); + const configPath = join(fixture, 'config.json'); + writeFileSync(configPath, JSON.stringify({ + customSyntax: require.resolve('postcss-scss', { paths: [require.resolve('stylelint-scss')] }), + plugins: [join(packageRoot, 'tools', 'stylelint', pluginFile)], + rules: { [ruleName]: true }, + })); + + const run = (name: string, source: string, fix: boolean): Result => { + const file = join(fixture, name); + const reportFile = join(fixture, `${name}.report.json`); + writeFileSync(file, source); + writeFileSync(reportFile, ''); + const args = [file, '--config', configPath, '--formatter', 'json', '--output-file', reportFile, ...(fix ? ['--fix'] : [])]; + try { + execFileSync(stylelintBin, args, { stdio: 'ignore' }); + } catch (error) { + if ((error as { status?: number }).status !== 2) throw error; + } + const [{ warnings }] = JSON.parse(readFileSync(reportFile, 'utf8') || '[{"warnings":[]}]'); + return { + warnings: warnings.map(({ line, rule, text }: Warning) => ({ line, rule, text })), + output: readFileSync(file, 'utf8'), + }; + }; + + return { + lint: (name, source) => run(name, source, false), + fix: (name, source) => run(name, source, true), + }; +}; diff --git a/packages/devextreme-scss/tests/unused-elements.test.ts b/packages/devextreme-scss/tests/unused-elements.test.ts index 559096b709b3..52180125fabf 100644 --- a/packages/devextreme-scss/tests/unused-elements.test.ts +++ b/packages/devextreme-scss/tests/unused-elements.test.ts @@ -47,24 +47,41 @@ const removeAllCommentsFromContent = (content: string): string => content .filter((_, index) => index % 2 === 0) .join(''); -const extractVariables = (filePath: string): string[] => { +const variableRegex = new RegExp(`\\$[${VAR_NAME_CHARS}]+`, 'y'); + +type VariableUsage = { declared: Set; read: Set }; + +const collectVariableUsage = (filePath: string, usage: VariableUsage): void => { const content = removeAllCommentsFromContent(readFileSync(filePath, 'utf8')); - const regex = new RegExp(`\\$[${VAR_NAME_CHARS}]+`, 'g'); - return content.match(regex) ?? []; + let depth = 0; + let index = 0; + while (index < content.length) { + const char = content[index]; + if (char === '(') depth += 1; + if (char === ')') depth -= 1; + if (char === '$') { + variableRegex.lastIndex = index; + const [variable] = variableRegex.exec(content) ?? []; + if (!variable) { + index += 1; + continue; + } + const assigned = /^\s*:/.test(content.slice(index + variable.length)); + if (assigned && depth === 0) usage.declared.add(variable); + if (!assigned) usage.read.add(variable); + index += variable.length; + continue; + } + index += 1; + } }; -const findUniqueVariables = (variables: string[]): string[] => { - const variableCounts = new Map(); - variables.forEach((variable) => { - variableCounts.set(variable, (variableCounts.get(variable) ?? 0) + 1); - }); - return Array.from(variableCounts.entries()) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - // @ts-ignore - .filter(([variable, count]) => count === 1) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - // @ts-ignore - .map(([variable, count]) => variable); +const variableUsageOf = (directories: string[]): VariableUsage => { + const usage: VariableUsage = { declared: new Set(), read: new Set() }; + directories + .flatMap((directory) => getFilesFromDirectory(join('scss', 'widgets', directory), ['.scss'])) + .forEach((filePath) => collectVariableUsage(filePath, usage)); + return usage; }; test('There are no unused images in repository', () => { @@ -85,29 +102,14 @@ test('There are no unused images in repository', () => { expect(fullImagesFileList).toEqual(usedImagesFileList); }); -['generic', 'material', 'fluent'].forEach((themeName) => { - test(`There are no unused variables in ${themeName} SCSS files`, () => { - const baseScssFiles = getFilesFromDirectory(join('scss', 'widgets', 'base'), ['.scss']) - .map((fileName) => resolve(fileName)); - - const genericScssFiles = getFilesFromDirectory(join('scss', 'widgets', themeName), ['.scss']) - .map((fileName) => resolve(fileName)); - - const scssFiles = [...baseScssFiles, ...genericScssFiles]; - - let variables: string[] = []; - scssFiles.forEach((filePath) => { - variables = variables.concat(extractVariables(getFilePath(filePath.substring(filePath.indexOf('/scss'))))); - }); - - const uniqueVariables = findUniqueVariables(variables); +const themes = ['generic', 'material', 'fluent']; +const readAnywhere = variableUsageOf(['base', ...themes]).read; - const exclusions: { generic: string[]; material: string[]; fluent: string[] } = { - generic: [], - material: [], - fluent: [], - }; +themes.forEach((themeName) => { + test(`There are no unused variables in ${themeName} SCSS files`, () => { + const { declared } = variableUsageOf(['base', themeName]); + const unused = [...declared].filter((variable) => !readAnywhere.has(variable)).sort(); - expect(uniqueVariables).toEqual(exclusions[themeName]); + expect(unused).toEqual([]); }); }); diff --git a/packages/devextreme-scss/tools/stylelint/module-variable-default.mjs b/packages/devextreme-scss/tools/stylelint/module-variable-default.mjs new file mode 100644 index 000000000000..1bf97bfbf8ce --- /dev/null +++ b/packages/devextreme-scss/tools/stylelint/module-variable-default.mjs @@ -0,0 +1,55 @@ +import stylelint from 'stylelint'; + +const { + createPlugin, + utils: { report, ruleMessages, validateOptions }, +} = stylelint; + +const ruleName = 'dx/module-variable-default'; + +const messages = ruleMessages(ruleName, { + expected: (name) => `Expected the !default flag on "${name}": without it a value passed through @use … with() is ignored`, +}); + +const meta = { fixable: true }; + +const isConditional = (node) => node.type === 'atrule' && ['if', 'else'].includes(node.name); +const isModuleLevel = (decl) => { + let node = decl.parent; + while (isConditional(node)) node = node.parent; + return node.type === 'root'; +}; +const hasFlag = (decl, flag) => new RegExp(`!${flag}\\b`, 'i').test(decl.value); +const isNull = (decl) => /^null(\s+!default)?$/.test(decl.value.trim()); + +const ruleFunction = (primary) => (root, result) => { + if (!validateOptions(result, ruleName, { actual: primary, possible: [true] })) return; + + const assigned = new Set(); + + root.walkDecls((decl) => { + if (!decl.prop.startsWith('$') || !isModuleLevel(decl)) return; + + const reassignment = assigned.has(decl.prop); + if (decl.parent.type === 'root' && !isNull(decl)) assigned.add(decl.prop); + if (reassignment || hasFlag(decl, 'default') || hasFlag(decl, 'global')) return; + + report({ + message: messages.expected, + messageArgs: [decl.prop], + node: decl, + result, + ruleName, + fix: () => { + decl.value = `${decl.value} !default`; + if (decl.raws.value) decl.raws.value = { value: decl.value, raw: `${decl.raws.value.raw} !default` }; + }, + }); + }); +}; + +ruleFunction.ruleName = ruleName; +ruleFunction.messages = messages; +ruleFunction.meta = meta; + +export default createPlugin(ruleName, ruleFunction); diff --git a/packages/devextreme-scss/tools/stylelint/no-identical-branch-declarations.mjs b/packages/devextreme-scss/tools/stylelint/no-identical-branch-declarations.mjs new file mode 100644 index 000000000000..088b41e742bd --- /dev/null +++ b/packages/devextreme-scss/tools/stylelint/no-identical-branch-declarations.mjs @@ -0,0 +1,238 @@ +import stylelint from 'stylelint'; + +const { + createPlugin, + utils: { report, ruleMessages, validateOptions }, +} = stylelint; + +const ruleName = 'dx/no-identical-branch-declarations'; + +const messages = ruleMessages(ruleName, { + rejected: (name, count) => `"${name}" has the same value in all ${count} branches of the @if chain; declare it once outside the chain`, +}); + +const meta = { fixable: true }; + +const isElse = (node) => node.type === 'atrule' && node.name === 'else'; +const isIf = (node) => node.type === 'atrule' && node.name === 'if'; +const isVariable = (node) => node.type === 'decl' && node.prop.startsWith('$'); +const isEmpty = (block) => (block.nodes ?? []).length === 0; +const normalise = (value) => value.replace(/\s+/g, ' ').trim(); +const isNull = (value) => /^null(\s+!default)?$/.test(normalise(value)); +const hasDefaultFlag = (value) => /!default\b/.test(value); +const indentOf = (node) => (node.raws.before ?? '').match(/[ \t]*$/)[0]; + +const localReferences = (value) => [...value.matchAll(/(^|[^\w.-])(\$[\w-]+)/g)].map((match) => match[2]); + +const equalityTest = (atRule) => { + const match = atRule.params.trim().match(/^(\$[\w-]+)\s*==\s*("[^"]*"|'[^']*'|[\w.-]+)$/); + return match && { variable: match[1], literal: match[2] }; +}; + +const chainOf = (ifRule) => { + const branches = [ifRule]; + let next = ifRule.next(); + + while (next && (next.type === 'comment' || isElse(next))) { + if (isElse(next)) branches.push(next); + next = next.next(); + } + if (branches.length > 1) return { branches, independent: false }; + + const test = equalityTest(ifRule); + const literals = new Set([test?.literal]); + + while (test && next && (next.type === 'comment' || isIf(next))) { + if (isIf(next)) { + const other = equalityTest(next); + if (other?.variable !== test.variable || literals.has(other.literal)) break; + literals.add(other.literal); + branches.push(next); + } + next = next.next(); + } + return { branches, independent: true }; +}; + +const collectChains = (root) => { + const chains = []; + const seen = new WeakSet(); + + root.walkAtRules('if', (ifRule) => { + if (seen.has(ifRule)) return; + const chain = chainOf(ifRule); + chain.branches.forEach((branch) => seen.add(branch)); + if (chain.branches.length > 1) chains.push(chain); + }); + return chains.reverse(); +}; + +const directDeclarations = (block) => { + const byName = new Map(); + (block.nodes ?? []).filter(isVariable).forEach((decl) => byName.set(decl.prop, decl)); + return byName; +}; + +const identicalDeclarations = (branches) => { + const [first, ...rest] = branches.map(directDeclarations); + const identical = new Map(); + + for (const [name, decl] of first) { + const declarations = [decl, ...rest.map((byName) => byName.get(name))]; + const value = normalise(decl.value); + if (declarations.every((other) => other && normalise(other.value) === value)) identical.set(name, declarations); + } + return identical; +}; + +const walkFinds = (branches, predicate) => branches.some((branch) => { + let found = false; + branch.walkDecls((decl) => { + if (!predicate(decl, branch)) return undefined; + found = true; + return false; + }); + return found; +}); + +const assignedInside = (branches, name) => walkFinds(branches, (decl) => decl.prop === name); + +const planMoves = (branches, identical) => { + const movable = new Set(identical.keys()); + const placement = new Map(); + + const settle = () => { + placement.clear(); + for (const name of movable) { + const behind = localReferences(identical.get(name)[0].value).some((ref) => ( + movable.has(ref) ? placement.get(ref) === 'after' : assignedInside(branches, ref) + )); + placement.set(name, behind ? 'after' : 'before'); + } + }; + const readByStaying = (name) => walkFinds(branches, (decl, branch) => ( + !(movable.has(decl.prop) && decl.parent === branch) && localReferences(decl.value).includes(name) + )); + + for (let changed = true; changed;) { + settle(); + changed = false; + for (const name of movable) { + if (placement.get(name) === 'after' && readByStaying(name)) { + movable.delete(name); + changed = true; + } + } + } + return { movable, placement }; +}; + +const earlierDeclaration = (parent, ifRule, name) => { + let found; + for (const sibling of parent.nodes) { + if (sibling === ifRule) break; + if (isVariable(sibling) && sibling.prop === name) found = sibling; + } + return found; +}; + +const declaredBetween = (parent, after, before, name) => parent.nodes + .slice(parent.index(after) + 1, parent.index(before)) + .some((node) => isVariable(node) && node.prop === name); + +const attachedComment = (decl) => { + const previous = decl.prev(); + const attached = previous?.type === 'comment' && !/\n[ \t]*\n/.test(decl.raws.before ?? ''); + return attached ? previous : undefined; +}; + +const dropEmptyBranches = (branches, independent) => { + if (independent) { + branches.filter(isEmpty).forEach((branch) => branch.remove()); + return; + } + while (branches.length && isEmpty(branches[branches.length - 1])) { + branches.pop().remove(); + } +}; + +const ruleFunction = (primary) => (root, result) => { + if (!validateOptions(result, ruleName, { actual: primary, possible: [true] })) return; + + const lastHoisted = new WeakMap(); + + for (const { branches, independent } of collectChains(root)) { + const ifRule = branches[0]; + const { parent } = ifRule; + const identical = identicalDeclarations(branches); + const { movable, placement } = planMoves(branches, identical); + + for (const [name, declarations] of identical) { + const [first] = declarations; + const earlier = earlierDeclaration(parent, ifRule, name); + const overridden = earlier !== undefined && !isNull(earlier.value); + const fixable = overridden ? declarations.every((decl) => hasDefaultFlag(decl.value)) : movable.has(name); + + const fix = () => { + const live = branches.filter((branch) => branch.parent); + const tidy = () => { + dropEmptyBranches(live, independent); + live.forEach((branch) => { + if (branch.parent && branch.first?.type === 'decl') branch.first.raws.before = branch.first.raws.before.replace(/^\s*\n/, '\n'); + }); + parent.raws.semicolon = true; + if (parent.type === 'root' && parent.first) parent.first.raws.before = ''; + }; + + if (overridden) { + declarations.forEach((decl) => decl.remove()); + tidy(); + return; + } + + const hoisted = first.clone(); + const references = localReferences(first.value); + const comment = earlier && attachedComment(earlier); + declarations.forEach((decl) => decl.remove()); + + const relocate = (insert, gap) => { + const indent = indentOf(ifRule); + const moved = comment?.clone({ raws: { ...comment.raws, before: `${gap}${indent}` } }); + hoisted.raws.before = moved ? `\n${indent}` : `${gap}${indent}`; + if (moved) insert(moved); + insert(hoisted, moved); + comment?.remove(); + earlier?.remove(); + }; + + if (placement.get(name) === 'after') { + const anchor = lastHoisted.get(ifRule) ?? live[live.length - 1]; + relocate((node, previous) => parent.insertAfter(previous ?? anchor, node), lastHoisted.has(ifRule) ? '\n' : '\n\n'); + lastHoisted.set(ifRule, hoisted); + } else if (earlier && !references.some((ref) => declaredBetween(parent, earlier, ifRule, ref))) { + hoisted.raws.before = earlier.raws.before; + earlier.replaceWith(hoisted); + } else { + relocate((node, previous) => (previous ? parent.insertAfter(previous, node) : parent.insertBefore(ifRule, node)), '\n'); + } + + tidy(); + }; + + report({ + message: messages.rejected, + messageArgs: [name, branches.length], + node: first, + result, + ruleName, + ...(fixable && { fix: { apply: fix, node: parent } }), + }); + } + } +}; + +ruleFunction.ruleName = ruleName; +ruleFunction.messages = messages; +ruleFunction.meta = meta; + +export default createPlugin(ruleName, ruleFunction); diff --git a/packages/devextreme-themebuilder/src/metadata/generator.ts b/packages/devextreme-themebuilder/src/metadata/generator.ts index 2a328f6a1451..446da5b8fb70 100644 --- a/packages/devextreme-themebuilder/src/metadata/generator.ts +++ b/packages/devextreme-themebuilder/src/metadata/generator.ts @@ -112,7 +112,7 @@ export default class MetadataGenerator { }); const overriddenVariables = MetadataGenerator.getOverriddenVariables(metaItems); - const collector = `$never-used: collector(${MetadataGenerator.getMapFromMeta(metaItems)});\n`; + const collector = `$_never-used: collector(${MetadataGenerator.getMapFromMeta(metaItems)});\n`; modifiedContent = overriddenVariables + '\n\n' + modifiedContent + collector; return modifiedContent; diff --git a/packages/devextreme-themebuilder/src/modules/post-compiler.ts b/packages/devextreme-themebuilder/src/modules/post-compiler.ts index b0357609a572..a88720fdc708 100644 --- a/packages/devextreme-themebuilder/src/modules/post-compiler.ts +++ b/packages/devextreme-themebuilder/src/modules/post-compiler.ts @@ -68,8 +68,10 @@ export function fixSwatchCss(css: string | Buffer, swatchClass: string, colorSch const swatchOrderRegex = new RegExp(`([ \\t]*)([\\w\\.#:\\*][\\w\\.#:\\*\\->()\\s]*)(${escapedSelector}\\s)([^,{+~]*)`, 'gm'); const changeTypographyRulesOrderRegex = /(\.dx-swatch-.*?)\s(\.dx-theme-.*?-typography)(.*?)(\s{|,)/g; const themeMarkerRegex = /(\.dx-theme-marker\s*{\s*font-family:\s*['"]dx\..*?\.)(.*)(['"])/g; + const rootSelectorRegex = new RegExp(`${escapedSelector}\\s+:root\\b`, 'g'); result = result + .replace(rootSelectorRegex, swatchClass) .replace(swatchOrderRegex, '$1$3$2$4') .replace(changeTypographyRulesOrderRegex, '$2 $1$3,$2$1$3$4') .replace(themeMarkerRegex, `$1${colorScheme}$3`); diff --git a/packages/devextreme-themebuilder/tests/data/scss/widgets/generic/_colors.scss b/packages/devextreme-themebuilder/tests/data/scss/widgets/generic/_colors.scss index e15f6dbe91cd..7d115ebee381 100644 --- a/packages/devextreme-themebuilder/tests/data/scss/widgets/generic/_colors.scss +++ b/packages/devextreme-themebuilder/tests/data/scss/widgets/generic/_colors.scss @@ -18,7 +18,7 @@ $float-channel-color: null !default; } -$never-used: collector( +$_never-used: collector( ( "$base-font-family": $base-font-family, "$base-accent": $base-accent, diff --git a/packages/devextreme-themebuilder/tests/data/scss/widgets/generic/accordion/_colors.scss b/packages/devextreme-themebuilder/tests/data/scss/widgets/generic/accordion/_colors.scss index d671f1015680..e08a47d95169 100644 --- a/packages/devextreme-themebuilder/tests/data/scss/widgets/generic/accordion/_colors.scss +++ b/packages/devextreme-themebuilder/tests/data/scss/widgets/generic/accordion/_colors.scss @@ -5,8 +5,6 @@ $accordion-item-title-opened-bg: getCustomVar(("$accordion-item-title-opened-bg" @use "../sizes" as *; @use "../colors" as *; -// adduse - $accordion-title-color: null !default; $accordion-item-title-opened-bg: null !default; @@ -15,7 +13,7 @@ $accordion-item-title-opened-bg: null !default; $accordion-item-title-opened-bg: transparent !default; } -$never-used: collector( +$_never-used: collector( ( "$accordion-title-color": $accordion-title-color, "$accordion-item-title-opened-bg": $accordion-item-title-opened-bg, diff --git a/packages/devextreme-themebuilder/tests/metadata/generator.test.ts b/packages/devextreme-themebuilder/tests/metadata/generator.test.ts index ee6b3aa83eb0..4b0cb63f446f 100644 --- a/packages/devextreme-themebuilder/tests/metadata/generator.test.ts +++ b/packages/devextreme-themebuilder/tests/metadata/generator.test.ts @@ -200,7 +200,7 @@ $slideout-background: #000 !default; * $type color */ $slideout-background: #000 !default; -$never-used: collector(( +$_never-used: collector(( "$slideout-background": $slideout-background, )); `; diff --git a/packages/devextreme-themebuilder/tests/modules/builder.test.ts b/packages/devextreme-themebuilder/tests/modules/builder.test.ts index b4f0f8ac7620..8adeb9deb6d3 100644 --- a/packages/devextreme-themebuilder/tests/modules/builder.test.ts +++ b/packages/devextreme-themebuilder/tests/modules/builder.test.ts @@ -38,6 +38,8 @@ describe('Builder integration tests', () => { return buildTheme(config).then((result) => { expect(result.css).not.toBe(''); expect(result.swatchSelector).toBe('.dx-swatch-custom-scheme'); + expect(result.css).not.toMatch(/\.dx-swatch-custom-scheme\s*:root/); + expect(result.css).toMatch(/\.dx-swatch-custom-scheme\s*\{[^}]*--dx-color-primary:/); }); }, buildTimeout); @@ -122,6 +124,22 @@ describe('Builder integration tests', () => { }); }, buildTimeout); + test('Theme built without parameters is the same that in distribution (fluent)', async () => { + const config: ConfigSettings = { + command: commands.BUILD_THEME, + outputColorScheme: 'custom-scheme', + baseTheme: 'fluent.blue.light', + items: [], + }; + + return buildTheme(config).then((result) => { + const themeBuilderCss = normalizeCss(result.css); + const cssPath = path.resolve(__dirname, '../../../devextreme/artifacts/css/dx.fluent.blue.light.css'); + const distributionCss = normalizeCss(readFileSync(cssPath, 'utf8')); + expect(themeBuilderCss).toBe(distributionCss); + }); + }, buildTimeout); + test('Theme built without parameters is the same that in distribution (material)', async () => { const config: ConfigSettings = { command: commands.BUILD_THEME, diff --git a/packages/devextreme-themebuilder/tests/modules/post-compiler.test.ts b/packages/devextreme-themebuilder/tests/modules/post-compiler.test.ts index 41717ff21294..de51a0368c62 100644 --- a/packages/devextreme-themebuilder/tests/modules/post-compiler.test.ts +++ b/packages/devextreme-themebuilder/tests/modules/post-compiler.test.ts @@ -163,6 +163,96 @@ describe('PostCompiler - swatch features (fixSwatchCss)', () => { expect(result).toBe(expectedCss); }); + test('fixSwatchCss - apply custom properties to the swatch class instead of :root (T1334644)', () => { + const compilerCss = ` +.dx-swatch-c :root { + --dx-color-primary: #fff; + --dx-color-text: #000; +}`; + + const expectedCss = ` +.dx-swatch-c { + --dx-color-primary: #fff; + --dx-color-text: #000; +}`; + + const result = fixSwatchCss(compilerCss, '.dx-swatch-c', 'c'); + expect(result).toBe(expectedCss); + }); + + test('fixSwatchCss - do not change the order of cascade\'s classes in rules next to :root', () => { + const compilerCss = ` +.dx-swatch-c .dx-widget { + color: #fff; +} +.dx-swatch-c :root { + --dx-color-primary: #fff; +} +.dx-swatch-c .dx-checkbox-checked.dx-checkbox-icon { + background-color: #fff; +}`; + + const expectedCss = ` +.dx-swatch-c .dx-widget { + color: #fff; +} +.dx-swatch-c { + --dx-color-primary: #fff; +} +.dx-swatch-c .dx-checkbox-checked.dx-checkbox-icon { + background-color: #fff; +}`; + + const result = fixSwatchCss(compilerCss, '.dx-swatch-c', 'c'); + expect(result).toBe(expectedCss); + }); + + test('fixSwatchCss - apply compound :root selectors to the swatch class itself', () => { + const compilerCss = ` +.dx-swatch-c :root.dx-foo { + --dx-color-primary: #fff; +} +.dx-swatch-c :root:not(.dx-bar) { + --dx-color-text: #000; +} +.dx-swatch-c :root .dx-widget { + color: #fff; +}`; + + const expectedCss = ` +.dx-swatch-c.dx-foo { + --dx-color-primary: #fff; +} +.dx-swatch-c:not(.dx-bar) { + --dx-color-text: #000; +} +.dx-swatch-c .dx-widget { + color: #fff; +}`; + + const result = fixSwatchCss(compilerCss, '.dx-swatch-c', 'c'); + expect(result).toBe(expectedCss); + }); + + test('fixSwatchCss - apply custom properties to the swatch class inside media queries', () => { + const compilerCss = ` +@media (max-width: 600px) { + .dx-swatch-c :root { + --dx-color-primary: #fff; + } +}`; + + const expectedCss = ` +@media (max-width: 600px) { + .dx-swatch-c { + --dx-color-primary: #fff; + } +}`; + + const result = fixSwatchCss(compilerCss, '.dx-swatch-c', 'c'); + expect(result).toBe(expectedCss); + }); + test('fixSwatchCss - fix theme marker in swatch css', () => { const compilerCss = '.dx-swatch-c .dx-theme-marker { font-family: \'dx.generic.light\'; }'; diff --git a/packages/devextreme/js/__internal/common/core/animation/m_position.ts b/packages/devextreme/js/__internal/common/core/animation/m_position.ts index 0c4089d10c8d..ce6207053ab0 100644 --- a/packages/devextreme/js/__internal/common/core/animation/m_position.ts +++ b/packages/devextreme/js/__internal/common/core/animation/m_position.ts @@ -2,7 +2,6 @@ import { move, resetPosition } from '@js/common/core/animation/translator'; import devices from '@js/core/devices'; import domAdapter from '@js/core/dom_adapter'; import $ from '@js/core/renderer'; -import browser from '@js/core/utils/browser'; import { extend } from '@js/core/utils/extend'; import { each } from '@js/core/utils/iterator'; import { getBoundingRect } from '@js/core/utils/position'; @@ -21,7 +20,6 @@ const horzRe = /left|right/; const vertRe = /top|bottom/; const collisionRe = /fit|flip|none/; const scaleRe = /scale\(.+?\)/; -const IS_SAFARI = browser.safari; const normalizeAlign = function (raw) { const result = { @@ -258,14 +256,18 @@ const calculatePosition = function (what, options) { if (isWindow(of[0])) { h.atLocation = of.scrollLeft(); v.atLocation = of.scrollTop(); - if (devices.real().deviceType === 'phone' && of[0].visualViewport) { + const device = devices.real(); + + if (device.deviceType === 'phone' && of[0].visualViewport) { h.atLocation = Math.max(h.atLocation, of[0].visualViewport.offsetLeft); v.atLocation = Math.max(v.atLocation, of[0].visualViewport.offsetTop); h.atSize = of[0].visualViewport.width; v.atSize = of[0].visualViewport.height; } else { - h.atSize = of[0].innerWidth > of[0].outerWidth ? of[0].innerWidth : getWidth(of); - v.atSize = of[0].innerHeight > of[0].outerHeight || IS_SAFARI ? of[0].innerHeight : getHeight(of); + const isIos = device.platform === 'ios'; + + h.atSize = getWidth(of); + v.atSize = isIos ? of[0].innerHeight : getHeight(of); } } else if (of[0].nodeType === 9) { h.atLocation = 0; diff --git a/packages/devextreme/js/__internal/core/localization/number.ts b/packages/devextreme/js/__internal/core/localization/number.ts index 53631e00f316..67c8f5bbede1 100644 --- a/packages/devextreme/js/__internal/core/localization/number.ts +++ b/packages/devextreme/js/__internal/core/localization/number.ts @@ -286,7 +286,7 @@ const numberLocalizationBase = { return value.replace(regExp, (char) => String.fromCharCode(char.charCodeAt(0) + (toFirstDigit.charCodeAt(0) - fromFirstDigit.charCodeAt(0)))) as TValue; }, - getNegativeEtalonRegExp(format: FormatConfig | string): RegExp { + getNegativeEtalonRegExp(format: LocalizationFormat): RegExp { const separators: FormatterConfig = this._getSeparators(); const digitalRegExp = new RegExp(`[0-9${escapeRegExp(separators.decimalSeparator + separators.thousandsSeparator)}]+`, 'g'); const specialCharacters = ['\\', '(', ')', '[', ']', '*', '+', '$', '^', '?', '|', '{', '}']; @@ -301,7 +301,7 @@ const numberLocalizationBase = { return new RegExp(negativeEtalon, 'g'); }, - getSign(text: string, format?: FormatConfig | string): 1 | -1 { + getSign(text: string, format?: LocalizationFormat): 1 | -1 { if (!format) { if (text.replace(/[^0-9-]/g, '').startsWith('-')) { return -1; diff --git a/packages/devextreme/js/__internal/core/utils/__tests__/document_size_callbacks.test.ts b/packages/devextreme/js/__internal/core/utils/__tests__/document_size_callbacks.test.ts new file mode 100644 index 000000000000..2b5f4d05e1cf --- /dev/null +++ b/packages/devextreme/js/__internal/core/utils/__tests__/document_size_callbacks.test.ts @@ -0,0 +1,161 @@ +import { + afterEach, describe, expect, it, jest, +} from '@jest/globals'; +import domAdapter from '@js/core/dom_adapter'; +import documentSizeCallbacks from '@ts/core/utils/document_size_callbacks'; + +type DocumentElement = ReturnType; + +interface Environment { + documentElement: { clientWidth: number; clientHeight: number }; + resizeVisualViewport: () => void; + addEventListenerSpy: jest.SpiedFunction; + removeEventListenerSpy: jest.SpiedFunction; +} + +function setup(): Environment { + const documentElement = { clientWidth: 1000, clientHeight: 800 }; + const visualViewport = new EventTarget(); + + jest.spyOn(domAdapter, 'getDocumentElement') + .mockImplementation(() => documentElement as unknown as DocumentElement); + Object.defineProperty(window, 'visualViewport', { value: visualViewport, configurable: true }); + window.innerWidth = 1024; + window.innerHeight = 768; + + return { + documentElement, + resizeVisualViewport: (): void => { visualViewport.dispatchEvent(new Event('resize')); }, + addEventListenerSpy: jest.spyOn(visualViewport, 'addEventListener'), + removeEventListenerSpy: jest.spyOn(visualViewport, 'removeEventListener'), + }; +} + +describe('documentSizeCallbacks', () => { + const addedHandlers: (() => void)[] = []; + + function addHandler(): jest.Mock { + const handler = jest.fn(); + + addedHandlers.push(handler); + documentSizeCallbacks.add(handler); + + return handler; + } + + afterEach(() => { + addedHandlers.splice(0).forEach((handler) => documentSizeCallbacks.remove(handler)); + delete (window as { visualViewport?: VisualViewport }).visualViewport; + jest.restoreAllMocks(); + }); + + it('should listen to the visual viewport resize once, no matter how many handlers are added', () => { + const { addEventListenerSpy } = setup(); + + addHandler(); + addHandler(); + + expect(addEventListenerSpy).toHaveBeenCalledTimes(1); + expect(addEventListenerSpy.mock.calls[0][0]).toBe('resize'); + }); + + it('should not call handlers when the document size has not changed', () => { + const { resizeVisualViewport } = setup(); + const handler = addHandler(); + + resizeVisualViewport(); + + expect(handler).not.toHaveBeenCalled(); + }); + + it('should call handlers when a scrollbar changes the client width', () => { + const { documentElement, resizeVisualViewport } = setup(); + const handler = addHandler(); + + documentElement.clientWidth = 985; + resizeVisualViewport(); + + expect(handler).toHaveBeenCalledTimes(1); + }); + + it('should call handlers when a scrollbar changes the client height', () => { + const { documentElement, resizeVisualViewport } = setup(); + const handler = addHandler(); + + documentElement.clientHeight = 785; + resizeVisualViewport(); + + expect(handler).toHaveBeenCalledTimes(1); + }); + + it('should leave a window resize to the window resize callbacks', () => { + const { documentElement, resizeVisualViewport } = setup(); + const handler = addHandler(); + + window.innerWidth = 1224; + documentElement.clientWidth = 1200; + resizeVisualViewport(); + + expect(handler).not.toHaveBeenCalled(); + + documentElement.clientWidth = 1185; + resizeVisualViewport(); + + expect(handler).toHaveBeenCalledTimes(1); + }); + + it('should hold a handler once even when it is added twice', () => { + const { documentElement, resizeVisualViewport, removeEventListenerSpy } = setup(); + const handler = addHandler(); + + documentSizeCallbacks.add(handler); + + documentElement.clientWidth = 985; + resizeVisualViewport(); + + expect(handler).toHaveBeenCalledTimes(1); + + documentSizeCallbacks.remove(handler); + + expect(removeEventListenerSpy).toHaveBeenCalledTimes(1); + }); + + it('should stop listening once the last handler is removed', () => { + const { removeEventListenerSpy } = setup(); + const first = addHandler(); + + addHandler(); + documentSizeCallbacks.remove(first); + + expect(removeEventListenerSpy).not.toHaveBeenCalled(); + + documentSizeCallbacks.remove(addedHandlers[1]); + + expect(removeEventListenerSpy).toHaveBeenCalledTimes(1); + }); + + it('should measure the document anew when the first handler is added again', () => { + const { documentElement, resizeVisualViewport } = setup(); + const first = addHandler(); + + documentSizeCallbacks.remove(first); + documentElement.clientWidth = 985; + + const second = addHandler(); + + resizeVisualViewport(); + + expect(second).not.toHaveBeenCalled(); + }); + + it('should do nothing when the browser has no visual viewport', () => { + setup(); + delete (window as { visualViewport?: VisualViewport }).visualViewport; + + const listenSpy = jest.spyOn(domAdapter, 'listen'); + const handler = addHandler(); + + expect(listenSpy).not.toHaveBeenCalled(); + expect(() => documentSizeCallbacks.remove(handler)).not.toThrow(); + }); +}); diff --git a/packages/devextreme/js/__internal/core/utils/document_size_callbacks.ts b/packages/devextreme/js/__internal/core/utils/document_size_callbacks.ts new file mode 100644 index 000000000000..a9fcb7f7015d --- /dev/null +++ b/packages/devextreme/js/__internal/core/utils/document_size_callbacks.ts @@ -0,0 +1,99 @@ +import domAdapter from '@js/core/dom_adapter'; +import { Callbacks } from '@ts/core/utils/m_callbacks'; +import windowUtils from '@ts/core/utils/m_window'; + +interface Size { + width: number; + height: number; +} + +type DocumentSizeHandler = () => void; + +interface HandlerList { + add: (handler: DocumentSizeHandler) => void; + remove: (handler: DocumentSizeHandler) => void; + has: () => boolean; + fire: () => void; +} + +const callbacks = Callbacks({ unique: true }) as HandlerList; + +let removeListener: (() => void) | null = null; +let previousDocumentSize: Size | null = null; +let previousWindowSize: Size | null = null; + +function getDocumentSize(): Size { + const documentElement = domAdapter.getDocumentElement(); + + return { + width: documentElement.clientWidth, + height: documentElement.clientHeight, + }; +} + +function getWindowSize(): Size { + const window: Window = windowUtils.getWindow(); + + return { + width: window.innerWidth, + height: window.innerHeight, + }; +} + +function isSameSize(previous: Size | null, current: Size): boolean { + return previous?.width === current.width && previous?.height === current.height; +} + +function handleVisualViewportResize(): void { + const documentSize = getDocumentSize(); + const windowSize = getWindowSize(); + const isDocumentSizeChanged = !isSameSize(previousDocumentSize, documentSize); + const isWindowSizeChanged = !isSameSize(previousWindowSize, windowSize); + + previousDocumentSize = documentSize; + previousWindowSize = windowSize; + + if (isDocumentSizeChanged && !isWindowSizeChanged) { + callbacks.fire(); + } +} + +function getVisualViewport(): VisualViewport | null { + if (!windowUtils.hasWindow()) { + return null; + } + + const window: Window = windowUtils.getWindow(); + + return window.visualViewport; +} + +function add(handler: DocumentSizeHandler): void { + const visualViewport = getVisualViewport(); + + if (!visualViewport) { + return; + } + + callbacks.add(handler); + + if (!removeListener) { + previousDocumentSize = getDocumentSize(); + previousWindowSize = getWindowSize(); + removeListener = domAdapter.listen(visualViewport, 'resize', handleVisualViewportResize); + } +} + +function remove(handler: DocumentSizeHandler): void { + callbacks.remove(handler); + + if (removeListener && !callbacks.has()) { + removeListener(); + removeListener = null; + previousDocumentSize = null; + previousWindowSize = null; + } +} + +export const documentSizeCallbacks = { add, remove }; +export default documentSizeCallbacks; diff --git a/packages/devextreme/js/__internal/core/utils/m_icon.ts b/packages/devextreme/js/__internal/core/utils/m_icon.ts index 41e144fa31e9..9a41ae01a46b 100644 --- a/packages/devextreme/js/__internal/core/utils/m_icon.ts +++ b/packages/devextreme/js/__internal/core/utils/m_icon.ts @@ -1,8 +1,14 @@ +import messageLocalization from '@js/common/core/localization/message'; import $ from '@js/core/renderer'; +import { camelize } from '@js/core/utils/inflector'; export const ICON_CLASS = 'dx-icon'; const SVG_ICON_CLASS = 'dx-svg-icon'; +const NOT_URL_REGEXP = /^(?!(?:https?:\/\/)|(?:ftp:\/\/)|(?:www\.))[^\s]+$/; +const FILE_NAME_REGEXP = /.+\/([^.]+)\..+$/; +const SVG_TITLE_REGEXP = /(.+?)<\/title>/; + export const getImageSourceType = (source) => { if (!source || typeof source !== 'string') { return false; @@ -41,3 +47,21 @@ export const getImageContainer = (source) => { return null; } }; + +export const getImageAriaLabel = (source: string): string | undefined => { + switch (getImageSourceType(source)) { + case 'image': { + const isPathToImage = !source.includes('base64') && NOT_URL_REGEXP.test(source); + + return isPathToImage ? source.replace(FILE_NAME_REGEXP, '$1') : undefined; + } + case 'dxIcon': + return messageLocalization.format(camelize(source, true)) || source; + case 'fontIcon': + return source; + case 'svg': + return SVG_TITLE_REGEXP.exec(source)?.[1]; + default: + return undefined; + } +}; diff --git a/packages/devextreme/js/__internal/core/widget/widget.ts b/packages/devextreme/js/__internal/core/widget/widget.ts index 7a9b950fecbd..479d82c583cb 100644 --- a/packages/devextreme/js/__internal/core/widget/widget.ts +++ b/packages/devextreme/js/__internal/core/widget/widget.ts @@ -33,6 +33,7 @@ export const DISABLED_STATE_CLASS = 'dx-state-disabled'; export const ACTIVE_STATE_CLASS = 'dx-state-active'; export const FOCUSED_STATE_CLASS = 'dx-state-focused'; export const HOVER_STATE_CLASS = 'dx-state-hover'; +export const SCREEN_READER_ONLY_CLASS = 'dx-screen-reader-only'; const INVISIBLE_STATE_CLASS = 'dx-state-invisible'; export const EMPTY_ACTIVE_STATE_UNIT = ''; diff --git a/packages/devextreme/js/__internal/data/abstract_store.ts b/packages/devextreme/js/__internal/data/abstract_store.ts index 8987913c2ed7..21bfe4fd6764 100644 --- a/packages/devextreme/js/__internal/data/abstract_store.ts +++ b/packages/devextreme/js/__internal/data/abstract_store.ts @@ -156,6 +156,7 @@ class Store { loadOptions._langParams = { ...this._langParams, ...loadOptions._langParams }; } + // @ts-expect-error public Query.enumerate() is a Promise, the query resolves a Deferred const result: DeferredObj<unknown[]> = queryByOptions( this.createQuery(loadOptions), loadOptions, @@ -193,6 +194,7 @@ class Store { } _totalCountImpl(options?: StoreLoadOptions): DeferredObj<number> { + // @ts-expect-error public Query.count() is a Promise, the query resolves a Deferred const result: DeferredObj<number> = queryByOptions( this.createQuery(options), options, diff --git a/packages/devextreme/js/__internal/data/array_store.ts b/packages/devextreme/js/__internal/data/array_store.ts index 7ae9ef524ffc..814e5a272bb9 100644 --- a/packages/devextreme/js/__internal/data/array_store.ts +++ b/packages/devextreme/js/__internal/data/array_store.ts @@ -47,27 +47,23 @@ class ArrayStore extends Store { const index = indexByKey(this, this._array, key); if (index === -1) { - // @ts-expect-error data/utils is untyped: `rejectedPromise` reads its arguments object const rejected: DeferredObj<unknown> = rejectedPromise(errors.Error('E4009')); return rejected; } - // @ts-expect-error data/utils is untyped: `trivialPromise` reads its arguments object const resolved: DeferredObj<unknown> = trivialPromise(this._array[index]); return resolved; } _insertImpl(values: unknown): DeferredObj<unknown> { - // @ts-expect-error array_utils is untyped: `insert` declares every argument as required const result: DeferredObj<unknown> = insert(this, this._array, values); return result; } _pushImpl(changes: StoreChange[]): void { - // @ts-expect-error array_utils is untyped: `applyBatch` destructures every option as required applyBatch({ keyInfo: this, data: this._array, @@ -76,14 +72,12 @@ class ArrayStore extends Store { } _updateImpl(key: unknown, values: unknown): DeferredObj<unknown> { - // @ts-expect-error array_utils is untyped: `update` declares every argument as required const result: DeferredObj<unknown> = update(this, this._array, key, values); return result; } _removeImpl(key: unknown): DeferredObj<unknown> { - // @ts-expect-error array_utils is untyped: `remove` declares every argument as required const result: DeferredObj<unknown> = remove(this, this._array, key); return result; diff --git a/packages/devextreme/js/__internal/data/array_utils.ts b/packages/devextreme/js/__internal/data/array_utils.ts new file mode 100644 index 000000000000..2e8dd20a73fd --- /dev/null +++ b/packages/devextreme/js/__internal/data/array_utils.ts @@ -0,0 +1,427 @@ +import { errors } from '@js/common/data/errors'; +import { keysEqual, rejectedPromise, trivialPromise } from '@js/common/data/utils'; +import config from '@js/core/config'; +import Guid from '@js/core/guid'; +import { compileGetter } from '@js/core/utils/data'; +import type { DeferredObj } from '@js/core/utils/deferred'; +import { extend } from '@js/core/utils/extend'; +import { deepExtendArraySafe } from '@js/core/utils/object'; +import { + isDefined, isEmptyObject, isObject, isPlainObject, isString, +} from '@js/core/utils/type'; +import type { StoreChange } from '@js/data/store'; +import { isCollectionLike } from '@ts/core/utils/m_object'; +import type { StoreKey } from '@ts/data/abstract_store'; + +export type KeyExpr = StoreKey | Function; + +export interface KeyInfo { + /* eslint-disable @typescript-eslint/method-signature-style */ + key(): KeyExpr | undefined; + keyOf(obj: unknown): unknown; + /* eslint-enable @typescript-eslint/method-signature-style */ +} + +interface CachedArray extends Array<unknown> { + _dataByKeyMap?: Record<string, unknown>; + _dataByKeyMapLength?: number; +} + +interface GroupItem { + items?: unknown[]; + collapsedItems?: unknown[]; +} + +export interface ApplyBatchOptions { + keyInfo: KeyInfo; + data: unknown[]; + changes: StoreChange[]; + groupCount?: number; + useInsertIndex?: boolean; + immutable?: boolean; + disableCache?: boolean; + logError?: boolean; + skipCopying?: boolean; +} + +export interface ApplyChangesOptions { + keyExpr?: StoreKey; + immutable?: boolean; +} + +function hasKey(target: unknown, keyOrKeys: KeyExpr): boolean { + let keys: string[] = []; + if (isString(keyOrKeys)) { + keys = [keyOrKeys]; + } else if (Array.isArray(keyOrKeys)) { + keys = keyOrKeys.slice(); + } + + while (keys.length) { + const key = keys.shift(); + if (isDefined(key) && isObject(target) && key in target) { + return true; + } + } + + return false; +} + +function generateDataByKeyMap(keyInfo: KeyInfo, array: CachedArray): void { + if (keyInfo.key() && (!array._dataByKeyMap || array._dataByKeyMapLength !== array.length)) { + const dataByKeyMap: Record<string, unknown> = {}; + const arrayLength = array.length; + for (let i = 0; i < arrayLength; i += 1) { + dataByKeyMap[JSON.stringify(keyInfo.keyOf(array[i]))] = array[i]; + } + + array._dataByKeyMap = dataByKeyMap; + array._dataByKeyMapLength = arrayLength; + } +} + +function getCacheValue(array: CachedArray, key: unknown): unknown { + return array._dataByKeyMap?.[JSON.stringify(key)]; +} + +function getHasKeyCacheValue(array: CachedArray, key: unknown): unknown { + if (array._dataByKeyMap) { + return array._dataByKeyMap[JSON.stringify(key)]; + } + + return true; +} + +function setDataByKeyMapValue(array: CachedArray, key: unknown, data: unknown): void { + if (array._dataByKeyMap) { + array._dataByKeyMap[JSON.stringify(key)] = data; + array._dataByKeyMapLength = (array._dataByKeyMapLength ?? 0) + (data ? 1 : -1); + } +} + +function indexByKey(keyInfo: KeyInfo, array: unknown[], key: unknown): number { + const keyExpr = keyInfo.key(); + + if (!getHasKeyCacheValue(array, key)) { + return -1; + } + + for (let i = 0, arrayLength = array.length; i < arrayLength; i += 1) { + if (keysEqual(keyExpr, keyInfo.keyOf(array[i]), key)) { + return i; + } + } + return -1; +} + +function findItems( + keyInfo: KeyInfo, + items: unknown[], + key: unknown, + groupCount: number, +): unknown[] | undefined { + if (groupCount) { + for (const item of items) { + const group: GroupItem = isObject(item) ? item : {}; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const childItems = group.items || group.collapsedItems || []; + const result = findItems(keyInfo, childItems, key, groupCount - 1); + if (result) { + return result; + } + } + } else if (indexByKey(keyInfo, items, key) >= 0) { + return items; + } + + return undefined; +} + +function getItems( + keyInfo: KeyInfo, + items: unknown[], + key: unknown, + groupCount: number | undefined, +): unknown[] { + if (groupCount) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + return findItems(keyInfo, items, key, groupCount) || []; + } + + return items; +} + +function cloneInstanceWithChangedPaths( + instance: unknown, + changes: unknown, + clonedInstances: WeakMap<object, unknown> = new WeakMap<object, unknown>(), +): unknown { + if (isCollectionLike(instance)) { + return instance; + } + + const source: object = isObject(instance) ? instance : {}; + const result: object = isObject(instance) + ? Object.create(Object.getPrototypeOf(instance)) + : {}; + + if (isObject(instance)) { + clonedInstances.set(instance, result); + } + + const instanceWithoutPrototype = { ...source }; + deepExtendArraySafe(result, instanceWithoutPrototype, true, true, true); + // eslint-disable-next-line no-restricted-syntax, guard-for-in + for (const name in instanceWithoutPrototype) { + const value: unknown = instanceWithoutPrototype[name]; + const change: unknown = isObject(changes) ? changes[name] : undefined; + + if (isObject(value) && !isPlainObject(value) + && isObject(change) && !clonedInstances.has(value)) { + result[name] = cloneInstanceWithChangedPaths(value, change, clonedInstances); + } + } + // eslint-disable-next-line no-restricted-syntax, guard-for-in + for (const name in result) { + const prop: unknown = result[name]; + + if (isObject(prop) && clonedInstances.has(prop)) { + result[name] = clonedInstances.get(prop); + } + } + + return result; +} + +function createObjectWithChanges( + target: unknown, + changes?: unknown, +): Record<string, unknown> { + const result = cloneInstanceWithChangedPaths(target, changes); + const extended: Record<string, unknown> = deepExtendArraySafe(result, changes, true, true, true); + + return extended; +} + +function getErrorResult( + isBatch: boolean | undefined, + logError: boolean | undefined, + errorCode: string, +): DeferredObj<unknown> | undefined { + if (!isBatch) { + const error: unknown = errors.Error(errorCode); + return rejectedPromise(error); + } + + if (logError) { + errors.log(errorCode); + } + + return undefined; +} + +function update( + keyInfo: KeyInfo, array: unknown[], key: unknown, data: unknown, +): DeferredObj<unknown>; +function update( + keyInfo: KeyInfo, array: unknown[], key: unknown, data: unknown, + isBatch: true, immutable?: boolean, logError?: boolean, +): undefined; +function update( + keyInfo: KeyInfo, + array: unknown[], + key: unknown, + data: unknown, + isBatch?: boolean, + immutable?: boolean, + logError?: boolean, +): DeferredObj<unknown> | undefined { + // eslint-disable-next-line @typescript-eslint/init-declarations + let target: unknown; + const extendComplexObject = true; + const keyExpr = keyInfo.key(); + + if (keyExpr) { + if (hasKey(data, keyExpr) && !keysEqual(keyExpr, key, keyInfo.keyOf(data))) { + return getErrorResult(isBatch, logError, 'E4017'); + } + + target = getCacheValue(array, key); + if (!target) { + const index = indexByKey(keyInfo, array, key); + if (index < 0) { + return getErrorResult(isBatch, logError, 'E4009'); + } + + target = array[index]; + + if (immutable === true && isDefined(target)) { + const newTarget = createObjectWithChanges(target, data); + array[index] = newTarget; + return isBatch ? undefined : trivialPromise(newTarget, key); + } + } + } else { + target = key; + } + + deepExtendArraySafe(target, data, extendComplexObject, false, true, true); + if (!isBatch) { + if (config().useLegacyStoreResult) { + return trivialPromise(key, data); + } + return trivialPromise(target, key); + } + + return undefined; +} + +function insert( + keyInfo: KeyInfo, array: unknown[], data: unknown, index?: number, +): DeferredObj<unknown>; +function insert( + keyInfo: KeyInfo, array: unknown[], data: unknown, index: number | undefined, + isBatch: true, logError?: boolean, skipCopying?: boolean, +): undefined; +function insert( + keyInfo: KeyInfo, + array: unknown[], + data: unknown, + index?: number, + isBatch?: boolean, + logError?: boolean, + skipCopying?: boolean, +): DeferredObj<unknown> | undefined { + // eslint-disable-next-line @typescript-eslint/init-declarations + let keyValue: unknown; + const keyExpr = keyInfo.key(); + + const obj: unknown = isPlainObject(data) && !skipCopying ? extend({}, data) : data; + + if (keyExpr) { + keyValue = keyInfo.keyOf(obj); + if (keyValue === undefined || (typeof keyValue === 'object' && isEmptyObject(keyValue))) { + if (Array.isArray(keyExpr)) { + throw errors.Error('E4007'); + } + + const generatedKey = String(new Guid()); + + if (isString(keyExpr) && isObject(obj)) { + obj[keyExpr] = generatedKey; + } + keyValue = generatedKey; + } else if (array[indexByKey(keyInfo, array, keyValue)] !== undefined) { + return getErrorResult(isBatch, logError, 'E4008'); + } + } else { + keyValue = obj; + } + if (isDefined(index) && index >= 0) { + array.splice(index, 0, obj); + } else { + array.push(obj); + } + + setDataByKeyMapValue(array, keyValue, obj); + + if (!isBatch) { + return trivialPromise(config().useLegacyStoreResult ? data : obj, keyValue); + } + + return undefined; +} + +function remove( + keyInfo: KeyInfo, array: unknown[], key: unknown, +): DeferredObj<unknown>; +function remove( + keyInfo: KeyInfo, array: unknown[], key: unknown, isBatch: true, logError?: boolean, +): undefined; +function remove( + keyInfo: KeyInfo, + array: unknown[], + key: unknown, + isBatch?: boolean, + logError?: boolean, +): DeferredObj<unknown> | undefined { + const index = indexByKey(keyInfo, array, key); + if (index > -1) { + array.splice(index, 1); + setDataByKeyMapValue(array, key, null); + } + if (!isBatch) { + return trivialPromise(key); + } + if (index < 0) { + return getErrorResult(isBatch, logError, 'E4009'); + } + + return undefined; +} + +function applyBatch({ + keyInfo, data, changes, groupCount, useInsertIndex, immutable, + disableCache, logError, skipCopying, +}: ApplyBatchOptions): unknown[] { + const resultItems = immutable === true ? [...data] : data; + + changes.forEach((item) => { + const items = item.type === 'insert' + ? resultItems + : getItems(keyInfo, resultItems, item.key, groupCount); + + if (!disableCache) { + generateDataByKeyMap(keyInfo, items); + } + + const insertIndex = useInsertIndex && isDefined(item.index) ? item.index : -1; + + // eslint-disable-next-line default-case + switch (item.type) { + case 'update': + update(keyInfo, items, item.key, item.data, true, immutable, logError); + break; + case 'insert': + insert(keyInfo, items, item.data, insertIndex, true, logError, skipCopying); + break; + case 'remove': + remove(keyInfo, items, item.key, true, logError); + break; + } + }); + return resultItems; +} + +function applyChanges( + data: unknown[], + changes: StoreChange[], + options: ApplyChangesOptions = {}, +): unknown[] { + const { keyExpr = 'id', immutable = true } = options; + // @ts-expect-error compileGetter() is declared as (expr: string) => unknown + const keyGetter: Function = compileGetter(keyExpr); + const keyInfo: KeyInfo = { + key: () => keyExpr, + keyOf: (obj: unknown): unknown => keyGetter(obj), + }; + + return applyBatch({ + keyInfo, + data, + changes, + immutable, + disableCache: true, + logError: true, + }); +} + +export { + applyBatch, + applyChanges, + createObjectWithChanges, + indexByKey, + insert, + remove, + update, +}; diff --git a/packages/devextreme/js/__internal/data/custom_store.ts b/packages/devextreme/js/__internal/data/custom_store.ts index 90e720d4056c..77d22482a4c7 100644 --- a/packages/devextreme/js/__internal/data/custom_store.ts +++ b/packages/devextreme/js/__internal/data/custom_store.ts @@ -209,7 +209,7 @@ function runRawLoadWithQuery( const rawDataQuery = arrayQuery(rawData, { errorHandler: store._errorHandler }); const waitList: DeferredObj<unknown>[] = []; - const result: { items?: unknown[]; totalCount?: unknown } = {}; + const result: { items?: unknown; totalCount?: unknown } = {}; if (!countOnly) { const itemsQuery = storeHelper.queryByOptions(rawDataQuery, loadOptions, false); @@ -217,7 +217,7 @@ function runRawLoadWithQuery( result.items = rawData.slice(0); } else { const itemsPromise: DeferredObj<unknown> = itemsQuery.enumerate() - .done((asyncResult: unknown[]) => { + .done((asyncResult: unknown) => { result.items = asyncResult; }); waitList.push(itemsPromise); @@ -367,7 +367,6 @@ class CustomStore extends Store { _pushImpl(changes: StoreChange[]): void { if (this.__rawData) { - // @ts-expect-error array_utils is untyped: `applyBatch` destructures every option as required applyBatch({ keyInfo: this, data: this.__rawData, diff --git a/packages/devextreme/js/__internal/data/data_controller/data_controller.ts b/packages/devextreme/js/__internal/data/data_controller/data_controller.ts index 840781fd8302..3b4d5b4bf14f 100644 --- a/packages/devextreme/js/__internal/data/data_controller/data_controller.ts +++ b/packages/devextreme/js/__internal/data/data_controller/data_controller.ts @@ -65,7 +65,6 @@ class DataController { this._isSharedDataSource = true; this._dataSource = dataSourceOptions; } else { - // @ts-expect-error const normalizedDataSourceOptions = normalizeDataSourceOptions(dataSourceOptions); this._dataSource = new DataSource( extend(true, {}, {}, normalizedDataSourceOptions), diff --git a/packages/devextreme/js/__internal/data/data_source/data_source.ts b/packages/devextreme/js/__internal/data/data_source/data_source.ts index 72aae3df3fab..1f1cc8ee8fec 100644 --- a/packages/devextreme/js/__internal/data/data_source/data_source.ts +++ b/packages/devextreme/js/__internal/data/data_source/data_source.ts @@ -28,6 +28,7 @@ import type { ChangedEvent, DataSourceEventName, EventOptionName, LoadOperation, LoadResult, NormalizedDataSourceOptions, StoreLoadOptions, } from './types'; +import type { Mapper } from './utils'; // Mirrors the coercion the global `isFinite` applies to non-numeric values. const isFiniteValue = (value: unknown): value is number => isFinite(Number(value)); @@ -58,11 +59,11 @@ export class DataSource { _onPushHandler: Function; - _aggregationTimeoutId?: number; + _aggregationTimeoutId?: ReturnType<typeof setTimeout>; _storeLoadOptions: StoreLoadOptions; - _mapFunc?: Function; + _mapFunc?: Mapper; _postProcessFunc?: Function; @@ -567,7 +568,6 @@ export class DataSource { dataSourceChanges = changingArgs.postProcessChanges(dataSourceChanges); } - // @ts-expect-error array_utils is untyped: `applyBatch` destructures every option as required applyBatch({ keyInfo: this.store(), data: items, diff --git a/packages/devextreme/js/__internal/data/data_source/m_utils.ts b/packages/devextreme/js/__internal/data/data_source/m_utils.ts deleted file mode 100644 index 40192c669e20..000000000000 --- a/packages/devextreme/js/__internal/data/data_source/m_utils.ts +++ /dev/null @@ -1,121 +0,0 @@ -import ArrayStore from '@js/common/data/array_store'; -import { CustomStore } from '@js/common/data/custom_store'; -import { normalizeSortingInfo } from '@js/common/data/utils'; -import ajaxUtils from '@js/core/utils/ajax'; -import { extend } from '@js/core/utils/extend'; -import { each, map } from '@js/core/utils/iterator'; -import { isPlainObject } from '@js/core/utils/type'; -import Store from '@js/data/abstract_store'; - -export const CANCELED_TOKEN = 'canceled'; - -export const isPending = (deferred) => deferred.state() === 'pending'; - -export const normalizeStoreLoadOptionAccessorArguments = (originalArguments) => { - // eslint-disable-next-line default-case - switch (originalArguments.length) { - case 0: - return undefined; - case 1: - return originalArguments[0]; - } - return [].slice.call(originalArguments); -}; - -const mapGroup = (group, level, mapper) => map(group, (item) => { - const { items, ...restItem } = item; - return { - ...restItem, - items: mapRecursive(item.items, level - 1, mapper), - }; -}); - -const mapRecursive = (items, level, mapper) => { - if (!Array.isArray(items)) return items; - return level ? mapGroup(items, level, mapper) : map(items, mapper); -}; - -export const mapDataRespectingGrouping = (items, mapper, groupInfo) => { - const level = groupInfo ? normalizeSortingInfo(groupInfo).length : 0; - - return mapRecursive(items, level, mapper); -}; - -export const normalizeLoadResult = (data, extra) => { - if (data?.data) { - extra = data; - data = data.data; - } - - if (!Array.isArray(data)) { - data = [data]; - } - - return { - data, - extra, - }; -}; - -const createCustomStoreFromLoadFunc = (options) => { - const storeConfig = {}; - - each(['useDefaultSearch', 'key', 'load', 'loadMode', 'cacheRawData', 'byKey', 'lookup', 'totalCount', 'insert', 'update', 'remove'], function () { - storeConfig[this] = options[this]; - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete options[this]; - }); - return new CustomStore(storeConfig); -}; - -const createStoreFromConfig = (storeConfig) => { - const alias = storeConfig.type; - - delete storeConfig.type; - // @ts-expect-error - return Store.create(alias, storeConfig); -}; - -const createCustomStoreFromUrl = (url, normalizationOptions) => new CustomStore({ - load: () => ajaxUtils.sendRequest({ url, dataType: 'json' }), - loadMode: normalizationOptions?.fromUrlLoadMode, -}); - -export const normalizeDataSourceOptions = (options, normalizationOptions) => { - let store; - - if (typeof options === 'string') { - options = { - paginate: false, - store: createCustomStoreFromUrl(options, normalizationOptions), - }; - } - - if (options === undefined) { - options = []; - } - - if (Array.isArray(options) || options instanceof Store) { - options = { store: options }; - } else { - options = extend({}, options); - } - - if (options.store === undefined) { - options.store = []; - } - - store = options.store; - - if ('load' in options) { - store = createCustomStoreFromLoadFunc(options); - } else if (Array.isArray(store)) { - store = new ArrayStore(store); - } else if (isPlainObject(store)) { - store = createStoreFromConfig(extend({}, store)); - } - - options.store = store; - - return options; -}; diff --git a/packages/devextreme/js/__internal/data/data_source/m_operation_manager.ts b/packages/devextreme/js/__internal/data/data_source/operation_manager.ts similarity index 53% rename from packages/devextreme/js/__internal/data/data_source/m_operation_manager.ts rename to packages/devextreme/js/__internal/data/data_source/operation_manager.ts index cfde35fffb6f..4cb21cb79b92 100644 --- a/packages/devextreme/js/__internal/data/data_source/m_operation_manager.ts +++ b/packages/devextreme/js/__internal/data/data_source/operation_manager.ts @@ -1,32 +1,24 @@ import { CANCELED_TOKEN } from '@js/common/data/data_source/utils'; +import type { DeferredObj } from '@js/core/utils/deferred'; export default class OperationManager { - constructor() { - // @ts-expect-error - this._counter = -1; - // @ts-expect-error - this._deferreds = {}; - } + _counter = -1; + + _deferreds: Record<number, DeferredObj<unknown>> = {}; - add(deferred) { - // @ts-expect-error - this._counter++; - // @ts-expect-error + add(deferred: DeferredObj<unknown>): number { + this._counter += 1; this._deferreds[this._counter] = deferred; - // @ts-expect-error return this._counter; } - remove(operationId) { - // @ts-expect-error + remove(operationId: number): boolean { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete return delete this._deferreds[operationId]; } - cancel(operationId) { - // @ts-expect-error + cancel(operationId: number): boolean { if (operationId in this._deferreds) { - // @ts-expect-error this._deferreds[operationId].reject(CANCELED_TOKEN); return true; } @@ -34,13 +26,10 @@ export default class OperationManager { return false; } - cancelAll() { - // @ts-expect-error + cancelAll(): void { while (this._counter > -1) { - // @ts-expect-error this.cancel(this._counter); - // @ts-expect-error - this._counter--; + this._counter -= 1; } } } diff --git a/packages/devextreme/js/__internal/data/data_source/utils.ts b/packages/devextreme/js/__internal/data/data_source/utils.ts new file mode 100644 index 000000000000..c6aed87cfcc6 --- /dev/null +++ b/packages/devextreme/js/__internal/data/data_source/utils.ts @@ -0,0 +1,174 @@ +import ArrayStore from '@js/common/data/array_store'; +import { CustomStore } from '@js/common/data/custom_store'; +import { normalizeSortingInfo } from '@js/common/data/utils'; +import ajaxUtils from '@js/core/utils/ajax'; +import type { DeferredObj } from '@js/core/utils/deferred'; +import { extend } from '@js/core/utils/extend'; +import { isObject, isPlainObject } from '@js/core/utils/type'; +import Store from '@ts/data/abstract_store'; + +import type { NormalizedDataSourceOptions } from './types'; + +export const CANCELED_TOKEN = 'canceled'; + +export type Mapper = (item: unknown) => unknown; + +interface GroupItem { + items?: unknown[]; +} + +export interface NormalizationOptions { + fromUrlLoadMode?: string; +} + +interface DataSourceOptionsInput extends Record<string, unknown> { + store?: unknown; +} + +export const isPending = (deferred: DeferredObj<unknown>): boolean => deferred.state() === 'pending'; + +export const normalizeStoreLoadOptionAccessorArguments = ( + originalArguments: unknown[], +): unknown => { + switch (originalArguments.length) { + case 0: + return undefined; + case 1: + return originalArguments[0]; + default: + return originalArguments.slice(); + } +}; + +const mapRecursive = (items: unknown, level: number, mapper: Mapper): unknown => { + if (!Array.isArray(items)) return items; + + if (!level) { + return items.map(mapper); + } + + return items.map((item) => { + const groupItem: GroupItem = isObject(item) ? item : {}; + + return { + ...groupItem, + items: mapRecursive(groupItem.items, level - 1, mapper), + }; + }); +}; + +export const mapDataRespectingGrouping = ( + items: unknown[], + mapper: Mapper, + groupInfo?: unknown, +): unknown[] => { + const level = groupInfo ? normalizeSortingInfo(groupInfo).length : 0; + const mapped = mapRecursive(items, level, mapper); + + return Array.isArray(mapped) ? mapped : []; +}; + +export interface NormalizedLoadResult { + data: unknown[]; + extra: unknown; +} + +export const normalizeLoadResult = (data: unknown, extra?: unknown): NormalizedLoadResult => { + const loadResult: { data?: unknown } = isObject(data) ? data : {}; + + const resultData: unknown = loadResult.data ? loadResult.data : data; + const resultExtra: unknown = loadResult.data ? data : extra; + + return { + data: Array.isArray(resultData) ? resultData : [resultData], + extra: resultExtra, + }; +}; + +const CUSTOM_STORE_OPTION_NAMES = [ + 'useDefaultSearch', 'key', 'load', 'loadMode', 'cacheRawData', 'byKey', + 'lookup', 'totalCount', 'insert', 'update', 'remove', +]; + +const createCustomStoreFromLoadFunc = (options: DataSourceOptionsInput): CustomStore => { + const storeConfig: Record<string, unknown> = {}; + + CUSTOM_STORE_OPTION_NAMES.forEach((optionName) => { + storeConfig[optionName] = options[optionName]; + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete options[optionName]; + }); + + return new CustomStore(storeConfig); +}; + +const createStoreFromConfig = (storeConfig: Record<string, unknown>): Store => { + const alias = String(storeConfig.type); + + delete storeConfig.type; + + const store: Store = Store.create(alias, storeConfig); + + return store; +}; + +const createCustomStoreFromUrl = ( + url: string, + normalizationOptions?: NormalizationOptions, +): CustomStore => new CustomStore({ + load: (): unknown => ajaxUtils.sendRequest({ url, dataType: 'json' }), + loadMode: normalizationOptions?.fromUrlLoadMode, +}); + +const resolveStore = (options: DataSourceOptionsInput): Store => { + if ('load' in options) { + return createCustomStoreFromLoadFunc(options); + } + + const { store } = options; + + if (Array.isArray(store)) { + return new ArrayStore(store); + } + if (isPlainObject(store)) { + return createStoreFromConfig(extend({}, store)); + } + + // @ts-expect-error the `store` option is user-provided and is not necessarily a Store + const passedThrough: Store = store; + + return passedThrough; +}; + +export const normalizeDataSourceOptions = ( + options: unknown, + normalizationOptions?: NormalizationOptions, +): NormalizedDataSourceOptions => { + let source: unknown = options; + + if (typeof source === 'string') { + source = { + paginate: false, + store: createCustomStoreFromUrl(source, normalizationOptions), + }; + } + + if (source === undefined) { + source = []; + } + + const normalized: DataSourceOptionsInput = Array.isArray(source) || source instanceof Store + ? { store: source } + : extend({}, source); + + if (normalized.store === undefined) { + normalized.store = []; + } + + const store = resolveStore(normalized); + + return { + ...normalized, + store, + }; +}; diff --git a/packages/devextreme/js/__internal/data/endpoint_selector.ts b/packages/devextreme/js/__internal/data/endpoint_selector.ts new file mode 100644 index 000000000000..7726e93a3db4 --- /dev/null +++ b/packages/devextreme/js/__internal/data/endpoint_selector.ts @@ -0,0 +1,47 @@ +/* global Debug */ +import errors from '@js/core/errors'; +import { getWindow } from '@js/core/utils/window'; + +const window = getWindow(); + +interface Endpoint { + local: string; + production?: string; +} + +type EndpointConfig = Record<string, Endpoint | undefined>; + +let isWinJsOrigin = false; +let isLocalOrigin = false; + +function isLocalHostName(url: string): boolean { + return /^(localhost$|127\.)/i.test(url); // TODO more precise check for 127.x.x.x IP +} + +class EndpointSelector { + config: EndpointConfig; + + constructor(config: EndpointConfig) { + this.config = config; + isWinJsOrigin = window.location.protocol === 'ms-appx:'; + isLocalOrigin = isLocalHostName(window.location.hostname); + } + + urlFor(key: string): string { + const bag = this.config[key]; + if (!bag) { + throw errors.Error('E0006'); + } + + if (bag.production) { + // @ts-expect-error `Debug` is a WinJS global that has no ambient declaration here + if ((isWinJsOrigin && !Debug.debuggerEnabled) || (!isWinJsOrigin && !isLocalOrigin)) { + return bag.production; + } + } + + return bag.local; + } +} + +export default EndpointSelector; diff --git a/packages/devextreme/js/__internal/data/m_errors.ts b/packages/devextreme/js/__internal/data/errors.ts similarity index 84% rename from packages/devextreme/js/__internal/data/m_errors.ts rename to packages/devextreme/js/__internal/data/errors.ts index 0268bd78af3d..076821979898 100644 --- a/packages/devextreme/js/__internal/data/m_errors.ts +++ b/packages/devextreme/js/__internal/data/errors.ts @@ -1,5 +1,6 @@ import coreErrors from '@js/core/errors'; import errorUtils from '@js/core/utils/error'; +import { isObject } from '@js/core/utils/type'; export const errors = errorUtils(coreErrors.ERROR_MESSAGES, { @@ -59,15 +60,20 @@ export const errors = errorUtils(coreErrors.ERROR_MESSAGES, { W4002: 'Data loading has failed for some cells due to the following error: {0}', }); + +export type DataErrorHandler = (error: unknown) => void; + // eslint-disable-next-line import/no-mutable-exports -export let errorHandler = null; -export const handleError = function (error) { +export let errorHandler: DataErrorHandler | null = null; + +export const handleError = function (error: unknown): void { /// #DEBUG - const id = error && '__id' in error ? error.__id : 'E4000'; + const id = isObject(error) && '__id' in error ? error.__id : 'E4000'; errors.log(id, error); /// #ENDDEBUG - // @ts-expect-error errorHandler?.(error); }; -// eslint-disable-next-line no-return-assign -export const setErrorHandler = (handler) => errorHandler = handler; + +export const setErrorHandler = (handler: DataErrorHandler | null): void => { + errorHandler = handler; +}; diff --git a/packages/devextreme/js/__internal/data/m_local_store.ts b/packages/devextreme/js/__internal/data/local_store.ts similarity index 61% rename from packages/devextreme/js/__internal/data/m_local_store.ts rename to packages/devextreme/js/__internal/data/local_store.ts index 14c956b01cd9..7bb8e4121ab8 100644 --- a/packages/devextreme/js/__internal/data/m_local_store.ts +++ b/packages/devextreme/js/__internal/data/local_store.ts @@ -3,14 +3,26 @@ import eventsEngine from '@js/common/core/events/core/events_engine'; import ArrayStore from '@js/common/data/array_store'; import { errors } from '@js/common/data/errors'; import domAdapter from '@js/core/dom_adapter'; +import type { DeferredObj } from '@js/core/utils/deferred'; import { getWindow } from '@js/core/utils/window'; +import type { ArrayStoreOptions } from '@ts/data/array_store'; import Store from './abstract_store'; const window = getWindow(); +export interface LocalStoreOptions extends ArrayStoreOptions { + name?: string; + immediate?: boolean; + flushInterval?: number; +} + +interface LocalStoreData { + _array: unknown[]; +} + class LocalStoreBackend { - _store: any; + _store: LocalStoreData; _dirty: boolean; @@ -18,7 +30,7 @@ class LocalStoreBackend { _key: string; - constructor(store, storeOptions) { + constructor(store: LocalStoreData, storeOptions: LocalStoreOptions) { this._store = store; this._dirty = !!storeOptions.data; @@ -31,14 +43,16 @@ class LocalStoreBackend { this.save(); - const immediate = this._immediate = storeOptions.immediate; + const immediate = storeOptions.immediate ?? false; + this._immediate = immediate; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const flushInterval = Math.max(100, storeOptions.flushInterval || 10 * 1000); if (!immediate) { const saveProxy = this.save.bind(this); setInterval(saveProxy, flushInterval); eventsEngine.on(window, 'beforeunload', saveProxy); - // @ts-expect-error + // @ts-expect-error `cordova` is injected by the Cordova container, `Window` has no such field if (window.cordova) { domAdapter.listen(domAdapter.getDocument(), 'pause', saveProxy, false); } @@ -66,16 +80,17 @@ class LocalStoreBackend { this._dirty = false; } - _loadImpl(): any { + _loadImpl(): unknown[] { const raw = window.localStorage.getItem(this._key); if (raw) { - return JSON.parse(raw); + const stored: unknown = JSON.parse(raw); + return Array.isArray(stored) ? stored : []; } return []; } - _saveImpl(array): void { + _saveImpl(array: unknown[]): void { if (!array.length) { window.localStorage.removeItem(this._key); } else { @@ -83,22 +98,21 @@ class LocalStoreBackend { } } } + class LocalStore extends ArrayStore { _backend: LocalStoreBackend; - _array: any; - - constructor(options) { - if (typeof options === 'string') { - options = { name: options }; - } else { - options = options || {}; - } + constructor(options?: LocalStoreOptions | string) { + const storeOptions: LocalStoreOptions = typeof options === 'string' + ? { name: options } + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + : options || {}; - super(options); - this._array = options.data || []; + super(storeOptions); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + this._array = storeOptions.data || []; - this._backend = new LocalStoreBackend(this, options); + this._backend = new LocalStoreBackend(this, storeOptions); this._backend.load(); } @@ -111,17 +125,17 @@ class LocalStore extends ArrayStore { this._backend.notifyChanged(); } - _insertImpl(values): any { + _insertImpl(values: unknown): DeferredObj<unknown> { const b = this._backend; return super._insertImpl(values).done(b.notifyChanged.bind(b)); } - _updateImpl(key, values): any { + _updateImpl(key: unknown, values: unknown): DeferredObj<unknown> { const b = this._backend; return super._updateImpl(key, values).done(b.notifyChanged.bind(b)); } - _removeImpl(key): any { + _removeImpl(key: unknown): DeferredObj<unknown> { const b = this._backend; return super._removeImpl(key).done(b.notifyChanged.bind(b)); } diff --git a/packages/devextreme/js/__internal/data/m_array_utils.ts b/packages/devextreme/js/__internal/data/m_array_utils.ts deleted file mode 100644 index 22d11b7c9c69..000000000000 --- a/packages/devextreme/js/__internal/data/m_array_utils.ts +++ /dev/null @@ -1,284 +0,0 @@ -import { errors } from '@js/common/data/errors'; -import { keysEqual, rejectedPromise, trivialPromise } from '@js/common/data/utils'; -import config from '@js/core/config'; -import Guid from '@js/core/guid'; -import { compileGetter } from '@js/core/utils/data'; -import { extend } from '@js/core/utils/extend'; -import { deepExtendArraySafe } from '@js/core/utils/object'; -import { - isDefined, isEmptyObject, isObject, isPlainObject, -} from '@js/core/utils/type'; -import { isCollectionLike } from '@ts/core/utils/m_object'; - -function hasKey(target, keyOrKeys) { - let key; - // @ts-expect-error - const keys = typeof keyOrKeys === 'string' ? keyOrKeys.split() : keyOrKeys.slice(); - - while (keys.length) { - key = keys.shift(); - if (key in target) { - return true; - } - } - - return false; -} - -function findItems(keyInfo, items, key, groupCount) { - let childItems; - let result; - - if (groupCount) { - for (let i = 0; i < items.length; i++) { - childItems = items[i].items || items[i].collapsedItems || []; - result = findItems(keyInfo, childItems || [], key, groupCount - 1); - if (result) { - return result; - } - } - } else if (indexByKey(keyInfo, items, key) >= 0) { - return items; - } -} - -function getItems(keyInfo, items, key, groupCount) { - if (groupCount) { - return findItems(keyInfo, items, key, groupCount) || []; - } - - return items; -} - -function generateDataByKeyMap(keyInfo, array) { - if (keyInfo.key() && (!array._dataByKeyMap || array._dataByKeyMapLength !== array.length)) { - const dataByKeyMap = {}; - const arrayLength = array.length; - for (let i = 0; i < arrayLength; i++) { - dataByKeyMap[JSON.stringify(keyInfo.keyOf(array[i]))] = array[i]; - } - - array._dataByKeyMap = dataByKeyMap; - array._dataByKeyMapLength = arrayLength; - } -} - -function getCacheValue(array, key) { - if (array._dataByKeyMap) { - return array._dataByKeyMap[JSON.stringify(key)]; - } -} - -function getHasKeyCacheValue(array, key) { - if (array._dataByKeyMap) { - return array._dataByKeyMap[JSON.stringify(key)]; - } - - return true; -} - -function setDataByKeyMapValue(array, key, data) { - if (array._dataByKeyMap) { - array._dataByKeyMap[JSON.stringify(key)] = data; - array._dataByKeyMapLength += data ? 1 : -1; - } -} - -function cloneInstanceWithChangedPaths(instance, changes, clonedInstances) { - if (isCollectionLike(instance)) { - return instance; - } - - clonedInstances = clonedInstances || new WeakMap(); - - const result = instance ? Object.create(Object.getPrototypeOf(instance)) : {}; - if (instance) { - clonedInstances.set(instance, result); - } - - const instanceWithoutPrototype = { ...instance }; - deepExtendArraySafe(result, instanceWithoutPrototype, true, true, true); - // eslint-disable-next-line no-restricted-syntax, guard-for-in - for (const name in instanceWithoutPrototype) { - const value = instanceWithoutPrototype[name]; - const change = changes?.[name]; - - if (isObject(value) && !isPlainObject(value) && isObject(change) && !clonedInstances.has(value)) { - result[name] = cloneInstanceWithChangedPaths(value, change, clonedInstances); - } - } - // eslint-disable-next-line no-restricted-syntax, guard-for-in - for (const name in result) { - const prop = result[name]; - - if (isObject(prop) && clonedInstances.has(prop)) { - result[name] = clonedInstances.get(prop); - } - } - - return result; -} - -function createObjectWithChanges(target, changes) { - // @ts-expect-error - const result = cloneInstanceWithChangedPaths(target, changes); - - return deepExtendArraySafe(result, changes, true, true, true); -} - -function applyBatch({ - keyInfo, data, changes, groupCount, useInsertIndex, immutable, disableCache, logError, skipCopying, -}) { - const resultItems = immutable === true ? [...data] : data; - - changes.forEach((item) => { - const items = item.type === 'insert' ? resultItems : getItems(keyInfo, resultItems, item.key, groupCount); - - !disableCache && generateDataByKeyMap(keyInfo, items); - // eslint-disable-next-line default-case - switch (item.type) { - case 'update': update(keyInfo, items, item.key, item.data, true, immutable, logError); break; - case 'insert': insert(keyInfo, items, item.data, useInsertIndex && isDefined(item.index) ? item.index : -1, true, logError, skipCopying); break; - case 'remove': remove(keyInfo, items, item.key, true, logError); break; - } - }); - return resultItems; -} - -function getErrorResult(isBatch, logError, errorCode) { - // @ts-expect-error - return !isBatch ? rejectedPromise(errors.Error(errorCode)) : logError && errors.log(errorCode); -} - -function applyChanges(data, changes, options = {}) { - // @ts-expect-error - const { keyExpr = 'id', immutable = true } = options; - const keyGetter = compileGetter(keyExpr); - const keyInfo = { - key: () => keyExpr, - // @ts-expect-error - keyOf: (obj) => keyGetter(obj), - }; - // @ts-expect-error - return applyBatch({ - keyInfo, - data, - changes, - immutable, - disableCache: true, - logError: true, - }); -} - -function update(keyInfo, array, key, data, isBatch, immutable, logError) { - let target; - const extendComplexObject = true; - const keyExpr = keyInfo.key(); - - if (keyExpr) { - if (hasKey(data, keyExpr) && !keysEqual(keyExpr, key, keyInfo.keyOf(data))) { - return getErrorResult(isBatch, logError, 'E4017'); - } - - target = getCacheValue(array, key); - if (!target) { - const index = indexByKey(keyInfo, array, key); - if (index < 0) { - return getErrorResult(isBatch, logError, 'E4009'); - } - - target = array[index]; - - if (immutable === true && isDefined(target)) { - const newTarget = createObjectWithChanges(target, data); - array[index] = newTarget; - // @ts-expect-error - return !isBatch && trivialPromise(newTarget, key); - } - } - } else { - target = key; - } - - deepExtendArraySafe(target, data, extendComplexObject, false, true, true); - if (!isBatch) { - if (config().useLegacyStoreResult) { - // @ts-expect-error - return trivialPromise(key, data); - } - // @ts-expect-error - return trivialPromise(target, key); - } -} - -function insert(keyInfo, array, data, index, isBatch, logError, skipCopying) { - let keyValue; - const keyExpr = keyInfo.key(); - - const obj = isPlainObject(data) && !skipCopying ? extend({}, data) : data; - - if (keyExpr) { - keyValue = keyInfo.keyOf(obj); - if (keyValue === undefined || typeof keyValue === 'object' && isEmptyObject(keyValue)) { - if (Array.isArray(keyExpr)) { - throw errors.Error('E4007'); - } - keyValue = obj[keyExpr] = String(new Guid()); - } else if (array[indexByKey(keyInfo, array, keyValue)] !== undefined) { - return getErrorResult(isBatch, logError, 'E4008'); - } - } else { - keyValue = obj; - } - if (index >= 0) { - array.splice(index, 0, obj); - } else { - array.push(obj); - } - - setDataByKeyMapValue(array, keyValue, obj); - - if (!isBatch) { - // @ts-expect-error - return trivialPromise(config().useLegacyStoreResult ? data : obj, keyValue); - } -} - -function remove(keyInfo, array, key, isBatch, logError) { - const index = indexByKey(keyInfo, array, key); - if (index > -1) { - array.splice(index, 1); - setDataByKeyMapValue(array, key, null); - } - if (!isBatch) { - // @ts-expect-error - return trivialPromise(key); - } if (index < 0) { - return getErrorResult(isBatch, logError, 'E4009'); - } -} - -function indexByKey(keyInfo, array, key) { - const keyExpr = keyInfo.key(); - - if (!getHasKeyCacheValue(array, key)) { - return -1; - } - - for (let i = 0, arrayLength = array.length; i < arrayLength; i++) { - if (keysEqual(keyExpr, keyInfo.keyOf(array[i]), key)) { - return i; - } - } - return -1; -} - -export { - applyBatch, - applyChanges, - createObjectWithChanges, - indexByKey, - insert, - remove, - update, -}; diff --git a/packages/devextreme/js/__internal/data/m_endpoint_selector.ts b/packages/devextreme/js/__internal/data/m_endpoint_selector.ts deleted file mode 100644 index 22f200d13ddd..000000000000 --- a/packages/devextreme/js/__internal/data/m_endpoint_selector.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* global Debug */ -import errors from '@js/core/errors'; -import { getWindow } from '@js/core/utils/window'; - -const window = getWindow(); - -let IS_WINJS_ORIGIN; -let IS_LOCAL_ORIGIN; - -function isLocalHostName(url) { - return /^(localhost$|127\.)/i.test(url); // TODO more precise check for 127.x.x.x IP -} - -const EndpointSelector = function (config) { - this.config = config; - IS_WINJS_ORIGIN = window.location.protocol === 'ms-appx:'; - IS_LOCAL_ORIGIN = isLocalHostName(window.location.hostname); -}; - -EndpointSelector.prototype = { - urlFor(key) { - const bag = this.config[key]; - if (!bag) { - throw errors.Error('E0006'); - } - - if (bag.production) { - // @ts-expect-error - if (IS_WINJS_ORIGIN && !Debug.debuggerEnabled || !IS_WINJS_ORIGIN && !IS_LOCAL_ORIGIN) { - return bag.production; - } - } - - return bag.local; - }, - -}; - -export default EndpointSelector; diff --git a/packages/devextreme/js/__internal/data/m_query.ts b/packages/devextreme/js/__internal/data/m_query.ts deleted file mode 100644 index c35f346ae08c..000000000000 --- a/packages/devextreme/js/__internal/data/m_query.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { queryImpl } from '@js/common/data/query_implementation'; - -const query = function () { - const impl = Array.isArray(arguments[0]) ? 'array' : 'remote'; - // @ts-expect-error - return queryImpl[impl].apply(this, arguments); -}; - -export default query; diff --git a/packages/devextreme/js/__internal/data/m_remote_query.ts b/packages/devextreme/js/__internal/data/m_remote_query.ts deleted file mode 100644 index 53fb79c54c50..000000000000 --- a/packages/devextreme/js/__internal/data/m_remote_query.ts +++ /dev/null @@ -1,155 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import arrayQueryImpl from '@js/common/data/array_query'; -import { errors, handleError } from '@js/common/data/errors'; -import queryAdapters from '@js/common/data/query_adapters'; -import { Deferred } from '@js/core/utils/deferred'; -import { each } from '@js/core/utils/iterator'; -import { isFunction } from '@js/core/utils/type'; - -const remoteQueryImpl = function (url, queryOptions, tasks) { - tasks = tasks || []; - queryOptions = queryOptions || {}; - - const createTask = function (name, args) { - return { name, args }; - }; - - const exec = function (executorTask) { - // @ts-expect-error - const d = new Deferred(); - let _adapterFactory; - let _adapter; - let _taskQueue; - let _currentTask; - let _mergedSortArgs; - - const rejectWithNotify = function (error) { - const handler = queryOptions.errorHandler; - if (handler) { - handler(error); - } - - handleError(error); - d.reject(error); - }; - - function mergeSortTask(task) { - // eslint-disable-next-line default-case - switch (task.name) { - case 'sortBy': - _mergedSortArgs = [task.args]; - return true; - - case 'thenBy': - if (!_mergedSortArgs) { - throw errors.Error('E4004'); - } - - _mergedSortArgs.push(task.args); - return true; - } - - return false; - } - - function unmergeSortTasks() { - const head = _taskQueue[0]; - const unmergedTasks = []; - - if (head && head.name === 'multiSort') { - _taskQueue.shift(); - each(head.args[0], function () { - // @ts-expect-error - unmergedTasks.push(createTask(unmergedTasks.length ? 'thenBy' : 'sortBy', this)); - }); - } - - _taskQueue = unmergedTasks.concat(_taskQueue); - } - - try { - _adapterFactory = queryOptions.adapter; - if (!isFunction(_adapterFactory)) { - _adapterFactory = queryAdapters[_adapterFactory]; - } - - _adapter = _adapterFactory(queryOptions); - - _taskQueue = [].concat(tasks).concat(executorTask); - - const { optimize } = _adapter; - if (optimize) optimize(_taskQueue); - - while (_taskQueue.length) { - /* eslint-disable-next-line prefer-destructuring */ - _currentTask = _taskQueue[0]; - - if (!mergeSortTask(_currentTask)) { - if (_mergedSortArgs) { - _taskQueue.unshift(createTask('multiSort', [_mergedSortArgs])); - _mergedSortArgs = null; - continue; - } - - if (String(_currentTask.name) !== 'enumerate') { - if (!_adapter[_currentTask.name] || _adapter[_currentTask.name].apply(_adapter, _currentTask.args) === false) { - break; - } - } - } - _taskQueue.shift(); - } - - unmergeSortTasks(); - - _adapter.exec(url) - .done((result, extra) => { - if (!_taskQueue.length) { - d.resolve(result, extra); - } else { - let clientChain = arrayQueryImpl(result, { - errorHandler: queryOptions.errorHandler, - }); - each(_taskQueue, function () { - clientChain = clientChain[this.name].apply(clientChain, this.args); - }); - clientChain - // @ts-expect-error - .done(d.resolve) - .fail(d.reject); - } - }) - .fail(rejectWithNotify); - } catch (x) { - rejectWithNotify(x); - } - - return d.promise(); - }; - - const query = {}; - - each( - ['sortBy', 'thenBy', 'filter', 'slice', 'select', 'groupBy'], - function () { - const name = String(this); - query[name] = function () { - return remoteQueryImpl(url, queryOptions, tasks.concat(createTask(name, arguments))); - }; - }, - ); - - each( - ['count', 'min', 'max', 'sum', 'avg', 'aggregate', 'enumerate'], - function () { - const name = String(this); - query[name] = function () { - return exec.call(this, createTask(name, arguments)); - }; - }, - ); - - return query; -}; - -export default remoteQueryImpl; diff --git a/packages/devextreme/js/__internal/data/m_store_helper.ts b/packages/devextreme/js/__internal/data/m_store_helper.ts deleted file mode 100644 index 9c30c9243f73..000000000000 --- a/packages/devextreme/js/__internal/data/m_store_helper.ts +++ /dev/null @@ -1,89 +0,0 @@ -import arrayQuery from '@js/common/data/array_query'; -import { normalizeSortingInfo } from '@js/common/data/utils'; -// @ts-expect-error -import { grep } from '@js/core/utils/common'; -import { extend } from '@js/core/utils/extend'; -import { each } from '@js/core/utils/iterator'; - -function multiLevelGroup(query, groupInfo) { - query = query.groupBy(groupInfo[0].selector); - - if (groupInfo.length > 1) { - query = query.select((g) => extend({}, g, { - items: multiLevelGroup(arrayQuery(g.items), groupInfo.slice(1)).toArray(), - })); - } - - return query; -} - -function arrangeSortingInfo(groupInfo, sortInfo) { - const filteredGroup = []; - each(groupInfo, (_, group) => { - const collision = grep(sortInfo, (sort) => group.selector === sort.selector); - - if (collision.length < 1) { - // @ts-expect-error - filteredGroup.push(group); - } - }); - return filteredGroup.concat(sortInfo); -} - -function queryByOptions(query, options, isCountQuery) { - options = options || {}; - - const { filter } = options; - - if (options?.langParams) { - query.setLangParams?.(options.langParams); - } - - if (filter) { - query = query.filter(filter); - } - - if (isCountQuery) { - return query; - } - - let { sort } = options; - const { select } = options; - let { group } = options; - const { skip } = options; - const { take } = options; - - if (group) { - group = normalizeSortingInfo(group); - group.keepInitialKeyOrder = !!options.group.keepInitialKeyOrder; - } - if (sort || group) { - sort = normalizeSortingInfo(sort || []); - if (group && !group.keepInitialKeyOrder) { - sort = arrangeSortingInfo(group, sort); - } - each(sort, function (index) { - query = query[index ? 'thenBy' : 'sortBy'](this.selector, this.desc, this.compare); - }); - } - - if (select) { - query = query.select(select); - } - - if (group) { - query = multiLevelGroup(query, group); - } - - if (take || skip) { - query = query.slice(skip || 0, take); - } - - return query; -} - -export default { - multiLevelGroup, - arrangeSortingInfo, - queryByOptions, -}; diff --git a/packages/devextreme/js/__internal/data/m_utils.ts b/packages/devextreme/js/__internal/data/m_utils.ts deleted file mode 100644 index ea0a65a494d8..000000000000 --- a/packages/devextreme/js/__internal/data/m_utils.ts +++ /dev/null @@ -1,304 +0,0 @@ -/* eslint-disable spellcheck/spell-checker */ -import domAdapter from '@js/core/dom_adapter'; -import { equalByValue } from '@js/core/utils/common'; -import { Deferred } from '@js/core/utils/deferred'; -import { map } from '@js/core/utils/iterator'; -import readyCallbacks from '@js/core/utils/ready_callbacks'; -import { isFunction } from '@js/core/utils/type'; -import { getWindow } from '@js/core/utils/window'; - -const ready = readyCallbacks.add; - -export const XHR_ERROR_UNLOAD = 'DEVEXTREME_XHR_ERROR_UNLOAD'; - -export const normalizeBinaryCriterion = function (crit) { - return [ - crit[0], - crit.length < 3 ? '=' : String(crit[1]).toLowerCase(), - crit.length < 2 ? true : crit[crit.length - 1], - ]; -}; - -export const normalizeSortingInfo = function (info) { - if (!Array.isArray(info)) { - info = [info]; - } - - return map(info, (i) => { - const result = { - selector: isFunction(i) || typeof i === 'string' ? i : i.getter || i.field || i.selector, - desc: !!(i.desc || String(i.dir).charAt(0).toLowerCase() === 'd'), - }; - if (i.compare) { - // @ts-expect-error - result.compare = i.compare; - } - return result; - }); -}; - -export const errorMessageFromXhr = (function () { - const textStatusMessages = { - timeout: 'Network connection timeout', - error: 'Unspecified network error', - parsererror: 'Unexpected server response', - }; - - /// #DEBUG - const textStatusDetails = { - timeout: 'possible causes: the remote host is not accessible, overloaded or is not included into the domain white-list when being run in the native container', - error: 'if the remote host is located on another domain, make sure it properly supports cross-origin resource sharing (CORS), or use the JSONP approach instead', - parsererror: 'the remote host did not respond with valid JSON data', - }; - /// #ENDDEBUG - - const explainTextStatus = function (textStatus) { - let result = textStatusMessages[textStatus]; - - if (!result) { - return textStatus; - } - - /// #DEBUG - result += ` (${textStatusDetails[textStatus]})`; - /// #ENDDEBUG - - return result; - }; - - // T542570, https://stackoverflow.com/a/18170879 - let unloading; - ready(() => { - const window = getWindow(); - domAdapter.listen(window, 'beforeunload', () => { unloading = true; }); - }); - - return function (xhr, textStatus) { - if (unloading) { - return XHR_ERROR_UNLOAD; - } - if (xhr.status < 400) { - return explainTextStatus(textStatus); - } - return xhr.statusText; - }; -}()); - -export const aggregators = { - count: { - seed: 0, - step(count) { return 1 + count; }, - }, - sum: { - seed: 0, - step(sum, item) { return sum + item; }, - }, - min: { - step(min, item) { return item < min ? item : min; }, - }, - max: { - step(max, item) { return item > max ? item : max; }, - }, - avg: { - seed: [0, 0], - step(pair, value) { - return [pair[0] + value, pair[1] + 1]; - }, - finalize(pair) { - return pair[1] ? pair[0] / pair[1] : NaN; - }, - }, -}; - -export const processRequestResultLock = (function () { - let lockCount = 0; - let lockDeferred; - - const obtain = function () { - if (lockCount === 0) { - // @ts-expect-error - lockDeferred = new Deferred(); - } - lockCount++; - }; - - const release = function () { - lockCount--; - if (lockCount < 1) { - lockDeferred.resolve(); - } - }; - - const promise = function () { - // @ts-expect-error - const deferred = lockCount === 0 ? new Deferred().resolve() : lockDeferred; - return deferred.promise(); - }; - - const reset = function () { - lockCount = 0; - if (lockDeferred) { - lockDeferred.resolve(); - } - }; - - return { - obtain, - release, - promise, - reset, - }; -}()); - -export function isDisjunctiveOperator(condition) { - return /^(or|\|\||\|)$/i.test(condition); -} - -export function isConjunctiveOperator(condition) { - return /^(and|&&|&)$/i.test(condition); -} - -export const keysEqual = function (keyExpr, key1, key2) { - if (Array.isArray(keyExpr)) { - const names = map(key1, (v, k) => k); - let name; - for (let i = 0; i < names.length; i++) { - name = names[i]; - if (!equalByValue(key1[name], key2[name], { strict: false })) { - return false; - } - } - return true; - } - - return equalByValue(key1, key2, { strict: false }); -}; - -const BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; - -// eslint-disable-next-line @typescript-eslint/naming-convention -export const base64_encode = function (input) { - if (!Array.isArray(input)) { - input = stringToByteArray(String(input)); - } - - let result = ''; - - function getBase64Char(index) { - return BASE64_CHARS.charAt(index); - } - - for (let i = 0; i < input.length; i += 3) { - const octet1 = input[i]; - const octet2 = input[i + 1]; - const octet3 = input[i + 2]; - - result += map( - [ - octet1 >> 2, - ((octet1 & 3) << 4) | octet2 >> 4, - isNaN(octet2) ? 64 : ((octet2 & 15) << 2) | octet3 >> 6, - isNaN(octet3) ? 64 : octet3 & 63, - ], - getBase64Char, - ).join(''); - } - - return result; -}; - -function stringToByteArray(str) { - const bytes: number[] = []; - let code: number; - let i; - - for (i = 0; i < str.length; i++) { - code = str.charCodeAt(i); - - if (code < 128) { - bytes.push(code); - } else if (code < 2048) { - bytes.push(192 + (code >> 6), 128 + (code & 63)); - } else if (code < 65536) { - bytes.push(224 + (code >> 12), 128 + ((code >> 6) & 63), 128 + (code & 63)); - } else if (code < 2097152) { - bytes.push(240 + (code >> 18), 128 + ((code >> 12) & 63), 128 + ((code >> 6) & 63), 128 + (code & 63)); - } - } - return bytes; -} - -export const isUnaryOperation = function (crit) { - return crit[0] === '!' && Array.isArray(crit[1]); -}; - -const isGroupOperator = function (value) { - return value === 'and' || value === 'or'; -}; - -export const isUniformEqualsByOr = function (crit) { - if (crit.length > 2 && Array.isArray(crit[0]) && crit[1] === 'or' && typeof crit[0][0] === 'string' && crit[0][1] === '=') { - const [prop] = crit[0]; - return !crit.find((el, i) => (i % 2 !== 0 ? el !== 'or' - : !Array.isArray(el) || el.length !== 3 || el[0] !== prop || el[1] !== '=')); - } - return false; -}; - -export const isGroupCriterion = function (crit) { - const first = crit[0]; - const second = crit[1]; - - if (Array.isArray(first)) { - return true; - } - if (isFunction(first)) { - if (Array.isArray(second) || isFunction(second) || isGroupOperator(second)) { - return true; - } - } - - return false; -}; - -export const trivialPromise = function () { - // @ts-expect-error - const d = new Deferred(); - return d.resolve.apply(d, arguments).promise(); -}; - -export const rejectedPromise = function () { - // @ts-expect-error - const d = new Deferred(); - return d.reject.apply(d, arguments).promise(); -}; - -function throttle(func, timeout) { - let timeoutId; - return function () { - if (!timeoutId) { - timeoutId = setTimeout(() => { - timeoutId = undefined; - func.call(this); - }, isFunction(timeout) ? timeout() : timeout); - } - return timeoutId; - }; -} - -export function throttleChanges(func, timeout) { - let cache = []; - const throttled = throttle(function () { - func.call(this, cache); - cache = []; - }, timeout); - - return function (changes) { - if (Array.isArray(changes)) { - // @ts-expect-error - cache.push(...changes); - } - // @ts-expect-error - return throttled.call(this, cache); - }; -} diff --git a/packages/devextreme/js/__internal/data/odata/context.ts b/packages/devextreme/js/__internal/data/odata/context.ts index 6e070b4c937c..dbd3dadbde42 100644 --- a/packages/devextreme/js/__internal/data/odata/context.ts +++ b/packages/devextreme/js/__internal/data/odata/context.ts @@ -9,7 +9,7 @@ import { each } from '@js/core/utils/iterator'; import { isDefined, isPlainObject } from '@js/core/utils/type'; import type { StoreErrorHandler } from '@ts/data/abstract_store'; -import { errors, handleError } from '../m_errors'; +import { errors, handleError } from '../errors'; import { escapeServiceOperationParams, formatFunctionInvocationUrl } from './utils'; export type ServiceOperationParams = Record<string, unknown>; diff --git a/packages/devextreme/js/__internal/data/odata/m_query_adapter.ts b/packages/devextreme/js/__internal/data/odata/m_query_adapter.ts deleted file mode 100644 index 6205484aa0a9..000000000000 --- a/packages/devextreme/js/__internal/data/odata/m_query_adapter.ts +++ /dev/null @@ -1,363 +0,0 @@ -import queryAdapters from '@js/common/data/query_adapters'; -import config from '@js/core/config'; -import { extend } from '@js/core/utils/extend'; -import { each } from '@js/core/utils/iterator'; -import { isFunction } from '@js/core/utils/type'; - -import { errors } from '../m_errors'; -import { - isConjunctiveOperator, - isUnaryOperation, - normalizeBinaryCriterion, -} from '../m_utils'; -import { - convertPrimitiveValue, - generateExpand, - generateSelect, - sendRequest, - serializePropName, - serializeValue, -} from './utils'; - -const DEFAULT_PROTOCOL_VERSION = 4; -const STRING_FUNCTIONS = ['contains', 'notcontains', 'startswith', 'endswith']; - -const compileCriteria = (() => { - let protocolVersion; - let forceLowerCase; - let fieldTypes; - - const createBinaryOperationFormatter = (op) => (prop, val) => `${prop} ${op} ${val}`; - - const createStringFuncFormatter = (op, reverse) => (prop, val) => { - const bag = [op, '(']; - - if (forceLowerCase) { - prop = prop.indexOf('tolower(') === -1 ? `tolower(${prop})` : prop; - val = val.toLowerCase(); - } - - if (reverse) { - bag.push(val, ',', prop); - } else { - bag.push(prop, ',', val); - } - - bag.push(')'); - return bag.join(''); - }; - - const isStringFunction = function (name) { - return STRING_FUNCTIONS.some((funcName) => funcName === name); - }; - - const formatters = { - '=': createBinaryOperationFormatter('eq'), - '<>': createBinaryOperationFormatter('ne'), - '>': createBinaryOperationFormatter('gt'), - '>=': createBinaryOperationFormatter('ge'), - '<': createBinaryOperationFormatter('lt'), - '<=': createBinaryOperationFormatter('le'), - // @ts-expect-error - startswith: createStringFuncFormatter('startswith'), - // @ts-expect-error - endswith: createStringFuncFormatter('endswith'), - }; - - const formattersV2 = extend({}, formatters, { - /* eslint-disable spellcheck/spell-checker */ - contains: createStringFuncFormatter('substringof', true), - notcontains: createStringFuncFormatter('not substringof', true), - }); - - const formattersV4 = extend({}, formatters, { - // @ts-expect-error - contains: createStringFuncFormatter('contains'), - // @ts-expect-error - notcontains: createStringFuncFormatter('not contains'), - }); - - const compileBinary = (criteria) => { - criteria = normalizeBinaryCriterion(criteria); - - const op = criteria[1]; - const fieldName = criteria[0]; - const fieldType = fieldTypes && fieldTypes[fieldName]; - - if (fieldType && isStringFunction(op) && fieldType !== 'String') { - // @ts-expect-error - throw new errors.Error('E4024', op, fieldName, fieldType); - } - - const formatters = protocolVersion === 4 - ? formattersV4 - : formattersV2; - const formatter = formatters[op.toLowerCase()]; - - if (!formatter) { - throw errors.Error('E4003', op); - } - - let value = criteria[2]; - - if (fieldTypes?.[fieldName]) { - value = convertPrimitiveValue(fieldTypes[fieldName], value); - } - - return formatter( - serializePropName(fieldName), - serializeValue(value, protocolVersion, fieldTypes?.[fieldName]), - ); - }; - - const compileUnary = (criteria) => { - const op = criteria[0]; - const crit = compileCore(criteria[1]); - - if (op === '!') { - return `not (${crit})`; - } - - throw errors.Error('E4003', op); - }; - - const compileGroup = (criteria) => { - const bag = []; - let groupOperator; - let nextGroupOperator; - - each(criteria, function (index, criterion) { - if (Array.isArray(criterion)) { - if (bag.length > 1 && groupOperator !== nextGroupOperator) { - // @ts-expect-error - throw new errors.Error('E4019'); - } - // @ts-expect-error - bag.push(`(${compileCore(criterion)})`); - - groupOperator = nextGroupOperator; - nextGroupOperator = 'and'; - } else { - nextGroupOperator = isConjunctiveOperator(this) ? 'and' : 'or'; - } - }); - - return bag.join(` ${groupOperator} `); - }; - - const compileCore = (criteria) => { - if (Array.isArray(criteria[0])) { - return compileGroup(criteria); - } - - if (isUnaryOperation(criteria)) { - return compileUnary(criteria); - } - - return compileBinary(criteria); - }; - - return (criteria, version, types, filterToLower) => { - fieldTypes = types; - forceLowerCase = filterToLower ?? config().oDataFilterToLower; - protocolVersion = version; - - return compileCore(criteria); - }; -})(); - -const createODataQueryAdapter = (queryOptions) => { - /* eslint-disable @typescript-eslint/naming-convention */ - let _sorting = []; - const _criteria = []; - const _expand = queryOptions.expand; - let _select; - let _skip; - let _take; - let _countQuery; - - const _oDataVersion = queryOptions.version || DEFAULT_PROTOCOL_VERSION; - - const hasSlice = () => _skip || _take !== undefined; - - const hasFunction = (criterion) => { - for (let i = 0; i < criterion.length; i++) { - if (isFunction(criterion[i])) { - return true; - } - - if (Array.isArray(criterion[i]) && hasFunction(criterion[i])) { - return true; - } - } - return false; - }; - - const requestData = () => { - const result = {}; - - if (!_countQuery) { - if (_sorting.length) { - // @ts-expect-error - result.$orderby = _sorting.join(','); - } - if (_skip) { - // @ts-expect-error - result.$skip = _skip; - } - if (_take !== undefined) { - // @ts-expect-error - result.$top = _take; - } - // @ts-expect-error - result.$select = generateSelect(_oDataVersion, _select) || undefined; - // @ts-expect-error - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - result.$expand = generateExpand(_oDataVersion, _expand, _select) || undefined; - } - - if (_criteria.length) { - const criteria = _criteria.length < 2 ? _criteria[0] : _criteria; - const fieldTypes = queryOptions?.fieldTypes; - const filterToLower = queryOptions?.filterToLower; - // @ts-expect-error - result.$filter = compileCriteria(criteria, _oDataVersion, fieldTypes, filterToLower); - } - - if (_countQuery) { - // @ts-expect-error - result.$top = 0; - } - - if (queryOptions.requireTotalCount || _countQuery) { - // todo: tests!!! - if (_oDataVersion !== 4) { - // @ts-expect-error - result.$inlinecount = 'allpages'; - } else { - // @ts-expect-error - result.$count = 'true'; - } - } - - return result; - }; - - const tryLiftSelect = (tasks) => { - let selectIndex = -1; - for (let i = 0; i < tasks.length; i++) { - if (tasks[i].name === 'select') { - selectIndex = i; - break; - } - } - - if (selectIndex < 0 || !isFunction(tasks[selectIndex].args[0])) return; - - const nextTask = tasks[1 + selectIndex]; - if (!nextTask || nextTask.name !== 'slice') return; - - tasks[1 + selectIndex] = tasks[selectIndex]; - tasks[selectIndex] = nextTask; - }; - - return { - - optimize: tryLiftSelect, - - exec(url) { - return sendRequest( - _oDataVersion, - { - url, - params: extend(requestData(), queryOptions?.params), - }, - { - beforeSend: queryOptions.beforeSend, - jsonp: queryOptions.jsonp, - withCredentials: queryOptions.withCredentials, - countOnly: _countQuery, - processDatesAsUtc: queryOptions.processDatesAsUtc, - fieldTypes: queryOptions.fieldTypes, - isPaged: isFinite(_take), - }, - ); - }, - /* eslint-disable @typescript-eslint/no-invalid-void-type */ - multiSort(args): boolean | void { - let rules; - - if (hasSlice()) { - return false; - } - - for (let i = 0; i < args.length; i++) { - const getter = args[i][0]; - const desc = !!args[i][1]; - let rule; - - if (typeof getter !== 'string') { - return false; - } - - rule = serializePropName(getter); - if (desc) { - rule += ' desc'; - } - - rules = rules || []; - rules.push(rule); - } - - _sorting = rules; - }, - - slice(skipCount, takeCount): boolean | void { - if (hasSlice()) { - return false; - } - - _skip = skipCount; - _take = takeCount; - }, - - filter(criterion): boolean | void { - if (hasSlice()) { - return false; - } - - if (!Array.isArray(criterion)) { - criterion = [].slice.call(arguments); - } - - if (hasFunction(criterion)) { - return false; - } - - if (_criteria.length) { - // @ts-expect-error - _criteria.push('and'); - } - // @ts-expect-error - _criteria.push(criterion); - }, - - select(expr): boolean | void { - if (_select || isFunction(expr)) { - return false; - } - - if (!Array.isArray(expr)) { - expr = [].slice.call(arguments); - } - - _select = expr; - }, - // eslint-disable-next-line no-return-assign - count: () => _countQuery = true, - }; -}; - -queryAdapters.odata = createODataQueryAdapter; - -export const odata = createODataQueryAdapter; diff --git a/packages/devextreme/js/__internal/data/odata/m_request_dispatcher.ts b/packages/devextreme/js/__internal/data/odata/m_request_dispatcher.ts deleted file mode 100644 index e37f10217123..000000000000 --- a/packages/devextreme/js/__internal/data/odata/m_request_dispatcher.ts +++ /dev/null @@ -1,72 +0,0 @@ -import '@js/common/data/odata/query_adapter'; - -import { sendRequest } from '@js/common/data/odata/utils'; - -const DEFAULT_PROTOCOL_VERSION = 4; - -export default class RequestDispatcher { - constructor(options) { - options = options || {}; - // @ts-expect-error - this._url = String(options.url).replace(/\/+$/, ''); - // @ts-expect-error - this._beforeSend = options.beforeSend; - // @ts-expect-error - this._jsonp = options.jsonp; - // @ts-expect-error - this._version = options.version || DEFAULT_PROTOCOL_VERSION; - // @ts-expect-error - this._withCredentials = options.withCredentials; - // @ts-expect-error - this._processDatesAsUtc = options.processDatesAsUtc ?? options.deserializeDates ?? false; - // @ts-expect-error - this._filterToLower = options.filterToLower; - } - - sendRequest(url, method, params, payload) { - return sendRequest( - this.version, - { - url, - method, - params: params || {}, - payload, - }, - { - // @ts-expect-error - beforeSend: this._beforeSend, - // @ts-expect-error - jsonp: this._jsonp, - // @ts-expect-error - withCredentials: this._withCredentials, - // @ts-expect-error - processDatesAsUtc: this._processDatesAsUtc, - }, - ); - } - - get version() { - // @ts-expect-error - return this._version; - } - - get beforeSend() { - // @ts-expect-error - return this._beforeSend; - } - - get url() { - // @ts-expect-error - return this._url; - } - - get jsonp() { - // @ts-expect-error - return this._jsonp; - } - - get filterToLower() { - // @ts-expect-error - return this._filterToLower; - } -} diff --git a/packages/devextreme/js/__internal/data/odata/query_adapter.ts b/packages/devextreme/js/__internal/data/odata/query_adapter.ts new file mode 100644 index 000000000000..932de8a1bef3 --- /dev/null +++ b/packages/devextreme/js/__internal/data/odata/query_adapter.ts @@ -0,0 +1,367 @@ +import queryAdapters from '@js/common/data/query_adapters'; +import config from '@js/core/config'; +import type { DeferredObj } from '@js/core/utils/deferred'; +import { extend } from '@js/core/utils/extend'; +import { isFunction, isString } from '@js/core/utils/type'; +import type { QueryAdapter, RemoteQueryOptions, RemoteTask } from '@ts/data/remote_query'; + +import { errors } from '../errors'; +import { + isConjunctiveOperator, + isUnaryOperation, + normalizeBinaryCriterion, +} from '../utils'; +import { + convertPrimitiveValue, + EdmLiteral, + generateExpand, + generateSelect, + sendRequest, + serializePropName, + serializeValue, +} from './utils'; + +const DEFAULT_PROTOCOL_VERSION = 4; +const STRING_FUNCTIONS = ['contains', 'notcontains', 'startswith', 'endswith']; + +type Formatter = (prop: string, val: string) => string; + +type FieldTypes = Record<string, string>; + +interface ODataRequestParams { + [param: string]: unknown; + $orderby?: string; + $skip?: number; + $top?: number; + $select?: string; + $expand?: string; + $filter?: string; + // eslint-disable-next-line spellcheck/spell-checker + $inlinecount?: string; + $count?: string; +} + +const compileCriteria = (() => { + // eslint-disable-next-line @typescript-eslint/init-declarations + let protocolVersion: number; + // eslint-disable-next-line @typescript-eslint/init-declarations + let forceLowerCase: boolean | undefined; + // eslint-disable-next-line @typescript-eslint/init-declarations + let fieldTypes: FieldTypes | undefined; + + const createBinaryOperationFormatter = (op: string): Formatter => (prop, val) => `${prop} ${op} ${val}`; + + const createStringFuncFormatter = (op: string, reverse?: boolean): Formatter => (prop, val) => { + const bag = [op, '(']; + const propName = forceLowerCase && !prop.includes('tolower(') ? `tolower(${prop})` : prop; + const value = forceLowerCase ? val.toLowerCase() : val; + + if (reverse) { + bag.push(value, ',', propName); + } else { + bag.push(propName, ',', value); + } + + bag.push(')'); + return bag.join(''); + }; + + const isStringFunction = function (name: string): boolean { + return STRING_FUNCTIONS.some((funcName) => funcName === name); + }; + + const formatters: Record<string, Formatter> = { + '=': createBinaryOperationFormatter('eq'), + '<>': createBinaryOperationFormatter('ne'), + '>': createBinaryOperationFormatter('gt'), + '>=': createBinaryOperationFormatter('ge'), + '<': createBinaryOperationFormatter('lt'), + '<=': createBinaryOperationFormatter('le'), + startswith: createStringFuncFormatter('startswith'), + endswith: createStringFuncFormatter('endswith'), + }; + + /* eslint-disable spellcheck/spell-checker */ + const formattersV2: Record<string, Formatter> = { + ...formatters, + contains: createStringFuncFormatter('substringof', true), + notcontains: createStringFuncFormatter('not substringof', true), + }; + /* eslint-enable spellcheck/spell-checker */ + + /* eslint-disable spellcheck/spell-checker */ + const formattersV4: Record<string, Formatter> = { + ...formatters, + contains: createStringFuncFormatter('contains'), + notcontains: createStringFuncFormatter('not contains'), + }; + /* eslint-enable spellcheck/spell-checker */ + + const compileBinary = (criteria: unknown[]): string => { + const crit = normalizeBinaryCriterion(criteria); + + const [rawFieldName, op] = crit; + const fieldName = isString(rawFieldName) || rawFieldName instanceof EdmLiteral + ? rawFieldName + : String(rawFieldName); + const fieldType = fieldTypes?.[String(fieldName)]; + + if (fieldType && isStringFunction(op) && fieldType !== 'String') { + throw errors.Error('E4024', op, fieldName, fieldType); + } + + const criterionFormatters = protocolVersion === 4 + ? formattersV4 + : formattersV2; + const formatter = criterionFormatters[op.toLowerCase()]; + + if (!formatter) { + throw errors.Error('E4003', op); + } + + const value = fieldType ? convertPrimitiveValue(fieldType, crit[2]) : crit[2]; + + return formatter( + serializePropName(fieldName), + serializeValue(value, protocolVersion, fieldType), + ); + }; + + const compileUnary = (criteria: unknown[]): string => { + const op = criteria[0]; + // eslint-disable-next-line @typescript-eslint/no-use-before-define + const crit = compileCore(criteria[1]); + + if (op === '!') { + return `not (${crit})`; + } + + throw errors.Error('E4003', op); + }; + + const compileGroup = (criteria: unknown[]): string => { + const bag: string[] = []; + // eslint-disable-next-line @typescript-eslint/init-declarations + let groupOperator: string | undefined; + // eslint-disable-next-line @typescript-eslint/init-declarations + let nextGroupOperator: string | undefined; + + criteria.forEach((criterion) => { + if (Array.isArray(criterion)) { + if (bag.length > 1 && groupOperator !== nextGroupOperator) { + throw errors.Error('E4019'); + } + // eslint-disable-next-line @typescript-eslint/no-use-before-define + bag.push(`(${compileCore(criterion)})`); + + groupOperator = nextGroupOperator; + nextGroupOperator = 'and'; + } else { + nextGroupOperator = isConjunctiveOperator(criterion) ? 'and' : 'or'; + } + }); + + return bag.join(` ${groupOperator} `); + }; + + const compileCore = (criteria: unknown): string => { + const criterion: unknown[] = Array.isArray(criteria) ? criteria : [criteria]; + + if (Array.isArray(criterion[0])) { + return compileGroup(criterion); + } + + if (isUnaryOperation(criterion)) { + return compileUnary(criterion); + } + + return compileBinary(criterion); + }; + + return ( + criteria: unknown, + version: number, + types: FieldTypes | undefined, + filterToLower: boolean | undefined, + ): string => { + fieldTypes = types; + forceLowerCase = filterToLower ?? config().oDataFilterToLower; + protocolVersion = version; + + return compileCore(criteria); + }; +})(); + +const createODataQueryAdapter = (queryOptions: RemoteQueryOptions): QueryAdapter => { + let sorting: string[] = []; + const criteria: unknown[] = []; + const { expand } = queryOptions; + // eslint-disable-next-line @typescript-eslint/init-declarations + let select: string[] | undefined; + // eslint-disable-next-line @typescript-eslint/init-declarations + let skip: number | undefined; + // eslint-disable-next-line @typescript-eslint/init-declarations + let take: number | undefined; + let countQuery = false; + + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const oDataVersion = queryOptions.version || DEFAULT_PROTOCOL_VERSION; + + const hasSlice = (): boolean => !!skip || take !== undefined; + + const hasFunction = (criterion: unknown[]): boolean => criterion.some( + (item) => isFunction(item) || (Array.isArray(item) && hasFunction(item)), + ); + + const requestData = (): ODataRequestParams => { + const result: ODataRequestParams = {}; + + if (!countQuery) { + if (sorting.length) { + result.$orderby = sorting.join(','); + } + if (skip) { + result.$skip = skip; + } + if (take !== undefined) { + result.$top = take; + } + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + result.$select = generateSelect(oDataVersion, select) || undefined; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + result.$expand = generateExpand(oDataVersion, expand, select) || undefined; + } + + if (criteria.length) { + const filterCriteria: unknown = criteria.length < 2 ? criteria[0] : criteria; + const { fieldTypes, filterToLower } = queryOptions; + result.$filter = compileCriteria(filterCriteria, oDataVersion, fieldTypes, filterToLower); + } + + if (countQuery) { + result.$top = 0; + } + + if (queryOptions.requireTotalCount || countQuery) { + // todo: tests!!! + if (oDataVersion !== 4) { + // eslint-disable-next-line spellcheck/spell-checker + result.$inlinecount = 'allpages'; + } else { + result.$count = 'true'; + } + } + + return result; + }; + + const tryLiftSelect = (tasks: RemoteTask[]): void => { + const selectIndex = tasks.findIndex((task) => task.name === 'select'); + + if (selectIndex < 0 || !isFunction(tasks[selectIndex].args[0])) return; + + const nextTask = tasks[1 + selectIndex]; + if (nextTask?.name !== 'slice') return; + + tasks[1 + selectIndex] = tasks[selectIndex]; + tasks[selectIndex] = nextTask; + }; + + return { + + optimize: tryLiftSelect, + + exec(url: string): DeferredObj<unknown> { + return sendRequest( + oDataVersion, + { + url, + params: extend(requestData(), queryOptions.params), + }, + { + beforeSend: queryOptions.beforeSend, + jsonp: queryOptions.jsonp, + withCredentials: queryOptions.withCredentials, + countOnly: countQuery, + processDatesAsUtc: queryOptions.processDatesAsUtc, + fieldTypes: queryOptions.fieldTypes, + isPaged: isFinite(Number(take)), + }, + ); + }, + + multiSort(args: unknown[]): boolean | undefined { + const rules: string[] = []; + + if (hasSlice()) { + return false; + } + + for (const arg of args) { + const [getter, desc] = Array.isArray(arg) ? arg : []; + + if (typeof getter !== 'string') { + return false; + } + + rules.push(desc ? `${serializePropName(getter)} desc` : serializePropName(getter)); + } + + sorting = rules; + + return undefined; + }, + + slice(skipCount: number, takeCount: number): boolean | undefined { + if (hasSlice()) { + return false; + } + + skip = skipCount; + take = takeCount; + + return undefined; + }, + + filter(...args: unknown[]): boolean | undefined { + if (hasSlice()) { + return false; + } + + const [first] = args; + const criterion: unknown[] = Array.isArray(first) ? first : args.slice(); + + if (hasFunction(criterion)) { + return false; + } + + if (criteria.length) { + criteria.push('and'); + } + criteria.push(criterion); + + return undefined; + }, + + select(...args: unknown[]): boolean | undefined { + const [expr] = args; + + if (select || isFunction(expr)) { + return false; + } + + select = (Array.isArray(expr) ? expr : args.slice()).map(String); + + return undefined; + }, + + count(): boolean { + countQuery = true; + return countQuery; + }, + }; +}; + +queryAdapters.odata = createODataQueryAdapter; + +export const odata = createODataQueryAdapter; diff --git a/packages/devextreme/js/__internal/data/odata/request_dispatcher.ts b/packages/devextreme/js/__internal/data/odata/request_dispatcher.ts new file mode 100644 index 000000000000..36acb68ebdde --- /dev/null +++ b/packages/devextreme/js/__internal/data/odata/request_dispatcher.ts @@ -0,0 +1,91 @@ +import '@js/common/data/odata/query_adapter'; + +import { sendRequest } from '@js/common/data/odata/utils'; +import type { DeferredObj } from '@js/core/utils/deferred'; + +const DEFAULT_PROTOCOL_VERSION = 4; + +export interface RequestDispatcherOptions { + url?: string; + beforeSend?: Function; + jsonp?: boolean; + version?: number; + withCredentials?: boolean; + processDatesAsUtc?: boolean; + deserializeDates?: boolean; + filterToLower?: boolean; +} + +export default class RequestDispatcher { + _url: string; + + _beforeSend?: Function; + + _jsonp?: boolean; + + _version: number; + + _withCredentials?: boolean; + + _processDatesAsUtc: boolean; + + _filterToLower?: boolean; + + constructor(options?: RequestDispatcherOptions) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const dispatcherOptions: RequestDispatcherOptions = options || {}; + + this._url = String(dispatcherOptions.url).replace(/\/+$/, ''); + this._beforeSend = dispatcherOptions.beforeSend; + this._jsonp = dispatcherOptions.jsonp; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + this._version = dispatcherOptions.version || DEFAULT_PROTOCOL_VERSION; + this._withCredentials = dispatcherOptions.withCredentials; + this._processDatesAsUtc = dispatcherOptions.processDatesAsUtc + ?? dispatcherOptions.deserializeDates ?? false; + this._filterToLower = dispatcherOptions.filterToLower; + } + + sendRequest( + url: string, + method?: string, + params?: Record<string, unknown> | null, + payload?: unknown, + ): DeferredObj<unknown> { + return sendRequest( + this.version, + { + url, + method, + params: params ?? {}, + payload, + }, + { + beforeSend: this._beforeSend, + jsonp: this._jsonp, + withCredentials: this._withCredentials, + processDatesAsUtc: this._processDatesAsUtc, + }, + ); + } + + get version(): number { + return this._version; + } + + get beforeSend(): Function | undefined { + return this._beforeSend; + } + + get url(): string { + return this._url; + } + + get jsonp(): boolean | undefined { + return this._jsonp; + } + + get filterToLower(): boolean | undefined { + return this._filterToLower; + } +} diff --git a/packages/devextreme/js/__internal/data/odata/store.ts b/packages/devextreme/js/__internal/data/odata/store.ts index 4464dc43d888..d02f3cf322fe 100644 --- a/packages/devextreme/js/__internal/data/odata/store.ts +++ b/packages/devextreme/js/__internal/data/odata/store.ts @@ -35,13 +35,6 @@ export interface ODataLoadOptions extends StoreLoadOptions { customQueryParams?: Record<string, unknown>; } -// `RequestDispatcher` does not declare its own fields yet, so the members the store reads -// directly are described here until that class is typed. -type StoreRequestDispatcher = RequestDispatcher & { - _withCredentials?: unknown; - _processDatesAsUtc?: unknown; -}; - const expandKeyType = (key: StoreKey, keyType: string): FieldTypes => ({ [String(key)]: keyType }); const getProperty = (source: unknown, name: string): unknown => ( @@ -72,7 +65,7 @@ const mergeFieldTypesWithKeyType = ( }; class ODataStore extends Store { - _requestDispatcher: StoreRequestDispatcher; + _requestDispatcher: RequestDispatcher; _fieldTypes: FieldTypes; diff --git a/packages/devextreme/js/__internal/data/query.ts b/packages/devextreme/js/__internal/data/query.ts new file mode 100644 index 000000000000..f25f87f4734f --- /dev/null +++ b/packages/devextreme/js/__internal/data/query.ts @@ -0,0 +1,16 @@ +import { queryImpl } from '@js/common/data/query_implementation'; +import type { ArrayQuery, QueryOptions } from '@ts/data/array_query'; +import type { RemoteQuery, RemoteQueryOptions } from '@ts/data/remote_query'; + +function query(array: unknown[], queryOptions?: QueryOptions): ArrayQuery; +function query(url: string, queryOptions?: RemoteQueryOptions): RemoteQuery; +function query( + source: unknown[] | string, + queryOptions?: QueryOptions & RemoteQueryOptions, +): ArrayQuery | RemoteQuery { + return Array.isArray(source) + ? queryImpl.array(source, queryOptions) + : queryImpl.remote(source, queryOptions); +} + +export default query; diff --git a/packages/devextreme/js/__internal/data/query_implementation.ts b/packages/devextreme/js/__internal/data/query_implementation.ts index 2654903ad637..d7185d49d29e 100644 --- a/packages/devextreme/js/__internal/data/query_implementation.ts +++ b/packages/devextreme/js/__internal/data/query_implementation.ts @@ -1,5 +1,5 @@ import arrayQueryImpl from './array_query'; -import remoteQueryImpl from './m_remote_query'; +import remoteQueryImpl from './remote_query'; export const queryImpl = { array: arrayQueryImpl, diff --git a/packages/devextreme/js/__internal/data/remote_query.ts b/packages/devextreme/js/__internal/data/remote_query.ts new file mode 100644 index 000000000000..d7ed4b829649 --- /dev/null +++ b/packages/devextreme/js/__internal/data/remote_query.ts @@ -0,0 +1,217 @@ +import arrayQueryImpl from '@js/common/data/array_query'; +import { errors, handleError } from '@js/common/data/errors'; +import queryAdapters from '@js/common/data/query_adapters'; +import type { DeferredObj } from '@js/core/utils/deferred'; +import { Deferred } from '@js/core/utils/deferred'; +import { isFunction } from '@js/core/utils/type'; +import type { ArrayQuery, LangParams } from '@ts/data/array_query'; + +export interface RemoteTask { + name: string; + args: unknown[]; +} + +export interface QueryAdapter { + [taskName: string]: unknown; + optimize?: (tasks: RemoteTask[]) => void; + exec: (url: string) => DeferredObj<unknown>; +} + +export interface RemoteQueryOptions { + adapter?: string | ((options: RemoteQueryOptions) => QueryAdapter); + errorHandler?: (error: unknown) => void; + langParams?: LangParams; + version?: number; + expand?: string | string[] | Function; + fieldTypes?: Record<string, string>; + filterToLower?: boolean; + requireTotalCount?: boolean; + params?: Record<string, unknown>; + beforeSend?: Function; + jsonp?: boolean; + withCredentials?: boolean; + processDatesAsUtc?: boolean; + deserializeDates?: boolean; +} + +export interface RemoteQuery { + sortBy: (...args: unknown[]) => RemoteQuery; + thenBy: (...args: unknown[]) => RemoteQuery; + filter: (...args: unknown[]) => RemoteQuery; + slice: (...args: unknown[]) => RemoteQuery; + select: (...args: unknown[]) => RemoteQuery; + groupBy: (...args: unknown[]) => RemoteQuery; + count: (...args: unknown[]) => DeferredObj<unknown>; + min: (...args: unknown[]) => DeferredObj<unknown>; + max: (...args: unknown[]) => DeferredObj<unknown>; + sum: (...args: unknown[]) => DeferredObj<unknown>; + avg: (...args: unknown[]) => DeferredObj<unknown>; + aggregate: (...args: unknown[]) => DeferredObj<unknown>; + enumerate: (...args: unknown[]) => DeferredObj<unknown>; +} + +const createTask = function (name: string, args: unknown[]): RemoteTask { + return { name, args }; +}; + +const remoteQueryImpl = function ( + url: string, + options?: RemoteQueryOptions, + previousTasks?: RemoteTask[], +): RemoteQuery { + const tasks: RemoteTask[] = previousTasks ?? []; + const queryOptions: RemoteQueryOptions = options ?? {}; + + const exec = function (executorTask: RemoteTask): DeferredObj<unknown> { + const d = Deferred<unknown>(); + // eslint-disable-next-line @typescript-eslint/init-declarations + let mergedSortArgs: unknown[][] | undefined; + + const rejectWithNotify = function (error: unknown): void { + const handler = queryOptions.errorHandler; + if (handler) { + handler(error); + } + + handleError(error); + d.reject(error); + }; + + function mergeSortTask(task: RemoteTask): boolean { + switch (task.name) { + case 'sortBy': + mergedSortArgs = [task.args]; + return true; + + case 'thenBy': + if (!mergedSortArgs) { + throw errors.Error('E4004'); + } + + mergedSortArgs.push(task.args); + return true; + + default: + return false; + } + } + + function unmergeSortTasks(queue: RemoteTask[]): RemoteTask[] { + const head = queue[0]; + const unmergedTasks: RemoteTask[] = []; + + if (head?.name === 'multiSort') { + queue.shift(); + const [sortArgsList] = head.args; + if (Array.isArray(sortArgsList)) { + sortArgsList.forEach((sortArgs: unknown) => { + unmergedTasks.push(createTask( + unmergedTasks.length ? 'thenBy' : 'sortBy', + Array.isArray(sortArgs) ? sortArgs : [sortArgs], + )); + }); + } + } + + return unmergedTasks.concat(queue); + } + + function passTaskToAdapter(adapter: QueryAdapter, task: RemoteTask): boolean { + if (String(task.name) === 'enumerate') { + return true; + } + + const taskHandler = adapter[task.name]; + + return isFunction(taskHandler) && taskHandler.apply(adapter, task.args) !== false; + } + + function collectAdapterTasks(adapter: QueryAdapter, queue: RemoteTask[]): void { + while (queue.length) { + const currentTask = queue[0]; + + if (!mergeSortTask(currentTask)) { + if (mergedSortArgs) { + queue.unshift(createTask('multiSort', [mergedSortArgs])); + mergedSortArgs = undefined; + // eslint-disable-next-line no-continue + continue; + } + + if (!passTaskToAdapter(adapter, currentTask)) { + break; + } + } + queue.shift(); + } + } + + try { + const adapterFactory = isFunction(queryOptions.adapter) + ? queryOptions.adapter + : queryAdapters[String(queryOptions.adapter)]; + + const adapter: QueryAdapter = adapterFactory(queryOptions); + + let taskQueue: RemoteTask[] = [...tasks, executorTask]; + + const { optimize } = adapter; + if (optimize) optimize(taskQueue); + + collectAdapterTasks(adapter, taskQueue); + + taskQueue = unmergeSortTasks(taskQueue); + + adapter.exec(url) + .done((result: unknown, extra: unknown) => { + if (!taskQueue.length) { + d.resolve(result, extra); + } else { + // @ts-expect-error the adapter resolves with whatever the service returned + let clientChain: ArrayQuery = arrayQueryImpl(result, { + errorHandler: queryOptions.errorHandler, + }); + taskQueue.forEach((task) => { + const method = clientChain[task.name]; + if (isFunction(method)) { + clientChain = method.apply(clientChain, task.args); + } + }); + // @ts-expect-error the queue ends with enumerate, so the chain ends with a Deferred + clientChain.done(d.resolve).fail(d.reject); + } + }) + .fail(rejectWithNotify); + } catch (x) { + rejectWithNotify(x); + } + + // @ts-expect-error DeferredObj typings: promise() is declared as a plain Promise + return d.promise(); + }; + + const chain = (name: string, args: unknown[]): RemoteQuery => remoteQueryImpl( + url, + queryOptions, + tasks.concat(createTask(name, args)), + ); + + return { + sortBy: (...args: unknown[]): RemoteQuery => chain('sortBy', args), + thenBy: (...args: unknown[]): RemoteQuery => chain('thenBy', args), + filter: (...args: unknown[]): RemoteQuery => chain('filter', args), + slice: (...args: unknown[]): RemoteQuery => chain('slice', args), + select: (...args: unknown[]): RemoteQuery => chain('select', args), + groupBy: (...args: unknown[]): RemoteQuery => chain('groupBy', args), + + count: (...args: unknown[]): DeferredObj<unknown> => exec(createTask('count', args)), + min: (...args: unknown[]): DeferredObj<unknown> => exec(createTask('min', args)), + max: (...args: unknown[]): DeferredObj<unknown> => exec(createTask('max', args)), + sum: (...args: unknown[]): DeferredObj<unknown> => exec(createTask('sum', args)), + avg: (...args: unknown[]): DeferredObj<unknown> => exec(createTask('avg', args)), + aggregate: (...args: unknown[]): DeferredObj<unknown> => exec(createTask('aggregate', args)), + enumerate: (...args: unknown[]): DeferredObj<unknown> => exec(createTask('enumerate', args)), + }; +}; + +export default remoteQueryImpl; diff --git a/packages/devextreme/js/__internal/data/store_helper.ts b/packages/devextreme/js/__internal/data/store_helper.ts new file mode 100644 index 000000000000..798d00fd5bee --- /dev/null +++ b/packages/devextreme/js/__internal/data/store_helper.ts @@ -0,0 +1,138 @@ +import arrayQuery from '@js/common/data/array_query'; +import type { SortingInfo } from '@js/common/data/utils'; +import { normalizeSortingInfo } from '@js/common/data/utils'; +import { extend } from '@js/core/utils/extend'; +import { isObject } from '@js/core/utils/type'; +import type { LangParams } from '@ts/data/array_query'; + +export interface DataQuery { + /* eslint-disable @typescript-eslint/method-signature-style */ + setLangParams?(langParams: LangParams): void; + filter(criteria: unknown): this; + sortBy(getter: unknown, desc?: unknown, compare?: unknown): this; + thenBy(getter: unknown, desc?: unknown, compare?: unknown): this; + select(getter: unknown): this; + slice(skip: number, take?: number): this; + groupBy(getter: unknown): this; + toArray(): unknown[]; + /* eslint-enable @typescript-eslint/method-signature-style */ +} + +interface GroupingInfo extends Array<SortingInfo> { + keepInitialKeyOrder?: boolean; +} + +export interface QueryByOptions { + filter?: unknown; + sort?: unknown; + select?: unknown; + group?: unknown; + skip?: number; + take?: number; + langParams?: LangParams; +} + +interface GroupResult { + items?: unknown[]; +} + +function multiLevelGroup<TQuery extends DataQuery>( + query: TQuery, + groupInfo: SortingInfo[], +): TQuery { + let result = query.groupBy(groupInfo[0].selector); + + if (groupInfo.length > 1) { + result = result.select((group: unknown) => { + const { items }: GroupResult = isObject(group) ? group : {}; + + const merged: unknown = extend({}, group, { + items: multiLevelGroup(arrayQuery(items ?? []), groupInfo.slice(1)).toArray(), + }); + + return merged; + }); + } + + return result; +} + +function arrangeSortingInfo(groupInfo: SortingInfo[], sortInfo: SortingInfo[]): SortingInfo[] { + const filteredGroup = groupInfo.filter( + (group) => !sortInfo.some((sort) => group.selector === sort.selector), + ); + + return filteredGroup.concat(sortInfo); +} + +function queryByOptions<TQuery extends DataQuery>( + query: TQuery, + options?: QueryByOptions, + isCountQuery?: boolean, +): TQuery { + const queryOptions: QueryByOptions = options ?? {}; + let result = query; + + const { filter } = queryOptions; + + if (queryOptions.langParams) { + result.setLangParams?.(queryOptions.langParams); + } + + if (filter) { + result = result.filter(filter); + } + + if (isCountQuery) { + return result; + } + + const { + sort, select, skip, take, + } = queryOptions; + + // eslint-disable-next-line @typescript-eslint/init-declarations + let group: GroupingInfo | undefined; + if (queryOptions.group) { + const groupOption: { keepInitialKeyOrder?: unknown } = isObject(queryOptions.group) + ? queryOptions.group + : {}; + + group = normalizeSortingInfo(queryOptions.group); + group.keepInitialKeyOrder = !!groupOption.keepInitialKeyOrder; + } + + if (sort || group) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const sortInfo = normalizeSortingInfo(sort || []); + const sortRules = group && !group.keepInitialKeyOrder + ? arrangeSortingInfo(group, sortInfo) + : sortInfo; + + sortRules.forEach((rule, index) => { + result = index + ? result.thenBy(rule.selector, rule.desc, rule.compare) + : result.sortBy(rule.selector, rule.desc, rule.compare); + }); + } + + if (select) { + result = result.select(select); + } + + if (group) { + result = multiLevelGroup(result, group); + } + + if (take || skip) { + result = result.slice(skip ?? 0, take); + } + + return result; +} + +export default { + multiLevelGroup, + arrangeSortingInfo, + queryByOptions, +}; diff --git a/packages/devextreme/js/__internal/data/utils.ts b/packages/devextreme/js/__internal/data/utils.ts new file mode 100644 index 000000000000..373235940504 --- /dev/null +++ b/packages/devextreme/js/__internal/data/utils.ts @@ -0,0 +1,366 @@ +/* eslint-disable spellcheck/spell-checker */ +import domAdapter from '@js/core/dom_adapter'; +import { equalByValue } from '@js/core/utils/common'; +import type { DeferredObj } from '@js/core/utils/deferred'; +import { Deferred } from '@js/core/utils/deferred'; +import { map } from '@js/core/utils/iterator'; +import readyCallbacks from '@js/core/utils/ready_callbacks'; +import { isFunction, isObject, isString } from '@js/core/utils/type'; +import { getWindow } from '@js/core/utils/window'; + +const ready = readyCallbacks.add; + +export const XHR_ERROR_UNLOAD = 'DEVEXTREME_XHR_ERROR_UNLOAD'; + +export type BinaryCriterion = [unknown, string, unknown]; + +export const normalizeBinaryCriterion = function (crit: unknown[]): BinaryCriterion { + return [ + crit[0], + crit.length < 3 ? '=' : String(crit[1]).toLowerCase(), + crit.length < 2 ? true : crit[crit.length - 1], + ]; +}; + +export type SortingSelector = string | Function; + +export interface SortingInfo { + selector: SortingSelector | undefined; + desc: boolean; + compare?: unknown; + isExpanded?: boolean; + groupInterval?: unknown; +} + +interface SortingDescriptor { + getter?: SortingSelector; + field?: SortingSelector; + selector?: SortingSelector; + desc?: unknown; + dir?: unknown; + compare?: unknown; +} + +export const normalizeSortingInfo = function (info: unknown): SortingInfo[] { + const infoArray: unknown[] = Array.isArray(info) ? info : [info]; + + return infoArray.map((item: unknown): SortingInfo => { + const descriptor: SortingDescriptor = isObject(item) ? item : {}; + const result: SortingInfo = { + selector: isFunction(item) || isString(item) + ? item + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + : descriptor.getter || descriptor.field || descriptor.selector, + desc: !!(descriptor.desc || String(descriptor.dir).charAt(0).toLowerCase() === 'd'), + }; + if (descriptor.compare) { + result.compare = descriptor.compare; + } + return result; + }); +}; + +export const errorMessageFromXhr = (function () { + const textStatusMessages: Record<string, string> = { + timeout: 'Network connection timeout', + error: 'Unspecified network error', + parsererror: 'Unexpected server response', + }; + + /// #DEBUG + const textStatusDetails: Record<string, string> = { + timeout: 'possible causes: the remote host is not accessible, overloaded or is not included into the domain white-list when being run in the native container', + error: 'if the remote host is located on another domain, make sure it properly supports cross-origin resource sharing (CORS), or use the JSONP approach instead', + parsererror: 'the remote host did not respond with valid JSON data', + }; + /// #ENDDEBUG + + const explainTextStatus = function (textStatus: string): string { + let result = textStatusMessages[textStatus]; + + if (!result) { + return textStatus; + } + + /// #DEBUG + result += ` (${textStatusDetails[textStatus]})`; + /// #ENDDEBUG + + return result; + }; + + // T542570, https://stackoverflow.com/a/18170879 + let unloading = false; + ready(() => { + const window = getWindow(); + domAdapter.listen(window, 'beforeunload', () => { unloading = true; }); + }); + + return function (xhr: unknown, textStatus: unknown): string { + if (unloading) { + return XHR_ERROR_UNLOAD; + } + + const status = isObject(xhr) && 'status' in xhr ? Number(xhr.status) : NaN; + + if (status < 400) { + return explainTextStatus(String(textStatus)); + } + + const statusText = isObject(xhr) && 'statusText' in xhr ? xhr.statusText : undefined; + + return isString(statusText) ? statusText : ''; + }; +}()); + +type AggregatableValue = number | string | Date; + +export const aggregators = { + count: { + seed: 0, + step(count: number): number { return 1 + count; }, + }, + sum: { + seed: 0, + step(sum: number, item: number): number { return sum + item; }, + }, + min: { + step(min: AggregatableValue, item: AggregatableValue): AggregatableValue { + return item < min ? item : min; + }, + }, + max: { + step(max: AggregatableValue, item: AggregatableValue): AggregatableValue { + return item > max ? item : max; + }, + }, + avg: { + seed: [0, 0], + step(pair: [number, number], value: number): [number, number] { + return [pair[0] + value, pair[1] + 1]; + }, + finalize(pair: [number, number]): number { + return pair[1] ? pair[0] / pair[1] : NaN; + }, + }, +}; + +interface RequestResultLock { + obtain: () => void; + release: () => void; + promise: () => DeferredObj<unknown>; + reset: () => void; +} + +export const processRequestResultLock: RequestResultLock = (function (): RequestResultLock { + let lockCount = 0; + // eslint-disable-next-line @typescript-eslint/init-declarations + let lockDeferred: DeferredObj<unknown> | undefined; + + const obtain = function (): void { + if (lockCount === 0) { + lockDeferred = Deferred<unknown>(); + } + lockCount += 1; + }; + + const release = function (): void { + lockCount -= 1; + if (lockCount < 1) { + lockDeferred?.resolve(); + } + }; + + const promise = function (): DeferredObj<unknown> { + const deferred = lockCount === 0 || !lockDeferred + ? Deferred<unknown>().resolve() + : lockDeferred; + + // @ts-expect-error DeferredObj typings: promise() is declared as a plain Promise + return deferred.promise(); + }; + + const reset = function (): void { + lockCount = 0; + lockDeferred?.resolve(); + }; + + return { + obtain, + release, + promise, + reset, + }; +}()); + +export function isDisjunctiveOperator(condition: unknown): boolean { + return /^(or|\|\||\|)$/i.test(String(condition)); +} + +export function isConjunctiveOperator(condition: unknown): boolean { + return /^(and|&&|&)$/i.test(String(condition)); +} + +const isRecord = (value: unknown): value is Record<string, unknown> => isObject(value) + || Array.isArray(value); + +export const keysEqual = function (keyExpr: unknown, key1: unknown, key2: unknown): boolean { + if (Array.isArray(keyExpr)) { + const names: string[] = map(key1, (value: unknown, name: string): string => name); + const values1: Record<string, unknown> = isRecord(key1) ? key1 : {}; + const values2: Record<string, unknown> = isRecord(key2) ? key2 : {}; + + for (const name of names) { + if (!equalByValue(values1[name], values2[name], { strict: false })) { + return false; + } + } + return true; + } + + return equalByValue(key1, key2, { strict: false }); +}; + +const BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + +/* eslint-disable no-bitwise */ + +function stringToByteArray(str: string): number[] { + const bytes: number[] = []; + + for (let i = 0; i < str.length; i += 1) { + const code = str.charCodeAt(i); + + if (code < 128) { + bytes.push(code); + } else if (code < 2048) { + bytes.push(192 + (code >> 6), 128 + (code & 63)); + } else if (code < 65536) { + bytes.push(224 + (code >> 12), 128 + ((code >> 6) & 63), 128 + (code & 63)); + } else if (code < 2097152) { + bytes.push( + 240 + (code >> 18), + 128 + ((code >> 12) & 63), + 128 + ((code >> 6) & 63), + 128 + (code & 63), + ); + } + } + return bytes; +} + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const base64_encode = function (input: string | number[]): string { + const bytes: number[] = Array.isArray(input) ? input : stringToByteArray(String(input)); + + let result = ''; + + function getBase64Char(index: number): string { + return BASE64_CHARS.charAt(index); + } + + for (let i = 0; i < bytes.length; i += 3) { + const octet1 = bytes[i]; + const octet2 = bytes[i + 1]; + const octet3 = bytes[i + 2]; + + result += [ + octet1 >> 2, + ((octet1 & 3) << 4) | (octet2 >> 4), + isNaN(octet2) ? 64 : ((octet2 & 15) << 2) | (octet3 >> 6), + isNaN(octet3) ? 64 : octet3 & 63, + ].map(getBase64Char).join(''); + } + + return result; +}; + +/* eslint-enable no-bitwise */ + +export const isUnaryOperation = function (crit: unknown): boolean { + return Array.isArray(crit) && crit[0] === '!' && Array.isArray(crit[1]); +}; + +const isGroupOperator = function (value: unknown): boolean { + return value === 'and' || value === 'or'; +}; + +export const isUniformEqualsByOr = function (crit: unknown[]): boolean { + if (crit.length > 2 && Array.isArray(crit[0]) && crit[1] === 'or' && typeof crit[0][0] === 'string' && crit[0][1] === '=') { + const [prop] = crit[0]; + return !crit.find((el, i) => (i % 2 !== 0 ? el !== 'or' + : !Array.isArray(el) || el.length !== 3 || el[0] !== prop || el[1] !== '=')); + } + return false; +}; + +export const isGroupCriterion = function (crit: unknown): boolean { + if (!Array.isArray(crit)) { + return false; + } + + const first = crit[0]; + const second = crit[1]; + + if (Array.isArray(first)) { + return true; + } + if (isFunction(first)) { + if (Array.isArray(second) || isFunction(second) || isGroupOperator(second)) { + return true; + } + } + + return false; +}; + +export const trivialPromise = function <T>(...args: T[]): DeferredObj<T> { + const d = Deferred<T>(); + // @ts-expect-error DeferredObj typings: promise() is declared as a plain Promise + return d.resolve(...args).promise(); +}; + +export const rejectedPromise = function <T>(...args: T[]): DeferredObj<T> { + const d = Deferred<T>(); + // @ts-expect-error DeferredObj typings: promise() is declared as a plain Promise + return d.reject(...args).promise(); +}; + +type ThrottleTimeout = number | (() => number); + +type TimeoutId = ReturnType<typeof setTimeout>; + +function throttle( + func: (this: unknown) => void, + timeout: ThrottleTimeout, +): (this: unknown) => TimeoutId | undefined { + // eslint-disable-next-line @typescript-eslint/init-declarations + let timeoutId: TimeoutId | undefined; + return function (this: unknown): TimeoutId | undefined { + if (!timeoutId) { + timeoutId = setTimeout(() => { + timeoutId = undefined; + func.call(this); + }, isFunction(timeout) ? timeout() : timeout); + } + return timeoutId; + }; +} + +export function throttleChanges<T>( + func: (this: unknown, changes: T[]) => void, + timeout: ThrottleTimeout, +): (this: unknown, changes: T[]) => TimeoutId | undefined { + let cache: T[] = []; + const throttled = throttle(function (this: unknown): void { + func.call(this, cache); + cache = []; + }, timeout); + + return function (this: unknown, changes: T[]): TimeoutId | undefined { + if (Array.isArray(changes)) { + cache.push(...changes); + } + return throttled.call(this); + }; +} diff --git a/packages/devextreme/js/__internal/events/__tests__/click.test.ts b/packages/devextreme/js/__internal/events/__tests__/click.test.ts new file mode 100644 index 000000000000..86a52d657434 --- /dev/null +++ b/packages/devextreme/js/__internal/events/__tests__/click.test.ts @@ -0,0 +1,84 @@ +import { + afterEach, describe, expect, it, jest, +} from '@jest/globals'; +import { removeEvent } from '@js/common/core/events/remove'; +import eventsEngine from '@ts/events/core/events_engine'; + +import { name as clickEventName } from '../click'; + +const noop = (): void => {}; + +const getRemoveHandlersCount = (element: Element): number => { + const elementData = eventsEngine.elementDataMap?.get(element); + + return elementData?.[removeEvent]?.handleObjects.length ?? 0; +}; + +describe('dxclick nodes disposing (5025)', () => { + const clickableNodes: HTMLElement[] = []; + + const createClickableNode = (): HTMLElement => { + const node = document.createElement('div'); + + document.body.appendChild(node); + eventsEngine.on(node, clickEventName, noop); + clickableNodes.push(node); + + return node; + }; + + afterEach(() => { + clickableNodes.forEach((node) => { + eventsEngine.off(node); + node.remove(); + }); + clickableNodes.length = 0; + }); + + it('keeps a foreign dxremove handler on the previously clicked node', () => { + const clicked = createClickableNode(); + const other = createClickableNode(); + + clicked.click(); + + const foreignHandler = jest.fn(); + eventsEngine.on(clicked, removeEvent, foreignHandler); + + other.click(); + eventsEngine.triggerHandler(clicked, { type: removeEvent }); + + expect(foreignHandler).toHaveBeenCalledTimes(1); + }); + + it('keeps a foreign dxremove handler when the same node is clicked twice', () => { + const clicked = createClickableNode(); + + clicked.click(); + expect(getRemoveHandlersCount(clicked)).toBe(1); + + const foreignHandler = jest.fn(); + eventsEngine.on(clicked, removeEvent, foreignHandler); + + clicked.click(); + expect(getRemoveHandlersCount(clicked)).toBe(2); + + eventsEngine.triggerHandler(clicked, { type: removeEvent }); + + expect(foreignHandler).toHaveBeenCalledTimes(1); + }); + + it('removes only its own dxremove handler from the previously clicked node', () => { + const clicked = createClickableNode(); + const other = createClickableNode(); + + clicked.click(); + expect(getRemoveHandlersCount(clicked)).toBe(1); + + eventsEngine.on(clicked, removeEvent, noop); + expect(getRemoveHandlersCount(clicked)).toBe(2); + + other.click(); + + expect(getRemoveHandlersCount(clicked)).toBe(1); + }); +}); diff --git a/packages/devextreme/js/__internal/events/click.ts b/packages/devextreme/js/__internal/events/click.ts index 29194146f916..d6f497463d3f 100644 --- a/packages/devextreme/js/__internal/events/click.ts +++ b/packages/devextreme/js/__internal/events/click.ts @@ -24,10 +24,11 @@ type NativeClickEvent = Event & { let prevented: boolean | null = null; let lastFiredEvent: NativeClickEvent | null = null; -const subscriptions = new Map<NativeClickEvent, NodesDisposingSubscription>(); +let lastSubscription: NodesDisposingSubscription | null = null; const onNodeRemove = (): void => { lastFiredEvent = null; + lastSubscription = null; }; const clickHandler = function (e: EmitterEvent & { originalEvent: NativeClickEvent }): void { @@ -41,25 +42,16 @@ const clickHandler = function (e: EmitterEvent & { originalEvent: NativeClickEve originalEvent.DXCLICK_FIRED = true; } - if (lastFiredEvent && subscriptions.has(lastFiredEvent)) { - // @ts-expect-error the subscription stores onceCallback, not callback, so this - // destructured callback is always undefined and off() drops every dxremove - // handler from the nodes - const { nodes, callback } = subscriptions.get(lastFiredEvent) as NodesDisposingSubscription; + if (lastSubscription) { + const { nodes, onceCallback } = lastSubscription; - unsubscribeNodesDisposing(lastFiredEvent, callback, nodes); - - subscriptions.delete(lastFiredEvent); + unsubscribeNodesDisposing(lastFiredEvent, onceCallback, nodes); } lastFiredEvent = originalEvent; - - const subscriptionData: NodesDisposingSubscription = subscribeNodesDisposing( - lastFiredEvent, - onNodeRemove, - ); - - subscriptions.set(lastFiredEvent, subscriptionData); + lastSubscription = originalEvent + ? subscribeNodesDisposing(originalEvent, onNodeRemove) + : null; fireEvent({ type: CLICK_EVENT_NAME, diff --git a/packages/devextreme/js/__internal/grids/data_grid/data_source/data_source_controller.ts b/packages/devextreme/js/__internal/grids/data_grid/data_source/data_source_controller.ts new file mode 100644 index 000000000000..c978b138692e --- /dev/null +++ b/packages/devextreme/js/__internal/grids/data_grid/data_source/data_source_controller.ts @@ -0,0 +1,20 @@ +import errors from '@js/ui/widget/ui.errors'; +import dataSourceAdapterProvider from '@ts/grids/data_grid/m_data_source_adapter'; +import { DataSourceController } from '@ts/grids/grid_core/data_source/data_source_controller'; +import type { DataSourceAdapterProvider } from '@ts/grids/grid_core/data_source_adapter/types'; + +export class DataGridDataSourceController extends DataSourceController { + protected getAdapterProvider(): DataSourceAdapterProvider { + return dataSourceAdapterProvider; + } + + protected getSpecificDataSourceOption(): unknown { + const dataSource = this.option('dataSource'); + + if (dataSource && !Array.isArray(dataSource) && this.option('keyExpr')) { + errors.log('W1011'); + } + + return super.getSpecificDataSourceOption(); + } +} diff --git a/packages/devextreme/js/__internal/grids/data_grid/data_source/data_source_module.ts b/packages/devextreme/js/__internal/grids/data_grid/data_source/data_source_module.ts new file mode 100644 index 000000000000..87d5621798c0 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/data_grid/data_source/data_source_module.ts @@ -0,0 +1,9 @@ +import gridCore from '@ts/grids/data_grid/m_core'; + +import { DataGridDataSourceController } from './data_source_controller'; + +gridCore.registerModule('dataSource', { + controllers: { + dataSource: DataGridDataSourceController, + }, +}); diff --git a/packages/devextreme/js/__internal/grids/data_grid/export/__tests__/export.integration.test.ts b/packages/devextreme/js/__internal/grids/data_grid/export/__tests__/export.integration.test.ts new file mode 100644 index 000000000000..95e088cca601 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/data_grid/export/__tests__/export.integration.test.ts @@ -0,0 +1,119 @@ +import { + afterEach, beforeEach, describe, expect, it, jest, +} from '@jest/globals'; +import { Deferred } from '@js/core/utils/deferred'; +import type { Properties as DataGridProperties } from '@js/ui/data_grid'; + +import { + afterTest, + beforeTest, + createDataGrid, + flushAsync, +} from '../../../grid_core/__tests__/__mock__/helpers/utils'; + +const DATA = [ + { id: 1, value: 'a' }, + { id: 2, value: 'b' }, +]; + +const SELECTED_ROWS_DATA = [DATA[0]]; + +const setup = async (options: DataGridProperties) => { + const { instance } = await createDataGrid({ + dataSource: DATA, + keyExpr: 'id', + selectedRowKeys: [1], + ...options, + }); + + await flushAsync(); + + const exportController = instance.getController('export'); + const selectionController = instance.getController('selection'); + + const filteredItems = Deferred().resolve(SELECTED_ROWS_DATA); + + return { + exportController, + filteredItems, + loadSelectedItemsWithFilter: jest + .spyOn(selectionController, 'loadSelectedItemsWithFilter') + .mockReturnValue(filteredItems), + getSelectedRowsData: jest + .spyOn(selectionController, 'getSelectedRowsData') + .mockReturnValue(SELECTED_ROWS_DATA), + getAllItems: jest.spyOn(exportController, '_getAllItems'), + }; +}; + +describe('ExportController', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + describe('_getSelectedItems', () => { + it('loads the selected items with a filter when loadItemsOnExportingSelectedItems is enabled', async () => { + const { + exportController, + loadSelectedItemsWithFilter, + getSelectedRowsData, + getAllItems, + filteredItems, + } = await setup({ loadItemsOnExportingSelectedItems: true } as DataGridProperties); + + exportController._getSelectedItems(); + + expect(loadSelectedItemsWithFilter).toHaveBeenCalledTimes(1); + expect(getSelectedRowsData).not.toHaveBeenCalled(); + expect(getAllItems).toHaveBeenCalledWith(filteredItems, true); + }); + + it('loads the selected items with a filter when filtering is a remote operation', async () => { + const { + exportController, + loadSelectedItemsWithFilter, + getSelectedRowsData, + getAllItems, + filteredItems, + } = await setup({ remoteOperations: { filtering: true } }); + + exportController._getSelectedItems(); + + expect(loadSelectedItemsWithFilter).toHaveBeenCalledTimes(1); + expect(getSelectedRowsData).not.toHaveBeenCalled(); + expect(getAllItems).toHaveBeenCalledWith(filteredItems, true); + }); + + it('uses the already selected rows when every operation is local', async () => { + const { + exportController, + loadSelectedItemsWithFilter, + getSelectedRowsData, + getAllItems, + } = await setup({}); + + exportController._getSelectedItems(); + + expect(getSelectedRowsData).toHaveBeenCalledTimes(1); + expect(loadSelectedItemsWithFilter).not.toHaveBeenCalled(); + expect(getAllItems).toHaveBeenCalledWith(SELECTED_ROWS_DATA); + }); + + it('uses the already selected rows when loadItemsOnExportingSelectedItems is disabled explicitly, even with remote filtering', async () => { + const { + exportController, + loadSelectedItemsWithFilter, + getSelectedRowsData, + getAllItems, + } = await setup({ + loadItemsOnExportingSelectedItems: false, + remoteOperations: { filtering: true }, + } as DataGridProperties); + + exportController._getSelectedItems(); + + expect(getSelectedRowsData).toHaveBeenCalledTimes(1); + expect(loadSelectedItemsWithFilter).not.toHaveBeenCalled(); + expect(getAllItems).toHaveBeenCalledWith(SELECTED_ROWS_DATA); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/data_grid/export/m_export.ts b/packages/devextreme/js/__internal/grids/data_grid/export/m_export.ts index e94c768bf7e8..624c83309f76 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/export/m_export.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/export/m_export.ts @@ -16,6 +16,7 @@ import List from '@ts/ui/list/list.edit.search'; import type { ColumnHeadersView } from '../../grid_core/column_headers/m_column_headers'; import type { ColumnsController } from '../../grid_core/columns_controller/m_columns_controller'; import type { DataController } from '../../grid_core/data_controller/data_controller'; +import type { DataSourceController } from '../../grid_core/data_source/data_source_controller'; import type { EditingController } from '../../grid_core/editing/m_editing'; import type { HeaderPanel } from '../../grid_core/header_panel/m_header_panel'; import type { ModuleType } from '../../grid_core/m_types'; @@ -337,6 +338,8 @@ export class ExportController extends dataGridCore.ViewController { private _dataController!: DataController; + private dataSourceController!: DataSourceController; + private _selectionController!: SelectionController; private _headersView!: ColumnHeadersView; @@ -353,6 +356,7 @@ export class ExportController extends dataGridCore.ViewController { this.throwWarningIfNoOnExportingEvent(); this._columnsController = this.getController('columns'); this._dataController = this.getController('data'); + this.dataSourceController = this.getController('dataSource'); this._selectionController = this.getController('selection'); this._rowsView = this.getView('rowsView'); this._headersView = this.getView('columnHeadersView'); @@ -701,7 +705,7 @@ export class ExportController extends dataGridCore.ViewController { private needLoadItemsOnExportingSelectedItems(): boolean { return this.option('loadItemsOnExportingSelectedItems') - ?? (this._dataController._dataSource?.remoteOperations().filtering ?? false); + ?? (this.dataSourceController.remoteOperations().filtering ?? false); } } diff --git a/packages/devextreme/js/__internal/grids/data_grid/grouping/__tests__/utils.test.ts b/packages/devextreme/js/__internal/grids/data_grid/grouping/__tests__/utils.test.ts index ff48341edb38..4bf12e9d36b8 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/grouping/__tests__/utils.test.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/grouping/__tests__/utils.test.ts @@ -5,7 +5,7 @@ import type { ProcessedItem } from '@ts/grids/grid_core/data_controller/types'; import type { RawItemData } from '@ts/grids/grid_core/data_source_adapter/types'; import { - isGroupNode, isGroupRow, isSameContinuationState, isSameExpandedState, + getGroupColumnIndices, isGroupNode, isGroupRow, isSameContinuationState, isSameExpandedState, } from '../utils'; const groupRow = (partial: Partial<ProcessedItem> = {}): ProcessedItem => ({ @@ -136,3 +136,39 @@ describe('isSameContinuationState', () => { )).toBe(true); }); }); + +describe('getGroupColumnIndices', () => { + const expandedGroupRow = (partial: Partial<ProcessedItem> = {}): ProcessedItem => groupRow({ + isExpanded: true, + data: { isContinuation: false, isContinuationOnNextPage: false }, + ...partial, + }); + + it('should skip the group expand cell', () => { + const oldItem = expandedGroupRow({ + cells: [{ column: { type: 'groupExpand' } }, {}, { column: { dataField: 'name' } }], + }); + + expect(getGroupColumnIndices(oldItem, expandedGroupRow())).toEqual([1, 2]); + }); + + it('should return undefined when the old row has no cells', () => { + expect(getGroupColumnIndices(expandedGroupRow(), expandedGroupRow())).toBeUndefined(); + }); + + it('should return undefined when the expanded state has changed', () => { + const oldItem = expandedGroupRow({ cells: [{}] }); + + expect(getGroupColumnIndices(oldItem, expandedGroupRow({ isExpanded: false }))) + .toBeUndefined(); + }); + + it('should return undefined when the continuation state has changed', () => { + const oldItem = expandedGroupRow({ cells: [{}] }); + const newItem = expandedGroupRow({ + data: { isContinuation: true, isContinuationOnNextPage: false }, + }); + + expect(getGroupColumnIndices(oldItem, newItem)).toBeUndefined(); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/data_grid/grouping/extenders/__tests__/grouping_data_controller.data_row_index.test.ts b/packages/devextreme/js/__internal/grids/data_grid/grouping/extenders/__tests__/grouping_data_controller.data_row_index.test.ts new file mode 100644 index 000000000000..6e1d64269b75 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/data_grid/grouping/extenders/__tests__/grouping_data_controller.data_row_index.test.ts @@ -0,0 +1,81 @@ +import { + afterEach, beforeEach, describe, expect, it, +} from '@jest/globals'; +import { + afterTest, + beforeTest, + createDataGrid, +} from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils'; +import type { DataController } from '@ts/grids/grid_core/data_controller/data_controller'; +import type { ProcessedItem } from '@ts/grids/grid_core/data_controller/types'; + +declare class ExposedDataController extends DataController { + public _items: ProcessedItem[]; + + public adjustInsertRowIndex: (visibleRowIndex: number) => number; +} + +const row = (rowType: ProcessedItem['rowType']): ProcessedItem => ({ + rowType, + key: rowType, + data: {}, + values: [], +}); + +const withVisibleRows = async ( + rows: ProcessedItem[], +): Promise<(visibleRowIndex: number) => number> => { + const { instance } = await createDataGrid({ + dataSource: [], + columns: ['name', 'age'], + }); + const dataController = instance.getController('data') as unknown as ExposedDataController; + + dataController._items = rows; + + return (visibleRowIndex: number): number => dataController.adjustInsertRowIndex(visibleRowIndex); +}; + +describe('Grouping data controller data row index', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + it('should count the group rows along with the data rows', async () => { + const dataRowIndex = await withVisibleRows([ + row('data'), row('group'), row('detail'), row('data'), + ]); + + expect(dataRowIndex(3)).toBe(2); + expect(dataRowIndex(4)).toBe(3); + }); + + it('should not count the group footer rows', async () => { + const dataRowIndex = await withVisibleRows([row('data'), row('groupFooter'), row('data')]); + + expect(dataRowIndex(3)).toBe(2); + }); + + it('should not count the adaptive detail rows', async () => { + const dataRowIndex = await withVisibleRows([row('data'), row('detailAdaptive'), row('data')]); + + expect(dataRowIndex(3)).toBe(2); + }); + + it('should count only the data rows when there are no group rows', async () => { + const dataRowIndex = await withVisibleRows([row('data'), row('detail'), row('data')]); + + expect(dataRowIndex(3)).toBe(2); + }); + + it('should return zero for the first visible index', async () => { + const dataRowIndex = await withVisibleRows([row('group'), row('data')]); + + expect(dataRowIndex(0)).toBe(0); + }); + + it('should count the rows that are there when the index is out of range', async () => { + const dataRowIndex = await withVisibleRows([row('group'), row('data')]); + + expect(dataRowIndex(10)).toBe(2); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/data_grid/grouping/extenders/__tests__/grouping_data_controller.row_changes.test.ts b/packages/devextreme/js/__internal/grids/data_grid/grouping/extenders/__tests__/grouping_data_controller.row_changes.test.ts index 4a0559e43a5c..3c7bea04f2e4 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/grouping/extenders/__tests__/grouping_data_controller.row_changes.test.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/grouping/extenders/__tests__/grouping_data_controller.row_changes.test.ts @@ -12,20 +12,31 @@ interface GroupRowState { isExpanded?: boolean; isContinuation?: boolean; isContinuationOnNextPage?: boolean; + values?: unknown[]; + cells?: ProcessedItem['cells']; } const groupRow = ({ isExpanded = true, isContinuation = false, isContinuationOnNextPage = false, + values = [], + cells, }: GroupRowState = {}): ProcessedItem => ({ rowType: 'group', key: [1], data: { isContinuation, isContinuationOnNextPage }, - values: [], + values, isExpanded, + cells, }); +const renderedCells: ProcessedItem['cells'] = [ + { column: { type: 'groupExpand' } }, + {}, + { column: { dataField: 'name' } }, +]; + const dataRow = (partial: Partial<ProcessedItem> = {}): ProcessedItem => ({ rowType: 'data', key: 1, @@ -46,12 +57,13 @@ describe('Grouping data controller row changes', () => { it('should report a group row when isExpanded changed', async () => { const change = await refreshRow( - groupRow({ isExpanded: true }), + groupRow({ isExpanded: true, cells: renderedCells }), groupRow({ isExpanded: false }), ); expect(change.rowIndices).toEqual([0]); expect(change.changeTypes).toEqual(['update']); + expect(change.columnIndices).toEqual([undefined]); }); it('should report a group row when isContinuation changed', async () => { @@ -68,6 +80,26 @@ describe('Grouping data controller row changes', () => { expect(change.changeTypes).toEqual(['update']); }); + it('should diff every group cell but the expand one when values changed', async () => { + const change = await refreshRow( + groupRow({ values: ['Alex'], cells: renderedCells }), + groupRow({ values: ['Bob'] }), + ); + + expect(change.rowIndices).toEqual([0]); + expect(change.columnIndices).toEqual([[1, 2]]); + }); + + it('should repaint the whole group row when it was never rendered', async () => { + const change = await refreshRow( + groupRow({ values: ['Alex'] }), + groupRow({ values: ['Bob'] }), + ); + + expect(change.rowIndices).toEqual([0]); + expect(change.columnIndices).toEqual([undefined]); + }); + it('should not report a data row when isExpanded changed (master detail row)', async () => { const change = await refreshRow( dataRow({ isExpanded: false }), diff --git a/packages/devextreme/js/__internal/grids/data_grid/grouping/extenders/grouping_data_controller.ts b/packages/devextreme/js/__internal/grids/data_grid/grouping/extenders/grouping_data_controller.ts index 440fb56af6d1..769d0aa1590f 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/grouping/extenders/grouping_data_controller.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/grouping/extenders/grouping_data_controller.ts @@ -3,6 +3,7 @@ import { Deferred, when } from '@js/core/utils/deferred'; import type { Properties } from '@js/ui/data_grid'; import type { DataController } from '@ts/grids/grid_core/data_controller/data_controller'; import type { ItemProcessingOptions, ProcessedItem } from '@ts/grids/grid_core/data_controller/types'; +import { countRowsBefore } from '@ts/grids/grid_core/data_controller/utils/row_changes'; import type { RawItemData } from '@ts/grids/grid_core/data_source_adapter/types'; import type { ModuleType, @@ -16,7 +17,7 @@ import type { ChangeRowExpandArgs, GroupItem, ProcessGroupItemsOptions, } from '../types'; import { - isGroupNode, isGroupRow, isSameContinuationState, isSameExpandedState, + getGroupColumnIndices, isGroupNode, isGroupRow, isSameContinuationState, isSameExpandedState, } from '../utils'; export const groupingDataControllerExtender = ( @@ -134,6 +135,25 @@ export const groupingDataControllerExtender = ( return resultItems; } + protected adjustInsertRowIndex(visibleRowIndex: number): number { + const groupRowCount = countRowsBefore(this.getVisibleRows(), visibleRowIndex, 'group'); + + return super.adjustInsertRowIndex(visibleRowIndex) + groupRowCount; + } + + protected getChangedColumnIndices( + oldItem: ProcessedItem, + newItem: ProcessedItem, + visibleRowIndex: number, + isLiveUpdate?: boolean, + ): number[] | undefined { + if (oldItem.rowType === 'group' && newItem.rowType === 'group') { + return getGroupColumnIndices(oldItem, newItem); + } + + return super.getChangedColumnIndices(oldItem, newItem, visibleRowIndex, isLiveUpdate); + } + protected isSameRowState(item1: ProcessedItem, item2: ProcessedItem): boolean { if (item1.rowType === 'group' && (!isSameExpandedState(item1, item2) || !isSameContinuationState(item1, item2))) { diff --git a/packages/devextreme/js/__internal/grids/data_grid/grouping/m_grouping.ts b/packages/devextreme/js/__internal/grids/data_grid/grouping/m_grouping.ts index 608c733636ba..e8886c114650 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/grouping/m_grouping.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/grouping/m_grouping.ts @@ -105,6 +105,7 @@ const dataSourceAdapterExtender = (Base: ModuleType<DataSourceAdapter>) => class groups[i].isExpanded = group[i].isExpanded; } } + // @ts-expect-error normalizeSortingInfo() types selector wider than public KeySelector dataSource.group(groups); that._grouping.foreachGroups((groupInfo, parents) => { if (groupIndex === undefined || groupIndex === parents.length - 1) { diff --git a/packages/devextreme/js/__internal/grids/data_grid/grouping/m_grouping_expanded.ts b/packages/devextreme/js/__internal/grids/data_grid/grouping/m_grouping_expanded.ts index 023624dca8b4..f153e81939df 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/grouping/m_grouping_expanded.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/grouping/m_grouping_expanded.ts @@ -343,7 +343,6 @@ export class GroupingHelper extends GroupingHelperCore { if (groupCount) { let { data } = options; - // @ts-expect-error const query = dataQuery(data); storeHelper.multiLevelGroup(query, groups).enumerate().done((groupedData) => { data = groupedData; diff --git a/packages/devextreme/js/__internal/grids/data_grid/grouping/utils.ts b/packages/devextreme/js/__internal/grids/data_grid/grouping/utils.ts index 52a05945ae26..e7e8a71a566e 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/grouping/utils.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/grouping/utils.ts @@ -25,3 +25,19 @@ export function isSameContinuationState(item1: ProcessedItem, item2: ProcessedIt return item1.data?.isContinuation === item2.data?.isContinuation && item1.data?.isContinuationOnNextPage === item2.data?.isContinuationOnNextPage; } + +export function getGroupColumnIndices( + oldItem: ProcessedItem, + newItem: ProcessedItem, +): number[] | undefined { + const isSameState = isSameExpandedState(oldItem, newItem) + && isSameContinuationState(oldItem, newItem); + + if (!oldItem.cells || !isSameState) { + return undefined; + } + + return oldItem.cells + .map((cell, index) => (cell.column?.type !== 'groupExpand' ? index : -1)) + .filter((index) => index >= 0); +} diff --git a/packages/devextreme/js/__internal/grids/data_grid/m_data_controller.ts b/packages/devextreme/js/__internal/grids/data_grid/m_data_controller.ts deleted file mode 100644 index e6632d7df28f..000000000000 --- a/packages/devextreme/js/__internal/grids/data_grid/m_data_controller.ts +++ /dev/null @@ -1,31 +0,0 @@ -import errors from '@js/ui/widget/ui.errors'; -import { DataController, dataControllerModule } from '@ts/grids/grid_core/data_controller/data_controller'; - -import type { DataSourceAdapterProvider } from '../grid_core/data_source_adapter/types'; -import gridCore from './m_core'; -import dataSourceAdapterProvider from './m_data_source_adapter'; - -class DataGridDataController extends DataController { - protected _getDataSourceAdapterProvider(): DataSourceAdapterProvider { - return dataSourceAdapterProvider; - } - - protected _getSpecificDataSourceOption() { - const dataSource = this.option('dataSource'); - - if (dataSource && !Array.isArray(dataSource) && this.option('keyExpr')) { - errors.log('W1011'); - } - - return super._getSpecificDataSourceOption(); - } -} - -export { DataGridDataController as DataController }; - -gridCore.registerModule('data', { - defaultOptions: dataControllerModule.defaultOptions, - controllers: { - data: DataGridDataController, - }, -}); diff --git a/packages/devextreme/js/__internal/grids/data_grid/m_widget_base.ts b/packages/devextreme/js/__internal/grids/data_grid/m_widget_base.ts index 1710af383abd..fa9cba25a7fe 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/m_widget_base.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/m_widget_base.ts @@ -1,6 +1,8 @@ import './module_not_extended/column_headers'; import './m_columns_controller'; -import './m_data_controller'; +import './data_source/data_source_module'; +import './module_not_extended/filter'; +import './module_not_extended/data_controller'; import './module_not_extended/sorting'; import './module_not_extended/rows'; import './module_not_extended/context_menu'; @@ -24,6 +26,7 @@ import gridCore from './m_core'; const DATAGRID_DEPRECATED_TEMPLATE_WARNING = 'Specifying grid templates with the jQuery selector name is now deprecated. Use the DOM Node or the jQuery object that references this selector instead.'; gridCore.registerModulesOrder([ + 'dataSource', 'stateStoring', 'columns', 'selection', @@ -37,6 +40,7 @@ gridCore.registerModulesOrder([ 'masterDetail', 'validating', 'adaptivity', + 'filter', 'data', 'virtualScrolling', 'aiColumn', diff --git a/packages/devextreme/js/__internal/grids/data_grid/module_not_extended/data_controller.ts b/packages/devextreme/js/__internal/grids/data_grid/module_not_extended/data_controller.ts new file mode 100644 index 000000000000..5965240a4e70 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/data_grid/module_not_extended/data_controller.ts @@ -0,0 +1,5 @@ +import { dataControllerModule } from '@ts/grids/grid_core/data_controller/data_controller'; + +import gridCore from '../m_core'; + +gridCore.registerModule('data', dataControllerModule); diff --git a/packages/devextreme/js/__internal/grids/data_grid/module_not_extended/filter.ts b/packages/devextreme/js/__internal/grids/data_grid/module_not_extended/filter.ts new file mode 100644 index 000000000000..842edf76f69d --- /dev/null +++ b/packages/devextreme/js/__internal/grids/data_grid/module_not_extended/filter.ts @@ -0,0 +1,5 @@ +import { filterModule } from '@ts/grids/grid_core/filter/filter_module'; + +import gridCore from '../m_core'; + +gridCore.registerModule('filter', filterModule); diff --git a/packages/devextreme/js/__internal/grids/data_grid/summary/extenders/__tests__/summary_data_controller.load_all_items.test.ts b/packages/devextreme/js/__internal/grids/data_grid/summary/extenders/__tests__/summary_data_controller.load_all_items.test.ts new file mode 100644 index 000000000000..ebbef09ad345 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/data_grid/summary/extenders/__tests__/summary_data_controller.load_all_items.test.ts @@ -0,0 +1,46 @@ +import { + afterEach, beforeEach, describe, expect, it, +} from '@jest/globals'; +import type { Properties as DataGridProperties } from '@js/ui/data_grid'; +import { + afterTest, + beforeTest, + createDataGrid, + flushAsync, +} from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils'; + +const DATA = [{ id: 1, value: 10 }, { id: 2, value: 20 }]; + +const loadAllSummary = async ( + options: DataGridProperties = {}, +): Promise<unknown> => { + const { instance } = await createDataGrid({ dataSource: DATA, ...options }); + const dataController = instance.getController('data'); + + let resolvedSummary: unknown = null; + dataController.loadAllItems().done((_items, summary) => { + resolvedSummary = summary; + }); + await flushAsync(); + + return resolvedSummary; +}; + +describe('Summary data controller loadAllItems', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + it('should resolve the total summary as the second argument', async () => { + const summary = await loadAllSummary({ + summary: { totalItems: [{ column: 'value', summaryType: 'sum' }] }, + }); + + expect(summary).toEqual([30]); + }); + + it('should resolve no second argument when no summary is configured', async () => { + const summary = await loadAllSummary(); + + expect(summary).toBeUndefined(); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/data_grid/summary/extenders/__tests__/summary_data_controller.row_changes.test.ts b/packages/devextreme/js/__internal/grids/data_grid/summary/extenders/__tests__/summary_data_controller.row_changes.test.ts index 08455315c79f..fbfd87bbc4ea 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/summary/extenders/__tests__/summary_data_controller.row_changes.test.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/summary/extenders/__tests__/summary_data_controller.row_changes.test.ts @@ -55,6 +55,7 @@ describe('Summary data controller row changes', () => { expect(change.rowIndices).toEqual([0]); expect(change.changeTypes).toEqual(['update']); + expect(change.columnIndices).toEqual([undefined]); }); it('should report a group footer when isContinuation changed', async () => { diff --git a/packages/devextreme/js/__internal/grids/data_grid/summary/extenders/summary_data_controller.ts b/packages/devextreme/js/__internal/grids/data_grid/summary/extenders/summary_data_controller.ts index a32233d8861f..fa06dc0332d0 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/summary/extenders/summary_data_controller.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/summary/extenders/summary_data_controller.ts @@ -6,7 +6,10 @@ import type { Format } from '@js/localization'; import type { SummaryGroupItem as SummaryGroupItemOption } from '@js/ui/data_grid'; import type { Column } from '@ts/grids/data_grid/types'; import type { DataController } from '@ts/grids/grid_core/data_controller/data_controller'; -import type { DataChange, ItemProcessingOptions, ProcessedItem } from '@ts/grids/grid_core/data_controller/types'; +import type { + DataChange, ItemProcessingOptions, LoadAllItemsDeferred, ProcessedItem, +} from '@ts/grids/grid_core/data_controller/types'; +import type { CustomLoadResult } from '@ts/grids/grid_core/data_source_adapter/custom_loader'; import type { RawItemData } from '@ts/grids/grid_core/data_source_adapter/types'; import type { ModuleType, OptionChanged } from '@ts/grids/grid_core/m_types'; @@ -288,6 +291,26 @@ export const summaryDataControllerExtender = ( return super.isSameRowState(item1, item2); } + protected getChangedColumnIndices( + oldItem: ProcessedItem, + newItem: ProcessedItem, + visibleRowIndex: number, + isLiveUpdate?: boolean, + ): number[] | undefined { + if (newItem.rowType === DATAGRID_GROUP_FOOTER_ROW_TYPE) { + return undefined; + } + + return super.getChangedColumnIndices(oldItem, newItem, visibleRowIndex, isLiveUpdate); + } + + protected resolveLoadAllItems( + d: LoadAllItemsDeferred, + loadResult: CustomLoadResult, + ): void { + d.resolve(this.processLoadAllItems(loadResult), loadResult.extra?.summary); + } + protected _updateItemsCore(change: DataChange): void { const dataSource = this._dataSource; const summaryTotalItems = this.option('summary.totalItems'); diff --git a/packages/devextreme/js/__internal/grids/data_grid/summary/m_summary.ts b/packages/devextreme/js/__internal/grids/data_grid/summary/m_summary.ts index baeb374a1b7b..b6d8e21f13df 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/summary/m_summary.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/summary/m_summary.ts @@ -117,7 +117,6 @@ const sortGroupsBySummaryCore = function (items, groups, sortByGroups) { let query; if (group && sorts && sorts.length) { - // @ts-expect-error query = dataQuery(items); each(sorts, function (index) { if (index === 0) { @@ -307,7 +306,6 @@ export const summaryDataSourceAdapterExtender = ( } private sortLastLevelGroupItems(items, groups, paths) { - // @ts-expect-error const groupedItems = storeHelper.multiLevelGroup(dataQuery(items), groups).toArray(); let result = []; diff --git a/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/helpers/row_changes.ts b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/helpers/row_changes.ts index 67df1f4eda7f..a93f657a28d5 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/helpers/row_changes.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/helpers/row_changes.ts @@ -1,3 +1,4 @@ +import type { Properties as DataGridProperties } from '@js/ui/data_grid'; import { createDataGrid } from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils'; import type { DataController } from '@ts/grids/grid_core/data_controller/data_controller'; import type { @@ -10,26 +11,37 @@ declare class ExposedDataController extends DataController { public applyChangesOnly: (change: DataChange) => void; } -const createRefreshChange = (items: ProcessedItem[]): DataChange => ({ +export interface RefreshRowOptions { + gridOptions?: DataGridProperties; + isLiveUpdate?: boolean; +} + +const createRefreshChange = ( + items: ProcessedItem[], + isLiveUpdate?: boolean, +): DataChange => ({ changeType: 'refresh', repaintChangesOnly: true, items, + isLiveUpdate, }); export const refreshRow = async ( oldItem: ProcessedItem, newItem: ProcessedItem, + { gridOptions, isLiveUpdate }: RefreshRowOptions = {}, ): Promise<UpdateChange> => { const { instance } = await createDataGrid({ dataSource: [], columns: ['name', 'age'], repaintChangesOnly: true, + ...gridOptions, }); const dataController = instance.getController('data') as unknown as ExposedDataController; dataController._items = [oldItem]; - const change = createRefreshChange([newItem]); + const change = createRefreshChange([newItem], isLiveUpdate); dataController.applyChangesOnly(change); diff --git a/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/helpers/utils.ts b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/helpers/utils.ts index 3acad5bf9f84..d040c7d964aa 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/helpers/utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/helpers/utils.ts @@ -20,6 +20,12 @@ export type OptionSpy = jest.Mock<(optionName: string, optionValue?: unknown) => export const spyOnOption = (instance: InternalGrid): OptionSpy => jest .spyOn(instance, 'option') as unknown as OptionSpy; +// Reads the DataController._dataSource mirror. Assertions that pin the mirror have to read the +// protected field itself rather than a delegating method. Goes away with the field, in Task B4. +export const getMirroredAdapter = ( + instance: { getController: (name: 'data') => unknown }, +): unknown => (instance.getController('data') as { _dataSource?: unknown })._dataSource; + export const SELECTORS = { gridContainer: '#gridContainer', }; @@ -78,3 +84,7 @@ export const flushAsync = async (): Promise<void> => { jest.runAllTimers(); await Promise.resolve(); }; + +export const toPlainFilter = (value: unknown): unknown => (Array.isArray(value) + ? Array.from(value, toPlainFilter) + : value); diff --git a/packages/devextreme/js/__internal/grids/grid_core/__tests__/m_utils.combine_filters.test.ts b/packages/devextreme/js/__internal/grids/grid_core/__tests__/m_utils.combine_filters.test.ts new file mode 100644 index 000000000000..0496144016dd --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/__tests__/m_utils.combine_filters.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, it } from '@jest/globals'; +import type { DataFilter } from '@ts/grids/grid_core/data_controller/types'; +import gridCoreUtils from '@ts/grids/grid_core/m_utils'; + +const FIRST = ['a', '=', 1]; +const SECOND = ['b', '=', 2]; +const THIRD = ['c', '=', 3]; +const MATCH_NOTHING = ['!']; + +const combineFilters = ( + filters: unknown[], + operation?: 'and' | 'or', +): DataFilter => gridCoreUtils.combineFilters(filters as DataFilter[], operation); + +describe('combineFilters', () => { + describe('when the list is empty', () => { + it('should return undefined', () => { + expect(combineFilters([])).toBeUndefined(); + }); + }); + + describe('when every entry is empty', () => { + it('should return undefined', () => { + expect(combineFilters([null, undefined])).toBeUndefined(); + }); + }); + + describe('when a single expression is given', () => { + it('should unwrap it', () => { + expect(combineFilters([FIRST])).toEqual(FIRST); + }); + }); + + describe('when several expressions are given', () => { + it('should join them with and by default', () => { + expect(combineFilters([FIRST, SECOND])).toEqual([FIRST, 'and', SECOND]); + }); + + it('should join them with the requested operation', () => { + expect(combineFilters([FIRST, SECOND], 'or')).toEqual([FIRST, 'or', SECOND]); + }); + + it('should skip the empty entries', () => { + expect(combineFilters([null, FIRST, undefined, SECOND])) + .toEqual([FIRST, 'and', SECOND]); + }); + }); + + describe('when the same expressions are folded pairwise and combined in one call', () => { + it('should nest to the left while folding pairwise', () => { + expect(combineFilters([combineFilters([FIRST, SECOND]), THIRD])) + .toEqual([[FIRST, 'and', SECOND], 'and', THIRD]); + }); + + it('should stay flat while combining in one call', () => { + expect(combineFilters([FIRST, SECOND, THIRD])) + .toEqual([FIRST, 'and', SECOND, 'and', THIRD]); + }); + + it('should produce different structures for the two ways', () => { + expect(combineFilters([combineFilters([FIRST, SECOND]), THIRD])) + .not.toEqual(combineFilters([FIRST, SECOND, THIRD])); + }); + }); + + describe('when a match-nothing filter is among the entries', () => { + it('should collapse an and group', () => { + expect(combineFilters([FIRST, MATCH_NOTHING])).toEqual(MATCH_NOTHING); + }); + + it('should be skipped in an or group', () => { + expect(combineFilters([FIRST, MATCH_NOTHING], 'or')).toEqual(FIRST); + }); + + it('should survive on its own', () => { + expect(combineFilters([MATCH_NOTHING])).toEqual(MATCH_NOTHING); + }); + }); + + describe('when a predicate function is among the entries', () => { + const predicate = (item: unknown): boolean => Boolean(item); + + it('should pass it through on its own', () => { + expect(combineFilters([predicate])).toBe(predicate); + }); + + it('should make it a member of the group next to an expression', () => { + const combined = combineFilters([predicate, FIRST]) as unknown as unknown[]; + + expect(combined).toHaveLength(3); + expect(combined[0]).toBe(predicate); + expect(combined.slice(1)).toEqual(['and', FIRST]); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/adaptivity/extenders/adaptivity_data_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/adaptivity/extenders/adaptivity_data_controller.ts index ed851fd89be5..67486143bdda 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/adaptivity/extenders/adaptivity_data_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/adaptivity/extenders/adaptivity_data_controller.ts @@ -4,6 +4,7 @@ import type { DataChange, ProcessedItem } from '@ts/grids/grid_core/data_control import type { RawItemData } from '@ts/grids/grid_core/data_source_adapter/types'; import type { ModuleType, RowKey } from '@ts/grids/grid_core/m_types'; import gridCoreUtils from '@ts/grids/grid_core/m_utils'; +import type { MasterDetailDataControllerExtension } from '@ts/grids/grid_core/master_detail/m_master_detail'; import { ADAPTIVE_ROW_TYPE, @@ -15,8 +16,10 @@ import type { AdaptiveColumnsController } from '../m_adaptivity'; import type { AdaptivityDataController } from '../types'; import { getAdaptiveDetailRowIndex, resolveAdaptiveDetailRowTarget } from '../utils'; +type AdaptivityDataControllerBase = DataController & MasterDetailDataControllerExtension; + export const adaptivityDataControllerExtender = ( - Base: ModuleType<DataController>, + Base: ModuleType<AdaptivityDataControllerBase>, ): ModuleType<AdaptivityDataController> => class AdaptivityDataControllerExtender extends Base { private adaptiveExpandedKey?: RowKey; @@ -59,7 +62,7 @@ export const adaptivityDataControllerExtender = ( return processedItems; } - protected getRowIndicesForExpand(key: RowKey): number[] { + public override getRowIndicesForExpand(key: RowKey): number[] { const rowIndices = super.getRowIndicesForExpand(key); if (this.adaptiveColumnsController.isAdaptiveDetailRowExpanded(key)) { diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/ai_assistant_integration_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/ai_assistant_integration_controller.ts index 816a27943382..742073e23aab 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/ai_assistant_integration_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/ai_assistant_integration_controller.ts @@ -7,6 +7,7 @@ import errors from '@js/ui/widget/ui.errors'; import type { ColumnsController } from '@ts/grids/grid_core/columns_controller/m_columns_controller'; import type { Column } from '@ts/grids/grid_core/columns_controller/types'; import type { DataController } from '@ts/grids/grid_core/data_controller/data_controller'; +import type { DataSourceController } from '@ts/grids/grid_core/data_source/data_source_controller'; import { Controller } from '../m_modules'; import type { @@ -22,6 +23,8 @@ export class AIAssistantIntegrationController extends Controller { private dataController!: DataController; + private dataSourceController!: DataSourceController; + private getAICommandCallbacks( callbacks?: RequestCallbacks<ExecuteGridAssistantCommandResult>, ): RequestCallbacks<ExecuteGridAssistantCommandResult> { @@ -46,7 +49,7 @@ export class AIAssistantIntegrationController extends Controller { protected buildContext(): GridContext { return { - keyExpr: this.option('keyExpr') ?? this.dataController.getDataSource()?.store()?.key(), + keyExpr: this.option('keyExpr') ?? this.dataSourceController.store()?.key(), columns: this.buildColumnsContext(), filtering: { filterValue: this.option('filterValue'), @@ -97,6 +100,7 @@ export class AIAssistantIntegrationController extends Controller { public init(): void { this.createAction('onAIAssistantRequestCreating'); this.dataController = this.getController('data'); + this.dataSourceController = this.getController('dataSource'); this.columnsController = this.getController('columns'); } diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/selection.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/selection.ts index 76105e91357c..2cd56c186cb9 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/selection.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/selection.ts @@ -187,8 +187,7 @@ const resolveKeysFromAllPages = ( component: InternalGrid, indexes: number[], ): Promise<RowKey[] | null> => { - const dataController = component.getController('data'); - const isRemotePaging = !!dataController.dataSource()?.remoteOperations()?.paging; + const isRemotePaging = !!component.getController('dataSource').remoteOperations().paging; return isRemotePaging ? resolveKeysFromAllPagesRemote(component, indexes) diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_column/controllers/m_ai_column_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_column/controllers/m_ai_column_controller.ts index 94ac23fecd5f..3825239cf26e 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_column/controllers/m_ai_column_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_column/controllers/m_ai_column_controller.ts @@ -5,6 +5,7 @@ import type { Column } from '@ts/grids/grid_core/columns_controller/types'; import type { ColumnsController } from '../../columns_controller/m_columns_controller'; import type { DataController } from '../../data_controller/data_controller'; +import type { DataSourceController } from '../../data_source/data_source_controller'; import type { ChangedEvent, RawItemData } from '../../data_source_adapter/types'; import { Controller } from '../../m_modules'; import type { RowKey } from '../../m_types'; @@ -16,6 +17,8 @@ import { AIColumnIntegrationController } from './m_ai_column_integration_control export class AIColumnController extends Controller { private dataController!: DataController; + private dataSourceController!: DataSourceController; + private columnsController!: ColumnsController; private aiColumnIntegrationController!: AIColumnIntegrationController; @@ -56,12 +59,12 @@ export class AIColumnController extends Controller { private addAICommandColumn(): void { const that = this; - const { dataController, aiColumnIntegrationController } = this; + const { dataSourceController, aiColumnIntegrationController } = this; this.columnsController.addCommandColumn({ ...getAICommandColumnDefaultOptions(), calculateCellValue(data: RawItemData) { - const key = dataController.keyOf(data); + const key = dataSourceController.keyOf(data); const cellValue = aiColumnIntegrationController.getAIColumnText(this.name, key); const defaultValue = that.getDefaultCellValue(this, cellValue); @@ -73,7 +76,7 @@ export class AIColumnController extends Controller { private subscribeToDataSourceChanged(): void { this.dataSourceChangedHandler = this.handleDataSourceChanged.bind(this); - this.dataController.dataSource()?.changed.add(this.dataSourceChangedHandler); + this.dataSourceController.getAdapter()?.changed.add(this.dataSourceChangedHandler); } private unsubscribeFromDataControllerChanged(): void { @@ -102,7 +105,7 @@ export class AIColumnController extends Controller { } private unsubscribeFromStoreEvents(): void { - const store = this.dataController.store(); + const store = this.dataSourceController.store(); if (this.storeUpdatedHandler) { store?.off('updated', this.storeUpdatedHandler); @@ -116,7 +119,7 @@ export class AIColumnController extends Controller { } private subscribeToStoreEvents(): void { - const store = this.dataController.store(); + const store = this.dataSourceController.store(); if (!store) { return; @@ -156,7 +159,7 @@ export class AIColumnController extends Controller { } private checkStoreKey(): boolean { - const store = this.dataController.store(); + const store = this.dataSourceController.store(); if (store && !store.key()) { this.dataController.fireError('E1042', 'AI Column'); @@ -204,6 +207,7 @@ export class AIColumnController extends Controller { public init(): void { this.columnsController = this.getController('columns'); this.dataController = this.getController('data'); + this.dataSourceController = this.getController('dataSource'); this.aiColumnIntegrationController = new AIColumnIntegrationController(this.component); this.aiColumnIntegrationController.init(); @@ -326,7 +330,7 @@ export class AIColumnController extends Controller { public dispose(): void { super.dispose(); - this.dataController.dataSource()?.changed.remove(this.dataSourceChangedHandler); + this.dataSourceController.getAdapter()?.changed.remove(this.dataSourceChangedHandler); this.unsubscribeFromStoreEvents(); this.unsubscribeFromDataControllerChanged(); } diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_column/controllers/m_ai_column_integration_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_column/controllers/m_ai_column_integration_controller.ts index 7654f0f132e5..a82985bf1c18 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_column/controllers/m_ai_column_integration_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_column/controllers/m_ai_column_integration_controller.ts @@ -8,6 +8,7 @@ import errors from '@js/ui/widget/ui.errors'; import type { ColumnsController } from '../../columns_controller/m_columns_controller'; import type { DataController } from '../../data_controller/data_controller'; +import type { DataSourceController } from '../../data_source/data_source_controller'; import type { RawItemData } from '../../data_source_adapter/types'; import type { ErrorHandlingViewController } from '../../error_handling/error_handling_view_controller'; import { Controller } from '../../m_modules'; @@ -23,6 +24,8 @@ export class AIColumnIntegrationController extends Controller { private dataController!: DataController; + private dataSourceController!: DataSourceController; + private errorHandlingController!: ErrorHandlingViewController; private aiColumnCacheController!: AIColumnCacheController; @@ -78,12 +81,13 @@ export class AIColumnIntegrationController extends Controller { } private getRowKeyHash(item: RawItemData): PropertyKey { - return getKeyHash(this.dataController.keyOf(item)) as PropertyKey; + return getKeyHash(this.dataSourceController.keyOf(item)) as PropertyKey; } public init(): void { this.columnsController = this.getController('columns'); this.dataController = this.getController('data'); + this.dataSourceController = this.getController('dataSource'); this.errorHandlingController = this.getController('errorHandling'); this.aiColumnCacheController = new AIColumnCacheController(this.component); @@ -136,7 +140,7 @@ export class AIColumnIntegrationController extends Controller { return; } - const keyField = this.dataController.key(); + const keyField = this.dataSourceController.key(); if (keyField === undefined || isKeyMissingInData(args.data, keyField)) { this.dataController.fireError('E1046', keyField); return; diff --git a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/__tests__/columns_controller.update_filter.test.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/__tests__/columns_controller.update_filter.test.ts new file mode 100644 index 000000000000..2f0122ced85b --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/__tests__/columns_controller.update_filter.test.ts @@ -0,0 +1,130 @@ +import { + afterEach, + beforeEach, + describe, + expect, + it, +} from '@jest/globals'; +import type { dxElementWrapper } from '@js/core/renderer'; +import { extend } from '@js/core/utils/extend'; +import type { DataGridModel } from '@ts/grids/data_grid/__tests__/__mock__/model/data_grid'; +import type { DataGridInstance } from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils'; +import { + afterTest, + beforeTest, + createDataGrid, +} from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils'; + +const DATA = [ + { id: 1, name: 'Alex', age: 15 }, + { id: 2, name: 'Dan', age: 20 }, +]; + +interface TaggedSelector { + columnIndex?: number; + filterValue?: unknown; + selectedFilterOperation?: unknown; +} + +const createGrid = (): Promise<{ + $container: dxElementWrapper; + component: DataGridModel; + instance: DataGridInstance; +}> => createDataGrid({ + dataSource: DATA, + columns: ['name', 'age'], +}); + +const updateFilter = ( + instance: DataGridInstance, + filter: unknown, + remoteFiltering: boolean, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): any => instance.getController('columns').updateFilter(filter, remoteFiltering); + +describe('ColumnsController.updateFilter', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + describe('when filtering locally', () => { + it('should replace the data field with the column selector itself', async () => { + const { instance } = await createGrid(); + const nameColumn = instance.getController('columns').getVisibleColumns()[0]; + + const result = updateFilter(instance, ['name', '=', 'Alex'], false); + + expect(result[0]).toBe(nameColumn.selector); + }); + + it('should tag the column selector with its column index', async () => { + const { instance } = await createGrid(); + const columns = instance.getController('columns').getVisibleColumns(); + + updateFilter(instance, ['age', '=', 15], false); + + expect((columns[1].selector as TaggedSelector).columnIndex).toBe(columns[1].index); + }); + + it('should walk nested groups', async () => { + const { instance } = await createGrid(); + const columns = instance.getController('columns').getVisibleColumns(); + + const result = updateFilter( + instance, + [['name', '=', 'Alex'], 'and', ['age', '=', 15]], + false, + ); + + expect(result[0][0]).toBe(columns[0].selector); + expect(result[1]).toBe('and'); + expect(result[2][0]).toBe(columns[1].selector); + }); + + it('should leave an unknown field alone', async () => { + const { instance } = await createGrid(); + + expect(updateFilter(instance, ['unknown', '=', 1], false)).toEqual(['unknown', '=', 1]); + }); + }); + + describe('when filtering remotely', () => { + it('should keep the data fields', async () => { + const { instance } = await createGrid(); + + expect(updateFilter(instance, ['name', '=', 'Alex'], true)).toEqual(['name', '=', 'Alex']); + }); + }); + + describe('when the filter array carries its own properties', () => { + it('should carry them over to the result', async () => { + const { instance } = await createGrid(); + const filter = extend([], [['name', '=', 'Alex'], 'and', ['age', '=', 15]]); + + filter.columnIndex = 7; + filter.filterValue = 'ZZ'; + filter.selectedFilterOperation = 'between'; + + const result = updateFilter(instance, filter, false); + + expect(result.columnIndex).toBe(7); + expect(result.filterValue).toBe('ZZ'); + expect(result.selectedFilterOperation).toBe('between'); + }); + + it('should pass columnIndex and filterValue down but not selectedFilterOperation', async () => { + const { instance } = await createGrid(); + const customSelector = (): number => 1; + const filter = extend([], [[customSelector, '=', 'Alex']]); + + filter.columnIndex = 3; + filter.filterValue = 'inherited'; + filter.selectedFilterOperation = 'between'; + + updateFilter(instance, filter, false); + + expect((customSelector as TaggedSelector).columnIndex).toBe(3); + expect((customSelector as TaggedSelector).filterValue).toBe('inherited'); + expect((customSelector as TaggedSelector).selectedFilterOperation).toBeUndefined(); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.ts index aeab55d20f08..859b58d185bc 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.ts @@ -1823,7 +1823,6 @@ export class ColumnsController extends modules.Controller { } if (isPlainObject(dataSource) || (dataSource instanceof Store) || Array.isArray(dataSource)) { if (that.valueExpr) { - // @ts-expect-error const dataSourceOptions = normalizeDataSourceOptions(dataSource); dataSourceOptions.paginate = false; dataSource = new DataSource(dataSourceOptions); diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_controller/__tests__/data_controller.combined_filter.test.ts b/packages/devextreme/js/__internal/grids/grid_core/data_controller/__tests__/data_controller.combined_filter.test.ts new file mode 100644 index 000000000000..82ab7a5ba0ce --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/data_controller/__tests__/data_controller.combined_filter.test.ts @@ -0,0 +1,375 @@ +import { + afterEach, + beforeEach, + describe, + expect, + it, +} from '@jest/globals'; +import type { dxElementWrapper } from '@js/core/renderer'; +import type { Properties as DataGridProperties } from '@js/ui/data_grid'; +import type { DataGridModel } from '@ts/grids/data_grid/__tests__/__mock__/model/data_grid'; +import type { DataGridInstance } from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils'; +import { + afterTest, + beforeTest, + createDataGrid, + toPlainFilter, +} from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils'; +import type { Column } from '@ts/grids/grid_core/columns_controller/types'; + +type ColumnWithSelector = Column & { selector?: unknown }; + +const DATA = [ + { + id: 1, name: 'Alex', age: 15, city: 'Berlin', + }, + { + id: 2, name: 'Dan', age: 20, city: 'Munich', + }, +]; + +const COLUMNS_WITH_ROW_AND_HEADER_FILTERS: DataGridProperties['columns'] = [ + { dataField: 'name', filterValue: 'Alex' }, + { dataField: 'age', filterValues: [15] }, + 'city', +]; + +const createGrid = (options: DataGridProperties): Promise<{ + $container: dxElementWrapper; + component: DataGridModel; + instance: DataGridInstance; +}> => createDataGrid({ dataSource: DATA, ...options }); + +const getDataFieldFilter = ( + instance: DataGridInstance, +): unknown => toPlainFilter(instance.getCombinedFilter(true)); + +const getVisibleColumns = (instance: DataGridInstance): ColumnWithSelector[] => instance + .getController('columns') + .getVisibleColumns() as ColumnWithSelector[]; + +const getFilterWithoutColumn = ( + instance: DataGridInstance, + column: Column, +): unknown => toPlainFilter( + instance.getController('data').getCombinedFilterWithExcludedColumn(column, true), +); + +describe('DataController combined filter', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + describe('when only the filter row has values', () => { + it('should take the value of a single column', async () => { + const { instance } = await createGrid({ + columns: [{ dataField: 'name', filterValue: 'Alex' }, 'age'], + }); + + expect(getDataFieldFilter(instance)).toEqual(['name', 'contains', 'Alex']); + }); + + it('should combine the values of several columns flatly', async () => { + const { instance } = await createGrid({ + columns: [ + { dataField: 'name', filterValue: 'Alex' }, + { dataField: 'age', filterValue: 15 }, + ], + }); + + expect(getDataFieldFilter(instance)).toEqual([ + ['name', 'contains', 'Alex'], 'and', ['age', '=', 15], + ]); + }); + }); + + describe('when only the header filter has values', () => { + it('should take a single value', async () => { + const { instance } = await createGrid({ + columns: [{ dataField: 'age', filterValues: [15] }, 'name'], + }); + + expect(getDataFieldFilter(instance)).toEqual(['age', '=', 15]); + }); + + it('should combine several values with or', async () => { + const { instance } = await createGrid({ + columns: [{ dataField: 'age', filterValues: [15, 20] }, 'name'], + }); + + expect(getDataFieldFilter(instance)).toEqual([ + ['age', '=', 15], 'or', ['age', '=', 20], + ]); + }); + + it('should negate the group for the exclude filter type', async () => { + const { instance } = await createGrid({ + columns: [{ dataField: 'age', filterValues: [15], filterType: 'exclude' }, 'name'], + }); + + expect(getDataFieldFilter(instance)).toEqual(['!', ['age', '=', 15]]); + }); + }); + + describe('when only the search panel has text', () => { + it('should match every searchable column with or', async () => { + const { instance } = await createGrid({ + searchPanel: { text: 'Al' }, + columns: ['name', 'city'], + }); + + expect(getDataFieldFilter(instance)).toEqual([ + ['name', 'contains', 'Al'], 'or', ['city', 'contains', 'Al'], + ]); + }); + + it('should skip hidden columns while searchVisibleColumnsOnly is on', async () => { + const { instance } = await createGrid({ + searchPanel: { text: 'Al', searchVisibleColumnsOnly: true }, + columns: [{ dataField: 'name', visible: false }, 'city'], + }); + + expect(getDataFieldFilter(instance)).toEqual(['city', 'contains', 'Al']); + }); + }); + + describe('when only the filterValue option is set', () => { + it('should take the option as is', async () => { + const { instance } = await createGrid({ + filterValue: ['city', '=', 'Berlin'], + columns: ['name', 'age', 'city'], + }); + + expect(getDataFieldFilter(instance)).toEqual(['city', '=', 'Berlin']); + }); + }); + + describe('when no source contributes', () => { + it('should produce no filter', async () => { + const { instance } = await createGrid({ columns: ['name', 'age'] }); + + expect(getDataFieldFilter(instance)).toBeUndefined(); + }); + }); + + describe('when all four sources contribute', () => { + // The chain nests to the left: every layer passes the result of `super` as the first + // element of its own array. A flat combination carries the same meaning but a different + // structure, and the QUnit suites compare structures whole. + it('should nest them left to right in the source order', async () => { + const { instance } = await createGrid({ + filterSyncEnabled: false, + filterValue: ['city', '=', 'Berlin'], + searchPanel: { text: 'Al' }, + columns: [ + { dataField: 'name', filterValue: 'A', selectedFilterOperation: 'contains' }, + { dataField: 'age', filterValues: [15, 20] }, + 'city', + ], + }); + + const filterRow = ['name', 'contains', 'A']; + const headerFilter = [['age', '=', 15], 'or', ['age', '=', 20]]; + const search = [['name', 'contains', 'Al'], 'or', ['city', 'contains', 'Al']]; + const filterValue = ['city', '=', 'Berlin']; + + expect(getDataFieldFilter(instance)).toEqual([ + [[filterRow, 'and', headerFilter], 'and', search], 'and', filterValue, + ]); + }); + + it('should keep the source order when the middle sources are empty', async () => { + const { instance } = await createGrid({ + filterSyncEnabled: false, + filterValue: ['city', '=', 'Berlin'], + columns: [{ dataField: 'name', filterValue: 'Alex' }, 'age', 'city'], + }); + + expect(getDataFieldFilter(instance)).toEqual([ + ['name', 'contains', 'Alex'], 'and', ['city', '=', 'Berlin'], + ]); + }); + }); + + describe('when filter sync is off', () => { + it('should keep the column filters next to filterValue', async () => { + const { instance } = await createGrid({ + filterSyncEnabled: false, + filterValue: ['city', '=', 'Berlin'], + columns: COLUMNS_WITH_ROW_AND_HEADER_FILTERS, + }); + + expect(getDataFieldFilter(instance)).toEqual([ + [['name', 'contains', 'Alex'], 'and', ['age', '=', 15]], 'and', ['city', '=', 'Berlin'], + ]); + }); + }); + + describe('when filter sync is on', () => { + it('should drop the column filters while filterValue is set', async () => { + const { instance } = await createGrid({ + filterSyncEnabled: true, + filterValue: ['city', '=', 'Berlin'], + columns: COLUMNS_WITH_ROW_AND_HEADER_FILTERS, + }); + + expect(getDataFieldFilter(instance)).toEqual(['city', '=', 'Berlin']); + }); + + it('should keep the column filters while filterValue is not set', async () => { + const { instance } = await createGrid({ + filterSyncEnabled: true, + columns: COLUMNS_WITH_ROW_AND_HEADER_FILTERS, + }); + + expect(getDataFieldFilter(instance)).toEqual([ + ['name', 'contains', 'Alex'], 'and', ['age', '=', 15], + ]); + }); + }); + + describe('when filterSyncEnabled is auto', () => { + it('should keep the column filters while the filter panel is hidden', async () => { + const { instance } = await createGrid({ + filterValue: ['city', '=', 'Berlin'], + columns: COLUMNS_WITH_ROW_AND_HEADER_FILTERS, + }); + + expect(getDataFieldFilter(instance)).toEqual([ + [['name', 'contains', 'Alex'], 'and', ['age', '=', 15]], 'and', ['city', '=', 'Berlin'], + ]); + }); + + it('should drop the column filters while the filter panel is visible', async () => { + const { instance } = await createGrid({ + filterPanel: { visible: true }, + filterValue: ['city', '=', 'Berlin'], + columns: COLUMNS_WITH_ROW_AND_HEADER_FILTERS, + }); + + expect(getDataFieldFilter(instance)).toEqual(['city', '=', 'Berlin']); + }); + }); + + describe('when filterPanel.filterEnabled is false', () => { + it('should drop filterValue and keep the column filters', async () => { + const { instance } = await createGrid({ + filterSyncEnabled: false, + filterPanel: { visible: true, filterEnabled: false }, + filterValue: ['city', '=', 'Berlin'], + columns: COLUMNS_WITH_ROW_AND_HEADER_FILTERS, + }); + + expect(getDataFieldFilter(instance)).toEqual([ + ['name', 'contains', 'Alex'], 'and', ['age', '=', 15], + ]); + }); + }); + + describe('when no column allows filtering', () => { + it('should skip filterValue', async () => { + const { instance } = await createGrid({ + filterSyncEnabled: false, + filterValue: ['name', '=', 'Alex'], + columns: [ + { dataField: 'name', allowFiltering: false, allowHeaderFiltering: false }, + { dataField: 'age', allowFiltering: false, allowHeaderFiltering: false }, + ], + }); + + expect(instance.getController('columns').getFilteringColumns()).toHaveLength(0); + expect(getDataFieldFilter(instance)).toBeUndefined(); + }); + }); + + describe('when a column is excluded', () => { + it('should drop its filter row condition', async () => { + const { instance } = await createGrid({ + columns: COLUMNS_WITH_ROW_AND_HEADER_FILTERS, + }); + + const filter = getFilterWithoutColumn(instance, getVisibleColumns(instance)[0]); + + expect(filter).toEqual(['age', '=', 15]); + }); + + it('should drop its header filter condition', async () => { + const { instance } = await createGrid({ + columns: COLUMNS_WITH_ROW_AND_HEADER_FILTERS, + }); + + const filter = getFilterWithoutColumn(instance, getVisibleColumns(instance)[1]); + + expect(filter).toEqual(['name', 'contains', 'Alex']); + }); + + it('should change nothing when the column has no conditions', async () => { + const { instance } = await createGrid({ + columns: COLUMNS_WITH_ROW_AND_HEADER_FILTERS, + }); + + const filter = getFilterWithoutColumn(instance, getVisibleColumns(instance)[2]); + + expect(filter).toEqual([ + ['name', 'contains', 'Alex'], 'and', ['age', '=', 15], + ]); + }); + + it('should drop its filterValue conditions while filter sync is on', async () => { + const { instance } = await createGrid({ + filterPanel: { visible: true }, + filterSyncEnabled: true, + filterValue: [['city', '=', 'Berlin'], 'and', ['name', '=', 'Alex']], + columns: ['name', 'age', 'city'], + }); + const columns = getVisibleColumns(instance); + + expect(getFilterWithoutColumn(instance, columns[2])).toEqual(['name', '=', 'Alex']); + expect(getFilterWithoutColumn(instance, columns[0])).toEqual(['city', '=', 'Berlin']); + }); + + it('should not remember the excluded column after the call', async () => { + const { instance } = await createGrid({ + columns: COLUMNS_WITH_ROW_AND_HEADER_FILTERS, + }); + + getFilterWithoutColumn(instance, getVisibleColumns(instance)[0]); + + expect(getDataFieldFilter(instance)).toEqual([ + ['name', 'contains', 'Alex'], 'and', ['age', '=', 15], + ]); + }); + }); + + describe('when returnDataField is off', () => { + it('should return the column selector instead of the data field', async () => { + const { instance } = await createGrid({ + columns: [{ dataField: 'name', filterValue: 'Alex' }, 'age'], + }); + + const filter = instance.getCombinedFilter() as unknown as unknown[]; + + expect(typeof filter[0]).toBe('function'); + expect(filter[0]).toBe(getVisibleColumns(instance)[0].selector); + }); + }); + + describe('when the search text fits no column', () => { + it('should produce a match-nothing filter on its own', async () => { + const { instance } = await createGrid({ + searchPanel: { text: 'Al' }, + columns: [{ dataField: 'age', dataType: 'number' }], + }); + + expect(getDataFieldFilter(instance)).toEqual(['!']); + }); + + it('should collapse the other sources too', async () => { + const { instance } = await createGrid({ + searchPanel: { text: 'Al' }, + columns: [{ dataField: 'age', dataType: 'number', filterValue: 15 }], + }); + + expect(getDataFieldFilter(instance)).toEqual(['!']); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_controller/__tests__/data_controller.data_source.test.ts b/packages/devextreme/js/__internal/grids/grid_core/data_controller/__tests__/data_controller.data_source.test.ts deleted file mode 100644 index 0d64fec2e747..000000000000 --- a/packages/devextreme/js/__internal/grids/grid_core/data_controller/__tests__/data_controller.data_source.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - afterEach, - beforeEach, - describe, - expect, - it, -} from '@jest/globals'; -import DataSource from '@js/data/data_source'; -import type { DataGridInstance } from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils'; -import { - afterTest, - beforeTest, - createDataGrid, - flushAsync, -} from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils'; - -import { DataController } from '../data_controller'; - -declare class ExposedDataController extends DataController { - public isSharedDataSource?: boolean; -} - -const DATA = [ - { id: 1, value: 'a' }, - { id: 2, value: 'b' }, -]; - -const getIsSharedDataSource = (instance: DataGridInstance): boolean | undefined => { - const dataController = instance.getController('data') as unknown as ExposedDataController; - - return dataController.isSharedDataSource; -}; - -describe('DataController data source', () => { - beforeEach(beforeTest); - afterEach(afterTest); - - describe('isSharedDataSource', () => { - it('should be true when a DataSource instance is passed', async () => { - const sharedDataSource = new DataSource({ store: DATA, key: 'id' }); - - const { instance } = await createDataGrid({ dataSource: sharedDataSource }); - - expect(getIsSharedDataSource(instance)).toBe(true); - }); - - it('should be false when a plain array is passed', async () => { - const { instance } = await createDataGrid({ dataSource: DATA }); - - expect(getIsSharedDataSource(instance)).toBe(false); - }); - - it('should reset to false after switching from a shared DataSource to a plain array', async () => { - const sharedDataSource = new DataSource({ store: DATA, key: 'id' }); - - const { instance } = await createDataGrid({ dataSource: sharedDataSource }); - expect(getIsSharedDataSource(instance)).toBe(true); - - instance.option('dataSource', DATA); - await flushAsync(); - - expect(getIsSharedDataSource(instance)).toBe(false); - }); - }); -}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_controller/__tests__/data_controller.paging.test.ts b/packages/devextreme/js/__internal/grids/grid_core/data_controller/__tests__/data_controller.paging.test.ts index 1294df3428d5..d2550e54c791 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/data_controller/__tests__/data_controller.paging.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/data_controller/__tests__/data_controller.paging.test.ts @@ -38,13 +38,9 @@ const getIsPaging = (instance: DataGridInstance): unknown => { return dataController._isPaging as unknown; }; -// The controller's `dataSource()` is the DataSourceAdapter, not the raw DataSource. -const getAdapter = (instance: DataGridInstance): PagingAdapter => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const dataController = instance.getController('data') as any; - - return dataController.dataSource() as PagingAdapter; -}; +const getAdapter = (instance: DataGridInstance): PagingAdapter => instance + .getController('dataSource') + .getAdapter() as unknown as PagingAdapter; describe('DataController paging', () => { beforeEach(beforeTest); diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_controller/data_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/data_controller/data_controller.ts index 5b58e6902297..4991d4ff46d7 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/data_controller/data_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/data_controller/data_controller.ts @@ -1,23 +1,20 @@ -import { DataSource as DataSourceClass } from '@js/common/data/data_source/data_source'; -import { normalizeDataSourceOptions } from '@js/common/data/data_source/utils'; import type { Callback } from '@js/core/utils/callbacks'; import { deferRender } from '@js/core/utils/common'; import { logger } from '@js/core/utils/console'; import type { DeferredObj } from '@js/core/utils/deferred'; import { Deferred, when } from '@js/core/utils/deferred'; -import { extend } from '@js/core/utils/extend'; import { isDefined } from '@js/core/utils/type'; import type { StoreChange } from '@js/data/store'; import errors from '@js/ui/widget/ui.errors'; import { findChanges } from '@ts/core/utils/m_array_compare'; import { fromPromise } from '@ts/core/utils/m_deferred'; -import type Store from '@ts/data/abstract_store'; import type { DataSource } from '@ts/data/data_source/data_source'; import type { ChangingEvent } from '@ts/data/data_source/types'; import type { Column, ColumnsChanges } from '@ts/grids/grid_core/columns_controller/types'; +import type { DataSourceController } from '@ts/grids/grid_core/data_source/data_source_controller'; import type DataSourceAdapter from '@ts/grids/grid_core/data_source_adapter/m_data_source_adapter'; import type { - ChangedEvent, DataSourceAdapterProvider, LoadOperation, OperationTypes, RawItemData, + ChangedEvent, LoadOperation, OperationTypes, RawItemData, } from '@ts/grids/grid_core/data_source_adapter/types'; import { isLocalStore } from '@ts/grids/grid_core/data_source_adapter/utils/store'; import modules from '@ts/grids/grid_core/m_modules'; @@ -38,6 +35,7 @@ import type { ItemChangeOptions, ItemOperationOptions, ItemProcessingOptions, + LoadAllItemsDeferred, PagingChanges, PagingDataSource, PagingOptionName, @@ -50,15 +48,13 @@ import type { UpdateItemChange, UserState, } from './types'; -import { resolvePaginate, syncPaging } from './utils/paging'; +import { syncPaging } from './utils/paging'; import { getRefreshOptions } from './utils/refresh'; import { attachChangedItems, - canDiffColumns, convertToUpdateChange, + countRowsBefore, getChangedRowIndices, - getDataRowIndex, - getGroupColumnIndices, getItemChange, getRowKey, indexRowsByKey, @@ -70,9 +66,7 @@ import { import { generateRowValues } from './utils/row_values'; export class DataController extends modules.Controller { - public _dataSource?: DataSourceAdapter | null; - - protected isSharedDataSource?: boolean; + protected _dataSource?: DataSourceAdapter | null; protected _items!: ProcessedItem[]; @@ -126,10 +120,12 @@ export class DataController extends modules.Controller { public rowIndicesChanged!: Callback<[RowIndexCorrection]>; + protected dataSourceController!: DataSourceController; + // TODO public controller public _columnsController!: Controllers['columns']; - private _filterExcludedColumn: Column | null = null; + protected filterController!: Controllers['filter']; private loadErrorHandlerProxy!: (e: Error | string) => void; @@ -140,7 +136,9 @@ export class DataController extends modules.Controller { public init(): void { this._items = []; this._cachedProcessedItems = null; + this.dataSourceController = this.getController('dataSource'); this._columnsController = this.getController('columns'); + this.filterController = this.getController('filter'); this._isPaging = false; this._currentOperationTypes = null; @@ -162,18 +160,6 @@ export class DataController extends modules.Controller { this.resetDataSource(); } - /** - * TODO: Define this method only in masterDetail. - * Remove the override from adaptive behavior - * and move the implementation to masterDetail. - * - * @extended: adaptivity, master_detail - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected getRowIndicesForExpand(key: RowKey): number[] { - return []; - } - /** * @extended: virtual_scrolling */ @@ -203,11 +189,9 @@ export class DataController extends modules.Controller { 'endCustomLoading', 'filter', 'getCombinedFilter', - 'getDataSource', 'getKeyByRowIndex', 'getRowIndexByKey', 'getVisibleRows', - 'keyOf', 'pageCount', 'pageIndex', 'pageSize', @@ -237,7 +221,7 @@ export class DataController extends modules.Controller { )) { const isValueChanged = args.value !== args.previousValue; if (isValueChanged) { - const store = this.store(); + const store = this.dataSourceController.store(); if (isLocalStore(store)) { store._array = args.value; } @@ -279,21 +263,20 @@ export class DataController extends modules.Controller { case 'remoteOperations': case 'keyExpr': case 'dataSource': - case 'scrolling': args.handled = true; this.reset(); break; case 'paging': { - const dataSource = this.dataSource(); + const dataSourceAdapter = this.dataSourceController.getAdapter(); - if (dataSource) { - const changedPagingOptions = this.applyPagingOptions(dataSource); + if (dataSourceAdapter) { + const changedPagingOptions = this.applyPagingOptions(dataSourceAdapter); if (changedPagingOptions.hasChanges) { - const pageIndex = dataSource.pageIndex(); + const pageIndex = dataSourceAdapter.pageIndex(); this._isPaging = changedPagingOptions.isPageIndexChanged; - dataSource.load().done(() => { + dataSourceAdapter.load().done(() => { this._isPaging = false; this.pageChanged.fire(pageIndex); }); @@ -306,11 +289,11 @@ export class DataController extends modules.Controller { this.reset(); break; case 'columns': { - const dataSource = this.dataSource(); + const dataSourceAdapter = this.dataSourceController.getAdapter(); - if (dataSource?.isLoading() && args.name === args.fullName) { + if (dataSourceAdapter?.isLoading() && args.name === args.fullName) { this._useSortingGroupingFromColumns = true; - dataSource.load(); + dataSourceAdapter.load(); } break; } @@ -323,31 +306,22 @@ export class DataController extends modules.Controller { return !this._isLoading; } - public getDataSource(): DataSource | null { - return this._dataSource?._dataSource ?? null; - } - public getCombinedFilter(returnDataField?: boolean): DataFilter { return this.combinedFilter(undefined, returnDataField); } - public getFilterExcludedColumn(): Column | null { - return this._filterExcludedColumn; - } - public getCombinedFilterWithExcludedColumn( excludedColumn: Column | null, returnDataField?: boolean, ): DataFilter { - this._filterExcludedColumn = excludedColumn; - try { - return this.getCombinedFilter(returnDataField); - } finally { - this._filterExcludedColumn = null; - } + return this.combinedFilter(undefined, returnDataField, excludedColumn); } - private combinedFilter(filter: DataFilter, returnDataField?: boolean): DataFilter { + private combinedFilter( + filter: DataFilter, + returnDataField?: boolean, + excludedColumn: Column | null = null, + ): DataFilter { if (!this._dataSource) { return filter; } @@ -358,7 +332,7 @@ export class DataController extends modules.Controller { || this._columnsController.isAllDataTypesDefined(); if (isColumnsTypesDefined) { - const additionalFilter = this.calculateAdditionalFilter(); + const additionalFilter = this.filterController.getAdditionalFilter(excludedColumn); combined = additionalFilter ? gridCoreUtils.combineFilters([additionalFilter, combined]) @@ -572,7 +546,7 @@ export class DataController extends modules.Controller { this._isDataSourceApplying = false; const hasAdditionalFilter = (): boolean => { - const additionalFilter = this.calculateAdditionalFilter(); + const additionalFilter = this.filterController.getAdditionalFilter(); return Boolean(additionalFilter?.length); }; @@ -630,33 +604,29 @@ export class DataController extends modules.Controller { } private applyPagingOptions(dataSource: PagingDataSource): PagingChanges { - const { scrolling, paging } = this.option(); + const { paging } = this.option(); - // Not paging state to reconcile, but a per-load request flag: infinite - // scrolling detects the last page locally and needs no grand total. - dataSource.requireTotalCount(scrolling?.mode !== 'infinite'); + dataSource.requireTotalCount(this.requiresTotalCount()); return syncPaging(dataSource, { - paginate: resolvePaginate(paging?.enabled, scrolling?.mode), + paginate: this.resolvePaginate(paging?.enabled), pageSize: paging?.pageSize, pageIndex: paging?.pageIndex, }); } - protected _getSpecificDataSourceOption(): unknown { - const dataSource = this.option('dataSource'); - - if (Array.isArray(dataSource)) { - return { - store: { - type: 'array', - data: dataSource, - key: this.option('keyExpr'), - }, - }; - } + /** + * @extended: virtual_scrolling + */ + protected resolvePaginate(enabled: boolean | undefined): boolean | undefined { + return enabled; + } - return dataSource; + /** + * @extended: virtual_scrolling + */ + protected requiresTotalCount(): boolean { + return true; } /** @@ -672,7 +642,9 @@ export class DataController extends modules.Controller { protected _initDataSource(): void { const hadDataSource = !!this._dataSource; - const dataSource = this.recreateDataSource(); + this._disposeDataSource(); + + const dataSource = this.dataSourceController.createDataSource(); this._useSortingGroupingFromColumns = true; this._cachedProcessedItems = null; @@ -686,27 +658,6 @@ export class DataController extends modules.Controller { } } - private recreateDataSource(): DataSource | undefined { - const dataSourceOptions = this._getSpecificDataSourceOption(); - - this._disposeDataSource(); - - if (!dataSourceOptions) { - this.isSharedDataSource = false; - return undefined; - } - - if (dataSourceOptions instanceof DataSourceClass) { - this.isSharedDataSource = true; - return dataSourceOptions as unknown as DataSource; - } - - this.isSharedDataSource = false; - return new DataSourceClass( - extend(true, {}, normalizeDataSourceOptions(dataSourceOptions, {})), - ) as unknown as DataSource; - } - /** * @extended: selection, virtual_scrolling */ @@ -749,6 +700,16 @@ export class DataController extends modules.Controller { // eslint-disable-next-line @typescript-eslint/no-unused-vars protected getDataIndex(change: DataChange): number { return 0; } + /** + * A store change is indexed by data rows, while an insert index coming from the grid counts + * every visible row. Each module that puts its own countable rows into the stream adds their + * count on top of this one. + * @extended: grouping (DataGrid) + */ + protected adjustInsertRowIndex(visibleRowIndex: number): number { + return countRowsBefore(this.getVisibleRows(), visibleRowIndex, 'data'); + } + /** * @extended: adaptivity, editing, master_detail, virtual_scrolling */ @@ -797,7 +758,7 @@ export class DataController extends modules.Controller { return { rowType: 'data', data, - key: this.keyOf(data), + key: this.dataSourceController.keyOf(data), }; } @@ -956,19 +917,12 @@ export class DataController extends modules.Controller { const oldValue = oldRow.values[columnIndex]; const newValue = newRow.values[columnIndex]; - if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) { - return true; - } - - const isCellModified = ( - row: ProcessedItem, - ): boolean => row.modifiedValues?.[columnIndex] !== undefined; - - return isCellModified(oldRow) !== isCellModified(newRow); + return JSON.stringify(oldValue) !== JSON.stringify(newValue); } /** - * @extended: editing_row_based, editing, editing_form_based + * @extended: editing_row_based, editing, editing_form_based, grouping (DataGrid), + * summary (DataGrid) */ protected getChangedColumnIndices( oldItem: ProcessedItem, @@ -976,18 +930,17 @@ export class DataController extends modules.Controller { visibleRowIndex: number, isLiveUpdate?: boolean, ): number[] | undefined { - if (!canDiffColumns(oldItem, newItem)) { + if (oldItem.rowType !== newItem.rowType) { return undefined; } - - switch (newItem.rowType) { - case 'group': - return getGroupColumnIndices(oldItem, newItem); - case 'detail': - return []; - default: - return this.getChangedColumnIndicesCore(oldItem, newItem, visibleRowIndex, isLiveUpdate); + // The detail type is not owned by a single module: Master-Detail creates these rows, + // while form-based editing reuses the same type for its edit row. Therefore, the check + // remains in the base module for now. + if (newItem.rowType === 'detail') { + return []; } + + return this.getChangedColumnIndicesCore(oldItem, newItem, visibleRowIndex, isLiveUpdate); } private getChangedColumnIndicesCore( @@ -1150,16 +1103,13 @@ export class DataController extends modules.Controller { } private readonly changingHandler = (e: ChangingEvent): void => { - const rows = this.getVisibleRows(); - const dataSource = this.dataSource(); - - if (!dataSource) { + if (!this.dataSourceController.hasAdapter()) { return; } e.changes.forEach((change) => { if (change.type === 'insert' && change.index !== undefined && change.index >= 0) { - change.index = getDataRowIndex(rows, change.index); + change.index = this.adjustInsertRowIndex(change.index); } }); }; @@ -1202,7 +1152,7 @@ export class DataController extends modules.Controller { return; } - const operationTypes = this.dataSource()?.operationTypes() ?? undefined; + const operationTypes = this.dataSourceController.operationTypes() ?? undefined; change.isDataChanged = true; change.repaintChangesOnly = resolveRepaintChangesOnly( @@ -1224,13 +1174,6 @@ export class DataController extends modules.Controller { ); } - public loadingOperationTypes(): OperationTypes { - const dataSource = this.dataSource(); - const operationTypes: OperationTypes | undefined = dataSource?.loadingOperationTypes(); - - return operationTypes ?? {}; - } - /** * @extended: virtual_scrolling, focus */ @@ -1252,13 +1195,6 @@ export class DataController extends modules.Controller { this.loadingChanged.fire(this.isLoading(), this._loadingText); } - /** - * @extended: filter_row, filter_sync, header_filter, search - */ - protected calculateAdditionalFilter(): DataFilter { - return null; - } - /** * @extended: filter_sync, virtual_scrolling */ @@ -1359,18 +1295,6 @@ export class DataController extends modules.Controller { this.dataSourceChanged.fire(); }; - protected _getDataSourceAdapterProvider(): DataSourceAdapterProvider { - throw new Error('Method not implemented.'); - } - - protected _createDataSourceAdapter(dataSource: DataSource): DataSourceAdapter { - const dataSourceAdapterProvider = this._getDataSourceAdapterProvider(); - const dataSourceAdapter = dataSourceAdapterProvider.create(this.component); - - dataSourceAdapter.init(dataSource); - return dataSourceAdapter; - } - private subscribeToDataSource(dataSourceAdapter: DataSourceAdapter): void { dataSourceAdapter.changed.add(this.dataChangedHandlerProxy); dataSourceAdapter.loadingChanged.add(this.loadingChangedHandler); @@ -1389,29 +1313,17 @@ export class DataController extends modules.Controller { dataSourceAdapter.pushed.remove(this.dataPushedHandlerProxy); } - private setDataSource(dataSource: DataSource | null): void { - const oldDataSource = this._dataSource; - - if (!dataSource && oldDataSource) { - oldDataSource.cancelAll(); - this.unsubscribeFromDataSource(oldDataSource); - oldDataSource.dispose(this.isSharedDataSource); - } - - const dataSourceAdapter = dataSource - ? this._createDataSourceAdapter(dataSource) - : null; + private setDataSource(dataSource: DataSource): void { + const dataSourceAdapter = this.dataSourceController.createAdapter(dataSource); this._dataSource = dataSourceAdapter; - if (dataSourceAdapter) { - this._isLoading = !dataSourceAdapter.isLoaded(); - this._needApplyFilter = true; - this._isAllDataTypesDefined = this._columnsController.isAllDataTypesDefined(); + this._isLoading = !dataSourceAdapter.isLoaded(); + this._needApplyFilter = true; + this._isAllDataTypesDefined = this._columnsController.isAllDataTypesDefined(); - this.changed.add(this.fireDataSourceChanged); - this.subscribeToDataSource(dataSourceAdapter); - } + this.changed.add(this.fireDataSourceChanged); + this.subscribeToDataSource(dataSourceAdapter); } /** @@ -1433,19 +1345,11 @@ export class DataController extends modules.Controller { return this._dataSource ? this._dataSource.pageCount() : 1; } - public dataSource(): DataSourceAdapter | undefined { - return this._dataSource ?? undefined; - } - - public store(): Store | undefined { - return this._dataSource?.store(); - } - public loadAllItems( data?: RawItemData[], skipFilter = false, - ): DeferredObj<ProcessedItem[]> { - const d = Deferred<ProcessedItem[]>(); + ): LoadAllItemsDeferred { + const d = Deferred<ProcessedItem[]>() as LoadAllItemsDeferred; const dataSource = this._dataSource; if (!dataSource) { @@ -1453,14 +1357,8 @@ export class DataController extends modules.Controller { return d; } - const resolveWithProcessedItems = (loadResult: CustomLoadResult): void => { - const items = this._processItems( - this._beforeProcessItems(loadResult.data), - { changeType: 'loadingAll' }, - ); - - // @ts-expect-error DataGrid-only summary leaks into grid_core - d.resolve(items, loadResult.extra?.summary); + const resolveLoaded = (loadResult: CustomLoadResult): void => { + this.resolveLoadAllItems(d, loadResult); }; if (data) { @@ -1469,11 +1367,11 @@ export class DataController extends modules.Controller { group: dataSource.group(), sort: dataSource.sort(), }) - .done(resolveWithProcessedItems) + .done(resolveLoaded) .fail(d.reject as (...args: unknown[]) => void); } else if (!dataSource.isLoading()) { dataSource.customLoader.loadAll() - .done(resolveWithProcessedItems) + .done(resolveLoaded) .fail(d.reject as (...args: unknown[]) => void); } else { d.reject(); @@ -1482,6 +1380,23 @@ export class DataController extends modules.Controller { return d; } + protected processLoadAllItems(loadResult: CustomLoadResult): ProcessedItem[] { + return this._processItems( + this._beforeProcessItems(loadResult.data), + { changeType: 'loadingAll' }, + ); + } + + /** + * @extended: summary (DataGrid) + */ + protected resolveLoadAllItems( + d: LoadAllItemsDeferred, + loadResult: CustomLoadResult, + ): void { + d.resolve(this.processLoadAllItems(loadResult)); + } + public async getAllDataRowKeys(): Promise<RowKey[]> { const items = await Promise.resolve(this.loadAllItems()); @@ -1504,12 +1419,8 @@ export class DataController extends modules.Controller { return this.items()?.[this.getRowIndexByKey(key)]; } - public keyOf(data: RawItemData): RowKey | undefined { - return this.store()?.keyOf(data); - } - private byKey(key: RowKey): DeferredObj<RawItemData> { - const store = this.store(); + const store = this.dataSourceController.store(); if (!store) { return Deferred<RawItemData>().reject(); @@ -1524,10 +1435,6 @@ export class DataController extends modules.Controller { return fromPromise(store.byKey(key)) as DeferredObj<RawItemData>; } - public key(): string | string[] | undefined { - return this.store()?.key(); - } - /** * @extended: virtual_scrolling */ @@ -1614,7 +1521,7 @@ export class DataController extends modules.Controller { public refresh(options?: boolean | RefreshOptions): DeferredObj<unknown> { const refreshOptions = getRefreshOptions(options); - const dataSource = this.getDataSource(); + const dataSource = this.dataSourceController.getDataSource(); const { changesOnly } = refreshOptions; const d = Deferred(); @@ -1650,7 +1557,16 @@ export class DataController extends modules.Controller { } protected _disposeDataSource(): void { - this.setDataSource(null); + const oldDataSource = this._dataSource; + + if (oldDataSource) { + // Before unsubscribing: cancelling in-flight loads still notifies this controller. + oldDataSource.cancelAll(); + this.unsubscribeFromDataSource(oldDataSource); + } + + this._dataSource = null; + this.dataSourceController.disposeAdapter(); } public dispose(): void { @@ -1681,20 +1597,15 @@ export class DataController extends modules.Controller { } /** - * @extended: TreeList's state_storing + * @extended: search, TreeList's state_storing */ public getUserState(): UserState { return { - searchText: this.option('searchPanel.text'), pageIndex: this.pageIndex(), pageSize: this.pageSize(), }; } - public getCachedStoreData(): RawItemData[] | undefined { - return this._dataSource?.getCachedStoreData(); - } - /** * @extended: virtual_scrolling */ diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_controller/types.ts b/packages/devextreme/js/__internal/grids/grid_core/data_controller/types.ts index 4e2aeb8eba80..6f0aba6a1d85 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/data_controller/types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/data_controller/types.ts @@ -18,9 +18,9 @@ export interface RefreshOptions { } export interface UserState { - searchText: string | undefined; pageIndex: number; pageSize: number; + searchText?: string; expandedRowKeys?: unknown[]; } @@ -73,6 +73,13 @@ export interface ProcessedItem extends GeneratedItem { watch?: RowWatch; } +type LoadAllItemsCallback = (items: ProcessedItem[], totalAggregates?: unknown[]) => void; + +export type LoadAllItemsDeferred = Omit<DeferredObj<ProcessedItem[]>, 'done' | 'resolve'> & { + done: (callback: LoadAllItemsCallback) => LoadAllItemsDeferred; + resolve: LoadAllItemsCallback; +}; + /** changes */ export type RowChangeType = 'update' | 'insert' | 'remove'; diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_controller/utils/__tests__/paging.test.ts b/packages/devextreme/js/__internal/grids/grid_core/data_controller/utils/__tests__/paging.test.ts index 185dc4ae86a5..0a58ae260560 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/data_controller/utils/__tests__/paging.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/data_controller/utils/__tests__/paging.test.ts @@ -1,32 +1,7 @@ import { describe, expect, it } from '@jest/globals'; import type { PagingDataSource } from '../../types'; -import { resolvePaginate, syncPaging } from '../paging'; - -describe('resolvePaginate', () => { - it.each([ - { enabled: true, scrollingMode: 'standard', expected: true }, - { enabled: false, scrollingMode: 'standard', expected: false }, - // Virtual and infinite scrolling paginate even with paging disabled. - { enabled: false, scrollingMode: 'virtual', expected: true }, - { enabled: false, scrollingMode: 'infinite', expected: true }, - { enabled: true, scrollingMode: 'virtual', expected: true }, - { enabled: true, scrollingMode: 'infinite', expected: true }, - ])('should be $expected for enabled=$enabled, scrolling.mode=$scrollingMode', ({ - enabled, scrollingMode, expected, - }) => { - expect(resolvePaginate(enabled, scrollingMode)).toBe(expected); - }); - - // An undefined `paging.enabled` leaves the data source's paginate alone, - // even in a mode that would otherwise force it on. - it.each(['standard', 'virtual', 'infinite'])( - 'should be undefined when enabled is undefined in %s mode', - (scrollingMode) => { - expect(resolvePaginate(undefined, scrollingMode)).toBeUndefined(); - }, - ); -}); +import { syncPaging } from '../paging'; const createDataSourceMock = ( state: { paginate: boolean; pageSize: number; pageIndex: number }, diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_controller/utils/__tests__/row_changes.test.ts b/packages/devextreme/js/__internal/grids/grid_core/data_controller/utils/__tests__/row_changes.test.ts index 6a214c39d3a1..3f75575829b7 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/data_controller/utils/__tests__/row_changes.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/data_controller/utils/__tests__/row_changes.test.ts @@ -8,16 +8,13 @@ import type { } from '../../types'; import { attachChangedItems, - canDiffColumns, convertToUpdateChange, + countRowsBefore, getChangedRowIndices, - getDataRowIndex, - getGroupColumnIndices, getItemChange, getRowKey, getRowOperation, indexRowsByKey, - isSameGroupRowState, isSameItem, partialUpdateItem, resolveRepaintChangesOnly, @@ -96,84 +93,6 @@ describe('isSameItem', () => { }); }); -describe('isSameGroupRowState', () => { - const groupRow = (partial: Partial<ProcessedItem>): ProcessedItem => row({ - rowType: 'group', - isExpanded: true, - data: { isContinuation: false, isContinuationOnNextPage: false }, - ...partial, - }); - - it('should return true for the same state', () => { - expect(isSameGroupRowState(groupRow({}), groupRow({}))).toBe(true); - }); - - it('should compare the expanded state', () => { - expect(isSameGroupRowState(groupRow({}), groupRow({ isExpanded: false }))).toBe(false); - }); - - it('should compare the continuation flags', () => { - expect(isSameGroupRowState( - groupRow({}), - groupRow({ data: { isContinuation: true, isContinuationOnNextPage: false } }), - )).toBe(false); - - expect(isSameGroupRowState( - groupRow({}), - groupRow({ data: { isContinuation: false, isContinuationOnNextPage: true } }), - )).toBe(false); - }); - - it('should not compare the data beyond the continuation flags', () => { - expect(isSameGroupRowState( - groupRow({ data: { key: 1, isContinuation: false, isContinuationOnNextPage: false } }), - groupRow({ data: { key: 2, isContinuation: false, isContinuationOnNextPage: false } }), - )).toBe(true); - }); -}); - -describe('canDiffColumns', () => { - it('should allow the diff for the rows of the same type', () => { - expect(canDiffColumns(row({ rowType: 'data' }), row({ rowType: 'data' }))).toBe(true); - }); - - it('should forbid the diff for the rows of different types', () => { - expect(canDiffColumns(row({ rowType: 'data' }), row({ rowType: 'detail' }))).toBe(false); - }); - - it('should forbid the diff for group footers', () => { - expect(canDiffColumns(row({ rowType: 'groupFooter' }), row({ rowType: 'groupFooter' }))) - .toBe(false); - }); -}); - -describe('getGroupColumnIndices', () => { - const groupRow = (partial: Partial<ProcessedItem>): ProcessedItem => row({ - rowType: 'group', - isExpanded: true, - data: { isContinuation: false, isContinuationOnNextPage: false }, - ...partial, - }); - - it('should skip the group expand cell', () => { - const oldItem = groupRow({ - cells: [{ column: { type: 'groupExpand' } }, {}, { column: { dataField: 'name' } }], - }); - - expect(getGroupColumnIndices(oldItem, groupRow({}))).toEqual([1, 2]); - }); - - it('should return undefined when the old row has no cells', () => { - expect(getGroupColumnIndices(groupRow({}), groupRow({}))).toBeUndefined(); - }); - - it('should return undefined when the group state has changed', () => { - const oldItem = groupRow({ cells: [{}] }); - - expect(getGroupColumnIndices(oldItem, groupRow({ isExpanded: false }))).toBeUndefined(); - }); -}); - describe('getRowKey', () => { it('should tell apart the rows of different types with the same key', () => { expect(getRowKey(row({ key: 1, rowType: 'data' }))) @@ -307,7 +226,7 @@ describe('updateKeptRows', () => { }); }); -describe('getDataRowIndex', () => { +describe('countRowsBefore', () => { const rows = [ row({ rowType: 'data' }), row({ rowType: 'group' }), @@ -315,14 +234,24 @@ describe('getDataRowIndex', () => { row({ rowType: 'data' }), ]; - it('should count the data and group rows before the visible index', () => { - expect(getDataRowIndex(rows, 0)).toBe(0); - expect(getDataRowIndex(rows, 3)).toBe(2); - expect(getDataRowIndex(rows, rows.length)).toBe(3); + it('should count the rows of the requested type before the visible index', () => { + expect(countRowsBefore(rows, 0, 'data')).toBe(0); + expect(countRowsBefore(rows, 3, 'data')).toBe(1); + expect(countRowsBefore(rows, rows.length, 'data')).toBe(2); + }); + + it('should not count the rows of any other type', () => { + expect(countRowsBefore(rows, rows.length, 'group')).toBe(1); + expect(countRowsBefore(rows, rows.length, 'detail')).toBe(1); + expect(countRowsBefore(rows, rows.length, 'groupFooter')).toBe(0); }); it('should count the rows that are there when the index is out of range', () => { - expect(getDataRowIndex(rows, 10)).toBe(3); + expect(countRowsBefore(rows, 10, 'data')).toBe(2); + }); + + it('should return zero for an empty row set', () => { + expect(countRowsBefore([], 3, 'data')).toBe(0); }); }); diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_controller/utils/paging.ts b/packages/devextreme/js/__internal/grids/grid_core/data_controller/utils/paging.ts index 0d4ba2ec690a..dc8f50219e70 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/data_controller/utils/paging.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/data_controller/utils/paging.ts @@ -1,16 +1,5 @@ import type { PagingChanges, PagingDataSource, SyncPagingOptions } from '../types'; -export function resolvePaginate( - enabled: boolean | undefined, - scrollingMode: string | undefined, -): boolean | undefined { - if (enabled === undefined) { - return undefined; - } - - return enabled || scrollingMode === 'virtual' || scrollingMode === 'infinite'; -} - export function syncPaging( dataSource: PagingDataSource, options: SyncPagingOptions, diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_controller/utils/row_changes.ts b/packages/devextreme/js/__internal/grids/grid_core/data_controller/utils/row_changes.ts index c4ae25369d6e..b713669479e5 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/data_controller/utils/row_changes.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/data_controller/utils/row_changes.ts @@ -26,30 +26,6 @@ export function isSameItem( return isSameRowType && (!isDetailRow || isSameEditingState); } -// TODO remove after related checks moved to extenders (duplicated in grouping) -export function isSameGroupRowState(item1: ProcessedItem, item2: ProcessedItem): boolean { - return item1.isExpanded === item2.isExpanded - && item1.data?.isContinuation === item2.data?.isContinuation - && item1.data?.isContinuationOnNextPage === item2.data?.isContinuationOnNextPage; -} - -export function canDiffColumns(oldItem: ProcessedItem, newItem: ProcessedItem): boolean { - return oldItem.rowType === newItem.rowType && newItem.rowType !== 'groupFooter'; -} - -export function getGroupColumnIndices( - oldItem: ProcessedItem, - newItem: ProcessedItem, -): number[] | undefined { - if (!oldItem.cells || !isSameGroupRowState(oldItem, newItem)) { - return undefined; - } - - return oldItem.cells - .map((cell, index) => (cell.column?.type !== 'groupExpand' ? index : -1)) - .filter((index) => index >= 0); -} - /** * Rows of different types may share a key, so the row type is a part of the key * the diff is built on. @@ -122,14 +98,14 @@ export function updateKeptRows( }); } -/** - * A store change is indexed by data rows, while an insert index coming from the - * grid counts every visible row — group rows included. - */ -export function getDataRowIndex(rows: ProcessedItem[], visibleRowIndex: number): number { +export function countRowsBefore( + rows: ProcessedItem[], + visibleRowIndex: number, + rowType: ProcessedItem['rowType'], +): number { const previousRows = rows.slice(0, visibleRowIndex); - return previousRows.filter((row) => row?.rowType === 'data' || row?.rowType === 'group').length; + return previousRows.filter((row) => row?.rowType === rowType).length; } export function getChangedRowIndices( diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_source/__tests__/data_source_controller.integration.test.ts b/packages/devextreme/js/__internal/grids/grid_core/data_source/__tests__/data_source_controller.integration.test.ts new file mode 100644 index 000000000000..47b9ae87e9ba --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/data_source/__tests__/data_source_controller.integration.test.ts @@ -0,0 +1,303 @@ +import { + afterEach, + beforeEach, + describe, + expect, + it, + jest, +} from '@jest/globals'; +import DataSourceClass from '@js/data/data_source'; +import errors from '@js/ui/widget/ui.errors'; +import { + afterTest, + beforeTest, + createDataGrid, + flushAsync, + getMirroredAdapter, +} from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils'; +import type { InternalGrid } from '@ts/grids/grid_core/m_types'; + +import { DataSourceController } from '../data_source_controller'; + +const DATA = [ + { id: 1, parentId: 0, value: 'a' }, + { id: 2, parentId: 1, value: 'b' }, +]; + +const OTHER_DATA = [ + { id: 3, parentId: 0, value: 'c' }, +]; + +const getControllerNames = (instance: unknown): string[] => Object + .keys((instance as InternalGrid)._controllers); + +describe('dataSource module registration', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + it('is reachable from DataGrid', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + + expect(instance.getController('dataSource')).toBeInstanceOf(DataSourceController); + }); + + it('owns the getDataSource widget method', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + + expect(instance.getDataSource()).toBeInstanceOf(DataSourceClass); + expect(instance.getDataSource()) + .toBe(instance.getController('dataSource').getDataSource()); + }); + + it('owns the keyOf widget method', async () => { + const { instance } = await createDataGrid({ dataSource: DATA, keyExpr: 'id' }); + + expect(instance.keyOf(DATA[1])).toBe(2); + expect(instance.keyOf(DATA[1])) + .toBe(instance.getController('dataSource').keyOf(DATA[1])); + }); + + it('sits at the bottom of the controller order', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + + expect(getControllerNames(instance)[0]).toBe('dataSource'); + }); +}); + +describe('dataSource controller holds the adapter', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + it('holds the same adapter object as DataController', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + const dataSourceController = instance.getController('dataSource'); + const adapter = getMirroredAdapter(instance); + + expect(adapter).toBeTruthy(); + expect(dataSourceController.hasAdapter()).toBe(true); + expect(dataSourceController.getAdapter()).toBe(adapter); + }); + + it('follows the rebuilt adapter when the dataSource option changes', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + const dataSourceController = instance.getController('dataSource'); + const firstAdapter = dataSourceController.getAdapter(); + + instance.option('dataSource', OTHER_DATA); + await flushAsync(); + + expect(dataSourceController.getAdapter()).not.toBe(firstAdapter); + expect(dataSourceController.getAdapter()).toBe(getMirroredAdapter(instance)); + }); + + it('releases the adapter when the dataSource option is cleared', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + const dataSourceController = instance.getController('dataSource'); + + instance.option('dataSource', undefined); + await flushAsync(); + + expect(dataSourceController.hasAdapter()).toBe(false); + expect(dataSourceController.getAdapter()).toBeNull(); + expect(getMirroredAdapter(instance)).toBeNull(); + expect(dataSourceController.getDataSource()).toBeNull(); + expect(dataSourceController.store()).toBeUndefined(); + }); + + it('recovers after the dataSource option is set again', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + const dataSourceController = instance.getController('dataSource'); + + instance.option('dataSource', undefined); + await flushAsync(); + instance.option('dataSource', OTHER_DATA); + await flushAsync(); + + expect(dataSourceController.hasAdapter()).toBe(true); + expect(dataSourceController.getAdapter()).toBe(getMirroredAdapter(instance)); + }); + + it('still holds the same adapter after a refresh', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + const dataSourceController = instance.getController('dataSource'); + + const refreshed = instance.refresh(); + await flushAsync(); + await refreshed; + + expect(dataSourceController.hasAdapter()).toBe(true); + expect(dataSourceController.getAdapter()).toBe(getMirroredAdapter(instance)); + }); + + it('releases the adapter on dispose', async () => { + const { $container, instance } = await createDataGrid({ dataSource: DATA }); + const dataSourceController = instance.getController('dataSource'); + + instance.dispose(); + $container.remove(); + + expect(getMirroredAdapter(instance)).toBeNull(); + expect(dataSourceController.hasAdapter()).toBe(false); + }); +}); + +describe('dataSource controller reads delegate to the adapter', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + it('delegates key', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + + expect(instance.getController('dataSource').key()).toBe('id'); + }); + + it('unwraps one hop for getDataSource', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + const dataSourceController = instance.getController('dataSource'); + + expect(dataSourceController.getDataSource()).toBeInstanceOf(DataSourceClass); + expect(dataSourceController.getDataSource()) + .not.toBe(dataSourceController.getAdapter()); + }); + + it('delegates remoteOperations instead of falling back to an empty object', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + const dataSourceController = instance.getController('dataSource'); + const adapter = dataSourceController.getAdapter(); + + expect(dataSourceController.remoteOperations()).toBe(adapter?.remoteOperations()); + }); + + it('delegates getDataIndexGetter', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + + expect(typeof instance.getController('dataSource').getDataIndexGetter()).toBe('function'); + }); + + it('answers key and keyOf from the store when the store is keyed', async () => { + const { instance } = await createDataGrid({ dataSource: DATA, keyExpr: 'id' }); + const dataSourceController = instance.getController('dataSource'); + + expect(dataSourceController.key()).toBe('id'); + expect(dataSourceController.keyOf(DATA[1])).toBe(2); + }); + + it('answers key and keyOf from the store when the store has no key', async () => { + const { instance } = await createDataGrid({ dataSource: DATA, keyExpr: undefined }); + const dataSourceController = instance.getController('dataSource'); + + expect(dataSourceController.key()).toBeUndefined(); + // A keyless ArrayStore identifies a row by the object itself. + expect(dataSourceController.keyOf(DATA[1])).toBe(DATA[1]); + }); +}); + +describe('dataSource controller resolves its own component adapter provider', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + it('builds a DataGrid adapter in DataGrid', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + const adapter = instance.getController('dataSource').getAdapter(); + + expect(adapter).toBeTruthy(); + expect('forEachNode' in (adapter as object)).toBe(false); + }); +}); + +describe('dataSource controller owns the dataSource option reading', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + it('warns W1011 in DataGrid when keyExpr is combined with a non-array dataSource', async () => { + const log = jest.spyOn(errors, 'log').mockImplementation(() => {}); + + try { + await createDataGrid({ dataSource: { store: { type: 'array', data: DATA } }, keyExpr: 'id' }); + + expect(log).toHaveBeenCalledWith('W1011'); + } finally { + log.mockRestore(); + } + }); + + it('does not warn W1011 in DataGrid for an array dataSource', async () => { + const log = jest.spyOn(errors, 'log').mockImplementation(() => {}); + + try { + await createDataGrid({ dataSource: DATA, keyExpr: 'id' }); + + expect(log).not.toHaveBeenCalledWith('W1011'); + } finally { + log.mockRestore(); + } + }); + + it('builds a DataSource from the array option and keys it by keyExpr', async () => { + const { instance } = await createDataGrid({ dataSource: DATA, keyExpr: 'id' }); + + expect(instance.getController('dataSource').key()).toBe('id'); + }); +}); + +describe('dataSource controller owns adapter disposal', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + it('spares a DataSource the caller still owns when the grid is disposed', async () => { + const shared = new DataSourceClass({ store: DATA, key: 'id' }); + const dispose = jest.spyOn(shared, 'dispose'); + const { $container, instance } = await createDataGrid({ dataSource: shared }); + + instance.dispose(); + // afterTest reads the component off #gridContainer, so a disposed one must not linger. + $container.remove(); + + expect(dispose).not.toHaveBeenCalled(); + + dispose.mockRestore(); + shared.dispose(); + }); + + it('destroys a DataSource it built itself when the grid is disposed', async () => { + const { $container, instance } = await createDataGrid({ dataSource: DATA, keyExpr: 'id' }); + const built = instance.getController('dataSource').getDataSource(); + + if (!built) { + throw new Error('expected the controller to have built a DataSource'); + } + + const dispose = jest.spyOn(built, 'dispose'); + + instance.dispose(); + $container.remove(); + + expect(dispose).toHaveBeenCalledTimes(1); + + dispose.mockRestore(); + }); + + it('spares a shared DataSource when the dataSource option is replaced', async () => { + const shared = new DataSourceClass({ store: DATA, key: 'id' }); + const dispose = jest.spyOn(shared, 'dispose'); + const { instance } = await createDataGrid({ dataSource: shared }); + + instance.option('dataSource', OTHER_DATA); + await flushAsync(); + + expect(dispose).not.toHaveBeenCalled(); + + dispose.mockRestore(); + shared.dispose(); + }); + + it('leaves DataController and the controller agreeing that the adapter is gone', async () => { + const { instance } = await createDataGrid({ dataSource: DATA, keyExpr: 'id' }); + + instance.option('dataSource', undefined); + await flushAsync(); + + expect(instance.getController('dataSource').hasAdapter()).toBe(false); + expect(instance.getController('dataSource').getAdapter()).toBeNull(); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_source/__tests__/data_source_controller.test.ts b/packages/devextreme/js/__internal/grids/grid_core/data_source/__tests__/data_source_controller.test.ts new file mode 100644 index 000000000000..d64d70ec46ab --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/data_source/__tests__/data_source_controller.test.ts @@ -0,0 +1,493 @@ +import { + describe, + expect, + it, + jest, +} from '@jest/globals'; +import { DataSource as DataSourceClass } from '@js/common/data/data_source/data_source'; +import type Store from '@ts/data/abstract_store'; +import type { StoreKey } from '@ts/data/abstract_store'; +import type { DataSource } from '@ts/data/data_source/data_source'; +import type DataSourceAdapter from '@ts/grids/grid_core/data_source_adapter/m_data_source_adapter'; +import type { + DataSourceAdapterProvider, RawItemData, RemoteOperationsOptions, +} from '@ts/grids/grid_core/data_source_adapter/types'; +import type { InternalGrid } from '@ts/grids/grid_core/m_types'; + +import { DataSourceController } from '../data_source_controller'; + +interface AdapterStub { + _dataSource: DataSource; + store: jest.Mock<() => Store | undefined>; + key: jest.Mock<() => StoreKey | undefined>; + remoteOperations: jest.Mock<() => RemoteOperationsOptions>; + getDataIndexGetter: jest.Mock<() => (data: RawItemData) => number>; + dispose: jest.Mock<(isShared?: boolean) => void>; + init: jest.Mock<(dataSource: DataSource) => void>; +} + +interface ProviderStub { + nextAdapter: AdapterStub; + create: jest.Mock<(component: InternalGrid) => DataSourceAdapter>; + extend: jest.Mock<() => void>; +} + +const createAdapterStub = (marker: string): AdapterStub => ({ + _dataSource: { marker } as unknown as DataSource, + store: jest.fn(() => ({ marker } as unknown as Store)), + key: jest.fn(() => marker as StoreKey), + remoteOperations: jest.fn(() => ({ filtering: true } as RemoteOperationsOptions)), + getDataIndexGetter: jest.fn(() => (): number => 0), + dispose: jest.fn(), + init: jest.fn(), +}); + +const asAdapter = (stub: AdapterStub): DataSourceAdapter => stub as unknown as DataSourceAdapter; + +class TestDataSourceController extends DataSourceController { + public providerStub!: DataSourceAdapterProvider; + + protected getAdapterProvider(): DataSourceAdapterProvider { + return this.providerStub; + } + + public readSpecificDataSourceOption(): unknown { + return this.getSpecificDataSourceOption(); + } +} + +const createControllerWith = (): { + controller: DataSourceController; + component: InternalGrid; +} => { + const component = { + _optionCache: {}, + _controllers: {}, + option: jest.fn(), + } as unknown as InternalGrid; + + return { controller: new DataSourceController(component), component }; +}; + +const createController = (): DataSourceController => createControllerWith().controller; + +const createProviderStub = (adapter: AdapterStub): ProviderStub => { + const stub: ProviderStub = { + nextAdapter: adapter, + create: jest.fn(() => asAdapter(stub.nextAdapter)), + extend: jest.fn(), + }; + + return stub; +}; + +const asProvider = ( + stub: ProviderStub, +): DataSourceAdapterProvider => stub as unknown as DataSourceAdapterProvider; + +const SOURCE = { marker: 'source' } as unknown as DataSource; + +const withProvider = (adapter: AdapterStub): { + controller: TestDataSourceController; + component: InternalGrid; + provider: ProviderStub; +} => { + const component = { + _optionCache: {}, + _controllers: {}, + option: jest.fn(), + } as unknown as InternalGrid; + const controller = new TestDataSourceController(component); + const provider = createProviderStub(adapter); + + controller.providerStub = asProvider(provider); + + return { controller, component, provider }; +}; + +const withOptions = (options: Record<string, unknown>): TestDataSourceController => { + const component = { + _controllers: {}, + option: jest.fn((name?: string) => (name === undefined ? options : options[name])), + } as unknown as InternalGrid; + + return new TestDataSourceController(component); +}; + +// isShared is private and read only by disposal, so that is where it becomes observable. +const flagHandedToAdapter = ( + controller: TestDataSourceController, +): boolean | undefined => { + const probe = createAdapterStub('probe'); + + controller.providerStub = asProvider(createProviderStub(probe)); + controller.createAdapter(SOURCE); + controller.disposeAdapter(); + + return probe.dispose.mock.calls[0]?.[0]; +}; + +const withAdapter = (marker = 'first'): { + controller: TestDataSourceController; + adapter: AdapterStub; + provider: ProviderStub; +} => { + const adapter = createAdapterStub(marker); + const { controller, provider } = withProvider(adapter); + + controller.createAdapter(SOURCE); + + return { controller, adapter, provider }; +}; + +describe('DataSourceController', () => { + describe('with no adapter', () => { + it('has no adapter right after construction, without init()', () => { + const controller = createController(); + + expect(controller.hasAdapter()).toBe(false); + expect(controller.getAdapter()).toBeNull(); + }); + + it('returns null from getDataSource', () => { + expect(createController().getDataSource()).toBeNull(); + }); + + it('returns undefined from store and key', () => { + const controller = createController(); + + expect(controller.store()).toBeUndefined(); + expect(controller.key()).toBeUndefined(); + }); + + it('returns undefined from getDataIndexGetter', () => { + expect(createController().getDataIndexGetter()).toBeUndefined(); + }); + + it('returns an empty object from remoteOperations, so callers can enumerate it', () => { + const controller = createController(); + + expect(controller.remoteOperations()).toEqual({}); + expect(Object.keys(controller.remoteOperations())).toEqual([]); + }); + }); + + describe('with an adapter', () => { + it('reports the adapter as present and hands back the same object', () => { + const { controller, adapter } = withAdapter(); + + expect(controller.hasAdapter()).toBe(true); + expect(controller.getAdapter()).toBe(asAdapter(adapter)); + }); + + it('delegates store to the adapter once per call', () => { + const { controller, adapter } = withAdapter(); + + expect(controller.store()).toBe(adapter.store.mock.results[0]?.value); + expect(adapter.store).toHaveBeenCalledTimes(1); + }); + + it('delegates key to the adapter', () => { + const { controller, adapter } = withAdapter(); + + expect(controller.key()).toBe('first'); + expect(adapter.key).toHaveBeenCalledTimes(1); + }); + + it('returns the adapter remoteOperations object as-is', () => { + const { controller, adapter } = withAdapter(); + + expect(controller.remoteOperations()).toEqual({ filtering: true }); + expect(adapter.remoteOperations).toHaveBeenCalledTimes(1); + }); + + it('delegates getDataIndexGetter to the adapter', () => { + const { controller, adapter } = withAdapter(); + const getter = controller.getDataIndexGetter(); + + expect(getter).toBe(adapter.getDataIndexGetter.mock.results[0]?.value); + expect(adapter.getDataIndexGetter).toHaveBeenCalledTimes(1); + }); + + it('returns the inner DataSource from getDataSource, not the adapter', () => { + const { controller, adapter } = withAdapter(); + + expect(controller.getDataSource()).toBe(adapter._dataSource); + expect(controller.getDataSource()).not.toBe(asAdapter(adapter)); + }); + }); + + describe('replacing the adapter', () => { + it('follows the new adapter after a replacement', () => { + const { controller, adapter: first, provider } = withAdapter(); + const second = createAdapterStub('second'); + + provider.nextAdapter = second; + controller.createAdapter(SOURCE); + + expect(controller.getAdapter()).toBe(asAdapter(second)); + expect(controller.key()).toBe('second'); + expect(controller.getDataSource()).toBe(second._dataSource); + expect(first.key).not.toHaveBeenCalled(); + }); + + it('returns to the absent state after disposeAdapter', () => { + const { controller } = withAdapter(); + + controller.disposeAdapter(); + + expect(controller.hasAdapter()).toBe(false); + expect(controller.getAdapter()).toBeNull(); + expect(controller.getDataSource()).toBeNull(); + expect(controller.store()).toBeUndefined(); + expect(controller.key()).toBeUndefined(); + expect(controller.getDataIndexGetter()).toBeUndefined(); + expect(controller.remoteOperations()).toEqual({}); + }); + }); + + describe('layering', () => { + it('reads no other controller', () => { + const { controller } = withAdapter(); + const getController = jest.spyOn(controller, 'getController'); + + controller.hasAdapter(); + controller.getAdapter(); + controller.getDataSource(); + controller.store(); + controller.key(); + controller.remoteOperations(); + controller.getDataIndexGetter(); + controller.disposeAdapter(); + + expect(getController).not.toHaveBeenCalled(); + }); + }); + + describe('createAdapter', () => { + it('builds the adapter through the provider, passing the component', () => { + const { controller, component, provider } = withProvider(createAdapterStub('built')); + + controller.createAdapter(SOURCE); + + expect(provider.create).toHaveBeenCalledTimes(1); + expect(provider.create).toHaveBeenCalledWith(component); + }); + + it('initialises the adapter with the given data source', () => { + const adapter = createAdapterStub('built'); + + withProvider(adapter).controller.createAdapter(SOURCE); + + expect(adapter.init).toHaveBeenCalledTimes(1); + expect(adapter.init).toHaveBeenCalledWith(SOURCE); + }); + + it('returns the adapter the provider produced', () => { + const adapter = createAdapterStub('built'); + + const result = withProvider(adapter).controller.createAdapter(SOURCE); + + expect(result).toBe(asAdapter(adapter)); + }); + + it('stores the adapter it built', () => { + const adapter = createAdapterStub('built'); + const { controller } = withProvider(adapter); + + controller.createAdapter(SOURCE); + + expect(controller.hasAdapter()).toBe(true); + expect(controller.getAdapter()).toBe(asAdapter(adapter)); + }); + + it('replaces a previously held adapter without disposing it', () => { + const first = createAdapterStub('first'); + const { controller, provider } = withProvider(first); + const second = createAdapterStub('second'); + + controller.createAdapter(SOURCE); + provider.nextAdapter = second; + controller.createAdapter(SOURCE); + + expect(controller.getAdapter()).toBe(asAdapter(second)); + expect(first.dispose).not.toHaveBeenCalled(); + }); + + it('throws on the base class, where no component has supplied a provider', () => { + expect(() => createController().createAdapter(SOURCE)).toThrow('Method not implemented.'); + }); + }); + + describe('getSpecificDataSourceOption', () => { + it('wraps an array option into an array store, keyed by keyExpr', () => { + const data = [{ id: 1 }]; + + const result = withOptions({ dataSource: data, keyExpr: 'id' }) + .readSpecificDataSourceOption(); + + expect(result).toEqual({ store: { type: 'array', data, key: 'id' } }); + }); + + it('keeps the caller array by reference, leaving the copy to createDataSource', () => { + const data = [{ id: 1 }]; + + const result = withOptions({ dataSource: data, keyExpr: 'id' }) + .readSpecificDataSourceOption() as { store: { data: unknown } }; + + expect(result.store.data).toBe(data); + }); + + it('passes a non-array option straight through', () => { + const config = { store: { type: 'odata', url: 'x' } }; + + const result = withOptions({ dataSource: config }).readSpecificDataSourceOption(); + + expect(result).toBe(config); + }); + + it('returns the unset option as-is, so createDataSource can see it is absent', () => { + expect(withOptions({}).readSpecificDataSourceOption()).toBeUndefined(); + }); + + it('does not throw on the base class, unlike getAdapterProvider', () => { + expect(() => withOptions({}).readSpecificDataSourceOption()).not.toThrow(); + }); + }); + + describe('createDataSource', () => { + it('returns undefined when the dataSource option is absent', () => { + const controller = withOptions({}); + + expect(controller.createDataSource()).toBeUndefined(); + }); + + it('leaves the source not-shared when the dataSource option is absent', () => { + const controller = withOptions({}); + + controller.createDataSource(); + + expect(flagHandedToAdapter(controller)).toBe(false); + }); + + it('builds a DataSource from a plain array', () => { + const controller = withOptions({ dataSource: [{ id: 1 }], keyExpr: 'id' }); + + expect(controller.createDataSource()).toBeInstanceOf(DataSourceClass); + }); + + it('keys the built DataSource by keyExpr', () => { + const controller = withOptions({ dataSource: [{ id: 1 }], keyExpr: 'id' }); + + expect(controller.createDataSource()?.key()).toBe('id'); + }); + + it('marks what it built not-shared, so disposal may destroy it', () => { + const controller = withOptions({ dataSource: [{ id: 1 }], keyExpr: 'id' }); + + controller.createDataSource(); + + expect(flagHandedToAdapter(controller)).toBe(false); + }); + + it('builds a DataSource from a store config', () => { + const controller = withOptions({ dataSource: { store: { type: 'array', data: [] } } }); + + expect(controller.createDataSource()).toBeInstanceOf(DataSourceClass); + }); + + it('builds a fresh DataSource on every call', () => { + const controller = withOptions({ dataSource: [{ id: 1 }], keyExpr: 'id' }); + + expect(controller.createDataSource()).not.toBe(controller.createDataSource()); + }); + + it('hands back the very DataSource the caller passed, without rebuilding it', () => { + const shared = new DataSourceClass({ store: [{ id: 1 }], key: 'id' }); + const controller = withOptions({ dataSource: shared }); + + expect(controller.createDataSource()).toBe(shared); + + shared.dispose(); + }); + + it('marks a caller-owned DataSource shared, so disposal spares it', () => { + const shared = new DataSourceClass({ store: [{ id: 1 }], key: 'id' }); + const controller = withOptions({ dataSource: shared }); + + controller.createDataSource(); + + expect(flagHandedToAdapter(controller)).toBe(true); + + shared.dispose(); + }); + + it('clears the shared flag when the option moves from a DataSource to an array', () => { + const shared = new DataSourceClass({ store: [{ id: 1 }], key: 'id' }); + const options: Record<string, unknown> = { dataSource: shared, keyExpr: 'id' }; + const controller = withOptions(options); + + controller.createDataSource(); + options.dataSource = [{ id: 2 }]; + controller.createDataSource(); + + expect(flagHandedToAdapter(controller)).toBe(false); + + shared.dispose(); + }); + + it('starts out not-shared, before anything has been created', () => { + expect(flagHandedToAdapter(withOptions({}))).toBe(false); + }); + + it('leaves the held adapter alone — creating a source is not creating an adapter', () => { + const controller = withOptions({ dataSource: [{ id: 1 }], keyExpr: 'id' }); + + controller.createDataSource(); + + expect(controller.hasAdapter()).toBe(false); + }); + + it('reads no other controller', () => { + const controller = withOptions({ dataSource: [{ id: 1 }], keyExpr: 'id' }); + const getController = jest.spyOn(controller, 'getController'); + + controller.createDataSource(); + controller.readSpecificDataSourceOption(); + + expect(getController).not.toHaveBeenCalled(); + }); + }); + + describe('disposeAdapter', () => { + it('disposes the adapter it holds', () => { + const { controller, adapter } = withAdapter(); + + controller.disposeAdapter(); + + expect(adapter.dispose).toHaveBeenCalledTimes(1); + }); + + it('does nothing when there is no adapter to dispose', () => { + expect(() => createController().disposeAdapter()).not.toThrow(); + }); + + it('disposes only once across repeated calls', () => { + const { controller, adapter } = withAdapter(); + + controller.disposeAdapter(); + controller.disposeAdapter(); + + expect(adapter.dispose).toHaveBeenCalledTimes(1); + }); + + it('reads no other controller', () => { + const { controller } = withAdapter(); + const getController = jest.spyOn(controller, 'getController'); + + controller.disposeAdapter(); + + expect(getController).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_source/data_source_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/data_source/data_source_controller.ts new file mode 100644 index 000000000000..7868bde16226 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/data_source/data_source_controller.ts @@ -0,0 +1,150 @@ +import { DataSource as DataSourceClass } from '@js/common/data/data_source/data_source'; +import { normalizeDataSourceOptions } from '@js/common/data/data_source/utils'; +import { extend } from '@js/core/utils/extend'; +import type Store from '@ts/data/abstract_store'; +import type { StoreKey } from '@ts/data/abstract_store'; +import type { DataSource } from '@ts/data/data_source/data_source'; +import type { StoreLoadOptions } from '@ts/data/data_source/types'; +import type DataSourceAdapter from '@ts/grids/grid_core/data_source_adapter/m_data_source_adapter'; +import type { + DataSourceAdapterProvider, LoadOperation, OperationTypes, RawItemData, RemoteOperationsOptions, +} from '@ts/grids/grid_core/data_source_adapter/types'; +import modules from '@ts/grids/grid_core/m_modules'; +import type { RowKey } from '@ts/grids/grid_core/m_types'; + +export class DataSourceController< + TAdapter extends DataSourceAdapter = DataSourceAdapter, +> extends modules.Controller { + // Absent before the first dataSource assignment and again after a reset. + protected adapter: TAdapter | null = null; + + private isShared = false; + + /** + * @extended: DataGrid's and TreeList's data_source_controller + */ + protected getAdapterProvider(): DataSourceAdapterProvider<TAdapter> { + throw new Error('Method not implemented.'); + } + + public publicMethods(): string[] { + return ['getDataSource', 'keyOf']; + } + + /** + * @extended: DataGrid's data_source_controller + */ + protected getSpecificDataSourceOption(): unknown { + const dataSource = this.option('dataSource'); + + if (Array.isArray(dataSource)) { + return { + store: { + type: 'array', + data: dataSource, + key: this.option('keyExpr'), + }, + }; + } + + return dataSource; + } + + public createDataSource(): DataSource | undefined { + const dataSourceOptions = this.getSpecificDataSourceOption(); + + if (!dataSourceOptions) { + this.isShared = false; + return undefined; + } + + if (dataSourceOptions instanceof DataSourceClass) { + this.isShared = true; + return dataSourceOptions as unknown as DataSource; + } + + this.isShared = false; + return new DataSourceClass( + extend(true, {}, normalizeDataSourceOptions(dataSourceOptions, {})), + ) as unknown as DataSource; + } + + public getDataSource(): DataSource | null { + return this.adapter?._dataSource ?? null; + } + + public createAdapter(dataSource: DataSource): TAdapter { + const adapter = this.getAdapterProvider().create(this.component); + + adapter.init(dataSource); + this.adapter = adapter; + + return adapter; + } + + public hasAdapter(): boolean { + return this.adapter !== null; + } + + public getAdapter(): TAdapter | null { + return this.adapter; + } + + public disposeAdapter(): void { + this.adapter?.dispose(this.isShared); + this.adapter = null; + } + + public store(): Store | undefined { + return this.adapter?.store(); + } + + /** + * The key the component identifies rows by. Not interchangeable with `store()?.key()` (TreeList) + * Callers that need what the store itself can identify a row by have to ask the store. + * + * @extended: TreeList's data_source_controller + */ + public key(): StoreKey | undefined { + return this.adapter?.key(); + } + + /** + * @extended: TreeList's data_source_controller + */ + public keyOf(data: RawItemData): RowKey | undefined { + return this.adapter?.store()?.keyOf(data); + } + + public remoteOperations(): RemoteOperationsOptions { + return this.adapter?.remoteOperations() ?? {}; + } + + public getDataIndexGetter(): ((data: RawItemData) => number) | undefined { + return this.adapter?.getDataIndexGetter(); + } + + public operationTypes(): OperationTypes | null | undefined { + return this.adapter?.operationTypes(); + } + + public loadingOperationTypes(): OperationTypes { + return this.adapter?.loadingOperationTypes() ?? {}; + } + + public isLoading(): boolean { + return this.adapter?.isLoading() ?? false; + } + + public select(): StoreLoadOptions['select'] { + return this.adapter?.select(); + } + + public lastLoadOptions(): Partial<NonNullable<LoadOperation['lastLoadOptions']>> { + return this.adapter?.lastLoadOptions() ?? {}; + } + + public getCachedStoreData(): RawItemData[] | undefined { + return this.adapter?.getCachedStoreData(); + } +} diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_source_adapter/m_data_source_adapter.ts b/packages/devextreme/js/__internal/grids/grid_core/data_source_adapter/m_data_source_adapter.ts index 4b135fbd8680..10c8b496f377 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/data_source_adapter/m_data_source_adapter.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/data_source_adapter/m_data_source_adapter.ts @@ -297,7 +297,6 @@ export default class DataSourceAdapter extends modules.Controller { this.resetPagesCache(true); if (this._cachedStoreData) { - // @ts-expect-error applyBatch({ keyInfo: store, data: this._cachedStoreData, @@ -365,7 +364,6 @@ export default class DataSourceAdapter extends modules.Controller { const getItemCount = () => (groupCount ? this.itemsCount() : this.items().length); const oldItemCount = getItemCount(); - // @ts-expect-error applyBatch({ keyInfo, data: this._items, @@ -374,7 +372,6 @@ export default class DataSourceAdapter extends modules.Controller { useInsertIndex: true, skipCopying: !this._needToCopyDataObject(), }); - // @ts-expect-error applyBatch({ keyInfo, data: dataSource.items(), @@ -673,7 +670,7 @@ export default class DataSourceAdapter extends modules.Controller { /** * @extended: TreeLists's data_source_adapter */ - protected customizeLoadResultHandlerCore(options) { + protected customizeLoadResultHandlerCore(options: LoadOperation): void { if (options.remoteOperations && !options.remoteOperations.paging && Array.isArray(options.data)) { if (options.skip !== undefined) { options.data = options.data.slice(options.skip); diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_source_adapter/provider.ts b/packages/devextreme/js/__internal/grids/grid_core/data_source_adapter/provider.ts index cbd0748990ce..1268e5291419 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/data_source_adapter/provider.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/data_source_adapter/provider.ts @@ -2,16 +2,16 @@ import type { ModuleType } from '../m_types'; import type DataSourceAdapter from './m_data_source_adapter'; import type { DataSourceAdapterProvider } from './types'; -export function createDataSourceAdapterProvider( - BaseType: ModuleType<DataSourceAdapter>, -): DataSourceAdapterProvider { +export function createDataSourceAdapterProvider<TAdapter extends DataSourceAdapter>( + BaseType: ModuleType<TAdapter>, +): DataSourceAdapterProvider<TAdapter> { let AdapterType = BaseType; return { extend(extender): void { - AdapterType = extender(AdapterType); + AdapterType = extender(AdapterType) as ModuleType<TAdapter>; }, - create(component): DataSourceAdapter { + create(component): TAdapter { return new AdapterType(component); }, }; diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_source_adapter/types.ts b/packages/devextreme/js/__internal/grids/grid_core/data_source_adapter/types.ts index 9b0b9d52dce1..64e3088c344b 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/data_source_adapter/types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/data_source_adapter/types.ts @@ -81,7 +81,9 @@ export type DataSourceAdapterExtender = ( Base: ModuleType<DataSourceAdapter>, ) => ModuleType<DataSourceAdapter>; -export interface DataSourceAdapterProvider { +export interface DataSourceAdapterProvider< + TAdapter extends DataSourceAdapter = DataSourceAdapter, +> { extend: (extender: DataSourceAdapterExtender) => void; - create: (component: InternalGrid) => DataSourceAdapter; + create: (component: InternalGrid) => TAdapter; } diff --git a/packages/devextreme/js/__internal/grids/grid_core/editing/extenders/__tests__/editing_data_controller.row_changes.test.ts b/packages/devextreme/js/__internal/grids/grid_core/editing/extenders/__tests__/editing_data_controller.row_changes.test.ts index 77a44b18000c..1184e8adf5fa 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/editing/extenders/__tests__/editing_data_controller.row_changes.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/editing/extenders/__tests__/editing_data_controller.row_changes.test.ts @@ -16,6 +16,11 @@ const dataRow = (partial: Partial<ProcessedItem> = {}): ProcessedItem => ({ ...partial, }); +const editFormRow = (partial: Partial<ProcessedItem> = {}): ProcessedItem => dataRow({ + rowType: 'detail', + ...partial, +}); + describe('Editing data controller row changes', () => { beforeEach(beforeTest); afterEach(afterTest); @@ -38,4 +43,25 @@ describe('Editing data controller row changes', () => { expect(change.changeTypes).toEqual(['update']); }, ); + + it('should repaint the whole edit form row instead of diffing its columns', async () => { + const change = await refreshRow( + editFormRow({ isEditing: true, values: ['Alex', 15] }), + editFormRow({ isEditing: true, values: ['Bob', 15] }), + { gridOptions: { editing: { mode: 'form' } }, isLiveUpdate: false }, + ); + + expect(change.rowIndices).toEqual([0]); + expect(change.columnIndices).toEqual([undefined]); + }); + + it('should report a cell whose modified mark appeared without a new value', async () => { + const change = await refreshRow( + dataRow(), + dataRow({ modified: true, modifiedValues: [undefined, 15] }), + ); + + expect(change.rowIndices).toEqual([0]); + expect(change.columnIndices).toEqual([[1]]); + }); }); diff --git a/packages/devextreme/js/__internal/grids/grid_core/editing/extenders/editing_data_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/editing/extenders/editing_data_controller.ts index 999590624de4..6e401fa19485 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/editing/extenders/editing_data_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/editing/extenders/editing_data_controller.ts @@ -15,6 +15,7 @@ import gridCoreUtils from '@ts/grids/grid_core/m_utils'; import { EDITING_EDITROWKEY_OPTION_NAME } from '../const'; import type { EditingController } from '../m_editing'; +import { isCellModified } from '../m_editing_utils'; export interface EditingDataControllerExtension { _editingController: EditingController; @@ -147,6 +148,10 @@ export const editingDataControllerExtender = ( return true; } + if (isCellModified(oldRow, columnIndex) !== isCellModified(newRow, columnIndex)) { + return true; + } + return super._isCellChanged(oldRow, newRow, visibleRowIndex, columnIndex, isLiveUpdate); } @@ -173,7 +178,7 @@ export const editingDataControllerExtender = ( return; } - const dataSourceKeys = dataSource.map((item) => this.keyOf(item)); + const dataSourceKeys = dataSource.map((item) => this.dataSourceController.keyOf(item)); const survivingChanges = changes.filter( (change) => change.type === 'insert' || dataSourceKeys.some((key) => equalByValue(change.key, key)), ); diff --git a/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing.ts b/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing.ts index 92dcc54ab619..9d25d2a22197 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing.ts @@ -117,6 +117,8 @@ class EditingControllerImpl extends modules.ViewController { protected _dataController!: Controllers['data']; + protected dataSourceController!: Controllers['dataSource']; + protected adaptiveColumnsController!: Controllers['adaptiveColumns']; protected _validatingController!: Controllers['validating']; @@ -170,6 +172,7 @@ class EditingControllerImpl extends modules.ViewController { public init() { this._columnsController = this.getController('columns'); this._dataController = this.getController('data'); + this.dataSourceController = this.getController('dataSource'); this.adaptiveColumnsController = this.getController('adaptiveColumns'); this._validatingController = this.getController('validating'); this._editorFactoryController = this.getController('editorFactory'); @@ -309,7 +312,7 @@ class EditingControllerImpl extends modules.ViewController { } public getUpdatedData(data) { - const key = this._dataController.keyOf(data); + const key = this.dataSourceController.keyOf(data); const changes = this.getChanges(); const editIndex = gridCoreUtils.getIndexByKey(key, changes); @@ -614,8 +617,9 @@ class EditingControllerImpl extends modules.ViewController { if (change.type === 'insert') { this._addInsertInfo(change); } else { - const items = dataController.getCachedStoreData() || dataController.items()?.map((item) => item.data); - const rowIndex = gridCoreUtils.getIndexByKey(change.key, items, dataController.key()); + const items = this.dataSourceController.getCachedStoreData() + || dataController.items()?.map((item) => item.data); + const rowIndex = gridCoreUtils.getIndexByKey(change.key, items, this.dataSourceController.key()); this._addInternalData({ key: change.key, oldData: items[rowIndex] }); } }); @@ -717,7 +721,7 @@ class EditingControllerImpl extends modules.ViewController { if (equalByValue(item.key, key)) { result = index; } - } else if (equalByValue(dataController.keyOf(item as RawItemData), key)) { + } else if (equalByValue(this.dataSourceController.keyOf(item as RawItemData), key)) { result = index; } } @@ -910,7 +914,7 @@ class EditingControllerImpl extends modules.ViewController { return change.key; } - const keyExpr = this._dataController.key(); + const keyExpr = this.dataSourceController.key(); let keyValue; if (change.data && keyExpr && !Array.isArray(keyExpr)) { keyValue = change.data[keyExpr]; @@ -1003,7 +1007,7 @@ class EditingControllerImpl extends modules.ViewController { */ protected addRow(parentKey) { const dataController = this._dataController; - const store = dataController.store(); + const store = this.dataSourceController.store(); if (!store) { dataController.fireError('E1052', this.component.NAME); @@ -1015,9 +1019,7 @@ class EditingControllerImpl extends modules.ViewController { } protected _addRow(parentKey) { - const dataController = this._dataController; - const store = dataController.store(); - const key = store && store.key(); + const key = this.dataSourceController.store()?.key(); const param: any = { data: {} }; const oldEditRowIndex = this._getVisibleEditRowIndex(); // @ts-expect-error @@ -1612,7 +1614,7 @@ class EditingControllerImpl extends modules.ViewController { } private _processChanges(deferreds, results, dataChanges, changes) { - const store = this._dataController.store() as Store; + const store = this.dataSourceController.store() as Store; each(changes, (index, change) => { const oldData = this._getOldData(change.key); @@ -1812,7 +1814,7 @@ class EditingControllerImpl extends modules.ViewController { const results = []; const deferreds = []; const dataChanges = []; - const dataSource = this._dataController.dataSource(); + const dataSourceAdapter = this.dataSourceController.getAdapter(); when(this._fireOnSaving()) .done(({ cancel, changes }) => { @@ -1824,17 +1826,17 @@ class EditingControllerImpl extends modules.ViewController { if (deferreds.length) { this._refocusEditCell = true; - dataSource?.beginLoading(); + dataSourceAdapter?.beginLoading(); when(...deferreds).done(() => { if (this._processSaveEditDataResult(results)) { this._endSaving(dataChanges, changes, result); } else { - dataSource?.endLoading(); + dataSourceAdapter?.endLoading(); result.resolve(); } }).fail((error) => { - dataSource?.endLoading(); + dataSourceAdapter?.endLoading(); result.resolve(error); }); @@ -1855,11 +1857,11 @@ class EditingControllerImpl extends modules.ViewController { } private _endSaving(dataChanges, changes, deferred) { - const dataSource = this._dataController.dataSource(); + const dataSourceAdapter = this.dataSourceController.getAdapter(); this._beforeEndSaving(changes); - dataSource?.endLoading(); + dataSourceAdapter?.endLoading(); this._refreshDataAfterSave(dataChanges, changes, deferred); } diff --git a/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_utils.ts index ddffd2ed0859..67632b7d5599 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_utils.ts @@ -1,6 +1,8 @@ import Guid from '@js/core/guid'; import { isObject } from '@js/core/utils/type'; +import type { ProcessedItem } from '../data_controller/types'; + const NEW_ROW_TEMP_KEY_PREFIX = '_DX_KEY_'; const GUID_LENGTH = 36; @@ -80,3 +82,8 @@ export const forEachFormItems = (items, callBack) => { } }); }; + +export const isCellModified = ( + row: ProcessedItem, + columnIndex: number, +): boolean => row.modifiedValues?.[columnIndex] !== undefined; diff --git a/packages/devextreme/js/__internal/grids/grid_core/filter/filter_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/filter/filter_controller.ts new file mode 100644 index 000000000000..dd6e280b862f --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/filter/filter_controller.ts @@ -0,0 +1,34 @@ +import type { LangParams } from '@js/common/data'; +import type { Column } from '@ts/grids/grid_core/columns_controller/types'; +import type { DataFilter } from '@ts/grids/grid_core/data_controller/types'; +import modules from '@ts/grids/grid_core/m_modules'; +import type { Controllers } from '@ts/grids/grid_core/m_types'; + +export class FilterController extends modules.Controller { + protected columnsController!: Controllers['columns']; + + protected dataSourceController!: Controllers['dataSource']; + + public init(): void { + this.columnsController = this.getController('columns'); + this.dataSourceController = this.getController('dataSource'); + } + + public isFilterSyncActive(): boolean | undefined { + const filterSyncEnabled = this.option('filterSyncEnabled'); + + return filterSyncEnabled === 'auto' ? this.option('filterPanel.visible') : filterSyncEnabled; + } + + protected getLangParams(): LangParams | undefined { + return this.dataSourceController.getDataSource()?.loadOptions?.()?.langParams; + } + + /** + * @extended: filter_row, filter_sync, header_filter, search + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public getAdditionalFilter(excludedColumn?: Column | null): DataFilter { + return null; + } +} diff --git a/packages/devextreme/js/__internal/grids/grid_core/filter/filter_module.ts b/packages/devextreme/js/__internal/grids/grid_core/filter/filter_module.ts new file mode 100644 index 000000000000..942a85e95802 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/filter/filter_module.ts @@ -0,0 +1,9 @@ +import type { Module } from '@ts/grids/grid_core/m_types'; + +import { FilterController } from './filter_controller'; + +export const filterModule: Module = { + controllers: { + filter: FilterController, + }, +}; diff --git a/packages/devextreme/js/__internal/grids/grid_core/filter_panel/m_filter_panel.ts b/packages/devextreme/js/__internal/grids/grid_core/filter_panel/m_filter_panel.ts index c25f156bd2df..7b472e65d6c6 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/filter_panel/m_filter_panel.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/filter_panel/m_filter_panel.ts @@ -14,6 +14,7 @@ import { } from '@ts/filter_builder/m_utils'; import type { ColumnsController } from '@ts/grids/grid_core/columns_controller/m_columns_controller'; import type { DataController } from '@ts/grids/grid_core/data_controller/data_controller'; +import type { DataSourceController } from '@ts/grids/grid_core/data_source/data_source_controller'; import type { FilterSyncController } from '@ts/grids/grid_core/filter_sync/m_filter_sync'; import { registerKeyboardAction } from '@ts/grids/grid_core/m_accessibility'; import modules from '@ts/grids/grid_core/m_modules'; @@ -33,12 +34,15 @@ export class FilterPanelView extends modules.View { private _dataController!: DataController; + private dataSourceController!: DataSourceController; + private _filterSyncController!: FilterSyncController; private readonly _filterValueBuffer: any; public init() { this._dataController = this.getController('data'); + this.dataSourceController = this.getController('dataSource'); this._columnsController = this.getController('columns'); this._filterSyncController = this.getController('filterSync'); @@ -46,7 +50,7 @@ export class FilterPanelView extends modules.View { } public isVisible() { - return !!(this.option('filterPanel.visible') && this._dataController.dataSource()); + return !!this.option('filterPanel.visible') && this.dataSourceController.hasAdapter(); } protected _renderCore() { diff --git a/packages/devextreme/js/__internal/grids/grid_core/filter_row/m_filter_row.ts b/packages/devextreme/js/__internal/grids/grid_core/filter_row/m_filter_row.ts index b4a100906344..318f6a928d94 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/filter_row/m_filter_row.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/filter_row/m_filter_row.ts @@ -16,8 +16,10 @@ import type { ColumnHeadersView } from '@ts/grids/grid_core/column_headers/m_col import type { ColumnsController } from '@ts/grids/grid_core/columns_controller/m_columns_controller'; import type { Column } from '@ts/grids/grid_core/columns_controller/types'; import type { ColumnsResizerViewController } from '@ts/grids/grid_core/columns_resizing_reordering/m_columns_resizing_reordering'; -import type { DataController } from '@ts/grids/grid_core/data_controller/data_controller'; +import type { DataFilter } from '@ts/grids/grid_core/data_controller/types'; +import type { DataSourceController } from '@ts/grids/grid_core/data_source/data_source_controller'; import type { EditingController } from '@ts/grids/grid_core/editing/m_editing'; +import type { FilterController } from '@ts/grids/grid_core/filter/filter_controller'; import type { HeaderPanel } from '@ts/grids/grid_core/header_panel/m_header_panel'; import modules from '@ts/grids/grid_core/m_modules'; import type { ModuleType } from '@ts/grids/grid_core/m_types'; @@ -204,7 +206,11 @@ const columnHeadersView = (Base: ModuleType<ColumnHeadersView>) => class ColumnH private _applyFilterViewController!: ApplyFilterViewController; + private dataSourceController!: DataSourceController; + public init() { + this.dataSourceController = this.getController('dataSource'); + super.init(); this._applyFilterViewController = this.getController('applyFilter'); } @@ -545,12 +551,12 @@ const columnHeadersView = (Base: ModuleType<ColumnHeadersView>) => class ColumnH private _renderEditor($editorContainer, options) { $editorContainer.empty(); const $element = $('<div>').appendTo($editorContainer); - const dataSource = this._dataController.dataSource(); + const dataSourceAdapter = this.dataSourceController.getAdapter(); if (options.lookup && this.option('syncLookupFilterValues')) { const filter = this._dataController.getCombinedFilterWithExcludedColumn(options); - const lookupDataSource = gridCoreUtils.getWrappedLookupDataSource(options, dataSource, filter); + const lookupDataSource = gridCoreUtils.getWrappedLookupDataSource(options, dataSourceAdapter, filter); const lookupOptions = { ...options, lookup: { @@ -739,8 +745,7 @@ const columnHeadersView = (Base: ModuleType<ColumnHeadersView>) => class ColumnH } protected _handleDataChanged(e) { - const dataSource = this._dataController?.dataSource?.(); - const lastLoadOptions = dataSource?.lastLoadOptions?.(); + const lastLoadOptions = this.dataSourceController.lastLoadOptions(); // @ts-expect-error super._handleDataChanged.apply(this, arguments); @@ -760,7 +765,7 @@ const columnHeadersView = (Base: ModuleType<ColumnHeadersView>) => class ColumnH } const columns = this._columnsController.getVisibleColumns(); - const dataSource = this._dataController.dataSource(); + const dataSourceAdapter = this.dataSourceController.getAdapter(); const rowIndex = this.element().find(`.${this.addWidgetPrefix(FILTER_ROW_CLASS)}`).index(); if (rowIndex === -1) { @@ -789,7 +794,7 @@ const columnHeadersView = (Base: ModuleType<ColumnHeadersView>) => class ColumnH editor.option('items', [...items, selectedItem]); } - const lookupDataSource = gridCoreUtils.getWrappedLookupDataSource(column, dataSource, filter); + const lookupDataSource = gridCoreUtils.getWrappedLookupDataSource(column, dataSourceAdapter, filter); editor.option('dataSource', lookupDataSource); } } @@ -811,20 +816,20 @@ const columnHeadersView = (Base: ModuleType<ColumnHeadersView>) => class ColumnH } }; -const data = (Base: ModuleType<DataController>) => class DataControllerFilterRowExtender extends Base { +const filterController = ( + Base: ModuleType<FilterController>, +) => class FilterControllerFilterRowExtender extends Base { private skipCalculateColumnFilters() { return false; } - protected calculateAdditionalFilter() { + public getAdditionalFilter(excludedColumn?: Column | null): DataFilter { if (this.skipCalculateColumnFilters()) { - return super.calculateAdditionalFilter(); + return super.getAdditionalFilter(excludedColumn); } - const filters = [super.calculateAdditionalFilter()]; - const columns = this._columnsController.getVisibleColumns(null, true); - - const excludedColumn = this.getFilterExcludedColumn(); + const filters = [super.getAdditionalFilter(excludedColumn)]; + const columns = this.columnsController.getVisibleColumns(null, true); each(columns, function () { const shouldSkip = excludedColumn?.index === this.index; @@ -1046,7 +1051,7 @@ export const filterRowModule = { }, extenders: { controllers: { - data, + filter: filterController, columnsResizer, editing, }, diff --git a/packages/devextreme/js/__internal/grids/grid_core/filter_sync/__tests__/filter_sync.filter_value_edge_cases.test.ts b/packages/devextreme/js/__internal/grids/grid_core/filter_sync/__tests__/filter_sync.filter_value_edge_cases.test.ts new file mode 100644 index 000000000000..a1a293d60d4c --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/filter_sync/__tests__/filter_sync.filter_value_edge_cases.test.ts @@ -0,0 +1,93 @@ +import { + afterEach, + beforeEach, + describe, + expect, + it, +} from '@jest/globals'; +import type { dxElementWrapper } from '@js/core/renderer'; +import type { Properties as DataGridProperties } from '@js/ui/data_grid'; +import type { DataGridModel } from '@ts/grids/data_grid/__tests__/__mock__/model/data_grid'; +import type { DataGridInstance } from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils'; +import { + afterTest, + beforeTest, + createDataGrid, + toPlainFilter, +} from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils'; + +const DATA = [ + { id: 1, name: 'Alex', age: 15 }, + { id: 2, name: 'Dan', age: 20 }, +]; + +const SAVED_STATE_WITHOUT_IDENTIFIER = { + columns: [ + { visibleIndex: 0, dataField: 'name', filterValue: 'Alex' }, + { visibleIndex: 1, filterValue: 42, allowFiltering: false }, + ], +}; + +const createGrid = (options: DataGridProperties): Promise<{ + $container: dxElementWrapper; + component: DataGridModel; + instance: DataGridInstance; +}> => createDataGrid({ dataSource: DATA, ...options }); + +describe('Filter sync filterValue', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + describe('when a condition has only two elements', () => { + it('should filter the data as if the operation were =', async () => { + const { instance } = await createGrid({ + filterPanel: { visible: true }, + filterSyncEnabled: true, + filterValue: ['name', 'Alex'], + columns: ['name', 'age'], + }); + + const names = instance.getVisibleRows().map((row) => (row.data as { name: string }).name); + + expect(toPlainFilter(instance.getCombinedFilter(true))).toEqual(['name', '=', 'Alex']); + expect(names).toEqual(['Alex']); + }); + + it('should fill both the filter row and the header filter of the column', async () => { + const { instance } = await createGrid({ + filterPanel: { visible: true }, + filterSyncEnabled: true, + filterValue: ['name', 'Alex'], + columns: ['name', 'age'], + }); + + expect(instance.columnOption('name', 'filterValue')).toBe('Alex'); + expect(instance.columnOption('name', 'filterValues')).toEqual(['Alex']); + }); + }); + + describe('when a saved column has no identifier', () => { + it('should throw while restoring the state', async () => { + const { instance } = await createGrid({ + filterPanel: { visible: true }, + columns: ['name', 'age'], + stateStoring: { enabled: true, type: 'custom', customLoad: () => Promise.resolve({}) }, + }); + + expect(() => instance.state(SAVED_STATE_WITHOUT_IDENTIFIER)).toThrow(TypeError); + }); + + it('should leave behind a filterValue whose field is undefined', async () => { + const { instance } = await createGrid({ + filterPanel: { visible: true }, + columns: ['name', 'age'], + stateStoring: { enabled: true, type: 'custom', customLoad: () => Promise.resolve({}) }, + }); + + expect(() => instance.state(SAVED_STATE_WITHOUT_IDENTIFIER)).toThrow(TypeError); + expect(instance.option('filterValue')).toEqual([ + ['name', 'contains', 'Alex'], 'and', [undefined, 'contains', 42], + ]); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/filter_sync/extenders/filter_sync_column_headers_view.ts b/packages/devextreme/js/__internal/grids/grid_core/filter_sync/extenders/filter_sync_column_headers_view.ts index c55044878687..ba42c4a876ce 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/filter_sync/extenders/filter_sync_column_headers_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/filter_sync/extenders/filter_sync_column_headers_view.ts @@ -1,15 +1,20 @@ import { filterHasField } from '@ts/filter_builder/m_utils'; import type { ColumnHeadersView } from '@ts/grids/grid_core/column_headers/m_column_headers'; -import type { DataController } from '@ts/grids/grid_core/data_controller/data_controller'; +import type { FilterController } from '@ts/grids/grid_core/filter/filter_controller'; import type { ModuleType, OptionChanged } from '@ts/grids/grid_core/m_types'; import { getColumnIdentifier } from '../utils'; -import type { FilterSyncDataControllerExtension } from './filter_sync_data_controller'; export const filterSyncColumnHeadersViewExtender = ( Base: ModuleType<ColumnHeadersView>, ): ModuleType<ColumnHeadersView> => class ColumnHeadersViewFilterSyncExtender extends Base { - declare protected _dataController: DataController & FilterSyncDataControllerExtension; + private filterController!: FilterController; + + public init(): void { + this.filterController = this.getController('filter'); + + super.init(); + } public optionChanged(args: OptionChanged): void { if (args.name === 'filterValue') { @@ -21,7 +26,7 @@ export const filterSyncColumnHeadersViewExtender = ( } private _isHeaderFilterEmpty(column): boolean { - if (this._dataController.isFilterSyncActive()) { + if (this.filterController.isFilterSyncActive()) { return !filterHasField(this.option('filterValue'), getColumnIdentifier(column)); } @@ -30,6 +35,6 @@ export const filterSyncColumnHeadersViewExtender = ( } private _needUpdateFilterIndicators(): boolean { - return !this._dataController.isFilterSyncActive(); + return !this.filterController.isFilterSyncActive(); } }; diff --git a/packages/devextreme/js/__internal/grids/grid_core/filter_sync/extenders/filter_sync_data_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/filter_sync/extenders/filter_sync_data_controller.ts index f55b1c37f880..053fee3c9810 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/filter_sync/extenders/filter_sync_data_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/filter_sync/extenders/filter_sync_data_controller.ts @@ -1,28 +1,14 @@ import type { DeferredObj } from '@js/core/utils/deferred'; import { Deferred } from '@js/core/utils/deferred'; -import { isDefined } from '@js/core/utils/type'; -import { - getFilterExpression, - removeFieldConditionsFromFilter, -} from '@ts/filter_builder/m_utils'; import type { Column } from '@ts/grids/grid_core/columns_controller/types'; import type { DataController } from '@ts/grids/grid_core/data_controller/data_controller'; -import type { DataFilter } from '@ts/grids/grid_core/data_controller/types'; import { FILTER_TYPES_EXCLUDE } from '@ts/grids/grid_core/filter_sync/const'; import type { FilterSyncController } from '@ts/grids/grid_core/filter_sync/m_filter_sync'; -import { getColumnIdentifier } from '@ts/grids/grid_core/filter_sync/utils'; import type { ModuleType, OptionChanged } from '@ts/grids/grid_core/m_types'; -import gridCoreUtils from '@ts/grids/grid_core/m_utils'; - -export interface FilterSyncDataControllerExtension { - isFilterSyncActive: () => boolean | undefined; -} export const filterSyncDataControllerExtender = ( Base: ModuleType<DataController>, -): ModuleType< - DataController & FilterSyncDataControllerExtension -> => class FilterSyncDataControllerExtender extends Base { +): ModuleType<DataController> => class FilterSyncDataControllerExtender extends Base { private filterSyncController!: FilterSyncController; public init(): void { @@ -36,7 +22,7 @@ export const filterSyncDataControllerExtender = ( case 'filterValue': this.applyFilter(); - if (this.isFilterSyncActive()) { + if (this.filterController.isFilterSyncActive()) { this.filterSyncController.syncFilterValue(); } args.handled = true; @@ -45,7 +31,7 @@ export const filterSyncDataControllerExtender = ( args.handled = true; break; case 'columns': - if (this.isFilterSyncActive()) { + if (this.filterController.isFilterSyncActive()) { const column: Column = this._columnsController.getColumnByPath(args.fullName); if (column && !this.filterSyncController.isSyncingColumnOptions()) { @@ -66,50 +52,6 @@ export const filterSyncDataControllerExtender = ( } } - public isFilterSyncActive(): boolean | undefined { - const filterSyncEnabledValue = this.option('filterSyncEnabled'); - return filterSyncEnabledValue === 'auto' ? this.option('filterPanel.visible') : filterSyncEnabledValue; - } - - private skipCalculateColumnFilters(): boolean | undefined { - const hasFilterValueOrSyncing = isDefined(this.option('filterValue')) - || this.filterSyncController.isSyncingColumnOptions(); - - return hasFilterValueOrSyncing && this.isFilterSyncActive(); - } - - protected calculateAdditionalFilter(): DataFilter { - const columns = this._columnsController.getFilteringColumns(); - const isFilterValueDisabled = this.option('filterPanel.filterEnabled') === false; - - if (!columns?.length || isFilterValueDisabled) { - return super.calculateAdditionalFilter(); - } - - const filters = [super.calculateAdditionalFilter()]; - let filterValue = this.option('filterValue'); - - if (this.isFilterSyncActive()) { - const excludedColumn = this.getFilterExcludedColumn(); - - if (isDefined(excludedColumn) && filterValue) { - filterValue = removeFieldConditionsFromFilter( - filterValue, - getColumnIdentifier(excludedColumn), - ); - } - } - - const customOperations = this.filterSyncController.getCustomFilterOperations(); - const calculatedFilterValue: DataFilter = getFilterExpression(filterValue, columns, customOperations, 'filterBuilder'); - - if (calculatedFilterValue) { - filters.push(calculatedFilterValue); - } - - return gridCoreUtils.combineFilters(filters); - } - private parseColumnPropertyName(fullName: string): string | null { const matched = /.*\.(.*)/.exec(fullName); diff --git a/packages/devextreme/js/__internal/grids/grid_core/filter_sync/extenders/filter_sync_filter_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/filter_sync/extenders/filter_sync_filter_controller.ts new file mode 100644 index 000000000000..2f7eaacf733a --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/filter_sync/extenders/filter_sync_filter_controller.ts @@ -0,0 +1,59 @@ +import { isDefined } from '@js/core/utils/type'; +import { + getFilterExpression, + removeFieldConditionsFromFilter, +} from '@ts/filter_builder/m_utils'; +import type { Column } from '@ts/grids/grid_core/columns_controller/types'; +import type { DataFilter } from '@ts/grids/grid_core/data_controller/types'; +import type { FilterController } from '@ts/grids/grid_core/filter/filter_controller'; +import type { FilterSyncController } from '@ts/grids/grid_core/filter_sync/m_filter_sync'; +import { getColumnIdentifier } from '@ts/grids/grid_core/filter_sync/utils'; +import type { ModuleType } from '@ts/grids/grid_core/m_types'; +import gridCoreUtils from '@ts/grids/grid_core/m_utils'; + +export const filterSyncFilterControllerExtender = ( + Base: ModuleType<FilterController>, +): ModuleType<FilterController> => class FilterControllerFilterSyncExtender extends Base { + private filterSyncController!: FilterSyncController; + + public init(): void { + this.filterSyncController = this.getController('filterSync'); + + super.init(); + } + + private skipCalculateColumnFilters(): boolean | undefined { + const hasFilterValueOrSyncing = isDefined(this.option('filterValue')) + || this.filterSyncController.isSyncingColumnOptions(); + + return hasFilterValueOrSyncing && this.isFilterSyncActive(); + } + + public getAdditionalFilter(excludedColumn?: Column | null): DataFilter { + const columns = this.columnsController.getFilteringColumns(); + const isFilterValueDisabled = this.option('filterPanel.filterEnabled') === false; + + if (!columns?.length || isFilterValueDisabled) { + return super.getAdditionalFilter(excludedColumn); + } + + const filters = [super.getAdditionalFilter(excludedColumn)]; + let filterValue = this.option('filterValue'); + + if (this.isFilterSyncActive() && isDefined(excludedColumn) && filterValue) { + filterValue = removeFieldConditionsFromFilter( + filterValue, + getColumnIdentifier(excludedColumn), + ); + } + + const customOperations = this.filterSyncController.getCustomFilterOperations(); + const calculatedFilterValue: DataFilter = getFilterExpression(filterValue, columns, customOperations, 'filterBuilder'); + + if (calculatedFilterValue) { + filters.push(calculatedFilterValue); + } + + return gridCoreUtils.combineFilters(filters); + } +}; diff --git a/packages/devextreme/js/__internal/grids/grid_core/filter_sync/filter_sync_module.ts b/packages/devextreme/js/__internal/grids/grid_core/filter_sync/filter_sync_module.ts index dc3ee7dd096f..64b53f5f3a5d 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/filter_sync/filter_sync_module.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/filter_sync/filter_sync_module.ts @@ -2,6 +2,7 @@ import type { InternalGridOptions } from '@ts/grids/grid_core/m_types'; import { filterSyncColumnHeadersViewExtender } from './extenders/filter_sync_column_headers_view'; import { filterSyncDataControllerExtender } from './extenders/filter_sync_data_controller'; +import { filterSyncFilterControllerExtender } from './extenders/filter_sync_filter_controller'; import { FilterSyncController } from './m_filter_sync'; export const filterSyncModule = { @@ -17,6 +18,7 @@ export const filterSyncModule = { extenders: { controllers: { data: filterSyncDataControllerExtender, + filter: filterSyncFilterControllerExtender, }, views: { columnHeadersView: filterSyncColumnHeadersViewExtender, diff --git a/packages/devextreme/js/__internal/grids/grid_core/filter_sync/m_filter_sync.ts b/packages/devextreme/js/__internal/grids/grid_core/filter_sync/m_filter_sync.ts index 426b8c33f8ff..c93a7ff34294 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/filter_sync/m_filter_sync.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/filter_sync/m_filter_sync.ts @@ -11,11 +11,9 @@ import type { } from '@ts/grids/grid_core/columns_controller/types'; import type { DataController } from '@ts/grids/grid_core/data_controller/data_controller'; import type { FilterValue, FilterValueCondition } from '@ts/grids/grid_core/data_controller/types'; +import type { FilterController } from '@ts/grids/grid_core/filter/filter_controller'; import modules from '@ts/grids/grid_core/m_modules'; -import type { - FilterSyncDataControllerExtension, -} from './extenders/filter_sync_data_controller'; import { anyOf, noneOf } from './m_filter_custom_operations'; import { checkForErrors, @@ -31,15 +29,18 @@ import { export class FilterSyncController extends modules.Controller { private skipSyncColumnOptions = false; - private dataController!: DataController & FilterSyncDataControllerExtension; + private dataController!: DataController; private columnsController!: ColumnsController; + private filterController!: FilterController; + public init(): void { - this.dataController = this.getController('data') as DataController & FilterSyncDataControllerExtension; + this.dataController = this.getController('data'); this.columnsController = this.getController('columns'); + this.filterController = this.getController('filter'); - if (this.dataController.isFilterSyncActive()) { + if (this.filterController.isFilterSyncActive()) { if (this.columnsController.isAllDataTypesDefined()) { this.initSync(); } else { @@ -132,7 +133,7 @@ export class FilterSyncController extends modules.Controller { public getFilterValueFromColumns( columns: ColumnUserState[] | undefined, ): FilterValue { - if (!this.dataController.isFilterSyncActive()) { + if (!this.filterController.isFilterSyncActive()) { return null; } diff --git a/packages/devextreme/js/__internal/grids/grid_core/focus/__tests__/focus.integration.test.ts b/packages/devextreme/js/__internal/grids/grid_core/focus/__tests__/focus.integration.test.ts new file mode 100644 index 000000000000..0fa8c9bfa5d8 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/focus/__tests__/focus.integration.test.ts @@ -0,0 +1,70 @@ +import { + afterEach, beforeEach, describe, expect, it, +} from '@jest/globals'; +import type { Properties as DataGridProperties } from '@js/ui/data_grid'; + +import { + afterTest, + beforeTest, + createDataGrid, + flushAsync, +} from '../../__tests__/__mock__/helpers/utils'; + +const DATA = [ + { id: 1, name: 'Alex' }, + { id: 2, name: 'Dan' }, +]; + +const setup = async (options: DataGridProperties) => { + const { instance } = await createDataGrid({ + keyExpr: 'id', + focusedRowEnabled: true, + ...options, + }); + + await flushAsync(); + + return { + columnsController: instance.getController('columns'), + dataSourceController: instance.getController('dataSource'), + }; +}; + +describe('Focus columns extender', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + describe('getSortDataSourceParameters', () => { + it('sorts by the data index when every operation is local', async () => { + const { columnsController, dataSourceController } = await setup({ dataSource: DATA }); + + const result = columnsController.getSortDataSourceParameters(); + + expect(result).toHaveLength(1); + expect(result[0].selector).toBe(dataSourceController.getDataIndexGetter()); + expect(result[0].desc).toBe(false); + }); + + it('sorts by the key columns when operations are remote', async () => { + const { columnsController } = await setup({ + dataSource: DATA, + remoteOperations: true, + }); + + const result = columnsController.getSortDataSourceParameters(); + + expect(result).toEqual([{ selector: 'id', desc: false }]); + }); + + it('leaves the base sort parameters alone when there is no data source', async () => { + const { columnsController, dataSourceController } = await setup({ + columns: [{ dataField: 'name', sortOrder: 'asc' }], + }); + + expect(dataSourceController.getAdapter()).toBeNull(); + expect(columnsController.getSortDataSourceParameters()).toEqual([ + { selector: 'name', desc: false }, + ]); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/focus/m_focus.ts b/packages/devextreme/js/__internal/grids/grid_core/focus/m_focus.ts index fb91bbd9d68d..91bc8a1cc790 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/focus/m_focus.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/focus/m_focus.ts @@ -5,6 +5,7 @@ import { Deferred, type DeferredObj, when } from '@js/core/utils/deferred'; import { each } from '@js/core/utils/iterator'; import { isBoolean, isDefined } from '@js/core/utils/type'; import type { StoreChange } from '@js/data/store'; +import type { DataSourceController } from '@ts/grids/grid_core/data_source/data_source_controller'; import type { Key } from '@ts/grids/new/grid_core/data_controller/types'; import type { ColumnsController } from '../columns_controller/m_columns_controller'; @@ -39,6 +40,10 @@ export class FocusController extends core.ViewController { return this.getController('data'); } + private getDataSourceController(): DataSourceController { + return this.getController('dataSource'); + } + public init() { this.component._optionsByReference.focusedRowKey = true; } @@ -213,7 +218,7 @@ export class FocusController extends core.ViewController { const d = new Deferred(); const rowsView = this.getView('rowsView'); - if (key === undefined || !this.getDataController().dataSource()) { + if (key === undefined || !this.getDataSourceController().hasAdapter()) { return d.reject().promise(); } @@ -534,8 +539,11 @@ const focusEditorFactoryViewControllerExtender = ( const columns = (Base: ModuleType<ColumnsController>) => class FocusColumnsExtender extends Base { protected focusController!: FocusController; + protected dataSourceController!: DataSourceController; + public init(isApplyingUserState?: boolean): void { this.focusController = this.getController('focus'); + this.dataSourceController = this.getController('dataSource'); super.init(isApplyingUserState); } @@ -543,9 +551,8 @@ const columns = (Base: ModuleType<ColumnsController>) => class FocusColumnsExten public getSortDataSourceParameters(_, sortByKey?) { // @ts-expect-error let result = super.getSortDataSourceParameters.apply(this, arguments); - const dataSource = this._dataController._dataSource; - let key = dataSource?.store()?.key(); - const remoteOperations = dataSource && dataSource.remoteOperations() || {}; + let key = this.dataSourceController.store()?.key(); + const remoteOperations = this.dataSourceController.remoteOperations(); const isLocalOperations = Object.keys(remoteOperations).every((operationName) => !remoteOperations[operationName]); if (key && (this.option('focusedRowEnabled') && this.focusController.isAutoNavigateToFocusedRow() !== false || sortByKey)) { @@ -555,7 +562,7 @@ const columns = (Base: ModuleType<ColumnsController>) => class FocusColumnsExten if (notSortedKeys.length) { result = result || []; if (isLocalOperations) { - result.push({ selector: dataSource?.getDataIndexGetter(), desc: false }); + result.push({ selector: this.dataSourceController.getDataIndexGetter(), desc: false }); } else { notSortedKeys.forEach((notSortedKey) => result.push({ selector: notSortedKey, desc: false })); } @@ -931,7 +938,7 @@ const rowsView = (Base: ModuleType<RowsView>) => class RowsViewFocusController e super._checkRowKeys.apply(this, arguments); if (this.option('focusedRowEnabled') && this.option('dataSource')) { - const store = this._dataController.store(); + const store = this.dataSourceController.store(); if (store && !store.key()) { this._dataController.fireError('E1042', 'Row focusing'); } @@ -965,8 +972,7 @@ const rowsView = (Base: ModuleType<RowsView>) => class RowsViewFocusController e let columnIndex = this.option('focusedColumnIndex')!; const $row = this._findRowElementForTabIndex(); - const dataSource = this._dataController.dataSource(); - const operationTypes = dataSource?.operationTypes(); + const operationTypes = this.dataSourceController.operationTypes(); const isPaging = !operationTypes || operationTypes.paging; if (!isDefined(this._scrollToFocusOnResize)) { diff --git a/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts b/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts index 10d151fa7380..b0d69199395c 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts @@ -15,7 +15,11 @@ import { isDefined, isFunction, isObject } from '@js/core/utils/type'; import { restoreFocus, saveFocusedElementInfo } from '@js/ui/shared/accessibility'; import filterUtils from '@js/ui/shared/filtering'; import type { ColumnHeadersView } from '@ts/grids/grid_core/column_headers/m_column_headers'; +import type { Column } from '@ts/grids/grid_core/columns_controller/types'; import type { DataController } from '@ts/grids/grid_core/data_controller/data_controller'; +import type { DataFilter } from '@ts/grids/grid_core/data_controller/types'; +import type { DataSourceController } from '@ts/grids/grid_core/data_source/data_source_controller'; +import type { FilterController } from '@ts/grids/grid_core/filter/filter_controller'; import type { HeaderPanel } from '@ts/grids/grid_core/header_panel/m_header_panel'; import Modules from '@ts/grids/grid_core/m_modules'; import type { ModuleType } from '@ts/grids/grid_core/m_types'; @@ -79,13 +83,11 @@ function ungroupUTCDates(items, dateParts?, dates?) { export function convertDataFromUTCToLocal(data, column) { const dates = ungroupUTCDates(data); - // @ts-expect-error const query = dataQuery(dates); const group = gridCoreUtils.getHeaderFilterGroupParameters({ ...column, calculateCellValue: (date) => date, }); - // @ts-expect-error return storeHelper.queryByOptions(query, { group }).toArray(); } @@ -122,11 +124,14 @@ export class HeaderFilterController extends Modules.ViewController { private _dataController!: DataController; + private dataSourceController!: DataSourceController; + private _headerFilterView!: HeaderFilterView; public init() { this._columnsController = this.getController('columns'); this._dataController = this.getController('data'); + this.dataSourceController = this.getController('dataSource'); this._headerFilterView = this.getView('headerFilterView'); } @@ -232,8 +237,8 @@ export class HeaderFilterController extends Modules.ViewController { } private getDataSource(column) { - const dataSource = this._dataController.dataSource(); - const remoteGrouping = dataSource?.remoteOperations().grouping; + const dataSourceAdapter = this.dataSourceController.getAdapter(); + const remoteGrouping = this.dataSourceController.remoteOperations().grouping; const group = gridCoreUtils.getHeaderFilterGroupParameters(column, remoteGrouping); const headerFilterDataSource = column.headerFilter?.dataSource; const headerFilterOptions = this.option('headerFilter'); @@ -242,10 +247,9 @@ export class HeaderFilterController extends Modules.ViewController { component: this.component, }; - if (!dataSource) return; + if (!dataSourceAdapter) return; if (isDefined(headerFilterDataSource) && !isFunction(headerFilterDataSource)) { - // @ts-expect-error options.dataSource = normalizeDataSourceOptions(headerFilterDataSource); } else if (column.lookup) { isLookup = true; @@ -253,7 +257,7 @@ export class HeaderFilterController extends Modules.ViewController { if (this.option('syncLookupFilterValues')) { const filter = this._dataController.getCombinedFilterWithExcludedColumn(column); - options.dataSource = gridCoreUtils.getWrappedLookupDataSource(column, dataSource, filter); + options.dataSource = gridCoreUtils.getWrappedLookupDataSource(column, dataSourceAdapter, filter); } else { options.dataSource = gridCoreUtils.normalizeLookupDataSource(column.lookup); } @@ -272,9 +276,10 @@ export class HeaderFilterController extends Modules.ViewController { // TODO remove in 16.1 options.dataField = column.dataField || column.name; - dataSource.customLoader.load(options).done(({ data }) => { + dataSourceAdapter.customLoader.load(options).done(({ data }) => { const convertUTCDates = remoteGrouping && isUTCFormat(column.serializationFormat) && cutoffLevel > 3; if (convertUTCDates) { + // @ts-expect-error queryByOptions().toArray() is typed as unknown[] data = convertDataFromUTCToLocal(data, column); } that._processGroupItems(data, null, null, { @@ -351,8 +356,7 @@ export class HeaderFilterController extends Modules.ViewController { if (column) { const groupInterval = filterUtils.getGroupInterval(column); - const dataSource = that._dataController.dataSource(); - const remoteFiltering = dataSource && dataSource.remoteOperations().filtering; + const remoteFiltering = that.dataSourceController.remoteOperations().filtering; const previousOnHidden = options.onHidden; extend(options, column, { @@ -493,20 +497,20 @@ export function invertFilterExpression(filter) { return ['!', filter]; } -const data = (Base: ModuleType<DataController>) => class DataControllerFilterRowExtender extends Base { +const filterController = ( + Base: ModuleType<FilterController>, +) => class FilterControllerHeaderFilterExtender extends Base { private skipCalculateColumnFilters() { return false; } - protected calculateAdditionalFilter() { + public getAdditionalFilter(excludedColumn?: Column | null): DataFilter { if (this.skipCalculateColumnFilters()) { - return super.calculateAdditionalFilter(); + return super.getAdditionalFilter(excludedColumn); } - const that = this; - const filters = [super.calculateAdditionalFilter()]; - const columns = that._columnsController.getVisibleColumns(null, true); - const excludedColumn = this.getFilterExcludedColumn(); + const filters = [super.getAdditionalFilter(excludedColumn)]; + const columns = this.columnsController.getVisibleColumns(null, true); each(columns, (_, column) => { let filter; @@ -576,7 +580,7 @@ export const headerFilterModule = { }, extenders: { controllers: { - data, + filter: filterController, }, views: { columnHeadersView, diff --git a/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_keyboard_navigation.ts b/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_keyboard_navigation.ts index cb1e1d542bdb..9564e9d39939 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_keyboard_navigation.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_keyboard_navigation.ts @@ -28,6 +28,7 @@ import type { AdaptiveColumnsController } from '@ts/grids/grid_core/adaptivity/m import type { Column } from '@ts/grids/grid_core/columns_controller/types'; import type { DataController } from '@ts/grids/grid_core/data_controller/data_controller'; import type { RowIndexCorrection } from '@ts/grids/grid_core/data_controller/types'; +import type { DataSourceController } from '@ts/grids/grid_core/data_source/data_source_controller'; import type { EditingController } from '@ts/grids/grid_core/editing/m_editing'; import type { RowsView } from '@ts/grids/grid_core/views/m_rows_view'; import type { RowsViewScrollEvent } from '@ts/grids/grid_core/views/types'; @@ -131,6 +132,8 @@ export class KeyboardNavigationController extends KeyboardNavigationControllerCo protected _dataController!: DataController & Partial<VirtualScrollingDataControllerExtension>; + protected dataSourceController!: DataSourceController; + private _selectionController!: Controllers['selection']; protected _editingController!: Controllers['editing']; @@ -152,6 +155,7 @@ export class KeyboardNavigationController extends KeyboardNavigationControllerCo // #region Initialization public init() { this._dataController = this.getController('data'); + this.dataSourceController = this.getController('dataSource'); this._selectionController = this.getController('selection'); this._editingController = this.getController('editing'); this._editorFactory = this.getController('editorFactory'); @@ -704,7 +708,6 @@ export class KeyboardNavigationController extends KeyboardNavigationControllerCo const $row = this._focusedView && this._focusedView.getRow(visibleRowIndex); const $event = eventArgs.originalEvent; const isUpArrow = eventArgs.keyName === 'upArrow'; - const dataSource = this._dataController.dataSource(); const isRowEditingInCurrentRow = this._editingController?.isEditRowByIndex?.(visibleRowIndex); const isEditingNavigationMode = this._isFastEditingStarted(); const isInsideMasterDetail = this.isInsideMasterDetail($($event?.target)); @@ -719,8 +722,8 @@ export class KeyboardNavigationController extends KeyboardNavigationControllerCo if ( this._isVirtualRowRender() && isUpArrow - && dataSource - && !dataSource.isLoading() + && this.dataSourceController.hasAdapter() + && !this.dataSourceController.isLoading() ) { const rowHeight = getOuterHeight($row); const rowIndex = this._focusedCellPosition.rowIndex - 1; @@ -2588,7 +2591,7 @@ export class KeyboardNavigationController extends KeyboardNavigationControllerCo private _fireFocusedRowChanging(eventArgs: any, $newFocusedRow: dxElementWrapper) { const newRowIndex = this._getRowIndex($newFocusedRow); const prevFocusedRowIndex = this.option('focusedRowIndex'); - const loadingOperationTypes = this._dataController.loadingOperationTypes(); + const loadingOperationTypes = this.dataSourceController.loadingOperationTypes(); const args: any = { rowElement: $newFocusedRow, diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_types.ts b/packages/devextreme/js/__internal/grids/grid_core/m_types.ts index 662de74a6193..9204705e55f0 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_types.ts @@ -203,12 +203,14 @@ export interface Controllers { columnsResizer: import('./columns_resizing_reordering/m_columns_resizing_reordering').ColumnsResizerViewController; contextMenu: import('./context_menu/m_context_menu').ContextMenuController; data: import('./data_controller/data_controller').DataController; + dataSource: import('./data_source/data_source_controller').DataSourceController; draggingHeader: import('./columns_resizing_reordering/m_columns_resizing_reordering').DraggingHeaderViewController; // todo: export is dataGrid-only controller editing: import('./editing/m_editing').EditingController; editorFactory: import('./editor_factory/m_editor_factory').EditorFactory; errorHandling: import('./error_handling/error_handling_view_controller').ErrorHandlingViewController; export: import('../data_grid/export/m_export').ExportController; + filter: import('./filter/filter_controller').FilterController; filterSync: import('./filter_sync/m_filter_sync').FilterSyncController; focus: import('./focus/m_focus').FocusController; headerFilter: import('./header_filter/m_header_filter').HeaderFilterController; diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index ae057d8d7009..971de5eadb7f 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -674,11 +674,10 @@ export default { lookupDataSourceOptions = lookupDataSourceOptions({}); } } - // @ts-expect-error return normalizeDataSourceOptions(lookupDataSourceOptions); }, - getWrappedLookupDataSource(column, dataSource: DataSourceAdapter | undefined, filter) { + getWrappedLookupDataSource(column, dataSource: DataSourceAdapter | null | undefined, filter) { if (!dataSource) { return []; } diff --git a/packages/devextreme/js/__internal/grids/grid_core/master_detail/__tests__/get_row_indices_for_expand.test.ts b/packages/devextreme/js/__internal/grids/grid_core/master_detail/__tests__/get_row_indices_for_expand.test.ts new file mode 100644 index 000000000000..d54dad4f1b78 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/master_detail/__tests__/get_row_indices_for_expand.test.ts @@ -0,0 +1,47 @@ +import { + afterEach, beforeEach, describe, expect, it, +} from '@jest/globals'; +import { + afterTest, beforeTest, createDataGrid, flushAsync, +} from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils'; +import type { DataController } from '@ts/grids/grid_core/data_controller/data_controller'; +import type { MasterDetailDataControllerExtension } from '@ts/grids/grid_core/master_detail/m_master_detail'; +import { + afterTest as afterTreeListTest, + beforeTest as beforeTreeListTest, + createTreeList, +} from '@ts/grids/tree_list/__tests__/__mock__/helpers/utils'; + +type MasterDetailDataController = DataController & MasterDetailDataControllerExtension; + +const DATA = [{ id: 1 }, { id: 2 }, { id: 3 }]; + +describe('DataController getRowIndicesForExpand', () => { + describe('DataGrid', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + it('should reach master_detail through the adaptivity chain', async () => { + const { instance } = await createDataGrid({ dataSource: DATA }); + await flushAsync(); + const dataController = instance.getController('data') as MasterDetailDataController; + + expect(dataController.getRowIndicesForExpand(1)).toEqual([0, 1]); + expect(dataController.getRowIndicesForExpand(3)).toEqual([2, 3]); + }); + }); + + describe('TreeList', () => { + beforeEach(beforeTreeListTest); + afterEach(afterTreeListTest); + + it('should reach master_detail through the adaptivity chain', async () => { + const { instance } = await createTreeList({ dataSource: DATA }); + await flushAsync(); + const dataController = instance.getController('data') as MasterDetailDataController; + + expect(dataController.getRowIndicesForExpand(1)).toEqual([0, 1]); + expect(dataController.getRowIndicesForExpand(3)).toEqual([2, 3]); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/master_detail/m_master_detail.ts b/packages/devextreme/js/__internal/grids/grid_core/master_detail/m_master_detail.ts index 2ab0f0fdebca..2be2bbe8aad4 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/master_detail/m_master_detail.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/master_detail/m_master_detail.ts @@ -44,7 +44,14 @@ const initMasterDetail = function (that) { that._isExpandAll = that.option('masterDetail.autoExpandAll'); }; -export const dataMasterDetailExtenderMixin = (Base: ModuleType<DataController>) => class DataMasterDetailExtender extends Base { +export interface MasterDetailDataControllerExtension { + // eslint-disable-next-line @typescript-eslint/method-signature-style + getRowIndicesForExpand(key: RowKey): number[]; +} + +export const dataMasterDetailExtenderMixin = ( + Base: ModuleType<DataController>, +): ModuleType<DataController & MasterDetailDataControllerExtension> => class DataMasterDetailExtender extends Base { private _isExpandAll: any; private _expandedItems: any; @@ -98,7 +105,7 @@ export const dataMasterDetailExtenderMixin = (Base: ModuleType<DataController>) return !!(that._isExpandAll ^ (expandIndex >= 0 && that._expandedItems[expandIndex].visible)); } - protected getRowIndicesForExpand(key: RowKey): number[] { + public getRowIndicesForExpand(key: RowKey): number[] { const rowIndex = this.getRowIndexByKey(key); return [rowIndex, rowIndex + 1]; diff --git a/packages/devextreme/js/__internal/grids/grid_core/search/__tests__/search_data_controller.user_state.test.ts b/packages/devextreme/js/__internal/grids/grid_core/search/__tests__/search_data_controller.user_state.test.ts new file mode 100644 index 000000000000..30c939574e2a --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/search/__tests__/search_data_controller.user_state.test.ts @@ -0,0 +1,27 @@ +import { + afterEach, beforeEach, describe, expect, it, +} from '@jest/globals'; + +import { + afterTest, + beforeTest, + createDataGrid, +} from '../../__tests__/__mock__/helpers/utils'; + +describe('Search data controller user state', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + it('should include the search panel text', async () => { + const { instance } = await createDataGrid({ + dataSource: [], + searchPanel: { text: 'Alex' }, + }); + + expect(instance.getController('data').getUserState()).toEqual({ + searchText: 'Alex', + pageIndex: 0, + pageSize: 20, + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts b/packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts index f35045af3838..796df8e7148a 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts @@ -12,7 +12,8 @@ import type { Column } from '@ts/grids/grid_core/columns_controller/types'; import type { ToolbarItem } from '@ts/grids/new/grid_core/toolbar/types'; import type { DataController } from '../data_controller/data_controller'; -import type { DataFilter } from '../data_controller/types'; +import type { DataFilter, UserState } from '../data_controller/types'; +import type { FilterController } from '../filter/filter_controller'; import type { HeaderPanel } from '../header_panel/m_header_panel'; import modules from '../m_modules'; import type { ModuleType, OptionChanged } from '../m_types'; @@ -62,23 +63,34 @@ const dataController = ( return super.publicMethods().concat(['searchByText']); } - protected calculateAdditionalFilter(): DataFilter { - const dataSource = this.getDataSource(); - const langParams = dataSource?.loadOptions?.()?.langParams; - - const filter = super.calculateAdditionalFilter(); - const searchFilter = this.calculateSearchFilter(this.option('searchPanel.text'), langParams); - - return gridCoreUtils.combineFilters([filter, searchFilter]); + public getUserState(): UserState { + return { + ...super.getUserState(), + searchText: this.option('searchPanel.text'), + }; } public searchByText(text: string | undefined): void { this.option('searchPanel.text', text); } +}; + +const filterController = ( + base: ModuleType<FilterController>, +) => class FilterControllerSearchExtender extends base { + public getAdditionalFilter(excludedColumn?: Column | null): DataFilter { + const filter = super.getAdditionalFilter(excludedColumn); + const searchFilter = this.calculateSearchFilter( + this.option('searchPanel.text'), + this.getLangParams(), + ); + + return gridCoreUtils.combineFilters([filter, searchFilter]); + } private calculateSearchFilter(text: string | undefined, langParams?: LangParams): DataFilter { let column; - const columns = this._columnsController.getColumns(); + const columns = this.columnsController.getColumns(); const searchVisibleColumnsOnly = this.option('searchPanel.searchVisibleColumnsOnly'); let lookup; const filters: any[] = []; @@ -106,9 +118,7 @@ const dataController = ( const filterValue = parseValue(column, text); if (lookup?.items) { - // @ts-expect-error dataQuery(lookup.items, { langParams }) - // @ts-expect-error .filter( column.createFilterExpression.call( { @@ -269,7 +279,7 @@ const rowsView = ( private _getStringNormalizer() { const isCaseSensitive = this.option('searchPanel.highlightCaseSensitive'); - const dataSource = this._dataController?.getDataSource?.(); + const dataSource = this.dataSourceController.getDataSource(); const langParams = dataSource?.loadOptions?.()?.langParams; return (str: string): string => toComparable(str, isCaseSensitive, langParams); @@ -421,6 +431,7 @@ export const searchModule = { extenders: { controllers: { data: dataController, + filter: filterController, }, views: { rowsView, diff --git a/packages/devextreme/js/__internal/grids/grid_core/selection/m_selection.ts b/packages/devextreme/js/__internal/grids/grid_core/selection/m_selection.ts index 3af9873058e0..c3b0f1b197ad 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/selection/m_selection.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/selection/m_selection.ts @@ -27,6 +27,7 @@ import type { SelectionChangeEvent, SelectionFilter, SelectionOptions } from '@t import type { DataController } from '../data_controller/data_controller'; import type { DataChange } from '../data_controller/types'; +import type { DataSourceController } from '../data_source/data_source_controller'; import { isEditRow } from '../keyboard_navigation/utils'; import modules from '../m_modules'; import gridCoreUtils from '../m_utils'; @@ -71,8 +72,7 @@ const processLongTap = function (that, dxEvent) { const isSeveralRowsSelected = function (that, selectionFilter) { let keyIndex = 0; - const store = that._dataController.store(); - const key = store?.key(); + const key = that.dataSourceController.store()?.key(); const isComplexKey = Array.isArray(key); if (!selectionFilter.length) { @@ -120,6 +120,8 @@ const selectionHeaderTemplate = (container, options) => { export class SelectionController extends modules.Controller { protected _dataController!: DataController; + protected dataSourceController!: DataSourceController; + private _columnsController!: ColumnsController; protected _stateStoringController!: StateStoringController; @@ -144,6 +146,7 @@ export class SelectionController extends modules.Controller { } this._dataController = this.getController('data'); + this.dataSourceController = this.getController('dataSource'); this._columnsController = this.getController('columns'); this._stateStoringController = this.getController('stateStoring'); // mode has a default value @@ -206,6 +209,7 @@ export class SelectionController extends modules.Controller { */ protected _getSelectionConfig(): SelectionOptions { const dataController = this._dataController; + const { dataSourceController } = this; const columnsController = this._columnsController; const selectionOptions: any = this.option('selection') ?? {}; const { deferred } = selectionOptions; @@ -230,17 +234,17 @@ export class SelectionController extends modules.Controller { return virtualPaging && !legacyScrollingMode && !hasGroupColumns && allowSelectAll && !deferred; }, key() { - return dataController?.key(); + return dataSourceController.key(); }, keyOf(item) { - return dataController?.keyOf(item); + return dataSourceController.keyOf(item); }, dataFields() { - return dataController.dataSource()?.select(); + return dataSourceController.select(); }, load(options) { // @ts-expect-error - return dataController.dataSource()?.customLoader.load(options) || new Deferred().resolve([]); + return dataSourceController.getAdapter()?.customLoader.load(options) || new Deferred().resolve([]); }, // eslint-disable-next-line plainItems(cached?) { @@ -261,7 +265,7 @@ export class SelectionController extends modules.Controller { }, totalCount: () => dataController.totalCount(), getLoadOptions(loadItemIndex, focusedItemIndex, shiftItemIndex) { - const { sort, filter } = dataController.dataSource()?.lastLoadOptions() ?? {}; + const { sort, filter } = dataSourceController.lastLoadOptions(); let minIndex = Math.min(loadItemIndex, focusedItemIndex); let maxIndex = Math.max(loadItemIndex, focusedItemIndex); diff --git a/packages/devextreme/js/__internal/grids/grid_core/state_storing/m_state_storing.ts b/packages/devextreme/js/__internal/grids/grid_core/state_storing/m_state_storing.ts index a9562c531e4a..eafb2d17ae80 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/state_storing/m_state_storing.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/state_storing/m_state_storing.ts @@ -13,7 +13,7 @@ export const rowsView = (Base: ModuleType<RowsView>) => class StateStoringRowsVi super.init(); this._dataController.stateLoaded.add(() => { - if (this._dataController.isLoaded() && !this._dataController.getDataSource()) { + if (this._dataController.isLoaded() && !this.dataSourceController.getDataSource()) { this.setLoading(false); this.renderNoDataText(); // TODO getView diff --git a/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts b/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts index 1c2e43f2909b..1b2b53961b65 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts @@ -767,7 +767,7 @@ export const validatingEditingExtender = (Base: ModuleType<EditingController>) = const { key } = change; each(items, (i, item) => { - if (equalByValue(key, isInsert ? item.key : this._dataController.keyOf(item))) { + if (equalByValue(key, isInsert ? item.key : this.dataSourceController.keyOf(item))) { index = i; return false; } diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/a11y_status_container_component.ts b/packages/devextreme/js/__internal/grids/grid_core/views/a11y_status_container_component.ts index 875af6837f7d..824b1e86c46c 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/a11y_status_container_component.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/a11y_status_container_component.ts @@ -1,8 +1,9 @@ import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; +import { SCREEN_READER_ONLY_CLASS } from '@ts/core/widget/widget'; const CLASSES = { - container: 'dx-gridbase-a11y-status-container', + container: SCREEN_READER_ONLY_CLASS, }; export interface A11yStatusContainerComponentProps { diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/m_rows_view.ts b/packages/devextreme/js/__internal/grids/grid_core/views/m_rows_view.ts index 144746a8bfa7..61afaf522a24 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/m_rows_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/m_rows_view.ts @@ -22,6 +22,7 @@ import type { ColumnsResizerViewController, } from '@ts/grids/grid_core/columns_resizing_reordering/m_columns_resizing_reordering'; import { generateRowValues } from '@ts/grids/grid_core/data_controller/utils/row_values'; +import type { DataSourceController } from '@ts/grids/grid_core/data_source/data_source_controller'; import type { FocusController } from '@ts/grids/grid_core/focus/m_focus'; import type { KeyboardNavigationController } from '@ts/grids/grid_core/keyboard_navigation/m_keyboard_navigation'; import type { ValidatingController } from '@ts/grids/grid_core/validating/m_validating'; @@ -104,6 +105,8 @@ export class RowsView extends ColumnsView { protected _validatingController!: ValidatingController; + protected dataSourceController!: DataSourceController; + public _columnHeadersView!: ColumnHeadersView; public _hasHeight: boolean | undefined; @@ -135,6 +138,7 @@ export class RowsView extends ColumnsView { this._focusController = this.getController('focus'); this._keyboardNavigationController = this.getController('keyboardNavigation'); this._validatingController = this.getController('validating'); + this.dataSourceController = this.getController('dataSource'); this._columnHeadersView = this.getView('columnHeadersView'); this._rowHeight = 0; this._scrollTop = 0; @@ -353,7 +357,7 @@ export class RowsView extends ColumnsView { $element.append('<div>'); } if (force || !that._loadPanel) { - that._renderLoadPanel($element, $element.parent(), isLocalStore(that._dataController.store())); + that._renderLoadPanel($element, $element.parent(), isLocalStore(that.dataSourceController.store())); } if ((force || !that.getScrollable()) && that._dataController.isLoaded()) { @@ -578,7 +582,7 @@ export class RowsView extends ColumnsView { protected _checkRowKeys(options) { const that = this; const rows = that._getRows(options); - const keyExpr = that._dataController.store()?.key(); + const keyExpr = that.dataSourceController.store()?.key(); keyExpr && rows.some((row) => { if (row.rowType === 'data' && row.key === undefined) { @@ -1236,7 +1240,7 @@ export class RowsView extends ColumnsView { return; } - if (!loadPanel && messageText !== undefined && isLocalStore(dataController.store()) && loadPanelOptions.enabled === 'auto' && $element) { + if (!loadPanel && messageText !== undefined && isLocalStore(that.dataSourceController.store()) && loadPanelOptions.enabled === 'auto' && $element) { that._renderLoadPanel($element, $element.parent()); loadPanel = that._loadPanel; } diff --git a/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/extenders/__tests__/virtual_scrolling_data_controller.paging.test.ts b/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/extenders/__tests__/virtual_scrolling_data_controller.paging.test.ts new file mode 100644 index 000000000000..74c28fa4b410 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/extenders/__tests__/virtual_scrolling_data_controller.paging.test.ts @@ -0,0 +1,73 @@ +import { + afterEach, beforeEach, describe, expect, it, jest, +} from '@jest/globals'; +import type { DataGridScrollMode } from '@js/ui/data_grid'; +import { + afterTest, + beforeTest, + createDataGrid, +} from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils'; +import type { DataController } from '@ts/grids/grid_core/data_controller/data_controller'; + +declare class ExposedDataController extends DataController { + public resolvePaginate(enabled: boolean | undefined): boolean | undefined; + + public requiresTotalCount(): boolean; +} + +const withScrollingMode = async (mode: DataGridScrollMode): Promise<ExposedDataController> => { + const { instance } = await createDataGrid({ dataSource: [], scrolling: { mode } }); + + return instance.getController('data') as unknown as ExposedDataController; +}; + +describe('Virtual scrolling data controller paging', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + describe('resolvePaginate', () => { + it.each<{ mode: DataGridScrollMode; enabled: boolean; expected: boolean }>([ + { mode: 'standard', enabled: true, expected: true }, + { mode: 'standard', enabled: false, expected: false }, + { mode: 'virtual', enabled: false, expected: true }, + { mode: 'infinite', enabled: false, expected: true }, + ])('should be $expected in $mode mode with enabled=$enabled', async ({ + mode, enabled, expected, + }) => { + const dataController = await withScrollingMode(mode); + + expect(dataController.resolvePaginate(enabled)).toBe(expected); + }); + + it.each<DataGridScrollMode>(['standard', 'virtual', 'infinite'])( + 'should leave paginate untouched when enabled is undefined in %s mode', + async (mode) => { + const dataController = await withScrollingMode(mode); + + expect(dataController.resolvePaginate(undefined)).toBeUndefined(); + }, + ); + }); + + describe('requiresTotalCount', () => { + it.each<{ mode: DataGridScrollMode; expected: boolean }>([ + { mode: 'standard', expected: true }, + { mode: 'virtual', expected: true }, + { mode: 'infinite', expected: false }, + ])('should be $expected in $mode mode', async ({ mode, expected }) => { + const dataController = await withScrollingMode(mode); + + expect(dataController.requiresTotalCount()).toBe(expected); + }); + }); + + it('should reset the data controller when the scrolling option changes', async () => { + const { instance } = await createDataGrid({ dataSource: [] }); + const dataController = instance.getController('data'); + const resetSpy = jest.spyOn(dataController, 'reset'); + + instance.option('scrolling.mode', 'virtual'); + + expect(resetSpy).toHaveBeenCalled(); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/extenders/virtual_scrolling_data_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/extenders/virtual_scrolling_data_controller.ts index ccef9dd99ab3..5fc7324010ca 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/extenders/virtual_scrolling_data_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/extenders/virtual_scrolling_data_controller.ts @@ -25,7 +25,7 @@ import type { DataChange, PagingOptionName, PagingResult, ProcessedItem, RefreshOptions, } from '@ts/grids/grid_core/data_controller/types'; import type { RawItemData } from '@ts/grids/grid_core/data_source_adapter/types'; -import type { ModuleType } from '@ts/grids/grid_core/m_types'; +import type { ModuleType, OptionChanged } from '@ts/grids/grid_core/m_types'; import type { VirtualItemsCount } from '@ts/grids/grid_core/virtual_data_loader/types'; import gridCoreUtils from '../../m_utils'; @@ -123,7 +123,7 @@ export const virtualScrollingDataControllerExtender = ( const rowIndex = Math.floor(itemIndex) - rowIndexOffset; const { component } = this; const scrollable = component.getScrollable && component.getScrollable(); - const isSortingOperation = this.dataSource()?.operationTypes()?.sorting; + const isSortingOperation = this.dataSourceController.operationTypes()?.sorting; if (scrollable && !isSortingOperation && rowIndex >= 0) { const rowElement = component.getRowElement(rowIndex); @@ -933,6 +933,28 @@ export const virtualScrollingDataControllerExtender = ( return result; } + protected resolvePaginate(enabled: boolean | undefined): boolean | undefined { + if (enabled === undefined) { + return undefined; + } + + return enabled || isVirtualPaging(this); + } + + protected requiresTotalCount(): boolean { + return !isInfiniteMode(this); + } + + public optionChanged(args: OptionChanged): void { + if (args.name === 'scrolling') { + args.handled = true; + this.reset(); + return; + } + + super.optionChanged(args); + } + public isEmpty(): boolean { return this.option(LEGACY_SCROLLING_MODE) === false ? !this.items(true).length : super.isEmpty.apply(this, arguments as any); } diff --git a/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/m_virtual_scrolling.ts b/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/m_virtual_scrolling.ts index 8a8071655a3b..ca5ae364efd4 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/m_virtual_scrolling.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/m_virtual_scrolling.ts @@ -11,6 +11,7 @@ import { getWindow } from '@js/core/utils/window'; import LoadIndicator from '@js/ui/load_indicator'; import errors from '@js/ui/widget/ui.errors'; import type { DataController } from '@ts/grids/grid_core/data_controller/data_controller'; +import type { DataSourceController } from '@ts/grids/grid_core/data_source/data_source_controller'; import type DataSourceAdapter from '@ts/grids/grid_core/data_source_adapter/m_data_source_adapter'; import type { ErrorHandlingViewController } from '@ts/grids/grid_core/error_handling/error_handling_view_controller'; import type { ModuleType } from '@ts/grids/grid_core/m_types'; @@ -39,6 +40,10 @@ import { isInfiniteMode, isVirtualMode, isVirtualPaging } from './utils/scrollin export type VirtualScrollingDataSourceAdapter = InstanceType<ReturnType<typeof dataSourceAdapterExtender>>; +interface VirtualScrollingDataSourceController extends DataSourceController { + getAdapter: () => VirtualScrollingDataSourceAdapter | null; +} + export const updateLoading = function (that) { const beginPageIndex = that._virtualScrollController.beginPageIndex(-1); @@ -488,6 +493,8 @@ export const resizing = (Base: ModuleType<ResizingController>) => class VirtualS export const rowsView = (Base: ModuleType<RowsView>) => class VirtualScrollingRowsViewExtender extends Base { protected _dataController!: DataController & Partial<StateStoringDataControllerExtension>; + protected dataSourceController!: VirtualScrollingDataSourceController; + protected _errorHandlingController!: ErrorHandlingViewController; private _isFixedTableRendering: any; @@ -502,6 +509,7 @@ export const rowsView = (Base: ModuleType<RowsView>) => class VirtualScrollingRo super.init(); this._errorHandlingController = this.getController('errorHandling'); + this.dataSourceController = this.getController('dataSource') as VirtualScrollingDataSourceController; this._dataController.pageChanged.add((pageIndex) => { const scrollTop = this._scrollTop; @@ -581,18 +589,18 @@ export const rowsView = (Base: ModuleType<RowsView>) => class VirtualScrollingRo const deferred = super._renderCore.apply(this, arguments as any); - const dataSource = this._dataController._dataSource as VirtualScrollingDataSourceAdapter | null | undefined; + const dataSourceAdapter = this.dataSourceController.getAdapter(); - if (dataSource && e) { + if (dataSourceAdapter && e) { const itemCount = e.items ? e.items.length : 20; const viewportSize = this._dataController // @ts-expect-error .viewportSize() || 20; if (gridCoreUtils.isVirtualRowRendering(this) && itemCount > 0 && this.option(LEGACY_SCROLLING_MODE) !== false) { - dataSource._renderTime = (Date.now() - startRenderTime) * viewportSize / itemCount; + dataSourceAdapter._renderTime = (Date.now() - startRenderTime) * viewportSize / itemCount; } else { - dataSource._renderTime = Date.now() - startRenderTime; + dataSourceAdapter._renderTime = Date.now() - startRenderTime; } } return deferred; @@ -707,7 +715,7 @@ export const rowsView = (Base: ModuleType<RowsView>) => class VirtualScrollingRo private _correctRowHeights(rowHeights) { const dataController = this._dataController; - const dataSource = dataController._dataSource; + const dataSourceAdapter = this.dataSourceController.getAdapter(); const correctedRowHeights: any = []; const visibleRows = dataController.getVisibleRows(); let itemSize = 0; @@ -726,7 +734,7 @@ export const rowsView = (Base: ModuleType<RowsView>) => class VirtualScrollingRo itemSize = 0; } lastLoadIndex = currentItem.loadIndex; - } else if (isItemCountableByDataSource(currentItem, dataSource as unknown as GroupCountableDataSource)) { + } else if (isItemCountableByDataSource(currentItem, dataSourceAdapter as unknown as GroupCountableDataSource)) { if (firstCountableItem) { firstCountableItem = false; } else { diff --git a/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/utils/__tests__/items.test.ts b/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/utils/__tests__/items.test.ts index e1c1680aad3b..ac6f3d39f1a3 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/utils/__tests__/items.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/utils/__tests__/items.test.ts @@ -96,6 +96,14 @@ describe('isItemCountableByDataSource', () => { it('should not count other row types', () => { expect(isItemCountableByDataSource(asItem({ rowType: 'groupFooter' }), dataSource)).toBe(false); }); + + it('should not count a group row when there is no data source', () => { + expect(isItemCountableByDataSource(asItem({ rowType: 'group', data: 'countable' }), null)).toBe(false); + }); + + it('should still count a data row when there is no data source', () => { + expect(isItemCountableByDataSource(asItem({ rowType: 'data', isNewRow: false }), null)).toBe(true); + }); }); describe('updateItemIndices', () => { diff --git a/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/utils/items.ts b/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/utils/items.ts index 28412664c581..17612088f962 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/utils/items.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/virtual_scrolling/utils/items.ts @@ -30,9 +30,9 @@ export const correctCount = ( export const isItemCountableByDataSource = ( item: ProcessedItem, - dataSource: GroupCountableDataSource, + dataSourceAdapter: GroupCountableDataSource | null | undefined, ): boolean => (item.rowType === 'data' && !item.isNewRow) - || (item.rowType === 'group' && dataSource.isGroupItemCountable(item.data)); + || (item.rowType === 'group' && (dataSourceAdapter?.isGroupItemCountable(item.data) ?? false)); export const updateItemIndices = (items: ProcessedItem[]): ProcessedItem[] => { items.forEach((item, index) => { diff --git a/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap b/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap index d3a512588033..b10cd6f13ea4 100644 --- a/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap +++ b/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap @@ -10,7 +10,7 @@ exports[`common initial render should be successfull 1`] = ` role="group" > <div - class="dx-gridbase-a11y-status-container dx-cardview-exclude-flexbox" + class="dx-screen-reader-only dx-cardview-exclude-flexbox" role="status" /> <div diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/accessibility/status.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/accessibility/status.tsx index 819e89cdf83a..cb40c03dc84d 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/accessibility/status.tsx +++ b/packages/devextreme/js/__internal/grids/new/grid_core/accessibility/status.tsx @@ -1,10 +1,11 @@ +import { SCREEN_READER_ONLY_CLASS } from '@ts/core/widget/widget'; import { Component } from 'inferno'; import { CLASSES as BASE_CLASSES } from '../const'; const CLASSES = { ...BASE_CLASSES, - container: 'dx-gridbase-a11y-status-container', + container: SCREEN_READER_ONLY_CLASS, }; export interface A11yStatusContainerComponentProps { diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/public_methods.ts b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/public_methods.ts index 4d43b738af41..b1e2ccb921c4 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/public_methods.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/public_methods.ts @@ -5,7 +5,7 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import type { FilterDescriptor } from '@js/data'; import type DataSource from '@js/data/data_source'; -import { keysEqual } from '@ts/data/m_utils'; +import { keysEqual } from '@ts/data/utils'; import type { Constructor } from '../types'; import type { GridCoreNewBase } from '../widget'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/utils.ts b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/utils.ts index ac96d84c87ac..24c6f5c90bb8 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/utils.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/utils.ts @@ -6,7 +6,7 @@ import type { Store } from '@js/data'; import type { DataSourceLike } from '@js/data/data_source'; import DataSource from '@js/data/data_source'; import { normalizeDataSourceOptions } from '@js/data/data_source/utils'; -import { applyBatch } from '@ts/data/m_array_utils'; +import { applyBatch } from '@ts/data/array_utils'; import type { DataObject, @@ -35,7 +35,7 @@ export function normalizeDataSource( }; } - // TODO: research making second param not required + // @ts-expect-error the public DataSource constructor takes the public option types return new DataSource(normalizeDataSourceOptions(dataSourceLike, undefined)); } diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/data_source/compatibility.ts b/packages/devextreme/js/__internal/grids/new/grid_core/data_source/compatibility.ts new file mode 100644 index 000000000000..f618d194e289 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/data_source/compatibility.ts @@ -0,0 +1,15 @@ +import { DataController } from '../data_controller/data_controller'; + +// Reused grid_core views reach the adapter through the dataSource controller. The new +// architecture has no adapter, so the mock answers from the DataSource itself. +export class CompatibilityDataSourceController { + public static dependencies = [DataController] as const; + + constructor( + private readonly realDataController: DataController, + ) {} + + public hasAdapter(): boolean { + return !!this.realDataController.dataSource.peek(); + } +} diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/di.ts b/packages/devextreme/js/__internal/grids/new/grid_core/di.ts index 56871436920a..4755ccb7a770 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/di.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/di.ts @@ -6,6 +6,7 @@ import { AccessibilityController } from './accessibility/controller'; import * as ColumnChooserModule from './column_chooser/index'; import * as ColumnsControllerModule from './columns_controller/index'; import * as DataControllerModule from './data_controller/index'; +import { CompatibilityDataSourceController } from './data_source/compatibility'; import { ConfirmController } from './editing/confirm_controller'; import { EditingController } from './editing/controller'; import { EditPopupView } from './editing/popup/view'; @@ -33,6 +34,7 @@ import { ToolbarView } from './toolbar/view'; export function register(diContext: DIContext): void { diContext.register(DataControllerModule.DataController); diContext.register(DataControllerModule.CompatibilityDataController); + diContext.register(CompatibilityDataSourceController); diContext.register(ItemsController); diContext.register(ColumnsControllerModule.ColumnsController); diContext.register(SelectionControllerModule.Controller); diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/__snapshots__/options.integration.test.ts.snap b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/__snapshots__/options.integration.test.ts.snap index 6b48d20c0743..4ed2f2e07526 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/__snapshots__/options.integration.test.ts.snap +++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/__snapshots__/options.integration.test.ts.snap @@ -267,6 +267,12 @@ exports[`Options Column.HeaderFilter dataSource: custom dataSource 1`] = ` style="width: 222px; height: 90px;" /> </div> + <div + class="dx-screen-reader-only" + role="status" + > + Items: 2 + </div> </div> </div> <div @@ -635,6 +641,12 @@ exports[`Options Column.HeaderFilter dataSource: custom dataSource with exclude style="width: 222px; height: 90px;" /> </div> + <div + class="dx-screen-reader-only" + role="status" + > + Items: 2 + </div> </div> </div> <div @@ -1003,6 +1015,12 @@ exports[`Options Column.HeaderFilter dataSource: custom dataSource with exclude style="width: 222px; height: 90px;" /> </div> + <div + class="dx-screen-reader-only" + role="status" + > + Items: 2 + </div> </div> </div> <div @@ -1371,6 +1389,12 @@ exports[`Options Column.HeaderFilter dataSource: custom dataSource with filter v style="width: 222px; height: 90px;" /> </div> + <div + class="dx-screen-reader-only" + role="status" + > + Items: 2 + </div> </div> </div> <div @@ -1850,6 +1874,12 @@ exports[`Options Column.HeaderFilter filterType + values: exclude filter 1`] = ` style="width: 222px; height: 90px;" /> </div> + <div + class="dx-screen-reader-only" + role="status" + > + Items: 5 + </div> </div> </div> <div @@ -2329,6 +2359,12 @@ exports[`Options Column.HeaderFilter filterType + values: exclude filter with va style="width: 222px; height: 90px;" /> </div> + <div + class="dx-screen-reader-only" + role="status" + > + Items: 5 + </div> </div> </div> <div @@ -2808,6 +2844,12 @@ exports[`Options Column.HeaderFilter filterType + values: filter values 1`] = ` style="width: 222px; height: 90px;" /> </div> + <div + class="dx-screen-reader-only" + role="status" + > + Items: 5 + </div> </div> </div> <div @@ -3139,6 +3181,12 @@ exports[`Options HeaderFilter texts: custom translations 1`] = ` style="width: 222px; height: 90px;" /> </div> + <div + class="dx-screen-reader-only" + role="status" + > + Items: 1 + </div> </div> </div> <div @@ -3470,6 +3518,12 @@ exports[`Options HeaderFilter texts: default translation 1`] = ` style="width: 222px; height: 90px;" /> </div> + <div + class="dx-screen-reader-only" + role="status" + > + Items: 1 + </div> </div> </div> <div diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/__snapshots__/view.integration.test.tsx.snap b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/__snapshots__/view.integration.test.tsx.snap index 4d9df25d8c25..08a9545773b2 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/__snapshots__/view.integration.test.tsx.snap +++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/__snapshots__/view.integration.test.tsx.snap @@ -378,6 +378,12 @@ exports[`HeaderFilter View integration should render popup with list by default style="width: 222px; height: 90px;" /> </div> + <div + class="dx-screen-reader-only" + role="status" + > + Items: 5 + </div> </div> </div> <div @@ -857,6 +863,12 @@ exports[`HeaderFilter View integration should render popup with tree list if dat style="width: 222px; height: 90px;" /> </div> + <div + class="dx-screen-reader-only" + role="status" + > + Items: 5 + </div> </div> </div> <div diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/legacy_header_filter.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/legacy_header_filter.ts index 76ee06aa32f8..1072316e25ba 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/legacy_header_filter.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/legacy_header_filter.ts @@ -16,7 +16,7 @@ import { isDefined, isFunction, isObject } from '@js/core/utils/type'; import messageLocalization from '@js/localization/message'; import filteringUtils from '@js/ui/shared/filtering'; import { extend } from '@ts/core/utils/m_extend'; -import { normalizeDataSourceOptions as oldNormalizeDataSourceOptions } from '@ts/data/data_source/m_utils'; +import { normalizeDataSourceOptions as oldNormalizeDataSourceOptions } from '@ts/data/data_source/utils'; import { convertDataFromUTCToLocal, getFormatOptions, @@ -158,7 +158,6 @@ export const getDataSourceOptions = ( const options: any = {}; if (isDefined(headerFilterDataSource) && !isFunction(headerFilterDataSource)) { - // @ts-expect-error options.dataSource = oldNormalizeDataSourceOptions(headerFilterDataSource); } else { const cutoffLevel = Array.isArray(group) ? group.length - 1 : 0; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts b/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts index 76725b7d6086..cb045d2e63b8 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts @@ -16,6 +16,7 @@ import * as ColumnChooserModule from './column_chooser/index'; import { CompatibilityColumnsController } from './columns_controller/compatibility'; import * as ColumnsControllerModule from './columns_controller/index'; import * as DataControllerModule from './data_controller/index'; +import { CompatibilityDataSourceController } from './data_source/compatibility'; import * as di from './di'; import * as EditingModule from './editing/index'; import { EditPopupView } from './editing/popup/view'; @@ -100,6 +101,7 @@ export class GridCoreNewBase< this.diContext.registerInstance(WidgetMock, new WidgetMock( this, this.diContext.get(DataControllerModule.CompatibilityDataController), + this.diContext.get(CompatibilityDataSourceController), this.diContext.get(CompatibilityColumnsController), this.diContext.get(CompatibilityHeaderFilterController), this.diContext.get(CompatibilityFilterSyncController), diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/widget_mock.ts b/packages/devextreme/js/__internal/grids/new/grid_core/widget_mock.ts index 5c99d89670cd..6eeca8104fd3 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/widget_mock.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/widget_mock.ts @@ -1,5 +1,6 @@ import type { CompatibilityColumnsController } from './columns_controller/compatibility'; import type { CompatibilityDataController } from './data_controller'; +import type { CompatibilityDataSourceController } from './data_source/compatibility'; import type { CompatibilityFilterSyncController } from './filtering/filter_sync/compatibility'; import type { CompatibilityHeaderFilterController } from './filtering/header_filter/compatibility'; import type { GridCoreNewBase } from './widget'; @@ -9,6 +10,7 @@ export class WidgetMock { private readonly _controllers = { data: this.data, + dataSource: this.dataSource, columns: this.columns, headerFilter: this.headerFilter, filterSync: this.filterSync, @@ -18,6 +20,7 @@ export class WidgetMock { // eslint-disable-next-line @typescript-eslint/no-explicit-any private readonly widget: GridCoreNewBase<any>, private readonly data: CompatibilityDataController, + private readonly dataSource: CompatibilityDataSourceController, private readonly columns: CompatibilityColumnsController, private readonly headerFilter: CompatibilityHeaderFilterController, private readonly filterSync: CompatibilityFilterSyncController, diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/data_source/m_data_source.ts b/packages/devextreme/js/__internal/grids/pivot_grid/data_source/m_data_source.ts index b5b1683bc03f..ef1a61eaa54e 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/data_source/m_data_source.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/data_source/m_data_source.ts @@ -341,7 +341,6 @@ class PivotGridDataSource { createLocalOrRemoteStore(dataSourceOptions, notifyProgress) { const StoreConstructor = dataSourceOptions.remoteOperations || dataSourceOptions.paginate ? RemoteStore : LocalStore; - // @ts-expect-error return new StoreConstructor(extend(normalizeDataSourceOptions(dataSourceOptions), { onChanged: null, onLoadingChanged: null, diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/local_store/m_local_store.ts b/packages/devextreme/js/__internal/grids/pivot_grid/local_store/m_local_store.ts index 174166bf6091..4ee6f3179b67 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/local_store/m_local_store.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/local_store/m_local_store.ts @@ -424,7 +424,6 @@ const LocalStore = Class.inherit((function () { if (dataSource.store() instanceof CustomStore && filter) { filter = processFilter(filter, fieldSelectors); - // @ts-expect-error return dataQuery(dataSource.items()).filter(filter).toArray(); } diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/remote_store/m_remote_store.ts b/packages/devextreme/js/__internal/grids/pivot_grid/remote_store/m_remote_store.ts index 3412120c14f2..b514df7df779 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/remote_store/m_remote_store.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/remote_store/m_remote_store.ts @@ -561,7 +561,6 @@ class RemoteStore { skip: 0, take: 20, }).done((data) => { - // @ts-expect-error const normalizedArguments = normalizeLoadResult(data); d.resolve(pivotGridUtils.discoverObjectFields(normalizedArguments.data, fields)); }).fail(d.reject); diff --git a/packages/devextreme/js/__internal/grids/tree_list/__tests__/__mock__/helpers/utils.ts b/packages/devextreme/js/__internal/grids/tree_list/__tests__/__mock__/helpers/utils.ts new file mode 100644 index 000000000000..ffd6ce2fb2dd --- /dev/null +++ b/packages/devextreme/js/__internal/grids/tree_list/__tests__/__mock__/helpers/utils.ts @@ -0,0 +1,66 @@ +import { jest } from '@jest/globals'; +import fx from '@js/common/core/animation/fx'; +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import type { Properties as TreeListProperties } from '@js/ui/tree_list'; +import TreeList from '@js/ui/tree_list'; +import type { Controllers, Views } from '@ts/grids/grid_core/m_types'; +import { TreeListModel } from '@ts/grids/tree_list/__tests__/__mock__/model/tree_list'; + +export interface TreeListInstance extends TreeList { + getController: <T extends keyof Controllers>(name: T) => Controllers[T]; + getView: <T extends keyof Views>(name: T) => Views[T]; +} + +export const SELECTORS = { + treeListContainer: '#treeListContainer', +}; + +export const TREELIST_CONTAINER_ID = 'treeListContainer'; + +export const createTreeList = ( + options: TreeListProperties = {}, +): Promise<{ + $container: dxElementWrapper; + component: TreeListModel; + instance: TreeListInstance; +}> => new Promise((resolve) => { + const $container = $('<div>') + .attr('id', TREELIST_CONTAINER_ID) + .appendTo(document.body); + + const treeListOptions: TreeListProperties = { + keyExpr: 'id', + ...options, + }; + + const instance = new TreeList( + $container.get(0) as HTMLDivElement, + treeListOptions, + ) as TreeListInstance; + const component = new TreeListModel($container.get(0) as HTMLElement); + + jest.runAllTimers(); + + resolve({ + $container, + component, + instance, + }); +}); + +export const beforeTest = (): void => { + fx.off = true; + jest.useFakeTimers(); +}; + +export const afterTest = (): void => { + const $container = $(SELECTORS.treeListContainer); + const treeList = TreeList.getInstance($container.get(0)); + + treeList?.dispose(); + $container.remove(); + jest.clearAllMocks(); + jest.useRealTimers(); + fx.off = false; +}; diff --git a/packages/devextreme/js/__internal/grids/tree_list/__tests__/state_storing.user_state.test.ts b/packages/devextreme/js/__internal/grids/tree_list/__tests__/state_storing.user_state.test.ts new file mode 100644 index 000000000000..5158ac72be87 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/tree_list/__tests__/state_storing.user_state.test.ts @@ -0,0 +1,28 @@ +import { + afterEach, beforeEach, describe, expect, it, +} from '@jest/globals'; + +import { + afterTest, + beforeTest, + createTreeList, +} from './__mock__/helpers/utils'; + +describe('TreeList user state', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + it('should include the expandedRowKeys', async () => { + const { instance } = await createTreeList({ + dataSource: [{ id: 1, parentId: 0, name: 'Alex' }], + expandedRowKeys: [1], + }); + + expect(instance.getController('data').getUserState()).toEqual({ + searchText: '', + expandedRowKeys: [1], + pageIndex: 0, + pageSize: 20, + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/tree_list/data_controller/m_data_controller.ts b/packages/devextreme/js/__internal/grids/tree_list/data_controller/m_data_controller.ts index e87ee434b1e8..6120980c9b71 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/data_controller/m_data_controller.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/data_controller/m_data_controller.ts @@ -2,24 +2,14 @@ import { equalByValue } from '@js/core/utils/common'; import { Deferred } from '@js/core/utils/deferred'; import { extend } from '@js/core/utils/extend'; import { DataController, dataControllerModule } from '@ts/grids/grid_core/data_controller/data_controller'; -import type { DataSourceAdapterProvider } from '@ts/grids/grid_core/data_source_adapter/types'; import type { RowKey } from '@ts/grids/grid_core/m_types'; import type { DataSourceAdapterTreeList } from '../data_source_adapter/m_data_source_adapter'; -import dataSourceAdapterProvider from '../data_source_adapter/m_data_source_adapter'; import treeListCore from '../m_core'; export class TreeListDataController extends DataController { public declare _dataSource?: DataSourceAdapterTreeList | null; - public dataSource(): DataSourceAdapterTreeList | undefined { - return this._dataSource ?? undefined; - } - - protected _getDataSourceAdapterProvider(): DataSourceAdapterProvider { - return dataSourceAdapterProvider; - } - private _getNodeLevel(node) { let level = -1; while (node.parent) { @@ -83,18 +73,6 @@ export class TreeListDataController extends DataController { super.init.apply(this, arguments as any); } - public keyOf(data) { - const dataSource = this._dataSource; - - if (dataSource) { - return dataSource.keyOf(data); - } - } - - public key() { - return this._dataSource?.getKeyExpr(); - } - public publicMethods() { return super.publicMethods().concat(['expandRow', 'collapseRow', 'isRowExpanded', 'getRootNode', 'getNodeByKey', 'loadDescendants', 'forEachNode']); } diff --git a/packages/devextreme/js/__internal/grids/tree_list/data_source/__tests__/data_source_controller.integration.test.ts b/packages/devextreme/js/__internal/grids/tree_list/data_source/__tests__/data_source_controller.integration.test.ts new file mode 100644 index 000000000000..329df8827c2e --- /dev/null +++ b/packages/devextreme/js/__internal/grids/tree_list/data_source/__tests__/data_source_controller.integration.test.ts @@ -0,0 +1,93 @@ +import { + afterEach, beforeEach, describe, expect, it, jest, +} from '@jest/globals'; +import errors from '@js/ui/widget/ui.errors'; +import { getMirroredAdapter } from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils'; + +import { + afterTest, + beforeTest, + createTreeList, +} from '../../__tests__/__mock__/helpers/utils'; +import { TreeListDataSourceController } from '../data_source_controller'; + +const DATA = [ + { id: 1, parentId: 0, value: 'a' }, + { id: 2, parentId: 1, value: 'b' }, +]; + +describe('TreeList dataSource controller', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + it('is the TreeList subclass', async () => { + const { instance } = await createTreeList({ dataSource: DATA }); + + expect(instance.getController('dataSource')) + .toBeInstanceOf(TreeListDataSourceController); + }); + + it('builds a TreeList adapter', async () => { + const { instance } = await createTreeList({ dataSource: DATA }); + const adapter = instance.getController('dataSource').getAdapter(); + + expect(adapter).toBeTruthy(); + expect('forEachNode' in (adapter as object)).toBe(true); + }); + + it('holds the same adapter object as DataController', async () => { + const { instance } = await createTreeList({ dataSource: DATA }); + const dataSourceController = instance.getController('dataSource'); + + expect(dataSourceController.hasAdapter()).toBe(true); + expect(dataSourceController.getAdapter()).toBe(getMirroredAdapter(instance)); + }); + + it('does not warn W1011, because the override does not apply to TreeList', async () => { + const log = jest.spyOn(errors, 'log').mockImplementation(() => {}); + + try { + await createTreeList({ + dataSource: { store: { type: 'array', data: DATA } }, + keyExpr: 'id', + }); + + expect(log).not.toHaveBeenCalledWith('W1011'); + } finally { + log.mockRestore(); + } + }); +}); + +describe('TreeList answers key and keyOf from the key expression', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + // The base controller answers both from the store, which has no key in either case here. + it('falls back to the default key expression when nothing declares a key', async () => { + const { instance } = await createTreeList({ dataSource: DATA, keyExpr: undefined }); + const dataSourceController = instance.getController('dataSource'); + + expect(dataSourceController.key()).toBe('id'); + expect(dataSourceController.keyOf(DATA[1])).toBe(2); + }); + + it('parts company with the store key, which is what store-based callers still read', async () => { + const { instance } = await createTreeList({ dataSource: DATA, keyExpr: undefined }); + const dataSourceController = instance.getController('dataSource'); + + expect(dataSourceController.store()?.key()).toBeUndefined(); + expect(dataSourceController.key()).toBe('id'); + }); + + it('uses the declared key expression when the store has no key', async () => { + const { instance } = await createTreeList({ + dataSource: { store: { type: 'array', data: DATA } }, + keyExpr: 'value', + }); + const dataSourceController = instance.getController('dataSource'); + + expect(dataSourceController.key()).toBe('value'); + expect(dataSourceController.keyOf(DATA[1])).toBe('b'); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/tree_list/data_source/data_source_controller.ts b/packages/devextreme/js/__internal/grids/tree_list/data_source/data_source_controller.ts new file mode 100644 index 000000000000..87467224ba13 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/tree_list/data_source/data_source_controller.ts @@ -0,0 +1,21 @@ +import type { StoreKey } from '@ts/data/abstract_store'; +import { DataSourceController } from '@ts/grids/grid_core/data_source/data_source_controller'; +import type { DataSourceAdapterProvider, RawItemData } from '@ts/grids/grid_core/data_source_adapter/types'; +import type { RowKey } from '@ts/grids/grid_core/m_types'; +import type { DataSourceAdapterTreeList } from '@ts/grids/tree_list/data_source_adapter/m_data_source_adapter'; +import dataSourceAdapterProvider from '@ts/grids/tree_list/data_source_adapter/m_data_source_adapter'; + +export class TreeListDataSourceController + extends DataSourceController<DataSourceAdapterTreeList> { + protected getAdapterProvider(): DataSourceAdapterProvider<DataSourceAdapterTreeList> { + return dataSourceAdapterProvider; + } + + public key(): StoreKey | undefined { + return this.adapter?.getKeyExpr(); + } + + public keyOf(data: RawItemData): RowKey | undefined { + return this.adapter?.keyOf(data); + } +} diff --git a/packages/devextreme/js/__internal/grids/tree_list/data_source/data_source_module.ts b/packages/devextreme/js/__internal/grids/tree_list/data_source/data_source_module.ts new file mode 100644 index 000000000000..d3b5072248af --- /dev/null +++ b/packages/devextreme/js/__internal/grids/tree_list/data_source/data_source_module.ts @@ -0,0 +1,9 @@ +import treeListCore from '@ts/grids/tree_list/m_core'; + +import { TreeListDataSourceController } from './data_source_controller'; + +treeListCore.registerModule('dataSource', { + controllers: { + dataSource: TreeListDataSourceController, + }, +}); diff --git a/packages/devextreme/js/__internal/grids/tree_list/data_source_adapter/m_data_source_adapter.ts b/packages/devextreme/js/__internal/grids/tree_list/data_source_adapter/m_data_source_adapter.ts index b76ec5607f45..35ad2a94f1b2 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/data_source_adapter/m_data_source_adapter.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/data_source_adapter/m_data_source_adapter.ts @@ -15,9 +15,15 @@ import type { BeforePushEvent } from '@ts/data/types'; import type { CustomLoadResult } from '@ts/grids/grid_core/data_source_adapter/custom_loader'; import DataSourceAdapter from '@ts/grids/grid_core/data_source_adapter/m_data_source_adapter'; import { createDataSourceAdapterProvider } from '@ts/grids/grid_core/data_source_adapter/provider'; +import type { RawItemData } from '@ts/grids/grid_core/data_source_adapter/types'; import gridCoreUtils from '@ts/grids/grid_core/m_utils'; import treeListCore from '../m_core'; +import type { LoadOperation, NodeByKey, TreeNode } from './types'; +import type { NodesContext } from './utils/nodes'; +import { + convertItemToNode, createNodesByItems, fillNodes, getVisibleNodes, +} from './utils/nodes'; const { queryByOptions } = storeHelper; @@ -39,9 +45,7 @@ const getChildKeys = function (that, keys) { return childKeys; }; -// @ts-expect-error const applySorting = (data: any[], sort: any): any => queryByOptions( - // @ts-expect-error query(data), { sort, @@ -67,11 +71,11 @@ export class DataSourceAdapterTreeList extends DataSourceAdapter { private _isChildrenLoaded: any; - private _nodeByKey: any; + private _nodeByKey!: NodeByKey; private _isReload: any; - private _rootNode: any; + private _rootNode?: TreeNode; public _isNodesInitializing = false; @@ -139,85 +143,15 @@ export class DataSourceAdapterTreeList extends DataSourceAdapter { }); } - private _calculateHasItems(node, options) { - const that = this; - const { parentIds } = options.storeLoadOptions; - let hasItems; - const isFullBranch = isFullBranchFilterMode(that); - - if (that._hasItemsGetter && (parentIds || !options.storeLoadOptions.filter || isFullBranch)) { - hasItems = that._hasItemsGetter(node.data); - } - - if (hasItems === undefined) { - if (!that._isChildrenLoaded[node.key] && options.remoteOperations.filtering && (parentIds || isFullBranch)) { - hasItems = true; - } else if (options.loadOptions.filter && !options.remoteOperations.filtering && isFullBranch) { - hasItems = node.children.length; - } else { - hasItems = node.hasChildren; - } - } - return !!hasItems; - } - - private _fillVisibleItemsByNodes(nodes, options, result) { - for (let i = 0; i < nodes.length; i++) { - if (nodes[i].visible) { - result.push(nodes[i]); - } - - if ((this.isRowExpanded(nodes[i].key, options) || !nodes[i].visible) && nodes[i].hasChildren && nodes[i].children.length) { - this._fillVisibleItemsByNodes(nodes[i].children, options, result); - } - } - } - - private _convertItemToNode(item, rootValue, nodeByKey) { - const key = this._keyGetter(item); - let parentId = this._parentIdGetter(item); - - parentId = isDefined(parentId) ? parentId : rootValue; - const parentNode = nodeByKey[parentId] = nodeByKey[parentId] || { key: parentId, children: [] }; - - const node = nodeByKey[key] = nodeByKey[key] || { key, children: [] }; - node.data = item; - node.parent = parentNode; - - return node; - } - - private _createNodesByItems(items, visibleItems) { - const that = this; - const rootValue: any = that.option('rootValue'); - const visibleByKey = {}; - const nodeByKey = that._nodeByKey = {}; - let i; - - if (visibleItems) { - for (i = 0; i < visibleItems.length; i++) { - visibleByKey[this._keyGetter(visibleItems[i])] = true; - } - } - - for (i = 0; i < items.length; i++) { - const node = that._convertItemToNode(items[i], rootValue, nodeByKey); - - if (node.key === undefined) { - return; - } - - node.visible = !visibleItems || !!visibleByKey[node.key]; - if (node.parent) { - node.parent.children.push(node); - } - } - - const rootNode = nodeByKey[rootValue] || { key: rootValue, children: [] }; - - rootNode.level = -1; - - return rootNode; + private _getNodesContext(): NodesContext { + return { + rootValue: this.option('rootValue'), + isFullBranchFilterMode: isFullBranchFilterMode(this), + keyGetter: this._keyGetter, + parentIdGetter: this._parentIdGetter, + hasItemsGetter: this._hasItemsGetter, + isChildrenLoaded: this._isChildrenLoaded, + }; } private _convertDataToPlainStructure(data, parentId?, result?) { @@ -227,7 +161,6 @@ export class DataSourceAdapterTreeList extends DataSourceAdapter { result = result || []; for (let i = 0; i < data.length; i++) { - // @ts-expect-error const item = createObjectWithChanges(data[i]); key = this._keyGetter(item); @@ -438,11 +371,13 @@ export class DataSourceAdapterTreeList extends DataSourceAdapter { return d.resolve(data); } - let cachedNodes = keys.map((id) => this.getNodeByKey(id)).filter((node) => node && node.data); + let cachedNodes = keys + .map((id) => this.getNodeByKey(id)) + .filter((node) => node && node.data) as TreeNode[]; if (cachedNodes.length === keys.length) { if (needChildren) { - cachedNodes = cachedNodes.reduce((result, node) => result.concat(node.children), []); + cachedNodes = cachedNodes.reduce((result: TreeNode[], node) => result.concat(node.children), []); } if (cachedNodes.length) { @@ -479,7 +414,6 @@ export class DataSourceAdapterTreeList extends DataSourceAdapter { if (loadedData.length) { if (needLocalFiltering) { - // @ts-expect-error loadedData = query(loadedData).filter(filter).toArray(); } @@ -571,20 +505,20 @@ export class DataSourceAdapterTreeList extends DataSourceAdapter { const parentNode = that.getNodeByKey(parentId); if (parentNode) { - const rootValue = that.option('rootValue'); - const node = that._convertItemToNode(change.data, rootValue, that._nodeByKey); + const node = convertItemToNode(change.data, that._nodeByKey, that._getNodesContext()); node.hasChildren = false; - node.level = parentNode.level + 1; + node.level = parentNode.level! + 1; node.visible = true; parentNode.children.push(node); - that._isChildrenLoaded[node.key] = true; + that._isChildrenLoaded[node.key as string] = true; that._setHasItems(parentNode, true); if ((!parentNode.parent || that.isRowExpanded(parentNode.key)) && change.index !== undefined) { + // @ts-expect-error Badly typed items() let index = that.items().indexOf(parentNode) + 1; index += change.index >= 0 ? Math.min(change.index, parentNode.children.length) : parentNode.children.length; @@ -625,7 +559,6 @@ export class DataSourceAdapterTreeList extends DataSourceAdapter { public customizeLoadResultHandler(options) { const data = options.data = this._convertDataToPlainStructure(options.data); if (!options.remoteOperations.filtering && options.loadOptions.filter) { - // @ts-expect-error options.fullData = queryByOptions(query(options.data), { sort: options.loadOptions && options.loadOptions.sort }).toArray(); } this._updateHasItemsMap(options); @@ -640,61 +573,29 @@ export class DataSourceAdapterTreeList extends DataSourceAdapter { } } - private _fillNodes(nodes, options, expandedRowKeys, level?) { - const isFullBranch = isFullBranchFilterMode(this); - - level = level || 0; - for (let i = 0; i < nodes.length; i++) { - const node = nodes[i]; - let needToExpand = false; - - // node.hasChildren = false; - this._fillNodes(nodes[i].children, options, expandedRowKeys, level + 1); - - node.level = level; - node.hasChildren = this._calculateHasItems(node, options); - - if (node.visible && node.hasChildren) { - if (isFullBranch) { - if (node.children.filter((node) => node.visible).length) { - needToExpand = true; - } else if (node.children.length) { - treeListCore.foreachNodes(node.children, (node) => { - node.visible = true; - }); - } - } else { - needToExpand = true; - } - if (options.expandVisibleNodes && needToExpand) { - expandedRowKeys.push(node.key); - } - } - - if (node.visible || node.hasChildren) { - node.parent.hasChildren = true; - } - } - } - - private _processTreeStructure(options, visibleItems?) { - let { data } = options; + private _processTreeStructure(options: LoadOperation, visibleItems?: RawItemData[]): void { + let data = options.data as RawItemData[]; const { parentIds } = options.storeLoadOptions; - const expandedRowKeys = []; if (parentIds && parentIds.length || this._isReload) { if (options.fullData) { data = options.fullData; - visibleItems = visibleItems || options.data; + visibleItems ??= options.data as RawItemData[]; } - this._rootNode = this._createNodesByItems(data, visibleItems); + const nodesContext = this._getNodesContext(); + const { rootNode, nodeByKey } = createNodesByItems(data, visibleItems, nodesContext); + + this._nodeByKey = nodeByKey; + this._rootNode = rootNode; + if (!this._rootNode) { - // @ts-expect-error - options.data = new Deferred().reject(errors.Error('E1046', this.getKeyExpr())); + // @ts-expect-error badly typed Deferred + options.data = Deferred().reject(errors.Error('E1046', this.getKeyExpr())); return; } - this._fillNodes(this._rootNode.children, options, expandedRowKeys); + + const expandedRowKeys = fillNodes(this._rootNode.children, options, nodesContext); this._isNodesInitializing = true; if (options.collapseVisibleNodes || expandedRowKeys.length) { @@ -705,18 +606,20 @@ export class DataSourceAdapterTreeList extends DataSourceAdapter { this._isNodesInitializing = false; } - const resultData = []; - - this._fillVisibleItemsByNodes(this._rootNode.children, options, resultData); + const resultData = getVisibleNodes( + this._rootNode!.children, + (key) => this.isRowExpanded(key, options), + ); + // @ts-expect-error From here on the rows are nodes, not the loaded items the base type describes. options.data = resultData; this._totalItemsCount = resultData.length; } - protected customizeLoadResultHandlerCore(options) { + protected customizeLoadResultHandlerCore(options: LoadOperation): void { const that = this; const { data } = options; - const filter = options.storeLoadOptions.filter || options.loadOptions.filter; + const filter = options.storeLoadOptions.filter || options.loadOptions?.filter; const filterMode = that.option('filterMode'); let visibleItems; const { parentIds } = options.storeLoadOptions; @@ -730,6 +633,7 @@ export class DataSourceAdapterTreeList extends DataSourceAdapter { if (filterMode === 'matchOnly') { visibleItems = data; } + return that._loadParents(data, options).done((data) => { that._loadChildrenIfNeed(data, options).done((data) => { options.data = data; @@ -847,10 +751,12 @@ export class DataSourceAdapterTreeList extends DataSourceAdapter { return this._isNodesInitializing ? new Deferred().resolve() : this.load(); } - public getNodeByKey(key) { + public getNodeByKey(key): TreeNode | undefined { if (this._nodeByKey) { return this._nodeByKey[key]; } + + return undefined; } private getNodeLeafKeys() { @@ -918,7 +824,7 @@ export class DataSourceAdapterTreeList extends DataSourceAdapter { // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any public forEachNode(nodeCallback?: (node: any) => void) { - let nodes = []; + let nodes: TreeNode[] = []; let callback; if (arguments.length === 1) { @@ -926,7 +832,7 @@ export class DataSourceAdapterTreeList extends DataSourceAdapter { callback = arguments[0]; const rootNode = this.getRootNode(); - nodes = rootNode && rootNode.children || []; + nodes = rootNode?.children ?? []; } else if (arguments.length === 2) { // eslint-disable-next-line prefer-destructuring callback = arguments[1]; diff --git a/packages/devextreme/js/__internal/grids/tree_list/data_source_adapter/types.ts b/packages/devextreme/js/__internal/grids/tree_list/data_source_adapter/types.ts new file mode 100644 index 000000000000..443ab021fa11 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/tree_list/data_source_adapter/types.ts @@ -0,0 +1,22 @@ +import type { LoadOperation as BaseLoadOperation, RawItemData } from '@ts/grids/grid_core/data_source_adapter/types'; + +export interface LoadOperation extends BaseLoadOperation { + storeLoadOptions: BaseLoadOperation['storeLoadOptions'] & { + parentIds?: unknown[]; + }; + fullData?: RawItemData[]; + collapseVisibleNodes?: boolean; + expandVisibleNodes?: boolean; +} + +export interface TreeNode { + key: unknown; + children: TreeNode[]; + data?: unknown; + parent?: TreeNode; + level?: number; + visible?: boolean; + hasChildren?: boolean; +} + +export type NodeByKey = Record<string, TreeNode>; diff --git a/packages/devextreme/js/__internal/grids/tree_list/data_source_adapter/utils/__tests__/nodes.test.ts b/packages/devextreme/js/__internal/grids/tree_list/data_source_adapter/utils/__tests__/nodes.test.ts new file mode 100644 index 000000000000..ea1141373c60 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/tree_list/data_source_adapter/utils/__tests__/nodes.test.ts @@ -0,0 +1,637 @@ +import { describe, expect, it } from '@jest/globals'; + +import type { LoadOperation, NodeByKey, TreeNode } from '../../types'; +import type { NodesContext } from '../nodes'; +import { + convertItemToNode, createNodesByItems, fillNodes, getVisibleNodes, +} from '../nodes'; + +interface Item { + [field: string]: unknown; + id?: unknown; + parentId?: unknown; + hasItems?: unknown; +} + +const ROOT = 0; + +function createContext(overrides?: Partial<NodesContext>): NodesContext { + return { + rootValue: ROOT, + isFullBranchFilterMode: false, + keyGetter: (data): unknown => (data as Item).id, + parentIdGetter: (data): unknown => (data as Item).parentId, + hasItemsGetter: undefined, + isChildrenLoaded: {}, + ...overrides, + }; +} + +function createLoadOptions(options: { + parentIds?: unknown[]; + storeFilter?: unknown; + loadFilter?: unknown; + remoteFiltering?: boolean; + expandVisibleNodes?: boolean; +} = {}): LoadOperation { + return { + storeLoadOptions: { + parentIds: options.parentIds, + filter: options.storeFilter, + }, + loadOptions: { + filter: options.loadFilter, + }, + remoteOperations: { + filtering: options.remoteFiltering, + }, + expandVisibleNodes: options.expandVisibleNodes, + }; +} + +function buildTree(items: Item[], visibleKeys?: unknown[]): { root: TreeNode; nodes: NodeByKey } { + const visibleItems = visibleKeys && items.filter((item) => visibleKeys.includes(item.id)); + const { rootNode, nodeByKey } = createNodesByItems(items, visibleItems, createContext()); + + return { root: rootNode as TreeNode, nodes: nodeByKey }; +} + +describe('convertItemToNode', () => { + it('wires the node to a placeholder parent and registers both by key', () => { + const nodeByKey = {}; + const item = { id: 2, parentId: 1 }; + + const node = convertItemToNode(item, nodeByKey, createContext()); + + expect(node).toMatchObject({ key: 2, data: item, children: [] }); + expect(node.parent).toEqual({ key: 1, children: [] }); + expect(nodeByKey).toEqual({ 1: node.parent, 2: node }); + }); + + const rootFallbackCases: [string, unknown][] = [ + ['undefined', undefined], + ['null', null], + ]; + + it.each(rootFallbackCases)('attaches the item to rootValue when its parent id is %s', (_, parentId) => { + const node = convertItemToNode({ id: 1, parentId }, {}, createContext()); + + expect(node.parent?.key).toBe(ROOT); + }); + + it('reuses an already registered node, keeping the children it accumulated', () => { + const nodeByKey = {}; + const context = createContext(); + + // The child is converted first, so node `1` only exists as a placeholder parent. + const child = convertItemToNode({ id: 2, parentId: 1 }, nodeByKey, context); + child.parent?.children.push(child); + + const parentItem = { id: 1, parentId: ROOT }; + const parent = convertItemToNode(parentItem, nodeByKey, context); + + expect(parent).toBe(child.parent); + expect(parent.data).toBe(parentItem); + expect(parent.children).toEqual([child]); + }); + + it('reuses an already registered parent', () => { + const nodeByKey = {}; + const context = createContext(); + + const first = convertItemToNode({ id: 2, parentId: 1 }, nodeByKey, context); + const second = convertItemToNode({ id: 3, parentId: 1 }, nodeByKey, context); + + expect(second.parent).toBe(first.parent); + }); +}); + +describe('createNodesByItems', () => { + it('builds a tree and indexes every node by key', () => { + const items = [ + { id: 1, parentId: ROOT }, + { id: 2, parentId: 1 }, + { id: 3, parentId: 1 }, + ]; + + const { rootNode, nodeByKey } = createNodesByItems(items, undefined, createContext()); + + expect(rootNode?.key).toBe(ROOT); + expect(rootNode?.level).toBe(-1); + expect(rootNode?.children.map((node) => node.key)).toEqual([1]); + expect(rootNode?.children[0].children.map((node) => node.key)).toEqual([2, 3]); + expect(Object.keys(nodeByKey)).toEqual(['0', '1', '2', '3']); + }); + + it('does not depend on item order — a child may precede its parent', () => { + const items = [ + { id: 2, parentId: 1 }, + { id: 1, parentId: ROOT }, + ]; + + const { rootNode } = createNodesByItems(items, undefined, createContext()); + + expect(rootNode?.children.map((node) => node.key)).toEqual([1]); + expect(rootNode?.children[0].children.map((node) => node.key)).toEqual([2]); + expect(rootNode?.children[0].data).toBe(items[1]); + }); + + it('leaves an orphaned branch out of the root, but keeps it indexed', () => { + const items = [{ id: 2, parentId: 'missing' }]; + + const { rootNode, nodeByKey } = createNodesByItems(items, undefined, createContext()); + + expect(rootNode?.children).toEqual([]); + expect(nodeByKey.missing.children.map((node) => node.key)).toEqual([2]); + }); + + const rootValueCases: [string, unknown][] = [ + ['null', null], + ['a string', 'root'], + ]; + + it.each(rootValueCases)('supports %s as rootValue', (_, rootValue) => { + const items = [{ id: 1, parentId: rootValue }]; + + const { rootNode } = createNodesByItems(items, undefined, createContext({ rootValue })); + + expect(rootNode?.key).toBe(rootValue); + expect(rootNode?.children.map((node) => node.key)).toEqual([1]); + }); + + it('returns a synthetic root when there are no items', () => { + const { rootNode, nodeByKey } = createNodesByItems([], undefined, createContext()); + + expect(rootNode).toEqual({ key: ROOT, children: [], level: -1 }); + expect(nodeByKey).toEqual({}); + }); + + it('marks every node visible when visibleItems is not passed', () => { + const items = [{ id: 1, parentId: ROOT }, { id: 2, parentId: 1 }]; + + const { nodeByKey } = createNodesByItems(items, undefined, createContext()); + + expect(nodeByKey[1].visible).toBe(true); + expect(nodeByKey[2].visible).toBe(true); + }); + + it('marks only the nodes listed in visibleItems visible', () => { + const items = [{ id: 1, parentId: ROOT }, { id: 2, parentId: 1 }]; + + const { nodeByKey } = createNodesByItems(items, [items[1]], createContext()); + + expect(nodeByKey[1].visible).toBe(false); + expect(nodeByKey[2].visible).toBe(true); + }); + + it('hides every node when visibleItems is an empty array', () => { + const items = [{ id: 1, parentId: ROOT }]; + + const { nodeByKey } = createNodesByItems(items, [], createContext()); + + expect(nodeByKey[1].visible).toBe(false); + }); + + it('bails out without a root node when an item has no key (E1046)', () => { + const items = [ + { id: 1, parentId: ROOT }, + { id: undefined, parentId: 1 }, + { id: 3, parentId: 1 }, + ]; + + const { rootNode, nodeByKey } = createNodesByItems(items, undefined, createContext()); + + expect(rootNode).toBeUndefined(); + // The adapter keeps this partially filled map, so the nodes seen so far stay indexed. + expect(nodeByKey[1]).toMatchObject({ key: 1 }); + expect(nodeByKey[3]).toBeUndefined(); + }); +}); + +describe('fillNodes', () => { + it('assigns the level of every node depth first', () => { + const { root, nodes } = buildTree([ + { id: 1, parentId: ROOT }, + { id: 2, parentId: 1 }, + { id: 3, parentId: 2 }, + { id: 4, parentId: ROOT }, + ]); + + fillNodes(root.children, createLoadOptions(), createContext()); + + expect([1, 2, 3, 4].map((key) => nodes[key].level)).toEqual([0, 1, 2, 0]); + }); + + it('returns no keys for an empty node list', () => { + expect(fillNodes([], createLoadOptions(), createContext())).toEqual([]); + }); + + describe('hasChildren propagation', () => { + it('propagates up from the visible leaves', () => { + const { root, nodes } = buildTree([ + { id: 1, parentId: ROOT }, + { id: 2, parentId: 1 }, + { id: 3, parentId: 2 }, + ], [3]); + + fillNodes(root.children, createLoadOptions(), createContext()); + + expect([1, 2, 3].map((key) => nodes[key].hasChildren)).toEqual([true, true, false]); + expect(root.hasChildren).toBe(true); + }); + + it('stays false on a branch of hidden leaves', () => { + const { root, nodes } = buildTree([ + { id: 1, parentId: ROOT }, + { id: 2, parentId: 1 }, + ], []); + + fillNodes(root.children, createLoadOptions(), createContext()); + + expect(nodes[1].hasChildren).toBe(false); + expect(root.hasChildren).toBeUndefined(); + }); + + it('is overridden by hasItemsExpr on the node itself', () => { + const context = createContext({ + hasItemsGetter: (data): unknown => (data as Item).hasItems, + }); + const { root, nodes } = buildTree([ + { id: 1, parentId: ROOT, hasItems: false }, + { id: 2, parentId: 1 }, + ], [2]); + + fillNodes(root.children, createLoadOptions(), context); + + expect(nodes[1].hasChildren).toBe(false); + }); + }); + + describe('hasChildren of one node', () => { + interface Fixture { + items: Item[]; + visibleKeys: unknown[]; + } + + const leaf: Fixture = { + items: [{ id: 1, parentId: ROOT }], + visibleKeys: [], + }; + + const withVisibleChild: Fixture = { + items: [{ id: 1, parentId: ROOT }, { id: 2, parentId: 1 }], + visibleKeys: [2], + }; + + const withHiddenChild: Fixture = { + items: [{ id: 1, parentId: ROOT }, { id: 2, parentId: 1 }], + visibleKeys: [], + }; + + function calculateHasChildren( + fixture: Fixture, + options: LoadOperation, + context: NodesContext, + ): boolean | undefined { + const { root, nodes } = buildTree(fixture.items, fixture.visibleKeys); + + fillNodes(root.children, options, context); + + return nodes[1].hasChildren; + } + + describe('hasItemsExpr', () => { + const getterCases: [string, unknown, boolean][] = [ + ['true', true, true], + ['false', false, false], + ]; + + it.each(getterCases)('follows the getter when it returns %s', (_, hasItems, expected) => { + const context = createContext({ hasItemsGetter: () => hasItems }); + + // Shaped so the fallback would answer the opposite: the getter has to win. + const fixture = expected ? leaf : withVisibleChild; + + expect(calculateHasChildren(fixture, createLoadOptions(), context)).toBe(expected); + }); + + it('falls back to the children when the getter returns undefined', () => { + const context = createContext({ hasItemsGetter: () => undefined }); + + expect(calculateHasChildren(withVisibleChild, createLoadOptions(), context)).toBe(true); + }); + + it('is ignored while a store filter is applied', () => { + const context = createContext({ hasItemsGetter: () => true }); + const options = createLoadOptions({ storeFilter: ['id', '=', 1] }); + + expect(calculateHasChildren(leaf, options, context)).toBe(false); + }); + + it('is used under a store filter when parentIds are requested', () => { + const context = createContext({ hasItemsGetter: () => true }); + const options = createLoadOptions({ storeFilter: ['id', '=', 1], parentIds: [ROOT] }); + + expect(calculateHasChildren(leaf, options, context)).toBe(true); + }); + + // Pins current behaviour: an empty parentIds array is truthy here, while + // _processTreeStructure treats it as "no parent ids requested". + it('is used under a store filter when parentIds is an empty array', () => { + const context = createContext({ hasItemsGetter: () => true }); + const options = createLoadOptions({ storeFilter: ['id', '=', 1], parentIds: [] }); + + expect(calculateHasChildren(leaf, options, context)).toBe(true); + }); + + it('is used under a store filter in fullBranch mode', () => { + const context = createContext({ + hasItemsGetter: () => true, + isFullBranchFilterMode: true, + }); + const options = createLoadOptions({ storeFilter: ['id', '=', 1] }); + + expect(calculateHasChildren(leaf, options, context)).toBe(true); + }); + }); + + describe('children not loaded yet', () => { + it('assumes items exist under remote filtering when parentIds are requested', () => { + const options = createLoadOptions({ remoteFiltering: true, parentIds: [ROOT] }); + + expect(calculateHasChildren(leaf, options, createContext())).toBe(true); + }); + + it('assumes items exist under remote filtering in fullBranch mode', () => { + const context = createContext({ isFullBranchFilterMode: true }); + const options = createLoadOptions({ remoteFiltering: true }); + + expect(calculateHasChildren(leaf, options, context)).toBe(true); + }); + + it('assumes nothing once the children are loaded', () => { + const context = createContext({ isChildrenLoaded: { 1: true } }); + const options = createLoadOptions({ remoteFiltering: true, parentIds: [ROOT] }); + + expect(calculateHasChildren(leaf, options, context)).toBe(false); + }); + + it('assumes nothing without remote filtering', () => { + const options = createLoadOptions({ parentIds: [ROOT] }); + + expect(calculateHasChildren(leaf, options, createContext())).toBe(false); + }); + }); + + describe('local filtering in fullBranch mode', () => { + const options = createLoadOptions({ loadFilter: ['id', '=', 1] }); + const context = createContext({ isFullBranchFilterMode: true }); + + it('reports items for any child that survived the filter, visible or not', () => { + expect(calculateHasChildren(withHiddenChild, options, context)).toBe(true); + }); + + it('reports no items when every child was filtered out', () => { + expect(calculateHasChildren(leaf, options, context)).toBe(false); + }); + }); + + describe('fallback', () => { + it('reports items once a descendant propagated them', () => { + expect(calculateHasChildren(withVisibleChild, createLoadOptions(), createContext())) + .toBe(true); + }); + + it('reports none for a node nothing propagated into', () => { + expect(calculateHasChildren(leaf, createLoadOptions(), createContext())).toBe(false); + }); + }); + }); + + describe('expandedRowKeys', () => { + it('collects the keys to expand deepest first', () => { + const { root } = buildTree([ + { id: 1, parentId: ROOT }, + { id: 2, parentId: 1 }, + { id: 3, parentId: 2 }, + ]); + const expandedRowKeys = fillNodes( + root.children, + createLoadOptions({ expandVisibleNodes: true }), + createContext(), + ); + + expect(expandedRowKeys).toEqual([2, 1]); + }); + + it('collects nothing when expandVisibleNodes is not set', () => { + const { root, nodes } = buildTree([ + { id: 1, parentId: ROOT }, + { id: 2, parentId: 1 }, + ]); + const expandedRowKeys = fillNodes(root.children, createLoadOptions(), createContext()); + + expect(expandedRowKeys).toEqual([]); + expect(nodes[1].hasChildren).toBe(true); + }); + + it('skips a node that is hidden itself', () => { + const { root, nodes } = buildTree([ + { id: 1, parentId: ROOT }, + { id: 2, parentId: 1 }, + ], [2]); + const expandedRowKeys = fillNodes( + root.children, + createLoadOptions({ expandVisibleNodes: true }), + createContext(), + ); + + expect(nodes[1].hasChildren).toBe(true); + expect(expandedRowKeys).toEqual([]); + }); + + it('skips a node without children', () => { + const { root } = buildTree([{ id: 1, parentId: ROOT }]); + const expandedRowKeys = fillNodes( + root.children, + createLoadOptions({ expandVisibleNodes: true }), + createContext(), + ); + + expect(expandedRowKeys).toEqual([]); + }); + }); + + describe('fullBranch mode', () => { + const context = createContext({ + isFullBranchFilterMode: true, + hasItemsGetter: (data): unknown => (data as Item).hasItems, + }); + const options = createLoadOptions({ expandVisibleNodes: true }); + + it('expands a node that kept at least one visible child', () => { + const { root, nodes } = buildTree([ + { id: 1, parentId: ROOT }, + { id: 2, parentId: 1 }, + { id: 3, parentId: 1 }, + ], [1, 2]); + const expandedRowKeys = fillNodes(root.children, options, context); + + expect(expandedRowKeys).toEqual([1]); + expect(nodes[3].visible).toBe(false); + }); + + it('reveals the branch instead of expanding it when every child was filtered out', () => { + const { root, nodes } = buildTree([ + { id: 1, parentId: ROOT, hasItems: true }, + { id: 2, parentId: 1 }, + ], [1]); + const expandedRowKeys = fillNodes(root.children, options, context); + + expect(expandedRowKeys).toEqual([]); + expect(nodes[2].visible).toBe(true); + }); + + it('stops revealing a branch at a node that reports no items of its own', () => { + const { root, nodes } = buildTree([ + { id: 1, parentId: ROOT, hasItems: true }, + { id: 2, parentId: 1 }, + { id: 3, parentId: 2 }, + ], [1]); + + fillNodes(root.children, options, context); + + // foreachNodes only descends into nodes whose hasChildren is already truthy, + // and node 2 reports none once its own children were filtered out. + expect(nodes[2].visible).toBe(true); + expect(nodes[3].visible).toBe(false); + }); + + it('keeps revealing a branch through a node that reports items', () => { + const { root, nodes } = buildTree([ + { id: 1, parentId: ROOT, hasItems: true }, + { id: 2, parentId: 1, hasItems: true }, + { id: 3, parentId: 2 }, + ], [1]); + + fillNodes(root.children, options, context); + + expect(nodes[3].visible).toBe(true); + }); + + it('handles a node that reports items but has no loaded children', () => { + const { root, nodes } = buildTree([{ id: 1, parentId: ROOT, hasItems: true }], [1]); + const expandedRowKeys = fillNodes(root.children, options, context); + + expect(expandedRowKeys).toEqual([]); + expect(nodes[1].hasChildren).toBe(true); + }); + + it('expands a visible node with children when the mode is off', () => { + const plainContext = createContext({ + hasItemsGetter: (data): unknown => (data as Item).hasItems, + }); + const { root, nodes } = buildTree([ + { id: 1, parentId: ROOT, hasItems: true }, + { id: 2, parentId: 1 }, + ], [1]); + const expandedRowKeys = fillNodes(root.children, options, plainContext); + + expect(expandedRowKeys).toEqual([1]); + expect(nodes[2].visible).toBe(false); + }); + }); +}); + +describe('getVisibleNodes', () => { + const noneExpanded = (): boolean => false; + const allExpanded = (): boolean => true; + + // Mirrors the adapter: the tree is filled first, then flattened into rows. + function getVisibleKeys( + items: Item[], + visibleKeys: unknown[] | undefined, + isRowExpanded: (key: unknown) => boolean, + context = createContext(), + ): unknown[] { + const { root } = buildTree(items, visibleKeys); + + fillNodes(root.children, createLoadOptions(), context); + + return getVisibleNodes(root.children, isRowExpanded).map((node) => node.key); + } + + const parentAndChild: Item[] = [ + { id: 1, parentId: ROOT }, + { id: 2, parentId: 1 }, + ]; + + it('returns the node objects themselves', () => { + const { root, nodes } = buildTree([{ id: 1, parentId: ROOT }]); + + fillNodes(root.children, createLoadOptions(), createContext()); + + expect(getVisibleNodes(root.children, noneExpanded)[0]).toBe(nodes[1]); + }); + + it('returns nothing for an empty node list', () => { + expect(getVisibleNodes([], noneExpanded)).toEqual([]); + }); + + it('stops at a collapsed node', () => { + expect(getVisibleKeys(parentAndChild, undefined, noneExpanded)).toEqual([1]); + }); + + it('includes the children of an expanded node', () => { + expect(getVisibleKeys(parentAndChild, undefined, allExpanded)).toEqual([1, 2]); + }); + + it('returns the nodes in tree order', () => { + const items = [ + { id: 1, parentId: ROOT }, + { id: 2, parentId: 1 }, + { id: 3, parentId: 2 }, + { id: 4, parentId: ROOT }, + ]; + + expect(getVisibleKeys(items, undefined, allExpanded)).toEqual([1, 2, 3, 4]); + }); + + // How matchOnly/fullBranch surface a match whose ancestors were filtered out. + it('descends through a hidden node without listing it', () => { + expect(getVisibleKeys(parentAndChild, [2], noneExpanded)).toEqual([2]); + }); + + it('does not descend into a node that reports no children', () => { + const context = createContext({ + hasItemsGetter: (data): unknown => (data as Item).hasItems, + }); + const items = [ + { id: 1, parentId: ROOT, hasItems: false }, + { id: 2, parentId: 1 }, + ]; + + expect(getVisibleKeys(items, undefined, allExpanded, context)).toEqual([1]); + }); + + it('handles a node that reports children it has not loaded', () => { + const context = createContext({ + hasItemsGetter: (data): unknown => (data as Item).hasItems, + }); + const items = [{ id: 1, parentId: ROOT, hasItems: true }]; + + expect(getVisibleKeys(items, undefined, allExpanded, context)).toEqual([1]); + }); + + it('asks about expansion using each node key', () => { + const asked: unknown[] = []; + + getVisibleKeys(parentAndChild, undefined, (key) => { + asked.push(key); + + return true; + }); + + expect(asked).toEqual([1, 2]); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/tree_list/data_source_adapter/utils/nodes.ts b/packages/devextreme/js/__internal/grids/tree_list/data_source_adapter/utils/nodes.ts new file mode 100644 index 000000000000..ffbb7bd84f30 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/tree_list/data_source_adapter/utils/nodes.ts @@ -0,0 +1,194 @@ +import { isDefined } from '@js/core/utils/type'; +import type { RawItemData } from '@ts/grids/grid_core/data_source_adapter/types'; + +import treeListCore from '../../m_core'; +import type { LoadOperation, NodeByKey, TreeNode } from '../types'; + +export interface NodesContext { + rootValue: unknown; + isFullBranchFilterMode: boolean; + keyGetter: (data: unknown) => unknown; + parentIdGetter: (data: unknown) => unknown; + hasItemsGetter?: (data: unknown) => unknown; + isChildrenLoaded: Record<string, boolean>; +} + +export type ConvertContext = Pick<NodesContext, 'keyGetter' | 'parentIdGetter' | 'rootValue'>; + +export type FillNodesContext = Pick<NodesContext, 'hasItemsGetter' | 'isChildrenLoaded' | 'isFullBranchFilterMode'>; + +export function convertItemToNode( + item: unknown, + nodeByKey: NodeByKey, + context: ConvertContext, +): TreeNode { + const key = context.keyGetter(item) as string; + const itemParentId = context.parentIdGetter(item); + const parentId = (isDefined(itemParentId) ? itemParentId : context.rootValue) as string; + + const parentNode = nodeByKey[parentId] ?? { key: parentId, children: [] }; + nodeByKey[parentId] = parentNode; + + const node = nodeByKey[key] ?? { key, children: [] }; + nodeByKey[key] = node; + + node.data = item; + node.parent = parentNode; + + return node; +} + +export function createNodesByItems( + items: RawItemData[], + visibleItems: RawItemData[] | undefined, + context: ConvertContext, +): { rootNode?: TreeNode; nodeByKey: NodeByKey } { + const nodeByKey: NodeByKey = {}; + const visibleByKey: Record<string, boolean> = {}; + + visibleItems?.forEach((item) => { + visibleByKey[context.keyGetter(item) as string] = true; + }); + + for (const item of items) { + const node = convertItemToNode(item, nodeByKey, context); + + if (node.key === undefined) { + return { nodeByKey }; + } + + node.visible = !visibleItems || !!visibleByKey[node.key as string]; + if (node.parent) { + node.parent.children.push(node); + } + } + + const rootNode = nodeByKey[context.rootValue as string] ?? { + key: context.rootValue, + children: [], + }; + + rootNode.level = -1; + + return { rootNode, nodeByKey }; +} + +function calculateHasChildren( + node: TreeNode, + options: LoadOperation, + context: FillNodesContext, +): boolean { + const { parentIds } = options.storeLoadOptions; + const isFullBranch = context.isFullBranchFilterMode; + const canUseHasItemsGetter = !!parentIds || !options.storeLoadOptions.filter || isFullBranch; + + const hasItemsFromData = context.hasItemsGetter && canUseHasItemsGetter + ? context.hasItemsGetter(node.data) + : undefined; + + if (hasItemsFromData !== undefined) { + return !!hasItemsFromData; + } + + const isChildrenLoaded = context.isChildrenLoaded[node.key as string]; + + if (!isChildrenLoaded && options.remoteOperations?.filtering && (parentIds || isFullBranch)) { + return true; + } + + if (options.loadOptions?.filter && !options.remoteOperations?.filtering && isFullBranch) { + return !!node.children.length; + } + + return !!node.hasChildren; +} + +// In `fullBranch` mode a node whose children are all filtered out stays collapsed +// and its whole branch becomes visible instead. +function resolveNeedToExpand(node: TreeNode, isFullBranch: boolean): boolean { + if (!isFullBranch) { + return true; + } + + if (node.children.some((child) => child.visible)) { + return true; + } + + if (node.children.length) { + treeListCore.foreachNodes(node.children, (child: TreeNode) => { + child.visible = true; + }); + } + + return false; +} + +function fillNodesCore( + nodes: TreeNode[], + options: LoadOperation, + context: FillNodesContext, + expandedRowKeys: unknown[], + level: number, +): void { + nodes.forEach((node) => { + fillNodesCore(node.children, options, context, expandedRowKeys, level + 1); + + node.level = level; + node.hasChildren = calculateHasChildren(node, options, context); + + if (node.visible && node.hasChildren) { + const needToExpand = resolveNeedToExpand(node, context.isFullBranchFilterMode); + + if (options.expandVisibleNodes && needToExpand) { + expandedRowKeys.push(node.key); + } + } + + if (node.parent && (node.visible || node.hasChildren)) { + node.parent.hasChildren = true; + } + }); +} + +export function fillNodes( + nodes: TreeNode[], + options: LoadOperation, + context: FillNodesContext, +): unknown[] { + const expandedRowKeys: unknown[] = []; + + fillNodesCore(nodes, options, context, expandedRowKeys, 0); + + return expandedRowKeys; +} + +function collectVisibleNodes( + nodes: TreeNode[], + isRowExpanded: (key: unknown) => boolean, + result: TreeNode[], +): void { + nodes.forEach((node) => { + if (node.visible) { + result.push(node); + } + + const shouldDescend = (isRowExpanded(node.key) || !node.visible) + && node.hasChildren + && node.children.length > 0; + + if (shouldDescend) { + collectVisibleNodes(node.children, isRowExpanded, result); + } + }); +} + +export function getVisibleNodes( + nodes: TreeNode[], + isRowExpanded: (key: unknown) => boolean, +): TreeNode[] { + const result: TreeNode[] = []; + + collectVisibleNodes(nodes, isRowExpanded, result); + + return result; +} diff --git a/packages/devextreme/js/__internal/grids/tree_list/editing/__tests__/add_row_generated_key.integration.test.ts b/packages/devextreme/js/__internal/grids/tree_list/editing/__tests__/add_row_generated_key.integration.test.ts new file mode 100644 index 000000000000..6a9448f832ec --- /dev/null +++ b/packages/devextreme/js/__internal/grids/tree_list/editing/__tests__/add_row_generated_key.integration.test.ts @@ -0,0 +1,82 @@ +import { + afterEach, beforeEach, describe, expect, it, jest, +} from '@jest/globals'; +import { flushAsync } from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils'; + +import { + afterTest, + beforeTest, + createTreeList, +} from '../../__tests__/__mock__/helpers/utils'; + +const DATA = [ + { id: 1, parentId: 0, value: 'a' }, + { id: 2, parentId: 1, value: 'b' }, +]; + +interface InitNewRowEvent { + data: Record<string, unknown>; +} + +const addRowAndReadNewData = async ( + keyExpr: string | undefined, +): Promise<Record<string, unknown>> => { + const onInitNewRow = jest.fn<(e: InitNewRowEvent) => void>(); + + // A store built from a plain object option carries no key, unlike the array shorthand, which + // copies keyExpr onto the store it generates. + const { instance } = await createTreeList({ + dataSource: { store: { type: 'array', data: DATA } }, + keyExpr, + editing: { mode: 'row', allowAdding: true }, + onInitNewRow, + }); + + const added = instance.addRow(); + await flushAsync(); + await added; + + expect(onInitNewRow).toHaveBeenCalledTimes(1); + + return onInitNewRow.mock.calls[0][0].data; +}; + +describe('TreeList adds a generated key to a new row the store cannot identify', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + // TreeList answers key() from its key expression, which always resolves — to 'id' by default. + // Only the store knows whether it can identify a row, so _addRow has to ask the store. + it('generates __KEY__ when the store declares no key', async () => { + const data = await addRowAndReadNewData(undefined); + + expect(data.__KEY__).toEqual(expect.any(String)); + }); + + it('generates __KEY__ even when a key expression is declared, because the store still has none', async () => { + const data = await addRowAndReadNewData('id'); + + expect(data.__KEY__).toEqual(expect.any(String)); + }); +}); + +describe('TreeList leaves a new row alone when the store can identify it', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + it('does not generate __KEY__ for a keyed store', async () => { + const onInitNewRow = jest.fn<(e: InitNewRowEvent) => void>(); + const { instance } = await createTreeList({ + dataSource: { store: { type: 'array', data: DATA, key: 'id' } }, + editing: { mode: 'row', allowAdding: true }, + onInitNewRow, + }); + + const added = instance.addRow(); + await flushAsync(); + await added; + + expect(onInitNewRow).toHaveBeenCalledTimes(1); + expect(onInitNewRow.mock.calls[0][0].data).not.toHaveProperty('__KEY__'); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/tree_list/editing/m_editing.ts b/packages/devextreme/js/__internal/grids/tree_list/editing/m_editing.ts index 7c23b3a932b7..ca79247b904a 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/editing/m_editing.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/editing/m_editing.ts @@ -15,6 +15,7 @@ import gridCoreUtils from '@ts/grids/grid_core/m_utils'; import type { RowsView } from '../../grid_core/views/m_rows_view'; import type { TreeListDataController } from '../data_controller/m_data_controller'; +import type { TreeListDataSourceController } from '../data_source/data_source_controller'; import treeListCore from '../m_core'; const TREELIST_EXPAND_ICON_CONTAINER_CLASS = 'dx-treelist-icon-container'; @@ -25,6 +26,8 @@ const DATA_EDIT_DATA_INSERT_TYPE = 'insert'; class EditingController extends editingModule.controllers.editing { protected declare _dataController: TreeListDataController; + protected declare dataSourceController: TreeListDataSourceController; + protected _generateNewItem(key) { const item: any = super._generateNewItem(key); @@ -43,7 +46,7 @@ class EditingController extends editingModule.controllers.editing { } protected _setInsertAfterOrBeforeKey(change, parentKey) { - const dataSourceAdapter = this._dataController.dataSource(); + const dataSourceAdapter = this.dataSourceController.getAdapter(); const key = parentKey || dataSourceAdapter?.parentKeyOf(change.data); if (key !== undefined && key !== this.option('rootValue')) { @@ -55,7 +58,7 @@ class EditingController extends editingModule.controllers.editing { } protected _getLoadedRowIndex(items, change, isProcessedItems?) { - const dataSourceAdapter = this._dataController.dataSource(); + const dataSourceAdapter = this.dataSourceController.getAdapter(); const insertParentKey = this._getInternalData(change.key)?.insertInfo?.parentKey; const parentKey = insertParentKey !== undefined ? insertParentKey @@ -108,8 +111,7 @@ class EditingController extends editingModule.controllers.editing { const result = super._beforeSaveEditData.apply(this, arguments); if (change && change.type !== DATA_EDIT_DATA_INSERT_TYPE) { - const store = this._dataController?.store(); - const key = store?.key(); + const key = this.dataSourceController.store()?.key(); if (!isDefined(key)) { throw errors.Error('E1045'); @@ -135,7 +137,7 @@ class EditingController extends editingModule.controllers.editing { protected _addRowCore(data, parentKey, oldEditRowIndex) { const rootValue = this.option('rootValue'); - const dataSourceAdapter = this._dataController.dataSource(); + const dataSourceAdapter = this.dataSourceController.getAdapter(); const parentKeyGetter = dataSourceAdapter?.createParentIdGetter(); parentKey = parentKeyGetter ? parentKeyGetter(data) : parentKey; @@ -159,7 +161,7 @@ class EditingController extends editingModule.controllers.editing { } protected _initNewRow(options, parentKey?) { - const dataSourceAdapter = this._dataController.dataSource(); + const dataSourceAdapter = this.dataSourceController.getAdapter(); const parentIdSetter = dataSourceAdapter?.createParentIdSetter(); parentIdSetter?.(options.data, parentKey); diff --git a/packages/devextreme/js/__internal/grids/tree_list/m_focus.ts b/packages/devextreme/js/__internal/grids/tree_list/m_focus.ts index cf1ae9b9f206..dcd46912a52f 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/m_focus.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/m_focus.ts @@ -114,7 +114,7 @@ const data = ( return; } - const offset = findIndex(nodes, (node) => this.keyOf(node.data) === key); + const offset = findIndex(nodes, (node) => this.dataSourceController.keyOf(node.data) === key); let pageIndex = -1; diff --git a/packages/devextreme/js/__internal/grids/tree_list/m_widget_base.ts b/packages/devextreme/js/__internal/grids/tree_list/m_widget_base.ts index c933bd272e7e..e226743ad372 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/m_widget_base.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/m_widget_base.ts @@ -1,5 +1,7 @@ import './module_not_extended/column_headers'; import './m_columns_controller'; +import './data_source/data_source_module'; +import './module_not_extended/filter'; import './data_controller/m_data_controller'; import './module_not_extended/sorting'; import './rows/m_rows'; @@ -19,6 +21,7 @@ import treeListCore from './m_core'; const TREELIST_CLASS = 'dx-treelist'; treeListCore.registerModulesOrder([ + 'dataSource', 'stateStoring', 'columns', 'selection', @@ -32,6 +35,7 @@ treeListCore.registerModulesOrder([ 'masterDetail', 'validating', 'adaptivity', + 'filter', 'data', 'virtualScrolling', 'aiColumn', diff --git a/packages/devextreme/js/__internal/grids/tree_list/module_not_extended/filter.ts b/packages/devextreme/js/__internal/grids/tree_list/module_not_extended/filter.ts new file mode 100644 index 000000000000..dc4b83b8545c --- /dev/null +++ b/packages/devextreme/js/__internal/grids/tree_list/module_not_extended/filter.ts @@ -0,0 +1,5 @@ +import { filterModule } from '@ts/grids/grid_core/filter/filter_module'; + +import treeListCore from '../m_core'; + +treeListCore.registerModule('filter', filterModule); diff --git a/packages/devextreme/js/__internal/grids/tree_list/selection/m_selection.ts b/packages/devextreme/js/__internal/grids/tree_list/selection/m_selection.ts index 985a78826091..7e6ba1084bcf 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/selection/m_selection.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/selection/m_selection.ts @@ -95,14 +95,14 @@ SelectionController & TreeListSelectionControllerExtension config.plainItems = (cached) => { let result; if (cached) { - result = this._dataController.getCachedStoreData(); + result = this.dataSourceController.getCachedStoreData(); } result ||= plainItems.apply(this, arguments as any).map((item) => item.data); return result || []; }; config.isItemSelected = (item) => { - const key = this._dataController.keyOf(item); + const key = this.dataSourceController.keyOf(item); return this.isRowSelected(key); }; diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts index ebc5ff07fa50..80418b75a673 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts @@ -1,4 +1,5 @@ import { within } from '@testing-library/dom'; +import { SCREEN_READER_ONLY_CLASS } from '@ts/core/widget/widget'; import { ToolbarModel } from '@ts/scheduler/__tests__/__mock__/model/toolbar'; import { APPOINTMENT_POPUP_CLASS } from '../../../appointment_popup/popup'; @@ -55,7 +56,7 @@ export class SchedulerModel { } getStatusContent(): string { - const statusElement = this.container.querySelector('.dx-screen-reader-only'); + const statusElement = this.container.querySelector(`.${SCREEN_READER_ONLY_CLASS}`); return statusElement?.textContent ?? ''; } diff --git a/packages/devextreme/js/__internal/scheduler/a11y_status/a11y_status_render.ts b/packages/devextreme/js/__internal/scheduler/a11y_status/a11y_status_render.ts index 558f93c69c36..788d4d742fd2 100644 --- a/packages/devextreme/js/__internal/scheduler/a11y_status/a11y_status_render.ts +++ b/packages/devextreme/js/__internal/scheduler/a11y_status/a11y_status_render.ts @@ -1,8 +1,9 @@ import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; +import { SCREEN_READER_ONLY_CLASS } from '@ts/core/widget/widget'; const CLASSES = { - container: 'dx-screen-reader-only', + container: SCREEN_READER_ONLY_CLASS, }; export const createA11yStatusContainer = (statusText = ''): dxElementWrapper => $('<div>') diff --git a/packages/devextreme/js/__internal/scheduler/utils/loader/utils.ts b/packages/devextreme/js/__internal/scheduler/utils/loader/utils.ts index ccf48a23701c..982fd38239af 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/loader/utils.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/loader/utils.ts @@ -1,6 +1,6 @@ import type { DataSourceLike } from '@js/data/data_source'; import DataSource from '@js/data/data_source'; -import { normalizeDataSourceOptions } from '@ts/data/data_source/m_utils'; +import { normalizeDataSourceOptions } from '@ts/data/data_source/utils'; export const normalizeDataSource = <T>( dataSourceOptions: DataSourceLike<T> | null | undefined, @@ -19,6 +19,7 @@ export const normalizeDataSource = <T>( ...options, }; + // @ts-expect-error the public DataSource constructor takes the public option types return new DataSource(result); }; diff --git a/packages/devextreme/js/__internal/ui/button/button.tsx b/packages/devextreme/js/__internal/ui/button/button.tsx index e9b8ef9ce442..fdc740883193 100644 --- a/packages/devextreme/js/__internal/ui/button/button.tsx +++ b/packages/devextreme/js/__internal/ui/button/button.tsx @@ -1,10 +1,7 @@ import { click } from '@js/common/core/events/short'; -import messageLocalization from '@js/common/core/localization/message'; import devices from '@js/core/devices'; import type { DefaultOptionsRule } from '@js/core/options/utils'; import { convertRulesToOptions, createDefaultOptionRules } from '@js/core/options/utils'; -import { getImageSourceType } from '@js/core/utils/icon'; -import { camelize } from '@js/core/utils/inflector'; import type { DxEvent } from '@js/events'; import type { Properties as ButtonProperties, TemplateData } from '@js/ui/button.d'; import { current, isMaterial } from '@js/ui/themes'; @@ -16,6 +13,7 @@ import type { EffectReturn } from '@ts/core/r1/utils/effect_return'; import { getTemplate } from '@ts/core/r1/utils/index'; import { Widget } from '@ts/core/r1/widget'; import { combineClasses } from '@ts/core/utils/combine_classes'; +import { getImageAriaLabel } from '@ts/core/utils/m_icon'; import { createRef as infernoCreateRef } from 'inferno'; import { Icon } from './icon'; @@ -229,34 +227,7 @@ export class Button extends InfernoWrapperComponent<ButtonProps> { get aria(): Record<string, string> { const { icon, text } = this.props; - let label = text ?? ''; - - if (!text && icon) { - const iconSource = getImageSourceType(icon); - - switch (iconSource) { - case 'image': - { - const notURLRegexp = /^(?!(?:https?:\/\/)|(?:ftp:\/\/)|(?:www\.))[^\s]+$/; - const isPathToImage = !icon.includes('base64') && notURLRegexp.test(icon); - label = isPathToImage ? icon.replace(/.+\/([^.]+)\..+$/, '$1') : ''; - break; - } - case 'dxIcon': - label = messageLocalization.format(camelize(icon, true)) || icon; - break; - case 'fontIcon': - label = icon; - break; - case 'svg': { - const titleRegexp = /<title>(.*?)<\/title>/; - label = titleRegexp.exec(icon)?.[1] ?? ''; - break; - } - default: - break; - } - } + const label = !text && icon ? getImageAriaLabel(icon) : text; return { role: 'button', diff --git a/packages/devextreme/js/__internal/ui/calendar/calendar.views.ts b/packages/devextreme/js/__internal/ui/calendar/calendar.views.ts index ee55cec31e05..6383dbca9d1c 100644 --- a/packages/devextreme/js/__internal/ui/calendar/calendar.views.ts +++ b/packages/devextreme/js/__internal/ui/calendar/calendar.views.ts @@ -8,6 +8,7 @@ import dateSerialization from '@js/core/utils/date_serialization'; import type { CalendarSelectionMode, DayOfWeek, WeekNumberRule, } from '@js/ui/calendar'; +import { SCREEN_READER_ONLY_CLASS } from '@ts/core/widget/widget'; import type { BaseViewProperties } from './calendar.base_view'; import BaseView from './calendar.base_view'; @@ -18,8 +19,6 @@ const CALENDAR_WEEK_NUMBER_CELL_CLASS = 'dx-calendar-week-number-cell'; const CALENDAR_WEEK_NUMBER_HEADER_CELL_CLASS = 'dx-week-number-header'; const CALENDAR_WEEK_SELECTION_CLASS = 'dx-calendar-week-selection'; -const SCREEN_READER_ONLY_CLASS = 'dx-screen-reader-only'; - const WEEK_NUMBER_TEXT = 'WeekNumber'; export interface MonthViewProperties extends BaseViewProperties { diff --git a/packages/devextreme/js/__internal/ui/chat/message_box/chat_text_area.ts b/packages/devextreme/js/__internal/ui/chat/message_box/chat_text_area.ts index b06013080e0d..6a809d60bb82 100644 --- a/packages/devextreme/js/__internal/ui/chat/message_box/chat_text_area.ts +++ b/packages/devextreme/js/__internal/ui/chat/message_box/chat_text_area.ts @@ -619,7 +619,7 @@ class ChatTextArea extends TextArea<Properties> { return maxHeight; } - _keyPressHandler(e: { originalEvent: InputEvent & KeyboardEvent }): void { + _keyPressHandler(e: DxEvent): void { super._keyPressHandler(e); this._updateButtonsState(); diff --git a/packages/devextreme/js/__internal/ui/collection/collection_widget.edit.ts b/packages/devextreme/js/__internal/ui/collection/collection_widget.edit.ts index 06d89a472973..85bcffe6447f 100644 --- a/packages/devextreme/js/__internal/ui/collection/collection_widget.edit.ts +++ b/packages/devextreme/js/__internal/ui/collection/collection_widget.edit.ts @@ -277,7 +277,6 @@ class CollectionWidget< if (that._disposed) { return; } - // @ts-expect-error arguments const items = normalizeLoadResult(loadResult).data; dataController.applyMapFunction(items); diff --git a/packages/devextreme/js/__internal/ui/collection/collection_widget.live_update.ts b/packages/devextreme/js/__internal/ui/collection/collection_widget.live_update.ts index d1d0c0fdfd0b..a73e374f5136 100644 --- a/packages/devextreme/js/__internal/ui/collection/collection_widget.live_update.ts +++ b/packages/devextreme/js/__internal/ui/collection/collection_widget.live_update.ts @@ -204,7 +204,6 @@ class CollectionWidgetLiveUpdate< } else { const changedItem = items[indexByKey(keyInfo, items, change.key)]; if (changedItem) { - // @ts-expect-error ts-error update(keyInfo, items, change.key, change.data).done(() => { this._renderItem( items.indexOf(changedItem), @@ -224,7 +223,6 @@ class CollectionWidgetLiveUpdate< isPartialRefresh?: boolean, ): void { when( - // @ts-expect-error ts-error isPartialRefresh ?? insert(keyInfo, items, change.data, change.index), ).done(() => { this._beforeItemElementInserted(change); diff --git a/packages/devextreme/js/__internal/ui/color_box/color_view.ts b/packages/devextreme/js/__internal/ui/color_box/color_view.ts index 349261fdff67..55072f72e081 100644 --- a/packages/devextreme/js/__internal/ui/color_box/color_view.ts +++ b/packages/devextreme/js/__internal/ui/color_box/color_view.ts @@ -18,8 +18,8 @@ import { isCommandKeyPressed } from '@ts/events/utils/index'; import Draggable from '@ts/m_draggable'; import type { EditorProperties, ValueChangedEvent } from '@ts/ui/editor/editor'; import Editor from '@ts/ui/editor/editor'; -import NumberBox from '@ts/ui/number_box/m_number_box'; -import { WIDGET_CLASS as NUMBERBOX_CLASS } from '@ts/ui/number_box/m_number_box.base'; +import NumberBox from '@ts/ui/number_box/number_box'; +import { WIDGET_CLASS as NUMBERBOX_CLASS } from '@ts/ui/number_box/number_box.base'; import TextBox from '@ts/ui/text_box/text_box'; const COLOR_VIEW_CLASS = 'dx-colorview'; diff --git a/packages/devextreme/js/__internal/ui/date_box/date_box.mask.ts b/packages/devextreme/js/__internal/ui/date_box/date_box.mask.ts index d62dd2b23c62..378a4486fb42 100644 --- a/packages/devextreme/js/__internal/ui/date_box/date_box.mask.ts +++ b/packages/devextreme/js/__internal/ui/date_box/date_box.mask.ts @@ -273,7 +273,7 @@ class DateBoxMask< this._caret(this._getActivePartProp('caret')); } - _keyPressHandler(e: { originalEvent: InputEvent & KeyboardEvent }): void { + _keyPressHandler(e: DxEvent<InputEvent>): void { const { originalEvent: event } = e; const isBackwardDeletion = event?.inputType === 'deleteContentBackward'; diff --git a/packages/devextreme/js/__internal/ui/date_box/time_view.ts b/packages/devextreme/js/__internal/ui/date_box/time_view.ts index a8ba0626d342..314967f395a4 100644 --- a/packages/devextreme/js/__internal/ui/date_box/time_view.ts +++ b/packages/devextreme/js/__internal/ui/date_box/time_view.ts @@ -9,8 +9,8 @@ import Box from '@ts/ui/box'; import dateUtils from '@ts/ui/date_box/date_utils'; import type { EditorProperties } from '@ts/ui/editor/editor'; import Editor from '@ts/ui/editor/editor'; -import NumberBox from '@ts/ui/number_box/m_number_box'; -import type { NumberBoxMaskProperties } from '@ts/ui/number_box/m_number_box.mask'; +import NumberBox from '@ts/ui/number_box/number_box'; +import type { NumberBoxMaskProperties } from '@ts/ui/number_box/number_box.mask'; import SelectBox from '@ts/ui/select_box'; const TIMEVIEW_CLASS = 'dx-timeview'; diff --git a/packages/devextreme/js/__internal/ui/drop_down_editor/drop_down_button.ts b/packages/devextreme/js/__internal/ui/drop_down_editor/drop_down_button.ts index f528e4b59f2e..4038648987ff 100644 --- a/packages/devextreme/js/__internal/ui/drop_down_editor/drop_down_button.ts +++ b/packages/devextreme/js/__internal/ui/drop_down_editor/drop_down_button.ts @@ -35,9 +35,7 @@ interface EditorWithDropDown { export default class DropDownButton< // eslint-disable-next-line @typescript-eslint/no-explicit-any TComponent extends Editor<any> & EditorWithDropDown = DropDownEditor, -> extends TextEditorButton<TComponent> { - declare instance: Button | null; - +> extends TextEditorButton<TComponent, Button> { currentTemplate: DropDownEditorProperties['dropDownButtonTemplate'] | null; constructor(name: string, editor: TComponent, options: ButtonProperties) { diff --git a/packages/devextreme/js/__internal/ui/hierarchical_collection/data_adapter.ts b/packages/devextreme/js/__internal/ui/hierarchical_collection/data_adapter.ts index 4e5370ec834f..c5048c5be6d0 100644 --- a/packages/devextreme/js/__internal/ui/hierarchical_collection/data_adapter.ts +++ b/packages/devextreme/js/__internal/ui/hierarchical_collection/data_adapter.ts @@ -691,8 +691,8 @@ class DataAdapter { lookForParents(matches, 0); if (this.options.sort) { + // @ts-expect-error queryByOptions().toArray() is typed as unknown[] matches = storeHelper - // @ts-expect-error ts-error .queryByOptions(query(matches), { sort: this.options.sort, langParams: this.options.langParams, diff --git a/packages/devextreme/js/__internal/ui/list/list.base.ts b/packages/devextreme/js/__internal/ui/list/list.base.ts index d3411be672fd..3cb11fe713f5 100644 --- a/packages/devextreme/js/__internal/ui/list/list.base.ts +++ b/packages/devextreme/js/__internal/ui/list/list.base.ts @@ -45,6 +45,7 @@ import { render } from '@ts/core/utils/ink_ripple'; import supportUtils from '@ts/core/utils/m_support'; import type { OptionChanged } from '@ts/core/widget/types'; import type { SupportedKeys } from '@ts/core/widget/widget'; +import { SCREEN_READER_ONLY_CLASS } from '@ts/core/widget/widget'; import { getDataSourceOptions } from '@ts/data/data_converter/grouped'; import type { CollectionItemInfo, @@ -132,6 +133,8 @@ export class ListBase extends CollectionWidget<ListBaseProperties, Item> { _$nextButton!: dxElementWrapper | null; + _$a11yStatusContainer?: dxElementWrapper; + _holdTimer?: ReturnType<typeof setTimeout>; _loadNextPageTimer?: ReturnType<typeof setTimeout>; @@ -975,6 +978,7 @@ export class ListBase extends CollectionWidget<ListBaseProperties, Item> { this._itemElementsCache = $(); this.$element().addClass(LIST_CLASS); + this._renderA11yStatusContainer(); super._initMarkup(); const { useInkRipple } = this.option(); @@ -997,10 +1001,37 @@ export class ListBase extends CollectionWidget<ListBaseProperties, Item> { const isEmpty = super._renderEmptyMessage(rootNodes); this.setAria({ role: isEmpty ? undefined : 'application' }, this._focusTarget()); + this._updateA11yStatusText(); return isEmpty; } + _renderA11yStatusContainer(): void { + const isContainerRendered = this._$a11yStatusContainer?.parent().is(this.$element()); + + if (isContainerRendered) { + return; + } + + this._$a11yStatusContainer = $('<div>') + .addClass(SCREEN_READER_ONLY_CLASS) + .attr('role', 'status') + .appendTo(this.$element()); + } + + _updateA11yStatusText(): void { + if (this._dataController.isLoading()) { + this._$a11yStatusContainer?.text(''); + return; + } + + const { noDataText } = this.option(); + const itemsCount = this._editStrategy.itemsGetter().length; + const itemsLabel = messageLocalization.format('dxList-listAriaLabel'); + + this._$a11yStatusContainer?.text(itemsCount ? `${itemsLabel}: ${itemsCount}` : noDataText ?? ''); + } + _isMultiSelectMode(): boolean { const { selectionMode } = this.option(); return selectionMode === 'multiple' || selectionMode === 'all'; diff --git a/packages/devextreme/js/__internal/ui/number_box/__tests__/utils.test.ts b/packages/devextreme/js/__internal/ui/number_box/__tests__/utils.test.ts new file mode 100644 index 000000000000..a767c38ea29e --- /dev/null +++ b/packages/devextreme/js/__internal/ui/number_box/__tests__/utils.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from '@jest/globals'; + +import { + asPattern, getNthOccurrence, getRealSeparatorIndex, splitByIndex, +} from '../utils'; + +describe('getNthOccurrence', () => { + it('returns the position of the requested occurrence', () => { + expect(getNthOccurrence('1.234.567', '.', 1)).toBe(1); + expect(getNthOccurrence('1.234.567', '.', 2)).toBe(5); + }); + + it('returns -1 when the character is absent', () => { + expect(getNthOccurrence('1234', '.', 1)).toBe(-1); + expect(getNthOccurrence('', '.', 1)).toBe(-1); + }); + + it('returns -1 when there are fewer occurrences than requested', () => { + expect(getNthOccurrence('abc.d', '.', 2)).toBe(-1); + expect(getNthOccurrence('abc.d', '.', 3)).toBe(-1); + expect(getNthOccurrence('a.b.c', '.', 4)).toBe(-1); + expect(getNthOccurrence('a.b.c', '.', 5)).toBe(-1); + }); + + it('returns -1 when no occurrence is requested', () => { + expect(getNthOccurrence('1.2', '.', 0)).toBe(-1); + }); + + it('counts adjacent occurrences separately', () => { + expect(getNthOccurrence('...', '.', 1)).toBe(0); + expect(getNthOccurrence('...', '.', 3)).toBe(2); + expect(getNthOccurrence('...', '.', 4)).toBe(-1); + }); +}); + +describe('getRealSeparatorIndex', () => { + it('reports the first separator of a plain pattern', () => { + expect(getRealSeparatorIndex('#0.00')).toEqual({ occurrence: 1, index: 2 }); + expect(getRealSeparatorIndex('#,##0.##')).toEqual({ occurrence: 1, index: 5 }); + }); + + it('skips separators inside escaped stubs', () => { + expect(getRealSeparatorIndex("'.'#0.00")).toEqual({ occurrence: 2, index: 5 }); + expect(getRealSeparatorIndex("'..' #0.00")).toEqual({ occurrence: 3, index: 7 }); + }); + + it('reports a missing separator for patterns without a float part', () => { + expect(getRealSeparatorIndex('#,##0')).toEqual({ occurrence: 1, index: -1 }); + }); + + it('treats a format that is not a pattern as having no separator', () => { + expect(getRealSeparatorIndex({ type: 'fixedPoint', precision: 2 })) + .toEqual({ occurrence: 1, index: -1 }); + expect(getRealSeparatorIndex(undefined)).toEqual({ occurrence: 1, index: -1 }); + }); + + it('locates the separator of a stubbed pattern in the formatted text', () => { + const { occurrence } = getRealSeparatorIndex("'...' #0.0"); + + expect(getNthOccurrence('... 12.5', '.', occurrence)).toBe(6); + expect(getNthOccurrence('.. 12', '.', occurrence)).toBe(-1); + }); +}); + +describe('splitByIndex', () => { + it('splits the text around the given index', () => { + expect(splitByIndex('12.34', 2)).toEqual(['12', '34']); + }); + + it('keeps the text whole when there is no split point', () => { + expect(splitByIndex('1234', -1)).toEqual(['1234']); + }); +}); + +describe('asPattern', () => { + it('passes a pattern string through', () => { + expect(asPattern('#0.00')).toBe('#0.00'); + }); + + it('reports an empty pattern for formats that are not strings', () => { + expect(asPattern(undefined)).toBe(''); + expect(asPattern({ type: 'currency' })).toBe(''); + expect(asPattern((value: number): string => String(value))).toBe(''); + }); +}); diff --git a/packages/devextreme/js/__internal/ui/number_box/m_number_box.caret.ts b/packages/devextreme/js/__internal/ui/number_box/m_number_box.caret.ts deleted file mode 100644 index 0ba137034deb..000000000000 --- a/packages/devextreme/js/__internal/ui/number_box/m_number_box.caret.ts +++ /dev/null @@ -1,142 +0,0 @@ -import number from '@js/common/core/localization/number'; -import { escapeRegExp } from '@js/core/utils/common'; -import { fitIntoRange } from '@js/core/utils/math'; - -import { getNthOccurrence, getRealSeparatorIndex, splitByIndex } from './m_utils'; - -export const getCaretBoundaries = function (text, format) { - if (typeof format === 'string') { - const signParts = format.split(';'); - const sign = number.getSign(text, format); - - signParts[1] = signParts[1] || `-${signParts[0]}`; - format = signParts[sign < 0 ? 1 : 0]; - - const mockEscapedStubs = (str) => str.replace(/'([^']*)'/g, (str) => str.split('').map(() => ' ').join('').substr(2)); - - format = mockEscapedStubs(format); - - // @ts-expect-error - const prefixStubLength = /^[^#0.,]*/.exec(format)[0].length; - // @ts-expect-error - const postfixStubLength = /[^#0.,]*$/.exec(format)[0].length; - - return { - start: prefixStubLength, - end: text.length - postfixStubLength, - }; - } - return { start: 0, end: text.length }; -}; - -// eslint-disable-next-line @typescript-eslint/naming-convention -const _getDigitCountBeforeIndex = function (index, text) { - const decimalSeparator = number.getDecimalSeparator(); - const regExp = new RegExp(`[^0-9${escapeRegExp(decimalSeparator)}]`, 'g'); - const textBeforePosition = text.slice(0, index); - - return textBeforePosition.replace(regExp, '').length; -}; - -// eslint-disable-next-line @typescript-eslint/naming-convention -const _reverseText = function (text) { - return text.split('').reverse().join(''); -}; - -// eslint-disable-next-line @typescript-eslint/naming-convention -const _getDigitPositionByIndex = function (digitIndex, text) { - if (!digitIndex) { - return -1; - } - - const regExp = /[0-9]/g; - let counter = 1; - let index: number | null = null; - let result = regExp.exec(text); - - while (result) { - index = result.index; - if (counter >= digitIndex) { - return index; - } - counter++; - result = regExp.exec(text); - } - - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return index === null ? text.length : index; -}; - -// eslint-disable-next-line @typescript-eslint/naming-convention -const _trimNonNumericCharsFromEnd = function (text) { - return text.replace(/[^0-9e]+$/, ''); -}; - -export const getCaretWithOffset = function (caret, offset) { - if (caret.start === undefined) { - caret = { start: caret, end: caret }; - } - - return { - start: caret.start + offset, - end: caret.end + offset, - }; -}; - -export const getCaretAfterFormat = function (text, formatted, caret, format) { - caret = getCaretWithOffset(caret, 0); - - const point = number.getDecimalSeparator(); - const isSeparatorBasedText = isSeparatorBasedString(text); - const realSeparatorOccurrenceIndex = getRealSeparatorIndex(format).occurrence; - const pointPosition = isSeparatorBasedText ? 0 : getNthOccurrence(text, point, realSeparatorOccurrenceIndex); - const newPointPosition = getNthOccurrence(formatted, point, realSeparatorOccurrenceIndex); - const textParts = splitByIndex(text, pointPosition); - const formattedParts = splitByIndex(formatted, newPointPosition); - const isCaretOnFloat = pointPosition !== -1 && caret.start > pointPosition; - - if (isCaretOnFloat) { - const relativeIndex = caret.start - pointPosition - 1; - const digitsBefore = _getDigitCountBeforeIndex(relativeIndex, textParts[1]); - const newPosition = formattedParts[1] ? newPointPosition + 1 + _getDigitPositionByIndex(digitsBefore, formattedParts[1]) + 1 : formatted.length; - - return getCaretInBoundaries(newPosition, formatted, format); - } - const formattedIntPart = _trimNonNumericCharsFromEnd(formattedParts[0]); - const positionFromEnd = textParts[0].length - caret.start; - const digitsFromEnd = _getDigitCountBeforeIndex(positionFromEnd, _reverseText(textParts[0])); - const newPositionFromEnd = _getDigitPositionByIndex(digitsFromEnd, _reverseText(formattedIntPart)); - const newPositionFromBegin = formattedIntPart.length - (newPositionFromEnd + 1); - - return getCaretInBoundaries(newPositionFromBegin, formatted, format); -}; - -function isSeparatorBasedString(text) { - return text.length === 1 && !!text.match(/^[,.][0-9]*$/g); -} - -export const isCaretInBoundaries = function (caret, text, format) { - caret = getCaretWithOffset(caret, 0); - - const boundaries = getCaretInBoundaries(caret, text, format); - return caret.start >= boundaries.start && caret.end <= boundaries.end; -}; - -export function getCaretInBoundaries(caret, text, format) { - caret = getCaretWithOffset(caret, 0); - - const boundaries = getCaretBoundaries(text, format); - const adjustedCaret = { - start: fitIntoRange(caret.start, boundaries.start, boundaries.end), - end: fitIntoRange(caret.end, boundaries.start, boundaries.end), - }; - - return adjustedCaret; -} - -export const getCaretOffset = function (previousText, newText, format) { - const previousBoundaries = getCaretBoundaries(previousText, format); - const newBoundaries = getCaretBoundaries(newText, format); - - return newBoundaries.start - previousBoundaries.start; -}; diff --git a/packages/devextreme/js/__internal/ui/number_box/m_number_box.spins.ts b/packages/devextreme/js/__internal/ui/number_box/m_number_box.spins.ts deleted file mode 100644 index 75560e162804..000000000000 --- a/packages/devextreme/js/__internal/ui/number_box/m_number_box.spins.ts +++ /dev/null @@ -1,132 +0,0 @@ -import eventsEngine from '@js/common/core/events/core/events_engine'; -import pointer from '@js/common/core/events/pointer'; -import { addNamespace } from '@js/common/core/events/utils/index'; -import type { dxElementWrapper } from '@js/core/renderer'; -import $ from '@js/core/renderer'; -import { extend } from '@js/core/utils/extend'; -import TextEditorButton from '@ts/ui/text_box/texteditor_button_collection/button'; - -import SpinButton from './m_number_box.spin'; - -const SPIN_CLASS = 'dx-numberbox-spin'; -const SPIN_CONTAINER_CLASS = 'dx-numberbox-spin-container'; -const SPIN_TOUCH_FRIENDLY_CLASS = 'dx-numberbox-spin-touch-friendly'; - -export default class SpinButtons extends TextEditorButton { - _attachEvents( - instance: dxElementWrapper, - $spinContainer: dxElementWrapper, - ): void { - const { editor } = this; - - if (!editor) { - return; - } - - const eventName = addNamespace(pointer.down, editor.NAME ?? ''); - const $spinContainerChildren = $spinContainer.children(); - - const pointerDownAction = editor._createAction( - // @ts-expect-error Private API - (e) => { this.editor?._spinButtonsPointerDownHandler(e); }, - ); - - eventsEngine.off($spinContainer, eventName); - - eventsEngine.on( - $spinContainer, - eventName, - (e) => pointerDownAction({ event: e }), - ); - - SpinButton.getInstance($spinContainerChildren.eq(0)).option( - 'onChange', - // @ts-expect-error Private API - (e) => { this.editor?._spinUpChangeHandler(e); }, - ); - - SpinButton.getInstance($spinContainerChildren.eq(1)).option( - 'onChange', - // @ts-expect-error Private API - (e) => { this.editor?._spinDownChangeHandler(e); }, - ); - } - - _create(): { - $element: dxElementWrapper; - instance: dxElementWrapper; - } { - const { editor } = this; - - const $spinContainer = $('<div>').addClass(SPIN_CONTAINER_CLASS); - const $spinUp = $('<div>').appendTo($spinContainer); - const $spinDown = $('<div>').appendTo($spinContainer); - const options = this._getOptions(); - - this._addToContainer($spinContainer); - - editor?._createComponent($spinUp, SpinButton, extend({ direction: 'up' }, options)); - editor?._createComponent($spinDown, SpinButton, extend({ direction: 'down' }, options)); - - this._legacyRender(editor?.$element(), this._isTouchFriendly(), options.visible); - - return { - instance: $spinContainer, - $element: $spinContainer, - }; - } - - _getOptions() { - const { editor } = this; - - const visible = this._isVisible(); - const disabled = editor?.option('disabled'); - - return { - visible, - disabled, - }; - } - - // @ts-expect-error - _isVisible() { - const { editor } = this; - - return super._isVisible() && editor?.option('showSpinButtons'); - } - - _isTouchFriendly() { - const { editor } = this; - - return editor?.option('showSpinButtons') && editor?.option('useLargeSpinButtons'); - } - - // TODO: get rid of it - _legacyRender($editor, isTouchFriendly, isVisible) { - $editor.toggleClass(SPIN_TOUCH_FRIENDLY_CLASS, isTouchFriendly); - $editor.toggleClass(SPIN_CLASS, isVisible); - } - - // @ts-expect-error - update(): void { - const shouldUpdate = super.update(); - - if (shouldUpdate) { - const { editor, instance } = this; - - const $editor = editor?.$element(); - const isVisible = this._isVisible(); - const isTouchFriendly = this._isTouchFriendly(); - // @ts-expect-error - const $spinButtons = instance.children(); - const spinUp = SpinButton.getInstance($spinButtons.eq(0)); - const spinDown = SpinButton.getInstance($spinButtons.eq(1)); - const options = this._getOptions(); - - spinUp.option(options); - spinDown.option(options); - - this._legacyRender($editor, isTouchFriendly, isVisible); - } - } -} diff --git a/packages/devextreme/js/__internal/ui/number_box/m_utils.ts b/packages/devextreme/js/__internal/ui/number_box/m_utils.ts deleted file mode 100644 index 388587c56f7f..000000000000 --- a/packages/devextreme/js/__internal/ui/number_box/m_utils.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { adjust, roundFloatPart } from '@js/core/utils/math'; - -const getRealSeparatorIndex = function (str) { - let quoteBalance = 0; - let separatorCount = 0; - - for (let i = 0; i < str.length; ++i) { - if (str[i] === '\'') { - quoteBalance++; - } - if (str[i] === '.') { - ++separatorCount; - if (quoteBalance % 2 === 0) { - return { - occurrence: separatorCount, - index: i, - }; - } - } - } - - return { occurrence: 1, index: -1 }; -}; - -const getNthOccurrence = function (str, c, n) { - let i = -1; - - while (n-- && i++ < str.length) { - i = str.indexOf(c, i); - } - - return i; -}; - -const splitByIndex = function (str, index) { - if (index === -1) { - return [str]; - } - - return [str.slice(0, index), str.slice(index + 1)]; -}; - -const adjustPercentValue = function (rawValue, interval) { - if (!rawValue) { - return rawValue; - } - - return adjust(rawValue / 100, interval / 100); -}; - -const roundFloatPartPercentValue = function (rawValue: number, precision: number) { - if (!rawValue) { - return rawValue; - } - return roundFloatPart(rawValue / 100, precision); -}; - -export { - adjustPercentValue, - getNthOccurrence, - getRealSeparatorIndex, - roundFloatPartPercentValue, - splitByIndex, -}; diff --git a/packages/devextreme/js/__internal/ui/number_box/m_number_box.base.ts b/packages/devextreme/js/__internal/ui/number_box/number_box.base.ts similarity index 61% rename from packages/devextreme/js/__internal/ui/number_box/m_number_box.base.ts rename to packages/devextreme/js/__internal/ui/number_box/number_box.base.ts index 76577f3c5d31..c87bcd6f5ad1 100644 --- a/packages/devextreme/js/__internal/ui/number_box/m_number_box.base.ts +++ b/packages/devextreme/js/__internal/ui/number_box/number_box.base.ts @@ -13,25 +13,37 @@ import browser from '@js/core/utils/browser'; import { // @ts-expect-error ts-error applyServerDecimalSeparator, - ensureDefined, } from '@js/core/utils/common'; +import type { DeferredObj } from '@js/core/utils/deferred'; import { Deferred } from '@js/core/utils/deferred'; import { fitIntoRange, inRange } from '@js/core/utils/math'; import { isDefined } from '@js/core/utils/type'; +import type { DxEvent, InteractionEvent } from '@js/events'; +import type { Format } from '@js/localization'; import { getGlobalFormatByDataType } from '@ts/core/global_format_config'; +import type { OptionChanged } from '@ts/core/widget/types'; +import type { SupportedKeys } from '@ts/core/widget/widget'; +import type { DxMouseWheelEvent } from '@ts/ui/scroll_view/types'; import TextEditor from '@ts/ui/text_box/text_editor'; -import type { TextEditorBaseProperties } from '../text_box/text_editor.base'; +import type { TextEditorBaseProperties, TextEditorInputAttributes } from '../text_box/text_editor.base'; import type { TextEditorButtonInfo } from '../text_box/texteditor_button_collection/index'; -import SpinButtons from './m_number_box.spins'; - -const math = Math; +import type { SpinChangeEvent } from './number_box.spin'; +import SpinButtons from './number_box.spins'; export const WIDGET_CLASS = 'dx-numberbox'; const FIREFOX_CONTROL_KEYS = ['tab', 'del', 'backspace', 'leftArrow', 'rightArrow', 'home', 'end', 'enter']; const FORCE_VALUECHANGE_EVENT_NAMESPACE = 'NumberBoxForceValueChange'; +export type NumberBoxValue = number | null; + +export type SpinValueChangeEvent = DxEvent<InteractionEvent> | DxMouseWheelEvent; + +const getSpinEvent = ( + e: SpinChangeEvent | DxEvent<InteractionEvent>, +): SpinValueChangeEvent => ('event' in e ? e.event : e); + export interface NumberBoxBaseProperties extends TextEditorBaseProperties { min?: number; max?: number; @@ -39,6 +51,9 @@ export interface NumberBoxBaseProperties extends TextEditorBaseProperties { showSpinButtons?: boolean; useLargeSpinButtons?: boolean; invalidValueMessage?: string; + format?: Format | null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + displayValueFormatter?: ((value: any) => string); } class NumberBoxBase< @@ -48,24 +63,24 @@ class NumberBoxBase< _$submitElement!: dxElementWrapper; - _supportedKeys(): Record<string, (e: KeyboardEvent) => void> { + _supportedKeys(): SupportedKeys { return { ...super._supportedKeys(), - upArrow(e): void { + upArrow: (e): void => { if (!isCommandKeyPressed(e)) { e.preventDefault(); e.stopPropagation(); this._spinUpChangeHandler(e); } }, - downArrow(e): void { + downArrow: (e): void => { if (!isCommandKeyPressed(e)) { e.preventDefault(); e.stopPropagation(); this._spinDownChangeHandler(e); } }, - enter(): void {}, + enter: (): void => {}, }; } @@ -80,12 +95,10 @@ class NumberBoxBase< useLargeSpinButtons: true, mode: 'text', invalidValueMessage: messageLocalization.format('dxNumberBox-invalidValueMessage'), - // eslint-disable-next-line no-void - buttons: void 0, + buttons: undefined, }; } - // eslint-disable-next-line class-methods-use-this _useTemplates(): boolean { return false; } @@ -95,32 +108,27 @@ class NumberBoxBase< return super._getDefaultButtons().concat([{ name: 'spins', Ctor: SpinButtons }]); } - _isSupportInputMode() { - // @ts-expect-error ts-error - const version = parseFloat(browser.version); + _isSupportInputMode(): boolean { + const version = parseFloat(browser.version ?? ''); + + const isSupportedChrome = !!browser.chrome && version >= 66; + const isSupportedSafari = !!browser.safari && version >= 12; - return ( - browser.chrome && version >= 66 - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - || browser.safari && version >= 12 - ); + return isSupportedChrome || isSupportedSafari; } _defaultOptionsRules(): DefaultOptionsRule<TProperties>[] { // @ts-expect-error ts-error return super._defaultOptionsRules().concat([ { - device() { - return devices.real().generic && !devices.isSimulator(); - }, + device: (): boolean => !!devices.real().generic && !devices.isSimulator(), options: { useLargeSpinButtons: false, }, }, { - device: function () { - return devices.real().deviceType !== 'desktop' && !this._isSupportInputMode(); - }.bind(this), + device: (): boolean => devices.real().deviceType !== 'desktop' + && !this._isSupportInputMode(), options: { mode: 'number', }, @@ -136,7 +144,7 @@ class NumberBoxBase< this._toggleTabIndex(); } - _getDefaultAttributes() { + _getDefaultAttributes(): TextEditorInputAttributes { const attributes = super._getDefaultAttributes(); // eslint-disable-next-line spellcheck/spell-checker attributes.inputmode = 'decimal'; @@ -144,18 +152,25 @@ class NumberBoxBase< } _renderContentImpl(): void { - this.option('isValid') && this._validateValue(this.option('value')); + const { isValid, value } = this.option(); + + if (isValid) { + this._validateValue(value); + } + this.setAria('role', 'spinbutton'); } _renderSubmitElement(): void { + const { value } = this.option(); + this._$submitElement = $('<input>') .attr('type', 'hidden') .appendTo(this.$element()); - this._setSubmitValue(this.option('value')); + this._setSubmitValue(value); } - _setSubmitValue(value) { + _setSubmitValue(value: NumberBoxValue | undefined): void { this._getSubmitElement().val(applyServerDecimalSeparator(value)); } @@ -163,7 +178,7 @@ class NumberBoxBase< return this._$submitElement; } - _keyPressHandler(e) { + _keyPressHandler(e: DxEvent<KeyboardEvent>): void { super._keyPressHandler(); const char = getChar(e); @@ -173,7 +188,7 @@ class NumberBoxBase< if (!isInputCharValid) { const keyName = normalizeKeyName(e); // NOTE: Additional check for Firefox control keys - if (isCommandKeyPressed(e) || keyName && FIREFOX_CONTROL_KEYS.includes(keyName)) { + if (isCommandKeyPressed(e) || (keyName && FIREFOX_CONTROL_KEYS.includes(keyName))) { return; } @@ -188,13 +203,14 @@ class NumberBoxBase< return true; } - _onMouseWheel(dxEvent): void { - dxEvent.delta > 0 ? this._spinValueChange(1, dxEvent) : this._spinValueChange(-1, dxEvent); + _onMouseWheel(e: DxMouseWheelEvent): void { + this._spinValueChange(e.delta > 0 ? 1 : -1, e); } - _renderValue() { + _renderValue(): DeferredObj<unknown> { const inputValue = this._input().val(); - const value = this.option('value'); + const { value } = this.option(); + if (!inputValue.length || Number(inputValue) !== value) { this._forceValueRender(); this._toggleEmptinessEventHandler(); @@ -203,9 +219,8 @@ class NumberBoxBase< const valueText = isDefined(value) ? null : messageLocalization.format('dxNumberBox-noDataText'); this.setAria({ - // @ts-expect-error ts-error // eslint-disable-next-line spellcheck/spell-checker - valuenow: ensureDefined(value, ''), + valuenow: value ?? '', // eslint-disable-next-line spellcheck/spell-checker valuetext: valueText, }); @@ -217,17 +232,18 @@ class NumberBoxBase< } _forceValueRender(): void { - const value = this.option('value'); - const number = Number(value); - const formattedValue = isNaN(number) + const { value } = this.option(); + const formattedValue = isNaN(Number(value)) ? '' : this._applyDisplayValueFormatter(value); this._renderDisplayText(formattedValue); } - _applyDisplayValueFormatter(value): string | undefined { - if (!this.option('format')) { + _applyDisplayValueFormatter(value: NumberBoxValue | undefined): string | undefined { + const { format, displayValueFormatter } = this.option(); + + if (!format) { const globalNumberFormat = getGlobalFormatByDataType('number'); if (globalNumberFormat) { @@ -238,70 +254,66 @@ class NumberBoxBase< } } - const { displayValueFormatter } = this.option(); - return displayValueFormatter?.(value); } _renderProps(): void { + const { min, max, step } = this.option(); + // @ts-expect-error ts-error - this._input().prop({ - min: this.option('min'), - max: this.option('max'), - step: this.option('step'), - }); + this._input().prop({ min, max, step }); this.setAria({ - // @ts-expect-error ts-error // eslint-disable-next-line spellcheck/spell-checker - valuemin: ensureDefined(this.option('min'), ''), - // @ts-expect-error ts-error + valuemin: min ?? '', // eslint-disable-next-line spellcheck/spell-checker - valuemax: ensureDefined(this.option('max'), ''), + valuemax: max ?? '', }); } _spinButtonsPointerDownHandler(): void { + const { useLargeSpinButtons } = this.option(); const $input = this._input(); - if (!this.option('useLargeSpinButtons') && domAdapter.getActiveElement() !== $input[0]) { + + if (!useLargeSpinButtons && domAdapter.getActiveElement() !== $input[0]) { // @ts-expect-error ts-error eventsEngine.trigger($input, 'focus'); } } - _spinUpChangeHandler(e): void { - if (!this.option('readOnly')) { - this._spinValueChange(1, e.event || e); + _spinUpChangeHandler(e: SpinChangeEvent | DxEvent<InteractionEvent>): void { + const { readOnly } = this.option(); + + if (!readOnly) { + this._spinValueChange(1, getSpinEvent(e)); } } - _spinDownChangeHandler(e): void { - if (!this.option('readOnly')) { - this._spinValueChange(-1, e.event || e); + _spinDownChangeHandler(e: SpinChangeEvent | DxEvent<InteractionEvent>): void { + const { readOnly } = this.option(); + + if (!readOnly) { + this._spinValueChange(-1, getSpinEvent(e)); } } - _spinValueChange(sign, dxEvent): void { - // @ts-expect-error ts-error - const step = parseFloat(this.option('step')); + _spinValueChange(sign: number, dxEvent?: SpinValueChangeEvent): void { + const { step: stepOption, min, max } = this.option(); + const step = parseFloat(String(stepOption)); + if (step === 0) { return; } - // @ts-expect-error ts-error - let value = parseFloat(this._normalizeInputValue()) || 0; - value = this._correctRounding(value, step * sign); + let value = parseFloat(String(this._normalizeInputValue())) || 0; - const min = this.option('min'); - const max = this.option('max'); + value = this._correctRounding(value, step * sign); if (isDefined(min)) { - // @ts-expect-error ts-error value = Math.max(min, value); } if (isDefined(max)) { - // @ts-expect-error ts-error value = Math.min(max, value); } @@ -309,35 +321,28 @@ class NumberBoxBase< this.option('value', value); } - _correctRounding(value, step) { + _correctRounding(value: number, step: number): number { const regex = /[,.](.*)/; - const isFloatValue = regex.test(value); - const isFloatStep = regex.test(step); + const valueText = String(value); + const stepText = String(step); + const isFloatValue = regex.test(valueText); + const isFloatStep = regex.test(stepText); if (isFloatValue || isFloatStep) { - // @ts-expect-error - const valueAccuracy = isFloatValue ? regex.exec(value)[0].length : 0; - // @ts-expect-error - const stepAccuracy = isFloatStep ? regex.exec(step)[0].length : 0; - const accuracy = math.max(valueAccuracy, stepAccuracy); - - value = this._round(value + step, accuracy); + const valueAccuracy = isFloatValue ? regex.exec(valueText)?.[0].length ?? 0 : 0; + const stepAccuracy = isFloatStep ? regex.exec(stepText)?.[0].length ?? 0 : 0; + const accuracy = Math.max(valueAccuracy, stepAccuracy); - return value; + return this._round(value + step, accuracy); } return value + step; } - _round(value, precision) { - precision = precision || 0; - + _round(value: number, precision = 0): number { const multiplier = 10 ** precision; - value *= multiplier; - value = Math.round(value) / multiplier; - - return value; + return Math.round(value * multiplier) / multiplier; } _renderValueChangeEvent(): void { @@ -348,20 +353,21 @@ class NumberBoxBase< eventsEngine.on(this.element(), forceValueChangeEvent, this._forceRefreshInputValue.bind(this)); } - _forceRefreshInputValue() { - const { mode } = this.option(); + _forceRefreshInputValue(): void { + const { mode, value } = this.option(); + if (mode === 'number') { return; } const $input = this._input(); - const formattedValue = this._applyDisplayValueFormatter(this.option('value')); + const formattedValue = this._applyDisplayValueFormatter(value); // @ts-expect-error ts-error $input.val(null); $input.val(formattedValue); } - _valueChangeEventHandler(e) { + _valueChangeEventHandler(e: DxEvent): void { const $input = this._input(); const inputValue = this._normalizeText(); const value = this._parseValue(inputValue); @@ -373,8 +379,7 @@ class NumberBoxBase< } if (valueHasDigits) { - // @ts-expect-error ts-error - super._valueChangeEventHandler(e, isNaN(value) ? null : value); + super._valueChangeEventHandler(e, isNaN(Number(value)) ? null : value); } this._applyValueBoundaries(inputValue, value); @@ -385,9 +390,9 @@ class NumberBoxBase< }); } - _applyValueBoundaries(inputValue, parsedValue) { + _applyValueBoundaries(inputValue: string, parsedValue: NumberBoxValue | undefined): void { const isValueIncomplete = this._isValueIncomplete(inputValue); - const isValueCorrect = this._isValueInRange(inputValue); + const isValueCorrect = this._isValueInRange(Number(inputValue)); if (!isValueIncomplete && !isValueCorrect && parsedValue !== null) { if (Number(inputValue) !== parsedValue) { @@ -396,20 +401,19 @@ class NumberBoxBase< } } - _replaceCommaWithPoint(value): string { + _replaceCommaWithPoint(value: string): string { return value.replace(',', '.'); } _inputIsInvalid(): boolean { const { mode } = this.option(); const isNumberMode = mode === 'number'; - // @ts-expect-error ts-error - const validityState = this._input().get(0).validity; + const input = this._input().get(0) as HTMLInputElement | undefined; - return isNumberMode && validityState?.badInput; + return isNumberMode && !!input?.validity.badInput; } - _renderDisplayText(text) { + _renderDisplayText(text: string | undefined): void { if (this._inputIsInvalid()) { return; } @@ -417,20 +421,23 @@ class NumberBoxBase< super._renderDisplayText(text); } - _isValueIncomplete(value) { + _isValueIncomplete(value: string): boolean { const incompleteRegex = /(^-$)|(^-?\d*\.$)|(\d+e-?$)/i; return incompleteRegex.test(value); } - _isValueInRange(value) { - return inRange(value, this.option('min'), this.option('max')); + _isValueInRange(value: number): boolean { + const { min, max } = this.option(); + + return inRange(value, min, max); } - _isNumber(value) { + _isNumber(value: string): boolean { return this._parseValue(value) !== null; } - _validateValue(value?) { + _validateValue(value?: NumberBoxValue): boolean { + const { invalidValueMessage } = this.option(); const inputValue = this._normalizeText(); const isValueValid = this._isValueValid(); let isValid = true; @@ -450,31 +457,32 @@ class NumberBoxBase< isValid, validationError: isValid ? null : { editorSpecific: true, - message: this.option('invalidValueMessage'), + message: invalidValueMessage, }, }); return isValid; } - _normalizeInputValue(): number | null { + _normalizeInputValue(): NumberBoxValue | undefined { return this._parseValue(this._normalizeText()); } - _normalizeText() { + _normalizeText(): string { const value = this._input().val().trim(); return this._replaceCommaWithPoint(value); } - _parseValue(value): number | null { - const number = parseFloat(value); + _parseValue(value?: string | NumberBoxValue): NumberBoxValue | undefined { + const { min, max } = this.option(); + const parsedValue = parseFloat(String(value ?? '')); - if (isNaN(number)) { + if (isNaN(parsedValue)) { return null; } - return fitIntoRange(number, this.option('min'), this.option('max')); + return fitIntoRange(parsedValue, min, max); } _clearValue(): void { @@ -486,7 +494,9 @@ class NumberBoxBase< } clear(): void { - if (this.option('value') === null) { + const { value } = this.option(); + + if (value === null) { this.option('text', ''); if (this._input().length) { this._renderValue(); @@ -496,22 +506,28 @@ class NumberBoxBase< } } - _optionChanged(args) { + _optionChanged(args: OptionChanged<TProperties>): void { switch (args.name) { - case 'value': - this._validateValue(args.value); - this._setSubmitValue(args.value); + case 'value': { + const value = args.value as NumberBoxValue | undefined; + + this._validateValue(value); + this._setSubmitValue(value); super._optionChanged(args); this._resumeValueChangeAction(); break; + } case 'step': this._renderProps(); break; case 'min': - case 'max': + case 'max': { + const { value } = this.option(); + this._renderProps(); - this.option('value', this._parseValue(this.option('value'))); + this.option('value', this._parseValue(value)); break; + } case 'showSpinButtons': case 'useLargeSpinButtons': this._updateButtons(['spins']); diff --git a/packages/devextreme/js/__internal/ui/number_box/number_box.caret.ts b/packages/devextreme/js/__internal/ui/number_box/number_box.caret.ts new file mode 100644 index 000000000000..426ef848ce10 --- /dev/null +++ b/packages/devextreme/js/__internal/ui/number_box/number_box.caret.ts @@ -0,0 +1,161 @@ +import number from '@js/common/core/localization/number'; +import { escapeRegExp } from '@js/core/utils/common'; +import { fitIntoRange } from '@js/core/utils/math'; +import type { Format } from '@js/localization'; +import type { CaretRange } from '@ts/ui/text_box/utils.caret'; + +import { getNthOccurrence, getRealSeparatorIndex, splitByIndex } from './utils'; + +export interface CaretBoundaries { + start: number; + end: number; +} + +export type CaretPosition = number | CaretRange; + +const mockEscapedStubs = (format: string): string => format.replace( + /'([^']*)'/g, + (stub) => stub.split('').map(() => ' ').join('').substr(2), +); + +const getDigitCountBeforeIndex = (index: number, text: string): number => { + const decimalSeparator: string = number.getDecimalSeparator(); + const regExp = new RegExp(`[^0-9${escapeRegExp(decimalSeparator)}]`, 'g'); + const textBeforePosition = text.slice(0, index); + + return textBeforePosition.replace(regExp, '').length; +}; + +const reverseText = (text: string): string => text.split('').reverse().join(''); + +const getDigitPositionByIndex = (digitIndex: number, text: string): number => { + if (!digitIndex) { + return -1; + } + + const regExp = /[0-9]/g; + let counter = 1; + let index: number | null = null; + let result = regExp.exec(text); + + while (result) { + index = result.index; + if (counter >= digitIndex) { + return index; + } + counter += 1; + result = regExp.exec(text); + } + + return index ?? text.length; +}; + +const trimNonNumericCharsFromEnd = (text: string): string => text.replace(/[^0-9e]+$/, ''); + +const isSeparatorBasedString = (text: string): boolean => text.length === 1 + && !!text.match(/^[,.][0-9]*$/g); + +export const getCaretBoundaries = (text: string, format: Format): CaretBoundaries => { + if (typeof format === 'string') { + const signParts = format.split(';'); + const sign = number.getSign(text, format); + + signParts[1] = signParts[1] || `-${signParts[0]}`; + + const signFormat = mockEscapedStubs(signParts[sign < 0 ? 1 : 0]); + + const prefixStubLength = /^[^#0.,]*/.exec(signFormat)?.[0].length ?? 0; + const postfixStubLength = /[^#0.,]*$/.exec(signFormat)?.[0].length ?? 0; + + return { + start: prefixStubLength, + end: text.length - postfixStubLength, + }; + } + return { start: 0, end: text.length }; +}; + +export const getCaretWithOffset = ( + caret: CaretPosition, + offset: number, +): CaretBoundaries => { + const range = typeof caret === 'number' ? { start: caret, end: caret } : caret; + + return { + start: (range.start ?? 0) + offset, + end: (range.end ?? 0) + offset, + }; +}; + +export const getCaretInBoundaries = ( + caret: CaretPosition, + text: string, + format: Format, +): CaretBoundaries => { + const normalizedCaret = getCaretWithOffset(caret, 0); + const boundaries = getCaretBoundaries(text, format); + + return { + start: fitIntoRange(normalizedCaret.start, boundaries.start, boundaries.end), + end: fitIntoRange(normalizedCaret.end, boundaries.start, boundaries.end), + }; +}; + +export const getCaretAfterFormat = ( + text: string, + formatted: string, + caret: CaretPosition, + format: Format, +): CaretBoundaries => { + const normalizedCaret = getCaretWithOffset(caret, 0); + + const point: string = number.getDecimalSeparator(); + const realSeparatorOccurrenceIndex = getRealSeparatorIndex(format).occurrence; + const pointPosition = isSeparatorBasedString(text) + ? 0 + : getNthOccurrence(text, point, realSeparatorOccurrenceIndex); + const newPointPosition = getNthOccurrence(formatted, point, realSeparatorOccurrenceIndex); + const textParts = splitByIndex(text, pointPosition); + const formattedParts = splitByIndex(formatted, newPointPosition); + const isCaretOnFloat = pointPosition !== -1 && normalizedCaret.start > pointPosition; + + if (isCaretOnFloat) { + const relativeIndex = normalizedCaret.start - pointPosition - 1; + const digitsBefore = getDigitCountBeforeIndex(relativeIndex, textParts[1]); + const newPosition = formattedParts[1] + ? newPointPosition + 1 + getDigitPositionByIndex(digitsBefore, formattedParts[1]) + 1 + : formatted.length; + + return getCaretInBoundaries(newPosition, formatted, format); + } + + const formattedIntPart = trimNonNumericCharsFromEnd(formattedParts[0]); + const positionFromEnd = textParts[0].length - normalizedCaret.start; + const digitsFromEnd = getDigitCountBeforeIndex(positionFromEnd, reverseText(textParts[0])); + const newPositionFromEnd = getDigitPositionByIndex(digitsFromEnd, reverseText(formattedIntPart)); + const newPositionFromBegin = formattedIntPart.length - (newPositionFromEnd + 1); + + return getCaretInBoundaries(newPositionFromBegin, formatted, format); +}; + +export const isCaretInBoundaries = ( + caret: CaretPosition, + text: string, + format: Format, +): boolean => { + const normalizedCaret = getCaretWithOffset(caret, 0); + const boundaries = getCaretInBoundaries(normalizedCaret, text, format); + + return normalizedCaret.start >= boundaries.start && normalizedCaret.end <= boundaries.end; +}; + +export const getCaretOffset = ( + previousText: string, + newText: string, + format: Format, +): number => { + const previousBoundaries = getCaretBoundaries(previousText, format); + const newBoundaries = getCaretBoundaries(newText, format); + + return newBoundaries.start - previousBoundaries.start; +}; diff --git a/packages/devextreme/js/__internal/ui/number_box/m_number_box.mask.ts b/packages/devextreme/js/__internal/ui/number_box/number_box.mask.ts similarity index 61% rename from packages/devextreme/js/__internal/ui/number_box/m_number_box.mask.ts rename to packages/devextreme/js/__internal/ui/number_box/number_box.mask.ts index 11a9995839ad..72f9dabe79e3 100644 --- a/packages/devextreme/js/__internal/ui/number_box/m_number_box.mask.ts +++ b/packages/devextreme/js/__internal/ui/number_box/number_box.mask.ts @@ -3,30 +3,36 @@ import { name as dxDblClickEvent } from '@js/common/core/events/double_click'; import { addNamespace, getChar, isCommandKeyPressed, normalizeKeyName, } from '@js/common/core/events/utils/index'; -import { getFormat as getLDMLFormat } from '@js/common/core/localization/ldml/number'; import number from '@js/common/core/localization/number'; import devices from '@js/core/devices'; -import { ensureDefined, escapeRegExp } from '@js/core/utils/common'; +import { escapeRegExp } from '@js/core/utils/common'; +import type { DeferredObj } from '@js/core/utils/deferred'; import { fitIntoRange, inRange } from '@js/core/utils/math'; import { isDefined, isFunction, isNumeric, isPlainObject, isString, } from '@js/core/utils/type'; -import type { NativeEventInfo } from '@js/events'; +import type { DxEvent, NativeEventInfo } from '@js/events'; import type { Format, FormatObject } from '@js/localization'; import type { Properties } from '@js/ui/number_box'; import { getGlobalFormatByDataType } from '@ts/core/global_format_config'; +import { getFormat as getLDMLFormat } from '@ts/core/localization/ldml/number'; +import type { OptionChanged } from '@ts/core/widget/types'; +import type { SupportedKeys } from '@ts/core/widget/widget'; +import type { KeyboardKeyDownEvent } from '@ts/events/core/keyboard_processor'; import type { TextEditorInternalProperties } from '@ts/ui/text_box/text_editor.base'; +import type { CaretRange } from '@ts/ui/text_box/utils.caret'; -import NumberBoxBase from './m_number_box.base'; +import type { NumberBoxBaseProperties, NumberBoxValue } from './number_box.base'; +import NumberBoxBase from './number_box.base'; import { getCaretAfterFormat, getCaretBoundaries, getCaretInBoundaries, getCaretOffset, getCaretWithOffset, isCaretInBoundaries, -} from './m_number_box.caret'; +} from './number_box.caret'; import { - adjustPercentValue, getNthOccurrence, getRealSeparatorIndex, + adjustPercentValue, asPattern, getNthOccurrence, getRealSeparatorIndex, splitByIndex, -} from './m_utils'; +} from './utils'; const NUMBER_FORMATTER_NAMESPACE = 'dxNumberFormatter'; const MOVE_FORWARD = 1 as const; @@ -46,17 +52,24 @@ const asFormatObject = (format: Format | undefined): FormatObject | undefined => isPlainObject(format) ? format as FormatObject : undefined ); +const isNegativeValue = (value: NumberBoxValue | undefined): boolean => { + const parsedValue = Number(value); + + return parsedValue < 0 || 1 / parsedValue === -Infinity; +}; + export interface NumberBoxMaskProperties extends Omit<Properties, 'onChange' | 'onCopy' | 'onCut' | 'onEnterKey' | 'onFocusIn' | 'onFocusOut' | 'onInput' | 'onKeyDown' | 'onKeyUp' | 'onPaste' | 'onValueChanged' | 'onContentReady' | 'onDisposing' -| 'onOptionChanged' | 'onInitialized' >, Omit<TextEditorInternalProperties, 'displayValueFormatter'> { +| 'onOptionChanged' | 'onInitialized' | 'format' >, Omit<TextEditorInternalProperties, 'displayValueFormatter'> { + format?: Format | null; + useMaskBehavior?: boolean; - displayValueFormatter?: ((value: any) => string); + displayValueFormatter?: NumberBoxBaseProperties['displayValueFormatter']; onValueChanged?: ( - e: NativeEventInfo<unknown> & { value?: number | null; previousValue?: number | null }, + e: NativeEventInfo<unknown> & { value?: NumberBoxValue; previousValue?: NumberBoxValue }, ) => void; - } class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { @@ -64,7 +77,7 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { _focusOutOccurs?: boolean; - _parsedValue?: number | null; + _parsedValue?: NumberBoxValue; _lastKeyName?: string | null; @@ -72,40 +85,35 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { _formattedValue?: string; - _isValuePasted?: boolean; - - _currentFormat?: any; + _currentFormat?: Format; _getDefaultOptions(): NumberBoxMaskProperties { return { ...super._getDefaultOptions(), useMaskBehavior: true, - // @ts-expect-error ts-error format: null, }; } - _isDeleteKey(key): boolean { + _isDeleteKey(key: string | null | undefined): boolean { return key === 'del'; } - _supportedKeys() { + _supportedKeys(): SupportedKeys { if (!this._useMaskBehavior()) { return super._supportedKeys(); } - const that = this; - return { ...super._supportedKeys(), - minus: that._revertSign.bind(that), - del: that._removeHandler.bind(that), - backspace: that._removeHandler.bind(that), - leftArrow: that._arrowHandler.bind(that, MOVE_BACKWARD), - rightArrow: that._arrowHandler.bind(that, MOVE_FORWARD), - home: that._boundaryKeyHandler.bind(that, MOVE_FORWARD), - enter: that._updateFormattedValue.bind(that), - end: that._boundaryKeyHandler.bind(that, MOVE_BACKWARD), + minus: (e): void => this._revertSign(e), + del: (e): void => this._removeHandler(e), + backspace: (e): void => this._removeHandler(e), + leftArrow: (e): void => this._arrowHandler(MOVE_BACKWARD, e), + rightArrow: (e): void => this._arrowHandler(MOVE_FORWARD, e), + home: (e): void => this._boundaryKeyHandler(MOVE_FORWARD, e), + enter: (): void => this._updateFormattedValue(), + end: (e): void => this._boundaryKeyHandler(MOVE_BACKWARD, e), }; } @@ -117,15 +125,13 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { : getGlobalFormatByDataType('number'); } - _getTextSeparatorIndex(text) { - const decimalSeparator = number.getDecimalSeparator(); - const formatPattern = this._getFormatPattern(); - const patternString = isString(formatPattern) ? formatPattern : ''; - const realSeparatorOccurrenceIndex = getRealSeparatorIndex(patternString).occurrence; + _getTextSeparatorIndex(text: string): number { + const decimalSeparator: string = number.getDecimalSeparator(); + const realSeparatorOccurrenceIndex = getRealSeparatorIndex(this._getFormatPattern()).occurrence; return getNthOccurrence(text, decimalSeparator, realSeparatorOccurrenceIndex); } - _focusInHandler(e): void { + _focusInHandler(e: DxEvent<FocusEvent>): void { if (!this._preventNestedFocusEvent(e)) { this.clearCaretTimeout(); this._caretTimeout = setTimeout(() => { @@ -148,7 +154,7 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { super._focusInHandler(e); } - _focusOutHandler(e): void { + _focusOutHandler(e: DxEvent): void { const shouldHandleEvent = !this._preventNestedFocusEvent(e); if (shouldHandleEvent) { @@ -165,15 +171,15 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { } } - _hasValueBeenChanged(inputValue) { + _hasValueBeenChanged(inputValue: string): boolean { const format = this._getFormatPattern(); - const value = this.option('value'); + const { value } = this.option(); const formatted = this._format(value, format) || ''; return formatted !== inputValue; } - _updateFormattedValue() { + _updateFormattedValue(): void { const inputValue = this._getInputVal(); if (this._hasValueBeenChanged(inputValue)) { @@ -191,19 +197,26 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { } } - _arrowHandler(step, e) { + _arrowHandler(step: CaretMoveDirection, e: DxEvent<KeyboardEvent>): void { if (!this._useMaskBehavior()) { return; } + const caret = this._caret(); + + if (!caret) { + return; + } + const text = this._getInputVal(); const format = this._getFormatPattern(); - let nextCaret = getCaretWithOffset(this._caret(), step); + const nextCaret = getCaretWithOffset(caret, step); if (!isCaretInBoundaries(nextCaret, text, format)) { - nextCaret = step === MOVE_FORWARD ? nextCaret.end : nextCaret.start; + const nextCaretPosition = step === MOVE_FORWARD ? nextCaret.end : nextCaret.start; + e.preventDefault(); - this._caret(getCaretInBoundaries(nextCaret, text, format)); + this._caret(getCaretInBoundaries(nextCaretPosition, text, format)); this._scrollInputTo(step === MOVE_FORWARD ? 'end' : 'start'); } } @@ -232,7 +245,7 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { this._caret(newCaret); } - _boundaryKeyHandler(direction: CaretMoveDirection, e: KeyboardEvent): void { + _boundaryKeyHandler(direction: CaretMoveDirection, e: DxEvent<KeyboardEvent>): void { if (!this._useMaskBehavior() || e.shiftKey) { return; } @@ -242,19 +255,25 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { this._scrollInputTo(direction === MOVE_FORWARD ? 'start' : 'end'); } - _shouldMoveCaret(text, caret) { + _shouldMoveCaret(text: string, caret: CaretRange | undefined): boolean { + if (!caret) { + return false; + } + const decimalSeparator = number.getDecimalSeparator(); - const isDecimalSeparatorNext = text.charAt(caret.end) === decimalSeparator; - const moveToFloat = (this._lastKey === decimalSeparator || this._lastKey === '.' || this._lastKey === ',') && isDecimalSeparatorNext; + const isDecimalSeparatorNext = text.charAt(caret.end ?? 0) === decimalSeparator; + const isSeparatorKey = this._lastKey === decimalSeparator || this._lastKey === '.' || this._lastKey === ','; - return moveToFloat; + return isSeparatorKey && isDecimalSeparatorNext; } - _getInputVal() { - return number.convertDigits(this._input().val(), true); + _getInputVal(): string { + const inputValue: string = number.convertDigits(this._input().val(), true); + + return inputValue; } - _keyboardHandler(e): boolean { + _keyboardHandler(e: KeyboardKeyDownEvent): boolean { this.clearCaretTimeout(); this._lastKey = number.convertDigits(getChar(e), true); @@ -267,11 +286,11 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { const normalizedText = this._getInputVal(); const caret = this._caret(); - let enteredChar; + let enteredChar = this._lastKey; if (this._lastKeyName === MINUS_KEY) { enteredChar = ''; - } else { - enteredChar = e.which === NUMPAD_DOT_KEY_CODE ? number.getDecimalSeparator() : this._lastKey; + } else if (e.which === NUMPAD_DOT_KEY_CODE) { + enteredChar = number.getDecimalSeparator(); } const newValue = this._tryParse(normalizedText, caret, enteredChar); @@ -291,13 +310,13 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { return super._keyboardHandler(e); } - _keyPressHandler(e): void { + _keyPressHandler(e: DxEvent<KeyboardEvent>): void { if (!this._useMaskBehavior()) { super._keyPressHandler(e); } } - _removeHandler(e) { + _removeHandler(e: DxEvent<KeyboardEvent>): void { const caret = this._caret(); const text = this._getInputVal(); @@ -312,14 +331,15 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { if (start === end) { const caretPosition = start; - const canDelete = isBackspaceKey - && caretPosition > 0 - || isDeleteKey - && caretPosition < text.length; + const canDelete = (isBackspaceKey && caretPosition > 0) + || (isDeleteKey && caretPosition < text.length); if (canDelete) { - isDeleteKey && end++; - isBackspaceKey && start--; + if (isDeleteKey) { + end += 1; + } else { + start -= 1; + } } else { e.preventDefault(); return; @@ -330,12 +350,11 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { if (this._isStub(char)) { this._moveCaret(isDeleteKey ? 1 : -1); - // @ts-expect-error ts-error - if (this._parsedValue < 0 || 1 / this._parsedValue === -Infinity) { + if (isNegativeValue(this._parsedValue)) { this._revertSign(e); this._setTextByParsedValue(); - // @ts-expect-error ts-error - const shouldTriggerInputEvent = this.option('valueChangeEvent').split(' ').includes('input'); + const { valueChangeEvent } = this.option(); + const shouldTriggerInputEvent = valueChangeEvent?.split(' ').includes('input'); if (shouldTriggerInputEvent) { // @ts-expect-error ts-error eventsEngine.trigger(this._input(), 'input'); @@ -345,7 +364,7 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { return; } - const decimalSeparator = number.getDecimalSeparator(); + const decimalSeparator: string = number.getDecimalSeparator(); if (char === decimalSeparator) { const decimalSeparatorIndex = text.indexOf(decimalSeparator); if (this._isNonStubAfter(decimalSeparatorIndex + 1)) { @@ -360,8 +379,7 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { const noDigits = editedText.search(/[0-9]/) < 0; if (noDigits && this._isValueInRange(0)) { - // @ts-expect-error ts-error - this._parsedValue = this._parsedValue < 0 || 1 / this._parsedValue === -Infinity ? -0 : 0; + this._parsedValue = isNegativeValue(this._parsedValue) ? -0 : 0; return; } } @@ -374,48 +392,52 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { } } - _isPercentFormat() { - const format = this._getFormatPattern(); - const noEscapedFormat = format.replace(/'[^']+'/g, ''); + _isPercentFormat(): boolean { + const noEscapedFormat = asPattern(this._getFormatPattern()).replace(/'[^']+'/g, ''); - return noEscapedFormat.indexOf('%') !== -1; + return noEscapedFormat.includes('%'); } - _parse(text, format) { - const formatOption = this._getEffectiveFormatOption(); - const customParser = asFormatObject(formatOption)?.parser; - const isCustomParser = isFunction(customParser); - const parser = isCustomParser ? customParser : number.parse; + _parse(text: string, format: Format): NumberBoxValue | undefined { + const customParser = asFormatObject(this._getEffectiveFormatOption())?.parser; + + if (isFunction(customParser)) { + const customParsedValue = customParser(text); + + return customParsedValue instanceof Date ? undefined : customParsedValue; + } + let integerPartStartIndex = 0; - if (!isCustomParser) { + if (isString(format)) { const formatPointIndex = getRealSeparatorIndex(format).index; const textPointIndex = this._getTextSeparatorIndex(text); const formatIntegerPartLength = formatPointIndex !== -1 ? formatPointIndex : format.length; const textIntegerPartLength = textPointIndex !== -1 ? textPointIndex : text.length; - if (textIntegerPartLength > formatIntegerPartLength && format.indexOf('#') === -1) { + if (textIntegerPartLength > formatIntegerPartLength && !format.includes('#')) { integerPartStartIndex = textIntegerPartLength - formatIntegerPartLength; } } - text = text.substr(integerPartStartIndex); - - return parser(text, format); + return number.parse( + text.substr(integerPartStartIndex), + isString(format) ? format : asFormatObject(format), + ); } - _format(value, format) { + _format(value: NumberBoxValue | undefined, format: Format): string | undefined { const formatOption = this._getEffectiveFormatOption(); const customFormatter = asFormatObject(formatOption)?.formatter ?? formatOption; - const formatter = isFunction(customFormatter) ? customFormatter : number.format; - - const formattedValue = value === null ? '' : formatter(value, format); + const formatter = (isFunction(customFormatter) ? customFormatter : number.format) as ( + value: number, valueFormat: Format, + ) => string | undefined; - return formattedValue; + return isDefined(value) ? formatter(value, format) : ''; } - _getFormatPattern() { + _getFormatPattern(): Format { if (!this._currentFormat) { this._updateFormat(); } @@ -433,28 +455,30 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { this._currentFormat = shouldUseFormatAsIs ? format - : getLDMLFormat((value) => { - const text = this._format(value, format); - return number.convertDigits(text, true); + : getLDMLFormat((value: number): string => { + const text = this._format(value, format) ?? ''; + const convertedText: string = number.convertDigits(text, true); + + return convertedText; }); } - _getFormatForSign(text) { + _getFormatForSign(text: string): string { const format = this._getFormatPattern(); if (isString(format)) { const signParts = format.split(';'); - const sign = number.getSign(text, format); + const sign: number = number.getSign(text, format); signParts[1] = signParts[1] || `-${signParts[0]}`; return sign < 0 ? signParts[1] : signParts[0]; } - const sign = number.getSign(text); + const sign: number = number.getSign(text); return sign < 0 ? '-' : ''; } - _removeStubs(text, excludeComma?): string { + _removeStubs(text: string, excludeComma?: boolean): string { const format = this._getFormatForSign(text); - const thousandsSeparator = number.getThousandsSeparator(); + const thousandsSeparator: string = number.getThousandsSeparator(); const stubs = this._getStubs(format); let result = text; @@ -472,42 +496,54 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { return result; } - _getStubs(format) { - const regExpResult = /[^']([#0.,]+)/g.exec(format); - const pattern = regExpResult && regExpResult[0].trim(); + _getStubs(format: string): string[] { + const patternMatch = /[^']([#0.,]+)/g.exec(format); + const pattern = patternMatch?.[0].trim(); + const stubs = isDefined(pattern) ? format.split(pattern) : [format]; - return format - .split(pattern) - .map((stub) => stub.replace(/'/g, '')); + return stubs.map((stub) => stub.replace(/'/g, '')); } - _truncateToPrecision(value, maxPrecision) { + _truncateToPrecision( + value: NumberBoxValue | undefined, + maxPrecision: number, + ): NumberBoxValue | undefined { if (isDefined(value)) { const strValue = value.toString(); const decimalSeparatorIndex = strValue.indexOf('.'); if (strValue && decimalSeparatorIndex > -1) { - const parsedValue = parseFloat(strValue.substr(0, decimalSeparatorIndex + maxPrecision + 1)); + const truncatedValue = strValue.substr(0, decimalSeparatorIndex + maxPrecision + 1); + const parsedValue = parseFloat(truncatedValue); + return isNaN(parsedValue) ? value : parsedValue; } } return value; } - _tryParse(text, selection, char?) { - const isTextSelected = selection.start !== selection.end; - const isWholeTextSelected = isTextSelected && selection.start === 0 && selection.end === text.length; - const decimalSeparator = number.getDecimalSeparator(); + _tryParse( + text: string, + selection: CaretRange | undefined, + char?: string | null, + ): NumberBoxValue | undefined { + const { start = 0, end = 0 } = selection ?? {}; + const isTextSelected = start !== end; + const isWholeTextSelected = isTextSelected && start === 0 && end === text.length; + const decimalSeparator: string = number.getDecimalSeparator(); if (isWholeTextSelected && char === decimalSeparator) { return 0; } - const editedText = this._replaceSelectedText(text, selection, char); + const editedText = this._replaceSelectedText(text, { start, end }, char ?? undefined); const format = this._getFormatPattern(); + const hasCustomParser = isFunction(asFormatObject(format)?.parser); let parsedValue = this._getParsedValue(editedText, format); - const maxPrecision = !format.parser && this._getPrecisionLimits(editedText).max; + const maxPrecision = hasCustomParser + ? undefined + : this._getPrecisionLimits(this._getFormatForSign(editedText)).max; const isValueChanged = parsedValue !== this._parsedValue; const isDecimalPointRestricted = char === decimalSeparator && maxPrecision === 0; @@ -521,8 +557,7 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { } if (this._removeStubs(editedText) === '') { - // @ts-expect-error ts-error - parsedValue = Math.abs(this._parsedValue * 0); + parsedValue = Math.abs(Number(this._parsedValue) * 0); } if (isNaN(Number(parsedValue))) { @@ -532,8 +567,8 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { const value = parsedValue === null ? this._parsedValue : parsedValue; parsedValue = maxPrecision ? this._truncateToPrecision(value, maxPrecision) : parsedValue; - if (!format.parser && this._isPercentFormat()) { - const interval = this._getIntervalFromPrecision(maxPrecision); + if (!hasCustomParser && this._isPercentFormat()) { + const interval = this._getIntervalFromPrecision(maxPrecision ?? 0); return adjustPercentValue(parsedValue, interval); } @@ -541,7 +576,7 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { return parsedValue; } - _getIntervalFromPrecision(precision) { + _getIntervalFromPrecision(precision: number): number { if (precision < 1) { return 1; } @@ -549,23 +584,24 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { return 10 ** -precision; } - _getParsedValue(text, format) { - const sign = number.getSign(text, format?.formatter || format); + _getParsedValue(text: string, format: Format): NumberBoxValue | undefined { + const signFormat = asFormatObject(format)?.formatter ?? format; + const sign: number = number.getSign(text, signFormat); const textWithoutStubs = this._removeStubs(text, true); const parsedValue = this._parse(textWithoutStubs, format); - const parsedValueSign = parsedValue != null && parsedValue < 0 ? -1 : 1; - const parsedValueWithSign = isNumeric(parsedValue) && sign !== parsedValueSign ? sign * parsedValue : parsedValue; + const parsedValueSign = Number(parsedValue) < 0 ? -1 : 1; + const shouldRevertSign = isNumeric(parsedValue) && sign !== parsedValueSign; - return parsedValueWithSign; + return shouldRevertSign ? sign * parsedValue : parsedValue; } - _isValueIncomplete(text) { + _isValueIncomplete(text: string): boolean { if (!this._useMaskBehavior()) { return super._isValueIncomplete(text); } const caret = this._caret(); - const point = number.getDecimalSeparator(); + const point: string = number.getDecimalSeparator(); const pointIndex = this._getTextSeparatorIndex(text); const isCaretOnFloat = pointIndex >= 0 && pointIndex < (caret?.start ?? 0); const textParts = this._removeStubs(text, true).split(point); @@ -575,34 +611,41 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { } const floatLength = textParts[1].length; - const format = this._getFormatPattern(); - const isCustomParser = !!format.parser; - const precision = !isCustomParser && this._getPrecisionLimits(this._getFormatPattern()); - const isPrecisionInRange = isCustomParser - ? true - // @ts-expect-error ts-error - : inRange(floatLength, precision.min, precision.max); const endsWithZero = textParts[1].charAt(floatLength - 1) === '0'; + const isFloatPartComplete = endsWithZero || !floatLength; + + const format = this._getFormatPattern(); - return isPrecisionInRange && (endsWithZero || !floatLength); + if (isFunction(asFormatObject(format)?.parser)) { + return isFloatPartComplete; + } + + const precision = this._getPrecisionLimits(this._getFormatForSign(asPattern(format))); + + return inRange(floatLength, precision.min, precision.max) && isFloatPartComplete; } - _isValueInRange(value) { - const min = ensureDefined(this.option('min'), -Infinity); - const max = ensureDefined(this.option('max'), Infinity); + _isValueInRange(value: number): boolean { + const { min, max } = this.option(); - return inRange(value, min, max); + return inRange(value, min ?? -Infinity, max ?? Infinity); } - _setInputText(text): void { - const normalizedText = number.convertDigits(text, true); - const newCaret = getCaretAfterFormat(this._getInputVal(), normalizedText, this._caret(), this._getFormatPattern()); + _setInputText(text: string): void { + const normalizedText: string = number.convertDigits(text, true); + const caret = this._caret(); + const newCaret = caret && getCaretAfterFormat( + this._getInputVal(), + normalizedText, + caret, + this._getFormatPattern(), + ); this._input().val(text); this._toggleEmptinessEventHandler(); this._formattedValue = text; - if (!this._focusOutOccurs) { + if (newCaret && !this._focusOutOccurs) { this._caret(newCaret); } } @@ -625,24 +668,31 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { } } - _isChar(str) { + _isChar(str: string | null | undefined): boolean { return isString(str) && str.length === 1; } - _moveCaret(offset?): void { - if (!offset) { + _moveCaret(offset?: number): void { + const caret = this._caret(); + + if (!offset || !caret) { return; } - const newCaret = getCaretWithOffset(this._caret(), offset); - const adjustedCaret = getCaretInBoundaries(newCaret, this._getInputVal(), this._getFormatPattern()); + const newCaret = getCaretWithOffset(caret, offset); + const adjustedCaret = getCaretInBoundaries( + newCaret, + this._getInputVal(), + this._getFormatPattern(), + ); this._caret(adjustedCaret); } - _shouldHandleKey(e): boolean { + _shouldHandleKey(e: KeyboardEvent): boolean { const keyName = normalizeKeyName(e); - const isSpecialChar = isCommandKeyPressed(e) || e.altKey || e.shiftKey || !this._isChar(keyName); + const isSpecialChar = isCommandKeyPressed(e) || e.altKey || e.shiftKey + || !this._isChar(keyName); const isMinusKey = keyName === MINUS_KEY; const useMaskBehavior = this._useMaskBehavior(); @@ -654,7 +704,7 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { this._renderFormatter(); } - _renderFormatter() { + _renderFormatter(): void { this._clearCache(); this._detachFormatterEvents(); @@ -663,32 +713,37 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { } } - _detachFormatterEvents() { + _detachFormatterEvents(): void { eventsEngine.off(this._input(), `.${NUMBER_FORMATTER_NAMESPACE}`); } - _isInputFromPaste(e) { - const inputType = e.originalEvent?.inputType; - - if (isDefined(inputType)) { - return inputType === 'insertFromPaste'; - } - return this._isValuePasted; + _isInputFromPaste(e: DxEvent<InputEvent>): boolean { + return e.originalEvent?.inputType === 'insertFromPaste'; } _attachFormatterEvents(): void { const $input = this._input(); - eventsEngine.on($input, addNamespace(INPUT_EVENT, NUMBER_FORMATTER_NAMESPACE), (e) => { - this._formatValue(e); - this._isValuePasted = false; - }); + eventsEngine.on( + $input, + addNamespace(INPUT_EVENT, NUMBER_FORMATTER_NAMESPACE), + (e: DxEvent<InputEvent>) => { this._formatValue(e); }, + ); eventsEngine.on($input, addNamespace('dxclick', NUMBER_FORMATTER_NAMESPACE), () => { if (!this._caretTimeout) { this._caretTimeout = setTimeout(() => { this._caretTimeout = undefined; - this._caret(getCaretInBoundaries(this._caret(), this._getInputVal(), this._getFormatPattern())); + + const caret = this._caret(); + + if (caret) { + this._caret(getCaretInBoundaries( + caret, + this._getInputVal(), + this._getFormatPattern(), + )); + } }, CARET_TIMEOUT_DURATION); } }); @@ -703,44 +758,44 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { this._caretTimeout = undefined; } - _forceRefreshInputValue() { + _forceRefreshInputValue(): void { if (!this._useMaskBehavior()) { - return super._forceRefreshInputValue(); + super._forceRefreshInputValue(); } } - _isNonStubAfter(index) { + _isNonStubAfter(index: number): boolean { const text = this._getInputVal().slice(index); - return text && !this._isStub(text, true); + + return !!text && !this._isStub(text, true); } - _isStub(str, isString?) { + _isStub(str: string | null | undefined, allowMultipleChars?: boolean): boolean { const escapedDecimalSeparator = escapeRegExp(number.getDecimalSeparator()); const regExpString = `^[^0-9${escapedDecimalSeparator}]+$`; const stubRegExp = new RegExp(regExpString, 'g'); - return stubRegExp.test(str) && (isString || this._isChar(str)); + return stubRegExp.test(str ?? '') && (!!allowMultipleChars || this._isChar(str)); } - _parseValue(text?): number | null { + _parseValue(text?: string | NumberBoxValue): NumberBoxValue | undefined { if (!this._useMaskBehavior()) { return super._parseValue(text); } - // @ts-expect-error ts-error + return this._parsedValue; } - _getPrecisionLimits(text): { min: number; max: number } { - const currentFormat = this._getFormatForSign(text); - const realSeparatorIndex = getRealSeparatorIndex(currentFormat).index; - const floatPart = (splitByIndex(currentFormat, realSeparatorIndex)[1] || '').replace(/[^#0]/g, ''); + _getPrecisionLimits(pattern: string): { min: number; max: number } { + const realSeparatorIndex = getRealSeparatorIndex(pattern).index; + const floatPart = (splitByIndex(pattern, realSeparatorIndex)[1] || '').replace(/[^#0]/g, ''); const minPrecision = floatPart.replace(/^(0*)#*/, '$1').length; const maxPrecision = floatPart.length; return { min: minPrecision, max: maxPrecision }; } - _revertSign(e) { + _revertSign(e: DxEvent<KeyboardEvent>): void { if (!this._useMaskBehavior()) { return; } @@ -758,8 +813,12 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { this._applyRevertedSign(e, caret); } - _applyRevertedSign(e, caret, preserveSelectedText?): void { - const newValue = -1 * ensureDefined(this._parsedValue, null); + _applyRevertedSign( + e: DxEvent<KeyboardEvent>, + caret: CaretRange | undefined, + preserveSelectedText?: boolean, + ): void { + const newValue = -1 * (this._parsedValue ?? 0); if (this._isValueInRange(newValue) || newValue === 0) { this._parsedValue = newValue; @@ -771,25 +830,26 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { this._setTextByParsedValue(); e.preventDefault(); - const currentText = this._getInputVal(); - const offset = getCaretOffset(previousText, currentText, format); - - caret = getCaretWithOffset(caret, offset); + if (caret) { + const currentText = this._getInputVal(); + const offset = getCaretOffset(previousText, currentText, format); + const caretWithOffset = getCaretWithOffset(caret, offset); + const caretInBoundaries = getCaretInBoundaries(caretWithOffset, currentText, format); - const caretInBoundaries = getCaretInBoundaries(caret, currentText, format); - - this._caret(caretInBoundaries); + this._caret(caretInBoundaries); + } } } } - _removeMinusFromText(text, caret) { - const isMinusPressed = this._lastKeyName === MINUS_KEY && text.charAt(caret.start - 1) === MINUS; + _removeMinusFromText(text: string, caret: CaretRange | undefined): string { + const caretStart = caret?.start ?? 0; + const isMinusPressed = this._lastKeyName === MINUS_KEY + && text.charAt(caretStart - 1) === MINUS; - return isMinusPressed ? this._replaceSelectedText(text, { - start: caret.start - 1, - end: caret.start, - }, '') : text; + return isMinusPressed + ? this._replaceSelectedText(text, { start: caretStart - 1, end: caretStart }, '') + : text; } _setTextByParsedValue(): void { @@ -800,7 +860,7 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { this._setInputText(formatted); } - _formatValue(e): void { + _formatValue(e: DxEvent<InputEvent>): void { let normalizedText = this._getInputVal(); const caret = this._caret(); const textWithoutMinus = this._removeMinusFromText(normalizedText, caret); @@ -817,7 +877,8 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { return; } - const textWasChanged = number.convertDigits(this._formattedValue, true) !== normalizedText; + const formattedValue: string | undefined = number.convertDigits(this._formattedValue, true); + const textWasChanged = formattedValue !== normalizedText; if (textWasChanged) { const value = this._tryParse(normalizedText, caret, ''); @@ -830,18 +891,18 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { this._setTextByParsedValue(); } - _renderDisplayText(): void { + _renderDisplayText(text?: string): void { if (this._useMaskBehavior()) { this._toggleEmptinessEventHandler(); } else { - // @ts-expect-error ts-error - super._renderDisplayText.apply(this, arguments); + super._renderDisplayText(text); } } - _renderValue() { + _renderValue(): DeferredObj<unknown> { if (this._useMaskBehavior()) { const { value } = this.option(); + this._parsedValue = value; this._setTextByParsedValue(); } @@ -867,12 +928,15 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { return; } - this._parsedValue = fitIntoRange(parsedValue, this.option('min'), this.option('max')); + const { min, max } = this.option(); + + this._parsedValue = fitIntoRange(parsedValue, min, max); } - _valueChangeEventHandler(e): void { + _valueChangeEventHandler(e: DxEvent): void { if (!this._useMaskBehavior()) { - return super._valueChangeEventHandler(e); + super._valueChangeEventHandler(e); + return; } const caret = this._caret(); @@ -890,7 +954,7 @@ class NumberBoxMask extends NumberBoxBase<NumberBoxMaskProperties> { } } - _optionChanged(args): void { + _optionChanged(args: OptionChanged<NumberBoxMaskProperties>): void { switch (args.name) { case 'format': case 'useMaskBehavior': diff --git a/packages/devextreme/js/__internal/ui/number_box/m_number_box.spin.ts b/packages/devextreme/js/__internal/ui/number_box/number_box.spin.ts similarity index 92% rename from packages/devextreme/js/__internal/ui/number_box/m_number_box.spin.ts rename to packages/devextreme/js/__internal/ui/number_box/number_box.spin.ts index d739bae81ba5..2ec292543b26 100644 --- a/packages/devextreme/js/__internal/ui/number_box/m_number_box.spin.ts +++ b/packages/devextreme/js/__internal/ui/number_box/number_box.spin.ts @@ -8,6 +8,7 @@ import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import type { DeferredObj } from '@js/core/utils/deferred'; import { Deferred } from '@js/core/utils/deferred'; +import type { DxEvent, PointerInteractionEvent } from '@js/events/events.types'; import type { OptionChanged } from '@ts/core/widget/types'; import type { WidgetProperties } from '@ts/core/widget/widget'; import Widget from '@ts/core/widget/widget'; @@ -21,10 +22,14 @@ const NUMBER_BOX = 'dxNumberBox'; const POINTERUP_EVENT_NAME = addNamespace(pointerEvents.up, NUMBER_BOX); const POINTERCANCEL_EVENT_NAME = addNamespace(pointerEvents.cancel, NUMBER_BOX); +export interface SpinChangeEvent { + event: DxEvent<PointerInteractionEvent>; +} + export interface SpinButtonProperties extends WidgetProperties { direction?: string; - onChange?: () => void; + onChange?: ((e: SpinChangeEvent) => void) | null; } class SpinButton extends Widget<SpinButtonProperties> { @@ -40,7 +45,6 @@ class SpinButton extends Widget<SpinButtonProperties> { return { ...super._getDefaultOptions(), direction: 'up', - // @ts-expect-error ts-error onChange: null, activeStateEnabled: true, hoverStateEnabled: true, @@ -73,7 +77,7 @@ class SpinButton extends Widget<SpinButtonProperties> { this._spinChangeHandler = this._createActionByOption('onChange'); } - _spinDownHandler(e): void { + _spinDownHandler(e: DxEvent<PointerInteractionEvent>): void { e.preventDefault(); this._clearTimer(); diff --git a/packages/devextreme/js/__internal/ui/number_box/number_box.spins.ts b/packages/devextreme/js/__internal/ui/number_box/number_box.spins.ts new file mode 100644 index 000000000000..1c289dd3c24a --- /dev/null +++ b/packages/devextreme/js/__internal/ui/number_box/number_box.spins.ts @@ -0,0 +1,140 @@ +import eventsEngine from '@js/common/core/events/core/events_engine'; +import pointer from '@js/common/core/events/pointer'; +import { addNamespace } from '@js/common/core/events/utils/index'; +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import type { DxEvent, PointerInteractionEvent } from '@js/events/events.types'; +import TextEditorButton from '@ts/ui/text_box/texteditor_button_collection/button'; + +import type NumberBoxBase from './number_box.base'; +import type { SpinChangeEvent } from './number_box.spin'; +import SpinButton from './number_box.spin'; + +const SPIN_CLASS = 'dx-numberbox-spin'; +const SPIN_CONTAINER_CLASS = 'dx-numberbox-spin-container'; +const SPIN_TOUCH_FRIENDLY_CLASS = 'dx-numberbox-spin-touch-friendly'; + +interface SpinButtonsOptions { + visible: boolean; + disabled: boolean; +} + +export default class SpinButtons extends TextEditorButton<NumberBoxBase, dxElementWrapper> { + _attachEvents( + instance: dxElementWrapper, + $spinContainer: dxElementWrapper, + ): void { + const { editor } = this; + + if (!editor) { + return; + } + + const eventName = addNamespace(pointer.down, editor.NAME ?? ''); + const $spinContainerChildren = $spinContainer.children(); + + const pointerDownAction = editor._createAction( + () => { this.editor?._spinButtonsPointerDownHandler(); }, + ); + + eventsEngine.off($spinContainer, eventName); + + eventsEngine.on( + $spinContainer, + eventName, + (e: DxEvent<PointerInteractionEvent>) => pointerDownAction({ event: e }), + ); + + SpinButton.getInstance<SpinButton>($spinContainerChildren.eq(0)).option( + 'onChange', + (e: SpinChangeEvent) => { this.editor?._spinUpChangeHandler(e); }, + ); + + SpinButton.getInstance<SpinButton>($spinContainerChildren.eq(1)).option( + 'onChange', + (e: SpinChangeEvent) => { this.editor?._spinDownChangeHandler(e); }, + ); + } + + _create(): { + $element: dxElementWrapper; + instance: dxElementWrapper; + } | undefined { + const { editor } = this; + + if (!editor) { + return undefined; + } + + const $spinContainer = $('<div>').addClass(SPIN_CONTAINER_CLASS); + const $spinUp = $('<div>').appendTo($spinContainer); + const $spinDown = $('<div>').appendTo($spinContainer); + const options = this._getOptions(); + + this._addToContainer($spinContainer); + + editor._createComponent($spinUp, SpinButton, { direction: 'up', ...options }); + editor._createComponent($spinDown, SpinButton, { direction: 'down', ...options }); + + this._legacyRender(editor.$element(), this._isTouchFriendly(), options.visible); + + return { + instance: $spinContainer, + $element: $spinContainer, + }; + } + + _getOptions(): SpinButtonsOptions { + const { editor } = this; + + const { disabled } = editor?.option() ?? {}; + + return { + visible: this._isVisible(), + disabled: !!disabled, + }; + } + + _isVisible(): boolean { + const { editor } = this; + const { showSpinButtons } = editor?.option() ?? {}; + + return super._isVisible() && !!showSpinButtons; + } + + _isTouchFriendly(): boolean { + const { editor } = this; + const { showSpinButtons, useLargeSpinButtons } = editor?.option() ?? {}; + + return !!showSpinButtons && !!useLargeSpinButtons; + } + + // TODO: get rid of it + _legacyRender( + $editor: dxElementWrapper, + isTouchFriendly: boolean, + isVisible: boolean, + ): void { + $editor.toggleClass(SPIN_TOUCH_FRIENDLY_CLASS, isTouchFriendly); + $editor.toggleClass(SPIN_CLASS, isVisible); + } + + update(): boolean { + const shouldUpdate = super.update(); + const { editor, instance } = this; + + if (shouldUpdate && instance && editor) { + const $spinButtons = instance.children(); + const spinUp = SpinButton.getInstance<SpinButton>($spinButtons.eq(0)); + const spinDown = SpinButton.getInstance<SpinButton>($spinButtons.eq(1)); + const options = this._getOptions(); + + spinUp.option(options); + spinDown.option(options); + + this._legacyRender(editor.$element(), this._isTouchFriendly(), options.visible); + } + + return shouldUpdate; + } +} diff --git a/packages/devextreme/js/__internal/ui/number_box/m_number_box.ts b/packages/devextreme/js/__internal/ui/number_box/number_box.ts similarity index 77% rename from packages/devextreme/js/__internal/ui/number_box/m_number_box.ts rename to packages/devextreme/js/__internal/ui/number_box/number_box.ts index 9b862fa02055..1b484d08e202 100644 --- a/packages/devextreme/js/__internal/ui/number_box/m_number_box.ts +++ b/packages/devextreme/js/__internal/ui/number_box/number_box.ts @@ -1,6 +1,6 @@ import registerComponent from '@js/core/component_registrator'; -import NumberBoxMask from './m_number_box.mask'; +import NumberBoxMask from './number_box.mask'; // STYLE numberBox diff --git a/packages/devextreme/js/__internal/ui/number_box/utils.ts b/packages/devextreme/js/__internal/ui/number_box/utils.ts new file mode 100644 index 000000000000..ca4d4ec140f2 --- /dev/null +++ b/packages/devextreme/js/__internal/ui/number_box/utils.ts @@ -0,0 +1,74 @@ +import { adjust } from '@js/core/utils/math'; +import { isString } from '@js/core/utils/type'; +import type { Format } from '@js/localization'; + +export interface SeparatorPosition { + occurrence: number; + index: number; +} + +const asPattern = (format: Format): string => (isString(format) ? format : ''); + +const getRealSeparatorIndex = (format: Format): SeparatorPosition => { + const pattern = asPattern(format); + let quoteBalance = 0; + let separatorCount = 0; + + for (let i = 0; i < pattern.length; i += 1) { + if (pattern[i] === '\'') { + quoteBalance += 1; + } + if (pattern[i] === '.') { + separatorCount += 1; + if (quoteBalance % 2 === 0) { + return { + occurrence: separatorCount, + index: i, + }; + } + } + } + + return { occurrence: 1, index: -1 }; +}; + +const getNthOccurrence = (str: string, char: string, occurrence: number): number => { + let index = -1; + + for (let remaining = occurrence; remaining > 0; remaining -= 1) { + index = str.indexOf(char, index + 1); + + if (index === -1) { + return -1; + } + } + + return index; +}; + +const splitByIndex = (str: string, index: number): string[] => { + if (index === -1) { + return [str]; + } + + return [str.slice(0, index), str.slice(index + 1)]; +}; + +const adjustPercentValue = ( + rawValue: number | null | undefined, + interval: number, +): number | null | undefined => { + if (!rawValue) { + return rawValue; + } + + return adjust(rawValue / 100, interval / 100); +}; + +export { + adjustPercentValue, + asPattern, + getNthOccurrence, + getRealSeparatorIndex, + splitByIndex, +}; diff --git a/packages/devextreme/js/__internal/ui/overlay/overlay.ts b/packages/devextreme/js/__internal/ui/overlay/overlay.ts index d6b3068d30ee..e1a18de96da5 100644 --- a/packages/devextreme/js/__internal/ui/overlay/overlay.ts +++ b/packages/devextreme/js/__internal/ui/overlay/overlay.ts @@ -39,6 +39,7 @@ import type { } from '@js/events'; import type { dxOverlayAnimation, Properties } from '@js/ui/overlay'; import uiErrors from '@js/ui/widget/ui.errors'; +import documentSizeCallbacks from '@ts/core/utils/document_size_callbacks'; import domUtils from '@ts/core/utils/m_dom'; import selectors from '@ts/core/utils/m_selectors'; import windowUtils from '@ts/core/utils/m_window'; @@ -262,6 +263,8 @@ class Overlay< _hideTopOverlayHandler!: () => void; + _documentSizeChangedHandler?: () => void; + _hideAnimationProcessing?: boolean; _showAnimationProcessing?: boolean; @@ -565,14 +568,13 @@ class Overlay< super._initTemplates(); } - _isTopOverlay(): boolean { + _isTopFocusableOverlay(): boolean { const overlayStack = this._overlayStack(); for (let i = overlayStack.length - 1; i >= 0; i -= 1) { const tabbableElements = overlayStack[i]._findTabbableBounds(); if (tabbableElements.$first || tabbableElements.$last) { - // @ts-ignore expected: types Overlay<OverlayProperties> and this have no overlap return overlayStack[i] === this; } } @@ -580,6 +582,25 @@ class Overlay< return false; } + _isLastInOverlayStack(): boolean { + const overlayStack = this._overlayStack(); + + return overlayStack[overlayStack.length - 1] === this; + } + + _handlesDocumentEscapeKey(): boolean { + return false; + } + + _isEscapeHandledByOverlayAbove(): boolean { + const overlayStack = this._overlayStack(); + const index = overlayStack.indexOf(this); + + return overlayStack + .slice(index + 1) + .some((overlay) => overlay._handlesDocumentEscapeKey()); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any _overlayStack(): Overlay<any>[] { return OVERLAY_STACK; @@ -1052,7 +1073,7 @@ class Overlay< } _tabKeyHandler(e: KeyboardEvent): void { - if (normalizeKeyName(e) !== TAB_KEY || !this._isTopOverlay()) { + if (normalizeKeyName(e) !== TAB_KEY || !this._isTopFocusableOverlay()) { return; } @@ -1086,6 +1107,19 @@ class Overlay< if (windowUtils.hasWindow()) { this._toggleHideTopOverlayCallback(enabled); this._toggleHideOnParentsScrollSubscription(enabled); + this._toggleDocumentSizeSubscription(enabled); + } + } + + _toggleDocumentSizeSubscription(subscribe: boolean): void { + this._documentSizeChangedHandler ??= (): void => { + this._dimensionChanged(); + }; + + if (subscribe) { + documentSizeCallbacks.add(this._documentSizeChangedHandler); + } else { + documentSizeCallbacks.remove(this._documentSizeChangedHandler); } } diff --git a/packages/devextreme/js/__internal/ui/popover/popover.ts b/packages/devextreme/js/__internal/ui/popover/popover.ts index fa5d0fbf46e5..d071cd9f425e 100644 --- a/packages/devextreme/js/__internal/ui/popover/popover.ts +++ b/packages/devextreme/js/__internal/ui/popover/popover.ts @@ -225,19 +225,19 @@ class Popover< _initEscapeKeyHandler(): void { this._documentEscapeKeyHandler = (e: KeyboardEvent): void => { - const { visible } = this.option(); - - const overlayStack = this._overlayStack(); - // @ts-ignore expected: types Overlay<OverlayProperties> and this have no overlap - const isTopOverlay = overlayStack[overlayStack.length - 1] === this; - - if (normalizeKeyName(e) === ESC_KEY_NAME && visible && isTopOverlay) { + if (normalizeKeyName(e) === ESC_KEY_NAME && this._handlesDocumentEscapeKey()) { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.hide(); } }; } + _handlesDocumentEscapeKey(): boolean { + const { visible } = this.option(); + + return Boolean(visible) && this._isLastInOverlayStack(); + } + _attachEscapeKeyHandler(): void { const eventName = addNamespace('keydown', this.NAME as string); diff --git a/packages/devextreme/js/__internal/ui/popup/popup.ts b/packages/devextreme/js/__internal/ui/popup/popup.ts index efbd2cf74297..ace3c8ebbc71 100644 --- a/packages/devextreme/js/__internal/ui/popup/popup.ts +++ b/packages/devextreme/js/__internal/ui/popup/popup.ts @@ -245,10 +245,14 @@ class Popup< const e = options.originalEvent; const $target = $(e.target); - if (this._$content && !$target.is(this._$content) - && options.keyName === ESC_KEY_NAME - && !e.isDefaultPrevented() - && !_ignoreCloseOnChildEscape) { + const shouldCloseOnChildEscape = !!this._$content + && !$target.is(this._$content) + && options.keyName === ESC_KEY_NAME + && !e.isDefaultPrevented() + && !_ignoreCloseOnChildEscape + && !this._isEscapeHandledByOverlayAbove(); + + if (shouldCloseOnChildEscape) { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.hide(); } diff --git a/packages/devextreme/js/__internal/ui/selection/selection.strategy.deferred.ts b/packages/devextreme/js/__internal/ui/selection/selection.strategy.deferred.ts index d6e370a5d233..a261105c07d6 100644 --- a/packages/devextreme/js/__internal/ui/selection/selection.strategy.deferred.ts +++ b/packages/devextreme/js/__internal/ui/selection/selection.strategy.deferred.ts @@ -89,7 +89,6 @@ export default class DeferredStrategy< const queryParams = this._getQueryParams(); - // @ts-expect-error dataQuery return !!dataQuery([itemData], queryParams).filter(selectionFilter).toArray().length; } diff --git a/packages/devextreme/js/__internal/ui/selection/selection.strategy.standard.ts b/packages/devextreme/js/__internal/ui/selection/selection.strategy.standard.ts index 4e1ae4bd8b5d..8e711f66e938 100644 --- a/packages/devextreme/js/__internal/ui/selection/selection.strategy.standard.ts +++ b/packages/devextreme/js/__internal/ui/selection/selection.strategy.standard.ts @@ -139,11 +139,11 @@ export default class StandardStrategy< forceCombinedFilter, ); - let deselectedItems = []; + let deselectedItems: TItem[] = []; if (isDeselect) { const { selectedItems } = this.options; + // @ts-expect-error dataQuery().toArray() is typed as unknown[] deselectedItems = combinedFilter && keys.length !== selectedItems.length - // @ts-expect-error dataQuery ? dataQuery(selectedItems).filter(combinedFilter).toArray() : selectedItems.slice(0); } diff --git a/packages/devextreme/js/__internal/ui/selection/selection.strategy.ts b/packages/devextreme/js/__internal/ui/selection/selection.strategy.ts index 932eb2111af9..1a7b9056d022 100644 --- a/packages/devextreme/js/__internal/ui/selection/selection.strategy.ts +++ b/packages/devextreme/js/__internal/ui/selection/selection.strategy.ts @@ -199,7 +199,6 @@ export default class SelectionStrategy< if (localFilter && !isSelectAll) { filteredItems = filteredItems.filter(localFilter); } else if (needLoadAllData) { - // @ts-expect-error dataQuary filteredItems = dataQuery(filteredItems).filter(remoteFilter).toArray(); } diff --git a/packages/devextreme/js/__internal/ui/shared/ui.editor_factory_mixin.ts b/packages/devextreme/js/__internal/ui/shared/ui.editor_factory_mixin.ts index 0458f8f1c274..501d8e2b300b 100644 --- a/packages/devextreme/js/__internal/ui/shared/ui.editor_factory_mixin.ts +++ b/packages/devextreme/js/__internal/ui/shared/ui.editor_factory_mixin.ts @@ -180,7 +180,6 @@ function prepareLookupEditor(options): void { } if (isObject(dataSource) || Array.isArray(dataSource)) { - // @ts-expect-error ts- dataSource = normalizeDataSourceOptions(dataSource); if (isFilterRow) { postProcess = dataSource.postProcess; diff --git a/packages/devextreme/js/__internal/ui/tab_panel/tab_panel.ts b/packages/devextreme/js/__internal/ui/tab_panel/tab_panel.ts index 0022f9aa56d5..827c7cdbe7ad 100644 --- a/packages/devextreme/js/__internal/ui/tab_panel/tab_panel.ts +++ b/packages/devextreme/js/__internal/ui/tab_panel/tab_panel.ts @@ -12,11 +12,11 @@ import { getPublicElement } from '@js/core/element'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { BindableTemplate } from '@js/core/templates/bindable_template'; -import { getImageContainer } from '@js/core/utils/icon'; import { isDefined, isPlainObject } from '@js/core/utils/type'; import type { DxEvent } from '@js/events'; import type { Item, Properties } from '@js/ui/tab_panel'; import { current as currentTheme, isFluent, isMaterialBased } from '@js/ui/themes'; +import { getImageAriaLabel, getImageContainer } from '@ts/core/utils/m_icon'; import supportUtils from '@ts/core/utils/m_support'; import type { OptionChanged } from '@ts/core/widget/types'; import type { MultiViewProperties } from '@ts/ui/multi_view/multi_view'; @@ -388,6 +388,13 @@ class TabPanel extends MultiView<TabPanelProperties> { iconPosition, stylingMode, _itemAttributes: { class: TABPANEL_TABS_ITEM_CLASS }, + _itemAriaLabelExpr: (data: Item): string | undefined => { + if (data?.title || !data?.icon) { + return undefined; + } + + return getImageAriaLabel(data.icon); + }, _indicatorPosition: tabsIndicatorPosition, }; } diff --git a/packages/devextreme/js/__internal/ui/tabs/tabs.ts b/packages/devextreme/js/__internal/ui/tabs/tabs.ts index 9a8d09132c7e..8b24c7fe49dd 100644 --- a/packages/devextreme/js/__internal/ui/tabs/tabs.ts +++ b/packages/devextreme/js/__internal/ui/tabs/tabs.ts @@ -9,7 +9,6 @@ import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import resizeObserverSingleton from '@js/core/resize_observer'; import { BindableTemplate } from '@js/core/templates/bindable_template'; -import { getImageContainer } from '@js/core/utils/icon'; import { each } from '@js/core/utils/iterator'; import { getHeight, getOuterWidth, getWidth } from '@js/core/utils/size'; import { isDefined, isPlainObject } from '@js/core/utils/type'; @@ -27,8 +26,13 @@ import type { } from '@js/ui/tabs'; import { current as currentTheme, isFluent, isMaterial } from '@js/ui/themes'; import { render } from '@ts/core/utils/ink_ripple'; +import { getImageAriaLabel, getImageContainer } from '@ts/core/utils/m_icon'; import type { OptionChanged } from '@ts/core/widget/types'; -import type { CollectionItemInfo, InkRippleEvent } from '@ts/ui/collection/collection_widget.base'; +import type { + CollectionItemInfo, + InkRippleEvent, + PostprocessRenderItemInfo, +} from '@ts/ui/collection/collection_widget.base'; import type { CollectionWidgetLiveUpdateProperties } from '@ts/ui/collection/collection_widget.live_update'; import Scrollable from '@ts/ui/scroll_view/scrollable'; import { @@ -136,6 +140,8 @@ export interface TabsProperties extends Properties, Omit< badgeExpr?: (data) => string | undefined; + _itemAriaLabelExpr?: (data: Item) => string | undefined; + _indicatorPosition?: Position | null; } @@ -180,6 +186,13 @@ class Tabs extends CollectionWidgetLiveUpdate<TabsProperties> { badgeExpr(data: Item): string | undefined { return data?.badge; }, + _itemAriaLabelExpr(data: Item): string | undefined { + if (data?.text || data?.html || !data?.icon) { + return undefined; + } + + return getImageAriaLabel(data.icon); + }, _itemAttributes: { role: 'tab' }, _indicatorPosition: null, }; @@ -318,6 +331,20 @@ class Tabs extends CollectionWidgetLiveUpdate<TabsProperties> { this._renderScrolling(); } + _postprocessRenderItem(args: PostprocessRenderItemInfo<Item>): void { + super._postprocessRenderItem(args); + + this._renderItemAriaLabel(args); + } + + _renderItemAriaLabel({ itemData, itemElement }: PostprocessRenderItemInfo<Item>): void { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { _itemAriaLabelExpr } = this.option(); + const label = _itemAriaLabelExpr?.(itemData); + + this.setAria('label', label, itemElement); + } + _renderScrolling(): void { const removeClasses = [TABS_STRETCHED_CLASS, TABS_EXPANDED_CLASS, OVERFLOW_HIDDEN_CLASS]; this.$element().removeClass(removeClasses.join(' ')); @@ -836,6 +863,7 @@ class Tabs extends CollectionWidgetLiveUpdate<TabsProperties> { super._optionChanged(args); break; case 'badgeExpr': + case '_itemAriaLabelExpr': this._invalidate(); break; case 'focusedElement': { diff --git a/packages/devextreme/js/__internal/ui/text_box/text_editor.base.ts b/packages/devextreme/js/__internal/ui/text_box/text_editor.base.ts index a95ae65b7456..6fe15ab75575 100644 --- a/packages/devextreme/js/__internal/ui/text_box/text_editor.base.ts +++ b/packages/devextreme/js/__internal/ui/text_box/text_editor.base.ts @@ -34,6 +34,15 @@ import { TextEditorLabel } from './text_editor.label'; import type { TextEditorButtonInfo } from './texteditor_button_collection/index'; import TextEditorButtonCollection from './texteditor_button_collection/index'; +export interface TextEditorInputAttributes { + autocomplete: string; + + placeholder: string | null; + + // eslint-disable-next-line spellcheck/spell-checker + inputmode?: string; +} + export interface TextEditorInternalProperties extends EditorInternalProperties { // eslint-disable-next-line @typescript-eslint/no-explicit-any displayValueFormatter?: ((value: string | any[]) => string); @@ -446,12 +455,7 @@ class TextEditorBase< return value; } - _getDefaultAttributes(): { - autocomplete: string; - placeholder: string | null; - // eslint-disable-next-line spellcheck/spell-checker - inputmode?: string; - } { + _getDefaultAttributes(): TextEditorInputAttributes { const defaultAttributes = { autocomplete: 'off', placeholder: this._getPlaceholderAttr(), @@ -773,7 +777,7 @@ class TextEditorBase< } // eslint-disable-next-line @typescript-eslint/no-unused-vars - _keyPressHandler(e?: { originalEvent: InputEvent & KeyboardEvent }): void { + _keyPressHandler(e?: DxEvent): void { this.option('text', this._input().val()); } diff --git a/packages/devextreme/js/__internal/ui/text_box/text_editor.clear.ts b/packages/devextreme/js/__internal/ui/text_box/text_editor.clear.ts index f072220c37d5..4935f48cee4f 100644 --- a/packages/devextreme/js/__internal/ui/text_box/text_editor.clear.ts +++ b/packages/devextreme/js/__internal/ui/text_box/text_editor.clear.ts @@ -32,7 +32,7 @@ interface EditorWithClearButton { export default class ClearButton< // eslint-disable-next-line @typescript-eslint/no-explicit-any TComponent extends Editor<any> & EditorWithClearButton = TextEditorBase, -> extends TextEditorButton<TComponent> { +> extends TextEditorButton<TComponent, dxElementWrapper> { _create(): { instance: dxElementWrapper; $element: dxElementWrapper; @@ -101,7 +101,6 @@ export default class ClearButton< const isVisible = this._isVisible(); if (instance) { - // @ts-expect-error instance is dxElementWrapper instance.toggleClass(STATE_INVISIBLE_CLASS, !isVisible); } diff --git a/packages/devextreme/js/__internal/ui/text_box/text_editor.mask.ts b/packages/devextreme/js/__internal/ui/text_box/text_editor.mask.ts index 46d0e038133a..4a18e79436ff 100644 --- a/packages/devextreme/js/__internal/ui/text_box/text_editor.mask.ts +++ b/packages/devextreme/js/__internal/ui/text_box/text_editor.mask.ts @@ -340,7 +340,7 @@ class TextEditorMask< _replaceSelectedText( text: string, selection: CaretRange, - char: string, + char: string | undefined, ): string { if (char === undefined) { return text; diff --git a/packages/devextreme/js/__internal/ui/text_box/texteditor_button_collection/button.ts b/packages/devextreme/js/__internal/ui/text_box/texteditor_button_collection/button.ts index f519c2a6e2e2..04f31a4fb39e 100644 --- a/packages/devextreme/js/__internal/ui/text_box/texteditor_button_collection/button.ts +++ b/packages/devextreme/js/__internal/ui/text_box/texteditor_button_collection/button.ts @@ -5,7 +5,7 @@ import Button from '@js/ui/button'; import type Widget from '@ts/core/widget/widget'; import type TextEditorBase from '@ts/ui/text_box/text_editor.base'; -type TextEditorButtonInstance = dxElementWrapper | Button; +export type TextEditorButtonInstance = dxElementWrapper | Button; export const isButtonInstance = ( instance: unknown, @@ -14,12 +14,13 @@ export const isButtonInstance = ( export default class TextEditorButton< // eslint-disable-next-line @typescript-eslint/no-explicit-any TComponent extends Widget<any> = TextEditorBase, + TInstance extends TextEditorButtonInstance = TextEditorButtonInstance, > { $container!: dxElementWrapper | null; $placeMarker?: dxElementWrapper | null; - instance?: TextEditorButtonInstance | null; + instance?: TInstance | null; editor!: TComponent | null; @@ -55,12 +56,12 @@ export default class TextEditorButton< } // eslint-disable-next-line @typescript-eslint/no-unused-vars - _attachEvents(instance: unknown, $element?: dxElementWrapper): void { + _attachEvents(instance: TInstance | undefined, $element?: dxElementWrapper): void { throw new Error('Not implemented'); } _create(): { - instance: Button | dxElementWrapper; + instance: TInstance; $element: dxElementWrapper; } | undefined { throw new Error('Not implemented'); diff --git a/packages/devextreme/js/__internal/ui/text_box/texteditor_button_collection/custom.ts b/packages/devextreme/js/__internal/ui/text_box/texteditor_button_collection/custom.ts index fdb6748a60fb..225953ea1f41 100644 --- a/packages/devextreme/js/__internal/ui/text_box/texteditor_button_collection/custom.ts +++ b/packages/devextreme/js/__internal/ui/text_box/texteditor_button_collection/custom.ts @@ -6,11 +6,11 @@ import $ from '@js/core/renderer'; import type { Properties as ButtonProperties } from '@js/ui/button'; import Button from '@js/ui/button'; import type TextEditorBase from '@ts/ui/text_box/text_editor.base'; -import TextEditorButton, { isButtonInstance } from '@ts/ui/text_box/texteditor_button_collection/button'; +import TextEditorButton from '@ts/ui/text_box/texteditor_button_collection/button'; const CUSTOM_BUTTON_HOVERED_CLASS = 'dx-custom-button-hovered'; -export default class CustomButton extends TextEditorButton { +export default class CustomButton extends TextEditorButton<TextEditorBase, Button> { _attachEvents( instance: Button, $element: dxElementWrapper, @@ -70,7 +70,7 @@ export default class CustomButton extends TextEditorButton { update(): boolean { const isUpdated = super.update(); - if (isButtonInstance(this.instance)) { + if (this.instance) { this.instance.option('disabled', this._isDisabled()); } @@ -86,7 +86,7 @@ export default class CustomButton extends TextEditorButton { _isDisabled(): boolean | undefined { const isDefinedByUser = this.options.disabled !== undefined; if (isDefinedByUser) { - if (isButtonInstance(this.instance)) { + if (this.instance) { return this.instance.option('disabled'); } diff --git a/packages/devextreme/js/__internal/ui/validation_summary.ts b/packages/devextreme/js/__internal/ui/validation_summary.ts index 13538c1d59f7..8bbdf63d43b6 100644 --- a/packages/devextreme/js/__internal/ui/validation_summary.ts +++ b/packages/devextreme/js/__internal/ui/validation_summary.ts @@ -3,6 +3,7 @@ import registerComponent from '@js/core/component_registrator'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import type { OptionChanged } from '@ts/core/widget/types'; +import { SCREEN_READER_ONLY_CLASS } from '@ts/core/widget/widget'; import type { PostprocessRenderItemInfo } from '@ts/ui/collection/collection_widget.base'; import type { CollectionWidgetEditProperties } from '@ts/ui/collection/collection_widget.edit'; import CollectionWidget from '@ts/ui/collection/collection_widget.edit'; @@ -17,7 +18,6 @@ import ValidationEngine from '@ts/ui/validation_engine'; import type Validator from '@ts/ui/validator'; const VALIDATION_SUMMARY_CLASS = 'dx-validationsummary'; -const SCREEN_READER_ONLY_CLASS = 'dx-screen-reader-only'; const ITEM_CLASS = `${VALIDATION_SUMMARY_CLASS}-item`; const ITEM_DATA_KEY = `${VALIDATION_SUMMARY_CLASS}-item-data`; diff --git a/packages/devextreme/js/common/data/array_utils.js b/packages/devextreme/js/common/data/array_utils.js index 74993adf03d4..d3f8e91646bc 100644 --- a/packages/devextreme/js/common/data/array_utils.js +++ b/packages/devextreme/js/common/data/array_utils.js @@ -1 +1 @@ -export * from '../../__internal/data/m_array_utils'; +export * from '../../__internal/data/array_utils'; diff --git a/packages/devextreme/js/common/data/data_source/operation_manager.js b/packages/devextreme/js/common/data/data_source/operation_manager.js index 059f55b8e65f..6de10df1d202 100644 --- a/packages/devextreme/js/common/data/data_source/operation_manager.js +++ b/packages/devextreme/js/common/data/data_source/operation_manager.js @@ -1 +1 @@ -export { default } from '../../../__internal/data/data_source/m_operation_manager'; +export { default } from '../../../__internal/data/data_source/operation_manager'; diff --git a/packages/devextreme/js/common/data/data_source/utils.js b/packages/devextreme/js/common/data/data_source/utils.js index 8b2686832ca6..52e070da838e 100644 --- a/packages/devextreme/js/common/data/data_source/utils.js +++ b/packages/devextreme/js/common/data/data_source/utils.js @@ -1 +1 @@ -export * from '../../../__internal/data/data_source/m_utils'; +export * from '../../../__internal/data/data_source/utils'; diff --git a/packages/devextreme/js/common/data/endpoint_selector.js b/packages/devextreme/js/common/data/endpoint_selector.js index e613c011c0ce..ea7b56fad2d1 100644 --- a/packages/devextreme/js/common/data/endpoint_selector.js +++ b/packages/devextreme/js/common/data/endpoint_selector.js @@ -4,4 +4,4 @@ * @param1 options:Object * @hidden */ -export { default } from '../../__internal/data/m_endpoint_selector'; +export { default } from '../../__internal/data/endpoint_selector'; diff --git a/packages/devextreme/js/common/data/errors.js b/packages/devextreme/js/common/data/errors.js index f2ad9f675151..255b551e3774 100644 --- a/packages/devextreme/js/common/data/errors.js +++ b/packages/devextreme/js/common/data/errors.js @@ -88,4 +88,4 @@ * @name ErrorsData.W4002 */ -export * from '../../__internal/data/m_errors'; +export * from '../../__internal/data/errors'; diff --git a/packages/devextreme/js/common/data/local_store.js b/packages/devextreme/js/common/data/local_store.js index 2b903d32d98b..48ec3d1bcc87 100644 --- a/packages/devextreme/js/common/data/local_store.js +++ b/packages/devextreme/js/common/data/local_store.js @@ -1 +1 @@ -export { default } from '../../__internal/data/m_local_store'; +export { default } from '../../__internal/data/local_store'; diff --git a/packages/devextreme/js/common/data/odata/query_adapter.js b/packages/devextreme/js/common/data/odata/query_adapter.js index a56104665e6c..dc61288953ee 100644 --- a/packages/devextreme/js/common/data/odata/query_adapter.js +++ b/packages/devextreme/js/common/data/odata/query_adapter.js @@ -1 +1 @@ -export * from '../../../__internal/data/odata/m_query_adapter'; +export * from '../../../__internal/data/odata/query_adapter'; diff --git a/packages/devextreme/js/common/data/odata/request_dispatcher.js b/packages/devextreme/js/common/data/odata/request_dispatcher.js index 13fc3dc1b4c6..434e13d5a8eb 100644 --- a/packages/devextreme/js/common/data/odata/request_dispatcher.js +++ b/packages/devextreme/js/common/data/odata/request_dispatcher.js @@ -1 +1 @@ -export { default } from '../../../__internal/data/odata/m_request_dispatcher'; +export { default } from '../../../__internal/data/odata/request_dispatcher'; diff --git a/packages/devextreme/js/common/data/query.js b/packages/devextreme/js/common/data/query.js index b1e2c444e038..bbb50289ac52 100644 --- a/packages/devextreme/js/common/data/query.js +++ b/packages/devextreme/js/common/data/query.js @@ -1 +1 @@ -export { default } from '../../__internal/data/m_query'; +export { default } from '../../__internal/data/query'; diff --git a/packages/devextreme/js/common/data/remote_query.js b/packages/devextreme/js/common/data/remote_query.js index 592ebfe65d2c..51f3df760e1e 100644 --- a/packages/devextreme/js/common/data/remote_query.js +++ b/packages/devextreme/js/common/data/remote_query.js @@ -1 +1 @@ -export { default } from '../../__internal/data/m_remote_query'; +export { default } from '../../__internal/data/remote_query'; diff --git a/packages/devextreme/js/common/data/store_helper.js b/packages/devextreme/js/common/data/store_helper.js index bb0fca23c7d1..5b66fa3425b1 100644 --- a/packages/devextreme/js/common/data/store_helper.js +++ b/packages/devextreme/js/common/data/store_helper.js @@ -1 +1 @@ -export { default } from '../../__internal/data/m_store_helper'; +export { default } from '../../__internal/data/store_helper'; diff --git a/packages/devextreme/js/common/data/utils.js b/packages/devextreme/js/common/data/utils.js index a59212919375..ba1bc28b4c72 100644 --- a/packages/devextreme/js/common/data/utils.js +++ b/packages/devextreme/js/common/data/utils.js @@ -3,7 +3,7 @@ import { compileGetter, compileSetter } from '../../core/utils/data'; /** * @name Utils */ -export * from '../../__internal/data/m_utils'; +export * from '../../__internal/data/utils'; export { compileGetter, diff --git a/packages/devextreme/js/ui/number_box.js b/packages/devextreme/js/ui/number_box.js index 5ea396acda04..a682bbaa8a92 100644 --- a/packages/devextreme/js/ui/number_box.js +++ b/packages/devextreme/js/ui/number_box.js @@ -1,4 +1,4 @@ -import NumberBox from '../__internal/ui/number_box/m_number_box'; +import NumberBox from '../__internal/ui/number_box/number_box'; export default NumberBox; diff --git a/packages/devextreme/project.json b/packages/devextreme/project.json index a5174d282eb1..aa39d154dbbc 100644 --- a/packages/devextreme/project.json +++ b/packages/devextreme/project.json @@ -1812,15 +1812,6 @@ "lint-texts" ] }, - "lint-css": { - "executor": "nx:run-script", - "options": { - "script": "lint-css" - }, - "inputs": [ - "{projectRoot}/scss/**/*" - ] - }, "lint-dts": { "executor": "nx:run-script", "options": { @@ -1888,17 +1879,6 @@ "{projectRoot}/build/**/*", "{projectRoot}/jest.*" ] - }, - "test-styles": { - "executor": "nx:run-script", - "options": { - "script": "test-styles" - }, - "inputs": [ - "default", - "!{projectRoot}/js/**/*", - "!{projectRoot}/ts/**/*" - ] } }, "namedInputs": { diff --git a/packages/devextreme/testing/helpers/gridBaseMocks.js b/packages/devextreme/testing/helpers/gridBaseMocks.js index 05441eea0baa..36b48f913316 100644 --- a/packages/devextreme/testing/helpers/gridBaseMocks.js +++ b/packages/devextreme/testing/helpers/gridBaseMocks.js @@ -1,6 +1,81 @@ module.exports = function($, gridCore, columnResizingReordering, domUtils, commonUtils, typeUtils, ArrayStore, nameWidget) { const exports = {}; + exports.MockDataSourceAdapter = function(options) { + // Loads read options.items: the stores tests pass in options.store are write-only spies. + const itemsStore = function() { + return new ArrayStore(options.items); + }; + + const loadCustomResult = (loadOptions) => { + const d = $.Deferred(); + + itemsStore().load(loadOptions) + .done((data, extra) => { + d.resolve({ data: data, extra: extra }); + }) + .fail(d.reject); + + return d; + }; + + return { + _dataSource: options.dataSource, + beginLoading: function() { + }, + endLoading: function() { + }, + remoteOperations: function() { + return {}; + }, + loadOptions: function() { + return {}; + }, + key: function() { + return options.key; + }, + isLoading: function() { + return false; + }, + lastLoadOptions: function() { + return {}; + }, + operationTypes: function() { + return undefined; + }, + select: function() { + return undefined; + }, + getDataIndexGetter: function() { + return undefined; + }, + getCachedStoreData: function() { + return undefined; + }, + loadingOperationTypes: function() { + return undefined; + }, + dispose: function() { + }, + store: function() { + return options.store; + }, + load: function(loadOptions) { + return itemsStore().load(loadOptions); + }, + customLoader: { + load: loadCustomResult, + loadFromStore: loadCustomResult, + isLoading: function() { + return false; + }, + isLoadingAll: function() { + return false; + } + } + }; + }; + exports.MockDataController = function(options) { if(!typeUtils.isDefined(options.itemsCount)) { @@ -37,50 +112,7 @@ module.exports = function($, gridCore, columnResizingReordering, domUtils, commo changedArgs: [], - dataSource: function() { - const store = function() { - return new ArrayStore(options.items); - }; - - const loadCustomResult = (loadOptions) => { - const d = $.Deferred(); - - store().load(loadOptions) - .done((data, extra) => { - d.resolve({ data: data, extra: extra }); - }) - .fail(d.reject); - - return d; - }; - - return { - beginLoading: function() { - }, - endLoading: function() { - }, - remoteOperations: function() { - return {}; - }, - loadOptions: function() { - return {}; - }, - store: store, - load: function(loadOptions) { - return store().load(loadOptions); - }, - customLoader: { - load: loadCustomResult, - loadFromStore: loadCustomResult, - isLoading: function() { - return false; - }, - isLoadingAll: function() { - return false; - } - } - }; - }, + mockOptions: options, pageSize: function(value) { if(value === undefined) { @@ -149,10 +181,6 @@ module.exports = function($, gridCore, columnResizingReordering, domUtils, commo }); }, - store: function() { - return options.store; - }, - insertItems: function(insertingItems) { $.each(insertingItems, function() { options.items.push(this); @@ -260,18 +288,12 @@ module.exports = function($, gridCore, columnResizingReordering, domUtils, commo getCombinedFilter: commonUtils.noop, - getFilterExcludedColumn: commonUtils.noop, - getCombinedFilterWithExcludedColumn: commonUtils.noop, getRowIndexByKey: function(key) { return gridCore.getIndexByKey(key, options.items); }, - loadingOperationTypes: function() { - return {}; - }, - skipProcessingPagingChange: commonUtils.noop, changed: $.Callbacks(), loadingChanged: $.Callbacks(), @@ -994,11 +1016,15 @@ module.exports = function($, gridCore, columnResizingReordering, domUtils, commo _subscribeToEvents(rootElement) { } }; + // The dataSource and filter controllers are leaves that the data controller resolves + // in init(), so they are always included rather than listed by every caller. + const ALWAYS_INCLUDED_MODULES = ['dataSource', 'filter']; + exports['setup' + nameWidget + 'Modules'] = function(that, moduleNames, options) { const modules = []; $.each(gridCore.modules, function() { - if($.inArray(this.name, moduleNames) !== -1) { + if($.inArray(this.name, moduleNames) !== -1 || $.inArray(this.name, ALWAYS_INCLUDED_MODULES) !== -1) { modules.push(this); } }); @@ -1210,6 +1236,13 @@ module.exports = function($, gridCore, columnResizingReordering, domUtils, commo options && options.controllers && $.extend(that._controllers, options.controllers); options && options.views && $.extend(that._views, options.views); + const mockedDataController = options && options.controllers && options.controllers.data; + if(mockedDataController && mockedDataController.mockOptions && that._controllers.dataSource) { + that._controllers.dataSource.adapter = new exports.MockDataSourceAdapter( + mockedDataController.mockOptions + ); + } + $.each(that._controllers, function(name) { that[name + 'Controller'] = this; this.init && this.init(); diff --git a/packages/devextreme/testing/tests/DevExpress.animation/position.tests.js b/packages/devextreme/testing/tests/DevExpress.animation/position.tests.js index 817c90f1fcd0..752e74173c64 100644 --- a/packages/devextreme/testing/tests/DevExpress.animation/position.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.animation/position.tests.js @@ -3,6 +3,7 @@ import positionUtils from 'common/core/animation/position'; import translator from '__internal/common/core/animation/translatorModule'; import { stubSeam } from '../../helpers/moduleSeam.js'; import browser from 'core/utils/browser'; +import devices from '__internal/core/m_devices'; import fixtures from '../../helpers/positionFixtures.js'; import { implementationsMap } from 'core/utils/size'; import { getWindow } from 'core/utils/window.js'; @@ -943,23 +944,30 @@ const testCollision = (name, fixtureName, params, expectedHorzDist, expectedVert assert.deepEqual(positionUtils.offset($event), { left: 100, top: 200 }, 'position.offset() is correct'); }); - QUnit.test('position should return window.innerHeight if window.outerHeight < window.innerHeight', function(assert) { + QUnit.test('position should measure the window by its client height whatever window.outerHeight reports', function(assert) { const $what = $('#what').height(300); const initialInnerHeight = window.innerHeight; const initialOuterHeight = window.outerHeight; + const heightStub = sinon.stub(implementationsMap, 'getHeight').returns(1000); + try { window.innerHeight = 500; - window.outerHeight = 200; - const resultPosition = setupPosition($what, { - of: $(window) - }); + // 0 is what the browser reports before the first frame, a smaller value comes from a zoomed out page + [0, 200].forEach((outerHeight) => { + window.outerHeight = outerHeight; + + const resultPosition = setupPosition($what, { + of: $(window) + }); - assert.roughEqual(resultPosition.v.location, 100, 50, 'vertical location is correct'); + assert.roughEqual(resultPosition.v.location, 350, 50, `vertical location is correct, outerHeight is ${outerHeight}`); + }); } finally { window.innerHeight = initialInnerHeight; window.outerHeight = initialOuterHeight; + heightStub.restore(); } }); @@ -1019,6 +1027,86 @@ const testCollision = (name, fixtureName, params, expectedHorzDist, expectedVert } }); + QUnit.test('position should measure the window by its client height on desktop Safari', function(assert) { + const $what = $('#what').height(300); + const initialInnerHeight = window.innerHeight; + const initialSafari = browser.safari; + + const heightStub = sinon.stub(implementationsMap, 'getHeight').returns(1000); + const devicesStub = sinon.stub(devices, 'real').returns({ deviceType: 'desktop', platform: 'generic' }); + + try { + window.innerHeight = 500; + browser.safari = true; + + const resultPosition = setupPosition($what, { + of: $(window) + }); + + assert.roughEqual(resultPosition.v.location, 350, 50, 'vertical location is correct'); + } finally { + window.innerHeight = initialInnerHeight; + browser.safari = initialSafari; + heightStub.restore(); + devicesStub.restore(); + } + }); + + QUnit.test('position should measure the window by window.innerHeight on iOS whatever the browser is', function(assert) { + const $what = $('#what').height(300); + const initialInnerHeight = window.innerHeight; + const initialSafari = browser.safari; + + const heightStub = sinon.stub(implementationsMap, 'getHeight').returns(1000); + const devicesStub = sinon.stub(devices, 'real').returns({ deviceType: 'desktop', platform: 'ios' }); + + try { + window.innerHeight = 500; + + [true, false].forEach((isSafari) => { + browser.safari = isSafari; + + const resultPosition = setupPosition($what, { + of: $(window) + }); + + assert.roughEqual(resultPosition.v.location, 100, 50, `vertical location is correct, browser.safari is ${isSafari}`); + }); + } finally { + window.innerHeight = initialInnerHeight; + browser.safari = initialSafari; + heightStub.restore(); + devicesStub.restore(); + } + }); + + QUnit.test('position should measure the window by its client width whatever window.outerWidth reports', function(assert) { + const $what = $('#what').width(300); + const initialInnerWidth = window.innerWidth; + const initialOuterWidth = window.outerWidth; + + const widthStub = sinon.stub(implementationsMap, 'getWidth').returns(1000); + + try { + window.innerWidth = 500; + + // 0 is what the browser reports before the first frame, a smaller value comes from a zoomed out page + [0, 200].forEach((outerWidth) => { + window.outerWidth = outerWidth; + + const resultPosition = setupPosition($what, { + of: $(window) + }); + + assert.roughEqual(resultPosition.h.location, 350, 50, `horizontal location is correct, outerWidth is ${outerWidth}`); + }); + } finally { + window.innerWidth = initialInnerWidth; + window.outerWidth = initialOuterWidth; + widthStub.restore(); + } + }); + // T664522 QUnit.test('setup should call resetPosition with finishTransition argument', function(assert) { const resetPositionStub = stubSeam(translator, 'resetPosition', 'DEBUG_set_resetPosition').callsFake(($element, finishTransition) => { diff --git a/packages/devextreme/testing/tests/DevExpress.core/utils.icon.tests.js b/packages/devextreme/testing/tests/DevExpress.core/utils.icon.tests.js index 2956a9598ff1..c61c0e8a06b9 100644 --- a/packages/devextreme/testing/tests/DevExpress.core/utils.icon.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.core/utils.icon.tests.js @@ -1,4 +1,7 @@ import { getImageSourceType, getImageContainer } from 'core/utils/icon'; +import { getImageAriaLabel } from '__internal/core/utils/m_icon'; +import localization from 'localization'; +import ja from 'localization/messages/ja.json!'; const { module: testModule, test } = QUnit; @@ -144,3 +147,36 @@ testModule('icon utils', { }); }); }); + +testModule('getImageAriaLabel', () => { + [ + { source: 'close', expected: 'Close', description: 'a dxIcon with a localized message' }, + { source: 'iconName', expected: 'iconName', description: 'a dxIcon without a localized message' }, + { source: 'fa fa-home', expected: 'fa fa-home', description: 'a font icon' }, + { source: '/path/file.png', expected: 'file', description: 'a path to an image' }, + { source: 'https://example.com/path/file.png', expected: undefined, description: 'an image URL' }, + { source: 'www.example.com/file.png', expected: undefined, description: 'an image URL without a protocol' }, + { source: 'data:image/png;base64,qwerty', expected: undefined, description: 'a base64 image' }, + { source: '<svg><title>Svg title', expected: 'Svg title', description: 'an svg with a title' }, + { source: '', expected: undefined, description: 'an svg with an empty title' }, + { source: '', expected: undefined, description: 'an svg without a title' }, + { source: '', expected: undefined, description: 'an empty string' }, + ].forEach(({ source, expected, description }) => { + test(`should return ${JSON.stringify(expected) || 'undefined'} for ${description}`, function(assert) { + assert.strictEqual(getImageAriaLabel(source), expected); + }); + }); + + test('should localize the dxIcon name with the current locale', function(assert) { + const defaultLocale = localization.locale(); + + try { + localization.loadMessages(ja); + localization.locale('ja'); + + assert.strictEqual(getImageAriaLabel('close'), '閉じる'); + } finally { + localization.locale(defaultLocale); + } + }); +}); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.events/click.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.events/click.tests.js index b1a60379837d..d7faf25dfb2d 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.events/click.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.events/click.tests.js @@ -1,6 +1,7 @@ import $ from 'jquery'; import { noop } from 'core/utils/common'; import clickEvent from 'common/core/events/click'; +import { removeEvent } from 'common/core/events/remove'; import domUtils from '__internal/core/utils/m_dom'; import support from '__internal/core/utils/m_support'; import devices from '__internal/core/m_devices'; @@ -425,3 +426,52 @@ QUnit.test('dxclick should not be fired twice when \'click\' is triggered from i pointer.start().down().up(); $(document).off('dxclick', $.noop); }); + +QUnit.test('foreign dxremove handler on the previously clicked node should survive a dxclick on another node (5025)', function(assert) { + let clickedNodeClickCount = 0; + let otherNodeClickCount = 0; + let foreignHandlerCallCount = 0; + + const $clicked = $('#first').on('dxclick', function() { + clickedNodeClickCount++; + }); + const $other = $('#second').on('dxclick', function() { + otherNodeClickCount++; + }); + + nativePointerMock($clicked).start().click(); + + $clicked.on(removeEvent, function() { + foreignHandlerCallCount++; + }); + + nativePointerMock($other).start().click(); + + $clicked.triggerHandler({ type: removeEvent }); + + assert.equal(clickedNodeClickCount, 1, 'dxclick fired on the clicked node'); + assert.equal(otherNodeClickCount, 1, 'dxclick fired on the other node'); + assert.equal(foreignHandlerCallCount, 1, `foreign ${removeEvent} handler is still subscribed`); +}); + +QUnit.test('foreign dxremove handler should survive a second dxclick on the same node', function(assert) { + let clickCount = 0; + let foreignHandlerCallCount = 0; + + const $clicked = $('#first').on('dxclick', function() { + clickCount++; + }); + + nativePointerMock($clicked).start().click(); + + $clicked.on(removeEvent, function() { + foreignHandlerCallCount++; + }); + + nativePointerMock($clicked).start().click(); + + $clicked.triggerHandler({ type: removeEvent }); + + assert.equal(clickCount, 2, 'dxclick fired twice on the same node'); + assert.equal(foreignHandlerCallCount, 1, `foreign ${removeEvent} handler is still subscribed`); +}); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.events/events.utils.nodesDisposing.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.events/events.utils.nodesDisposing.tests.js index c59021aa41f3..b42d196ee5ec 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.events/events.utils.nodesDisposing.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.events/events.utils.nodesDisposing.tests.js @@ -31,7 +31,7 @@ QUnit.test('should clean elementDataMap when using subscribeNodesDisposing and u ? afterSubscribeElementData[removeEvent].handleObjects.length : 0; - unsubscribeNodesDisposing(clickEvent, subscriptionData.callback, subscriptionData.nodes); + unsubscribeNodesDisposing(clickEvent, subscriptionData.onceCallback, subscriptionData.nodes); const finalElementData = eventsEngine.elementDataMap.get(document); const afterUnsubscribeHandleObjectsCount = finalElementData && finalElementData[removeEvent] diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsResizingReorderingModule.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsResizingReorderingModule.tests.js index ea8ba47f77f1..545a24d13539 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsResizingReorderingModule.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsResizingReorderingModule.tests.js @@ -968,6 +968,11 @@ QUnit.module('Columns resizing', { ] }), + dataSource: { + key: noop, + store: noop + }, + columnsResizer: { isResizing: () => true }, diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataController.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataController.tests.js index c2fc39385b72..f018d1e4c3f9 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataController.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataController.tests.js @@ -241,7 +241,7 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod this.dataController.dataPushedHandlerProxy = dataPushedHandlerSpy; this.dataController.setDataSource(dataSource); - dataSource = this.dataController.dataSource(); + dataSource = this.dataSourceController.getAdapter(); dataSource.load(); // assert @@ -319,7 +319,7 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod this.option('dataSource', []); - const dataSource = this.dataController.dataSource()._dataSource; + const dataSource = this.dataSourceController.getDataSource(); this.dataController.changed.add(function(args) { changedCount++; @@ -344,7 +344,7 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod // assert assert.ok(dataSource._disposed, 'dataSource is disposed'); - assert.strictEqual(this.dataController._dataSource, null, 'dataSourceAdapter is removed'); + assert.strictEqual(this.dataSourceController.getAdapter(), null, 'dataSourceAdapter is removed'); }); // T697860 @@ -383,7 +383,7 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod dataSource.load(); // act - this.dataController.setDataSource(null); + this.dataController._disposeDataSource(); // assert assert.strictEqual(loadingChangedSpy.callCount, 2, 'loadingChanged call count'); @@ -503,7 +503,7 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod // assert - assert.deepEqual(this.dataController._dataSource.group(), [{ selector: 'name', isExpanded: true, desc: false }, { selector: 'age', isExpanded: true, desc: false }]); + assert.deepEqual(this.dataSourceController.getAdapter().group(), [{ selector: 'name', isExpanded: true, desc: false }, { selector: 'age', isExpanded: true, desc: false }]); assert.equal(this.dataController.items()[0].data.key, 'Alex'); assert.equal(this.dataController.items()[1].data.key, 30); assert.equal(changedCallCount, 1, 'changed called one time'); // T122785 @@ -535,7 +535,7 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod this.dataController.resetDataSource(); // assert - assert.deepEqual(this.dataController._dataSource.group(), [{ selector: 'name', isExpanded: false, desc: false }]); + assert.deepEqual(this.dataSourceController.getAdapter().group(), [{ selector: 'name', isExpanded: false, desc: false }]); assert.equal(this.dataController.items()[0].data.key, 'Alex'); assert.deepEqual(this.dataController.items()[0].data.collapsedItems, [{ name: 'Alex', age: 30 }]); assert.equal(this.dataController.items()[0].data.items, null); @@ -577,7 +577,7 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod assert.equal(loadingCount, 1, 'loading called one time'); // assert - assert.deepEqual(this.dataController._dataSource.group(), [{ selector: 'name', isExpanded: true, desc: false }]); + assert.deepEqual(this.dataSourceController.getAdapter().group(), [{ selector: 'name', isExpanded: true, desc: false }]); assert.equal(this.dataController.items()[0].data.key, 'Alex'); assert.equal(this.dataController.items()[0].data.items.length, 1); @@ -590,7 +590,7 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod assert.equal(changedCallCount, 1, 'changed called one time'); // T372049 assert.equal(loadingCount, 1, 'loading is not called on collapseAll'); - assert.deepEqual(this.dataController._dataSource.group(), [{ selector: 'name', isExpanded: false, desc: false }]); + assert.deepEqual(this.dataSourceController.getAdapter().group(), [{ selector: 'name', isExpanded: false, desc: false }]); assert.equal(this.dataController.items()[0].data.key, 'Alex'); assert.strictEqual(this.dataController.items()[0].data.items, null); }); @@ -632,7 +632,7 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod assert.deepEqual(loadingArgs[0].group, [{ selector: 'name', isExpanded: true, desc: false }], 'loading group arg'); // assert - assert.deepEqual(this.dataController._dataSource.group(), [{ selector: 'name', isExpanded: true, desc: false }]); + assert.deepEqual(this.dataSourceController.getAdapter().group(), [{ selector: 'name', isExpanded: true, desc: false }]); assert.equal(this.dataController.items()[0].data.key, 'Alex'); assert.equal(this.dataController.items()[0].data.items.length, 1); @@ -645,7 +645,7 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod assert.equal(changedCallCount, 1, 'changed called one time after collapseAll'); // T372049 assert.equal(loadingArgs.length, 1, 'loading is not called after collapseAll'); - assert.deepEqual(this.dataController._dataSource.group(), [{ selector: 'name', isExpanded: false, desc: false }]); + assert.deepEqual(this.dataSourceController.getAdapter().group(), [{ selector: 'name', isExpanded: false, desc: false }]); assert.equal(this.dataController.items()[0].data.key, 'Alex'); assert.strictEqual(this.dataController.items()[0].data.items, null); }); @@ -873,8 +873,7 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod // act this.dataController.resetDataSource(); - const dataIndexGetter = this.dataController._dataSource.getDataIndexGetter(); - this.dataController._dataSource.getDataIndexGetter(); + const dataIndexGetter = this.dataSourceController.getDataIndexGetter(); assert.deepEqual(this.dataController._columnsController.getSortDataSourceParameters(), [{ desc: false, selector: dataIndexGetter }], 'Sort parameters'); // assert assert.equal(dataSource.items()[0].name, 'Alex', 'Item0'); @@ -1955,7 +1954,7 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod this.dataController.resetDataSource(); // assert - assert.deepEqual(this.dataController._dataSource.group(), [{ selector: 'team', isExpanded: true, desc: false }]); + assert.deepEqual(this.dataSourceController.getAdapter().group(), [{ selector: 'team', isExpanded: true, desc: false }]); assert.equal(this.dataController.items()[0].data.key, 'internal'); assert.equal(this.dataController.items()[1].data.name, 'Bob'); }); @@ -1998,13 +1997,13 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod this.dataController.setDataSource(dataSource); dataSource.load(); - assert.ok(this.dataController.dataSource().sort(), 'sort parameters'); + assert.ok(this.dataSourceController.getAdapter().sort(), 'sort parameters'); // act this.columnsController.changeSortOrder(0, 'none'); // assert - assert.ok(!this.dataController.dataSource().sort(), 'no sort parameters'); + assert.ok(!this.dataSourceController.getAdapter().sort(), 'no sort parameters'); assert.deepEqual(this.dataController.items()[0].values, ['Alex', 30]); assert.deepEqual(this.dataController.items()[1].values, ['Dan', 25]); @@ -2028,7 +2027,7 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod this.dataController.setDataSource(dataSource); dataSource.load(); - assert.ok(this.dataController._dataSource.group()); + assert.ok(this.dataSourceController.getAdapter().group()); assert.equal(this.dataController.items()[0].rowType, 'group'); }); @@ -2080,8 +2079,8 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod }); // assert - assert.deepEqual(this.dataController._dataSource.sort(), [{ selector: 'age', desc: false }], 'sort from user state is applied by dataField'); - assert.deepEqual(this.dataController._dataSource.group(), null, 'group from user state not applied because dataSource not has name column'); + assert.deepEqual(this.dataSourceController.getAdapter().sort(), [{ selector: 'age', desc: false }], 'sort from user state is applied by dataField'); + assert.deepEqual(this.dataSourceController.getAdapter().group(), null, 'group from user state not applied because dataSource not has name column'); assert.deepEqual(this.dataController.items().length, 3); assert.deepEqual(this.dataController.items()[0].values, [20]); assert.deepEqual(this.dataController.items()[1].values, [25]); @@ -2099,12 +2098,12 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod this.dataController.setDataSource(dataSource); dataSource.load(); - assert.ok(this.dataController.dataSource().group()); + assert.ok(this.dataSourceController.getAdapter().group()); // act this.columnsController.moveColumn(0, 0, 'group', 'header'); // assert - assert.ok(!this.dataController.dataSource().group()); + assert.ok(!this.dataSourceController.getAdapter().group()); assert.equal(this.dataController.items()[0].rowType, 'data'); }); @@ -2710,7 +2709,7 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod this.dataController.resetDataSource(); // assert - assert.deepEqual(this.dataController.dataSource().remoteOperations(), {}, 'remote operations set correct'); + assert.deepEqual(this.dataSourceController.getAdapter().remoteOperations(), {}, 'remote operations set correct'); }); QUnit.test('Enable all remote operations if groupPaging is true', function(assert) { @@ -2728,7 +2727,7 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod this.dataController.resetDataSource(); // assert - assert.deepEqual(this.dataController.dataSource().remoteOperations(), { + assert.deepEqual(this.dataSourceController.getAdapter().remoteOperations(), { filtering: true, sorting: true, paging: true, @@ -2755,7 +2754,7 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod this.dataController.resetDataSource(); // assert - assert.deepEqual(this.dataController.dataSource().remoteOperations(), { + assert.deepEqual(this.dataSourceController.getAdapter().remoteOperations(), { filtering: true, sorting: false, paging: true, @@ -2778,7 +2777,7 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod this.dataController.resetDataSource(); // assert - assert.deepEqual(this.dataController.dataSource().remoteOperations(), {}, 'remote operations set correct'); + assert.deepEqual(this.dataSourceController.getAdapter().remoteOperations(), {}, 'remote operations set correct'); }); QUnit.test('Set remoteOperations option to true', function(assert) { @@ -2795,7 +2794,7 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod this.dataController.resetDataSource(); // assert - assert.deepEqual(this.dataController.dataSource().remoteOperations(), { filtering: true, sorting: true, paging: true, grouping: true, summary: true }, 'remote operations set correct'); + assert.deepEqual(this.dataSourceController.getAdapter().remoteOperations(), { filtering: true, sorting: true, paging: true, grouping: true, summary: true }, 'remote operations set correct'); }); // T541798 @@ -3539,7 +3538,7 @@ QUnit.module('Virtual scrolling', { beforeEach: setupVirtualScrollingModule, aft QUnit.test('virtual items on end when last page size less than half page size', function(assert) { const dataController = this.dataController; - dataController.store().insert({ id: 1001, value: 'value1001' }); + this.dataSourceController.store().insert({ id: 1001, value: 'value1001' }); dataController.refresh(); dataController.setViewportItemIndex(999); @@ -3560,10 +3559,10 @@ QUnit.module('Virtual scrolling', { beforeEach: setupVirtualScrollingModule, aft // T866890 QUnit.test('virtual items on end when last page size less than viewport size', function(assert) { const dataController = this.dataController; - dataController.store().insert({ id: 1001 }); - dataController.store().insert({ id: 1002 }); - dataController.store().insert({ id: 1003 }); - dataController.store().insert({ id: 1004 }); + this.dataSourceController.store().insert({ id: 1001 }); + this.dataSourceController.store().insert({ id: 1002 }); + this.dataSourceController.store().insert({ id: 1003 }); + this.dataSourceController.store().insert({ id: 1004 }); dataController.refresh(); dataController.viewportSize(3); @@ -3579,7 +3578,7 @@ QUnit.module('Virtual scrolling', { beforeEach: setupVirtualScrollingModule, aft // B233350 QUnit.test('virtual items on end when visibleRowsCount < pageSize and last page size greater than half page size', function(assert) { const dataController = this.dataController; - dataController._dataSource.store().remove(999); + this.dataSourceController.store().remove(999); dataController.refresh(); dataController.setViewportItemIndex(998); assert.strictEqual(this.dataController.pageIndex(), 49); @@ -3616,7 +3615,7 @@ QUnit.module('Virtual scrolling', { beforeEach: setupVirtualScrollingModule, aft const that = this; let countCallChanged = 0; let countCallDataSourceChanged = 0; - const dataSource = that.dataController.dataSource(); + const dataSource = that.dataSourceController.getAdapter(); that.dataController.changed.add(function() { that.columnOption('id', 'sortOrder', 'desc'); @@ -3642,7 +3641,7 @@ QUnit.module('Virtual scrolling', { beforeEach: setupVirtualScrollingModule, aft const firstRow = rows[0]; // act - this.dataController.dataSource().changed.fire({ changeType: 'pageIndex' }); + this.dataSourceController.getAdapter().changed.fire({ changeType: 'pageIndex' }); // assert assert.strictEqual(this.getVisibleRows(), rows, 'rows are not changed'); @@ -3683,7 +3682,7 @@ const setupVirtualRenderingModule = function() { this.dataController.viewportItemSize(10); this.dataController.viewportSize(10); - this.dataController._dataSource._renderTime = 50; + this.dataSourceController.getAdapter()._renderTime = 50; this.clock.tick(10); @@ -4965,19 +4964,19 @@ QUnit.module('Virtual scrolling (ScrollingDataSource)', { this.dataController.viewportSize(15); // assert - assert.strictEqual(this.dataController.dataSource().loadPageCount(), 1, 'initial load page count'); + assert.strictEqual(this.dataSourceController.getAdapter().loadPageCount(), 1, 'initial load page count'); assert.strictEqual(this.dataController.items().length, 10, 'initial visible items count'); // act this.dataController.setViewportPosition(500); this.clock.tick(10); const visibleItems = this.dataController.items(); - const loadedItems = this.dataController.dataSource().items(); + const loadedItems = this.dataSourceController.getAdapter().items(); // assert assert.deepEqual(this.dataController.getLoadPageParams(), { pageIndex: 2, loadPageCount: 4, skipForCurrentPage: 5 }, 'load page params after scrolling'); assert.deepEqual(this.dataController.pageIndex(), 2, 'page index after scrolling'); - assert.strictEqual(this.dataController.dataSource().loadPageCount(), 4, 'load page count after scrolling'); + assert.strictEqual(this.dataSourceController.getAdapter().loadPageCount(), 4, 'load page count after scrolling'); assert.equal(loadedItems.length, 40, 'loaded items count'); assert.deepEqual(loadedItems[0], { id: 21, name: 'Name 21' }, 'first loaded item'); assert.deepEqual(loadedItems[39], { id: 60, name: 'Name 60' }, 'last loaded item'); @@ -5150,7 +5149,7 @@ QUnit.module('Virtual scrolling (ScrollingDataSource)', { pageSize: 10 }); - const viewportSizeSpy = sinon.spy(this.dataController.dataSource(), 'viewportSize'); + const viewportSizeSpy = sinon.spy(this.dataSourceController.getAdapter(), 'viewportSize'); try { this.dataController.viewportSize(); @@ -5177,7 +5176,7 @@ QUnit.module('Virtual scrolling (ScrollingDataSource)', { pageSize: 10 }); - const viewportItemSizeSpy = sinon.spy(this.dataController.dataSource(), 'viewportItemSize'); + const viewportItemSizeSpy = sinon.spy(this.dataSourceController.getAdapter(), 'viewportItemSize'); try { this.dataController.viewportItemSize(); @@ -5204,7 +5203,7 @@ QUnit.module('Virtual scrolling (ScrollingDataSource)', { pageSize: 10 }); - const setContentItemSizesSpy = sinon.spy(this.dataController.dataSource(), 'setContentItemSizes'); + const setContentItemSizesSpy = sinon.spy(this.dataSourceController.getAdapter(), 'setContentItemSizes'); try { this.dataController.setContentItemSizes([30]); @@ -5452,7 +5451,7 @@ QUnit.module('Virtual scrolling preload', { }, () => { QUnit.test('New mode. One page should be loaded on start', function(assert) { // arrange - assert.strictEqual(this.dataController.dataSource().loadPageCount(), 1, 'initial load page count'); + assert.strictEqual(this.dataSourceController.getAdapter().loadPageCount(), 1, 'initial load page count'); assert.strictEqual(this.getVisibleRows().length, 20, 'initial visible items count'); }); @@ -5460,7 +5459,7 @@ QUnit.module('Virtual scrolling preload', { // act this.dataController.setViewportPosition(1); const visibleRows = this.getVisibleRows(); - const dataSourceAdapter = this.dataController.dataSource(); + const dataSourceAdapter = this.dataSourceController.getAdapter(); const loadedItems = dataSourceAdapter.items(); // assert @@ -5477,7 +5476,7 @@ QUnit.module('Virtual scrolling preload', { // act this.dataController.setViewportPosition(1); const visibleRows = this.getVisibleRows(); - const dataSourceAdapter = this.dataController.dataSource(); + const dataSourceAdapter = this.dataSourceController.getAdapter(); const loadedItems = dataSourceAdapter.items(); // assert @@ -5494,7 +5493,7 @@ QUnit.module('Virtual scrolling preload', { // act this.dataController.setViewportPosition(1); const visibleRows = this.getVisibleRows(); - const dataSourceAdapter = this.dataController.dataSource(); + const dataSourceAdapter = this.dataSourceController.getAdapter(); const loadedItems = dataSourceAdapter.items(); // assert @@ -5511,7 +5510,7 @@ QUnit.module('Virtual scrolling preload', { // act this.dataController.setViewportPosition(1); const visibleRows = this.getVisibleRows(); - const dataSourceAdapter = this.dataController.dataSource(); + const dataSourceAdapter = this.dataSourceController.getAdapter(); const loadedItems = dataSourceAdapter.items(); // assert @@ -5527,7 +5526,7 @@ QUnit.module('Virtual scrolling preload', { // act this.dataController.setViewportPosition(1000); const visibleRows = this.getVisibleRows(); - const dataSourceAdapter = this.dataController.dataSource(); + const dataSourceAdapter = this.dataSourceController.getAdapter(); const loadedItems = dataSourceAdapter.items(); // assert @@ -5544,7 +5543,7 @@ QUnit.module('Virtual scrolling preload', { this.dataController.setViewportPosition(1000); this.dataController.setViewportPosition(999); const visibleRows = this.getVisibleRows(); - const dataSourceAdapter = this.dataController.dataSource(); + const dataSourceAdapter = this.dataSourceController.getAdapter(); const loadedItems = dataSourceAdapter.items(); // assert @@ -5562,7 +5561,7 @@ QUnit.module('Virtual scrolling preload', { this.dataController.setViewportPosition(1000); this.dataController.setViewportPosition(999); const visibleRows = this.getVisibleRows(); - const dataSourceAdapter = this.dataController.dataSource(); + const dataSourceAdapter = this.dataSourceController.getAdapter(); const loadedItems = dataSourceAdapter.items(); // assert @@ -5700,7 +5699,7 @@ QUnit.module('Infinite scrolling', { // T193217 QUnit.test('setViewportItemIndex to end current page several times load next page one time', function(assert) { let loadingCount = 0; - this.dataController._dataSource.customizeStoreLoadOptions.add(function() { + this.dataSourceController.getAdapter().customizeStoreLoadOptions.add(function() { loadingCount++; }); // act @@ -6068,19 +6067,19 @@ QUnit.module('Infinite scrolling (ScrollingDataSource)', { this.dataController.viewportSize(15); // assert - assert.strictEqual(this.dataController.dataSource().loadPageCount(), 1, 'initial load page count'); + assert.strictEqual(this.dataSourceController.getAdapter().loadPageCount(), 1, 'initial load page count'); assert.strictEqual(this.dataController.items().length, 10, 'initial visible items count'); // act this.dataController.setViewportPosition(500); this.clock.tick(10); const visibleItems = this.dataController.items(); - const loadedItems = this.dataController.dataSource().items(); + const loadedItems = this.dataSourceController.getAdapter().items(); // assert assert.deepEqual(this.dataController.getLoadPageParams(), { pageIndex: 2, loadPageCount: 4, skipForCurrentPage: 5 }, 'load page params after scrolling'); assert.deepEqual(this.dataController.pageIndex(), 2, 'page index after scrolling'); - assert.strictEqual(this.dataController.dataSource().loadPageCount(), 4, 'load page count after scrolling'); + assert.strictEqual(this.dataSourceController.getAdapter().loadPageCount(), 4, 'load page count after scrolling'); assert.equal(loadedItems.length, 40, 'loaded items count'); assert.deepEqual(loadedItems[0], { id: 21, name: 'Name 21' }, 'first loaded item'); assert.deepEqual(loadedItems[39], { id: 60, name: 'Name 60' }, 'last loaded item'); @@ -7746,7 +7745,6 @@ QUnit.module('Filtering', { remoteOperations: { filtering: true } }); - this.dataController.isSharedDataSource = true; this.dataController.setDataSource(this.dataSource); let loadingCount = 0; @@ -7846,7 +7844,7 @@ QUnit.module('Filtering', { columns: [{ dataField: 'name', selectedFilterOperation: 'contains', filterValue: 'Al' }, 'age'] }); - this.dataController._dataSource.changed.add(function() { + this.dataSourceController.getAdapter().changed.add(function() { dataSourceChanged = true; }); @@ -7868,7 +7866,7 @@ QUnit.module('Filtering', { columns: [{ dataField: 'name', selectedFilterOperation: 'contains', filterValue: 'Al' }, 'age'] }); - this.dataController._dataSource.changed.add(function() { + this.dataSourceController.getAdapter().changed.add(function() { dataSourceChanged = true; }); @@ -7890,7 +7888,7 @@ QUnit.module('Filtering', { columns: [{ dataField: 'name', selectedFilterOperation: 'contains' }, 'age'] }); - this.dataController._dataSource.changed.add(function() { + this.dataSourceController.getAdapter().changed.add(function() { dataSourceChanged = true; }); @@ -8097,7 +8095,7 @@ QUnit.module('Filtering', { this.dataSource = undefined; // assert - assert.notOk(this.dataController.getDataSource(), 'no dataSource'); + assert.notOk(this.dataSourceController.getDataSource(), 'no dataSource'); // act this.dataController.clearFilter(); @@ -12981,7 +12979,7 @@ QUnit.module('Partial update', { QUnit.test('update one row', function(assert) { this.setupModules(); - const dataSourceItems = this.dataController.dataSource().items(); + const dataSourceItems = this.dataSourceController.getAdapter().items(); const oldItems = this.dataController.items().slice(0); let changedArgs; @@ -13013,7 +13011,7 @@ QUnit.module('Partial update', { QUnit.test('update several rows', function(assert) { this.setupModules(); - const dataSourceItems = this.dataController.dataSource().items(); + const dataSourceItems = this.dataSourceController.getAdapter().items(); const oldItems = this.dataController.items().slice(0); let changedArgs; @@ -13050,7 +13048,7 @@ QUnit.module('Partial update', { QUnit.test('update with incorrect rowIndices', function(assert) { this.setupModules(); - const dataSourceItems = this.dataController.dataSource().items(); + const dataSourceItems = this.dataSourceController.getAdapter().items(); const oldItems = this.dataController.items().slice(0); let changedArgs; @@ -13085,7 +13083,7 @@ QUnit.module('Partial update', { // T266696 QUnit.test('insert item with editing of the first item', function(assert) { this.setupModules(); - const dataSourceItems = this.dataController.dataSource().items(); + const dataSourceItems = this.dataSourceController.getAdapter().items(); let changedArgs; this.dataController.changed.add(function(args) { @@ -13113,7 +13111,7 @@ QUnit.module('Partial update', { QUnit.test('insert one item', function(assert) { this.setupModules(); - const dataSourceItems = this.dataController.dataSource().items(); + const dataSourceItems = this.dataSourceController.getAdapter().items(); const oldItems = this.dataController.items().slice(0); let changedArgs; @@ -13150,7 +13148,7 @@ QUnit.module('Partial update', { QUnit.test('update and insert', function(assert) { this.setupModules(); - const dataSourceItems = this.dataController.dataSource().items(); + const dataSourceItems = this.dataSourceController.getAdapter().items(); const oldItems = this.dataController.items().slice(0); let changedArgs; @@ -13220,7 +13218,7 @@ QUnit.module('Partial update', { QUnit.test('remove one item', function(assert) { this.setupModules(); - const dataSourceItems = this.dataController.dataSource().items(); + const dataSourceItems = this.dataSourceController.getAdapter().items(); const oldItems = this.dataController.items().slice(0); let changedArgs; @@ -13256,7 +13254,7 @@ QUnit.module('Partial update', { QUnit.test('update and remove', function(assert) { this.setupModules(); - const dataSourceItems = this.dataController.dataSource().items(); + const dataSourceItems = this.dataSourceController.getAdapter().items(); const oldItems = this.dataController.items().slice(0); let changedArgs; @@ -13293,7 +13291,7 @@ QUnit.module('Partial update', { // T186415 QUnit.test('remove and update next row', function(assert) { this.setupModules(); - const dataSourceItems = this.dataController.dataSource().items(); + const dataSourceItems = this.dataSourceController.getAdapter().items(); const oldItems = this.dataController.items().slice(0); let changedArgs; @@ -13329,7 +13327,7 @@ QUnit.module('Partial update', { QUnit.test('insert and remove', function(assert) { this.setupModules(); - const dataSourceItems = this.dataController.dataSource().items(); + const dataSourceItems = this.dataSourceController.getAdapter().items(); const oldItems = this.dataController.items().slice(0); let changedArgs; @@ -14412,7 +14410,7 @@ QUnit.module('Using DataSource instance', { this.clock.tick(10); assert.equal(this.columnsController.getGroupColumns().length, 1, 'grouped columns count'); - assert.deepEqual(this.dataController._dataSource.group(), [{ selector: 'field1', desc: false, isExpanded: true }], 'dataSource group when autoExpandAll true'); + assert.deepEqual(this.dataSourceController.getAdapter().group(), [{ selector: 'field1', desc: false, isExpanded: true }], 'dataSource group when autoExpandAll true'); // act this.option('grouping.autoExpandAll', false); @@ -14421,7 +14419,7 @@ QUnit.module('Using DataSource instance', { // assert assert.equal(this.columnsController.getGroupColumns().length, 1, 'grouped columns count'); - assert.deepEqual(this.dataController._dataSource.group(), [{ selector: 'field1', desc: false, isExpanded: false }], 'dataSource group when autoExpandAll false'); + assert.deepEqual(this.dataSourceController.getAdapter().group(), [{ selector: 'field1', desc: false, isExpanded: false }], 'dataSource group when autoExpandAll false'); assert.equal(this.dataController.totalItemsCount(), 2); assert.equal(this.dataController.items().length, 2); }); @@ -14459,7 +14457,7 @@ QUnit.module('Using DataSource instance', { try { // act this.dataSource.dispose(); - this.dataController.dataSource().dispose(true); + this.dataSourceController.getAdapter().dispose(true); // assert assert.ok(true, 'No exception'); @@ -15439,7 +15437,7 @@ QUnit.module('onOptionChanged', { that.option.restore(); sinon.stub(that, 'option').callsFake(function(optionName, value) { if(optionName === 'paging.pageSize' && value === 3) { - pageSize = that.dataController.dataSource().pageSize(); + pageSize = that.dataSourceController.getAdapter().pageSize(); } if(optionName === 'editing.changes') { return that.options.editing.changes; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js index 49bc940d87f1..074b5e6b1d11 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js @@ -1577,7 +1577,7 @@ QUnit.module('Assign options', baseModuleConfig, () => { // assert assert.ok(dataGrid.getController('data').isEmpty(), 'no data'); - assert.ok(!dataGrid.getController('data').dataSource(), 'no dataSource'); + assert.ok(!dataGrid.getController('dataSource').getAdapter(), 'no dataSource'); assert.strictEqual(dataGrid.getController('data')._cachedProcessedItems, null, 'cached processed items are cleared'); // T1045202 assert.strictEqual(dataGrid.getController('columns')._dataSource, null, 'no dataSource inside columnsController'); // T1045202 assert.equal(dataGrid.getController('data').items().length, 0, 'items count'); @@ -2101,7 +2101,7 @@ QUnit.module('Assign options', baseModuleConfig, () => { }); // assert - const dataSource = dataGrid.getController('data').dataSource(); + const dataSource = dataGrid.getController('dataSource').getAdapter(); assert.ok(!errorMessage, 'No error messages'); assert.ok(dataSource, 'dataSource assigned'); assert.ok(dataSource.requireTotalCount(), 'requireTotalCount assigned'); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataSource.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataSource.tests.js index d83fa3a157c7..eccc30888fb8 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataSource.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataSource.tests.js @@ -31,7 +31,7 @@ const createDataSource = function(options) { setupDataGridModules(dataGridStub, ['data', 'columns']); - const dataSourceAdapter = dataGridStub.dataController._createDataSourceAdapter(dataSource); + const dataSourceAdapter = dataGridStub.dataSourceController.createAdapter(dataSource); const origItems = dataSourceAdapter.items; const processItems = function(items) { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/editing.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/editing.tests.js index bd01b78bb8c3..aece23220c57 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/editing.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/editing.tests.js @@ -3638,7 +3638,7 @@ QUnit.module('Editing with real dataController', { // arrange const that = this; const rowsView = this.rowsView; - const originalInsert = that.dataController.store().insert; + const originalInsert = that.dataSourceController.store().insert; const testElement = $('#container'); $.extend(that.options.editing, { @@ -3649,7 +3649,7 @@ QUnit.module('Editing with real dataController', { } }); - that.dataController.store().insert = function(values) { + that.dataSourceController.store().insert = function(values) { // assert assert.deepEqual(values, { Test1: 'test1', @@ -3691,7 +3691,7 @@ QUnit.module('Editing with real dataController', { assert.equal(testElement.find('input').length, 0, 'not has inputs'); // arrange - that.dataController.store().insert = originalInsert; + that.dataSourceController.store().insert = originalInsert; that.option('onRowInserting', function(params) { params.cancel = true; }); @@ -4163,7 +4163,7 @@ QUnit.module('Editing with real dataController', { // arrange const that = this; const rowsView = that.rowsView; - const originalUpdate = that.dataController.store().update; + const originalUpdate = that.dataSourceController.store().update; const testElement = $('#container'); $.extend(that.options.editing, { @@ -4174,7 +4174,7 @@ QUnit.module('Editing with real dataController', { } }); - that.dataController.store().update = function(key, values) { + that.dataSourceController.store().update = function(key, values) { // assert assert.deepEqual(values, { age: 15, @@ -4247,7 +4247,7 @@ QUnit.module('Editing with real dataController', { assert.equal(testElement.find('.dx-cell-modified').length, 0, 'no element with class name dx-cell-modified'); // arrange - that.dataController.store().update = originalUpdate; + that.dataSourceController.store().update = originalUpdate; that.option('onRowUpdating', function(params) { params.cancel = true; }); @@ -4295,7 +4295,7 @@ QUnit.module('Editing with real dataController', { } }); - sinon.spy(that.dataController.store(), 'update'); + sinon.spy(that.dataSourceController.store(), 'update'); const cancelDeferred = $.Deferred(); @@ -4326,14 +4326,14 @@ QUnit.module('Editing with real dataController', { that.saveEditData(); // assert - assert.ok(!that.dataController.store().update.called, 'update is not called'); + assert.ok(!that.dataSourceController.store().update.called, 'update is not called'); // act cancelDeferred.resolve(); // assert - assert.equal(that.dataController.store().update.callCount, 1, 'update called one time'); - assert.deepEqual(that.dataController.store().update.lastCall.args, [that.array[0], { name: 'Test1', room: 666 }], 'update args'); + assert.equal(that.dataSourceController.store().update.callCount, 1, 'update called one time'); + assert.deepEqual(that.dataSourceController.store().update.lastCall.args, [that.array[0], { name: 'Test1', room: 666 }], 'update args'); }); QUnit.test('Update cell when edit mode batch and cancel in onRowUpdating is Promise and resolved', function(assert) { @@ -4350,7 +4350,7 @@ QUnit.module('Editing with real dataController', { } }); - sinon.spy(that.dataController.store(), 'update'); + sinon.spy(that.dataSourceController.store(), 'update'); let resolve; const cancelPromise = new Promise(function(onResolve) { @@ -4384,10 +4384,10 @@ QUnit.module('Editing with real dataController', { that.saveEditData(); // assert - assert.ok(!that.dataController.store().update.called, 'update is not called'); + assert.ok(!that.dataSourceController.store().update.called, 'update is not called'); cancelPromise.then(function() { - assert.equal(that.dataController.store().update.callCount, 1, 'update called one time'); - assert.deepEqual(that.dataController.store().update.lastCall.args, [that.array[0], { name: 'Test1', room: 666 }], 'update args'); + assert.equal(that.dataSourceController.store().update.callCount, 1, 'update called one time'); + assert.deepEqual(that.dataSourceController.store().update.lastCall.args, [that.array[0], { name: 'Test1', room: 666 }], 'update args'); }); // act @@ -4410,7 +4410,7 @@ QUnit.module('Editing with real dataController', { } }); - sinon.spy(that.dataController.store(), 'update'); + sinon.spy(that.dataSourceController.store(), 'update'); const dataErrors = []; @@ -4446,13 +4446,13 @@ QUnit.module('Editing with real dataController', { that.saveEditData(); // assert - assert.ok(!that.dataController.store().update.called, 'update is not called'); + assert.ok(!that.dataSourceController.store().update.called, 'update is not called'); // act cancelDeferred.resolve(true); // assert - assert.ok(!that.dataController.store().update.called, 'update is not called'); + assert.ok(!that.dataSourceController.store().update.called, 'update is not called'); assert.ok(!dataErrors.length, 'no data errors'); }); @@ -4470,7 +4470,7 @@ QUnit.module('Editing with real dataController', { } }); - sinon.spy(that.dataController.store(), 'update'); + sinon.spy(that.dataSourceController.store(), 'update'); const cancelDeferred = $.Deferred(); @@ -4507,7 +4507,7 @@ QUnit.module('Editing with real dataController', { cancelDeferred.reject('Test Error Message'); // assert - assert.ok(!that.dataController.store().update.called, 'update is not called'); + assert.ok(!that.dataSourceController.store().update.called, 'update is not called'); assert.deepEqual(dataErrors.length, 1, 'data errors count'); assert.ok(dataErrors[0] instanceof Error, 'error has Error type'); @@ -5827,10 +5827,10 @@ QUnit.module('Editing with real dataController', { return testElement; }; - const oldUpdate = that.dataController.store().update; + const oldUpdate = that.dataSourceController.store().update; const updateDeferred = $.Deferred(); - that.dataController.store().update = function() { + that.dataSourceController.store().update = function() { updateCallCount++; oldUpdate.apply(this, arguments); return updateDeferred; @@ -12850,7 +12850,7 @@ QUnit.module('Editing with validation', { }, 'lastName'] }); - this.dataController.store().update = function() { + this.dataSourceController.store().update = function() { return $.Deferred().reject('Test Error'); }; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/filterSync.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/filterSync.tests.js index 8e41eed858c5..c79faaef52bc 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/filterSync.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/filterSync.tests.js @@ -289,7 +289,7 @@ QUnit.module('Sync with FilterValue', { this.dataController.clearFilter(); assert.equal(this.option('filterValue'), null); - assert.equal(this.dataController.getDataSource().filter(), null); + assert.equal(this.dataSourceController.getDataSource().filter(), null); }); // T659816 @@ -306,7 +306,7 @@ QUnit.module('Sync with FilterValue', { this.dataController.clearFilter('filterValue'); assert.equal(this.option('filterValue'), null); - assert.deepEqual(this.dataController.getDataSource().filter(), dataSourceFilter); + assert.deepEqual(this.dataSourceController.getDataSource().filter(), dataSourceFilter); }); // T639390 diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/focus.integration.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/focus.integration.tests.js index 1ff1c6b8d421..ac80de2240c1 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/focus.integration.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/focus.integration.tests.js @@ -1286,7 +1286,7 @@ QUnit.module('Initialization', baseModuleConfig, () => { }); const rowsView = dataGrid.getView('rowsView'); const $lastRow = rowsView.getRow(2); - const dataSource = dataGrid.getController('data').dataSource(); + const dataSource = dataGrid.getController('dataSource').getAdapter(); sinon.spy(dataSource, 'load'); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/focus.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/focus.tests.js index 4b93ae71c30e..e49b3f3ae333 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/focus.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/focus.tests.js @@ -4571,8 +4571,7 @@ QUnit.module('Focused row', getModuleConfig(true), () => { this.setupModule(); - this.getController('data').store = function() { - }; + this.getController('dataSource').disposeAdapter(); this.getController('data').dataErrorOccurred.add(function(e) { dataErrors.push(e); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerFilter.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerFilter.tests.js index 9924268baf7d..3c193e4058ed 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerFilter.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerFilter.tests.js @@ -5,7 +5,6 @@ import gridCoreUtils from '__internal/grids/grid_core/m_utils'; import $ from 'jquery'; import ArrayStore from 'common/data/array_store'; -import { noop } from 'core/utils/common'; import ODataStore from 'common/data/odata/store'; import devices from '__internal/core/m_devices'; import { DataSource } from 'common/data/data_source/data_source'; @@ -536,10 +535,10 @@ QUnit.module('Header Filter', { const that = this; const testElement = $('#container'); + that.generateItems(5); that.setupDataGrid(); - that.dataController.dataSource = noop; - + that.dataSourceController.disposeAdapter(); that.columnHeadersView.render(testElement); that.headerFilterView.render(testElement); @@ -4085,7 +4084,7 @@ QUnit.module('Header Filter with real columnsController', { that.setupDataGrid(); const column = that.columnsController.getVisibleColumns()[0]; - that.dataController.store().on('loading', function(options) { + that.dataSourceController.store().on('loading', function(options) { loadOptions = options; }); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/keyboardNavigation.accessibility.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/keyboardNavigation.accessibility.tests.js index 0ec9ea62580c..bd25a02825a4 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/keyboardNavigation.accessibility.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/keyboardNavigation.accessibility.tests.js @@ -373,14 +373,14 @@ QUnit.module('Keyboard navigation accessibility', { headersWrapper.getHeaderItem(0, 0).focus(); // assert - assert.notOk(this.getController('data').getDataSource().sort(), 'Sorting'); + assert.notOk(this.getController('dataSource').getDataSource().sort(), 'Sorting'); // act fireKeyDown(headersWrapper.getHeaderItem(0, 0), 'Enter'); this.clock.tick(10); // assert - assert.deepEqual(this.getController('data').getDataSource().sort(), [{ selector: 'name', desc: false }], 'Sorting'); + assert.deepEqual(this.getController('dataSource').getDataSource().sort(), [{ selector: 'name', desc: false }], 'Sorting'); assert.equal(keyDownFiresCount, 1, 'keyDownFiresCount'); // act @@ -388,7 +388,7 @@ QUnit.module('Keyboard navigation accessibility', { this.clock.tick(10); // assert - assert.deepEqual(this.getController('data').getDataSource().sort(), [{ selector: 'name', desc: true }], 'Sorting'); + assert.deepEqual(this.getController('dataSource').getDataSource().sort(), [{ selector: 'name', desc: true }], 'Sorting'); assert.equal(keyDownFiresCount, 2, 'keyDownFiresCount'); }); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/keyboardNavigation.keyboardKeys.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/keyboardNavigation.keyboardKeys.tests.js index 791e37b4728b..e0a87d92c437 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/keyboardNavigation.keyboardKeys.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/keyboardNavigation.keyboardKeys.tests.js @@ -1810,14 +1810,12 @@ QUnit.module('Keyboard keys', { return $container; }; - this.dataController.store = function() { - return { - key: function() { }, - update: function(key, values) { - isStoreUpdated = true; - return $.Deferred().resolve(key, values); - } - }; + this.dataControllerOptions.store = { + key: function() { }, + update: function(key, values) { + isStoreUpdated = true; + return $.Deferred().resolve(key, values); + } }; // act diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/rowsView.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/rowsView.tests.js index 8273c90ba416..232f18a9723c 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/rowsView.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/rowsView.tests.js @@ -19,7 +19,6 @@ import { setupDataGridModules, MockDataController, MockColumnsController, MockSe import { findShadowHostOrDocument } from '../../helpers/dataGridHelper.js'; import numberLocalization from 'common/core/localization/number'; import virtualScrollingCore from '__internal/grids/grid_core/virtual_scrolling/m_virtual_scrolling_core'; -import ODataStore from 'common/data/odata/store'; import ArrayStore from 'common/data/array_store'; const expandCellTemplate = gridCoreUtils.getExpandCellTemplate(); @@ -941,14 +940,18 @@ QUnit.module('Rows view', { { data: { name: 'aaaüaaa' }, values: ['aaaüaaa'] }, { data: { name: 'aaaaaaü' }, values: ['aaaaaaü'] }, { data: { name: 'üaaaaaa' }, values: ['üaaaaaa'] }]; - const dataController = new MockDataController({ items: rows }); - dataController.getDataSource = () => ({ loadOptions: () => ({ - langParams: { - collatorOptions: { - sensitivity: 'base' - }, + const dataController = new MockDataController({ + items: rows, + dataSource: { + loadOptions: () => ({ + langParams: { + collatorOptions: { + sensitivity: 'base' + }, + } + }) } - }) }); + }); const rowsView = this.createRowsView(this.items, dataController, columns); const testElement = $('#container'); @@ -980,11 +983,6 @@ QUnit.module('Rows view', { const dataController = new MockDataController({ items: rows }); const rowsView = this.createRowsView(this.items, dataController, columns); const testElement = $('#container'); - const store = new ODataStore({ url: 'test.org' }); - - dataController.store = function() { - return store; - }; // act this.options.searchPanel = { @@ -1014,11 +1012,6 @@ QUnit.module('Rows view', { const dataController = new MockDataController({ items: rows }); const rowsView = this.createRowsView(this.items, dataController, columns); const testElement = $('#container'); - const store = new ODataStore({ url: 'test.org' }); - - dataController.store = function() { - return store; - }; // act this.options.searchPanel = { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/selection.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/selection.tests.js index ad017bce1050..104f13b7c870 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/selection.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/selection.tests.js @@ -490,7 +490,7 @@ QUnit.module('Selection', { beforeEach: setupSelectionModule, afterEach: teardow // T135244 QUnit.test('set selectedRows. Not Loading data then no selected rows', function(assert) { let loadingCount = 0; - this.dataController.store().on('loading', function() { + this.dataSourceController.store().on('loading', function() { loadingCount++; }); @@ -1002,7 +1002,7 @@ QUnit.module('Selection', { beforeEach: setupSelectionModule, afterEach: teardow this.dataController.optionChanged({ name: 'dataSource' }); this.clock.tick(10); - this.dataController.store().on('loading', function(options) { + this.dataSourceController.store().on('loading', function(options) { loadOptions = options; }); @@ -1344,7 +1344,7 @@ QUnit.module('Selection', { beforeEach: setupSelectionModule, afterEach: teardow this.dataController.resetDataSource(); let loadingCount = 0; - this.dataController.store().on('loading', function() { + this.dataSourceController.store().on('loading', function() { loadingCount++; }); @@ -2120,7 +2120,7 @@ QUnit.module('Multiple selection. DataSource with key', { beforeEach: setupSelec allowSelectAll: true } }); - const store = this.dataController.dataSource().store(); + const store = this.dataSourceController.store(); this.selectionController.selectRows([2]); @@ -2146,7 +2146,7 @@ QUnit.module('Multiple selection. DataSource with key', { beforeEach: setupSelec allowSelectAll: true }, }); - const store = this.dataController.dataSource().store(); + const store = this.dataSourceController.store(); // act this.selectionController.selectRows([1]); @@ -2177,7 +2177,7 @@ QUnit.module('Multiple selection. DataSource with key', { beforeEach: setupSelec allowSelectAll: true } }); - const store = this.dataController.dataSource().store(); + const store = this.dataSourceController.store(); // act this.selectionController.selectAll(); @@ -2206,7 +2206,7 @@ QUnit.module('Multiple selection. DataSource with key', { beforeEach: setupSelec mode: 'multiple', } }); - const store = this.dataController.dataSource().store(); + const store = this.dataSourceController.store(); sinon.spy(this.selectionController, '_updateSelectedOnPush'); sinon.spy(this.selectionController, 'getSelectedRowsData'); @@ -2227,7 +2227,7 @@ QUnit.module('Multiple selection. DataSource with key', { beforeEach: setupSelec deferred: true } }); - const store = this.dataController.dataSource().store(); + const store = this.dataSourceController.store(); sinon.spy(this.selectionController, '_updateSelectedOnPush'); sinon.spy(this.selectionController, 'getSelectedRowsData'); @@ -2599,7 +2599,7 @@ QUnit.module('Selection SelectAllMode', { // act $.each(this.selectionController.getSelectedRowKeys(), function(index, key) { - that.dataController.store().remove(key); + that.dataSourceController.store().remove(key); }); this.dataController.refresh(); @@ -3192,7 +3192,7 @@ QUnit.module('Selection when grouping', { this.dataController.pageIndex(1); this.selectionController.changeItemSelection(1); - assert.ok(this.dataController._dataSource.items()[0].isContinuation); + assert.ok(this.dataSourceController.getAdapter().items()[0].isContinuation); assert.ok(!this.dataController.items()[0].isSelected); assert.ok(this.dataController.items()[1].isSelected); assert.ok(!this.dataController.items()[2].isSelected); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/virtualScrolling.integration.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/virtualScrolling.integration.tests.js index ff4057a2e1ca..fcf55d210597 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/virtualScrolling.integration.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/virtualScrolling.integration.tests.js @@ -6916,7 +6916,7 @@ QUnit.module('Infinite Scrolling', baseModuleConfig, () => { this.clock.tick(10); // assert assert.ok(dataGrid.getController('data').viewportSize() > 0); - assert.ok(!dataGrid.getController('data').dataSource().requireTotalCount()); + assert.ok(!dataGrid.getController('dataSource').getAdapter().requireTotalCount()); }); QUnit.test('New mode. Load panel should not be displayed at the bottom when all items are loaded', function(assert) { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/lookup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/lookup.tests.js index 1176c8994059..472cd7a1c70d 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/lookup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/lookup.tests.js @@ -3969,6 +3969,62 @@ if(devices.real().deviceType === 'desktop') { }); } +QUnit.module('screen reader announcements (T1334729)', { + beforeEach: function() { + fx.off = true; + this.clock = sinon.useFakeTimers(); + + this.instance = $('#lookup').dxLookup({ + dataSource: [1, 2, 3], + searchTimeout: 0, + 'dropDownOptions.animation': null, + opened: true + }).dxLookup('instance'); + + this.search = (value) => { + this.instance._searchBox.option('value', value); + this.clock.tick(0); + }; + + this.getA11yStatus = () => $(`.${LIST_CLASS}`).children('[role="status"]'); + this.getItemsAnnouncement = (itemsCount) => `${messageLocalization.format('dxList-listAriaLabel')}: ${itemsCount}`; + }, + afterEach: function() { + this.clock.restore(); + fx.off = false; + } +}, () => { + QUnit.test('list should have a single live region', function(assert) { + assert.strictEqual(this.getA11yStatus().length, 1); + }); + + QUnit.test('item count should be announced when the drop-down is opened', function(assert) { + assert.strictEqual(this.getA11yStatus().text(), this.getItemsAnnouncement(3)); + }); + + QUnit.test('noDataText should be announced if search returns no items', function(assert) { + this.search('4'); + + assert.strictEqual(this.getA11yStatus().text(), messageLocalization.format('dxCollectionWidget-noDataText')); + }); + + QUnit.test('item count should be announced if search returns items', function(assert) { + this.search('1'); + + assert.strictEqual(this.getA11yStatus().text(), this.getItemsAnnouncement(1)); + }); + + QUnit.test('live region should not be re-created on search', function(assert) { + const a11yStatusElement = this.getA11yStatus().get(0); + + this.search('4'); + + assert.strictEqual(this.getA11yStatus().get(0), a11yStatusElement, 'the live region is the same element'); + assert.strictEqual(a11yStatusElement.textContent, messageLocalization.format('dxCollectionWidget-noDataText'), + 'the announcement is written into that element, otherwise screen readers do not announce it'); + }); +}); + QUnit.module('default options', { beforeEach: function() { fx.off = true; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/numberBoxParts/common.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/numberBoxParts/common.tests.js index 9e82f2737153..63d935a73456 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/numberBoxParts/common.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/numberBoxParts/common.tests.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import SpinButton from '__internal/ui/number_box/m_number_box.spin'; +import SpinButton from '__internal/ui/number_box/number_box.spin'; import config from 'core/config'; import eventsEngine from 'common/core/events/core/events_engine'; import keyboardMock from '../../../helpers/keyboardMock.js'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/numberBoxParts/mask.caret.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/numberBoxParts/mask.caret.tests.js index 546b5dd3d923..b070db9ed5b1 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/numberBoxParts/mask.caret.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/numberBoxParts/mask.caret.tests.js @@ -4,7 +4,7 @@ import { getCaretInBoundaries, isCaretInBoundaries, getCaretAfterFormat, -} from '__internal/ui/number_box/m_number_box.caret'; +} from '__internal/ui/number_box/number_box.caret'; QUnit.module('format caret', () => { const customFormat = { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.treeList/dataController.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.treeList/dataController.tests.js index ee7a4c5f2ac6..7084f5faf6d2 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.treeList/dataController.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.treeList/dataController.tests.js @@ -72,7 +72,7 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod }); // assert - assert.notOk(this.dataController.dataSource().paginate(), 'paginate is disabled'); + assert.notOk(this.dataSourceController.getAdapter().paginate(), 'paginate is disabled'); }); QUnit.test('Paginate should be enabled when virtual scrolling is enabled', function(assert) { @@ -85,7 +85,7 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod }); // assert - assert.ok(this.dataController.dataSource().paginate(), 'paginate is enabled'); + assert.ok(this.dataSourceController.getAdapter().paginate(), 'paginate is enabled'); }); QUnit.test('Initialize from dataSource with plain structure', function(assert) { @@ -707,7 +707,7 @@ QUnit.module('Initialization', { beforeEach: setupModule, afterEach: teardownMod QUnit.test('There are no exceptions on getting node when hasn\'t datasource', function(assert) { // arrange - this.dataController.setDataSource(undefined); + this.dataController._disposeDataSource(); // act, assert assert.equal(this.getNodeByKey(1), undefined, 'no exceptions'); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.treeList/stateStoring.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.treeList/stateStoring.tests.js index e8889da63876..b22518b54933 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.treeList/stateStoring.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.treeList/stateStoring.tests.js @@ -34,6 +34,9 @@ QUnit.module('State Storing', { keyExpr: 'id', parentIdExpr: 'parentId', loadingTimeout: null, + paging: { + enabled: true + }, scrolling: { mode: 'virtual' } diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/listParts/commonTests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/listParts/commonTests.js index 806250bc64e4..bf34a80301c4 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/listParts/commonTests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/listParts/commonTests.js @@ -19,9 +19,12 @@ import { CustomStore } from 'common/data/custom_store'; import DOMComponent from 'core/dom_component'; import List from 'ui/list'; import { setScrollView } from '__internal/ui/list/list.base'; +import { SCREEN_READER_ONLY_CLASS } from '__internal/core/widget/widget'; + import ScrollView from 'ui/scroll_view'; import eventsEngine from 'common/core/events/core/events_engine'; import ariaAccessibilityTestHelper from '../../../helpers/ariaAccessibilityTestHelper.js'; +import messageLocalization from 'common/core/localization/message'; const LIST_ITEM_CLASS = 'dx-list-item'; const LIST_ITEM_CONTENT_CLASS = 'dx-list-item-content'; @@ -1004,7 +1007,7 @@ QUnit.module('options', moduleSetup, () => { pageLoadMode: 'scrollBottom' }); - assert.equal(element.text(), '12345'); + assert.equal(element.find(`.${LIST_ITEMS_CLASS}`).text(), '12345'); assert.deepEqual(element.dxList('instance').option('items'), [1, 2, 3, 4, 5]); }); @@ -1014,7 +1017,7 @@ QUnit.module('options', moduleSetup, () => { pageLoadMode: 'scrollBottom' }); - assert.equal(element.text(), '12345'); + assert.equal(element.find(`.${LIST_ITEMS_CLASS}`).text(), '12345'); assert.deepEqual(element.dxList('instance').option('items'), [1, 2, 3, 4, 5]); }); @@ -1024,7 +1027,7 @@ QUnit.module('options', moduleSetup, () => { pageLoadMode: 'scrollBottom' }); - assert.equal(element.text(), '12345'); + assert.equal(element.find(`.${LIST_ITEMS_CLASS}`).text(), '12345'); assert.deepEqual(element.dxList('instance').option('items'), [1, 2, 3, 4, 5]); }); @@ -1068,7 +1071,7 @@ QUnit.module('options changed', moduleSetup, () => { pageLoadMode: 'scrollBottom' }); - assert.equal(element.text(), '01354'); + assert.equal(element.find(`.${LIST_ITEMS_CLASS}`).text(), '01354'); assert.deepEqual(element.dxList('instance').option('items'), [0, 1, 3, 5, 4]); element.dxList({ @@ -1077,7 +1080,7 @@ QUnit.module('options changed', moduleSetup, () => { } }); - assert.equal(element.text(), '26897'); + assert.equal(element.find(`.${LIST_ITEMS_CLASS}`).text(), '26897'); assert.deepEqual(element.dxList('instance').option('items'), [2, 6, 8, 9, 7]); }); @@ -1086,13 +1089,13 @@ QUnit.module('options changed', moduleSetup, () => { items: [0, 1, 3, 5, 4] }); - assert.equal(element.text(), '01354'); + assert.equal(element.find(`.${LIST_ITEMS_CLASS}`).text(), '01354'); element.dxList({ items: [2, 6, 8, 9, 7] }); - assert.equal(element.text(), '26897'); + assert.equal(element.find(`.${LIST_ITEMS_CLASS}`).text(), '26897'); }); QUnit.test('scrollingEnabled', function(assert) { @@ -2759,14 +2762,14 @@ QUnit.module('infinite list scenario', moduleSetup, () => { } }); - assert.equal(element.text(), '12', 'correct items generated'); + assert.equal(element.find(`.${LIST_ITEMS_CLASS}`).text(), '12', 'correct items generated'); assert.deepEqual(element.dxList('instance').option('items'), [1, 2], 'correct items presented in options'); element.find(`.${LIST_ITEM_CLASS}`).data('rendered', true); element.dxScrollView('instance').scrollBottom(); - assert.equal(element.text(), '1234', 'correct items generated'); + assert.equal(element.find(`.${LIST_ITEMS_CLASS}`).text(), '1234', 'correct items generated'); assert.deepEqual(element.dxList('instance').option('items'), [1, 2, 3, 4], 'correct items presented in options'); assert.strictEqual(element.find(`.${LIST_ITEM_CLASS}`).eq(0).data('rendered'), true, 'first item is not rerendered'); @@ -2875,7 +2878,7 @@ QUnit.module('infinite list scenario', moduleSetup, () => { element.dxScrollView('instance').scrollBottom(); - assert.equal(element.text(), '12', 'error occurred'); + assert.equal(element.find(`.${LIST_ITEMS_CLASS}`).text(), '12', 'error occurred'); assert.equal(element.dxList('instance')._startIndexForAppendedItems, null, 'flag set correctly'); }); @@ -4952,6 +4955,9 @@ if(devices.real().deviceType === 'desktop') { } QUnit.module('Accessibility', () => { + const getA11yStatus = (instance) => instance.$element().children('[role="status"]'); + const getItemsAnnouncement = (itemsCount) => `${messageLocalization.format('dxList-listAriaLabel')}: ${itemsCount}`; + QUnit.test('SelectAll checkbox should have aria-label="Select All" attribute', function(assert) { $('#list').dxList({ selectionMode: 'all', @@ -5378,4 +5384,104 @@ QUnit.module('Accessibility', () => { assert.strictEqual(instance.$element().find(`.${SCROLLVIEW_CONTENT_CLASS}`).eq(0).attr('role'), 'application'); }); }); + + QUnit.test('list should render a single visually hidden live region (T1334729)', function(assert) { + const instance = $('#list').dxList({ items: ['Item 1'] }).dxList('instance'); + + const $a11yStatus = getA11yStatus(instance); + + assert.strictEqual($a11yStatus.length, 1, 'the list has a single live region'); + assert.ok($a11yStatus.hasClass(SCREEN_READER_ONLY_CLASS), 'the live region is visually hidden'); + }); + + QUnit.test('item count should be announced (T1334729)', function(assert) { + const instance = $('#list').dxList({ items: ['Item 1', 'Item 2'] }).dxList('instance'); + + assert.strictEqual(getA11yStatus(instance).text(), getItemsAnnouncement(2)); + }); + + QUnit.test('item count of a grouped list should not count groups (T1334729)', function(assert) { + const instance = $('#list').dxList({ + grouped: true, + items: [ + { key: 'group 1', items: ['Item 1', 'Item 2'] }, + { key: 'group 2', items: ['Item 3'] }, + ], + }).dxList('instance'); + + assert.strictEqual(getA11yStatus(instance).text(), getItemsAnnouncement(3)); + }); + + QUnit.test('noDataText should be announced when the list is empty (T1334729)', function(assert) { + const instance = $('#list').dxList({ items: [] }).dxList('instance'); + + assert.strictEqual(getA11yStatus(instance).text(), messageLocalization.format('dxCollectionWidget-noDataText')); + }); + + QUnit.test('custom noDataText should be announced and follow its runtime change (T1334729)', function(assert) { + const instance = $('#list').dxList({ items: [], noDataText: 'custom-no-data' }).dxList('instance'); + + assert.strictEqual(getA11yStatus(instance).text(), 'custom-no-data', 'custom noDataText is announced'); + + instance.option('noDataText', 'another-no-data'); + + assert.strictEqual(getA11yStatus(instance).text(), 'another-no-data', 'announcement is updated after runtime change'); + }); + + QUnit.test('nothing should be announced when the list is empty and noDataText is empty (T1334729)', function(assert) { + const instance = $('#list').dxList({ items: [], noDataText: '' }).dxList('instance'); + + assert.strictEqual(getA11yStatus(instance).text(), ''); + }); + + QUnit.test('nothing should be announced while the initial data is loading (T1334729)', function(assert) { + const loadResult = $.Deferred(); + const instance = $('#list').dxList({ + dataSource: new CustomStore({ load: () => loadResult.promise(), loadMode: 'processed' }) + }).dxList('instance'); + + assert.strictEqual(getA11yStatus(instance).text(), '', 'nothing is announced while data is loading'); + + loadResult.resolve(['Item 1', 'Item 2']); + + assert.strictEqual(getA11yStatus(instance).text(), getItemsAnnouncement(2), 'item count is announced once data is loaded'); + }); + + QUnit.test('announcement should be cleared while new data is loading (T1334729)', function(assert) { + const instance = $('#list').dxList({ items: ['Item 1', 'Item 2', 'Item 3'] }).dxList('instance'); + const loadResult = $.Deferred(); + + instance.option('dataSource', new CustomStore({ load: () => loadResult.promise(), loadMode: 'processed' })); + + assert.strictEqual(getA11yStatus(instance).text(), '', + 'neither the outdated item count nor noDataText is announced while data is loading'); + + loadResult.resolve(['Item 1']); + + assert.strictEqual(getA11yStatus(instance).text(), getItemsAnnouncement(1), 'item count is announced once data is loaded'); + }); + + [true, false].forEach(repaintChangesOnly => { + QUnit.test(`noDataText should be announced when dataSource is cleared at runtime and repaintChangesOnly=${repaintChangesOnly} (T1334729)`, function(assert) { + const instance = $('#list').dxList({ dataSource: ['Item 1'], repaintChangesOnly }).dxList('instance'); + const a11yStatusElement = getA11yStatus(instance).get(0); + + instance.option('dataSource', []); + + assert.strictEqual(getA11yStatus(instance).get(0), a11yStatusElement, + 'the live region is not re-created, otherwise screen readers do not announce its text'); + assert.strictEqual(getA11yStatus(instance).text(), messageLocalization.format('dxCollectionWidget-noDataText')); + }); + + QUnit.test(`item count should be announced when dataSource is set at runtime and repaintChangesOnly=${repaintChangesOnly} (T1334729)`, function(assert) { + const instance = $('#list').dxList({ dataSource: [], repaintChangesOnly }).dxList('instance'); + const a11yStatusElement = getA11yStatus(instance).get(0); + + instance.option('dataSource', ['Item 1', 'Item 2']); + + assert.strictEqual(getA11yStatus(instance).get(0), a11yStatusElement, + 'the live region is not re-created, otherwise screen readers do not announce its text'); + assert.strictEqual(getA11yStatus(instance).text(), getItemsAnnouncement(2)); + }); + }); }); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/overlay.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/overlay.tests.js index 8010f8a63cf5..2df7d757220a 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/overlay.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/overlay.tests.js @@ -22,6 +22,7 @@ import * as zIndex from '__internal/ui/overlay/z_index'; import 'ui/scroll_view/ui.scrollable'; import selectors from '__internal/core/utils/m_selectors'; import swatch from '__internal/core/utils/swatch_container'; +import documentSizeCallbacks from '__internal/core/utils/document_size_callbacks'; import keyboardMock from '../../helpers/keyboardMock.js'; import pointerMock from '../../helpers/pointerMock.js'; import nativePointerMock from '../../helpers/nativePointerMock.js'; @@ -4471,3 +4472,81 @@ QUnit.module('Memory Leaks', { assert.strictEqual(positionController._$visualContainer, undefined, 'PositionController._$visualContainer is undefined after dispose'); }); }); + +QUnit.module('document size subscription', { + beforeEach: function() { + fx.off = true; + this.$element = $('#overlay'); + this.addSpy = sinon.spy(documentSizeCallbacks, 'add'); + this.removeSpy = sinon.spy(documentSizeCallbacks, 'remove'); + this.subscribedHandler = () => this.addSpy.lastCall.args[0]; + this.createOverlay = (options) => { + this.overlay = new Overlay(this.$element, options); + + return this.overlay; + }; + }, + afterEach: function() { + if(this.overlay) { + this.overlay.dispose(); + } + this.addSpy.restore(); + this.removeSpy.restore(); + fx.off = false; + } +}, () => { + QUnit.test('overlay should subscribe when shown and unsubscribe when hidden', function(assert) { + const overlay = this.createOverlay({ visible: true }); + + assert.ok(this.addSpy.calledOnce, 'subscribed while visible'); + + const handler = this.subscribedHandler(); + + overlay.hide(); + + assert.ok(this.removeSpy.calledWith(handler), 'unsubscribed when hidden'); + }); + + QUnit.test('overlay should unsubscribe on dispose', function(assert) { + const overlay = this.createOverlay({ visible: true }); + const handler = this.subscribedHandler(); + + overlay.dispose(); + this.overlay = null; + + assert.ok(this.removeSpy.calledWith(handler), 'unsubscribed on dispose'); + }); + + QUnit.test('geometry should be re-rendered when the visible area changes', function(assert) { + const overlay = this.createOverlay({ visible: true }); + const handler = this.subscribedHandler(); + const renderGeometrySpy = sinon.spy(overlay, '_renderGeometry'); + + handler(); + + assert.strictEqual(renderGeometrySpy.callCount, 1, 'geometry is re-rendered'); + }); + + QUnit.test('geometry should be re-rendered for an overlay placed against an element as well', function(assert) { + const overlay = this.createOverlay({ + visible: true, + visualContainer: $('#container') + }); + const handler = this.subscribedHandler(); + const renderGeometrySpy = sinon.spy(overlay, '_renderGeometry'); + + handler(); + + assert.strictEqual(renderGeometrySpy.callCount, 1, 'geometry is re-rendered'); + }); + + QUnit.test('a visible area change should be handled the same way as a window resize', function(assert) { + const overlay = this.createOverlay({ visible: true }); + const handler = this.subscribedHandler(); + const dimensionChangedSpy = sinon.spy(overlay, '_dimensionChanged'); + + handler(); + + assert.strictEqual(dimensionChangedSpy.callCount, 1, '_dimensionChanged is called'); + }); +}); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/popup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/popup.tests.js index 79a1ca867418..142a559496e8 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/popup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/popup.tests.js @@ -33,6 +33,8 @@ import { BUTTON_CLASS } from '__internal/ui/button/button'; import 'fluent_blue_light.css!'; import 'ui/popup'; +import 'ui/tooltip'; +import 'ui/load_panel'; import 'ui/tab_panel'; import 'ui/scroll_view'; import 'ui/date_box'; @@ -2774,6 +2776,41 @@ QUnit.module('keyboard navigation', { assert.strictEqual(this.popup.option('visible'), true, 'popup remains visible when _ignoreCloseOnChildEscape is true'); }); + + QUnit.test('should remain visible when child element presses escape and a tooltip is shown above it (T1334708)', function(assert) { + this.init({ dragEnabled: false }); + + const $input = $('').appendTo(this.popup.$content()); + const tooltip = $('
') + .appendTo('#qunit-fixture') + .dxTooltip({ target: $input, visible: true, animation: null }) + .dxTooltip('instance'); + + assert.strictEqual(tooltip.option('visible'), true, 'tooltip is shown'); + + keyboardMock($input).keyDown('esc'); + + assert.strictEqual(tooltip.option('visible'), false, 'tooltip is hidden'); + assert.strictEqual(this.popup.option('visible'), true, 'popup remains visible'); + + tooltip.dispose(); + }); + + QUnit.test('should be closed on child element escape key press when an overlay that ignores escape is shown above it', function(assert) { + this.init({ dragEnabled: false }); + + const $input = $('').appendTo(this.popup.$content()); + const loadPanel = $('
') + .appendTo('#qunit-fixture') + .dxLoadPanel({ visible: true, animation: null }) + .dxLoadPanel('instance'); + + keyboardMock($input).keyDown('esc'); + + assert.strictEqual(this.popup.option('visible'), false, 'popup is closed'); + + loadPanel.dispose(); + }); }); QUnit.module('rendering', { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/tabPanel.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/tabPanel.markup.tests.js index 738a7c0e6c4b..0c3a12da8732 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/tabPanel.markup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/tabPanel.markup.tests.js @@ -202,4 +202,96 @@ QUnit.module('aria accessibility', () => { instance.option('focusedElement', $element.find('.dx-item:eq(1)')); assert.equal($element.attr('aria-activedescendant'), undefined, 'aria-activedescendant does not exist after selection update'); }); + + QUnit.module('aria-label of tabs', { + beforeEach: function() { + this.createTabPanel = (options) => { + this.$element = $('
').appendTo('#qunit-fixture').dxTabPanel(options); + this.instance = this.$element.dxTabPanel('instance'); + }; + this.getTab = (index) => this.$element.find(`.${TABS_ITEM_CLASS}`).eq(index); + } + }, () => { + QUnit.test('tab with title and icon should not get aria-label', function(assert) { + this.createTabPanel({ items: [{ title: 'User', icon: 'user' }] }); + + assert.strictEqual(this.getTab(0).attr('aria-label'), undefined); + }); + + QUnit.test('string item should not get aria-label', function(assert) { + this.createTabPanel({ items: ['User'] }); + + assert.strictEqual(this.getTab(0).attr('aria-label'), undefined); + }); + + QUnit.test('tab of the item without title and icon should not get aria-label', function(assert) { + this.createTabPanel({ items: [{ text: 'User content' }] }); + + assert.strictEqual(this.getTab(0).attr('aria-label'), undefined); + }); + + [ + { icon: 'user', expected: 'user', description: 'a dxIcon without a localized message' }, + { icon: 'close', expected: 'Close', description: 'a dxIcon with a localized message' }, + { icon: '/path/file.png', expected: 'file', description: 'a path to an image' }, + ].forEach(({ icon, expected, description }) => { + QUnit.test(`icon-only tab should get aria-label "${expected}" for ${description}`, function(assert) { + this.createTabPanel({ items: [{ icon }] }); + + assert.strictEqual(this.getTab(0).attr('aria-label'), expected); + }); + }); + + QUnit.test('icon-only tab should get aria-label even if the item has content text or html', function(assert) { + this.createTabPanel({ + items: [ + { icon: 'user', text: 'User content' }, + { icon: 'find', html: 'Find content' }, + ], + }); + + assert.strictEqual(this.getTab(0).attr('aria-label'), 'user', 'item with text'); + assert.strictEqual(this.getTab(1).attr('aria-label'), 'find', 'item with html'); + }); + + QUnit.test('icon-only tab with badge should get aria-label derived from icon', function(assert) { + this.createTabPanel({ items: [{ icon: 'user', badge: '5' }] }); + + assert.strictEqual(this.getTab(0).attr('aria-label'), 'user'); + }); + + QUnit.test('aria-label should be derived from item data when a custom itemTitleTemplate is used', function(assert) { + this.createTabPanel({ + items: [{ icon: 'user' }, { icon: 'user', title: 'User' }], + itemTitleTemplate: (data) => ``, + }); + + assert.strictEqual(this.getTab(0).attr('aria-label'), 'user', 'icon-only item'); + assert.strictEqual(this.getTab(1).attr('aria-label'), undefined, 'item with title'); + }); + + [true, false].forEach((repaintChangesOnly) => { + QUnit.test(`aria-label should follow item title and icon changes, repaintChangesOnly: ${repaintChangesOnly}`, function(assert) { + this.createTabPanel({ items: [{ title: 'User', icon: 'user' }], repaintChangesOnly }); + + this.instance.option('items[0].title', ''); + assert.strictEqual(this.getTab(0).attr('aria-label'), 'user', 'label appears when title is cleared'); + + this.instance.option('items[0].icon', 'close'); + assert.strictEqual(this.getTab(0).attr('aria-label'), 'Close', 'label follows the icon'); + + this.instance.option('items[0].title', 'Close'); + assert.strictEqual(this.getTab(0).attr('aria-label'), undefined, 'label is removed when title is set'); + }); + + QUnit.test(`aria-label should be recalculated when items are replaced, repaintChangesOnly: ${repaintChangesOnly}`, function(assert) { + this.createTabPanel({ items: [{ icon: 'user' }], repaintChangesOnly }); + + this.instance.option('items', [{ title: 'User', icon: 'user' }, { icon: 'find' }]); + + assert.strictEqual(this.getTab(0).attr('aria-label'), undefined, 'title and icon item'); + assert.strictEqual(this.getTab(1).attr('aria-label'), 'find', 'icon-only item'); + }); + }); + }); }); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/tabs.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/tabs.markup.tests.js index cb451d655c54..05fa8dab32f4 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/tabs.markup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/tabs.markup.tests.js @@ -1,10 +1,12 @@ import $ from 'jquery'; import Tabs from 'ui/tabs'; +import { DataSource } from 'common/data/data_source/data_source'; import windowUtils from 'core/utils/window'; import ariaAccessibilityTestHelper from '../../helpers/ariaAccessibilityTestHelper.js'; import { TABS_CLASS, TABS_WRAPPER_CLASS, + TABS_ITEM_CLASS, TABS_ITEM_TEXT_SPAN_CLASS, TABS_ITEM_TEXT_SPAN_PSEUDO_CLASS } from '__internal/ui/tabs/tabs'; @@ -195,6 +197,144 @@ QUnit.module('Aria accessibility', { }); }); +QUnit.module('Aria label', { + beforeEach: function() { + this.createTabs = (options) => { + this.$element = $('#tabs').dxTabs(options); + this.instance = this.$element.dxTabs('instance'); + }; + this.getTab = (index) => this.$element.find(`.${TABS_ITEM_CLASS}`).eq(index); + } +}, () => { + QUnit.test('item with text and icon should not get aria-label', function(assert) { + this.createTabs({ items: [{ text: 'User', icon: 'user' }] }); + + assert.strictEqual(this.getTab(0).attr('aria-label'), undefined); + }); + + QUnit.test('item with html and icon should not get aria-label', function(assert) { + this.createTabs({ items: [{ html: 'User', icon: 'user' }] }); + + assert.strictEqual(this.getTab(0).attr('aria-label'), undefined); + }); + + QUnit.test('string item should not get aria-label', function(assert) { + this.createTabs({ items: ['User'] }); + + assert.strictEqual(this.getTab(0).attr('aria-label'), undefined); + }); + + QUnit.test('item without text and icon should not get aria-label', function(assert) { + this.createTabs({ items: [{ badge: '5' }] }); + + assert.strictEqual(this.getTab(0).attr('aria-label'), undefined); + }); + + [ + { icon: 'user', expected: 'user', description: 'a dxIcon without a localized message' }, + { icon: 'close', expected: 'Close', description: 'a dxIcon with a localized message' }, + { icon: 'fa fa-home', expected: 'fa fa-home', description: 'a font icon' }, + { icon: '/path/file.png', expected: 'file', description: 'a path to an image' }, + { icon: 'Svg title', expected: 'Svg title', description: 'an svg with a title' }, + ].forEach(({ icon, expected, description }) => { + QUnit.test(`icon-only item should get aria-label "${expected}" for ${description}`, function(assert) { + this.createTabs({ items: [{ icon }] }); + + assert.strictEqual(this.getTab(0).attr('aria-label'), expected); + }); + }); + + [ + { icon: 'data:image/png;base64,qwerty', description: 'a base64 image' }, + { icon: 'https://example.com/path/file.png', description: 'an image URL' }, + { icon: '', description: 'an svg without a title' }, + ].forEach(({ icon, description }) => { + QUnit.test(`icon-only item should not get aria-label for ${description}`, function(assert) { + this.createTabs({ items: [{ icon }] }); + + assert.strictEqual(this.getTab(0).attr('aria-label'), undefined); + }); + }); + + QUnit.test('icon-only item with badge should get aria-label derived from icon', function(assert) { + this.createTabs({ items: [{ icon: 'user', badge: '5' }] }); + + assert.strictEqual(this.getTab(0).attr('aria-label'), 'user'); + }); + + QUnit.test('only icon-only items should get aria-label', function(assert) { + this.createTabs({ items: [{ text: 'Users' }, { icon: 'user' }, { text: 'Find', icon: 'find' }] }); + + assert.strictEqual(this.getTab(0).attr('aria-label'), undefined, 'text item'); + assert.strictEqual(this.getTab(1).attr('aria-label'), 'user', 'icon-only item'); + assert.strictEqual(this.getTab(2).attr('aria-label'), undefined, 'text and icon item'); + }); + + QUnit.test('aria-label should be derived from item data when a custom itemTemplate is used', function(assert) { + this.createTabs({ + items: [{ icon: 'user' }, { icon: 'user', text: 'User' }], + itemTemplate: (data) => ``, + }); + + assert.strictEqual(this.getTab(0).attr('aria-label'), 'user', 'icon-only item'); + assert.strictEqual(this.getTab(1).attr('aria-label'), undefined, 'item with text'); + }); + + [true, false].forEach((repaintChangesOnly) => { + QUnit.test(`aria-label should follow item text and icon changes, repaintChangesOnly: ${repaintChangesOnly}`, function(assert) { + this.createTabs({ items: [{ text: 'User', icon: 'user' }], repaintChangesOnly }); + + this.instance.option('items[0].text', ''); + assert.strictEqual(this.getTab(0).attr('aria-label'), 'user', 'label appears when text is cleared'); + + this.instance.option('items[0].icon', 'close'); + assert.strictEqual(this.getTab(0).attr('aria-label'), 'Close', 'label follows the icon'); + + this.instance.option('items[0].text', 'Close'); + assert.strictEqual(this.getTab(0).attr('aria-label'), undefined, 'label is removed when text is set'); + }); + + QUnit.test(`aria-label should be recalculated when items are replaced, repaintChangesOnly: ${repaintChangesOnly}`, function(assert) { + this.createTabs({ items: [{ icon: 'user' }], repaintChangesOnly }); + + this.instance.option('items', [{ text: 'User', icon: 'user' }, { icon: 'find' }]); + + assert.strictEqual(this.getTab(0).attr('aria-label'), undefined, 'text and icon item'); + assert.strictEqual(this.getTab(1).attr('aria-label'), 'find', 'icon-only item'); + }); + }); + + QUnit.test('aria-label should be recalculated when the _itemAriaLabelExpr option changes', function(assert) { + this.createTabs({ items: [{ icon: 'user', badge: 'New' }] }); + + this.instance.option('_itemAriaLabelExpr', (data) => data.badge); + assert.strictEqual(this.getTab(0).attr('aria-label'), 'New', 'label follows the new rule'); + + this.instance.option('_itemAriaLabelExpr', () => undefined); + assert.strictEqual(this.getTab(0).attr('aria-label'), undefined, 'label is removed when the rule returns nothing'); + }); + + QUnit.test('items inserted and updated through the data source push should get aria-label', function(assert) { + const data = [{ id: 0, text: 'User', icon: 'user' }]; + const dataSource = new DataSource({ + paginate: false, + pushAggregationTimeout: 0, + load: () => data, + key: 'id', + }); + + this.createTabs({ dataSource, repaintChangesOnly: true }); + + const store = dataSource.store(); + + store.push([{ type: 'insert', data: { id: 1, icon: 'find' } }]); + assert.strictEqual(this.getTab(1).attr('aria-label'), 'find', 'inserted icon-only item'); + + store.push([{ type: 'update', key: 0, data: { id: 0, text: '', icon: 'user' } }]); + assert.strictEqual(this.getTab(0).attr('aria-label'), 'user', 'updated item without text'); + }); +}); + QUnit.module('Default template', { beforeEach: function() { this.prepareItemTest = (data) => { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/toolbar.kbn.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/toolbar.kbn.tests.js index 564c6efcfa6b..d5fec4952418 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/toolbar.kbn.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/toolbar.kbn.tests.js @@ -29,7 +29,7 @@ import { DROP_DOWN_BUTTON_CLASS } from '__internal/ui/drop_down_button'; import { TEXTEDITOR_CLASS, TEXTEDITOR_INPUT_CLASS } from '__internal/ui/text_box/text_editor.base'; import { TEXTBOX_CLASS } from '__internal/ui/text_box/text_box'; -import { WIDGET_CLASS as NUMBERBOX_CLASS } from '__internal/ui/number_box/m_number_box.base'; +import { WIDGET_CLASS as NUMBERBOX_CLASS } from '__internal/ui/number_box/number_box.base'; import { SELECTBOX_CLASS } from '__internal/ui/select_box'; import { SWITCH_CLASS } from '__internal/ui/switch'; diff --git a/packages/sbom/pnpm-lock.yaml b/packages/sbom/pnpm-lock.yaml index 9c823e841f73..fd286ee42947 100644 --- a/packages/sbom/pnpm-lock.yaml +++ b/packages/sbom/pnpm-lock.yaml @@ -8,7 +8,8 @@ overrides: tar@<7.5.19: ^7.5.19 brace-expansion@<5.0.8: ^5.0.8 body-parser@>=2.0.0 <2.3.0: ^2.3.0 - fast-uri@>=3.0.0 <3.1.5: ^3.1.5 + fast-uri@>=3.0.0 <3.1.6: ^3.1.6 + qs@>=2.2.5 <6.16.0: ^6.16.0 undici@>=7.23.0 <7.29.0: ^7.29.0 brace-expansion@>=3.0.0 <5.0.9: ^5.0.9 @@ -422,8 +423,8 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-uri@3.1.5: - resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} + fast-uri@3.1.7: + resolution: {integrity: sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==} finalhandler@1.1.2: resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} @@ -691,8 +692,8 @@ packages: resolution: {integrity: sha512-WPn+h9RGEExOKdu4bsF4HksG/uzd3cFq3MFtq8PsFeExPse5Ha/VOjQNyHhjboBFwGXGev6muJYTSPAOkROq2g==} engines: {node: '>=18'} - qs@6.15.3: - resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + qs@6.16.0: + resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==} engines: {node: '>=0.6'} quick-lru@5.1.1: @@ -1157,7 +1158,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.5 + fast-uri: 3.1.7 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -1183,7 +1184,7 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.7.2 on-finished: 2.4.1 - qs: 6.15.3 + qs: 6.16.0 raw-body: 3.0.2 type-is: 2.1.0 transitivePeerDependencies: @@ -1381,7 +1382,7 @@ snapshots: fast-deep-equal@3.1.3: {} - fast-uri@3.1.5: {} + fast-uri@3.1.7: {} finalhandler@1.1.2: dependencies: @@ -1661,7 +1662,7 @@ snapshots: transitivePeerDependencies: - supports-color - qs@6.15.3: + qs@6.16.0: dependencies: es-define-property: 1.0.1 side-channel: 1.1.1 diff --git a/packages/sbom/pnpm-workspace.yaml b/packages/sbom/pnpm-workspace.yaml index 9f665ac75ee6..f2a0edddd8fc 100644 --- a/packages/sbom/pnpm-workspace.yaml +++ b/packages/sbom/pnpm-workspace.yaml @@ -8,6 +8,7 @@ overrides: tar@<7.5.19: ^7.5.19 brace-expansion@<5.0.8: ^5.0.8 body-parser@>=2.0.0 <2.3.0: ^2.3.0 - fast-uri@>=3.0.0 <3.1.5: ^3.1.5 + fast-uri@>=3.0.0 <3.1.6: ^3.1.6 + qs@>=2.2.5 <6.16.0: ^6.16.0 undici@>=7.23.0 <7.29.0: ^7.29.0 brace-expansion@>=3.0.0 <5.0.9: ^5.0.9 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1628ba9434cc..afcec45c9a22 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,8 +83,8 @@ catalogs: specifier: 8.52.0 version: 8.52.0 axe-core: - specifier: 4.11.3 - version: 4.11.3 + specifier: 4.13.0 + version: 4.13.0 devexpress-diagram: specifier: 2.2.31 version: 2.2.31 @@ -179,15 +179,15 @@ catalogs: overrides: basic-ftp@<5.3.1: '>=5.3.1' - js-yaml@<4.3.1: ^4.3.1 + js-yaml@<4.3.2: ^4.3.2 rollup@<4.59.0: ^4.59.0 json5@<1.0.2: ^2.2.3 axios@<1.18.0: ^1.18.0 braces@<3.0.3: ^3.0.3 semver@<5.7.2: ^5.7.2 - qs: '>=6.15.2' + qs: '>=6.16.0' vite@>=7.0.0 <=7.3.4: ^7.3.5 - hono@<4.12.34: ^4.12.34 + hono@<4.13.5: ^4.13.5 minimatch@>=9.0.0 < 9.0.7: 9.0.9 picomatch@>=4.0.0 <4.0.4: 4.0.4 micromatch@<4.0.8: ^4.0.8 @@ -223,13 +223,15 @@ overrides: tar@<7.5.19: ^7.5.19 shell-quote@<=1.8.4: ^1.9.0 babel-traverse: npm:@babel/traverse@^7.23.2 - fast-uri@>=3.0.0 <3.1.5: ^3.1.5 + fast-uri@>=3.0.0 <3.1.6: ^3.1.6 '@hono/node-server@<2.0.12': ^2.0.12 dompurify@<=3.4.12: ^3.4.13 nanoid@<3.3.17: ^3.3.17 browserslist@<4.28.7: ^4.28.7 postcss-selector-parser@>=6.1.0 <6.1.3: ^6.1.3 postcss-selector-parser@>=7.1.0 <7.1.3: ^7.1.3 + fflate@>=0.8.0 <0.8.3: ^0.8.3 + colord@<2.9.4: ^2.9.4 patchedDependencies: testcafe-hammerhead@31.7.8: 8655c07786177d3b611a05abf6b0ea87d32796eb2601f92a800ef041095f264c @@ -261,7 +263,7 @@ importers: version: 17.0.35 axe-core: specifier: 'catalog:' - version: 4.11.3 + version: 4.13.0 chokidar: specifier: 5.0.0 version: 5.0.0 @@ -623,7 +625,7 @@ importers: version: 5.10.0(eslint@9.39.4(jiti@2.6.1)) '@testcafe-community/axe': specifier: 3.5.0 - version: 3.5.0(axe-core@4.11.3)(testcafe@3.7.5) + version: 3.5.0(axe-core@4.13.0)(testcafe@3.7.5) '@types/eslint': specifier: 8.56.12 version: 8.56.12 @@ -656,7 +658,7 @@ importers: version: 0.9.1(typescript@6.0.3)(vue@3.5.32(typescript@6.0.3)) axe-core: specifier: 'catalog:' - version: 4.11.3 + version: 4.13.0 babel-jest: specifier: 30.4.1 version: 30.4.1(@babel/core@7.29.7) @@ -1100,7 +1102,7 @@ importers: version: 5.10.0(eslint@9.39.4(jiti@2.6.1)) '@testcafe-community/axe': specifier: 3.5.0 - version: 3.5.0(axe-core@4.11.3)(testcafe@3.7.5) + version: 3.5.0(axe-core@4.13.0)(testcafe@3.7.5) '@types/jquery': specifier: 'catalog:' version: 4.0.1 @@ -1112,7 +1114,7 @@ importers: version: 8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) axe-core: specifier: 'catalog:' - version: 4.11.3 + version: 4.13.0 devextreme: specifier: workspace:* version: link:../../packages/devextreme/artifacts/npm/devextreme @@ -1435,7 +1437,7 @@ importers: version: 8.52.0(eslint@9.39.4(jiti@2.6.1))(typescript@4.9.5) axe-core: specifier: 'catalog:' - version: 4.11.3 + version: 4.13.0 babel-plugin-inferno: specifier: 6.8.5 version: 6.8.5(@babel/core@7.29.7) @@ -2176,6 +2178,34 @@ importers: specifier: 'catalog:' version: 3.7.5 + tools/flaky-test-reporter: + dependencies: + yargs: + specifier: 17.7.2 + version: 17.7.2 + devDependencies: + '@types/jest': + specifier: 30.0.0 + version: 30.0.0 + '@types/node': + specifier: catalog:tools + version: 20.19.37 + '@types/yargs': + specifier: 17.0.35 + version: 17.0.35 + prettier: + specifier: catalog:tools + version: 3.5.3 + ts-jest: + specifier: 29.4.12 + version: 29.4.12(@babel/core@7.29.7)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@30.4.1(@babel/core@7.29.7))(jest-util@30.4.1)(jest@30.4.2(@types/node@20.19.37)(babel-plugin-macros@3.1.0)(node-notifier@9.0.1)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.21))(@types/node@20.19.37)(typescript@5.9.3)))(typescript@5.9.3) + ts-node: + specifier: catalog:tools + version: 10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.21))(@types/node@20.19.37)(typescript@5.9.3) + typescript: + specifier: catalog:tools + version: 5.9.3 + packages: '@adobe/css-tools@4.4.4': @@ -4726,7 +4756,7 @@ packages: resolution: {integrity: sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==} engines: {node: '>=20'} peerDependencies: - hono: ^4.12.34 + hono: ^4.13.5 '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} @@ -5716,6 +5746,7 @@ packages: '@ngtools/webpack@22.1.2': resolution: {integrity: sha512-5o3zbOdEHv5+wjRCMN+t6RBou28dbPkio5/rxEJBsQ9iEq11K+atsSKEaZXBMUhYr9nLYR8Pckm5Cqx2FP9R2Q==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + deprecated: Angular's Webpack support is deprecated. Use the esbuild and Vite-based "@angular/build" package instead. peerDependencies: '@angular/compiler-cli': ^22.0.0 typescript: '>=6.0 <6.1' @@ -8705,12 +8736,8 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axe-core@4.11.3: - resolution: {integrity: sha512-zBQouZixDTbo3jMGqHKyePxYxr1e5W8UdTmBQ7sNtaA9M2bE32daxxPLS/jojhKOHxQ7LWwPjfiwf/fhaJWzlg==} - engines: {node: '>=4'} - - axe-core@4.12.1: - resolution: {integrity: sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==} + axe-core@4.13.0: + resolution: {integrity: sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==} engines: {node: '>=4'} axios@1.18.1: @@ -9433,8 +9460,8 @@ packages: resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==} engines: {node: '>=18'} - colord@2.9.3: - resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + colord@2.10.0: + resolution: {integrity: sha512-AidJptpBJmjTclAp9BkLwJi0T93fo5epJnbaZslpg6QVzpHjAiveF55mE9AcUJiGMqRHgMDY8soMsQtuNYMHfw==} colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -10828,8 +10855,8 @@ packages: fast-string-width@3.0.2: resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} - fast-uri@3.1.5: - resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} + fast-uri@3.1.7: + resolution: {integrity: sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==} fast-wrap-ansi@0.2.2: resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} @@ -10863,8 +10890,8 @@ packages: fecha@4.2.3: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} - fflate@0.8.2: - resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} @@ -11368,8 +11395,8 @@ packages: hmac-drbg@1.0.1: resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} - hono@4.13.0: - resolution: {integrity: sha512-jhunvfHWxd7J5EFfSgH4xsYJzSe/lfqbUCxiyyeaQasUsXeEHXtzVid+7EOGByc5JnFa23SSFL3Y2RV/z1T+eQ==} + hono@4.13.7: + resolution: {integrity: sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==} engines: {node: '>=16.9.0'} hookified@1.15.1: @@ -12401,8 +12428,8 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - js-yaml@4.3.1: - resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true jsdom@26.1.0: @@ -14359,6 +14386,7 @@ packages: engines: {node: '>=0.6.0', teleport: '>=0.2.0'} deprecated: |- You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. + (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) qified@0.9.1: @@ -14373,12 +14401,8 @@ packages: resolution: {integrity: sha512-ZvWjbAj4MWAj6bnCc9CnculsXnJr7eoKsvH/8rVpZbqYxP2z05HNQa43ZVwe/dVRcFxgfFHE2CkUqn0sCyLfHw==} hasBin: true - qs@6.15.2: - resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} - engines: {node: '>=0.6'} - - qs@6.15.3: - resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + qs@6.16.0: + resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==} engines: {node: '>=0.6'} querystring-es3@0.2.1: @@ -20536,7 +20560,7 @@ snapshots: globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.3.1 + js-yaml: 4.3.2 minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -20577,9 +20601,9 @@ snapshots: '@harperfast/extended-iterable@1.0.3': optional: true - '@hono/node-server@2.0.12(hono@4.13.0)': + '@hono/node-server@2.0.12(hono@4.13.7)': dependencies: - hono: 4.13.0 + hono: 4.13.7 '@humanfs/core@0.19.2': dependencies: @@ -21117,7 +21141,7 @@ snapshots: camelcase: 5.3.1 find-up: 4.1.0 get-package-type: 0.1.0 - js-yaml: 4.3.1 + js-yaml: 4.3.2 resolve-from: 5.0.0 '@istanbuljs/schema@0.1.6': {} @@ -21901,7 +21925,7 @@ snapshots: '@modelcontextprotocol/sdk@1.26.0(zod@4.1.13)': dependencies: - '@hono/node-server': 2.0.12(hono@4.13.0) + '@hono/node-server': 2.0.12(hono@4.13.7) ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) content-type: 1.0.5 @@ -21911,7 +21935,7 @@ snapshots: eventsource-parser: 3.0.8 express: 5.2.1 express-rate-limit: 8.3.2(express@5.2.1) - hono: 4.13.0 + hono: 4.13.7 jose: 6.2.2 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 @@ -21923,7 +21947,7 @@ snapshots: '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': dependencies: - '@hono/node-server': 2.0.12(hono@4.13.0) + '@hono/node-server': 2.0.12(hono@4.13.7) ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) content-type: 1.0.5 @@ -21933,7 +21957,7 @@ snapshots: eventsource-parser: 3.0.8 express: 5.2.1 express-rate-limit: 8.3.2(express@5.2.1) - hono: 4.13.0 + hono: 4.13.7 jose: 6.2.2 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 @@ -23822,9 +23846,9 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@testcafe-community/axe@3.5.0(axe-core@4.11.3)(testcafe@3.7.5)': + '@testcafe-community/axe@3.5.0(axe-core@4.13.0)(testcafe@3.7.5)': dependencies: - axe-core: 4.11.3 + axe-core: 4.13.0 chalk: 2.4.2 testcafe: 3.7.5 @@ -24337,7 +24361,7 @@ snapshots: dependencies: '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 6.21.0(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.4.3(supports-color@7.2.0) eslint: 9.39.4(jiti@2.6.1) @@ -24555,7 +24579,7 @@ snapshots: '@typescript-eslint/type-utils@6.21.0(eslint@9.39.4(jiti@2.6.1))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: - '@typescript-eslint/typescript-estree': 6.21.0(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 6.21.0(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/utils': 6.21.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3) debug: 4.4.3(supports-color@7.2.0) eslint: 9.39.4(jiti@2.6.1) @@ -24653,7 +24677,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@6.21.0(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@6.21.0(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 @@ -24840,7 +24864,7 @@ snapshots: '@types/semver': 7.7.1 '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 6.21.0(supports-color@7.2.0)(typescript@6.0.3) eslint: 9.39.4(jiti@2.6.1) semver: 7.8.5 transitivePeerDependencies: @@ -25604,14 +25628,14 @@ snapshots: ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.5 + fast-uri: 3.1.7 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.5 + fast-uri: 3.1.7 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -25871,9 +25895,7 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 - axe-core@4.11.3: {} - - axe-core@4.12.1: {} + axe-core@4.13.0: {} axios@1.18.1: dependencies: @@ -26232,7 +26254,7 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.4.24 on-finished: 2.4.1 - qs: 6.15.3 + qs: 6.16.0 raw-body: 2.5.3 type-is: 1.6.18 unpipe: 1.0.0 @@ -26247,7 +26269,7 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.7.2 on-finished: 2.4.1 - qs: 6.15.3 + qs: 6.16.0 raw-body: 3.0.2 type-is: 2.1.0 transitivePeerDependencies: @@ -26812,7 +26834,7 @@ snapshots: color-convert: 3.1.3 color-string: 2.1.4 - colord@2.9.3: {} + colord@2.10.0: {} colorette@2.0.20: {} @@ -26996,7 +27018,7 @@ snapshots: cosmiconfig@8.3.6(typescript@5.9.3): dependencies: import-fresh: 3.3.1 - js-yaml: 4.3.1 + js-yaml: 4.3.2 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: @@ -27006,7 +27028,7 @@ snapshots: dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 - js-yaml: 4.3.1 + js-yaml: 4.3.2 parse-json: 5.2.0 optionalDependencies: typescript: 4.9.5 @@ -27015,7 +27037,7 @@ snapshots: dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 - js-yaml: 4.3.1 + js-yaml: 4.3.2 parse-json: 5.2.0 optionalDependencies: typescript: 5.8.3 @@ -27024,7 +27046,7 @@ snapshots: dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 - js-yaml: 4.3.1 + js-yaml: 4.3.2 parse-json: 5.2.0 optionalDependencies: typescript: 5.9.3 @@ -27033,7 +27055,7 @@ snapshots: dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 - js-yaml: 4.3.1 + js-yaml: 4.3.2 parse-json: 5.2.0 optionalDependencies: typescript: 6.0.3 @@ -27310,7 +27332,7 @@ snapshots: object-keys: 1.1.1 object.assign: 4.1.7 regexp.prototype.flags: 1.5.4 - side-channel: 1.1.0 + side-channel: 1.1.1 which-boxed-primitive: 1.1.1 which-collection: 1.0.2 which-typed-array: 1.1.20 @@ -28414,7 +28436,7 @@ snapshots: array-includes: 3.1.9 array.prototype.flatmap: 1.3.3 ast-types-flow: 0.0.8 - axe-core: 4.12.1 + axe-core: 4.13.0 axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 @@ -28832,7 +28854,7 @@ snapshots: parseurl: 1.3.3 path-to-regexp: 0.1.13 proxy-addr: 2.0.7 - qs: 6.15.2 + qs: 6.16.0 range-parser: 1.2.1 safe-buffer: 5.2.1 send: 0.19.2 @@ -28867,7 +28889,7 @@ snapshots: once: 1.4.0 parseurl: 1.3.3 proxy-addr: 2.0.7 - qs: 6.15.3 + qs: 6.16.0 range-parser: 1.3.0 router: 2.2.0 send: 1.2.1 @@ -28931,7 +28953,7 @@ snapshots: dependencies: fast-string-truncated-width: 3.0.3 - fast-uri@3.1.5: {} + fast-uri@3.1.7: {} fast-wrap-ansi@0.2.2: dependencies: @@ -28965,7 +28987,7 @@ snapshots: fecha@4.2.3: {} - fflate@0.8.2: {} + fflate@0.8.3: {} figures@3.2.0: dependencies: @@ -29611,7 +29633,7 @@ snapshots: minimalistic-assert: 1.0.1 minimalistic-crypto-utils: 1.0.1 - hono@4.13.0: {} + hono@4.13.7: {} hookified@1.15.1: {} @@ -31478,7 +31500,7 @@ snapshots: js-tokens@9.0.1: {} - js-yaml@4.3.1: + js-yaml@4.3.2: dependencies: argparse: 2.0.1 @@ -31559,7 +31581,7 @@ snapshots: dependencies: '@babel/runtime': 7.29.2 fast-png: 6.4.0 - fflate: 0.8.2 + fflate: 0.8.3 optionalDependencies: canvg: 3.0.11 core-js: 3.49.0 @@ -33847,11 +33869,7 @@ snapshots: qrcode-terminal@0.10.0: {} - qs@6.15.2: - dependencies: - side-channel: 1.1.0 - - qs@6.15.3: + qs@6.16.0: dependencies: es-define-property: 1.0.1 side-channel: 1.1.1 @@ -35403,7 +35421,7 @@ snapshots: '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.5) '@dual-bundle/import-meta-resolve': 4.2.1 balanced-match: 2.0.0 - colord: 2.9.3 + colord: 2.10.0 cosmiconfig: 9.0.1(typescript@4.9.5) css-functions-list: 3.3.3 css-tree: 3.2.1 @@ -35447,7 +35465,7 @@ snapshots: '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.5) '@dual-bundle/import-meta-resolve': 4.2.1 balanced-match: 2.0.0 - colord: 2.9.3 + colord: 2.10.0 cosmiconfig: 9.0.1(typescript@5.8.3) css-functions-list: 3.3.3 css-tree: 3.2.1 @@ -35491,7 +35509,7 @@ snapshots: '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.5) '@dual-bundle/import-meta-resolve': 4.2.1 balanced-match: 2.0.0 - colord: 2.9.3 + colord: 2.10.0 cosmiconfig: 9.0.1(typescript@5.9.3) css-functions-list: 3.3.3 css-tree: 3.2.1 @@ -35535,7 +35553,7 @@ snapshots: '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.5) '@dual-bundle/import-meta-resolve': 4.2.1 balanced-match: 2.0.0 - colord: 2.9.3 + colord: 2.10.0 cosmiconfig: 9.0.1(typescript@6.0.3) css-functions-list: 3.3.3 css-tree: 3.2.1 @@ -36523,7 +36541,7 @@ snapshots: union@0.5.0: dependencies: - qs: 6.15.2 + qs: 6.16.0 unist-util-find-after@4.0.1: dependencies: @@ -36640,7 +36658,7 @@ snapshots: url@0.11.4: dependencies: punycode: 1.4.1 - qs: 6.15.2 + qs: 6.16.0 use-sync-external-store@1.6.0(react@18.3.1): dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 723fdeb948d3..d31827f24daa 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,25 +3,29 @@ packages: - 'e2e/*' - 'packages/*' - '!packages/sbom' + - 'tools/*' linkWorkspacePackages: true preferWorkspacePackages: true saveWorkspaceProtocol: false packageManagerStrict: false engineStrict: true -minimumReleaseAge: 0 + +# Supply-chain guard: ignore npm versions published less than 24 hours ago. +# Need a fresher version? Add a targeted minimumReleaseAgeExclude entry with the exact version. +minimumReleaseAge: 1440 # ⚠️ Do NOT modify this. overrides: "basic-ftp@<5.3.1": ">=5.3.1" - "js-yaml@<4.3.1": "^4.3.1" + "js-yaml@<4.3.2": "^4.3.2" "rollup@<4.59.0": "^4.59.0" "json5@<1.0.2": ^2.2.3 "axios@<1.18.0": ^1.18.0 "braces@<3.0.3": ^3.0.3 "semver@<5.7.2": ^5.7.2 - qs: ">=6.15.2" + qs: ">=6.16.0" "vite@>=7.0.0 <=7.3.4": ^7.3.5 - "hono@<4.12.34": "^4.12.34" + "hono@<4.13.5": "^4.13.5" "minimatch@>=9.0.0 < 9.0.7": 9.0.9 "picomatch@>=4.0.0 <4.0.4": 4.0.4 "micromatch@<4.0.8": ^4.0.8 @@ -57,15 +61,17 @@ overrides: "tar@<7.5.19": "^7.5.19" "shell-quote@<=1.8.4": ^1.9.0 "babel-traverse": "npm:@babel/traverse@^7.23.2" - "fast-uri@>=3.0.0 <3.1.5": "^3.1.5" + "fast-uri@>=3.0.0 <3.1.6": "^3.1.6" "@hono/node-server@<2.0.12": "^2.0.12" "dompurify@<=3.4.12": "^3.4.13" "nanoid@<3.3.17": "^3.3.17" "browserslist@<4.28.7": "^4.28.7" "postcss-selector-parser@>=6.1.0 <6.1.3": "^6.1.3" "postcss-selector-parser@>=7.1.0 <7.1.3": "^7.1.3" + "fflate@>=0.8.0 <0.8.3": "^0.8.3" + "colord@<2.9.4": "^2.9.4" catalog: - axe-core: 4.11.3 + axe-core: 4.13.0 inferno: ^8.2.3 inferno-hydrate: ^8.2.3 inferno-create-element: ^8.2.3 diff --git a/tools/.prettierignore b/tools/.prettierignore new file mode 100644 index 000000000000..090fc6536af2 --- /dev/null +++ b/tools/.prettierignore @@ -0,0 +1,3 @@ +# Shared by every tools/* package. Prettier does not search parent directories for this file, +# so each package passes it explicitly via --ignore-path. +*.json diff --git a/tools/.prettierrc.yaml b/tools/.prettierrc.yaml new file mode 100644 index 000000000000..08e20ffc0b60 --- /dev/null +++ b/tools/.prettierrc.yaml @@ -0,0 +1,12 @@ +arrowParens: always +bracketSameLine: true +bracketSpacing: true +endOfLine: lf +experimentalOperatorPosition: start +objectWrap: preserve +printWidth: 100 +quoteProps: preserve +semi: true +singleQuote: true +tabWidth: 2 +trailingComma: all diff --git a/tools/flaky-test-reporter/jest.config.js b/tools/flaky-test-reporter/jest.config.js new file mode 100644 index 000000000000..ae286b8ce8ad --- /dev/null +++ b/tools/flaky-test-reporter/jest.config.js @@ -0,0 +1,4 @@ +module.exports = { + displayName: 'flaky-test-reporter', + preset: '../jest.config.base.js', +}; diff --git a/tools/flaky-test-reporter/package.json b/tools/flaky-test-reporter/package.json new file mode 100644 index 000000000000..6a5ef539078c --- /dev/null +++ b/tools/flaky-test-reporter/package.json @@ -0,0 +1,27 @@ +{ + "name": "flaky-test-reporter", + "version": "26.2.0", + "type": "commonjs", + "private": true, + "author": "Developer Express Inc.", + "license": "MIT", + "scripts": { + "collect": "ts-node src/main.ts", + "format": "prettier --write . --ignore-path ../.prettierignore", + "format:check": "prettier --check . --ignore-path ../.prettierignore", + "lint": "pnpm run format:check", + "test": "jest" + }, + "dependencies": { + "yargs": "17.7.2" + }, + "devDependencies": { + "@types/jest": "30.0.0", + "@types/node": "catalog:tools", + "@types/yargs": "17.0.35", + "prettier": "catalog:tools", + "ts-jest": "29.4.12", + "ts-node": "catalog:tools", + "typescript": "catalog:tools" + } +} diff --git a/tools/flaky-test-reporter/src/main.test.ts b/tools/flaky-test-reporter/src/main.test.ts new file mode 100644 index 000000000000..7d09cbcaffeb --- /dev/null +++ b/tools/flaky-test-reporter/src/main.test.ts @@ -0,0 +1,44 @@ +import { parseArgs } from './main'; + +const flags = [ + '--repo', + 'o/r', + '--workflow', + 'testcafe_tests.yml', + '--artifact', + 'flaky-candidates', + '--branch', + 'main', + '--window-hours', + '48', + '--out-json', + 'report.json', + '--out-markdown', + 'report.md', +]; + +describe('parseArgs', () => { + it('parses the flags', () => { + expect(parseArgs(flags)).toMatchObject({ + repo: 'o/r', + workflow: 'testcafe_tests.yml', + artifact: 'flaky-candidates', + branch: 'main', + windowHours: 48, + outJson: 'report.json', + outMarkdown: 'report.md', + }); + }); + + it('parses identically when invoked through `pnpm run