diff --git a/.github/DEVELOPMENT.md b/.github/DEVELOPMENT.md index ed44e34..6beee97 100644 --- a/.github/DEVELOPMENT.md +++ b/.github/DEVELOPMENT.md @@ -22,6 +22,18 @@ This page contains the steps to build and run the Syncfusion Toolkit for Blazor dotnet build ./Syncfusion.Blazor.Toolkit.slnx ``` +### Release sanity check (local) + +If you want to mimic what `.github/workflows/nuget-publish.yml` does on a release runner, pass `-p:ContinuousIntegrationBuild=true` so SourceLink and the package hash match what CI produces: + +```dotnetcli +dotnet restore src/Syncfusion.Blazor.Toolkit.csproj -p:ContinuousIntegrationBuild=true +dotnet build src/Syncfusion.Blazor.Toolkit.csproj -c Release --no-restore -p:ContinuousIntegrationBuild=true +dotnet pack src/Syncfusion.Blazor.Toolkit.csproj -c Release --no-build -o nupkg -p:ContinuousIntegrationBuild=true +``` + +> **Note**: `dotnet pack` triggers a `BeforeBuild` target that runs `npm install` and `gulp blazor-toolkit-themes` if `src/wwwroot/styles/fluent.min.css` is absent. Make sure Node.js (LTS) is on `PATH`. The release workflow installs Node 22 explicitly to handle this. + ## Running Samples - Open the `samples/Blazor.Toolkit.Samples.slnx` file in Visual Studio. diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 8ef95be..a9426e2 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -58,4 +58,4 @@ On a **monthly cadence** (targeting the second Wednesday of each month), the mai This project maintains a current security reference in the repository's [THREAT-MODEL.md](../THREAT-MODEL.md) document. The project team has reviewed the current architecture, package surface, and release flow and has documented the principal risks and mitigations in good faith. -This attestation reflects the project’s current understanding as of 2026-08-12 and is intended to be updated as the toolkit evolves. +This attestation reflects the project’s current understanding as of 2026-08-21 and is intended to be updated as the toolkit evolves. diff --git a/.github/THREAT-MODEL.md b/.github/THREAT-MODEL.md index d6138ab..e3c8aad 100644 --- a/.github/THREAT-MODEL.md +++ b/.github/THREAT-MODEL.md @@ -129,6 +129,10 @@ This threat model should be reviewed when: ## Self-attestation -This threat model was prepared as a current security reference for the Syncfusion Blazor Toolkit project and reflects the maintainers’ understanding of the project as of 2026-08-12. The project team intends to review and update this document as changes to the component library, assets, or build pipeline occur. +This threat model was prepared as a current security reference for the Syncfusion Blazor Toolkit project and reflects the maintainers’ understanding of the project as of 2026-08-21. The project team intends to review and update this document as changes to the component library, assets, or build pipeline occur. The maintainers attest that the information provided here is a good-faith assessment of the project’s current security risks and mitigations based on the repository structure and package design at the time of publication. + +### Change since last review + +- **2026-08-21 — Hardened CD pipeline for nuget-publish.** Added SLSA build provenance attestation (`actions/attest-build-provenance`), deterministic builds via `ContinuousIntegrationBuild=true`, exit-code-driven vulnerability scan with downloadable `vuln-report` artifact, and concurrency guard for re-tagged same-version pushes. Accepted-risks entries AR-1 and AR-2 were reviewed and remain applicable; no new accepted risk was introduced. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f2299a0 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,40 @@ +version: 2 +updates: + # NuGet packages (.NET) + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 10 + labels: + - "dependencies" + - "nuget" + commit-message: + prefix: "deps(nuget)" + + # npm (gulp, Playwright, etc.) + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "npm" + commit-message: + prefix: "deps(npm)" + + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "github-actions" + commit-message: + prefix: "deps(actions)" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..955ea67 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,261 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + +jobs: + # ========================================================= + # Job 1: bUnit (matrix) + # ========================================================= + bunit: + name: bUnit (.NET ${{ matrix.dotnet-version }}) + runs-on: ubuntu-latest + timeout-minutes: 20 + + env: + TZ: UTC + LANG: en_US.UTF-8 + + strategy: + fail-fast: false + matrix: + dotnet-version: ['8.0.x', '9.0.x', '10.0.x'] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET ${{ matrix.dotnet-version }} + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ matrix.dotnet-version }} + + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: nuget-${{ runner.os }}-${{ matrix.dotnet-version }}-${{ hashFiles('**/*.*proj') }} + restore-keys: | + nuget-${{ runner.os }}-${{ matrix.dotnet-version }}- + nuget-${{ runner.os }}- + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Cache npm + uses: actions/cache@v4 + with: + path: ~/.npm + key: npm-${{ runner.os }}-${{ hashFiles('**/package.json') }} + restore-keys: npm-${{ runner.os }}- + + - name: Install npm dependencies + run: | + if [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then + npm ci + else + npm install --no-fund --no-audit + fi + + - name: Restore + run: dotnet restore src/Syncfusion.Blazor.Toolkit.csproj + + - name: Build + run: dotnet build src/Syncfusion.Blazor.Toolkit.csproj -c Release --no-restore + + - name: Set timezone and locale for deterministic tests + run: | + sudo ln -fs /usr/share/zoneinfo/UTC /etc/localtime + sudo apt-get update -y + sudo apt-get install -y locales + sudo locale-gen en_US.UTF-8 + export LANG=en_US.UTF-8 + export TZ=UTC + shell: bash + + - name: Run bUnit tests + run: | + mkdir -p TestResults + dotnet test tests/Syncfusion.Blazor.Toolkit.BUnitTest/ \ + -c Release \ + -f net8.0 \ + --logger "trx;LogFileName=bunit-${{ matrix.dotnet-version }}.trx" \ + --logger "html;LogFileName=bunit-${{ matrix.dotnet-version }}.html" \ + --results-directory TestResults \ + --collect:"XPlat Code Coverage" \ + --verbosity normal + + - name: Upload bUnit results + if: always() + uses: actions/upload-artifact@v4 + with: + name: bunit-results-${{ matrix.dotnet-version }} + path: | + TestResults/ + **/coverage.cobertura.xml + retention-days: 14 + if-no-files-found: ignore + + - name: Publish bUnit test results + if: always() + uses: dorny/test-reporter@v1 + continue-on-error: true + with: + name: bUnit (.NET ${{ matrix.dotnet-version }}) + path: 'TestResults/**/*.trx' + reporter: dotnet-trx + fail-on-error: false + fail-on-empty: false + + # ========================================================= + # Job 2: Playwright + # ========================================================= + playwright: + name: Playwright + runs-on: ubuntu-latest + timeout-minutes: 35 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.x + 9.x + 10.x + + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: nuget-${{ runner.os }}-${{ hashFiles('**/*.*proj') }} + restore-keys: nuget-${{ runner.os }}- + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Cache npm + uses: actions/cache@v4 + with: + path: ~/.npm + key: npm-${{ runner.os }}-${{ hashFiles('**/package.json') }} + restore-keys: npm-${{ runner.os }}- + + - name: Install npm dependencies + run: | + if [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then + npm ci + else + npm install --no-fund --no-audit + fi + + - name: Cache Playwright browsers + uses: actions/cache@v4 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('**/package.json') }} + + - name: Install Playwright browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: npx playwright install --with-deps chromium + + - name: Install Playwright system deps + if: steps.playwright-cache.outputs.cache-hit == 'true' + run: npx playwright install-deps chromium + + - name: Restore + run: dotnet restore src/Syncfusion.Blazor.Toolkit.csproj + + - name: Build + run: dotnet build src/Syncfusion.Blazor.Toolkit.csproj -c Release --no-restore + + - name: Run Playwright tests + run: npx playwright test --reporter=html,line + env: + CI: true + + - name: Upload Playwright report + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: | + playwright-report/ + test-results/ + retention-days: 14 + if-no-files-found: ignore + + # ========================================================= + # Job 3: Summary + # ========================================================= + summary: + name: CI Summary + runs-on: ubuntu-latest + needs: [bunit, playwright] + if: always() + permissions: + contents: read + pull-requests: write + + steps: + - name: Check results and fail if needed + run: | + echo "bUnit result: ${{ needs.bunit.result }}" + echo "Playwright result: ${{ needs.playwright.result }}" + + if [[ "${{ needs.bunit.result }}" != "success" || "${{ needs.playwright.result }}" != "success" ]]; then + echo "One or more jobs failed → failing the workflow" + exit 1 + fi + + echo "All jobs succeeded" + + - name: Post summary comment (PRs only) + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const bunitOk = '${{ needs.bunit.result }}' === 'success'; + const pwOk = '${{ needs.playwright.result }}' === 'success'; + const overall = bunitOk && pwOk ? '✅ All checks passed' : '❌ Some checks failed'; + + const body = `### CI Summary + + | Job | Status | + |-----|--------| + | **bUnit** (.NET 8 / 9 / 10) | ${bunitOk ? '✅ Passed' : '❌ Failed'} | + | **Playwright** | ${pwOk ? '✅ Passed' : '❌ Failed'} | + + **Overall:** ${overall} + `; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body + }); \ No newline at end of file diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..9657b4b --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,63 @@ +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '0 6 * * 1' # Every Monday 06:00 UTC + workflow_dispatch: + +concurrency: + group: codeql-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + timeout-minutes: 360 + + permissions: + security-events: write + packages: read + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: csharp + build-mode: autobuild # Best for .NET + - language: javascript-typescript + build-mode: none # For gulp / package.json / Playwright + - language: actions + build-mode: none # For .github/workflows/*.yml + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # Uncomment if you want extra queries later: + # queries: security-extended,security-and-quality + + # Only needed when build-mode is "manual" + - name: Manual build (csharp) + if: matrix.build-mode == 'manual' + run: | + dotnet restore ./Syncfusion.Blazor.Toolkit.slnx + dotnet build ./Syncfusion.Blazor.Toolkit.slnx -c Release --no-restore + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{ matrix.language }}" \ No newline at end of file diff --git a/.github/workflows/nuget-publish.yml b/.github/workflows/nuget-publish.yml index 16ced6f..b55d35d 100644 --- a/.github/workflows/nuget-publish.yml +++ b/.github/workflows/nuget-publish.yml @@ -1,24 +1,28 @@ name: Publish NuGet -# Triggered by version tags: v1.0.0, v1.2.3-preview, etc. on: push: tags: - 'v[0-9]+.[0-9]+.[0-9]+' - 'v[0-9]+.[0-9]+.[0-9]+-*' +concurrency: + group: nuget-publish-${{ github.ref }} + cancel-in-progress: false + jobs: publish: runs-on: ubuntu-latest permissions: contents: read - id-token: write # required for both NuGet OIDC trusted publishing and Azure Workload Identity + id-token: write + attestations: write steps: - name: Checkout uses: actions/checkout@v4 with: - fetch-depth: 0 # full history required for SourceLink commit SHA + fetch-depth: 0 - name: Setup .NET uses: actions/setup-dotnet@v4 @@ -28,40 +32,156 @@ jobs: 9.x 10.x - # Decode the strong-name key stored as a base64 GitHub Secret. - # To create the secret: certutil -encode sf.snk sf.snk.b64 (or base64 sf.snk) + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Cache npm + uses: actions/cache@v4 + with: + path: ~/.npm + key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + npm-${{ runner.os }}- + + - name: Install npm dependencies + run: | + if [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then + npm ci + else + npm install --no-fund --no-audit + fi + + - name: Run gulp themes task + run: npx gulp blazor-toolkit-themes + - name: Write strong-name key run: echo "${{ secrets.STRONG_NAME_KEY_BASE64 }}" | base64 --decode > src/sf.snk - name: Restore - run: dotnet restore src/Syncfusion.Blazor.Toolkit.csproj + run: dotnet restore src/Syncfusion.Blazor.Toolkit.csproj -p:ContinuousIntegrationBuild=true - # Fail the release if any direct or transitive dependency has a known CVE. - name: Dependency vulnerability scan run: | - dotnet list src/Syncfusion.Blazor.Toolkit.csproj package --vulnerable --include-transitive 2>&1 | tee vuln-report.txt - if grep -q "has the following vulnerable packages" vuln-report.txt; then - echo "::error::Vulnerable packages detected — release blocked. See vuln-report.txt for details." + set -euo pipefail + # Capture both formats. Exit non-zero captures the actual gate. Captures, gate, output. + set +e + dotnet list src/Syncfusion.Blazor.Toolkit.csproj package --vulnerable --include-transitive \ + --format json > vuln-report.json + vuln_exit=$? + dotnet list src/Syncfusion.Blazor.Toolkit.csproj package --vulnerable --include-transitive \ + > vuln-report.txt 2>&1 || true + set -e + + if [ "$vuln_exit" -ne 0 ]; then + echo "::error::Vulnerable packages detected — release blocked. See 'vuln-report' artifact for details." + echo "::group::Vulnerability scan output" + cat vuln-report.txt + echo "::endgroup::" exit 1 fi + - name: Upload vulnerability report + if: always() + uses: actions/upload-artifact@v4 + with: + name: vuln-report + path: | + vuln-report.json + vuln-report.txt + retention-days: 14 + if-no-files-found: ignore + + - name: Resolve next version + id: version + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + NUGET_PKG="Syncfusion.Blazor.Toolkit" + REPO="${GITHUB_REPOSITORY}" + + # --- 1. Latest version on nuget.org (flat-container). --- + # flat-container returns absolute URLs to each version's .nupkg; we only need the + # list itself to compute the floor. A non-fatal warning is emitted if the lookup + # fails (e.g. transient CDN issue) so a stale GitHub-only floor still keeps the + # release moving. + nuget_latest="0.0.0" + if nuget_response=$(curl -fsS "https://api.nuget.org/v3-flatcontainer/${NUGET_PKG}/index.json" 2>/dev/null); then + nuget_latest=$(echo "$nuget_response" \ + | jq -r '.versions[]?' \ + | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' \ + | sort -V \ + | tail -n 1) || true + nuget_latest="${nuget_latest:-0.0.0}" + else + echo "::warning::Could not query nuget.org for latest version of ${NUGET_PKG} — falling back to '0.0.0'." + fi + + # --- 2. Latest GitHub release tag for this repo. --- + # Fetched via the GitHub REST API. Drafts and prereleases are filtered out so a + # `v1.1.0-rc.1` tag never blocks a `v1.0.x` maintenance bump. Authenticated via + # the workflow's built-in GITHUB_TOKEN to lift the 60-req/hr anonymous rate limit. + releases_latest="0.0.0" + if releases_response=$(curl -fsS \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${GH_TOKEN}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/${REPO}/releases?per_page=20" 2>/dev/null); then + releases_latest=$(echo "$releases_response" \ + | jq -r '.[]? | select(.draft == false and .prerelease == false) | .tag_name' \ + | sed 's/^v//' \ + | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' \ + | sort -V \ + | tail -n 1) || true + releases_latest="${releases_latest:-0.0.0}" + else + echo "::warning::Could not query GitHub releases for ${REPO} — falling back to '0.0.0'." + fi + + # --- 3. Pick the highest of the two floors. --- + # `sort -V` gives correct numeric ordering; `tail -n 1` picks the max. + floor=$(printf '%s\n%s\n' "${nuget_latest}" "${releases_latest}" | sort -V | tail -n 1) + floor="${floor:-0.0.0}" + + # --- 4. Bump the patch component by 1. --- + # If both lookups returned 0.0.0 the result is 0.0.1 — safe because nuget.org + # would have rejected a republish under the hardcoded "1.0.0" anyway. + IFS='.' read -r f_major f_minor f_patch <<< "${floor}" + next_major="${f_major:-0}" + next_minor="${f_minor:-0}" + next_patch="$((${f_patch:-0} + 1))" + next_version="${next_major}.${next_minor}.${next_patch}" + + echo "nuget_latest = ${nuget_latest}" + echo "releases_latest = ${releases_latest}" + echo "floor = ${floor}" + echo "next_version = ${next_version}" + echo "next_version=${next_version}" >> "$GITHUB_OUTPUT" + - name: Build - run: dotnet build src/Syncfusion.Blazor.Toolkit.csproj -c Release --no-restore + run: dotnet build src/Syncfusion.Blazor.Toolkit.csproj -c Release --no-restore -p:ContinuousIntegrationBuild=true -p:PackageVersion=${{ steps.version.outputs.next_version }} - name: Pack - run: dotnet pack src/Syncfusion.Blazor.Toolkit.csproj -c Release --no-build -o nupkg + run: dotnet pack src/Syncfusion.Blazor.Toolkit.csproj -c Release --no-build -o nupkg -p:ContinuousIntegrationBuild=true -p:PackageVersion=${{ steps.version.outputs.next_version }} - name: Remove strong-name key if: always() run: rm -f src/sf.snk + - name: Attest build provenance + uses: actions/attest-build-provenance@v2 + with: + subject-path: | + nupkg/*.nupkg + nupkg/*.snupkg + provenance-repository-refs: ${{ github.ref }} + - name: Install NuGetKeyVaultSignTool run: dotnet tool install --global NuGetKeyVaultSignTool - # Log in to Azure using Workload Identity Federation — no client secret needed. - # Prerequisite: add a federated credential for this repo+workflow on the service principal in Azure. - # Non-secret config values (AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_KEY_VAULT_URL, - # AZURE_KEY_VAULT_CERT_NAME) are stored as GitHub Actions repository *variables* (vars.*), not secrets. - name: Azure login (OIDC) uses: azure/login@v2 with: @@ -69,7 +189,6 @@ jobs: tenant-id: ${{ vars.AZURE_TENANT_ID }} allow-no-subscriptions: true - # DefaultAzureCredential picks up the ambient Workload Identity token set by azure/login above. - name: Sign NuGet packages run: | NuGetKeyVaultSignTool sign nupkg/*.nupkg \ @@ -79,8 +198,6 @@ jobs: --azure-key-vault-url "${{ vars.AZURE_KEY_VAULT_URL }}" \ --azure-key-vault-certificate "${{ vars.AZURE_KEY_VAULT_CERT_NAME }}" - # Exchange the GitHub OIDC token for a short-lived NuGet.org API token (trusted publishing). - # Prerequisite: configure a trusted publisher on nuget.org for this repo + workflow file. - name: Push to NuGet.org run: | NUGET_TOKEN=$(curl -sS \ diff --git a/src/Base/Globalization.cs b/src/Base/Globalization.cs index 392ca5a..797d54a 100644 --- a/src/Base/Globalization.cs +++ b/src/Base/Globalization.cs @@ -79,6 +79,7 @@ internal static string GetDateFormat(T date, string? format = null) { return string.Empty; } + dateCulture = dateCulture.Replace('\u202F', ' '); dateCulture = GetNativeDigits(dateCulture, currentCulture.NumberFormat.NativeDigits); return dateCulture; } diff --git a/src/Components/Calendars/DateTimePicker/SfDateTimePicker.razor.cs b/src/Components/Calendars/DateTimePicker/SfDateTimePicker.razor.cs index 523e039..114eb94 100644 --- a/src/Components/Calendars/DateTimePicker/SfDateTimePicker.razor.cs +++ b/src/Components/Calendars/DateTimePicker/SfDateTimePicker.razor.cs @@ -1073,15 +1073,14 @@ internal override async Task UpdateCalendarPropertyAsync(string key, object? dat TimeSpan offset = ((DateTimeOffset)dateTimeValue).Offset; int hour = ((DateTimeOffset)dateTimeValue).Hour; int minute = ((DateTimeOffset)dateTimeValue).Minute; - int second = ((DateTimeOffset)dateTimeValue).Second; - int milliSecond = ((DateTimeOffset)dateTimeValue).Millisecond; - dateValue = new DateTimeOffset(year, month, day, hour, minute, second, milliSecond, offset); + dateValue = new DateTimeOffset(year, month, day, hour, minute, 0, 0, offset); } else { if (dateTimeValue is not null) { - dateValue = new DateTime(((DateTime)dateTimeValue).Ticks, DateTimeKind.Local); + DateTime dt = (DateTime)dateTimeValue; + dateValue = new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0, DateTimeKind.Local); } } await UpdateValueAsync(dateValue).ConfigureAwait(false); diff --git a/src/Components/Calendars/TimePicker/SfTimePicker.razor.cs b/src/Components/Calendars/TimePicker/SfTimePicker.razor.cs index a272df6..8fd3f75 100644 --- a/src/Components/Calendars/TimePicker/SfTimePicker.razor.cs +++ b/src/Components/Calendars/TimePicker/SfTimePicker.razor.cs @@ -914,6 +914,28 @@ private async Task GenerateListAsync() { formatString = string.IsNullOrEmpty(Format) ? "HH:mm:ss" : formatString.Replace("hh", "HH", StringComparison.Ordinal); } + if (DatePart == default) + { + if (Value is not null && !IsTimeSpanType()) + { + try + { + DateTime source = ConvertDate(Value); + if (source != default) + { + DatePart = source.Date; + } + } + catch + { + // Fall through to today's date + } + } + if (DatePart == default) + { + DatePart = DateTime.Today; + } + } while (end >= start) { DateTime listDateTime = new(DatePart.Year, DatePart.Month, DatePart.Day, start.Hours, start.Minutes, start.Seconds, start.Milliseconds, DatePart.Kind); diff --git a/src/Syncfusion.Blazor.Toolkit.csproj b/src/Syncfusion.Blazor.Toolkit.csproj index 46631cd..7ee6872 100644 --- a/src/Syncfusion.Blazor.Toolkit.csproj +++ b/src/Syncfusion.Blazor.Toolkit.csproj @@ -9,7 +9,8 @@ true true net8.0;net9.0;net10.0 - Copyright 2001 - 2026 Syncfusion® Inc. + + Copyright 2001 - $([System.DateTime]::UtcNow.Year) Syncfusion® Inc. Syncfusion Blazor Toolkit Components The Syncfusion® Toolkit for Blazor is a high-performance, open-source collection of lightweight UI components designed to accelerate Blazor application development (Server and WebAssembly). These controls help developers build modern, responsive, and feature-rich web applications faster, with clean code and excellent performance. syncfusion_logo.png diff --git a/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DatePicker/DateFormat.cs b/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DatePicker/DateFormat.cs index 60c4a7e..d9f138b 100644 --- a/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DatePicker/DateFormat.cs +++ b/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DatePicker/DateFormat.cs @@ -31,8 +31,9 @@ private string GetDateFormat(T date, string? format = null, string? culture = var currentCulture = CultureInfo.CurrentCulture; IFormattable? dateValue = date as IFormattable; var dateCulture = dateValue?.ToString(format, currentCulture); + dateCulture = dateCulture?.Replace('\u202F', ' '); dateCulture = GetNativeDigits(dateCulture!, currentCulture.NumberFormat.NativeDigits); - return dateCulture; + return dateCulture!; } catch (Exception e) { diff --git a/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DateTimePicker/DateTimeFormat.cs b/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DateTimePicker/DateTimeFormat.cs index 5def04f..4d32d4a 100644 --- a/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DateTimePicker/DateTimeFormat.cs +++ b/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DateTimePicker/DateTimeFormat.cs @@ -34,6 +34,7 @@ private string GetDateFormat(T date, string format = null, string culture = n var currentCulture = CultureInfo.CurrentCulture; IFormattable dateValue = date as IFormattable; var dateCulture = dateValue.ToString(format, currentCulture); + dateCulture = dateCulture?.Replace('\u202F', ' '); dateCulture = GetNativeDigits(dateCulture, currentCulture.NumberFormat.NativeDigits); return dateCulture; } diff --git a/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DateTimePicker/DateTimeNavigation.cs b/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DateTimePicker/DateTimeNavigation.cs index 3b46e6c..173b9df 100644 --- a/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DateTimePicker/DateTimeNavigation.cs +++ b/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DateTimePicker/DateTimeNavigation.cs @@ -31,6 +31,7 @@ private string GetDateFormat(T date, string format = null, string culture = n var currentCulture = CultureInfo.CurrentCulture; IFormattable dateValue = date as IFormattable; var dateCulture = dateValue.ToString(format, currentCulture); + dateCulture = dateCulture?.Replace('\u202F', ' '); dateCulture = GetNativeDigits(dateCulture, currentCulture.NumberFormat.NativeDigits); return dateCulture; } diff --git a/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DateTimePicker/DateTimePicker.cs b/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DateTimePicker/DateTimePicker.cs index 51569e8..de33cf4 100644 --- a/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DateTimePicker/DateTimePicker.cs +++ b/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DateTimePicker/DateTimePicker.cs @@ -246,6 +246,7 @@ private string GetDateFormat(T date, string format = null, string culture = n var currentCulture = CultureInfo.CurrentCulture; IFormattable dateValue = date as IFormattable; var dateCulture = dateValue.ToString(format, currentCulture); + dateCulture = dateCulture?.Replace('\u202F', ' '); dateCulture = GetNativeDigits(dateCulture, currentCulture.NumberFormat.NativeDigits); return dateCulture; } diff --git a/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DateTimePicker/DateTimePickerEvents.cs b/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DateTimePicker/DateTimePickerEvents.cs index 0ffc76e..58cf7ba 100644 --- a/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DateTimePicker/DateTimePickerEvents.cs +++ b/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DateTimePicker/DateTimePickerEvents.cs @@ -33,6 +33,7 @@ private string GetDateFormat(T date, string format = null, string culture = n var currentCulture = CultureInfo.CurrentCulture; IFormattable dateValue = date as IFormattable; var dateCulture = dateValue.ToString(format, currentCulture); + dateCulture = dateCulture?.Replace('\u202F', ' '); dateCulture = GetNativeDigits(dateCulture, currentCulture.NumberFormat.NativeDigits); return dateCulture; } diff --git a/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DateTimePicker/DateTimePickerMask.cs b/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DateTimePicker/DateTimePickerMask.cs index e41c7d1..94a13a9 100644 --- a/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DateTimePicker/DateTimePickerMask.cs +++ b/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DateTimePicker/DateTimePickerMask.cs @@ -113,6 +113,7 @@ private string GetDateFormat(T date, string format = null, string culture = n var currentCulture = CultureInfo.CurrentCulture; IFormattable dateValue = date as IFormattable; var dateCulture = dateValue.ToString(format, currentCulture); + dateCulture = dateCulture?.Replace('\u202F', ' '); dateCulture = GetNativeDigits(dateCulture, currentCulture.NumberFormat.NativeDigits); return dateCulture; } diff --git a/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DateTimePicker/DateTimeStrictMode.cs b/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DateTimePicker/DateTimeStrictMode.cs index 5a29fa4..c9470e2 100644 --- a/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DateTimePicker/DateTimeStrictMode.cs +++ b/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Calendars/DateTimePicker/DateTimeStrictMode.cs @@ -33,6 +33,7 @@ private string GetDateFormat(T date, string format = null, string culture = n var currentCulture = CultureInfo.CurrentCulture; IFormattable dateValue = date as IFormattable; var dateCulture = dateValue.ToString(format, currentCulture); + dateCulture = dateCulture?.Replace('\u202F', ' '); dateCulture = GetNativeDigits(dateCulture, currentCulture.NumberFormat.NativeDigits); return dateCulture; }