diff --git a/.browserslistrc b/.browserslistrc index c71c8b9c717b..cddd23005246 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -9,3 +9,4 @@ Firefox ESR iOS >= 12 Safari >= 12 not Explorer <= 11 +not kaios <= 2.5 # fix floating label issues in Firefox (see https://github.com/postcss/autoprefixer/issues/1533) diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json index e641382b80b1..6f680664ca67 100644 --- a/.bundlewatch.config.json +++ b/.bundlewatch.config.json @@ -2,47 +2,47 @@ "files": [ { "path": "./dist/css/bootstrap-grid.css", - "maxSize": "7.25 kB" + "maxSize": "6.5 kB" }, { "path": "./dist/css/bootstrap-grid.min.css", - "maxSize": "6.5 kB" + "maxSize": "6.0 kB" }, { "path": "./dist/css/bootstrap-reboot.css", - "maxSize": "2 kB" + "maxSize": "3.5 kB" }, { "path": "./dist/css/bootstrap-reboot.min.css", - "maxSize": "2 kB" + "maxSize": "3.25 kB" }, { "path": "./dist/css/bootstrap-utilities.css", - "maxSize": "7.75 kB" + "maxSize": "11.75 kB" }, { "path": "./dist/css/bootstrap-utilities.min.css", - "maxSize": "6.85 kB" + "maxSize": "10.75 kB" }, { "path": "./dist/css/bootstrap.css", - "maxSize": "25.5 kB" + "maxSize": "32.5 kB" }, { "path": "./dist/css/bootstrap.min.css", - "maxSize": "23.25 kB" + "maxSize": "30.25 kB" }, { "path": "./dist/js/bootstrap.bundle.js", - "maxSize": "42 kB" + "maxSize": "43.0 kB" }, { "path": "./dist/js/bootstrap.bundle.min.js", - "maxSize": "22.25 kB" + "maxSize": "23.5 kB" }, { "path": "./dist/js/bootstrap.esm.js", - "maxSize": "27.5 kB" + "maxSize": "28.0 kB" }, { "path": "./dist/js/bootstrap.esm.min.js", @@ -50,11 +50,11 @@ }, { "path": "./dist/js/bootstrap.js", - "maxSize": "28 kB" + "maxSize": "28.75 kB" }, { "path": "./dist/js/bootstrap.min.js", - "maxSize": "15.75 kB" + "maxSize": "16.25 kB" } ], "ci": { diff --git a/.cspell.json b/.cspell.json new file mode 100644 index 000000000000..d2434c30a608 --- /dev/null +++ b/.cspell.json @@ -0,0 +1,131 @@ +{ + "version": "0.2", + "words": [ + "affordance", + "allowfullscreen", + "Analyser", + "autohide", + "autohiding", + "autoplay", + "autoplays", + "autoplaying", + "blazingly", + "Blockquotes", + "Bootstrappers", + "borderless", + "Brotli", + "browserslist", + "browserslistrc", + "btncheck", + "btnradio", + "callout", + "callouts", + "camelCase", + "clearfix", + "Codesniffer", + "combinator", + "Contentful", + "Cpath", + "Crossfade", + "crossfading", + "cssgrid", + "Csvg", + "Datalists", + "Deque", + "discoverability", + "docsearch", + "docsref", + "dropend", + "dropleft", + "dropright", + "dropstart", + "dropup", + "dgst", + "errorf", + "favicon", + "favicons", + "fieldsets", + "flexbox", + "fullscreen", + "getbootstrap", + "Grayscale", + "Hoverable", + "hreflang", + "hstack", + "importmap", + "jsdelivr", + "Jumpstart", + "keyframes", + "libera", + "libman", + "Libsass", + "lightboxes", + "Lowercased", + "markdownify", + "mediaqueries", + "minifiers", + "misfunction", + "mkdir", + "monospace", + "mouseleave", + "navbars", + "navs", + "Neue", + "noindex", + "Noto", + "offcanvas", + "offcanvases", + "Packagist", + "popperjs", + "prebuild", + "prefersreducedmotion", + "prepended", + "printf", + "rects", + "relref", + "rgba", + "roboto", + "RTLCSS", + "ruleset", + "sassrc", + "screenreaders", + "scrollbars", + "scrollspy", + "Segoe", + "semibold", + "socio", + "srcset", + "stackblitz", + "stickied", + "Stylelint", + "subnav", + "tabbable", + "textareas", + "toggleable", + "topbar", + "touchend", + "twbs", + "unitless", + "unstylable", + "unstyled", + "Uppercased", + "urlize", + "urlquery", + "vbtn", + "viewports", + "Vite", + "vstack", + "walkthroughs", + "WCAG", + "zindex" + ], + "language": "en-US", + "ignorePaths": [ + ".cspell.json", + "dist/", + "*.min.*", + "**/*rtl*", + "**/tests/**" + ], + "useGitignore": true +} diff --git a/.eslintignore b/.eslintignore index a18b03a5df54..e42161487a5b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,5 +2,8 @@ **/dist/ **/vendor/ /_site/ +/site/public/ /js/coverage/ /site/static/sw.js +/site/static/docs/**/assets/sw.js +/site/layouts/partials/ diff --git a/.eslintrc.json b/.eslintrc.json index 0174b84d02c6..da686166f5a8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -14,6 +14,35 @@ "error", "never" ], + "import/extensions": [ + "error", + "ignorePackages", + { + "js": "always" + } + ], + "import/first": "error", + "import/newline-after-import": "error", + "import/no-absolute-path": "error", + "import/no-amd": "error", + "import/no-cycle": [ + "error", + { + "ignoreExternal": true + } + ], + "import/no-duplicates": "error", + "import/no-extraneous-dependencies": "error", + "import/no-mutable-exports": "error", + "import/no-named-as-default": "error", + "import/no-named-as-default-member": "error", + "import/no-named-default": "error", + "import/no-self-import": "error", + "import/no-unassigned-import": [ + "error" + ], + "import/no-useless-path-segments": "error", + "import/order": "error", "indent": [ "error", 2, @@ -22,6 +51,7 @@ "SwitchCase": 1 } ], + "logical-assignment-operators": "off", "max-params": [ "warn", 5 @@ -37,6 +67,7 @@ } ], "no-console": "error", + "no-negated-condition": "off", "object-curly-spacing": [ "error", "always" @@ -45,26 +76,166 @@ "error", "after" ], + "prefer-object-has-own": "off", + "prefer-template": "error", "semi": [ "error", "never" ], - "unicorn/consistent-function-scoping": "off", + "strict": "error", "unicorn/explicit-length-check": "off", + "unicorn/filename-case": "off", + "unicorn/no-anonymous-default-export": "off", "unicorn/no-array-callback-reference": "off", - "unicorn/no-array-for-each": "off", "unicorn/no-array-method-this-argument": "off", - "unicorn/no-for-loop": "off", "unicorn/no-null": "off", + "unicorn/no-typeof-undefined": "off", "unicorn/no-unused-properties": "error", - "unicorn/no-useless-undefined": "off", "unicorn/numeric-separators-style": "off", "unicorn/prefer-array-flat": "off", + "unicorn/prefer-at": "off", "unicorn/prefer-dom-node-dataset": "off", + "unicorn/prefer-global-this": "off", "unicorn/prefer-module": "off", - "unicorn/prefer-prototype-methods": "off", "unicorn/prefer-query-selector": "off", "unicorn/prefer-spread": "off", + "unicorn/prefer-string-raw": "off", + "unicorn/prefer-string-replace-all": "off", + "unicorn/prefer-structured-clone": "off", "unicorn/prevent-abbreviations": "off" - } + }, + "overrides": [ + { + "files": [ + "build/**" + ], + "env": { + "browser": false, + "node": true + }, + "parserOptions": { + "sourceType": "module" + }, + "rules": { + "no-console": "off", + "unicorn/prefer-top-level-await": "off" + } + }, + { + "files": [ + "js/**" + ], + "parserOptions": { + "sourceType": "module" + } + }, + { + "files": [ + "js/tests/*.js", + "js/tests/integration/rollup*.js" + ], + "env": { + "node": true + }, + "parserOptions": { + "sourceType": "script" + } + }, + { + "files": [ + "js/tests/unit/**" + ], + "env": { + "jasmine": true + }, + "rules": { + "no-console": "off", + "unicorn/consistent-function-scoping": "off", + "unicorn/no-useless-undefined": "off", + "unicorn/prefer-add-event-listener": "off" + } + }, + { + "files": [ + "js/tests/visual/**" + ], + "plugins": [ + "html" + ], + "settings": { + "html/html-extensions": [ + ".html" + ] + }, + "rules": { + "no-console": "off", + "no-new": "off", + "unicorn/no-array-for-each": "off" + } + }, + { + "files": [ + "scss/tests/**" + ], + "env": { + "node": true + }, + "parserOptions": { + "sourceType": "script" + } + }, + { + "files": [ + "site/**" + ], + "env": { + "browser": true, + "node": false + }, + "parserOptions": { + "sourceType": "script", + "ecmaVersion": 2019 + }, + "rules": { + "no-new": "off", + "unicorn/no-array-for-each": "off" + } + }, + { + "files": [ + "site/src/assets/application.js", + "site/src/assets/partials/*.js", + "site/src/assets/search.js", + "site/src/assets/snippets.js", + "site/src/assets/stackblitz.js", + "site/src/plugins/*.js" + ], + "parserOptions": { + "sourceType": "module", + "ecmaVersion": 2020 + } + }, + { + "files": [ + "**/*.md" + ], + "plugins": [ + "markdown" + ], + "processor": "markdown/markdown" + }, + { + "files": [ + "**/*.md/*.js", + "**/*.md/*.mjs" + ], + "extends": "plugin:markdown/recommended-legacy", + "parserOptions": { + "sourceType": "module" + }, + "rules": { + "unicorn/prefer-node-protocol": "off" + } + } + ] } diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index be4ad836de22..73b21ac047f3 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to Bootstrap -Looking to contribute something to Bootstrap? **Here's how you can help.** +Looking to contribute something to Bootstrap? **Here’s how you can help.** Please take a moment to review this document in order to make the contribution process easy and effective for everyone involved. @@ -18,24 +18,26 @@ the preferred channel for [bug reports](#bug-reports), [features requests](#feat and [submitting pull requests](#pull-requests), but please respect the following restrictions: -* Please **do not** use the issue tracker for personal support requests. Stack - Overflow ([`bootstrap-5`](https://stackoverflow.com/questions/tagged/bootstrap-5) tag), - [Slack](https://bootstrap-slack.herokuapp.com/) or [IRC](/README.md#community) are better places to get help. +- Please **do not** use the issue tracker for personal support requests. Stack Overflow ([`bootstrap-5`](https://stackoverflow.com/questions/tagged/bootstrap-5) tag), [our GitHub Discussions](https://github.com/twbs/bootstrap/discussions) or [IRC](/README.md#community) are better places to get help. -* Please **do not** derail or troll issues. Keep the discussion on topic and +- Please **do not** derail or troll issues. Keep the discussion on topic and respect the opinions of others. -* Please **do not** post comments consisting solely of "+1" or ":thumbsup:". - Use [GitHub's "reactions" feature](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) +- Please **do not** post comments consisting solely of "+1" or ":thumbsup:". + Use [GitHub's "reactions" feature](https://github.blog/news-insights/product-news/add-reactions-to-pull-requests-issues-and-comments/) instead. We reserve the right to delete comments which violate this rule. -* Please **do not** open issues regarding the official themes offered on . - Instead, please email any questions or feedback regarding those themes to `themes AT getbootstrap DOT com`. + +## Issues assignment + +The core team will be looking at the open issues, analyze them, and provide guidance on how to proceed. **Issues won’t be assigned to anyone outside the core team.** However, contributors are welcome to participate in the discussion and provide their input on how to best solve the issue, and even submit a PR if they want to. Please wait that the issue is ready to be worked on before submitting a PR, we don’t want to waste your time. + +Please keep in mind that the core team is small, has limited resources and that we are not always able to respond immediately. We will try to provide feedback as soon as possible, but please be patient. If you don’t get a response immediately, it doesn’t mean that we are ignoring you or that we don’t care about your issue or PR. We will get back to you as soon as we can. ## Issues and labels -Our bug tracker utilizes several labels to help organize and identify issues. Here's what they represent and how we use them: +Our bug tracker utilizes several labels to help organize and identify issues. Here’s what they represent and how we use them: - `browser bug` - Issues that are reported to us, but actually are the result of a browser-specific bug. These are diagnosed with reduced test cases and result in an issue opened on that browser's own bug tracker. - `confirmed` - Issues that have been confirmed with a reduced test case and identify a bug in Bootstrap. @@ -59,7 +61,7 @@ Good bug reports are extremely helpful, so thanks! Guidelines for bug reports: 0. **[Validate your HTML](https://html5.validator.nu/)** to ensure your - problem isn't caused by a simple error in your own code. + problem isn’t caused by a simple error in your own code. 1. **Use the GitHub issue search** — check if the issue has already been reported. @@ -69,10 +71,10 @@ Guidelines for bug reports: 3. **Isolate the problem** — ideally create a [reduced test case](https://css-tricks.com/reduced-test-cases/) and a live example. - [This JS Bin](https://jsbin.com/lolome/edit?html,output) is a helpful template. + These [v4 CodePen](https://codepen.io/team/bootstrap/pen/yLabNQL) and [v5 CodePen](https://codepen.io/team/bootstrap/pen/qBamdLj) are helpful templates. -A good bug report shouldn't leave others needing to chase you up for more +A good bug report shouldn’t leave others needing to chase you up for more information. Please try to be as detailed as possible in your report. What is your environment? What steps will reproduce the issue? What browser(s) and OS experience the problem? Do other browsers show the bug differently? What @@ -103,17 +105,17 @@ Sometimes bugs reported to us are actually caused by bugs in the browser(s) them | Vendor(s) | Browser(s) | Rendering engine | Bug reporting website(s) | Notes | | ------------- | ---------------------------- | ---------------- | ------------------------------------------------------ | -------------------------------------------------------- | -| Mozilla | Firefox | Gecko | https://bugzilla.mozilla.org/enter_bug.cgi | "Core" is normally the right product option to choose. | -| Apple | Safari | WebKit | https://bugs.webkit.org/enter_bug.cgi?product=WebKit | In Apple's bug reporter, choose "Safari" as the product. | -| Google, Opera | Chrome, Chromium, Opera v15+ | Blink | https://bugs.chromium.org/p/chromium/issues/list | Click the "New issue" button. | -| Microsoft | Edge | Blink | https://developer.microsoft.com/en-us/microsoft-edge/ | Go to "Help > Send Feedback" from the browser | +| Mozilla | Firefox | Gecko | | "Core" is normally the right product option to choose. | +| Apple | Safari | WebKit | | In Apple’s bug reporter, choose "Safari" as the product. | +| Google, Opera | Chrome, Chromium, Opera v15+ | Blink | | Click the "New issue" button. | +| Microsoft | Edge | Blink | | Go to "Help > Send Feedback" from the browser | ## Feature requests Feature requests are welcome. But take a moment to find out whether your idea -fits with the scope and aims of the project. It's up to *you* to make a strong -case to convince the project's developers of the merits of this feature. Please +fits with the scope and aims of the project. It’s up to _you_ to make a strong +case to convince the project’s developers of the merits of this feature. Please provide as much detail and context as possible. @@ -126,8 +128,8 @@ commits. **Please ask first** before embarking on any **significant** pull request (e.g. implementing features, refactoring code, porting to a different language), otherwise you risk spending a lot of time working on something that the -project's developers might not want to merge into the project. For trivial -things, or things that don't require a lot of your time, you can go ahead and +project’s developers might not want to merge into the project. For trivial +things, or things that don’t require a lot of your time, you can go ahead and make a PR. Please adhere to the [coding guidelines](#code-guidelines) used throughout the @@ -139,7 +141,7 @@ any dist files (`dist/` or `js/dist`).** Those files are automatically generated edit the source files in [`/bootstrap/scss/`](https://github.com/twbs/bootstrap/tree/main/scss) and/or [`/bootstrap/js/src/`](https://github.com/twbs/bootstrap/tree/main/js/src) instead. -Similarly, when contributing to Bootstrap's documentation, you should edit the +Similarly, when contributing to Bootstrap’s documentation, you should edit the documentation source files in [the `/bootstrap/site/content/docs/` directory of the `main` branch](https://github.com/twbs/bootstrap/tree/main/site/content/docs). **Do not edit the `gh-pages` branch.** That branch is generated from the @@ -167,33 +169,47 @@ included in the project: git pull upstream main ``` -3. Create a new topic branch (off the main project development branch) to +3. Install or update project dependencies with npm: + + ```bash + npm install + ``` + +4. Create a new topic branch (off the main project development branch) to contain your feature, change, or fix: ```bash git checkout -b ``` -4. Commit your changes in logical chunks. Please adhere to these [git commit +5. Commit your changes in logical chunks. Please adhere to these [git commit message guidelines](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) - or your code is unlikely be merged into the main project. Use Git's + or your code is unlikely be merged into the main project. Use Git’s [interactive rebase](https://help.github.com/articles/about-git-rebase/) feature to tidy up your commits before making them public. -5. Locally merge (or rebase) the upstream development branch into your topic branch: +6. Ensure your changes compile the dist CSS and JS files in the `dist/` directory. Verify + the build succeeds locally without errors. + + ```bash + npm run dist + ``` + +7. Locally merge (or rebase) the upstream development branch into your topic branch: ```bash git pull [--rebase] upstream main ``` -6. Push your topic branch up to your fork: +8. Commit your changes, but **do not push compiled CSS and JS files in `dist` and `js/dist`**. + Push your topic branch up to your fork: ```bash git push origin ``` -7. [Open a Pull Request](https://help.github.com/articles/about-pull-requests/) - with a clear title and description against the `main` branch. +9. [Open a pull request](https://help.github.com/articles/about-pull-requests/) + with a clear title and description against the `main` branch. **IMPORTANT**: By submitting a patch, you agree to allow the project owners to license your work under the terms of the [MIT License](../LICENSE) (if it @@ -209,15 +225,15 @@ includes code changes) and under the terms of the [Adhere to the Code Guide.](https://codeguide.co/#html) - Use tags and elements appropriate for an HTML5 doctype (e.g., self-closing tags). -- Use CDNs and HTTPS for third-party JS when possible. We don't use protocol-relative URLs in this case because they break when viewing the page locally via `file://`. +- Use CDNs and HTTPS for third-party JS when possible. We don’t use protocol-relative URLs in this case because they break when viewing the page locally via `file://`. - Use [WAI-ARIA](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA) attributes in documentation examples to promote accessibility. ### CSS [Adhere to the Code Guide.](https://codeguide.co/#css) -- When feasible, default color palettes should comply with [WCAG color contrast guidelines](https://www.w3.org/TR/WCAG20/#visual-audio-contrast). -- Except in rare cases, don't remove default `:focus` styles (via e.g. `outline: none;`) without providing alternative styles. See [this A11Y Project post](https://www.a11yproject.com/posts/2013-01-25-never-remove-css-outlines/) for more details. +- When feasible, default color palettes should comply with [WCAG color contrast guidelines](https://www.w3.org/TR/WCAG/#distinguishable). +- Except in rare cases, don’t remove default `:focus` styles (via e.g. `outline: none;`) without providing alternative styles. See [this A11Y Project post](https://www.a11yproject.com/posts/2013-01-25-never-remove-css-outlines/) for more details. ### JS @@ -236,4 +252,4 @@ Run `npm run test` before committing to ensure your changes follow our coding st By contributing your code, you agree to license your contribution under the [MIT License](../LICENSE). By contributing to the documentation, you agree to license your contribution under the [Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/). -Prior to v3.1.0, Bootstrap's code was released under the Apache License v2.0. +Prior to v3.1.0, Bootstrap’s code was released under the Apache License v2.0. diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md new file mode 100644 index 000000000000..f3e8f5266ecb --- /dev/null +++ b/.github/INCIDENT_RESPONSE.md @@ -0,0 +1,162 @@ +# Incident response plan + +This document describes how the Bootstrap maintainers respond to and manage security or operational incidents affecting the project, its website, or its distributed releases. This plan is public to promote transparency and community trust. Operational details (e.g., private contacts, credentials, or internal coordination tools) are maintained separately in the maintainers’ private documentation. + +--- + +## 1. Purpose & Scope + +This plan defines how Bootstrap maintainers will: + +- Identify, triage, and manage security or integrity incidents affecting project code, releases, or infrastructure. +- Communicate with the community and downstream consumers during and after an incident. +- Record lessons learned and update processes to reduce future risk. + +It applies to: + +- The Bootstrap source code, documentation, and build pipelines. +- Release artifacts (npm, CDN, GitHub releases). +- The main website ([https://getbootstrap.com](https://getbootstrap.com)). +- Any official Bootstrap GitHub organization infrastructure. + +It does **not** cover unrelated third-party forks or integrations. + +--- + +## 2. Definitions + +- **Incident**: Any event that could compromise the confidentiality, integrity, or availability of Bootstrap code, releases, or users. Examples include: + - A discovered security vulnerability. + - A compromised GitHub account or CI/CD token. + - A malicious dependency or injected code in a release. + - Website defacement or unauthorized modification of documentation. + - Leaked secrets related to the project infrastructure. + +- **Incident Commander (IC)**: The maintainer responsible for coordinating the overall response. + +--- + +## 3. Roles & Responsibilities + +| Role | Responsibilities | +|------|-------------------| +| **Incident Commander (IC)** | Coordinate the response, assign tasks, ensure timely communication. | +| **Security Maintainers** | Triage reported vulnerabilities, assess impact, create fixes, handle embargoes. | +| **Infrastructure Lead** | Manage CI/CD, website, and release infrastructure. | +| **Communications Lead** | Manage public announcements, blog posts, and social updates. | +| **Contributors & Community** | Promptly report suspected security issues and follow responsible disclosure guidelines. | + +In practice, Bootstrap’s core team fulfills these roles collectively, assigning an IC on a per-incident basis. + +--- + +## 4. Incident workflow + +### 4.1 Detection & Reporting + +- All security issues should be **privately reported** via the contact method in [`SECURITY.md`](../SECURITY.md) or through GitHub’s Security Advisory mechanism. +- Maintainers also monitor: + - Automated dependency scanners (e.g., Dependabot, npm audit). + - GitHub notifications and vulnerability alerts. + - Community channels for suspicious activity. + +### 4.2 Initial triage + +Upon receiving a report: + +1. A maintainer acknowledges receipt within 3 business days (or sooner, when possible). + Bootstrap is maintained by a small volunteer team; response times may vary slightly outside normal working hours. +2. The IC assesses severity and impact: + - **Critical:** immediate compromise of release infrastructure or code integrity. + - **High:** exploitable vulnerability in distributed assets. + - **Medium:** minor vulnerability or low-likelihood attack vector. + - **Low:** informational, no direct risk. +3. If confirmed as an incident, the IC opens a private coordination channel for maintainers and begins containment. + +### 4.3 Containment & Eradication + +- Revoke or rotate any affected credentials. +- Disable compromised infrastructure or build pipelines if necessary. +- Patch affected branches or dependencies. +- Verify integrity of artifacts and releases. + +### 4.4 Communication + +- Keep the reporting party informed (when applicable). +- For major incidents, the Communications Lead drafts a public advisory describing: + - What happened + - What was impacted + - How users can verify or mitigate + - What actions were taken +- Communications occur after containment to avoid amplifying risk. + +Public disclosures are posted via: + +- GitHub Security Advisory if appropriate +- [blog.getbootstrap.com/](https://blog.getbootstrap.com/) +- [Bootstrap GitHub discussions](https://github.com/orgs/twbs/discussions) +- [@getbootstrap](https://x.com/getbootstrap) on X (formerly Twitter) for critical security notices. + +### 4.5 Recovery + +- Validate all systems and releases are secure. +- Resume normal operations. +- Tag patched releases and notify affected users. + +### 4.6 Post-incident review + +Within two weeks after resolution: + +- Conduct an internal debrief. +- Record: + - Root cause + - What worked / what didn’t + - Remediation steps + - Documentation or automation updates needed +- Summarize lessons learned in the private maintainers’ wiki (with optional public summary if appropriate). + +--- + +## 5. Severity levels & Response targets + +| Severity | Example | Target response (volunteer team) | +|-----------|----------|----------------------------------| +| **Critical** | Compromised release, stolen signing keys | Acknowledge ≤ 24h (best effort), containment ≤ 48h, fix ideally ≤ 14d | +| **High** | Vulnerability enabling arbitrary code execution | Acknowledge ≤ 3 business days, fix ideally ≤ 14–21d | +| **Medium** | XSS or content injection on docs site | Acknowledge ≤ 5 business days, fix in next release cycle | +| **Low** | Minor issue with limited risk | Acknowledge ≤ 7 business days, fix as scheduled | + +**Note:** Timelines represent good-faith targets for a small volunteer core team, not hard SLAs. The maintainers will always prioritize public safety and transparency, even if timing varies. + +--- + +## 6. Public disclosure principles + +Bootstrap follows a responsible disclosure approach: + +- Work privately with reporters and affected parties before publishing details. +- Never name reporters without consent. +- Coordinate embargo periods with downstream consumers when needed. +- Publish advisories only after patches or mitigations are available. + +--- + +## 7. Communication Channels + +| Purpose | Channel | +|----------|----------| +| Private reporting | Email address in [`SECURITY.md`](./SECURITY.md) or GitHub advisory form | +| General updates | [blog.getbootstrap.com/](https://blog.getbootstrap.com/) blog | +| Security advisories | GitHub Security Advisory dashboard | +| Social alerts | [@getbootstrap](https://x.com/getbootstrap) | +| GitHub discussion alerts | [github.com/orgs/twbs/discussions](https://github.com/orgs/twbs/discussions) | + +--- + +## 8. Plan Maintenance + +This plan is reviewed at least annually or after any major incident. Changes are approved by the Core Team and recorded in Git history. + +--- + +_The Bootstrap maintainers are committed to transparency, user trust, and continuous improvement in our security and response practices._ diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 70dcfd53281a..000000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Bug report -about: Tell us about a bug you may have identified in Bootstrap. -title: '' -labels: '' -assignees: '' - ---- - -Before opening: - -- [Search for duplicate or closed issues](https://github.com/twbs/bootstrap/issues?utf8=%E2%9C%93&q=is%3Aissue) -- [Validate](https://html5.validator.nu/) any HTML to avoid common problems -- Read the [contributing guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md) - -Bug reports must include: - -- Operating system and version (Windows, macOS, Android, iOS) -- Browser and version (Chrome, Firefox, Safari, Microsoft Edge, Opera, Android Browser) -- A [reduced test case](https://css-tricks.com/reduced-test-cases/) or suggested fix using [CodePen](https://codepen.io/) or [JS Bin](https://jsbin.com/) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000000..3e3d6b9e55f7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,62 @@ +name: Report a bug +description: Tell us about a bug or issue you may have identified in Bootstrap. +title: "Provide a general summary of the issue" +labels: [bug] +assignees: "-" +body: + - type: checkboxes + attributes: + label: Prerequisites + description: Take a couple minutes to help our maintainers work faster. + options: + - label: I have [searched](https://github.com/twbs/bootstrap/issues?utf8=%E2%9C%93&q=is%3Aissue) for duplicate or closed issues + required: true + - label: I have [validated](https://html5.validator.nu/) any HTML to avoid common problems + required: true + - label: I have read the [contributing guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md) + required: true + - type: textarea + id: what-happened + attributes: + label: Describe the issue + description: Provide a summary of the issue and what you expected to happen, including specific steps to reproduce. + validations: + required: true + - type: textarea + id: reduced-test-case + attributes: + label: Reduced test cases + description: Include links [reduced test case](https://css-tricks.com/reduced-test-cases/) links or suggested fixes using CodePen ([v4 template](https://codepen.io/team/bootstrap/pen/yLabNQL) or [v5 template](https://codepen.io/team/bootstrap/pen/qBamdLj)). + validations: + required: true + - type: dropdown + id: os + attributes: + label: What operating system(s) are you seeing the problem on? + multiple: true + options: + - Windows + - macOS + - Android + - iOS + - Linux + validations: + required: true + - type: dropdown + id: browser + attributes: + label: What browser(s) are you seeing the problem on? + multiple: true + options: + - Chrome + - Safari + - Firefox + - Microsoft Edge + - Opera + - type: input + id: version + attributes: + label: What version of Bootstrap are you using? + placeholder: "e.g., v5.1.0 or v4.5.2" + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 2913b45f02ed..f1520711335c 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,4 +1,4 @@ contact_links: - - name: Ask a question + - name: Ask the community url: https://github.com/twbs/bootstrap/discussions/new - about: Ask and discuss questions with other Bootstrap community members + about: Ask and discuss questions with other Bootstrap community members. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 422fa2bb4c3c..000000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for a new feature in Bootstrap. -title: '' -labels: feature -assignees: '' - ---- - -Before opening: - -- [Search for duplicate or closed issues](https://github.com/twbs/bootstrap/issues?utf8=%E2%9C%93&q=is%3Aissue) -- Read the [contributing guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md) - -Feature requests must include: - -- As much detail as possible for what we should add and why it's important to Bootstrap -- Relevant links to prior art, screenshots, or live demos whenever possible diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000000..4b757b1d6753 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,29 @@ +name: Feature request +description: Suggest new or updated features to include in Bootstrap. +title: "Suggest a new feature" +labels: [feature] +assignees: [] +body: + - type: checkboxes + attributes: + label: Prerequisites + description: Take a couple minutes to help our maintainers work faster. + options: + - label: I have [searched](https://github.com/twbs/bootstrap/issues?utf8=%E2%9C%93&q=is%3Aissue) for duplicate or closed feature requests + required: true + - label: I have read the [contributing guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md) + required: true + - type: textarea + id: proposal + attributes: + label: Proposal + description: Provide detailed information for what we should add, including relevant links to prior art, screenshots, or live demos whenever possible. + validations: + required: true + - type: textarea + id: motivation + attributes: + label: Motivation and context + description: Tell us why this change is needed or helpful, and what problems it may help solve. + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000000..04df74f36a56 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,38 @@ +### Description + + + +### Motivation & Context + + + +### Type of changes + + + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Refactoring (non-breaking change) +- [ ] Breaking change (fix or feature that would change existing functionality) + +### Checklist + + + + +- [ ] I have read the [contributing guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md) +- [ ] My code follows the code style of the project _(using `npm run lint`)_ +- [ ] My change introduces changes to the documentation +- [ ] I have updated the documentation accordingly +- [ ] I have added tests to cover my changes +- [ ] All new and existing tests passed + +#### Live previews + + + +- + +### Related issues + + diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md index a4739f589a4f..26b3be42c5b5 100644 --- a/.github/SUPPORT.md +++ b/.github/SUPPORT.md @@ -6,6 +6,6 @@ See the [contributing guidelines](CONTRIBUTING.md) for sharing bug reports. For general troubleshooting or help getting started: -- Join [the official Slack room](https://bootstrap-slack.herokuapp.com/). +- Ask and explore [our GitHub Discussions](https://github.com/twbs/bootstrap/discussions). - Chat with fellow Bootstrappers in IRC. On the `irc.libera.chat` server, in the `#bootstrap` channel. - Ask and explore Stack Overflow with the [`bootstrap-5`](https://stackoverflow.com/questions/tagged/bootstrap-5) tag. diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 000000000000..957877282f68 --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,3 @@ +name: "CodeQL config" +paths-ignore: + - dist diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 29135b4007e5..75ebd6ec9d52 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,24 +1,86 @@ version: 2 updates: - - package-ecosystem: npm + - package-ecosystem: "github-actions" directory: "/" schedule: interval: weekly day: tuesday time: "12:00" timezone: Europe/Athens - open-pull-requests-limit: 10 - reviewers: - - XhmikosR + cooldown: + default-days: 7 + groups: + github-actions: + patterns: + - "*" + - package-ecosystem: npm + directory: "/" labels: - dependencies - v5 - versioning-strategy: increase - rebase-strategy: disabled - - package-ecosystem: "github-actions" - directory: "/" schedule: interval: weekly day: tuesday time: "12:00" timezone: Europe/Athens + cooldown: + default-days: 7 + versioning-strategy: increase + rebase-strategy: disabled + ignore: + - dependency-name: "@astrojs/mdx" + update-types: + - "version-update:semver-major" + - dependency-name: "@docsearch/js" + update-types: + - "version-update:semver-major" + - "version-update:semver-minor" + - dependency-name: "astro" + update-types: + - "version-update:semver-major" + - dependency-name: "eslint" + update-types: + - "version-update:semver-major" + - "version-update:semver-minor" + - dependency-name: "eslint-config-xo" + update-types: + - "version-update:semver-major" + - "version-update:semver-minor" + - dependency-name: "eslint-plugin-unicorn" + update-types: + - "version-update:semver-major" + - "version-update:semver-minor" + - dependency-name: "hammer-simulator" + update-types: + - "version-update:semver-major" + - "version-update:semver-minor" + - dependency-name: "jquery" + update-types: + - "version-update:semver-major" + - "version-update:semver-minor" + - dependency-name: "karma-browserstack-launcher" + update-types: + - "version-update:semver-major" + - "version-update:semver-minor" + - dependency-name: "karma-rollup-preprocessor" + update-types: + - "version-update:semver-major" + - "version-update:semver-minor" + - dependency-name: "sass" + update-types: + - "version-update:semver-major" + - "version-update:semver-minor" + - dependency-name: "stylelint" + update-types: + - "version-update:semver-major" + - "version-update:semver-minor" + - dependency-name: "vnu-jar" + update-types: + - "version-update:semver-major" + - "version-update:semver-minor" + - "version-update:semver-patch" + groups: + production-dependencies: + dependency-type: "production" + development-dependencies: + dependency-type: "development" diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 5d0f45de88d1..0289984bec06 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -7,6 +7,9 @@ categories: - title: '❗ Breaking Changes' labels: - 'breaking-change' + - title: '🚀 Highlights' + labels: + - 'release-highlight' - title: '🚀 Features' labels: - 'new-feature' diff --git a/.github/workflows/browserstack.yml b/.github/workflows/browserstack.yml index ffa594a77429..c5cc00745456 100644 --- a/.github/workflows/browserstack.yml +++ b/.github/workflows/browserstack.yml @@ -2,23 +2,32 @@ name: BrowserStack on: push: + branches: + - "**" + - "!dependabot/**" + workflow_dispatch: env: FORCE_COLOR: 2 - NODE: 14 + NODE: 22 + +permissions: + contents: read jobs: browserstack: runs-on: ubuntu-latest - if: github.repository == 'twbs/bootstrap' && (!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')) + if: github.repository == 'twbs/bootstrap' timeout-minutes: 30 steps: - name: Clone repository - uses: actions/checkout@v2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "${{ env.NODE }}" cache: npm @@ -34,3 +43,4 @@ jobs: env: BROWSER_STACK_ACCESS_KEY: "${{ secrets.BROWSER_STACK_ACCESS_KEY }}" BROWSER_STACK_USERNAME: "${{ secrets.BROWSER_STACK_USERNAME }}" + GITHUB_SHA: "${{ github.sha }}" diff --git a/.github/workflows/bundlewatch.yml b/.github/workflows/bundlewatch.yml index c212290df022..d26c2b958cd7 100644 --- a/.github/workflows/bundlewatch.yml +++ b/.github/workflows/bundlewatch.yml @@ -2,11 +2,17 @@ name: Bundlewatch on: push: + branches: + - main pull_request: + workflow_dispatch: env: FORCE_COLOR: 2 - NODE: 14 + NODE: 22 + +permissions: + contents: read jobs: bundlewatch: @@ -14,10 +20,12 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "${{ env.NODE }}" cache: npm diff --git a/.github/workflows/calibreapp-image-actions.yml b/.github/workflows/calibreapp-image-actions.yml new file mode 100644 index 000000000000..56b8a02a01a6 --- /dev/null +++ b/.github/workflows/calibreapp-image-actions.yml @@ -0,0 +1,32 @@ +name: Compress Images + +on: + pull_request: + paths: + - '**.jpg' + - '**.jpeg' + - '**.png' + - '**.webp' + +permissions: + contents: read + +jobs: + build: + # Only run on Pull Requests within the same repository, and not from forks. + if: github.event.pull_request.head.repo.full_name == github.repository + name: calibreapp/image-actions + runs-on: ubuntu-latest + permissions: + # allow calibreapp/image-actions to update PRs + pull-requests: write + steps: + - name: Clone repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Compress Images + uses: calibreapp/image-actions@f32575787d333b0579f0b7d506ff03be63a669d1 # v1.4.1 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 13e2eb598fd7..a9603515a0ac 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -7,31 +7,38 @@ on: - v4-dev - "!dependabot/**" pull_request: - # The branches below must be a subset of the branches above branches: - main - v4-dev - "!dependabot/**" schedule: - - cron: "0 2 * * 5" + - cron: "0 2 * * 4" + workflow_dispatch: jobs: analyze: name: Analyze runs-on: ubuntu-latest + permissions: + security-events: write steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 with: + config-file: ./.github/codeql/codeql-config.yml languages: "javascript" + queries: +security-and-quality - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + with: + category: "/language:javascript" diff --git a/.github/workflows/cspell.yml b/.github/workflows/cspell.yml new file mode 100644 index 000000000000..55002176051a --- /dev/null +++ b/.github/workflows/cspell.yml @@ -0,0 +1,36 @@ +name: cspell + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +env: + FORCE_COLOR: 2 + +permissions: + contents: read + +jobs: + cspell: + permissions: + # allow streetsidesoftware/cspell-action to fetch files for commits and PRs + contents: read + pull-requests: read + runs-on: ubuntu-latest + + steps: + - name: Clone repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Run cspell + uses: streetsidesoftware/cspell-action@de2a73e963e7443969755b648a1008f77033c5b2 # v8.4.0 + with: + config: ".cspell.json" + files: "**/*.{md,mdx}" + inline: error + incremental_files_only: false diff --git a/.github/workflows/css.yml b/.github/workflows/css.yml index 48f11d4598bf..c25fd480e1e6 100644 --- a/.github/workflows/css.yml +++ b/.github/workflows/css.yml @@ -2,13 +2,17 @@ name: CSS on: push: - branches-ignore: - - "dependabot/**" + branches: + - main pull_request: + workflow_dispatch: env: FORCE_COLOR: 2 - NODE: 14 + NODE: 22 + +permissions: + contents: read jobs: css: @@ -16,10 +20,12 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "${{ env.NODE }}" cache: npm @@ -29,3 +35,6 @@ jobs: - name: Build CSS run: npm run css + + - name: Run CSS tests + run: npm run css-test diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index cc5f4bd6d60b..430e59dd8b45 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,13 +2,17 @@ name: Docs on: push: - branches-ignore: - - "dependabot/**" + branches: + - main pull_request: + workflow_dispatch: env: FORCE_COLOR: 2 - NODE: 14 + NODE: 22 + +permissions: + contents: read jobs: docs: @@ -16,10 +20,12 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "${{ env.NODE }}" cache: npm @@ -29,5 +35,16 @@ jobs: - name: Install npm dependencies run: npm ci - - name: Test docs - run: npm run docs + - name: Build docs + run: npm run docs-build + + - name: Validate HTML + run: npm run docs-vnu + + - name: Run linkinator + uses: JustinBeckwith/linkinator-action@7b6b0bc671f6264e1a8daa4488a5bd91ce61dcd4 # v2.4.2 + with: + paths: _site + recurse: true + verbosity: error + skip: "^http://localhost" diff --git a/.github/workflows/issue-close-require.yml b/.github/workflows/issue-close-require.yml new file mode 100644 index 000000000000..d57e92f9068c --- /dev/null +++ b/.github/workflows/issue-close-require.yml @@ -0,0 +1,26 @@ +name: Close Issue Awaiting Reply + +on: + schedule: + - cron: "0 0 * * *" + +permissions: + contents: read + +jobs: + issue-close-require: + permissions: + # allow actions-cool/issues-helper to update issues and PRs + issues: write + pull-requests: write + runs-on: ubuntu-latest + if: github.repository == 'twbs/bootstrap' + steps: + - name: awaiting reply + uses: actions-cool/issues-helper@200c78641dbf33838311e5a1e0c31bbdb92d7cf0 # v3.8.0 + with: + actions: "close-issues" + labels: "awaiting-reply" + inactive-day: 14 + body: | + As the issue was labeled with `awaiting-reply`, but there has been no response in 14 days, this issue will be closed. If you have any questions, you can comment/reply. diff --git a/.github/workflows/issue-labeled.yml b/.github/workflows/issue-labeled.yml new file mode 100644 index 000000000000..c8418ae4e056 --- /dev/null +++ b/.github/workflows/issue-labeled.yml @@ -0,0 +1,26 @@ +name: Issue Labeled + +on: + issues: + types: [labeled] + +permissions: + contents: read + +jobs: + issue-labeled: + permissions: + # allow actions-cool/issues-helper to update issues and PRs + issues: write + pull-requests: write + if: github.repository == 'twbs/bootstrap' + runs-on: ubuntu-latest + steps: + - name: awaiting reply + if: github.event.label.name == 'needs-example' + uses: actions-cool/issues-helper@200c78641dbf33838311e5a1e0c31bbdb92d7cf0 # v3.8.0 + with: + actions: "create-comment" + token: ${{ secrets.GITHUB_TOKEN }} + body: | + Hello @${{ github.event.issue.user.login }}. Bug reports must include a **live demo** of the issue. Per our [contributing guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md), please create a reduced test case on [CodePen](https://codepen.io/) or [StackBlitz](https://stackblitz.com/) and report back with your link, Bootstrap version, and specific browser and Operating System details. diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml index 266b1576deda..b85d73771715 100644 --- a/.github/workflows/js.yml +++ b/.github/workflows/js.yml @@ -2,31 +2,37 @@ name: JS Tests on: push: - branches-ignore: - - "dependabot/**" + branches: + - main pull_request: + workflow_dispatch: env: FORCE_COLOR: 2 + NODE: 22 + +permissions: + contents: read jobs: run: - name: Node ${{ matrix.node }} + permissions: + # allow coverallsapp/github-action to create new checks issues and fetch code + checks: write + contents: read + name: JS Tests runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - node: [12, 14, 16] - steps: - name: Clone repository - uses: actions/checkout@v2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: ${{ matrix.node }} + node-version: ${{ env.NODE }} cache: npm - name: Install npm dependencies @@ -39,8 +45,8 @@ jobs: run: npm run js-test - name: Run Coveralls - uses: coverallsapp/github-action@1.1.3 - if: matrix.node == 14 + uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e # v2.3.7 + if: ${{ !github.event.repository.fork }} with: github-token: "${{ secrets.GITHUB_TOKEN }}" path-to-lcov: "./js/coverage/lcov.info" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 153ad6f222a1..374b9b669eac 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,13 +2,17 @@ name: Lint on: push: - branches-ignore: - - "dependabot/**" + branches: + - main pull_request: + workflow_dispatch: env: FORCE_COLOR: 2 - NODE: 14 + NODE: 22 + +permissions: + contents: read jobs: lint: @@ -16,10 +20,12 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "${{ env.NODE }}" cache: npm diff --git a/.github/workflows/node-sass.yml b/.github/workflows/node-sass.yml index ee64b21527d9..5fe75a52b913 100644 --- a/.github/workflows/node-sass.yml +++ b/.github/workflows/node-sass.yml @@ -2,13 +2,17 @@ name: CSS (node-sass) on: push: - branches-ignore: - - "dependabot/**" + branches: + - main pull_request: + workflow_dispatch: env: FORCE_COLOR: 2 - NODE: 14 + NODE: 22 + +permissions: + contents: read jobs: css: @@ -16,10 +20,12 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "${{ env.NODE }}" @@ -28,3 +34,16 @@ jobs: npx --package node-sass@latest node-sass --version npx --package node-sass@latest node-sass --output-style expanded --source-map true --source-map-contents true --precision 6 scss/ -o dist-sass/css/ ls -Al dist-sass/css + + - name: Check built CSS files for Sass variables + shell: bash + run: | + SASS_VARS_FOUND=$(find "dist-sass/css/" -type f -name "*.css" -print0 | xargs -0 --no-run-if-empty grep -F "\$" || true) + if [[ -z "$SASS_VARS_FOUND" ]]; then + echo "All good, no Sass variables found!" + exit 0 + else + echo "Found $(echo "$SASS_VARS_FOUND" | wc -l | bc) Sass variables:" + echo "$SASS_VARS_FOUND" + exit 1 + fi diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml new file mode 100644 index 000000000000..c768a8090476 --- /dev/null +++ b/.github/workflows/publish-nuget.yml @@ -0,0 +1,34 @@ +name: Publish NuGet Packages + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + package-nuget: + runs-on: windows-latest + if: ${{ github.repository == 'twbs/bootstrap' && startsWith(github.event.release.tag_name, 'v') }} + env: + GITHUB_REF_NAME: ${{ github.ref_name }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up NuGet + uses: nuget/setup-nuget@fd55a6f3b34392fa83fde1454582407d8c714123 # v4.0 + with: + nuget-api-key: ${{ secrets.NuGetAPIKey }} + nuget-version: '5.x' + + - name: Pack NuGet packages + shell: pwsh + run: | + $bsversion = $env:GITHUB_REF_NAME.Substring(1) + nuget pack "nuget\bootstrap.nuspec" -Verbosity detailed -NonInteractive -BasePath . -Version $bsversion + nuget pack "nuget\bootstrap.sass.nuspec" -Verbosity detailed -NonInteractive -BasePath . -Version $bsversion + nuget push "bootstrap.$bsversion.nupkg" -Verbosity detailed -NonInteractive -Source "https://api.nuget.org/v3/index.json" + nuget push "bootstrap.sass.$bsversion.nupkg" -Verbosity detailed -NonInteractive -Source "https://api.nuget.org/v3/index.json" diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml index ab2f37694dcc..36d71bd18924 100644 --- a/.github/workflows/release-notes.yml +++ b/.github/workflows/release-notes.yml @@ -4,12 +4,20 @@ on: push: branches: - main + workflow_dispatch: + +permissions: + contents: read jobs: update_release_draft: + permissions: + # allow release-drafter/release-drafter to create GitHub releases and add labels to PRs + contents: write + pull-requests: write runs-on: ubuntu-latest if: github.repository == 'twbs/bootstrap' steps: - - uses: release-drafter/release-drafter@v5 + - uses: release-drafter/release-drafter@5de93583980a40bd78603b6dfdcda5b4df377b32 # v7.2.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 000000000000..ae6220427391 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,78 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '27 12 * * 2' + push: + branches: [ "main" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + # `publish_results: true` only works when run from the default branch. conditional can be removed if disabled. + if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request' + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + # Uncomment the permissions below if installing in a private repository. + # contents: read + # actions: read + + steps: + - name: "Checkout code" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecard on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # (Optional) Uncomment file_mode if you have a .gitattributes with files marked export-ignore + # file_mode: git + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard (optional). + # Commenting out will disable upload of results to your repo's Code Scanning dashboard + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + with: + sarif_file: results.sarif diff --git a/.gitignore b/.gitignore index 9817c71118f8..235ad54948dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,5 @@ # Ignore docs files /_site/ -# Hugo resources folder -/resources/ # Numerous always-ignore extensions *.diff @@ -28,7 +26,6 @@ *.sublime-workspace nbproject Thumbs.db -/.vscode/ # Local Netlify folder .netlify @@ -37,5 +34,12 @@ Thumbs.db *.komodoproject # Folders to ignore +/dist-sass/ /js/coverage/ /node_modules/ + +# Site +/site/dist +/site/node_modules +/site/.astro +/site/public diff --git a/.ncurc.json b/.ncurc.json new file mode 100644 index 000000000000..f0e521f858ee --- /dev/null +++ b/.ncurc.json @@ -0,0 +1,17 @@ +{ + "reject": [ + "@astrojs/mdx", + "@docsearch/js", + "astro", + "eslint", + "eslint-config-xo", + "eslint-plugin-unicorn", + "hammer-simulator", + "jquery", + "karma-browserstack-launcher", + "karma-rollup-preprocessor", + "sass", + "stylelint", + "vnu-jar" + ] +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000000..32442e87a0db --- /dev/null +++ b/.prettierignore @@ -0,0 +1,12 @@ +# Prettier is only used for the website + +site/.astro +site/dist +site/public +site/src/assets +site/src/scss +site/src/pages/**/*.md +site/src/pages/**/*.mdx +site/src/content/**/*.mdx +site/src/layouts/RedirectLayout.astro +site/static diff --git a/.stylelintignore b/.stylelintignore index 0759a69acead..b7013de7ef81 100644 --- a/.stylelintignore +++ b/.stylelintignore @@ -2,4 +2,5 @@ **/dist/ **/vendor/ /_site/ +/site/public/ /js/coverage/ diff --git a/.stylelintrc b/.stylelintrc deleted file mode 100644 index c068d30b572a..000000000000 --- a/.stylelintrc +++ /dev/null @@ -1,31 +0,0 @@ -{ - "extends": [ - "stylelint-config-twbs-bootstrap/scss" - ], - "rules": { - "declaration-property-value-disallowed-list": { - "border": "none", - "outline": "none" - }, - "function-disallowed-list": [ - "calc", - "lighten", - "darken" - ], - "property-disallowed-list": [ - "border-radius", - "border-top-left-radius", - "border-top-right-radius", - "border-bottom-right-radius", - "border-bottom-left-radius", - "transition" - ], - "scss/dollar-variable-default": [ - true, - { - "ignore": "local" - } - ], - "scss/selector-no-union-class-name": true - } -} diff --git a/.stylelintrc.json b/.stylelintrc.json new file mode 100644 index 000000000000..589884aae7ab --- /dev/null +++ b/.stylelintrc.json @@ -0,0 +1,60 @@ +{ + "extends": [ + "stylelint-config-twbs-bootstrap" + ], + "reportInvalidScopeDisables": true, + "reportNeedlessDisables": true, + "overrides": [ + { + "files": "**/*.scss", + "rules": { + "declaration-property-value-disallowed-list": { + "border": "none", + "outline": "none" + }, + "function-disallowed-list": [ + "calc", + "lighten", + "darken" + ], + "property-disallowed-list": [ + "border-radius", + "border-top-left-radius", + "border-top-right-radius", + "border-bottom-right-radius", + "border-bottom-left-radius", + "transition" + ], + "scss/dollar-variable-default": [ + true, + { + "ignore": "local" + } + ], + "scss/selector-no-union-class-name": true + } + }, + { + "files": "scss/**/*.{test,spec}.scss", + "rules": { + "scss/dollar-variable-default": null, + "declaration-no-important": null + } + }, + { + "files": "site/**/*.scss", + "rules": { + "scss/dollar-variable-default": null + } + }, + { + "files": "site/**/examples/**/*.css", + "rules": { + "comment-empty-line-before": null, + "property-no-vendor-prefix": null, + "selector-no-qualifying-type": null, + "value-no-vendor-prefix": null + } + } + ] +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000000..bc5df00e1618 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + "recommendations": [ + "astro-build.astro-vscode", + "dbaeumer.vscode-eslint", + "EditorConfig.EditorConfig", + "hossaini.bootstrap-intellisense", + "streetsidesoftware.code-spell-checker", + "stylelint.vscode-stylelint" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000000..9c6ae2d622a3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.fixAll.stylelint": "always" + }, + "editor.renderWhitespace": "all", + "scss.validate": false, + "stylelint.enable": true, + "stylelint.validate": ["scss"] +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 5c6a4e727cd4..756298316059 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,42 +2,131 @@ ## Our Pledge -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to creating a positive environment include: +Examples of behavior that contributes to a positive environment for our +community include: -- Using welcoming and inclusive language -- Being respectful of differing viewpoints and experiences -- Gracefully accepting constructive criticism -- Focusing on what is best for the community -- Showing empathy towards other community members +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -- The use of sexualized language or imagery and unwelcome sexual attention or advances -- Trolling, insulting/derogatory comments, and personal or political attacks +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment -- Publishing others' private information, such as a physical or electronic address, without explicit permission -- Other conduct which could reasonably be considered inappropriate in a professional setting +- Publishing others’ private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting -## Our Responsibilities +## Enforcement Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. ## Scope -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at mdo@getbootstrap.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +mdo@getbootstrap.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. -Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 1.4, available at +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/LICENSE b/LICENSE index 72dda234edaa..d27a1619347b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,6 @@ The MIT License (MIT) -Copyright (c) 2011-2021 Twitter, Inc. -Copyright (c) 2011-2021 The Bootstrap Authors +Copyright (c) 2011-2026 The Bootstrap Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index e260fcbebaf7..afd3047917f2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- Bootstrap logo + Bootstrap logo

@@ -9,14 +9,12 @@

Sleek, intuitive, and powerful front-end framework for faster and easier web development.
- Explore Bootstrap docs » + Explore Bootstrap docs »

- Report bug + Report bug · - Request feature - · - Themes + Request feature · Blog

@@ -31,7 +29,7 @@ Our default branch is for development of our Bootstrap 5 release. Head to the [` - [Quick start](#quick-start) - [Status](#status) -- [What's included](#whats-included) +- [What’s included](#whats-included) - [Bugs and feature requests](#bugs-and-feature-requests) - [Documentation](#documentation) - [Contributing](#contributing) @@ -46,113 +44,115 @@ Our default branch is for development of our Bootstrap 5 release. Head to the [` Several quick start options are available: -- [Download the latest release](https://github.com/twbs/bootstrap/archive/v5.1.0.zip) +- [Download the latest release](https://github.com/twbs/bootstrap/archive/v5.3.8.zip) - Clone the repo: `git clone https://github.com/twbs/bootstrap.git` -- Install with [npm](https://www.npmjs.com/): `npm install bootstrap` -- Install with [yarn](https://yarnpkg.com/): `yarn add bootstrap` -- Install with [Composer](https://getcomposer.org/): `composer require twbs/bootstrap:5.1.0` +- Install with [npm](https://www.npmjs.com/): `npm install bootstrap@v5.3.8` +- Install with [yarn](https://yarnpkg.com/): `yarn add bootstrap@v5.3.8` +- Install with [Bun](https://bun.sh/): `bun add bootstrap@v5.3.8` +- Install with [Composer](https://getcomposer.org/): `composer require twbs/bootstrap:5.3.8` - Install with [NuGet](https://www.nuget.org/): CSS: `Install-Package bootstrap` Sass: `Install-Package bootstrap.sass` -Read the [Getting started page](https://getbootstrap.com/docs/5.1/getting-started/introduction/) for information on the framework contents, templates and examples, and more. +Read the [Getting started page](https://getbootstrap.com/docs/5.3/getting-started/introduction/) for information on the framework contents, templates, examples, and more. ## Status -[![Slack](https://bootstrap-slack.herokuapp.com/badge.svg)](https://bootstrap-slack.herokuapp.com/) -[![Build Status](https://img.shields.io/github/workflow/status/twbs/bootstrap/JS%20Tests/main?label=JS%20Tests&logo=github)](https://github.com/twbs/bootstrap/actions?query=workflow%3AJS+Tests+branch%3Amain) -[![npm version](https://img.shields.io/npm/v/bootstrap)](https://www.npmjs.com/package/bootstrap) -[![Gem version](https://img.shields.io/gem/v/bootstrap)](https://rubygems.org/gems/bootstrap) -[![Meteor Atmosphere](https://img.shields.io/badge/meteor-twbs%3Abootstrap-blue)](https://atmospherejs.com/twbs/bootstrap) -[![Packagist Prerelease](https://img.shields.io/packagist/vpre/twbs/bootstrap)](https://packagist.org/packages/twbs/bootstrap) -[![NuGet](https://img.shields.io/nuget/vpre/bootstrap)](https://www.nuget.org/packages/bootstrap/absoluteLatest) -[![peerDependencies Status](https://img.shields.io/david/peer/twbs/bootstrap)](https://david-dm.org/twbs/bootstrap?type=peer) -[![devDependency Status](https://img.shields.io/david/dev/twbs/bootstrap)](https://david-dm.org/twbs/bootstrap?type=dev) -[![Coverage Status](https://img.shields.io/coveralls/github/twbs/bootstrap/main)](https://coveralls.io/github/twbs/bootstrap?branch=main) +[![Build Status](https://img.shields.io/github/actions/workflow/status/twbs/bootstrap/js.yml?branch=main&label=JS%20Tests&logo=github)](https://github.com/twbs/bootstrap/actions/workflows/js.yml?query=workflow%3AJS+branch%3Amain) +[![npm version](https://img.shields.io/npm/v/bootstrap?logo=npm&logoColor=fff)](https://www.npmjs.com/package/bootstrap) +[![Gem version](https://img.shields.io/gem/v/bootstrap?logo=rubygems&logoColor=fff)](https://rubygems.org/gems/bootstrap) +[![Meteor Atmosphere](https://img.shields.io/badge/meteor-twbs%3Abootstrap-blue?logo=meteor&logoColor=fff)](https://atmospherejs.com/twbs/bootstrap) +[![Packagist Prerelease](https://img.shields.io/packagist/vpre/twbs/bootstrap?logo=packagist&logoColor=fff)](https://packagist.org/packages/twbs/bootstrap) +[![NuGet](https://img.shields.io/nuget/vpre/bootstrap?logo=nuget&logoColor=fff)](https://www.nuget.org/packages/bootstrap/absoluteLatest) +[![Coverage Status](https://img.shields.io/coveralls/github/twbs/bootstrap/main?logo=coveralls&logoColor=fff)](https://coveralls.io/github/twbs/bootstrap?branch=main) [![CSS gzip size](https://img.badgesize.io/twbs/bootstrap/main/dist/css/bootstrap.min.css?compression=gzip&label=CSS%20gzip%20size)](https://github.com/twbs/bootstrap/blob/main/dist/css/bootstrap.min.css) [![CSS Brotli size](https://img.badgesize.io/twbs/bootstrap/main/dist/css/bootstrap.min.css?compression=brotli&label=CSS%20Brotli%20size)](https://github.com/twbs/bootstrap/blob/main/dist/css/bootstrap.min.css) [![JS gzip size](https://img.badgesize.io/twbs/bootstrap/main/dist/js/bootstrap.min.js?compression=gzip&label=JS%20gzip%20size)](https://github.com/twbs/bootstrap/blob/main/dist/js/bootstrap.min.js) [![JS Brotli size](https://img.badgesize.io/twbs/bootstrap/main/dist/js/bootstrap.min.js?compression=brotli&label=JS%20Brotli%20size)](https://github.com/twbs/bootstrap/blob/main/dist/js/bootstrap.min.js) -[![BrowserStack Status](https://www.browserstack.com/automate/badge.svg?badge_key=SkxZcStBeExEdVJqQ2hWYnlWckpkNmNEY213SFp6WHFETWk2bGFuY3pCbz0tLXhqbHJsVlZhQnRBdEpod3NLSDMzaHc9PQ==--3d0b75245708616eb93113221beece33e680b229)](https://www.browserstack.com/automate/public-build/SkxZcStBeExEdVJqQ2hWYnlWckpkNmNEY213SFp6WHFETWk2bGFuY3pCbz0tLXhqbHJsVlZhQnRBdEpod3NLSDMzaHc9PQ==--3d0b75245708616eb93113221beece33e680b229) -[![Backers on Open Collective](https://img.shields.io/opencollective/backers/bootstrap)](#backers) -[![Sponsors on Open Collective](https://img.shields.io/opencollective/sponsors/bootstrap)](#sponsors) - - -## What's included - -Within the download you'll find the following directories and files, logically grouping common assets and providing both compiled and minified variations. You'll see something like this: - -```text -bootstrap/ -├── css/ -│ ├── bootstrap-grid.css -│ ├── bootstrap-grid.css.map -│ ├── bootstrap-grid.min.css -│ ├── bootstrap-grid.min.css.map -│ ├── bootstrap-grid.rtl.css -│ ├── bootstrap-grid.rtl.css.map -│ ├── bootstrap-grid.rtl.min.css -│ ├── bootstrap-grid.rtl.min.css.map -│ ├── bootstrap-reboot.css -│ ├── bootstrap-reboot.css.map -│ ├── bootstrap-reboot.min.css -│ ├── bootstrap-reboot.min.css.map -│ ├── bootstrap-reboot.rtl.css -│ ├── bootstrap-reboot.rtl.css.map -│ ├── bootstrap-reboot.rtl.min.css -│ ├── bootstrap-reboot.rtl.min.css.map -│ ├── bootstrap-utilities.css -│ ├── bootstrap-utilities.css.map -│ ├── bootstrap-utilities.min.css -│ ├── bootstrap-utilities.min.css.map -│ ├── bootstrap-utilities.rtl.css -│ ├── bootstrap-utilities.rtl.css.map -│ ├── bootstrap-utilities.rtl.min.css -│ ├── bootstrap-utilities.rtl.min.css.map -│ ├── bootstrap.css -│ ├── bootstrap.css.map -│ ├── bootstrap.min.css -│ ├── bootstrap.min.css.map -│ ├── bootstrap.rtl.css -│ ├── bootstrap.rtl.css.map -│ ├── bootstrap.rtl.min.css -│ └── bootstrap.rtl.min.css.map -└── js/ - ├── bootstrap.bundle.js - ├── bootstrap.bundle.js.map - ├── bootstrap.bundle.min.js - ├── bootstrap.bundle.min.js.map - ├── bootstrap.esm.js - ├── bootstrap.esm.js.map - ├── bootstrap.esm.min.js - ├── bootstrap.esm.min.js.map - ├── bootstrap.js - ├── bootstrap.js.map - ├── bootstrap.min.js - └── bootstrap.min.js.map -``` - -We provide compiled CSS and JS (`bootstrap.*`), as well as compiled and minified CSS and JS (`bootstrap.min.*`). [source maps](https://developers.google.com/web/tools/chrome-devtools/javascript/source-maps) (`bootstrap.*.map`) are available for use with certain browsers' developer tools. Bundled JS files (`bootstrap.bundle.js` and minified `bootstrap.bundle.min.js`) include [Popper](https://popper.js.org/). +![Open Source Security Foundation Scorecard](https://img.shields.io/ossf-scorecard/github.com/twbs/bootstrap) +[![Backers on Open Collective](https://img.shields.io/opencollective/backers/bootstrap?logo=opencollective&logoColor=fff)](#backers) +[![Sponsors on Open Collective](https://img.shields.io/opencollective/sponsors/bootstrap?logo=opencollective&logoColor=fff)](#sponsors) + + +## What’s included + +Within the download you’ll find the following directories and files, logically grouping common assets and providing both compiled and minified variations. + +
+ Download contents + + ```text + bootstrap/ + ├── css/ + │ ├── bootstrap-grid.css + │ ├── bootstrap-grid.css.map + │ ├── bootstrap-grid.min.css + │ ├── bootstrap-grid.min.css.map + │ ├── bootstrap-grid.rtl.css + │ ├── bootstrap-grid.rtl.css.map + │ ├── bootstrap-grid.rtl.min.css + │ ├── bootstrap-grid.rtl.min.css.map + │ ├── bootstrap-reboot.css + │ ├── bootstrap-reboot.css.map + │ ├── bootstrap-reboot.min.css + │ ├── bootstrap-reboot.min.css.map + │ ├── bootstrap-reboot.rtl.css + │ ├── bootstrap-reboot.rtl.css.map + │ ├── bootstrap-reboot.rtl.min.css + │ ├── bootstrap-reboot.rtl.min.css.map + │ ├── bootstrap-utilities.css + │ ├── bootstrap-utilities.css.map + │ ├── bootstrap-utilities.min.css + │ ├── bootstrap-utilities.min.css.map + │ ├── bootstrap-utilities.rtl.css + │ ├── bootstrap-utilities.rtl.css.map + │ ├── bootstrap-utilities.rtl.min.css + │ ├── bootstrap-utilities.rtl.min.css.map + │ ├── bootstrap.css + │ ├── bootstrap.css.map + │ ├── bootstrap.min.css + │ ├── bootstrap.min.css.map + │ ├── bootstrap.rtl.css + │ ├── bootstrap.rtl.css.map + │ ├── bootstrap.rtl.min.css + │ └── bootstrap.rtl.min.css.map + └── js/ + ├── bootstrap.bundle.js + ├── bootstrap.bundle.js.map + ├── bootstrap.bundle.min.js + ├── bootstrap.bundle.min.js.map + ├── bootstrap.esm.js + ├── bootstrap.esm.js.map + ├── bootstrap.esm.min.js + ├── bootstrap.esm.min.js.map + ├── bootstrap.js + ├── bootstrap.js.map + ├── bootstrap.min.js + └── bootstrap.min.js.map + ``` +
+ +We provide compiled CSS and JS (`bootstrap.*`), as well as compiled and minified CSS and JS (`bootstrap.min.*`). [Source maps](https://web.dev/articles/source-maps) (`bootstrap.*.map`) are available for use with certain browsers’ developer tools. Bundled JS files (`bootstrap.bundle.js` and minified `bootstrap.bundle.min.js`) include [Popper](https://popper.js.org/docs/v2/). ## Bugs and feature requests -Have a bug or a feature request? Please first read the [issue guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md#using-the-issue-tracker) and search for existing and closed issues. If your problem or idea is not addressed yet, [please open a new issue](https://github.com/twbs/bootstrap/issues/new). +Have a bug or a feature request? Please first read the [issue guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md#using-the-issue-tracker) and search for existing and closed issues. If your problem or idea is not addressed yet, [please open a new issue](https://github.com/twbs/bootstrap/issues/new/choose). ## Documentation -Bootstrap's documentation, included in this repo in the root directory, is built with [Hugo](https://gohugo.io/) and publicly hosted on GitHub Pages at . The docs may also be run locally. +Bootstrap’s documentation, included in this repo in the root directory, is built with [Astro](https://astro.build/) and publicly hosted on GitHub Pages at . The docs may also be run locally. -Documentation search is powered by [Algolia's DocSearch](https://community.algolia.com/docsearch/). Working on our search? Be sure to set `debug: true` in `site/assets/js/search.js`. +Documentation search is powered by [Algolia's DocSearch](https://docsearch.algolia.com/). ### Running documentation locally -1. Run `npm install` to install the Node.js dependencies, including Hugo (the site builder). +1. Run `npm install` to install the Node.js dependencies, including Astro (the site builder). 2. Run `npm run test` (or a specific npm script) to rebuild distributed CSS and JavaScript files, as well as our docs assets. 3. From the root `/bootstrap` directory, run `npm run docs-serve` in the command line. -4. Open `http://localhost:9001/` in your browser, and voilà. +4. Open in your browser, and voilà. -Learn more about using Hugo by reading its [documentation](https://gohugo.io/documentation/). +Learn more about using Astro by reading its [documentation](https://docs.astro.build/en/getting-started/). ### Documentation for previous releases @@ -172,11 +172,12 @@ Editor preferences are available in the [editor config](https://github.com/twbs/ ## Community -Get updates on Bootstrap's development and chat with the project maintainers and community members. +Get updates on Bootstrap’s development and chat with the project maintainers and community members. -- Follow [@getbootstrap on Twitter](https://twitter.com/getbootstrap). +- Follow [@getbootstrap on X](https://x.com/getbootstrap). - Read and subscribe to [The Official Bootstrap Blog](https://blog.getbootstrap.com/). -- Join [the official Slack room](https://bootstrap-slack.herokuapp.com/). +- Ask questions and explore [our GitHub Discussions](https://github.com/twbs/bootstrap/discussions). +- Discuss, ask questions, and more on [the community Discord](https://discord.gg/bZUvakRU3M) or [Bootstrap subreddit](https://www.reddit.com/r/bootstrap/). - Chat with fellow Bootstrappers in IRC. On the `irc.libera.chat` server, in the `#bootstrap` channel. - Implementation help may be found at Stack Overflow (tagged [`bootstrap-5`](https://stackoverflow.com/questions/tagged/bootstrap-5)). - Developers should use the keyword `bootstrap` on packages which modify or add to the functionality of Bootstrap when distributing through [npm](https://www.npmjs.com/browse/keyword/bootstrap) or similar delivery mechanisms for maximum discoverability. @@ -193,23 +194,29 @@ See [the Releases section of our GitHub project](https://github.com/twbs/bootstr **Mark Otto** -- +- - **Jacob Thornton** -- +- - ## Thanks - BrowserStack Logo + BrowserStack Thanks to [BrowserStack](https://www.browserstack.com/) for providing the infrastructure that allows us to test in real browsers! + + Netlify + + +Thanks to [Netlify](https://www.netlify.com/) for providing us with Deploy Previews! + ## Sponsors @@ -236,4 +243,4 @@ Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com ## Copyright and license -Code and documentation copyright 2011–2021 the [Bootstrap Authors](https://github.com/twbs/bootstrap/graphs/contributors) and [Twitter, Inc.](https://twitter.com) Code released under the [MIT License](https://github.com/twbs/bootstrap/blob/main/LICENSE). Docs released under [Creative Commons](https://creativecommons.org/licenses/by/3.0/). +Code and documentation copyright 2011-2026 the [Bootstrap Authors](https://github.com/twbs/bootstrap/graphs/contributors). Code released under the [MIT License](https://github.com/twbs/bootstrap/blob/main/LICENSE). Docs released under [Creative Commons](https://creativecommons.org/licenses/by/3.0/). diff --git a/build/.eslintrc.json b/build/.eslintrc.json deleted file mode 100644 index 679bd26f7ba2..000000000000 --- a/build/.eslintrc.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "env": { - "browser": false, - "node": true - }, - "parserOptions": { - "sourceType": "script" - }, - "extends": "../.eslintrc.json", - "rules": { - "no-console": "off", - "strict": "error" - } -} diff --git a/build/banner.js b/build/banner.mjs similarity index 50% rename from build/banner.js rename to build/banner.mjs index df82ff32edf3..3fea93c8f1d2 100644 --- a/build/banner.js +++ b/build/banner.mjs @@ -1,6 +1,12 @@ -'use strict' +import fs from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +const pkgJson = path.join(__dirname, '../package.json') +const pkg = JSON.parse(await fs.readFile(pkgJson, 'utf8')) -const pkg = require('../package.json') const year = new Date().getFullYear() function getBanner(pluginFilename) { @@ -11,4 +17,4 @@ function getBanner(pluginFilename) { */` } -module.exports = getBanner +export default getBanner diff --git a/build/build-plugins.js b/build/build-plugins.js deleted file mode 100644 index 15a53784584b..000000000000 --- a/build/build-plugins.js +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/env node - -/*! - * Script to build our plugins to use them separately. - * Copyright 2020-2021 The Bootstrap Authors - * Copyright 2020-2021 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ - -'use strict' - -const path = require('path') -const rollup = require('rollup') -const { babel } = require('@rollup/plugin-babel') -const banner = require('./banner.js') - -const rootPath = path.resolve(__dirname, '../js/dist/') -const plugins = [ - babel({ - // Only transpile our source code - exclude: 'node_modules/**', - // Include the helpers in each file, at most one copy of each - babelHelpers: 'bundled' - }) -] -const bsPlugins = { - Data: path.resolve(__dirname, '../js/src/dom/data.js'), - EventHandler: path.resolve(__dirname, '../js/src/dom/event-handler.js'), - Manipulator: path.resolve(__dirname, '../js/src/dom/manipulator.js'), - SelectorEngine: path.resolve(__dirname, '../js/src/dom/selector-engine.js'), - Alert: path.resolve(__dirname, '../js/src/alert.js'), - Base: path.resolve(__dirname, '../js/src/base-component.js'), - Button: path.resolve(__dirname, '../js/src/button.js'), - Carousel: path.resolve(__dirname, '../js/src/carousel.js'), - Collapse: path.resolve(__dirname, '../js/src/collapse.js'), - Dropdown: path.resolve(__dirname, '../js/src/dropdown.js'), - Modal: path.resolve(__dirname, '../js/src/modal.js'), - Offcanvas: path.resolve(__dirname, '../js/src/offcanvas.js'), - Popover: path.resolve(__dirname, '../js/src/popover.js'), - ScrollSpy: path.resolve(__dirname, '../js/src/scrollspy.js'), - Tab: path.resolve(__dirname, '../js/src/tab.js'), - Toast: path.resolve(__dirname, '../js/src/toast.js'), - Tooltip: path.resolve(__dirname, '../js/src/tooltip.js') -} - -const defaultPluginConfig = { - external: [ - bsPlugins.Data, - bsPlugins.Base, - bsPlugins.EventHandler, - bsPlugins.SelectorEngine - ], - globals: { - [bsPlugins.Data]: 'Data', - [bsPlugins.Base]: 'Base', - [bsPlugins.EventHandler]: 'EventHandler', - [bsPlugins.SelectorEngine]: 'SelectorEngine' - } -} - -const getConfigByPluginKey = pluginKey => { - switch (pluginKey) { - case 'Alert': - case 'Offcanvas': - case 'Tab': - return defaultPluginConfig - - case 'Base': - case 'Button': - case 'Carousel': - case 'Collapse': - case 'Modal': - case 'ScrollSpy': { - const config = Object.assign(defaultPluginConfig) - config.external.push(bsPlugins.Manipulator) - config.globals[bsPlugins.Manipulator] = 'Manipulator' - return config - } - - case 'Dropdown': - case 'Tooltip': { - const config = Object.assign(defaultPluginConfig) - config.external.push(bsPlugins.Manipulator, '@popperjs/core') - config.globals[bsPlugins.Manipulator] = 'Manipulator' - config.globals['@popperjs/core'] = 'Popper' - return config - } - - case 'Popover': - return { - external: [ - bsPlugins.Data, - bsPlugins.SelectorEngine, - bsPlugins.Tooltip - ], - globals: { - [bsPlugins.Data]: 'Data', - [bsPlugins.SelectorEngine]: 'SelectorEngine', - [bsPlugins.Tooltip]: 'Tooltip' - } - } - - case 'Toast': - return { - external: [ - bsPlugins.Data, - bsPlugins.Base, - bsPlugins.EventHandler, - bsPlugins.Manipulator - ], - globals: { - [bsPlugins.Data]: 'Data', - [bsPlugins.Base]: 'Base', - [bsPlugins.EventHandler]: 'EventHandler', - [bsPlugins.Manipulator]: 'Manipulator' - } - } - - default: - return { - external: [] - } - } -} - -const utilObjects = new Set([ - 'Util', - 'Sanitizer', - 'Backdrop' -]) - -const domObjects = new Set([ - 'Data', - 'EventHandler', - 'Manipulator', - 'SelectorEngine' -]) - -const build = async plugin => { - console.log(`Building ${plugin} plugin...`) - - const { external, globals } = getConfigByPluginKey(plugin) - const pluginFilename = path.basename(bsPlugins[plugin]) - let pluginPath = rootPath - - if (utilObjects.has(plugin)) { - pluginPath = `${rootPath}/util/` - } - - if (domObjects.has(plugin)) { - pluginPath = `${rootPath}/dom/` - } - - const bundle = await rollup.rollup({ - input: bsPlugins[plugin], - plugins, - external - }) - - await bundle.write({ - banner: banner(pluginFilename), - format: 'umd', - name: plugin, - sourcemap: true, - globals, - file: path.resolve(__dirname, `${pluginPath}/${pluginFilename}`) - }) - - console.log(`Building ${plugin} plugin... Done!`) -} - -const main = async () => { - try { - await Promise.all(Object.keys(bsPlugins).map(plugin => build(plugin))) - } catch (error) { - console.error(error) - - process.exit(1) - } -} - -main() diff --git a/build/build-plugins.mjs b/build/build-plugins.mjs new file mode 100644 index 000000000000..b5a88d7c062e --- /dev/null +++ b/build/build-plugins.mjs @@ -0,0 +1,108 @@ +#!/usr/bin/env node + +/*! + * Script to build our plugins to use them separately. + * Copyright 2020-2026 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ + +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { babel } from '@rollup/plugin-babel' +import { globby } from 'globby' +import { rollup } from 'rollup' +import banner from './banner.mjs' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +const sourcePath = path.resolve(__dirname, '../js/src/').replace(/\\/g, '/') +const jsFiles = await globby(`${sourcePath}/**/*.js`) + +// Array which holds the resolved plugins +const resolvedPlugins = [] + +// Trims the "js" extension and uppercases => first letter, hyphens, backslashes & slashes +const filenameToEntity = filename => filename.replace('.js', '') + .replace(/(?:^|-|\/|\\)[a-z]/g, str => str.slice(-1).toUpperCase()) + +for (const file of jsFiles) { + resolvedPlugins.push({ + src: file, + dist: file.replace('src', 'dist'), + fileName: path.basename(file), + className: filenameToEntity(path.basename(file)) + // safeClassName: filenameToEntity(path.relative(sourcePath, file)) + }) +} + +const build = async plugin => { + /** + * @type {import('rollup').GlobalsOption} + */ + const globals = {} + + const bundle = await rollup({ + input: plugin.src, + plugins: [ + babel({ + // Only transpile our source code + exclude: 'node_modules/**', + // Include the helpers in each file, at most one copy of each + babelHelpers: 'bundled' + }) + ], + external(source) { + // Pattern to identify local files + const pattern = /^(\.{1,2})\// + + // It's not a local file, e.g a Node.js package + if (!pattern.test(source)) { + globals[source] = source + return true + } + + const usedPlugin = resolvedPlugins.find(plugin => { + return plugin.src.includes(source.replace(pattern, '')) + }) + + if (!usedPlugin) { + throw new Error(`Source ${source} is not mapped!`) + } + + // We can change `Index` with `UtilIndex` etc if we use + // `safeClassName` instead of `className` everywhere + globals[path.normalize(usedPlugin.src)] = usedPlugin.className + return true + } + }) + + await bundle.write({ + banner: banner(plugin.fileName), + format: 'umd', + name: plugin.className, + sourcemap: true, + globals, + generatedCode: 'es2015', + file: plugin.dist + }) + + console.log(`Built ${plugin.className}`) +} + +(async () => { + try { + const basename = path.basename(__filename) + const timeLabel = `[${basename}] finished` + + console.log('Building individual plugins...') + console.time(timeLabel) + + await Promise.all(Object.values(resolvedPlugins).map(plugin => build(plugin))) + + console.timeEnd(timeLabel) + } catch (error) { + console.error(error) + process.exit(1) + } +})() diff --git a/build/change-version.js b/build/change-version.js deleted file mode 100644 index 63f231ea2be5..000000000000 --- a/build/change-version.js +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env node - -/*! - * Script to update version number references in the project. - * Copyright 2017-2021 The Bootstrap Authors - * Copyright 2017-2021 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ - -'use strict' - -const fs = require('fs').promises -const path = require('path') -const globby = require('globby') - -const VERBOSE = process.argv.includes('--verbose') -const DRY_RUN = process.argv.includes('--dry') || process.argv.includes('--dry-run') - -// These are the filetypes we only care about replacing the version -const GLOB = [ - '**/*.{css,html,js,json,md,scss,txt,yml}' -] -const GLOBBY_OPTIONS = { - cwd: path.join(__dirname, '..'), - gitignore: true -} - -// Blame TC39... https://github.com/benjamingr/RegExp.escape/issues/37 -function regExpQuote(string) { - return string.replace(/[$()*+-.?[\\\]^{|}]/g, '\\$&') -} - -function regExpQuoteReplacement(string) { - return string.replace(/\$/g, '$$') -} - -async function replaceRecursively(file, oldVersion, newVersion) { - const originalString = await fs.readFile(file, 'utf8') - const newString = originalString.replace( - new RegExp(regExpQuote(oldVersion), 'g'), regExpQuoteReplacement(newVersion) - ) - - // No need to move any further if the strings are identical - if (originalString === newString) { - return - } - - if (VERBOSE) { - console.log(`FILE: ${file}`) - } - - if (DRY_RUN) { - return - } - - await fs.writeFile(file, newString, 'utf8') -} - -async function main(args) { - const [oldVersion, newVersion] = args - - if (!oldVersion || !newVersion) { - console.error('USAGE: change-version old_version new_version [--verbose] [--dry[-run]]') - console.error('Got arguments:', args) - process.exit(1) - } - - // Strip any leading `v` from arguments because otherwise we will end up with duplicate `v`s - [oldVersion, newVersion].map(arg => arg.startsWith('v') ? arg.slice(1) : arg) - - try { - const files = await globby(GLOB, GLOBBY_OPTIONS) - - await Promise.all(files.map(file => replaceRecursively(file, oldVersion, newVersion))) - } catch (error) { - console.error(error) - process.exit(1) - } -} - -main(process.argv.slice(2)) diff --git a/build/change-version.mjs b/build/change-version.mjs new file mode 100644 index 000000000000..c1433d5780e6 --- /dev/null +++ b/build/change-version.mjs @@ -0,0 +1,113 @@ +#!/usr/bin/env node + +/*! + * Script to update version number references in the project. + * Copyright 2017-2026 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ + +import { execFile } from 'node:child_process' +import fs from 'node:fs/promises' +import process from 'node:process' + +const VERBOSE = process.argv.includes('--verbose') +const DRY_RUN = process.argv.includes('--dry') || process.argv.includes('--dry-run') + +// These are the files we only care about replacing the version +const FILES = [ + 'README.md', + 'config.yml', + 'js/src/base-component.js', + 'package.js', + 'scss/mixins/_banner.scss', + 'site/data/docs-versions.yml' +] + +// Blame TC39... https://github.com/benjamingr/RegExp.escape/issues/37 +function regExpQuote(string) { + return string.replace(/[$()*+-.?[\\\]^{|}]/g, '\\$&') +} + +function regExpQuoteReplacement(string) { + return string.replace(/\$/g, '$$') +} + +async function replaceRecursively(file, oldVersion, newVersion) { + const originalString = await fs.readFile(file, 'utf8') + const newString = originalString + .replace( + new RegExp(regExpQuote(oldVersion), 'g'), + regExpQuoteReplacement(newVersion) + ) + // Also replace the version used by the rubygem, + // which is using periods (`.`) instead of hyphens (`-`) + .replace( + new RegExp(regExpQuote(oldVersion.replace(/-/g, '.')), 'g'), + regExpQuoteReplacement(newVersion.replace(/-/g, '.')) + ) + + // No need to move any further if the strings are identical + if (originalString === newString) { + return + } + + if (VERBOSE) { + console.log(`Found ${oldVersion} in ${file}`) + } + + if (DRY_RUN) { + return + } + + await fs.writeFile(file, newString, 'utf8') +} + +function bumpNpmVersion(newVersion) { + if (DRY_RUN) { + return + } + + execFile('npm', ['version', newVersion, '--no-git-tag'], { shell: true }, error => { + if (error) { + console.error(error) + process.exit(1) + } + }) +} + +function showUsage(args) { + console.error('USAGE: change-version old_version new_version [--verbose] [--dry[-run]]') + console.error('Got arguments:', args) + process.exit(1) +} + +async function main(args) { + let [oldVersion, newVersion] = args + + if (!oldVersion || !newVersion) { + showUsage(args) + } + + // Strip any leading `v` from arguments because + // otherwise we will end up with duplicate `v`s + [oldVersion, newVersion] = [oldVersion, newVersion].map(arg => { + return arg.startsWith('v') ? arg.slice(1) : arg + }) + + if (oldVersion === newVersion) { + showUsage(args) + } + + bumpNpmVersion(newVersion) + + try { + await Promise.all( + FILES.map(file => replaceRecursively(file, oldVersion, newVersion)) + ) + } catch (error) { + console.error(error) + process.exit(1) + } +} + +main(process.argv.slice(2)) diff --git a/build/docs-prep.sh b/build/docs-prep.sh new file mode 100755 index 000000000000..357768abea7a --- /dev/null +++ b/build/docs-prep.sh @@ -0,0 +1,157 @@ +#!/bin/bash + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Default branch suffix +BRANCH_SUFFIX="release" + +# Check if a custom version parameter was provided +if [ $# -eq 1 ]; then + BRANCH_SUFFIX="$1" +fi + +# Branch name to create +NEW_BRANCH="gh-pages-${BRANCH_SUFFIX}" + +# Get the current docs version from config +DOCS_VERSION=$(node -p "require('js-yaml').load(require('fs').readFileSync('config.yml', 'utf8')).docs_version") + +# Function to print colored messages +print_success() { + echo -e "${GREEN}✓ $1${NC}" +} + +print_error() { + echo -e "${RED}✗ $1${NC}" + exit 1 +} + +print_info() { + echo -e "${BLUE}ℹ $1${NC}" +} + +print_warning() { + echo -e "${YELLOW}⚠ $1${NC}" +} + +# Function to execute command with error handling +execute() { + print_info "Running: $1" + eval $1 + if [ $? -ne 0 ]; then + print_error "Failed to execute: $1" + else + print_success "Successfully executed: $1" + fi +} + +# Check if /tmp/_site directory exists from a previous run +if [ -d "/tmp/_site" ]; then + print_warning "Found existing /tmp/_site directory. Removing it…" + rm -rf /tmp/_site +fi + +# Main process +print_info "Starting documentation deployment process…" + +# Step 1: Build documentation +print_info "Building documentation with npm run docs…" +npm run docs +if [ $? -ne 0 ]; then + print_error "Documentation build failed!" +fi +print_success "Documentation built successfully" + +# Step 2: Move _site to /tmp/ +print_info "Moving _site to temporary location…" +execute "mv _site /tmp/" + +# Step 3: Switch to gh-pages branch +print_info "Checking out gh-pages branch…" +git checkout gh-pages +if [ $? -ne 0 ]; then + print_error "Failed to checkout gh-pages branch. Make sure it exists." +fi +print_success "Switched to gh-pages branch" + +git reset --hard origin/gh-pages +if [ $? -ne 0 ]; then + print_error "Failed to reset to origin/gh-pages. Check your git configuration." +fi +print_success "Reset to origin/gh-pages" + +git pull origin gh-pages +if [ $? -ne 0 ]; then + print_error "Failed to pull from origin/gh-pages. Check your network connection and git configuration." +fi +print_success "Pulled latest changes from origin/gh-pages" + +# Step 4: Create a new branch for the update +print_info "Checking if branch ${NEW_BRANCH} exists and deleting it if it does…" +if git show-ref --verify --quiet refs/heads/${NEW_BRANCH}; then + execute "git branch -D ${NEW_BRANCH}" +else + print_info "Branch ${NEW_BRANCH} does not exist, proceeding with creation…" +fi +print_info "Creating new branch ${NEW_BRANCH}…" +execute "git checkout -b ${NEW_BRANCH}" + +# Step 5: Move all root-level files from Astro build +find /tmp/_site -maxdepth 1 -type f -exec mv {} . \; + +# Step 6: Move all top-level directories except 'docs' (which needs special handling) +find /tmp/_site -maxdepth 1 -type d ! -name "_site" ! -name "docs" -exec sh -c 'dir=$(basename "$1"); rm -rf "$dir"; mv "$1" .' _ {} \; + +# Step 7: Handle docs directory specially +if [ -d "/tmp/_site/docs" ]; then + # Replace only the current version's docs + if [ -d "docs/$DOCS_VERSION" ]; then + rm -rf "docs/$DOCS_VERSION" + fi + mv "/tmp/_site/docs/$DOCS_VERSION" "docs/" + + # Handle docs root files + find /tmp/_site/docs -maxdepth 1 -type f -exec mv {} docs/ \; + + # Handle special docs directories (getting-started, versions) + for special_dir in getting-started versions; do + if [ -d "/tmp/_site/docs/$special_dir" ]; then + rm -rf "docs/$special_dir" + mv "/tmp/_site/docs/$special_dir" "docs/" + fi + done +fi + +# Clean up remaining files in /tmp/_site if any +if [ -d "/tmp/_site" ]; then + remaining_files=$(find /tmp/_site -type f | wc -l) + remaining_dirs=$(find /tmp/_site -type d | wc -l) + if [ $remaining_files -gt 0 ] || [ $remaining_dirs -gt 1 ]; then + print_warning "There are still some files or directories in /tmp/_site that weren't moved." + print_warning "You may want to inspect /tmp/_site to see if anything important was missed." + else + print_info "Cleaning up temporary directory…" + rm -rf /tmp/_site + print_success "Temporary directory cleaned up" + fi +fi + +# Step 10: Remove empty site directory if it exists +if [ -d "site" ]; then + print_info "Removing empty site directory…" + execute "rm -rf site" +fi + +print_success "Docs prep complete!" +print_info "Review changes before committing and pushing." +print_info "Next steps:" +print_info " 1. Run a local server to review changes" +print_info " 2. Check browser and web inspector for any errors" +print_info " 3. git add ." +print_info " 4. git commit -m \"Update documentation\"" +print_info " 5. git push origin ${NEW_BRANCH}" diff --git a/build/generate-sri.js b/build/generate-sri.mjs similarity index 58% rename from build/generate-sri.js rename to build/generate-sri.mjs index 221873b8fedc..3d9ce7f5b148 100644 --- a/build/generate-sri.js +++ b/build/generate-sri.mjs @@ -5,17 +5,17 @@ * Remember to use the same vendor files as the CDN ones, * otherwise the hashes won't match! * - * Copyright 2017-2021 The Bootstrap Authors - * Copyright 2017-2021 Twitter, Inc. + * Copyright 2017-2026 The Bootstrap Authors * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -'use strict' +import crypto from 'node:crypto' +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import sh from 'shelljs' -const crypto = require('crypto') -const fs = require('fs') -const path = require('path') -const sh = require('shelljs') +const __dirname = path.dirname(fileURLToPath(import.meta.url)) sh.config.fatal = true @@ -47,18 +47,18 @@ const files = [ } ] -files.forEach(file => { - fs.readFile(file.file, 'utf8', (err, data) => { - if (err) { - throw err +for (const { file, configPropertyName } of files) { + fs.readFile(file, 'utf8', (error, data) => { + if (error) { + throw error } - const algo = 'sha384' - const hash = crypto.createHash(algo).update(data, 'utf8').digest('base64') - const integrity = `${algo}-${hash}` + const algorithm = 'sha384' + const hash = crypto.createHash(algorithm).update(data, 'utf8').digest('base64') + const integrity = `${algorithm}-${hash}` - console.log(`${file.configPropertyName}: ${integrity}`) + console.log(`${configPropertyName}: ${integrity}`) - sh.sed('-i', new RegExp(`^(\\s+${file.configPropertyName}:\\s+["'])\\S*(["'])`), `$1${integrity}$2`, configFile) + sh.sed('-i', new RegExp(`^(\\s+${configPropertyName}:\\s+["'])\\S*(["'])`), `$1${integrity}$2`, configFile) }) -}) +} diff --git a/build/postcss.config.js b/build/postcss.config.js deleted file mode 100644 index b179a0e77c4c..000000000000 --- a/build/postcss.config.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict' - -module.exports = ctx => { - return { - map: ctx.file.dirname.includes('examples') ? - false : - { - inline: false, - annotation: true, - sourcesContent: true - }, - plugins: { - autoprefixer: { - cascade: false - }, - rtlcss: ctx.env === 'RTL' ? {} : false - } - } -} diff --git a/build/postcss.config.mjs b/build/postcss.config.mjs new file mode 100644 index 000000000000..7717cfc3f1f6 --- /dev/null +++ b/build/postcss.config.mjs @@ -0,0 +1,17 @@ +const mapConfig = { + inline: false, + annotation: true, + sourcesContent: true +} + +export default context => { + return { + map: context.file.dirname.includes('examples') ? false : mapConfig, + plugins: { + autoprefixer: { + cascade: false + }, + rtlcss: context.env === 'RTL' + } + } +} diff --git a/build/rollup.config.js b/build/rollup.config.mjs similarity index 59% rename from build/rollup.config.js rename to build/rollup.config.mjs index 8cecec9aa24a..dd6c7d13e66f 100644 --- a/build/rollup.config.js +++ b/build/rollup.config.mjs @@ -1,15 +1,17 @@ -'use strict' +import path from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' +import { babel } from '@rollup/plugin-babel' +import { nodeResolve } from '@rollup/plugin-node-resolve' +import replace from '@rollup/plugin-replace' +import banner from './banner.mjs' -const path = require('path') -const { babel } = require('@rollup/plugin-babel') -const { nodeResolve } = require('@rollup/plugin-node-resolve') -const replace = require('@rollup/plugin-replace') -const banner = require('./banner.js') +const __dirname = path.dirname(fileURLToPath(import.meta.url)) const BUNDLE = process.env.BUNDLE === 'true' const ESM = process.env.ESM === 'true' -let fileDest = `bootstrap${ESM ? '.esm' : ''}` +let destinationFile = `bootstrap${ESM ? '.esm' : ''}` const external = ['@popperjs/core'] const plugins = [ babel({ @@ -24,7 +26,7 @@ const globals = { } if (BUNDLE) { - fileDest += '.bundle' + destinationFile += '.bundle' // Remove last entry in external array to bundle Popper external.pop() delete globals['@popperjs/core'] @@ -40,10 +42,11 @@ if (BUNDLE) { const rollupConfig = { input: path.resolve(__dirname, `../js/index.${ESM ? 'esm' : 'umd'}.js`), output: { - banner, - file: path.resolve(__dirname, `../dist/js/${fileDest}.js`), + banner: banner(), + file: path.resolve(__dirname, `../dist/js/${destinationFile}.js`), format: ESM ? 'esm' : 'umd', - globals + globals, + generatedCode: 'es2015' }, external, plugins @@ -53,4 +56,4 @@ if (!ESM) { rollupConfig.output.name = 'bootstrap' } -module.exports = rollupConfig +export default rollupConfig diff --git a/build/svgo.yml b/build/svgo.yml deleted file mode 100644 index 67940d393ee3..000000000000 --- a/build/svgo.yml +++ /dev/null @@ -1,59 +0,0 @@ -# Usage: -# install svgo globally: `npm i -g svgo` -# svgo --config=build/svgo.yml --input=foo.svg - -# https://github.com/svg/svgo/blob/master/docs/how-it-works/en.md -# replace default config - -multipass: true -#full: true - -# https://github.com/svg/svgo/blob/master/lib/svgo/js2svg.js#L6 for more config options - -js2svg: - pretty: true - indent: 2 - -plugins: -# - addAttributesToSVGElement: -# attributes: -# - focusable: false - - cleanupAttrs: true - - cleanupEnableBackground: true - - cleanupIDs: true - - cleanupListOfValues: true - - cleanupNumericValues: true - - collapseGroups: true - - convertColors: true - - convertPathData: true - - convertShapeToPath: true - - convertStyleToAttrs: true - - convertTransform: true - - inlineStyles: true - - mergePaths: true - - minifyStyles: true - - moveElemsAttrsToGroup: true - - moveGroupAttrsToElems: true - - removeAttrs: - attrs: - - "data-name" - - removeComments: true - - removeDesc: true - - removeDoctype: true - - removeEditorsNSData: true - - removeEmptyAttrs: true - - removeEmptyContainers: true - - removeEmptyText: true - - removeHiddenElems: true - - removeMetadata: true - - removeNonInheritableGroupAttrs: true - - removeTitle: false - - removeUnknownsAndDefaults: - keepRoleAttr: true - - removeUnusedNS: true - - removeUselessDefs: true - - removeUselessStrokeAndFill: true - - removeViewBox: false - - removeXMLNS: false - - removeXMLProcInst: true - - sortAttrs: true diff --git a/build/vnu-jar.js b/build/vnu-jar.js deleted file mode 100644 index 6c3517ca5504..000000000000 --- a/build/vnu-jar.js +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env node - -/*! - * Script to run vnu-jar if Java is available. - * Copyright 2017-2021 The Bootstrap Authors - * Copyright 2017-2021 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ - -'use strict' - -const { execFile, spawn } = require('child_process') -const vnu = require('vnu-jar') - -execFile('java', ['-version'], (error, stdout, stderr) => { - if (error) { - console.error('Skipping vnu-jar test; Java is missing.') - return - } - - const is32bitJava = !/64-Bit/.test(stderr) - - // vnu-jar accepts multiple ignores joined with a `|`. - // Also note that the ignores are string regular expressions. - const ignores = [ - // "autocomplete" is included in ' - ].join('') + fixtureEl.innerHTML = '' const expectedElements = [] @@ -209,9 +209,7 @@ describe('SelectorEngine', () => { }) it('should return contenteditable elements', () => { - fixtureEl.innerHTML = [ - '
lorem
' - ].join('') + fixtureEl.innerHTML = '
lorem
' const expectedElements = [fixtureEl.querySelector('[contenteditable="true"]')] @@ -219,9 +217,7 @@ describe('SelectorEngine', () => { }) it('should not return disabled elements', () => { - fixtureEl.innerHTML = [ - '' - ].join('') + fixtureEl.innerHTML = '' const expectedElements = [] @@ -229,14 +225,190 @@ describe('SelectorEngine', () => { }) it('should not return invisible elements', () => { - fixtureEl.innerHTML = [ - '' - ].join('') + fixtureEl.innerHTML = '' const expectedElements = [] expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements) }) }) -}) + describe('getSelectorFromElement', () => { + it('should get selector from data-bs-target', () => { + fixtureEl.innerHTML = [ + '
', + '
' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getSelectorFromElement(testEl)).toEqual('.target') + }) + + it('should get selector from href if no data-bs-target set', () => { + fixtureEl.innerHTML = [ + '', + '
' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getSelectorFromElement(testEl)).toEqual('.target') + }) + + it('should get selector from href if data-bs-target equal to #', () => { + fixtureEl.innerHTML = [ + '', + '
' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getSelectorFromElement(testEl)).toEqual('.target') + }) + + it('should return null if a selector from a href is a url without an anchor', () => { + fixtureEl.innerHTML = [ + '', + '
' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getSelectorFromElement(testEl)).toBeNull() + }) + + it('should return the anchor if a selector from a href is a url', () => { + fixtureEl.innerHTML = [ + '', + '
' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getSelectorFromElement(testEl)).toEqual('#target') + }) + + it('should return null if selector not found', () => { + fixtureEl.innerHTML = '' + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getSelectorFromElement(testEl)).toBeNull() + }) + + it('should return null if no selector', () => { + fixtureEl.innerHTML = '
' + + const testEl = fixtureEl.querySelector('div') + + expect(SelectorEngine.getSelectorFromElement(testEl)).toBeNull() + }) + }) + + describe('getElementFromSelector', () => { + it('should get element from data-bs-target', () => { + fixtureEl.innerHTML = [ + '
', + '
' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getElementFromSelector(testEl)).toEqual(fixtureEl.querySelector('.target')) + }) + + it('should get element from href if no data-bs-target set', () => { + fixtureEl.innerHTML = [ + '', + '
' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getElementFromSelector(testEl)).toEqual(fixtureEl.querySelector('.target')) + }) + + it('should return null if element not found', () => { + fixtureEl.innerHTML = '' + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getElementFromSelector(testEl)).toBeNull() + }) + + it('should return null if no selector', () => { + fixtureEl.innerHTML = '
' + + const testEl = fixtureEl.querySelector('div') + + expect(SelectorEngine.getElementFromSelector(testEl)).toBeNull() + }) + }) + + describe('getMultipleElementsFromSelector', () => { + it('should get elements from data-bs-target', () => { + fixtureEl.innerHTML = [ + '
', + '
', + '
' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toEqual(Array.from(fixtureEl.querySelectorAll('.target'))) + }) + + it('should get elements if several ids are given', () => { + fixtureEl.innerHTML = [ + '
', + '
', + '
' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toEqual(Array.from(fixtureEl.querySelectorAll('.target'))) + }) + + it('should get elements if several ids with special chars are given', () => { + fixtureEl.innerHTML = [ + '
', + '
', + '
' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toEqual(Array.from(fixtureEl.querySelectorAll('.target'))) + }) + + it('should get elements in array, from href if no data-bs-target set', () => { + fixtureEl.innerHTML = [ + '', + '
', + '
' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toEqual(Array.from(fixtureEl.querySelectorAll('.target'))) + }) + + it('should return empty array if elements not found', () => { + fixtureEl.innerHTML = '' + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toHaveSize(0) + }) + + it('should return empty array if no selector', () => { + fixtureEl.innerHTML = '
' + + const testEl = fixtureEl.querySelector('div') + + expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toHaveSize(0) + }) + }) +}) diff --git a/js/tests/unit/dropdown.spec.js b/js/tests/unit/dropdown.spec.js index 2b6d8cd781ce..63ae4bd102bc 100644 --- a/js/tests/unit/dropdown.spec.js +++ b/js/tests/unit/dropdown.spec.js @@ -1,9 +1,9 @@ -import Dropdown from '../../src/dropdown' -import EventHandler from '../../src/dom/event-handler' -import { noop } from '../../src/util' - -/** Test helpers */ -import { clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture' +import EventHandler from '../../src/dom/event-handler.js' +import Dropdown from '../../src/dropdown.js' +import { noop } from '../../src/util/index.js' +import { + clearFixture, createEvent, getFixture, jQueryMock +} from '../helpers/fixture.js' describe('Dropdown', () => { let fixtureEl @@ -59,36 +59,61 @@ describe('Dropdown', () => { expect(dropdownByElement._element).toEqual(btnDropdown) }) - it('should create offset modifier correctly when offset option is a function', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should work on invalid markup', () => { + return new Promise(resolve => { + // TODO: REMOVE in v6 + fixtureEl.innerHTML = [ + '' + ].join('') - const getOffset = jasmine.createSpy('getOffset').and.returnValue([10, 20]) - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown, { - offset: getOffset, - popperConfig: { - onFirstUpdate: state => { - expect(getOffset).toHaveBeenCalledWith({ - popper: state.rects.popper, - reference: state.rects.reference, - placement: state.placement - }, btnDropdown) - done() + const dropdownElem = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(dropdownElem) + + dropdownElem.addEventListener('shown.bs.dropdown', () => { + resolve() + }) + + expect().nothing() + dropdown.show() + }) + }) + + it('should create offset modifier correctly when offset option is a function', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const getOffset = jasmine.createSpy('getOffset').and.returnValue([10, 20]) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown, { + offset: getOffset, + popperConfig: { + onFirstUpdate(state) { + expect(getOffset).toHaveBeenCalledWith({ + popper: state.rects.popper, + reference: state.rects.reference, + placement: state.placement + }, btnDropdown) + resolve() + } } - } - }) - const offset = dropdown._getOffset() + }) + const offset = dropdown._getOffset() - expect(typeof offset).toEqual('function') + expect(typeof offset).toEqual('function') - dropdown.show() + dropdown.show() + }) }) it('should create offset modifier correctly when offset option is a string into data attribute', () => { @@ -132,7 +157,7 @@ describe('Dropdown', () => { it('should allow to pass config to Popper with `popperConfig` as a function', () => { fixtureEl.innerHTML = [ '', + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const input = fixtureEl.querySelector('input') - const textarea = fixtureEl.querySelector('textarea') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') + const textarea = fixtureEl.querySelector('textarea') - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - input.focus() - const keydown = createEvent('keydown') + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + input.focus() + const keydown = createEvent('keydown') - keydown.key = 'ArrowUp' - input.dispatchEvent(keydown) + keydown.key = 'ArrowUp' + input.dispatchEvent(keydown) - expect(document.activeElement).toEqual(input, 'input still focused') + expect(document.activeElement).toEqual(input, 'input still focused') - textarea.focus() - textarea.dispatchEvent(keydown) + textarea.focus() + textarea.dispatchEvent(keydown) - expect(document.activeElement).toEqual(textarea, 'textarea still focused') - done() - }) + expect(document.activeElement).toEqual(textarea, 'textarea still focused') + resolve() + }) - triggerDropdown.click() + triggerDropdown.click() + }) }) - it('should skip disabled element when using keyboard navigation', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should skip disabled element when using keyboard navigation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' - triggerDropdown.dispatchEvent(keydown) - triggerDropdown.dispatchEvent(keydown) + triggerDropdown.dispatchEvent(keydown) + triggerDropdown.dispatchEvent(keydown) - expect(document.activeElement.classList.contains('disabled')).toEqual(false, '.disabled not focused') - expect(document.activeElement.hasAttribute('disabled')).toEqual(false, ':disabled not focused') - done() - }) + expect(document.activeElement).not.toHaveClass('disabled') + expect(document.activeElement.hasAttribute('disabled')).toBeFalse() + resolve() + }) - triggerDropdown.click() + triggerDropdown.click() + }) }) - it('should skip hidden element when using keyboard navigation', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') + it('should skip hidden element when using keyboard navigation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' - triggerDropdown.dispatchEvent(keydown) + triggerDropdown.dispatchEvent(keydown) - expect(document.activeElement.classList.contains('d-none')).toEqual(false, '.d-none not focused') - expect(document.activeElement.style.display).not.toBe('none', '"display: none" not focused') - expect(document.activeElement.style.visibility).not.toBe('hidden', '"visibility: hidden" not focused') + expect(document.activeElement).not.toHaveClass('d-none') + expect(document.activeElement.style.display).not.toEqual('none') + expect(document.activeElement.style.visibility).not.toEqual('hidden') - done() - }) + resolve() + }) - triggerDropdown.click() + triggerDropdown.click() + }) }) - it('should focus next/previous element when using keyboard navigation', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should focus next/previous element when using keyboard navigation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const item1 = fixtureEl.querySelector('#item1') - const item2 = fixtureEl.querySelector('#item2') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const item1 = fixtureEl.querySelector('#item1') + const item2 = fixtureEl.querySelector('#item2') - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - const keydownArrowDown = createEvent('keydown') - keydownArrowDown.key = 'ArrowDown' + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydownArrowDown = createEvent('keydown') + keydownArrowDown.key = 'ArrowDown' - triggerDropdown.dispatchEvent(keydownArrowDown) - expect(document.activeElement).toEqual(item1, 'item1 is focused') + triggerDropdown.dispatchEvent(keydownArrowDown) + expect(document.activeElement).toEqual(item1, 'item1 is focused') - document.activeElement.dispatchEvent(keydownArrowDown) - expect(document.activeElement).toEqual(item2, 'item2 is focused') + document.activeElement.dispatchEvent(keydownArrowDown) + expect(document.activeElement).toEqual(item2, 'item2 is focused') - const keydownArrowUp = createEvent('keydown') - keydownArrowUp.key = 'ArrowUp' + const keydownArrowUp = createEvent('keydown') + keydownArrowUp.key = 'ArrowUp' - document.activeElement.dispatchEvent(keydownArrowUp) - expect(document.activeElement).toEqual(item1, 'item1 is focused') + document.activeElement.dispatchEvent(keydownArrowUp) + expect(document.activeElement).toEqual(item1, 'item1 is focused') - done() - }) + resolve() + }) - triggerDropdown.click() + triggerDropdown.click() + }) }) - it('should open the dropdown and focus on the last item when using ArrowUp for the first time', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should open the dropdown and focus on the last item when using ArrowUp for the first time', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const lastItem = fixtureEl.querySelector('#item2') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const lastItem = fixtureEl.querySelector('#item2') - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - setTimeout(() => { - expect(document.activeElement).toEqual(lastItem, 'item2 is focused') - done() + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + setTimeout(() => { + expect(document.activeElement).toEqual(lastItem, 'item2 is focused') + resolve() + }) }) - }) - const keydown = createEvent('keydown') - keydown.key = 'ArrowUp' - triggerDropdown.dispatchEvent(keydown) + const keydown = createEvent('keydown') + keydown.key = 'ArrowUp' + triggerDropdown.dispatchEvent(keydown) + }) }) - it('should open the dropdown and focus on the first item when using ArrowDown for the first time', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should open the dropdown and focus on the first item when using ArrowDown for the first time', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const firstItem = fixtureEl.querySelector('#item1') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const firstItem = fixtureEl.querySelector('#item1') - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - setTimeout(() => { - expect(document.activeElement).toEqual(firstItem, 'item1 is focused') - done() + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + setTimeout(() => { + expect(document.activeElement).toEqual(firstItem, 'item1 is focused') + resolve() + }) }) - }) - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' - triggerDropdown.dispatchEvent(keydown) + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' + triggerDropdown.dispatchEvent(keydown) + }) }) - it('should not close the dropdown if the user clicks on a text field within dropdown-menu', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should not close the dropdown if the user clicks on a text field within dropdown-menu', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const input = fixtureEl.querySelector('input') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') - input.addEventListener('click', () => { - expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') - done() - }) + input.addEventListener('click', () => { + expect(triggerDropdown).toHaveClass('show') + resolve() + }) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') - input.dispatchEvent(createEvent('click')) - }) + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdown).toHaveClass('show') + input.dispatchEvent(createEvent('click')) + }) - triggerDropdown.click() + triggerDropdown.click() + }) }) - it('should not close the dropdown if the user clicks on a textarea within dropdown-menu', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should not close the dropdown if the user clicks on a textarea within dropdown-menu', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const textarea = fixtureEl.querySelector('textarea') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const textarea = fixtureEl.querySelector('textarea') - textarea.addEventListener('click', () => { - expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') - done() - }) + textarea.addEventListener('click', () => { + expect(triggerDropdown).toHaveClass('show') + resolve() + }) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') - textarea.dispatchEvent(createEvent('click')) - }) + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdown).toHaveClass('show') + textarea.dispatchEvent(createEvent('click')) + }) - triggerDropdown.click() + triggerDropdown.click() + }) }) - it('should close the dropdown if the user clicks on a text field that is not contained within dropdown-menu', done => { - fixtureEl.innerHTML = [ - '', - '' - ] + it('should close the dropdown if the user clicks on a text field that is not contained within dropdown-menu', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const input = fixtureEl.querySelector('input') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') - triggerDropdown.addEventListener('hidden.bs.dropdown', () => { - expect().nothing() - done() - }) + triggerDropdown.addEventListener('hidden.bs.dropdown', () => { + expect().nothing() + resolve() + }) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - input.dispatchEvent(createEvent('click', { - bubbles: true - })) - }) + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + input.dispatchEvent(createEvent('click', { + bubbles: true + })) + }) - triggerDropdown.click() - }) + triggerDropdown.click() + }) + }) + + it('should ignore keyboard events for s and ', + ' ', + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') + const textarea = fixtureEl.querySelector('textarea') + + const test = (eventKey, elementToDispatch) => { + const event = createEvent('keydown') + event.key = eventKey + elementToDispatch.focus() + elementToDispatch.dispatchEvent(event) + expect(document.activeElement).toEqual(elementToDispatch, `${elementToDispatch.tagName} still focused`) + } - it('should ignore keyboard events for s and ', - ' ', - '' - ].join('') + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const input = fixtureEl.querySelector('input') - const textarea = fixtureEl.querySelector('textarea') + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + // Key Space + test('Space', input) + + test('Space', textarea) + + // Key ArrowUp + test('ArrowUp', input) + + test('ArrowUp', textarea) - const keydownSpace = createEvent('keydown') - keydownSpace.key = 'Space' + // Key ArrowDown + test('ArrowDown', input) - const keydownArrowUp = createEvent('keydown') - keydownArrowUp.key = 'ArrowUp' + test('ArrowDown', textarea) - const keydownArrowDown = createEvent('keydown') - keydownArrowDown.key = 'ArrowDown' + // Key Escape + input.focus() + input.dispatchEvent(keydownEscape) - const keydownEscape = createEvent('keydown') - keydownEscape.key = 'Escape' + expect(triggerDropdown).not.toHaveClass('show') + resolve() + }) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - // Key Space - input.focus() - input.dispatchEvent(keydownSpace) + triggerDropdown.click() + }) + }) - expect(document.activeElement).toEqual(input, 'input still focused') + it('should not open dropdown if escape key was pressed on the toggle', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ].join('') - textarea.focus() - textarea.dispatchEvent(keydownSpace) + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(triggerDropdown) + const button = fixtureEl.querySelector('button[data-bs-toggle="dropdown"]') - expect(document.activeElement).toEqual(textarea, 'textarea still focused') + const spy = spyOn(dropdown, 'toggle') - // Key ArrowUp - input.focus() - input.dispatchEvent(keydownArrowUp) + // Key escape + button.focus() + // Key escape + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' + button.dispatchEvent(keydownEscape) - expect(document.activeElement).toEqual(input, 'input still focused') + setTimeout(() => { + expect(spy).not.toHaveBeenCalled() + expect(triggerDropdown).not.toHaveClass('show') + resolve() + }, 20) + }) + }) + + it('should propagate escape key events if dropdown is closed', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ].join('') + + const parent = fixtureEl.querySelector('.parent') + const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + + const parentKeyHandler = jasmine.createSpy('parentKeyHandler') + + parent.addEventListener('keydown', parentKeyHandler) + parent.addEventListener('keyup', () => { + expect(parentKeyHandler).toHaveBeenCalled() + resolve() + }) - textarea.focus() - textarea.dispatchEvent(keydownArrowUp) + const keydownEscape = createEvent('keydown', { bubbles: true }) + keydownEscape.key = 'Escape' + const keyupEscape = createEvent('keyup', { bubbles: true }) + keyupEscape.key = 'Escape' - expect(document.activeElement).toEqual(textarea, 'textarea still focused') + toggle.focus() + toggle.dispatchEvent(keydownEscape) + toggle.dispatchEvent(keyupEscape) + }) + }) - // Key ArrowDown - input.focus() - input.dispatchEvent(keydownArrowDown) + it('should not propagate escape key events if dropdown is open', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ].join('') - expect(document.activeElement).toEqual(input, 'input still focused') + const parent = fixtureEl.querySelector('.parent') + const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - textarea.focus() - textarea.dispatchEvent(keydownArrowDown) + const parentKeyHandler = jasmine.createSpy('parentKeyHandler') - expect(document.activeElement).toEqual(textarea, 'textarea still focused') + parent.addEventListener('keydown', parentKeyHandler) + parent.addEventListener('keyup', () => { + expect(parentKeyHandler).not.toHaveBeenCalled() + resolve() + }) - // Key Escape - input.focus() - input.dispatchEvent(keydownEscape) + const keydownEscape = createEvent('keydown', { bubbles: true }) + keydownEscape.key = 'Escape' + const keyupEscape = createEvent('keyup', { bubbles: true }) + keyupEscape.key = 'Escape' - expect(triggerDropdown.classList.contains('show')).toEqual(false, 'dropdown menu is not shown') - done() + toggle.click() + toggle.dispatchEvent(keydownEscape) + toggle.dispatchEvent(keyupEscape) }) - - triggerDropdown.click() }) - it('should not open dropdown if escape key was pressed on the toggle', done => { - fixtureEl.innerHTML = [ - '
', - ' ', - '
' - ] + it('should close dropdown using `escape` button, and return focus to its trigger', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(triggerDropdown) - const button = fixtureEl.querySelector('button[data-bs-toggle="dropdown"]') + const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - spyOn(dropdown, 'toggle') + toggle.addEventListener('shown.bs.dropdown', () => { + const keydownEvent = createEvent('keydown', { bubbles: true }) + keydownEvent.key = 'ArrowDown' + toggle.dispatchEvent(keydownEvent) + keydownEvent.key = 'Escape' + toggle.dispatchEvent(keydownEvent) + }) - // Key escape - button.focus() - // Key escape - const keydownEscape = createEvent('keydown') - keydownEscape.key = 'Escape' - button.dispatchEvent(keydownEscape) + toggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { + expect(document.activeElement).toEqual(toggle) + resolve() + })) - setTimeout(() => { - expect(dropdown.toggle).not.toHaveBeenCalled() - expect(triggerDropdown.classList.contains('show')).toEqual(false) - done() - }, 20) + toggle.click() + }) }) - it('should propagate escape key events if dropdown is closed', done => { - fixtureEl.innerHTML = [ - '
', - ' ', - '
' - ] + it('should close dropdown (only) by clicking inside the dropdown menu when it has data-attribute `data-bs-auto-close="inside"`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const parent = fixtureEl.querySelector('.parent') - const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const parentKeyHandler = jasmine.createSpy('parentKeyHandler') + const expectDropdownToBeOpened = () => setTimeout(() => { + expect(dropdownToggle).toHaveClass('show') + dropdownMenu.click() + }, 150) - parent.addEventListener('keydown', parentKeyHandler) - parent.addEventListener('keyup', () => { - expect(parentKeyHandler).toHaveBeenCalled() - done() - }) + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + document.documentElement.click() + expectDropdownToBeOpened() + }) - const keydownEscape = createEvent('keydown', { bubbles: true }) - keydownEscape.key = 'Escape' - const keyupEscape = createEvent('keyup', { bubbles: true }) - keyupEscape.key = 'Escape' + dropdownToggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { + expect(dropdownToggle).not.toHaveClass('show') + resolve() + })) - toggle.focus() - toggle.dispatchEvent(keydownEscape) - toggle.dispatchEvent(keyupEscape) + dropdownToggle.click() + }) }) - it('should close dropdown (only) by clicking inside the dropdown menu when it has data-attribute `data-bs-auto-close="inside"`', done => { - fixtureEl.innerHTML = [ - '' - ] + it('should close dropdown (only) by clicking outside the dropdown menu when it has data-attribute `data-bs-auto-close="outside"`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const expectDropdownToBeOpened = () => setTimeout(() => { - expect(dropdownToggle.classList.contains('show')).toEqual(true) - dropdownMenu.click() - }, 150) + const expectDropdownToBeOpened = () => setTimeout(() => { + expect(dropdownToggle).toHaveClass('show') + document.documentElement.click() + }, 150) - dropdownToggle.addEventListener('shown.bs.dropdown', () => { - document.documentElement.click() - expectDropdownToBeOpened() - }) + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + dropdownMenu.click() + expectDropdownToBeOpened() + }) - dropdownToggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { - expect(dropdownToggle.classList.contains('show')).toEqual(false) - done() - })) + dropdownToggle.addEventListener('hidden.bs.dropdown', () => { + expect(dropdownToggle).not.toHaveClass('show') + resolve() + }) - dropdownToggle.click() + dropdownToggle.click() + }) }) - it('should close dropdown (only) by clicking outside the dropdown menu when it has data-attribute `data-bs-auto-close="outside"`', done => { - fixtureEl.innerHTML = [ - '' - ] + it('should not close dropdown by clicking inside or outside the dropdown menu when it has data-attribute `data-bs-auto-close="false"`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const expectDropdownToBeOpened = () => setTimeout(() => { - expect(dropdownToggle.classList.contains('show')).toEqual(true) - document.documentElement.click() - }, 150) + const expectDropdownToBeOpened = (shouldTriggerClick = true) => setTimeout(() => { + expect(dropdownToggle).toHaveClass('show') + if (shouldTriggerClick) { + document.documentElement.click() + } else { + resolve() + } - dropdownToggle.addEventListener('shown.bs.dropdown', () => { - dropdownMenu.click() - expectDropdownToBeOpened() - }) + expectDropdownToBeOpened(false) + }, 150) - dropdownToggle.addEventListener('hidden.bs.dropdown', () => { - expect(dropdownToggle.classList.contains('show')).toEqual(false) - done() - }) + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + dropdownMenu.click() + expectDropdownToBeOpened() + }) - dropdownToggle.click() + dropdownToggle.click() + }) }) - it('should not close dropdown by clicking inside or outside the dropdown menu when it has data-attribute `data-bs-auto-close="false"`', done => { + it('should be able to identify clicked dropdown, no matter the markup order', () => { fixtureEl.innerHTML = [ '', + ' ', '' - ] + ].join('') const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - - const expectDropdownToBeOpened = (shouldTriggerClick = true) => setTimeout(() => { - expect(dropdownToggle.classList.contains('show')).toEqual(true) - if (shouldTriggerClick) { - document.documentElement.click() - } else { - done() - } - - expectDropdownToBeOpened(false) - }, 150) - - dropdownToggle.addEventListener('shown.bs.dropdown', () => { - dropdownMenu.click() - expectDropdownToBeOpened() - }) + const spy = spyOn(Dropdown, 'getOrCreateInstance').and.callThrough() dropdownToggle.click() + expect(spy).toHaveBeenCalledWith(dropdownToggle) + dropdownMenu.click() + expect(spy).toHaveBeenCalledWith(dropdownToggle) }) }) @@ -1965,7 +2277,7 @@ describe('Dropdown', () => { const div = fixtureEl.querySelector('div') - expect(Dropdown.getInstance(div)).toEqual(null) + expect(Dropdown.getInstance(div)).toBeNull() }) }) @@ -1986,7 +2298,7 @@ describe('Dropdown', () => { const div = fixtureEl.querySelector('div') - expect(Dropdown.getInstance(div)).toEqual(null) + expect(Dropdown.getInstance(div)).toBeNull() expect(Dropdown.getOrCreateInstance(div)).toBeInstanceOf(Dropdown) }) @@ -1995,7 +2307,7 @@ describe('Dropdown', () => { const div = fixtureEl.querySelector('div') - expect(Dropdown.getInstance(div)).toEqual(null) + expect(Dropdown.getInstance(div)).toBeNull() const dropdown = Dropdown.getOrCreateInstance(div, { display: 'dynamic' }) @@ -2023,52 +2335,54 @@ describe('Dropdown', () => { }) }) - it('should open dropdown when pressing keydown or keyup', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should open dropdown when pressing keydown or keyup', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = fixtureEl.querySelector('.dropdown') - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' - const keyup = createEvent('keyup') - keyup.key = 'ArrowUp' + const keyup = createEvent('keyup') + keyup.key = 'ArrowUp' - const handleArrowDown = () => { - expect(triggerDropdown.classList.contains('show')).toEqual(true) - expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true') - setTimeout(() => { - dropdown.hide() - keydown.key = 'ArrowUp' - triggerDropdown.dispatchEvent(keyup) - }, 20) - } - - const handleArrowUp = () => { - expect(triggerDropdown.classList.contains('show')).toEqual(true) - expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - } - - dropdown.addEventListener('shown.bs.dropdown', event => { - if (event.target.key === 'ArrowDown') { - handleArrowDown() - } else { - handleArrowUp() + const handleArrowDown = () => { + expect(triggerDropdown).toHaveClass('show') + expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true') + setTimeout(() => { + dropdown.hide() + keydown.key = 'ArrowUp' + triggerDropdown.dispatchEvent(keyup) + }, 20) } - }) - triggerDropdown.dispatchEvent(keydown) + const handleArrowUp = () => { + expect(triggerDropdown).toHaveClass('show') + expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + } + + dropdown.addEventListener('shown.bs.dropdown', event => { + if (event.target.key === 'ArrowDown') { + handleArrowDown() + } else { + handleArrowUp() + } + }) + + triggerDropdown.dispatchEvent(keydown) + }) }) it('should allow `data-bs-toggle="dropdown"` click events to bubble up', () => { @@ -2094,27 +2408,29 @@ describe('Dropdown', () => { expect(delegatedClickListener).toHaveBeenCalled() }) - it('should open the dropdown when clicking the child element inside `data-bs-toggle="dropdown"`', done => { - fixtureEl.innerHTML = [ - '
', - ' ', - '
' - ].join('') + it('should open the dropdown when clicking the child element inside `data-bs-toggle="dropdown"`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const childElement = fixtureEl.querySelector('#childElement') + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const childElement = fixtureEl.querySelector('#childElement') - btnDropdown.addEventListener('shown.bs.dropdown', () => setTimeout(() => { - expect(btnDropdown.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - })) + btnDropdown.addEventListener('shown.bs.dropdown', () => setTimeout(() => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + })) - childElement.click() + childElement.click() + }) }) }) diff --git a/js/tests/unit/jquery.spec.js b/js/tests/unit/jquery.spec.js index 289612df50ce..7d7f29dc7288 100644 --- a/js/tests/unit/jquery.spec.js +++ b/js/tests/unit/jquery.spec.js @@ -1,18 +1,18 @@ /* eslint-env jquery */ -import Alert from '../../src/alert' -import Button from '../../src/button' -import Carousel from '../../src/carousel' -import Collapse from '../../src/collapse' -import Dropdown from '../../src/dropdown' -import Modal from '../../src/modal' -import Popover from '../../src/popover' -import ScrollSpy from '../../src/scrollspy' -import Tab from '../../src/tab' -import Toast from '../../src/toast' -import Tooltip from '../../src/tooltip' - -/** Test helpers */ -import { getFixture, clearFixture } from '../helpers/fixture' + +import Alert from '../../src/alert.js' +import Button from '../../src/button.js' +import Carousel from '../../src/carousel.js' +import Collapse from '../../src/collapse.js' +import Dropdown from '../../src/dropdown.js' +import Modal from '../../src/modal.js' +import Offcanvas from '../../src/offcanvas.js' +import Popover from '../../src/popover.js' +import ScrollSpy from '../../src/scrollspy.js' +import Tab from '../../src/tab.js' +import Toast from '../../src/toast.js' +import Tooltip from '../../src/tooltip.js' +import { clearFixture, getFixture } from '../helpers/fixture.js' describe('jQuery', () => { let fixtureEl @@ -32,6 +32,7 @@ describe('jQuery', () => { expect(Collapse.jQueryInterface).toEqual(jQuery.fn.collapse) expect(Dropdown.jQueryInterface).toEqual(jQuery.fn.dropdown) expect(Modal.jQueryInterface).toEqual(jQuery.fn.modal) + expect(Offcanvas.jQueryInterface).toEqual(jQuery.fn.offcanvas) expect(Popover.jQueryInterface).toEqual(jQuery.fn.popover) expect(ScrollSpy.jQueryInterface).toEqual(jQuery.fn.scrollspy) expect(Tab.jQueryInterface).toEqual(jQuery.fn.tab) @@ -39,19 +40,21 @@ describe('jQuery', () => { expect(Tooltip.jQueryInterface).toEqual(jQuery.fn.tooltip) }) - it('should use jQuery event system', done => { - fixtureEl.innerHTML = [ - '
', - ' ', - '
' - ].join('') - - $(fixtureEl).find('.alert') - .one('closed.bs.alert', () => { - expect($(fixtureEl).find('.alert').length).toEqual(0) - done() - }) - - $(fixtureEl).find('button').trigger('click') + it('should use jQuery event system', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ].join('') + + $(fixtureEl).find('.alert') + .one('closed.bs.alert', () => { + expect($(fixtureEl).find('.alert')).toHaveSize(0) + resolve() + }) + + $(fixtureEl).find('button').trigger('click') + }) }) }) diff --git a/js/tests/unit/modal.spec.js b/js/tests/unit/modal.spec.js index a65ed4afa8ff..2aa0b7655c14 100644 --- a/js/tests/unit/modal.spec.js +++ b/js/tests/unit/modal.spec.js @@ -1,9 +1,9 @@ -import Modal from '../../src/modal' -import EventHandler from '../../src/dom/event-handler' -import ScrollBarHelper from '../../src/util/scrollbar' - -/** Test helpers */ -import { clearBodyAndDocument, clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture' +import EventHandler from '../../src/dom/event-handler.js' +import Modal from '../../src/modal.js' +import ScrollBarHelper from '../../src/util/scrollbar.js' +import { + clearBodyAndDocument, clearFixture, createEvent, getFixture, jQueryMock +} from '../helpers/fixture.js' describe('Modal', () => { let fixtureEl @@ -17,10 +17,9 @@ describe('Modal', () => { clearBodyAndDocument() document.body.classList.remove('modal-open') - document.querySelectorAll('.modal-backdrop') - .forEach(backdrop => { - backdrop.remove() - }) + for (const backdrop of document.querySelectorAll('.modal-backdrop')) { + backdrop.remove() + } }) beforeEach(() => { @@ -59,95 +58,101 @@ describe('Modal', () => { }) describe('toggle', () => { - it('should call ScrollBarHelper to handle scrollBar on body', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should call ScrollBarHelper to handle scrollBar on body', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + const spyHide = spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() + const spyReset = spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + + modalEl.addEventListener('shown.bs.modal', () => { + expect(spyHide).toHaveBeenCalled() + modal.toggle() + }) - spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() - spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(spyReset).toHaveBeenCalled() + resolve() + }) - modalEl.addEventListener('shown.bs.modal', () => { - expect(ScrollBarHelper.prototype.hide).toHaveBeenCalled() modal.toggle() }) - - modalEl.addEventListener('hidden.bs.modal', () => { - expect(ScrollBarHelper.prototype.reset).toHaveBeenCalled() - done() - }) - - modal.toggle() }) }) describe('show', () => { - it('should show a modal', done => { - fixtureEl.innerHTML = '' + it('should show a modal', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - modalEl.addEventListener('show.bs.modal', e => { - expect(e).toBeDefined() - }) + modalEl.addEventListener('show.bs.modal', event => { + expect(event).toBeDefined() + }) - modalEl.addEventListener('shown.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toEqual('true') - expect(modalEl.getAttribute('role')).toEqual('dialog') - expect(modalEl.getAttribute('aria-hidden')).toBeNull() - expect(modalEl.style.display).toEqual('block') - expect(document.querySelector('.modal-backdrop')).not.toBeNull() - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toEqual('true') + expect(modalEl.getAttribute('role')).toEqual('dialog') + expect(modalEl.getAttribute('aria-hidden')).toBeNull() + expect(modalEl.style.display).toEqual('block') + expect(document.querySelector('.modal-backdrop')).not.toBeNull() + resolve() + }) - modal.show() + modal.show() + }) }) - it('should show a modal without backdrop', done => { - fixtureEl.innerHTML = '' + it('should show a modal without backdrop', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - backdrop: false - }) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + backdrop: false + }) - modalEl.addEventListener('show.bs.modal', e => { - expect(e).toBeDefined() - }) + modalEl.addEventListener('show.bs.modal', event => { + expect(event).toBeDefined() + }) - modalEl.addEventListener('shown.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toEqual('true') - expect(modalEl.getAttribute('role')).toEqual('dialog') - expect(modalEl.getAttribute('aria-hidden')).toBeNull() - expect(modalEl.style.display).toEqual('block') - expect(document.querySelector('.modal-backdrop')).toBeNull() - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toEqual('true') + expect(modalEl.getAttribute('role')).toEqual('dialog') + expect(modalEl.getAttribute('aria-hidden')).toBeNull() + expect(modalEl.style.display).toEqual('block') + expect(document.querySelector('.modal-backdrop')).toBeNull() + resolve() + }) - modal.show() + modal.show() + }) }) - it('should show a modal and append the element', done => { - const modalEl = document.createElement('div') - const id = 'dynamicModal' + it('should show a modal and append the element', () => { + return new Promise(resolve => { + const modalEl = document.createElement('div') + const id = 'dynamicModal' - modalEl.setAttribute('id', id) - modalEl.classList.add('modal') - modalEl.innerHTML = '' + modalEl.setAttribute('id', id) + modalEl.classList.add('modal') + modalEl.innerHTML = '' - const modal = new Modal(modalEl) + const modal = new Modal(modalEl) - modalEl.addEventListener('shown.bs.modal', () => { - const dynamicModal = document.getElementById(id) - expect(dynamicModal).not.toBeNull() - dynamicModal.remove() - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + const dynamicModal = document.getElementById(id) + expect(dynamicModal).not.toBeNull() + dynamicModal.remove() + resolve() + }) - modal.show() + modal.show() + }) }) it('should do nothing if a modal is shown', () => { @@ -156,12 +161,12 @@ describe('Modal', () => { const modalEl = fixtureEl.querySelector('.modal') const modal = new Modal(modalEl) - spyOn(EventHandler, 'trigger') + const spy = spyOn(EventHandler, 'trigger') modal._isShown = true modal.show() - expect(EventHandler.trigger).not.toHaveBeenCalled() + expect(spy).not.toHaveBeenCalled() }) it('should do nothing if a modal is transitioning', () => { @@ -170,488 +175,595 @@ describe('Modal', () => { const modalEl = fixtureEl.querySelector('.modal') const modal = new Modal(modalEl) - spyOn(EventHandler, 'trigger') + const spy = spyOn(EventHandler, 'trigger') modal._isTransitioning = true modal.show() - expect(EventHandler.trigger).not.toHaveBeenCalled() + expect(spy).not.toHaveBeenCalled() }) - it('should not fire shown event when show is prevented', done => { - fixtureEl.innerHTML = '' + it('should not fire shown event when show is prevented', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - modalEl.addEventListener('show.bs.modal', e => { - e.preventDefault() + modalEl.addEventListener('show.bs.modal', event => { + event.preventDefault() - const expectedDone = () => { - expect().nothing() - done() - } + const expectedDone = () => { + expect().nothing() + resolve() + } - setTimeout(expectedDone, 10) - }) + setTimeout(expectedDone, 10) + }) - modalEl.addEventListener('shown.bs.modal', () => { - throw new Error('shown event triggered') - }) + modalEl.addEventListener('shown.bs.modal', () => { + reject(new Error('shown event triggered')) + }) - modal.show() + modal.show() + }) }) - it('should be shown after the first call to show() has been prevented while fading is enabled ', done => { - fixtureEl.innerHTML = '' + it('should be shown after the first call to show() has been prevented while fading is enabled ', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - let prevented = false - modalEl.addEventListener('show.bs.modal', e => { - if (!prevented) { - e.preventDefault() - prevented = true + let prevented = false + modalEl.addEventListener('show.bs.modal', event => { + if (!prevented) { + event.preventDefault() + prevented = true - setTimeout(() => { - modal.show() - }) - } - }) + setTimeout(() => { + modal.show() + }) + } + }) - modalEl.addEventListener('shown.bs.modal', () => { - expect(prevented).toBeTrue() - expect(modal._isAnimated()).toBeTrue() - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + expect(prevented).toBeTrue() + expect(modal._isAnimated()).toBeTrue() + resolve() + }) - modal.show() + modal.show() + }) }) + it('should set is transitioning if fade class is present', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - it('should set is transitioning if fade class is present', done => { - fixtureEl.innerHTML = '' + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + modalEl.addEventListener('show.bs.modal', () => { + setTimeout(() => { + expect(modal._isTransitioning).toBeTrue() + }) + }) - modalEl.addEventListener('show.bs.modal', () => { - setTimeout(() => { - expect(modal._isTransitioning).toEqual(true) + modalEl.addEventListener('shown.bs.modal', () => { + expect(modal._isTransitioning).toBeFalse() + resolve() }) - }) - modalEl.addEventListener('shown.bs.modal', () => { - expect(modal._isTransitioning).toEqual(false) - done() + modal.show() }) - - modal.show() }) - it('should close modal when a click occurred on data-bs-dismiss="modal" inside modal', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const modalEl = fixtureEl.querySelector('.modal') - const btnClose = fixtureEl.querySelector('[data-bs-dismiss="modal"]') - const modal = new Modal(modalEl) - - spyOn(modal, 'hide').and.callThrough() + it('should close modal when a click occurred on data-bs-dismiss="modal" inside modal', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const modalEl = fixtureEl.querySelector('.modal') + const btnClose = fixtureEl.querySelector('[data-bs-dismiss="modal"]') + const modal = new Modal(modalEl) + + const spy = spyOn(modal, 'hide').and.callThrough() + + modalEl.addEventListener('shown.bs.modal', () => { + btnClose.click() + }) - modalEl.addEventListener('shown.bs.modal', () => { - btnClose.click() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(spy).toHaveBeenCalled() + resolve() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modal.hide).toHaveBeenCalled() - done() + modal.show() }) - - modal.show() }) - it('should close modal when a click occurred on a data-bs-dismiss="modal" with "bs-target" outside of modal element', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') + it('should close modal when a click occurred on a data-bs-dismiss="modal" with "bs-target" outside of modal element', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') - const modalEl = fixtureEl.querySelector('.modal') - const btnClose = fixtureEl.querySelector('[data-bs-dismiss="modal"]') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const btnClose = fixtureEl.querySelector('[data-bs-dismiss="modal"]') + const modal = new Modal(modalEl) - spyOn(modal, 'hide').and.callThrough() + const spy = spyOn(modal, 'hide').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - btnClose.click() - }) + modalEl.addEventListener('shown.bs.modal', () => { + btnClose.click() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modal.hide).toHaveBeenCalled() - done() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(spy).toHaveBeenCalled() + resolve() + }) - modal.show() + modal.show() + }) }) - it('should set .modal\'s scroll top to 0', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should set .modal\'s scroll top to 0', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - modalEl.addEventListener('shown.bs.modal', () => { - expect(modalEl.scrollTop).toEqual(0) - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + expect(modalEl.scrollTop).toEqual(0) + resolve() + }) - modal.show() + modal.show() + }) }) - it('should set modal body scroll top to 0 if modal body do not exists', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const modalEl = fixtureEl.querySelector('.modal') - const modalBody = modalEl.querySelector('.modal-body') - const modal = new Modal(modalEl) + it('should set modal body scroll top to 0 if modal body do not exists', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const modalEl = fixtureEl.querySelector('.modal') + const modalBody = modalEl.querySelector('.modal-body') + const modal = new Modal(modalEl) + + modalEl.addEventListener('shown.bs.modal', () => { + expect(modalBody.scrollTop).toEqual(0) + resolve() + }) - modalEl.addEventListener('shown.bs.modal', () => { - expect(modalBody.scrollTop).toEqual(0) - done() + modal.show() }) - - modal.show() }) - it('should not trap focus if focus equal to false', done => { - fixtureEl.innerHTML = '' + it('should not trap focus if focus equal to false', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - focus: false - }) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + focus: false + }) - spyOn(modal._focustrap, 'activate').and.callThrough() + const spy = spyOn(modal._focustrap, 'activate').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - expect(modal._focustrap.activate).not.toHaveBeenCalled() - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + expect(spy).not.toHaveBeenCalled() + resolve() + }) - modal.show() + modal.show() + }) }) - it('should add listener when escape touch is pressed', done => { - fixtureEl.innerHTML = '' + it('should add listener when escape touch is pressed', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - spyOn(modal, 'hide').and.callThrough() + const spy = spyOn(modal, 'hide').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - const keydownEscape = createEvent('keydown') - keydownEscape.key = 'Escape' + modalEl.addEventListener('shown.bs.modal', () => { + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' - modalEl.dispatchEvent(keydownEscape) - }) + modalEl.dispatchEvent(keydownEscape) + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modal.hide).toHaveBeenCalled() - done() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(spy).toHaveBeenCalled() + resolve() + }) - modal.show() + modal.show() + }) }) - it('should do nothing when the pressed key is not escape', done => { - fixtureEl.innerHTML = '' + it('should do nothing when the pressed key is not escape', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - spyOn(modal, 'hide') + const spy = spyOn(modal, 'hide') - const expectDone = () => { - expect(modal.hide).not.toHaveBeenCalled() + const expectDone = () => { + expect(spy).not.toHaveBeenCalled() - done() - } + resolve() + } - modalEl.addEventListener('shown.bs.modal', () => { - const keydownTab = createEvent('keydown') - keydownTab.key = 'Tab' + modalEl.addEventListener('shown.bs.modal', () => { + const keydownTab = createEvent('keydown') + keydownTab.key = 'Tab' - modalEl.dispatchEvent(keydownTab) - setTimeout(expectDone, 30) - }) + modalEl.dispatchEvent(keydownTab) + setTimeout(expectDone, 30) + }) - modal.show() + modal.show() + }) }) - it('should adjust dialog on resize', done => { - fixtureEl.innerHTML = '' + it('should adjust dialog on resize', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - spyOn(modal, '_adjustDialog').and.callThrough() + const spy = spyOn(modal, '_adjustDialog').and.callThrough() - const expectDone = () => { - expect(modal._adjustDialog).toHaveBeenCalled() + const expectDone = () => { + expect(spy).toHaveBeenCalled() - done() - } + resolve() + } - modalEl.addEventListener('shown.bs.modal', () => { - const resizeEvent = createEvent('resize') + modalEl.addEventListener('shown.bs.modal', () => { + const resizeEvent = createEvent('resize') - window.dispatchEvent(resizeEvent) - setTimeout(expectDone, 10) - }) + window.dispatchEvent(resizeEvent) + setTimeout(expectDone, 10) + }) - modal.show() + modal.show() + }) }) - it('should not close modal when clicking outside of modal-content if backdrop = false', done => { - fixtureEl.innerHTML = '' + it('should not close modal when clicking on modal-content', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = [ + '' + ].join('') - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - backdrop: false - }) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - const shownCallback = () => { - setTimeout(() => { - expect(modal._isShown).toEqual(true) - done() - }, 10) - } + const shownCallback = () => { + setTimeout(() => { + expect(modal._isShown).toEqual(true) + resolve() + }, 10) + } - modalEl.addEventListener('shown.bs.modal', () => { - modalEl.click() - shownCallback() - }) + modalEl.addEventListener('shown.bs.modal', () => { + fixtureEl.querySelector('.modal-dialog').click() + fixtureEl.querySelector('.modal-content').click() + shownCallback() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - throw new Error('Should not hide a modal') - }) + modalEl.addEventListener('hidden.bs.modal', () => { + reject(new Error('Should not hide a modal')) + }) - modal.show() + modal.show() + }) }) - it('should not close modal when clicking outside of modal-content if backdrop = static', done => { - fixtureEl.innerHTML = '' + it('should not close modal when clicking outside of modal-content if backdrop = false', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - backdrop: 'static' - }) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + backdrop: false + }) - const shownCallback = () => { - setTimeout(() => { - expect(modal._isShown).toEqual(true) - done() - }, 10) - } + const shownCallback = () => { + setTimeout(() => { + expect(modal._isShown).toBeTrue() + resolve() + }, 10) + } - modalEl.addEventListener('shown.bs.modal', () => { - modalEl.click() - shownCallback() - }) + modalEl.addEventListener('shown.bs.modal', () => { + modalEl.click() + shownCallback() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - throw new Error('Should not hide a modal') - }) + modalEl.addEventListener('hidden.bs.modal', () => { + reject(new Error('Should not hide a modal')) + }) - modal.show() + modal.show() + }) }) - it('should close modal when escape key is pressed with keyboard = true and backdrop is static', done => { - fixtureEl.innerHTML = '' + it('should not close modal when clicking outside of modal-content if backdrop = static', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - backdrop: 'static', - keyboard: true - }) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + backdrop: 'static' + }) + + const shownCallback = () => { + setTimeout(() => { + expect(modal._isShown).toBeTrue() + resolve() + }, 10) + } - const shownCallback = () => { - setTimeout(() => { - expect(modal._isShown).toEqual(false) - done() - }, 10) - } + modalEl.addEventListener('shown.bs.modal', () => { + modalEl.click() + shownCallback() + }) - modalEl.addEventListener('shown.bs.modal', () => { - const keydownEscape = createEvent('keydown') - keydownEscape.key = 'Escape' + modalEl.addEventListener('hidden.bs.modal', () => { + reject(new Error('Should not hide a modal')) + }) - modalEl.dispatchEvent(keydownEscape) - shownCallback() + modal.show() }) - - modal.show() }) + it('should close modal when escape key is pressed with keyboard = true and backdrop is static', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + backdrop: 'static', + keyboard: true + }) - it('should not close modal when escape key is pressed with keyboard = false', done => { - fixtureEl.innerHTML = '' + const shownCallback = () => { + setTimeout(() => { + expect(modal._isShown).toBeFalse() + resolve() + }, 10) + } - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - keyboard: false + modalEl.addEventListener('shown.bs.modal', () => { + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' + + modalEl.dispatchEvent(keydownEscape) + shownCallback() + }) + + modal.show() }) + }) - const shownCallback = () => { - setTimeout(() => { - expect(modal._isShown).toEqual(true) - done() - }, 10) - } + it('should not close modal when escape key is pressed with keyboard = false', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = '' - modalEl.addEventListener('shown.bs.modal', () => { - const keydownEscape = createEvent('keydown') - keydownEscape.key = 'Escape' + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + keyboard: false + }) - modalEl.dispatchEvent(keydownEscape) - shownCallback() - }) + const shownCallback = () => { + setTimeout(() => { + expect(modal._isShown).toBeTrue() + resolve() + }, 10) + } - modalEl.addEventListener('hidden.bs.modal', () => { - throw new Error('Should not hide a modal') - }) + modalEl.addEventListener('shown.bs.modal', () => { + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' - modal.show() - }) + modalEl.dispatchEvent(keydownEscape) + shownCallback() + }) - it('should not overflow when clicking outside of modal-content if backdrop = static', done => { - fixtureEl.innerHTML = '' + modalEl.addEventListener('hidden.bs.modal', () => { + reject(new Error('Should not hide a modal')) + }) - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - backdrop: 'static' + modal.show() }) + }) - modalEl.addEventListener('shown.bs.modal', () => { - modalEl.click() - setTimeout(() => { - expect(modalEl.clientHeight).toEqual(modalEl.scrollHeight) - done() - }, 20) - }) + it('should not overflow when clicking outside of modal-content if backdrop = static', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - modal.show() - }) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + backdrop: 'static' + }) - it('should not queue multiple callbacks when clicking outside of modal-content and backdrop = static', done => { - fixtureEl.innerHTML = '' + modalEl.addEventListener('shown.bs.modal', () => { + modalEl.click() + setTimeout(() => { + expect(modalEl.clientHeight).toEqual(modalEl.scrollHeight) + resolve() + }, 20) + }) - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - backdrop: 'static' + modal.show() }) + }) - modalEl.addEventListener('shown.bs.modal', () => { - const spy = spyOn(modal, '_queueCallback').and.callThrough() + it('should not queue multiple callbacks when clicking outside of modal-content and backdrop = static', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - modalEl.click() - modalEl.click() + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + backdrop: 'static' + }) - setTimeout(() => { - expect(spy).toHaveBeenCalledTimes(1) - done() - }, 20) - }) + modalEl.addEventListener('shown.bs.modal', () => { + const spy = spyOn(modal, '_queueCallback').and.callThrough() + const mouseDown = createEvent('mousedown') - modal.show() + modalEl.dispatchEvent(mouseDown) + modalEl.click() + modalEl.dispatchEvent(mouseDown) + modalEl.click() + + setTimeout(() => { + expect(spy).toHaveBeenCalledTimes(1) + resolve() + }, 20) + }) + + modal.show() + }) }) - it('should trap focus', done => { - fixtureEl.innerHTML = '' + it('should trap focus', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - spyOn(modal._focustrap, 'activate').and.callThrough() + const spy = spyOn(modal._focustrap, 'activate').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - expect(modal._focustrap.activate).toHaveBeenCalled() - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + expect(spy).toHaveBeenCalled() + resolve() + }) - modal.show() + modal.show() + }) }) }) describe('hide', () => { - it('should hide a modal', done => { - fixtureEl.innerHTML = '' + it('should hide a modal', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + const backdropSpy = spyOn(modal._backdrop, 'hide').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - modal.hide() - }) + modalEl.addEventListener('shown.bs.modal', () => { + modal.hide() + }) - modalEl.addEventListener('hide.bs.modal', e => { - expect(e).toBeDefined() - }) + modalEl.addEventListener('hide.bs.modal', event => { + expect(event).toBeDefined() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toBeNull() - expect(modalEl.getAttribute('role')).toBeNull() - expect(modalEl.getAttribute('aria-hidden')).toEqual('true') - expect(modalEl.style.display).toEqual('none') - expect(document.querySelector('.modal-backdrop')).toBeNull() - done() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toBeNull() + expect(modalEl.getAttribute('role')).toBeNull() + expect(modalEl.getAttribute('aria-hidden')).toEqual('true') + expect(modalEl.style.display).toEqual('none') + expect(backdropSpy).toHaveBeenCalled() + resolve() + }) - modal.show() + modal.show() + }) }) - it('should close modal when clicking outside of modal-content', done => { - fixtureEl.innerHTML = '' + it('should close modal when clicking outside of modal-content', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const dialogEl = modalEl.querySelector('.modal-dialog') + const modal = new Modal(modalEl) - modalEl.addEventListener('shown.bs.modal', () => { - modalEl.click() - }) + const spy = spyOn(modal, 'hide') + + modalEl.addEventListener('shown.bs.modal', () => { + const mouseDown = createEvent('mousedown') + + dialogEl.dispatchEvent(mouseDown) + modalEl.click() + expect(spy).not.toHaveBeenCalled() - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toBeNull() - expect(modalEl.getAttribute('role')).toBeNull() - expect(modalEl.getAttribute('aria-hidden')).toEqual('true') - expect(modalEl.style.display).toEqual('none') - expect(document.querySelector('.modal-backdrop')).toBeNull() - done() + modalEl.dispatchEvent(mouseDown) + modalEl.click() + expect(spy).toHaveBeenCalled() + resolve() + }) + + modal.show() }) + }) - modal.show() + it('should not close modal when clicking on an element removed from modal content', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const modalEl = fixtureEl.querySelector('.modal') + const buttonEl = modalEl.querySelector('.btn') + const modal = new Modal(modalEl) + + const spy = spyOn(modal, 'hide') + buttonEl.addEventListener('click', () => { + buttonEl.remove() + }) + + modalEl.addEventListener('shown.bs.modal', () => { + modalEl.dispatchEvent(createEvent('mousedown')) + buttonEl.click() + expect(spy).not.toHaveBeenCalled() + resolve() + }) + + modal.show() + }) }) it('should do nothing is the modal is not shown', () => { @@ -677,52 +789,56 @@ describe('Modal', () => { expect().nothing() }) - it('should not hide a modal if hide is prevented', done => { - fixtureEl.innerHTML = '' + it('should not hide a modal if hide is prevented', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - modalEl.addEventListener('shown.bs.modal', () => { - modal.hide() - }) + modalEl.addEventListener('shown.bs.modal', () => { + modal.hide() + }) - const hideCallback = () => { - setTimeout(() => { - expect(modal._isShown).toEqual(true) - done() - }, 10) - } + const hideCallback = () => { + setTimeout(() => { + expect(modal._isShown).toBeTrue() + resolve() + }, 10) + } - modalEl.addEventListener('hide.bs.modal', e => { - e.preventDefault() - hideCallback() - }) + modalEl.addEventListener('hide.bs.modal', event => { + event.preventDefault() + hideCallback() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - throw new Error('should not trigger hidden') - }) + modalEl.addEventListener('hidden.bs.modal', () => { + reject(new Error('should not trigger hidden')) + }) - modal.show() + modal.show() + }) }) - it('should release focus trap', done => { - fixtureEl.innerHTML = '' + it('should release focus trap', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) - spyOn(modal._focustrap, 'deactivate').and.callThrough() + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + const spy = spyOn(modal._focustrap, 'deactivate').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - modal.hide() - }) + modalEl.addEventListener('shown.bs.modal', () => { + modal.hide() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modal._focustrap.deactivate).toHaveBeenCalled() - done() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(spy).toHaveBeenCalled() + resolve() + }) - modal.show() + modal.show() + }) }) }) @@ -733,17 +849,17 @@ describe('Modal', () => { const modalEl = fixtureEl.querySelector('.modal') const modal = new Modal(modalEl) const focustrap = modal._focustrap - spyOn(focustrap, 'deactivate').and.callThrough() + const spyDeactivate = spyOn(focustrap, 'deactivate').and.callThrough() expect(Modal.getInstance(modalEl)).toEqual(modal) - spyOn(EventHandler, 'off') + const spyOff = spyOn(EventHandler, 'off') modal.dispose() expect(Modal.getInstance(modalEl)).toBeNull() - expect(EventHandler.off).toHaveBeenCalledTimes(3) - expect(focustrap.deactivate).toHaveBeenCalled() + expect(spyOff).toHaveBeenCalledTimes(3) + expect(spyDeactivate).toHaveBeenCalled() }) }) @@ -754,255 +870,298 @@ describe('Modal', () => { const modalEl = fixtureEl.querySelector('.modal') const modal = new Modal(modalEl) - spyOn(modal, '_adjustDialog') + const spy = spyOn(modal, '_adjustDialog') modal.handleUpdate() - expect(modal._adjustDialog).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) }) describe('data-api', () => { - it('should toggle modal', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') + it('should toggle modal', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') + + const modalEl = fixtureEl.querySelector('.modal') + const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') + + modalEl.addEventListener('shown.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toEqual('true') + expect(modalEl.getAttribute('role')).toEqual('dialog') + expect(modalEl.getAttribute('aria-hidden')).toBeNull() + expect(modalEl.style.display).toEqual('block') + expect(document.querySelector('.modal-backdrop')).not.toBeNull() + setTimeout(() => trigger.click(), 10) + }) - const modalEl = fixtureEl.querySelector('.modal') - const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') - - modalEl.addEventListener('shown.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toEqual('true') - expect(modalEl.getAttribute('role')).toEqual('dialog') - expect(modalEl.getAttribute('aria-hidden')).toBeNull() - expect(modalEl.style.display).toEqual('block') - expect(document.querySelector('.modal-backdrop')).not.toBeNull() - setTimeout(() => trigger.click(), 10) - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toBeNull() + expect(modalEl.getAttribute('role')).toBeNull() + expect(modalEl.getAttribute('aria-hidden')).toEqual('true') + expect(modalEl.style.display).toEqual('none') + expect(document.querySelector('.modal-backdrop')).toBeNull() + resolve() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toBeNull() - expect(modalEl.getAttribute('role')).toBeNull() - expect(modalEl.getAttribute('aria-hidden')).toEqual('true') - expect(modalEl.style.display).toEqual('none') - expect(document.querySelector('.modal-backdrop')).toBeNull() - done() + trigger.click() }) - - trigger.click() }) - it('should not recreate a new modal', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') + it('should not recreate a new modal', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) - const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') - spyOn(modal, 'show').and.callThrough() + const spy = spyOn(modal, 'show').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - expect(modal.show).toHaveBeenCalled() - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + expect(spy).toHaveBeenCalled() + resolve() + }) - trigger.click() + trigger.click() + }) }) - it('should prevent default when the trigger is or ', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') + it('should prevent default when the trigger is or ', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') + + const modalEl = fixtureEl.querySelector('.modal') + const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') + + const spy = spyOn(Event.prototype, 'preventDefault').and.callThrough() + + modalEl.addEventListener('shown.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toEqual('true') + expect(modalEl.getAttribute('role')).toEqual('dialog') + expect(modalEl.getAttribute('aria-hidden')).toBeNull() + expect(modalEl.style.display).toEqual('block') + expect(document.querySelector('.modal-backdrop')).not.toBeNull() + expect(spy).toHaveBeenCalled() + resolve() + }) - const modalEl = fixtureEl.querySelector('.modal') - const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') - - spyOn(Event.prototype, 'preventDefault').and.callThrough() - - modalEl.addEventListener('shown.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toEqual('true') - expect(modalEl.getAttribute('role')).toEqual('dialog') - expect(modalEl.getAttribute('aria-hidden')).toBeNull() - expect(modalEl.style.display).toEqual('block') - expect(document.querySelector('.modal-backdrop')).not.toBeNull() - expect(Event.prototype.preventDefault).toHaveBeenCalled() - done() + trigger.click() }) - - trigger.click() }) - it('should focus the trigger on hide', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') + it('should focus the trigger on hide', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') - const modalEl = fixtureEl.querySelector('.modal') - const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') + const modalEl = fixtureEl.querySelector('.modal') + const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') - spyOn(trigger, 'focus') + const spy = spyOn(trigger, 'focus') - modalEl.addEventListener('shown.bs.modal', () => { - const modal = Modal.getInstance(modalEl) + modalEl.addEventListener('shown.bs.modal', () => { + const modal = Modal.getInstance(modalEl) - modal.hide() - }) + modal.hide() + }) - const hideListener = () => { - setTimeout(() => { - expect(trigger.focus).toHaveBeenCalled() - done() - }, 20) - } + const hideListener = () => { + setTimeout(() => { + expect(spy).toHaveBeenCalled() + resolve() + }, 20) + } - modalEl.addEventListener('hidden.bs.modal', () => { - hideListener() + modalEl.addEventListener('hidden.bs.modal', () => { + hideListener() + }) + + trigger.click() }) + }) - trigger.click() + it('should open modal, having special characters in its id', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') + + const modalEl = fixtureEl.querySelector('.modal') + const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') + + modalEl.addEventListener('shown.bs.modal', () => { + resolve() + }) + + trigger.click() + }) }) - it('should not prevent default when a click occurred on data-bs-dismiss="modal" where tagName is DIFFERENT than or ', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should not prevent default when a click occurred on data-bs-dismiss="modal" where tagName is DIFFERENT than or ', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const modalEl = fixtureEl.querySelector('.modal') - const btnClose = fixtureEl.querySelector('button[data-bs-dismiss="modal"]') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const btnClose = fixtureEl.querySelector('button[data-bs-dismiss="modal"]') + const modal = new Modal(modalEl) - spyOn(Event.prototype, 'preventDefault').and.callThrough() + const spy = spyOn(Event.prototype, 'preventDefault').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - btnClose.click() - }) + modalEl.addEventListener('shown.bs.modal', () => { + btnClose.click() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(Event.prototype.preventDefault).not.toHaveBeenCalled() - done() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(spy).not.toHaveBeenCalled() + resolve() + }) - modal.show() + modal.show() + }) }) - it('should prevent default when a click occurred on data-bs-dismiss="modal" where tagName is or ', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should prevent default when a click occurred on data-bs-dismiss="modal" where tagName is or ', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const modalEl = fixtureEl.querySelector('.modal') - const btnClose = fixtureEl.querySelector('a[data-bs-dismiss="modal"]') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const btnClose = fixtureEl.querySelector('a[data-bs-dismiss="modal"]') + const modal = new Modal(modalEl) - spyOn(Event.prototype, 'preventDefault').and.callThrough() + const spy = spyOn(Event.prototype, 'preventDefault').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - btnClose.click() - }) + modalEl.addEventListener('shown.bs.modal', () => { + btnClose.click() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(Event.prototype.preventDefault).toHaveBeenCalled() - done() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(spy).toHaveBeenCalled() + resolve() + }) - modal.show() + modal.show() + }) }) + it('should not focus the trigger if the modal is not visible', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') - it('should not focus the trigger if the modal is not visible', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') + const modalEl = fixtureEl.querySelector('.modal') + const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') - const modalEl = fixtureEl.querySelector('.modal') - const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') + const spy = spyOn(trigger, 'focus') - spyOn(trigger, 'focus') + modalEl.addEventListener('shown.bs.modal', () => { + const modal = Modal.getInstance(modalEl) - modalEl.addEventListener('shown.bs.modal', () => { - const modal = Modal.getInstance(modalEl) + modal.hide() + }) - modal.hide() - }) + const hideListener = () => { + setTimeout(() => { + expect(spy).not.toHaveBeenCalled() + resolve() + }, 20) + } - const hideListener = () => { - setTimeout(() => { - expect(trigger.focus).not.toHaveBeenCalled() - done() - }, 20) - } + modalEl.addEventListener('hidden.bs.modal', () => { + hideListener() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - hideListener() + trigger.click() }) - - trigger.click() }) + it('should not focus the trigger if the modal is not shown', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') - it('should not focus the trigger if the modal is not shown', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') + const modalEl = fixtureEl.querySelector('.modal') + const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') - const modalEl = fixtureEl.querySelector('.modal') - const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') + const spy = spyOn(trigger, 'focus') - spyOn(trigger, 'focus') + const showListener = () => { + setTimeout(() => { + expect(spy).not.toHaveBeenCalled() + resolve() + }, 10) + } - const showListener = () => { - setTimeout(() => { - expect(trigger.focus).not.toHaveBeenCalled() - done() - }, 10) - } + modalEl.addEventListener('show.bs.modal', event => { + event.preventDefault() + showListener() + }) - modalEl.addEventListener('show.bs.modal', e => { - e.preventDefault() - showListener() + trigger.click() }) - - trigger.click() }) - it('should call hide first, if another modal is open', done => { - fixtureEl.innerHTML = [ - '', - '', - '' - ].join('') - - const trigger2 = fixtureEl.querySelector('button') - const modalEl1 = document.querySelector('#modal1') - const modalEl2 = document.querySelector('#modal2') - const modal1 = new Modal(modalEl1) - - modalEl1.addEventListener('shown.bs.modal', () => { - trigger2.click() - }) - modalEl1.addEventListener('hidden.bs.modal', () => { - expect(Modal.getInstance(modalEl2)).not.toBeNull() - expect(modalEl2.classList.contains('show')).toBeTrue() - done() + it('should call hide first, if another modal is open', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '', + '' + ].join('') + + const trigger2 = fixtureEl.querySelector('button') + const modalEl1 = document.querySelector('#modal1') + const modalEl2 = document.querySelector('#modal2') + const modal1 = new Modal(modalEl1) + + modalEl1.addEventListener('shown.bs.modal', () => { + trigger2.click() + }) + modalEl1.addEventListener('hidden.bs.modal', () => { + expect(Modal.getInstance(modalEl2)).not.toBeNull() + expect(modalEl2).toHaveClass('show') + resolve() + }) + modal1.show() }) - modal1.show() }) }) - describe('jQueryInterface', () => { it('should create a modal', () => { fixtureEl.innerHTML = '' @@ -1026,12 +1185,12 @@ describe('Modal', () => { jQueryMock.elements = [div] jQueryMock.fn.modal.call(jQueryMock, { keyboard: false }) - spyOn(Modal.prototype, 'constructor') - expect(Modal.prototype.constructor).not.toHaveBeenCalledWith(div, { keyboard: false }) + const spy = spyOn(Modal.prototype, 'constructor') + expect(spy).not.toHaveBeenCalledWith(div, { keyboard: false }) const modal = Modal.getInstance(div) expect(modal).not.toBeNull() - expect(modal._config.keyboard).toBe(false) + expect(modal._config.keyboard).toBeFalse() }) it('should not re create a modal', () => { @@ -1071,11 +1230,11 @@ describe('Modal', () => { jQueryMock.fn.modal = Modal.jQueryInterface jQueryMock.elements = [div] - spyOn(modal, 'show') + const spy = spyOn(modal, 'show') jQueryMock.fn.modal.call(jQueryMock, 'show') - expect(modal.show).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) it('should not call show method', () => { @@ -1086,11 +1245,11 @@ describe('Modal', () => { jQueryMock.fn.modal = Modal.jQueryInterface jQueryMock.elements = [div] - spyOn(Modal.prototype, 'show') + const spy = spyOn(Modal.prototype, 'show') jQueryMock.fn.modal.call(jQueryMock) - expect(Modal.prototype.show).not.toHaveBeenCalled() + expect(spy).not.toHaveBeenCalled() }) }) @@ -1131,7 +1290,7 @@ describe('Modal', () => { const div = fixtureEl.querySelector('div') - expect(Modal.getInstance(div)).toEqual(null) + expect(Modal.getInstance(div)).toBeNull() expect(Modal.getOrCreateInstance(div)).toBeInstanceOf(Modal) }) @@ -1140,13 +1299,13 @@ describe('Modal', () => { const div = fixtureEl.querySelector('div') - expect(Modal.getInstance(div)).toEqual(null) + expect(Modal.getInstance(div)).toBeNull() const modal = Modal.getOrCreateInstance(div, { backdrop: true }) expect(modal).toBeInstanceOf(Modal) - expect(modal._config.backdrop).toEqual(true) + expect(modal._config.backdrop).toBeTrue() }) it('should return the instance when exists without given configuration', () => { @@ -1164,7 +1323,7 @@ describe('Modal', () => { expect(modal).toBeInstanceOf(Modal) expect(modal2).toEqual(modal) - expect(modal2._config.backdrop).toEqual(true) + expect(modal2._config.backdrop).toBeTrue() }) }) }) diff --git a/js/tests/unit/offcanvas.spec.js b/js/tests/unit/offcanvas.spec.js index ecbb710a5942..3b6c98c1004c 100644 --- a/js/tests/unit/offcanvas.spec.js +++ b/js/tests/unit/offcanvas.spec.js @@ -1,10 +1,10 @@ -import Offcanvas from '../../src/offcanvas' -import EventHandler from '../../src/dom/event-handler' - -/** Test helpers */ -import { clearBodyAndDocument, clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture' -import { isVisible } from '../../src/util' -import ScrollBarHelper from '../../src/util/scrollbar' +import EventHandler from '../../src/dom/event-handler.js' +import Offcanvas from '../../src/offcanvas.js' +import { isVisible } from '../../src/util/index.js' +import ScrollBarHelper from '../../src/util/scrollbar.js' +import { + clearBodyAndDocument, clearFixture, createEvent, getFixture, jQueryMock +} from '../helpers/fixture.js' describe('Offcanvas', () => { let fixtureEl @@ -53,12 +53,12 @@ describe('Offcanvas', () => { const closeEl = fixtureEl.querySelector('a') const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas, 'hide') + const spy = spyOn(offCanvas, 'hide') closeEl.click() - expect(offCanvas._config.keyboard).toBe(true) - expect(offCanvas.hide).toHaveBeenCalled() + expect(offCanvas._config.keyboard).toBeTrue() + expect(spy).toHaveBeenCalled() }) it('should hide if esc is pressed', () => { @@ -69,11 +69,26 @@ describe('Offcanvas', () => { const keyDownEsc = createEvent('keydown') keyDownEsc.key = 'Escape' - spyOn(offCanvas, 'hide') + const spy = spyOn(offCanvas, 'hide') + + offCanvasEl.dispatchEvent(keyDownEsc) + + expect(spy).toHaveBeenCalled() + }) + + it('should hide if esc is pressed and backdrop is static', () => { + fixtureEl.innerHTML = '
' + + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl, { backdrop: 'static' }) + const keyDownEsc = createEvent('keydown') + keyDownEsc.key = 'Escape' + + const spy = spyOn(offCanvas, 'hide') offCanvasEl.dispatchEvent(keyDownEsc) - expect(offCanvas.hide).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) it('should not hide if esc is not pressed', () => { @@ -84,66 +99,115 @@ describe('Offcanvas', () => { const keydownTab = createEvent('keydown') keydownTab.key = 'Tab' - spyOn(offCanvas, 'hide') + const spy = spyOn(offCanvas, 'hide') - document.dispatchEvent(keydownTab) + offCanvasEl.dispatchEvent(keydownTab) - expect(offCanvas.hide).not.toHaveBeenCalled() + expect(spy).not.toHaveBeenCalled() }) it('should not hide if esc is pressed but with keyboard = false', () => { - fixtureEl.innerHTML = '
' + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl, { keyboard: false }) - const keyDownEsc = createEvent('keydown') - keyDownEsc.key = 'Escape' + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl, { keyboard: false }) + const keyDownEsc = createEvent('keydown') + keyDownEsc.key = 'Escape' + + const spy = spyOn(offCanvas, 'hide') + const hidePreventedSpy = jasmine.createSpy('hidePrevented') + offCanvasEl.addEventListener('hidePrevented.bs.offcanvas', hidePreventedSpy) - spyOn(offCanvas, 'hide') + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvas._config.keyboard).toBeFalse() + offCanvasEl.dispatchEvent(keyDownEsc) - document.dispatchEvent(keyDownEsc) + expect(hidePreventedSpy).toHaveBeenCalled() + expect(spy).not.toHaveBeenCalled() + resolve() + }) - expect(offCanvas._config.keyboard).toBe(false) - expect(offCanvas.hide).not.toHaveBeenCalled() + offCanvas.show() + }) + }) + + it('should not hide if user clicks on static backdrop', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' + + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl, { backdrop: 'static' }) + + const clickEvent = new Event('mousedown', { bubbles: true, cancelable: true }) + const spyClick = spyOn(offCanvas._backdrop._config, 'clickCallback').and.callThrough() + const spyHide = spyOn(offCanvas._backdrop, 'hide').and.callThrough() + const hidePreventedSpy = jasmine.createSpy('hidePrevented') + offCanvasEl.addEventListener('hidePrevented.bs.offcanvas', hidePreventedSpy) + + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(spyClick).toEqual(jasmine.any(Function)) + + offCanvas._backdrop._getElement().dispatchEvent(clickEvent) + expect(hidePreventedSpy).toHaveBeenCalled() + expect(spyHide).not.toHaveBeenCalled() + resolve() + }) + + offCanvas.show() + }) + }) + + it('should call `hide` on resize, if element\'s position is not fixed any more', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' + + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl) + + const spy = spyOn(offCanvas, 'hide').and.callThrough() + + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + const resizeEvent = createEvent('resize') + offCanvasEl.style.removeProperty('position') + + window.dispatchEvent(resizeEvent) + expect(spy).toHaveBeenCalled() + resolve() + }) + + offCanvas.show() + }) }) }) describe('config', () => { it('should have default values', () => { - fixtureEl.innerHTML = [ - '
', - '
' - ].join('') + fixtureEl.innerHTML = '
' const offCanvasEl = fixtureEl.querySelector('.offcanvas') const offCanvas = new Offcanvas(offCanvasEl) - expect(offCanvas._config.backdrop).toEqual(true) - expect(offCanvas._backdrop._config.isVisible).toEqual(true) - expect(offCanvas._config.keyboard).toEqual(true) - expect(offCanvas._config.scroll).toEqual(false) + expect(offCanvas._config.backdrop).toBeTrue() + expect(offCanvas._backdrop._config.isVisible).toBeTrue() + expect(offCanvas._config.keyboard).toBeTrue() + expect(offCanvas._config.scroll).toBeFalse() }) it('should read data attributes and override default config', () => { - fixtureEl.innerHTML = [ - '
', - '
' - ].join('') + fixtureEl.innerHTML = '
' const offCanvasEl = fixtureEl.querySelector('.offcanvas') const offCanvas = new Offcanvas(offCanvasEl) - expect(offCanvas._config.backdrop).toEqual(false) - expect(offCanvas._backdrop._config.isVisible).toEqual(false) - expect(offCanvas._config.keyboard).toEqual(false) - expect(offCanvas._config.scroll).toEqual(true) + expect(offCanvas._config.backdrop).toBeFalse() + expect(offCanvas._backdrop._config.isVisible).toBeFalse() + expect(offCanvas._config.keyboard).toBeFalse() + expect(offCanvas._config.scroll).toBeTrue() }) it('given a config object must override data attributes', () => { - fixtureEl.innerHTML = [ - '
', - '
' - ].join('') + fixtureEl.innerHTML = '
' const offCanvasEl = fixtureEl.querySelector('.offcanvas') const offCanvas = new Offcanvas(offCanvasEl, { @@ -151,90 +215,120 @@ describe('Offcanvas', () => { keyboard: true, scroll: false }) - expect(offCanvas._config.backdrop).toEqual(true) - expect(offCanvas._config.keyboard).toEqual(true) - expect(offCanvas._config.scroll).toEqual(false) + expect(offCanvas._config.backdrop).toBeTrue() + expect(offCanvas._config.keyboard).toBeTrue() + expect(offCanvas._config.scroll).toBeFalse() }) }) - describe('options', () => { - it('if scroll is enabled, should allow body to scroll while offcanvas is open', done => { - fixtureEl.innerHTML = '
' - - spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() - spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl, { scroll: true }) - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(ScrollBarHelper.prototype.hide).not.toHaveBeenCalled() - offCanvas.hide() + describe('options', () => { + it('if scroll is enabled, should allow body to scroll while offcanvas is open', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' + + const spyHide = spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() + const spyReset = spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl, { scroll: true }) + + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(spyHide).not.toHaveBeenCalled() + offCanvas.hide() + }) + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(spyReset).not.toHaveBeenCalled() + resolve() + }) + offCanvas.show() }) - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(ScrollBarHelper.prototype.reset).not.toHaveBeenCalled() - done() + }) + + it('if scroll is disabled, should call ScrollBarHelper to handle scrollBar on body', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' + + const spyHide = spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() + const spyReset = spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl, { scroll: false }) + + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(spyHide).toHaveBeenCalled() + offCanvas.hide() + }) + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(spyReset).toHaveBeenCalled() + resolve() + }) + offCanvas.show() }) - offCanvas.show() }) - it('if scroll is disabled, should call ScrollBarHelper to handle scrollBar on body', done => { - fixtureEl.innerHTML = '
' + it('should hide a shown element if user click on backdrop', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() - spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl, { scroll: false }) + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl, { backdrop: true }) - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(ScrollBarHelper.prototype.hide).toHaveBeenCalled() - offCanvas.hide() - }) - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(ScrollBarHelper.prototype.reset).toHaveBeenCalled() - done() + const clickEvent = new Event('mousedown', { bubbles: true, cancelable: true }) + const spy = spyOn(offCanvas._backdrop._config, 'clickCallback').and.callThrough() + + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvas._backdrop._config.clickCallback).toEqual(jasmine.any(Function)) + + offCanvas._backdrop._getElement().dispatchEvent(clickEvent) + }) + + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(spy).toHaveBeenCalled() + resolve() + }) + + offCanvas.show() }) - offCanvas.show() }) - it('should hide a shown element if user click on backdrop', done => { - fixtureEl.innerHTML = '
' - - const offCanvasEl = fixtureEl.querySelector('div') - const offCanvas = new Offcanvas(offCanvasEl, { backdrop: true }) + it('should not trap focus if scroll is allowed', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const clickEvent = document.createEvent('MouseEvents') - clickEvent.initEvent('mousedown', true, true) - spyOn(offCanvas._backdrop._config, 'clickCallback').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl, { + scroll: true, + backdrop: false + }) - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(typeof offCanvas._backdrop._config.clickCallback).toBe('function') + const spy = spyOn(offCanvas._focustrap, 'activate').and.callThrough() - offCanvas._backdrop._getElement().dispatchEvent(clickEvent) - }) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(spy).not.toHaveBeenCalled() + resolve() + }) - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(offCanvas._backdrop._config.clickCallback).toHaveBeenCalled() - done() + offCanvas.show() }) - - offCanvas.show() }) - it('should not trap focus if scroll is allowed', done => { - fixtureEl.innerHTML = '
' + it('should trap focus if scroll is allowed OR backdrop is enabled', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl, { - scroll: true - }) + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl, { + scroll: true, + backdrop: true + }) - spyOn(offCanvas._focustrap, 'activate').and.callThrough() + const spy = spyOn(offCanvas._focustrap, 'activate').and.callThrough() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvas._focustrap.activate).not.toHaveBeenCalled() - done() - }) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(spy).toHaveBeenCalled() + resolve() + }) - offCanvas.show() + offCanvas.show() + }) }) }) @@ -245,30 +339,57 @@ describe('Offcanvas', () => { const offCanvasEl = fixtureEl.querySelector('.offcanvas') const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas, 'show') + const spy = spyOn(offCanvas, 'show') offCanvas.toggle() - expect(offCanvas.show).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) it('should call hide method if show class is present', () => { - fixtureEl.innerHTML = '
' + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl) - offCanvas.show() - expect(offCanvasEl.classList.contains('show')).toBe(true) + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas, 'hide') + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvasEl).toHaveClass('show') + const spy = spyOn(offCanvas, 'hide') - offCanvas.toggle() + offCanvas.toggle() + + expect(spy).toHaveBeenCalled() + resolve() + }) - expect(offCanvas.hide).toHaveBeenCalled() + offCanvas.show() + }) }) }) describe('show', () => { + it('should add `showing` class during opening and `show` class on end', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl) + + offCanvasEl.addEventListener('show.bs.offcanvas', () => { + expect(offCanvasEl).not.toHaveClass('show') + }) + + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvasEl).not.toHaveClass('showing') + expect(offCanvasEl).toHaveClass('show') + resolve() + }) + + offCanvas.show() + expect(offCanvasEl).toHaveClass('showing') + }) + }) + it('should do nothing if already shown', () => { fixtureEl.innerHTML = '
' @@ -276,166 +397,206 @@ describe('Offcanvas', () => { const offCanvas = new Offcanvas(offCanvasEl) offCanvas.show() - expect(offCanvasEl.classList.contains('show')).toBe(true) + expect(offCanvasEl).toHaveClass('show') - spyOn(offCanvas._backdrop, 'show').and.callThrough() - spyOn(EventHandler, 'trigger').and.callThrough() + const spyShow = spyOn(offCanvas._backdrop, 'show').and.callThrough() + const spyTrigger = spyOn(EventHandler, 'trigger').and.callThrough() offCanvas.show() - expect(EventHandler.trigger).not.toHaveBeenCalled() - expect(offCanvas._backdrop.show).not.toHaveBeenCalled() + expect(spyTrigger).not.toHaveBeenCalled() + expect(spyShow).not.toHaveBeenCalled() }) - it('should show a hidden element', done => { - fixtureEl.innerHTML = '
' + it('should show a hidden element', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('div') - const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._backdrop, 'show').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl) + const spy = spyOn(offCanvas._backdrop, 'show').and.callThrough() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvasEl.classList.contains('show')).toEqual(true) - expect(offCanvas._backdrop.show).toHaveBeenCalled() - done() - }) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvasEl).toHaveClass('show') + expect(spy).toHaveBeenCalled() + resolve() + }) - offCanvas.show() + offCanvas.show() + }) }) - it('should not fire shown when show is prevented', done => { - fixtureEl.innerHTML = '
' + it('should not fire shown when show is prevented', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('div') - const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._backdrop, 'show').and.callThrough() - - const expectEnd = () => { - setTimeout(() => { - expect(offCanvas._backdrop.show).not.toHaveBeenCalled() - done() - }, 10) - } - - offCanvasEl.addEventListener('show.bs.offcanvas', e => { - e.preventDefault() - expectEnd() - }) + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl) + const spy = spyOn(offCanvas._backdrop, 'show').and.callThrough() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - throw new Error('should not fire shown event') - }) + const expectEnd = () => { + setTimeout(() => { + expect(spy).not.toHaveBeenCalled() + resolve() + }, 10) + } - offCanvas.show() + offCanvasEl.addEventListener('show.bs.offcanvas', event => { + event.preventDefault() + expectEnd() + }) + + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + reject(new Error('should not fire shown event')) + }) + + offCanvas.show() + }) }) - it('on window load, should make visible an offcanvas element, if its markup contains class "show"', done => { - fixtureEl.innerHTML = '
' + it('on window load, should make visible an offcanvas element, if its markup contains class "show"', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('div') - spyOn(Offcanvas.prototype, 'show').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('div') + const spy = spyOn(Offcanvas.prototype, 'show').and.callThrough() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - done() - }) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + resolve() + }) - window.dispatchEvent(createEvent('load')) + window.dispatchEvent(createEvent('load')) - const instance = Offcanvas.getInstance(offCanvasEl) - expect(instance).not.toBeNull() - expect(Offcanvas.prototype.show).toHaveBeenCalled() + const instance = Offcanvas.getInstance(offCanvasEl) + expect(instance).not.toBeNull() + expect(spy).toHaveBeenCalled() + }) }) - it('should trap focus', done => { - fixtureEl.innerHTML = '
' + it('should trap focus', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl) + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._focustrap, 'activate').and.callThrough() + const spy = spyOn(offCanvas._focustrap, 'activate').and.callThrough() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvas._focustrap.activate).toHaveBeenCalled() - done() - }) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(spy).toHaveBeenCalled() + resolve() + }) - offCanvas.show() + offCanvas.show() + }) }) }) describe('hide', () => { + it('should add `hiding` class during closing and remover `show` & `hiding` classes on end', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl) + + offCanvasEl.addEventListener('hide.bs.offcanvas', () => { + expect(offCanvasEl).not.toHaveClass('showing') + expect(offCanvasEl).toHaveClass('show') + }) + + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(offCanvasEl).not.toHaveClass('hiding') + expect(offCanvasEl).not.toHaveClass('show') + resolve() + }) + + offCanvas.show() + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + offCanvas.hide() + expect(offCanvasEl).not.toHaveClass('showing') + expect(offCanvasEl).toHaveClass('hiding') + }) + }) + }) + it('should do nothing if already shown', () => { fixtureEl.innerHTML = '
' - spyOn(EventHandler, 'trigger').and.callThrough() + const spyTrigger = spyOn(EventHandler, 'trigger').and.callThrough() const offCanvasEl = fixtureEl.querySelector('div') const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._backdrop, 'hide').and.callThrough() + const spyHide = spyOn(offCanvas._backdrop, 'hide').and.callThrough() offCanvas.hide() - expect(offCanvas._backdrop.hide).not.toHaveBeenCalled() - expect(EventHandler.trigger).not.toHaveBeenCalled() + expect(spyHide).not.toHaveBeenCalled() + expect(spyTrigger).not.toHaveBeenCalled() }) - it('should hide a shown element', done => { - fixtureEl.innerHTML = '
' + it('should hide a shown element', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('div') - const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._backdrop, 'hide').and.callThrough() - offCanvas.show() + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl) + const spy = spyOn(offCanvas._backdrop, 'hide').and.callThrough() + offCanvas.show() - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(offCanvasEl.classList.contains('show')).toEqual(false) - expect(offCanvas._backdrop.hide).toHaveBeenCalled() - done() - }) + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(offCanvasEl).not.toHaveClass('show') + expect(spy).toHaveBeenCalled() + resolve() + }) - offCanvas.hide() + offCanvas.hide() + }) }) - it('should not fire hidden when hide is prevented', done => { - fixtureEl.innerHTML = '
' + it('should not fire hidden when hide is prevented', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('div') - const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._backdrop, 'hide').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl) + const spy = spyOn(offCanvas._backdrop, 'hide').and.callThrough() - offCanvas.show() + offCanvas.show() - const expectEnd = () => { - setTimeout(() => { - expect(offCanvas._backdrop.hide).not.toHaveBeenCalled() - done() - }, 10) - } + const expectEnd = () => { + setTimeout(() => { + expect(spy).not.toHaveBeenCalled() + resolve() + }, 10) + } - offCanvasEl.addEventListener('hide.bs.offcanvas', e => { - e.preventDefault() - expectEnd() - }) + offCanvasEl.addEventListener('hide.bs.offcanvas', event => { + event.preventDefault() + expectEnd() + }) - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - throw new Error('should not fire hidden event') - }) + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + reject(new Error('should not fire hidden event')) + }) - offCanvas.hide() + offCanvas.hide() + }) }) - it('should release focus trap', done => { - fixtureEl.innerHTML = '
' + it('should release focus trap', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('div') - const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._focustrap, 'deactivate').and.callThrough() - offCanvas.show() + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl) + const spy = spyOn(offCanvas._focustrap, 'deactivate').and.callThrough() + offCanvas.show() - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(offCanvas._focustrap.deactivate).toHaveBeenCalled() - done() - }) + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(spy).toHaveBeenCalled() + resolve() + }) - offCanvas.hide() + offCanvas.hide() + }) }) }) @@ -446,41 +607,41 @@ describe('Offcanvas', () => { const offCanvasEl = fixtureEl.querySelector('div') const offCanvas = new Offcanvas(offCanvasEl) const backdrop = offCanvas._backdrop - spyOn(backdrop, 'dispose').and.callThrough() + const spyDispose = spyOn(backdrop, 'dispose').and.callThrough() const focustrap = offCanvas._focustrap - spyOn(focustrap, 'deactivate').and.callThrough() + const spyDeactivate = spyOn(focustrap, 'deactivate').and.callThrough() expect(Offcanvas.getInstance(offCanvasEl)).toEqual(offCanvas) - spyOn(EventHandler, 'off') - offCanvas.dispose() - expect(backdrop.dispose).toHaveBeenCalled() + expect(spyDispose).toHaveBeenCalled() expect(offCanvas._backdrop).toBeNull() - expect(focustrap.deactivate).toHaveBeenCalled() + expect(spyDeactivate).toHaveBeenCalled() expect(offCanvas._focustrap).toBeNull() - expect(Offcanvas.getInstance(offCanvasEl)).toEqual(null) + expect(Offcanvas.getInstance(offCanvasEl)).toBeNull() }) }) describe('data-api', () => { - it('should not prevent event for input', done => { - fixtureEl.innerHTML = [ - '', - '
' - ].join('') - - const target = fixtureEl.querySelector('input') - const offCanvasEl = fixtureEl.querySelector('#offcanvasdiv1') - - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvasEl.classList.contains('show')).toEqual(true) - expect(target.checked).toEqual(true) - done() + it('should not prevent event for input', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
' + ].join('') + + const target = fixtureEl.querySelector('input') + const offCanvasEl = fixtureEl.querySelector('#offcanvasdiv1') + + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvasEl).toHaveClass('show') + expect(target.checked).toBeTrue() + resolve() + }) + + target.click() }) - - target.click() }) it('should not call toggle on disabled elements', () => { @@ -491,83 +652,89 @@ describe('Offcanvas', () => { const target = fixtureEl.querySelector('a') - spyOn(Offcanvas.prototype, 'toggle') + const spy = spyOn(Offcanvas.prototype, 'toggle') target.click() - expect(Offcanvas.prototype.toggle).not.toHaveBeenCalled() - }) - - it('should call hide first, if another offcanvas is open', done => { - fixtureEl.innerHTML = [ - '', - '
', - '
' - ].join('') - - const trigger2 = fixtureEl.querySelector('#btn2') - const offcanvasEl1 = document.querySelector('#offcanvas1') - const offcanvasEl2 = document.querySelector('#offcanvas2') - const offcanvas1 = new Offcanvas(offcanvasEl1) - - offcanvasEl1.addEventListener('shown.bs.offcanvas', () => { - trigger2.click() + expect(spy).not.toHaveBeenCalled() + }) + + it('should call hide first, if another offcanvas is open', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
', + '
' + ].join('') + + const trigger2 = fixtureEl.querySelector('#btn2') + const offcanvasEl1 = document.querySelector('#offcanvas1') + const offcanvasEl2 = document.querySelector('#offcanvas2') + const offcanvas1 = new Offcanvas(offcanvasEl1) + + offcanvasEl1.addEventListener('shown.bs.offcanvas', () => { + trigger2.click() + }) + offcanvasEl1.addEventListener('hidden.bs.offcanvas', () => { + expect(Offcanvas.getInstance(offcanvasEl2)).not.toBeNull() + resolve() + }) + offcanvas1.show() }) - offcanvasEl1.addEventListener('hidden.bs.offcanvas', () => { - expect(Offcanvas.getInstance(offcanvasEl2)).not.toBeNull() - done() - }) - offcanvas1.show() }) - it('should focus on trigger element after closing offcanvas', done => { - fixtureEl.innerHTML = [ - '', - '
' - ].join('') - - const trigger = fixtureEl.querySelector('#btn') - const offcanvasEl = fixtureEl.querySelector('#offcanvas') - const offcanvas = new Offcanvas(offcanvasEl) - spyOn(trigger, 'focus') - - offcanvasEl.addEventListener('shown.bs.offcanvas', () => { - offcanvas.hide() + it('should focus on trigger element after closing offcanvas', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
' + ].join('') + + const trigger = fixtureEl.querySelector('#btn') + const offcanvasEl = fixtureEl.querySelector('#offcanvas') + const offcanvas = new Offcanvas(offcanvasEl) + const spy = spyOn(trigger, 'focus') + + offcanvasEl.addEventListener('shown.bs.offcanvas', () => { + offcanvas.hide() + }) + offcanvasEl.addEventListener('hidden.bs.offcanvas', () => { + setTimeout(() => { + expect(spy).toHaveBeenCalled() + resolve() + }, 5) + }) + + trigger.click() }) - offcanvasEl.addEventListener('hidden.bs.offcanvas', () => { - setTimeout(() => { - expect(trigger.focus).toHaveBeenCalled() - done() - }, 5) - }) - - trigger.click() }) - it('should not focus on trigger element after closing offcanvas, if it is not visible', done => { - fixtureEl.innerHTML = [ - '', - '
' - ].join('') - - const trigger = fixtureEl.querySelector('#btn') - const offcanvasEl = fixtureEl.querySelector('#offcanvas') - const offcanvas = new Offcanvas(offcanvasEl) - spyOn(trigger, 'focus') - - offcanvasEl.addEventListener('shown.bs.offcanvas', () => { - trigger.style.display = 'none' - offcanvas.hide() - }) - offcanvasEl.addEventListener('hidden.bs.offcanvas', () => { - setTimeout(() => { - expect(isVisible(trigger)).toBe(false) - expect(trigger.focus).not.toHaveBeenCalled() - done() - }, 5) + it('should not focus on trigger element after closing offcanvas, if it is not visible', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
' + ].join('') + + const trigger = fixtureEl.querySelector('#btn') + const offcanvasEl = fixtureEl.querySelector('#offcanvas') + const offcanvas = new Offcanvas(offcanvasEl) + const spy = spyOn(trigger, 'focus') + + offcanvasEl.addEventListener('shown.bs.offcanvas', () => { + trigger.style.display = 'none' + offcanvas.hide() + }) + offcanvasEl.addEventListener('hidden.bs.offcanvas', () => { + setTimeout(() => { + expect(isVisible(trigger)).toBeFalse() + expect(spy).not.toHaveBeenCalled() + resolve() + }, 5) + }) + + trigger.click() }) - - trigger.click() }) }) @@ -646,13 +813,13 @@ describe('Offcanvas', () => { const div = fixtureEl.querySelector('div') - spyOn(Offcanvas.prototype, 'show') + const spy = spyOn(Offcanvas.prototype, 'show') jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.offcanvas.call(jQueryMock, 'show') - expect(Offcanvas.prototype.show).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) it('should create a offcanvas with given config', () => { @@ -667,7 +834,7 @@ describe('Offcanvas', () => { const offcanvas = Offcanvas.getInstance(div) expect(offcanvas).not.toBeNull() - expect(offcanvas._config.scroll).toBe(true) + expect(offcanvas._config.scroll).toBeTrue() }) }) @@ -708,7 +875,7 @@ describe('Offcanvas', () => { const div = fixtureEl.querySelector('div') - expect(Offcanvas.getInstance(div)).toEqual(null) + expect(Offcanvas.getInstance(div)).toBeNull() expect(Offcanvas.getOrCreateInstance(div)).toBeInstanceOf(Offcanvas) }) @@ -717,13 +884,13 @@ describe('Offcanvas', () => { const div = fixtureEl.querySelector('div') - expect(Offcanvas.getInstance(div)).toEqual(null) + expect(Offcanvas.getInstance(div)).toBeNull() const offcanvas = Offcanvas.getOrCreateInstance(div, { scroll: true }) expect(offcanvas).toBeInstanceOf(Offcanvas) - expect(offcanvas._config.scroll).toEqual(true) + expect(offcanvas._config.scroll).toBeTrue() }) it('should return the instance when exists without given configuration', () => { @@ -741,7 +908,7 @@ describe('Offcanvas', () => { expect(offcanvas).toBeInstanceOf(Offcanvas) expect(offcanvas2).toEqual(offcanvas) - expect(offcanvas2._config.scroll).toEqual(true) + expect(offcanvas2._config.scroll).toBeTrue() }) }) }) diff --git a/js/tests/unit/popover.spec.js b/js/tests/unit/popover.spec.js index c54fc49eee16..1338821bc86d 100644 --- a/js/tests/unit/popover.spec.js +++ b/js/tests/unit/popover.spec.js @@ -1,7 +1,8 @@ -import Popover from '../../src/popover' - -/** Test helpers */ -import { clearFixture, getFixture, jQueryMock } from '../helpers/fixture' +import EventHandler from '../../src/dom/event-handler.js' +import Popover from '../../src/popover.js' +import { + clearFixture, getFixture, jQueryMock, createEvent +} from '../helpers/fixture.js' describe('Popover', () => { let fixtureEl @@ -15,9 +16,9 @@ describe('Popover', () => { const popoverList = document.querySelectorAll('.popover') - popoverList.forEach(popoverEl => { + for (const popoverEl of popoverList) { popoverEl.remove() - }) + } }) describe('VERSION', () => { @@ -44,12 +45,6 @@ describe('Popover', () => { }) }) - describe('Event', () => { - it('should return plugin events', () => { - expect(Popover.Event).toEqual(jasmine.any(Object)) - }) - }) - describe('EVENT_KEY', () => { it('should return plugin event key', () => { expect(Popover.EVENT_KEY).toEqual('.bs.popover') @@ -63,171 +58,312 @@ describe('Popover', () => { }) describe('show', () => { - it('should show a popover', done => { - fixtureEl.innerHTML = 'BS twitter' + it('should toggle a popover after show', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = 'BS X' + + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl) + + popoverEl.addEventListener('shown.bs.popover', () => { + expect(document.querySelector('.popover')).not.toBeNull() + popover.toggle() + }) + popoverEl.addEventListener('hidden.bs.popover', () => { + expect(document.querySelector('.popover')).toBeNull() + resolve() + }) - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl) - - popoverEl.addEventListener('shown.bs.popover', () => { - expect(document.querySelector('.popover')).not.toBeNull() - done() + popover.show() }) - - popover.show() }) - it('should set title and content from functions', done => { - fixtureEl.innerHTML = 'BS twitter' + it('should show a popover', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = 'BS X' - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl, { - title: () => 'Bootstrap', - content: () => 'loves writing tests (╯°□°)╯︵ ┻━┻' - }) + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl) - popoverEl.addEventListener('shown.bs.popover', () => { - const popoverDisplayed = document.querySelector('.popover') + popoverEl.addEventListener('shown.bs.popover', () => { + expect(document.querySelector('.popover')).not.toBeNull() + resolve() + }) - expect(popoverDisplayed).not.toBeNull() - expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('Bootstrap') - expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('loves writing tests (╯°□°)╯︵ ┻━┻') - done() + popover.show() }) - - popover.show() }) - it('should show a popover with just content', done => { - fixtureEl.innerHTML = 'BS twitter' + it('should set title and content from functions', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = 'BS X' - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl, { - content: 'Popover content' - }) + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl, { + title: () => 'Bootstrap', + content: () => 'loves writing tests (╯°□°)╯︵ ┻━┻' + }) - popoverEl.addEventListener('shown.bs.popover', () => { - const popoverDisplayed = document.querySelector('.popover') + popoverEl.addEventListener('shown.bs.popover', () => { + const popoverDisplayed = document.querySelector('.popover') - expect(popoverDisplayed).not.toBeNull() - expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('Popover content') - done() - }) + expect(popoverDisplayed).not.toBeNull() + expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('Bootstrap') + expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('loves writing tests (╯°□°)╯︵ ┻━┻') + resolve() + }) - popover.show() + popover.show() + }) }) - it('should show a popover with just content without having header', done => { - fixtureEl.innerHTML = 'Nice link' + it('should call content and title functions with trigger element', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = 'BS X' + + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl, { + title(el) { + return el.dataset.foo + }, + content(el) { + return el.dataset.foo + } + }) + + popoverEl.addEventListener('shown.bs.popover', () => { + const popoverDisplayed = document.querySelector('.popover') + + expect(popoverDisplayed).not.toBeNull() + expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('bar') + expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('bar') + resolve() + }) - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl, { - content: 'Some beautiful content :)' + popover.show() }) + }) - popoverEl.addEventListener('shown.bs.popover', () => { - const popoverDisplayed = document.querySelector('.popover') + it('should call content and title functions with correct this value', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = 'BS X' + + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl, { + title() { + return this.dataset.foo + }, + content() { + return this.dataset.foo + } + }) + + popoverEl.addEventListener('shown.bs.popover', () => { + const popoverDisplayed = document.querySelector('.popover') + + expect(popoverDisplayed).not.toBeNull() + expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('bar') + expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('bar') + resolve() + }) - expect(popoverDisplayed).not.toBeNull() - expect(popoverDisplayed.querySelector('.popover-header')).toBeNull() - expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('Some beautiful content :)') - done() + popover.show() }) + }) - popover.show() + it('should show a popover with just content without having header', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = 'Nice link' + + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl, { + content: 'Some beautiful content :)' + }) + + popoverEl.addEventListener('shown.bs.popover', () => { + const popoverDisplayed = document.querySelector('.popover') + + expect(popoverDisplayed).not.toBeNull() + expect(popoverDisplayed.querySelector('.popover-header')).toBeNull() + expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('Some beautiful content :)') + resolve() + }) + + popover.show() + }) }) - it('should show a popover with just title without having body', done => { - fixtureEl.innerHTML = 'Nice link' + it('should show a popover with just title without having body', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = 'Nice link' - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl, { - title: 'Title, which does not require content' + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl, { + title: 'Title which does not require content' + }) + + popoverEl.addEventListener('shown.bs.popover', () => { + const popoverDisplayed = document.querySelector('.popover') + + expect(popoverDisplayed).not.toBeNull() + expect(popoverDisplayed.querySelector('.popover-body')).toBeNull() + expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('Title which does not require content') + resolve() + }) + + popover.show() }) + }) + + it('should show a popover with just title without having body using data-attribute to get config', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = 'Nice link' - popoverEl.addEventListener('shown.bs.popover', () => { - const popoverDisplayed = document.querySelector('.popover') + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl) - expect(popoverDisplayed).not.toBeNull() - expect(popoverDisplayed.querySelector('.popover-body')).toBeNull() - expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('Title, which does not require content') - done() + popoverEl.addEventListener('shown.bs.popover', () => { + const popoverDisplayed = document.querySelector('.popover') + + expect(popoverDisplayed).not.toBeNull() + expect(popoverDisplayed.querySelector('.popover-body')).toBeNull() + expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('Title which does not require content') + resolve() + }) + + popover.show() }) + }) + + it('should NOT show a popover without `title` and `content`', () => { + fixtureEl.innerHTML = 'Nice link' + + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl, { animation: false }) + const spy = spyOn(EventHandler, 'trigger').and.callThrough() popover.show() + + expect(spy).not.toHaveBeenCalledWith(popoverEl, Popover.eventName('show')) + expect(document.querySelector('.popover')).toBeNull() }) - it('should call setContent once', done => { - fixtureEl.innerHTML = 'BS twitter' + it('"setContent" should keep the initial template', () => { + fixtureEl.innerHTML = 'BS X' const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl, { - content: 'Popover content' - }) + const popover = new Popover(popoverEl) - const spy = spyOn(popover, 'setContent').and.callThrough() - let times = 1 + popover.setContent({ '.tooltip-inner': 'foo' }) + const tip = popover._getTipElement() + + expect(tip).toHaveClass('popover') + expect(tip).toHaveClass('bs-popover-auto') + expect(tip.querySelector('.popover-arrow')).not.toBeNull() + expect(tip.querySelector('.popover-header')).not.toBeNull() + expect(tip.querySelector('.popover-body')).not.toBeNull() + }) - popoverEl.addEventListener('hidden.bs.popover', () => { + it('should call setContent once', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = 'BS X' + + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl, { + content: 'Popover content' + }) + expect(popover._templateFactory).toBeNull() + let spy = null + let times = 1 + + popoverEl.addEventListener('hidden.bs.popover', () => { + popover.show() + }) + + popoverEl.addEventListener('shown.bs.popover', () => { + spy = spy || spyOn(popover._templateFactory, 'constructor').and.callThrough() + const popoverDisplayed = document.querySelector('.popover') + + expect(popoverDisplayed).not.toBeNull() + expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('Popover content') + expect(spy).toHaveBeenCalledTimes(0) + if (times > 1) { + resolve() + } + + times++ + popover.hide() + }) popover.show() }) + }) + + it('should show a popover with provided custom class', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = 'BS X' - popoverEl.addEventListener('shown.bs.popover', () => { - const popoverDisplayed = document.querySelector('.popover') + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl) - expect(popoverDisplayed).not.toBeNull() - expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('Popover content') - expect(spy).toHaveBeenCalledTimes(1) - if (times > 1) { - done() - } + popoverEl.addEventListener('shown.bs.popover', () => { + const tip = document.querySelector('.popover') + expect(tip).not.toBeNull() + expect(tip).toHaveClass('custom-class') + resolve() + }) - times++ - popover.hide() + popover.show() }) - popover.show() }) - it('should show a popover with provided custom class', done => { - fixtureEl.innerHTML = 'BS twitter' + it('should keep popover open when mouse leaves after click trigger', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = 'BS X' - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl) + const popoverEl = fixtureEl.querySelector('a') + new Popover(popoverEl) // eslint-disable-line no-new - popoverEl.addEventListener('shown.bs.popover', () => { - const tip = document.querySelector('.popover') - expect(tip).not.toBeNull() - expect(tip.classList.contains('custom-class')).toBeTrue() - done() - }) + popoverEl.addEventListener('shown.bs.popover', () => { + popoverEl.dispatchEvent(createEvent('mouseout')) - popover.show() + popoverEl.addEventListener('hide.bs.popover', () => { + throw new Error('Popover should not hide when mouse leaves after click') + }) + + expect(document.querySelector('.popover')).not.toBeNull() + resolve() + }) + + popoverEl.click() + }) }) }) describe('hide', () => { - it('should hide a popover', done => { - fixtureEl.innerHTML = 'BS twitter' + it('should hide a popover', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = 'BS X' - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl) + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl) - popoverEl.addEventListener('shown.bs.popover', () => { - popover.hide() - }) + popoverEl.addEventListener('shown.bs.popover', () => { + popover.hide() + }) - popoverEl.addEventListener('hidden.bs.popover', () => { - expect(document.querySelector('.popover')).toBeNull() - done() - }) + popoverEl.addEventListener('hidden.bs.popover', () => { + expect(document.querySelector('.popover')).toBeNull() + resolve() + }) - popover.show() + popover.show() + }) }) }) describe('jQueryInterface', () => { it('should create a popover', () => { - fixtureEl.innerHTML = 'BS twitter' + fixtureEl.innerHTML = 'BS X' const popoverEl = fixtureEl.querySelector('a') @@ -240,7 +376,7 @@ describe('Popover', () => { }) it('should create a popover with a config object', () => { - fixtureEl.innerHTML = 'BS twitter' + fixtureEl.innerHTML = 'BS X' const popoverEl = fixtureEl.querySelector('a') @@ -255,7 +391,7 @@ describe('Popover', () => { }) it('should not re create a popover', () => { - fixtureEl.innerHTML = 'BS twitter' + fixtureEl.innerHTML = 'BS X' const popoverEl = fixtureEl.querySelector('a') const popover = new Popover(popoverEl) @@ -269,7 +405,7 @@ describe('Popover', () => { }) it('should throw error on undefined method', () => { - fixtureEl.innerHTML = 'BS twitter' + fixtureEl.innerHTML = 'BS X' const popoverEl = fixtureEl.querySelector('a') const action = 'undefinedMethod' @@ -283,7 +419,7 @@ describe('Popover', () => { }) it('should should call show method', () => { - fixtureEl.innerHTML = 'BS twitter' + fixtureEl.innerHTML = 'BS X' const popoverEl = fixtureEl.querySelector('a') const popover = new Popover(popoverEl) @@ -291,17 +427,17 @@ describe('Popover', () => { jQueryMock.fn.popover = Popover.jQueryInterface jQueryMock.elements = [popoverEl] - spyOn(popover, 'show') + const spy = spyOn(popover, 'show') jQueryMock.fn.popover.call(jQueryMock, 'show') - expect(popover.show).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) }) describe('getInstance', () => { it('should return popover instance', () => { - fixtureEl.innerHTML = 'BS twitter' + fixtureEl.innerHTML = 'BS X' const popoverEl = fixtureEl.querySelector('a') const popover = new Popover(popoverEl) @@ -311,11 +447,11 @@ describe('Popover', () => { }) it('should return null when there is no popover instance', () => { - fixtureEl.innerHTML = 'BS twitter' + fixtureEl.innerHTML = 'BS X' const popoverEl = fixtureEl.querySelector('a') - expect(Popover.getInstance(popoverEl)).toEqual(null) + expect(Popover.getInstance(popoverEl)).toBeNull() }) }) @@ -336,7 +472,7 @@ describe('Popover', () => { const div = fixtureEl.querySelector('div') - expect(Popover.getInstance(div)).toEqual(null) + expect(Popover.getInstance(div)).toBeNull() expect(Popover.getOrCreateInstance(div)).toBeInstanceOf(Popover) }) @@ -345,7 +481,7 @@ describe('Popover', () => { const div = fixtureEl.querySelector('div') - expect(Popover.getInstance(div)).toEqual(null) + expect(Popover.getInstance(div)).toBeNull() const popover = Popover.getOrCreateInstance(div, { placement: 'top' }) diff --git a/js/tests/unit/scrollspy.spec.js b/js/tests/unit/scrollspy.spec.js index ad44d5b3c466..fc44471c42da 100644 --- a/js/tests/unit/scrollspy.spec.js +++ b/js/tests/unit/scrollspy.spec.js @@ -1,30 +1,71 @@ -import ScrollSpy from '../../src/scrollspy' -import Manipulator from '../../src/dom/manipulator' - -/** Test helpers */ -import { getFixture, clearFixture, createEvent, jQueryMock } from '../helpers/fixture' +import EventHandler from '../../src/dom/event-handler.js' +import ScrollSpy from '../../src/scrollspy.js' +import { + clearFixture, createEvent, getFixture, jQueryMock +} from '../helpers/fixture.js' describe('ScrollSpy', () => { let fixtureEl - const testElementIsActiveAfterScroll = ({ elementSelector, targetSelector, contentEl, scrollSpy, spy, cb }) => { + const getElementScrollSpy = element => element.scrollTo ? + spyOn(element, 'scrollTo').and.callThrough() : + spyOnProperty(element, 'scrollTop', 'set').and.callThrough() + + const scrollTo = (el, height) => { + el.scrollTop = height + } + + const onScrollStop = (callback, element, timeout = 30) => { + let handle = null + const onScroll = function () { + if (handle) { + window.clearTimeout(handle) + } + + handle = setTimeout(() => { + element.removeEventListener('scroll', onScroll) + callback() + }, timeout + 1) + } + + element.addEventListener('scroll', onScroll) + } + + const getDummyFixture = () => { + return [ + '', + '
', + '
div 1
', + '
' + ].join('') + } + + const testElementIsActiveAfterScroll = ({ elementSelector, targetSelector, contentEl, scrollSpy, cb }) => { const element = fixtureEl.querySelector(elementSelector) const target = fixtureEl.querySelector(targetSelector) - // add top padding to fix Chrome on Android failures - const paddingTop = 5 - const scrollHeight = Math.ceil(contentEl.scrollTop + Manipulator.position(target).top) + paddingTop - - function listener() { - expect(element.classList.contains('active')).toEqual(true) - contentEl.removeEventListener('scroll', listener) - expect(scrollSpy._process).toHaveBeenCalled() - spy.calls.reset() + const paddingTop = 0 + const parentOffset = getComputedStyle(contentEl).getPropertyValue('position') === 'relative' ? 0 : contentEl.offsetTop + const scrollHeight = (target.offsetTop - parentOffset) + paddingTop + + contentEl.addEventListener('activate.bs.scrollspy', event => { + if (scrollSpy._activeTarget !== element) { + return + } + + expect(element).toHaveClass('active') + expect(scrollSpy._activeTarget).toEqual(element) + expect(event.relatedTarget).toEqual(element) cb() - } + }) - contentEl.addEventListener('scroll', listener) - contentEl.scrollTop = scrollHeight + setTimeout(() => { // in case we scroll something before the test + scrollTo(contentEl, scrollHeight) + }, 100) } beforeAll(() => { @@ -55,28 +96,25 @@ describe('ScrollSpy', () => { describe('constructor', () => { it('should take care of element either passed as a CSS selector or DOM element', () => { - fixtureEl.innerHTML = '
' + fixtureEl.innerHTML = getDummyFixture() - const sSpyEl = fixtureEl.querySelector('#navigation') - const sSpyBySelector = new ScrollSpy('#navigation') + const sSpyEl = fixtureEl.querySelector('.content') + const sSpyBySelector = new ScrollSpy('.content') const sSpyByElement = new ScrollSpy(sSpyEl) expect(sSpyBySelector._element).toEqual(sSpyEl) expect(sSpyByElement._element).toEqual(sSpyEl) }) - it('should not process element without target', () => { + it('should null, if element is not scrollable', () => { fixtureEl.innerHTML = [ '', - '
', - '
', - '
', + '
', + '
test
', '
' ].join('') @@ -84,533 +122,567 @@ describe('ScrollSpy', () => { target: '#navigation' }) - expect(scrollSpy._targets.length).toEqual(2) + expect(scrollSpy._observer.root).toBeNull() + expect(scrollSpy._rootElement).toBeNull() }) - it('should only switch "active" class on current target', done => { + it('should respect threshold option', () => { fixtureEl.innerHTML = [ - '
', - '
', - '
', - '
', - ' ', - '
', - '
', - '
', - '
', - '
', - '

Overview

', - '

', - '
', - '
', - '

Detail

', - '

', - '
', - '
', + '', + '
', + ' ', '
' ].join('') - const scrollSpyEl = fixtureEl.querySelector('#scrollspy-example') - const rootEl = fixtureEl.querySelector('#root') - const scrollSpy = new ScrollSpy(scrollSpyEl, { - target: 'ss-target' - }) - - spyOn(scrollSpy, '_process').and.callThrough() - - scrollSpyEl.addEventListener('scroll', () => { - expect(rootEl.classList.contains('active')).toEqual(true) - expect(scrollSpy._process).toHaveBeenCalled() - done() + const scrollSpy = new ScrollSpy('#content', { + target: '#navigation', + threshold: [1] }) - scrollSpyEl.scrollTop = 350 + expect(scrollSpy._observer.thresholds).toEqual([1]) }) - it('should only switch "active" class on current target specified w element', done => { + it('should respect threshold option markup', () => { fixtureEl.innerHTML = [ - '
', - '
', - '
', - '
', - ' ', - '
', - '
', - '
', - '
', - '
', - '

Overview

', - '

', - '
', - '
', - '

Detail

', - '

', - '
', - '
', + '', + '
', + ' ', '
' ].join('') - const scrollSpyEl = fixtureEl.querySelector('#scrollspy-example') - const rootEl = fixtureEl.querySelector('#root') - const scrollSpy = new ScrollSpy(scrollSpyEl, { - target: fixtureEl.querySelector('#ss-target') + const scrollSpy = new ScrollSpy('#content', { + target: '#navigation' }) - spyOn(scrollSpy, '_process').and.callThrough() - - scrollSpyEl.addEventListener('scroll', () => { - expect(rootEl.classList.contains('active')).toEqual(true) - expect(scrollSpy._process).toHaveBeenCalled() - done() - }) + // See https://stackoverflow.com/a/45592926 + const expectToBeCloseToArray = (actual, expected) => { + expect(actual.length).toBe(expected.length) + for (const x of actual) { + const i = actual.indexOf(x) + expect(x).withContext(`[${i}]`).toBeCloseTo(expected[i]) + } + } - scrollSpyEl.scrollTop = 350 + expectToBeCloseToArray(scrollSpy._observer.thresholds, [0, 0.2, 1]) }) - it('should correctly select middle navigation option when large offset is used', done => { + it('should not take count to not visible sections', () => { fixtureEl.innerHTML = [ - '', '', '
', - '
', - '
', - '
', + '
test
', + ' ', + ' ', '
' ].join('') - const contentEl = fixtureEl.querySelector('#content') - const scrollSpy = new ScrollSpy(contentEl, { - target: '#navigation', - offset: Manipulator.position(contentEl).top - }) - - spyOn(scrollSpy, '_process').and.callThrough() - - contentEl.addEventListener('scroll', () => { - expect(fixtureEl.querySelector('#one-link').classList.contains('active')).toEqual(false) - expect(fixtureEl.querySelector('#two-link').classList.contains('active')).toEqual(true) - expect(fixtureEl.querySelector('#three-link').classList.contains('active')).toEqual(false) - expect(scrollSpy._process).toHaveBeenCalled() - done() + const scrollSpy = new ScrollSpy(fixtureEl.querySelector('#content'), { + target: '#navigation' }) - contentEl.scrollTop = 550 + expect(scrollSpy._observableSections.size).toBe(1) + expect(scrollSpy._targetLinks.size).toBe(1) }) - it('should add the active class to the correct element', done => { + it('should not process element without target', () => { fixtureEl.innerHTML = [ - '
- - - - - - + diff --git a/js/tests/visual/dropdown.html b/js/tests/visual/dropdown.html index f1dd705f37f7..04cf06d7b62c 100644 --- a/js/tests/visual/dropdown.html +++ b/js/tests/visual/dropdown.html @@ -10,7 +10,7 @@

Dropdown Bootstrap Visual Test

-
-
+
Dropup split align end
-
+
Dropend split
- - -
+
Dropstart split
- - - -```html -
- - - - ... - - - - - ... - - - ... - - - - - - - - -
......This cell is aligned to the top....
-
-``` - -## Nesting - -Border styles, active styles, and table variants are not inherited by nested tables. - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
#FirstLastHandle
1MarkOtto@mdo
- - - - - - - - - - - - - - - - - - - - - - - - - -
HeaderHeaderHeader
AFirstLast
BFirstLast
CFirstLast
-
3Larrythe Bird@twitter
-
- -```html - - - ... - - - ... - - - - ... - -
- - ... -
-
-``` - -## How nesting works - -To prevent _any_ styles from leaking to nested tables, we use the child combinator (`>`) selector in our CSS. Since we need to target all the `td`s and `th`s in the `thead`, `tbody`, and `tfoot`, our selector would look pretty long without it. As such, we use the rather odd looking `.table > :not(caption) > * > *` selector to target all `td`s and `th`s of the `.table`, but none of any potential nested tables. - -Note that if you add ``s as direct children of a table, those `` will be wrapped in a `` by default, thus making our selectors work as intended. - -## Anatomy - -### Table head - -Similar to tables and dark tables, use the modifier classes `.table-light` or `.table-dark` to make ``s appear light or dark gray. - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#FirstLastHandle
1MarkOtto@mdo
2JacobThornton@fat
3Larrythe Bird@twitter
-
- -```html - - - ... - - - ... - -
-``` - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#FirstLastHandle
1MarkOtto@mdo
2JacobThornton@fat
3Larrythe Bird@twitter
-
- -```html - - - ... - - - ... - -
-``` - -### Table foot - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#FirstLastHandle
1MarkOtto@mdo
2JacobThornton@fat
3Larrythe Bird@twitter
FooterFooterFooterFooter
-
- -```html - - - ... - - - ... - - - ... - -
-``` - -### Captions - -A `` functions like a heading for a table. It helps users with screen readers to find a table and understand what it's about and decide if they want to read it. - -
- - - {{< partial "table-content.html" >}} -
List of users
-
- -```html - - - - ... - - - ... - -
List of users
-``` - -You can also put the `` on the top of the table with `.caption-top`. - -{{< example >}} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
List of users
#FirstLastHandle
1MarkOtto@mdo
2JacobThornton@fat
3Larrythe Bird@twitter
-{{< /example >}} - -## Responsive tables - -Responsive tables allow tables to be scrolled horizontally with ease. Make any table responsive across all viewports by wrapping a `.table` with `.table-responsive`. Or, pick a maximum breakpoint with which to have a responsive table up to by using `.table-responsive{-sm|-md|-lg|-xl|-xxl}`. - -{{< callout warning >}} -##### Vertical clipping/truncation - -Responsive tables make use of `overflow-y: hidden`, which clips off any content that goes beyond the bottom or top edges of the table. In particular, this can clip off dropdown menus and other third-party widgets. -{{< /callout >}} - -### Always responsive - -Across every breakpoint, use `.table-responsive` for horizontally scrolling tables. - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#HeadingHeadingHeadingHeadingHeadingHeadingHeadingHeadingHeading
1CellCellCellCellCellCellCellCellCell
2CellCellCellCellCellCellCellCellCell
3CellCellCellCellCellCellCellCellCell
-
-
- -```html -
- - ... -
-
-``` - -### Breakpoint specific - -Use `.table-responsive{-sm|-md|-lg|-xl|-xxl}` as needed to create responsive tables up to a particular breakpoint. From that breakpoint and up, the table will behave normally and not scroll horizontally. - -**These tables may appear broken until their responsive styles apply at specific viewport widths.** - -{{< tables.inline >}} -{{ range $.Site.Data.breakpoints }} -{{ if not (eq . "xs") }} -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#HeadingHeadingHeadingHeadingHeadingHeadingHeadingHeading
1CellCellCellCellCellCellCellCell
2CellCellCellCellCellCellCellCell
3CellCellCellCellCellCellCellCell
-
-
-{{ end -}} -{{- end -}} -{{< /tables.inline >}} - -{{< highlight html >}} -{{< tables.inline >}} -{{- range $.Site.Data.breakpoints -}} -{{- if not (eq . "xs") }} -
- - ... -
-
-{{ end -}} -{{- end -}} -{{< /tables.inline >}} -{{< /highlight >}} - -## Sass - -### Variables - -{{< scss-docs name="table-variables" file="scss/_variables.scss" >}} - -### Loop - -{{< scss-docs name="table-loop" file="scss/_variables.scss" >}} - -### Customizing - -- The factor variables (`$table-striped-bg-factor`, `$table-active-bg-factor` & `$table-hover-bg-factor`) are used to determine the contrast in table variants. -- Apart from the light & dark table variants, theme colors are lightened by the `$table-bg-level` variable. diff --git a/site/content/docs/5.1/content/typography.md b/site/content/docs/5.1/content/typography.md deleted file mode 100644 index 7d41f04dee7e..000000000000 --- a/site/content/docs/5.1/content/typography.md +++ /dev/null @@ -1,320 +0,0 @@ ---- -layout: docs -title: Typography -description: Documentation and examples for Bootstrap typography, including global settings, headings, body text, lists, and more. -group: content -toc: true ---- - -## Global settings - -Bootstrap sets basic global display, typography, and link styles. When more control is needed, check out the [textual utility classes]({{< docsref "/utilities/text" >}}). - -- Use a [native font stack]({{< docsref "/content/reboot#native-font-stack" >}}) that selects the best `font-family` for each OS and device. -- For a more inclusive and accessible type scale, we use the browser's default root `font-size` (typically 16px) so visitors can customize their browser defaults as needed. -- Use the `$font-family-base`, `$font-size-base`, and `$line-height-base` attributes as our typographic base applied to the ``. -- Set the global link color via `$link-color`. -- Use `$body-bg` to set a `background-color` on the `` (`#fff` by default). - -These styles can be found within `_reboot.scss`, and the global variables are defined in `_variables.scss`. Make sure to set `$font-size-base` in `rem`. - -## Headings - -All HTML headings, `

` through `

`, are available. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
HeadingExample
- {{< markdown >}}`

`{{< /markdown >}} -
h1. Bootstrap heading
- {{< markdown >}}`

`{{< /markdown >}} -
h2. Bootstrap heading
- {{< markdown >}}`

`{{< /markdown >}} -
h3. Bootstrap heading
- {{< markdown >}}`

`{{< /markdown >}} -
h4. Bootstrap heading
- {{< markdown >}}`
`{{< /markdown >}} -
h5. Bootstrap heading
- {{< markdown >}}`
`{{< /markdown >}} -
h6. Bootstrap heading
- -```html -

h1. Bootstrap heading

-

h2. Bootstrap heading

-

h3. Bootstrap heading

-

h4. Bootstrap heading

-
h5. Bootstrap heading
-
h6. Bootstrap heading
-``` - -`.h1` through `.h6` classes are also available, for when you want to match the font styling of a heading but cannot use the associated HTML element. - -{{< example >}} -

h1. Bootstrap heading

-

h2. Bootstrap heading

-

h3. Bootstrap heading

-

h4. Bootstrap heading

-

h5. Bootstrap heading

-

h6. Bootstrap heading

-{{< /example >}} - -### Customizing headings - -Use the included utility classes to recreate the small secondary heading text from Bootstrap 3. - -{{< example >}} -

- Fancy display heading - With faded secondary text -

-{{< /example >}} - -## Display headings - -Traditional heading elements are designed to work best in the meat of your page content. When you need a heading to stand out, consider using a **display heading**—a larger, slightly more opinionated heading style. - -
-
Display 1
-
Display 2
-
Display 3
-
Display 4
-
Display 5
-
Display 6
-
- -```html -

Display 1

-

Display 2

-

Display 3

-

Display 4

-

Display 5

-

Display 6

-``` - -Display headings are configured via the `$display-font-sizes` Sass map and two variables, `$display-font-weight` and `$display-line-height`. - -{{< scss-docs name="display-headings" file="scss/_variables.scss" >}} - -## Lead - -Make a paragraph stand out by adding `.lead`. - -{{< example >}} -

- This is a lead paragraph. It stands out from regular paragraphs. -

-{{< /example >}} - -## Inline text elements - -Styling for common inline HTML5 elements. - -{{< example >}} -

You can use the mark tag to highlight text.

-

This line of text is meant to be treated as deleted text.

-

This line of text is meant to be treated as no longer accurate.

-

This line of text is meant to be treated as an addition to the document.

-

This line of text will render as underlined.

-

This line of text is meant to be treated as fine print.

-

This line rendered as bold text.

-

This line rendered as italicized text.

-{{< /example >}} - -Beware that those tags should be used for semantic purpose: - -- `` represents text which is marked or highlighted for reference or notation purposes. -- `` represents side-comments and small print, like copyright and legal text. -- `` represents element that are no longer relevant or no longer accurate. -- `` represents a span of inline text which should be rendered in a way that indicates that it has a non-textual annotation. - -If you want to style your text, you should use the following classes instead: - -- `.mark` will apply the same styles as ``. -- `.small` will apply the same styles as ``. -- `.text-decoration-underline` will apply the same styles as ``. -- `.text-decoration-line-through` will apply the same styles as ``. - -While not shown above, feel free to use `` and `` in HTML5. `` is meant to highlight words or phrases without conveying additional importance, while `` is mostly for voice, technical terms, etc. - -## Text utilities - -Change text alignment, transform, style, weight, line-height, decoration and color with our [text utilities]({{< docsref "/utilities/text" >}}) and [color utilities]({{< docsref "/utilities/colors" >}}). - -## Abbreviations - -Stylized implementation of HTML's `` element for abbreviations and acronyms to show the expanded version on hover. Abbreviations have a default underline and gain a help cursor to provide additional context on hover and to users of assistive technologies. - -Add `.initialism` to an abbreviation for a slightly smaller font-size. - -{{< example >}} -

attr

-

HTML

-{{< /example >}} - -## Blockquotes - -For quoting blocks of content from another source within your document. Wrap `
` around any HTML as the quote. - -{{< example >}} -
-

A well-known quote, contained in a blockquote element.

-
-{{< /example >}} - -### Naming a source - -The HTML spec requires that blockquote attribution be placed outside the `
`. When providing attribution, wrap your `
` in a `
` and use a `
` or a block level element (e.g., `

`) with the `.blockquote-footer` class. Be sure to wrap the name of the source work in `` as well. - -{{< example >}} -

-
-

A well-known quote, contained in a blockquote element.

-
- -
-{{< /example >}} - -### Alignment - -Use text utilities as needed to change the alignment of your blockquote. - -{{< example >}} -
-
-

A well-known quote, contained in a blockquote element.

-
- -
-{{< /example >}} - -{{< example >}} -
-
-

A well-known quote, contained in a blockquote element.

-
- -
-{{< /example >}} - -## Lists - -### Unstyled - -Remove the default `list-style` and left margin on list items (immediate children only). **This only applies to immediate children list items**, meaning you will need to add the class for any nested lists as well. - -{{< example >}} -
    -
  • This is a list.
  • -
  • It appears completely unstyled.
  • -
  • Structurally, it's still a list.
  • -
  • However, this style only applies to immediate child elements.
  • -
  • Nested lists: -
      -
    • are unaffected by this style
    • -
    • will still show a bullet
    • -
    • and have appropriate left margin
    • -
    -
  • -
  • This may still come in handy in some situations.
  • -
-{{< /example >}} - -### Inline - -Remove a list's bullets and apply some light `margin` with a combination of two classes, `.list-inline` and `.list-inline-item`. - -{{< example >}} -
    -
  • This is a list item.
  • -
  • And another one.
  • -
  • But they're displayed inline.
  • -
-{{< /example >}} - -### Description list alignment - -Align terms and descriptions horizontally by using our grid system's predefined classes (or semantic mixins). For longer terms, you can optionally add a `.text-truncate` class to truncate the text with an ellipsis. - -{{< example >}} -
-
Description lists
-
A description list is perfect for defining terms.
- -
Term
-
-

Definition for the term.

-

And some more placeholder definition text.

-
- -
Another term
-
This definition is short, so no extra paragraphs or anything.
- -
Truncated term is truncated
-
This can be useful when space is tight. Adds an ellipsis at the end.
- -
Nesting
-
-
-
Nested definition list
-
I heard you like definition lists. Let me put a definition list inside your definition list.
-
-
-
-{{< /example >}} - -## Responsive font sizes - -In Bootstrap 5, we've enabled responsive font sizes by default, allowing text to scale more naturally across device and viewport sizes. Have a look at the [RFS page]({{< docsref "/getting-started/rfs" >}}) to find out how this works. - -## Sass - -### Variables - -Headings have some dedicated variables for sizing and spacing. - -{{< scss-docs name="headings-variables" file="scss/_variables.scss" >}} - -Miscellaneous typography elements covered here and in [Reboot]({{< docsref "/content/reboot" >}}) also have dedicated variables. - -{{< scss-docs name="type-variables" file="scss/_variables.scss" >}} - -### Mixins - -There are no dedicated mixins for typography, but Bootstrap does use [Responsive Font Sizing (RFS)]({{< docsref "/getting-started/rfs" >}}). diff --git a/site/content/docs/5.1/customize/color.md b/site/content/docs/5.1/customize/color.md deleted file mode 100644 index 63e5d19e6a7b..000000000000 --- a/site/content/docs/5.1/customize/color.md +++ /dev/null @@ -1,150 +0,0 @@ ---- -layout: docs -title: Color -description: Bootstrap is supported by an extensive color system that themes our styles and components. This enables more comprehensive customization and extension for any project. -group: customize -toc: true ---- - -## Theme colors - -We use a subset of all colors to create a smaller color palette for generating color schemes, also available as Sass variables and a Sass map in Bootstrap's `scss/_variables.scss` file. - -
- {{< theme-colors.inline >}} - {{- range (index $.Site.Data "theme-colors") }} -
-
{{ .name | title }}
-
- {{ end -}} - {{< /theme-colors.inline >}} -
- -All these colors are available as a Sass map, `$theme-colors`. - -{{< scss-docs name="theme-colors-map" file="scss/_variables.scss" >}} - -Check out [our Sass maps and loops docs]({{< docsref "/customize/sass#maps-and-loops" >}}) for how to modify these colors. - -## All colors - -All Bootstrap colors are available as Sass variables and a Sass map in `scss/_variables.scss` file. To avoid increased file sizes, we don't create text or background color classes for each of these variables. Instead, we choose a subset of these colors for a [theme palette](#theme-colors). - -Be sure to monitor contrast ratios as you customize colors. As shown below, we've added three contrast ratios to each of the main colors—one for the swatch's current colors, one for against white, and one for against black. - -
- {{< theme-colors.inline >}} - {{- range $color := $.Site.Data.colors }} - {{- if (and (not (eq $color.name "white")) (not (eq $color.name "gray")) (not (eq $color.name "gray-dark"))) }} -
-
- ${{ $color.name }} - {{ $color.hex }} -
- {{ range (seq 100 100 900) }} -
${{ $color.name }}-{{ . }}
- {{ end }} -
- {{ end -}} - {{ end -}} - -
-
- $gray-500 - #adb5bd -
- {{- range $.Site.Data.grays }} -
$gray-{{ .name }}
- {{ end -}} -
- {{< /theme-colors.inline >}} - -
-
- $black - #000 -
-
- $white - #fff -
-
-
- -### Notes on Sass - -Sass cannot programmatically generate variables, so we manually created variables for every tint and shade ourselves. We specify the midpoint value (e.g., `$blue-500`) and use custom color functions to tint (lighten) or shade (darken) our colors via Sass's `mix()` color function. - -Using `mix()` is not the same as `lighten()` and `darken()`—the former blends the specified color with white or black, while the latter only adjusts the lightness value of each color. The result is a much more complete suite of colors, as [shown in this CodePen demo](https://codepen.io/emdeoh/pen/zYOQOPB). - -Our `tint-color()` and `shade-color()` functions use `mix()` alongside our `$theme-color-interval` variable, which specifies a stepped percentage value for each mixed color we produce. See the `scss/_functions.scss` and `scss/_variables.scss` files for the full source code. - -## Color Sass maps - -Bootstrap's source Sass files include three maps to help you quickly and easily loop over a list of colors and their hex values. - -- `$colors` lists all our available base (`500`) colors -- `$theme-colors` lists all semantically named theme colors (shown below) -- `$grays` lists all tints and shades of gray - -Within `scss/_variables.scss`, you'll find Bootstrap's color variables and Sass map. Here's an example of the `$colors` Sass map: - -{{< scss-docs name="colors-map" file="scss/_variables.scss" >}} - -Add, remove, or modify values within the map to update how they're used in many other components. Unfortunately at this time, not _every_ component utilizes this Sass map. Future updates will strive to improve upon this. Until then, plan on making use of the `${color}` variables and this Sass map. - -### Example - -Here's how you can use these in your Sass: - -```scss -.alpha { color: $purple; } -.beta { - color: $yellow-300; - background-color: $indigo-900; -} -``` - -[Color]({{< docsref "/utilities/colors" >}}) and [background]({{< docsref "/utilities/background" >}}) utility classes are also available for setting `color` and `background-color` using the `500` color values. - -## Generating utilities - -Added in v5.1.0 - -Bootstrap doesn't include `color` and `background-color` utilities for every color variable, but you can generate these yourself with our [utility API]({{< docsref "/utilities/api" >}}) and our extended Sass maps added in v5.1.0. - -1. To start, make sure you've imported our functions, variables, mixins, and utilities. -2. Use our `map-merge-multiple()` function to quickly merge multiple Sass maps together in a new map. -3. Merge this new combined map to extend any utility with a `{color}-{level}` class name. - -Here's an example that generates text color utilities (e.g., `.text-purple-500`) using the above steps. - -```scss -@import "bootstrap/scss/functions"; -@import "bootstrap/scss/variables"; -@import "bootstrap/scss/mixins"; -@import "bootstrap/scss/utilities"; - -$all-colors: map-merge-multiple($blues, $indigos, $purples, $pinks, $reds, $oranges, $yellows, $greens, $teals, $cyans); - -$utilities: map-merge( - $utilities, - ( - "color": map-merge( - map-get($utilities, "color"), - ( - values: map-merge( - map-get(map-get($utilities, "color"), "values"), - ( - $all-colors - ), - ), - ), - ), - ) -); - -@import "bootstrap/scss/utilities/api"; -``` - -This will generate new `.text-{color}-{level}` utilities for every color and level. You can do the same for any other utility and property as well. diff --git a/site/content/docs/5.1/customize/components.md b/site/content/docs/5.1/customize/components.md deleted file mode 100644 index b512a9036bd9..000000000000 --- a/site/content/docs/5.1/customize/components.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -layout: docs -title: Components -description: Learn how and why we build nearly all our components responsively and with base and modifier classes. -group: customize -toc: true ---- - -## Base classes - -Bootstrap's components are largely built with a base-modifier nomenclature. We group as many shared properties as possible into a base class, like `.btn`, and then group individual styles for each variant into modifier classes, like `.btn-primary` or `.btn-success`. - -To build our modifier classes, we use Sass's `@each` loops to iterate over a Sass map. This is especially helpful for generating variants of a component by our `$theme-colors` and creating responsive variants for each breakpoint. As you customize these Sass maps and recompile, you'll automatically see your changes reflected in these loops. - -Check out [our Sass maps and loops docs]({{< docsref "/customize/sass#maps-and-loops" >}}) for how to customize these loops and extend Bootstrap's base-modifier approach to your own code. - -## Modifiers - -Many of Bootstrap's components are built with a base-modifier class approach. This means the bulk of the styling is contained to a base class (e.g., `.btn`) while style variations are confined to modifier classes (e.g., `.btn-danger`). These modifier classes are built from the `$theme-colors` map to make customizing the number and name of our modifier classes. - -Here are two examples of how we loop over the `$theme-colors` map to generate modifiers to the `.alert` and `.list-group` components. - -{{< scss-docs name="alert-modifiers" file="scss/_alert.scss" >}} - -{{< scss-docs name="list-group-modifiers" file="scss/_list-group.scss" >}} - -## Responsive - -These Sass loops aren't limited to color maps, either. You can also generate responsive variations of your components. Take for example our responsive alignment of the dropdowns where we mix an `@each` loop for the `$grid-breakpoints` Sass map with a media query include. - -{{< scss-docs name="responsive-breakpoints" file="scss/_dropdown.scss" >}} - -Should you modify your `$grid-breakpoints`, your changes will apply to all the loops iterating over that map. - -{{< scss-docs name="grid-breakpoints" file="scss/_variables.scss" >}} - -For more information and examples on how to modify our Sass maps and variables, please refer to [the Sass section of the Grid documentation]({{< docsref "/layout/grid#sass" >}}). - -## Creating your own - -We encourage you to adopt these guidelines when building with Bootstrap to create your own components. We've extended this approach ourselves to the custom components in our documentation and examples. Components like our callouts are built just like our provided components with base and modifier classes. - -
-
- This is a callout. We built it custom for our docs so our messages to you stand out. It has three variants via modifier classes. -
-
- -```html -
...
-``` - -In your CSS, you'd have something like the following where the bulk of the styling is done via `.callout`. Then, the unique styles between each variant is controlled via modifier class. - -```scss -// Base class -.callout {} - -// Modifier classes -.callout-info {} -.callout-warning {} -.callout-danger {} -``` - -For the callouts, that unique styling is just a `border-left-color`. When you combine that base class with one of those modifier classes, you get your complete component family: - -{{< callout info >}} -**This is an info callout.** Example text to show it in action. -{{< /callout >}} - -{{< callout warning >}} -**This is a warning callout.** Example text to show it in action. -{{< /callout >}} - -{{< callout danger >}} -**This is a danger callout.** Example text to show it in action. -{{< /callout >}} diff --git a/site/content/docs/5.1/customize/css-variables.md b/site/content/docs/5.1/customize/css-variables.md deleted file mode 100644 index 079f9ad23fb1..000000000000 --- a/site/content/docs/5.1/customize/css-variables.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -layout: docs -title: CSS variables -description: Use Bootstrap's CSS custom properties for fast and forward-looking design and development. -group: customize -toc: true ---- - -Bootstrap includes many [CSS custom properties (variables)](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) in its compiled CSS for real-time customization without the need to recompile Sass. These provide easy access to commonly used values like our theme colors, breakpoints, and primary font stacks when working in your browser's inspector, a code sandbox, or general prototyping. - -**All our custom properties are prefixed with `bs-`** to avoid conflicts with third party CSS. - -## Root variables - -Here are the variables we include (note that the `:root` is required) that can be accessed anywhere Bootstrap's CSS is loaded. They're located in our `_root.scss` file and included in our compiled dist files. - -```css -{{< root.inline >}} -{{- $css := readFile "dist/css/bootstrap.css" -}} -{{- $match := findRE ":root {([^}]*)}" $css 1 -}} - -{{- if (eq (len $match) 0) -}} -{{- errorf "Got no matches for :root in %q!" $.Page.Path -}} -{{- end -}} - -{{- index $match 0 -}} - -{{< /root.inline >}} -``` - -## Component variables - -We're also beginning to make use of custom properties as local variables for various components. This way we can reduce our compiled CSS, ensure styles aren't inherited in places like nested tables, and allow some basic restyling and extending of Bootstrap components after Sass compilation. - -Have a look at our table documentation for some [insight into how we're using CSS variables]({{< docsref "/content/tables#how-do-the-variants-and-accented-tables-work" >}}). - -We're also using CSS variables across our grids—primarily for gutters—with more component usage coming in the future. - -## Examples - -CSS variables offer similar flexibility to Sass's variables, but without the need for compilation before being served to the browser. For example, here we're resetting our page's font and link styles with CSS variables. - -```css -body { - font: 1rem/1.5 var(--bs-font-sans-serif); -} -a { - color: var(--bs-blue); -} -``` - -## Grid breakpoints - -While we include our grid breakpoints as CSS variables (except for `xs`), be aware that **CSS variables do not work in media queries**. This is by design in the CSS spec for variables, but may change in coming years with support for `env()` variables. Check out [this Stack Overflow answer](https://stackoverflow.com/a/47212942) for some helpful links. In the mean time, you can use these variables in other CSS situations, as well as in your JavaScript. diff --git a/site/content/docs/5.1/customize/optimize.md b/site/content/docs/5.1/customize/optimize.md deleted file mode 100644 index 29b152154fb8..000000000000 --- a/site/content/docs/5.1/customize/optimize.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -layout: docs -title: Optimize -description: Keep your projects lean, responsive, and maintainable so you can deliver the best experience and focus on more important jobs. -group: customize -toc: true ---- - -## Lean Sass imports - -When using Sass in your asset pipeline, make sure you optimize Bootstrap by only `@import`ing the components you need. Your largest optimizations will likely come from the `Layout & Components` section of our `bootstrap.scss`. - -{{< scss-docs name="import-stack" file="scss/bootstrap.scss" >}} - - -If you're not using a component, comment it out or delete it entirely. For example, if you're not using the carousel, remove that import to save some file size in your compiled CSS. Keep in mind there are some dependencies across Sass imports that may make it more difficult to omit a file. - -## Lean JavaScript - -Bootstrap's JavaScript includes every component in our primary dist files (`bootstrap.js` and `bootstrap.min.js`), and even our primary dependency (Popper) with our bundle files (`bootstrap.bundle.js` and `bootstrap.bundle.min.js`). While you're customizing via Sass, be sure to remove related JavaScript. - -For instance, assuming you're using your own JavaScript bundler like Webpack or Rollup, you'd only import the JavaScript you plan on using. In the example below, we show how to just include our modal JavaScript: - -```js -// Import just what we need - -// import 'bootstrap/js/dist/alert'; -// import 'bootstrap/js/dist/button'; -// import 'bootstrap/js/dist/carousel'; -// import 'bootstrap/js/dist/collapse'; -// import 'bootstrap/js/dist/dropdown'; -import 'bootstrap/js/dist/modal'; -// import 'bootstrap/js/dist/offcanvas'; -// import 'bootstrap/js/dist/popover'; -// import 'bootstrap/js/dist/scrollspy'; -// import 'bootstrap/js/dist/tab'; -// import 'bootstrap/js/dist/toast'; -// import 'bootstrap/js/dist/tooltip'; -``` - -This way, you're not including any JavaScript you don't intend to use for components like buttons, carousels, and tooltips. If you're importing dropdowns, tooltips or popovers, be sure to list the Popper dependency in your `package.json` file. - -{{< callout info >}} -### Default Exports - -Files in `bootstrap/js/dist` use the **default export**, so if you want to use one of them you have to do the following: - -```js -import Modal from 'bootstrap/js/dist/modal' - -const modal = new Modal(document.getElementById('myModal')) -``` -{{< /callout >}} - -## Autoprefixer .browserslistrc - -Bootstrap depends on Autoprefixer to automatically add browser prefixes to certain CSS properties. Prefixes are dictated by our `.browserslistrc` file, found in the root of the Bootstrap repo. Customizing this list of browsers and recompiling the Sass will automatically remove some CSS from your compiled CSS, if there are vendor prefixes unique to that browser or version. - -## Unused CSS - -_Help wanted with this section, please consider opening a PR. Thanks!_ - -While we don't have a prebuilt example for using [PurgeCSS](https://github.com/FullHuman/purgecss) with Bootstrap, there are some helpful articles and walkthroughs that the community has written. Here are some options: - -- -- - -Lastly, this [CSS Tricks article on unused CSS](https://css-tricks.com/how-do-you-remove-unused-css-from-a-site/) shows how to use PurgeCSS and other similar tools. - -## Minify and gzip - -Whenever possible, be sure to compress all the code you serve to your visitors. If you're using Bootstrap dist files, try to stick to the minified versions (indicated by the `.min.css` and `.min.js` extensions). If you're building Bootstrap from the source with your own build system, be sure to implement your own minifiers for HTML, CSS, and JS. - -## Nonblocking files - -While minifying and using compression might seem like enough, making your files nonblocking ones is also a big step in making your site well-optimized and fast enough. - -If you are using a [Lighthouse](https://developers.google.com/web/tools/lighthouse/) plugin in Google Chrome, you may have stumbled over FCP. [The First Contentful Paint](https://web.dev/fcp/) metric measures the time from when the page starts loading to when any part of the page's content is rendered on the screen. - -You can improve FCP by deferring non-critical JavaScript or CSS. What does that mean? Simply, JavaScript or stylesheets that don't need to be present on the first paint of your page should be marked with `async` or `defer` attributes. - -This ensures that the less important resources are loaded later and not blocking the first paint. On the other hand, critical resources can be included as inline scripts or styles. - -If you want to learn more about this, there are already a lot of great articles about it: - -- -- - -## Always use HTTPS - -Your website should only be available over HTTPS connections in production. HTTPS improves the security, privacy, and availability of all sites, and [there is no such thing as non-sensitive web traffic](https://https.cio.gov/everything/). The steps to configure your website to be served exclusively over HTTPS vary widely depending on your architecture and web hosting provider, and thus are beyond the scope of these docs. - -Sites served over HTTPS should also access all stylesheets, scripts, and other assets over HTTPS connections. Otherwise, you'll be sending users [mixed active content](https://developer.mozilla.org/en-US/docs/Web/Security/Mixed_content), leading to potential vulnerabilities where a site can be compromised by altering a dependency. This can lead to security issues and in-browser warnings displayed to users. Whether you're getting Bootstrap from a CDN or serving it yourself, ensure that you only access it over HTTPS connections. diff --git a/site/content/docs/5.1/customize/overview.md b/site/content/docs/5.1/customize/overview.md deleted file mode 100644 index 03b4bff333f1..000000000000 --- a/site/content/docs/5.1/customize/overview.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -layout: docs -title: Customize -description: Learn how to theme, customize, and extend Bootstrap with Sass, a boatload of global options, an expansive color system, and more. -group: customize -toc: false -aliases: "/docs/5.1/customize/" -sections: - - title: Sass - description: Utilize our source Sass files to take advantage of variables, maps, mixins, and functions. - - title: Options - description: Customize Bootstrap with built-in variables to easily toggle global CSS preferences. - - title: Color - description: Learn about and customize the color systems that support the entire toolkit. - - title: Components - description: Learn how we build nearly all our components responsively and with base and modifier classes. - - title: CSS variables - description: Use Bootstrap's CSS custom properties for fast and forward-looking design and development. - - title: Optimize - description: Keep your projects lean, responsive, and maintainable so you can deliver the best experience. ---- - -## Overview - -There are multiple ways to customize Bootstrap. Your best path can depend on your project, the complexity of your build tools, the version of Bootstrap you're using, browser support, and more. - -Our two preferred methods are: - -1. Using Bootstrap [via package manager]({{< docsref "/getting-started/download#package-managers" >}}) so you can use and extend our source files. -2. Using Bootstrap's compiled distribution files or [jsDelivr]({{< docsref "/getting-started/download#cdn-via-jsdelivr" >}}) so you can add onto or override Bootstrap's styles. - -While we cannot go into details here on how to use every package manager, we can give some guidance on [using Bootstrap with your own Sass compiler]({{< docsref "/customize/sass" >}}). - -For those who want to use the distribution files, review the [getting started page]({{< docsref "/getting-started/introduction" >}}) for how to include those files and an example HTML page. From there, consult the docs for the layout, components, and behaviors you'd like to use. - -As you familiarize yourself with Bootstrap, continue exploring this section for more details on how to utilize our global options, making use of and changing our color system, how we build our components, how to use our growing list of CSS custom properties, and how to optimize your code when building with Bootstrap. - -## CSPs and embedded SVGs - -Several Bootstrap components include embedded SVGs in our CSS to style components consistently and easily across browsers and devices. **For organizations with more strict CSP configurations**, we've documented all instances of our embedded SVGs (all of which are applied via `background-image`) so you can more thoroughly review your options. - -- [Accordion]({{< docsref "/components/accordion" >}}) -- [Close button]({{< docsref "/components/close-button" >}}) (used in alerts and modals) -- [Form checkboxes and radio buttons]({{< docsref "/forms/checks-radios" >}}) -- [Form switches]({{< docsref "/forms/checks-radios#switches" >}}) -- [Form validation icons]({{< docsref "/forms/validation#server-side" >}}) -- [Select menus]({{< docsref "/forms/select" >}}) -- [Carousel controls]({{< docsref "/components/carousel#with-controls" >}}) -- [Navbar toggle buttons]({{< docsref "/components/navbar#responsive-behaviors" >}}) - -Based on [community conversation](https://github.com/twbs/bootstrap/issues/25394), some options for addressing this in your own codebase include replacing the URLs with locally hosted assets, removing the images and using inline images (not possible in all components), and modifying your CSP. Our recommendation is to carefully review your own security policies and decide on the best path forward, if necessary. diff --git a/site/content/docs/5.1/customize/sass.md b/site/content/docs/5.1/customize/sass.md deleted file mode 100644 index 137f1cfd8281..000000000000 --- a/site/content/docs/5.1/customize/sass.md +++ /dev/null @@ -1,303 +0,0 @@ ---- -layout: docs -title: Sass -description: Utilize our source Sass files to take advantage of variables, maps, mixins, and functions to help you build faster and customize your project. -group: customize -toc: true ---- - -Utilize our source Sass files to take advantage of variables, maps, mixins, and more. - -## File structure - -Whenever possible, avoid modifying Bootstrap's core files. For Sass, that means creating your own stylesheet that imports Bootstrap so you can modify and extend it. Assuming you're using a package manager like npm, you'll have a file structure that looks like this: - -```text -your-project/ -├── scss -│ └── custom.scss -└── node_modules/ - └── bootstrap - ├── js - └── scss -``` - -If you've downloaded our source files and aren't using a package manager, you'll want to manually setup something similar to that structure, keeping Bootstrap's source files separate from your own. - -```text -your-project/ -├── scss -│ └── custom.scss -└── bootstrap/ - ├── js - └── scss -``` - -## Importing - -In your `custom.scss`, you'll import Bootstrap's source Sass files. You have two options: include all of Bootstrap, or pick the parts you need. We encourage the latter, though be aware there are some requirements and dependencies across our components. You also will need to include some JavaScript for our plugins. - -```scss -// Custom.scss -// Option A: Include all of Bootstrap - -// Include any default variable overrides here (though functions won't be available) - -@import "../node_modules/bootstrap/scss/bootstrap"; - -// Then add additional custom code here -``` - -```scss -// Custom.scss -// Option B: Include parts of Bootstrap - -// 1. Include functions first (so you can manipulate colors, SVGs, calc, etc) -@import "../node_modules/bootstrap/scss/functions"; - -// 2. Include any default variable overrides here - -// 3. Include remainder of required Bootstrap stylesheets -@import "../node_modules/bootstrap/scss/variables"; -@import "../node_modules/bootstrap/scss/mixins"; -@import "../node_modules/bootstrap/scss/root"; - -// 4. Include any optional Bootstrap CSS as needed -@import "../node_modules/bootstrap/scss/utilities"; -@import "../node_modules/bootstrap/scss/reboot"; -@import "../node_modules/bootstrap/scss/type"; -@import "../node_modules/bootstrap/scss/images"; -@import "../node_modules/bootstrap/scss/containers"; -@import "../node_modules/bootstrap/scss/grid"; -@import "../node_modules/bootstrap/scss/helpers"; - -// 5. Optionally include utilities API last to generate classes based on the Sass map in `_utililies.scss` -@import "../node_modules/bootstrap/scss/utilities/api"; - -// 6. Add additional custom code here -``` - -With that setup in place, you can begin to modify any of the Sass variables and maps in your `custom.scss`. You can also start to add parts of Bootstrap under the `// Optional` section as needed. We suggest using the full import stack from our `bootstrap.scss` file as your starting point. - -## Variable defaults - -Every Sass variable in Bootstrap includes the `!default` flag allowing you to override the variable's default value in your own Sass without modifying Bootstrap's source code. Copy and paste variables as needed, modify their values, and remove the `!default` flag. If a variable has already been assigned, then it won't be re-assigned by the default values in Bootstrap. - -You will find the complete list of Bootstrap's variables in `scss/_variables.scss`. Some variables are set to `null`, these variables don't output the property unless they are overridden in your configuration. - -Variable overrides must come after our functions are imported, but before the rest of the imports. - -Here's an example that changes the `background-color` and `color` for the `` when importing and compiling Bootstrap via npm: - -```scss -// Required -@import "../node_modules/bootstrap/scss/functions"; - -// Default variable overrides -$body-bg: #000; -$body-color: #111; - -// Required -@import "../node_modules/bootstrap/scss/variables"; -@import "../node_modules/bootstrap/scss/mixins"; -@import "../node_modules/bootstrap/scss/root"; - -// Optional Bootstrap components here -@import "../node_modules/bootstrap/scss/reboot"; -@import "../node_modules/bootstrap/scss/type"; -// etc -``` - -Repeat as necessary for any variable in Bootstrap, including the global options below. - -{{< callout info >}} -{{< partial "callout-info-npm-starter.md" >}} -{{< /callout >}} - -## Maps and loops - -Bootstrap includes a handful of Sass maps, key value pairs that make it easier to generate families of related CSS. We use Sass maps for our colors, grid breakpoints, and more. Just like Sass variables, all Sass maps include the `!default` flag and can be overridden and extended. - -Some of our Sass maps are merged into empty ones by default. This is done to allow easy expansion of a given Sass map, but comes at the cost of making _removing_ items from a map slightly more difficult. - -### Modify map - -All variables in the `$theme-colors` map are defined as standalone variables. To modify an existing color in our `$theme-colors` map, add the following to your custom Sass file: - -```scss -$primary: #0074d9; -$danger: #ff4136; -``` - -Later on, these variables are set in Bootstrap's `$theme-colors` map: - -```scss -$theme-colors: ( - "primary": $primary, - "danger": $danger -); -``` - -### Add to map - -Add new colors to `$theme-colors`, or any other map, by creating a new Sass map with your custom values and merging it with the original map. In this case, we'll create a new `$custom-colors` map and merge it with `$theme-colors`. - -```scss -// Create your own map -$custom-colors: ( - "custom-color": #900 -); - -// Merge the maps -$theme-colors: map-merge($theme-colors, $custom-colors); -``` - -### Remove from map - -To remove colors from `$theme-colors`, or any other map, use `map-remove`. Be aware you must insert it between our requirements and options: - -```scss -// Required -@import "../node_modules/bootstrap/scss/functions"; -@import "../node_modules/bootstrap/scss/variables"; -@import "../node_modules/bootstrap/scss/mixins"; -@import "../node_modules/bootstrap/scss/root"; - -$theme-colors: map-remove($theme-colors, "info", "light", "dark"); - -// Optional -@import "../node_modules/bootstrap/scss/reboot"; -@import "../node_modules/bootstrap/scss/type"; -// etc -``` - -## Required keys - -Bootstrap assumes the presence of some specific keys within Sass maps as we used and extend these ourselves. As you customize the included maps, you may encounter errors where a specific Sass map's key is being used. - -For example, we use the `primary`, `success`, and `danger` keys from `$theme-colors` for links, buttons, and form states. Replacing the values of these keys should present no issues, but removing them may cause Sass compilation issues. In these instances, you'll need to modify the Sass code that makes use of those values. - -## Functions - -### Colors - -Next to the [Sass maps]({{< docsref "/customize/color#color-sass-maps" >}}) we have, theme colors can also be used as standalone variables, like `$primary`. - -```scss -.custom-element { - color: $gray-100; - background-color: $dark; -} -``` - -You can lighten or darken colors with Bootstrap's `tint-color()` and `shade-color()` functions. These functions will mix colors with black or white, unlike Sass' native `lighten()` and `darken()` functions which will change the lightness by a fixed amount, which often doesn't lead to the desired effect. - -{{< scss-docs name="color-functions" file="scss/_functions.scss" >}} - -In practice, you'd call the function and pass in the color and weight parameters. - -```scss -.custom-element { - color: tint-color($primary, 10%); -} - -.custom-element-2 { - color: shade-color($danger, 30%); -} -``` - -### Color contrast - -In order to meet [WCAG 2.0 accessibility standards for color contrast](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html), authors **must** provide [a contrast ratio of at least 4.5:1](https://www.w3.org/WAI/WCAG20/quickref/20160105/Overview.php#visual-audio-contrast-contrast), with very few exceptions. - -An additional function we include in Bootstrap is the color contrast function, `color-contrast`. It utilizes the [WCAG 2.0 algorithm](https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests) for calculating contrast thresholds based on [relative luminance](https://www.w3.org/WAI/GL/wiki/Relative_luminance) in a `sRGB` colorspace to automatically return a light (`#fff`), dark (`#212529`) or black (`#000`) contrast color based on the specified base color. This function is especially useful for mixins or loops where you're generating multiple classes. - -For example, to generate color swatches from our `$theme-colors` map: - -```scss -@each $color, $value in $theme-colors { - .swatch-#{$color} { - color: color-contrast($value); - } -} -``` - -It can also be used for one-off contrast needs: - -```scss -.custom-element { - color: color-contrast(#000); // returns `color: #fff` -} -``` - -You can also specify a base color with our color map functions: - -```scss -.custom-element { - color: color-contrast($dark); // returns `color: #fff` -} -``` - -### Escape SVG - -We use the `escape-svg` function to escape the `<`, `>` and `#` characters for SVG background images. When using the `escape-svg` function, data URIs must be quoted. - -### Add and Subtract functions - -We use the `add` and `subtract` functions to wrap the CSS `calc` function. The primary purpose of these functions is to avoid errors when a "unitless" `0` value is passed into a `calc` expression. Expressions like `calc(10px - 0)` will return an error in all browsers, despite being mathematically correct. - -Example where the calc is valid: - -```scss -$border-radius: .25rem; -$border-width: 1px; - -.element { - // Output calc(.25rem - 1px) is valid - border-radius: calc($border-radius - $border-width); -} - -.element { - // Output the same calc(.25rem - 1px) as above - border-radius: subtract($border-radius, $border-width); -} -``` - -Example where the calc is invalid: - -```scss -$border-radius: .25rem; -$border-width: 0; - -.element { - // Output calc(.25rem - 0) is invalid - border-radius: calc($border-radius - $border-width); -} - -.element { - // Output .25rem - border-radius: subtract($border-radius, $border-width); -} -``` - -## Mixins - -Our `scss/mixins/` directory has a ton of mixins that power parts of Bootstrap and can also be used across your own project. - -### Color schemes - -A shorthand mixin for the `prefers-color-scheme` media query is available with support for `light`, `dark`, and custom color schemes. - -{{< scss-docs name="mixin-color-scheme" file="scss/mixins/_color-scheme.scss" >}} - -```scss -.custom-element { - @include color-scheme(dark) { - // Insert dark mode styles here - } - - @include color-scheme(custom-named-scheme) { - // Insert custom color scheme styles here - } -} -``` diff --git a/site/content/docs/5.1/examples/.stylelintrc b/site/content/docs/5.1/examples/.stylelintrc deleted file mode 100644 index dc76dedbde32..000000000000 --- a/site/content/docs/5.1/examples/.stylelintrc +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": [ - "stylelint-config-twbs-bootstrap/css" - ], - "rules": { - "at-rule-no-vendor-prefix": null, - "comment-empty-line-before": null, - "media-feature-name-no-vendor-prefix": null, - "property-blacklist": null, - "property-no-vendor-prefix": null, - "selector-no-qualifying-type": null, - "selector-no-vendor-prefix": null, - "value-no-vendor-prefix": null - } -} diff --git a/site/content/docs/5.1/examples/_index.md b/site/content/docs/5.1/examples/_index.md deleted file mode 100644 index 3d5bfab2fb46..000000000000 --- a/site/content/docs/5.1/examples/_index.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -layout: single -title: Examples -description: Quickly get a project started with any of our examples ranging from using parts of the framework to custom components and layouts. -aliases: "/examples/" ---- - -{{< list-examples.inline >}} -{{ range $entry := $.Site.Data.examples -}} -

{{ $entry.category }}

-

{{ $entry.description }}

- {{ if eq $entry.category "RTL" -}} -
-

The RTL feature is still experimental and will probably evolve according to user feedback. Spotted something or have an improvement to suggest? Open an issue, we'd love to get your insights.

-
- {{ end -}} - - {{ range $i, $example := $entry.examples -}} - {{- $len := len $entry.examples -}} - {{ if (eq $i 0) }}
{{ end }} -
- - -

{{ $example.name }}

-
-

{{ $example.description }}

-
- {{ if (eq (add $i 1) $len) }}
{{ end }} - {{ end -}} -{{ end -}} -{{< /list-examples.inline >}} diff --git a/site/content/docs/5.1/examples/blog/blog.css b/site/content/docs/5.1/examples/blog/blog.css deleted file mode 100644 index 437a540f6abc..000000000000 --- a/site/content/docs/5.1/examples/blog/blog.css +++ /dev/null @@ -1,103 +0,0 @@ -/* stylelint-disable selector-list-comma-newline-after */ - -.blog-header { - line-height: 1; - border-bottom: 1px solid #e5e5e5; -} - -.blog-header-logo { - font-family: "Playfair Display", Georgia, "Times New Roman", serif/*rtl:Amiri, Georgia, "Times New Roman", serif*/; - font-size: 2.25rem; -} - -.blog-header-logo:hover { - text-decoration: none; -} - -h1, h2, h3, h4, h5, h6 { - font-family: "Playfair Display", Georgia, "Times New Roman", serif/*rtl:Amiri, Georgia, "Times New Roman", serif*/; -} - -.display-4 { - font-size: 2.5rem; -} -@media (min-width: 768px) { - .display-4 { - font-size: 3rem; - } -} - -.nav-scroller { - position: relative; - z-index: 2; - height: 2.75rem; - overflow-y: hidden; -} - -.nav-scroller .nav { - display: flex; - flex-wrap: nowrap; - padding-bottom: 1rem; - margin-top: -1px; - overflow-x: auto; - text-align: center; - white-space: nowrap; - -webkit-overflow-scrolling: touch; -} - -.nav-scroller .nav-link { - padding-top: .75rem; - padding-bottom: .75rem; - font-size: .875rem; -} - -.card-img-right { - height: 100%; - border-radius: 0 3px 3px 0; -} - -.flex-auto { - flex: 0 0 auto; -} - -.h-250 { height: 250px; } -@media (min-width: 768px) { - .h-md-250 { height: 250px; } -} - -/* Pagination */ -.blog-pagination { - margin-bottom: 4rem; -} -.blog-pagination > .btn { - border-radius: 2rem; -} - -/* - * Blog posts - */ -.blog-post { - margin-bottom: 4rem; -} -.blog-post-title { - margin-bottom: .25rem; - font-size: 2.5rem; -} -.blog-post-meta { - margin-bottom: 1.25rem; - color: #727272; -} - -/* - * Footer - */ -.blog-footer { - padding: 2.5rem 0; - color: #727272; - text-align: center; - background-color: #f9f9f9; - border-top: .05rem solid #e5e5e5; -} -.blog-footer p:last-child { - margin-bottom: 0; -} diff --git a/site/content/docs/5.1/examples/blog/blog.rtl.css b/site/content/docs/5.1/examples/blog/blog.rtl.css deleted file mode 100644 index 35eac731f8e5..000000000000 --- a/site/content/docs/5.1/examples/blog/blog.rtl.css +++ /dev/null @@ -1,103 +0,0 @@ -/* stylelint-disable selector-list-comma-newline-after */ - -.blog-header { - line-height: 1; - border-bottom: 1px solid #e5e5e5; -} - -.blog-header-logo { - font-family: Amiri, Georgia, "Times New Roman", serif; - font-size: 2.25rem; -} - -.blog-header-logo:hover { - text-decoration: none; -} - -h1, h2, h3, h4, h5, h6 { - font-family: Amiri, Georgia, "Times New Roman", serif; -} - -.display-4 { - font-size: 2.5rem; -} -@media (min-width: 768px) { - .display-4 { - font-size: 3rem; - } -} - -.nav-scroller { - position: relative; - z-index: 2; - height: 2.75rem; - overflow-y: hidden; -} - -.nav-scroller .nav { - display: flex; - flex-wrap: nowrap; - padding-bottom: 1rem; - margin-top: -1px; - overflow-x: auto; - text-align: center; - white-space: nowrap; - -webkit-overflow-scrolling: touch; -} - -.nav-scroller .nav-link { - padding-top: .75rem; - padding-bottom: .75rem; - font-size: .875rem; -} - -.card-img-right { - height: 100%; - border-radius: 3px 0 0 3px; -} - -.flex-auto { - flex: 0 0 auto; -} - -.h-250 { height: 250px; } -@media (min-width: 768px) { - .h-md-250 { height: 250px; } -} - -/* Pagination */ -.blog-pagination { - margin-bottom: 4rem; -} -.blog-pagination > .btn { - border-radius: 2rem; -} - -/* - * Blog posts - */ -.blog-post { - margin-bottom: 4rem; -} -.blog-post-title { - margin-bottom: .25rem; - font-size: 2.5rem; -} -.blog-post-meta { - margin-bottom: 1.25rem; - color: #727272; -} - -/* - * Footer - */ -.blog-footer { - padding: 2.5rem 0; - color: #727272; - text-align: center; - background-color: #f9f9f9; - border-top: .05rem solid #e5e5e5; -} -.blog-footer p:last-child { - margin-bottom: 0; -} diff --git a/site/content/docs/5.1/examples/checkout/form-validation.js b/site/content/docs/5.1/examples/checkout/form-validation.js deleted file mode 100644 index f8fd583de4b1..000000000000 --- a/site/content/docs/5.1/examples/checkout/form-validation.js +++ /dev/null @@ -1,20 +0,0 @@ -// Example starter JavaScript for disabling form submissions if there are invalid fields -(function () { - 'use strict' - - // Fetch all the forms we want to apply custom Bootstrap validation styles to - var forms = document.querySelectorAll('.needs-validation') - - // Loop over them and prevent submission - Array.prototype.slice.call(forms) - .forEach(function (form) { - form.addEventListener('submit', function (event) { - if (!form.checkValidity()) { - event.preventDefault() - event.stopPropagation() - } - - form.classList.add('was-validated') - }, false) - }) -})() diff --git a/site/content/docs/5.1/examples/dashboard-rtl/index.html b/site/content/docs/5.1/examples/dashboard-rtl/index.html deleted file mode 100644 index 19db4f3b4840..000000000000 --- a/site/content/docs/5.1/examples/dashboard-rtl/index.html +++ /dev/null @@ -1,253 +0,0 @@ ---- -layout: examples -title: قالب لوحة القيادة -direction: rtl -extra_css: - - "../dashboard/dashboard.rtl.css" -extra_js: - - src: "https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/feather.min.js" - integrity: "sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE" - - src: "https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js" - integrity: "sha384-zNy6FEbO50N+Cg5wap8IKA4M/ZnLJgzc6w2NqACZaK0u0FXfOWRRJOnQtpZun8ha" - - src: "dashboard.js" ---- - - - -
-
- - -
-
-

لوحة القيادة

-
-
- - -
- -
-
- - - -

عنوان القسم

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#عنوانعنوانعنوانعنوان
1,001بياناتعشوائيةتثريالجدول
1,002تثريمبهةتصميمتنسيق
1,003عشوائيةغنيةقيمةمفيدة
1,003معلوماتتثريتوضيحيةعشوائية
1,004الجدولبياناتتنسيققيمة
1,005قيمةمبهةالجدولتثري
1,006قيمةتوضيحيةغنيةعشوائية
1,007تثريمفيدةمعلوماتمبهة
1,008بياناتعشوائيةتثريالجدول
1,009تثريمبهةتصميمتنسيق
1,010عشوائيةغنيةقيمةمفيدة
1,011معلوماتتثريتوضيحيةعشوائية
1,012الجدولتثريتنسيققيمة
1,013قيمةمبهةالجدولتصميم
1,014قيمةتوضيحيةغنيةعشوائية
1,015بياناتمفيدةمعلوماتالجدول
-
-
-
-
diff --git a/site/content/docs/5.1/examples/dashboard/dashboard.css b/site/content/docs/5.1/examples/dashboard/dashboard.css deleted file mode 100644 index e1099fbb310c..000000000000 --- a/site/content/docs/5.1/examples/dashboard/dashboard.css +++ /dev/null @@ -1,100 +0,0 @@ -body { - font-size: .875rem; -} - -.feather { - width: 16px; - height: 16px; - vertical-align: text-bottom; -} - -/* - * Sidebar - */ - -.sidebar { - position: fixed; - top: 0; - /* rtl:raw: - right: 0; - */ - bottom: 0; - /* rtl:remove */ - left: 0; - z-index: 100; /* Behind the navbar */ - padding: 48px 0 0; /* Height of navbar */ - box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1); -} - -@media (max-width: 767.98px) { - .sidebar { - top: 5rem; - } -} - -.sidebar-sticky { - position: relative; - top: 0; - height: calc(100vh - 48px); - padding-top: .5rem; - overflow-x: hidden; - overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ -} - -.sidebar .nav-link { - font-weight: 500; - color: #333; -} - -.sidebar .nav-link .feather { - margin-right: 4px; - color: #727272; -} - -.sidebar .nav-link.active { - color: #2470dc; -} - -.sidebar .nav-link:hover .feather, -.sidebar .nav-link.active .feather { - color: inherit; -} - -.sidebar-heading { - font-size: .75rem; - text-transform: uppercase; -} - -/* - * Navbar - */ - -.navbar-brand { - padding-top: .75rem; - padding-bottom: .75rem; - font-size: 1rem; - background-color: rgba(0, 0, 0, .25); - box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25); -} - -.navbar .navbar-toggler { - top: .25rem; - right: 1rem; -} - -.navbar .form-control { - padding: .75rem 1rem; - border-width: 0; - border-radius: 0; -} - -.form-control-dark { - color: #fff; - background-color: rgba(255, 255, 255, .1); - border-color: rgba(255, 255, 255, .1); -} - -.form-control-dark:focus { - border-color: transparent; - box-shadow: 0 0 0 3px rgba(255, 255, 255, .25); -} diff --git a/site/content/docs/5.1/examples/dashboard/dashboard.rtl.css b/site/content/docs/5.1/examples/dashboard/dashboard.rtl.css deleted file mode 100644 index a88226ecfdd5..000000000000 --- a/site/content/docs/5.1/examples/dashboard/dashboard.rtl.css +++ /dev/null @@ -1,96 +0,0 @@ -body { - font-size: .875rem; -} - -.feather { - width: 16px; - height: 16px; - vertical-align: text-bottom; -} - -/* - * Sidebar - */ - -.sidebar { - position: fixed; - top: 0; - right: 0; - bottom: 0; - z-index: 100; /* Behind the navbar */ - padding: 48px 0 0; /* Height of navbar */ - box-shadow: inset 1px 0 0 rgba(0, 0, 0, .1); -} - -@media (max-width: 767.98px) { - .sidebar { - top: 5rem; - } -} - -.sidebar-sticky { - position: relative; - top: 0; - height: calc(100vh - 48px); - padding-top: .5rem; - overflow-x: hidden; - overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ -} - -.sidebar .nav-link { - font-weight: 500; - color: #333; -} - -.sidebar .nav-link .feather { - margin-left: 4px; - color: #727272; -} - -.sidebar .nav-link.active { - color: #2470dc; -} - -.sidebar .nav-link:hover .feather, -.sidebar .nav-link.active .feather { - color: inherit; -} - -.sidebar-heading { - font-size: .75rem; - text-transform: uppercase; -} - -/* - * Navbar - */ - -.navbar-brand { - padding-top: .75rem; - padding-bottom: .75rem; - font-size: 1rem; - background-color: rgba(0, 0, 0, .25); - box-shadow: inset 1px 0 0 rgba(0, 0, 0, .25); -} - -.navbar .navbar-toggler { - top: .25rem; - left: 1rem; -} - -.navbar .form-control { - padding: .75rem 1rem; - border-width: 0; - border-radius: 0; -} - -.form-control-dark { - color: #fff; - background-color: rgba(255, 255, 255, .1); - border-color: rgba(255, 255, 255, .1); -} - -.form-control-dark:focus { - border-color: transparent; - box-shadow: 0 0 0 3px rgba(255, 255, 255, .25); -} diff --git a/site/content/docs/5.1/examples/dashboard/index.html b/site/content/docs/5.1/examples/dashboard/index.html deleted file mode 100644 index 04c187dbc102..000000000000 --- a/site/content/docs/5.1/examples/dashboard/index.html +++ /dev/null @@ -1,252 +0,0 @@ ---- -layout: examples -title: Dashboard Template -extra_css: - - "dashboard.css" -extra_js: - - src: "https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/feather.min.js" - integrity: "sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE" - - src: "https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js" - integrity: "sha384-zNy6FEbO50N+Cg5wap8IKA4M/ZnLJgzc6w2NqACZaK0u0FXfOWRRJOnQtpZun8ha" - - src: "dashboard.js" ---- - - - -
-
- - -
-
-

Dashboard

-
-
- - -
- -
-
- - - -

Section title

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#HeaderHeaderHeaderHeader
1,001randomdataplaceholdertext
1,002placeholderirrelevantvisuallayout
1,003datarichdashboardtabular
1,003informationplaceholderillustrativedata
1,004textrandomlayoutdashboard
1,005dashboardirrelevanttextplaceholder
1,006dashboardillustrativerichdata
1,007placeholdertabularinformationirrelevant
1,008randomdataplaceholdertext
1,009placeholderirrelevantvisuallayout
1,010datarichdashboardtabular
1,011informationplaceholderillustrativedata
1,012textplaceholderlayoutdashboard
1,013dashboardirrelevanttextvisual
1,014dashboardillustrativerichdata
1,015randomtabularinformationtext
-
-
-
-
diff --git a/site/content/docs/5.1/examples/dropdowns/dropdowns.css b/site/content/docs/5.1/examples/dropdowns/dropdowns.css deleted file mode 100644 index 4341c59ea428..000000000000 --- a/site/content/docs/5.1/examples/dropdowns/dropdowns.css +++ /dev/null @@ -1,89 +0,0 @@ -.b-example-divider { - height: 3rem; - background-color: rgba(0, 0, 0, .1); - border: solid rgba(0, 0, 0, .15); - border-width: 1px 0; - box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15); -} - -.bi { - vertical-align: -.125em; - fill: currentColor; -} - -.dropdown-menu { - position: static; - display: block; - width: auto; - margin: 4rem auto; -} - -.dropdown-menu-macos { - display: grid; - gap: .25rem; - padding: .5rem; - border-radius: .5rem; -} -.dropdown-menu-macos .dropdown-item { - border-radius: .25rem; -} - -.dropdown-item-danger { - color: var(--bs-red); -} -.dropdown-item-danger:hover, -.dropdown-item-danger:focus { - color: #fff; - background-color: var(--bs-red); -} -.dropdown-item-danger.active { - background-color: var(--bs-red); -} - -.btn-hover-light { - text-align: left; - background-color: var(--bs-white); - border-radius: .25rem; -} -.btn-hover-light:hover, -.btn-hover-light:focus { - color: var(--bs-blue); - background-color: var(--bs-light); -} - -.cal-month, -.cal-days, -.cal-weekdays { - display: grid; - grid-template-columns: repeat(7, 1fr); - align-items: center; -} -.cal-month-name { - grid-column-start: 2; - grid-column-end: 7; - text-align: center; -} -.cal-weekday, -.cal-btn { - display: flex; - flex-shrink: 0; - align-items: center; - justify-content: center; - height: 3rem; - padding: 0; -} -.cal-btn:not([disabled]) { - font-weight: 500; -} -.cal-btn:hover, -.cal-btn:focus { - background-color: rgba(0, 0, 0, .05); -} -.cal-btn[disabled] { - opacity: .5; -} - -.form-control-dark { - background-color: rgba(255, 255, 255, .05); - border-color: rgba(255, 255, 255, .15); -} diff --git a/site/content/docs/5.1/examples/dropdowns/index.html b/site/content/docs/5.1/examples/dropdowns/index.html deleted file mode 100644 index 5296d1507ef1..000000000000 --- a/site/content/docs/5.1/examples/dropdowns/index.html +++ /dev/null @@ -1,339 +0,0 @@ ---- -layout: examples -title: Dropdowns -extra_css: - - "dropdowns.css" -body_class: "" ---- - - - - Bootstrap - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - -
- - - -
- - \ No newline at end of file diff --git a/site/content/docs/5.1/examples/features/features.css b/site/content/docs/5.1/examples/features/features.css deleted file mode 100644 index 33942f7f13ff..000000000000 --- a/site/content/docs/5.1/examples/features/features.css +++ /dev/null @@ -1,61 +0,0 @@ -.b-example-divider { - height: 3rem; - background-color: rgba(0, 0, 0, .1); - border: solid rgba(0, 0, 0, .15); - border-width: 1px 0; - box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15); -} - -.bi { - vertical-align: -.125em; - fill: currentColor; -} - -.feature-icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 4rem; - height: 4rem; - margin-bottom: 1rem; - font-size: 2rem; - color: #fff; - border-radius: .75rem; -} - -.icon-link { - display: inline-flex; - align-items: center; -} -.icon-link > .bi { - margin-top: .125rem; - margin-left: .125rem; - transition: transform .25s ease-in-out; - fill: currentColor; -} -.icon-link:hover > .bi { - transform: translate(.25rem); -} - -.icon-square { - display: inline-flex; - align-items: center; - justify-content: center; - width: 3rem; - height: 3rem; - font-size: 1.5rem; - border-radius: .75rem; -} - -.rounded-4 { border-radius: .5rem; } -.rounded-5 { border-radius: 1rem; } - -.text-shadow-1 { text-shadow: 0 .125rem .25rem rgba(0, 0, 0, .25); } -.text-shadow-2 { text-shadow: 0 .25rem .5rem rgba(0, 0, 0, .25); } -.text-shadow-3 { text-shadow: 0 .5rem 1.5rem rgba(0, 0, 0, .25); } - -.card-cover { - background-repeat: no-repeat; - background-position: center center; - background-size: cover; -} diff --git a/site/content/docs/5.1/examples/footers/footers.css b/site/content/docs/5.1/examples/footers/footers.css deleted file mode 100644 index 4e826827ecb0..000000000000 --- a/site/content/docs/5.1/examples/footers/footers.css +++ /dev/null @@ -1,12 +0,0 @@ -.b-example-divider { - height: 3rem; - background-color: rgba(0, 0, 0, .1); - border: solid rgba(0, 0, 0, .15); - border-width: 1px 0; - box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15); -} - -.bi { - vertical-align: -.125em; - fill: currentColor; -} diff --git a/site/content/docs/5.1/examples/grid/grid.css b/site/content/docs/5.1/examples/grid/grid.css deleted file mode 100644 index 18e3568b1bbf..000000000000 --- a/site/content/docs/5.1/examples/grid/grid.css +++ /dev/null @@ -1,13 +0,0 @@ -.themed-grid-col { - padding-top: .75rem; - padding-bottom: .75rem; - background-color: rgba(86, 61, 124, .15); - border: 1px solid rgba(86, 61, 124, .2); -} - -.themed-container { - padding: .75rem; - margin-bottom: 1.5rem; - background-color: rgba(0, 123, 255, .15); - border: 1px solid rgba(0, 123, 255, .2); -} diff --git a/site/content/docs/5.1/examples/headers/headers.css b/site/content/docs/5.1/examples/headers/headers.css deleted file mode 100644 index 661a74d55a05..000000000000 --- a/site/content/docs/5.1/examples/headers/headers.css +++ /dev/null @@ -1,32 +0,0 @@ -.b-example-divider { - height: 3rem; - background-color: rgba(0, 0, 0, .1); - border: solid rgba(0, 0, 0, .15); - border-width: 1px 0; - box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15); -} - -.form-control-dark { - color: #fff; - background-color: var(--bs-dark); - border-color: var(--bs-gray); -} -.form-control-dark:focus { - color: #fff; - background-color: var(--bs-dark); - border-color: #fff; - box-shadow: 0 0 0 .25rem rgba(255, 255, 255, .25); -} - -.bi { - vertical-align: -.125em; - fill: currentColor; -} - -.text-small { - font-size: 85%; -} - -.dropdown-toggle { - outline: 0; -} diff --git a/site/content/docs/5.1/examples/heroes/bootstrap-docs.png b/site/content/docs/5.1/examples/heroes/bootstrap-docs.png deleted file mode 100644 index 471a9eddfe57..000000000000 Binary files a/site/content/docs/5.1/examples/heroes/bootstrap-docs.png and /dev/null differ diff --git a/site/content/docs/5.1/examples/heroes/heroes.css b/site/content/docs/5.1/examples/heroes/heroes.css deleted file mode 100644 index 380b70a4a788..000000000000 --- a/site/content/docs/5.1/examples/heroes/heroes.css +++ /dev/null @@ -1,11 +0,0 @@ -.b-example-divider { - height: 3rem; - background-color: rgba(0, 0, 0, .1); - border: solid rgba(0, 0, 0, .15); - border-width: 1px 0; - box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15); -} - -@media (min-width: 992px) { - .rounded-lg-3 { border-radius: .3rem; } -} diff --git a/site/content/docs/5.1/examples/list-groups/index.html b/site/content/docs/5.1/examples/list-groups/index.html deleted file mode 100644 index c16bad944d15..000000000000 --- a/site/content/docs/5.1/examples/list-groups/index.html +++ /dev/null @@ -1,186 +0,0 @@ ---- -layout: examples -title: List groups -extra_css: - - "list-groups.css" -body_class: "" ---- - - - - Bootstrap - - - - - - - - - - - - - - - - - - - - -
- -
-
- - - -
- -
- - - -
-
- -
- -
- - - - -
- -
- -
- - - - - - - - - - - -
\ No newline at end of file diff --git a/site/content/docs/5.1/examples/list-groups/list-groups.css b/site/content/docs/5.1/examples/list-groups/list-groups.css deleted file mode 100644 index 11351f87e2e0..000000000000 --- a/site/content/docs/5.1/examples/list-groups/list-groups.css +++ /dev/null @@ -1,61 +0,0 @@ -.b-example-divider { - height: 3rem; - background-color: rgba(0, 0, 0, .1); - border: solid rgba(0, 0, 0, .15); - border-width: 1px 0; - box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15); -} - -.bi { - vertical-align: -.125em; - fill: currentColor; -} - -.opacity-50 { opacity: .5; } -.opacity-75 { opacity: .75; } - -.list-group { - width: auto; - max-width: 460px; - margin: 4rem auto; -} - -.form-check-input:checked + .form-checked-content { - opacity: .5; -} - -.form-check-input-placeholder { - pointer-events: none; - border-style: dashed; -} -[contenteditable]:focus { - outline: 0; -} - -.list-group-checkable { - display: grid; - gap: .5rem; - border: 0; -} -.list-group-checkable .list-group-item { - cursor: pointer; - border-radius: .5rem; -} -.list-group-item-check { - position: absolute; - clip: rect(0, 0, 0, 0); - pointer-events: none; -} -.list-group-item-check:hover + .list-group-item { - background-color: var(--bs-light); -} -.list-group-item-check:checked + .list-group-item { - color: #fff; - background-color: var(--bs-blue); -} -.list-group-item-check[disabled] + .list-group-item, -.list-group-item-check:disabled + .list-group-item { - pointer-events: none; - filter: none; - opacity: .5; -} diff --git a/site/content/docs/5.1/examples/modals/index.html b/site/content/docs/5.1/examples/modals/index.html deleted file mode 100644 index cc0feff873be..000000000000 --- a/site/content/docs/5.1/examples/modals/index.html +++ /dev/null @@ -1,173 +0,0 @@ ---- -layout: examples -title: Modals -extra_css: - - "modals.css" -body_class: "" ---- - - - - Bootstrap - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - -
- - - -
diff --git a/site/content/docs/5.1/examples/modals/modals.css b/site/content/docs/5.1/examples/modals/modals.css deleted file mode 100644 index 8fda8212abc8..000000000000 --- a/site/content/docs/5.1/examples/modals/modals.css +++ /dev/null @@ -1,34 +0,0 @@ -.b-example-divider { - height: 3rem; - background-color: rgba(0, 0, 0, .1); - border: solid rgba(0, 0, 0, .15); - border-width: 1px 0; - box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15); -} - -.bi { - vertical-align: -.125em; - fill: currentColor; -} - -.rounded-4 { border-radius: .5rem; } -.rounded-5 { border-radius: .75rem; } -.rounded-6 { border-radius: 1rem; } - -.modal-sheet .modal-dialog { - width: 380px; - transition: bottom .75s ease-in-out; -} -.modal-sheet .modal-footer { - padding-bottom: 2rem; -} - -.modal-alert .modal-dialog { - width: 380px; -} - -.border-right { border-right: 1px solid #eee; } - -.modal-tour .modal-dialog { - width: 380px; -} diff --git a/site/content/docs/5.1/examples/pricing/pricing.css b/site/content/docs/5.1/examples/pricing/pricing.css deleted file mode 100644 index c7304d10be4e..000000000000 --- a/site/content/docs/5.1/examples/pricing/pricing.css +++ /dev/null @@ -1,11 +0,0 @@ -body { - background-image: linear-gradient(180deg, #eee, #fff 100px, #fff); -} - -.container { - max-width: 960px; -} - -.pricing-header { - max-width: 700px; -} diff --git a/site/content/docs/5.1/examples/product/index.html b/site/content/docs/5.1/examples/product/index.html deleted file mode 100644 index 291901efa269..000000000000 --- a/site/content/docs/5.1/examples/product/index.html +++ /dev/null @@ -1,148 +0,0 @@ ---- -layout: examples -title: Product example -extra_css: - - "product.css" ---- - - - -
-
-
-

Punny headline

-

And an even wittier subheading to boot. Jumpstart your marketing efforts with this example based on Apple’s marketing pages.

- Coming soon -
-
-
-
- -
-
-
-

Another headline

-

And an even wittier subheading.

-
-
-
-
-
-

Another headline

-

And an even wittier subheading.

-
-
-
-
- -
-
-
-

Another headline

-

And an even wittier subheading.

-
-
-
-
-
-

Another headline

-

And an even wittier subheading.

-
-
-
-
- -
-
-
-

Another headline

-

And an even wittier subheading.

-
-
-
-
-
-

Another headline

-

And an even wittier subheading.

-
-
-
-
- -
-
-
-

Another headline

-

And an even wittier subheading.

-
-
-
-
-
-

Another headline

-

And an even wittier subheading.

-
-
-
-
-
- - diff --git a/site/content/docs/5.1/examples/sidebars/index.html b/site/content/docs/5.1/examples/sidebars/index.html deleted file mode 100644 index 4d628f1c06eb..000000000000 --- a/site/content/docs/5.1/examples/sidebars/index.html +++ /dev/null @@ -1,391 +0,0 @@ ---- -layout: examples -title: Sidebars -extra_css: - - "sidebars.css" -extra_js: - - src: "sidebars.js" -body_class: "" ---- - - - - Bootstrap - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

Sidebars examples

- - - -
- - - -
- -
- - - Icon-only - - - -
- -
- -
- - - Collapsible - - -
- -
- - - -
-
diff --git a/site/content/docs/5.1/examples/sidebars/sidebars.js b/site/content/docs/5.1/examples/sidebars/sidebars.js deleted file mode 100644 index 68384c1633e8..000000000000 --- a/site/content/docs/5.1/examples/sidebars/sidebars.js +++ /dev/null @@ -1,8 +0,0 @@ -/* global bootstrap: false */ -(function () { - 'use strict' - var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) - tooltipTriggerList.forEach(function (tooltipTriggerEl) { - new bootstrap.Tooltip(tooltipTriggerEl) - }) -})() diff --git a/site/content/docs/5.1/examples/sign-in/index.html b/site/content/docs/5.1/examples/sign-in/index.html deleted file mode 100644 index fb885ba41437..000000000000 --- a/site/content/docs/5.1/examples/sign-in/index.html +++ /dev/null @@ -1,32 +0,0 @@ ---- -layout: examples -title: Signin Template -extra_css: - - "signin.css" -body_class: "text-center" -include_js: false ---- - -
-
- -

Please sign in

- -
- - -
-
- - -
- -
- -
- -

© 2017–{{< year >}}

-
-
diff --git a/site/content/docs/5.1/examples/starter-template/index.html b/site/content/docs/5.1/examples/starter-template/index.html deleted file mode 100644 index 3623ff180da1..000000000000 --- a/site/content/docs/5.1/examples/starter-template/index.html +++ /dev/null @@ -1,51 +0,0 @@ ---- -layout: examples -title: Starter Template -extra_css: - - "starter-template.css" ---- - -
-
- - Bootstrap - Starter template - -
- -
-

Get started with Bootstrap

-

Quickly and easily get started with Bootstrap's compiled, production-ready files with this barebones example featuring some basic HTML and helpful links. Download all our examples to get started.

- - - -
- -
-
-

Starter projects

-

Ready to beyond the starter template? Check out these open source projects that you can quickly duplicate to a new GitHub repository.

- -
- -
-

Guides

-

Read more detailed instructions and documentation on using or contributing to Bootstrap.

- -
-
-
-
- Created by the Bootstrap team · © {{< year >}} -
-
diff --git a/site/content/docs/5.1/examples/starter-template/starter-template.css b/site/content/docs/5.1/examples/starter-template/starter-template.css deleted file mode 100644 index d03436db03be..000000000000 --- a/site/content/docs/5.1/examples/starter-template/starter-template.css +++ /dev/null @@ -1,18 +0,0 @@ -.icon-list { - padding-left: 0; - list-style: none; -} -.icon-list li { - display: flex; - align-items: flex-start; - margin-bottom: .25rem; -} -.icon-list li::before { - display: block; - flex-shrink: 0; - width: 1.5em; - height: 1.5em; - margin-right: .5rem; - content: ""; - background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23212529' viewBox='0 0 16 16'%3E%3Cpath d='M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0zM4.5 7.5a.5.5 0 0 0 0 1h5.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3a.5.5 0 0 0 0-.708l-3-3a.5.5 0 1 0-.708.708L10.293 7.5H4.5z'/%3E%3C/svg%3E") no-repeat center center / 100% auto; -} diff --git a/site/content/docs/5.1/examples/sticky-footer/index.html b/site/content/docs/5.1/examples/sticky-footer/index.html deleted file mode 100644 index 7a6e127cd621..000000000000 --- a/site/content/docs/5.1/examples/sticky-footer/index.html +++ /dev/null @@ -1,24 +0,0 @@ ---- -layout: examples -title: Sticky Footer Template -extra_css: - - "sticky-footer.css" -html_class: "h-100" -body_class: "d-flex flex-column h-100" -include_js: false ---- - - -
-
-

Sticky footer

-

Pin a footer to the bottom of the viewport in desktop browsers with this custom HTML and CSS.

-

Use }}">the sticky footer with a fixed navbar if need be, too.

-
-
- -
-
- Place sticky footer content here. -
-
diff --git a/site/content/docs/5.1/extend/icons.md b/site/content/docs/5.1/extend/icons.md deleted file mode 100644 index 1e26503bdb79..000000000000 --- a/site/content/docs/5.1/extend/icons.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -layout: docs -title: Icons -description: Guidance and suggestions for using external icon libraries with Bootstrap. -group: extend ---- - -While Bootstrap doesn't include an icon set by default, we do have our own comprehensive icon library called Bootstrap Icons. Feel free to use them or any other icon set in your project. We've included details for Bootstrap Icons and other preferred icon sets below. - -While most icon sets include multiple file formats, we prefer SVG implementations for their improved accessibility and vector support. - -## Bootstrap Icons - -Bootstrap Icons is a growing library of SVG icons that are designed by [@mdo](https://github.com/mdo) and maintained by [the Bootstrap Team](https://github.com/orgs/twbs/people). The beginnings of this icon set come from Bootstrap's very own components—our forms, carousels, and more. Bootstrap has very few icon needs out of the box, so we didn't need much. However, once we got going, we couldn't stop making more. - -Oh, and did we mention they're completely open source? Licensed under MIT, just like Bootstrap, our icon set is available to everyone. - -[Learn more about Bootstrap Icons]({{< param icons >}}), including how to install them and recommended usage. - -## Alternatives - -We've tested and used these icon sets ourselves as preferred alternatives to Bootstrap Icons. - -{{< markdown >}} -{{< icons.inline >}} -{{- $type := .Get "type" | default "preferred" -}} - -{{- range (index .Site.Data.icons $type) }} -- [{{ .name }}]({{ .website }}) -{{- end }} -{{< /icons.inline >}} -{{< /markdown >}} - -## More options - -While we haven't tried these out ourselves, they do look promising and provide multiple formats, including SVG. - -{{< markdown >}} -{{< icons.inline type="more" />}} -{{< /markdown >}} diff --git a/site/content/docs/5.1/forms/checks-radios.md b/site/content/docs/5.1/forms/checks-radios.md deleted file mode 100644 index 0f5507f54bdb..000000000000 --- a/site/content/docs/5.1/forms/checks-radios.md +++ /dev/null @@ -1,277 +0,0 @@ ---- -layout: docs -title: Checks and radios -description: Create consistent cross-browser and cross-device checkboxes and radios with our completely rewritten checks component. -group: forms -aliases: "/docs/5.1/forms/checks/" -toc: true ---- - -## Approach - -Browser default checkboxes and radios are replaced with the help of `.form-check`, a series of classes for both input types that improves the layout and behavior of their HTML elements, that provide greater customization and cross browser consistency. Checkboxes are for selecting one or several options in a list, while radios are for selecting one option from many. - -Structurally, our ``s and `
-{{< /example >}} - -To set a custom height on your ` - -
-{{< /example >}} - -## Selects - -Other than `.form-control`, floating labels are only available on `.form-select`s. They work in the same way, but unlike ``s, they'll always show the `
-{{< /example >}} - -## Sizing - -Set heights using classes like `.form-control-lg` and `.form-control-sm`. - -{{< example >}} - - - -{{< /example >}} - -## Disabled - -Add the `disabled` boolean attribute on an input to give it a grayed out appearance and remove pointer events. - -{{< example >}} - - -{{< /example >}} - -## Readonly - -Add the `readonly` boolean attribute on an input to prevent modification of the input's value. - -{{< example >}} - -{{< /example >}} - -## Readonly plain text - -If you want to have `` elements in your form styled as plain text, use the `.form-control-plaintext` class to remove the default form field styling and preserve the correct margin and padding. - -{{< example >}} -
- -
- -
-
-
- -
- -
-
-{{< /example >}} - -{{< example >}} -
-
- - -
-
- - -
-
- -
-
-{{< /example >}} - -## File input - -{{< example >}} -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-{{< /example >}} - -## Color - -{{< example >}} - - -{{< /example >}} - -## Datalists - -Datalists allow you to create a group of `
- -{{< /example >}} - -## Horizontal form - -Create horizontal forms with the grid by adding the `.row` class to form groups and using the `.col-*-*` classes to specify the width of your labels and controls. Be sure to add `.col-form-label` to your `
-
- - -
-
-
- - -
-
- - - -{{< /example >}} - -## Accessibility - -Ensure that all form controls have an appropriate accessible name so that their purpose can be conveyed to users of assistive technologies. The simplest way to achieve this is to use a `
- -
- - -
Example invalid feedback text
-
- -
- - -
-
- - -
More example invalid feedback text
-
- -
- -
Example invalid select feedback
-
- -
- -
Example invalid form file feedback
-
- -
- -
- -{{< /example >}} - -## Tooltips - -If your form layout allows it, you can swap the `.{valid|invalid}-feedback` classes for `.{valid|invalid}-tooltip` classes to display validation feedback in a styled tooltip. Be sure to have a parent with `position: relative` on it for tooltip positioning. In the example below, our column classes have this already, but your project may require an alternative setup. - -{{< example >}} -
-
- - -
- Looks good! -
-
-
- - -
- Looks good! -
-
-
- -
- @ - -
- Please choose a unique and valid username. -
-
-
-
- - -
- Please provide a valid city. -
-
-
- - -
- Please select a valid state. -
-
-
- - -
- Please provide a valid zip. -
-
-
- -
-
-{{< /example >}} - -## Sass - -### Variables - -{{< scss-docs name="form-feedback-variables" file="scss/_variables.scss" >}} - -### Mixins - -Two mixins are combined together, through our [loop](#loop), to generate our form validation feedback styles. - -{{< scss-docs name="form-validation-mixins" file="scss/mixins/_forms.scss" >}} - -### Map - -This is the validation Sass map from `_variables.scss`. Override or extend this to generate different or additional states. - -{{< scss-docs name="form-validation-states" file="scss/_variables.scss" >}} - -Maps of `$form-validation-states` can contain three optional parameters to override tooltips and focus styles. - -### Loop - -Used to iterate over `$form-validation-states` map values to generate our validation styles. Any modifications to the above Sass map will be reflected in your compiled CSS via this loop. - -{{< scss-docs name="form-validation-states-loop" file="scss/forms/_validation.scss" >}} - -### Customizing - -Validation states can be customized via Sass with the `$form-validation-states` map. Located in our `_variables.scss` file, this Sass map is how we generate the default `valid`/`invalid` validation states. Included is a nested map for customizing each state's color, icon, tooltip color, and focus shadow. While no other states are supported by browsers, those using custom styles can easily add more complex form feedback. - -Please note that **we do not recommend customizing `$form-validation-states` values without also modifying the `form-validation-state` mixin**. diff --git a/site/content/docs/5.1/getting-started/accessibility.md b/site/content/docs/5.1/getting-started/accessibility.md deleted file mode 100644 index e9f1cb31652d..000000000000 --- a/site/content/docs/5.1/getting-started/accessibility.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -layout: docs -title: Accessibility -description: A brief overview of Bootstrap's features and limitations for the creation of accessible content. -group: getting-started -toc: true ---- - -Bootstrap provides an easy-to-use framework of ready-made styles, layout tools, and interactive components, allowing developers to create websites and applications that are visually appealing, functionally rich, and accessible out of the box. - -## Overview and limitations - -The overall accessibility of any project built with Bootstrap depends in large part on the author's markup, additional styling, and scripting they've included. However, provided that these have been implemented correctly, it should be perfectly possible to create websites and applications with Bootstrap that fulfill [WCAG 2.1](https://www.w3.org/TR/WCAG/) (A/AA/AAA), [Section 508](https://www.section508.gov/), and similar accessibility standards and requirements. - -### Structural markup - -Bootstrap's styling and layout can be applied to a wide range of markup structures. This documentation aims to provide developers with best practice examples to demonstrate the use of Bootstrap itself and illustrate appropriate semantic markup, including ways in which potential accessibility concerns can be addressed. - -### Interactive components - -Bootstrap's interactive components—such as modal dialogs, dropdown menus, and custom tooltips—are designed to work for touch, mouse, and keyboard users. Through the use of relevant [WAI-ARIA](https://www.w3.org/WAI/standards-guidelines/aria/) roles and attributes, these components should also be understandable and operable using assistive technologies (such as screen readers). - -Because Bootstrap's components are purposely designed to be fairly generic, authors may need to include further ARIA roles and attributes, as well as JavaScript behavior, to more accurately convey the precise nature and functionality of their component. This is usually noted in the documentation. - -### Color contrast - -Some combinations of colors that currently make up Bootstrap's default palette—used throughout the framework for things such as button variations, alert variations, form validation indicators—may lead to *insufficient* color contrast (below the recommended [WCAG 2.1 text color contrast ratio of 4.5:1](https://www.w3.org/TR/WCAG/#contrast-minimum) and the [WCAG 2.1 non-text color contrast ratio of 3:1](https://www.w3.org/TR/WCAG/#non-text-contrast)), particularly when used against a light background. Authors are encouraged to test their specific uses of color and, where necessary, manually modify/extend these default colors to ensure adequate color contrast ratios. - -### Visually hidden content - -Content which should be visually hidden, but remain accessible to assistive technologies such as screen readers, can be styled using the `.visually-hidden` class. This can be useful in situations where additional visual information or cues (such as meaning denoted through the use of color) need to also be conveyed to non-visual users. - -```html -

- Danger: - This action is not reversible -

-``` - -For visually hidden interactive controls, such as traditional "skip" links, use the `.visually-hidden-focusable` class. This will ensure that the control becomes visible once focused (for sighted keyboard users). **Watch out, compared to the equivalent `.sr-only` and `.sr-only-focusable` classes in past versions, Bootstrap 5's `.visually-hidden-focusable` is a standalone class, and must not be used in combination with the `.visually-hidden` class.** - -```html -Skip to main content -``` - -### Reduced motion - -Bootstrap includes support for the [`prefers-reduced-motion` media feature](https://www.w3.org/TR/mediaqueries-5/#prefers-reduced-motion). In browsers/environments that allow the user to specify their preference for reduced motion, most CSS transition effects in Bootstrap (for instance, when a modal dialog is opened or closed, or the sliding animation in carousels) will be disabled, and meaningful animations (such as spinners) will be slowed down. - -On browsers that support `prefers-reduced-motion`, and where the user has *not* explicitly signaled that they'd prefer reduced motion (i.e. where `prefers-reduced-motion: no-preference`), Bootstrap enables smooth scrolling using the `scroll-behavior` property. - -## Additional resources - -- [Web Content Accessibility Guidelines (WCAG) 2.1](https://www.w3.org/TR/WCAG/) -- [The A11Y Project](https://www.a11yproject.com/) -- [MDN accessibility documentation](https://developer.mozilla.org/en-US/docs/Web/Accessibility) -- [Tenon.io Accessibility Checker](https://tenon.io/) -- [Color Contrast Analyser (CCA)](https://www.tpgi.com/color-contrast-checker/) -- ["HTML Codesniffer" bookmarklet for identifying accessibility issues](https://github.com/squizlabs/HTML_CodeSniffer) -- [Microsoft Accessibility Insights](https://accessibilityinsights.io/) -- [Deque Axe testing tools](https://www.deque.com/axe/) -- [Introduction to Web Accessibility](https://www.w3.org/WAI/fundamentals/accessibility-intro/) diff --git a/site/content/docs/5.1/getting-started/best-practices.md b/site/content/docs/5.1/getting-started/best-practices.md deleted file mode 100644 index e17fc1290a4d..000000000000 --- a/site/content/docs/5.1/getting-started/best-practices.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -layout: docs -title: Best practices -description: Learn about some of the best practices we've gathered from years of working on and using Bootstrap. -group: getting-started ---- - -We've designed and developed Bootstrap to work in a number of environments. Here are some of the best practices we've gathered from years of working on and using it ourselves. - -{{< callout info >}} -**Heads up!** This copy is a work in progress. -{{< /callout >}} - -### General outline - -- Working with CSS -- Working with Sass files -- Building new CSS components -- Working with flexbox -- Ask in [Slack](https://bootstrap-slack.herokuapp.com/) diff --git a/site/content/docs/5.1/getting-started/browsers-devices.md b/site/content/docs/5.1/getting-started/browsers-devices.md deleted file mode 100644 index bdda154a309a..000000000000 --- a/site/content/docs/5.1/getting-started/browsers-devices.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -layout: docs -title: Browsers and devices -description: Learn about the browsers and devices, from modern to old, that are supported by Bootstrap, including known quirks and bugs for each. -group: getting-started -toc: true ---- - -## Supported browsers - -Bootstrap supports the **latest, stable releases** of all major browsers and platforms. - -Alternative browsers which use the latest version of WebKit, Blink, or Gecko, whether directly or via the platform's web view API, are not explicitly supported. However, Bootstrap should (in most cases) display and function correctly in these browsers as well. More specific support information is provided below. - -You can find our supported range of browsers and their versions [in our `.browserslistrc file`]({{< param repo >}}/blob/v{{< param current_version >}}/.browserslistrc): - -```text -{{< rf.inline >}} -{{- readFile ".browserslistrc" | chomp | htmlEscape -}} -{{< /rf.inline >}} -``` - -We use [Autoprefixer](https://github.com/postcss/autoprefixer) to handle intended browser support via CSS prefixes, which uses [Browserslist](https://github.com/browserslist/browserslist) to manage these browser versions. Consult their documentation for how to integrate these tools into your projects. - -### Mobile devices - -Generally speaking, Bootstrap supports the latest versions of each major platform's default browsers. Note that proxy browsers (such as Opera Mini, Opera Mobile's Turbo mode, UC Browser Mini, Amazon Silk) are not supported. - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ChromeFirefoxSafariAndroid Browser & WebView
AndroidSupportedSupportedv6.0+
iOSSupportedSupportedSupported
- -### Desktop browsers - -Similarly, the latest versions of most desktop browsers are supported. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ChromeFirefoxMicrosoft EdgeOperaSafari
MacSupportedSupportedSupportedSupportedSupported
WindowsSupportedSupportedSupportedSupported
- -For Firefox, in addition to the latest normal stable release, we also support the latest [Extended Support Release (ESR)](https://www.mozilla.org/en-US/firefox/enterprise/) version of Firefox. - -Unofficially, Bootstrap should look and behave well enough in Chromium and Chrome for Linux, and Firefox for Linux, though they are not officially supported. - -## Internet Explorer - -Internet Explorer is not supported. **If you require Internet Explorer support, please use Bootstrap v4.** - -## Modals and dropdowns on mobile - -### Overflow and scrolling - -Support for `overflow: hidden;` on the `` element is quite limited in iOS and Android. To that end, when you scroll past the top or bottom of a modal in either of those devices' browsers, the `` content will begin to scroll. See [Chrome bug #175502](https://bugs.chromium.org/p/chromium/issues/detail?id=175502) (fixed in Chrome v40) and [WebKit bug #153852](https://bugs.webkit.org/show_bug.cgi?id=153852). - -### iOS text fields and scrolling - -As of iOS 9.2, while a modal is open, if the initial touch of a scroll gesture is within the boundary of a textual `` or a `
- {{< /example >}} + `} /> @@ -603,113 +617,107 @@

العناصر

- {{< example show_markup="false" >}} +
-

+

-
+
- هذا هو محتوى عنصر المطوية الأول. سيكون المحتوى مخفيًا بشكل إفتراضي حتى يقوم Bootstrap بإضافة الكلاسات اللازمة لكل عنصر في المطوية. هذه الكلاسات تتحكم بالمظهر العام ووتتحكم أيضا بإظهار وإخفاء أقسام المطوية عبر حركات CSS الإنتقالية. يمكنك تعديل أي من هذه عبر كلاسات CSS خاصة بك، او عبر تغيير القيم الإفتراضية المقدمة من Bootsrap. من الجدير بالذكر أنه يمكن وضع أي كود HTML هنا، ولكن الحركة الإنتقالية قد تحد من الoverflow. + هذا هو محتوى عنصر المطوية الأول. سيكون المحتوى مخفيًا بشكل إفتراضي حتى يقوم Bootstrap بإضافة الكلاسات اللازمة لكل عنصر في المطوية. هذه الكلاسات تتحكم بالمظهر العام ووتتحكم أيضا بإظهار وإخفاء أقسام المطوية عبر حركات CSS الإنتقالية. يمكنك تعديل أي من هذه عبر كلاسات CSS خاصة بك، او عبر تغيير القيم الإفتراضية المقدمة من Bootstrap. من الجدير بالذكر أنه يمكن وضع أي كود HTML هنا، ولكن الحركة الإنتقالية قد تحد من الoverflow.
-

+

-
+
- هذا هو محتوى عنصر المطوية الثاني. سيكون المحتوى مخفيًا بشكل إفتراضي حتى يقوم Bootstrap بإضافة الكلاسات اللازمة لكل عنصر في المطوية. هذه الكلاسات تتحكم بالمظهر العام ووتتحكم أيضا بإظهار وإخفاء أقسام المطوية عبر حركات CSS الإنتقالية. يمكنك تعديل أي من هذه عبر كلاسات CSS خاصة بك، او عبر تغيير القيم الإفتراضية المقدمة من Bootsrap. من الجدير بالذكر أنه يمكن وضع أي كود HTML هنا، ولكن الحركة الإنتقالية قد تحد من الoverflow. + هذا هو محتوى عنصر المطوية الثاني. سيكون المحتوى مخفيًا بشكل إفتراضي حتى يقوم Bootstrap بإضافة الكلاسات اللازمة لكل عنصر في المطوية. هذه الكلاسات تتحكم بالمظهر العام ووتتحكم أيضا بإظهار وإخفاء أقسام المطوية عبر حركات CSS الإنتقالية. يمكنك تعديل أي من هذه عبر كلاسات CSS خاصة بك، او عبر تغيير القيم الإفتراضية المقدمة من Bootstrap. من الجدير بالذكر أنه يمكن وضع أي كود HTML هنا، ولكن الحركة الإنتقالية قد تحد من الoverflow.
-

+

-
+
- هذا هو محتوى عنصر المطوية الثالث. سيكون المحتوى مخفيًا بشكل إفتراضي حتى يقوم Bootstrap بإضافة الكلاسات اللازمة لكل عنصر في المطوية. هذه الكلاسات تتحكم بالمظهر العام ووتتحكم أيضا بإظهار وإخفاء أقسام المطوية عبر حركات CSS الإنتقالية. يمكنك تعديل أي من هذه عبر كلاسات CSS خاصة بك، او عبر تغيير القيم الإفتراضية المقدمة من Bootsrap. من الجدير بالذكر أنه يمكن وضع أي كود HTML هنا، ولكن الحركة الإنتقالية قد تحد من الoverflow. + هذا هو محتوى عنصر المطوية الثالث. سيكون المحتوى مخفيًا بشكل إفتراضي حتى يقوم Bootstrap بإضافة الكلاسات اللازمة لكل عنصر في المطوية. هذه الكلاسات تتحكم بالمظهر العام ووتتحكم أيضا بإظهار وإخفاء أقسام المطوية عبر حركات CSS الإنتقالية. يمكنك تعديل أي من هذه عبر كلاسات CSS خاصة بك، او عبر تغيير القيم الإفتراضية المقدمة من Bootstrap. من الجدير بالذكر أنه يمكن وضع أي كود HTML هنا، ولكن الحركة الإنتقالية قد تحد من الoverflow.
- {{< /example >}} + `} />
- {{< example show_markup="false" >}} - {{< alerts.inline >}} - {{- range (index $.Site.Data "theme-colors") }} - + `)} /> - {{< example show_markup="false" >}} +

أحسنت!

لقد نجحت في قراءة رسالة التنبيه المهمة هذه. سيتم تشغيل نص المثال هذا لفترة أطول قليلاً حتى تتمكن من رؤية كيفية عمل التباعد داخل التنبيه مع هذا النوع من المحتوى.


كلما احتجت إلى ذلك ، تأكد من استخدام أدوات الهامش للحفاظ على الأشياء لطيفة ومرتبة.

- {{< /example >}} + `} />
- {{< example show_markup="false" >}} + مثال على عنوان جديد

مثال على عنوان جديد

مثال على عنوان جديد

مثال على عنوان جديد

-

مثال على عنوان جديد

-

مثال على عنوان جديد

-

مثال على عنوان جديد

+

مثال على عنوان جديد

+

مثال على عنوان جديد

+

مثال على عنوان جديد

مثال على عنوان جديد

- {{< /example >}} + `} /> - {{< example show_markup="false" >}} - {{< badge.inline >}} - {{- range (index $.Site.Data "theme-colors") }} - {{ .name | title }}{{- end -}} - {{< /badge.inline >}} - {{< /example >}} + ` + ${themeColor.title} + `)} />
- {{< example show_markup="false" >}} - {{< buttons.inline >}} - {{- range (index $.Site.Data "theme-colors") }} - - {{- end -}} - {{< /buttons.inline >}} - - - {{< /example >}} - - {{< example show_markup="false" >}} - {{< buttons.inline >}} - {{- range (index $.Site.Data "theme-colors") }} - - {{- end -}} - {{< /buttons.inline >}} - {{< /example >}} - - {{< example show_markup="false" >}} + ` + + `), + ``]} /> + + ` + + `)} /> + + زر صغير - {{< /example >}} + `} />
- {{< example show_markup="false" >}} +
- {{< placeholder width="100%" height="180" class="card-img-top" text="غطاء الصورة" >}} +
عنوان البطاقة

بعض الأمثلة السريعة للنصوص للبناء على عنوان البطاقة وتشكيل الجزء الأكبر من محتوى البطاقة.

@@ -808,7 +808,7 @@
عنوان البطاقة

بعض الأمثلة السريعة للنصوص للبناء على عنوان البطاقة وتشكيل الجزء الأكبر من محتوى البطاقة.

اذهب لمكان ما
- @@ -834,30 +834,30 @@
عنوان البطاقة
- {{< placeholder width="100%" height="250" text="صورة" >}} +
عنوان البطاقة

هذه بطاقة أعرض مع نص داعم تحتها كمقدمة طبيعية لمحتوى إضافي. هذا المحتوى أطول قليلاً.

-

آخر تحديث منذ 3 دقائق

+

آخر تحديث منذ 3 دقائق

- {{< /example >}} + `} />
- {{< example show_markup="false" >}} +
  • عنصر معطل
  • عنصر ثاني
  • @@ -1096,9 +1096,9 @@

    مجموعة العناصر

  • عنصر رابع
  • وعنصر خامس أيضًا
  • - {{< /example >}} + `} /> - {{< example show_markup="false" >}} +
  • عنصر
  • عنصر ثاني
  • @@ -1106,28 +1106,26 @@

    مجموعة العناصر

  • عنصر رابع
  • وعنصر خامس أيضًا
  • - {{< /example >}} + `} /> - {{< example show_markup="false" >}} + - عنصر مجموعة قائمة default بسيط - {{< list.inline >}} - {{- range (index $.Site.Data "theme-colors") }} - عنصر مجموعة قائمة {{ .name }} بسيط - {{- end -}} - {{< /list.inline >}} -
    - {{< /example >}} + عنصر مجموعة قائمة default بسيط`, + ...getData('theme-colors').map((themeColor) => ` + عنصر مجموعة قائمة ${themeColor.name} بسيط + `), + ` + `]} />
    - {{< example show_markup="false" >}} +
    • 1
    • @@ -1306,9 +1303,9 @@

      ترقيم الصفحات

    • 3
    - {{< /example >}} + `} /> - {{< example show_markup="false" >}} +
    • @@ -1326,13 +1323,13 @@

      ترقيم الصفحات

    - {{< /example >}} + `} /> - {{< example show_markup="false" >}} + - {{< /example >}} + `} />

    الصناديق المنبثقة

    - }}">دليل الإستخدام + دليل الإستخدام
    - {{< example show_markup="false" >}} + انقر لعرض/إخفاء الصندوق المنبثق - {{< /example >}} + `} /> - {{< example show_markup="false" >}} + انبثاق إلى الأعلى @@ -1373,160 +1370,156 @@

    الصناديق المنبثقة

    - {{< /example >}} + `} />
    - {{< example show_markup="false" >}} -
    -
    0%
    + +
    0%
    -
    -
    25%
    +
    +
    25%
    -
    -
    50%
    +
    +
    50%
    -
    -
    75%
    +
    +
    75%
    -
    -
    100%
    +
    +
    100%
    - {{< /example >}} + `} /> - {{< example show_markup="false" >}} -
    -
    -
    + +
    +
    +
    +
    +
    +
    - {{< /example >}} + `} />
    -
    - -
    -

    @fat

    -

    محتوى لتوضيح كيف تعمل المخطوطة. ببساطة، المخطوطة عبارة عن منشور طويل يحتوي على عدة أقسام، ولديه شريط تنقل يسهل الوصول إلى هذه الأقسام الفرعية.

    -

    @mdo

    -

    بصرف النظر عن تحسيننا جدوى المكيّفات أو عدم تحسينها، فإن الطلب على الطاقة سيزداد. وطبقاً لما جاء في مقالة معهد ماساشوستس للتكنولوجيا، السالف ذكره، ثمَّة أمر يجب عدم إغفاله، وهو كيف أن هذا الطلب سيضغط على نظم توفير الطاقة الحالية. إذ لا بد من إعادة تأهيل كل شبكات الكهرباء، وتوسيعها لتلبية طلب الطاقة في زمن الذروة، خلال موجات الحرارة المتزايدة. فحين يكون الحر شديداً يجنح الناس إلى البقاء في الداخل، وإلى زيادة تشغيل المكيّفات، سعياً إلى جو لطيف وهم يستخدمون أدوات وأجهزة مختلفة أخرى.

    -

    واحد

    -

    وكل هذه الأمور المتزامنة من تشغيل الأجهزة، يزيد الضغط على شبكات الطاقة، كما أسلفنا. لكن مجرد زيادة سعة الشبكة ليس كافياً. إذ لا بد من تطوير الشبكات الذكية التي تستخدم الجسّاسات، ونظم المراقبة، والبرامج الإلكترونية، لتحديد متى يكون الشاغلون في المبنى، ومتى يكون ثمَّة حاجة إلى الطاقة، ومتى تكون الحرارة منخفضة، وبذلك يخرج الناس، فلا يستخدمون كثيراً من الكهرباء.

    -

    اثنان

    -

    مع الأسف، كل هذه الحلول المبتكرة مكلِّفة، وهذا ما يجعلها عديمة الجدوى في نظر بعض الشركات الخاصة والمواطن المتقشّف. إن بعض الأفراد الواعين بيئياً يبذلون قصارى جهدهم في تقليص استهلاكهم من الطاقة، ويعون جيداً أهمية أجهزة التكييف المجدية والأرفق بالبيئة. ولكن جهات كثيرة لن تتحرّك لمجرد حافز سلامة المناخ ووقف هدر الطاقة، ما دامت لا تحركها حوافز قانونية. وعلى الحكومات أن تُقدِم عند الاهتمام بالتغيّر المناخي، على وضع التشريعات المناسبة. فبالنظم والحوافز والدعم، يمكن دفع الشركات إلى اعتماد الحلول الأجدى في مكاتبها.

    -

    ثلاثة

    -

    وكما يتبيّن لنا، من عدد الحلول الملطِّفة للمشكلة، ومن تنوّعها، وهي الحلول التي أسلفنا الحديث عنها، فإن التكنولوجيا التي نحتاج إليها من أجل معالجة هذه التحديات، هي في مدى قدرتنا، لكنها ربما تتطلّب بعض التحسين، ودعماً استثمارياً أكبر!

    -

    ولا مانع من إضافة محتوى آخر ليس تحت أي قسم معين.

    -
    + + شريط التنقل + + +
    +

    @fat

    +

    محتوى لتوضيح كيف تعمل المخطوطة. ببساطة، المخطوطة عبارة عن منشور طويل يحتوي على عدة أقسام، ولديه شريط تنقل يسهل الوصول إلى هذه الأقسام الفرعية.

    +

    @mdo

    +

    بصرف النظر عن تحسيننا جدوى المكيّفات أو عدم تحسينها، فإن الطلب على الطاقة سيزداد. وطبقاً لما جاء في مقالة معهد ماساشوستس للتكنولوجيا، السالف ذكره، ثمَّة أمر يجب عدم إغفاله، وهو كيف أن هذا الطلب سيضغط على نظم توفير الطاقة الحالية. إذ لا بد من إعادة تأهيل كل شبكات الكهرباء، وتوسيعها لتلبية طلب الطاقة في زمن الذروة، خلال موجات الحرارة المتزايدة. فحين يكون الحر شديداً يجنح الناس إلى البقاء في الداخل، وإلى زيادة تشغيل المكيّفات، سعياً إلى جو لطيف وهم يستخدمون أدوات وأجهزة مختلفة أخرى.

    +

    واحد

    +

    وكل هذه الأمور المتزامنة من تشغيل الأجهزة، يزيد الضغط على شبكات الطاقة، كما أسلفنا. لكن مجرد زيادة سعة الشبكة ليس كافياً. إذ لا بد من تطوير الشبكات الذكية التي تستخدم الجسّاسات، ونظم المراقبة، والبرامج الإلكترونية، لتحديد متى يكون الشاغلون في المبنى، ومتى يكون ثمَّة حاجة إلى الطاقة، ومتى تكون الحرارة منخفضة، وبذلك يخرج الناس، فلا يستخدمون كثيراً من الكهرباء.

    +

    اثنان

    +

    مع الأسف، كل هذه الحلول المبتكرة مكلِّفة، وهذا ما يجعلها عديمة الجدوى في نظر بعض الشركات الخاصة والمواطن المتقشّف. إن بعض الأفراد الواعين بيئياً يبذلون قصارى جهدهم في تقليص استهلاكهم من الطاقة، ويعون جيداً أهمية أجهزة التكييف المجدية والأرفق بالبيئة. ولكن جهات كثيرة لن تتحرّك لمجرد حافز سلامة المناخ ووقف هدر الطاقة، ما دامت لا تحركها حوافز قانونية. وعلى الحكومات أن تُقدِم عند الاهتمام بالتغيّر المناخي، على وضع التشريعات المناسبة. فبالنظم والحوافز والدعم، يمكن دفع الشركات إلى اعتماد الحلول الأجدى في مكاتبها.

    +

    ثلاثة

    +

    وكما يتبيّن لنا، من عدد الحلول الملطِّفة للمشكلة، ومن تنوّعها، وهي الحلول التي أسلفنا الحديث عنها، فإن التكنولوجيا التي نحتاج إليها من أجل معالجة هذه التحديات، هي في مدى قدرتنا، لكنها ربما تتطلّب بعض التحسين، ودعماً استثمارياً أكبر!

    +

    ولا مانع من إضافة محتوى آخر ليس تحت أي قسم معين.

    + `} />

    الدوائر المتحركة

    - }}">دليل الإستخدام + دليل الإستخدام
    - {{< example show_markup="false" >}} - {{< spinner.inline >}} - {{- range (index $.Site.Data "theme-colors") }} -
    + ` +
    جار التحميل...
    - {{- end -}} - {{< /spinner.inline >}} - {{< /example >}} - - {{< example show_markup="false" >}} - {{< spinner.inline >}} - {{- range (index $.Site.Data "theme-colors") }} -
    + `)} /> + + ` +
    جار التحميل...
    - {{- end -}} - {{< /spinner.inline >}} - {{< /example >}} + `)} />
    - {{< example show_markup="false" class="bg-dark p-5 align-items-center" >}} +
    - {{< placeholder width="20" height="20" background="#007aff" class="rounded me-2" text="false" title="false" >}} + Bootstrap - قبل 11 دقيقة + قبل 11 دقيقة
    - مرحبا بالعالم! هذه رسالة إشعار. + مرحبًا بالعالم! هذه رسالة إشعار.
    - {{< /example >}} + `} />
    - {{< example show_markup="false" class="tooltip-demo" >}} + تلميح يظهر في الأعلى - {{< /example >}} + `} />
    - + @@ -320,11 +320,11 @@ Link
  • Dropdown item text
  • +
  • Action
  • +
  • Another action
  • +
  • Something else here
  • + `} /> + +### Active + +Add `.active` to items in the dropdown to **style them as active**. To convey the active state to assistive technologies, use the `aria-current` attribute — using the `page` value for the current page, or `true` for the current item in a set. + + +
  • Regular link
  • +
  • Active link
  • +
  • Another link
  • + `} /> + +### Disabled + +Add `.disabled` to items in the dropdown to **style them as disabled**. + + +
  • Regular link
  • +
  • Disabled link
  • +
  • Another link
  • + `} /> + +## Menu alignment + +By default, a dropdown menu is automatically positioned 100% from the top and along the left side of its parent. You can change this with the directional `.drop*` classes, but you can also control them with additional modifier classes. + +Add `.dropdown-menu-end` to a `.dropdown-menu` to right align the dropdown menu. Directions are mirrored when using Bootstrap in RTL, meaning `.dropdown-menu-end` will appear on the left side. + + +**Heads up!** Dropdowns are positioned thanks to Popper except when they are contained in a navbar. + + + + + + `} /> + +### Responsive alignment + +If you want to use responsive alignment, disable dynamic positioning by adding the `data-bs-display="static"` attribute and use the responsive variation classes. + +To align **right** the dropdown menu with the given breakpoint or larger, add `.dropdown-menu{-sm|-md|-lg|-xl|-xxl}-end`. + + + + + `} /> + +To align **left** the dropdown menu with the given breakpoint or larger, add `.dropdown-menu-end` and `.dropdown-menu{-sm|-md|-lg|-xl|-xxl}-start`. + + + + + `} /> + +Note that you don’t need to add a `data-bs-display="static"` attribute to dropdown buttons in navbars, since Popper isn’t used in navbars. + +### Alignment options + +Taking most of the options shown above, here’s a small kitchen sink demo of various dropdown alignment options in one place. + + + + + + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    `} /> + +## Menu content + +### Headers + +Add a header to label sections of actions in any dropdown menu. + + +
  • +
  • Action
  • +
  • Another action
  • + `} /> + +### Dividers + +Separate groups of related menu items with a divider. + + +
  • Action
  • +
  • Another action
  • +
  • Something else here
  • +
  • +
  • Separated link
  • + `} /> + +### Text + +Place any freeform text within a dropdown menu with text and use [spacing utilities]([[docsref:/utilities/spacing]]). Note that you’ll likely need additional sizing styles to constrain the menu width. + + +

    + Some example text that’s free-flowing within the dropdown menu. +

    +

    + And this is more example text. +

    + `} /> + +### Forms + +Put a form within a dropdown menu, or make it into a dropdown menu, and use [margin or padding utilities]([[docsref:/utilities/spacing]]) to give it the negative space you require. + + +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    + +
    + + New around here? Sign up + Forgot password? + `} /> + + + + + `} /> + +## Dropdown options + +Use `data-bs-offset` or `data-bs-reference` to change the location of the dropdown. + + + +
    + + + +
    + `} /> + +### Auto close behavior + +By default, the dropdown menu is closed when clicking inside or outside the dropdown menu. You can use the `autoClose` option to change this behavior of the dropdown. + + + + + + +
    + + +
    + +
    + + +
    + +
    + + +
    `} /> + +## CSS + +### Variables + + + +As part of Bootstrap’s evolving CSS variables approach, dropdowns now use local CSS variables on `.dropdown-menu` for enhanced real-time customization. Values for the CSS variables are set via Sass, so Sass customization is still supported, too. + + + + +Dropdown items include at least one variable that is not set on `.dropdown`. This allows you to provide a new value while Bootstrap defaults to a fallback value. + +- `--bs-dropdown-item-border-radius` + + +Customization through CSS variables can be seen on the `.dropdown-menu-dark` class where we override specific values without adding duplicate CSS selectors. + + + +### Sass variables + +Variables for all dropdowns: + + + +Variables for the [dark dropdown](#dark-dropdowns): + + + +Variables for the CSS-based carets that indicate a dropdown’s interactivity: + + + +### Sass mixins + +Mixins are used to generate the CSS-based carets and can be found in `scss/mixins/_caret.scss`. + + + +## Usage + +Via data attributes or JavaScript, the dropdown plugin toggles hidden content (dropdown menus) by toggling the `.show` class on the parent `.dropdown-menu`. The `data-bs-toggle="dropdown"` attribute is relied on for closing dropdown menus at an application level, so it’s a good idea to always use it. + + +On touch-enabled devices, opening a dropdown adds empty `mouseover` handlers to the immediate children of the `` element. This admittedly ugly hack is necessary to work around a [quirk in iOs’ event delegation](https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html), which would otherwise prevent a tap anywhere outside of the dropdown from triggering the code that closes the dropdown. Once the dropdown is closed, these additional empty `mouseover` handlers are removed. + + +### Via data attributes + +Add `data-bs-toggle="dropdown"` to a link or button to toggle a dropdown. + +```html + +``` + +### Via JavaScript + + +Dropdowns must have `data-bs-toggle="dropdown"` on their trigger element, regardless of whether you call your dropdown via JavaScript or use the data-api. + + +Call the dropdowns via JavaScript: + +```js +const dropdownElementList = document.querySelectorAll('.dropdown-toggle') +const dropdownList = [...dropdownElementList].map(dropdownToggleEl => new bootstrap.Dropdown(dropdownToggleEl)) +``` + +### Options + + + + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `autoClose` | boolean, string | `true` | Configure the auto close behavior of the dropdown:
    • `true` - the dropdown will be closed by clicking outside or inside the dropdown menu.
    • `false` - the dropdown will be closed by clicking the toggle button and manually calling `hide` or `toggle` method. (Also will not be closed by pressing Esc key)
    • `'inside'` - the dropdown will be closed (only) by clicking inside the dropdown menu.
    • `'outside'` - the dropdown will be closed (only) by clicking outside the dropdown menu.
    Note: the dropdown can always be closed with the Esc key. | +| `boundary` | string, element | `'clippingParents'` | Overflow constraint boundary of the dropdown menu (applies only to Popper’s preventOverflow modifier). By default it’s `clippingParents` and can accept an HTMLElement reference (via JavaScript only). For more information refer to Popper’s [detectOverflow docs](https://popper.js.org/docs/v2/utils/detect-overflow/#boundary). | +| `display` | string | `'dynamic'` | By default, we use Popper for dynamic positioning. Disable this with `static`. | +| `offset` | array, string, function | `[0, 2]` | Offset of the dropdown relative to its target. You can pass a string in data attributes with comma separated values like: `data-bs-offset="10,20"`. When a function is used to determine the offset, it is called with an object containing the popper placement, the reference, and popper rects as its first argument. The triggering element DOM node is passed as the second argument. The function must return an array with two numbers: [skidding](https://popper.js.org/docs/v2/modifiers/offset/#skidding-1), [distance](https://popper.js.org/docs/v2/modifiers/offset/#distance-1). For more information refer to Popper’s [offset docs](https://popper.js.org/docs/v2/modifiers/offset/#options). | +| `popperConfig` | null, object, function | `null` | To change Bootstrap’s default Popper config, see [Popper’s configuration](https://popper.js.org/docs/v2/constructors/#options). When a function is used to create the Popper configuration, it’s called with an object that contains the Bootstrap’s default Popper configuration. It helps you use and merge the default with your own configuration. The function must return a configuration object for Popper. | +| `reference` | string, element, object | `'toggle'` | Reference element of the dropdown menu. Accepts the values of `'toggle'`, `'parent'`, an HTMLElement reference or an object providing `getBoundingClientRect`. For more information refer to Popper’s [constructor docs](https://popper.js.org/docs/v2/constructors/#createpopper) and [virtual element docs](https://popper.js.org/docs/v2/virtual-elements/). | +
    + +#### Using function with `popperConfig` + +```js +const dropdown = new bootstrap.Dropdown(element, { + popperConfig(defaultBsPopperConfig) { + // const newPopperConfig = {...} + // use defaultBsPopperConfig if needed... + // return newPopperConfig + } +}) +``` + +### Methods + + +| Method | Description | +| --- | --- | +| `dispose` | Destroys an element’s dropdown. (Removes stored data on the DOM element) | +| `getInstance` | Static method which allows you to get the dropdown instance associated to a DOM element, you can use it like this: `bootstrap.Dropdown.getInstance(element)`. | +| `getOrCreateInstance` | Static method which returns a dropdown instance associated to a DOM element or create a new one in case it wasn’t initialized. You can use it like this: `bootstrap.Dropdown.getOrCreateInstance(element)`. | +| `hide` | Hides the dropdown menu of a given navbar or tabbed navigation. | +| `show` | Shows the dropdown menu of a given navbar or tabbed navigation. | +| `toggle` | Toggles the dropdown menu of a given navbar or tabbed navigation. | +| `update` | Updates the position of an element’s dropdown. | + + +### Events + +All dropdown events are fired at the toggling element and then bubbled up. So you can also add event listeners on the `.dropdown-menu`’s parent element. `hide.bs.dropdown` and `hidden.bs.dropdown` events have a `clickEvent` property (only when the original Event type is `click`) that contains an Event Object for the click event. + + +| Event type | Description | +| --- | --- | +| `hide.bs.dropdown` | Fires immediately when the `hide` instance method has been called. | +| `hidden.bs.dropdown` | Fired when the dropdown has finished being hidden from the user and CSS transitions have completed. | +| `show.bs.dropdown` | Fires immediately when the `show` instance method is called. | +| `shown.bs.dropdown` | Fired when the dropdown has been made visible to the user and CSS transitions have completed. | + + +```js +const myDropdown = document.getElementById('myDropdown') +myDropdown.addEventListener('show.bs.dropdown', event => { + // do something... +}) +``` diff --git a/site/src/content/docs/components/list-group.mdx b/site/src/content/docs/components/list-group.mdx new file mode 100644 index 000000000000..59827ddd41bb --- /dev/null +++ b/site/src/content/docs/components/list-group.mdx @@ -0,0 +1,448 @@ +--- +title: List group +description: List groups are a flexible and powerful component for displaying a series of content. Modify and extend them to support just about any content within. +toc: true +--- + +import { getData } from '@libs/data' + +## Basic example + +The most basic list group is an unordered list with list items and the proper classes. Build upon it with the options that follow, or with your own CSS as needed. + + +
  • An item
  • +
  • A second item
  • +
  • A third item
  • +
  • A fourth item
  • +
  • And a fifth one
  • + `} /> + +## Active items + +Add `.active` to a `.list-group-item` to indicate the current active selection. + + +
  • An active item
  • +
  • A second item
  • +
  • A third item
  • +
  • A fourth item
  • +
  • And a fifth one
  • + `} /> + +## Links and buttons + +Use ``s or ` + + + + + `} /> + +## Flush + +Add `.list-group-flush` to remove some borders and rounded corners to render list group items edge-to-edge in a parent container (e.g., cards). + + +
  • An item
  • +
  • A second item
  • +
  • A third item
  • +
  • A fourth item
  • +
  • And a fifth one
  • + `} /> + +## Numbered + +Add the `.list-group-numbered` modifier class (and optionally use an `
      ` element) to opt into numbered list group items. Numbers are generated via CSS (as opposed to a `
        `s default browser styling) for better placement inside list group items and to allow for better customization. + +Numbers are generated by `counter-reset` on the `
          `, and then styled and placed with a `::before` pseudo-element on the `
        1. ` with `counter-increment` and `content`. + + +
        2. A list item
        3. +
        4. A list item
        5. +
        6. A list item
        7. +
        `} /> + +These work great with custom content as well. + + +
      1. +
        +
        Subheading
        + Content for list item +
        + 14 +
      2. +
      3. +
        +
        Subheading
        + Content for list item +
        + 14 +
      4. +
      5. +
        +
        Subheading
        + Content for list item +
        + 14 +
      6. +
      `} /> + +## Horizontal + +Add `.list-group-horizontal` to change the layout of list group items from vertical to horizontal across all breakpoints. Alternatively, choose a responsive variant `.list-group-horizontal-{sm|md|lg|xl|xxl}` to make a list group horizontal starting at that breakpoint’s `min-width`. Currently **horizontal list groups cannot be combined with flush list groups.** + +**ProTip:** Want equal-width list group items when horizontal? Add `.flex-fill` to each list group item. + + `
        +
      • An item
      • +
      • A second item
      • +
      • A third item
      • +
      `)} /> + +## Variants + + +**Heads up!** As of v5.3.0, the `list-group-item-variant()` Sass mixin is deprecated. List group item variants now have their CSS variables overridden in [a Sass loop](#sass-loops). + + +Use contextual classes to style list items with a stateful background and color. + + +
    1. A simple default list group item
    2. + `, + ...getData('theme-colors').map((themeColor) => `
    3. A simple ${themeColor.name} list group item
    4. `), + `` + ]} /> + +### For links and buttons + +Contextual classes also work with `.list-group-item-action` for `
      ` and ` -
      - -
      +Launch demo modal +`} /> ```html @@ -117,7 +115,7 @@ Toggle a working modal demo by clicking the button below. It will slide down and + +
      + + +
      + +
      + + + +
      + +
      + + + +
      `} /> + +## Buttons with dropdowns + + + + + + + + + + `} /> + +## Segmented buttons + + + + + + + + +
      + + + + +
      `} /> + +## Custom forms + +Input groups include support for custom selects and custom file inputs. Browser default versions of these are not supported. + +### Custom select + + + + + + +
      + + +
      + +
      + + +
      + +
      + + +
      `} /> + +### Custom file input + + + + + + +
      + + +
      + +
      + + +
      + +
      + + +
      `} /> + +## CSS + +### Sass variables + + diff --git a/site/src/content/docs/forms/layout.mdx b/site/src/content/docs/forms/layout.mdx new file mode 100644 index 000000000000..04d3bd5a8b58 --- /dev/null +++ b/site/src/content/docs/forms/layout.mdx @@ -0,0 +1,307 @@ +--- +title: Layout +description: Give your forms some structure—from inline to horizontal to custom grid implementations—with our form layout options. +toc: true +--- + +## Forms + +Every group of form fields should reside in a `
      ` element. Bootstrap provides no default styling for the `` element, but there are some powerful browser features that are provided by default. + +- New to browser forms? Consider reviewing [the MDN form docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) for an overview and complete list of available attributes. +- ` + +
      `} /> + +## Horizontal form + +Create horizontal forms with the grid by adding the `.row` class to form groups and using the `.col-*-*` classes to specify the width of your labels and controls. Be sure to add `.col-form-label` to your `